numpypythonиндексацияboolean-маскианалитика данныхnumpy с нуля

NumPy с нуля. Часть 4: Индексация — срезы, boolean-маски, fancy

2026-06-28 12 мин

Индексация в numpy — это главный навык, ради которого numpy вообще учат. Если вы умеете доставать нужные строки, столбцы и значения по условию — вы умеете фильтровать данные без единого цикла for. В этом уроке разберём четыре способа: срезы (a[1:3]), 2D-доступ (a[1, 2], a[:, 0]), boolean-маски (a[a > 0]) и fancy-индексацию (a[[0, 2, 4]]). Это та самая механика, которая потом один в один работает в pandas (df[df['revenue'] > 0]) — просто там она поверх таблиц, а здесь поверх массивов.

В прошлых частях курса numpy с нуля мы создавали массивы и считали по ним метрики. Теперь научимся выбирать из массива именно те данные, которые нужны: будни из недельной выручки, столбец «вариант B» из A/B-матрицы, только дни с положительным приростом. Для аналитика это ежедневная работа — отфильтровать, отрезать, заменить.

Всё на простых примерах из аналитики: выручка, клики, прирост метрик. В конце — три интерактивных задания в тренажёре, где вы сами напишете срезы и маски, и мостик к следующей части.

Срезы 1D: a[начало:конец:шаг]

Срез (slice) — это «отрезать кусок массива». Синтаксис тот же, что у списков Python: a[start:stop:step]. Главное правило: левая граница включается, правая — нет. То есть a[1:3] берёт элементы с индексами 1 и 2, но не 3.

import numpy as np

a = np.array([10, 20, 30, 40, 50])

print(a[1:3])    # [20 30]   индексы 1 и 2
print(a[:3])     # [10 20 30] с начала до 3 (не включая)
print(a[2:])     # [30 40 50] с индекса 2 до конца
print(a[::2])    # [10 30 50] каждый второй (шаг 2)
print(a[::-1])   # [50 40 30 20 10] разворот (шаг -1)

Если пропустить start — numpy берёт с начала, если пропустить stop — до конца. Отрицательные индексы считают с конца: a[-1] — последний элемент, a[-2:] — два последних.

Для аналитика это сразу полезно. Допустим, в массиве выручка по дням недели (7 значений). Будни — это первые пять, выходные — последние два:

revenue = np.array([120, 135, 150, 90, 200, 260, 240])
print(revenue[:5])   # [120 135 150  90 200] будни
print(revenue[5:])   # [260 240] выходные

Обратите внимание на вывод [120 135 150 90 200]: перед 90 два пробела. numpy выравнивает числа по правому краю под самое широкое значение (здесь 3 цифры), поэтому двузначное 90 получает лишний пробел. Это нормально, печать numpy всегда так делает.

Важно: срез — это view, а не копия

Тут новичков ждёт ловушка. В numpy срез не копирует данные, а создаёт «вид» (view) на тот же кусок памяти. Если вы изменили элемент в срезе — изменится и исходный массив.

a = np.array([10, 20, 30, 40, 50])
b = a[1:3]      # b смотрит на те же данные, что a
b[0] = 999
print(a)        # [ 10 999  30  40  50]  -- a тоже изменился!

Это сделано ради скорости: копировать большие массивы дорого. Но если вам нужна именно независимая копия (чтобы менять её и не трогать оригинал) — берите .copy():

b = a[1:3].copy()
b[0] = 999
print(a)        # [10 20 30 40 50] -- оригинал цел

Правило для аналитика простое: если режете массив только чтобы посчитать (сумму, среднее) — view идеален, ничего копировать не надо. Если режете, чтобы потом менять значения отдельно от оригинала — добавляйте .copy(). В pandas с df ровно та же история и то же предупреждение SettingWithCopyWarning растёт из этого механизма.

Попробуй сам

Задача · Python
Условие
В массиве revenue лежит выручка за 7 дней недели (пн-вс). Достаньте срезами: будни (первые 5 дней) в weekdays, выходные (последние 2 дня) в weekend, и отдельно выручку последнего дня недели (воскресенье) через отрицательный индекс. Выведите каждое через print в этом порядке.
Подсказка к решению — раскрыть только если застрял
Спойлер! Сначала попробуй сам. Срез revenue[0:5] берёт индексы 0..4 (правая граница не входит). Для выходных revenue[5:] — с индекса 5 до конца. Последний элемент — revenue[-1]. Помни: numpy печатает [120 135 150 90 200] с двойным пробелом перед 90, потому что выравнивает по ширине.

2D-индексация: строки и столбцы

Реальные данные аналитика — это таблицы: строки и столбцы. В numpy таблица — это 2D-массив, и доступ к ней идёт через [строка, столбец] — два индекса через запятую.

Возьмём матрицу: 3 канала рекламы (строки) на 4 недели (столбцы), в ячейках — клики.

