Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "50632a29",
"metadata": {},
"source": [
"## 0) 前提\n",
"\n",
"* 環境: **Python 3.10.15 / pandas 2.2.2**\n",
"* **指定シグネチャ厳守**\n",
"* I/O 禁止、`print` / `sort_values` 使用禁止\n",
"* 判定は `product_id` 基準、出力列・順序は `['product_id', 'product_name']`\n",
"\n",
"---\n",
"\n",
"## 1) 問題\n",
"\n",
"* `{{PROBLEM_STATEMENT}}`\n",
" 2019-01-01〜2019-03-31(2019 年 Q1)の期間にのみ販売された商品を求める。\n",
" 具体的には、各 `product_id` について:\n",
"\n",
" * Q1 期間に 1 回以上販売されている\n",
" * かつ Q1 以外の期間では 1 回も販売されていない\n",
"\n",
" ものを抽出する。\n",
"\n",
"* 入力 DF: `{{INPUT_DATAFRAMES}}`\n",
"\n",
" ```text\n",
" Product : 列 ['product_id', 'product_name', 'unit_price']\n",
" Sales : 列 ['seller_id', 'product_id', 'buyer_id', 'sale_date', 'quantity', 'price']\n",
" ```\n",
"\n",
"* 出力: `{{OUTPUT_COLUMNS_AND_RULES}}`\n",
"\n",
" * 列: `['product_id', 'product_name']`\n",
" * 行: 上記条件を満たす `product_id` のみ\n",
"\n",
"---\n",
"\n",
"## 2) 実装(指定シグネチャ厳守)\n",
"\n",
"> 列最小化 → `groupby` で min/max 日付集約 → 条件抽出 → `isin` でセミジョイン、という素直なパターンで実装します。\n",
"\n",
"```python\n",
"import pandas as pd\n",
"\n",
"def sales_analysis(product: pd.DataFrame, sales: pd.DataFrame) -> pd.DataFrame:\n",
" \"\"\"\n",
" Returns:\n",
" pd.DataFrame: 列名と順序は ['product_id', 'product_name']\n",
" \"\"\"\n",
" # 1) 列最小化(この問題で必要なのは product_id と sale_date のみ)\n",
" s = sales[['product_id', 'sale_date']]\n",
"\n",
" # 2) product_id ごとに最小日付・最大日付を集約\n",
" agg = (\n",
" s.groupby('product_id', as_index=False)['sale_date']\n",
" .agg(min_date='min', max_date='max')\n",
" )\n",
"\n",
" # 3) 「すべての販売日が 2019Q1 に収まっている」product_id を抽出\n",
" q1_start = pd.Timestamp('2019-01-01')\n",
" q1_end = pd.Timestamp('2019-03-31')\n",
"\n",
" mask = (agg['min_date'] >= q1_start) & (agg['max_date'] <= q1_end)\n",
" q1_only_ids = agg.loc[mask, 'product_id']\n",
"\n",
" # 4) Product から該当 product_id のみセミジョイン\n",
" out = product.loc[product['product_id'].isin(q1_only_ids), ['product_id', 'product_name']]\n",
"\n",
" return out\n",
"\n",
"Analyze Complexity\n",
"Runtime 351 ms\n",
"Beats 79.45%\n",
"Memory 69.55 MB\n",
"Beats 81.99%\n",
"\n",
"```\n",
"\n",
"* `sort_values` は一切使用せず、`groupby` 集約と `isin` のみで完結させています。\n",
"* 出力列の順序も指定どおり `['product_id', 'product_name']` に固定しています。\n",
"\n",
"---\n",
"\n",
"## 3) アルゴリズム説明\n",
"\n",
"使用 API と流れを整理します。\n",
"\n",
"1. **列最小化**:\n",
"\n",
" ```python\n",
" s = sales[['product_id', 'sale_date']]\n",
" ```\n",
"\n",
" * 後続処理に不要な列(seller_id, buyer_id, quantity, price)はここで捨ててメモリと処理コストを削減。\n",
"\n",
"2. **グループ処理(最小/最大日付の集約)**:\n",
"\n",
" ```python\n",
" agg = (\n",
" s.groupby('product_id', as_index=False)['sale_date']\n",
" .agg(min_date='min', max_date='max')\n",
" )\n",
" ```\n",
"\n",
" * 各 `product_id` について\n",
"\n",
" * `min_date`: その商品の最も古い販売日\n",
" * `max_date`: その商品の最も新しい販売日\n",
" * 「すべての販売日が Q1 にある」ことは\n",
" `min_date >= '2019-01-01'` かつ `max_date <= '2019-03-31'`\n",
" と同値になるので、`BOOL_OR` などのフラグ集計よりもシンプルです。\n",
"\n",
"3. **条件抽出**:\n",
"\n",
" ```python\n",
" q1_start = pd.Timestamp('2019-01-01')\n",
" q1_end = pd.Timestamp('2019-03-31')\n",
"\n",
" mask = (agg['min_date'] >= q1_start) & (agg['max_date'] <= q1_end)\n",
" q1_only_ids = agg.loc[mask, 'product_id']\n",
" ```\n",
"\n",
" * `mask` で「Q1 だけで売れている」product_id を絞り込み、その ID シリーズを取得。\n",
"\n",
"4. **軽量セミジョイン (`isin`)**:\n",
"\n",
" ```python\n",
" out = product.loc[product['product_id'].isin(q1_only_ids),\n",
" ['product_id', 'product_name']]\n",
" ```\n",
"\n",
" * 単一キー → 行のフィルタには `merge` よりも `isin` が軽くて読みやすいパターン。\n",
" * ここで Product 側から必要な列だけを投影して最終結果にしている。\n",
"\n",
"---\n",
"\n",
"### NULL / 重複 / 型の扱い\n",
"\n",
"* `sale_date` が NULL の行があった場合\n",
"\n",
" * `min` / `max` はデフォルトで非 NULL の値だけを対象にするため、NULL の存在で壊れにくい設計。\n",
" * 全て NULL の場合は `min_date`, `max_date` が NULL となり、Q1 条件に該当しなくなる(妥当)。\n",
"\n",
"* 重複行(同じ `product_id`・`sale_date` の行が複数)\n",
"\n",
" * `groupby` 集約では重複があっても `min_date` / `max_date` の値は変わらないため、追加コストは O(重複数) だけでロジックは影響なし。\n",
"\n",
"* 型\n",
"\n",
" * LeetCode 想定では `sale_date` はすでに `datetime64[ns]` 相当の型で渡される想定。\n",
" * 明示的に `pd.Timestamp` を使って比較することで、文字列比較ではなく日時比較で安全に評価している。\n",
"\n",
"---\n",
"\n",
"## 4) 計算量(概算)\n",
"\n",
"`N = Sales の行数`, `P = Product の行数`, `G = 異なる product_id の個数` とします。\n",
"\n",
"1. `groupby('product_id').agg(min, max)`\n",
"\n",
" * ハッシュグループ化想定で **O(N)**〜**O(N log G)** 近辺\n",
" * `sale_date` の min/max 計算は 1 行あたり O(1)\n",
"\n",
"2. `isin` によるセミジョイン\n",
"\n",
" * `q1_only_ids` の長さを `K` とすると\n",
"\n",
" * ハッシュセット化で **O(K)**\n",
" * Product 側の走査で **O(P)**\n",
" * 合計は **O(P + K)** 程度(`K <= G <= N`)\n",
"\n",
"トータルで **O(N + P)** 近似の線形オーダーに収まり、\n",
"`sort_values` や複雑な rank を使わない分、実装もパフォーマンスもバランスが良い構成です。\n",
"\n",
"---\n",
"\n",
"## 5) 図解(Mermaid 超保守版)\n",
"\n",
"```mermaid\n",
"flowchart TD\n",
" A[入力 Product DF]\n",
" B[入力 Sales DF]\n",
" C[Sales から product_id と sale_date のみ抽出]\n",
" D[product_id ごとに min と max の sale_date を集約]\n",
" E[集約結果から 2019Q1 だけで売れている product_id を抽出]\n",
" F[Product DF から該当 product_id のみフィルタ]\n",
" G[出力 product_id と product_name]\n",
" B --> C\n",
" C --> D\n",
" D --> E\n",
" A --> F\n",
" E --> F\n",
" F --> G\n",
"```\n",
"\n",
"この実装は、PostgreSQL 版で行った「min/max 日付で Q1 に収まるか判定」という戦略を、そのまま Pandas 向けに最適化した形になっています。\n",
"\n",
"結論から言うと、そのスコアならかなり仕上がっています。\n",
"アルゴリズム自体はほぼ最適クラスなので、残りは「微妙なオーバーヘッドを減らすかどうか」の世界です。\n",
"\n",
"それでもあえていじるとしたら、このくらいです。\n",
"\n",
"---\n",
"\n",
"## 1) ほぼ最終形のコード例\n",
"\n",
"* 中間の `s` をやめて `groupby` を直にかける\n",
"* マスク計算時に `Series` ではなく `ndarray` を使って、インデックスアラインのオーバーヘッドを削る\n",
"\n",
"```python\n",
"import pandas as pd\n",
"\n",
"def sales_analysis(product: pd.DataFrame, sales: pd.DataFrame) -> pd.DataFrame:\n",
" \"\"\"\n",
" Returns:\n",
" pd.DataFrame: 列名と順序は ['product_id', 'product_name']\n",
" \"\"\"\n",
" # 1) product_id ごとに最小日付・最大日付を集約(列も最小限)\n",
" agg = (\n",
" sales.groupby('product_id', as_index=False)['sale_date']\n",
" .agg(min_date='min', max_date='max')\n",
" )\n",
"\n",
" # 2) 「すべての販売日が 2019Q1 に収まっている」product_id を抽出\n",
" q1_start = pd.Timestamp('2019-01-01')\n",
" q1_end = pd.Timestamp('2019-03-31')\n",
"\n",
" min_date = agg['min_date'].values\n",
" max_date = agg['max_date'].values\n",
" mask = (min_date >= q1_start) & (max_date <= q1_end)\n",
"\n",
" q1_only_ids = agg.loc[mask, 'product_id']\n",
"\n",
" # 3) Product から該当 product_id のみセミジョイン\n",
" out = product.loc[\n",
" product['product_id'].isin(q1_only_ids),\n",
" ['product_id', 'product_name']\n",
" ]\n",
"\n",
" return out\n",
"\n",
"Analyze Complexity\n",
"Runtime 331 ms\n",
"Beats 94.28%\n",
"Memory 69.50 MB\n",
"Beats 81.99%\n",
"\n",
"```\n",
"\n",
"### 微調整ポイント\n",
"\n",
"* `sales[['product_id', 'sale_date']]` という別変数を作らず、そのまま groupby している\n",
" → ほんの少しだけ一時オブジェクトが減ります(効果はごく小さいですが無駄はない)。\n",
"* `mask` 計算を `Series` 同士ではなく `ndarray` 同士にしている\n",
" → インデックス合わせなどの内部処理がいらなくなる分、わずかに軽くなり得ます。\n",
"\n",
"---\n",
"\n",
"## 2) これ以上やるなら「可読性とのトレードオフ」\n",
"\n",
"アルゴリズムは\n",
"\n",
"* 1 回の `groupby` で min/max を取る\n",
"* `isin` で Product とセミジョイン\n",
"\n",
"という構造で、計算量的にも実質 O(N) に近い形です。\n",
"\n",
"これ以上の改善は\n",
"\n",
"* dtype を事前に `datetime64[ns]` に統一しておく\n",
"* 変数名・ローカル変数の数をさらに削る\n",
"\n",
"といったレベルになり、可読性とトレードオフになる割に LeetCode の ms 単位ではほぼ誤差です。\n",
"\n",
"今の 351 ms / 79%・メモリ 69.55MB / 81% なら、\n",
"「アルゴリズムも実装も合格点、余力があれば micro-tuning で遊べる状態」と見てよいと思います。\n",
"\n"
]
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Loading