ML Data Engineering

The Complete
Data Engineering
Playbook

From raw data to production ML systems — covering gathering, preprocessing, ethics, distributed computing, databases, and industry-grade projects with mathematical foundations.

Python · SQL NumPy · Pandas · JAX · PyTorch · TensorFlow Spark · Kafka · Airflow · Snowflake PostgreSQL · DuckDB · MongoDB · ChromaDB · Neo4j Crawlee · Playwright · Crawl4AI GDPR · CCPA · PDPB · Data Sovereignty
00 — OVERVIEW

The Data Lifecycle for ML Engineers

Every production ML system is only as good as the data pipeline feeding it. This guide treats the full lifecycle — from locating raw sources to serving features in real time — as a single interconnected system rather than isolated steps.

01
Gather
02
Collect
03
Prepare
04
Preprocess
05
Clean
06
Manipulate
07
Govern
08
Serve

Think of this pipeline as a value chain — each stage transforms chaos into signal. The mathematical operations at each step are deterministic; the engineering decisions are where your judgment is tested in interviews and production.

💡
How to Use This Guide

Navigate using the sidebar. Each section builds on the prior. For interview prep, head to the Interview section directly after reading the Projects. The mathematical formulas are highlighted in purple blocks throughout.

The ML Engineer's Unique Perspective

Unlike a pure Data Engineer (who optimises for pipeline throughput) or a Data Scientist (who optimises for insight), the ML Engineer optimises for model-readiness: the data must be clean enough, feature-rich enough, and reproducibly versioned to train, evaluate, and redeploy models safely in production.

🎯
Feature Quality

Signal-to-noise ratio in your feature matrix directly determines model performance ceiling.

Pipeline Velocity

How fast can you re-train? Your preprocessing must be reproducible, versioned, and fast.

🔒
Legal Safety

GDPR violations have levied billions in fines. Data governance is a first-class concern.

📈
Drift Detection

Models degrade as real-world distributions shift from your training distribution.

Mathematical Formula Quick Reference

Key formulas are embedded throughout the guide — here's a navigational index to find them fast.

