Урок курса

Изменение формы: reshape, transpose, добавление и удаление осей

NumPy и pandas: практический тренажёр

В прошлом уроке вы создавали массивы через np.arange, np.zeros, np.ones и другие конструкторы — и каждый раз получали массив с конкретной формой. Теперь задача другая: взять уже существующий массив и поменять его форму, не трогая данные внутри.

reshape: изменение формы и правило сохранения числа элементов

Метод arr.reshape(new_shape) возвращает новый взгляд на те же данные в другой форме. Сами элементы никуда не перемещаются — меняется только интерпретация: сколько строк, столбцов, «слоёв» видит NumPy.

Единственное жёсткое условие: произведение всех размеров в новой форме должно совпадать с исходным size массива. Иначе NumPy выбросит ValueError.

import numpy as np

a = np.arange(12)   # shape (12,), size = 12
print(a.shape)      # (12,)

b = a.reshape(3, 4)  # 3 × 4 = 12 ✓
print(b.shape)       # (3, 4)

c = a.reshape(2, 6)  # 2 × 6 = 12 ✓
print(c.shape)       # (2, 6)

d = a.reshape(6, 2)  # 6 × 2 = 12 ✓
print(d.shape)       # (6, 2)

Во всех трёх случаях size остаётся 12. Порядок элементов при обходе сохраняется: NumPy раскладывает их построчно (row-major, или C-порядок по умолчанию).

Что происходит, если произведение не сходится:

a.reshape(2, 4)  # 2 × 4 = 8 ≠ 12 → ValueError
# ValueError: cannot reshape array of size 12 into shape (2,4)

Ошибка возникает сразу при вызове — NumPy не пытается ничего подгонять. Это хорошо: лучше упасть с понятным сообщением, чем молча создать массив неожиданной формы.

Рабочий паттерн в реальном коде — всегда проверять shape после reshape:

b = a.reshape(3, 4)
print(b.shape)   # убеждаемся, что получили (3, 4)
print(b.size)    # 12 — size не изменился

Обратите внимание: reshape возвращает новый объект, но часто это представление (view) того же буфера памяти, а не копия. Если изменить элемент в b, изменение может отразиться в a. Для учебных задач это обычно не важно, но знать стоит.

-1 как авторазмер в reshape

Когда делаешь reshape, часто знаешь одно измерение заранее, а второе приходится считать вручную: 12 элементов, хочу 3 строки — значит 4 столбца. NumPy умеет делать это сам, если вместо одного из размеров написать -1.

import numpy as np

a = np.arange(12)   # shape (12,), size = 12

b = a.reshape(3, -1)   # NumPy считает: 12 // 3 = 4 → shape (3, 4)
print(b.shape)         # (3, 4)

c = a.reshape(-1, 4)   # NumPy считает: 12 // 4 = 3 → shape (3, 4)
print(c.shape)         # (3, 4)

d = a.reshape(-1, 6)   # 12 // 6 = 2 → shape (2, 6)
print(d.shape)         # (2, 6)

Формула простая: NumPy берёт size массива и делит на произведение всех явно указанных размеров. Результат и есть значение, которое подставляется вместо -1.

То же работает для трёхмерных форм:

e = a.reshape(2, 2, -1)   # 12 // (2 × 2) = 3 → shape (2, 2, 3)
print(e.shape)             # (2, 2, 3)

f = a.reshape(2, -1, 3)   # 12 // (2 × 3) = 2 → shape (2, 2, 3)
print(f.shape)             # (2, 2, 3)

Одно жёсткое ограничение: -1 можно указать ровно один раз. Если написать reshape(-1, -1), NumPy не знает, как разложить size на два неизвестных — это сразу ValueError.

a.reshape(-1, -1)   # ValueError: can only specify one unknown dimension

Второй подводный камень — когда size не делится нацело на явно указанные размеры:

a.reshape(5, -1)   # 12 // 5 = 2.4 — не целое
# ValueError: cannot reshape array of size 12 into shape (5,newaxis)

NumPy не округляет и не обрезает — просто падает с ошибкой.

