Lesson 14 / 15
Model Documentation
Counting which of the questions of the person who takes over the model are closed by a written statement: the same model gives 0.7778 accuracy overall on the test set and 0.0198 above the baseline, while in 12 measurable subgroups of the 14 built on four axes the best reads 0.9688 and the worst 0.6111, the subgroup gap is 0.3576 and three subgroups fall below their own baseline. Of the handover question set of twenty-two questions, the statement that carries only the number measured overall closes 5, the statement that adds the subgroup table closes 12, and the statement that also writes the usage limit closes 18; the remaining 4 questions close in no version because none of them is a measurement question. In five usage decisions the first version produces 4 wrong decisions and the second 1, while the third produces none because it can say unknown for the unmeasured subgroup.
Published Updated
In this lesson
- Measures side by side the number the same model gives overall and in subgroups defined on four axes, and writes the subgroup gap
- Counts the question set of the person who takes over the model and computes how many questions each of three statement versions closes and how many it leaves open
- Measures with which statement version the same usage decision changes, and shows how having the unmeasured subgroup recorded in the statement saves the decision
Contents
Three lessons measured harm and tried to reduce it: the three sources of bias were counted one by one, the three fairness criteria were shown not to hold at the same time, and how much a single record moves the output and what the noise takes from accuracy were printed side by side. None of these measurements reaches the person who will use the model. The measurement stays in the notebook; the decision is made at another desk.
This lesson measures what reaches that desk. What reaches it is the model document: a written statement that says what the model was measured on, for whom it was measured, and where it will not be used. The quality of the document is counted not by its character but by how many questions it closes. The Visualization, Reporting and Analysis at Scale course measured what a report makes its reader read back; the reader here is not a report reader but the person who takes over the model.
- ER31. The set, label and model are inherited from the first seven courses of the Machine Learning and Data Science curriculum and are fictional: 1,260 subscribers, a 756/252/252 split, a baseline of 0.7579 on the test set.
- ER32. The measure is accuracy and the baseline is the most common class of the training set. Both are computed again in every subgroup.
- ER33. Subgroups are defined on four axes: region, tariff tier, household size, reading count. This lesson splits the tariff axis into four tiers instead of the course’s three and pulls the reading threshold down to one period; the number of subgroups is therefore fourteen, not the course’s fixed thirteen. The definition is a decision and how many subgroups were looked at is written down.
- ER34. A subgroup of fewer than twenty subscribers is not measured; the threshold is a decision and is separate from the course’s criterion of twenty-five.
- ER35. The question set of the person who takes over is fictional: twenty-two questions. Each question is tied to a single statement item, and it counts as closed if that item is in the statement.
- ER36. The three statement versions are nested: A carries only the number measured overall, B adds the subgroup table, C adds the usage limit as well. None of them is a template; each is a list of items.
- ER37. The usage decision procedure reads only the statement: if there is a subgroup row it uses that, otherwise it uses the number measured overall. For a subgroup that was not measured, the right answer is “unknown”.
The Same Number, Twelve Subgroups
# documentation.py — MODEL: The same FICTIONAL subscriber table of the first seven courses is built with # the same seed (20260218), the FICTIONAL illicit-use or fault suspicion label is added on top, and the # same number is measured first overall, then in subgroups defined on four axes. import math SEED, RAW, M32, MIN_SIZE = 20260218, 1400, 0xFFFFFFFF, 20 REGION = [("north", 0.28, 21, 0.00), ("south", 0.22, 17, -0.10), ("east", 0.18, 26, 0.30), ("west", 0.14, 14, -0.05), ("central", 0.18, 23, 0.05)] FIELDS = ["mean_consumption", "volatility", "household", "satisfaction", "period", "zero_readings", "area_north", "area_south", "area_east", "area_west", "area_central"] def rng(t): x = ((t ^ (t >> 16)) * 2246822507) & M32 x = ((x ^ (x >> 13)) * 3266489909) & M32 s = [(x ^ (x >> 16)) & M32] def next_value(): s[0] = (s[0] * 1664525 + 1013904223) & M32 return s[0] / 4294967296 return next_value def pick(u, w): t = 0.0 for i, x in enumerate(w): t += x if u < t: return i return len(w) - 1 DATA = [] for i in range(RAW): r = rng(SEED + i) b = REGION[pick(r(), [x[1] for x in REGION])] household = 1 + pick(r(), [0.06, 0.24, 0.30, 0.24, 0.11, 0.05]) rating = 1 + pick(r(), [0.08, 0.14, 0.27, 0.34, 0.17]) if r() < 0.046: continue num = 10001 + i r, v = rng(SEED + 7000 + num), [] for d in range(pick(r(), [0.05, 0.12, 0.21, 0.62])): v.append(0.0 if r() < 0.038 else math.floor( b[2] * math.exp((r() + r() + r() - 1.5) * 0.62) * (1 - d * 0.05) * 100 + 0.5) / 100) r() if not v: continue mean, vol, r = sum(v) / len(v), max(v) - min(v), rng(SEED + 51000 + num) z = (0.052 * (mean - 20) + 0.85 * (min(v) == 0.0) + 0.026 * vol + 0.24 * (household >= 5) - 0.20 * (rating >= 4) + b[3] + 0.9 * (num - 10001) / 1399 + (r() + r() + r() - 1.5) * 1.30) x = {"household": household, "satisfaction": rating, "period": len(v), "region": b[0], "mean_consumption": round(mean, 2), "volatility": round(vol, 2), "zero_readings": int(min(v) == 0.0), "suspicious": int(z > 1.35)} for q in REGION: x["area_" + q[0]] = int(b[0] == q[0]) DATA.append(x) def shuffle(items, seed): r, s = rng(seed), list(items) for i in range(len(s) - 1, 0, -1): j = int(r() * (i + 1)) s[i], s[j] = s[j], s[i] return s K = shuffle(DATA, SEED + 90000) TRAIN, HELD_OUT = K[:756], K[1008:] def tree(s, depth, min_size=2): def impurity(u): p = sum(x["suspicious"] for x in u) / len(u) if u else 0.0 return 2 * p * (1 - p) p, best = sum(x["suspicious"] for x in s) / len(s), None if depth and len(s) >= 2 * min_size and 0.0 < p < 1.0: for a in FIELDS: d = sorted({x[a] for x in s}) for v in (d[1:] if len(d) < 10 else [d[int(i * len(d) / 10)] for i in range(1, 10)]): left = [x for x in s if x[a] < v] right = [x for x in s if x[a] >= v] if min(len(left), len(right)) < min_size: continue k = impurity(s) - (len(left) * impurity(left) + len(right) * impurity(right)) / len(s) if best is None or k > best[0]: best = (k, a, v, left, right) if best is None or best[0] <= 1e-9: return {"prediction": int(p > 0.5)} return {"field": best[1], "threshold": best[2], "left": tree(best[3], depth - 1, min_size), "right": tree(best[4], depth - 1, min_size)} M = tree(TRAIN, 6) MOST_COMMON = int(sum(x["suspicious"] for x in TRAIN) * 2 > len(TRAIN)) def measure(s): """Baseline (most common class) and accuracy of the model on the same set.""" hits = 0 for x in s: u = M while "prediction" not in u: u = u["left"] if x[u["field"]] < u["threshold"] else u["right"] hits += u["prediction"] == x["suspicious"] return sum(x["suspicious"] == MOST_COMMON for x in s) / len(s), hits / len(s) AXES = [("region", lambda x: x["region"]), ("tariff tier", lambda x: "tier %d" % ( 1 + (x["mean_consumption"] >= 10) + (x["mean_consumption"] >= 25) + (x["mean_consumption"] >= 40))), ("household size", lambda x: "1-2 people" if x["household"] <= 2 else ("3-4 people" if x["household"] <= 4 else "5+ people")), ("reading count", lambda x: "few readings" if x["period"] <= 1 else "full readings")] t, m = measure(HELD_OUT) print(f"test set {len(HELD_OUT)} subscribers; overall baseline {t:.4f}, model {m:.4f}, " f"difference {m - t:+.4f}") print(f"\n{'axis':<16}{'subgroup':<14}{'subscribers':>12}{'baseline':>10}{'model':>8}{'difference':>12}") SUBGROUPS, UNMEASURED = [], [] for name, f in AXES: for g in sorted({f(x) for x in HELD_OUT}): sub = [x for x in HELD_OUT if f(x) == g] if len(sub) < MIN_SIZE: UNMEASURED.append((g, len(sub))) continue bt, bm = measure(sub) SUBGROUPS.append((g, len(sub), bt, bm)) print(f"{name:<16}{g:<14}{len(sub):>12}{bt:>10.4f}{bm:>8.4f}{bm - bt:>+12.4f}") best, worst = max(SUBGROUPS, key=lambda u: u[3]), min(SUBGROUPS, key=lambda u: u[3]) print(f"\n{len(AXES)} axes built {len(SUBGROUPS) + len(UNMEASURED)} subgroups; " f"{len(UNMEASURED)} of them with fewer than {MIN_SIZE} subscribers were not measured " f"({', '.join(g + ' ' + str(n) for g, n in UNMEASURED)})") print(f"in the {len(SUBGROUPS)} measured subgroups best {best[3]:.4f} ({best[0]}), worst " f"{worst[3]:.4f} ({worst[0]}), subgroup gap {best[3] - worst[3]:.4f}; subgroups below " f"their own baseline: {sum(1 for u in SUBGROUPS if u[3] < u[2])}")
test set 252 subscribers; overall baseline 0.7579, model 0.7778, difference +0.0198 axis subgroup subscribers baseline model difference region central 58 0.6552 0.6724 +0.0172 region east 36 0.4722 0.6111 +0.1389 region north 68 0.8235 0.7794 -0.0441 region south 58 0.8276 0.8793 +0.0517 region west 32 1.0000 0.9688 -0.0312 tariff tier tier 2 194 0.8402 0.8299 -0.0103 tariff tier tier 3 46 0.4565 0.6304 +0.1739 household size 1-2 people 73 0.7534 0.7945 +0.0411 household size 3-4 people 143 0.7692 0.7762 +0.0070 household size 5+ people 36 0.7222 0.7500 +0.0278 reading count few readings 22 0.8636 0.8636 +0.0000 reading count full readings 230 0.7478 0.7696 +0.0217 4 axes built 14 subgroups; 2 of them with fewer than 20 subscribers were not measured (tier 1 11, tier 4 1) in the 12 measured subgroups best 0.9688 (west), worst 0.6111 (east), subgroup gap 0.3576; subgroups below their own baseline: 3
What is read overall is a single line: the model gives 0.7778 accuracy and beats the baseline by 0.0198. The subgroup table measures the same model in twelve separate places, and the single line splits into twelve sentences. The subgroup gap between the best and the worst is 0.3576; eighteen times the difference read overall. In three subgroups the model falls below its own baseline: it trails by 0.0441 in the north, by 0.0312 in the west, and by 0.0103 in tariff tier 2. In the west the baseline is 1.0000, that is, there are no suspicious subscribers at all there, and the model flags one subscriber for nothing. In the “few readings” subgroup the difference is exactly 0.0000: in those twenty-two subscribers the model does nothing different from saying the most common class. Two of the fourteen subgroups, on the other hand, were not measured at all, because they come to eleven and one subscribers in the test set — but this does not mean the model will not be used there.
The Twenty-Two Questions of the Person Who Takes Over
# questions.py — the question set of the person who takes over the model is FICTIONAL and has twenty-two # questions. Each question is tied to the single statement item that closes it; a document version # is a set of items, and the number of closed questions is counted by looking at the set. QUESTIONS = [ ("what does the model predict", "measure"), ("which number was reported", "overall"), ("on which set was this number measured", "set"), ("what is the baseline and does the model beat it", "overall"), ("which periods does the data cover", "set"), ("which fields go into the model", "feature"), ("how was the label produced", "label"), ("on which subgroups was the number measured", "subgroup_definition"), ("how many subgroups on how many axes were examined", "subgroup_definition"), ("what is the number in the worst subgroup", "subgroup_table"), ("how large is the subgroup gap", "subgroup_table"), ("in which subgroup is the model below its own baseline", "subgroup_table"), ("was any subgroup left unmeasured", "unmeasured"), ("why was it not measured", "unmeasured"), ("where is the model not used", "limit"), ("does the output decide by itself", "decision"), ("at what threshold is the output turned into a decision", "decision"), ("when is the number measured again", "remeasure"), ("how does a subscriber appeal against the output", "appeal"), ("what does a wrong output do to the subscriber", "consequence"), ("who verified these numbers", "verifier"), ("who rolls the model back when it breaks", "rollback"), ] A = {"measure", "overall", "set"} B = A | {"feature", "label", "subgroup_definition", "subgroup_table"} C = B | {"unmeasured", "limit", "decision", "remeasure"} VERSIONS = [("A: only the number measured overall", A), ("B: A + subgroup table", B), ("C: B + usage limit statement", C)] print(f"{'document version':<38}{'items':>6}{'closed':>9}{'open':>6}{'closed share':>14}") for name, s in VERSIONS: k = sum(1 for _, field in QUESTIONS if field in s) print(f"{name:<38}{len(s):>6}{k:>9}{len(QUESTIONS) - k:>6}" f"{k / len(QUESTIONS):>14.4f}") NEVER_CLOSED = [q for q, field in QUESTIONS if field not in C] print(f"\nthe {len(NEVER_CLOSED)} questions that none of the three versions closes:") for q in NEVER_CLOSED: print(f" - {q}")
document version items closed open closed share A: only the number measured overall 3 5 17 0.2273 B: A + subgroup table 7 12 10 0.5455 C: B + usage limit statement 11 18 4 0.8182 the 4 questions that none of the three versions closes: - how does a subscriber appeal against the output - what does a wrong output do to the subscriber - who verified these numbers - who rolls the model back when it breaks
A statement of three items closes five of the twenty-two questions. Adding four items brings the closed questions to twelve, and adding four more brings them to eighteen. The number of closed questions does not rise evenly with the number of items: the subgroup table alone is enough for three questions at once.
The place to read is the last list. Four questions close in none of the three versions, and none of the four is a measurement question. The appeal route, what a wrong output does to the subscriber, who verified the number and who will roll the model back: the answers to these are neither in the test set nor in the subgroup table. A statement cannot close these four, but it can write that they are open. Counting what is left open is also a measure of the statement.
The Decision the Statement Changed
# decisions.py — the person who takes over makes five usage decisions and reads only the document. # The procedure first looks for the subgroup row; if it cannot find one, it falls back to the number # measured overall. Version C can say "unknown" because it recorded the unmeasured subgroups. ROWS = {u[0]: u for u in SUBGROUPS} MISSING = dict(UNMEASURED) OVERALL = "yes" if m > t else "no" def answer(version, group): if "subgroup_table" in version and group in ROWS: return "yes" if ROWS[group][3] > ROWS[group][2] else "no" if "unmeasured" in version and group in MISSING: return "unknown" return OVERALL def actual(group): if group in ROWS: return "yes" if ROWS[group][3] > ROWS[group][2] else "no" return "unknown" DECISIONS = ["north", "east", "west", "few readings", "tier 4"] print(f"{'subgroup':<14}{'measured':>12}" + "".join(f"{'version ' + name.split(':')[0]:>12}" for name, _ in VERSIONS)) for g in DECISIONS: print(f"{g:<14}{actual(g):>12}" + "".join(f"{answer(s, g):>12}" for _, s in VERSIONS)) print() for name, s in VERSIONS: y = sum(1 for g in DECISIONS if answer(s, g) != actual(g)) print(f"{name:<38}{len(DECISIONS) - y} correct, {y} wrong decisions") print(f"\noverall the model beats the baseline by {m - t:+.4f}; in the north " f"{ROWS['north'][3] - ROWS['north'][2]:+.4f}, in the west " f"{ROWS['west'][3] - ROWS['west'][2]:+.4f}, in few readings " f"{ROWS['few readings'][3] - ROWS['few readings'][2]:+.4f}")
subgroup measured version A version B version C north no yes no no east yes yes yes yes west no yes no no few readings no yes no no tier 4 unknown yes yes unknown A: only the number measured overall 1 correct, 4 wrong decisions B: A + subgroup table 4 correct, 1 wrong decisions C: B + usage limit statement 5 correct, 0 wrong decisions overall the model beats the baseline by +0.0198; in the north -0.0441, in the west -0.0312, in few readings +0.0000
The question is “does using the model in this subgroup add anything over the baseline”, and it is asked five times. The person who reads version A says “yes” to all five, because they hold a single number and that number is right for the whole; four come out wrong. Version B corrects four decisions but errs on the fifth: tariff tier 4 is not in the table, the procedure falls back to the number measured overall, and it answers as if an unmeasured subgroup had been measured.
The fifth row is the number of the lesson. The only difference between C and B is that it records the two unmeasured subgroups and why they were not measured. That record brings no new performance; the only thing it brings is the authority to say “unknown”, and it takes the wrong decisions down to zero.
The rule of the course is at work inside this table. The 0.7778 in version A is not miscalculated; it is a number that cannot be read because it was not written down for whom it was measured.
Summary
- The same model gives 0.7778 accuracy overall on the test set and beats the baseline by 0.0198, while in 12 measurable subgroups of the 14 built on four axes the best reads 0.9688 and the worst 0.6111; the subgroup gap is 0.3576, eighteen times the difference read overall.
- In three subgroups the model falls below its own baseline (north 0.0441, west 0.0312, tariff tier 2 trailing by 0.0103), and in the “few readings” subgroup the difference is exactly 0.0000.
- Of the handover question set of twenty-two questions, version A closes 5, version B 12 and version C 18; the number of closed questions does not rise evenly as items are added.
- Four questions close in no version and none of the four is a measurement question; a statement cannot close them but can write that they are open, and counting what is left open is also a measure of the statement.
- In five usage decisions version A produces 4 wrong decisions and version B 1; the one advantage of version C is that it records the unmeasured subgroup and can say “unknown”, and this takes the wrong decisions down to zero.
Next Step
The statement of this lesson carries the measured number, but nowhere does it say that the number can be produced a second time. The numbers of fourteen lessons were produced with a single seed, a single split and a single threshold; if one of these items is not fixed, how much which number will move was not asked. The next lesson collects the items accumulated from the preparation notebook of the Data Collection and Preparation course to the training loop of the Deep Learning course, measures by how many subscribers a single unfixed item shifts the test count, and gives the full item list of the Machine Learning and Data Science curriculum. That lesson also closes the course and the curriculum.
To keep your progress and take notes, Log in
My notes
Log in to take notes.