fix stats

they're running now, but need to be sanity checked to make sure they're doing the right thing
This commit is contained in:
Damien Elmes 2011-03-24 10:44:13 +09:00
parent 63d1448d1e
commit 942bf43b52
4 changed files with 82 additions and 45 deletions

View file

@ -617,6 +617,17 @@ select conf from gconf where id = (select gcid from groups where id = ?)""",
import anki.find import anki.find
return anki.find.findDuplicates(self, fmids) return anki.find.findDuplicates(self, fmids)
# Stats
##########################################################################
def cardStats(self, card):
from anki.stats import CardStats
return CardStats(self, card).report()
def deckStats(self):
from anki.stats import DeckStats
return DeckStats(self).report()
# Timeboxing # Timeboxing
########################################################################## ##########################################################################

View file

@ -4,6 +4,7 @@
import time, sys, os, datetime import time, sys, os, datetime
import anki, anki.utils import anki, anki.utils
from anki.consts import *
from anki.lang import _, ngettext from anki.lang import _, ngettext
from anki.hooks import runFilter from anki.hooks import runFilter
@ -21,12 +22,12 @@ class CardStats(object):
fmt = anki.utils.fmtTimeSpan fmt = anki.utils.fmtTimeSpan
fmtFloat = anki.utils.fmtFloat fmtFloat = anki.utils.fmtFloat
self.txt = "<table>" self.txt = "<table>"
self.addLine(_("Added"), self.strTime(c.created)) self.addLine(_("Added"), self.strTime(c.crt))
first = self.deck.db.scalar( first = self.deck.db.scalar(
"select time/1000 from revlog where rep = 1 and cardId = :id", id=c.id) "select time/1000 from revlog where rep = 1 and cid = :id", id=c.id)
if first: if first:
self.addLine(_("First Review"), self.strTime(first)) self.addLine(_("First Review"), self.strTime(first))
self.addLine(_("Changed"), self.strTime(c.modified)) self.addLine(_("Changed"), self.strTime(c.mod))
if c.reps: if c.reps:
next = time.time() - c.due next = time.time() - c.due
if next > 0: if next > 0:
@ -34,18 +35,18 @@ class CardStats(object):
else: else:
next = _("in %s") % fmt(abs(next)) next = _("in %s") % fmt(abs(next))
self.addLine(_("Due"), next) self.addLine(_("Due"), next)
self.addLine(_("Interval"), fmt(c.interval * 86400)) self.addLine(_("Interval"), fmt(c.ivl * 86400))
self.addLine(_("Ease"), fmtFloat(c.factor, point=2)) self.addLine(_("Ease"), fmtFloat(c.factor, point=2))
if c.reps: if c.reps:
self.addLine(_("Reviews"), "%d/%d (s=%d)" % ( self.addLine(_("Reviews"), "%d/%d (s=%d)" % (
c.reps-c.lapses, c.reps, c.successive)) c.reps-c.lapses, c.reps, c.streak))
(cnt, total) = self.deck.db.first( (cnt, total) = self.deck.db.first(
"select count(), sum(userTime)/1000 from revlog where cardId = :id", id=c.id) "select count(), sum(taken)/1000 from revlog where cid = :id", id=c.id)
if cnt: if cnt:
self.addLine(_("Average Time"), fmt(total / float(cnt), point=2)) self.addLine(_("Average Time"), fmt(total / float(cnt), point=2))
self.addLine(_("Total Time"), fmt(total, point=2)) self.addLine(_("Total Time"), fmt(total, point=2))
self.addLine(_("Model Tags"), c.fact.model.tags) self.addLine(_("Model"), c.model().name)
self.addLine(_("Card Template") + "&nbsp;"*5, c.cardModel.name) self.addLine(_("Template") + "&nbsp;"*5, c.template()['name'])
self.txt += "</table>" self.txt += "</table>"
return self.txt return self.txt
@ -66,32 +67,32 @@ class DeckStats(object):
def matureCardCount(self): def matureCardCount(self):
return self.deck.db.scalar( return self.deck.db.scalar(
"select count(id) from cards where interval >= :t ", "select count(id) from cards where ivl >= :t ",
t=MATURE_THRESHOLD) t=MATURE_THRESHOLD)
def youngCardCount(self): def youngCardCount(self):
return self.deck.db.scalar( return self.deck.db.scalar(
"select count(id) from cards where interval < :t " "select count(id) from cards where ivl < :t "
"and reps != 0", t=MATURE_THRESHOLD) "and reps != 0", t=MATURE_THRESHOLD)
def newCountAll(self): def newCountAll(self):
"All new cards, including spaced." "All new cards, including spaced."
return self.deck.db.scalar( return self.deck.db.scalar(
"select count(id) from cards where type = 2") "select count(id) from cards where type = 0")
def report(self): def report(self):
"Return an HTML string with a report." "Return an HTML string with a report."
fmtPerc = anki.utils.fmtPercentage fmtPerc = anki.utils.fmtPercentage
fmtFloat = anki.utils.fmtFloat fmtFloat = anki.utils.fmtFloat
if self.deck.isEmpty(): if not self.deck.cardCount():
return _("Please add some cards first.") + "<p/>" return _("Please add some cards first.") + "<p/>"
d = self.deck d = self.deck
html="<h1>" + _("Deck Statistics") + "</h1>" html="<h1>" + _("Deck Statistics") + "</h1>"
html += _("Deck created: <b>%s</b> ago<br>") % self.createdTimeStr() html += _("Deck created: <b>%s</b> ago<br>") % self.crtTimeStr()
total = d.cardCount() total = d.cardCount()
new = d.newCountAll() new = self.newCountAll()
young = d.youngCardCount() young = self.youngCardCount()
old = d.matureCardCount() old = self.matureCardCount()
newP = new / float(total) * 100 newP = new / float(total) * 100
youngP = young / float(total) * 100 youngP = young / float(total) * 100
oldP = old / float(total) * 100 oldP = old / float(total) * 100
@ -109,7 +110,7 @@ class DeckStats(object):
'young': stats['young'], 'youngP' : fmtPerc(stats['youngP'])} 'young': stats['young'], 'youngP' : fmtPerc(stats['youngP'])}
html += _("Unseen cards:") + " <b>%(new)d</b> (%(newP)s)<br>" % { html += _("Unseen cards:") + " <b>%(new)d</b> (%(newP)s)<br>" % {
'new': stats['new'], 'newP' : fmtPerc(stats['newP'])} 'new': stats['new'], 'newP' : fmtPerc(stats['newP'])}
avgInt = self.getAverageInterval() avgInt = self.getAverageIvl()
if avgInt: if avgInt:
html += _("Average interval: ") + ("<b>%s</b> ") % fmtFloat(avgInt) + _("days") html += _("Average interval: ") + ("<b>%s</b> ") % fmtFloat(avgInt) + _("days")
html += "<br>" html += "<br>"
@ -131,7 +132,7 @@ class DeckStats(object):
'partOf' : nYes, 'partOf' : nYes,
'totalSum' : nAll } + "<br><br>") 'totalSum' : nAll } + "<br><br>")
# average pending time # average pending time
existing = d.cardCount() - d.newCount existing = d.cardCount() - self.newCountAll()
def tr(a, b): def tr(a, b):
return "<tr><td>%s</td><td align=right>%s</td></tr>" % (a, b) return "<tr><td>%s</td><td align=right>%s</td></tr>" % (a, b)
def repsPerDay(reps,days): def repsPerDay(reps,days):
@ -170,7 +171,7 @@ class DeckStats(object):
else: else:
html += "<table width=200>" html += "<table width=200>"
html += tr(_("Deck life"), ("<b>%s</b> ") % ( html += tr(_("Deck life"), ("<b>%s</b> ") % (
fmtFloat(self.getSumInverseRoundInterval())) + _("cards/day")) fmtFloat(self.getSumInverseRoundIvl())) + _("cards/day"))
html += tr(_("In next week"), ("<b>%s</b> ") % ( html += tr(_("In next week"), ("<b>%s</b> ") % (
fmtFloat(self.getWorkloadPeriod(7))) + _("cards/day")) fmtFloat(self.getWorkloadPeriod(7))) + _("cards/day"))
html += tr(_("In next month"), ("<b>%s</b> ") % ( html += tr(_("In next month"), ("<b>%s</b> ") % (
@ -234,37 +235,39 @@ class DeckStats(object):
html += "</table>" html += "</table>"
html += "<br><br><b>" + _("Card Ease") + "</b><br>" html += "<br><br><b>" + _("Card Ease") + "</b><br>"
html += _("Lowest factor: %.2f") % d.s.scalar( html += _("Lowest factor: %.2f") % d.db.scalar(
"select min(factor) from cards") + "<br>" "select min(factor)/1000.0 from cards") + "<br>"
html += _("Average factor: %.2f") % d.s.scalar( html += _("Average factor: %.2f") % d.db.scalar(
"select avg(factor) from cards") + "<br>" "select avg(factor)/1000.0 from cards") + "<br>"
html += _("Highest factor: %.2f") % d.s.scalar( html += _("Highest factor: %.2f") % d.db.scalar(
"select max(factor) from cards") + "<br>" "select max(factor)/1000.0 from cards") + "<br>"
html = runFilter("deckStats", html) html = runFilter("deckStats", html)
return html return html
def getMatureCorrect(self, test=None): def getMatureCorrect(self, test=None):
if not test: if not test:
test = "lastInterval > 21" test = "lastIvl > 21"
head = "select count() from revlog where %s" head = "select count() from revlog where %s"
all = self.deck.db.scalar(head % test) all = self.deck.db.scalar(head % test)
yes = self.deck.db.scalar((head % test) + " and ease > 1") yes = self.deck.db.scalar((head % test) + " and ease > 1")
if not all:
return (0, 0, 0)
return (all, yes, yes/float(all)*100) return (all, yes, yes/float(all)*100)
def getYoungCorrect(self): def getYoungCorrect(self):
return self.getMatureCorrect("lastInterval <= 21 and rep != 1") return self.getMatureCorrect("lastIvl <= 21 and rep != 1")
def getNewCorrect(self): def getNewCorrect(self):
return self.getMatureCorrect("rep = 1") return self.getMatureCorrect("rep = 1")
def getDaysReviewed(self, start, finish): def getDaysReviewed(self, start, finish):
today = self.deck.failedCutoff today = self.deck.sched.dayCutoff
x = today + 86400*start x = today + 86400*start
y = today + 86400*finish y = today + 86400*finish
return self.deck.db.scalar(""" return self.deck.db.scalar("""
select count(distinct(cast((time/1000-:off)/86400 as integer))) from revlog select count(distinct(cast((time/1000-:off)/86400 as integer))) from revlog
where time >= :x*1000 and time <= :y*1000""",x=x,y=y, off=self.deck.utcOffset) where time >= :x*1000 and time <= :y*1000""",x=x,y=y, off=self.deck.crt)
def getRepsDone(self, start, finish): def getRepsDone(self, start, finish):
now = datetime.datetime.today() now = datetime.datetime.today()
@ -274,13 +277,13 @@ where time >= :x*1000 and time <= :y*1000""",x=x,y=y, off=self.deck.utcOffset)
"select count() from revlog where time >= :x*1000 and time <= :y*1000", "select count() from revlog where time >= :x*1000 and time <= :y*1000",
x=x, y=y) x=x, y=y)
def getAverageInterval(self): def getAverageIvl(self):
return self.deck.db.scalar( return self.deck.db.scalar(
"select sum(interval) / count(interval) from cards " "select sum(ivl) / count(ivl) from cards "
"where cards.reps > 0") or 0 "where cards.reps > 0") or 0
def intervalReport(self, intervals, labels, total): def ivlReport(self, ivls, labels, total):
boxes = self.splitIntoIntervals(intervals) boxes = self.splitIntoIvls(ivls)
keys = boxes.keys() keys = boxes.keys()
keys.sort() keys.sort()
html = "" html = ""
@ -292,13 +295,13 @@ where time >= :x*1000 and time <= :y*1000""",x=x,y=y, off=self.deck.utcOffset)
fmtPerc(boxes[key] / float(total) * 100)) fmtPerc(boxes[key] / float(total) * 100))
return html return html
def splitIntoIntervals(self, intervals): def splitIntoIvls(self, ivls):
boxes = {} boxes = {}
n = 0 n = 0
for i in range(len(intervals) - 1): for i in range(len(ivls) - 1):
(min, max) = (intervals[i], intervals[i+1]) (min, max) = (ivls[i], ivls[i+1])
for c in self.deck: for c in self.deck:
if c.interval > min and c.interval <= max: if c.ivl > min and c.ivl <= max:
boxes[n] = boxes.get(n, 0) + 1 boxes[n] = boxes.get(n, 0) + 1
n += 1 n += 1
return boxes return boxes
@ -307,15 +310,15 @@ where time >= :x*1000 and time <= :y*1000""",x=x,y=y, off=self.deck.utcOffset)
"Average number of new cards added each day." "Average number of new cards added each day."
return self.deck.cardCount() / max(1, self.ageInDays()) return self.deck.cardCount() / max(1, self.ageInDays())
def createdTimeStr(self): def crtTimeStr(self):
return anki.utils.fmtTimeSpan(time.time() - self.deck.created) return anki.utils.fmtTimeSpan(time.time() - self.deck.crt)
def ageInDays(self): def ageInDays(self):
return (time.time() - self.deck.created) / 86400.0 return (time.time() - self.deck.crt) / 86400.0
def getSumInverseRoundInterval(self): def getSumInverseRoundIvl(self):
return self.deck.db.scalar( return self.deck.db.scalar(
"select sum(1/round(max(interval, 1)+0.5)) from cards " "select sum(1/round(max(ivl, 1)+0.5)) from cards "
"where cards.reps > 0 " "where cards.reps > 0 "
"and queue != -1") or 0 "and queue != -1") or 0
@ -336,7 +339,7 @@ where time > :cutoff*1000""", cutoff=cutoff) or 0) / float(period)
cutoff = time.time() - 86400 * period cutoff = time.time() - 86400 * period
return (self.deck.db.scalar(""" return (self.deck.db.scalar("""
select count(id) from cards select count(id) from cards
where created > :cutoff""", cutoff=cutoff) or 0) where crt > :cutoff""", cutoff=cutoff) or 0)
def getFirstPeriod(self, period): def getFirstPeriod(self, period):
cutoff = time.time() - 86400 * period cutoff = time.time() - 86400 * period

View file

@ -138,8 +138,8 @@ create table if not exists revlog (
cid integer not null, cid integer not null,
ease integer not null, ease integer not null,
rep integer not null, rep integer not null,
int integer not null, ivl integer not null,
lastInt integer not null, lastIvl integer not null,
factor integer not null, factor integer not null,
taken integer not null, taken integer not null,
type integer not null type integer not null

23
tests/test_stats.py Normal file
View file

@ -0,0 +1,23 @@
# coding: utf-8
import time, copy
from tests.shared import assertException, getEmptyDeck
from anki.stdmodels import BasicModel
from anki.utils import stripHTML, intTime
from anki.hooks import addHook
def test_stats():
d = getEmptyDeck()
f = d.newFact()
f['Front'] = "foo"
d.addFact(f)
c = f.cards()[0]
# card stats
assert d.cardStats(c)
d.reset()
c = d.sched.getCard()
d.sched.answerCard(c, 3)
d.sched.answerCard(c, 2)
assert d.cardStats(c)
# deck stats
assert d.deckStats()