NumPy

NumPy — фундамент научных вычислений на Python: N-мерные массивы, векторизованные операции и быстрая математика без циклов.

Уровень: начальный 19 разделов ⏱ 3–4 часа

1. Что такое NumPy

NumPy добавляет в Python тип ndarray — многомерный массив фиксированного типа. Операции над массивами выполняются в миллионы раз быстрее циклов на чистом Python, потому что работают на уровне C без интерпретатора.

Над NumPy построены Pandas, Scikit-Learn, TensorFlow и почти вся аналитика.

2. Установка

pip install numpy

В дистрибутиве Miniconda/Anaconda numpy уже есть.

import numpy as np
print(np.__version__)

Традиционный псевдоним пакета — np.

3. Создание массивов

a = np.array([1, 2, 3])          # из списка
m = np.array([[1, 2], [3, 4]])   # матрица 2x2
z = np.zeros((3, 2))             # нули
o = np.ones(5)                   # единицы
e = np.eye(3)                    # единичная матрица
r = np.arange(0, 10, 2)          # [0, 2, 4, 6, 8]
l = np.linspace(0, 1, 5)         # 5 точек от 0 до 1

Форма массива — свойство .shape, число элементов — .size, тип — .dtype.

4. Индексация и слайсы

a = np.array([10, 20, 30, 40])
a[0]        # 10
a[-1]       # 40
a[1:3]      # [20, 30]
a[::2]      # [10, 30]

m = np.array([[1, 2, 3], [4, 5, 6]])
m[1, 2]     # 6 (строка 1, столбец 2)
m[:, 0]     # [1, 4]
m[0, :]     # [1, 2, 3]

Логическая индексация возвращает элементы по условию:

a[a > 25]   # [30, 40]

5. Формы и изменение размера

a = np.arange(12)
a.reshape(3, 4)    # 3x4
a.flatten()        # в 1D
a.T                # транспонирование
np.concatenate([a, a])   # склейка
np.stack([a, a], axis=1) # по новой оси
reshape(-1) самостоятельно вычисляет размер: a.reshape(-1) — переход к одному измерению.

6. Универсальные функции (ufunc)

Арифметика и тригонометрия применяются поэлементно и быстро:

a = np.array([1, 2, 3])
a + 10        # [11, 12, 13]
np.sqrt(a)    # корни
np.exp(a)     # экспонента
np.sin(a)     # синус
np.sum(a)     # сумма
np.mean(a)    # среднее

Статистика: min, max, std, var, argmax.

7. Трансляции (Broadcasting)

NumPy умеет совмещать массивы разных форм по правилам трансляции:

m = np.arange(9).reshape(3, 3)   # 3x3
v = np.array([1, 2, 3])          # 1D
m + v                            # v "размазан" по строкам
m * 2                            # скаляр по всем элементам
Трансляция избавляет от циклов: вычитание строки из матрицы — одна строка кода вместо двух вложенных for.

8. Линейная алгебра

a = np.array([[2, 1], [1, 3]])
b = np.array([8, 13])

np.linalg.det(a)        # определитель
np.linalg.inv(a)        # обратная матрица
np.linalg.eig(a)        # собственные значения
np.linalg.solve(a, b)   # решение системы

Скалярное и матричное произведение — np.dot(a, b) или оператор @.

9. Маски и where

data = np.array([3, 15, 7, 20, 2])
is_big = data > 10           # булева маска
data[is_big]                 # [15, 20]
np.where(data > 10, 1, 0)    # замена по условию

Фильтрация выбросов: data[(data > 5) & (data < 15)].

10. Случайные числа

rng = np.random.default_rng(42)       # генератор с seed
rng.random((3, 3))                    # равномерное [0,1)
rng.normal(0, 1, 1000)                # нормальное распределение
rng.integers(1, 7, size=10)           # целые
Современный стиль — default_rng вместо старого np.random.seed; так результаты воспроизводимы и безопасны.

11. NumPy в реальных задачах

Массивы NumPy — «топливо» соседних библиотек:

Производительность: заменяй циклы for по спискам на векторные операции NumPy — ускорение в 10–100 раз.