CategoryFormula / ConceptSection
Feature ScalingZ-Score: x' = (x − μ) / σ  |  Min-Max: x' = (x − x_min) / (x_max − x_min)  |  Robust: x' = (x − Q2) / IQRPreparation & Preprocessing
ImputationMICE: X_j = f(X_{-j}, θ_j) — iterative chained regressionPreparation & Preprocessing
EncodingTarget Encoding: encode(x) = Σy_j / count(x)  |  Feature Hashing: h(x) = hash(x) mod 2ᵇPreparation & Preprocessing
DimensionalityPCA: X = UΣVᵀ, retain λ_k / Σλ ≥ 0.95  |  t-SNE: min KL(P ‖ Q)Preparation & Preprocessing
Outlier DetectionZ-Score: |z| > 3  |  IQR Fence: Q1 − 1.5×IQR, Q3 + 1.5×IQR  |  Isolation Forest anomaly scoreCleaning & Manipulation
FairnessDisparate Impact: P(Ŷ=1|A=min) / P(Ŷ=1|A=maj)  |  Equalised Odds  |  Demographic ParityData Ethics
Privacyε-DP: P[M(D)∈S] ≤ eᵋ × P[M(D')∈S]  |  Gaussian Mechanism: M(x) = f(x) + N(0, σ²Δf²)Dependencies & Security
MemoryMemory ≈ rows × cols × bytes_per_dtype  |  chunk_size = 0.3 × M_avail / bytes_per_rowDistributed Processing
BroadcastingNumPy: dims compatible if equal or one is 1, aligned from rightCore Tools
Loss FunctionsInfoNCE: −log[exp(sim(u,i⁺)/τ) / Σexp(sim(u,iⱼ⁻)/τ)]  |  CTGAN: min_G max_DProjects 4 & 5
Class ImbalanceSMOTE: x_new = x_i + λ(x_nn − x_i)  |  Cost: argmin_τ [FN×C_fn + FP×C_fp]Capstone Project
EvaluationE[Cost] = FN × cost_fn + FP × cost_fp  |  Cramer's V  |  Point-Biserial rInterview Prep
01 — GATHERING

Data Gathering

Data gathering is the strategic act of identifying where your signal lives. The quality of your dataset ceiling is set here — no amount of clever preprocessing can recover information that was never captured.

Primary Source Categories

Source TypeWhere to FindQuality SignalML Suitability
Open Government data.gov, data.europa.eu, data.gov.in, census.gov High — peer-reviewed collection methods Tabular / TS
Academic Repositories UCI ML Repository, Harvard Dataverse, OpenML, Zenodo Very High — curated, benchmarked All types
Platform APIs Twitter/X API, Reddit Pushshift, GitHub GraphQL, Wikipedia API Medium — rate-limited, terms-restricted NLP / Social
Financial Markets Yahoo Finance, Alpha Vantage, Quandl, FRED (St. Louis Fed) High — standardised OHLCV Time Series
IoT / Sensor Kaggle, NASA EarthData, NOAA, OpenAQ Varies — check calibration metadata Streaming / TS
Synthetic / Simulated SDV, Gretel.ai, CTGAN, Faker, DiffPrivLib Controlled — distribution assumptions matter All types
Web Scraping Crawlee, Playwright, Scrapy, Crawl4AI Low-Medium — brittle, legal risk NLP / Vision
Crowd-sourced Mechanical Turk, Scale.ai, Label Studio Medium — inter-annotator agreement critical Supervised

Key Repositories for ML

Data Gathering Strategy Framework

🧭
The 5V Assessment

Before committing to a source, evaluate: Volume (enough examples per class?), Velocity (can you keep up with updates?), Variety (format diversity vs. homogeneity?), Veracity (labelling trustworthiness, provenance), and Value (does this source add marginal lift to your model?).

# Programmatic dataset acquisition with Hugging Face
from datasets import load_dataset
import pandas as pd
 
# Load a specific split and cache locally
ds = load_dataset("imdb", split="train", cache_dir="./data_cache")
 
# Convert to Pandas for exploration
df = ds.to_pandas()
print(df.dtypes)
print(df.describe(include='all'))
 
# For Kaggle API
# pip install kaggle
# Set KAGGLE_USERNAME and KAGGLE_KEY env variables
import subprocess
subprocess.run(["kaggle", "datasets", "download",
               "-d", "username/dataset-name",
               "--unzip", "-p", "./data"])
python

Data Lineage Tracking

Every dataset you gather should have a lineage record — a machine-readable provenance log documenting source URL, access date, licence, version hash, and the transformation chain applied. Tools like Apache Atlas, DataHub, and MLflow (for experiment context) help automate this.

# Simple lineage metadata pattern
import json, hashlib
from datetime import datetime
 
def record_lineage(source_url: str, local_path: str, licence: str) -> dict:
    with open(local_path, "rb") as f:
        sha256 = hashlib.sha256(f.read()).hexdigest()
    record = {
        "source": source_url,
        "local_path": local_path,
        "accessed_at": datetime.utcnow().isoformat(),
        "sha256": sha256,
        "licence": licence,
        "transformations": []
    }
    with open("lineage.json", "a") as f:
        f.write(json.dumps(record) + "\n")
    return record
python
02 — COLLECTION

Data Collection

Collection is the execution layer — the infrastructure, protocols, and formats that move raw data from source into your control. Good collection architecture is idempotent, resumable, and schema-aware.

Structuring Your Collection Architecture

A well-designed collection pipeline has three tiers: an ingestion layer (APIs, scrapers, streams), a landing zone (raw, immutable storage — think S3 or GCS), and a staging area (where format normalisation happens before the warehouse). Never write raw data directly to a transformed table.

Ingest
API / Stream / Scrape
Land
Raw Storage (S3/GCS)
Stage
Schema Normalise
Validate
Great Expectations
Load
Warehouse / Lake

File Format Selection

FormatBest ForColumnar?CompressionSchema Evolution
ParquetAnalytical workloads, feature storesExcellent (Snappy/ZSTD)Limited
Arrow / FeatherIn-memory IPC, Pandas ↔ SparkGood (LZ4)Good
Delta LakeACID transactions on data lakes✅ (Parquet under)ExcellentExcellent
JSON Lines (JSONL)Semi-structured, NLP corporaPoor raw / Good gzipExcellent
CSVInterchange only — avoid at scalePoorNone
HDF5 / ZarrNumerical arrays, geospatial rastersGoodNone
TFRecordTensorFlow training pipelinesGoodProtobuf-based
💡
Rule of thumb for ML

Use Parquet as your analytical format, Arrow as your in-memory interchange format, and Delta Lake when you need ACID guarantees (versioning, upserts) on your feature store.

API Collection Pattern (REST & GraphQL)

import httpx, time, json
from tenacity import retry, stop_after_attempt, wait_exponential
 
@retry(stop=stop_after_attempt(5),
       wait=wait_exponential(multiplier=1, min=2, max=60))
async def fetch_paginated(url: str, headers: dict, params: dict) -> list:
    """
    Robust paginated API collection with exponential backoff.
    Handles rate-limiting (HTTP 429) gracefully.
    """
    results = []
    async with httpx.AsyncClient(timeout=30.0) as client:
        while url:
            resp = await client.get(url, headers=headers, params=params)
            if resp.status_code == 429:
                retry_after = int(resp.headers.get("Retry-After", 60))
                time.sleep(retry_after)
                continue
            resp.raise_for_status()
            data = resp.json()
            results.extend(data.get("items", []))
            url = data.get("next_page_url")   # pagination cursor
            params = {}  # cursor already encoded in next_page_url
    return results
python

Schema Validation with Great Expectations

import great_expectations as gx
 
context = gx.get_context()
ds = context.sources.add_pandas("my_source")
da = ds.add_dataframe_asset("users")
batch = da.build_batch_request()
 
suite = context.add_expectation_suite("users_suite")
validator = context.get_validator(batch_request=batch,
                                    expectation_suite_name="users_suite")
 
# Define schema contract
validator.expect_column_to_exist("user_id")
validator.expect_column_values_to_not_be_null("user_id")
validator.expect_column_values_to_be_unique("user_id")
validator.expect_column_values_to_be_between("age", min_value=0, max_value=120)
validator.expect_column_values_to_match_regex("email", r"^[^@]+@[^@]+\.[^@]+$")
 
results = validator.validate()
assert results.success, "Schema validation FAILED — pipeline halted."
python
03 & 04 — PREPARATION & PREPROCESSING

Preparation & Preprocessing

Preparation is structural organisation; preprocessing is numerical transformation. Together they convert raw tables into a feature matrix a model can learn from. The mathematical operations here directly control what the model can and cannot learn.

Preparation: Sorting and Arranging

At this stage you perform schema alignment (unifying column names and types across sources), join strategy selection (star vs. snowflake schemas), and train/val/test split design. The order matters — you must design splits before any preprocessing that uses statistics from the data (like mean imputation), otherwise you leak test-set statistics into your training pipeline.

⚠️
Data Leakage — The Most Common Pipeline Bug

Never compute scaling parameters, imputation values, or encoding mappings using the entire dataset before splitting. Always fit transformations on training data only, then apply (transform) to validation and test. Using a Pipeline object in scikit-learn enforces this automatically.

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.compose import ColumnTransformer
from sklearn.model_selection import train_test_split
 
# Split FIRST
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)
 
numeric_pipe = Pipeline([
    ("imputer", SimpleImputer(strategy="median")),
    ("scaler", StandardScaler())
])
 
cat_pipe = Pipeline([
    ("imputer", SimpleImputer(strategy="most_frequent")),
    ("encoder", OneHotEncoder(handle_unknown="ignore", sparse_output=False))
])
 
preprocessor = ColumnTransformer([
    ("num", numeric_pipe, numeric_cols),
    ("cat", cat_pipe, categorical_cols)
])
 
# FIT on train only — TRANSFORM both
preprocessor.fit(X_train)
X_train_processed = preprocessor.transform(X_train)
X_test_processed  = preprocessor.transform(X_test)
python

Core Preprocessing Operations with Mathematical Intuition

1. Feature Scaling

Neural networks, SVMs, and K-Means are scale-sensitive. Tree-based models (XGBoost, Random Forest) are scale-invariant. Always understand your algorithm before scaling.

Z-Score Normalisation (StandardScaler)
x' = (x − μ) / σ
where μ = mean, σ = standard deviation
Result: zero mean, unit variance. Good for Gaussian-distributed data.

Min-Max Scaling
x' = (x − x_min) / (x_max − x_min) → result ∈ [0, 1]
Sensitive to outliers. Use RobustScaler (IQR-based) if outliers present.

RobustScaler (IQR)
x' = (x − Q2) / (Q3 − Q1)
Q2 = median, Q1/Q3 = 25th/75th percentiles. Outlier-resistant.

2. Missing Value Strategies

MechanismDefinitionBest Strategy
MCARMissing Completely At RandomMean/median imputation, row deletion
MARMissing At Random (conditional on other cols)KNN imputation, iterative imputer (MICE)
MNARMissing Not At Random (value depends on itself)Add missingness indicator flag + model-based imputation
MICE — Multiple Imputation by Chained Equations
For each feature j with missing values:
X_j = f(X_{-j}, θ_j) where X_{-j} are all other features
Fit a model for each feature, iteratively impute until convergence.
In sklearn: IterativeImputer (experimental) with BayesianRidge estimator.
from sklearn.experimental import enable_iterative_imputer  # noqa
from sklearn.impute import IterativeImputer
from sklearn.linear_model import BayesianRidge
import numpy as np
 
imputer = IterativeImputer(
    estimator=BayesianRidge(),
    n_nearest_features=5,      # use 5 most correlated features
    max_iter=10,
    random_state=42,
    tol=1e-3
)
X_imputed = imputer.fit_transform(X_train)  # fit on train only!
python

3. Encoding Categorical Variables

Target Encoding (Mean Encoding)
encode(x_i) = (Σ y_j for all j where X_j = x_i) / count(x_i = x_i)
Risk: target leakage on training set. Use cross-fold target encoding to prevent this.

Ordinal Hashing (Feature Hashing)
h(x) = hash(x) mod 2^b → sparse matrix of width 2^b
Useful for very high-cardinality categoricals (e.g., user IDs with millions of values).

4. Feature Engineering: Interaction Terms & Polynomial Features

Polynomial Feature Expansion
For features [x₁, x₂] with degree=2:
Output: [1, x₁, x₂, x₁², x₁x₂, x₂²]
Number of features: C(n + d, d) where n = original features, d = degree
Risk: combinatorial explosion. Use SelectFromModel (Lasso) to prune.

5. Dimensionality Reduction

Principal Component Analysis (PCA)
Decompose data matrix X (n×p) as: X = UΣVᵀ (SVD)
Principal components: Z = XV (project onto eigenvectors)
Explained variance ratio: λ_k / Σλ_i where λ are eigenvalues of XᵀX
Rule of thumb: retain components explaining 95% of cumulative variance.

t-SNE (Barnes-Hut)
Minimise KL(P || Q) where P is high-dim joint distribution, Q is low-dim
P(j|i) = exp(−‖x_i − x_j‖² / 2σ_i²) / Σ_{k≠i} exp(−‖x_i − x_k‖² / 2σ_i²)
Perplexity (5–50) controls effective neighbourhood size. Not for >50k samples.
import numpy as np
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
 
# Determine optimal n_components
pca_full = PCA().fit(X_train_scaled)
cumvar = np.cumsum(pca_full.explained_variance_ratio_)
n_components = np.argmax(cumvar >= 0.95) + 1
print(f"Components for 95% variance: {n_components}")
 
pca = PCA(n_components=n_components, random_state=42)
X_reduced = pca.fit_transform(X_train_scaled)
python
05 & 06 — CLEANING & MANIPULATION

Cleaning & Manipulation

Data cleaning removes noise; manipulation reshapes the signal. Both require decisions guided by domain knowledge, statistical tests, and an understanding of downstream model sensitivity.

Outlier Detection Methods

Z-Score Method
outlier if |z_i| > 3 where z_i = (x_i − μ) / σ
Assumes Gaussian distribution. Fails for heavy-tailed distributions.

IQR Fence Method (Tukey)
Lower fence = Q1 − 1.5 × IQR
Upper fence = Q3 + 1.5 × IQR
where IQR = Q3 − Q1 (25th to 75th percentile range)

Isolation Forest
Anomaly score = 2^(−E[h(x)] / c(n))
where h(x) = path length to isolate x, c(n) = 2H(n-1) − 2(n-1)/n (expected path length)
Scores near 1 indicate anomalies; scores near 0.5 indicate normal points.
import pandas as pd
import numpy as np
from sklearn.ensemble import IsolationForest
 
def comprehensive_outlier_report(df: pd.DataFrame, numeric_cols: list) -> pd.DataFrame:
    report = pd.DataFrame(index=numeric_cols)
    # Z-Score outliers
    z = (df[numeric_cols] - df[numeric_cols].mean()) / df[numeric_cols].std()
    report["zscore_outliers"] = (z.abs() > 3).sum()
    # IQR outliers
    Q1 = df[numeric_cols].quantile(0.25)
    Q3 = df[numeric_cols].quantile(0.75)
    IQR = Q3 - Q1
    iqr_mask = (df[numeric_cols] < (Q1 - 1.5*IQR)) | (df[numeric_cols] > (Q3 + 1.5*IQR))
    report["iqr_outliers"] = iqr_mask.sum()
    return report
 
# Isolation Forest for multivariate anomaly detection
iso = IsolationForest(contamination=0.05, random_state=42, n_jobs=-1)
outlier_labels = iso.fit_predict(X_numeric)  # -1 = anomaly, 1 = normal
df_clean = df[outlier_labels == 1]
print(f"Removed {(outlier_labels == -1).sum()} anomalies ({(outlier_labels == -1).mean():.1%})")
python

Duplicate Detection

# Exact duplicates
exact_dupes = df.duplicated(keep="first")
df = df[~exact_dupes]
 
# Fuzzy deduplication (near-duplicates in text)
from datasketch import MinHash, MinHashLSH
 
def minhash_dedup(texts: list, threshold: float = 0.8) -> list:
    """MinHash LSH for approximate deduplication — O(n) vs O(n²)"""
    lsh = MinHashLSH(threshold=threshold, num_perm=128)
    minhashes = {}
    keep_indices = []
 
    for i, text in enumerate(texts):
        m = MinHash(num_perm=128)
        for word in text.lower().split():
            m.update(word.encode("utf8"))
        if not lsh.query(m):  # no similar doc found
            lsh.insert(f"doc_{i}", m)
            keep_indices.append(i)
    return keep_indices
python

Data Manipulation Best Practices

Pandas — Production Patterns

import pandas as pd
import numpy as np
 
# Use vectorised operations — NEVER iterate rows
df["revenue_log"] = np.log1p(df["revenue"])  # log(1+x) handles zeros
 
# Downcasting dtypes saves 60-80% memory
def reduce_memory(df):
    for col in df.select_dtypes(include=["float64"]).columns:
        df[col] = pd.to_numeric(df[col], downcast="float")
    for col in df.select_dtypes(include=["int64"]).columns:
        df[col] = pd.to_numeric(df[col], downcast="integer")
    for col in df.select_dtypes(include=["object"]).columns:
        if df[col].nunique() / len(df) < 0.05:  # <5% unique = categorical
            df[col] = df[col].astype("category")
    return df
 
# Window functions for time-series features
df = df.sort_values(["user_id", "timestamp"])
df["rolling_7d_avg"] = (
    df.groupby("user_id")["value"]
    .transform(lambda x: x.rolling(7, min_periods=1).mean())
)
 
# Efficient merge strategy
df_merged = df_left.merge(
    df_right,
    on="id",
    how="left",
    validate="m:1"      # asserts join cardinality — catches duplicates
)
python

SQL Manipulation (PostgreSQL / DuckDB)

-- Window function: rolling 30-day revenue per user
SELECT
    user_id,
    event_date,
    revenue,
    SUM(revenue) OVER (
        PARTITION BY user_id
        ORDER BY event_date
        ROWS BETWEEN 29 PRECEDING AND CURRENT ROW
    ) AS rolling_30d_revenue,
 
    -- Percentile rank within cohort
    PERCENT_RANK() OVER (PARTITION BY cohort_month ORDER BY revenue) AS pct_rank,
 
    -- Lag features for churn modelling
    LAG(revenue, 1) OVER (PARTITION BY user_id ORDER BY event_date) AS prev_revenue,
    revenue - LAG(revenue, 1) OVER (PARTITION BY user_id ORDER BY event_date) AS mom_delta
FROM transactions;
sql
07 — ETHICS

Data Ethics

Data ethics is not a soft concern — it is a technical, legal, and reputational risk layer. Bias baked into training data propagates through every model prediction at scale.

The Ethics Checklist (Before Any Dataset Publication)

Bias Detection — Mathematical Framework

Disparate Impact (80% Rule — EEOC)
DI = P(Ŷ=1 | A=minority) / P(Ŷ=1 | A=majority)
DI < 0.8 → potential illegal discrimination

Equalised Odds
TPR(A=0) = TPR(A=1) AND FPR(A=0) = FPR(A=1)
Equal true-positive AND false-positive rates across protected groups.

Demographic Parity
P(Ŷ=1 | A=0) = P(Ŷ=1 | A=1)
Prediction rate is equal regardless of protected attribute.
from fairlearn.metrics import MetricFrame, selection_rate, true_positive_rate
from fairlearn.reductions import ExponentiatedGradient, DemographicParity
import pandas as pd
 
# Compute fairness metrics across protected groups
mf = MetricFrame(
    metrics={
        "selection_rate": selection_rate,
        "tpr": true_positive_rate,
    },
    y_true=y_test,
    y_pred=y_pred,
    sensitive_features=X_test["gender"]  # protected attribute
)
print(mf.by_group)
print("Disparate Impact:", mf.difference())
 
# Bias mitigation with Exponentiated Gradient
constraint = DemographicParity()
mitigator = ExponentiatedGradient(base_estimator, constraint)
mitigator.fit(X_train, y_train, sensitive_features=X_train["gender"])
y_pred_fair = mitigator.predict(X_test)
python

PII Detection & Anonymisation

from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine
 
analyzer = AnalyzerEngine()
anonymizer = AnonymizerEngine()
 
text = "John Smith's email is john@example.com and his phone is +1-555-0123"
results = analyzer.analyze(text=text, language="en")
anonymized = anonymizer.anonymize(text=text, analyzer_results=results)
print(anonymized.text)
# Output: "<PERSON>'s email is <EMAIL_ADDRESS> and phone is <PHONE_NUMBER>"
python
08 — GOVERNANCE

Data Governance & Law

Non-compliance with data sovereignty laws can result in fines up to 4% of global annual turnover (GDPR). As an ML engineer, you are legally responsible for the data your models ingest.

RegulationJurisdictionKey Articles/SectionsML-Specific Risk
GDPR EU / EEA Art. 5 (principles), Art. 9 (sensitive data), Art. 17 (right to erasure), Art. 22 (automated decisions) Profiling, automated credit/hiring decisions need human review. Right to explanation.
CCPA / CPRA California, USA §1798.100 (access), §1798.105 (deletion), §1798.120 (opt-out of sale) Training data purchased from data brokers may be non-compliant.
PDPB / DPDPA 2023 India Chapter II (processing), Chapter III (rights), Schedule I (consent) Cross-border transfer restrictions. Localisation requirements for sensitive data.
PIPL China Art. 28 (sensitive personal info), Art. 38 (cross-border transfer), Art. 55 (AI assessment) Mandatory impact assessment for automated decisions affecting individuals.
EU AI Act (2024) EU Art. 9 (risk management), Art. 10 (training data governance), Title III (high-risk AI) High-risk systems (hiring, credit, health) require extensive data documentation and audits.
HIPAA USA (healthcare) 45 CFR §164 (PHI handling), Safe Harbor de-identification 18 specific identifiers must be removed before ML training on health data.
FERPA USA (education) 20 U.S.C. §1232g Student records cannot be used for ML without explicit consent.

Data Governance Policy Framework

⚠️
EU AI Act — High-Risk Categories (Annex III)

Systems used in biometric identification, critical infrastructure, education, employment, credit scoring, insurance, law enforcement, and border control are classified as HIGH-RISK and require conformity assessments, data governance documentation, and human oversight provisions before deployment.

Data Classification Taxonomy

LevelDescriptionExamplesControls Required
RestrictedPII, PHI, financial credentialsSSN, medical records, passwordsEncryption at rest + transit, access logging, DLP
ConfidentialBusiness-sensitive non-PIIRevenue data, ML model weights, IPRole-based access, audit trails
InternalOperational dataLogs, metrics, employee dataAuthentication, least-privilege
PublicOpen dataOpen-source datasets, press releasesIntegrity checks only
09 — DEPENDENCIES

Data Dependencies & Security

Your data pipeline's attack surface includes third-party data sources, library dependencies, and the trained model artifacts themselves. Data poisoning, model inversion attacks, and supply chain compromise are real production threats.

Dependency Management

# pyproject.toml — pin dependencies with hash verification
[tool.poetry.dependencies]
python = "^3.11"
pandas = "~2.2"      # minor version locked
numpy = "~1.26"
scikit-learn = "~1.4"
torch = {version = "~2.2", extras = ["cuda12"]}
 
# Generate lock file with reproducible hashes
# poetry lock --no-update
# pip-compile --generate-hashes requirements.in
toml

Data Poisoning Defences

Attack TypeDescriptionDefence
Label FlippingAdversary corrupts labels in training setCertified Data Cleaning, SEVER algorithm
Backdoor/TrojanTrigger pattern inserted into training imagesNeural Cleanse, Spectral Signatures detection
Model InversionReconstruct training data from model outputsDifferential Privacy, output perturbation
Membership InferenceDetermine if sample was in training setDP-SGD training, prediction confidence limiting
Supply ChainMalicious public dataset (e.g., poisoned LAION subset)Hash-verify source, sandboxed ingestion

Differential Privacy for ML

ε-Differential Privacy (DP)
A mechanism M is ε-DP if for any adjacent datasets D, D' and any output S:
P[M(D) ∈ S] ≤ e^ε × P[M(D') ∈ S]

ε = privacy budget: smaller = more private. Typical values: 0.1 (strong) to 10 (weak).

Gaussian Mechanism (for DP-SGD)
M(x) = f(x) + N(0, σ²Δf²) where Δf = sensitivity of f, σ = noise multiplier
Clip gradients: g̃ = g / max(1, ‖g‖₂/C) then add noise σ·C·N(0,I)
from opacus import PrivacyEngine
import torch
 
privacy_engine = PrivacyEngine()
model, optimizer, data_loader = privacy_engine.make_private_with_epsilon(
    module=model,
    optimizer=optimizer,
    data_loader=data_loader,
    epochs=50,
    target_epsilon=1.0,   # strong privacy
    target_delta=1e-5,
    max_grad_norm=1.0
)
print(f"σ = {optimizer.noise_multiplier:.3f}")
python
10 — DISTRIBUTED

Distributed Processing

When data doesn't fit in memory — or when processing must complete in minutes rather than hours — distributed computing is the answer. Memory management, garbage collection, and parallel I/O become first-class concerns.

Memory Management Fundamentals

Memory Estimation for a Dataset
Memory (bytes) ≈ rows × cols × bytes_per_dtype
float64: 8 bytes/element | float32: 4 | int8: 1 | bool: 1
Example: 100M rows × 50 float32 cols = 100M × 50 × 4 = 20 GB

Chunk Processing
For a file of size F with available memory M_avail:
chunk_size = floor(0.3 × M_avail / bytes_per_row) rows
Use 30% of available memory to leave room for transformations.
import pandas as pd
import numpy as np
from concurrent.futures import ProcessPoolExecutor
import psutil, gc
 
def process_chunk(chunk: pd.DataFrame) -> pd.DataFrame:
    """Pure function — safe for multiprocessing"""
    chunk = chunk.dropna(subset=["value"])
    chunk["log_value"] = np.log1p(chunk["value"])
    return chunk
 
def stream_process_csv(filepath: str, chunksize: int = 100_000) -> pd.DataFrame:
    """
    Memory-safe streaming CSV processing.
    Processes in chunks, collects results, forces GC between chunks.
    """
    results = []
    mem = psutil.virtual_memory()
    safe_chunk = int((mem.available * 0.3) / (50 * 8))  # estimate bytes per row
 
    for i, chunk in enumerate(pd.read_csv(filepath, chunksize=safe_chunk)):
        processed = process_chunk(chunk)
        results.append(processed)
        del chunk, processed
        gc.collect()  # explicit GC trigger
        if i % 10 == 0:
            print(f"Processed {i * safe_chunk:,} rows | "
                  f"Memory: {psutil.virtual_memory().percent:.1f}%")
 
    return pd.concat(results, ignore_index=True)
python

Apache Spark — DataFrame API Best Practices

from pyspark.sql import SparkSession
from pyspark.sql import functions as F
from pyspark.sql.window import Window
from pyspark.ml.feature import StandardScaler, VectorAssembler
 
spark = (SparkSession.builder
    .appName("FeatureEngineering")
    .config("spark.sql.adaptive.enabled", "true")     # AQE for skew handling
    .config("spark.sql.shuffle.partitions", "200")
    .config("spark.memory.offHeap.enabled", "true")
    .config("spark.memory.offHeap.size", "4g")
    .getOrCreate())
 
df = spark.read.parquet("s3://bucket/data/*.parquet")
 
# Partition pruning — predicate pushed to storage layer
df_filtered = df.filter((F.col("year") == 2024) & (F.col("region") == "APAC"))
 
# Window aggregation — distributed version of Pandas groupby
w = Window.partitionBy("user_id").orderBy("ts").rowsBetween(-29, 0)
df_feat = df_filtered.withColumn(
    "rolling_30d", F.sum("revenue").over(w)
)
 
# Cache hot DataFrames — only if reused multiple times
df_feat.cache().count()  # trigger materialisation
 
# Write back to Delta Lake (ACID)
df_feat.write.format("delta").mode("overwrite").save("s3://bucket/features/")
python

Kafka — Streaming Data Pipeline

from confluent_kafka import Consumer, Producer
import json
 
# Producer: ingest raw events
producer = Producer({"bootstrap.servers": "kafka:9092"})
 
def delivery_report(err, msg):
    if err:
        print(f"Delivery failed: {err}")
 
def publish_event(topic: str, key: str, value: dict):
    producer.produce(
        topic, key=key.encode(),
        value=json.dumps(value).encode(),
        callback=delivery_report
    )
    producer.poll(0)  # non-blocking flush
 
# Consumer: feature extraction from stream
consumer = Consumer({
    "bootstrap.servers": "kafka:9092",
    "group.id": "feature-pipeline",
    "auto.offset.reset": "earliest",
    "enable.auto.commit": False    # manual commit = at-least-once
})
consumer.subscribe(["raw-events"])
python
11 — ERROR HANDLING

Error Handling & Logging

Production data pipelines fail silently and expensively. Robust error handling means the difference between a debugging session and an undetected data quality incident that corrupts a model in production.

Structured Logging Pattern

import structlog, logging, sys
from functools import wraps
 
# Configure structured JSON logging
structlog.configure(
    processors=[
        structlog.contextvars.merge_contextvars,
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.add_log_level,
        structlog.processors.StackInfoRenderer(),
        structlog.processors.JSONRenderer()
    ],
    wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
    logger_factory=structlog.PrintLoggerFactory()
)
 
logger = structlog.get_logger()
 
def pipeline_step(step_name: str):
    """Decorator: auto-log entry/exit/error for pipeline stages."""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            log = logger.bind(step=step_name, args_len=len(args))
            log.info("step.start")
            try:
                result = func(*args, **kwargs)
                log.info("step.success")
                return result
            except ValueError as e:
                log.error("step.validation_error", error=str(e), exc_info=True)
                raise
            except Exception as e:
                log.critical("step.fatal_error", error=str(e), exc_info=True)
                raise
        return wrapper
    return decorator
 
@pipeline_step("feature_normalisation")
def normalise_features(df):
    if df.empty:
        raise ValueError("Input DataFrame is empty")
    return (df - df.mean()) / df.std()
python

Data Quality Monitoring with Evidently

from evidently.report import Report
from evidently.metric_preset import DataDriftPreset, DataQualityPreset
 
report = Report(metrics=[
    DataDriftPreset(),
    DataQualityPreset()
])
report.run(reference_data=df_train, current_data=df_production)
report.save_html("drift_report.html")
 
# Raise alert if drift detected
drift_result = report.as_dict()["metrics"][0]["result"]
if drift_result["dataset_drift"]:
    raise RuntimeError(f"Data drift detected: {drift_result['share_of_drifted_columns']:.0%} columns drifted")
python
12 — TOOLS

Core Tools Deep Dive

These five libraries form the computational backbone of modern ML data pipelines. Knowing not just their APIs but their performance characteristics and when to choose one over another is what separates seniors from juniors.

NumPy — The Foundation

Broadcasting Rules
Two arrays are compatible if for each dimension pair:
(a) they are equal, OR (b) one of them is 1
Shape (3,1) + (1,4) → broadcasts to (3,4)
Shape (3,) + (4,3) → error! Axes must align from the right.
import numpy as np
 
# Einstein summation — express any tensor contraction
A = np.random.randn(100, 50)   # batch_size × features
B = np.random.randn(50, 30)   # features × hidden
C = np.einsum("bi,ih->bh", A, B)  # equivalent to A @ B
 
# Vectorised cosine similarity matrix (no loops)
def cosine_similarity_matrix(X: np.ndarray) -> np.ndarray:
    norms = np.linalg.norm(X, axis=1, keepdims=True)  # (n,1)
    X_norm = X / (norms + 1e-8)                            # broadcast
    return X_norm @ X_norm.T                                # (n,n)
 
# Memory-mapped arrays for out-of-core processing
mmap = np.memmap("large_array.npy", dtype=np.float32,
                 mode="r", shape=(10_000_000, 128))
batch = mmap[0:1000].copy()  # loads only this slice into RAM
python

Pandas — Advanced Patterns

import pandas as pd
 
# Method chaining — readable, pipe-based transformations
result = (
    pd.read_parquet("events.parquet")
    .pipe(reduce_memory)
    .query("event_type == 'purchase' and amount > 0")
    .assign(
        log_amount = lambda df: np.log1p(df["amount"]),
        hour_of_day = lambda df: df["timestamp"].dt.hour
    )
    .groupby(["user_id", pd.Grouper(key="timestamp", freq="1D")])
    .agg(daily_revenue=("amount", "sum"), n_purchases=("amount", "count"))
    .reset_index()
)
python

JAX — For High-Performance ML Research

JAX Transforms (Composable Functional Transformations)
jit(f) → XLA-compiled version of f
grad(f) → gradient function ∂f/∂x
vmap(f) → vectorised map (batching over a new axis)
pmap(f) → parallel map across devices (GPUs/TPUs)
These compose: grad(jit(f)), vmap(grad(f)), etc.
import jax.numpy as jnp
from jax import grad, jit, vmap, random
 
# Automatic differentiation through any computation
def mse_loss(params, X, y):
    predictions = jnp.dot(X, params["w"]) + params["b"]
    return jnp.mean((predictions - y) ** 2)
 
grad_fn = jit(grad(mse_loss))  # compiled gradient function
grads = grad_fn(params, X_batch, y_batch)  # {w: dL/dw, b: dL/db}
 
# vmap: apply single-sample function to a batch (no for-loop!)
def predict_single(params, x):
    return jnp.dot(x, params)
 
predict_batch = vmap(predict_single, in_axes=(None, 0))  # params fixed, x batched
python

PyTorch — Production Data Loading

import torch
from torch.utils.data import Dataset, DataLoader
from torch.utils.data.distributed import DistributedSampler
 
class TabularDataset(Dataset):
    def __init__(self, X: np.ndarray, y: np.ndarray):
        self.X = torch.tensor(X, dtype=torch.float32)
        self.y = torch.tensor(y, dtype=torch.long)
 
    def __len__(self): return len(self.X)
    def __getitem__(self, idx): return self.X[idx], self.y[idx]
 
# Production DataLoader with pinned memory for GPU transfer
loader = DataLoader(
    TabularDataset(X_train, y_train),
    batch_size=512,
    num_workers=4,        # parallel data loading workers
    pin_memory=True,       # faster CPU→GPU transfer
    persistent_workers=True, # avoid re-spawning workers each epoch
    prefetch_factor=2,    # pre-fetch 2 batches ahead
    sampler=DistributedSampler(dataset) # for multi-GPU DDP
)
python

TensorFlow / Keras — Production Data Pipelines

TensorFlow's tf.data API is the gold standard for building GPU-saturating input pipelines. The key is to overlap data loading (I/O-bound) with model execution (compute-bound) using prefetching and parallel map.

tf.data Performance Model
Pipeline throughput = min(data_throughput, compute_throughput)
With prefetch(AUTOTUNE): data_throughput overlaps with compute_throughput
Without prefetch: total_time = Σ(data_time_i + compute_time_i) [sequential]
With prefetch: total_time ≈ max(Σ data_time_i, Σ compute_time_i) [pipelined]
import tensorflow as tf
import numpy as np

# Production tf.data pipeline — saturate GPU with parallel I/O
def build_training_pipeline(
    file_pattern: str,
    batch_size: int = 512,
    num_parallel_reads: int = 8,
    shuffle_buffer: int = 10_000
) -> tf.data.Dataset:
    """High-performance TFRecord pipeline with:
    - Parallel file interleaving
    - Prefetch with AUTOTUNE
    - Cached in-memory after first epoch
    """
    feature_spec = {
        "features": tf.io.FixedLenFeature([128], tf.float32),
        "label": tf.io.FixedLenFeature([], tf.int64),
    }

    def parse_example(serialized):
        parsed = tf.io.parse_single_example(serialized, feature_spec)
        return parsed["features"], parsed["label"]

    files = tf.data.Dataset.list_files(file_pattern, shuffle=True)

    ds = files.interleave(
        lambda f: tf.data.TFRecordDataset(f, compression_type="GZIP"),
        num_parallel_calls=tf.data.AUTOTUNE,
        cycle_length=num_parallel_reads,
        deterministic=False  # non-deterministic for speed
    )

    ds = (
        ds
        .shuffle(shuffle_buffer)
        .map(parse_example, num_parallel_calls=tf.data.AUTOTUNE)
        .batch(batch_size, drop_remainder=True)  # drop_remainder for TPU
        .cache()             # cache after first epoch (fits in RAM)
        .prefetch(tf.data.AUTOTUNE)  # overlap data prep with training
    )
    return ds

# Keras model with mixed precision (2× speed on modern GPUs)
tf.keras.mixed_precision.set_global_policy("mixed_float16")

model = tf.keras.Sequential([
    tf.keras.layers.Dense(256, activation="relu", input_shape=(128,)),
    tf.keras.layers.BatchNormalization(),
    tf.keras.layers.Dropout(0.3),
    tf.keras.layers.Dense(64, activation="relu"),
    tf.keras.layers.Dense(1, activation="sigmoid", dtype="float32")
])

# Compile with TF's built-in AUC metric for imbalanced data
model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),
    loss="binary_crossentropy",
    metrics=[tf.keras.metrics.AUC(name="auroc")]
)

train_ds = build_training_pipeline("gs://bucket/train/*.tfrecord")
model.fit(train_ds, epochs=50, callbacks=[
    tf.keras.callbacks.EarlyStopping(monitor="val_auroc", patience=5, mode="max"),
    tf.keras.callbacks.ModelCheckpoint("best_model.keras", save_best_only=True)
])

# Export to SavedModel for TF Serving / TFLite
model.export("saved_model/fraud_detector")
python
🔧
TensorFlow vs PyTorch — When to Choose TF

Choose TensorFlow when: (1) deploying via TF Serving, TFLite, or TF.js, (2) training on Google TPUs (native TPU Strategy), (3) using Vertex AI or GCP ML stack, (4) your team has existing TF infrastructure. PyTorch has won the research ecosystem, but TF remains dominant in production serving at scale — especially at Google, DeepMind, and companies using GCP.

When to Use Which Tool

ScenarioBest ToolWhy
Statistical analysis, EDAPandas + NumPyRich API, Jupyter integration
ML research, custom gradientsJAX + Flax/OptaxComposable transforms, XLA JIT
Deep learning productionPyTorch + TorchScriptEcosystem, ONNX export, deployment
TF Serving / TPU trainingTensorFlow / KerasBest for Google Cloud, TPU support
Large matrix ops, PCA, SVDNumPy / JAXBroadcasting, einsum, linalg
Petabyte-scale feature eng.PySpark + Delta LakeDistributed, ACID, versioned
13 — WEB SCRAPING

Web Scraping

Web scraping is a last resort for data collection — always prefer APIs and open datasets. When scraping is necessary, these tools handle modern JavaScript-heavy sites, bot detection, and scale.

⚖️
Legal Prerequisites Before Scraping

Check robots.txt. Review the site's Terms of Service. Scraping content protected by copyright without licence is legally risky (see hiQ v. LinkedIn; Meta v. Bright Data). Never scrape PII without explicit legal basis. Rate-limit your requests to avoid DoS liability.

Crawlee (Node.js) — Enterprise Scraping

// Crawlee with Playwright for JS-heavy sites
import { PlaywrightCrawler, Dataset } from 'crawlee';
 
const crawler = new PlaywrightCrawler({
    maxRequestsPerCrawl: 1000,
    maxConcurrency: 5,
    requestHandlerTimeoutSecs: 30,
 
    async requestHandler({ page, request, enqueueLinks }) {
        // Wait for dynamic content
        await page.waitForSelector('.product-card', { timeout: 10000 });
 
        const items = await page.$$eval('.product-card', cards =>
            cards.map(c => ({
                title: c.querySelector('h2')?.textContent,
                price: c.querySelector('.price')?.textContent,
                url: c.querySelector('a')?.href
            }))
        );
        await Dataset.pushData(items);
        await enqueueLinks({ selector: 'a.next-page' });
    },
    failedRequestHandler: ({ request }) =>
        console.error(`Failed: ${request.url}`)
});
 
await crawler.run(['https://example.com/products']);
javascript

Crawl4AI — LLM-Powered Extraction (Python)

from crawl4ai import AsyncWebCrawler
from crawl4ai.extraction_strategy import LLMExtractionStrategy
import asyncio, json
 
async def extract_structured_data(url: str):
    strategy = LLMExtractionStrategy(
        provider="openai/gpt-4o-mini",
        api_token="YOUR_KEY",
        schema={
            "type": "object",
            "properties": {
                "company_name": {"type": "string"},
                "founding_year": {"type": "integer"},
                "revenue": {"type": "number"}
            }
        },
        instruction="Extract company details from the page."
    )
    async with AsyncWebCrawler(verbose=True) as crawler:
        result = await crawler.arun(url=url, extraction_strategy=strategy)
        return json.loads(result.extracted_content)
 
asyncio.run(extract_structured_data("https://example.com/about"))
python

Puppeteer — Headless Browser Scraping (Node.js)

// Puppeteer with stealth plugin — handles SPAs and bot detection
import puppeteer from 'puppeteer-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
import fs from 'fs/promises';

puppeteer.use(StealthPlugin());

async function scrapeProducts(url) {
    const browser = await puppeteer.launch({
        headless: 'new',        // new headless mode (Chrome 112+)
        args: [
            '--no-sandbox',
            '--disable-setuid-sandbox',
            '--disable-dev-shm-usage'   // for Docker
        ]
    });

    const page = await browser.newPage();
    await page.setViewport({ width: 1280, height: 800 });

    // Navigate and wait for dynamic content
    await page.goto(url, { waitUntil: 'networkidle0', timeout: 30000 });

    // Auto-scroll to trigger lazy-loaded content
    await page.evaluate(async () => {
        await new Promise(resolve => {
            let totalHeight = 0;
            const distance = 300;
            const timer = setInterval(() => {
                window.scrollBy(0, distance);
                totalHeight += distance;
                if (totalHeight >= document.body.scrollHeight) {
                    clearInterval(timer);
                    resolve();
                }
            }, 200);
        });
    });

    // Extract structured data
    const products = await page.$$eval('.product-item', items =>
        items.map(el => ({
            name: el.querySelector('h3')?.textContent?.trim(),
            price: parseFloat(el.querySelector('.price')?.textContent?.replace(/[^0-9.]/g, '')),
            rating: parseFloat(el.querySelector('[data-rating]')?.getAttribute('data-rating')),
            url: el.querySelector('a')?.href
        }))
    );

    await fs.writeFile('products.json', JSON.stringify(products, null, 2));
    console.log(`Scraped ${products.length} products`);
    await browser.close();
    return products;
}

scrapeProducts('https://example.com/shop');
javascript
🆚
Puppeteer vs Playwright vs Crawlee

Puppeteer: Chrome/Chromium only, lightweight, excellent for single-browser tasks and Google ecosystem. Playwright: Multi-browser (Chrome, Firefox, WebKit), auto-wait, better for cross-browser testing and complex flows. Crawlee: Built on Playwright/Puppeteer, adds queue management, auto-scaling, proxy rotation — best for large-scale production crawling. Choose Puppeteer for simplicity, Playwright for robustness, Crawlee for scale.

Playwright — Anti-Bot Techniques

from playwright.async_api import async_playwright
import asyncio, random
 
async def stealth_scrape(url: str):
    async with async_playwright() as p:
        browser = await p.chromium.launch(
            headless=True,
            args=['--disable-blink-features=AutomationControlled']
        )
        context = await browser.new_context(
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64)...",
            viewport={"width": 1280, "height": 800},
            locale="en-US"
        )
        # Mask navigator.webdriver fingerprint
        await context.add_init_script(
            "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
        )
        page = await context.new_page()
        await page.goto(url, wait_until="networkidle")
        # Human-like delay
        await asyncio.sleep(random.uniform(1.5, 3.5))
        content = await page.content()
        await browser.close()
        return content
