Quick Topic Map
Read & InspectQ1
Filter & BooleanQ2
groupby + aggQ3, Q12
Missing valuesQ4
String opsQ5
Datetime / ISTQ6, Q14
transformQ7, Q15
mergeQ8, Q19
pivot_tableQ9
apply / np.selectQ10
De-duplicationQ11
Rolling windowQ13
Cohort retentionQ16
Funnel analysisQ17
RFM featuresQ18
Reshape / unstackQ20
Q1EasyRead & inspectLoad an orders CSV and show the first 5 rows, shape, column types, and null counts.
import pandas as pd
df = pd.read_csv('orders.csv', parse_dates=['order_date'])
print(df.head())
print(f"Shape: {df.shape}")
print(df.dtypes)
print(df.isnull().sum())WHAT THE INTERVIEWER TESTS: Standard first move in any analysis. parse_dates at read time avoids a separate to_datetime call.
Q2EasyFilteringFind orders from Delhi with amount > ₹2,000 placed after 1 September 2026.
filtered = df[
(df['city'] == 'Delhi') &
(df['amount_inr'] > 2000) &
(df['order_date'] > '2026-09-01')
].copy()
print(filtered.sort_values('amount_inr', ascending=False))WHAT THE INTERVIEWER TESTS: Use .copy() to avoid SettingWithCopyWarning when modifying the result. Each condition in parentheses, joined by & (not "and").
Q3Easygroupby / aggCalculate total revenue, order count, and average order value per city.
summary = (
df.groupby('city')['amount_inr']
.agg(
total_revenue='sum',
order_count='count',
avg_order_value='mean'
)
.round(0)
.sort_values('total_revenue', ascending=False)
)
print(summary.head(10))WHAT THE INTERVIEWER TESTS: Named aggregations (col='func') produce clean column names. .round(0) keeps numbers readable. Chain methods for brevity.
Q4EasyMissing valuesFill missing discount values with 0 and drop rows where city is missing.
df['discount_inr'] = df['discount_inr'].fillna(0)
df = df.dropna(subset=['city'])
# Verify
print(df[['discount_inr', 'city']].isnull().sum())
WHAT THE INTERVIEWER TESTS: fillna on one column vs dropna with subset= to target specific columns. Always verify with isnull().sum() after cleaning.
Q5EasyString opsStandardise the city column: strip whitespace, title-case it, and fix "delhi" → "Delhi".
df['city'] = (
df['city']
.str.strip()
.str.title()
.replace({'Delhi Ncr': 'Delhi', 'New Delhi': 'Delhi'})
)
print(df['city'].value_counts().head())WHAT THE INTERVIEWER TESTS: .str accessor vectorises string operations without loops. replace() handles specific string fixes after case normalisation.
Q6MediumDatetimeExtract year, month, day-of-week, and Indian financial year quarter from order_date.
df['year'] = df['order_date'].dt.year
df['month'] = df['order_date'].dt.month
df['dow'] = df['order_date'].dt.day_name() # Monday, Tuesday …
# Indian FY starts April (month 4)
df['fy_quarter'] = df['order_date'].apply(
lambda d: f"Q{((d.month - 4) % 12 // 3) + 1}" if d.month >= 4
else f"Q{((d.month + 8) // 3)}"
)
# Simpler alternative using pd.PeriodIndex offset 'Q-MAR'
df['fy_quarter_v2'] = pd.PeriodIndex(df['order_date'], freq='Q-MAR').astype(str)WHAT THE INTERVIEWER TESTS: freq='Q-MAR' creates March-end quarters — the Indian financial year ends March 31. PeriodIndex is the cleanest solution; the lambda version shows underlying logic.
Q7MediumtransformAdd a column showing each order's percentage share of its city's total revenue.
df['city_total'] = df.groupby('city')['amount_inr'].transform('sum')
df['city_share_pct'] = (df['amount_inr'] / df['city_total'] * 100).round(1)
print(df[['order_id', 'city', 'amount_inr', 'city_share_pct']].head())WHAT THE INTERVIEWER TESTS: transform('sum') returns a same-length Series aligned to the original index — no merge needed. This is the key difference from groupby().agg().
Q8MediummergeJoin orders with customers to get customer name and registration date for each order.
result = pd.merge(
df,
customers[['customer_id', 'name', 'email', 'registration_date']],
on='customer_id',
how='left' # keep all orders, even if customer record is missing
)
# Check for unmatched orders
unmatched = result[result['name'].isnull()].shape[0]
print(f"Orders with no customer record: {unmatched}")WHAT THE INTERVIEWER TESTS: how='left' keeps all orders. Always check for NULLs after a left join — if many rows are unmatched, the key columns may not align (int vs str, extra spaces).
Q9Mediumpivot_tableCreate a pivot table: cities as rows, payment methods (UPI, COD, Card) as columns, total revenue as values.
pivot = pd.pivot_table(
df,
index='city',
columns='payment_method',
values='amount_inr',
aggfunc='sum',
fill_value=0
).round(0)
# Add row total
pivot['Total'] = pivot.sum(axis=1)
pivot = pivot.sort_values('Total', ascending=False)
print(pivot.head(10))WHAT THE INTERVIEWER TESTS: fill_value=0 avoids NaN for city/payment combinations with no orders. axis=1 sums across columns. Pivot tables are tested in almost every analyst Python interview.
Q10Mediumapply + custom functionApply a tiered discount: >=₹5000 → 10%, ₹2000–₹4999 → 5%, else 0%.
def discount_tier(amount):
if amount >= 5000:
return 0.10
elif amount >= 2000:
return 0.05
return 0.0
df['discount_rate'] = df['amount_inr'].apply(discount_tier)
df['net_amount'] = df['amount_inr'] * (1 - df['discount_rate'])
# Faster vectorised alternative with np.select:
import numpy as np
conditions = [df['amount_inr'] >= 5000, df['amount_inr'] >= 2000]
df['discount_rate_v2'] = np.select(conditions, [0.10, 0.05], default=0.0)WHAT THE INTERVIEWER TESTS: apply() with a function is readable but slower than vectorised np.select(). Show both in interviews — it signals that you know the performance tradeoff.
Q11Mediumde-duplicationRemove duplicate orders (same order_id), keeping the most recent updated_at.
df_clean = (
df
.sort_values('updated_at', ascending=False)
.drop_duplicates(subset='order_id', keep='first')
.reset_index(drop=True)
)
print(f"Before: {len(df)}, After: {len(df_clean)}")WHAT THE INTERVIEWER TESTS: Sort descending by updated_at so the latest record is first, then drop_duplicates keep='first' keeps it. Equivalent to SQL ROW_NUMBER() de-duplication pattern.
Q12MediumcrosstabShow the count of orders by city and order status as a percentage heatmap table.
ct = pd.crosstab(
df['city'],
df['status'],
normalize='index' # row percentages
).round(3) * 100
ct.columns.name = None
print(ct.sort_values('returned', ascending=False).head(10))WHAT THE INTERVIEWER TESTS: normalize='index' gives row percentages (each city sums to 100%). Sort by returned to surface high-return cities — a common business question.
Q13Mediumrolling windowCalculate a 7-day rolling average of daily orders and a 7-day rolling sum of daily revenue.
daily = (
df.groupby('order_date')
.agg(orders=('order_id','count'), revenue=('amount_inr','sum'))
.sort_index()
)
daily['orders_7d_avg'] = daily['orders'].rolling(7, min_periods=1).mean().round(1)
daily['revenue_7d_sum'] = daily['revenue'].rolling(7, min_periods=1).sum()
print(daily.tail(14))WHAT THE INTERVIEWER TESTS: min_periods=1 avoids NaN for the first 6 days. rolling() on a DatetimeIndex requires data to be sorted by date first. Classic time-series interview question.
Q14MediumIST timezoneConvert order timestamps from UTC to IST (Asia/Kolkata, UTC+5:30) and extract the local hour.
df['order_ts_utc'] = pd.to_datetime(df['order_timestamp'], utc=True)
df['order_ts_ist'] = df['order_ts_utc'].dt.tz_convert('Asia/Kolkata')
df['hour_ist'] = df['order_ts_ist'].dt.hour
# Peak hour analysis
print(df.groupby('hour_ist')['order_id'].count().sort_values(ascending=False))WHAT THE INTERVIEWER TESTS: utc=True marks the source as UTC; tz_convert moves it to IST. Never use manual +5:30 offsets — they break during daylight-saving edge cases in other zones.
Q15Mediumrank within groupAdd a column showing each customer's order ranked by amount (1 = highest spend).
df['spend_rank'] = (
df.groupby('customer_id')['amount_inr']
.rank(method='dense', ascending=False)
.astype(int)
)
# Each customer's top order
top_orders = df[df['spend_rank'] == 1]
print(top_orders[['customer_id','order_id','amount_inr']].head())WHAT THE INTERVIEWER TESTS: method='dense' mirrors SQL DENSE_RANK — no gaps after ties. Equivalent to SQL ROW_NUMBER if method='first'. Ranks are aligned to the original DataFrame via transform-like broadcast.
Q16Hardcohort retentionBuild a monthly cohort retention table: rows = cohort month, columns = months since first order.
# 1. Label each customer's cohort (first order month)
df['cohort'] = df.groupby('customer_id')['order_date'].transform('min').dt.to_period('M')
df['order_period'] = df['order_date'].dt.to_period('M')
df['months_since'] = (df['order_period'] - df['cohort']).apply(lambda x: x.n)
# 2. Count unique customers per cohort × months_since cell
cohort_data = (
df.groupby(['cohort','months_since'])['customer_id']
.nunique()
.reset_index(name='customers')
)
# 3. Pivot
cohort_pivot = cohort_data.pivot(index='cohort', columns='months_since', values='customers')
# 4. Divide by cohort size (month 0) to get retention rates
retention = cohort_pivot.divide(cohort_pivot[0], axis=0).round(3) * 100
print(retention.iloc[:, :6]) # show first 6 monthsWHAT THE INTERVIEWER TESTS: Period arithmetic (subtract two Period objects) gives exact month differences without calendar ambiguity. Dividing the entire pivot by column 0 broadcasts correctly. Cohort retention is the hardest standard Python interview question.
Q17Hardfunnel analysisGiven an events table (user_id, event, timestamp), calculate conversion rates at each funnel step.
events = pd.DataFrame(...) # columns: user_id, event, timestamp
funnel_steps = ['product_viewed','added_to_cart','checkout_started','purchase_completed']
# Count unique users at each step
funnel = {
step: events[events['event'] == step]['user_id'].nunique()
for step in funnel_steps
}
funnel_df = pd.DataFrame.from_dict(funnel, orient='index', columns=['users'])
funnel_df['pct_of_top'] = (funnel_df['users'] / funnel_df['users'].iloc[0] * 100).round(1)
funnel_df['step_conversion'] = (
funnel_df['users'] / funnel_df['users'].shift(1) * 100
).round(1)
print(funnel_df)WHAT THE INTERVIEWER TESTS: step_conversion uses shift(1) to compare each step to the prior step. pct_of_top shows overall funnel efficiency. Interviewers want you to articulate what a low step_conversion means for the business (e.g., 40% add-to-cart → checkout signals a checkout UX problem).
Q18Hardfeature engineeringFrom an orders DataFrame, build an RFM feature table (Recency, Frequency, Monetary) per customer as of today.
import numpy as np
from datetime import date
today = pd.Timestamp(date.today())
rfm = df.groupby('customer_id').agg(
recency = ('order_date', lambda x: (today - x.max()).days),
frequency = ('order_id', 'count'),
monetary = ('amount_inr', 'sum')
).reset_index()
# Score each dimension 1–5 using quintiles
for col, ascending in [('recency', False), ('frequency', True), ('monetary', True)]:
rfm[f'{col}_score'] = pd.qcut(
rfm[col], q=5,
labels=[1,2,3,4,5] if ascending else [5,4,3,2,1],
duplicates='drop'
).astype(int)
rfm['rfm_segment'] = rfm['recency_score'].astype(str) + rfm['frequency_score'].astype(str) + rfm['monetary_score'].astype(str)
print(rfm.sort_values('monetary', ascending=False).head())WHAT THE INTERVIEWER TESTS: pd.qcut with labels scores into quintiles. For recency, a lower number of days = better, so labels are reversed. duplicates='drop' handles tied edges in real data. RFM is the most common feature engineering question in Indian e-commerce analyst interviews.
Q19Hardmerge_asofFor each order, find the exchange rate (USD→INR) that was active at the time of the order.
# orders: order_id, order_date, amount_usd
# fx_rates: date, usd_to_inr (one row per trading day)
orders_sorted = orders.sort_values('order_date')
fx_sorted = fx_rates.sort_values('date')
matched = pd.merge_asof(
orders_sorted,
fx_sorted,
left_on='order_date',
right_on='date',
direction='backward' # use most recent rate on or before order date
)
matched['amount_inr'] = (matched['amount_usd'] * matched['usd_to_inr']).round(2)
print(matched[['order_id','order_date','amount_usd','usd_to_inr','amount_inr']].head())WHAT THE INTERVIEWER TESTS: merge_asof finds the nearest prior match — critical for rate tables, SLA lookup, or any event-driven time-series join. direction='backward' uses the rate valid at or before the event time.
Q20Hardmulti-index reshapeProduce a category × city revenue matrix and find the city with max revenue per category.
matrix = (
df.groupby(['category','city'])['amount_inr']
.sum()
.unstack(fill_value=0)
)
# Best city per category
best_city = matrix.idxmax(axis=1).rename('top_city')
best_rev = matrix.max(axis=1).rename('top_city_revenue')
result = pd.concat([best_city, best_rev], axis=1)
print(result)WHAT THE INTERVIEWER TESTS: unstack() pivots the inner index level (city) into columns. idxmax(axis=1) returns the column name with the maximum value per row — the city name. Combine concat, unstack, and idxmax to solve reshape problems without pivot_table.
10 More Questions to Practice
| # | Difficulty | Topic | Question |
|---|
| 21 | Easy | value_counts | Show the top 10 most ordered products by order count. |
| 22 | Easy | describe() | Compare summary statistics of order values for returned vs completed orders. |
| 23 | Medium | cumsum | Add a column showing each customer's cumulative spend sorted by order date. |
| 24 | Medium | explode | An order can have multiple tags stored as a list — explode tags and count orders per tag. |
| 25 | Medium | idxmax per group | Find the best-selling product (by quantity) in each category. |
| 26 | Medium | concat + source label | Combine two DataFrames (online orders, offline orders) and add a source column to each. |
| 27 | Medium | Seaborn heatmap | Plot a correlation heatmap of numeric order features. |
| 28 | Hard | melt / wide-to-long | Convert a pivot table (cities as rows, months as columns) back to long format for charting. |
| 29 | Hard | Custom groupby aggregation | For each city: revenue, count, median order, IQR, and % of orders from UPI. |
| 30 | Hard | Classification pipeline | Build a logistic regression to predict order return: feature engineering → train/test split → fit → confusion matrix → AUC. |
Frequently Asked Questions
What Python topics are tested in Indian data analyst interviews in 2026?
Based on patterns from Indian tech, e-commerce, fintech, and analytics firms in 2026, the most frequently tested Python topics for data analyst roles are: (1) Pandas fundamentals — reading CSV/Excel files, df.info(), df.describe(), selecting columns with df[['col']] vs df.col, filtering with boolean indexing. (2) Data cleaning — handling missing values (isnull, fillna, dropna), dropping duplicates (drop_duplicates), type conversion (astype), and string operations with .str accessor. (3) groupby — groupby().agg() with multiple functions, transform() for within-group operations (% of group, rank within group), and pivot_table(). (4) Merging — pd.merge() with different how options (inner, left, right, outer), merge_asof for date-based nearest-match, and concat. (5) Datetime — pd.to_datetime(), dt.year/month/day/dayofweek, pd.Timestamp, India-specific timezone localization to IST (Asia/Kolkata), financial year quarter calculation. (6) Apply and lambda — row-wise and column-wise custom functions, apply with axis=0/1. (7) Basic statistics — mean, median, std, quantile, crosstab, and correlation matrix. (8) Matplotlib/Seaborn — plotting grouped bar charts, heatmaps for correlations, and line charts for time series — interviewers often ask you to "visualise this result" as a follow-up.
Is pandas still important for data analyst interviews in India in 2026?
Yes — pandas remains the dominant tool tested in data analyst Python interviews in India in 2026, but the role of Python in analyst interviews has evolved. Three years ago, heavy pandas manipulation (row-by-row loops, complex apply functions) was common. Today, interviewers test cleaner, vectorised pandas code and expect analysts to know when to use SQL vs Python. The practical split in 2026: SQL is expected for aggregation, filtering, and joining structured data stored in databases; Python/pandas is expected for data cleaning, feature engineering, exploratory analysis on flat files, working with datetime/text columns, and building analytical outputs like pivot tables and correlation matrices. If a company uses Jupyter notebooks for analysis (common in D2C e-commerce, fintech, and consulting analytics teams), expect deeper pandas questions. If the company runs a mature BI stack (Looker, Power BI, Tableau), the Python questions may be lighter — focused on scripting and automation. Always ask the interviewer about the day-to-day tool stack to calibrate preparation depth.
What is the difference between apply() and transform() in pandas?
apply() and transform() are both used with groupby(), but they produce different outputs and are used in different situations. apply() collapses the group into a single value or a new DataFrame — it changes the shape of the output. For example, df.groupby("city")["revenue"].apply(lambda x: x.nlargest(3)) returns just the top-3 rows per city, a smaller DataFrame. transform() returns a Series of the same length as the original DataFrame — one value per original row, aligned with the original index. For example, df.groupby("city")["revenue"].transform("sum") returns total city revenue for every row, so you can compute each row's share of its city total in one step: df["city_share"] = df["revenue"] / df.groupby("city")["revenue"].transform("sum"). The rule: use transform() when you want to add a within-group metric as a new column without changing the shape of the DataFrame. Use apply() when you want to aggregate or reshape within groups. This distinction is a very common interview question in 2026 — candidates who know it immediately signal pandas fluency.
EVIKA ACADEMY · NOIDA SECTOR 51
Master Python for Data Analyst Interviews
Our Python for Data Analysts module covers 60+ hands-on pandas problems using Indian e-commerce datasets, with mock interview practice and live code reviews by working analysts.
Book Free Demo Class →