Rubric
Reviewing a security-ML notebook
You are given one notebook. It contains a trained model and a headline score. Review it the way a referee would. Decide how much of the result you believe.
The rubric has two halves.
Part A (6 points) you can answer by reading the notebook.
Part B (4 points) requires running code. Those four answers do not exist
anywhere in the notebook. You have to produce them.
Each criterion is worth 1 point, awarded as 1, 0.5, or 0.
Part A — Read the notebook (6 points)
1. Data provenance
Answer the mirror question for every notebook. Look at what the loader downloads. Then look at whose work the notebook cites. Are they the same party?
Five notebooks pull from the authoritative distribution. That means scikit-learn’s UCI mirror, the CIC’s own AWS Open Data bucket, or the Stratosphere site.
The other 31 pull from Kaggle, where anyone can re-upload anyone’s dataset. Sometimes the uploader is the author or their lab. Often it is an unrelated account. That account may have re-published, and possibly modified, someone else’s corpus.
This matters because nothing downstream can repair it. Suppose the file you trained on is not the file the citation describes. Then every number in the notebook measures something other than what it claims to.
Ask three things, and say which you could settle:
- Who published the file the loader fetches, and who authored the corpus?
- Does the shape you get match what the origin paper describes? Row count, column count, class balance.
- Does the corpus’s own licence permit that person to have published it?
| 1.0 | The source is named and citable, the description matches what the loader reads, and either the channel is authoritative or the notebook accounts for the mirror. |
|---|---|
| 0.5 | The source is named, but the description overstates it. A sample called the full corpus, a simulation described as collected traffic. Or the notebook reads a third-party mirror without saying so. |
| 0 | You cannot tell where the data came from, the description is wrong, or the file is republished in breach of the licence. |
A mirror you cannot verify is not automatically a zero. An unexamined one is.
2. Task definition
| 1.0 | The label rule is stated plainly. You could reproduce it from the notebook alone. |
|---|---|
| 0.5 | The label is stated, but one class is less certain than the prose admits. |
| 0 | The label rule is unclear, or the negative class is not what the prose says it is. |
3. Baseline comparison
Find the majority-class baseline accuracy. Then find the winning model’s accuracy in the comparison table. Compare those two numbers yourself. The notebook may not do it for you.
| 1.0 | The baseline is printed, and the winning model clears it on the same axis. |
|---|---|
| 0.5 | The baseline is printed but the comparison is left undone. Or the axes are mixed, such as an AUC held against an accuracy baseline. |
| 0 | No baseline. Or the winning model fails it and the notebook does not say so. |
4. Is the result one column?
Read the single-feature audit and the ablation together. They can disagree. When they do, the ablation is the stronger evidence.
| 1.0 | Removing the top feature leaves most of the score intact. The result rests on many columns. |
|---|---|
| 0.5 | Removing it costs a lot, and the notebook says so. But it never settles whether that column is real signal or an artifact. |
| 0 | Removing it collapses the score, and the notebook still reads the headline as detection. |
5. Do the conclusion’s numbers appear in the output?
Check the conclusion cell only, not the whole notebook.
| 1.0 | Every number in the conclusion appears in a visible output cell. |
|---|---|
| 0.5 | At least one figure or comparison is asserted rather than shown. |
| 0 | A central claim has no printed support. |
6. Scope
| 1.0 | States what the result does not cover, and names the question that was not tested. |
|---|---|
| 0.5 | Limits are mentioned, but only in general terms. |
| 0 | An in-distribution score is presented as an estimate of real-world performance. |
Part B — Run the notebook (4 points)
The probe
Execute every cell. Then paste the probe below at the end and run it. Include its output in your submission. Without that output, Part B scores zero.
probe_model is a fixed comparison model, not the notebook’s. Judge B5 on
the gaps between the three numbers, not their absolute values.
# ===================== PART B PROBE =====================
from sklearn.metrics import roc_auc_score
import numpy as np, pandas as pd
import xgboost as xgb
def sf_grade(a):
return 'F' if a>=0.999 else 'D' if a>=0.99 else 'C' if a>=0.95 else 'B' if a>=0.85 else 'A'
def best_single(frame, labels):
au = {}
for c in feat:
col = frame[c].to_numpy(float)
if col.std() == 0: continue
a = roc_auc_score(labels, col); au[c] = max(a, 1-a)
return max(au.values()), max(au, key=au.get)
# B1 - does the grade survive a different random draw?
for seed in range(1, 9):
s = X.sample(min(60_000, len(X)), random_state=seed)
b, col = best_single(s, y[s.index])
print(f"B1 seed {seed}: {b:.4f} grade {sf_grade(b)} ({col})")
# B2 - what is the value with no subsampling at all?
b, col = best_single(X, y)
print(f"\nB2 full corpus: {b:.4f} grade {sf_grade(b)} ({col})")
# B3 - does contamination depend on WHICH test rows were checked?
_trk = set(map(tuple, np.round(Xtr.to_numpy(), 6)))
_te = np.round(Xte.to_numpy(), 6)
first = float(np.mean([tuple(r) in _trk for r in _te[:50_000]]))
ridx = np.random.default_rng(0).choice(len(_te), size=min(50_000, len(_te)), replace=False)
rand = float(np.mean([tuple(_te[i]) in _trk for i in ridx]))
print(f"B3 contamination: first-50k {first:.4f} vs random-50k {rand:.4f}")
# B4 - how many groups did the per-group report leave out?
_p = pd.Series(df['family'].to_numpy()[Xte.index][yte == 1]).value_counts()
print(f"B4 groups: {len(_p)} with >=1 positive, {int((_p<5).sum())} skipped by the count-5 floor "
f"{list(_p[_p<5].index)}")
# B5 - the notebook drops ONE feature. Drop the top TWO and refit.
def probe_model():
return xgb.XGBClassifier(n_estimators=120, max_depth=6, n_jobs=-1,
eval_metric='logloss', random_state=0)
m = probe_model().fit(Xtr, ytr)
base = roc_auc_score(yte, m.predict_proba(Xte)[:, 1])
imp = pd.Series(m.feature_importances_, index=feat).sort_values(ascending=False)
t1, t2 = imp.index[0], imp.index[1]
a1 = roc_auc_score(yte, probe_model().fit(Xtr.drop(columns=[t1]), ytr)
.predict_proba(Xte.drop(columns=[t1]))[:, 1])
a2 = roc_auc_score(yte, probe_model().fit(Xtr.drop(columns=[t1, t2]), ytr)
.predict_proba(Xte.drop(columns=[t1, t2]))[:, 1])
print(f"B5 ablation: base {base:.4f} | drop {t1} -> {a1:.4f} | drop {t1}+{t2} -> {a2:.4f}") 7. Reproduction
| 1.0 | The notebook runs end to end. Your numbers match the stored ones. |
|---|---|
| 0.5 | It runs, but some numbers differ. You identify which, and give a reason. |
| 0 | It does not run, or you did not check. |
8. Grade stabilityuses B1
| 1.0 | All eight seeds give the same letter, and the value stays clear of the cutoff. |
|---|---|
| 0.5 | The letter holds. But at least one seed lands within 0.01 of a cutoff. |
| 0 | The letter changes across seeds. |
9. Sampling artifactsuses B2 and B3
| 1.0 | The full-corpus value and the contamination windows both agree with what the notebook printed. |
|---|---|
| 0.5 | One of them differs enough to matter. You say which, and by how much. |
| 0 | A published number is not representative of the quantity it claims to measure. |
10. How deep does the shortcut gouses B4 and B5
| 1.0 | Dropping the top two features still leaves a usable score. No group was silently skipped. |
|---|---|
| 0.5 | Dropping two costs most of the score. Or some groups fell below the count floor unreported. |
| 0 | Dropping the top feature alone already collapses the score to near-random. |
Verdict bands
| Total | Verdict |
|---|---|
| 8.5 – 10 | Quote the headline, with the stated limits. |
| 6 – 8 | Quote it only alongside the weaknesses you found. |
| 3 – 5.5 | Do not quote the headline. Say what would have to change first. |
| below 3 | The result is not evidence yet. |
A high score is a legitimate outcome. Some of these notebooks are sound. Saying so is worth as much as finding a flaw.
Your two minutes
| Time | What you say |
|---|---|
| 0:00–0:20 | What the notebook claims, and its headline number. |
| 0:20–0:45 | Your total, and your verdict band. |
| 0:45–1:30 | The two criteria that moved your score most, with the numbers behind them. |
| 1:30–2:00 | The one thing you would run next to settle it. |
At least one of your two criteria must come from Part B. Roughly thirty seconds each. Slides optional.
What these ten criteria cannot see
Ten criteria are not a proof of correctness. Applying this rubric across the whole set surfaced three blind spots. Know them before you trust a high score.
It tests columns, not relationships between columns. Criteria 4 and 10 drop the top feature, then the top two. They check what survives. That catches a single leaky column. It cannot catch a shortcut carried by a relation among several columns. Consider a synthetic corpus whose generator writes the label and several numeric columns from one rule. Dropping two of them still leaves the rest. The notebook can score 10/10 with its headline unexplained.
A passing audit is not evidence. A notebook can clear every mechanical check and still have an artifact for a headline. Here is one tell the rubric has no box for. A linear model scores near 0.50. Tree models score far higher on the same data. That gap is the signature of a split-based artifact, not a learnable trend. Often it is driven by which rows have a missing value.
The grade thresholds are sharp and the measurements are noisy. The single-feature AUC is computed on a 60,000-row sample. Sampling noise is roughly ±0.03. Some notebooks sit within 0.001 of a grade cutoff. Criterion 8 exists to expose exactly this. But it means a letter grade near a boundary tells you nothing on its own.
If you find something the criteria have no box for, say so in your two minutes. Naming a gap the rubric cannot reach is worth more than a clean sweep of ten.