diff --git a/.agent/workflows/svg_flowchart_guidelines.md b/.agent/workflows/svg_flowchart_guidelines.md new file mode 100644 index 00000000..08e94055 --- /dev/null +++ b/.agent/workflows/svg_flowchart_guidelines.md @@ -0,0 +1,52 @@ +--- +description: SVG flowchart and data visualization best practices to avoid common rendering issues +--- + +# SVG Flowchart & Data Visualization Guidelines + +## Arrow Marker Best Practices + +1. **`refX` should be less than the arrowhead length.** If your arrowhead path is `M0,0 L0,6 L9,3 z` (length=9), set `refX` to `5` (not `9`). A `refX` equal to the arrowhead length causes the tip to land exactly at the path endpoint, which gets hidden behind destination nodes. + +2. **End arrow paths 5-10px before the target node boundary.** This leaves room for the arrowhead to be visible and not obscured by the node's fill. + +3. **Use `markerUnits="strokeWidth"`** for consistent arrowhead sizing regardless of stroke width changes. + +## SVG viewBox Sizing + +1. **Always add 30-50px padding** below the last element in your viewBox height. If the last element ends at y=940 with ry=35, set viewBox height to at least `1000`. + +2. **For dynamic data tables**, calculate viewBox height based on number of rows: + + ``` + viewBox height = header_height + (row_count × row_height) + summary_text_spacing + padding + ``` + +## Text Overlap Prevention + +1. **Summary text below data tables**: Position summary text at least `30px` below the last row's bottom edge (y + height), not at a fixed y coordinate. + +2. **For N data rows at spacing S starting at Y0**: Last row bottom = `Y0 + (N-1) × S + row_height`. Summary text y should be `last_row_bottom + 30`. + +## Prism.js Copy Button with Tailwind CSS + +Tailwind's preflight CSS resets button styles. Override with `!important`: + +```css +.code-toolbar > .toolbar { + opacity: 1 !important; +} +.code-toolbar > .toolbar .toolbar-item { + display: inline-block !important; +} +.code-toolbar > .toolbar button, +.code-toolbar > .toolbar a, +.code-toolbar > .toolbar span { + background: #10b981 !important; + color: white !important; + border: none !important; + display: inline-block !important; + opacity: 1 !important; + visibility: visible !important; +} +``` diff --git a/SQL/Leetcode/Intermediate Join/1174. Immediate Food Delivery II/Claude Sonnet 4.5 Extended/Immediate_Food_Delivery_II.html b/SQL/Leetcode/Intermediate Join/1174. Immediate Food Delivery II/Claude Sonnet 4.5 Extended/Immediate_Food_Delivery_II.html new file mode 100644 index 00000000..945da176 --- /dev/null +++ b/SQL/Leetcode/Intermediate Join/1174. Immediate Food Delivery II/Claude Sonnet 4.5 Extended/Immediate_Food_Delivery_II.html @@ -0,0 +1,2455 @@ + + + + + + LeetCode 1174: Immediate Food Delivery II - グループ内最小値抽出 + + + + + + + + + + + + + + + + + +
+ + + + +
+

+ アルゴリズム概要 +

+ +

問題の説明

+

