🎓 Section 5 · Machine Learning 🔴 Capstone Project MODULE 30

Final Project — Student Performance Predictor

⏱️ 120 min · hands-on
📖 Full ML Pipeline Walkthrough
🧩 3 Quiz Questions
🏗️ 1 Challenge · 3 tasks
Your progress in Section 586%
🎯 The Project: This is the capstone of Section 5 — and the biggest project in the whole course. You'll take a synthetic student_performance.csv dataset from raw CSV all the way to a trained, evaluated, interpretable machine learning model. Along the way you'll reuse pandas EDA from Section 2 (.isnull().sum(), .describe()), the correlation concepts from Section 4, train_test_split and RandomForestClassifier from Lessons 24–26, and the exact evaluation tools — classification_report and a confusion matrix — from Lesson 27. Nothing here is a new API; it's everything you already know, assembled into one real pipeline.

The Project Brief

Imagine a school wants a rough early-warning tool: given a few easy-to-collect numbers about a student, can we flag who's at risk of failing an upcoming exam before it happens? We'll build exactly that — a binary classifier that predicts pass or fail from three numeric features.

As with Lesson 13's retail dataset, student_performance.csv here is a plausible, illustrative synthetic dataset built to teach the workflow end to end — not a real published study. The pandas and scikit-learn methods are 100% real and correct; the specific numbers are stand-ins for whatever CSV a real project would hand you.

study_hours_per_week
Self-reported hours spent studying per week, as a decimal (e.g. 12.5).
🏫
attendance_pct
Percentage of classes attended this term, 0–100.
📊
past_exam_avg
Average score across the student's previous exams, 0–100.
🎯
passed
The target column — 1 if the student passed the upcoming exam, 0 if they failed.
1
Load and explore
Read the CSV, check its shape, preview rows, and inspect dtypes — same first move as Lesson 13.
2
Check for missing data and describe the stats
.isnull().sum() and .describe(), then clean what's missing.
3
Correlation heatmap
.corr() — which raw features move together with passed, echoing Section 4's correlation lesson.
4
Train/test split
train_test_split, stratified so both splits keep the same pass/fail balance.
5
Train a RandomForestClassifier
The ensemble classifier from Lesson 26, fit on the three numeric features.
6
Evaluate it properly
classification_report and a confusion matrix, exactly like Lesson 27.
7
Interpret feature importance
Which of the three features actually mattered most to the model?

Load the Data and Take a First Look

Same opening move as every EDA in this course: read the file, then look before touching anything.

load_data.py
PYTHON
import pandas as pd

df = pd.read_csv("student_performance.csv")

print(df.shape)
# (2000, 4)

print(df.head())
df.head()
study_hours_per_weekattendance_pctpast_exam_avgpassed
012.591.274.01
14.062.548.50
218.0NaN88.01
37.570.055.00
49.081.060.01

Row 2 is already missing its attendance_pct — worth remembering for the next step. Next, the dtypes pandas inferred:

inspect_dtypes.py
PYTHON
print(df.dtypes)
# study_hours_per_week    float64
# attendance_pct          float64
# past_exam_avg           float64
# passed                    int64
# dtype: object
📝
passed is already a clean int64 — the label, not a feature
passed has no missing values, so it never got upgraded to float64 the way Lesson 13's quantity column did. It's the target we're trying to predict, so it will be kept separate from X (the features) once we get to the train/test split in Section 5.

Check for Missing Values and Describe the Stats

Two quick pandas calls before any modeling happens: .isnull().sum() to find gaps, and .describe() to get a feel for each column's range and center — the same habit from Lesson 13's EDA project.

check_missing.py
PYTHON
print(df.isnull().sum())
# study_hours_per_week     0
# attendance_pct          34
# past_exam_avg             0
# passed                    0
# dtype: int64

print(df.describe().round(2))
df.describe().round(2)
statstudy_hours_per_weekattendance_pctpast_exam_avgpassed
count2000.01966.02000.02000.0
mean10.4278.3568.910.59
std4.8712.6414.220.49
min0.5040.0030.000.00
25%6.8070.1058.750.00
50%10.2079.0069.001.00
75%13.9087.2079.101.00
max25.00100.00100.001.00
📝
describe() works on a 0/1 column too
passed is numeric (0 or 1), so .describe() summarizes it right alongside the real-valued features. Its mean of 0.59 is actually the most useful number in that column here — since the values are only 0 and 1, the mean is exactly the proportion of students who passed (59%). That's the class balance we'll carry into the train/test split.

