COMP328 Fundamentals of Machine Learning

1 Learning objectives

By the end of this tutorial you will be able to

  • Run the complete eight-stage regression workflow from Tutorial 7 on a different dataset, without prompting.
  • Distinguish a feature you are permitted to use from a feature that produces a fair model, and explain why the two questions are different.
  • Identify proxy variables — features that are not themselves protected attributes but encode membership in a protected group — and describe their consequences.
  • Perform a subgroup analysis: compute performance metrics separately for groups defined by a sensitive attribute, and interpret the differences.
  • Explain why a model can be accurate on average and harmful for a subgroup, with reference to the specific metrics involved.
  • Read a coefficient table and identify which coefficients are stable enough to report and which are not.
  • Write a model card — a structured document recording a model's purpose, data, performance, limitations and intended use.
  • Decide, with justification, whether a given predictive model should be deployed, as distinct from whether it can be.

Estimated time

40 minutes of reading, plus 60–90 minutes to work through the notebook and the problem set. This is the last tutorial in Unit 4 and it is deliberately the most reflective — budget time for the discussion prompts, not just the code.

2 Motivating example

A university's teaching and learning office has a proposal.

"We lose too many students in first year. By the time we see them failing, it is too late to help. Build us a model that predicts each student's final mark at the end of week four, so we can reach out to the ones who are going to struggle."

The motivation is good. Early intervention works, and the university genuinely wants to help students who are at risk. But the request contains a decision that the requester has not noticed they are making: the model will be used to decide which students get support.

That changes everything about how the model must be evaluated. In the house price tutorial, a bad prediction cost money and could be corrected with a valuation. Here, a bad prediction costs a student their access to help — and if the model is wrong systematically for a particular group of students, it will withhold support from exactly the students who need it most, while appearing to work perfectly well on average.

This tutorial does the modelling properly, because that is the baseline standard. But it spends most of its effort on the questions that follow: is this model fair? what can its coefficients honestly be said to mean? and should it be used at all?

The two-question test

Before building any model about people, ask both of these questions. They are distinct and you need answers to both.

  • Can we? Is the data available, are the features legal, is the model technically sound?
  • Should we? Would using this model make the situation better, or would it entrench a disadvantage that already exists?

The house price model in Tutorial 7 raised the first question and answered it comfortably. This one raises the second, and the answer is genuinely not obvious.

3 Concept explanation

3.1 A reminder of the workflow

The eight stages from Tutorial 7 apply unchanged. What changes is the weight carried by stages 2, 4 and 8.

  1. Frame the problem Target, units, horizon, cost of error — and here, crucially, who will be affected by a mistake.
  2. Audit the features Not just for leakage, but for proxy variables and for features the subjects would object to being used.
  3. Split the data Same as before. Train, validation, test.
  4. Explore the training set Including the distribution of sensitive attributes, and whether the training data represents the population the model will serve.
  5. Build the pipeline Same as before. Correctness is not optional just because the problem is sensitive.
  6. Fit and evaluate on validation Now with subgroup metrics, not just overall metrics.
  7. Diagnose Residual plots overall, and residual plots per subgroup.
  8. Report — and decide A model card, a fairness analysis, and an explicit recommendation about deployment.

3.2 Permitted features and fair models

The first instinct when told to be fair is to remove the protected attribute from the feature set. Do not use gender. Do not use ethnicity. Do not use disability status. This is called fairness through unawareness, and it does not work.

It does not work because the other features carry the same information. If a student's postcode, secondary school, or the subject they study correlates with a protected attribute, then a model trained on those features can reconstruct the protected attribute's effect without ever being shown it. The protected attribute is still influencing the prediction — just indirectly, through a proxy variable.

Proxy variable

A feature that is not itself a protected attribute, but which correlates with one strongly enough that including it in a model reproduces much of the effect that including the protected attribute would have had.

Common examples in an educational setting:

Proxy variables in student prediction
Feature What it proxies Why it is a problem
Home postcode Socioeconomic status, and often ethnicity Encodes disadvantage that the student did not choose and cannot change. Predictions become partly a function of where they grew up.
Secondary school name Prior educational advantage or disadvantage Students from under-resourced schools are penalised for the resourcing of their school, not their ability.
Term-time employment hours Socioeconomic status Students who must work to support themselves are flagged as at risk precisely because they are supporting themselves.
Caring responsibilities Gender, socioeconomic status Caring roles are distributed unequally and correlate with gender.
English as an additional language Nationality, ethnicity, migration status Can encode language background as a deficit rather than a difference.
Age at entry Maturity, and indirectly socioeconomic background Older students are more likely to be mature learners with caring or work responsibilities.

None of these features is inherently illegitimate. Term-time employment is genuinely predictive of academic difficulty, and a model that ignored it would be less accurate. The problem is not that the features are wrong — it is that using them means the model is partly predicting disadvantage rather than need, and the intervention it triggers may then reinforce the disadvantage.

The proxy variable trade-off

Removing a proxy variable reduces the model's accuracy, which means fewer students get help. Keeping it means the help is distributed inequitably, reaching some students more reliably than others. There is no answer that is free of cost. The only wrong response is to make the choice without noticing that a choice was made.

3.3 Subgroup analysis

Fairness in a regression model is not one thing, and there is no single metric that captures it. What you can do is measure performance separately for each group you are concerned about, and look at the differences.

The standard approach computes the same metrics overall and per subgroup:

Metrics to compute per subgroup
Metric What a difference means
RMSE per group How large the typical error is for each group. A large gap means the model is less precise for one group.
MAE per group The same, less influenced by outliers.
Mean residual per group (bias) Whether the model systematically over- or under-predicts for a group. A non-zero mean residual is systematic error, which is far more concerning than random error.
R² per group How much of the group's variance the model explains. A low R² means the model understands that group less well.
Coverage of the intervention If the model triggers an intervention for the bottom 10% of predictions, what fraction of each group receives it? Compare with the fraction that would receive it under a perfect model.

The last row is the one that matters most in practice. A model can have equal RMSE across groups and still distribute support unequally, because the threshold that triggers support interacts with the distribution of predictions in each group.

There is no fairness metric that satisfies everyone

Several mathematically precise definitions of fairness exist, and a well-known result shows that they cannot all hold at once except in degenerate cases. Equal error rates across groups and equal selection rates across groups, for example, generally conflict — you can have one or the other, not both, unless the base rates are identical.

This is not a reason to give up. It is a reason to decide which notion of fairness matters for this specific deployment, state the choice explicitly, and accept the trade-off knowingly. Module 6 covers this in detail; the purpose here is to make you suspicious of anyone who claims a model is simply "fair".

3.4 What a coefficient means when the data is about people

Tutorial 2 established that a coefficient is a partial association, not a causal effect, and Tutorial 7 warned against reading it causally. In an educational context the stakes are higher, and three specific misreadings are common.

Three misreadings of an educational coefficient
Misreading Why it is wrong
"Term-time work reduces marks by 4 points per 10 hours." The coefficient describes students who work and students who do not. Those two groups differ in many ways the model does not measure. The coefficient is not evidence that reducing a student's hours would raise their mark — and in fact a student who cut their hours might simply have less money and more stress.
"Attendance is the biggest factor — it has the largest coefficient." A larger coefficient may simply reflect a feature with a wider range or smaller standard deviation. Standardised coefficients are comparable; raw ones are not. And even a standardised coefficient is not a claim about what would happen if you intervened on that feature.
"We control for prior attainment, so the remaining effect of school type is real." Controlling for a variable that is itself a consequence of disadvantage removes part of the very effect you are trying to measure. Prior attainment is not a neutral baseline — it is a summary of everything that came before, including the disadvantage.

The safe framing is always the same: in this dataset, among students with these measured characteristics, this feature was associated with a difference of this size in the predicted outcome. That statement is defensible. Anything stronger is not.

3.5 The model card

A model card is a short structured document that accompanies a model and records what it is for, how it was built, how well it works, and where it should not be used. It is the deliverable that turns a notebook into something an organisation can govern.

For a regression model of this kind, a model card should contain at least:

  1. Purpose — what decision the model supports, and for whom.
  2. Intended use — the specific context in which it may be used.
  3. Out-of-scope use — contexts in which it must not be used.
  4. Data — the population it was built from, and how representative that is of the population it will serve.
  5. Features — what was included, and what was excluded and why.
  6. Performance — overall metrics on held-out data, and the baseline comparison.
  7. Subgroup performance — the same metrics computed for each group of concern.
  8. Known limitations — the failure modes the developer has identified.
  9. Ethical considerations — the proxy variable decisions made, and the reasoning behind them.
  10. Monitoring plan — what would indicate that the model has stopped working, and what would trigger a review.

Two of these are routinely omitted and both are consequential. Out-of-scope use prevents a model built for one purpose being quietly repurposed for another. And the monitoring plan is what turns a one-off analysis into a system that can be held accountable over time.

3.6 Deciding whether to deploy

A model can be technically sound, well evaluated, honestly reported and still not worth deploying. The final judgement is not statistical.

