アルゴリズム概要

問題: Products テーブルには製品の価格変更履歴が記録されています。 すべての製品は初期価格 10 でスタートし、change_date に新しい価格 new_price に変更されます。 2019-08-16 時点での全製品の価格を求めてください。

入力例

Products table:
+------------+-----------+-------------+
| product_id | new_price | change_date |
+------------+-----------+-------------+
| 1          | 20        | 2019-08-14  |
| 2          | 50        | 2019-08-14  |
| 1          | 30        | 2019-08-15  |
| 1          | 35        | 2019-08-16  |
| 2          | 65        | 2019-08-17  |
| 3          | 20        | 2019-08-18  |
+------------+-----------+-------------+

出力例

+------------+-------+
| product_id | price |
+------------+-------+
| 1          | 35    |
| 2          | 50    |
| 3          | 10    |
+------------+-------+

解法の戦略

  • ステップ1: 対象日 (2019-08-16) 以前のデータのみにフィルタ
  • ステップ2: groupby('product_id')['change_date'].idxmax() で各製品の最新変更日のインデックスを取得
  • ステップ3: 全製品リストを生成(重複削除)
  • ステップ4: map() で高速結合
  • ステップ5: fillna(10) で価格変更履歴がない製品にデフォルト値を設定

主要ポイント

  • 時間計算量: O(N) - Nは全レコード数
  • 空間計算量: O(M) - Mはユニーク製品数
  • 最適化手法: idxmax() によるインデックスベースの抽出で、ソート不要
  • 高速結合: map()merge() より高速(単一キー時)

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

Python実装

import pandas as pd

def price_at_given_date(products: pd.DataFrame) -> pd.DataFrame:
    """
    2019-08-16時点での全製品の価格を算出

    Parameters
    ----------
    products : pd.DataFrame
        Columns: product_id, new_price, change_date

    Returns
    -------
    pd.DataFrame
        Columns: product_id, price
    """

    # --- 対象日以前のデータのみ抽出
    target_date = '2019-08-16'
    before_target = products[products['change_date'] <= target_date]

    # --- 各製品の最新価格を取得(groupby + idxmax)
    if not before_target.empty:
        latest_idx = before_target.groupby('product_id')['change_date'].idxmax()
        latest_prices = before_target.loc[latest_idx, ['product_id', 'new_price']]
    else:
        latest_prices = pd.DataFrame(columns=['product_id', 'new_price'])

    # --- 全製品リストを生成
    all_products = products[['product_id']].drop_duplicates()

    # --- 軽量結合(map優先)
    price_mapper = latest_prices.set_index('product_id')['new_price']

    out = pd.DataFrame({
        'product_id': all_products['product_id'],
        'price': all_products['product_id'].map(price_mapper).fillna(10).astype(int)
    })

    return out


# テストデータ
products = pd.DataFrame({
    'product_id': [1, 2, 1, 1, 2, 3],
    'new_price': [20, 50, 30, 35, 65, 20],
    'change_date': pd.to_datetime([
        '2019-08-14', '2019-08-14', '2019-08-15',
        '2019-08-16', '2019-08-17', '2019-08-18'
    ])
})

result = price_at_given_date(products)
print(result)

# 出力:
#    product_id  price
# 0           1     35
# 1           2     50
# 2           3     10

フローチャート

開始 入力読み込み products DataFrame 対象日フィルタ change_date <= 2019-08-16 データあり? empty check はい いいえ groupby + idxmax 各製品の最新日付 インデックスを取得 latest_idx loc で行抽出 latest_prices 空DataFrame 作成 latest_prices = empty 全製品リスト生成 drop_duplicates() map 結合 set_index + map fillna(10) デフォルト価格設定 終了

フローの説明:
1. 入力読み込み: products DataFrame を受け取る
2. 対象日フィルタ: change_date <= 2019-08-16 の条件でフィルタ
3. データ存在確認: フィルタ後のデータが空でないかチェック
4a. はい: groupby + idxmax で各製品の最新日付のインデックスを取得 → loc で行抽出
4b. いいえ: 空の latest_prices DataFrame を作成
5. 全製品リスト生成: 元データから product_id をユニーク化
6. map結合: set_index で辞書化し、map() で高速マッピング
7. fillna(10): 価格変更履歴がない製品にデフォルト値 10 を設定
8. 終了: 結果を返却

計算量分析

処理 計算量 備考
フィルタ O(N) ブール索引で全行をスキャン
groupby + idxmax O(N) ハッシュテーブル構築 + 各グループで最大値探索
loc 抽出 O(M) M = ユニーク製品数、インデックスベースで高速
drop_duplicates O(N) ハッシュセットで重複削除
map O(M) 辞書ルックアップ、merge より高速
合計 O(N) N = 全レコード数

代替手法との比較

手法 時間 空間 メリット
本実装(idxmax) O(N) O(M) ソート不要、最速
sort + first() O(N log N) O(N) 直感的だが遅い
merge ベース O(N) O(N) メモリ消費大

最適化のポイント

  • idxmax() の優位性: ソートせずに各グループの最大値インデックスを取得できるため、O(N log N) を回避
  • map() の高速性: 単一キーの結合では merge() より高速。辞書ルックアップ O(1) を利用
  • メモリ効率: 中間DataFrameは最小限の列のみ保持。latest_prices は M 行のみ
  • スケーラビリティ: 製品数が増えても線形時間で処理可能