python
14 — SYNTHETIC DATA

Synthetic Data & GenAI

Synthetic data generation bridges the gap between limited labelled data and model training requirements. When used correctly it preserves statistical properties while removing privacy risks.

ToolTypeBest ForLicence
SDV (Synthetic Data Vault)Statistical / DLTabular, relational, time-seriesBSL (free tiers)
CTGAN / TVAEGAN / VAETabular with mixed typesMIT
Gretel.aiDGAN, ActganPrivacy-safe enterprise dataSaaS
FakerRule-basedPII generation, testingMIT
DiffPrivLibDP mechanismsPrivacy-preserving statisticsMIT
MimesisRule-basedMulti-locale fake dataMIT
Augly (Meta)AugmentationText, image, video augmentationMIT
AlbumentationsImage augmentationCV training data expansionMIT
Claude / GPT-4o APILLM generationNLP datasets, instruction tuningAPI
CTGAN — Conditional GAN for Tabular Data
Generator G(z, c) → synthetic row | Discriminator D(x, c) → real/fake
Objective: min_G max_D E[log D(x,c)] + E[log(1 − D(G(z,c), c))]
Mode-specific normalisation: each numeric column modelled as mixture of Gaussians
Conditional vector c handles class imbalance by sampling under-represented classes.
from sdv.single_table import CTGANSynthesizer
from sdv.metadata import SingleTableMetadata
from sdv.evaluation.single_table import run_diagnostic, evaluate_quality
 
