AI and Machine Learning

Real-World MLOps Pipeline: Notebook to Production

Transform ML prototypes into robust production systems. Learn to build a real-world MLOps pipeline, from notebooks to continuous deployment and monitoring, with code and architecture.

Khader Vali July 26, 2026 15 min read

Real-World MLOps Pipeline: From Notebook to Production

As a senior software engineer and technical writer, I’ve seen countless brilliant machine learning models developed in the isolated comfort of a Jupyter notebook. These models showcase incredible potential, delivering impressive metrics on validation sets. Yet, the chasm between a compelling notebook prototype and a reliable, scalable, and maintainable production system remains one of the most significant challenges in modern data science and engineering. This chasm is precisely what MLOps aims to bridge.

In this comprehensive guide, we’ll embark on a journey to demystify the “notebook to production” pipeline. We’ll explore the critical stages, tools, and best practices required to transform your experimental ML models into robust, real-world applications. This isn’t just about deploying a model; it’s about building an entire ecosystem that supports continuous integration, delivery, deployment, and monitoring of machine learning systems.

What is MLOps, and Why is It Crucial?

MLOps, a portmanteau of Machine Learning and Operations, is a set of practices that combines Machine Learning, DevOps, and Data Engineering to deploy and maintain ML systems in production reliably and efficiently. It’s about applying DevOps principles like CI/CD, automation, version control, and monitoring to the unique complexities of machine learning workflows.

Why is MLOps so crucial? Consider the inherent differences between traditional software and ML systems:

  • Data Dependency: ML models are not just code; they are code + data. Changes in data distribution (data drift) or concept drift can degrade model performance without any code changes.
  • Experimentation: ML development is highly experimental, involving numerous models, hyperparameters, and feature engineering iterations. Tracking these experiments is vital.
  • Reproducibility: Reproducing results in ML requires precise versions of code, data, dependencies, and environment configurations.
  • Model Decay: Unlike traditional software, ML model performance can degrade over time due to real-world changes, necessitating continuous monitoring and retraining.
  • Cross-functional Teams: ML projects typically involve data scientists, ML engineers, software engineers, and operations teams, requiring seamless collaboration.

Without MLOps, projects often stall at the proof-of-concept stage, struggle with inconsistent deployments, face performance degradation in production, and become difficult to scale or maintain. A well-implemented MLOps pipeline ensures that models are not just built but are also deployed, monitored, and maintained effectively throughout their lifecycle.

The MLOps Journey: From Concept to Deployment

The transition from a data scientist’s notebook to a production-grade machine learning service is a multi-stage process. While the exact tools and technologies may vary, the core phases remain consistent. Let’s outline the journey:

PHASE 1: Experimentation & Development
(Focus: Data Scientist)
Problem Definition & Data Exploration -> Feature Engineering & Model Training -> Local Validation

PHASE 2: Transition to Production-Ready Code
(Focus: ML Engineer, Data Scientist)
Modularization & Scripting -> Data Versioning & Pipelines -> Environment Management

PHASE 3: Building the MLOps Pipeline
(Focus: ML Engineer, DevOps Engineer)
CI/CD for ML -> Model Registry & Versioning -> Deployment Strategies

PHASE 4: Monitoring & Maintenance
(Focus: ML Engineer, Operations Team)
Model Monitoring -> Automated Retraining & Feedback Loops

We’ll now dive deep into each phase, offering practical insights and code examples.

<

Real-World MLOps Pipeline: Notebook to Production
Generated Image

>

Phase 1: Experimentation & Development (The Notebook World)

This is where the magic begins – the realm of data scientists, where ideas are born, data is explored, and models are prototyped. Jupyter notebooks are the undisputed champions here, offering an interactive environment for rapid iteration.

Problem Definition & Data Exploration

Before writing any code, it’s paramount to clearly define the business problem we’re trying to solve. What are the objectives? What metrics will define success? For example, if we’re building a fraud detection system, success might be maximizing recall while maintaining precision above a certain threshold, or minimizing false positives to reduce operational overhead.