A workable decision procedure:

  1. Is it accurate enough to help? If the errors are larger than the intervention's benefit, the model cannot improve on doing nothing.
  2. Does it beat the alternative? Compare against what the university does now, not against a perfect model. If the current process is "wait until they fail", a moderate model may be a clear improvement.
  3. Is it fair enough? Are the subgroup differences small enough that the intervention reaches each group in proportion to need? If not, can the threshold be adjusted to compensate?
  4. What happens when it is wrong? If a student is wrongly flagged, do they receive an unwelcome label? If a student is wrongly missed, do they lose access to help they would otherwise have received?
  5. Can it be explained? If a student asks why they were flagged, is there an answer that respects them as a person rather than as a data point?
  6. Can it be monitored and withdrawn? Is there a plan for detecting degradation, and a route to switching it off if it causes harm?

The honest answer to a well-built model in a sensitive domain is often "yes, but with these specific safeguards and a review in six months". That is a legitimate professional answer and it belongs in the report.

4 Visual illustration

Diagram showing how a proxy variable transmits the effect of a removed protected attribute Two panels. In the top panel, a protected attribute such as socioeconomic background connects directly to the outcome and also to a proxy feature such as home postcode. In the bottom panel, the protected attribute has been removed from the model, but the proxy feature still carries much of its effect, so the prediction remains influenced by the protected attribute. Protected attribute included directly Socioeconomic background home postcode final mark (prediction) direct influence on the outcome Protected attribute removed — proxy remains Socioeconomic background removed from model home postcode final mark (prediction) correlated the outcome is still influenced removing a feature is not the same as removing its effect
Figure 1. Fairness through unawareness does not work. Removing the protected attribute from the feature set removes the direct arrow, but the proxy feature still carries a substantial part of the same information into the prediction. The model is unaware of the attribute; the outcome is not.
Overall performance compared with performance for each subgroup Two panels. The left panel shows a single overall R-squared value of 0.71 for the whole dataset. The right panel shows the same model evaluated separately for four student groups, with R-squared values ranging from 0.79 for one group down to 0.41 for another, and a systematic residual bias for one group. Overall performance 0.71 R² across all students RMSE 9.4 marks looks like a reasonable model Same model, by subgroup 0.79 0.72 0.58 0.41 group A group B group C group D group D mean residual = −6.2 marks the model systematically over-predicts this group the average hides a four-fold spread
Figure 2. The same model, evaluated two ways. On the left, an R² of 0.71 that would satisfy most practitioners. On the right, the same model evaluated per subgroup: R² ranges from 0.79 to 0.41, and for one group the model systematically over-predicts by more than six marks. A model can be adequate on average and unreliable for a group that already faces disadvantage.
How a single threshold distributes support unequally across groups Two overlapping prediction distributions, one for each of two groups. A vertical line marks the threshold below which a student is flagged for support. Although both groups have the same underlying need, the group whose predictions are shifted upward receives proportionally less support because fewer of its members fall below the threshold. support threshold group A group B 18% flagged 4% flagged Equal need does not produce equal support when predictions are shifted predicted final mark →
Figure 3. The mechanism by which a fair-seeming model produces unequal support. Both groups contain students who need help. But the intervention is triggered by a threshold on the prediction, and if the model's predictions are shifted upward for one group — for instance because its members tend to attend better-resourced schools — then fewer of its members fall below the threshold. The model does not need to be biased in its error to distribute support unequally.
Six-question decision procedure for deploying a model in a sensitive domain A vertical sequence of six questions, each with a yes and no branch. The questions concern accuracy, comparison with the current process, fairness across subgroups, consequences of errors, explainability, and monitoring. If any question is answered no, the model should not be deployed without addressing that point. 1 · Is it accurate enough to help? yes → 2 · Does it beat the current process? yes → 3 · Is it fair enough across groups? review 4 · What happens when it is wrong? assess 5 · Can it be explained to a student? yes → 6 · Can it be monitored and withdrawn? yes → deploy with review A "no" anywhere is not a veto — it is a condition to be addressed, documented and reviewed
Figure 4. The decision to deploy is a sequence of questions, none of them statistical. A model can pass every technical check and still fail question 4 or 6, and a failure at question 3 does not necessarily mean the model should be abandoned — it means the threshold or the feature set has to be reconsidered, and the reasoning recorded.

5 Worked example

The dataset is synthetic: 900 first-year undergraduate records, with a mixture of prior attainment, engagement and demographic features. It has been constructed to contain a genuine proxy variable, so that the fairness analysis has something real to find. The workflow follows Tutorial 7 exactly, with the additional steps marked.

Step 1 — Frame the problem, with the affected party named

Target: the student's final module mark, on a 0–100 scale.

Horizon: predicted at the end of week four, using only information available by then.

Decision supported: which students are invited to an optional study-skills programme. The invitation is not compulsory and the programme is not recorded on a transcript.

Cost of error: asymmetric in a specific way. A false positive (a student flagged who would have been fine) costs a wasted invitation and a small amount of the student's time. A false negative (a student not flagged who then struggles) costs the student access to support at the moment it would have helped. Both matter, but the second is worse.

Who is affected: every first-year student in the cohort. The students are not customers; they are the subjects of a decision made about them by an institution that holds power over their education.

The framing changes the standard of evidence

In Tutorial 7, the affected party was a seller who could ignore the estimate. Here they cannot. A student who is wrongly not invited does not know the model exists, does not know they were missed, and cannot appeal. That asymmetry is what raises the standard of evidence the model has to meet.

Step 2 — Audit the features, with proxy variables in view

available_columns = [
    # prior attainment
    "prior_qualification_points",   # UCAS-style points
    "entry_maths_grade",

    # engagement (weeks 1–4)
    "vle_logins_weeks_1_4",
    "assignment_1_mark",
    "attendance_pct_weeks_1_4",

    # context
    "term_time_work_hours",
    "caring_responsibilities",
    "commute_minutes",
    "home_postcode_area",           # reduced to outward code
    "secondary_school_type",        # state / independent / FE college

    # demographics
    "age_at_entry",
    "gender",
    "ethnicity",
    "disability_status",
    "eal_flag",                     # English as an additional language

    # NOT available at week 4
    "final_mark",                   # the target
    "exam_board_flag",              # set at the end of the year
]

Applying the leakage test from Tutorial 7 removes final_mark (the target) and exam_board_flag (set at the end of the year). That part is straightforward.

The harder decisions concern the context features. None of them is a protected attribute. All of them are available at week four. But three of them — term_time_work_hours, home_postcode_area and secondary_school_type — are strongly associated with socioeconomic background, and one of them, caring_responsibilities, is associated with gender. They are proxy variables.

The proxy variable decisions
Feature Decision Reasoning
gender, ethnicity, disability_status, eal_flag Exclude from model, retain for analysis Using a protected attribute directly as a predictor is not appropriate. But these columns must be kept in the dataset so that subgroup analysis is possible — otherwise the model's fairness cannot be measured at all.
term_time_work_hours Include, but flag Genuinely predictive of academic difficulty, and the student can report it. But flagging a student as "at risk" partly because they work is close to penalising them for needing an income. Retained, and noted in the model card.
home_postcode_area Exclude A direct proxy for socioeconomic status that adds little predictive value once prior attainment is included. Removing it costs a small amount of accuracy and removes a substantial source of inequity.
secondary_school_type Include, but flag Predictive, but encodes the resourcing of the school the student attended rather than the student's own preparation. Retained for now; to be tested in the fairness analysis.
caring_responsibilities Include, but flag Strongly predictive and highly relevant to whether the student can attend an optional programme. Retained, with a note that the intervention design must account for it.
Prior attainment and engagement features Include The core of the model. These describe what the student has done, not who they are.

Two features are excluded on fairness grounds, and four protected attributes are retained for analysis but excluded from the model. Those four retained columns are essential — a model whose fairness cannot be measured is a model whose fairness cannot be claimed.

Step 3 — Split, and check representativeness

X_train, X_temp, y_train, y_temp = train_test_split(
    X, y, test_size=0.40, random_state=42, stratify=df["ethnicity"]
)
X_val, X_test, y_val, y_test = train_test_split(
    X_temp, y_temp, test_size=0.50, random_state=42,
    stratify=X_temp["ethnicity"]
)

# Keep the sensitive attributes aligned with the splits for later analysis
sensitive_train = X_train[PROTECTED].copy()
sensitive_val   = X_val[PROTECTED].copy()

# Then drop them from the model features
X_train_model = X_train.drop(columns=PROTECTED)
X_val_model   = X_val.drop(columns=PROTECTED)

Two details here are new.

  • Stratified splitting. With a smaller dataset, an unstratified random split can produce a validation set with almost no students from a minority group, which makes subgroup metrics meaningless. Stratifying on the sensitive attribute preserves the group proportions across splits.
  • Sensitive attributes are separated before dropping. They are removed from the model's feature set, but kept alongside so that the analysis in Step 7 is possible. If you delete them from the dataframe entirely at the start, you cannot check the model afterwards.

It is also worth checking whether the training data resembles the population the model will serve. If the cohort is 62% female and the training data is 71%, the model will be less well calibrated for male students — not because of any modelling error, but because there is less data to learn from.

Step 4 — Explore, and look for the structure that will drive the fairness result

print(y_train.describe())
print(f"skewness: {y_train.skew():.2f}")

# Distribution of the sensitive attributes in the training set
for col in PROTECTED:
    print(f"\n{col}:")
    print(X_train[col].value_counts(normalize=True).round(3))