12. Векторизация вместо циклов

NumPy выигрывает за счёт выполнения операций сразу над всем массивом в C-коде, минуя Python-интерпретатор. Циклы по элементам в Python — самая частая причина медленных программ.

import numpy as np

data = np.linspace(0, 100, 1_000_000)

# Медленно: цикл по элементам
slow = [x * 2 + 1 for x in data]

# Быстро: векторизация
fast = data * 2 + 1
Векторизованный код читается как алгебра и почти всегда ускоряет вычисления в десятки и сотни раз. Если написан цикл по элементам массива — ищите векторную альтернативу.
Булевы маски заменяют ветвления: res = np.where(data > 50, data * 2, data) обходит условие без if по элементам.

13. Типы данных и память

Массив NumPy хранит однотипные значения с фиксированным типом dtype — это компактно и предсказуемо. Тип влияет на точность и занимаемую память.

dtypeБайтДиапазон
int324±2,1 млрд
float324семь значащих цифр
float648точность по умолчанию
bool10 или 1
arr = np.array([1.0, 2.0, 3.0], dtype=np.float32)
print(arr.dtype, arr.nbytes)  # 12 байт вместо 24

big = np.arange(10_000, dtype=np.int8)
print(big.dtype)  # экономим память на маленьких значениях
float32 теряет точность на больших числах и накопленных суммах. Перед расчётами с деньгами, координатами и метками времени используйте float64, иначе получите незаметные смещения.

14. Работа с большими данными: memory-mapped файлы

Файлы в сотни гигабайт не влезают в оперативную память. NumPy умеет читать их по частям через memory-mapping — файл и массив ведут себя одинаково, но данные подгружаются с диска лениво.

# Запись большого массива на диск
big = np.memmap('big.dat', mode='w+', dtype=np.float64,
                shape=(100_000, 100_000))
big[:1000] = np.random.rand(1000, 100_000)
del big  # flush на диск

# Чтение без загрузки целиком
mm = np.memmap('big.dat', dtype=np.float64,
               shape=(100_000, 100_000))
print(mm[5000, 2000])  # доступная строка без загрузки всего файла
MMAP идеален для пагинации и stream-обработки: перебирайте массив поблочно с np.lib.stride_tricks.sliding_window_view или нарезкой mm[i:j]. Ограничение — зависимость от локального диска, для сетевых данных используйте Apache Arrow.

15. Интеграция NumPy и pandas

pandas основан на NumPy: колонка DataFrame — это массив с dtype. Перевод данных между ними почти бесплатен, а векторные операции NumPy напрямую работают с колонками.

import pandas as pd
import numpy as np

df = pd.DataFrame({'x': np.arange(10), 'y': np.random.rand(10)})

# Массив из колонки
arr = df['y'].to_numpy()

# Векторная операция с сохранением индекса
df['normalized'] = (df['y'] - arr.mean()) / arr.std()

print(np.corrcoef(df['x'].to_numpy(), df['y'].to_numpy()))
Смешивайте подходы: группировки и джойны — у pandas, тяжёлую математику (скаляры, матричные умножения, быстрая статистика) — у NumPy. Передавайте данные якобы односторонне через to_numpy(), чтобы не копировать случайно.
Не вызывайте NumPy-функции на DataFrame целиком — сначала конвертируйте колонки в массив. Иначе pandas без предупреждения применит функцию поэлементно и результат будет на порядок медленнее.

16. Даты и время: datetime64

Тип datetime64 хранит даты компактно и позволяет арифметику с временными интервалами: вычитание даёт timedelta64, а смещение на неделю выполняется добавлением константы. В отличие от Python-объектов datetime, массивы дат в NumPy работают векторно.

import numpy as np

dates = np.array(['2026-09-19', '2026-09-20'],
                 dtype='datetime64[D]')
week_later = dates + np.timedelta64(7, 'D')
print(dates, week_later)

# рабочие дни до конца месяца (business days)
biz = np.busday_count(dates[0], '2026-09-30')
print(biz)

Единицы задаются кодом: D (дни), M (месяцы), Y — только годов, us/ns для индексирования с микро- и наносекундной точностью. Ключевой нюанс: месяцы и годы не кратны суткам, поэтому их смещение может вызвать ошибку округления при преобразовании.

