groupby + idxmax による O(N) 最新価格抽出
問題: 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 |
+------------+-------+
groupby('product_id')['change_date'].idxmax()
で各製品の最新変更日のインデックスを取得
map() で高速結合
fillna(10)
で価格変更履歴がない製品にデフォルト値を設定
O(N)
- Nは全レコード数
O(M)
- Mはユニーク製品数
idxmax() によるインデックスベースの抽出で、ソート不要
map() は
merge() より高速(単一キー時)
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
フローの説明:
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) | メモリ消費大 |