対応関係を渡して値を変換する: pandas / polars / BigQuery

python
SQL
前処理
Author

Ryo Nakagami

Published

2026-03-16

Modified

2026-04-27

問題設定: 文字列カテゴリを数値にマッピングしたい

Exercise 1

商品グレード (grade) が文字列で格納されているテーブルに対し, {C: 1, B: 2, A: 3, S: 4} という対応関係を使って数値カラム grade_norm を追加したい. 入力例は次のようなレコード形式とします.

import pandas as pd

df = pd.DataFrame(
    [
        (1, "C", 1000),
        (2, "B", 2500),
        (3, "A", 5000),
        (4, "S", 10000),
    ],
    columns=["id", "grade", "price"],
)
df
id grade price
0 1 C 1000
1 2 B 2500
2 3 A 5000
3 4 S 10000

期待する出力

id grade price grade_norm
1 C 1000 1
2 B 2500 2
3 A 5000 3
4 S 10000 4

ポイント

  • mapping は「キー → 値」の辞書として与えられているケースを想定する
  • mapping にないキーが入力に出現したときの挙動 (NaN にする / 元の値を残す / エラー) を意識して書き分ける
  • pandas, polars, BigQuery のいずれも元のカラムを破壊せずに別名で追加できる書き方が望ましい
grade_mapping = {"C": 1, "B": 2, "A": 3, "S": 4}

Pattern 1: pandas

Series.map(dict) が王道.元 DataFrame を残したまま新しいカラムとして追加するには DataFrame.assign と組み合わせる.

df.assign(grade_norm=df["grade"].map(grade_mapping))
id grade price grade_norm
0 1 C 1000 1
1 2 B 2500 2
2 3 A 5000 3
3 4 S 10000 4

mapmapping にないキーを NaN にするため,結果のカラム dtype が float64 に昇格する点に注意. 整数型のまま持ちたい場合は nullable integer 型 (Int64) を使う.

df.assign(grade_norm=df["grade"].map(grade_mapping).astype("Int64"))
id grade price grade_norm
0 1 C 1000 1
1 2 B 2500 2
2 3 A 5000 3
3 4 S 10000 4
Warning
  • Series.replace(dict) でも同じ変換は書けるが,map の方が高速かつ意図が明確 (replace は正規表現マッチも見るためオーバヘッドがある)
  • mapping にないキーをエラーにしたいときは 事前に set(df["grade"]) - set(grade_mapping) で差集合を取って assert する

Pattern 2: polars

polars では pl.col(...).replace_strict(mapping)最も意図に近いreplace_strict は mapping にないキーが入力に存在するとエラーになるので,未知カテゴリの混入を取りこぼさない.

import polars as pl

df_pl = pl.from_pandas(df)

df_pl.with_columns(
    pl.col("grade").replace_strict(grade_mapping, return_dtype=pl.Int64).alias("grade_norm")
)
shape: (4, 4)
id grade price grade_norm
i64 str i64 i64
1 "C" 1000 1
2 "B" 2500 2
3 "A" 5000 3
4 "S" 10000 4

未知キーを許容したい場合

replace_strictdefault= を渡すと,mapping にないキーをエラーにせず指定値で埋められる. null を fallback にすれば pandas の Series.map と同じ挙動になる.

df_pl.with_columns(
    pl.col("grade")
      .replace_strict(grade_mapping, default=None, return_dtype=pl.Int64)
      .alias("grade_norm")
)
shape: (4, 4)
id grade price grade_norm
i64 str i64 i64
1 "C" 1000 1
2 "B" 2500 2
3 "A" 5000 3
4 "S" 10000 4

注意点 !

  • polars 1.0 以降,replacedefault=return_dtype= は deprecatedになり replace_strict 側に統一された.新規コードは常に replace_strict を使うのが安全
  • replace(strict なし) はmapping の対象キーだけを置換し,それ以外は元の値を残す用途に絞られている.dtype も入力と同じになるため,文字列 → 数値のような型変換は表現できない

