Build an ML Prediction Pipeline

Train a classifier and serve predictions through a REST API.

Introduction

30 Second Summary

Every time a streaming service predicts you might cancel your subscription, or a bank flags a suspicious transaction, there is an ML prediction system working behind the scenes. These systems follow a repeatable pattern: take data, train a model, and serve predictions through an endpoint that other software can call.

In this project, you will build a complete machine learning prediction pipeline that takes raw customer data, trains a classifier, evaluates its performance, and serves live predictions through a REST API.

What You'll Build

A local ML prediction service for customer churn classification, built with scikit-learn for training and FastAPI for serving predictions via HTTP.

By the end of this project, you'll have:

  • A modular ML pipeline that preprocesses mixed feature types, trains a Random Forest classifier, and evaluates it with standard metrics.
  • A serialized model artifact that bundles preprocessing and prediction into one portable file, preventing train/serve skew.
  • A running FastAPI prediction endpoint with automatic Swagger documentation that accepts customer data and returns churn predictions via HTTP.
  • Secret Mission: Add a health check endpoint and batch prediction support to your API.

Are there any prerequisites?

You should be comfortable with basic Python, including writing functions and working with classes. No prior machine learning experience is needed.

Before We Start

Before We Start...

Set Up Your Python Environment

Every ML system starts with the same foundation: a clean, isolated environment where dependencies don't conflict with other projects on your machine.

In this step, you'll create a dedicated project folder, set up a Python virtual environment, and install the libraries you'll need for data processing, model training, and API serving.

In this step, get ready to:
  • Create a project directory and virtual environment.
  • Install ML and API dependencies.
  • Set up your project file structure.
Create a project directory and virtual environment

Before writing any code, you need an isolated space where your project's packages won't interfere with anything else on your system.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder by running this command:
mkdir ml-pipeline
  • Confirm the folder was created by running:
ls

You should see ml-pipeline in the output.

Now open the folder in your editor.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code (or PyCharm) and press Enter to open your editor.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the ml-pipeline folder, and click Open.

Next, open the integrated terminal inside your editor.

  • Press Cmd+` (macOS) or Ctrl+` (Windows) to toggle the terminal panel.

Why the integrated terminal?

Using the terminal built into your editor means it automatically starts in your project folder. You won't need to navigate back to ml-pipeline every time you run a command.

  • Create your virtual environment by running this command:
python -m venv .venv

What does this command do?

python -m venv .venv creates a self-contained Python installation inside a .venv folder in your project. Any packages you install here stay isolated from the rest of your system.

Now activate the virtual environment so your terminal uses the isolated Python.

macOS

  • Activate the virtual environment by running:
source .venv/bin/activate

Windows

  • Activate the virtual environment by running:
.venv\Scripts\activate

You should see (.venv) appear at the beginning of your terminal prompt. This confirms you're working inside the virtual environment.

  • Verify your Python version by running:
python --version

You should see Python 3.13.x in the output.

Don't see (.venv) in your prompt?

Make sure you ran the activation command for your operating system. On macOS it starts with source. On Windows it starts with .venv\Scripts\.

If you see a permissions error on Windows, you may need to allow script execution. Run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned in PowerShell, then try the activation command again.

help me activate my Python virtual environment

Install project dependencies

Your project needs four libraries: scikit-learn for ML pipelines, pandas for data manipulation, FastAPI for serving predictions, and joblib for saving trained models to disk.

  • In your editor, create a new file by pressing Cmd+N (macOS) or Ctrl+N (Windows).
  • Save it immediately with Cmd+S (macOS) or Ctrl+S (Windows) and name it requirements.txt.
  • Paste the following content into requirements.txt:
scikit-learn==1.9.0
pandas==3.0.3
fastapi[standard]==0.136.3
joblib==1.5.3

What does this file do?

A requirements.txt file pins the exact version of every dependency your project uses. This ensures anyone who runs your project gets the same library versions and avoids compatibility issues.

The == syntax locks each package to a specific version. For example, scikit-learn==1.9.0 means exactly version 1.9.0 of scikit-learn.

  • Save requirements.txt.
  • Install all the dependencies by running this command in your terminal:
pip install -r requirements.txt

This downloads and installs all four packages plus their sub-dependencies. It may take a minute.

  • Verify the installation by running:
python -c "import sklearn; print(sklearn.__version__)"

You should see 1.9.0 printed in your terminal.

Seeing an installation error?

