outcome
def outcome(
correct:bool, confidence:int=None, hints:int=0, dont_know:bool=False
)->str:One of solid, fragile, misconception or gap. With no confidence, only the answer and the hints count.
This module holds the learner model as plain functions with no model calls and no storage. It finds what an answer means, updates the state of a concept, finds the next depth and picks the next concept. The tutor calls these functions and the store keeps their results.
Import the tools that this module uses.
Each concept has a depth from 0 to 5. The tutor asks each question at one depth.
| depth | name | a question at this depth asks you to |
|---|---|---|
| 0 | unseen |
(no question) |
| 1 | recognise |
pick the correct statement from options |
| 2 | recall |
state the idea or the rule |
| 3 | explain |
say why it is true, in your own words |
| 4 | apply |
use it to solve a problem |
| 5 | transfer |
use it in a new case or link it to another subject |
The floor is the highest depth that you pass reliably. The ceiling is the lowest depth that you failed (6 until you fail). The next question goes between them. Thus each answer moves one edge of this range.
These constants name the depths, give the question kind at each depth, and set the goal and the high-confidence line.
Correctness and confidence together tell what an answer means.
| high confidence | low confidence | |
|---|---|---|
| correct | solid |
fragile |
| wrong | misconception |
gap |
“I don’t know” is a gap. A correct answer that used two or more hints is fragile.
One of solid, fragile, misconception or gap. With no confidence, only the answer and the hints count.
A correct answer is solid with high confidence. Two hints or low confidence make it fragile.
With no confidence given, a clean pass is solid and a wrong answer is a gap, never a misconception.
A wrong answer is a misconception with high confidence, else a gap. “I don’t know” is always a gap.
Recall falls with time as in FSRS: \(R(t) = (1 + \frac{19}{81}\frac{t}{S})^{-1/2}\). The time \(t\) and the stability \(S\) are in days. At \(t = S\), recall is 90%. Thus the due date is the last review plus \(S\) days.
recall gives the recall now. grow gives the new stability after an outcome.
The new stability after an outcome, from the old stability s and the recall r at the time of the answer.
The chance, from 0 to 1, that you still recall concept c now.
Recall is 90% after \(S\) days, and 0 for a concept with no review.
A pass grows stability. A harder recall grows it more. A gap shrinks it.
update returns the fields to change after one answer at depth d.
| outcome | change |
|---|---|
solid |
The floor rises to d. The streak grows. |
fragile |
The floor stays. The concept is shaky at d, so the next question is at d again. |
misconception |
The ceiling falls to d. The floor falls below d. The misconception is kept. |
gap |
The ceiling falls to d. The floor falls below d. |
The state changes for concept c after one answer at depth, with the outcome in kind.
A solid pass at depth 3 on a new concept raises the floor to 3. The review is due in one day.
A confident wrong answer at depth 4 lowers the ceiling to 4 and keeps the misconception.
A miss below the floor lowers the floor. A low-confidence pass makes the concept shaky at that depth.
Ask in the middle of the range between floor and ceiling, so one answer cuts the range in half. Ask a shaky concept at the same depth again. When the ceiling is the floor plus 1, ask at the ceiling, because the work is at that edge.
A new concept starts at depth 3 (explain). An open range gives its middle.
A closed range gives the ceiling. Depth 5 is the top. A shaky depth comes first.
pick chooses the concept. Code makes the choice, not the model, so the app can show the reason.
focus concept, when you chose one and did not learn it yet.After two misses in a row on a concept, pick goes back to its weakest prereq that is below the goal.
learned and ready test one concept. _back finds the prereq to go back to.
Whether each prereq of c that is a known concept is at depth 3 or more.
pick returns the concept, the depth and the reason.
The next (concept, depth, why), or None when no review is due and the frontier is empty. Concepts in near come first on the frontier.
understanding reads one concept and tells how well you know it: the floor, the ceiling, the recall now, and the reasons that it is weak. It uses only the state of the concept. Thus a page of all concepts does not need to read the attempts. weak is true when a reason says that you need help now. strong needs depth 3 or more, recall of 80% or more, and no reasons.
How well you know concept c: floor, ceiling, recall, the reasons it is weak, weak, strong, one state, and a score to sort by.
A concept with a misconception and two misses is weak, with both reasons. A deep, fresh concept with no misses is strong.
now = 1e6
u = understanding(dict(slug='a', floor=1, ceiling=3, misses=2, misconceptions=['scores add'], seen=3, stability=5., last=now), now)
test_eq((u.weak, u.strong, u.reasons[:2]), (True, False, ['a misconception: scores add', 'missed the last 2 times']))
s = understanding(dict(slug='b', floor=4, ceiling=6, seen=5, stability=30., last=now - DAY), now)
test_eq((s.weak, s.strong, s.reasons), (False, True, []))
test_eq(understanding(dict(slug='g', floor=0, provenance=[dict(src='gap')]), now).reasons, ['you said you did not know it'])
assert understanding(dict(slug='f', floor=3, seen=4, stability=1., last=now - 30 * DAY), now).reasons[0].startswith('fading')
test_eq([u.state, s.state, understanding(dict(slug='n')).state], ['weak', 'strong', 'unseen'])Three concepts: b needs a. The first pick is a, because b waits for its prereq.
A started concept below the goal comes first. When a is learned, b is ready.
A due review comes before the frontier, at the floor of the concept.
Two misses go back to the weakest prereq. A learned concept that is not due gives no question.
A concept in near comes first on the frontier, before a concept you started elsewhere.
A gap that you named on a concept comes first, at the easiest depth. After you pass it, or after two questions on it, the concept comes back.
nn = dict(slug='neuron', name='Neuron', floor=0, ceiling=6, gaps=['wsum'])
ws = dict(slug='wsum', name='Weighted sum', floor=0, ceiling=6, seen=0)
c, d, why = pick([nn, ws], focus='neuron')
test_eq((c['slug'], d), ('wsum', 1)); assert 'you said you did not know Weighted sum' in why
test_eq(pick([nn, dict(ws, floor=1, seen=1)], focus='neuron')[0]['slug'], 'neuron')
test_eq(pick([nn, dict(ws, seen=2)], focus='neuron')[0]['slug'], 'neuron')