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

На предыдущем занятии мы немного познакомились со способами изменения формы массивов пакета NumPy, то есть изменением их размерностей. На этом занятии поближе рассмотрим эту тему и познакомимся с наиболее употребительными функциями и свойствами, влияющих на представление массивов.

Изменение размерности массивов

Предположим, у нас имеется массив, состоящий из десяти чисел:

a = np.arange(10)  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Мы уже знаем, что для изменения формы этого массива, достаточно указать свойству shape кортеж с новыми размерами, например, так:

a.shape = 2, 5   # массив размерностью 2x5

В результате изменится представление массива, на которое ссылается переменная a. Если же требуется создать новое представление массива, сохранив прежнее, то следует воспользоваться методом reshape():

b = a.reshape(10)  # массив [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

И, как мы с вами говорили на предыдущем занятии, ссылки b и a будут использовать одни и те же данные, то есть, изменение массива через b:

b[0] = -1

приведет к изменению соответствующего элемента массива a:

array([[-1,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9]])

Об этом всегда следует помнить. Также следует помнить, что у свойства shape и метода reshape() размерность должна охватывать все элементы массива. Например, вот такая команда:

a.shape = 3, 3

приведет к ошибке, т.к. размерность 3x3 = 9 элементов, а в массиве 10 элементов. Здесь всегда должно выполняться равенство:

n1 x n2 x … x nN = число элементов массива

Но допускается делать так:

a.shape = -1, 2  # размерность 5x2

Отрицательное значение -1 означает автоматическое вычисление размерности по первой оси. По второй берется значение 2. В этом случае получим размерность 5x2.

То же самое можно делать и в методе reshape():

b.reshape(-1, 1) # размерность 10x1
b.reshape(1, -1) # размерность 1x10

Обратите внимание, в последних двух случаях мы получаем представления двумерных массивов, преобразуя одномерный. Это важный момент, так как на выходе метода reshape() получается матрица с двумя осями (строки и столбцы), тогда как изначально массив b имел только одну ось. Не случайно последнее представление отображается с двумя квадратными скобками:

array([[-1,  1,  2,  3,  4,  5,  6,  7,  8,  9]])

Первая скобка – это первая ось (строка), а вторая скобка (вторая ось) описывает столбцы. Одномерный же массив b отображается с одной квадратной скобкой:

array([-1,  1,  2,  3,  4,  5,  6,  7,  8,  9])

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

b.reshape(2, -1)  # размерность 2x5
b.reshape(-1, 2)  # размерность 5x2

Первое представление (2x5) отображается следующим образом:

array([[-1,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9]])

Здесь снова мы видим две квадратные скобки (значит, массив двумерный). Первая описывает ось axis0, отвечающую за строки, а вложенные скобки описывают вторую ось axis1, отвечающую за столбцы.

Если нам требуется многомерный массив преобразовать в обычный одномерный, то можно воспользоваться методом ravel(), который возвращает новое представление, не меняя текущего:

c = b.ravel() # с ссылается на одномерное представление массива

Если же нам нужно текущий массив преобразовать в одномерный, то это можно сделать так:

b.shape = -1

Помимо свойства shape можно использовать метод resize, который выполняет подобную операцию с текущим массивом. Например:

a.resize(2, 5) # массив 2x5

Но, как мы уже говорили, вот такая строчка приведет к ошибке:

a.resize(3, 3)  # ошибка: 3x3 != 10

Однако, мы все-таки можем выполнить такую операцию, указав дополнительно флаг refcheck=False:

a.resize(3, 3, refcheck=False)  # массив 3x3

или так:

a.resize(4, 5, refcheck=False) # массив 4x5

В первом случае происходит удаление одного 10-го элемента, а во втором случае – добавление 4∙5 - 3∙3 = 11 нулей. Это очень удобно, когда нам нужно менять не только форму, но и размер самого массива.

Транспонирование матриц и векторов

Очень часто в математических операциях требуется выполнять транспонирование матриц и векторов, то есть, заменять строки на столбцы. Например, если имеется матрица (двумерный массив):

