当业务方丢来一份“很不干净”的 Excel 表格,要求第二天给出销售趋势图和分类占比图时,大多数人的第一反应是打开 WPS 或 Excel 手动拉透视表、插入图表。但如果数据量上万行、字段错乱、日期格式五花八门、还夹杂空值和重复记录,手动操作很快就会变成一场噩梦。
本文围绕“9.5.1 数据整理与图表生成案例”这一主题,以 Python 生态中最常用的 pandas 和 matplotlib 为核心工具,完整演示从原始数据清洗、字段加工、分组聚合到图表生成的全流程。如果你的日常工作也经常和数据打交道,或者正准备学习数据分析,这篇文章非常适合收藏后照着操作。
为了让案例更贴近真实场景,我会使用一份模拟的电商订单数据,数据里刻意加入了缺失值、重复记录、格式不统一等问题。这样既能展示数据整理的必要性,也能让图表生成的结果更有说服力。
1. 数据整理与图表生成的核心概念
1.1 什么是数据整理
数据整理(Data Wrangling)是指将原始、杂乱的数据转换为适合分析和可视化表达的结构化数据的过程。它并不是简单地把 Excel 里的列复制到新表里,而是包含:
| 整理动作 | 说明 | 常见操作 |
|---|---|---|
| 数据探查 | 观察数据的整体结构、字段类型、数据量 | head()、info()、describe() |
| 数据清洗 | 处理缺失值、重复值、异常值 | dropna()、fillna()、drop_duplicates() |
| 数据转换 | 调整字段格式、单位、类型 | astype()、pd.to_datetime() |
| 数据聚合 | 按维度统计汇总,构造业务指标 | groupby()、pivot_table() |
| 数据合并 | 将多张表按关键字段拼接 | merge()、concat() |
在实际数据分析项目中,数据整理往往占整个流程 60% 以上的时间。一个结构化、干净的数据集,是后续绘图和分析结论可靠的前提。
1.2 图表生成的定位
图表生成是基于整理后的数据,使用可视化工具将数据中的趋势、占比、分布、对比关系直观呈现。图表不是数据的装饰品,而是一种沟通语言。例如:
- 折线图适合表达随时间变化的趋势。
- 柱状图适合对比不同类别的数值差异。
- 饼图适合展示各部分占整体的比例。
- 散点图适合观察两个变量的相关性。
Python 中最常用的可视化库是 Matplotlib 和 Seaborn。Matplotlib 灵活度高,几乎可以控制图表的每个细节;Seaborn 则在统计图表方面更简洁、美观。本案例以 Matplotlib 为主,同时穿插 pandas 内置的绘图接口,让代码更简洁。
1.3 两者之间的关系
数据整理决定了图表能“画什么”,图表生成决定了数据“怎么表现”。如果数据没有清洗干净,画出来的图表可能包含缺失值断点、错误数值导致的异常峰值,甚至完全错误的结论。
举个简单的例子:一份包含“2024年1月1日”“2024/01/02”“2024-01-03”三种日期格式的销售明细,如果不进行统一转换,直接按日期分组绘图,结果会被拆成多个互不相干的类别,折线图自然无法形成连续的趋势。
所以,本文虽然叫“数据整理与图表生成案例”,但本质上是一条完整的分析流水线:读取 → 探查 → 清洗 → 转换 → 聚合 → 可视化。
2. 环境准备与版本说明
2.1 运行环境
本文示例以 Python 3.10 环境为基础,操作系统为 Windows 10/11,但代码在 macOS 和 Linux 下同样适用。主要依赖库如下:
| 库名称 | 用途 | 安装命令 |
|---|---|---|
| pandas | 数据读取、清洗、聚合 | pip install pandas |
| matplotlib | 图表绘制与导出 | pip install matplotlib |
| numpy | 数值计算辅助 | pip install numpy |
| openpyxl | 读取和写入 Excel 文件 | pip install openpyxl |
需要注意的是,pandas 的版本迭代较快,不同版本的 API 会有细微差异。如果你使用 pandas 2.x 版本,示例代码可以直接运行;如果使用 1.x 版本,大部分功能也兼容。安装时建议使用如下命令一次性装齐:
pip install pandas numpy matplotlib openpyxl如果下载速度较慢,可以切换为国内镜像源,例如:
pip install pandas numpy matplotlib openpyxl -i https://pypi.tuna.tsinghua.edu.cn/simple2.2 准备示例数据
为了模拟真实情况,我在本案例中手工构造了一份电商订单数据order_data.csv。字段包括:
- order_id:订单编号
- order_date:下单日期
- category:商品分类
- region:销售地区
- sales:销售额
- cost:成本
- quantity:销量
这份数据中故意包含空值、重复行、日期格式不统一等“脏数据”,便于展示数据整理过程。你可以先复制下面的 CSV 内容,保存为order_data.csv文件:
order_id,order_date,category,region,sales,cost,quantity 1001,2024-01-01,电子产品,华东,1200,800,2 1002,2024/01/02,服装,华南,350,150,3 1003,2024-01-03,食品,华东,88.5,50,5 1004,2024-01-03,电子产品,华北,2400,1600,1 1005,2024-01-04,图书,华北,79,40,2 1006,2024-01-05,服装,西南,420,200,2 1007,2024-01-05,电子产品,华南,1899,1200,1 1008,,食品,华中,156,90,4 1009,2024-01-07,图书,华东,128,60,1 1010,2024-01-08,电子产品,东北,3299,2100,1 1001,2024-01-01,电子产品,华东,1200,800,2 1011,2024/01/09,服装,华北,560,280,4 1012,2024-01-10,食品,华南,99.9,55,6这是一个非常典型的“脏乱差”数据集:日期格式不统一、存在空值、存在完全重复的行。后续所有整理操作都会围绕这份数据展开。
3. 数据整理核心操作详解
在正式画图之前,我们需要先把数据处理干净。本节会逐步讲解 pandas 中最常用的数据整理操作,每个操作都配有代码示例和结果说明。
3.1 读取数据与初步探查
读取 CSV 文件使用pd.read_csv();如果需要读取 Excel 文件,可以使用pd.read_excel()。数据读取之后,第一件事不是急着清洗,而是先“看”。
import pandas as pd # 读取 CSV 文件,指定文件编码为 UTF-8 df = pd.read_csv('order_data.csv', encoding='utf-8') # 查看前 5 行 print(df.head())输出结果:
order_id order_date category region sales cost quantity 0 1001 2024-01-01 电子产品 华东 1200.0 800.0 2 1 1002 2024/01/02 服装 华南 350.0 150.0 3 2 1003 2024-01-03 食品 华东 88.5 50.0 5 3 1004 2024-01-03 电子产品 华北 2400.0 1600.0 1 4 1005 2024-01-04 图书 华北 79.0 40.0 2通过info()方法可以查看每列的数据类型和非空数量,这一步非常重要:
print(df.info())输出结果:
<class 'pandas.core.frame.DataFrame'> RangeIndex: 13 entries, 0 to 12 Data columns (total 7 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 order_id 13 non-null int64 1 order_date 12 non-null object 2 category 13 non-null object 3 region 13 non-null object 4 sales 12 non-null float64 5 cost 12 non-null float64 6 quantity 13 non-null int64 dtypes: float64(2), int64(2), object(3)从输出可以看到,order_date是 object 类型,说明日期没有解析为时间类型;sales和cost存在缺失值。这些信息是后续清洗的依据。
3.2 缺失值处理
缺失值是数据清洗中最常见的问题。处理方式通常有两种:删除和填充。对于销售金额缺失的记录,如果整行其他字段齐全,可以先看看缺失比例有多大。
# 统计每列缺失值数量 print(df.isnull().sum())输出结果:
order_id 0 order_date 1 category 0 region 0 sales 1 cost 1 quantity 0 dtype: int64本案例中缺失值数量较少,直接删除缺失行对整体影响不大。使用dropna()可以删除包含缺失值的行:
# 删除包含空值的行 df = df.dropna().copy() # 再次确认缺失值情况 print(df.isnull().sum())需要注意,dropna()默认会删除只要含任意一个空值的行。如果只想删除指定列存在空值的行,可以传入subset参数:
# 只根据 order_date 和 sales 两列判断是否删除 df = df.dropna(subset=['order_date', 'sales']).copy()实际项目中,缺失值处理策略取决于业务含义:数值型字段可以用均值、中位数填充;分类字段常用众数填充;时间字段如果缺失,往往只能删除或通过其他字段推算。
3.3 重复值处理
重复数据会让统计结果虚高,必须清理。使用duplicated()可以判断哪些行是重复的,返回的是布尔序列;drop_duplicates()则直接删除重复行。
# 查看重复行数量 print(df.duplicated().sum()) # 删除重复行 df = df.drop_duplicates().copy() # 删除后查看数据量 print(f'处理后数据量: {len(df)}')输出结果:
1 处理后数据量: 12这里删除的就是order_id=1001的重复记录。drop_duplicates()默认会保留第一次出现的行;如果希望保留最后一次出现的行,可以设置keep='last'。如果判断重复的规则不是整行完全一致,而是某几列相同,也可以传入subset参数:
# 以 order_id 为唯一判断依据 df = df.drop_duplicates(subset=['order_id']).copy()3.4 日期格式统一
原始数据中的日期格式有三种:2024-01-01、2024/01/02和空值。虽然经过dropna()后空值已经删除,但两种格式仍然存在。如果不统一,分组聚合时会得到错误的维度。
使用pd.to_datetime()可以将列转换为统一的时间类型:
# 统一转换日期格式 df['order_date'] = pd.to_datetime(df['order_date']) # 查看转换后的结果 print(df['order_date'].dtype) print(df[['order_id', 'order_date']])pd.to_datetime()会自动识别大多数常见日期格式,包括2024-01-01、2024/01/02、2024.01.03等。如果希望提取年、月、日等字段,也可以通过dt访问器完成:
df['year'] = df['order_date'].dt.year df['month'] = df['order_date'].dt.month df['weekday'] = df['order_date'].dt.dayofweek # 0=周一, 6=周日3.5 字段计算与加工
很多业务指标不是原始数据里直接有的,而是通过计算得到的。本案例中,利润profit可以通过销售额减去成本得到:
# 计算利润字段 df['profit'] = df['sales'] - df['cost'] # 添加利润率的辅助字段 df['profit_rate'] = (df['profit'] / df['sales'] * 100).round(2) print(df[['order_id', 'sales', 'cost', 'profit', 'profit_rate']])输出结果:
order_id sales cost profit profit_rate 0 1001 1200.0 800.0 400.0 33.33 1 1002 350.0 150.0 200.0 57.14 2 1003 88.5 50.0 38.5 43.50 ...这里使用向量化运算,效率远高于遍历每一行。pandas 的向量化特性也是它比 Excel 手写公式更适合大批量数据处理的原因之一。
3.6 分组聚合
数据整理完成后,需要按维度汇总,才能用于图表绘制。groupby()是 pandas 中最核心的聚合方法。
例如,按商品分类统计销售额总和:
# 按分类分组,计算销售额总和 category_sales = df.groupby('category')['sales'].sum().reset_index() print(category_sales)输出结果:
category sales 0 图书 207.0 1 服装 930.0 2 食品 344.4 3 电子产品 8798.0reset_index()的作用是把分组键从索引变回普通列,便于后续绘图。如果不调用它,category会成为索引,图表生成时处理起来会麻烦一些。
再比如,按日期统计每天的销售总额和订单数:
# 按日期分组,统计销售额和订单数量 daily_sales = df.groupby('order_date').agg( total_sales=('sales', 'sum'), total_quantity=('quantity', 'sum'), order_count=('order_id', 'count') ).reset_index() print(daily_sales)输出结果:
order_date total_sales total_quantity order_count 0 2024-01-01 1200.0 2 1 1 2024-01-02 350.0 3 1 2 2024-01-03 2488.5 6 2 ...上述agg()方法支持为不同字段指定不同的聚合函数,在很多分析场景中非常实用。
4. 图表生成完整实战案例
数据整理完毕,接下来进入图表生成环节。本案例会绘制四种常用图表:每日销售趋势折线图、分类销售额柱状图、地区销售占比饼图、利润与销售额对比图。
4.1 创建项目结构
为了方便管理,建议按照以下目录结构组织代码和输出文件:
data_analysis_case/ │ ├── data/ │ └── order_data.csv │ ├── output/ │ ├── daily_trend.png │ ├── category_sales.png │ ├── region_share.png │ └── profit_compare.png │ └── analysis.py如果目录不存在,可以提前创建,或者在 Python 代码中通过os.makedirs()自动创建输出目录:
import os os.makedirs('output', exist_ok=True)4.2 导入依赖与读取数据
将前面的数据整理流程封装成一个函数,便于后续复用:
import os import pandas as pd import matplotlib.pyplot as plt # 解决中文显示问题 plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False os.makedirs('output', exist_ok=True) def load_and_clean_data(file_path): """读取原始数据并完成基础清洗""" df = pd.read_csv(file_path, encoding='utf-8') df = df.dropna() df = df.drop_duplicates() df['order_date'] = pd.to_datetime(df['order_date']) df['profit'] = df['sales'] - df['cost'] return df df = load_and_clean_data('data/order_data.csv') print(f'数据清洗完成,当前数据量: {len(df)}')4.3 绘制每日销售趋势折线图
折线图适合展示时间序列数据的变化趋势。这里需要先按日期分组,绘制时把日期作为 x 轴,销售额作为 y 轴。
# 按日期汇总销售额 daily_sales = df.groupby('order_date')['sales'].sum().reset_index() # 绘制折线图 plt.figure(figsize=(10, 5)) plt.plot(daily_sales['order_date'], daily_sales['sales'], marker='o', linewidth=2) plt.title('每日销售趋势') plt.xlabel('日期') plt.ylabel('销售额(元)') plt.grid(True, linestyle='--', alpha=0.6) plt.tight_layout() plt.savefig('output/daily_trend.png', dpi=150) plt.show()关键参数解释:
marker='o':在数据点上显示圆形标记,便于观察每个具体值。linewidth=2:设置线条粗细。grid(True, linestyle='--', alpha=0.6):添加半透明虚线网格,提高可读性。dpi=150:设置导出图片分辨率,清晰度足够放到 PPT 或文档中。
执行后会生成output/daily_trend.png图片,你可以打开查看趋势是否平滑、是否存在异常峰值。
4.4 绘制分类销售额柱状图
柱状图适合进行类别间的横向对比。在绘制前,建议先按销售额降序排列,这样图中柱子的顺序更直观。
# 按分类汇总销售额并排序 category_sales = df.groupby('category')['sales'].sum().sort_values(ascending=False).reset_index() # 绘制柱状图 plt.figure(figsize=(8, 5)) bars = plt.bar(category_sales['category'], category_sales['sales'], color=['#4C72B0', '#55A868', '#C44E52', '#8172B2']) plt.title('各分类销售额对比') plt.xlabel('商品分类') plt.ylabel('销售额(元)') # 在柱子上方显示数值 for bar in bars: height = bar.get_height() plt.text(bar.get_x() + bar.get_width() / 2, height + 30, f'{height:.0f}', ha='center', va='bottom', fontsize=10) plt.tight_layout() plt.savefig('output/category_sales.png', dpi=150) plt.show()这里使用了plt.text()在柱子顶部添加数值标签。如果不加这个标签,柱状图的精确数值只能靠目测,不够专业。
4.5 绘制地区销售占比饼图
饼图适合展示各分类的占比关系。虽然在某些场景下饼图受到争议,但在部分占比对比不复杂的情况下,它仍然直观易懂。
# 按地区汇总销售额 region_sales = df.groupby('region')['sales'].sum().reset_index() # 绘制饼图 plt.figure(figsize=(8, 8)) plt.pie(region_sales['sales'], labels=region_sales['region'], autopct='%.1f%%', startangle=90, explode=[0.05, 0.05, 0.05, 0.05, 0.05, 0.05]) plt.title('各地区销售占比') plt.tight_layout() plt.savefig('output/region_share.png', dpi=150) plt.show()参数说明:
autopct='%.1f%%':在扇区中心显示百分比,保留一位小数。startangle=90:从 90 度方向开始绘制,便于调整起始角度。explode:将扇区向外拉开一段距离,形成分离效果。
如果地区数量较多,饼图上的标签会重叠,此时更建议改用水平柱状图。
4.6 绘制利润与销售额对比图
为了对比不同分类的销售额和利润情况,可以使用分组柱状图。制作分组柱状图的技巧是控制柱子的位置偏移。
import numpy as np # 按分类汇总销售额和利润 category_summary = df.groupby('category').agg( total_sales=('sales', 'sum'), total_profit=('profit', 'sum') ).reset_index() # 排序,按销售额降序 category_summary = category_summary.sort_values('total_sales', ascending=False) x = np.arange(len(category_summary)) width = 0.35 plt.figure(figsize=(10, 6)) bars1 = plt.bar(x - width / 2, category_summary['total_sales'], width, label='销售额', color='#4C72B0') bars2 = plt.bar(x + width / 2, category_summary['total_profit'], width, label='利润', color='#55A868') plt.title('各分类销售额与利润对比') plt.xlabel('商品分类') plt.ylabel('金额(元)') plt.xticks(x, category_summary['category']) plt.legend() plt.tight_layout() plt.savefig('output/profit_compare.png', dpi=150) plt.show()np.arange()生成等间距的 x 坐标,通过x - width / 2和x + width / 2让两组柱子并排显示,这是分组柱状图的标准做法。
4.7 运行与验证
将上述代码整合到同一个analysis.py文件中,并在终端执行:
cd data_analysis_case python analysis.py如果终端没有报错,且 output 目录下生成了 4 张图片,说明整个流程已经跑通。
你也可以在命令中追加plt.close()避免多次绘图时内存占用过高:
plt.close()5. 常见问题与排查思路
在实际运行数据整理和图表生成代码时,最容易出现以下几类问题:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 图表中文显示为方块 | 系统中没有匹配的中文字体 | 设置plt.rcParams['font.sans-serif'] = ['SimHei'];macOS 可改为['Arial Unicode MS'] |
| 日期列无法排序 | 日期仍是字符串格式 | 使用pd.to_datetime()转换列类型 |
| 图表横坐标过于拥挤 | 类别太多或日期跨度大 | 旋转标签:plt.xticks(rotation=45);或者缩小画布比例 |
| 柱状图上没有数值标签 | 未调用plt.text() | 遍历柱子对象,读取get_height()添加文本 |
| 数据量很大时绘图卡顿 | 图表元素过多 | 先聚合再绘图,不要直接用明细数据绘图 |
| 保存的图片背景不透明 | 默认背景是白色 | 设置transparent=True或调整facecolor参数 |
其中,中文显示问题是最常见的。如果你在 PyCharm 或 Jupyter Notebook 中运行代码,遇到图上的中文全部变成小方块,优先检查当前系统的中文字体可用性。
如果你使用的是 macOS 系统,可以将字体配置改为:
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'PingFang SC', 'SimHei']如果是在 Linux 服务器上运行,可能需要安装中文字体,例如:
sudo apt install fonts-wqy-zenhei安装完成后重启 Python 进程,图表中的中文就能正常显示。
6. 最佳实践与工程建议
6.1 关于数据整理的建议
数据整理的目标是“干净、规范、可复现”。为了实现这一点,建议遵循以下原则:
第一,把清洗逻辑封装成函数,而不是在代码中从头到尾堆命令。函数化的好处是当原始数据更新时,只要重新执行一次函数,就能得到最新的清洗结果,避免复制粘贴导致的遗漏或错误。
第二,保存中间结果。数据清洗过程中的很多步骤是不可逆的。如果后续发现清洗规则错误,重新从原始数据执行成本很高。建议在合理的位置使用to_csv()保留中间数据:
df.to_csv('output/cleaned_order_data.csv', index=False, encoding='utf-8-sig')使用utf-8-sig编码而不是utf-8,是为了解决 Excel 打开 CSV 文件时中文乱码的问题。这一点在交付数据文件给非技术同事时特别重要。
第三,要养成查看info()、describe()、isnull().sum()的习惯。数据分析里的很多“坑”不是代码问题,而是数据理解问题。
6.2 关于图表生成的建议
图表生成并不是“用默认参数画出来就行”,至少要考虑以下三个维度:
一是图表类型要匹配业务意图。趋势数据用折线图,对比数据用柱状图,占比数据用饼图或堆叠柱状图。不要为了好看而选择错误的图表类型。
二是配色要克制。默认配色的效果通常够用,不要为每个柱子单独指定“五彩斑斓”的颜色。颜色过多会分散注意力,反而不利于表达。
三是图片导出要满足使用场景。如果图要放进论文或报告,建议设置dpi=300;如果只是放进 PPT,dpi=150已经足够。导出前用tight_layout()调整布局,避免标题或坐标轴标签被截断。
6.3 工程化建议
如果你的分析脚本需要定期运行,可以考虑:
- 将路径配置放到代码开头,方便修改。
- 使用日志记录每个阶段处理的数据量,便于排查问题。
- 绘图前判断数据是否为空,避免因空数据导致绘图报错。
- 重要分析结论建议输出文本或 Excel 汇总表,而不是只输出图片。
一个简单的日志输出示例:
import logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logging.info(f'数据清洗完成,数据量: {len(df)}') logging.info(f'输出图表至 output/ 目录')7. 总结与后续学习方向
本文以一份包含缺失值、重复记录、日期格式混乱的电商订单数据作为切入点,完整演示了 Python 数据整理到图表生成的实战流程。你掌握了 pandas 的缺失值处理、重复值去重、日期转换、字段加工和分组聚合,也学会了使用 Matplotlib 绘制折线图、柱状图、饼图和分组对比图。
这套方法不仅适用于本案例中的电商订单,也可以直接迁移到销售报表、考试分析、用户行为统计、题库正确率分析等场景。
接下来你可以继续学习以下方向:
- 使用 Seaborn 绘制更美观的统计图表,例如热力图、箱线图、回归图。
- 掌握
pivot_table数据透视表方法,处理更复杂的多维汇总需求。 - 使用 Plotly 或 Pyecharts 绘制交互式图表,方便在 Web 端展示分析结果。
- 将分析流程封装成函数或类,结合定时任务实现报表自动化生成。
数据整理与图表生成是数据分析的基本功,也是很多数据岗位面试时的核心考察内容。建议你拿到一份真实数据后,不急着画图,先按照本文的顺序:探查、清洗、转换、聚合,最后再思考用什么图表表达。动手运行一遍完整案例,比只看文章理解深刻得多。