从Pandas DataFrame高效地创建元组系列

3
我正在使用apply()从现有DataFrame的值构建一个Series元组。我需要按照特定顺序构建元组中的值,并将除一列外的所有NaN替换为'{}'
以下函数可用于产生期望的结果,但执行速度较慢:
def build_insert_tuples_series(row):
    # Here I attempt to handle ordering the final tuple
    # I must also replace NaN with "{}" for all but v2 column.
    vals = [row['v2']]
    row_sans_v2 = row.drop(labels=['v2'])
    row_sans_v2.fillna("{}", inplace=True)
    res = [val for val in row_sans_token]
    vals += res
    return tuple(vals)

def generate_insert_values_series(df):
    df['insert_vals'] = df.apply(lambda x: build_insert_tuples_series(x), axis=1)
    return df['insert_vals']

原始数据框:

    id   v1    v2
0  1.0  foo  quux
1  2.0  bar   foo
2  NaN  NaN   baz

调用generate_insert_values_series(df)后产生的DataFrame:
最终元组的顺序逻辑为(v2, ..所有其他列..)
    id   v1    v2       insert_vals
0  1.0  foo  quux  (quux, 1.0, foo)
1  2.0  bar   foo   (foo, 2.0, bar)
2  NaN  NaN   baz     (baz, {}, {})

定时生成结果数据框的函数:

%%timeit
generate_insert_values_series(df)
100 loops, best of 3: 2.69 ms per loop

我感觉可能有更有效率的方法来构建Series,但不确定如何使用向量化或其他方法来优化操作。


1
最终元组的顺序逻辑是什么? - ASGM
最终元组的排序逻辑为 (v2, ..所有其他列..) - Wes Doyle
3个回答

3

zipgetmaskfillnasorted

这是一条有用的代码一句话总结。

df.assign(
    insert_vals=
    [*zip(*map(df.mask(df.isna(), {}).get, sorted(df, key=lambda x: x != 'v2')))])

    id   v1    v2       insert_vals
0  1.0  foo  quux  (quux, 1.0, foo)
1  2.0  bar   foo   (foo, 2.0, bar)
2  NaN  NaN   baz     (baz, {}, {})

更少的一行代码

get = df.mask(df.isna(), {}).get
key = lambda x: x != 'v2'
cols = sorted(df, key=key)

df.assign(insert_vals=[*zip(*map(get, cols))])

    id   v1    v2       insert_vals
0  1.0  foo  quux  (quux, 1.0, foo)
1  2.0  bar   foo   (foo, 2.0, bar)
2  NaN  NaN   baz     (baz, {}, {})

这应该适用于旧版Python。

get = df.mask(df.isna(), {}).get
key = lambda x: x != 'v2'
cols = sorted(df, key=key)

df.assign(insert_vals=zip(*map(get, cols)))

看起来很不错 - 我认为这种解包语法可能只适用于Python 3? - Wes Doyle
1
我已经更新了我的帖子,并提供了在Python 2中可用的代码。 - piRSquared
非常好,只需要进行一处小修改以适应我的需求:get = df.mask(df.isna(), "{}").get - Wes Doyle
1
@WesDoyle 我以为那是一个字典字面量。在这种情况下,您可以使用 get = df.fillna("{}").get - piRSquared
啊,谢谢 - 我更新了我的问题以反映“{}”。 - Wes Doyle

2

首先,您可以使用numpynull值替换为dicts

import pandas as pd
import numpy as np

temp = pd.DataFrame({'id':[1,2, None], 'v1':['foo', 'bar', None], 'v2':['quux', 'foo', 'bar']})

def replace_na(col): 
    return np.where(temp[col].isnull(), '{}', temp[col])

def generate_tuple(df):
    df['id'], df['v1'] = replace_na('id'), replace_na('v1')
    return df.apply(lambda x: tuple([x['v2'], x['id'], x['v1']]), axis=1)

你的收益是

%%timeit
temp['insert_tuple'] = generate_tuple(temp)
>>>> 1000 loops, best of 3 : 1ms per loop

如果您将generate_tuple return更改为类似以下内容
def generate_tuple(df):
    df['id'], df['v1'] = replace_na('id'), replace_na('v1')
    return list(zip(df['v2'], df['id'], df['v1']))

您的收益变为:
%%timeit
temp['insert_tuple'] = generate_tuple(temp)
1000 loops, best of 3 : 674 µs per loop

2

您不应该这样做,因为您的新系列将失去所有矢量化功能。

但是,如果您必须这样做,可以通过使用pd.DataFrame.itertuples、列表推导式或map来避免在此处使用apply。唯一的复杂之处在于重新排序列,您可以通过转换为list来完成:

df = pd.concat([df]*10000, ignore_index=True)

col_lst = df.columns.tolist()
cols = [col_lst.pop(col_lst.index('v2'))] + col_lst

%timeit list(df[cols].itertuples(index=False))  # 31.3 ms per loop
%timeit [tuple(x) for x in df[cols].values]     # 74 ms per loop
%timeit list(map(tuple, df[cols].values))       # 73 ms per loop

上面的基准测试是在Python 3.6.0上进行的,但即使在2.7上,您可能会发现这些方法比apply更有效率。请注意,在最终版本中不需要将list转换,因为map在v2.7中返回一个list
如果绝对必要,您可以通过系列使用fillna
s = pd.Series([{} for _ in range(len(df.index))], index=df.index)

for col in df[cols]:
    df[cols].fillna(s)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接