AI and Machine Learning

Real-World MLOps Pipeline: Notebook to Production

Learn to build a robust MLOps pipeline from scratch. This comprehensive guide covers experimentation, orchestration, deployment, and monitoring for real-world ML projects.

Khader Vali September 20, 2026 15 min read

Building a Real-World MLOps Pipeline: From Notebook to Production

As a senior software engineer and technical writer, I’ve seen countless machine learning projects start with great promise in a Jupyter Notebook, only to languish in “notebook purgatory” or struggle with the leap to production. The gap between a successful model prototype and a reliable, scalable, and maintainable production system is vast. This is where MLOps comes in – a set of practices that combines Machine Learning, DevOps, and Data Engineering to streamline the entire ML lifecycle.

In this comprehensive guide, we’ll embark on a journey to demystify MLOps. We’ll walk through the process of taking a machine learning model from an experimental notebook all the way to a robust, monitored production service. We’ll cover the essential phases, tools, code examples, and architectural considerations, providing you with a blueprint to implement your own real-world MLOps pipelines.

Our goal isn’t just to explain the concepts but to show you how to apply them practically. We’ll illustrate these principles using a common scenario: building a credit risk prediction model. While the specifics might change, the underlying MLOps principles remain universally applicable across various domains and problem types.

<

Real-World MLOps Pipeline: Notebook to Production
Generated Image

>

Why MLOps? The Imperative for Production ML

Before diving into the “how,” let’s briefly recap the “why.” Traditional software development has benefited immensely from DevOps practices, enabling faster releases, greater reliability, and better collaboration. Machine learning, however, introduces unique challenges:

  • Experimentation Overhead: ML development is highly iterative and experimental, involving numerous model architectures, hyperparameter tunes, and data preprocessing steps. Tracking these experiments manually is error-prone and inefficient.
  • Data Dependency: ML models are intrinsically tied to data. Changes in upstream data sources, data drift, or data quality issues can silently degrade model performance in production.
  • Model Versioning and Reproducibility: Ensuring that a specific model version can be reproduced with the exact code, data, and environment it was trained on is crucial for auditing, debugging, and regulatory compliance.
  • Deployment Complexity: Deploying ML models often involves specialized serving infrastructure, API endpoints, and integration with existing applications.
  • Continuous Monitoring: Unlike traditional software, ML models can degrade over time due to changes in the real-world data distribution (data drift) or concept drift, necessitating continuous monitoring and retraining.
  • Resource Management: Training large models or serving high-throughput inference can be resource-intensive, requiring efficient allocation and scaling.

MLOps addresses these challenges by bringing automation, collaboration, and continuous improvement principles to the ML lifecycle, turning sporadic model deployments into a streamlined, reliable, and observable process.

The MLOps Lifecycle: A Holistic View

The MLOps lifecycle can be broadly categorized into several interconnected phases, each with its own set of activities and considerations:

  1. Data Engineering & Experimentation: Data collection, cleaning, feature engineering, model exploration, training, and local evaluation.
  2. Pipeline Orchestration & Automation: Automating the end-to-end ML workflow, including data preprocessing, model training, validation, and registration. This phase involves CI/CD for ML.
  3. Model Deployment & Serving: Packaging the validated model and deploying it to a production environment for inference (real-time or batch).
  4. Monitoring & Management: Continuously tracking model performance, data quality, infrastructure health, and triggering retraining when necessary.

These phases are not strictly sequential but form a continuous loop, enabling rapid iteration and adaptation to changing data and business requirements.

Phase 1: Experimentation & Development (From Notebook to Script)

Every ML project typically begins in an interactive environment like a Jupyter Notebook. This phase is characterized by rapid iteration, exploration, and discovery. However, for production readiness, we need to transition from exploratory notebooks to structured, version-controlled scripts.

1.1. Data Exploration and Preparation

Our credit risk prediction model will rely on historical customer data, including features like credit score, income, loan amount, existing debts, and payment history. In the notebook, we’d perform initial EDA (Exploratory Data Analysis) to understand distributions, identify outliers, and handle missing values.

Example Notebook Snippet (Conceptual):


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

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

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