Once the problem is clear, data exploration (EDA) takes center stage. This involves understanding the available data, identifying potential features, handling missing values, and detecting outliers. Tools like Pandas, Matplotlib, Seaborn, and Plotly are indispensable here.

A typical notebook might start with:


import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# Load data
df = pd.read_csv('data/raw_transactions.csv')

# Initial inspection
print(df.head())
print(df.info())
print(df.describe())

# Check for missing values
print(df.isnull().sum())

# Visualize distributions
sns.histplot(df['transaction_amount'])
plt.title('Distribution of Transaction Amounts')
plt.show()

# Correlation matrix
sns.heatmap(df.corr(), annot=True, cmap='coolwarm')
plt.title('Feature Correlation Matrix')
plt.show()

The goal here is to gain insights, formulate hypotheses about features, and identify potential challenges early on.

Feature Engineering & Model Training

This iterative phase involves transforming raw data into meaningful features and training various machine learning models. Data scientists experiment with different algorithms, hyperparameter tuning, and cross-validation strategies.

Key practices during this phase:

  • Version Control for Notebooks: While notebooks are excellent for exploration, their JSON structure can make Git diffs challenging. Nevertheless, versioning notebooks (alongside scripts) is crucial for tracking changes and collaboration. Tools like nbdime can help with diffing.
  • Experiment Tracking: This is non-negotiable for MLOps. As you train dozens or hundreds of models, you need a system to log metrics, parameters, code versions, and artifacts (e.g., trained models, plots). Tools like MLflow, Weights & Biases, Comet ML, or DVC are essential here. They allow you to compare experiments, reproduce past results, and identify the best-performing models.

Let’s look at an example using MLflow for experiment tracking:


import mlflow
import mlflow.sklearn
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score

# Assume df has 'features' and 'target'
X = df[['feature1', 'feature2', 'feature3']]
y = df['target']

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

# Start an MLflow run
with mlflow.start_run():
    # Define hyperparameters
    n_estimators = 100
    max_depth = 10
    random_state = 42

    # Log parameters
    mlflow.log_param("n_estimators", n_estimators)
    mlflow.log_param("max_depth", max_depth)

    # Train model
    model = RandomForestClassifier(n_estimators=n_estimators, max_depth=max_depth, random_state=random_state)
    model.fit(X_train, y_train)

    # Make predictions
    y_pred = model.predict(X_test)

    # Calculate metrics
    accuracy = accuracy_score(y_test, y_pred)
    precision = precision_score(y_test, y_pred)
    recall = recall_score(y_test, y_pred)
    f1 = f1_score(y_test, y_pred)

    # Log metrics
    mlflow.log_metric("accuracy", accuracy)
    mlflow.log_metric("precision", precision)
    mlflow.log_metric("recall", recall)
    mlflow.log_metric("f1_score", f1)

    # Log the model itself
    mlflow.sklearn.log_model(model, "random_forest_model")

    print(f"MLflow Run ID: {mlflow.active_run().info.run_id}")
    print(f"Accuracy: {accuracy}")

This snippet demonstrates how to encapsulate an experiment, logging all relevant information for future reference and comparison.

Local Validation & Baseline Models

Before even thinking about production, rigorous local validation is crucial. This includes techniques like K-fold cross-validation, using appropriate evaluation metrics (not just accuracy!), and establishing a baseline model. A simple baseline (e.g., a dummy classifier, a basic logistic regression) helps to confirm that more complex models are indeed adding value.


from sklearn.model_selection import cross_val_score

# Perform cross-validation
scores = cross_val_score(model, X, y, cv=5, scoring='f1')
print(f"Cross-validation F1 scores: {scores}")
print(f"Mean F1 score: {np.mean(scores)}")

Phase 2: Transition to Production-Ready Code

The notebook, while excellent for exploration, is rarely suitable for direct production deployment. This phase is about transforming that exploratory code into robust, modular, and maintainable scripts that can be integrated into automated pipelines.

Modularization & Scripting