pandas との対応関係

操作 pandas polars
辞書で値を変換 (型変換あり) s.map(mapping) pl.col(c).replace_strict(mapping)
未知キーを NaN/null にする s.map(mapping) (デフォルト) pl.col(c).replace_strict(mapping, default=None)
未知キーで元の値を残す (型保持) s.replace(mapping) pl.col(c).replace(mapping)
未知キーをエラーにする (手動チェック) pl.col(c).replace_strict(mapping)
別カラムとして追加 df.assign(new=...) df.with_columns(... .alias("new"))

注意点 !

  • return_dtype= を明示しないと polars がmapping の値から dtype を推論するため,意図せず Int32 などになる場合がある.スキーマを固定したいなら明示する
  • 大規模データでは df_pl.lazy().with_columns(...).collect() でクエリ最適化が効く

Pattern 3: BigQuery

BigQuery では mapping のサイズと再利用性で書き方を選ぶ.

小さい mapping: CASE WHEN

数件〜十数件であれば CASE WHEN が最もシンプルで読みやすい.

SELECT
  id,
  grade,
  price,
  CASE grade
    WHEN 'C' THEN 1
    WHEN 'B' THEN 2
    WHEN 'A' THEN 3
    WHEN 'S' THEN 4
  END AS grade_norm
FROM `project.dataset.products`;

中規模 mapping: UNNEST(ARRAY<STRUCT>) で inline join

mapping を SQL の中にデータとして埋め込みたい場合は UNNEST で擬似テーブル化して LEFT JOIN する. CASE WHEN の羅列より見通しが良く,mapping の追加・削除も差分が小さい.

WITH grade_map AS (
  SELECT * FROM UNNEST([
    STRUCT('C' AS grade, 1 AS grade_norm),
    STRUCT('B'         , 2),
    STRUCT('A'         , 3),
    STRUCT('S'         , 4)
  ])
)
SELECT
  p.id,
  p.grade,
  p.price,
  m.grade_norm
FROM `project.dataset.products` AS p
LEFT JOIN grade_map AS m USING (grade);

再利用する mapping: 専用テーブル + LEFT JOIN

複数のクエリで使い回すなら mapping テーブルを永続化する.LEFT JOIN にすることで未知キーは NULL になり, 未知キーを検知したい場合は WHERE m.grade_norm IS NULL でカウントできる.

-- 一度だけ実行
CREATE OR REPLACE TABLE `project.dataset.grade_map` (
  grade STRING NOT NULL,
  grade_norm INT64 NOT NULL
);

INSERT INTO `project.dataset.grade_map` VALUES
  ('C', 1), ('B', 2), ('A', 3), ('S', 4);

-- 利用クエリ
SELECT
  p.id,
  p.grade,
  p.price,
  m.grade_norm
FROM `project.dataset.products` AS p
LEFT JOIN `project.dataset.grade_map` AS m USING (grade);
Warning
  • CASE WHEN条件式が増えるほどクエリの可読性が落ちる.目安として10件を超えたら UNNEST 方式に切り替えた方がよい
  • INNER JOIN を使うと未知キーの行が暗黙に脱落するので,集計値がずれる原因になる.mapping 用途では原則 LEFT JOIN
  • mapping テーブルを別データセットに置く場合,クロスリージョン参照はコスト・レイテンシのペナルティが発生する点に注意

まとめ

Tip3パターンの使い分け
  • pandas: notebook での探索的な変換ならこれ.Series.map(dict) + DataFrame.assign で1行に収まる
  • polars: 未知カテゴリを漏らさず検知したいETLでは replace_strict が便利.return_dtype= でスキーマを固定できる点も型安全に寄与する
  • BigQuery: mapping のサイズ次第で CASE WHEN / UNNEST(ARRAY<STRUCT>) / 専用テーブル+LEFT JOIN を使い分ける.再利用と運用を考えるなら早めにテーブル化しておくと後の修正コストが小さい