AW Dev Rethought

🕵️ Debugging is like being the detective in a crime movie where you are also the murderer - Filipe Fortes

⚡️ Saturday ML Spark – 🐤 Canary Deployment for Machine Learning Models


Description:

Deploying a new machine learning model directly to every user can be risky. Even if a model performs exceptionally well during testing, real-world production environments often behave differently. Changes in user behaviour, unseen data, or unexpected edge cases can lead to poor predictions that affect thousands—or even millions—of users.

To reduce this risk, organisations use a deployment strategy called Canary Deployment.

In this project, we’ll learn how Canary Deployment works and simulate a gradual rollout of a new machine learning model using Python.


What Is Canary Deployment?

Canary Deployment is a release strategy where a new model is initially deployed to only a small percentage of users, while the majority continue using the current production model.

For example:

90% Users
        │
        ▼
Production Model (v1)

10% Users
        │
        ▼
Canary Model (v2)

Instead of replacing the existing model immediately, the new model is tested with a limited group of real users.


Why Is It Called “Canary”?

The term comes from the historical practice of coal miners carrying canaries into mines.

If dangerous gases were present, the canary would show signs of distress before humans were affected, providing an early warning.

Similarly, a Canary Deployment exposes only a small portion of production traffic to the new model. If issues arise, they are detected early before impacting the entire user base.


Training Two Models

To simulate a production environment, we’ll train two different models.

  • Production Model (v1) → Logistic Regression
  • Canary Model (v2) → Random Forest
production_model.fit(
    X_train,
    y_train
)

canary_model.fit(
    X_train,
    y_train
)

The production model represents the stable version already serving users, while the canary model is the new candidate being evaluated.


Splitting Production Traffic

Next, we simulate live production traffic.

Instead of sending every request to the new model, we split the traffic.

For example:

90% → Production Model

10% → Canary Model

In Python:

traffic_percentage = 0.10

This means only 10% of incoming requests are handled by the new model.


Evaluating Model Performance

Both models make predictions on their assigned traffic.

We then compare evaluation metrics such as:

  • Accuracy
  • F1-score
  • Latency
  • Error rate

A simple example compares accuracy.

accuracy_score(
    y_true,
    predictions
)

If the canary model performs at least as well as the production model, it becomes a strong candidate for wider deployment.


Making the Deployment Decision

After evaluating the results, the deployment strategy determines the next step.

If the new model performs well:

90% → Production

10% → Canary

        ↓

75% → Production

25% → Canary

        ↓

50% → Production

50% → Canary

        ↓

100% → Canary

Eventually, the canary model replaces the production model completely.

If performance declines at any stage, the rollout is stopped and traffic is redirected back to the stable model.


Canary vs Shadow Deployment

Although both strategies reduce deployment risk, they work differently.

Shadow Deployment

  • Real requests are copied to the new model.
  • Predictions are not shown to users.
  • Used for silent evaluation.

Canary Deployment

  • Real users receive predictions from the new model.
  • Only a small percentage of traffic is affected.
  • Used for gradual production rollout.

Shadow deployment tests safely behind the scenes, while Canary Deployment validates the model with actual users.


Where Canary Deployment Is Used

Canary deployments are common in:

  • recommendation systems
  • fraud detection platforms
  • search engines
  • customer support AI
  • financial risk models
  • healthcare prediction systems

Any production ML application where reliability is critical can benefit from this deployment strategy.


Key Takeaways

  1. Canary Deployment gradually introduces a new model to production.
  2. Only a small percentage of users initially receive predictions from the new model.
  3. Model performance is monitored before increasing traffic.
  4. Poor-performing models can be rolled back immediately with minimal impact.
  5. Canary Deployment is one of the safest and most widely adopted production deployment strategies for machine learning.

Conclusion

Deploying machine learning models isn’t just about achieving high accuracy—it also requires minimising risk during rollout. Canary Deployment provides a controlled and measurable way to introduce new models into production by exposing them to a small percentage of real users before a full release. This approach enables teams to validate performance under real-world conditions, detect issues early, and confidently promote successful models while retaining the ability to roll back instantly if needed.

This continues the ML Systems Advanced track in Saturday ML Spark ⚡️, where we’re exploring the production strategies and operational practices that keep modern machine learning systems reliable, scalable, and safe.


Code Snippet:

# 📦 Import Required Libraries
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier

from sklearn.metrics import (
    accuracy_score,
    precision_score,
    recall_score,
    f1_score
)


# =========================================================
# 🧩 Load Dataset
# =========================================================

data = load_breast_cancer()

X = pd.DataFrame(
    data.data,
    columns=data.feature_names
)

# Keep target as a Pandas Series
y = pd.Series(
    data.target,
    name="target"
)


# =========================================================
# ✂️ Split Training and Live Data
# =========================================================

X_train, X_live, y_train, y_live = train_test_split(
    X,
    y,
    test_size=0.30,
    random_state=42,
    stratify=y
)


# Reset live-data indices for clean traffic routing
X_live = X_live.reset_index(drop=True)
y_live = y_live.reset_index(drop=True)


# =========================================================
# 🤖 Train Production Model – Version 1
# =========================================================

production_model = LogisticRegression(
    max_iter=5000
)

production_model.fit(
    X_train,
    y_train
)


# =========================================================
# 🐤 Train Canary Model – Version 2
# =========================================================

canary_model = RandomForestClassifier(
    n_estimators=200,
    random_state=42
)

canary_model.fit(
    X_train,
    y_train
)


# =========================================================
# 🚦 Configure Canary Traffic
# =========================================================

CANARY_TRAFFIC_PERCENTAGE = 0.10

rng = np.random.default_rng(seed=42)

