# Домашнє завдання · Pandas: таблиці

> Лекція: [lecture.html](lecture.html) · Практика: [practice.ipynb](practice.ipynb)

Дані для всіх трьох рівнів генеруй **сам**, у тимчасовій теці через
`tempfile.mkdtemp()`, і прибирай її наприкінці через `shutil.rmtree()`.
Нічого не завантажуй з мережі: усе, що потрібно, ти вмієш записати у файл
із [теми 20](../20-files-pathlib/lecture.html).

---

## 🟢 Рівень 1 — База

Придумай свою таблицю на 12-15 рядків: щось, що ти справді бачив у житті —
витрати за тиждень, оцінки з предметів, тренування, покупки. Обовʼязково
мусять бути **щонайменше один текстовий стовпець** (для групування)
і **два числові**.

Далі:

1. збери її як `DataFrame` зі словника й запиши у CSV із `to_csv(..., index=False)`;
2. прочитай назад через `read_csv` і покажи `head()`, `shape`, `info()`, `describe()`;
3. додай новий стовпець, порахований з двох наявних;
4. відфільтруй рядки однією умовою, потім — двома через `&`;
5. згрупуй за текстовим стовпцем і порахуй `sum` та `mean` числового.

**Зроблено, якщо:** зошит виконується наскрізь без правок; є `assert`, який
перевіряє, що таблиця з файлу збігається з початковою
(`прочитана.equals(початкова)`); є `assert`, який перевіряє, що сума значень
по групах дорівнює сумі по всій таблиці; тимчасова тека наприкінці видалена
(`assert not тека.exists()`).

---

## 🟡 Рівень 2 — Плюс

Візьми свою таблицю з рівня 1 і **зіпсуй** її так, як псують дані в житті:

- вибий 3-4 значення в числових стовпцях (постав `np.nan`);
- в одному рядку напиши категорію з великої літери, в іншому — з малої,
  у третьому — з пробілом у кінці («Кава», «кава», «кава ») ;
- запиши файл із роздільником `;` і десятковою комою, як це робить
  український Excel.

Тепер полагодь:

1. прочитай файл із правильним діалектом і покажи, що станеться, якщо про
   діалект не сказати (порівняй `shape`);
2. знайди всі пропуски через `isna().sum()`;
3. порахуй середнє числового стовпця **трьома способами** — як є,
   після `dropna()` і після `fillna(0)` — і поясни словами, чому числа різні
   і яке з них ти вважаєш чесним;
4. почисть категорії через `.str.strip().str.capitalize()` і покажи
   `value_counts()` до й після.

**Зроблено, якщо:** у зошиті є три різні числа середнього з підписом, звідки
кожне взялося; `value_counts()` до чищення дає більше категорій, ніж після,
і це підтверджено `assert`-ом; є речення про те, чому `fillna(0)` у твоїх
даних доречний або недоречний — з посиланням на зміст стовпця, а не на код.

---

## 🔴 Рівень 3 — Виклик

Напиши власну функцію `моє_groupby(таблиця, ключ, стовпець, дія)`, яка
повторює `таблиця.groupby(ключ)[стовпець].agg(дія)`, але **не користується
`groupby`**: тільки цикл, словник і список. Підтримай три дії:
`"sum"`, `"mean"`, `"count"`.

Умови:

- функція повертає `pd.Series` з тим самим індексом і в тому самому порядку,
  що й pandas (нагадаємо: `groupby` сортує ключі);
- пропуски вона має ігнорувати так само, як згортки pandas: `NaN` не входить
  ані в суму, ані в лічильник для `mean`;
- порівняння з бібліотечним результатом робиться через
  `pd.testing.assert_series_equal`, а не через `==`.

Потім заміряй час обох варіантів на таблиці з 200 000 рядків
(`np.random.default_rng(42)` для генерації) і запиши висновок одним реченням:
у скільки разів твоя версія повільніша й **чому** — що саме робить pandas,
чого не робить твій цикл.

**Зроблено, якщо:** `assert_series_equal` проходить для всіх трьох дій і на
чистій таблиці, і на таблиці з пропусками; заміряний час обох варіантів
надрукований; у висновку названа конкретна причина різниці (не «pandas
швидший», а «згортка виконується всередині C над суцільним масивом, а мій
цикл створює пітонівський обʼєкт на кожне значення»).

---

## Підказки

- `dropna()` без аргументів і `dropna(subset=[...])` — це два різні рішення.
  Перше викидає рядок, де порожня будь-яка клітинка; друге — лише там, де
  бракує саме потрібного стовпця. На рівні 2 різниця між ними і є половиною
  відповіді.
- Індекс після фільтра перестає збігатися з позиціями. Якщо в рівні 3 твоя
  функція збирає результат у список, а потім будує `Series`, — не забудь
  передати `index=` явно, інакше отримаєш `RangeIndex` і `assert_series_equal`
  скаже про це першим.
- `.str.strip()` і `.str.capitalize()` працюють одразу над усім стовпцем,
  циклу не треба. Це те саме, що методи рядків із
  [теми 05](../05-strings/lecture.html), тільки застосовані векторно.
- Для заміру часу вистачить `time.perf_counter()` навколо виклику. Одного
  прогону досить: різниця там така, що її не сплутаєш із шумом.
