Week06 Machine Learning Introduction Lecture
Week06 Machine Learning Introduction Lecture
Table of Contents
1. What is Machine Learning?
2. The Scikit-Learn Workflow
3. The Accuracy Trap
4. The Confusion Matrix
5. Precision, Recall, and the Tradeoff
6. ROC Curves and AUC
7. Overfitting
8. Data Leakage
9. Putting It Together
We begin by importing the libraries we will use throughout this lecture. Don't worry if you don't recognize all of these yet — we will introduce each one as we need it.
They could try writing explicit rules — "if SPT blow count is below 15 and the groundwater table is shallow, flag it" — but how do you set those thresholds? What about the interactions between soil type, depth, seismic intensity,
and dozens of other factors?
Machine learning takes a different approach: instead of programming rules, you provide examples and let the algorithm discover the patterns.
Machine Learning is about building mathematical models to understand data. Models learn from examples rather than explicit programming.
"Instead of coding rules for predicting soil behavior, we provide examples of sites that did and didn't liquefy — the model learns the relationship."
Key Insight: The Key Difference Supervised: "Here are examples with answers — learn the pattern" vs Unsupervised: "Find patterns in this data on your own"
CE Examples Soil: will it liquefy in an earthquake? Building heating load from geometry
Pipe segment: will it fail this year? Traffic speed from vehicle density
plt.tight_layout()
[Link]()
Key Steps:
[QUICK] For each scenario below, decide: supervised or unsupervised? Classification or regression?
1. Predicting a building's annual heating load from its wall area, roof area, and glazing percentage
2. Grouping soil samples into clusters based on grain size, plasticity, and moisture (no labels)
3. Deciding whether a soil layer will liquefy during an earthquake based on borehole data
4. Estimating traffic speed on a highway from vehicle density measurements
| | Features Matrix: X | Target Array: y | |---|---|---| | Shape | [n samples , nfeatures ] | [n samples ] | | Content | Each row = one sample, each column = one feature | Labels (classification) or values (regression) | | Type |
2D NumPy array or pandas DataFrame | 1D NumPy array or pandas Series |
This is inspired by the UCI Energy Efficiency Dataset — a real dataset of 768 buildings analyzed for energy performance. Let's see the Estimator API in action.
In [3]: # Building wall area vs heating load (inspired by UCI Energy Efficiency dataset)
X_wall = [Link]([[245], [280], [318], [350], [390], [416]])
y_heat = [Link]([12, 16, 22, 28, 35, 41]) # Heating load in kWh/m²
# 1. Choose model
model_energy = LinearRegression()
# 2. Fit model
model_energy.fit(X_wall, y_heat)
# 3. Make predictions
X_new_wall = [Link]([[300], [375]])
predictions_energy = model_energy.predict(X_new_wall)
Whether you're doing linear regression, logistic regression, decision trees, or neural networks — the pattern is always fit() → predict() → score() . Master this pattern once, and you can use any model
in scikit-learn.
[QUICK] What would you change to predict heating load from multiple building features (wall area, roof area, glazing percentage, overall height)? Hint: Only the shape of X needs to change — the API stays the
same.
Every beginner's first question about a model is: "What's the accuracy?"
This section shows why that question, on its own, can be dangerously misleading — especially in geotechnical engineering, where soil failures are rare but catastrophic.
A weather model that always predicts "no rain" in Izmir would be right ~90% of the time. But you'd never trust it during the rainy season.
A spam filter that never flags anything as spam has 100% accuracy on legitimate emails — but lets every phishing attack through.
A medical screening test that always says "healthy" would have >99% accuracy, because most people ARE healthy. But it would miss every case of disease.
The pattern: when one outcome is much rarer than the other, a lazy model that always predicts the common outcome gets high accuracy for free.
In [6]: [Link](42)
# Soil liquefaction dataset — inspired by SPT-based records from the 1999 Izmit earthquake
# HIGHLY IMBALANCED: most sites remain stable
n_stable = 900
n_liquefied = 100
# Features for stable sites (higher SPT N, deeper groundwater, lower PGA)
X_stable = np.column_stack([
[Link](8, 45, n_stable), # spt_n: SPT blow count
[Link](1, 20, n_stable), # depth (m)
[Link](0, 70, n_stable), # fines_content (%)
[Link](0.05, 0.45, n_stable), # pga: peak ground acceleration (g)
[Link](1, 10, n_stable) # gw_depth: groundwater depth (m)
])
# Features for liquefied sites (low SPT N, shallow GW, high PGA, high fines)
X_liquefied = np.column_stack([
[Link](2, 25, n_liquefied),
[Link](1, 15, n_liquefied),
[Link](10, 90, n_liquefied),
[Link](0.15, 0.55, n_liquefied),
[Link](0, 6, n_liquefied)
])
X = [Link]([X_stable, X_liquefied])
y = [Link]([0] * n_stable + [1] * n_liquefied) # 0=stable, 1=liquefied
Features: SPT blow count, depth, fines content, PGA, groundwater depth
dumb_model = AlwaysStableModel()
y_pred_dumb = dumb_model.predict(X)
accuracy_dumb = accuracy_score(y, y_pred_dumb)
print(f"Accuracy of the 'always stable' model: {accuracy_dumb:.1%}")
print(f"\nBut how many liquefiable sites did it catch?")
print(f"Liquefied sites detected: {[Link](y_pred_dumb[y == 1]):.0f} out of {n_liquefied}")
[Link]('The Accuracy Illusion: A "90% Accurate" Model That Catches Nothing', fontsize=13, fontweight='bold')
plt.tight_layout()
[Link]()
The model above has 90% accuracy — and it's completely useless. It missed ALL 100 liquefiable sites.
In geotechnical engineering, soil failures are rare (thankfully). But that makes accuracy a terrible metric for safety-critical applications.
[DISCUSS] Can you think of other engineering scenarios where the "rare event" is the one you care about most? Consider:
Earthquake damage assessment — most buildings survive, but the damaged ones need urgent attention
Water pipe failure — most pipes are fine, but a burst main floods entire neighborhoods
Landslide prediction — most slopes are stable, but a single failure can destroy a highway
Dam safety monitoring — most readings are normal, but anomalies can precede catastrophic failure
[QUICK] If you were a geotechnical engineer assessing 1,000 borehole sites across the Marmara region and your liquefaction model had 90% accuracy, would you trust it? What additional information would you
demand before approving construction on a "stable" site?
Imagine you have tested 100 borehole sites. You know the truth (from detailed geotechnical investigation after the earthquake) and you have the model's prediction. Let's sort every site into one of four buckets:
1. Model says "stable," site IS stable — great, safe to build (True Negative)
2. Model says "liquefiable," site IS stable — unnecessary ground improvement, costs money (False Positive)
3. Model says "stable," site IS liquefiable — building on dangerous ground (False Negative)
4. Model says "liquefiable," site IS liquefiable — caught it, apply ground improvement (True Positive)
The confusion matrix simply counts how many sites fell into each bucket.
Notice: Site S003 was liquefiable but predicted stable — that's the dangerous one!
| | Predicted Negative | Predicted Positive | |---|---|---| | Actually Negative | True Negative (TN) | False Positive (FP) | | Actually Positive | False Negative (FN) | True Positive (TP) |
Now let's use sklearn to compute the confusion matrix on the full dataset. Note the class_weight='balanced' parameter — this tells the model to pay extra attention to the minority class (liquefied sites), counteracting
the imbalance in our dataset.
cm = confusion_matrix(y_test, y_pred)
| Error Type | Meaning | Consequence | |---|---|---| | False Negative (FN) | Liquefiable site predicted as stable | Building collapses in next earthquake | | False Positive (FP) | Stable site predicted as liquefiable |
Unnecessary ground improvement (costs money) |
A false negative in liquefaction assessment can be catastrophic — as seen in Adapazarı during the 1999 Izmit earthquake, where buildings on liquefiable soil collapsed. A false positive is merely expensive.
True Positive: Fire alarm goes off, there IS a fire. Life saved.
False Positive: Fire alarm goes off, it's just burnt toast. Annoying but harmless.
False Negative: Fire alarm stays silent during a fire. Fatal.
True Negative: No alarm, no fire. Normal day.
In most safety systems, we tolerate false positives (nuisance alarms) to minimize false negatives (missed dangers). This is exactly the tradeoff in structural health monitoring.
[Link]('Which Model Would You Deploy for a Real Construction Project?', fontsize=13, fontweight='bold')
plt.tight_layout()
[Link]()
[QUICK] Sklearn provides a full classification report with precision, recall, and F1 for each class. Let's see what it looks like.
Precision answers: "When the alarm rings, should I trust it?" — Of all the times it warned me, how often was there actually a fire?
Recall answers: "Am I catching everything dangerous?" — Of all the real fires, how many did the detector catch?
A very sensitive smoke detector has high recall (catches every fire) but low precision (also goes off when you cook). A very conservative detector has high precision (only alarms on real fires) but low recall
(might miss a slow-burning fire).
Precision = TP / (TP + FP) — "Of all alarms raised, how many were real?"
Recall = TP / (TP + FN) — "Of all actual positives, how many did we catch?"
These two metrics are in tension. Improving one typically hurts the other.
1 if P (positive) ≥ threshold
y
^ = {
0 if P (positive) < threshold
By default, most classifiers use a threshold of 0.5, but this is not always the best choice.
Key Insight: Changing the threshold shifts the balance between precision and recall:
| Threshold | Effect | |-----------|--------| | Low (e.g., 0.1) | More samples predicted positive → higher recall (fewer missed positives) but lower precision (more false alarms) | | High (e.g., 0.9) | Only very confident
predictions are positive → higher precision but lower recall (more missed positives) |
There is no free lunch — improving one metric typically comes at the cost of the other.
Example: Civil Engineering Consider a model that predicts whether a bridge component will fail. A low threshold means we flag more components for inspection (costly but safe). A high threshold means we
only flag components the model is very confident about (cheaper but riskier — we might miss a real failure).
for t in thresholds:
y_pred_t = (y_proba >= t).astype(int)
tp = [Link]((y_pred_t == 1) & (y_test == 1))
fp = [Link]((y_pred_t == 1) & (y_test == 0))
fn = [Link]((y_pred_t == 0) & (y_test == 1))
prec = tp / (tp + fp) if (tp + fp) > 0 else 0
rec = tp / (tp + fn) if (tp + fn) > 0 else 0
[Link](prec)
[Link](rec)
[Link]('How Changing the Threshold Affects the Confusion Matrix', fontsize=14, fontweight='bold')
plt.tight_layout()
[Link]()
[TOGETHER] Look at the 6 plots above. As the threshold moves from low (0.1) to high (0.9):
To increase recall, we lower the threshold so we catch more true positives — but this also lets in more false positives, which decreases precision.
To increase precision, we raise the threshold so we only predict positive when we are very confident — but this causes us to miss some true positives, which decreases recall.
The only way to improve both simultaneously is to build a better model (one that assigns higher probabilities to true positives and lower probabilities to true negatives).
Consider a model with Precision = 1.0 and Recall = 0.01 (almost never predicts positive, but when it does, it's always right):
1.0 + 0.01
Arithmetic mean = = 0.505 (looks decent!)
2
1.0 × 0.01
F1 = 2 ⋅ = 0.02 (correctly shows this model is nearly useless)
1.0 + 0.01
The harmonic mean penalizes extreme imbalances — the F1 score is high only when both metrics are reasonably high.
F1 Score Interpretation
< 0.5 Weak — the model is making many errors of one type or both
Weighted Variants: F β
Sometimes false negatives and false positives are not equally costly. The generalized F score lets you control the balance:
β
Precision × Recall
2
Fβ = (1 + β ) ⋅
2
(β ⋅ Precision) + Recall
F0.5 0.5 Weighs precision more When false alarms are costly (e.g., dispatching emergency crews)
F2 2.0 Weighs recall more When missing positives is dangerous (e.g., structural failure screening)
Key Insight: The F1 score is the best default when you have no strong reason to prefer precision over recall. In real engineering applications, always think about what type of error is more costly before choosing
your evaluation metric.
Liquefaction screening → High recall (don't miss a dangerous site, even if you trigger some unnecessary ground improvement)
Routine pipe inspection → High precision (don't waste crew time inspecting healthy pipes)
Emergency earthquake response → High recall (better to over-evacuate than miss a damaged building)
You build a model that predicts whether a river will flood in the next 24 hours.
High recall strategy (threshold = 0.2): You evacuate neighborhoods whenever there is even a 20% chance of flooding. Many false evacuations, but you never miss a real flood. Cost: public trust erodes over
time from false alarms.
High precision strategy (threshold = 0.8): You only evacuate when the model is 80%+ confident. Fewer unnecessary evacuations, but you might miss a real flood. Cost: lives at risk.
The tradeoff is real and there is no "correct" answer — it depends on the consequences.
[PRACTICE] Let's compute precision, recall, and F1 manually for our liquefaction model to reinforce the formulas.
print(f"Precision: {precision_val:.3f}")
print(f"Recall: {recall_val:.3f}")
print(f"F1 Score: {f1_val:.3f}")
print()
if recall_val > precision_val:
print("This model leans toward recall — it catches more liquefiable sites")
print("but raises more false alarms. For geotechnical safety, this is usually preferred.")
else:
print("This model leans toward precision — its alarms are more trustworthy")
print("but it may miss some liquefiable sites. For safety applications, be cautious.")
Precision: 0.354
Recall: 0.933
F1 Score: 0.514
[TOGETHER] Would you accept this F1 score for a liquefaction screening system? What F1 score would you consider the minimum for deploying such a model in practice?
The Receiver Operating Characteristic (ROC) curve gives us a way to evaluate a classifier across every threshold at once. The Area Under the Curve (AUC) summarizes this into a single number — think of it as a report card
GPA for your model: it doesn't tell you how the model performed on any single exam (threshold), but it summarizes the overall performance.
Remember that Recall (also called True Positive Rate) answers: "Of all liquefiable sites, how many did we catch?"
Now we need one more metric: False Positive Rate (FPR) = FP / (FP + TN) — "Of all stable sites, how many did we falsely flag?"
1. At threshold = 0.5, our liquefaction model produces a specific confusion matrix. From that matrix, compute TPR and FPR. That gives us one point on the ROC plot: (FPR, TPR).
2. Change the threshold to 0.3. New confusion matrix → new TPR and FPR → another point.
3. Repeat for every possible threshold from 0 to 1. Connect the dots.
4. That's the ROC curve.
for t in demo_thresholds:
y_pred_t = (y_proba >= t).astype(int)
tp = [Link]((y_pred_t == 1) & (y_test == 1))
fp = [Link]((y_pred_t == 1) & (y_test == 0))
fn = [Link]((y_pred_t == 0) & (y_test == 1))
tn = [Link]((y_pred_t == 0) & (y_test == 0))
tpr = tp / (tp + fn) if (tp + fn) > 0 else 0
fpr = fp / (fp + tn) if (fp + tn) > 0 else 0
roc_points.append((fpr, tpr))
print(f"\nThreshold = {t:.1f}:")
print(f" Confusion matrix: TN={tn}, FP={fp}, FN={fn}, TP={tp}")
print(f" FPR = {fp}/({fp}+{tn}) = {fpr:.3f} | TPR = {tp}/({tp}+{fn}) = {tpr:.3f}")
print(f" → Plot point: ({fpr:.3f}, {tpr:.3f})")
# Now plot these individual points AND the full smooth curve
fpr_full, tpr_full, _ = roc_curve(y_test, y_proba)
print("\nEach dot is one threshold. Connect them all → you get the ROC curve!")
Threshold = 0.1:
Confusion matrix: TN=131, FP=139, FN=0, TP=30
FPR = 139/(139+131) = 0.515 | TPR = 30/(30+0) = 1.000
→ Plot point: (0.515, 1.000)
Threshold = 0.3:
Confusion matrix: TN=191, FP=79, FN=1, TP=29
FPR = 79/(79+191) = 0.293 | TPR = 29/(29+1) = 0.967
→ Plot point: (0.293, 0.967)
Threshold = 0.5:
Confusion matrix: TN=219, FP=51, FN=2, TP=28
FPR = 51/(51+219) = 0.189 | TPR = 28/(28+2) = 0.933
→ Plot point: (0.189, 0.933)
Threshold = 0.7:
Confusion matrix: TN=241, FP=29, FN=7, TP=23
FPR = 29/(29+241) = 0.107 | TPR = 23/(23+7) = 0.767
→ Plot point: (0.107, 0.767)
Threshold = 0.9:
Confusion matrix: TN=264, FP=6, FN=15, TP=15
FPR = 6/(6+264) = 0.022 | TPR = 15/(15+15) = 0.500
→ Plot point: (0.022, 0.500)
Each dot is one threshold. Connect them all → you get the ROC curve!
Logistic Regression — fits a linear decision boundary (the model we've been using)
Decision Tree — splits data using a sequence of if/else rules on individual features
K-Nearest Neighbors (KNN) — classifies a sample based on the majority vote of its closest neighbors in feature space
We won't dive deep into how these models work — that's for future weeks. For now, the point is that different models can produce very different ROC curves on the same data.
Key Insight: AUC Tells You How Well Your Model Separates Classes
AUC = 1.0: Perfect separation (every liquefiable site ranked higher than every stable site)
AUC = 0.5: Random guessing (the diagonal — no better than flipping a coin)
Higher AUC = better model, regardless of threshold choice
Think of it like a GPA: it summarizes overall performance without telling you the score on any single test.
[DISCUSS] A colleague tells you: "My liquefaction model has AUC = 0.95, so it's great." What questions would you ask before trusting this claim for a real construction project?
Consider:
ROC curve: Best when classes are roughly balanced. The FPR axis can be misleading when negatives vastly outnumber positives (a small FPR can still mean many false alarms).
Precision-Recall curve: Better for imbalanced datasets (like our liquefaction data with 90% stable sites). It focuses on the minority class.
For most geotechnical safety problems, the precision-recall curve is more informative than ROC.
Student A memorizes every answer from past exams word-for-word. On a practice test using those same past questions, they score 100%. But on the real exam with new questions, they fail.
Student B studies the underlying concepts (equilibrium, compatibility, material behavior). They score 85% on practice tests but also 82% on the real exam.
A model that performs perfectly on training data but poorly on new data has memorized the noise in the training set rather than learning the underlying pattern. This is one of the most fundamental challenges in machine
learning.
In [21]: [Link](42)
n_pts = 30
Underfitting (degree 1): A straight line cannot capture the exponential decay of speed with density. High bias, low variance.
Just right (degree 3): Model captures the smooth decay without fitting sensor noise.
Overfitting (degree 20): Model chases every noisy speed reading. Low bias, high variance.
The goal is to find the sweet spot — complex enough to capture real patterns, simple enough to generalize.
To get an honest evaluation, we need unseen data: data the model has never seen during training.
Never use test data during training or hyperparameter tuning. The test set estimates how well your model will perform on truly new data.
ax.set_xlim(-2, 102)
ax.set_ylim(-0.6, 0.6)
ax.set_xlabel('All Available Data', fontsize=12)
ax.set_yticks([])
[Link]['top'].set_visible(False)
[Link]['right'].set_visible(False)
[Link]['left'].set_visible(False)
plt.tight_layout()
[Link]()
# Split data
x_train_ov, x_test_ov, y_train_ov, y_test_ov = train_test_split(
x, y_noisy, test_size=0.3, random_state=42
)
for d in degrees:
pipe = make_pipeline(PolynomialFeatures(d), LinearRegression())
[Link](x_train_ov.reshape(-1, 1), y_train_ov)
train_errors.append([Link](mean_squared_error(y_train_ov, train_pred)))
test_errors.append([Link](mean_squared_error(y_test_ov, test_pred)))
# Cap very large test errors for visualization (values above 5 are truncated)
test_errors_capped = [min(e, 5) for e in test_errors]
1. At degree 1, both training error and test error are high. What does that mean?
2. Around degree 3, test error is lowest. What does that mean?
3. Beyond degree 8, training error approaches zero but test error explodes. What does that mean?
4. The green dashed line marks the sweet spot. In practice, how do you find this spot?
Key Insight: A Model That's Perfect on Training Data is Suspicious, Not Impressive
You collect speed-density data from 50 highway sensors during one week on the O-4 motorway near Istanbul. Your degree-12 polynomial fits the training data beautifully (R² = 0.99).
Then you deploy it for the following week's traffic management system. At moderate densities, your model predicts negative speeds — a physical impossibility. The polynomial oscillations that fit the noise in
training data produce absurd predictions on new data.
The model memorized the specific traffic patterns of that one week, not the general physics of traffic flow. This is overfitting in the real world.
[TOGETHER] Let's examine the exact train and test error numbers at each polynomial degree. Watch how the status changes from underfitting to good fit to overfitting.
Why it works: Every data point gets used for both training and testing, and the average smooths out the luck of any single split.
n_folds = 5
colors_train = 'steelblue'
colors_test = 'indianred'
for i in range(n_folds):
y_pos = n_folds - 1 - i
for j in range(n_folds):
x_start = j * (1.0 / n_folds)
width = 1.0 / n_folds
if j == i:
color = colors_test
alpha = 0.5
label = 'Test'
else:
color = colors_train
alpha = 0.4
label = 'Train'
rect = [Link]((x_start, y_pos - 0.35), width, 0.7,
facecolor=color, alpha=alpha, edgecolor='k', linewidth=1)
ax.add_patch(rect)
if j == i:
[Link](x_start + width / 2, y_pos, 'Test', ha='center', va='center',
fontsize=10, fontweight='bold')
[Link](-0.08, y_pos, f'Fold {i+1}:', ha='right', va='center', fontsize=11)
# Legend
from [Link] import Patch
legend_elements = [Patch(facecolor=colors_train, alpha=0.4, edgecolor='k', label='Train'),
Patch(facecolor=colors_test, alpha=0.5, edgecolor='k', label='Test')]
[Link](handles=legend_elements, loc='upper right', fontsize=11)
ax.set_xlim(-0.15, 1.05)
ax.set_ylim(-0.6, n_folds - 0.2)
ax.set_xticks([])
ax.set_yticks([])
ax.set_title('5-Fold Cross-Validation', fontsize=14)
[Link]['top'].set_visible(False)
[Link]['right'].set_visible(False)
[Link]['bottom'].set_visible(False)
[Link]['left'].set_visible(False)
plt.tight_layout()
[Link]()
print()
print("Note: High std dev across folds = model is sensitive to which data it sees = overfitting")
Note: High std dev across folds = model is sensitive to which data it sees = overfitting
Cross-validation is precisely the tool you use to detect overfitting in practice: if your model performs well on all K folds (not just the training set), you have evidence that it generalizes. If performance varies wildly
across folds, your model may be overfitting to specific subsets of the data.
This is arguably the most common and most dangerous mistake in applied machine learning.
Imagine you're studying for a final exam and you accidentally find the answer key beforehand. You ace the exam with a perfect score. But you didn't actually learn anything — your "performance" is an illusion.
On the next exam (with different questions), you fail miserably because the leaked answers are no longer available.
Data leakage is the same: your model "saw the answers" during training, so it looks brilliant. But in production (the next exam), it fails.
In [27]: [Link](42)
n = 200
df_leak = [Link]({
'n_floors': n_floors,
'building_age': building_age,
'soil_score': soil_score,
'crack_width_mm': observed_crack_width, # LEAKED — post-earthquake measurement!
'damage_score': damage_score # TARGET
})
print("Features available:")
print(df_leak.[Link]())
print("\n'crack_width_mm' was measured AFTER the earthquake — this is data leakage!")
print("You can't know crack widths before the earthquake happens.")
Features available:
['n_floors', 'building_age', 'soil_score', 'crack_width_mm', 'damage_score']
print(f"WITH leaked feature (crack width): R² = {model_leaked.score(X_te, y_te):.3f} <- Too good to be true!")
print(f"WITHOUT leaked feature: R² = {model_clean.score(X_te2, y_te2):.3f} <- The real performance")
print(f"\nThe leaked model looks amazing but would fail in practice,")
print(f"because you can't measure crack widths before an earthquake.")
WITH leaked feature (crack width): R² = 0.921 <- Too good to be true!
WITHOUT leaked feature: R² = 0.738 <- The real performance
# Leaked model
y_pred_leaked = model_leaked.predict(X_te)
axes[0].scatter(y_te, y_pred_leaked, alpha=0.6, color='indianred', edgecolors='k', s=50)
axes[0].plot([0, 100], [0, 100], 'k--', linewidth=1.5, label='Perfect prediction')
axes[0].set_xlabel('Actual Damage Score', fontsize=11)
axes[0].set_ylabel('Predicted Damage Score', fontsize=11)
axes[0].set_title(f'WITH Leaked Feature (R² = {model_leaked.score(X_te, y_te):.3f})\nSuspiciously perfect!', fontsize=12)
axes[0].legend(fontsize=10)
axes[0].grid(True, alpha=0.3)
# Clean model
y_pred_clean = model_clean.predict(X_te2)
axes[1].scatter(y_te2, y_pred_clean, alpha=0.6, color='steelblue', edgecolors='k', s=50)
axes[1].plot([0, 100], [0, 100], 'k--', linewidth=1.5, label='Perfect prediction')
axes[1].set_xlabel('Actual Damage Score', fontsize=11)
axes[1].set_ylabel('Predicted Damage Score', fontsize=11)
axes[1].set_title(f'WITHOUT Leaked Feature (R² = {model_clean.score(X_te2, y_te2):.3f})\nRealistic scatter', fontsize=12)
axes[1].legend(fontsize=10)
axes[1].grid(True, alpha=0.3)
Always ask: "Would I have this feature at the time I need to make my prediction?"
1. Target leakage: A feature derived from or strongly correlated with the target. Example: Using "observed crack width" (measured after the earthquake) to predict earthquake damage.
2. Train-test contamination: Scaling/normalizing before splitting. Example: Computing mean building height across ALL buildings (including test set) to normalize.
3. Temporal leakage: Using future data to predict the past. Example: Using 2024 repair records to predict which buildings were damaged in 2023.
[PRACTICE] Let's see what happens when we accidentally let test data statistics leak into training through feature scaling.
# WRONG way: scale before split (test data statistics leak into training)
scaler_wrong = StandardScaler()
X_scaled_wrong = scaler_wrong.fit_transform(X_clean) # fit on ALL data
Xw_tr, Xw_te, yw_tr, yw_te = train_test_split(X_scaled_wrong, y_target, test_size=0.2, random_state=42)
model_wrong = LinearRegression().fit(Xw_tr, yw_tr)
# RIGHT way: scale after split (only use training data statistics)
Xr_tr, Xr_te, yr_tr, yr_te = train_test_split(X_clean, y_target, test_size=0.2, random_state=42)
scaler_right = StandardScaler()
Xr_tr_scaled = scaler_right.fit_transform(Xr_tr) # fit ONLY on training
Xr_te_scaled = scaler_right.transform(Xr_te) # transform test with train stats
model_right = LinearRegression().fit(Xr_tr_scaled, yr_tr)
Imagine you are predicting building damage in a future earthquake. Ask yourself: "When does each feature become available?"
| Feature | Available before the earthquake? | Why? | |---|---|---| | Number of floors | Yes | Known from building records | | Building age | Yes | Known from permit records | | Soil type at site | Yes | Known from
geological surveys | | Observed crack width | No | Only measurable after the earthquake | | Differential settlement | No | Only measurable after the earthquake | | Repair cost estimate | No — this is essentially
the target! | Circular reasoning |
This is directly relevant to Turkey's post-earthquake building screening program (Law 6306). The goal is to identify at-risk buildings before the next earthquake — so only pre-earthquake features are valid.
[PRACTICE] You are building a model to predict landslide risk along a highway corridor in the Black Sea region. Your dataset includes:
(Hint: Apply the timeline test — would you have this feature BEFORE the landslide happens?)
9. Putting It Together
Model Evaluation Checklist
Before you report results, ask yourself:
2 Do I know the cost of each error type? Define: What's worse, FP or FN?
5 Could there be data leakage? Audit every feature: would I have it at prediction time?
You've been hired to build a model that predicts which pipe segments are most likely to fail in the next year. Your dataset: 5,000 pipe segments — 4,750 operational and 250 with recorded failures.
Let's walk through the full evaluation pipeline with this scenario.
n_pipes = 5000
n_operational = 4750
n_failed = 250
# Features for failed pipes (older, smaller, cast iron, more corrosive soil, more breaks)
X_fail = np.column_stack([
[Link](40, 120, n_failed), # older pipes
[Link](75, 300, n_failed), # smaller diameter
[Link](5, 500, n_failed), # length_m
[Link]([0, 1, 2, 3, 4], n_failed, p=[0.45, 0.25, 0.10, 0.15, 0.05]), # mostly cast iron
[Link](3, 8, n_failed), # pressure_bar
[Link]([1, 2, 3], n_failed, p=[0.15, 0.35, 0.50]), # more corrosive soil
[Link](0.8, 2.5, n_failed), # burial_depth_m
[Link](4, n_failed) # more previous breaks
])
# Shuffle
shuffle_idx = [Link](n_pipes)
X_pipe, y_pipe = X_pipe[shuffle_idx], y_pipe[shuffle_idx]
# Results
acc_pipe = accuracy_score(y_pipe_test, y_pipe_pred)
cm_pipe = confusion_matrix(y_pipe_test, y_pipe_pred)
Accuracy: 95.9%
Confusion Matrix:
TN=1367 FP= 58
FN= 4 TP= 71
Classification Report:
precision recall f1-score support
1. Is the accuracy misleadingly high? (Check the class balance — 95% of pipes are operational.)
2. What does the confusion matrix tell you about false negatives? How many pipe failures did we miss?
3. For this application (water infrastructure), should we optimize for precision or recall? Consider: a missed pipe failure can flood a neighborhood, but sending a crew to a healthy pipe wastes resources.
4. If ISKI can only inspect 500 pipe segments this year, how would you use the model's probability scores to prioritize?
[TOGETHER] If ISKI can inspect 500 pipe segments this year, which threshold gives the best balance between catching failures and not wasting crew time?
Quick Self-Check
==================================================
Week 07 (Naive Bayes): Before reporting "my Naive Bayes has 94% accuracy," ask — accuracy at what cost?
Week 08 (SVM): Before choosing an SVM kernel, check — am I overfitting to the training set?
Evaluation pervades the entire workflow. From data collection (is this representative?) to deployment (is the model still accurate on new data?).
Ask: "What's the cost of my model's errors, and can I live with it?"
This question should follow you into every ML project you work on — in this course and beyond.
Questions?
Dr. Eyuphan Koc [Link]@[Link]