PDEP-6: setitem-like操作でのアップキャストの禁止

概要

setitem-like操作はSeriesのdtype(およびDataFrameの列のdtype)を変更しないという提案です。

現在の動作

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は異なるdtypeの扱いにおいて非常に柔軟です。しかし、これはバグを隠したり、ユーザーの期待を裏切ったり、in-place操作であるかのように見える部分でデータをコピーしたりする可能性があります。

バグを隠す例としては

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-like操作に限定されます。例えば、以下から開始すると

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. 「アップキャストの禁止」を、アップキャストされたdtypeがobjectの場合に限定する(つまり、int64 Seriesをfloat64にアップキャストする現在の動作を維持する)。

他のライブラリが何をしているかを比較してみましょう。

オプション2は、pandasの破壊的な動作変更となります。さらに、このPDEPの目的がバグの防止である場合、これも望ましくありません。誰かが1.5を設定し、後で実際に1を設定したことを知って驚くかもしれません。

オプション3にはいくつかの欠点があります。

オプション1は、バグからユーザーを保護するという点で最大限に安全であり、null許容dtypeの現在の動作と一貫性があり、教えやすいです。したがって、このPDEPで選択されるオプションはオプション1です。

使用法と影響

これによりpandasはより厳格になるため、バグを導入するリスクはないはずです。むしろ、バグの防止に役立つでしょう。

残念ながら、意図的にアップキャストしていたユーザーを煩わせるリスクもあります。

ユーザーがSeriesを明示的にfloatにキャストすることで現在の動作を維持できることを考えると、厳格な側に偏る方がコミュニティ全体にとって有益でしょう。

範囲外

拡大。例えば

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

そもそもそれを許可すべきかどうかについては、より大きな議論が必要であると言えます。この提案を焦点を絞るため、意図的に範囲から除外されています。

F.A.Q.

Q: int8 Seriesに1.0を設定した場合どうなりますか?

A: 現在の動作では、1.01として挿入され、dtypeはint8のままです。したがって、これは変更されません。

Q: int8 Seriesに1_000_000.0を設定した場合どうなりますか?

A: 現在の動作では、int32にアップキャストされます。したがって、このPDEPでは、代わりにエラーを発生させます。

Q: int8 Seriesに16.000000000000001を設定した場合どうなりますか?

A: Pythonに関する限り、16.00000000000000116.0は同じ数値です。したがって、16として挿入され、dtypeは変更されません(現在の動作と同じで、ここでは変更はありません)。

Q: int8 Seriesに1.00000000011.0として挿入したい場合はどうすればよいですか?

A: 以下のような独自のヘルパー関数を定義することができます。

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履歴