pandas 如何加速 Dataframe 中的循环

kpbwa7wx  于 2023-01-01  发布在  其他
关注(0)|答案(3)|浏览(221)

我想加快我的循环,因为我必须对900 000个数据进行循环。
为了简单起见,我给你看一个例子。
我想添加一个列名“计数”来计算同一个球员得分低于目标得分的次数。但是每一行的目标都会改变。
输入:

index Nom player  score  target score
0      0      felix      3            10
1      1      felix      8             7
2      2       theo      4             5
3      3    patrick     12             6
4      4     sophie      7             6
5      5     sophie      3             6
6      6      felix      2             4
7      7      felix      2             2
8      8      felix      2             3

结果:

index Nom player  score  target score  Count
0      0      felix      3            10      5
1      1      felix      8             7      4
2      2       theo      4             5      1
3      3    patrick     12             6      0
4      4     sophie      7             6      1
5      5     sophie      3             6      1
6      6      felix      2             4      4
7      7      felix      2             2      0
8      8      felix      2             3      3

下面的代码是我目前使用的,但是有没有可能加快速度?我看过一些关于矢量化的文章,有没有可能应用到我的计算上?如果有,怎么做

df2 = df.copy()
df2['Count']= [np.count_nonzero((df.values[:,1] == row[2] )& ( df.values[:,2] < row[4]) )    for row in df.itertuples()]
print(df2)
crcmnpdw

crcmnpdw1#

O(n log n)解决方案的Jérôme Richard's insights可以转换为pandas。速度的提高取决于 Dataframe 中组的数量和大小。

df2 = df.copy()
gr = df2.groupby('Nom player')
lookup = gr.score.apply(np.sort).to_dict()
df2['count'] = gr.apply(
    lambda x: pd.Series(
        np.searchsorted(lookup[x.name], x['target score']),
        index=x.index)
    ).droplevel(0)
print(df2)

产出

index Nom player  score  target score  count
0      0      felix      3            10      5
1      1      felix      8             7      4
2      2       theo      4             5      1
3      3    patrick     12             6      0
4      4     sophie      7             6      1
5      5     sophie      3             6      1
6      6      felix      2             4      4
7      7      felix      2             2      0
8      8      felix      2             3      3
jq6vz3qz

jq6vz3qz2#

您可以尝试:

df['Count'] = df.groupby("Nom player").apply(
    lambda x: pd.Series((sum(x["score"] < s) for s in x["target score"]), index=x.index)
).droplevel(0)

print(df)

图纸:

index Nom player  score  target score  Count
0      0      felix      3            10      5
1      1      felix      8             7      4
2      2       theo      4             5      1
3      3    patrick     12             6      0
4      4     sophie      7             6      1
5      5     sophie      3             6      1
6      6      felix      2             4      4
7      7      felix      2             2      0
8      8      felix      2             3      3

编辑:快速基准测试:

from timeit import timeit

def add_count1(df):
    df["Count"] = (
        df.groupby("Nom player")
        .apply(
            lambda x: pd.Series(
                ((x["score"] < s).sum() for s in x["target score"]), index=x.index
            )
        )
        .droplevel(0)
    )

def add_count2(df):
    df["Count"] = [
        np.count_nonzero((df.values[:, 1] == row[2]) & (df.values[:, 2] < row[4]))
        for row in df.itertuples()
    ]

def add_count3(df):
    gr = df.groupby('Nom player')
    lookup = gr.score.apply(lambda x: np.sort(np.array(x))).to_dict()
    df['count'] = gr.apply(
        lambda x: pd.Series(
            np.searchsorted(lookup[x.name], x['target score']),
            index=x.index)
        ).droplevel(0)

df = pd.concat([df] * 1000).reset_index(drop=True)  # DataFrame of len=9000

t1 = timeit("add_count1(x)", "x=df.copy()", number=1, globals=globals())
t2 = timeit("add_count2(x)", "x=df.copy()", number=1, globals=globals())
t3 = timeit("add_count3(x)", "x=df.copy()", number=1, globals=globals())

print(t1)
print(t2)
print(t3)

我的计算机上的指纹:

0.7540620159707032
6.63946107000811
0.004106967011466622

所以我的答案应该比原来的答案快,但迈克尔·什琴斯尼的答案是最快的。

mznpcxlj

mznpcxlj3#

当前代码存在两个主要问题,CPython字符串对象比较慢,尤其是字符串比较,而且当前算法具有二次复杂度:它会比较所有与当前行匹配的行,对于每一行,后者是大 Dataframe 的最大问题。

