这已经是我第三篇学习笔记了。前两篇我分别搞定了Python的基础语法和用爬虫往本地扒数据,当时觉得自己特牛,一口气存了几千条数据到CSV里。结果等我真拿这些数据想做点分析的时候,人直接傻掉了:字段对不上、日期有的是字符串有的是时间戳、中英文混着来,还有一堆重复项。那一刻我才意识到,爬虫只是万里长征的第一步,数据清洗和可视化才是真正让人头疼的部分。这篇笔记我就把从脏数据到可视化看板的整个过程记录下来,包括我踩过的坑、试错的经验和最后沉淀下来的一套操作流程,希望能给同样卡在这里的人一点参考。
1. 拿到数据后先别急着画图,把脏数据摊开看看
1.1 数据长什么样:字段、类型、缺失值、重复值一眼扫清
我一开始的做法特别蠢,爬完数据直接print(df.head()),看两眼就急吼吼地写画图代码。结果画出来的图不是横轴挤成一团,就是数值全部堆积在一起,根本没法看。后来我养成一个习惯,拿到任何DataFrame,先花三分钟做一次“体检”,主要看四件事:
- 有多少行多少列,字段名有没有重复或者带空格
- 每列的数据类型是否符合预期,比如“价格”这列到底是int还是字符串
- 缺失值集中在哪些列,缺失比例大概是多少
- 完全重复的行有多少,需不需要去重
具体代码很简单:
import pandas as pd df = pd.read_csv('raw_data.csv') print('shape:', df.shape) print('columns:', df.columns.tolist()) print(df.dtypes) print(df.isnull().sum()) print('duplicated rows:', df.duplicated().sum())这段代码跑完,我心里基本就有底了。就拿我之前爬的一份商品数据来说,光“价格”这一列就有三种格式:'¥ 59.9'、'99'、'1,299.00'。这种数据不处理,画图的时候pandas根本没法把它当数值用。
1.2 为什么数据清洗是可视化之前最不能省的步骤
很多人觉得清洗是浪费时间,觉得“反正图能出来就行”。我踩过坑之后才明白,数据不进清洗就直接可视化,图表里的信息和结论很可能是错的。比如你画一个TOP10排行的柱状图,结果“价格”是字符串类型,排序的时候按字典序排,'99'会排在'1000'后面,你得到的榜单完全是乱的。
再举个例子,日期字段如果有的时候是2023-05-01,有的时候是2023/5/1,还有的时候是20230501,统一性就没了。画折线图的时候时间轴会乱跳,x轴标签会重复,趋势线根本没法看。
所以我现在的原则是:不清洁的数据绝不进画图流程。清洗不是额外工作,它是分析的一部分,而且往往比画图本身更花时间。
2. 清洗数据的几个“重灾区”及我的处理套路
2.1 字符串里的乱码和空格,别让它们毁了你的图表
爬虫拿回来的数据,最脏的就是字符串列。常见的问题有:开头结尾有空格、全角半角混用、中间有多余的换行符、甚至偶尔冒出个\xa0这种不换行空格。这些东西你用肉眼在表格里看可能注意不到,但画到图里就变成图例上的奇怪字符或者柱状图标签错位。
我一般这样处理:
def clean_str(s): if isinstance(s, str): # 先替换不换行空格和全角空格 s = s.replace('\xa0', ' ').replace('\u3000', ' ') # 多个空格缩成一个 s = ' '.join(s.split()) # 去掉首尾空格 s = s.strip() return s return s df['name'] = df['name'].apply(clean_str)注意顺序:先替换特殊空格,再用split合并中间的空格,最后strip。这个函数写完之后,对每一个字符串型的列都跑一遍,能省掉后面很多麻烦。
2.2 日期时间格式统一:翻车最频繁的一环
日期清洗是我翻车最多的地方。一开始我用pd.to_datetime(df['date'])一把梭,结果经常报错,因为里面混了'2023年5月1日'这种格式,还有'2023.05.01'。后来我养成一个习惯:先看df['date'].unique()里到底有哪几种格式,然后分情况处理。
我总结了一个比较稳的方案:
def parse_date(s): # 先把中文年月日换成横线 s = str(s).replace('年', '-').replace('月', '-').replace('日', '') s = s.replace('.', '-').replace('/', '-') # 统一成datetime对象 return pd.to_datetime(s, errors='coerce') df['date'] = df['date'].apply(parse_date)这里特别强调errors='coerce',解析失败会变成NaT,这样不会直接中断,但你要回头看看哪些行解析失败了。如果丢太多,说明原始格式还有我没见过的,得回去继续补规则。
2.3 缺失值不是无脑删,要区分情况
以前我觉得缺失值直接dropna()一删就完事。后来发现有的列缺失50%,删完整个表就剩三分之一了,后面分析根本没法做。我现在会分情况:
- 如果是分析目标必需的字段(比如价格、日期),缺失比例极小(低于5%),直接删掉这些行影响不大。
- 如果缺失比例很高,但字段不重要,那就保留着,不进入分析就行。
- 如果缺失值有规律,比如“销量”缺失其实代表0,那就得补0,而不是删除。
- 数值列可以用中位数或均值填充,但我会先看分布,如果偏态严重就用中位数。
我一般写一个小函数来处理:
def handle_missing(df): # 占比过高且不重要的列直接丢弃 threshold = 0.5 for col in df.columns: if df[col].isnull().mean() > threshold and col not in ['关键字段']: df.drop(columns=[col], inplace=True) # 数值列用中位数填充 for col in df.select_dtypes(include=['float64', 'int64']).columns: df[col] = df[col].fillna(df[col].median()) # 日期列先不填充,后续单独处理 return df注意一点,填充和删除都要记下当时的数据量,万一后面发现问题还能回溯。我一般会在清洗前后的行数打印出来对比。
3. 一张图讲清楚:用Pandas做聚合与筛选
3.1 确定分析目标:你想从数据里得到什么
拿到洗好的数据,下一步不是立刻画图,而是先问自己想回答什么问题。这个习惯极大提升了我的效率。比如针对我爬的商品数据,我当时最想搞明白三件事:
- 哪个类别的商品数量最多?
- 不同价位的商品在评分上有没有明显差异?
- 最近一个月的销量走势怎么样?
带着这些问题去操作,就不会乱做一堆图表最后不知道讲什么。我强烈建议先写一句话:“我要通过分析,向别人说明XX现象。”这句话在后续所有步骤里都会帮你做选择。
3.2 groupby和pivot_table的配合用法
有了目标之后,groupby和pivot_table就是我的两大主力。比如想看每个类别的商品数量,一行搞定:
category_count = df.groupby('category')['item_id'].count().sort_values(ascending=False)这里为什么用item_id计数而不是直接用category?因为count()会忽略NaN,而item_id理论上不会有缺失值,计数更可靠。如果数据里没有item_id,用size()也可以。
再看价位和评分的关系,我想把价格切成几段然后看平均评分:
df['price_range'] = pd.cut(df['price'], bins=[0, 50, 100, 200, 500, 10000], labels=['0-50', '50-100', '100-200', '200-500', '500+']) pivot = df.pivot_table(index='price_range', values='rating', aggfunc='mean')这样一张表就能看出来,是不是越贵的评分越高,或者中间价位反而评分更好。pivot_table比多次groupby写起来更简洁,而且能同时聚合多个指标,比如aggfunc={'rating': 'mean', 'price': 'count'},一次性拿到评分均值和商品数量。学会这两个函数,百分之八十的汇总工作都能解决。
4. 可视化工具选型与踩坑:Matplotlib、Seaborn还是ECharts
4.1 静态图用Matplotlib+Seaborn,怎样快速上手
一开始我只用Matplotlib,感觉每次画图都要写一堆细节代码,连坐标轴标签都要手动设置。后来我发现Seaborn是在Matplotlib之上做的封装,画统计图特别省事,比如箱线图、热力图、分布图都能一行代码出图。
我的默认组合是:用Seaborn画图,用Matplotlib做微调。比如画价格分布的直方图:
import seaborn as sns import matplotlib.pyplot as plt sns.set_theme(style='whitegrid', palette='muted') sns.histplot(df['price'], bins=30, kde=True) plt.title('Price Distribution') plt.xlabel('Price') plt.ylabel('Count') plt.show()注意一点,中文标题和图例需要设置字体,否则会变成方块。后面我单独讲这个问题。这个组合对日常分析足够了,而且出图速度快,适合放在笔记里记录结果。
4.2 动态交互图用ECharts,前端工程师那套也能拿来用
静态图适合看大方向,但有些场景需要交互,比如鼠标悬停看数值、筛选类别、数据下钻。这时候我选择ECharts,虽然它是前端技术栈里的工具,但Python也有对应的pyecharts库,用法非常接近原生ECharts,只是通过Python代码生成配置。
我的经验是,对于非前端背景的我来说,用pyecharts比直接在JavaScript里写更友好。比如画一个可交互的折线图:
from pyecharts.charts import Line from pyecharts import options as opts line = ( Line() .add_xaxis(date_list) .add_yaxis('销量', sales_list) .set_global_opts(title_opts=opts.TitleOpts(title='销量走势'), tooltip_opts=opts.TooltipOpts(trigger='axis')) ) line.render('sales_line.html')这个HTML文件可以直接用浏览器打开,交互效果很好。不过我后来发现一个坑:当数据量上千条的时候,ECharts渲染也会卡顿。解决方案是数据抽样或聚合到日/周维度,而不是直接把所有点都画上去。
4.3 颜色主题和字体是显眼包,别让默认样式拉低质感
做了几个图之后我开始意识到,图表的审美也很重要,同样一个柱状图,默认的蓝色一堆柱子挤在一起和调整过颜色间距的图,完全是两个观感。我总结了几个提升质感的小技巧:
- 配色不要超过三个主要颜色,可以用Seaborn内置的
muted、deep、pastel主题。 - 柱状图里的柱子宽度和间距要调节,让数据分布看起来舒服。
- 字体大小,标题、坐标轴、图例最好分级,比如标题16,轴标签12,图例10。
- 该加的标注要加,比如在最高点加个文字说明,比让读者自己看更友好。
我经常用Matplotlib的plt.rcParams统一设置:
plt.rcParams['figure.figsize'] = (10, 6) plt.rcParams['font.size'] = 12 plt.rcParams['axes.titlesize'] = 16 plt.rcParams['axes.labelsize'] = 12这行代码写在你所有画图代码之前,能让输出风格统一,尤其适合一篇笔记里有很多图的情况。
5. 实操:打造一个五步走的数据可视化小看板
5.1 数据准备:合并多张表,统一字段
这次实操我用了从某平台爬来的模拟数据,为了演示我把数据拆成了两张表,一张记录商品基本信息,一张记录每天的销售情况。第一步是合并,用merge,注意指定on和how。
df_items = pd.read_csv('items.csv') df_sales = pd.read_csv('sales.csv') df = pd.merge(df_sales, df_items, on='item_id', how='left')这里用的是左连接,因为销售表为主表,如果某些商品没匹配上基本信息,那它的商品名就是NaN,后面再处理。如果你用内连接,可能会丢掉一些销售记录,我不建议一开始就用内连接,先左连接保留全部销售数据更稳妥。
5.2 数据清洗:写一个能复用的清洗函数
我把前面说的清洗套路封装成一个函数,这样每次拿到新数据可以直接复用:
def clean_dataframe(df_raw): df = df_raw.copy() # 去重 df.drop_duplicates(inplace=True) # 字符串清洗 for col in df.select_dtypes(include=['object']).columns: df[col] = df[col].apply(clean_str) # 日期解析 if 'date' in df.columns: df['date'] = df['date'].apply(parse_date) df = df.dropna(subset=['date']) # 数值列转换 if 'price' in df.columns: df['price'] = df['price'].astype(str).str.replace('¥', '') df['price'] = df['price'].str.replace(',', '').astype(float) # 缺失值处理 df = handle_missing(df) return df df = clean_dataframe(df)注意在函数里先copy(),避免操作修改到原始数据。清洗完成后我通常会把结果保存成新文件:
df.to_csv('cleaned_data.csv', index=False, encoding='utf-8-sig')utf-8-sig这个编码很重要,不然用Excel打开中文就是乱码。
5.3 业务指标计算:转化率、趋势、TOP榜
清洗完就要算指标了。我的模拟数据里有“浏览数”和“购买数”,我定义一个转化率:
df['conversion_rate'] = df['purchase_count'] / df['view_count']这要小心分母为0的情况,所以最好加一个过滤:
df_view = df[df['view_count'] > 0].copy() df_view['conversion_rate'] = df_view['purchase_count'] / df_view['view_count']再按日期算总销量趋势:
daily_sales = df.groupby('date')['purchase_count'].sum().reset_index()TOP榜单就是按类别聚合:
top_categories = df.groupby('category')['purchase_count'].sum().sort_values(ascending=False).head(10)做完这些之后,数据层面就已经准备好了,可以进入画图环节。
5.4 图表的组合与排版
画图的时候我喜欢组合在一起看,用plt.subplots画多宫格,比如一行三列展示:转化率分布、每日销量走势、TOP10类别。
fig, axes = plt.subplots(1, 3, figsize=(18, 5)) sns.histplot(df_view['conversion_rate'], bins=30, kde=True, ax=axes[0]) axes[0].set_title('Conversion Rate Distribution') daily_sales['date'] = pd.to_datetime(daily_sales['date']) axes[1].plot(daily_sales['date'], daily_sales['purchase_count']) axes[1].set_title('Daily Sales Trend') axes[1].tick_params(axis='x', rotation=45) sns.barplot(data=top_categories, x='purchase_count', y='category', ax=axes[2]) axes[2].set_title('Top 10 Categories by Purchase Count') plt.tight_layout() plt.show()画完之后别急着保存,先肉眼看一遍,确认坐标轴是否自适应,标题有没有重叠,数据是不是有明显异常。我自己经常在这一步发现清洗遗漏,比如某个日期解析后变成1970年,或者某个类别的样本量只有个位数导致占比失真。
5.5 导出结果,附上我的成品图
最后导出图片用savefig,注意加dpi和bbox_inches='tight',不然图片被截断:
plt.savefig('dashboard.png', dpi=150, bbox_inches='tight')这次的成品图虽然简单,但比我第一次画的东西清爽多了。我看了一眼就明白了哪些类别的商品更受欢迎,转化率整体集中在哪个区间,销量有没有明显的周末效应。整个过程从清洗到出图,熟练的话二十分钟就能跑完。
6. 常见问题与排查技巧实录
6.1 图例中文乱码怎么办
中文乱码是新手最容易遇到也最沮丧的问题。代码看着没问题,图出来全是方块,网上搜了一堆答案,但经常不知道怎么用。
Matplotlib的解决方式是指定一个系统支持的中文字体,比如在Windows上:
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei'] plt.rcParams['axes.unicode_minus'] = False第一行设置字体,第二行是让负号正常显示。如果你用Mac,可以把'Microsoft YaHei'换成'PingFang SC'。别忘了设置之后重启一下内核或者在设置之后重新画图。
Seaborn画图也基于Matplotlib,所以设置一次生效。pyecharts这边不存在乱码问题,因为它是HTML渲染,字体由浏览器决定。
6.2 数据量太大图卡顿怎么优化
我有一次画一个几万行的时间序列折线图,Matplotlib直接卡了好几秒,生成的SVG图片巨大无比,浏览器打开都卡。后来我用两种方式解决:
第一种是采样,每N个点取一个:
sample = df.iloc[::10] # 每10个点取1个第二种是聚合,把细粒度数据聚合成日/周/月级别:
daily = df.set_index('date').resample('D').sum()聚合能展示趋势,同时不会丢失宏观信息。如果数据量大到内存都放不下,那我建议先做ETL而不是死磕一个DataFrame,后面用Dask或者直接进数据库再分析。
6.3 柱状图和折线图画在一起时坐标轴打架怎么处理
有一次我想在同一张图里既展示销量(柱状图)又展示转化率(折线图),结果转化率数值在0到1之间,销量在几百到几千,折线被压在底部根本看不见。解决方案是用双y轴:
fig, ax1 = plt.subplots() ax2 = ax1.twinx() ax1.bar(daily['date'], daily['purchase_count'], color='#4C72B0', label='sales') ax2.plot(daily['date'], daily['conversion_rate'], color='#DD8452', label='conversion') ax1.set_ylabel('Sales Count') ax2.set_ylabel('Conversion Rate')注意要加上图例的时候,图例需要分别从两个axes获取。
6.4 清洗时把行删多了,如何避免误删
我经常在清洗中手滑删多了,尤其是dropna()的时候没指定subset,结果很多有用的行被删掉。现在我每次删行前都会打印一下当前行数,而且养成“先备份关键列”的习惯:
backup_cols = df[['item_id', 'date']].copy() df = df.dropna(subset=['price', 'date']) print(f'dropped: {len(backup_cols) - len(df)} rows')万一后面发现删多了,还能从备份里找回部分数据。另外建议清洗全程使用inplace=False,每次操作生成新对象,这样能保留中间步骤,便于回溯。
7. 笔记之外:几点体会
写到这里,第三篇学习笔记也差不多该收尾了。我想分享一个贯穿我整个学习过程的经验:不要追求一次把所有工具都掌握,而是要带着问题去学。我学数据清洗和可视化的时候,并不是先把pandas所有函数背完才开始动手,而是真遇到乱码、遇到日期解析失败、遇到图画出来很丑,才去查资料、试方案。这种“问题驱动”的方式记忆更牢,而且每解决一个问题,下一次遇到类似情况就能直接秒杀。
另外我也想给自己提个醒,也是给同样在学习的你一个建议:图表是给别人看的,不是自嗨。画完图之后,退后一步看看,能不能三秒钟之内看懂它表达的核心信息?如果不能,就需要重新设计。把坐标轴标签写清楚、把结论直接标在图上、删掉不必要的装饰,这些比堆砌酷炫的图表技巧重要得多。
这次的学习笔记就先到这里。下一篇我大概率会去研究一下怎么把清洗和可视化流程自动化,比如写一个定时任务,每天自动更新数据并生成最新看板。等实践出结果了,我会回来继续记录。