traffic_assignment = rng.choice(
    ["production", "canary"],
    size=len(X_live),
    p=[
        1 - CANARY_TRAFFIC_PERCENTAGE,
        CANARY_TRAFFIC_PERCENTAGE
    ]
)


# =========================================================
# 🛣️ Route Live Requests
# =========================================================

production_mask = (
    traffic_assignment == "production"
)

canary_mask = (
    traffic_assignment == "canary"
)

X_production = X_live.loc[production_mask]
y_production = y_live.loc[production_mask]

X_canary = X_live.loc[canary_mask]
y_canary = y_live.loc[canary_mask]


print("=== Traffic Distribution ===")
print(
    "Production Requests:",
    len(X_production)
)

print(
    "Canary Requests:",
    len(X_canary)
)


# =========================================================
# 📊 Generate Predictions
# =========================================================

production_predictions = production_model.predict(
    X_production
)

canary_predictions = canary_model.predict(
    X_canary
)


# =========================================================
# 📈 Calculate Model Metrics
# =========================================================

production_metrics = {
    "Model": "Production Model",
    "Traffic Count": len(X_production),

    "Accuracy": accuracy_score(
        y_production,
        production_predictions
    ),

    "Precision": precision_score(
        y_production,
        production_predictions,
        zero_division=0
    ),

    "Recall": recall_score(
        y_production,
        production_predictions,
        zero_division=0
    ),

    "F1 Score": f1_score(
        y_production,
        production_predictions,
        zero_division=0
    )
}


canary_metrics = {
    "Model": "Canary Model",
    "Traffic Count": len(X_canary),

    "Accuracy": accuracy_score(
        y_canary,
        canary_predictions
    ),

    "Precision": precision_score(
        y_canary,
        canary_predictions,
        zero_division=0
    ),

    "Recall": recall_score(
        y_canary,
        canary_predictions,
        zero_division=0
    ),

    "F1 Score": f1_score(
        y_canary,
        canary_predictions,
        zero_division=0
    )
}


# =========================================================
# 📋 Create Results DataFrame
# =========================================================

results = pd.DataFrame([
    production_metrics,
    canary_metrics
])

print("\n=== Canary Deployment Results ===\n")
print(results.round(4))


# =========================================================
# 📊 Visualize Traffic Allocation
# =========================================================

traffic_summary = pd.Series(
    traffic_assignment
).value_counts()

plt.figure(figsize=(6, 4))

plt.bar(
    ["Production", "Canary"],
    [
        traffic_summary.get("production", 0),
        traffic_summary.get("canary", 0)
    ]
)

plt.title("Canary Deployment Traffic Allocation")
plt.xlabel("Model")
plt.ylabel("Request Count")
plt.tight_layout()
plt.show()


# =========================================================
# 📈 Visualize Metric Comparison
# =========================================================

metric_columns = [
    "Accuracy",
    "Precision",
    "Recall",
    "F1 Score"
]

results.set_index("Model")[
    metric_columns
].plot(
    kind="bar",
    figsize=(9, 5)
)

plt.title("Production vs Canary Model Performance")
plt.ylabel("Score")
plt.ylim(0, 1.05)
plt.xticks(rotation=0)
plt.grid(
    axis="y",
    linestyle="--",
    alpha=0.5
)
plt.tight_layout()
plt.show()


# =========================================================
# 🚦 Simulate Deployment Decision
# =========================================================

production_f1 = production_metrics[
    "F1 Score"
]

canary_f1 = canary_metrics[
    "F1 Score"
]

MINIMUM_CANARY_SAMPLES = 10
MINIMUM_IMPROVEMENT = 0.00


print("\n=== Deployment Decision ===")

if len(X_canary) < MINIMUM_CANARY_SAMPLES:

    decision = (
        "Continue collecting canary traffic "
        "before making a rollout decision."
    )

elif (
    canary_f1
    >= production_f1 + MINIMUM_IMPROVEMENT
):

    decision = (
        "Increase canary traffic gradually."
    )

else:

    decision = (
        "Roll back the canary model "
        "and keep the production model active."
    )

print(decision)


# =========================================================
# 📝 Create Request-Level Deployment Logs
# =========================================================

deployment_logs = pd.DataFrame({
    "request_id": range(
        1,
        len(X_live) + 1
    ),

    "assigned_model": traffic_assignment,

    "actual": y_live
})


deployment_logs["prediction"] = np.nan


deployment_logs.loc[
    production_mask,
    "prediction"
] = production_predictions


deployment_logs.loc[
    canary_mask,
    "prediction"
] = canary_predictions


deployment_logs["prediction"] = (
    deployment_logs["prediction"]
    .astype(int)
)


deployment_logs["correct"] = (
    deployment_logs["actual"]
    ==
    deployment_logs["prediction"]
)


print("\n=== Deployment Logs Preview ===\n")
print(deployment_logs.head(10))


# =========================================================
# 💾 Save Results
# =========================================================

results.to_csv(
    "canary_model_metrics.csv",
    index=False
)

deployment_logs.to_csv(
    "canary_deployment_logs.csv",
    index=False
)

print(
    "\nModel metrics saved to canary_model_metrics.csv"
)

print(
    "Deployment logs saved to canary_deployment_logs.csv"
)


# =========================================================
# ✅ Final Summary
# =========================================================

print("\n" + "=" * 48)
print(" Canary Deployment Simulation Completed")
print("=" * 48)

print(
    f"Production Traffic: "
    f"{len(X_production)} requests"
)

print(
    f"Canary Traffic: "
    f"{len(X_canary)} requests"
)

print(
    f"Production F1: "
    f"{production_f1:.4f}"
)

print(
    f"Canary F1: "
    f"{canary_f1:.4f}"
)

print(
    f"Decision: {decision}"
)

Link copied!

Comments

Add Your Comment

Comment Added!