Pandas 3.0 完全ガイド:Copy-on-Write・新しいstr型・pd.col()など全新機能を徹底解説

2026年1月リリースのpandas 3.0を徹底解説。Copy-on-Writeのデフォルト化、新しいstr dtype、pd.col()カラム式、日時解像度変更、Anti-Joinサポートなど、破壊的変更と移行手順を実践コード例とともに紹介します。

はじめに

2026年1月21日、ついにpandas 3.0.0が正式にリリースされました。正直なところ、このアップデートを待ち望んでいたデータサイエンティストは多いんじゃないでしょうか。

今回のリリースは、ちょっとしたバグ修正やマイナーな改善とは訳が違います。pandasの歴史の中でも最大級の転換点で、長年の「なんでこうなるの?」を一気に解決してくれる破壊的変更と新機能が盛りだくさんです。

主要な変更点をざっとまとめると、こんな感じです。

  • Copy-on-Write(CoW)がデフォルト動作に — あの悪名高いSettingWithCopyWarningとはもうお別れ
  • 新しい文字列型(str dtype) — PyArrowバックエンドで文字列処理が爆速に
  • カラム式(pd.col()) — ラムダ関数なしでカラム参照できる、Polars風の新構文
  • 日時解像度の推論変更 — ナノ秒デフォルトからマイクロ秒への移行
  • タイムゾーン処理の刷新 — pytzからPython標準のzoneinfoへ
  • Anti-Joinのネイティブサポートmerge()left_anti/right_antiが使える
  • Arrow PyCapsuleインターフェース — ゼロコピーでのデータ交換

データサイエンティストやデータエンジニアにとって、これは単なるバージョン番号の更新じゃありません。パフォーマンスの大幅な向上、予測可能な動作、そして表現力の高いAPIが手に入るんです。ただし、破壊的変更もかなり含まれているので、移行は慎重に進める必要があります。公式チームは「まずpandas 2.3にアップグレードして、非推奨警告を全部潰してから3.0に行ってね」と推奨しています。

では、pandas 3.0の新機能と変更点を一つずつ見ていきましょう。

Copy-on-Write(CoW)がデフォルトに

Copy-on-Writeとは何か

Copy-on-Write(CoW)は、pandasでのデータのコピーとビューの動作を根本から変える仕組みです。pandas 3.0では、CoWがデフォルトかつ唯一の動作モードになりました。

これまでのpandasって、インデクシング操作がビュー(元データへの参照)を返すのかコピー(独立した複製)を返すのかが、操作の種類やデータの内部構造によってバラバラだったんですよね。この挙動は本当に予測しづらくて、悪名高いSettingWithCopyWarningの原因でもありました。個人的にも、このWarningには何度悩まされたかわかりません。

以前の問題点

pandas 2.x以前では、こんなコードが予測不可能な動作を引き起こしていました。

# pandas 2.x以前の問題のあるコード
import pandas as pd
import numpy as np

df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})

# このサブセットはビューなのかコピーなのか?
subset = df[df['a'] > 1]
subset.iloc[0] = 999  # dfが変更されるかもしれないし、されないかもしれない

# チェーン代入 - 動作が不定
df['a'][0] = 100  # SettingWithCopyWarning が発生

この問題を回避するために、多くの開発者が防御的に.copy()を呼びまくっていました。コードは読みにくくなるし、不必要なメモリコピーでパフォーマンスも落ちるし、正直いいことなしでした。

pandas 3.0での新しい動作

pandas 3.0では、すべてのインデクシング操作やメソッドが「コピーのように振る舞うオブジェクト」を返します。内部的にはビューとして実装されますが、実際に変更を加えた時点で初めてコピーが作成される仕組みです。つまり、一つの操作で複数のオブジェクトが勝手に変更されるような事態はもう起きません。

# pandas 3.0での動作
import pandas as pd

df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})

# サブセットは常にコピーのように振る舞う
subset = df[df['a'] > 1]
subset.iloc[0] = 999  # dfは絶対に変更されない

# チェーン代入はもう動作しない(サイレントに無視される)
# df['a'][0] = 100  # 効果なし - 元のdfは変更されない

