一分钟一个 Pandas 小技巧(二)

一分钟一个 Pandas 小技巧(二)

在逛 Kaggle 的时候发现了一篇不错的 Pandas 技巧,我将挑选一些有用的并外加一些自己的想法分享给大家。 本系列虽基础但带仍有一些奇怪操作,粗略扫一遍,您或将发现一些您需要的技巧。

纸上得来终觉浅,绝知此事要躬行,所谓的熟练使用 Pandas 是建立在您大致了解每个函数功能上,希望本系列能给您带来些许收获。

本篇所涉及知识点:

map、apply、applymap
groupby
MultiIndex DataFrame
统计函数、累计函数
agg、transform、filter
map、apply 和 applymap
先总结一下:

map() 适用于 Series,相当于将 Series 中的每个元素作为参数带入指定函数,注意是 Series 中的每个元素。
apply() 适用于 DataFrame 的行或者列,将整个行列作为参数带入指定函数,注意是 DataFrame 的行或列,即整个 Series。
applymap() 适用于 DataFrame 中的每个元素,注意是 DataFrame 中的每个元素。
map
Series.map(self,arg,na_action) 作用于一个 Series 上,DataFrame 的一行或者一列都是 Series。

map() 最主要的用途是实现以下两个功能:

字典替换
简单的有参函数(有且仅有一个参数,其实就是 Series 里的每个元素了)
df = pd.DataFrame([[‘小王’, ‘m’, 80], [‘小红’, ‘f’, 90], [‘小明’, ‘x’, 85], [
‘小林’, np.nan, 83], ], columns=[‘student’, ‘sex’, ‘score’])
df

student sex score
0 小王 m 80
1 小红 f 90
2 小明 x 85
3 小林 NaN 83
使用 dict 对 Series 进行映射替换。如果 Series 中的值并没有在给定字典中出现,那该值会被替换为 np.nan。

下面案例中我想把 sex=’m’替换为 "男",sex=’f’替换为 "女",小明的 sex=’x’我并没有在字典中给出映射,所以 map() 后直接被替换为 np.nan。

my_dict = {‘m’: ‘男’, ‘f’: ‘女’}
df[‘dict’] = df[‘sex’].map(my_dict)
df

student sex score dict
0 小王 m 80 男
1 小红 f 90 女
2 小明 x 85 NaN
3 小林 NaN 83 NaN
如果对于未给定的键我们也想统一替换,那就要在字典中设置默认值,接下来教大家如何设置字典默认值。

from collections import defaultdict

def ret():
"""
describe: 因为 defaultdict 接受一个工厂函数,所以咱们直接创建一个函数,返回 Unknown
return :str
"""
return ‘Unknown’

生成一个新的 dict,使用 defaultdict 函数并传入我们创建的工厂函数

当然你也可以填入其他的数据类型,具体类型及对应默认值如下:

int->0,set->set(),str->'',list->[]

my_hasdefault_dict = defaultdict(ret)

将之前的 dict 再传入新的含有默认值的 dict

my_dict = {‘m’: ‘男’, ‘f’: ‘女’}
for k, v in my_dict.items():
my_hasdefault_dict[k] = v

因为小林的 sex=np.nan 没有在给定字典中出现,所以也替换成了默认值

df[‘hasdefault_dict’] = df[‘sex’].map(my_hasdefault_dict)

在 map 中设置 na_action=’ignore’时就会忽略 np.nan,不进行任何操作

df[‘ignore_na’] = df[‘sex’].map(my_hasdefault_dict, na_action=‘ignore’)
df

student sex score dict hasdefault_dict ignore_na
0 小王 m 80 男 男 男
1 小红 f 90 女 女 女
2 小明 x 85 NaN Unknown Unknown
3 小林 NaN 83 NaN Unknown NaN
map() 可以应用一些简单的自定义函数。

def my_cut(x):
"""
descrieb: 对成绩分桶
return :str
"""
if x >= 90:
return ‘score>=90’
if 85 <= x < 90:
return ‘85<=score<90’
else:
return ‘score<85’

map 里面只要写入函数名就可以,不用括号

df[‘my_cut’] = df[‘score’].map(my_cut)

上面的判断比较简单,所以我们可以改写成 lambda 的形式

