pandas Essentials¶
The workhorse of tabular data in Python: load, clean, transform, aggregate, and merge datasets with expressive, vectorized operations.
Table of Contents¶
- Installation / Setup
- Core Syntax
- Common Operations
- Practical Examples
- Common Patterns
- Best Practices
- Common Mistakes
- Troubleshooting
- Related Cheat Sheets
- Official Documentation
Installation / Setup¶
Use pandas ≥ 2.x and install pyarrow (the modern engine for Parquet and string data).
Core Syntax¶
Series & DataFrame¶
s = pd.Series([10, 20, 30], index=["a", "b", "c"], name="values")
df = pd.DataFrame({
"name": ["ada", "lin", "sam"],
"score": [92, 85, 78],
})
df = pd.DataFrame({"a": [1, 2], "b": [3, 4]}, index=["x", "y"])
Reading & writing files¶
| Format | Read | Write |
|---|---|---|
| CSV | pd.read_csv("f.csv") |
df.to_csv("f.csv", index=False) |
| Parquet | pd.read_parquet("f.parquet") |
df.to_parquet("f.parquet", index=False) |
| Excel | pd.read_excel("f.xlsx", sheet_name=0) |
df.to_excel("f.xlsx", index=False) |
| JSON | pd.read_json("f.json") |
df.to_json("f.json", orient="records") |
| SQL | pd.read_sql("SELECT * FROM t", conn) |
df.to_sql("t", conn, if_exists="replace") |
| Clipboard/HTML | pd.read_clipboard(), pd.read_html(url) |
— |
pd.read_csv("data.csv",
sep=",", encoding="utf-8", # delimiters & text encoding
index_col="id", # use a column as index
usecols=["a", "b", "c"], # read only some columns
parse_dates=["date"], # parse datetime columns
dtype={"zip": str}, # force dtypes (keep leading zeros)
na_values=["", "n/a", "NULL"], # treat as missing
skiprows=1, nrows=10_000) # skip/limit rows
For very large files, read in chunks or use Polars: for chunk in pd.read_csv("big.csv", chunksize=100_000): ....
Inspection¶
df.head() / df.tail(3) # first / last rows
df.info() # dtypes, non-null counts, memory
df.describe() # summary stats of numeric columns
df.shape # (rows, cols)
df.columns # column labels
df.index # row labels
df.dtypes # per-column dtype
df["col"].value_counts(normalize=True) # distribution
df["col"].nunique()
df.memory_usage(deep=True) # real memory incl. strings
Common Operations¶
Selection & filtering¶
df["col"] # Series
df[["a", "b"]] # DataFrame (list = columns)
df.loc["row_label"] # label-based
df.loc[df["score"] > 80, ["name"]] # rows + columns by label
df.iloc[0] # first row, position-based
df.iloc[1:4, 0:2] # positional slice
df.at["x", "a"] # single value, fast
df.iat[0, 1]
mask = df["score"].between(60, 90)
mask = df["name"].isin(["ada", "sam"])
mask = df["name"].str.contains("^a", regex=True)
df[mask] # boolean filtering
df.query("score > 80 and name != 'sam'") # query syntax
Indexing¶
df = df.set_index("id") # move column -> index
df = df.reset_index() # index -> column
df.sort_index() # sort by row labels
df.set_axis(new_names, axis=1) # rename columns positionally
df.reindex(["a", "b", "c"]) # conform to new index (fills/aligns)
.loc is label-inclusive, .iloc is position-exclusive on the right edge.
Missing values¶
df.isna().sum() # missing count per column
df.notna()
df.dropna() # drop rows with any NaN
df.dropna(subset=["score"]) # drop only if key col missing
df.dropna(how="all") # drop fully-empty rows
df.fillna(0) # constant
df.fillna(df["col"].median()) # per-column value
df["col"].ffill() # forward fill
df["col"].bfill() # backward fill
df["col"].interpolate() # linear interpolation
df.replace({"old": "new", -1: None})
df.isna().sum() after every load is a reflex worth keeping.
Duplicates¶
df.duplicated() # boolean Series
df.duplicated(subset=["email"]) # only these columns
df.drop_duplicates() # keep first by default
df.drop_duplicates(subset=["user"], keep="last")
Sorting¶
df.sort_values("score", ascending=False)
df.sort_values(["score", "name"], ascending=[False, True])
df.sort_values("score", na_position="first")
df.sort_index()
groupby & aggregation¶
df.groupby("region")["sales"].sum()
df.groupby("region")["sales"].agg(["sum", "mean", "count"])
df.groupby(["region", "year"]).agg(
total=("sales", "sum"),
avg_score=("score", "mean"),
)
df.groupby("region")["sales"].transform("mean") # same length as df
df.groupby("region").filter(lambda g: g["sales"].sum() > 1000)
df.groupby("region").apply(custom_fn) # flexible, slower
df.groupby(pd.Grouper(key="date", freq="ME"))["sales"].sum() # monthly
Merge, join, concat¶
pd.merge(df_a, df_b, on="user_id") # shared column
pd.merge(df_a, df_b, on="user_id", how="left") # keep all of df_a
pd.merge(df_a, df_b, left_on="id", right_on="user", suffixes=("", "_b"))
df_a.join(df_b, on="user_id") # join on index or column, left by default
df_a.join(df_b.set_index("id"), on="user_id")
pd.concat([df_jan, df_feb], axis=0, ignore_index=True) # stack rows
pd.concat([df_x, df_y], axis=1) # side by side
how |
Meaning |
|---|---|
inner |
Only matching keys (default) |
left / right |
All rows of one side, fill the other |
outer |
Union of keys, NaN where missing |
Watch for duplicated keys multiplying rows in merges — check with df_a["key"].duplicated().any() first.
Pivot & reshape¶
df.pivot(index="date", columns="city", values="temp") # long -> wide
df.pivot_table(index="region", columns="year",
values="sales", aggfunc="sum", margins=True)
pd.crosstab(df["region"], df["churned"]) # counts table
df.melt(id_vars=["user"], value_vars=["q1", "q2"], # wide -> long
var_name="quarter", value_name="score")
df.stack() / df.unstack() # index <-> columns
df.wide_to_long(df, stubnames=["val"], i="id", j="time")
Keep data tidy: one row per observation, one column per variable — reshape with melt/pivot before analysis.
Datetime¶
df["date"] = pd.to_datetime(df["date"]) # parse
df["date"].dt.year / .dt.month / .dt.day # extract parts
df["date"].dt.strftime("%Y-%m-%d") # format
df["date"].dt.dayofweek # 0=Monday
df["date"].dt.to_period("M") # month period
idx = pd.date_range("2024-01-01", periods=12, freq="ME")
df.set_index("date").resample("W").sum() # weekly totals
df.set_index("date").resample("D").ffill() # daily forward fill
df["date"].shift(1) # previous value (lag)
| Resample alias | Meaning |
|---|---|
D, W, ME, QE, YE |
day, week, month/quarter/year end |
H, T, S |
hour, minute, second |
W-MON |
weekly anchored to Monday |
String operations (str accessor)¶
df["name"].str.strip().str.lower()
df["email"].str.contains("@", na=False)
df["code"].str.replace(r"\s+", "", regex=True)
df["full"].str.split("_", expand=True) # -> multiple columns
df["text"].str.extract(r"(?P<num>\d+)") # regex groups
df["name"].str.startswith("a") / .str.len()
df["name"].str.cat(sep=", ") # join rows
String ops are elementwise Python (slower than numeric) — vectorize with regex carefully; for huge text columns use str dtype with pyarrow or Polars.
Categorical data¶
df["cat"] = df["cat"].astype("category")
df["cat"].cat.categories # unique categories
df["cat"].cat.codes # integer codes
df["cat"].cat.rename_categories(["low", "high"])
df["cat"].cat.reorder_categories(["low", "med", "high"], ordered=True)
df["cat"].cat.set_categories(["a", "b", "c", "d"]) # add unseen
Categoricals cut memory (repeated strings stored once) and give ordered semantics.
apply / map¶
df["score"].map(lambda x: x * 2) # Series elementwise
df["grade"] = df["score"].map({90: "A", 80: "B"}) # dict lookup
df["col"].apply(custom_func) # Series, elementwise
df.apply(func, axis=1) # per row (SLOW — prefer vectorized)
df["col"].map(fn) # preferred alias since pandas 2.1
Vectorized ops and groupby.transform beat apply(axis=1) by orders of magnitude on big frames.
Performance tips¶
- Vectorize: prefer
df["a"] + df["b"]overapply. - Use
query/ boolean masks over Python-side row loops. - Downcast dtypes:
df["c"] = df["c"].astype("float32"); categoricals for repeated strings. - Choose
engine="pyarrow"for Parquet I/O and string dtype. - Chain operations lazily — pandas 3.x makes copy-on-write default (no more
SettingWithCopy). - Read only what you need:
usecols,dtype,parse_dates,chunksize. - Keep the index simple (default RangeIndex) unless you need lookups;
set_indexspeeds label-based reads. - Profile big pipelines with
.to_numpy()handoffs to NumPy where pandas overhead dominates.
Practical Examples¶
1. End-to-end EDA workflow¶
df = pd.read_csv("sales.csv", parse_dates=["date"])
# Clean
df = df.drop_duplicates(subset=["order_id"])
df["amount"] = pd.to_numeric(df["amount"], errors="coerce")
df = df.dropna(subset=["amount"])
# Inspect
print(df.info()); print(df.describe())
# Aggregate
monthly = (df.set_index("date")
.resample("ME")["amount"]
.agg(["sum", "count"]))
top_customers = df.groupby("customer_id")["amount"].sum().nlargest(10)
2. Feature engineering¶
X = pd.read_csv("users.csv", parse_dates=["signup_date"])
X["signup_year"] = X["signup_date"].dt.year
X["active_days"] = (pd.Timestamp("today") - X["signup_date"]).dt.days
X["email_provider"] = X["email"].str.split("@").str[1]
X["is_premium"] = X["plan"].isin(["pro", "enterprise"]).astype(int)
X["plan"] = X["plan"].astype("category")
X = pd.get_dummies(X, columns=["plan"], dtype=int) # or keep categorical
3. Merge features with labels for modeling¶
features = pd.read_parquet("features.parquet")
labels = pd.read_csv("labels.csv")
data = features.merge(labels, on="user_id", how="inner", validate="one_to_one")
X = data.drop(columns=["user_id", "churned"])
y = data["churned"]
Common Patterns¶
# Missing-value report
missing = df.isna().sum()
missing = missing[missing > 0].sort_values(ascending=False)
# First/last per group (top row per group)
df.sort_values("date").groupby("user_id").tail(1)
# Rolling mean
df.set_index("date")["sales"].rolling(7).mean()
# Relative frequency table
df.groupby("region")["churned"].mean() # churn rate by region
# Z-score column
df["z"] = (df["score"] - df["score"].mean()) / df["score"].std()
# Check merge cardinality before joining
assert not df["key"].duplicated().any(), "duplicate keys would multiply rows"
Best Practices¶
- Always check
df.isna().sum()and dtypes right after loading. - Prefer vectorized operations and
groupby.transformoverapply(axis=1). - Write files with
index=Falseunless the index is meaningful. - Pass
errors="coerce"when converting messy columns, then handle NaN explicitly. - Use
how="left"merges deliberately and validate cardinality (validate=...). - Keep data tidy (long format) and reshape only for display/features.
- Set copy-on-write explicitly (
pd.set_option("mode.copy_on_write", True)) on pandas 2.x for safety. - Use Polars or chunking when DataFrames exceed memory.
Common Mistakes¶
| Mistake | Consequence | Correct approach |
|---|---|---|
df[df.col == 5] chain after chain |
SettingWithCopyWarning, silent misses |
Assign to a copy or use .loc |
dropna() without subset |
Loses rows with any missing cell | dropna(subset=[...]) |
Chained df["a"]["b"] assignment |
SettingWithCopy | df.loc[mask, "col"] = value |
fillna(0) before aggregation |
Biased means | Fill with median or drop |
| Merge without checking duplicates | Rows multiply silently | validate="one_to_one" |
read_csv on large file |
Slow, memory-heavy | dtype, usecols, chunksize, or Polars |
to_csv(index=True) |
Mysterious Unnamed: 0 column |
index=False |
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
KeyError: 'col' |
Column name mismatch (spaces/case) | df.columns.tolist() to inspect |
SettingWithCopyWarning |
Chained assignment on a slice | Use .loc or enable copy-on-write |
| Dates read as strings | parse_dates not set |
Pass parse_dates=[...]; pd.to_datetime |
PerformanceWarning |
Slicing non-monotonic index | df.sort_index() or use sort_values first |
| Merge output grew huge | Duplicate keys | Deduplicate keys before merge |
MemoryError |
Whole file in RAM | Chunks, dtypes, categoricals, or Polars |
Column of object dtype slow |
Mixed/text columns | astype("string") with pyarrow backend |
Related Cheat Sheets¶
- NumPy — the array engine under pandas
- Polars — faster alternative for large tabular data
- SciPy · statsmodels — statistics on cleaned frames
- Python Essentials · Python Standard Library —
csv,json,collections - Data Visualization — plot the results
- Home · Central Navigation Index
Official Documentation¶
- pandas documentation
- pandas cheatsheet (official)
- 10 minutes to pandas
- pandas user guide: merging & reshaping
- Copy-on-Write
Last Updated¶
2026-09-06 — Initial version.