Skip to content
academia.sh

Lesson 06 / 15

The Need for Interpretability

Counting how many of the twelve questions that can be asked about a decision are closed by the model's output, how many by an explanation, and how many by neither: the output alone closes 4 questions, the explanation raises the average to 8.58, and three questions are closed by no explanation. The one-step change at which the decision flips exists for 159 subscribers of the test set and for 2 of the thirty-two subscribers in the west region; thirteen subgroups were examined, and among the twelve that pass the threshold the number of closed questions ranges from 8.89 at best to 7.88 at worst, a gap of 1.02 questions.

Published Updated

In this lesson

  • Splits the questions that can be asked about a model decision into three: those the output closes, those an explanation closes, and those closed by neither
  • Searches for the one-step change at which the decision flips to count whether an explanation can actually be produced for a given subscriber
  • Measures the number of closed questions separately in thirteen subgroups and writes the gap between the groups in units of questions
Contents

The previous topic measured in five lessons what the number is and for whom it is: the share the split inflated, the operating point the cost matrix moved, the choice of baseline, the margin of the difference between two models, and distribution shift. In all five, what was measured was the model’s output. What the model based that output on was never asked.

This question is usually presented as a matter of preference and is generally backed by a list of requirements. No list of requirements is written here; what is measured is a function: that the rationale for a decision made about a subscriber can be documented. Documentability comes down to a concrete number. There is a set of questions that can be asked about a decision; how many of that set are answered by looking at the model’s output, how many by looking at an explanation, and how many by looking at neither. That is the measure of this lesson, and it is read separately in the subgroups.

  • IN1. The set, the label, the seed and the split are the fictional setup used throughout the first seven courses of the Machine Learning and Data Science curriculum: the subscriber table derived from the regional measurement network, the label for suspected illicit use or a fault, seed 20260218, split 756/252/252, baseline 0.7579 on the test set.
  • IN2. The model to be explained is the linear model chosen in the Supervised Learning course: twelve scaled columns, learning rate 0.3, 300 epochs, 0.8016 on the test set. In this topic the model is not chosen but taken as given; what is measured is not the model’s performance but the readability of its decision.
  • IN3. The subgroups are fixed throughout this topic and follow the four axes set up in the Evaluation Design topic: five regions, three tariff tiers (10, 25 and 40 cubic meters), three household size ranges, and two reading levels (a subscriber with readings in all three periods and one with fewer). The number of subgroups examined is 13.
  • IN4. A subgroup’s number is read only if it has at least 25 records in the test set. A group that falls below the threshold enters the table, but its number does not count as a finding.
  • IN5. A counterfactual is a one-step change in a single column. The step is one standard deviation in the training set for numeric columns, a flip of the value for two-valued columns, and a move to another region for the region. These three steps are not the same size, and that is what makes the comparison hard.
  • IN6. Consumption per person is derived from mean consumption and household size; when its source is changed it is recomputed, and it is not changed on its own.
  • IN7. A question counts as closed if its answer can be read mechanically from the model’s output or from an explanation. A question that requires expert judgment, field verification or a policy decision is open.

Questions That Can Be Asked About a Decision

When the subscriber in the setup is flagged as suspicious, the questions of the person who will listen to them on the phone, of the team that will go to the field, and of the person who will later audit the decision are not the same. The twelve questions are the union of the questions of these three people.

What the model’s output closes is four: did the subscriber receive the suspicious flag, what is the score and how close is it to the threshold, at what threshold is the decision made, and when the same row is submitted again does the same flag come out.

What an explanation can close is five: which column pushes the score most, in which direction does that column push, which single column’s one-step change flips the flag, do the other subscribers who receive the same flag rely on the same column, and in this subscriber’s subgroup is the flag rate different from the overall one.

What neither closes is three: is the flag correct, does the suspicion come from illicit use or from a fault, and what should be done after this flag. The first requires field verification, the second is already not separated in the setup’s label, and the third is a policy decision.

# interpret.py — MODEL: the FICTIONAL subscriber table of the first seven courses of the curriculum is rebuilt with the same seed;
# the twelve-column feature set and the split stay fixed throughout the course.
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),
         "region_name": k["region"][0]}
    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)


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)


def scale(train, fields):
    return ({a: statistics.fmean(x[a] for x in train) for a in fields},
            {a: max(1e-9, statistics.pstdev([x[a] for x in train])) for a in fields})


def train_model(train, fields, o, s, rate=0.3, epoch=300):    # MODEL: logistic regression
    X = [[1.0] + [(x[a] - o[a]) / s[a] for a in fields] for x in train]
    Y = [x["suspicious"] for x in train]
    w = [0.0] * len(X[0])
    for _ in range(epoch):
        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)
    return w


def score(w, fields, o, s, x):
    return squash(w[0] + sum(w[j + 1] * (x[a] - o[a]) / s[a]
                             for j, a in enumerate(fields)))


