作用于索引的向量化函数

Vectorize function acting on indexes

TL;DR:我知道 .apply() 在 pandas 中很慢。但是,我有一个作用于索引的函数,我不知道如何对其进行矢量化。我希望此函数作用于两组参数(分别为 500,000 和 1,500 长)。我想生成一个数据框,其中第一个参数作为行索引,第二个参数作为列名,以及包含该特定行和列的函数输出的单元格。就目前而言,代码似乎需要几天时间才能完成 运行。下面是更多详细信息和最小的可重现示例:


输入数据:

我有一系列唯一的学生证,有50万学生长。我有一个 df (exam_score_df),由这些学生 ID 索引,包含每个学生在数学、语言、历史和科学方面的相应分数。

我还有一系列的学校代码(每个学校代码对应一所学校),一共有1500所学校。我有一个由学校代码索引的 df (school_weight_df),包含学校在数学、语言、历史和科学方面的权重,用于计算学生的分数。每行还包含一个 'Y' 或 'N' 索引 'Alternative_Score' 因为有些学校允许你选择历史和科学之间最好的科目分数来计算你的总分。

我写的要向量化的函数:

def calc_score(student_ID, program_code):
'''
For a given student and program, returns students score for that program.
'''
if school_weight_df.loc[program_code]['Alternative_Score'] == 'N':
    
    return np.dot(np.array(exam_score_df.loc[student_ID][['LANG', 'MAT', 'HIST', 'SCI']]), 
                  np.array(school_weight_df.loc[program_code][['%LANG','%MAT','%HIST','%SCI']]))

elif school_weight_df.loc[program_code]['Alternative_Score'] == 'Y':

    history_score = np.dot(np.array(exam_score_df.loc[student_ID][['LANG', 'MAT', 'HIST']]), 
                           np.array(school_weight_df.loc[program_code][['%LANG','%MAT','%HIST']]))
    
    science_score = np.dot(np.array(exam_score_df.loc[student_ID][['LANG', 'MAT', 'SCI']]), 
                           np.array(school_weight_df.loc[program_code][['%LANG','%MAT','%SCI']]))

    return max(history_score, science_score)

示例 DF:

这里是 exam_score_df 和 school_weight_df 的示例 dfs:

student_data = [[3, 620, 688, 638, 688], [5, 534, 602, 606, 700], [9, 487, 611, 477, 578]]
exam_score_df = pd.DataFrame(student_data, columns = ['student_ID', 'LANG', 'MAT', 'HIST', 'SCI'])
exam_score_df.set_index('student_ID')

program_data = [[101, 20, 30, 25, 25, 'N'], [102, 40, 10, 50, 50, 'Y']]
school_weight_df = pd.DataFrame(program_data, columns = ['program_code', '%LANG','%MAT','%HIST','%SCI', 'Alternative_Score'])
school_weight_df.set_index('program_code', inplace = True)

这是用于索引以下代码的系列:

series_student_IDs = pd.Series(exam_score_df.index, inplace = True)
series_program_codes = pd.Series(school_weight_df.index, inplace = True)

使用函数创建 DF 的代码:

为了在每个项目中创建所有学生分数的 df,我使用了嵌套的 .apply():

new_df = pd.DataFrame(series_student_IDs.apply(lambda x: series_program_codes.apply(lambda y: calc_score(x, y))))

我已经在 pandas 中阅读了一些关于优化代码的入门读物,包括写得很好的 Guide by Sofia Heisler. My primary concern, and reason for why I can't figure out how to vectorize this code, is that my function needs to act on indexes. I also have a secondary concern that, even if I do vectorize, there is this problem with ,无论如何我都想循环阅读它。

感谢大家的帮助!我才编码几个月,所以非常感谢所有有用的评论。

应用 = 糟糕,双重应用 = 非常糟糕

如果你在函数中使用 Numpy,为什么不完全使用 Numpy?您仍然更喜欢分批处理的方法,因为整个矩阵会占用大量内存。检查以下方法。

我在低端 macbook pro 上对一批 5000 名学生进行每次迭代花费了 2.05 秒。这意味着对于 500,000 名学生,您可以预计大约 200 秒,这还不错。

我 运行 以下是关于 100000 名学生和 1500 所学校,我总共花了大约 30-40 秒。

  1. 首先我创建了一个虚拟集数据:考试分数(100,000 名学生,4 个分数),学校权重(1500 所学校,4 个权重)和一个 布尔值学校可以选择 Y 或 N 的标志,Y==TrueN==False

  2. 接下来,对于一批 5000 名学生,我使用 np.einsum 简单地计算 2 个矩阵之间 4 个科目中每个科目的元素乘积。这给了我 (5000,4) * (1500,4) -> (1500,5000,4)。将其视为点积的第一部分(没有总和)。

  3. 我这样做的原因是因为这对你的条件N或Y来说都是必要的步骤。

  4. 接下来,FOR N:我简单地根据alt_flag过滤上面的矩阵,在最后一个轴和t运行spose上减少(求和)得到(5000, 766),其中 766 是 alternative == N

    的学校数量
  5. FOR Y:,我根据 alt_flag 进行过滤,然后我计算前 2 个主题的总和(因为它们很常见)并将它们添加到第 3 个和第 4 个单独科目,取一个最大值和 return 作为我的最终分数。 Post那个T运行姿势。这给了我 (5000, 734).

  6. 我对每批 5000 个都这样做,直到我附加了所有批次,然后简单地 np.vstack 得到最终表 (100000, 766)(100000, 734) .

  7. 现在我可以简单地将它们堆叠在 axis=0 上以获得 (100000, 1500) 但是如果我想将它们映射到 ID(学生、学校),这样做会更容易分别使用 pd.DataFrame(data, columns=list_of_schools_alt_Y, index=list_of_student_ids 然后组合它们。为您阅读最后一步。

  8. 由于我没有完整的数据集,最后一步由您执行。由于索引的顺序是通过批量矢量化保留的,因此您现在可以简单地将 766 个学校 ID 映射到 N,将 734 个学校 ID 映射到 Y,以及 100000 个学生 ID,按照它们在主数据集中出现的顺序。然后简单地附加 2 个数据帧以创建最终(大量)数据帧。

  9. 注意:您必须将 for 循环中的 100000 更改为 500000,不要忘记!!

import numpy as np
import pandas as pd
from tqdm import notebook

exam_scores = np.random.randint(300,800,(100000,4))
school_weights = np.random.randint(10,50,(1500,4))
alt_flag = np.random.randint(0,2,(1500,), dtype=bool) #0 for N, 1 for Y
batch = 5000

n_alts = []
y_alts = []

for i in notebook.tqdm(range(0,100000,batch)):
    scores = np.einsum('ij,kj->kij', exam_scores[i:i+batch], school_weights) #(1500,5000,4)

    #Alternative == N
    n_alt = scores[~alt_flag].sum(-1).T   #(766, 5000, 4) -> (5000, 766)

    #Alternative == Y
    lm = scores[alt_flag,:,:2].sum(-1)    #(734, 5000, 2) -> (734, 5000); lang+math
    h = scores[alt_flag,:,2]              #(734, 5000); history
    s = scores[alt_flag,:,3]              #(734, 5000); science
    y_alt = np.maximum(lm+h, lm+s).T      #(5000, 734)
    
    n_alts.append(n_alt)
    y_alts.append(y_alt)

final_n_alts = np.vstack(n_alts)
final_y_alts = np.vstack(y_alts)

print(final_n_alts.shape)
print(final_y_alts.shape)