# Домашнє завдання · NumPy: масиви

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

Дані тут інші, ніж у лекції, — навмисно: якщо та сама механіка працює на чужій задачі,
значить, справа не в завченому прикладі. Працюємо з **показаннями метеостанцій**:
таблиця «станція × година», у комірці температура в градусах. Усе роби в окремому файлі
`pogoda.py` або в новому зошиті — практику не редагуй, вона знадобиться для звірки.

Дані генеруй так і ніяк інакше, щоб числа збігались із критеріями нижче:

```python
import numpy as np

генератор = np.random.default_rng(2024)
температури = генератор.normal(12.0, 6.0, size=(6, 24)).round(1)   # 6 станцій × 24 години
станції = ["Лиман", "Гора", "Поле", "Ліс", "Берег", "Місто"]
```

---

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

Роздивись масив і навчись діставати з нього шматки за координатами.

1. Надрукуй паспорт масиву: `shape`, `ndim`, `size`, `dtype`, `itemsize`, `nbytes` —
   кожне з підписом, що воно означає.
2. Порахуй, скільки памʼяті займав би той самий набір чисел у вигляді вкладеного
   списку. Заміряй чесно через `tracemalloc`, а не на око.
3. Дістань і надрукуй з підписами: температуру на станції «Поле» о 15-й годині;
   увесь день станції «Берег»; 8-му годину на всіх станціях; прямокутник
   «станції 1-3, години 6-12».
4. Переведи весь масив у Фаренгейти однією векторною дією
   (`градуси * 9 / 5 + 32`) — без жодного циклу.

**Зроблено, якщо:**

- проходить `assert температури.shape == (6, 24) and температури.size == 144`;
- проходить `assert температури.nbytes == температури.size * температури.itemsize`;
- проходить `assert температури[2, 15].round(1) == температури[2][15].round(1)` —
  і поруч коментар, чому обидва записи дають те саме, але перший кращий;
- проходить `assert температури[1:4, 6:12].shape == (3, 6)`;
- проходить `assert np.allclose(у_фаренгейтах, [[г * 9 / 5 + 32 for г in рядок] for рядок in температури])`
  — тобто векторна дія дала те саме, що ручне включення;
- у файлі надруковано два числа памʼяті (масив і список) і в скільки разів вони різняться.

---

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

Тепер підсумки, осі, транслювання й маски — і жодного `for` по елементах.

1. Порахуй середню температуру **по станціях** і **по годинах**. Надрукуй обидва
   масиви з підписами й поясни в коментарі, чому в одному випадку `axis=1`, а в
   іншому `axis=0`.
2. Знайди для кожної станції її найтеплішу годину — через `argmax` з віссю, а не циклом.
3. Побудуй масив `аномалії` = наскільки кожне вимірювання відхиляється від середнього
   **по своїй станції**. Скористайся `keepdims=True`.
4. Побудуй масив `холодні` — маску «нижче за нуль». Порахуй, скільки таких вимірювань
   усього і скільки їх на кожній станції.
5. Одним рядком зроби `безпечні` — копію масиву, у якій усе нижче за нуль замінено на 0,
   а решта лишилась як була. Оригінал при цьому має вціліти.

**Зроблено, якщо:**

- проходить `assert середнє_по_станціях.shape == (6,) and середнє_по_годинах.shape == (24,)`;
- проходить `assert np.allclose(середнє_по_станціях.mean(), температури.mean())` —
  і поруч коментар, чому це працює саме тут і зламалося б, якби рядки мали різну довжину;
- проходить `assert np.allclose(аномалії.sum(axis=1), 0.0)` — сума відхилень від
  власного середнього завжди нуль;
- проходить `assert холодні.sum() == сума_вручну`, де `сума_вручну` порахована
  звичайним вкладеним циклом;
- проходить `assert безпечні.min() >= 0 and температури.min() < 0` — тобто оригінал
  не постраждав;
- у файлі немає жодного `for` по елементах масиву (цикли по назвах станцій для друку — можна).

---

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

Досліди межі: де вигляд кусає, де транслювання падає й де векторизація перестає вигравати.

