Перейти к содержанию
Learning Platform
Глоссарий Troubleshooting
Урок 11.01 · 25 мин
Средний
pandasDataFrameSeriesgroupbymergejoineager-evaluationcopy-on-writechained-assignmentRun-on-Your-MachinePattern 4cross-course
Требуемые знания:

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.

В этом уроке:

  1. Why pandas — production контекст + почему conceptual в browser.
  2. DataFrame / Series model — columns как numpy arrays (cross-link M02 урок 01 contiguous memory).
  3. Indexing & filteringdf['col'], .loc[], .iloc[], boolean mask.
  4. groupby semantics — split-apply-combine + Pattern 4 algorithm.
  5. merge / join — четыре flavors (cross-course → Spark M03 joins).
  6. Chained-assignment problemSettingWithCopyWarning причины.
  7. Copy-on-Write — opt-in в pandas 2.0+, default since 3.0 (Jan 2026).
  8. Pattern 4 challengepy-m10-01-code-1 groupby pure-stdlib equivalent.
  9. Run-on-Your-Machine #1 — реальный pandas demo.
  10. Cross-course → Spark M03 / ClickHouse FORMAT — distributed equivalents.

Why pandas — production контекст

pandas решает три проблемы:

  1. Tabular data в Python — раньше использовали list of dict / csv.DictReader (М09 урок 02) — но операции (filter, groupby, join) приходилось писать вручную. pandas даёт высокоуровневую DataFrame абстракцию.
  2. Vectorized operationsdf['amount'] * 1.2 — векторизованная C-loop (через NumPy backbone). Существенно быстрее Python-loop для numeric workloads.
  3. Interactive analysisdf.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
ТаблицаDataFramedict of Series
КолонкаSerieslabel index + 1D numpy.ndarray (или Arrow ChunkedArray в pandas 2.0+ если dtype_backend='pyarrow')
Низкоуровневое хранилищеnumpy.ndarraycontiguous 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] где Seriesnumpy.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):

  1. Split — partition rows по category (hash table или sort).
  2. Apply — для каждой group вычислить .mean().
  3. 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 df unchanged → 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 defaultpd.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

TIP

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:

Three-layer cross-course bridge:

  1. Python lens (this lesson) — DataFrame model concept + groupby pure-stdlib equivalent.
  2. Distributed engine lens — Spark M03 (cluster-distributed DataFrames).
  3. 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).

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7