# Do the proxy features differ across groups?
print("\nmean term_time_work_hours by ethnicity:")
print(X_train.groupby("ethnicity")["term_time_work_hours"].mean().round(1))

print("\nmean secondary_school_type == 'independent' by ethnicity:")
print((X_train.groupby("ethnicity")["secondary_school_type"]
       .apply(lambda s: (s == "independent").mean())).round(3))
final mark:  mean 63.4   std 13.8   min 21   max 96
skewness: -0.21

ethnicity:
  group_a   0.612
  group_b   0.198
  group_c   0.112
  group_d   0.078

mean term_time_work_hours by ethnicity:
  group_a   8.2
  group_b   9.1
  group_c  13.4
  group_d  15.7

mean secondary_school_type == 'independent' by ethnicity:
  group_a   0.241
  group_b   0.152
  group_c   0.071
  group_d   0.042

This is the structure that will produce a fairness problem. Students in group D work nearly twice as many hours as students in group A, and are six times less likely to have attended an independent school. Both features are in the model. Both are predictive of a lower mark. Neither is a protected attribute.

The model has no idea that group D exists. It is about to learn that working long hours and attending a state school predict lower marks — and it will then apply that learning uniformly, which means it will flag group D students more often than group A students, for reasons that have nothing to do with their individual ability.

Step 5 — Build the pipeline and fit

numeric_cols = [
    "prior_qualification_points", "entry_maths_grade",
    "vle_logins_weeks_1_4", "assignment_1_mark",
    "attendance_pct_weeks_1_4", "term_time_work_hours",
    "commute_minutes", "age_at_entry",
]
categorical_cols = [
    "secondary_school_type", "caring_responsibilities",
]

numeric_pipe = Pipeline([
    ("impute", SimpleImputer(strategy="median")),
    ("scale",  StandardScaler()),
])
categorical_pipe = Pipeline([
    ("impute", SimpleImputer(strategy="most_frequent")),
    ("onehot", OneHotEncoder(handle_unknown="ignore", drop="first")),
])

preprocess = ColumnTransformer([
    ("num", numeric_pipe,     numeric_cols),
    ("cat", categorical_pipe, categorical_cols),
])

model = Pipeline([
    ("preprocess", preprocess),
    ("regressor",  LinearRegression()),
])

model.fit(X_train_model, y_train)

Nothing here is new. The pipeline is structurally identical to Tutorial 7's, with a smaller feature set.

Step 6 — Evaluate overall, then per subgroup

from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score

def group_metrics(y_true, y_pred, label):
    rmse = np.sqrt(mean_squared_error(y_true, y_pred))
    mae  = mean_absolute_error(y_true, y_pred)
    r2   = r2_score(y_true, y_pred)
    bias = (y_true - y_pred).mean()          # mean residual — systematic error
    n    = len(y_true)
    print(f"{label:<18} n={n:>4}  RMSE {rmse:>5.2f}  MAE {mae:>5.2f}  "
          f"R² {r2:>6.3f}  bias {bias:>+6.2f}")

y_pred_val = model.predict(X_val_model)

print("=== OVERALL ===")
group_metrics(y_val, y_pred_val, "all students")

print("\n=== BY ETHNICITY ===")
for group in sorted(sensitive_val["ethnicity"].unique()):
    mask = sensitive_val["ethnicity"] == group
    group_metrics(y_val[mask], y_pred_val[mask], f"  {group}")

print("\n=== BY DISABILITY STATUS ===")
for group in sorted(sensitive_val["disability_status"].unique()):
    mask = sensitive_val["disability_status"] == group
    group_metrics(y_val[mask], y_pred_val[mask], f"  {group}")
=== OVERALL ===
all students       n= 180  RMSE  9.42  MAE  7.51  R²  0.714  bias  +0.00

=== BY ETHNICITY ===
  group_a          n= 110  RMSE  8.88  MAE  7.10  R²  0.742  bias  +0.62
  group_b          n=  36  RMSE  9.71  MAE  7.68  R²  0.702  bias  −0.51
  group_c          n=  20  RMSE 10.94  MAE  8.72  R²  0.618  bias  −1.84
  group_d          n=  14  RMSE 12.31  MAE  9.96  R²  0.412  bias  −6.21

=== BY DISABILITY STATUS ===
  no               n= 152  RMSE  9.28  MAE  7.42  R²  0.722  bias  +0.18
  yes              n=  28  RMSE 10.02  MAE  7.98  R²  0.668  bias  −0.94

The overall model looks reasonable: RMSE 9.42 marks, R² 0.71, no overall bias. A report that stopped here would be misleading in three specific ways.

  • R² ranges from 0.74 to 0.41 across ethnic groups. The model explains less than half the variance for group D. It understands that group substantially less well than the others.
  • Group D has a mean residual of −6.21 marks. The model systematically under-predicts group D — it predicts marks that are on average six marks lower than the students actually achieve. This is not random error; it is a consistent directional error, and it is the most serious finding in the table.
  • Group D has the smallest sample. Fourteen students in the validation set. The estimates for this group are themselves uncertain, and a responsible analysis would report confidence intervals and note the small n rather than treating the numbers as precise.

What the mean residual means for a student

The model under-predicts group D by six marks on average. In a system where the bottom 10% of predictions are flagged for support, an under-prediction of six marks moves a student closer to that threshold. Group D students will be flagged for support more often than their actual performance warrants — and while being offered support may sound benign, the flagging is also the mechanism by which the institution forms an impression of who is struggling. The model is not neutral about group D. It is making a claim about them that is, on average, too low.

Step 7 — Trace the bias to its source

A subgroup difference is a symptom. To act on it, the cause has to be identified. Two checks:

# 1. How does the model distribute support across groups?
threshold = np.percentile(y_pred_val, 10)     # bottom 10% of predictions
flagged = y_pred_val <= threshold

print(f"threshold: {threshold:.1f} marks\n")
print("group   n    flagged   rate    actual_bottom10   over_flag")
for group in sorted(sensitive_val["ethnicity"].unique()):
    mask   = (sensitive_val["ethnicity"] == group).values
    n_grp  = mask.sum()
    n_flag = (flagged & mask).sum()
    # what fraction of this group is genuinely in the bottom 10% of actual marks?
    actual = (y_val <= np.percentile(y_val, 10)).values
    actual_rate = (actual & mask).sum() / n_grp
    print(f"{group:<7} {n_grp:>3}   {n_flag:>4}    "
          f"{n_flag/n_grp:>5.1%}   {actual_rate:>5.1%}          "
          f"{n_flag/n_grp - actual_rate:>+6.1%}")

# 2. Refit without the flagged proxy features and compare subgroup bias
proxy_cols = ["term_time_work_hours", "secondary_school_type"]
model_noproxy = Pipeline([
    ("preprocess", ColumnTransformer([
        ("num", numeric_pipe,
         [c for c in numeric_cols if c not in proxy_cols]),
        ("cat", categorical_pipe,
         [c for c in categorical_cols if c not in proxy_cols]),
    ])),
    ("regressor", LinearRegression()),
]).fit(X_train_model, y_train)

y_pred_np = model_noproxy.predict(X_val_model)

print("\n=== group D bias, with and without proxy features ===")
for label, preds in [("with proxies", y_pred_val), ("without proxies", y_pred_np)]:
    mask = (sensitive_val["ethnicity"] == "group_d").values
    bias = (y_val[mask] - preds[mask]).mean()
    r2   = r2_score(y_val[mask], preds[mask])
    print(f"{label:<18} bias {bias:>+6.2f}   R² {r2:>6.3f}")
threshold: 50.5 marks

group   n    flagged   rate    actual_bottom10   over_flag
group_a 110      6       5.5%      4.5%            +1.0%
group_b  36      4      11.1%      8.3%            +2.8%
group_c  20      4      20.0%     15.0%            +5.0%
group_d  14      4      28.6%     21.4%            +7.2%

=== group D bias, with and without proxy features ===
with proxies       bias  -6.21   R²  0.412
without proxies    bias  -2.08   R²  0.298

Two findings, and they point in opposite directions.

  • The support is distributed unequally. Group D students are flagged at 28.6% versus 5.5% for group A. Part of that is genuine need — 21.4% of group D are in the actual bottom 10%, compared with 4.5% of group A. But the over-flagging is also unequal: +7.2 percentage points for group D, +1.0 for group A.
  • Removing the proxy features reduces the bias but also the accuracy. Group D's mean residual shrinks from −6.21 to −2.08 — a substantial improvement. But R² for group D falls from 0.41 to 0.30, and the overall model's R² falls as well. The model is now fairer and less useful.

This is the trade-off from Section 3.2, made concrete with numbers. There is no version of this model that is both as accurate and as fair as the alternatives. Which one to deploy depends on which cost is judged more serious — and that is not a statistical question.

Step 8 — Read the coefficients, with the correlation structure in mind

regressor = model.named_steps["regressor"]
features  = model.named_steps["preprocess"].get_feature_names_out()

coef_df = pd.DataFrame({
    "feature": features,
    "coef":    regressor.coef_,
})