def decision(w, fields, o, s, x):
    return int(score(w, fields, o, s, x) > 0.5)


O, S = scale(TRAIN, FIELDS)
W = train_model(TRAIN, FIELDS, O, S)
ACCURACY = sum(decision(W, FIELDS, O, S, x) == x["suspicious"] for x in TEST) / len(TEST)
print(f"train {len(TRAIN)}, validation {len(VAL)}, test {len(TEST)}")
print(f"baseline {BASELINE:.4f}, test accuracy of the linear model {ACCURACY:.4f}")
train 756, validation 252, test 252
baseline 0.7579, test accuracy of the linear model 0.8016

The One Step at Which the Decision Flips

Three of the five explanation questions close for every subscriber, because the linear model’s score is the sum of one term per column, and those terms can always be read. The fourth — which single column’s one-step change flips the flag — may not close. For a subscriber who stands far enough from the decision boundary, no one-step change flips the flag, and that question goes unanswered. This is not a shortcoming of the explanation method but the certainty of that subscriber’s decision; but the result leads to the same place: that row stays empty in the rationale document.

def tariff_tier(x):
    return ("tariff 10" if x["mean_consumption"] <= 10 else
            "tariff 25" if x["mean_consumption"] <= 25 else "tariff 40")


def household_group(x):
    return "household 1-2" if x["household"] <= 2 else ("household 3-4" if x["household"] <= 4
                                                        else "household 5+")


def reading_group(x):
    return "readings few" if x["period"] <= 2 else "readings all"


def equals(f, value):
    return lambda x: f(x) == value


SUBGROUPS = ([("region " + b[0], equals(lambda x: x["region_name"], b[0])) for b in REGION]
             + [(t, equals(tariff_tier, t)) for t in ("tariff 10", "tariff 25", "tariff 40")]
             + [(h, equals(household_group, h)) for h in ("household 1-2", "household 3-4", "household 5+")]
             + [(o, equals(reading_group, o)) for o in ("readings few", "readings all")])
MIN_SIZE, SIZES = 25, {}
for name, f in SUBGROUPS:
    SIZES[name] = sum(1 for x in TEST if f(x))
SHORT = [name for name in SIZES if SIZES[name] < MIN_SIZE]
print(f"subgroup count {len(SUBGROUPS)}, threshold {MIN_SIZE} records, "
      f"below the threshold {SHORT}")

SINGLE = ["mean_consumption", "volatility", "household", "satisfaction", "period",
          "zero_readings", "region_name"]


def change(x, a, v):
    d = dict(x)
    d[a] = v
    if a in ("mean_consumption", "household"):
        d["per_person"] = round(d["mean_consumption"] / d["household"], 3)
    if a == "region_name":
        for b in REGION:
            d["area_" + b[0]] = int(v == b[0])
    return d


def counterfactual(x):               # one-step change in a single column
    k = decision(W, FIELDS, O, S, x)
    for a in SINGLE:
        if a == "zero_readings":
            candidates = [1 - x[a]]
        elif a == "region_name":
            candidates = [b[0] for b in REGION if b[0] != x["region_name"]]
        else:
            candidates = [x[a] + S[a], x[a] - S[a]]
        for v in candidates:
            if decision(W, FIELDS, O, S, change(x, a, v)) != k:
                return a
    return None


COUNTERFACTUALS = [counterfactual(x) for x in TEST]
EXAMPLE = TEST[4]
print(f"\nexample subscriber: region {EXAMPLE['region_name']}, household {EXAMPLE['household']}, "
      f"readings {EXAMPLE['period']}, mean consumption {EXAMPLE['mean_consumption']}")
print(f"  score {score(W, FIELDS, O, S, EXAMPLE):.4f}, decision "
      f"{decision(W, FIELDS, O, S, EXAMPLE)}, counterfactual column {COUNTERFACTUALS[4]}")
print(f"subscribers with a counterfactual in the whole test set "
      f"{sum(1 for k in COUNTERFACTUALS if k)}/{len(TEST)} "
      f"({sum(1 for k in COUNTERFACTUALS if k) / len(TEST):.4f})")
subgroup count 13, threshold 25 records, below the threshold ['tariff 10']

example subscriber: region central, household 4, readings 2, mean consumption 34.16
  score 0.4768, decision 0, counterfactual column mean_consumption
subscribers with a counterfactual in the whole test set 159/252 (0.6310)

The example subscriber’s score of 0.4768 is just below the threshold, and when mean consumption rises by one standard deviation the flag flips. For this subscriber, the row in the rationale document can be written. For 159 of the test set’s 252 subscribers such a row can be written; for the remaining 93 subscribers, no one-step change flips the flag.

Number of Closed Questions in the Subgroups

The 0.6310 measured overall is not read on its own. The same number is measured in thirteen subgroups separately and converted into a number of closed questions: for every subscriber, four questions are closed by the output, three by the explanation that can always be produced, the fourth explanation question closes if a counterfactual exists, and the fifth closes if all of the subscriber’s subgroups are above the threshold size.