The first step is to refactor notebook code into well-structured Python scripts. This involves:

  • Functions and Classes: Encapsulate logic into reusable functions and classes (e.g., load_data(), preprocess_data(), train_model(), evaluate_model()).
  • Clear Interfaces: Ensure functions have clear inputs and outputs.
  • Configuration Management: Avoid hardcoding parameters. Use configuration files (YAML, JSON) or environment variables.
  • Error Handling: Implement robust error handling.
  • Logging: Add proper logging for debugging and operational visibility.

Instead of a single monolithic notebook, you might end up with a project structure like this:


my_ml_project/
├── data/
│   ├── raw/
│   └── processed/
├── notebooks/
│   ├── exploratory_analysis.ipynb
│   └── model_prototyping.ipynb
├── src/
│   ├── __init__.py
│   ├── data_loader.py
│   ├── preprocessor.py
│   ├── model_trainer.py
│   └── evaluator.py
├── scripts/
│   ├── train.py
│   └── predict.py
├── tests/
│   ├── test_data_loader.py
│   └── test_model_trainer.py
├── configs/
│   └── model_config.yaml
├── Dockerfile
├── requirements.txt

An example of a modularized training script:


# src/data_loader.py
import pandas as pd

def load_raw_data(filepath):
    return pd.read_csv(filepath)

# src/preprocessor.py
from sklearn.preprocessing import StandardScaler

class DataPreprocessor:
    def __init__(self):
        self.scaler = None

    def fit(self, X):
        self.scaler = StandardScaler()
        self.scaler.fit(X)
        return self

    def transform(self, X):
        if self.scaler is None:
            raise ValueError("Preprocessor not fitted. Call .fit() first.")
        return self.scaler.transform(X)

    def fit_transform(self, X):
        return self.fit(X).transform(X)

# scripts/train.py
import argparse
import yaml
import mlflow
import mlflow.sklearn
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, f1_score
import logging

from src.data_loader import load_raw_data
from src.preprocessor import DataPreprocessor
# Potentially src.model_trainer and src.evaluator

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

def train_model(config_path):
    with open(config_path, 'r') as f:
        config = yaml.safe_load(f)

    data_config = config['data']
    model_config = config['model']

    logging.info("Starting MLflow run...")
    with mlflow.start_run():
        mlflow.log_params(model_config['params'])

        # 1. Load Data
        df = load_raw_data(data_config['filepath'])
        X = df[data_config['features']]
        y = df[data_config['target']]
        X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=data_config['test_size'], random_state=data_config['random_state'])
        logging.info(f"Data loaded and split. Train samples: {len(X_train)}, Test samples: {len(X_test)}")

        # 2. Preprocess Data
        preprocessor = DataPreprocessor().fit(X_train)
        X_train_processed = preprocessor.transform(X_train)
        X_test_processed = preprocessor.transform(X_test)
        logging.info("Data preprocessed.")

        # 3. Train Model
        model = RandomForestClassifier(**model_config['params'], random_state=data_config['random_state'])
        model.fit(X_train_processed, y_train)
        logging.info("Model training complete.")

        # 4. Evaluate Model
        y_pred = model.predict(X_test_processed)
        accuracy = accuracy_score(y_test, y_pred)
        f1 = f1_score(y_test, y_pred)
        mlflow.log_metric("accuracy", accuracy)
        mlflow.log_metric("f1_score", f1)
        logging.info(f"Model evaluated. Accuracy: {accuracy:.4f}, F1 Score: {f1:.4f}")

        # 5. Log Model
        mlflow.sklearn.log_model(model, "random_forest_model")
        logging.info("Model logged to MLflow.")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Train a machine learning model.")
    parser.add_argument("--config", type=str, default="configs/model_config.yaml",
                        help="Path to the model configuration YAML file.")
    args = parser.parse_args()
    train_model(args.config)

And the configs/model_config.yaml file:


data:
  filepath: data/raw/transactions.csv
  features: ['feature1', 'feature2', 'feature3']
  target: target
  test_size: 0.2
  random_state: 42

model:
  name: RandomForestClassifier
  params:
    n_estimators: 100
    max_depth: 10
    min_samples_leaf: 5

Data Versioning & Pipelines (DataOps for ML)

