🧠 Classical ML · Tier 2
🔴 Advanced
MODULE 10 — FINAL
Capstone Project
Classical ML — Module 10 of 100%
🎯 What you'll learn: Framing a real ML problem from scratch, building a full end-to-end pipeline (EDA → feature engineering → model selection → evaluation), packaging a trained model as a REST API with Flask and FastAPI, presenting results to non-technical stakeholders, and a preview of where Deep Learning (Tier 3) picks up from here.
10.1 · The Real ML Project Workflow
From Business Problem to Deployed Model
Tutorials show you how to fit a model. Real projects start with a vague business goal and end with a system someone else uses. The gap between the two is wider than most courses acknowledge.
Frame the problem precisely
What exactly are we predicting? Who uses the output? What is the cost of a false positive vs false negative? What baseline does the model need to beat?
Exploratory Data Analysis (EDA)
Understand distributions, missingness, correlations, outliers. The purpose is forming hypotheses about useful features — not producing charts for a report.
Feature engineering & preprocessing
Build domain-informed features. Set up a Pipeline so no preprocessing step leaks between CV folds.
Model selection & tuning
Start with a simple baseline (logistic regression, mean predictor). Improve systematically with RandomizedSearchCV. Track experiments.
Evaluate & interpret
Use holdout test set only once. Report precision/recall breakdown. Use SHAP or permutation importance to explain predictions.
Deploy & monitor
Serialize with joblib. Expose as REST API. Monitor for data drift — model performance degrades as the world changes.
10.2 · Complete Pipeline — House Price Prediction
Everything in One Reusable Script
Full end-to-end ML pipeline (Ames Housing dataset)PYTHON
import pandas as pd import numpy as np import joblib from sklearn.model_selection import train_test_split, RandomizedSearchCV from sklearn.pipeline import Pipeline from sklearn.compose import ColumnTransformer from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.impute import SimpleImputer from xgboost import XGBRegressor from sklearn.metrics import mean_absolute_percentage_error from scipy.stats import loguniform, randint # ── 1. Load ────────────────────────────────────────────── df = pd.read_csv('ames_housing.csv') target = 'SalePrice' df[target] = np.log1p(df[target]) # log-transform skewed target # ── 2. Feature engineering ─────────────────────────────── df['TotalSF'] = df['TotalBsmtSF'] + df['GrLivArea'] df['HouseAge'] = df['YrSold'] - df['YearBuilt'] df['RemodAge'] = df['YrSold'] - df['YearRemodAdd'] df['TotalBath'] = (df['FullBath'] + 0.5*df['HalfBath'] + df['BsmtFullBath'] + 0.5*df['BsmtHalfBath']) num_cols = ['TotalSF', 'HouseAge', 'RemodAge', 'TotalBath', 'OverallQual', 'OverallCond', 'LotArea'] cat_cols = ['Neighborhood', 'BldgType', 'HouseStyle'] X = df[num_cols + cat_cols] y = df[target] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42) # ── 3. Pipeline ────────────────────────────────────────── num_pipe = Pipeline([ ('imp', SimpleImputer(strategy='median')), ('scale', StandardScaler()) ]) cat_pipe = Pipeline([ ('imp', SimpleImputer(strategy='most_frequent')), ('ohe', OneHotEncoder(handle_unknown='ignore', sparse_output=False)) ]) pre = ColumnTransformer([ ('num', num_pipe, num_cols), ('cat', cat_pipe, cat_cols) ]) pipe = Pipeline([('pre', pre), ('model', XGBRegressor())]) # ── 4. Tune ────────────────────────────────────────────── param_dist = { 'model__n_estimators': randint(200, 800), 'model__learning_rate': loguniform(0.01, 0.3), 'model__max_depth': randint(3, 8), 'model__subsample': [0.7, 0.8, 0.9], } search = RandomizedSearchCV(pipe, param_dist, n_iter=40, cv=5, scoring='neg_root_mean_squared_error', n_jobs=-1) search.fit(X_train, y_train) # ── 5. Evaluate (once, on holdout) ─────────────────────── y_pred_log = search.predict(X_test) y_pred = np.expm1(y_pred_log) # undo log1p mape = mean_absolute_percentage_error(np.expm1(y_test), y_pred) print(f"Test MAPE: {mape:.2%}") # target: < 8% # ── 6. Save ────────────────────────────────────────────── joblib.dump(search.best_estimator_, 'house_price_model.pkl', compress=3)
10.3 · Deployment — Flask REST API
Serving Predictions Over HTTP
A trained model is useless unless something can call it. Flask is the simplest way to expose a model as a REST API — a single endpoint that accepts JSON input and returns predictions. FastAPI is a modern alternative with automatic input validation and async support.
Flask prediction API — app.pyPYTHON
from flask import Flask, request, jsonify import joblib import pandas as pd import numpy as np app = Flask(__name__) model = joblib.load('house_price_model.pkl') @app.route('/predict', methods=['POST']) def predict(): data = request.get_json() df = pd.DataFrame([data]) # Feature engineering must match training pipeline df['TotalSF'] = df['TotalBsmtSF'] + df['GrLivArea'] df['HouseAge'] = df['YrSold'] - df['YearBuilt'] df['RemodAge'] = df['YrSold'] - df['YearRemodAdd'] df['TotalBath'] = (df['FullBath'] + 0.5*df['HalfBath'] + df['BsmtFullBath'] + 0.5*df['BsmtHalfBath']) log_price = model.predict(df)[0] price = float(np.expm1(log_price)) return jsonify({'predicted_price': round(price, 2)}) if __name__ == '__main__': app.run(debug=True, port=5000)
FastAPI alternative — main.pyPYTHON
from fastapi import FastAPI from pydantic import BaseModel import joblib, numpy as np class HouseFeatures(BaseModel): # Pydantic validates input types TotalBsmtSF: float GrLivArea: float OverallQual: int Neighborhood: str # ... other fields app = FastAPI() model = joblib.load('house_price_model.pkl') @app.post('/predict') def predict(features: HouseFeatures): data = features.dict() # ... feature engineering same as training log_price = model.predict(pd.DataFrame([data]))[0] return {'predicted_price': round(float(np.expm1(log_price)), 2)}
💡
Feature engineering consistency is critical
The biggest deployment mistake: the prediction API computes features differently than the training script. Double-underscore edge cases (how NaN is handled, rounding, column ordering) cause silent errors. Extract feature engineering into a shared function called by both training and serving code.
Ready for Tier 3 — Deep Learning?
Classical ML has limits: it struggles with raw pixels, audio waveforms, and long-range language dependencies. Deep learning uses end-to-end differentiable architectures to learn representations directly from raw data — no manual feature engineering required.
Start Tier 3: Deep Learning →
🏆
Tier 2 — Classical ML Complete!
You've mastered sklearn pipelines, supervised and unsupervised learning, feature engineering, model tuning, time series, recommenders, and deployment. You are now a capable ML practitioner.
🧩 Module 10 — Capstone Check
5 questions · instant feedback
1. You log-transform the target variable (house prices) before training. What must you do when serving predictions?
2. Why should the test holdout set be used only once, at the very end?
3. A colleague checks your deployed Flask API and says "the predictions are 10x too small." What is the most likely cause?
4. What is "data drift" and why does it matter after deployment?
5. What is the most important reason to put all feature engineering inside a sklearn Pipeline?