FIXED = 4 + 3          # output closes four, three explanation questions always close


def closed(i, x):
    readable = all(SIZES[name] >= MIN_SIZE for name, f in SUBGROUPS if f(x))
    return FIXED + (1 if COUNTERFACTUALS[i] else 0) + (1 if readable else 0)


ALL = sum(closed(i, x) for i, x in enumerate(TEST)) / len(TEST)
print(f"{'subgroup':<16}{'records':>8}{'counterfactual rate':>21}{'closed questions':>18}")
ROWS = []
for name, f in SUBGROUPS:
    g = [(i, x) for i, x in enumerate(TEST) if f(x)]
    cf_rate = sum(1 for i, x in g if COUNTERFACTUALS[i]) / len(g)
    closed_mean = sum(closed(i, x) for i, x in g) / len(g)
    ROWS.append((name, closed_mean))
    print(f"{name:<16}{len(g):>8}{cf_rate:>21.4f}{closed_mean:>18.2f}")
lowest = min(ROWS, key=lambda t: t[1])
highest = max(ROWS, key=lambda t: t[1])
print(f"\nclosed questions overall {ALL:.2f}/12; output alone 4/12; "
      f"closed by no explanation 3/12")
print(f"best subgroup {highest[0]} {highest[1]:.2f}, worst subgroup {lowest[0]} "
      f"{lowest[1]:.2f}, gap {highest[1] - lowest[1]:.2f} questions")
subgroup         records  counterfactual rate  closed questions
region north          68               0.8235              8.82
region south          58               0.3793              8.31
region east           36               0.8333              8.81
region west           32               0.0625              7.88
region central        58               0.8448              8.83
tariff 10             12               0.2500              7.25
tariff 25            193               0.5907              8.59
tariff 40             47               0.8936              8.89
household 1-2         73               0.5753              8.49
household 3-4        143               0.6503              8.62
household 5+          36               0.6667              8.64
readings few          85               0.5647              8.49
readings all         167               0.6647              8.63

closed questions overall 8.58/12; output alone 4/12; closed by no explanation 3/12
best subgroup tariff 40 8.89, worst subgroup tariff 10 7.25, gap 1.64 questions

Overall, the explanation raises the number of closed questions from 4 to 8.58. In the subgroups, the same number is spread between 7.88 and 8.89 across the twelve groups that pass the threshold, and the gap is 1.02 questions; if tariff 10, which falls below the threshold, is also counted, the lower end drops to 7.25. The counterfactual rate, on the other hand, separates far more sharply: 0.8448 in the central region, 0.0625 in the west region. In the west region, the one step at which the decision flips can be shown for two of the thirty-two subscribers. The reason can be read in the setup: the west’s base consumption of 14 cubic meters is the lowest of the five regions, so the subscribers there stand far from the decision boundary and no movement of one standard deviation takes them to the other side.

Whether the decision changes is read here. Someone looking at the overall number writes the sentence “the rationale for the decisions can be documented.” Someone looking at the subgroup cannot write the same sentence for the west region; in the rationale document of the subscribers there, the counterfactual row is empty. The number of the lowest tariff tier is not even read, because it has 12 records in the test set and the threshold is 25. The number of subgroups examined is 13, and the number read is 12.

That three questions are closed by no explanation also sits in the same table, and it is the most important row for this lesson. An explanation tells how the decision was produced; it does not tell whether the decision is correct, what the source of the suspicion is, or what to do next. Closing these three questions takes not an explanation but field data, a finer label, and a written policy.

Summary

  • Of the twelve questions that can be asked about a decision, 4 can be closed by the model’s output, 5 by an explanation, and 3 by neither; the three that stay open are correctness, the source of the suspicion, and what to do next.
  • Overall, the average number of closed questions is 8.58/12; without an explanation this number stays at 4/12.
  • The one-step change at which the decision flips exists for 159 subscribers of the test set, a rate of 0.6310.
  • Thirteen subgroups were examined, and the 12 that pass the 25-record threshold were read; among the groups read, the number of closed questions ranges from 7.88 to 8.89, and the gap is 1.02 questions.
  • The counterfactual rate is 0.8448 in the central region and 0.0625 in the west region; because the west has the lowest base consumption, the subscribers there stand far from the decision boundary.

Next Step

In this lesson, three of the five explanation questions were passed over as “always closes,” and the reason was that the linear model’s score can be decomposed into one term per column. The ranking of those terms is read as the global answer to which column the model relies on, and the Data Preparation and Feature Engineering course and the Supervised Learning course had already measured that ranking. But a ranking is not produced in only one way. Measuring a column by permuting it instead of reading it from the model — shuffling its values among themselves and looking at how much performance drops — gives a second ranking, and there is no reason for the two rankings to come out the same. The next lesson puts the two rankings side by side, counts in how many ranks they disagree, and measures how mean consumption and consumption per person, two forms of the same quantity, split the importance.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close