pandas 使用groupby填充缺失值的有效方法

aiazj4mn  于 2023-02-20  发布在  其他
关注(0)|答案(2)|浏览(162)

我有一个一百万行的 Dataframe
Dataframe 包括ID、FDAT、LACT列
每个ID可能有多个FDAT和LACT。该ID的每个LACT的FDAT应该相同。有时候会有一个缺失的FDAT,我想用该ID中与该LACT匹配的FDAT来填充
示例数据

ID  FDAT      LACT
1   1/1/2020    1
1   1/1/2020    1
1   1/1/2021    2
1   NA          2
1   1/1/2021    2
1   1/1/2022    3

在本例中,NA应为1/1/2021
我使用下面的代码来完成这个任务。不幸的是它非常慢。我只想填充缺失的值。我不想改变任何非空的FDAT条目。

df.sort_values(["ID",'DATE'], inplace=True)

df.loc[:, 'FDAT'] = df.groupby(['ID','LACT']).fillna(method="ffill")
df.loc[:, 'FDAT'] = df.groupby(['ID','LACT']).fillna(method="bfill")

我在寻找能做同样事情但运行速度更快的代码。

agxfikkp

agxfikkp1#

下面是一些矢量化代码,它可以在一秒钟内处理100万行。

def fillna_fdat(df):
    a = df.set_index(['ID', 'LACT'])['FDAT']
    b = a.dropna()
    return df.assign(
        FDAT=a.fillna(b[~b.index.duplicated(keep='first')]).to_numpy()
    )

应用于示例输入数据:

df = pd.DataFrame({
    'ID': [1, 1, 1, 1, 1, 1],
    'FDAT': [
        '1/1/2020', '1/1/2020', '1/1/2021', float('nan'),
        '1/1/2021', '1/1/2022'],
    'LACT': [1, 1, 2, 2, 2, 3],
})

>>> fillna_fdat(df)
   ID      FDAT  LACT
0   1  1/1/2020     1
1   1  1/1/2020     1
2   1  1/1/2021     2
3   1  1/1/2021     2
4   1  1/1/2021     2
5   1  1/1/2022     3

解释

基本思想是对(ID, LACT): FDAT进行一个干净的Map,为了高效地实现这一点,我们使用df的一个版本,其中索引由[ID, LACT]组成:

a = df.set_index(['ID', 'LACT'])['FDAT']
>>> a
ID  LACT
1   1       1/1/2020
    1       1/1/2020
    2       1/1/2021
    2            NaN
    2       1/1/2021
    3       1/1/2022

我们删除NaN值和重复的索引:

b = a.dropna()
c = b[~b.index.duplicated(keep='first')]
>>> c
ID  LACT
1   1       1/1/2020
    2       1/1/2021
    3       1/1/2022

现在,我们可以将a中的所有NaN替换为来自c的值,其中c对应于相同的索引['ID', 'LACT']

d = a.fillna(b[~b.index.duplicated(keep='first')])
>>> d
ID  LACT
1   1       1/1/2020
    1       1/1/2020
    2       1/1/2021
    2       1/1/2021  <-- this was filled from d.loc[(1,2)]
    2       1/1/2021
    3       1/1/2022

此时,我们只想获取这些值,它们的顺序与原始df中的顺序相同,并且在用这些值替换df['FDAT']时忽略索引(因此是.to_numpy()部分)。为了使原始df保持不变(我强烈反对任何更改输入的代码,除非明确声明),我们使用习惯用法df.assign(FDAT=...)派生一个新的df,并返回它。给出了上面的函数。

其他观察结果

注意,其他列(如果有的话)会被保留,为了说明这一点并度量性能,让我们编写一个随机数df的生成器:

def gen(n, k=None):
    nhalf = n // 2
    k = n // 3 if k is None else k
    df = pd.DataFrame({
        'ID': np.random.randint(0, k, nhalf),
        'FDAT': [f'1/1/{y}' for y in np.random.randint(2010, 2012+k, nhalf)],
        'LACT': np.random.randint(0, k, nhalf),
    })
    df = pd.concat([
        df,
        df.assign(FDAT=np.nan),
    ]).sample(frac=1).reset_index(drop=True).assign(
        other=np.random.uniform(size=2*nhalf)
    )
    return df

小例子:

np.random.seed(0)  # reproducible example
df = gen(10)

>>> df
   ID      FDAT  LACT     other
0   0  1/1/2010     2  0.957155
1   1  1/1/2014     0  0.140351
2   1  1/1/2010     2  0.870087
3   1       NaN     1  0.473608
4   0       NaN     2  0.800911
5   0  1/1/2012     2  0.520477
6   1       NaN     2  0.678880
7   1       NaN     0  0.720633
8   0       NaN     2  0.582020
9   1  1/1/2014     1  0.537373

>>> fillna_fdat(df)
   ID      FDAT  LACT     other
0   0  1/1/2010     2  0.957155
1   1  1/1/2014     0  0.140351
2   1  1/1/2010     2  0.870087
3   1  1/1/2014     1  0.473608
4   0  1/1/2010     2  0.800911
5   0  1/1/2012     2  0.520477
6   1  1/1/2010     2  0.678880
7   1  1/1/2014     0  0.720633
8   0  1/1/2010     2  0.582020
9   1  1/1/2014     1  0.537373

速度

np.random.seed(0)
df = gen(1_000_000)

%timeit fillna_fdat(df)
# 806 ms ± 13.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

一百万行不到一秒。

jdg4fx2g

jdg4fx2g2#

如下图所示,我给出了一个更快的替代方案,以及原始时间和计算时间:

import pandas as pd

data = {'ID': [1, 1, 1, 1, 1, 1],
        'FDAT': ['1/1/2020', '1/1/2020', '1/1/2021', None, '1/1/2021', '1/1/2022'],
        'LACT': [1, 1, 2, 2, 2, 3]}

df = pd.DataFrame(data)
import time

start_time = time.time()

df.sort_values(["ID", "FDAT", "LACT"], inplace=True)
df["FDAT"] = df.groupby(["ID", "LACT"])["FDAT"].transform(lambda x: x.fillna(method="ffill"))

print(df)

end_time = time.time()
print("Execution time:", end_time - start_time, "seconds")

返回:

ID      FDAT  LACT
0   1  1/1/2020     1
1   1  1/1/2020     1
2   1  1/1/2021     2
4   1  1/1/2021     2
5   1  1/1/2022     3
3   1  1/1/2021     2
Execution time: 0.008013486862182617 seconds

而您的解决方案:

import pandas as pd

data = {'ID': [1, 1, 1, 1, 1, 1],
        'FDAT': ['1/1/2020', '1/1/2020', '1/1/2021', None, '1/1/2021', '1/1/2022'],
        'LACT': [1, 1, 2, 2, 2, 3]}

df = pd.DataFrame(data)
import time

start_time = time.time()

df.loc[:, 'FDAT'] = df.groupby(['ID','LACT']).fillna(method="ffill")
df.loc[:, 'FDAT'] = df.groupby(['ID','LACT']).fillna(method="bfill")
print(df)

end_time = time.time()
print("Execution time:", end_time - start_time, "seconds")

退货:

ID      FDAT  LACT
0   1  1/1/2020     1
1   1  1/1/2020     1
2   1  1/1/2021     2
3   1  1/1/2021     2
4   1  1/1/2021     2
5   1  1/1/2022     3
Execution time: 0.011833429336547852 seconds

因此,同时使用transformfffill大约要快1.5倍。注意,在您的代码示例中,sort_values()不包括在时间中。因此,我估计使用我建议的方法应该要快2.5倍。

相关问题