明 编译整理
量子位 报道 | 公众号 QbitAI
用Python和Pandas进行数据分析,很快就会用到循环。
但在这其中,就算是较小的DataFrame,使用标准循环也比较耗时。
遇到较大的DataFrame时,需要的时间会更长,会让人更加头疼。
现在,有人忍不了了。他是一位来自德国的数据分析师,名叫Benedikt Droste。
他说,当自己花了大半个小时等待代码执行的时候,决定寻找速度更快的替代方案。
在给出的替代方案中,使用Numpy向量化,与使用标准循环相比,速度提升了71803倍。
他是怎么实现的?我们一起来看看~
标准循环处理3年足球赛数据:20.7秒
DataFrame是具有行和列的Pandas对象。如果使用循环,需要遍历整个对象。
Python不能利用任何内置函数,而且速度很慢。在Benedikt Droste的提供的示例中,是一个包含65列和1140行的Dataframe,包含了2016-2019赛季的足球赛结果。
需要解决的问题是:创建一个新的列,用于指示某个特定的队是否打了平局。可以这样开始:
def soc_loop(leaguedf,TEAM,): leaguedf['Draws']=99999 for row in range(0, len(leaguedf)): if ((leaguedf['HomeTeam'].iloc[row]==TEAM) & (leaguedf['FTR'].iloc[row]=='D')) | \ ((leaguedf['AwayTeam'].iloc[row]==TEAM) & (leaguedf['FTR'].iloc[row]=='D')): leaguedf['Draws'].iloc[row]='Draw' elif ((leaguedf['HomeTeam'].iloc[row]==TEAM) & (leaguedf['FTR'].iloc[row] !='D')) | \ ((leaguedf['AwayTeam'].iloc[row]==TEAM) & (leaguedf['FTR'].iloc[row] !='D')): leaguedf['Draws'].iloc[row]='No_Draw' else: leaguedf['Draws'].iloc[row]='No_Game'
在这个案例中是阿森纳,在实现目标之前要确认阿森纳参加了哪些场比赛,是主队还是客队。但使用标准循环非常慢,执行时间为20.7秒。
那么,怎么才能更有效率?
Pandas 内置函数: iterrows ()ー快321倍
在第一个示例中,循环遍历了整个DataFrame。iterrows()为每一行返回一个Series,它以索引对的形式遍历DataFrame,以Series的形式遍历感兴趣的列。这使得它比标准循环更快:
def soc_iter(TEAM,home,away,ftr): #team, row['HomeTeam'], row['AwayTeam'], row['FTR'] if [((home==TEAM) & (ftr=='D')) | ((away==TEAM) & (ftr=='D'))]: result='Draw' elif [((home==TEAM) & (ftr !='D')) | ((away==TEAM) & (ftr !='D'))]: result='No_Draw' else: result='No_Game' return result
代码运行时间为68毫秒,比标准循环快321倍。但是,许多人建议不要使用它,因为仍然有更快的选项,而且iterrows()不能跨行保存dtype。
这意味着,如果你在DataFrame dtypes上使用iterrows(),可以更改它,但这会导致很多问题。
一定要保存dtypes的话,你还可以使用itertuples()。这里我们不详细讨论 ,你可以在这里找到官方文件:
https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.itertuples.html
apply ()方法ー快811倍
apply 本身并不快,但与DataFrame结合使用时,它具有优势。这取决于 apply 表达式的内容。如果可以在 Cython 空间中执行,那么apply要快得多,这里的示例就是这种情况。
大家可以在Lambda函数中使用apply。所要做的就是指定这个轴。在本文的示例中,想要执行按列操作,要使用 axis 1:
这段代码甚至比之前的方法更快,完成时间为27毫秒。
Pandas向量化—快9280倍
此外,也可以利用向量化的优点来创建非常快的代码。
重点是避免像之前的示例中的Python级循环,并使用优化后的C语言代码,这将更有效地使用内存。只需要稍微修改一下函数:
def soc_iter(TEAM,home,away,ftr): df['Draws']='No_Game' df.loc[((home==TEAM) & (ftr=='D')) | ((away==TEAM) & (ftr=='D')), 'Draws']='Draw' df.loc[((home==TEAM) & (ftr !='D')) | ((away==TEAM) & (ftr !='D')), 'Draws']='No_Draw'
现在,可以用 Pandas 列作为输入创建新列:
在这种情况下,甚至不需要循环。所要做的就是调整函数的内容。现可以直接将Pandas 列传递给函数,从而获得巨大的速度增益。
Numpy向量化—快71803倍
在上面的示例中,将将Pandas 列传递给函数。通过添加.values,可以得到一个Numpy数组:
因为引用了局部性的好处,Numpy数组的速度非常快,代码运行时间仅为0.305毫秒,比一开始使用的标准循环快71803倍。
谁更强一目了然
最后,Benedikt Droste对上述方案进行了总结。
他说,如果你使用Python、Pandas和Numpy进行数据分析,总会有改进代码的空间。
在对上述五种方法进行比较之后,哪个更快一目了然:
从这个图中,可以得出两个结论:
原文链接:
https://towardsdatascience.com/how-to-make-your-pandas-loop-71-803-times-faster-805030df4f06
— 完 —
诚挚招聘
量子位正在招募编辑/记者,工作地点在北京中关村。期待有才气、有热情的同学加入我们!相关细节,请在量子位公众号(QbitAI)对话界面,回复“招聘”两个字。
量子位 QbitAI · 头条号签约作者
?'?' ? 追踪AI技术和产品新动态
读:Pandas是一个基于Numpy库开发的更高级的结构化数据分析工具,提供了Series、DataFrame、Panel等数据结构,可以很方便地对序列、截面数据(二维表)、面板数据进行处理。
作者:张秋剑 张浩 周大川 常国珍
来源:华章科技
我们可通过限定时间ID和样本ID获得对应的Series和DataFrame。
由于这些对象的常用操作方法十分相似,因此本文主要使用DataFrame进行演示。
Pandas库提供了便捷读取本地结构化数据的方法。这里主要以csv数据为例,read_csv函数可以读取csv数据,代码如下:
import pandas as pd
csv=pd.read_csv('data/sample.csv')
csv
id name scores
0 1 小明 78.0
1 2 小红 87.0
2 3 小白 99.0
3 4 小青 99999.0
4 5 小兰 NaN
按照惯例,Pandas会以pd为别名,以read_csv函数读取指定路径下的文件,然后返回一个DataFrame对象。如果在命令行中打印DataFrame对象,可读性可能会略差一些;如果在Jupyter Notebook中打印的话,可读性会大幅提升。
打印出来的DataFrame包含索引(第一列),列名(第一行)及数据内容(除第一行和第一列之外的部分)。
此外,read_csv函数有很多参数可以设置,如下所示。
Pandas除了可以直接读取csv、excel、json、html等文件生成的DataFrame,也可以在列表、元组、字典等数据结构中创建DataFrame。
使用参数usecol和nrows读取指定的列和前n行,这样可以加快数据读取速度。读取原数据的两列、两行示例如下。
csv=pd.read_csv('data/sample.csv',\
usecols=['id','name'],\
nrows=2) #读取'id'和'name'两列,仅读取前两行
csv
id name
0 1 小明
1 2 小红
参数chunksize可以指定分块读取的行数,并返回一个可迭代对象。这里,big.csv是一个4500行、4列的csv数据,设定chunksize=900,分5块读取数据,每块900行,4个变量,如下所示:
csvs=pd.read_csv('data/big.csv',chunksize=900)
for i in csvs:
print (i.shape)
(900, 4)
(900, 4)
(900, 4)
(900, 4)
(900, 4)
可以使用pd.concat函数读取全部数据:
csvs=pd.read_csv('data/big.csv',chunksize=900)
dat=pd.concat(csvs,ignore_index=True)
dat.shape
(4500, 4)
在数据sample.csv中,“小青”的分数中有的取值为99999,这里令其读取为缺失值,操作如下:
csv=pd.read_csv('data/sample.csv',
na_values='99999')
csv
id name scores
0 1 小明 78.0
1 2 小红 87.0
2 3 小白 99.0
3 4 小青 NaN
4 5 小兰 NaN
读取数据时,乱码情况经常出现。这里需要先弄清楚原始数据的编码形式,再以指定的编码形式读取,例如sample.csv编码为UTF-8,这里以指定编码(参数encoding)方式读取。
csv=pd.read_csv('data/sample.csv',
encoding='utf-8')
csv
id name scores
0 1 小明 78.0
1 2 小红 87.0
2 3 小白 99.0
3 4 小青 99999.0
4 5 小兰 NaN
关于作者:张秋剑,就职于腾讯云金融拓展中心,从事微信财富营销管理、数据中台、AI应用等解决方案拓展工作,研究方向包括数字化转型、创新实践等。
张浩,曾任腾讯云金融首席架构师和星环科技金融行业技术总监,主要从事大数据、人工智能、云计算、区块链、联邦学习等相关技术研发与产品设计,具有丰富的企业架构设计、企业数字化战略转型运营与业务咨询经验。
周大川,就职于某中央金融企业金融科技研发中心,主要从事企业级数据平台开发、核心业务平台建设、AI赋能金融科技创新等工作,具有丰富的新一代金融业务系统建设经验。
常国珍,曾任毕马威咨询大数据总监,具有近20年数据挖掘、精益数据治理、数字化运营咨询经验,是金融信用风险、反欺诈和反洗钱算法领域的专家。
本文摘编自《金融商业数据分析:基于Python和SAS》,经出版方授权发布。(ISBN:9787111695837)
《金融商业数据分析:基于Python和SAS》
推荐语:腾讯云等资深数据架构师、商业分析师20年经验,全流程讲解金融数据分析思路、方法、技巧,快速入门到精通。
来看一个实例问题。
?如下销售数据中展现了三笔订单,每笔订单买了多种商品,求每种商品销售额占该笔订单总金额的比例。例如第一条数据的最终结果为:235.83 / (235.83+232.32+107.97)=40.93%。
后台回复“transform”获取本文全部代码和pdf版本。
常规的解法是,先用对订单id分组,求出每笔订单的总金额,再将源数据和得到的总金额进行“关联”。最后把相应的两列相除即可。相应的代码如下:
1.对订单id分组,求每笔订单总额。由于有三个order,因此最终会产生三条记录表示三个总金额。
2.数据关联合并
为了使每行都出现相应order的总金额,需要使用“左关联”。我们使用源数据在左,聚合后的总金额数据在右(反过来也可)。不指定连接key,则会自动查找相应的关联字段。由于是多行对一行的关联,关联上的就会将总金额重复显示多次,刚好符合我们后面计算的需要。结果如上图所示。
3.计算占比
有了前面的基础,就可以进行最终计算了:直接用商品金额ext_price除以订单总额sum_price。并赋值给新的列pct即可。
4.格式调整
为了美观,可以将小数形式转换为百分比形式,自定义函数即可实现。
对于上面的过程,pandas中的transform函数提供了更简洁的实现方式,如下所示:
可以看到,这种方法把前面的第一步和第二步合成了一步,直接得到了sum_price列。这就是transform的核心:作用于groupby之后的每个组的所有数据。可以参考下面的示意图帮助理解:
后面的步骤和前面一致。
这种方法在需要对多列分组的时候同样适用。
为演示效果,我们虚构了如下数据,id,name,cls为维度列。
我们想求:以(id,name,cls)为分组,每组stu的数量占各组总stu的比例。使用transform处理如下:
同样再次计算占比和格式化,得到最终结果:
transform函数的官方文档签名为:DataFrame.transform(func,axis=0,*args,**kwargs),表示调用func函数进行转换,返回转换后的值,且返回值与原来的数据在相同的轴上具有相同的长度。func可以是函数,字符串,列表或字典。具体可以参考官方文档:
https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.transform.html#pandas.DataFrame.transform。
transform既可以和groupby一起使用,也可以单独使用。
1.单独使用
此时,在某些情况下可以实现和apply函数类似的结果。
2.与groupby一起使用
此时,transform函数返回与原数据一样数量的行,并将函数的结果分配回原始的dataframe。也就是说返回的shape是(len(df),1)。本文开头的例子就是这样。而apply函数返回聚合后的行数。例如:
transform和apply的另一个区别是,apply函数可以同时作用于多列,而transform不可以。下面用例子说明:
上图中的例子,定义了处理两列差的函数,在groupby之后分别调用apply和transform,transform并不能执行。如果不采用groupby,直接调用,也会有问题,参见下面的第二种调用方式。
第三种调用调用方式修改了函数,transform依然不能执行。以上三种调用apply的方式处理两列的差,换成transform都会报错。
transform另一个比较突出的作用是用于填充缺失值。举例如下:
在上面的示例数据中,按照name可以分为三组,每组都有缺失值。用平均值填充是一种处理缺失值常见的方式。此处我们可以使用transform对每一组按照组内的平均值填充缺失值。
transform函数经常与groupby一起使用,并将返回的数据重新分配到每个组去。利用这一点可以方便求占比和填充缺失值。但需要注意,相比于apply,它的局限在于只能处理单列的数据。
后台回复“transform”获取本文全部代码和pdf版本。
Reference:
https://www.codenong.com/19966018/
https://www.cnblogs.com/junge-mike/p/12761227.html
https://blog.csdn.net/qq_40587575/article/details/81204514
https://pbpython.com/pandas_transform.html
*请认真填写需求信息,我们会在24小时内与您取得联系。