31 Data Scientist Interview Questions and Answers for 2026
Data scientist interview questions come from 6 areas: SQL, statistics and A/B testing, machine learning, Python, product cases, and behavioral stories.
This guide covers 31 common questions, grouped by the round where you'll usually hear them. Each technical answer opens with a short bolded answer you could say out loud in under a minute, then gives the detail. For the behavioral questions, you'll get a structure for your own story instead.
Where a question needs code, we ran it on Python 3.11, with the SQL also checked in DuckDB and SQLite 3.45. The output under each block is what came back. To get the same results, install the same versions:
pip install pandas==3.0.5 numpy==2.4.6 scipy==1.17.1 scikit-learn==1.9.1 statsmodels==0.15.0 duckdb==1.5.6
The random examples use fixed seeds, but other versions can print slightly different numbers.
Quick Answer
Most processes start with a recruiter call, then test SQL, statistics, machine learning, Python, and a product case. Behavioral questions come at the start and the end. For every question, practice a short answer you can say in under a minute, then the detail to add if asked.
Table of Contents
- What Each Interview Round Tests
- SQL Interview Questions (4 Questions)
- Statistics and A/B Testing Questions (9 Questions)
- Machine Learning Interview Questions (9 Questions)
- Python and Data Handling Questions (3 Questions)
- Product and Business Case Questions (2 Questions)
- Behavioral Questions (4 Questions)
- What to Do This Week
- Frequently Asked Questions
What Each Interview Round Tests
Most data scientist hiring processes start with a recruiter call. Then come one or more technical rounds, and then a final set of interviews with the team. Some companies also send a take-home project, where you get a dataset and a few days to analyze it.
If you want to see how much real processes differ, Dataquest's guide to the data science job interview walks through several of them. Here's how each kind of round lines up with the questions in this post.
| Round | What it checks | Questions below |
|---|---|---|
| SQL screen | Whether you can write correct queries under time pressure | 1–4 |
| Statistics and experiments | Whether you can work through probability, design tests, and read results without overclaiming | 5–13 |
| Machine learning | How you choose, test, and maintain models | 14–22 |
| Python coding | Whether you can clean and group data in pandas and write small functions | 23–25 |
| Product case | How you turn a vague business problem into an analysis | 26–27 |
| Recruiter, hiring manager, or behavioral round | Your background, how you work with people, and how you handle setbacks | 28–31 |
Companies name their rounds differently, so match on the middle column rather than the round name.
SQL Interview Questions
SQL often shows up in the first technical screen, usually in a shared online editor the interviewer can watch. If your syntax is rusty, keep the SQL cheat sheet open while you practice. The output under each query is what DuckDB prints.
If you're building these skills from scratch, the SQL part of Dataquest's Data Scientist path covers grouping, joins, and subqueries. Its Window Functions in SQL course teaches the DENSE_RANK() function from question 1.
1. Given an Employees Table, How Would You Find the Second-Highest Salary?
"Here's an
employeestable. Write a query that returns the second-highest salary."
| name | salary |
|---|---|
| Ana | 120000 |
| Ben | 120000 |
| Cho | 95000 |
| Dev | 88000 |
Rank the salaries from highest to lowest with DENSE_RANK() in a subquery, then return the salary ranked 2.
DENSE_RANK() gives tied salaries the same rank, so Ana and Ben both get rank 1 and Cho's 95000 gets rank 2. A plain LIMIT 1 OFFSET 1 would skip one row, not one salary, and return the tied 120000 instead.
DENSE_RANK() is a window function, which means it calculates a value for each row by looking at other rows, without collapsing them the way GROUP BY does. Tied salaries get the same rank, and the next salary gets the next number, with no gap.
The ranking sits in a subquery because WHERE runs before window functions are calculated. DISTINCT keeps one row if several people share the second-highest salary. The first lines rebuild the table above, so you can run it yourself:
CREATE TABLE employees (name TEXT, salary INTEGER);
INSERT INTO employees VALUES
('Ana', 120000), ('Ben', 120000), ('Cho', 95000), ('Dev', 88000);
SELECT DISTINCT salary
FROM (
SELECT salary,
DENSE_RANK() OVER (ORDER BY salary DESC) AS salary_rank
FROM employees
) AS ranked
WHERE salary_rank = 2;
┌────────┐
│ salary │
│ int32 │
├────────┤
│ 95000 │
└────────┘
Here's the LIMIT and OFFSET version the answer warns about. It skips one row, so it lands on Ben's half of the tie:
SELECT salary
FROM employees
ORDER BY salary DESC
LIMIT 1 OFFSET 1;
┌────────┐
│ salary │
│ int32 │
├────────┤
│ 120000 │
└────────┘
Adding DISTINCT to the LIMIT and OFFSET query also fixes it, because it collapses the two 120000 rows into one. Mention both fixes, DENSE_RANK() and DISTINCT. It shows you know why the plain version fails. We ran both queries in DuckDB 1.5 and SQLite 3.45 and got the same results.
A common follow-up asks for the Nth-highest salary instead. Change the 2 in salary_rank = 2 to any number, and the same query works.
Another follow-up asks what happens when there's no second-highest salary, like when everyone earns 120000. This query returns no rows. Wrapping it in an outer SELECT (...) AS second_highest returns NULL instead, which some interviewers expect. DuckDB and SQLite both return NULL that way.
2. Given a Users Table, How Do You Find Duplicate Records?
"Here's a
userstable. Each email should appear only once. Write a query that finds the emails that show up more than once."
| user_id | |
|---|---|
| 1 | kim@example.com |
| 2 | raj@example.com |
| 3 | kim@example.com |
| 4 | lee@example.com |
| 5 | KIM@example.com |
Group by email, count the rows in each group, and keep the groups with HAVING COUNT(*) > 1. Clean the email with LOWER() first, so KIM@example.com and kim@example.com count as the same address.
Filter with HAVING, not WHERE. WHERE runs before the rows are grouped, so the count doesn't exist yet. HAVING runs after.
The first lines rebuild the table above:
CREATE TABLE users (user_id INTEGER, email TEXT);
INSERT INTO users VALUES
(1, 'kim@example.com'), (2, 'raj@example.com'),
(3, 'kim@example.com'), (4, 'lee@example.com'),
(5, 'KIM@example.com');
SELECT LOWER(email) AS email, COUNT(*) AS times_seen
FROM users
GROUP BY LOWER(email)
HAVING COUNT(*) > 1;
┌─────────────────┬────────────┐
│ email │ times_seen │
│ varchar │ int64 │
├─────────────────┼────────────┤
│ kim@example.com │ 3 │
└─────────────────┴────────────┘
Without LOWER(), the query misses user 5. To SQL, KIM@example.com and kim@example.com are different strings:
SELECT email, COUNT(*) AS times_seen
FROM users
GROUP BY email
HAVING COUNT(*) > 1;
┌─────────────────┬────────────┐
│ email │ times_seen │
│ varchar │ int64 │
├─────────────────┼────────────┤
│ kim@example.com │ 2 │
└─────────────────┴────────────┘
A likely follow-up is which duplicate you'd keep. A reasonable answer is the oldest or most complete record, chosen with ROW_NUMBER() inside each group.
3. Given an Orders Table, How Would You Total Sales per Product for One Month?
"Here's an
orderstable. Write a query that totals the units sold for each product in March 2026."
| product_id | qty | ordered_at |
|---|---|---|
| 1 | 2 | 2026-03-02 09:15:00 |
| 1 | 1 | 2026-03-31 18:45:00 |
| 2 | 5 | 2026-03-15 12:00:00 |
| 2 | 3 | 2026-04-01 08:00:00 |
Keep orders placed on or after March 1 and before April 1, then group by product and sum the quantity.
Use < '2026-04-01' instead of BETWEEN and March 31. '2026-03-31' means midnight at the start of that day, so BETWEEN drops the 6:45 p.m. order.
This is called a half-open range. It includes its start but not its end, so every order on March 31, even a late one, still counts.
The first lines rebuild the table above:
CREATE TABLE orders (product_id INTEGER, qty INTEGER, ordered_at TIMESTAMP);
INSERT INTO orders VALUES
(1, 2, '2026-03-02 09:15:00'),
(1, 1, '2026-03-31 18:45:00'),
(2, 5, '2026-03-15 12:00:00'),
(2, 3, '2026-04-01 08:00:00');
SELECT product_id, SUM(qty) AS units_sold
FROM orders
WHERE ordered_at >= '2026-03-01'
AND ordered_at < '2026-04-01'
GROUP BY product_id
ORDER BY product_id;
┌────────────┬────────────┐
│ product_id │ units_sold │
│ int32 │ int128 │
├────────────┼────────────┤
│ 1 │ 3 │
│ 2 │ 5 │
└────────────┴────────────┘
Here's the BETWEEN version the answer warns about:
SELECT product_id, SUM(qty) AS units_sold
FROM orders
WHERE ordered_at BETWEEN '2026-03-01' AND '2026-03-31'
GROUP BY product_id
ORDER BY product_id;
┌────────────┬────────────┐
│ product_id │ units_sold │
│ int32 │ int128 │
├────────────┼────────────┤
│ 1 │ 2 │
│ 2 │ 5 │
└────────────┴────────────┘
Product 1 shows 2 units instead of 3, because the 6:45 p.m. order on March 31 fell outside the range.
A common follow-up asks for the same totals for every month. Group by the month instead of filtering to one. In DuckDB, that's DATE_TRUNC('month', ordered_at). SQLite doesn't have DATE_TRUNC, so use strftime('%Y-%m', ordered_at) there.
4. What Types of Relationships Can Two Tables Have?
The 3 main types are one-to-one, one-to-many, and many-to-many. The first 2 usually use a foreign key, a column that points to a row in the other table, and many-to-many needs a junction table in between. The type tells you whether a join will repeat rows.
A user and their profile settings are usually one-to-one. A customer and their orders are one-to-many, since one customer can place many orders. Students and courses are many-to-many, and the junction table stores one row per student-course pair.
A table can also relate to itself. An employees table with a manager_id column that points back to another employee is the classic case.
The type matters because a one-to-many join repeats rows. Join customers to orders, then sum a customer column like credit limit, and each customer counts once per order. Total the orders first, then join.
Statistics and A/B Testing Questions
These questions cover core statistics, A/B tests, and probability puzzles. Questions 5 through 9 build toward reading an A/B test in question 10, so take them in order.
For a deeper base, the Data Scientist path has 5 statistics and probability courses. Introduction to Conditional Probability in Python covers the Bayes' theorem behind question 13. The path doesn't cover A/B test design, so questions 9 and 10 fill that gap.
5. What Is a P-Value?
A p-value is the probability of seeing results at least as extreme as yours if the null hypothesis, usually "no effect," were true and the test's assumptions held.
A small p-value means your data would be surprising if there were no effect. It isn't the chance that the null hypothesis is true, and it doesn't tell you how big the effect is.
The American Statistical Association (ASA) made the same points in its 2016 statement on p-values. It adds one more worth knowing. A decision shouldn't rest only on whether p lands above or below 0.05.
You can see what the 0.05 cutoff means by running 10,000 fake experiments where nothing is going on. Each one uses a t-test, which compares the means of two groups:
import numpy as np
from scipy import stats
rng = np.random.default_rng(42)
false_positives = 0
for _ in range(10_000):
a = rng.normal(loc=100, scale=15, size=50)
b = rng.normal(loc=100, scale=15, size=50) # same distribution, so no real effect
if stats.ttest_ind(a, b).pvalue < 0.05:
false_positives += 1
print(f"Share of 'significant' results with no real effect: {false_positives / 10_000:.1%}")
Share of 'significant' results with no real effect: 4.7%
About 1 in 20 came out "significant" anyway, which is what a 0.05 threshold allows in the long run.
If you're asked what a high p-value means, don't say it proves the null hypothesis is true. It means your data didn't give strong evidence against it, which is a weaker claim. The test may simply have been too small to see a real effect.
6. What's the Difference Between a Type I and a Type II Error?
A Type I error is a false positive, where you find an effect that isn't there, and a Type II error is a false negative, where you miss one that is.
Lowering one error rate tends to raise the other, unless you collect more data. So the right balance depends on which mistake costs more.
In product terms, a Type I error means shipping a feature that doesn't help. A Type II error means killing a feature that would have helped.
Your significance level, the p-value cutoff you set in advance (usually 0.05), caps the Type I rate for a single, planned test. Power, which is the chance your test detects a real effect of a given size, controls the Type II rate.
Which mistake costs more depends on the team. A fraud team might accept more false alarms to catch more fraud, while a team testing a risky redesign might want stronger evidence before shipping.
7. What Is the Central Limit Theorem, and Why Does It Matter?
The central limit theorem says that if you take large random samples, their averages follow an approximately normal distribution, even when the data itself is skewed.
That's why t-tests and confidence intervals for an average still work reasonably well on skewed data, like order values, once the samples are big enough.
Order values are a good test, because most orders are small and a few are huge. The code below draws 10,000 samples at 3 sizes and measures the skew of their averages. A skew near 0 suggests the shape is symmetric, like a normal curve.
import numpy as np
from scipy import stats
rng = np.random.default_rng(1)
order_values = rng.exponential(scale=50, size=100_000) # skewed, like order values
print(f"skew of the raw data: {stats.skew(order_values):.2f}")
for n in [5, 30, 200]:
means = rng.exponential(scale=50, size=(10_000, n)).mean(axis=1)
print(f"samples of {n:>3}: skew of the sample means = {stats.skew(means):.2f}")
skew of the raw data: 1.99
samples of 5: skew of the sample means = 0.87
samples of 30: skew of the sample means = 0.35
samples of 200: skew of the sample means = 0.15
The raw data has a skew of about 2, and the averages get more symmetric as the samples grow. By 200 orders per sample, they're close to a normal shape.
A few details are worth adding in an interview. "Big enough" depends on how skewed the data is, so the common rule of thumb of 30 is a starting point, not a guarantee. The averages of 30 orders above still have a skew of 0.35, so they still lean right.
The theorem is also about averages, not the raw data. Individual orders stay just as skewed no matter how many you collect. And it assumes the observations are independent, come from the same distribution, and the data doesn't have extremely heavy tails (technically, finite variance).
8. What Does a 95% Confidence Interval Mean?
If you repeated the same study many times, about 95% of the intervals you built would contain the true value.
It doesn't mean there's a 95% chance the true value sits in this one interval. The true value is fixed, and it's the interval that changes from study to study.
People often say "there's a 95% chance the true value is in this interval" in everyday talk. That's exactly why interviewers ask, so stick with the repeated-studies version.
In practice, look at the width. A narrow interval means your estimate is stable, and a wide one means it's shaky. For a difference, like the lift in an A/B test (how far the new version moves the conversion rate), an interval that crosses zero means you don't know the direction of the effect yet.
9. How Would You Design an A/B Test?
Pick one primary metric and the smallest lift worth detecting, then use a power calculation to set the sample size before the test starts. Also decide your significance level and when you'll check the results.
Randomize at the right level, usually by user, run it for at least a full week, and track guardrail metrics like revenue or page speed, so a win on one metric doesn't hide damage on another.
Sample size is where answers usually get vague, so it helps to show the math. Say checkout converts at 4% today. Power, from question 6, is the chance the test detects the lift if it's real, and 80% is a common target:
from statsmodels.stats.power import NormalIndPower
from statsmodels.stats.proportion import proportion_effectsize
baseline = 0.04 # checkout converts at 4% today
for target in [0.050, 0.045, 0.0425]:
effect = proportion_effectsize(target, baseline)
n = NormalIndPower().solve_power(effect_size=effect, alpha=0.05, power=0.8)
print(f"detect {baseline:.2%} -> {target:.2%}: {n:,.0f} users per group")
detect 4.00% -> 5.00%: 6,726 users per group
detect 4.00% -> 4.50%: 25,530 users per group
detect 4.00% -> 4.25%: 99,309 users per group
Each time the lift you want to detect is cut in half, the sample size roughly quadruples. So the smallest lift worth detecting is a business decision, and it often decides whether the test is practical at all.
Randomizing by user keeps one person from seeing both versions. But sometimes users affect each other, like friends on a social app or buyers and sellers in a marketplace.
That blurs a user-level split, because one group changes what the other group sees or does. Splitting by region or by clusters of connected users is a common fix.
Running for at least a full week, and often 2, captures both weekday and weekend behavior. A longer test also lets a novelty effect fade, which is when users try a new design just because it's new.
10. Given Conversion Counts From an A/B Test, How Would You Read the Results?
"We tested a new checkout page. We planned 10,000 visitors per group, and the test is done. Here are the results. Did the variant win?"
| group | visitors | conversions |
|---|---|---|
| Control | 10,000 | 412 |
| Variant (new page) | 10,000 | 468 |
First check that the test ran as planned, with users split randomly and the sample size you set in advance. Then look at the p-value, the size of the lift, and its confidence interval together, and make a call that fits the cost of being wrong.
For these numbers, the honest answer is not proven. The variant is 0.56 percentage points ahead, but the p-value is 0.054, and the confidence interval for the lift runs from just below 0 to 1.1 points. So the variant could even be slightly worse.
Don't keep this test running until p drops below 0.05. Plan a bigger test, or decide based on what a wrong call would cost.
Here's how to check those numbers in Python. The lift is the variant's conversion rate minus the control's:
from statsmodels.stats.proportion import proportions_ztest, confint_proportions_2indep
conversions = [412, 468] # control, variant
visitors = [10_000, 10_000]
z_stat, p_value = proportions_ztest(conversions, visitors)
# variant first here, so a positive lift means the variant did better
# "wald" is the textbook formula; statsmodels' default is "newcomb"
low, high = confint_proportions_2indep(
conversions[1], visitors[1], conversions[0], visitors[0], method="wald"
)
print(f"Control: {conversions[0] / visitors[0]:.2%}")
print(f"Variant: {conversions[1] / visitors[1]:.2%}")
print(f"p-value: {p_value:.3f}")
print(f"95% CI for the lift: {low:.2%} to {high:.2%}")
Control: 4.12%
Variant: 4.68%
p-value: 0.054
95% CI for the lift: -0.01% to 1.13%
That's where the numbers in the answer come from. The output prints the lift in percentage points with a % sign, so -0.01% to 1.13% means from just below 0 to about 1.1 points.
The groups are exactly 10,000 each, so the split looks healthy. A split like 10,000 and 9,400 on a planned 50/50 test is a warning sign called sample ratio mismatch. It usually means a bug in how users were assigned or logged.
With p at 0.054, it's tempting to call the result "almost significant." That still treats 0.05 as the line that decides everything, which is what the ASA statement in question 5 warns against.
Question 9 covers how to set the test up. When you read the results, the rule that matters most is not to stop the test the first time p dips below 0.05.
Checking repeatedly and stopping early pushes your Type I error rate well above 5%. This simulation has no real difference between groups and checks after every 100 users per group, up to 1,000:
import numpy as np
from scipy import stats
rng = np.random.default_rng(0)
early_wins = 0
for _ in range(4_000):
a = rng.normal(size=1_000)
b = rng.normal(size=1_000) # same distribution, so no real effect
checks = range(100, 1_001, 100) # look after every 100 users per group
if any(stats.ttest_ind(a[:n], b[:n]).pvalue < 0.05 for n in checks):
early_wins += 1
print(f"Share of tests that hit p < 0.05 at some check: {early_wins / 4_000:.1%}")
Share of tests that hit p < 0.05 at some check: 18.9%
With 10 checks, almost 1 in 5 tests "wins" even though nothing changed. That's nearly 4 times the 5% you planned for.
The likely follow-up is "So would you keep it running?" Adding users until p drops below 0.05 is the same mistake from the other side. The real issue is that this test was too small.
With 10,000 users per group, it could reliably catch a lift of about 0.8 points, so a real 0.56-point lift could easily come out unclear.
Size the next test with the power calculation from question 9. At this baseline, catching a 0.5-point lift with 80% power takes about 26,000 users per group.
11. What Are the Assumptions of Linear Regression?
Linear regression assumes a linear relationship, independent errors, constant error variance, and roughly normal errors. A residual plot is the quick check for the linear relationship and constant variance.
It's also worth flagging multicollinearity, meaning strongly correlated features, because it makes the coefficients unstable. Normal errors matter mostly for p-values and intervals on small samples.
The errors are the random noise in the outcome. You estimate them with residuals, which are the actual values minus the predicted ones.
A linear relationship means the outcome changes by a steady amount as each input changes, holding the others fixed. An input can be a transformed feature, like x squared. So a model that fits a curve can still count as linear regression.
Constant variance, called homoscedasticity, means the errors are about the same size across the range of predictions.
The residual plot charts each residual against the predicted value. A funnel shape means the variance isn't constant, and a curve means the relationship isn't linear. For a funnel, a log transform of the outcome or robust standard errors are the usual fixes.
Multicollinearity isn't only about 2 features moving together. It also covers a feature that's nearly a combination of others. The model can still predict well, but you can't trust any single coefficient.
A likely follow-up is how you'd measure it. The variance inflation factor (VIF) scores each feature, and a common rule of thumb flags anything above 5 or 10. The usual fix is to drop or combine one of the correlated features.
12. Four People Get on an Elevator That Stops at 4 Floors. What's the Chance Each Gets Off on a Different Floor?
It's 24/256, which is 3/32 or about 9.4%, assuming each person is equally likely to pick any floor. There are 4 × 4 × 4 × 4 = 256 equally likely outcomes, and 4 × 3 × 2 × 1 = 24 of them put everyone on a different floor.
A common follow-up asks for the chance that at least 2 people share a floor. That's 1 minus 3/32, or about 90.6%. It's the same idea as the birthday problem, where shared outcomes are far more likely than people guess.
Checking a counting answer with a quick simulation is a good habit, and it takes only a few lines:
from math import factorial
import numpy as np
exact = factorial(4) / 4**4
rng = np.random.default_rng(7)
floors = rng.integers(1, 5, size=(100_000, 4)) # 100,000 trips, 4 riders each
simulated = np.mean([len(set(trip)) == 4 for trip in floors])
print(f"exact={exact:.4f} simulated={simulated:.4f}")
exact=0.0938 simulated=0.0935
13. A Fraud Model Catches 99% of Fraud and Wrongly Flags 1% of Normal Transactions. If 1 in 1,000 Transactions Is Fraud, What's the Chance a Flagged One Is Fraud?
About 9%. By Bayes' theorem, it's the real fraud the model flags (0.99 × 0.001) divided by all flags, which adds the false alarms (0.01 × 0.999), and that comes to about 0.090.
Most flags are false alarms, because normal transactions outnumber fraud 999 to 1.
Interviewers ask this to see whether you account for the base rate, which is how common fraud is before the model says anything. It's easy to anchor on the 99% and guess high.
Counting real cases makes it concrete. Out of 100,000 transactions, 100 are fraud, and the model flags 99 of them. It also wrongly flags about 999 of the 99,900 normal ones. So 99 of the 1,098 flags are fraud, about 9%.
A quick simulation gives the same answer:
import numpy as np
exact = (0.99 * 0.001) / (0.99 * 0.001 + 0.01 * 0.999)
rng = np.random.default_rng(3)
n = 1_000_000
is_fraud = rng.random(n) < 0.001
flagged = np.where(is_fraud, rng.random(n) < 0.99, rng.random(n) < 0.01)
print(f"exact={exact:.3f} simulated={is_fraud[flagged].mean():.3f}")
exact=0.090 simulated=0.090
In model terms, that 9% is the model's precision, the share of its flags that are correct. Question 16 shows the same effect on a trained model. When the positive class is rare, a model can catch almost all of it and still be wrong on most of its flags.
Machine Learning Interview Questions
Questions 14, 15, 17, and 21 all ask how you'd catch a model that looks fine but isn't. In each one, a good score hides a real problem, like overfitting, a rare class, a leaky feature, or data that changes after launch. Have an example ready for each one.
The Data Scientist path builds these skills across 9 machine learning courses. Optimizing Machine Learning Models in Python covers the cross-validation and regularization from question 14.
14. How Do You Spot and Prevent Overfitting?
Overfitting means a model learned the noise in its training data, so it does worse on new data than on data it has seen.
You spot it when the training score is much higher than the test score, and you prevent it by simplifying the model, tuning with cross-validation, adding regularization, or getting more data.
Here's what that gap looks like on a noisy dataset:
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
X, y = make_classification(n_samples=2_000, n_features=20, flip_y=0.15, random_state=3)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=3)
for depth in [None, 4]:
tree = DecisionTreeClassifier(max_depth=depth, random_state=3).fit(X_train, y_train)
print(f"max_depth={depth}: train={tree.score(X_train, y_train):.3f} "
f"test={tree.score(X_test, y_test):.3f}")
max_depth=None: train=1.000 test=0.766
max_depth=4: train=0.833 test=0.832
The unlimited tree memorized the training set, scoring 100%, but managed only 76.6% on new data. Capping it at 4 levels lowered the training score and raised the test score to 83.2%.
Giving up some training accuracy to gain test accuracy is how the bias-variance tradeoff shows up. Bias is error from a model that's too simple, and variance is error from a model that's too sensitive to its training data. The unlimited tree had low bias and high variance.
To prevent overfitting, you can:
- Simplify the model, the way capping the depth at 4 did.
- Use cross-validation to pick settings like depth. It trains and scores the model on several different splits of the data.
- Add regularization, a penalty that pushes the model toward simpler fits. For linear models, L1 (Lasso) can shrink some coefficients to exactly zero, which drops those features, while L2 (Ridge) shrinks all of them toward zero but keeps every feature.
- Collect more data, though a tree with no depth limit will still memorize it.
15. How Do You Handle an Imbalanced Dataset?
Stop judging the model by accuracy, and look at recall, the share of rare cases it actually catches, alongside precision, the share of its flags that are correct.
Then handle the imbalance with class weights, resampling the training data, or a different decision threshold.
Here's a fraud-style dataset where only about 1.5% of rows are fraud. The code compares a model that always guesses "not fraud" with a logistic regression that uses class weights:
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.dummy import DummyClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, recall_score
# weights=[0.99] makes about 99% of rows 'not fraud'. By default,
# make_classification also reassigns 1% of labels at random, so fraud lands near 1.5%
X, y = make_classification(n_samples=20_000, weights=[0.99], random_state=0)
X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, random_state=0)
baseline = DummyClassifier(strategy="most_frequent").fit(X_train, y_train)
weighted = LogisticRegression(class_weight="balanced", max_iter=1000).fit(X_train, y_train)
for name, model in [("always 'not fraud'", baseline), ("weighted logistic", weighted)]:
pred = model.predict(X_test)
print(f"{name:20} accuracy={accuracy_score(y_test, pred):.3f} "
f"recall={recall_score(y_test, pred):.3f}")
always 'not fraud' accuracy=0.985 recall=0.000
weighted logistic accuracy=0.738 recall=0.680
A model that never predicts fraud scores 98.5% accuracy and catches nothing. Its recall of 0 shows the problem right away.
The weighted model catches 68% of the fraud. Its accuracy fell to 73.8% because it now flags many legitimate transactions too. Question 16 measures that cost, and only about 4% of its flags turn out to be fraud.
From there, the usual fixes are:
- Class weights, as above, which make mistakes on the rare class cost more.
- Resampling, which adds copies of rare rows or drops some common ones.
- Moving the decision threshold, the probability cutoff above which the model says "fraud." Lowering it from the default 0.5 catches more fraud, but usually raises more false alarms too.
If you resample, do it only on the training set. Oversampling before the train/test split can put copies of the same row in both sets, which inflates your scores.
16. What's the Difference Between Precision and Recall, and When Does Each Matter More?
Precision is the share of the model's positive predictions that are correct, and recall is the share of real positives it finds.
Favor recall when a missed case is expensive, like fraud or a disease screen, and favor precision when a false alarm is expensive, like a spam filter hiding a real email. Then move the decision threshold until the balance fits the business.
Here's the fraud model from question 15 again, scored at 2 thresholds. The last line adds 2 summary scores that don't depend on a threshold.
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (precision_score, recall_score,
roc_auc_score, average_precision_score)
X, y = make_classification(n_samples=20_000, weights=[0.99], random_state=0)
X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, random_state=0)
model = LogisticRegression(class_weight="balanced", max_iter=1000).fit(X_train, y_train)
probs = model.predict_proba(X_test)[:, 1] # fraud score for each row (class weights inflate it)
for threshold in [0.5, 0.9]:
pred = (probs > threshold).astype(int)
print(f"threshold {threshold}: precision={precision_score(y_test, pred):.3f} "
f"recall={recall_score(y_test, pred):.3f}")
print(f"ROC AUC={roc_auc_score(y_test, probs):.3f} "
f"average precision={average_precision_score(y_test, probs):.3f}")
threshold 0.5: precision=0.038 recall=0.680
threshold 0.9: precision=0.917 recall=0.147
ROC AUC=0.779 average precision=0.297
At the default 0.5, the model catches 68% of the fraud, but only 3.8% of its flags are actually fraud. At 0.9, 91.7% of its flags are right (11 of 12), but it catches only 14.7% of the fraud. Neither threshold is "right" on its own. It depends on what each mistake costs.
The ROC AUC line shows a trap on rare classes. ROC AUC scores how well the model ranks fraud above legitimate rows. A score of 0.5 is random guessing and 1 is perfect, so 0.779 sounds decent.
But average precision, which sums up precision across every threshold, is only 0.297. Random guessing would score about 0.015, the fraud share. So the model beats chance but is still far from precise.
That's why a PLOS ONE study recommends precision-recall plots over ROC for imbalanced data.
If you need one number at a single threshold, the F1 score combines precision and recall. It's their harmonic mean, so it stays low unless both are decent.
17. How Do You Decide Which Features to Keep?
Remove leaky features first, meaning ones you wouldn't have at prediction time. Then narrow the rest with filter methods that score each feature alone, wrapper methods that retrain on different subsets, or embedded methods like Lasso that drop weak features during training.
Leakage deserves the most attention. Picture a "refund issued" flag in a model that predicts which orders will be returned.
A refund usually happens after a return, so in past data the flag nearly gives away the answer. The test data has it too, so the model scores well in testing. For a new order, though, the flag is always blank, and the model underperforms once it's deployed.
For the 3 method types, have an example ready. A filter method might rank features by correlation with the target. A wrapper method like recursive feature elimination drops the weakest feature and retrains, over and over. Lasso is the usual embedded example.
Run any selection inside cross-validation, on the training folds only. If you pick features using all the data first, the test folds have already shaped the choice, and your score comes out too high.
18. What Is Dimensionality Reduction, and When Would You Use It?
Dimensionality reduction turns many features into fewer ones while keeping most of the variation, usually with principal component analysis (PCA). Use it for many correlated features, slow training, or a 2D plot. Scale the features first, so no single feature takes over.
PCA builds new features, called components, from combinations of the original ones. Each component is a direction through the data. The first points where the data spreads out the most, and each next one does the same at a right angle to the ones before.
Technically, the components are the eigenvectors of the features' covariance matrix. That's the linear algebra interviewers expect you to name. Here's why scaling matters:
from sklearn.datasets import load_wine
from sklearn.decomposition import PCA
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
X, _ = load_wine(return_X_y=True) # 13 features on very different scales
raw = PCA().fit(X)
scaled = make_pipeline(StandardScaler(), PCA()).fit(X)[-1]
print(f"unscaled: first component explains {raw.explained_variance_ratio_[0]:.1%}")
print(f"scaled: first component explains {scaled.explained_variance_ratio_[0]:.1%}")
print(f"scaled: components needed for 90%: "
f"{(scaled.explained_variance_ratio_.cumsum() < 0.90).sum() + 1}")
unscaled: first component explains 99.8%
scaled: first component explains 36.2%
scaled: components needed for 90%: 8
Without scaling, one component seems to explain almost everything. That isn't a real pattern. The proline feature runs from about 300 to 1,700, while every other feature stays under 165, so PCA mostly copies proline.
A likely follow-up is how many components to keep. A common answer is enough to explain about 90% of the variation, which takes 8 of the 13 here. For a plot, you keep just 2.
The cost is interpretability. A component is a blend of features, so you can't explain it the way you'd explain "customer age."
19. How Does a Random Forest Improve on a Single Decision Tree?
A random forest trains many decision trees, each on a random sample of the rows and usually a random subset of features at each split, then combines their predictions. Because the trees make different mistakes, combining them cuts the variance that makes a single tree overfit.
The rows are drawn with replacement, which is called bagging. In scikit-learn, the classifier also picks a random subset of features at each split by default.
You lose the single tree's readable if-then rules, though, and the model takes longer to train and run. If someone asks why not just use one deep tree, say that deep trees overfit. In question 14, the unlimited tree scored 100% on training data but only 76.6% on test data.
A common follow-up is how gradient boosting differs. It also combines many trees, but it builds them one at a time, and each new tree is fit to the errors the earlier ones left behind.
Boosting usually works best with shallow trees and a small learning rate, which limits how much each new tree can change the prediction. It can overfit if you keep adding trees. Early stopping prevents that by halting once the score on held-out data stops improving.
20. How Would You Evaluate a Clustering Model?
Compare the clusters to true labels if you have them, and use an internal score like the silhouette score if you don't. Then check that the clusters are useful, meaning the business can act on each one differently.
Clustering doesn't use labels to train, but sometimes you have them to check against. Then you can compare the clusters to them with a metric like the adjusted Rand index, which scores how well 2 groupings agree.
Without labels, use an internal metric like the silhouette score, which runs from -1 to 1 and rises when clusters are tight and well separated.
Both scores are in scikit-learn's metrics module, as adjusted_rand_score and silhouette_score.
Usefulness is the most convincing check. If the marketing team can't treat one cluster differently from another, the clustering hasn't done its job.
The most likely follow-up is how you'd pick the number of clusters. Fit the model at several values of k, compare the silhouette scores, and start from the highest. Treat it as a guide, though. It can miss when groups overlap, so check that each cluster means something too.
21. When Should You Retrain a Model in Production?
Retrain when monitoring shows performance dropping, and investigate shifts in the input data to see whether retraining is needed. Keep a regular schedule as a backup.
A fixed schedule alone can retrain too late after a sudden change, or too often when nothing has moved.
Two kinds of change are worth naming. Data drift means the inputs look different from training, like a new customer segment arriving.
Concept drift means the relationship between inputs and outcome changed, like fraud patterns adapting to your model. Watch both, and alert when a monitored metric crosses a threshold you set in advance.
A likely follow-up is what you watch when the true outcome arrives late. Fraud labels can take weeks to confirm, so you can't see accuracy drop in real time. Watch the inputs and the model's scores instead, since a shift there is an early warning.
A common measure is the population stability index (PSI). It compares a feature's spread of values now with its spread in the training data.
22. How Would You Build a Model to Predict Customer Churn?
Start by defining churn and the decision the model supports, like who gets a retention offer. Then build features from data available before the prediction date, split by time, beat a simple baseline, and judge the model on precision and recall at the cutoff the team can act on.
Walk through it in that order, since each step depends on the one before.
- Define the target. "Churned" might mean a canceled subscription, or no purchase in 60 days. Pin down the window, because it changes who counts as churned.
- Build features as of a cutoff date. Use only what you'd know on that date, like past orders and support tickets, so nothing from the future leaks in (question 17). Then measure churn in the window after that date.
- Split by time. Train on older months and test on the most recent month whose churn window has closed, because that's how the model will be used. Random splits on time-ordered data give a poor estimate of how it will do on new data.
- Start simple. A logistic regression gives you a baseline to beat before you try a random forest or gradient boosting (question 19).
- Pick the metric from the decision. If the team can call 500 customers a week, what matters is precision among the 500 highest-risk customers, not overall accuracy (questions 15 and 16).
- Test the action. High churn risk doesn't mean the offer will work, so compare offers against a holdout group that gets nothing.
- Plan for production. Decide how you'll monitor the model and when you'll retrain it (question 21).
Python and Data Handling Questions
Live coding rounds for data scientists usually mean pandas and plain Python, so practice cleaning data, grouping it, and writing small functions.
23. How Would You Handle Missing Values?
First check how much is missing and whether the gaps are random or follow a pattern. Then you can drop rows with a few random gaps, fill numeric columns with something like the median, or add a "was missing" flag column when the gap itself might mean something.
Here's a small example where some incomes are missing:
import numpy as np
import pandas as pd
df = pd.DataFrame({
"platform": ["web", "mobile", "web", "mobile", "web", "web", "mobile", "web"],
"income": [52_000, np.nan, 61_000, 64_000, 58_000, 49_000, np.nan, 55_000],
})
print(df.isna().mean()) # share missing in each column
print(df["income"].isna().groupby(df["platform"]).mean().round(2)) # is there a pattern?
train, test = df.iloc[:6].copy(), df.iloc[6:].copy()
fill = train["income"].median() # computed from the training rows only
for part in (train, test):
part["income_missing"] = part["income"].isna().astype(int)
part["income"] = part["income"].fillna(fill)
print(test)
platform 0.00
income 0.25
dtype: float64
platform
mobile 0.67
web 0.00
Name: income, dtype: float64
platform income income_missing
6 mobile 58000.0 1
7 web 55000.0 0
A quarter of incomes are missing, and every gap is a mobile user. That's a pattern, so the income_missing flag keeps it as a feature instead of hiding it.
The median comes from the training rows only. If you compute it on all the data first, the test rows leak into training.
A likely follow-up uses the formal names. Missing completely at random (MCAR) means the gaps have no pattern. Missing at random (MAR) means the gaps can depend on other columns you can see, like platform here, but not on the missing values themselves once you account for those columns.
Missing not at random (MNAR) means they depend on the missing value itself, like high earners skipping an income question. MNAR is the hardest case, because nothing in the data can fully fix it.
24. Given a DataFrame of Test Scores, How Would You Count Score Buckets by Grade in pandas?
"Here's a DataFrame of students' grades and test scores. Put the scores into buckets (0 to 49, 50 to 74, 75 to 89, and 90 to 99), then count how many students in each grade land in each bucket."
| grade | test_score |
|---|---|
| 10 | 45 |
| 10 | 78 |
| 10 | 92 |
| 11 | 58 |
| 11 | 62 |
| 11 | 88 |
Use pd.cut() to assign each score to a bucket, then group by grade and bucket and count the rows with size(). In pandas 3, also pass observed=False, or empty buckets drop out of the result.
The first lines rebuild the DataFrame above:
import pandas as pd
scores = pd.DataFrame({
"grade": [10, 10, 10, 11, 11, 11],
"test_score": [45, 78, 92, 58, 62, 88],
})
scores["bucket"] = pd.cut(
scores["test_score"],
bins=[0, 50, 75, 90, 100],
labels=["0-49", "50-74", "75-89", "90-99"],
right=False,
)
counts = scores.groupby(["grade", "bucket"], observed=False).size()
print(counts.unstack())
bucket 0-49 50-74 75-89 90-99
grade
10 1 0 1 1
11 0 2 1 0
The zeros are the point. Leave out observed=False, and pandas 3 returns only the 5 grade and bucket pairs that have students. Grade 10's "50-74" row and grade 11's "0-49" and "90-99" rows vanish, with no error.
That's because starting in pandas 3.0.0, grouping by a categorical column (one with a fixed set of allowed values, like these buckets) defaults to observed=True, which keeps only the buckets that appear in the data.
In pandas 2, the default was False, and pandas 2.1 and later warned that it would change. So code written for pandas 2 can lose rows without raising any error.
Mention one edge case out loud. With right=False, the last bin stops just before 100, so a perfect score of 100 becomes a missing value (NaN). pandas then leaves it out of the counts without raising an error.
A common follow-up asks for cumulative shares per grade. That's a running sum on the same counts. The first lines repeat the setup, and only the last 3 lines are new:
import pandas as pd
scores = pd.DataFrame({
"grade": [10, 10, 10, 11, 11, 11],
"test_score": [45, 78, 92, 58, 62, 88],
})
scores["bucket"] = pd.cut(
scores["test_score"],
bins=[0, 50, 75, 90, 100],
labels=["0-49", "50-74", "75-89", "90-99"],
right=False,
)
counts = scores.groupby(["grade", "bucket"], observed=False).size()
running_total = counts.groupby("grade").cumsum() # count so far, bucket by bucket
grade_total = counts.groupby("grade").transform("sum") # each grade's total, on every row
print((running_total / grade_total).unstack().round(2))
bucket 0-49 50-74 75-89 90-99
grade
10 0.33 0.33 0.67 1.0
11 0.00 0.67 1.00 1.0
Each row is a running share. For grade 10, 0.33 under "50-74" means a third of grade 10 scored below 75, and the 0.00 under "0-49" for grade 11 is an empty bucket that observed=False kept.
25. Write a Function That Checks Whether a Phrase Is a Palindrome
"Write a function that returns
Trueif a phrase reads the same forward and backward. Ignore capital letters, spaces, and punctuation. It should returnTruefor 'Taco cat' and '(Level)', andFalsefor 'Data'."
Lowercase the text, strip everything that isn't a-z or 0-9, and compare what's left with its reverse.
It runs in O(n) time, meaning the work grows in step with the length of the text. Before coding, ask whether an empty string should count as a palindrome.
import re
def is_palindrome(text):
cleaned = re.sub(r"[^a-z0-9]", "", text.lower())
return cleaned == cleaned[::-1]
for phrase in ["Taco cat", "(Level)", "Data"]:
print(phrase, is_palindrome(phrase))
Taco cat True
(Level) True
Data False
Write regex patterns as raw strings, with an r in front, like the one above. Without the r, a backslash escape Python doesn't recognize, like '\W+' (one or more characters that aren't a letter, digit, or underscore), triggers a SyntaxWarning: invalid escape sequence '\W' in Python 3.12 and later.
On Python 3.11, the same mistake raises a DeprecationWarning instead, which Python hides by default, so you'd see nothing. Either way, the code still runs today, but the Python docs say a future version will turn it into an error.
An escape Python does recognize is sneakier. Without the r, '\b' silently becomes a backspace character, so a pattern like '\bcat\b' finds nothing in normal text.
A common follow-up asks you to skip the cleaned copy to save memory. Walk 2 pointers in from each end, and skip anything that isn't a letter or digit:
def is_palindrome(text):
left, right = 0, len(text) - 1
while left < right:
if not text[left].isalnum():
left += 1
elif not text[right].isalnum():
right -= 1
elif text[left].lower() != text[right].lower():
return False
else:
left += 1
right -= 1
return True
for phrase in ["Taco cat", "(Level)", "Data", "Bébé"]:
print(phrase, is_palindrome(phrase))
Taco cat True
(Level) True
Data False
Bébé False
This version uses almost no extra memory. isalnum() also counts letters like é. The [^a-z0-9] pattern strips them, so the first version wrongly returns True for "Bébé."
Product and Business Case Questions
Product cases use the SQL and statistics from earlier sections. There's rarely one right answer, so talk through each step out loud as you go.
26. A Key Metric Dropped 15% This Month. How Would You Investigate?
"Posts per user on our app dropped 15% this month. How would you figure out why?"
Rule out a data problem first, like a tracking or pipeline change. Then pin down when the drop started, compare it with last year to rule out seasonality, and split the metric into posts and users. Only then test causes, like a new release, one segment at a time.
Talk through it in that order, since each step narrows the next.
- Check the data. Ask whether tracking, a dashboard filter, or a data pipeline (the jobs that load data into your tables) changed. A logging bug can look exactly like a real drop.
- Check the timing. Was it a sudden step on one day, or a slow slide? Compare against the same month last year to rule out seasonality and holidays.
- Split the metric. Posts per user can fall because posts fell or because users grew faster than posts. Then segment by platform, country, app version, and new versus returning users.
- Form and test hypotheses. A new app release or a change to the recommendation feed are typical suspects. Say what data would confirm or rule out each one.
When you compare segments, look at the median as well as the mean. Metrics like posts or time spent per user are right-skewed, with most people near the low end and a long tail of heavy users. A few heavy users can move the mean on their own.
27. How Would You Explain a Model's Results to Someone Without a Technical Background?
Start with the decision they need to make, then give the result in their units, and say plainly how sure you are. Skip the method unless they ask for it.
For example, "If we contact the 2,000 customers the model flags, we expect to reach about 60% of the people who'd cancel this quarter. On last quarter's data, it landed between 55% and 65%. Some people we contact weren't going to cancel anyway."
That sentence gives them a number they can act on and an honest warning about false alarms. If you add a chart, show just one, labeled in their terms.
A likely follow-up is why the model flagged one specific customer. Give the top 2 or 3 reasons in plain words, like "no logins in 3 weeks and 2 support tickets." For complex models, a tool like SHAP can pull those reasons out for each customer.
Behavioral Questions
Behavioral questions often come early, from the recruiter or hiring manager, and again at the end of the process. Expect a setback, a project you're proud of, a disagreement, and how you work with a team.
The STAR method works well for structure. That's Situation, Task, Action, and Result, told in about 2 minutes.
28. Tell Me About a Project That Didn't Go as Planned
Pick a real setback where you changed course, and spend most of your answer on what you did and what you'd do differently. A good answer shows you spotted the problem early and said so.
A dataset that turned out to be unusable halfway through works well, and so does a model that looked great until it met real data.
Name the moment you realized something was wrong, what you told people, and the fix. Then end with the habit you changed afterward, like validating data sources before building on them.
29. Walk Me Through a Project You're Proud Of
Give the question you were answering, the data, what you did, the result, and what you'd change, in about 2 minutes. Then stop and let the interviewer pick what to dig into.
If you don't have work experience, a portfolio project counts. But be ready to explain every choice in it, because interviewers will ask why you used that model or how you handled a messy column. If your portfolio is thin, these data science project ideas range from beginner to advanced.
30. Tell Me About a Time You Disagreed With a Teammate or Stakeholder
Pick a disagreement about the work, like which metric to use or whether a result was ready to share, not a clash of personalities. Spend most of your answer on how you worked it out.
A strong story shows you listened first, then brought evidence. For example, a product manager wants to call a test after 3 days, and you show how stopping early inflates false positives, like the simulation in question 10.
End with the outcome. It's fine if the decision didn't go your way, as long as you disagreed openly and then supported it. Avoid stories where the other person is the villain.
31. How Do You Work With Engineers and Product Managers?
Interviewers want to hear that you can turn a vague request into a clear question, agree on what "done" means, and hand off work others can use.
Use one real project as the example. Say how you agreed on the question and the metric up front, how you shared progress, and what you handed off, like a model engineers could deploy or a dashboard the team kept using.
Name one habit you use on purpose, like writing a one-page plan before you start or sharing rough results early so nobody's surprised. If you've mostly worked alone, a group or portfolio project where others gave you feedback still counts.
What to Do This Week
You don't need to memorize all 31 answers. Each task below takes about an evening.
- Rerun the queries from questions 1, 2, and 3 in a free SQLite or DuckDB editor. Then change the data, and predict which version breaks before you run it.
- Write your short answer to questions 5 through 10, and say each one out loud. If you can't say it without looking at notes, it's not ready.
- Rerun the overfitting and imbalanced-data examples with different settings. Change
max_depthandweights, and predict the result before you run it. - Rerun question 24 without
observed=Falseand find the rows that vanish. Then write the palindrome function from question 25 from memory, with a timer running. - Talk through question 26 out loud, using a metric from an app you use every day. Say each step before you explain it.
- Draft a 2-minute story for each of questions 28 through 31. Record yourself telling one of them.
- Read the job posting again and circle every tool and method it names. Those topics get extra study time.
If the gaps feel bigger than a week of practice, Dataquest's Data Scientist path covers SQL, statistics, and machine learning with 27 projects. It runs about 9 months at 5 hours a week.
The Data Scientist path includes hypothesis testing, but A/B test design and mock interviews aren't part of it, so pair it with the practice above.
Frequently Asked Questions
Are data science interviews hard?
They cover a lot of ground, so the hard part is preparing for 6 areas at once. Rate yourself against the "What it checks" column in the round table near the top, and give your weakest area extra time.
Are Python and SQL enough for a data scientist interview?
For the coding parts, mostly yes, though some teams work in R and will say so in the posting. But you also need statistics and machine learning concepts, and interviewers test those by asking you to explain them out loud.
How is a data scientist interview different from a data analyst interview?
A data analyst interview leans harder on SQL, dashboards, and business questions. A data scientist interview adds machine learning and deeper statistics. If you're applying for both, our data analyst interview questions cover the analyst side.
How much math do data scientist interviews test?
Expect probability, basic statistics, and enough linear algebra to explain what PCA does. You'll rarely derive anything by hand. Instead, expect to explain when a method works and when it fails.