はじめに
2026-01-21、pandas 3がリリースされました。約3年ぶりのメジャーバージョンアップです。新機能、不具合修正、性能向上など大量の更新があります。デフォルト動作において従来との互換性を損なう破壊的変更も多く導入されています。
本稿では、v2→v3移行のために、v3で導入された破壊的変更に焦点を当て、その内容、およびv2/v3互換コードの書き方を、具体的なコード例とともに紹介します (なお本稿でv2の挙動として紹介する内容は、v2.3系を前提としています)。
※新バージョンの全般的な紹介についてはリリースノートをご参照ください。
string dtypeの導入
デフォルトの文字列カラムのdtypeは、v2以前は汎用的な object でしたが、v3では専用のstr (pd.StringDtype) となります。
>>> ser = pd.Series(["a", "b"])
>>> ser
0 a
1 b
dtype: object
>>>
>>> ser = pd.Series(["a", "b"])
>>> ser
0 a
1 b
dtype: str # objectではない
dtype名の変更にとどまらず、欠損値の扱いにも変更があり、多くのアプリケーションで影響を受けるものと思われます。以降のセクションでstring dtype導入に伴い、修正が必要となる既存コード例を紹介します。
なお、pd.options.future.infer_string = True を設定することで、v2でもstring dtypeを導入できます。v3移行前の動作確認としてぜひ利用してみてください。
dtype検査が動作しなくなる
分かりやすい点として、文字列カラムかどうかの判定に直接dtypeを検査しているコードはv3では動作しなくなります。
>>> ser = pd.Series(["a", "b"])
>>> ser.dtype == "object"
True
>>> ser.dtype == "str"
False
>>> ser.dtype == "string"
False
>>>
>>> ser = pd.Series(["a", "b"])
>>> ser.dtype == "object"
False
>>> ser.dtype == "str"
True
>>> ser.dtype == "string"
True
v2/v3互換コード (dtypeの検査)
直接dtypeを検査するのではなく、is_string_dtype() を使用します。
>>> pd.api.types.is_string_dtype(ser.dtype)
True
select_dtypes(include="object") の維持と警告
上記と矛盾するように思えますが、v2でDataFrameから文字列カラムを取得する df.select_dtypes(include="object") は、v3でも同じ挙動が維持されています。が、Pandas4Warning が発生するようになりました。この点は、互換性が維持されていることが、かえって混乱の元にもなるので補足します。
>>> df = pd.DataFrame({"str": ["a", "b"], "num": [1, 2], "obj": [3, "c"]})
>>> df.select_dtypes(include="object")
str obj
0 a 3
1 b c
>>> df = pd.DataFrame({"str": ["a", "b"], "num": [1, 2], "obj": [3, "c"]})
>>> df.select_dtypes(include="object")
<stdin-9>:1: Pandas4Warning: For backward compatibility, 'str' dtypes are included by select_dtypes when 'object' dtype is specified. This behavior is deprecated and will be removed in a future version. Explicitly pass 'str' to `include` to select them, or to `exclude` to remove them and silence this warning.
See https://pandas.pydata.org/docs/user_guide/migration-3-strings.html#string-migration-select-dtypes for details on how to write code that works with pandas 2 and 3.
str obj
0 a 3
1 b c
v2では df.select_dtypes(include="object") で文字列カラムに加えてobjectカラムも取得されていました。しかし、stringとobjectのdtypeを区別するv3では、objectを指定したのに文字列カラムまで取得できてしまうと、コード記述と挙動が合致せず混乱を招きます。この挙動はv2との互換性のために維持されたものですが、動作として直感的でないので将来のv4では変更予定です。そのため、Pandas4Warning 警告を発生させて注意を促しています。
v3で、警告を解消しつつobjectカラムを取得するには、ユーザーの意図を明示する必要があります。すなわち以下のいずれかとなります:
>>> df.select_dtypes(include=["object", "string"])
str obj
0 a 3
1 b c
>>> df.select_dtypes(include="object", exclude="string")
obj
0 3
1 c
もちろんv3では"string"を指定して文字列カラムのみを取得できます。これはv3だからこその挙動です。
>>> df.select_dtypes(include="string")
str
0 a
1 b
v2/v3互換コード df.select_dtypes()
繰り返しになりますが、従来の挙動を維持したまま警告なく動作するv2/v3互換コードは以下となります。
>>> df.select_dtypes(include=["object", "string"])
str obj
0 a 3
1 b c
v3では "string" の代わりに "str" を select_dtypes() に指定しても同じ結果となりますが、v2で "str" を指定すると TypeError が発生します。
文字列カラムの欠損値NoneがNaNに変わる
文字列カラムのNoneは、v2ではNoneのまま維持されていましたが、v3ではNaN (np.nan) に変換されます。欠損値の判定に None を直接使用しているコードは動作しなくなります。
>>> ser = pd.Series(["a", None])
>>> ser
0 a
1 None
dtype: object
>>> ser[1] is None
True
>>> ser = pd.Series(["a", None])
>>> ser
0 a
1 NaN
dtype: str
>>> ser[1] is None
False
>>> ser[1] is np.nan
True
v2/v3互換コード (欠損値判定)
欠損値の検査は pd.isna() を使用します。
>>> ser = pd.Series(["a", None])
>>> pd.isna(ser[1])
True
astype("str") で欠損値が維持される
v2以前は、データをastype("str")で文字列にキャストすると、欠損値が "nan" という文字列に変換されていました。これ自体はpandasに長い間存在した不具合として認知されていますが、修正は破壊的変更をともなう影響の大きなものだったのでしばらく維持されていました。この不具合はstring dtypeの導入タイミングで修正されました。
>>> ser = pd.Series([0, np.nan])
>>> ser
0 0.0
1 NaN #=> 欠損値
dtype: float64
>>> pd.isna(ser[1])
True
>>> ser.astype("str")[1]
'nan' #=> 文字列の"nan" (欠損値でなくなっている)
>>> pd.isna(ser.astype("str")[1])
False
>>> ser = pd.Series([0, np.nan])
>>> ser
0 0.0
1 NaN #=> 欠損値
dtype: float64
>>> pd.isna(ser[1])
True
>>> ser.astype("str")[1]
nan #=> 欠損値のまま
>>> pd.isna(ser.astype("str")[1])
True
v2/v3互換コード (文字列キャスト)
v2の挙動 (文字列 "nan" へ変換) を維持したい場合は、map(str) を使用します。
>>> ser = pd.Series([0, np.nan])
>>> ser.map(str)
0 0.0
1 nan
dtype: str
>>> ser.map(str)[1]
'nan'
map(str, na_action="ignore") を適用すれば、欠損値を残したまま変換できます。
>>> ser = pd.Series([0, np.nan])
>>> ser.map(str, na_action="ignore")
0 0.0
1 NaN
dtype: str
>>> ser.map(str, na_action="ignore")[1]
nan
>>> pd.isna(ser.map(str, na_action="ignore")[1])
True
文字列以外をsetitemするとTypeErrorとなる
>>> ser = pd.Series(["a", "b"])
>>> ser[0] = 0
>>> ser
0 0
1 b
dtype: object
>>> ser = pd.Series(["a", "b"])
>>> ser[0] = 0
(略)
TypeError: Invalid value '0' for dtype 'str'. Value should be a string or missing value, got 'int' instead.
v2/v3互換コード (文字列以外をsetitem)
>>> ser = pd.Series(["a", "b"])
>>> ser = ser.astype("object")
>>> ser[0] = 0
あるいは、インスタンス生成を自分で制御できるなら生成時点でobjectとすることも可能です。
>>> ser = pd.Series(["a", "b"], dtype="object")
>>> ser[0] = 0
なお、異なる型のデータをsetitemするとエラーになるv3の挙動変更は、string dtype以外でも同様です。
>>> ser = pd.Series([1, 2])
>>> ser
0 1
1 2
dtype: int64
>>> ser[0] = "a"
<stdin-3>:1: FutureWarning: Setting an item of incompatible dtype is deprecated and will raise an error in a future version of pandas. Value 'a' has dtype incompatible with int64, please explicitly cast to a compatible dtype first.
>>> ser
0 a # FutureWarning警告は出るがsetitem可能
1 2
dtype: object
>>> ser = pd.Series([1, 2])
>>> ser
0 1
1 2
dtype: int64
>>> ser[0] = "a"
(略)
TypeError: Invalid value 'a' for dtype 'int64'
文字列Seriesの.valuesがNumPy配列を返さなくなる
v2以前の object dtypeでは、Series.valuesがバックエンドのNumPy配列を返していましたが、v3のstring dtypeでは、ExtensionArray(のサブクラス)となります。
>>> type(pd.Series(["a", "b"]).values)
<class 'numpy.ndarray'>
>>> type(pd.Series(["a", "b"]).values)
<class 'pandas.arrays.StringArray'>
# pyarrowをインストールしている場合
>>> type(pd.Series(["a", "b"]).values)
<class 'pandas.arrays.ArrowStringArray'>
v2/v3互換コード (Series.values対応)
NumPy配列が必要な場合は to_numpy()を使用します。
>>> type(pd.Series(["a", "b"]).to_numpy())
<class 'numpy.ndarray'>
文字列 Seriesの prod() が必ずエラーとなる
v2では1個以下の文字列データを含むSeriesに対して prod() を呼び出してもエラーになりませんが、v3ではstring dtypeのSeriesに対して prod() を実行すると、データ有無にかかわらず TypeErrorとなります。
>>> pd.Series(["a"]).prod()
'a'
>>> pd.Series(["a"]).prod()
(略)
TypeError: Cannot perform reduction 'prod' with string dtype
# データ0件でもエラー
>>> pd.Series([], dtype="str").prod()
(略)
TypeError: Cannot perform reduction 'prod' with string dtype
v2/v3互換コード (string prod())
文字列に対して prod() するケースは少ないでしょうが、v2互換コードが必要な場合は、Seriesのdtypeを明示的にobjectとして扱います。
>>> ser = pd.Series(["a"])
>>> ser = ser.astype("object")
>>> ser.prod()
'a'
Copy-on-Writeのデフォルト適用
v3からデフォルトで Copy-on-Write (CoW) 動作が有効になります。既にv2でも、CoW有効時に動作しなくなるコードに対してはSettingWithCopyWarning 警告が出るなどしているので、もうCoW対応が完了しているアプリケーションも多いでしょう。
以降のセクションで、CoWにより動作しなくなるコード例を紹介します。
なお、v2でも pd.options.mode.copy_on_write = True を設定することでCoWを有効化できます。v3移行前の動作確認としてぜひ利用してみてください。
連鎖代入が動作しなくなる
連鎖代入 (Chained Assignment) とは、ここでは、DataFrameにインデクシングを連鎖的に複数回適用した結果に対して、代入で値を変更しようとする操作を意味します。
インデクシングを連鎖して得られるデータは、元のDataFrameのコピーとして扱われます。コピーの値を変更しても元のDataFrameにはそれが反映されません。直接的に連鎖代入を行った場合、注意を促すため ChainedAssignmentError 警告も出ます。
>>> df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
>>> df["foo"][0] = 100
>>> df
foo bar
0 100 4 #=> 値が変更される
1 2 5
2 3 6
>>> df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
>>> df["foo"][0] = 100
<stdin-2>:1: ChainedAssignmentError: A value is trying to be set on a copy of a DataFrame or Series through chained assignment.
Such chained assignment never works to update the original DataFrame or Series, because the intermediate object on which we are setting values always behaves as a copy (due to Copy-on-Write).
Try using '.loc[row_indexer, col_indexer] = value' instead, to perform the assignment in a single step.
See the documentation for a more detailed explanation: https://pandas.pydata.org/pandas-docs/version/3.0/user_guide/copy_on_write.html#chained-assignment
>>> df
foo bar
0 1 4 #=> 変更が反映されない
1 2 5
2 3 6
# 連鎖代入の亜種として、以下のような間接的な連鎖代入も値変更がDataFrameに反映されない
>>> df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
>>> foo = df["foo"]
>>> foo[0] = 100
>>> df
foo bar
0 1 4 #=> 変更が反映されない
1 2 5
2 3 6
# ただし、上記はSeries fooの値を変更するという意味になるので、fooは変更される点に注意
>>> foo
0 100
1 2
2 3
Name: foo, dtype: int64
また、以下のようにDataFrameからカラムを取得して replace() で値を更新する方法も連鎖代入の亜種であり、v3では動作しません。
>>> df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
>>> df["foo"].replace(1, 100, inplace=True)
>>> df
foo bar
0 100 4
1 2 5
2 3 6
>>> df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
>>> df["foo"].replace(1, 100, inplace=True)
<stdin-2>:1: ChainedAssignmentError: A value is trying to be set on a copy of a DataFrame or Series through chained assignment using an inplace method.
Such inplace method never works to update the original DataFrame or Series, because the intermediate object on which we are setting values always behaves as a copy (due to Copy-on-Write).
For example, when doing 'df[col].method(value, inplace=True)', try using 'df.method({col: value}, inplace=True)' instead, to perform the operation inplace on the original object, or try to avoid an inplace operation using 'df[col] = df[col].method(value)'.
See the documentation for a more detailed explanation: https://pandas.pydata.org/pandas-docs/version/3.0/user_guide/copy_on_write.html
0 100
1 2
2 3
Name: foo, dtype: int64
>>> df
foo bar
0 1 4
1 2 5
2 3 6
v2/v3互換コード (replace()によるDataFrame更新)
上記のv3実行例の警告メッセージに示されている通り、DataFrame.replace({col: value}, inplace=True) を使用します。
>>> df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
>>> df.replace({"foo": {1: 100}}, inplace=True)
>>> df
foo bar
0 100 4
1 2 5
2 3 6
あるいは、シンプルに inplace をやめてコピーを再代入する形も可能です。
>>> df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
>>> df["foo"] = df["foo"].replace(1, 100)
>>> df
foo bar
0 100 4
1 2 5
2 3 6
コンストラクタに渡したNumPy配列はコピーされる
DataFrame (Seriesも同様) のコンストラクタにNumPy配列を直接渡すと、v3以降はNumPy配列をコピーし、そのコピーを内部のデータ実体とするDataFrameが生成されます。v2以前は、NumPy配列のコピーではなく参照を保持しているため、元のNumPy配列を書き換えるとDataFrameにも変更が反映されますが、v3では反映されなくなります。
>>> arr = np.array([[1, 4], [2, 5], [3, 6]])
>>> df = pd.DataFrame(arr, columns=["foo", "bar"])
>>> df
foo bar
0 1 4
1 2 5
2 3 6
>>> arr[0, 0] = 100
>>> df
foo bar
0 100 4
1 2 5
2 3 6
>>> arr = np.array([[1, 4], [2, 5], [3, 6]])
>>> df = pd.DataFrame(arr, columns=["foo", "bar"])
>>> df
foo bar
0 1 4
1 2 5
2 3 6
>>> arr[0, 0] = 100
>>> df
foo bar
0 1 4
1 2 5
2 3 6
v2/v3互換コード (コンストラクタNumPy配列コピー対応)
copy=False を指定します。
>>> arr = np.array([[1, 4], [2, 5], [3, 6]])
>>> df = pd.DataFrame(arr, columns=["foo", "bar"], copy=False)
>>> df
foo bar
0 1 4
1 2 5
2 3 6
>>> arr[0, 0] = 100
>>> df
foo bar
0 100 4
1 2 5
2 3 6
※ただし、この方法はCopy-on-Writeの利点を損なうため、使用にあたっては慎重になるべきです。
バックエンドのNumPy配列が読み取り専用に
CoWが有効でないv2以前は、DataFrameのデータ実体であるNumPy配列を直接変更することで、DataFrameに値変更を反映できます。v3では、NumPy配列は読み取り専用となり、これが禁止されます。値を変更しようとした場合、ValueError が発生します。
>>> df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
>>> arr = df.to_numpy()
>>> arr[0, 0] = 100
>>> df
foo bar
0 100 4 #=> NumPy配列を通じた値変更が可能
1 2 5
2 3 6
>>> df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
>>> arr = df.to_numpy()
>>> arr[0, 0] = 100
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
arr[0, 0] = 100
~~~^^^^^^
ValueError: assignment destination is read-only
v2/v3互換コード (読み取り専用NumPy配列対応)
arr.flags.writeable = True を設定します。
>>> df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
>>> arr = df.to_numpy()
>>> arr.flags.writeable = True
>>> arr[0, 0] = 100
>>> df
foo bar
0 100 4
1 2 5
2 3 6
※ただし、この方法はCopy-on-Writeの利点を損なうため、使用にあたっては慎重になるべきです。
時刻、時間関連の変更
時間単位がナノ秒から入力に応じて推論されるように変更
分かりやすい文字列を例に確認してみます。v2では必ずナノ秒単位だったものが、v3ではデフォルトでマイクロ秒単位となり、入力にナノ秒書式が含まれる場合のみナノ秒単位で推論されます。ナノ秒精度が要求されるアプリケーションは少ないかもしれませんが、時刻を数値に変換して処理しているケースでは、値が従来より1000分の1になるので問題となります。
>>> d = pd.to_datetime(["2026-01-20T00:00:00.123456"])
>>> d.dtype
dtype('<M8[ns]') # ナノ秒
>>> d.astype("int64")[0]
np.int64(1768867200123456000) # ナノ秒単位で数値化される
>>> d = pd.to_datetime(["2026-01-20T00:00:00.123456"])
>>> d.dtype
dtype('<M8[us]') # マイクロ秒
>>> d.astype("int64")[0]
np.int64(1768867200123456) # マイクロ秒単位で数値化される。v2と比べて1/1000となる
>>> d = pd.to_datetime(["2026-01-20T00:00:00.123456789"]) # 元の値がナノ秒精度
>>> d.dtype
dtype('<M8[ns]')
>>> d.astype("int64")[0]
np.int64(1768867200123456789) # ナノ秒単位で数値化される
v2/v3互換コード (時刻単位粒度変更)
ナノ秒が必要な場合 as_unit("ns") でナノ秒単位に変換する。
>>> d = pd.to_datetime(["2026-01-20T00:00:00.123456"])
>>> d = d.as_unit("ns")
>>> d.dtype
dtype('<M8[ns]')
>>> d.astype("int64")[0]
np.int64(1768867200123456000)
offsets.Day がタイムゾーンを考慮するようになった
従来 offsets.Day は、タイムゾーンによらず24時間として扱われていました。しかし、現実世界においてサマータイムを採用するタイムゾーンでは1日=24時間でない日が存在します。サマータイムシフトが1時間の場合、標準時→夏時間の切り替わりが発生する日は1日23時間、逆に夏時間→標準時の切り替わり日は1日25時間となります。結果的に、日付を1日ずらすつもりで + pd.offsets.Day(1) しても、意図通りにずれていないということがありました。v3からはタイムゾーンを考慮してpd.offsets.Day(1)が正しく1日となります。この変更はv2以前の不具合修正という位置付けですが、結果的に互換性を損なう破壊的変更となっています。
# 夏時間 → 標準時
>>> ts = pd.Timestamp("2026-03-29T00:00:00", tz="Europe/Berlin")
>>> ts
Timestamp('2026-03-29 00:00:00+0100', tz='Europe/Berlin')
>>> ts + pd.offsets.Day(1)
Timestamp('2026-03-30 01:00:00+0200', tz='Europe/Berlin') # 日付だけ更新したつもりが時間までずれている
# 夏時間 → 標準時
>>> ts = pd.Timestamp("2026-10-25T00:00:00", tz="Europe/Berlin")
>>> ts
Timestamp('2026-10-25 00:00:00+0200', tz='Europe/Berlin')
>>> ts + pd.offsets.Day(1)
Timestamp('2026-10-25 23:00:00+0100', tz='Europe/Berlin') # 日付を更新したつもりなのに日付が変わっていない
# 標準時 → 夏時間
>>> ts = pd.Timestamp("2026-03-29T00:00:00", tz="Europe/Berlin")
>>> ts
Timestamp('2026-03-29 00:00:00+0100', tz='Europe/Berlin')
>>> ts + pd.offsets.Day(1)
Timestamp('2026-03-30 00:00:00+0200', tz='Europe/Berlin') # 正しく日付のみ更新
# 夏時間 → 標準時
>>> ts = pd.Timestamp("2026-10-25T00:00:00", tz="Europe/Berlin")
>>> ts
Timestamp('2026-10-25 00:00:00+0200', tz='Europe/Berlin')
>>> ts + pd.offsets.Day(1)
Timestamp('2026-10-26 00:00:00+0100', tz='Europe/Berlin') # 正しく日付のみ更新
v2/v3互換コード (offsets.Dayのタイムゾーン考慮対応)
従来動作のようなタイムゾーンに関係なく1日=24時間固定で扱う処理が求められる場合は、offsets.Day ではなく offsets.Hour を使用する。
>>> ts = pd.Timestamp("2026-03-29T00:00:00", tz="Europe/Berlin")
>>> ts
Timestamp('2026-03-29 00:00:00+0100', tz='Europe/Berlin')
>>> ts + pd.offsets.Hour(24)
Timestamp('2026-03-30 01:00:00+0200', tz='Europe/Berlin')
DatetimeIndex を持つオブジェクトの concat() における sort 動作の変更と警告
v2では、DatetimeIndex をインデックスとするオブジェクトを pd.concat() すると、sort=False を指定してもソートされてしまう不具合がありました。v2では、sortの値や、指定自体の有無に関係なくソートされてしまっていたわけです。これがv3では正しくsortの値を反映するようになります。
>>> idx1 = pd.date_range("2025-01-02", periods=3, freq="h")
>>> df1 = pd.DataFrame({"a": [1, 2, 3]}, index=idx1)
>>> df1
a
2025-01-02 00:00:00 1
2025-01-02 01:00:00 2
2025-01-02 02:00:00 3
>>> idx2 = pd.date_range("2025-01-01", periods=3, freq="h")
>>> df2 = pd.DataFrame({"b": [1, 2, 3]}, index=idx2)
>>> df2
b
2025-01-01 00:00:00 1
2025-01-01 01:00:00 2
2025-01-01 02:00:00 3
>>> pd.concat([df1, df2], axis=1, sort=False)
a b
2025-01-01 00:00:00 NaN 1.0 # sort=False を指定したのにDatetimeIndexでソートされてしまっている
2025-01-01 01:00:00 NaN 2.0
2025-01-01 02:00:00 NaN 3.0
2025-01-02 00:00:00 1.0 NaN
2025-01-02 01:00:00 2.0 NaN
2025-01-02 02:00:00 3.0 NaN
>>> idx1 = pd.date_range("2025-01-02", periods=3, freq="h")
>>> df1 = pd.DataFrame({"a": [1, 2, 3]}, index=idx1)
>>> df1
a
2025-01-02 00:00:00 1
2025-01-02 01:00:00 2
2025-01-02 02:00:00 3
>>> idx2 = pd.date_range("2025-01-01", periods=3, freq="h")
>>> df2 = pd.DataFrame({"b": [1, 2, 3]}, index=idx2)
>>> df2
b
2025-01-01 00:00:00 1
2025-01-01 01:00:00 2
2025-01-01 02:00:00 3
>>> pd.concat([df1, df2], axis=1, sort=False)
a b
2025-01-02 00:00:00 1.0 NaN # 指定通り未ソートとなる
2025-01-02 01:00:00 2.0 NaN
2025-01-02 02:00:00 3.0 NaN
2025-01-01 00:00:00 NaN 1.0
2025-01-01 01:00:00 NaN 2.0
2025-01-01 02:00:00 NaN 3.0
v2/v3互換コード (concatソート修正対応)
v2の挙動は明確な不具合ですが、従来通りソートしてほしい場合は、明示的にsort=Trueを指定してください。
>>> pd.concat([df1, df2], axis=1, sort=True)
a b
2025-01-01 00:00:00 NaN 1.0
2025-01-01 01:00:00 NaN 2.0
2025-01-01 02:00:00 NaN 3.0
2025-01-02 00:00:00 1.0 NaN
2025-01-02 01:00:00 2.0 NaN
2025-01-02 02:00:00 3.0 NaN
v3で sort 引数を指定しない場合、従来との互換性のためにソートされます。これに関して2点注意事項があります。
-
デフォルトソート動作によって並びが変わる場合、ユーザーの意図に反する可能性もあるのでv4廃止予定として警告 (Pandas4Warning) が出ます。これはノイズにもなるので、pandasはできるだけ警告を抑えるために、デフォルトソートによって順序に変更が生じるかどうかを内部で調べ、順序が変わらない場合は警告を出さないような振る舞いをします。これには余計な計算コストが掛かるので、できる限りアプリケーション側で
sort引数を明示した方が良いでしょう。v3# sort無指定時にソートされる場合警告が出る >>> pd.concat([df1, df2], axis=1) <stdin-7>:1: Pandas4Warning: Sorting by default when concatenating all DatetimeIndex is deprecated. In the future, pandas will respect the default of `sort=False`. Specify `sort=True` or `sort=False` to silence this message. If you see this warnings when not directly calling concat, report a bug to pandas. a b 2025-01-01 00:00:00 NaN 1.0 2025-01-01 01:00:00 NaN 2.0 2025-01-01 02:00:00 NaN 3.0 2025-01-02 00:00:00 1.0 NaN 2025-01-02 01:00:00 2.0 NaN 2025-01-02 02:00:00 3.0 NaN # 既にソート済みの場合警告は出ない >>> pd.concat([df2, df1], axis=1) b a 2025-01-01 00:00:00 1.0 NaN 2025-01-01 01:00:00 2.0 NaN 2025-01-01 02:00:00 3.0 NaN 2025-01-02 00:00:00 NaN 1.0 2025-01-02 01:00:00 NaN 2.0 2025-01-02 02:00:00 NaN 3.0 -
pandasは、内部実装でも
concat()を利用しており、ユーザーの関知していないところで意図せず上記の警告が出る可能性があります。ユーザーが直接concat()していないのに警告が出る場合、それは意図しないpandasの不具合の可能性があるので、Issue報告してほしいとリリースノートで呼びかけられています。If this does occur, we ask users to open an issue so that we may address any potential behavior changes.
その他
DataFrame.value_counts(), DataFrameGroupBy.value_counts() の sort=False 動作変更
DataFrame.value_counts() にsort=Falseを指定した場合、DataFrameのカラム値の大小に基づいてソートされていました (sort=Trueの場合は度数(件数)に応じてソート。v3も同様)。sort=Falseなのにソートされる点が分かりづらいため、v3では元のデータ順序が維持されるように変更されました。DataFrameGroupBy.value_countsにも適用されています。
>>> df = pd.DataFrame(
... {
... "a": [2, 2, 2, 2, 1, 1, 1, 1],
... "b": [2, 1, 3, 1, 2, 3, 1, 1],
... }
... )
>>> df
a b
0 2 2
1 2 1
2 2 3
3 2 1
4 1 2
5 1 3
6 1 1
7 1 1
>>> df.value_counts(sort=False) # sort=Falseなのにカラム値の大小でソートされる
a b
1 1 2
2 1
3 1
2 1 2
2 1
3 1
>>> df = pd.DataFrame(
... {
... "a": [2, 2, 2, 2, 1, 1, 1, 1],
... "b": [2, 1, 3, 1, 2, 3, 1, 1],
... }
... )
>>> df
a b
0 2 2
1 2 1
2 2 3
3 2 1
4 1 2
5 1 3
6 1 1
7 1 1
>>> df.value_counts(sort=False)
a b
2 2 1
1 2
3 1
1 2 1
3 1
1 2
v2/v3互換コード (value_counts()のsort=False動作変更対応)
v2と同じ挙動を維持するには、アプリケーション側でソートすることになります。データ件数によっては性能に影響しうる点に注意してください。
>>> df.value_counts(sort=False).sort_index()
a b
1 1 2
2 1
3 1
2 1 2
2 1
3 1
本稿で扱わなかった変更点
他にも、依存するPythonおよびパッケージの最小バージョンの変更、個別のAPIの変更、旧バージョンで廃止予定とされた機能の廃止等、多くの変更があります。その他、本稿で扱っていない更新内容については、リリースノートをご参照いただければと思います。