ailiteracynepal 🇳🇵
Text size

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:

  1. Load the cleaned dataset.
  2. Choose features and the target column.
  3. Split into train and test.
  4. Scale numeric features.
  5. Train a logistic regression.
  6. 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_ratio increases the predicted probability of default (+0.812 weight). Expected and reassuring.
  • Higher monthly_income decreases it (-0.394). Expected.
  • Higher loan_amount alone — 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:

  1. 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.

  2. 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.

  3. 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.