# Standardised features: coefficients are per standard deviation
# Convert to per-original-unit by dividing by the feature std dev
stds = X_train_model[numeric_cols].std()
for name, sd in stds.items():
    key = f"num__{name}"
    coef_df.loc[coef_df["feature"] == key, "coef"] /= sd

print(coef_df.sort_values("coef", key=abs, ascending=False).to_string(index=False))

# How stable are these? Refit on 20 bootstrap samples and look at the spread.
boots = []
for seed in range(20):
    idx = np.random.RandomState(seed).choice(len(X_train_model), len(X_train_model), replace=True)
    m = clone(model).fit(X_train_model.iloc[idx], y_train.iloc[idx])
    boots.append(m.named_steps["regressor"].coef_)
boots = np.array(boots)

coef_df["std_over_boot"] = boots.std(axis=0)
coef_df["stability"] = coef_df["coef"].abs() / coef_df["std_over_boot"].replace(0, np.nan)

print("\ncoefficient stability (|coef| / bootstrap sd):")
print(coef_df[["feature", "coef", "std_over_boot", "stability"]]
      .sort_values("stability").to_string(index=False))
              feature   coef
num__assignment_1_mark  0.412
num__attendance_pct_weeks_1_4  0.287
num__prior_qualification_points  0.196
num__vle_logins_weeks_1_4  0.121
num__term_time_work_hours  -0.108
cat__secondary_school_type_independent  0.094
num__entry_maths_grade  0.063
num__commute_minutes  -0.041
num__age_at_entry  0.019

coefficient stability (|coef| / bootstrap sd):
              feature   coef   sd_over_boot   stability
num__age_at_entry  0.019        0.031          0.61
num__commute_minutes  -0.041        0.038          1.08
num__entry_maths_grade  0.063        0.041          1.54
cat__secondary_school_type_independent  0.094        0.052          1.81
num__term_time_work_hours  -0.108        0.044          2.45
num__vle_logins_weeks_1_4  0.121        0.039          3.10
num__prior_qualification_points  0.196        0.036          5.44
num__attendance_pct_weeks_1_4  0.287        0.029          9.90
num__assignment_1_mark  0.412        0.022         18.73

Three coefficients are stable enough to report with confidence: assignment 1 mark, attendance, and prior qualification points. They are the core of the model and their signs and magnitudes are robust.

Two coefficients are not stable: age_at_entry and commute_minutes. Their bootstrap standard deviations are comparable to their point estimates, meaning that a different sample of the same population could easily produce a coefficient of the opposite sign. They should not appear in any report, and they should not be used to justify a decision. If the module leader asks "does commuting hurt marks?", the honest answer is that this dataset cannot say.

Stability is not the same as causality

The attendance coefficient is stable at 0.287 marks per percentage point of attendance, and it is the second-largest in the model. That is a robust association in this dataset. It is not evidence that attending more lectures would raise a student's mark by that amount. Students who attend more may differ in motivation, prior preparation, health, or caring responsibilities in ways the model does not measure. A stable coefficient is one you can report; it is not one you can act on causally.

Step 9 — Evaluate on the test set, once

y_pred_test = model.predict(X_test_model)

print("=== TEST SET ===")
group_metrics(y_test, y_pred_test, "all students")

print("\nby ethnicity:")
for group in sorted(X_test["ethnicity"].unique()):
    mask = (X_test["ethnicity"] == group).values
    if mask.sum() >= 5:
        group_metrics(y_test[mask], y_pred_test[mask], f"  {group}")
=== TEST SET ===
all students       n= 180  RMSE  9.58  MAE  7.63  R²  0.701  bias  +0.11

by ethnicity:
  group_a          n= 110  RMSE  8.95  MAE  7.18  R²  0.729  bias  +0.44
  group_b          n=  36  RMSE  9.88  MAE  7.79  R²  0.688  bias  −0.62
  group_c          n=  20  RMSE 11.20  MAE  8.91  R²  0.601  bias  −2.01
  group_d          n=  14  RMSE 12.64  MAE 10.12  R²  0.398  bias  −6.48

The test results confirm the validation findings, including the group D bias. The pattern is not an artefact of the validation split — it is a genuine property of the model.

Step 10 — Write the model card

Model card — first-year module mark prediction

Purpose

To identify first-year students who would benefit from an optional study-skills programme, at the end of week four, so that invitations can be issued before the first assessment period.

Intended use

As one input to a human decision about which students to invite. The invitation is optional, unrecorded on the transcript, and does not affect assessment.

Out-of-scope use

  • Must not be used to grade, rank, or make progression decisions.
  • Must not be shared with individual students as a forecast of their result.
  • Must not be applied to students outside the first-year cohort it was built on.
  • Must not be used beyond week eight — its features are all from weeks 1–4.

Data

900 first-year undergraduate records from a single institution, 2022–2024 entry. The cohort is 61% group A, 20% group B, 11% group C and 8% group D by ethnicity. The dataset is small for subgroup analysis: group D has 14 students in each of the validation and test sets, so the subgroup metrics carry substantial uncertainty.

Features

Prior attainment, week 1–4 engagement and a small number of context features. home_postcode_area was excluded as a socioeconomic proxy that added little predictive value. term_time_work_hours, caring_responsibilities and secondary_school_type were retained despite being proxies, because they are strongly predictive and directly relevant to whether a student can attend an optional programme. gender, ethnicity, disability_status and eal_flag were excluded from the model and retained for analysis only.

Performance (held-out test set, n = 180)

Group n RMSE MAE R² Mean residual
All students1809.587.630.701+0.11
Group A1108.957.180.729+0.44
Group B369.887.790.688−0.62
Group C2011.208.910.601−2.01
Group D1412.6410.120.398−6.48

Mean baseline RMSE: 13.82 marks. The model reduces error by about 31%.

Known limitations

  • Systematic under-prediction for group D. The model predicts marks approximately 6.5 marks lower than students in group D actually achieve. This is the most serious limitation and the reason for the deployment conditions below.
  • Lower R² for groups C and D, indicating the model understands these groups less well.
  • Small subgroup sample sizes make the per-group estimates uncertain.
  • Built on a single institution's data and may not transfer.
  • The two largest coefficients — assignment 1 mark and attendance — are robust associations, not causal effects. Attending more lectures will not mechanically raise a mark by 0.29 per percentage point.

Ethical considerations

Three features in the model are socioeconomic or gender proxies. Removing them reduces group D's bias from −6.21 to −2.08 marks but reduces overall R² from 0.714 to approximately 0.60. The team judged that the accuracy cost was worth paying if the deployment conditions below are met, and recommends re-testing without the proxies after one academic year of operation.

Deployment conditions

  • Support must be offered, not withheld. The model may be used to invite students to an optional programme. It must not be used to exclude anyone from any provision.
  • Group D students flagged by the model must be reviewed individually before any action is taken, given the systematic bias identified.
  • No prediction is shown to a student. The output is an invitation, not a forecast.
  • Monitoring plan: subgroup metrics to be recomputed each semester. A group D mean residual below −4 marks, or an R² gap above 0.25 between any two groups, triggers a review and possible withdrawal.
  • Review date: end of the current academic year, with a decision on whether to refit without the proxy features.

Recommendation

Deploy in a limited pilot with the conditions above, on the basis that the current process — waiting until students fail — is worse for every group. The pilot must include an independent review of the group D findings before any expansion.

The complete code

import numpy as np
import pandas as pd

from sklearn.base import clone
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler

RANDOM_STATE = 42
TARGET = "final_mark"

PROTECTED = ["gender", "ethnicity", "disability_status", "eal_flag"]

NUMERIC = [
    "prior_qualification_points", "entry_maths_grade",
    "vle_logins_weeks_1_4", "assignment_1_mark",
    "attendance_pct_weeks_1_4", "term_time_work_hours",
    "commute_minutes", "age_at_entry",
]
CATEGORICAL = ["secondary_school_type", "caring_responsibilities"]

EXCLUDE_PROXY = ["home_postcode_area"]   # dropped on fairness grounds

# ------------------------------------------------------------------
# Load
# ------------------------------------------------------------------
df = pd.read_csv("student_cohort.csv")

y = df[TARGET]
sensitive = df[PROTECTED].copy()

feature_cols = NUMERIC + CATEGORICAL + PROTECTED   # keep protected for splitting
X_raw = df[feature_cols + EXCLUDE_PROXY]
X_raw = X_raw.drop(columns=EXCLUDE_PROXY)

# ------------------------------------------------------------------
# Split (stratified on ethnicity so subgroups survive the split)
# ------------------------------------------------------------------
X_train, X_temp, y_train, y_temp = train_test_split(
    X_raw, y, test_size=0.40, random_state=RANDOM_STATE,
    stratify=df["ethnicity"],
)
X_val, X_test, y_val, y_test = train_test_split(
    X_temp, y_temp, test_size=0.50, random_state=RANDOM_STATE,
    stratify=X_temp["ethnicity"],
)

# Keep sensitive columns aligned, then drop them from the model features
sens_train = X_train[PROTECTED].reset_index(drop=True)
sens_val   = X_val[PROTECTED].reset_index(drop=True)
sens_test  = X_test[PROTECTED].reset_index(drop=True)

X_train_m = X_train.drop(columns=PROTECTED)
X_val_m   = X_val.drop(columns=PROTECTED)
X_test_m  = X_test.drop(columns=PROTECTED)

