拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Pandas数据分析实战:从数据清洗到分组聚合的完整指南

做数据分析这几年Pandas是我电脑里打开频率最高的库没有之一。凡是从Excel、数据库、接口里扒拉出来的结构化数据到了它手里基本都能干干净净地整理好再喂给后续的可视化、机器学习流程。网上关于Pandas的教程不少但多半是零散的API翻译或者只讲某个孤立技巧很多新手对着DataFrame一脸懵这玩意到底怎么建、怎么改、怎么跟真实业务对上这篇就把我从数据整理、数据清洗到进阶聚合的完整使用路径梳理一遍每一步都配上能直接跑的示例代码希望能帮你把这块硬骨头啃下来。1. 为什么数据科学场景里绕不开Pandas我最早接触数据处理时用的是纯Python写循环等数据量一旦到几万行那种龟速会让你怀疑人生。换成Pandas之后同样的筛选、分组、求和几乎就是一行代码的事背后的向量化计算把性能瓶颈直接抹平了。这也是Pandas在Python数据分析生态里地位这么稳固的根本原因它把Excel的操作逻辑搬到了代码里又比SQL更灵活尤其在探索性分析和数据预处理阶段几乎没有哪个库能像它这样顺手。Pandas的两个核心数据结构是Series和DataFrame。Series可以理解成一列带索引的数据像一张表的某一行或某一列DataFrame则是整个二维表有行有列每个列可以是不同的类型。很多初学者一开始纠结要不要先学Series再学DataFrame我的建议是直接上手DataFrame等遇到取某列、取某行的场景自然就搞清楚Series是什么了。你可以在Jupyter Notebook里随手敲几行创建练习理解一下传字典、传列表、传二维数组这些常见构造方式底子打牢之后后面的筛选和变换才能跟得上。安装这一步拦住了不少新人。不同环境下的安装方式稍有差别比如在PyCharm里可以直接去Settings找Project Interpreter点加号搜索pandas安装也可以在终端用pip install pandas或者conda install pandas。Jupyter里则更简单一般只需在代码单元格写!pip install pandas运行一次就能装好。安装慢或者超时的话记得换成国内镜像源比如清华的镜像速度会明显提升。装完之后一定要验证一下导入是否正常输入import pandas as pd不报错就说明环境已经就绪了。1.1 环境准备与安装排查很多报错其实不是代码的问题而是Pandas根本没装进当前使用的Python解释器里。比如在PyCharm里新建了项目但没用虚拟环境或者Jupyter的kernel对应的Python版本和pip安装时用的是同一个解释器这些不一致会导致import pandas时报ModuleNotFoundError。我的习惯是装完环境后马上运行print(pd.version)把版本号打出来看一眼至少能确定Pandas确实在这个解释器里可用。另外有个很容易被忽略的点Pandas对Python版本有要求新版本Pandas通常需要Python 3.9及以上。如果你的Python太老pip会尝试去找旧版Pandas却可能因为编译问题失败。这时候我建议要么升级Python版本要么安装与Python版本匹配的Pandas历史版本不要卡在报错信息里死磕。还有Windows环境下偶尔会出现缺少VC运行时依赖的诡异问题直接去官网装对应的Microsoft Visual C Redistributable就能解决这类问题通常和代码逻辑无关。1.2 Series和DataFrame的创建练习Pandas里创建Series最常用的方式是传一个字典键会成为索引值成为数据传列表则自动生成从0开始的整数索引。DataFrame的创建方式更灵活字典、列表、NumPy二维数组、甚至另一个DataFrame都能变成DataFrame。下面这个简单练习我经常推荐给入门的人它能一次性搞明白索引和数据的关系import pandas as pd import numpy as np s pd.Series({语文: 90, 数学: 92, 英语: 85}) print(s) df pd.DataFrame({ 姓名: [张伟, 李娜, 王芳], 城市: [北京, 上海, 广州], 年龄: [25, 30, 28] }) print(df)创建之后可以试试df.shape、df.columns、df.index、df.info()这些查看属性慢慢就会建立对数据结构的直观感觉。很多网上的练习题也会围绕Series和DataFrame的创建展开比如头歌Educoder平台的数据预处理Pandas练习基本上都是从完成这两个结构的花式创建开始的。2. 数据读取与写出实际业务里的数据几乎不会手动敲进去都是从CSV、Excel、JSON、数据库或接口里读进来的。Pandas在这块做得非常成熟read_csv、read_excel、read_json接口设计得高度统一参数也很丰富只要掌握了几个常用参数基本能应对绝大多数日常导入场景。读取CSV时我最低限度会设置的有三个参数文件路径、编码方式、分隔符。最常见的中文乱码问题大部分通过encodingutf-8或enginepython就能解决某些特殊格式的分隔符并不是逗号而是制表符或分号这时候直接指定sep参数就行。Excel文件的读取则要看你读到的是xlsx还是xls格式xlsx依赖openpyxlxls依赖xlrd两者版本不一致时容易出现import错误必要时需要pip install openpyxl或在环境中同时装好这两个库。2.1 结构化数据读取的详细参数先看一个比较完整的CSV读取示例我把常见参数都标上注释import pandas as pd df pd.read_csv( sales_data.csv, sep,, encodingutf-8, parse_dates[下单时间], dtype{订单号: str, 金额: float}, usecols[订单号, 客户名, 下单时间, 金额] )parse_dates能让指定列在读取时直接转成时间类型比读进来之后再做to_datetime省一步。dtype参数用于指定列的数据类型比如订单号为纯数字但实际是字符串性质的编号如果不在读取阶段指定str后面就会出现类似101和0101这种前导零丢失的问题。usecols则能只保留需要的列对大文件来说既省内存又减少后面无意义的列操作。读取Excel也是类似思路区别在于工作表的指定。如果你不指定sheet_name会默认读到第一个工作表如果工作簿里有多个sheet可以传sheet_name销售明细或者sheet_name0来指定。读取多张表还可以用sheet_nameNone返回的是一个以工作表名为键、以DataFrame为值的字典。2.2 数据写出与文件管理数据处理完总要落盘保存。to_csv和to_excel使用逻辑跟读取是对称的写CSV时indexFalse几乎是必须的否则数据会多出一列行号后面再读进来就会多一列Unnamed: 0的冗余列。写Excel时多个DataFrame要写到同一个工作簿里可以用pd.ExcelWriter配合to_excel的sheet_name参数这个需求在做日报、周报时特别常见。df.to_csv(clean_sales.csv, indexFalse, encodingutf-8-sig)这里编码我特意用utf-8-sig而不是utf-8是因为utf-8-sig输出的文件用Excel打开时不会出现中文乱码。这个细节不干这行的人很难注意到但做数据交付时是关键。写多个sheet的示例也一并给出with pd.ExcelWriter(report.xlsx, engineopenpyxl) as writer: df1.to_excel(writer, sheet_name销售汇总, indexFalse) df2.to_excel(writer, sheet_name订单明细, indexFalse) df3.to_excel(writer, sheet_name退款记录, indexFalse)3. 数据清洗实战说句实在话真实项目里数据清洗占据的时间常常比建模本身还要多。脏数据不外乎几个特征缺失值、重复值、类型不对、异常值。Pandas给每种脏数据都提供了对应的处理手段关键是你要知道在什么场景下选哪种方案而不是所有缺失值都无脑fillna(0)。数据预处理在像头歌这样的练习平台上是单独一个模块但在真实工作中它从来不是孤立的步骤而是决定后续分析结果可靠性的地基。清洗得好不好最直接的后果就是统计口径对不对所以这块我非常建议多花时间练习不要急于往建模阶段冲。3.1 缺失值处理拿到一个DataFrame后的第一件事一般是检查缺失值分布。isnull()配合sum()可以快速统计每列的缺失数量info()也会在输出里显示每列的非空计数二者搭配着用对数据的健康程度心里就有数了。删除缺失值用的是dropna()这里的参数有点讲究dropna()默认删除任何含有缺失值的行如果你只想删除那些全部列都为缺失值的行要用howall如果只想看某一列或某几列是否有缺失可以用subset指定列名列表。但删除不是万能的数据本身很贵时填充往往更合适df[金额].fillna(df[金额].median(), inplaceTrue) df[备注].fillna(无, inplaceTrue) df[时间].ffill(inplaceTrue)能注意到的规律是数值型列适合用均值或中位数填充类别型列适合用众数或固定值填充时间序列数据则常用ffill或bfill也就是用上一个或下一个有效值来填充。再加上interpolate()方法可以按线性插值填充许多看似复杂的缺失场景其实用这几招就能覆盖。3.2 数据类型转换与异常值处理数据类型转换是另一个高频操作。astype()是通用转型方法但要小心把浮点列直接转int时如果是空值会报错把“20230101”这种字符串转成时间需要pd.to_datetime把object类型且内容是数字的列转成数值型需要pd.to_numeric并且可以配合errorscoerce把非法值变成NaN。这三个方法基本能处理日常90%的类型转换需求。df[年龄] df[年龄].astype(int) df[下单时间] pd.to_datetime(df[下单时间]) df[销售额] pd.to_numeric(df[销售额], errorscoerce)异常值处理往往依赖业务规则比如销售金额不能为负、年龄不能在0到150之外筛选出来之后可以用布尔索引结合clip()或loc直接修正也可以做截断。更统计学的做法是看四分位数和IQR把超出上下限的值标记出来再处理。处理重复值时duplicated()和drop_duplicates()是一对好用的组合。前者返回布尔序列方便查看后者直接去重。注意去重时可以指定subset参数比如只根据手机号去重还可以用keep参数控制保留第一行还是最后一行。4. 筛选、分组与聚合筛选和分组是Pandas里使用频率最高的两块功能。筛选让人又爱又恨因为写法实在太多df[df[年龄] 30]、df.loc[条件, 列名]、df.query(年龄 30)每种都能做类似的事但在性能和可读性上各有取舍。我的一般原则是简单条件用布尔索引复杂条件或需要选列时用loc要求书写简便时用query。分组聚合是让Pandas真正显出威力的地方。groupby之后可以跟agg、transform、filter、apply等方法配合reset_index把分组键重新变成普通列整个数据整理链路就非常顺畅了。如果你做数据预处理时头歌上的练习都能顺手完成那么到这里你离实战已经不远了剩下的就是多拿真实数据练手。4.1 筛选与切片先看一个组合筛选的示例# 筛选年龄大于30且城市为上海的记录 condition (df[年龄] 30) (df[城市] 上海) sub_df df.loc[condition, [姓名, 城市, 年龄]]这里有两个新手容易踩的坑一是多条件必须用、|、~而不能用and、or、not二是条件表达式外层一定要加括号因为Python运算符优先级的问题不加括号会报错。筛选之后的子集尽量用副本避免后续修改影响原DataFrame。loc是基于标签的索引iloc是基于整数位置的索引两者都支持切片写法。df.loc[2:5]和df.iloc[2:5]在索引是连续整数时看起来差不多但一旦索引不是0到N-1的顺序这两个切片结果就完全不同所以掌握它们之间的差别是基本功。4.2 groupby分组聚合实战分组聚合的真实价值在于用一句话完成原本要写很多行for循环的数据汇总。groupby对象本身是懒计算的只有调用聚合方法时才会真正执行。最常用的几个聚合函数是sum()、mean()、count()、size()、max()、min()、nunique()。如果需要同时看多个统计量直接groupby后调用agg并传一个字典就能搞定grouped df.groupby(城市).agg({ 销售额: [sum, mean, count], 年龄: [min, max] })得到的结果会带多层列索引如果嫌麻烦可以把列名用列表传进去再看一眼结果结构。分组后要重新恢复成扁平表记得调用reset_index()。另外一个常见需求是按天、周、月做聚合这就要把时间列设成索引然后用resample()这比groupby更贴时间序列场景。多个DataFrame需要合并时concat是纵向或横向拼接的首选merge则适合像SQL一样按键关联。concat在处理相同结构的多张表时非常方便merge则解决一对一、一对多、多对多这类复杂关联。合并之后要留意索引重置避免出现重复或丢失索引。5. 进阶技巧与性能优化Pandas入门容易进阶难。很多人能把日常统计做完但一遇到大数据量或复杂变换就开始发怵。这一节我从实际踩坑经验里提炼几个话题apply与向量化的选择、时间序列的进阶处理、以及如何用Pandas对接接口数据甚至多线程采集。真实项目里性能优化往往不是从更复杂的语法技巧开始的而是先看你的代码是否在循环里反复处理DataFrame。如果你发现自己写了for循环在循环里一行行地改DataFrame那大概率性能会非常难看。我的经验是能用向量化运算就别用apply能用apply就别用循环。5.1 apply函数与向量化apply的定位是当你无法用原生向量化运算表达逻辑时用自定义函数逐行处理。比如计算一个订单的折扣后金额可以先定义一个计算逻辑的函数再通过apply应用到每一行def calc_discount(row): if row[金额] 1000: return row[金额] * 0.8 elif row[金额] 500: return row[金额] * 0.9 return row[金额] df[折后金额] df.apply(calc_discount, axis1)axis1表示逐行处理这是初学apply时最容易弄错的参数。但apply在数据量大时性能不算好它本质上是Python层面的循环。如果业务逻辑能改写成NumPy的内置函数或Pandas的向量化方法尽量优先用后者。比如上面的计算其实用np.where或Pandas布尔索引就可以更高效地实现。掌握apply能让你灵活处理复杂逻辑同时也要有意识地把简单的if-else逻辑拆成向量化表达式。5.2 时间序列处理时间序列在Pandas里是个大话题。把字符串列转成datetime之后就可以用dt访问器提取年月日、星期、季度等属性。按小时或按天重采样时resample非常方便df[时间] pd.to_datetime(df[时间]) df.set_index(时间, inplaceTrue) daily_sales df[金额].resample(D).sum() monthly_sales df[金额].resample(M).count()resample必须作用在时间索引上所以set_index那步不能省。重采样之后得到的结果时间索引可能不是连续的日期可以用reindex或asfreq补全缺失日期。再往上进阶移动窗口计算rolling()、时间差计算diff()和shift()也比较常用比如计算销售额环比、同比时就能派上用场。5.3 多线程与真实接口数据案例实际工作里经常需要从接口拉取数据再把返回的JSON或列表整理成DataFrame。这里有个从热搜词里看到的典型场景我给它补全成一个可运行的多线程采集示例很适合做练习。假设你有一批股票代码需要分别请求财务数据返回的数据每条大概是一个字典或嵌套结构可以用如下方式并发采集import pandas as pd import requests import random import time from concurrent.futures import ThreadPoolExecutor, as_completed def fetch_financial_data(stock_code): url fhttp://example.com/api/financial/{stock_code} try: resp requests.get(url, timeout10) resp.raise_for_status() data resp.json() return {stock_code: stock_code, report_type: data.get(report_type), net_profit: data.get(net_profit)} except Exception as e: return {stock_code: stock_code, error: str(e)} stock_codes [000001, 600519, 000002, 601318] results [] with ThreadPoolExecutor(max_workers4) as executor: future_map {executor.submit(fetch_financial_data, code): code for code in stock_codes} for future in as_completed(future_map): result future.result() results.append(result) print(已完成: , result[stock_code]) df_result pd.DataFrame(results)这个例子里几个要点值得多说几句requests访问接口时要设置超时避免某个请求卡住导致整个线程池等待返回结果统一先存成列表再一次性转DataFrame比在循环里反复pd.concat高效得多异常要单独捕获否则一个接口报错可能拖垮整个采集任务。6. 常见问题与排查技巧实录写代码多了你早晚会遇到一些诡谲的报错。Pandas的报错有时候很长但信息量很大关键是要耐下心看最上面和最下面的部分。这里记录几个我经常遇到、也被问过很多次的问题按出现频率排序每个都给出定位思路和解决方案。先讲最让人摸不着头脑的一个AttributeError: module pandas has no attribute core。这个报错网上一搜一大把原因五花八门最常见的是你的文件名或脚本名叫pandas.py导致import pandas时实际导入的是自己写的那个文件自然找不到core属性。解决方法很简单重命名你自己的脚本不要用pandas.py这种跟库重名的文件名。6.1 安装与导入阶段问题安装Pandas时最容易遇到的是镜像源慢、超时、权限不够这三种。慢和超时可以用-i参数指定国内镜像解决权限不够在Windows下使用管理员终端重装在Linux或macOS下可以考虑使用虚拟环境绕开系统的site-packages保护机制。还有pip版本太旧可能导致依赖解析异常先升级一下pip是个好习惯。PyCharm里安装后仍然无法import多半是解释器没有选对。打开Settings里的Project Interpreter确认一下当前项目的解释器路径如果刚才装Pandas用的pip属于系统Python但项目选择的是虚拟环境那就肯定找不到。把解释器切换到同一个环境问题自然消失。Jupyter里有时候import pandas会报错而命令行里却不报错这通常是ipykernel与当前Python环境不一致可以在Notebook里执行import sys; sys.executable看看当前用的是哪个Python再和命令行对比。6.2 数据处理阶段问题读取Excel报错一般是缺少依赖库。xlsx格式需要openpyxlxls格式需要xlrd老版本的Pandas可能还要求xlrd为主依赖所以如果你读xlsx文件报错直接pip install openpyxl就能解决。读取CSV中文乱码时把encoding从utf-8改成gbk或utf-8-sig逐个测试注意保存文件时如果是要交付给同事以Excel打开不乱码为准。数据导入后没有报错但类型全部是object这往往是CSV文件里混入了空字符串或特殊符号。可以先看看df[列名].unique()里的值再决定是替换成NaN还是直接剔除。处理前建议先复制一份DataFrame用df.copy()做操作避免原数据被污染后无法追溯。类型转换时如果遇到Cannot interpret X as a data type这种报错一般是astype的参数不合法检查一下有没有把字符串拼错。分组聚合时出现KeyError通常是分组键的列名不存在或者列名里有不可见字符。打印df.columns就能发现端倪可能有的列名带空格有的列名大小写不匹配。用简单的重命名步骤df.columns [c.strip() for c in df.columns]能把类似问题一次性解决。merge时报错on参数找不到列也基本是列名不一致先把公共键的列名统一再合并能省去很多无谓的排查时间。隐式链式赋值警告也是Pandas里的经典坑。大概场景是你先筛选出一个子集再给这个子集赋值结果发现原DataFrame没变或代码本身有警告。原因是Pandas无法确定你赋值的目标是视图还是副本。最好的习惯是只要想修改数据就明确用loc在一个完整的赋值表达式里完成或者在筛选后先来一个.copy()让Pandas知道你要操作的是一个独立的副本。6.3 进阶避坑与性能调试数据量大时Pandas运行慢是正常现象。定位慢在哪一步可以用%timeit或ipython的time魔法命令做小范围验证。如果发现是apply太慢尝试把函数改成向量化写法如果是merge太慢看看合并键有没有先排序或去重偶尔提前用sort_values给键排序能让merge快不少。内存优化方面一个实用技巧是把不必要的float64降到float32把object列转成category类型尤其当某列重复值很多时category类型能大幅降低内存占用。读取大文件时还可以尝试分块读取pd.read_csv的chunksize参数能让你分批处理完再合并结果。真正超大的数据该用Dask或Polars之类但那是另一个话题了至少要能分清Pandas的适用边界避免在错误场景里硬扛。还有个细节是Pandas版本升级带来的API变更。旧代码在旧版本Pandas上跑得好好的换到新版就开始报FutureWarning甚至DeprecationWarning这时候先看警告信息里提示的替代写法再对照Pandas官方文档迁移。比如早期的一些inplace参数和append方法在新版本里都被弱化或不建议使用了处理方式通常是把链式调用改成赋值式调用熟悉这种迁移思路才能跟上不断更新的生态。如果看到pd.Series.append之类的方法被移除不要慌。这属于Pandas 2.0以后逐步清理旧API的结果思路是改用pd.concat([s1, s2])。新版越来越强调显式、可预测的操作这种变化方向其实更利于写健壮代码。我在好几个老项目里都遇到过这种兼容性问题处理方式万变不离其宗看警告查官方改调用。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门