☰
Pandas数据清洗与可视化实战:从脏数据到洞察的完整链路
2026/10/8 8:42:19 网站建设 项目流程

咱们整天聊数据分析,很多人一上来就抱着“我要用机器学习建模”的心态,结果卡在第一步就懵了:数据拿到手根本没法直接用。作为常年和数据打交道的人,我越来越觉得,真正决定一个分析项目成败的环节,往往不是后面那个花哨的模型,而是前期的数据清洗工作。Pandas在这块儿就是一把锋利的瑞士军刀,从读取杂乱无章的原始数据,到整理成规规矩矩的表格,再到最后画出能讲出故事的图表,全链路打通。这篇文章我就结合自己实际干活的经验,从数据清洗一路聊到可视化,把那些文档里不常写的取舍逻辑和踩过的坑一并倒出来,希望能给正在这条路上摸索的朋友一些参考。

先放一句我个人的总结:数据分析百分之八十的时间都花在清洗和整理数据上,这很正常,也不要觉得枯燥。把清洗这关过了,你会发现连画图都顺手很多。

1. 为什么数据清洗比建模更值得花时间:一个反直觉的结论

大多数人提起数据分析,第一反应是那个高深莫测的模型训练过程,似乎只有敲出一行行算法代码才算得上“技术活”。但真正在项目里摸爬滚打过的朋友心里都清楚,数据清洗才是整个分析链路里最耗时、最磨人、也最决定成败的环节。数据科学家圈子里流传着一句老话,叫“垃圾进,垃圾出”,你再牛的分析算法,喂进去一堆格式混乱、缺胳膊少腿、充满重复和异常的数据,输出的结果也毫无价值可言。这个阶段的工作虽然看起来平平无奇,但恰恰是这种“不起眼”,才让后面的分析能够顺理成章地展开。

Pandas作为一个专门为数据处理而生的Python库,之所以在数据分析领域地位如此稳固,靠的并不是什么高深莫测的模型算法,而是它提供了极其丰富且高效的数据清洗与整理工具。从读入各种杂乱的源数据开始,Pandas就能帮你把不同格式的数据统一成结构化表格,把缺失值、重复值、异常值这些“顽疾”逐一揪出来处理掉,再把字段类型、业务口径通过转换对齐。可以说,Pandas替你把“数据厨艺”里的洗菜、切菜、配菜功夫全都包揽了,等你真正要“下锅炒菜”也就是做分析、建模、可视化的时候,手里的料已经是干干净净、整整齐齐的了。

这种体验上的差异,说个很直白的例子你们就明白了。我曾经接过一个电商平台的月度销售数据,原始文件是从业务系统导出的,里面日期字段有的是“2024/08/15”,有的是“20240815”,还有的是“15-Aug-2024”,下单金额有的带人民币符号,有的带千分位逗号,甚至还有几条记录的用户ID直接是空字符串。如果不做清洗,光是统一这个日期格式就够你喝一壶的,更别提后续按日、按月做趋势分析了。而用Pandas,几行to_datetime配合自定义格式解析,就能把这些五花八门的字符串一股脑儿转成标准的日期类型,后续所有时间维度的聚合都变得顺理成章。

所以,我的建议是:无论你的项目目标是做一个简单的描述性统计,还是打算上机器学习模型,都请先正视数据清洗这个环节。它不酷炫,但它是地基。你愿意在地基上花多少功夫,直接决定了你的分析结果能盖多高的楼。这篇文章之所以从一开始就聊这个,就是想帮大家把这个容易被低估的环节,放到它应有的核心位置上来。

2. 开始之前:Pandas的数据结构认知与安装避坑

聊清洗之前,得先把手里的工具摸清楚。Pandas有两个核心的数据结构,一个是Series,一个是DataFrame。很多人一开始学的时候容易混淆,我这里用个大白话解释一下:Series就是带标签的一维数组,像一列数据配上它每行的名字;DataFrame就是一张二维表格,既有行索引又有列名,你平时在Excel里看到的那种表,它的形态就非常接近DataFrame。