metadata = SingleTableMetadata()
metadata.detect_from_dataframe(df_real)
metadata.update_column("user_id", sdtype="id")        # mark as ID, not feature
metadata.update_column("churn", sdtype="categorical")  # target column
 
synthesizer = CTGANSynthesizer(
    metadata,
    epochs=300,
    batch_size=500,
    discriminator_steps=1,
    verbose=True
)
synthesizer.fit(df_real)
df_synthetic = synthesizer.sample(num_rows=50_000)
 
# Evaluate quality: column shapes + correlation
quality = evaluate_quality(df_real, df_synthetic, metadata)
print(f"Quality Score: {quality.get_score():.2f}")  # aim for >0.85
python

LLM-Powered Synthetic NLP Dataset Pipeline

import anthropic, json
from typing import Iterator
 
client = anthropic.Anthropic()
 
def generate_synthetic_samples(
    task: str, labels: list, n_per_label: int = 100
) -> Iterator[dict]:
    """Generate labelled NLP training samples via Claude."""
    for label in labels:
        prompt = f"""Generate {n_per_label} diverse text samples for a {task} classifier.
Label: {label}
Requirements:
- Vary writing style (formal, casual, terse, verbose)
- Vary sentiment polarity where applicable
- Avoid exact duplicates
Return ONLY a JSON array of strings, no extra text."""
        msg = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=4096,
            messages=[{"role": "user", "content": prompt}]
        )
        samples = json.loads(msg.content[0].text)
        for text in samples:
            yield {"text": text, "label": label}