a = np.array([(1, 2, 3), (1, 4, 9), (1, 8, 27)])

то операция транспонирования может быть реализована так:

b = a.T

Обратите внимание, мы здесь создаем лишь новое представление тех же самых данных массива a. И изменение элементов в массиве b:

b[0, 1] = 10

приведет к соответствующему изменению значения элемента и массива a. Это следует помнить, используя операцию транспонирования.

Транспонирование векторов работает несколько иначе. Предположим, имеется одномерный массив:

x = np.arange(1, 10)

и мы выполняем операцию транспонирования:

x.T

В результате ничего не изменилось: вектор как был строкой, так строкой и остался. Почему? Дело в том, что массив x имеет только одну размерность, поэтому здесь нет понятия строк и столбцов. Соответственно, операция транспонирования ни к чему не приводит. Чтобы получить ожидаемый эффект, нужно добавить к массиву еще одну ось, например, так:

x.shape = 1, -1

И теперь, при транспонировании получим вектор-столбец:

x.T # вектор-столбец 9x1

Добавление и удаление осей

Часто при работе с массивами NumPy требуется добавлять новые оси измерений и удалять существующие. Есть множество способов выполнять эти операции, но мы рассмотрим два наиболее распространенных с помощью функций:

  • np.expand_dims(a, axis) – добавление новой оси;
  • np.squeeze(a[, axis]) – удаление оси (без удаления элементов).

Давайте предположим, что у нас имеется некий многомерный массив:

x_test = np.arange(32).reshape(8, 2, 2) # массив 8x2x2

И нам потребовалось добавить еще одно измерение (ось), причем, в самое начало, то есть, ось axis0. Сейчас на этой оси 8 элементов – матриц 2x2, но мы хотим сделать четырехмерный массив, сохранив остальные три оси и их данные без изменений. Как раз это достаточно просто сделать с помощью функции expand_dims, следующим образом:

x_test4 = np.expand_dims(x_test, axis=0)

Обращаясь к свойству shape:

x_test4.shape # (1, 8, 2, 2)

Видим, что массив стал четырехмерным и первая добавленная ось axis0 содержит один элемент – трехмерный массив 8x2x2. При необходимости, мы всегда можем добавить новый элемент на эту ось:

a = np.append(x_test4, x_test4, axis=0) # размерность (2, 8, 2, 2)

или удалить ненужные элементы:

b = np.delete(a, 0, axis=0) # размерность (1, 8, 2, 2)

Здесь второй параметр 0 – индекс удаляемого элемента на оси axis0.

Если нам нужно добавить последнюю ось в массиве, то для этого можно записать такую команду:

b = np.expand_dims(x_test4, axis=-1) # размерность (1, 8, 2, 2, 1)

Отрицательный индекс -1 – это следующая с конца ось. Если указать индекс -2, то добавится предпоследняя ось и так далее. Отрицательные индексы очень удобно использовать при работе с массивами произвольных размерностей.

Следующая функция squeeze позволяет удалить все оси с одним элементом. Например, строчка:

c = np.squeeze(b) # размерность (8, 2, 2)

превращает массив размерностью (1, 8, 2, 2) в массив размерностью (8, 2, 2). При необходимости, дополнительно мы можем самостоятельно указать оси, которые следует удалять, например, так:

c = np.squeeze(b, axis=0) # удалит только ось axis0, не затронув другие

Но, если указать ось с числом элементов больше 1, то возникнет ошибка:

c = np.squeeze(b, axis=1) # ошибка, на оси axis1 8 элементов

Объект newaxis

В NumPy добавлять новые оси иногда удобнее с помощью специального объекта np.newaxis. Например, пусть у нас есть одномерный массив:

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

У него одна ось – одно измерение. Добавим еще одну ось, допустим, в начало. С помощью объекта np.newaxis это можно сделать так:

b = a[np.newaxis, :] # добавление оси axis0
b.shape # (1, 9)

Или, можно прописать сразу две оси:

c = a[np.newaxis, :, np.newaxis]
c.shape # (1, 9, 1)

Как видите, это достаточно удобная операция.