アルゴリズム概要

縦持ち(EAV形式)で格納された Department テーブルを、部門ID × 月 の2次元テーブル(横持ち)にピボット変換する問題です。 SQLでは GROUP BY id条件付き集計 MAX … FILTER を組み合わせて12列を一度に展開します。

📥 入力

id revenue month
1 8000 Jan
2 9000 Jan
3 10000 Feb
1 7000 Feb
1 6000 Mar

📤 出力(12列)

id Jan_Revenue Feb_Revenue Mar_Revenue
1 8000 7000 6000 null
2 9000 null null null
3 null 10000 null null
🔑
主キー保証
(id, month) が主キー = 各グループに最大1行 → MAX / first で確定
📐
静的ピボット
月は固定12種類 → 動的SQLや crosstab() 不要、FILTER 句12個で完結
🚀
計算量
時間 O(N)・空間 O(#dept × 12) — N行を1パス集計で完了

ステップバイステップ解説

SQL実装(PostgreSQL 16.6+)

最適解 FILTER句(PostgreSQL 9.4+)
WITH pivoted AS (
  SELECT
    id,
    MAX(revenue) FILTER (WHERE month = 'Jan') AS "Jan_Revenue",
    MAX(revenue) FILTER (WHERE month = 'Feb') AS "Feb_Revenue",
    MAX(revenue) FILTER (WHERE month = 'Mar') AS "Mar_Revenue",
    MAX(revenue) FILTER (WHERE month = 'Apr') AS "Apr_Revenue",
    MAX(revenue) FILTER (WHERE month = 'May') AS "May_Revenue",
    MAX(revenue) FILTER (WHERE month = 'Jun') AS "Jun_Revenue",
    MAX(revenue) FILTER (WHERE month = 'Jul') AS "Jul_Revenue",
    MAX(revenue) FILTER (WHERE month = 'Aug') AS "Aug_Revenue",
    MAX(revenue) FILTER (WHERE month = 'Sep') AS "Sep_Revenue",
    MAX(revenue) FILTER (WHERE month = 'Oct') AS "Oct_Revenue",
    MAX(revenue) FILTER (WHERE month = 'Nov') AS "Nov_Revenue",
    MAX(revenue) FILTER (WHERE month = 'Dec') AS "Dec_Revenue"
  FROM Department
  GROUP BY id
)
SELECT *
FROM pivoted
ORDER BY id;

💡 なぜ MAX を使うのか?

主キー制約により各 (id, month) グループは必ず1行以下。MAX は単一値をそのまま返し、行が存在しない場合は NULL を返します。 SUMMIN でも同結果になりますが、「1つの値を選ぶ」という語義が最も正確なのは MAX です。 FILTER (WHERE ...) はオプティマイザが集計前に条件を適用でき、CASE WHEN より高効率です。

代替:CASE WHEN(互換性重視)

SELECT
  id,
  MAX(CASE WHEN month = 'Jan' THEN revenue END) AS "Jan_Revenue",
  MAX(CASE WHEN month = 'Feb' THEN revenue END) AS "Feb_Revenue",
  MAX(CASE WHEN month = 'Mar' THEN revenue END) AS "Mar_Revenue",
  -- ... 残り9ヶ月分
  MAX(CASE WHEN month = 'Dec' THEN revenue END) AS "Dec_Revenue"
FROM Department
GROUP BY id
ORDER BY id;

Pandas実装(Python 3.10 / pandas 2.2.2)

改善版 set_index + unstack(Memory最適化済) 中間オブジェクト: 1個
import pandas as pd

_MONTHS = ["Jan","Feb","Mar","Apr","May","Jun",
           "Jul","Aug","Sep","Oct","Nov","Dec"]

def reformat_department(department: pd.DataFrame) -> pd.DataFrame:
    """
    縦持ちの Department テーブルを横持ちにピボットする。

    Args:
        department (pd.DataFrame): columns = [id, revenue, month]

    Returns:
        pd.DataFrame: [id, Jan_Revenue, Feb_Revenue, ..., Dec_Revenue]
                      売上なし月は NaN。
    """
    # ① (id, month) が主キー → 集計不要、直接ピボット軸を構築
    #    unstack は Cython レベルで動作し Python 集計呼び出しなし
    out = (
        department
        .set_index(["id", "month"])["revenue"]   # MultiIndex Series: O(N)
        .unstack("month")                         # Series → 2D DataFrame: O(N)
        .reindex(columns=_MONTHS)                 # 欠損月補完 + カレンダー順: O(12)
    )

    # ② 列名を一括リネーム(リスト代入はコピーなし)
    out.columns = [f"{m}_Revenue" for m in out.columns]

    # ③ id を通常列に戻す
    out = out.reset_index()
    out.columns.name = None

    return out
❌ 旧実装 pivot_table
中間オブジェクト3〜4個、Python レベルの aggfunc 呼び出し発生。Memory Beats 5.66%
✅ 改善版 set_index + unstack
中間オブジェクト1個、Cythonレベル直接展開。Memory Beats 50〜80% 期待

フローチャート

開始 入力テーブル確認 Department(id, revenue, month) (id, month) 主キー保証あり? はい 集計は MAX (1行確定) いいえ 重複あり 事前集計を検討 GROUP BY id 部門ごとにグループ化 月別 FILTER 集計(12回) MAX(revenue) FILTER (WHERE month='Jan') … × 12ヶ月分 全12列 展開完了? 次の月 はい NULL補完 売上なし月は自動的に NULL 横持ちテーブル出力 id + 12列(Jan_Revenue … Dec_Revenue) 終了
フローの説明:
1. 入力テーブルの構造(id, revenue, month)を確認する
2. (id, month) が主キーかどうかを確認 — 主キー保証があれば MAX 集計が安全
3. GROUP BY id で部門ごとにグループを形成する
4. MAX(revenue) FILTER (WHERE month = 'X') を月ごとに12回適用(紫ループ)
5. 全12列の展開が完了したら NULL 補完フェーズへ進む
6. 行が存在しない月には自動的に NULL が格納される
7. 最終的な横持ちテーブルを出力して終了

計算量分析

手法 時間計算量 空間計算量 中間オブジェクト 備考
FILTER句(推奨) O(N) O(#dept × 12) 最小 Cython集計前フィルタ
CASE WHEN O(N) O(#dept × 12) 行ごとにWHEN評価
unstack (Pandas) O(N) O(#dept × 12) 1個 Memory最小、Beats↑
pivot_table (Pandas) O(N) O(#dept × 12) 3〜4個 Python aggfunc呼び出し
crosstab() O(N log N) O(#dept × 12) 動的列が必要な場合のみ

⏱ 時間計算量

GROUP BY id はハッシュ集計で O(N)
FILTER 条件評価は O(12×N) = O(N)
インデックスがある場合は Index Scan → Hash Agg で線形近似。

💾 空間計算量

出力行列は O(#dept × 12) の固定サイズ。
N が大きくなっても出力サイズは部門数 × 12列に収束するため、メモリ効率は高い。