当前位置: 首页 > 后端技术 > Python

Pandasapply、map、transform介绍及性能测试

时间:2023-03-26 01:10:03 Python

apply函数是我们经常用到的一个Pandas操作。虽然这在较小的数据集上不是问题,但在处理大量数据时,由此产生的性能问题会变得更加明显。虽然apply的灵活性使其成为一个简单的选择,但本文介绍了其他Pandas函数作为潜在的替代品。在本文中,我们将通过一些示例讨论apply、agg、map和transform的预期用途。我们以学生成绩为例df_english=pd.DataFrame({"student":["John","James","Jennifer"],"gender":["male","male","female"],"score":[20,30,30],"subject":"english"})df_math=pd.DataFrame({"student":["John","James","Jennifer"],"gender":["male","male","female"],"score":[90,100,95],"主题":"数学"})df=pd.concat([df_english,df_math],ignore_index=True)mapSeries.map(arg,na_action=None)->Seriesmap方法适用于Series,它根据传递给函数的参数映射每个值。arg可以是一个函数-就像apply可以采用-或字典或系列。na_action是如何处理指定序列的NaN值。当设置为“忽略”时,arg将不会应用于NaN值。例如,当你想使用映射来替换性别的分类表示时:GENDER_ENCODING={"male":0,"female":1}df["gender"].map(GENDER_ENCODING)虽然apply不接受字典,可以做同样的操作。df["gender"].apply(lambdax:GENDER_ENCODING.get(x,np.nan))性能比较在对包含一百万条记录的性别序列进行编码的简单测试中,map比apply快10倍。random_gender_series=pd.Series([random.choice(["male","female"])for_inrange(1_000_000)])random_gender_series.value_counts()""">>>female500094male499906dtype:int64"""看对比results"""地图性能"""%%timeitrandom_gender_series.map(GENDER_ENCODING)#41.4ms±4.24msperloop(mean±std.dev.of7runs,10loopseach)"""applyperformance"""%%timeitrandom_gender_series.apply(lambdax:GENDER_ENCODING.get(x,np.nan))#417ms±5.32msperloop(mean±std.dev.of7runs,1loopeach)因为地图也可以接受函数,所以可以使用任何不依赖于其他元素的转换操作。使用像map(len)或map(upper)这样的东西可以使预处理更容易。applymapDataFrame.applymap(func,na_action=None,**kwargs)->DataFrameapplymap与map非常相似,内部使用apply实现。applymap类似于map,但在DataFrames上按元素工作,但由于它是由apply在内部实现的,因此它不能接受字典或系列作为输入-只允许函数。尝试:df.applymap(dict())exceptTypeError:print("Onlycallablesarevalid!Error:",e)"""Onlycallablesarevalid!Error:thefirstargumentmustbecallable"""na_actionworksandthesame就像在地图上一样。transformDataFrame.transform(func,axis=0,*args,**kwargs)->DataFrame前两个函数在元素级别工作,而transform在列级别工作。我们可以通过转换使用聚合逻辑。假设你想标准化数据:df.groupby("subject")["score"]\.transform(lambdax:(x-x.mean())/x.std())"""0-1.15470110.57735020.5773503-1.00000041.00000050.000000Name:score,dtype:float64我们需要做的是从每组中取出score并将每个元素替换为其归一化值。这肯定无法通过map实现,因为它需要计算列按列计算,而Map只能按元素计算,如果熟悉apply,实现起来就很简单了。df.groupby("subject")["score"]\.apply(lambdax:(x-x.mean())/x.std())"""0-1.15470110.57735020.5773503-1.00000041.00000050.000000name:score,dtype:float64"""不仅本质,代码也基本相同。那么改造的意义何在?Transform必须返回一个与其应用的轴长度相同的DataFrame。也就是说,即使transform与返回聚合值的groupby操作一起使用,它也会将这些聚合值分配给每个元素。例如,假设我们想知道每个班级所有学生的分数总和。我们可以这样使用apply:df.groupby("subject")["score"]\.apply(sum)"""subjectenglish80math285Name:score,dtype:int64"""但我们按subjectScores求和,个别学生和他们的分数之间的关联信息丢失了。对变换做同样的事情,我们得到更有趣的东西:df.groupby("subject")["score"]\.transform(sum)"""080180280328542855285Name:score,dtype:int64"""所以虽然我们对组进行操作,但是我们仍然可以得到组级信息和行级信息之间的关系。因此,任何形式的聚合都会报告错误,如果逻辑未返回转换后的序列,则转换会抛出ValueError。尝试:df["score"].transform("mean")exceptValueErrorase:print("Aggregationdoesn'tworkwithtransform.Error:",e)"""Aggregationdoesn'tworkwithtransform.错误:函数没有transform"""和Apply的灵活性确保它即使在聚合时也能很好地工作。df["score"].apply("mean")"""60.833333333333336"""性能对比在性能上,transform是apply的两倍。random_score_df=pd.DataFrame({"subject":random.choices(["english","math","science","history"],k=1_000_000),"score":random.choices(list(np.arange(1,100)),k=1_000_000)})"""转换性能测试"""%%timeitrandom_score_df.groupby("subject")["score"]\.transform(lambdax:(x-x.mean())/x.std())"""202ms±5.37msperloop(mean±std.dev.of7runs,1loopeach)""""""ApplyPerformanceTest"""%%timeitrandom_score_df.groupby("subject")["score"]\.apply(lambdax:(x-x.mean())/x.std())"""401ms±5.37msperloop(mean±std.dev.of7次运行,每次1个循环)"""aggDataFrame.agg(func=None,axis=0,*args,**kwargs)->标量|pd.系列|pd.DataFrameagg函数更容可以理解,因为它只返回传递给它的数据的聚合,所以无论自定义聚合器如何实现,结果将是传递给它的每一列的单个值。让我们看一个简单的聚合——计算score列上每个组的平均值。df.groupby("subject")["score"].agg(mean_score="mean").round(2)多个聚合器也可以作为列表传递。df.groupby("subject")["score"].agg(["min","mean","max"]).round(2)Agg为执行聚合提供了更多选项。我们还可以构建自定义聚合器并对每一列执行多个特定聚合,例如计算一列的平均值和另一列的中值。性能比较在性能方面,agg比apply稍微快一些,至少对于简单的聚合是这样。random_score_df=pd.DataFrame({"subject":random.choices(["english","math","science","history"],k=1_000_000),"score":random.choices(list(np.arange(1,100)),k=1_000_000)})"""Agg性能测试"""%%timeitrandom_score_df.groupby("subject")["score"].agg("mean")"""74.2ms±5.02ms每个循环(平均±std.dev.7次运行,每次10次循环)“””“””应用性能测试“””%%timeitrandom_score_df.groupby("subject")["score"]。apply(lambdax:x.mean())"""102.3ms±1.16msperloop(mean±std.dev.of7runs,10loopseach)"""你可以看到大约30%的性能提升。在针对多个聚合进行测试时,我们得到类似的结果。"""多个聚合器性能测试与agg"""%%timeitrandom_score_df.groupby("subject")["score"].agg(["min","mean","max"])"""90.5ms每个循环±16.7毫秒(平均±std.dev.7次运行,每次1个循环)].apply(lambdax:pd.Series({"min":x.min(),"mean":x.mean(),"max":x.max()})).unstack()"""104ms±5.78msperloop(mean±std.dev.of7runs,10loopseach)"""apply我们使用它是因为它很灵活。上面的每个示例都可以使用apply来实现,但是这种灵活性是有代价的:它明显变慢,性能测试证明了这一点。apply的一些问题apply的灵活性很好,但是也有一些问题。比如从2014年开始,这个问题就一直困扰着pandas。当整个列中只有一个组时,就会发生这种情况。在这种情况下,即使apply函数预期返回一个Series,它最终也会生成一个DataFrame。结果类似于额外的拆栈操作。我们尝试在这里重现它。我们将使用我们的原始数据框并添加一个城市列。假设我们的三个学生约翰、詹姆斯和詹妮弗都来自波士顿。df_single_group=df.copy()df_single_group["city"]="Boston"让我们计算两组的组均值:一组基于主题列,另一组基于城市。在主题列上分组,我们得到了我们预期的多索引。df_single_group.groupby("subject").apply(lambdax:x["score"])但当我们按城市列分组时,只有一组(对应于“波士顿”),我们得到:df_single_group.groupby("city").apply(lambdax:x["score"])看看结果是怎么旋转的?如果我们堆叠这些,我们会得到预期的结果。df_single_group.groupby("city").apply(lambdax:x["score"]).stack()这个问题在写的时候还没有解决。总结apply提供的灵活性使其在大多数场景下都是非常方便的选择,所以如果你的数据不大,或者对处理时间没有硬性要求,直接使用apply即可。如果实在有时间要求,应该找一个优化的方式来操作,这样可以节省很多时间。本文代码:https://avoid.overfit.cn/post/9917bf07402b473c909248dbb5cdebef作者:AniruddhaKarajgi