Skip to content

Instantly share code, notes, and snippets.

@kevinmungai
Forked from riceissa/anki_algorithm.py
Created February 22, 2023 18:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kevinmungai/bc3dfe9b212679e3e88ad4c4db7da35d to your computer and use it in GitHub Desktop.
Save kevinmungai/bc3dfe9b212679e3e88ad4c4db7da35d to your computer and use it in GitHub Desktop.
my current understanding of Anki's spacing algorithm
"""
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