为什么要强调这两个概念?因为后面所有的事情——筛选、清洗、聚合、合并——本质上都是围绕这两种结构来展开的。DataFrame可以看作是由多个Series拼接而成的一张表,所以你对一列数据进行清洗操作,本质上就是在操作一个Series。搞清楚了这一层关系,你在写代码的时候思路就会清晰很多。

在实际写代码的第一步,往往是安装Pandas。这件事看着简单,但很多人第一次装就栽了跟头,尤其是用pip install pandas的时候,速度慢到令人发指不说,还经常中途报错超时。这里分享一个可靠的办法,直接用清华的镜像源下载,速度飞快,还能绕开不少网络问题。命令行下执行:

pip install pandas -i https://pypi.tuna.tsinghua.edu.cn/simple

另外,你要是用的PyCharm,其实不需要单独跑到命令行里去敲命令。直接在PyCharm的设置里找到Project Interpreter,点加号搜索pandas,点安装就能搞定,它会自动帮你处理依赖关系。不过我个人经验是,如果你已经开始用PyCharm了,尽量用虚拟环境来装,避免不同项目之间的包版本冲突。那种“我明明装好了pandas,怎么跑代码还是报ModuleNotFoundError”的情况,多半就是装到了系统环境,而PyCharm用的是虚拟环境,两边没对上。

装好之后,习惯上都会给它起一个别名,也就是那句经典代码:

import pandas as pd

这行代码,基本可以看作是Pandas世界的入场券。需要注意的是,有些教程会顺带让你装一个叫numpy的库,别嫌多,Pandas底层运算依赖它,你后面用Pandas做条件筛选、向量化运算时,很多结果返回的也是numpy类型,装好无害且有用。

还有一个小提示:如果你的数据文件比较大,比如有几百MB那种,在读取的时候会有明显等待时间。这时候可以考虑用read_csv的chunksize参数分块读取,或者直接用dtype参数指定某些列的数据类型,减少内存消耗。别小看这个,处理真实项目数据的时候,光是一个memory error就能让你心态崩掉,提前用这些参数做优化比事后补救强太多。

3. 数据清洗的四个核心关卡:缺失值、重复值、异常值与类型乾坤大挪移

数据清洗本身是个细碎活,但如果把它拆解开来,核心无非四个关卡:缺失值、重复值、异常值、数据类型。每一关都有它特定的处理手法和考量逻辑。接下来我逐一展开,把我平时的处理思路和代码一并放出来。

3.1 缺失值处理:先判断“为什么缺失”,再谈“怎么补”

缺失值是数据清洗里最常遇到的关卡。拿到一张表,第一件事别急着dropna,先搞清楚这些缺失值到底是怎么来的。它们可能是业务上的天然缺失,也可能是数据采集时的遗漏,这两种情况的处理逻辑是完全不同的。我习惯先通过isnull()方法配合sum()统计一下列的缺失情况,大致有个底:

import pandas as pd df = pd.read_csv('sales_data.csv') missing_stats = df.isnull().sum() print(missing_stats[missing_stats > 0])

这里有个容易忽略的细节:isnull()对空字符串''是识别不出来的,它只认NaN、None这类Pandas标准缺失值标记。所以很多时候你扫一眼数据,觉得某些字段明明是空的,但统计出来的缺失值却是0。这也算是一个常见的坑吧,处理的时候可以先用strip()去掉空格,再把空字符串统一替换成NaN:

df['customer_name'] = df['customer_name'].str.strip().replace('', pd.NA)

替换之后,再重新统计缺失情况,就会准确很多。

真正处理缺失值的时候,面对不同的列有不同的策略。数值型字段,比如销售额、点击量,如果你的数据分布比较均匀,缺失比例也不高,直接用均值或者中位数填充是可行的;但如果数据波动很大,或者存在明显的离群值,用均值填充就容易干扰整体分布,这时候用中位数会更稳健。如果缺失的字段是类别型的,比如用户等级、所属区域,用众数填充或者单独标记一个“未知”类别,通常是不错的选择。