1. Напиши дві функції з однаковою логікою — «обнулити першу годину доби на всіх станціях»:
   `зіпсувати(масив)`, яка пише прямо у зріз переданого масиву, і `безпечно(масив)`,
   яка починає з `дані = масив.copy()`. Доведи `assert`-ами: після `безпечно` оригінал
   цілий, після `зіпсувати` — ні. Другий `assert` спіймай через
   `try/except AssertionError`, щоб файл усе одно дорахувався до кінця.
2. Збери **таблицю поєднань форм**: для пар `(6, 24)` з `(24,)`, `(6,)`, `(6, 1)`,
   `(1, 24)`, `(24, 1)`, а також окремо для пари `(6, 1)` з `(1, 24)` спробуй додавання
   й запиши в таблицю або форму результату, або текст помилки. Кожен рядок поясни
   одним реченням: яка вісь із якою зіткнулась.
3. Порівняй швидкість на трьох розмірах — 1 000, 100 000 і 5 000 000 елементів — для
   трьох способів однієї дії `× 9 / 5 + 32`: цикл `for` зі списком, спискове включення,
   векторна дія. Виміряй `timeit.repeat(..., repeat=7)` і бери мінімум. Побудуй
   табличку з трьох рядків і чотирьох стовпців.
4. Знайди в цій табличці розмір, на якому **пришвидшення падає** порівняно з попереднім
   рядком, і поясни причину числом: порахуй `nbytes` масиву на кожному розмірі й
   назви, з якого моменту дані перестали вміщуватись у кеш процесора.
5. Порахуй ту саму таблицю аномалій із рівня 2 у трьох типах: `float64`, `float32` і
   `float16`. Для кожного надрукуй `nbytes` і максимальне відхилення від
   `float64`-версії. Зроби висновок одним реченням: де економія памʼяті виправдана,
   а де вже ні.

**Зроблено, якщо:**

- `assert np.array_equal(до, після)` після `безпечно` проходить, а такий самий `assert`
  після `зіпсувати` падає й це падіння спіймано та надруковано;
- у таблиці з пункту 2 є щонайменше одна форма, яка **падає**, і одна, де
  розтягуються **обидва** масиви;
- у табличці з пункту 3 всі дванадцять чисел заповнені, і найшвидший стовпець — векторний;
- названо конкретний розмір із пункту 4 і поруч стоїть його `nbytes` у мегабайтах;
- у пункті 5 надруковано три значення `nbytes` і три максимальні відхилення, і
  відхилення `float16` виявилось на кілька порядків більшим за відхилення `float32`.

---

## Підказки

- **Рівень 1, пункт 2.** `sys.getsizeof` для списку покаже лише масив посилань і
  збреше в рази. Чесну відповідь дає `tracemalloc.start()` / `get_traced_memory()`
  навколо самої побудови списку — і не забудь `del` між двома замірами, інакше
  другий побачить памʼять, яку тримає перший.
- **Рівень 2, пункт 1.** Не намагайся запамʼятати «станції — це нуль». Дивись на
  форму: тобі потрібні шість чисел, отже, з `(6, 24)` має зникнути двадцять чотири.
- **Рівень 2, пункт 5.** Потрібна функція, яка не викидає елементи, а замінює їх,
  зберігаючи форму. Вона згадана в розділі 09 лекції.
- **Рівень 3, пункт 1.** Різниця між функціями — рівно один рядок. Саме тому цю
  помилку так важко помітити в чужому коді: він виглядає бездоганно.
- **Рівень 3, пункт 2.** Форми порівнюються **з кінця**. Якщо здається, що
  `(6, 24)` і `(6,)` мають поєднатись, бо шістка є в обох, — випиши обидві форми
  вирівняними праворуч і подивись, що з чим насправді стикається.
- **Рівень 3, пункт 4.** Кеш останнього рівня у звичайного ноутбука — одиниці
  мегабайтів. Шукай розмір, на якому `nbytes` цю межу перетинає; далі час починає
  зʼїдати не арифметика, а перевезення байтів.
- **Рівень 3, пункт 5.** У `float16` на мантису лишається десять бітів. Це приблизно
  три десяткові знаки на все число — для температури 12.3 ще терпимо, для суми
  тисячі таких чисел уже ні.