Make sure your virtual environment is active. You should see (.venv) in your terminal prompt. If not, run the activation command from the previous substep.

If you see a network error, check your internet connection and try running pip install -r requirements.txt again.

help me fix my pip install error

✔️ Awesome, I've got everything!

Great. Double check you've saved requirements.txt and confirmed scikit-learn prints 1.9.0.

ⓧ I'd like to double check the full code

Here's the complete requirements.txt file:

scikit-learn==1.9.0
pandas==3.0.3
fastapi[standard]==0.136.3
joblib==1.5.3
Create the project file structure

Your project will have three Python modules, each responsible for one stage of the ML lifecycle: generating data, training the model, and serving predictions.

macOS

  • Create the three project files by running this command:
touch data.py train.py serve.py

Windows

  • Create the three project files by running this command:
New-Item -ItemType "File" -Path "data.py", "train.py", "serve.py"
  • Confirm the files exist by running:
ls

You should see data.py, train.py, serve.py, and requirements.txt in the output (along with the .venv folder).

Why three separate files?

Each file handles one responsibility. data.py generates training data. train.py builds and trains the model. serve.py serves predictions through an API.

This separation mirrors how production ML systems are structured. It lets you re-run training without touching the API, or swap out the data source without changing the model code.

Don't see the files in your editor sidebar?

Your editor's file explorer might need a moment to refresh. In VS Code, click anywhere in the Explorer sidebar on the left. You should see all three .py files listed under ml-pipeline.

help me find my project files in VS Code

Your environment is ready and your project skeleton is in place. Next up, you'll build the data pipeline that generates a synthetic customer dataset for training.

Build the Data Pipeline

Your environment is locked and loaded. Now it's time to build the data that your ML model will learn from.

Every ML pipeline starts with data. In production, this usually comes from a database or data warehouse. For this project, you'll generate synthetic customer data programmatically so the project stays self-contained and repeatable.

In this step, get ready to:
  • Create a synthetic customer churn dataset with numeric and categorical features.
  • Build a preprocessing pipeline that transforms raw features into model-ready inputs.
Create a synthetic customer churn dataset

Customer churn means a customer stops using a service. Predicting churn lets companies intervene before losing customers. Your dataset will simulate customer records with features like how long they've been a customer, how much they pay, and what contract type they're on.

Why synthetic data?

Real datasets require downloads, licensing, and cleaning. Generating data programmatically keeps the project self-contained and lets you control the exact properties (number of rows, class balance, feature distributions) your pipeline needs.

  • Open data.py in your editor and add the following imports and generate_data() function:
import numpy as np
import pandas as pd
from sklearn.datasets import make_classification


def generate_data(n_samples=1000, random_state=42):
    """Generate a synthetic customer churn dataset."""
    # Generate numeric features and binary target using sklearn
    X, y = make_classification(
        n_samples=n_samples,
        n_features=3,
        n_informative=3,
        n_redundant=0,
        n_classes=2,
        weights=[0.7, 0.3],
        random_state=random_state,
    )

    # Create a categorical feature with realistic contract type distribution
    rng = np.random.default_rng(random_state)
    contract_types = rng.choice(
        ["month-to-month", "one-year", "two-year"],
        size=n_samples,
        p=[0.5, 0.3, 0.2],
    )

    # Build a DataFrame with realistic column names and value ranges
    df = pd.DataFrame(
        {
            "tenure": np.clip(X[:, 0] * 20 + 30, 1, 72).astype(int),
            "monthly_charges": np.clip(X[:, 1] * 20 + 65, 20, 120).round(2),
            "total_charges": np.clip(X[:, 2] * 1000 + 3000, 100, 8000).round(2),
            "contract_type": contract_types,
            "churned": y,
        }
    )

    return df

What does this code do?

This function builds a fake-but-realistic customer dataset with 1000 rows.

  • make_classification from scikit-learn generates three numeric features with a known relationship to the binary target (churned or not).
  • weights=[0.7, 0.3] makes the dataset imbalanced. 70% of customers stay, 30% churn. This mirrors real-world churn rates.
  • np.clip scales the raw feature values into realistic ranges (e.g., tenure between 1 and 72 months, monthly charges between $20 and $120).
  • contract_types adds a categorical column. The model needs to handle both numbers and text, just like production data.

The function is ready, but you need a way to run it directly and confirm the output looks correct.

  • Add this block at the bottom of data.py, below the return df line:

