优化 Python 代码。优化 Pandas 应用。 Numba 比纯 python 慢

Optimize Python code. Optimize Pandas apply. Numba slow than pure python

我面临着一个巨大的瓶颈,我将 method() 应用于 Pandas DataFrame 中的每一行。执行时间大约为 15-20 分钟。

现在,我使用的代码如下:

def FillTarget(self, df):
    backup = df.copy()

    target = list(set(df['ACTL_CNTRS_BY_DAY']))
    df = df[~df['ACTL_CNTRS_BY_DAY'].isnull()]
    tmp = df[df['ACTL_CNTRS_BY_DAY'].isin(target)]
    tmp = tmp[['APPT_SCHD_ARVL_D', 'ACTL_CNTRS_BY_DAY']]
    tmp.drop_duplicates(subset='APPT_SCHD_ARVL_D', inplace=True)
    t1 = dt.datetime.now()
    backup['ACTL_CNTRS_BY_DAY'] = backup.apply(self.ImputeTargetAcrossSameDate,args=(tmp, ), axis=1)
    # backup['ACTL_CNTRS_BY_DAY'] = self.compute_(tmp, backup)
    t2 = dt.datetime.now()
    print("Time for the bottleneck is ", (t2-t1).microseconds)

    print("step f")

    return backup

并且,方法ImputeTargetAcrossSameDate()方法如下:

def ImputeTargetAcrossSameDate(self, x, tmp):
    ret = tmp[tmp['APPT_SCHD_ARVL_D'] == x['APPT_SCHD_ARVL_D']]
    ret = ret['ACTL_CNTRS_BY_DAY']

    if ret.empty:
        r = 0
    else:
        r = ret.values
        r = r[0]

    return r

有什么方法可以优化此 apply() 调用以减少总体时间。 请注意,我必须 运行 在存储数据 2 年的 DataFrame 上执行此过程。我运行宁了15天,用了15-20分钟,而运行1个月的数据时,执行了45多分钟,之后我不得不强制停止这个过程,因此当 运行 在完整数据集上运行时,这将是一个巨大的问题。

另请注意,我遇到了几个例子 http://pandas.pydata.org/pandas-docs/stable/enhancingperf.html 引入 numba 来优化代码,以下是我的 numba 实现:

调用numba方法的语句:

backup['ACTL_CNTRS_BY_DAY'] = self.compute_(tmp, backup)

numba的计算方法:

@numba.jit
def compute_(self, df1, df2):
    n = len(df2)
    result = np.empty(n, dtype='float64')
    for i in range(n):
        d = df2.iloc[i]
        result[i] = self.apply_ImputeTargetAcrossSameDate_method(df1['APPT_SCHD_ARVL_D'].values, df1['ACTL_CNTRS_BY_DAY'].values,
                                                                    d['APPT_SCHD_ARVL_D'], d['ACTL_CNTRS_BY_DAY'])
    return result

这是替换 Pandas' 的包装器方法,适用于在每一行上调用 Impute 方法。使用numba的impute方法如下:

@numba.jit
def apply_ImputeTargetAcrossSameDate_method(self, df1col1, df1col2, df2col1, df2col2):

    dd = np.datetime64(df2col1)

    idx1 = np.where(df1col1 == dd)[0]

    if idx1.size == 0:
        idx1 = idx1
    else:
        idx1 = idx1[0]

    val = df1col2[idx1]

    if val.size == 0:
        r = 0
    else:
        r = val

    return r

我 运行 正常的 apply() 方法以及 numba() 方法用于时间段为 5 天的数据,以下是我的结果:

With Numba:
749805 microseconds

With DF.apply()
484603 microseconds.

如您所见,numba 变慢了,这不应该发生,所以如果我遗漏了什么,请告诉我,以便我可以优化这段代码。

提前致谢

编辑 1 根据要求,截取的数据(前 20 行的头部)添加如下: 之前:

    APPT_SCHD_ARVL_D  ACTL_CNTRS_BY_DAY
919       2020-11-17                NaN
917       2020-11-17                NaN
916       2020-11-17                NaN
915       2020-11-17                NaN
918       2020-11-17                NaN
905       2014-06-01                NaN
911       2014-06-01                NaN
913       2014-06-01                NaN
912       2014-06-01                NaN
910       2014-06-01                NaN
914       2014-06-01                NaN
908       2014-06-01                NaN
906       2014-06-01                NaN
909       2014-06-01                NaN
907       2014-06-01                NaN
898       2014-05-29                NaN
892       2014-05-29                NaN
893       2014-05-29                NaN
894       2014-05-29                NaN
895       2014-05-29                NaN

