Pandasdrill

Pandasdrill / Cheat sheet

pandas cheat sheet

A hundred entries in ten groups, written for pandas 2.x with the code you actually type, the gotcha next to it, and a drill for most. Print the page or get the PDF by email.

No newsletter spam: the PDF, one follow-up with the free exercises, and nothing else unless you ask.

The ones worth learning first

WhatHow
Read a CSVdf = pd.read_csv('sales.csv', parse_dates=['date'])
Filter rowsdf[df['total'] > 100]
Pick columnsdf[['name', 'total']]
Group and sumdf.groupby('region')['total'].sum()
Mergeorders.merge(customers, on='customer_id', how='left')
Sortdf.sort_values(['region', 'total'], ascending=[True, False])
Missing valuesdf['qty'] = df['qty'].fillna(0)
New column from othersdf['net'] = df['price'] * (1 - df['discount'])
Datesdf['date'] = pd.to_datetime(df['date']); df['month'] = df['date'].dt.to_period('M')
Quick lookdf.head(); df.info(); df.describe()

Load and inspect

CSV, Excel, JSONpd.read_csv('f.csv') pd.read_excel('f.xlsx', sheet_name='Data') pd.read_json('f.json')
From a dictpd.DataFrame({'a': [1, 2], 'b': ['x', 'y']})
Shape, columns, typesdf.shape df.columns df.dtypes
First rows, summarydf.head(10) df.info() df.describe()
Missing per columndf.isna().sum()
Unique values and countsdf['region'].unique() df['region'].value_counts()
Savedf.to_csv('out.csv', index=False) df.to_excel('out.xlsx', index=False)
Memory and samplingdf.memory_usage(deep=True).sum() df.sample(5)

Select and filter

One column (Series)df['total']
Several columnsdf[['name', 'total']] drill
Boolean maskdf[df['total'] > 100] drill
Two conditionsParentheses are required; use & | ~ not and/or/not.df[(df['total'] > 100) & (df['region'] == 'North')]
Rows and columns by labeldf.loc[df['total'] > 100, ['name', 'total']] drill
By positiondf.iloc[:5, 0:2] drill
In a listdf[df['region'].isin(['North', 'East'])] drill
BetweenBoth ends included.df[df['total'].between(10, 50)] drill
queryVariables: @threshold.df.query("total > 100 and region == 'North'") drill
Sortdf.sort_values(['region', 'total'], ascending=[True, False]) drill
Top nnsmallest for the other end.df.nlargest(3, 'salary') drill
Not null rowsdf[df['email'].notna()]
Set and reset the indexdf.set_index('id') df.reset_index(drop=True)

Clean

Fill missingdf['qty'] = df['qty'].fillna(0) drill
Fill with the mediandf['age'] = df['age'].fillna(df['age'].median())
Drop rows missing a fielddf = df.dropna(subset=['email']) drill
Text to numbererrors='coerce' turns junk into NaN instead of failing.df['price'] = pd.to_numeric(df['price'], errors='coerce') drill
Change typedf['qty'] = df['qty'].astype(int)
Duplicatesdf = df.drop_duplicates(subset=['email'], keep='first') drill
Find duplicatesdf[df.duplicated(subset=['email'], keep=False)]
Rename columnsdf = df.rename(columns={'Total ($)': 'total'}) drill
Clean all column namesdf.columns = df.columns.str.strip().str.lower().str.replace(' ', '_')
Replace valuesdf['status'] = df['status'].replace({'N/A': None, 'paid ': 'paid'})
Map to new valuesdf['region'] = df['code'].map({'N': 'North', 'S': 'South'})
Drop columnsdf = df.drop(columns=['tmp', 'notes'])
Clip outliersdf['total'] = df['total'].clip(lower=0, upper=10000)

Strings

Strip and lowerdf['name'] = df['name'].str.strip().str.lower() drill
Containsdf[df['product'].str.contains('pro', case=False, na=False)] drill
Starts / ends withdf['sku'].str.startswith('AB') df['email'].str.endswith('.edu')
Extract with regexdf['num'] = df['code'].str.extract(r'(\d+)').astype(int) drill
Replacedf['phone'] = df['phone'].str.replace(r'\D', '', regex=True)
Split into columnsdf[['first', 'last']] = df['name'].str.split(' ', n=1, expand=True)
Length, title casedf['name'].str.len() df['name'].str.title()
Join columnsdf['full'] = df['first'] + ' ' + df['last']
Paddf['id'].astype(str).str.zfill(6)

Dates

Parsedf['date'] = pd.to_datetime(df['date'], errors='coerce') drill
Parse with a formatpd.to_datetime(df['date'], format='%d/%m/%Y')
Partsdf['date'].dt.year .dt.month .dt.day_name()
Month perioddf['month'] = df['date'].dt.to_period('M')
Difference in days(df['end'] - df['start']).dt.days
Add daysdf['due'] = df['date'] + pd.Timedelta(days=30)
Filter a rangedf[(df['date'] >= '2026-01-01') & (df['date'] < '2026-02-01')]
Resample to months'MS' month start, 'W' weekly, 'QS' quarter.df.set_index('date')['total'].resample('MS').sum() drill
Date rangepd.date_range('2026-01-01', periods=12, freq='MS')
Business days betweennp.busday_count(start.values.astype('datetime64[D]'), end.values.astype('datetime64[D]'))

Group and aggregate