if __name__ == "__main__":
    df = generate_data()
    print(f"Dataset shape: {df.shape}")
    print(f"Columns: {list(df.columns)}")
    print(f"Churn rate: {df['churned'].mean():.2%}")
    print(df.head())

What does this block do?

The if __name__ == "__main__" guard means this code only runs when you execute the file directly (e.g., python data.py). It won't run when another file imports generate_data().

  • Save data.py.
  • Run the file to verify your dataset by running this command:
python data.py

You should see output showing a dataset shape of (1000, 5), all five column names, a churn rate around 30%, and the first five rows of data.

Seeing a ModuleNotFoundError?

  • Check that your virtual environment is active. Your terminal prompt should show (.venv) at the beginning.
  • If it's not showing, re-activate with source .venv/bin/activate (macOS/Linux) or .venv\Scripts\activate (Windows).
  • Confirm packages are installed by running pip install -r requirements.txt again.

Help me debug a ModuleNotFoundError when running data.py

✔️ Awesome, I've got everything!

Great. Double check you've saved your file and you're seeing the dataset output in your terminal.

ⓧ I'd like to double check the full code

Here's the complete data.py file for comparison:

import numpy as np
import pandas as pd
from sklearn.datasets import make_classification


def generate_data(n_samples=1000, random_state=42):
    """Generate a synthetic customer churn dataset."""
    X, y = make_classification(
        n_samples=n_samples,
        n_features=3,
        n_informative=3,
        n_redundant=0,
        n_classes=2,
        weights=[0.7, 0.3],
        random_state=random_state,
    )

    rng = np.random.default_rng(random_state)
    contract_types = rng.choice(
        ["month-to-month", "one-year", "two-year"],
        size=n_samples,
        p=[0.5, 0.3, 0.2],
    )

    df = pd.DataFrame(
        {
            "tenure": np.clip(X[:, 0] * 20 + 30, 1, 72).astype(int),
            "monthly_charges": np.clip(X[:, 1] * 20 + 65, 20, 120).round(2),
            "total_charges": np.clip(X[:, 2] * 1000 + 3000, 100, 8000).round(2),
            "contract_type": contract_types,
            "churned": y,
        }
    )

    return df


if __name__ == "__main__":
    df = generate_data()
    print(f"Dataset shape: {df.shape}")
    print(f"Columns: {list(df.columns)}")
    print(f"Churn rate: {df['churned'].mean():.2%}")
    print(df.head())
Write a preprocessing pipeline using ColumnTransformer

Your dataset has two types of features. Numeric features (tenure, monthly_charges, total_charges) and a categorical feature (contract_type). ML models need these handled differently.

Numeric features get scaled so they're on the same range. Categorical features get converted to numbers through one-hot encoding. A ColumnTransformer lets you apply different transformations to different columns in one step.

Why bundle preprocessing into a Pipeline?

A common production bug is train/serve skew. This happens when training data gets preprocessed one way, but prediction data gets preprocessed differently (e.g., forgetting to scale, or scaling with different statistics).

By bundling the preprocessor and model into one serializable object, the exact same transformations apply at training and inference time. One object, one artifact, zero skew.

  • Open train.py and add the following imports and build_preprocessor() function:
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, StandardScaler

from data import generate_data


def build_preprocessor():
    """Build a ColumnTransformer for numeric and categorical features."""
    # Define which columns get which treatment
    numeric_features = ["tenure", "monthly_charges", "total_charges"]
    categorical_features = ["contract_type"]

    # StandardScaler normalizes numbers, OneHotEncoder converts categories to binary columns
    preprocessor = ColumnTransformer(
        transformers=[
            ("num", StandardScaler(), numeric_features),
            ("cat", OneHotEncoder(handle_unknown="ignore"), categorical_features),
        ]
    )
    return preprocessor

What does this code do?

  • StandardScaler centers numeric features around zero and scales them to unit variance. This stops large-range features (like total_charges in the thousands) from dominating small-range ones (like tenure in the tens).
  • OneHotEncoder converts each category value into its own binary column (e.g., "month-to-month" becomes a 1 in one column and 0 in the others).
  • handle_unknown="ignore" prevents errors if the API receives a contract type the model never saw during training.
  • from data import generate_data pulls in the function you just wrote. You'll use it in the next step when training the model.
  • Save train.py.

