pandas数据处理实战:从环境搭建到清洗分析全流程 1. 为什么我坚持用 pandas 做数据处理——一个十年老手的坦白你可能已经听过一百遍“pandas 是 Python 数据分析的基石”但这句话背后的真实分量只有当你在凌晨两点对着一份 237 列、480 万行的销售日志发呆而隔壁组还在 Excel 里手动筛选、复制粘贴、反复刷新透视表时才真正体会得到。这不是一句营销口号而是一套被工业级场景千锤百炼出来的、极度贴近人类直觉的数据操作范式。它最核心的魔法不在于能算多快而在于它把“我想看年龄大于 35 岁、来自华东地区、上季度销售额超 50 万的客户名单”这种自然语言几乎原封不动地翻译成了df[(df[age] 35) (df[region] 华东) (df[q3_sales] 500000)]这样一行代码。没有抽象的循环没有冗长的条件嵌套就是你脑子里想的那句话。我第一次接触 pandas 是在 2014 年当时还在一家传统制造业做 ERP 数据对接。每天要从 Oracle 数据库导出十几张表再用 VBA 脚本把它们拼成一张大宽表最后导入 BI 工具。一个需求改三次脚本就要重写三遍每次运行都要等十分钟。后来我试着用刚学的 pandas 重写核心逻辑就三行pd.read_sql()读取pd.merge()关联.groupby().agg()汇总。整个流程压缩到 47 秒而且改需求只需要动一两个参数。那一刻我才明白pandas 的本质不是“又一个 Python 库”而是一种全新的、以“数据本身”为中心的思考方式。它强迫你去理解数据的结构DataFrame 的行列、索引、类型而不是把数据当成一堆需要被“搬运”的字节流。它的.loc[]和.iloc[]不是语法糖而是两种截然不同的世界观.loc[]是“按名字找人”.iloc[]是“按座位号找人”。你在写代码时必须先决定自己是在和数据的“身份”对话还是在和它的“位置”对话。这种强制性的思维训练恰恰是新手最容易忽略、也最该掌握的底层心法。这篇文章就是我过去十年在金融风控、电商运营、医疗数据分析等多个一线战场踩坑、复盘、再优化后浓缩出来的一份“生存指南”。它不讲那些教科书式的定义比如“DataFrame 是二维的、大小可变的、异构的表格数据结构”。我只告诉你在真实世界里当你面对一份从财务系统导出的 CSV第一件事不是急着.read_csv()而是先用文本编辑器打开它看看它的分隔符到底是逗号、制表符还是那个该死的分号当你发现.describe()输出里某个数值列的count明显少于shape[0]这说明有缺失值但更关键的是你要立刻问自己这个缺失是“数据没录”还是“这个字段对这条记录根本不适用”前者可能要填均值后者可能得填一个特殊的标记值比如 -999。这些细节决定了你的分析结论是金玉其外还是经得起推敲。所以别把它当教程就当是一个老同事在你开始动手前把你可能撞上的第一堵墙提前给你画了出来。2. 从零搭建环境安装、验证与避坑实录2.1 安装不是终点验证才是起点安装 pandas 看似简单pip install pandas一行命令搞定。但在我经手的上百个项目中有超过 30% 的“pandas 报错”根源都出在安装环节。最常见的陷阱不是装不上而是装错了版本或者装在了错误的 Python 环境里。我见过最离谱的一次是某位同事在公司内网服务器上用系统自带的 Python 2.7早已停止维护执行pip install pandas结果 pip 自动降级安装了一个 2015 年的老版本导致所有.assign()和链式操作全部报错。所以我的第一条铁律是永远先确认你的 Python 版本和目标环境。# 第一步确认你正在使用的 Python 解释器路径和版本 which python python --version # 第二步确认 pip 对应的 Python 版本非常重要 pip --version # 输出类似pip 23.1.2 from /Users/xxx/miniconda3/envs/myproject/lib/python3.9/site-packages/pip (python 3.9) # 这里的 python 3.9 就是你 pip 正在服务的 Python 版本如果你用的是 Anaconda 或 Miniconda强烈建议优先使用conda安装。因为 conda 不仅管理 Python 包还管理底层的 C/C 编译器和 BLAS 数学库这对 pandas 的性能至关重要。pip在安装时有时会下载预编译的 wheel 文件但这些文件可能没有针对你的 CPU 进行最优配置比如未启用 AVX2 指令集而 conda 的包通常是经过严格测试和优化的。# 推荐的 conda 安装方式创建一个干净的环境 conda create -n pandas_env python3.10 conda activate pandas_env conda install pandas numpy matplotlib seaborn scikit-learn提示不要在 base 环境里直接安装所有包。为每个项目创建独立的 conda 环境是避免未来“包冲突地狱”的唯一可靠方法。一个环境里 pandas 1.5另一个环境里 pandas 2.0互不干扰。2.2 验证安装三步走缺一不可装完之后绝不能只跑import pandas as pd就算完事。我有一套固定的“三步验证法”每次新环境必做第一步版本与兼容性检查import pandas as pd import numpy as np print(fpandas version: {pd.__version__}) print(fnumpy version: {np.__version__}) # 检查 pandas 是否能正确识别 numpy 的版本兼容性 # 如果这里报错说明 numpy 版本太低或太高需要降级或升级 pd._testing.assert_produces_warning() # 这是一个内部函数用于触发警告检查第二步核心功能压力测试光能 import 不代表能用。我习惯用一个极小的、但覆盖了最常用操作的数据集来“热身”# 创建一个 3 行 4 列的测试 DataFrame df_test pd.DataFrame({ A: [1, 2, 3], B: [x, y, z], C: [1.1, 2.2, 3.3], D: [True, False, True] }) # 测试最核心的五种操作 print(1. 查看前两行:, df_test.head(2)) print(2. 数据类型:, df_test.dtypes) print(3. 基础统计:, df_test.describe(includeall)) print(4. 条件筛选:, df_test[df_test[A] 1]) print(5. 新增列:, df_test.assign(Edf_test[A] * 2))如果这五步都顺利通过说明你的 pandas 核心引擎是健康的。第三步I/O 功能实测最容易被忽略很多问题出在读写文件上。我一定会立刻测试本地 CSV 的读写# 写入一个临时 CSV df_test.to_csv(test_pandas_io.csv, indexFalse) # 立刻读取回来并对比 df_read pd.read_csv(test_pandas_io.csv) print(读写一致性检查:, df_test.equals(df_read)) # 清理临时文件 import os os.remove(test_pandas_io.csv)注意df.equals()比更严格它会检查索引、数据类型、NaN 的处理方式等所有细节。如果这里返回False哪怕只是因为to_csv默认把整数写成了浮点数如1.0也意味着你的 I/O 链路存在隐患必须深挖原因。2.3 常见安装失败场景与终极解决方案现象根本原因我的实战解决方案ERROR: Could not find a version that satisfies the requirement pandaspip 版本过旧无法解析新的 PyPI 包索引python -m pip install --upgrade pip然后重试ImportError: DLL load failed(Windows)缺少 Microsoft Visual C Redistributable下载并安装最新版 Microsoft Visual C Redistributable for Visual StudioModuleNotFoundError: No module named pandas._libs.skiplistpandas 与 numpy 版本严重不匹配conda install pandas2.0.3 numpy1.24.3用 conda 锁定版本组合pandas安装成功但pd.read_excel()报错ImportError: Missing optional dependency openpyxlpandas 的 Excel 支持是插件化的需要额外安装pip install openpyxl xlrd注意xlrd 2.0 只支持 xlsxlsx 请用 openpyxl最后一个血泪教训永远不要在生产服务器上用pip install --upgrade pandas。这无异于在高速公路上给汽车换轮胎。正确的做法是在开发机上用pip freeze requirements.txt固化所有依赖版本然后在服务器上用pip install -r requirements.txt精确还原。一次意外的版本升级曾让我负责的一个实时风控模型停摆了 6 小时只因为新版本 pandas 对datetime64类型的默认时区处理逻辑变了。3. 数据导入从文件到 DataFrame 的全链路解析3.1 为什么read_csv()是你最该精通的函数在 pandas 的所有 I/O 函数中read_csv()的使用频率远超其他所有函数之和。它之所以如此重要是因为它不仅是“读取 CSV”更是你与数据进行第一次深度对话的“谈判桌”。CSV 文件看似简单实则暗藏玄机编码格式UTF-8, GBK, ISO-8859-1、分隔符逗号、分号、制表符、甚至竖线|、引号规则是否用双引号包裹字段、缺失值标记NULL,N/A,?, 空字符串、以及最致命的——头部信息的混乱。一个标准的 CSV 文件第一行应该是列名但现实中的业务系统导出文件常常是前 3 行都是无用的标题、单位、备注真正的列名在第 4 行。因此read_csv()的每一个参数都是为了解决一个具体的、真实的痛点。我们来逐个拆解# 这是一个我在处理某银行交易流水时的真实配置 df pd.read_csv( transaction_202310.csv, encodinggbk, # 关键银行系统导出的中文文件99% 是 gbk 编码 sep|, # 分隔符是竖线不是逗号 skiprows3, # 跳过前 3 行无用的标题 header0, # 第 0 行即跳过后的第一行作为列名 na_values[NULL, N/A, ?], # 明确告诉 pandas哪些字符串应该被识别为 NaN keep_default_naTrue, # 允许 pandas 同时识别它自己的默认缺失值如空格 dtype{ # 强制指定列的数据类型防止自动推断出错 account_id: string, # 账户号是字符串绝不能是 int否则会丢失前导零 amount: float64, # 金额必须是浮点数 trans_date: string # 日期先读成字符串后续再用 pd.to_datetime 转换更可控 } )注意dtype参数是性能和准确性的双重保障。如果不指定pandas 会尝试自动推断对于一列包含123,456,abc的混合数据它可能会推断为object类型这会导致后续所有数值计算如.sum()全部失败。而string类型pandas 1.0 引入是专为文本设计的比object更安全、内存占用更低。3.2 处理“非标准”Excel多工作表、复杂表头与合并单元格Excel 是业务部门的最爱也是数据工程师的噩梦。read_excel()函数的强大体现在它对 Excel “不规范”特性的包容上。场景一多工作表的智能处理业务部门发来的 Excel往往一个文件里有“汇总表”、“明细表”、“参数表”三个 Sheet。你不可能每次都手动指定sheet_name明细表。我的做法是先用pd.ExcelFile对象探查# 先加载 Excel 文件对象不立即读取数据 excel_file pd.ExcelFile(report_q3.xlsx) # 查看所有工作表名称 print(所有工作表:, excel_file.sheet_names) # 批量读取所有工作表并用字典存储 all_sheets {sheet_name: excel_file.parse(sheet_name) for sheet_name in excel_file.sheet_names} # 或者只读取包含特定关键词的工作表 detail_sheet [name for name in excel_file.sheet_names if detail in name.lower()] if detail_sheet: df_detail excel_file.parse(detail_sheet[0])场景二应对“花式”表头Excel 里常见的“合并单元格表头”会让read_csv()直接崩溃。read_excel()提供了header和skiprows的组合拳# 假设表头跨越了第 1 行和第 2 行且第 1 行是大类如“销售数据”第 2 行是具体字段如“订单号”、“客户名” # 我们可以将前两行都作为 MultiIndex 的层级 df pd.read_excel( sales_report.xlsx, header[0, 1], # 将第 0 行和第 1 行都作为列索引 skiprows2 # 跳过前两行让数据从第 3 行开始 ) # 读取后列名会变成一个元组如 (销售数据, 订单号), (销售数据, 客户名) # 可以用以下方式扁平化列名 df.columns [_.join(col).strip() for col in df.columns.values]场景三处理“脏”数据——空行、注释行、分页符业务 Excel 里常有“此页数据截止至2023-10-31”这样的注释行或者每 50 行就有一个空行作为分页符。read_excel()的skipfooter和nrows参数在这里大显身手# 读取前 1000 行跳过最后 5 行通常是总结、签名、页脚 df pd.read_excel(data.xlsx, nrows1000, skipfooter5) # 或者用 comment 参数跳过以特定字符开头的行如 # df pd.read_excel(data.xlsx, comment#)3.3 从数据库和 API 获取数据连接池与分块读取的艺术当数据量达到百万、千万级别时read_sql()就不再是简单的“一条 SQL 语句”了。一次性拉取所有数据不仅会耗尽内存还会让数据库连接长时间占用影响其他业务。方案一分块读取Chunking这是处理大数据集的黄金法则。read_sql()的chunksize参数会返回一个TextFileReader对象你可以像迭代器一样逐块处理# 从 MySQL 数据库读取用户行为日志每次只读 10000 条 from sqlalchemy import create_engine engine create_engine(mysqlpymysql://user:passhost:3306/dbname) # chunksize10000 返回一个可迭代对象 for chunk in pd.read_sql(SELECT * FROM user_log WHERE dt20231001, engine, chunksize10000): # 对每一块数据进行清洗、转换 cleaned_chunk chunk.dropna(subset[user_id]) # 然后保存到本地 CSV或直接写入另一个数据库 cleaned_chunk.to_csv(cleaned_log_20231001.csv, modea, headerFalse, indexFalse)方案二SQL 层面的优化与其在 Python 里用.query()做二次过滤不如把过滤逻辑下推到数据库# ❌ 低效先拉取所有数据再在内存里过滤 df_all pd.read_sql(SELECT * FROM orders, engine) df_filtered df_all[df_all[status] completed] # ✅ 高效让数据库只返回你需要的数据 df_filtered pd.read_sql(SELECT * FROM orders WHERE status completed, engine)方案三API 数据的健壮性处理调用 REST API 时网络不稳定是常态。read_json()本身不带重试机制必须自己封装import requests import time def robust_read_json(url, max_retries3, backoff_factor1): for attempt in range(max_retries): try: response requests.get(url, timeout30) response.raise_for_status() # 检查 HTTP 状态码 return pd.read_json(response.text) except (requests.RequestException, ValueError) as e: if attempt max_retries - 1: raise e time.sleep(backoff_factor * (2 ** attempt)) # 指数退避 # 使用 df_api robust_read_json(https://api.example.com/data.json)4. 数据探索与理解超越.head()和.describe()的深度洞察4.1.info()你的数据健康报告单.info()是我打开任何新数据集后的第一个动作它提供的信息远比.head()更有价值。它是一份关于数据“体质”的全面体检报告。df.info(verboseTrue, show_countsTrue, memory_usagedeep)这个命令的输出每一行都值得深究RangeIndex: 768 entries, 0 to 767告诉我数据有多少行索引范围是否连续。如果索引是0, 1, 2, ..., 767那是健康的如果是0, 2, 4, 6, ...说明中间有数据被删掉了这会影响.iloc[]的使用。Data columns (total 9 columns):列的总数。# Column Non-Null Count Dtype这是核心Non-Null Count告诉你每列有多少非空值。如果一列的Non-Null Count是764而总行数是768那就明确告诉你有 4 个缺失值。Dtype告诉你 pandas 认为这一列是什么类型。object类型通常意味着是字符串但也可能是混合类型比如一列里既有数字又有文字这往往是数据质量的红灯。memory usage: 54.1 KB内存占用。如果一个只有 1000 行的 DataFrame 占用了 10MB那基本可以断定有列被错误地识别为了object比如本该是int的 ID 列因为混入了一个字母变成了object导致内存爆炸。实操心得我习惯把.info()的输出保存为一个 Markdown 表格作为项目文档的开篇。这不仅是给自己看更是给后续接手的同事一个清晰的数据快照。它比任何文字描述都更客观、更准确。4.2.describe()的进阶用法不只是“平均数”.describe()是初学者的最爱但它常被用得过于浅薄。它的真正威力在于其高度的可定制性。定制 1聚焦特定数据类型# 只看数值型列的统计 df.describe() # 只看类别型列object的统计频次、唯一值数量等 df.describe(include[object]) # 只看整数型列 df.describe(include[int]) # 同时看数值和类别 df.describe(includeall)定制 2自定义分位数.describe()默认显示 25%, 50%(中位数), 75% 分位数。但在风控场景中我更关心 1%, 5%, 95%, 99% 这些极端分位数它们能揭示数据的长尾风险# 获取 1%, 5%, 95%, 99% 分位数 df.describe(percentiles[0.01, 0.05, 0.95, 0.99])定制 3转置与格式化原始.describe()的输出是“行是统计指标列是字段”这在列数很多时很难阅读。转置后变成“行是字段列是统计指标”一目了然# 转置让字段成为行索引 desc_transposed df.describe().T # 还可以添加一列显示该字段的缺失率 desc_transposed[missing_rate] df.isnull().mean() # 按缺失率排序找出问题最大的列 desc_transposed.sort_values(missing_rate, ascendingFalse)4.3 发现隐藏的模式.value_counts()与.nunique()的组合技.value_counts()常被用来统计分类变量的频次但它配合.nunique()能揭示数据的“纯净度”。# 统计 Outcome 列的分布 print(df[Outcome].value_counts()) # 输出1 268 # 0 500 # 但更重要的是检查这个列的唯一值数量 print(fOutcome 列的唯一值数量: {df[Outcome].nunique()}) # 输出2 # 如果 nunique() 是 3而 value_counts() 显示有 1, 0, Unknown这就暴露了一个数据录入问题更进一步我们可以用.value_counts()来诊断数据质量问题# 检查 Pregnancies 列看是否有异常值比如怀孕次数为 -1或 20 preg_counts df[Pregnancies].value_counts().sort_index() print(preg_counts.head(10)) # 看最小的几个值 print(preg_counts.tail(10)) # 看最大的几个值 # 如果发现 preg_counts[-1] 0说明有负数这显然是错误数据需要清洗4.4 可视化辅助探索用seaborn快速定位问题文字和数字是冰冷的图表是温暖的。我从不单独依赖.describe()一定会配上快速可视化import seaborn as sns import matplotlib.pyplot as plt # 设置中文字体避免乱码 plt.rcParams[font.sans-serif] [SimHei, Arial Unicode MS] plt.rcParams[axes.unicode_minus] False # 1. 数值列的分布直方图 fig, axes plt.subplots(2, 2, figsize(12, 8)) sns.histplot(df[Age], kdeTrue, axaxes[0, 0]) sns.histplot(df[Glucose], kdeTrue, axaxes[0, 1]) sns.histplot(df[BMI], kdeTrue, axaxes[1, 0]) sns.histplot(df[DiabetesPedigreeFunction], kdeTrue, axaxes[1, 1]) plt.suptitle(核心数值特征分布) plt.tight_layout() plt.show() # 2. 类别列的条形图 plt.figure(figsize(8, 4)) sns.countplot(datadf, xOutcome) plt.title(糖尿病诊断结果分布) plt.show() # 3. 散点图矩阵看变量间关系 sns.pairplot(df, hueOutcome, vars[Age, Glucose, BMI], plot_kws{alpha:0.6}) plt.suptitle(核心变量关系散点图, y1.02) plt.show()这些图能在几秒钟内告诉你Age是否有明显的右偏老年人更多Glucose是否有大量集中在 0 的异常值可能代表未检测Outcome的分布是否严重不均衡这会影响后续建模这些洞察是任何.describe()输出都无法替代的。5. 数据清洗一场与“脏数据”的持久战5.1 缺失值不是“删”或“填”而是“理解”处理缺失值是数据清洗的第一道关卡。但新手常犯的错误是把它当成一个技术问题“用.dropna()还是.fillna()” 而资深从业者知道这是一个业务理解问题。步骤一分类缺失值缺失值不是同质的。我习惯将其分为三类结构性缺失Structural Missing这个字段对这条记录天生就不适用。例如一个“婚姻状况”字段对于一个 15 岁的用户填“未婚”是错的“未知”也不够好应该用一个业务上明确的占位符如N/ANot Applicable。信息性缺失Informational Missing这个字段本该有值但因为各种原因录入遗漏、系统故障没录上。这才是.fillna()的主战场。随机性缺失Random Missing纯粹的随机丢失没有业务含义。这种情况.dropna()可能是合理的。步骤二选择填充策略一旦确定是“信息性缺失”填什么就至关重要均值/中位数/众数适用于数值型/类别型字段且缺失比例不高5%。前向/后向填充ffill/bfill适用于时间序列数据。例如股票价格的缺失用前一天的价格填充比用全样本均值更合理。基于业务规则的填充这是最高级的技巧。例如在电商数据中“用户等级”字段缺失但我们可以根据其“历史总消费额”来反推一个合理的等级。# 示例用业务规则填充 # 假设我们有 total_spent 列想据此填充 user_tier def fill_user_tier(row): if pd.isna(row[user_tier]): if row[total_spent] 100000: return VIP elif row[total_spent] 10000: return Gold else: return Standard else: return row[user_tier] df[user_tier] df.apply(fill_user_tier, axis1)5.2 重复值不仅仅是.drop_duplicates().drop_duplicates()是一个强大的工具但它的默认行为keepfirst有时会掩盖真相。在金融交易数据中两条完全相同的记录可能意味着一笔交易被重复记账了这是严重的业务风险不能简单删除了事。我的四步排查法识别df.duplicated().sum()看总共有多少重复行。定位df[df.duplicated(keepFalse)]找出所有重复的行包括第一次出现的。分析对这些重复行按业务逻辑分组看它们是“完全一致”真重复还是“部分一致”如只有时间戳不同但金额、账户都一样这可能是系统重发。决策是删除、合并如将重复的金额相加还是标记为待人工审核# 找出所有重复的交易记录基于关键业务字段 duplicate_mask df.duplicated( subset[order_id, account_id, amount, trans_time], keepFalse ) duplicates df[duplicate_mask].sort_values([order_id, trans_time]) # 检查这些重复记录的时间差 duplicates[time_diff] duplicates.groupby(order_id)[trans_time].diff().dt.seconds # 如果 time_diff 60 秒很可能是系统重发可以保留第一条删除其余5.3 数据类型转换从object到category的性能飞跃pandas 中object类型是内存和性能的黑洞。一个包含 100 万个“省份”名称的列如果存为object会为每个字符串单独分配内存如果存为category则只存储一个唯一的字符串列表如[北京, 上海, 广东, ...]然后用一个整数数组如[0, 1, 2, ...]来引用它。内存占用能减少 80% 以上且.groupby()等操作速度提升数倍。# 将 province 列转换为 category df[province] df[province].astype(category) # 查看 category 的构成 print(df[province].cat.categories) print(df[province].cat.codes) # 查看底层的整数编码 # 对于有序类别如 Low, Medium, High可以指定顺序 df[risk_level] df[risk_level].astype(pd.CategoricalDtype( categories[Low, Medium, High], orderedTrue ))5.4 字符串清洗.str访问器的魔法pandas 的.str访问器是处理文本数据的瑞士军刀。它让原本需要for循环和正则表达式的操作变得向量化、极快。# 假设 address 列有很多脏数据前后空格、全角空格、多余换行符 df[address_clean] ( df[address] .str.strip() # 去除首尾空格 .str.replace(r\s, , regexTrue) # 将所有连续空白字符包括\t, \n替换为单个空格 .str.replace( , ) # 替换全角空格中文常见 .str.upper() # 统一为大写便于后续匹配 ) # 提取手机号中的数字去掉括号、横线等 df[phone_digits] df[phone].str.extract(r(\d{11})) # 提取 11 位数字 # 判断地址是否包含“市”或“区” df[has_district] df[address_clean].str.contains(市|区, naFalse)6. 数据操作与分析从筛选到聚合的全流程6.1 筛选的三种境界布尔索引、.query()与.loc[]筛选是数据操作的灵魂。pandas 提供了多种方式它们各有千秋。境界一布尔索引基础# 最基础的方式 mask (df[Age] 30) (df[Glucose] 140) result df[mask]优点直观易于理解。缺点当条件复杂时括号嵌套多可读性差。境界二.query()优雅# 用字符串表达式像写 SQL 一样 result df.query(Age 30 and Glucose 140 and Outcome 1)优点可读性极佳支持and/or/not和in操作如region in [华东, 华南]还能引用外部变量min_age。缺点性能略低于纯布尔索引但对于大多数场景差异可忽略。境界三.loc[]全能# .loc[] 是最强大、最灵活的因为它同时处理“行筛选”和“列选择” # 选择满足条件的行并只取其中几列 result df.loc[(df[Age] 30) (df[Glucose] 140), [Age, Glucose, Outcome]] # 用标签切片选择一个范围的行注意这里是闭区间 result df.loc[100:200, [Age, Glucose]] # 包含索引 100 和 200 的行 # 用标签列表选择不连续的行 result df.loc[[1, 5, 10, 100], :].loc[]的精髓在于“标签”它让你的操作与数据的“身份”绑定而不是与它的“位置”绑定。这使得代码更具可维护性。6.2.groupby()数据聚合的核武器.groupby()是 pandas 最强大的功能之一它实现了 SQL 中GROUP BY的全部能力并且更灵活。基础聚合# 按 Outcome 分组计算各列的均值 df.groupby(Outcome).mean() # 按多个列分组 df.groupby([Outcome, Pregnancies]).agg({ Age: mean, Glucose: [min, max, std], BMI: median })高级聚合自定义函数# 计算每个分组的“血糖超标率”Glucose 140 的比例 def glucose_over_rate(series): return (series 140).mean() df.groupby(Outcome)[Glucose