Practice Problems & Tips
Timed practice challenges to simulate real interview pressure, optimization tips for writing faster code, and an FAQ accordion covering the most common questions candidates ask before ML interviews.
Timed Practice Challenges
Set a timer and try to solve each problem within the allotted time. These simulate the time pressure of real interviews. Solutions follow each problem.
15 min Challenge 1: Implement Batch Normalization
import numpy as np
class BatchNorm:
"""Batch Normalization layer (NumPy implementation).
During training: normalize using batch mean/var, update running stats.
During inference: normalize using running mean/var.
"""
def __init__(self, num_features, momentum=0.1, eps=1e-5):
self.gamma = np.ones(num_features) # Learnable scale
self.beta = np.zeros(num_features) # Learnable shift
self.running_mean = np.zeros(num_features)
self.running_var = np.ones(num_features)
self.momentum = momentum
self.eps = eps
self.training = True
def forward(self, X):
"""
Args:
X: Shape (batch_size, num_features).
Returns:
Normalized X, same shape.
"""
if self.training:
mean = X.mean(axis=0)
var = X.var(axis=0)
# Update running statistics
self.running_mean = (1 - self.momentum) * self.running_mean + self.momentum * mean
self.running_var = (1 - self.momentum) * self.running_var + self.momentum * var
else:
mean = self.running_mean
var = self.running_var
# Normalize
X_norm = (X - mean) / np.sqrt(var + self.eps)
# Scale and shift
return self.gamma * X_norm + self.beta
# Test
np.random.seed(42)
X_train = np.random.randn(32, 4) * 5 + 3 # Non-zero mean, large variance
bn = BatchNorm(num_features=4)
# Training: should normalize to ~zero mean, ~unit variance
out_train = bn.forward(X_train)
print(f"Train output mean: {out_train.mean(axis=0).round(4)}") # ~[0, 0, 0, 0]
print(f"Train output std: {out_train.std(axis=0).round(4)}") # ~[1, 1, 1, 1]
# Switch to eval mode
bn.training = False
X_test = np.random.randn(8, 4) * 5 + 3
out_eval = bn.forward(X_test)
print(f"Eval output (uses running stats): mean={out_eval.mean(axis=0).round(2)}")
10 min Challenge 2: Top-K Accuracy
import numpy as np
def topk_accuracy(y_true: np.ndarray, y_probs: np.ndarray, k: int = 5) -> float:
"""Compute top-k accuracy.
Args:
y_true: True labels, shape (n,).
y_probs: Predicted probabilities, shape (n, num_classes).
k: Number of top predictions to consider.
Returns:
Top-k accuracy as a float in [0, 1].
"""
# Get indices of top-k predictions per sample
top_k_preds = np.argsort(y_probs, axis=1)[:, -k:] # Last k = highest
# Check if true label is in top-k
correct = np.any(top_k_preds == y_true[:, np.newaxis], axis=1)
return correct.mean()
# Test
np.random.seed(42)
n, num_classes = 100, 10
y_true = np.random.randint(0, num_classes, n)
y_probs = np.random.dirichlet(np.ones(num_classes), n) # Random probabilities
# Make some predictions "correct" by boosting true class probability
for i in range(60): # 60% should be easy
y_probs[i, y_true[i]] += 2.0
y_probs = y_probs / y_probs.sum(axis=1, keepdims=True) # Re-normalize
top1 = topk_accuracy(y_true, y_probs, k=1)
top5 = topk_accuracy(y_true, y_probs, k=5)
print(f"Top-1 Accuracy: {top1:.4f}")
print(f"Top-5 Accuracy: {top5:.4f}") # Should be higher than top-1
15 min Challenge 3: Label Encoding with Unseen Categories
fit, transform, and inverse_transform methods.import numpy as np
class SafeLabelEncoder:
"""Label encoder that handles unseen categories gracefully."""
def __init__(self):
self.classes_ = None
self.class_to_idx_ = None
self.unknown_idx_ = None
def fit(self, values):
"""Learn unique classes from training data."""
unique = sorted(set(values))
self.classes_ = ['<UNKNOWN>'] + list(unique)
self.class_to_idx_ = {cls: idx for idx, cls in enumerate(self.classes_)}
self.unknown_idx_ = 0
return self
def transform(self, values):
"""Transform values to integer codes. Unseen -> 0."""
return np.array([
self.class_to_idx_.get(v, self.unknown_idx_)
for v in values
])
def inverse_transform(self, codes):
"""Convert integer codes back to original values."""
return np.array([
self.classes_[c] if 0 <= c < len(self.classes_) else '<UNKNOWN>'
for c in codes
])
def fit_transform(self, values):
return self.fit(values).transform(values)
# Test
encoder = SafeLabelEncoder()
train_data = ['cat', 'dog', 'bird', 'cat', 'dog', 'fish']
test_data = ['cat', 'dog', 'hamster', 'snake', 'bird'] # hamster, snake are unseen
encoded_train = encoder.fit_transform(train_data)
encoded_test = encoder.transform(test_data)
print(f"Classes: {encoder.classes_}")
print(f"Train encoded: {encoded_train}")
print(f"Test encoded: {encoded_test}") # hamster, snake -> 0 (unknown)
print(f"Test decoded: {encoder.inverse_transform(encoded_test)}")
12 min Challenge 4: Stratified Train/Test Split
import numpy as np
from collections import Counter
def stratified_split(X, y, test_size=0.2, random_state=42):
"""Stratified train/test split maintaining class proportions.
Args:
X: Feature array of shape (n, d).
y: Label array of shape (n,).
test_size: Fraction of data for test set.
random_state: Random seed.
Returns:
X_train, X_test, y_train, y_test
"""
rng = np.random.RandomState(random_state)
classes = np.unique(y)
train_indices = []
test_indices = []
for cls in classes:
# Get indices for this class
cls_indices = np.where(y == cls)[0]
rng.shuffle(cls_indices)
# Split this class
n_test = max(1, int(len(cls_indices) * test_size))
test_indices.extend(cls_indices[:n_test])
train_indices.extend(cls_indices[n_test:])
# Shuffle the final indices
train_indices = np.array(train_indices)
test_indices = np.array(test_indices)
rng.shuffle(train_indices)
rng.shuffle(test_indices)
return X[train_indices], X[test_indices], y[train_indices], y[test_indices]
# Test with imbalanced data
np.random.seed(42)
X = np.random.randn(200, 5)
y = np.array([0]*160 + [1]*30 + [2]*10) # 80/15/5 split
X_train, X_test, y_train, y_test = stratified_split(X, y, test_size=0.2)
print(f"Original distribution: {dict(Counter(y))}")
print(f"Train distribution: {dict(Counter(y_train))} (n={len(y_train)})")
print(f"Test distribution: {dict(Counter(y_test))} (n={len(y_test)})")
# Class ratios should be approximately maintained
20 min Challenge 5: End-to-End Mini Pipeline
import numpy as np
import pandas as pd
# Raw data
df = pd.DataFrame({
'age': [25, 30, np.nan, 45, 22, 35, np.nan, 50, 28, 40],
'income': [30000, 50000, 60000, np.nan, 25000, 55000, 45000, 80000, np.nan, 65000],
'education': ['HS', 'BS', 'MS', 'PhD', 'HS', 'BS', 'MS', 'PhD', 'BS', 'MS'],
'purchased': [0, 0, 1, 1, 0, 1, 0, 1, 0, 1]
})
# Step 1: Handle missing values (median for numeric)
for col in ['age', 'income']:
median_val = df[col].median()
df[col] = df[col].fillna(median_val)
# Step 2: One-hot encode categoricals
education_dummies = pd.get_dummies(df['education'], prefix='edu', drop_first=True)
df = pd.concat([df.drop('education', axis=1), education_dummies], axis=1)
# Step 3: Normalize numeric features
for col in ['age', 'income']:
mean_val = df[col].mean()
std_val = df[col].std()
df[col] = (df[col] - mean_val) / (std_val if std_val > 0 else 1)
# Step 4: Split data
X = df.drop('purchased', axis=1).values.astype(float)
y = df['purchased'].values
# Simple train/test (first 8 train, last 2 test for this small demo)
X_train, X_test = X[:8], X[8:]
y_train, y_test = y[:8], y[8:]
# Step 5: Logistic regression from scratch
def sigmoid(z):
return 1 / (1 + np.exp(-np.clip(z, -500, 500)))
# Train
lr = 0.1
w = np.zeros(X_train.shape[1])
b = 0.0
for epoch in range(1000):
z = X_train @ w + b
pred = sigmoid(z)
error = pred - y_train
w -= lr * (X_train.T @ error) / len(y_train)
b -= lr * error.mean()
# Step 6: Predict and evaluate
y_pred_prob = sigmoid(X_test @ w + b)
y_pred = (y_pred_prob >= 0.5).astype(int)
# Metrics from scratch
tp = np.sum((y_pred == 1) & (y_test == 1))
fp = np.sum((y_pred == 1) & (y_test == 0))
fn = np.sum((y_pred == 0) & (y_test == 1))
precision = tp / (tp + fp) if (tp + fp) > 0 else 0
recall = tp / (tp + fn) if (tp + fn) > 0 else 0
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
print(f"Predictions: {y_pred}, Actual: {y_test}")
print(f"Precision: {precision:.4f}")
print(f"Recall: {recall:.4f}")
print(f"F1 Score: {f1:.4f}")
Optimization Tips for Interview Day
Time Management
Spend 20% of time understanding the problem, 60% coding, 20% testing. If stuck for more than 3 minutes, ask for a hint — interviewers prefer candidates who communicate over those who silently struggle.
Start with Brute Force
Get a working solution first, then optimize. A correct O(n²) solution that you explain well beats an incorrect O(n) attempt. Always tell the interviewer: “I will start with a simple approach, then optimize.”
Think Out Loud
Narrate your thought process: “I am going to use broadcasting here because...” This lets the interviewer evaluate your reasoning even if you make a minor syntax error.
Debug Systematically
When code does not work, print shapes first (print(X.shape)), then intermediate values. 80% of ML bugs are shape mismatches. Stay calm and methodical.
The Day-Before Checklist
| Category | Items to Review |
|---|---|
| NumPy | Broadcasting rules, @ operator, einsum, argpartition, keepdims |
| Pandas | groupby + agg, merge types, pivot_table vs crosstab, transform vs apply |
| Sklearn | Pipeline + ColumnTransformer, custom transformer template, cross_val_score, data leakage prevention |
| PyTorch | Training loop (5 steps), Dataset/DataLoader, model.eval(), torch.no_grad() |
| Common Patterns | Softmax with max subtraction, sigmoid from logits, MSE/CE loss gradients, one-hot encoding tricks |
| Edge Cases | Empty arrays, zero division, NaN handling, shape (n,) vs (n,1), float32 vs float64 |
Frequently Asked Questions
Click any question to expand the answer.
Use a class structure when implementing ML models (it mirrors the sklearn API that interviewers expect: __init__, fit, predict). Use functions for data processing utilities and one-off computations. When in doubt, ask the interviewer: “Would you prefer a class-based or functional approach?”
This happens to everyone. Say: “I know there is a function that does X, let me describe what it does.” Then write pseudocode or a comment explaining the behavior. Interviewers care about your understanding of the concept, not whether you memorized every API name. You can also say: “I would look this up in practice, but the approach is...”
Break it down into sub-problems you do recognize. Most ML coding questions combine 2–3 patterns you already know. Say: “I have not seen this exact problem, but it looks like a combination of X and Y. Let me start with...” Interviewers are testing your problem-solving process, not whether you have seen every possible question.
It depends on the format. In live coding interviews (screen share), you generally cannot. In take-home assignments, looking up documentation is expected (but copying entire solutions is not). For in-person/video interviews, some companies allow you to reference official documentation (numpy.org, pandas docs). Always ask the interviewer at the start: “Can I reference documentation if I need to check a function signature?”
Vectorized thinking with NumPy. The ability to replace a Python loop with a NumPy operation is the strongest signal of ML programming fluency. Practice converting for-loops into broadcasting, matrix multiplication, or einsum operations. If you can do this naturally, you will pass the Python fluency bar at any company.
Quality over quantity. Solve 20–30 problems deeply (understand every line of the solution, be able to reproduce it from memory) rather than rushing through 100 problems superficially. The challenges in this course cover the most common patterns. If you can solve all 58 challenges in this course without looking at the solutions, you are well prepared.
Do not panic. This is normal and interviewers expect it. Follow this debugging protocol: (1) Print intermediate variable shapes, (2) Check the first few values of each variable, (3) Test with a trivial input where you know the answer, (4) Check axis parameters (axis=0 vs axis=1 is the most common bug). Say: “Let me add some debug prints to isolate the issue.” This shows systematic debugging skills.
Readability first, always. Write clean, well-named code that works correctly. Then, if time permits, mention optimizations: “This is O(n²), but I could reduce it to O(n) by using a hash map.” The interviewer values that you know the optimization exists more than whether you implemented it. Exception: if the problem specifically asks for an efficient solution, optimize from the start.
FAANG companies have more standardized formats: 45–60 minutes, specific problem types, structured rubrics. Startups tend to be more open-ended: “Here is a dataset, build something useful.” Startups may also give take-home assignments (2–4 hours) that test end-to-end skills. For FAANG, practice algorithmic ML problems. For startups, practice building complete pipelines with messy data.
If your code works: run a test case, discuss edge cases you handled, and mention improvements you would make with more time (“I would add type hints, handle larger-than-memory data, add logging”). If your code does not work: explain what you think the bug is and how you would fix it. Having a clear plan shows competence even if time runs out. Never spend the last minutes silently staring at code.
Final Review: Python for ML Interviews Cheat Sheet
NumPy Essentials
# Broadcasting: (n,1) op (1,m) -> (n,m)
# Matrix multiply: X @ W (not np.dot for 2D+)
# Softmax: exp(x - x.max()) / sum(exp(x - x.max()))
# Distances: ||a-b||^2 = ||a||^2 + ||b||^2 - 2*a.b
# One-hot: np.eye(num_classes)[labels]
# Top-k: np.argpartition(arr, -k)[:, -k:] # O(n) not O(n log n)
# Stable sigmoid: use piecewise for pos/neg z
Pandas Essentials
# Named aggregation: df.groupby('col').agg(name=('col', 'func'))
# Transform (same shape): df.groupby('g')['v'].transform('mean')
# Merge indicator: df1.merge(df2, indicator=True)
# Pivot: pd.pivot_table(df, index=, columns=, values=, aggfunc=)
# Melt (unpivot): df.melt(id_vars=, value_vars=)
# Rolling: df['col'].rolling(window=7, min_periods=1).mean()
# Sessionize: df.groupby('user')['time'].diff() > threshold -> cumsum()
Sklearn Essentials
# Pipeline: Pipeline([('step', Transformer()), ('model', Model())])
# ColumnTransformer: for mixed types (numeric + categorical)
# Custom transformer: BaseEstimator + TransformerMixin, fit/transform
# Grid search params: 'step__param_name'
# CRITICAL: all preprocessing inside Pipeline to prevent data leakage
# Stratified CV: StratifiedKFold for classification
PyTorch Essentials
# Training loop: zero_grad() -> forward -> loss -> backward -> step
# model.train() for training, model.eval() for validation
# torch.no_grad() during validation
# Custom Dataset: __init__, __len__, __getitem__
# Gradient clipping: torch.nn.utils.clip_grad_norm_()
# Transfer learning: freeze backbone, replace head, differential LR
Lilly Tech Systems