Data Engineering Mastery Platform · Part 2 of 6

The learning engine: how the app knows what Priya needs

The learning engine: how the app knows what Priya needs

Six small algorithms decide what she studies, when she reviews, and whether she's ready — real code, hand-worked numbers, zero AI.

2026-09-05beginnerPython 3.11, Pytest
  • python
  • algorithms
  • testing
  • beginners

Priya’s Tuesday morning

She opens the dashboard: 3 flashcards due, “Liquid Clustering” flagged weak, recommended next: re-read + 5 practice questions, mock exam readiness 61%. No human tutor is awake. Six small functions produced that morning — this part builds each one with you, and you’ll compute every number by hand before trusting it.

You are here: Part 2 of 6 — the study brain. Part 1 mapped the repo; Parts 3–6 serve this math over HTTP, feed it trustworthy content, let agents query it, and prove it running. The naive version serves random questions and calls it practice; what breaks is trust — Priya rehearses what she knows, skips what she fears, and the score lies. What follows is the machinery that makes the order honest.

The contract of a pure function

Every module in backend/app/algorithms/ obeys one contract: same inputs in, same outputs out — no database, no network, no clock surprises. That is what “pure” means, and it buys three things: tests run in milliseconds, results are reproducible for Priya, and you can reason about each module on paper, which is exactly what follows.

The six residents: mastery (how well known), retention (when to review), weakness (what hurts), recommendations (what next), path_finding (in what order), exam_assembly (fair papers). Nine test files pin them — 86 tests, engine modules at 93–100% coverage.

One number for “how well she knows it”

A recommendation is a weighted sum over measurable things. Real code from backend/app/algorithms/mastery.py (verbatim):

Weighted mastery with recency decaybackend/app/algorithms/mastery.py:15-78
quiz_accuracy = quiz_correct / quiz_total if quiz_total > 0 else 0.0
difficulty_coverage = len(difficulty_covered) / 5.0 if difficulty_covered else 0.0
practice_completion = practice_completed / practice_total if practice_total > 0 else 0.0
hands_on = 1.0 if hands_on_completed else 0.0
recency = math.exp(-days_ago / 14.0) # half-life 14 days
score = (quiz_accuracy * 0.40 + difficulty_coverage * 0.25 +
practice_completion * 0.15 + hands_on * 0.10 + recency * 0.10)
# <0.2 unknown, <0.4 recognition, <0.6 understanding,
# <0.8 application, <0.9 implementation, else troubleshooting

What. Quiz accuracy, hard-difficulty coverage, practice completion, hands-on work, and freshness combine into a 0–1 score with a human level name.

Why. Fixed weights make scores explainable, and the recency term (half-life 14 days) models forgetting instead of pretending knowledge is permanent.

Tradeoff. Weights are judgment frozen in code, never A/B tested. Transparent-and-arbitrary beats learned-and-opaque — but only if you say so openly.

Compute Priya’s score with her: 8/10 quiz (0.8), difficulties 5 (0.6), 6/8 practice (0.75), hands-on done (1.0), correct 7 days ago (e⁻⁰·⁵ ≈ 0.61). Score ≈ 0.32 + 0.15 + 0.1125 + 0.10 + 0.061 = 0.744 → “application”. Notice the embedded teaching philosophy: hard things (0.25) outweigh many things (0.15). When you write scoring, the weights are the product philosophy — choose them deliberately, then test every level boundary (this repo does: 11 mastery tests).

Two production gotchas inside that file

Two senior details in the real file, both worth stealing:

  1. The timezone guard. The database returns timezone-aware timestamps; utcnow() is naive. Comparing them crashes — a classic production incident — pre-empted here in a comment and a conversion. Lesson: every timestamp crossing a boundary gets normalized, no exceptions.
  2. The dual scale. Mastery lives 0–1 here but 0–1000 in storage. Two scales for one concept is a standing invitation to mix them up; the defense is tests on both sides of the conversion. Know both before integrating.

When she sees it again (SM-2)

if quality < 3:
interval_days, repetitions = 1, 0 # failed: tomorrow
new_ease = max(1.3, ease_factor - 0.2)
else:
if repetitions == 0: interval_days = 1
elif repetitions == 1: interval_days = 6
else: interval_days = round(interval_days * ease_factor)
repetitions += 1
new_ease = ease_factor + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02))
new_ease = max(1.3, new_ease)