# ------------------------------------------------------------------
# Pipeline
# ------------------------------------------------------------------
numeric_pipe = Pipeline([
    ("impute", SimpleImputer(strategy="median")),
    ("scale",  StandardScaler()),
])
categorical_pipe = Pipeline([
    ("impute", SimpleImputer(strategy="most_frequent")),
    ("onehot", OneHotEncoder(handle_unknown="ignore", drop="first")),
])

preprocess = ColumnTransformer([
    ("num", numeric_pipe,     NUMERIC),
    ("cat", categorical_pipe, CATEGORICAL),
])

model = Pipeline([
    ("preprocess", preprocess),
    ("regressor",  LinearRegression()),
]).fit(X_train_m, y_train)

# ------------------------------------------------------------------
# Metrics, overall and per subgroup
# ------------------------------------------------------------------
def group_metrics(y_true, y_pred, label):
    rmse = np.sqrt(mean_squared_error(y_true, y_pred))
    mae  = mean_absolute_error(y_true, y_pred)
    r2   = r2_score(y_true, y_pred)
    bias = (y_true - y_pred).mean()
    print(f"{label:<22} n={len(y_true):>4}  RMSE {rmse:>5.2f}  "
          f"MAE {mae:>5.2f}  R² {r2:>6.3f}  bias {bias:>+6.2f}")

y_pred_val = model.predict(X_val_m)

group_metrics(y_val, y_pred_val, "all students")
for col in ["ethnicity", "disability_status", "gender"]:
    print(f"\n--- by {col} ---")
    for grp in sorted(sens_val[col].unique()):
        mask = (sens_val[col] == grp).values
        if mask.sum() >= 5:
            group_metrics(y_val[mask], y_pred_val[mask], f"  {grp}")

# ------------------------------------------------------------------
# Coefficients with bootstrap stability
# ------------------------------------------------------------------
regressor = model.named_steps["regressor"]
features  = model.named_steps["preprocess"].get_feature_names_out()

coef_df = pd.DataFrame({"feature": features, "coef": regressor.coef_})

# Convert numeric coefficients to per-original-unit
for name in NUMERIC:
    key = f"num__{name}"
    coef_df.loc[coef_df["feature"] == key, "coef"] /= X_train_m[name].std()

# Bootstrap stability
boots = []
for seed in range(20):
    idx = np.random.RandomState(seed).choice(
        len(X_train_m), len(X_train_m), replace=True
    )
    m = clone(model).fit(X_train_m.iloc[idx], y_train.iloc[idx])
    boots.append(m.named_steps["regressor"].coef_)

coef_df["boot_sd"]   = np.array(boots).std(axis=0)
coef_df["stability"] = coef_df["coef"].abs() / coef_df["boot_sd"].replace(0, np.nan)

print("\ncoefficients, ordered by stability:")
print(coef_df.sort_values("stability")[
    ["feature", "coef", "boot_sd", "stability"]
].to_string(index=False))

# ------------------------------------------------------------------
# Final evaluation on the test set
# ------------------------------------------------------------------
y_pred_test = model.predict(X_test_m)

print("\n=== TEST SET ===")
group_metrics(y_test, y_pred_test, "all students")
for grp in sorted(sens_test["ethnicity"].unique()):
    mask = (sens_test["ethnicity"] == grp).values
    if mask.sum() >= 5:
        group_metrics(y_test[mask], y_pred_test[mask], f"  {grp}")
nb-08 · Student grade prediction, with fairness analysis The complete notebook, including the subgroup metric functions, the support-distribution analysis, the bootstrap stability check, and a template model card.

6 Interactive activity

Make the trade-off yourself

In the Tutorial 8 notebook, the fairness–accuracy trade-off is not abstract. You will fit the model with four different feature sets and measure both the overall accuracy and the group D bias in each case. Your task is to decide which one to recommend, and to defend the decision in writing.

  1. Model A — full feature set. All features except the protected attributes. This is what the worked example used.
  2. Model B — no socioeconomic proxies. Remove term_time_work_hours, secondary_school_type and caring_responsibilities.
  3. Model C — proxy-free and threshold-adjusted. Use Model B's features, but choose the support threshold so that the selection rate for each group matches the group's rate in the actual bottom 10%.
  4. Model D — accuracy-focused. Model A's features plus the postcode area that was excluded on fairness grounds. This is the highest-accuracy version available.

For each model, record:

  • Overall RMSE, MAE, R² on the validation set.
  • R² and mean residual per ethnic group.
  • The percentage of each group flagged for support, and the corresponding actual rate.
  • The number of students who would be flagged under each model and would not be flagged under the others.

Then write 200 words recommending one of the four, on the assumption that the pilot runs for one semester and will be reviewed. State explicitly which cost you are accepting, and what evidence would make you change your recommendation.

Show the discussion

There is no uniquely correct answer, but there are better and worse arguments. The four models occupy different positions on the trade-off, and a defensible recommendation does three things: it names the metric it is optimising, it acknowledges what it is giving up, and it states a review condition.

  • Model A — best overall accuracy, worst group D bias. Defensible only with a strong monitoring plan and the deployment conditions from the model card.
  • Model B — reduced bias, reduced accuracy. Defensible if the group D bias is judged more consequential than the overall error. The right choice if the intervention is only useful when it reaches the right students.
  • Model C — the most sophisticated option. Adjusting the threshold is a legitimate remedy: it does not make the underlying predictions less biased, but it changes how the bias translates into a decision. The weakness is that a group-specific threshold is harder to explain and harder to monitor than a single rule, and it may itself be seen as unfair by students who learn about it.
  • Model D — highest accuracy, most severe proxy problem. Hard to defend. A model that uses postcode to predict educational outcomes is making predictions that encode historical inequity, and the accuracy gain is unlikely to justify it.

The strongest answers are the ones that engage seriously with Model B or C, since those are the options that take the fairness finding seriously without abandoning the model's purpose. A recommendation for Model A is defensible but requires a detailed answer to the question "what happens to a group D student who is wrongly flagged?"

The point of the exercise

The exercise forces you to make an explicit choice between values that conflict. There is no version of this model that is best on every measure, and pretending otherwise — by reporting only overall metrics, or by claiming the model is "fair" — is the failure mode the whole tutorial is designed to prevent.

7 Self-check questions

Answer each question in your own words before revealing the answer.

Question 1

What is a proxy variable, and why does removing the protected attribute from a model not make the model fair?

Show answer
A proxy variable is a feature that is not itself a protected attribute, but which correlates with one strongly enough that including it reproduces much of the effect the protected attribute would have had. Home postcode proxies socioeconomic status; caring responsibilities proxy gender; term-time work hours proxy socioeconomic status.

Removing the protected attribute removes the direct path, but the proxy features still carry the same information into the prediction. The model is unaware of the attribute; the outcome is not. In the worked example, the model was never shown ethnicity, and it still systematically under-predicted group D by more than six marks — because group D students were more likely to work long hours and less likely to have attended an independent school, and those features were in the model.

Question 2

A model has an overall RMSE of 9.4 marks, and a mean residual of −6.2 marks for one subgroup. Why is the second number more concerning than the first?

Show answer
RMSE measures the size of the errors. A mean residual measures their direction. Random error of a given size is unavoidable; systematic error is not.

A mean residual of −6.2 marks means the model consistently predicts lower marks for that group than its members actually achieve. That is not noise — it is a consistent bias, and it will produce a consistent pattern of decisions. In a system where the bottom 10% of predictions receive support, the bias pushes students from that group toward the threshold, so more of them are flagged than their performance warrants. The overall RMSE hides this entirely, because the errors for other groups go in the opposite direction and cancel it out.

Question 3

A colleague reports that the model is fair because the protected attributes were excluded from the feature set, and the R² is similar across all groups. What is wrong with this reasoning?

Show answer
Two problems.

First, excluding the protected attributes does not remove their influence, as Question 1 explains. The relevant question is not which features were used but what effect the model has on each group.

Second, and more specifically: R² is not the metric that matters for a threshold-based decision. In the worked example, group D's R² was 0.41 versus 0.74 for group A — a substantial gap that the colleague would need to address. But even if R² were equal across groups, the model could still distribute support unequally, because the decision depends on where each group's predictions fall relative to a single threshold. A model with equal R² and a shifted prediction distribution will flag one group far more often than another. The metric to check is the selection rate, not R².

Question 4

In the worked example, removing the proxy features reduced group D's bias from −6.21 to −2.08 marks but reduced the overall R² from 0.71 to around 0.60. Is removing them the right decision?

Show answer
There is no statistically correct answer, because the two quantities measure different things and the comparison between them is a value judgement, not a calculation.

The argument for removing them: the systematic bias is a harm inflicted on a specific group of students, and it is the kind of harm that tends to compound — a student who is repeatedly under-predicted may receive less support, perform worse, and confirm the model's prediction. Accuracy gains that come from encoding structural disadvantage are not costless improvements.

The argument for keeping them: the overall accuracy loss means fewer students get help, including students in group D who would have been correctly identified. If the intervention works, removing the proxies may leave more students unsupported.

What is not acceptable is making the choice silently. The decision belongs in the model card, along with the reasoning and a plan to revisit it.

Question 5

