method

The ways you learn: code, maths, analogies, worked examples, diagrams and plain words. This module finds which ways work for you, and when to try a new one.

Each question, hint and lesson uses one way to teach: its method. The tutor picks the method before it writes the prompt. So the tag is known, not guessed. When a question closes, its outcome counts for its method: solid 1, shaky 0.5, a miss 0.

The counts give each method a Beta posterior, for each subject and over all subjects. choose uses them in two ways:

The counts are in one stats document on the shelf. This document is only a cache: method_stats builds it again from the attempts.

Import the modules that this notebook uses.

The methods

METHODS maps each method to the line that the prompt gets. EXAMPLES_TO maps the profile’s “examples that help” to methods, to set the prior.

The stats document

stats reads the stats document. It gives empty counts when there is no document. set_stats writes the document. A lock makes sure that two closes at the same time do not lose a count.


source

Learning.set_stats

def set_stats(
    **kv
):

Change fields of the stats document.


source

Learning.stats

def stats()->fastcore.basics.AttrDict:

The cached stats: method counts for each subject (* is all), recent credits, and calibration.

Credit

credit adds one outcome to the counts and returns the new stats. It keeps the last 8 credits of each method, to find a method that gets better.


source

Learning.credit_method

def credit_method(
    method:str, subject:str, kind:str
):

Count outcome kind for method in the stats document.


source

credit

def credit(
    st, method:str, subject:str, kind:str
)->dict:

The stats st with one outcome kind added for method, in subject and in *.

Solid adds 1 to a. A miss adds 1 to b. Shaky adds half to each. An unknown outcome or method changes nothing.

st = credit(credit(credit(EMPTY, 'code', 'ml', 'solid'), 'code', 'ml', 'fragile'), 'maths', 'ml', 'gap')
test_eq(st['methods']['ml'], {'code': [1.5, .5], 'maths': [0., 1.]})
test_eq(st['methods']['*'], st['methods']['ml'])
test_eq(st['recent'], {'code': [1., .5], 'maths': [0.]})
test_eq(credit(st, 'code', 'ml', 'closed'), st)

The posterior

posterior gives (a, b) for a method: Beta(1, 1), plus 2 on a for a method in the profile’s examples. For a subject, it adds the subject counts and the * counts. It scales the * counts down to at most 4 pseudo-counts. So a new subject starts from what works for you in other subjects.


source

improving

def improving(
    st, m:str
)->tuple | None:

(right, n) over the last 5 credits of m, when they are at least 0.2 above its mean over all; else None.


source

mean

def mean(
    ab
)->float:

source

posterior

def posterior(
    st, m:str, subject:str='', prior:tuple=()
)->tuple:

The Beta (a, b) of method m in subject.

With no counts, the prior decides. improving finds a method that got better recently.

test_eq(posterior(EMPTY, 'code'), (1., 1.))
test_eq(posterior(EMPTY, 'code', prior=['code']), (3., 1.))
gm = dict(methods={'*': {'maths': [2., 4.]}}, recent={'maths': [0., 0., 1., 1., 1., 1.]})
test_eq(improving(gm, 'maths'), (4, 5))
test_is(improving(dict(gm, recent={'maths': [1., 0., 0., 0.]}), 'maths'), None)

Choose a method

choose returns (method, note). The note goes in the line that says why you get this question.


source

choose

def choose(
    st, subject:str='', weak:bool=False, rng:NoneType=None, explore:str='sometimes', prior:tuple=()
)->tuple:

The method for the next question, and a note on why. A weak concept gets the best method; else, sometimes, another one.

A weak concept gets the best method. With explore='never', choose always gives the best method. With sometimes, it tries a method that got better at times. It still gives the best method most of the time.

cs = dict(methods={'*': {'code': [7., 2.], 'maths': [2., 4.]}}, recent={'maths': [0., 0., 1., 1., 1., 1.]})
m, note = choose(cs, 'ml', weak=True)
test_eq(m, 'code'); assert note.startswith('drill with code: 7 of 9')
test_eq({choose(cs, 'ml', rng=random.Random(i), explore='never')[0] for i in range(30)}, {'code'})
picks = [choose(cs, 'ml', rng=random.Random(i)) for i in range(60)]
assert any(n.startswith('trying maths: you got better') for _, n in picks)
test_eq(sum(m == 'code' for m, _ in picks) > 30, True)   # with `sometimes`, the best method still comes most of the time

In plain words, and from the attempts

methods_text says what works, for a prompt and for the page. method_stats builds the counts again from the attempts, in order.


source

Learning.method_stats

def method_stats(
    n:int=2000
)->dict:

Count every final attempt with a method again, oldest first, and save the counts. Return the stats.


source

methods_text

def methods_text(
    st, subject:str='', n:int=3
)->str:

The methods with counts, best first, as one line; empty when nothing is counted.

The counts that credit_method keeps match a rebuild from the attempts.

ml_ = temp_learning(); ml_.put('Ranking', 'Order items by a score.', 'ml')
for m, k in (('code', 'solid'), ('code', 'fragile'), ('maths', 'gap')):
    ml_.record('ranking', f'{m} {k}?', final=True, method=m, outcome=k); ml_.credit_method(m, 'ml', k); time.sleep(.01)
inc = ml_.stats()
test_eq(ml_.method_stats()['methods'], inc.methods)
assert methods_text(inc, 'ml').startswith('Ways that work for you: code 2 of 2')