Menu
CoddyTech

Pandas Cheat Sheet

Last updated

Create a DataFrame

Small DataFrames built by hand, for examples, tests and lookup tables.

TaskCode
Import pandas and NumPyimport pandas as pd import numpy as np
From a dict of columnsdf = pd.DataFrame({"name": ["Ada", "Ben"], "age": [36, 28]})
From a list of rowspd.DataFrame([{"name": "Ada", "age": 36}, {"name": "Ben", "age": 28}])
From a NumPy arraypd.DataFrame(np.arange(6).reshape(3, 2), columns=["x", "y"])
A Series with labelss = pd.Series([3, 5, 2], index=["a", "b", "c"])
A range of datespd.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.

TaskCode
Read a CSV filedf = pd.read_csv("sales.csv")
Only some columns, with real datespd.read_csv("sales.csv", usecols=["date", "city", "sales"], parse_dates=["date"])
Use a column as the indexpd.read_csv("sales.csv", index_col="id")
Tab or other separatorpd.read_csv("sales.tsv", sep="\t")
Treat extra markers as missingpd.read_csv("sales.csv", na_values=["n/a", "-"])
Read only the first rowspd.read_csv("sales.csv", nrows=1000)
Read an Excel sheet (needs openpyxl)pd.read_excel("report.xlsx", sheet_name="2026")
Read JSON recordspd.read_json("records.json")
Read from a SQL databasepd.read_sql("SELECT * FROM orders", conn)
Write a CSV without the indexdf.to_csv("clean.csv", index=False)
Write Excel or Parquetdf.to_excel("clean.xlsx", index=False), df.to_parquet("clean.parquet")

Inspect the data

The first calls to make on any new dataset.

TaskCode
First and last rowsdf.head(), df.tail(3)
Number of rows and columnsdf.shape # (6, 7)
Column types and non-null countsdf.info()
Summary statistics of numeric columnsdf.describe()
Summary of every columndf.describe(include="all")
Column names and typesdf.columns, df.dtypes
Count each valuedf["city"].value_counts()
Share of each valuedf["city"].value_counts(normalize=True)
Distinct values and how manydf["city"].unique(), df["city"].nunique()
Missing values per columndf.isna().sum()
Random sample of rowsdf.sample(3, random_state=0)
Correlation of numeric columnsdf.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.

TaskCode
One column, as a Seriesdf["age"]
Several columns, as a DataFramedf[["name", "age"]]
Rows and columns by labeldf.loc[0:2, ["name", "age"]]
Rows and columns by positiondf.iloc[0:2, 0:3]
One valuedf.at[0, "age"], df.iat[0, 3]
Last rowdf.iloc[-1]
Columns of one typedf.select_dtypes(include="number")
Text columns (pandas 3)df.select_dtypes(include="str")
Columns by name matchdf.filter(like="sal"), df.filter(regex="^(age|salary)$")
Look rows up by a columndf.set_index("name").loc["Ada", "salary"]
Back to a 0, 1, 2 indexdf.reset_index(drop=True)
Drop columns or rowsdf.drop(columns=["email"]), df.drop(index=[0, 1])

Filter rows

Wrap each condition in parentheses and combine them with & (and), | (or) and ~ (not).

TaskCode
Rows that match a conditiondf[df["age"] > 30]
Both conditionsdf[(df["age"] > 30) & (df["city"] == "Oslo")]
Either conditiondf[(df["city"] == "Oslo") | (df["salary"] > 6000)]
Value in a listdf[df["city"].isin(["Oslo", "Rome"])]
Value not in a listdf[~df["city"].isin(["Oslo", "Rome"])]
Between two values, ends includeddf[df["age"].between(30, 40)]
Filter with a query stringdf.query("age > 30 and city == 'Oslo'")
Use a variable in a querymin_age = 30 df.query("age > @min_age")
Text containsdf[df["name"].str.contains("e", case=False)]
Missing or present valuesdf[df["age"].isna()], df[df["age"].notna()]
Filter rows and pick columnsdf.loc[df["age"] > 30, ["name", "salary"]]
Top or bottom n rowsdf.nlargest(3, "salary"), df.nsmallest(2, "age")
Rows with a repeated valuedf[df.duplicated(subset=["city"], keep=False)]

Sort and rank