data = np.array([
    [100, 120, 130,  90],   # канал 0
    [200, 210, 190, 205],   # канал 1
    [ 50,  60,  55,  70],   # канал 2
])

print(data[1, 2])   # 190 -- канал 1, неделя 2 (одно число)

Первый индекс — номер строки, второй — номер столбца. data[1, 2] — это пересечение: вторая строка, третий столбец.

Чтобы взять целый столбец или строку, ставим двоеточие : на место «всё подряд»:

print(data[:, 0])   # [100 200  50] весь столбец 0 (неделя 1 по всем каналам)
print(data[1, :])   # [200 210 190 205] вся строка 1 (канал 1 по всем неделям)
print(data[2])      # [50 60 55 70] строку можно и без ', :' -- то же самое

Запомните мнемонику: data[:, 0] — двоеточие первым значит «все строки», 0 вторым значит «нулевой столбец». Это самый частый приём аналитика: вытащить один признак (столбец) из всей таблицы. Например, столбец «выручка» или столбец «вариант B» из A/B-матрицы:

ab = np.array([
    [0.11, 0.13],   # день 1: [вариант A, вариант B]
    [0.09, 0.15],   # день 2
])
print(ab[:, 1])     # [0.13 0.15] -- конверсия варианта B по дням

Boolean-маски: фильтр по условию

Вот ради этого numpy и pandas вообще существуют. Boolean-маска — это способ сказать «дай мне только те элементы, для которых условие истинно», без всякого цикла.

Сначала сравним массив с числом. Сравнение применяется к каждому элементу и возвращает массив из True/False:

arr = np.array([-3, 7, -1, 5, 0, -8])
print(arr > 0)   # [False  True False  True False False]

Теперь если этот массив масок подставить в квадратные скобки — numpy вернёт только элементы, где стоит True:

print(arr[arr > 0])   # [7 5] -- только положительные

Читается как «возьми из arr те, где arr больше нуля». Для аналитика это фильтрация: только дни с продажами, только пользователи с покупкой, только положительный прирост.

Маски прекрасно сочетаются с агрегатами. Хотите узнать, в скольких днях были клики, и сколько в среднем кликов в активные дни:

clicks = np.array([12, 0, 5, 0, 9])
print(np.sum(clicks > 0))            # 3  -- сколько дней с кликами (True=1)
print(clicks[clicks > 0].mean())     # 8.666... -- среднее по активным дням
np.sum(clicks > 0) работает, потому что True считается за 1, а False за 0 — частый трюк, чтобы посчитать «сколько раз условие выполнилось».

Как boolean-маска выбирает элементы

  arr           [ -3 ][  7 ][ -1 ][  5 ][  0 ][ -8 ]
  индекс           0     1     2     3     4     5
                   |     |     |     |     |     |
  arr > 0          v     v     v     v     v     v
  (маска)       [False][True][False][True][False][False]
                         |           |
                         | оставляем  | оставляем
                         | (True)     | (True)
                         v           v
  arr[arr > 0]        [  7  ]      [  5  ]   ->  [7 5]

  Правило: на каждой позиции True -> элемент берём,
           False -> элемент выбрасываем. Цикл for не нужен.

Маска arr > 0 — это массив той же длины из True/False. Подставляя её в arr[...], numpy оставляет только элементы на позициях True (здесь 7 и 5) и склеивает их в новый массив. Так работает фильтрация без единого цикла — та же механика потом в pandas: df[df['x'] > 0].

Попробуй сам

Задача · Python
Условие
data — матрица кликов: 3 канала рекламы (строки) на 4 недели (столбцы). Выведите через 2D-индексацию: число кликов канала 1 на неделе 2 (data[1,2]), весь столбец недели 1 (индекс 0) по всем каналам, и всю строку канала 2 (индекс 2) по всем неделям. В этом порядке, каждое через print.
Подсказка к решению — раскрыть только если застрял
Спойлер! Сначала попробуй сам. Первый индекс -- строка, второй -- столбец: data[1, 2]. Двоеточие на месте индекса значит 'все': data[:, 0] -- все строки, столбец 0. data[2, :] -- строка 2, все столбцы. Столбец [100 200 50] печатается с двойным пробелом перед 50 из-за выравнивания по ширине.

Сложные условия: & | и обязательные скобки

Часто фильтр — это не одно условие, а несколько: «прирост положительный И не больше 40», «канал A ИЛИ канал B». В numpy для этого свои операторы:

Два правила, которые экономят часы отладки:

delta = np.array([10, -5, 30, -20, 0, 45, -8])

# рост, но не больше 40 -- ОБА условия
print(delta[(delta > 0) & (delta <= 40)])   # [10 30]

# падение ИЛИ ноль (т.е. не было роста)
print(delta[(delta <= 0)])                  # [ -5 -20   0  -8]

# инверсия: НЕ положительные
print(delta[~(delta > 0)])                  # [ -5 -20   0  -8]