还有一类情况:这个字段本身对业务决策没有多大意义,比如某个人工备注列,缺失率高达八成,保留着对后续分析也没帮助,那直接drop掉整列,反而是最干净的处理方式。判断依据就是一句大白话:留着它,能不能为后面的分析提供有效信息?不能,就删。

至于彻底删除含有缺失值的行,也就是dropna(),则需要谨慎再谨慎。如果缺失的行数占总体比例很小,比如不到5%,而且这些行分布较随机,删掉对整体结果影响不大,那可以删。但如果缺失比例高,或者缺失集中在某些关键字段上,删了会导致样本严重偏差,那还是得老老实实走“填充”这条路线。

3.2 重复值处理:别无脑去重,先看清业务语义

重复值这个话题,听起来特别简单,好像drop_duplicates()一行代码就完事了。但实际项目里,这里面的门道并不比缺失值少。

首先,你需要搞清楚什么叫“重复”。在Pandas里,默认的去重逻辑是按行判断,也就是说,如果两行数据的每一列都一模一样,才会被判定为重复。但真实业务中,这种全列重复的记录其实不多见,更常见的是“关键字段重复”。比如用户行为日志,同一用户在同一时间点产生了两条记录,时间、用户ID完全相同,只有某个非核心属性略有差异,这时候你就得想清楚:到底该按哪些字段判断重复才算合理?

我一般会先看业务主键,如果明确知道哪几列组合起来应该唯一,比如订单号、用户ID加下单时间,那就按这些列去重。drop_duplicates方法在这方面给了足够的灵活性:

df = df.drop_duplicates(subset=['order_id', 'customer_id', 'order_time'], keep='first')

keep参数也有讲究。keep='first'会保留重复组里的第一条记录,keep='last'保留最后一条,而keep=False则是把所有重复记录全删掉。具体用哪个,取决于你的业务场景:如果数据是按时间顺序追加的,那最后一条往往状态最新,保留last更合适;如果几条记录之间没有先后语义差别,保留first也没问题。千万别一上来就keep=False,那很可能会把你本来想保留的那条有效记录也给误删了。

另外还有一个高频操作,就是先查一下到底有多少重复行,再做决定:

duplicate_count = df.duplicated(subset=['order_id']).sum() print(f"发现重复订单数: {duplicate_count}")

这个输出可以让你对数据集的质量有个直观判断。如果重复比例很高,说明上游数据链路可能存在问题,不只是清洗的问题了,得更深一层去排查根源。

3.3 异常值识别:用好描述性统计和箱线图

异常值处理也是清洗环节里的一大重头戏。所谓异常值,就是明显偏离正常取值范围的数据点。比如订单金额出现负数,比如用户年龄显示200岁,这些显然都是不合理的。对于这类异常,处理方法相对简单粗暴——直接过滤掉或者替换成缺失值再走填充逻辑:

df = df[df['age'].between(0, 120)]

但有些异常值比较隐蔽,单靠肉眼和经验看不出问题。这时候就要借助统计学工具了。我惯用的做法是先用describe()一键生成描述性统计,看一下各个数值列的min、max、四分位数,注意力重点放在那些极值上。

numeric_cols = df.select_dtypes(include=['number']).columns print(df[numeric_cols].describe())

比如你可以看到某列数值的75%分位数是80,但最大值却是10000,这种巨大的落差背后往往藏着异常值。进一步确认的方法可以用箱线图,也叫箱型图。它通过四分位距(IQR)来判断离群点,用代码画出来很简单,借助matplotlib或者seaborn都可以:

import matplotlib.pyplot as plt import seaborn as sns plt.figure(figsize=(10, 6)) sns.boxplot(data=df['sales_amount']) plt.show()

箱线图的好处在于它不依赖对数据的先验认知,而是纯粹从数据分布本身出发,识别出那些落在上下须之外的点。这些点到底是不是异常,还得结合业务来判断。有些时候,某个“异常点”恰恰是业务上的重要信号,比如大促期间的成交量暴增。这时候你不应该粗暴地把它删掉,而应该单独标记它们在分析中的权重。