You won't see a visible change yet. The preprocessor is a building block that the next step will use when training the full model pipeline.

  • Confirm both files are saved by running this quick check:
python -c "from data import generate_data; print(generate_data().shape)"

You should see (1000, 5) printed, confirming your dataset has 1000 rows and 5 columns.

Seeing an ImportError for data?

  • Make sure your terminal is in the ml-pipeline folder where data.py lives. Python looks for modules in the current directory.
  • Check that data.py is saved and the filename is exactly data.py (not Data.py or data.py.py).

Help me fix an ImportError when importing from data.py

✔️ Awesome, I've got everything!

Great. Double check you've saved train.py and you saw the (1000, 5) output.

ⓧ I'd like to double check the full code

Here's the complete train.py file so far:

from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, StandardScaler

from data import generate_data


def build_preprocessor():
    """Build a ColumnTransformer for numeric and categorical features."""
    numeric_features = ["tenure", "monthly_charges", "total_charges"]
    categorical_features = ["contract_type"]

    preprocessor = ColumnTransformer(
        transformers=[
            ("num", StandardScaler(), numeric_features),
            ("cat", OneHotEncoder(handle_unknown="ignore"), categorical_features),
        ]
    )
    return preprocessor

You now have a working data pipeline and a preprocessor that knows how to transform raw customer features into model-ready inputs. Next up, you'll combine the preprocessor with a classifier, train it, and evaluate how well it predicts churn.

Train and Evaluate the Model

Your data pipeline is generating synthetic customer churn data and your preprocessor knows how to handle both numeric and categorical features.

Now it's time for the core ML step: training a model on that data and measuring how well it performs. You'll bundle the preprocessor and a Random Forest classifier into a single Pipeline object, evaluate it with standard metrics, and save the trained artifact to disk.

In this step, get ready to:
  • Build an sklearn Pipeline combining your preprocessor with a Random Forest classifier.
  • Train the pipeline and evaluate it with accuracy, precision, recall, and F1 metrics.
  • Serialize the trained pipeline to disk as a reusable model artifact.
Build a full sklearn Pipeline combining preprocessor and classifier

A Pipeline chains your preprocessing and model into one callable object. When you call fit() on the pipeline, it runs the preprocessor first, then feeds the transformed data to the classifier.

  • In train.py, replace the existing import lines at the top of the file with the following expanded imports:
import joblib
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler

from data import generate_data

What are the new imports?

  • joblib handles saving and loading Python objects (especially those with large NumPy arrays inside).
  • RandomForestClassifier is the model that learns patterns in the training data.
  • accuracy_score, precision_score, recall_score, f1_score measure different aspects of model performance.
  • train_test_split holds out a portion of data so you can test on unseen examples.
  • Pipeline bundles your preprocessor and model into one serializable object.
  • Below your existing build_preprocessor() function, add the train_model() function and the entry point by pasting this code:
def train_model():
    """Train the full ML pipeline and save to disk."""
    # Generate the synthetic dataset
    df = generate_data()

    # Separate features from the target column
    feature_columns = ["tenure", "monthly_charges", "total_charges", "contract_type"]
    X = df[feature_columns]
    y = df["churned"]

    # Hold out 20% of data for evaluation
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.2, random_state=42
    )

    # Bundle preprocessing and classifier into one Pipeline
    preprocessor = build_preprocessor()
    pipeline = Pipeline(
        steps=[
            ("preprocessor", preprocessor),
            ("classifier", RandomForestClassifier(n_estimators=100, random_state=42)),
        ]
    )

    # Train the full pipeline on training data
    pipeline.fit(X_train, y_train)

    # Evaluate on the held-out test set
    y_pred = pipeline.predict(X_test)
    print("Model Evaluation:")
    print(f"  Accuracy:  {accuracy_score(y_test, y_pred):.4f}")
    print(f"  Precision: {precision_score(y_test, y_pred):.4f}")
    print(f"  Recall:    {recall_score(y_test, y_pred):.4f}")
    print(f"  F1 Score:  {f1_score(y_test, y_pred):.4f}")

    # Save the entire pipeline (preprocessor + model) as one artifact
    joblib.dump(pipeline, "model.joblib")
    print("\nModel saved to model.joblib")

    return pipeline


if __name__ == "__main__":
    train_model()