之后:

APPT_SCHD_ARVL_D  ACTL_CNTRS_BY_DAY
919       2020-11-17                0.0
917       2020-11-17                0.0
916       2020-11-17                0.0
915       2020-11-17                0.0
918       2020-11-17                0.0
905       2014-06-01                0.0
911       2014-06-01                0.0
913       2014-06-01                0.0
912       2014-06-01                0.0
910       2014-06-01                0.0
914       2014-06-01                0.0
908       2014-06-01                0.0
906       2014-06-01                0.0
909       2014-06-01                0.0
907       2014-06-01                0.0
898       2014-05-29                0.0
892       2014-05-29                0.0
893       2014-05-29                0.0
894       2014-05-29                0.0
895       2014-05-29                0.0

这个方法有什么作用? 在上面的数据示例中,您可以看到一些日期是重复的,并且针对它们的值为 NaN。如果具有相同日期的所有行的值为 NaN,则将它们替换为 0。 但是在某些情况下,例如:2014-05-29 将有 10 行具有相同的日期,并且只有 1 行针对该日期会有一些值。 (比方说 10)。然后 method() 应使用 10 而不是 NaN 填充针对该特定日期的所有值。

示例:

898       2014-05-29                NaN
892       2014-05-29                NaN
893       2014-05-29                NaN
894       2014-05-29                10
895       2014-05-29                NaN

以上变为:

898       2014-05-29                10
892       2014-05-29                10
893       2014-05-29                10
894       2014-05-29                10
895       2014-05-29                10

这个解决方案有点仓促,因为我现在就要离开到周末了,但它确实有效。

输入数据帧:

index    APPT_SCHD_ARVL_D  ACTL_CNTRS_BY_DAY
919       2020-11-17                NaN
917       2020-11-17                NaN
916       2020-11-17                NaN
915       2020-11-17                NaN
918       2020-11-17                NaN
905       2014-06-01                NaN
911       2014-06-01                NaN
913       2014-06-01                NaN
912       2014-06-01                NaN
910       2014-06-01                NaN
914       2014-06-01                NaN
908       2014-06-01                NaN
906       2014-06-01                NaN
909       2014-06-01                NaN
907       2014-06-01                NaN
898       2014-05-29                NaN
892       2014-05-29                NaN
893       2014-05-29                NaN
894       2014-05-29                10
895       2014-05-29                NaN
898       2014-05-29                NaN

代码:

tt = df[pd.notnull(df.ACTL_CNTRS_BY_DAY)].APPT_SCHD_ARVL_D.unique()
vv = df[pd.notnull(df.ACTL_CNTRS_BY_DAY)]
for i,_ in df.iterrows():
    if df.ix[i,"APPT_SCHD_ARVL_D"] in tt:
        df.ix[i,"ACTL_CNTRS_BY_DAY"] = vv[vv.APPT_SCHD_ARVL_D == df.ix[i,"APPT_SCHD_ARVL_D"]]["ACTL_CNTRS_BY_DAY"].values[0]
df = df.fillna(0.0)

基本上不需要apply一个函数。我在这里做的是:

  • 获取所有具有非空值的唯一日期。 -> tt
  • 创建仅包含非空值的数据框。 -> vv
  • 遍历所有行并测试每行中的日期是否存在于tt
  • 如果为真,则从 vv 中取值,其中 df 中的日期相同,并将其分配给 df
  • 然后用0.0填充所有其他空值。

遍历行并不是一件很快的事情,但我希望它比您的旧代码更快。如果我有更多时间,我会想到一个无需迭代的解决方案,也许在星期一。

编辑: 不使用 pd.merge() 代替迭代的解决方案:

dg = df[pd.notnull(df.ACTL_CNTRS_BY_DAY)].groupby("APPT_SCHD_ARVL_D").first()["ACTL_CNTRS_BY_DAY"].to_frame().reset_index()
df = pd.merge(df,dg,on="APPT_SCHD_ARVL_D",how='outer').rename(columns={"ACTL_CNTRS_BY_DAY_y":"ACTL_CNTRS_BY_DAY"}).drop("ACTL_CNTRS_BY_DAY_x",axis=1).fillna(0.0)

您的数据表明 ACTL_CNTRS_BY_DAY 中最多只有一个值不为空,因此我在 groupby 中使用 first() 来选择唯一存在的值.