Неожиданное поведение для назначения «iloc»

#python #pandas

Вопрос:

Рассмотрим следующий фрагмент кода:

 df = pd.DataFrame(data=[(i,i 1) for i in range(0,20,2)])
perm = list(np.random.permutation(10))
perm_df = df.iloc[perm, :]
perm_df.iloc[perm, :] = perm_df.values
 

Цель проста-один раз переставить фрейм данных, а затем вернуть его в исходную форму.

Индексация цепочки не используется, и предупреждение не выдается.

В результате получается следующее

     0   1
6  10  11
8   2   3
2   4   5
5  16  17
1  12  13
0  10  11
7  12  13
4  12  13
9  16  17
3  16  17
 

Я подозревал, что это может быть как-то связано с целочисленным индексом, но преобразование его в str тип не помогло.

Затем я закончил тем, что следовал золотому правилу копирования перед назначением: perm_df.iloc[perm, :] = perm_df.copy().values

и в итоге получил (частично) ожидаемый результат:

     0   1
6   0   1
8   2   3
2   4   5
5   6   7
1   8   9
0  10  11
7  12  13
4  14  15
9  16  17
3  18  19
 

Я понимаю, что индекс не изменится после iloc выполнения задания, поэтому reset_index сделаю свое дело.

Итак, вопрос в том, почему это perm_df.iloc[perm, :] = perm_df.values приводит к такому поведению? Это определенно связано с тем фактом , что я использовал то же DataFrame самое, но я ожидал values просто вернуть копию содержимого.

Мое предположение заключается в том, что под капотом values происходят изменения во время выполнения задания. Известно ли это поведение?

Комментарии:

1. Кстати, это не связано с пандами, это глупо. Попробуйте x = np.arange(20).reshape(10, 2); y = x[perm] то , что вы делаете (и что приводит к нежелательным результатам) y[perm] = y , потому что массив индексов используется для редактирования самого себя, поэтому он меняется по мере выполнения назначения. Что действительно работает, так это y[perm] = y.copy() .

Ответ №1:

Это поведение происходит из строки 981 pandas/core/internals/blocks.py setitem функции:

 values[indexer] = value
 

Та же настройка, что и раньше:

 import numpy as np
import pandas as pd

np.random.seed(5)
df = pd.DataFrame(data=[(i, i   1) for i in range(0, 20, 2)])
perm = list(np.random.permutation(10))
perm_df = df.iloc[perm, :]
 

Обратите внимание на разницу между:

 value = perm_df.values
values = perm_df.values
values[(perm, slice(None, None, None))] = value
 

values :

 [[ 0  1]
 [10 11]
 [ 4  5]
 [18 19]
 [ 8  9]
 [10 11]
 [ 8  9]
 [ 8  9]
 [ 8  9]
 [18 19]]
 

И

 value = perm_df.copy().values
values = perm_df.values
values[(perm, slice(None, None, None))] = value
 

values :

 [[ 0  1]
 [ 2  3]
 [ 4  5]
 [ 6  7]
 [ 8  9]
 [10 11]
 [12 13]
 [14 15]
 [16 17]
 [18 19]]
 

Since arrays are mutable, by not copying the values , only one array exists, and that single array is being updated iteratively.

Процесс назначения выглядит примерно так:

perm :

 [9, 5, 2, 4, 7, 1, 0, 8, 6, 3]
 

values :

 [[18 19]  # 0 
 [10 11]  # 1 
 [ 4  5]  # 2 
 [ 8  9]  # 3 
 [14 15]  # 4 
 [ 2  3]  # 5 
 [ 0  1]  # 6 
 [16 17]  # 7 
 [12 13]  # 8 
 [ 6  7]] # 9 
 

0 -> 9

 [[18 19]  # 0 
 [10 11]  # 1 
 [ 4  5]  # 2 
 [ 8  9]  # 3 
 [14 15]  # 4 
 [ 2  3]  # 5 
 [ 0  1]  # 6 
 [16 17]  # 7 
 [12 13]  # 8 
 [18 19]] # 9 ([ 6  7])
 

1 -> 5 :

 [[18 19]  # 0 
 [10 11]  # 1 
 [ 4  5]  # 2 
 [ 8  9]  # 3 
 [14 15]  # 4 
 [10 11]  # 5 ([ 2  3])
 [ 0  1]  # 6 
 [16 17]  # 7 
 [12 13]  # 8 
 [18 19]] # 9 
 

2 -> 2 :

 [[18 19]  # 0 
 [10 11]  # 1 
 [ 4  5]  # 2 ([ 4  5])
 [ 8  9]  # 3 
 [14 15]  # 4 
 [10 11]  # 5 
 [ 0  1]  # 6 
 [16 17]  # 7 
 [12 13]  # 8 
 [18 19]] # 9 
 

