34. (Л) Агрегати та складні запити у pandas¶
Зміст лекції¶
- Від «що в таблиці» до «що з цього випливає»
- Набір даних для лекції
- Агрегати над усією таблицею
agg: кілька агрегатів одразуgroupby: розділити — застосувати — об'єднати- Групування за одним ключем
- Групування за кількома ключами і
MultiIndex unstackіstack: з довгої таблиці в широку- Іменовані агрегати та власні функції
size,countі пропуски в ключахtransform: результат групи — назад у кожен рядокfilter: відбір цілих груп- Топ-N у групі, ранги та накопичувальні суми
- Групування за датами
- Групування за інтервалами:
cutіqcut - Зведені таблиці:
pivot_tableіcrosstab pivotіmelt: зміна форми таблиці- Об'єднання таблиць:
merge - Типи з'єднань і пошук «сиріт»
- Склеювання таблиць:
concat query: фільтрація рядком-виразом- Ланцюжки методів:
assignіpipe - pandas і SQL
- Повний приклад: звіт з трьох таблиць
- Типові помилки
- Підсумок
Від «що в таблиці» до «що з цього випливає»¶
У лекції 32 ми навчилися читати таблицю, вибирати стовпці й рядки, фільтрувати й сортувати. Це відповідає на питання «що в таблиці». Але бізнес питає інакше:
- яка виручка в кожному місті?
- який товар найкращий у кожній категорії?
- як змінювалися продажі по місяцях?
- яку частку виручки міста дав кожен продаж?
- які замовлення посилаються на товари, яких немає в каталозі?
Усі ці питання мають спільну структуру: розбити рядки на групи, для кожної групи порахувати агрегат, а іноді ще й з'єднати кілька таблиць. Саме цьому присвячена лекція.
Ось як виглядає перше питання без групування — фільтром і циклом по містах:
import pandas as pd
df = pd.DataFrame({
"city": ["Kyiv", "Lviv", "Kyiv", "Odesa", "Lviv", "Kyiv"],
"revenue": [10788.0, 8737.5, 29996.0, 7192.0, 18750.0, 10485.0],
})
# без groupby: цикл по унікальних містах і маска на кожній ітерації
for city in df["city"].unique():
total = df.loc[df["city"] == city, "revenue"].sum()
print(city, total)
Працює, але кожна ітерація проходить усю таблицю ще раз. Для 3 міст — дрібниця, для 30 000 клієнтів — хвилини. Те саме з groupby:
import pandas as pd
df = pd.DataFrame({
"city": ["Kyiv", "Lviv", "Kyiv", "Odesa", "Lviv", "Kyiv"],
"revenue": [10788.0, 8737.5, 29996.0, 7192.0, 18750.0, 10485.0],
})
# з groupby: одна операція, один прохід по даних
print(df.groupby("city")["revenue"].sum())
Один рядок, один прохід по даних, а результат — готова Series з містами в індексі, яку можна далі сортувати, фільтрувати чи зберігати.
Набір даних для лекції¶
Більшість прикладів працює з однією таблицею продажів магазину електроніки за березень–квітень. Її код повторюється в кожному прикладі, щоб будь-який з них можна було скопіювати й запустити окремо.
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
print(df)
print()
df.info()
date city product category units price revenue
0 2026-03-02 Kyiv Keyboard peripherals 12 899.0 10788.0
1 2026-03-02 Lviv Mouse peripherals 25 349.5 8737.5
2 2026-03-05 Kyiv Monitor displays 4 7499.0 29996.0
3 2026-03-09 Odesa Keyboard peripherals 8 899.0 7192.0
4 2026-03-12 Lviv Headset audio 15 1250.0 18750.0
5 2026-03-20 Kyiv Mouse peripherals 30 349.5 10485.0
6 2026-04-01 Odesa Monitor displays 6 7499.0 44994.0
7 2026-04-03 Kyiv Headset audio 11 1250.0 13750.0
8 2026-04-08 Lviv Keyboard peripherals 14 899.0 12586.0
9 2026-04-15 Kyiv Monitor displays 3 7499.0 22497.0
10 2026-04-22 Odesa Mouse peripherals 18 349.5 6291.0
11 2026-04-28 Lviv Headset audio 9 1250.0 11250.0
<class 'pandas.DataFrame'>
RangeIndex: 12 entries, 0 to 11
Data columns (total 7 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 date 12 non-null datetime64[us]
1 city 12 non-null str
2 product 12 non-null str
3 category 12 non-null str
4 units 12 non-null int64
5 price 12 non-null float64
6 revenue 12 non-null float64
dtypes: datetime64[us](1), float64(2), int64(1), str(3)
memory usage: 804.0 bytes
Один рядок — один продаж: дата, місто, товар, категорія, кількість, ціна за одиницю і виручка (units * price).
Агрегати над усією таблицею¶
Агрегат — функція, яка перетворює багато значень на одне: сума, середнє, мінімум, кількість. На Series ви вже бачили їх у лекції 32:
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
print("sum: ", df["revenue"].sum())
print("mean: ", round(df["revenue"].mean(), 2))
print("median: ", df["revenue"].median())
print("min/max:", df["units"].min(), df["units"].max())
print("count: ", df["units"].count())
print("nunique:", df["product"].nunique())
print("idxmax: ", df["revenue"].idxmax())
print()
# рядок із найбільшою виручкою цілком
print(df.loc[df["revenue"].idxmax()])
sum: 197316.5
mean: 16443.04
median: 11918.0
min/max: 3 30
count: 12
nunique: 4
idxmax: 6
date 2026-04-01 00:00:00
city Odesa
product Monitor
category displays
units 6
price 7499.0
revenue 44994.0
Name: 6, dtype: object
idxmax повертає мітку рядка, тож через .loc можна дістати весь рядок-рекордсмен.
На DataFrame агрегат рахується для кожного стовпця окремо і повертає Series:
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
# агрегат по стовпцях (axis=0, за замовчуванням)
print(df[["units", "price", "revenue"]].sum())
print()
# лише числові стовпці: рядки й дати пропускаються
print(df.mean(numeric_only=True).round(2))
print()
# агрегат по рядках (axis=1): сума двох стовпців у кожному рядку
print(df[["units", "price"]].sum(axis=1).head(3))
units 155.0
price 29992.5
revenue 197316.5
dtype: float64
units 12.92
price 2499.38
revenue 16443.04
dtype: float64
0 911.0
1 374.5
2 7503.0
dtype: float64
Правило axis те саме, що в NumPy (лекція 30): axis=0 — «згорнути рядки», результат по одному числу на стовпець; axis=1 — «згорнути стовпці», результат по одному числу на рядок.
numeric_only=True
df.mean() на таблиці з текстовими стовпцями кидає TypeError: середнє від "Kyiv" не існує. Або виберіть числові стовпці явно (df[["units", "price"]]), або передайте numeric_only=True.
Основні агрегати:
| Метод | Що рахує |
|---|---|
sum() |
сума |
mean(), median() |
середнє, медіана |
min(), max() |
мінімум, максимум |
idxmin(), idxmax() |
мітка мінімуму / максимуму |
count() |
кількість непорожніх значень |
size (атрибут) |
кількість значень разом із пропусками |
nunique() |
кількість унікальних значень |
std(), var() |
стандартне відхилення, дисперсія (ddof=1) |
quantile(q) |
квантиль |
first(), last() |
перше / останнє значення |
agg: кілька агрегатів одразу¶
Метод agg (скорочення від aggregate) приймає список функцій або словник «стовпець → функції»:
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
# список агрегатів для одного стовпця
print(df["revenue"].agg(["sum", "mean", "max"]))
print()
# список агрегатів для кількох стовпців
print(df[["units", "revenue"]].agg(["sum", "mean", "max"]))
print()
# словник: для кожного стовпця — свої агрегати
print(df.agg({"units": "sum", "price": ["min", "max"], "date": ["min", "max"]}))
sum 197316.500000
mean 16443.041667
max 44994.000000
Name: revenue, dtype: float64
units revenue
sum 155.000000 197316.500000
mean 12.916667 16443.041667
max 30.000000 44994.000000
units price date
sum 155.0 NaN NaT
min NaN 349.5 2026-03-02
max NaN 7499.0 2026-04-28
Функції задають рядком-назвою ("sum", "mean") — pandas сам візьме оптимізовану реалізацію. Там, де для стовпця агрегат не просили, стоїть NaN.
Сам по собі agg рідко потрібен на всій таблиці. Його справжня сила — у поєднанні з groupby.
groupby: розділити — застосувати — об'єднати¶
Групування працює за схемою split — apply — combine:
- split — розбити рядки на групи за значенням ключа;
- apply — до кожної групи окремо застосувати функцію (агрегат, перетворення, фільтр);
- combine — зібрати результати в нову таблицю.
graph LR
T["таблиця<br/>12 рядків"] --> S1["Kyiv<br/>5 рядків"]
T --> S2["Lviv<br/>4 рядки"]
T --> S3["Odesa<br/>3 рядки"]
S1 --> A1["sum = 87 516"]
S2 --> A2["sum = 51 323.5"]
S3 --> A3["sum = 58 477"]
A1 --> R["Series<br/>3 рядки"]
A2 --> R
A3 --> R
Виклик df.groupby("city") ще нічого не рахує — він повертає об'єкт групування, який лише знає, які рядки куди потрапили:
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
grouped = df.groupby("city")
print(type(grouped))
print("groups:", grouped.ngroups)
print()
# які мітки рядків потрапили в кожну групу
for city, labels in grouped.groups.items():
print(city, list(labels))
print()
# одна група як окрема таблиця
print(grouped.get_group("Odesa"))
<class 'pandas.api.typing.DataFrameGroupBy'>
groups: 3
Kyiv [0, 2, 5, 7, 9]
Lviv [1, 4, 8, 11]
Odesa [3, 6, 10]
date city product category units price revenue
3 2026-03-09 Odesa Keyboard peripherals 8 899.0 7192.0
6 2026-04-01 Odesa Monitor displays 6 7499.0 44994.0
10 2026-04-22 Odesa Mouse peripherals 18 349.5 6291.0
Якщо потрібно подивитися на групи під час налагодження, по об'єкту можна пройти циклом: for key, group in grouped: ... — кожна group є звичайним DataFrame. Але для обчислень цикл не потрібен: усе роблять методи групування.
Групування за одним ключем¶
Типовий виклик складається з трьох частин: за чим групувати, який стовпець рахувати, який агрегат:
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
# за чим групувати -> який стовпець -> який агрегат
print(df.groupby("city")["revenue"].sum())
print()
# кілька стовпців -> DataFrame
print(df.groupby("category")[["units", "revenue"]].sum())
print()
# одразу відсортувати результат
print(df.groupby("product")["units"].sum().sort_values(ascending=False))
city
Kyiv 87516.0
Lviv 51323.5
Odesa 58477.0
Name: revenue, dtype: float64
units revenue
category
audio 35 43750.0
displays 13 97487.0
peripherals 107 56079.5
product
Mouse 73
Headset 35
Keyboard 34
Monitor 13
Name: units, dtype: int64
Зверніть увагу:
- ключ групування стає індексом результату;
- групи відсортовані за ключем (алфавіт). Якщо порядок не важливий,
sort=Falseтрохи пришвидшує роботу; ["revenue"]післяgroupby— один стовпець, результатSeries;[["units", "revenue"]]— кілька, результатDataFrame.
Якщо ключ потрібен як звичайний стовпець (наприклад, для запису в CSV), є два способи:
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
# спосіб 1: reset_index після агрегату
print(df.groupby("city")["revenue"].sum().reset_index())
print()
# спосіб 2: as_index=False одразу
print(df.groupby("city", as_index=False)["revenue"].sum())
city revenue
0 Kyiv 87516.0
1 Lviv 51323.5
2 Odesa 58477.0
city revenue
0 Kyiv 87516.0
1 Lviv 51323.5
2 Odesa 58477.0
Групувати можна не лише за стовпцем таблиці, а й за будь-якою Series тієї ж довжини — наприклад, за обчисленою умовою:
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
# ключ — обчислена Series, а не стовпець таблиці
is_big = df["revenue"] > 15000
print(df.groupby(is_big)["revenue"].agg(["size", "sum"]))
Групування за кількома ключами і MultiIndex¶
Список ключів дає групи за комбінаціями значень:
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
by_city_cat = df.groupby(["city", "category"])["revenue"].sum()
print(by_city_cat)
print()
print(by_city_cat.index)
city category
Kyiv audio 13750.0
displays 52493.0
peripherals 21273.0
Lviv audio 30000.0
peripherals 21323.5
Odesa displays 44994.0
peripherals 13483.0
Name: revenue, dtype: float64
MultiIndex([( 'Kyiv', 'audio'),
( 'Kyiv', 'displays'),
( 'Kyiv', 'peripherals'),
( 'Lviv', 'audio'),
( 'Lviv', 'peripherals'),
('Odesa', 'displays'),
('Odesa', 'peripherals')],
names=['city', 'category'])
Індекс результату тепер багаторівневий (MultiIndex): кожна мітка — кортеж (city, category). Повторювані значення зовнішнього рівня pandas просто не друкує, щоб таблиця читалася легше.
Доступ до елементів — через кортежі або за зовнішнім рівнем:
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
by_city_cat = df.groupby(["city", "category"])["revenue"].sum()
# одна клітинка — кортеж міток
print(by_city_cat.loc[("Kyiv", "displays")])
print()
# усе для одного міста — мітка зовнішнього рівня
print(by_city_cat.loc["Lviv"])
print()
# зріз за внутрішнім рівнем: одна категорія в усіх містах
print(by_city_cat.xs("audio", level="category"))
print()
# агрегат по одному рівню MultiIndex
print(by_city_cat.groupby(level="city").max())
52493.0
category
audio 30000.0
peripherals 21323.5
Name: revenue, dtype: float64
city
Kyiv 13750.0
Lviv 30000.0
Name: revenue, dtype: float64
city
Kyiv 52493.0
Lviv 30000.0
Odesa 44994.0
Name: revenue, dtype: float64
MultiIndex потужний, але незручний для подальшої роботи. У більшості випадків після групування роблять reset_index() або unstack().
unstack і stack: з довгої таблиці в широку¶
unstack переносить внутрішній рівень індексу в стовпці — виходить звична двовимірна таблиця «міста × категорії»:
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
by_city_cat = df.groupby(["city", "category"])["revenue"].sum()
wide = by_city_cat.unstack()
print(wide)
print()
# пропущені комбінації -> 0
wide = by_city_cat.unstack(fill_value=0)
print(wide)
print()
# stack — зворотна операція
print(wide.stack().head(4))
category audio displays peripherals
city
Kyiv 13750.0 52493.0 21273.0
Lviv 30000.0 NaN 21323.5
Odesa NaN 44994.0 13483.0
category audio displays peripherals
city
Kyiv 13750.0 52493.0 21273.0
Lviv 30000.0 0.0 21323.5
Odesa 0.0 44994.0 13483.0
city category
Kyiv audio 13750.0
displays 52493.0
peripherals 21273.0
Lviv audio 30000.0
dtype: float64
NaN з'явився там, де такої комбінації в даних немає: в Одесі не продавали аудіо. Для сум логічно поставити нуль, для середніх — ні (див. попередження про fillna(0) у лекції 32).
Іменовані агрегати та власні функції¶
Коли для групи потрібно кілька показників, agg з групуванням дає цілий звіт одним викликом:
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
# список агрегатів -> стовпці з двома рівнями назв
print(df.groupby("city")["revenue"].agg(["size", "sum", "mean"]))
print()
# словник: різні агрегати для різних стовпців
report = df.groupby("city").agg({"units": "sum", "revenue": ["sum", "max"]})
print(report)
print()
print(list(report.columns))
size sum mean
city
Kyiv 5 87516.0 17503.200000
Lviv 4 51323.5 12830.875000
Odesa 3 58477.0 19492.333333
units revenue
sum sum max
city
Kyiv 60 87516.0 29996.0
Lviv 63 51323.5 18750.0
Odesa 32 58477.0 44994.0
[('units', 'sum'), ('revenue', 'sum'), ('revenue', 'max')]
У другому випадку стовпці отримали двоярусні назви — кортежі ("revenue", "sum"). З такими незручно працювати. Тому в сучасному коді використовують іменовані агрегати: нова_назва=("стовпець", "функція").
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
report = df.groupby("city").agg(
orders=("revenue", "size"),
units=("units", "sum"),
revenue=("revenue", "sum"),
avg_check=("revenue", "mean"),
first_sale=("date", "min"),
products=("product", "nunique"),
)
print(report.round({"avg_check": 2}))
print()
print(list(report.columns))
orders units revenue avg_check first_sale products
city
Kyiv 5 60 87516.0 17503.20 2026-03-02 4
Lviv 4 63 51323.5 12830.88 2026-03-02 3
Odesa 3 32 58477.0 19492.33 2026-03-09 3
['orders', 'units', 'revenue', 'avg_check', 'first_sale', 'products']
Назви стовпців — прості рядки, які ви обрали самі. Це найчитабельніша форма агрегації: з коду одразу видно, який показник з якого стовпця і як порахований.
Замість назви функції можна передати власну функцію. Вона отримує Series однієї групи й має повернути одне значення:
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
def price_range(values):
# різниця між найдорожчим і найдешевшим товаром у групі
return values.max() - values.min()
report = df.groupby("city").agg(
price_range=("price", price_range),
units_span=("units", lambda s: s.max() - s.min()),
top_product=("product", lambda s: s.value_counts().idxmax()),
)
print(report)
price_range units_span top_product
city
Kyiv 7149.5 27 Monitor
Lviv 900.5 16 Headset
Odesa 7149.5 12 Keyboard
Вбудовані агрегати — швидкі, власні — повільні
"sum", "mean", "max" pandas рахує оптимізованим кодом без циклу Python. Власна функція викликається окремо для кожної групи — на мільйонах груп це відчутно. Використовуйте її лише тоді, коли вбудованого еквівалента немає.
Про apply: на групуванні є ще метод apply, який передає у функцію всю групу як DataFrame і дозволяє повернути що завгодно. Він найгнучкіший і найповільніший. Майже завжди задачу можна розв'язати через agg, transform або filter, які розглянемо далі.
size, count і пропуски в ключах¶
Дві схожі функції, що часто плутають:
size— кількість рядків у групі;count— кількість непорожніх значень у кожному стовпці групи.
import pandas as pd
df = pd.DataFrame({
"city": ["Kyiv", "Kyiv", "Lviv", "Lviv", None, "Kyiv"],
"units": [12.0, None, 25.0, 15.0, 7.0, 30.0],
"rating": [5.0, 4.0, None, None, 3.0, 4.0],
})
print(df.groupby("city").size())
print()
print(df.groupby("city").count())
print()
# рядки з пропуском у ключі за замовчуванням відкидаються
print(df.groupby("city", dropna=False).size())
city
Kyiv 3
Lviv 2
dtype: int64
units rating
city
Kyiv 2 3
Lviv 2 0
city
Kyiv 3
Lviv 2
NaN 1
dtype: int64
Два важливі висновки:
- Для питання «скільки замовлень» потрібен
size.countвідповідає на інше питання: «скільки заповнених значень». - Рядок, у якого ключ порожній, тихо зникає з групування. Звіт може не зійтися із загальною сумою — і ніякої помилки.
dropna=Falseробить такі рядки окремою групоюNaN.
transform: результат групи — назад у кожен рядок¶
Агрегат згортає групу в одне значення: з 12 рядків виходить 3. Але часто потрібно інше — щоб у кожному рядку було значення, пораховане по його групі. Наприклад, «частка цього продажу у виручці свого міста».
transform рахує той самий агрегат, але розтягує результат назад на всі рядки групи, зберігаючи вихідний індекс:
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
# agg: одне значення на групу (3 рядки)
print(df.groupby("city")["revenue"].sum())
print()
# transform: те саме значення в кожному рядку групи (12 рядків)
city_total = df.groupby("city")["revenue"].transform("sum")
print(city_total.tolist())
city
Kyiv 87516.0
Lviv 51323.5
Odesa 58477.0
Name: revenue, dtype: float64
[87516.0, 51323.5, 87516.0, 58477.0, 51323.5, 87516.0, 58477.0, 87516.0, 51323.5, 87516.0, 58477.0, 51323.5]
graph LR
subgraph agg
A1["12 рядків"] --> A2["3 значення<br/>(по одному на місто)"]
end
subgraph transform
T1["12 рядків"] --> T2["12 значень<br/>(сума свого міста в кожному рядку)"]
end
Оскільки довжина та індекс збігаються з вихідною таблицею, результат можна одразу покласти в новий стовпець або використати в арифметиці:
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
df["city_total"] = df.groupby("city")["revenue"].transform("sum")
df["share"] = df["revenue"] / df["city_total"] * 100
# відхилення від середньої кількості у своїй категорії
df["units_vs_cat"] = df["units"] - df.groupby("category")["units"].transform("mean")
print(df[["city", "product", "revenue", "city_total", "share", "units_vs_cat"]].round(2))
print()
# перевірка: частки всередині міста дають 100%
print(df.groupby("city")["share"].sum().round(6))
city product revenue city_total share units_vs_cat
0 Kyiv Keyboard 10788.0 87516.0 12.33 -5.83
1 Lviv Mouse 8737.5 51323.5 17.02 7.17
2 Kyiv Monitor 29996.0 87516.0 34.27 -0.33
3 Odesa Keyboard 7192.0 58477.0 12.30 -9.83
4 Lviv Headset 18750.0 51323.5 36.53 3.33
5 Kyiv Mouse 10485.0 87516.0 11.98 12.17
6 Odesa Monitor 44994.0 58477.0 76.94 1.67
7 Kyiv Headset 13750.0 87516.0 15.71 -0.67
8 Lviv Keyboard 12586.0 51323.5 24.52 -3.83
9 Kyiv Monitor 22497.0 87516.0 25.71 -1.33
10 Odesa Mouse 6291.0 58477.0 10.76 0.17
11 Lviv Headset 11250.0 51323.5 21.92 -2.67
city
Kyiv 100.0
Lviv 100.0
Odesa 100.0
Name: share, dtype: float64
Ще одне класичне застосування transform — заповнення пропусків середнім по групі, а не по всій таблиці:
import pandas as pd
df = pd.DataFrame({
"category": ["displays", "displays", "displays", "peripherals", "peripherals", "peripherals"],
"product": ["Monitor A", "Monitor B", "Monitor C", "Mouse", "Keyboard", "Webcam"],
"price": [7499.0, None, 8100.0, 349.5, 899.0, None],
})
# середня ціна по всій таблиці спотворює обидві категорії
print("global mean:", round(df["price"].mean(), 2))
# середня ціна своєї категорії — розумніша заміна
df["price_filled"] = df["price"].fillna(df.groupby("category")["price"].transform("mean"))
print(df)
global mean: 4211.88
category product price price_filled
0 displays Monitor A 7499.0 7499.00
1 displays Monitor B NaN 7799.50
2 displays Monitor C 8100.0 8100.00
3 peripherals Mouse 349.5 349.50
4 peripherals Keyboard 899.0 899.00
5 peripherals Webcam NaN 624.25
Замість середнього по всій таблиці (4211.88 — ні монітор, ні мишка) кожен пропуск отримав середнє своєї категорії.
filter: відбір цілих груп¶
Звичайна маска відбирає рядки. filter відбирає групи: функція отримує групу як DataFrame і повертає True (залишити всі рядки групи) або False (викинути всі).
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
# залишити лише категорії, де було щонайменше 4 продажі
frequent = df.groupby("category").filter(lambda g: len(g) >= 4)
print(frequent[["date", "product", "category", "units"]])
print()
# залишити лише міста з сумарною виручкою понад 55000
rich = df.groupby("city").filter(lambda g: g["revenue"].sum() > 55000)
print(rich["city"].unique().tolist())
date product category units
0 2026-03-02 Keyboard peripherals 12
1 2026-03-02 Mouse peripherals 25
3 2026-03-09 Keyboard peripherals 8
5 2026-03-20 Mouse peripherals 30
8 2026-04-08 Keyboard peripherals 14
10 2026-04-22 Mouse peripherals 18
['Kyiv', 'Odesa']
Результат — вихідні рядки (з вихідним індексом), а не агрегати.
Той самий результат дає маска з transform — і вона працює швидше, бо не викликає функцію Python для кожної групи:
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
# те саме, що filter(lambda g: g["revenue"].sum() > 55000), але векторно
mask = df.groupby("city")["revenue"].transform("sum") > 55000
print(df.loc[mask, "city"].unique().tolist())
| Метод | Що повертає | Розмір результату |
|---|---|---|
agg |
одне значення на групу | кількість груп |
transform |
значення групи в кожному рядку | як у вихідної таблиці |
filter |
вихідні рядки груп, що пройшли умову | менший або рівний вихідному |
Топ-N у групі, ранги та накопичувальні суми¶
«Найкращий продаж у кожному місті» — не агрегат, адже потрібен весь рядок, а не одне число. Стандартний прийом: відсортувати, згрупувати, взяти перші рядки кожної групи.
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
# найбільший продаж у кожному місті
best = df.sort_values("revenue", ascending=False).groupby("city").head(1)
print(best[["city", "date", "product", "revenue"]])
print()
# два найбільші продажі в кожній категорії
top2 = df.sort_values("revenue", ascending=False).groupby("category").head(2)
print(top2.sort_values(["category", "revenue"], ascending=[True, False])[["category", "product", "revenue"]])
print()
# через idxmax: мітки рядків-рекордсменів
print(df.loc[df.groupby("city")["revenue"].idxmax(), ["city", "product", "revenue"]])
city date product revenue
6 Odesa 2026-04-01 Monitor 44994.0
2 Kyiv 2026-03-05 Monitor 29996.0
4 Lviv 2026-03-12 Headset 18750.0
category product revenue
4 audio Headset 18750.0
7 audio Headset 13750.0
6 displays Monitor 44994.0
2 displays Monitor 29996.0
8 peripherals Keyboard 12586.0
0 peripherals Keyboard 10788.0
city product revenue
2 Kyiv Monitor 29996.0
4 Lviv Headset 18750.0
6 Odesa Monitor 44994.0
Для топ-N по всій таблиці є ще коротші nlargest і nsmallest: df.nlargest(3, "revenue").
Всередині групи працюють і «віконні» операції, які рахують значення для кожного рядка з урахуванням його сусідів у групі:
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
df = df.sort_values(["city", "date"])
g = df.groupby("city")["revenue"]
df["sale_no"] = df.groupby("city").cumcount() + 1 # номер продажу в місті
df["running"] = g.cumsum() # накопичена виручка
df["prev"] = g.shift(1) # попередній продаж у місті
df["rank"] = g.rank(ascending=False).astype(int) # місце за виручкою в місті
print(df[["city", "date", "revenue", "sale_no", "running", "prev", "rank"]])
city date revenue sale_no running prev rank
0 Kyiv 2026-03-02 10788.0 1 10788.0 NaN 4
2 Kyiv 2026-03-05 29996.0 2 40784.0 10788.0 1
5 Kyiv 2026-03-20 10485.0 3 51269.0 29996.0 5
7 Kyiv 2026-04-03 13750.0 4 65019.0 10485.0 3
9 Kyiv 2026-04-15 22497.0 5 87516.0 13750.0 2
1 Lviv 2026-03-02 8737.5 1 8737.5 NaN 4
4 Lviv 2026-03-12 18750.0 2 27487.5 8737.5 1
8 Lviv 2026-04-08 12586.0 3 40073.5 18750.0 2
11 Lviv 2026-04-28 11250.0 4 51323.5 12586.0 3
3 Odesa 2026-03-09 7192.0 1 7192.0 NaN 2
6 Odesa 2026-04-01 44994.0 2 52186.0 7192.0 1
10 Odesa 2026-04-22 6291.0 3 58477.0 44994.0 3
cumcount()— порядковий номер рядка в групі, починаючи з 0;cumsum()— накопичувальна сума в межах групи; для нової групи починається спочатку;shift(1)— значення з попереднього рядка тієї ж групи; для першого рядка групи —NaN;rank()— місце значення всередині групи.
Порядок рядків тут важливий: накопичувальна сума «за датою» має сенс лише після сортування за датою.
Групування за датами¶
Часова динаміка — одне з найчастіших питань. Якщо стовпець має тип datetime64 (через parse_dates або pd.to_datetime), аксесор .dt дає частини дати, за якими можна групувати:
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
print(df.groupby(df["date"].dt.month)["revenue"].sum())
print()
# назва дня тижня
print(df.groupby(df["date"].dt.day_name())["units"].sum())
print()
# період "рік-місяць" — надійніше, ніж просто номер місяця
print(df.groupby(df["date"].dt.to_period("M"))["revenue"].agg(["size", "sum"]))
date
3 85948.5
4 111368.0
Name: revenue, dtype: float64
date
Friday 41
Monday 45
Thursday 19
Tuesday 9
Wednesday 41
Name: units, dtype: int64
size sum
date
2026-03 6 85948.5
2026-04 6 111368.0
Номер місяця без року
dt.month зливає березень 2025 і березень 2026 в одну групу 3. Якщо дані охоплюють кілька років, групуйте за dt.to_period("M") або за парою [dt.year, dt.month].
Для часових рядів є спеціальний інструмент — resample. Він працює, коли дата стоїть в індексі, і створює всі інтервали, навіть порожні:
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
by_date = df.set_index("date")
# виручка по тижнях (тиждень закінчується в неділю)
weekly = by_date["revenue"].resample("W").sum()
print(weekly)
print()
# те саме без set_index: pd.Grouper у groupby, разом з іншим ключем
print(df.groupby([pd.Grouper(key="date", freq="MS"), "category"])["revenue"].sum().unstack(fill_value=0))
date
2026-03-08 49521.5
2026-03-15 25942.0
2026-03-22 10485.0
2026-03-29 0.0
2026-04-05 58744.0
2026-04-12 12586.0
2026-04-19 22497.0
2026-04-26 6291.0
2026-05-03 11250.0
Freq: W-SUN, Name: revenue, dtype: float64
category audio displays peripherals
date
2026-03-01 18750.0 29996.0 37202.5
2026-04-01 25000.0 67491.0 18877.0
Тиждень з 23 по 29 березня без продажів отримав 0.0 — groupby за номером тижня такого рядка просто б не створив. Для графіків і звітів «по днях / тижнях / місяцях» це важливо: провал у даних має бути видно.
Часті частоти: "D" — день, "W" — тиждень, "MS" / "ME" — місяць (мітка на початку / в кінці), "QS" — квартал, "YS" — рік, "h" — година.
Групування за інтервалами: cut і qcut¶
Числовий стовпець часто хочуть розбити на діапазони: дешеві / середні / дорогі товари, вікові групи, оцінки. Для цього є pd.cut (межі задаєте ви) і pd.qcut (межі — квантилі, групи однакового розміру):
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
# межі задаємо самі: (0, 1000], (1000, 5000], (5000, 10000]
df["price_band"] = pd.cut(
df["price"],
bins=[0, 1000, 5000, 10000],
labels=["cheap", "mid", "premium"],
)
print(df[["product", "price", "price_band"]].drop_duplicates("product"))
print()
print(df.groupby("price_band", observed=True)["revenue"].agg(["size", "sum"]))
print()
# qcut: 3 групи приблизно однакового розміру за кількістю
df["units_q"] = pd.qcut(df["units"], q=3, labels=["low", "mid", "high"])
print(df["units_q"].value_counts().sort_index())
product price price_band
0 Keyboard 899.0 cheap
1 Mouse 349.5 cheap
2 Monitor 7499.0 premium
4 Headset 1250.0 mid
size sum
price_band
cheap 6 56079.5
mid 3 43750.0
premium 3 97487.0
units_q
low 4
mid 4
high 4
Name: count, dtype: int64
Результат cut — стовпець типу category: він пам'ятає всі можливі значення, навіть ті, що не трапилися в даних. Параметр observed=True у groupby каже показувати лише ті категорії, що справді зустрілися.
Зведені таблиці: pivot_table і crosstab¶
Зведена таблиця (як Pivot Table в Excel) — це групування за двома ключами, де один ключ іде в рядки, а другий — у стовпці. Ми вже робили її вручну через groupby + unstack. pivot_table робить це одним викликом:
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
table = df.pivot_table(
index="city",
columns="category",
values="revenue",
aggfunc="sum",
fill_value=0,
margins=True,
margins_name="total",
)
print(table)
category audio displays peripherals total
city
Kyiv 13750.0 52493.0 21273.0 87516.0
Lviv 30000.0 0.0 21323.5 51323.5
Odesa 0.0 44994.0 13483.0 58477.0
total 43750.0 97487.0 56079.5 197316.5
index— що піде в рядки;columns— що піде в стовпці;values— що агрегуємо;aggfunc— як агрегуємо (за замовчуванням"mean"— не забувайте вказувати!);fill_value— чим заповнити порожні комбінації;margins=True— додати підсумковий рядок і стовпець.
Можна передати кілька ключів і кілька агрегатів:
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
df["month"] = df["date"].dt.to_period("M")
table = df.pivot_table(
index=["city"],
columns="month",
values="units",
aggfunc=["sum", "count"],
fill_value=0,
)
print(table)
pd.crosstab — спеціальний випадок зведеної таблиці для підрахунку частот: скільки разів трапилася кожна комбінація двох ознак.
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
# скільки продажів кожної категорії в кожному місті
print(pd.crosstab(df["city"], df["category"]))
print()
# частки по рядках: яку частку продажів міста дає кожна категорія
print(pd.crosstab(df["city"], df["category"], normalize="index").round(2))
category audio displays peripherals
city
Kyiv 1 2 2
Lviv 2 0 2
Odesa 0 1 2
category audio displays peripherals
city
Kyiv 0.2 0.40 0.40
Lviv 0.5 0.00 0.50
Odesa 0.0 0.33 0.67
pivot і melt: зміна форми таблиці¶
Дані бувають у двох формах:
- довга (long): одне спостереження — один рядок.
(city, month, revenue). Зручно для групування, фільтрації, запису в БД; - широка (wide): одна сутність — один рядок, значення розкладені по стовпцях.
city | 2026-03 | 2026-04. Зручно для читання людиною та Excel.
pivot перетворює довгу форму на широку без агрегації, melt — навпаки:
import pandas as pd
long = pd.DataFrame({
"city": ["Kyiv", "Kyiv", "Lviv", "Lviv", "Odesa"],
"month": ["2026-03", "2026-04", "2026-03", "2026-04", "2026-04"],
"revenue": [51269.0, 36247.0, 27487.5, 23836.0, 51285.0],
})
# довга -> широка
wide = long.pivot(index="city", columns="month", values="revenue")
print(wide)
print()
# широка -> довга
back = wide.reset_index().melt(id_vars="city", var_name="month", value_name="revenue")
print(back)
month 2026-03 2026-04
city
Kyiv 51269.0 36247.0
Lviv 27487.5 23836.0
Odesa NaN 51285.0
city month revenue
0 Kyiv 2026-03 51269.0
1 Lviv 2026-03 27487.5
2 Odesa 2026-03 NaN
3 Kyiv 2026-04 36247.0
4 Lviv 2026-04 23836.0
5 Odesa 2026-04 51285.0
pivot проти pivot_table
pivot лише переставляє значення і кидає ValueError: Index contains duplicate entries, якщо для якоїсь пари (index, columns) є більше одного рядка. pivot_table у такому разі агрегує дублікати. Якщо дані «сирі» — вам потрібен pivot_table.
melt особливо корисний при читанні Excel-звітів, де місяці йдуть стовпцями: після melt такий звіт можна групувати, фільтрувати й з'єднувати, як звичайні дані.
Об'єднання таблиць: merge¶
У реальних системах дані рідко лежать в одній таблиці. Продажі зберігають ідентифікатор товару, а назва, категорія й ціна живуть у каталозі. Щоб порахувати виручку за категоріями, таблиці треба з'єднати за спільним ключем — так само, як JOIN у SQL.
import pandas as pd
orders = pd.DataFrame({
"order_id": [101, 102, 103, 104, 105, 106],
"product_id": [1, 2, 1, 3, 2, 99],
"units": [2, 1, 5, 1, 3, 4],
})
products = pd.DataFrame({
"product_id": [1, 2, 3, 4],
"name": ["Keyboard", "Mouse", "Monitor", "Webcam"],
"price": [899.0, 349.5, 7499.0, 2100.0],
})
merged = orders.merge(products, on="product_id")
print(merged)
print()
merged["revenue"] = merged["units"] * merged["price"]
print(merged.groupby("name")["revenue"].sum())
order_id product_id units name price
0 101 1 2 Keyboard 899.0
1 102 2 1 Mouse 349.5
2 103 1 5 Keyboard 899.0
3 104 3 1 Monitor 7499.0
4 105 2 3 Mouse 349.5
name
Keyboard 6293.0
Monitor 7499.0
Mouse 1398.0
Name: revenue, dtype: float64
merge знайшов для кожного замовлення рядок каталогу з тим самим product_id і приклеїв його стовпці. Зверніть увагу на дві речі:
- замовлення
106з товаром99зникло — такого товару в каталозі немає; - товар
Webcam(4) теж не потрапив у результат — його ніхто не замовляв.
Це поведінка внутрішнього з'єднання (how="inner", за замовчуванням): у результат потрапляють лише ключі, що є в обох таблицях.
Типи з'єднань і пошук «сиріт»¶
Параметр how визначає, що робити з ключами, які є лише в одній таблиці:
how |
Які рядки залишаються | SQL |
|---|---|---|
"inner" |
ключ є в обох таблицях | INNER JOIN |
"left" |
усі рядки лівої таблиці | LEFT JOIN |
"right" |
усі рядки правої таблиці | RIGHT JOIN |
"outer" |
усі рядки обох таблиць | FULL OUTER JOIN |
"cross" |
кожен з кожним | CROSS JOIN |
graph LR
subgraph "orders.product_id"
O["1, 2, 3, 99"]
end
subgraph "products.product_id"
P["1, 2, 3, 4"]
end
O --> I["inner: 1, 2, 3"]
P --> I
O --> L["left: 1, 2, 3, 99"]
P --> OU["outer: 1, 2, 3, 4, 99"]
O --> OU
import pandas as pd
orders = pd.DataFrame({
"order_id": [101, 102, 103, 104, 105, 106],
"product_id": [1, 2, 1, 3, 2, 99],
"units": [2, 1, 5, 1, 3, 4],
})
products = pd.DataFrame({
"product_id": [1, 2, 3, 4],
"name": ["Keyboard", "Mouse", "Monitor", "Webcam"],
"price": [899.0, 349.5, 7499.0, 2100.0],
})
print("left:")
print(orders.merge(products, on="product_id", how="left"))
print()
print("outer + indicator:")
full = orders.merge(products, on="product_id", how="outer", indicator=True)
print(full)
left:
order_id product_id units name price
0 101 1 2 Keyboard 899.0
1 102 2 1 Mouse 349.5
2 103 1 5 Keyboard 899.0
3 104 3 1 Monitor 7499.0
4 105 2 3 Mouse 349.5
5 106 99 4 NaN NaN
outer + indicator:
order_id product_id units name price _merge
0 101.0 1 2.0 Keyboard 899.0 both
1 103.0 1 5.0 Keyboard 899.0 both
2 102.0 2 1.0 Mouse 349.5 both
3 105.0 2 3.0 Mouse 349.5 both
4 104.0 3 1.0 Monitor 7499.0 both
5 NaN 4 NaN Webcam 2100.0 right_only
6 106.0 99 4.0 NaN NaN left_only
При left замовлення 106 залишилося, а стовпці каталогу для нього — NaN. Параметр indicator=True додає стовпець _merge, який каже, звідки прийшов рядок: both, left_only або right_only. З ним легко відповісти на питання, що часто звучать при перевірці якості даних:
import pandas as pd
orders = pd.DataFrame({
"order_id": [101, 102, 103, 104, 105, 106],
"product_id": [1, 2, 1, 3, 2, 99],
"units": [2, 1, 5, 1, 3, 4],
})
products = pd.DataFrame({
"product_id": [1, 2, 3, 4],
"name": ["Keyboard", "Mouse", "Monitor", "Webcam"],
"price": [899.0, 349.5, 7499.0, 2100.0],
})
full = orders.merge(products, on="product_id", how="outer", indicator=True)
# замовлення з неіснуючим товаром ("сироти")
print(full.loc[full["_merge"] == "left_only", ["order_id", "product_id"]])
print()
# товари, яких ніхто не замовляв
print(full.loc[full["_merge"] == "right_only", ["product_id", "name"]])
print()
# коротший спосіб для другого питання — isin
print(products[~products["product_id"].isin(orders["product_id"])])
Якщо ключі в таблицях називаються по-різному, або однакові назви мають і інші стовпці, допоможуть left_on / right_on і suffixes:
import pandas as pd
orders = pd.DataFrame({
"order_id": [101, 102, 103],
"item": [1, 2, 1],
"price": [850.0, 349.5, 899.0], # фактична ціна продажу (зі знижкою)
})
products = pd.DataFrame({
"id": [1, 2],
"name": ["Keyboard", "Mouse"],
"price": [899.0, 349.5], # ціна за каталогом
})
merged = orders.merge(
products,
left_on="item",
right_on="id",
suffixes=("_sold", "_list"),
validate="many_to_one",
)
merged["discount"] = merged["price_list"] - merged["price_sold"]
print(merged)
order_id item price_sold id name price_list discount
0 101 1 850.0 1 Keyboard 899.0 49.0
1 102 2 349.5 2 Mouse 349.5 0.0
2 103 1 899.0 1 Keyboard 899.0 0.0
validate="many_to_one" — страховка: pandas перевірить, що в правій таблиці кожен ключ унікальний, і кине MergeError, якщо ні. Без цієї перевірки дублікат у каталозі тихо подвоїть рядки замовлень — і всі суми після з'єднання стануть неправильними.
Дублікати ключів множать рядки
Якщо ключ 1 зустрічається у лівій таблиці 3 рази, а в правій 2 — у результаті буде 3 × 2 = 6 рядків. Після кожного merge корисно перевірити len(result) і порівняти з очікуваним.
Коли ключ лежить в індексі, замість merge можна використати join: orders.join(products.set_index("product_id"), on="product_id"). Це те саме з'єднання, лише інший синтаксис.
Склеювання таблиць: concat¶
merge приклеює таблиці збоку за ключем. concat ставить їх одну під одну (або поруч за індексом) — наприклад, щоб зібрати продажі з файлів за різні місяці:
import pandas as pd
march = pd.DataFrame({
"product": ["Keyboard", "Mouse"],
"units": [12, 25],
})
april = pd.DataFrame({
"product": ["Monitor", "Keyboard", "Headset"],
"units": [6, 14, 11],
})
# один під одним, з новою нумерацією рядків
both = pd.concat([march, april], ignore_index=True)
print(both)
print()
# з позначкою джерела: ключі стають зовнішнім рівнем індексу
labeled = pd.concat({"2026-03": march, "2026-04": april}, names=["month", "row"])
print(labeled)
print()
print(labeled.groupby(level="month")["units"].sum())
product units
0 Keyboard 12
1 Mouse 25
2 Monitor 6
3 Keyboard 14
4 Headset 11
product units
month row
2026-03 0 Keyboard 12
1 Mouse 25
2026-04 0 Monitor 6
1 Keyboard 14
2 Headset 11
month
2026-03 37
2026-04 31
Name: units, dtype: int64
Без ignore_index=True індекси обох таблиць збережуться як є: у результаті буде дві мітки 0 і дві мітки 1, і .loc[0] поверне два рядки.
Якщо в таблицях різні набори стовпців, concat об'єднає їх, а відсутні значення заповнить NaN.
Збирати таблиці — списком, а не в циклі
result = pd.concat([result, new_df]) усередині циклу щоразу копіює все накопичене — квадратична складність. Складайте таблиці у звичайний список і викликайте pd.concat(frames) один раз у кінці.
query: фільтрація рядком-виразом¶
Складні маски швидко стають нечитабельними через дужки й повторення df[...]. Метод query приймає умову рядком, у якому стовпці пишуться просто назвами:
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
# маска
a = df[(df["city"] == "Kyiv") & (df["units"] > 5) & (df["price"] < 5000)]
# те саме через query
b = df.query("city == 'Kyiv' and units > 5 and price < 5000")
print(b[["city", "product", "units", "price"]])
print("same:", a.equals(b))
city product units price
0 Kyiv Keyboard 12 899.0
5 Kyiv Mouse 30 349.5
7 Kyiv Headset 11 1250.0
same: True
Усередині query можна:
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
min_units = 10
cities = ["Lviv", "Odesa"]
# @ — посилання на змінну Python
print(df.query("units >= @min_units and city in @cities")[["city", "product", "units"]])
print()
# подвійна нерівність і арифметика
print(df.query("1000 <= price <= 5000 and units * price > 15000")[["product", "units", "revenue"]])
print()
# порівняння дат
print(df.query("date >= '2026-04-15'")[["date", "product"]])
print()
# not, or і рядкові методи
print(df.query("not (category == 'peripherals' or product.str.startswith('H'))")[["product", "category"]])
city product units
1 Lviv Mouse 25
4 Lviv Headset 15
8 Lviv Keyboard 14
10 Odesa Mouse 18
product units revenue
4 Headset 15 18750.0
date product
9 2026-04-15 Monitor
10 2026-04-22 Mouse
11 2026-04-28 Headset
product category
2 Monitor displays
6 Monitor displays
9 Monitor displays
- у
queryпрацюютьand,or,not— на відміну від масок, де потрібні&,|,~; @nameпідставляє змінну Python;in/not in— аналогisin;- подвійна нерівність
a <= x <= bпишеться природно; - назви стовпців із пробілами беруться у зворотні лапки:
`unit price` > 100.
Для діапазонів без query є ще метод between: df[df["price"].between(1000, 5000)] (обидві межі включно).
Коли query, а коли маска
query зручний для інтерактивної роботи та довгих умов. Маски універсальніші: з ними працюють .loc для присвоєння, і їх можна будувати програмно (зберігати в змінні, комбінувати). В одному проєкті краще дотримуватися одного стилю.
Ланцюжки методів: assign і pipe¶
Більшість методів pandas повертають нову таблицю, тому їх можна викликати ланцюжком: результат одного — вхід наступного. Такий код читається зверху вниз як рецепт:
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
report = (
df
.query("category != 'audio'")
.assign(
month=lambda d: d["date"].dt.to_period("M"),
big=lambda d: d["revenue"] > 10000,
)
.groupby(["month", "city"], as_index=False)
.agg(
orders=("revenue", "size"),
big_orders=("big", "sum"),
revenue=("revenue", "sum"),
)
.sort_values(["month", "revenue"], ascending=[True, False])
.reset_index(drop=True)
)
print(report)
month city orders big_orders revenue
0 2026-03 Kyiv 3 3 51269.0
1 2026-03 Lviv 1 0 8737.5
2 2026-03 Odesa 1 0 7192.0
3 2026-04 Odesa 2 1 51285.0
4 2026-04 Kyiv 1 1 22497.0
5 2026-04 Lviv 1 1 12586.0
assignдодає стовпці і повертає нову таблицю (на відміну відdf["x"] = ..., яке змінює таблицю на місці і нічого не повертає). Функціяlambda d: ...отримує поточний стан таблиці в ланцюжку — тому можна посилатися на стовпці, додані на попередніх кроках;- весь ланцюжок береться в круглі дужки — тоді кожен крок можна писати з нового рядка.
Якщо крок ланцюжка — ваша власна функція, вставте її через pipe:
import pandas as pd
rows = [
("2026-03-02", "Kyiv", "Keyboard", "peripherals", 12, 899.0),
("2026-03-02", "Lviv", "Mouse", "peripherals", 25, 349.5),
("2026-03-05", "Kyiv", "Monitor", "displays", 4, 7499.0),
("2026-03-09", "Odesa", "Keyboard", "peripherals", 8, 899.0),
("2026-03-12", "Lviv", "Headset", "audio", 15, 1250.0),
("2026-03-20", "Kyiv", "Mouse", "peripherals", 30, 349.5),
("2026-04-01", "Odesa", "Monitor", "displays", 6, 7499.0),
("2026-04-03", "Kyiv", "Headset", "audio", 11, 1250.0),
("2026-04-08", "Lviv", "Keyboard", "peripherals", 14, 899.0),
("2026-04-15", "Kyiv", "Monitor", "displays", 3, 7499.0),
("2026-04-22", "Odesa", "Mouse", "peripherals", 18, 349.5),
("2026-04-28", "Lviv", "Headset", "audio", 9, 1250.0),
]
df = pd.DataFrame(rows, columns=["date", "city", "product", "category", "units", "price"])
df["date"] = pd.to_datetime(df["date"])
df["revenue"] = df["units"] * df["price"]
def drop_small(frame, min_revenue):
# прибрати дрібні продажі
return frame[frame["revenue"] >= min_revenue]
def add_share(frame):
# частка кожного продажу у виручці свого міста, %
total = frame.groupby("city")["revenue"].transform("sum")
return frame.assign(share=(frame["revenue"] / total * 100).round(1))
result = (
df
.pipe(drop_small, min_revenue=10000)
.pipe(add_share)
.loc[:, ["city", "product", "revenue", "share"]]
.sort_values(["city", "share"], ascending=[True, False])
)
print(result)
city product revenue share
2 Kyiv Monitor 29996.0 34.3
9 Kyiv Monitor 22497.0 25.7
7 Kyiv Headset 13750.0 15.7
0 Kyiv Keyboard 10788.0 12.3
5 Kyiv Mouse 10485.0 12.0
4 Lviv Headset 18750.0 44.0
8 Lviv Keyboard 12586.0 29.6
11 Lviv Headset 11250.0 26.4
6 Odesa Monitor 44994.0 100.0
df.pipe(f, x=1) — те саме, що f(df, x=1), але не розриває ланцюжок. Кожну функцію можна окремо протестувати й перевикористати.
Налагодження ланцюжка
Якщо довгий ланцюжок видає дивний результат, закоментуйте кроки з кінця або вставте .pipe(lambda d: print(d.shape) or d) між кроками — він надрукує розмір проміжної таблиці і передасть її далі без змін.
pandas і SQL¶
Якщо ви знайомі з SQL, більшість операцій pandas мають прямий відповідник:
| SQL | pandas |
|---|---|
SELECT a, b |
df[["a", "b"]] |
WHERE a > 1 AND b = 'x' |
df[(df["a"] > 1) & (df["b"] == "x")] або df.query("a > 1 and b == 'x'") |
WHERE a IN (1, 2) |
df[df["a"].isin([1, 2])] |
WHERE a BETWEEN 1 AND 5 |
df[df["a"].between(1, 5)] |
ORDER BY a DESC |
df.sort_values("a", ascending=False) |
LIMIT 10 |
.head(10) |
SELECT DISTINCT a |
df["a"].unique() / df.drop_duplicates("a") |
COUNT(*) |
len(df) / .size() у групі |
GROUP BY a |
df.groupby("a") |
SUM(b) AS total |
.agg(total=("b", "sum")) |
HAVING SUM(b) > 100 |
.filter(lambda g: g["b"].sum() > 100) або фільтр після agg |
INNER / LEFT JOIN ... ON |
df.merge(other, on=..., how=...) |
UNION ALL |
pd.concat([df1, df2]) |
SUM(b) OVER (PARTITION BY a) |
df.groupby("a")["b"].transform("sum") |
ROW_NUMBER() OVER (PARTITION BY a ORDER BY b) |
df.sort_values("b").groupby("a").cumcount() + 1 |
Різниця в тому, що pandas працює з даними в пам'яті вашої програми: це швидко й гнучко, але таблиця має вміщатися в оперативну пам'ять. Для даних, що живуть у базі, часто вигідніше зробити агрегацію в SQL і завантажити в pandas уже невеликий результат.
Повний приклад: звіт з трьох таблиць¶
Магазин вивантажив три файли: замовлення, каталог товарів і список магазинів з регіонами. Потрібно:
- з'єднати таблиці й знайти замовлення з невідомим товаром;
- порахувати по регіонах і місяцях виручку, кількість замовлень і середній чек;
- знайти найкращий товар у кожному регіоні;
- зберегти зведену таблицю «регіон × місяць» у CSV.
from pathlib import Path
import pandas as pd
ORDERS_CSV = """order_id,date,store_id,product_id,units
1,2026-03-02,S1,P1,12
2,2026-03-02,S2,P2,25
3,2026-03-05,S1,P3,4
4,2026-03-09,S3,P1,8
5,2026-03-12,S2,P4,15
6,2026-03-20,S1,P2,30
7,2026-04-01,S3,P3,6
8,2026-04-03,S4,P4,11
9,2026-04-08,S2,P1,14
10,2026-04-15,S4,P3,3
11,2026-04-22,S3,P2,18
12,2026-04-28,S2,P9,9
"""
PRODUCTS_CSV = """product_id,name,category,price
P1,Keyboard,peripherals,899.00
P2,Mouse,peripherals,349.50
P3,Monitor,displays,7499.00
P4,Headset,audio,1250.00
P5,Webcam,video,2100.00
"""
STORES_CSV = """store_id,city,region
S1,Kyiv,north
S2,Lviv,west
S3,Odesa,south
S4,Chernihiv,north
"""
Path("orders.csv").write_text(ORDERS_CSV, encoding="utf-8")
Path("products.csv").write_text(PRODUCTS_CSV, encoding="utf-8")
Path("stores.csv").write_text(STORES_CSV, encoding="utf-8")
# 1. читання
orders = pd.read_csv("orders.csv", parse_dates=["date"])
products = pd.read_csv("products.csv")
stores = pd.read_csv("stores.csv")
# 2. з'єднання з перевіркою унікальності ключів у довідниках
full = (
orders
.merge(products, on="product_id", how="left", validate="many_to_one", indicator=True)
.merge(stores, on="store_id", how="left", validate="many_to_one")
)
orphans = full[full["_merge"] == "left_only"]
print("orders with unknown product:")
print(orphans[["order_id", "date", "product_id", "units"]])
print()
# 3. працюємо лише з коректними замовленнями
sales = (
full[full["_merge"] == "both"]
.drop(columns="_merge")
.assign(
revenue=lambda d: d["units"] * d["price"],
month=lambda d: d["date"].dt.to_period("M"),
)
)
# 4. показники по регіонах і місяцях
summary = (
sales
.groupby(["region", "month"], as_index=False)
.agg(
orders=("order_id", "size"),
units=("units", "sum"),
revenue=("revenue", "sum"),
avg_check=("revenue", "mean"),
)
.round({"avg_check": 2})
)
print("region x month:")
print(summary)
print()
# 5. частка регіону в загальній виручці
by_region = sales.groupby("region")["revenue"].sum().sort_values(ascending=False)
print("revenue share by region, %:")
print((by_region / by_region.sum() * 100).round(1))
print()
# 6. найкращий товар у кожному регіоні
best = (
sales
.groupby(["region", "name"], as_index=False)["revenue"].sum()
.sort_values("revenue", ascending=False)
.groupby("region")
.head(1)
.sort_values("region")
)
print("best product per region:")
print(best)
print()
# 7. зведена таблиця для керівництва і збереження
pivot = sales.pivot_table(
index="region",
columns="month",
values="revenue",
aggfunc="sum",
fill_value=0,
margins=True,
margins_name="total",
)
print(pivot)
pivot.to_csv("revenue_by_region.csv")
print()
print(Path("revenue_by_region.csv").read_text(encoding="utf-8"))
orders with unknown product:
order_id date product_id units
11 12 2026-04-28 P9 9
region x month:
region month orders units revenue avg_check
0 north 2026-03 3 46 51269.0 17089.67
1 north 2026-04 2 14 36247.0 18123.50
2 south 2026-03 1 8 7192.0 7192.00
3 south 2026-04 2 24 51285.0 25642.50
4 west 2026-03 2 40 27487.5 13743.75
5 west 2026-04 1 14 12586.0 12586.00
revenue share by region, %:
region
north 47.0
south 31.4
west 21.5
Name: revenue, dtype: float64
best product per region:
region name revenue
2 north Monitor 52493.0
5 south Monitor 44994.0
7 west Headset 18750.0
month 2026-03 2026-04 total
region
north 51269.0 36247.0 87516.0
south 7192.0 51285.0 58477.0
west 27487.5 12586.0 40073.5
total 85948.5 100118.0 186066.5
region,2026-03,2026-04,total
north,51269.0,36247.0,87516.0
south,7192.0,51285.0,58477.0
west,27487.5,12586.0,40073.5
total,85948.5,100118.0,186066.5
Структура типового аналітичного скрипту: прочитати → з'єднати й перевірити → почистити → збагатити стовпцями → згрупувати → звести → зберегти. Кожен крок — одна-дві операції над цілими стовпцями, жодного циклу по рядках.
У зведеній таблиці індекс (region) несе зміст, тому to_csv тут викликано без index=False.
Типові помилки¶
pivot_table без aggfunc. За замовчуванням рахується середнє, а не сума. Таблиця виглядає правдоподібно, але числа неправильні. Завжди вказуйте aggfunc явно.
count замість size. count рахує непорожні значення в кожному стовпці. Якщо в стовпці є пропуски, «кількість замовлень» вийде заниженою.
Зникнення рядків з пропуском у ключі. groupby за замовчуванням відкидає рядки, де ключ — NaN. Сума по групах не зійдеться з загальною. Перевіряйте df["key"].isna().sum() або використовуйте dropna=False.
df.groupby("city").mean() на таблиці з текстом. Кине TypeError, бо не знає, як усереднити product. Вибирайте стовпці: df.groupby("city")["revenue"].mean(), або передайте numeric_only=True.
Спроба присвоїти результат agg як стовпець.
import pandas as pd
df = pd.DataFrame({
"city": ["Kyiv", "Lviv", "Kyiv"],
"revenue": [100.0, 200.0, 300.0],
})
# НЕПРАВИЛЬНО: agg повертає 2 значення з індексом-містами, а не 3 з індексом 0..2
df["wrong"] = df.groupby("city")["revenue"].sum()
# ПРАВИЛЬНО: transform повертає значення для кожного рядка
df["city_total"] = df.groupby("city")["revenue"].transform("sum")
print(df)
Помилки не було — pandas вирівняв за індексом, не знайшов міток 0, 1, 2 серед "Kyiv", "Lviv" і тихо записав NaN. Для «значення групи в кожному рядку» потрібен transform.
Двоярусні назви стовпців після agg зі списком. df.groupby("a").agg({"b": ["sum", "mean"]}) дає стовпці-кортежі ("b", "sum"), з якими незручно працювати. Використовуйте іменовані агрегати: .agg(b_sum=("b", "sum"), b_mean=("b", "mean")).
Групування за місяцем без року. dt.month зливає однакові місяці різних років. Використовуйте dt.to_period("M").
Дублікати ключа в довіднику при merge. Рядки мовчки множаться, суми зростають. Додавайте validate="many_to_one" і перевіряйте len() результату.
Неочікуване зникнення рядків після merge. how="inner" за замовчуванням викидає рядки без пари. Якщо важливо зберегти всі замовлення — how="left", а indicator=True покаже, у яких рядках пари не знайшлося.
Різні типи ключів. Якщо в одній таблиці product_id — число 1, а в іншій рядок "1", merge кине ValueError про несумісні типи. Приведіть обидва ключі до одного типу через astype перед з'єднанням.
pd.concat у циклі. Кожен виклик копіює все накопичене. Збирайте таблиці у список і склеюйте один раз.
concat без ignore_index=True. Індекси повторюються, і .loc[0] раптом повертає кілька рядків.
pivot на даних з дублікатами. ValueError: Index contains duplicate entries. Потрібен pivot_table з aggfunc.
apply там, де вистачає вбудованого агрегату. df.groupby("a").apply(lambda g: g["b"].sum()) дає той самий результат, що df.groupby("a")["b"].sum(), але в рази повільніше.
and / or у масках, але & / | у query. У масках працюють лише &, |, ~; у рядку query — навпаки, природніше писати and, or, not.
Підсумок¶
- Агрегат згортає багато значень в одне:
sum,mean,median,min,max,count,nunique,idxmax. НаDataFrameрахується по кожному стовпцю (axis=0) або по кожному рядку (axis=1). aggрахує кілька агрегатів одразу: список або словник «стовпець → функції».groupbyпрацює за схемою split — apply — combine; ключ стає індексом результату (as_index=Falseабоreset_index()повертають його в стовпці).- Кілька ключів дають
MultiIndex;unstackпереносить рівень у стовпці,stack— назад. - Іменовані агрегати
.agg(name=("col", "func"))— найчитабельніший спосіб побудувати звіт. size— рядків у групі,count— непорожніх значень; рядки зNaNу ключі відкидаються, якщо не вказатиdropna=False.transformповертає значення групи в кожному рядку: частки, відхилення від середнього групи, заповнення пропусків по групі.filterзалишає або викидає цілі групи.- Топ-N у групі:
sort_values(...).groupby(...).head(n); у групах працюютьcumsum,cumcount,shift,rank. - Дати групують через
.dt(to_period("M")),resampleабоpd.Grouper; інтервали — черезpd.cutіpd.qcut. pivot_table— зведена таблиця з агрегацією (вказуйтеaggfunc!);crosstab— таблиця частот;pivot/melt— зміна форми без агрегації.mergeз'єднує таблиці за ключем (inner,left,right,outer);indicator=Trueзнаходить «сиріт»,validateзахищає від дублікатів.concatскладає таблиці одна під одною; збирайте їх у список і склеюйте один раз.query— фільтр рядком-виразом зand,or,not,inі@змінними.- Ланцюжки з
assign,pipe,query,groupby,aggчитаються як рецепт і не засмічують код проміжними змінними.
Корисні посилання¶
- Group by: split-apply-combine
- Reshaping and pivot tables
- Merge, join, concatenate and compare
- Comparison with SQL
- Time series / date functionality
DataFrame.querypandas.pivot_tablepandas.merge
Знайшли помилку чи бажаєте додати інформацію, щоб покращити курс? Створіть issue на GitHub