
这已经是我第三篇学习笔记了。前两篇我分别搞定了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, errorscoerce) df[date] df[date].apply(parse_date)这里特别强调errorscoerce解析失败会变成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], inplaceTrue) # 数值列用中位数填充 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(ascendingFalse)这里为什么用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(indexprice_range, valuesrating, aggfuncmean)这样一张表就能看出来是不是越贵的评分越高或者中间价位反而评分更好。pivot_table比多次groupby写起来更简洁而且能同时聚合多个指标比如aggfunc{rating: mean, price: count}一次性拿到评分均值和商品数量。学会这两个函数百分之八十的汇总工作都能解决。4. 可视化工具选型与踩坑Matplotlib、Seaborn还是ECharts4.1 静态图用MatplotlibSeaborn怎样快速上手一开始我只用Matplotlib感觉每次画图都要写一堆细节代码连坐标轴标签都要手动设置。后来我发现Seaborn是在Matplotlib之上做的封装画统计图特别省事比如箱线图、热力图、分布图都能一行代码出图。我的默认组合是用Seaborn画图用Matplotlib做微调。比如画价格分布的直方图import seaborn as sns import matplotlib.pyplot as plt sns.set_theme(stylewhitegrid, palettemuted) sns.histplot(df[price], bins30, kdeTrue) 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_optsopts.TitleOpts(title销量走势), tooltip_optsopts.TooltipOpts(triggeraxis)) ) 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, onitem_id, howleft)这里用的是左连接因为销售表为主表如果某些商品没匹配上基本信息那它的商品名就是NaN后面再处理。如果你用内连接可能会丢掉一些销售记录我不建议一开始就用内连接先左连接保留全部销售数据更稳妥。5.2 数据清洗写一个能复用的清洗函数我把前面说的清洗套路封装成一个函数这样每次拿到新数据可以直接复用def clean_dataframe(df_raw): df df_raw.copy() # 去重 df.drop_duplicates(inplaceTrue) # 字符串清洗 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, indexFalse, encodingutf-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(ascendingFalse).head(10)做完这些之后数据层面就已经准备好了可以进入画图环节。5.4 图表的组合与排版画图的时候我喜欢组合在一起看用plt.subplots画多宫格比如一行三列展示转化率分布、每日销量走势、TOP10类别。fig, axes plt.subplots(1, 3, figsize(18, 5)) sns.histplot(df_view[conversion_rate], bins30, kdeTrue, axaxes[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(axisx, rotation45) sns.barplot(datatop_categories, xpurchase_count, ycategory, axaxes[2]) axes[2].set_title(Top 10 Categories by Purchase Count) plt.tight_layout() plt.show()画完之后别急着保存先肉眼看一遍确认坐标轴是否自适应标题有没有重叠数据是不是有明显异常。我自己经常在这一步发现清洗遗漏比如某个日期解析后变成1970年或者某个类别的样本量只有个位数导致占比失真。5.5 导出结果附上我的成品图最后导出图片用savefig注意加dpi和bbox_inchestight不然图片被截断plt.savefig(dashboard.png, dpi150, bbox_inchestight)这次的成品图虽然简单但比我第一次画的东西清爽多了。我看了一眼就明白了哪些类别的商品更受欢迎转化率整体集中在哪个区间销量有没有明显的周末效应。整个过程从清洗到出图熟练的话二十分钟就能跑完。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, labelsales) ax2.plot(daily[date], daily[conversion_rate], color#DD8452, labelconversion) 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(fdropped: {len(backup_cols) - len(df)} rows)万一后面发现删多了还能从备份里找回部分数据。另外建议清洗全程使用inplaceFalse每次操作生成新对象这样能保留中间步骤便于回溯。7. 笔记之外几点体会写到这里第三篇学习笔记也差不多该收尾了。我想分享一个贯穿我整个学习过程的经验不要追求一次把所有工具都掌握而是要带着问题去学。我学数据清洗和可视化的时候并不是先把pandas所有函数背完才开始动手而是真遇到乱码、遇到日期解析失败、遇到图画出来很丑才去查资料、试方案。这种“问题驱动”的方式记忆更牢而且每解决一个问题下一次遇到类似情况就能直接秒杀。另外我也想给自己提个醒也是给同样在学习的你一个建议图表是给别人看的不是自嗨。画完图之后退后一步看看能不能三秒钟之内看懂它表达的核心信息如果不能就需要重新设计。把坐标轴标签写清楚、把结论直接标在图上、删掉不必要的装饰这些比堆砌酷炫的图表技巧重要得多。这次的学习笔记就先到这里。下一篇我大概率会去研究一下怎么把清洗和可视化流程自动化比如写一个定时任务每天自动更新数据并生成最新看板。等实践出结果了我会回来继续记录。