使用字符串和浮点数字典进行Pandas DataFrame赋值时出现Bug?

11

问题

Pandas似乎支持使用df.loc将字典分配给行条目,如下所示:

df = pd.DataFrame(columns = ['a','b','c'])
entry = {'a':'test', 'b':1, 'c':float(2)}
df.loc[0] = entry

正如预期的那样,Pandas根据字典键将字典值插入到相应的列中。打印结果如下:

      a  b    c
0  test  1  2.0

然而,如果您覆盖相同的条目,Pandas将分配字典而不是字典值。打印此内容如下:

   a  b  c
0  a  b  c

问题

为什么会发生这种情况?

具体来说,为什么这种情况只会在第二次分配时发生?所有后续的分配都会恢复到原始结果,包含(几乎)预期值:

      a  b  c
0  test  1  2

我说“几乎”是因为对于所有后续的结果,c 上的 dtype 实际上是一个 object 而不是 float


我已经确定,只要涉及到字符串和浮点数,就会出现这种情况。如果只涉及字符串和整数,或整数和浮点数,就不会出现这种行为。

示例代码

df = pd.DataFrame(columns = ['a','b','c'])
print(f'empty df:\n{df}\n\n')

entry = {'a':'test', 'b':1, 'c':float(2.3)}
print(f'dictionary to be entered:\n{entry}\n\n')

df.loc[0] = entry
print(f'df after entry:\n{df}\n\n')

df.loc[0] = entry
print(f'df after second entry:\n{df}\n\n')

df.loc[0] = entry
print(f'df after third entry:\n{df}\n\n')

df.loc[0] = entry
print(f'df after fourth entry:\n{df}\n\n')

这会产生以下打印输出:
empty df:
Empty DataFrame
Columns: [a, b, c]
Index: []


dictionary to be entered:
{'a': 'test', 'b': 1, 'c': float(2)}


df after entry:
      a  b    c
0  test  1  2.0


df after second entry:
   a  b  c
0  a  b  c


df after third entry:
      a  b  c
0  test  1  2


df after fourth entry:
      a  b  c
0  test  1  2

2
有趣的发现。在 pandas 版本 1.2.4 上,所有 后续的数据框都具有值 a b c,而不仅仅是第二个数据框。 - aneroid
即使您将其包装在 pd.Series() 中,@aneroid? - rudolfovic
@rudolfovic 把它包装成一个系列就可以解决问题。但我不关心这个解决方法。期望的行为与观察到的行为不一致。df.loc[0] = entry.values() 也可以工作,但这又是一个解决方法。只有在分配给新行时才能正常工作。 - aneroid
在版本 1.1.5 中可以复现,因此在 1.1.51.2.4 之间某处进行了更改。 - DeepSpace
1
文档中没有说明可以传递字典。 - rudolfovic
4
我认为这应该成为 https://github.com/pandas-dev/pandas 上的一个问题。 - DeepSpace
2个回答

10

1.2.4版本的行为如下:

empty df:
Empty DataFrame
Columns: [a, b, c]
Index: []


dictionary to be entered:
{'a': 'test', 'b': 1, 'c': 2.3}


df after entry:
      a  b    c
0  test  1  2.3


df after second entry:
   a  b  c
0  a  b  c


df after third entry:
   a  b  c
0  a  b  c


df after fourth entry:
   a  b  c
0  a  b  c

第一次运行df.loc[0]函数时,_setitem_with_indexer_missing函数被调用,因为轴上没有索引0:

运行以下代码:

elif isinstance(value, dict):
    value = Series(
        value, index=self.obj.columns, name=indexer, dtype=object
    )

dict转换为系列,它会按预期运行。


在未来,由于索引不缺失(存在一个索引0),将运行_setitem_with_indexer_split_path
elif len(ilocs) == len(value):
    # We are setting multiple columns in a single row.
    for loc, v in zip(ilocs, value):
        self._setitem_single_column(loc, v, pi)

这只是将列位置与dict中的每个值压缩在一起:

在这种情况下,大致相当于:

entry = {'a': 'test', 'b': 1, 'c': float(2.3)}
print(list(zip([0, 1, 2], entry)))
# [(0, 'a'), (1, 'b'), (2, 'c')]

因此,这就是为什么值成为键的原因。
因此,这个问题并不像表面看起来的那么具体:
import pandas as pd

