pandas
03 / 03

Cleaning, Merging & Performance

pandas: Cleaning, Merging & Performance

Missing Data

# Detect missing values
df.isnull()                         # boolean DataFrame
df.isnull().sum()                   # count per column
df.isnull().sum() / len(df) * 100  # % missing per column

# Drop missing
df.dropna()                         # drop rows with ANY null
df.dropna(subset=['name', 'email']) # drop rows with null in specific columns
df.dropna(how='all')                # drop rows where ALL values are null
df.dropna(thresh=3)                 # keep rows with at least 3 non-null values
df.dropna(axis=1)                   # drop columns with any null

# Fill missing values
df.fillna(0)
df.fillna({'salary': df['salary'].median(), 'dept': 'Unknown'})
df['salary'].fillna(df['salary'].mean(), inplace=True)
df.ffill()                          # forward fill (propagate last valid)
df.bfill()                          # backward fill
df['salary'].interpolate(method='linear')

# Replace specific values
df.replace(-999, np.nan)
df.replace({'dept': {'Eng': 'Engineering', 'Des': 'Design'}})

Merging & Joining

# merge — like SQL JOIN
pd.merge(df_left, df_right, on='id', how='inner')   # inner, left, right, outer
pd.merge(df_left, df_right, left_on='user_id', right_on='id')  # different key names
pd.merge(df_left, df_right, on=['dept', 'level'])   # multiple keys
pd.merge(df_left, df_right, on='id', how='left', suffixes=('_left', '_right'))  # duplicate column names

# concat — stack DataFrames
pd.concat([df1, df2])               # stack rows (union)
pd.concat([df1, df2], ignore_index=True)  # reset index
pd.concat([df1, df2], axis=1)       # stack columns side by side

# join — index-based merge
df1.join(df2, how='left')
df1.join(df2, on='dept_id')        # join on a column to other's index

# Update values from another DataFrame
df1.update(df2)                     # overwrite non-null values from df2

# merge_asof — time-series merge (nearest key)
pd.merge_asof(events, prices, on='timestamp', direction='backward')

Reshaping

# melt — wide to long (unpivot)
df_long = pd.melt(df,
    id_vars=['name', 'dept'],          # columns to keep
    value_vars=['q1', 'q2', 'q3', 'q4'],
    var_name='quarter',
    value_name='revenue',
)

# pivot — long to wide
df_wide = df_long.pivot(index='name', columns='quarter', values='revenue')
df_wide = df_long.pivot_table(index='name', columns='quarter', values='revenue', aggfunc='sum')

# stack / unstack (multi-index)
df.stack()      # columns → index level
df.unstack()    # innermost index level → columns

# explode — one element per row from list column
df_exploded = df.explode('tags')

Performance Tips

  • Use categorical dtype for low-cardinality string columns: df["dept"] = df["dept"].astype("category") — saves 5-10x memory.

  • Downcast numeric types: pd.to_numeric(df["age"], downcast="integer") — int64 → int8 saves 8x.

  • Avoid loops: use vectorized operations, .str, .dt, .apply with caution. Loops kill performance.

  • Read only needed columns: pd.read_csv(usecols=[...]) — avoids loading unused data.

  • Use read_parquet over read_csv for repeated analysis — 10-100x faster loading.

  • eval() and query(): use pandas.eval("a + b") for large arithmetic — avoids creating intermediate arrays.

  • inplace=True rarely helps performance and makes code harder to chain. Return new DataFrames instead.

  • polars: if pandas is too slow, polars is a Rust-based DataFrame library that is 10-100x faster for most operations.

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free