# Handle missing values (example: imputation)
for col in ['CreditScore', 'LoanAmount', 'MonthlyIncome']:
    df[col].fillna(df[col].median(), inplace=True)

# Feature engineering (example: debt-to-income ratio)
df['DebtToIncomeRatio'] = df['TotalDebt'] / df['MonthlyIncome']
df['DebtToIncomeRatio'].replace([np.inf, -np.inf], np.nan, inplace=True) # handle division by zero
df['DebtToIncomeRatio'].fillna(df['DebtToIncomeRatio'].median(), inplace=True)

# Encode categorical features (example: one-hot encoding)
df = pd.get_dummies(df, columns=['LoanPurpose', 'EmploymentType'], drop_first=True)

# ... more data cleaning and preprocessing ...

# Save processed data for model training
df.to_csv('processed_credit_data.csv', index=False)
    

During this stage, it’s critical to start documenting data sources, transformations, and schema changes. This forms the foundation for data versioning and data lineage.

1.2. Model Training and Evaluation

With clean, prepared data, we move to model selection, training, and local evaluation. We might experiment with various algorithms (Logistic Regression, Gradient Boosting, etc.) and hyperparameter configurations.

Example Notebook Snippet (Conceptual):


from sklearn.model_selection import train_test_split
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score

# Load processed data
df_processed = pd.read_csv('processed_credit_data.csv')

# Define features (X) and target (y)
X = df_processed.drop('CreditRisk', axis=1) # 'CreditRisk' is our target variable
y = df_processed['CreditRisk']

# Split data
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)

# Initialize and train model
model = GradientBoostingClassifier(n_estimators=100, learning_rate=0.1, max_depth=3, random_state=42)
model.fit(X_train, y_train)

# Make predictions
y_pred = model.predict(X_test)
y_proba = model.predict_proba(X_test)[:, 1]

# Evaluate model
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}")
print(f"ROC AUC: {roc_auc_score(y_test, y_proba):.4f}")

# Save the trained model locally (for initial testing)
import joblib
joblib.dump(model, 'initial_credit_risk_model.joblib')
    

1.3. Version Control for Code and Data

This is where the transition from “notebook land” to MLOps begins. All code, including notebooks, scripts, and configuration files, must be under version control using Git. For data and models, which are often large and don’t fit well into Git, we use specialized tools.

  • Code Versioning (Git): Use Git and platforms like GitHub, GitLab, or Bitbucket for all code. Implement branching strategies (e.g., GitFlow, GitHub Flow) and code reviews.
  • Data Versioning (DVC): Data Version Control (DVC) allows you to version datasets and machine learning models in a Git-like fashion. It stores metadata about your data files in Git, while the actual large data files are stored in remote storage (S3, GCS, Azure Blob, local filesystem). This ensures reproducibility of experiments based on specific data versions.
  • Model Versioning (DVC/MLflow): Similar to data, models can be versioned with DVC. Alternatively, MLflow’s Model Registry offers a more centralized, API-driven approach to manage model lifecycle, stages, and versions.

1.4. Experiment Tracking (MLflow)

As we run numerous experiments (different features, algorithms, hyperparameters), we need a systematic way to track them. MLflow is an open-source platform that provides tools for:

  • Tracking: Logging parameters, metrics, and artifacts (models, plots) from ML experiments.
  • Projects: Packaging ML code in a reusable and reproducible format.
  • Models: Managing and deploying ML models from various ML libraries.
  • Model Registry: A centralized store for managing the full lifecycle of an MLflow Model, including versioning, stage transitions (Staging, Production), and annotations.

Example: Integrating MLflow into Training Script


import mlflow
import mlflow.sklearn
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score
import joblib

def train_credit_risk_model(n_estimators, learning_rate, max_depth, random_state, data_path):
    with mlflow.start_run():
        # Log hyperparameters
        mlflow.log_param("n_estimators", n_estimators)
        mlflow.log_param("learning_rate", learning_rate)
        mlflow.log_param("max_depth", max_depth)
        mlflow.log_param("random_state", random_state)

        # Load processed data (assuming data_path points to our processed CSV)
        df_processed = pd.read_csv(data_path)
        X = df_processed.drop('CreditRisk', axis=1)
        y = df_processed['CreditRisk']
        X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=random_state, stratify=y)

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

        # Evaluate model
        y_pred = model.predict(X_test)
        y_proba = model.predict_proba(X_test)[:, 1]

        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)
        roc_auc = roc_auc_score(y_test, y_proba)

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

        # Log the model
        mlflow.sklearn.log_model(model, "model", registered_model_name="CreditRiskClassifier")

        print(f"MLflow Run ID: {mlflow.active_run().info.run_id}")
        print(f"Metrics logged: Accuracy={accuracy:.4f}, ROC AUC={roc_auc:.4f}")

