From ca95ded1981ee1b77b4485a9fe1f41131b50b5c3 Mon Sep 17 00:00:00 2001 From: myoshizumi Date: Mon, 17 Nov 2025 18:50:36 +0900 Subject: [PATCH] SQL: Intermediate Select 1070. Product Sales Analysis III --- .../Product_Sales_Analysis_III_mysql.ipynb | 434 +++++++++++++++++ .../Product_Sales_Analysis_III_pandas.ipynb | 380 +++++++++++++++ .../Product_Sales_Analysis_III_posgres.ipynb | 456 ++++++++++++++++++ 3 files changed, 1270 insertions(+) create mode 100644 SQL/Leetcode/Intermediate Select/1070. Product Sales Analysis III/gpt 5.1 thinking customized/Product_Sales_Analysis_III_mysql.ipynb create mode 100644 SQL/Leetcode/Intermediate Select/1070. Product Sales Analysis III/gpt 5.1 thinking customized/Product_Sales_Analysis_III_pandas.ipynb create mode 100644 SQL/Leetcode/Intermediate Select/1070. Product Sales Analysis III/gpt 5.1 thinking customized/Product_Sales_Analysis_III_posgres.ipynb diff --git a/SQL/Leetcode/Intermediate Select/1070. Product Sales Analysis III/gpt 5.1 thinking customized/Product_Sales_Analysis_III_mysql.ipynb b/SQL/Leetcode/Intermediate Select/1070. Product Sales Analysis III/gpt 5.1 thinking customized/Product_Sales_Analysis_III_mysql.ipynb new file mode 100644 index 00000000..594c1aae --- /dev/null +++ b/SQL/Leetcode/Intermediate Select/1070. Product Sales Analysis III/gpt 5.1 thinking customized/Product_Sales_Analysis_III_mysql.ipynb @@ -0,0 +1,434 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "dcca9b4a", + "metadata": {}, + "source": [ + "## 0) 前提整理\n", + "\n", + "* エンジン: MySQL 8.0.40\n", + "* 並び順任意なので `ORDER BY` は付けない\n", + "* `NOT IN` は使用せず、ウィンドウ関数 / 集約 + 結合で解決\n", + "* 判定軸:\n", + "\n", + " * 「その product_id にとって最初に販売された year」\n", + " * その年に属する全ての販売行(= 行レベルでそのまま残す)\n", + "\n", + "---\n", + "\n", + "## 1) 問題の要点再整理\n", + "\n", + "**テーブル: `Sales`**\n", + "\n", + "```text\n", + "sale_id int -- (sale_id, year) が PK\n", + "product_id int\n", + "year int\n", + "quantity int\n", + "price int -- 単価\n", + "```\n", + "\n", + "**要求仕様**\n", + "\n", + "* 各 `product_id` ごとに「最初に登場した year(最小 year)」を求める\n", + "* その `product_id` について、その最小 year に属する全行を返す\n", + "\n", + " * つまり「最初の年」の「全売上レコード」を返す(集約しない)\n", + "* 出力列:\n", + "\n", + " * `product_id`\n", + " * `first_year` … 対象行の `year`\n", + " * `quantity`\n", + " * `price`\n", + "* 並び順は任意\n", + "\n", + "---\n", + "\n", + "## 2) 最適解(単一クエリ / ウィンドウ関数)\n", + "\n", + "`product_id` 毎に `year` 昇順でランクを付け、最初の年(ランク 1)の行だけを残す、という王道パターンです。\n", + "\n", + "```sql\n", + "WITH win AS (\n", + " SELECT\n", + " product_id,\n", + " year,\n", + " quantity,\n", + " price,\n", + " DENSE_RANK() OVER (\n", + " PARTITION BY product_id\n", + " ORDER BY year\n", + " ) AS rnk\n", + " FROM Sales\n", + ")\n", + "SELECT\n", + " product_id,\n", + " year AS first_year,\n", + " quantity,\n", + " price\n", + "FROM win\n", + "WHERE rnk = 1;\n", + "\n", + "Runtime 736 ms\n", + "Beats 36.82%\n", + "\n", + "```\n", + "\n", + "### ポイント\n", + "\n", + "* `PARTITION BY product_id ORDER BY year`\n", + " → 各商品ごとに販売年で並べ、`DENSE_RANK()` で\n", + " 最も早い年の行に `rnk = 1` を振る\n", + "* 「同じ product_id・同じ year で複数行」ある場合も、その年の行はすべて `rnk = 1` となる\n", + " → 仕様どおり「最初の年の全 sales 行」が残る\n", + "* `ORDER BY` 句なし → 実行計画の自由度を確保しつつ、要件も満たす\n", + "\n", + "---\n", + "\n", + "## 3) 代替解(集約 + 結合 / ウィンドウ未対応環境も意識)\n", + "\n", + "ウィンドウ関数を避ける/重いと判断されるケースでは、**最小年のみを先に集約**し、それに結合するパターンがシンプルで高速です。\n", + "\n", + "```sql\n", + "-- 各 product_id の最初の年を集約\n", + "WITH first_years AS (\n", + " SELECT\n", + " product_id,\n", + " MIN(year) AS first_year\n", + " FROM Sales\n", + " GROUP BY product_id\n", + ")\n", + "SELECT\n", + " s.product_id,\n", + " f.first_year,\n", + " s.quantity,\n", + " s.price\n", + "FROM Sales AS s\n", + "JOIN first_years AS f\n", + " ON s.product_id = f.product_id\n", + " AND s.year = f.first_year;\n", + "\n", + "Runtime 724 ms\n", + "Beats 41.50%\n", + "```\n", + "\n", + "### ポイント\n", + "\n", + "* `first_years` で `product_id` ごとの `MIN(year)` を算出\n", + "\n", + " * ここは 1 行 / product_id に縮約されるのでデータ量が大きいほど効く\n", + "* 元の `Sales` と `product_id` + `year` で内部結合\n", + "\n", + " * これにより「最初の年の sales 行」だけを抽出\n", + "* `NOT IN` などを使わず、安全に実装できる\n", + "\n", + "---\n", + "\n", + "## 4) 要点解説\n", + "\n", + "### 方針\n", + "\n", + "1. **最初の年の特定**\n", + "\n", + " * ウィンドウ版: `DENSE_RANK() OVER(PARTITION BY product_id ORDER BY year)`\n", + " * 集約版: `MIN(year)` を `GROUP BY product_id` で求める\n", + "2. **行のフィルタ**\n", + "\n", + " * ランク = 1 の行のみ残す(ウィンドウ)\n", + " * `product_id` & `year = first_year` に一致する行のみ残す(結合)\n", + "3. **必要列のみ投影**\n", + "\n", + " * `product_id`, `first_year`, `quantity`, `price` の 4 列に限定\n", + "\n", + "### NULL / 重複の扱い\n", + "\n", + "* この問題設定では `year` が NULL になる前提は通常ないが、もしあり得る場合:\n", + "\n", + " * 最初の年を「数値として最小」にしたいなら、`WHERE year IS NOT NULL` を前処理に入れる\n", + "* 同一 `product_id`・同一 `year` で複数行が存在しうる(問題文にも明記)\n", + "\n", + " * **集約しない**ことが重要\n", + " * 「最初の年の全ての売上」を出したいので、その年の行はすべて結果に含める\n", + "\n", + "### 安定性と実務的観点\n", + "\n", + "* 並び順は問われていないため `ORDER BY` を付けない方が、オプティマイザに自由度があり高速になりやすい\n", + "* 実務では:\n", + "\n", + " * `product_id, year` にインデックスがあると、どちらの解法でもかなり効く\n", + " * 利用頻度が高いならビューやマテビューで隠蔽するのも手\n", + "\n", + "---\n", + "\n", + "## 5) 計算量(概算)\n", + "\n", + "N = `Sales` の行数、P = `product_id` の種類数(パーティション数)\n", + "\n", + "### ウィンドウ関数版\n", + "\n", + "* パーティションごとにソートが入るため、概ね:\n", + "\n", + " * 時間計算量: **O(N log N)** 近辺\n", + " * 空間計算量: ソートバッファ分を含め **O(N)** 近辺\n", + "* `product_id` ごとの行数が均一なら、`O(P * (N/P log(N/P))) ≒ O(N log(N/P))`\n", + "\n", + "### 集約 + 結合版\n", + "\n", + "* `GROUP BY product_id` での集約: **O(N log N)**(インデックス次第で軽減)\n", + "* その結果(P 行)との結合:\n", + "\n", + " * インデックスがあれば概ね **O(N)** 近似\n", + "* いずれもオーダー的にはウィンドウ版と同程度だが、\n", + "\n", + " * 「first_years がかなり小さい」+「結合キーに良インデックスあり」\n", + " の場合、集約版が有利になるケースもある\n", + "\n", + "---\n", + "\n", + "## 6) 図解(Mermaid 超保守版)\n", + "\n", + "```mermaid\n", + "flowchart TD\n", + " A[入力 Sales テーブル]\n", + " B[集約 各商品ごとの最初の年を算出]\n", + " C[元の Sales と集約結果を結合]\n", + " D[最初の年の行だけが残る]\n", + " E[出力 product_id first_year quantity price]\n", + " A --> B\n", + " A --> C\n", + " B --> C\n", + " C --> D\n", + " D --> E\n", + "```\n", + "\n", + "---\n", + "\n", + "どちらも MySQL 8.0.40 で素直に動きますが、\n", + "\n", + "* クエリの読みやすさ重視 → **集約 + 結合版**\n", + "* ウィンドウ関数で揃えたい / 他の順位ロジックも追加予定 → **ウィンドウ関数版**\n", + "\n", + "という形で使い分けると扱いやすいと思います。\n", + "\n", + "結論から言うと、**どちらのクエリもロジックとしてはほぼ最適クラス**で、LeetCode のスコアを劇的に押し上げるような「アルゴリズム改善」は正直ほとんどありません。\n", + "そのうえで、\n", + "\n", + "* 「書き方を少しシンプルにする案」\n", + "* 「理論的には速くなり得るパターン」\n", + "* 「実務でのチューニングポイント」\n", + "\n", + "という観点で整理します。\n", + "\n", + "---\n", + "\n", + "## 1. 現状 2 クエリの評価\n", + "\n", + "### ① ウィンドウ関数版\n", + "\n", + "```sql\n", + "WITH win AS (\n", + " SELECT\n", + " product_id,\n", + " year,\n", + " quantity,\n", + " price,\n", + " DENSE_RANK() OVER (\n", + " PARTITION BY product_id\n", + " ORDER BY year\n", + " ) AS rnk\n", + " FROM Sales\n", + ")\n", + "SELECT\n", + " product_id,\n", + " year AS first_year,\n", + " quantity,\n", + " price\n", + "FROM win\n", + "WHERE rnk = 1;\n", + "\n", + "Runtime 660 ms\n", + "Beats 78.09%\n", + "\n", + "```\n", + "\n", + "* 計算量: パーティションごとのソートが入るので概ね **O(N log N)**\n", + "* 「最初の年の全行」を出すという要件に対しては非常に素直で可読性も高い\n", + "* LeetCode の 736ms / 36.82% は、プラットフォームのノイズもあるので「十分許容範囲」です\n", + "\n", + "### ② 集約 + 結合版\n", + "\n", + "```sql\n", + "WITH first_years AS (\n", + " SELECT\n", + " product_id,\n", + " MIN(year) AS first_year\n", + " FROM Sales\n", + " GROUP BY product_id\n", + ")\n", + "SELECT\n", + " s.product_id,\n", + " f.first_year,\n", + " s.quantity,\n", + " s.price\n", + "FROM Sales AS s\n", + "JOIN first_years AS f\n", + " ON s.product_id = f.product_id\n", + " AND s.year = f.first_year;\n", + "\n", + "Runtime 636 ms\n", + "Beats 91.96%\n", + "\n", + "```\n", + "\n", + "* 計算量: `GROUP BY` が **O(N log N)**、結合が **O(N)** 近似 → 全体としても **O(N log N)** クラス\n", + "* ロジックは非常にオーソドックスで、実務でもこの書き方をよく使います\n", + "* Runtime 724ms / 41.50% なので、ウィンドウ版よりややマシ、という感じですが誤差レベルです\n", + "\n", + "→ どちらも「ちゃんとした解法」で、スコアも全く悪くありません。\n", + "\n", + "---\n", + "\n", + "## 2. もう少しシンプルに書く「第3案」(相関サブクエリ)\n", + "\n", + "LeetCode や面接的には、**相関サブクエリで `MIN(year)` をそのまま使うパターン**もよく出てきます。\n", + "MySQL のオプティマイザ次第ですが、場合によってはこれが一番速く出るケースもあります。\n", + "\n", + "```sql\n", + "SELECT\n", + " s.product_id,\n", + " s.year AS first_year,\n", + " s.quantity,\n", + " s.price\n", + "FROM Sales AS s\n", + "WHERE s.year = (\n", + " SELECT MIN(year)\n", + " FROM Sales\n", + " WHERE product_id = s.product_id\n", + ");\n", + "\n", + "Time Limit Exceeded\n", + "5 / 10 testcases passed\n", + "```\n", + "\n", + "### 特徴\n", + "\n", + "* ロジックは非常に明快\n", + "\n", + " * 「自分の product_id の中で最小 year と同じ年の行だけ残す」\n", + "* `NOT IN` は使っていないので、NULL 罠もなし\n", + "* インデックスが `product_id, year` に張られている場合は、\n", + " 内側の `MIN(year)` がかなり効きやすく、実行計画によっては他案より有利になることもあります\n", + "\n", + "LeetCode 環境ではインデックスをいじれないため「必ず速くなる」とまでは言えませんが、\n", + "**SQL の書き方としてはコンパクトで、十分「最適解候補」**です。\n", + "\n", + "---\n", + "\n", + "## 3. ウィンドウ版をあえてチューニングするなら\n", + "\n", + "ウィンドウにかける行数を減らす構成も一応あります。\n", + "\n", + "### 3.1 product_id × year で一度ユニーク化してからウィンドウ\n", + "\n", + "もし「同じ product_id・同じ year の行が大量にある」ようなデータなら、\n", + "ウィンドウをかける手前でユニーク化してから join し直す手もあります。\n", + "\n", + "```sql\n", + "WITH year_level AS (\n", + " SELECT DISTINCT\n", + " product_id,\n", + " year\n", + " FROM Sales\n", + "),\n", + "win AS (\n", + " SELECT\n", + " product_id,\n", + " year,\n", + " DENSE_RANK() OVER (\n", + " PARTITION BY product_id\n", + " ORDER BY year\n", + " ) AS rnk\n", + " FROM year_level\n", + ")\n", + "SELECT\n", + " s.product_id,\n", + " w.year AS first_year,\n", + " s.quantity,\n", + " s.price\n", + "FROM Sales AS s\n", + "JOIN win AS w\n", + " ON s.product_id = w.product_id\n", + " AND s.year = w.year\n", + "WHERE w.rnk = 1;\n", + "\n", + "Runtime 760 ms\n", + "Beats 29.46%\n", + "\n", + "```\n", + "\n", + "* **年レベルでの distinct 行数 << Sales 全行数** なら、\n", + " ウィンドウをかける対象が減るので、理論上は多少有利になります\n", + "* ただし、LeetCode のテストデータ規模では\n", + "\n", + " * `DISTINCT` のコスト\n", + " * 追加の `JOIN` コスト\n", + " が上回る可能性もあり、必ずしも速くなるとは限りません\n", + "\n", + "---\n", + "\n", + "## 4. 実務でのチューニング観点(LeetCode では変更不可ですが)\n", + "\n", + "LeetCode ではインデックス定義は触れませんが、**現場ならまずインデックス設計を見直す**箇所です。\n", + "\n", + "1. 典型的には以下のようなインデックスを検討します\n", + "\n", + " ```sql\n", + " CREATE INDEX idx_sales_product_year\n", + " ON Sales (product_id, year);\n", + " ```\n", + "\n", + " これにより\n", + "\n", + " * `GROUP BY product_id` + `MIN(year)`\n", + " * `WHERE product_id = ... ORDER BY year LIMIT 1`\n", + " * 相関サブクエリの `MIN(year)`\n", + "\n", + " がすべて「product_id ごとの year の先頭」だけを見にいけるようになります。\n", + "\n", + "2. ウィンドウ関数版でも、`PARTITION BY product_id ORDER BY year` が\n", + " ちょうどそのインデックス順と一致するため、ソートがかなり軽くなります。\n", + "\n", + "---\n", + "\n", + "## 5. 結局「改善の余地」はあるか?\n", + "\n", + "整理すると:\n", + "\n", + "* アルゴリズムクラスとしては\n", + "\n", + " * 既存 2 クエリも\n", + " * 第3案(相関サブクエリ)も\n", + " いずれも **O(N log N) の妥当な解法**で、これ以上劇的に下げるのは難しい\n", + "* LeetCode の Runtime パーセンタイルは\n", + "\n", + " * サーバ負荷や他ユーザの同時実行状況で ±100ms くらい平気でブレる\n", + " * 36〜41% というのは「十分許容で、改善必須とは言えないレベル」\n", + "* その中でやるとすれば\n", + "\n", + " * 相関サブクエリ版を一度試してみる\n", + " * ウィンドウ版を「distinct year → join 戻し」で組み直してみる\n", + " 程度の「微調整」です\n", + "\n", + "なので、**大きな改善余地はほぼないが、書き方を変えた第3案を試す価値はある**、くらいの温度感が現実的だと思います。" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/SQL/Leetcode/Intermediate Select/1070. Product Sales Analysis III/gpt 5.1 thinking customized/Product_Sales_Analysis_III_pandas.ipynb b/SQL/Leetcode/Intermediate Select/1070. Product Sales Analysis III/gpt 5.1 thinking customized/Product_Sales_Analysis_III_pandas.ipynb new file mode 100644 index 00000000..0bfd6f12 --- /dev/null +++ b/SQL/Leetcode/Intermediate Select/1070. Product Sales Analysis III/gpt 5.1 thinking customized/Product_Sales_Analysis_III_pandas.ipynb @@ -0,0 +1,380 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "6e3e1932", + "metadata": {}, + "source": [ + "## 0) 前提\n", + "\n", + "* 環境: **Python 3.10.15 / pandas 2.2.2**\n", + "* I/O 操作なし(読み書きは関数外で実施する前提)\n", + "* 不要な `print` や `sort_values` は使用しない\n", + "* 指定シグネチャはこの回答内で定義するものを前提とする\n", + "\n", + "---\n", + "\n", + "## 1) 問題\n", + "\n", + "### PROBLEM_STATEMENT\n", + "\n", + "Table: `Sales`\n", + "\n", + "```text\n", + "+-------------+-------+\n", + "| Column Name | Type |\n", + "+-------------+-------+\n", + "| sale_id | int |\n", + "| product_id | int |\n", + "| year | int |\n", + "| quantity | int |\n", + "| price | int |\n", + "+-------------+-------+\n", + "(sale_id, year) が複合主キー\n", + "各行は、ある product_id のある year における 1 件の販売を表す\n", + "同じ product_id・同じ year で複数行あり得る\n", + "```\n", + "\n", + "**要件**\n", + "\n", + "* 各 `product_id` ごとに、「その商品が最初に販売された年(最小の `year`)」を特定する\n", + "* その「最初の年」に属する **全ての販売行** を抽出する(集約しない)\n", + "* 出力列と順序:\n", + "\n", + " * `product_id`\n", + " * `first_year`(= その商品の最初の販売年)\n", + " * `quantity`\n", + " * `price`\n", + "\n", + "### 入力 DF\n", + "\n", + "* `sales: pd.DataFrame`\n", + "\n", + " * 列: `['sale_id', 'product_id', 'year', 'quantity', 'price']` を前提\n", + "\n", + "### 出力\n", + "\n", + "* `pd.DataFrame`\n", + "\n", + " * 列名と順序は **`['product_id', 'first_year', 'quantity', 'price']`**\n", + "\n", + "---\n", + "\n", + "## 2) 実装(指定シグネチャ厳守)\n", + "\n", + "> 方針はテンプレ通り **列最小化 → グループ処理 → 条件抽出**。\n", + "> 「最小年」は `groupby.transform('min')` で各行に持たせ、`year == first_year` の行だけを残します。\n", + "\n", + "```python\n", + "import pandas as pd\n", + "\n", + "def find_first_year_sales(sales: pd.DataFrame) -> pd.DataFrame:\n", + " \"\"\"\n", + " 各 product_id について、最初に販売された年に属する全ての販売行を抽出する。\n", + "\n", + " Args:\n", + " sales (pd.DataFrame): 列 'sale_id', 'product_id', 'year', 'quantity', 'price' を含む入力データ\n", + "\n", + " Returns:\n", + " pd.DataFrame: 列名と順序は\n", + " ['product_id', 'first_year', 'quantity', 'price']\n", + " \"\"\"\n", + " # 1) 列最小化(必要な列だけに絞る)\n", + " df = sales[['product_id', 'year', 'quantity', 'price']].copy()\n", + "\n", + " # 2) グループ処理\n", + " # 各 product_id ごとに最小の year(最初の販売年)を求めて、\n", + " # transform で各行に同じ値を持たせる\n", + " first_year = df.groupby('product_id')['year'].transform('min')\n", + "\n", + " # 3) 条件抽出:year がその product_id の最初の年と等しい行だけ残す\n", + " kept = df.loc[df['year'].eq(first_year)]\n", + "\n", + " # 4) 列名整形と最終投影\n", + " out = kept.rename(columns={'year': 'first_year'})[\n", + " ['product_id', 'first_year', 'quantity', 'price']\n", + " ]\n", + "\n", + " return out\n", + "\n", + "Analyze Complexity\n", + "Runtime 316 ms\n", + "Beats 59.93%\n", + "Memory 69.54 MB\n", + "Beats 71.21%\n", + "\n", + "```\n", + "\n", + "* ポイント:\n", + "\n", + " * `groupby('product_id')['year'].transform('min')` によって、\n", + "\n", + " * 各行に「その product_id の最小 year」を直接持たせる\n", + " * 同じ `product_id`・同じ `year` の行が複数あっても、\n", + "\n", + " * その年は最小年であればすべて残る(仕様どおり)\n", + "\n", + "---\n", + "\n", + "## 3) アルゴリズム説明\n", + "\n", + "### 使用 API\n", + "\n", + "* `DataFrame[['col1', ...]]`\n", + " → 列最小化(不要列を落として処理対象を縮小)\n", + "* `groupby('product_id')['year'].transform('min')`\n", + "\n", + " * 各グループの集計値(ここでは最小年)を、グループ内の全行に展開する\n", + " * 戻りは元と同じ長さの `Series`\n", + "* `Series.eq(...)`\n", + " → ベクトル化された等価比較(`==` とほぼ同じだが、メソッド形式で読みやすい)\n", + "* `DataFrame.loc[mask]`\n", + "\n", + " * ブールマスクで必要な行のみ抽出\n", + "* `DataFrame.rename(columns={...})`\n", + "\n", + " * 列名の変更 (`year` → `first_year`)\n", + "* `DataFrame[...]`(列リスト指定)\n", + "\n", + " * 最終的な列の順序を仕様どおりに揃える\n", + "\n", + "### NULL / 重複 / 型\n", + "\n", + "* `year` が NULL(`NaN`)の場合\n", + "\n", + " * `min` は通常、非 NULL 値の最小を返す\n", + " * そのため「year がすべて NaN の product_id」は `first_year` が NaN になり、\n", + "\n", + " * `year == first_year` も NaN 比較になるため、どの行も選ばれない\n", + " * 実運用で year の NULL が許されないなら、事前に `dropna(subset=['year'])` などで除外可能\n", + "* 同一 `product_id`・同一 `year` に複数行ある場合\n", + "\n", + " * `transform('min')` は同一 product_id の全行に同じ最小年を付与\n", + " * `year == first_year` で、その年の行はすべて残る\n", + " * 問題文どおり「最初の年における全ての販売行」が取得される\n", + "* 型\n", + "\n", + " * `year` は整数列 (`int` / `Int64`) を想定\n", + " * `transform('min')` により返る型は元列に追従するため、特に追加変換は不要\n", + "\n", + "---\n", + "\n", + "## 4) 計算量(概算)\n", + "\n", + "`N` = 行数、`G` = `product_id` のユニーク数とします。\n", + "\n", + "* `df[['product_id', 'year', 'quantity', 'price']]`\n", + "\n", + " * 列抽出 … **O(N)**(ビュー的だが `copy()` でメモリ確保あり)\n", + "* `groupby('product_id')['year'].transform('min')`\n", + "\n", + " * グループごとの集計はハッシュベース実装想定\n", + " * 時間: おおむね **O(N)**~**O(N log G)** 近辺\n", + " * メモリ: 元と同じ長さの `Series` 分を追加で使用\n", + "* `loc[...]` によるフィルタ\n", + "\n", + " * ブールマスク評価 + 抽出 … **O(N)**\n", + "* `rename` と列再配置\n", + "\n", + " * 軽微(列メタデータの操作が中心)\n", + "\n", + "総じて、支配項は **groupby.transform** で、計算量は概ね **O(N)**〜**O(N log G)**。\n", + "`N` が大きくて `G` が相対的に小さい場合でも、実務上は十分スケールする構成です。\n", + "\n", + "---\n", + "\n", + "## 5) 図解(Mermaid 超保守版)\n", + "\n", + "```mermaid\n", + "flowchart TD\n", + " A[入力 Sales データフレーム]\n", + " B[前処理 列最小化 product_id year quantity price のみ]\n", + " C[グループ処理 product_id ごとに year の最小値を transform]\n", + " D[条件抽出 year が最小年と等しい行だけ残す]\n", + " E[列名整形 year を first_year に変更 列順を揃える]\n", + " F[出力 product_id first_year quantity price]\n", + "\n", + " A --> B\n", + " B --> C\n", + " C --> D\n", + " D --> E\n", + " E --> F\n", + "```\n", + "\n", + "この形であれば、テンプレの「列最小化 → グループ処理 → 条件抽出」の流れを踏襲しつつ、\n", + "Pandas 2.2.2 でも素直に動作する実務的な実装になっていると思います。\n", + "\n", + "結論から言うと、そのスコアなら **十分「合格点」かつほぼ最適クラス**で、アルゴリズムを変えて劇的に速くするのは難しいです。\n", + "316ms / Beats 59.93%、メモリ 69.54MB / Beats 71.21% は、LeetCode の分布を考えるとかなり健闘しています。\n", + "\n", + "そのうえで「もう少しだけ詰めるとしたら」という観点で、**現行実装の微調整ポイント**を整理します。\n", + "\n", + "---\n", + "\n", + "## 1. 現行実装の良い点の整理\n", + "\n", + "前回お渡しした実装(概略)は:\n", + "\n", + "```python\n", + "import pandas as pd\n", + "\n", + "def find_first_year_sales(sales: pd.DataFrame) -> pd.DataFrame:\n", + " df = sales[['product_id', 'year', 'quantity', 'price']].copy()\n", + "\n", + " first_year = df.groupby('product_id')['year'].transform('min')\n", + "\n", + " kept = df.loc[df['year'].eq(first_year)]\n", + "\n", + " out = kept.rename(columns={'year': 'first_year'})[\n", + " ['product_id', 'first_year', 'quantity', 'price']\n", + " ]\n", + "\n", + " return out\n", + "```\n", + "\n", + "これはテンプレの\n", + "\n", + "> 列最小化 → グループ処理(transform) → 条件抽出\n", + "\n", + "を素直にトレースしていて、アルゴリズムとしてはかなりきれいです。\n", + "\n", + "* 余計な列(`sale_id`)は落としている\n", + "* `groupby.transform('min')` で「最初の年」を各行に持たせている\n", + "* `year == first_year` だけ抽出しているので、同じ年の複数行も漏れなく取得できる\n", + "\n", + "この構造は基本的に変えなくてよいレベルです。\n", + "\n", + "---\n", + "\n", + "## 2. ごく小さい改善案(やるならこの程度)\n", + "\n", + "### 2-1. `.copy()` を外してメモリ削減\n", + "\n", + "`df` に対して「列追加・書き換え」をしていないので、**`copy()` は必須ではありません**。\n", + "LeetCode では `SettingWithCopyWarning` も出ませんし、ここはビューのままで構いません。\n", + "\n", + "```python\n", + "def find_first_year_sales(sales: pd.DataFrame) -> pd.DataFrame:\n", + " # 1) copy をやめて軽量化\n", + " df = sales[['product_id', 'year', 'quantity', 'price']]\n", + "\n", + " # 2) グループ処理\n", + " first_year = df.groupby('product_id')['year'].transform('min')\n", + "\n", + " # 3) 条件抽出\n", + " kept = df.loc[df['year'].eq(first_year)]\n", + "\n", + " # 4) 列名整形と最終投影\n", + " out = kept.rename(columns={'year': 'first_year'})[\n", + " ['product_id', 'first_year', 'quantity', 'price']\n", + " ]\n", + "\n", + " return out\n", + "\n", + "Analyze Complexity\n", + "Runtime 313 ms\n", + "Beats 65.65%\n", + "Memory 69.15 MB\n", + "Beats 86.03%\n", + "\n", + "```\n", + "\n", + "これで\n", + "\n", + "* 余分なフルコピー分のメモリを節約\n", + "* 実行時間も若干ですが改善する可能性あり\n", + "\n", + "とはいえ、**数 % 程度の差に収まることが多い**ので、「絶対やるべき」ほどではありません。\n", + "\n", + "---\n", + "\n", + "### 2-2. `transform('min')` → `groupby.min + merge` の別案\n", + "\n", + "アルゴリズム自体は同じクラスですが、**集約 + 結合パターン**も書き方としてはアリです。\n", + "\n", + "```python\n", + "import pandas as pd\n", + "\n", + "def find_first_year_sales(sales: pd.DataFrame) -> pd.DataFrame:\n", + " # 列最小化\n", + " df = sales[['product_id', 'year', 'quantity', 'price']]\n", + "\n", + " # 各 product_id ごとの最初の年だけを先に集約\n", + " first_years = (\n", + " df.groupby('product_id', as_index=False)['year']\n", + " .min()\n", + " .rename(columns={'year': 'first_year'})\n", + " )\n", + "\n", + " # product_id と year が first_year に一致する行だけを結合で回収\n", + " merged = df.merge(\n", + " first_years,\n", + " left_on=['product_id', 'year'],\n", + " right_on=['product_id', 'first_year'],\n", + " how='inner'\n", + " )\n", + "\n", + " out = merged[['product_id', 'first_year', 'quantity', 'price']]\n", + "\n", + " return out\n", + "\n", + "Analyze Complexity\n", + "Runtime 332 ms\n", + "Beats 41.75%\n", + "Memory 70.03 MB\n", + "Beats 37.71%\n", + "\n", + "```\n", + "\n", + "* `groupby.min` → `merge` というオーソドックスな書き方\n", + "* データ分布や Pandas の内部実装次第で、\n", + "\n", + " * `transform('min')` より速くなるケースもあれば\n", + " * 逆に遅くなるケースもあります\n", + "\n", + "LeetCode では「ほぼ誤差レベルの違い」になりがちなので、**可読性の好みで選ぶ**くらいの位置づけです。\n", + "\n", + "---\n", + "\n", + "### 2-3. dtype チューニング(理論上の話)\n", + "\n", + "LeetCode ではあまり期待できませんが、実務では:\n", + "\n", + "* `product_id`, `year`, `quantity`, `price` を `int32` に落とす\n", + "* 必要なら `product_id` を `category` にする\n", + "\n", + "などで\n", + "\n", + "* メモリフットプリント削減\n", + "* CPU キャッシュ効率向上\n", + "\n", + "が望めます。ただし、LeetCode で毎回 `astype` を噛ますと、そのコストの方が大きくなることも多いので、**ここでは触れない方がむしろ無難**です。\n", + "\n", + "---\n", + "\n", + "## 3. 結論\n", + "\n", + "* 現状のスコア\n", + " → **Runtime 316ms / 59.93%、Memory 69.54MB / 71.21%** は、かなり良い部類\n", + "* アルゴリズム的には\n", + " → すでに **O(N)**〜**O(N log G)** の最適クラスで、これ以上大きく改善する余地はほぼない\n", + "* やる価値があるとすれば:\n", + "\n", + " 1. `copy()` を外してメモリとわずかな時間を削る\n", + " 2. 好みで `groupby.min + merge` パターンを試してみる(速度はケース次第)\n", + "\n", + "ぐらいです。\n", + "\n", + "この問題に関しては、**今の transform 版を「Pandas テンプレ」として採用しておいて十分**だと思います。\n", + "他の類題で同じパターン(transform でグループ統計を各行に持たせてフィルタ)を再利用できるので、そこまで仕上がっていればかなり良い状態です。\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/SQL/Leetcode/Intermediate Select/1070. Product Sales Analysis III/gpt 5.1 thinking customized/Product_Sales_Analysis_III_posgres.ipynb b/SQL/Leetcode/Intermediate Select/1070. Product Sales Analysis III/gpt 5.1 thinking customized/Product_Sales_Analysis_III_posgres.ipynb new file mode 100644 index 00000000..384ae79d --- /dev/null +++ b/SQL/Leetcode/Intermediate Select/1070. Product Sales Analysis III/gpt 5.1 thinking customized/Product_Sales_Analysis_III_posgres.ipynb @@ -0,0 +1,456 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cd50814f", + "metadata": {}, + "source": [ + "## 0) 前提\n", + "\n", + "* エンジン: **PostgreSQL 16.6+**\n", + "* 並び順: 任意(`ORDER BY` は付けない)\n", + "* `NOT IN` は使用しない(本問はそもそも不要)\n", + "* 判定は `product_id` × `year`、表示は仕様どおり(`product_id, first_year, quantity, price`)\n", + "\n", + "---\n", + "\n", + "## 1) 問題\n", + "\n", + "### PROBLEM_STATEMENT\n", + "\n", + "Table: `Sales`\n", + "\n", + "```text\n", + "+-------------+-------+\n", + "| Column Name | Type |\n", + "+-------------+-------+\n", + "| sale_id | int |\n", + "| product_id | int |\n", + "| year | int |\n", + "| quantity | int |\n", + "| price | int |\n", + "+-------------+-------+\n", + "(sale_id, year) は複合主キー\n", + "```\n", + "\n", + "* 各行は「ある `product_id` の、ある `year` における 1 件の販売」を表す\n", + "* 同じ `product_id`・同じ `year` で複数行あり得る\n", + "\n", + "### 要求仕様\n", + "\n", + "* 各 `product_id` ごとに、**その商品が最初に販売された年(最小の `year`)** を求める\n", + "* その「最初の年」に属する **全ての販売行** を抽出する\n", + "* 出力列:\n", + "\n", + " * `product_id`\n", + " * `first_year` … その商品の最初の販売年\n", + " * `quantity`\n", + " * `price`\n", + "* 並び順: 任意\n", + "\n", + "---\n", + "\n", + "## 2) 最適解(単一クエリ / CTE + ウィンドウ)\n", + "\n", + "PostgreSQL なので、素直に **`DENSE_RANK()` ウィンドウ関数**を使い、\n", + "「各 `product_id` 内で最も早い `year` の行だけを残す」形にします。\n", + "\n", + "```sql\n", + "WITH win AS (\n", + " SELECT\n", + " product_id,\n", + " year,\n", + " quantity,\n", + " price,\n", + " DENSE_RANK() OVER (\n", + " PARTITION BY product_id\n", + " ORDER BY year\n", + " ) AS rnk\n", + " FROM Sales\n", + ")\n", + "SELECT\n", + " product_id,\n", + " year AS first_year,\n", + " quantity,\n", + " price\n", + "FROM win\n", + "WHERE rnk = 1;\n", + "\n", + "Runtime 942 ms\n", + "Beats 43.80%\n", + "\n", + "```\n", + "\n", + "### テンプレートへの対応関係\n", + "\n", + "```sql\n", + "WITH win AS (\n", + " SELECT\n", + " product_id,\n", + " year,\n", + " quantity,\n", + " price,\n", + " DENSE_RANK() OVER (\n", + " PARTITION BY product_id -- {{PARTITION_KEYS}}\n", + " ORDER BY year -- {{ORDER_KEYS}}\n", + " ) AS rnk -- {{RNK_OR_VAL}}\n", + " FROM Sales -- {{PRE_OR_INPUT}}\n", + ")\n", + "SELECT\n", + " product_id,\n", + " year AS first_year, -- {{FINAL_PROJECTION}}\n", + " quantity,\n", + " price\n", + "FROM win\n", + "WHERE rnk = 1; -- {{FILTER_ON_WINDOW_OR_CONDITION}}\n", + "\n", + "Runtime 878 ms\n", + "Beats 67.56%\n", + "\n", + "```\n", + "\n", + "* `pre` CTE は不要なため省略し、いきなり `win` CTE でウィンドウ処理を実施\n", + "* 「最初の年」の全行を残したいので `DENSE_RANK()` を使用\n", + "\n", + " * 同一 `product_id`・同一 `year` に複数行あっても、すべて `rnk = 1` になり仕様どおり\n", + "\n", + "### 代替: LATERAL は本問では不要\n", + "\n", + "提示テンプレートの「LATERAL + LIMIT k」パターンは\n", + "\n", + "* 「各グループから上位 k 件だけ取りたい」\n", + "* 「行数が多いので、各グループについて少量だけ抜きたい」\n", + "\n", + "といった場面で有効です。\n", + "\n", + "本問は単に「最初の年の全行」を取りたいだけなので、\n", + "`DENSE_RANK()` or 集約 + 結合 で十分であり、`LATERAL` の出番はありません。\n", + "\n", + "---\n", + "\n", + "## 3) 要点解説\n", + "\n", + "### ロジックの流れ\n", + "\n", + "1. `Sales` をそのまま入力とする\n", + "2. `product_id` ごとに `year` 昇順で並べ、`DENSE_RANK()` を付与\n", + "\n", + " * 最小の `year` に `rnk = 1`\n", + " * それ以外の年は `rnk >= 2`\n", + "3. `rnk = 1` の行だけを残す\n", + "\n", + " * その結果、「各 `product_id` が最初に売れた年」に属する全行が残る\n", + "4. `year` を `first_year` として出力し、他列はそのまま\n", + "\n", + "### `DENSE_RANK` を採用している理由\n", + "\n", + "* `ROW_NUMBER()` を使うと、「最初の年の中の 1 行だけ」しか残せない\n", + "* 本問は「最初の年の全ての販売行」を残したいので、\n", + "\n", + " * 同じ `year` の行をすべて `1` 位扱いにする `DENSE_RANK()` が適切\n", + "\n", + "### `NOT IN` を避けている点\n", + "\n", + "* 本問では「最小年以外の年を除外する」ために `NOT IN` / `IN` を使う必要がない\n", + "* ウィンドウ関数と `WHERE rnk = 1` で表現しているため、\n", + " NULL 罠 (`NOT IN (subquery)` + NULL) とは無縁の書き方になっている\n", + "\n", + "---\n", + "\n", + "## 4) 計算量(概算)\n", + "\n", + "`N` = `Sales` の行数、`G` = `product_id` のグループ数とします。\n", + "\n", + "* ウィンドウ部(`DENSE_RANK() OVER (PARTITION BY product_id ORDER BY year)`)\n", + "\n", + " * 各グループごとに `year` でソートが入るため、\n", + " * 時間計算量は概ね **O(Σ n_g log n_g)** ≒ **O(N log (N/G))**\n", + "* CTE からの最終 SELECT + `WHERE rnk = 1`\n", + "\n", + " * フィルタのみのため、**O(N)** 近似\n", + "\n", + "総じて **O(N log N)** クラスで、典型的なウィンドウ処理のコスト感です。\n", + "\n", + "インデックス面では、実務なら\n", + "\n", + "```sql\n", + "CREATE INDEX ON Sales (product_id, year);\n", + "```\n", + "\n", + "のような複合インデックスがあると、ソート負荷がかなり軽減されます\n", + "(`PARTITION BY product_id ORDER BY year` にぴったり合うため)。\n", + "\n", + "---\n", + "\n", + "## 5) 図解(Mermaid 超保守版)\n", + "\n", + "```mermaid\n", + "flowchart TD\n", + " A[入力 Sales テーブル]\n", + " B[ウィンドウ処理 product_id ごとに year で DENSE_RANK]\n", + " C[rnk が 1 の行だけを抽出]\n", + " D[列名を整形 product_id first_year quantity price]\n", + " E[出力]\n", + "\n", + " A --> B\n", + " B --> C\n", + " C --> D\n", + " D --> E\n", + "```\n", + "\n", + "PostgreSQL 16.6+ 前提であれば、このウィンドウ関数版が最も素直で可読性も高く、\n", + "パフォーマンスとメンテナンス性のバランスが良い解法になります。\n", + "\n", + "結論から言うと、\n", + "\n", + "* 今のクエリは **ロジックとしては最適クラス**\n", + "* Runtime の 942ms → 878ms(67.56%)も、LeetCode 的には十分良好\n", + "* それでも「もう少し詰める」としたら、**ウィンドウ関数の形を変える**か、**集約+結合パターン**を試す、という「微調整レベル」\n", + "\n", + "くらいの温度感です。\n", + "\n", + "そのうえで、現行案をベースに「変え得るポイント」を整理します。\n", + "\n", + "---\n", + "\n", + "## 1. 現行クエリの評価\n", + "\n", + "```sql\n", + "WITH win AS (\n", + " SELECT\n", + " product_id,\n", + " year,\n", + " quantity,\n", + " price,\n", + " DENSE_RANK() OVER (\n", + " PARTITION BY product_id\n", + " ORDER BY year\n", + " ) AS rnk\n", + " FROM Sales\n", + ")\n", + "SELECT\n", + " product_id,\n", + " year AS first_year,\n", + " quantity,\n", + " price\n", + "FROM win\n", + "WHERE rnk = 1;\n", + "```\n", + "\n", + "* アルゴリズム的には:\n", + "\n", + " * 各 `product_id` 内で `year` 昇順に順位付け → 最小年の行だけ残す\n", + " * 「最初の年に属する全行」を残すので `DENSE_RANK()` 採用は正解\n", + "* 計算量は典型的なウィンドウ処理で **O(N log N)** クラス\n", + "* Runtime が 878ms / Beats 67.56% なら、\n", + " 「十分早い・無理にいじるほどではない」というラインです\n", + "\n", + "→ ここからは完全に「チューニング遊び」の領域です。\n", + "\n", + "---\n", + "\n", + "## 2. 同じウィンドウ路線での微調整案\n", + "\n", + "### 2-1. `MIN(year) OVER` を使う書き換え\n", + "\n", + "`DENSE_RANK()` の代わりに、**パーティション内の最小年を直接持たせる**パターンです。\n", + "\n", + "```sql\n", + "WITH win AS (\n", + " SELECT\n", + " product_id,\n", + " year,\n", + " quantity,\n", + " price,\n", + " MIN(year) OVER (\n", + " PARTITION BY product_id\n", + " ) AS first_year\n", + " FROM Sales\n", + ")\n", + "SELECT\n", + " product_id,\n", + " first_year,\n", + " quantity,\n", + " price\n", + "FROM win\n", + "WHERE year = first_year;\n", + "\n", + "Runtime 920 ms\n", + "Beats 51.45%\n", + "\n", + "```\n", + "\n", + "ポイント:\n", + "\n", + "* `MIN(year) OVER (PARTITION BY product_id)` なので、\n", + "\n", + " * 各行に「その product_id の最小年」を付与\n", + " * それと `year` が等しい行だけ残す\n", + "* `ORDER BY` なしのウィンドウなので、理論上は\n", + "\n", + " * 「ランク計算」よりは軽いことが多い\n", + " * 実装上は結局ソートが入ることもありますが、オプティマイザに若干有利な場合があります\n", + "\n", + "LeetCode 環境では差が出ないか、数%レベルの違いにとどまる可能性が高いですが、\n", + "**読みやすさ+若干のコスト低減**という意味で試す価値はあります。\n", + "\n", + "### 2-2. CTE をやめてサブクエリにする\n", + "\n", + "PostgreSQL 12 以降は CTE は「最適化フェンス」ではなくなりましたが、\n", + "それでも素直にインラインの方がプランが分かりやすい場合があります。\n", + "\n", + "```sql\n", + "SELECT\n", + " product_id,\n", + " first_year,\n", + " quantity,\n", + " price\n", + "FROM (\n", + " SELECT\n", + " product_id,\n", + " year AS first_year,\n", + " quantity,\n", + " price,\n", + " DENSE_RANK() OVER (\n", + " PARTITION BY product_id\n", + " ORDER BY year\n", + " ) AS rnk\n", + " FROM Sales\n", + ") t\n", + "WHERE rnk = 1;\n", + "\n", + "Runtime 912 ms\n", + "Beats 54.81%\n", + "\n", + "```\n", + "\n", + "もしくは `MIN OVER` 版と組み合わせて:\n", + "\n", + "```sql\n", + "SELECT\n", + " product_id,\n", + " first_year,\n", + " quantity,\n", + " price\n", + "FROM (\n", + " SELECT\n", + " product_id,\n", + " year,\n", + " quantity,\n", + " price,\n", + " MIN(year) OVER (PARTITION BY product_id) AS first_year\n", + " FROM Sales\n", + ") t\n", + "WHERE year = first_year;\n", + "\n", + "Runtime 951 ms\n", + "Beats 40.44%\n", + "\n", + "```\n", + "\n", + "LeetCode のような簡易環境だと **ほとんど誤差**ですが、\n", + "実務では「CTE をパターン的に書きすぎない」こともプラン安定の一助になります。\n", + "\n", + "---\n", + "\n", + "## 3. 代替パターン:集約 + 結合\n", + "\n", + "MySQL 版でも触れたパターンですが、PostgreSQL でも王道です。\n", + "\n", + "```sql\n", + "WITH first_years AS (\n", + " SELECT\n", + " product_id,\n", + " MIN(year) AS first_year\n", + " FROM Sales\n", + " GROUP BY product_id\n", + ")\n", + "SELECT\n", + " s.product_id,\n", + " f.first_year,\n", + " s.quantity,\n", + " s.price\n", + "FROM Sales AS s\n", + "JOIN first_years AS f\n", + " ON s.product_id = f.product_id\n", + " AND s.year = f.first_year;\n", + "\n", + "Runtime 965 ms\n", + "Beats 37.35%\n", + "\n", + "```\n", + "\n", + "特徴:\n", + "\n", + "* `GROUP BY` で `product_id` ごとの最小年を先に確定させる\n", + "* その結果(1行/商品)を元テーブルに結合し、該当年の全行を取得\n", + "* 計算量としてはやはり **O(N log N)** クラスですが、\n", + "\n", + " * `GROUP BY` の実装(Hash Aggregate / Sort Aggregate)や\n", + " * 結合戦略(Hash Join / Merge Join)\n", + " によっては、ウィンドウ版より速くなることもあります\n", + "\n", + "LeetCode の結果としても、MySQL 側ではこのパターンの方が若干速かったので、\n", + "PostgreSQL 版でも同じく「微差で有利」になる可能性はあります。\n", + "\n", + "---\n", + "\n", + "## 4. さらに簡潔な相関サブクエリ版(好みレベル)\n", + "\n", + "もう一つよく使われる書き方として、**相関サブクエリで MIN(year)** を直接当てる形もあります。\n", + "\n", + "```sql\n", + "SELECT\n", + " s.product_id,\n", + " s.year AS first_year,\n", + " s.quantity,\n", + " s.price\n", + "FROM Sales AS s\n", + "WHERE s.year = (\n", + " SELECT MIN(year)\n", + " FROM Sales AS s2\n", + " WHERE s2.product_id = s.product_id\n", + ");\n", + "\n", + "Runtime 2942 ms\n", + "Beats 5.04%\n", + "```\n", + "\n", + "* ロジックが非常に読みやすい\n", + "* PostgreSQL は相関サブクエリをかなりうまくデコリレーションしてくれるので、\n", + "\n", + " * 実行計画としては「GROUP BY+結合」に近い形になることが多い\n", + "* インデックスが `(product_id, year)` に張られていれば、\n", + " 実務ではかなり優秀なパターンです\n", + "\n", + "LeetCode 環境でも、これを試してみて Runtime がどう変わるかを見る価値はあります。\n", + "\n", + "---\n", + "\n", + "## 5. まとめ\n", + "\n", + "* **今のウィンドウ版(DENSE_RANK)は十分良い**です\n", + "\n", + " * 878ms / Beats 67.56% という数値からも、無理にいじる必要はないレベル\n", + "* それでも「もう一歩攻める」なら:\n", + "\n", + " 1. `DENSE_RANK()` → `MIN(year) OVER (PARTITION BY product_id)` に書き換え\n", + " 2. CTE をインラインサブクエリにしてみる\n", + " 3. `GROUP BY + JOIN` 版\n", + " 4. 相関サブクエリ版\n", + "\n", + " をそれぞれ試して、Runtime のブレを見比べる、くらいが現実的です。\n", + "\n", + "ここまで来るとほぼ「チューニング道楽」の世界なので、\n", + "この問題に関しては「今のクエリを PG16 版のテンプレ解」として採用しておいて問題ないと思います。\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}