# 正しい方法: .locを使用する
df.loc[0, 'a'] = 100  # これが正しい代入方法
print(df)
#      a  b
# 0  100  4
# 1    2  5
# 2    3  6

SettingWithCopyWarningの廃止

SettingWithCopyWarningはpandas 3.0で完全に削除されました。CoWの導入によって、この警告が対処しようとしていた問題自体がもう存在しないからです。嬉しいですね。

mode.copy_on_writeオプションも非推奨となり、設定しても効果はありません。

パフォーマンスの改善

CoWの最大の恩恵の一つが、不要な防御的コピーを排除できることによるパフォーマンスの向上です。実際のプロダクション環境では、大量のデータを扱うETLパイプラインにおいて最大38%のパフォーマンス改善が報告されています。

import pandas as pd
import time

# 大規模なDataFrameを作成
df = pd.DataFrame({
    'id': range(1_000_000),
    'value': range(1_000_000),
    'category': ['A', 'B', 'C', 'D'] * 250_000
})

start = time.time()

# ETLパイプラインの模擬
# pandas 3.0では.copy()が不要 - CoWが自動的に最適化
filtered = df[df['value'] > 500_000]
grouped = filtered.groupby('category')['value'].mean()
result = grouped.reset_index()

elapsed = time.time() - start
print(f"処理時間: {elapsed:.3f}秒")

# pandas 2.xでは防御的な.copy()を多用していたため
# 同様の処理が約38%遅くなっていた
# pandas 3.0のCoWにより、コピーは遅延評価され
# 実際に必要な場合にのみ実行される

特に、複数のフィルタリングステップや変換を含むパイプラインでは、この改善効果がはっきりと現れます。

新しい文字列型(str dtype)

object型からstr型への移行

pandas 3.0では、文字列データのデフォルトdtypeがobjectからstrに変更されました。「え、それだけ?」と思うかもしれませんが、これは見た目以上に大きな変更です。

新しいstr型は、PyArrowがインストールされていればPyArrowの文字列配列をバックエンドとして使い、なければNumPyのobject配列にフォールバックします。

import pandas as pd

# pandas 3.0の動作
ser = pd.Series(["東京", "大阪", "名古屋"])
print(ser.dtype)  # str(以前はobject)

# dtypeの確認
print(type(ser.dtype))  # pandas.StringDtype

パフォーマンスの大幅な向上

PyArrowバックエンドの文字列型は、従来のobject型と比べて劇的なパフォーマンス向上を実現します。PyArrowはデータを連続したメモリブロックで処理するので、個々のPythonオブジェクトを扱う従来の方法よりもはるかに効率的です。

import pandas as pd
import numpy as np
import time

# 大量の文字列データを作成
n = 1_000_000
data = [f"item_{i}" for i in range(n)]

# 新しいstr dtype(PyArrowバックエンド)
ser_str = pd.Series(data)  # 自動的にstr dtypeに
print(f"dtype: {ser_str.dtype}")  # str
print(f"メモリ使用量: {ser_str.memory_usage(deep=True) / 1024 / 1024:.1f} MB")

# 従来のobject dtype(比較用)
ser_obj = pd.Series(data, dtype="object")
print(f"dtype: {ser_obj.dtype}")  # object
print(f"メモリ使用量: {ser_obj.memory_usage(deep=True) / 1024 / 1024:.1f} MB")

# 文字列操作のベンチマーク
start = time.time()
result_str = ser_str.str.upper()
time_str = time.time() - start

start = time.time()
result_obj = ser_obj.str.upper()
time_obj = time.time() - start

print(f"\nstr.upper() 処理時間:")
print(f"  str dtype:    {time_str:.3f}秒")
print(f"  object dtype: {time_obj:.3f}秒")
print(f"  高速化率:     {time_obj / time_str:.1f}x")

# 典型的な結果:
# str dtype は 5-10倍高速
# メモリ使用量は最大50%削減

100万行の文字列データで5〜10倍の高速化って、かなりインパクトがありますよね。メモリ使用量も最大50%削減されるので、大規模なテキストデータを扱うプロジェクトでは体感できるレベルの改善です。

文字列型の制約と注意点

