Coming soon: Build Data + AI systems with evidence built in. Preview
← Back to Blog
Data correctness Mitari · June 2026

Passed Review, Broke Production: 5 Data Science Bugs Code Review Tools Can’t Catch

Some of the most damaging Data Science bugs never trip a test or a linter. The code runs, the diff looks clean, a reviewer approves it — and the model or metric it produces is quietly wrong. Here are five failures that survive ordinary review, why they slip through, and how to catch them.

Traditional code review and static tools are good at a specific job: they check whether code is well-formed, readable, and internally consistent. They are far weaker at a different question — whether the data reasoning encoded in that code is sound. A pipeline can execute end to end, a notebook can produce a clean chart, and a model can report strong validation numbers while the underlying measurement is invalid. The five patterns below are the ones we see most often. Each includes a short explanation, a code example, why review misses it, and how to catch it.

Bug 1

Preprocessing fit on the full dataset before the split

A scaler, imputer, or encoder is fit on the entire dataset and only then split into train and test. The transform has now “seen” the test rows — their mean, variance, or category set leak into training. Your offline metrics look better than the model will ever do in production.

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
X_train, X_test = train_test_split(X_scaled)

Why review misses it

Every line is idiomatic scikit-learn. Nothing is undefined, nothing throws, and the ordering looks natural top-to-bottom. The bug is the sequence — fit before split — not any single statement, so line-by-line reading approves it.

How to catch it

Split first, then fit only on training data and apply the fitted transform to the test set — ideally inside a Pipeline so the boundary can’t be crossed by accident.

X_train, X_test = train_test_split(X)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
Bug 2

Look-ahead bias in time-series features

A feature is computed with a window that includes the current — or future — timestamp. A rolling mean, a “days since last event,” or a normalization that uses the full series all quietly encode information that would not exist at prediction time.

df["rolling_mean"] = df["value"].rolling(window=7).mean()
df["target"] = df["value"].shift(-1)

Here the rolling window is not shifted, so the feature for a given row can include the same row’s value, while the target is next-step. At serving time you would not yet have that point.

Why review misses it

The rolling and shift calls are individually correct and common. Whether they cause leakage depends on alignment and on what is known at prediction time — context a reviewer rarely reconstructs from the diff alone.

How to catch it

Lag features so they only use strictly past data, and validate with a time-ordered split rather than a random one.

df["rolling_mean"] = df["value"].shift(1).rolling(window=7).mean()
df["target"] = df["value"].shift(-1)
Bug 3

Target leakage hiding in a feature name

A column that is derived from — or is a proxy for — the label ends up in the feature set. The model looks near-perfect in validation because it is partly reading the answer.

features = df.drop(columns=["churned"])
model.fit(features, df["churned"])

If features still contains something like days_since_cancellation or refund_issued, the model is trained on information that only exists because the customer churned.

Why review misses it

The obvious label column was dropped, so the code looks careful. The leak is semantic: a differently named feature carries the target’s information. You have to understand what each column means, not just its name.

How to catch it

Audit features for post-outcome information, and treat suspiciously high validation scores as a signal to investigate rather than to celebrate.

leaky = ["days_since_cancellation", "refund_issued"]
features = df.drop(columns=["churned", *leaky])
model.fit(features, df["churned"])
Bug 4

An evaluation that measures the wrong thing

On an imbalanced problem, accuracy is reported and looks excellent — while the model never usefully identifies the rare class it was built to find.

preds = model.predict(X_test)
print("accuracy:", accuracy_score(y_test, preds))

If 98% of rows are negative, a model that predicts “negative” every time scores 98% accuracy and catches nothing.

Why review misses it

accuracy_score is a valid, well-known metric and the call is correct. The defect is that the metric does not match the objective — a judgment about the problem, not about the code.

How to catch it

Choose metrics that reflect the goal: precision, recall, F1, or PR-AUC for imbalanced targets, and inspect the confusion matrix.

preds = model.predict(X_test)
print(classification_report(y_test, preds))
print("pr_auc:", average_precision_score(y_test, model.predict_proba(X_test)[:, 1]))
Bug 5

A join or filter that silently changes your data

A join fans out to more rows than intended, or a filter is applied too late or too broadly. Row counts change, aggregates inflate, and every metric computed downstream is wrong — even though each step ran without error.

orders = orders.merge(payments, on="customer_id")
revenue = orders["amount"].sum()

If a customer has many payments, the merge multiplies their order rows, and sum() over the exploded table overstates revenue.

Why review misses it

merge and sum are ordinary pandas. Whether the join grain is correct depends on the keys and the real cardinality of the tables — information not visible in the diff.

How to catch it

Assert the expected grain: check row counts before and after joins, validate key uniqueness, and aggregate to the intended level before combining.

before = len(orders)
orders = orders.merge(payments, on="customer_id", validate="many_to_one")
assert len(orders) == before
revenue = orders["amount"].sum()

The common thread

None of these bugs are syntax errors. In every case the code is valid, the pipeline runs, and the tests — which encode what the author already thought to check — pass. What fails is the method: the data was measured, split, joined, or scored in a way that does not support the conclusion being drawn. Ordinary review approves them because they look reasonable line by line, and the defect only shows up as a semantic mismatch between intent and data flow.

Code review is one important verification surface, but the larger problem begins earlier. Teams need explicit intent, relevant context, evaluation evidence, and independent judgment throughout the development lifecycle.

← Back to Blog Mitari Blog