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

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

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

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

本篇所涉及知识点:

  1. map、apply、applymap
  2. groupby
    1. MultiIndex DataFrame
    2. 统计函数、累计函数
    3. 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()最主要的用途是实现以下两个功能:

  1. 字典替换
  2. 简单的有参函数(有且仅有一个参数,其实就是Series里的每个元素了)
df = pd.DataFrame([['小王', 'm', 80], ['小红', 'f', 90], ['小明', 'x', 85], [
                  '小林', np.nan, 83], ], columns=['student', 'sex', 'score'])
  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)
  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')
  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)
  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'])
  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')
  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)
  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'])
0 4 9
1 4 9
2 4 9
df.applymap(lambda x: x+2)
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的基本流程如下:

  1. 根据某些标准将数据分成组。
  2. 对每个组独立应用一个函数。
  3. 将结果组合到数据结构中。
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'])
  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可以用的统计函数

FunctionDescription mean()平均值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)
  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