新しいstr型には一つ大事な制約があります。文字列または欠損値(NaN)しか保持できません。文字列以外の値を入れようとするとTypeErrorが発生します。

import pandas as pd

ser = pd.Series(["a", "b", "c"], dtype="str")

# 文字列以外の値を設定するとTypeError
try:
    ser[0] = 123
except TypeError as e:
    print(f"エラー: {e}")
    # TypeError: Invalid value '123' for dtype 'str'

# 欠損値はNaNとして扱われる
ser_with_na = pd.Series(["a", "b", None], dtype="str")
print(ser_with_na)
# 0      a
# 1      b
# 2    NaN
# dtype: str

# 欠損値の確認にはpd.isna()を使用
print(pd.isna(ser_with_na[2]))  # True

移行時の注意点

ここ、結構ハマりポイントです。既存のコードでdtype == "object"をチェックしている箇所があると、pandas 3.0ではFalseを返すようになります。

import pandas as pd

ser = pd.Series(["a", "b", "c"])

# pandas 2.xと3.xの両方で動作するコード
# 推奨: pd.api.types.is_string_dtype() を使用
print(pd.api.types.is_string_dtype(ser.dtype))  # True(両バージョンで動作)

# dtype文字列の直接比較は避ける
# ser.dtype == "object"  # pandas 3.0ではFalse
# ser.dtype == "str"     # pandas 3.0ではTrue

# DataFrameから文字列カラムを選択
df = pd.DataFrame({
    'name': ['Alice', 'Bob'],
    'age': [30, 25],
    'city': ['Tokyo', 'Osaka']
})
# 両バージョン互換の方法
string_cols = df.select_dtypes(include=["object", "string"])
print(string_cols.columns.tolist())  # ['name', 'city']

astype(str)の動作も変わっています。pandas 3.0では、欠損値が文字列の"nan"に変換されず、NaNとして保持されるようになりました。地味だけど、実はこの変更のおかげでデータクレンジングが楽になるケースが多いです。

import pandas as pd
import numpy as np

ser = pd.Series([1.5, np.nan])

# pandas 3.0での動作
result = ser.astype("str")
print(result)
# 0    1.5
# 1    NaN    ← 欠損値が保持される(以前は文字列"nan"になっていた)
# dtype: str

# 旧動作が必要な場合はmap(str)を使用
result_old = ser.map(str)
print(result_old)
# 0    1.5
# 1    nan    ← 文字列"nan"に変換される
# dtype: str

カラム式(pd.col())

より直感的なカラム参照

pandas 3.0で個人的に一番ワクワクした新機能がこれです。pd.col()は、PolarsやPySparkにインスパイアされた新しい式構文で、ラムダ関数を使わずにカラムを参照できます。

import pandas as pd

df = pd.DataFrame({
    'a': [1, 1, 2],
    'b': [4, 5, 6],
    'name': ['Alice', 'Bob', 'Charlie']
})

# === 従来の方法(ラムダ関数)===
result_old = df.assign(c=lambda df: df['a'] + df['b'])

# === 新しい方法(pd.col())===
result_new = df.assign(c=pd.col('a') + pd.col('b'))

print(result_new)
#    a  b     name  c
# 0  1  4    Alice  5
# 1  1  5      Bob  6
# 2  2  6  Charlie  8

ラムダ関数より断然読みやすいですよね。

演算子とメソッドのサポート

pd.col()は、標準的な演算子(+-*/など)はもちろん、Seriesのメソッドや.str.dtといった名前空間アクセサもサポートしています。

import pandas as pd

df = pd.DataFrame({
    'name': ['alice', 'bob', 'charlie'],
    'price': [100, 200, 300],
    'quantity': [2, 3, 1],
    'date': pd.to_datetime(['2026-01-01', '2026-01-02', '2026-01-03'])
})

# 文字列メソッド・算術演算・日時アクセサを組み合わせて使う
result = df.assign(
    upper_name=pd.col('name').str.upper(),
    total=pd.col('price') * pd.col('quantity'),
    year=pd.col('date').dt.year
)

print(result)
#       name  price  quantity       date upper_name  total  year
# 0    alice    100         2 2026-01-01      ALICE    200  2026
# 1      bob    200         3 2026-01-02        BOB    600  2026
# 2  charlie    300         1 2026-01-03    CHARLIE    300  2026

