ML Pipeline Guide: Building Production Data Pipelines
A machine learning pipeline connects raw data to model predictions through a series of repeatable, automated steps. Well-designed pipelines ensure reproducibility, reduce errors, and scale from experimentation to production.
Pipeline Architecture
A complete ML pipeline consists of stages:
- Data ingestion — collect data from sources
- Data validation — check schema, quality, distributions
- Data preprocessing — clean, impute, scale
- Feature engineering — create features from raw data
- Feature selection — choose relevant features
- Model training — train and tune models
- Model evaluation — validate performance
- Model deployment — save and serve model
- Prediction — generate predictions on new data
Data Ingestion
import pandas as pd
from sqlalchemy import create_engine
import pyarrow.parquet as pq
class DataIngestor:
def __init__(self, config: dict):
self.config = config
self.db_engine = create_engine(config['database_url'])
def ingest_from_db(self, query: str) -> pd.DataFrame:
return pd.read_sql(query, self.db_engine)
def ingest_from_s3(self, path: str) -> pd.DataFrame:
return pd.read_parquet(path, engine='pyarrow')
def ingest_from_api(self, url: str, params: dict) -> pd.DataFrame:
response = requests.get(url, params=params)
response.raise_for_status()
return pd.DataFrame(response.json())
def ingest_streaming(self, topic: str) -> pd.DataFrame:
"""Ingest from Kafka topic."""
consumer = KafkaConsumer(
topic,
bootstrap_servers=self.config['kafka_brokers'],
value_deserializer=lambda m: json.loads(m.decode('utf-8'))
)
messages = [consumer.poll(timeout_ms=5000)]
return pd.DataFrame(messages)Data Validation
import pandera as pa
from typing import Dict, Any
class DataValidator:
def __init__(self):
self.schema = pa.DataFrameSchema({
'customer_id': pa.Column(int, pa.Check.unique(), nullable=False),
'age': pa.Column(int, pa.Check.in_range(0, 120)),
'income': pa.Column(float, pa.Check.greater_than(0)),
'signup_date': pa.Column(pd.Timestamp, nullable=False),
'plan_type': pa.Column(str, pa.Check.isin(['basic', 'premium', 'enterprise'])),
'email': pa.Column(str, pa.Check.str_matches(r'^[\w.+-]+@[\w-]+\.[\w.]+$')),
})
def validate(self, df: pd.DataFrame) -> pa.DataFrameModel:
"""Validate dataframe against schema. Raises SchemaError on failure."""
return self.schema.validate(df, lazy=True)
def generate_report(self, df: pd.DataFrame) -> Dict[str, Any]:
"""Generate data quality report."""
report = {
'row_count': len(df),
'column_count': len(df.columns),
'missing_values': df.isnull().sum().to_dict(),
'dtypes': df.dtypes.astype(str).to_dict(),
'unique_counts': {col: df[col].nunique() for col in df.columns},
'basic_stats': df.describe().to_dict(),
}
return reportFeature Engineering Pipeline
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
class DateFeatureExtractor(BaseEstimator, TransformerMixin):
"""Extract temporal features from datetime columns."""
def __init__(self, columns: list):
self.columns = columns
def fit(self, X, y=None):
return self
def transform(self, X):
X = X.copy()
for col in self.columns:
X[f'{col}_year'] = X[col].dt.year
X[f'{col}_month'] = X[col].dt.month
X[f'{col}_day'] = X[col].dt.day
X[f'{col}_dayofweek'] = X[col].dt.dayofweek
X[f'{col}_quarter'] = X[col].dt.quarter
X[f'{col}_is_weekend'] = X[col].dt.dayofweek.isin([5, 6]).astype(int)
return X.drop(columns=self.columns)
class AggregateFeatures(BaseEstimator, TransformerMixin):
"""Create aggregate features from transaction history."""
def fit(self, X, y=None):
return self
def transform(self, X):
X = X.copy()
X['total_purchases_log'] = np.log1p(X['total_purchases'])
X['avg_order_value_clipped'] = X['avg_order_value'].clip(upper=X['avg_order_value'].quantile(0.99))
X['purchase_frequency'] = X['total_purchases'] / (X['customer_tenure_days'] + 1)
X['value_per_day'] = X['total_revenue'] / (X['customer_tenure_days'] + 1)
return X
# Build pipeline
numeric_features = ['age', 'income', 'total_purchases', 'avg_order_value']
categorical_features = ['plan_type', 'country', 'source_channel']
date_features = ['signup_date', 'last_purchase_date']
preprocessing_pipeline = ColumnTransformer(
transformers=[
('numeric', StandardScaler(), numeric_features),
('categorical', OneHotEncoder(drop='first', sparse_output=False), categorical_features),
('date', DateFeatureExtractor(date_features), date_features),
('aggregates', AggregateFeatures(), numeric_features),
]
)
full_pipeline = Pipeline([
('preprocessing', preprocessing_pipeline),
('classifier', RandomForestClassifier(n_estimators=200)),
])Training Pipeline
import mlflow
from sklearn.model_selection import train_test_split, cross_val_score
def train_pipeline(config: dict):
"""End-to-end training pipeline."""
mlflow.set_experiment(config['experiment_name'])
with mlflow.start_run() as run:
# 1. Ingest data
ingestor = DataIngestor(config)
df = ingestor.ingest_from_db(config['query'])
# 2. Validate
validator = DataValidator()
validator.validate(df)
# 3. Split
X = df.drop(columns=[config['target_column']])
y = df[config['target_column']]
X_train, X_val, y_train, y_val = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# 4. Train
pipeline = full_pipeline
pipeline.fit(X_train, y_train)
# 5. Evaluate
train_score = pipeline.score(X_train, y_train)
val_score = pipeline.score(X_val, y_val)
cv_scores = cross_val_score(pipeline, X_train, y_train, cv=5)
# 6. Log metrics
mlflow.log_metrics({
'train_accuracy': train_score,
'val_accuracy': val_score,
'cv_mean': cv_scores.mean(),
'cv_std': cv_scores.std(),
})
# 7. Log model
mlflow.sklearn.log_model(pipeline, 'model')
return run.info.run_idOrchestration with Airflow
from airflow import DAG
from airflow.operators.python import PythonOperator
from datetime import datetime, timedelta
default_args = {
'owner': 'ml-team',
'depends_on_past': False,
'email_on_failure': True,
'email_on_retry': False,
'retries': 1,
'retry_delay': timedelta(minutes=5),
---
dag = DAG(
'ml_training_pipeline',
default_args=default_args,
description='Daily ML model training pipeline',
schedule_interval='0 2 * * *', # daily at 2 AM
start_date=datetime(2024, 1, 1),
catchup=False,
tags=['ml', 'training'],
)
def ingest_task(**context):
df = ingest_data()
df.to_parquet('/data/raw/{{ ds }}.parquet')
def validate_task(**context):
df = pd.read_parquet('/data/raw/{{ ds }}.parquet')
validate_data(df)
def train_task(**context):
run_id = train_model()
context['ti'].xcom_push(key='run_id', value=run_id)
def evaluate_task(**context):
run_id = context['ti'].xcom_pull(key='run_id', task_ids='train')
evaluate_model(run_id)
def deploy_task(**context):
run_id = context['ti'].xcom_pull(key='run_id', task_ids='train')
deploy_model(run_id)
with dag:
t1 = PythonOperator(task_id='ingest', python_callable=ingest_task)
t2 = PythonOperator(task_id='validate', python_callable=validate_task)
t3 = PythonOperator(task_id='train', python_callable=train_task)
t4 = PythonOperator(task_id='evaluate', python_callable=evaluate_task)
t5 = PythonOperator(task_id='deploy', python_callable=deploy_task)
t1 >> t2 >> t3 >> t4 >> t5Versioning
Use DVC (Data Version Control) for data and pipeline versioning:
# Initialize DVC
dvc init
# Track data
dvc add data/raw/customer_data.parquet
git add data/raw/customer_data.parquet.dvc
git commit -m "add customer data"
# Define pipeline
dvc run -n preprocess \
-d src/preprocess.py \
-d data/raw/customer_data.parquet \
-o data/processed/customer_features.parquet \
python src/preprocess.py
dvc run -n train \
-d src/train.py \
-d data/processed/customer_features.parquet \
-o models/churn_model.pkl \
python src/train.py
# Reproduce pipeline
dvc reproReal-World Implementation Tips
Production Considerations
When moving from development to production, several factors become critical. Error handling should be comprehensive — every external call (database, API, file system) should have proper error checking, logging, and retry logic where appropriate. Performance monitoring through metrics and structured logging helps identify bottlenecks before they affect users.
Testing Strategy
A thorough testing approach combines multiple levels:
- Unit tests verify individual functions and methods in isolation
- Integration tests validate that components work together correctly
- Edge case tests cover boundary conditions, empty inputs, and error states
- Performance tests ensure the system meets latency and throughput requirements
Test data should be realistic but controlled. Mock external dependencies to make tests fast and deterministic. Aim for tests that are independent, repeatable, and fast enough to run on every commit.
Documentation
Good documentation is essential for maintainable code. Follow these principles:
- Document the “why” not just the “what” — explain design decisions
- Keep examples up to date with the code
- Include usage examples for public APIs
- Document configuration options and their defaults
- Explain error conditions and recovery strategies
Security Best Practices
Security should be considered throughout development:
- Validate all inputs at system boundaries
- Use parameterized queries for database access
- Store secrets in environment variables or secret managers
- Keep dependencies updated to patch vulnerabilities
- Apply the principle of least privilege
Performance Optimization
Optimize based on measured data, not assumptions:
- Profile before optimizing — identify actual bottlenecks
- Measure the impact of each change
- Consider the trade-off between speed and readability
- Cache expensive operations with appropriate invalidation
- Use connection pooling for database and network resources
Monitoring and Observability
Production systems need visibility:
- Structured logging with correlation IDs for request tracking
- Metrics for latency, throughput, error rates, and resource usage
- Health check endpoints for load balancers and orchestration
- Distributed tracing for request flows across services
- Alerts for anomaly detection based on baselines
These patterns apply across all programming languages and frameworks. The specific implementation varies, but the principles remain consistent.
FAQ
Q: What is the difference between an ML pipeline and a data pipeline? A: A data pipeline moves and transforms raw data. An ML pipeline includes training, evaluation, and model-specific steps. ML pipelines are data pipelines plus ML-specific orchestration.
Q: How do I handle data versioning in pipelines? A: Use DVC for data and pipeline versioning, or lakeFS for git-like data versioning. Store data snapshots corresponding to each model training run.
Q: Should I use a single pipeline or multiple pipelines? A: Separate pipelines for training and inference. Training runs on a schedule (daily/weekly). Inference runs on-demand (real-time) or scheduled (batch). They share preprocessing logic but have different performance requirements.
Q: How do I ensure training and serving use the same features? A: Use the same preprocessing code (notebook cells become functions). A feature store ensures consistent feature computation. Serialize the preprocessing pipeline with the model.
Q: What is pipeline orchestration? A: Orchestration manages the execution order, retries, error handling, and scheduling of pipeline steps. Airflow, Prefect, and Kubeflow are common orchestrators.
Q: How do I test ML pipelines? A: Test each component independently (unit tests for feature engineering, validation, model loading). Test the full pipeline on a small dataset. Monitor production pipeline runs for failures and drift.
ML Pipeline Automation
Complete Pipeline Example
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.model_selection import cross_val_score
numeric_features = ['age', 'income', 'score']
categorical_features = ['education', 'region', 'occupation']
preprocessor = ColumnTransformer(
transformers=[
('num', StandardScaler(), numeric_features),
('cat', OneHotEncoder(drop='first', sparse_output=False), categorical_features)
])
pipeline = Pipeline([
('preprocessor', preprocessor),
('feature_selector', SelectKBest(k=20)),
('classifier', GradientBoostingRegressor(
n_estimators=300, max_depth=5, learning_rate=0.05
))
])
scores = cross_val_score(pipeline, X, y, cv=5, scoring='r2')
print(f"Cross-val R-squared: {scores.mean():.3f} plus or minus {scores.std():.3f}")Pipeline Persistence
import joblib
# Save the complete pipeline
joblib.dump(pipeline, 'model_pipeline.pkl')
# Load and predict in production
loaded_pipeline = joblib.load('model_pipeline.pkl')
predictions = loaded_pipeline.predict(new_data)Feature Union (Multiple Feature Extractors)
from sklearn.pipeline import FeatureUnion
from sklearn.decomposition import PCA
from sklearn.feature_selection import SelectKBest
combined_features = FeatureUnion([
('pca', PCA(n_components=5)),
('kbest', SelectKBest(k=10))
])
full_pipeline = Pipeline([
('preprocessor', preprocessor),
('features', combined_features),
('classifier', RandomForestClassifier())
])For a comprehensive overview, read our article on Deep Learning Guide.
For a comprehensive overview, read our article on Ensemble Methods Guide.