crosstab() в Pandas — кросс-таблицы

Коротко

pd.crosstab() строит таблицу частот для двух (или более) категориальных переменных. Строки — значения одной переменной, столбцы — другой, ячейки — количество наблюдений. Это первый инструмент для анализа связи категорий: «как сегмент пользователя связан с платформой?», «как город влияет на конверсию?». Аналог PIVOT в SQL и сводной таблицы в Excel.

Базовое использование

import pandas as pd

df = pd.DataFrame({
    'segment': ['premium', 'free', 'premium', 'free', 'trial', 'premium', 'free', 'trial'],
    'platform': ['ios', 'android', 'ios', 'ios', 'android', 'android', 'android', 'ios'],
    'revenue': [500, 0, 800, 0, 100, 600, 0, 50]
})

ct = pd.crosstab(df['segment'], df['platform'])
print(ct)
platform android ios
free 2 1
premium 1 2
trial 1 1

Каждая ячейка — количество пользователей с такой комбинацией сегмент × платформа.

Нормализация

# По строкам (доля внутри каждого сегмента)
pd.crosstab(df['segment'], df['platform'], normalize='index')
# free:    android 0.67, ios 0.33
# premium: android 0.33, ios 0.67

# По столбцам (доля внутри каждой платформы)
pd.crosstab(df['segment'], df['platform'], normalize='columns')

# От общего числа
pd.crosstab(df['segment'], df['platform'], normalize='all')

normalize='index' — проценты по строкам. Показывает: из premium 67% на iOS, 33% на Android.

Маргиналы (итоги)

pd.crosstab(df['segment'], df['platform'], margins=True, margins_name='Total')
platform android ios Total
free 2 1 3
premium 1 2 3
trial 1 1 2
Total 4 4 8

margins=True добавляет строку и столбец с итогами.

Агрегация значений

# Средняя выручка по сегмент × платформа
pd.crosstab(
    df['segment'],
    df['platform'],
    values=df['revenue'],
    aggfunc='mean'
)
platform android ios
free 0 0
premium 600 650
trial 100 50

С values и aggfunc crosstab работает как pivot_table. Можно использовать 'sum', 'mean', 'count', 'median' и любую функцию.

Практические примеры

Конверсия по городу и каналу

ct = pd.crosstab(
    df['city'],
    df['utm_source'],
    values=df['converted'],
    aggfunc='mean',
    margins=True
).round(3)

# Какой канал лучше конвертит в каждом городе?

Распределение оценок по продукту

ct = pd.crosstab(
    df['product'],
    df['rating'],
    normalize='index'
).round(3) * 100

# Процент 5-звёздочных отзывов по каждому продукту

Активность по дню недели и часу

df['weekday'] = df['event_time'].dt.day_name()
df['hour'] = df['event_time'].dt.hour

ct = pd.crosstab(df['hour'], df['weekday'])

# Визуализация heatmap
import seaborn as sns
sns.heatmap(ct, cmap='YlOrRd', annot=True, fmt='d')

crosstab vs pivot_table

# Эквивалентные вызовы:

# crosstab
pd.crosstab(df['segment'], df['platform'])

# pivot_table
df.pivot_table(index='segment', columns='platform', aggfunc='size', fill_value=0)

# groupby
df.groupby(['segment', 'platform']).size().unstack(fill_value=0)
Метод Когда
crosstab Частоты двух категорий, быстрый EDA
pivot_table Агрегация с несколькими функциями
groupby + unstack Максимальная гибкость

crosstab — шорткат для частотного анализа. pivot_table — для сложных агрегаций.

Хи-квадрат тест на crosstab

crosstab — входные данные для теста хи-квадрат (проверка независимости категорий):

from scipy.stats import chi2_contingency

ct = pd.crosstab(df['segment'], df['platform'])
chi2, p_value, dof, expected = chi2_contingency(ct)

print(f'χ² = {chi2:.2f}, p-value = {p_value:.4f}')
# Если p-value < 0.05 → связь между segment и platform статистически значима

Аналог в SQL

-- Кросс-таблица через CASE WHEN
SELECT
    segment,
    COUNT(CASE WHEN platform = 'ios' THEN 1 END) AS ios,
    COUNT(CASE WHEN platform = 'android' THEN 1 END) AS android,
    COUNT(*) AS total
FROM users
GROUP BY segment;

Подробнее — в гайде по PIVOT в SQL.

Типичные ошибки

crosstab без нормализации. Абсолютные числа обманчивы: если free-пользователей 10000, а premium — 100, raw counts не показывают реальную разницу. Используйте normalize='index'.

Слишком много категорий. crosstab из 100 городов × 50 каналов — нечитаемая. Группируйте мелкие категории в «Другие».

Забывают fill_value. Если комбинации нет в данных — NaN. Для подсчётов: pd.crosstab(...).fillna(0).

Вопросы с собеседований

-- Как построить таблицу частот для двух категорий? -- pd.crosstab(df['col1'], df['col2']). С нормализацией: normalize='index' (по строкам), 'columns' (по столбцам), 'all' (от общего).

-- Чем crosstab отличается от pivot_table? -- crosstab принимает Series (столбцы), по умолчанию считает частоты. pivot_table принимает DataFrame, по умолчанию считает mean. Для частотного анализа crosstab удобнее.

-- Как проверить связь двух категориальных переменных? -- Построить crosstab, затем тест хи-квадрат: chi2_contingency(ct). Если p-value < 0.05 — связь статистически значима.


Потренируйтесь решать задачи — откройте тренажёр с 1500+ вопросами для подготовки к собеседованиям аналитиков.

FAQ

crosstab с тремя переменными?

Передайте список в index или columns: pd.crosstab([df['city'], df['segment']], df['platform']). Создаст MultiIndex.

Как сохранить crosstab в Excel?

ct.to_excel('crosstab.xlsx'). Для красивого форматирования — ct.style.background_gradient().to_excel('styled.xlsx').

Как тренироваться

crosstab — базовый инструмент EDA. Задачи на pandas — в тренажёре Карьерник. Больше вопросов — в разделе с примерами.