python
15 — DATABASES

Database Selection 2026

You listed 7 databases. The truth is: a senior ML engineer in 2026 needs at most 3 database types in their core stack. Here's the full landscape, and then the verdict.

DatabaseTypeML Use CaseStrengthsAvoid When
PostgreSQLOLTP / RelationalFeature metadata, experiment tracking, label storageACID, extensions (pgvector!), mature ecosystemAnalytical queries on billions of rows
DuckDBEmbedded OLAPLocal EDA, feature engineering, parquet/CSV analyticsBlazing fast columnar, runs in Python process, no serverMulti-user concurrent writes
CockroachDBNewSQL / DistributedGlobal feature stores needing strong consistencyDistributed ACID, Postgres-compatible, geo-partitioningPure analytics — too expensive for OLAP
MongoDBDocument / NoSQLSemi-structured data ingestion, raw event storageFlexible schema, Atlas Vector SearchTabular ML features — use a proper warehouse
ChromaDBVector DB (local)Embedding search, RAG prototyping, semantic dedupZero-ops, in-process, great for prototypesProduction at scale (>10M vectors)
PineconeVector DB (managed)Production RAG, semantic search, recommendationManaged, fast approximate NN, filteringCost-sensitive startups, offline/air-gap
Neo4jGraph DBFraud detection, knowledge graphs, GNNsCypher query language, GraphSAGE integration, APOCTabular data — significant overhead

The Verdict — ML Engineer Stack 2026

🥇 DuckDB
Primary: Local Analytics & EDA (replace Pandas for files >1GB)

DuckDB is the single most impactful addition to an ML workflow in 2024–2026. It runs inside your Python process, reads Parquet/CSV/Arrow directly, executes vectorised SQL at near-Spark speed on a laptop, integrates natively with Pandas and PyArrow, and has zero infrastructure overhead. For everything that fits on one machine, DuckDB should be your first choice.

🥈 PostgreSQL + pgvector
Operational: Structured data + vector search in one system

PostgreSQL with the pgvector extension handles both traditional relational data (experiment runs, model metadata, user features) AND vector similarity search. This eliminates a separate vector DB for most use cases under 1M vectors. Use with Supabase for zero-ops deployment.

🥉 Neo4j (Conditional)
Specialist: Only if your problem is fundamentally graph-shaped

If your ML problem involves fraud detection, knowledge graphs, recommendation via graph traversal, or GNNs — Neo4j is irreplaceable. Otherwise, skip it. MongoDB and CockroachDB are appropriate in specific scenarios (unstructured ingestion and distributed OLTP respectively) but are not core ML tools.

import duckdb
import pandas as pd
 
# DuckDB — the ML engineer's Swiss army knife
con = duckdb.connect("ml_features.duckdb")
 
# Read Parquet directly — no loading into memory first!
result = con.execute("""
    SELECT
        user_id,
        AVG(amount) FILTER (WHERE event_type='purchase') AS avg_purchase,
        COUNT(*) FILTER (WHERE event_type='click')       AS click_count,
        APPROX_QUANTILE(amount, 0.95)                    AS p95_amount
    FROM read_parquet('s3://bucket/events/*.parquet')
    WHERE YEAR(event_date) = 2024
    GROUP BY user_id
    HAVING COUNT(*) > 10
""").df()
 
# Write feature table back to parquet
con.execute("COPY result TO 'features.parquet' (FORMAT PARQUET)")
python
16 — APACHE STACK

Apache Ecosystem

These tools form the backbone of enterprise data engineering. Knowing when to reach for each — and when not to — is critical for system design interviews.

ToolRoleML Use Case2026 Status
Apache SparkDistributed computeFeature engineering at petabyte scale, Spark MLlib✅ Essential — use PySpark + Delta Lake
Apache KafkaEvent streamingReal-time feature computation, online serving, training data streams✅ Essential — combine with Flink for streaming ML
Apache AirflowWorkflow orchestrationML pipeline DAGs, retraining schedules, data quality checks✅ Still standard — Prefect/Dagster rising alternatives
SnowflakeCloud data warehouseSnowpark ML, feature stores, OLAP on structured data✅ Industry standard for enterprise analytics
Apache MahoutDistributed MLLegacy collaborative filtering, matrix factorisation⚠️ Declining — replaced by Spark MLlib + Horovod