关于异常值,我的原则是:先识别,再分类,最后才能决定处理方式。可修正的修正,可剔除的剔除,需要保留的信号就单独处理。千万别一看到“异常”就下意识删除,那会让你错过很多有价值的信息。

3.4 数据类型转换:让每一列都变成它该有的样子

数据类型转换是清洗过程中一个看似琐碎但影响重大的环节。Excel读进来的数据经常会出现“看似数值其实是字符串”的状况,比如金额列带着货币符号,ID列被读成了数字导致前面的0丢失,日期列变成了各种格式混杂的对象类型。这些如果不加处理,后面做聚合计算、排序、时间序列分析的时候,各种诡异的报错就会接踵而至。

Pandas里的核心方法是astype(),它可以帮你把列从字符串转换成数值、从数值转换成字符串等。比如订单ID如果是013开头的,读进来会被当成整数1去掉前导零,这时候你就需要先进性字符串填充:

df['order_id'] = df['order_id'].astype(str).str.zfill(6)

这里的zfill(6)是把位数不够的ID前面补0补到6位。

日期处理就更常见了。pd.to_datetime()是处理日期列的神器,它能自动识别大部分常见的日期格式。但对于那种格式特别混乱的列,建议显式指定格式参数,避免它自作聪明解析错误:

df['order_date'] = pd.to_datetime(df['order_date'], format='%Y-%m-%d')

%Y代表四位年份,%m代表两位月份,%d代表两位日期,这个格式串你可以根据实际数据的样式灵活调整。转换之后,你还可以顺手用它提取年、月、周、星期几等新字段,为后续的时间维度分析做好准备。

还有个值得留意的小点是:astype('category')可以把某些取值有限的字符串列转换成Pandas的类别类型。这样做除了语义上更清晰,还能大幅减少内存占用,尤其是对那些有几百万行的数据集来说,效果非常可观。反正我处理那种“省份”“性别”“支付方式”这类列时,只要有性能顾虑,就会优先考虑转成category类型。

4. 从清洗走向分析:筛选、聚合、合并的实战思路

清洗工作做到位之后,接下来就要开始真正的数据处理和分析操作了。这一阶段的核心工作,简单说就是三件事:筛选出你需要的数据子集、聚合出有价值的统计信息、把多张表按关键字段拼接起来。这三板斧用熟了,大部分业务分析需求你都能稳稳接住。

4.1 条件筛选:用好布尔索引与query方法

筛选数据,直观的做法是布尔索引。比如我想筛选出订单金额大于500且支付成功的所有记录:

paid_orders = df[(df['sales_amount'] > 500) & (df['payment_status'] == 'paid')]

这里的关键点是:多个条件必须用括号括起来,条件之间用&(与)、|(或)、~(非)来组合。很多人第一次写这段很容易漏掉括号,结果报错或者结果不对。所以每次写完条件筛选,我都建议立刻打印一下shape看看行数有没有变化,以及head()看看结果是否符合预期。

如果你觉得一堆df['列名']写起来又长又不直观,Pandas还提供了一个叫query的接口:

paid_orders = df.query("sales_amount > 500 and payment_status == 'paid'")

这个写法和SQL语感很接近,方便你直接在字符串里写筛选逻辑。这里唯一要注意的是列名如果有特殊字符或者空格,需要用反引号引起来。总体来说,query方法在列数多、条件复杂的情况下可读性更好,也更容易维护。

筛选操作不仅是简单的行过滤,也可以配合列选择一起使用。比如我只需要订单号、金额和日期三列,筛选完再按列名提取即可:

result = paid_orders[['order_id', 'sales_amount', 'order_date']]

列选择还有一个高级用法,filter(regex=...)可以根据正则表达式匹配列名,比如你想一次性选出所有以“_amount”结尾的列,这个方式就会非常高效。

4.2 分组聚合:从groupby到透视表的层层递进