КодЕдиницаПример
Dденьдат индексации без времени
Mмесяцагрегации по периодам
nsнаносекундалоги событий и сенсоров
usмикросекундавысокочастотные временные ряды
Для чтения дат в pandas достаточно передать в колонку массив datetime64: pandas сами приведут к своему типу datetime64 с наносекундами.
При конвертации между единицами с дробной частью (например, D → M) NumPy не округляет, а может выдать ошибку для неполных интервалов — явно контролируйте точность.

17. Fancy-индексация и сортировка массивов

Fancy-индексация обращается к массиву по списку индексов или булевой маске. Первая копирует данные в новом порядке (важно для понимания, это не срез), вторая выбирает элементы по условию — мощный инструмент фильтрации без циклов.

import numpy as np

a = np.array([10, 20, 30, 40])
idx = np.array([3, 0, 2])          # перестановка
print(a[idx])                       # [40 10 30]

mask = a > 20
print(a[mask])                      # [30 40]

# порядок по двум ключам
people = np.array(['Аня', 'Боря', 'Витя'])
ages = np.array([30, 25, 25])
order = np.lexsort((people, ages))
print(order, people[order])

Сортировка по значению — np.sort (новая копия) или a.sort() (на месте). Когда нужно вернуть порядок индексов для повторного применения, используйте np.argsort, а для удаления дубликатов — np.unique.

Fancy-индексация возвращает копию, а не представление: изменения в ней не отразятся на исходном массиве — в отличие от срезов.

18. View vs copy: как не удвоить память

Работа с большими массивами упирается в память. Срезы и многие операции возвращают view — представление, разделяющее буфер данных с исходником. Копия создаётся только при явном copy() или при изменении упаковки/шага. Проверить, делятся ли массивы памятью, помогает np.shares_memory.

import numpy as np

m = np.arange(100_000).reshape(100, 1000)
row = m[10]                # view: без копирования
copy_row = m[10].copy()   # явная копия

print(np.shares_memory(m, row))     # True
print(np.shares_memory(m, copy_row))  # False

row[0] = -1
print(m[10, 0])            # -1: исходник изменился!

Чтобы ускорить тяжёлые расчёты, либо пишите на чистом NumPy (векторизация вместо циклов), либо переключайте критичные функции на Numba/численные библиотеки. Перед передачей данных в pandas или другую библиотеку решайте сознательно: нужна ли независимая копия или разделённое представление достаточно.

Случайное изменение view рискует «испортить» исходные данные незаметно — например, в долго работающем аналитическом скрипте.
Наблюдайте рост памяти через a.nbytes и профилировщик; цепочка вложенных срезов всё равно указывает на один и тот же буфер.

19. Численная устойчивость и пропуски (NaN)

На больших данных встречаются пропуски и крайние значения. NumPy зарезервировал NaN и Inf для таких случаев, а агрегаты с префиксом nan (nanmean, nanstd) игнорируют пропуски. Сравнение значений «в лоб» здесь не работает: nan == nan даст False, поэтому проверка делается через np.isnan.

import numpy as np

data = np.array([1.0, np.nan, 3.0, 5.0])

print(np.mean(data))             # nan: пропуск портит среднее
print(np.nanmean(data))          # 3.0, пропуск пропущен
print(np.isnan(data).any())      # True

# устойчивый softmax: вычесть максимум перед exp
x = np.array([1000.0, 1001.0])
z = x - x.max()
print(np.exp(z) / np.exp(z).sum())

Переполнение — тихая беда: np.exp(1000) вернёт inf, а не исключение. Правило практика: всякий раз, когда в формуле есть экспонента или деление на близкую к нулю величину, стабилизируйте вычисления — нормируйте, сдвигайте или логарифмируйте.

NaN в индексах или ключах группировок ломает сравнение и сортировку — приведите данные к чистому виду до перехода в pandas или на графики.

Что дальше

Дальше — таблицы Pandas и визуализация Matplotlib. Всё это удобно собирать в Jupyter Notebook.

← Вернуться к курсам