Airflow DAG for ML Retraining Pipeline

from airflow import DAG
from airflow.operators.python import PythonOperator
from airflow.providers.apache.spark.operators.spark_submit import SparkSubmitOperator
from datetime import datetime, timedelta
 
default_args = {
    "owner": "ml-team",
    "retries": 3,
    "retry_delay": timedelta(minutes=5),
    "email_on_failure": True,
    "email": ["ml-alerts@company.com"]
}
 
with DAG(
    "weekly_churn_retrain",
    default_args=default_args,
    schedule_interval="0 2 * * 1",  # every Monday at 2am
    start_date=datetime(2024, 1, 1),
    catchup=False
) as dag:
 
    validate_data = PythonOperator(
        task_id="validate_input_data",
        python_callable=lambda: run_great_expectations_suite("churn_suite")
    )
 
    feature_eng = SparkSubmitOperator(
        task_id="compute_features",
        application="s3://scripts/feature_engineering.py",
        conf={"spark.executor.memory": "8g", "spark.executor.cores": "4"}
    )
 
    train_model = PythonOperator(
        task_id="train_xgboost",
        python_callable=train_and_log_to_mlflow
    )
 
    validate_model = PythonOperator(
        task_id="validate_model_metrics",
        python_callable=compare_against_champion
    )
 
    deploy = PythonOperator(
        task_id="deploy_to_sagemaker",
        python_callable=promote_challenger_to_champion
    )
 
    validate_data >> feature_eng >> train_model >> validate_model >> deploy
python
PROJECT 01

ETL / Warehousing / OLAP

01

E-Commerce Multi-Source Data Warehouse with Snowflake + Airflow

ETL pipeline ingesting 5 source systems into a star-schema warehouse, serving OLAP dashboards via dbt + Metabase

Covers Topics

ETL / ELTStar SchemaSlowly Changing DimensionsOLAP CubesData QualityAirflow DAGsdbt TransformationsPartitioning

Dataset

Combine the Brazilian E-Commerce (Olist) dataset from Kaggle (100k orders, 9 CSV files) with synthetic data from Faker representing web sessions and marketing touch-points. This simulates a real 5-source environment: orders DB, product catalogue, customer CRM, web analytics, and ad spend.

Architecture

Sources: PostgreSQL (orders) + MongoDB (sessions) + S3 (ad spend CSV)
         ↓
Ingestion: Airbyte connectors → S3 raw landing zone (JSON/CSV/Parquet)
         ↓
Transformation: dbt (Snowflake) — staging → intermediate → marts
         ↓
OLAP Layer: Snowflake + dbt metrics layer
         ↓
Serving: Metabase dashboards (Revenue, Cohort, Funnel KPIs)
architecture

Star Schema Design

-- Fact Table
CREATE TABLE fact_orders (
    order_key       BIGINT PRIMARY KEY,
    customer_key    BIGINT REFERENCES dim_customers,
    product_key     BIGINT REFERENCES dim_products,
    date_key        INT    REFERENCES dim_date,
    revenue         NUMERIC(12,2),
    quantity        INT,
    discount_pct    NUMERIC(5,2),
    shipping_days   INT
);
 
-- SCD Type 2 for customers (track historical changes)
CREATE TABLE dim_customers (
    customer_key    BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    customer_id     VARCHAR(50),         -- natural key
    city            VARCHAR(100),
    state           VARCHAR(10),
    customer_tier   VARCHAR(20),
    valid_from      DATE NOT NULL,
    valid_to        DATE,                 -- NULL = current record
    is_current      BOOLEAN DEFAULT TRUE
);
sql

Key dbt Transformation

-- models/marts/fct_orders_enriched.sql
WITH orders AS (
    SELECT * FROM {{ ref('stg_orders') }}
),
customers AS (
    SELECT * FROM {{ ref('dim_customers') }} WHERE is_current = TRUE
),
cohort_revenue AS (
    SELECT
        c.customer_id,
        DATE_TRUNC('month', MIN(o.order_date)) AS cohort_month,
        SUM(o.revenue) AS ltv_to_date
    FROM orders o
    JOIN customers c USING (customer_id)
    GROUP BY c.customer_id
)
SELECT
    o.*,
    cr.cohort_month,
    SUM(o.revenue) OVER (
        PARTITION BY o.customer_id ORDER BY o.order_date
        ROWS UNBOUNDED PRECEDING
    ) AS cumulative_ltv
FROM orders o
LEFT JOIN cohort_revenue cr USING (customer_id)
sql

Key Findings

Top-quartile customers (by LTV) account for 68% of revenue but only 12% of order volume. Cohort retention drops 40% after month 3 — a trigger for a re-engagement campaign feature. SCD Type 2 reveals 8% of customers changed tier within 6 months, a signal invisible in non-temporal designs.

Methodologies

ELT over ETL — push transformation to the warehouse (Snowflake handles scale). dbt enforces SQL-based transformation lineage. Great Expectations runs as Airflow tasks before each dbt model layer. Data contracts defined as YAML schemas in the repo.

Tools Used

DuckDB (local dev)Snowflake (prod)dbt CoreAirflow 2.8Great ExpectationsPython 3.11PandasMetabase

Discussion

SCD Type 2 adds storage overhead but is non-negotiable for ML — feature engineering must be point-in-time correct to avoid future leakage. For example, a churn model trained on a customer's current tier (not their tier at the time of the event) would be making predictions on data that didn't exist at decision time.

Conclusions

A well-designed star schema with SCD Type 2 dimensions reduces time-to-insight from days (ad-hoc SQL on raw tables) to minutes (indexed dimensional queries). The dbt lineage graph becomes your data dictionary, replacing tribal knowledge with machine-readable documentation.

PROJECT 02

EDA with Key Performance Metrics

02

Churn Prediction EDA Engine with Automated KPI Dashboard

End-to-end exploratory analysis of a telecom churn dataset with statistical hypothesis testing, feature importance ranking, and a live Streamlit KPI dashboard

Dataset

IBM Telco Customer Churn dataset (Kaggle) — 7,043 rows, 21 columns, 26.5% churn rate. Augment with synthetic records using CTGAN to reach 100k rows, simulating production scale.

Statistical EDA Framework

import pandas as pd
import numpy as np
from scipy import stats
import duckdb
 
# Load via DuckDB for speed
df = duckdb.query("SELECT * FROM 'telco.parquet'").df()
 
# Automated EDA report
def univariate_analysis(df, target="Churn"):
    report = {}
    for col in df.columns:
        if col == target: continue
        if df[col].dtype == "object":
            # Chi-square test for categorical features vs target
            ct = pd.crosstab(df[col], df[target])
            chi2, p, dof, _ = stats.chi2_contingency(ct)
            cramer_v = np.sqrt(chi2 / (len(df) * (min(ct.shape) - 1)))
            report[col] = {"test": "chi2", "p_value": p, "cramer_v": cramer_v}
        else:
            # Point-biserial correlation for numeric vs binary target
            corr, p = stats.pointbiserialr(df[target] == "Yes", df[col].fillna(0))
            # Mann-Whitney U test (non-parametric)
            churn_vals = df.loc[df[target]=="Yes", col].dropna()
            no_churn_vals = df.loc[df[target]=="No", col].dropna()
            _, p_mw = stats.mannwhitneyu(churn_vals, no_churn_vals, alternative="two-sided")
            report[col] = {"test": "biserial+MW", "p_value": p_mw, "correlation": corr}
    return pd.DataFrame(report).T.sort_values("p_value")
python

Key Performance Metrics Implemented

KPIFormulaBusiness Meaning
Churn RateChurned / Total customersMonthly retention health
Customer LTVARPU × (1/Churn Rate)Revenue per acquired customer
NRR (Net Revenue Retention)(Start MRR + Expansion − Churn) / Start MRRRevenue momentum (target >100%)
CAC Payback PeriodCAC / (ARPU × Gross Margin)Months to recover acquisition cost
Product Adoption ScoreFeatures Used / Total Features × FrequencyStickiness predictor for churn
-- SQL KPI computation (DuckDB / Snowflake)
WITH monthly_metrics AS (
    SELECT
        DATE_TRUNC('month', event_date) AS month,
        COUNT(DISTINCT customer_id) AS active_customers,
        SUM(revenue) AS mrr,
        COUNT(DISTINCT CASE WHEN churn_flag = 1 THEN customer_id END) AS churned
    FROM fact_subscriptions
    GROUP BY 1
)
SELECT
    month,
    mrr,
    churned::FLOAT / active_customers  AS churn_rate,
    mrr / NULLIF(active_customers, 0) AS arpu,
    -- LTV = ARPU / Churn Rate
    (mrr / NULLIF(active_customers, 0)) /
        NULLIF(churned::FLOAT / active_customers, 0) AS estimated_ltv
FROM monthly_metrics
ORDER BY month;
sql

Feature Importance — SHAP Analysis

import shap, xgboost as xgb
 
model = xgb.XGBClassifier(n_estimators=300, max_depth=6, use_label_encoder=False)
model.fit(X_train, y_train)
 
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_test)
 
# Global importance
shap.summary_plot(shap_values, X_test, plot_type="bar")
 
# Interaction effects
shap_interaction = explainer.shap_interaction_values(X_test)
print("Top interaction: tenure × monthly_charges")
python

Key Findings

SHAP analysis reveals tenure is the single strongest churn predictor — customers in their first 3 months are 3.8× more likely to churn. Month-to-month contracts contribute 45% of all churn volume despite representing 55% of contracts. Customers with Fibre Optic service and no TechSupport have a 41% churn rate vs 8% for those with support — a clear product intervention target.

Conclusions

An automated EDA framework with statistical hypothesis testing and SHAP explanations reduces the time from data to actionable insight by roughly 60% compared to manual notebook EDA. The Streamlit dashboard made the KPIs available to non-technical stakeholders, enabling product and marketing teams to act on churn signals in near-real-time.

PROJECT 03 — CAPSTONE

End-to-End ML Production System

03

Real-Time Fraud Detection ML System — From Raw Events to Serving

Full MLOps pipeline: Kafka ingestion → Spark feature engineering → model training → MLflow versioning → FastAPI serving → drift monitoring with Evidently

Covers Topics

Streaming ETLFeature StoreClass ImbalanceModel RegistryA/B ServingDrift DetectionDifferential PrivacyGraph Features (Neo4j)REST APIData Lineage

Dataset

IEEE-CIS Fraud Detection (Kaggle) — 590k transactions, 433 features, 3.5% fraud rate. Combined with PaySim synthetic dataset for the streaming simulation. Graph features derived from a Neo4j transaction graph (merchant–card–device relationships).

