Урок курса

Объединение массивов: concatenate и stack

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

В предыдущих уроках мы работали с отдельными массивами: считали агрегаты, обнаруживали NaN, управляли формой. Теперь встаёт другой вопрос — как склеить несколько массивов в один. Именно здесь начинается работа с np.concatenate и np.stack.

np.concatenate: объединение по существующей оси

Когда нужно склеить несколько массивов в один, первый инструмент — np.concatenate. Его сигнатура выглядит так:

np.concatenate([arr1, arr2, ...], axis=k)

Параметр axis=k указывает, вдоль какой из уже существующих осей происходит «склейка». Ключевое слово здесь — существующей: concatenate не добавляет новых измерений, он только удлиняет массив по одной из тех осей, которые уже есть.

Чтобы операция прошла без ошибки, входные массивы должны удовлетворять двум условиям:

  • одинаковое число осей (одинаковый ndim);
  • совпадающие размеры по всем осям, кроме той, вдоль которой идёт склейка.

Правило для shape результата простое. Пусть все входные массивы имеют ndim=2, и мы склеиваем по оси 0. Тогда по оси 0 берём сумму размеров всех входных массивов; по оси 1 — размер остаётся тем же, что у каждого входного (они обязаны совпадать).

В общем виде: если склеиваем по оси k, то размер результата по оси k равен сумме соответствующих размеров входных массивов, а по всем остальным осям размер совпадает с размером входных.

Конкретно: два массива shape (2, 3) при axis=0 дают (4, 3), при axis=1 дают (2, 6). Это можно проверить ещё до запуска кода — сложить нужные цифры вручную.

Нарушение условий совместимости NumPy сразу сообщит ошибкой ValueError: all the input array dimensions except for the concatenation axis must match exactly. Это сигнал: посмотри на shape всех входных массивов и найди несовпадение.

Практика np.concatenate по осям 0 и 1

Посмотрим, как работает concatenate на конкретном коде. Возьмём два двумерных массива одинаковой формы:

import numpy as np

a = np.array([[1, 2, 3], [4, 5, 6]])    # shape (2, 3)
b = np.array([[7, 8, 9], [10, 11, 12]]) # shape (2, 3)

# Склеиваем по оси 0 — добавляем строки
r0 = np.concatenate([a, b], axis=0)
print(r0.shape)  # (4, 3)
print(r0)
# [[ 1  2  3]
#  [ 4  5  6]
#  [ 7  8  9]
#  [10 11 12]]

# Склеиваем по оси 1 — добавляем столбцы
r1 = np.concatenate([a, b], axis=1)
print(r1.shape)  # (2, 6)
print(r1)
# [[ 1  2  3  7  8  9]
#  [ 4  5  6 10 11 12]]

При axis=0 массивы встают друг под другом: строк стало 2+2=4, а три столбца остались. При axis=1 — встают рядом: строки те же две, а столбцов стало 3+3=6.

Одномерный случай немного отличается — у вектора только одна ось, поэтому axis=0 единственный вариант:

x = np.array([1, 2, 3])  # shape (3,)
y = np.array([4, 5])     # shape (2,)

result = np.concatenate([x, y], axis=0)
print(result.shape)  # (5,)
print(result)        # [1 2 3 4 5]

Обратите внимание: здесь x и y имеют разные длины, и это нормально — по единственной оси размеры суммируются, никаких дополнительных ограничений нет.

Главное, что стоит закрепить из этих примеров: shape результата всегда можно посчитать вручную ещё до запуска. Берёте shape входных массивов, в позиции оси k складываете числа, по остальным позициям оставляете как есть. Если цифры не сходятся — NumPy выдаст ValueError ещё до того, как что-то пойдёт не так в логике программы.

np.stack: создание новой оси и практика по разным позициям

np.stack решает другую задачу: он не удлиняет массив вдоль существующей оси, а создаёт новую ось и складывает массивы вдоль неё. Это единственное, но принципиальное отличие от concatenate.

Сигнатура:

np.stack([arr1, arr2, ...], axis=k)

Параметр axis=k теперь означает: на какую позицию в кортеже shape вставить новое измерение. После вставки размер по этой позиции будет равен числу сложенных массивов.

Жёсткое требование: все входные массивы должны иметь строго одинаковую shape. Не просто совпадать по одной оси — а быть идентичными по всем осям. Если хотя бы один массив отличается, NumPy выбросит ValueError.

