Cohort 4Register

PART 6 · PYTHON FOR DATA ENGINEERING · CHAPTER 31

pandas pitfalls and performance

FreeLesson 68 of 78

Four mistakes that return wrong numbers without an error, and the one habit that makes pandas fast.

TRAP 1: GROUPBY DROPS NAN KEYSPYTHON 3.12
import json
import pandas as pd

cust = pd.json_normalize(json.load(open("customers.json")))
orders = pd.read_csv("orders.csv")
df = orders.merge(cust[["customer_id", "address.city"]],
                  on="customer_id")

by_city = df.groupby("address.city")["amount"].sum()
print(len(by_city), "cities, total", by_city.sum())
print(df["amount"].sum(), "<- real total")
print(df.groupby("address.city", dropna=False)["amount"]
        .sum().tail(2))

Output

4 cities, total 21270.0
23268.0 <- real total
address.city
Pune    5096.0
NaN     1998.0
Name: amount, dtype: float64
TRAP 2 AND 3: CHAINED ASSIGNMENT, SILENT FAN-OUTPYTHON 3.12
import pandas as pd

df = pd.read_csv("orders.csv")
returned = df.status == "returned"

df[returned]["amount"] = 0       # chained: edits a copy
print(df.loc[returned, "amount"].tolist())

df.loc[returned, "amount"] = 0   # one .loc call: works
print(df.loc[returned, "amount"].tolist())

dup = pd.DataFrame({"customer_id": [1, 1],
                    "tier": ["gold", "new"]})
try:
    df.merge(dup, on="customer_id", validate="many_to_one")
except pd.errors.MergeError as e:
    print("MergeError:", e)

Output

[798.0]
[0.0]
MergeError: Merge keys are not unique in right dataset; not a many-to-one merge

Duplicates in right:
  customer_id
           1 ...
SPEED: VECTORIZE, AND USE THE RIGHT DTYPEPYTHON 3.12
import time
import numpy as np
import pandas as pd

n = 200_000
rng = np.random.default_rng(0)
df = pd.DataFrame({"qty": rng.integers(1, 5, n), "price": 799.0})

def ms(fn):
    start = time.perf_counter(); fn()
    return (time.perf_counter() - start) * 1000

row_fn = lambda r: r.qty * r.price
print(f"vectorized {ms(lambda: df.qty * df.price):8.1f} ms")
print(f"apply      {ms(lambda: df.apply(row_fn, axis=1)):8.1f} ms")
loop = lambda: [row_fn(r) for _, r in df.iterrows()]
print(f"iterrows   {ms(loop):8.1f} ms")

s = pd.Series(["delivered", "cancelled", "returned"] * 100_000)
as_text = s.memory_usage(deep=True) // 1024
as_cat = s.astype("category").memory_usage(deep=True) // 1024
print(as_text, "KB as text,", as_cat, "KB as category")

Output

vectorized      2.1 ms
apply        3157.4 ms
iterrows     8050.6 ms
4882 KB as text, 293 KB as category

The times are what this run measured when the book was built. Yours will differ.

Practice before you move on

  1. Find a groupby in this lesson, add dropna=False, and compare the output.
  2. Convert the status column to category and print memory_usage(deep=True) before and after.

Watch the free lesson

The same topic in the Free Lessons library, free with a sign-in.