State the data grain, assumptions, metric, leakage or failure risk, and how you would validate the result before discussing tools.
Question set
10 detailed answers
01How does a Series differ from a DataFrame, and how do you load and quickly inspect data?
junior
Short answer: A Series is a one-dimensional labelled array (a single column); a DataFrame is a two-dimensional table of column-Series sharing a row index. You load data with pd.read_csv() / pd.read_sql() and inspect it with .head(), .info(), .describe(), .dtypes, .shape.
In depth:
- Series vs DataFrame —
df['col']returns aSeries;df[['col']](double brackets) returns a one-columnDataFrame. Both carry an.index. - Loading —
read_csv(file/URL),read_sql(a SQL query plus a connection),read_parquet(fast columnar format). - Inspecting —
.head()/.tail()peek at the edges,.info()shows dtypes and null counts,.describe()gives stats on numeric columns,.dtypeslists types,.shapegives size.
import pandas as pd
df = pd.read_csv("sales.csv", parse_dates=["order_date"])
df.shape # (10000, 8) — rows, columns
df.info() # column dtypes and non-null counts
df.describe() # count/mean/std/min/quartiles/max for numerics
df["revenue"].head() # Series — a single column
⚠️ Common mistake: forgetting parse_dates= on load — the date column stays object (a string), so date arithmetic and resample() won't work.
02What's the difference between loc and iloc, how do you filter with a boolean mask, and where does SettingWithCopyWarning come from?
middle
Short answer: loc selects by labels (row/column names), iloc by integer positions. You filter with a boolean mask: df[df['x'] > 0]. SettingWithCopyWarning appears with chained indexing (df[mask]['col'] = ...), when pandas can't tell whether you're writing to a copy or a view.
In depth:
loc[rows, cols]— by label, bounds inclusive:df.loc[df['region']=='EU', ['revenue','qty']].iloc[rows, cols]— by position, Python-slice bounds (right end excluded):df.iloc[0:5, :2].- Boolean mask — combine with
&/|and parentheses around each condition:(df['a']>0) & (df['b']<10).
# Right: a single loc on the full DataFrame
mask = (df["price"] > 100) & (df["in_stock"])
df.loc[mask, "tier"] = "premium" # writes in place, no copy
# Wrong: chained -> SettingWithCopyWarning
# df[mask]["tier"] = "premium" # writes to a temp copy, df unchanged
⚠️ Common mistake: chained assignment df[mask]["col"] = val — the change lands in a throwaway copy and is lost. Use a single df.loc[mask, "col"] = val; if you keep working on a subset, take an explicit .copy().
03How does groupby with aggregation work, and why is it the equivalent of SQL's GROUP BY?
middle
Short answer: groupby implements the split-apply-combine pattern: data is split by keys, an aggregating function is applied to each group, and results are combined into one object. It's the direct analogue of GROUP BY. Multiple aggregations are cleanest via named aggregation in .agg().
In depth:
- Split —
df.groupby('region')forms groups by the key's unique values (a list of keys works too). - Apply — a function per group:
.sum(),.mean(),.size(), or several at once via.agg(). - Combine — the result carries the groups in its index;
reset_index()flattens it.
| SQL | pandas |
|---|---|
GROUP BY region |
df.groupby('region') |
SUM(revenue) |
.agg(rev=('revenue','sum')) |
COUNT(*) |
.size() |
HAVING SUM(x)>100 |
.loc[lambda g: g['rev']>100] |
out = (df.groupby("region")
.agg(revenue=("revenue", "sum"),
orders=("order_id", "nunique"),
avg_check=("revenue", "mean"))
.reset_index())
⚠️ Common mistake: by default groupby drops rows with NaN in the key — missing values in the grouping column silently vanish from the result (use dropna=False if they matter).
04How does pd.merge differ from concat, what does how= control, and why use validate=?
middle
Short answer: pd.merge joins tables on keys (like a SQL JOIN); pd.concat simply glues them along an axis (stacking rows or appending columns). how= sets the join type, and validate= catches unexpected row multiplication in many-to-many joins.
In depth:
| how= | what it keeps |
|---|---|
inner |
only matching keys (default) |
left |
all left rows + matching right |
outer |
union of keys from both tables |
right |
all right rows + matching left |
m = pd.merge(
orders, customers,
on="customer_id",
how="left",
validate="many_to_one", # raises if the right key isn't unique
indicator=True # _merge column: both/left_only/right_only
)
# concat is a different operation: stack months on top of each other
year = pd.concat([jan, feb, mar], ignore_index=True)
⚠️ Common mistake: if the right table's key isn't unique, merge silently multiplies the left rows (a cartesian blow-up), and your totals come out inflated. Guard with validate="many_to_one" and a .shape check before/after.
05How do you clean data in pandas: missing values, dtypes, dates and duplicates?
middle
Short answer: Find missing values with isna() and handle them via fillna() or dropna(); coerce types with astype() / to_numeric(); parse dates with to_datetime(); remove duplicates with drop_duplicates(). Whether to fill or drop depends on the share of missing data and why it's missing.
In depth:
- Find gaps —
df.isna().sum()shows theNaNcount per column. - Fill or drop —
fillna(value)/fillna(df['x'].median())to replace;dropna(subset=[...])when keyless rows are useless. - Types and dates —
to_numeric(..., errors='coerce')turns garbage intoNaN;to_datetime()parses dates. - Duplicates —
duplicated()flags them,drop_duplicates(subset=[...], keep='last')removes them.
df["price"] = pd.to_numeric(df["price"], errors="coerce")
df["signup"] = pd.to_datetime(df["signup"], errors="coerce")
df["price"] = df["price"].fillna(df["price"].median())
df = (df.dropna(subset=["customer_id"])
.drop_duplicates(subset=["order_id"], keep="last"))
⚠️ Common mistake: filling gaps with the mean blindly — it shifts the distribution and shrinks variance. First understand why data is missing (MCAR/MAR/MNAR), and for skewed quantities use the median, not mean.
06How does pivot_table differ from melt, and when does an analyst need each?
middle
Short answer: pivot_table reshapes from long to wide (categories become columns, with aggregation); melt does the reverse — gathering many columns into variable–value pairs. Wide format suits a report table; long format suits aggregations and plotting.
In depth:
| Operation | Direction | When you need it |
|---|---|---|
| pivot_table | long → wide | report summary, cross-tab, region×month matrix |
| melt | wide → long | collapse "month columns" into one column for groupby/plot |
# long -> wide: revenue by region and month, aggregated
wide = df.pivot_table(index="region", columns="month",
values="revenue", aggfunc="sum", fill_value=0)
# wide -> long: unpack the month columns back into rows
long = wide.reset_index().melt(
id_vars="region", var_name="month", value_name="revenue")
⚠️ Common mistake: confusing pivot with pivot_table. pivot raises on duplicate index/column pairs, whereas pivot_table aggregates them (aggfunc). For analytics with possible repeats you almost always want pivot_table.
07Why does vectorization in pandas/NumPy beat loops and apply, and when is apply unavoidable?
senior
Short answer: Vectorized operations run in compiled C over whole arrays, with no per-row Python interpreter overhead, so they're tens to hundreds of times faster than loops or apply. apply is justified only when the logic can't be expressed vectorially (complex per-row function, calling an external API).
In depth:
- Vectorization — arithmetic, comparisons,
np.where, string.strand.dtaccessors operate on the entire column at once. apply/loops are slow — each iteration calls a Python function; on millions of rows this destroys performance.- When apply is needed — non-trivial per-row logic with no vectorized equivalent; for conditionals prefer
np.where/np.select. - Memory —
categoryfor repeated strings, downcasting numeric dtypes, reading in pieces withchunksize=.
import numpy as np
# Vectorized (fast):
df["margin"] = (df["price"] - df["cost"]) / df["price"]
df["tier"] = np.where(df["price"] > 100, "premium", "basic")
# Instead of the slow:
# df["tier"] = df.apply(lambda r: "premium" if r.price > 100 else "basic", axis=1)
df["region"] = df["region"].astype("category") # memory savings
⚠️ Common mistake: reaching for df.apply(..., axis=1) by reflex — it's a hidden per-row Python loop. Look for a vectorized path first (np.where, np.select, .str, .dt); apply is a last resort.
08How do you work with time series in pandas: a datetime index, resample and rolling?
middle
Short answer: Time series need a DatetimeIndex. resample() changes the frequency (downsampling: days → weeks/months with aggregation) — it's a "groupby over time". rolling() builds a fixed-size moving window for moving averages and smoothing.
In depth:
- Datetime index —
df = df.set_index('order_date'), and the index must bedatetime(otherwiseresamplefails). - resample —
'D'day,'W'week,'ME'month-end,'QE'quarter; then an aggregator:.sum(),.mean(). - rolling — a window by point count (
window=7) with.mean()/.sum();min_periodshandles the edges.
ts = df.set_index("order_date").sort_index()
# Downsample to monthly: revenue sum
monthly = ts["revenue"].resample("ME").sum()
# 7-day moving average over the daily series
daily = ts["revenue"].resample("D").sum()
ma7 = daily.rolling(window=7, min_periods=1).mean()
⚠️ Common mistake: calling resample() on a string column or without a datetime index gives a TypeError. First to_datetime() + set_index(); and don't confuse resample (by calendar time) with rolling (by row count).
09How does plotting work: figure/axes, df.plot(), and where does seaborn fit?
junior
Short answer: In matplotlib a figure is the canvas and axes is a specific coordinate system (one plot) on it. For speed you can plot straight from pandas via df.plot(kind=...), which draws onto an axes. seaborn is a layer on top of matplotlib for statistical plots in a couple of lines with nice defaults.
In depth:
- figure vs axes —
fig, ax = plt.subplots()creates the canvas and axes; everything draws onax, with labels viaax.set_*. - df.plot() —
kind='line'|'bar'|'hist'|'scatter'; acceptsax=to place it on specific axes. - seaborn —
sns.histplot,sns.boxplot,sns.heatmap— statistics and grouping out of the box, on top of the same axes.
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(8, 4))
monthly.plot(kind="line", ax=ax, marker="o")
ax.set_title("Revenue by month")
ax.set_ylabel("Revenue, $")
df["price"].plot(kind="hist", bins=30, ax=ax) # quick histogram
plt.tight_layout()
⚠️ Common mistake: in a script/notebook, spawning plots without plt.subplots() — everything lands on the same "current" axes (plt.gca()) and series overlap. Create explicit fig, ax per plot.
10When should work happen in the database via SQL versus in pandas, and how do SQL ops map to pandas?
senior
Short answer: Do heavy filtering, aggregation and joins over large tables in the database (it has indexes, a query optimizer, and you avoid pulling everything into memory); load the already-reduced result into pandas for flexible finishing, iterative analysis and visualization. Most SQL operations have a direct pandas analogue.
In depth:
- In the database — when data doesn't fit in RAM, when a filter/aggregation sharply shrinks the volume, when indexes exist.
- In pandas — when the data is already small, you need iteration, complex transforms, plots, or ML features.
| SQL | pandas |
|---|---|
WHERE x > 0 |
df[df.x > 0] |
SELECT a, b |
df[['a','b']] |
GROUP BY g |
df.groupby('g').agg(...) |
JOIN ON k |
pd.merge(a, b, on='k') |
ORDER BY x DESC |
df.sort_values('x', ascending=False) |
LIMIT 10 |
df.head(10) |
# Heavy work in the DB, fine-tuning in pandas
q = "SELECT region, revenue, order_date FROM sales WHERE revenue > 0"
df = pd.read_sql(q, conn) # already filtered on the DB side
monthly = (df.groupby("region").agg(rev=("revenue", "sum")))
⚠️ Common mistake: SELECT * of the whole table and filtering in pandas afterward — you ship gigabytes over the network for nothing and hit memory limits. Push WHERE/GROUP BY to the database and pull only the slice you need into pandas.
Source notes
References and review policy
RecallDeck’s interview answers are editorial material, reviewed against maintained official documentation where a primary reference is available. Tool selections use direct provider links and contain no affiliate placements. Features can change after the review date.
From reading to recall
Practice the full interview loop.
RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.