分组聚合是业务分析里出镜率最高的操作。想要知道每个区域的销售额总和?每个产品类别的平均订单金额?每种支付方式的使用次数?这些需求问法不同,但底层都是同一个套路——groupby。

region_summary = df.groupby('region')['sales_amount'].sum().reset_index()

这里的写法拆开来看是:先按region分组,然后对sales_amount列做sum求和,最后reset_index把分组字段从索引变回普通列,方便后续查看和处理。如果你不需要索引变回列,可以直接用as_index=False这个参数,效果一样但更简洁:

region_summary = df.groupby('region', as_index=False)['sales_amount'].sum()

分组之后能做的聚合函数五花八门,sum、mean、count、max、min、median、std等等都是常用操作。如果你想同时算多个指标,可以用agg方法传一个字典进去:

product_stats = df.groupby('product_category').agg( total_sales=('sales_amount', 'sum'), avg_sales=('sales_amount', 'mean'), order_count=('order_id', 'count') ).reset_index()

这样一张表里既能看到总销售额,又能看到平均客单价和订单量,后续做业务复盘时一张表就能看明白很多问题。如果你的分析需求更复杂,比如行列都要做交叉分类统计,就可以考虑用pivot_table:

pivot = pd.pivot_table(df, values='sales_amount', index='region', columns='product_category', aggfunc='sum', fill_value=0)

透视表在Excel里大家都很熟悉了,Pandas里的用法逻辑其实差不多:values指定需要汇总的数值列,index指定行索引,columns指定列索引,aggfunc指定聚合方式。有一个小细节,fill_value=0能把表格中因为无数据而产生的NaN填充为0,这样后续无论是导出还是做可视化,都不会被缺失值干扰。

4.3 多表合并:理解merge与concat的不同场景

处理真实业务数据,你不会总在一张表里打转。用户信息是一张表、订单明细是一张表、商品信息是第三张表,这时候就需要把表按某些关键字段连接起来。Pandas里最常用的两个方法,一个是merge,一个是concat。

merge比较适合做“SQL风格”的连接操作。比如我有订单表orders和用户表users,两张表通过customer_id关联,我想要在订单表上补充用户所在城市、会员等级等信息,就可以这么写:

merged_df = pd.merge(orders, users, on='customer_id', how='left')

参数的含义需要重点说说。on指定连接键;how决定连接方式:left是保留左表全部记录,右表没有匹配到的数据用NaN填充;right相反;inner是只保留两边都能匹配上的记录;outer是全连接,保留两边所有记录。分工协同上,我90%的场景用的都是left,因为主分析表通常就一张,其他的表只是靠上去“附赠”附加字段,用一个left join最稳妥。

concat则更像是纯粹的“拼接”。两种常见情况:一种是按行拼接多张结构相同的表,比如1到12月的月度数据,每个月一张表,把12张表上下堆叠成全年数据;另一种是按列拼接,把两张列不同的表左右并排。前者用axis=0,后者用axis=1:

year_data = pd.concat([jan_df, feb_df, mar_df], axis=0, ignore_index=True)

这里ignore_index=True很关键,因为上下拼接时索引容易冲突,加上这个参数重新生成连续索引,能避免后续很多莫名其妙的索引问题。

多表合并有一个高频出现的坑:连接键的数据类型不一致。比如订单表里的customer_id是数值类型,用户表里却是字符串类型,直接合并时会发现匹配不上,结果merge出来一堆NaN。遇到这种情况,先用astype(str)统一两边键的数据类型再做合并,问题就能顺利解决。

5. 让数据开口说话:Matplotlib与Seaborn可视化实战

数据整理完了,统计指标也出来了,但一堆数字放在那里,人眼很难快速找出规律。这时候就需要把数据“画”出来,用图表讲清楚数据背后的故事。Python生态里最常用的两把可视化利器就是Matplotlib和Seaborn。Matplotlib底子扎实、自定义能力极强,Seaborn则是站在Matplotlib肩膀上的高级接口,画统计图表既简单又好看。两者配合使用,几乎能覆盖日常所有可视化需求。