lambda 的格式是 lambda x: 结果 1 if 条件 1 else 结果 2 if 条件 2 else 结果 3

df[‘cut_lambda’] = df[‘score’].map(
lambda x: ‘score>=90’ if x >= 90 else ‘85<=score<90’ if 85 <= x < 90 else ‘score<85’)

当然上面只是为了演示 map,其实用 Pandas 自带的 cut 更方便

right=True 是右闭区间,right=False 是左闭区间

df[‘cut’] = pd.cut(df[‘score’], bins=[-np.inf, 85, 90, np.inf],
labels=[‘score<85’, ‘85<=score<90’, ‘score>=90’], right=False)
df

student sex score my_cut cut_lambda cut
0 小王 m 80 score<85 score<85 score<85
1 小红 f 90 score>=90 score>=90 score>=90
2 小明 x 85 85<=score<90 85<=score<90 85<=score<90
3 小林 NaN 83 score<85 score<85 score<85
apply
DataFrame.apply(self, func, axis=0, raw=False, result_type=None, args=(), **kwds)
Series.apply(self, func, convert_dtype=True, args=(), **kwds)
GroupBy.apply(self, func, args, *kwargs)
… apply()有好多,我们最主要的就是用到上面这三种结构下的 apply()
apply()不可以像 map() 那样使用字典映射替换字符,但是可以通过字典调用函数(起码我还没用到过)。

df = pd.DataFrame([[‘小王’, ‘m’, 80, ‘汉族’], [‘小红’, ‘f’, 90, ‘回族’], [‘小明’, ‘m’, 85, ‘汉族’], [
‘小林’, ‘f’, 83, ‘保安族’], ], columns=[‘student’, ‘sex’, ‘score’, ‘nationality’])
df

student sex score nationality
0 小王 m 80 汉族
1 小红 f 90 回族
2 小明 m 85 汉族
3 小林 f 83 保安族
为了证明 apply 是真的可以字典调用函数,我们来做个没有实际意义的测试,结果是可行的。

如果 sex=m 则后面拼接 ale,如果 sex=f 则后面拼接 emale

func_dict = {‘m’: lambda x: x+‘ale’, ‘f’: lambda x: x+‘emale’}
df[‘sex’].apply(func_dict)

m 0 male
1 fale
2 male
3 fale
f 0 memale
1 female
2 memale
3 female
是不是感觉和 map 出来的效果有点不一样?继续往下看。

apply 可以添加参数,如果用 *args 接收参数,那就使用元组的方式按位置索引,**kwargs 就按照字典方式按 key 索引

def birth(x, **kwargs):
return f’{x} 身高 {kwargs[“height”]}cm’

调用随机函数,指定相同的范围

df[‘apply_birth’] = df[‘student’].apply(
birth, height=np.random.randint(160, 180))
df[‘map_birth’] = df[‘student’].map(
lambda x: f’{x} 身高 {np.random.randint(160,180)}cm’)
df

student sex score nationality apply_birth map_birth
0 小王 m 80 汉族 小王身高 164cm 小王身高 171cm
1 小红 f 90 回族 小红身高 164cm 小红身高 178cm
2 小明 m 85 汉族 小明身高 164cm 小明身高 169cm
3 小林 f 83 保安族 小林身高 164cm 小林身高 175cm
我们来对比一下 map()和 apply(),同样的想法却出现不一样的效果。apply()出来的身高都是一样的,而 map() 出来的身高是不一样的。所以就如之前的总结所说一般,map()作用于 Series 中的每个元素,apply() 作用于整个 Series。

我们看一下 apply() 是如何作用于多行 / 列的,下面案例中我们调取多列来实现对少数民族同学加 5 分的操作。

多列选择后使用 apply,可以使用位置索引选择列,比如这里 x[1] 代表的是 df[‘score’]

df[‘加分后成绩’] = df’nationality’, ’score’.apply(
lambda x: x[1]+5 if x[0] != ‘汉族’ else x[1], axis=1)
df

student sex score nationality 加分后成绩
0 小王 m 80 汉族 80
1 小红 f 90 回族 95
2 小明 m 85 汉族 85
3 小林 f 83 保安族 88
上面案例中为什么要用 axis=1 呢,下面的例子,请各位意会一下。