34 missing values in attendance_pct is under 2% of 2,000 rows — small enough to fill rather than drop, exactly the same call made in Lesson 13.

clean_data.py
PYTHON
# Fill the small number of missing attendance values with the column median
df["attendance_pct"] = df["attendance_pct"].fillna(df["attendance_pct"].median())

assert df.isnull().sum().sum() == 0, "Still missing values!"

A Correlation Heatmap

Before training anything, it's worth asking — in plain statistics terms from Section 4 — which raw features actually move together with passed. DataFrame.corr() computes the pairwise Pearson correlation between every numeric column, from -1 (perfectly opposite) through 0 (no linear relationship) to +1 (perfectly together).

correlation_heatmap.py
PYTHON
corr = df.corr().round(2)
print(corr)

# A real project would typically visualize this with Seaborn (Section 3):
# import seaborn as sns
# sns.heatmap(corr, annot=True, cmap="Blues")
corr — darker = stronger positive correlation
study_hoursattendancepast_exampassed
study_hours 1.00 0.34 0.41 0.52
attendance 0.34 1.00 0.38 0.46
past_exam 0.41 0.38 1.00 0.61
passed 0.52 0.46 0.61 1.00
past_exam_avg already looks like the strongest single signal
In this illustrative sample, past_exam_avg has the highest correlation with passed (0.61), followed by study_hours_per_week (0.52) and attendance_pct (0.46). All three are meaningfully correlated with the target — none is close to 0 — which is a good early sign that a model trained on them has something real to learn from. Keep this ranking in mind; Section 8 checks whether the trained model's own feature importances agree.
⚠️
Correlation is a preview, not a guarantee
These are simple pairwise correlations — they can't capture interactions between features (e.g. low study hours combined with low attendance being worse than either alone). That's part of why we still train an actual model instead of stopping here.

Splitting Into Features and a Train/Test Set

Same rule from Lesson 24 onward: never evaluate a model on the same rows it was trained on. Separate X (the three feature columns) from y (the passed label), then hold out 20% of the rows for testing only.

train_test_split.py
PYTHON
from sklearn.model_selection import train_test_split

feature_cols = ["study_hours_per_week", "attendance_pct", "past_exam_avg"]
X = df[feature_cols]
y = df["passed"]

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

print(f"Train: {X_train.shape}, Test: {X_test.shape}")
# Train: (1600, 3), Test: (400, 3)
📝
Why stratify=y here
The class split is roughly 59% pass / 41% fail, not perfectly balanced. Passing stratify=y tells train_test_split to preserve that same pass/fail ratio in both y_train and y_test — without it, an unlucky random split could leave the test set with a noticeably different balance, making the evaluation in Section 7 harder to trust.

Training a RandomForestClassifier

This is a classification problem — passed is 0 or 1, not a continuous number — so it calls for a classifier, not a regressor. We'll use RandomForestClassifier from Lesson 26: an ensemble of decision trees that tends to handle non-linear relationships between features well and, as a bonus, ships with a built-in feature importance score for Section 8.

train_model.py
PYTHON
from sklearn.ensemble import RandomForestClassifier

clf = RandomForestClassifier(
    n_estimators=200,
    max_depth=6,
    random_state=42
)
clf.fit(X_train, y_train)
n_estimators and max_depth, in plain terms
n_estimators=200 means the forest builds 200 individual decision trees and averages their votes — more trees generally means a more stable prediction, at the cost of more compute. max_depth=6 caps how deep each individual tree can grow, which helps prevent any single tree (and therefore the forest) from memorizing quirks of the training data instead of learning general patterns — the overfitting concern first raised back in Lesson 24.
📝
LogisticRegression would also be a completely valid choice here
Lesson 25's LogisticRegression is just as legitimate a starting point for this pass/fail problem — it's simpler, faster, and its coefficients are directly interpretable. This lesson sticks with RandomForestClassifier throughout for consistency, but comparing the two is exactly Task 1 in this lesson's "Try It Yourself" challenge below.

Evaluating the Model Properly

