Home/Machine Learning/Python & Data Manipulation (pandas/NumPy)
Back
๐Ÿ

Applied Scientist

Python & Data Manipulation (pandas/NumPy)

Python data manipulation is the practical foundation of every Applied Scientist and Data Scientist role. Amazon, Meta, Google, and Airbnb routinely give take-home assignments and whiteboard coding sessions that require proficient pandas and NumPy โ€” sessionizing user events, computing cohort retention, finding top-N per group, and building lag features for time series models. The highest-signal skill is knowing when NOT to use a Python loop and how to express transformations as vectorized pandas operations, which can be 100-1000x faster on production-scale data.

Python basicsBasic SQLUnderstanding of DataFramesNumPy arrays and indexingpandasNumPyGroupByWindow FunctionsVectorizationTime SeriesSQL Translation

Core Theory

  • 1pandas groupby mechanics: groupby returns a DataFrameGroupBy object โ€” a lazy computation that only executes when you call an aggregation (.sum(), .mean(), .agg(), .transform(), .apply()). Key distinction: .agg() reduces each group to a scalar (returns a smaller DataFrame); .transform() returns a DataFrame of the same shape as the input (each row gets the group-level statistic โ€” essential for adding group features without a merge); .apply() applies a user function to each group and can return any shape. Performance: .agg({'col': 'sum'}) is fastest (dispatches to C); .transform(lambda x: x.fillna(x.mean())) is Cython-accelerated; .apply(custom_fn) is slowest (pure Python). For named aggregations in pandas โ‰ฅ1.0: df.groupby('group').agg(total_spend=('spend', 'sum'), avg_spend=('spend', 'mean')).
  • 2Merge and join types: pd.merge(left, right, on='key', how='inner/left/right/outer'). Inner join: only rows where key exists in both โ€” equivalent to SQL INNER JOIN. Left join: all rows from left, matching rows from right, NaN where no match โ€” equivalent to SQL LEFT JOIN. Cross join: pd.merge(left, right, how='cross') creates the Cartesian product โ€” useful for enumerating all user-date combinations. Anti-join (rows in left with NO match in right): left_merge = left.merge(right, on='key', how='left', indicator=True); anti = left_merge[left_merge['_merge'] == 'left_only']. Merge on multiple columns: on=['user_id', 'date']. Merge on inequality: pd.merge_asof() for nearest-match on sorted keys (useful for point-in-time feature lookups). Index merges: df.join(other, on='key') uses the index of `other`.
  • 3pivot_table and reshaping: pd.pivot_table(df, values='revenue', index='user_id', columns='product_category', aggfunc='sum', fill_value=0) creates a pivot table โ€” equivalent to a spreadsheet pivot. df.melt(id_vars=['user_id', 'date'], value_vars=['feature_A', 'feature_B'], var_name='feature', value_name='value') unpivots from wide to long format (inverse of pivot). df.stack() pivots the innermost column level to the row index; df.unstack() does the reverse. df.explode('list_column') expands a column of lists into separate rows (one row per list element) โ€” essential for processing JSON arrays from event logs.
  • 4Window functions (rolling, expanding, ewm): df['rolling_7d'] = df.groupby('user_id')['revenue'].transform(lambda x: x.rolling(7, min_periods=1).mean()). Rolling computes over a fixed-width window. Expanding computes over all data up to the current row (expanding().mean() = cumulative mean). EWM (exponentially weighted moving average): df['ewma'] = df['value'].ewm(span=7, adjust=False).mean() โ€” assigns exponentially decaying weights to older observations. Key parameter: min_periods โ€” minimum non-NaN values required to return a result. For per-group rolling features: always sort by (user_id, timestamp) before applying rolling to ensure time order within each group. Use .shift(1) after rolling to prevent the current row from being included in its own feature (lag-1 feature).
  • 5Time series โ€” resampling and date arithmetic: pd.date_range('2023-01-01', periods=365, freq='D') generates daily timestamps. df.resample('W').sum() resamples to weekly aggregations (like a groupby on time windows). Supported frequencies: 'D' (day), 'W' (week-end), 'MS' (month start), 'QS' (quarter start), 'H' (hour), '15T' (15-minute). Date arithmetic: (df['end_date'] - df['start_date']).dt.days extracts integer day difference. pd.to_datetime() parses strings; df['date'].dt.dayofweek returns 0=Monday, 6=Sunday. Business day offset: pd.tseries.offsets.BusinessDay(n). Time zone handling: df['ts'].dt.tz_localize('UTC').dt.tz_convert('US/Eastern'). The .dt accessor unlocks datetime properties and methods on a Series.
  • 6NumPy broadcasting: Broadcasting allows operations on arrays with different shapes without explicit loops. Rules: (1) If arrays have different numbers of dimensions, the shape of the smaller-dimensioned array is padded with 1s on the left. (2) Arrays with size 1 along a dimension are stretched to match the other array's size. Example: a.shape = (3,4), b.shape = (1,4) โ€” b is broadcast to (3,4). a.shape = (3,1), b.shape = (3,4) โ€” a is broadcast to (3,4). Common pattern: compute pairwise distances. X.shape = (n, d). distances = np.sqrt(((X[:, None, :] - X[None, :, :])**2).sum(axis=2)) โ€” X[:, None, :] has shape (n, 1, d), X[None, :, :] has shape (1, n, d), difference is (n, n, d). Vectorization: avoid Python loops over rows. A loop over 1M rows at 1ฮผs/iteration = 1 second; the equivalent NumPy operation = 10ms (100x speedup). Rule: if you're writing 'for i, row in df.iterrows()', think about whether a .apply(), .groupby(), or broadcasting can replace it.
  • 7Efficient pandas โ€” avoiding common performance pitfalls: (1) chained indexing anti-pattern: df['A'][mask] = value triggers a SettingWithCopyWarning and may silently fail on a copy. Always use df.loc[mask, 'A'] = value for in-place mutation. (2) .iterrows() is ~100x slower than .apply() which is ~10x slower than vectorized ops. (3) String columns: object dtype stores Python strings โ€” use df['col'].astype('category') for low-cardinality string columns (reduces memory ~10x and speeds up groupby). (4) dtype optimization: use int32 instead of int64 where values fit, float32 instead of float64 for model features. df['col'] = pd.to_numeric(df['col'], downcast='integer'). (5) Avoiding copies: df.copy() creates a full copy; operations that return a view (slices, column selection) share memory โ€” modifying a view can modify the original. Use df.copy() when you need an independent DataFrame. (6) Large data: use chunking (pd.read_csv(file, chunksize=100_000)) or switch to Polars/DuckDB for DataFrames that don't fit in RAM.
  • 8SQL-to-pandas translation (the most common interview skill): SELECT + GROUP BY = df.groupby().agg(). WHERE = df[mask]. HAVING = df.groupby().filter(). ORDER BY = df.sort_values(). LIMIT = df.head(). JOIN = pd.merge(). Window function ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY ts) = df.groupby('user_id').cumcount() + 1. RANK() = df.groupby('user_id')['value'].rank(method='min', ascending=False). LAG(col, 1) = df.groupby('user_id')['col'].shift(1). LEAD(col, 1) = df.groupby('user_id')['col'].shift(-1). SUM() OVER (PARTITION BY user_id ORDER BY ts ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) = df.groupby('user_id')['col'].cumsum(). NTILE(4) = df.groupby('user_id')['col'].transform(lambda x: pd.qcut(x, 4, labels=False)).
  • 9Common interview coding patterns โ€” sessionizing user events: A session is a contiguous sequence of events separated by gaps of at most T minutes (commonly 30 minutes). Algorithm: (1) sort by (user_id, timestamp); (2) compute time gap between consecutive events within each user: df['gap'] = df.groupby('user_id')['ts'].diff().dt.total_seconds() / 60; (3) flag new sessions where gap > 30 or it's the user's first event: df['is_new_session'] = (df['gap'] > 30) | df['gap'].isna(); (4) session_id = df.groupby('user_id')['is_new_session'].cumsum(). Now each row has a session identifier. Aggregate session features: df.groupby(['user_id', 'session_id']).agg(session_start=('ts', 'min'), session_end=('ts', 'max'), n_events=('event_type', 'count')).
  • 10Common interview coding patterns โ€” top-N per group: Find the top 3 products by revenue per country. Without SQL window functions, this requires careful pandas: method 1 โ€” sort then groupby head: df.sort_values('revenue', ascending=False).groupby('country').head(3). Method 2 โ€” rank then filter: df['rank'] = df.groupby('country')['revenue'].rank(method='first', ascending=False); df[df['rank'] <= 3]. Method 1 is concise; Method 2 is more flexible (can use dense rank, handle ties). For the 'median of medians' or 'Nth percentile per group': df.groupby('group')['value'].quantile(0.9). For deduplication keeping the latest record per user: df.sort_values('timestamp').drop_duplicates('user_id', keep='last').
  • 11Handling large datasets โ€” chunking, dtypes, memory profiling: df.info(memory_usage='deep') reports per-column memory. df.memory_usage(deep=True).sum() / 1e9 gives total GB. Dtype optimization can reduce memory 4-8x: float64โ†’float32 (saves 50%), int64โ†’int32 (saves 50%), objectโ†’category for strings with <100 unique values (saves ~70%). Chunked reading: for chunk in pd.read_csv('large.csv', chunksize=100_000): process(chunk). For datasets larger than RAM, switch to: DuckDB (SQL queries on Parquet files with minimal RAM), Polars (Rust-based DataFrame library, 5-10x faster than pandas on large data, lazy evaluation), Dask (parallelizes pandas across cores), or PySpark (distributed). In interviews, mentioning when you would switch from pandas to a more scalable tool signals production experience.

