Skip to content
Home
ML Pipeline Guide: Building Production Data Pipelines

ML Pipeline Guide: Building Production Data Pipelines

Machine Learning Machine Learning 7 min read 1431 words Beginner ExcellentWiki Editorial Team

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:

  1. Data ingestion — collect data from sources
  2. Data validation — check schema, quality, distributions
  3. Data preprocessing — clean, impute, scale
  4. Feature engineering — create features from raw data
  5. Feature selection — choose relevant features
  6. Model training — train and tune models
  7. Model evaluation — validate performance
  8. Model deployment — save and serve model
  9. 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 report

Feature 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_id

Orchestration 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 >> t5

Versioning

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 repro

Real-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:

  1. Profile before optimizing — identify actual bottlenecks
  2. Measure the impact of each change
  3. Consider the trade-off between speed and readability
  4. Cache expensive operations with appropriate invalidation
  5. 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.

Section: Machine Learning 1431 words 7 min read Beginner 756 articles in section Report inaccuracy Back to top