Chapter 01 · Section III · 26 min read
Your first classifier, start to finish
Everything from the first two sections, in one runnable script. Load Course 02's cleaned loans, train a logistic regression, evaluate it honestly, and read what the model learned. The full ML loop in under 50 lines.
This is the moment you have been working towards since Course 02 Chapter 1. We will load the cleaned loan-default dataset, train a real model, evaluate it on data it has not seen, and read what the model learned. It is fewer than fifty lines of code. Run it once and you will have done end-to-end supervised machine learning. Nothing in the next five courses is fundamentally different — only larger, sharper, more involved. The shape is the shape.
The plan
Six steps. Each one or two lines of code:
- Load the cleaned dataset.
- Choose features and the target column.
- Split into train and test.
- Scale numeric features.
- Train a logistic regression.
- Evaluate on the held-out test set, and read the weights.
Open a new notebook in your building-ai-notes/ml-first-classifier/ folder. Make sure you have done pip install scikit-learn pandas in your activated environment from Course 01.
Step 1 — load
import pandas as pd
df = pd.read_csv("data/clean/loans.csv")
print(df.shape)
print(df.head())
You should see 12,847 rows by 8 columns (or whatever your cleaned dataset has). Look at .head() to remember what columns you have. We expect something like loan_amount, monthly_income, district, occupation_category, defaulted, and a few engineered features.
Step 2 — choose features and target
For a first classifier, keep it small. Three features:
numeric_features = ["loan_amount", "monthly_income"]
categorical_features = ["district"]
target = "defaulted"
# Compute the loan-to-income ratio as an engineered feature, from Course 02 Chapter 5
df["loan_to_income_ratio"] = df["loan_amount"] / df["monthly_income"]
numeric_features.append("loan_to_income_ratio")
X = df[numeric_features + categorical_features].copy()
y = df[target].copy()
# Drop rows with missing target or any missing feature (lazy first-pass; better strategies in Chapter 3)
mask = X.notna().all(axis=1) & y.notna()
X, y = X[mask], y[mask]
print("Final shape:", X.shape, "Defaults:", y.sum(), "of", len(y))
A useful sanity check: what fraction of loans defaulted? If it’s 5%, you have an imbalanced problem (we’ll handle that in Chapter 4). If it’s 30-50%, you have a balanced problem. Either way, know the answer before you train anything.
Step 3 — split
Straight from Course 02 Chapter 5:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42,
)
print("Train:", X_train.shape, "Test:", X_test.shape)
stratify=y preserves the default-rate in both splits. random_state=42 makes the split reproducible.
Step 4 — scale (and encode categorical features)
A Pipeline plus a ColumnTransformer, as in Course 02 Chapter 5. This handles scaling, encoding, and the fit-on-train-only discipline automatically:
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
preprocess = ColumnTransformer([
("num", StandardScaler(), numeric_features),
("cat", OneHotEncoder(handle_unknown="ignore"), categorical_features),
])
Step 5 — train
The big moment. Two lines:
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
model = Pipeline([
("preprocess", preprocess),
("classifier", LogisticRegression(max_iter=1000)),
])
model.fit(X_train, y_train)
print("Trained.")
That .fit(X_train, y_train) is the entire training procedure: the optimiser runs, the loss minimises, the model’s knobs settle into place. On a small dataset like this, it takes a fraction of a second.
Step 6 — evaluate and read the weights
First, predictions on the test set:
from sklearn.metrics import accuracy_score, classification_report
predictions = model.predict(X_test)
print("Test accuracy:", accuracy_score(y_test, predictions))
print()
print(classification_report(y_test, predictions))
You will see something like:
Test accuracy: 0.83
precision recall f1-score support
False 0.87 0.91 0.89 2031
True 0.71 0.62 0.66 540
accuracy 0.83 2571
macro avg 0.79 0.76 0.78 2571
weighted avg 0.83 0.83 0.83 2571
83% accuracy. Chapter 4 will teach you that accuracy alone is misleading — and explain precision, recall, and F1 properly. For now, notice: the model is better at predicting “not defaulted” (87% precision, 91% recall) than “defaulted” (71% precision, 62% recall). That asymmetry is real and important. Hold the question for Chapter 4.
Now read the weights:
import numpy as np
classifier = model.named_steps["classifier"]
preprocessor = model.named_steps["preprocess"]
# Get the feature names after one-hot encoding
encoded_names = preprocessor.get_feature_names_out()
# Coefficients
coefs = classifier.coef_[0]
for name, coef in sorted(zip(encoded_names, coefs), key=lambda x: abs(x[1]), reverse=True)[:10]:
print(f" {name:40s} {coef:+.3f}")
print(f" {'intercept':40s} {classifier.intercept_[0]:+.3f}")
Output (illustrative):
num__loan_to_income_ratio +0.812
num__monthly_income -0.394
num__loan_amount +0.215
cat__district_Karnali +0.184
cat__district_Bagmati -0.122
...
intercept -1.456
Read this. The model learned that:
- A higher
loan_to_income_ratioincreases the predicted probability of default (+0.812 weight). Expected and reassuring. - Higher
monthly_incomedecreases it (-0.394). Expected. - Higher
loan_amountalone — controlling for income — slightly increases default risk. Sensible. - Loans from Karnali district have slightly higher predicted default risk; Bagmati lower. Worth investigating whether this is a real signal or an artefact of bias in the training data (recall Course 02 Chapter 6).
This is the part of working with sklearn that practitioners enjoy most. The model is not a black box: you can read what it learned. The weights are a story your data told the model. Reading the story before deploying anything is part of the discipline.
The full script in one place
For reference, the whole pipeline assembled:
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.metrics import accuracy_score, classification_report
def main():
df = pd.read_csv("data/clean/loans.csv")
df["loan_to_income_ratio"] = df["loan_amount"] / df["monthly_income"]
numeric = ["loan_amount", "monthly_income", "loan_to_income_ratio"]
categorical = ["district"]
X = df[numeric + categorical].copy()
y = df["defaulted"].copy()
mask = X.notna().all(axis=1) & y.notna()
X, y = X[mask], y[mask]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42,
)
model = Pipeline([
("preprocess", ColumnTransformer([
("num", StandardScaler(), numeric),
("cat", OneHotEncoder(handle_unknown="ignore"), categorical),
])),
("classifier", LogisticRegression(max_iter=1000)),
])
model.fit(X_train, y_train)
predictions = model.predict(X_test)
print("Test accuracy:", accuracy_score(y_test, predictions))
print(classification_report(y_test, predictions))
if __name__ == "__main__":
main()
Under fifty lines. Run it. Watch the accuracy print. Read the report. You have done end-to-end ML.
What to take from this section
Three things to carry forward:
-
The shape is permanent. Load, choose features, split, preprocess, fit, evaluate, read. Every project in the rest of this course follows this rhythm. The models change. The pattern doesn’t.
-
The test accuracy of 83% is a starting point. Without context, it is just a number — it does not tell you whether the model is useful. Chapter 4 will show why accuracy can lie, and how to read the precision/recall asymmetry properly.
-
Reading the weights is part of the job. A model whose coefficients you cannot explain is a model you should not deploy. For simple models, sklearn makes this easy. Use the access.
Check your understanding
Quick check
—A teammate writes: preprocess = StandardScaler() preprocess.fit(X) # fit on the entire dataset X_train, X_test = train_test_split(preprocess.transform(X), y) model.fit(X_train, y_train) Why is this the wrong order, and what does the Pipeline approach in this section do instead?
Quick check
—Your trained classifier shows test accuracy of 95%. You're about to deploy it. The classification report shows 95% accuracy because the dataset is 95% non-defaulters and only 5% defaulters, and the model predicts 'no default' for almost every loan. What's the correct read?
What comes next
You have trained, evaluated, and read your first classifier. The whole rest of this course is sharpening each step. Chapter 2 introduces two more classifiers — nearest neighbours and decision trees — and a fully worked SMS-spam example. Chapter 3 covers regression (predicting numbers). Chapter 4 deeply addresses the accuracy/precision/recall question raised in this section. Chapter 5 makes models better with feature engineering and tuning. Chapter 6 is a full capstone project.
The pattern stays. The competence grows.