5.1 绘图的第一步:设置全局风格与中文字体

很多人第一次用Matplotlib画图,第一个撞上的问题就是:图表里的中文全部变成了小方块。这是因为Matplotlib默认字体不支持中文。解决办法很简单,提前设置中文字体:

import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] plt.rcParams['axes.unicode_minus'] = False

第一行指定了无衬线字体优先使用黑体或微软雅黑,第二行是解决负号显示异常的问题——如果不设置这个,坐标轴上的负号经常会变成一个莫名其妙的方块。这两行代码建议放在脚本最开头,一劳永逸,后面所有画图都不会再被字体问题困扰。

在颜值方面,可以用Seaborn的全局设置快速统一风格:

import seaborn as sns sns.set_theme(style='whitegrid', palette='muted')

这一行就能让所有图表的背景变成带有网格线的浅色底,配色也变得柔和统一,图表整体质感立刻就上来了。具体绘图的时候,还可以再微调坐标轴标签、图例位置等细节。

5.2 单变量与多变量图表的选型逻辑

面对不同的数据形态和分析目的,选对图表类型是可视化成功的关键。我在这里把最常用的几种场景和相应的图表列一下,方便你直接对号入座。

数值分布:画直方图(hist)或核密度图(kde),用来了解单列数据的分布形态。

plt.figure(figsize=(10, 6)) sns.histplot(df['sales_amount'], bins=30, kde=True) plt.xlabel('订单金额') plt.ylabel('频数') plt.title('订单金额分布') plt.show()

bins控制柱子的数量,kde=True会在直方图上方叠加一条平滑的密度曲线,让你更容易看出分布是否偏态、是否有多个峰值。

类别对比:用柱状图(barplot)或者箱线图(boxplot)。柱状图适合对比不同类别的总量或平均值,箱线图则适合展示不同类别下的数值分布差异。

plt.figure(figsize=(12, 6)) sns.barplot(data=region_summary, x='region', y='sales_amount') plt.title('各区域销售额对比') plt.show()

如果不同组别的数据量差异大,柱状图上还可以加置信区间误差条,这也是barplot默认带上的一项好处,在做A/B测试场景时格外有用。

时间趋势:最常用的是折线图(lineplot)。但这里有个前提:时间列必须先转成datetime类型并且排序好,否则画出来的折线会乱成一团。

daily_sales = df.groupby('order_date')['sales_amount'].sum().reset_index() plt.figure(figsize=(14, 6)) sns.lineplot(data=daily_sales, x='order_date', y='sales_amount') plt.xticks(rotation=45) plt.title('每日销售趋势') plt.tight_layout() plt.show()

rotation=45让横轴日期标签旋转45度避免重叠,tight_layout()自动调整间距防止标签被裁剪,这两个细节会让图表看起来专业很多。

多变量关系:散点图(scatterplot)是起步首选,可以用颜色映射第三维,比如会员等级、地区等:

plt.figure(figsize=(10, 8)) sns.scatterplot(data=df, x='unit_price', y='sales_amount', hue='membership_level', alpha=0.6) plt.xlabel('单价') plt.ylabel('销售金额') plt.title('单价与销售金额关系') plt.show()

alpha=0.6设置了点的透明度,当数据点多、重叠严重时,这个参数能让密集区域的分布状态清晰可见。hue参数按照会员等级给点上了色,可以直观看到不同等级用户的消费模式差异。

选图核心逻辑就一句话:想清楚你要展示的是“分布”“对比”还是“趋势”,然后选对应的图表。别为了炫技硬上一些花哨图,能用柱状图讲清楚的事,用雷达图反而容易让人觉得刻意。

5.3 布局与保存:让你的图表既有面子又有里子

在实际汇报或者写文档时,你往往不会只画一张图,而是需要把多个图组合在一起对比。Matplotlib提供的subplots方法就可以实现多子图布局:

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

这行代码创建了一个两行两列的画布,axes是一个二维数组,你可以通过axes[0][0]这样定位到第一个子图,然后在每个子图里绘制想要的内容。用完了如果不希望子图之间相互干扰,还能配合plt.tight_layout()自动调整子图间距。