-1 особенно удобен в реальном коде, когда размер массива заранее неизвестен — например, данные пришли из файла. Вместо того чтобы писать arr.reshape(arr.size // 4, 4), достаточно arr.reshape(-1, 4). Смысл тот же, но читается чище и не требует промежуточного вычисления.

Транспонирование: .T и np.transpose

Транспонирование — это не перераспределение элементов по новой сетке, как в reshape, а перестановка осей. Для двумерного массива это означает одно: строки становятся столбцами, а столбцы — строками.

Есть два эквивалентных способа. Атрибут .T — самый короткий:

import numpy as np

a = np.arange(6).reshape(2, 3)
print(a.shape)   # (2, 3)
print(a)
# [[0 1 2]
#  [3 4 5]]

b = a.T
print(b.shape)   # (3, 2)
print(b)
# [[0 3]
#  [1 4]
#  [2 5]]

Функция np.transpose(arr) делает то же самое и возвращает идентичный результат:

c = np.transpose(a)
print(c.shape)          # (3, 2)
print(np.array_equal(b, c))  # True

Что происходит с shape: исходный (r, c) превращается в (c, r). Число элементов не изменяется — size остаётся прежним, это тот же инвариант, что и у reshape.

Обратите внимание на элементы в результате. В исходном массиве первая строка — [0, 1, 2]. После транспонирования эти значения оказываются в первом столбце: 0 в строке 0, 1 в строке 1, 2 в строке 2. Это и есть перестановка осей: то, что шло по оси 1 (столбцы), теперь идёт по оси 0 (строки), и наоборот.

Поэтому .T не эквивалентен reshape с переставленными размерами. Сравните:

a = np.arange(6).reshape(2, 3)

print(a.T)
# [[0 3]
#  [1 4]
#  [2 5]]

print(a.reshape(3, 2))
# [[0 1]
#  [2 3]
#  [4 5]]

Результаты разные. reshape(3, 2) просто переупаковывает элементы в новую сетку, сохраняя их порядок обхода строка за строкой. .T меняет назначение осей — элемент, который стоял в позиции [i, j], теперь стоит в позиции [j, i].

Для одномерного массива .T не даёт никакого эффекта: у вектора shape (n,) нет второй оси, которую можно переставить, поэтому shape остаётся (n,). Если нужно превратить вектор-строку в вектор-столбец, это задача для np.newaxis — об этом в следующем разделе.

Добавление и удаление единичных осей: np.newaxis и squeeze

Иногда нужно не переупаковать данные, а буквально «вставить» новую ось в конкретное место формы — не меняя ни одного элемента. Именно для этого существует np.newaxis.

Технически np.newaxis — это просто псевдоним для None. Его помещают внутрь квадратных скобок при индексировании, и NumPy вставляет единичную ось там, где он стоит.

import numpy as np

v = np.arange(4)          # shape (4,)
print(v.shape)             # (4,)

col = v[:, np.newaxis]    # добавляем ось справа → shape (4, 1)
print(col.shape)           # (4, 1)

row = v[np.newaxis, :]    # добавляем ось слева → shape (1, 4)
print(row.shape)           # (1, 4)

Позиция np.newaxis в индексном выражении определяет, куда именно вставляется новая ось. v[:, np.newaxis] — ось появляется вторым измерением (вектор-столбец). v[np.newaxis, :] — ось появляется первым (вектор-строка).

Важно понимать, что именно это решает задачу, с которой .T не справляется. Транспонирование одномерного массива ничего не меняет, потому что перестановка единственной оси самой с собой — это тождественная операция. А np.newaxis сначала явно добавляет вторую ось, и только тогда форма становится двумерной.

v = np.arange(4)
print(v.T.shape)              # (4,) — .T не помог
print(v[:, np.newaxis].shape) # (4, 1) — теперь это вектор-столбец

Обратная операция — np.squeeze. Она удаляет из формы все оси размером 1, независимо от их числа и позиции:

x = np.arange(4).reshape(1, 4, 1)   # shape (1, 4, 1)
print(np.squeeze(x).shape)           # (4,)

y = np.arange(3).reshape(3, 1)       # shape (3, 1)
print(np.squeeze(y).shape)           # (3,)

z = np.arange(6).reshape(2, 3)       # shape (2, 3) — единичных осей нет
print(np.squeeze(z).shape)           # (2, 3) — форма не изменилась

squeeze удобен, когда данные приходят с лишними единичными измерениями — например, после загрузки или после операции, которая «по правилам» возвращает двумерный результат там, где ожидается одномерный.

Один практический момент: squeeze без аргументов удаляет все единичные оси сразу. Если нужно убрать только конкретную ось, есть параметр axis:

x = np.zeros((1, 4, 1))              # shape (1, 4, 1)
print(np.squeeze(x, axis=0).shape)   # (4, 1) — убрали только первую ось
print(np.squeeze(x, axis=2).shape)   # (1, 4) — убрали только третью ось

Теперь соберём все операции урока в один сквозной пример, чтобы видеть цепочку преобразований и shape на каждом шаге:

import numpy as np

a = np.arange(12)             # shape (12,), size 12
print(a.shape)                 # (12,)

b = a.reshape(3, 4)           # shape (3, 4)
print(b.shape)                 # (3, 4)

c = a.reshape(4, -1)          # -1 → 12 // 4 = 3, shape (4, 3)
print(c.shape)                 # (4, 3)

d = b.T                       # shape (4, 3)
print(d.shape)                 # (4, 3)

e = np.transpose(b)           # shape (4, 3), идентично b.T
print(np.array_equal(d, e))   # True

f = a.reshape(12, 1)          # shape (12, 1)
print(f.shape)                 # (12, 1)

g = a[:, np.newaxis]          # shape (12, 1) — то же через newaxis
print(g.shape)                 # (12, 1)

h = np.squeeze(g)             # shape (12,) — единичная ось убрана
print(h.shape)                 # (12,)

print(a.size == b.size == g.size == h.size)  # True — size не менялся

f и g дают одинаковый результат — shape (12, 1) — разными путями: reshape(12, 1) и [:, np.newaxis]. В реальном коде np.newaxis читается нагляднее именно когда задача — добавить ось к уже существующему массиву, не задумываясь о его полной форме.

Последняя строка явно подтверждает инвариант: все промежуточные формы разные, но size везде одинаковый. Это и есть главное ограничение, которое NumPy проверяет при каждом преобразовании формы.

Закрепите тему на задачах

Перед практикой выпишите исходную и итоговую shape. После reshape число элементов должно сохраниться, а transpose меняет порядок осей. Проверьте, затронет ли изменение результата исходный массив.

NumPy squeeze и newaxis: строка, столбец и форма массива

NumPy reshape и transpose: одинаковая форма, разные значения

Откройте условие и решите задачу перед чтением следующей темы. В задачах на код проверяется программа, в тестовых заданиях — выбранный ответ. Для последовательного обучения вернитесь в программу бесплатного курса.

Задача 1

Массив arr имеет shape (6, 4). Чему будет равен arr.T.shape?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 2

Программист написал arr = np.ones((3, 4)); b = arr.reshape(2, -1). Какой shape будет у b?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 3

Цепочка: reshape с -1, newaxis, transpose, squeeze

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.

Что нужно сделать

Напишите программу, которая:

  1. Читает из stdin одну строку с двумя целыми числами: rows и cols.
  2. Создаёт одномерный массив a через np.arange(rows * cols) — shape (rows * cols,).
  3. Преобразует a в двумерный массив b через reshape(rows, -1) — пусть NumPy сам вычислит второй размер.
  4. Транспонирует b через np.transpose — получает массив c shape (cols, rows).
  5. Добавляет единичную ось слева к c через c[np.newaxis, :] — получает массив d shape (1, cols, rows).
  6. Убирает все единичные оси из d через np.squeeze — получает массив e.
  7. Выводит в stdout ровно пять строк — по одной на каждый промежуточный результат — в формате <имя>: shape=<shape> size=<size>:
  • b: shape=...
  • c: shape=...
  • d: shape=...
  • e: shape=...
  • И финальную строку: size_invariant=True если все четыре массива имеют одинаковый size, иначе size_invariant=False.

Ввод и вывод

Программа читает stdin и пишет в stdout.

Формат ввода

Одна строка: два целых числа rows и cols, разделённые пробелом. Гарантируется, что rows >= 2, cols >= 2.

Формат вывода

Пять строк:

b: shape=(rows, cols) size=<rows*cols>
c: shape=(cols, rows) size=<rows*cols>
d: shape=(1, cols, rows) size=<rows*cols>
e: shape=(cols, rows) size=<rows*cols>
size_invariant=True

Формат shape — строго как у Python: (r, c) или (1, c, r) и т. д.

РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 4

Найди и исправь: неверные вызовы reshape и transpose

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.

В редакторе уже есть сломанный код. Он пытается выполнить три преобразования формы, но в каждом из трёх вызовов допущена ошибка: либо неверное число элементов в новой форме, либо неправильное использование -1, либо перепутаны операции.

Что нужно сделать

Найдите и исправьте все три ошибки в коде так, чтобы программа:

  1. Читала из stdin одну строку с двумя целыми числами: rows и cols.
  2. Создавала одномерный массив a через np.arange(rows * cols) — shape (rows * cols,).
  3. Преобразовывала a через reshape(rows, cols + 1) → ошибка: правильный вызов должен быть a.reshape(rows, -1), чтобы NumPy сам вычислил второй размер.
  4. Транспонировала результат через .reshape(cols, rows) → ошибка: правильная операция — .T (транспонирование, а не reshape).
  5. Применяла np.squeeze к результату предыдущего шага, добавив перед этим единичную ось через np.newaxis → ошибка: newaxis добавляется через индексирование arr[np.newaxis, :], а не присваиванием.
  6. Выводила в stdout ровно три строки:
reshaped: <shape>
transposed: <shape>
squeezed: <shape>

Формат shape — строго как у Python: (r, c) или (n,) и т. д.

Ввод и вывод

Программа читает stdin и пишет в stdout.

Формат ввода

Одна строка: два целых числа rows и cols, разделённые пробелом. Гарантируется, что rows >= 2, cols >= 2.

Формат вывода

Три строки:

reshaped: (rows, cols)
transposed: (cols, rows)
squeezed: (cols, rows)
РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 5

Вектор-столбец, вектор-строка и squeeze с axis: управление единичными осями

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.

Что нужно сделать

Напишите программу, которая:

  1. Читает из stdin одну строку с целым числом n.
  2. Создаёт одномерный массив v через np.arange(n) — shape (n,).
  3. Превращает v в вектор-столбец col через v[:, np.newaxis] — shape (n, 1).
  4. Превращает v в вектор-строку row через v[np.newaxis, :] — shape (1, n).
  5. Применяет np.squeeze(col, axis=1) — убирает только вторую ось у col, получает массив s1 shape (n,).
  6. Применяет np.squeeze(row, axis=0) — убирает только первую ось у row, получает массив s2 shape (n,).
  7. Выводит в stdout ровно шесть строк в формате <имя>: shape=<shape> size=<size>:
  • v: shape=...
  • col: shape=...
  • row: shape=...
  • s1: shape=...
  • s2: shape=...
  • all_equal=True если v.size == col.size == row.size == s1.size == s2.size, иначе all_equal=False.

Ввод и вывод

Программа читает stdin и пишет в stdout.

Формат ввода

Одна строка: целое число n (гарантируется n >= 2).

Формат вывода

Шесть строк:

v: shape=(n,) size=<n>
col: shape=(n, 1) size=<n>
row: shape=(1, n) size=<n>
s1: shape=(n,) size=<n>
s2: shape=(n,) size=<n>
all_equal=True

Формат shape — строго как у Python: (n,), (n, 1), (1, n) и т. д.

РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 6

.T и reshape: одинаковая форма, разное расположение значений

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.

Что нужно сделать

Напишите программу, которая на одном массиве показывает разницу между транспонированием и изменением формы:

  1. Читает два целых числа rows и cols. Гарантируется, что они различаются и оба не меньше 2.
  2. Создаёт flat = np.arange(rows * cols).
  3. Создаёт mat формы (rows, cols) через reshape.
  4. Создаёт via_t = mat.T.
  5. Независимо создаёт via_reshape = flat.reshape(cols, rows).
  6. Печатает заголовок T:, затем сам массив via_t, заголовок reshape:, затем сам массив via_reshape.
  7. Последней строкой печатает same_shape=True, если формы результатов совпадают.

Не переставляйте значения вручную. В этой задаче важно увидеть: .T и reshape(cols, rows) дают одинаковую форму, но располагают исходные значения по-разному.

Ввод и вывод

Программа читает данные из stdin и пишет результат в stdout.

Формат ввода

Одна строка: два различных целых числа rows cols, каждое не меньше 2.

Формат вывода

Сначала T: и напечатанный NumPy-массив via_t, затем reshape: и массив via_reshape, затем строка same_shape=True.

РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задачи к уроку

Все задачи курса →

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку