Практика · NumPy: масиви¶
Супровід до лекції lecture.html · тест: quiz.html · домашнє: homework.md
Наскрізний приклад той самий, що в лекції — журнал продажів мережі кавʼярень: чотири точки, сім днів, у комірці кількість проданих чашок.
Що зробимо:
- зберемо масив і роздивимось його паспорт:
shape,ndim,size,dtype,nbytes - порівняємо памʼять масиву з памʼяттю списку тих самих чисел
- створимо масиви без списків:
zeros,arange,linspace,default_rng - візьмемо двовимірні зрізи — рядок, стовпець, прямокутник
- спіймаємо пастку вигляду: доведемо
assert-ом, що запис у зріз змінив оригінал - заміряємо цикл проти векторизації власним
timeit - збудуємо таблицю відхилень транслюванням і звіримо її з порахованою вручну
- пройдемось по осях і по булевих масках
Мережею не користуємось, усі випадкові числа — з np.random.default_rng(42).
1 · Готуємо інструмент¶
Перевіримо версії — числа в замірах нижче залежать від них, і твої можуть відрізнятись.
import sys
import timeit
import tracemalloc
import numpy as np
print("Python:", sys.version.split()[0])
print("NumPy: ", np.__version__)
Python: 3.12.2 NumPy: 2.5.1
2 · Журнал продажів¶
Той самий масив, що в лекції. Рядок — магазин, стовпець — день тижня.
Списки магазини й дні тримаємо окремо: масив про підписи не знає нічого
(саме цю дірку залатає наступна тема про pandas).
магазини = ["Центр", "Вокзал", "Кампус", "Парк"]
дні = ["пн", "вт", "ср", "чт", "пт", "сб", "нд"]
продажі = np.array([
[180, 195, 175, 205, 240, 120, 90], # Центр — офіси, вихідні провальні
[140, 135, 150, 145, 160, 155, 130], # Вокзал — рівно щодня
[210, 230, 225, 240, 190, 40, 25], # Кампус — на вихідних пусто
[ 60, 55, 70, 65, 110, 240, 260], # Парк — живе саме на вихідних
])
print(продажі)
[[180 195 175 205 240 120 90] [140 135 150 145 160 155 130] [210 230 225 240 190 40 25] [ 60 55 70 65 110 240 260]]
Паспорт масиву¶
Чотири головні числа плюс два про памʼять. Зверни увагу: nbytes — це рівно
size × itemsize, тобто самі дані без службового заголовка.
print("shape :", продажі.shape, "— рядків × стовпців")
print("ndim :", продажі.ndim, "— вимірів")
print("size :", продажі.size, "— елементів усього")
print("dtype :", продажі.dtype, "— тип КОЖНОГО елемента")
print("itemsize:", продажі.itemsize, "Б — на один елемент")
print("nbytes :", продажі.nbytes, "Б — на весь масив")
# перевіряємо тотожність, про яку йшлося в лекції
assert продажі.nbytes == продажі.size * продажі.itemsize, "nbytes рахується інакше?"
assert продажі.size == продажі.shape[0] * продажі.shape[1], "size — це добуток shape"
print("✅ nbytes = size × itemsize, size = добуток shape")
shape : (4, 7) — рядків × стовпців ndim : 2 — вимірів size : 28 — елементів усього dtype : int64 — тип КОЖНОГО елемента itemsize: 8 Б — на один елемент nbytes : 224 Б — на весь масив ✅ nbytes = size × itemsize, size = добуток shape
3 · Скільки насправді коштує список¶
Порівняємо памʼять чесно — не sys.getsizeof (він бачить лише масив посилань),
а tracemalloc, який рахує ще й мільйон обʼєктів-чисел за цими посиланнями.
СКІЛЬКИ = 1_000_000
tracemalloc.start()
числа_списком = [номер for номер in range(СКІЛЬКИ)]
_, пік_списку = tracemalloc.get_traced_memory()
tracemalloc.stop()
del числа_списком # звільняємо памʼять перед другим заміром
tracemalloc.start()
числа_масивом = np.arange(СКІЛЬКИ)
_, пік_масиву = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"список : {пік_списку:>10,} Б = {пік_списку/2**20:6.2f} МБ")
print(f"масив : {пік_масиву:>10,} Б = {пік_масиву/2**20:6.2f} МБ")
print(f"різниця : у {пік_списку/пік_масиву:.1f} раза")
print()
print("nbytes масиву:", числа_масивом.nbytes, "Б — рівно 8 байтів на число, без залишку")
список : 40,460,584 Б = 38.59 МБ масив : 8,019,326 Б = 7.65 МБ різниця : у 5.0 раза nbytes масиву: 8000000 Б — рівно 8 байтів на число, без залишку
4 · Шість способів створити масив¶
Головне тут: усе, крім np.array, будує масив без проміжного списку —
одразу потрібного розміру.
print("zeros(5) :", np.zeros(5))
print("ones((2,3), int64):", np.ones((2, 3), dtype=np.int64).tolist())
print("full((2,2), 7) :", np.full((2, 2), 7).tolist())
print("arange(0, 10, 2) :", np.arange(0, 10, 2), "— крок 2, права межа НЕ входить")
print("linspace(0, 1, 5) :", np.linspace(0, 1, 5), "— 5 точок, права межа входить")
# arange із дробовим кроком — джерело сюрпризів через похибку з теми 04
дробовий = np.arange(1, 1.3, 0.1)
print()
print("arange(1, 1.3, 0.1) дав", len(дробовий), "числа замість трьох:", дробовий)
print("останнє з них =", repr(дробовий[-1]), "← права межа, якої тут бути не мало")
print("linspace(1, 1.2, 3) натомість дає рівно", len(np.linspace(1, 1.2, 3)), "точки:", np.linspace(1, 1.2, 3))
zeros(5) : [0. 0. 0. 0. 0.] ones((2,3), int64): [[1, 1, 1], [1, 1, 1]] full((2,2), 7) : [[7, 7], [7, 7]] arange(0, 10, 2) : [0 2 4 6 8] — крок 2, права межа НЕ входить linspace(0, 1, 5) : [0. 0.25 0.5 0.75 1. ] — 5 точок, права межа входить arange(1, 1.3, 0.1) дав 4 числа замість трьох: [1. 1.1 1.2 1.3] останнє з них = np.float64(1.3000000000000003) ← права межа, якої тут бути не мало linspace(1, 1.2, 3) натомість дає рівно 3 точки: [1. 1.1 1.2]
Випадкові числа з фіксованим зерном¶
default_rng(42) створює власний генератор: скільки їх зробиш, стільки й буде,
і жоден нічого не знає про інших. Тому результат відтворюється завжди.
генератор = np.random.default_rng(42)
вибірка = генератор.integers(0, 100, size=5)
print("перший генератор :", вибірка)
# новий генератор із тим самим зерном дає ту саму послідовність
повтор = np.random.default_rng(42).integers(0, 100, size=5)
print("другий генератор :", повтор)
assert np.array_equal(вибірка, повтор), "однакове зерно має давати однакові числа!"
print("✅ зерно 42 відтворюється")
перший генератор : [ 8 77 65 43 43] другий генератор : [ 8 77 65 43 43] ✅ зерно 42 відтворюється
5 · Індекс і зріз у двох вимірах¶
Кома розділяє виміри: до неї — рядки, після — стовпці. Двокрапка сама по собі означає «увесь цей вимір».
print("продажі[2, 5] =", продажі[2, 5], "— Кампус у суботу")
print("продажі[0] =", продажі[0], "— увесь тиждень Центру")
print("продажі[:, 5] =", продажі[:, 5], "— субота в усіх магазинах")
print()
print("продажі[1:3, 4:7] — Вокзал і Кампус за пт-нд:")
print(продажі[1:3, 4:7])
print()
print("продажі[:, -2:] — усі магазини, тільки вихідні:")
print(продажі[:, -2:])
продажі[2, 5] = 40 — Кампус у суботу продажі[0] = [180 195 175 205 240 120 90] — увесь тиждень Центру продажі[:, 5] = [120 155 40 240] — субота в усіх магазинах продажі[1:3, 4:7] — Вокзал і Кампус за пт-нд: [[160 155 130] [190 40 25]] продажі[:, -2:] — усі магазини, тільки вихідні: [[120 90] [155 130] [ 40 25] [240 260]]
Перевіримо себе¶
Форма зрізу має відповідати тому, що ми просили. Це найшвидший спосіб зловити
помилку в індексах: не дивитись на числа, а глянути на shape.
assert продажі[2, 5] == 40, "комірка [2,5] — це Кампус у суботу"
assert продажі[0].shape == (7,), "рядок — одновимірний масив із семи чисел"
assert продажі[:, 5].shape == (4,), "стовпець — одновимірний масив із чотирьох чисел"
assert продажі[1:3, 4:7].shape == (2, 3), "прямокутник 2 рядки × 3 стовпці"
print("✅ усі форми зрізів такі, як очікувалось")
✅ усі форми зрізів такі, як очікувалось
тиждень_списком = [180, 195, 175, 205, 240, 120, 90]
частина_списку = тиждень_списком[1:4] # зріз списку — НОВА структура
частина_списку[0] = 0
print("список після запису в зріз:", тиждень_списком)
assert тиждень_списком[1] == 195, "зріз списку не мав зачепити оригінал"
print("✅ оригінал-список цілий — саме так нас навчила тема 06")
список після запису в зріз: [180, 195, 175, 205, 240, 120, 90] ✅ оригінал-список цілий — саме так нас навчила тема 06
А тепер той самий код над масивом. Зверни увагу: рядки коду буквально ті самі, змінився лише тип контейнера.
тиждень = np.array([180, 195, 175, 205, 240, 120, 90])
частина = тиждень[1:4] # зріз масиву — ВИГЛЯД на ту саму памʼять
частина[0] = 0
print("масив після запису в зріз:", тиждень)
# головний assert теми: оригінал ЗМІНИВСЯ, і це не помилка, а нормальна робота NumPy
assert тиждень[1] == 0, "запис у зріз мав змінити оригінал — зріз масиву це вигляд!"
assert np.shares_memory(тиждень, частина), "вигляд і оригінал ділять один блок памʼяті"
print("✅ оригінал змінився — пастку спіймано")
масив після запису в зріз: [180 0 175 205 240 120 90] ✅ оригінал змінився — пастку спіймано
Ліки: .copy()¶
Метод зветься так само, як у списку, але робить принципово інше — виділяє новий блок памʼяті й переносить туди числа.
тиждень_2 = np.array([180, 195, 175, 205, 240, 120, 90])
копія = тиждень_2[1:4].copy() # просимо копію явно
копія[0] = 0
print("масив після запису в КОПІЮ зрізу:", тиждень_2)
assert тиждень_2[1] == 195, ".copy() мав розірвати звʼязок з оригіналом"
assert not np.shares_memory(тиждень_2, копія), "копія не ділить памʼять з оригіналом"
print("✅ з .copy() оригінал цілий")
масив після запису в КОПІЮ зрізу: [180 195 175 205 240 120 90] ✅ з .copy() оригінал цілий
Як перевірити, що перед тобою¶
Атрибут base у вигляду вказує на масив-власника даних, а в самостійного дорівнює
None. Але надійніше питати прямо — np.shares_memory.
І окремо запамʼятай: індексація списком номерів або маскою повертає копію, бо потрібні елементи лежать не з рівним кроком.
основа = np.arange(10)
print("зріз base is основа :", основа[2:6].base is основа)
print("зріз .copy() base is None :", основа[2:6].copy().base is None)
print("список номерів ділить памʼять:", np.shares_memory(основа, основа[[1, 3, 5]]))
print("маска ділить памʼять :", np.shares_memory(основа, основа[основа > 5]))
assert np.shares_memory(основа, основа[2:6]), "звичайний зріз — вигляд"
assert not np.shares_memory(основа, основа[[1, 3, 5]]), "індексація списком — копія"
print("✅ зріз — вигляд, «розумна» індексація — копія")
зріз base is основа : True зріз .copy() base is None : True список номерів ділить памʼять: False маска ділить памʼять : False ✅ зріз — вигляд, «розумна» індексація — копія
7 · Векторизація: заміряємо самі¶
Одна й та сама дія — помножити кожне число на 1.2 — трьома способами. Числа будуть свої, бо залежать від машини; важлива пропорція.
ДОВЖИНА = 200_000
ціни_списком = [float(номер) for номер in range(ДОВЖИНА)]
ціни_масивом = np.arange(ДОВЖИНА, dtype=np.float64)
def через_цикл():
"Класичний for з накопиченням у список — так пишуть без NumPy."
результат = []
for ціна in ціни_списком:
результат.append(ціна * 1.2)
return результат
def через_включення():
"Спискове включення з теми 13 — коротше, але цикл усередині той самий."
return [ціна * 1.2 for ціна in ціни_списком]
def через_масив():
"Векторна дія: жодного циклу в Python, увесь обхід усередині NumPy."
return ціни_масивом * 1.2
# найкращий із семи прогонів — щоб випадкове гальмо системи не зіпсувало картину
час_циклу = min(timeit.repeat(через_цикл, number=1, repeat=7))
час_включення = min(timeit.repeat(через_включення, number=1, repeat=7))
час_масиву = min(timeit.repeat(через_масив, number=1, repeat=7))
print(f"for + append : {час_циклу*1000:9.3f} мс")
print(f"включення : {час_включення*1000:9.3f} мс")
print(f"масив * 1.2 : {час_масиву*1000:9.3f} мс")
print()
print(f"масив швидший за включення у {час_включення/час_масиву:.0f} разів")
for + append : 9.496 мс включення : 8.121 мс масив * 1.2 : 0.085 мс масив швидший за включення у 95 разів
Обовʼязкова перевірка: результат той самий¶
Швидкість нічого не варта, якщо відповідь інша. Порівняємо поелементно.
assert np.allclose(через_включення(), через_масив()), "векторна дія дала інші числа!"
print("✅ включення і векторна дія дають однаковий результат")
✅ включення і векторна дія дають однаковий результат
Чому цикл по масиву ще повільніший¶
Це виглядає парадоксом, але має просте пояснення: у масиві немає готових обʼєктів,
і Python мусить створювати новий numpy.float64 на кожен елемент.
час_циклу_по_масиву = min(timeit.repeat(lambda: [ціна * 1.2 for ціна in ціни_масивом],
number=1, repeat=5))
print(f"включення по списку : {час_включення*1000:8.2f} мс")
print(f"включення по масиву : {час_циклу_по_масиву*1000:8.2f} мс")
print(f"векторна дія : {час_масиву*1000:8.2f} мс")
print()
print(f"цикл по масиву у {час_циклу_по_масиву/час_включення:.1f} раза повільніший за цикл по списку")
включення по списку : 8.12 мс включення по масиву : 22.34 мс векторна дія : 0.09 мс цикл по масиву у 2.8 раза повільніший за цикл по списку
8 · Транслювання¶
Спершу проста дія: сім коефіцієнтів знижки на всі чотири магазини одразу.
Форми (4, 7) і (7,) поєднуються, бо остання вісь у них однакова.
знижка = np.array([1.0, 1.0, 1.0, 1.0, 0.9, 0.8, 0.8]) # пт -10 %, вихідні -20 %
виторг = продажі * знижка
print("форма продажів:", продажі.shape, " форма знижки:", знижка.shape)
print("форма результату:", виторг.shape)
print()
print("Центр після знижки:", виторг[0])
форма продажів: (4, 7) форма знижки: (7,) форма результату: (4, 7) Центр після знижки: [180. 195. 175. 205. 216. 96. 72.]
Коли транслювання падає — і як це полагодити¶
Хочемо для кожного дня побачити відхилення від середнього по цьому магазину.
Перша спроба падає, бо (4,) перетворюється на (1, 4) і четвірка стикається з сімкою.
середнє_магазину = продажі.mean(axis=1)
print("середнє кожного магазину:", середнє_магазину.round(2))
print("його форма:", середнє_магазину.shape)
try:
продажі - середнє_магазину
except ValueError as помилка:
print()
print("ValueError:", помилка)
середнє кожного магазину: [172.14 145. 165.71 122.86] його форма: (4,) ValueError: operands could not be broadcast together with shapes (4,7) (4,)
Ліки — попросити агрегацію не викидати вісь, а лишити її довжиною 1.
Тоді форма буде (4, 1) — стовпець, і транслювання спрацює саме собою.
середнє_стовпцем = продажі.mean(axis=1, keepdims=True)
print("з keepdims=True форма:", середнє_стовпцем.shape)
відхилення = продажі - середнє_стовпцем
print()
print("наскільки кожен день кращий за середній тиждень свого магазину:")
for номер, назва in enumerate(магазини):
print(f" {назва:<7}", відхилення[номер].round(1))
з keepdims=True форма: (4, 1) наскільки кожен день кращий за середній тиждень свого магазину: Центр [ 7.9 22.9 2.9 32.9 67.9 -52.1 -82.1] Вокзал [ -5. -10. 5. 0. 15. 10. -15.] Кампус [ 44.3 64.3 59.3 74.3 24.3 -125.7 -140.7] Парк [-62.9 -67.9 -52.9 -57.9 -12.9 117.1 137.1]
Перевірка «наша реалізація = бібліотечна»¶
Порахуємо ту саму таблицю відхилень вручну, звичайними циклами — і переконаємось, що транслювання дало рівно те саме. Всередині NumPy магії немає.
відхилення_вручну = []
for рядок in продажі:
середнє_рядка = sum(рядок) / len(рядок) # чистий Python, без NumPy
відхилення_вручну.append([число - середнє_рядка for число in рядок])
assert np.allclose(відхилення, відхилення_вручну), "транслювання розійшлося з ручним рахунком!"
print("✅ транслювання = ручні цикли, до останнього знака")
✅ транслювання = ручні цикли, до останнього знака
9 · Осі: axis=0 проти axis=1¶
axis=k — це вісь, уздовж якої йде підсумовування, і саме вона зникає з форми.
(4, 7) при axis=0 стає (7,), при axis=1 — (4,).
по_днях = продажі.sum(axis=0) # склали магазини, лишились дні
по_магазинах = продажі.sum(axis=1) # склали дні, лишились магазини
print("сума по днях ", по_днях, " форма", по_днях.shape)
print("сума по магазинах", по_магазинах, " форма", по_магазинах.shape)
print()
for назва, чашки in zip(дні, по_днях):
print(f" {назва}: {чашки}")
сума по днях [590 615 620 655 700 555 505] форма (7,) сума по магазинах [1205 1015 1160 860] форма (4,) пн: 590 вт: 615 ср: 620 чт: 655 пт: 700 сб: 555 нд: 505
Обидва підсумки складаються в те саме загальне число — інакше десь помилка з осями.
усього = продажі.sum()
print("усього чашок за тиждень:", усього)
assert по_днях.sum() == усього, "сума по днях має дати те саме загальне число"
assert по_магазинах.sum() == усього, "сума по магазинах — теж"
assert по_днях.shape == (7,) and по_магазинах.shape == (4,), "вісь мала зникнути з форми"
print("✅ обидва розрізи узгоджені й форми правильні")
усього чашок за тиждень: 4240 ✅ обидва розрізи узгоджені й форми правильні
argmax повертає номер, а не значення¶
У парі з осями це найзручніший спосіб відповісти на питання «а коли саме».
найкращий_день = продажі.argmax(axis=1) # номер стовпця для кожного рядка
найкращі_чашки = продажі.max(axis=1)
for назва, номер, чашки in zip(магазини, найкращий_день, найкращі_чашки):
print(f"{назва:<7} найкращий день — {дні[номер]}, {чашки} чашок")
# argmax і max мають вказувати на одне й те саме
for рядок, номер, чашки in zip(продажі, найкращий_день, найкращі_чашки):
assert рядок[номер] == чашки, "argmax вказав не на максимум!"
print()
print("✅ argmax і max узгоджені")
Центр найкращий день — пт, 240 чашок Вокзал найкращий день — пт, 160 чашок Кампус найкращий день — чт, 240 чашок Парк найкращий день — нд, 260 чашок ✅ argmax і max узгоджені
10 · Булеві маски¶
Умова над масивом повертає масив відповідей. Ним можна індексувати, його можна
підсумовувати, і .mean() над ним дає частку.
тиждень_центру = продажі[0]
маска = тиждень_центру > 150
print("тиждень Центру:", тиждень_центру)
print("маска > 150 :", маска, " dtype:", маска.dtype)
print("відібрані :", тиждень_центру[маска])
print()
print("скільки таких днів:", маска.sum())
print("яка їх частка :", маска.mean().round(3))
print("np.where :", np.where(маска, тиждень_центру, 0))
тиждень Центру: [180 195 175 205 240 120 90] маска > 150 : [ True True True True True False False] dtype: bool відібрані : [180 195 175 205 240] скільки таких днів: 5 яка їх частка : 0.714 np.where : [180 195 175 205 240 0 0]
Умови поєднуються через &, |, ~ — і кожну обовʼязково беруть у дужки.
Слово and тут не працює й падає з промовистою помилкою.
середні_дні = тиждень_центру[(тиждень_центру > 100) & (тиждень_центру < 200)]
print("дні між 100 і 200:", середні_дні)
try:
тиждень_центру[(тиждень_центру > 100) and (тиждень_центру < 200)]
except ValueError as помилка:
print()
print("а зі словом and:", помилка)
дні між 100 і 200: [180 195 175 120] а зі словом and: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
Маска над двовимірним масивом¶
Результат завжди одновимірний: підходящі клітинки розкидані по таблиці, прямокутника з них не складеш.
вдалі_дні = продажі > 200
print("усі значення понад 200:", продажі[вдалі_дні], " форма:", продажі[вдалі_дні].shape)
print()
print("скільки вдалих днів у кожного магазину:")
for назва, скільки in zip(магазини, вдалі_дні.sum(axis=1)):
print(f" {назва:<7} {скільки}")
# маску можна перевірити прямим підрахунком
кількість_вручну = sum(1 for рядок in продажі for число in рядок if число > 200)
assert вдалі_дні.sum() == кількість_вручну, "маска порахувала не те, що ручний цикл"
print()
print("✅ маска = ручний підрахунок")
усі значення понад 200: [205 240 210 230 225 240 240 260] форма: (8,) скільки вдалих днів у кожного магазину: Центр 2 Вокзал 0 Кампус 4 Парк 2 ✅ маска = ручний підрахунок
11 · Для тих, кому мало: dtype і nan¶
Тісний тип економить памʼять, але мовчки переповнюється. Жодного винятку не буде — просто інше число.
малі = np.array([100, 50, 25], dtype=np.int8) # діапазон -128…127
print("int8 [100, 50, 25] * 3 =", малі * 3, "← сто помножити на три дало 44")
print()
for тип in (np.int8, np.int16, np.int32, np.int64, np.float32, np.float64):
заготовка = np.zeros(1_000_000, dtype=тип)
print(f" {np.dtype(тип).name:>8}: {заготовка.itemsize} Б на елемент, "
f"мільйон = {заготовка.nbytes/2**20:5.2f} МБ")
int8 [100, 50, 25] * 3 = [ 44 -106 75] ← сто помножити на три дало 44
int8: 1 Б на елемент, мільйон = 0.95 МБ
int16: 2 Б на елемент, мільйон = 1.91 МБ
int32: 4 Б на елемент, мільйон = 3.81 МБ
int64: 8 Б на елемент, мільйон = 7.63 МБ
float32: 4 Б на елемент, мільйон = 3.81 МБ
float64: 8 Б на елемент, мільйон = 7.63 МБ
Точність — той самий компроміс, але для дробових. float32 удвічі економніший,
проте на довгих сумах помилка накопичується.
точна_сума = 999_999 * 1_000_000 // 2 # формула суми арифметичної прогресії
сума_32 = float(np.arange(1_000_000, dtype=np.float32).sum())
сума_64 = float(np.arange(1_000_000, dtype=np.float64).sum())
print("точне значення :", точна_сума)
print("float32 :", сума_32, " похибка:", точна_сума - сума_32)
print("float64 :", сума_64, " похибка:", точна_сума - сума_64)
assert сума_64 == точна_сума, "float64 має впоратись точно"
assert сума_32 != точна_сума, "а float32 — ні, і це очікувано"
print()
print("✅ float64 порахував точно, float32 промахнувся на", точна_сума - сума_32)
точне значення : 499999500000 float32 : 499999506432.0 похибка: -6432.0 float64 : 499999500000.0 похибка: 0.0 ✅ float64 порахував точно, float32 промахнувся на -6432.0
І наостанок — дірки в даних. Уявімо, що в суботу Кампус не працював і числа немає.
np.nan заражає будь-який підсумок, тому для таких масивів є окрема родина функцій.
кампус = продажі[2].astype(float) # nan живе лише у світі float, тому переводимо тип
кампус[5] = np.nan # у суботу даних немає
print("Кампус із діркою:", кампус)
print("mean() :", кампус.mean(), "← одна дірка зіпсувала весь підсумок")
print("np.nanmean():", round(float(np.nanmean(кампус)), 2), "← версія, яка дірки пропускає")
print()
print("np.nan == np.nan :", np.nan == np.nan, "← шукати дірки порівнянням марно")
print("np.isnan(кампус) :", np.isnan(кампус))
assert np.isnan(кампус.mean()), "звичайне середнє мало стати nan"
assert not np.isnan(np.nanmean(кампус)), "nanmean мав дірку пропустити"
print()
print("✅ nan поводиться так, як обіцяла лекція")
Кампус із діркою: [210. 230. 225. 240. 190. nan 25.] mean() : nan ← одна дірка зіпсувала весь підсумок np.nanmean(): 186.67 ← версія, яка дірки пропускає np.nan == np.nan : False ← шукати дірки порівнянням марно np.isnan(кампус) : [False False False False False True False] ✅ nan поводиться так, як обіцяла лекція
12 · Завдання¶
Роби просто в цьому зошиті — додай клітинки нижче.
🟢 Рівень 1 — База¶
Побудуй масив витрати тієї самої форми (4, 7): скільки грошей кожен магазин
витратив за кожен день (візьми числа з голови або з default_rng(42)).
Порахуй витрати.sum(axis=0) і витрати.sum(axis=1) і поясни словами в
markdown-клітинці, що означає кожен із двох масивів.
Зроблено, якщо: обидві суми дають те саме загальне число, і assert це підтверджує.
🟡 Рівень 2 — Плюс¶
Порахуй частку кожного магазину в продажах кожного дня: скільки відсотків
денного підсумку припадає на цей магазин. Знадобиться sum(axis=0, keepdims=True)
і транслювання.
Зроблено, якщо: сума кожного стовпця результату дорівнює 1.0 —
перевір це assert np.allclose(частки.sum(axis=0), 1.0).
🔴 Рівень 3 — Виклик¶
Напиши функцію нормалізувати(масив), яка віднімає від кожного рядка його середнє
і ділить на його стандартне відхилення (std). Зроби так, щоб вона не псувала
переданий масив, і доведи це assert-ом. Потім спеціально зламай її — прибери
захист — і покажи, що оригінал зіпсувався.
Зроблено, якщо: після виклику правильної версії assert np.array_equal(продажі, копія_до_виклику)
проходить, а після виклику зламаної — падає (спіймай це через try/except AssertionError).