🧠 Classical ML · Tier 2 🔴 Advanced MODULE 10 — FINAL

Capstone Project

⏱️ 4 hours
🚀 Full Pipeline + Flask Deployment
🧩 5 Quiz Questions
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.

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.

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)

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.
Neural Networks CNNs RNNs / LSTMs Transformers Transfer Learning PyTorch
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.
← Course Home Tier 3: Deep Learning →
🧩 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?