numpyndarraynumpy для аналитикааналитика данныхPython для анализаосновы numpy

NumPy с нуля. Часть 1: ndarray — массивы, dtype, shape, axis

2026-06-28 15 мин

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.]]

Попробуй сам

Задача · Python
Условие
Дан массив дневной выручки за 6 дней. Узнай о нём всё: тип данных (dtype), форму (shape), размерность (ndim), число элементов (size), а также сумму и среднее.
Подсказка к решению — раскрыть только если застрял
Спойлер! Сначала попробуй сам. Атрибуты пишутся БЕЗ скобок: 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 — выбор, когда нужна равномерная шкала.

Где это нужно аналитику:

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 раскладывает их в памяти подряд и обрабатывает максимально быстро.

Основные типы, которые встретит аналитик:

Посмотреть тип можно через атрибут .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

Попробуй сам

Задача · Python
Условие
Дан плоский массив из 12 чисел — выручка за 4 недели по 3 каналам подряд. Разложи его в матрицу 4x3 через reshape, затем посчитай сумму по каналам (axis=0) и сумму по неделям (axis=1).
Подсказка к решению — раскрыть только если застрял
Спойлер! Сначала попробуй сам. 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 — форма массива

У каждого массива есть форма — сколько в нём измерений и сколько элементов вдоль каждого. Это описывают три атрибута (заметь — без скобок, это свойства, а не методы):

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 указывает на то измерение, которое исчезает. 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, — это та, что исчезает из результата.

Попробуй сам

Задача · Python
Условие
Создай пять массивов разными способами-конструкторами: zeros, ones (целые), full, arange и linspace. Выведи каждый, чтобы увидеть разницу в формате печати (float с точкой против int).
Подсказка к решению — раскрыть только если застрял
Спойлер! Сначала попробуй сам. По умолчанию 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 отличается от обычного списка 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 столбца. Удобно, когда не хочешь считать размерность вручную.
Закрепи NumPy на практике
Реши Python-задачи аналитика в тренажёре — pandas, numpy, scipy прямо в браузере. Первые 5 — без регистрации.
Открыть Python-тренажёр →