{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "0507b9ff",
   "metadata": {},
   "source": [
    "# 第 4 讲｜Python 字符串处理：基础操作与文本分析\n",
    "\n",
    "**对象：** 已会变量、类型转换、条件与循环的 Python 初学者。  \n",
    "**课堂目标：** 能准确匹配文本、清理常见脏字符，并在所有计算结束后按题目要求格式化输出。  \n",
    "**参考：** 《Python 快速编程入门（第 3 版）》第 4 章，参考文件 `4f.pdf`。  \n",
    "**真题依据：** 名仕、科图、青娅三套 Python 题。\n",
    "\n",
    "本课使用本地微型数据，不访问网络。代码可按顺序直接运行。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4265d2a5",
   "metadata": {},
   "source": [
    "## 1. 本课只解决竞赛最需要的五件事\n",
    "\n",
    "1. 区分字符串与数值；\n",
    "2. 精确匹配前先检查空白和符号；\n",
    "3. 会用 `strip()`、`replace()`、`split()`、`join()`；\n",
    "4. 会写 f-string，也能看懂真题中的 `.format()`；\n",
    "5. 坚持“清洗 -> 计算 -> 选择 -> 最后格式化”。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fe561f77",
   "metadata": {},
   "source": [
    "## 2. 真题中的直接证据\n",
    "\n",
    "| 套题 | 字符串出现在哪里 | 本课对应能力 |\n",
    "|---|---|---|\n",
    "| 名仕 | URL、请求头、字段名、数字文本 | 字符串定义、类型转换 |\n",
    "| 科图 | `x == \"女\"`、`'{:.2f}%'.format(x)` | 精确匹配、百分比格式化 |\n",
    "| 青娅 | `==`、`.isin([...])`、固定两位输出 | 文本筛选、最终格式化 |\n",
    "\n",
    "字符串不是单独出一道算法题，而是藏在采集、筛选、分组和交付中。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f8b085ef",
   "metadata": {},
   "source": [
    "## 3. 引号决定“这是文本”\n",
    "\n",
    "`\"125\"` 是三个字符组成的字符串；`125` 是整数。它们看起来相似，但计算和排序规则不同。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fe694ad5",
   "metadata": {},
   "source": [
    "### 只有业务上需要计算时才转换\n",
    "\n",
    "名仕题中的销量、退单量和售价若来自网页文本，应先转换，再计算实际销量和销售额。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4874493d",
   "metadata": {},
   "source": [
    "### 代码讲解\n",
    "\n",
    "\n",
    "先用 type(value) 判断当前类型。int(整数文本) 和 float(小数文本) 负责转换；\n",
    "减法、乘法必须使用转换后的数值。f-string 只放在最后展示，不参与原始计算。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "00cb3759",
   "metadata": {},
   "source": [
    "### 课堂演示（样本代码）\n",
    "\n",
    "先由教师逐行解释输入、处理和输出，再运行下方样本代码。运行后修改一个值，\n",
    "观察结果是否符合刚才的预测。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ecbe4b02",
   "metadata": {},
   "outputs": [],
   "source": [
    "quantity_text = \"125\"\n",
    "quantity_number = 125\n",
    "\n",
    "print(quantity_text, type(quantity_text))\n",
    "print(quantity_number, type(quantity_number))\n",
    "print(\"文本重复：\", quantity_text * 3)\n",
    "print(\"数值相乘：\", quantity_number * 3)\n",
    "print(\"是否相等：\", quantity_text == quantity_number)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "db99906d",
   "metadata": {},
   "outputs": [],
   "source": [
    "sales_text = \"125\"\n",
    "returns_text = \"8\"\n",
    "price_text = \"19.9\"\n",
    "\n",
    "actual_sales = int(sales_text) - int(returns_text)\n",
    "actual_amount = actual_sales * float(price_text)\n",
    "print(f\"实际销量：{actual_sales}\")\n",
    "print(f\"实际销售额：{actual_amount:.2f} 元\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "78e5e059",
   "metadata": {},
   "source": [
    "### 课堂练习（学生填写）\n",
    "\n",
    "根据刚才的样本代码独立补全下方空位；先预测结果，完成后再取消注释运行。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "987dff83",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 目标：把数量和单价文本转换为数值，计算销售总额。\n",
    "# product_name = \"数据线\"\n",
    "# quantity_text = \"6\"\n",
    "# price_text = \"12.5\"\n",
    "# quantity =                  # 请填写 int 转换\n",
    "# price =                     # 请填写 float 转换\n",
    "# total =                     # 请填写乘法\n",
    "# print(f\"{product_name}：{quantity} 件，合计 {total:.2f} 元\")\n",
    "# 预期合计：75.00 元"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d729a6f5",
   "metadata": {},
   "source": [
    "## 4. 精确匹配：多一个空格也不相等\n",
    "\n",
    "科图题用 `x == \"女\"` 计算女性占比。若原数据是 `\"女 \"`，肉眼很难看出差异，但精确比较会失败。`repr()` 可以把隐藏空白显示出来。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "122f6826",
   "metadata": {},
   "outputs": [],
   "source": [
    "\"a \""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "054a70e6",
   "metadata": {},
   "outputs": [],
   "source": [
    "\"a\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "38a0a2fa",
   "metadata": {},
   "outputs": [],
   "source": [
    "\"a \" == \"a\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5c0d960e",
   "metadata": {},
   "source": [
    "## 5. 字符串不可变：方法返回新字符串\n",
    "\n",
    "`strip()` 不会原地改变变量。必须接住返回值。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b28d1de7",
   "metadata": {},
   "source": [
    "## 6. `strip()` 是本课最重要的字符串方法\n",
    "\n",
    "- `strip()`：删除两端空白；\n",
    "- `lstrip()`：只删除左端；\n",
    "- `rstrip()`：只删除右端。\n",
    "\n",
    "真题清洗通常优先使用 `strip()`。它不会删除字符串中间的空格。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c61b5a7e",
   "metadata": {},
   "source": [
    "### 代码讲解\n",
    "\n",
    "\n",
    "repr(text) 用于看清尾部空格、换行等隐藏字符。text.strip() 返回删除两端空白后的\n",
    "新字符串；字符串本身不可变，所以必须把返回值重新赋给变量，再做精确比较。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "430aa84a",
   "metadata": {},
   "source": [
    "### 课堂演示（样本代码）\n",
    "\n",
    "先由教师逐行解释输入、处理和输出，再运行下方样本代码。运行后修改一个值，\n",
    "观察结果是否符合刚才的预测。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6ce2362d",
   "metadata": {},
   "outputs": [],
   "source": [
    "gender = \"女 \"\n",
    "print(\"普通打印：\", gender)\n",
    "print(\"诊断打印：\", repr(gender))\n",
    "print(\"清洗前：\", gender == \"女\")\n",
    "print(\"清洗后：\", gender.strip() == \"女\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5444db3e",
   "metadata": {},
   "outputs": [],
   "source": [
    "raw_label = \"  支付宝  \"\n",
    "raw_label.strip()\n",
    "print(\"没有重新赋值：\", repr(raw_label))\n",
    "\n",
    "clean_label = raw_label.strip()\n",
    "print(\"接住返回值：\", repr(clean_label))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7ee687c3",
   "metadata": {},
   "outputs": [],
   "source": [
    "raw_label = raw_label.strip()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a1ac5285",
   "metadata": {},
   "outputs": [],
   "source": [
    "label = \"  华 东  \"\n",
    "print(\"原值：\", repr(label))\n",
    "print(\"strip：\", repr(label.strip()))\n",
    "print(\"中间空格仍保留：\", label.strip() == \"华东\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1730331d",
   "metadata": {},
   "source": [
    "### 课堂练习（学生填写）\n",
    "\n",
    "根据刚才的样本代码独立补全下方空位；先预测结果，完成后再取消注释运行。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "92c3bc0a",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 目标：诊断并清理性别与付款方式字段。\n",
    "# raw_gender = \" 女 \"\n",
    "# raw_payment = \"支付宝 \"\n",
    "# print(repr(raw_gender), repr(raw_payment))\n",
    "# clean_gender =              # 请填写 strip()\n",
    "# clean_payment =             # 请填写 strip()\n",
    "# print(clean_gender == \"女\")\n",
    "# print(clean_payment == \"支付宝\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0e92a9b1",
   "metadata": {},
   "outputs": [],
   "source": [
    "# str class 的 strip() 方法\n",
    "str.strip(\" 支付宝\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e26c4327",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Dog:\n",
    "    \"\"\"小狗类\"\"\"\n",
    "    \n",
    "    def __init__(self, name, age):\n",
    "        \"\"\"初始化方法：创建小狗对象时自动调用\"\"\"\n",
    "        # property\n",
    "        self.name = name\n",
    "        self.age = age\n",
    "        self.is_sitting = False  # 默认没坐着\n",
    "\n",
    "    # method\n",
    "    def run(self):\n",
    "        \"\"\"小狗跑步方法\"\"\"\n",
    "        if self.is_sitting:\n",
    "            print(f\"{self.name} 嗖地站起来，然后跑起来了！🏃🐕\")\n",
    "            self.is_sitting = False\n",
    "        else:\n",
    "            print(f\"{self.name} 正在欢快地奔跑！🏃🐕\")\n",
    "    \n",
    "    def sit(self):\n",
    "        \"\"\"小狗坐下方法\"\"\"\n",
    "        if self.is_sitting:\n",
    "            print(f\"{self.name} 已经坐下了，别让我再坐啦！😅\")\n",
    "        else:\n",
    "            print(f\"{self.name} 乖乖坐下了。🧘🐕\")\n",
    "            self.is_sitting = True\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7879e149",
   "metadata": {},
   "outputs": [],
   "source": [
    "my_dog = Dog(\"旺财\", 3)\n",
    "\n",
    "print(\"--- 让小狗试试 ---\")\n",
    "my_dog.run()   # 第一次跑\n",
    "# my_dog.sit()   # 坐下\n",
    "# my_dog.sit()   # 再试一次坐下（已经有坐了）\n",
    "# my_dog.run()   # 从坐姿站起来跑"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "25b9dc88",
   "metadata": {},
   "source": [
    "## 7. `replace()`：只在业务规则明确时统一符号\n",
    "\n",
    "青娅题的项目名称包含中文冒号。如果网页数据混入英文冒号，可以在确认含义一致后进行替换。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3f527dc6",
   "metadata": {},
   "source": [
    "### 代码讲解\n",
    "\n",
    "\n",
    "replace(old, new) 返回替换后的新字符串。链式写法从左向右执行：\n",
    "先 strip() 删除两端空白，再 replace(\":\", \"：\") 统一经过确认的同义符号。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ecc15786",
   "metadata": {},
   "source": [
    "### 课堂演示（样本代码）\n",
    "\n",
    "先由教师逐行解释输入、处理和输出，再运行下方样本代码。运行后修改一个值，\n",
    "观察结果是否符合刚才的预测。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b447ef5f",
   "metadata": {},
   "outputs": [],
   "source": [
    "project = \" 加:期初现金及现金等价物余额 \"\n",
    "normalized_project = project.strip().replace(\":\", \"：\")\n",
    "# normalized_project\n",
    "\n",
    "print(\"原值：\", repr(project))\n",
    "print(\"标准值：\", normalized_project)\n",
    "print(normalized_project == \"加：期初现金及现金等价物余额\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0f6fbf2d",
   "metadata": {},
   "source": [
    "### 课堂练习（学生填写）\n",
    "\n",
    "根据刚才的样本代码独立补全下方空位；先预测结果，完成后再取消注释运行。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "97806eb0",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 目标：把项目名称清理为青娅题中的标准文本。\n",
    "# raw_project = \" 五、现金及现金等价物净增加额 \"\n",
    "# clean_project =             # 请填写 strip()\n",
    "# print(clean_project == \"五、现金及现金等价物净增加额\")\n",
    "#\n",
    "# raw_balance = \" 加:期初现金及现金等价物余额 \"\n",
    "# clean_balance =             # 先 strip，再把英文冒号替换为中文冒号\n",
    "# print(clean_balance)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cf202852",
   "metadata": {},
   "source": [
    "## 8. `split()` 与 `join()`：一个拆开，一个拼回\n",
    "\n",
    "`split()` 返回列表；`join()` 用连接符把多个文本元素拼成新字符串。它们适合订单编号、复合字段和输出列名整理。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "05197618",
   "metadata": {},
   "source": [
    "### 代码讲解\n",
    "\n",
    "\n",
    "text.split(\"-\") 按连字符拆分并返回列表；\"_\".join(parts) 使用下划线拼接列表中的\n",
    "文本元素并返回新字符串。先判断返回类型，避免把列表继续当作字符串使用。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5390d12c",
   "metadata": {},
   "source": [
    "### 课堂演示（样本代码）\n",
    "\n",
    "先由教师逐行解释输入、处理和输出，再运行下方样本代码。运行后修改一个值，\n",
    "观察结果是否符合刚才的预测。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1197f332",
   "metadata": {},
   "outputs": [],
   "source": [
    "order_code = \"2024-East-0018\"\n",
    "\n",
    "parts = order_code.split(\"-\")\n",
    "\n",
    "parts"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8aa3e57c",
   "metadata": {},
   "outputs": [],
   "source": [
    "rebuilt = \"*\".join(parts)\n",
    "\n",
    "print(parts, type(parts))\n",
    "print(rebuilt, type(rebuilt))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b5f5f34c",
   "metadata": {},
   "source": [
    "### 课堂练习（学生填写）\n",
    "\n",
    "根据刚才的样本代码独立补全下方空位；先预测结果，完成后再取消注释运行。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0d0bda06",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 目标：拆分批次编号，并按新格式重新拼接。\n",
    "# batch_code = \"2024-Q2-East-018\"\n",
    "# parts =                      # 请按连字符拆分\n",
    "# rebuilt =                    # 请使用 \"/\" 拼接\n",
    "# print(parts)\n",
    "# print(rebuilt)\n",
    "# 预期：2024/Q2/East/018"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c23334f6",
   "metadata": {},
   "source": [
    "## 9. 两种格式化：会写 f-string，会读 `.format()`\n",
    "\n",
    "科图参考代码使用 `.format()`；课堂统一推荐 f-string。两种写法都应能看懂。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "05cfd531",
   "metadata": {},
   "source": [
    "### 代码讲解\n",
    "\n",
    "\n",
    "格式说明 :.2f 表示保留两位小数。科图参考代码使用 \"{:.2f}%\".format(value)；\n",
    "f\"{value:.2f}%\" 更直观。学生应能读懂前者，熟练书写后者。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4bf97645",
   "metadata": {},
   "outputs": [],
   "source": [
    "apple = \"大的\"\n",
    "\n",
    "print(f\"苹果:{apple}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2f1ca9b8",
   "metadata": {},
   "outputs": [],
   "source": [
    "apple = \"大的\"\n",
    "\n",
    "print(\"苹果:{}\".format(apple))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cc61dc61",
   "metadata": {},
   "outputs": [],
   "source": [
    "price = 12\n",
    "print(f\"单价：{price:.2f} 元\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "342b508d",
   "metadata": {},
   "source": [
    "### 课堂演示（样本代码）\n",
    "\n",
    "先由教师逐行解释输入、处理和输出，再运行下方样本代码。运行后修改一个值，\n",
    "观察结果是否符合刚才的预测。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5f0421e6",
   "metadata": {},
   "outputs": [],
   "source": [
    "value = 66.6667\n",
    "\n",
    "old_answer_style = \"{:.2f}%\".format(value)\n",
    "recommended_style = f\"{value:.2f}%\"\n",
    "\n",
    "print(old_answer_style)\n",
    "print(recommended_style)\n",
    "print(old_answer_style == recommended_style)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "92ada368",
   "metadata": {},
   "source": [
    "### 课堂练习（学生填写）\n",
    "\n",
    "根据刚才的样本代码独立补全下方空位；先预测结果，完成后再取消注释运行。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "06f5f530",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 目标：分别使用 format() 和 f-string 输出百分比。\n",
    "# female_rate = 2 / 3 * 100\n",
    "# by_format =                  # 请使用 \"{:.2f}%\".format(...)\n",
    "# by_f_string =                # 请使用 f-string\n",
    "# print(by_format)\n",
    "# print(by_f_string)\n",
    "# 预期：两行都是 66.67%"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ffa1fe53",
   "metadata": {},
   "source": [
    "## 10. `round(..., 2)` 不等于固定显示两位\n",
    "\n",
    "青娅题要求“不足两位用 0 补足”。这里要区分两件事：\n",
    "\n",
    "- `round(数值, 2)`：用于**计算时**保留两位小数，结果仍是数值；\n",
    "- `f\"{数值:.2f}\"`：用于**展示时**生成固定两位的小数文本。\n",
    "\n",
    "例如，`round(12.3, 2)` 的计算结果是数值 `12.3`；但提交或打印金额时，通常需要显示为\n",
    "`12.30`。因此，先计算，最后再格式化显示。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "1ca7678f",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "12.3"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "round(12.3, 2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0bd7431b",
   "metadata": {},
   "source": [
    "### 课堂演示（样本代码）\n",
    "\n",
    "先运行代码，观察相同的金额使用两种写法后，值和显示形式有什么差别。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "7d9eac3d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "原始数值： 12.3\n",
      "round 后： 12.3\n",
      "固定两位显示： 12.30\n",
      "显示文本： 应提交金额 12.30 元\n"
     ]
    }
   ],
   "source": [
    "amount = 12.3\n",
    "rounded_amount = round(amount, 2)\n",
    "display_amount = f\"{amount:.2f}\"\n",
    "\n",
    "print(\"原始数值：\", amount)\n",
    "print(\"round 后：\", rounded_amount)\n",
    "print(\"固定两位显示：\", display_amount)\n",
    "print(\"显示文本：\", \"应提交金额 \" + display_amount + \" 元\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "d105cebd",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "12.3"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "rounded_amount"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fd948058",
   "metadata": {},
   "source": [
    "### 代码讲解\n",
    "\n",
    "1. `amount` 是用于计算的数值；\n",
    "2. `round(amount, 2)` 表示把数值按两位小数处理，但不会自动在末尾补 `0`；\n",
    "3. `f\"{amount:.2f}\"` 中的 `.2f` 表示固定显示两位小数，结果用于文本展示；\n",
    "4. 计算、比较、求最大值时应保留数值；准备输出结果时再使用格式化。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "df084b94",
   "metadata": {},
   "source": [
    "### 课堂练习（学生填写）\n",
    "\n",
    "先预测两行输出，再补全空位并运行。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "563dc13a",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "计算结果： 4.0\n",
      "提交显示： 4.00\n"
     ]
    }
   ],
   "source": [
    "amount = 4.0\n",
    "rounded_amount =    round(amount, 2)             # 用 round 保留两位\n",
    "display_amount =     f\"{rounded_amount:.2f}\"             # 固定显示两位\n",
    "print(\"计算结果：\", rounded_amount)\n",
    "print(\"提交显示：\", display_amount)\n",
    "# 预期：计算结果为 4.0，提交显示为 4.00"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ed792534",
   "metadata": {},
   "source": [
    "## 11. 先计算和选择，最后格式化\n",
    "\n",
    "比赛题常要求“找出最大占比，再显示为百分比”。正确顺序是：\n",
    "\n",
    "1. 在**数值列表**中找最大值；\n",
    "2. 得到最大数值后，再格式化为百分比文本。\n",
    "\n",
    "`max(列表)` 的作用是取列表中的最大值。不要先把数值变成字符串再比较，因为字符串按字符\n",
    "顺序比较：`\"9.50%\"` 会被认为比 `\"10.00%\"` 大，这是错误的业务结果。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3b927337",
   "metadata": {},
   "source": [
    "### 课堂演示（样本代码）\n",
    "\n",
    "先运行并比较两次 `max()` 的结果：一次处理数值，一次处理已经格式化的文本。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "id": "bb351f04",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "文本列表中的 max（错误口径）： 9.50\n"
     ]
    }
   ],
   "source": [
    "# rates = [9.5, 10.0, 8.75]\n",
    "# largest_rate = max(rates)\n",
    "# largest_text = f\"{largest_rate:.2f}%\"\n",
    "\n",
    "text_rates = [\"9.50\", \"10.00\", \"8.75\"]\n",
    "\n",
    "# print(\"数值列表中的最大值：\", largest_rate)\n",
    "# print(\"正确的最终显示：\", largest_text)\n",
    "print(\"文本列表中的 max（错误口径）：\", max(text_rates))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "id": "d3e3f6a9",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "10.0"
      ]
     },
     "execution_count": 36,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "text_rates = [9.50, \"10.00\", \"8.75\"]\n",
    "float_rates = [float(rate) for rate in text_rates]\n",
    "max(float_rates)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "id": "4728d64d",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[9.5, 10.0, 8.75]"
      ]
     },
     "execution_count": 38,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "float_rates = []\n",
    "for rate in text_rates:\n",
    "    float_rates.append(float(rate))\n",
    "\n",
    "float_rates"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "526bc382",
   "metadata": {},
   "source": [
    "### 代码讲解\n",
    "\n",
    "1. `rates` 中保存的是可计算、可比较的数值；\n",
    "2. `max(rates)` 先选出真正最大的数值 `10.0`；\n",
    "3. `f\"{largest_rate:.2f}%\"` 只在最终交付前把结果显示为 `10.00%`；\n",
    "4. `text_rates` 仅用于说明错误做法：对百分比字符串使用 `max()`，得到的不是数值意义上的最大值。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f55ba388",
   "metadata": {},
   "source": [
    "### 课堂练习（学生填写）\n",
    "\n",
    "先在数值列表中选择最大值，再把最终结果格式化为两位百分比。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0a515cce",
   "metadata": {},
   "outputs": [],
   "source": [
    "# rates = [8.75, 9.5, 10.0]\n",
    "# largest_rate =                 # 对数值列表使用 max\n",
    "# largest_text =                  # 格式化为两位百分比\n",
    "# print(largest_text)\n",
    "# 预期：10.00%"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "97766028",
   "metadata": {},
   "source": [
    "## 12. 普通字符串与 pandas 整列文本\n",
    "\n",
    "### 背景：为什么这里要学习 pandas 和 Series？\n",
    "\n",
    "前面的字符串操作处理的是**一个值**，例如一条姓名、一个付款方式。实际比赛数据通常来自\n",
    "Excel 或 CSV：每一列都有很多条记录，逐条手动处理既慢又容易遗漏。**pandas** 是 Python 中\n",
    "处理表格数据的工具库，适合一次处理整列数据。\n",
    "\n",
    "- `import pandas as pd`：导入 pandas，并约定用简写 `pd` 调用它；\n",
    "- `DataFrame`：一张二维表，类似一整个 Excel 工作表；\n",
    "- `Series`：表中的一列数据，也可以单独创建；它是一串带行号的数据。\n",
    "\n",
    "下一段样例中的 `labels` 就是一个 Series：其中每个元素是一条“性别”记录。我们要一次清理\n",
    "这一整列记录两端多余的空格，再计算“女”所占比例。\n",
    "\n",
    "### 一个字符串与一整列字符串\n",
    "\n",
    "- 一个字符串：`text.strip()`；\n",
    "- 一整列字符串：`series.str.strip()`。\n",
    "\n",
    "`.str` 是 pandas 提供的文本访问器，不是教材中普通字符串语法的替代品。它的作用是把\n",
    "同一个字符串方法应用到 Series 中的每一个元素。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "0e32d20d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "a    1\n",
      "b    2\n",
      "c    3\n",
      "dtype: int64\n"
     ]
    }
   ],
   "source": [
    "import pandas as pd \n",
    "\n",
    "a = pd.Series([1, 2, 3], index=[\"a\", \"b\", \"c\"])\n",
    "print(a)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "ad1b232b",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "苹果    5.5\n",
      "香蕉    3.2\n",
      "橙子    4.8\n",
      "dtype: float64\n"
     ]
    }
   ],
   "source": [
    "s = pd.Series({'苹果': 5.5, '香蕉': 3.2, '橙子': 4.8})\n",
    "print(s)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "360b164e",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.microsoft.datawrangler.viewer.v0+json": {
       "columns": [
        {
         "name": "index",
         "rawType": "object",
         "type": "string"
        },
        {
         "name": "重量",
         "rawType": "float64",
         "type": "float"
        }
       ],
       "ref": "7ef47108-9481-4ad1-9cfb-577b70d2a953",
       "rows": [
        [
         "苹果",
         "5.5"
        ],
        [
         "香蕉",
         "3.2"
        ],
        [
         "橙子",
         "4.8"
        ]
       ],
       "shape": {
        "columns": 1,
        "rows": 3
       }
      },
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>重量</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>苹果</th>\n",
       "      <td>5.5</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>香蕉</th>\n",
       "      <td>3.2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>橙子</th>\n",
       "      <td>4.8</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "     重量\n",
       "苹果  5.5\n",
       "香蕉  3.2\n",
       "橙子  4.8"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "s.to_frame(\"重量\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "c2083019",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "dict"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "data = {\n",
    "    '姓名': ['张三', '李四', '王五', '赵六'],\n",
    "    '年龄': [25, 30, 28, 35],\n",
    "    '城市': ['北京', '上海', '广州', '深圳'],\n",
    "    '工资': [8000, 12000, 9500, 15000]\n",
    "}\n",
    "\n",
    "type(data)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "bd60e191",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "    0   1   2      3\n",
      "0  张三  25  北京   8000\n",
      "1  李四  30  上海  12000\n",
      "2  王五  28  广州   9500\n"
     ]
    }
   ],
   "source": [
    "# 列表的每个元素是一行数据\n",
    "data = [\n",
    "    ['张三', 25, '北京', 8000],\n",
    "    ['李四', 30, '上海', 12000],\n",
    "    ['王五', 28, '广州', 9500]\n",
    "]\n",
    "# data\n",
    "df = pd.DataFrame(data, columns=['姓名', '年龄', '城市', '工资'])\n",
    "df = pd.DataFrame(data)\n",
    "print(df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "c8507619",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.microsoft.datawrangler.viewer.v0+json": {
       "columns": [
        {
         "name": "index",
         "rawType": "int64",
         "type": "integer"
        },
        {
         "name": "姓名",
         "rawType": "object",
         "type": "string"
        },
        {
         "name": "年龄",
         "rawType": "int64",
         "type": "integer"
        },
        {
         "name": "城市",
         "rawType": "object",
         "type": "string"
        },
        {
         "name": "工资",
         "rawType": "int64",
         "type": "integer"
        }
       ],
       "ref": "71ca1161-d80e-40d1-b9f8-921c1d6bf4ba",
       "rows": [
        [
         "0",
         "张三",
         "25",
         "北京",
         "8000"
        ],
        [
         "1",
         "李四",
         "30",
         "上海",
         "12000"
        ],
        [
         "2",
         "王五",
         "28",
         "广州",
         "9500"
        ],
        [
         "3",
         "赵六",
         "35",
         "深圳",
         "15000"
        ]
       ],
       "shape": {
        "columns": 4,
        "rows": 4
       }
      },
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>姓名</th>\n",
       "      <th>年龄</th>\n",
       "      <th>城市</th>\n",
       "      <th>工资</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>张三</td>\n",
       "      <td>25</td>\n",
       "      <td>北京</td>\n",
       "      <td>8000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>李四</td>\n",
       "      <td>30</td>\n",
       "      <td>上海</td>\n",
       "      <td>12000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>王五</td>\n",
       "      <td>28</td>\n",
       "      <td>广州</td>\n",
       "      <td>9500</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>赵六</td>\n",
       "      <td>35</td>\n",
       "      <td>深圳</td>\n",
       "      <td>15000</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   姓名  年龄  城市     工资\n",
       "0  张三  25  北京   8000\n",
       "1  李四  30  上海  12000\n",
       "2  王五  28  广州   9500\n",
       "3  赵六  35  深圳  15000"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "df = pd.DataFrame(data)\n",
    "df  "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "02880b30",
   "metadata": {},
   "source": [
    "### 代码讲解\n",
    "\n",
    "\n",
    "单个字符串使用 text.strip()；pandas Series 必须通过 .str 访问文本方法。\n",
    "astype(\"string\") 将列转成 pandas 字符串类型，并能保留缺失值，再调用 .str.strip()。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8df4c984",
   "metadata": {},
   "source": [
    "### 课堂演示（样本代码）\n",
    "\n",
    "先由教师逐行解释输入、处理和输出，再运行下方样本代码。运行后修改一个值，\n",
    "观察结果是否符合刚才的预测。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "f0ec265c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "     原值   清洗后\n",
      "0    女      女\n",
      "1     男     男\n",
      "2     女     女\n",
      "3  None  <NA>\n",
      "女性占比： 0.6666666666666666\n"
     ]
    }
   ],
   "source": [
    "import pandas as pd\n",
    "\n",
    "labels = pd.Series([\"女 \", \" 男\", \"女\", None], name=\"性别\")\n",
    "cleaned = labels.astype(\"string\").str.strip()\n",
    "\n",
    "print(pd.DataFrame({\"原值\": labels, \"清洗后\": cleaned}))\n",
    "print(\"女性占比：\", (cleaned == \"女\").mean())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8c4b1164",
   "metadata": {},
   "source": [
    "### 课堂练习（学生填写）\n",
    "\n",
    "根据刚才的样本代码独立补全下方空位；先预测结果，完成后再取消注释运行。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6c31ec7c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 目标：清理一整列性别，并计算女性占比。\n",
    "# practice_gender = pd.Series([\" 女\", \"男 \", \"女\", \"男\"])\n",
    "# clean_gender =               # astype(\"string\") 后使用 .str.strip()\n",
    "# female_rate =                # 与 \"女\" 比较后求 mean()\n",
    "# print(clean_gender.tolist())\n",
    "# print(f\"女性占比：{female_rate * 100:.2f}%\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b372d4f4",
   "metadata": {},
   "source": [
    "## 13. 科图真题微案例：付款方式占比\n",
    "\n",
    "固定流程：\n",
    "\n",
    "1. 清理付款方式；\n",
    "2. 按年份和区域分组；\n",
    "3. 计算组内付款方式占比；\n",
    "4. 每组只保留占比最大的一种；\n",
    "5. 最后格式化百分比。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "64f7ee0d",
   "metadata": {},
   "source": [
    "### 代码讲解\n",
    "\n",
    "\n",
    "科图付款方式流程分成两段：第一段对文本清洗并计算仍为数值的组内占比；\n",
    "第二段用 idxmax() 每组选择一行，排序完成后才把占比格式化为百分比字符串。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3d0403c1",
   "metadata": {},
   "source": [
    "### 课堂演示（样本代码）\n",
    "\n",
    "先由教师逐行解释输入、处理和输出，再运行下方样本代码。运行后修改一个值，\n",
    "观察结果是否符合刚才的预测。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "86d3a994",
   "metadata": {},
   "outputs": [],
   "source": [
    "payment_data = pd.DataFrame(\n",
    "    {\n",
    "        \"年\": [2023, 2023, 2023, 2023, 2023, 2023, 2023],\n",
    "        \"区域分布\": [\"华东\", \"华东\", \"华东\", \"华北\", \"华北\", \"华北\", \"华北\"],\n",
    "        \"付款方式\": [\"支付宝\", \"支付宝 \", \"微信\", \"微信\", \"微信 \", \"银行卡\", \"微信\"],\n",
    "    }\n",
    ")\n",
    "\n",
    "work = payment_data.copy()\n",
    "work[\"付款方式\"] = work[\"付款方式\"].astype(\"string\").str.strip()\n",
    "grouped = work.groupby([\"年\", \"区域分布\"])\n",
    "\n",
    "payment_result = (\n",
    "    grouped[\"付款方式\"]\n",
    "    .value_counts(normalize=True)\n",
    "    .mul(100)\n",
    "    .rename(\"占比\")\n",
    "    .reset_index()\n",
    ")\n",
    "payment_result"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "730e827d",
   "metadata": {},
   "outputs": [],
   "source": [
    "top_payment = payment_result.loc[\n",
    "    payment_result.groupby([\"年\", \"区域分布\"])[\"占比\"].idxmax()\n",
    "].copy()\n",
    "\n",
    "top_payment = top_payment.sort_values([\"年\", \"区域分布\"]).reset_index(drop=True)\n",
    "top_payment[\"占比\"] = top_payment[\"占比\"].map(lambda value: f\"{value:.2f}%\")\n",
    "top_payment"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a33cce7e",
   "metadata": {},
   "source": [
    "### 课堂练习（学生填写）\n",
    "\n",
    "根据刚才的样本代码独立补全下方空位；先预测结果，完成后再取消注释运行。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d547f9f2",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 目标：用不同样例完成“每个区域主要付款方式”。\n",
    "# practice_payment = pd.DataFrame({\n",
    "#     \"年\": [2024, 2024, 2024, 2024, 2024],\n",
    "#     \"区域分布\": [\"华南\", \"华南\", \"华南\", \"西北\", \"西北\"],\n",
    "#     \"付款方式\": [\"微信 \", \"微信\", \"支付宝\", \"银行卡\", \"银行卡 \"],\n",
    "# })\n",
    "# work = practice_payment.copy()\n",
    "# work[\"付款方式\"] =            # 清理整列\n",
    "# grouped =                     # 按年、区域分布分组\n",
    "# result = (                    # value_counts(normalize=True) 后乘 100\n",
    "# )\n",
    "# top_result =                  # 每组用 idxmax() 保留最大占比\n",
    "# top_result[\"占比\"] =          # 最后格式化\n",
    "# print(top_result)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "67ad8843",
   "metadata": {},
   "source": [
    "## 14. 科图真题微案例：女性占比\n",
    "\n",
    "清洗后的性别列再与 `\"女\"` 比较。布尔值中 `True` 按 1、`False` 按 0 计算，均值就是女性占比。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cfc50f83",
   "metadata": {},
   "source": [
    "### 代码讲解\n",
    "\n",
    "\n",
    "清洗后的性别列与 \"女\" 比较会得到布尔值；True 按 1、False 按 0，\n",
    "因此布尔均值就是女性占比。分组聚合完成后再乘 100 和格式化。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fff6df73",
   "metadata": {},
   "source": [
    "### 课堂演示（样本代码）\n",
    "\n",
    "先由教师逐行解释输入、处理和输出，再运行下方样本代码。运行后修改一个值，\n",
    "观察结果是否符合刚才的预测。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e4e25415",
   "metadata": {},
   "outputs": [],
   "source": [
    "customer_data = pd.DataFrame(\n",
    "    {\n",
    "        \"年\": [2023, 2023, 2023, 2023],\n",
    "        \"客户类型\": [\"普通\", \"普通\", \"会员\", \"会员\"],\n",
    "        \"性别\": [\"女 \", \"男\", \" 女\", \"女\"],\n",
    "        \"年龄\": [28, 34, 31, 37],\n",
    "    }\n",
    ")\n",
    "\n",
    "customer_data[\"性别\"] = customer_data[\"性别\"].astype(\"string\").str.strip()\n",
    "customer_result = (\n",
    "    customer_data.groupby([\"年\", \"客户类型\"])\n",
    "    .agg(\n",
    "        女性占比=(\"性别\", lambda values: (values == \"女\").mean() * 100),\n",
    "        平均年龄=(\"年龄\", \"mean\"),\n",
    "    )\n",
    "    .reset_index()\n",
    ")\n",
    "customer_result[\"女性占比\"] = customer_result[\"女性占比\"].map(\n",
    "    lambda value: f\"{value:.2f}%\"\n",
    ")\n",
    "customer_result[\"平均年龄\"] = customer_result[\"平均年龄\"].astype(int)\n",
    "customer_result"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1b546b93",
   "metadata": {},
   "source": [
    "### 课堂练习（学生填写）\n",
    "\n",
    "根据刚才的样本代码独立补全下方空位；先预测结果，完成后再取消注释运行。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c62db008",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 目标：按客户类型计算女性占比和平均年龄。\n",
    "# practice_customer = pd.DataFrame({\n",
    "#     \"客户类型\": [\"新客\", \"新客\", \"老客\", \"老客\"],\n",
    "#     \"性别\": [\"女 \", \"男\", \" 女\", \"女\"],\n",
    "#     \"年龄\": [20, 24, 30, 36],\n",
    "# })\n",
    "# practice_customer[\"性别\"] =  # 清理整列\n",
    "# summary = (                  # groupby 后 agg\n",
    "# )\n",
    "# summary[\"女性占比\"] =        # 最后格式化为百分比\n",
    "# print(summary)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "faa8191b",
   "metadata": {},
   "source": [
    "## 15. 青娅真题微案例：项目名称筛选\n",
    "\n",
    "`.isin([...])` 是 pandas 的多值筛选，不是普通字符串方法。目标值仍必须与清洗后的项目名称完全一致。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9959ffd5",
   "metadata": {},
   "source": [
    "### 交付副本固定两位，计算副本仍保留数值\n",
    "\n",
    "格式化会把金额列变成字符串，因此只对最终导出副本操作。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "27d5d046",
   "metadata": {},
   "source": [
    "### 代码讲解\n",
    "\n",
    "\n",
    "先清理项目列，再用 Series.isin(targets) 一次匹配两个目标项目。\n",
    ".copy() 生成独立结果；金额保持数值完成 fillna 和 round，最后只对导出副本固定两位。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b93fbeb5",
   "metadata": {},
   "source": [
    "### 课堂演示（样本代码）\n",
    "\n",
    "先由教师逐行解释输入、处理和输出，再运行下方样本代码。运行后修改一个值，\n",
    "观察结果是否符合刚才的预测。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "802848b7",
   "metadata": {},
   "outputs": [],
   "source": [
    "cashflow_data = pd.DataFrame(\n",
    "    {\n",
    "        \"项目\": [\n",
    "            \" 加：期初现金及现金等价物余额 \",\n",
    "            \"五、现金及现金等价物净增加额\",\n",
    "            \"经营活动产生的现金流量净额\",\n",
    "        ],\n",
    "        \"2022年度\": [12.3, 4.0, 22.678],\n",
    "        \"2021年度\": [None, 5.126, 18.0],\n",
    "        \"2020年度\": [8.0, None, 16.2],\n",
    "    }\n",
    ")\n",
    "\n",
    "targets = [\n",
    "    \"加：期初现金及现金等价物余额\",\n",
    "    \"五、现金及现金等价物净增加额\",\n",
    "]\n",
    "amount_columns = [\"2022年度\", \"2021年度\", \"2020年度\"]\n",
    "\n",
    "cashflow_data[\"项目\"] = cashflow_data[\"项目\"].astype(\"string\").str.strip()\n",
    "selected_rows = cashflow_data.loc[cashflow_data[\"项目\"].isin(targets)].copy()\n",
    "selected_rows[amount_columns] = selected_rows[amount_columns].fillna(0).round(2)\n",
    "selected_rows"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4f1d51c7",
   "metadata": {},
   "outputs": [],
   "source": [
    "export_rows = selected_rows.copy()\n",
    "for column in amount_columns:\n",
    "    export_rows[column] = export_rows[column].map(lambda value: f\"{value:.2f}\")\n",
    "\n",
    "print(export_rows.to_string(index=False))\n",
    "print(\"导出列类型：\", export_rows[\"2022年度\"].dtype)\n",
    "print(\"计算列类型：\", selected_rows[\"2022年度\"].dtype)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "abd41d2a",
   "metadata": {},
   "source": [
    "### 课堂练习（学生填写）\n",
    "\n",
    "根据刚才的样本代码独立补全下方空位；先预测结果，完成后再取消注释运行。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c0a34fba",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 目标：筛选两个现金流量项目，并生成固定两位的导出副本。\n",
    "# practice_cashflow = pd.DataFrame({\n",
    "#     \"项目\": [\n",
    "#         \" 加：期初现金及现金等价物余额 \",\n",
    "#         \"五、现金及现金等价物净增加额\",\n",
    "#         \"其他项目\",\n",
    "#     ],\n",
    "#     \"2022年度\": [6.2, 3.456, 8.0],\n",
    "#     \"2021年度\": [None, 2.0, 7.0],\n",
    "# })\n",
    "# targets = [                 # 请填写两个目标项目\n",
    "# ]\n",
    "# practice_cashflow[\"项目\"] = # 清理整列\n",
    "# selected =                  # 使用 isin(targets) 筛选并 copy\n",
    "# amount_columns = [\"2022年度\", \"2021年度\"]\n",
    "# selected[amount_columns] =  # fillna(0).round(2)\n",
    "# export_rows = selected.copy()\n",
    "# for column in amount_columns:\n",
    "#     export_rows[column] =   # 固定两位文本\n",
    "# print(export_rows.to_string(index=False))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ea44fe05",
   "metadata": {},
   "source": [
    "## 16. 网页采集中的字符串只需理解数据流\n",
    "\n",
    "`response.text` 得到 HTML 字符串，BeautifulSoup 再把它解析成可查询的节点树。字符串章节不需要展开网络协议或 HTML 算法。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "31ea588b",
   "metadata": {},
   "source": [
    "### 代码讲解\n",
    "\n",
    "\n",
    "response.text 是 requests 响应对象中的 HTML 字符串。字符串只负责承载网页文本；\n",
    "BeautifulSoup 或 pandas 再负责结构化解析。本章只需看懂这条数据流。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "904b92f7",
   "metadata": {},
   "source": [
    "### 课堂演示（样本代码）\n",
    "\n",
    "先由教师逐行解释输入、处理和输出，再运行下方样本代码。运行后修改一个值，\n",
    "观察结果是否符合刚才的预测。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "35b7dd88",
   "metadata": {},
   "outputs": [],
   "source": [
    "html_text = \"<table><tr><td>支付宝</td></tr></table>\"\n",
    "print(type(html_text))\n",
    "print(\"包含 table 标签：\", \"<table>\" in html_text)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e10bad8f",
   "metadata": {},
   "source": [
    "### 课堂练习（学生填写）\n",
    "\n",
    "根据刚才的样本代码独立补全下方空位；先预测结果，完成后再取消注释运行。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fc5da78e",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 目标：判断 HTML 文本是否包含表格标签，并替换单元格内容。\n",
    "# html_text = \"<table><tr><td>微信</td></tr></table>\"\n",
    "# has_table =                  # 使用 in 判断 \"<table>\"\n",
    "# changed_html =               # 把 \"微信\" 替换为 \"支付宝\"\n",
    "# print(has_table)\n",
    "# print(changed_html)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "948553b3",
   "metadata": {},
   "source": [
    "## 17. 本章哪些内容不作为必做\n",
    "\n",
    "- 旧式 `%` 格式化：不要求书写；\n",
    "- `find()`：知道能查找即可，真题优先用比较、`in` 或 pandas 筛选；\n",
    "- `upper()`、`lower()`、`capitalize()`、`title()`：按需查阅；\n",
    "- `center()`、`ljust()`、`rjust()`：不进入竞赛训练；\n",
    "- 地区时间转换器、名片、文字排版：不安排综合作业。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "762cacea",
   "metadata": {},
   "source": [
    "## 18. 离场检验\n",
    "\n",
    "- 为什么 `\"女 \" == \"女\"` 为假？如何诊断？\n",
    "- `round(12.3, 2)` 与 `f\"{12.3:.2f}\"` 有什么不同？\n",
    "- 为什么不能先把占比变成百分比字符串再取最大值？\n",
    "- 一个字符串和一整列文本分别怎样删除两端空白？\n",
    "- 科图和青娅微案例的处理顺序是什么？"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "vix",
   "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.11.8"
  },
  "updated_for": "第10、11节补充独立的演示、讲解与练习"
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