+ 食品配達サービスにおいて、各顧客の最初の注文が即日配達(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. +
  3. + 最小値抽出: 各グループ内で order_date + が最小の行を特定(ROW_NUMBER または idxmin) +
  4. +
  5. + 条件判定: order_date = customer_pref_delivery_date + かどうか +
  6. +
  7. 集計: 即日配達の件数 ÷ 総顧客数 × 100
  8. +
  9. 丸め: ROUND(..., 2) で小数点2桁
  10. +
+
+ +

主要ポイント

+ +
+ + +
+

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

+
+
+ + +
+

+ 実装コード +

+ +

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()より効率的(全行にランク値を保持しない) +
  • +
  • + 並列処理: 大規模データではパーティション並列化が有効 +
  • +
+
+
+
+ + + + + + + + + + + + + + + + + + + + diff --git a/SQL/Leetcode/Intermediate Join/1174. Immediate Food Delivery II/Claude Sonnet 4.5 Extended/Immediate_Food_Delivery_II_pandas.md b/SQL/Leetcode/Intermediate Join/1174. Immediate Food Delivery II/Claude Sonnet 4.5 Extended/Immediate_Food_Delivery_II_pandas.md new file mode 100644 index 00000000..8694f38b --- /dev/null +++ b/SQL/Leetcode/Intermediate Join/1174. Immediate Food Delivery II/Claude Sonnet 4.5 Extended/Immediate_Food_Delivery_II_pandas.md @@ -0,0 +1,179 @@ +# Pandas 2.2.2用 + +## 0) 前提 + +- 環境: **Python 3.10.15 / pandas 2.2.2** +- **指定シグネチャ厳守**(関数名・引数名・返却列・順序) +- I/O 禁止、不要な `print` や `sort_values` 禁止 + +## 1) 問題 + +- **各顧客の最初の注文(order_dateが最も早い注文)における「即日配達」の割合を求める** + - 即日配達(immediate): `order_date == customer_pref_delivery_date` + - 予約配達(scheduled): `order_date < customer_pref_delivery_date` + - 結果は小数点2桁のパーセンテージ +- 入力 DF: `delivery` (delivery_id, customer_id, order_date, customer_pref_delivery_date) +- 出力: `immediate_percentage` (float, 小数点2桁) + +## 2) 実装(指定シグネチャ厳守) + +> 原則は **groupby.idxmin で最初の注文を特定 → 条件判定 → 集計**。`rank` より `idxmin` が効率的。 + +```python +# Analyze Complexity +# Runtime 306 ms +# Beats 88.58% +# Memory 67.94 MB +# Beats 68.54% + +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桁) + if is_immediate.empty: + percentage = 0.0 + else: + percentage = round(100.0 * is_immediate.sum() / len(is_immediate), 2) + + return pd.DataFrame({'immediate_percentage': [percentage]}) +``` + +### 代替案(rank使用) + +```python +# Analyze Complexity +# Runtime 321 ms +# Beats 70.23% +# Memory 68.14 MB +# Beats 52.06% +def immediate_food_delivery(delivery: pd.DataFrame) -> pd.DataFrame: + # 各顧客内でorder_dateの昇順ランク付け(元のDataFrameを変更しない) + delivery_with_rank = delivery.assign( + rn=delivery.groupby('customer_id')['order_date'].rank(method='first', ascending=True) + ) + + # 最初の注文のみ抽出 + first_orders = delivery_with_rank.loc[delivery_with_rank['rn'] == 1] + + # 即日配達判定と集計 + is_immediate = (first_orders['order_date'] == first_orders['customer_pref_delivery_date']) + if is_immediate.empty: + percentage = 0.0 + else: + percentage = round(100.0 * is_immediate.sum() / len(is_immediate), 2) + + return pd.DataFrame({'immediate_percentage': [percentage]}) +``` + +### 代替案(transform使用) + +```python +# Analyze Complexity +# Runtime 315 ms +# Beats 78.65% +# Memory 67.42 MB +# Beats 95.88% + +def immediate_food_delivery(delivery: pd.DataFrame) -> pd.DataFrame: + # 各顧客の最小order_dateを全行に展開 + min_order_date = delivery.groupby('customer_id')['order_date'].transform('min') + + # 最初の注文のみ抽出(同じorder_dateが複数ある場合は1行のみ) + first_orders = delivery[delivery['order_date'] == min_order_date].drop_duplicates( + subset=['customer_id', 'order_date'] + ) + + # 即日配達判定と集計 + is_immediate = (first_orders['order_date'] == first_orders['customer_pref_delivery_date']) + if is_immediate.empty: + percentage = 0.0 + else: + percentage = round(100.0 * is_immediate.sum() / len(is_immediate), 2) + + return pd.DataFrame({'immediate_percentage': [percentage]}) +``` + +## 3) アルゴリズム説明 + +- **使用 API**: + - `groupby('customer_id')['order_date'].idxmin()`: 各顧客グループ内で order_date が最小の行のインデックスを取得 + - `loc[idx, cols]`: インデックス指定で行抽出、列も最小化 + - `==` による列間比較: 即日配達判定(Boolean Series) + - `sum()` / `len()`: 条件を満たす行数とトータル行数 + - `round(value, 2)`: 小数点2桁に丸め + +- **NULL / 重複 / 型**: + - `idxmin()` は NaT(欠損日付)を無視して最小値を返す + - 同一顧客で同じ order_date が複数ある場合、`idxmin()` は最初に出現する行のインデックスを返す + - 日付比較は `==` で厳密一致判定(時刻情報がある場合は注意) + +- **効率化ポイント**: + - `idxmin()` は各グループで1回の走査で最小値インデックスを取得(O(N)) + - `rank()` より `idxmin()` の方がメモリ効率が良い(全行にランク値を保持しない) + - 抽出時に必要な列のみ指定して `.loc[idx, ['col1', 'col2']]` でメモリ削減 + +## 4) 計算量(概算) + +- `groupby.idxmin()`: **O(N)** (全行を1回走査、各グループで最小値インデックスを記録) +- `loc` によるインデックス抽出: **O(顧客数)** (顧客数分の行のみ抽出) +- 列間比較 `==`: **O(顧客数)** +- 集計 `sum()` / `len()`: **O(顧客数)** +- **全体**: **O(N)** (Nは全配達記録数、支配項はgroupby処理) + +メモリ: O(顧客数) のインデックス配列と抽出後データフレーム + +## 5) 図解(Mermaid 超保守版) + +```mermaid +flowchart TD + A[Delivery DataFrame
全配達記録 N行] + B[groupby customer_id
order_date.idxmin
各顧客の最初の注文インデックス] + C[loc で抽出
顧客数分の行のみ] + D[即日判定
order_date == pref_date
Boolean Series] + E[集計
sum True / len all
100倍して round 2桁] + F[DataFrame 1行
immediate_percentage] + + A --> B + B --> C + C --> D + D --> E + E --> F +``` + +--- + +**動作検証例**: + +```python +# Example data +data = { + 'delivery_id': [1, 2, 3, 4, 5, 6, 7], + 'customer_id': [1, 2, 1, 3, 3, 2, 4], + 'order_date': pd.to_datetime(['2019-08-01', '2019-08-02', '2019-08-11', + '2019-08-24', '2019-08-21', '2019-08-11', '2019-08-09']), + 'customer_pref_delivery_date': pd.to_datetime(['2019-08-02', '2019-08-02', '2019-08-12', + '2019-08-24', '2019-08-22', '2019-08-13', '2019-08-09']) +} +delivery = pd.DataFrame(data) +result = immediate_food_delivery(delivery) +# 期待値: immediate_percentage = 50.00 +# (customer 1: scheduled, 2: immediate, 3: scheduled, 4: immediate → 2/4 = 50%) +``` diff --git a/SQL/Leetcode/Intermediate Join/1174. Immediate Food Delivery II/Claude Sonnet 4.5 Extended/Immediate_Food_Delivery_II_postgre.md b/SQL/Leetcode/Intermediate Join/1174. Immediate Food Delivery II/Claude Sonnet 4.5 Extended/Immediate_Food_Delivery_II_postgre.md new file mode 100644 index 00000000..45ebcd84 --- /dev/null +++ b/SQL/Leetcode/Intermediate Join/1174. Immediate Food Delivery II/Claude Sonnet 4.5 Extended/Immediate_Food_Delivery_II_postgre.md @@ -0,0 +1,133 @@ +# PostgreSQL 16.6+ + +## 0) 前提 + +- エンジン: **PostgreSQL 16.6+** +- 並び順: 任意 +- `NOT IN` 回避(`EXISTS` / `LEFT JOIN ... IS NULL` を推奨) +- 判定は ID 基準、表示は仕様どおり + +## 1) 問題 + +- **各顧客の最初の注文(order_dateが最も早い注文)における「即日配達」の割合を求める** + - 即日配達(immediate): `order_date = customer_pref_delivery_date` + - 予約配達(scheduled): `order_date < customer_pref_delivery_date` +- 入力: `Delivery(delivery_id, customer_id, order_date, customer_pref_delivery_date)` +- 出力: `immediate_percentage` (小数点2桁、パーセンテージ表示) + +## 2) 最適解(単一クエリ) + +PostgreSQL では **CTE + ウィンドウ関数** で各顧客の最初の注文を特定し、集計で割合を算出。 + +Runtime 369 ms +Beats 73.30% + +```sql +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 + COALESCE( + ROUND( + 100.0 * SUM(CASE WHEN order_date = customer_pref_delivery_date THEN 1 ELSE 0 END) + / COUNT(*), + 2 + ), + 0.0 + ) AS immediate_percentage +FROM first_orders +WHERE rn = 1; +``` + +### 代替案(AVG + FILTER) + +Runtime 373 ms +Beats 68.54% + +```sql +WITH first_orders AS ( + SELECT + customer_id, + order_date = customer_pref_delivery_date AS is_immediate, + ROW_NUMBER() OVER ( + PARTITION BY customer_id + ORDER BY order_date + ) AS rn + FROM Delivery +) +SELECT + ROUND( + 100.0 * COALESCE(AVG(is_immediate::int), 0.0), + 2 + ) AS immediate_percentage +FROM first_orders +WHERE rn = 1; +``` + +### 代替案(DISTINCT ON - PostgreSQL特有) + +Runtime 367 ms +Beats 76.31% + +```sql +WITH first_orders AS ( + SELECT DISTINCT ON (customer_id) + customer_id, + order_date = customer_pref_delivery_date AS is_immediate + FROM Delivery + ORDER BY customer_id, order_date +) +SELECT + COALESCE( + ROUND( + 100.0 * COUNT(*) FILTER (WHERE is_immediate) / NULLIF(COUNT(*), 0), + 2 + ), + 0.0 + ) AS immediate_percentage +FROM first_orders; +``` + +## 3) 要点解説 + +- **`ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date)`**: 各顧客ごとに注文を日付順に並べ、1番目を特定 +- **`ROUND(100.0 * ... / ..., 2)`**: パーセンテージ計算時に `100.0` で浮動小数点除算を強制し、2桁で丸め +- **`DISTINCT ON`**: PostgreSQL特有の構文で、各グループの先頭行のみを効率的に取得可能 +- **`COUNT(*) FILTER (WHERE ...)`**: PostgreSQL 9.4+の集計フィルタ構文で条件付きカウントを簡潔に記述 + +## 4) 計算量(概算) + +- ウィンドウ処理: **O(n log n)** (全行をcustomer_id + order_dateでソート) +- 集計: **O(顧客数)** (最初の注文のみをスキャン) +- インデックス `(customer_id, order_date)` があれば **Index Scan** で効率化 +- 全体: **O(n log n)** ~ **O(n)** (インデックス有り) + +## 5) 図解(Mermaid 超保守版) + +```mermaid +flowchart TD + A[Delivery テーブル
全配達記録] + B[ウィンドウ関数適用
ROW_NUMBER OVER
PARTITION BY customer_id
ORDER BY order_date] + C[rn = 1 でフィルタ
各顧客の最初の注文のみ抽出] + D[即日判定
order_date = pref_date] + E[集計と割合計算
SUM CASE / COUNT
100倍してROUND 2桁] + F[immediate_percentage
50.00] + + A --> B + B --> C + C --> D + D --> E + E --> F +``` + +--- + +**補足**: 例のデータで検証すると、customer 1と3がscheduled、customer 2と4がimmediate → 2/4 = 50.00% となり正しく算出されます。