Selection • Cleaning • GroupBy • Merge & Reshape • Time Series • Performance • 2026

Pandas Interview Questions

30 questions What each one tests, an answer frame, a spoken answer 30 min read

This page is for analysts, data scientists and engineers facing a pandas round, whether it's a live notebook exercise or a spoken technical interview. Most rounds start with Series, DataFrames and loc versus iloc, then move to cleaning messy columns, groupby, merges and reshaping. Stronger rounds test dates and time series, why apply is slow, how to handle files bigger than memory and the SettingWithCopyWarning trap. Every question has short code you should be able to type from memory, plus a spoken answer. Run the snippets on a small table of your own, then practise saying the answers out loud.

Search all questions by round, difficulty and level, or save the ones you want to practise.

Core Objects 3 questions

Easy Technical round Fresher Practice question

1. What is the difference between a Series and a DataFrame, and how are they related?

What the interviewer is really testing:
Whether you picture a DataFrame as labelled columns sharing one index, which explains most pandas behaviour you will meet later.
Answer frame:

Series: one-dimensional, one dtype, values plus an index of labels.

DataFrame: two-dimensional; each column is a Series and all columns share the same row index.

In practice: one bracket returns a Series, a list of columns returns a DataFrame.

Sample spoken answer:

"A Series is a one-dimensional labelled array. It has values of a single dtype and an index that gives each value a label. A DataFrame is a table, and the easiest way to think of it is a collection of Series that all share the same row index, one Series per column. Each column can have its own dtype, so I can have text, integers and dates side by side. That's also why selection behaves the way it does: df['amount'] gives me a Series, while df[['amount']] with a list gives me a DataFrame with one column. It matters because some methods only exist on one of them, like the .str and .dt accessors, which live on Series."

Code:
import pandas as pd

df = pd.DataFrame({'name': ['Ana', 'Ben'], 'amount': [120, 80]})
type(df['amount'])    # Series
type(df[['amount']])  # DataFrame with one column
df['amount'].index.equals(df.index)  # True: columns share the row index
Red flag to avoid:

Describing a DataFrame as just a list of lists, with no mention of the shared index or per-column dtypes.

They may ask next:
  • Can a single DataFrame column hold values of different types, and what dtype would it get?
  • What is the index for, and when would you set a column as the index?
Say it in 60 seconds
Medium Technical round Fresher, Mid-level Practice question

2. You add two Series together and get NaN in places you didn't expect. What happened?

What the interviewer is really testing:
Whether you know pandas matches values by index label, not by position, which quietly changes the result of arithmetic and assignment.
Answer frame:

Alignment: arithmetic lines values up by index label, not by position.

Result: the output index is the union of both; a label found in only one side gives NaN.

Fix: use add with fill_value, or reset the index if position is what you meant.

Sample spoken answer:

"Pandas aligns on the index before it does any arithmetic. So when I add two Series, it matches values by label, not by position, and the result has the union of both indexes. Any label that exists in only one of them has nothing to add to, so it comes out as NaN. This often happens after filtering or sorting one of the Series, because the labels no longer line up the way the rows look on screen. If missing labels should count as zero, I use s1.add(s2, fill_value=0). If I really wanted positional addition, I reset both indexes first. The same rule applies when you assign a Series into a DataFrame column: it lines up by index, which surprises people."

Code:
s1 = pd.Series([10, 20, 30], index=['a', 'b', 'c'])
s2 = pd.Series([1, 2, 3], index=['b', 'c', 'd'])
s1 + s2                   # a NaN, b 21, c 32, d NaN
s1.add(s2, fill_value=0)  # a 10, b 21, c 32, d 3
Red flag to avoid:

Saying pandas adds row by row like a list, or blaming the NaN on bad input data without mentioning the index.

They may ask next:
  • You assign a filtered Series back into a DataFrame column. What happens to the rows that were filtered out?
  • When would alignment by label be exactly what you want?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

3. What is method chaining in pandas, and how do assign and pipe help you write it?

What the interviewer is really testing:
Whether you can write readable, step-by-step transformations without piles of temporary variables or in-place edits.
Answer frame:

Chain: each method returns a new DataFrame, so steps read top to bottom inside parentheses.

assign: adds columns; a lambda receives the frame at that point in the chain.

pipe: drops your own function into the chain.

Debug: comment out lines from the bottom, or pipe in a step that prints the shape.

Sample spoken answer:

"Method chaining means writing a transformation as one expression, where each step returns a new DataFrame and the next method runs on it. I wrap it in parentheses and put one step per line, so it reads like a recipe: load, filter, add columns, group, sort. assign is what makes it work for new columns. If I pass a lambda, it receives the DataFrame as it is at that point in the chain, so I can use columns created earlier in the chain. pipe lets me slot in my own function, like a cleaning step I reuse. The benefit is no half-finished intermediate variables and no inplace calls. The cost is debugging, so I comment out lines from the bottom, or pipe in a small function that prints the shape."

Code:
monthly = (
    pd.read_csv('orders.csv', parse_dates=['order_date'])
      .query('status == "complete"')
      .assign(month=lambda d: d['order_date'].dt.to_period('M'),
              net=lambda d: d['amount'] - d['discount'])
      .groupby('month', as_index=False)['net'].sum()
      .sort_values('month')
)
Red flag to avoid:

Writing a fifty-step chain with no way to inspect the middle, or not knowing why assign needs a lambda there.

They may ask next:
  • Why do many pandas users avoid inplace=True?
  • How would you write a clean_columns function and use it with pipe?
Say it in 60 seconds

Selection & Indexing 3 questions

Easy Technical round Fresher, Mid-level Practice question

4. What's the difference between loc and iloc? Give me a case where they return different rows.

What the interviewer is really testing:
Whether you separate labels from positions, and know the slicing difference that causes off-by-one bugs.
Answer frame:

loc: selects by label, accepts boolean masks, and label slices include the end.

iloc: selects by integer position, and slices exclude the end like normal Python.

