Индексация в 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 растёт из этого механизма.
Попробуй сам
Подсказка к решению — раскрыть только если застрял
Срез 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].
Попробуй сам
Подсказка к решению — раскрыть только если застрял
Первый индекс -- строка, второй -- столбец: data[1, 2]. Двоеточие на месте индекса значит 'все': data[:, 0] -- все строки, столбец 0. data[2, :] -- строка 2, все столбцы. Столбец [100 200 50] печатается с двойным пробелом перед 50 из-за выравнивания по ширине.Сложные условия: & | и обязательные скобки
Часто фильтр — это не одно условие, а несколько: «прирост положительный И не больше 40», «канал A ИЛИ канал B». В numpy для этого свои операторы:
&— это «И» (оба условия должны быть истинны)|— это «ИЛИ» (хотя бы одно истинно)~— это «НЕ» (инвертировать маску)
Два правила, которые экономят часы отладки:
- Используйте
&и|, а НЕ словаand/or. Питоновскиеand/orна массивах numpy выдадут ошибкуambiguous truth value. - Каждое условие оборачивайте в скобки. У
&и|приоритет выше, чем у>и<, поэтому без скобок 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% чистки данных, которую аналитик делает каждый день: убрать выбросы, заполнить пропуски, разметить флаги.
Попробуй сам
Подсказка к решению — раскрыть только если застрял
Маска: delta[delta > 0]. Для двух условий оборачивай каждое в скобки и соединяй через & : (delta > 0) & (delta <= 40). Fancy -- список индексов в скобках: delta[[0, 2, 5]]. Присваивание: delta[delta < 0] = 0 меняет массив на месте. Итог [10 0 30 0 0 45 0] -- двойные пробелы из-за выравнивания под двузначное 45.Главное
- Срез a[start:stop:step] включает левую границу и НЕ включает правую: a[1:3] -- это индексы 1 и 2.
- Срез в numpy -- это view (тот же кусок памяти), а не копия. Меняете срез -- меняется оригинал. Нужна независимость -- берите .copy().
- 2D-доступ -- через запятую [строка, столбец]. Двоеточие значит 'все': a[:, 0] -- весь столбец 0, a[1, :] -- вся строка 1.
- Boolean-маска a[a > 0] возвращает элементы, где условие истинно -- фильтрация без цикла for.
- В сложных условиях используйте & | ~ (не and/or) и оборачивайте каждое сравнение в скобки: (a > 0) & (a <= 40).
- Fancy-индексация a[[0, 2, 4]] выбирает элементы по списку индексов в любом порядке и всегда возвращает копию.
- Присваивание по маске a[a < 0] = 0 -- стандартный способ чистки данных: обнулить выбросы, заменить ошибки.
Частые вопросы
Чем срез отличается от 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.