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.
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.
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.
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.
| Category | Formula / Concept | Section |
|---|---|---|
| Feature Scaling | Z-Score: x' = (x − μ) / σ | Min-Max: x' = (x − x_min) / (x_max − x_min) | Robust: x' = (x − Q2) / IQR | Preparation & Preprocessing |
| Imputation | MICE: X_j = f(X_{-j}, θ_j) — iterative chained regression | Preparation & Preprocessing |
| Encoding | Target Encoding: encode(x) = Σy_j / count(x) | Feature Hashing: h(x) = hash(x) mod 2ᵇ | Preparation & Preprocessing |
| Dimensionality | PCA: X = UΣVᵀ, retain λ_k / Σλ ≥ 0.95 | t-SNE: min KL(P ‖ Q) | Preparation & Preprocessing |
| Outlier Detection | Z-Score: |z| > 3 | IQR Fence: Q1 − 1.5×IQR, Q3 + 1.5×IQR | Isolation Forest anomaly score | Cleaning & Manipulation |
| Fairness | Disparate Impact: P(Ŷ=1|A=min) / P(Ŷ=1|A=maj) | Equalised Odds | Demographic Parity | Data Ethics |
| Privacy | ε-DP: P[M(D)∈S] ≤ eᵋ × P[M(D')∈S] | Gaussian Mechanism: M(x) = f(x) + N(0, σ²Δf²) | Dependencies & Security |
| Memory | Memory ≈ rows × cols × bytes_per_dtype | chunk_size = 0.3 × M_avail / bytes_per_row | Distributed Processing |
| Broadcasting | NumPy: dims compatible if equal or one is 1, aligned from right | Core Tools |
| Loss Functions | InfoNCE: −log[exp(sim(u,i⁺)/τ) / Σexp(sim(u,iⱼ⁻)/τ)] | CTGAN: min_G max_D | Projects 4 & 5 |
| Class Imbalance | SMOTE: x_new = x_i + λ(x_nn − x_i) | Cost: argmin_τ [FN×C_fn + FP×C_fp] | Capstone Project |
| Evaluation | E[Cost] = FN × cost_fn + FP × cost_fp | Cramer's V | Point-Biserial r | Interview Prep |
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 Type | Where to Find | Quality Signal | ML 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
- Kaggle Datasets —
kaggle.com/datasets— Community-driven, versioned, competition-tested - Hugging Face Hub —
huggingface.co/datasets— Best for NLP, multimodal; usedatasetslibrary - TensorFlow Datasets (TFDS) — Standardised pipelines for CV/NLP benchmarks
- Paperswithcode Datasets — Tied to reproducible research benchmarks
- AWS Open Data Registry — Petabyte-scale scientific datasets on S3
- Google Dataset Search —
datasetsearch.research.google.com - OpenStreetMap / Overpass API — Geospatial ground truth
Data Gathering Strategy Framework
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
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.
File Format Selection
| Format | Best For | Columnar? | Compression | Schema Evolution |
|---|---|---|---|---|
| Parquet | Analytical workloads, feature stores | ✅ | Excellent (Snappy/ZSTD) | Limited |
| Arrow / Feather | In-memory IPC, Pandas ↔ Spark | ✅ | Good (LZ4) | Good |
| Delta Lake | ACID transactions on data lakes | ✅ (Parquet under) | Excellent | Excellent |
| JSON Lines (JSONL) | Semi-structured, NLP corpora | ❌ | Poor raw / Good gzip | Excellent |
| CSV | Interchange only — avoid at scale | ❌ | Poor | None |
| HDF5 / Zarr | Numerical arrays, geospatial rasters | ❌ | Good | None |
| TFRecord | TensorFlow training pipelines | ❌ | Good | Protobuf-based |
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
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.
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.
where μ = mean, σ = standard deviation
Result: zero mean, unit variance. Good for Gaussian-distributed data.
Sensitive to outliers. Use RobustScaler (IQR-based) if outliers present.
Q2 = median, Q1/Q3 = 25th/75th percentiles. Outlier-resistant.
2. Missing Value Strategies
| Mechanism | Definition | Best Strategy |
|---|---|---|
| MCAR | Missing Completely At Random | Mean/median imputation, row deletion |
| MAR | Missing At Random (conditional on other cols) | KNN imputation, iterative imputer (MICE) |
| MNAR | Missing Not At Random (value depends on itself) | Add missingness indicator flag + model-based imputation |
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
Risk: target leakage on training set. Use cross-fold target encoding to prevent this.
Useful for very high-cardinality categoricals (e.g., user IDs with millions of values).
4. Feature Engineering: Interaction Terms & Polynomial Features
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 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.
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
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
Assumes Gaussian distribution. Fails for heavy-tailed distributions.
Upper fence = Q3 + 1.5 × IQR
where IQR = Q3 − Q1 (25th to 75th percentile range)
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
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)
- Informed consent: was data collected with explicit user knowledge and agreement?
- Purpose limitation: are you using data only for the stated purpose?
- Data minimisation: have you collected the minimum data sufficient for the task?
- Anonymisation / pseudonymisation: are PII fields removed or hashed?
- Re-identification risk: is the combination of fields enough to re-identify individuals?
- Demographic representation: does your dataset reflect the population the model serves?
- Labelling fairness: were annotators from diverse backgrounds? Check inter-annotator agreement.
- Historical bias: does your target variable encode historical discrimination (e.g., loan defaults shaped by redlining)?
- Right to erasure: can you delete a user's data from the training set and retrain?
- Transparency: is the dataset documented with a Datasheet (Gebru et al. 2018) or Data Nutrition Label?
Bias Detection — Mathematical Framework
DI < 0.8 → potential illegal discrimination
Equal true-positive AND false-positive rates across protected groups.
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
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.
| Regulation | Jurisdiction | Key Articles/Sections | ML-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
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
| Level | Description | Examples | Controls Required |
|---|---|---|---|
| Restricted | PII, PHI, financial credentials | SSN, medical records, passwords | Encryption at rest + transit, access logging, DLP |
| Confidential | Business-sensitive non-PII | Revenue data, ML model weights, IP | Role-based access, audit trails |
| Internal | Operational data | Logs, metrics, employee data | Authentication, least-privilege |
| Public | Open data | Open-source datasets, press releases | Integrity checks only |
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 Type | Description | Defence |
|---|---|---|
| Label Flipping | Adversary corrupts labels in training set | Certified Data Cleaning, SEVER algorithm |
| Backdoor/Trojan | Trigger pattern inserted into training images | Neural Cleanse, Spectral Signatures detection |
| Model Inversion | Reconstruct training data from model outputs | Differential Privacy, output perturbation |
| Membership Inference | Determine if sample was in training set | DP-SGD training, prediction confidence limiting |
| Supply Chain | Malicious public dataset (e.g., poisoned LAION subset) | Hash-verify source, sandboxed ingestion |
Differential Privacy for ML
P[M(D) ∈ S] ≤ e^ε × P[M(D') ∈ S]
ε = privacy budget: smaller = more private. Typical values: 0.1 (strong) to 10 (weak).
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
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
float64: 8 bytes/element | float32: 4 | int8: 1 | bool: 1
Example: 100M rows × 50 float32 cols = 100M × 50 × 4 = 20 GB
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
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
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
(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
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.
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
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
| Scenario | Best Tool | Why |
|---|---|---|
| Statistical analysis, EDA | Pandas + NumPy | Rich API, Jupyter integration |
| ML research, custom gradients | JAX + Flax/Optax | Composable transforms, XLA JIT |
| Deep learning production | PyTorch + TorchScript | Ecosystem, ONNX export, deployment |
| TF Serving / TPU training | TensorFlow / Keras | Best for Google Cloud, TPU support |
| Large matrix ops, PCA, SVD | NumPy / JAX | Broadcasting, einsum, linalg |
| Petabyte-scale feature eng. | PySpark + Delta Lake | Distributed, ACID, versioned |
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.
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: 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
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.
| Tool | Type | Best For | Licence |
|---|---|---|---|
| SDV (Synthetic Data Vault) | Statistical / DL | Tabular, relational, time-series | BSL (free tiers) |
| CTGAN / TVAE | GAN / VAE | Tabular with mixed types | MIT |
| Gretel.ai | DGAN, Actgan | Privacy-safe enterprise data | SaaS |
| Faker | Rule-based | PII generation, testing | MIT |
| DiffPrivLib | DP mechanisms | Privacy-preserving statistics | MIT |
| Mimesis | Rule-based | Multi-locale fake data | MIT |
| Augly (Meta) | Augmentation | Text, image, video augmentation | MIT |
| Albumentations | Image augmentation | CV training data expansion | MIT |
| Claude / GPT-4o API | LLM generation | NLP datasets, instruction tuning | API |
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
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.
| Database | Type | ML Use Case | Strengths | Avoid When |
|---|---|---|---|---|
| PostgreSQL | OLTP / Relational | Feature metadata, experiment tracking, label storage | ACID, extensions (pgvector!), mature ecosystem | Analytical queries on billions of rows |
| DuckDB | Embedded OLAP | Local EDA, feature engineering, parquet/CSV analytics | Blazing fast columnar, runs in Python process, no server | Multi-user concurrent writes |
| CockroachDB | NewSQL / Distributed | Global feature stores needing strong consistency | Distributed ACID, Postgres-compatible, geo-partitioning | Pure analytics — too expensive for OLAP |
| MongoDB | Document / NoSQL | Semi-structured data ingestion, raw event storage | Flexible schema, Atlas Vector Search | Tabular ML features — use a proper warehouse |
| ChromaDB | Vector DB (local) | Embedding search, RAG prototyping, semantic dedup | Zero-ops, in-process, great for prototypes | Production at scale (>10M vectors) |
| Pinecone | Vector DB (managed) | Production RAG, semantic search, recommendation | Managed, fast approximate NN, filtering | Cost-sensitive startups, offline/air-gap |
| Neo4j | Graph DB | Fraud detection, knowledge graphs, GNNs | Cypher query language, GraphSAGE integration, APOC | Tabular data — significant overhead |
The Verdict — ML Engineer Stack 2026
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 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.
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
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.
| Tool | Role | ML Use Case | 2026 Status |
|---|---|---|---|
| Apache Spark | Distributed compute | Feature engineering at petabyte scale, Spark MLlib | ✅ Essential — use PySpark + Delta Lake |
| Apache Kafka | Event streaming | Real-time feature computation, online serving, training data streams | ✅ Essential — combine with Flink for streaming ML |
| Apache Airflow | Workflow orchestration | ML pipeline DAGs, retraining schedules, data quality checks | ✅ Still standard — Prefect/Dagster rising alternatives |
| Snowflake | Cloud data warehouse | Snowpark ML, feature stores, OLAP on structured data | ✅ Industry standard for enterprise analytics |
| Apache Mahout | Distributed ML | Legacy 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
ETL / Warehousing / OLAP
Covers Topics
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
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.
EDA with Key Performance Metrics
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
| KPI | Formula | Business Meaning |
|---|---|---|
| Churn Rate | Churned / Total customers | Monthly retention health |
| Customer LTV | ARPU × (1/Churn Rate) | Revenue per acquired customer |
| NRR (Net Revenue Retention) | (Start MRR + Expansion − Churn) / Start MRR | Revenue momentum (target >100%) |
| CAC Payback Period | CAC / (ARPU × Gross Margin) | Months to recover acquisition cost |
| Product Adoption Score | Features Used / Total Features × Frequency | Stickiness 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.
End-to-End ML Production System
Covers Topics
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)
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.
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.
Real-Time ML Pipeline
Covers Topics
Dataset
MovieLens 25M (GroupLens) — 25M ratings, 62k movies, 162k users. Simulate streaming events with Kafka producer replaying historical ratings at 5k events/second.
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.
Synthetic Benchmark Suite
Covers Topics
Dataset
MIMIC-III Clinical Database Demo (PhysioNet) — 100 de-identified ICU patients. Extend to 10k synthetic patients using CTGAN + DP mechanisms.
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.
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 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."
Systematic Diagnosis — Data-Side Root Causes
| Root Cause | Mechanism | Diagnostic Test | Fix |
|---|---|---|---|
| Training-Serving Skew | Feature computation differs between training (offline) and serving (online) | Log online features; compare distribution to training set using KS test | Use a unified feature store (Feast/Tecton) |
| Data Leakage in Training | Future information leaked into training features, inflating offline AUROC | Check feature timestamps vs label timestamp; audit feature engineering code | Point-in-time correct joins; strict temporal splits |
| Concept Drift | Real-world distribution shifted after training cutoff | Run Evidently drift report; compare monthly feature histograms | Retrain on recent data; add drift alerts |
| Label Delay | Ground truth labels arrive with lag (fraud confirmed weeks later), causing mislabelled recent training data | Plot label confirmation delay distribution | Delay training cutoff by label lag period |
| Population Shift | Training set over-represents certain segments (e.g., US users); prod traffic is global | Compare demographic distributions train vs prod | Stratified sampling; re-weighting |
| Feedback Loop | Model's past decisions changed the distribution of incoming data | Compare feature distributions before/after model deployment | Log 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.
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."
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."
Books, Papers & Courses
Curated to the highest-quality resources — the ones actually cited in production teams and PhD dissertations alike.
Essential Books
- BookDesigning Machine Learning Systems — Chip Huyen (O'Reilly 2022) — The definitive guide to production ML, covers feature stores, data quality, and deployment end-to-end.
- BookFundamentals of Data Engineering — Reis & Housley (O'Reilly 2022) — Best overview of the full data stack from ingestion to serving.
- BookHigh Performance Spark — Karau & Warren (O'Reilly) — Internals, tuning, and optimisation of PySpark pipelines.
- BookThe Elements of Statistical Learning — Hastie, Tibshirani, Friedman (Free PDF: web.stanford.edu/~hastie/ElemStatLearn/) — Mathematical foundations essential for every ML engineer.
- BookData Pipelines Pocket Reference — Densmore (O'Reilly 2021) — Practical guide covering Kafka, Airflow, Spark.
- BookResponsible Data Science — Schelter & Stoyanovich (MIT Press) — Ethics, fairness, and governance with code examples.
Landmark Papers
- PaperDatasheets for Datasets — Gebru et al. (2018) — arxiv.org/abs/1803.09010 — Standard for dataset documentation. Read before publishing any dataset.
- PaperData Management Challenges in Production ML — Sculley et al. (NIPS 2015) — "Hidden Technical Debt in ML Systems" — Essential reading on ML engineering pitfalls.
- PaperDifferential Privacy: A Survey of Results — Dwork (2008) — Foundational DP theory with mathematical proofs.
- PaperSMOTE: Synthetic Minority Over-sampling Technique — Chawla et al. (JAIR 2002) — Original SMOTE paper, still the most cited class imbalance technique.
- PaperAttention Is All You Need — Vaswani et al. (2017) — The transformer architecture that powers every modern NLP feature pipeline.
- PaperLakehouse: A New Generation of Open Platforms — Zaharia et al. (CIDR 2021) — Foundational paper for Delta Lake / Iceberg / Hudi architectures.
Courses & Learning Paths
- Coursemlops-zoomcamp — github.com/DataTalksClub/mlops-zoomcamp — Free, hands-on MLOps with MLflow, Prefect, and FastAPI. Highest ROI course in this space.
- CourseFull Stack Deep Learning — fullstackdeeplearning.com — From data pipelines to LLM deployment. Free online lectures.
- CourseStanford CS329S: ML Systems Design — stanford-cs329s.github.io — Chip Huyen's course. Lecture notes freely available.
- Coursedbt Fundamentals — courses.getdbt.com — Free, the best way to learn dbt for data transformation.
- DocsApache Spark Official Documentation — spark.apache.org/docs/latest — Internals, tuning guide, and PySpark API.
- DocsDuckDB Documentation — duckdb.org/docs — Especially the SQL extensions and Parquet/Arrow integration.
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.