Pandas Performance & ScalePandas: производительность и масштаб

Make pandas fast and memory-lean: vectorization vs loops, dtypes & categoricals, copy-vs-view, chunking, fast joins, and when to graduate to Polars / DuckDB / PySpark. Делаем pandas быстрым и экономным по памяти: векторизация vs циклы, dtypes и категории, copy-vs-view, чтение по чанкам, быстрые join'ы, и когда переходить на Polars / DuckDB / PySpark.
🟢 EssentialsОсновы 🔴 PerformanceПроизводительность Scale out: PySparkМасштаб: PySpark

1. Vectorize — never loop rows1. Векторизуй — не перебирай строки

The #1 pandas performance rule, and the most common interview answer. Pandas is fast because operations run on whole columns in compiled C/NumPy. A Python-level loop throws that away.

Правило производительности №1 и самый частый ответ на интервью. Pandas быстр, потому что операции идут над целыми колонками в скомпилированном C/NumPy. Python-цикл это рушит.

ApproachRelative speedUse
Vectorized (df["a"]*2, np.where)⚡ fastest (1×)Always try first
Built-in methods (.str, .dt, groupby)⚡ fastString/date/group work
.apply(axis=1)🐢 ~10–100× slowerOnly if no vectorized form
.iterrows() / Python for🐌 slowestAvoid
ПодходОтносит. скоростьКогда
Векторизация (df["a"]*2, np.where)⚡ быстрее всего (1×)Всегда пробуй первым
Встроенные методы (.str, .dt, groupby)⚡ быстроСтроки/даты/группы
.apply(axis=1)🐢 в ~10–100× медленнееТолько если нет векторной формы
.iterrows() / Python for🐌 медленнее всегоИзбегай
# SLOW — Python loop per row
df["net"] = df.apply(lambda r: r["amount"] - r["fee"], axis=1)

# FAST — one vectorized C operation
df["net"] = df["amount"] - df["fee"]

2. Memory & dtypes2. Память и типы (dtypes)

Pandas often uses far more RAM than the file on disk. Right-sizing dtypes is the cheapest big win — and a great interview talking point.

Pandas часто ест намного больше RAM, чем файл на диске. Правильный подбор dtypes — самая дешёвая большая победа и отличная тема на интервью.

  • Downcast numbers: int64→int32/int16, float64→float32 when range allows.
  • category for repeated strings (country, status): stores codes once → often 10×+ smaller and faster groupby.
  • Avoid object dtype for strings where you can; it's pointers to Python objects. Use category or the newer string[pyarrow] dtype.
  • Measure: df.memory_usage(deep=True) shows true per-column bytes.
  • Понижай числа: int64→int32/int16, float64→float32, если диапазон позволяет.
  • category для повторяющихся строк (country, status): хранит коды один раз → часто в 10×+ меньше и быстрее groupby.
  • Избегай object для строк, где можно; это указатели на Python-объекты. Используй category или новый string[pyarrow].
  • Измеряй: df.memory_usage(deep=True) показывает реальные байты по колонкам.
df["country"] = df["country"].astype("category")
df["qty"]     = pd.to_numeric(df["qty"], downcast="integer")
df.memory_usage(deep=True).sum() / 1e6   # MB

3. Copy vs view & SettingWithCopyWarning3. Copy vs view и SettingWithCopyWarning

A near-universal interview/debug topic. Some slices return a view (shares memory), others a copy. Assigning into a chained slice may silently hit a temporary copy and not change your data — that's what the warning means.

Почти универсальная тема интервью/отладки. Одни срезы возвращают view (общая память), другие copy. Присвоение в цепочечный срез может тихо попасть во временную копию и не изменить твои данные — об этом и предупреждение.

# RISKY — chained indexing, may not write back
df[df.amount > 0]["flag"] = 1          # SettingWithCopyWarning

# CORRECT — single .loc call does the assignment
df.loc[df.amount > 0, "flag"] = 1

# Want an independent frame? be explicit:
sub = df[df.amount > 0].copy()
The fixРешение Never chain [] twice for assignment. Use one .loc[rows, cols] = value. If you subset then modify, add .copy(). (Pandas 3.0 / Copy-on-Write makes this stricter and clearer.) Никогда не цепляй [] дважды для присваивания. Используй один .loc[строки, колонки] = значение. Если делаешь подвыборку и потом меняешь — добавь .copy(). (Pandas 3.0 / Copy-on-Write делает это строже и яснее.)

4. Data bigger than comfort4. Данные больше комфортного

Still want pandas but it's straining memory? Reduce what you load and process in pieces:

Всё ещё хочешь pandas, но память на пределе? Сократи объём загрузки и обрабатывай по частям:

  • Load less: usecols=, dtype=, parse_dates= at read time; filter rows ASAP.
  • Columnar source: Parquet reads only needed columns & is pre-typed — much lighter than CSV.
  • Chunking: read_csv(..., chunksize=N) yields frames; aggregate incrementally.
  • Drop intermediates: reassign / del big temporaries; avoid keeping every step.
  • Грузи меньше: usecols=, dtype=, parse_dates= при чтении; фильтруй строки как можно раньше.
  • Колоночный источник: Parquet читает только нужные колонки и уже типизирован — намного легче CSV.
  • Чанкинг: read_csv(..., chunksize=N) отдаёт фреймы; агрегируй инкрементально.
  • Убирай промежуточное: переприсваивай / del больших временных; не держи каждый шаг.
total = 0
for chunk in pd.read_csv("big.csv", chunksize=1_000_000, usecols=["amount"]):
    total += chunk["amount"].sum()

