Most ML screens recycle the same forty or so theory fundamentals, just reworded. That is good news: the list is learnable. This checkpoint collects the machine learning interview questions that actually come up, grouped the way real screens group them, each with a spoken-style answer and runnable code that proves the claims people get wrong. Tally your score at the end and turn every miss into a short revisit list.
“A model is only as trustworthy as the way you tested it.”
Last Updated: July 2026 | Tested on: Python 3.14.6, scikit-learn 1.9.0, NumPy 2.4.6 | Difficulty: Intermediate | Reading Time: 28 minutes
Classic ML interviews run on three currents: theory you can state, tradeoffs you can defend, and debugging scenarios you can reason through live. Bias versus variance is theory. Choosing recall over precision for fraud detection is a tradeoff. “Training accuracy is 99% but production is 60%, what do you check first” is the debugging scenario, and it is where most candidates stall. The forty questions below train all three muscles, and every claim comes with runnable scikit-learn or NumPy code, because “I watched it happen in a real run” beats “I read it somewhere” in any technical conversation.
These forty stay on ML theory; for the language round of the same loop, the Python interview questions checkpoint drills the core-Python side.
That flowchart is your study route: five theory groups produce a score out of 40, and under 25 means a revisit list with a linked post for every miss. Land 25 or better and you earn the coding round, two from-scratch drills plus a churn-predictor sketch that sets up the ML system-design chapter later. Each section below follows the route in order.
Table of Contents
How This Checkpoint Works
Each of the five groups below has eight questions, forty in total, chosen because these machine learning interview questions show up again and again in real screens across companies of every size. For each group you get a table with a one-line answer you could say out loud and a link to the post that explains it properly, then one runnable code block that proves the part people usually get wrong. Read the answer, cover it, and say it back in your own words. If you cannot, that question goes on your revisit list.
Give yourself one point for every question you can answer cleanly without peeking, so the group tables add up to 40. Keep a running tally on paper. A spoken answer counts only if you could explain it to a teammate in three sentences, not just recognize the words. We total it up in the scoring section, and the honest truth is that this checkpoint measures whether you understand ML theory, while the drills at the end measure whether you can still write the math in NumPy under mild pressure.
Bias, Variance, and Fit
Think of a student cramming for an exam. One student learns three shallow rules and applies them to everything, so they miss the nuance and score badly on any question, that is high bias, or underfitting. Another student memorizes the exact answer to every practice question word for word, then panics when the real exam rephrases them, that is high variance, or overfitting. The whole group is about spotting which student your model is behaving like, and steering it to the calm middle that actually learned the pattern.
| Question | One-line answer | Deep dive |
|---|---|---|
| Bias vs variance? | High bias underfits (too simple); high variance overfits (too sensitive to noise). | ML Math Intuition |
| What is overfitting? | The model memorizes training noise and fails on new data. | Train/Test Split |
| What is underfitting? | The model is too simple to capture the real pattern. | Linear Regression |
| The bias-variance tradeoff? | Lowering one usually raises the other; aim for the sweet spot between them. | ML Math Intuition |
| How do you spot overfitting? | A big gap between low training error and high validation error. | Train/Test Split |
| What is generalization? | Performance on unseen data, the only score that actually counts. | ML Introduction |
| More data or simpler model? | Both cut variance; more data lets a richer model behave, less data needs simpler. | Random Forest |
| Signal vs noise? | Signal is the real pattern; noise is random. Fit the signal, never the noise. | Feature Selection |
📄 fit.py: watch the train-test gap open as model complexity climbs
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import make_pipeline
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
rng = np.random.default_rng(0)
X = np.linspace(0, 1, 60).reshape(-1, 1)
y = np.sin(2 * np.pi * X).ravel() + rng.normal(0, 0.15, 60)
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.4, random_state=0)
for degree in (1, 4, 15):
model = make_pipeline(PolynomialFeatures(degree), LinearRegression())
model.fit(Xtr, ytr)
tr = mean_squared_error(ytr, model.predict(Xtr))
te = mean_squared_error(yte, model.predict(Xte))
verdict = "underfit" if degree == 1 else ("good fit" if degree == 4 else "overfit")
print(f"degree {degree:2d}: train MSE {tr:.3f} | test MSE {te:.3f} -> {verdict}")
▶ Output
degree 1: train MSE 0.211 | test MSE 0.149 -> underfit degree 4: train MSE 0.022 | test MSE 0.037 -> good fit degree 15: train MSE 0.011 | test MSE 0.050 -> overfit
What happened here: The straight line (degree 1) has high error on both training and test data, that is bias, the model is too simple to bend with a sine wave. The degree-4 model fits nicely and its train and test errors stay close together, the healthy sign. The degree-15 model earns the lowest training error of all, 0.011, by wiggling through the noise, but its test error jumps back up to 0.050, and that gap between a great training score and a worse test score is the fingerprint of overfitting. This one table is the whole bias-variance conversation, and interviewers love that you can show it rather than recite it.
Regularization and Regression
Picture packing a bag for a weekend trip. Without a limit you throw in ten pairs of shoes just in case, and the bag becomes heavy and useless. Regularization is a weight limit on the bag: it charges the model a penalty for every large coefficient, so it only keeps the features that truly earn their place. Ridge quietly shrinks everything a little, while Lasso is stricter and drops the weak items to zero entirely, which is why Lasso doubles as automatic feature selection.
| Question | One-line answer | Deep dive |
|---|---|---|
| What is regularization? | A penalty on large weights that curbs overfitting and simplifies the model. | Ridge & Lasso |
| L1 (Lasso) vs L2 (Ridge)? | L1 pushes weak weights to exactly zero; L2 shrinks all weights smoothly. | Ridge & Lasso |
| What does alpha (lambda) control? | Penalty strength: bigger alpha means a simpler, more shrunken model. | Ridge & Lasso |
| Why does Lasso select features? | Its L1 penalty drives unhelpful coefficients all the way to zero. | Feature Selection |
| Linear regression assumptions? | Linearity, independent errors, constant variance, roughly normal residuals. | Linear Regression |
| What is multicollinearity? | Correlated features make coefficients unstable; Ridge steadies them. | Ridge & Lasso |
| What does R-squared mean? | The share of the target’s variance the model explains; 1.0 is perfect. | Linear Regression |
| When use polynomial features? | When the relationship curves; but a high degree overfits very fast. | Ridge & Lasso |
📄 regularize.py: five noise features, and only Lasso zeros them out
import numpy as np
from sklearn.linear_model import LinearRegression, Ridge, Lasso
rng = np.random.default_rng(1)
n = 80
# Only the first 3 features really matter; the rest are pure noise
X = rng.normal(size=(n, 8))
true_w = np.array([5.0, -4.0, 3.0, 0, 0, 0, 0, 0])
y = X @ true_w + rng.normal(0, 0.5, n)
plain = LinearRegression().fit(X, y)
ridge = Ridge(alpha=5.0).fit(X, y)
lasso = Lasso(alpha=0.3).fit(X, y)
np.set_printoptions(precision=2, suppress=True)
print("plain OLS coefs:", plain.coef_)
print("ridge coefs: ", ridge.coef_)
print("lasso coefs: ", lasso.coef_)
print("lasso zeroed features:", int(np.sum(np.abs(lasso.coef_) < 1e-8)), "of 8")
▶ Output
plain OLS coefs: [ 4.94 -3.97 3. 0.09 -0.02 -0.13 0.12 -0.04] ridge coefs: [ 4.5 -3.71 2.68 0.04 0.03 -0.04 0.18 -0.08] lasso coefs: [ 4.43 -3.67 2.55 0. 0. -0. 0. -0. ] lasso zeroed features: 5 of 8
What happened here: All three models recover the real coefficients near 5, -4, and 3 for the first three features. The difference is what they do with the five noise features that should be zero. Plain least squares leaves them as small nonzero values like 0.09 and -0.13, harmless here but noise the model still listens to. Ridge shrinks everything toward zero a little without removing anything. Lasso is the interesting one: it drives all five noise features to exactly 0.0, so the printout confirms it zeroed 5 of 8, which is the concrete reason people say Lasso performs feature selection while Ridge does not.
Metrics: When Accuracy Lies
Imagine a smoke alarm that never goes off. In a house that rarely catches fire, it is right more than 99% of the time, and that number is worthless because the one moment it matters, it fails. That is the accuracy trap on imbalanced data. This group is about the metrics that survive imbalance: precision asks how many of your alarms were real fires, recall asks how many real fires you caught, and F1 blends the two so one comfortable number cannot hide a disaster.
| Question | One-line answer | Deep dive |
|---|---|---|
| When does accuracy mislead? | On imbalanced classes; always guessing the majority can score 96%. | Model Evaluation |
| Precision vs recall? | Precision: of predicted positives, how many were right. Recall: of real positives, how many caught. | Model Evaluation |
| What is the F1 score? | The harmonic mean of precision and recall, one number balancing both. | Model Evaluation |
| What is a confusion matrix? | A table of true and false positives and negatives (TP, FP, FN, TN). | Confusion Matrix |
| What is ROC-AUC? | The chance the model ranks a random positive above a random negative. | ROC Curves |
| PR curve vs ROC, when each? | Precision-recall for heavy imbalance; ROC when classes are fairly balanced. | ROC Curves |
| The precision-recall tradeoff? | Moving the decision threshold trades one against the other. | Model Evaluation |
| How do you pick a metric? | From the real cost of a false positive versus a false negative. | Model Evaluation |
📄 metrics.py: a 96% accurate model that catches zero real positives
import numpy as np
from sklearn.dummy import DummyClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix
from sklearn.datasets import make_classification
X, y = make_classification(n_samples=2000, weights=[0.97, 0.03],
n_informative=5, random_state=7)
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, random_state=7)
# A lazy model that always predicts the majority class
dummy = DummyClassifier(strategy="most_frequent").fit(Xtr, ytr)
pred_d = dummy.predict(Xte)
print(f"always-majority: accuracy {accuracy_score(yte, pred_d):.3f} recall {recall_score(yte, pred_d):.3f}")
# A real model that pays attention to the rare class
clf = LogisticRegression(max_iter=1000, class_weight="balanced").fit(Xtr, ytr)
pred_c = clf.predict(Xte)
print(f"real classifier: accuracy {accuracy_score(yte, pred_c):.3f} "
f"precision {precision_score(yte, pred_c):.3f} recall {recall_score(yte, pred_c):.3f} f1 {f1_score(yte, pred_c):.3f}")
tn, fp, fn, tp = confusion_matrix(yte, pred_c).ravel()
print(f"confusion matrix -> TN {tn} FP {fp} FN {fn} TP {tp}")
▶ Output
always-majority: accuracy 0.963 recall 0.000 real classifier: accuracy 0.772 precision 0.129 recall 0.909 f1 0.226 confusion matrix -> TN 443 FP 135 FN 2 TP 20
What happened here: The lazy model that always predicts the majority class scores a shiny 96.3% accuracy and yet its recall is 0.000, it never catches a single real positive, which is the accuracy trap in one line. The real classifier looks worse on accuracy, 77.2%, but its recall is 0.909, meaning it catches 20 of the 22 actual positives, as the confusion matrix confirms with only 2 false negatives. Its precision is low at 0.129 because it raises many false alarms, and that is the honest tradeoff you would tune with the threshold. The lesson interviewers want stated plainly: on imbalanced data you report precision, recall, and F1, never accuracy alone.
Validation and Leakage
Here is the classroom version. If a teacher hands out the exam answers during revision, everyone scores full marks and the exam measures nothing. Data leakage is exactly that: information from the test set sneaks into training, and your reported score becomes a fantasy. The cure is discipline about what your model is allowed to see and when, and the safest way to enforce it is a scikit-learn Pipeline that refits its preprocessing on each training fold only, so the validation fold stays a true stranger.
| Question | One-line answer | Deep dive |
|---|---|---|
| Why a train/test split? | To estimate how the model does on data it has never seen. | Train/Test Split |
| What is cross-validation? | Rotate the validation fold k times and average, for a steadier estimate. | Cross-Validation |
| What is k-fold? | Split data into k parts; each takes a turn as the validation set. | Cross-Validation |
| What is stratified k-fold? | K-fold that keeps each class ratio steady per fold; vital for imbalance. | Cross-Validation |
| What is data leakage? | Test information reaches training, so scores look far better than reality. | ML Pipeline |
| Scale before or after the split? | Fit the scaler on training data only, ideally inside a pipeline. | ML Pipeline |
| Why use a Pipeline? | It bundles preprocessing with the model so CV refits it per fold, no leakage. | ML Pipeline |
| Validation vs test set? | Validation tunes choices; the test set is the final untouched judge. | Hyperparameter Tuning |
📄 leakage.py: random labels that score 76% when you leak, 51% when you do not
import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.model_selection import cross_val_score
rng = np.random.default_rng(3)
# 200 samples, 1000 pure-noise features, labels are random: TRUE score must be ~0.5
X = rng.normal(size=(200, 1000))
y = rng.integers(0, 2, size=200)
# LEAKY: pick the "best" features using ALL the data, then cross-validate
Xsel = SelectKBest(f_classif, k=20).fit_transform(X, y)
leaky = cross_val_score(LogisticRegression(max_iter=1000), Xsel, y, cv=5).mean()
# HONEST: selection happens inside the pipeline, refit on each training fold only
pipe = make_pipeline(StandardScaler(), SelectKBest(f_classif, k=20),
LogisticRegression(max_iter=1000))
honest = cross_val_score(pipe, X, y, cv=5).mean()
print(f"leaky CV accuracy (selection outside CV): {leaky:.3f}")
print(f"honest CV accuracy (selection in pipeline): {honest:.3f}")
print("truth: labels are random, so real accuracy should be ~0.50")
▶ Output
leaky CV accuracy (selection outside CV): 0.765 honest CV accuracy (selection in pipeline): 0.510 truth: labels are random, so real accuracy should be ~0.50
What happened here: The labels are coin flips, so no honest model can beat about 50%. Yet the leaky version scores 76.5%, a completely fake result, because it chose the 20 “best” features by peeking at all 200 samples including the ones each fold would later use for validation. Those features looked predictive only because they were cherry-picked on the same data being scored. The honest version puts feature selection inside the pipeline, so every fold reselects features from its training rows alone, and the score falls back to a truthful 51.0%. This is the single most common way a beginner reports an amazing model that collapses in production, and naming it calmly is a strong signal in a screen.
Models and Unsupervised Learning
Two mental models cover this group. For ensembles, imagine a panel of judges. Bagging (random forest) asks many independent judges and averages their votes, which cancels out individual mistakes and lowers variance. Boosting builds judges one after another, where each new judge focuses on the cases the panel got wrong so far, which lowers bias. For unsupervised learning, K-means sorts points into groups by nearest center, and Principal Component Analysis (PCA) rotates your data to find the few directions that carry most of the variation, so you can compress or visualize.
| Question | One-line answer | Deep dive |
|---|---|---|
| Bagging vs boosting? | Bagging trains trees in parallel to cut variance; boosting builds them in sequence to cut bias. | Random Forest |
| How does a tree split? | Greedily on the feature that most reduces impurity (Gini or entropy). | Decision Trees |
| Why does a forest use many trees? | Averaging many decorrelated trees lowers variance without adding bias. | Random Forest |
| The gradient boosting idea? | Each new tree fits the errors the current ensemble still makes. | Gradient Boosting |
| How does K-means work? | Assign each point to its nearest centroid, move centroids to the mean, repeat. | K-Means |
| How do you choose k? | The elbow of the inertia curve, or the highest silhouette score. | K-Means |
| What is PCA? | A rotation to new axes ordered by variance, keeping only the top few. | PCA |
| When would you use PCA? | To compress correlated features or to visualize; not for interpretability. | PCA |
📄 models.py: bagging vs boosting side by side, then PCA variance
import numpy as np
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.decomposition import PCA
from sklearn.datasets import load_wine
from sklearn.model_selection import cross_val_score
from sklearn.preprocessing import StandardScaler
X, y = load_wine(return_X_y=True)
rf = cross_val_score(RandomForestClassifier(random_state=0), X, y, cv=5).mean()
gb = cross_val_score(GradientBoostingClassifier(random_state=0), X, y, cv=5).mean()
print(f"random forest (bagging, parallel trees): CV accuracy {rf:.3f}")
print(f"gradient boosting (sequential, fixes errors): CV accuracy {gb:.3f}")
Xs = StandardScaler().fit_transform(X)
pca = PCA().fit(Xs)
cum = np.cumsum(pca.explained_variance_ratio_)
k = int(np.argmax(cum >= 0.90) + 1)
print(f"PCA: first 2 components keep {cum[1]:.1%} of variance")
print(f"PCA: {k} of {X.shape[1]} components reach 90% variance")
▶ Output
random forest (bagging, parallel trees): CV accuracy 0.983 gradient boosting (sequential, fixes errors): CV accuracy 0.939 PCA: first 2 components keep 55.4% of variance PCA: 8 of 13 components reach 90% variance
What happened here: On this small wine dataset the random forest reaches 98.3% cross-validated accuracy and gradient boosting 93.9%, and the point is not that one always wins, it is that you can explain the difference: the forest averages many independent trees to fight variance, while boosting stacks trees in sequence, each correcting the last, to fight bias.
The PCA lines show the compression story: the first two components already hold 55.4% of the total variance, and you need 8 of the 13 original features to reach 90%, which tells you the wine features carry real, spread-out information rather than being mostly redundant. That single number, how many components reach 90%, is how you justify a dimensionality choice instead of guessing.
Score Yourself: The Revisit List
Add up your points across the five tables, one per question you could answer out loud without peeking, for a total out of 40. The band you land in tells you what to do next. With machine learning interview questions the honest goal is not a perfect score today, it is a shrinking revisit list over the next two weeks.
| Score | Where you stand | Do next |
|---|---|---|
| 33 to 40 | Screen-ready on ML theory | Go straight to the two coding drills below. |
| 25 to 32 | Solid, a few soft spots | Re-read the deep-dive posts for your misses, then rescore. |
| Under 25 | Gaps to close first | Build the revisit list below and work it before the drills. |
Here is the trick that makes this efficient: every question in the five tables already names its deep-dive post in the last column, so your revisit list writes itself. For each question you missed, jot the topic and open the linked lesson, for example a shaky answer on leakage sends you to the ML Pipeline post, and a blank on Lasso sends you to Ridge and Lasso. Work only those posts, not the whole chapter, then come back and rescore in a week. Targeted review beats re-reading everything, and the delay is what moves a fact from “I recognize it” to “I can say it cold.”
Two From-Scratch Drills
The standard ML coding round asks you to write an algorithm in plain NumPy, no scikit-learn, to prove you understand the math and not just the import. Two come up constantly: K-means clustering and a logistic-regression gradient step. Write each yourself, then check it against the library to confirm you got it right. Both run on Python 3.14.6 exactly as shown.
First, K-means. The loop is two lines of idea: assign every point to its nearest centroid, then move each centroid to the average of its members, and repeat until nothing moves. We run it a few times from different random starts and keep the best, exactly what the library means by n_init, then compare inertia (the total squared distance to centroids) against scikit-learn.
📄 kmeans_from_scratch.py: assign, move, repeat, and match the library
import numpy as np
from sklearn.cluster import KMeans as SkKMeans
from sklearn.datasets import make_blobs
def one_run(X, k, rng, iters=100):
centers = X[rng.choice(len(X), k, replace=False)].copy()
for _ in range(iters):
# assign each point to its nearest centroid
dists = np.linalg.norm(X[:, None, :] - centers[None, :, :], axis=2)
labels = dists.argmin(axis=1)
new = centers.copy()
for j in range(k): # move centroid to its members' mean
if np.any(labels == j):
new[j] = X[labels == j].mean(axis=0)
if np.allclose(new, centers):
break
centers = new
inertia = ((X - centers[labels]) ** 2).sum()
return labels, centers, inertia
def kmeans(X, k, n_init=10, seed=0):
rng = np.random.default_rng(seed)
best = None
for _ in range(n_init): # keep the best of several restarts
res = one_run(X, k, rng)
if best is None or res[2] < best[2]:
best = res
return best
X, _ = make_blobs(n_samples=300, centers=4, cluster_std=0.6, random_state=42)
labels, centers, inertia = kmeans(X, k=4, seed=1)
print(f"from-scratch inertia: {inertia:.1f}")
sk = SkKMeans(n_clusters=4, n_init=10, random_state=42).fit(X)
print(f"sklearn inertia: {sk.inertia_:.1f}")
print(f"within 2% of sklearn: {abs(inertia - sk.inertia_) / sk.inertia_ < 0.02}")
▶ Output
from-scratch inertia: 203.9 sklearn inertia: 203.9 within 2% of sklearn: True
What happened here: Our hand-written K-means landed on an inertia of 203.9, identical to scikit-learn’s 203.9, which is the confirmation you want: the algorithm is the same, the library just adds smarter initialization and a C-speed inner loop. The two details that separate a passing answer from a buggy one are both visible here. We guard against an empty cluster with the if np.any(labels == j) check so a centroid never divides by zero, and we run several random restarts and keep the lowest inertia, because a single run can settle into a bad local grouping. Talk through those two while you type them and you have shown real understanding.
Second, the logistic-regression gradient step. This is the heart of how most models actually learn: predict, measure the error, and nudge the weights in the direction that reduces it. The gradient for logistic regression is beautifully simple, it is the input features times the prediction error, averaged over the samples. We loop that step and then check our trained weights against scikit-learn on the same data.
📄 logreg_gradient.py: predict, error, nudge the weights, repeat
import numpy as np
from sklearn.linear_model import LogisticRegression
def sigmoid(z):
return 1.0 / (1.0 + np.exp(-z))
def train(X, y, lr=0.3, epochs=8000):
w = np.zeros(X.shape[1])
b = 0.0
n = len(y)
for _ in range(epochs):
p = sigmoid(X @ w + b) # predicted probability
error = p - y # the gradient's core term
w -= lr * (X.T @ error) / n # one gradient step on the weights
b -= lr * error.mean() # and on the bias
return w, b
rng = np.random.default_rng(0)
X = rng.normal(size=(600, 3))
true_w = np.array([2.0, -1.0, 0.5])
# sample labels from the real probability, so the classes overlap (not separable)
y = (rng.random(600) < sigmoid(X @ true_w + 0.3)).astype(float)
w, b = train(X, y)
# turn sklearn's L2 almost off so both fit the same objective
sk = LogisticRegression(C=1e4, max_iter=8000).fit(X, y)
def cosine(a, b):
return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))
np.set_printoptions(precision=2, suppress=True)
print("from-scratch weights:", w, "bias:", round(b, 2))
print("sklearn weights: ", sk.coef_[0], "bias:", round(sk.intercept_[0], 2))
print(f"weight-direction agreement (cosine): {cosine(w, sk.coef_[0]):.4f}")
▶ Output
from-scratch weights: [ 1.94 -1.19 0.55] bias: 0.39 sklearn weights: [ 1.94 -1.19 0.55] bias: 0.39 weight-direction agreement (cosine): 1.0000
What happened here: Our from-scratch weights came out as 1.94, -1.19, 0.55 with a bias of 0.39, and scikit-learn, solving the same problem with a far fancier optimizer, landed on exactly the same numbers, so the cosine agreement between the two weight vectors is 1.0000. That match is the proof the gradient is correct. The line to remember for the interview is error = p - y then w -= lr * (X.T @ error) / n: the update is just the features weighted by how wrong each prediction was, scaled by the learning rate.
Note we sampled labels from the true probability so the classes overlap; on perfectly separable data the weights would run off to infinity without any regularization, which is itself a good thing to mention.
Mini System-Design Warmup: A Churn Predictor
The last thing a screen sometimes throws in is an open question like “how would you build a model to predict which customers will cancel?” You are not expected to code a full system, you are expected to sketch the shape and name the traps. Say a subscription service wants to flag customers likely to churn next month so the retention team can call them. Here is the four-step sketch worth saying out loud, and it doubles as the on-ramp to the ML system-design chapter later in the series.
- Frame it. Binary classification: will this customer cancel in the next 30 days, yes or no. The label comes from historical cancellations, and churn is usually imbalanced, so recall and ROC-AUC matter more than accuracy.
- Features. Tenure, monthly charge, plan type, support tickets, recent usage trend. Mix numeric and categorical, and only use data known before the prediction date to avoid time-travel leakage.
- Pipeline. Scale numbers, one-hot the categories, fit a model, all inside one Pipeline so cross-validation stays leak-free, exactly the discipline from the validation group.
- Evaluate and act. Report ROC-AUC and recall on a time-based holdout, then set a threshold that matches how many calls the retention team can actually make.
That sketch is enough to say in an interview, but it is far more convincing if you can stand up the skeleton in a few lines. Here is that whole plan as a runnable pipeline on synthetic data, mixing numeric and categorical columns and scoring with cross-validated Receiver Operating Characteristic (ROC)-Area Under the Curve (AUC).
📄 churn_sketch.py: a leak-free mixed-type pipeline, scored by ROC-AUC
import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
rng = np.random.default_rng(5)
n = 1000
df = pd.DataFrame({
"tenure_months": rng.integers(1, 72, n),
"monthly_charge": rng.normal(70, 25, n).round(2),
"plan": rng.choice(["basic", "plus", "pro"], n),
})
# higher churn for short tenure + costly plan (plus a little noise)
risk = (df["tenure_months"] < 12).astype(int) + (df["monthly_charge"] > 90).astype(int)
df["churned"] = ((risk + rng.normal(0, 0.5, n)) > 0.9).astype(int)
X = df.drop(columns="churned")
y = df["churned"]
num = ["tenure_months", "monthly_charge"]
cat = ["plan"]
pre = ColumnTransformer([
("num", StandardScaler(), num),
("cat", OneHotEncoder(), cat),
])
model = Pipeline([("prep", pre), ("clf", LogisticRegression(max_iter=1000))])
scores = cross_val_score(model, X, y, cv=5, scoring="roc_auc")
print(f"churn base rate: {y.mean():.1%}")
print(f"5-fold ROC-AUC: {scores.mean():.3f} +/- {scores.std():.3f}")
▶ Output
churn base rate: 23.7% 5-fold ROC-AUC: 0.801 +/- 0.031
What happened here: The base rate shows 23.7% of customers churned, so accuracy alone would reward a lazy model that predicts “stays” for everyone, which is why we scored ROC-AUC instead. The ColumnTransformer scales the two numeric columns and one-hot encodes the plan, and wrapping it with the model in a single Pipeline means each of the five cross-validation folds fits its preprocessing on training rows only, so there is no leakage even though we standardized.
The result, an ROC-AUC of 0.801, says the model ranks a random churner above a random stayer about 80% of the time. That is a genuine end-to-end answer built from the exact pieces this checkpoint drilled, and it is the natural lead-in to the full ML system-design lesson.
Common Mistakes
❌ Mistake: Scaling the whole dataset before the train/test split
# Bad: the scaler sees test rows, so their statistics leak into training scaler = StandardScaler().fit(X_all) # fit on everything, then split X_all = scaler.transform(X_all) X_train, X_test = train_test_split(X_all) # Good: fit preprocessing inside a pipeline so CV refits it per fold pipe = make_pipeline(StandardScaler(), LogisticRegression()) scores = cross_val_score(pipe, X, y, cv=5) # no test statistics leak in
Why: When you fit the scaler on all the data, its mean and standard deviation are computed partly from rows you later score on, so a whisper of the test set has already entered training. The scores look a little better than the truth, and worse, the habit hides a real bug in bigger pipelines. Putting every fit-based step inside a Pipeline means cross-validation refits it on each training fold alone, which is the leak-free default you should reach for every time.
❌ Mistake: Reporting accuracy on imbalanced data
# Wrong: "my fraud model is 98% accurate!" # If 98% of transactions are legitimate, predicting "legit" always scores 98% # and catches zero fraud. # Right: report metrics that survive imbalance # precision, recall, F1, and ROC-AUC or PR-AUC, chosen by the cost of each error
Why: Accuracy counts every correct call equally, so on a rare-positive problem it is dominated by the easy majority and tells you almost nothing about the cases you care about. Name the base rate first, then reach for precision, recall, and F1, and decide which one to optimize from the real-world cost of a false positive versus a false negative. That framing, cost before metric, is what separates a memorized answer from an experienced one.
Best Practices
- Answer in three sentences, then stop. Say what it is, why it matters, and one concrete example. Rambling past a correct answer usually walks you into a follow-up you did not need.
- Name the trap before the tool. For any modeling question, mention leakage, imbalance, or overfitting first, then the fix. It reads as someone who has shipped models, not just trained them.
- Show the math in NumPy when asked. Being able to write the K-means loop or the logistic gradient step proves you understand what the library does under the hood.
- Turn every miss into a named revisit. Do not re-read the whole chapter; open only the deep-dive post your wrong answer points to, then rescore that question in a week.
- Quote versions honestly. The scikit-learn API here was verified on version 1.9.0 at the time of writing; the theory is evergreen, but say “at the time of writing” when a method name might drift.
Wrapping Up
Machine learning interview questions stop being scary once you see the screen as a fixed checklist of forty theory fundamentals, grouped into bias and variance, regularization, metrics, validation and leakage, and models with clustering. You ran real code to settle the ones people get wrong: you watched the train-test gap open as a model overfit, watched Lasso zero out five noise features, watched a 96% accurate model catch zero real positives, watched leakage inflate a random-label score to 76%, and matched your own NumPy K-means and logistic gradient step to scikit-learn exactly.
These ideas are evergreen, they were true decades ago and they behave the same today, with the one honest caveat that specific library method names should be checked against your installed version, verified here on scikit-learn 1.9.0 at the time of writing. Score yourself, build the revisit list from your misses, work the two drills until the loops flow, and the ML screen becomes a formality instead of a fear.
This post is the checkpoint that closes the machine learning half of the journey and hands you off to the deep learning and AI chapters ahead. For the full roadmap, from beginner basics through the AI/ML deep dives, visit the Python + AI/ML tutorial series home.
Frequently Asked Questions
What machine learning interview questions are asked most often?
The most common machine learning interview questions cluster into five theory groups: bias and variance (over and underfitting), regularization (Ridge vs Lasso), metrics (precision, recall, F1, ROC-AUC and when accuracy lies), validation and leakage (cross-validation and pipelines), and models with unsupervised learning (bagging vs boosting, K-means, PCA). A screen usually pulls a handful from each group, which is why this checkpoint organizes forty of them the same way, each with a runnable proof and a link to the lesson that teaches it fully.
What is the difference between bias and variance?
High bias means the model is too simple and underfits, missing the real pattern and scoring poorly on both training and test data. High variance means the model is too flexible and overfits, memorizing training noise so it does great on training data but poorly on new data. The bias-variance tradeoff is that reducing one usually raises the other, and the goal is the sweet spot where training and validation error are both low and close together.
What is data leakage in machine learning?
Data leakage is when information from the test set influences training, so your reported score is far better than the model will do in production. Classic causes are scaling or selecting features on the whole dataset before splitting, or using a feature that would not be known at prediction time. The fix is to put every fit-based preprocessing step inside a scikit-learn Pipeline so cross-validation refits it on each training fold only.
Why is accuracy a bad metric for imbalanced data?
On imbalanced data, one class dominates, so a model that always predicts the majority class scores high accuracy while catching none of the rare cases you actually care about. A fraud model can be 98% accurate and detect zero fraud. Instead report precision, recall, and F1, plus ROC-AUC or PR-AUC, and choose which to optimize based on the real cost of a false positive versus a false negative.
How should I use this checkpoint to prepare?
Cover each answer, say it out loud in three sentences, and give yourself a point only if you could explain it to a teammate. Total your score out of 40, then for every miss open the deep-dive post named in that question’s row and re-read just that lesson. Rescore in a week so the ideas move from recognition to recall, and work the two from-scratch drills so your NumPy stays sharp for the coding round.
Interview Questions About the Interview
The same machine learning interview questions as they show up in a live conversation, framed as scenarios you can practice out loud.
Q: Your model gets 99% accuracy on a fraud dataset. Are you happy? Walk me through it.
My first reaction is suspicion, not celebration, because fraud is rare, so if 99% of transactions are legitimate, a model that predicts “legit” for everything already scores 99% and catches no fraud at all. I would look at the confusion matrix and report recall and precision on the fraud class, plus ROC-AUC or PR-AUC. If recall is near zero, the accuracy is meaningless, and I would rebalance with class weights or resampling and tune the threshold to the cost of a missed fraud versus a false alarm.
Q: A teammate scaled the features before splitting into train and test. What do you tell them?
That it is a data leak, gently but clearly. Fitting the scaler on the full dataset means its mean and standard deviation were computed partly from the test rows, so test information has quietly entered training and the reported score is optimistic. The fix is to fit the scaler on the training data only, and the clean way to guarantee that is to wrap scaling and the model in a single Pipeline, so cross-validation refits the scaler on each training fold by itself.
Q: Explain the difference between bagging and boosting to someone non-technical.
Bagging is asking many independent judges the same question and averaging their votes, so their individual mistakes cancel out and the group is steadier, that is a random forest and it mainly reduces variance. Boosting is a relay where each new judge studies the cases the previous ones got wrong and focuses there, so the team gradually fixes its blind spots, that is gradient boosting and it mainly reduces bias. Same idea of combining many small trees, opposite strategy for how they are built.
Q: You are asked to code K-means without any library. Where do you start, and what breaks?
I start with the two-step loop: assign each point to its nearest centroid, then move each centroid to the mean of the points assigned to it, and repeat until the centroids stop moving. Two things break if you are not careful. A cluster can end up empty, so dividing by its count is a crash, and you guard it by leaving that centroid in place or reseeding it. And a single random start can settle into a poor grouping, so you run it several times from different starts and keep the one with the lowest inertia, which is exactly what the library’s n_init parameter does.
Further reading: for the full reference, see the official Python documentation.
Related Posts
Previous: ML: End-to-End Project, House Price Prediction
Next: Google Colab Graphics Processing Unit (GPU) Setup: Where to Run Deep Learning for Free
Series Home: Python + AI/ML Tutorial Series

No comment