我的用例是这样的:我有一个从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)
实际上是完成这一任务最慢的方法之一。
1条答案
按热度按时间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组合它们。