if __name__ == "__main__":
    # Example usage:
    # Set MLflow tracking URI (e.g., to a local file, remote server, or Databricks)
    mlflow.set_tracking_uri("http://localhost:5000") # Replace with your MLflow server URI
    mlflow.set_experiment("Credit_Risk_Prediction")

    # This 'data_path' would ideally be versioned by DVC
    # For now, assume 'processed_credit_data.csv' is available
    train_credit_risk_model(n_estimators=150, learning_rate=0.05, max_depth=4, random_state=42, data_path='processed_credit_data.csv')
    

This script can now be run repeatedly, and each run’s parameters, metrics, and the trained model itself will be logged to the MLflow server. You can then compare runs in the MLflow UI to select the best performing model.

1.5. Transitioning from Notebook to Modular Scripts

This is a critical step. While notebooks are great for exploration, they become unwieldy for production. Refactor your notebook code into modular Python scripts:

  • data_preprocessing.py: Functions for data cleaning, feature engineering.
  • model_training.py: Function to train the model, log with MLflow.
  • model_evaluation.py: Functions to evaluate model performance, generate plots.
  • config.py: Store parameters, file paths.

This modularity improves readability, testability, and maintainability, making it easier to integrate into automated pipelines.

Phase 2: Orchestration & Automation (The MLOps Pipeline)

With our modularized code and experiment tracking in place, the next step is to automate the entire ML workflow. This involves creating an MLOps pipeline that can be triggered automatically (e.g., by new data, code changes) or on a schedule.

2.1. Defining the Pipeline Components and Stages

An MLOps pipeline for our credit risk model might look like this:


    +-----------------+        +---------------------+        +---------------------+        +------------------+        +-------------------+
    | Data Ingestion  | ---->  | Data Preprocessing  | ---->  | Feature Engineering | ---->  | Model Training   | ---->  | Model Evaluation  |
    | (Fetch raw data)|        | (Clean, validate)   |        | (Create new features)|       | (Train, tune, log)|        | (Compare, register) |
    +-----------------+        +---------------------+        +---------------------+        +------------------+        +-------------------+
            |                                                                                                                  |
            |                                                                                                                  v
            |                                                                                                          +-------------------+
            |                                                                                                          | Model Registration|
            |                                                                                                          | (MLflow Registry) |
            |                                                                                                          +-------------------+
            v
    +---------------------+
    | Data Version Control|
    | (DVC)               |
    +---------------------+
    

2.2. Data Ingestion & Validation

This stage fetches raw data from various sources (databases, data lakes, APIs). Data validation is crucial here to ensure data quality and schema adherence before it enters the pipeline.

  • Tools: Pandas, Great Expectations, Deequ.
  • Action: Fetch credit_data.csv from S3, validate schema and ranges using Great Expectations.

Code Snippet (Conceptual Data Validation with Great Expectations):


import great_expectations as ge
import pandas as pd

def validate_data(data_path):
    df = pd.read_csv(data_path)
    context = ge.data_context.DataContext()
    
    # Create a simple expectation suite (this would typically be defined beforehand)
    suite = context.create_expectation_suite(
        expectation_suite_name="credit_data_suite", overwrite_existing=True
    )
    batch = context.get_batch(df, batch_kwargs={"dataset": df}, expectation_suite=suite)
    
    batch.expect_column_to_exist("CreditScore")
    batch.expect_column_values_to_be_between("CreditScore", min_value=300, max_value=850)
    batch.expect_column_values_to_not_be_null("CustomerID")
    batch.expect_column_values_to_be_of_type("LoanAmount", "float")
    batch.expect_column_distinct_values_to_be_in_set("CreditRisk", [0, 1])

    batch.save_expectation_suite(discard_failed_expectations=False)

    # Run validation
    results = context.run_validation_operator(
        "action_list_operator",
        assets_to_validate=[batch]
    )

    if not results.success:
        print("Data validation failed! Check report.")
        # Optionally, raise an error to stop the pipeline
        # raise ValueError("Data validation failed")
    else:
        print("Data validation successful!")
    
    return results.success