Just as code needs versioning, so does data. Data Version Control (DVC) allows you to version datasets, models, and intermediate files, making your experiments fully reproducible. It works by storing metadata about files in Git, while the actual large data files are stored in remote storage (S3, GCS, Azure Blob, local storage).


# Initialize DVC in your repo
dvc init

# Add a dataset to DVC
dvc add data/raw/transactions.csv

# Now, 'data/raw/transactions.csv.dvc' is tracked by Git,
# and the actual data is managed by DVC.

# Pull data
dvc pull

# Push data
dvc push

This approach ensures that every experiment run, every model training, can be tied back to a specific version of the data, enhancing reproducibility significantly.

Data ingestion pipelines ensure that raw data is consistently cleaned, transformed, and validated before it reaches the model training stage. These pipelines can range from simple Python scripts to sophisticated ETL jobs orchestrated by tools like Airflow or dbt. For complex features, a feature store can standardize feature generation, serving, and versioning across training and inference.

Environment Management

Reproducibility hinges on consistent environments. Docker is the de facto standard for packaging ML applications. It ensures that your code runs in the exact same environment (OS, libraries, dependencies) in development, testing, and production.

A Dockerfile for our training script might look like this:


# Use a lightweight Python base image
FROM python:3.9-slim-buster

# Set the working directory in the container
WORKDIR /app

# Copy requirements file first to leverage Docker cache
COPY requirements.txt .

# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt

# Copy the rest of the application code
COPY . .

# Create directories for data (if not existing in the image)
RUN mkdir -p data/raw data/processed

# Command to run the training script (entrypoint)
# ENTRYPOINT ["python", "scripts/train.py"]
# CMD ["--config", "configs/model_config.yaml"]

requirements.txt would list all Python dependencies:


pandas
scikit-learn
mlflow
pyyaml

Building and running:


docker build -t my-ml-trainer .
docker run my-ml-trainer python scripts/train.py --config configs/model_config.yaml

This ensures that the model is always trained with the exact dependencies it was developed with.

Phase 3: Building the MLOps Pipeline

This is the core of MLOps – automating the entire lifecycle from code changes to model deployment. CI/CD principles are paramount here.

CI/CD for ML

Continuous Integration (CI): Every code commit triggers automated tests (unit, integration, data validation, model validation). If tests pass, the code is integrated into the main branch.

Continuous Delivery/Deployment (CD): After CI, the validated model is packaged and made ready for deployment (CD). Depending on the strategy, it might be automatically deployed to production (Continuous Deployment) or await manual approval.

An MLOps CI/CD pipeline typically involves these steps, orchestrated by tools like Jenkins, GitLab CI, GitHub Actions, Azure DevOps, AWS CodePipeline, or dedicated ML orchestrators like Airflow or Kubeflow Pipelines:

1. Trigger:
* Code push to Git repository (e.g., a new feature branch merged to main).
* Scheduled retraining (e.g., weekly).
* Data drift detection (from monitoring, covered in Phase 4).
* Manual trigger.

2. Data Validation & Preprocessing:
* Check for schema changes, missing values, outliers in the new data.
* Run the data preprocessing pipeline to prepare features.
* Example tools: Great Expectations, Apache Deequ.

3. Model Training:
* Fetch the latest processed data.
* Train the model using the latest production-ready code.
* Log all experiment details (parameters, metrics, artifacts) to MLflow or similar experiment tracker.

4. Model Evaluation & Comparison:
* Evaluate the newly trained model against a hold-out test set.
* Compare its performance against the currently deployed model (or a baseline). This is crucial for deciding if the new model is better.
* Perform model validation tests (e.g., slice performance, fairness checks).

5. Model Versioning & Registry:
* If the new model meets performance criteria, register it in a model registry (e.g., MLflow Model Registry).
* Assign a new version number.
* Transition its stage (e.g., “Staging” -> “Production”).

6. Model Packaging:
* Containerize the model for serving (e.g., Docker image with a FastAPI/Flask endpoint).

7. Deployment:
* Deploy the packaged model to a staging environment for further testing.
* Once validated, deploy to production. Strategies include A/B testing, canary deployments, or blue/green deployments to minimize risk.