axis=0 是对行计算,返回的行数与原表列索引数相等。
axis=1 是对列进行计算,返回的行数应该与原表行索引数相等。
applymap
applymap() 是 DataFrame 的方法,只能作用于整个 DataFrame。

df = pd.DataFrame(4, 9 * 3, columns=[‘A’, ‘B’])
df

A B
0 4 9
1 4 9
2 4 9

df.applymap(lambda x: x+2)

A B
0 6 11
1 6 11
2 6 11

错误示范

df[‘A’].applymap(lambda x: x+2)

AttributeError: ‘Series’ object has no attribute ‘applymap’
不得不会的 groupby
敲黑板!pandas 中相当重要的一环!

DataFrame.groupby(self, by=None, axis=0, level=None, as_index: bool = True, sort: bool = True, group_keys: bool = True, squeeze: bool = False, observed: bool = False)

返回的是 DataFrameGroupBy 对象,groupby 的基本流程如下:

根据某些标准将数据分成组。
对每个组独立应用一个函数。
将结果组合到数据结构中。
import random

以下所有案例皆基于这个 DataFrame

cat = [‘product1’, ‘product2’, ‘product3’]
df = pd.DataFrame(list(zip(pd.date_range(start=‘2020/1/1’,end=‘2020/1/10’),random.choices(‘ABC’, k=10), random.choices(cat, k=10), random.choices(
range(1, 100), k=10))), columns=[‘date’,‘salesman’, ‘product’, ‘volume’])
df

date salesman product volume
0 2020-01-01 C product3 49
1 2020-01-02 C product3 30
2 2020-01-03 B product1 35
3 2020-01-04 A product3 63
4 2020-01-05 B product2 26
5 2020-01-06 A product3 37
6 2020-01-07 B product1 50
7 2020-01-08 C product2 16
8 2020-01-09 A product2 45
9 2020-01-10 B product3 22
看到 ABC,偷偷教大家一个 Python 快速打出 26 个英文字母的方法。
from string import ascii_uppercase # 26 个大写字母
from string import ascii_lowercase # 26 个小写字母
from string import ascii_uppercase # 52 个字母,前 26 小写,后 26 大写
只要直接 print(ascii_uppercase/ascii_lowercase/ascii_uppercase) 就可以了。
我们想看一下销售员的总销量,相当于 SQL 中的 group by。

df.groupby(by=‘salesman’).sum()

     volume

salesman
A 145
B 133
C 95
再看一下各销售员每类产品的总销量。

groupby 可以根据多个标签分组,但是注意,默认情况下多标签分组后获得的 DataFrameGroupBy 是含有 MultiIndex 的。

默认情况下 sort=True,Pandas 会将分组标签自动排序,也可以设置为 False,加快 groupby 速度,但大多数情况还是让它排序吧

df_mulitindex = df.groupby(by=[‘salesman’,‘product’],sort=True).sum()
df_mulitindex

                 volume

salesman product
A product2 45
product3 100
B product1 85
product2 26
product3 22
C product2 16
product3 79
即使是 MultiIndex DataFrame 也一样可以 groupby 的,你可以用 level 也可以用 by 来控制分组的标签。

如果原 DataFrame 是普通索引,那只能设置 by 参数
如果是 MultiIndex DataFrame,那 by 和 level 都可以设置,但是更推荐用 level,别问为什么,问就是不知道
df_mulitindex.groupby(level=‘product’,sort=False).sum()

     volume

product
product2 87
product3 201
product1 85
我想先不进行聚合运算,看看分好组的样子可以吗?接下来几个函数可以帮到你。

df.groupby().first() 返回每个分组的第一行
df.groupby().last() 返回每个分组的最后一行
df.groupby().nth(n) 返回每个分组的第 n 行
df.groupby().head(n) 返回每个分组前 n 行
df.groupby().tail(n) 返回每个分组后 n 行
groupby 的第一步操作,是按照指定字段分组,所以首先我将原 DataFrame 对列索引进行排序,方便大家理解。

ascending=False 降序 /True 升序

df.sort_values(by=[‘salesman’,‘product’,‘volume’],ascending=[True,True,False])

上述函数我就不一一展示,就举一例子,查询各业务员各类产品销量最高的那行数据。