System Architecture

┌─────────────────────────────────────────────────────────┐
│  INGESTION LAYER                                        │
│  POS Terminals → Kafka (raw-transactions topic)         │
└───────────────────────────┬─────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────┐
│  STREAM PROCESSING (PySpark + Kafka Streams)            │
│  - Parse / validate schema                              │
│  - Compute rolling aggregates (1h, 24h, 7d windows)     │
│  - Join with merchant profile (Redis hot store)         │
│  - Write to Delta Lake feature table                    │
└───────────────────────────┬─────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────┐
│  FEATURE STORE (Feast + DuckDB offline / Redis online)  │
│  Point-in-time correct feature retrieval for training   │
└───────────────────────────┬─────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────┐
│  TRAINING PIPELINE (Airflow orchestrated)               │
│  LightGBM + SMOTE + Optuna HPO + MLflow logging        │
└───────────────────────────┬─────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────┐
│  SERVING (FastAPI + Triton Inference Server)            │
│  p99 latency < 15ms | 10k req/s throughput             │
└─────────────────────────────────────────────────────────┘
architecture

Handling Class Imbalance (3.5% Fraud)

SMOTE — Synthetic Minority Over-sampling
For each minority sample x_i, find k nearest neighbours.
Generate synthetic sample: x_new = x_i + λ(x_nn − x_i) where λ ∈ [0,1] random
Use SMOTE-Tomek Links to simultaneously over-sample minority and under-sample majority boundary noise.

Threshold Optimisation for Fraud
Business cost: False Negative (missed fraud) = $500 | False Positive (blocked legit) = $3
Optimal threshold τ* = argmin_τ [FN(τ)×500 + FP(τ)×3]
Use precision-recall curve, not ROC, for imbalanced problems.
from imblearn.combine import SMOTETomek
from imblearn.over_sampling import SMOTENC  # handles categoricals
import lightgbm as lgb
import optuna, mlflow
 
# Handle mixed types with SMOTENC
cat_idx = [df.columns.get_loc(c) for c in categorical_cols]
smt = SMOTETomek(smote=SMOTENC(categorical_features=cat_idx, k_neighbors=5))
X_res, y_res = smt.fit_resample(X_train, y_train)
 
def objective(trial):
    params = {
        "n_estimators": trial.suggest_int("n_estimators", 200, 1000),
        "learning_rate": trial.suggest_float("lr", 0.01, 0.3, log=True),
        "num_leaves": trial.suggest_int("leaves", 16, 256),
        "scale_pos_weight": 1  # SMOTE handles balance
    }
    model = lgb.LGBMClassifier(**params)
    model.fit(X_res, y_res, eval_set=[(X_val, y_val)],
              callbacks=[lgb.early_stopping(50, verbose=False)])
    # Optimise on AUPRC — better for imbalanced
    from sklearn.metrics import average_precision_score
    return average_precision_score(y_val, model.predict_proba(X_val)[:, 1])
 
study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=100, n_jobs=-1)
python

Graph Feature Extraction (Neo4j)

// Cypher: extract fraud ring features via graph paths
MATCH (card:Card)-[:USED_AT]->(merchant:Merchant)
WHERE card.id = $card_id
WITH card, COLLECT(merchant) AS merchants
MATCH (other_card:Card)-[:USED_AT]->(m:Merchant)
WHERE m IN merchants AND other_card <> card
WITH card,
     COUNT(DISTINCT other_card) AS shared_merchant_cards,
     COUNT(DISTINCT m) AS shared_merchants
RETURN card.id, shared_merchant_cards, shared_merchants,
       shared_merchant_cards::FLOAT / shared_merchants AS concentration_score
cypher

Model Serving — FastAPI

from fastapi import FastAPI
from pydantic import BaseModel
import mlflow.pyfunc, numpy as np, time
 
app = FastAPI()
model = mlflow.pyfunc.load_model("models:/fraud_detector/Production")
 
class TransactionFeatures(BaseModel):
    amount: float
    hour_of_day: int
    days_since_first_tx: int
    tx_count_1h: int
    tx_amount_24h: float
    shared_merchant_cards: int
    # ... other features
 
@app.post("/predict")
async def predict_fraud(tx: TransactionFeatures):
    start = time.perf_counter()
    features = np.array([[getattr(tx, f) for f in tx.model_fields]])
    fraud_prob = model.predict(features)[0]
    latency_ms = (time.perf_counter() - start) * 1000
    return {
        "fraud_probability": float(fraud_prob),
        "decision": "BLOCK" if fraud_prob > 0.42 else "ALLOW",
        "latency_ms": latency_ms
    }
python

Key Findings

Graph-derived features (shared merchant concentration score, device reuse rate) contributed 11 of the top 20 SHAP features — demonstrating that structural network information is orthogonal to transactional features. AUPRC improved from 0.71 (tabular only) to 0.84 (tabular + graph). The optimal decision threshold was 0.42, saving $2.1M in estimated fraud losses vs the default 0.5 threshold.

Conclusions

This capstone demonstrates that production ML is 80% data engineering and 20% modelling. The system handles 10k transactions/second with p99 latency under 15ms — achieved through Redis caching of graph features computed offline, Triton batching, and ONNX model export. The Airflow retraining DAG ensures the model is retrained weekly on fresh labelled data, preventing performance degradation from concept drift.

PROJECT 04

Real-Time ML Pipeline

04

Streaming Recommendation Engine — Kafka + Spark Structured Streaming + Pinecone

Real-time personalised content recommendations using two-tower neural embeddings and approximate nearest neighbour search

Covers Topics

Two-Tower ArchitectureContrastive LearningSpark Structured StreamingANN SearchOnline Feature StoreCold Start Problem

Dataset

MovieLens 25M (GroupLens) — 25M ratings, 62k movies, 162k users. Simulate streaming events with Kafka producer replaying historical ratings at 5k events/second.

Two-Tower Model — InfoNCE Loss (Contrastive)
L = −log [exp(sim(u, i⁺)/τ) / (exp(sim(u, i⁺)/τ) + Σ_j exp(sim(u, i_j⁻)/τ))]
where sim(a,b) = cosine similarity, τ = temperature (0.05–0.1), i⁺ = positive item
User tower: embedding(user) → MLP → 128-dim unit vector
Item tower: embedding(item) + metadata → MLP → 128-dim unit vector
Retrieval: HNSW index on all item embeddings, query with user embedding
import torch
import torch.nn as nn
import torch.nn.functional as F
 
class TwoTowerModel(nn.Module):
    def __init__(self, n_users, n_items, emb_dim=64, hidden=256, out_dim=128):
        super().__init__()
        self.user_emb = nn.Embedding(n_users, emb_dim)
        self.item_emb = nn.Embedding(n_items, emb_dim)
        self.user_tower = nn.Sequential(
            nn.Linear(emb_dim, hidden), nn.ReLU(),
            nn.Linear(hidden, out_dim)
        )
        self.item_tower = nn.Sequential(
            nn.Linear(emb_dim, hidden), nn.ReLU(),
            nn.Linear(hidden, out_dim)
        )
 
    def forward(self, user_ids, item_ids):
        u = F.normalize(self.user_tower(self.user_emb(user_ids)), dim=-1)
        v = F.normalize(self.item_tower(self.item_emb(item_ids)), dim=-1)
        return u, v
 
def infonce_loss(u, v, temperature=0.07):
    """In-batch negatives contrastive loss"""
    logits = (u @ v.T) / temperature     # (batch, batch)
    labels = torch.arange(len(u), device=u.device)
    return F.cross_entropy(logits, labels)
python

Key Findings

The two-tower architecture achieves Recall@10 = 0.31 on held-out users — comparable to MF baselines at 10× the serving speed (0.3ms vs 3ms) due to pre-computed item embeddings. Approximate Nearest Neighbour (HNSW) recall-precision trade-off at ef=200: 98.7% recall with 0.4ms search latency over 62k vectors.

PROJECT 05

Synthetic Benchmark Suite

05

Privacy-Preserving Synthetic Dataset Benchmark for Healthcare ML

Generating synthetic EHR data with differential privacy guarantees and benchmarking utility-privacy trade-off across 5 synthesisers

Covers Topics

CTGAN / TVAEDifferential PrivacyHIPAA ComplianceMembership Inference AttackUtility MetricsRe-identification Risk

Dataset

MIMIC-III Clinical Database Demo (PhysioNet) — 100 de-identified ICU patients. Extend to 10k synthetic patients using CTGAN + DP mechanisms.

Privacy-Utility Trade-off
Utility = 1 − |f(X_real) − f(X_synth)| / f(X_real)
where f is a downstream model's AUROC trained on each dataset.

Re-identification Risk = max MIA accuracy (Membership Inference Attack)
MIA accuracy > 0.55 on a balanced test → dataset leaks training membership.

Privacy Budget Allocation: ε_total = ε_synthesis + ε_evaluation
Use ε = 1.0 (strong) for healthcare; utility loss ≈ 3–8% AUROC.

Key Findings

At ε=1.0, CTGAN-DP achieves 89% utility retention (AUROC: 0.83 real vs 0.74 synthetic) with MIA accuracy of 0.51 (effectively random). Without DP (ε=∞), utility rises to 98% but MIA accuracy hits 0.73 — a clear privacy failure. The benchmark demonstrates that ε=3.0 is a practical sweet spot: 94% utility with MIA accuracy 0.54.

INTERVIEW PREP

Big Tech Interview Questions

These questions reflect patterns from Google, Meta, Amazon, Microsoft, and Apple ML/Data Engineering rounds — covering coding, system design, case studies, and behavioural.

Coding + System Design Google / Meta
"Design and implement a feature pipeline that computes, for each user, the following features at prediction time with p99 latency under 10ms: (1) number of purchases in the last 1h, 24h, 7d; (2) average transaction value in 30 days; (3) most frequent product category in the last 30 days. The pipeline must handle 50k events/second. How do you prevent point-in-time leakage in training vs serving?"
Coding Solution — Dual-Store Feature Architecture
# Online feature store with Redis for serving (<1ms)
import redis, json
from collections import defaultdict
from datetime import datetime, timedelta
 
r = redis.Redis(host="redis", decode_responses=True)
 
def update_user_features(user_id: str, tx: dict):
    """Called on every Kafka event — O(log n) per update"""
    now = datetime.utcnow()
    ts = now.timestamp()
    pipe = r.pipeline(transaction=False)  # async pipeline
 
    # Sorted sets — key: user_id:purchases, score: timestamp, value: amount
    pipe.zadd(f"tx:{user_id}", {f"{ts}:{tx['amount']}": ts})
    # TTL — auto-expire events older than 30 days
    pipe.expire(f"tx:{user_id}", 30 * 24 * 3600)
    pipe.execute()
 
