アルゴリズム概要

Transactions テーブルから、 月(YYYY-MM)× 国 の組み合わせごとに以下の4指標を集計する問題です。

trans_count
全トランザクション数
approved_count
承認件数
trans_total_amount
全合計金額
approved_total_amount
承認合計金額

入出力例

id country state amount trans_date
121 US approved 1000 2018-12-18
122 US declined 2000 2018-12-19
123 US approved 2000 2019-01-01
124 NULL approved 2000 2019-01-07

⚠️ 落とし穴まとめ

❌ SUM の NULL 問題
承認行が 0 件のグループで SUMNULL を返す → COALESCE(...,0) 必須
⚠️ country=NULL の扱い
SQL の GROUP BY は NULL 値を1つのグループとして保持しますが、pandas の groupby はデフォルトで NULL キーを除外します
❌ FILTER 句の環境差異
PostgreSQL 独自構文のため MySQL では動作しない。本問は PostgreSQL 対応済み

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

PostgreSQL 実装

SELECT
    TO_CHAR(trans_date, 'YYYY-MM')                              AS month,
    country,
    COUNT(*)                                                     AS trans_count,
    COUNT(*) FILTER (WHERE state = 'approved')                  AS approved_count,
    SUM(amount)                                                  AS trans_total_amount,
    COALESCE(SUM(amount) FILTER (WHERE state = 'approved'), 0)  AS approved_total_amount
FROM Transactions
GROUP BY
    TO_CHAR(trans_date, 'YYYY-MM'),
    country;
TO_CHAR(..., 'YYYY-MM')
日付を YYYY-MM 文字列に変換。DATE_TRUNC より出力形式が直接的
FILTER (WHERE ...)
PostgreSQL 拡張。CASE WHEN より意図が明確で最適化しやすい
COALESCE(..., 0)
承認行が 0 件時に SUM が NULL → 0 になるのを防ぐ。★ 最重要修正点

Pandas 実装(Python 3.10 / pandas 2.2.2)

import pandas as pd

def monthly_transactions(transactions: pd.DataFrame) -> pd.DataFrame:
    """
    Returns:
        pd.DataFrame: 列名と順序は
            [month, country, trans_count, approved_count,
             trans_total_amount, approved_total_amount]
    """
    # ★ copy() 廃止: 必要列のみで軽量 DataFrame を新規構築
    is_approved = (transactions['state'] == 'approved').astype('int8')  # int8 で省メモリ

    tmp = pd.DataFrame({
        'month'        : transactions['trans_date'].dt.to_period('M').astype(str),
        'country'      : transactions['country'],
        'id'           : transactions['id'],
        'amount'       : transactions['amount'],
        'is_approved'  : is_approved,
        'approved_amt' : transactions['amount'] * is_approved,
    })

    out = (
        tmp.groupby(['month', 'country'], sort=False, dropna=False)  # ★ dropna=False
           .agg(
               trans_count           = ('id',          'count'),
               approved_count        = ('is_approved', 'sum'),
               trans_total_amount    = ('amount',       'sum'),
               approved_total_amount = ('approved_amt', 'sum'),
           )
           .reset_index()
    )

    # dtype を明示的に int に統一(NaN キー混在時の float64 混入を防ぐ)
    out[['trans_count', 'approved_count',
         'trans_total_amount', 'approved_total_amount']] = \
        out[['trans_count', 'approved_count',
             'trans_total_amount', 'approved_total_amount']].astype(int)

    return out
★ 最重要: dropna=False
pandas の groupby はデフォルト dropna=Truecountry=NaN のグループを無言で除外する。dropna=False で NaN キーも1グループとして保持。
メモリ最適化
.astype('int8') で承認フラグを 1 byte/行に圧縮(int64 比 87.5% 削減)。copy() 廃止で全列複製コストをゼロに。

処理フローチャート

開始 月文字列を生成 TO_CHAR(trans_date, 'YYYY-MM') 承認フラグ列を付与 is_approved = (state == 'approved').astype('int8') GROUP BY month × country dropna=False で NaN キーも保持 ★ 4 指標を一括集計 COUNT(*) → trans_count SUM(is_approved) → approved_count SUM(amount) → trans_total_amount SUM(approved_amt) → approved_total_amount NULL / dtype を統一 COALESCE(..., 0) / .astype(int) reset_index() で列に昇格 出力 DataFrame(6列) month | country | trans_count | approved_count trans_total_amount | approved_total_amount 終了

フローの説明:
1. trans_date から YYYY-MM 形式の月文字列列を生成する
2. state == 'approved' の boolean を int8 に変換し、フラグ列・金額列を付与する
3. month × country で GROUP BY。dropna=Falsecountry=NaN を保持する
4. COUNT / SUM で4指標を一括集計する
5. COALESCE / astype(int) で NULL・dtype を統一する
6. reset_index() で month・country を通常列に昇格し返却する

計算量分析

フェーズ SQL Pandas 備考
月文字列生成 O(N) O(N) ベクトル演算
GROUP BY / groupby.agg O(N) O(N) ハッシュ集計。G ≪ N なら線形近似
COALESCE / astype O(G) O(G) G = 月×国のユニーク数
合計 O(N) O(N) sort=False でソートコスト回避

手法比較

手法 時間 空間 可読性 NULL 安全
✅ FILTER + COALESCE O(N) O(G)
CASE WHEN O(N) O(G) 要 COALESCE
サブクエリ結合 O(N) O(G×2) 要注意
pandas apply O(N×G) O(N)