Функции записи, которые принимают как массивы 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))
(последнее кажется быстрее)