The coefficient on attendance is 0.287 marks per percentage point, with a bootstrap stability ratio of 9.90. Can you tell the module leader that improving attendance would raise marks?

Show answer
No, and the stability of the coefficient is not evidence that you can.

Stability tells you that the coefficient would be similar if the study were repeated on a similar sample. It does not tell you what would happen if you intervened. The coefficient compares students who attend more with students who attend less, and those two groups differ in many ways the model does not measure — motivation, health, paid work, caring responsibilities, prior preparation.

A defensible statement is: "In this dataset, students who attended more lectures achieved higher marks, and this association is robust to resampling." That is a description of the data. "Improving attendance would raise marks" is a causal claim, and this model provides no evidence for it. Getting evidence for the causal claim would require an intervention study, not a regression on observational data.

Question 6

The model card recommends deployment "in a limited pilot with conditions". Why is this a better answer than either "deploy" or "do not deploy"?

Show answer
Because the two simpler answers both throw away information that the analysis produced.

"Deploy" ignores the group D bias, which is a real finding that has real consequences for real students. "Do not deploy" ignores the fact that the current process — waiting until students fail — is worse for every group, including group D. A model with known limitations that reaches students earlier may still be an improvement over a system that reaches no one until it is too late.

The conditional recommendation captures both findings. It says: the model is worth using, but only if the specific harm it can cause is mitigated, the limitations are documented, and there is a plan to detect and correct the problem. It also commits to a review date, which is what turns a static analysis into an ongoing responsibility.

"Deploy with these safeguards and review in six months" is a legitimate professional answer in a sensitive domain. In practice, it is more often the right answer than either of the alternatives.

8 Practice exercises

Exercise 1 — Identify the proxies

For each scenario below, list the features that are likely to act as proxy variables for a protected attribute, and state which attribute each proxies.

  1. A model predicting creditworthiness, using income, employment history, home ownership, years at current address, and postal district.
  2. A model predicting employee performance ratings, using project delivery metrics, peer feedback scores, hours logged, and number of internal networking events attended.
  3. A model predicting whether a defendant will reoffend, using prior convictions, age at first offence, employment status, neighbourhood crime rate and whether they have a fixed address.
  4. A model predicting whether a patient will adhere to a treatment plan, using distance to clinic, appointment attendance history, number of dependents, and work schedule flexibility.
Show answers
  1. Postal district proxies ethnicity and socioeconomic status. Home ownership and years at current address both proxy wealth and, indirectly, age and migration status. Employment history proxies age and, in some labour markets, gender through career interruption patterns.
  2. Hours logged is a well-documented proxy for gender and caring responsibilities — employees with caring duties log fewer hours and are penalised for it even when their output is equivalent. Networking events attended proxies socioeconomic background and, in some workplaces, gender, because attendance is easier for those without caring responsibilities.
  3. This is the COMPAS scenario. Neighbourhood crime rate proxies race through residential segregation. Employment status and fixed address both proxy socioeconomic status, which in turn correlates with race. Prior convictions proxies race through differential policing, which affects who is arrested and convicted in the first place — a profound issue, because the feature measures the criminal justice system's behaviour as much as the individual's.
  4. Distance to clinic proxies rurality and socioeconomic status. Work schedule flexibility proxies occupational class and gender. Number of dependents proxies gender and, in many societies, migration status.

In every case, the proxies are legitimate features on their face. The point is not that they should all be removed — it is that including them has consequences that must be examined rather than assumed away.

Exercise 2 — Compute subgroup metrics by hand

A model produces the following predictions on a validation set of 20 students, split into two groups.

Student Group Actual mark Predicted mark
1A7270
2A5861
3A8179
4A6466
5A9086
6A5558
7A7776
8A6869
9A4954
10A8381
11B6658
12B5247
13B7465
14B6153
15B5850
16B7062
17B4541
18B7969
19B6356
20B5549
  1. Compute overall RMSE, MAE, R² and mean residual.
  2. Compute the same four metrics for group A.
  3. Compute the same four metrics for group B.
  4. What is the difference in mean residual between the two groups, and what does it mean?
  5. If the bottom 25% of predictions are flagged for support, how many students from each group are flagged?
  6. What is the actual bottom 25% of the whole cohort, and how many from each group are in it?
Show answers

Residuals (actual − predicted):
Group A: +2, −3, +2, −2, +4, −3, +1, −1, −5, +2
Group B: +8, +5, +9, +8, +8, +8, +4, +10, +7, +6

1. Overall.
Σe² = (4+9+4+4+16+9+1+1+25+4) + (64+25+81+64+64+64+16+100+49+36) = 77 + 563 = 640.
RMSE = √(640/20) = √32 = 5.66.
Σ|e| = 25 + 73 = 98. MAE = 98/20 = 4.90.
Mean of actual: (sum of all 20) / 20 = 1290/20 = 64.5.
TSS = Σ(y − 64.5)² = let us compute: for group A the deviations are 7.5, −6.5, 16.5, −0.5, 25.5, −9.5, 12.5, 3.5, −15.5, 18.5 → squares 56.25, 42.25, 272.25, 0.25, 650.25, 90.25, 156.25, 12.25, 240.25, 342.25 = 1,862.5. For group B deviations: 1.5, −12.5, 9.5, −3.5, −6.5, 5.5, −19.5, 14.5, −1.5, −9.5 → squares 2.25, 156.25, 90.25, 12.25, 42.25, 30.25, 380.25, 210.25, 2.25, 90.25 = 1,016.5. TSS = 2,879.
R² = 1 − 640/2879 = 1 − 0.222 = 0.778.
Mean residual = 0 (by construction here — the errors happen to balance).

2. Group A.
RSS = 77, n = 10, RMSE = √7.7 = 2.77.
MAE = 25/10 = 2.50.
Mean of A = 697/10 = 69.7. TSS_A = 1,862.5.
R² = 1 − 77/1862.5 = 0.959.
Mean residual = 0/10 = 0.00.

3. Group B.
RSS = 563, n = 10, RMSE = √56.3 = 7.50.
MAE = 73/10 = 7.30.
Mean of B = 623/10 = 62.3. TSS_B = 1,016.5.
R² = 1 − 563/1016.5 = 0.446.
Mean residual = +73/10 = +7.30.

4. The mean residual for group B is +7.30 marks versus 0.00 for group A. Every single one of group B's residuals is positive, which means the model systematically under-predicts group B — the students actually achieve marks about seven points higher than the model expects. That is a consistent directional error, not random noise.

5. The overall threshold for the bottom 25% of predictions: the 5 lowest predicted values are 41, 47, 49, 50, 53. Of these, one is from group A (the 54 prediction for student 9 — wait, 54 is higher; the bottom five are 41(B), 47(B), 49(B), 50(B), 53(B)) — all five flagged students are from group B. Group A: 0 flagged. Group B: 5 flagged.

6. The actual bottom 25% of the cohort by real mark: the 5 lowest marks are 45, 49, 52, 55, 55. Of these, one is from group A (the 49) and four are from group B (45, 52, 55, 55). So the correct distribution would be 1 from group A and 4 from group B. The model flags 0 and 5 — it misses the group A student who genuinely needs support, and over-flags group B by one student. Both errors follow directly from the systematic under-prediction of group B.

Exercise 3 — Draft a model card

Using the worked example as a template, write a model card for the following scenario. Include all ten sections, and be specific about the deployment conditions.

A local council wants to predict which households are at risk of fuel poverty, so that it can proactively offer energy-efficiency grants. The model would be built on council tax records, energy consumption data, and housing stock data. The target is whether a household fell into fuel poverty in the following winter.

Pay particular attention to: (a) the features that might act as proxies for ethnicity, disability or age; (b) what the cost of a false negative is, given that the household never learns it was missed; (c) what out-of-scope uses you would prohibit; and (d) what monitoring would tell you the model had stopped working.

Show a model answer outline

Purpose. Identify households likely to enter fuel poverty next winter, so that energy-efficiency grants can be proactively offered.

Intended use. Prioritising households for grant outreach. A human officer reviews the list and makes the final decision on each household.

Out-of-scope use.

  • Must not be used to deny benefits, prioritise debt collection, or set council tax bands.
  • Must not be shared with landlords, energy companies, or third parties.
  • Must not be used to predict individual benefit claims rather than fuel-poverty risk.
  • Must not be applied to households outside the council's area.

Data. Council tax records, smart meter or supplier consumption data, and housing stock characteristics for the council area. Note that households not on the council tax register, in Houses in Multiple Occupation, or without smart meters may be systematically under-represented — precisely the households most likely to be in fuel poverty.

Features. Energy consumption relative to property size, property age and EPC rating, occupancy size, tenure type. Potential proxies to flag: tenure type proxies socioeconomic status and, in some areas, ethnicity; property age and location proxy socioeconomic geography; occupancy size proxies family structure and, indirectly, ethnicity and religion. Consider whether receipt of means-tested benefits is a legitimate feature or a proxy — it directly measures low income, so it is not a proxy, but using it may mean the model mainly finds households the council already knows about.

Performance. Report overall and per subgroup. The critical subgroup metric is the false-negative rate: households genuinely in fuel poverty that the model did not flag.

Subgroup performance. By tenure type, by property type, and by ward. If the model is less accurate for private rented households, that is significant because private renters are both harder to reach and more likely to be in fuel poverty.

