python 每行处理速度 VS 块中处理速度

python speed processing per line VS in chunk

我正在尝试对一个巨大的文件执行非常简单的计算,例如计算某些列的标签数量或其他列的平均值和标准偏差。该文件太大,无法放入内存,我目前正在按行处理它:

unique = {key: [] for key in categorical_keys}
means = {key: 0.0 for key in numerical_keys}
sds = {key: 0.0 for key in numerical_keys}
with open('input/train.csv', 'r') as read_file:
    reader = csv.DictReader(read_file, delimiter=',', quotechar='|')
    for i, row in enumerate(reader):
        for key, value in row.iteritems():
            if key in categorical_keys:
                if row[key] not in unique[key]:
                    unique[key].extend([value])
            elif key in numerical_keys:
                if value:
                    means[key] = (means[key]*i + float(value))/(i+1)
                    if i > 1:
                        sds[key] = (sds[key]*(i-1) + (float(value)-means[key])**2)/i

现在这似乎太慢了,我想知道在适合内存的块中处理它是否会更快。会更快吗?如果是,为什么?

感谢您的帮助。

循环优化

如果您需要提高速度:

  • 请确定,您确实需要加速(否则您会在无价值的任务上花费太多时间)
  • 从循环开始
    • 检查是否可以防止某些循环
    • optimize/remove 循环内指令
      • 每条指令都很重要
      • 每个引用计数

这是我的优化代码草稿(未测试):

from collections import defaultdict


unique = defaultdict(set)
means = {key: 0.0 for key in numerical_keys}
sds = {key: 0.0 for key in numerical_keys}
with open('input/train.csv', 'r') as read_file:
    reader = csv.DictReader(read_file, delimiter=',', quotechar='|')
    for i, row in enumerate(reader):
        for key in categorical_keys:
            unique[key].add(row[key])
        for key in numerical_keys:
            try:
                # shall throw ValueError if None or empty string
                value=float(row[key])
                mean_val = (means[key]*i + value)/(i+1)
                means[key] = mean_val
                # following fails for i < = 1 with ZeroDivisionError
                sds[key] = (sds[key]*(i-1) + (value-mead_val)**2)/i
            except (ValueError, ZeroDivisionError):
                pass

收集唯一值

您将 dict 与唯一值列表一起使用:

unique = {key: [] for key in categorical_keys}

并将唯一值作为列表项添加到它(发生在循环内):

if key in categorical_keys:
    if row[key] not in unique[key]:
        unique[key].extend([value])

如果您直接将该值添加到集合中,您可以安全地进行一些测试(如果该值存在于列表中)- 该集合会注意,那里只收集唯一值。

使用 defaultdict 将确保,如果您使用任何键,您已经有一些空集, 尚未使用。

不在每个循环中测试记录键的类型,提前知道它们

您的代码重复循环记录键并测试它们的类型,然后执行某些操作:

        if key in categorical_keys:
            if row[key] not in unique[key]:
                unique[key].extend([value])
        elif key in numerical_keys:
            if value:
                means[key] = (means[key]*i + float(value))/(i+1)
                if i > 1:
                    sds[key] = (sds[key]*(i-1) + (float(value)-means[key])**2)/i

如果您的 categorical_keysnumerical_keys 正确设置为 现有值。然后就可以直接循环遍历已知键名了:

        for key in categorical_keys:
            unique[key].add(row[value])
        for key in numerical_keys:
            try:
                # shall throw ValueError if None or empty string
                value=float(row[value])
                means[key] = (means[key]*i + value)/(i+1)
                if i > 1:
                    sds[key] = (sds[key]*(i-1) + (value-means[key])**2)/i
            except ValueError:
                pass

重新使用一次计算值

您的代码重复计算值:

float(value)

做一次重复使用。

另外mean[key]的值也计算一次并设置到means[key],并且 两行之后再次使用该值。最好将值存储在局部变量中 并使用它两次。任何查找(如 means[key])都需要一些费用。

捕获异常通常比值测试快

您的代码测试值是否为空:

        elif key in numerical_keys:
            if value:
                # something here

您可以将其替换为直接与值一起使用的代码。如果值错误,它将 失败和异常 ValueError 将被捕获并忽略。如果您设置了大多数值,这将加快速度 向上。

            try:
                value=float(value)
                means[key] = (means[key]*i + value)/(i+1)
                if i > 1:
                    sds[key] = (sds[key]*(i-1) + (value-means[key])**2)/i
            except ValueError:
                pass

你能阻止 if i > 1: 测试吗?

这个条件在大多数情况下是正确的,但是你在每个循环中测试它。如果你找到一种方法(我没有) 阻止这个测试,你也可以更快地得到它。

正如您所建议的,我们可以通过捕获 ZeroDivisionError for i <= 1:

来解决它
            try:
                # shall throw ValueError if None or empty string
                value=float(value)
                means[key] = (means[key]*i + value)/(i+1)
                # for i <= 1 shall raise ZeroDivisionError
                sds[key] = (sds[key]*(i-1) + (value-means[key])**2)/i
            except (ValueError, ZeroDivisionError):
                pass

正在分块处理数据

关于分块处理:

  • 它确实增加了一些复杂性
  • 如果小心操作,它可能会加快程序速度
  • 它可能会减慢速度或提供错误的结果

分块可以提高速度的地方

读取较大的文件

这听起来很明显,但图书馆已经解决了。期待轻微或 none 改进。

获取块中的 CSV 记录

我不知道 csv.readercsv.DictReader 的方法允许获取 直接记录大块。你必须自己做。这是可能的,我 建议使用 itertools.groupby.

不要指望它本身的加速(它会减慢一点),但它是以后其他基于块的加速的先决条件。

将值块添加到集合中

代码(当前)将值一个一个地添加到集合中。如果你用块做 值(越多越好),它会更快,因为每个 python 调用都有 一些小开销。

计算平均值和标准差值

你可以利用 statistics 包,它可能有 优化代码(但它似乎是纯粹的 python)。

无论如何,因为您要分块处理数据,很简单 statistics.mean 对您不起作用,否则您必须汇总 结果以某种方式在一起(如果可能的话)。

如果你自己计算这个值,通过仔细编码你可以得到一些 加速,主要基于事实,你直接在一个块中获取值并且 不必在每个循环中逐个取消引用值。

分块结论

对我来说,分块优化似乎太复杂了,很难做到 预测,如果它带来任何价值。

Jan 的回答很好,但是如果你想加快速度,你可以在下面进行自己的研究:

执行的逐行分析:如果你不知道什么地方慢,你就无法进一步改进 (https://github.com/rkern/line_profiler)

由于您的大部分代码都是数值运算,您可能会在类型检查开销上浪费大量时间。这里有两条路径 - 用类型装饰代码并使用 Cython, or leave the code as it is and use Pypy - 两者都应该给你一个很好的提升。