NumPy — это фундамент, на котором стоит весь анализ данных в Python: pandas, scipy, sklearn внутри хранят числа именно как numpy-массивы. В этой части ты разберёшься с главным объектом библиотеки — ndarray (n-мерный массив), научишься создавать его разными способами и понимать три ключевых свойства любого массива: тип данных (dtype), форму (shape) и направление операций (axis). Это база, без которой следующие части курса будут магией.
Если ты уже прошёл курсы по SQL и pandas — отлично, тут многое ляжет на знакомую почву. Колонка в pandas (Series) под капотом — это numpy-массив. SQL-агрегаты типа SUM по группам — это то же, что суммирование по оси в numpy. Мы идём на уровень ниже pandas: туда, где живёт «голая» скорость и где аналитику важно понимать, что происходит с числами на самом деле.
Зачем это аналитику? Когда ты считаешь метрики на миллионах строк, гоняешь A/B-тесты, бутстрапишь доверительные интервалы или векторизуешь вычисления вместо медленных циклов — везде под капотом numpy. Освоив numpy с нуля, ты перестанешь бояться shape mismatch, научишься думать массивами целиком, а не по одному элементу, и будешь писать код, который работает в сотни раз быстрее обычных списков Python.
Что такое NumPy и зачем он аналитику
NumPy (Numerical Python) — это библиотека для быстрых вычислений над числами. Её сердце — объект ndarray: контейнер для чисел одного типа, разложенных в строгую сетку (одномерную, двумерную или больше).
По традиции numpy импортируют под псевдонимом np. Так делают все в мире данных — увидишь np.что-то в чужом коде, знай, это numpy.
import numpy as np
revenue = np.array([1200, 890, 3400, 560])
print(revenue)
Выведется так (обрати внимание: пробелы между числами, без запятых — это стиль печати numpy, он отличается от обычного списка Python):
[1200 890 3400 560]
Почему не обычный список Python? Списки гибкие, но медленные: в них могут лежать числа, строки, другие списки вперемешку, и Python тратит силы на проверку типа каждого элемента. numpy-массив хранит числа одного типа подряд в памяти — как один монолитный блок. Это даёт два больших выигрыша.
Память. Список из миллиона чисел в Python — это миллион отдельных объектов плюс массив указателей на них. numpy-массив тех же чисел — один компактный кусок памяти. Разница в разы.
Скорость. Операции в numpy выполняются над всем массивом сразу на уровне оптимизированного C-кода, без медленного Python-цикла. Это называется векторизацией.
import numpy as np
prices = np.array([100, 250, 90, 400])
# умножаем ВЕСЬ массив на 1.2 одной операцией — никакого цикла
with_vat = prices * 1.2
print(with_vat)
[120. 300. 108. 480.]
Сравни мысленно с обычным списком: там пришлось бы писать [p * 1.2 for p in prices] — цикл, который на миллионе элементов будет ползти. numpy делает то же самое в сотни раз быстрее, потому что думает не элементами, а массивом целиком. Этот «массовый» способ мышления — главное, к чему привыкаешь в numpy.
Создание массивов: array, arange, zeros, ones, full
Массив можно создать руками из списка, а можно сгенерировать по правилу. Разберём базовые способы — они встречаются постоянно.
Из списка через np.array. Самый прямой путь: передаёшь список (или вложенные списки) — получаешь массив.
import numpy as np
# одномерный — выручка по дням
daily = np.array([120, 340, 90, 510])
# двумерный — таблица: список списков
matrix = np.array([
[120, 340],
[90, 510],
[60, 410]
])
print(matrix)
[[120 340]
[ 90 510]
[ 60 410]]
Двумерный массив печатается с переносами строк — каждая внутренняя строка списка становится строкой таблицы. Числа выравниваются по правому краю, поэтому 90 и 60 получают лидирующий пробел.
np.arange — числовая последовательность. Работает как range в Python, но возвращает массив. Аргументы: start, stop, step (stop не включается).
import numpy as np
print(np.arange(5)) # от 0 до 5, шаг 1
print(np.arange(0, 10, 2)) # от 0 до 10, шаг 2
[0 1 2 3 4]
[0 2 4 6 8]
np.zeros, np.ones, np.full — заготовки. Часто нужен массив-болванка нужного размера: для накопления результата, для маски, для инициализации. Тут пригодятся:
import numpy as np
print(np.zeros(4)) # четыре нуля (по умолчанию float)
print(np.ones(3)) # три единицы
print(np.full(3, 7)) # три семёрки — заполни значением 7
[0. 0. 0. 0.]
[1. 1. 1.]
[7 7 7]
Заметь точку после нулей и единиц: 0. и 1. — это float. По умолчанию zeros и ones создают вещественные числа. А full(3, 7) дал целые, потому что 7 — целое.
Для двумерных заготовок размер передаётся кортежем (строки, колонки):
import numpy as np
print(np.zeros((2, 3))) # матрица 2 строки на 3 колонки из нулей
[[0. 0. 0.]
[0. 0. 0.]]
Попробуй сам
Подсказка к решению — раскрыть только если застрял
Атрибуты пишутся БЕЗ скобок: revenue.dtype, revenue.shape, revenue.ndim, revenue.size. А вот агрегаты — это методы, со скобками: revenue.sum(), revenue.mean(). Форма одномерного массива печатается как (6,) — кортеж из одного числа с висящей запятой. Среднее тут выходит с длинным хвостом 1486.6666666666667 — это нормально, 8920 не делится на 6 нацело.
Решение:
print("Тип:", revenue.dtype)
print("Форма:", revenue.shape)
print("Размерность:", revenue.ndim)
print("Элементов:", revenue.size)
print("Сумма:", revenue.sum())
print("Среднее:", revenue.mean())linspace — равные интервалы для шкал и графиков
np.arange задаёт последовательность через шаг. Но часто нужно другое: «дай мне ровно N точек между A и B». Для этого есть np.linspace(start, stop, num) — он делит отрезок на равные части, включая оба конца.
import numpy as np
# 5 точек от 0 до 1 включительно
grid = np.linspace(0, 1, 5)
print(grid)
[0. 0.25 0.5 0.75 1. ]
Разница с arange принципиальная: в arange ты задаёшь шаг и не всегда знаешь, сколько точек получишь и попадёшь ли в конец. В linspace ты задаёшь количество точек и гарантированно получаешь и start, и stop. Поэтому linspace — выбор, когда нужна равномерная шкала.
Где это нужно аналитику:
- Оси графиков. Подписи делений, равномерная сетка по X.
- Перцентили и квантили. Нужно 11 точек от 0 до 1 для построения квантилей —
np.linspace(0, 1, 11). - Сетка параметров. Перебор значений гипотезы: 20 равноотстоящих порогов от 0 до 100.
import numpy as np
# 5 равных отметок выручки от 0 до 100 тысяч
scale = np.linspace(0, 100, 5)
print(scale)
[ 0. 25. 50. 75. 100.]
Результат linspace всегда float — даже если концы целые, ведь деление на части обычно даёт дроби.
dtype — тип данных, и почему он важен
Все элементы numpy-массива имеют один общий тип данных — dtype. Это не каприз библиотеки, а основа её скорости: зная, что все числа одного типа и одного размера, numpy раскладывает их в памяти подряд и обрабатывает максимально быстро.
Основные типы, которые встретит аналитик:
int64— целые числа (количество заказов, id, штуки).float64— числа с дробной частью (суммы, средние, доли). Сюда же попадаетNaN(пропуск).bool—True/False(флаги, маски для фильтрации).
Посмотреть тип можно через атрибут .dtype:
import numpy as np
orders = np.array([10, 25, 9, 40])
print(orders.dtype)
int64
numpy сам выбирает тип по данным. Если хоть один элемент дробный — весь массив становится float64, потому что тип общий и должен вместить всё:
import numpy as np
mixed = np.array([1, 2, 3.5])
print(mixed)
print(mixed.dtype)
[1. 2. 3.5]
float64
Целые 1 и 2 превратились в 1. и 2. (с точкой), потому что весь массив теперь float64.
Тип можно задать явно при создании через параметр dtype, или поменять у готового массива через .astype:
import numpy as np
# явно целые единицы вместо float по умолчанию
ones_int = np.ones(3, dtype=int)
print(ones_int) # [1 1 1]
# округление вниз при переводе float -> int (дробная часть отбрасывается)
floats = np.array([1.9, 2.1, 3.7])
print(floats.astype(int)) # [1 2 3]
[1 1 1]
[1 2 3]
Почему dtype критичен для аналитика? От него зависит, что вообще можно сделать с массивом и сколько памяти он съест. Если суммы заказов случайно загрузились как строки (object/str) — mean() либо упадёт, либо склеит строки вместо сложения. Половина «странных багов» новичка — это неправильный dtype. Поэтому после загрузки данных всегда полезно глянуть .dtype.
Ещё одна частая штука — булев тип как маска. True при суммировании считается за 1, и это удобный способ посчитать «сколько элементов удовлетворяют условию»:
import numpy as np
paid = np.array([True, False, True, True])
print(paid.sum()) # 3 — сколько True
3
Попробуй сам
Подсказка к решению — раскрыть только если застрял
reshape(4, 3) превращает 12 чисел в 4 строки по 3. Запомни: axis=0 схлопывает строки (идёт вниз) — получаешь сумму по КАЖДОМУ каналу (3 числа). axis=1 схлопывает колонки (идёт вправо) — получаешь сумму по КАЖДОЙ неделе (4 числа). Числа в выводе выравниваются по правому краю, поэтому у 90 и 60 появляется лишний пробел. Пустые строки делаются через print() без аргументов.
Решение:
matrix = flat.reshape(4, 3)
print("Матрица 4x3 (4 недели, 3 канала):")
print(matrix)
print()
print("Форма:", matrix.shape)
print()
print("Сумма по каналам (axis=0):", matrix.sum(axis=0))
print("Сумма по неделям (axis=1):", matrix.sum(axis=1))shape, ndim, size и reshape — форма массива
У каждого массива есть форма — сколько в нём измерений и сколько элементов вдоль каждого. Это описывают три атрибута (заметь — без скобок, это свойства, а не методы):
.shape— кортеж с размерами по каждому измерению:(строки, колонки)..ndim— число измерений (1 для вектора, 2 для таблицы)..size— общее число элементов.
import numpy as np
sales = np.array([
[120, 340, 90],
[510, 280, 175]
])
print(sales.shape) # (2, 3) — 2 строки, 3 колонки
print(sales.ndim) # 2 — двумерный
print(sales.size) # 6 — всего элементов
(2, 3)
2
6
Для одномерного массива форма выглядит как (N,) — кортеж из одного числа с висящей запятой. Это нормально, так Python отличает кортеж из одного элемента от обычных скобок:
import numpy as np
flat = np.array([1, 2, 3, 4, 5])
print(flat.shape) # (5,)
print(flat.ndim) # 1
(5,)
1
reshape — поменять форму, не трогая данные. Очень частая операция: взять плоский массив и разложить его в таблицу нужного размера. Главное правило — общее число элементов должно сохраняться (5 элементов нельзя разложить в 2x3=6 ячеек).
import numpy as np
flat = np.arange(12) # [0 1 2 ... 11], 12 элементов
matrix = flat.reshape(3, 4) # 3 строки на 4 колонки = 12
print(matrix)
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
Есть удобный приём: подставь -1 вместо одного из размеров, и numpy сам вычислит его. «Разложи в 3 строки, а сколько колонок — посчитай сам»:
import numpy as np
flat = np.arange(12)
print(flat.reshape(3, -1)) # numpy сам поймёт: 4 колонки
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
Это спасает, когда длину знаешь, а делитель считать лень или он динамический.
axis — направление операций (0 — строки, 1 — столбцы)
Самая частая путаница новичков в numpy (и в pandas тоже) — это axis. Когда ты считаешь сумму или среднее у двумерного массива, нужно сказать, в каком направлении агрегировать. Это и задаёт axis.
Правило простое, но его надо запомнить намертво:
axis=0— идём ВНИЗ по строкам, схлопываем строки. Результат — по одному числу на КОЛОНКУ.axis=1— идём ВПРАВО по колонкам, схлопываем колонки. Результат — по одному числу на СТРОКУ.
Мнемоника: axis указывает на то измерение, которое исчезает. axis=0 убирает измерение строк → остаются колонки. axis=1 убирает колонки → остаются строки.
Представь таблицу: строки — недели, колонки — каналы продаж.
import numpy as np
# строки = 3 недели, колонки = 2 канала (сайт, приложение)
sales = np.array([
[120, 80],
[200, 150],
[90, 110]
])
print("Всего по каждому каналу:", sales.sum(axis=0))
print("Всего по каждой неделе:", sales.sum(axis=1))
Всего по каждому каналу: [410 340]
Всего по каждой неделе: [200 350 200]
Разберём. sum(axis=0) сложил числа сверху вниз внутри каждой колонки: 120+200+90=410 (сайт) и 80+150+110=340 (приложение). Строки-недели схлопнулись, осталось по числу на канал.
sum(axis=1) сложил числа слева направо внутри каждой строки: 120+80=200, 200+150=350, 90+110=200. Колонки-каналы схлопнулись, осталось по числу на неделю.
Если axis вообще не указать — numpy схлопнет весь массив в одно число:
import numpy as np
sales = np.array([[120, 80], [200, 150], [90, 110]])
print(sales.sum()) # всё вместе: 750
750
Эта же логика работает для mean, max, min, std и других агрегатов. Связь с тем, что ты уже знаешь: axis=0 — это как GROUP BY по колонке в SQL (агрегат на каждую категорию-колонку), а суммирование без axis — как простой SELECT SUM(...) по всей таблице. В pandas тот же axis управляет направлением df.sum(axis=...) — выучив его тут, ты получишь его бесплатно в pandas.
shape (2, 3) и направления axis на 2D-массиве
axis=1 --> идём ВПРАВО по колонкам
(схлопываем колонки, итог на КАЖДУЮ строку)
col0 col1 col2
+------+------+------+
row0 | 120 | 340 | 90 | --> sum(axis=1) = 550
axis=0 +-------+------+------+------+
| row1 | 510 | 280 | 175 | --> sum(axis=1) = 965
v +-------+------+------+------+
идём sum sum sum
ВНИЗ axis=0 axis=0 axis=0
по строк. = 630 = 620 = 265
(схлоп.
строки,
итог на
КАЖДУЮ
колонку)
shape = (2, 3) -> (строки=2, колонки=3)
^axis 0 ^axis 1
Правило: axis указывает на ИСЧЕЗАЮЩЕЕ измерение.
axis=0 убирает строки -> остаются 3 числа (по колонкам).
axis=1 убирает колонки -> остаются 2 числа (по строкам).
shape=(2,3) читается как (строки, колонки): позиция 0 в кортеже — это axis=0 (строки), позиция 1 — axis=1 (колонки). sum(axis=0) идёт вниз и схлопывает строки, давая по числу на каждую колонку; sum(axis=1) идёт вправо и схлопывает колонки, давая по числу на каждую строку. Ось, указанная в axis, — это та, что исчезает из результата.
Попробуй сам
Подсказка к решению — раскрыть только если застрял
По умолчанию zeros и linspace дают float — числа печатаются с точкой (0., 0.25). А ones мы заставили быть целым через dtype=int, поэтому единицы без точки. arange(0, 10, 2) НЕ включает 10 (стоп не входит), даёт [0 2 4 6 8]. linspace(0, 1, 5), наоборот, включает оба конца — 0 и 1 — и делит на 5 равных точек. Обрати внимание на выравнивание: в linspace после 0., 0.5, 1. идут пробелы для ровных колонок.
Решение:
zeros = np.zeros(5)
ones = np.ones(3, dtype=int)
full = np.full(4, 100)
seq = np.arange(0, 10, 2)
grid = np.linspace(0, 1, 5)
print("zeros:", zeros)
print("ones:", ones)
print("full:", full)
print("arange:", seq)
print("linspace:", grid)Главное
- ndarray — главный объект numpy: контейнер чисел одного типа, уложенных в строгую сетку; быстрее и компактнее списков Python за счёт векторизации
- Создавай массивы через np.array (из списка), np.arange (шаг), np.zeros/ones/full (заготовки), np.linspace (N равных точек, включая оба конца)
- dtype — общий тип всех элементов (int64/float64/bool). Один дробный элемент делает весь массив float64. Тип задаётся через dtype= или меняется через .astype()
- shape (форма), ndim (число измерений), size (всего элементов) — атрибуты без скобок. reshape меняет форму без потери данных, -1 даёт numpy досчитать размер
- axis=0 схлопывает строки → результат на каждую колонку; axis=1 схлопывает колонки → результат на каждую строку. axis указывает на исчезающее измерение
- Числовой формат печати numpy: пробелы вместо запятых, float с точкой (0.), 2D-массивы с переносами строк и выравниванием по правому краю
Частые вопросы
Чем ndarray отличается от обычного списка Python?
ndarray хранит элементы одного типа (dtype) в непрерывном блоке памяти, поэтому операции над ним идут в разы быстрее и занимают меньше памяти, чем список. Список может держать что угодно вперемешку, но каждый элемент — отдельный объект с накладными расходами. Плюс над массивом работают векторные операции без циклов.
Что означают axis=0 и axis=1 в numpy?
axis=0 — это движение по строкам вниз (вдоль первой оси), поэтому arr.sum(axis=0) схлопывает строки и даёт сумму по каждому столбцу. axis=1 — движение по столбцам вправо, sum(axis=1) даёт сумму по каждой строке. Правило простое: ось, указанная в axis, «исчезает» в результате. Это любимый вопрос на собеседовании — похожие разбираем в банке вопросов.
В чём разница между arange и linspace?
np.arange(0, 10, 2) задаёт шаг между значениями (0, 2, 4, 6, 8), а правый конец не включается. np.linspace(0, 10, 5) задаёт количество точек — 5 штук равномерно от 0 до 10 включительно. Когда важен шаг — бери arange, когда важно число точек (например, для оси графика) — linspace.
Что такое dtype и зачем за ним следить?
dtype — это тип элементов массива: int64, float64, bool, object и другие. От него зависят точность и память: int8 займёт в 8 раз меньше int64. Частая ловушка — целочисленный массив при делении молча становится float, а NaN возможен только во float, поэтому колонка с пропусками не бывает чистым int.
Что делает reshape и что значит -1 внутри него?
reshape меняет форму массива без изменения самих данных — например, 12 элементов в матрицу 3x4. Число элементов должно совпадать, иначе будет ошибка. -1 означает «посчитай сам»: arr.reshape(-1, 4) попросит numpy подобрать число строк так, чтобы вышло 4 столбца. Удобно, когда не хочешь считать размерность вручную.