Known limitations.

  • Under-representation of the hardest-to-reach households in the training data.
  • Energy consumption data lags by up to a year, so the model reflects last winter's patterns.
  • Weather variation between winters means the base rate shifts.
  • Households that have already received grants look different from those that have not.

Ethical considerations. A false negative here means a household in fuel poverty does not receive an offer of help and never knows it was missed. That asymmetry means the model should be tuned for recall, not precision. The cost of offering a grant to a household that turns out not to need it is much lower than the cost of missing one that does.

Deployment conditions.

  • The model produces a priority list, not an eligibility decision. All eligible households remain eligible.
  • The threshold is set to prioritise recall over precision — deliberately over-inclusive.
  • Households not on the council tax register must be reached through a separate, non-model route.
  • Outreach materials must not disclose that the household was algorithmically identified.

Monitoring plan. Compare the modelled list against actual fuel-poverty outcomes each spring. If the false-negative rate for any tenure category exceeds 25%, or if the ward-level selection rates diverge from the actual distribution of fuel poverty by more than 10 percentage points, the model is suspended pending review. Annual re-fit, with a full fairness analysis each time.

Exercise 4 — Coefficient stability

A model has the following coefficients, along with their bootstrap standard deviations:

Feature Coefficient Bootstrap SD Ratio Report it?
attendance_pct0.2870.029
assignment_1_mark0.4120.022
commute_minutes−0.0410.038
age_at_entry0.0190.031
prior_points0.1960.036
term_work_hours−0.1080.044
  1. Compute the ratio for each coefficient.
  2. Which coefficients would you report with confidence? Which would you not?
  3. What ratio threshold would you use, and why?
  4. What would you tell a colleague who wants to include the commute finding in a policy briefing?
Show answers

1. Ratios: attendance 9.90, assignment 1 mark 18.73, commute 1.08, age at entry 0.61, prior points 5.44, term work hours 2.45.

2. Report with confidence: assignment 1 mark (18.73), attendance (9.90), prior points (5.44). Report with care: term work hours (2.45) — the coefficient is about two and a half times its standard deviation, which is suggestive but not decisive. Do not report: commute (−1.08) and age at entry (0.61), whose standard deviations are comparable to or larger than the point estimates themselves.

3. A ratio of about 3 is a common threshold, corresponding roughly to a coefficient that is unlikely to change sign under resampling. Below about 2, the coefficient is essentially noise, and a different sample could easily produce a coefficient of the opposite sign. Between 2 and 3 is a judgement call — report it with the uncertainty stated explicitly.

4. Tell them that the commute finding is not supported by this data. The point estimate is −0.041 marks per minute of commute, but the bootstrap standard deviation is 0.038, meaning the true value could plausibly be zero or positive. Including it in a policy briefing would create the impression of a finding where there is none, and it could be used to argue for a change to timetabling or campus provision that the evidence does not justify. The correct statement is: "this dataset cannot determine whether commuting distance affects marks."

Exercise 5 — Write the recommendation

Using the worked example's model card, write a 300-word recommendation to the university's teaching committee. Assume the committee contains people who are enthusiastic about using data to help students, and people who are sceptical of algorithmic decision-making.

Your recommendation must:

  1. State clearly whether the model should be deployed, and under what conditions.
  2. Present the strongest argument for deployment, honestly.
  3. Present the strongest argument against, honestly.
  4. Explain the group D finding in terms a non-specialist can follow, without either hiding it or overstating it.
  5. State what evidence would change your recommendation.
  6. Commit to a review date and a monitoring process.

Do not write for the enthusiasts or for the sceptics. Write for the people who will have to defend the decision to a student who asks why they were or were not contacted.

Show a model answer

Recommendation to the Teaching Committee — week 4 support model

I recommend a one-semester pilot of the week-4 support model, with three specific conditions, and a review before the end of the academic year.

The case for. Our current approach identifies struggling students only when they fail their first assessment, which is too late to help. The model identifies them four weeks earlier and halves the error compared with simply assuming everyone performs at the average. For students who need support, being identified earlier may be the difference between receiving help and not.

The case against. The model does not work equally well for every group of students. Overall it explains about 70% of the variation in marks, but for students in group D it explains only about 40%, and it systematically predicts their marks about six points too low. In practice this means group D students will be flagged for support more often than their actual performance justifies. Even though being offered support sounds benign, being repeatedly identified as "at risk" carries a cost, and the model is applying that label inequitably.

What we do not yet know. The dataset is small. Group D contains fourteen students in each evaluation set, so the six-point bias is an estimate with substantial uncertainty. It might be larger. It might be smaller. It is unlikely to be zero.

The conditions. First, group D students flagged by the model must be reviewed individually by a member of staff before any action is taken. Second, no prediction is shown to any student — the output is an invitation, not a forecast. Third, subgroup metrics are recomputed each semester, and a group D mean residual below −4 marks triggers a review.

What would change my recommendation. If the group D bias persists or worsens in the pilot data, I would recommend withdrawing the model and refitting without the socioeconomic features, accepting the accuracy loss. If it narrows, I would recommend continuing. Either way, we should have a better-justified answer in six months than we do today, because we will have measured it rather than estimated it.

9 Problem set

These problems close out Unit 4 and feed directly into Assignment 2, the module checkpoint and the capstone.

Problem 1 — Full pipeline with fairness analysis on a real dataset

Find a public regression dataset that contains at least one protected attribute, or a plausible proxy for one. Options:

Carry out the complete workflow from this tutorial.

  1. Frame the problem, including who is affected by a prediction error.
  2. Audit the features. Identify every feature that could act as a proxy, and justify each decision to keep or remove it.
  3. Split and check representativeness of each subgroup in the training data.
  4. Fit two models: one with proxies retained, one without.
  5. Compute overall and per-subgroup RMSE, MAE, R² and mean residual for both.
  6. Compute the selection rate by subgroup for the bottom-10% threshold under both models.
  7. Compute bootstrap stability for the coefficients of both models. Which coefficients would you report?
  8. Write a complete model card for the model you would recommend.
  9. Write a 250-word recommendation to a decision-maker, presenting both the case for and the case against deployment.

Problem 2 — Quantify a proxy variable

This problem asks you to measure, rather than assert, how much of a protected attribute's effect a proxy feature carries.

  1. Choose a dataset with a protected attribute and at least two plausible proxy features.
  2. Fit a model that includes the protected attribute as a feature. Record its coefficient and the model's overall R².
  3. Fit a model that excludes the protected attribute but includes the proxies. Record overall R² and the coefficients on the proxies.
  4. Fit a model that excludes both the protected attribute and the proxies. Record overall R².
  5. The R² difference between (2) and (4) estimates how much the protected attribute contributes overall. The difference between (3) and (4) estimates how much of that contribution is captured by the proxies. Report both figures.
  6. Repeat steps 2–4 for a second protected attribute if the dataset has one.
  7. Write a paragraph on what your results imply about the "fairness through unawareness" approach. Does removing the protected attribute remove its effect, or simply relocate it?
  8. Is there a version of the model that is genuinely unaffected by the protected attribute? If not, what does that tell you about the limits of feature engineering as a fairness remedy?

Problem 3 — Build a monitoring specification

A model that is fair at the time of deployment can become unfair as the population it serves changes. Write a monitoring specification for the student grade model, as if it were going into operation next semester.

  1. Define the metrics that will be tracked, and how often. At minimum: overall RMSE, per-subgroup RMSE, per-subgroup mean residual, and per-subgroup selection rate.
  2. For each metric, define a warning threshold and an action threshold. The warning threshold triggers a review; the action threshold triggers suspension.
  3. Justify each threshold in terms of its consequences. Why is a group D mean residual of −4 marks a warning rather than −2 or −8?
  4. Describe what kind of change in the world would cause each metric to move. For example, what would cause subgroup mean residuals to diverge without any change in the model?
  5. Specify the review process: who reviews, what evidence they consider, what decisions they can make, and what the timeline is.
  6. Specify how students would be informed that a model is being used, and how they could raise a concern about its use.
  7. Write a short paragraph on why monitoring is ethically required for a model in this domain, not merely good practice.

Problem 4 — Should this model exist?

This problem asks you to argue a position on a contested question, using the technical machinery from Unit 4 and the ethical framework from this tutorial.

Some practitioners argue that regression models should never be used to predict outcomes for individuals in domains where the prediction itself can affect the outcome — education, criminal justice, credit, employment. The argument is that even a well-built model, deployed with safeguards, will tend to reproduce existing patterns of disadvantage, because it learns from data generated by those patterns. Others argue that refusing to build such models leaves the current, worse system in place, and that a monitored model is more transparent and more accountable than human judgement.

Choose a position and defend it in 500 words. Your answer must:

  1. Engage seriously with the strongest argument on the other side, not a straw version of it.
  2. Use at least one concrete finding from the worked example — the group D bias, the proxy trade-off, or the threshold distribution — as evidence.
  3. Distinguish between what the model can do and what the institution will do with it. A model that is fair in isolation may be harmful in a context with different incentives.
  4. State clearly what would change your mind. If you can think of nothing that would, you have not engaged with the question honestly.
  5. Avoid both naive enthusiasm and reflexive rejection. Both are easy positions; the question is hard.