What does this code do?

  • generate_data() produces 1000 rows of synthetic customer data from the data.py module you built earlier.
  • train_test_split holds back 20% (200 rows) that the model never sees during training. This gives you an honest performance estimate.
  • The Pipeline wraps both the preprocessor and classifier. When you call pipeline.fit(), it scales numeric columns, one-hot encodes the categorical column, and trains the Random Forest all in one step.
  • The four metrics (accuracy, precision, recall, F1) give you a balanced picture of how well the model distinguishes churning vs non-churning customers.
  • joblib.dump() saves the entire pipeline object to model.joblib. This means the API can load one file and get both preprocessing and prediction in a single call.
  • Save train.py.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

import joblib
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler

from data import generate_data


def build_preprocessor():
    """Build a ColumnTransformer for numeric and categorical features."""
    numeric_features = ["tenure", "monthly_charges", "total_charges"]
    categorical_features = ["contract_type"]

    preprocessor = ColumnTransformer(
        transformers=[
            ("num", StandardScaler(), numeric_features),
            ("cat", OneHotEncoder(handle_unknown="ignore"), categorical_features),
        ]
    )
    return preprocessor


def train_model():
    """Train the full ML pipeline and save to disk."""
    df = generate_data()

    feature_columns = ["tenure", "monthly_charges", "total_charges", "contract_type"]
    X = df[feature_columns]
    y = df["churned"]

    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.2, random_state=42
    )

    preprocessor = build_preprocessor()
    pipeline = Pipeline(
        steps=[
            ("preprocessor", preprocessor),
            ("classifier", RandomForestClassifier(n_estimators=100, random_state=42)),
        ]
    )

    pipeline.fit(X_train, y_train)

    y_pred = pipeline.predict(X_test)
    print("Model Evaluation:")
    print(f"  Accuracy:  {accuracy_score(y_test, y_pred):.4f}")
    print(f"  Precision: {precision_score(y_test, y_pred):.4f}")
    print(f"  Recall:    {recall_score(y_test, y_pred):.4f}")
    print(f"  F1 Score:  {f1_score(y_test, y_pred):.4f}")

    joblib.dump(pipeline, "model.joblib")
    print("\nModel saved to model.joblib")

    return pipeline


if __name__ == "__main__":
    train_model()
Train the pipeline and print evaluation metrics

Time to train your model. Running the script triggers the full pipeline: data generation, splitting, training, and evaluation.

  • Train your model by running this command:
python train.py
  • You should see output similar to this in your terminal:
Model Evaluation:
  Accuracy:  0.8750
  Precision: 0.8125
  Recall:    0.7647
  F1 Score:  0.7879

Model saved to model.joblib

What do these metrics mean?

  • Accuracy is the percentage of all predictions that were correct.
  • Precision measures how many of the "will churn" predictions were actually correct. High precision means few false alarms.
  • Recall measures how many actual churners the model caught. High recall means few missed churners.
  • F1 Score balances precision and recall into a single number. It's the most useful single metric for imbalanced datasets.

Your exact numbers may differ slightly, but accuracy should be around 0.85 or higher. The synthetic data has a 70/30 class split, so the model has enough signal to learn useful patterns.

Seeing a ModuleNotFoundError?

  • Make sure your virtual environment is activated. Your terminal prompt should show (.venv) at the beginning.
  • If you see an error about a missing module, re-run pip install -r requirements.txt inside the activated virtual environment.
  • Check that you haven't named any file sklearn.py in your project. That would shadow the real scikit-learn package.

Help me debug a ModuleNotFoundError when running train.py

Serialize the trained pipeline to disk using joblib

The script already saved model.joblib to your project directory. This file contains the entire fitted pipeline: the scaler parameters, the one-hot encoder categories, and the trained Random Forest weights.

  • Confirm the model artifact exists by running:
ls
  • You should see model.joblib listed alongside data.py, train.py, and serve.py in the output.

Why save the full pipeline instead of just the model?

If you saved only the Random Forest, you'd need to rebuild and re-fit the preprocessor every time you want to make a prediction. Worse, a mismatch between training-time preprocessing and serving-time preprocessing causes "train/serve skew." The predictions silently become wrong because the features don't match what the model learned.

By saving the entire Pipeline in one file, loading model.joblib gives you both preprocessing and prediction in a single .predict() call. The API you build next will use exactly this pattern.

Don't see model.joblib?

  • Make sure you ran python train.py and saw the "Model saved to model.joblib" message in the output.
  • Check that your terminal is in the ml-pipeline directory. If you ran the script from a different folder, the file was saved there instead.