Sum per groupdf.groupby('region')['total'].sum() drill
As a flat tabledf.groupby('region', as_index=False)['total'].mean() drill
Count rowscount() counts non-null per column; size() counts rows.df.groupby('region').size() drill
Named aggregationsdf.groupby('region').agg(revenue=('total', 'sum'), orders=('id', 'count'), avg=('total', 'mean')) drill
Two keysdf.groupby(['region', 'rep'])['total'].sum().reset_index() drill
Several statsdf.groupby('region')['total'].agg(['sum', 'mean', 'max']) drill
Pivot tabledf.pivot_table(index='region', columns='month', values='total', aggfunc='sum', fill_value=0) drill
Crosstabpd.crosstab(df['region'], df['status']) drill
Sharesdf['region'].value_counts(normalize=True) drill
Group total next to each rowdf['region_total'] = df.groupby('region')['total'].transform('sum')
Share within groupdf['share'] = df['total'] / df.groupby('region')['total'].transform('sum')
Top row per groupdf.sort_values('total', ascending=False).groupby('region').head(1)
Filter groupsdf.groupby('region').filter(lambda g: g['total'].sum() > 1000)
Cumulative within groupdf.groupby('region')['total'].cumsum()

Merge and reshape

Inner mergeorders.merge(customers, on='customer_id') drill
Left mergeAdd validate='m:1' to catch duplicate keys.orders.merge(customers, on='customer_id', how='left') drill
Different key namesorders.merge(customers, left_on='cust', right_on='id') drill
Rows with no matchm = a.merge(b, on='k', how='left', indicator=True); m[m['_merge'] == 'left_only']
Stack tablespd.concat([jan, feb], ignore_index=True) drill
Side by sidepd.concat([a, b], axis=1)
Merge twice then aggregateitems.merge(orders, on='order_id').merge(products, on='product_id').groupby('category')['qty'].sum() drill
Wide to longdf.melt(id_vars=['region'], var_name='month', value_name='total') drill
Long to widepivot needs unique index/column pairs; pivot_table aggregates.df.pivot(index='region', columns='month', values='total') drill
Stack and unstackdf.set_index(['region', 'month'])['total'].unstack() drill
Columns into rowsdf.set_index('region').stack() drill
Join on indexa.join(b, how='left')

Time series and ranks

Rolling averagedf['ma7'] = df['total'].rolling(7).mean() drill
Change from previousdf['diff'] = df['total'].diff() drill
Percent changedf['mom'] = df['total'].pct_change() drill
Running totaldf['cum'] = df['total'].cumsum() drill
Previous rowdf['prev'] = df['total'].shift(1)
Rankmethod='dense' for no gaps.df['rank'] = df['score'].rank(method='min', ascending=False) drill
Quartilesdf['q'] = pd.qcut(df['total'], 4, labels=['Q1', 'Q2', 'Q3', 'Q4']) drill
Bandsdf['band'] = pd.cut(df['age'], bins=[0, 18, 35, 65, 120], labels=['child', 'young', 'adult', 'senior']) drill
Expanding maxdf['peak'] = df['total'].expanding().max()
Exponential averagedf['ewm'] = df['total'].ewm(span=10).mean()

Apply and vectorize

Classify with a functiondf['size'] = df['total'].apply(lambda x: 'big' if x > 100 else 'small') drill
Vectorized instead of applyMuch faster than apply on large frames.np.where(df['total'] > 100, 'big', 'small')
Several conditionsnp.select([df['t'] > 100, df['t'] > 50], ['big', 'mid'], default='small')
Row-wise functionPrefer df['price'] * df['qty'].df.apply(lambda r: r['price'] * r['qty'], axis=1)
Assign in a chaindf.assign(net=lambda d: d['price'] * (1 - d['discount']))
Pipedf.pipe(clean).pipe(enrich)
Iterate (last resort)Avoid iterrows in loops over large data.for row in df.itertuples(): ...
Where / maskdf['total'].where(df['total'] > 0, 0)

Gotchas

SettingWithCopyWarningUse df.loc[mask, 'col'] = value, not df[mask]['col'] = value
Chained comparisons(df['a'] > 1) & (df['b'] < 5), never df['a'] > 1 and df['b'] < 5
NaN is not equal to NaNUse .isna(), not == np.nan
Integer column with NaN becomes floatUse the nullable 'Int64' dtype: df['qty'].astype('Int64')
groupby drops NaN keysdf.groupby('region', dropna=False)
merge duplicates rowsCheck keys are unique: validate='one_to_one' or 'many_to_one'
Copy vs viewdf2 = df.copy() before modifying a subset
Show all columnspd.set_option('display.max_columns', None)

pandasdrill.com: practice every one of these with instant checking. 105 entries.

Questions

Which pandas operations come up most in data interviews?

Filtering with boolean masks, groupby with named aggregations, merge with how='left', pivot_table, value_counts, handling missing values, string cleaning, and date parsing with resample. Window-style questions (running totals, month over month, rank per group) use cumsum, pct_change, shift and groupby transform.

apply or vectorized?

Vectorized. np.where, np.select and column arithmetic run in C and are often a hundred times faster than apply with a lambda. Use apply only when there is no vectorized form.

merge or join?

merge for columns as keys (the usual case), join for index-aligned frames. They do the same thing underneath.

What version is this for?

pandas 2.x. The code also runs on 1.5 except for a few dtype details; the nullable Int64 and string dtypes are the main differences.

A cheat sheet does not make them stick

Doing them with instant checking does. The free exercises need no account.

Start practicing