Trap: after sorting or filtering, label 0 is no longer the first row.

Sample spoken answer:

"loc works with labels, so row index labels and column names, and it also takes a boolean mask. iloc works with integer positions, like a list. Slicing is different too: df.loc[2:4] includes label 4, while df.iloc[2:4] stops before position 4. The case where people get burned is after a sort or a filter. The default index keeps its original labels, so after sorting by amount, df.loc[0] is still the row that was originally first, while df.iloc[0] is the row that's now on top. My habit is loc whenever I'm selecting rows by a condition or columns by name, and iloc only when I truly mean position, like taking the first row of each sorted group."

Code:
df = pd.DataFrame({'amount': [50, 10, 30]})
sorted_df = df.sort_values('amount')
sorted_df.loc[0, 'amount']  # 50: the row labelled 0
sorted_df.iloc[0, 0]        # 10: the row now in first position
df.loc[df['amount'] > 20, 'amount']  # mask plus column name
Red flag to avoid:

Saying they are interchangeable on a default index, without noticing that sorting or filtering breaks that.

They may ask next:
  • What do df.loc[1:3] and df.iloc[1:3] return on a default RangeIndex?
  • How would you select the last three rows and the first two columns?
Say it in 60 seconds
Easy Coding round Fresher Practice question

5. Filter a DataFrame to orders from the north or west regions with an amount over 100. Why can't you use Python's and here?

What the interviewer is really testing:
Whether you can write a boolean mask correctly and know why element-wise operators and parentheses are needed.
Answer frame:

Mask: each condition returns a boolean Series; combine them with & and |, negate with ~.

Parentheses: & binds tighter than comparisons, so each condition needs its own brackets.

Why not and: and asks for one True or False for the whole Series, which pandas refuses as ambiguous.

Sample spoken answer:

"I build one boolean Series per condition and combine them. For the region I use isin with a list, which is cleaner than chaining equals checks with or. Then I combine that with the amount condition using the ampersand, and each condition goes in its own parentheses, because the ampersand binds more tightly than the greater-than sign. Python's and doesn't work because it tries to turn the whole Series into a single True or False, and pandas raises a ValueError saying the truth value of a Series is ambiguous. For readability with many conditions, query is a nice alternative, and for a not condition I use the tilde."

Code:
mask = df['region'].isin(['north', 'west']) & (df['amount'] > 100)
big_orders = df[mask]

# same thing with query
big_orders = df.query("region in ['north', 'west'] and amount > 100")

# everything except those rows
rest = df[~mask]
Red flag to avoid:

Writing df[df.a > 1 and df.b < 5] and not recognising the error it throws.

They may ask next:
  • How would you filter rows where a text column contains a word, ignoring case and missing values?
  • How do you use a Python variable inside query?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

6. What causes SettingWithCopyWarning, and how do you write the assignment so it's clearly correct?

What the interviewer is really testing:
Whether you understand chained assignment and views versus copies well enough to avoid updates that silently go nowhere.
Answer frame:

Cause: chained indexing like df[mask]['col'] = x; the first step may give a copy, so the write may not reach df.

Fix in place: one loc call with the row mask and the column together.

Separate subset: if you want a new table, take .copy() explicitly.

Copy-on-Write: the default from pandas 3.0; every selection acts as a copy, so chained assignment never edits the original, and a ChainedAssignmentError warning replaces the old one.

Sample spoken answer:

"The warning comes from chained assignment, like df[df.amount > 100]['flag'] = True. That's two steps: the first selection builds a new object that may or may not share data with df, and the second sets a value on it. If it's a copy, the original never changes, and the warning is the only hint. The fix depends on what I meant. To change the original, I use one loc call with the mask and the column together. If I want a separate subset to work on, I call .copy() when I create it, so it's clearly independent. From pandas 3.0, Copy-on-Write is the default: every selection behaves like a copy, so chained assignment never updates the original, and the old warning is replaced by a ChainedAssignmentError warning. One loc call is correct in every version."

Code:
# chained: sets the value on a temporary copy, df is unchanged
df[df['amount'] > 100]['flag'] = True

# clear: one step, updates df
df.loc[df['amount'] > 100, 'flag'] = True

# clear: an independent subset on purpose
big = df[df['amount'] > 100].copy()
big['flag'] = True
Red flag to avoid:

Silencing the warning globally, or saying it's harmless noise, without knowing whether the original data was updated.

They may ask next:
  • Why does the warning sometimes appear on a line that looks like a single assignment?
  • What changed for your code when pandas made Copy-on-Write the default?
Say it in 60 seconds

Loading & Cleaning 4 questions

Easy Technical round Fresher, Mid-level Practice question

7. How do you find missing values in a DataFrame, and what are your options once you've found them?

What the interviewer is really testing:
Whether you know the pandas tools for missing data and their side effects, not just that dropna exists.
Answer frame:

Find: isna().sum() per column; NaN, None and NaT all count; never compare with == NaN.

Drop: dropna with subset or thresh, so you only drop what the analysis can't use.

Fill: fillna with a value or a dict per column, or ffill for ordered data.

Side effects: an int column with gaps turns float unless you use the nullable Int64 dtype.

Sample spoken answer:

"First I measure: df.isna().sum() gives the count per column, and dividing by the length gives the share. isna catches NaN, None and NaT for dates, and I never test with double equals, because NaN isn't equal to itself. Then I decide per column. If a row is useless without a key field, I drop with dropna and a subset, rather than dropping any row with any gap. If a sensible default exists, I fillna with a dict so each column gets its own value. For time-ordered data, forward fill can make sense. Two things I keep in mind: an integer column with a missing value becomes float unless I use the nullable Int64 dtype, and sum and mean skip missing values by default, so a mean may cover fewer rows than I think."

Code:
df.isna().sum()                        # count per column
df.isna().mean().sort_values()         # share per column
clean = df.dropna(subset=['customer_id'])
clean = clean.fillna({'discount': 0, 'channel': 'unknown'})
clean['visits'] = clean['visits'].astype('Int64')  # keeps gaps as <NA>
Red flag to avoid:

