资讯动态

Python数据分析入门:Pandas DataFrame核心操作与实战清洗指南

发布时间:2026/10/14 17:21:30 来源:尧图企业网站定制
1. 为什么数据分析入门绕不开DataFrame刚开始学Python数据分析的时候大部分人都会经历一个阶段列表、字典、循环写得很溜了但一碰到真实数据就手足无措。比如给你一份几千行的Excel订单表里面有日期、金额、客户ID、商品分类用纯Python去处理循环套条件代码写到怀疑人生。直到你认识了pandas里的DataFrame才发现原来处理表格数据可以这么干净利落。DataFrame是pandas库最核心的数据结构本质上就是一张二维表格有行有列长得和Excel表格、SQL数据库表几乎一模一样。但它比Excel强大得多比SQL又灵活得多。你可以把它理解为“带标签的二维数组”每一列可以有不同的数据类型——数值、字符串、时间、布尔值都可以混在一张表里而且pandas对每一列的操作都做了高度优化很多在纯Python里需要写几十行循环的操作用DataFrame一句话就搞定了。这篇文章不是给你抄官方文档而是从一个实际使用者的角度把DataFrame最常用、最关键的知识点梳理出来。内容包括DataFrame的创建、索引与切片、缺失值处理、数据类型转换、常用聚合操作以及一个完整的数据清洗案例。适合刚学完Python基础、准备进入数据分析方向的人也适合写了两年代码但一直没系统捋过pandas的老手。2. DataFrame的核心概念2.1 从Excel说起行、列与轴的理解如果你用过Excel理解DataFrame就不难。Excel里一张表有行号1、2、3……和列标A、B、C……DataFrame也类似但它更讲究两个东西索引和列名。先看一个最简单的创建方式import pandas as pd df pd.DataFrame({ 姓名: [张三, 李四, 王五], 年龄: [25, 30, 35], 城市: [北京, 上海, 广州] }) print(df)输出结果姓名 年龄 城市 0 张三 25 北京 1 李四 30 上海 2 王五 35 广州注意看左边的0、1、2是行索引index顶部的“姓名”“年龄”“城市”是列名columns。DataFrame就是由这两套标签系统定位数据的。轴axis是理解pandas操作的关键。axis0表示纵向也就是行方向axis1表示横向也就是列方向。很多初学者在处理drop、apply、sum这些方法的时候被axis参数折磨得不行。我个人的记忆方法是axis0是“沿着行往下走”axis1是“沿着列往右走”。比如df.drop(0, axis0)是删掉第0行df.drop(年龄, axis1)是删掉年龄这一列。2.2 索引为什么重要位置与标签的区别DataFrame的索引有两个维度一个是行索引index一个是列索引columns。行索引默认是整数从0开始自增但你也可以指定有意义的标签比如日期、用户ID。列索引就是列名。这里有一个特别容易让新手懵的点位置和标签是两个概念。位置是数据在表格中的物理位置第几行第几列标签是你给它起的名字。df[0]在早期版本里是取第一列但在某些场景下会报错或者行为不一致因为方括号直接传入整数时pandas在列索引和位置之间做了很多兼容处理。所以我强烈建议取列用df[列名]取行用df.loc[行标签]或df.iloc[行位置]不要混用。心得用df[列名]得到一个Series一维数据用df[[列名1, 列名2]]得到一个DataFrame二维表格。这个区别在处理后续的apply、map操作时非常关键。3. DataFrame的创建与数据加载3.1 从字典、列表和NumPy数组创建创建DataFrame最常用的方式是从字典创建键是列名值是列数据。这种方式最直观适合小型测试数据。import numpy as np data { 商品: [手机, 电脑, 平板], 价格: [3999, 5999, 2599], 销量: [120, 85, 96] } df pd.DataFrame(data)从列表的列表嵌套列表创建时需要同时指定列名否则列名就是默认的0、1、2data [ [手机, 3999, 120], [电脑, 5999, 85], [平板, 2599, 96] ] df pd.DataFrame(data, columns[商品, 价格, 销量])还有一种常见做法是从NumPy数组创建pandas和NumPy的配合是天作之合。数组创建之后加上行索引和列名瞬间就变成了可读性极强的表格arr np.random.randn(5, 3) df pd.DataFrame(arr, columns[A, B, C], index[r1, r2, r3, r4, r5])3.2 从CSV、Excel和SQL读取数据实际工作中数据很少是手敲进去的更多的是从文件或数据库加载。从CSV读取是最常见的一个pd.read_csv()基本搞定df pd.read_csv(sales_data.csv)但这里有很多隐藏参数值得注意encodingutf-8解决中文乱码问题的首选。遇到GBK编码的文件要改成encodinggbk。sep,默认分隔符是逗号但有些CSV用分号或制表符分隔需要指定。parse_dates[日期]自动把指定列解析成日期类型避免后面手动转换。dtype{用户ID: str}强制指定列类型比如用户ID虽然是数字但本质上是字符串导入时就要定好。df pd.read_csv(sales_data.csv, encodingutf-8, parse_dates[日期], dtype{用户ID: str})从Excel读取需要依赖xlrd或openpyxl库方法基本一致df pd.read_excel(sales_data.xlsx, sheet_name订单表)从SQL读取需要先用SQLAlchemy或sqlite3建立连接然后pd.read_sql(sql, con)。这一步在数据分析岗位面试里经常问实际操作也确实是pandas连接关系型数据库的标准姿势。直觉对比一下Excel适合人类阅读CSV适合程序交换数据SQL适合大规模存储。DataFrame的定位是在这些数据源之上提供一个统一的内存中的操作界面。3.3 先看一眼数据再动手head、info和describe拿到一个陌生的DataFrame第一件事不是处理而是观察。我个人的习惯是固定三连df.head() # 看前5行了解列名和数据大致样子 df.info() # 看每列类型、非空值数量、内存占用 df.describe() # 看数值列的统计摘要均值、标准差、最小值、最大值、分位数这三行代码能让你在10秒内判断出数据的规模、质量、是否有空值、哪些列是数值型哪些是字符串。df.info()的输出里会显示每一列的非空计数如果某一列的总行数是1000但非空只有800说明有200个缺失值后面要处理。4. 索引与切片DataFrame最常用的操作4.1 loc、iloc和布尔索引的区别这是整个DataFrame学习中最重要的一节也是面试和笔试的重点。很多人卡在DataFrame的索引上根本原因是没有搞清楚loc和iloc各自负责什么。方法按什么取典型用法返回值df.loc[行, 列]行标签、列标签df.loc[0, 年龄]标量df.iloc[行, 列]行位置、列位置df.iloc[0, 1]标量df.loc[行列表, 列列表]多个标签df.loc[[0, 2], [姓名, 城市]]DataFramedf.iloc[行切片, 列切片]位置切片df.iloc[1:3, 0:2]DataFrame关键区别在于loc的切片是闭区间iloc的切片是半开区间。比如df.loc[0:2]会返回第0、1、2行三行数据而df.iloc[0:2]只返回第0、1行两行。布尔索引布尔掩码是数据分析中最实用的筛选方式。给你一个场景筛选出所有年龄大于28岁的行。df[df[年龄] 28]这个操作拆开看就是df[年龄] 28先得到一个布尔Series每一行对应True或False然后把这个布尔Series传给df的方括号pandas只保留值为True的行。这个写法可以串联多个条件df[(df[年龄] 28) (df[城市] 上海)]注意两个细节多个条件必须每个都加圆括号逻辑与用而不是and逻辑或用|而不是or。这是初学者最容易踩的坑and和or在pandas里面对Series时无法进行逐元素判断会直接报错。4.2 链式赋值的坑SettingWithCopyWarning这是我在实际项目中遇过的最隐蔽的一个坑。大致场景是这样的df df[df[年龄] 25] df[新列] df[年龄] * 2有时候会冒出一个SettingWithCopyWarning的警告但不影响结果。有些时候你会发现改了df之后原数据源df_original也跟着变了或者反过来改了之后没生效。这个警告的本质是pandas无法确认你是在修改一个副本copy还是视图view。我的建议是如果不确定先用.copy()显式复制一份。比如df_filtered df[df[年龄] 25].copy() df_filtered[新列] df_filtered[年龄] * 2这样做的代价是额外的内存占用但换来的是行为的确定性。在数据量不大几万行以内时这点内存完全可以接受。而且copy()之后后续的链式操作不会再出现那个恼人的警告排查问题也方便很多。4.3 修改行和列的常用方式给DataFrame新增一列最直接的方式是赋值df[年收入] df[月收入] * 12根据条件新增一列可以用numpy.where比apply写自定义函数更快import numpy as np df[收入等级] np.where(df[月收入] 10000, 高收入, 普通收入)删除行或列用drop这个方法的axis参数很多人搞反df.drop(columns[月收入], inplaceTrue) # 删列 df.drop(index0, inplaceTrue) # 删行关于inplaceTrue最近几个版本的pandas已经开始不推荐了。它直接在原对象上修改返回None如果不写inplace则返回一个修改后的新DataFrame原对象不变。在实际工作中我基本不用inplace而是写成df df.drop(...)这样链式操作更方便也不会出现“原对象被改了但没收到返回值”的困惑。5. 缺失值与数据类型数据清洗的主战场5.1 认识缺失值NaN究竟是什么真实数据里空值几乎无法避免。Excel导出的时候某个单元格没填数据库里某条记录的字段为NULL爬虫抓取的时候某种情况没有返回值——这些都是NaN的来源。在pandas里缺失值用NaNNot a Number表示。需要注意NaN不是0不是空字符串也不是None。它是浮点数类型所以一个包含NaN的列整列都会被提升为浮点数这其实就是为什么很多整数字段读进来变成float的原因。判断缺失值不能用要用专门的方法df.isna() # 返回一个布尔DataFrame每个单元格标记是不是缺失 df.isna().sum() # 每个列的缺失数量汇总这一步非常好用5.2 dropna和fillna的选择逻辑处理缺失值只有两条路删掉或填补。选择哪条路取决于数据量、缺失比例和业务含义。df.dropna() # 删除任何包含缺失值的行慎用 df.dropna(howall) # 只删除全为缺失值的行 df.dropna(subset[关键列]) # 只在关键列缺失时才删除df.dropna()不带参数直接删除所有含NaN的行在数据量小的时候可能导致大量信息丢失。如果你有一份10000行的数据表某一列有5000个缺失直接dropna会把剩下5000行也一起丢了因为其他列也有缺失。这就是为什么dropna必须配合参数使用的原因。填补缺失值用fillnadf[年龄] df[年龄].fillna(df[年龄].median()) # 用中位数填充 df[城市] df[城市].fillna(未知) # 用固定值填充 df[销量] df[销量].fillna(methodffill) # 用上一条记录填充适合时间序列对于数值列我更喜欢用中位数而不是平均数填充因为中位数不受极端值影响。五分之一的年龄数据缺失用平均年龄填充一旦数据里有一个“年龄200”的异常值平均值就被带偏了而中位数依然稳定。5.3 数据类型转换与astype的大坑pandas的列类型可能和你预期的不一样。最常见的就是明明是一列数字号码读进来却变成了浮点数因为某一行空着或者日期数据读进来是字符串还得手动转换。类型转换的基础方法是astypedf[用户ID] df[用户ID].astype(str) # 数值转字符串 df[年龄] df[年龄].astype(int) # 前提是没有缺失值 df[日期] pd.to_datetime(df[日期]) # 字符串转日期用to_datetime而非astypeastype有个大坑当列里存在NaN时转成int会直接报错。因为NaN是浮点数无法直接变成整数。解决办法是先fillna填补或者用pd.to_numeric配合errorscoerce把无法转换的值变成NaNdf[价格] pd.to_numeric(df[价格], errorscoerce)errorscoerce的意思是把“不能转换成数值的”统统变成NaN。这在清洗混合格式数据时极其好用比如一列价格里既有数字又有“约100元”这种脏数据coerce之后脏数据变成NaN你再用fillna或者直接删除它们。日期转换的坑也值得单独说说。pd.to_datetime很智能它能识别大多数常见格式比如2024-01-01、2024/1/1、20240101。但如果你的数据里混了两种格式它可能会抛异常或者把某一行解析成NaT缺失时间。出现这种情况时先加errorscoerce再检查转换失败的条数。6. 实战拆解DataFrame常用操作与综合案例6.1 常用的数据聚合和分组操作分组聚合是DataFrame的灵魂操作对应SQL里的GROUP BY。核心方法就是groupbydf.groupby(城市)[销量].sum()这个写法拆解开来是先按“城市”分组然后取“销量”这一列最后求和。结果是一个以城市为索引的Series。如果你想要DataFrame格式加.reset_index()。groupby最常见的一个困惑是聚合后列的选择。df.groupby(城市).sum()会对所有数值列求和有时候你不想要这样。更规范的做法是指定聚合列和聚合函数result df.groupby(城市).agg({ 销量: sum, 价格: mean, 订单数: count })agg可以同时做多个不同聚合非常灵活推荐优先掌握。另外还有一个高阶技巧groupby.diff()、groupby.transform()、groupby.shift()在金融数据和面板数据处理中非常实用但基础的sum、mean、count已经能覆盖80%的需求。6.2 apply和applymap给DataFrame加自定义逻辑当内置方法不够用的时候apply函数可以让你传入自定义函数对每一行或每一列做处理。这是DataFrame扩展性的体现。def 收入区间(x): if x 10000: return 高 elif x 5000: return 中 else: return 低 df[收入区间] df[月收入].apply(收入区间)axis1表示按行应用此时函数接收的是一整行数据可以跨列计算df[总价] df.apply(lambda row: row[单价] * row[数量], axis1)apply虽然好用但性能不如向量化操作。在几万行的数据上循环写法可能只要几秒apply有时要几十秒。如果你追求性能优先用np.where或直接列运算apply只有在逻辑确实复杂时才上。6.3 综合案例一口气清洗一份订单数据现在把前面的知识点串起来模拟一个真实的清洗场景。假设你从某个系统导出了一份订单数据CSV结构如下订单号、日期、客户ID、商品分类、单价、数量、总金额、备注。现在要做数据清洗目标是生成一份可以进入统计建模的干净表格。import pandas as pd import numpy as np # 读取数据同时指定日期类型和客户ID为字符串 df pd.read_csv(orders.csv, encodingutf-8, parse_dates[日期], dtype{客户ID: str}) # 第一步看数据概况 print(df.info()) print(df.head(3)) # 第二步处理缺失值 # 关键字段缺失就删除非关键字段用填充 df df.dropna(subset[订单号, 日期]) df[单价] pd.to_numeric(df[单价], errorscoerce) df[数量] pd.to_numeric(df[数量], errorscoerce) df[总金额] df[总金额].fillna(df[单价] * df[数量]) # 总金额缺失时用单价乘数量补 df[商品分类] df[商品分类].fillna(未分类) # 第三步筛掉异常数据 # 单价或数量为负的直接删 df df[(df[单价] 0) (df[数量] 0)] # 第四步新增字段 df[成交金额] df[单价] * df[数量] df[月] df[日期].dt.month # 第五步按月份统计成交额 monthly df.groupby(月)[成交金额].sum().reset_index() print(monthly)整个过程看起来不难但每一步都有学问。第一步的dtype{客户ID: str}如果漏了后面你做客户维度分析时客户ID会被当成整数处理明明前面没有0但你合并数据时可能就会错位。第四步的df[日期].dt.month用到了datetimelike属性访问器dt只有列类型是datetime时才能用这也是为什么一开始就parse_dates的原因。6.4 保存结果的正确姿势清洗好的数据要输出to_csv和to_excel是标准操作df.to_csv(cleaned_orders.csv, indexFalse, encodingutf-8-sig)这里有几个细节值得注意indexFalse不保存行索引否则文件里会多一列未命名数字。encodingutf-8-sig很多数据分析师的坚持。直接写utf-8导出的CSV用Excel打开时中文会乱码而utf-8-sig不会。原因很简单utf-8-sig在文件开头加了BOMByte Order MarkExcel识别这个标记后就知道该用什么编码打开。如果只是中间结果推荐用to_pickle保存读取速度快且能保留数据类型不会出现CSV那种“年龄列又被读成浮点数”的问题。7. 常见问题速查与避坑清单7.1 高频报错和解决办法在学习DataFrame的过程中会碰到各种报错下面这几个是我见过最频繁的报错信息原因解决办法KeyError: 列名列名不存在先df.columns确认列名注意空格和大小写ValueError: cannot convert float NaN to integer列里有NaN但转int先fillna或dropna再转SettingWithCopyWarning链式赋值导致pandas无法确定操作对象操作前加.copy()ValueError: The truth value of a Series is ambiguous用and/or连接多个布尔条件改成和AttributeError: DataFrame object has no attribute append新版pandas移除了append方法用pd.concat([df1, df2])7.2 两个值得养成的习惯第一个习惯是随时查看df.info()和df.describe()。很多人拿到数据不管三七二十一直接处理后面发现了问题又开始从头查。信息密度最高的两个方法就是info和describe花十秒钟看一眼能帮你省下两个小时的排查时间。第二个习惯是操作前先备份原数据。在用链式操作处理一份重要数据时先用df.copy()存一份副本。清洗过程中出现任何可疑结果回头对比原数据就能快速定位。别觉得麻烦我在实际工作中就因为没备份清洗到一半发现逻辑写错只能从头再来浪费了大量时间。7.3 版本差异和兼容性pandas版本更新很快不同版本之间的API有些差异。如果你用的是旧版教程可能会发现某个方法在你当前版本里不存在。不必慌张最稳妥的办法是查看当前版本print(pd.__version__)然后搜索新版API对应的写法。常用方法如read_csv、merge、groupby接口都比较稳定不用太担心。但像append这种已经废弃的方法用了会直接报错建议用pd.concat替代。8. 写在最后的实操心得从纯Python列表操作转向pandas时最难转变的不是语法而是思维方式。列表操作关注“怎么循环”而DataFrame操作关注“怎么声明我要做什么”大部分操作只需要一句表达式。我自己的体会是DataFrame的学习曲线并不是线性的前期会有一段“明明知道有对应方法但就是找不到”的阶段。这个阶段没有任何捷径就是靠多写。建议你手头准备一份模拟数据把读入、清洗、分组、聚合这四个环节反复做几遍每次卡住就查文档时间长了很多操作会变成一种直觉。另外想强调的一点是不要一味追求某种“最高效”的写法。对于大多数场景可读性和稳定性比性能重要得多。一行复杂度极高的链式操作看起来很酷但三个月后你自己回来看可能都看不懂当初的逻辑。我见过太多代码为了省几行结果可维护性变得极差。只要head()看一眼结果没问题、info()检查类型正确这个写法就是好写法。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