df = pd.DataFrame([[1, 2, 3]], columns=['a', 'b', 'c'])
print(f'df:\n{df}\n\n')

entry = {'a': 'test', 'b': 1, 'c': float(2.3)}
print(f'dictionary to be entered:\n{entry}\n\n')

df.loc[0] = entry
print(f'df after entry:\n{df}\n\n')

initial df:
   a  b  c
0  1  2  3

dictionary to be entered:
{'a': 'test', 'b': 1, 'c': 2.3}

df after entry:
   a  b  c
0  a  b  c

如果索引 loc 存在,则不会将其转换为系列:它只是将 locs 列与可迭代对象一起压缩。在字典的情况下,这意味着键是包含在框架中的值。这也很可能是为什么仅返回其值的迭代器的可迭代对象是 loc 分配的左操作数可接受的原因。
我也同意@DeepSpace的观点,认为这应该作为一个bug提出。

1.1.5行为如下:

初始分配与1.2.4相同,但是:

这里需要注意的是数据类型:

import pandas as pd

df = pd.DataFrame({0: [1, 2, 3]}, columns=['a', 'b', 'c'])

entry = {'a': 'test', 'b': 1, 'c': float(2.3)}

# First Entry
df.loc[0] = entry
print(df.dtypes)
# a     object
# b     object
# c    float64
# dtype: object

# Second Entry
df.loc[0] = entry
print(df.dtypes)
# a    object
# b    object
# c    object
# dtype: object

# Third Entry
df.loc[0] = entry
print(df.dtypes)
# a    object
# b    object
# c    object
# dtype: object

# Fourth Entry
df.loc[0] = entry
print(df.dtypes)
# a    object
# b    object
# c    object
# dtype: object

\{\{值得注意的原因是当\}\}
take_split_path = self.obj._is_mixed_type

是正确的。它执行与1.2.4中相同的zip操作。

然而,在1.1.5中,所有的数据类型都是object,因此只有在第一次赋值后为float64take_split_path才为false。随后的赋值使用:

if isinstance(value, (ABCSeries, dict)):
    # TODO(EA): ExtensionBlock.setitem this causes issues with
    # setting for extensionarrays that store dicts. Need to decide
    # if it's worth supporting that.
    value = self._align_series(indexer, Series(value))

这自然地使得dict正确对齐。


2
有趣的发现。在pandas版本1.2.4上,所有后续的数据框都具有值a b c,而不仅仅是第二个数据框。
empty df:
Empty DataFrame
Columns: [a, b, c]
Index: []

dictionary to be entered:
{'a': 'test', 'b': 1, 'c': 2.3}

df after entry:
      a  b    c
0  test  1  2.3

df after second entry:
   a  b  c
0  a  b  c

df after third entry:
   a  b  c
0  a  b  c

顺便说一下,它似乎只在分配给新的行时才能正常工作。因此,在所有后续重新分配给现有行的情况下,它都具有观察到的意外行为,在1.2.4中。
df.loc[1] = entry
print(f'df after assigning to a new row:\n{df}\n\n')
# output:
df after assigning to a new row:
      a  b    c
0     a  b    c
1  test  1  2.3

df.loc[1] = entry
print(f'df after reapting:\n{df}\n')
# output:
df after reapting:
   a  b  c
0  a  b  c
1  a  b  c

可能发生在现有行的原因(除了是一个 bug)是,它正在“迭代集合”。对于字典来说,这是键。在文档的 "设置时扩充" 部分中,.loc/[] 操作可以在为该轴设置不存在的键时执行扩充操作。在 Series 的情况下,这实际上是一种追加操作。因此,对于新行,它正在“扩充”输入,但对于现有行,则正在迭代输入(字典的键而不是值)。对于列表,它的工作方式符合预期。
df.loc[2] = list(entry.values())
print(f'df when assigning from a list\n{df}\n')
# output
df when assigning from a list
      a  b    c
0     a  b    c
1     a  b    c
2  test  1  2.3


df.loc[2] = list(entry.values())
print(f'df when assigning from a list 2nd time\n{df}\n')
# output
df when assigning from a list 2nd time
      a  b    c
0     a  b    c
1     a  b    c
2  test  1  2.3

这是根据文档的原因。我认为实际的技术原因只有在查看源代码后才能明确。在我看来,它应该对所有的赋值/重新赋值都起作用,或者根本不允许。我同意这应该被视为一个错误,正如@DeepSpace提到的那样。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接