Reaching for df.dropna() on the whole table as the default, without checking how many rows it throws away.

They may ask next:
  • What's the difference between count and size after a groupby when there are missing values?
  • When would filling with the mean make a model or a report worse?
Say it in 60 seconds
Medium Coding round Fresher, Mid-level Practice question

8. A customer table has several rows per customer_id from repeated updates. In pandas, how do you find the duplicates and keep only the newest row for each?

What the interviewer is really testing:
Whether you can deduplicate on a key with a clear rule for which row survives, instead of dropping duplicates blindly.
Answer frame:

Inspect: duplicated with keep=False shows every row involved, so you see what differs.

Rule: sort by the timestamp, then drop_duplicates on the key keeping the last.

Check: the result has one row per key, and exact duplicates are a separate question.

Sample spoken answer:

"First I look before deleting anything. duplicated on customer_id with keep=False marks every row that shares a key, so I can see whether they differ in real fields or are exact copies. Then I need a rule for which row wins, and here it's the newest. I sort by updated_at and call drop_duplicates on customer_id with keep='last', so the latest row survives. An alternative is idxmax on the timestamp per group, which gives the index of the newest row. Afterwards I check that customer_id is unique. Without the subset argument, drop_duplicates only removes rows that match on every column, which usually isn't what the business means by a duplicate customer."

Code:
dups = df[df.duplicated('customer_id', keep=False)]
print(dups.sort_values(['customer_id', 'updated_at']).head())

latest = (df.sort_values('updated_at')
            .drop_duplicates('customer_id', keep='last'))
assert latest['customer_id'].is_unique

# alternative
latest2 = df.loc[df.groupby('customer_id')['updated_at'].idxmax()]
Red flag to avoid:

Calling drop_duplicates() with no subset and no sort, then trusting that the right row was kept.

They may ask next:
  • Two rows have the same customer_id and the same updated_at. Which one survives, and how would you make that deterministic?
  • How would you find customers that look like duplicates because of different spacing or capitalisation in the email?
Say it in 60 seconds
Medium Coding round Fresher, Mid-level Practice question

9. A price column came in as text, with values like ' 1,200 ', 'N/A' and blanks. How do you turn it into numbers without losing track of the bad rows?

What the interviewer is really testing:
Whether you clean text with the vectorised .str methods and convert safely, while still reporting what could not be parsed.
Answer frame:

Clean text: .str.strip and .str.replace to remove spaces and thousands separators.

Convert: pd.to_numeric with errors='coerce' turns anything unparseable into NaN.

Audit: list the raw values that became NaN but weren't blank, before trusting the column.

Sample spoken answer:

"I'd clean the text first with the .str methods, which work on the whole column at once: strip the spaces and remove the commas. Then pd.to_numeric with errors set to coerce converts everything it can and turns the rest into NaN. The step people skip is the audit. I compare the converted column with the raw one and pull out values that became NaN but weren't empty to start with. That tells me whether it's just 'N/A' placeholders, or something like a currency code or a range I should handle properly. Only then do I replace the column. I don't call astype(int) directly, because it fails on the first bad value and can't hold NaN anyway."

Code:
raw = df['price']
clean = pd.to_numeric(
    raw.astype(str).str.strip().str.replace(',', '', regex=False),
    errors='coerce',
)
bad = raw[clean.isna() & raw.notna() & (raw.astype(str).str.strip() != '')]
print(bad.value_counts().head(10))  # what failed, and how often
df['price'] = clean
Red flag to avoid:

Using errors='coerce' and moving on, so real data problems quietly turn into missing values nobody reviews.

They may ask next:
  • If read_csv is the source, which option would have handled the commas at load time?
  • How would you pull the number out of values like '12 kg' using a regular expression?
Say it in 60 seconds
Easy Technical round Fresher, Mid-level Practice question

10. You've been handed a CSV export. Which read_csv options do you reach for so it loads correctly the first time?

What the interviewer is really testing:
Whether you control types and parsing at load time, instead of loading everything as guesses and fixing it later.
Answer frame:

Peek: nrows to look at a sample, and check the separator and encoding.

Types: dtype for IDs and codes as strings, so leading zeros survive; parse_dates for dates.

Values: na_values for placeholders like 'N/A', thousands for separators, usecols to skip what you don't need.

Sample spoken answer:

"I start by reading a few rows with nrows to see the real shape, then set the options on purpose. Anything that looks like a number but is really a code, such as an ID or a postal code, I load as a string with dtype, otherwise leading zeros disappear and long IDs can get mangled. Date columns go in parse_dates. If the export uses placeholders like 'N/A' or a dash for missing, I list them in na_values, and if numbers have thousands separators I set thousands to a comma. If the file is wide, usecols loads only what I need, which saves memory and time. And if I get a decode error, I check the encoding and the separator before anything else."

Code:
df = pd.read_csv(
    'customers.csv',
    usecols=['id', 'postcode', 'signup_date', 'spend'],
    dtype={'id': 'string', 'postcode': 'string'},
    parse_dates=['signup_date'],
    na_values=['N/A', '-'],
    thousands=',',
)
df.dtypes
Red flag to avoid:

Loading with defaults and then being surprised that postcodes lost their leading zeros or dates stayed as text.

They may ask next:
  • The file has a few header lines of notes before the real column names. How do you skip them?
  • Why might you save the cleaned data as Parquet instead of writing it back to CSV?
Say it in 60 seconds

GroupBy & Aggregation 3 questions

Easy Coding round Fresher, Mid-level Practice question

11. Show me orders, total revenue and average order value per region in one groupby, with clear column names.

What the interviewer is really testing:
Whether you can write a clean multi-metric groupby and know what split, apply, combine means in practice.
Answer frame:

Split: groupby on the key column.

Apply: named aggregation, each output as name=(column, function).