df.sort_values(by=‘volume’,ascending=False).groupby(by=[‘salesman’,‘product’]).first()

讲一讲 MultiIndex
多了几个标签作为行索引而已,跟普通的 Index 没什么的,只是索引方式有那么一点点的小不同,看下去就知道。

agg 不知道是什么不要紧,下面会讲

df_multi =df.groupby(by=[‘salesman’,‘product’]).agg(vol_sum=(‘volume’,‘sum’),vol_max=(‘volume’,‘max’))
df_multi

              vol_sum   vol_max

salesman product
A product2 45 45
product3 100 63
B product1 85 50
product2 26 26
product3 22 22
C product2 16 16
product3 79 49
还记得 loc 吗!标签索引,忘记了回头看一下第一篇。

最基本的索引其实就是加上括号,把元组作为索引条件

df_multi.loc[(‘B’,‘product3’)]# 这样返回的是 Series

vol_sum 32
vol_max 32
Name: (B, product3), dtype: int64

df_multi.loc(’B’,’product3’)# 这样返回的是 DataFrame

              vol_sum vol_max

salesman product
B product3 32 32

切片,一样的

df_multi.loc[(‘B’,‘product3’):(‘C’,‘product3’),‘vol_max’] # 这样返回的是 Series

salesman product
B product3 32
C product2 96
product3 72
Name: vol_max, dtype: int64

df_multi.loc[(‘B’,‘product3’):(‘C’,‘product3’),[‘vol_max’]] # 这样返回的是 DataFrame

                 vol_max

salesman product
B product3 32
C product2 96
product3 72
MultiIndex DataFrame 怎么变为一维表?

分组时加入 as_index=False 使返回的 DataFrame 直接变为一维表
或使用 reset_index() 将含有 MultiIndex 的 DataFrame 变为一维表
df.groupby(by=[‘salesman’,‘product’],as_index=False).sum()
df.groupby(by=[‘salesman’,‘product’]).sum().reset_index()

出来的效果都是这样

salesman product volume
0 A product2 45
1 A product3 100
2 B product1 85
3 B product2 26
4 B product3 22
5 C product2 16
6 C product3 79
MultiIndex DataFrame 怎么变成二维表?

unstack()
df.groupby(by=[‘salesman’,‘product’]).sum().unstack()

      volume

product product1 product2 product3
salesman
A NaN 45.0 100.0
B 85.0 26.0 22.0
C NaN 16.0 79.0
DataFrameGroupBy 可以用的统计函数
FunctionDescriptionmean()平均值 sum() 求和 size()返回 group 的大小 count() 计数 std()标准差 var() 方差 sem()标准误 describe() 超级方便的数据表述函数,包括了很多常见统计量 first()组中的第一个值 last() 组中的最后一个值 nth()取 n 个值 min() 最小值 max() 最大值

如果确定要对某一列进行聚合运算,强烈建议先指出该列再使用聚合函数,可以提高效率。

df.groupby(by=‘salesman’).describe()

     volume
      count mean        std       min   25%   50%  75%   max

salesman
A 1.0 3.000000 NaN 3.0 3.00 3.0 3.0 3.0
B 6.0 46.166667 28.596620 2.0 32.75 47.5 66.0 80.0
C 3.0 77.666667 16.258331 65.0 68.50 72.0 84.0 96.0
累计函数
累计函数,相信大家看一下案例就能理解了!

方法名函数功能 cumsum()依次给出前 1、2、… 、n 个数的和 cumprod() 依次给出前 1、2、… 、n 个数的积 cummax()依次给出前 1、2、… 、n 个数的最大值 cummin() 依次给出前 1、2、… 、n 个数的最小值

d = {
‘salesperson’: [
‘Nico’,
‘Carlos’,
‘Juan’,
‘Nico’,
‘Nico’,
‘Juan’,
‘Maria’,
‘Carlos’,
],
‘item’: [‘Car’, ‘Truck’, ‘Car’, ‘Truck’, ‘cAr’, ‘Car’, ‘Truck’, ‘Moto’],
}
df = pd.DataFrame(d)
df

salesperson item
0 Nico Car
1 Carlos Truck
2 Juan Car
3 Nico Truck
4 Nico cAr
5 Juan Car
6 Maria Truck
7 Carlos Moto

