
1. 案例背景数据整理是图表生成的前提不少初学者拿到一份数据后第一反应就是直接画图结果要么报错要么图表内容与实际业务对不上。原因并不在绘图代码本身而在于数据没有经过整理。原始数据通常存在日期格式不统一、字段名混乱、分类值带空格、存在缺失值或重复记录等问题直接喂给绘图函数会出现柱状图高度异常、折线图横轴乱序、饼图占比合计不等于 100% 等现象。数据整理Data Wrangling指的是把杂乱数据转换为结构化、规范、便于分析和可视化的表格过程主要包括读取数据、检查数据质量、清洗缺失值和重复值、修正数据类型、重命名字段、分组聚合等环节。图表生成Chart Generation则是基于整理后的数据用柱状图、折线图、饼图、箱线图等图形展示规律和结论。两者是前后衔接的关系数据整理决定图表能否画出来也决定图表传递的信息是否可信。本文以一个销售订单数据集为例完整演示从 CSV 读取、数据清洗、字段加工、分组统计到生成 4 张不同图表的全过程。案例面向 Python 数据分析初学者也适用于有基础但希望规范操作流程的开发者。学完你能够独立完成一份“原始表 → 干净表 → 统计表 → 可视化图表”的分析链路。2. 环境准备与数据集设计2.1 运行环境与依赖安装本案例基于 Python 实现核心依赖为 pandas 和 matplotlib可选依赖为 seaborn。建议使用 Python 3.8 及以上版本pandas 与 matplotlib 的版本以当前常见稳定版本为准不同小版本之间的 API 差异不大。如果本地还没有安装依赖可以在终端中执行以下命令pip install pandas matplotlib安装完成后可以通过以下方式验证环境import pandas as pd import matplotlib.pyplot as plt print(pandas version:, pd.__version__) print(matplotlib version:, matplotlib.__version__)执行后能够输出版本号说明环境就绪。需要说明的是不同操作系统、不同 Python 版本下的版本号会有所差异只要 pandas 版本在 1.0 以上本文代码的绝大多数用法都可以正常执行。2.2 模拟数据文件准备为了演示方便我们不去刻意寻找真实企业数据而是先写一个小脚本生成一份带有一系列“脏数据”特征的销售订单表。这份表覆盖了常见的数据整理场景包括缺失值、重复记录、日期格式不一致、分类值前后带空格等情况。在项目目录下新建generate_data.py代码内容如下# 文件路径generate_data.py import random import pandas as pd from datetime import datetime, timedelta random.seed(42) categories { 手机数码: [智能手机, 蓝牙耳机, 智能手表, 移动电源], 家用电器: [电饭煲, 空气炸锅, 吸尘器, 电风扇], 服饰鞋包: [T恤, 运动鞋, 双肩包, 牛仔裤], 食品生鲜: [坚果礼盒, 进口牛奶, 咖啡豆, 新鲜水果], } records [] start_date datetime(2024, 1, 1) order_id 1001 for _ in range(120): day_offset random.randint(0, 181) order_date start_date timedelta(daysday_offset) category random.choice(list(categories.keys())) product random.choice(categories[category]) quantity random.randint(1, 10) price random.choice([99, 199, 299, 499, 699, 1299, 1999, 3999]) amount quantity * price # 模拟少量脏数据 if order_id % 37 0: quantity None if order_id % 41 0: amount None if order_id % 29 0: category category records.append([ ORD str(order_id), order_date.strftime(%Y-%m-%d), category, product, quantity, price, amount, ]) order_id 1 # 插入两条完全重复的记录 records.append(records[0]) records.append(records[1]) df pd.DataFrame(records, columns[ 订单编号, 日期, 商品类别, 商品名称, 销量, 单价, 销售额 ]) df.to_csv(sales_orders.csv, indexFalse, encodingutf-8-sig) print(数据已生成共, len(df), 条记录)运行该脚本会在当前目录生成sales_orders.csv文件。文件字段说明如下字段含义示例订单编号订单唯一标识ORD1001日期下单日期2024-01-13商品类别商品所属大类手机数码商品名称具体商品名智能手机销量该订单的商品数量2单价单个商品价格1999销售额订单总金额3998生成的数据中故意包含了缺失值、重复记录、类别字段两侧空白、日期字段仍为字符串等问题正好用于演示数据整理。你也可以用自己的业务数据替换sales_orders.csv整理思路保持不变。2.3 项目目录结构为了方便管理和后续扩展建议把脚本、数据和产物分开存放。案例目录结构参考如下data_analysis_case/ ├── generate_data.py # 生成模拟数据 ├── sales_orders.csv # 生成的原始数据 ├── data_process.py # 数据整理脚本 ├── chart_analysis.py # 图表生成脚本 └── output/ ├── sales_clean.csv # 清洗后的数据 ├── category_amount_bar.png ├── monthly_trend_line.png ├── category_quantity_pie.png └── price_boxplot.pngoutput目录需要手动创建或者由脚本通过os.makedirs自动创建。后续操作中我们按“先整理、后绘图”的顺序逐步完成。3. 数据整理核心操作拆解3.1 读取数据与初步检查读取 CSV 文件并快速了解数据规模、字段类型和缺失情况是一切整理工作的起点。# 文件路径data_process.py第 1 部分 import pandas as pd df pd.read_csv(sales_orders.csv, encodingutf-8-sig) print(数据形状, df.shape) print(\n前 5 行) print(df.head()) print(\n字段信息) print(df.info()) print(\n描述性统计) print(df.describe())运行结果可以得到以下关键信息共有多少行、多少列判断数据量级。每条字段是否为正确的数据类型比如销量应为整数却可能是浮点数。是否存在缺失值info()输出中Non-Null Count会明确显示每个字段非空数量。describe()只对数值列计算统计量可用于观察销售额、单价的量级和分布。这里有一个常见误区read_csv默认把日期读成字符串把缺失值读成NaN重复记录不会自动删除。因此读取之后必须根据业务需求做后续清洗。3.2 缺失值与重复值处理缺失值和重复值并不是直接删除就万事大吉。删除前要想清楚这一列缺失是否代表业务含义重复记录是录入重复还是本来就可能出现的多条订单本案例的模拟规则是部分订单缺失销量或销售额属于录入不完整第 0 行和第 1 行被完全复制了一遍属于重复记录。处理方式如下# 文件路径data_process.py第 2 部分 print(清洗前重复行数, df.duplicated().sum()) df df.drop_duplicates() print(清洗后行数, len(df)) print(缺失值统计) print(df.isnull().sum()) # 有缺失的销量和销售额按业务常理用中位数填充 df[销量] df[销量].fillna(df[销量].median()) df[销售额] df[销售额].fillna(df[销售额].median()) # 重新计算销售额保证 销量 × 单价 销售额 的一致性 df[销售额] df[销量] * df[单价] # 确认没有缺失 print(处理后的缺失值数量, df[销量].isnull().sum() df[销售额].isnull().sum())drop_duplicates()默认对所有列进行完全匹配判断如果想仅依据订单编号去重可以传入subset[订单编号]。本案例的重复记录是完全相同的两行因此默认参数即可。缺失值的填充策略需要结合实际业务。销售额缺失时最稳妥的做法不是简单填充而是利用“销售额 销量 × 单价”的公式重新计算。填充后再统一重算一遍可以保证字段间的逻辑关系自洽避免后续画趋势图时出现与业务常识矛盾的数据。3.3 数据清洗与类型转换商品类别字段在生成时人为加入了空格比如 手机数码 如果不处理分组时手机数码和 手机数码 会被看成两个不同的类别导致统计结果分裂。日期字段是字符串直接按年月聚合时也需要转换为 pandas 的日期时间类型。# 文件路径data_process.py第 3 部分 # 分类字段去掉两侧空格 df[商品类别] df[商品类别].str.strip() # 日期字符串转 datetime df[日期] pd.to_datetime(df[日期]) # 新增“月份”字段方便按月统计 df[月份] df[日期].dt.to_period(M).astype(str) # 商品名称也做一次清洗 df[商品名称] df[商品名称].str.strip() # 修正数值字段类型为整数 df[销量] df[销量].astype(int) df[销售额] df[销售额].astype(int) print(df.head()) print(\n处理后字段信息) print(df.info())这段代码做了四件事用str.strip()去除字符串两侧空格避免同义不同值导致的分组错误。用pd.to_datetime()把日期列转为标准日期类型这样后续可以按年、月、日灵活聚合。提取出月份字段其值为2024-01这样的格式比直接使用日期分组更直观。把销量、销售额从浮点数转为整数保证图表坐标轴刻度干净。这里强调一点字符串类型的数字和数值类型的数字在排序、求和时表现完全不同。如果发现字段类型是object但内容看起来是数字需要先排查是否存在1,299、199这类带格式的字符串否则直接astype(int)会抛异常。3.4 分组聚合生成统计口径图表表达的是统计结果不是原始明细。我们需要按业务问题确定“维度”和“度量”比如“各品类销售额”“每月销售额变化”“各品类销量占比”。pandas 的groupby是完成这类统计最常用的方法。# 文件路径data_process.py第 4 部分 # 各品类销售额汇总 category_amount ( df.groupby(商品类别)[销售额] .sum() .reset_index() .sort_values(销售额, ascendingFalse) ) print(各品类销售额) print(category_amount) # 各品类销量汇总 category_quantity ( df.groupby(商品类别)[销量] .sum() .reset_index() ) print(\n各品类销量) print(category_quantity) # 每月销售额趋势 monthly_trend ( df.groupby(月份)[销售额] .sum() .reset_index() ) print(\n每月销售额) print(monthly_trend) # 保存清洗后的数据 df.to_csv(output/sales_clean.csv, indexFalse, encodingutf-8-sig)groupby后的结果是一个带有分组索引的 Series 或 DataFrame直接用于绘图时索引反而容易引起困惑因此这里先用reset_index()把它转成普通表格。sort_values按销售额降序排列后续柱状图横轴就会自动按金额大小展示可读性更好。从数据整理的角度看清洗后的sales_clean.csv就是一份“干净数据表”而category_amount、monthly_trend则是“统计结果表”。建议在分析项目中明确区分原始数据、清洗数据和统计数据的产物避免反复修改造成流程混乱。4. 图表生成实战4.1 中文字体与画布基础设置matplotlib 默认字体对中文支持并不友好直接绘制中文标题时经常出现方块“口口口”需要显式指定中文字体。Windows 环境下常用SimHei或Microsoft YaHeimacOS 可以用Arial Unicode MS。不同机器可用字体不同最稳妥的设置方式如下# 文件路径chart_analysis.py第 1 部分 import matplotlib.pyplot as plt import pandas as pd plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, Arial Unicode MS] plt.rcParams[axes.unicode_minus] False # 解决负号显示为方块的问题 df_clean pd.read_csv(output/sales_clean.csv) df_clean[日期] pd.to_datetime(df_clean[日期])第一行设置字体列表matplotlib 会按顺序尝试使用第二行设置坐标轴负号正常显示。如果你发现字体仍然不对可以在matplotlib.font_manager中查询本机可用字体后替换名称。这里的df_clean直接复用上一节保存的清洗结果文件保证绘图数据与清洗数据完全一致。4.2 各品类销售额柱状图柱状图适合展示分类变量的数量差异。这里统计口径是“各商品类别的累计销售额”它是一个分组求和的结果。# 文件路径chart_analysis.py第 2 部分 category_amount ( df_clean.groupby(商品类别)[销售额] .sum() .reset_index() .sort_values(销售额, ascendingTrue) ) plt.figure(figsize(8, 5)) bars plt.bar( category_amount[商品类别], category_amount[销售额], color[#4C72B0, #55A868, #C44E52, #8172B2], ) # 在柱状图上添加数据标签 for bar in bars: height bar.get_height() plt.text( bar.get_x() bar.get_width() / 2, height 500, f{height:,.0f}, hacenter, vabottom, ) plt.title(2024 年上半年各品类销售额对比, fontsize14) plt.xlabel(商品类别) plt.ylabel(销售额元) plt.grid(axisy, linestyle--, alpha0.6) plt.tight_layout() plt.savefig(output/category_amount_bar.png, dpi150) plt.show()这里有一个细节排序时我们使用了ascendingTrue这样绘制出来的柱状图是按金额从小到大排列反过来ascendingFalse则是从大到小。具体使用哪种排序取决于你想让最大品类出现在右端还是左端。数据标签通过plt.text添加坐标取自柱体的 x 中心位置和高度height 500的作用是让文字略微高过柱顶避免文字被柱子遮挡。grid(axisy)只画水平网格线视觉上更干净alpha0.6则降低网格线亮度。4.3 月度销售趋势折线图折线图适合展示时间序列变化金字塔形的核心是横轴必须是按时间顺序排列的。如果直接对原始日期分组横轴可能因日期格式问题出现乱序因此我们在数据整理阶段已经生成了月份字段。# 文件路径chart_analysis.py第 3 部分 monthly_trend ( df_clean.groupby(月份)[销售额] .sum() .reset_index() ) plt.figure(figsize(10, 5)) plt.plot( monthly_trend[月份], monthly_trend[销售额], markero, linewidth2, color#4C72B0, ) # 突出最高点 max_idx monthly_trend[销售额].idxmax() plt.annotate( f峰值{monthly_trend[销售额][max_idx]:,} 元, xy(monthly_trend[月份][max_idx], monthly_trend[销售额][max_idx]), xytext(monthly_trend[月份][max_idx - 1], monthly_trend[销售额][max_idx] 5000), arrowpropsdict(arrowstyle-, colorgray), ) plt.title(2024 年 1-6 月销售额趋势, fontsize14) plt.xlabel(月份) plt.ylabel(销售额元) plt.grid(linestyle--, alpha0.5) plt.tight_layout() plt.savefig(output/monthly_trend_line.png, dpi150) plt.show()markero在每个数据点位置显示圆形标记方便观察具体数值点annotate用来在最高峰位置添加箭头注释让读者一眼看到趋势中的关键节点。如果在真实场景中月份数量很多可以增加plt.xticks(rotation45)旋转横轴标签防止文字重叠。4.4 销量占比饼图与价格分布箱线图饼图展示构成比例适合分类数量较少的场景。箱线图则展示数值分布可以看到不同品类的价格区间和中位数水平。# 文件路径chart_analysis.py第 4 部分 category_quantity ( df_clean.groupby(商品类别)[销量] .sum() .reset_index() ) plt.figure(figsize(7, 7)) plt.pie( category_quantity[销量], labelscategory_quantity[商品类别], autopct%.1f%%, startangle90, colors[#4C72B0, #55A868, #C44E52, #8172B2], ) plt.title(各品类销量占比, fontsize14) plt.tight_layout() plt.savefig(output/category_quantity_pie.png, dpi150) plt.show() # 不同品类单价分布箱线图 plt.figure(figsize(8, 5)) df_clean.boxplot(column单价, by商品类别, gridFalse) plt.title(各品类商品单价分布) plt.suptitle() plt.xlabel(商品类别) plt.ylabel(单价元) plt.tight_layout() plt.savefig(output/price_boxplot.png, dpi150) plt.show()饼图使用autopct%.1f%%在扇形内部显示保留一位小数的百分比startangle90让第一块扇形从 12 点方向开始。箱线图这里直接调用了 DataFrame 内置的boxplot方法by商品类别会自动按分类分组生成多个箱体。plt.suptitle()是为了去掉 pandas 自动添加的Box Plot grouped by 商品类别外层标题避免标题重复。关于箱线图要说明的是如果品类的单价跨度差异很大直接观察各类别箱体位置和上下须是比均值更稳健的方法因为均值容易被极端值拉动。5. 完整流程一键脚本5.1 合并文件路径实际项目中通常不希望每次都手动运行两个脚本更推荐把数据整理和图表生成合并为一个主脚本并且由脚本自动创建output目录。下面把前面所有功能整合成一个完整脚本。文件路径仍然放在项目根目录下。5.2 完整代码# 文件路径run_analysis.py import os import pandas as pd import matplotlib.pyplot as plt os.makedirs(output, exist_okTrue) plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, Arial Unicode MS] plt.rcParams[axes.unicode_minus] False # ---------- 1. 读取数据 ---------- df pd.read_csv(sales_orders.csv, encodingutf-8-sig) print(原始数据形状, df.shape) # ---------- 2. 数据清洗 ---------- df df.drop_duplicates() df[销量] df[销量].fillna(df[销量].median()) df[销售额] df[销售额].fillna(df[销售额].median()) df[销售额] df[销量] * df[单价] df[商品类别] df[商品类别].str.strip() df[商品名称] df[商品名称].str.strip() df[日期] pd.to_datetime(df[日期]) df[月份] df[日期].dt.to_period(M).astype(str) df[销量] df[销量].astype(int) df[销售额] df[销售额].astype(int) print(清洗后数据形状, df.shape) print(缺失值, df.isnull().sum().sum()) df.to_csv(output/sales_clean.csv, indexFalse, encodingutf-8-sig) # ---------- 3. 统计口径 ---------- category_amount ( df.groupby(商品类别)[销售额] .sum() .reset_index() .sort_values(销售额, ascendingTrue) ) monthly_trend df.groupby(月份)[销售额].sum().reset_index() category_quantity df.groupby(商品类别)[销量].sum().reset_index() # ---------- 4. 柱状图各品类销售额 ---------- plt.figure(figsize(8, 5)) bars plt.bar( category_amount[商品类别], category_amount[销售额], color[#4C72B0, #55A868, #C44E52, #8172B2], ) for bar in bars: height bar.get_height() plt.text( bar.get_x() bar.get_width() / 2, height 500, f{height:,.0f}, hacenter, vabottom, ) plt.title(各品类销售额对比, fontsize14) plt.xlabel(商品类别) plt.ylabel(销售额元) plt.grid(axisy, linestyle--, alpha0.6) plt.tight_layout() plt.savefig(output/category_amount_bar.png, dpi150) plt.close() # ---------- 5. 折线图月度趋势 ---------- plt.figure(figsize(10, 5)) plt.plot( monthly_trend[月份], monthly_trend[销售额], markero, linewidth2, color#4C72B0, ) max_idx monthly_trend[销售额].idxmax() plt.annotate( f峰值{monthly_trend[销售额][max_idx]:,} 元, xy(monthly_trend[月份][max_idx], monthly_trend[销售额][max_idx]), xytext(monthly_trend[月份][max_idx - 1], monthly_trend[销售额][max_idx] 5000), arrowpropsdict(arrowstyle-, colorgray), ) plt.title(月度销售额趋势, fontsize14) plt.xlabel(月份) plt.ylabel(销售额元) plt.grid(linestyle--, alpha0.5) plt.tight_layout() plt.savefig(output/monthly_trend_line.png, dpi150) plt.close() # ---------- 6. 饼图各品类销量占比 ---------- plt.figure(figsize(7, 7)) plt.pie( category_quantity[销量], labelscategory_quantity[商品类别], autopct%.1f%%, startangle90, colors[#4C72B0, #55A868, #C44E52, #8172B2], ) plt.title(各品类销量占比, fontsize14) plt.tight_layout() plt.savefig(output/category_quantity_pie.png, dpi150) plt.close() # ---------- 7. 箱线图单价分布 ---------- plt.figure(figsize(8, 5)) df.boxplot(column单价, by商品类别, gridFalse) plt.title(各品类商品单价分布) plt.suptitle() plt.xlabel(商品类别) plt.ylabel(单价元) plt.tight_layout() plt.savefig(output/price_boxplot.png, dpi150) plt.close() print(分析完成图表已保存到 output 目录。)注意脚本中保存每张图后立即调用plt.close()这是为了避免多个 Figure 对象在内存中堆积。在低频脚本中影响不明显但如果未来改造成循环绘制几十张图表不关闭画布会出现内存占用持续增长的问题。5.3 运行结果与产物说明在项目目录下依次运行python generate_data.py python run_analysis.py第二个脚本执行完成后控制台会输出清洗前后的数据形状和缺失值数量output目录下出现以下文件文件说明sales_clean.csv清洗后的明细数据category_amount_bar.png各品类销售额柱状图monthly_trend_line.png月度销售额趋势折线图category_quantity_pie.png各品类销量占比饼图price_boxplot.png各品类单价分布箱线图你可以打开这些图片检查柱状图最长柱对应销售额第一的品类折线图能看出销售额最高的月份饼图所有百分比之和应为 100%箱线图能够观察不同品类的价格中位数和离群点。任何一处异常都应回溯到数据整理环节而不是在绘图参数上盲目调整。6. 常见问题与排查思路问题现象常见原因解决思路运行read_csv后中文乱码文件编码与encoding参数不匹配确认 CSV 保存编码统一使用utf-8-sig或gbk再读取柱状图横轴顺序混乱未对分组结果做排序或类别字段含有空格检查groupby后的值先strip()再去重排序图表中文显示为方块matplotlib 未配置中文字体设置plt.rcParams[font.sans-serif]为本机可用中文字体负数显示为方块未关闭 unicode 负数支持设置plt.rcParams[axes.unicode_minus] Falsepd.to_datetime报错日期列中存在无法解析的格式或时间戳先查看唯一值df[日期].unique()再针对性修正格式箱线图出现很多离群点真实数据中单价差异较大并非代码错误确认业务上离群点是否有意义必要时单独分析生成的 PNG 图片文字模糊dpi参数过低保存时设置dpi150或更高满足论文和报告需要astype(int)报 ValueError数据中存在无法转成整数的字符串或缺失值先fillna再检查脏字符如逗号、货币符号最后转类型需要特别说明的是上述表格中的多数问题在“数据整理阶段”就可以预防。很多图表绘制异常本质上是上游数据质量没有处理干净。遇到报错时建议先打印数据样本再逐字段判断数据类型和取值最后才去修改绘图代码。7. 最佳实践与工程建议7.1 数据整理阶段第一永远保留原始数据。清洗过程中的修改应输出到新文件或新变量例如本文的sales_clean.csv不要在原始 CSV 上覆盖这样便于回溯问题。第二清洗逻辑尽量拆成步骤化的函数或注释分段。数据整理往往需要反复调整步骤清晰后新增清洗规则不会影响已有逻辑。第三统计口径必须有记录。比如“销售额”是指订单金额还是实付金额“月份”是按下单日期还是支付日期拆分这些口径决定图表的业务含义统一口径比调整绘图参数重要得多。第四处理真实生产数据时凡是涉及删除行或填充值都应该先行打印统计量并确认影响范围。若数据量大可以用抽样数据测试清洗逻辑再全量执行。7.2 图表生成阶段图表不是越花哨越好。柱状图用不同的颜色区分类别即可饼图不要超过 6 个扇区折线图如果有多条线一定要提供图例。保存图片时建议统一设置dpi日常分析 150 够用准备发表或报告可以提高到 300。另外所有图表都要有完整的标题、轴标签和数据标签。读图的人不一定了解数据背景如果你的横轴只写一个“月份”纵轴只写一个“销售额”那么图表的含义是不完整的。数据标签适量添加密集时可以使用空心圆点和色块代替逐点文字。7.3 从实验脚本到生产脚本本文的脚本适合学习和小规模分析但放到生产环境还需要关注以下几点路径参数化不要硬编码sales_orders.csv改为通过命令行参数或配置文件传入。日志记录用logging替换print记录脚本每次运行的时间、数据量、清洗前后对比。异常处理对数据库连接、文件读取、类型转换增加try-except避免单条脏数据导致整个流程中断。扩展性如果图表数量增加可以把每一张图封装成函数输入统计表输出图片文件便于单元测试。例如将柱状图封装为函数后新增品类时无需修改绘图逻辑只需修改输入统计表。这样的结构在真实项目中能显著降低维护成本。8. 下一步可以延伸的方向运行完本文的案例你已经掌握了一条基本的数据分析链路从原始数据、清洗整理、统计聚合到可视化的完整流程。这是很多数据分析和商业报告脚本的通用骨架。接下来的进阶方向可以根据自己的需求选择一是把统计结果导出为 Excel 多工作表报告使用pd.ExcelWriter实现二是把图表嵌入 HTML 周报使用savefig保存后由邮件附件发送三是使用 seaborn 增加热力图、成对图等更复杂的可视化四是把 pandas 的query、pivot_table和apply应用到更多样的脏数据场景。建议你直接修改生成数据脚本中的参数比如增加商品类别、修改日期范围、加入更多缺失值然后重新运行整个流程观察图表如何变化。只有数据“脏”过才知道整理步骤每一步的价值。数据整理和图表生成是互相成就的前者保证正确后者放大价值两者都值得在项目中持续打磨。如果这篇教程对你有所帮助建议收藏备用方便下次处理类似分析任务时直接对照参考。