Одномерный случай

Возьмём два вектора shape (3,):

import numpy as np

a = np.array([1, 2, 3])  # shape (3,)
b = np.array([4, 5, 6])  # shape (3,)

# Новая ось в позиции 0 — массивы становятся строками
s0 = np.stack([a, b], axis=0)
print(s0.shape)  # (2, 3)
print(s0)
# [[1, 2, 3],
#  [4, 5, 6]]

# Новая ось в позиции 1 — массивы становятся столбцами
s1 = np.stack([a, b], axis=1)
print(s1.shape)  # (3, 2)
print(s1)
# [[1, 4],
#  [2, 5],
#  [3, 6]]

При axis=0 два вектора длиной 3 превращаются в матрицу (2, 3): два — это число массивов, 3 — исходная длина. При axis=1 новая ось вставляется между существующей длиной и ничем: получаем (3, 2). Можно представить это как «каждый элемент вектора теперь имеет два значения — из a и из b».

Двумерный случай

Когда входные массивы двумерные, результат np.stack будет трёхмерным — новое измерение добавляется к уже существующим двум:

A = np.ones((3, 4))        # shape (3, 4)
B = np.ones((3, 4)) * 2   # shape (3, 4)

print(np.stack([A, B], axis=0).shape)  # (2, 3, 4)
print(np.stack([A, B], axis=1).shape)  # (3, 2, 4)
print(np.stack([A, B], axis=2).shape)  # (3, 4, 2)

Логика прямая: берёте исходный кортеж (3, 4) и вставляете 2 (число массивов) на позицию axis:

  • axis=0 → вставляем 2 перед 3, 4(2, 3, 4)
  • axis=1 → вставляем 2 между 3 и 4(3, 2, 4)
  • axis=2 → вставляем 2 после 3, 4(3, 4, 2)

Эта операция вставки — ключ к тому, как читать shape результата np.stack без запуска кода. Нужно знать: сколько массивов складываете и куда вставляете новое измерение.

Разница между concatenate и stack: алгоритм проверки shape

Оба инструмента объединяют массивы, но делают это принципиально по-разному. np.concatenate работает с уже существующими осями: число измерений результата совпадает с числом измерений входных массивов, а один из размеров увеличивается. np.stack создаёт новую ось: результат всегда имеет на одно измерение больше, чем входные.

Это различие хорошо видно на одномерных векторах:

  • np.concatenate([a, b], axis=0) для двух векторов shape (n,) даёт shape (2n,) — один длинный вектор.
  • np.stack([a, b], axis=0) для тех же векторов даёт shape (2, n) — матрицу из двух строк.

Результаты морфологически разные, хотя данные «те же самые».

Алгоритм предсказания shape для concatenate

Чтобы узнать shape результата до запуска кода, достаточно трёх шагов:

  1. Выпишите shape каждого входного массива.
  2. Убедитесь, что по всем осям, кроме k, размеры совпадают — иначе будет ValueError.
  3. По оси k сложите соответствующие размеры всех входных массивов; по остальным осям оставьте размер как есть.

Пример: массивы (2, 3) и (2, 3) при axis=0 → складываем первые числа: 2+2=4, второе остаётся 3 → результат (4, 3).

Алгоритм предсказания shape для stack

  1. Убедитесь, что все входные массивы имеют строго одинаковую shape.
  2. Возьмите исходный кортеж shape.
  3. Вставьте число массивов n в позицию k этого кортежа.

Пример: два массива (3, 4) при axis=1 → берём (3, 4), вставляем 2 на позицию 1 → (3, 2, 4).

Этот алгоритм работает для любого числа осей и любого числа складываемых массивов. Если складываете пять массивов shape (6, 7) через np.stack(axis=0), результат — (5, 6, 7): число массивов на нулевую позицию, исходная форма следом.

Главный практический вопрос при выборе между двумя функциями: нужно ли сохранить число осей или добавить новое измерение? Если нужно просто удлинить — concatenate. Если нужно «сложить в стопку» с сохранением структуры каждого массива — stack.

Попробуйте решить

Даны два массива shape (3, 4). Какой shape будет у результата np.concatenate([a, b], axis=1)?

Продолжить с проверкой и прогрессом

Откройте интерактивный раннер с заданиями урока.

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