实施

第一件要做的事情是用更快的东西来代替字符串比较,Strings对象可以用np.array转换成原生字符串,然后,使用np.unique可以提取唯一字符串及其位置,这基本上帮助我们将字符串匹配问题替换为整数匹配问题。比较原生整数通常要快得多,主要是因为像Numpy这样的处理器可以使用高效的SIMD指令来比较整数。下面是如何将字符串列转换为标签索引:

# 0.065 ms
labels, labelIds = np.unique(np.array(df.values[:,1], dtype='U'), return_inverse=True)

现在,我们可以通过标签(球员姓名)有效地对分数进行分组。问题是Numpy没有提供任何分组函数。虽然使用多个np.argsort可以有效地做到这一点,但一个基本的基于Python dict的方法在实践中被证明是非常快的。下面是代码,它通过标签对分数进行分组,并对与每个标签相关的分数集进行排序(对下一步很有用):

# 0.014 ms

from collections import defaultdict

scoreByGroups = defaultdict(lambda: [])

labelIdsList = labelIds.tolist()
scoresList = df['score'].tolist()
targetScoresList = df['target score'].tolist()

for labelId, score in zip(labelIdsList, scoresList):
    scoreByGroups[labelId].append(score)

for labelId, scoreGroup in scoreByGroups.items():
    scoreByGroups[labelId] = np.sort(np.array(scoreGroup, np.int32))

现在可以使用scoreByGroups来有效地查找给定标签的小于给定值的分数,只需读取scoreByGroups[label](常数时间),然后对得到的数组(O(log n))进行二进制搜索,具体方法如下:

# 0.014 ms
counts = [np.searchsorted(scoreByGroups[labelId], score)
          for labelId, score in zip(labelIdsList, targetScoresList)]

# Copies are slow, but adding a new column seems even slower
# 0.212 ms
df2 = df.copy()
df2['Count'] = np.fromiter(counts, np.int32)

结果

在我的机器上,示例输入的结果代码花费了0.305 ms,而初始代码花费了1.35 ms。这意味着此实现大约快了4.5倍。遗憾的是,2/3的时间花费在创建包含新列的新 Dataframe 上。请注意,提供的代码应该比大型 Dataframe 上的初始代码快得多这是由于O(n log n)复杂度而不是O(n²)复杂度。

更快地实施大型 Dataframe

在大型 Dataframe 上,由于Numpy的开销,为每个项调用np.searchsorted是昂贵的。一个容易消除此开销的解决方案是使用Numba。可以使用列表而不是字典来优化计算,因为标签是0..len(labelIds)范围内的整数。也可以部分并行地完成计算。
使用pd.factorize可以显著加快字符串到int的转换,尽管这仍然是一个代价高昂的过程。
以下是基于Numba的完整解决方案:

import numba as nb

@nb.njit('(int64[:], int64[:], int64[:])', parallel=True)
def compute_counts(labelIds, scores, targetScores):
    groupSizes = np.bincount(labelIds)
    groupOffset = np.zeros(groupSizes.size, dtype=np.int64)
    scoreByGroups = [np.empty(e, dtype=np.int64) for e in groupSizes]
    counts = np.empty(labelIds.size, dtype=np.int64)

    for labelId, score in zip(labelIds, scores):
        offset = groupOffset[labelId]
        scoreByGroups[labelId][offset] = score
        groupOffset[labelId] = offset + 1

    for labelId in nb.prange(len(scoreByGroups)):
        scoreByGroups[labelId].sort()

    for i in nb.prange(labelIds.size):
        counts[i] = np.searchsorted(scoreByGroups[labelIds[i]], targetScores[i])

    return counts

df2 = df.copy()                                      # Slow part
labelIds, labels = pd.factorize(df['Nom player'])    # Slow part
counts = compute_counts(                             # Pretty fast part
    labelIds.astype(np.int64), 
    df['score'].to_numpy().astype(np.int64), 
    df['target score'].to_numpy().astype(np.int64)
)
df2['Count'] = counts                                # Slow part

在我的6核机器上,这段代码在大型 Dataframe 上要快得多。这是最快的答案之一。在9000行的随机 Dataframe 上,它只比@MichaelSzczesny2.5。字符串到int的转换占用了40 - 45%的时间,并且创建了新的Pandas Dataframe (带附加列)花费了25%的时间。Numba函数花费的时间实际上最后很小。大部分时间最终都浪费在了开销中
请注意,使用类别数据可以只进行一次(预计算),它对其他计算也很有用,因此实际上可能并不那么昂贵。

相关问题