Derive: ratios such as average order value come from the aggregated columns, not a mean of lines.

Combine: reset_index or as_index=False to get a flat table back.

Sample spoken answer:

"groupby splits the rows by region, applies a function to each group and combines the results into one table. For several metrics with readable names, I use named aggregation, where each output is written as a name equal to a pair of source column and function. Orders is nunique of order_id, because one order can have several lines, and revenue is the sum of amount. Average order value is then revenue divided by orders, which I add with assign. Taking the mean of amount would give the average line, not the average order, which is an easy slip. Named aggregation also avoids the multi-level column names you get from a dict of lists, and reset_index makes region a normal column again."

Code:
summary = (
    df.groupby('region')
      .agg(orders=('order_id', 'nunique'),
           revenue=('amount', 'sum'))
      .assign(avg_order=lambda d: d['revenue'] / d['orders'])
      .reset_index()
      .sort_values('revenue', ascending=False)
)
Red flag to avoid:

Looping over df['region'].unique() and filtering the table once per region instead of using groupby.

They may ask next:
  • What happens to rows whose region is missing, and how would you keep them as their own group?
  • How would you add a column showing each region's share of total revenue?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

12. What's the difference between agg, transform and filter on a groupby? Show one use of each.

What the interviewer is really testing:
Whether you know the shape each one returns, which is what decides whether the result can go straight back into the original table.
Answer frame:

agg: one row per group, such as a total per category.

transform: same length and index as the input, so it can be assigned back as a column.

filter: keeps or drops whole groups based on a condition about the group.

apply: most flexible, slowest; use it only when the others can't express it.

Sample spoken answer:

"The difference is the shape of what comes back. agg reduces each group to one row, so I get one line per category. transform returns something the same length as the original, aligned to its index, so I can assign it straight back as a new column. That's how I fill missing prices with the median price of their own category, or compute each order's share of its region's total. filter works on whole groups: it keeps every row of groups that pass a test, like customers with at least three orders, and drops the rest. apply can return anything, but it calls Python once per group, so I only use it when agg, transform or filter can't do the job."

Code:
per_cat = df.groupby('category')['price'].agg('median')

cat_median = df.groupby('category')['price'].transform('median')
df['price'] = df['price'].fillna(cat_median)
df['share'] = df['amount'] / df.groupby('region')['amount'].transform('sum')

regulars = df.groupby('customer_id').filter(lambda g: len(g) >= 3)
Red flag to avoid:

Computing a group total with agg and then trying to assign it back to the original rows, getting misaligned NaNs.

They may ask next:
  • How would you write the filter example without a lambda, using transform?
  • Why is transform('sum') usually faster than transform(lambda x: x.sum())?
Say it in 60 seconds
Medium Coding round Fresher, Mid-level Practice question

13. Given order lines, find the top three products by revenue within each region.

What the interviewer is really testing:
Whether you can combine aggregation, sorting and a per-group cut, and think about ties.
Answer frame:

Aggregate: revenue per region and product first.

Rank: sort by region and revenue descending, then head(3) per group.

Ties: use rank with method='dense' if equal revenues should share a place.

Sample spoken answer:

"I first aggregate to one row per region and product with the summed revenue, because the raw data has many lines per product. Then I sort by region, and by revenue from highest to lowest, and take groupby region head three. head keeps the first rows of each group in their current order, so the sort does the ranking. If ties matter, like two products with the same revenue in third place, I'd use rank with method dense, descending, within each region, and keep ranks up to three. That can return more than three rows for a region, which is the honest answer, and I'd say so to whoever asked."

Code:
rev = df.groupby(['region', 'product'], as_index=False)['amount'].sum()

top3 = (rev.sort_values(['region', 'amount'], ascending=[True, False])
           .groupby('region')
           .head(3))

# tie-aware version
rev['rk'] = rev.groupby('region')['amount'].rank(method='dense', ascending=False)
top3_ties = rev[rev['rk'] <= 3]
Red flag to avoid:

Taking the overall top three products and then splitting them by region, which answers a different question.

They may ask next:
  • How would you also show each product's share of its region's revenue?
  • What's the difference between rank methods min, dense and first?
Say it in 60 seconds

Combining & Reshaping 5 questions

Easy Technical round Fresher, Mid-level Practice question

14. When do you use merge, join and concat in pandas?

What the interviewer is really testing:
Whether you know which tool matches rows on keys and which one simply stacks tables.
Answer frame:

merge: SQL-style join on one or more columns, with how set to inner, left, right or outer; inner by default.

join: a DataFrame method that joins on the index by default, left join by default.

concat: stacks tables along rows or columns, lining up by column names or index, no key matching.

Sample spoken answer:

"merge is the general SQL-style join. I give it the key columns with on, or left_on and right_on if the names differ, and choose how: inner is the default, and left, right and outer are the others. join is a convenience method on a DataFrame that matches on the index by default and does a left join, so it's handy when both tables are already indexed by the key. concat doesn't match keys at all. It stacks tables, by default one under the other, lining up columns by name, so it's what I use for monthly files with the same layout. If the columns don't match, concat fills the gaps with NaN, which is a quick way to spot a renamed column."

Code:
orders_full = orders.merge(customers, on='customer_id', how='left')

by_id = orders.set_index('customer_id').join(customers.set_index('customer_id'))

all_months = pd.concat([jan, feb, mar], ignore_index=True)
Red flag to avoid:

Using concat with axis=1 to combine two tables that should have been merged on a key, silently pairing unrelated rows.

They may ask next:
  • What does ignore_index do in concat, and when do you need it?
  • How do you merge when the key column has different names in the two tables?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

15. After a left merge, your table has more rows than before. Why, and how do you guard against it?

What the interviewer is really testing:
Whether you have debugged a real join problem and know the checks that stop duplicate keys inflating totals.
Answer frame:

Cause: the key repeats in the right table, so each left row matches several rows.

Guard: validate='many_to_one' raises if the right key isn't unique; assert the row count.