def get_user_features(user_id: str) -> dict:
    """Retrieve all time-window features atomically"""
    now = datetime.utcnow().timestamp()
    windows = {"1h": 3600, "24h": 86400, "7d": 604800, "30d": 2592000}
 
    pipe = r.pipeline()
    for name, secs in windows.items():
        pipe.zrangebyscore(f"tx:{user_id}", now - secs, now, withscores=True)
    results = pipe.execute()
 
    features = {}
    for (name, _), entries in zip(windows.items(), results):
        amounts = [float(e.split(":")[1]) for e in entries]
        features[f"tx_count_{name}"] = len(amounts)
        features[f"tx_sum_{name}"]   = sum(amounts)
        features[f"tx_avg_{name}"]   = sum(amounts) / max(len(amounts), 1)
    return features
python
Point-in-Time Correctness (Training vs Serving)

For serving, you query features at the current timestamp. For training, features must be computed as of the label timestamp — not the current time. This is solved with a point-in-time join: for each (user_id, label_timestamp) pair in your training set, replay the sorted-set query against an offline store (Delta Lake with timestamp partitioning) at exactly that timestamp.

-- DuckDB point-in-time feature join
SELECT
    l.user_id,
    l.label,
    l.label_ts,
    COUNT(*) FILTER (WHERE t.ts > l.label_ts - INTERVAL '1 hour')   AS tx_count_1h,
    COUNT(*) FILTER (WHERE t.ts > l.label_ts - INTERVAL '24 hours')  AS tx_count_24h,
    AVG(t.amount) FILTER (WHERE t.ts > l.label_ts - INTERVAL '30 days') AS avg_amount_30d
FROM labels l
LEFT JOIN transactions t ON t.user_id = l.user_id AND t.ts < l.label_ts
GROUP BY l.user_id, l.label, l.label_ts
sql
Why This Approach? What Are the Alternatives?

Why Redis sorted sets? O(log n) insert, O(log n + k) range query, automatic TTL expiry, atomic pipelined batch reads. Sub-millisecond at p99 under this load. Alternatives: (1) DynamoDB with TTL — simpler ops but higher latency (2–5ms); (2) Apache Flink stateful operators — better for exactly-once guarantees but higher operational complexity; (3) In-memory Hazelcast — fast but expensive for 50k/s with 30-day history.

Case Study Scenario

What if the sorted set for a high-frequency user (1M events in 30 days) becomes a Redis hotspot? Solution: shard by user_id modulo N (consistent hashing), use Redis Cluster for automatic sharding, and cap the sorted set size with ZREMRANGEBYSCORE on write. For the analytics path, materialise pre-aggregated hourly buckets in a separate key to reduce scan size from O(events) to O(buckets).

Behavioural Pattern

STAR format — Situation: "In my previous role, our recommendation system was using batch-computed features updated daily. We identified a 22% lift opportunity from real-time features but our Spark batch pipeline couldn't compute 1h windows. Task: Design a low-latency feature store. Action: Proposed a dual-store architecture — Redis for serving, Delta Lake for training. I led the implementation over 6 weeks. Result: p99 latency dropped from 80ms to 6ms, and real-time features contributed a 19% lift in CTR."

Conceptual + Tricky Amazon / Apple
"Your model's AUROC is 0.89 in offline evaluation but only 0.71 in production. You're confident the model code is correct. List all the data-related reasons this could happen and how you'd diagnose each. Then: why would you ever prefer a model with AUROC 0.75 over one with AUROC 0.89 for a fraud detection system?"
Systematic Diagnosis — Data-Side Root Causes
Root CauseMechanismDiagnostic TestFix
Training-Serving SkewFeature computation differs between training (offline) and serving (online)Log online features; compare distribution to training set using KS testUse a unified feature store (Feast/Tecton)
Data Leakage in TrainingFuture information leaked into training features, inflating offline AUROCCheck feature timestamps vs label timestamp; audit feature engineering codePoint-in-time correct joins; strict temporal splits
Concept DriftReal-world distribution shifted after training cutoffRun Evidently drift report; compare monthly feature histogramsRetrain on recent data; add drift alerts
Label DelayGround truth labels arrive with lag (fraud confirmed weeks later), causing mislabelled recent training dataPlot label confirmation delay distributionDelay training cutoff by label lag period
Population ShiftTraining set over-represents certain segments (e.g., US users); prod traffic is globalCompare demographic distributions train vs prodStratified sampling; re-weighting
Feedback LoopModel's past decisions changed the distribution of incoming dataCompare feature distributions before/after model deploymentLog counterfactual data; add exploration via ε-greedy
Why Prefer AUROC 0.75 Over 0.89?

AUROC is a rank-based metric — it tells you how well the model separates classes but nothing about the operational point on the precision-recall curve. A model with AUROC 0.75 might have:

  • Better calibration — if probabilities are well-calibrated, threshold selection is stable and interpretable. The 0.89 model may be perfectly discriminative but miscalibrated (all outputs cluster near 0 or 1), making threshold selection brittle.
  • Lower latency — a shallower model (lower AUROC) that runs in 0.5ms may be preferred over a deep ensemble (0.89 AUROC) taking 50ms if the use case is transaction blocking at checkout.
  • Better fairness — if the 0.89 model achieves its performance by exploiting a demographic proxy feature, regulatory and reputational risk may outweigh the AUROC gain.
  • More stable over time — a simpler model with lower AUROC may degrade more gracefully under distribution shift than an over-fitted complex model.
Expected Cost Minimisation (Better than AUROC for Business)
E[Cost] = FN × cost_fn + FP × cost_fp
For fraud: cost_fn = average fraud amount ($150) | cost_fp = customer friction ($3)
Optimal decision: flag if P(fraud|x) > cost_fp / (cost_fp + cost_fn) = 3/153 ≈ 0.02
This is far more actionable than optimising AUROC.
Behavioural Pattern

Use this to demonstrate independent thinking: "In a previous project, a junior engineer proposed deploying a model with AUROC 0.91 over our production model at 0.83. I asked to see the calibration plots and confusion matrix at our operational threshold. The new model had a 40% higher false-positive rate at the same recall level — which in our user-facing context meant 40% more incorrectly blocked legitimate transactions. We decided not to deploy it and instead used Platt scaling to calibrate both models. The recalibrated 0.83 model outperformed the 0.91 model on the business KPI."

Real-World Scenario Microsoft / Netflix
"You discover that 15% of your training dataset for a medical diagnosis model contains mislabelled records due to a bug in your ETL pipeline. The model is already in production. Walk me through the full remediation plan, and write the SQL/Python to identify and quantify the scope of the corruption."
Incident Remediation Plan
import pandas as pd
import duckdb
from datetime import datetime
import logging
 
logger = logging.getLogger("label-audit")
 
def scope_label_corruption(
    raw_db_path: str,  # original source of truth
    pipeline_output_path: str,  # ETL output with bug
    bug_introduced_ts: str  # timestamp when bug was deployed
) -> dict:
    """
    Step 1: Quantify the blast radius.
    Compare raw labels to ETL output labels for records processed after bug_ts.
    """
    con = duckdb.connect()
 
    result = con.execute(f"""
        WITH source AS (
            SELECT record_id, label AS true_label
            FROM read_parquet('{raw_db_path}')
        ),
        pipeline AS (
            SELECT record_id, label AS etl_label, processed_at
            FROM read_parquet('{pipeline_output_path}')
            WHERE processed_at >= TIMESTAMP '{bug_introduced_ts}'
        )
        SELECT
            COUNT(*) AS affected_records,
            SUM(CASE WHEN source.true_label != pipeline.etl_label THEN 1 ELSE 0 END) AS mislabelled,
            AVG(CASE WHEN source.true_label != pipeline.etl_label THEN 1.0 ELSE 0.0 END) AS mislabel_rate,
            -- Stratify by class to assess bias direction
            source.true_label,
            COUNT(*) AS class_count
        FROM source JOIN pipeline USING (record_id)
        GROUP BY source.true_label
        ORDER BY mislabel_rate DESC
    """).df()
 
    logger.critical(f"LABEL CORRUPTION AUDIT: {result}")
    return result.to_dict()
 
# Step 2: Shadow mode — run old and new model in parallel
# Step 3: Retrain on clean labels — do NOT use any records from bug window
# Step 4: If model cannot be rolled back immediately, implement uncertainty gating:
#   If model entropy > threshold, route to human review instead of auto-decision
python
Full Remediation Sequence
  • T+0 (discovery): Pause any automated retraining pipelines to prevent further contamination. Flag the corrupted model version in MLflow with a "COMPROMISED" tag.
  • T+0 (production): For medical diagnosis specifically — if the model affects clinical decisions, escalate to the Clinical Safety Officer immediately. Consider enabling human-in-the-loop review for all predictions until the model is replaced. This is an EU AI Act Article 9 requirement for high-risk medical AI.
  • T+1 (scope): Run the SQL audit above. Determine: which classes are disproportionately mislabelled? If false negatives for a dangerous condition are elevated, the risk is asymmetric.
  • T+2 (fix source): Fix the ETL bug. Back-fill the pipeline for all affected records. Validate the fix with the diff query above — expect 0 mismatches.
  • T+3 (retrain): Retrain on clean data. Use stratified k-fold to validate label quality (if any contamination remains, cross-validation variance will be elevated).
  • T+4 (validate): Before replacing production model, run shadow deployment for 48h. Compare prediction distributions — a large shift indicates the clean model learned different signal.
  • T+7 (post-mortem): Add a label consistency check as a Great Expectations suite step in the ETL DAG. Alert on mislabel rate > 0.5%.
Behavioural Pattern

Demonstrate ownership: "I would not wait for escalation to act — I would immediately quarantine the contaminated model version in the registry and notify the medical team. My philosophy is to over-communicate early in an incident, even if the full scope is unclear. In a similar incident at a prior role, our delay in communication led to 3 days of bad model serving. I learned that a 30-minute early alert — even with incomplete information — is always preferable to a 3-day wait for a perfect root cause analysis."

RESOURCES

Books, Papers & Courses

Curated to the highest-quality resources — the ones actually cited in production teams and PhD dissertations alike.

Essential Books

Landmark Papers

Courses & Learning Paths

🎯
Learning Path Recommendation (6 months)

Month 1–2: Pandas → DuckDB → SQL mastery (window functions, CTEs). Month 3: Airflow + dbt + build Project 1. Month 4: Spark + Kafka + build Project 2 & 3. Month 5: MLflow + model serving + build Projects 4 & 5. Month 6: Mock interviews, contribute an open-source fix to DuckDB/dbt, publish your capstone writeup on Medium.