Help me figure out why model.joblib wasn't created

Your model is trained, evaluated, and saved. Next up, you'll build a FastAPI endpoint that loads this artifact and serves predictions over HTTP.

Serve Predictions with FastAPI

You have a trained model sitting on disk as model.joblib. Right now, the only way to get predictions from it is to import it in another Python script. For a model to be useful in the real world, other applications need to send it data and get predictions back over the network.

In this step, you'll wrap your model in a REST API using FastAPI. Any application (a web frontend, a mobile app, or another service) will be able to send customer data to your API and receive a churn prediction in response.

In this step, get ready to:
  • Create a FastAPI app that loads the trained pipeline.
  • Implement a prediction endpoint that accepts customer data.
  • Test the endpoint with a sample request via Swagger UI.
Create the FastAPI app with a Pydantic request model

The API needs two things before it can serve predictions: a running web server and a definition of what input data looks like. Pydantic models handle the input definition by validating that incoming requests have the correct fields and types.

  • Open serve.py in your editor and add the following code:
import joblib
import pandas as pd
from fastapi import FastAPI
from pydantic import BaseModel

# Create the FastAPI application instance
app = FastAPI(title="Churn Prediction API")

# Load the trained pipeline once at startup
pipeline = joblib.load("model.joblib")


# Define the expected shape of incoming customer data
class CustomerData(BaseModel):
    tenure: int
    monthly_charges: float
    total_charges: float
    contract_type: str

What does this code do?

  • joblib.load("model.joblib") loads your trained sklearn Pipeline (preprocessor + classifier) into memory once when the server starts. Every future request reuses this loaded object instead of re-reading the file.
  • FastAPI(title="Churn Prediction API") creates the web application. The title appears in the auto-generated documentation page.
  • CustomerData defines the exact fields a request must include. FastAPI uses this to validate incoming JSON and reject malformed requests automatically.

Why FastAPI instead of Flask?

FastAPI generates interactive API documentation automatically at /docs. It also uses Pydantic for request validation, so you get type checking and helpful error messages for free. These features make it the current industry standard for serving ML models.

You won't see a change yet. The next chunk adds the endpoint that makes this app functional.

Implement the /predict endpoint

The endpoint is where the actual prediction happens. It receives validated customer data, converts it into the DataFrame format the sklearn Pipeline expects, runs the prediction, and sends back a labeled result.

  • Add the following code below the CustomerData class in serve.py:
# Define the shape of the prediction response
class PredictionResponse(BaseModel):
    prediction: int
    label: str


# POST endpoint that accepts customer data and returns a churn prediction
@app.post("/predict", response_model=PredictionResponse)
def predict(customer: CustomerData):
    """Predict whether a customer will churn."""
    input_df = pd.DataFrame([customer.model_dump()])
    prediction = int(pipeline.predict(input_df)[0])
    label = "will churn" if prediction == 1 else "will not churn"
    return PredictionResponse(prediction=prediction, label=label)

What does this code do?

  • PredictionResponse defines what the API sends back: a numeric prediction (0 or 1) and a human-readable label.
  • customer.model_dump() converts the validated Pydantic model into a dictionary. Wrapping it in pd.DataFrame([...]) creates the single-row DataFrame that sklearn's predict() method expects.
  • The pipeline handles all preprocessing (scaling numeric columns, encoding the categorical column) before making a prediction. This is the same pipeline from training, so there is no risk of train/serve skew.
  • Save serve.py.
  • Start the development server by running this command:
fastapi dev serve.py
  • Wait for the terminal output to show Application startup complete. and the server URL.
  • Open your browser and navigate to http://127.0.0.1:8000/docs. You should see the Swagger UI documentation page with your /predict endpoint listed.

What does fastapi dev do?

The fastapi dev command starts a local development server with auto-reload. It detects the app object in your file and serves it at http://127.0.0.1:8000. Every time you save a change to your code, the server restarts automatically.

Server not starting?

  • If you see a ModuleNotFoundError, make sure your virtual environment is activated and you ran pip install -r requirements.txt earlier.
  • If you see a FileNotFoundError for model.joblib, make sure you ran python train.py in the previous step. The file must exist in the same directory where you run fastapi dev.
  • If port 8000 is already in use, stop whatever process is using it and try again.

Still stuck? Help me debug why my FastAPI server won't start for serve.py.

✔️ Awesome, I've got everything!

