Многомерные массивы и оси: reshape, axis и работа с матрицами
Форма массива — это взгляд на данные: reshape и магия -1, оси axis на пальцах, транспонирование T, матричное умножение @ и склейка vstack/hstack.
Редакция Питоники
Отчёт по продажам пришёл одним длинным столбцом: 150 чисел — пять магазинов, тридцать дней. В таком виде не видно ни худшего дня, ни лидера недели. Хочется разложить эти числа в таблицу «магазин на день» — и вдруг всё станет видно. Именно это делает numpy reshape: берёт те же байты и меняет взгляд на них. Форма многомерного массива — не про то, где лежат данные, а про то, как мы на них смотрим. В этом уроке научимся менять форму без копирования, понимать оси axis, транспонировать таблицы, умножать матрицы и склеивать массивы из кусков.
Как работает reshape: форма без копирования данных
У метода reshape одна работа: взять существующие данные и выдать на них другой взгляд. Числа при этом не пересчитываются и не перемещаются — меняется только shape. Жёсткое требование одно: количество элементов должно сходиться. Двенадцать чисел можно представить как 3×4, как 4×3, как 2×6 — а вот в 5×3 не получится: там нужно пятнадцать слотов, а у нас двенадцать.
import numpy as np
sales = np.arange(1, 13) # двенадцать чисел подряд
table = sales.reshape(3, 4) # три строки, четыре столбца
print(table)
print(table.shape)
[[ 1 2 3 4] [ 5 6 7 8] [ 9 10 11 12]] (3, 4)
Что возвращает reshape в NumPy — копию или вид?
Что произошло под капотом? NumPy не создавал новую память. Он отдал вам вид (view) на тот же блок байтов, просто с другой системой координат: теперь первый индекс — строка, второй — столбец. Мы уже встречались с view при срезах: это не копия данных, а окно в них. Отсюда следствие, которое экономит и память, и время: reshape почти мгновенен даже для массива в миллиард элементов — двигать-то ничего не нужно.
Порядок обхода: C-порядок по умолчанию
По умолчанию reshape раскладывает элементы по строкам: 1, 2, 3, 4 — первая строка, следом вторая. Это C-порядок: строки идут друг за другом, как в языке C. Есть и обратный, Fortran-порядок — заполняем столбцами: sales.reshape(3, 4, order="F"). В жизни он попадается редко, но если после reshape числа «легли не так, как ждали», первым делом вспомните про порядок обхода — скорее всего, данные пришли из столбцового источника.
reshape(-1): пусть NumPy сам посчитает размерность
Часто вы знаете только часть новой формы. «Сделай в один столбец», «разложи на две строки, а столбцы посчитай сам». Для этого в reshape есть специальное значение -1 — «на эту ось отдай всё, что осталось». Минус здесь не «последний элемент», как в индексации, а именно авто-размерность. И такая размерность может быть ровно одна: если минусов два, системе нечего решать.
import numpy as np
table = np.arange(1, 13).reshape(3, 4)
print(table.reshape(-1)) # всё в одну строку
print(table.reshape(2, -1)) # 2 строки, столбцы сами
print(table.reshape(6, -1)) # 6 строк, столбцы сами
[ 1 2 3 4 5 6 7 8 9 10 11 12] [[ 1 2 3 4 5 6] [ 7 8 9 10 11 12]] [[ 1 2] [ 3 4] [ 5 6] [ 7 8] [ 9 10] [11 12]]
reshape(-1) вообще без других чисел — самый короткий способ выпрямить массив любой глубины в одну строку. Двенадцать слотов делятся на 2 строки нацело, поэтому вторая строка примера дала 2×6, а третья — 6×2. И да, результат всё тот же вид: копии данных снова нет.
Оси по-человечески: что значит axis=0 и axis=1
Слово «ось» пугает, хотя интуиция у него земная. У двумерного массива две оси: ось 0 идёт вдоль строк сверху вниз, ось 1 — вдоль столбцов слева направо. Номер оси — это просто позиция в shape: у формы (3, 4) ось 0 имеет длину 3, ось 1 — длину 4. Когда вы просите агрегацию с axis=0, операция прокатывается вдоль оси 0, и эта ось из результата исчезает: остаётся по одному числу на каждый столбец.
import numpy as np
scores = np.array([[8, 7, 9],
[6, 9, 7]])
print(scores.sum()) # всё вместе
print(scores.sum(axis=0)) # по каждому столбцу
print(scores.sum(axis=1)) # по каждой строке
print(scores.argmax(axis=1)) # где максимум в каждой строке
46 [14 16 16] [24 22] [2 1]
Мнемоника, которую я использую сам: axis — это то, что исчезнет из результата. Сумма по axis=0 у матрицы (2, 3) даёт массив длины 3 — по числу на столбец. По axis=1 — длины 2, по числу строк. В примере выше оценка каждого из двух судей по трём дисциплинам: sum(axis=0) — итог по дисциплинам, sum(axis=1) — итог по судьям, argmax(axis=1) — номер лучшей дисциплины каждого судьи.
a.shape— кортеж длин всех осей; у таблицы их две, у «куба» триa.ndim— число осей: у вектора 1, у таблицы 2, у трёхмерного массива 3a.size— произведение длин осей; не меняется ни при reshape, ни при транспонировании
Мы подробно гоняли sum, mean и std по осям на реальных данных в уроке про агрегирующие функции — там же лежит правило про nan-варианты. Здесь цель другая: закрепить саму механику осей, потому что она же управляет склейкой массивов и матричным умножением ниже по тексту.
Транспонирование .T: как транспонировать матрицу numpy
Транспонирование — зеркальный поворот таблицы вокруг диагонали: строки становятся столбцами. В NumPy это свойство .T или функция np.transpose(a). Форма (3, 2) превращается в (2, 3). Классический кейс — подогнать данные под форму, которую ждёт другая функция: развернуть «товары на дни» в «дни на товары» перед склейкой или умножением.
import numpy as np
report = np.array([[150, 220],
[ 90, 180],
[130, 240]])
print(report.shape)
print(report.T)
print(report.T.shape)
(3, 2) [[150 90 130] [220 180 240]] (2, 3)
Два нюанса. Для одномерного массива .T ничего не делает — разворачивать нечего, строка остаётся строкой. И .T, как и reshape, возвращает вид: транспонированная матрица смотрит на те же байты, просто в другом порядке осей. Никакого копирования строк на самом деле не происходит — поэтому report.T[0, 0] = 0 изменит и report.
Матричное умножение: @, np.matmul и np.dot
С матрицами у новичков главный источник путаницы — два разных умножения. Звёздочка * умножает поэлементно: (1, 2) * (3, 4) даст (3, 8). А оператор @ (он же np.matmul и np.dot) выполняет матричное умножение из линейной алгебры: строка левой матрицы «лестницей» проходит по столбцам правой, произведения складываются в одно число. Правило согласования форм: столбцов у левой матрицы должно быть ровно столько, сколько строк у правой. (3, 2) @ (2, 4) даст (3, 4).
Живой пример из счётов: три заказа, в каждом два товара. Матрица заказов (3, 2) умножается на колонку цен (2, 1) — на выходе счёт по каждому клиенту.
import numpy as np
# заказы: 3 клиента x 2 товара, штуки
orders = np.array([[2, 3],
[1, 4],
[5, 1]])
# цены: 2 товара, рубли за штуку
prices = np.array([[100],
[200]])
bills = orders @ prices
print(bills)
[[800] [900] [700]]
Первый клиент взял два товара по 100 и три по 200: 2·100 + 3·200 = 800. Проверьте вторую строку — 1·100 + 4·200 = 900. Больше всего ошибок приходит от перепутанных форм: (3, 4) @ (2, 3) не сработает — у левой четыре столбца, у правой две строки. NumPy пишет это прямо с числами; ниже ловим текст на np.dot, у него формулировка самая наглядная (у @ в новых версиях NumPy то же ограничение, только другими словами):
import numpy as np
a = np.ones((3, 4))
b = np.ones((2, 3))
try:
np.dot(a, b)
except ValueError as e:
print("ValueError:", e)
ValueError: shapes (3,4) and (2,3) not aligned: 4 (dim 1) != 2 (dim 0)
Прочитайте претензию NumPy как уравнение: размерность 1 левой матрицы (четыре столбца) не равна размерности 0 правой (две строки). Лечится reshape'ом до совместимых форм или транспонированием b.T. Кстати, скалярное произведение векторов — тоже @: (3,) @ (3,) вернёт одно число. Для полноты: a * b требует одинаковых форм или совместимости через broadcasting — это совсем другая операция, не перепутайте.
Как объединить два массива numpy: concatenate, vstack, hstack
Обратная reshape задача — собрать большой массив из нескольких маленьких. np.vstack ставит массивы друг на друга (добавляет строки), np.hstack — в ряд (добавляет столбцы). Универсальный вариант — np.concatenate со свободным выбором оси: axis=0 добавляет строки, axis=1 — столбцы. В отличие от reshape, здесь данные действительно копируются в новую память.
import numpy as np
week1 = np.array([12, 14, 11, 15])
week2 = np.array([16, 13, 14, 17])
# друг под другом: таблица 2 на 4
by_week = np.vstack([week1, week2])
print(by_week)
# друг за другом: одна длинная строка
timeline = np.hstack([week1, week2])
print(timeline)
print(np.concatenate([week1, week2]).shape)
[[12 14 11 15] [16 13 14 17]] [12 14 11 15 16 13 14 17] (8,)
Обратите внимание на одномерные массивы: ось у них всего одна, поэтому hstack для них — обычная склейка подряд, а vstack превращает каждый массив в отдельную строку матрицы. Именно так чаще всего собирают таблицу из колонок, пришедших из разных источников. Формы по второй оси обязаны совпадать: 4 элемента к 4 — ок, 4 к 5 — ошибка о несовпадении форм при склейке.
flatten против ravel: копия или вид
Вытянуть массив в одну строку можно тремя способами: reshape(-1), ravel() и flatten(). Внешне — одинаковый результат. Разница в отношении к памяти: flatten() всегда создаёт независимую копию, а ravel() старается отдать вид и копирует только когда данные лежат неудобно, например после транспонирования с разрывом строк.
import numpy as np
table = np.arange(1, 7).reshape(2, 3)
flat_copy = table.flatten() # всегда копия
flat_view = table.ravel() # вид, если можно
table[0, 0] = 100
print(flat_copy[0]) # копия не заметила изменений
print(flat_view[0]) # вид показывает новые данные
1 100
Правило простое. Будете менять вытянутый массив и не хотите трогать оригинал — берите flatten(). Нужно только прочитать и сэкономить память — ravel(). reshape(-1) ведёт себя как ravel: вид, если данные лежат подряд.
Практика: превращаем поток чисел в таблицу продаж
Соберём всё вместе. У вас поток из двенадцати чисел — продажи трёх магазинов за четыре недели. Задачи: увидеть таблицу, посчитать итог каждого магазина, среднее по неделям и лучшую неделю каждого магазина.
import numpy as np
# продажи: 3 магазина x 4 недели, одним потоком
raw = [310, 290, 405, 380,
280, 425, 390, 300,
350, 370, 340, 360]
shops = np.array(raw).reshape(3, 4)
print(shops.sum(axis=1)) # итог по каждому магазину
print(shops.mean(axis=0)) # среднее по каждой неделе
print(shops.argmax(axis=1)) # лучшая неделя каждого магазина
[1385 1395 1420] [313.33333333 361.66666667 378.33333333 346.66666667] [2 1 1]
Три строки вычислений вместо трёх вложенных циклов. Разбор по осям: sum(axis=1) схлопнул ось недель и оставил по числу на магазин; mean(axis=0) схлопнул ось магазинов и оставил среднее по каждой неделе; argmax(axis=1) вернул индекс лучшей недели в каждой строке — у первого магазина это неделя с индексом 2 (405 штук). Это и есть мышление в осях: сначала решаете, что схлопнуть, потом выбираете axis.
Шпаргалка по форме массива
Сводим функции формы в одну таблицу — пригодится как шпаргалка перед задачами:
| Задача | Инструмент | Пример |
|---|---|---|
| Другая форма тех же данных | reshape | a.reshape(3, 4) |
| Авто-размерность | reshape(-1) | a.reshape(2, -1) |
| Выпрямить в строку | reshape(-1), ravel, flatten | a.flatten() |
| Поменять оси местами | T, transpose | a.T |
| Матричное умножение | @, matmul, dot | a @ b |
| Добавить строки | vstack, concatenate(axis=0) | np.vstack([a, b]) |
| Добавить столбцы | hstack, concatenate(axis=1) | np.hstack([a, b]) |
Что дальше
Теперь двенадцать чисел можно увидеть и строкой, и таблицей, и колонкой — и посчитать по любой оси. Следующий шаг — булева индексация: достать из таблицы не «строку и столбец», а «все дни, где продажи просели», и почистить выбросы. Спойлер: первое, что там встретится, — загадочная ошибка про The truth value of an array, если склеить условия через and. Пришли из поиска за одним reshape? Полный маршрут из десяти уроков — самоучитель NumPy с нуля, и формы здесь седьмая ступень.
Сначала предскажи ответ в голове — это главный навык программиста.
import numpy as np
a = np.arange(6).reshape(2, 3)
b = a.reshape(3, 2)
a[0, 0] = 99
print(b[0, 0])
import numpy as np
m = np.array([[1, 2, 3],
[4, 5, 6]])
print(m.T.shape, m.size)
1. Что обычно возвращает метод reshape — копию или вид на данные?
2. Что означает reshape(2, -1) для массива из 12 элементов?
3. Чем a @ b отличается от a * b для матриц?
4. Что вернёт scores.sum(axis=0) для матрицы 3 на 4?
5. Чем flatten отличается от ravel?
Создайте массив из чисел от 1 до 12 через np.arange, превратите его в таблицу 3 на 4 методом reshape, затем выведите форму таблицы и сумму по каждому столбцу.
Что делает функция reshape в NumPy?
Меняет форму массива, не изменяя и не копируя данные: np.arange(12).reshape(3, 4) вернёт таблицу 3 на 4 из тех же чисел. Количество элементов должно совпадать: нельзя 12 чисел уложить в форму (5, 3) — будет ValueError: cannot reshape array of size 12 into shape (5,3). Обычный reshape возвращает вид на исходные данные, поэтому изменения видны в обоих массивах.
Что значит -1 в reshape?
Это авто-размерность: NumPy сам вычисляет длину этой оси из общего числа элементов. np.arange(12).reshape(2, -1) даст форму (2, 6). Минус одна может быть только одна в форме, иначе появится ошибка can only specify one unknown dimension. Запись reshape(-1) без других размеров выпрямляет массив любой глубины в одну строку.
Как транспонировать матрицу в NumPy?
Свойством .T: a.T возвращает массив с осями наоборот, форма (3, 2) становится (2, 3). Есть и функция np.transpose(a) с возможностью указать порядок осей. Транспонирование возвращает вид — новая память не выделяется, а для одномерного массива .T ничего не меняет.
Как объединить два массива NumPy в один?
np.concatenate([a, b]) склеивает по оси 0, np.vstack — друг на друга, np.hstack — в ряд. Для одномерных массивов vstack превращает их в строки новой матрицы, а hstack просто дописывает подряд. Совпадающие по склейке оси размеры обязательны: нельзя hstack массив из 4 элементов с массивом из 5.
Чем flatten отличается от ravel?
Оба вытягивают массив в одну строку, но flatten() всегда создаёт независимую копию, а ravel() возвращает вид и копирует данные, только когда они лежат неудобно, например после транспонирования. Пока данные лежат подряд, изменения видны в обоих массивах: в примере урока правка исходника изменила и ravel-результат (100 вместо 1), а flatten остался со старым значением. reshape(-1) ведёт себя как ravel.
Понравился урок? Сошлитесь на него
«Форма многомерного массива — не про то, где лежат данные, а про то, как мы на них смотрим.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
NumPy · Урок 6
Агрегирующие функции NumPy: sum, mean, min, max, std на реальных данных
Считаем статистику на данных сети кофеен: sum, mean, median, std и percentile, загадка axis=0 против axis=1 и nan-функции для пропусков.
NumPy · Урок 8
Булева индексация и маски: фильтрация данных в NumPy
Достаём из массива только нужное: булевы маски, операторы & | ~ вместо and/or, np.where в двух ролях, argmax и честная чистка выбросов в данных датчика.
Pandas · Урок 1
Что такое Pandas и как установить через pip: первые Series и DataFrame
Первая таблица DataFrame в библиотеке pandas, умный столбец Series и быстрый осмотр данных через head, info и describe — старт сквозного анализа данных интернет-магазина.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
Matplotlib / Seaborn · Урок 9
Тепловые карты и pairplot: heatmap и матрицы диаграмм
Пять строк — и весь датасет на экране: heatmap рисует корреляционную матрицу цветом, pairplot раскладывает все пары переменных в матрицу диаграмм. Разведочный анализ, который раньше занимал день, теперь занимает минуту.
pearson и spearman разница
json · Урок 4
Файлы JSON: json.dump и json.load
Данные, которые переживают скрипт: json.dump пишет словарь в файл, json.load читает обратно, а round-trip подтверждает — сохранил, прочитал, совпало.
json dump load разница
NumPy · Урок 3
Индексация и срезы массивов NumPy: как достать любой элемент
Индексы с нуля и с конца, срезы с шагом, строки и столбцы двумерных массивов — и главный подводный камень NumPy: срез возвращает view, а не копию. С примерами, разбором ошибок и шпаргалкой.
индексация numpyсрезы массива python