メソッドチェーンでの活用

pd.col()の真価が発揮されるのは、メソッドチェーンの中で使う場面です。ラムダ関数と比較すると、コードの意図がぐっと明確になります。

import pandas as pd

df = pd.DataFrame({
    'product': ['laptop', 'phone', 'tablet', 'laptop', 'phone'],
    'revenue': [1200, 800, 500, 1100, 900],
    'cost': [800, 400, 300, 750, 420],
    'region': ['East', 'West', 'East', 'West', 'East']
})

# ラムダ関数を使った従来のメソッドチェーン
result_old = (
    df
    .assign(profit=lambda df: df['revenue'] - df['cost'])
    .assign(margin=lambda df: df['profit'] / df['revenue'] * 100)
)

# pd.col()を使った新しいメソッドチェーン - すっきり!
result_new = (
    df
    .assign(profit=pd.col('revenue') - pd.col('cost'))
    .assign(margin=(pd.col('revenue') - pd.col('cost')) / pd.col('revenue') * 100)
)

print(result_new)
#   product  revenue  cost region  profit     margin
# 0  laptop     1200   800   East     400  33.333333
# 1   phone      800   400   West     400  50.000000
# 2  tablet      500   300   East     200  40.000000
# 3  laptop     1100   750   West     350  31.818182
# 4   phone      900   420   East     480  53.333333

もう一つの嬉しい点として、pd.col()はラムダ関数のクロージャ問題を回避できます。ラムダは変数を参照で捕捉するので、ループ内で使うと予期しないバグが起きることがありましたが、pd.col()ではそういった問題は発生しません。

Arrow PyCapsule インターフェース

ゼロコピーデータ交換

pandas 3.0では、Arrow PyCapsule Interface(Arrow C Data Interface)を通じたゼロコピーデータ交換がサポートされました。DuckDBやPolars、PyArrowなど、Arrow互換ライブラリとの間でデータを効率的にやり取りできるようになります。

新しいメソッド

以下のメソッドが新たに追加されています。

  • DataFrame.from_arrow() — Arrow互換オブジェクトからDataFrameをインポート
  • Series.from_arrow() — Arrow互換オブジェクトからSeriesをインポート
  • DataFrame.__arrow_c_stream__() — Arrow C Streamとしてエクスポート
  • Series.__arrow_c_stream__() — Arrow C Streamとしてエクスポート
import pandas as pd
import pyarrow as pa

# PyArrow テーブルからpandas DataFrameへの変換
arrow_table = pa.table({
    'id': [1, 2, 3],
    'name': ['Alice', 'Bob', 'Charlie'],
    'score': [95.5, 87.3, 92.1]
})

# Arrow PyCapsule Interfaceを使用したインポート
df = pd.DataFrame.from_arrow(arrow_table)
print(df)
#    id     name  score
# 0   1    Alice   95.5
# 1   2      Bob   87.3
# 2   3  Charlie   92.1

# エクスポート(Arrow互換ライブラリで消費可能)
stream = df.__arrow_c_stream__()

# DuckDBなどの他のライブラリとの相互運用も可能
# import duckdb
# result = duckdb.sql("SELECT * FROM df WHERE score > 90")

大規模なデータセットを複数のライブラリ間でやり取りする場合、シリアライゼーション/デシリアライゼーションが不要なのは大きなメリットです。データパイプラインの中でpandasとDuckDBを行き来するような構成では、体感できるレベルの速度改善が期待できます。

日時解像度の推論変更

ナノ秒デフォルトからの脱却

これまでpandasでは、すべてのdatetimeデータがナノ秒精度(datetime64[ns])で格納されていました。その結果、1678年以前や2262年以降の日付を表現できないという、ちょっと困った制限がありました。歴史データを扱う人には特に痛い問題だったんじゃないでしょうか。

pandas 3.0では、入力データに応じた適切な解像度が推論されるようになりました。

import pandas as pd

# 文字列からの変換 - デフォルトでマイクロ秒
ts = pd.to_datetime(["2026-01-21 10:30:00"])
print(ts.dtype)  # datetime64[us]  ← 以前はdatetime64[ns]

