A dense reference for everyday pandas (2.x and 3.x): reading data, selecting, cleaning, grouping, joining, reshaping, and time series, with SQL and polars translations.
df[(df["amount"]>20)&(df["category"]=="toys")]# & | ~ with parentheses, never and/ordf[df["category"].isin(["toys","games"])]df[df["amount"].between(20,50)]# inclusive on both endsdf[~df["city"].str.contains("test",case=False,na=False)]df.query("amount > 20 and category == 'toys'")# string expression; @var for Python varsdf.nlargest(5,"amount");df.nsmallest(5,"amount")
out=(df.assign(amount=lambdat:t["amount"].fillna(0),# lambda sees the current framebig=lambdat:t["amount"]>25).query("amount > 0").sort_values("amount",ascending=False).pipe(lambdat:t.head(10))# any function of a DataFrame)df["tier"]=np.where(df["amount"]>20,"high","low")# vectorized if/elsedf["band"]=np.select([df["amount"]<10,df["amount"]<40],["low","mid"],default="high")df=df.rename(columns={"amount":"amt"})
skip NaN by default (skipna=True); s.sum(min_count=1) returns NaN if all missing
Missing values change your denominators
df["a"].mean() averages only the non-missing values. Always report how many values were missing, and think about why they are missing before filling them.
g=df.groupby("category").agg(# named aggregation: new_col=(col, func)n=("order_id","count"),revenue=("amount","sum"),avg=("amount","mean"),)df["share"]=df["amount"]/df.groupby("category")["amount"].transform("sum")# same length as dfdf.groupby("category").filter(lambdat:len(t)>=3)# keep whole groups that passdf.groupby(["category","city"],observed=True).size()# observed=True: only seen category combosdf.groupby("customer_id")["amount"].cumsum()# running total within groupdf.groupby("customer_id")["date"].rank(method="first")# order within group
m=orders.merge(customers,on="customer_id",how="left",validate="many_to_one",# raises if customers has duplicate keysindicator=True)# adds _merge: both / left_only / right_onlym["_merge"].value_counts()orders.merge(products,left_on="sku",right_on="product_sku",suffixes=("","_prod"))pd.concat([df1,df2],ignore_index=True)# stack rowspd.concat([df1,df2],axis=1)# side by side, aligned on index
how=
Keeps
inner
keys in both
left / right
all keys from the left / right table
outer
all keys from both
cross
every combination
Row explosion
If a key is duplicated on both sides, a merge returns every pairing (many-to-many). Check len() before and after, and use validate= to catch it early.
wide=df.pivot_table(index="customer_id",columns="category",values="amount",aggfunc="sum",fill_value=0)# long -> wide, aggregates duplicateslong=wide.reset_index().melt(id_vars="customer_id",var_name="category",value_name="amount")df.pivot(index="order_id",columns="category",values="amount")# no aggregation; fails on duplicatespd.crosstab(df["category"],df["city"],normalize="index",margins=True)wide.stack();long.set_index(["customer_id","category"]).unstack()df.explode("tags")# list column -> one row per element
In pandas 3, Copy-on-Write is always on: any DataFrame or Series derived from another behaves as a copy. Modifying a subset never changes the parent, and the old SettingWithCopyWarning is gone.
sub=df[df["amount"]>20]sub.loc[:,"amount"]=0# changes sub only, never dfdf.loc[df["amount"]>50,"flag"]=True# the one correct way to update df in placedf["a"][0]=1# chained assignment: never updates df in pandas 3
The default text dtype in pandas 3 is str (Arrow-backed when pyarrow is installed) rather than object.