Назначение массива numage num 2 для pandas столбца DataFrame ведет себя непоследовательно
Ive заметил, что назначение на pandas
DataFrame
колонок ( с использованием .loc
индексатора) ведет себя по- разному в зависимости от того, каких других столбцов присутствуют в DataFrame
и точной форме задания. Используя три примера DataFrame
s:
df1 = pandas.DataFrame({
'col1': [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
})
# col1
# 0 [1, 2, 3]
# 1 [4, 5, 6]
# 2 [7, 8, 9]
df2 = pandas.DataFrame({
'col1': [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
'col2': [[10, 20, 30], [40, 50, 60], [70, 80, 90]]
})
# col1 col2
# 0 [1, 2, 3] [10, 20, 30]
# 1 [4, 5, 6] [40, 50, 60]
# 2 [7, 8, 9] [70, 80, 90]
df3 = pandas.DataFrame({
'col1': [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
'col2': [1, 2, 3]
})
# col1 col2
# 0 [1, 2, 3] 1
# 1 [4, 5, 6] 2
# 2 [7, 8, 9] 3
x = numpy.array([[111, 222, 333],
[444, 555, 666],
[777, 888, 999]])
Ive нашел следующее:
-
df1
:
-
df1.col1 = x
Результат:
df1
# col1
# 0 111
# 1 444
# 2 777
-
df1.loc[:, 'col1'] = x
Результат:
df1
# col1
# 0 111
# 1 444
# 2 777
-
df1.loc[0:2, 'col1'] = x
Результат:
# […]
# ValueError: could not broadcast input array from shape (3,3) into shape (3)
-
df2
:
-
df2.col1 = x
Результат:
df2
# col1 col2
# 0 111 [10, 20, 30]
# 1 444 [40, 50, 60]
# 2 777 [70, 80, 90]
-
df2.loc[:, 'col1'] = x
Результат:
df2
# col1 col2
# 0 111 [10, 20, 30]
# 1 444 [40, 50, 60]
# 2 777 [70, 80, 90]
-
df2.loc[0:2, 'col1'] = x
Результат:
# […]
# ValueError: could not broadcast input array from shape (3,3) into shape (3)
-
df3
:
-
df3.col1 = x
Результат:
df3
# col1 col2
# 0 111 1
# 1 444 2
# 2 777 3
-
df3.loc[:, 'col1'] = x
Результат:
# ValueError: Must have equal len keys and value when setting with an ndarray
-
df3.loc[0:2, 'col1'] = x
Результат:
# ValueError: Must have equal len keys and value when setting with an ndarray
Таким образом, кажется, что df.loc
ведет себя по-другому, если один из других столбцов в DataFrame
не имеет object
dtype.
Мой вопрос:
- Почему присутствие других столбцов имеет значение в этом назначении?
- Почему разные версии задания не эквивалентны? В частности, почему результат в случаях, которые не приводят к
ValueError
что столбец DataFrame
заполняется значениями первого столбца массива numpy
?
Примечание. Мне не интересно обсуждать, имеет ли смысл назначать столбец массиву numpy
таким образом. Я только хочу знать о различиях в поведении и может ли это считаться ошибкой.
Ответы
Ответ 1
Почему присутствие других столбцов имеет значение в этом назначении?
Простой ответ заключается в том, что Pandas проверяет смешанные типы в кадре данных. Вы можете проверить это самостоятельно, используя тот же метод, который используется в исходном коде:
print(df1._is_mixed_type) # False
print(df2._is_mixed_type) # False
print(df3._is_mixed_type) # True
Используемая логика отличается от значения _is_mixed_type
. В частности, следующий тест в _setitem_with_indexer
терпит неудачу, если _is_mixed_type
имеет значение True
для предоставленных вами входов:
if len(labels) != value.shape[1]:
raise ValueError('Must have equal len keys and value '
'when setting with an ndarray')
Другими словами, в массиве больше столбцов, чем в столбце данных назначаются столбцы.
Это ошибка? На мой взгляд, любое использование списков или массивов в кадре данных Pandas чревато опасностью. 1 Была добавлена проверка ValueError
для исправления более важной проблемы (GH 7551).
Почему разные версии задания не эквивалентны?
Причина, по которой назначение через df3['col1'] = x
работает, потому что col1
является существующей серией. Попробуйте df3['col3'] = x
и ваш код будет терпеть неудачу с ValueError
.
Копаем глубже, __setitem__
для datframe, для которого df[]
является синтаксическим сахаром, преобразует 'col1'
в серию (если она существует) через key = com._apply_if_callable(key, self)
:
def _apply_if_callable(maybe_callable, obj, **kwargs):
"""
Evaluate possibly callable input using obj and kwargs if it is callable,
otherwise return as it is
"""
if callable(maybe_callable):
return maybe_callable(obj, **kwargs)
return maybe_callable
Затем логика может обойти проверку логики в _setitem_with_indexer
. Вы можете сделать это, потому что мы переходим к _setitem_array
вместо _set_item
когда мы предоставляем ярлык для существующей серии:
def __setitem__(self, key, value):
key = com._apply_if_callable(key, self)
if isinstance(key, (Series, np.ndarray, list, Index)):
self._setitem_array(key, value)
elif isinstance(key, DataFrame):
self._setitem_frame(key, value)
else:
self._set_item(key, value)
Все вышеперечисленные детали реализации; вы не должны основывать свой синтаксис Pandas на основе этих базовых методов, так как они могут меняться вперед.
1 Я бы сказал, что он должен быть отключен по умолчанию и включен только через настройку. Это чрезвычайно неэффективный способ хранения и обработки данных. Иногда он предлагает краткосрочное удобство, но за счет запутанного кода вниз по линии.
Ответ 2
Во-первых, позвольте мне попробовать менее техничную и менее строгую версию объяснения @jpp. Вообще говоря, когда вы пытаетесь вставить массив numpy в кадр данных pandas, pandas ожидает, что они будут иметь одинаковый ранг и размер (например, оба они равны 4x2, хотя это также может быть ОК, если ранг массива numpy ниже, чем pandas Например, если размер pandas равен 4x2, а размер numpy равен 4x1 или 2x1 - просто прочитайте на широковещании numpy для получения дополнительной информации).
Точка в предыдущем случае просто заключается в том, что при попытке поместить массив 3x3 numpy в столбец pandas длиной 3 (в основном 3x1), pandas на самом деле не имеет стандартного способа справиться с этим, а непоследовательное поведение - это просто результат того, что. Было бы лучше, если бы панды всегда вызывали исключение, но, вообще говоря, панды пытаются что-то сделать, но это может быть не что-то полезное.
Во-вторых, (и я понимаю, что это не буквальный ответ) в конечном итоге я могу гарантировать, что вам будет намного лучше, если вы не потратите много времени на разработку подробностей о том, как перерезать двумерные массивы в один pandas. Вместо этого просто следуйте более типичному подходу pandas, например следующему, который будет генерировать код, который (1) ведет себя более предсказуемо, (2) более читабельен, и (3) выполняется намного быстрее.
x = np.arange(1,10).reshape(3,3)
y = x * 10
z = x * 100
df = pd.DataFrame( np.hstack((x,y)), columns=['x1 x2 x3 y1 y2 y3'.split()] )
# x1 x2 x3 y1 y2 y3
# 0 1 2 3 10 20 30
# 1 4 5 6 40 50 60
# 2 7 8 9 70 80 90
df.loc[:,'x1':'x3'] = z
# x1 x2 x3 y1 y2 y3
# 0 100 200 300 10 20 30
# 1 400 500 600 40 50 60
# 2 700 800 900 70 80 90
Я сохранил это как простой индекс, но похоже, что то, что вы пытаетесь сделать, - это создать более иерархическую структуру, и панды могут там помочь, с функцией MultiIndex. В этом случае результатом является более чистый синтаксис, но обратите внимание, что его может быть сложнее использовать в других случаях (не стоит вдаваться в подробности здесь):
df = pd.DataFrame( np.hstack((x,y)),
columns=pd.MultiIndex.from_product( [list('xy'),list('123')] ) )
df.loc[:,'x'] = z # now you can replace 'x1':'x3' with 'x'
И вы, вероятно, знаете это, но также чрезвычайно легко извлечь массивы numpy из dataframes, поэтому вы ничего не потеряли, просто поставив массив numpy в несколько столбцов. Например, в случае с несколькими индексами:
df.loc[:,'x'].values
# array([[100, 200, 300],
# [400, 500, 600],
# [700, 800, 900]])