- Находить пропуски и обрабатывать их подходящей стратегией (удаление или заполнение)
- Строить сводные таблицы с помощью
groupby, именованной агрегации,transformиpivot_table - Правильно объединять таблицы через
mergeи заранее знать число строк в результате - Работать с датами и считать месячный отчёт и темпы роста
Реальные данные никогда не бывают такими аккуратными, как в учебнике: часть ячеек пуста, информация разбросана по нескольким таблицам, даты записаны текстом. Работа аналитика состоит из четырёх шагов: очистить → объединить → сгруппировать → ответить. В этом уроке ты освоишь инструменты pandas для каждого шага, а в конце проанализируешь продажи сети кафе с двумя филиалами за квартал.
Пропущенные значения
pandas показывает пропущенное значение как NaN (Not a Number). isna().sum() считает пропуски в каждом столбце. count() считает только заполненные значения, а size — все строки. Функции mean, sum, std по умолчанию пропускают NaN. Дальше выбираем: dropna() удаляет строки с пропусками, а fillna() заполняет их — константой, медианой или средним.
import numpy as np
import pandas as pd
df = pd.DataFrame({
'student': ['Aysel', 'Murad', 'Leyla', 'Elvin', 'Nigar'],
'group': ['A', 'B', 'A', 'B', 'A'],
'math': [91, np.nan, 85, 64, np.nan],
'physics': [88, 72, np.nan, 70, 81],
})
print(df.isna().sum())
print(df['math'].mean(), df['math'].count())
print(df.dropna().shape)
filled = df.fillna({'math': df['math'].median(), 'physics': df['physics'].mean()})
print(filled)
print(np.nan == np.nan)▸ Ожидаемый результат
student 0 group 0 math 2 physics 1 dtype: int64 80.0 3 (2, 4) student group math physics 0 Aysel A 91.0 88.00 1 Murad B 85.0 72.00 2 Leyla A 85.0 77.75 3 Elvin B 64.0 70.00 4 Nigar A 85.0 81.00 False
dropna() оставил лишь 2 строки из 5 — большая часть информации потеряна.Группировка: разделить, применить, объединить
groupby разбивает таблицу на группы по значениям ключевого столбца, применяет к каждой группе функцию (сумма, среднее, количество) и объединяет результаты в новую таблицу. Та же идея, что и GROUP BY в SQL.
Через agg считаем сразу несколько показателей; именованная агрегация имя=('столбец', 'функция') даёт столбцам результата понятные имена. Группировка по двум ключам создаёт многоуровневый индекс (MultiIndex). А transform возвращает результат в каждую строку исходной таблицы — например, чтобы найти долю каждой продажи в выручке своего филиала.
import pandas as pd
df = pd.DataFrame({
'store': ['Baku', 'Baku', 'Ganja', 'Baku', 'Ganja', 'Ganja', 'Baku'],
'category': ['Food', 'Drinks', 'Food', 'Food', 'Drinks', 'Food', 'Drinks'],
'amount': [120.0, 45.5, 80.0, 99.5, 30.0, 64.0, 52.0],
})
print(df.groupby('store')['amount'].sum())
summary = df.groupby('store').agg(
total=('amount', 'sum'),
average=('amount', 'mean'),
orders=('amount', 'size'),
)
print(summary.round(2))
print(df.groupby(['store', 'category'])['amount'].sum())
df['share'] = (df['amount'] / df.groupby('store')['amount'].transform('sum')).round(3)
print(df.head(3))▸ Ожидаемый результат
store
Baku 317.0
Ganja 174.0
Name: amount, dtype: float64
total average orders
store
Baku 317.0 79.25 4
Ganja 174.0 58.00 3
store category
Baku Drinks 97.5
Food 219.5
Ganja Drinks 30.0
Food 144.0
Name: amount, dtype: float64
store category amount share
0 Baku Food 120.0 0.379
1 Baku Drinks 45.5 0.144
2 Ganja Food 80.0 0.460- xᵢсреднее i-й группы (например, средний чек)
- wᵢвес группы — обычно число наблюдений
Взвешенное среднее. В pandas: np.average(x, weights=w) или просто общая сумма, делённая на общее количество.
В бакинском филиале 300 чеков со средним чеком 8 манатов, в гянджинском — 100 чеков со средним чеком 12 манатов. Каков средний чек по всей сети?
Показать решениеСкрыть решение
Верно: общая выручка = 300 · 8 + 100 · 12 = 2400 + 1200 = 3600 манатов на 400 чеков.
x̄_w = 3600 / 400 = 9 манатов.
В pandas не бери среднее от результата
groupby(...).mean() — считай mean() по исходным строкам или дели суммы.Объединение таблиц: merge
Заказы лежат в одной таблице, клиенты — в другой; объединяем их по общему ключевому столбцу (customer_id). Параметр how определяет, что делать со строками, ключ которых есть только в одной таблице. indicator=True добавляет столбец _merge, показывающий происхождение каждой строки.
| how | Какие ключи остаются в результате |
|---|---|
inner | только те, что есть в обеих таблицах (по умолчанию) |
left | все ключи левой таблицы; ненайденные справа — NaN |
right | все ключи правой таблицы |
outer | все ключи обеих таблиц |
import pandas as pd
orders = pd.DataFrame({'order_id': [1, 2, 3, 4],
'customer_id': [10, 11, 10, 13],
'amount': [25.0, 40.0, 15.5, 60.0]})
customers = pd.DataFrame({'customer_id': [10, 11, 12],
'name': ['Aysel', 'Murad', 'Leyla']})
inner = orders.merge(customers, on='customer_id')
print(inner)
left = orders.merge(customers, on='customer_id', how='left')
print(left[['order_id', 'name']])
outer = orders.merge(customers, on='customer_id', how='outer', indicator=True)
print(outer[['customer_id', 'order_id', 'name', '_merge']])▸ Ожидаемый результат
order_id customer_id amount name 0 1 10 25.0 Aysel 1 2 11 40.0 Murad 2 3 10 15.5 Aysel order_id name 0 1 Aysel 1 2 Murad 2 3 Aysel 3 4 NaN customer_id order_id name _merge 0 10 1.0 Aysel both 1 10 3.0 Aysel both 2 11 2.0 Murad both 3 12 NaN Leyla right_only 4 13 4.0 NaN left_only
outer столбец order_id стал дробным, потому что в целочисленном столбце нельзя хранить NaN.а) Для orders (4 строки) и customers (3 строки) выше заранее найди число строк при inner, left и outer. б) Если ключ встречается 3 раза в левой таблице и 2 раза в правой, сколько строк даст inner для этого ключа?
Показать решениеСкрыть решение
inner = 3.left сохраняет все заказы: 4 (для клиента 13 имя — NaN).outer = 4 + Лейла (12) = 5.б) Каждая левая строка сочетается с каждой правой: 3 · 2 = 6 строк. Это «взрыв» «многие-ко-многим» — таблица неожиданно растёт, а суммы учитываются дважды.
pivot_table, map, apply и даты
pivot_table — то же, что сводная таблица в Excel: index задаёт строки, columns — столбцы, а values и aggfunc — что считать в ячейках. fill_value=0 заполняет пустые ячейки, а margins=True добавляет итоговые строку и столбец «All».
import pandas as pd
df = pd.DataFrame({
'store': ['Baku', 'Baku', 'Ganja', 'Baku', 'Ganja', 'Ganja', 'Baku'],
'category': ['Food', 'Drinks', 'Food', 'Food', 'Drinks', 'Food', 'Drinks'],
'amount': [120.0, 45.5, 80.0, 99.5, 30.0, 64.0, 52.0],
})
pt = df.pivot_table(index='store', columns='category', values='amount',
aggfunc='sum', fill_value=0, margins=True)
print(pt)
print(df.pivot_table(index='store', columns='category', values='amount', aggfunc='count'))▸ Ожидаемый результат
category Drinks Food All store Baku 97.5 219.5 317.0 Ganja 30.0 144.0 174.0 All 127.5 363.5 491.0 category Drinks Food store Baku 2 2 Ganja 1 2
Преобразовать столбец можно тремя способами: map заменяет значения по словарю, apply применяет к каждому элементу любую функцию, а pd.cut раскладывает числа по интервалам (корзинам).
import numpy as np
import pandas as pd
df = pd.DataFrame({'code': ['BAK', 'GNJ', 'SMQ', 'BAK'], 'amount': [120.0, 45.5, 80.0, 30.0]})
df['city'] = df['code'].map({'BAK': 'Baku', 'GNJ': 'Ganja', 'SMQ': 'Sumgait'})
df['size_apply'] = df['amount'].apply(lambda x: 'large' if x >= 60 else 'small')
df['size_where'] = np.where(df['amount'] >= 60, 'large', 'small')
df['band'] = pd.cut(df['amount'], bins=[0, 50, 100, 200], labels=['low', 'mid', 'high'])
print(df)
print((df['size_apply'] == df['size_where']).all())▸ Ожидаемый результат
code amount city size_apply size_where band 0 BAK 120.0 Baku large large high 1 GNJ 45.5 Ganja small small low 2 SMQ 80.0 Sumgait large large mid 3 BAK 30.0 Baku small small low True
Даты, записанные текстом, превращаем в настоящие даты через pd.to_datetime (или read_csv(..., parse_dates=['date'])). Затем через .dt получаем год, месяц, день недели, а to_period('M') «округляет» дату до месяца. Разность двух дат — это Timedelta.
import pandas as pd
sales = pd.DataFrame({
'date': ['2026-01-15', '2026-01-28', '2026-02-03', '2026-02-20', '2026-03-05', '2026-03-18'],
'amount': [310.0, 275.5, 402.0, 350.0, 390.0, 441.5],
})
sales['date'] = pd.to_datetime(sales['date'])
sales['month'] = sales['date'].dt.to_period('M')
sales['weekday'] = sales['date'].dt.day_name()
print(sales)
print(sales.groupby('month')['amount'].sum())
print((sales['date'].max() - sales['date'].min()).days, 'days')▸ Ожидаемый результат
date amount month weekday 0 2026-01-15 310.0 2026-01 Thursday 1 2026-01-28 275.5 2026-01 Wednesday 2 2026-02-03 402.0 2026-02 Tuesday 3 2026-02-20 350.0 2026-02 Friday 4 2026-03-05 390.0 2026-03 Thursday 5 2026-03-18 441.5 2026-03 Wednesday month 2026-01 585.5 2026-02 752.0 2026-03 831.5 Freq: M, Name: amount, dtype: float64 62 days
Мини-анализ: продажи сети кафе
У сети кафе есть филиалы в Баку и Гяндже. В журнале продаж хранятся дата, филиал, код товара и количество, а в отдельной таблице — названия товаров и цены в манатах (данные выдуманы). Руководство спрашивает: сколько выручки принёс каждый филиал в каждом месяце, как менялась общая выручка и какой товар лидирует?
- 1Прочитай
Прочитай журнал через
read_csvи сразу преобразуй даты с помощьюparse_dates. - 2Объедини
Присоедини таблицу товаров с
how='left'иvalidate='many_to_one'. - 3Посчитай
Выручка = количество · цена; извлеки месяц из даты.
- 4Сведи
Построй таблицу месяц × филиал через
pivot_table, добавь итог и темп роста, упорядочь товары по выручке.
- xₜпоказатель текущего периода
- xₜ₋₁показатель предыдущего периода
Темп роста; в pandas его вычисляет pct_change() (без умножения на 100). У первого периода нет предыдущего значения, поэтому там NaN.
import io
import pandas as pd
sales_csv = '''date,store,product_id,qty
2026-01-05,Baku,1,30
2026-01-12,Ganja,2,25
2026-01-20,Baku,3,12
2026-02-02,Baku,1,34
2026-02-14,Ganja,3,18
2026-02-25,Ganja,1,20
2026-03-03,Baku,2,41
2026-03-15,Ganja,2,28
2026-03-28,Baku,3,15'''
products = pd.DataFrame({'product_id': [1, 2, 3], 'product': ['Tea', 'Coffee', 'Cake'],
'price': [4.5, 6.0, 12.0]})
sales = pd.read_csv(io.StringIO(sales_csv), parse_dates=['date'])
df = sales.merge(products, on='product_id', how='left', validate='many_to_one')
df['revenue'] = df['qty'] * df['price']
df['month'] = df['date'].dt.month
report = df.pivot_table(index='month', columns='store', values='revenue',
aggfunc='sum', fill_value=0)
report['total'] = report.sum(axis=1)
report['growth_%'] = report['total'].pct_change().mul(100).round(1)
print(report)
print(df.groupby('product')['revenue'].sum().sort_values(ascending=False))▸ Ожидаемый результат
store Baku Ganja total growth_% month 1 279.0 150.0 429.0 NaN 2 153.0 306.0 459.0 7.0 3 426.0 168.0 594.0 29.4 product Coffee 564.0 Cake 540.0 Tea 378.0 Name: revenue, dtype: float64
Выводы: за квартал общая выручка выросла с 429 до 594 манатов, в марте рост составил 29,4% — в основном за счёт продаж кофе и тортов в бакинском филиале. В феврале Гянджа обошла Баку, потому что там хорошо продавались торты. Лидер квартала — кофе (564 маната). Чай последний по выручке (378 манатов): продано 84 чашки, но по низкой цене, тогда как всего 45 тортов принесли 540 манатов. Одна таблица и несколько строк кода дали руководству основу для конкретных решений.
По таблице расходов выведи на отдельных строках: 1) общие расходы каждого человека в виде словаря (to_dict()); 2) категорию с наибольшей суммой; 3) число расходов каждого человека в виде словаря.
import pandas as pd
expenses = pd.DataFrame({
'person': ['Aysel', 'Murad', 'Aysel', 'Leyla', 'Murad', 'Aysel', 'Leyla'],
'category': ['food', 'transport', 'books', 'food', 'food', 'food', 'books'],
'amount': [12.5, 3.0, 25.0, 18.0, 9.5, 7.0, 14.0],
})
# 1) total per person (dict)
# 2) category with the largest total
# 3) number of expenses per person (dict)▸ Ожидаемый результат
{'Aysel': 44.5, 'Leyla': 32.0, 'Murad': 12.5}
food
{'Aysel': 3, 'Leyla': 2, 'Murad': 2}Объедини заказы с клиентами через how='left' и заполни ненайденные города значением 'Unknown'. Затем выведи: 1) сумму заказов по городам в виде словаря; 2) долю Баку в общей сумме в процентах, округлённую до 1 знака после запятой.
import pandas as pd
orders = pd.DataFrame({'order_id': [1, 2, 3, 4, 5], 'customer_id': [1, 2, 1, 3, 4],
'amount': [30.0, 45.0, 20.0, 15.0, 50.0]})
customers = pd.DataFrame({'customer_id': [1, 2, 3], 'city': ['Baku', 'Ganja', 'Baku']})
# merge, fill missing cities, then print the two results▸ Ожидаемый результат
{'Baku': 65.0, 'Ganja': 45.0, 'Unknown': 50.0}
40.6Главное
isna().sum()считает пропуски;dropnaудаляет,fillnaзаполняет;NaN == NaNдаётFalse.groupby+aggреализуют схему «разделить–применить–объединить»;transformвозвращает результат в каждую строку.- Объединяя средние групп, используй взвешенное среднее: ∑ wᵢxᵢ / ∑ wᵢ.
- В
mergeпараметрhow(inner, left, right, outer) определяет результат; повторяющиеся ключи размножают строки — проверяй черезvalidate. pivot_table— это сводная таблица Excel; по возможности выбирай векторные операции вместоapply.- Преобразуй даты через
to_datetimeилиparse_dates, затем анализируй периоды с помощью.dtиpct_change.
Проверь себя
Вопросов: 10. Каждый правильный ответ приносит XP.
s.mean() и s.count() для s = pd.Series([4, np.nan, 8])?