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 分组运算的一些例子

Python 分组处理

大家可以通过具体的例子体会 Pandas 分组的不便之处。

并行运算

Pandas 并不提供并行计算的方法,这也是 Pandas 被诟病最多的一方面,而 Python 所谓的多线程对于 CPU 而言还是单线程。

 

大数据计算

Pandas 虽然可以使用分段读取的方式来获取数据,但想要实现一些复杂的运算,比如排序、分组、关联等等都会非常非常麻烦,而且对程序员的技术要求也会很高。详细论述可以查看另一篇文档。

Python 如何处理大文件