Diagnose: indicator=True shows what matched; also check key dtypes, spaces and case.

Sample spoken answer:

"A left merge keeps every left row, but if the key appears more than once in the right table, each left row is repeated once per match. So the row count grows and any sum over the merged table is inflated. I find it by checking which keys are duplicated in the right table. To guard against it, I pass validate, for example many_to_one when I expect each order to match one customer. pandas then raises an error instead of quietly duplicating. I also assert the row count didn't change, and use indicator=True to count matched and unmatched rows. The opposite surprise, rows not matching, usually comes from keys that differ in dtype, stray spaces or case, so I normalise keys before the merge."

Code:
print(customers['customer_id'].duplicated().sum())  # repeated keys?

before = len(orders)
out = orders.merge(customers, on='customer_id', how='left',
                   validate='many_to_one', indicator=True)
assert len(out) == before
print(out['_merge'].value_counts())  # both / left_only
Red flag to avoid:

Fixing the inflated count by calling drop_duplicates after the merge, without understanding which rows were duplicated or why.

They may ask next:
  • The right table legitimately has several rows per key. How do you merge without inflating the totals?
  • What happens if one table stores the key as an integer and the other as text?
Say it in 60 seconds
Medium Coding round Fresher, Mid-level Practice question

16. Find the customers who have never placed an order, given a customers table and an orders table.

What the interviewer is really testing:
Whether you can express an anti-join in pandas, which has no direct method for it.
Answer frame:

Simple: isin on the key, negated with ~.

Explicit: left merge with indicator=True and keep left_only.

Care: deduplicate the right keys first so the merge stays one row per customer.

Sample spoken answer:

"pandas doesn't have an anti-join method, so there are two common ways. The quickest is isin: take the customers whose ID is not in the orders table's customer_id column, using the tilde to negate. The more explicit way is a left merge with indicator set to True, then keeping rows where the merge column says left_only. I like the merge version when I also want to report how many customers matched, and I deduplicate the order keys first so the merge doesn't repeat customers. Either way, I'd check the key dtypes match, because an integer ID in one table and a text ID in the other would make every customer look like they never ordered."

Code:
no_orders = customers[~customers['customer_id'].isin(orders['customer_id'])]

keys = orders[['customer_id']].drop_duplicates()
m = customers.merge(keys, on='customer_id', how='left', indicator=True)
no_orders2 = m[m['_merge'] == 'left_only'].drop(columns='_merge')
Red flag to avoid:

Looping over customers and checking each ID against the orders table one by one.

They may ask next:
  • How would you find customers whose last order was more than 90 days ago?
  • How would you get customers who ordered in January but not in February?
Say it in 60 seconds
Medium Technical round Fresher, Mid-level Practice question

17. What's the difference between pivot and pivot_table, and why does pivot sometimes throw an error about duplicate entries?

What the interviewer is really testing:
Whether you know that pivot only reshapes while pivot_table aggregates, and can read the duplicate-entries error correctly.
Answer frame:

pivot: pure reshape; each index and column pair must appear once.

pivot_table: aggregates duplicate pairs with aggfunc, which defaults to the mean.

Extras: fill_value for empty cells, margins for totals.

Sample spoken answer:

"pivot only reshapes. It takes one column for the new rows, one for the new columns and one for the values, and every row and column pair must appear exactly once. If the data has two rows for the same region and month, pivot doesn't know which value to put in the cell, so it raises an error about duplicate entries. pivot_table handles that by aggregating: I choose aggfunc, and if I don't, it takes the mean, which is a classic surprise when someone expected totals. It also has fill_value for combinations with no data, and margins to add totals. So for a sales-by-region-and-month report I use pivot_table with aggfunc set to sum."

Code:
report = df.pivot_table(
    index='region',
    columns='month',
    values='amount',
    aggfunc='sum',
    fill_value=0,
)
Red flag to avoid:

Using pivot_table without setting aggfunc and reporting averages as if they were totals.

They may ask next:
  • How would you turn this wide report back into one row per region and month?
  • How does pivot_table relate to a groupby followed by unstack?
Say it in 60 seconds
Medium Coding round Fresher, Mid-level Practice question

18. Sales arrive in a sheet with one column per month. How do you turn that into one row per store per month, and back again?

What the interviewer is really testing:
Whether you can move between wide and long layouts, and know why long data is easier to group, filter and plot.
Answer frame:

Wide to long: melt with id_vars for the columns to keep, var_name and value_name for the new columns.

Why long: one row per observation makes groupby, filtering and plotting simple.

Back to wide: pivot with the same three columns.

Sample spoken answer:

"I'd use melt. The store column stays as it is, so it goes in id_vars, and every month column gets folded into two new columns: month, holding the old column name, and sales, holding the value. Now each row is one store in one month. That long layout is what most pandas work wants, because I can group by month, filter to a quarter, or feed it to a plotting library without listing column names. To go back to the wide layout for a report, I pivot with store as the index, month as the columns and sales as the values. After melting, I'd usually convert the month text into a real date so it sorts in calendar order, not alphabetically."

Code:
long = wide.melt(id_vars=['store'], var_name='month', value_name='sales')
long['month'] = pd.to_datetime(long['month'], format='%Y-%m')

back = long.pivot(index='store', columns='month', values='sales')
Red flag to avoid:

Building the long table with nested loops over rows and columns instead of melt.

They may ask next:
  • The month columns are mixed with other columns you want to drop. How do you melt only the month ones?
  • How are stack and unstack related to melt and pivot?
Say it in 60 seconds

Performance 3 questions

Medium Technical round Fresher, Mid-level Practice question

19. Why is df.apply with axis=1 slow, and what do you use instead? Rewrite a row-wise apply for me.

What the interviewer is really testing:
Whether you can replace a per-row Python function with whole-column operations, the most common pandas speed fix.
Answer frame:

Why slow: apply with axis=1 calls a Python function once per row and builds a Series each time.

Vectorise: column arithmetic, np.where for two outcomes, np.select for several.