图表绘制好以后,保存成图片也是常规操作。但这里有个细节:直接用plt.show()之后保存,图片往往是带弹窗的交互界面,不方便后续嵌入报告。更好的方式是直接savefig:

plt.savefig('sales_dashboard.png', dpi=150, bbox_inches='tight')

dpi=150是分辨率参数,数字越高图片越清晰;bbox_inches='tight'会自动裁剪掉图片周围的空白边缘,让内容区域占满整张图。保存时注意先savefig再show,或者确保没有开启交互阻止模式,否则显示出来的图跟你保存下来的图可能不太一样。

这里还想额外提一个经验:保存图表文件时,文件名尽量语义清晰,比如用“202408_区域销售对比.png”这种带时间维度的命名方式,时间长了找图的时候方便,而且不容易覆盖同名文件。

6. 别只会跑通代码:一个设备健康度看板的实战复盘

之前的章节多少有点“知识点罗列”的感觉,这一部分我想用一套完整的小项目串一遍全流程,让大家看看清洗、分析、可视化是怎么在实际场景里咬合起来的。这个项目的背景是我之前做过的一个小任务:基于一批物联网设备上报的历史运行数据,分析设备健康状态并输出一个可视化看板。

6.1 原始数据的“脏乱差”现场

我拿到的原始数据是系统导出的CSV文件,将近10万行,列包括设备ID、上报时间、温度读数、CPU使用率、内存占用率、网络延迟、状态码等。光看几个样本,问题就不少:上报时间格式有的是2024-07-21T08:30:00Z这种UTC标准格式,有的是2024/07/21 08:30,还有个别行根本就是个空值;温度读数有极个别记录达到99.9,明显是传感器异常;CPU使用率一列里混着“85%”和“0.73”两种语义完全不同的写法;内存占用率存在部分负值,这种不用说,肯定是采集程序有bug。

这些脏乱差的点,如果不处理,后面画时间趋势图根本没法看,算平均负载的时候也会被异常值带偏。这也正是清洗环节价值最好的印证。

6.2 清洗逻辑落地:一行行代码把问题掰正

面对这份数据,我首先做了一次全量扫描:

print(df.dtypes) print(df.isnull().sum())

然后分步处理。第一,统一时间格式。把UTC时间字符串和普通北京时间字符串全部转成datetime类型:

df['report_time'] = pd.to_datetime(df['report_time'], errors='coerce', utc=True) df['report_time'] = df['report_time'].dt.tz_convert('Asia/Shanghai')

这里errors='coerce'表示解析不了的直接转成NaN,后续统一处理。utc=True先按UTC解析,再统一转换到北京时间,保证时区一致性。

第二,处理温度异常值。先画个箱线图看一下分布,确认哪些点落在合理范围之外,再用95%分位数做上下限过滤:

Q1 = df['temperature'].quantile(0.25) Q3 = df['temperature'].quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR df = df[(df['temperature'] >= lower_bound) & (df['temperature'] <= upper_bound)]

第三,把CPU使用率统一成小数形式。凡是带%符号的,先去掉百分号再除以100;原来的小数形式维持原值:

def parse_cpu(value): if isinstance(value, str) and value.endswith('%'): return float(value.strip('%')) / 100 return float(value) df['cpu_usage'] = df['cpu_usage'].apply(parse_cpu)

第四,内存负值直接替换为NaN,再用列均值做填充:

df.loc[df['memory_usage'] < 0, 'memory_usage'] = pd.NA df['memory_usage'] = df['memory_usage'].fillna(df['memory_usage'].median())

把这些步骤走完,再跑一次df.describe(),会发现各个字段的分布都变得合理了,数据质量有了质的提升。

6.3 分析重点与看板呈现

数据清洗干净之后,后续的分析思路就比较顺了。我先按设备ID分组,计算每个设备的平均CPU使用率、平均温度、内存平均占用率,然后把高于一定阈值的设备标记为“高负载设备”。再按天聚合,看整体集群的负载随时间的变化趋势,尝试找出是否存在固定的高峰时段。

