10 ошибок в pandas, которые замедляют ноутбуки в 10 раз — и быстрые фиксы

Вы когда-нибудь запускали ячейку, налили кофе… и она всё ещё крутится? Ниже — 10 самых частых антипаттернов в pandas, которые делают ноутбуки медленными, и быстрые, практичные исправления с примерами кода.

Краткое содержание

TL;DR (чек-лист скорости)

  1. Не используйте iterrows() — векторизуйте или берите itertuples()/NumPy.
  2. Избегайте df.apply(..., axis=1) для простой арифметики/логики — работайте поколоночно.
  3. Не df.append в цикле — накапливайте в списке и делайте один pd.concat(..., ignore_index=True).
  4. groupby.apply → замените на agg/transform/встроенные методы.
  5. Бейтесь с object-типами — задавайте dtype на входе, convert_dtypes(), to_numeric, to_datetime(cache=True).
  6. Низкая кардинальность? Используйте category.
  7. Работа со строками: str.replace(..., regex=False) для литералов, .isin вместо ручных проверок.
  8. Склейки: приводите ключи к одному типу, валидируйте, для 1-к-1 — map, индексируйте.
  9. Уберите лишние сортировки и переиндексации — groupby(..., sort=False), reset_index(drop=True).
  10. Не верьте 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 и копирования.

Примените фиксы сверху — и типовой ноутбук на миллионах строк станет работать заметно быстрее и стабильнее.

Если хотите, могу взглянуть на ваш конкретный код/ноутбук и предложить точечные оптимизации.

AI на дровах 🪵
Привет! Меня зовут Семён, я работаю в сфере ML и аналитики данных и пишу в блог nerdit.ru статьи о своем опыте и том, что может пригодиться начинающим в начале их пути изучения больших данных.

Подписаться на новости Nerd IT

Не пропустите последние выпуски. Зарегистрируйтесь сейчас, чтобы получить полный доступ к статьям.
jamie@example.com
Подписаться