This problem has no correct answer. It has well-argued and poorly-argued ones, and the difference is whether the reasoning is transparent enough to be examined and challenged.

10 Discussion and projects

Discussion prompt — post 150–250 words

A model that is accurate on average and biased against a subgroup is not "mostly working".

In the worked example, the model explained 70% of the variation in marks overall, but only 40% for group D, and it under-predicted group D by more than six marks on average. The overall figure would satisfy almost any technical review. The subgroup figure would fail one.

Describe a decision-making context — from education, healthcare, hiring, lending or elsewhere — where an overall metric could plausibly conceal a subgroup problem that would be unacceptable if known. Be specific about what the metric would conceal, who would be harmed, and whether the harm would be visible to the people affected. Then explain why the harm is often invisible: what about the deployment context makes it hard for a person to know that a model was wrong about them?

Mini-project — build a fairness audit toolkit

Write a reusable Python module that takes a fitted regression model, a validation dataframe with a target column, and a list of sensitive columns, and produces a complete fairness audit.

The module should output:

  1. A metrics table with RMSE, MAE, R² and mean residual, computed overall and for every level of every sensitive column.
  2. A bootstrap confidence interval for the mean residual of each group — essential when subgroup sample sizes are small.
  3. A flag for any group where the mean residual confidence interval excludes zero, indicating systematic bias.
  4. A selection-rate analysis: for a given threshold, what fraction of each group is flagged, compared with what fraction should be flagged based on actual outcomes?
  5. A coefficient stability table using bootstrap resampling, with a flag for any coefficient whose stability ratio falls below 2.
  6. A one-page plain-language summary, listing the findings that require action and the findings that are within tolerance.

The most valuable part of this project is deciding what counts as "requires action". A mean residual of −6 marks clearly does. Is −2 marks acceptable? −1? The decision has to be made somewhere, and making it explicitly — in code, with a comment explaining the threshold — is what turns a statistical analysis into a governance tool.

Discussion prompt — the limits of what you can fix

Every remedy in this tutorial is a partial remedy. Removing proxies reduces accuracy without eliminating the underlying pattern. Adjusting the threshold redistributes support but does not make the predictions less biased. Adding a monitoring plan detects harm but does not prevent it.

Post a short reflection on what this implies about the role of the individual data scientist. If a model's bias is a reflection of a biased dataset, and the dataset is a reflection of a biased society, what is the appropriate scope of a technical fix? Where does the responsibility of the person building the model end, and where does the responsibility of the institution deploying it begin? There is no clean answer, and the value of the discussion is in articulating the tension rather than resolving it.

11 Summary

What you should take away

Unit 4, in one page

Unit 4 built linear regression from the ground up. The final two tutorials were about using it responsibly, and this is where the unit's real argument lies.

What Unit 4 established
Tutorial The idea What it equips you to do
t-01 A prediction problem has a target, features and a unit of observation — and a set of features that will actually exist at prediction time. Frame a vague request into a well-posed question, and establish a baseline.
t-02 The linear model is a weighted sum of features plus a bias, and "linear" means linear in the parameters. Write a model, interpret a coefficient, engineer features within the linear framework.
t-03 Least squares minimises the sum of squared residuals, and the solution has a closed form. Derive and compute the optimal parameters, and understand the four properties they satisfy.
t-04 The cost function defines a surface over the parameter space, and for linear regression that surface is convex. Read a loss surface, compute a gradient, and explain why convexity is valuable.
t-05 Gradient descent reaches the minimum iteratively; the normal equation reaches it in one step; feature scaling decides how hard the search is. Implement both, tune a learning rate, and choose between them by reasoning about size and conditioning.
t-06 RMSE, MAE and R² measure different things; residual plots and learning curves diagnose what they conceal. Evaluate honestly, diagnose the four failure modes, and detect leakage.
t-07 A real project is a sequence of documented decisions, not a single fitting operation. Run the eight-stage workflow end to end, and report a model a stakeholder can act on.
t-08 When the data is about people, an accurate model can still be an unfair one, and fairness and accuracy frequently conflict. Audit for proxies, measure subgroup performance, decide whether to deploy, and document the decision.

Key terms

Proxy variable
A feature that is not a protected attribute but correlates strongly enough with one that including it reproduces much of the effect the protected attribute would have had.
Fairness through unawareness
The (ineffective) practice of removing protected attributes from a feature set in the belief that this makes the model fair.
Subgroup analysis
Computing performance metrics separately for each group of concern, rather than only in aggregate.
Mean residual (bias)
The average of y − ŷ for a group. A non-zero value indicates systematic over- or under-prediction, not random error.
Selection rate
The fraction of a group that receives an intervention when a threshold is applied to the model's predictions. Compare against the fraction that should receive it based on actual outcomes.
Bootstrap stability ratio
The absolute coefficient divided by its standard deviation across bootstrap resamples. A ratio below about 2 indicates the coefficient is not reliably distinguishable from zero.
Stratified split
A train/test split that preserves the proportion of each subgroup, used when subgroup sample sizes are small.
Model card
A structured document recording a model's purpose, intended and out-of-scope use, data, features, performance, subgroup performance, limitations, ethical considerations and monitoring plan.
Out-of-scope use
Contexts in which a model must not be used, documented explicitly so that the model cannot be quietly repurposed.
Monitoring plan
The set of metrics, thresholds and review processes that detect when a deployed model has stopped working as intended.
False negative (in an intervention context)
A person who genuinely needs support but is not flagged by the model. In many sensitive domains this is the more serious error, because the person never learns they were missed.
Fairness–accuracy trade-off
The observation that reducing a model's reliance on proxies or protected attributes generally reduces its accuracy, so fairness and accuracy cannot be maximised simultaneously.

Module 2 is complete when you can…

What comes next

Unit 5 turns to classification with logistic regression. The mathematics changes — a sigmoid link function, a maximum-likelihood objective, a cross-entropy loss — but the workflow does not. Everything from Tutorials 6, 7 and 8 carries over directly: the split, the pipeline, the subgroup analysis, the model card, the decision about deployment. The classification metric set is different — precision, recall, ROC-AUC — and Unit 6 of this module covers those. But the questions you ask about a model are the ones you have just learned to ask.

12 Further reading

Mitchell et al. — "Model Cards for Model Reporting" (2019) essential The original model card paper. Ten pages, and the source of the structure used in Section 3.5. The examples in the paper — including a face classification model and a smiling-detection model — show how much the card format reveals about models that would otherwise be reported only by their accuracy.
Barocas, Hardt & Narayanan — Fairness and Machine Learning free online The standard textbook on algorithmic fairness, freely available from the authors' website. Chapter 2 covers the limitations of fairness through unawareness with the proxy argument developed here; Chapters 3 and 4 formalise the fairness metrics and explain why several of them cannot hold simultaneously.
O'Neil — Weapons of Math Destruction (2016) accessible A journalistic account of the harms caused by algorithmic decision-making, with a substantial focus on education. Written for a general audience, and useful precisely because it describes how these systems fail in practice rather than in theory. Read it alongside the technical material rather than instead of it.
Kleinberg, Mullainathan & Raghavan — "Inherent Trade-Offs in the Fair Determination of Risk Scores" (2017) advanced The formal result that several intuitive fairness criteria cannot all hold at once except in degenerate cases. More mathematical than this course requires, but the introduction is accessible and the result is the reason Section 3.3 says there is no single fairness metric.
Brundage et al. — "Toward Trustworthy AI Development" (2020) free online A broad survey of the mechanisms that support trustworthy deployment: auditing, monitoring, documentation, and the institutional structures that make them enforceable. Sections 2 and 3 on documentation and monitoring map directly onto the model card and monitoring plan in this tutorial.
Gelman, Hill & Vehtari — Regression and Other Stories, ch. 19–21 textbook Covers regression applied to social data, including the interpretation problems specific to observational studies about people. Chapter 19 on causal inference from observational data explains precisely why a stable coefficient is not evidence of an intervention effect.
Raji et al. — "Closing the AI Accountability Gap" (2020) practitioner A practical account of what ongoing model auditing requires in an organisation: who does it, how often, what evidence is produced, and what happens when an audit finds a problem. Useful if you are interested in the organisational side of the deployment decision rather than only the technical one.
Eubanks — Automating Inequality (2018) accessible A detailed, qualitative account of automated decision systems in public services in the United States, including a substantial chapter on child welfare and one on welfare eligibility. It describes the experience of being on the receiving end of these systems, which is the perspective that the technical literature most often omits.
scikit-learn — Model evaluation and Cross-validation user guides reference The documentation for cross_val_score, GroupKFold and the metrics used in this tutorial. The GroupKFold splitter is worth knowing about if your data has a natural grouping structure — students within classes, for example — that should not be split across folds.

What to read next

If you read one thing from this list before moving on to Unit 5, read the model cards paper. It is short, it is practical, and it will change how you think about the deliverable at the end of a modelling project. If you want to go deeper on the fairness material specifically, Chapter 2 of Fairness and Machine Learning is the natural follow-up and is available free online.

The technical content of Unit 4 ends here. What carries forward into Unit 5, Unit 6 and the capstone is not the algebra — it is the workflow, the diagnostics, and the habit of asking what a model does to the people it is applied to.

👤