-
-
Save kevinmungai/bc3dfe9b212679e3e88ad4c4db7da35d to your computer and use it in GitHub Desktop.
my current understanding of Anki's spacing algorithm
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
This is my understanding of the Anki scheduling algorithm, which I mostly | |
got from watching https://www.youtube.com/watch?v=lz60qTP2Gx0 | |
and https://www.youtube.com/watch?v=1XaJjbCSXT0 | |
and from reading | |
https://faqs.ankiweb.net/what-spaced-repetition-algorithm.html | |
There is also https://github.com/dae/anki/blob/master/anki/sched.py but I find | |
it really hard to understand. | |
Things I don't bother to implement here: the random fudge factor (that Anki | |
uses to decorrelate cards that were added on the same day and have the same | |
responses throughout their history), leech tracking, checking if a card from | |
the same notes has been reviewed already that day, delay in response (i.e. I | |
assume all cards are reviewed exactly on the day they are due). | |
""" | |
# "New Cards" tab | |
NEW_STEPS = [1, 10] # in minutes | |
GRADUATING_INTERVAL = 1 # in days | |
EASY_INTERVAL = 4 # in days | |
STARTING_EASE = 250 # in percent | |
# "Reviews" tab | |
EASY_BONUS = 130 # in percent | |
INTERVAL_MODIFIER = 100 # in percent | |
MAXIMUM_INTERVAL = 36500 # in days | |
# "Lapses" tab | |
LAPSES_STEPS = [10] # in minutes | |
NEW_INTERVAL = 70 # in percent | |
MINIMUM_INTERVAL = 1 # in days | |
class Card: | |
def __init__(self): | |
self.status = 'learning' # can be 'learning', 'learned', or 'relearning' | |
self.steps_index = 0 | |
self.ease_factor = STARTING_EASE | |
self.interval = None | |
def __repr__(self): | |
return "Card[%s; steps_idx=%s; ease=%s; interval=%s]" % (self.status, | |
self.steps_index, | |
self.ease_factor, | |
str(self.interval)) | |
def schedule(card, response): | |
'''response is one of "again", "hard", "good", or "easy" | |
returns a result in days''' | |
if card.status == 'learning': | |
# for learning cards, there is no "hard" response possible | |
if response == "again": | |
card.steps_index = 0 | |
return minutes_to_days(NEW_STEPS[card.steps_index]) | |
elif response == "good": | |
card.steps_index += 1 | |
if card.steps_index < len(NEW_STEPS): | |
return minutes_to_days(NEW_STEPS[card.steps_index]) | |
else: | |
# we have graduated! | |
card.status = 'learned' | |
card.interval = GRADUATING_INTERVAL | |
return card.interval | |
elif response == "easy": | |
card.status = 'learned' | |
card.interval = EASY_INTERVAL | |
return EASY_INTERVAL | |
else: | |
raise ValueError("you can't press this button / we don't know how to deal with this case") | |
elif card.status == 'learned': | |
if response == "again": | |
card.status = 'relearning' | |
card.steps_index = 0 | |
card.ease_factor = max(130, card.ease_factor - 20) | |
card.interval = max(MINIMUM_INTERVAL, card.interval * NEW_INTERVAL/100) | |
return minutes_to_days(LAPSES_STEPS[0]) | |
elif response == "hard": | |
card.ease_factor = max(130, card.ease_factor - 15) | |
card.interval = card.interval * 1.2 * INTERVAL_MODIFIER/100 | |
return min(MAXIMUM_INTERVAL, card.interval) | |
elif response == "good": | |
card.interval = (card.interval * card.ease_factor/100 | |
* INTERVAL_MODIFIER/100) | |
return min(MAXIMUM_INTERVAL, card.interval) | |
elif response == "easy": | |
card.ease_factor += 15 | |
card.interval = (card.interval * card.ease_factor/100 | |
* INTERVAL_MODIFIER/100 * EASY_BONUS/100) | |
return min(MAXIMUM_INTERVAL, card.interval) | |
else: | |
raise ValueError("you can't press this button / we don't know how to deal with this case") | |
elif card.status == 'relearning': | |
if response == "again": | |
card.steps_index = 0 | |
return minutes_to_days(LAPSES_STEPS[0]) | |
elif response == "good": | |
card.steps_index += 1 | |
if card.steps_index < len(LAPSES_STEPS): | |
return minutes_to_days(LAPSES_STEPS[card.steps_index]) | |
else: | |
# we have re-graduated! | |
card.status = 'learned' | |
# we don't modify the interval here because that was already done when | |
# going from 'learned' to 'relearning' | |
return card.interval | |
else: | |
raise ValueError("you can't press this button / we don't know how to deal with this case") | |
def minutes_to_days(minutes): | |
return minutes / (60 * 24) | |
def human_friendly_time(days): | |
if not days: | |
return days | |
if days < 1: | |
return str(round(days * 24 * 60, 2)) + " minutes" | |
elif days < 30: | |
return str(round(days, 2)) + " days" | |
elif days < 365: | |
return str(round(days / (365.25 / 12), 2)) + " months" | |
else: | |
return str(round(days / 365.25, 2)) + " years" | |
card1 = Card() | |
# responses = ["good", "good", "good", "again", "good", "good", "good"] | |
responses = ["good"] * 10 | |
for r in responses: | |
print(str(card1) + " [%s]" % r, end="→ ") | |
t = schedule(card1, r) | |
print(human_friendly_time(t), card1) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment