🎯 What You'll Learn
- The 8 canonical stages of an ML project and how they connect to one another
- How to write a Scope of Work (SOW) that defines success before touching data
- What distinguishes high-quality training data from poor-quality data, and how to evaluate it
- How to choose a baseline model, fine-tune it, and deploy it via a REST API
- What model drift is, why it happens, and how MLOps practices (MLflow, PSI) catch it early
Most courses dive straight into algorithms. But in the real world, the majority of ML project failures are not algorithmic — they fail at the problem definition, data quality, or deployment stage. Understanding the full lifecycle before you write a single line of model code is what separates engineers who ship from engineers who prototype.
Because this lesson tours the entire journey, it names many tools and metrics you haven't learned yet — algorithm names like XGBoost (Lesson 24), evaluation metrics like AUC-ROC and recall (Lessons 19–20), deployment tools like MLflow (Phase 6). None of them are prerequisites. When an unfamiliar name appears, note which stage it belongs to and move on — every one of them gets its own lesson later in the course. What you should take from this lesson is the shape of the journey.
1 The 8 Stages of an ML Project
Every ML project — whether it's a fraud detection system at a bank, a recommendation engine at a streaming service, or a predictive maintenance model at a factory — passes through the same eight stages. The stages are rarely purely sequential; in practice they are deeply iterative, with frequent loops between stages 3–6 especially. But understanding the full map gives you the mental model to navigate any project.
| Stage | Name | Key Question | Output |
|---|---|---|---|
| 1 | Business Understanding & SOW | What problem are we solving? | Scope of Work document |
| 2 | Data Collection | Do we have the right data? | Raw dataset(s) |
| 3 | Exploratory Data Analysis | What does the data look like? | EDA notebook, insights |
| 4 | Feature Engineering | What inputs should the model use? | Feature matrix X, target y |
| 5 | Model Selection & Training | Which algorithm and why? | Trained candidate models |
| 6 | Fine-Tuning & Evaluation | Is this good enough to ship? | Final model + metrics report |
| 7 | Deployment | How do users/systems consume it? | API endpoint, batch job |
| 8 | Monitoring & Feedback | Is the model still accurate? | Alerts, retrain triggers |
The diagram above shows stages 1–8 in order, but in practice you will loop. EDA (stage 3) reveals that you need more data (back to stage 2). Fine-tuning (stage 6) reveals that a feature is leaking information (back to stage 4). Monitoring (stage 8) detects data drift (back to stages 2–4). Plan for iteration from the start — a project that never revisits earlier stages is probably missing something.
The table above lists the stages in reading order, but the real shape of an ML project is a loop, not a line. The diagram below makes that loop explicit — and color-codes each stage by the kind of work it really is: defining the problem, working with data, building the model, or running it in production.
The 8 lifecycle stages flow left-to-right across the top (problem definition → data work), then continue right-to-left across the bottom (modeling → ops). The dashed amber arrow shows the feedback loop: monitoring detects drift in production and sends the project back to data collection.
2 Stage 1: Business Understanding & Scope of Work
The single most important stage of any ML project is the one that happens before any code is written: agreeing on precisely what problem you are solving, what success looks like, and what constraints you are operating under. This agreement is captured in a Scope of Work (SOW) document.
A good SOW answers five questions:
- Problem type — Is this classification (spam/not spam), regression (predict a price), clustering (group customers), ranking, or something else?
- Success metric — What number defines "good enough"? For credit risk: AUC-ROC ≥ 0.85. For a recommender: CTR uplift ≥ 5%. The metric must be tied to a business outcome.
- Data requirements — What features are needed? How much historical data? Any labeling effort required?
- Constraints — Latency (must respond in <100ms?), interpretability (regulators require explainability?), fairness, data privacy.
- Deliverables and timeline — A deployed API? A report? A reusable pipeline? By when?
Example SOW: Credit Risk Model
"""
SCOPE OF WORK — Credit Default Risk Model
==========================================
Client: Finova Lending Group
Project: Predict probability that a loan applicant will default within 12 months.
PROBLEM TYPE: Binary classification (default=1, no default=0)
TARGET: loan_default (1 = defaulted within 12 months)
SUCCESS METRICS:
Primary: AUC-ROC >= 0.82 on held-out test set
Secondary: Recall >= 0.70 at a precision threshold of 0.60
(catch 70% of defaults while keeping false positives manageable)
DATA REQUIREMENTS:
- 3 years of loan application records (~150,000 rows)
- Features: applicant income, credit score, debt-to-income ratio,
loan amount, loan purpose, employment length, home ownership
- Label: binary flag from payment records team (already available)
CONSTRAINTS:
- Model must be explainable (regulatory requirement — SHAP values required)
- Inference latency: < 200ms per application
- No use of race, gender, or protected attributes as direct features
- Data stored on-premises — no cloud processing of raw PII
DELIVERABLES:
1. EDA report with data quality findings
2. Trained model (XGBoost or Logistic Regression) + SHAP analysis
3. FastAPI inference endpoint with Swagger docs
4. Monthly drift monitoring report (PSI on input features)
TIMELINE: 8 weeks (EDA: wk1-2, Modeling: wk3-5, Deploy: wk6-7, Monitor setup: wk8)
"""
If you optimize for accuracy on a dataset that is 95% "no default" and 5% "default", a model that predicts "no default" for every applicant achieves 95% accuracy while catching zero defaults. Always define success metrics that match business goals — not just model accuracy. (Lessons 19–20 teach the metrics designed for exactly this situation: precision, recall, F1, and AUC-ROC.)
The SOW above also commits to a timeline: 8 weeks, broken down by stage. Notice how little time is actually spent on the part most beginners associate with "doing ML" — training the model:
Planned effort across the 8-week credit risk SOW. EDA and modeling together still take less calendar time than deployment plus standing up monitoring.
3 Stage 2: Data Collection
The best algorithm trained on bad data will underperform the simplest algorithm trained on good data. Data collection is not glamorous, but it is often the highest-leverage stage of the entire project. There are two main categories of data sources:
Primary vs Secondary Sources
- Primary sources (you collect it yourself): internal databases (SQL, data warehouses), operational logs, surveys, sensors, web scraping, manual labeling (annotation platforms like Label Studio or Scale AI).
- Secondary sources (someone else collected it): public datasets (Kaggle, UCI ML Repository, HuggingFace Datasets, government open data portals), third-party data vendors, open web APIs (Twitter/X, OpenWeather, Alpha Vantage).
The Four Dimensions of Data Quality
Before you trust a dataset for training, evaluate it on four dimensions:
| Dimension | Definition | Warning signs |
|---|---|---|
| Completeness | All required records and fields are present | >5% missing in key features, sparse label coverage |
| Accuracy | Values reflect reality | Age = 200, negative salary, inconsistent label rates |
| Consistency | Same concept encoded the same way across records | "M", "Male", "male", "1" all meaning the same thing |
| Timeliness | Data is recent enough to reflect current patterns | Training on 2015 data for a 2026 model |
import pandas as pd
df = pd.read_csv('loan_applications.csv')
# Quick data quality audit
print("Shape:", df.shape)
print("\nMissing values (%):")
print((df.isnull().mean() * 100).sort_values(ascending=False).head(10))
print("\nValue counts for key categorical columns:")
print(df['loan_purpose'].value_counts())
print("\nNumerical summary:")
print(df[['income', 'loan_amount', 'credit_score']].describe())
# Check for obvious data quality issues
print("\nInvalid ages (< 18 or > 100):")
print(df[(df['age'] < 18) | (df['age'] > 100)].shape[0])
print("\nNegative income rows:")
print(df[df['income'] < 0].shape[0])
4 Stages 3–4: EDA & Feature Engineering
Exploratory Data Analysis (EDA) and Feature Engineering are covered in depth in dedicated lessons, but their role in the lifecycle deserves a clear description here.
EDA (Stage 3) is the process of getting to know your data before you model it. You visualize distributions, check for outliers, examine correlations, and understand the relationship between features and the target. EDA outputs are not just pretty charts — they directly inform the decisions in every subsequent stage. A rare-outcome imbalance found in EDA means you'll need the special handling taught in Lesson 26. A bimodal distribution in an income feature might suggest two distinct customer segments.
Feature Engineering (Stage 4) is transforming raw data into representations the model can learn from effectively. This includes:
- Encoding categorical variables (one-hot encoding, ordinal encoding, target encoding)
- Scaling numerical features (StandardScaler, MinMaxScaler)
- Creating interaction features (credit_score × debt_to_income)
- Extracting date components (day_of_week, month, is_holiday)
- Handling missing values (imputation strategies)
- Log-transforming skewed distributions
A domain expert who knows that "credit_utilization = revolving_balance / credit_limit" is a better predictor than either feature alone will outperform a pure data scientist who blindly throws raw features at a model. The best features come from understanding why data is collected, not just what it contains. Invest time in conversations with business stakeholders before finalizing your feature set.
The Iterative Nature of ML Development
In textbooks, stages appear sequential. In practice, the loop between EDA, feature engineering, model training, and evaluation repeats many times. A typical iteration looks like:
"""
Iteration Loop:
1. Train baseline model on raw features
2. Examine feature importances and error cases
3. Form hypothesis: "credit_utilization_ratio might help"
4. Engineer new feature, add to pipeline
5. Retrain, compare metrics
6. If improved: keep feature; if not: drop it
7. Repeat until diminishing returns
"""
# Iteration 1: baseline features
baseline_features = ['income', 'loan_amount', 'credit_score', 'employment_years']
# After EDA insight: credit utilization is highly predictive
# Iteration 2: add engineered feature
df['credit_utilization'] = df['revolving_balance'] / df['credit_limit']
df['debt_to_income'] = df['total_debt'] / df['annual_income']
df['loan_to_income'] = df['loan_amount'] / df['annual_income']
enhanced_features = baseline_features + ['credit_utilization', 'debt_to_income', 'loan_to_income']
5 Stages 5–6: Model Selection & Fine-Tuning
Model selection is not about finding "the best algorithm in the abstract" — it's about finding the right algorithm for your specific problem, data size, and constraints. A framework for making this decision:
Choosing the Algorithm Family
- Problem type first: classification → logistic regression, decision trees, random forests, XGBoost, SVMs; regression → linear regression, ridge, lasso, gradient boosting, SVR.
- Data size: small data (<10k rows) → regularized linear models, SVMs; medium data → tree ensembles; large data (>1M rows) → gradient boosting, shallow networks or mini-batch SGD.
- Interpretability requirement: regulatory/medical settings → logistic regression, decision trees with depth limit; otherwise → tree ensembles, neural networks fine.
- Baseline first — always: before training a complex model, train the simplest plausible model (logistic regression, a single decision tree, predicting the mean). This establishes a floor — a complex model that doesn't beat the baseline is not worth deploying.
# A sneak peek at what model comparison looks like in practice.
# Every model below is a Phase 2 lesson (11-24); every one follows the
# same 3-button interface: .fit() / .predict() / .score()
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.model_selection import cross_val_score
import numpy as np
# Always start with a baseline
models = {
'Logistic Regression (baseline)': LogisticRegression(max_iter=1000),
'Decision Tree': DecisionTreeClassifier(max_depth=5),
'Random Forest': RandomForestClassifier(n_estimators=100, random_state=42),
'Gradient Boosting': GradientBoostingClassifier(n_estimators=100, random_state=42),
}
results = {}
for name, model in models.items():
# cross_val_score: train/score 5 times on rotating folds (see Lesson 10)
scores = cross_val_score(model, X_train, y_train, cv=5, scoring='accuracy')
results[name] = scores.mean()
print(f"{name:40s} accuracy = {scores.mean():.4f} ± {scores.std():.4f}")
# Pick the best model, then fine-tune it
# Logistic Regression (baseline) accuracy = 0.7812 ± 0.0124
# Decision Tree accuracy = 0.7941 ± 0.0198
# Random Forest accuracy = 0.8347 ± 0.0087
# Gradient Boosting accuracy = 0.8512 ± 0.0063 <-- winner
# (In practice you'd often score with metrics from Lessons 19-20 instead)
Fine-Tuning with Cross-Validation
Hyperparameter tuning means searching over settings that you don't learn from data — the number of trees, learning rate, maximum depth, regularization strength. Always tune using cross-validation on the training set only. Never touch the test set until your final evaluation.
# GridSearchCV tries every combination of the settings below and
# cross-validates each one — a tool you'll learn to wield in Lesson 27.
# For now, just see WHERE tuning sits in the lifecycle.
from sklearn.model_selection import GridSearchCV
param_grid = {
'n_estimators': [100, 200, 300],
'max_depth': [3, 5, 7],
'learning_rate': [0.05, 0.1, 0.2],
'subsample': [0.8, 1.0],
}
grid_search = GridSearchCV(
GradientBoostingClassifier(random_state=42),
param_grid,
cv=5,
scoring='accuracy',
n_jobs=-1,
verbose=1
)
grid_search.fit(X_train, y_train)
print("Best params:", grid_search.best_params_)
print("Best CV accuracy:", grid_search.best_score_)
# Best params: {'learning_rate': 0.1, 'max_depth': 5, 'n_estimators': 200, 'subsample': 0.8}
# Best CV accuracy: 0.8589
6 Stage 7: Deployment
A model that lives in a notebook is not a product. Deployment is the stage where your trained model is packaged and integrated into a system that real users or other software can consume. There are two main deployment patterns:
- Real-time inference: a REST API that accepts a single prediction request and returns a result within milliseconds. Used for: loan approval at point-of-application, spam filtering, fraud detection. Tool: FastAPI, Flask.
- Batch inference: a scheduled job that runs the model on thousands or millions of records at once (nightly, weekly). Used for: churn scoring all customers, generating product recommendations. Tool: scheduled Python scripts, Airflow, Spark.
Model Serialization
Before deploying, you serialize your trained model to disk — converting the in-memory Python object into a file that can be reloaded without retraining.
import joblib
import pickle
# joblib — recommended for sklearn models (handles large arrays efficiently)
joblib.dump(grid_search.best_estimator_, 'credit_risk_model_v1.pkl')
# Load it back
model = joblib.load('credit_risk_model_v1.pkl')
# Make a prediction (same as the original model)
sample = [[65000, 25000, 720, 3, 0.28, 0.38, 0.38]] # 7 features
prob = model.predict_proba(sample)[0][1]
print(f"Default probability: {prob:.3f}") # e.g., 0.124
Serving with FastAPI
# app.py — minimal FastAPI inference endpoint
from fastapi import FastAPI
from pydantic import BaseModel
import joblib
import numpy as np
app = FastAPI(title="Credit Risk API", version="1.0")
model = joblib.load("credit_risk_model_v1.pkl")
class LoanApplication(BaseModel):
annual_income: float
loan_amount: float
credit_score: int
employment_years: int
credit_utilization: float
debt_to_income: float
loan_to_income: float
@app.post("/predict")
def predict_default_risk(application: LoanApplication):
features = [[
application.annual_income,
application.loan_amount,
application.credit_score,
application.employment_years,
application.credit_utilization,
application.debt_to_income,
application.loan_to_income,
]]
prob = model.predict_proba(features)[0][1]
decision = "HIGH RISK" if prob > 0.35 else "LOW RISK"
return {"default_probability": round(prob, 4), "decision": decision}
# Run with: uvicorn app:app --reload
# Test: POST http://localhost:8000/predict
Include a version tag in your model filename (credit_risk_model_v1.pkl). When you retrain, save as v2, v3, etc. Never overwrite a production model in place — always keep the previous version so you can roll back if the new one has issues. Tools like MLflow Model Registry automate this at scale.
7 Stage 8: Monitoring & MLOps
Deploying a model is not the end — it's the beginning of a new responsibility. Models degrade in production for predictable reasons, and catching degradation early prevents real business harm.
Types of Model Degradation
- Concept drift: the statistical relationship between features and target changes. Example: a credit risk model trained before a recession may underestimate default rates after one, because the relationship between income and default probability shifts.
- Data drift (covariate shift): the distribution of input features changes, even if the relationship to the target stays the same. Example: the average loan amount grows over time due to inflation, pushing values outside the training distribution.
- Upstream data issues: a data pipeline change means a feature arrives as null, or in a different unit, or with different encoding.
Detecting Drift with PSI (Population Stability Index)
import numpy as np
import pandas as pd
def calculate_psi(expected, actual, buckets=10):
"""
Population Stability Index (PSI).
PSI < 0.1: stable — no significant change
PSI 0.1-0.2: slight shift — investigate
PSI > 0.2: significant drift — consider retraining
"""
expected_pct = np.histogram(expected, bins=buckets)[0] / len(expected)
actual_pct = np.histogram(actual, bins=np.histogram(expected, bins=buckets)[1])[0] / len(actual)
# Avoid log(0)
expected_pct = np.where(expected_pct == 0, 0.0001, expected_pct)
actual_pct = np.where(actual_pct == 0, 0.0001, actual_pct)
psi = np.sum((actual_pct - expected_pct) * np.log(actual_pct / expected_pct))
return psi
# Compare training distribution vs recent production data
train_income = df_train['annual_income'].values
current_income = df_production_last_30d['annual_income'].values
psi_score = calculate_psi(train_income, current_income)
print(f"PSI for annual_income: {psi_score:.4f}")
# PSI for annual_income: 0.0312 → stable ✓
MLflow for Experiment Tracking
import mlflow
import mlflow.sklearn
# Track an experiment
with mlflow.start_run(run_name="GradientBoosting_v2"):
mlflow.log_params(grid_search.best_params_)
mlflow.log_metric("cv_auc", grid_search.best_score_)
mlflow.log_metric("test_auc", roc_auc_score(y_test, y_pred_proba))
mlflow.sklearn.log_model(grid_search.best_estimator_, "model")
print("Run logged to MLflow. View at: mlflow ui")
The worst time to design your monitoring strategy is after a model has been silently wrong for three months. Build in alerts, dashboards, and retraining triggers as part of the deployment process. A model without monitoring is not a production system — it's a time bomb.
8 Common Pitfalls at Each Stage
Every stage of the ML lifecycle has characteristic failure modes. Knowing them before you encounter them is half the battle:
| Stage | Pitfall | Prevention |
|---|---|---|
| SOW | Wrong success metric (accuracy on imbalanced data) | Always check class balance; prefer AUC/F1 for imbalanced tasks |
| Data Collection | Collecting data that doesn't exist at prediction time | Map each feature to its availability at inference time |
| Feature Engineering | Data leakage — using the target to create a feature | Never use target or future info to create features |
| Preprocessing | Fitting scaler on the full dataset (train + test) | Always fit transformers on train set only; transform test set |
| Fine-Tuning | Overfitting to the validation set during tuning | Use cross-validation, not a single validation split, for tuning |
| Deployment | Training/serving skew — different preprocessing in production | Serialize entire pipeline (not just the model); use sklearn Pipeline |
| Monitoring | No retraining trigger; model silently degrades | Set automated alerts on PSI, prediction distribution, business KPIs |
Data leakage is when information about the target variable (or future events) is inadvertently incorporated into your training features. A model trained with leakage will appear to perform brilliantly in evaluation — 99% AUC! — and then fail completely in production. Common sources: including post-event features, scaling on the full dataset, using the target column as a feature by accident, date-based splits that mix future data into training.
Real-World Spotlight: End-to-End Credit Risk Model
Let's trace all 8 stages for the credit risk project introduced in the SOW above:
# Stage 2 — Data Collection
df = pd.read_sql("SELECT * FROM loan_applications WHERE year >= 2022", conn)
print(df.shape) # (148321, 23)
print(df.isnull().sum()) # credit_score: 1204 missing, employment_years: 89 missing
# Stage 3 — EDA
print(df['loan_default'].value_counts(normalize=True))
# 0 0.937 — 93.7% no default
# 1 0.063 — 6.3% default → class imbalance detected! Use scale_pos_weight
import matplotlib.pyplot as plt
df.boxplot(column='annual_income', by='loan_default') # income distribution by class
# Stage 4 — Feature Engineering
df['credit_utilization'] = df['revolving_balance'] / df['credit_limit'].clip(lower=1)
df['loan_to_income'] = df['loan_amount'] / df['annual_income'].clip(lower=1)
df['credit_score'].fillna(df['credit_score'].median(), inplace=True) # impute
# Stage 5 — Model Selection (baseline first)
from sklearn.linear_model import LogisticRegression
lr = LogisticRegression(max_iter=500)
lr_auc = cross_val_score(lr, X_train_scaled, y_train, cv=5, scoring='roc_auc').mean()
print(f"Baseline LR AUC: {lr_auc:.4f}") # 0.7812
# XGBoost as primary model (handles imbalance via scale_pos_weight)
import xgboost as xgb
ratio = (y_train == 0).sum() / (y_train == 1).sum() # ~14.9
xgb_model = xgb.XGBClassifier(scale_pos_weight=ratio, n_estimators=200,
max_depth=5, learning_rate=0.1, random_state=42)
xgb_auc = cross_val_score(xgb_model, X_train, y_train, cv=5, scoring='roc_auc').mean()
print(f"XGBoost CV AUC: {xgb_auc:.4f}") # 0.8641 ✓ meets threshold
# Stage 6 — Final evaluation on held-out test set
xgb_model.fit(X_train, y_train)
test_auc = roc_auc_score(y_test, xgb_model.predict_proba(X_test)[:, 1])
print(f"Test AUC: {test_auc:.4f}") # 0.8589
# Stage 7 — Serialize and deploy
import joblib
joblib.dump(xgb_model, "credit_risk_xgb_v1.pkl")
# Deploy with FastAPI (see section 6 above)
# Stage 8 — Monitoring (run monthly)
psi = calculate_psi(X_train['credit_utilization'], X_prod_last_30d['credit_utilization'])
print(f"PSI credit_utilization: {psi:.4f}") # 0.042 → stable
Quick Check
✍️ Practice Exercises
- Write a one-page SOW for the following scenario: "A retail company wants to predict which customers will churn in the next 30 days." Define the problem type, success metric, key features, constraints, and deliverables.
- You have a dataset of 50,000 loan applications. Write Python code that outputs: (a) percentage of missing values per column, (b) count of rows with any missing value, (c) the class balance of the target variable.
- A teammate proposes using "days_until_payment_missed" as a feature for a default prediction model. Explain why this is an example of data leakage and how you would fix it.
- Using the
mlflowlibrary, log one complete training run that records: model type, hyperparameters, cross-validation AUC, and the serialized model artifact.
▶ Show hints
# Exercise 2
df = pd.read_csv('loan_applications.csv')
# (a) Missing per column
missing_pct = (df.isnull().mean() * 100).sort_values(ascending=False)
print(missing_pct[missing_pct > 0])
# (b) Rows with any missing value
print(df.isnull().any(axis=1).sum())
# (c) Class balance
print(df['loan_default'].value_counts(normalize=True))
# Exercise 3 — Leakage explanation:
# 'days_until_payment_missed' is derived from the future (the event we're predicting).
# At the time of application, this value does not exist.
# Fix: remove it from features; only use information available at application time.
📚 Primary Source for This Lesson
Rules of Machine Learning — Google Engineering
43 practical rules for ML projects, written by engineers who have shipped hundreds of real-world models. Especially relevant: Rules 1–12 on problem definition and baselines, Rules 27–38 on training/serving skew and model freshness.