Problems: Model Validation#
Get this problem set
Download everything (Topic2.2-Model_Validation_Problems.zip) — the notebook and
henry_law.csv, in a folder that is ready to run as-is.
The Download badge at the top of the page will also give you the notebook on its own. On Vocareum everything is already set up for you.
Before you start
This problem set accompanies Model Validation. It is worth 100 points:
Part A — Skill Checks (30 pts) — short answers, auto-graded, resubmit as often as you like until they pass.
Part B — Visualization (35 pts) — plots plus written interpretation, peer graded.
Part C — Open Ended (35 pts) — one synthesis problem, peer graded.
The parts build on each other: Part A works out the syntax you need for Part B, and Part B produces the evidence you argue from in Part C. Do them in order.
Setup#
Henry’s law constant \(H\) describes how much of a gas dissolves in water at equilibrium. Its temperature dependence is set by the enthalpy of solvation \(\Delta H_\text{sol}\) through the van ‘t Hoff relation. A linear free-energy relationship predicts that species which dissolve more exothermically should also be more soluble — that is, that \(\ln H\) should fall roughly linearly with \(\Delta H_\text{sol}\).
You will test that claim on Sander’s compilation of Henry’s law constants (version 5.0.0), which gathers values from the published literature for thousands of species. Each row is one species as reported by one literature reference:
column |
meaning |
|---|---|
|
chemical identity |
|
which literature reference reported this value |
|
how the value was obtained — |
|
Henry’s law solubility constant at 298.15 K, and its log |
|
van ‘t Hoff temperature coefficient, and \(\Delta H_\text{sol}\) |
The important structural feature: many species appear more than once, because several groups measured them independently. That gives you something rare — a direct measurement of how much independent laboratories disagree.
%matplotlib inline
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import KFold
from sklearn.metrics import r2_score, mean_absolute_error, root_mean_squared_error
try:
plt.style.use('../settings/plot_style.mplstyle') # available inside the book
except OSError:
pass # downloaded notebook: use defaults
df = pd.read_csv('data/henry_law.csv')
print(f"{len(df)} rows, {df.species.nunique()} species, {df.ref.nunique()} references")
df.head()
4344 rows, 1519 species, 519 references
| species | formula | casrn | ref | htype | H_mol_m3_Pa | mindHR_K | lnH | dHsol_kJ_mol | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | (1,1-dimethylethyl)-benzene | C6H5C4H9 | 98-06-6 | 2891 | M | 0.00160 | 4700 | -6.43775 | -39.0780 |
| 1 | (1,1-dimethylethyl)-benzene | C6H5C4H9 | 98-06-6 | 2904 | M | 0.00094 | 2400 | -6.96963 | -19.9547 |
| 2 | (1,1-dimethylethyl)-methanoate | HCOOC4H9 | 762-75-4 | 2646 | M | 0.01400 | 3600 | -4.26870 | -29.9321 |
| 3 | (1,1-dimethylethyl)-methanoate | HCOOC4H9 | 762-75-4 | 3518 | L | 0.01400 | 3600 | -4.26870 | -29.9321 |
| 4 | (1-methylpropyl)-benzene | C6H5C4H9 | 135-98-8 | 2891 | M | 0.00130 | 4600 | -6.64539 | -38.2465 |
Because that repetition matters later, here is what it looks like. To pull out every row for one species, build a boolean array by comparing the species column against a name, then use it to index — the same trick works on any array of the same length:
sel = (df['species'].values == 'carbon dioxide') # a True/False array, one per row
print(f"{sel.sum()} entries for carbon dioxide, from {df['ref'].values[sel].size} references")
print(df['lnH'].values[sel][:8].round(4))
29 entries for carbon dioxide, from 29 references
[-8.0164 -7.9866 -7.9866 -7.9866 -8.0164 -8.0164 -8.0164 -8.0164]
Those are the first eight of twenty-nine independent reports of the same constant, and they do not all agree. Keep that in mind — Part C is about exactly this.
Run this once to load the autograder. It will not run inside the book — use the downloaded notebook.
import otter
grader = otter.Notebook()
Part A — Skill Checks (30 pts)#
Three questions, 10 points each. Throughout, the model is an ordinary least-squares fit
of lnH (target) on the single feature dHsol_kJ_mol, using all 4,344 rows.
A1. Fit the relationship (10 pts)#
Exercise 147
Fit sklearn.linear_model.LinearRegression with dHsol_kJ_mol as the only feature and
lnH as the target, using the whole dataset. Report the \(r^2\) on the same data you fit
on.
Assign it to r2_full.
# YOUR CODE HERE
r2_full = ...
grader.check("q1")
A2. Validate it honestly (10 pts)#
Exercise 148
The A1 number is optimistic — it scores the model on the data used to fit it. Redo the evaluation with 5-fold cross-validation, built the way the chapter builds it: create
kf = KFold(n_splits=5, shuffle=True, random_state=0)
and loop over kf.split(X), fitting on the training indices and scoring on the test
indices each time. Report the mean of the five fold \(r^2\) values as r2_cv.
Do not change the seed. A shuffled split is a random quantity, so a reported cross-validation score means nothing unless the split is reproducible.
# YOUR CODE HERE
r2_cv = ...
grader.check("q2")
A3. Put the error in physical units (10 pts)#
Exercise 149
An \(r^2\) of 0.59 does not say how wrong a prediction typically is. Collect the
out-of-fold predictions — inside the same loop as A2, store each fold’s predictions in
the right positions of a full-length array, so that every row ends up predicted by a model
that never saw it. Then report the root-mean-squared error of those predictions against
lnH, using root_mean_squared_error.
Assign it to rmse_cv. The units are ln units of \(H\).
# YOUR CODE HERE
rmse_cv = ...
grader.check("q3")
Part B — Visualization (35 pts)#
Exercise 150
Three panels, using the three diagnostics from the chapter.
Parity plot. Out-of-fold prediction against actual
lnH, with the 1:1 line drawn. Color the points by whetherhtypeis'Q'(a QSAR estimate — a value produced by a correlation rather than measured) or anything else. You can select rows of a NumPy array with a boolean array, e.g.is_q = (df['htype'].values == 'Q')and theny[is_q].Error histogram. The out-of-fold errors \(y - \hat{y}\). Mark the mean and, using
np.percentile, the 5th and 95th percentiles.How stable is the estimate? Recompute the mean cross-validated \(r^2\) for
n_splitsin \(\{2, 5, 10, 20\}\), each for at least 10 differentrandom_statevalues, and show the spread as a boxplot pern_splits.
Then, in 4–6 sentences:
The
'Q'points sit closer to the 1:1 line than the rest. Explain why that is expected, given how a QSAR value is produced, and say what it implies about using this pooled dataset as evidence that the relationship is physically real.Is the error histogram centered and symmetric? Say what its shape and width add that \(r^2\) alone does not.
Does the choice of
n_splitsmatter much here? Say why, referring to the model’s size relative to the dataset.
Label all axes with units.
# YOUR CODE HERE
fig, axes = plt.subplots(1, 3, figsize=(14, 4))
Part C — Open Ended (35 pts)#
Exercise 151
An RMSE of 3.68 ln units sounds terrible. But no model can do better than the data it is checked against, and this dataset has something unusual that lets you find out how good “perfect” would even be: many species were measured independently by several different groups, so the disagreement among those repeated values is measurement scatter that no model could ever predict.
Decide whether this model is worth improving, and defend it with evidence.
Your answer should include:
For at least four species that appear many times, the standard deviation of
lnHacross their repeated entries. Select a species’ rows with a boolean array, e.g.sel = (df['species'].values == 'carbon dioxide'), then usenp.std(..., ddof=1). Some species with many entries: carbon dioxide, methane, benzene, ammonia, oxygen, trichloromethane, methylbenzene.A comparison of your
rmse_cvfrom A3 against those spreads. How many times larger is the model’s error than the disagreement between laboratories?A statement of how much of the model’s error could in principle be removed, with your assumptions stated. Note that your four species will not agree with each other — say what that does to the argument.
At least one concrete attempt to improve things, evaluated with the same cross-validation procedure as A2. A second feature, a different model from Non-parametric Models, or a restriction of the dataset are all fair game.
A short written argument (one paragraph) about whether the effort is warranted, citing your own numbers.
Be careful with step 4: at least one obvious-looking move makes the reported score worse, for a reason that is a property of the data rather than a failure of the model. If you hit it, explain it.
There is more than one defensible conclusion. You are graded on the reasoning and the evidence, not on reaching a particular verdict.
# YOUR CODE HERE
Summary#
Part A produced three numbers that answer three different questions: how well the model fits data it has already seen (0.602), how well it predicts data it has not (0.594), and how large a typical prediction error actually is (3.68 ln units, a factor of about 40 in \(H\) itself).
Part B looked at the same model three ways the chapter recommends — a parity plot, an error histogram, and the spread of the cross-validated score — and found that part of the apparent fit quality comes from rows that are model output rather than measurement.
Part C set the model’s error against the disagreement between independent laboratories, and found both that the headroom is large and that the floor itself is not a single number.
Additional Reading#
R. Sander, Compilation of Henry’s law constants (version 5.0.0) for water as solvent, Atmos. Chem. Phys. 23, 10901–12440 (2023).
sklearn.model_selection.KFoldandsklearn.metrics.root_mean_squared_error.