Other tools: .str and .dt accessors, map with a dict for lookups, merge for table lookups.

Sample spoken answer:

"apply with axis=1 looks vectorised but it isn't. It runs a Python function once per row, and builds a small Series for each row to pass in, so on millions of rows most of the time is overhead. Vectorised operations run on whole columns in compiled code. So if I'm labelling orders as high or low, I use np.where with the condition on the whole column. For several bands, np.select takes a list of conditions and a matching list of labels, checked in order, with a default. For text I use the .str methods, for dates the .dt accessor, and for looking values up I use map with a dict or a merge. I keep apply for logic that really can't be expressed on columns, and I measure before and after."

Code:
import numpy as np

# slow
df['band'] = df.apply(lambda r: 'high' if r['amount'] > 1000 else 'low', axis=1)

# vectorised
df['band'] = np.where(df['amount'] > 1000, 'high', 'low')

conds = [df['amount'] > 1000, df['amount'] > 100]
df['tier'] = np.select(conds, ['high', 'mid'], default='low')
Red flag to avoid:

Saying apply is vectorised, or switching from iterrows to apply and calling the problem solved.

They may ask next:
  • How would you vectorise a rule that depends on two columns and a lookup table?
  • When is iterating over rows still reasonable?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

20. A DataFrame takes far more memory than the file it came from. How do you measure that and bring it down?

What the interviewer is really testing:
Whether you know where pandas memory goes, especially text columns, and which dtype changes shrink it safely.
Answer frame:

Measure: df.info(memory_usage='deep') or memory_usage(deep=True) per column.

Text: low-cardinality text such as status or country code becomes category.

Numbers: downcast with pd.to_numeric; float32 only if the precision is acceptable.

At load: usecols and dtype so the big version never exists.

Sample spoken answer:

"I start with memory_usage with deep set to True, because without it pandas can undercount text columns, and text is usually where the memory goes. Then I go column by column. A text column with a few distinct values repeated millions of times, like status or country code, becomes the category dtype, which stores each value once plus small integer codes. Numeric columns default to 64-bit, so I downcast with pd.to_numeric, and use float32 only where the lost precision doesn't matter. The biggest win is often not loading data at all: usecols to skip columns I don't need and dtype at read time. I measure again after each change to see what actually helped."

Code:
print(df.memory_usage(deep=True).sort_values(ascending=False).head())

df['status'] = df['status'].astype('category')
df['qty'] = pd.to_numeric(df['qty'], downcast='integer')

print(df.memory_usage(deep=True).sum() / 1e6, 'MB')
Red flag to avoid:

Converting every text column to category, including unique IDs, without measuring first.

They may ask next:
  • When does converting to category not help, or even make things worse?
  • What happens to a category column when you concat two frames whose categories differ?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

21. You need totals per user from a CSV that's bigger than your machine's memory. How do you do it in pandas?

What the interviewer is really testing:
Whether you can process data in pieces, combine partial results correctly, and know when pandas is the wrong tool.
Answer frame:

Load less: usecols and dtype first; often that alone makes it fit.

Chunks: read_csv with chunksize, aggregate each chunk, then combine the partial results.

Combine correctly: sums and counts add up; a mean needs sum and count kept separately.

Next step: convert once to Parquet, or move to an engine built for out-of-memory work.

Sample spoken answer:

"First I load less. With usecols I read only the user ID and amount, with compact dtypes, and sometimes that alone fits. If not, I read in chunks with chunksize, which gives an iterator of DataFrames. For each chunk I group by user and sum, then add that into a running total with fill_value zero, so users seen in only some chunks still count. The thing to get right is which aggregates combine: sums and counts add up across chunks, but a mean doesn't, so for an average I keep sum and count and divide at the end. If this job runs often, I'd convert the file to Parquet once so later reads can pick columns, or move it to an engine that works out of memory."

Code:
totals = None
reader = pd.read_csv('events.csv', usecols=['user_id', 'amount'],
                     dtype={'user_id': 'int64', 'amount': 'float64'},
                     chunksize=1_000_000)
for chunk in reader:
    part = chunk.groupby('user_id')['amount'].sum()
    totals = part if totals is None else totals.add(part, fill_value=0)

totals = totals.sort_values(ascending=False)
Red flag to avoid:

Averaging the per-chunk averages, or suggesting buying more memory as the only answer.

They may ask next:
  • How would you compute the number of distinct users per day in chunks?
  • Why is a median much harder to compute in chunks than a sum?
Say it in 60 seconds

Dates & Time Series 3 questions

Easy Technical round Fresher, Mid-level Practice question

22. A date column loaded as text in day/month/year format. How do you convert it, and what do you do with values that won't parse?

What the interviewer is really testing:
Whether you parse dates with an explicit format, handle failures visibly, and know the .dt accessor.
Answer frame:

Parse: pd.to_datetime with an explicit format, so 03/04 isn't read the wrong way round.

Failures: errors='coerce' turns bad values into NaT; count and inspect them.

Use: the .dt accessor for year, day name, or month as a period.

Sample spoken answer:

"I use pd.to_datetime and give it the format explicitly. Without a format, a value like 03/04 is ambiguous, and I don't want pandas guessing whether that's March or April. With errors set to coerce, anything that doesn't match becomes NaT, the missing value for dates, instead of stopping the whole conversion. Then I check how many NaTs appeared where the text wasn't empty, and look at a few, because a cluster of failures usually means a second format hiding in the data. Once the column is a real datetime, the .dt accessor gives me year, month, day name or a month period for grouping, and date comparisons and sorting work properly."

Code:
df['order_date'] = pd.to_datetime(df['order_date_raw'],
                                  format='%d/%m/%Y', errors='coerce')
failed = df['order_date'].isna() & df['order_date_raw'].notna()
print(failed.sum(), df.loc[failed, 'order_date_raw'].head())

df['weekday'] = df['order_date'].dt.day_name()
df['month'] = df['order_date'].dt.to_period('M')
Red flag to avoid:

