python 迭代pandas DataFrame创建自定义对象的最快方法是什么?

olhwl3o2  于 2023-06-20  发布在  Python
关注(0)|答案(1)|浏览(135)

我的用例是这样的:我有一个从SQL数据库加载的pandas DataFrame。我想从每一行构造一个对象。您可能会认为应该使用df.apply,但实际上这非常慢,稍后将演示。我不知道最快的方法是什么。
为了弄清楚什么是最快的,我构建了一个repo来测试各种函数,但我不知道我是否遗漏了什么。
我的测试设置如下:
创建一个函数列表,该函数以DataFrame作为输入并输出Node对象列表。
设置一个随机种子,使每个函数都获得相同的输入DataFrame。
构造大小为N的DataFrame(从2、4、8、16、32、64、128、256、512、1024、2048、4096、8192、16384、32768)。
然后,这个DataFrame被传递到我们的测试函数中,该函数调用具有以下签名的函数:create_node(b, a, f, e, d, c, g)create_node_ignored_args(b, a, f, e, d, c, g, *args, **kwargs)
这些函数创建一个Node对象(类__init__签名:def __init__(self, node_id, b, a, f, c, d, e, g))。
请注意,DataFrame包含这些列b, a, f, e, d, c, g,但不是按此顺序。
重要的是我们能够以正确的顺序传递参数,否则Node将被错误地构造。
例如,如果你有一个DataFrame:{"a":1, "b":2, "c":3, "d":4, "e":5, "f":6: "g":{"subset": [1,2,3,4]}},你只是天真地把它作为 *args传递给create_node,你最终会得到一个不正确的Node,其中对象中的b字段被设置为a,依此类推。
如果将参数作为**kwargs传递,则可以避免这种情况,这将使DataFrame中的列与函数中的确切参数相匹配。
请注意,如果DataFrame中有太多列,那么调用所有列的create_node将失败,因为参数太多,这就是为什么我们还有create_node_ignored_args,如果有太多的args或不匹配的kwargs,它可以忽略args/kwargs。
下面是一些示例函数:

def index_df_apply(df):
    """Use apply, get the fields indexing using LOOKUP.

    Use as args in create_node (using *).
    """
    nodes = df.apply(lambda row: create_node(*row[LOOKUP]), axis=1)
    return [node for node in nodes]

def itertuples(df):
    """Loop over itertuples, convert namedtuple to dict, get fields using tuple_unwrap fn according to LOOKUP.

    Use as kwargs in create_node (using **).
    """
    nodes = []
    for row in df.itertuples(index=False):
        nodes.append(create_node(**tuple_unwrap(row, LOOKUP)))
    return nodes

def zip_comprehension_lookup(df):
    """List comprehension over zipped df columns, get fields using * and LOOKUP.

    Use as args in create_node (using *).
    """
    nodes = [create_node(*args) for args in zip(*(df[c] for c in LOOKUP))]
    return nodes

我们有两个案例要测试:大 Dataframe 在较少迭代中的性能,以及小 Dataframe 在许多迭代中的性能。我们将在较少的迭代中对大 Dataframe 进行可视化,并提供一个文本报告,但只在多次迭代中为小 Dataframe 提供文本报告。
对于大型DataFrames 1迭代,我们可以创建两个直接的perfplot图像:

以下是这些图像的一些大致等效的时序数据:https://github.com/Atheuz/pandas-to-object-perf-test/blob/master/time_one_iteration_count.txt
从这个时序测试中,我们看到最快的函数是:zip_comprehension_np_values_lookup,zip_comprehension_lookup,zip_comprehension_direct_access,to_numpy_direct_access,itertuples_direct_access_comprehension.
这些功能是:

def zip_comprehension_np_values_lookup(df):
    """List comprehension over zipped df columns, get fields using *, LOOKUP, use .values.

    Use as args in create_node (using *).
    """
    nodes = [create_node(*args) for args in zip(*(df[c].values for c in LOOKUP))]
    return nodes

def zip_comprehension_direct_access(df):
    """List comprehension over zip object, get fields using direct indexing.

    Use as args in create_node (using *).
    """
    nodes = [create_node(*args) for args in zip(df["b"], df["a"], df["f"], df["e"], df["d"], df["c"], df["g"])]
    return nodes

def zip_comprehension_lookup(df):
    """List comprehension over zipped df columns, get fields using * and LOOKUP.

    Use as args in create_node (using *).
    """
    nodes = [create_node(*args) for args in zip(*(df[c] for c in LOOKUP))]
    return nodes

def to_numpy_direct_access(df):
    """Get the names of columns in our dataframe, create indices lookup from name->idx, convert df to numpy and access fields using indices lookup.

    Use as kwargs in create_node (using direct assignment).
    """
    cols = list(df.columns)
    indices = {k: cols.index(k) for k in cols}
    nodes = [
        create_node(
            b=row[indices["b"]],
            a=row[indices["a"]],
            f=row[indices["f"]],
            e=row[indices["e"]],
            d=row[indices["d"]],
            c=row[indices["c"]],
            g=row[indices["g"]],
        )
        for row in df.to_numpy()
    ]
    return nodes

def itertuples_direct_access_comprehension(df):
    """List comprehension over itertuples, get fields accessing them directly.

    Use as kwargs in create_node (using direct assignment).
    """
    nodes = [create_node(b=row.b, a=row.a, f=row.f, e=row.e, d=row.d, c=row.c, g=row.g) for row in df.itertuples(index=False)]
    return nodes

类似地,另一种情况下的文本报告,我们想要测试具有高迭代次数的小DataFrames:https://github.com/Atheuz/pandas-to-object-perf-test/blob/master/time_high_iteration_count.txt
从这个时序测试中,似乎最快的功能是:
zip_comprehension_np_values_lookup,zip_comprehension_direct_access,zip_comprehension_lookup,to_numpy_direct_access,to_numpy_take
这种情况下唯一的新功能是:numpy_take

def to_numpy_take(df):
    """Get the names of columns in our dataframe, create indices lookup from name->idx using LOOKUP, convert df to numpy and access fields using the indices lookup using np.take.

    Use as args in create_node (using *).
    """
    cols = list(df.columns)
    indices = [cols.index(k) for k in LOOKUP]
    nodes = [create_node(*np.take(row, indices)) for row in df.to_numpy()]
    return nodes

有关更多信息,请访问我的GitHub repo:https://github.com/Atheuz/pandas-to-object-perf-test
无论如何,我的问题仍然存在:是否有一种普遍推荐的快速方法来从pandas DataFrames创建对象,或者我偶然发现了它?访问所需的DataFrame列并将它们压缩在一起。我也很困惑为什么df.apply(lambda row: create_node(*row[LOOKUP]), axis=1)实际上是完成这一任务最慢的方法之一。

jdzmm42g

jdzmm42g1#

根据您的测试,从pandas DataFrame创建对象的最快方法似乎是访问所需的列,并使用zip将它们组合成对象构造函数的参数。具体来说,以下函数在您的测试中表现良好:

  • zip_comprehension_np_values_lookup
  • zip_comprehension_direct_access
  • zip_comprehension_lookup
  • to_numpy_direct_access
  • to_numpy_take

目前还不完全清楚为什么df.apply(lambda row: create_node(*row[LOOKUP]), axis=1)是测试中最慢的方法之一。一种可能的解释是apply创建了大量开销,因为它实际上是在DataFrame中循环并对每行应用一个函数。另一方面,您测试的其他方法更有效,因为它们直接访问所需的列并使用zip组合它们。

相关问题