A single accuracy number can hide a lot — exactly the lesson from Lesson 27. Since passed isn't perfectly balanced (59/41), classification_report and a confusion matrix tell a much fuller story than .score() alone.

evaluate_model.py
PYTHON
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score

y_pred = clf.predict(X_test)

print(f"Accuracy: {accuracy_score(y_test, y_pred):.2f}")
print(classification_report(y_test, y_pred, target_names=["Fail", "Pass"]))
print(confusion_matrix(y_test, y_pred))
classification_report(y_test, y_pred, target_names=["Fail", "Pass"])
precisionrecallf1-scoresupport
Fail0.780.720.75164
Pass0.810.860.83236
accuracy0.80400
macro avg0.800.790.79400
weighted avg0.800.800.80400
confusion_matrix(y_test, y_pred) — rows = actual, columns = predicted
Pred: FailPred: Pass
Actual: Fail11846
Actual: Pass34202
⚠️
These numbers are illustrative for a synthetic dataset, not a verified benchmark
Exactly like Lesson 29's sentiment example, this is a demonstration of the correct evaluation mechanics on a toy dataset built for teaching — not a claim about real predictive accuracy for real students. A genuine deployment would need a much larger, real, carefully-collected dataset, plus the kind of cross-validation from Lesson 27 rather than one single train/test split, before anyone should trust its numbers.
Reading the confusion matrix
Of the 164 students who actually failed, the model correctly caught 118 of them but missed 46 (predicted them as "Pass" when they actually failed) — those 46 are the ones an early-warning tool like this exists to catch, so that miss rate matters more than the headline 0.80 accuracy alone would suggest.

Interpreting Feature Importance

A trained RandomForestClassifier exposes .feature_importances_ — one number per feature, summing to 1.0, measuring roughly how much each feature contributed to the forest's decisions across all its trees.

feature_importance.py
PYTHON
importances = pd.Series(clf.feature_importances_, index=feature_cols)
print(importances.sort_values(ascending=False).round(2))

# past_exam_avg            0.47
# study_hours_per_week     0.31
# attendance_pct           0.22
# dtype: float64
🥇
past_exam_avg — 0.47
The single biggest driver of the model's predictions — past academic performance carries the most weight, matching its top spot in Section 4's correlation heatmap.
🥈
study_hours_per_week — 0.31
A meaningful secondary factor — more weekly study time consistently nudges predictions toward "Pass."
🥉
attendance_pct — 0.22
Still contributes, but the least of the three — in this synthetic sample, showing up matters less than what a student already knows and how much they study.
Feature importance agrees with the correlation heatmap — that's a good sign
Section 4's correlation values (0.61, 0.52, 0.46) and the forest's feature importances (0.47, 0.31, 0.22) put the three features in the exact same order: past_exam_avg first, study_hours_per_week second, attendance_pct third. Real datasets don't always line up this cleanly — correlation is linear and pairwise, while feature importance accounts for interactions — but when the two views agree, it's a useful sanity check that the model learned something sensible rather than noise.
⚠️
Importance is not the same as causation
past_exam_avg being the top feature means it's the most useful for prediction — not proof that raising it would cause a student to pass. A student's past average likely reflects a whole bundle of real-world factors (study habits, support at home, prior teaching quality) that the model never sees directly.

The Complete Script, Start to Finish

Every step from this lesson, combined into one runnable pipeline against a CSV shaped like student_performance.csv.

student_performance_predictor.py — COMPLETE PROGRAM
PYTHON
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score

# 1. Load and take a first look
df = pd.read_csv("student_performance.csv")
print("Shape:", df.shape)
print(df.head())
print(df.dtypes)

# 2. Check for missing values, describe, then clean
print(df.isnull().sum())
print(df.describe().round(2))
df["attendance_pct"] = df["attendance_pct"].fillna(df["attendance_pct"].median())
assert df.isnull().sum().sum() == 0

# 3. Correlation heatmap (printed here; visualize with sns.heatmap in a notebook)
print(df.corr().round(2))

# 4. Split into features/target, then train/test
feature_cols = ["study_hours_per_week", "attendance_pct", "past_exam_avg"]
X = df[feature_cols]
y = df["passed"]
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# 5. Train the model
clf = RandomForestClassifier(n_estimators=200, max_depth=6, random_state=42)
clf.fit(X_train, y_train)