Parsing ambiguous dates without a format and never checking whether days and months were swapped.

They may ask next:
  • How would you filter orders placed in the last 30 days?
  • What changes when timestamps come from systems in different time zones?
Say it in 60 seconds
Medium Coding round Fresher, Mid-level Practice question

23. You have one row per sale with a timestamp. How do you get weekly totals, overall and per store?

What the interviewer is really testing:
Whether you know resample for time-based grouping and pd.Grouper for combining it with another key.
Answer frame:

Overall: set the timestamp as the index, or pass on=, then resample('W').sum().

Per store: groupby with the store and pd.Grouper(key=..., freq='W').

Gaps: resample creates empty weeks too, a sum shows zero and a mean NaN; the Grouper version leaves a store's empty weeks out.

Sample spoken answer:

"resample is groupby for time. It needs a datetime index, or a datetime column passed with on. Resampling to 'W' and summing gives one row per week, and by default those weekly bins end on Sunday and are labelled with that Sunday, which I'd confirm with whoever reads the report. A useful difference from a plain groupby is that resample creates every week in the range, even ones with no sales, so gaps show up as zero for a sum or NaN for a mean instead of just being missing. For weekly totals per store, I use groupby with two keys: the store column and a pd.Grouper on the date with weekly frequency. That gives one row per store per week, though a week with no sales for a store is simply absent, and I reset the index to get a flat table."

Code:
weekly = (df.set_index('sold_at')
            .sort_index()['amount']
            .resample('W')
            .sum())

per_store = (df.groupby(['store', pd.Grouper(key='sold_at', freq='W')])['amount']
               .sum()
               .reset_index())
Red flag to avoid:

Grouping by the week number alone, which merges the same week from different years.

They may ask next:
  • Your week should run Monday to Sunday and be labelled by its Monday. How do you change that?
  • How would you turn weekly data into daily data by carrying values forward?
Say it in 60 seconds
Hard Coding round Mid-level, Senior Practice question

24. For each store, show how much daily sales changed from the day before. What goes wrong with a plain shift?

What the interviewer is really testing:
Whether you shift within groups, in the right order, and notice that the previous row is not always the previous day.
Answer frame:

Order: sort by store and date first; shift works on row order.

Group: groupby('store') then shift, so one store's value never leaks into the next.

Gaps: a missing day makes the previous row an older date; reindex to a full daily range if that matters.

Sample spoken answer:

"shift moves values down by one row, so the change is the value minus the shifted value. A plain df['sales'].shift() has two problems. It works on row order, so if the data isn't sorted by date I'm comparing random days. And across stores, the first row of store B would pick up the last value of store A. So I sort by store and date, then do groupby store and shift, which restarts the shift for each store and leaves the first day as NaN. The subtler issue is gaps: if a store was closed on a Tuesday, Wednesday gets compared with Monday. If the question really means yesterday, I reindex each store to a full daily range first, or I compare on dates rather than rows."

Code:
df = df.sort_values(['store', 'date'])
prev = df.groupby('store')['sales'].shift(1)
df['change'] = df['sales'] - prev
df['growth'] = df['sales'] / prev - 1

# same as the row before, not necessarily yesterday:
df['gap_days'] = df.groupby('store')['date'].diff().dt.days
Red flag to avoid:

Using df['sales'].shift() on the whole unsorted table and never checking the first row of each store.

They may ask next:
  • How would you compute a seven-day rolling average per store?
  • How do you handle a previous value of zero when calculating growth?
Say it in 60 seconds

Real Work 3 questions

Hard Behavioral round Mid-level, Senior Practice question

25. Tell me about a time a pandas analysis gave a number that turned out to be wrong. How did you find the cause?

What the interviewer is really testing:
Whether you check your own numbers against a known source, trace the cause to a specific step, and put guards in so it doesn't happen again.
Answer frame:

The number: what was reported and how the error came to light.

The trace: how you narrowed it to one step, such as checking totals before and after each merge.

The guard: the fix plus the checks you added so it can't recur silently.

Sample spoken answer:

"At my last company I built a monthly revenue-by-segment report, and the finance lead said one segment looked too high against their system. I reconciled step by step: the raw orders total matched finance, but after I merged in the customer table, the total went up. The customer table had duplicate IDs for a few hundred customers after a system migration, so those customers' orders were counted twice. I fixed the source by keeping the newest customer record, and in the code I added validate set to many_to_one on the merge plus an assertion that the row count and revenue total don't change after joining. Since then I reconcile the headline total against a trusted source before I share any new report."

Red flag to avoid:

A story where the error was found by someone else and fixed by patching the final number, with no root cause or safeguard.

They may ask next:
  • What checks do you now run before you share a new analysis?
  • How did you tell the finance lead, and what happened to the earlier reports?
Say it in 60 seconds
Medium Behavioral round Fresher, Mid-level, Senior Practice question

26. Tell me about a slow pandas script you made much faster. How did you find where the time was going?

What the interviewer is really testing:
Whether you measure before optimising and know the typical fixes, instead of rewriting everything in a new tool.
Answer frame:

Context: what the script did and why the time mattered.

Measure: timing steps on a sample to find the slowest part.

Fix and result: the specific change, like replacing apply with vectorised code, and the before and after.

Sample spoken answer:

"At my last company we had a daily script that calculated a fee for each transaction and it took about forty minutes. Instead of guessing, I timed each step on a sample of a hundred thousand rows. Almost all the time was in one apply with axis=1 that looked up a rate from a dictionary and then used if and else on three columns. I replaced the lookup with a merge against a small rates table, and the if and else chain with np.select. The script went from forty minutes to under two, and I compared the old and new outputs row for row before switching it over."

Red flag to avoid:

Claiming a big speed-up without having measured where the time went, or without checking the results still matched.

They may ask next:
  • How did you prove the new version gave exactly the same results?
  • What would you have done if vectorising hadn't been enough?