# ナノ秒精度の文字列はナノ秒で推論
ts_ns = pd.to_datetime(["2026-01-21 10:30:00.123456789"])
print(ts_ns.dtype)  # datetime64[ns]

# Python datetimeオブジェクトからの変換 - マイクロ秒
from datetime import datetime
ts_py = pd.to_datetime([datetime(2026, 1, 21, 10, 30)])
print(ts_py.dtype)  # datetime64[us]

# 整数からの変換 - 指定された単位の解像度
ts_sec = pd.to_datetime([0, 1, 2], unit="s")
print(ts_sec.dtype)  # datetime64[s]

# np.datetime64の単位が保持される
import numpy as np
ts_np = pd.to_datetime([np.datetime64("2026-01-21", "D")])
print(ts_np.dtype)  # datetime64[s](日単位は秒に変換)

要注意:整数変換時の値の変化

ここは本当に気をつけてほしいポイントです。datetimeデータを整数に変換する際の値が変わります。以前はナノ秒単位の値が返されていましたが、今は格納されている解像度単位の値が返されるので、結果が1000倍小さくなることがあります。

import pandas as pd

ts = pd.Timestamp("2026-01-21")

# 安全な変換方法: as_unit()で明示的に単位を指定
ts_ns = ts.as_unit("ns")
print(f"ナノ秒値: {ts_ns.value}")

ts_us = ts.as_unit("us")
print(f"マイクロ秒値: {ts_us.value}")

# 推奨: 整数変換前に必ずas_unit()を使用
ser = pd.Series(pd.to_datetime(["2026-01-21", "2026-01-22"]))
# ser.astype("int64")  # 結果は解像度に依存するため危険

# 安全な方法
ser_ns = ser.dt.as_unit("ns").astype("int64")
print(ser_ns)

タイムゾーン処理の変更

pytzからzoneinfoへの移行

pandas 3.0では、タイムゾーンの実装がサードパーティのpytzから、Python標準ライブラリのzoneinfoに切り替わりました。外部依存が一つ減るのは、デプロイメントの観点からも嬉しい変更です。

import pandas as pd

# pandas 3.0でのタイムゾーン処理
ts = pd.Timestamp(2026, 1, 21).tz_localize("Asia/Tokyo")
print(ts)
# 2026-01-21 00:00:00+09:00

print(type(ts.tzinfo))
# <class 'zoneinfo.ZoneInfo'>  ← 以前はpytz.timezone

print(ts.tzinfo)
# zoneinfo.ZoneInfo(key='Asia/Tokyo')

pytzの扱い

pytzは必須依存関係ではなくなりましたが、引き続きサポートされています。pytzのタイムゾーンオブジェクトを直接渡せば、そのまま使えます。

# pytzが必要な場合のインストール
# pip install pandas[timezone]

# pytzオブジェクトを直接渡すことも可能
import pytz
ts = pd.Timestamp(2026, 1, 21, tz=pytz.timezone("Asia/Tokyo"))
print(type(ts.tzinfo))  # pytz.tzinfo

# ただし、文字列で指定した場合はzoneinfoが使用される
ts2 = pd.Timestamp(2026, 1, 21, tz="Asia/Tokyo")
print(type(ts2.tzinfo))  # zoneinfo.ZoneInfo

夏時間(DST)遷移時の動作変更

pd.offsets.Dayの動作も変更されました。以前は常に24時間として計算されていましたが、pandas 3.0ではカレンダー日として計算されます。夏時間の遷移を跨いでも時刻が保持されるようになったわけです。

import pandas as pd

# 夏時間遷移日(米国東部時間 2026年3月8日)
ts = pd.Timestamp("2026-03-08 08:00", tz="US/Eastern")

# pandas 3.0: カレンダー日として計算
result = ts + pd.offsets.Day(1)
print(result)
# 2026-03-09 08:00:00-04:00  ← 時刻が保持される

# 以前の動作(24時間加算)では:
# 2026-03-09 09:00:00-04:00  ← 夏時間の影響で1時間ずれていた