最终的可视化看板,我做了一个多子图的组合,包括三块内容:左上角是整体CPU使用率的时间趋势折线图,右上角是温度分布的箱线图,下方左右分别展示负载最高的前10个设备柱状图和内存占用分布的直方图。整个看板用subplots排布,输出成PNG文件,方便直接嵌入周报里。

做这个项目的过程中,我最大的体会是:清洗工作做得越细致,后面分析和展示就越顺滑。很多你觉得“画出来怎么这么丑”的图表,往往不是绘图代码的问题,而是数据本身没处理好,比如时间轴乱序、有缺失段、有极端值拉高坐标轴。数据干净了,哪怕用最简单的图表类型,效果也能立得住。

7. 我踩过的坑,希望你别再踩一遍

最后这部分,聊聊我在实际使用Pandas做数据清洗和可视化时,踩过的一些至今记忆犹新的坑。这些点都非常细节,但每一个都可能让你白折腾好几个小时。

第一个坑:链式赋值操作。刚学Pandas的时候,我习惯写df[df['age'] > 18]['age'] = 99这样的代码,想对筛选出来的子集做修改。结果运行完发现原表根本纹丝不动,甚至有时候还会收到一堆警告。原因在于“链式索引”有时候看到的是DataFrame的副本,而不是视图,你在副本上改了等于白改。正确的做法是用loc进行条件赋值:

df.loc[df['age'] > 18, 'age'] = 99

记住这个原则:要修改DataFrame时,优先用loc而不是链式索引。可以少踩很多坑。

第二个坑:读取CSV时默认把某些列解析成了错误类型。比如订单号的数值过大,或者包含前导零,被Pandas默认读成整数后会丢失精度。解决方法是read_csv时显式指定dtype,把订单号按字符串读取:

df = pd.read_csv('orders.csv', dtype={'order_id': str})

早年间我为了这个丢精度的问题,排查了一个下午才发现,原来几千几万条订单号后面几位全都变成了0。这类问题如果不在读取这一步就把类型锁死,后面再怎么清洗也补不回来。

第三个坑:合并时索引没对齐,产生大量NaN。这事常见于把两张索引不同但其实是一一对应的表直接合并时,经常会出现明明数据都有,结果合并后全都是NaN的情况。关键是要在merge之前先确认连接键的唯一性,并且统一连接键的数据类型。再不行就先用reset_index()把索引重置成普通列再合并,别让索引掺和进来捣乱。

第四个坑:绘图时中文字体不生效。有时候你明明设置了font.sans-serif为SimHei,但某个图表里的中文还是方框。这种情况多半出在使用了seaborn的主题覆盖或者局部修改了rcParams。解决办法是在绘图之前重新设置一次全局字体,并且确保设置代码在任何plt.figure()或者sns.set_theme()之前执行。

第五个坑:groupby之后忘了reset_index(),导致后续处理逻辑混乱。分组聚合后得到的结果按分组字段做了索引,看起来没什么大不了,但你再想去按列筛选或者绘图,索引层级就会带入图中,经常导致横纵坐标显示异常。所以我平时只要不是专门要做层级索引操作,一律顺手加上reset_index(),把结果拉平回普通的二维表结构。

这些坑说来说去,本质都是对Pandas内部的数据视图、索引机制、类型推断理解不够深入造成的。不做这一行的人是体会不到的,只有亲手踩一遍,再看一眼错误提示,才会真正记住。我把它们写在这里,就是希望大家能在看这篇文章时就建立起这个意识,少浪费一些无谓的调试时间。

说到底,Pandas数据分析这条链路,从数据清洗到可视化,每一步都像是在给数据“梳妆打扮”。你梳得越细致,它给你的反馈就越惊艳。如果你正在为手里的脏数据发愁,不妨耐下心来,按这篇文章的思路一步一步来试试。清洗过了坎,后面的分析之路会顺畅很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询