Code
import pandas as pd
import numpy as np
# Для відображення всіх колонок/рядків (регулюйте за потребою)
pd.set_option('display.max_columns', None)
pd.set_option('display.max_rows', 30)loc і ilocgroupby, agg
pivot_table, meltmerge, concat, joinЛекція з практичними прикладами на Python
Богдан Красюк
20 жовтня 2025 р.
pandas — це бібліотека Python для високопродуктивної роботи з табличними даними в оперативній пам’яті. Вона надає два ключові контейнери даних: Series (1D стовпчик) та DataFrame (2D таблиця), з індексами, типами даних, векторизованими операціями та зручними методами статистичного аналізу. Практично це «mini-таблична БД у Python» з можливостями фільтрації, групування, злиття, перетворення форми даних, роботи з датами/часом тощо.
💡 Коли брати pandas? Коли дані вміщуються у пам’ять однієї машини (десятки–сотні МБ, інколи ГБ), коли потрібна швидка розвідка даних (EDA), побудова звітів, підготовка фіч для ML або заміна повторюваних дій в Excel на відтворюваний код.
У прикладах нижче будемо використовувати файл з вашого проєкту: data/input_data.csv.
| Age | Attrition | BusinessTravel | DailyRate | Department | DistanceFromHome | Education | EducationField | EmployeeCount | EmployeeNumber | EnvironmentSatisfaction | Gender | HourlyRate | JobInvolvement | JobLevel | JobRole | JobSatisfaction | MaritalStatus | MonthlyIncome | MonthlyRate | NumCompaniesWorked | Over18 | OverTime | PercentSalaryHike | PerformanceRating | RelationshipSatisfaction | StandardHours | StockOptionLevel | TotalWorkingYears | TrainingTimesLastYear | WorkLifeBalance | YearsAtCompany | YearsInCurrentRole | YearsSinceLastPromotion | YearsWithCurrManager | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 41 | Yes | Travel_Rarely | 1102 | Sales | 1 | 2 | Life Sciences | 1 | 1 | 2 | Female | 94 | 3 | 2 | Sales Executive | 4 | Single | 5993 | 19479 | 8 | Y | Yes | 11 | 3 | 1 | 80 | 0 | 8 | 0 | 1 | 6 | 4 | 0 | 5 |
| 1 | 49 | No | Travel_Frequently | 279 | Research & Development | 8 | 1 | Life Sciences | 1 | 2 | 3 | Male | 61 | 2 | 2 | Research Scientist | 2 | Married | 5130 | 24907 | 1 | Y | No | 23 | 4 | 4 | 80 | 1 | 10 | 3 | 3 | 10 | 7 | 1 | 7 |
| 2 | 37 | Yes | Travel_Rarely | 1373 | Research & Development | 2 | 2 | Other | 1 | 4 | 4 | Male | 92 | 2 | 1 | Laboratory Technician | 3 | Single | 2090 | 2396 | 6 | Y | Yes | 15 | 3 | 2 | 80 | 0 | 7 | 3 | 3 | 0 | 0 | 0 | 0 |
| 3 | 33 | No | Travel_Frequently | 1392 | Research & Development | 3 | 4 | Life Sciences | 1 | 5 | 4 | Female | 56 | 3 | 1 | Research Scientist | 3 | Married | 2909 | 23159 | 1 | Y | Yes | 11 | 3 | 3 | 80 | 0 | 8 | 3 | 3 | 8 | 7 | 3 | 0 |
| 4 | 27 | No | Travel_Rarely | 591 | Research & Development | 2 | 1 | Medical | 1 | 7 | 1 | Male | 40 | 3 | 1 | Laboratory Technician | 2 | Married | 3468 | 16632 | 9 | Y | No | 12 | 3 | 4 | 80 | 1 | 6 | 3 | 3 | 2 | 2 | 2 | 2 |
((1470, 35),
Age int64
Attrition object
BusinessTravel object
DailyRate int64
Department object
...
WorkLifeBalance int64
YearsAtCompany int64
YearsInCurrentRole int64
YearsSinceLastPromotion int64
YearsWithCurrManager int64
Length: 35, dtype: object)
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1470 entries, 0 to 1469
Data columns (total 35 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Age 1470 non-null int64
1 Attrition 1470 non-null object
2 BusinessTravel 1470 non-null object
3 DailyRate 1470 non-null int64
4 Department 1470 non-null object
5 DistanceFromHome 1470 non-null int64
6 Education 1470 non-null int64
7 EducationField 1470 non-null object
8 EmployeeCount 1470 non-null int64
9 EmployeeNumber 1470 non-null int64
10 EnvironmentSatisfaction 1470 non-null int64
11 Gender 1470 non-null object
12 HourlyRate 1470 non-null int64
13 JobInvolvement 1470 non-null int64
14 JobLevel 1470 non-null int64
15 JobRole 1470 non-null object
16 JobSatisfaction 1470 non-null int64
17 MaritalStatus 1470 non-null object
18 MonthlyIncome 1470 non-null int64
19 MonthlyRate 1470 non-null int64
20 NumCompaniesWorked 1470 non-null int64
21 Over18 1470 non-null object
22 OverTime 1470 non-null object
23 PercentSalaryHike 1470 non-null int64
24 PerformanceRating 1470 non-null int64
25 RelationshipSatisfaction 1470 non-null int64
26 StandardHours 1470 non-null int64
27 StockOptionLevel 1470 non-null int64
28 TotalWorkingYears 1470 non-null int64
29 TrainingTimesLastYear 1470 non-null int64
30 WorkLifeBalance 1470 non-null int64
31 YearsAtCompany 1470 non-null int64
32 YearsInCurrentRole 1470 non-null int64
33 YearsSinceLastPromotion 1470 non-null int64
34 YearsWithCurrManager 1470 non-null int64
dtypes: int64(26), object(9)
memory usage: 402.1+ KB
| count | unique | top | freq | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Age | 1470.0 | NaN | NaN | NaN | 36.92381 | 9.135373 | 18.0 | 30.0 | 36.0 | 43.0 | 60.0 |
| Attrition | 1470 | 2 | No | 1233 | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| BusinessTravel | 1470 | 3 | Travel_Rarely | 1043 | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| DailyRate | 1470.0 | NaN | NaN | NaN | 802.485714 | 403.5091 | 102.0 | 465.0 | 802.0 | 1157.0 | 1499.0 |
| Department | 1470 | 3 | Research & Development | 961 | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| WorkLifeBalance | 1470.0 | NaN | NaN | NaN | 2.761224 | 0.706476 | 1.0 | 2.0 | 3.0 | 3.0 | 4.0 |
| YearsAtCompany | 1470.0 | NaN | NaN | NaN | 7.008163 | 6.126525 | 0.0 | 3.0 | 5.0 | 9.0 | 40.0 |
| YearsInCurrentRole | 1470.0 | NaN | NaN | NaN | 4.229252 | 3.623137 | 0.0 | 2.0 | 3.0 | 7.0 | 18.0 |
| YearsSinceLastPromotion | 1470.0 | NaN | NaN | NaN | 2.187755 | 3.22243 | 0.0 | 0.0 | 1.0 | 3.0 | 15.0 |
| YearsWithCurrManager | 1470.0 | NaN | NaN | NaN | 4.123129 | 3.568136 | 0.0 | 2.0 | 3.0 | 7.0 | 17.0 |
35 rows × 11 columns
loc і ilocdf.loc[рядки, колонки] — вибірка за мітками індексу/назвами колонок.df.iloc[рядки, колонки] — вибірка за позиціями (інтегральними індексами).| Age | Attrition | BusinessTravel | |
|---|---|---|---|
| 0 | 41 | Yes | Travel_Rarely |
| 1 | 49 | No | Travel_Frequently |
| 2 | 37 | Yes | Travel_Rarely |
| 3 | 33 | No | Travel_Frequently |
| 4 | 27 | No | Travel_Rarely |
⚠️ Рекомендація: використовуйте
loc/iloc, а не «коротку» індексацію[], щоб уникати плутанини між позиціями та мітками.
df.fillna(0) або df['col'].fillna('N/A')df.dropna() або з порогом заповнення thresh=| Age | Attrition | BusinessTravel | DailyRate | Department | DistanceFromHome | Education | EducationField | EmployeeCount | EmployeeNumber | EnvironmentSatisfaction | Gender | HourlyRate | JobInvolvement | JobLevel | JobRole | JobSatisfaction | MaritalStatus | MonthlyIncome | MonthlyRate | NumCompaniesWorked | Over18 | OverTime | PercentSalaryHike | PerformanceRating | RelationshipSatisfaction | StandardHours | StockOptionLevel | TotalWorkingYears | TrainingTimesLastYear | WorkLifeBalance | YearsAtCompany | YearsInCurrentRole | YearsSinceLastPromotion | YearsWithCurrManager | Age_ratio | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 41 | Yes | Travel_Rarely | 1102 | Sales | 1 | 2 | Life Sciences | 1 | 1 | 2 | Female | 94 | 3 | 2 | Sales Executive | 4 | Single | 5993 | 19479 | 8 | Y | Yes | 11 | 3 | 1 | 80 | 0 | 8 | 0 | 1 | 6 | 4 | 0 | 5 | 0.683333 |
| 1 | 49 | No | Travel_Frequently | 279 | Research & Development | 8 | 1 | Life Sciences | 1 | 2 | 3 | Male | 61 | 2 | 2 | Research Scientist | 2 | Married | 5130 | 24907 | 1 | Y | No | 23 | 4 | 4 | 80 | 1 | 10 | 3 | 3 | 10 | 7 | 1 | 7 | 0.816667 |
| 2 | 37 | Yes | Travel_Rarely | 1373 | Research & Development | 2 | 2 | Other | 1 | 4 | 4 | Male | 92 | 2 | 1 | Laboratory Technician | 3 | Single | 2090 | 2396 | 6 | Y | Yes | 15 | 3 | 2 | 80 | 0 | 7 | 3 | 3 | 0 | 0 | 0 | 0 | 0.616667 |
| 3 | 33 | No | Travel_Frequently | 1392 | Research & Development | 3 | 4 | Life Sciences | 1 | 5 | 4 | Female | 56 | 3 | 1 | Research Scientist | 3 | Married | 2909 | 23159 | 1 | Y | Yes | 11 | 3 | 3 | 80 | 0 | 8 | 3 | 3 | 8 | 7 | 3 | 0 | 0.550000 |
| 4 | 27 | No | Travel_Rarely | 591 | Research & Development | 2 | 1 | Medical | 1 | 7 | 1 | Male | 40 | 3 | 1 | Laboratory Technician | 2 | Married | 3468 | 16632 | 9 | Y | No | 12 | 3 | 4 | 80 | 1 | 6 | 3 | 3 | 2 | 2 | 2 | 2 | 0.450000 |
💬 Порада. Вибір стратегії залежить від контексту й типу моделі. Для деревоподібних алгоритмів інколи краще залишити пропуски або використовувати спеціальний маркер; для регресійних — частіше спрацьовують статистики.
groupby, aggГрупування реалізує шаблон split → apply → combine: розбили за категоріями, застосували функції, об’єднали назад.
# Знайдемо першу категоріальну колонку і порахуємо агрегати для першої числової
cat_cols = df_imputed.select_dtypes(include=['object', 'category', 'bool']).columns.tolist()
num_cols = df_imputed.select_dtypes(include='number').columns.tolist()
if cat_cols and num_cols:
gcol, vcol = cat_cols[0], num_cols[0]
grouped = (
df_imputed.groupby(gcol)[vcol]
.agg(['count', 'mean', 'median', 'min', 'max'])
.sort_values('mean', ascending=False)
)
grouped.head(10)
else:
df_imputed.head(3)pivot_table, melt# Приклад: зведена таблиця (якщо є категоріальна та числова колонки)
if cat_cols and num_cols:
idx, col, val = cat_cols[0], (cat_cols[1] if len(cat_cols) > 1 else cat_cols[0]), num_cols[0]
pivot = pd.pivot_table(df_imputed, index=idx, columns=col, values=val, aggfunc='mean')
pivot.head()
else:
df_imputed.head(3)merge, concat, join# Штучний приклад: створимо довідник унікальних категорій та "джойнимо" його
if cat_cols:
gcol = cat_cols[0]
lookup = pd.DataFrame({gcol: df_imputed[gcol].dropna().unique()})
lookup['is_special'] = np.random.choice([0, 1], size=len(lookup), p=[0.7, 0.3])
merged = df_imputed.merge(lookup, on=gcol, how='left')
merged[[gcol, 'is_special']].head()
else:
df_imputed.head(3)df2 = df.copy() перед ризикованими перетвореннями.np.random.seed(42).data/processed/.---
title: "Основи роботи з pandas для аналізу даних"
subtitle: "Лекція з практичними прикладами на Python"
author: "Богдан Красюк"
date: "2025-10-20"
lang: uk
categories: ["Лекції", "Аналітика даних"]
format:
html:
toc: true
toc-location: right
math: mathjax
toc-title: "План лекції"
toc-depth: 3
number-sections: true
code-fold: show
code-tools: true
smooth-scroll: true
execute:
echo: true
warning: false
message: false
---
## Презентація
<embed src="pdf/pandas.pdf" width="100%" height="400px" type="application/pdf">
> 🐼 **Мета лекції.** Навчитися базовим прийомам роботи з бібліотекою **pandas** для завантаження, огляду, очищення та агрегування табличних даних, а також підготовки датасетів до візуалізації й моделювання.
## Вступ: що таке pandas і коли її використовувати?
**pandas** — це бібліотека Python для високопродуктивної роботи з табличними даними в оперативній пам’яті. Вона надає два ключові контейнери даних: **Series** (1D стовпчик) та **DataFrame** (2D таблиця), з індексами, типами даних, векторизованими операціями та зручними методами статистичного аналізу. Практично це «mini-таблична БД у Python» з можливостями фільтрації, групування, злиття, перетворення форми даних, роботи з датами/часом тощо.
> 💡 **Коли брати pandas?** Коли дані вміщуються у пам’ять однієї машини (десятки–сотні МБ, інколи ГБ), коли потрібна швидка розвідка даних (EDA), побудова звітів, підготовка фіч для ML або заміна повторюваних дій в Excel на відтворюваний код.
## Підготовка середовища
```{python}
import pandas as pd
import numpy as np
# Для відображення всіх колонок/рядків (регулюйте за потребою)
pd.set_option('display.max_columns', None)
pd.set_option('display.max_rows', 30)
```
## Завантаження даних з CSV
У прикладах нижче будемо використовувати файл з вашого проєкту: **`data/input_data.csv`**.
```{python}
from pathlib import Path
DATA_PATH = Path("data/input_data.csv")
assert DATA_PATH.exists(), f"Не знайдено файл {DATA_PATH.resolve()}"
df = pd.read_csv(DATA_PATH)
df.head()
```
### Перевірка розміру та типів даних
```{python}
df.shape, df.dtypes
```
```{python}
df.info()
```
```{python}
df.describe(include='all').T
```
## Індексація та вибірка: `loc` і `iloc`
- `df.loc[рядки, колонки]` — вибірка **за мітками** індексу/назвами колонок.
- `df.iloc[рядки, колонки]` — вибірка **за позиціями** (інтегральними індексами).
```{python}
# Перші 5 рядків, кілька перших колонок (позиційно)
df.iloc[:5, :3]
```
```{python}
# Якщо у вас є колонка 'id' або інша назва — покажемо адресний вибір
cols = df.columns.tolist()
some_col = cols[0] if cols else None
if some_col is not None:
df.loc[:4, [some_col]]
else:
df.head(3)
```
> ⚠️ Рекомендація: використовуйте `loc/iloc`, а не «коротку» індексацію `[]`, щоб уникати плутанини між позиціями та мітками.
## Базові трансформації: фільтрація, сортування, нові колонки
### Фільтрація
```{python}
# Приклад: автоматично оберемо першу числову колонку і відфільтруємо за медіаною
num_cols = df.select_dtypes(include='number').columns.tolist()
if num_cols:
col = num_cols[0]
median_val = df[col].median()
filtered = df[df[col] > median_val]
filtered[[col]].head()
else:
df.head(3)
```
### Сортування
```{python}
if num_cols:
df.sort_values(num_cols[0], ascending=False).head()
else:
df.head(3)
```
### Нові колонки
```{python}
# Створимо приклад фічі: частка від максимуму в обраній числовій колонці
if num_cols:
col = num_cols[0]
df[f"{col}_ratio"] = df[col] / df[col].max()
df[[col, f"{col}_ratio"]].head()
else:
df.head(3)
```
## Робота з пропусками (NaN)
### Діагностика пропусків
```{python}
missing_by_col = df.isna().sum().sort_values(ascending=False)
missing_total = int(df.isna().sum().sum())
missing_by_col.head(), missing_total
```
### Стратегії заповнення/видалення
- **Заповнення константою**: `df.fillna(0)` або `df['col'].fillna('N/A')`
- **Заповнення статистикою**: середнє/медіана/мода
- **Видалення**: `df.dropna()` або з порогом заповнення `thresh=`
- **Індикатор відсутності**: додатковий бінарний стовпчик для MNAR-випадків
```{python}
# Демонстрація: оберемо числові колонки та заповнимо їх медіаною
num_cols = df.select_dtypes(include='number').columns
df_imputed = df.copy()
df_imputed[num_cols] = df_imputed[num_cols].apply(lambda s: s.fillna(s.median()))
df_imputed.head()
```
```{python}
# Приклад індикатора пропуску для однієї колонки (якщо є пропуски)
if num_cols.size:
col = num_cols[0]
df_imputed[f"{col}_was_na"] = df[col].isna().astype(int)
df_imputed[[col, f"{col}_was_na"]].head()
else:
df_imputed.head(3)
```
> 💬 **Порада.** Вибір стратегії залежить від контексту й типу моделі. Для деревоподібних алгоритмів інколи краще залишити пропуски або використовувати спеціальний маркер; для регресійних — частіше спрацьовують статистики.
## Групування та агрегування: `groupby`, `agg`
Групування реалізує шаблон **split → apply → combine**: розбили за категоріями, застосували функції, об’єднали назад.
```{python}
# Знайдемо першу категоріальну колонку і порахуємо агрегати для першої числової
cat_cols = df_imputed.select_dtypes(include=['object', 'category', 'bool']).columns.tolist()
num_cols = df_imputed.select_dtypes(include='number').columns.tolist()
if cat_cols and num_cols:
gcol, vcol = cat_cols[0], num_cols[0]
grouped = (
df_imputed.groupby(gcol)[vcol]
.agg(['count', 'mean', 'median', 'min', 'max'])
.sort_values('mean', ascending=False)
)
grouped.head(10)
else:
df_imputed.head(3)
```
### Комплексні агрегації кількома функціями
```{python}
if cat_cols and num_cols:
gcol = cat_cols[0]
agg_dict = {c: ['count', 'mean', 'median'] for c in num_cols[:3]} # приклад
multi = df_imputed.groupby(gcol).agg(agg_dict)
multi.head()
else:
df_imputed.head(3)
```
## Перетворення форми даних: `pivot_table`, `melt`
```{python}
# Приклад: зведена таблиця (якщо є категоріальна та числова колонки)
if cat_cols and num_cols:
idx, col, val = cat_cols[0], (cat_cols[1] if len(cat_cols) > 1 else cat_cols[0]), num_cols[0]
pivot = pd.pivot_table(df_imputed, index=idx, columns=col, values=val, aggfunc='mean')
pivot.head()
else:
df_imputed.head(3)
```
```{python}
# "Розплавлення" широкого формату у довгий
melt_cols = num_cols[:3] if len(num_cols) >= 2 else num_cols
if len(melt_cols) >= 2:
melted = df_imputed.melt(value_vars=melt_cols, var_name='feature', value_name='value')
melted.head()
else:
df_imputed.head(3)
```
## Об'єднання таблиць: `merge`, `concat`, `join`
```{python}
# Штучний приклад: створимо довідник унікальних категорій та "джойнимо" його
if cat_cols:
gcol = cat_cols[0]
lookup = pd.DataFrame({gcol: df_imputed[gcol].dropna().unique()})
lookup['is_special'] = np.random.choice([0, 1], size=len(lookup), p=[0.7, 0.3])
merged = df_imputed.merge(lookup, on=gcol, how='left')
merged[[gcol, 'is_special']].head()
else:
df_imputed.head(3)
```
## Збереження результатів
```{python}
OUT_DIR = Path("data")
OUT_DIR.mkdir(parents=True, exist_ok=True)
df_imputed.to_csv(OUT_DIR / "input_data_imputed.csv", index=False)
"Збережено до data/input_data_imputed.csv"
```
## Міні‑практикум 🧪
1. Знайдіть 3–5 ключових числових показників (mean/median/min/max/std) для вашого домену.
2. Оберіть категоріальну колонку й побудуйте *топ‑10* груп за середнім значенням метрики.
3. Створіть 2–3 прості нові фічі (відношення, різниця, лог‑перетворення) й порівняйте їхню кореляцію з цільовою.
4. Підготуйте чистий датасет без пропусків і з чіткими типами даних для наступної лекції (візуалізація/ML).
## Корисні патерни та нагадування ✅
- Працюйте з копіями: `df2 = df.copy()` перед ризикованими перетвореннями.
- Фіксуйте випадковість для відтворюваності: `np.random.seed(42)`.
- Зберігайте «сирі» дані незмінними, результати — у власну теку `data/processed/`.
- Додавайте тести перевірки якості даних: діапазони, унікальність ключів, відсоток пропусків.
- Документуйте кроки в коді, щоб інші могли повторити ваш аналіз.
---
### Додаткові ресурси
- Jupyter Notebook з лекції: [Pandas](notebooks/pandas.ipynb)
- Документація pandas: <https://pandas.pydata.org/docs/>
- Pandas Cheat Sheet: <https://pandas.pydata.org/Pandas_Cheat_Sheet.pdf>
- Публічні датасети: <https://www.kaggle.com/datasets>
---
:::: {.columns}
::: {.column width="20%"}
<img src="https://kleban.page/bc-2025/images/logo.png" alt="Лого" style="height: 70px;">
:::
::: {.column width="30%"}
<img src="https://kleban.page/bc-2025/images/eu-founded.png" alt="Лого" style="height: 100px;">
:::
::: {.column width="50%"}
Проєкт реалізується за підтримки **Європейського Союзу** в межах програми [Дім Європи](https://houseofeurope.org.ua/).
:::
::::