更新日: 2026年8月2日
Narwhalsは、pandas、Polars、PyArrow、Modin、cuDF、Daskの上に統一されたAPIを提供する、依存関係ゼロのデータフレーム抽象化ライブラリです。ライブラリ作者は一度Narwhalsで関数を書けば、ユーザーがどのバックエンドを渡しても同じコードが動作します。scikit-learn、Altair、Plotly、marimoなどの主要OSSがすでに内部採用しており、2026年時点で「バックエンド非依存のデータフレームコード」を書く事実上の標準となっています。
正直に言うと、私も最初は「また抽象化レイヤーか」と半信半疑でした。ただ、社内のETLライブラリをpandasからPolarsに移行しようとしたとき、Narwhalsを挟むだけで既存ユーザーを壊さず移行できた経験から、考えが変わりました。ここではその実感を交えつつ、2026年のNarwhalsの使いどころを整理します。
- Narwhalsは Polars APIのサブセットを提供し、内部でpandas・Polars・PyArrowなど各バックエンドの命令に変換する薄いラッパーライブラリです。
- 依存関係ゼロ設計のため、ライブラリに追加してもエンドユーザーのpandas/Polarsバージョンと衝突しません。
@nw.narwhalifyデコレータまたはnw.from_native() / nw.to_native()の組で、既存関数を簡単にバックエンド非依存化できます。
- LazyFrame(Polars)およびDaskの遅延評価APIにも対応し、大規模データのストリーミング処理が可能です。
- Ibisはデータベース側で計算するのに対し、Narwhalsはインメモリのデータフレーム間の互換性に特化しているため用途が異なります。
- 安定APIは
narwhals.stable.v1として凍結されており、Narwhalsのマイナーバージョン更新でも破壊的変更が入らないことが保証されています。
Narwhalsとは何か
Narwhalsは、Marco Gorelli氏が中心となって開発しているオープンソースのデータフレーム抽象化(dataframe-agnostic)ライブラリです。pandas、Polars、PyArrow Table、Modin、cuDF、Daskといった異なるバックエンドに対して、Polarsに似た統一APIを提供します。ライブラリ作者はNarwhalsのAPIだけを使って関数を書けば、その関数はユーザーが渡した任意のデータフレームを内部で自然にネイティブ操作へ変換して実行します。
「抽象化」と聞くとオーバーヘッドを想像するかもしれませんが、Narwhalsは純粋なPythonの薄いラッパーであり、実行時にNumPyや別のC拡張を挟むことはしません。すべての操作は最終的にネイティブのpandasやPolarsのメソッド呼び出しに変換されるため、性能はネイティブとほぼ同等になります。この設計により、統計計算やETLの分野で新しいバックエンドが登場しても、ライブラリ側は一切コードを変えずに追従できます。
Polarsの学び直しについてはpandasユーザーのためのPolars入門ガイドを先に読むと、Narwhalsが提供するAPIの背景がより明確になります。Narwhalsは「Polarsの表現力をpandasを含む全バックエンドに輸出する」プロジェクトと理解すると腑に落ちます。
なぜ2026年にNarwhalsが必要なのか
ちょっと歴史の話から。2022年頃までは「Pythonのデータフレーム=pandas」でしたが、2023年以降のPolarsの急成長、PyArrow Tableの普及、GPU向けcuDFの実運用化により、エコシステムは急速に多元化しました。scikit-learn、Altair、Plotly、seaborn、shapなどの下流ライブラリは、いずれか単一のバックエンドに縛られると新規ユーザーを失います。かといって全バックエンド分の分岐コード(if isinstance(df, pd.DataFrame):のような)を書くのは、テスト工数と保守負債を爆発させます。私自身、以前担当していた社内ライブラリで分岐が20本を超えた時点で「これはもう無理」と判断しました。
Narwhalsはこの構造的な問題を解決するために誕生しました。事実、scikit-learn 1.4以降のset_output(transform="polars")機能や、Altair 5.4以降のPolars対応、Plotly Express 6.xのネイティブPolars受け入れは、いずれもNarwhalsを内部で使用して実現されています。つまりNarwhalsは目に見えないインフラとしてすでに広く動いており、ライブラリ作者にとっては「使わない理由の方が説明しづらい」段階に来ています。
データエンジニアやアプリ開発者にとっても、Narwhalsを知っておく意義は大きいです。社内共通の前処理ライブラリをpandasで書いたあと、後からPolarsに移行するチームが出てきた場合、Narwhalsで書き直しておけば両方のチームが同じ関数を共有できます。pandas 3.0のCopy-on-Writeへの段階的移行フェーズと組み合わせても相性が良く、pandas 2系と3系、そしてPolarsが混在する現実的な組織構成にフィットします。
インストールとセットアップ
Narwhals自体は依存関係ゼロで配布されているため、任意のプロジェクトに追加してもバージョン競合の心配が最小限です。必要なのはNarwhalsパッケージ本体と、実際に扱うバックエンド(pandas、Polarsなど)のいずれかです。
# Narwhals本体のみ(バックエンドは呼び出し側が用意)
pip install narwhals
# pandasとPolarsの両方でテストしたい場合
pip install narwhals pandas polars pyarrow
# uv派の方は
uv add narwhals
ライブラリを配布する場合、pyproject.tomlのdependenciesにはnarwhalsだけを書き、pandasやPolarsはextrasやユーザー任せにするのがベストプラクティスです。これにより、cuDFしか使わないGPUユーザーにpandasをインストールさせるような無駄を避けられます。
# pyproject.toml の例
[project]
name = "my-analytics-lib"
dependencies = [
"narwhals>=1.20",
]
[project.optional-dependencies]
pandas = ["pandas>=2.2"]
polars = ["polars>=1.10"]
基本的な使い方:@narwhalifyデコレータ
Narwhalsの入門で最初に押さえるべきは、2つのパターンです。1つ目はfrom_nativeとto_nativeを明示的に呼び出す方法、2つ目は@nw.narwhalifyデコレータで関数全体を包む方法です。ほとんどのユースケースではデコレータで十分ですが、内部で複数の中間フレームを扱う場合は明示APIが便利です。
import narwhals.stable.v1 as nw
import pandas as pd
import polars as pl
# パターン1:明示的なfrom_native / to_native
def group_mean_explicit(df_native):
df = nw.from_native(df_native)
result = (
df.group_by("category")
.agg(nw.col("value").mean().alias("mean_value"))
.sort("mean_value", descending=True)
)
return nw.to_native(result)
# パターン2:@narwhalifyデコレータ(推奨)
@nw.narwhalify
def group_mean(df):
return (
df.group_by("category")
.agg(nw.col("value").mean().alias("mean_value"))
.sort("mean_value", descending=True)
)
# 同じ関数がpandasとPolarsの両方で動く
pdf = pd.DataFrame({"category": ["A", "A", "B"], "value": [10, 20, 30]})
pldf = pl.DataFrame({"category": ["A", "A", "B"], "value": [10, 20, 30]})
print(type(group_mean(pdf))) # <class 'pandas.core.frame.DataFrame'>
print(type(group_mean(pldf))) # <class 'polars.dataframe.frame.DataFrame'>
ポイントは、Narwhalsが入力の型を記憶し、同じ型で結果を返すことです。pandasを渡せばpandasが返り、Polarsを渡せばPolarsが返ります。この「型の透過性」こそが、ライブラリの利用側から見た自然な体験を作ります。ユーザーは「Narwhalsを使っている」ことを意識する必要すらありません。
対応バックエンドと選び方
Narwhals 1.x系(2026年8月時点)が正式サポートするバックエンドと、それぞれの得意分野を整理します。すべて同じAPIで書けますが、実行時の性能特性は大きく異なるため、ユーザーが選べるようにしておくことが重要です。
| バックエンド | サポート状況 | 得意分野 | Lazy対応 |
| pandas | 完全サポート | 既存資産・小〜中規模データ | ×(eagerのみ) |
| Polars(eager) | 完全サポート | 単一マシンでの高速処理 | △ |
| Polars(LazyFrame) | 完全サポート | クエリ最適化とストリーミング | ○ |
| PyArrow Table | 完全サポート | Parquet中心のETL・ゼロコピー連携 | × |
| Modin | 完全サポート | pandas互換の並列処理 | × |
| cuDF | 完全サポート | NVIDIA GPUでの数十倍高速化 | × |
| Dask DataFrame | 完全サポート | クラスタ規模の分散処理 | ○ |
| PySpark | 実験的サポート | 既存Spark資産との統合 | ○ |
選定の目安として、単一マシンで数百万〜数千万行を扱うならPolars、既存の分析ノートブックとの互換を優先するならpandas、Parquet中心の列指向パイプラインならPyArrow、GPUマシンを持っていてETLをさらに高速化したいならcuDFがおすすめです。Narwhalsを使えばコードは同じなので、ベンチマークで最速のバックエンドに乗り換えるのは基本的にimport文の変更だけです。
NarwhalsとIbisの違いは何ですか
Narwhalsとよく比較されるのがIbisです。両者は「Pythonから複数のバックエンドを叩ける統一API」という点では似ていますが、対象領域が根本的に異なります。ここは実際に混同するとアーキテクチャ選定を誤るポイントなので、以下の対比を頭に入れておきましょう(私も一度、Ibisで解決すべき問題にNarwhalsを持ち込んで遠回りしたことがあります)。
| 比較軸 | Narwhals | Ibis |
| 主なバックエンド | インメモリ:pandas / Polars / PyArrow / cuDF / Dask | データベース:DuckDB / BigQuery / Snowflake / Postgres 等 |
| 実行モデル | 各バックエンドのDataFrame APIに直接翻訳 | SQLに翻訳して各エンジンに送信 |
| 依存関係 | ゼロ(コアは純粋Python) | 各バックエンドドライバに依存 |
| API設計 | Polarsのサブセット | Ibis独自の式ビルダー |
| 典型ユースケース | OSSライブラリの内部実装 | データウェアハウスに対する分析クエリ |
| データ移動 | 不要(元のフレームをそのまま操作) | 結果セットのみクライアントに転送 |
ざっくりまとめると、NarwhalsはPythonプロセス内のDataFrame同士の互換性、IbisはSQLエンジンとの通信を担います。両者は競合ではなく補完関係にあり、実際にはIbisの結果をPolars DataFrameとして受け取り、下流の可視化ライブラリがNarwhalsで処理する、といった組み合わせが自然です。データベース側の集計はDuckDB × Python実践ガイドのようにIbis/DuckDBで、その先の変換・可視化はNarwhalsで、という役割分担がおすすめです。
LazyFrameとストリーミング処理
Narwhalsのユニークな価値の1つが、eagerとlazyの両方のAPIを同じコードで扱える点です。関数の中でdf.lazy()を呼べば、pandasバックエンドではno-op、Polarsバックエンドでは実際のLazyFrame化が起き、.collect()で結果を取り出します。これによりPolarsのクエリオプティマイザやストリーミングエンジンの恩恵を、pandasユーザーとPolarsユーザーの両方に自然に届けられます。
import narwhals.stable.v1 as nw
@nw.narwhalify
def top_n_by_group(df, n: int = 3):
return (
df.lazy() # Polarsのみ実際にlazy化
.group_by("region")
.agg([
nw.col("revenue").sum().alias("total"),
nw.col("customer_id").n_unique().alias("customers"),
])
.sort("total", descending=True)
.head(n)
.collect() # 実行トリガ
)
# Polars LazyFrameを渡せばストリーミングエンジンが動く
import polars as pl
lf = pl.scan_parquet("s3://bucket/sales/*.parquet")
result = top_n_by_group(lf)
この例では、Polars LazyFrameを渡した場合はクエリ最適化・述語プッシュダウン・列プルーニングが自動的に適用され、pandas DataFrameを渡した場合は通常のeager実行になります。ストリーミング/lazy評価の詳細はPolars LazyFrame完全ガイドを参照してください。同じ関数がバックエンドの能力に応じて最適化されるのは、抽象化ライブラリならではの強みです。
実践例:ライブラリ関数をバックエンド非依存にする
ここでは、社内共通の「特徴量エンジニアリング関数」をNarwhalsで書き直す例を示します。日次売上ログから、顧客ごとの直近30日購入額と平均単価を計算するユーティリティです。元はpandas前提でしたが、Polarsユーザーからも使いたいという要望が出た、というよくあるシチュエーションを想定します。
import narwhals.stable.v1 as nw
from datetime import timedelta
@nw.narwhalify
def customer_features(orders, as_of_date):
"""
orders : DataFrame with [customer_id, order_date, amount, quantity]
as_of_date : date (計算基準日)
"""
window_start = as_of_date - timedelta(days=30)
recent = orders.filter(
(nw.col("order_date") >= window_start) &
(nw.col("order_date") <= as_of_date)
)
features = (
recent
.with_columns(
(nw.col("amount") / nw.col("quantity")).alias("unit_price")
)
.group_by("customer_id")
.agg([
nw.col("amount").sum().alias("recent_30d_amount"),
nw.col("unit_price").mean().alias("avg_unit_price"),
nw.col("order_date").n_unique().alias("purchase_days"),
])
.sort("recent_30d_amount", descending=True)
)
return features
# 呼び出し側:好きなバックエンドを渡すだけ
# pandas版
import pandas as pd
pdf = pd.read_parquet("orders.parquet")
features_pd = customer_features(pdf, pd.Timestamp("2026-07-31").date())
# Polars版
import polars as pl
pldf = pl.read_parquet("orders.parquet")
features_pl = customer_features(pldf, date(2026, 7, 31))
ポイントは3つあります。第1に、nw.col()による列参照はpandasとPolarsの両方で自然に動きます。第2に、条件式(&や>=)もNarwhalsが両バックエンドの構文差を吸収します。第3に、集計関数(sum、mean、n_unique)は共通名で提供されており、内部でPolarsの表現に翻訳されます。既存のpandasコードから移植する際は「df[col].sum()」を「nw.col(col).sum()」に置き換える機械的な作業がほとんどです。
「抽象化レイヤーはオーバーヘッドを生む」というのは一般論としては正しいのですが、Narwhalsに関しては実測でほぼゼロと言って差し支えありません。Narwhalsは実行時にネイティブAPIへ「静的に」翻訳するだけで、行ごとのPythonループやリフレクションを挟みません。公式ベンチマークではpandasネイティブ実装に対する差は数マイクロ秒〜数十マイクロ秒のオーダーで、100万行以上のデータセットでは誤差レベルに収束します。私の手元で500万行のログを回した際も、pandas直接呼び出しとの差は測定ノイズの範囲でした。
ただし1つだけ注意点があります。Narwhalsが翻訳できない操作(例えばpandasにしかないpivot_tableの特定オプション)を呼び出そうとするとNotImplementedErrorが投げられます。この場合は「エスケープハッチ」としてnw.to_native()で一時的に元のフレームに戻し、ネイティブAPIを呼んでから再度nw.from_native()で包み直すことができます。この設計により、Narwhalsは「共通部分は抽象化し、独自機能はネイティブに委ねる」という現実的な妥協点を実現しています。
実測ベンチマークを行いたい場合は、pytest-benchmarkと組み合わせて「同一データ・同一操作」で3つの実装(純pandas、純Polars、Narwhals経由)を比較するのがおすすめです。ほとんどのケースで、Narwhals経由の実行時間はネイティブと区別できないことが確認できるはずです。
既存プロジェクトへの導入戦略
結論から言うと、既存のpandasコードベースにNarwhalsを一気に導入するのはおすすめしません(以前これをやろうとしてPRが2000行を超え、結局取り下げました)。代わりに、ライブラリ境界の関数から段階的にNarwhals化していく戦略が現実的です。以下の3ステップを推奨します。
- 公開APIの入り口を特定する:他モジュールやユーザーが直接呼ぶ関数(
fit、transform、featurizeなど)をリストアップします。ここが最も互換性の恩恵が大きい層です。
- デコレータで包む:該当関数の頭に
@nw.narwhalifyを付け、関数本体をnw.col()ベースの表現に書き換えます。内部で使う補助関数はまだpandas依存のままで構いません。
- 内部関数を順次移行:ユニットテストにPolarsケースを追加し、パスすることを確認しながら内部関数のシグネチャを揃えていきます。最終的にはコードベース全体がNarwhals化され、pandas固定の分岐が消滅します。
CI環境では、pandasとPolarsの両方で同じテストセットを走らせるパラメトリックテストが有効です。pytestの@pytest.mark.parametrizeにバックエンドファクトリを渡し、両方のバックエンドで結果が一致することを機械的に検証しましょう。データフレームの検証にはPandera完全ガイドで扱ったスキーマ検証と組み合わせると、より堅牢なテストになります。
Narwhalsの詳細なAPIリファレンスや最新の対応バックエンドはNarwhals公式ドキュメントで確認できます。開発の議論やロードマップはGitHubリポジトリで追えます。データフレーム間の標準化については、Consortium for Python Data API StandardsのDataFrame Interchange Protocol仕様も併せて読むと、Narwhalsが埋めているギャップの意義がより深く理解できます。
よくある質問
Narwhalsとpandasの違いは何ですか?
pandasは実際にデータを保持・演算するデータフレームライブラリです。一方Narwhalsは自らはデータを持たず、渡されたpandas/Polars/PyArrowなどのフレームに対して統一APIを提供する薄いラッパーです。競合関係ではなく、pandasの上にNarwhalsを載せて使う関係になります。
Narwhalsは本番環境で使えますか?
はい。scikit-learn、Altair、Plotly、marimoといった主要OSSがすでに本番採用しています。narwhals.stable.v1という凍結APIを使えば、Narwhals本体のマイナーバージョン更新でも破壊的変更が入らないため、長期運用でも安全です。
NarwhalsとIbisはどちらを選ぶべきですか?
用途で決まります。Pythonプロセス内のDataFrame(pandas、Polarsなど)の互換性ならNarwhals、DuckDBやBigQueryなどSQLエンジンへのクエリ発行ならIbisです。両者は補完関係にあり、Ibisで取得した結果をNarwhalsで加工するパイプラインも自然に成立します。
Narwhalsを使うとパフォーマンスに影響がありますか?
ほとんど影響ありません。Narwhalsは実行時にネイティブAPIへ静的翻訳するだけで、行ごとのループやリフレクションは行いません。100万行以上のデータセットではpandasネイティブと実測差は誤差レベルです。
どのようなプロジェクトでNarwhalsが推奨されますか?
データフレームを引数として受け取るライブラリ(前処理・特徴量エンジニアリング・可視化・統計モデリング等)の作者にとって最適です。逆にアプリケーションコードで単一のバックエンドしか使わない場合は、pandasやPolarsを直接使う方がシンプルで学習コストも低くて済みます。