Great. Double check you've saved serve.py and that the Swagger UI is visible at http://127.0.0.1:8000/docs.

ⓧ I'd like to double check the full code

import joblib
import pandas as pd
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI(title="Churn Prediction API")

pipeline = joblib.load("model.joblib")


class CustomerData(BaseModel):
    tenure: int
    monthly_charges: float
    total_charges: float
    contract_type: str


class PredictionResponse(BaseModel):
    prediction: int
    label: str


@app.post("/predict", response_model=PredictionResponse)
def predict(customer: CustomerData):
    """Predict whether a customer will churn."""
    input_df = pd.DataFrame([customer.model_dump()])
    prediction = int(pipeline.predict(input_df)[0])
    label = "will churn" if prediction == 1 else "will not churn"
    return PredictionResponse(prediction=prediction, label=label)
Test the endpoint with a sample request

The Swagger UI at /docs lets you send test requests directly from your browser without writing any client code.

  • In the Swagger UI page, click on the POST /predict endpoint to expand it.
  • Click the Try it out button on the right side.
  • Replace the example request body with this JSON:
{
  "tenure": 12,
  "monthly_charges": 85.5,
  "total_charges": 1200.0,
  "contract_type": "month-to-month"
}
  • Click the Execute button.

You should see a JSON response in the Response body section containing a prediction value (0 or 1) and a label (either "will churn" or "will not churn").

What does the response mean?

A prediction of 1 with label "will churn" means the model thinks this customer is likely to leave. A prediction of 0 with label "will not churn" means the model expects them to stay.

The exact result depends on the Random Forest's learned patterns. A short-tenure, high-charge, month-to-month customer tends to score higher for churn risk.

Seeing a 422 Validation Error?

  • A 422 error means your request JSON doesn't match the CustomerData schema. Check that all four fields are present and spelled exactly as shown (tenure, monthly_charges, total_charges, contract_type).
  • Make sure tenure is an integer (no decimal point), and contract_type is a string in quotes.

Why am I getting a 422 validation error from my FastAPI /predict endpoint?

Secret mission

Add Health Check and Batch Predictions

Your prediction API works for single customers, but production ML systems need more. Can you add a health check endpoint that exposes model metadata and a batch prediction endpoint that scores multiple customers in one request?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • The FastAPI development server (running process in your terminal).
  • The ml-pipeline project folder on your Desktop (includes all Python files, the virtual environment, and the trained model artifact).

Keep everything running

No action needed. Choose this if you're still actively building or experimenting with the prediction API.

  • Your FastAPI server continues running at http://127.0.0.1:8000 until you close the terminal or press Ctrl+C.
  • Your virtual environment stays active in the current terminal session.
  • All project files remain in ~/Desktop/ml-pipeline.

Pause - I'll come back to this later

Shut down running processes to free up memory. Your files stay on disk so you can pick up where you left off.

  • Stop the FastAPI server by pressing Ctrl+C in the terminal where it is running.
  • Deactivate your virtual environment by running:
deactivate

When you come back, reactivate the environment and restart the server.

  • On macOS/Linux, activate with source .venv/bin/activate.
  • On Windows, activate with .venv\Scripts\activate.
  • Start the server again with fastapi dev serve.py.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes your code, trained model, and virtual environment permanently.

  • Stop the FastAPI server by pressing Ctrl+C in the terminal where it is running.
  • Deactivate your virtual environment by running:
deactivate
  • Navigate to your Desktop and delete the entire project folder.

On macOS/Linux:

cd ~/Desktop
rm -rf ml-pipeline

On Windows:

cd ~\Desktop
rmdir /s /q ml-pipeline

This removes all Python files, the virtual environment, the model.joblib artifact, and installed packages.

Nice Work!

Nice Work!

You just built a complete end-to-end ML prediction pipeline, from raw data generation all the way through to a live REST API serving predictions over HTTP.

You've learned how to:

  • Built a scikit-learn Pipeline with ColumnTransformer that bundles preprocessing and model training into one serializable object, preventing train/serve skew.
  • Trained a Random Forest classifier, evaluated it with standard ML metrics (accuracy, precision, recall, F1), and serialized the entire pipeline to disk with joblib.
  • Served predictions through a FastAPI REST API with Pydantic validation and automatic Swagger documentation at /docs.
  • Secret Mission: Added production patterns including a /health endpoint for model metadata and a /predict/batch endpoint for efficient batch inference.

Ready to quiz yourself?