10 ошибок в pandas, которые замедляют ноутбуки в 10 раз — и быстрые фиксы
Вы когда-нибудь запускали ячейку, налили кофе… и она всё ещё крутится? Ниже — 10 самых частых антипаттернов в pandas, которые делают ноутбуки медленными, и быстрые, практичные исправления с примерами кода.
TL;DR (чек-лист скорости)
- Не используйте
iterrows()
— векторизуйте или беритеitertuples()
/NumPy. - Избегайте
df.apply(..., axis=1)
для простой арифметики/логики — работайте поколоночно. - Не
df.append
в цикле — накапливайте в списке и делайте одинpd.concat(..., ignore_index=True)
. groupby.apply
→ замените наagg
/transform
/встроенные методы.- Бейтесь с
object
-типами — задавайтеdtype
на входе,convert_dtypes()
,to_numeric
,to_datetime(cache=True)
. - Низкая кардинальность? Используйте
category
. - Работа со строками:
str.replace(..., regex=False)
для литералов,.isin
вместо ручных проверок. - Склейки: приводите ключи к одному типу, валидируйте, для 1-к-1 —
map
, индексируйте. - Уберите лишние сортировки и переиндексации —
groupby(..., sort=False)
,reset_index(drop=True)
. - Не верьте
inplace=True
и не делайте «цепочное индексирование» — используйтеloc
и присваивайте результат.
1) Перебор строк через iterrows()
Антипаттерн
out = []
for _, row in df.iterrows():
out.append(row["a"] * 2 + row["b"]**2)
df["score"] = out
Почему это медленноiterrows()
создаёт объекты Series
для каждой строки → накладные расходы Python.
Быстрый фикс (векторизация/NumPy)
import numpy as np
df["score"] = df["a"] * 2 + np.square(df["b"])
# либо полностью на NumPy, если это числовая матрица:
# score = 2*A + B**2
Когда без итераций нельзя
Если нужно именно построчно, itertuples(index=False, name=None)
обычно в несколько раз быстрее, чем iterrows()
.
2) apply(..., axis=1)
для тривиальных вычислений
Антипаттерн
df["flag"] = df.apply(lambda r: r["x"] > 0 and r["y"] < 5, axis=1)
Быстрый фикс (булева логика по сериям)
df["flag"] = (df["x"] > 0) & (df["y"] < 5)
Ещё примеры
# вместо объединения строк в apply:
df["full_name"] = df["first"] + " " + df["last"]
# вместо if/else в apply:
df["bucket"] = np.where(df["value"] > 100, "big", "small")
3) DataFrame.append
в цикле
Антипаттерн
df = pd.DataFrame()
for chunk in chunks:
df = df.append(process(chunk), ignore_index=True) # медленно!
Быстрый фикс (один concat)
frames = []
for chunk in chunks:
frames.append(process(chunk))
df = pd.concat(frames, ignore_index=True)
Бонус
Если заранее известны поля фиксированной длины — заполните массивы NumPy и один раз соберите DataFrame
.
4) groupby.apply
вместо agg
/transform
Антипаттерн
# хотим сумму и среднее
res = df.groupby("key").apply(lambda g: pd.Series({
"sum": g["x"].sum(),
"mean": g["x"].mean()
})).reset_index()
Быстрый фикс
res = df.groupby("key", sort=False)["x"].agg(sum="sum", mean="mean").reset_index()
Перенос значений «на уровень строк» — берите transform
df["x_mean_by_key"] = df.groupby("key")["x"].transform("mean")
Фактыapply
часто теряет векторизацию и тянет Python-цикл внутри, а agg/transform
реализованы на C/numexpr.
5) object
-столбцы: смешанные типы, числа в строках, даты как текст
Антипаттерн
df = pd.read_csv("data.csv") # всё стало object, операции тормозят
Быстрые фиксы
# 1) Сразу на входе:
df = pd.read_csv(
"data.csv",
dtype={"id": "int64", "flag": "boolean"},
# parse_dates быстрее, чем вручную потом:
parse_dates=["event_ts"]
)
# 2) После чтения:
df = df.convert_dtypes() # переводит к nullable dtypes
df["amount"] = pd.to_numeric(df["amount"], errors="coerce", downcast="float")
df["event_ts"] = pd.to_datetime(df["event_ts"], errors="coerce", cache=True)
Зачем
Числа/даты в object
→ операции идут интерпретатором Python. Правильные dtype
сильно ускоряют арифметику, сравнения и группировки.
6) Игнорирование category
для низкой кардинальности
Антипаттерн
# 10 млн строк, столбец 'city' — строка с ~200 уникальными значениями
df["city"].dtype # object
Быстрый фикс
df["city"] = df["city"].astype("category")
# группировки/сравнения станут быстрее и память уменьшится
Подсказка
После merge
категории могут «расшириться». Для ускорения группировок ещё укажите:
df.groupby("city", observed=True, sort=False).size()
7) Строковые операции: лишние регулярки и не-векторные проверки
Антипаттерн
# заменяем «-» на «_» (регулярка по умолчанию!)
df["code"] = df["code"].str.replace("-", "_") # медленнее
Быстрый фикс
df["code"] = df["code"].str.replace("-", "_", regex=False)
Ещё типичные ускорения
# точное вхождение по списку значений:
df["is_ok"] = df["status"].isin(["done", "ok", "pass"])
# фильтр по подстроке без regex:
df = df[df["path"].str.contains("/api/", regex=False, na=False)]
8) Неэффективные merge
: несовпадение типов, «склейка ради маппинга», отсутствие индекса
Антипаттерн
# ключи разных типов → pandas делает дорогое приведение
df_users["user_id"].dtype # int64
df_events["user_id"].dtype # object (строки)
merged = df_events.merge(df_users, on="user_id") # медленно и иногда неверно
Быстрые фиксы
# 1) Совместите типы ключей заранее
df_events["user_id"] = pd.to_numeric(df_events["user_id"], errors="coerce").astype("Int64")
df_users["user_id"] = df_users["user_id"].astype("Int64")
# 2) Валидируйте ожидания связи (ловит дубликаты и ошибки):
merged = df_events.merge(
df_users, on="user_id", how="left", validate="m:1"
)
# 3) Для простого соответствия ключ→значение используйте map (быстрее merge):
map_series = df_users.set_index("user_id")["country"]
df_events["country"] = df_events["user_id"].map(map_series)
Индексация помогает
df_users = df_users.set_index("user_id") # затем df_events.join(df_users, on="user_id")
9) Лишние сортировки и переиндексации
Антипаттерн
tmp = df.sort_values(["key"]).groupby("key").size()
Быстрый фикс
tmp = df.groupby("key", sort=False).size()
Ещё частые мелочи
# concat без сохранения старых индексов:
df = pd.concat(frames, ignore_index=True)
# ненужные reset_index:
df = df.reset_index(drop=True) # когда нужно — сразу drop=True
# выборка по маске — не вычисляйте её дважды:
mask = (df["x"] > 0) & (df["y"] < 5)
df1 = df[mask]
df.loc[mask, "flag"] = True
10) inplace=True
и «цепочное индексирование»
Антипаттерн
df.drop(columns=["tmp"], inplace=True)
df[df["x"] > 0]["y"] = 1 # SettingWithCopy и скрытые копии
Почему это плохоinplace=True
редко экономит память/время (часто делает копию внутри) и ухудшает читаемость пайплайна. «Цепочное индексирование» вызывает неявные копии и предупреждения.
Быстрые фиксы
# просто присвойте результат — часто быстрее и чище:
df = df.drop(columns=["tmp"])
# корректное обновление по маске:
mask = df["x"] > 0
df.loc[mask, "y"] = 1
Чистые пайплайны
df = (
df
.assign(flag=lambda d: (d["x"] > 0) & (d["y"] < 5))
.drop(columns=["tmp"])
)
Небольшие «рецепты ускорения» на каждый день
Загрузка данных
df = pd.read_csv(
"events.csv",
usecols=["event_id", "user_id", "amount", "event_ts", "status"],
dtype={"event_id": "int64", "user_id": "int64", "status": "category"},
parse_dates=["event_ts"]
)
df["amount"] = pd.to_numeric(df["amount"], errors="coerce", downcast="float")
Частые операции быстрее
# подсчитать топ-N без лишней сортировки всего столбца:
top = df["user_id"].value_counts(sort=True).head(20)
# квантиль по группам — без apply:
q90 = df.groupby("status")["amount"].quantile(0.9)
# нормализация по группе:
df["z"] = (
(df["amount"] - df.groupby("status")["amount"].transform("mean")) /
df.groupby("status")["amount"].transform("std")
)
Профилирование (сразу видно, где тормозит)
# в Jupyter:
%timeit df["x"] * 2 + df["y"]**2
%timeit df.apply(lambda r: r["x"] * 2 + r["y"]**2, axis=1)
# оценка памяти:
df.info(memory_usage="deep")
df.memory_usage(deep=True).sort_values(ascending=False).head(10)
Мини-кейсы «до/после»
Кейс A: быстрый фичеринжиниринг без axis=1
До
df["score"] = df.apply(lambda r: 0.3*r["a"] + 0.7*r["b"], axis=1)
После
df["score"] = 0.3*df["a"] + 0.7*df["b"]
Кейс B: быстрый словарь соответствий
До
mapping = {"ok": 1, "fail": 0}
df["label"] = df["status"].apply(lambda s: mapping.get(s, -1))
После
df["label"] = df["status"].map(mapping).fillna(-1).astype("int8")
Кейс C: распаковка столбца со списками без apply(pd.Series)
До
df[["lat","lon"]] = df["coords"].apply(pd.Series) # медленно
После
df[["lat","lon"]] = pd.DataFrame(df["coords"].to_list(), index=df.index)
Большинство «тормозов» в pandas — это:
- Python-циклы вместо векторизации,
- неверные
dtype
(особенноobject
), - тяжёлые
apply
/регулярки/сортировки, - неэффективные
merge
и копирования.
Примените фиксы сверху — и типовой ноутбук на миллионах строк станет работать заметно быстрее и стабильнее.
Если хотите, могу взглянуть на ваш конкретный код/ноутбук и предложить точечные оптимизации.
