Skip to content
Data & AI

10 Data Analyst Python for Analysts Interview Questions and Answers

This focused guide turns RecallDeck’s curated Data Analyst Python for Analysts material into 10 interview-ready questions. Answer each one before opening the explanation, then use the examples and edge cases to repair anything vague or incomplete.

10 min read10 detailed answersReviewed Aug 24, 2026
What to remember

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

01

How does a Series differ from a DataFrame, and how do you load and quickly inspect data?

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:

  1. Series vs DataFramedf['col'] returns a Series; df[['col']] (double brackets) returns a one-column DataFrame. Both carry an .index.
  2. Loadingread_csv (file/URL), read_sql (a SQL query plus a connection), read_parquet (fast columnar format).
  3. Inspecting.head()/.tail() peek at the edges, .info() shows dtypes and null counts, .describe() gives stats on numeric columns, .dtypes lists types, .shape gives 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.

02

What's the difference between loc and iloc, how do you filter with a boolean mask, and where does SettingWithCopyWarning come from?

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().

03

How does groupby with aggregation work, and why is it the equivalent of SQL's GROUP BY?

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:

  1. Splitdf.groupby('region') forms groups by the key's unique values (a list of keys works too).
  2. Apply — a function per group: .sum(), .mean(), .size(), or several at once via .agg().
  3. 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).

04

How does pd.merge differ from concat, what does how= control, and why use validate=?

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.

05

How do you clean data in pandas: missing values, dtypes, dates and duplicates?

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:

  1. Find gapsdf.isna().sum() shows the NaN count per column.
  2. Fill or dropfillna(value) / fillna(df['x'].median()) to replace; dropna(subset=[...]) when keyless rows are useless.
  3. Types and datesto_numeric(..., errors='coerce') turns garbage into NaN; to_datetime() parses dates.
  4. Duplicatesduplicated() 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.

06

How does pivot_table differ from melt, and when does an analyst need each?

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.

07

Why does vectorization in pandas/NumPy beat loops and apply, and when is apply unavoidable?

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:

  1. Vectorization — arithmetic, comparisons, np.where, string .str and .dt accessors operate on the entire column at once.
  2. apply/loops are slow — each iteration calls a Python function; on millions of rows this destroys performance.
  3. When apply is needed — non-trivial per-row logic with no vectorized equivalent; for conditionals prefer np.where / np.select.
  4. Memorycategory for repeated strings, downcasting numeric dtypes, reading in pieces with chunksize=.
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.

08

How do you work with time series in pandas: a datetime index, resample and rolling?

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:

  1. Datetime indexdf = df.set_index('order_date'), and the index must be datetime (otherwise resample fails).
  2. resample'D' day, 'W' week, 'ME' month-end, 'QE' quarter; then an aggregator: .sum(), .mean().
  3. rolling — a window by point count (window=7) with .mean()/.sum(); min_periods handles 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).

09

How does plotting work: figure/axes, df.plot(), and where does seaborn fit?

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:

  1. figure vs axesfig, ax = plt.subplots() creates the canvas and axes; everything draws on ax, with labels via ax.set_*.
  2. df.plot()kind='line'|'bar'|'hist'|'scatter'; accepts ax= to place it on specific axes.
  3. seabornsns.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.

10

When should work happen in the database via SQL versus in pandas, and how do SQL ops map to pandas?

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.

Start studying

Keep going

RecallDeck Interview Library

Detailed answers from the same curated interview deck, organized for search, study, and durable recall.

RSS