为啥我的 python DataFrame 执行如此缓慢

Posted

技术标签:

【中文标题】为啥我的 python DataFrame 执行如此缓慢【英文标题】:Why is my python DataFrame performing so slowly为什么我的 python DataFrame 执行如此缓慢 【发布时间】:2019-01-16 14:30:18 【问题描述】:

我正在构建一个应用程序,它可以对大型数据集进行一些非常简单的分析。这些数据集以 1000 万行 + 约 30 列的 CSV 文件形式提供。 (我不需要很多列。)

逻辑告诉我将整个文件放入 DataFrame 应该可以更快地访问它。但是我的电脑说没有。

我尝试过分批加载,以及加载整个文件,然后分批执行功能。

但最终结果是执行相同的过程所花费的时间是使用简单文件读取选项的 10 倍以上。

这是 DataFrame 版本:

def runProcess():
    global batchSize
    batchCount = 10
    if rowLimit < 0:
        with open(df_srcString) as f:
            rowCount = sum(1 for line in f)
        if batchSize < 0:
            batchSize = batchSize * -1
            runProc = readFileDf
        else:
            runProc = readFileDfBatch
        batchCount = int(rowCount / batchSize) + 1
    else:
        batchCount = int(rowLimit / batchSize) + 1
    for i in range(batchCount):
        result = runProc(batchSize, i)
        print(result)

def readFileDfBatch(batch, batchNo):
    sCount = 0
    lCount = 0
    jobStartTime = datetime.datetime.now()
    eof = False
    totalRowCount = 0

    startRow = batch * batchNo
    df_wf = pd.read_csv(df_srcString, sep='|', header=None, names=df_fldHeads.split(','), usecols=df_cols, dtype=str, nrows=batch, skiprows=startRow)
    for index, row in df_wf.iterrows():
        result = parseDfRow(row)
        totalRowCount = totalRowCount + 1
        if result == 1:
            sCount = sCount + 1
        elif result == 2:
            lCount = lCount + 1
    eof = batch > len(df_wf)
    if rowLimit >= 0:
        eof = (batch * batchNo >= rowLimit)
    jobEndTime = datetime.datetime.now()
    runTime = jobEndTime - jobStartTime
    return [batchNo, sCount, lCount, totalRowCount, runTime]

def parseDfRow(row):
#df_cols = ['ColumnA','ColumnB','ColumnC','ColumnD','ColumnE','ColumnF']
    status = 0
    s2 = getDate(row['ColumnB'])
    l2 = getDate(row['ColumnD'])
    gDate = datetime.date(1970,1,1)
    r1 = datetime.date(int(row['ColumnE'][1:5]),12,31)
    r2 = row['ColumnF']
    if len(r2) > 1:
        lastSeen = getLastDate(r2)
    else:
        lastSeen = r1
    status = False
    if s2 > lastSeen:
        status = 1
    elif l2 > lastSeen:
        status = 2
    return status

这是简单的文件阅读器版本:

def readFileStd(rows, batch):
    print("Starting read: ")
    batchNo = 1
    global targetFile
    global totalCount
    global sCount
    global lCount
    targetFile = open(df_srcString, "r")
    eof = False
    while not eof:
        batchStartTime = datetime.datetime.now()
        eof = readBatch(batch)
        batchEndTime = datetime.datetime.now()
        runTime = batchEndTime - batchStartTime
        if rows > 0 and totalCount >= rows: break
        batchNo = batchNo + 1
    targetFile.close()
    return [batchNo, sCount, lCount, totalCount, runTime]

def readBatch(batch):
    global targetFile
    global totalCount
    rowNo = 1
    rowStr = targetFile.readline()
    while rowStr:
        parseRow(rowStr)
        totalCount = totalCount + 1
        if rowNo == batch: 
            return False
        rowStr = targetFile.readline()
        rowNo = rowNo + 1
    return True

    def parseRow(rowData):
    rd = rowData.split('|')
    s2 = getDate(rd[3])
    l2 = getDate(rd[5])
    gDate = datetime.date(1970,1,1)
    r1 = datetime.date(int(rd[23][1:5]),12,31)
    r2 = rd[24]
    if len(r2) > 1:
        lastSeen = getLastDate(r2)
    else:
        lastSeen = r1
    status = False
    if s2 > lastSeen:
        global sCount
        sCount = sCount + 1
        status = True
        gDate = s2
    elif l2 > lastSeen:
        global lCount
        lCount = lCount + 1
        gDate = s2