df[‘count_by_person’] = df.groupby(by=‘salesperson’).cumcount() + 1
df[‘count_by_item’] = df.groupby(by=‘item’).cumcount() + 1
df[‘count_by_both’] = df.groupby(by=[‘salesperson’, ‘item’]).cumcount() + 1
df

agg、transform、filter
agg
agg()是 aggregate() 的别称,就像 isna()是 isnull() 的别称一样。

agg() 主要用在 DataFrameGroupBy 上,当然也可以用在 DataFrame 上,是用来在多列上计算不同函数的。

用法一

直接使用列表列出所要使用的统计函数,具体可用函数已经在上面表中列出

可以用字符串形式表示函数,也可以直接打函数名不加引号,如果报错就打 np. 函数名 (毕竟 pandas 是基于 numpy)

df.groupby(by=‘salesman’)[‘volume’].agg([‘sum’,‘max’])

      sum   max

salesman
A 145 63
B 133 50
C 95 49

用法二

可以自定义列名,格式是 agg(name=(column,func))

df.groupby(by=‘salesman’).agg(vol_max=(‘volume’,‘max’),vol_sum=(‘volume’,‘sum’))

      vol_max  vol_sum

salesman
A 63 145
B 50 133
C 49 95

方法三

您细细品一下效果

df.groupby(by=‘salesman’).agg({‘volume’:[‘max’,‘mean’],‘date’:[‘min’,‘max’]})

      volume         date
      max  mean      min        max

salesman
A 63 48.333333 2020-01-04 2020-01-09
B 50 33.250000 2020-01-03 2020-01-10
C 49 31.666667 2020-01-01 2020-01-08
当然,agg() 也可以自定义函数。

每个人的销量乘以 100 再求和

df.groupby(by=‘salesman’).agg({“volume”:lambda x:sum([i*100 for i in x])})

  volume

sales man
A 14500
B 13300
C 9500
agg 也是可以用在 DataFrame 或 Series 上,但个人感觉没样啥用,还不如直接用 apply。

df’volume’.agg([‘max’,‘min’])

volume

max 63
min 16
transform
转换,有时候还是有点用的,我们看一下实例。

我们重新创建一个 DataFrame,适用于 transform 和 filter 的

df_temp = pd.DataFrame([[‘2020/1/1’, ‘A’, 100], [‘2020/2/1’, ‘A’, 50], [‘2020/3/1’, ‘A’, np.nan], [‘2020/1/1’,
‘B’, 1000], [‘2020/2/1’, ‘B’, 3000], [‘2020/3/1’, ‘B’, 5000]], columns=[‘date’, ‘salesman’, ‘volume’])
df_temp

date salesman volume
0 2020/1/1 A 100.0
1 2020/2/1 A 50.0
2 2020/3/1 A NaN
3 2020/1/1 B 1000.0
4 2020/2/1 B 3000.0
5 2020/3/1 B 5000.0
我们可以看到 A 业务员 2020/3/1 的销量为 np.nan,没有记录,如果我们需要对数据进行清洗该怎么办呢?

直接删除吗,这不是最佳的选项。如果要填充,直接用整列的 volume 的平均值?那这样对 B 业务员是不是很吃亏?

所以我们可以尝试用 A 的均值来填充她的缺失值。

df_temp.volume = df_temp.groupby(by=‘salesman’)[‘volume’].transform(lambda x:x.fillna(x.mean()))
df_temp

date salesman volume
0 2020/1/1 A 100.0
1 2020/2/1 A 50.0
2 2020/3/1 A 75.0
3 2020/1/1 B 1000.0
4 2020/2/1 B 3000.0
5 2020/3/1 B 5000.0
filter
看名字就知道是过滤,可以自定义函数。

def filter_loser(x):
"""
describe: 筛选掉总销售量小于 300 的业务员
return
"""
return x[‘volume’].sum()>=300

df_temp.groupby(by=‘salesman’).filter(filter_loser)

date salesman volume
3 2020/1/1 B 1000.0
4 2020/2/1 B 3000.0
5 2020/3/1 B 5000.0
DataFrameGroupBy 是可以使用 apply() 的,用法如之前所述一致。