数据处理与统计分析篇-day08-apply()自定义函数与分组操作
一. 自定义函数
概述
-
当Pandas自带的API不能满足需求, 例如: 我们需要遍历的对Series中的每一条数据/DataFrame中的一列或一行数据做相同的自定义处理, 就可以使用Apply自定义函数
-
apply函数可以接收一个自定义函数, 可以将Series对象的逐个值或DataFrame的行/列数据传递给自定义函数处理
-
apply函数类似于编写一个for循环, 遍历行/列的每一个元素,但比使用for循环效率高很多
导包:
import numpy as np import pandas as pd import os os.chdir(r'D:\CodeProject\03data_processing_analysis\teacher_project') # 改变当前的工作目录. change current work directory
apply()操作Series对象
apply()函数操作Series对象, 是把Series的逐个值进行传入并操作的.
# 1. 定义1个df对象. df = pd.DataFrame({'a': [10, 20, 30], 'b': [20, 30, 40]}) df # 2. 定义1个函数, 用于求 值 的 平方. 2 => 4, 5 => 25 def my_fun1(x):print('看看我执行了嘛!')return x ** 2 # 扩展: 定义函数, 计算x的e次方. def my_fun2(x, e):return x ** e # 3. 把上述的函数, 作用于 df对象的 a列值(Series对象) df['a'].apply(my_fun1) # 细节: 这里写的是函数名, 即: 函数对象. 如果写: 函数名() 则表示是在调用函数. df.a.apply(my_fun2, e=3) # 细节: 传参数时, 使用 关键字参数 写法进行传参.
apply()操作DF对象
df的apply(func, axis=)函数, 默认是传入整列值的, 而不是逐个值进行传入的.
源码解释axis参数:0 or index: apply function to each colum1 or columns: apply function to each row
解释:
axis = 0 按列传递数据 传入一列数据(Series)
axis = 1 按行传递数据 传入一列数据(Series)
df df.apply(func1) # 默认axis=0, 代表列, 即: 整列值传入 # 计算平均值,验证默认是传入整列值 def func3(x, y, z):return (x + y + z) / 3 # 报错,apply函数默认将df对象的整列作为参数传入 df.apply(func3) def func4(x):print(x)print(type(x))df.apply(func4) def func5(x):return x.mean() df.apply(func5) # 默认: axis=0(列) df.apply(func5, axis=0) # 效果同上 df.apply(func5, axis=1) # 行 传入
函数向量化
def my_fun6(x, y):# 判断, 如果x的值是20, 就返回NaNif x == 20: # 报错: x是向量, 20是标量, 向量和标量无法直接计算. return np.NAN# for i in x:# if i == 20: # 手动遍历, 就不报错了, 但是结果不是我们要的.# return np.NAN# x代表第1列数据, y代表第2列数据return (x + y) / 2在处理向量和标量时, 无法将向量直接和标量进行比较, 虽然手动用for循环遍历不会报错, 但是结果不对.
此时需要使用np.vectorize()函数, 将自定义函数向量化. 即: 如果遇到了向量, 则会逐个进行遍历, 获取标量并操作.
函数向量化的写法类似于装饰器的写法
# 定义函数, 接收df对象的两列数据, 计算每行的平均值@np.vectorize @np.vectorize def func6(x, y):# 判断, 如果x的值是20, 就返回NaNif x == 2:return np.NAN# x 第一列, y 第二列return (x + y) / 2 func6(df.a, df.b) # 使用np.vectorize()函数, 将自定义函数进行向量化 func6 = np.vectorize(func6) func6(df.a, df.b)
apply()结合lambda表达式
如果需求比较简单, 没有必要重新定义1个新的函数, 可以直接传入Lambda表达式.
# 1. 定义数据集. df = pd.DataFrame({'a': [10, 20, 30], 'b': [20, 30, 40]}) df #%% # 2. 需求: 每个值 => 该值的平方. def my_fun1(x):return x ** 2 df.apply(my_fun1) #%% # 3. 上述的需求可以用 Lambda表达式来完成. df.apply(lambda x : x ** 2) df.apply(lambda x : x.mean()) df.apply(lambda x : x.mean(), axis=0) # 效果同上. df.apply(lambda x : x.mean(), axis=1) # 统计每行的平均值
apply()函数案例
加载数据
# 1. 加载数据集, 获取df对象. train = pd.read_csv('data/titanic_train.csv') train.head() #%% # 2. 查看数据集的 常用统计值. train.info() train.describe() train train.shape # (891, 12) len(train) # 891 行数 train.size # 891 * 12 = 10,772 len(train.Age) # 891 train.Age.size # 891
需求1:计算每列null总数, 缺失值占比, 非缺失值占比
# 1. 定义函数 count_missing(), 计算每列的缺失值总数 def count_missing(col): # col => 每列数据, Series对象return col.isnull().sum() # 2. 定义函数 prop_missing(), 计算每列的缺失值占比. def prop_missing(col):# 缺失值占比 = 缺失值数量 / 该列总长度# return count_missing(col) / len(col)return count_missing(col) / col.size # 3. 定义函数 prop_not_missing(), 计算每列的非缺失值占比. def prop_not_missing(col):# 非缺失值占比 = 1 - 缺失值占比return 1 - prop_missing(col) # 4. 调用上述的函数, 获取结果. train.apply(count_missing) # 获取每列的缺失值总数 train.apply(prop_missing) # 获取每列的缺失值占比 train.apply(prop_not_missing) # 获取每列的非缺失值占比
需求2: 计算泰坦尼克号数据中, 各年龄段总人数
# 方式1: 直接算每个年龄出现了多少次, 即: 每个年龄的总人数, 但是达不到我们要的效果. train.Age.value_counts() # 方式2:解题思路: 把年龄变成年龄段的值, 然后再进行统计. # 1. 定义函数, 接收年龄, 将其转成年龄段. def cut_age(age):if 0 <= age < 18:return '未成年'elif 18 <= age < 40:return '青年'elif 40 <= age < 60:return '壮年'elif 60 <= age < 80:return '老年'else:return '未知'# 2. 把上述的函数, 作用于Age列, 得到新的列, 计算结果即可. train.Age.apply(cut_age) train.Age.apply(cut_age).value_counts()
需求3: 统计VIP 和 非VIP的客户总数
# VIP规则, 乘客船舱等级为1, 或者 名字中带有: 'Master', 'Sir', 'Dr' def is_vip(rows):if rows.Pclass == 1 and ('Master' in rows.Name or 'Sir' in rows.Name or 'Dr' in rows.Name):return 'vip'else:return 'not_vip' train.apply(is_vip, axis=1).value_counts()
二. 分组操作
分组 + 聚合
概述
-
在SQL中我们经常使用 GROUP BY 将某个字段,按不同的取值进行分组,
-
在pandas中也有groupby函数, 分组之后,每组都会有至少1条数据, 将这些数据进一步处理,
-
返回单个值的过程就是聚合,比如分组之后计算算术平均值, 或者分组之后计算频数,都属于聚
代码演示
导入数据
# 1. 读取数据, 获取df对象 df = pd.read_csv('data/gapminder.tsv', sep='\t') df.head()
单变量
# 统计每年平均寿命 # 写法1 df.groupby('year')['lifeExp'].mean() # 写法2 df.groupby('year').lifeExp.mean() # 上述都是一步到位, 直接计算结果, 我们也可以手动计算. # 1. 我们先看看一共有多少个年 df.year.unique() # 12个年份, 底层算 12 次即可, 这里我们就用 1952年举例. # 2. 获取1952年所有的数据, 计算平均寿命 df[df['year'] == 1952].lifeExp.mean() df[df.year == 1952].lifeExp.mean() # 效果同上. # 统计各大洲平均寿命 # 写法1 df.groupby('continent')['lifeExp'].mean() # 分组之后, 也可以用 describe()同时计算多个统计量. df.groupby('continent')['lifeExp'].describe() # 写法2 df.groupby('continent')['lifeExp'].mean() df.groupby('continent')['lifeExp'].agg('mean') # 这里的mean是: pandas的函数 # df.groupby('continent')['lifeExp'].agg(np.mean) # 这里的mean是: Numpy的函数 df.groupby('continent')['lifeExp'].aggregate('mean') # 效果同上.
多变量agg
# 需求: 统计各个大洲 平均寿命, 人口的中位数, 最大GDP df.groupby('continent').agg({'lifeExp': 'mean', 'pop': 'median', 'gdpPercap': 'max'}) df.groupby('continent').aggregate({'lifeExp': 'mean', 'pop': 'median', 'gdpPercap': 'max'}) # 效果同上 # 语法糖, 如果聚合函数一样, 则可以简写成如下操作, 例如: 各个大洲平均寿命, 平均人口, 平均GDP df.groupby('continent').agg({'lifeExp': 'mean', 'pop': 'mean', 'gdpPercap': 'mean'}) df.groupby('continent')[['lifeExp', 'pop', 'gdpPercap']].mean()
自定义函数聚合运算
# 需求: 计算各个大洲的平均寿命 # 方式1: 使用Pandas的mean()函数. df.groupby('continent').lifeExp.mean() df.groupby('continent').lifeExp.agg('mean') # 方式2: 使用自定义函数, 计算平均值. # 1. 定义函数, 计算某列的平均值. def my_mean(col):# 某列平均值 = 该列元素和 / 该列元素个数# return col.sum() / len(col)return col.sum() / col.size # 2. 调用函数. df.groupby('continent').lifeExp.apply(my_mean) df.groupby('continent').lifeExp.agg(my_mean)
分组 + 转换
概述
-
transform 需要把DataFrame中的值传递给一个函数, 而后由该函数"转换"数据。
-
即: aggregate(聚合) 返回单个聚合值,但transform 不会减少数据量。
-
分组转换跟SQL中的窗口函数中的聚合函数作用一样。可以把每一条数据和这个数据所属的组的一个聚合值在放在一起, 可以根据需求进行相应计算。
代码演示
计算x的z-score分数
计算x的 z-score分数, 也叫: 标准分数, 公式为: (x - x_mean) / x_std
# 1. 查看数据源 df #%% # 2. 定义函数, 计算某列的 z-score分数. def my_zscore(col):return (col - col.mean()) / col.std() # (列值 - 平均值) / 标准差 # 3. 调用上述的格式. df.groupby('year').lifeExp.apply(my_zscore) # 1704条 #%% # 4. 查看原始df的数据集总数. df # 结论: 分组 + 转换处理后, 数据集总数不变.
分组填充
# 需求: 读取文件(小票信息), 获取df对象. 其中有1列 total_bill 表示总消费. 随机抽取4个缺失值, 然后进行填充. # 填充方式: 每个组的平均值. 即: 如果是Male => 就用 Male列的平均值填充, 如果是Female => Female列的平均值填充. # 1. 读取文件, 获取DataFrame对象 df = pd.read_csv('data/tips.csv') df #%% # 2. 抽样方式, 从上述的df对象中, 随机抽取10条数据. # tips_10 = df.sample(10) # 这里的10表示随机抽取 10 条数据. # random_state: 随机种子, 只要种子一样, 每次抽取的数值都是一样的. tips_10 = df.sample(10, random_state=21) tips_10 #%% # 3. 随机的从上述的10条数据中, 抽取4行数据, 设置他们的 total_bill(消费总金额) 为 NaN # 写法1: 每次固定 这四条数据 的 total_bill为 空值. # tips_10.loc[[173, 240, 243, 175], 'total_bill'] = np.NaN # 写法2: 每次随机4条数据, 设置它们的 total_bill为 空值. # np.random.permutation()解释: 随机打乱索引值, 并返回打乱后的索引值. # np.random.permutation()[索引数] 打乱索引顺序, 返回固定索引数 tips_10.loc[np.random.permutation(tips_10.index)[:4], 'total_bill'] = np.NaN tips_10 #%% # 4. 分别计算 Male 和 Female 的平均消费金额, 用于填充对应组的 缺失值. # 思路1: 直接用 整体的 总消费金额的 平均值 填充. tips_10.fillna(tips_10.total_bill.mean()) #%% # 思路2: 自定义函数, 计算每组的平均消费金额, 进行填充 def my_mean(col):# return col.sum() / col.size # 某列总金额 / 某列元素个数, 这种写法会导致: 本组所有的数据都会被新值覆盖.return col.fillna(col.mean()) # 用该列的平均值, 来填充该列的缺失值, 其它不变. # 调用上述函数, 实现: 分组填充, 即: 给我N条, 处理后, 还是返回N条数据. # tips_10.groupby('sex').total_bill.apply(my_mean) # n => 1 聚合的效果. tips_10.groupby('sex').total_bill.transform(my_mean) # n => n 类似于: MySQL的窗口函数的效果. # df.groupby('sex').total_bill.transform(my_mean) # n => n 类似于: MySQL的窗口函数的效果.
分组 + 过滤
概述
-
使用groupby方法还可以过滤数据
-
调用filter 方法,传入一个返回布尔值的函数,返回False的数据会被过滤掉
代码演示
# 1. 查看源数据 df #%% # 2. 查看用餐人数情况. tmp_df = df.groupby('size', as_index=False).total_bill.count() tmp_df.columns = ['size', 'count'] tmp_df df.size # 这样写, 会把 size当做 属性, 而不是 size列. df['size'].value_counts() #%% # 3. 我们发现, 在所有的 消费记录中, 就餐人数 在 1, 5, 6个人的消费次数相对较少, 我们可以过滤掉这部分的数据 tmp_df = df.groupby('size').filter(lambda x : x['size'].count() > 30) tmp_df #%% # 4. 验证上述筛选后的数据, size列只有 2, 3, 4 这三种就餐人数的情况. tmp_df['size'].value_counts() #%% # 5. 上述代码的合并版, 一行搞定. df.groupby('size').filter(lambda x : x['size'].count() > 30)['size'].value_counts() # 另外一种筛选的方式, 可以基于: query()函数 + 筛选条件, 找出要的合法的数据. df.query('size == 2 or size == 3 or size == 4') df.query('size in [2, 3, 4]')
DataFrameGroupby对象
概述
调用了groupby方法之后, 就会返回一个DataFrameGroupby对象
代码演示
# 1. 从小费数据中, 随机的获取10条数据. tips_10 = pd.read_csv('data/tips.csv').sample(10, random_state=21) tips_10 #%% # 2. 演示 根据性别分组, 获取: 分组对象. grouped = tips_10.groupby('sex') # DataFrameGroupBy 对象 grouped #%% # 3. 遍历上述的分组对象, 看看每个分组都是啥(即: 每个分组的数据) for sex_group in grouped:print(sex_group) # sex_group: 就是具体的每个分组的数据. #%% # 4. 获取指定的某个分组的数据. grouped.get_group('Male') grouped.get_group('Female') #%% # 5. 需求: 使用groupby() 按 性别 和 用餐时间分组, 计算小费数据的平均值. df.groupby(['sex', 'time']).tip.mean() #%% # 6. 分组对象不能使用 0 索引获取数据 grouped # grouped[0] # 分组对象不能使用 0 索引获取数据, 要获取数据, 可以通过 grouped.get_group() 函数实现 grouped.get_group(('Male'))