ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Pandas数据结构实战:Series、DataFrame与groupby

Pandas数据结构实战:Series、DataFrame与groupby 做数据分析这两年我最大的一个感受是Pandas这套东西入门容易但真正用得好不好差距往往就在几个核心数据结构的熟练度上。网上教程一大堆但很多都是照搬文档讲不透Series、DataFrame、groupby之间的联系和底层逻辑导致很多人学了就忘真到处理数据的时候又卡壳。这篇文章我就把自己实际项目里最常用的这些操作掰开揉碎讲一遍重点放在为什么要这么用以及那些文档里不会明说的坑。不管你是刚转行做数据分析还是已经在用Excel但想升级到Python或者写爬虫想顺便做数据清洗这篇文章都适合你。目标只有一个让你看完就能直接上手遇到真实数据不心虚。1. 先搞清楚Series一维数据的正确打开方式很多人一上来就学DataFrame结果Series反而成了“灯下黑”其实Series才是Pandas最基础的砖块。你可以把它理解成“带标签的一维数组”或者说它像是一个“Python字典的升级版”——既有像列表一样的位置下标又有像字典一样的key标签。1.1 Series的创建与本质创建一个Series非常随意可以从列表、字典、甚至一个标量来构造。实际工作中我用的最多的还是字典和列表混合的方式import pandas as pd # 从列表创建默认索引是 0,1,2... s1 pd.Series([88, 92, 75, 63]) print(s1) # 0 88 # 1 92 # 2 75 # 3 63 # dtype: int64 # 从字典创建键自动变成索引 s2 pd.Series({语文: 88, 数学: 92, 英语: 75}) print(s2) # 语文 88 # 数学 92 # 英语 75 # dtype: int64注意看Series打印出来是两列左边是索引右边是数据。这个结构决定了它和NumPy数组的本质区别NumPy数组只有位置下标而Series多了一层“标签索引”。标签索引带来的好处在于数据顺序乱了也不怕你可以通过名字直接取数这在真实数据集里太常用了。另外还有一个冷门但很实用的点Series的索引可以重复。虽然我不推荐随便搞重复索引但如果你遇到一个索引有重复值的Series访问的时候会返回一个Series切片而不是标量这经常是bug来源要格外小心。1.2 索引与切片位置索引和标签索引要分清Series的索引是最容易翻车的地方。Pandas里有两套索引方式一套是标签索引用.loc一套是位置索引用.iloc。还有一个容易混淆的就是直接在方括号里传值这属于“混合索引”——传的是标签值但如果遇到整数标签行为又会变化极其容易踩坑。s pd.Series([10, 20, 30], index[a, b, c]) print(s[a]) # 10标签索引 print(s[0]) # 10这里因为索引是字符串所以方括号会退化为位置索引 print(s.loc[a]) # 10明确是标签 print(s.iloc[0]) # 10明确是位置看到没有当索引是字符串时s[0]还能按照位置取到10但如果索引本身是整数呢比如s_int pd.Series([10, 20, 30], index[3, 1, 2]) # 这里 s_int[1] 会返回 20按标签取即索引为1的那个值 # 但有时候你以为它会按位置取第一个值结果完全不同这是新手最容易踩的坑。所以我的建议是任何情况下显式使用.loc和.iloc永远不要让Pandas猜你的意图。切片也一样标签切片是“闭区间”包含两端位置切片是“开区间”不包含末端这个规则搞混了数据一出错就是灾难性的。切片还有一个技巧就是步长和反向切片。偶尔需要逆序查看数据s.iloc[::-1]比任何方法都简单。1.3 缺失值处理真实数据里绕不开的坑Series数据一旦多起来就必然遇到NaN。我最常用的三个函数是isna()、dropna()、fillna()。它们的用法文档里都有我这里只想强调一个很多人忽视的细节dropna()默认是返回新对象不会修改原Series如果想原地改要加inplaceTrue但我个人不推荐这个参数——它容易让人产生“数据已经改好了”的错觉调试的时候很难发现bug。更稳妥的写法是显式赋值s pd.Series([1, None, 3, None, 5]) # 原来的 s 不会变 s.dropna() # 覆盖回去才是真正生效 s s.dropna()另外一个细节是关于fillna的method参数。对时间序列数据用methodffill向前填充用前一个值填空和methodbfill向后填充非常方便尤其处理传感器数据、水位数据时这招比直接用均值填充合理得多——因为相邻时间的数值往往更接近。要注意的是新版Pandas里methodffill被标记为废弃建议直接使用ffill()函数。2. DataFrame从一维到二维数据分析的主战场如果说Series是单列数据那DataFrame就是多列Series拼起来的一张二维表。你完全可以把它理解成Excel里的一个sheet或者数据库里的一张表。它是Pandas里绝对的主角80%的工作都在跟它打交道。2.1 三种创建方式在真实业务里DataFrame通常不是自己造的而是从csv、Excel、数据库里读出来的。但面试和测试场景经常考从字典创建DataFrame写法就两种# 方式一外层字典键是列名值是整列数据 df1 pd.DataFrame({ 姓名: [张三, 李四, 王五], 语文: [88, 92, 75], 数学: [95, 80, 66] }) # 方式二外层列表每一项是一行常见于爬虫返回的数据 rows [ {姓名: 张三, 语文: 88, 数学: 95}, {姓名: 李四, 语文: 92, 数学: 80}, {姓名: 王五, 语文: 75, 数学: 66} ] df2 pd.DataFrame(rows)两种方式都能得到同样的表结构区别在于数据来源。前者适合你手头有多个平行列表比如从多个列表打包后者适合爬虫结果或API返回的JSON列表。创建时还能指定columns参数调整列顺序。如果创建的字典里有缺失的列Pandas会自动填NaN。这个特性在做“对齐”需求时特别有用——比如你想让两张表有完全相同的列结构只创建缺失的部分也能安全合并。2.2 loc、iloc与布尔索引的实战用法DataFrame的取数逻辑比Series多了一个维度先取行再取列。这两种索引方式的语法我已经背得滚瓜烂熟# 标签索引行列都写标签 df.loc[行标签, 列标签] # 位置索引行列都写位置 df.iloc[行位置, 列位置]但真正在实战中花式取数最常用的其实是布尔索引。比如筛选语文成绩大于等于90分的学生mask df[语文] 90 # 得到一列 True/False 的 Series df[mask] # 保留mask为True的行这行代码几乎每天都在用。但要提醒一点布尔索引返回的是视图还是副本在Pandas里一直是个微妙的话题。官方文档说过df[mask]返回的是副本但如果你在这个结果上做链式赋值比如df[mask][新列] ...很容易触发SettingWithCopyWarning而且这个警告并不代表一定出错但它提醒你你改的可能是副本而不是原表。安全的方法是先拷贝一份再操作或者用.loc来赋值df.loc[df[语文] 90, 优秀] 是这种写法既不会产生告警也不会踩视图/副本的坑是我最推荐的做法。2.3 数据筛选与列操作的高频场景日常处理数据时列的增删改查比行更频繁。这里我直接给出几个常用操作新增列df[总分] df[语文] df[数学]删除列df.drop(columns[总分])注意不写inplaceTrue时不生效重命名列df.rename(columns{语文: Chinese})调整列顺序df df[[数学, 语文, 姓名]]双括号按指定顺序取列这个技巧很适合最后输出报表时调整字段顺序筛选行子集df[df[语文] 80][[姓名, 语文]]注意这种连续操作返回的是拷贝性能上如果你数据量上千万建议用df.loc[条件, 列名列表]单次操作实战里还有一个常用技巧是isin和between。isin用来筛选某一列的值是否在一个集合里between用来筛选区间# 筛选班级在一班和三班的学生 df[df[班级].isin([一班, 三班])] # 筛选成绩在80到90之间含边界 df[df[语文].between(80, 90, inclusiveboth)]这种写法比连续加两个条件80、90要简洁得多性能也更好因为Pandas内部对between做了专门优化。3. 常用运算与内置方法让数据“开口说话”基础取数会了接下来一步就是计算。Pandas里的运算遵循一个非常核心的机制——索引对齐这是它跟普通Python列表算术运算的最大区别。不理解索引对齐你会遇到一堆莫名其妙的NaN。3.1 算术运算与广播机制先看一个最简单的例子s_a pd.Series([1, 2, 3], index[a, b, c]) s_b pd.Series([10, 20, 30], index[b, c, d]) print(s_a s_b) # a NaN # b 22.0 # c 33.0 # d NaN看到没有两个Series相加时Pandas会自动把相同索引的值相加索引对不上的位置就变成NaN而不是像列表那样按位置相加。这个特性在数据分析里其实是优势——当你在合并多天的数据时日期索引能自动对齐。但它也是新手感到离谱的坑为什么我的结果到处都是NaN因为索引没对齐。如果非要按位置相加不推荐但偶尔需要可以用reset_index(dropTrue)先重置索引s_a.reset_index(dropTrue) s_b.reset_index(dropTrue)至于广播机制就是说标量跟Series/DataFrame运算时会“广播”到每个元素上。比如df[语文] * 2就是每一行都乘2再比如整个DataFrame乘一个系数每一列都乘。学会用广播机制就能避免很多显式循环代码又短又清晰。3.2 排序、去重与重命名排序是数据探索里绕不开的一步。最常见的两种按值排序df.sort_values(语文, ascendingFalse)默认升序降序要显式指定按索引排序df.sort_index()有时候拼接完数据顺序混乱就会用到sort_values有一个实用参数by可以传多个列名形成复合排序。比如先按班级排序再按语文成绩降序排列df.sort_values([班级, 语文], ascending[True, False])去重是数据清洗环节的高频操作。drop_duplicates()默认基于所有列判断是否重复如果你只想根据某一列去重可以指定subset参数# 按姓名去重保留第一次出现的那条记录 df.drop_duplicates(subset[姓名], keepfirst)这里有个容易忽略的参数是keep它可以取值first、last或False控制保留哪一行。我有时会特意用keepFalse把重复数据全部删掉而不是保留一个。重命名这个操作看起来简单但实际项目里我经常用它来做“列名标准化”。从数据库导出的字段名通常全是英文下划线风格而报表需要中文列名一个rename加字典映射就搞定了df.rename(columns{ stu_name: 姓名, chinese_score: 语文, math_score: 数学 }, inplaceFalse)3.3 apply、map与applymap的区分这三个函数是Pandas里比较容易混淆的东西我也是用了很久才完全厘清。map只适用于Series把每个元素应用一个函数或映射关系返回新的Seriesapply适用于Series和DataFrame在DataFrame上默认按列axis0应用函数applymap只适用于DataFrame把函数应用到每一个元素上逐元素变换实际工作中我用map最多的场景是做编码映射。比如把性别的中文映射成英文gender_map {男: M, 女: F} df[gender_en] df[性别].map(gender_map)map还能传一个自定义函数比如计算完成率时用lambda表达式df[完成率] df[已完成].map(lambda x: round(x * 100, 2))而apply在DataFrame上的操作更强大。比如你想统计每一列的非空值个数常规写法是循环但用apply一行搞定df.apply(lambda col: col.count())如果你想按行求均值指定axis1df[[语文, 数学, 英语]].apply(lambda row: round(row.mean(), 2), axis1)applymap用的场景稍微少一点通常是在处理全表清洗时比如把所有字符串的统一去掉空格或者把整个DataFrame的所有值统一保留两位小数。3.4 描述性统计和相关系数拿到一个陌生DataFrame第一步一定是先看数据长什么样这时候describe()就是神器df.describe()它会一次性输出每列数值型数据的计数、均值、标准差、最小值、四分位数、最大值。有了这些基础统计量你就能快速判断数据是否异常。比如最大值和均值差了好几个数量级那就要怀疑是不是有脏数据。这里有一个我经常用的小技巧describe(includeobject)可以看到非数值列的一些统计信息。默认describe()是不管字符串列的但当你需要快速了解一个城市字段有多少个唯一值、出现次数最多的值是哪个时加个includeobject就够了。如果要看两列之间的相关性比如“语文成绩”和“数学成绩”有没有关系直接用df[[语文, 数学]].corr()。输出的是一个2x2的相关系数矩阵越接近1就说明正相关越强。这个在特征工程里做特征筛选时非常常用通过相关系数矩阵你可以快速找出那些高度相关的冗余特征。4. groupby分组聚合从“全表看”到“分组看”前面的操作都是“全表视角”但数据分析里最高频的需求其实是“分组视角”——比如按班级统计平均分、按城市统计销售额、按月统计用户数。这就是groupby的主场。4.1 拆开看groupby的底层逻辑网上讲groupby的文章很多但大多只讲了“怎么用”没讲“是什么”。很多人学了df.groupby(班级).mean()以后遇到稍微复杂一点的操作就懵了因为没建立起groupby的执行模型。实际上groupby底层是三个步骤拆分split- 应用apply- 合并combine。拆分Pandas把DataFrame按照你指定的列分成若干组每一组是原表的一个子集。比如按“班级”分组一班的所有行是一组三班的所有行是一组。应用对每一组分别执行你指定的操作求和、均值、自定义函数等。这一步是核心也是最灵活的地方。合并Pandas把各组的计算结果拼起来形成最终输出。理解了这三步你就知道为什么groupby之后的操作那么灵活。比如下面的代码df.groupby(班级)[语文].mean()拆解开来先按班级拆成多个DataFrame子集再对每个子集的“语文”列求均值最后合并成一个Series输出索引是班级。这个输出你可以直接接续处理比如转成DataFrame、排序、合并到原表等。4.2 聚合函数sum、mean、agg的用法groupby之后最基本的就是调用聚合函数。sum()、mean()、count()、max()、min()这些都是现成的聚合函数直接跟在groupby后面就行# 每个班级语文、数学的平均分 df.groupby(班级)[[语文, 数学]].mean() # 每个班级的人数 df.groupby(班级).size()但实际工作中经常遇到一个需求不同列用不同的聚合函数。比如我想看每个班级的语文平均分、数学最高分、英语最低分。这时候就需要用agg()方法它可以接收一个字典键是列名值是要用的聚合函数df.groupby(班级).agg({ 语文: mean, 数学: max, 英语: min })agg还有一种更灵活的用法传一个列表同时输出多个统计量df.groupby(班级)[语文].agg([count, mean, max, min])输出会是一个DataFrame列名就是这些函数名。你可以直接通过列名比如[mean]去取结果非常方便。这里我必须提醒一个容易踩的坑groupby(班级).agg({语文: mean, 数学: mean})输出结果的列顺序有时候并不像你想象的那样是字典的顺序而是分组列在前聚合列按排序后的顺序排列。如果你对列顺序敏感建议输出后手动重排。4.3 transform与apply分组计算的两种进阶姿势agg很好用但它有一个局限——结果的行数等于组数没法拿到原表的长度。但现实中很多需求是我想新增一列列的值是“该行所属班级的平均分”。这时候agg就不行了需要用到transform。df[班级语文平均分] df.groupby(班级)[语文].transform(mean)transform的结果会保持与原DataFrame相同的行数它会把每个分组的聚合结果广播回原表的每一行。这个特征在做“剔除组内离群值”“同组间差值计算”这类操作时非常实用。apply则更底层、更灵活。它可以接收一个自定义函数这个函数的输入是每个分组子表输出可以是一个标量、一个Series或一个DataFrame。比如我想计算每个班级语文成绩的极差最大值减最小值df.groupby(班级)[语文].apply(lambda s: s.max() - s.min())apply的强大之处在于你可以在函数里写任何逻辑不只是简单的聚合。比如你可以在每个分组里先排序再取前两名再拼接成一个字符串什么都能干。代价是它比内置聚合函数慢很多数据量大的时候要慎用。4.4 多列多条件分组与透视表对比groupby还可以传多个列实现多级分组。比如想看“每个班级、每个性别”的语文平均分df.groupby([班级, 性别])[语文].mean()这个输出是一个带MultiIndex多重索引的Series。很多人第一次看到这个输出会有点不适应因为索引变成两层了。想转成表格形式可以加一个unstack()df.groupby([班级, 性别])[语文].mean().unstack()这样看起来就像一张透视表行是班级列是性别。这就自然引出了和第3个核心API——pivot_table的对比。pd.pivot_table(df, values语文, index班级, columns性别, aggfuncmean)同样是输出一张“班级 x 性别”的平均分表。两者的核心区别在于pivot_table是做数据透视而groupby是先分组再操作后者更灵活。如果你只是想按某些维度聚合取值groupby足够如果要做类似Excel数据透视表那种行列交叉汇总pivot_table更直观、可读性更好。5. 常见问题与排查技巧实录写代码最烦的就是报错和结果不对。我把这几年用Pandas遇到的高频问题整理成一个速查表尤其是围绕这几个核心结构的坑都列在这里现象原因解决办法出现大量NaN直接对Series做算术运算索引没对齐所致检查两个Series的索引是否一致或者先reset_index再用位置索引链式赋值报警告在df[mask]返回的副本上做赋值改用df.loc[mask, 列名] 值groupby(列)之后索引消失了分组列变成了新的索引列加reset_index()把索引转回普通列布尔筛选后行数不对条件里用了or而不是|还有一个低级问题是、忘了加括号sort_values没生效忘了赋值回去默认返回新对象要df df.sort_values(...)agg结果列顺序怪代码对输出列顺序敏感输出后手动按[列名列表]重排drop_duplicates去不掉重复没有指定subset而其他列有细微差异明确指定subset[关键列]注意上面表格里的很多问题表面上看是语法问题本质上都是对“索引对齐”和“返回副本还是视图”这两个底层机制理解不深。建议你遇到奇怪的结果时先打印出中间结果的shape和index看看定位问题会快很多。除了表格还有两个我自己总结的排查心得。第一个心得善用shape和head()做分步调试。我写稍微复杂一点的Pandas代码从来不会一口气写完一条长链。而是每一步都用print(df.shape)和print(df.head())来确认数据变化。等你排除了所有疑问再把代码合并成一条链。这样能最大程度减少“结果不对还得回头改代码”的窘境。第二个心得处理大量数据时尽量避免用iterrows()循环行。虽然iterrows()看起来方便但它非常慢。如果确实要遍历行优先考虑用apply(axis1)再不行就用to_records()转成数组后再循环性能能提升好几个量级。这在大数据量分组计算里是一个很关键的优化点。6. 实操总结一整套分析流程应该怎么串起来前面讲了这么多知识点我最后用一个非常典型的场景把它们串起来——假设你手上有一个CSV文件内容是某班两次考试的语文、数学成绩你需要完成读取数据查看数据基本信息有哪些列、每列缺失情况、唯一值情况按班级分组计算各科平均分计算总分并判断每个学生的进步情况筛选出语文成绩在平均分以上的学生整个流程写下来大概是这样的import pandas as pd # 1. 读取数据 df pd.read_csv(scores.csv) # 2. 查看基本信息 print(df.shape) # 确认行数列数 print(df.info()) # 确认每列类型和缺失情况 print(df.head()) # 看前几行 # 3. 按班级分组计算平均分 avg_by_class df.groupby(班级)[[语文, 数学]].mean() print(avg_by_class) # 4. 新增总分列 df[总分] df[语文] df[数学] # 5. 筛选语文高于全年级平均分的学生 avg_chinese df[语文].mean() top_students df[df[语文] avg_chinese] print(top_students) # 6. 新增列每个人语文成绩与班级平均分的差异 df[语文与班均差] df[语文] - df.groupby(班级)[语文].transform(mean)这个流程刚好把Series、DataFrame的索引、筛选、groupby、transform全部串在一块了。你会发现一旦理解了底层逻辑这些操作拼起来就是顺理成章的事。另一个实用小技巧是在你把数据导出给其他人之前记得把索引重设一下因为groupby之后的行索引常常不是0,1,2...直接导出会导致Excel里出现奇怪的索引列。用reset_index(dropTrue)或者reset_index()处理掉别人打开报表时才不会一头雾水。最后再说点实在的我之前带过几个新人发现他们学Pandas有个共同误区一上来就想把所有函数背下来结果背几天就放弃了。说实话Pandas函数根本不用背我用到现在也不可能记全所有参数。真正要紧的是把数据结构Series、DataFrame、Index想清楚把底层机制索引对齐、视图与副本、分组三步骤搞明白剩下的细节用的时候查文档就行。还有一个习惯我觉得很值得提遇到不会的函数别急着搜完整代码先自己写一个最小的DataFrame测试一下看输出是什么。很多疑惑只要动手打印几次中间结果就瞬间懂了。这也是我这几年最大的经验心得。
返回列表