我做错了吗?

【问题讨论】:

感谢@ManuValdés 的有用评论 【参考方案1】:

iterrows 没有利用矢量化操作。使用pandas 的大部分好处来自矢量化和并行操作。

for index, row in df_wf.iterrows(): 替换为df_wf.apply(something, axis=1),其中something 是一个函数,它封装了iterrows 所需的逻辑,并使用numpy 向量化操作。

此外,如果您的df 不适合内存,因此您需要批量读取,请考虑使用daskspark 而不是pandas

延伸阅读:https://pandas.pydata.org/pandas-docs/stable/enhancingperf.html

【讨论】:

次要观点:DataFrame.apply 对原生矢量化循环或并行化没有帮助……它只是将函数依次迭代地应用于每一行。这可能会比使用 iterrows 更快,但不是本机代码的数量级加速 @SamMason apply 确实有一些加速,请参阅文档中的注释:pandas.pydata.org/pandas-docs/stable/generated/…,但我调整了答案以说明您的观点 我添加了一些使用不同方法回答的示例。我对apply 似乎有多大帮助感到惊讶,我还认为它有花哨的加速/优化,但它似乎很容易被我认为会更慢的简单迭代器击败 是的,我已经阅读了该文档并使用过 Cython 几次......我还从一个更大的数据框开始,当我意识到它有多慢 @ 987654337@ 是!运行更多行会产生预期的线性差异,例如对于最后几个示例,1000 万行需要 1000 倍的时间(使用 apply 为 3.86 秒,最后一个 for 循环为 2.42 秒) @CharlesLandau 我可以从文档中重现相对性能,但我也可以通过更短且惯用的普通 Python 函数获得与他们不安全的 Cython 代码基本相同的性能。我想我会在 github 上提交一个问题【参考方案2】:

关于您的代码的一些信息:

所有这些global 变量都吓到我了!传递参数和返回状态有什么问题? 您没有使用来自 Pandas 的任何功能,创建一个数据框只是为了使用它对行进行愚蠢的迭代会导致它做很多不必要的工作 标准的csv 模块(可与delimiter='|' 一起使用)提供更紧密的界面,如果这确实是您可以做到的最佳方式

这对https://codereview.stackexchange.com/ 来说可能是一个更好的问题

只是玩一些替代工作方式的表现。从下面的带回家似乎是熊猫的“行明智”工作基本上总是很慢

首先创建一个数据框来测试:

import numpy as np
import pandas as pd

df = pd.DataFrame(np.random.randint(1, 1e6, (10_000, 2)))
df[1] = df[1].apply(str)

这需要 3.65 毫秒来创建一个包含 intstr 列的数据框。接下来我尝试iterrows 方法:

tot = 0
for i, row in df.iterrows():
    tot += row[0] / 1e5 < len(row[1])

聚合非常愚蠢,我只是想要使用两列的东西。它需要一个可怕的长 903 毫秒。接下来我尝试手动迭代:

tot = 0
for i in range(df.shape[0]):
    tot += df.loc[i, 0] / 1e5 < len(df.loc[i, 1])

这将其减少到 408 毫秒。接下来我试试apply

def fn(row):
    return row[0] / 1e5 < len(row[1])

sum(df.apply(fn, axis=1))

在 368 毫秒时基本相同。最后,我找到了一些 Pandas 满意的代码:

sum(df[0] / 1e5 < df[1].apply(len))

这需要 4.15 毫秒。以及我想到的另一种方法:

tot = 0
for a, b in zip(df[0], df[1]):
    tot += a / 1e5 < len(b)

这需要 2.78 毫秒。而另一个变体:

tot = 0
for a, b in zip(df[0] / 1e5, df[1]):
    tot += a < len(b)

需要 2.29 毫秒。

【讨论】:

以上是关于为啥我的 python DataFrame 执行如此缓慢的主要内容,如果未能解决你的问题,请参考以下文章

为啥我不能从我的 DataFrame 中的“日期”列中提取月份的列? [复制]

为啥我的 aws 胶水作业只使用一个执行器和驱动程序?

为啥我的 pandas DataFrame 列也是 Dataframes,而不是 Series?

为啥我不能从我的 python 脚本创建可执行文件?

如何用python将dataframe更新原来的sql表

[Spark][Python][DataFrame][SQL]Spark对DataFrame直接执行SQL处理的例子