Pandas+NumPy+Matplotlib数据可视化实战:从清洗到可交付图表 1. 项目概述为什么用 Pandas NumPy Matplotlib 做数据可视化而不是“点几下鼠标”我带过三届数据分析方向的实习生每年都会遇到同一个问题刚学完 Excel 图表功能的学生第一次打开 Jupyter Notebook 看到import pandas as pd就皱眉——“老师Excel 不都能画吗为啥非得写代码”这个问题问得特别实在也特别关键。答案不是“因为高大上”而是因为真实业务场景里数据从来不是静止的、干净的、一次性的。你拿到的 CSV 文件可能有 27 个字段其中 8 个是空列3 个字段名带空格和中文括号时间戳格式混着2023-07-24和24/07/2023两种你想对比数学、物理、化学三科最高分但原始数据里学生 ID 是字符串、分数是浮点数、科目名还拼错了两次Phyiscs 和 Chemestry。这时候 Excel 的“插入柱状图”按钮连数据都读不全。这就是 Pandas、NumPy、Matplotlib 这套组合拳存在的根本理由它们不是为了替代 Excel而是为了接管 Excel 做不了、做不快、做不稳的那部分工作。Pandas 负责把乱麻一样的原始数据拧成一股绳——它能用一行df.dropna(threshlen(df)*0.8, axis1)直接干掉缺失值超过 20% 的列也能用df[subject].str.replace(r[^\w\s], , regexTrue).str.strip().str.title()一口气清洗掉所有科目名里的标点、空格和大小写混乱NumPy 则在底层提供高速数组运算比如你要算全班各科标准差np.std(df[[math, physics, chemistry]], axis0)返回的就是一个长度为 3 的数组比用 Python 循环逐个计算快 50 倍以上而 Matplotlib 的价值在于它不追求“一键美化”而是给你一把可调焦距的显微镜——你可以精确控制每根柱子的宽度width0.65、每个刻度标签的旋转角度plt.xticks(rotation45)、甚至某根柱子的颜色是否随数值动态变化colorplt.cm.viridis(norm(values))。这种控制力决定了你做的不是“一张图”而是“一个可复现、可审计、可嵌入自动化报告的数据叙事单元”。关键词“Data Visualization”在这里绝不是指“把数字变成图形”的动作而是指用视觉语言构建数据可信度的过程。当财务总监指着你做的柱状图问“这个‘最高分’是怎么定义的是原始分还是标准化后的 Z 分剔除缺考学生了吗”你能立刻翻出df.query(status present).groupby(subject)[score].max()这行代码再附上清洗逻辑的注释这就是可视化真正的护城河。它解决的不是“怎么画”而是“凭什么信”。所以这篇内容不会教你“五步做出炫酷动效”而是带你亲手搭一座桥从原始 CSV 文件落地到生成一张能放进周报、经得起追问、下次数据更新时改两行代码就能重跑的静态图表。接下来的所有步骤都基于一个真实可验证的起点——Medium 文章数据集它足够复杂12 列、近 8000 行又足够典型含文本、数值、时间、分类字段是我们检验这套工具链是否“真能干活”的最佳沙盒。2. 核心工具链深度拆解不是“三个库”而是一套精密协作的流水线很多人把 Pandas、NumPy、Matplotlib 当作并列的三个独立工具像厨房里并排摆着的菜刀、砧板、炒锅。这理解没错但不够准。它们实际构成了一条单向流动、职责分明、接口严丝合缝的数据处理流水线。理解这条流水线的内在逻辑比死记plt.show()和df.groupby()的语法重要十倍。下面我用自己调试过 37 次的真实案例一层层剥开它们的协作机制。2.1 NumPy流水线最底层的“传送带电机”NumPy 的核心是ndarrayN-dimensional array它不是简单的“列表升级版”而是内存中一块连续、同类型、可被 CPU 向量化指令直接操作的矩形数据块。举个例子你有一个包含 100 万个学生成绩的列表scores_list [85, 92, 78, ...]想给所有人加 5 分。用纯 Python 写scores_plus_5 [x 5 for x in scores_list] # 需要遍历 100 万次而 NumPy 只需import numpy as np scores_array np.array(scores_list) # 创建 ndarray scores_plus_5 scores_array 5 # 一行CPU 向量化指令自动并行处理背后发生了什么scores_array 5并不是 Python 的循环而是 NumPy 调用底层 C/Fortran 库将整个内存块交给 CPU 的 SIMD单指令多数据单元一条指令同时处理 4~8 个浮点数。这就是为什么np.mean()比statistics.mean()快 20 倍以上。在 Medium 数据集里claps列有近 8000 个数值当你执行df[claps].describe()时Pandas 内部其实是把这一列转成 NumPy 数组再调用np.nanmean(),np.nanstd()等函数——Pandas 自己并不计算它只是 NumPy 的“高级调度员”。提示永远检查你的数据类型。df[claps].dtype返回object说明这列里混了字符串比如1.2K或-。必须先用pd.to_numeric(df[claps], errorscoerce)强制转为数值errorscoerce会把无法转换的值设为NaN这是安全清洗的第一步。别跳过这步否则df[claps].max()会报错或返回错误结果。2.2 Pandas流水线中段的“智能分拣机器人”如果说 NumPy 是传送带电机Pandas 就是装在传送带上的机械臂——它不负责动力但能精准识别、抓取、重组每一个“数据包裹”。它的核心是DataFrame和Series本质是带标签label的 NumPy 数组容器。DataFrame的行索引index和列名columns就是它的“智能标签系统”。看这个经典操作# 原始数据学生ID、科目、分数三列 df pd.DataFrame({ student_id: [S001, S001, S002, S002], subject: [math, physics, math, chemistry], score: [85, 92, 78, 88] }) # 想知道每个学生各科最高分一行搞定 result df.groupby(student_id).agg({score: max})这里groupby(student_id)的本质是 Pandas 根据student_id列的值在内存中为每一组学生创建一个虚拟的、只包含该组数据的 NumPy 子数组然后对每个子数组调用np.max()。agg()方法里的{score: max}告诉它“只对 score 列做 max 运算”。整个过程无需你写循环Pandas 自动完成分组、切片、聚合、合并。在 Medium 数据集中publication列有 12 个不同值如Towards AI,Better Programmingvalue_counts()就是groupby(publication).size().sort_values(ascendingFalse)的语法糖它背后依然是 NumPy 的高效计数。注意axis0和axis1是新手最大陷阱。记住口诀“0 是竖着切按行操作1 是横着切按列操作”。df.drop(columns[url], axis1)是删列横着切掉一整列df.drop(index[0,1], axis0)是删行竖着切掉前两行。混淆 axis 会导致删错数据我曾因此重跑过 3 小时的 ETL 任务。2.3 Matplotlib流水线末端的“精密绘图仪”Matplotlib 的设计哲学是“显式优于隐式”这和 Pandas 的“魔法感”截然相反。它不提供df.plot_bar()这种黑箱方法虽然 Pandas 封装了但那是另一层封装而是要求你明确声明每一个绘图元素。它的核心是Figure画布和Axes坐标系对象。plt.subplots()返回的就是(fig, ax)元组fig, ax plt.subplots(figsize(10, 6)) # 创建 10x6 英寸画布和坐标系 ax.bar(x_data, y_data, width0.7, colorsteelblue) # 在 ax 上画柱子 ax.set_title(Top 5 Publications by Claps, fontsize14) # 设置标题 ax.set_ylabel(Total Claps, fontsize12) # 设置 Y 轴标签 plt.show() # 渲染显示关键在于ax对象。ax.bar()画的不是“一张图”而是ax这个坐标系里的一个BarContainer对象它由多个Rectangle实例组成。这意味着你可以单独修改某一根柱子bars ax.bar(x_data, y_data) bars[0].set_color(red) # 把第一根柱子变红 bars[2].set_hatch(///) # 给第三根柱子加斜线纹理这种粒度控制是 Excel 图表编辑器永远做不到的。在 Medium 数据集的“各平台文章数量占比”图中如果你发现Towards AI的柱子特别高想用不同颜色突出它只需bars[0].set_color(#FF6B6B)—— 而不是重新做图、手动调色。实操心得永远用plt.tight_layout()收尾。它会自动调整子图参数防止标题被截断、图例压住柱子。我见过太多人因为没加这行导出的 PNG 图里标题只剩一半被老板退回重做。3. 实操全流程从下载 Medium 数据集到生成 5 张可交付图表现在我们把理论变成键盘上的真实操作。以下所有代码我都已在 Python 3.9 Pandas 1.5.3 Matplotlib 3.7.1 环境下逐行验证。你不需要复制粘贴但需要理解每一步的“意图”和“为什么这样写”。我会用 Medium 数据集Kaggle 链接已提供作为唯一数据源全程不依赖任何外部 API 或网络请求确保 100% 可离线复现。3.1 环境准备与数据加载避开编码和路径的两大深坑第一步永远不是写import而是确认你的工作环境。我见过太多人卡在第一步pd.read_csv(data.csv)报错UnicodeDecodeError: utf-8 codec cant decode byte 0xff。这是因为 Windows 系统默认用GBK编码保存 CSV而 Pandas 默认用UTF-8读取。解决方案不是“百度搜报错”而是建立标准流程import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns # 后续美化用非必需但强烈推荐 # 步骤1用二进制模式读取文件头检测真实编码 with open(medium_articles.csv, rb) as f: raw f.read(10000) # 读前10KB encoding chardet.detect(raw)[encoding] # 需 pip install chardet print(f检测到编码: {encoding}) # 通常是 utf-8 或 gbk # 步骤2用检测到的编码加载同时指定低内存模式防爆内存 df pd.read_csv( medium_articles.csv, encodingencoding, low_memoryFalse, # 防止混合类型列报错 on_bad_linesskip # 跳过格式错误的行如引号不匹配 ) print(f数据形状: {df.shape}) print(f列名: {list(df.columns)})注意low_memoryFalse是关键。Pandas 默认low_memoryTrue会分块读取并猜测每列类型遇到claps列前 1000 行是数字、后 100 行是1.2K时会报DtypeWarning并强制设为object导致后续数值计算失败。设为False让它一次性读完再统一推断更稳定。3.2 数据清洗实战用 7 行代码解决 80% 的脏数据问题Medium 数据集原始有 12 列但真正用于可视化的核心列只有 5 个title,publication,claps,responses,reading_time。其他如url,author,date原始是字符串需要清洗。以下是我在生产环境反复打磨的清洗模板# 1. 删除完全空的列所有值都是 NaN df df.dropna(axis1, howall) # 2. 清洗 claps 列移除逗号、K/M 单位转为整数 def clean_claps(x): if pd.isna(x): return 0 x str(x).replace(,, ).strip() if x.endswith(K): return int(float(x[:-1]) * 1000) if x.endswith(M): return int(float(x[:-1]) * 1000000) return int(x) if x.isdigit() else 0 df[claps] df[claps].apply(clean_claps) # 3. 清洗 reading_time只保留数字单位统一为分钟 df[reading_time] pd.to_numeric( df[reading_time].str.extract(r(\d))[0], errorscoerce ).fillna(0).astype(int) # 4. 清洗 publication去除首尾空格统一小写修正拼写 df[publication] df[publication].str.strip().str.lower() # 手动映射常见拼写错误根据实际数据观察 correction_map { towards ai: towards ai, better programming: better programming, javascript in plain english: javascript in plain english, level up coding: level up coding } df[publication] df[publication].map(correction_map).fillna(df[publication]) # 5. 删除 publication 为空或 unknown 的行 df df[df[publication].notna() (df[publication] ! unknown)] print(f清洗后数据形状: {df.shape}) print(fpublication 唯一值: {df[publication].nunique()})这段代码的价值不在“做了什么”而在“为什么这样设计”。clean_claps函数用if/elif/else而不是正则是因为re.sub()在处理1.2K时容易误伤12Kreading_time用str.extract(r(\d))而不是str.split()[0]是因为有些行是5 min read有些是10 minutes正则更鲁棒publication的map()修正比replace()更安全避免批量替换ai导致towards ai变成towards 。这些细节都是踩过坑才长出来的肌肉记忆。3.3 图表一各平台文章数量占比环形图 百分比标注这是最基础的分布图但也是最容易犯错的。很多人直接df[publication].value_counts().plot.pie()结果得到一个挤在一起、标签重叠、看不出数字的饼图。专业做法是# 计算各平台文章数及占比 pub_counts df[publication].value_counts() total len(df) pub_percentages (pub_counts / total * 100).round(1) # 创建环形图donut chart比饼图更现代 fig, ax plt.subplots(figsize(10, 8)) colors plt.cm.Set3(np.linspace(0, 1, len(pub_counts))) # 自动配色 # 绘制环形图内半径0.4外半径0.8 wedges, texts, autotexts ax.pie( pub_counts.values, labelsNone, # 不显示标签用下方自定义 colorscolors, startangle90, wedgepropsdict(width0.4, edgecolorwhite), # 环形宽度 autopctlambda pct: f{pct:.1f}% if pct 5 else , # 5%才显示百分比 pctdistance0.75 # 百分比文字离圆心距离 ) # 手动添加图例右侧避免标签重叠 legend_labels [f{pub} ({count}) for pub, count in zip(pub_counts.index, pub_counts.values)] ax.legend( wedges, legend_labels, titlePublication (Count), loccenter left, bbox_to_anchor(1, 0, 0.5, 1), fontsize10 ) ax.set_title(Distribution of Articles by Publication, fontsize16, pad20) plt.tight_layout() plt.show()关键技巧autopctlambda pct: f{pct:.1f}% if pct 5 else 这行代码让图表“聪明”起来——只有占比超过 5% 的平台才显示百分比避免小平台标签挤成一团。bbox_to_anchor(1, 0, 0.5, 1)把图例放在图右侧是出版级图表的标准布局。实测下来这张图在 1080p 屏幕上清晰度远超默认饼图。3.4 图表二各平台总点赞数对比水平柱状图 排序目标是回答“哪个平台的文章总体最受读者欢迎”注意是“总点赞数”不是“平均点赞数”。这需要groupbysum# 按 publication 分组求 claps 总和并按降序排列 pub_claps_sum df.groupby(publication)[claps].sum().sort_values(ascendingFalse) # 创建水平柱状图barh更适合长文本标签 fig, ax plt.subplots(figsize(12, 8)) bars ax.barh( ypub_claps_sum.index, widthpub_claps_sum.values, colorplt.cm.viridis(np.linspace(0, 1, len(pub_claps_sum))), # 渐变色 height0.7 ) # 添加数值标签在柱子右侧 for i, (v, label) in enumerate(zip(pub_claps_sum.values, pub_claps_sum.index)): ax.text(v v*0.01, i, f{v:,}, vacenter, fontsize10) # 加千分位符 ax.set_xlabel(Total Claps, fontsize12) ax.set_title(Total Claps by Publication (Descending Order), fontsize16, pad20) ax.grid(axisx, alpha0.3) # 只显示 X 轴网格线更清爽 plt.tight_layout() plt.show()实操心得ax.text(v v*0.01, i, f{v:,})中的v*0.01是关键。如果直接写v 1000当某平台总点赞是 500 万时标签会离柱子太远用v*0.01是相对偏移保证所有标签距离一致。f{v:,}的,是 Python 3.6 的千分位符让1234567显示为1,234,567大幅提升可读性。3.5 图表三阅读时长 vs 点赞数散点图带趋势线探索变量间关系。直觉上读得越久点赞越多用散点图验证# 只取 reading_time 0 且 claps 0 的有效数据 valid_data df[(df[reading_time] 0) (df[claps] 0)].copy() # 计算趋势线线性回归 z np.polyfit(valid_data[reading_time], valid_data[claps], 1) p np.poly1d(z) # 创建散点图 fig, ax plt.subplots(figsize(10, 7)) scatter ax.scatter( xvalid_data[reading_time], yvalid_data[claps], cvalid_data[claps], # 颜色映射点赞数 cmapplasma, alpha0.6, s30 ) # 绘制趋势线 x_line np.linspace(valid_data[reading_time].min(), valid_data[reading_time].max(), 100) y_line p(x_line) ax.plot(x_line, y_line, r--, linewidth2, labelfTrend: y{z[0]:.0f}x{z[1]:.0f}) ax.set_xlabel(Reading Time (minutes), fontsize12) ax.set_ylabel(Claps, fontsize12) ax.set_title(Relationship between Reading Time and Claps, fontsize16, pad20) ax.legend() ax.grid(True, alpha0.3) plt.colorbar(scatter, axax, labelClaps) plt.tight_layout() plt.show()注意np.polyfit(x, y, 1)返回的是[斜率, 截距]np.poly1d(z)把它变成一个可调用的函数p(x)。趋势线方程y123x456直接写在图例里比单纯说“正相关”有力得多。cmapplasma比默认viridis在打印稿上对比度更高。3.6 图表四各平台平均点赞数分组柱状图 误差线回答“哪个平台的单篇文章质量平均点赞最高”这需要groupbyagg计算均值和标准差# 按 publication 分组计算 claps 的均值、标准差、计数 stats df.groupby(publication)[claps].agg([mean, std, count]).round(0) stats stats.sort_values(mean, ascendingFalse) # 按均值排序 # 创建柱状图带误差线标准差 fig, ax plt.subplots(figsize(12, 8)) bars ax.bar( xstats.index, heightstats[mean], yerrstats[std], # 误差线高度 capsize5, # 误差线两端横线长度 colorplt.cm.tab10(np.linspace(0, 1, len(stats))), alpha0.8 ) # 添加数值标签 for i, (mean_val, std_val) in enumerate(zip(stats[mean], stats[std])): ax.text(i, mean_val std_val 50, f{int(mean_val):,}, hacenter, vabottom, fontsize10) ax.set_xlabel(Publication, fontsize12) ax.set_ylabel(Average Claps per Article, fontsize12) ax.set_title(Average Claps by Publication (with Standard Deviation), fontsize16, pad20) ax.tick_params(axisx, rotation45) # X轴标签旋转45度防重叠 plt.tight_layout() plt.show()关键点yerrstats[std]添加了标准差作为误差线直观显示数据离散程度。capsize5让误差线两端有小横线这是学术图表规范。ax.text(i, mean_val std_val 50, ...)中的50是为了让标签在误差线上方避免遮挡。3.7 图表五时间序列热力图月度点赞趋势最后用热力图展示时间维度。Medium 数据集有date列字符串需解析为日期# 解析 date 列为 datetime并提取年月 df[date_parsed] pd.to_datetime(df[date], errorscoerce) df df.dropna(subset[date_parsed]) df[year_month] df[date_parsed].dt.to_period(M) # 转为 2023-01 格式 # 按年月和 publication 分组求 claps 总和 heatmap_data df.groupby([year_month, publication])[claps].sum().unstack(fill_value0) # 创建热力图 fig, ax plt.subplots(figsize(14, 10)) im ax.imshow( heatmap_data.values, cmapYlGnBu, aspectauto, interpolationnearest ) # 设置坐标轴标签 ax.set_xticks(np.arange(len(heatmap_data.columns))) ax.set_xticklabels(heatmap_data.columns, rotation45, haright) ax.set_yticks(np.arange(len(heatmap_data.index))) ax.set_yticklabels(heatmap_data.index.astype(str)) # 添加数值标签 for i in range(len(heatmap_data.index)): for j in range(len(heatmap_data.columns)): text ax.text(j, i, f{int(heatmap_data.iloc[i, j]):,}, hacenter, vacenter, colorw, fontsize8) ax.set_title(Monthly Claps Heatmap by Publication, fontsize16, pad20) ax.set_xlabel(Publication, fontsize12) ax.set_ylabel(Year-Month, fontsize12) plt.colorbar(im, axax, labelTotal Claps) plt.tight_layout() plt.show()技巧unstack(fill_value0)是关键。它把year_month作为行索引、publication作为列索引缺失的组合如2023-01没有Level Up Coding的文章自动填0避免热力图出现空白块。interpolationnearest防止图像模糊保持像素级锐利。4. 常见问题与排查技巧实录那些文档里不会写的“血泪经验”在带团队做数据可视化项目时我整理了一份《高频故障速查表》里面全是真实发生过的、让人拍桌的瞬间。以下是最常遇到的 5 类问题附带我的排查路径和终极解决方案。4.1 问题matplotlib图表中文显示为方块现象ax.set_title(各平台文章数量)渲染出来全是小方块。排查路径先确认系统字体fc-list :lang(zh)Linux/macOS或查看 Windows 字体文件夹检查 Matplotlib 配置plt.matplotlib.rcParams[font.sans-serif]最常见原因Matplotlib 默认字体不支持中文且未正确设置rcParams。终极方案一劳永逸# 在所有绘图代码前执行一次 plt.rcParams[font.sans-serif] [SimHei, Arial Unicode MS, DejaVu Sans] # Windows/macOS/Linux 通用 plt.rcParams[axes.unicode_minus] False # 解决负号显示为方块 # 验证plt.text(0.5, 0.5, 测试中文); plt.show()注意SimHei是 Windows 黑体Arial Unicode MS是 macOS 预装字体DejaVu Sans是 Linux 通用字体。按顺序列出Matplotlib 会自动选用第一个可用的。别用plt.rcParams[font.family] sans-serif这不够具体。4.2 问题pandasgroupby后agg报错KeyError: claps现象df.groupby(pub)[claps].agg(sum)报错KeyError: claps。排查路径print(df.columns.tolist())查看列名是否有空格或不可见字符print(repr(df.columns[0]))查看第一个列名的原始表示可能有\xa0不间断空格df.columns df.columns.str.strip()清洗列名。终极方案# 安全的列名清洗处理所有不可见字符 df.columns df.columns.str.replace(r[^\w\s], , regexTrue).str.strip() # 然后用 df.columns.get_loc(claps) 获取列位置避免字符串匹配失败4.3 问题matplotlib图表导出为 PDF 后字体模糊或丢失现象plt.savefig(chart.pdf)生成的 PDF 在 Adobe Reader 里放大后文字锯齿。排查路径检查是否用了plt.savefig(..., dpi300)—— DPI 对 PDF 无效确认是否启用了pgf后端LaTeX 渲染最常见原因PDF 查看器渲染引擎差异。终极方案出版级输出# 使用 pgf 后端需安装 LaTeX plt.rcParams.update({ text.usetex: True, font.family: serif, font.serif: [Computer Modern Roman], pgf.texsystem: pdflatex, pgf.preamble: r\usepackage[utf8]{inputenc}\usepackage[T1]{fontenc} }) # 然后 plt.savefig(chart.pgf) 或 plt.savefig(chart.pdf)如果不想装 LaTeX用plt.savefig(chart.svg)导出 SVG 格式矢量无限缩放且浏览器原生支持。4.4 问题numpy数组计算时出现nan传播结果全为nan现象np.mean(df[claps])返回nan即使df[claps].isna().sum()是 0。排查路径print(df[claps].dtype)—— 如果是object说明有非数值字符串print(df[claps].head().tolist())查看前几行原始值print(np.isnan(df[claps].values))看哪些位置是True。终极方案# 安全的数值转换三步走 df[claps_clean] pd.to_numeric(df[claps], errorscoerce) # 转 NaN df[claps_clean] df[claps_clean].fillna(0) # 填 0 df[claps_clean] df[claps_clean].astype(int) # 转 int # 然后用 df[claps_clean].mean() 计算4.5 问题Jupyter Notebook 中plt.show()不显示图表或显示两次现象运行单元格后无图或图下方多出Figure size ...文本。排查路径检查是否开启了%matplotlib inline应在 notebook 开头检查是否重复写了plt.show()最常见原因inline后端和widget后端冲突。终极方案# 在 notebook 第一个单元格执行 %matplotlib inline # 如果想交互式可缩放用 # %matplotlib widget # 需 pip install ipympl # 确保每个绘图单元格末尾只有 plt.show()不要有多余 print # 如果仍显示两次加一句 plt.close() plt.show() plt.close() # 关闭当前 figure释放内存5. 工程化进阶如何把“一次性的脚本”变成“可维护的分析模块”做到上面 5 张图你已经超越了 80% 的初学者。但真正的职业分水岭在于能否把临时脚本升级为可复用、可测试、可部署的分析模块。这不是炫技而是应对真实业务需求的必然选择——当市场部下周突然要“按作者维度分析点赞趋势”你不能重写一遍所有代码。5.1 构建可配置的数据管道Pipeline把清洗、分析、绘图拆成独立函数用配置字典驱动# config.py CONFIG { data_path: medium_articles.csv, encoding: utf-8, target_columns: [publication, claps, reading_time, date], clean_rules: { claps: {type: numeric, unit_map: {K: 1000, M: 1000000}}, reading_time: {type: numeric, regex: r(\d)} } } # pipeline.py class MediumAnalyzer: def __init__(self, config): self.config config self.df None def load_data(self): self.df pd.read_csv(self.config[data_path], encodingself.config[encoding]) def clean_data(self): for col, rule in self.config[clean_rules].items(): if rule[type] numeric: self.df[col] self._clean_numeric(self.df[col], rule) def _clean_numeric(self, series, rule): # 实现清洗逻辑... return