pandas — DataFrame model, eager evaluation, copy-on-write
pandas (pandas docs — User Guide, 2008+) — индустриальный стандарт для tabular data manipulation в Python. Eager-evaluation (вычисляется немедленно), interactive-friendly, NumPy-backed под капотом. Pragmatic-DEEP в M10: учим что и зачем + Pattern 4 challenge (pure-stdlib groupby equivalent — то, что pandas делает под капотом для small data) + Run-on-Your-Machine для real demo.
В этом уроке:
- Why pandas — production контекст + почему conceptual в browser.
- DataFrame / Series model — columns как numpy arrays (cross-link M02 урок 01 contiguous memory).
- Indexing & filtering —
df['col'],.loc[],.iloc[], boolean mask. - groupby semantics — split-apply-combine + Pattern 4 algorithm.
- merge / join — четыре flavors (cross-course → Spark M03 joins).
- Chained-assignment problem —
SettingWithCopyWarningпричины. - Copy-on-Write — opt-in в pandas 2.0+, default since 3.0 (Jan 2026).
- Pattern 4 challenge —
py-m10-01-code-1groupby pure-stdlib equivalent. - Run-on-Your-Machine #1 — реальный pandas demo.
- Cross-course → Spark M03 / ClickHouse FORMAT — distributed equivalents.
Why pandas — production контекст
pandas решает три проблемы:
- Tabular data в Python — раньше использовали list of dict /
csv.DictReader(М09 урок 02) — но операции (filter, groupby, join) приходилось писать вручную. pandas даёт высокоуровневую DataFrame абстракцию. - Vectorized operations —
df['amount'] * 1.2— векторизованная C-loop (через NumPy backbone). Существенно быстрее Python-loop для numeric workloads. - Interactive analysis —
df.head()/df.describe()/df.info()— REPL-friendly inspection. Предназначен для Jupyter / IPython.
Why conceptual в M10: pandas (~50MB) + numpy (~30MB) + 30+ C-extensions не bundled в Pyodide default — browser challenges используют pure-stdlib equivalent (Pattern 4). Real demos — Run-on-Your-Machine callout (local Python). Подробности — урок 05 (Pyodide constraints).
DataFrame / Series model — columns как numpy arrays
import pandas as pd
df = pd.DataFrame({
'category': ['food', 'food', 'tech', 'tech', 'travel'],
'amount': [10, 20, 100, 300, 50],
})
print(df)
# category amount
# 0 food 10
# 1 food 20
# 2 tech 100
# 3 tech 300
# 4 travel 50
print(type(df['amount'])) # <class 'pandas.Series'>
print(df['amount'].dtype) # int64
Архитектура:
| Слой | Объект | Backend |
|---|---|---|
| Таблица | DataFrame | dict of Series |
| Колонка | Series | label index + 1D numpy.ndarray (или Arrow ChunkedArray в pandas 2.0+ если dtype_backend='pyarrow') |
| Низкоуровневое хранилище | numpy.ndarray | contiguous C-array fixed-width |
Cross-link M02 урок 01 (PyListObject): numpy.ndarray — contiguous memory layout same idea как PyListObject->ob_item (recap: PyListObject хранит указатели на heap-allocated PyObjects; numpy хранит сами значения continuously). DataFrame column → numpy array → contiguous memory — extension PyListObject contiguity insight в DE context. Для int/float columns это даёт SIMD-vectorization potential. Cite pandas docs — Internal architecture.
Production rule: thinks of DataFrame как dict[str, Series] где Series ≈ numpy.ndarray + index. Это объясняет 90% pandas semantics.
Indexing & filtering — [], .loc, .iloc, boolean mask
import pandas as pd
df = pd.DataFrame({
'name': ['alice', 'bob', 'carol'],
'age': [30, 25, 35],
'role': ['eng', 'pm', 'eng'],
})
# Column selection — square brackets
df['name'] # Series
df[['name', 'age']] # DataFrame (subset of columns)
# Row selection by label — .loc
df.loc[0] # Series (row 0)
df.loc[0:1] # DataFrame (rows 0, 1 — INCLUSIVE end!)
# Row selection by position — .iloc
df.iloc[0] # Series (row 0)
df.iloc[0:2] # DataFrame (rows 0, 1 — EXCLUSIVE end like Python slice)
# Boolean mask
df[df['age'] > 28] # DataFrame (только rows where age > 28)
# Combined — .loc[mask, columns]
df.loc[df['role'] == 'eng', ['name', 'age']]
Pitfall: .loc[0:1] — inclusive end (label-based — pandas interprets как “from label 0 to label 1 inclusive”); .iloc[0:2] — exclusive end (position-based — same as Python slice). Source of bugs при switch между .loc и .iloc. Cite pandas docs — Indexing and selecting data.
groupby semantics — split-apply-combine
import pandas as pd
df = pd.DataFrame({
'category': ['food', 'food', 'tech', 'tech', 'tech', 'travel'],
'amount': [10, 20, 100, 200, 300, 50],
})
result = df.groupby('category')['amount'].mean()
print(result)
# category
# food 15.0
# tech 200.0
# travel 50.0
# Name: amount, dtype: float64
Алгоритм (split-apply-combine):
- Split — partition rows по category (hash table или sort).
- Apply — для каждой group вычислить
.mean(). - Combine — collect результаты в Series indexed by group key.
Pattern 4 (pure-stdlib equivalent — challenge py-m10-01-code-1 ниже):
from itertools import groupby
from operator import itemgetter
from statistics import mean
def solve(rows):
rows_sorted = sorted(rows, key=itemgetter('category')) # split needs sorted input
out = {}
for cat, group in groupby(rows_sorted, key=itemgetter('category')):
amounts = [r['amount'] for r in group]
out[cat] = mean(amounts)
return out
Critical insight: itertools.groupby требует pre-sorted input — group по key работает только на consecutive equal-key entries. Если не сортируешь — получишь fragmented groups. pandas скрывает это (под капотом — hash table OR tree-sort на small data; для больших — параллельный Arrow expression). Polars скрывает это аналогично (Arrow grouped expression evaluator).
Stdlib pure-algorithm version помогает понять, что делают высокоуровневые библиотеки.
Cross-course → Spark M03 урок 04 groupby-aggregations: тот же split-apply-combine, distributed (sort-merge based) — стажёр уже знает алгоритм, Spark учит как scale-out через shuffle/partitioning.
Cite pandas docs — Group by + Python itertools.groupby.
merge / join — четыре flavors
import pandas as pd
users = pd.DataFrame({
'user_id': [1, 2, 3],
'name': ['alice', 'bob', 'carol'],
})
orders = pd.DataFrame({
'user_id': [1, 1, 2, 4],
'amount': [100, 200, 50, 300],
})
# inner — только matching keys (default)
pd.merge(users, orders, on='user_id', how='inner')
# user_id 1, 2 (carol — нет orders; user_id 4 — нет в users)
# left — keep ВСЕ users; orders добавляются если match
pd.merge(users, orders, on='user_id', how='left')
# users 1, 2, 3 (carol → NaN amount)
# right — keep ВСЕ orders; users добавляются если match
pd.merge(users, orders, on='user_id', how='right')
# orders для user_id 1, 1, 2, 4 (user_id 4 → NaN name)
# outer — keep ВСЁ; missing → NaN
pd.merge(users, orders, on='user_id', how='outer')
# union — все user_id из обеих + NaN для missing
Production rules:
- Default
how='inner'— частая ошибка для тех, кто ожидает SQL-default (тоже inner — но SQL делает это очень explicit; pandas — implicit). - Left join — самый частый в DE pipeline (enrichment): “keep all primary records; добавь данные если есть”.
- Outer join — для diff/audit: “что есть в одной table но нет в другой”.
Cross-course → Spark M03 урок 03 joins-deep-dive: тот же четыре flavors, distributed — sort-merge join / hash join / broadcast join. Cross-course → Spark M03 урок 01 dataframe-creation-schema — DataFrame creation parallels.
Cite pandas docs — Merging, joining.
Chained-assignment problem — SettingWithCopyWarning
import pandas as pd
df = pd.DataFrame({'x': [1, 2, 3], 'y': [10, 20, 30]})
# Ambiguous — какой объект на левой стороне?
df[df['x'] > 1]['y'] = 99 # SettingWithCopyWarning!
Что произошло:
df[df['x'] > 1] создаёт либо view либо copy (pandas decides at runtime в pre-2.0). Subsequent ['y'] = 99:
- Если view — modifies original
df. - Если copy — modifies temporary copy, original
dfunchanged → silent data loss.
Pre-2.0 fix — single-step .loc:
df.loc[df['x'] > 1, 'y'] = 99 # explicit — modifies df in place
Production rule: chained df[...][...] — always use .loc[mask, col] instead.
Cite pandas docs — Returning a view versus a copy.
Copy-on-Write — default since pandas 3.0 (Jan 2026)
pandas 2.0 (April 2023) ввёл Copy-on-Write (CoW) как opt-in. pandas 3.0 (released Jan 21, 2026) сделал CoW always-on default — pd.options.mode.copy_on_write deprecated и стал no-op.
Pre-3.0 opt-in (исторический контекст):
import pandas as pd
pd.options.mode.copy_on_write = True # pandas 2.x — opt-in
# или: pd.set_option('mode.copy_on_write', True)
# pandas 3.0+ — это no-op, CoW уже включён
Что меняется при CoW (теперь default):
- Все indexing operations возвращают lazy copy — view-vs-copy ambiguity исчезла.
- Subsequent
[...] = valueмодифицирует только этот copy — оригинал untouched. - Chained assignment теперь raises ChainedAssignmentError (а не silently игнорируется как в 2.x opt-in) — bugs ловятся явно.
- Внутренне — Arrow-backed memory + reference counting — copy materializes только on first write.
pandas 3.0 — другие default-changes:
- Default string dtype = PyArrow-backed — replaces object dtype для string columns; 5-10× faster string ops.
- PyArrow стал hard dependency (per PDEP-10) —
pip install pandasподтягивает PyArrow. - Python ≥ 3.9 required.
Production transition path (исторический): opt-in pandas 2.x → audit + rewrite chained assignments → default pandas 3.0 (Jan 2026, released).
Lesson takeaway: запоминай df.loc[mask, col] = value (explicit, all-version-compatible) always. С pandas 3.0 это уже default — но писать explicit полезно для портируемости назад на 2.x кодовые базы.
Cite pandas docs — Copy-on-Write + pandas 3.0.0 release notes (Jan 21, 2026) + PDEP-10 (PyArrow as required dependency).
Code-challenge — py-m10-01-code-1 (Pattern 4)
Прямо в quiz JSON ниже — py-m10-01-code-1: реализовать pandas df.groupby('category')['amount'].mean().to_dict() через itertools.groupby + statistics.mean. Это Pattern 4 canonical из RESEARCH lines 442-463 — pedagogical framing: “pandas скрывает алгоритм; здесь вы пишете его сами — это ровно то, что pandas делает под капотом для small data.”
Smoke test ниже — то же что в challenge:
from itertools import groupby
from operator import itemgetter
from statistics import mean
rows = [
{'category': 'food', 'amount': 10}, {'category': 'food', 'amount': 20},
{'category': 'tech', 'amount': 100}, {'category': 'tech', 'amount': 200}, {'category': 'tech', 'amount': 300},
{'category': 'travel', 'amount': 50},
]
def solve(rows):
rows_sorted = sorted(rows, key=itemgetter('category'))
out = {}
for cat, group in groupby(rows_sorted, key=itemgetter('category')):
amounts = [r['amount'] for r in group]
out[cat] = mean(amounts)
return out
print(solve(rows))
# {'food': 15, 'tech': 200, 'travel': 50}
Pedagogical chain: stdlib itertools.groupby → pandas df.groupby → Spark df.groupBy (distributed). Same algorithm, three abstraction layers.
Run-on-Your-Machine — pandas DataFrame creation + groupby
Run-on-Your-Machine: pandas DataFrame creation + groupby
Установите локально (browser cannot run pandas — 50MB+ download blocks page load + numpy C-extensions не bundled в Pyodide default):
pip install 'pandas>=2.2,<4.0'Создайте файл pandas_demo.py:
import pandas as pd
df = pd.DataFrame({
'category': ['food', 'food', 'tech', 'tech', 'travel'],
'amount': [10, 20, 100, 300, 50],
})
print(df.groupby('category')['amount'].mean())Запустите:
python3 pandas_demo.pyОжидаемый вывод:
category
food 15.0
tech 200.0
travel 50.0
Name: amount, dtype: float64В browser challenge выше мы делаем тот же computation через itertools.groupby + statistics.mean — pandas скрывает алгоритм + добавляет vectorized C-loop, но семантика идентична на small data. Version pin >=2.2,<4.0 (Pitfall 32) — покрывает 2.x + 3.0 (released Jan 21, 2026; CoW always-on, default string dtype = PyArrow-backed, PyArrow стал hard dependency).
Cross-course → Spark M03 / ClickHouse query formats
Distributed equivalents pandas DataFrame model:
-
Spark M03 dataframes-spark-sql — DataFrame model parallels (Spark distributed equivalent):
- 03/01 — DataFrame creation + schema —
spark.read.csv()+ schema inference (cross-link M09 урок 02 stdlibcsv.DictReader). - 03/03 — Joins deep dive — sort-merge / hash / broadcast joins (algorithms behind 4 pandas merge flavors).
- 03/04 — groupby + aggregations — distributed shuffle-based split-apply-combine.
- 03/01 — DataFrame creation + schema —
-
ClickHouse — query result formats /clickhouse-course/11-ingestion-patterns/07-format-clause/ — Native / JSON / CSV / TSV — analogues pandas data exchange formats. М09 урок 02 (CSV) и М09 урок 03 (JSON) уже cover Python lens; ClickHouse covers analytical-DB lens.
Three-layer cross-course bridge:
- Python lens (this lesson) — DataFrame model concept + groupby pure-stdlib equivalent.
- Distributed engine lens — Spark M03 (cluster-distributed DataFrames).
- Analytical-DB lens — ClickHouse (columnar OLAP, MergeTree engines, native data exchange formats).
Что в следующем уроке
М10 урок 02 — Polars (Rust-based DataFrame, lazy API, query optimizer, columnar Arrow backend). Cross-link назад к этому уроку (pandas eager vs Polars lazy comparison) и cross-link forward к М05 урок 02 (generator lazy iteration parallels Polars lazy expression-level).