Pandas Cheat Sheet
Last updated
Create a DataFrame
Small DataFrames built by hand, for examples, tests and lookup tables.
| Task | Code |
|---|---|
| Import pandas and NumPy | import pandas as pd
import numpy as np |
| From a dict of columns | df = pd.DataFrame({"name": ["Ada", "Ben"], "age": [36, 28]}) |
| From a list of rows | pd.DataFrame([{"name": "Ada", "age": 36}, {"name": "Ben", "age": 28}]) |
| From a NumPy array | pd.DataFrame(np.arange(6).reshape(3, 2), columns=["x", "y"]) |
| A Series with labels | s = pd.Series([3, 5, 2], index=["a", "b", "c"]) |
| A range of dates | pd.date_range("2026-01-01", periods=7, freq="D") |
Read and write files
One call loads a file into a DataFrame, and one call writes it back.
| Task | Code |
|---|---|
| Read a CSV file | df = pd.read_csv("sales.csv") |
| Only some columns, with real dates | pd.read_csv("sales.csv", usecols=["date", "city", "sales"], parse_dates=["date"]) |
| Use a column as the index | pd.read_csv("sales.csv", index_col="id") |
| Tab or other separator | pd.read_csv("sales.tsv", sep="\t") |
| Treat extra markers as missing | pd.read_csv("sales.csv", na_values=["n/a", "-"]) |
| Read only the first rows | pd.read_csv("sales.csv", nrows=1000) |
| Read an Excel sheet (needs openpyxl) | pd.read_excel("report.xlsx", sheet_name="2026") |
| Read JSON records | pd.read_json("records.json") |
| Read from a SQL database | pd.read_sql("SELECT * FROM orders", conn) |
| Write a CSV without the index | df.to_csv("clean.csv", index=False) |
| Write Excel or Parquet | df.to_excel("clean.xlsx", index=False), df.to_parquet("clean.parquet") |
Inspect the data
The first calls to make on any new dataset.
| Task | Code |
|---|---|
| First and last rows | df.head(), df.tail(3) |
| Number of rows and columns | df.shape # (6, 7) |
| Column types and non-null counts | df.info() |
| Summary statistics of numeric columns | df.describe() |
| Summary of every column | df.describe(include="all") |
| Column names and types | df.columns, df.dtypes |
| Count each value | df["city"].value_counts() |
| Share of each value | df["city"].value_counts(normalize=True) |
| Distinct values and how many | df["city"].unique(), df["city"].nunique() |
| Missing values per column | df.isna().sum() |
| Random sample of rows | df.sample(3, random_state=0) |
| Correlation of numeric columns | df.corr(numeric_only=True) |
Select columns and rows
loc selects by label and includes the end of a slice. iloc selects by position and excludes it.
| Task | Code |
|---|---|
| One column, as a Series | df["age"] |
| Several columns, as a DataFrame | df[["name", "age"]] |
| Rows and columns by label | df.loc[0:2, ["name", "age"]] |
| Rows and columns by position | df.iloc[0:2, 0:3] |
| One value | df.at[0, "age"], df.iat[0, 3] |
| Last row | df.iloc[-1] |
| Columns of one type | df.select_dtypes(include="number") |
| Text columns (pandas 3) | df.select_dtypes(include="str") |
| Columns by name match | df.filter(like="sal"), df.filter(regex="^(age|salary)$") |
| Look rows up by a column | df.set_index("name").loc["Ada", "salary"] |
| Back to a 0, 1, 2 index | df.reset_index(drop=True) |
| Drop columns or rows | df.drop(columns=["email"]), df.drop(index=[0, 1]) |
Filter rows
Wrap each condition in parentheses and combine them with & (and), | (or) and ~ (not).
| Task | Code |
|---|---|
| Rows that match a condition | df[df["age"] > 30] |
| Both conditions | df[(df["age"] > 30) & (df["city"] == "Oslo")] |
| Either condition | df[(df["city"] == "Oslo") | (df["salary"] > 6000)] |
| Value in a list | df[df["city"].isin(["Oslo", "Rome"])] |
| Value not in a list | df[~df["city"].isin(["Oslo", "Rome"])] |
| Between two values, ends included | df[df["age"].between(30, 40)] |
| Filter with a query string | df.query("age > 30 and city == 'Oslo'") |
| Use a variable in a query | min_age = 30
df.query("age > @min_age") |
| Text contains | df[df["name"].str.contains("e", case=False)] |
| Missing or present values | df[df["age"].isna()], df[df["age"].notna()] |
| Filter rows and pick columns | df.loc[df["age"] > 30, ["name", "salary"]] |
| Top or bottom n rows | df.nlargest(3, "salary"), df.nsmallest(2, "age") |
| Rows with a repeated value | df[df.duplicated(subset=["city"], keep=False)] |
Sort and rank
| Task | Code |
|---|---|
| Sort by a column | df.sort_values("salary") |
| Largest first | df.sort_values("salary", ascending=False) |
| Sort by several columns | df.sort_values(["city", "salary"], ascending=[True, False]) |
| Missing values first | df.sort_values("age", na_position="first") |
| Sort by the index | df.sort_index() |
| Rank values, 1 is the highest | df["salary"].rank(ascending=False) |
Add and change columns
Whole-column operations are fast. Reach for apply only when no built-in method does the job.
| Task | Code |
|---|---|
| New column from others | df["bonus"] = df["salary"] * 0.1 |
| Add a column inside a chain | df.assign(bonus=lambda d: d["salary"] * 0.1) |
| Value from a condition | df["level"] = np.where(df["age"] >= 40, "senior", "junior") |
| Value from several conditions | df["band"] = np.select([df["age"] < 30, df["age"] < 45], ["young", "mid"], default="senior") |
| Change values where a condition holds | df.loc[df["city"] == "Lima", "salary"] = 4000 |
| Map values with a dict | df["country"] = df["city"].map({"Oslo": "NO", "Rome": "IT", "Lima": "PE"}) |
| Apply a function to a column | df["salary"].apply(lambda x: round(x, -3)) |
| Apply a function to every cell | df[["age", "salary"]].map(lambda x: x / 1000) |
| Rename columns | df.rename(columns={"salary": "pay"}) |
| Change a column type | df["salary"].astype("float64") |
| Text to numbers, bad values to NaN | pd.to_numeric(s, errors="coerce") |
| Numbers into labelled bins | pd.cut(df["age"], bins=[0, 30, 45, 120], labels=["young", "mid", "senior"]) |
| Numbers into equal-size groups | pd.qcut(df["salary"], q=4, labels=False) |
| Loop over rows (slow, last resort) | for row in df.itertuples():
print(row.name, row.salary) |
Missing values and cleaning
Missing numbers show as NaN, missing dates as NaT. Each of these calls returns a new object, so assign the result.
| Task | Code |
|---|---|
| Share of missing values per column | df.isna().mean() |
| Drop rows with any missing value | df.dropna() |
| Drop rows missing a key column | df.dropna(subset=["age"]) |
| Fill with a value per column | df.fillna({"age": df["age"].median()}) |
| Fill from the previous or next row | df["age"].ffill(), df["age"].bfill() |
| Fill gaps by interpolation | df["age"].interpolate() |
| Turn markers into missing values | df.replace(["n/a", "-", ""], np.nan) |
| Drop duplicate rows | df.drop_duplicates() |
| Keep the last row per key | df.drop_duplicates(subset=["city"], keep="last") |
| Tidy column names | df.columns = df.columns.str.strip().str.lower().str.replace(" ", "_") |
| Cap extreme values | df["salary"].clip(lower=4000, upper=6000) |
| Category type for repeated text | df["city"] = df["city"].astype("category") |
Text columns (.str)
In pandas 3 text columns get the str dtype by default. The .str methods work on the whole column, and missing values stay missing.
| Task | Code |
|---|---|
| Upper, lower or title case | df["name"].str.upper(), df["name"].str.lower(), df["name"].str.title() |
| Trim spaces | df["name"].str.strip() |
| Replace text | df["email"].str.replace("mail.com", "example.com") |
| Replace with a regex | df["email"].str.replace(r"@.*", "", regex=True) |
| Contains, starts with | df["name"].str.contains("a"), df["name"].str.startswith("E") |
| Length of each value | df["name"].str.len() |
| First characters | df["name"].str[:2] |
| Split into columns | df["email"].str.split("@", expand=True) |
| Extract with a regex group | df["email"].str.extract(r"@(\w+)") |
| Join text columns | df["name"] + " (" + df["city"] + ")" |
| Pad numbers with zeros | df["salary"].astype("str").str.zfill(6) |
| The new text dtype | df["name"].dtype # str |
Dates and times
The .dt accessor works on datetime columns. A sorted date index (ts below) unlocks slicing by text and resampling.
| Task | Code |
|---|---|
| Parse a column to datetimes | df["date"] = pd.to_datetime(df["date"]) |
| Parse with an exact format | pd.to_datetime(["03/01/2026", "15/01/2026"], format="%d/%m/%Y") |
| Year, month, weekday name | df["date"].dt.year, df["date"].dt.month, df["date"].dt.day_name() |
| Group dates by month | df["date"].dt.to_period("M") |
| Days between two dates | (pd.Timestamp("2026-12-31") - df["date"]).dt.days |
| Add days or months | df["date"] + pd.Timedelta(days=30), df["date"] + pd.DateOffset(months=1) |
| Rows in a date range | df[df["date"].between("2021-01-01", "2022-12-31")] |
| Date index, sliced by text | ts = df.set_index("date").sort_index()
ts.loc["2021":"2022"] |
| Total per month | ts["salary"].resample("ME").sum() |
Weekly mean; "QE" is quarterly, "YE" yearly | ts["salary"].resample("W").mean() |
| Rolling average | ts["salary"].rolling(3).mean() |
| Change from the previous row | ts["salary"].diff(), ts["salary"].pct_change() |
| Shift values down one row | ts["salary"].shift(1) |
| Running total | ts["salary"].cumsum() |
| Set and convert time zones | ts.index.tz_localize("UTC").tz_convert("Europe/Oslo") |
Group and aggregate
groupby splits the rows by key, applies a function to each group and combines the results.
| Task | Code |
|---|---|
| Mean per group | df.groupby("city")["salary"].mean() |
| Several statistics at once | df.groupby("city")["salary"].agg(["mean", "max", "count"]) |
| Named result columns | df.groupby("city").agg(avg_pay=("salary", "mean"), people=("name", "count")) |
| Group by two columns | df.groupby(["city", "team"])["salary"].sum() |
| Keep the keys as columns | df.groupby("city", as_index=False)["salary"].mean() |
| Number of rows per group | df.groupby("city").size() |
| Group result on every row | df["city_avg"] = df.groupby("city")["salary"].transform("mean") |
| Keep only some groups | df.groupby("city").filter(lambda g: len(g) >= 2) |
| Row with the maximum per group | df.loc[df.groupby("city")["salary"].idxmax()] |
| Top n rows per group | df.sort_values("salary", ascending=False).groupby("city").head(2) |
| Number the rows in each group | df.groupby("city").cumcount() |
| Loop over the groups | for city, group in df.groupby("city"):
print(city, len(group)) |
| Filter, group and sort in one chain | (df.query("age > 30")
.groupby("city", as_index=False)
.agg(avg_pay=("salary", "mean"))
.sort_values("avg_pay", ascending=False)) |
Pivot and reshape
Long data has one row per observation. Wide data spreads one variable across columns.
| Task | Code |
|---|---|
| Pivot table | df.pivot_table(index="city", columns="team", values="salary", aggfunc="mean", fill_value=0) |
| Pivot table with totals | df.pivot_table(index="city", values="salary", aggfunc="sum", margins=True) |
| Count combinations | pd.crosstab(df["city"], df["team"]) |
| Shares per row | pd.crosstab(df["city"], df["team"], normalize="index") |
| Wide to long | long = df.melt(id_vars="name", value_vars=["age", "salary"], var_name="field", value_name="value") |
| Long to wide | long.pivot(index="name", columns="field", values="value") |
| Inner index level to columns | df.groupby(["city", "team"])["salary"].sum().unstack(fill_value=0) |
| One row per list item | pd.DataFrame({"id": [1, 2], "tags": [["a", "b"], ["c"]]}).explode("tags") |
| One-hot encode a column | pd.get_dummies(df["team"], prefix="team") |
| Swap rows and columns | df.T |
Combine DataFrames
concat stacks frames, merge joins them on key columns the way SQL does.
| Task | Code |
|---|---|
| Stack rows | pd.concat([df_2025, df_2026], ignore_index=True) |
| Place side by side | pd.concat([left, right], axis=1) |
| Join on a key column | orders.merge(customers, on="customer_id", how="left") |
| Join types | how="inner", "left", "right", "outer" |
| Keys with different names | orders.merge(users, left_on="customer_id", right_on="id") |
| Check the join and mark each row's source | orders.merge(customers, on="customer_id", how="left", validate="many_to_one", indicator=True) |
| Join on the index | left.join(right, how="inner") |
| Rows with no match in another table | orders[~orders["customer_id"].isin(customers["customer_id"])] |
Plot with pandas
pandas draws with Matplotlib. Every plot call returns a Matplotlib ax, so you can keep styling it.
| Task | Code |
|---|---|
| Line chart of a series | ts["salary"].plot() |
| Line chart from two columns | df.plot(x="date", y="salary") |
| Bar chart of a summary | df.groupby("city")["salary"].mean().plot.bar() |
| Sorted horizontal bars | df["city"].value_counts().sort_values().plot.barh() |
| Histogram | df["salary"].plot.hist(bins=20) |
| Scatter plot | df.plot.scatter(x="age", y="salary") |
| Box plot per group | df.boxplot(column="salary", by="city") |
| Title and axis label | ax = df.plot(x="date", y="salary", title="Salary")
ax.set_ylabel("EUR") |
| Show or save the chart | plt.show(), ax.figure.savefig("salary.png", dpi=150) |
pandas 3: old forms and what to write now
Code copied from older tutorials often fails or warns in pandas 3. Write the forms in the pandas 3 column instead.
| Task | pandas 3 | Old form |
|---|---|---|
| Function on every cell | df.map(f) | df.applymap(f) |
| Fill from the previous row | df.ffill() | df.fillna(method="ffill") |
| Add rows | pd.concat([df, new_rows]) | df.append(new_rows) |
| Month-end resampling | ts["salary"].resample("ME").sum() | resample("M") |
| Hourly dates | pd.date_range("2026-01-01", periods=24, freq="h") | freq="H" |
| Set one value | df.loc[0, "age"] = 37 | df["age"][0] = 37 |
| Change a filtered subset | high = df[df["salary"] > 5000]
high["salary"] = 0 | high = df[df["salary"] > 5000].copy() |
| Is this a text column? | df["name"].dtype == "str" | df["name"].dtype == object |
The pandas code a working data analyst reaches for every day, grouped by task: create and load data, inspect it, select and filter rows, add columns, clean missing values, work with text and dates, group and aggregate, reshape, merge and plot. Each row is a short snippet you can paste, plus one line on what it does.
Everything here is written for pandas 3, the version Coddy runs: text columns use the new str dtype, Copy-on-Write is always on, and removed APIs such as applymap and fillna(method=...) appear only in the table of old forms. The examples use a DataFrame df with the columns name, city, team, age, salary, date and email. New to Python itself? Start with the Python cheat sheet and the Python playground, and keep the NumPy, Matplotlib and scikit-learn cheat sheets next to this one.
Pandas cheat sheet FAQ
Is this pandas cheat sheet free?
Which pandas version does this cheat sheet cover?
str dtype, Copy-on-Write is always on, and calls removed in pandas 3 (applymap, fillna(method=...), append, the M and H frequency aliases) appear only in the last table, next to what to write instead. Almost every snippet also runs on pandas 2.2, apart from the rows that rely on the str dtype or on Copy-on-Write.What is the difference between loc and iloc?
loc selects by label: the index values and column names, and a slice such as df.loc[0:2] includes its end. iloc selects by integer position, like a Python list, so df.iloc[0:2] returns the first two rows and excludes the end. Use loc with a condition to filter rows and pick columns in one step: df.loc[df["age"] > 30, ["name"]].How do I filter a DataFrame on several conditions?
& for and, | for or, and ~ for not: df[(df["age"] > 30) & (df["city"] == "Oslo")]. Python's and and or do not work on whole columns. For long filters, df.query("age > 30 and city == 'Oslo'") is easier to read.What is the difference between merge, join and concat?
merge combines two DataFrames on key columns, like a SQL join, with how="inner", "left", "right" or "outer". join is a shortcut that merges on the index. concat does not match keys at all: it stacks frames on top of each other, or side by side with axis=1.Why does my assignment not change the DataFrame?
df["age"][0] = 37 changes a temporary object and leaves df untouched. Write it as one loc call instead: df.loc[0, "age"] = 37. Methods such as dropna and fillna also return a new object, so assign the result.