アルゴリズム概要

問題の説明

食品配達サービスにおいて、各顧客の最初の注文が即日配達(order_date = customer_pref_delivery_date)だった割合を求めます。 即日配達の場合は「immediate」、予約配達の場合は「scheduled」として分類されます。

入力例

Delivery table:
+-------------+-------------+------------+-----------------------------+
| delivery_id | customer_id | order_date | customer_pref_delivery_date |
+-------------+-------------+------------+-----------------------------+
| 1           | 1           | 2019-08-01 | 2019-08-02                  |
| 2           | 2           | 2019-08-02 | 2019-08-02                  |
| 3           | 1           | 2019-08-11 | 2019-08-12                  |
| 4           | 3           | 2019-08-24 | 2019-08-24                  |
| 5           | 3           | 2019-08-21 | 2019-08-22                  |
| 6           | 2           | 2019-08-11 | 2019-08-13                  |
| 7           | 4           | 2019-08-09 | 2019-08-09                  |
+-------------+-------------+------------+-----------------------------+

出力例

+----------------------+
| immediate_percentage |
+----------------------+
| 50.00                |
+----------------------+

制約条件

解法戦略

  1. グループ化: customer_id でグループ化
  2. 最小値抽出: 各グループ内で order_date が最小の行を特定(ROW_NUMBER または idxmin)
  3. 条件判定: order_date = customer_pref_delivery_date かどうか
  4. 集計: 即日配達の件数 ÷ 総顧客数 × 100
  5. 丸め: ROUND(..., 2) で小数点2桁

主要ポイント

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

実装コード

PostgreSQL 16.6+ 実装

WITH first_orders AS (
  SELECT
    customer_id,
    order_date,
    customer_pref_delivery_date,
    ROW_NUMBER() OVER (
      PARTITION BY customer_id
      ORDER BY order_date
    ) AS rn
  FROM Delivery
)
SELECT
  ROUND(
    100.0 * SUM(CASE WHEN order_date = customer_pref_delivery_date THEN 1 ELSE 0 END)
    / COUNT(*),
    2
  ) AS immediate_percentage
FROM first_orders
WHERE rn = 1;

Python (Pandas 2.2.2) 実装

import pandas as pd

def immediate_food_delivery(delivery: pd.DataFrame) -> pd.DataFrame:
    """
    各顧客の最初の注文における即日配達の割合を計算

    Args:
        delivery: 配達情報 (delivery_id, customer_id, order_date, customer_pref_delivery_date)

    Returns:
        pd.DataFrame: 列名は ['immediate_percentage']、1行のみ
    """
    # 各顧客の最初の注文(order_dateが最小)のインデックスを取得
    first_order_idx = delivery.groupby('customer_id')['order_date'].idxmin()

    # 最初の注文のみを抽出
    first_orders = delivery.loc[first_order_idx, ['order_date', 'customer_pref_delivery_date']]

    # 即日配達判定(order_date == customer_pref_delivery_date)
    is_immediate = (first_orders['order_date'] == first_orders['customer_pref_delivery_date'])

    # 割合を計算(パーセンテージ、小数点2桁)
    percentage = round(100.0 * is_immediate.sum() / len(is_immediate), 2)

    return pd.DataFrame({'immediate_percentage': [percentage]})

処理フローチャート

開始 Deliveryテーブル読込 全配達記録 N行 customer_idでグループ化 PARTITION BY customer_id ROW_NUMBER適用 ORDER BY order_date(昇順) rn = 1 でフィルタ 各顧客の最初の注文のみ抽出 order_date = pref_date? はい 即日配達 カウント+1 いいえ 予約配達 スキップ 割合計算 & 丸め ROUND(100 × 即日/総数, 2) 終了

フローの説明:

1. 入力: Deliveryテーブルから全配達記録を読み込み
2. グループ化: customer_idでグループを作成(PARTITION BY)
3. 順位付け: 各グループ内でorder_dateの昇順にROW_NUMBERを付与
4. 抽出: rn = 1(最初の注文)のみをフィルタ
5. 条件判定: order_date = customer_pref_delivery_date か確認
6a. はい → 即日配達カウントに加算
6b. いいえ → 予約配達としてスキップ
7. 集計: 即日配達の件数を総数で割って100倍
8. 丸め: ROUND(..., 2) で小数点2桁に丸めて出力

計算量分析

項目 本実装(Window Function) 代替案(Subquery)
時間計算量 O(N log N) O(N × 顧客数)
空間計算量 O(顧客数) O(N)
データベーススキャン 1回 顧客数分
実装の簡潔さ ★★★★★ ★★★☆☆
インデックス活用 効率的 非効率

詳細説明

時間計算量: O(N log N)

  • ROW_NUMBER(): 各グループ内でソートが必要 → O(N log N)
  • フィルタリング(rn = 1): O(N)
  • 集計(SUM, COUNT): O(顧客数)
  • 支配項: O(N log N)

空間計算量: O(顧客数)

  • CTEで最初の注文のみを保持(顧客数分の行)
  • インデックスがあれば更に効率化
  • Pandasの場合: idxmin()で顧客数分のインデックス配列

最適化のポイント

  • インデックス: (customer_id, order_date) に複合インデックス
  • DISTINCT ON: PostgreSQL特有の構文で更に簡潔に記述可能
  • Pandas idxmin(): rank()より効率的(全行にランク値を保持しない)
  • 並列処理: 大規模データではパーティション並列化が有効