if __name__ == "__main__":
    # Simulate fetching data
    # Create a dummy CSV for demonstration
    dummy_data = {
        'CustomerID': [1, 2, 3], 'CreditScore': [700, 650, 800],
        'LoanAmount': [10000.0, 15000.0, 20000.0], 'CreditRisk': [0, 1, 0],
        'TotalDebt': [5000, 8000, 10000], 'MonthlyIncome': [3000, 4000, 5000],
        'LoanPurpose': ['Car', 'Home', 'Education'], 'EmploymentType': ['Salaried', 'Self-employed', 'Salaried']
    }
    pd.DataFrame(dummy_data).to_csv('credit_data.csv', index=False)

    validate_data('credit_data.csv')
    

2.3. Feature Engineering (with Feature Stores)

Feature engineering transforms raw data into features suitable for ML models. For production-grade systems, a Feature Store (like Feast, Hopsworks, or managed services like AWS SageMaker Feature Store) is invaluable. It centralizes feature definitions, computes features consistently for training and inference, and serves them efficiently.

  • Benefits: Eliminates training-serving skew, promotes feature reuse, ensures consistency.
  • Action: Compute DebtToIncomeRatio, encode categorical features, and store them in Feast.

Architectural View with Feature Store:


    [Raw Data Sources] --(ETL)--> [Offline Feature Store (e.g., S3/Data Lake)]
                                      |
                                      | (Batch Feature Generation)
                                      V
    [Feature Store Service (e.g., Feast)] <--(Training Dataset Creation)--> [ML Training Pipeline]
        ^                                                                         |
        | (Real-time Feature Retrieval)                                           |
        |                                                                         V
    [Online Feature Store (e.g., Redis)] <--(Low-latency Retrieval)--> [Online Inference Service]
    

2.4. Model Training & Hyperparameter Tuning

This stage executes our model_training.py script, potentially within a hyperparameter tuning framework (Optuna, Hyperopt, or cloud services like SageMaker Hyperparameter Tuning). MLflow tracks all experiments.

Pipeline Task: Execute python model_training.py --n_estimators 100 --learning_rate 0.1 ...

2.5. Model Evaluation & Selection

After training, the model needs to be rigorously evaluated. This isn’t just about metrics; it’s about comparing the new model against the current production model, performing fairness checks, and ensuring it meets business criteria.

  • Action: Retrieve current production model from MLflow Model Registry, compare performance metrics, run A/B tests if needed.
  • Criteria: Is the new model significantly better? Is it stable? Does it introduce bias?

2.6. Model Registry (MLflow Model Registry)

Once a model is deemed production-worthy, it’s registered in a Model Registry. This serves as a central hub to manage models through their lifecycle: staging, production, archived. Each model version is associated with its lineage (training run, parameters, metrics, artifacts).

MLflow Model Registry States:


    [Model Trained & Logged (MLflow Tracking)]
            |
            V
    [Registered as Version 1 in MLflow Model Registry] --(Stage: None)
            |
            V
    [Transitioned to Staging] --(Stage: Staging)
            |
            V
    [Transitioned to Production] --(Stage: Production)
            |
            V
    [Transitioned to Archived] --(Stage: Archived)
    

2.7. Orchestrators: Apache Airflow, Kubeflow Pipelines, Prefect

Orchestrators tie all these modular scripts and components together into a directed acyclic graph (DAG) of tasks. They manage task dependencies, scheduling, error handling, and retries.

  • Apache Airflow: Widely used for batch-oriented data pipelines. Define DAGs in Python.
  • Kubeflow Pipelines: Designed for ML workflows on Kubernetes, offering containerization and reproducibility.
  • Prefect: A modern dataflow automation system, focusing on flexibility and resilience.

For our credit risk model, let’s consider a simplified Airflow DAG:

