25. (Л) Вступ до NumPy. Масиви ndarray, створення, типи даних¶
Зміст лекції¶
- Навіщо NumPy: числа у списках Python
- Встановлення та імпорт
- Що таке
ndarray - Атрибути масиву
- Створення масиву з Python-даних
- Масиви-заготовки:
zeros,ones,full,eye - Числові послідовності:
arangeіlinspace - Форма масиву:
reshape - Типи даних (
dtype) - Як NumPy обирає тип
- Перетворення типів:
astype - Межі типів і переповнення
- Поелементна арифметика
- Універсальні функції та агрегати
- Осі (
axis) - Типові помилки
- Підсумок
Навіщо NumPy: числа у списках Python¶
Список Python — універсальний контейнер. У ньому можна тримати числа, рядки, словники й інші списки впереміш. За цю гнучкість доводиться платити, і коли даних багато, ціна стає відчутною.
Задача: є мільйон чисел, треба знайти суму їхніх квадратів.
import time
import numpy as np
N = 1_000_000
# Звичайний Python: список і цикл
values = list(range(N))
start = time.perf_counter()
total_list = sum(x * x for x in values)
list_time = time.perf_counter() - start
# NumPy: масив і одна операція над усім масивом
array = np.arange(N)
start = time.perf_counter()
total_numpy = (array * array).sum()
numpy_time = time.perf_counter() - start
print(f"list: {list_time:.4f} s, total = {total_list}")
print(f"numpy: {numpy_time:.4f} s, total = {total_numpy}")
print(f"speedup: {list_time / numpy_time:.0f}x")
Приблизний результат (числа залежать від комп'ютера):
Результат однаковий, але NumPy рахує приблизно в десять разів швидше, а на складніших обчисленнях різниця сягає десятків і сотень разів. Причин дві.
1. Пам'ять. Список зберігає не числа, а посилання на окремі об'єкти int. Кожен такий об'єкт має заголовок (лічильник посилань, тип, розмір) і розкиданий десь у пам'яті. Масив NumPy зберігає самі числа — одним суцільним блоком байтів, одне за одним.
flowchart LR
subgraph list["list: посилання на об'єкти"]
L[list] --> P0[ptr] --> O0["int object<br/>28 bytes"]
L --> P1[ptr] --> O1["int object<br/>28 bytes"]
L --> P2[ptr] --> O2["int object<br/>28 bytes"]
end
subgraph nd["ndarray: суцільний буфер"]
H["заголовок<br/>shape, dtype"] --> B["| 8 bytes | 8 bytes | 8 bytes |"]
end
2. Цикл. У виразі sum(x * x for x in values) інтерпретатор на кожному кроці з'ясовує тип x, шукає для нього операцію *, створює новий об'єкт int для результату. У виразі array * array тип відомий заздалегідь (усі елементи однакові), і NumPy виконує цикл усередині скомпільованого коду на C — без інтерпретатора.
Різницю в пам'яті легко виміряти:
import sys
import numpy as np
N = 1_000_000
values = list(range(N))
# розмір самого списку (масив посилань) + розмір кожного об'єкта int
list_bytes = sys.getsizeof(values) + sum(sys.getsizeof(x) for x in values)
array = np.arange(N)
print(f"list: {list_bytes / 1_000_000:.1f} MB")
print(f"ndarray: {array.nbytes / 1_000_000:.1f} MB")
Такий спосіб роботи — одна операція над усім масивом замість циклу по елементах — називається векторизацією. Це головна ідея NumPy, і на ній тримається все, що буде далі в модулі: pandas, наукові обчислення, машинне навчання.
Що таке NumPy
NumPy (Numerical Python) — бібліотека для роботи з багатовимірними числовими масивами. Вона дає тип ndarray, сотні математичних функцій над ним, лінійну алгебру й генерацію випадкових чисел. На NumPy побудовані pandas, SciPy, Matplotlib, scikit-learn та багато інших бібліотек.
Встановлення та імпорт¶
NumPy — стороння бібліотека, її встановлюють у віртуальне середовище:
Перевірка:
Імпорт завжди пишуть так:
Скорочення np — загальноприйнята домовленість. Його використовують документація, підручники, Stack Overflow і будь-який код, який ви прочитаєте. Писати import numpy чи from numpy import * технічно можна, але так ніхто не робить.
Що таке ndarray¶
ndarray (N-dimensional array) — основний тип NumPy. Він має три ключові властивості:
| Властивість | Що означає | Чим відрізняється від list |
|---|---|---|
| однорідність | усі елементи мають один тип (dtype) |
у списку можуть бути елементи будь-яких типів |
| фіксований розмір | кількість елементів задається при створенні | список росте через append() |
| багатовимірність | масив може мати 1, 2, 3 і більше вимірів | «таблиця» в Python — це список списків |
Виміри масиву в NumPy називають осями (axes).
1D: shape (4,) 2D: shape (2, 3) 3D: shape (2, 2, 3)
[1 2 3 4] [[1 2 3] [[[ 1 2 3]
[4 5 6]] [ 4 5 6]]
вектор матриця: [[ 7 8 9]
2 рядки, 3 стовпці [10 11 12]]]
2 матриці 2x3
Приклади з життя:
- 1D — температура за кожну годину доби: 24 числа;
- 2D — оцінки студентів: рядок — студент, стовпець — предмет;
- 3D — кольорове зображення: висота × ширина × 3 канали (R, G, B).
Атрибути масиву¶
Кожен масив знає про себе все необхідне:
import numpy as np
matrix = np.array([[1, 2, 3], [4, 5, 6]])
print(matrix)
print("ndim: ", matrix.ndim)
print("shape: ", matrix.shape)
print("size: ", matrix.size)
print("dtype: ", matrix.dtype)
print("itemsize:", matrix.itemsize)
print("nbytes: ", matrix.nbytes)
print("type: ", type(matrix))
[[1 2 3]
[4 5 6]]
ndim: 2
shape: (2, 3)
size: 6
dtype: int64
itemsize: 8
nbytes: 48
type: <class 'numpy.ndarray'>
| Атрибут | Значення |
|---|---|
ndim |
кількість вимірів (осей) |
shape |
кортеж: розмір уздовж кожної осі |
size |
загальна кількість елементів, добуток чисел із shape |
dtype |
тип елементів |
itemsize |
розмір одного елемента в байтах |
nbytes |
розмір даних у байтах: size * itemsize |
shape одновимірного масиву
Для 1D-масиву shape — це кортеж з одного елемента: (4,), а не 4 і не (4). Кома обов'язкова: (4) у Python — просто число в дужках.
Зверніть увагу: print() показує масив без ком між елементами — так його легко відрізнити від списку. Функція repr() (або вивід в інтерактивній консолі) показує повну форму:
import numpy as np
numbers = np.array([1, 2, 3])
print(numbers)
print(repr(numbers))
print([1, 2, 3])
Створення масиву з Python-даних¶
Найпростіший спосіб — функція np.array(), яка приймає список (або кортеж) чи вкладені списки:
import numpy as np
vector = np.array([10, 20, 30, 40])
matrix = np.array([[1.5, 2.0], [3.0, 4.5]])
cube = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
from_tuple = np.array((True, False, True))
for name, array in [("vector", vector), ("matrix", matrix),
("cube", cube), ("from_tuple", from_tuple)]:
print(f"{name}: shape={array.shape}, dtype={array.dtype}")
print(array)
print()
vector: shape=(4,), dtype=int64
[10 20 30 40]
matrix: shape=(2, 2), dtype=float64
[[1.5 2. ]
[3. 4.5]]
cube: shape=(2, 2, 2), dtype=int64
[[[1 2]
[3 4]]
[[5 6]
[7 8]]]
from_tuple: shape=(3,), dtype=bool
[ True False True]
Кількість вкладених рівнів списку визначає кількість вимірів. Запис 2. — це 2.0: NumPy скорочує нулі після крапки.
Вкладені списки мають бути однакової довжини. Масив — прямокутний, «рваних» рядків у ньому не буває:
import numpy as np
try:
ragged = np.array([[1, 2, 3], [4, 5]])
except ValueError as error:
print("ValueError:", error)
ValueError: setting an array element with a sequence. The requested array has an inhomogeneous shape after 1 dimensions. The detected shape was (2,) + inhomogeneous part.
Типова помилка — передати числа окремими аргументами замість списку:
import numpy as np
try:
wrong = np.array(1, 2, 3)
except TypeError as error:
print("TypeError:", error)
right = np.array([1, 2, 3])
print(right)
Масиви-заготовки: zeros, ones, full, eye¶
Часто розмір масиву відомий, а значення будуть обчислені пізніше. Для цього є функції, які створюють масив потрібної форми, заповнений однаковими значеннями:
import numpy as np
print("zeros:")
print(np.zeros(5))
print("ones (2, 3), int:")
print(np.ones((2, 3), dtype=int))
print("full (2, 4) with 7:")
print(np.full((2, 4), 7))
print("eye 3:")
print(np.eye(3))
print("empty (2, 2):")
print(np.empty((2, 2)))
zeros:
[0. 0. 0. 0. 0.]
ones (2, 3), int:
[[1 1 1]
[1 1 1]]
full (2, 4) with 7:
[[7 7 7 7]
[7 7 7 7]]
eye 3:
[[1. 0. 0.]
[0. 1. 0.]
[0. 0. 1.]]
empty (2, 2):
[[6.91384439e-310 6.91384439e-310]
[0.00000000e+000 0.00000000e+000]]
| Функція | Що створює | Тип за замовчуванням |
|---|---|---|
np.zeros(shape) |
масив нулів | float64 |
np.ones(shape) |
масив одиниць | float64 |
np.full(shape, value) |
масив, заповнений value |
тип value |
np.eye(n) |
одинична матриця n × n |
float64 |
np.empty(shape) |
масив без ініціалізації | float64 |
Три моменти, які важливо запам'ятати:
- форма передається одним аргументом:
np.zeros((2, 3))— з подвійними дужками. Записnp.zeros(2, 3)— помилка: друге число NumPy сприйме якdtype; zerosіonesза замовчуванням створюють дробові числа —0.і1., а не0і1. Цілі потрібно просити явно черезdtype=int;np.emptyне заповнює масив нічим: у ньому лежить те, що було в пам'яті раніше (у вас значення будуть інші). Ця функція трохи швидша, але використовують її лише тоді, коли кожен елемент гарантовано буде перезаписано.
Функції з суфіксом _like створюють масив такої ж форми й типу, як інший:
import numpy as np
prices = np.array([[19.99, 5.49, 3.00], [7.25, 12.10, 1.75]])
discounts = np.zeros_like(prices)
counts = np.ones_like(prices, dtype=int)
print(prices.shape, discounts.shape, counts.shape)
print(discounts)
print(counts)
Числові послідовності: arange і linspace¶
np.arange¶
np.arange — аналог вбудованого range, але повертає масив і вміє працювати з дробовим кроком:
import numpy as np
print(np.arange(5)) # від 0 до 5, не включаючи 5
print(np.arange(2, 10)) # від 2 до 10
print(np.arange(0, 20, 5)) # крок 5
print(np.arange(10, 0, -3)) # крок може бути від'ємним
print(np.arange(0, 1, 0.25)) # дробовий крок
Як і в range, кінець не входить у результат.
arange із дробовим кроком
Дробові числа в комп'ютері неточні, тому кількість елементів при дробовому кроці може виявитися несподіваною:
Очікували три елементи (1.3 не мав увійти), отримали чотири: 1 + 3 * 0.1 дає 1.3000000000000003, і NumPy вирішує, що місця ще на один крок вистачає. Для дробових послідовностей використовуйте linspace.
np.linspace¶
np.linspace(start, stop, num) ділить відрізок на задану кількість точок. Крок NumPy обчислює сам, і кінець за замовчуванням входить:
import numpy as np
print(np.linspace(0, 1, 5))
print(np.linspace(0, 1, 5, endpoint=False))
points, step = np.linspace(0, 100, 9, retstep=True)
print(points)
print("step:", step)
arange(start, stop, step) |
linspace(start, stop, num) |
|
|---|---|---|
| Що задаєте | крок | кількість точок |
stop входить? |
ні | так (можна вимкнути: endpoint=False) |
| Тип | цілий, якщо всі аргументи цілі | завжди float64 |
| Коли використовувати | цілі послідовності, індекси | дробові сітки: час, координати, графіки |
Форма масиву: reshape¶
Метод reshape перекладає ті самі елементи в іншу форму. Кількість елементів при цьому змінитись не може:
import numpy as np
numbers = np.arange(12)
print(numbers)
print("reshape(3, 4):")
print(numbers.reshape(3, 4))
print("reshape(2, 6):")
print(numbers.reshape(2, 6))
print("reshape(2, 3, 2):")
print(numbers.reshape(2, 3, 2))
[ 0 1 2 3 4 5 6 7 8 9 10 11]
reshape(3, 4):
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
reshape(2, 6):
[[ 0 1 2 3 4 5]
[ 6 7 8 9 10 11]]
reshape(2, 3, 2):
[[[ 0 1]
[ 2 3]
[ 4 5]]
[[ 6 7]
[ 8 9]
[10 11]]]
Елементи заповнюють нову форму рядок за рядком — зліва направо, згори вниз.
Один із розмірів можна замінити на -1: NumPy обчислить його сам.
import numpy as np
numbers = np.arange(12)
print(numbers.reshape(4, -1).shape) # 12 / 4 = 3 стовпці
print(numbers.reshape(-1, 6).shape) # 12 / 6 = 2 рядки
matrix = numbers.reshape(3, 4)
print(matrix.reshape(-1)) # назад в один вимір
print(matrix.ravel()) # те саме
try:
numbers.reshape(5, 2)
except ValueError as error:
print("ValueError:", error)
(4, 3)
(2, 6)
[ 0 1 2 3 4 5 6 7 8 9 10 11]
[ 0 1 2 3 4 5 6 7 8 9 10 11]
ValueError: cannot reshape array of size 12 into shape (5,2)
Типовий прийом — створити послідовність через arange і одразу надати їй потрібну форму:
reshape не змінює оригінал
reshape повертає новий об'єкт-масив, а початковий лишається тієї самої форми. Проте дані в пам'яті вони зазвичай ділять спільні — це називається перегляд (view). Детально про перегляди й копії — у лекції 27.
Типи даних (dtype)¶
У масиві всі елементи одного типу, і цей тип — dtype — визначає, скільки байтів займає елемент і які значення в нього поміщаються.
| Група | dtype |
Байтів | Діапазон / опис |
|---|---|---|---|
| логічний | bool |
1 | True / False |
| цілі зі знаком | int8 |
1 | −128 .. 127 |
int16 |
2 | −32 768 .. 32 767 | |
int32 |
4 | ≈ ±2.1 млрд | |
int64 |
8 | ≈ ±9.2 · 10¹⁸ | |
| цілі без знаку | uint8 |
1 | 0 .. 255 |
uint16, uint32, uint64 |
2, 4, 8 | 0 .. 2ⁿ − 1 | |
| дробові | float16 |
2 | ≈ 4 значущі цифри |
float32 |
4 | ≈ 7 значущих цифр | |
float64 |
8 | ≈ 15–16 значущих цифр (як float у Python) |
|
| комплексні | complex128 |
16 | 1+2j |
| рядки | <U10 |
40 | рядок Unicode до 10 символів |
| об'єкти | object |
8 | посилання на довільні Python-об'єкти |
Число в назві — кількість біт, а не байтів: int32 — 32 біти, тобто 4 байти.
Точні межі можна дізнатися в коді:
import numpy as np
for dtype in [np.int8, np.int16, np.int32, np.int64, np.uint8]:
info = np.iinfo(dtype)
print(f"{info.dtype}: {info.min} .. {info.max}")
for dtype in [np.float16, np.float32, np.float64]:
info = np.finfo(dtype)
print(f"{info.dtype}: max={info.max}, digits={info.precision}")
int8: -128 .. 127
int16: -32768 .. 32767
int32: -2147483648 .. 2147483647
int64: -9223372036854775808 .. 9223372036854775807
uint8: 0 .. 255
float16: max=65504.0, digits=3
float32: max=3.4028234663852886e+38, digits=6
float64: max=1.7976931348623157e+308, digits=15
Тип задають параметром dtype будь-якої функції створення. Приймаються три форми запису — вони рівнозначні:
import numpy as np
a = np.array([1, 2, 3], dtype=np.int16) # об'єкт типу NumPy
b = np.array([1, 2, 3], dtype="int16") # рядок з назвою
c = np.zeros(3, dtype=float) # тип Python: float -> float64
print(a.dtype, b.dtype, c.dtype)
Вибір типу — це компроміс між пам'яттю й діапазоном. Для мільйона елементів різниця помітна:
import numpy as np
N = 1_000_000
for dtype in ["int8", "int16", "int32", "int64", "float32", "float64"]:
array = np.zeros(N, dtype=dtype)
print(f"{dtype:>7}: {array.nbytes / 1_000_000:>4.0f} MB")
Який тип обирати
Поки немає причини інакше — залишайте типи за замовчуванням: int64 і float64. Менші типи (uint8, float32) беруть свідомо: для зображень (пікселі 0..255), великих масивів, які мають поміститися в пам'ять, або коли цього вимагає інша бібліотека.
Як NumPy обирає тип¶
Якщо dtype не задано, NumPy обирає найменш загальний тип, у який поміщаються всі елементи:
import numpy as np
examples = [
[1, 2, 3],
[1.0, 2, 3],
[True, False],
[True, 2, 3.5],
[1, 2, 3 + 4j],
["kyiv", "lviv", "odesa"],
[1, "two", 3.0],
]
for data in examples:
array = np.array(data)
print(f"{str(data):<28} -> {array.dtype}: {array}")
[1, 2, 3] -> int64: [1 2 3]
[1.0, 2, 3] -> float64: [1. 2. 3.]
[True, False] -> bool: [ True False]
[True, 2, 3.5] -> float64: [1. 2. 3.5]
[1, 2, (3+4j)] -> complex128: [1.+0.j 2.+0.j 3.+4.j]
['kyiv', 'lviv', 'odesa'] -> <U5: ['kyiv' 'lviv' 'odesa']
[1, 'two', 3.0] -> <U32: ['1' 'two' '3.0']
Порядок «підвищення» типу: bool → цілі → дробові → комплексні. Одного дробового числа достатньо, щоб увесь масив став float64.
Останній рядок — пастка: число й рядок у одному списку перетворюють усі елементи на рядки. Із таким масивом уже не можна рахувати: '1' + '3.0' — не число.
<U5 читається так: < — порядок байтів, U — Unicode-рядок, 5 — максимальна довжина. Рядок, довший за цю межу, при записі в масив мовчки обріжеться.
int64 чи int32
На Linux і macOS цілі за замовчуванням — int64. У NumPy 1.x на Windows за замовчуванням був int32; починаючи з NumPy 2.0 скрізь int64.
Перетворення типів: astype¶
Метод astype створює новий масив із тими самими значеннями в іншому типі:
import numpy as np
temperatures = np.array([-2.7, -0.5, 0.5, 3.9, 21.5])
print("float: ", temperatures)
print("int: ", temperatures.astype(int))
print("rounded:", np.round(temperatures).astype(int))
print("bool: ", np.array([0, 3, 0, -1]).astype(bool))
text = np.array(["1.5", "2", "-3.25"])
print("from str:", text.astype(float))
print("to str: ", np.array([1, 20, 300]).astype(str))
print("original is unchanged:", temperatures.dtype)
float: [-2.7 -0.5 0.5 3.9 21.5]
int: [-2 0 0 3 21]
rounded: [-3 0 0 4 22]
bool: [False True False True]
from str: [ 1.5 2. -3.25]
to str: ['1' '20' '300']
original is unchanged: float64
Що варто помітити:
- дробові → цілі відкидають дробову частину (округлення до нуля):
-2.7→-2,3.9→3. Якщо потрібне математичне округлення — спочаткуnp.round, потімastype(int); np.roundокруглює.5до найближчого парного:-0.5→0,0.5→0,21.5→22;- ненульове число →
True, нуль →False; - рядки з числами перетворюються на числа — зручно, коли дані прочитано з текстового файла;
- оригінал не змінюється. Результат
astypeтреба зберегти у змінну.
Межі типів і переповнення¶
Звичайний int у Python не має верхньої межі: 2 ** 100 — коректне число. Цілі типи NumPy мають фіксований розмір, і результат, який виходить за межі, мовчки «загортається» по колу:
import numpy as np
small = np.array([100, 120, 127], dtype=np.int8)
print(small + 10)
pixels = np.array([200, 250, 10], dtype=np.uint8)
print(pixels + 100)
print(pixels - 20)
Жодної помилки, жодного попередження. 127 + 10 у int8 дає -119, а піксель яскравості 250 після «освітлення» на 100 стає майже чорним — 94.
Це схоже на механічний лічильник кілометрів: після 999999 він показує 000000.
Числа, які не поміщаються в тип ще при створенні, NumPy 2 не пропустить:
import numpy as np
try:
np.array([100, 200, 300], dtype=np.int8)
except OverflowError as error:
print("OverflowError:", error)
А ось переповнення під час обчислень над масивами ловити доведеться вам. Найпростіший захист — перейти на ширший тип до операції:
import numpy as np
pixels = np.array([200, 250, 10], dtype=np.uint8)
brighter = pixels.astype(np.int16) + 100 # у int16 є місце для 350
brighter = np.clip(brighter, 0, 255) # обрізати до меж яскравості
brighter = brighter.astype(np.uint8) # повернути тип зображення
print(brighter)
Дробові типи не загортаються, а переходять у inf (нескінченність) і мають обмежену точність:
import numpy as np
print(np.array([60000], dtype=np.float16) * 2)
value32 = np.float32(0.1)
value64 = np.float64(0.1)
print(value32)
print(value32 == value64)
print(f"{value32:.20f}")
print(f"{value64:.20f}")
float32 і float64 зберігають 0.1 з різною похибкою, тому порівняння == між ними дає False. Дробові числа не порівнюють через == — для цього є np.isclose і np.allclose:
import numpy as np
a = np.array([0.1, 0.2, 0.3], dtype=np.float32)
b = np.array([0.1, 0.2, 0.3])
print(a == b)
print(np.isclose(a, b))
print(np.allclose(a, b))
Поелементна арифметика¶
Арифметичні оператори над масивами працюють поелементно: перший елемент з першим, другий з другим і так далі. Результат — новий масив тієї ж форми.
import numpy as np
a = np.array([10, 20, 30, 40])
b = np.array([1, 2, 3, 4])
print("a + b =", a + b)
print("a - b =", a - b)
print("a * b =", a * b)
print("a / b =", a / b)
print("a // 3 =", a // 3)
print("a % 3 =", a % 3)
print("b ** 2 =", b ** 2)
print("-a =", -a)
a + b = [11 22 33 44]
a - b = [ 9 18 27 36]
a * b = [ 10 40 90 160]
a / b = [10. 10. 10. 10.]
a // 3 = [ 3 6 10 13]
a % 3 = [1 2 0 1]
b ** 2 = [ 1 4 9 16]
-a = [-10 -20 -30 -40]
Ділення / завжди дає float64, навіть якщо ділиться націло — так само, як у звичайному Python.
Операція з числом застосовується до кожного елемента. Без жодного циклу:
import numpy as np
prices_uah = np.array([120.0, 45.5, 999.0, 15.25])
USD_RATE = 41.5
prices_usd = prices_uah / USD_RATE
with_vat = prices_uah * 1.2
print(np.round(prices_usd, 2))
print(with_vat)
Порівняльні оператори теж поелементні й повертають масив bool:
import numpy as np
scores = np.array([55, 91, 74, 60, 88])
passed = scores >= 60
print(passed)
print(passed.dtype)
print("passed count:", passed.sum())
print("all passed:", passed.all())
print("any excellent:", (scores >= 90).any())
sum() над масивом bool рахує кількість True: True вважається за 1, False — за 0. Це найкоротший спосіб порахувати, скільки елементів задовольняють умову.
Масив — не список
Ті самі оператори над списками й масивами роблять зовсім різні речі:
import numpy as np
print([1, 2, 3] + [10, 20, 30])
print(np.array([1, 2, 3]) + np.array([10, 20, 30]))
print([1, 2, 3] * 2)
print(np.array([1, 2, 3]) * 2)
Для списку + — склеювання, * — повторення. Для масиву — арифметика.
Форми мають збігатися. Масиви різної довжини поелементно не складаються:
import numpy as np
try:
np.array([1, 2, 3]) + np.array([1, 2, 3, 4])
except ValueError as error:
print("ValueError:", error)
Слово broadcast у повідомленні — не випадкове. Операція «масив + число» насправді є окремим випадком загального механізму broadcasting, який дозволяє поєднувати масиви різних, але сумісних форм — наприклад, додати до кожного рядка матриці один і той самий вектор. Правила broadcasting розберемо в лекції 27.
Ділення на нуль не кидає виняток, а дає спеціальні значення й попередження:
import numpy as np
a = np.array([1.0, -1.0, 0.0])
with np.errstate(divide="ignore", invalid="ignore"):
result = a / 0
print(result)
print(np.isinf(result))
print(np.isnan(result))
inf — нескінченність, nan (Not a Number) — «невизначений результат», як 0 / 0. Без np.errstate NumPy виведе RuntimeWarning: divide by zero encountered in divide, але обчислення все одно продовжиться.
Універсальні функції та агрегати¶
Універсальні функції (ufunc)¶
Математичні функції NumPy — np.sqrt, np.abs, np.exp тощо — теж працюють поелементно. Їх називають універсальними функціями (universal functions, ufunc):
import numpy as np
x = np.array([1.0, 4.0, 9.0, 16.0])
angles = np.linspace(0, np.pi, 5)
print("sqrt: ", np.sqrt(x))
print("log10: ", np.log10(x))
print("abs: ", np.abs(np.array([-3, 0, 5])))
print("sin: ", np.round(np.sin(angles), 3))
print("maximum:", np.maximum(np.array([1, 8, 3]), np.array([5, 2, 6])))
sqrt: [1. 2. 3. 4.]
log10: [0. 0.60205999 0.95424251 1.20411998]
abs: [3 0 5]
sin: [0. 0.707 1. 0.707 0. ]
maximum: [5 8 6]
Функції з модуля math (math.sqrt) працюють лише з одним числом. Над масивами завжди використовуйте версії з np.
Агрегати¶
Агрегатна функція зводить масив до одного числа:
import numpy as np
temperatures = np.array([12.5, 15.0, 9.5, 18.0, 21.5, 17.0, 11.5])
print("sum: ", temperatures.sum())
print("min: ", temperatures.min())
print("max: ", temperatures.max())
print("mean: ", temperatures.mean())
print("argmin:", temperatures.argmin())
print("argmax:", temperatures.argmax())
print("prod: ", np.array([1, 2, 3, 4]).prod())
argmin і argmax повертають не значення, а позицію (індекс) мінімуму чи максимуму. Якщо дні тижня нумеруються з нуля, найхолодніший — день 2, найтепліший — день 4.
Більшість агрегатів доступні у двох формах, які роблять одне й те саме:
import numpy as np
data = np.array([3, 1, 4, 1, 5])
print(data.sum(), np.sum(data))
print(data.max(), np.max(data))
sum() чи np.sum()
Вбудовані sum, min, max Python теж приймають масив, але перебирають його елементи по одному, як список. На великих масивах це в десятки разів повільніше, а для 2D-масиву ще й дає інший результат. Використовуйте методи масиву або функції np..
Статистичні функції (median, std, var, percentile) детально розберемо в лекції 30.
Осі (axis)¶
Для багатовимірного масиву агрегат можна порахувати не по всіх елементах, а уздовж однієї осі. Приклад: продажі трьох магазинів за чотири дні.
import numpy as np
# рядок — магазин, стовпець — день
sales = np.array([
[12, 15, 11, 20],
[ 8, 9, 14, 10],
[20, 18, 25, 22],
])
print("total: ", sales.sum())
print("per day: ", sales.sum(axis=0))
print("per shop: ", sales.sum(axis=1))
print("best day idx: ", sales.sum(axis=0).argmax())
print("max per shop: ", sales.max(axis=1))
print("mean per day: ", sales.mean(axis=0))
total: 184
per day: [40 42 50 52]
per shop: [58 41 85]
best day idx: 3
max per shop: [20 14 25]
mean per day: [13.33333333 14. 16.66666667 17.33333333]
axis=1 ------------------->
day0 day1 day2 day3 sum(axis=1)
+-----+-----+-----+-----+
axis=0 shop0 | 12 | 15 | 11 | 20 | --> 58
| shop1 | 8 | 9 | 14 | 10 | --> 41
| shop2 | 20 | 18 | 25 | 22 | --> 85
v +-----+-----+-----+-----+
| | | |
v v v v
sum(axis=0) 40 42 50 52
Як запам'ятати: axis — це вісь, яка «зникає» після агрегації.
axis=0— рух згори вниз, уздовж рядків. Кожен стовпець схлопується в одне число. Форма(3, 4)→(4,): результат для кожного дня.axis=1— рух зліва направо, уздовж стовпців. Кожен рядок схлопується в одне число. Форма(3, 4)→(3,): результат для кожного магазину.- без
axis— усі елементи зводяться до одного числа.
import numpy as np
sales = np.arange(12).reshape(3, 4)
print(sales.shape)
print(sales.sum(axis=0).shape)
print(sales.sum(axis=1).shape)
print(sales.sum().shape)
Типові помилки¶
Форма без дужок. np.zeros(3, 4) замість np.zeros((3, 4)). Друге число сприймається як dtype, і NumPy повідомляє TypeError: Cannot interpret '4' as a data type.
Очікування цілих від zeros/ones. Вони за замовчуванням створюють float64. Якщо потрібні цілі — dtype=int.
arange із дробовим кроком. Кількість елементів може відрізнятися на один від очікуваної. Для дробових сіток — linspace.
Змішування чисел і рядків. np.array([1, "2", 3]) — масив рядків, а не чисел. Дані з файла спершу перетворюють через astype(float).
Непомічене переповнення. Операції над int8/uint8/int16 мовчки загортаються. Перед обчисленнями, які можуть вийти за межі, перейдіть на ширший тип.
Забутий результат astype. array.astype(int) не змінює array. Правильно: array = array.astype(int).
* як множення матриць. a * b — поелементне множення. Матричний добуток записується a @ b, про нього — у лекції 29.
Присвоєння замість копії. b = a не створює новий масив: обидві змінні вказують на той самий об'єкт, і зміна b змінить a. Незалежна копія — b = a.copy().
import numpy as np
a = np.zeros(3)
b = a
c = a.copy()
b += 5
print("a:", a)
print("b:", b)
print("c:", c)
Цикл замість векторизації. Код for i in range(len(a)): result[i] = a[i] * 2 працює, але повільно й довго читається. У NumPy те саме — result = a * 2. Якщо ви пишете for по елементах масиву, майже завжди є векторизований спосіб.
Підсумок¶
ndarray— однорідний масив фіксованого розміру з суцільним блоком пам'яті. Тому він компактніший за список і швидший у обчисленнях.- Атрибути
ndim,shape,size,dtype,nbytesописують форму й пам'ять масиву. - Створення:
np.arrayзі списків;zeros,ones,full,eye,*_like— заготовки;arange— послідовність із кроком,linspace— задана кількість точок. reshapeзмінює форму без зміни кількості елементів;-1означає «обчисли сам».dtypeвизначає діапазон і розмір елемента. За замовчуванням —int64іfloat64; NumPy обирає найзагальніший тип для всіх елементів.astypeповертає новий масив іншого типу; дробові → цілі відкидають дробову частину.- Цілі типи мають межі й мовчки загортаються при переповненні; дробові мають обмежену точність і порівнюються через
np.isclose. - Оператори й ufunc працюють поелементно, порівняння дають масив
bool, агрегати (sum,mean,argmax) зводять масив до числа або — зaxis— до масиву меншої розмірності.
Корисні посилання¶
- NumPy: офіційна документація
- NumPy: the absolute basics for beginners
- NumPy quickstart
- Array creation
- Data types
- Universal functions (ufunc)
- NumPy: the ndarray
Знайшли помилку чи бажаєте додати інформацію, щоб покращити курс? Створіть issue на GitHub