如何使用pandas实现对40万样本集的快速运算


说明

近期在项目中,涉及到对大样本、高维数据集的分析建模,总体上样本数量有40万个,特征维度有近300个。整个分析建模采用python机器学习框架,涉及pandas进行数据清洗,其中需要依据判断条件对异常样本进行剔除。当在小样本情况下,无论多复杂的处理逻辑,对pandas操作基本都会很快,但当样本变得庞大以后,对于速度的要求则会变得逐渐突出。

运算逻辑是能够定位到近300个特征中,任意一个特征出现异常数值范围的样本索引。比如正常值处于(0,1)之间,超出此范围的视为异常值。

本文从以下三个方式来说明加速pandas运算的一些技巧:

  • for loop进行pandas运算;
  • 通过间接列扩增实现pandas运算;
  • 采用pandas本身算子完成pandas运算。

for loop形式的pandas运算

这是新手容易产生的最直接做法,通过遍历读取逐行样本,若该行的最小值小于0或者最大值的大于1,则该行即为异常样本。

def drop_abnormal_val(df):
	ab_idxs = []
    for ii in range(0, len(df)):
		singsmaple = df.loc[ii, spc_names].to_numpy()
		if min(singsmaple) <= 0 or max(singsmaple) >= 1
			ab_idxs.append(ii)

    idxs_saved = list(set(list(range(0, len(df)))).difference(set(ab_idxs)))
    ab_idx = {
        "query_1":ab_idxs,
    }
    save_idx = {
        "query_1":idxs_saved,
    }
    new_df = df.iloc[save_idx['query_1'], :]
    new_df = new_df.reset_index(drop=True)

    abn_df = df.iloc[ab_idx['query_1'], :]
    abn_df = abn_df.reset_index(drop=True)
    return new_df, abn_df

df, df_abn = drop_abnormal_val(df_src)
df.info()

image

这种方式,处理完所有样本所需时间为11分54秒,对于数据集处理来说,显得过于耗时。

列扩增实现pandas运算

注意到运算逻辑是需要计算每个样本的最大和最小值。利用pandas本身的计算最值算子,将计算出的最大、最小值作为新的列扩增到原来的dataframe中。那么仅需定位这两个最值列,即可定位到异常样本索引。

def drop_abnormal_val(df):

    df['max_val'] = df.max(axis=1)
    df['min_val'] = df.min(axis=1)
    ab_idxs = df.index[(df['min_val']<=0)| (df['max_val']>=1)].tolist()

    idxs_saved = list(set(list(range(0, len(df)))).difference(set(ab_idxs)))
    ab_idx = {
        "query_1":ab_idxs,
    }
    save_idx = {
        "query_1":idxs_saved,
    }
    new_df = df.iloc[save_idx['query_1'], :]
    new_df = new_df.reset_index(drop=True)

    abn_df = df.iloc[ab_idx['query_1'], :]
    abn_df = abn_df.reset_index(drop=True)
    return new_df, abn_df

df, df_abn = drop_abnormal_val(df_src)
df.info()

image

依据这种思想,可以看到完成同样处理逻辑,所需时间下降到41.7s!

本身算子完成pandas运算

前面一种做法,显然产生了冗余数据,扩增的两列在后续业务处理时,还需要去掉。那么基于更简洁代码、更省存储资源的原则,直接针对原始多维特征进行条件判断;

def drop_abnormal_val(df):
    
    ab_idxs = df.index[(df[spc_names]<=0).any(axis=1)| (df[spc_names]>=1).any(axis=1)].tolist()

    idxs_saved = list(set(list(range(0, len(df)))).difference(set(ab_idxs)))
    ab_idx = {
        "query_1":ab_idxs,
    }
    save_idx = {
        "query_1":idxs_saved,
    }
    new_df = df.iloc[save_idx['query_1'], :]
    new_df = new_df.reset_index(drop=True)

    abn_df = df.iloc[ab_idx['query_1'], :]
    abn_df = abn_df.reset_index(drop=True)
    return new_df, abn_df

df, df_abn = drop_abnormal_val(df_src)
df.info()

image

速度下降到1.4s,相对于第一种方式提速了500多倍!!同样的数据,完成相同的目的。不同的方法实现的效率简直天壤之别!!!

另外,针对第2、3种方式,可以看到,使用iloc定位指定索引的样本时,采用了通过字典读取索引的方式,因为字典可以通过哈希表的方式加快读取速度。

结语

总体来说,提速思路从以下两个方面开展:

  • 要有改进代码的意识,输出结果不是最终目的,增加过程的体验很重要。
  • 要尽可能使用语言本身的算子(内置函数),做到代码简洁。这些算子是这门语言的有用利器。

另外,对于pandas运算,还有两个可以尝试的加速方向:一是使用numba,首先将dataframe转换为numpy数组的形式;二是通过cython,涉及到耗时的运算逻辑,采用c++来实现,通过cython实现python调用c++,从而实现提速。


文章作者: 安立广
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 安立广 !
  目录