Diagram in words for a typical MLOps CI/CD Pipeline:


[Git Commit (Code/Config/Data Metadata)]
       |
       v
[CI Trigger (e.g., GitHub Actions)]
       |
       v
[Stage 1: Build & Test]
    - Linting (flake8, black)
    - Unit Tests (pytest)
    - Integration Tests
    - Build Docker Image (for training)
       |
       v
[Stage 2: Model Training & Validation]
    - Pull latest data (DVC)
    - Run training script inside Docker container
    - Log experiment (MLflow)
    - Evaluate model performance (vs. baseline/production)
    - Model validation tests (e.g., fairness, robustness)
       |
       v
[Stage 3: Model Registry & Approval]
    - If performance acceptable: Register model in MLflow Model Registry
    - Transition model stage to "Staging" (manual/automatic)
    - Human approval gate (optional for production promotion)
       |
       v
[Stage 4: Model Serving Packaging]
    - Build Docker Image (for inference API)
    - Push image to Container Registry (ECR, GCR, Docker Hub)
       |
       v
[Stage 5: Deployment]
    - Deploy to Staging Environment (Kubernetes, SageMaker Endpoint, Cloud Run)
    - Run integration tests against staging endpoint
    - If all good: Deploy to Production (A/B, Canary, Blue/Green)
       |
       v
[Stage 6: Monitoring]
    - Monitor model performance, data drift, concept drift
    - Alerting
    - Trigger retraining if decay detected (feedback loop to Stage 1/2)

Model Registry & Versioning

A model registry serves as a centralized hub for managing the lifecycle of your machine learning models. MLflow Model Registry is a popular choice, providing:

  • Model Versioning: Track different versions of a model.
  • Stage Management: Define model stages (e.g., Staging, Production, Archived).
  • Metadata: Store associated metrics, parameters, and run IDs.
  • Model Lineage: Trace models back to the exact code, data, and parameters used to train them.

After a successful training run, you can register a model:


# After logging the model in an MLflow run:
# run_id = mlflow.active_run().info.run_id
# model_uri = f"runs:/{run_id}/random_forest_model"

# Register the model
registered_model = mlflow.register_model(
    model_uri="runs:/your_mlflow_run_id/random_forest_model", # Replace with actual run ID
    name="FraudDetectionClassifier"
)
print(f"Model registered with name: {registered_model.name} and version: {registered_model.version}")

# Transition the model to staging
client = mlflow.tracking.MlflowClient()
client.transition_model_version_stage(
    name="FraudDetectionClassifier",
    version=registered_model.version,
    stage="Staging"
)

This allows data scientists and engineers to easily discover, share, and deploy specific model versions.

Deployment Strategies

Deploying a model means making it available for inference. Common approaches include:

  • Real-time API Endpoints:
    • For low-latency predictions, models are exposed via REST APIs.
    • Frameworks: Flask, FastAPI, Django.
    • Infrastructure: Kubernetes (with tools like KServe/Seldon Core), serverless functions (AWS Lambda, Google Cloud Functions, Azure Functions), managed services (AWS SageMaker Endpoints, Google Cloud Vertex AI Endpoints, Azure ML Endpoints).
    • FastAPI example for serving a model:

# app/main.py
from fastapi import FastAPI
from pydantic import BaseModel
import mlflow.pyfunc
import os

# Load model from MLflow Model Registry
# In a real scenario, you'd fetch the 'Production' or 'Staging' version
# Here, we'll assume the model URI is an environment variable or config
MODEL_URI = os.getenv("MODEL_

Written by

Khader Vali

Senior Software Engineer specializing in cloud architecture, real-time systems, and enterprise-scale applications.

Share this article

Related Articles

Knowledge Graphs: Enhancing LLM Reasoning with Structured Data

Jul 14, 2026 · 17 min read

Building Custom GPTs with OpenAI Assistants API

Jul 07, 2026 · 16 min read

Vector Databases Under the Hood: ChromaDB vs Pinecone vs Qdrant

Jul 06, 2026 · 19 min read