做数据分析这些年,我带过不少新人,发现一个特别普遍的现象:很多人学Pandas是从某个小例子开始的,会读文件、会groupby、会画个折线图,觉得自己已经上手了。结果真拿到一份业务数据,当场就懵了——日期列有的是Excel序列号、有的是文本、还有的直接是空的;金额字段里混着“12,345万”这种带单位的字符串;分组后发现同一个公司重复出现三次但收入还不一样。这时候才发现,所谓的“数据分析”,80%的时间不是在建模画图,而是在跟脏数据搏斗。
这篇文章我就按一条完整的实战链路来写:从拿到数据开始,怎么读取、怎么快速掌握全貌、怎么做清洗和加工,最后怎么用图表把结论讲清楚。整条链路都围绕一个具体场景展开——整理一份财务数据,算指标,画图看趋势。过程中我会把Pandas里那些文档没写透的细节、踩过的坑和排查思路一起交代清楚。如果你是刚入门想做点真事的人,这篇文章能帮你把零散的API串成一条能跑通的流水线。
1. 拿到数据先别急着画图:整条分析链路的思路拆解
1.1 为什么说数据清洗才是重头戏
先纠正一个很多教程会误导人的地方:网上那些漂亮的图表、炫酷的大屏,看着很唬人,但真正决定分析质量的是数据分析的前半段——数据清洗和数据治理。行业里有一句老话叫“先采集再清洗”,采集只要保证拿到数据就算完成,而清洗才是把“数据”变成“能用数据”的关键。
举一个我真实踩过的例子。之前处理一份销售明细,需要按季度对比营收。第一版脚本跑出来,一季度营收竟然比二季度还高,明显不对劲。排查到最后发现,日期列里混着三种格式:一部分是真正的日期类型,一部分是字符串“2024/1/5”,还有一小部分居然是用Excel序列号存的数字。直接按字符串排序,1月、2月、10月、11月这种“字符串序”就会排在错误的位置。这就是典型的清洗不到位导致的结论错误。
所以你在规划一个分析项目时,心里要有数:读数据、探查、清洗、加工、可视化是一条流水线,清洗和加工往往占掉一半以上的工作量。这不是浪费时间,而是给后续的图表和结论打地基。地基歪了,房子再好看也不敢住。
1.2 用一个财务数据案例串起整篇文章
为了让整篇内容不空谈,我选了一个贯穿全文的实操场景:整理一份公司的财务利润表数据,目标是计算几个核心指标(营收、净利润、净利润率),然后按季度画出营收和净利润的走势图。
这个场景在真实工作中很常见。不管你是看上市公司财报,还是分析自家公司的经营情况,利润表都是绕不开的数据源。而且这类数据有个特点:字段结构相对固定,但数据质量参差不齐——有缺失、有异常、有空值,很适合用来演示清洗的各个步骤。
我做这件事的时候,先用requests从一个数据接口拉取了利润表数据,返回的是JSON,转成DataFrame后,结构大致如下:每一行是一家公司在某个报告期的一组指标,字段包括股票代码、报告期、营业收入、净利润、营业成本等,还有几个字符串类型的列。关于requests抓接口的部分不是本文重点,我们直接从“拿到了DataFrame之后”开始讲起。后面所有示例代码,你都可以在Jupyter Notebook或PyCharm里直接跑。
1.3 环境准备和装包避坑
开始实操之前,先说一个新手很容易卡住的环节:Pandas到底怎么装。搜索“pycharm怎么安装pandas包”的人特别多,其实根本不用在PyCharm的设置界面里折腾图形化安装,直接在项目终端里执行一行命令就行。
pip install pandas # 如果下载速度很慢,可以用清华镜像 pip install pandas -i https://pypi.tuna.tsinghua.edu.cn/simple很多人在这一步遇到“安装失败”,十有八九是Python版本太新,而本地的pandas版本太旧。比如Python 3.13刚出来的时候,很多第三方库的编译版还没跟上,直接用pip装就会报红。这时候最简单的办法是先升级pip:
python -m pip install --upgrade pip然后重新执行安装命令。如果你做的是数据分析,我更推荐直接装Anaconda或Miniconda,用conda创建环境,conda会自动帮你匹配对应Python版本下能够正常工作的pandas版本,省掉一堆编译报错。
跑下面的示例代码,你还需要matplotlib,同样用pip安装即可。装好之后,在代码开头把常用的导入写了,后面所有示例都基于这些导入:
import pandas as pd import numpy as np import matplotlib.pyplot as plt2. 数据读取与初步探查:别急着清洗,先看清数据长什么样
2.1 read_excel和read_csv:读取文件时最容易被忽视的三个参数
拿到一份数据,第一步永远是读取。Excel和CSV是最常见的两种格式。这里先说Excel,因为很多业务数据都躺在Excel里。
# 读取Excel文件,注意sheet_name和dtype df = pd.read_excel("财务数据.xlsx", sheet_name="利润表", dtype={"股票代码": str})sheet_name参数用来指定读取哪个工作表。不传的话默认读第一个sheet,如果你不确定文件里有几个sheet,可以传sheet_name=None,返回的是一个字典,键是sheet名,值是DataFrame:
sheets = pd.read_excel("财务数据.xlsx", sheet_name=None) print(sheets.keys())dtype参数则用来强制指定某些列的读取类型。为什么一定要处理这个?因为股票代码、身份证号这类字段,在Excel里经常被当成数字处理,读进来之后会变成int类型,前导零全部丢失——比如“000001”读进来就变成了1。还有那种很长的ID,如果不指定dtype,会直接变成科学计数法显示,精度还丢了,联表的时候怎么都对不上。
读取CSV文件的时候,最经典的问题是编码。Windows上用Excel导出的CSV,默认编码是gbk,直接用pd.read_csv读会报UnicodeDecodeError。解决办法很简单:
df = pd.read_csv("数据.csv", encoding="utf-8-sig")utf-8-sig能自动去掉UTF-8 BOM头,还能兼容多数中文内容。如果你的数据是GBK,就换成encoding="gbk"。还有一列是日期,建议读取时直接用parse_dates参数转好:
df = pd.read_csv("数据.csv", parse_dates=["报告期"])这样报告期这一列读进来的时候就已经是datetime64[ns]类型了,后面处理时间特征的效率会高很多。
2.2 探查四件套:head、tail、info、describe
读取完成后,先别急着清洗。你要先做“探查性分析”,搞清楚这堆数据大概是什么情况。我每次拿到一个新DataFrame,就固定用四个函数打底。
# 1. 看前5行,了解字段和值的大致长相 df.head() # 2. 看后5行,避免文件尾部有汇总行或奇怪内容 df.tail() # 3. 看整体结构:行数、列数、每列的非空数量、类型 df.info() # 4. 看数值型列的统计量:均值、标准差、分位数 df.describe()df.info()的输出里最关键的是Non-Null Count列。拿一份数据,我第一眼就是看这里:哪列的非空数量比总行数少,就说明哪列有缺失值。比如总行数1000行,净利润列Non-Null Count是980,那就说明有20个缺失值。
df.describe()输出的是数值列的统计量,重点关注min和max。如果营收的最小值是负数,那大概率是异常值,因为营业收入不太可能是负的。注意,describe()默认只统计数值型列,如果你发现某个明明是数值的列没出现在结果里,那就要回头看看这一列是不是被读成了object类型。
还有一个我习惯性用的组合:
print(df.shape) # 看行数和列数 print(df.columns.tolist()) # 把列名平铺打出来,方便复制2.3 dtype陷阱:object不一定是字符串
这是Pandas里最容易踩的坑之一。df.dtypes会显示每列的类型,很多人看到object就认为这一列是字符串,真假不一定。object是Pandas的统一兜底类型,它可能存的是字符串,也可能是混杂类型——同一列里既有数字又有文本。
举个典型例子:
df["报告期"] = pd.to_datetime(df["报告期"], errors="coerce")errors="coerce"的意思是,遇到转不了的格式,不报错,而是变成NaT(时间类型的缺失值)。这是一个非常实用的参数,后面处理缺失值的时候还会用到。转换完之后,你再打印一下df["报告期"].dtypes,确认它已经变成了datetime64[ns]。
另外,很多人在处理数值列的时候会遇到“明明看起来是数字,但mean()算了半天报错”的情况。那是因为这一列里混入了一个“-”或空字符串,导致整列被读成object。解决办法就是先pd.to_numeric批量转:
df["营业收入"] = pd.to_numeric(df["营业收入"], errors="coerce")转完原来那个“-”就变成NaN了,这一列也就顺理成章变成了float类型。
3. 数据清洗实战:缺失值、重复值、异常值和文本标准化
3.1 缺失值处理:先判断缺失原因,再决定填充策略
数据清洗的顺序我建议固定下来:先处理缺失值,再处理重复值,最后处理异常值。缺失值处理的核心不是“怎么填”,而是“为什么缺失”。同样是缺失,原因不同,处理方式完全不同。
先统计缺失情况:
df.isnull().sum()Pandas的isnull()是按单元格判断是否为缺失值,sum()按列加总。这一步能快速告诉你每一列有多少缺失。
缺失值处理的常用手段有这几种:
| 场景 | 处理方法 | 代码示例 |
|---|---|---|
| 缺失行占比很小,且样本足够 | 直接删除 | df.dropna(subset=["净利润"]) |
| 数值列少量缺失,不希望减少样本 | 用中位数或均值填充 | df["净利润"].fillna(df["净利润"].median()) |
| 时间序列中某个点缺失,用前后值衔接 | 前向填充或后向填充 | df["营收"].fillna(method="ffill") |
| 类别列缺失 | 填一个“未知”占位 | df["行业"].fillna("未知") |
用中位数填充而不是均值,是因为均值容易受异常值影响。比如一个部门10个人,9个人月薪1万,老板月薪100万,均值会被拉高到10万以上,但中位数还是1万,更能代表一般水平。
我自己的习惯是给缺失值处理加一个“审计”步骤:处理之前打印一条缺失统计,处理之后再打印一条,确保处理逻辑没有把不该删的行删掉。
print("处理前总行数:", len(df)) df = df.dropna(subset=["股票代码", "报告期"]) print("处理前总行数:", len(df))这一步虽然简单,但非常有用,后面我会再提一次。任何时候你不知道你的清洗操作有没有弄丢数据,就打印一下shape。
3.2 重复值处理:别无脑drop_duplicates
重复值处理看起来简单,但有一个关键问题:哪几列重复才算真正重复?
# 默认所有列都一样才算重复 df.duplicated().sum() # 按关键列判断重复,比如同一家公司同一个报告期 df.duplicated(subset=["股票代码", "报告期"]).sum()df.duplicated()返回的是一个布尔序列,True代表重复,False代表非重复。如果你只想看重复的那部分长什么样,可以这样:
df[df.duplicated(subset=["股票代码", "报告期"], keep=False)].sort_values("股票代码")加了keep=False之后,所有重复的行都会被标为True,而不只是后面的行。
删除重复行的时候,keep参数决定了保留哪个:
# 保留第一次出现的行,删除后续重复行 df = df.drop_duplicates(subset=["股票代码", "报告期"], keep="first")这里有一个重要的踩坑提醒:duplicated()和drop_duplicates()默认keep="first",也就是说保留第一行,删掉后面的。但在某些业务场景下,最后一行才是最新的数据(比如每天更新一次,同一只股票出现多次,最后一行是最新状态)。这时候你需要keep="last"。
还有一种情况,数据集里存在合法的重复——例如不同报表类型(合并报表和母公司报表)字段相同但口径不同。这种重复如果直接按全部列去重,可能把有意义的行给删了。所以一定要想清楚“重复”的业务定义是什么。
3.3 异常值识别:用分位数和标准差双重校验
异常值不是必须删除的,但你必须能识别出来,然后根据业务判断怎么处理。财务数据里最常见的异常值是:营收为负、净利润比营收还大、毛利率超过100%或小于0。
我常用的异常值识别方法有两种:标准差法和四分位距法。
标准差法适合数据近似正态分布的情况:如果某个值与均值的偏差超过3个标准差,就认为是异常值。
mean = df["净利润率"].mean() std = df["净利润率"].std() df["净利润率"].apply(lambda x: abs(x - mean) > 3 * std)四分位距法(IQR)不要求数据是正态分布,更稳健一些。它的思路是计算上四分位数Q3和下四分位数Q1,超出[Q1 - 1.5×IQR, Q3 + 1.5×IQR]区间的值视为异常值。
Q1 = df["净利润率"].quantile(0.25) Q3 = df["净利润率"].quantile(0.75) IQR = Q3 - Q1 lower = Q1 - 1.5 * IQR upper = Q3 + 1.5 * IQR df[(df["净利润率"] < lower) | (df["净利润率"] > upper)]异常值识别出来之后,处理方式要看业务背景:如果是录入错误,可以改成缺失值或删除;如果是真实存在但异常的值(比如某一年公司大亏损,净利润率为-80%),那就不能删,删了反而失真。
这里我强烈建议:异常值不要直接覆盖原数据,可以先加一列标记,比如df["异常标记"] = True,保留追溯的线索。数据清洗也要可审计,这个习惯能帮你省掉很多将来“这数字哪来的”的麻烦。
3.4 文本字段标准化:从“12,345万”到干净数字
文本清洗是很多人会忽略的部分。财务数据里常见的情况是:金额字段带着单位,比如“12,345.67万”“1.2亿”;或者数字里混了逗号千分位符号。这种字段不能直接用pd.to_numeric转换,得先做标准化。
Pandas的str访问器是做文本批量操作的神器。所谓“访问器”,就是字符串列后面可以跟.str,然后就有一堆字符串方法可以用:
# 去掉字符串两侧空格 df["行业"] = df["行业"].str.strip() # 去掉千分位逗号 df["营收"] = df["营收"].str.replace(",", "", regex=False) # 把“万”替换成“*10000”前的准备工作 df["营收"] = df["营收"].str.replace("万", "e4", regex=False) df["营收"] = pd.to_numeric(df["营收"], errors="coerce")这里我用了科学计数法的技巧:把“万”替换成“e4”,然后pd.to_numeric会直接把它解析成数字。比如“12,345万”经过处理后变成“12.345e4”,解析出来的数值是123450。这个技巧在处理“万”“亿”混杂的财务数据时非常实用。
如果金额里既有“万”又有“亿”,那就一次性处理:
def convert_amount(val): if isinstance(val, str): val = val.replace(",", "") if "亿" in val: return float(val.replace("亿", "")) * 1e8 elif "万" in val: return float(val.replace("万", "")) * 1e4 else: return float(val) return val df["营收"] = df["营收"].apply(convert_amount)4. 数据加工与特征构造:从会用函数到会讲故事
4.1 groupby分组聚合:用agg一次算出多个口径
数据清洗干净之后,下一步就是加工。groupby是Pandas里最核心的聚合操作,它的作用是把明细数据按照某个维度汇总成统计口径。比如我现在想知道每个行业、每个季度分别的营收总和、净利润均值、公司数量。
df_grouped = ( df.groupby(["行业", "季度"]) .agg( 营收合计=("营业收入", "sum"), 净利润均值=("净利润", "mean"), 公司数量=("股票代码", "count"), ) .reset_index() )这里有一个容易踩的坑:groupby之后,分组字段会被当作索引,而不是普通列。如果你不想要这种行为,有两种处理方式:一是在groupby的时候加as_index=False,二是后面跟一个reset_index()。两种效果一样,选一种用就行。
agg函数非常灵活,它可以接收一个字典,键是新列名,值是一个元组,元组里第一个元素是原始列名,第二个元素是聚合函数。这种写法比df.groupby(...).sum()灵活得多,可以一次对不同的列做不同的统计。
聚合函数除了sum、mean、count,还常用median(中位数)、std(标准差)、min/max。有一个细节:count()不会统计NaN值,而size()会统计。如果你要统计有效业务记录数,用count是合理的;如果你要统计“这一组里总共有几行数据”,得用size()。
4.2 merge多表合并:联表时最容易出问题的环节
业务分析经常要合并多张表。比如利润表数据和公司基本信息表,用“股票代码”关联。Pandas的pd.merge是这个场景的核心函数。
df_merged = pd.merge( df_profit, # 利润表 df_company, # 公司信息表 on="股票代码", # 关联键 how="left", # 左连接:保留左表全部行 suffixes=("_利润表", "_公司") # 重名列的后缀 )连接类型how有几种:inner只保留两张表都匹配到的行;left保留左表全部行;right保留右表全部行;outer保留所有行。日常分析中inner和left用得多,选哪种取决于你要保留哪边的样本口径。
用merge最容易出问题的是关联键的类型不一致。比如左表的股票代码是字符串“000001”,右表的股票代码是整数1,匹配的时候全部落空,合并完之后发现数据行数少了一大截。排查方法很简单:分别打印两边的dtype。
print(df_profit["股票代码"].dtype) print(df_company["股票代码"].dtype)第二个容易出问题的是关联键有重复值。如果右表中一个股票代码出现多次,而左表对应一行,合并后会生成多行,行数暴涨。这有时候是业务需要的,但大多数时候是数据问题。为了避免踩坑,可以在merge时加validate参数:
df_merged = pd.merge( df_profit, df_company, on="股票代码", how="left", validate="many_to_one", # 如果左表多行对右表一行,校验通过;右表有多行会报错 )validate支持one_to_one、one_to_many、many_to_one、many_to_many,它不改变结果,只会在不符合预期时抛异常。这个参数对排查脏数据特别有用,建议merge时都加上。
另一个隐藏在merge里的小坑是“列名冲突”。如果两张表里都有“营业收入”这个字段,merge后会自动加上后缀,变成“营业收入_利润表”和“营业收入_公司”。如果不指定suffixes,默认是_x和_y,读起来语义不够清晰。
4.3 别只会apply:向量化操作和性能对比
Pandas里最“误导人”的写法是:处理数据时第一反应写个apply+lambda。它确实方便,但性能很差,数据量一大就跑得很慢。其实大部分场景是可以用向量化操作解决的。
所谓向量化,就是直接对整列做运算,Pandas底层会调用NumPy的C语言实现,比Python循环快几十倍。
举个例子,我要计算净利润率:
# 低效的写法 df["净利润率"] = df.apply( lambda x: x["净利润"] / x["营业收入"] if x["营业收入"] else None, axis=1 ) # 高效的写法 df["净利润率"] = df["净利润"] / df["营业收入"]第二种写法完全不用循环,Pandas会自动对每一行做除法。除数为0或缺失时,结果会是inf或NaN,可以用replace处理:
df["净利润率"].replace([np.inf, -np.inf], np.nan, inplace=True)什么时候才真的需要apply?当这一行的计算没法用向量化表达的时候,比如要调一个自定义函数,且函数的输入是多列。这时候apply是对的。但它不应该成为首选。
还有一个常见的向量化需求是条件判断。np.where比apply+lambda快得多:
df["规模等级"] = np.where(df["营业收入"] > 100e8, "大型", "中小型")4.4 时间特征提取和季度趋势分析
财务分析里时间维度是绕不开的。Pandas对时间类型的处理很强大,前提是你的列真的是datetime类型。之前已经讲过用pd.to_datetime转换,转换成功后就可以提取各种时间特征:
df["报告期"] = pd.to_datetime(df["报告期"]) df["年份"] = df["报告期"].dt.year df["月份"] = df["报告期"].dt.month df["季度"] = df["报告期"].dt.quarterdt是时间列专属的访问器,类似于字符串列的.str。提取季度在财务分析里非常常见,因为很多指标要按季度口径看。
计算环比(和上个季度比)和同比(和去年同期比)也是财务分析的高频需求。用pct_change()可以算环比:
df_sorted = df.sort_values("报告期") df_sorted["营收环比"] = df_sorted.groupby("公司名称")["营业收入"].pct_change()注意顺序:要先按时间排序,再分组算环比,否则算出来的是乱序的比值。这里的groupby是为了防止把不同公司的数据混在一起算环比——每家公司的上一个季度,应该对应自己公司的上个季度。
如果要把数据从季度明细汇总成半年报或年报,可以用resample:
df_yearly = ( df.set_index("报告期") .groupby("公司名称")["营业收入"] .resample("Y") .sum() )resample是按时间频率重新采样的功能,"Y"表示按年汇总,"Q"表示按季度汇总。它的逻辑是“先set_index为时间列,再重新采样聚合”,写起来稍微绕一点,但功能很强大。
5. 可视化:一条链路快速出图,再用Matplotlib精修细节
5.1 Pandas内置的plot:一行代码快速出图
很多人不知道,Pandas本身就封装了最简单的绘图功能,底层调用的是Matplotlib。不需要单独学某个可视化库,就可以快速看数据的形状。
# 按季度汇总营收后画折线图 df_quarter = ( df.groupby("季度")["营业收入"] .sum() .reset_index() ) df_quarter.plot(x="季度", y="营业收入", kind="line", figsize=(10, 5))如果你的运行环境是Jupyter Notebook,需要先执行一行魔法命令:
%matplotlib inline这行命令的作用是让图表直接显示在Notebook的输出区域,而不是弹出新窗口。如果你在PyCharm里运行,图表会默认弹出单独窗口。
kind参数控制图表类型:line折线图、bar柱状图、barh水平柱状图、hist直方图、box箱线图、scatter散点图、pie饼图。对应到Excel里最常用的十种图表,Pandas的plot都能覆盖大部分基础场景。
5.2 图表选型:什么样的数据配什么样的图
很多新手拿到数据就想画图,但画出来之后别人根本看不懂他在讲什么。图表选型的本质是:你要传达什么信息,就用什么图形。
| 你要表达什么 | 推荐图表 | 对应Pandas代码 |
|---|---|---|
| 时间维度的变化趋势 | 折线图 | df.plot(x="报告期", y="营收", kind="line") |
| 类别之间的对比 | 柱状图 | df.plot(x="行业", y="营收", kind="bar") |
| 两个数值变量的关系 | 散点图 | df.plot(x="营收", y="净利润", kind="scatter") |
| 数据分布和异常值 | 箱线图 | df.plot(y="净利润率", kind="box") |
| 各类别占比 | 饼图 | df.plot(y="营收", kind="pie", subplots=True) |
举一个具体例子:同样是看“哪些行业赚钱”,柱状图比折线图合适,因为行业是离散类别,没有连续的时间关系;同样是看“营收和净利润有没有关系”,散点图比柱状图合适,因为要观察的是两个连续变量的相关性。选错图表,信息的传达效率会低很多。
我在实际做数据可视化时,还会刻意控制图表的数量:一个结论配一个图,一页放不下重点就等于没有重点。宁可少画几张,每张都讲清一件事。
5.3 中文字体、坐标轴和图例的精修
Pandas的plot函数画出来的图,默认是Matplotlib风格的,标题、轴标签、图例都需要手动补。这里有一个绕不开的坑:中文乱码。
Matplotlib默认字体不支持中文,直接画图,中文标签会显示成一个个小方框。解决办法是在画图之前设置中文字体:
plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei", "PingFang SC"] plt.rcParams["axes.unicode_minus"] = False # 解决负号显示成方块的问题第一行设置中文字体,第二行设置负号的显示。不同操作系统支持的字体不一样:Windows上有SimHei(黑体)和Microsoft YaHei(微软雅黑),macOS上有PingFang SC。如果你用的环境都没有这些字体,可以试试plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC"]。
画完基础图之后,建议不要停在那里,把标题、轴标签、图例补全。方法很简单,plot()函数会返回一个Axes对象,你在它上面继续设置:
ax = df_quarter.plot(x="季度", y="营业收入", kind="line", figsize=(10, 5)) ax.set_title("2024年各季度营业收入走势") ax.set_xlabel("季度") ax.set_ylabel("营业收入(元)") ax.grid(True, linestyle="--", alpha=0.6)细心的读者会发现,df.plot()直接把x=,y=,kind=,figsize=这些都消化掉了,剩下的标题、坐标轴标签通过ax再设置。这一层关系搞清楚了,你就能在“Pandas快速出图”和“Matplotlib精细控制”之间自由切换。
图例的默认位置有时会遮挡数据区域,可以用ax.legend(loc="best")自动选择位置。如果图例的标签不理想,你可以在plot()里用label参数提前指定:
df_quarter.plot(x="季度", y="营业收入", kind="line", label="营业收入", figsize=(10, 5))5.4 从静态图表到分析大屏:轻量化落地思路
现在很多公司喜欢做可视化大屏,把关键指标实时展示在一张大屏上。很多人一听到大屏就想到用专门的工具或前端框架,实际上数据分析和图表的基础工作,用Pandas+Matplotlib完全可以搞定,两者并不冲突。
Pandas这边负责的,是把数据清洗、加工、聚合做好,然后生成一组高质量的图表。大屏工具那边负责的,是把这些图表按照视觉方案排布到一块屏幕上。也就是说,你在Pandas阶段画的每一张图,都是给后续可视化方案提供“素材”和“结论”。
即便不做大屏,我建议每个分析项目结束时都沉淀两份东西:一份是汇总表,把关键指标用to_excel导出:
df_result.to_excel("分析结果.xlsx", index=False, sheet_name="汇总")另一份是核心图表,用fig.savefig保存成图片:
fig = df_quarter.plot(x="季度", y="营业收入", kind="line", figsize=(10, 5)).get_figure() fig.savefig("营收走势图.png", dpi=200, bbox_inches="tight")bbox_inches="tight"的作用是裁剪掉图表周围多余的空白,保存的图片更紧凑规范。这两个输出物,后续无论是做汇报PPT,还是给大屏做素材,都能直接用。
6. 常见问题与排查技巧实录
6.1 SettingWithCopyWarning:Pandas最常见的警告,到底怎么解决
跑Pandas代码时,经常会看到一行警告:SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame。这个警告不是报错,代码还能继续跑,但它是一个非常危险的信号——你的赋值操作可能没有写进原DataFrame,而是写进了一个临时副本。
典型的触发场景是这样:
df_sub = df[df["行业"] == "制造业"] df_sub["新指标"] = df_sub["营业收入"] * 0.1第二行代码会触发警告。原因是df_sub是通过布尔索引切片出来的,它可能是原DataFrame的一个视图(view),也可能是副本(copy)。Pandas无法确定,所以它会警告你:“你这个操作可能不会生效。”
正确的做法有两种。第一种,使用.loc的方式在原始DataFrame上操作:
df.loc[df["行业"] == "制造业", "新指标"] = df["营业收入"] * 0.1第二种,如果你确实只想操作子集,那就显式复制一份:
df_sub = df[df["行业"] == "制造业"].copy() df_sub["新指标"] = df_sub["营业收入"] * 0.1.copy()的作用是明确告诉Pandas:这是一份独立的副本,后续怎么改都不影响原数据。加了.copy()之后,警告通常会消失,行为也变得可预期。
6.2 inplace=True:少用,但必须知道它为什么有争议
Pandas的很多方法都有inplace参数,比如dropna(inplace=True)、fillna(inplace=True)。它的含义是“就地修改原DataFrame”,不用重新赋值。看起来挺方便,但我个人建议尽量少用。
第一个原因是可读性。Pandas官方文档和社区都更推荐“链式赋值”风格:df = df.dropna()。这种写法逻辑清晰,每一行都明确表示“我要把处理结果赋值给df”,别人看代码时很容易跟上思路。inplace=True则需要追查每一行是否修改了原数据,排查问题的时候很费劲。
第二个原因是有些情况下inplace=True并不生效。比如对df.groupby()的结果、对某些链式表达式的中间结果使用inplace=True,可能什么都没改。这就很容易造成隐性问题:你以为数据已经清洗过了,实际上原数据没有任何变化。
所以我的建议是:日常代码统一用df = df.dropna()这种显式赋值的写法,inplace=True只在两种情况下使用——一是在处理超大DataFrame、担心复制内存开销的时候,二是你明确知道这个操作会把原数据替换成新数据且不影响别的对象时。
6.3 大文件读不动:chunksize和dtype优化内存
读几个G的CSV文件卡死甚至内存溢出,是很多数据分析新手遇到的第一道坎。处理这种大文件,有两个思路。
第一个思路是用nrows先抽样看一眼:
df_sample = pd.read_csv("大文件.csv", nrows=10000)先用一万行观察列结构和数据长相,确认编码、列名、类型都没问题,再决定要不要全量读取。
第二个思路是分块读取。pd.read_csv支持chunksize参数,读取时会把文件分成一块一块的迭代对象,每次只读一块进内存:
chunk_list = [] for chunk in pd.read_csv("大文件.csv", chunksize=100000): # 对每一块做清洗 chunk = chunk.dropna(subset=["股票代码"]) chunk_list.append(chunk) df = pd.concat(chunk_list)这种方式适合“清洗逻辑可以逐块独立完成”的场景。如果你需要全表排序或者跨行计算,分块就不好搞了,这时候建议直接用数据库(比如SQLite)或Spark来处理,而不是硬扛Pandas。
还有一个减少内存的实用技巧:把字符串列转成category类型,对低基数列内存占用可以降一半以上:
df["行业"] = df["行业"].astype("category")6.4 排查数据异常的科学顺序:从类型到分布层层推进
最后分享一个实战排查方法。每次遇到分析结果不对劲,我都有固定的排查顺序。
第一步,查列类型df.dtypes,排除类型混乱的问题。第二步,查缺失df.isnull().sum(),看是不是有该死的数据少了。第三步,查唯一值分布df["营收"].value_counts(),看看有没有明显重复或异常值。第四步,查统计量df["营收"].describe(),看min、max是否合理。第五步,才是按业务维度分组,定位是哪一层出了问题。
这五步走下来,90%的数据问题都能定位。大部分“分析结果很奇怪”的案例,最后查出来的原因都不是算法问题,而是数据问题——要么类型错了,要么缺失值没处理,要么重复值没去干净。
结尾
写了这么多,最后想分享两个我个人的小习惯。第一个习惯是:每做一步清洗,都打印一次df.shape。清洗前多少行、去重后多少行、缺失值处理后多少行,每一步的数据量变化都要心里有数。这个习惯帮我避免过无数次“数据怎么少了”的疑案。
第二个习惯是:把常用的清洗逻辑封装成函数,形成自己的分析流水线。比如把“读Excel→规范列名→去重→填充缺失值→转时间格式→导出清洗结果”这些步骤写成一个clean_financial_data(file_path)函数,下次换一批数据,直接调用即可。这样做的好处不仅仅是省事,更重要的是——清洗逻辑是固定的、可复现的、可审计的,这比在Notebook里手动敲一遍要靠谱得多。
数据清洗和可视化这条路,本质上是在跟“混乱”打交道。Pandas只是一套工具,真正决定你分析质量的,是你面对脏数据时的思路和耐心。希望这篇实战笔记能帮你少踩几个坑,也欢迎在评论区交流你遇到过的奇怪数据问题。