Say it in 60 seconds
Medium Behavioral round Fresher, Mid-level Practice question

27. Tell me about a data cleaning decision, like dropping or filling missing values, that you had to explain to someone non-technical.

What the interviewer is really testing:
Whether you treat cleaning choices as decisions that change the answer, and can explain them in plain words.
Answer frame:

The gap: what was missing, and how much of the data it touched.

The choice: the options, what each would do to the result, and which you picked.

The explanation: how you put it in plain words, and what the person decided.

Sample spoken answer:

"In one analysis of customer survey data, about a fifth of the responses had no age. The marketing manager wanted results by age group. If I dropped those rows, the overall satisfaction score would shift, because the people skipping the age question were mostly newer customers. If I filled ages with an average, I'd be inventing data. I showed the manager a small table: the score with all responses, and the score with only responses that had an age. Then I explained in one sentence that dropping changes who we're describing. We agreed to keep everyone in the overall score, show the age breakdown with an 'age not given' group, and add a note on the chart."

Red flag to avoid:

Saying you always drop missing rows, or that the stakeholder didn't need to know how the data was cleaned.

They may ask next:
  • How would your approach change if this data were feeding a model instead of a report?
  • How do you record cleaning decisions so the next person understands them?
Say it in 60 seconds

Judgement Calls 3 questions

Medium Situational round Fresher, Mid-level Practice question

28. A report is due in an hour, and you notice to_datetime with errors='coerce' turned a noticeable chunk of the date column into NaT. What do you do?

What the interviewer is really testing:
Whether you size a data problem quickly and choose between fixing and flagging, rather than silently shipping or silently dropping.
Answer frame:

Size it: count the failures and look at examples; often it's one second format.

Quick fix: parse the second format and fill the gaps, then re-check.

If not fixable: ship with the affected rows counted and flagged, and say what it changes.

Sample spoken answer:

"First I'd spend five minutes sizing it. I count the rows that became NaT but had text, and look at the most common raw values. Very often it's a second date format from one source, like year first instead of day first. If so, I parse those with their own format, fill the gaps, and check the failure count drops to near zero. That's usually quicker than it sounds. If it's something I can't fix in time, I don't drop those rows quietly, because they'd vanish from the totals. I'd send the report on time with a clear note: how many rows had unreadable dates, which figures they affect, and when I'll send the corrected version."

Code:
failed = df['sold_at'].isna() & df['sold_at_raw'].notna()
print(failed.sum(), df.loc[failed, 'sold_at_raw'].value_counts().head())

alt = pd.to_datetime(df['sold_at_raw'], format='%Y-%m-%d', errors='coerce')
df['sold_at'] = df['sold_at'].fillna(alt)
Red flag to avoid:

Dropping the NaT rows without telling anyone, or missing the deadline to chase a perfect fix.

They may ask next:
  • The manager says to just drop those rows and send it. What do you say?
  • How would you stop this from happening next month?
Say it in 60 seconds
Medium Situational round Mid-level, Senior Practice question

29. You inherit a long notebook with no tests that produces a monthly revenue figure, and you're asked to change one business rule. How do you do it safely?

What the interviewer is really testing:
Whether you protect a trusted number while changing code you didn't write, using a baseline and a real comparison.
Answer frame:

Baseline: restart and run all, and save the current output before touching anything.

Isolate: find where the rule lives and move it into a small function you can check alone.

Compare: diff new against old; only rows touched by the rule should change, and you can explain each.

Sample spoken answer:

"Before changing anything, I'd restart the kernel and run it top to bottom, because notebooks often only work thanks to cells run out of order. If it reproduces last month's figure, I save that output as my baseline. Then I find where the rule is applied and pull that logic into a small function, so I can test it on a few hand-made rows. After the change, I rerun and compare against the baseline. I expect differences only in the rows the rule touches, so I check which rows changed and that I can explain each one. pd.testing.assert_frame_equal is handy to prove everything else is identical. Finally, I'd write down the rule change and the difference in the headline number for whoever signs it off."

Red flag to avoid:

Editing the rule, rerunning once and sending the new number because it looks reasonable.

They may ask next:
  • The notebook doesn't reproduce last month's figure even before your change. What now?
  • What would you change about this notebook if you owned it long term?
Say it in 60 seconds
Hard Situational round Senior Practice question

30. A nightly pandas job has started running out of memory as the data grows, and the team wants to rewrite it in a distributed framework. What's your call?

What the interviewer is really testing:
Whether you base a costly rewrite on measurement and growth, and try cheap fixes first without dismissing the long-term need.
Answer frame:

Measure: where memory peaks and how fast the input is growing.

Cheap fixes: load fewer columns, tighter dtypes, filter early, Parquet input, drop intermediates.

Decide: if the job will still outgrow one machine soon, plan the move with tests; if not, keep it simple.

Sample spoken answer:

"I'd hold the rewrite until we've measured. First I'd find where memory peaks. It's often one step, like a merge that multiplies rows or text columns loaded as full strings. Then I'd try the cheap fixes: read only the needed columns with tight dtypes, filter as early as possible, switch the input from CSV to Parquet, and delete big intermediate frames once used. Those often cut memory by a large factor in a day's work. Then I'd look at the growth trend. If the data will outgrow a single machine within months even after that, a distributed rewrite is the right call, and I'd plan it with the current output as the test baseline. If not, a rewrite adds cost and complexity for nothing."

Red flag to avoid:

Either rewriting straight away without measuring, or refusing to consider it without looking at how fast the data is growing.

They may ask next:
  • What would you need to see in the numbers to agree the rewrite is worth it?
  • How would you keep the nightly job running while the rewrite is in progress?
Say it in 60 seconds
Were you asked something else? Share it A person checks every question before it goes on the site. No name is shown.
For the call itself

The questions above are the prep. The call has ten more.

ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your resume and notes are never stored on our servers. It stays out of screen share on every plan; only you can see it.

Download ClapAssist with 10 free minutes
Mac and Windows · Stays out of screen share · No card