Wednesday, September 2, 2026
HomeData ScienceMLOps Best Practices – CI/CD for Machine Learning Pipelines 2026

MLOps Best Practices – CI/CD for Machine Learning Pipelines 2026

Table of Content

Building a machine learning model is 20% of the work. Getting it to production reliably, keeping it accurate over time, and retraining it automatically when performance degrades — that is MLOps. This guide covers the full MLOps lifecycle: experiment tracking, model registry, CI/CD pipelines, and production monitoring.

Why MLOps?

ML models are not static software. Data distributions shift, features get deprecated, and model accuracy degrades silently. Without MLOps, teams spend hours manually retraining and redeploying models, experiments are unreproducible, and production failures go undetected. MLOps applies DevOps principles to ML: automate everything, version everything, monitor everything.

Experiment Tracking with MLflow

pip install mlflow scikit-learn

import mlflow
import mlflow.sklearn
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
import numpy as np

mlflow.set_tracking_uri('sqlite:///mlflow.db')
mlflow.set_experiment('breast-cancer-classifier')

X, y = load_breast_cancer(return_X_y=True)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42)

# Grid of hyperparameters to try
configs = [
    {'n_estimators': 50,  'max_depth': 3},
    {'n_estimators': 100, 'max_depth': 5},
    {'n_estimators': 200, 'max_depth': 7},
]

for cfg in configs:
    with mlflow.start_run():
        mlflow.log_params(cfg)

        model  = RandomForestClassifier(**cfg, random_state=42, n_jobs=-1)
        cv_auc = cross_val_score(model, X_tr, y_tr, cv=5,
                                 scoring='roc_auc').mean()
        model.fit(X_tr, y_tr)
        test_acc = model.score(X_te, y_te)

        mlflow.log_metric('cv_auc',   cv_auc)
        mlflow.log_metric('test_acc', test_acc)
        mlflow.sklearn.log_model(model, 'model',
                                 registered_model_name='BreastCancerRF')

        print(f'n_est={cfg["n_estimators"]} depth={cfg["max_depth"]} '
              f'AUC={cv_auc:.4f} Acc={test_acc:.4f}')

# Launch the MLflow UI
# mlflow ui --backend-store-uri sqlite:///mlflow.db

Model Registry and Staging

from mlflow.tracking import MlflowClient

client = MlflowClient('sqlite:///mlflow.db')

# Find best run by CV AUC
exp    = client.get_experiment_by_name('breast-cancer-classifier')
runs   = client.search_runs(exp.experiment_id,
                             order_by=['metrics.cv_auc DESC'])
best   = runs[0]

print(f'Best run: {best.info.run_id}')
print(f'Best AUC: {best.data.metrics["cv_auc"]:.4f}')

# Promote best model to Production
model_versions = client.search_model_versions("name='BreastCancerRF'")
best_version   = sorted(model_versions,
                         key=lambda v: float(v.tags.get('cv_auc', 0)),
                         reverse=True)[0]

client.transition_model_version_stage(
    name='BreastCancerRF',
    version=best_version.version,
    stage='Production',
    archive_existing_versions=True
)
print(f'Version {best_version.version} promoted to Production')

# Load production model anywhere
prod_model = mlflow.sklearn.load_model(
    'models:/BreastCancerRF/Production')

CI/CD Pipeline with GitHub Actions

a close up of a computer screen with a bunch of text on it
Photo by Rahul Mishra on Unsplash
# .github/workflows/ml-pipeline.yml
name: ML CI/CD Pipeline

on:
  push:
    branches: [main]
  schedule:
    - cron: '0 6 * * 1'   # weekly retraining every Monday 6am

jobs:
  train-and-evaluate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Run data validation
        run: python src/validate_data.py

      - name: Train model
        run: python src/train.py
        env:
          MLFLOW_TRACKING_URI: ${{ secrets.MLFLOW_URI }}

      - name: Evaluate model
        run: python src/evaluate.py --threshold 0.92

      - name: Deploy if metrics pass
        if: success()
        run: python src/deploy.py
        env:
          WP_DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}

Model Monitoring and Drift Detection

from evidently.report import Report
from evidently.metric_preset import DataDriftPreset, ClassificationPreset
from evidently.metrics import *
import pandas as pd

# Reference data (training distribution)
ref_data  = pd.read_parquet('data/train.parquet')
# Current production data (last 7 days)
curr_data = pd.read_parquet('data/production_last_7d.parquet')

# Data drift report
drift_report = Report(metrics=[DataDriftPreset()])
drift_report.run(reference_data=ref_data,
                 current_data=curr_data)
drift_report.save_html('reports/drift_report.html')

# Check if drift is significant
result = drift_report.as_dict()
drifted_cols = [col for col, info in
                result['metrics'][0]['result']['drift_by_columns'].items()
                if info['drift_detected']]

if len(drifted_cols) > 0:
    print(f'⚠️  Drift detected in: {drifted_cols}')
    # Trigger retraining via webhook or CI/CD

# Model performance monitoring
perf_report = Report(metrics=[
    ClassificationPreset(),
    ColumnDriftMetric(column_name='prediction'),
])
perf_report.run(reference_data=ref_data.assign(prediction=ref_preds),
                current_data=curr_data.assign(prediction=curr_preds))
perf_report.save_html('reports/performance_report.html')

Automated Retraining Pipeline

import schedule
import time
import logging
from datetime import datetime

logger = logging.getLogger(__name__)

def check_and_retrain():
    logger.info('Running drift check...')

    drift_score = compute_drift_score()   # your drift metric
    curr_auc    = compute_current_auc()   # live model performance

    DRIFT_THRESHOLD = 0.15
    AUC_THRESHOLD   = 0.90

    if drift_score > DRIFT_THRESHOLD or curr_auc < AUC_THRESHOLD:
        logger.warning(f'Retraining triggered: drift={drift_score:.3f} '
                       f'auc={curr_auc:.3f}')
        retrain_and_deploy()
    else:
        logger.info(f'Model healthy: drift={drift_score:.3f} '
                    f'auc={curr_auc:.3f}')

def retrain_and_deploy():
    import subprocess
    result = subprocess.run(['python', 'src/train.py', '--auto'],
                            capture_output=True, text=True)
    if result.returncode == 0:
        logger.info('Retraining complete — deploying new model')
        subprocess.run(['python', 'src/deploy.py'])
    else:
        logger.error(f'Retraining failed: {result.stderr}')

# Schedule weekly check
schedule.every().monday.at('06:00').do(check_and_retrain)
schedule.every().day.at('08:00').do(lambda: log_daily_metrics())

while True:
    schedule.run_pending()
    time.sleep(60)

Conclusion

MLOps transforms ML from one-off experiments into reliable production systems. Start with MLflow for experiment tracking — it requires minimal setup and pays dividends immediately. Add a model registry to control what goes to production. Build a CI/CD pipeline that runs your training and evaluation automatically on every code push. And implement drift monitoring so you know when your model needs retraining before your users notice. These four practices alone will make your ML systems dramatically more robust.

Leave feedback about this

  • Rating

Durgesh Kekare
Durgesh Kekarehttps://www.dataexpertise.in
Durgesh Kekare is a data science educator and founder of DataExpertise.in. With expertise in Python, machine learning, and analytics, he helps 10,000+ learners break into data careers.

Latest Posts

List of Categories