CodeKitchen prep
0%
Contest in--:--:--
Python Β· lesson 04/13

🐼pandas in 15 minutes

⏳ ~15 min

If a problem hands you a CSV or asks you to "implement transform(df)", pandas is the fastest way to write it. Each idea below is shown next to its SQL equivalent, since you'll think in both.

Load & inspect

import pandas as pd
df = pd.read_csv("orders.csv", dtype={"zip": str}, parse_dates=["order_date"])
df = pd.read_json("events.jsonl", lines=True)
df.info(); df.head(); df.describe(include="all"); df.isna().sum(); df.nunique()
df = pd.json_normalize(list_of_nested_dicts, sep=".")   # flatten JSON into columns

SQL ↔ pandas Rosetta stone

SQLpandas
WHERE status='done' AND amt>10df[(df.status == "done") & (df.amt > 10)] or df.query("status == 'done' and amt > 10")
WHERE city IN ('A','B')df[df.city.isin(["A", "B"])]
WHERE x IS NULLdf[df.x.isna()]
SELECT a, b AS cdf[["a", "b"]].rename(columns={"b": "c"})
GROUP BY k + aggregatesdf.groupby("k", as_index=False).agg(n=("id","size"), total=("amt","sum"), users=("user","nunique"))
HAVING COUNT(*) > 5g[g.n > 5] or df.groupby("k").filter(lambda g: len(g) > 5)
JOIN … ONa.merge(b, on="id", how="inner|left|right|outer", validate="m:1")
ORDER BY t DESC, n ASCdf.sort_values(["t", "n"], ascending=[False, True])
LIMIT 10df.head(10) / df.nlargest(10, "amt")
SUM(x) OVER (PARTITION BY k)df.groupby("k")["x"].transform("sum")
ROW_NUMBER() OVER (PARTITION BY k ORDER BY t)df.sort_values("t").groupby("k").cumcount() + 1
RANK() / DENSE_RANK()df.groupby("k")["x"].rank(method="min" | "dense", ascending=False)
LAG(x) OVER (PARTITION BY k ORDER BY t)df.sort_values("t").groupby("k")["x"].shift(1)
Running totaldf.sort_values("t").groupby("k")["x"].cumsum()
COALESCE(x, 0)df.x.fillna(0)
CASE WHENnp.select([cond1, cond2], ["a", "b"], default="c") / np.where
SELECT DISTINCT / dedup latestdf.drop_duplicates() / df.sort_values("ts").drop_duplicates("id", keep="last")
UNION ALLpd.concat([a, b], ignore_index=True)

Dates & time series

df["ts"] = pd.to_datetime(df["ts"], errors="coerce")     # bad values become NaT, not a crash
df["month"] = df["ts"].dt.to_period("M").astype(str)       # '2026-10'
df["day"] = df["ts"].dt.date; df["dow"] = df["ts"].dt.day_name()
daily = df.set_index("ts").resample("D")["amount"].sum()    # fills missing days with 0
ma7 = daily.rolling(7, min_periods=1).mean()            # 7-day moving average (on the daily series)

# sessionize in pandas
df = df.sort_values(["user", "ts"])
gap = df.groupby("user")["ts"].diff() > pd.Timedelta(minutes=30)
df["session"] = gap.groupby(df["user"]).cumsum() + 1      # first event: NaT diff β†’ False β†’ session 1

Reshape

wide = df.pivot_table(index="user", columns="status", values="id", aggfunc="count", fill_value=0).reset_index()
long = wide.melt(id_vars="user", var_name="status", value_name="n")
items = orders.explode("items").reset_index(drop=True)     # one row per list element
items = pd.concat([items.drop(columns="items"), pd.json_normalize(items["items"])], axis=1)

After explode the index repeats, which is why the code above calls .reset_index(drop=True). Without it, concat(axis=1) misaligns rows.

