Назначение массива 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 нашел следующее:

  1. df1:

    1. df1.col1 = x

      Результат:

      df1
      #    col1
      # 0   111
      # 1   444
      # 2   777
      
    2. df1.loc[:, 'col1'] = x

      Результат:

      df1
      #    col1
      # 0   111
      # 1   444
      # 2   777
      
    3. df1.loc[0:2, 'col1'] = x

      Результат:

      # […]
      # ValueError: could not broadcast input array from shape (3,3) into shape (3)
      
  2. df2:

    1. df2.col1 = x

      Результат:

      df2
      #    col1          col2
      # 0   111  [10, 20, 30]
      # 1   444  [40, 50, 60]
      # 2   777  [70, 80, 90]
      
    2. df2.loc[:, 'col1'] = x

      Результат:

      df2
      #    col1          col2
      # 0   111  [10, 20, 30]
      # 1   444  [40, 50, 60]
      # 2   777  [70, 80, 90]
      
    3. df2.loc[0:2, 'col1'] = x

      Результат:

      # […]
      # ValueError: could not broadcast input array from shape (3,3) into shape (3)
      
  3. df3:

    1. df3.col1 = x

      Результат:

      df3
      #    col1  col2
      # 0   111     1
      # 1   444     2
      # 2   777     3
      
    2. df3.loc[:, 'col1'] = x

      Результат:

      # ValueError: Must have equal len keys and value when setting with an ndarray
      
    3. 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]])