Mental model: each card carries ease (how well known) and interval (when to resurface). Trace Priya’s card with her: quality=4, ease=2.5, interval=6, reps=2 → 15 days, ease 2.5, reps 3. Now quality=2 → tomorrow, ease taxed, reps reset. Two traces and you own the algorithm. Scope note: SM-2 schedules recall (flashcards), never skill — the engine correctly limits it there.

The 1.3 floor matters: without it, repeated failures would drive easiness toward zero and intervals toward nonsense. Every formula needs its guardrails stated — ask of any algorithm you meet: what stops the numbers from going feral?

What’s weak, what’s next, what path

One pattern, three applications — score candidates, rank, act:

weakness = (1 - mastery) * 0.5 + error_rate * 0.3 + recent_share * 0.2
flag above 0.4 with typed reason + fix suggestion
recommendations = blend(goal .20, gap .15, weakness .15, forgetting .15,
prereqs .10, project .10, difficulty .05)
path = ancestors(target) minus mastered, depth-sorted

Path finding runs Kahn’s topological sort that raises on cycles — a curriculum requiring A-before-B-before-A is broken by construction, and code should prove it. Fair exams come from stratified sampling (40/40/20 across difficulties, seeded shuffle: same seed, same paper). Every threshold is a named constant with a test.

Notice what the weakness formula refuses to do: it never says “study harder.” It names the gap (which topic, what kind of errors, how recent) and suggests the fix. Diagnostics beat exhortations — in software and in studying.

The content behind the math: a real trap

The engine reasons over curated confusions like this verbatim entry from knowledge-base/exam_traps.yaml:

“500 TB table, 3–4 query dimensions, partitioned by date, slow. What now?” → Liquid Clustering on the most-filtered dimensions — with explicit reversals (need DROP PARTITION lifecycle → partitioning wins).

The reversal is the tell of honest content: the trap admits when the fashionable answer loses. Content without reversals is marketing; content with them is teaching.

Directing an AI agent at this engine

Give the agent the contract, not just the feature:

You are extending backend/app/algorithms/ — pure functions only.
Goal: <e.g., add a "streak bonus" term to mastery>
Constraints:
- No database, network, or clock calls inside the module.
- New weights must be named constants with tests on every boundary.
- Run make test-unit; all 86 must stay green.
Before coding:
1. Show the formula change on paper with Priya's numbers.
2. Name which level boundaries shift and why that is acceptable.
3. Then implement.

An agent handed this prompt produces reviewable math. An agent handed “make mastery better” produces confident soup.

Failure modes of scoring systems

  • Weights nobody chose. Defaults inherited from a tutorial become product philosophy by accident. Defense: every weight has a comment with a reason and a test.
  • Untested boundaries. A score of exactly 0.8 landing in the wrong level mislabels Priya’s readiness. Defense: boundary tests (this repo has 11 for mastery alone).
  • Timezone crashes. Aware vs naive timestamps (Topic 4). Defense: normalize at every boundary.
  • Gaming the metric. Priya re-takes easy quizzes to inflate mastery. Partial defense: difficulty coverage (0.25) outweighs volume (0.15) — the formula anticipates the exploit. No formula survives all gaming; name what yours doesn’t cover.
  • Stale recency. A year-old perfect score still contributes a little. Defense: the 14-day half-life decays it toward zero — check that your decay actually reaches ~0 in your horizon.

Exercises

  1. Compute mastery for: 5/10 quiz, difficulties 2, 8/8 practice, no hands-on, 30 days ago. Which term dominates the low score?
  2. Trace an SM-2 card through fail → pass → pass with quality 5. Write every number.
  3. Change the recency half-life from 14 to 30 days. Whose study plan changes most — crammers or steady learners?
  4. Find the cycle-raising code path in path_finding.py. Construct a 3-node cycle and confirm it raises.
  5. Ask an AI agent to add a seventh module (e.g., burnout detection) using the Topic 8 prompt. Review: is it pure? Are boundaries tested?

Five things to take with you

  1. Weights are product philosophy — write them deliberately, test the boundaries.
  2. Pure functions are debuggable, and debuggable is teachable.
  3. Every formula needs guardrails (floors, timezone normalization, dual scales).
  4. Diagnostics beat exhortations: name the gap, suggest the fix.
  5. Deterministic systems can be checked by hand — that checkability is the feature.

Next: Part 3 serves these functions over HTTP — what each API does for Priya.

What worked, what I’d change

Worked: pure functions with per-module tests — every number above checkable in milliseconds. Changed already (resolved): the pre-audit codebase had no test framework; the engine split was the fix. Principle: deterministic systems are debuggable, and debuggable is teachable.