Pandas 不擅长的结构化数据运算
Pandas 是 python 的一个数据分析包,是基于 NumPy 的一种数据分析工具,其中纳入了大量库和一些标准的数据模型,提供了快速便捷地处理数据的函数和方法,是高效地操作结构化数据集所需的工具,也是使 Python 成为强大而高效的数据分析环境的重要因素之一。
但是相信经常使用 Pandas 的同学在处理结构化数据运算时也会遇到一些麻烦,这些问题要么使得问题解决很复杂(代码难写),要么使得运行极其缓慢(效率低下),下面总结整理了一些 Pandas 的困难问题进行吐槽,如有谬误欢迎指正,也欢迎大家参加到这次的“Pandas 吐槽大会”。
切片赋值
切片赋值,指取数据中的某个值或某一块值,修改其中的值,如把第 3 行第 5 列的 x 值修改为 y 值。
使用员工信息数据作为案例进行介绍,数据片段如下:
EID | NAME | SURNAME | GENDER | STATE | BIRTHDAY | HIREDATE | DEPT | SALARY |
1 | Rebecca | Moore | F | California | 1974/11/20 | 2005/3/11 | R&D | 7000 |
2 | Ashley | Wilson | F | New York | 1980/7/19 | 2008/3/16 | Finance | 11000 |
3 | Rachel | Johnson | F | New Mexico | 1970/12/17 | 2010/12/1 | Sales | 9000 |
4 | Emily | Smith | F | Texas | 1985/3/7 | 2006/8/15 | HR | 7000 |
5 | Ashley | Smith | F | Texas | 1975/5/13 | 2004/7/30 | R&D | 16000 |
… | … | … | … | … | … | … | … | … |
问题一:将R&D 部门员工的工资改成 20000
Python 代码
import pandas as pd data = pd.read_csv('Employees.csv') data[data['DEPT']=='R&D']['SALARY']=20000 print(data) |
导入 Pandas 读取数据 找到 R&D 部门,修改工资值 |
运行结果:
SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead
可以看到,报了这个 SettingWithCopyWarning,而且修改的值并没有起作用。相信这个问题对于大多数的 Pandas 用户并不陌生,那么怎么修改呢?
就像 SettingWithCopyWarning 中提示的那样,使用 df.loc[row_indexer,col_indexer] = value 进行修改,这样不仅可以得到正确的结果,而且也可以解决报警的问题。
代码修改如下:
import pandas as pd data = pd.read_csv('Employees.csv') data.loc[data['DEPT']=='R&D','SALARY']=20000 print(data) |
修改赋值 |
运行结果:
这才是 Pandas 解决问题的方案。
讨论:问题的实质是我们想通过修改视图修改源数据。而 data[data['DEPT']=='R&D']['SALARY']=2000 是将两个索引操作链接在一起,即直接使用了两次方括号的链式索引。
1. data[data['DEPT']=='R&D']
2. ['SALARY']=20000
以上两个链式操作一个接一个地独立执行。第一次链式操作是为了 Get,返回一个 DataFrame,其中包含所有 DEPT 等于 'R&D' 的行;第二次链式操作是为了 Set,是在这个新返回的 DataFrame 上运行的,并没有修改原始的 DataFrame。而此时使用 loc 函数获得原 DataFrame 的视图,在视图上赋值就可以修改原始 DataFrame 的值。
这种问题还是比较容易发现的,下面再来看一种情况:
问题二:修改 R&D 部门 5 号员工的工资为 19950
问题分析:在实际的工作中,经常把视图赋值给某个变量进行后续的计算,直到某一步,又想修改其中的某行的值,此时再使用 loc 函数时也会出现 SettingWithCopyWarning
Python 代码:
import pandas as pd data = pd.read_csv('Employees.csv') r_d = data.loc[data['DEPT']=='R&D'] ''' ... n行代码运算 ... ''' r_d.loc[r_d['EID']==5,'SALARY']=19950 print(r_d) |
获取视图并赋值给变量 r_d 修改 5 号员工的工资 |
运行结果:
SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead
观察发现,即使使用了 loc 函数,当再次使用 loc 函数时,还是会出现 SettingWithCopyWarning 的报警,其中的原因还是将两个索引操作链接在一起,第一次为 get,第二次为 set。这次所不同的是赋值结果起作用了,得到了我们期望的结果。但我们也不应该忽略此 Warning,而是应该明确的告诉 Pandas 变量 r_d 是 data 中截取视图的副本,然后再使用 loc 函数修改 5 号员工的工资。
代码如下:
import pandas as pd data = pd.read_csv('Employees.csv') r_d = data.loc[data['DEPT']=='R&D'].copy() ''' ... n行代码运算 ... ''' r_d.loc[r_d['EID']==5,'SALARY']=19950 print(r_d) |
获取视图并赋值给变量 r_d 修改 5 号员工的工资 |
运行结果:
讨论:var=df.copy() 是明确的告知此 var 是 DataFrame 的副本,此时再使用 loc 函数赋值时,就避免了两次链式索引,也就避免了 SettingWithCopyWarning 的警告。
Pandas 针对 df 的操作冷不防就会产生视图,赋值时会错位,同时也会浪费时间。
集合运算
常见的集合运算,包括交集,差集,并集,异或集和和集运算,下面来看下 Pandas 两个集合间的运算。
问题三:求销售部门的员工与女员工的交集,差集,并集,异或集和和集。
Python 代码:
import pandas as pd data = pd.read_csv('Employees.csv') sales = data.query('DEPT=="Sales"') femals = data.query('GENDER=="F"') isect_idx = sales.index.intersection(femals.index) diff_idx = sales.index.difference(femals.index) union_idx = sales.index.union(femals.index) symmetric_diff_idx = sales.index.symmetric_difference(femals.index) isect = data.loc[isect_idx] diff = data.loc[diff_idx] union = data.loc[union_idx] symmetric_diff = data.loc[symmetric_diff_idx] union_all = pd.concat([sales,femals]) print(isect,diff,union,symmetric_diff,union_all) |
销售部门 女员工 交集索引 差集索引 并集索引 异或集索引 交集 差集 并集 异或集 和集 |
讨论:Pandas 集合运算时,只能对着索引进行,然后再从原始数据中按索引截取得到结果。DataFrame 不可以直接进行集合运算。而且当集合数多于两个时,需要通过循环两两计算得到结果,再从原始数据按索引截取。 当希望按照某列进行集合运算时,则还需要把该列转成索引,计算完成后还要重置索引,得到结果。对于简单的集合运算看起来就很麻烦,如果 Pandas 能支持集合 (set) 数据类型的集合运算,通过符号 (&-|^) 进行运算就好了。
聚合运算
Pandas 提供了很多聚合运算函数,比如求和 sum(),平均 mean(),计数 count(),方差 var(),标准差 std() 等等。但遇到稍微特殊一点聚合运算时就有点麻烦,请看以下两个问题。
问题四:查看所有工资最高的员工的信息
问题分析:首先找到最高工资,再筛选出等于最高工资的员工。
Python 代码:
import pandas as pd data = pd.read_csv('Employees.csv') max_salary = data.SALARY.max() max_salary_emp = data.query('SALARY==%d'%max_salary) print(max_salary_emp) |
计算最高工资 找到最高工资的员工 |
讨论:这种方式需要遍历两边数据,计算最大值时一遍,过滤时一遍,效率比较低。有一种方式可以只遍历一遍。即找最大值的同时记录下最大值员工的索引,然后直接利用索引取数就可以了。可是 Pandas 的 idxmax() 函数只返回一个最大值的索引,不可以返回全部最大值的索引,因此就只能用上边的笨方法来解决这个问题。
问题五:找到年龄最大的 5 位员工,即 TOPN 问题。
问题分析:最大值是相当于 TOP1,因此 TOPN 问题也相当于聚合运算。
Python 代码:
import pandas as pd data = pd.read_csv('Employees.csv') data.sort_values('BIRTHDAY',inplace=True) top_5_age_emp = data.head(5) print(top_5_age_emp) |
排序 取前五 |
讨论:TOPN 问题并不需要大排序,只需要维护一个 N 长度的序列即可,保持序列中的 N 个数总是遍历过的数据中的最大值或者最小值即可。大家都知道大排序的效率是很低的,而且当数据量很大时,大排序复杂度和效率又会进一步恶化。但 Pandas 并没有提供高效的计算函数。即使是 nlargest()和 nsmallest() 函数底层也是大排序后取前五。
定位计算
Pandas 提供了索引功能,用户可以使用索引进行切片等操作,但当遇到需要计算指定索引(即位置)比前一行的行就比较麻烦,如下面这个问题:
问题六:计算股价超过 100 的交易日的当日涨幅
问题分析:需要筛选出股价超过 100 的交易日的交易信息,将数据提前一天,使用相同的索引截取两份数据,计算两者的涨幅。
Python 代码:
import pandas as pd data = pd.read_csv('Stock_Price.txt',sep='\t') gt100 = data.query('CL>=100') gt100_idx = gt100.index gt100_shift1 = data.shift(1).loc[gt100_idx] raise_per = (gt100.CL/gt100_shift1.CL-1).fillna(0) print(raise_per) |
筛选 记录索引 数据提前一天,按索引截取 计算涨幅 |
讨论:Python 并没有提供利用位置进行相关计算的函数,所以计算这类问题就略显麻烦。
分组运算
Pandas 提供了丰富的分组运算,既可以按照列名分组,也可以按照指定的数组分组,既可以对单列使用多种方式聚合,也可以对多列聚合,还可以循环各组,处理分组以后的集合。但有一些常见的分组运算使用 Pandas 做起来要么比较繁琐,要么效率低下。
比如按位置分组、值变化分组、条件变化分组都需要衍生出一个数组作为分组依据,对位分组则需要使用 left join 的方式来绕,枚举分组更是需要多次分组,筛选需要的分组再合并,这里有一篇文章详细介绍了 Pandas 分组运算的一些例子
大家可以通过具体的例子体会 Pandas 分组的不便之处。
并行运算
Pandas 并不提供并行计算的方法,这也是 Pandas 被诟病最多的一方面,而 Python 所谓的多线程对于 CPU 而言还是单线程。
大数据计算
Pandas 虽然可以使用分段读取的方式来获取数据,但想要实现一些复杂的运算,比如排序、分组、关联等等都会非常非常麻烦,而且对程序员的技术要求也会很高。详细论述可以查看另一篇文档。