# 明示的に24時間を加算したい場合はTimedeltaを使用
result_24h = ts + pd.Timedelta(hours=24)
print(result_24h)
# 2026-03-09 09:00:00-04:00

Anti-Join のサポート

Anti-Joinとは

Anti-Joinは、一方のテーブルにあるけど他方にはないレコードを抽出する結合操作です。SQLでいうNOT EXISTSNOT INのパターンで、データクレンジングや差分抽出では定番の手法ですよね。

pandas 3.0では、merge()left_antiright_antiの結合タイプが追加されました。

import pandas as pd

# 全顧客リスト
customers = pd.DataFrame({
    'customer_id': [1, 2, 3, 4, 5],
    'name': ['田中', '鈴木', '佐藤', '高橋', '伊藤']
})

# 注文した顧客
orders = pd.DataFrame({
    'customer_id': [1, 3, 5],
    'product': ['ノートPC', 'スマートフォン', 'タブレット']
})

# Left Anti-Join: 注文していない顧客を抽出
inactive = pd.merge(customers, orders, on='customer_id', how='left_anti')
print("注文していない顧客:")
print(inactive)
#    customer_id name
# 0            2   鈴木
# 1            4   高橋

# Right Anti-Join: 顧客マスタに存在しない注文を検出
unknown_orders = pd.DataFrame({
    'customer_id': [1, 6, 7],
    'product': ['モニター', 'キーボード', 'マウス']
})
orphaned = pd.merge(customers, unknown_orders, on='customer_id', how='right_anti')
print("\n顧客マスタに存在しない注文:")
print(orphaned)
#    customer_id  product
# 0            6  キーボード
# 1            7    マウス

従来の方法との比較

pandas 2.x以前では、Anti-Joinを実現するために外部結合とインジケータフラグを組み合わせるという(正直かなり面倒な)方法が必要でした。

import pandas as pd

customers = pd.DataFrame({
    'customer_id': [1, 2, 3, 4, 5],
    'name': ['田中', '鈴木', '佐藤', '高橋', '伊藤']
})

orders = pd.DataFrame({
    'customer_id': [1, 3, 5],
    'product': ['ノートPC', 'スマートフォン', 'タブレット']
})

# === pandas 2.x以前の方法(冗長)===
merged = pd.merge(customers, orders, on='customer_id', how='left', indicator=True)
anti_joined = merged[merged['_merge'] == 'left_only'].drop('_merge', axis=1)

# === pandas 3.0の方法(シンプル)===
anti_joined = pd.merge(customers, orders, on='customer_id', how='left_anti')

# 結果は同じだけど、3.0の方が圧倒的に読みやすい

NaN と NA の統一

nullable dtypesでの一貫した処理

pandas 3.0では、nullable dtypes(Int64Float64booleanなど)において、NaNが一貫してNAと同等に扱われるようになりました。

import pandas as pd
import numpy as np

# pandas 3.0での一貫した動作
ser = pd.Series([0, np.nan], dtype=pd.Float64Dtype())
result = ser / 0
print(result)
# 0    <NA>
# 1    <NA>
# dtype: Float64

print(result.isna())
# 0    True    ← NaN も欠損値として正しく識別される
# 1    True

以前はNaNが欠損値として認識されなかったケースがあり、地味にバグの温床になっていたので、この統一は地味ながら非常にありがたい改善です。

NaNNAを明示的に区別する必要がある場合(あまりないとは思いますが)、実験的なオプションpd.options.future.distinguish_nan_and_naを使えます。

その他の重要な変更

concat()のsort=Falseの尊重

pd.concat()DatetimeIndexに対してsort=Falseを正しく尊重するようになりました。

import pandas as pd

idx1 = pd.date_range("2026-01-02", periods=3, freq="h")
df1 = pd.DataFrame({"a": [1, 2, 3]}, index=idx1)

idx2 = pd.date_range("2026-01-01", periods=3, freq="h")
df2 = pd.DataFrame({"b": [4, 5, 6]}, index=idx2)

# sort=Falseが正しく動作する
result = pd.concat([df1, df2], axis=1, sort=False)
print(result)
# df1のインデックスが先、df2のインデックスが後に表示される

value_counts()のsort=Falseの動作改善

