PDEP-6: 禁止在 setitem 类操作中进行向上转换

摘要

建议 setitem 类操作不会改变 Series 的数据类型(也不改变 DataFrame 列的数据类型)。

当前行为

In [1]: ser = pd.Series([1, 2, 3], dtype='int64')

In [2]: ser[2] = 'potage'

In [3]: ser  # dtype changed to 'object'!
Out[3]:
0         1
1         2
2    potage
dtype: object

建议的行为

In [1]: ser = pd.Series([1, 2, 3])

In [2]: ser[2] = 'potage'  # raises!
---------------------------------------------------------------------------
ValueError: Invalid value 'potage' for dtype int64

动机和范围

目前,pandas 在处理不同数据类型方面非常灵活。但是,这可能会隐藏错误,违反用户预期,并在看起来应该是就地操作的情况下复制数据。

隐藏错误的一个例子是

In[9]: ser = pd.Series(pd.date_range("2000", periods=3))

In[10]: ser[2] = "2000-01-04"  # works, is converted to datetime64

In[11]: ser[2] = "2000-01-04x"  # typo - but pandas does not error, it upcasts to object

此 PDEP 的范围仅限于对 Series(和 DataFrame 列)的 setitem 类操作。例如,从

df = DataFrame({"a": [1, 2, np.nan], "b": [4, 5, 6]})
ser = df["a"].copy()

开始,以下所有操作都将引发异常

可能需要将上面的列表扩展到 Series.replaceSeries.update,但为了缩小 PDEP 的范围,目前将它们排除在外。

不会引发异常的操作示例

详细描述

具体来说,建议是

首先,这将涉及

  1. 更改Block.setitem,使其不再具有except

    value = extract_array(value, extract_numpy=True)
    try:
        casted = np_can_hold_element(values.dtype, value)
    except LossSetitiemError:
        # current dtype cannot store value, coerce to common dtype
        nb = self.coerce_to_target_dtype(value)
        return nb.setitem(index, value)
    else:
    
  2. 在以下位置进行类似的更改:

    • Block.where
    • Block.putmask
    • EABackedBlock.setitem
    • EABackedBlock.where
    • EABackedBlock.putmask

以上更改将需要调整数百个测试。请注意,一旦实施开始,要更改的位置列表可能会略有不同。

完全禁止向上转换,还是只禁止向上转换为object

此提案中最棘手的部分是,当在整数列中设置浮点数时该怎么办

In[1]: ser = pd.Series([1, 2, 3])

In [2]: ser
Out[2]:
0    1
1    2
2    3
dtype: int64

In[3]: ser[0] = 1.5  # what should this do?

当前行为是向上转换为“float64”

In [4]: ser
Out[4]:
0    1.5
1    2.0
2    3.0
dtype: float64

这并不一定意味着存在错误,因为用户可能只是将他们的Series视为数值(不太关心intfloat) - 'int64'只是 pandas 在构建它时碰巧推断出的类型。

可能的选项包括:

  1. 只接受圆整的浮点数(例如1.0),对其他任何浮点数(例如1.01)引发异常。
  2. 在设置之前将浮点值转换为int(即,静默地将所有浮点值四舍五入)。
  3. 将“禁止向上转换”限制在向上转换后的数据类型为object时(即,保留当前将 int64 Series 向上转换为 float64 的行为)。

让我们比较一下其他库的做法

选项2将是 pandas 中的一个破坏性行为变更。此外,如果此 PDEP 的目标是防止错误,那么这也不理想:有人可能会设置1.5,然后惊讶地发现他们实际上设置了1

选项3有几个缺点。

选项1在保护用户免受错误、与可空数据类型的当前行为保持一致以及易于教授方面是最安全的。因此,此 PDEP 选择的选项是选项 1。

使用和影响

这将使 pandas 更严格,因此不应该存在引入错误的风险。如果有的话,这将有助于防止错误。

不幸的是,它也可能惹恼那些可能有意进行提升的用户。

鉴于用户仍然可以通过首先将 Series 显式转换为浮点数来获得当前行为,因此对整个社区来说,在严格性方面犯错更有益。

超出范围

扩大。例如

ser = pd.Series([1, 2, 3])
ser[len(ser)] = 4.5

关于是否应该允许这样做,可能存在更大的讨论。为了使本提案保持重点,它有意地被排除在范围之外。

常见问题解答

问:在int8 Series 中设置1.0会发生什么?

:当前行为是将1.0插入为1,并将数据类型保持为int8。因此,这不会改变。

问:在int8 Series 中设置1_000_000.0会发生什么?

:当前行为是提升为int32。因此,在此 PDEP 下,它将改为引发异常。

问:在int8 Series 中设置16.000000000000001会发生什么?

:就 Python 而言,16.00000000000000116.0 是同一个数字。因此,它将被插入为16,数据类型不会改变(就像现在发生的那样,这里不会有任何改变)。

问:如果我想将1.0000000001 插入int8 Series 中的1.0,该怎么办?

:您可能希望定义自己的辅助函数,例如

def maybe_convert_to_int(x: int | float, tolerance: float):
    if np.abs(x - round(x)) < tolerance:
        return round(x)
    return x

您可以根据自己的需求进行调整。

时间线

在 2.x 版本(2.0.0 发布后)中弃用,并在 3.0.0 中强制执行。

PDEP 历史