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

TopicWhat it covers
Series and DataFrameA Series is a one-dimensional array with an index — labels attached to positions. Almost every confusing pandaslesson
Reading and writing dataThis sequence answers the questions that decide everything afterwards: how big is it, which columns have holes, arelesson
Selecting, filtering and cleaningloc, iloc, boolean filters, missing values and type conversion — the routine work that makes up most of a data tasklesson
Grouping, joining and reshapingPlotting libraries and most statistical models expect long data; humans read wide tables more easily. melt and pivotlesson
The index in depth: MultiIndex, set_index and reindexHierarchical indexes, stack and unstack, reindex, and the alignment rules that quietly decide whether your numbers landlesson
Pivot tables and crosstabA MultiIndex column set is fine inside a notebook and awkward everywhere else: CSV writers flatten it into odd headerlesson
String operations with .strBuild the pipeline in small steps and print the intermediate result after each one. Text cleaning fails by producing alesson
Categoricals, dtypes and memory optimisationAlways measure with deep=True: without it, an object column is reported as 8 bytes per row because only the pointerslesson
Plotting and quick exploratory chartsdf.plot, the chart kinds worth knowing, subplots for comparisons, styling, and when to hand the figure over tolesson
Debugging pandas: SettingWithCopy, dtype and index surprisesThe three failures that waste the most time - chained assignment, silent dtype coercion and misaligned indexes - andlesson

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 irrelevant

Full 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 smaller

Full 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 order

Full 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?
Yes. No sign-up and no tracking: the page is static, every example is on the page itself, and you can print it or save it as a one-page reference.
Where do the examples come from?
Every snippet is taken from the 10 lessons of the pandas course on this site, and each section links back to the lesson it was pulled from.
How do I go deeper than a cheat sheet?
Open the full pandas course — it carries the worked explanations, the edge cases and the exercises behind every line here.

Python 3 NumPy Matplotlib Jupyter Notebook Flask FastAPI

Last refreshed 2026-09-27.