mirror of
https://github.com/ankitects/anki.git
synced 2025-11-13 08:07:11 -05:00
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:
parent
63d1448d1e
commit
942bf43b52
4 changed files with 82 additions and 45 deletions
11
anki/deck.py
11
anki/deck.py
|
|
@ -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
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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") + " "*5, c.cardModel.name)
|
self.addLine(_("Template") + " "*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
|
||||||
|
|
|
||||||
|
|
@ -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
23
tests/test_stats.py
Normal 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()
|
||||||
Loading…
Reference in a new issue