DataFrame.value_counts()sort=Falseを指定すると、入力の出現順序がそのまま維持されるようになりました。Seriesのvalue_counts()と一貫した動作になったわけです。

import pandas as pd

df = pd.DataFrame({"a": [2, 2, 1], "b": [2, 1, 2]})
print(df.value_counts(sort=False))
# a  b
# 2  2    1
#    1    1
# 1  2    1
# dtype: int64
# ← 入力データの出現順序が維持される

インプレース操作の戻り値変更

inplace=Trueを指定した操作が、Noneではなく変更されたオブジェクト自身を返すようになりました。これ、地味ですがメソッドチェーンの柔軟性が増す変更です。

import pandas as pd
import numpy as np

df = pd.DataFrame({"a": [1, np.nan, 3], "b": [4, 5, np.nan]})

# pandas 3.0: 変更されたオブジェクト自身を返す
result = df.fillna(0, inplace=True)
print(result is df)  # True
print(result)
#      a    b
# 0  1.0  4.0
# 1  0.0  5.0
# 2  3.0  0.0

新しい警告クラス

pandas 3.0では、将来のメジャーバージョンに向けた段階的な非推奨ポリシー(PDEP-17)が導入されました。新しい警告クラスで、変更のターゲットバージョンが一目でわかるようになっています。

  • pandas.errors.Pandas4Warning — pandas 4.0で予定されている変更
  • pandas.errors.Pandas5Warning — pandas 5.0で予定されている変更
  • pandas.errors.PandasChangeWarning — すべての変更警告の基底クラス

最小バージョン要件

pandas 3.0では、依存関係の最小バージョンがかなり引き上げられています。アップグレード前に確認しておきましょう。

  • Python: 3.11以上(3.10のサポートは削除)
  • NumPy: 1.26.0以上
  • PyArrow: 13.0.0以上(オプションだが強く推奨)
  • matplotlib: 3.9.3以上
  • SQLAlchemy: 2.0.36以上
  • scipy: 1.14.1以上
  • numba: 0.60.0以上

read_iceberg()の追加

Apache Icebergテーブルの読み書きをサポートする新しいI/O関数が追加されました。Icebergはデータレイクでよく使われているオープンテーブルフォーマットで、pandasから直接アクセスできるようになるのは便利です。

import pandas as pd

# Apache Icebergテーブルの読み込み
# df = pd.read_iceberg("catalog.database.table_name")

# Icebergテーブルへの書き込み
# df.to_iceberg("catalog.database.table_name", mode='overwrite')

Windows ARM64ホイール

Windows ARM64プラットフォーム向けの公式ホイールが提供されるようになりました。Surface Pro Xなど、ARM64ベースのWindowsデバイスでもpandasが簡単にインストールできます。

その他のI/O改善

  • read_parquet()to_pandas_kwargsパラメータを受け付けるように
  • DataFrame.to_csv()がf-string形式のフォーマット指定をサポート(float_format="{:.6f}"
  • DataFrame.to_excel()autofilterパラメータとmerge_cells="columns"オプションが追加
  • DataFrame.to_json()Decimal型を文字列としてエンコード

移行ガイド:pandas 2.x から 3.0 へ

さて、ここからが実践編です。pandas 3.0への移行は段階的に進めるのがベストです。以下のステップに沿って進めましょう。

ステップ1:pandas 2.3へのアップグレード

まずはpandas 2.3にアップグレードしてください。pandas 3.0で削除された多くの機能は、2.3の時点で非推奨警告が出ます。

# pandas 2.3にアップグレード
pip install pandas==2.3.*

ステップ2:非推奨警告の解消

pandas 2.3で実行して、すべての非推奨警告を潰しましょう。

import warnings
import pandas as pd

# すべての警告を表示する設定
warnings.filterwarnings("always")

# テストスイートを実行して警告を確認
# python -W all -m pytest tests/

ステップ3:Copy-on-Writeを有効にしてテスト

pandas 2.3ではCoWをオプションで有効にできるので、本番に移行する前に試しておきましょう。

import pandas as pd

# pandas 2.3でCoWを有効化
pd.set_option("mode.copy_on_write", True)

# テストを実行
# チェーン代入を使っている箇所でエラーが出たら修正する

ステップ4:文字列dtypeの確認

同じくpandas 2.3で、新しい文字列推論もオプションとして有効にできます。

import pandas as pd

# 新しい文字列推論を有効化
pd.options.future.infer_string = True

# dtype == "object" をチェックしているコードを更新
# pd.api.types.is_string_dtype() の使用を推奨

ステップ5:チェーン代入パターンの更新

チェーン代入を使っているコードは、.loc.ilocを使った直接代入に書き換えてください。

import pandas as pd

df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})

