Функции записи, которые принимают как массивы 1-D, так и 2-D numpy?

Мое понимание заключается в том, что 1-D массивы в numpy могут быть интерпретированы как вектор, ориентированный на столбцы, или вектор, ориентированный на строку. Например, одномерный массив с формой (8,) можно рассматривать как 2-мерный массив формы (1,8) или shape (8,1) в зависимости от контекста.

Проблема, с которой я сталкиваюсь, заключается в том, что функции, которые я пишу для манипулирования массивами, имеют тенденцию хорошо обобщаться в двумерном случае для обработки как векторов, так и матриц, но не так хорошо в 1-D случае.

Таким образом, мои функции в конечном итоге делают что-то вроде этого:

if arr.ndim == 1:
    # Do it this way
else:
    # Do it that way

Или даже это:

# Reshape the 1-D array to a 2-D array
if arr.ndim == 1:
    arr = arr.reshape((1, arr.shape[0]))

# ... Do it the 2-D way ...

То есть, я нахожу, что могу обобщать код для обработки двумерных случаев (r,1), (1,c), (r,c), но не в 1-D случаях без разветвления или изменения.

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

Итак, мой вопрос: я пропустил какую-то лучшую идиому? Является ли описанный выше шаблон типичным для numpy-кода?

Кроме того, в качестве связанного аспекта принципов проектирования API, если вызывающий абонент передает 1-мерный массив некоторой функции, которая возвращает новый массив, а возвращаемое значение также является вектором, является ли распространенной практикой изменять 2- D vector (r,1) или (1,c) вернуться к 1-D массиву или просто документировать, что функция возвращает 2-мерный массив независимо?

Спасибо

Ответы

Ответ 1

Я думаю, что в целом функции NumPy, для которых требуется массив формы (r,c), не предусматривают специального учета для 1-D массивов. Вместо этого они ожидают, что пользователь либо передаст массив формы (r,c) в точности, либо для пользователя, чтобы передать 1-D массив, который трансляции до формы (r,c).

Если вы передадите такую ​​функцию 1-мерным массивом формы (c,), она будет транслироваться в форму (1,c), поскольку трансляция добавляет новые оси слева. Он также может транслироваться для формирования (r,c) для произвольного r (в зависимости от того, к какому другому массиву он сочетается).

С другой стороны, если у вас есть 1-D массив x, формы (r,), и вам нужно, чтобы он транслировался до формы (r,c), тогда NumPy ожидает, что пользователь передаст массив формы (r,1), поскольку трансляция не добавит новые оси справа.

Для этого пользователь должен передать x[:,np.newaxis] вместо x.


Что касается возвращаемых значений: я считаю, что лучше всегда возвращать 2-мерный массив. Если пользователь знает, что выход будет иметь форму (1,c) и хочет получить 1-мерный массив, пусть она срежет с 1-мерного массива x[0].

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

Кроме того, трансляция размывает различие между 1-D массивом формы (c,) и 2-мерным массивом формы (r,c). Если ваша функция возвращает 1-мерный массив при подаче 1-D входа и 2-мерного массива при подаче 2-D входа, то ваша функция делает различие строгим, а не размытым. Стилистически это напоминает мне проверку if isinstance(obj,type), которая идет вразрез с текстурой утки. Не делайте этого, если вам не нужно.

Ответ 2

unutbu объяснение хорошее, но я не согласен с измерением возврата.

Внутренний шаблон функции зависит от типа функции.

Сокращение операций с аргументом оси часто может быть записано так, что число измерений не имеет значения.

У Numpy также есть функция atleast_2d (и atleast_1d), которая также широко используется, если вам нужен явный массив 2d. В статистике я иногда использую такую ​​функцию, как atleast_2d_cols, которая преобразует 1d (r,) в 2d (r, 1) для кода, который ожидает 2d, или если входной массив равен 1d, тогда для интерпретации и линейной алгебры требуется вектор-столбец. (перестройка дешевая, так что это не проблема)

В третьем случае у меня могут быть разные коды кода, если более низкий размерный размер можно сделать дешевле или проще, чем в случае с более высоким размером. (пример: если для 2d требуется несколько точечных продуктов.)

возвращаемое измерение

Я думаю, что не следовать соглашению numpy с измерением return может быть очень запутанным для пользователей для общих функций. (функции, относящиеся к теме, могут быть разными). Например, уменьшите операции, потеряв один размер.

Для многих других функций выходной размер соответствует входному размеру. Я думаю, что 1d-вход должен иметь 1d-выход, а не дополнительный избыточный размер. За исключением функций в linalg, я не помню никаких функций, которые возвращают избыточное дополнительное измерение. (Случай скалярного и одноэлементного массива не всегда согласован.)

Стилистически это напоминает мне проверку isinstance:

Попробуйте без него, если вы допустили, например, для матриц numpy и маскированных массивов. Вы получите забавные результаты, которые нелегко отладить. Хотя для большинства функций numpy и scipy пользователь должен знать, будет ли тип массива работать с ними, поскольку существует несколько проверок isinstance, и asarray может не всегда поступать правильно.

Как пользователь, я всегда знаю, какой тип "array_like" у меня есть, список, кортеж или подкласс класса, особенно когда я использую умножение.

np.array(np.eye(3).tolist()*3)
np.matrix(range(3)) * np.eye(3)
np.arange(3) * np.eye(3)

другой пример: что это делает?

>>> x = np.array(tuple(range(3)), [('',int)]*3)
>>> x
array((0, 1, 2), 
      dtype=[('f0', '<i4'), ('f1', '<i4'), ('f2', '<i4')])
>>> x * np.eye(3)

Ответ 3

Это хорошее применение для декораторов

def atmost_2d(func):
  def wrapr(x):
    return func(np.atleast_2d(x)).squeeze()
  return wrapr

Например, эта функция выберет последний столбец своего ввода.

@atmost_2d
def g(x):
  return x[:,-1]

Но: он работает для:

1d:

In [46]: b
Out[46]: array([0, 1, 2, 3, 4, 5])

In [47]: g(b)
Out[47]: array(5)

2d:

In [49]: A
Out[49]:
array([[0, 1],
       [2, 3],
       [4, 5]])

In [50]: g(A)
Out[50]: array([1, 3, 5])

0d:

In [51]: g(99)
Out[51]: array(99)

Этот ответ основывается на предыдущих двух.

Ответ 4

У этого вопроса уже есть очень хорошие ответы. Здесь я просто хочу добавить то, что я обычно делаю (что как-то суммирует ответы других), когда я хочу писать функции, которые принимают широкий диапазон входов, в то время как для операций, которые я делаю на них, требуется вектор 2-й строки или столбца.

  • Если я знаю, что вход всегда 1d (массив или список):

    а. если мне нужна строка: x = np.asarray(x)[None,:]

    б. если мне нужен столбец: x = np.asarray(x)[:,None]

  • Если вход может быть либо 2d (массив или список) с правой формой или 1d (который необходимо преобразовать в 2d строку/столбец):

    а. если мне нужна строка: x = np.atleast_2d(x)

    б. если мне нужен столбец: x = np.atleast_2d(np.asarray(x).T).T или x = np.reshape(x, (len(x),-1)) (последнее кажется быстрее)