3 -> 4 :

 [[18 19]  # 0 
 [10 11]  # 1 
 [ 4  5]  # 2 
 [ 8  9]  # 3 
 [ 8  9]  # 4 ([14 15])
 [10 11]  # 5 
 [ 0  1]  # 6 
 [16 17]  # 7 
 [12 13]  # 8 
 [18 19]] # 9 
 

4 -> 7 :

 [[18 19]  # 0 
 [10 11]  # 1 
 [ 4  5]  # 2 
 [ 8  9]  # 3 
 [ 8  9]  # 4 
 [10 11]  # 5 
 [ 0  1]  # 6 
 [ 8  9]  # 7 ([16 17])
 [12 13]  # 8 
 [18 19]] # 9 
 

5 -> 1 :

 [[18 19]  # 0 
 [10 11]  # 1 
 [ 4  5]  # 2 
 [ 8  9]  # 3 
 [ 8  9]  # 4 
 [10 11]  # 5 ([10 11])
 [ 0  1]  # 6 
 [ 8  9]  # 7 
 [12 13]  # 8 
 [18 19]] # 9 
 

6 -> 0 :

 [[ 0  1]  # 0 ([18 19])
 [10 11]  # 1 
 [ 4  5]  # 2 
 [ 8  9]  # 3 
 [ 8  9]  # 4 
 [10 11]  # 5 
 [ 0  1]  # 6 
 [ 8  9]  # 7 
 [12 13]  # 8 
 [18 19]] # 9 
 

7 -> 8 :

 [[ 0  1]  # 0 
 [10 11]  # 1 
 [ 4  5]  # 2 
 [ 8  9]  # 3 
 [ 8  9]  # 4 
 [10 11]  # 5 
 [ 0  1]  # 6 
 [ 8  9]  # 7 
 [ 8  9]  # 8 ([12 13])
 [18 19]] # 9 
 

8 -> 6 :

 [[ 0  1]  # 0 
 [10 11]  # 1 
 [ 4  5]  # 2 
 [ 8  9]  # 3 
 [ 8  9]  # 4 
 [10 11]  # 5 
 [ 8  9]  # 6 ([ 0  1])
 [ 8  9]  # 7 
 [ 8  9]  # 8 
 [18 19]] # 9 
 

9 -> 3 :

 [[ 0  1]  # 0 
 [10 11]  # 1 
 [ 4  5]  # 2 
 [18 19]  # 3 ([ 8  9])
 [ 8  9]  # 4 
 [10 11]  # 5 
 [ 8  9]  # 6 
 [ 8  9]  # 7 
 [ 8  9]  # 8 
 [18 19]] # 9 
 

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


Примечание. вызов copy фрейма данных сразу после iloc этого не решает эту проблему, поскольку проблема заключается в дублировании ссылки на один и тот же (базовый) массив, а не в ссылке на прежний фрейм данных ( df ):

 perm_df = df.iloc[perm, :].copy()
perm_df.iloc[perm, :] = perm_df.values
 

perm_df :

 [[ 0  1]
 [10 11]
 [ 4  5]
 [18 19]
 [ 8  9]
 [10 11]
 [ 8  9]
 [ 8  9]
 [ 8  9]
 [18 19]]
 

Комментарии:

1. Спасибо, это именно то, что я подозревал. Чтобы завершить это и то, и iloc другое и values вернуть ссылку на базовый массив, правильно ли это? Кроме того, насколько я понимаю , эта проблема несколько специфична для использования перестановок — в случае, если мы используем какое-либо преобразование с сохранением индекса values , мы в целости и сохранности.

2. Да values возвращает ссылку на базовый массив и iloc изменяет этот же массив. Эта проблема на самом деле относится к любому numpy массиву, который изменяет себя. (именно для этого и решается этот вопрос)

Ответ №2:

В Pandas индексирование фрейма данных возвращает ссылку на исходный фрейм данных. Таким образом, изменение подмножества приведет к изменению исходного кадра данных. Таким образом, вы захотите использовать копию, если хотите убедиться, что исходный кадр данных не должен изменяться. Рассмотрим следующий код:

 df = DataFrame({'x': [1,2]})
df_sub = df[0:1]
df_sub.x = -1
print(df)
 

Вы получите

 x
0 -1
1  2
 

в то время как следующее оставляет df неизменным

 df_sub_copy = df[0:1].copy()
df_sub_copy.x = -1  
 

панды используют форму, известную как глубокая копия
https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.copy.html

Комментарии:

1. Проблема здесь заключалась не в том, какой объект изменяется, а скорее в том, как он был изменен. Тот факт, что изменение происходит итеративно что оно относится к ранее измененным записям во время этой итерации, является причиной расхождения.