Объединение и разбиение: concatenate, stack, split
Урок про сборку массивов из частей и обратное разрезание: ключевое различие между «склеить вдоль оси» и «сложить в новую ось».
concatenate склеивает массивы вдоль существующей оси (число осей не меняется), а stack складывает их вдоль новой оси (число осей увеличивается на одну).
concatenate: склейка вдоль существующей оси
Самая базовая операция объединения — np.concatenate. Она приклеивает массивы друг к другу вдоль указанной оси. Все оси, кроме оси склейки, должны совпадать по размеру. Число измерений результата такое же, как у входов.
import numpy as np
a = np.array([[1, 2],
[3, 4]])
b = np.array([[5, 6],
[7, 8]])
print(np.concatenate([a, b], axis=0)) # вертикально (добавить строки)
print(np.concatenate([a, b], axis=1)) # горизонтально (добавить столбцы)
Вывод:
[[1 2] [3 4] [5 6] [7 8]] [[1 2 5 6] [3 4 7 8]]
При axis=0 склеиваем по строкам — массивы должны иметь одинаковое число столбцов. При axis=1 — по столбцам, нужно одинаковое число строк. Идея проста — это «дописать» один список к другому вдоль нужного направления:
a = [[1, 2], [3, 4]]
b = [[5, 6], [7, 8]]
# axis=0: добавить строки
v = a + b
# axis=1: дописать столбцы в каждой строке
h = [a[i] + b[i] for i in range(len(a))]
print("axis=0:", v)
print("axis=1:", h)
Вывод:
axis=0: [[1, 2], [3, 4], [5, 6], [7, 8]] axis=1: [[1, 2, 5, 6], [3, 4, 7, 8]]
Ось склейки: что должно совпадать
Правило совместимости для concatenate легко запомнить через образ: вы прикладываете массивы друг к другу гранью вдоль оси склейки, поэтому все остальные грани должны идеально совпадать. Склеивая по axis=0 (добавляя строки), вы требуете одинакового числа столбцов — иначе строки не выстроятся в прямоугольник. Склеивая по axis=1 (добавляя столбцы), требуете одинакового числа строк. В N измерениях принцип тот же: размеры по всем осям, кроме оси склейки, обязаны совпадать. Если они не совпадают, NumPy выдаст ошибку с указанием, какие размеры не сошлись. Это полезная проверка-страховка: она не даст случайно склеить несовместимые данные. Когда видите такую ошибку, распечатайте формы всех склеиваемых массивов и найдите ось, по которой они расходятся, — почти всегда виноват один «не такой» кусок.
Удобные обёртки: vstack, hstack, dstack
Для частых случаев есть короткие имена. vstack (vertical) кладёт массивы друг под друга, hstack (horizontal) — рядом, dstack (depth) — в глубину (по третьей оси). Они умнее concatenate в обработке одномерных входов: vstack трактует каждый 1D-вектор как строку.
import numpy as np
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
print(np.vstack([a, b])) # как две строки -> (2, 3)
print(np.hstack([a, b])) # в один длинный вектор -> (6,)
print(np.column_stack([a, b])) # как два столбца -> (3, 2)
Вывод:
[[1 2 3] [4 5 6]] [1 2 3 4 5 6] [[1 4] [2 5] [3 6]]
Обратите внимание: для 1D-векторов hstack просто склеивает их в один длинный вектор, а vstack делает из них строки матрицы. Это удобно, но и источник путаницы — следите за размерностью результата.
Объединение всегда копирует
В отличие от срезов, reshape и транспонирования, операции объединения всегда создают новый массив и копируют данные. Это неизбежно: результат должен быть непрерывным блоком, а исходные массивы лежат в разных местах памяти, так что их физически переписывают в новый буфер. Отсюда практический вывод о производительности: если вам нужно собрать большой массив из множества мелких кусков, не вызывайте concatenate в цикле, добавляя по одному, — каждый вызов копирует всё накопленное заново, и сложность становится квадратичной. Правильный паттерн — собрать все куски в обычный список Python, а затем сделать один вызов np.concatenate(список) или np.stack(список), который скопирует данные ровно один раз. Эта разница между «объединять по ходу» и «накопить и объединить один раз» на больших объёмах превращается в разницу между секундами и минутами.
stack: добавление новой оси
Здесь — главное концептуальное различие. np.concatenate склеивает вдоль существующей оси, а np.stack создаёт новую ось и раскладывает массивы по ней. Если у вас два вектора длины 3 и вы их stack по axis=0, получится матрица (2, 3); по axis=1 — матрица (3, 2). Все входные массивы для stack должны иметь одинаковую форму.
import numpy as np
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
print(np.stack([a, b], axis=0)) # новая ось 0 -> (2, 3)
print(np.stack([a, b], axis=1)) # новая ось 1 -> (3, 2)
print(np.concatenate([a, b])) # без новой оси -> (6,)
Вывод:
[[1 2 3] [4 5 6]] [[1 4] [2 5] [3 6]] [1 2 3 4 5 6]
Запомните различие так: concatenate сохраняет число осей (склеивает «в длину»), stack увеличивает число осей на одну (складывает «в стопку»). Когда нужно собрать список одинаковых массивов в один многомерный (например, кадры видео в тензор), берут stack.
Почему 1D в vstack/hstack ведут себя по-разному
Поведение обёрток для одномерных входов — частый источник путаницы, поэтому проговорим его. vstack трактует каждый поданный 1D-вектор как строку будущей матрицы: подав три вектора длины 4, вы получите матрицу 3×4. А hstack для 1D просто склеивает векторы в один длинный — три вектора длины 4 дадут вектор длины 12, а не матрицу. Логика в том, что hstack склеивает вдоль «горизонтальной» оси, которая для вектора единственная, поэтому он удлиняет, а vstack добавляет новое «вертикальное» направление, превращая векторы в строки. Если вам нужны векторы именно как столбцы матрицы, есть column_stack. Эти различия не интуитивны, поэтому при работе с 1D-входами всегда проверяйте форму результата — одно лишнее или недостающее измерение тут возникает особенно легко.
Разбиение: split, hsplit, vsplit
Обратная операция — разрезать массив на части. np.split(a, n) делит на n равных кусков (массив должен делиться нацело), а np.split(a, [i, j]) режет в указанных позициях. Есть направленные варианты hsplit (по столбцам) и vsplit (по строкам).
import numpy as np
a = np.arange(12)
print(np.split(a, 3)) # три равных куска по 4
print(np.split(a, [2, 7])) # разрезы в позициях 2 и 7
m = np.arange(12).reshape(3, 4)
print(np.hsplit(m, 2)) # на две половины по столбцам
Вывод:
[array([0, 1, 2, 3]), array([4, 5, 6, 7]), array([ 8, 9, 10, 11])]
[array([0, 1]), array([2, 3, 4, 5, 6]), array([ 7, 8, 9, 10, 11])]
[array([[0, 1],
[4, 5],
[8, 9]]), array([[ 2, 3],
[ 6, 7],
[10, 11]])]
Результат split — это список массивов, а не один массив. Каждый кусок при этом является view на исходные данные.
split возвращает список, и это удобно
Результат любого из вариантов split — это список массивов, а не один многомерный массив. Поначалу это удивляет, но логично: куски могут иметь разную длину (при разрезании в произвольных позициях), и уложить их в один прямоугольный массив нельзя. Зато список удобно перебирать в цикле или распаковывать: left, right = np.hsplit(m, 2) сразу даёт два именованных куска. Частый сценарий — разрезать данные на блоки фиксированного размера и обработать каждый: for chunk in np.split(data, n): обработать(chunk). Важная деталь производительности: куски, которые возвращает split, обычно являются представлениями на исходный массив (для регулярных срезов), то есть память не дублируется. Поэтому разрезание большого массива на части дёшево — вы получаете «окна» в те же данные, а не их копии. Но это же означает, что запись в кусок изменит оригинал, как и с любым view.
Вставка и удаление: insert, delete, append
Для точечных изменений есть np.insert, np.delete, np.append. Все они возвращают новый массив (копию), а не меняют исходный — ведь размер ndarray фиксирован, и «вставить» означает построить массив большего размера заново. Поэтому в цикле их использовать дорого: каждый вызов перевыделяет память и копирует всё содержимое. Если предстоит много вставок и удалений, разумнее работать со списком Python, а в массив превратить готовый результат один раз в конце — это устраняет квадратичные накладные расходы.
import numpy as np
a = np.array([10, 20, 30, 40])
print(np.delete(a, 1)) # удалить элемент с индексом 1
print(np.insert(a, 2, 99)) # вставить 99 перед индексом 2
print(np.append(a, [50, 60])) # дописать в конец (новый массив!)
Вывод:
[10 30 40] [10 20 99 30 40] [10 20 30 40 50 60]
Подводные камни
- Путать concatenate и stack. Первый склеивает по существующей оси, второй создаёт новую. Это разные формы результата.
- Несовпадение форм. Для concatenate должны совпадать все оси, кроме оси склейки; для stack — все формы целиком.
- append/insert в цикле. Каждый вызов копирует весь массив — это квадратичная сложность. Накапливайте в списке Python.
- 1D в vstack/hstack. Поведение для векторов отличается от матриц; проверяйте форму результата.
Выбор между concatenate и stack: ход мысли
Решая, что применить, задайте себе один вопрос: результат должен иметь то же число осей, что и входы, или на одну больше? Если вы дописываете строки к таблице или столбцы к матрице — число осей сохраняется, это concatenate (или его обёртки vstack/hstack). Если вы собираете несколько однотипных объектов в новое измерение — например, кадры в видео, дни в куб «дни × строки × столбцы», образцы в батч — появляется новая ось, это stack. Наглядный признак: для stack все входы должны быть одинаковой формы (они станут «слоями» новой оси), тогда как для concatenate совпадать должны все оси, кроме оси склейки. Если перепутать, вы либо получите ошибку формы, либо массив не той размерности, что нужно дальше по коду. Поэтому привыкайте формулировать намерение словами — «сложить в стопку» против «приклеить в длину» — и инструмент выбирается сам.
Лучшие практики
- Используйте
concatenateдля склейки вдоль оси,stack— для сборки в новую ось. - Собирая много кусков, накапливайте их в обычном списке и объединяйте одним вызовом в конце.
- Избегайте
np.appendв циклах — это медленно из-за постоянного копирования. - Помните, что
splitвозвращает список массивов (часто views).
Сборка и разрезание массивов — повседневные операции при подготовке данных: объединить результаты по частям, собрать батч, разбить выборку на блоки. Запомнив главное различие (concatenate — вдоль оси, stack — в новую ось) и правило «накопить и объединить один раз», вы будете делать это и корректно, и эффективно.
Итог
concatenateсклеивает вдоль существующей оси, сохраняя число измерений.stackдобавляет новую ось, складывая одинаковые массивы «в стопку».vstack/hstack/dstack— удобные направленные обёртки;splitделит обратно.insert/delete/appendсоздают новый массив; в циклах они дороги.