Conceptual Airflow DAG Structure (credit_risk_pipeline.py):


from airflow import DAG
from airflow.operators.bash_operator import BashOperator
from airflow.operators.python_operator import PythonOperator
from datetime import datetime, timedelta

# Import our modularized scripts
# from scripts.data_preprocessing import preprocess_data
# from scripts.model_training import train_credit_risk_model
# from scripts.model_evaluation import evaluate_and_register_model
# from scripts.data_validation import validate_data

default_args = {
    'owner': 'khadervali',
    'depends_on_past': False,
    'start_date': datetime(2023, 1, 1),
    'email_on_failure': False,
    'email_on_retry': False,
    'retries': 1,
    'retry_delay': timedelta(minutes=5),
}

with DAG(
    'credit_risk_ml_pipeline',
    default_args=default_args,
    description='End-to-end MLOps pipeline for Credit Risk Prediction',
    schedule_interval=timedelta(days=7), # Run weekly
    catchup=False,
    tags=['mlops', 'credit_risk'],
) as dag:
    # Task 1: Data Ingestion (e.g., fetch from S3)
    fetch_data_task = BashOperator(
        task_id='fetch_raw_data',
        bash_command='aws s3 cp s3://my-credit-data-bucket/raw/credit_data.csv /tmp/credit_data.csv',
    )

    # Task 2: Data Validation
    validate_data_task = PythonOperator(
        task_id='validate_raw_data',
        python_callable=validate_data, # Assuming validate_data is imported from scripts.data_validation
        op_kwargs={'data_path': '/tmp/credit_data.csv'},
    )

    # Task 3: Data Preprocessing & Feature Engineering
    preprocess_data_task = PythonOperator(
        task_id='preprocess_and_feature_engineer_data',
        python_callable=preprocess_data, # Assuming preprocess_data is imported
        op_kwargs={'input_path': '/tmp/credit_data.csv', 'output_path': '/tmp/processed_credit_data.csv'},
    )

    # Task 4: Model Training (with MLflow logging)
    train_model_task = PythonOperator(
        task_id='train_credit_risk_model',
        python_callable=train_credit_risk_model, # Assuming train_credit_risk_model is imported
        op_kwargs={
            'n_estimators': 150,
            'learning_rate': 0.05,
            'max_depth': 4,
            'random_state': 42,
            'data_path': '/tmp/processed_credit_data.csv'
        },
    )

    # Task 5: Model Evaluation and Registration (to MLflow Model Registry)
    evaluate_register_model_task = PythonOperator(
        task_id='evaluate_and_register_model',
        python_callable=evaluate_and_register_model, # Assuming this function handles MLflow model registration
        op_kwargs={'model_name': 'CreditRiskClassifier', 'min_roc_auc': 0.85}, # Example evaluation criteria
    )

    # Define task dependencies
    fetch_data_task >> validate_data_task >> preprocess_data_task >> train_model_task >> evaluate_register_model_task
    

This DAG defines a clear flow, where each task represents a step in our MLOps pipeline. Airflow ensures tasks run in order, handles retries, and provides a UI to monitor progress.

2.8. CI/CD for ML Code

Continuous Integration/Continuous Deployment (CI/CD) is not just for application code; it’s essential for ML pipelines too.

  • CI (Continuous Integration):
    • Triggered on code commits (e.g., pull requests to main branch).
    • Lints code (Flake8, Black), runs unit tests, and potentially integration tests (e.g., small-scale pipeline run).
    • Builds Docker images for pipeline components.
  • CD (Continuous Deployment):
    • If CI passes, deploy the updated pipeline code (e.g., to Airflow scheduler).
    • If a new model is registered and approved in MLflow, trigger a model deployment pipeline (covered in the next phase).

Written by

Khader Vali

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

Share this article

Related Articles

Building AI Agents with Function Calling in Python hero image showcasing LLM tool use architecture

Building AI Agents with Function Calling in Python

Jun 01, 2026 · 16 min read

RLHF Explained: Aligning AI with Human Preferences

Oct 05, 2026 · 17 min read

Responsible AI: Fairness, Explainability & Bias in LLMs

Sep 01, 2026 · 17 min read