# 修正が必要なパターン

# パターン1: チェーン代入
# 修正前: df["a"][0] = 100
# 修正後:
df.loc[0, "a"] = 100

# パターン2: 条件付きチェーン代入
# 修正前: df["a"][df["b"] > 4] = 999
# 修正後:
df.loc[df["b"] > 4, "a"] = 999

# パターン3: 不要な.copy()の削除
# 修正前: subset = df[df["a"] > 1].copy()
# 修正後: (CoWにより.copy()は不要)
subset = df[df["a"] > 1]

ステップ6:日時処理コードの確認

datetimeデータを整数に変換している箇所がある場合は、解像度変更の影響を必ず確認してください。

import pandas as pd

# 整数変換では、明示的にas_unit()を使用する
ts = pd.Timestamp("2026-01-21")

# 安全な方法
value_ns = ts.as_unit("ns").value
value_us = ts.as_unit("us").value
print(f"ナノ秒: {value_ns}")
print(f"マイクロ秒: {value_us}")

ステップ7:pandas 3.0へのアップグレード

テストが通って、警告も解消できたら、いよいよpandas 3.0にアップグレードです。

# pandas 3.0にアップグレード
pip install pandas==3.0.*

# PyArrowを含むインストール(推奨)
pip install pandas[pyarrow]==3.0.*

# conda を使用する場合
conda install -c conda-forge pandas=3.0

ステップ8:徹底的なテスト

アップグレード後は、テストスイート全体を通して、特に以下の領域を重点的に確認してください。

  • DataFrameのスライスや代入操作
  • 文字列カラムの処理とdtype判定
  • 日時データの変換と計算
  • タイムゾーンを使用した処理
  • 欠損値(NaN/NA)の処理
  • 大規模データセットでのパフォーマンス
# テストスイートの実行
python -m pytest tests/ -v --tb=long

# パフォーマンスベンチマークの実行
python -m pytest tests/benchmarks/ -v

まとめ

pandas 3.0は、Pythonデータ分析エコシステムにおける大きなマイルストーンです。改めて主要な改善点を振り返っておきましょう。

  • Copy-on-Write(CoW)がデフォルトになり、あのSettingWithCopyWarningとは完全にお別れ。防御的な.copy()も不要になり、パフォーマンスも向上します。
  • 新しい文字列型(str dtype)で、文字列操作が5〜10倍高速化、メモリ使用量は最大50%削減。大規模テキストデータを扱うプロジェクトでは特に恩恵が大きいです。
  • カラム式(pd.col())が、ラムダ関数に代わるクリーンな構文を提供。メソッドチェーンの可読性が格段に上がります。
  • 日時解像度の推論変更で、1678年以前や2262年以降の日付も扱えるように。ただし、整数変換時の値の変化には注意が必要です。
  • タイムゾーン処理がPython標準のzoneinfoに移行し、外部依存が減りました。
  • Anti-Joinがネイティブサポートされ、データのクレンジングや差分抽出がぐっと直感的に。
  • NaN/NAの統一で、nullable dtypesでの欠損値処理が一貫したものに。
  • Arrow PyCapsuleインターフェースで、他のデータ処理ライブラリとのゼロコピーデータ交換が可能に。

移行にはコードの修正が必要ですが、パフォーマンスの向上と動作の一貫性を考えると、間違いなくその価値があります。pandas 2.3を経由して段階的に進めれば、リスクも最小限に抑えられます。

pandas 3.0で、より効率的で予測可能なデータ処理を実現していきましょう。新機能をうまく活用して、クリーンで高速なデータパイプラインを構築してみてください。