Key Formulas

01GroupBy aggregation: `df.groupby('key').agg(new_col=('col', 'sum'))`
02Window function (per-group rolling): `df.groupby('user_id')['val'].transform(lambda x: x.rolling(7).mean())`
03Lag/lead feature: `df.groupby('user_id')['col'].shift(1)` for lag-1, `shift(-1)` for lead-1
04Anti-join: `left.merge(right, on='key', how='left', indicator=True).query('_merge == "left_only"')`
05Sessionization: `df['session_id'] = df.groupby('user_id')['is_new_session'].cumsum()`
06Top-N per group: `df.sort_values('val', ascending=False).groupby('group').head(N)`
07NumPy broadcasting pairwise op: `result[i,j] = f(X[i], X[j])` โ†’ `f(X[:, None] - X[None, :])` โ€” eliminates double loop
08EWM: `df['ewma'] = df.groupby('user_id')['val'].transform(lambda x: x.ewm(span=7).mean())`
python
1import numpy as np
2import pandas as pd
3from datetime import datetime, timedelta
4
5np.random.seed(42)
6
7# โ”€โ”€โ”€ Simulate raw user event log โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
8n_users = 200
9n_events = 8_000
10
11user_ids = np.random.choice(range(1, n_users + 1), n_events)
12timestamps = pd.to_datetime('2023-01-01') + pd.to_timedelta(
13 np.random.exponential(5, n_events).cumsum() * 0.1, unit='h'
14)
15event_types = np.random.choice(
16 ['page_view', 'click', 'add_to_cart', 'purchase'], n_events,
17 p=[0.55, 0.25, 0.12, 0.08]
18)
19revenue = np.where(
20 event_types == 'purchase',
21 np.random.lognormal(4, 0.8, n_events),
22 0.0
23)
24
25events = pd.DataFrame({
26 'user_id': user_ids,
27 'timestamp': timestamps,
28 'event_type': event_types,
29 'revenue': revenue,
30}).sort_values(['user_id', 'timestamp']).reset_index(drop=True)
31
32print(f"Events shape: {events.shape}")
33print(events.head(4).to_string())
34
35# โ”€โ”€โ”€ 1. SESSIONIZATION โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
36# Question: assign a session_id to each event, where a new session starts
37# after 30+ minutes of inactivity.
38
39def sessionize(df: pd.DataFrame,
40 user_col: str = 'user_id',
41 ts_col: str = 'timestamp',
42 gap_minutes: float = 30.0) -> pd.DataFrame:
43 """
44 Assign session IDs. New session when time gap > gap_minutes or first event.
45 O(n log n) for the sort; O(n) for the gap computation.
46 """
47 df = df.sort_values([user_col, ts_col]).copy()
48
49 # Time gap between consecutive events per user
50 df['gap_min'] = (
51 df.groupby(user_col)[ts_col]
52 .diff()
53 .dt.total_seconds()
54 .div(60)
55 )
56
57 # New session: first event of user OR gap > threshold
58 df['is_new_session'] = df['gap_min'].isna() | (df['gap_min'] > gap_minutes)
59
60 # Session ID = cumulative sum of new session flags (globally unique when combined with user_id)
61 df['session_num'] = df.groupby(user_col)['is_new_session'].cumsum()
62 df['session_id'] = df[user_col].astype(str) + '_' + df['session_num'].astype(str)
63
64 return df.drop(columns=['gap_min', 'is_new_session', 'session_num'])
65
66events = sessionize(events)
67
68# Session-level aggregation
69sessions = (events
70 .groupby(['user_id', 'session_id'])
71 .agg(
72 session_start = ('timestamp', 'min'),
73 session_end = ('timestamp', 'max'),
74 n_events = ('event_type', 'count'),
75 n_purchases = ('event_type', lambda x: (x == 'purchase').sum()),
76 session_revenue = ('revenue', 'sum'),
77 )
78 .reset_index()
79)
80sessions['session_duration_min'] = (
81 sessions['session_end'] - sessions['session_start']
82).dt.total_seconds() / 60
83
84print(f"
85โ”€โ”€โ”€ Sessions (first 4) โ”€โ”€โ”€")
86print(sessions.head(4).to_string())
87print(f"Total sessions: {len(sessions):,}")
88print(f"Avg events/session: {sessions['n_events'].mean():.1f}")
89
90# โ”€โ”€โ”€ 2. TOP-N PER GROUP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
91# Question (Amazon): Find the top 3 users by total revenue in each event_type.
92
93# Method 1: sort + groupby head (concise)
94top3_per_type = (
95 events[events['revenue'] > 0]
96 .groupby(['event_type', 'user_id'])['revenue']
97 .sum()
98 .reset_index(name='total_revenue')
99 .sort_values('total_revenue', ascending=False)
100 .groupby('event_type')
101 .head(3)
102 .reset_index(drop=True)
103)
104
105# Method 2: rank (handles ties explicitly)
106user_event_rev = (
107 events[events['revenue'] > 0]
108 .groupby(['event_type', 'user_id'])['revenue']
109 .sum()
110 .reset_index(name='total_revenue')
111)
112user_event_rev['rank'] = (
113 user_event_rev
114 .groupby('event_type')['total_revenue']
115 .rank(method='dense', ascending=False)
116)
117top3_ranked = user_event_rev[user_event_rev['rank'] <= 3].sort_values(
118 ['event_type', 'rank']
119)
120
121print(f"
122โ”€โ”€โ”€ Top 3 users by revenue per event type (Method 1) โ”€โ”€โ”€")
123print(top3_per_type.to_string())
124
125# โ”€โ”€โ”€ 3. COHORT RETENTION (Meta/Google classic) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
126# Question: Build a D0/D1/D7/D30 cohort retention table.
127# Cohort = the week the user was first seen.
128
129first_seen = (
130 events.groupby('user_id')['timestamp']
131 .min()
132 .dt.to_period('W')
133 .rename('cohort_week')
134 .reset_index()
135)
136events_with_cohort = events.merge(first_seen, on='user_id')
137
138def cohort_retention(df: pd.DataFrame,
139 cohort_col: str = 'cohort_week',
140 user_col: str = 'user_id',
141 ts_col: str = 'timestamp',
142 day_offsets: list = [0, 1, 7, 14, 30]) -> pd.DataFrame:
143 """
144 For each cohort, compute the fraction of users active at each day offset.
145 Returns a wide table: rows = cohorts, columns = D0, D1, D7, D14, D30.
146 """
147 # First date of each user
148 user_first = (df.groupby(user_col)[ts_col]
149 .min()
150 .rename('first_date')
151 .reset_index())
152 df = df.merge(user_first, on=user_col)
153 df['days_since_first'] = (
154 df[ts_col].dt.normalize() - df['first_date'].dt.normalize()
155 ).dt.days
156
157 cohort_sizes = (df.drop_duplicates(user_col)
158 .groupby(cohort_col)[user_col]
159 .count()
160 .rename('cohort_size'))
161
162 rows = {}
163 for day in day_offsets:
164 window = (df['days_since_first'] >= day) & (df['days_since_first'] < day + 1)
165 retained = (df[window]
166 .groupby(cohort_col)[user_col]
167 .nunique())
168 rows[f'D{day}'] = (retained / cohort_sizes).round(3)
169
170 result = pd.DataFrame(rows)
171 result.index.name = 'cohort'
172 return result
173
174retention = cohort_retention(events_with_cohort)
175print(f"
176โ”€โ”€โ”€ Cohort Retention Table โ”€โ”€โ”€")
177print(retention.to_string())
178
179# โ”€โ”€โ”€ 4. MOVING AVERAGES AND LAG FEATURES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
180# Question: Build daily active user count with 7-day rolling avg and lag features.
181
182daily_stats = (
183 events.groupby(events['timestamp'].dt.date)
184 .agg(
185 dau = ('user_id', 'nunique'),
186 total_revenue = ('revenue', 'sum'),
187 n_purchases = ('event_type', lambda x: (x == 'purchase').sum()),
188 )
189 .reset_index()
190 .rename(columns={'timestamp': 'date'})
191)
192daily_stats['date'] = pd.to_datetime(daily_stats['date'])
193daily_stats = daily_stats.sort_values('date').reset_index(drop=True)
194
195# Rolling statistics
196daily_stats['dau_7d_ma'] = daily_stats['dau'].rolling(7, min_periods=1).mean()
197daily_stats['dau_7d_std'] = daily_stats['dau'].rolling(7, min_periods=1).std()
198daily_stats['revenue_7d_sum'] = daily_stats['total_revenue'].rolling(7, min_periods=1).sum()
199
200# Lag features (shift AFTER rolling to prevent current-row leakage)
201daily_stats['dau_lag1'] = daily_stats['dau'].shift(1)
202daily_stats['dau_lag7'] = daily_stats['dau'].shift(7)
203daily_stats['revenue_lag1'] = daily_stats['total_revenue'].shift(1)
204
205# Expanding (cumulative)
206daily_stats['cumulative_dau_max'] = daily_stats['dau'].expanding().max()
207
208print(f"
209โ”€โ”€โ”€ Daily stats with rolling/lag features (last 7 rows) โ”€โ”€โ”€")
210display_cols = ['date', 'dau', 'dau_7d_ma', 'dau_lag1', 'dau_lag7', 'revenue_7d_sum']
211print(daily_stats[display_cols].tail(7).round(1).to_string(index=False))
212
213# โ”€โ”€โ”€ 5. SQL-TO-PANDAS TRANSLATION (window functions) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
214# SQL: SELECT user_id, event_type, revenue,
215# ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY timestamp) AS rn,
216# SUM(revenue) OVER (PARTITION BY user_id ORDER BY timestamp
217# ROWS UNBOUNDED PRECEDING) AS cum_revenue,
218# LAG(event_type, 1) OVER (PARTITION BY user_id ORDER BY timestamp) AS prev_event
219# FROM events
220
221window_features = (
222 events
223 .sort_values(['user_id', 'timestamp'])
224 .assign(
225 # ROW_NUMBER(): per-user sequential event number
226 row_number = lambda df: df.groupby('user_id').cumcount() + 1,
227 # Cumulative sum (ROWS UNBOUNDED PRECEDING)
228 cum_revenue = lambda df: df.groupby('user_id')['revenue'].cumsum(),
229 # LAG(event_type, 1)
230 prev_event = lambda df: df.groupby('user_id')['event_type'].shift(1),
231 # RANK(): ranks within user by revenue descending
232 rev_rank = lambda df: df.groupby('user_id')['revenue']
233 .rank(method='min', ascending=False),
234 )
235)
236
237print(f"
238โ”€โ”€โ”€ Window features (first user's events) โ”€โ”€โ”€")
239sample_user = events['user_id'].value_counts().idxmax()
240print(window_features[window_features['user_id'] == sample_user]
241 [['user_id', 'timestamp', 'event_type', 'revenue',
242 'row_number', 'cum_revenue', 'prev_event', 'rev_rank']]
243 .head(6).to_string(index=False))
244
245# โ”€โ”€โ”€ 6. DTYPE OPTIMIZATION + MEMORY PROFILING โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
246print(f"
247โ”€โ”€โ”€ Memory optimization โ”€โ”€โ”€")
248print(f"Before optimization: {events.memory_usage(deep=True).sum() / 1e6:.1f} MB")
249
250events_opt = events.copy()
251events_opt['user_id'] = events_opt['user_id'].astype('int32')
252events_opt['event_type'] = events_opt['event_type'].astype('category')
253events_opt['revenue'] = events_opt['revenue'].astype('float32')
254
255print(f"After optimization: {events_opt.memory_usage(deep=True).sum() / 1e6:.1f} MB")
256reduction = 1 - events_opt.memory_usage(deep=True).sum() / events.memory_usage(deep=True).sum()
257print(f"Memory reduction: {reduction:.0%}")
258
259# โ”€โ”€โ”€ 7. NUMPY VECTORIZATION vs LOOP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
260import time
261
262values = np.random.randn(100_000)
263thresholds = np.random.uniform(0, 1, 100_000)
264
265# Slow: Python loop
266t0 = time.perf_counter()
267result_loop = [values[i] * 2 if values[i] > thresholds[i] else values[i] * 0.5
268 for i in range(len(values))]
269t_loop = time.perf_counter() - t0
270
271# Fast: vectorized NumPy
272t0 = time.perf_counter()
273result_vec = np.where(values > thresholds, values * 2, values * 0.5)
274t_vec = time.perf_counter() - t0
275
276print(f"
277โ”€โ”€โ”€ Vectorization speedup โ”€โ”€โ”€")
278print(f"Python loop: {t_loop*1000:.1f} ms")
279print(f"NumPy: {t_vec*1000:.2f} ms (speedup: {t_loop/t_vec:.0f}x)")
280assert np.allclose(result_loop, result_vec)

Seven production pandas patterns covering the most common interview coding questions: (1) sessionization โ€” sort by user+time, compute gaps, cumsum of new-session flags; (2) top-N per group โ€” two methods (sort+head vs rank+filter), handling ties; (3) cohort retention โ€” merge first-seen dates, compute days-since-signup, pivot by day offset; (4) rolling/lag features โ€” rolling().mean(), shift(), expanding() with min_periods; (5) SQL window functions translated to pandas โ€” cumcount, cumsum, shift, rank; (6) dtype optimization โ€” int32/float32/category cuts memory 40-60%; (7) NumPy vectorization benchmark showing 100x+ speedup over Python loops.

Worked Interview Problems

3 problems

Problem

Given a table of user purchases (user_id, purchase_date, amount), find all users whose monthly purchase amount has increased every month for the last 3 months (months M-2, M-1, M all strictly increasing). Return user_id and their monthly amounts.

Solution

1

Step 1 โ€” Aggregate to monthly level: group by (user_id, month) and sum purchase amounts. Use purchase_date truncated to month-start for grouping.

2

Step 2 โ€” Filter to last 3 months: identify the 3 most recent months in the data; keep only rows where month is in those 3 months.

3

Step 3 โ€” Ensure all 3 months present for each user: group by user_id and filter to users with exactly 3 rows (otherwise the increasing-3-month check is invalid for users with missing months).

4

Step 4 โ€” Compute lag features: sort by (user_id, month). Use groupby + shift to create prev_month_amount (lag-1). Compute is_increasing = current_amount > prev_month_amount.

5

Step 5 โ€” Filter users where all available is_increasing are True: group by user_id, check that min(is_increasing) == True (i.e., both M-1 > M-2 and M > M-1 are True).

6

Production consideration: handle users who joined after month M-2 (they won't have 3 months of data โ€” exclude them). Also handle months with zero purchases โ€” if they're absent from the data, they represent $0, not missing data โ€” fill with 0 before applying the trend check.

Answer

# Complete working solution import pandas as pd import numpy as np # Sample data purchases = pd.DataFrame({ 'user_id': [1,1,1,2,2,2,3,3,4,4,4], 'purchase_date': pd.to_datetime(['2023-09-15','2023-10-05','2023-11-20', '2023-09-01','2023-10-15','2023-11-30', '2023-10-01','2023-11-01', '2023-09-10','2023-10-10','2023-11-10']), 'amount': [100, 150, 200, # user 1: increasing โœ“ 200, 180, 210, # user 2: not strictly increasing (180 < 200) 50, 45, # user 3: only 2 months, not increasing 300, 350, 400], # user 4: increasing โœ“ }) # Step 1: monthly aggregation purchases['month'] = purchases['purchase_date'].dt.to_period('M') monthly = (purchases .groupby(['user_id', 'month'])['amount'] .sum() .reset_index(name='monthly_amount')) # Step 2: last 3 months only last_3 = sorted(monthly['month'].unique())[-3:] monthly = monthly[monthly['month'].isin(last_3)] # Step 3: users with all 3 months present users_3mo = (monthly.groupby('user_id')['month'] .count() .pipe(lambda s: s[s == 3]) .index) monthly = monthly[monthly['user_id'].isin(users_3mo)] # Step 4: lag and increasing flag monthly = monthly.sort_values(['user_id', 'month']) monthly['prev_amount'] = monthly.groupby('user_id')['monthly_amount'].shift(1) monthly['is_increasing'] = monthly['monthly_amount'] > monthly['prev_amount'] # Step 5: filter (drop first month row where prev_amount is NaN) monthly_with_trend = monthly.dropna(subset=['prev_amount']) increasing_users = (monthly_with_trend .groupby('user_id')['is_increasing'] .min() # True only if ALL months are increasing .pipe(lambda s: s[s == True]) .index) result = monthly[monthly['user_id'].isin(increasing_users)] print(result.pivot(index='user_id', columns='month', values='monthly_amount'))

Common Mistakes

!

Chained indexing: df[mask]['col'] = value silently modifies a copy, not the original DataFrame, leaving the original unchanged. This is one of the most common pandas bugs. Always use df.loc[mask, 'col'] = value for assignment. The SettingWithCopyWarning is pandas trying to warn you about this โ€” never ignore it.

!

Using .iterrows() for row-level transformations. It's ~100x slower than vectorized operations and ~10x slower than .apply(). Whenever you see 'for i, row in df.iterrows()', ask: can this be expressed as np.where(), a vectorized arithmetic operation, or a groupby + transform? Almost always yes.

!

Fitting preprocessing (StandardScaler, mean imputation, target encoding) on the full dataset before the train/test split. In take-home assignments, this is instant disqualification. Always split first, then fit transformers only on training data. Use sklearn Pipelines to make this automatic and error-proof.

!

Not sorting before applying shift() or rolling() within groups. If events are not sorted by timestamp within each user group, shift(1) gives you the previous row in DataFrame order, not the previous event in time order. Always sort_values(['user_id', 'timestamp']) before computing per-group lags and rolling features.

!

Using object dtype for categorical columns. A column with 5 unique string values stored as object dtype takes ~80 bytes per row. Storing it as category dtype takes ~1 byte per row โ€” an 80x memory reduction. For a DataFrame with 100M rows and 10 categorical columns, this is the difference between fitting in 80GB RAM or 1GB RAM. Always call .astype('category') for low-cardinality string columns.

Only works in the Electron app

<webview> is an Electron-only tag. Run npm run electron:dev to use this.

25:00Focus
0

25 min focus ยท 5 min break ยท long break every 4 sessions

The Ultimate Grind