Перейти к содержанию

172 уроков, 13 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 7 из 10 Средний 40 мин 120 XP

Многомерные массивы и оси: 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, у трёхмерного массива 3
  • a.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() старается отдать вид и копирует только когда данные лежат неудобно, например после транспонирования с разрывом строк.

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.

Шпаргалка по форме массива

Сводим функции формы в одну таблицу — пригодится как шпаргалка перед задачами:

ЗадачаИнструментПример
Другая форма тех же данныхreshapea.reshape(3, 4)
Авто-размерностьreshape(-1)a.reshape(2, -1)
Выпрямить в строкуreshape(-1), ravel, flattena.flatten()
Поменять оси местамиT, transposea.T
Матричное умножение@, matmul, dota @ 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)
Проверь себя
0 / 5

1. Что обычно возвращает метод reshape — копию или вид на данные?

2. Что означает reshape(2, -1) для массива из 12 элементов?

3. Чем a @ b отличается от a * b для матриц?

4. Что вернёт scores.sum(axis=0) для матрицы 3 на 4?

5. Чем flatten отличается от ravel?

Карточки терминов
Запомнено: 0 / 6
Практика

Создайте массив из чисел от 1 до 12 через np.arange, превратите его в таблицу 3 на 4 методом reshape, затем выведите форму таблицы и сумму по каждому столбцу.

practice.py
Вопросы и ответы по уроку

Что делает функция 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-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.