Lesson 05 / 20
Coefficient Interpretation
Treating readability as a number rather than a praise: the readable model chosen from eight candidates on the validation set carries twelve coefficients and gives 0.8016 on the test set, the best family chosen from the same set gives 0.8056 with 33 columns, and the price of interpretability is 0.0040, that is, a single subscriber. The twelve coefficients give the order of the generating structure in the fiction correctly. The same coefficients are not fixed: when the per-person consumption column is removed, the coefficient of mean consumption rises from 0.772 to 0.808 and the test number does not change at all; when mean consumption is removed, the coefficient of per-person consumption jumps from 0.071 to 0.442, that is, to six times. The ridge penalty brings the coefficients down from 0.772 to 0.326 while the test number falls by only 0.0079.
Published Updated
In this lesson
- Measures the held-out numbers of a model with readable coefficients and of the best family in this topic side by side and writes the difference in subscribers
- Reads which ranking the scaled coefficients give and compares that ranking with the generating structure in the fiction
- Counts, by removing columns and applying a penalty, that many different coefficient sets give the same held-out number
Contents
The model of the previous lesson carried twelve coefficients, and those coefficients are readable: each belongs to a column and has a sign and a magnitude. This is the most frequently cited advantage of linear models, and it is usually written down as a praise. This lesson treats it not as a praise but as a number: how many subscribers separate the held-out number of a model with readable coefficients from the number of the best family in this topic.
The second question is separate from the first and harder. What has been said when a coefficient is read. The Applied Statistics topic of the Statistical Analysis and Exploratory Data Analysis course had measured the causal face of this question: a one-variable coefficient changing sign when a confounding variable is added, and the within-group slopes turning against the aggregated slope, were shown there with numbers. It is not repeated here. The question here is specific to the prediction target: how many different coefficient sets give the same held-out number, and how much the value of a coefficient depends on which of those sets was chosen.
- LM36. The set, the split, the seed, the target and the fixed twelve-column feature set are as in the previous lesson; the baseline is 0.7579 on the test set.
- LM37. All candidates are learned with the same training setting: learning rate 0.3, 300 epochs. The tuning sweep of the previous lesson is not repeated; the only thing that changes in this lesson is the column set.
- LM38. A readable candidate is a model whose coefficients can be listed one by one and interpreted: the candidates with one, two, four and twelve columns.
- LM39. The expanded candidate is the model raised to 33 columns with the pairwise products and the squares of the six numeric columns. Its coefficients can be listed but not read one by one, because the same magnitude is spread across several columns.
- LM40. The ridge penalty is applied to the expanded candidate at four levels. The difference between the penalty forms was measured in the previous lessons; only the ridge penalty is used here.
- LM41. The number of candidates tried is 8: four readable candidates and four expanded candidates with a penalty. The choice is read from the validation set.
- LM42. The coefficients are read on scaled columns, so they are comparable with one another. A coefficient in raw units is the change per cubic meter and cannot be ranked, because the columns have different units.
- LM43. This lesson does not deal with for whom a coefficient is read, what happens in which subgroup, or how interpretability is built as a method; those questions belong to the Model Evaluation, Interpretability and Ethics course. Here interpretability is only the score paid.
The Price of Interpretability
The setup is the setup of the previous lesson.
# interpret.py — MODEL: the same FICTIONAL subscriber table, the same seed, the same split and # the same twelve-column feature set; the target is the suspicious label. import math import statistics SEED, RAW, M32 = 20260218, 1400, 0xFFFFFFFF 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)] 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 SUBSCRIBERS, 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: SUBSCRIBERS.append({"id": 10001 + i, "region": b, "household": household, "satisfaction": rating}) for k in SUBSCRIBERS: r, v = rng(SEED + 7000 + k["id"]), [] for n in range(pick(r(), [0.05, 0.12, 0.21, 0.62])): v.append(0.0 if r() < 0.038 else math.floor( k["region"][2] * math.exp((r() + r() + r() - 1.5) * 0.62) * (1 - n * 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 + k["id"]) z = (0.052 * (mean - 20) + 0.85 * (min(v) == 0.0) + 0.026 * vol + 0.24 * (k["household"] >= 5) - 0.20 * (k["satisfaction"] >= 4) + k["region"][3] + 0.9 * (k["id"] - 10001) / 1399 + (r() + r() + r() - 1.5) * 1.30) x = {"household": k["household"], "satisfaction": k["satisfaction"], "period": len(v), "mean_consumption": round(mean, 2), "volatility": round(vol, 2), "zero_readings": int(min(v) == 0.0), "suspicious": int(z > 1.35)} x["per_person"] = round(x["mean_consumption"] / x["household"], 3) for b in REGION: x["area_" + b[0]] = int(k["region"][0] == b[0]) DATA.append(x) def shuffle(data, seed): r, s = rng(seed), list(range(len(data))) for i in range(len(s) - 1, 0, -1): j = int(r() * (i + 1)) s[i], s[j] = s[j], s[i] return [data[i] for i in s] K = shuffle(DATA, SEED + 90000) TRAIN, VAL, TEST = K[:756], K[756:1008], K[1008:] FIELDS = ["mean_consumption", "volatility", "household", "satisfaction", "period", "zero_readings", "per_person", "area_north", "area_south", "area_east", "area_west", "area_central"] BASELINE = sum(x["suspicious"] == 0 for x in TEST) / len(TEST) print(f"train {len(TRAIN)}, validation {len(VAL)}, test {len(TEST)}, " f"baseline {BASELINE:.4f}")
train 756, validation 252, test 252, baseline 0.7579
The eight candidates are ranked in a single ladder: readable models from one column to twelve columns, then the expanded model with 33 columns at four penalty levels.
import itertools def squash(z): if z >= 0: return 1.0 / (1.0 + math.exp(-z)) if z < 700 else 1.0 e = math.exp(z) if z > -700 else 0.0 return e / (1.0 + e) PRODUCTS = ["mean_consumption", "volatility", "household", "satisfaction", "period", "per_person"] def expand(x): d = dict(x) for a, b in itertools.combinations(PRODUCTS, 2): d[a + "*" + b] = x[a] * x[b] for a in PRODUCTS: d[a + "^2"] = x[a] * x[a] return d EXPANDED = FIELDS + [a + "*" + b for a, b in itertools.combinations(PRODUCTS, 2)] \ + [a + "^2" for a in PRODUCTS] EX_TRAIN, EX_VAL, EX_TEST = [[expand(x) for x in s] for s in (TRAIN, VAL, TEST)] def scale(train, cols): return ({a: statistics.fmean(x[a] for x in train) for a in cols}, {a: max(1e-9, statistics.pstdev([x[a] for x in train])) for a in cols}) def train_model(train, cols, mu, sd, rate=0.3, epochs=300, penalty=0.0): # MODEL: logistic regression X = [[1.0] + [(x[a] - mu[a]) / sd[a] for a in cols] for x in train] Y = [x["suspicious"] for x in train] w = [0.0] * len(X[0]) for _ in range(epochs): g = [0.0] * len(w) for i in range(len(X)): h = squash(sum(w[j] * X[i][j] for j in range(len(w)))) - Y[i] for j in range(len(w)): g[j] += h * X[i][j] for j in range(len(w)): w[j] -= rate * (g[j] / len(X) + (penalty * w[j] if j else 0.0)) return w def score(w, cols, mu, sd, x): return w[0] + sum(w[j + 1] * (x[a] - mu[a]) / sd[a] for j, a in enumerate(cols)) def accuracy(w, cols, mu, sd, rows): return sum((squash(score(w, cols, mu, sd, x)) > 0.5) == x["suspicious"] for x in rows) / len(rows) LADDER = [("1 column", ["mean_consumption"]), ("2 columns", ["mean_consumption", "zero_readings"]), ("4 columns", ["mean_consumption", "zero_readings", "household", "satisfaction"]), ("12 columns", FIELDS)] RESULTS, tried = [], 0 print(f"{'model':<24} {'coefficients':>12} {'validation':>10} {'test':>8}") for name, cols in LADDER: mu, sd = scale(TRAIN, cols) w = train_model(TRAIN, cols, mu, sd) tried += 1 RESULTS.append((name, len(cols), accuracy(w, cols, mu, sd, VAL), accuracy(w, cols, mu, sd, TEST))) print(f"{name:<24} {len(cols):>12} {RESULTS[-1][2]:>10.4f} {RESULTS[-1][3]:>8.4f}") emu, esd = scale(EX_TRAIN, EXPANDED) for penalty in (0.0, 0.01, 0.05, 0.2): w = train_model(EX_TRAIN, EXPANDED, emu, esd, penalty=penalty) tried += 1 RESULTS.append((f"33 columns penalty {penalty}", len(EXPANDED), accuracy(w, EXPANDED, emu, esd, EX_VAL), accuracy(w, EXPANDED, emu, esd, EX_TEST))) print(f"{RESULTS[-1][0]:<24} {len(EXPANDED):>12} {RESULTS[-1][2]:>10.4f} " f"{RESULTS[-1][3]:>8.4f}") print(f"\nbaseline {BASELINE:.4f}, candidates tried {tried}") readable = max(RESULTS[:4], key=lambda t: t[2]) overall = max(RESULTS, key=lambda t: t[2]) print(f"readable candidate chosen from validation: {readable[0]}, test {readable[3]:.4f}") print(f"all candidates, chosen from validation: {overall[0]}, test {overall[3]:.4f}") print(f"price of interpretability {overall[3] - readable[3]:.4f}, that is " f"{round((overall[3] - readable[3]) * len(TEST))} subscriber")
model coefficients validation test 1 column 1 0.8254 0.7976 2 columns 2 0.8095 0.7976 4 columns 4 0.8095 0.7897 12 columns 12 0.8333 0.8016 33 columns penalty 0.0 33 0.8333 0.8056 33 columns penalty 0.01 33 0.8373 0.7976 33 columns penalty 0.05 33 0.8492 0.8056 33 columns penalty 0.2 33 0.8214 0.7937 baseline 0.7579, candidates tried 8 readable candidate chosen from validation: 12 columns, test 0.8016 all candidates, chosen from validation: 33 columns penalty 0.05, test 0.8056 price of interpretability 0.0040, that is 1 subscriber
The last line is the number that carries the title of the lesson. Choosing among the readable models by looking at the validation set gives the twelve-column model, and 0.8016 is read on the test set. Choosing among all candidates by looking at the same set gives the expanded model with a penalty, and 0.8056 is read on the test set. The price of interpretability is 0.0040, that is, one in 252 subscribers.
The smallness of this number is specific to this set and is not a general law; but what has to be read is not its size but that it was measured. When it is said that performance was given up for the sake of interpretability and the amount given up is not written, that sentence is not a measurement but a declaration of preference. Here the amount was written and its budget was put next to it: eight candidates.
The ladder itself says something separate. The one-column model beats the four-column model on validation with 0.8254, and on the test set both stay below the twelve-column one. Adding columns is not a one-way improvement, and this was the finding of the twelve lessons of the Data Preparation and Feature Engineering course.
What a Coefficient Says and Does Not Say
The twelve coefficients of the chosen readable model are read on scaled columns, and in this form they are comparable with one another.
mu, sd = scale(TRAIN, FIELDS) W = train_model(TRAIN, FIELDS, mu, sd) print(f"{'column':<16} {'coefficient':>11} (scaled columns, intercept " f"{W[0]:+.3f})") for j in sorted(range(len(FIELDS)), key=lambda j: -abs(W[j + 1])): print(f"{FIELDS[j]:<16} {W[j + 1]:>+11.3f}") print() TWIN = [("full 12 columns", FIELDS), ("per_person removed", [a for a in FIELDS if a != "per_person"]), ("mean_consumption removed", [a for a in FIELDS if a != "mean_consumption"])] print(f"{'model':<24} {'mean_consumption':>16} {'per_person':>10} {'test':>8}") for name, cols in TWIN: mu2, sd2 = scale(TRAIN, cols) w = train_model(TRAIN, cols, mu2, sd2) k = {a: w[j + 1] for j, a in enumerate(cols)} print(f"{name:<24} {k.get('mean_consumption', 0.0):>+16.3f} " f"{k.get('per_person', 0.0):>+10.3f} {accuracy(w, cols, mu2, sd2, TEST):>8.4f}") print() print(f"{'ridge penalty':>13} {'mean_consumption':>16} {'volatility':>10} " f"{'zero_readings':>13} {'test':>8}") for penalty in (0.0, 0.02, 0.1, 0.5): w = train_model(TRAIN, FIELDS, mu, sd, penalty=penalty) k = {a: w[j + 1] for j, a in enumerate(FIELDS)} print(f"{penalty:>13} {k['mean_consumption']:>+16.3f} {k['volatility']:>+10.3f} " f"{k['zero_readings']:>+13.3f} {accuracy(w, FIELDS, mu, sd, TEST):>8.4f}")
column coefficient (scaled columns, intercept -1.643)
mean_consumption +0.772
zero_readings +0.684
volatility +0.380
area_east +0.306
satisfaction -0.273
household +0.156
period +0.148
area_west -0.147
area_south -0.109
per_person +0.071
area_central -0.048
area_north -0.005
model mean_consumption per_person test
full 12 columns +0.772 +0.071 0.8016
per_person removed +0.808 +0.000 0.8016
mean_consumption removed +0.000 +0.442 0.7778
ridge penalty mean_consumption volatility zero_readings test
0.0 +0.772 +0.380 +0.684 0.8016
0.02 +0.573 +0.397 +0.523 0.7937
0.1 +0.326 +0.342 +0.310 0.7937
0.5 +0.133 +0.178 +0.127 0.7619
The first table shows what the coefficient says, and what it says is true. Mean consumption, the zero-reading indicator and volatility are in the first three places; these are exactly the three heaviest terms that the fiction used when generating the label. The sign of satisfaction is minus, that of household size is plus, and among the regions east is the highest. The coefficient of north sits at zero with -0.005; in the fiction the region contribution of north is also exactly zero. The twelve numbers give the order of the generating structure in a readable form.
The second table shows what the same coefficients do not say. In the twelve-column model per-person consumption stands near the end of the list with +0.071; someone who looks at this number concludes that the column is almost useless. When the column is removed from the model, the test number does not change at all, it stays at 0.8016; up to here the reading is correct. But when mean consumption is removed, the coefficient of the same column jumps from +0.071 to +0.442, that is, to six times. Per-person consumption was not a useless column; it looked useless while mean consumption was next to it, because the two are two forms of the same quantity, and the model divides the weight belonging to that quantity between the two as it pleases.
The third table measures the same uncertainty through the penalty. The coefficient of mean consumption falls from 0.772 to 0.326, that is, to below half; the zero-reading indicator falls from 0.684 to 0.310. The test number, on the other hand, falls from 0.8016 to 0.7937, that is, by only two subscribers. These three models are almost the same as predictions, and their coefficients hardly resemble one another. The held-out number is not enough to determine which coefficient set was chosen.
The rule that comes out of this is limited and has to be written exactly. The place of a coefficient in the ranking is readable information to the extent that the columns are independent of one another. The value of a coefficient, on the other hand, is meaningful only together with the set of columns beside it and the penalty applied. The causal reading of a coefficient does not come out of any of these tables; the Applied Statistics topic had measured that limit separately.
Summary
- The price of interpretability in this set is 0.0040, that is, one in 252 subscribers: the readable model chosen from validation gives 0.8016 on the test set, the best family chosen from the same set gives 0.8056, and the number of candidates tried is 8.
- The ladder is not one-way: the one-column model passes the four-column one on validation, and on the test set both stay below the twelve-column one.
- The twelve scaled coefficients give the order of the generating structure in the fiction correctly; the first three places are mean consumption, zero readings and volatility, and the coefficient of north sits at zero with -0.005.
- The value of a coefficient depends on the columns beside it: per-person consumption is +0.071, and it jumps to +0.442 when mean consumption is removed, that is, it rises to six times.
- The ridge penalty brings the coefficients of mean consumption and zero readings down to below half and hardly changes volatility, while the test number falls by only 0.0079; the coefficients of models that are almost the same as predictions hardly resemble one another.
Next Step
Across five lessons the model family changed but the form of the model never did. Least squares was a plane; polynomial regression bent the columns, but the decision was still a linear combination of the columns; the three penalties only shrank the coefficients of that combination; logistic regression squeezed the output of the plane, but the squeeze is a one-way increasing function, so the decision boundary is still a plane. In all five lessons the subscriber being classified fell on one side or the other according to a single weighted sum of twelve numbers.
This is a limit, and it has never been tested. In a set where the boundary itself is curved — a set where suspicion rises in a certain range of two columns and falls on both sides of the range — no plane works, and polynomial columns catch that curve only if we know its shape in advance. The next topic opens with families that do not build the decision with a plane: an instance-based procedure that decides by looking at its neighbors, and what happens to the distance measure itself as the number of columns grows.
To keep your progress and take notes, Log in
My notes
Log in to take notes.