Gotchas the judge will catch

  • Index: tests often compare to_dict("records") or use assert_frame_equal. Finish with .reset_index(drop=True) and the exact column order df[["a","b","c"]].
  • dtypes: an int column with NaN becomes float64 (1 β†’ 1.0). Use astype("Int64") if ints are required.
  • Chained assignment: write df.loc[mask, "col"] = v, not df[mask]["col"] = v.
  • groupby drops NaN keys by default; pass dropna=False to keep them.
  • Never loop with iterrows over big frames. Use vectorised ops, .map(dict), or np.where.
  • Sorting ties: pass all tie-break columns, and add kind="stable" if the input order matters.

Worked example

import pandas as pd
def monthly_top_customer(orders: pd.DataFrame) -> pd.DataFrame:
    """For each month: the customer with the highest completed revenue (ties β†’ smallest id)."""
    d = orders[orders.status == "completed"].copy()
    d["month"] = pd.to_datetime(d.order_date).dt.strftime("%Y-%m")
    g = d.groupby(["month", "customer_id"], as_index=False)["amount"].sum()
    g = g.sort_values(["month", "amount", "customer_id"], ascending=[True, False, True])
    return g.drop_duplicates("month", keep="first").reset_index(drop=True)

More tools worth knowing

df["city"].value_counts(normalize=True)                  # share per value (sorted desc)
pd.crosstab(df.user, df.status)                          # quick count pivot
df["bucket"] = pd.cut(df.amount, bins=[0, 10, 100, float("inf")], labels=["S", "M", "L"], right=False)
df["email"] = df["email"].str.strip().str.lower()        # vectorised string ops (.str.contains, .str.extract)
df["code"] = df["raw"].str.extract(r"#(\d+)", expand=False)
df["status"] = df["status"].map({"c": "completed", "x": "cancelled"}).fillna("unknown")

# method chaining with assign keeps each step readable
out = (df.query("status == 'completed'")
         .assign(revenue=lambda d: d.qty * d.price,
                 month=lambda d: d.order_date.str[:7])
         .groupby("month", as_index=False)["revenue"].sum())

# as-of join: latest rate at or before each trade (both sides sorted by the key)
trades = pd.merge_asof(trades.sort_values("ts"), rates.sort_values("ts"), on="ts", direction="backward")

# top-N per group
top3 = df.sort_values(["cat", "sales", "name"], ascending=[True, False, True]).groupby("cat").head(3)
# the row with the max per group
best = df.loc[df.groupby("cat")["sales"].idxmax()]

Reading messy files & writing exact output

df = pd.read_csv(path,
                 sep=";",                      # or sep=None, engine="python" to sniff
                 na_values=["", "NA", "null", "-"],
                 thousands=",",                # "1,200" β†’ 1200
                 dtype={"id": str},            # keep leading zeros in ids / zip codes
                 skipinitialspace=True)
df.columns = df.columns.str.strip().str.lower()  # headers with stray spaces / capitals

df.to_csv("out.csv", index=False, float_format="%.2f")   # no index column, fixed decimals
print(df.to_csv(index=False), end="")                    # same thing to stdout
print(df.to_string(index=False))                         # aligned text table

to_csv() writes the index by default. In a stdout problem, an extra leading column (or a missing final newline) is a Wrong Answer. Prefer plain Python for printing when the format is fiddly.

Performance in one paragraph

Vectorised column operations run in C and are roughly 100Γ— faster than apply(axis=1) or iterrows. groupby(...).agg with built-in names ("sum", "size", "nunique") is fast; a Python lambda inside agg is slow. Convert repeated strings to category dtype to save memory. For files bigger than RAM, read_csv(chunksize=…) and aggregate chunk by chunk (sums and counts combine; medians don't).

Quiz df.groupby('k')['v'].transform('sum') returns…
Quiz After left.merge(right, on='id', how='left') you get more rows than left had. Why?
Quiz df.to_csv('out.csv') with no other arguments writes…
🏁 --:--