Если забыть скобки и написать delta > 0 & delta <= 40, Python сначала выполнит 0 & delta — и всё сломается. Поэтому скобки вокруг каждого сравнения — это не стиль, а необходимость. Привыкайте сразу, потому что в pandas (df[(df.a > 0) & (df.b < 5)]) ровно те же правила.

Fancy-индексация и присваивание по маске

Fancy-индексация (она же «индексация списком») — это когда вы передаёте в скобки список нужных индексов, а numpy достаёт элементы именно в этом порядке.

import numpy as np

q = np.array([100, 200, 300, 400, 500])
print(q[[0, 2, 4]])   # [100 300 500] -- элементы 0, 2 и 4
print(q[[4, 0]])      # [500 100] -- порядок какой указали

В отличие от среза, fancy-индексация ВСЕГДА возвращает копию, а индексы могут повторяться и идти в любом порядке. Это удобно, когда нужно выбрать конкретные строки таблицы — например, топ-3 канала по индексам, которые вы посчитали отдельно.

И самое практичное — присваивание по маске. Можно не только читать по условию, но и записывать. Классика аналитика: заменить отрицательные значения (ошибки данных, возвраты) на ноль.

revenue = np.array([100, -50, 200, -10, 0])
revenue[revenue < 0] = 0
print(revenue)   # [100   0 200   0   0]

Слева маска revenue[revenue < 0] выбирает «бракованные» элементы, справа — значение, которым их заполняем. numpy сам разошлёт 0 по всем подходящим позициям.

Если нужно не обнулить, а выбрать одно из двух — есть np.where(условие, если_да, если_нет). Он не меняет массив, а строит новый:

x = np.array([5, -2, 8, -7])
print(np.where(x < 0, 0, x))   # [5 0 8 0] -- отрицательные -> 0, остальные как есть

Эти три приёма — fancy, присваивание по маске и np.where — покрывают 90% чистки данных, которую аналитик делает каждый день: убрать выбросы, заполнить пропуски, разметить флаги.

Попробуй сам

Задача · Python
Условие
delta — дневной прирост выручки (может быть отрицательным). Сделайте четыре вещи и выведите каждую через print: 1) boolean-маской только дни роста (delta>0); 2) условием с & и скобками рост не больше 40 ((delta>0)&(delta<=40)); 3) fancy-индексацией дни с индексами 0, 2, 5; 4) затем присваиванием по маске обнулите все падения (delta<0 = 0) и выведите итоговый delta.
Подсказка к решению — раскрыть только если застрял
Спойлер! Сначала попробуй сам. Маска: delta[delta > 0]. Для двух условий оборачивай каждое в скобки и соединяй через & : (delta > 0) & (delta <= 40). Fancy -- список индексов в скобках: delta[[0, 2, 5]]. Присваивание: delta[delta < 0] = 0 меняет массив на месте. Итог [10 0 30 0 0 45 0] -- двойные пробелы из-за выравнивания под двузначное 45.

Главное

Частые вопросы

Чем срез отличается от fancy-индексации в numpy?

Срез (arr[2:5]) берёт непрерывный диапазон по правилу start:stop:step и возвращает view — представление на те же данные. Fancy-индексация (arr[[0, 3, 5]]) берёт произвольные позиции по списку индексов и всегда возвращает копию. То есть срез можно менять на месте, и это затронет оригинал, а fancy — нет.

Как обратиться к элементу двумерного массива?

Через запятую: arr[1, 2] — вторая строка, третий столбец. Срезы тоже работают по обеим осям: arr[:, 0] — весь первый столбец, arr[0, :] — вся первая строка, arr[1:3, 0:2] — подматрица. Это удобнее и быстрее, чем arr[1][2] как в списках списков.

Срез numpy возвращает копию или представление?

Срез возвращает view — представление на исходные данные, без копирования. Поэтому sub = arr[0:3]; sub[0] = 99 изменит и оригинал — частый источник неожиданных багов. Если нужна независимая копия, вызови .copy() явно. А вот boolean-маска и fancy-индексация всегда дают копию.

Что такое boolean-маска и как ей фильтровать массив?

Boolean-маска — это массив из True/False той же формы, обычно полученный сравнением: arr > 0. Подставив его в индекс, arr[arr > 0], получишь только элементы, где маска True. Можно и присваивать: arr[arr < 0] = 0 обнулит все отрицательные. Условия комбинируют через &, |, ~ в скобках.

Что такое fancy-индексация и когда она нужна?

Fancy-индексация — это доступ по массиву индексов: arr[[3, 0, 3]] вернёт элементы на позициях 3, 0 и снова 3 — в заданном порядке и даже с повторами. Для 2D можно передать два массива — строки и столбцы. Она всегда возвращает копию и удобна для выборки и переупорядочивания строк. Похожие приёмы разбираем в справочнике pandas.

Закрепи NumPy на практике
Реши Python-задачи аналитика в тренажёре — pandas, numpy, scipy прямо в браузере. Первые 5 — без регистрации.
Открыть Python-тренажёр →