TaskCode
Sort by a columndf.sort_values("salary")
Largest firstdf.sort_values("salary", ascending=False)
Sort by several columnsdf.sort_values(["city", "salary"], ascending=[True, False])
Missing values firstdf.sort_values("age", na_position="first")
Sort by the indexdf.sort_index()
Rank values, 1 is the highestdf["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.

TaskCode
New column from othersdf["bonus"] = df["salary"] * 0.1
Add a column inside a chaindf.assign(bonus=lambda d: d["salary"] * 0.1)
Value from a conditiondf["level"] = np.where(df["age"] >= 40, "senior", "junior")
Value from several conditionsdf["band"] = np.select([df["age"] < 30, df["age"] < 45], ["young", "mid"], default="senior")
Change values where a condition holdsdf.loc[df["city"] == "Lima", "salary"] = 4000
Map values with a dictdf["country"] = df["city"].map({"Oslo": "NO", "Rome": "IT", "Lima": "PE"})
Apply a function to a columndf["salary"].apply(lambda x: round(x, -3))
Apply a function to every celldf[["age", "salary"]].map(lambda x: x / 1000)
Rename columnsdf.rename(columns={"salary": "pay"})
Change a column typedf["salary"].astype("float64")
Text to numbers, bad values to NaNpd.to_numeric(s, errors="coerce")
Numbers into labelled binspd.cut(df["age"], bins=[0, 30, 45, 120], labels=["young", "mid", "senior"])
Numbers into equal-size groupspd.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.

TaskCode
Share of missing values per columndf.isna().mean()
Drop rows with any missing valuedf.dropna()
Drop rows missing a key columndf.dropna(subset=["age"])
Fill with a value per columndf.fillna({"age": df["age"].median()})
Fill from the previous or next rowdf["age"].ffill(), df["age"].bfill()
Fill gaps by interpolationdf["age"].interpolate()
Turn markers into missing valuesdf.replace(["n/a", "-", ""], np.nan)
Drop duplicate rowsdf.drop_duplicates()
Keep the last row per keydf.drop_duplicates(subset=["city"], keep="last")
Tidy column namesdf.columns = df.columns.str.strip().str.lower().str.replace(" ", "_")
Cap extreme valuesdf["salary"].clip(lower=4000, upper=6000)
Category type for repeated textdf["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.

TaskCode
Upper, lower or title casedf["name"].str.upper(), df["name"].str.lower(), df["name"].str.title()
Trim spacesdf["name"].str.strip()
Replace textdf["email"].str.replace("mail.com", "example.com")
Replace with a regexdf["email"].str.replace(r"@.*", "", regex=True)
Contains, starts withdf["name"].str.contains("a"), df["name"].str.startswith("E")
Length of each valuedf["name"].str.len()
First charactersdf["name"].str[:2]
Split into columnsdf["email"].str.split("@", expand=True)
Extract with a regex groupdf["email"].str.extract(r"@(\w+)")
Join text columnsdf["name"] + " (" + df["city"] + ")"
Pad numbers with zerosdf["salary"].astype("str").str.zfill(6)
The new text dtypedf["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.

TaskCode
Parse a column to datetimesdf["date"] = pd.to_datetime(df["date"])
Parse with an exact formatpd.to_datetime(["03/01/2026", "15/01/2026"], format="%d/%m/%Y")
Year, month, weekday namedf["date"].dt.year, df["date"].dt.month, df["date"].dt.day_name()
Group dates by monthdf["date"].dt.to_period("M")
Days between two dates(pd.Timestamp("2026-12-31") - df["date"]).dt.days
Add days or monthsdf["date"] + pd.Timedelta(days=30), df["date"] + pd.DateOffset(months=1)
Rows in a date rangedf[df["date"].between("2021-01-01", "2022-12-31")]
Date index, sliced by textts = df.set_index("date").sort_index() ts.loc["2021":"2022"]
Total per monthts["salary"].resample("ME").sum()
Weekly mean; "QE" is quarterly, "YE" yearlyts["salary"].resample("W").mean()
Rolling averagets["salary"].rolling(3).mean()
Change from the previous rowts["salary"].diff(), ts["salary"].pct_change()
Shift values down one rowts["salary"].shift(1)
Running totalts["salary"].cumsum()
Set and convert time zonests.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.

TaskCode
Mean per groupdf.groupby("city")["salary"].mean()
Several statistics at oncedf.groupby("city")["salary"].agg(["mean", "max", "count"])
Named result columnsdf.groupby("city").agg(avg_pay=("salary", "mean"), people=("name", "count"))
Group by two columnsdf.groupby(["city", "team"])["salary"].sum()
Keep the keys as columnsdf.groupby("city", as_index=False)["salary"].mean()
Number of rows per groupdf.groupby("city").size()
Group result on every rowdf["city_avg"] = df.groupby("city")["salary"].transform("mean")
Keep only some groupsdf.groupby("city").filter(lambda g: len(g) >= 2)
Row with the maximum per groupdf.loc[df.groupby("city")["salary"].idxmax()]
Top n rows per groupdf.sort_values("salary", ascending=False).groupby("city").head(2)
Number the rows in each groupdf.groupby("city").cumcount()
Loop over the groupsfor 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.

TaskCode
Pivot tabledf.pivot_table(index="city", columns="team", values="salary", aggfunc="mean", fill_value=0)
Pivot table with totalsdf.pivot_table(index="city", values="salary", aggfunc="sum", margins=True)
Count combinationspd.crosstab(df["city"], df["team"])
Shares per rowpd.crosstab(df["city"], df["team"], normalize="index")
Wide to longlong = df.melt(id_vars="name", value_vars=["age", "salary"], var_name="field", value_name="value")
Long to widelong.pivot(index="name", columns="field", values="value")
Inner index level to columnsdf.groupby(["city", "team"])["salary"].sum().unstack(fill_value=0)
One row per list itempd.DataFrame({"id": [1, 2], "tags": [["a", "b"], ["c"]]}).explode("tags")
One-hot encode a columnpd.get_dummies(df["team"], prefix="team")
Swap rows and columnsdf.T

Combine DataFrames

concat stacks frames, merge joins them on key columns the way SQL does.

TaskCode
Stack rowspd.concat([df_2025, df_2026], ignore_index=True)
Place side by sidepd.concat([left, right], axis=1)
Join on a key columnorders.merge(customers, on="customer_id", how="left")
Join typeshow="inner", "left", "right", "outer"
Keys with different namesorders.merge(users, left_on="customer_id", right_on="id")
Check the join and mark each row's sourceorders.merge(customers, on="customer_id", how="left", validate="many_to_one", indicator=True)
Join on the indexleft.join(right, how="inner")
Rows with no match in another tableorders[~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.

TaskCode
Line chart of a seriests["salary"].plot()
Line chart from two columnsdf.plot(x="date", y="salary")
Bar chart of a summarydf.groupby("city")["salary"].mean().plot.bar()
Sorted horizontal barsdf["city"].value_counts().sort_values().plot.barh()
Histogramdf["salary"].plot.hist(bins=20)
Scatter plotdf.plot.scatter(x="age", y="salary")
Box plot per groupdf.boxplot(column="salary", by="city")
Title and axis labelax = df.plot(x="date", y="salary", title="Salary") ax.set_ylabel("EUR")
Show or save the chartplt.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.

Taskpandas 3Old form
Function on every celldf.map(f)df.applymap(f)
Fill from the previous rowdf.ffill()df.fillna(method="ffill")
Add rowspd.concat([df, new_rows])df.append(new_rows)
Month-end resamplingts["salary"].resample("ME").sum()resample("M")
Hourly datespd.date_range("2026-01-01", periods=24, freq="h")freq="H"
Set one valuedf.loc[0, "age"] = 37df["age"][0] = 37
Change a filtered subsethigh = df[df["salary"] > 5000] high["salary"] = 0high = 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?
Yes. This pandas cheat sheet is completely free, with no sign-up required. Bookmark it, or print it to PDF from your browser, and come back whenever you need the exact call for a groupby, a merge or a pivot table.
Which pandas version does this cheat sheet cover?
pandas 3. Text columns use the new 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?
Put each condition in parentheses and join them with & 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?
Since pandas 3, Copy-on-Write means every selection behaves like a copy. Chained assignment such as 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.
Can I run pandas code online?
Yes. The free Matplotlib playground runs Python with pandas, NumPy, Matplotlib and scikit-learn in your browser, so you can paste any snippet from this cheat sheet and run it without installing anything. Coddy's Machine Learning course teaches pandas step by step, from your first DataFrame to the data that feeds a model.
Coddy programming languages illustration

Learn Pandas with Coddy

GET STARTED