Python数据分析与可视化入门:Pandas清洗数据结合Matplotlib绘图
本文系统讲解Python数据分析与可视化入门流程,演示如何使用Pandas检测缺失值、剔除重复项、转换类型与规整数据,并结合Matplotlib面向对象接口绘制专业趋势图与对比图。
一、环境准备与业务脏数据样本构建
进行本篇操作需要安装两个 Python 核心库:数据分析库 pandas 与基础可视化库 matplotlib。在终端或命令行中执行以下命令即可完成安装:
pip install pandas matplotlib
为了让演示具有真实参考价值,我们构造一份包含典型“脏数据特征”的电商销售记录。这份样本包含了初学者最常遇到的几类缺陷:单价带有货币符号导致被识别为文本、下单时间格式不统一、关键销售额存在缺失(NaN)、包含完全重复的记录,以及商品类别字段存在尾部空格。
以下是生成并加载该模拟数据的完整代码:
import pandas as pd
import numpy as np
# 模拟构建包含常见脏数据问题的初始交易数据集
raw_data = {
'order_id': ['A101', 'A102', 'A103', 'A104', 'A105', 'A101', 'A106'],
'order_date': ['2023-10-01', '2023/10/02', '2023-10-02', '2023-10-03', '2023-10-03', '2023-10-01', '2023-10-04'],
'category': ['电子数码 ', '办公文具', '家居生活', '电子数码', '办公文具 ', '电子数码 ', '家居生活'],
'unit_price': ['¥199.0', '¥25.5', '¥88.0', '¥299.0', '¥45.0', '¥199.0', '¥120.0'],
'quantity': [2, 4, 1, 1, np.nan, 2, 3],
'total_amount': [398.0, 102.0, np.nan, 299.0, 45.0, 398.0, 360.0]
}
df = pd.DataFrame(raw_data)
print("【原始数据预览】:\n", df)
拿到数据后的第一件事不是直接上手绘图,而是调用 df.info() 与 df.isnull().sum() 进行全貌诊断。
print("\n【字段信息与数据类型诊断】:\n")
df.info()
print("\n【各列缺失值统计】:\n", df.isnull().sum())
控制台将输出以下关键特征(此为示例输出):
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7 entries, 0 to 6
Data columns (total 6 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 order_id 7 non-null object
1 order_date 7 non-null object
2 category 7 non-null object
3 unit_price 7 non-null object
4 quantity 6 non-null float64
5 total_amount 6 non-null float64
dtypes: float64(2), object(4)
从探测结果中可以明确发现三处阻碍绘图的硬伤:
order_date是object(文本)类型且混有斜杠与短横线,无法按真实时间周期排序和按天对齐;unit_price含有货币符号字符¥,导致本应为数字的列被判定为object,无法进行算术求和;total_amount与quantity存在NaN缺失,直接计算平均值或绘图会导致数据脱节。
二、Pandas 核心数据清洗五步法
针对探查发现的问题,我们按照“文本规整 ➔ 类型转换 ➔ 缺失填补 ➔ 重复剔除 ➔ 异常过滤”的标准流水线进行处理。
步骤一:文本空格清除与非法字符剥离
在类别列中,'电子数码 ' 带有尾随空格,这会导致后续 groupby 将其视作两个完全不同的独立分类。同时,单价列含有字符 ¥。我们使用 Pandas 的向量化字符串访问器 .str 分别进行清理:
# 1. 清理分类文本的首尾多余空格
df['category'] = df['category'].str.strip()
# 2. 去除单价中的人民币符号 '¥'
df['unit_price'] = df['unit_price'].str.replace('¥', '', regex=False)
步骤二:数据类型显式转换
将剥离符号后的 unit_price 转换为高精度浮点数 float,并将混杂格式的 order_date 通过 pd.to_datetime() 统一解析为标准时间戳:
# 转换为数值型浮点数
df['unit_price'] = df['unit_price'].astype(float)
# 转换为标准 DatetimeIndex 兼容的时间戳
df['order_date'] = pd.to_datetime(df['order_date'])
步骤三:缺失值诊断与业务逻辑填补
在样本数据中,第 2 行的 total_amount 为 NaN,而该行的单价与数量均已知;第 4 行的 quantity 为 NaN。在实际业务中,应优先利用已知字段的勾稽关系(总金额 = 单价 × 数量)进行精准推导填补,而非简单地盲目删除:
# 根据业务逻辑:若 total_amount 缺失但单价与数量齐全,则自动计算填补
mask_amount_nan = df['total_amount'].isna() & df['quantity'].notna()
df.loc[mask_amount_nan, 'total_amount'] = df['unit_price'] * df['quantity']
# 反向逻辑:若数量缺失但总金额与单价齐全,推导数量
mask_qty_nan = df['quantity'].isna() & df['total_amount'].notna()
df.loc[mask_qty_nan, 'quantity'] = df['total_amount'] / df['unit_price']
df.dropna(subset=['total_amount']) 直接剔除,以防引入虚假均值污染分析结果。
步骤四:剔除重复记录与边界逻辑校验
检查是否存在重复上报的订单记录。使用 drop_duplicates 保证每条交易在分析库中唯一存在:
# 查看重复条数
duplicate_count = df.duplicated(subset=['order_id']).sum()
print(f"发现重复订单记录数:{duplicate_count}")
# 仅保留第一条记录,剔除后续冗余重复项
df = df.drop_duplicates(subset=['order_id'], keep='first').copy()
# 过滤金额小于等于0的异常脏数据
df = df[df['total_amount'] > 0].reset_index(drop=True)
print("\n【清洗完成后的合规数据集】:\n", df)
三、数据聚合与准备:从明细到绘图输入
清洗后的 DataFrame 属于明细宽表。直接绘制折线图或条形图时,往往需要先按“分析维度”聚合出清晰的度量指标。
为了支撑后续的可视化任务,我们从清洗后的数据中提炼出两个维度的聚合数据:
- 时间维度聚合(趋势线):按
order_date统计每日销售总额; - 品类维度聚合(对比图):按
category统计各品类的销售总额并按从大到小排序。
# 1. 时间维度:按天汇总结算
daily_sales = df.groupby('order_date')['total_amount'].sum().reset_index()
# 2. 品类维度:按类别汇总并排序(便于条形图展现阶梯感)
category_sales = df.groupby('category')['total_amount'].sum().sort_values(ascending=True)
print("\n【每日销售总额】:\n", daily_sales)
print("\n【各品类销售总额】:\n", category_sales)
四、Matplotlib 绘图实战:面向对象 API 核心范式
很多初学者习惯直接使用 plt.plot() 这类快捷接口,但在多子图控制、坐标格式精细调节和批量导出时极易产生样式串扰。在生产环境中,推荐始终采用面向对象 API(Object-Oriented API),即通过 fig, ax = plt.subplots() 显式获得画布容器(Figure)与坐标系(Axes)。
中文字体与负号显示避坑配置
Matplotlib 默认缺少对中文无衬线字体的指向,如果不做配置,图表中的所有中文字符都会显示为乱码方块(Tofu box),并且负号容易被误渲染为缺失字符。绘图前务必加入以下两行配置:
import matplotlib.pyplot as plt
# 设置中文字体回退列表与正常显示负号
plt.rcParams['font.sans-serif'] = ['PingFang SC', 'Microsoft YaHei', 'SimHei', 'sans-serif']
plt.rcParams['axes.unicode_minus'] = False
案例 1:绘制每日销售额走势折线图
使用面向对象方式将 daily_sales 绘制为时间序列折线图,包含网格线、数据标记点与日期倾斜排版:
# 1. 创建画布与独立坐标系
fig, ax = plt.subplots(figsize=(8, 4.2), dpi=100)
# 2. 绘制折线图,设置线宽、主色与节点圆圈
ax.plot(
daily_sales['order_date'],
daily_sales['total_amount'],
marker='o',
markersize=6,
color='#17324D', # 深海蓝主色
linewidth=2,
label='每日营收'
)
# 3. 装饰标题、轴标签与参考网格
ax.set_title('2023年10月每日销售额变化趋势', fontsize=13, fontweight='bold', pad=12, color='#17324D')
ax.set_xlabel('下单日期', fontsize=11, color='#475569')
ax.set_ylabel('销售金额 (元)', fontsize=11, color='#475569')
ax.grid(True, linestyle='--', alpha=0.4, color='#94A3B8')
ax.legend(frameon=True, facecolor='#FFFFFF', edgecolor='#E2E8F0')
# 4. 自动旋转日期标签避免重叠粘连
fig.autofmt_xdate(rotation=30)
fig.tight_layout()
# 5. 导出并及时关闭图表对象
fig.savefig('daily_trend.png', dpi=300)
plt.close(fig)
案例 2:绘制品类销售对比水平条形图
当类别名称较长或分类较多时,水平条形图(ax.barh)比普通纵向柱状图(ax.bar)更易阅读,因为水平文字无须倾斜即可完整展现。我们利用 ax.bar_label 直接在条形顶端打上具体数值标签:
# 创建画布
fig, ax = plt.subplots(figsize=(8, 4.2), dpi=100)
categories = category_sales.index
amounts = category_sales.values
# 绘制水平条形图,采用灰青强调色
bars = ax.barh(categories, amounts, color='#659B9B', height=0.55)
# 在柱子右侧标注精准数值
ax.bar_label(bars, fmt='¥%.1f', padding=5, color='#17324D', fontweight='bold')
# 优化边框与坐标范围
ax.set_title('各商品品类总销售额对比', fontsize=13, fontweight='bold', pad=12, color='#17324D')
ax.set_xlabel('总金额 (元)', fontsize=11, color='#475569')
ax.set_xlim(0, max(amounts) * 1.18) # 留出右侧空间容纳文本标签
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.grid(axis='x', linestyle=':', alpha=0.5)
fig.tight_layout()
fig.savefig('category_comparison.png', dpi=300)
plt.close(fig)
五、常见问题排查与避坑要点
初学者将 Pandas 与 Matplotlib 结合使用时,往往会在以下四个环节遭遇非预期的报错或图表异样:
-
SettingWithCopyWarning 警告:对过滤后的切片 DataFrame 直接赋值时,Pandas 会发出警告。根本原因在于系统无法确定操作的是视图(View)还是副本(Copy)。解决方法是在切片或过滤后显式附加
.copy(),例如df = df[df['amount'] > 0].copy()。 -
日期刻度挤压重叠:当时间跨度较长、日期点密集时,X 轴文本极易连成一片黑块。除了使用
fig.autofmt_xdate()旋转外,还可以引入matplotlib.dates.DayLocator(interval=N)强制按固定步长稀疏显示刻度。 -
连续数值被误作离散分类绘图:如果在清洗阶段漏掉了
.astype(float),原本为数值的列依然是object。Matplotlib 会将每个数字字符串当作一个互不相关的独立分类标签绘制,导致坐标轴刻度失去连续数值比例尺。 -
循环绘图引发显存与内存泄漏:在批量处理数百份报告或轮询绘图时,仅调用
plt.savefig()并不会销毁底层对象。务必在图表保存后显式调用plt.close(fig)彻底释放图形句柄。
六、端到端完整复现脚本
将前述所有清洗步骤与双子图面板(Dashboard)整合为单个可独立执行的 Python 脚本。读者可直接复制并在本地环境中运行验证:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# 1. 字体环境初始化
plt.rcParams['font.sans-serif'] = ['PingFang SC', 'Microsoft YaHei', 'SimHei', 'sans-serif']
plt.rcParams['axes.unicode_minus'] = False
# 2. 模拟真实脏数据
raw_data = {
'order_id': ['A101', 'A102', 'A103', 'A104', 'A105', 'A101', 'A106'],
'order_date': ['2023-10-01', '2023/10/02', '2023-10-02', '2023-10-03', '2023-10-03', '2023-10-01', '2023-10-04'],
'category': ['电子数码 ', '办公文具', '家居生活', '电子数码', '办公文具 ', '电子数码 ', '家居生活'],
'unit_price': ['¥199.0', '¥25.5', '¥88.0', '¥299.0', '¥45.0', '¥199.0', '¥120.0'],
'quantity': [2, 4, 1, 1, np.nan, 2, 3],
'total_amount': [398.0, 102.0, np.nan, 299.0, 45.0, 398.0, 360.0]
}
df = pd.DataFrame(raw_data)
# 3. 标准化清洗处理
df['category'] = df['category'].str.strip()
df['unit_price'] = df['unit_price'].str.replace('¥', '', regex=False).astype(float)
df['order_date'] = pd.to_datetime(df['order_date'])
# 逻辑补齐缺失值
mask_amount = df['total_amount'].isna() & df['quantity'].notna()
df.loc[mask_amount, 'total_amount'] = df['unit_price'] * df['quantity']
mask_qty = df['quantity'].isna() & df['total_amount'].notna()
df.loc[mask_qty, 'quantity'] = df['total_amount'] / df['unit_price']
# 去重与边界过滤
df = df.drop_duplicates(subset=['order_id'], keep='first')
df = df[df['total_amount'] > 0].copy()
# 4. 聚合计算
daily_sales = df.groupby('order_date')['total_amount'].sum().reset_index()
category_sales = df.groupby('category')['total_amount'].sum().sort_values(ascending=True)
# 5. 组合画布面向对象绘制 (1行2列看板)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 5), dpi=100)
# 子图 1:销售趋势折线
ax1.plot(daily_sales['order_date'], daily_sales['total_amount'], marker='o', color='#17324D', lw=2.2)
ax1.set_title('每日销售走势', fontsize=12, fontweight='bold', color='#17324D')
ax1.set_ylabel('销售额 (元)')
ax1.grid(True, linestyle='--', alpha=0.4)
fig.autofmt_xdate(rotation=25)
# 子图 2:品类对比条形
bars = ax2.barh(category_sales.index, category_sales.values, color='#659B9B', height=0.55)
ax2.bar_label(bars, fmt='¥%.1f', padding=4, color='#17324D', fontweight='bold')
ax2.set_title('品类总营收对比', fontsize=12, fontweight='bold', color='#17324D')
ax2.set_xlim(0, max(category_sales.values) * 1.2)
ax2.spines['top'].set_visible(False)
ax2.spines['right'].set_visible(False)
fig.tight_layout()
fig.savefig('sales_analysis_dashboard.png', dpi=300)
plt.close(fig)
print("\n数据分析与图表生成执行完成,图像已保存为 sales_analysis_dashboard.png")免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









