pandas cheat sheet
A scannable pandas reference: 23 short snippets across 10 topics, each linking back to the lesson it came from.
At a glance
| Topic | What it covers | |
|---|---|---|
| Series and DataFrame | A Series is a one-dimensional array with an index — labels attached to positions. Almost every confusing pandas | lesson |
| Reading and writing data | This sequence answers the questions that decide everything afterwards: how big is it, which columns have holes, are | lesson |
| Selecting, filtering and cleaning | loc, iloc, boolean filters, missing values and type conversion — the routine work that makes up most of a data task | lesson |
| Grouping, joining and reshaping | Plotting libraries and most statistical models expect long data; humans read wide tables more easily. melt and pivot | lesson |
| The index in depth: MultiIndex, set_index and reindex | Hierarchical indexes, stack and unstack, reindex, and the alignment rules that quietly decide whether your numbers land | lesson |
| Pivot tables and crosstab | A MultiIndex column set is fine inside a notebook and awkward everywhere else: CSV writers flatten it into odd header | lesson |
| String operations with .str | Build the pipeline in small steps and print the intermediate result after each one. Text cleaning fails by producing a | lesson |
| Categoricals, dtypes and memory optimisation | Always measure with deep=True: without it, an object column is reported as 8 bytes per row because only the pointers | lesson |
| Plotting and quick exploratory charts | df.plot, the chart kinds worth knowing, subplots for comparisons, styling, and when to hand the figure over to | lesson |
| Debugging pandas: SettingWithCopy, dtype and index surprises | The three failures that waste the most time - chained assignment, silent dtype coercion and misaligned indexes - and | lesson |
Quick snippets
Series and DataFrame
Series: a labelled column
import pandas as pd
s = pd.Series([10, 20, 30], index=["a", "b", "c"])
s["b"] # 20 - by label
s.iloc[1] # 20 - by position
s.to_numpy() # array([10, 20, 30])
DataFrame: a table
df = pd.DataFrame({
"name": ["Ada", "Grace", "Alan"],
"score": [88, 95, 71],
"team": ["A", "B", "A"]
})
df.shape # (3, 3)
df.columns # Index(['name', 'score', 'team'], dtype='object')
df.dtypes
df.head(2)
df.describe() # numeric summary
df.info() # types + non-null counts: the first thing to run
Automatic alignment
a = pd.Series([1, 2, 3], index=["x", "y", "z"])
b = pd.Series([10, 20, 30], index=["z", "y", "x"])
a + b
# x 31
# y 22
# z 13
# both were matched by label - order was irrelevantFull lesson: Series and DataFrame →
Reading and writing data
First five minutes with any dataset
df.shape
df.head()
df.info() # dtypes + missing counts
df.isna().sum().sort_values(ascending=False)
df.duplicated().sum()
df.describe(include="all")
df["category"].value_counts(dropna=False)
Writing data out
df.to_csv("clean.csv", index=False) # drop the index: it is not data
df.to_csv("clean.csv", index=False, encoding="utf-8")
df.to_excel("report.xlsx", sheet_name="Summary", index=False)
df.to_json("out.json", orient="records", indent=2)
df.to_parquet("out.parquet") # keeps dtypes, much smallerFull lesson: Reading and writing data →
Selecting, filtering and cleaning
Selecting columns and rows
df["name"] # one column -> Series
df[["name", "score"]] # several -> DataFrame
df.loc[df["score"] > 80, ["name", "score"]] # label-based, with a condition
df.iloc[0:3, 0:2] # position-based
df.at[0, "name"] # single scalar by label
df.iat[0, 0] # single scalar by position
Filtering
df[df["score"] > 80]
df[(df["team"] == "A") & (df["score"] > 80)] # & and |, with parentheses
df[df["team"].isin(["A", "C"])]
df[df["name"].str.startswith("A")]
df[df["score"].between(70, 90)]
df[~df["name"].isna()]
Cleaning
df.isna().sum()
df.dropna(subset=["score"]) # drop rows missing a key field
df["score"] = df["score"].fillna(df["score"].median())
df["team"] = df["team"].fillna("unknown")
df = df.drop_duplicates(subset=["name"], keep="first")
df["score"] = pd.to_numeric(df["score"], errors="coerce") # bad values -> NaN
df["date"] = pd.to_datetime(df["date"], errors="coerce")
df["name"] = df["name"].str.strip().str.title()Full lesson: Selecting, filtering and cleaning →
Grouping, joining and reshaping
groupby: split, apply, combine
df.groupby("team")["score"].mean()
df.groupby(["team", "level"])["score"].agg(["count", "mean", "max"])
summary = df.groupby("team").agg(
n=("score", "size"),
avg=("score", "mean"),
best=("name", lambda s: s.value_counts().index[0]),
).reset_index()
df.groupby("team")["score"].transform("mean") # same length as df
Merging like SQL
orders.merge(customers, on="customer_id", how="left")
orders.merge(customers, left_on="cust", right_on="id", how="inner")
pd.concat([jan, feb], ignore_index=True) # stack rows
pd.concat([a, b], axis=1) # side by side, aligned on index
Long and wide
# wide -> long
long = df.melt(id_vars=["name"], value_vars=["math", "science"],
var_name="subject", value_name="score")
# long -> wide
wide = long.pivot(index="name", columns="subject", values="score")
wide.reset_index(inplace=True)
# safe aggregation when keys repeat
long.pivot_table(index="name", columns="subject", values="score", aggfunc="mean")Full lesson: Grouping, joining and reshaping →
The index in depth: MultiIndex, set_index and reindex
stack and unstack
wide = t.unstack(level="month") # the month level becomes columns
wide.columns # MultiIndex: (sales, feb), (sales, jan)
long = wide.stack(level="month") # columns back into the index
long = wide.stack(future_stack=True) # future behaviour, no silent row dropping
flat = wide.copy()
flat.columns = ["_".join(map(str, c)) for c in flat.columns] # flatten to strings
flat.columns.name = None
flat = flat.reset_index()
reindex and alignment
s = pd.Series([1, 2, 3], index=["a", "b", "c"])
s.reindex(["a", "b", "c", "d"]) # d -> NaN
s.reindex(["c", "a"]) # reordered, not filtered randomly
s.reindex(["a", "b", "d"], fill_value=0) # missing filled with 0
# a fully defined rectangular index: every region x every month
idx = pd.MultiIndex.from_product([["north", "south"], ["jan", "feb", "mar"]])
t.reindex(idx, fill_value=0)
left = pd.DataFrame({"v": [1, 2]}, index=["x", "y"])
right = pd.DataFrame({"v": [10, 20]}, index=["y", "x"])
left + right # matched by label, so x = 11 and y = 22 despite the orderFull lesson: The index in depth: MultiIndex, set_index and reindex →
Pivot tables and crosstab
crosstab and normalisation
counts = pd.crosstab(df["region"], df["product"])
shares = pd.crosstab(df["region"], df["product"], normalize="index") # row percentages
totals = pd.crosstab(
df["region"], df["product"],
values=df["revenue"], aggfunc="sum", margins=True,
)
pd.crosstab(df["region"], [df["product"], df["channel"]]) # cross two columns
pd.crosstab(df["region"], df["product"]).stack().to_frame("n") # back to long
Flattening for the next reader
flat = pivot.copy()
flat.columns = ["_".join(str(p) for p in c).strip("_") for c in flat.columns]
flat.columns.name = None
flat.index.name = "region"
flat = flat.reset_index()
# or, keeping the hierarchy but renaming levels
long = multi.stack(level="product", future_stack=True).rename_axis(
columns=["stat", "product"]
).reset_index()Full lesson: Pivot tables and crosstab →
String operations with .str
The .str accessor
s = df["name"].astype("string") # nullable string dtype
s.str.strip().str.title()
s.str.len()
s.str.upper()
s.str.contains("ada", case=False, na=False)
s.str.startswith("A").fillna(False)
s.str.zfill(6)
s.str.pad(10, side="right", fillchar=".")
Extraction and replacement with regex
df["clean"] = df["text"].str.replace(r"\s+", " ", regex=True).str.strip()
df["digits"] = df["code"].str.extract(r"(\d+)", expand=False)
parsed = df["email"].str.extract(r"(?P<user>[^@]+)@(?P<domain>.+)$")
df["user"] = parsed["user"]
df["domain"] = parsed["domain"]
df["tag_list"] = df["tags"].str.findall(r"#(\w+)")
df["has_vip"] = df["tags"].str.contains(r"#vip\b", regex=True, na=False)
df["masked"] = df["phone"].str.replace(r"\d(?=\d{3})", "*", regex=True)Full lesson: String operations with .str →
Categoricals, dtypes and memory optimisation
Knowing the dtypes
df.dtypes
df.select_dtypes(include="number").columns
df.select_dtypes(include="category").columns
df["year"].astype("Int64") # capital I: nullable
df["ok"].astype("boolean")
df["name"].astype("string")
The category dtype
df["team"] = df["team"].astype("category")
df["team"].cat.categories # Index(['A', 'B'], dtype='object')
df["team"].cat.codes # the integer codes actually stored
df["size"] = pd.Categorical(
df["size"], categories=["s", "m", "l"], ordered=True
)
df["size"] > "m" # meaningful only because ordered=True
df["team"] = df["team"].cat.add_categories(["bench"]).cat.remove_unused_categories()
df["team"].value_counts(dropna=False)Full lesson: Categoricals, dtypes and memory optimisation →
Plotting and quick exploratory charts
One line to a chart
import matplotlib.pyplot as plt
df.plot(x="date", y="sales", kind="line", figsize=(9, 4), title="Daily sales")
df.groupby("team")["score"].mean().sort_values().plot(kind="barh")
df.plot.scatter(x="price", y="units", alpha=0.6, s=12)
df["score"].plot.hist(bins=20, edgecolor="white")
df.set_index("date")["sales"].resample("ME").sum().plot.area(stacked=False)
df[["score", "hours"]].plot(subplots=True, layout=(1, 2), sharex=False, figsize=(11, 4))
plt.tight_layout()
plt.show()
When to leave pandas
fig, axes = plt.subplots(1, 2, figsize=(11, 4))
long = df.melt(id_vars=["date"], value_vars=["sales", "returns"], var_name="metric")
for name, part in long.groupby("metric"):
axes[0].plot(part["date"], part["value"], label=name)
axes[0].legend(loc="upper left")
axes[0].set_title("Sales and returns")
df.boxplot(column="score", by="team", ax=axes[1])
plt.suptitle("")
plt.tight_layout()Full lesson: Plotting and quick exploratory charts →
Debugging pandas: SettingWithCopy, dtype and index surprises
Silent dtype coercion
df["n"].dtype # int64
df.loc[df["n"] > 0, "n"] = None # a gap appears
df["n"].dtype # float64 - 1234 became 1234.0
df["amount"] = pd.to_numeric(df["amount"], errors="coerce") # bad text -> NaN
df["id"] = df["id"].astype("string") # keep leading zeros
df["score"].sum() # NaN is skipped, so a total can look fine
df["score"].count(), len(df) # compare: the difference is the missing rows
df["score"].mean(skipna=False) # NaN if any value is missing - the honest read
Index and alignment bugs
s = pd.Series([100, 200], index=["x", "y"])
df["bonus"] = s # aligned by label: "x" and "y" match, everything else NaN
df["bonus"] = s.to_numpy() # positional: length must match exactly
df = df.reset_index(drop=True) # when the labels carry no meaning
# duplicate labels break reindex and make .loc return a frame, not a row
df.index.is_unique # False?
df = df[~df.index.duplicated(keep="first")]
pd.testing.assert_frame_equal(expected, actual, check_dtype=False)
pd.testing.assert_series_equal(s1, s2, rtol=1e-6, check_names=False)Full lesson: Debugging pandas: SettingWithCopy, dtype and index surprises →
FAQ
Is this pandas cheat sheet free to use?
Where do the examples come from?
How do I go deeper than a cheat sheet?
Related cheat sheets
Python 3 NumPy Matplotlib Jupyter Notebook Flask FastAPI
Last refreshed 2026-09-27.