# 6. Evaluate
y_pred = clf.predict(X_test)
print(f"Accuracy: {accuracy_score(y_test, y_pred):.2f}")
print(classification_report(y_test, y_pred, target_names=["Fail", "Pass"]))
print(confusion_matrix(y_test, y_pred))

# 7. Interpret feature importance
importances = pd.Series(clf.feature_importances_, index=feature_cols)
print(importances.sort_values(ascending=False).round(2))

Writing Up Findings

A trained model isn't the deliverable by itself — the point is turning it into a few statements someone at the school could actually act on. Treat these as an example of the kind of takeaway a real project like this would produce, not as claims about any real student population.

📊
Past performance dominates
past_exam_avg is the strongest single predictor by both correlation and feature importance — an early-warning system built on this data would lean on it most.
Study hours matter, but less
Still a meaningful contributor — a student behind on past_exam_avg who increases study_hours_per_week shows up as more likely to pass in this data.
🚩
Missed fails are the real risk
46 of 164 actual fails were predicted "Pass" in the confusion matrix — for an early-warning tool, reducing that specific number matters more than the overall accuracy figure.
⚠️
A finding is a hypothesis, not a conclusion
"attendance mattered least" is something worth investigating further, not a fact to publish — maybe attendance interacts with study hours in ways a 3-feature model can't see, or maybe it's genuinely a weaker signal. Good ML work, like good EDA, produces sharper questions as often as it produces answers.

That's the full pipeline — and the full course core curriculum. Every earlier lesson fed into this one: pandas cleaning, correlation from Section 4's statistics, the classifier itself, and the exact same evaluation tools from Lesson 27. Section 6 next turns this project into something you can actually show off.

🧩 Knowledge Check — Lesson 30
3 questions on the capstone pipeline before you move on.
1. Why was stratify=y passed to train_test_split for this dataset?
2. According to clf.feature_importances_, which feature mattered most to the trained RandomForestClassifier?
3. In the confusion matrix, what do the off-diagonal cells (46 and 34) represent?
💪
Try It Yourself — Lesson 30
Extend the capstone project · Advanced Level

The base pipeline works end to end — now push it further. Use the cleaned df, X_train/X_test/y_train/y_test from Sections 5–9 as your starting point for each task below.

Task 1: Try a different model 🔬

Swap RandomForestClassifier for Lesson 25's LogisticRegression(max_iter=1000), fit it on the exact same X_train/y_train, and run it through the same classification_report/confusion_matrix code from Section 7. Does accuracy go up, down, or stay about the same? Which class (Fail or Pass) does each model handle better?
Task 2: Add a feature ➕

Imagine a fourth column, assignments_submitted_pct, existed in the CSV. Add it to feature_cols, re-run the split and training from Sections 5–6, and re-print clf.feature_importances_. Does adding a feature change the ranking of the original three, or just add a fourth entry?
Task 3: Tune a hyperparameter 🎛️

Try three different values of max_depth (for example 3, 6, and 12) while keeping everything else fixed, retraining and re-evaluating each time. Does test accuracy keep improving as trees get deeper, or does it plateau or get worse — and what does that tell you about overfitting?
💡 Show hints if you're stuck
  • Task 1: from sklearn.linear_model import LogisticRegression, then lr = LogisticRegression(max_iter=1000); lr.fit(X_train, y_train) — everything else stays identical.
  • Task 2: feature_cols = ["study_hours_per_week", "attendance_pct", "past_exam_avg", "assignments_submitted_pct"], then rebuild X = df[feature_cols] before splitting again.
  • Task 3: Wrap the training/evaluation code in a small loop over for depth in [3, 6, 12]:, printing accuracy_score(y_test, clf.predict(X_test)) each time.
Finished the capstone project?
Mark it complete to track your progress.
🎉

Section 5 Complete — Capstone Project Done!

You've built a full machine learning pipeline from scratch: EDA, a correlation heatmap, a stratified train/test split, a trained RandomForestClassifier, proper evaluation with classification_report and a confusion matrix, and feature importance interpretation. That's every core skill from Sections 1–5, combined. Section 6 is next — turning this project into a portfolio piece.

Module 30 of 30 Section 5 — Machine Learning with Scikit-Learn