Перейти к содержанию
Educora
Университет25 мин32 / 42

Анализ данных в pandas

Главные инструменты для реальных данных: пропуски, `groupby` и `agg`, объединение таблиц через `merge`, `pivot_table`, `map` и `apply`, даты и мини-анализ продаж сети кафе.

Проверь себя
В этом уроке ты узнаешь
  • Находить пропуски и обрабатывать их подходящей стратегией (удаление или заполнение)
  • Строить сводные таблицы с помощью groupby, именованной агрегации, transform и pivot_table
  • Правильно объединять таблицы через merge и заранее знать число строк в результате
  • Работать с датами и считать месячный отчёт и темпы роста

Реальные данные никогда не бывают такими аккуратными, как в учебнике: часть ячеек пуста, информация разбросана по нескольким таблицам, даты записаны текстом. Работа аналитика состоит из четырёх шагов: очистить → объединить → сгруппировать → ответить. В этом уроке ты освоишь инструменты pandas для каждого шага, а в конце проанализируешь продажи сети кафе с двумя филиалами за квартал.

Пропущенные значения

pandas показывает пропущенное значение как NaN (Not a Number). isna().sum() считает пропуски в каждом столбце. count() считает только заполненные значения, а size — все строки. Функции mean, sum, std по умолчанию пропускают NaN. Дальше выбираем: dropna() удаляет строки с пропусками, а fillna() заполняет их — константой, медианой или средним.

Python
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
Среднее по математике равно 80, потому что учитываются только 3 заполненных значения. dropna() оставил лишь 2 строки из 5 — большая часть информации потеряна.

Группировка: разделить, применить, объединить

Определение
Разделить–применить–объединить (split–apply–combine)

groupby разбивает таблицу на группы по значениям ключевого столбца, применяет к каждой группе функцию (сумма, среднее, количество) и объединяет результаты в новую таблицу. Та же идея, что и GROUP BY в SQL.

Через agg считаем сразу несколько показателей; именованная агрегация имя=('столбец', 'функция') даёт столбцам результата понятные имена. Группировка по двум ключам создаёт многоуровневый индекс (MultiIndex). А transform возвращает результат в каждую строку исходной таблицы — например, чтобы найти долю каждой продажи в выручке своего филиала.

Python
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̄_w = ∑ wᵢ · xᵢ / ∑ wᵢx̄_w = ∑ wᵢ · xᵢ / ∑ wᵢ
где:
  • xᵢсреднее i-й группы (например, средний чек)
  • wᵢвес группы — обычно число наблюдений

Взвешенное среднее. В pandas: np.average(x, weights=w) или просто общая сумма, делённая на общее количество.

Пример 1: ловушка «среднего от средних»

В бакинском филиале 300 чеков со средним чеком 8 манатов, в гянджинском — 100 чеков со средним чеком 12 манатов. Каков средний чек по всей сети?

Показать решение
Неверно: (8 + 12) / 2 = 10 манатов — так филиалы получают равный вес.
Верно: общая выручка = 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все ключи обеих таблиц
Python
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
Клиента 13 нет в таблице клиентов, а у Лейлы (12) нет заказов. В результате outer столбец order_id стал дробным, потому что в целочисленном столбце нельзя хранить NaN.
Пример 2: сколько строк будет в результате?

а) Для orders (4 строки) и customers (3 строки) выше заранее найди число строк при inner, left и outer. б) Если ключ встречается 3 раза в левой таблице и 2 раза в правой, сколько строк даст inner для этого ключа?

Показать решение
а) Ключ 10 → 2 заказа · 1 клиент = 2 строки, ключ 11 → 1 строка: 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».

Python
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 раскладывает числа по интервалам (корзинам).

Python
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.

Python
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. 1
    Прочитай

    Прочитай журнал через read_csv и сразу преобразуй даты с помощью parse_dates.

  2. 2
    Объедини

    Присоедини таблицу товаров с how='left' и validate='many_to_one'.

  3. 3
    Посчитай

    Выручка = количество · цена; извлеки месяц из даты.

  4. 4
    Сведи

    Построй таблицу месяц × филиал через pivot_table, добавь итог и темп роста, упорядочь товары по выручке.

Δ% = (xₜ − xₜ₋₁) / xₜ₋₁ · 100%Δ% = (xₜ − xₜ₋₁) / xₜ₋₁ · 100%
где:
  • xₜпоказатель текущего периода
  • xₜ₋₁показатель предыдущего периода

Темп роста; в pandas его вычисляет pct_change() (без умножения на 100). У первого периода нет предыдущего значения, поэтому там NaN.

Python
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) число расходов каждого человека в виде словаря.

Задание · Python
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 знака после запятой.

Задание · Python
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.

1 / 10
Чему равны s.mean() и s.count() для s = pd.Series([4, np.nan, 8])?