LearnFrenzy
Sign in Take a free test

Evaluating & Shipping Models · Lesson 6 of 6

Your First End-to-End Machine Learning Project with scikit-learn

Put everything together: frame the problem, explore the data, build a leak-proof pipeline, compare models with cross-validation, tune, evaluate once, save the model and use it for predictions — the workflow interviewers want to hear.

15 min read Intermediate Updated Nov 9, 2026 Reviewed by Saurabh Samir

Knowing individual algorithms is like knowing individual cricket shots. A project is playing a full innings. This lesson walks through a complete project in the order professionals follow — and in the order you should explain your own projects in interviews.

The problem

A telecom company wants to predict which customers are likely to leave (churn) in the next month, so the retention team can offer them a better plan. Our data has one row per customer: tenure in months, monthly charges, contract type, internet service, payment method, number of support calls — and whether they churned.

Step 1 — Frame it

  • Task: binary classification (churn yes/no).
  • Decision it supports: who gets a retention call this month — the team can call about 500 people.
  • Metric: churners are a minority, and a missed churner costs more than an extra call, so we focus on recall and precision among the top-ranked customers, with PR-AUC to compare models.
  • Baseline: "call the customers with the shortest tenure" — our model must beat that rule.

Step 2 — Explore before modelling

import pandas as pd

df = pd.read_csv("telecom_churn.csv")
print(df.shape, df["churn"].mean().round(3))            # how many rows, what share churned
print(df.isna().sum())
print(df.groupby("contract")["churn"].mean())            # month-to-month churns far more?
print(df.groupby(pd.cut(df["tenure"], [0, 6, 12, 24, 72]))["churn"].mean())

Exploration gives you hypotheses ("short tenure and month-to-month contracts churn most") and catches problems (missing values, impossible numbers) before they poison the model.

Step 3 — A pipeline and a fair comparison

from sklearn.model_selection import train_test_split, StratifiedKFold, cross_val_score
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier

numeric     = ["tenure", "monthly_charges", "support_calls"]
categorical = ["contract", "internet_service", "payment_method"]
X, y = df[numeric + categorical], df["churn"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)

prepare = ColumnTransformer([
    ("num", Pipeline([("impute", SimpleImputer(strategy="median")), ("scale", StandardScaler())]), numeric),
    ("cat", Pipeline([("impute", SimpleImputer(strategy="most_frequent")),
                      ("onehot", OneHotEncoder(handle_unknown="ignore"))]), categorical),
])
candidates = {
    "logistic": LogisticRegression(max_iter=1000, class_weight="balanced"),
    "forest":   RandomForestClassifier(n_estimators=300, class_weight="balanced", random_state=42),
}
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
for name, clf in candidates.items():
    pipe = Pipeline([("prepare", prepare), ("model", clf)])
    scores = cross_val_score(pipe, X_train, y_train, cv=cv, scoring="average_precision")
    print(f"{name:9s} PR-AUC {scores.mean():.3f} ± {scores.std():.3f}")

Step 4 — Tune the better model

from sklearn.model_selection import GridSearchCV

pipe = Pipeline([("prepare", prepare),
                 ("model", RandomForestClassifier(class_weight="balanced", random_state=42))])
grid = GridSearchCV(pipe,
                    param_grid={"model__n_estimators": [200, 400],
                                "model__max_depth": [None, 8, 12],
                                "model__min_samples_leaf": [1, 5]},
                    scoring="average_precision", cv=cv, n_jobs=-1)
grid.fit(X_train, y_train)
print(grid.best_params_, round(grid.best_score_, 3))
best = grid.best_estimator_

The model__ prefix tells the grid search which pipeline step a parameter belongs to. Because preparation is inside the pipeline, every fold is prepared using only its own training part — no leakage.

Step 5 — Evaluate once on the test set

from sklearn.metrics import average_precision_score, classification_report

proba = best.predict_proba(X_test)[:, 1]
print("Test PR-AUC:", round(average_precision_score(y_test, proba), 3))

top = pd.DataFrame({"churn": y_test.values, "score": proba}).sort_values("score", ascending=False)
k = max(1, int(len(top) * 0.10))                         # the 10% of customers the team can call
print(f"Churners caught in top 10%: {top['churn'].head(k).sum()} of {top['churn'].sum()}")

That last line speaks the business language: "If the team calls the top 10% our model ranks, they reach X% of all churners — compared with Y% using the tenure rule."

Step 6 — Save and use the model

import joblib

joblib.dump(best, "churn_model.joblib")                  # the whole pipeline: preparation + model

model = joblib.load("churn_model.joblib")
new_customer = pd.DataFrame([{"tenure": 3, "monthly_charges": 799, "support_calls": 4,
                              "contract": "Month-to-month", "internet_service": "Fiber",
                              "payment_method": "UPI"}])
print("Churn risk:", round(model.predict_proba(new_customer)[0, 1], 2))

Saving the whole pipeline means new raw data goes in exactly as the original did. To serve predictions to an app, wrap this in a small API (FastAPI or Flask) — a natural next step.

Step 7 — After launch

  • Monitor input data and prediction rates; customer behaviour changes (data drift), and models quietly get worse.
  • Measure the outcome: did retention calls to high-risk customers actually reduce churn?
  • Retrain on fresh data on a schedule.

How to present this project in an interview: problem and decision → data and key findings from exploration → baseline → models compared with cross-validation → final test result in business terms → what you would improve next. Five minutes, in that order.

Try it yourself: Find a public churn dataset (search "telco customer churn dataset") and follow these seven steps in a notebook. Write a README with your problem framing, baseline, final metric and one chart. That is a portfolio project recruiters actually open.

Common interview questions

Try answering each one aloud before you open it.

1Walk me through how you would build an ML model end to end.

Frame the problem and metric with the business decision in mind, explore and clean the data, set a baseline, build a preprocessing-plus-model pipeline, compare models with cross-validation, tune the best one, evaluate once on a held-out test set, save the pipeline, and monitor and retrain after deployment.

2Why put preprocessing inside a Pipeline?

So preprocessing is fitted only on training data within each cross-validation fold, preventing leakage, and so the exact same transformations are applied automatically at prediction time.

3What is data drift?

A change over time in the distribution of input data or in the relationship between inputs and the target, which can degrade a deployed model’s performance and is detected through monitoring.

Practise this

Build something with it

FAQs

Where can I find datasets for ML projects?

Public sources like Kaggle, the UCI Machine Learning Repository and India’s data.gov.in. Choose a dataset connected to a real decision so your project tells a story.

How do I deploy a scikit-learn model?

Save the pipeline with joblib, load it in a small API built with FastAPI or Flask, and host it on a cloud service. The Cloud Computing track covers the hosting side.

References & further reading

Finished this lesson?

Mark it complete. Create a free account to save it on every device.

Take the Skill Check

Search LearnFrenzy

Esc

Quick links