在本文中,我将介绍和评估不同的python方法,以便运行相同的函数并在pandas中创建新的列。 这些方法将在数字和非数字操作中进行检查。
![4e0125c425d555a323c33c42e057098b.png](https://i-blog.csdnimg.cn/blog_migrate/1045e9f83ea55291ef11fee66023ede6.jpeg)
> Photo by Marc Sendra Martorell on Unsplash
如果您以前拥有足够大的数据集,那么您已经知道有时简单的操作需要大量时间。 有很多方法可以使对Pandas DataFrame的操作更快。 您可以使用多处理,modin [ray],cuDF,Dask,Spark之类的库来完成工作。 另外,您可以修改算法以更快地执行任务。 但是,在寻求解决方案之前,让我们看看是否有任何简单的方法可以提高速度并节省时间!
至于我的数据集,我正在使用其中的一部分。 它有2个数字列和1个文本列,由" HelpfulnessNumerator"列中的一些0和" ProfileName"列中的一些NaN组成。 (数据集和所有笔记本均已上传到此Github存储库中。)
![46f492a49c7ee5ee287e1d856002a302.png](https://i-blog.csdnimg.cn/blog_migrate/a3b0aaa3e0baa9ff052c803072f7e6e5.jpeg)
数值运算
我想简单地检查出不同的方法,而主要功能没有任何变化。 数字部分中的函数是除法函数。
def divide(t,h): if h == 0: return np.nan return t/h
我学习Pandas数据框架的第一件事是df.iterrows()
%%timeititerrows_divide = []for index,row in df.iterrows(): Time = row['Time'] HelpfulnessNumerator = row['HelpfulnessNumerator'] iterrows_divide.append(divide(Time,HelpfulnessNumerator))5min 12s ± 31.1 s per loop (mean ± std. dev. of 7 runs, 1 loop each)
apply()方法是另一种选择:
%timeit df.apply(lambda row: divide(row['Time'],row['HelpfulnessNumerator']),axis = 1)1min 25s ± 4.88 s per loop (mean ± std. dev. of 7 runs, 1 loop each)
Pandas行操作中最好的选择是itertuples()方法:
%%timeititertuples_divide = []for _ in df.itertuples(): Time = getattr(_,'Time') HelpfulnessNumerator = getattr(_,'HelpfulnessNumerator') itertuples_divide.append(divide(Time,HelpfulnessNumerator))4 s ± 238 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
在大多数情况下,强制使用逐行操作。 但是,如果您必须这样做,则df.itertuples()是最快的方法。
让我们尝试一下map()方法:
%timeit list(map(divide,df['Time'],df['HelpfulnessNumerator']))861 ms ± 22.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
让我们做一些(假的)numpy vectorizer()方法:
%timeit np.vectorize(divide)(df['Time'],df['HelpfulnessNumerator'])636 ms ± 24.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
我们可以将numpy where()方法用于(实际)numpy矢量化程序:
%timeit np.where(df['HelpfulnessNumerator'] == 0 , 0 , df['Time']/df['HelpfulnessNumerator'])21.6 ms ± 329 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
如您所见,我们不能在所有地方都使用np.where()方法。 但这确实很快。 让我们稍微改变一下功能:
def modified_divide(t,h): return t/h
现在我们可以在np.where()中使用此函数:
%timeit np.where(df['HelpfulnessNumerator'] == 0 , 0 , modified_divide(df['Time'],df['HelpfulnessNumerator']))21.4 ms ± 200 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
另外,我们可以直接在pandas操作中使用此功能:
%timeit modified_divide(df['Time'],df['HelpfulnessNumerator'])8.75 ms ± 114 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
还有其他选择吗? 绝对没错。 如果可以使用Python编写代码,则可以使用Fortran编写代码。 我在Jupyter Notebook中为Fortran魔术制作了一个笔记本。 仅作为示例,请看以下示例:
%%fortransubroutine fortran_divide(x, y, z) real, intent(in) :: x,y real, intent(out) :: z z = x/y IF ( 0 .EQ. Y ) THEN Z = 0 END IF end subroutine fortran_divide%timeit np.vectorize(fortran_divide)(df['Time'],df['HelpfulnessNumerator'])669 ms ± 22.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
我们可以使用numba。 Numba是一个python库,我们可以很直接地使用它。 它比np.where()快2倍,并且您在这里没有np.where()的一些限制:
from numba import njit@njit()def divide(t, h): res = np.empty(t.shape) for i in range(len(t)): if h[i] != 0: res[i] = t[i] / h[i] else: res[i] = 0 return res%timeit divide(df['Time'].values, df['HelpfulnessNumerator'].value9.99 ms ± 133 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
numba和Fortran都可以选择使用多处理功能。 它取决于DataFrame的大小和操作。 有时我们可以使用多重处理来提高速度,有时numba和Fortran中的多重处理对我们没有太大帮助。
![ac8b6e078f6315d98aad6b21af3d07d8.png](https://i-blog.csdnimg.cn/blog_migrate/c06e83b2ff94072b481929261747a439.jpeg)
> logarithmic time for each method.
结论是,不要对pandas DataFrame使用按行操作。 如果必须的话,可以使用df.itertuples()。 永远不要使用df.iterrows()和df.apply(…,axis = 1)。
大多数时候,您可以使用np.where()进行一些技巧。 这是最好的选择。 但是,如果不能使用它,则可以在执行数字运算时使用np.vectorize()。
还有一些其他选项,例如numba和Fortran魔术,如果我们有大量的数据集,值得花一些时间来学习和使用它们,它们可以为我们提供帮助。
此外,有时候Pandas本身也支持我们想要的东西。 最好先检查大熊猫。 :)
非数值运算
我将定义一个简单的函数来评估不同的方法。 该函数将在每个记录的开头添加一个短语。
def edit_name(profileName): if profileName != profileName: return np.nan return 'Name is {}'.format(profileName)
使用df.iterrows()很简单。 但这太慢了:
%%timeititerrows_name=[]for index,row in df.iterrows(): name = edit_name(row['ProfileName']) iterrows_name.append(name)4min 15s ± 2.45 s per loop (mean ± std. dev. of 7 runs, 1 loop each)
使用逐行套用method():
%timeit df.apply(lambda row: edit_name(row['ProfileName']),axis = 1)43.2 s ± 687 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
使用itertuples()方法:
%%timeititertuples_name=[]for _ in df.itertuples(): name = edit_name(getattr(_,'ProfileName')) itertuples_name.append(name)3.78 s ± 128 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
另一种使用apply()方法的方法:
%timeit df['ProfileName'].apply(lambda x: edit_name(x))1.58 s ± 86.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
让我们使用map()方法:
%timeit list(map(edit_name,df['ProfileName']))1.41 s ± 65.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
糟糕,np.vectorizer的速度不如我们预期的快。 是的,因为该运算不是数字。
%timeit np.vectorize(edit_name)(df['ProfileName'])2.16 s ± 155 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
有一个pandas.Series.map可以非常快地完成有限的任务。 它本身可以处理NaN值。 例如,让我们更改函数并查看结果:
def modified_edit_name(profileName): return 'Name is {}'.format(profileName)
我们可以在np.where()中轻松处理NaN值:
%timeit np.where(df['ProfileName'] == np.nan,np.nan, modified_edit_name(df['ProfileName']))2.69 s ± 98.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Pandas系列map方法:
%timeit df['ProfileName'].map('Name is {}'.format, na_action='ignore')1.16 s ± 50.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
![4a04ce6fdcea19cd73fef94ba0b7c1ee.png](https://i-blog.csdnimg.cn/blog_migrate/9e28fa23316df867a7708202838fb2db.jpeg)
> logarithmic time for each method.
当涉及文本时,我更喜欢使用map(),因为它速度很快。 如果必须执行逐行操作,则将使用df.itertuples()。
就像我说的那样,经过简单的优化之后,我们可以继续进行多处理并使用其他库来对pandas DataFrame进行更快的操作。
(本文翻译自Mahdi Rasouli的文章《How to simply make an operation on pandas DataFrame faster》,参考:https://towardsdatascience.com/how-to-simply-make-an-operation-on-pandas-dataframe-faster-adaea5e41e96)