{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "bd01-title",
   "metadata": {},
   "source": [
    "# 🗺️ 01 · MapReduce：从原理到手写词频统计\n",
    "\n",
    "> MapReduce 是\"分布式计算的启蒙思想\"：**Map（分）→ Shuffle（排序分组）→ Reduce（合）**。\n",
    "> 本节手写**词频统计**与**平均分计算**，并演示 **Shuffle 排序**如何产生。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bd01-q",
   "metadata": {},
   "source": [
    "## 📋 本节要解决的问题\n",
    "\n",
    "| # | 问题 | 方法 |\n",
    "|---|---|---|\n",
    "| 1 | MapReduce 三个阶段？ | Map / Shuffle / Reduce |\n",
    "| 2 | 怎么处理大数据？ | 分而治之：数据分片并行 |\n",
    "| 3 | Shuffle 到底是什么？ | 排序 + 按键分组 |\n",
    "| 4 | 典型应用？ | 词频、TopN、分组聚合 |"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "bd01-setup",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-08T09:36:37.768652Z",
     "iopub.status.busy": "2026-10-08T09:36:37.767820Z",
     "iopub.status.idle": "2026-10-08T09:36:55.891779Z",
     "shell.execute_reply": "2026-10-08T09:36:55.889262Z"
    }
   },
   "outputs": [],
   "source": [
    "%matplotlib inline\n",
    "import matplotlib\n",
    "import matplotlib.pyplot as plt\n",
    "matplotlib.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'PingFang SC', 'Noto Sans CJK SC', 'Arial Unicode MS']\n",
    "matplotlib.rcParams['axes.unicode_minus'] = False\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import sqlite3\n",
    "import os\n",
    "rng = np.random.default_rng(42)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bd01-1",
   "metadata": {},
   "source": [
    "## 1️⃣ 思想三句话（必背）"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "31531c26",
   "metadata": {},
   "source": [
    "### 形式化理解（初学者版）\n",
    "\n",
    "把计算抽象成两个函数：\n",
    "\n",
    "- **map**：$\\text{map}(k1, v1) \\rightarrow \\text{list}(k2, v2)$ —— 对每条输入记录独立调用，天然可并行；\n",
    "- **reduce**：$\\text{reduce}(k2, \\text{list}(v2)) \\rightarrow \\text{list}(v2)$ —— 对同一个 $k2$ 的全部值做一次聚合。\n",
    "\n",
    "**词频统计**就是：`map(行号, 一行文本) → [(词, 1), ...]`，`reduce(词, [1,1,...]) → [(词, 求和)]`。\n",
    "\n",
    "**并行度由什么决定**？输入被切成 $M$ 个分片（split），就有 $M$ 个 Map 任务；\n",
    "指定 $R$ 个 Reduce 分区，就有 $R$ 个 Reduce 任务。分片大小默认 ≈ HDFS 块大小（128MB），\n",
    "这样 Map 任务**本地读数据**（数据本地性），不跨网络搬输入。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "bd01-idea",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-08T09:36:55.898149Z",
     "iopub.status.busy": "2026-10-08T09:36:55.896905Z",
     "iopub.status.idle": "2026-10-08T09:36:55.921399Z",
     "shell.execute_reply": "2026-10-08T09:36:55.916578Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1) Map：把每个输入记录变成 (key, value) 中间结果，可并行；\n",
      "2) Shuffle：系统按 key 排序并分组（相同 key 发给同一 Reduce）；\n",
      "3) Reduce：对一个 key 的所有 value 做聚合，输出最终结果。\n",
      "\n",
      "核心价值：把\"不可并行\"的聚合问题拆成\"可并行\"的 map + 局部聚合。\n",
      "\n"
     ]
    }
   ],
   "source": [
    "print('''1) Map：把每个输入记录变成 (key, value) 中间结果，可并行；\n",
    "2) Shuffle：系统按 key 排序并分组（相同 key 发给同一 Reduce）；\n",
    "3) Reduce：对一个 key 的所有 value 做聚合，输出最终结果。\n",
    "\n",
    "核心价值：把\"不可并行\"的聚合问题拆成\"可并行\"的 map + 局部聚合。\n",
    "''')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bd01-2",
   "metadata": {},
   "source": [
    "## 2️⃣ 构造模拟\"日志数据\"（分片）"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "bd01-data",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-08T09:36:55.927006Z",
     "iopub.status.busy": "2026-10-08T09:36:55.926150Z",
     "iopub.status.idle": "2026-10-08T09:36:56.519449Z",
     "shell.execute_reply": "2026-10-08T09:36:56.512266Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "分片0: ['apple banana apple', 'cherry apple']\n",
      "分片1: ['banana banana cherry', 'apple']\n",
      "分片2: ['cherry cherry banana apple', 'apple cherry']\n",
      "\n",
      "➡️ 真实场景：每个分片在（虚拟）不同机器上并行 Map\n"
     ]
    }
   ],
   "source": [
    "# 模拟 3 个数据分片，每片若干行单词\n",
    "splits = [\n",
    "    ['apple banana apple', 'cherry apple'],\n",
    "    ['banana banana cherry', 'apple'],\n",
    "    ['cherry cherry banana apple', 'apple cherry'],\n",
    "]\n",
    "for i, s in enumerate(splits):\n",
    "    print(f'分片{i}: {s}')\n",
    "print('\\n➡️ 真实场景：每个分片在（虚拟）不同机器上并行 Map')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bd01-3",
   "metadata": {},
   "source": [
    "## 3️⃣ Map 阶段：每个分片产出 (词, 1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "bd01-map",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-08T09:36:56.526044Z",
     "iopub.status.busy": "2026-10-08T09:36:56.525071Z",
     "iopub.status.idle": "2026-10-08T09:36:57.566703Z",
     "shell.execute_reply": "2026-10-08T09:36:57.559925Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "分片0 Map 输出: [('apple', 1), ('banana', 1), ('apple', 1), ('cherry', 1), ('apple', 1)]\n",
      "分片1 Map 输出: [('banana', 1), ('banana', 1), ('cherry', 1), ('apple', 1)]\n",
      "分片2 Map 输出: [('cherry', 1), ('cherry', 1), ('banana', 1), ('apple', 1), ('apple', 1), ('cherry', 1)]\n",
      "\n",
      "全部中间结果 15 条\n",
      "➡️ Map 输出也称 intermediate records，key=词，value=1\n"
     ]
    }
   ],
   "source": [
    "def mapper(line):\n",
    "    out = []\n",
    "    for w in line.lower().split():\n",
    "        out.append((w, 1))\n",
    "    return out\n",
    "\n",
    "intermediate = []\n",
    "for i, split in enumerate(splits):\n",
    "    partition_out = []\n",
    "    for line in split:\n",
    "        partition_out += mapper(line)\n",
    "    intermediate += partition_out\n",
    "    print(f'分片{i} Map 输出: {partition_out}')\n",
    "print(f'\\n全部中间结果 {len(intermediate)} 条')\n",
    "print('➡️ Map 输出也称 intermediate records，key=词，value=1')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bd01-4",
   "metadata": {},
   "source": [
    "## 4️⃣ Shuffle：按 key 排序与分组"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f23aeac9",
   "metadata": {},
   "source": [
    "### Shuffle 的完整链路（面试展开版）\n",
    "\n",
    "Shuffle 横跨 Map 结束到 Reduce 开始，真实过程分三步：\n",
    "\n",
    "1. **Map 端**：输出先写入**环形缓冲区**（默认 100MB，`mapreduce.task.io.sort.mb`），\n",
    "   写到 **80%**（溢写阈值）就**溢写**到本地磁盘——溢写前完成**分区、排序**，可执行 **Combiner**、压缩；\n",
    "   多个溢写文件最后**归并**成一个已排序大文件；\n",
    "2. **拷贝**：Reduce 任务通过 HTTP 从各 Map 节点**拉取**属于自己的分区数据；\n",
    "3. **Reduce 端**：拉到的文件**归并排序**后按 key 分组，逐组调用 reduce()。\n",
    "\n",
    "**Combiner**：可选的\"本地 reduce\"，在 map 端先聚一次。对**求和/最大值**这类满足\n",
    "结合律的聚合，能大幅减少传输量（本例 9 条 map 输出可压成 4 条）；但对**求平均**\n",
    "不能直接用（先平均再平均≠总平均）。\n",
    "\n",
    "**Partitioner**：决定每条 map 输出去哪个 Reduce，默认 `HashPartitioner`：\n",
    "`partition = hash(key) % R`。相同 key 必然同分区，这就是\"按 key 分组\"能跨机器成立的原因。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "bd01-shuffle",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-08T09:36:57.572792Z",
     "iopub.status.busy": "2026-10-08T09:36:57.571895Z",
     "iopub.status.idle": "2026-10-08T09:36:58.225549Z",
     "shell.execute_reply": "2026-10-08T09:36:58.222741Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "排序后（前 8 条）: [('apple', 1), ('apple', 1), ('apple', 1), ('apple', 1), ('apple', 1), ('apple', 1), ('banana', 1), ('banana', 1)]\n",
      "\n",
      "分组结果: {'apple': 6, 'banana': 4, 'cherry': 5}\n",
      "➡️ 面试点：Shuffle 是 MR 最耗时的环节（排序+网络传输），优化思路：map 端预聚合（combiner）、压缩中间数据\n"
     ]
    }
   ],
   "source": [
    "# Shuffle = 排序（按 key） + 分组（相同 key 的 value 集合到一起）\n",
    "intermediate_sorted = sorted(intermediate, key=lambda kv: kv[0])\n",
    "grouped = {}\n",
    "for k, v in intermediate_sorted:\n",
    "    grouped.setdefault(k, []).append(v)\n",
    "print('排序后（前 8 条）:', intermediate_sorted[:8])\n",
    "print('\\n分组结果:', {k: len(v) for k, v in grouped.items()})\n",
    "print('➡️ 面试点：Shuffle 是 MR 最耗时的环节（排序+网络传输），'\n",
    "      '优化思路：map 端预聚合（combiner）、压缩中间数据')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bd01-5",
   "metadata": {},
   "source": [
    "## 5️⃣ Reduce 阶段：聚合计数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "bd01-reduce",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-08T09:36:58.232600Z",
     "iopub.status.busy": "2026-10-08T09:36:58.231619Z",
     "iopub.status.idle": "2026-10-08T09:36:58.875463Z",
     "shell.execute_reply": "2026-10-08T09:36:58.871082Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "词频统计结果: {'apple': 6, 'banana': 4, 'cherry': 5}\n",
      "总单词数: 15\n",
      "✅ 与 pandas value_counts 对照完全一致\n",
      "数据总单词数: 15\n",
      "✅ MapReduce 逻辑正确\n"
     ]
    }
   ],
   "source": [
    "result = {k: sum(v) for k, v in grouped.items()}\n",
    "total = sum(result.values())\n",
    "print('词频统计结果:', dict(sorted(result.items())))\n",
    "print('总单词数:', total)\n",
    "\n",
    "# 对照 pandas 验证\n",
    "df = pd.DataFrame([w for s in splits for line in s for w in line.lower().split()], columns=['word'])\n",
    "expected = df['word'].value_counts().sort_index().to_dict()\n",
    "assert result == expected, 'MR 结果与 pandas 对照不一致'\n",
    "print('✅ 与 pandas value_counts 对照完全一致')\n",
    "total_words = sum(len(line.split()) for s in splits for line in s)\n",
    "print('数据总单词数:', total_words)\n",
    "assert total_words == 15\n",
    "print('✅ MapReduce 逻辑正确')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "d2717248",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-08T09:36:58.890074Z",
     "iopub.status.busy": "2026-10-08T09:36:58.889394Z",
     "iopub.status.idle": "2026-10-08T09:37:01.116993Z",
     "shell.execute_reply": "2026-10-08T09:37:01.113819Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "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u7tnOsDzi7c3eR+8TevWrZOlS5fqvXxoZGSkjB49WvlcTk9Pl08++UTatm372oeHvOuMRDLZP0DvHFNTU/To0QPr1683dCn5SlxcHExNTWFhYZErZ9ZS7rp16xYGDx6MRYsWZXrgOr36dRwbG4uoqKhc2b2Wm6ZPn47vv/8ewcHB71xtpE4HDhzA4sWLsX79+leeJf2+4jF/7xFPT0+DnEGV32Uc30LvJnd3dxw+fNjQZbzzXvU6trW1Vcbee5e4urrC09Mz02FDiN62Fi1avJXjgg2JPX9EREREKsJx/oiIiIhUhOGPiIiISEXe62P+0tPT8eDBA1hbW/NAfSIiIlItEUF8fDyKFSumc43ql73X4e/Bgwd6BzImIiIiUqOwsDCUKFEiyzbvdfiztrYG8PyB8oxNIiIiUqu4uDiULFlSyUZZea/DX8auXhsbG4Y/IiIiUr3sHAZn0PB35MgRJCYm6sy3srLK1jURiYiIiChnDBr+vvvuO4SFhWnNe/z4MapWrYpTp04ZqCoiIiKi/Mug4S8wMFBr+tmzZ3B3d8fo0aMNVBERERFR/vZOjfO3fPly2Nvbo1u3boYuhYiIiChfemdO+EhMTMScOXOwZMmSV45PQ0RERESv550Jf+vWrUOBAgXQuXPnTNtoNBpoNBplOi4u7m2URkRERJRvvDNdbEuXLsWQIUNgapp5Hp01axZsbW2VGwd4JiIiIsoZIxERQxfxzz//oGHDhrh3716WgU5fz1/JkiURGxvLcf6IiIhIteLi4mBra5utTPRO7Pbdtm0b6tat+8qePHNzc5ibm7+lqoiIiIjyn3dit++OHTvg7e1t6DKIiIiI8j2Dh7+IiAiEhoaiXr16hi6FiIiIKN8zePh7+PAh2rRpg1q1ahm6FCIiIqJ875044eN15eTgRiIiIqL8KieZyOA9f0RERET09jD8EREREakIwx8RERGRijD8EREREakIwx8RERGRijD8EREREakIwx8RERGRijD8EREREakIwx8RERGRijD8EREREakIwx8RERGRijD8EREREakIwx8RERGRijD8EREREakIwx8RERGRijD8EREREakIwx8RERGRijD8EREREakIwx8RERGRijD8EREREakIwx8RERGRijD8EREREamIqaELoHdDVFQU1q1bh7NnzyIhIQHFixfHkCFDUL16dUOX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",
      "text/plain": [
       "<Figure size 750x420 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# 可视化：词频统计结果\n",
    "fig, ax = plt.subplots(figsize=(7.5, 4.2))\n",
    "words_sorted = sorted(result.items(), key=lambda kv: -kv[1])\n",
    "ks = [k for k, _ in words_sorted]; vs = [v for _, v in words_sorted]\n",
    "bars = ax.bar(ks, vs, color=['#2F5597', '#ED7D31', '#548235', '#C00000'][:len(ks)], width=0.55)\n",
    "for b, v in zip(bars, vs):\n",
    "    ax.text(b.get_x() + b.get_width() / 2, v + 0.06, str(v), ha='center', fontsize=12, weight='bold')\n",
    "ax.set_ylabel('出现次数'); ax.set_title('MapReduce 词频统计结果（15 个单词，与 pandas 交叉验证一致）')\n",
    "ax.set_ylim(0, max(vs) + 1.2)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bd01-6",
   "metadata": {},
   "source": [
    "## 6️⃣ 进阶：TopN 与均值（Reduce 变体）"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "bd01-more",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-08T09:37:01.122337Z",
     "iopub.status.busy": "2026-10-08T09:37:01.121540Z",
     "iopub.status.idle": "2026-10-08T09:37:01.406844Z",
     "shell.execute_reply": "2026-10-08T09:37:01.401354Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "各班平均分: {'一班': np.float64(84.33), '二班': np.float64(93.5), '三班': np.float64(79.0)}\n",
      "词频 Top2: [('apple', 6), ('cherry', 5)]\n",
      "✅ 变体任务完成\n"
     ]
    }
   ],
   "source": [
    "# 平均分：Map 出 (班级, 分数)，Reduce 求和并计数\n",
    "records = [('一班', 85), ('二班', 92), ('一班', 78), ('三班', 88), ('二班', 95), ('一班', 90), ('三班', 70)]\n",
    "grouped_scores = {}\n",
    "for cls, sc in records:\n",
    "    grouped_scores.setdefault(cls, []).append(sc)\n",
    "avg = {cls: round(np.mean(vs), 2) for cls, vs in grouped_scores.items()}\n",
    "print('各班平均分:', avg)\n",
    "\n",
    "# TopN：全局 top2（跨分片，可用分区+归并）\n",
    "top2 = sorted((v, k) for k, v in result.items())[-2:]\n",
    "print('词频 Top2:', [(k, v) for v, k in top2[::-1]])\n",
    "assert len(avg) == 3\n",
    "print('✅ 变体任务完成')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2f1d1c13",
   "metadata": {},
   "source": [
    "### 均值为什么不能直接用 Combiner（对照实验）\n",
    "\n",
    "一班分数 [85, 78, 90]：若 map 端先求局部均值 (85+78)/2=81.5，再与 90 平均 → (81.5+90)/2=85.75，\n",
    "而真实均值 (85+78+90)/3=84.33——**结果错了**。正确做法：Combiner 输出 **(和, 计数)** 二元组，\n",
    "Reduce 端再 Σ和/Σ计数。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "95e81310",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-08T09:37:01.414710Z",
     "iopub.status.busy": "2026-10-08T09:37:01.413787Z",
     "iopub.status.idle": "2026-10-08T09:37:01.975951Z",
     "shell.execute_reply": "2026-10-08T09:37:01.973200Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "正确(传和与计数): {'一班': 84.33, '二班': 93.5}  | 一班真实均值 = 84.33\n",
      "错误(传局部均值): {'一班': np.float64(81.5)}  -> 与真实值不符\n"
     ]
    }
   ],
   "source": [
    "# 验证：均值任务必须传 (sum, count) 而不是局部均值\n",
    "records_c = [('一班', 85), ('一班', 78), ('一班', 90), ('二班', 92), ('二班', 95)]\n",
    "acc = {}\n",
    "for cls, sc in records_c:\n",
    "    s, c = acc.get(cls, (0, 0))\n",
    "    acc[cls] = (s + sc, c + 1)\n",
    "correct = {cls: round(s / c, 2) for cls, (s, c) in acc.items()}\n",
    "wrong = {'一班': round(np.mean([85, 78]) , 2)}  # 错误示范：局部平均后直接再平均\n",
    "print('正确(传和与计数):', correct, ' | 一班真实均值 =', round(np.mean([85, 78, 90]), 2))\n",
    "print('错误(传局部均值):', wrong, ' -> 与真实值不符')\n",
    "assert correct['一班'] == 84.33"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bd01-7",
   "metadata": {},
   "source": [
    "## 7️⃣ MapReduce 局限（面试必问）"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "bd01-limit",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-08T09:37:01.986974Z",
     "iopub.status.busy": "2026-10-08T09:37:01.986043Z",
     "iopub.status.idle": "2026-10-08T09:37:02.551388Z",
     "shell.execute_reply": "2026-10-08T09:37:02.547139Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1) 磁盘 IO 重：中间结果落盘，迭代计算慢；\n",
      "   -> 被 Spark（内存计算）替代，仅少数批任务保留；\n",
      "2) 编程模型受限：复杂 DAG/迭代算法难表达；\n",
      "3) 实时性差：分钟级延迟起步；\n",
      "4) 大量 shuffle 排序开销；\n",
      "5) 适用场景：超大单遍聚合（离线日志统计），如今多由 Hive/Spark SQL 语法糖承接。\n",
      "\n"
     ]
    }
   ],
   "source": [
    "print('''1) 磁盘 IO 重：中间结果落盘，迭代计算慢；\n",
    "   -> 被 Spark（内存计算）替代，仅少数批任务保留；\n",
    "2) 编程模型受限：复杂 DAG/迭代算法难表达；\n",
    "3) 实时性差：分钟级延迟起步；\n",
    "4) 大量 shuffle 排序开销；\n",
    "5) 适用场景：超大单遍聚合（离线日志统计），如今多由 Hive/Spark SQL 语法糖承接。\n",
    "''')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bd01-8",
   "metadata": {},
   "source": [
    "## 📝 自测清单\n",
    "\n",
    "- [ ] 能讲清 Map/Shuffle/Reduce 三阶段\n",
    "- [ ] 能手写词频统计（Map 产出 (w,1) → 分组 → 求和）\n",
    "- [ ] 能解释 Shuffle 为什么慢\n",
    "- [ ] 能说出 MR 五点局限与替代方案"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