5. Faster joins & groupby5. Быстрее join и groupby

  • Watch the row count after a merge. Duplicate keys fan out rows (a many-to-many blow-up). Use validate="one_to_many" to assert your assumption.
  • Join on an index when repeating joins on the same key: set_index once, then df.join is faster than re-merging on a column.
  • category keys make groupby and merges faster and lighter.
  • Pick the right agg: built-in named aggregations (("amount","sum")) beat groupby().apply(custom).
  • Sort once if you do many ordered ops; avoid repeated implicit sorts.
  • Следи за числом строк после merge. Дубли ключей размножают строки (взрыв many-to-many). Используй validate="one_to_many", чтобы зафиксировать допущение.
  • Join по индексу, если повторяешь join по тому же ключу: один раз set_index, потом df.join быстрее повторного merge по колонке.
  • category-ключи делают groupby и merge быстрее и легче.
  • Бери правильную агрегацию: встроенные именованные агрегации (("amount","sum")) быстрее groupby().apply(custom).
  • Сортируй один раз, если много упорядоченных операций; избегай повторных неявных сортировок.
The silent row explosionТихий взрыв строк A "wrong numbers after join" bug is almost always duplicate keys on the side you assumed was unique. Check df["key"].is_unique and use validate=. Баг «неверные числа после join» почти всегда — дубли ключей на стороне, которую ты считал уникальной. Проверь df["key"].is_unique и используй validate=.

6. When to leave pandas6. Когда уходить из pandas

ToolReach for it when
PolarsWant pandas-like single-node but much faster: Rust, multi-threaded, lazy, Arrow-native.
DuckDBYou'd rather write SQL over Parquet/larger-than-RAM on one machine; great with pandas.
PySpark / DaskData truly exceeds one machine; need a cluster & distributed shuffle.
pandas API on SparkKeep pandas syntax but run it on Spark (pyspark.pandas).
ИнструментБрать, когда
PolarsХочешь как pandas на одном узле, но намного быстрее: Rust, многопоточность, ленивость, Arrow.
DuckDBУдобнее писать SQL поверх Parquet / больше-чем-RAM на одной машине; дружит с pandas.
PySpark / DaskДанные реально превышают одну машину; нужен кластер и распределённый shuffle.
pandas API на SparkСохранить синтаксис pandas, но выполнять на Spark (pyspark.pandas).
Interview framingФормулировка на интервью "I start in pandas for single-node work; if memory or speed bites, I first cut dtypes/vectorize, then reach for Polars or DuckDB on one box, and only move to PySpark when the data genuinely needs a cluster." That sequence shows judgment, not just tool knowledge. «Начинаю в pandas для одного узла; если упираюсь в память/скорость — сперва правлю dtypes/векторизую, затем беру Polars или DuckDB на одной машине, и перехожу на PySpark, только когда данным реально нужен кластер.» Эта последовательность показывает суждение, а не просто знание инструментов.

7. Quick self-check7. Быстрая самопроверка

Answer in your head, then tap to flip.Ответь про себя, потом нажми, чтобы перевернуть.

Why avoid apply/iterrows?Почему избегать apply/iterrows?
tapнажми
They loop in Python, losing pandas' compiled C/NumPy speed (often 10–100× slower). Prefer vectorized column ops and built-in .str/.dt/groupby methods.Они идут циклом в Python, теряя скорость скомпилированного C/NumPy (часто в 10–100× медленнее). Предпочитай векторные операции и встроенные .str/.dt/groupby.
Biggest memory win for repeated strings?Главная экономия памяти для повторяющихся строк?
tapнажми
Convert to category dtype: stores each distinct value once + integer codes. Often 10×+ smaller and speeds up groupby/merge. Plus downcast numeric dtypes.Перевести в category: хранит каждое уникальное значение один раз + целые коды. Часто в 10×+ меньше и ускоряет groupby/merge. Плюс понижай числовые типы.
What is SettingWithCopyWarning?Что такое SettingWithCopyWarning?
tapнажми
You assigned into a chained slice that may be a temporary copy, so the write might not stick. Fix: one df.loc[rows, cols] = val, or .copy() when intentionally subsetting.Ты присвоил в цепочечный срез, который может быть временной копией, поэтому запись может не примениться. Решение: один df.loc[строки, колонки] = знач или .copy() при намеренной подвыборке.
Counts wrong after a merge — why?Неверные числа после merge — почему?
tapнажми
Duplicate keys on the "lookup" side fan out rows (many-to-many). Check .is_unique and pass validate="many_to_one" to fail loudly.Дубли ключей на «справочной» стороне размножают строки (many-to-many). Проверь .is_unique и передай validate="many_to_one", чтобы падало явно.
CSV won't fit in RAM but you want pandas. Options?CSV не влезает в RAM, но хочешь pandas. Варианты?
tapнажми
usecols+dtype to load less, switch to Parquet, or stream with chunksize and aggregate incrementally. Or move to DuckDB/Polars/PySpark.usecols+dtype чтобы грузить меньше, перейти на Parquet, или читать с chunksize и агрегировать инкрементально. Либо DuckDB/Polars/PySpark.
Pandas → when Polars/DuckDB vs PySpark?Pandas → когда Polars/DuckDB vs PySpark?
tapнажми
Polars/DuckDB = still one machine but much faster / SQL over large files. PySpark/Dask = data genuinely exceeds one machine and needs a distributed cluster.Polars/DuckDB = всё ещё одна машина, но намного быстрее / SQL над большими файлами. PySpark/Dask = данные реально превышают одну машину и нужен распределённый кластер.