如何在 Pandas 中使用 groupby 对一个列中的多个值进行分组?

5

我有一个如下的数据框:

import pandas as pd

data = {
    'brand': ['Mercedes', 'Renault', 'Ford', 'Mercedes', 'Mercedes', 'Mercedes', 'Renault'],
    'model': ['X', 'Y', 'Z', 'X', 'X', 'X', 'Q'],
    'year': [2011, 2010, 2009, 2010, 2012, 2020, 2011],
    'price': [None, 1000.4, 2000.3, 1000.0, 1100.3, 3000.5, None]
}

df = pd.DataFrame(data)
print(df)

      brand model  year   price
0  Mercedes     X  2011     NaN
1   Renault     Y  2010  1000.4
2      Ford     Z  2009  2000.3
3  Mercedes     X  2010  1000.0
4  Mercedes     X  2012  1100.3
5  Mercedes     X  2020  3000.5
6   Renault     Q  2011     NaN

以下是另一个案例,用于测试您的解决方案:

data = {
    'brand': ['Mercedes', 'Mercedes', 'Mercedes', 'Mercedes', 'Mercedes'], 
    'model': ['X', 'X', 'X', 'X', 'X'], 'year': [2017, 2018, 2018, 2019, 2019], 
    'price': [None, None, None, 1000.0, 1200.50]
}

期望输出结果:

      brand model  year    price
0  Mercedes     X  2017      NaN
1  Mercedes     X  2018  1100.25
2  Mercedes     X  2018  1100.25
3  Mercedes     X  2019  1000.00
4  Mercedes     X  2019  1200.50

我希望使用包含前一年、当年和后一年的同品牌和型号的观测值平均值来填补缺失值。例如,梅赛德斯 X 型在 2011 年的价格为空缺。查看数据后,我将计算出相应缺失值的平均值。
2011 - 1 = 2010
2011 + 1 = 2012

The 4th observation -> Mercedes,X,2010,1000.0
The 5th observation -> Mercedes,X,2012,1100.3

The mean -> (1000.0 + 1100.3) / 2 = 1050.15

我尝试了如下内容:
for c_key, _ in df.groupby(['brand', 'model', 'year']):
    fc = (
        (df['brand'] == c_key[0])
        & (df['model'] == c_key[1])
        & (df['year'].isin([c_key[2] + 1, c_key[2], c_key[2] - 1]))
    )
    
    sc = (
        (df['brand'] == c_key[0])
        & (df['model'] == c_key[1])
        & (df['year'] == c_key[2])
        & (df['price'].isnull())
    )
        
    mean_val = df[fc]['price'].mean()

    df.loc[sc, 'price'] = mean_val

print(df)

      brand model  year    price
0  Mercedes     X  2011  1050.15
1   Renault     Y  2010  1000.40
2      Ford     Z  2009  2000.30
3  Mercedes     X  2010  1000.00
4  Mercedes     X  2012  1100.30
5  Mercedes     X  2020  3000.50
6   Renault     Q  2011      NaN

但是这种解决方案对于90,000行和27列需要很长时间,因此是否有更有效的解决方案?例如,我可以使用groupby对于year-1、year、year+1、brand和model的值吗?

提前致谢。

5个回答

2

我认为更有效的方法是按照品牌年份排序,然后使用插值

df = df.sort_values(['brand', 'year']).groupby('brand').apply(lambda g: g.interpolate(limit_area='inside'))

输出:

>>> df
      brand model  year    price
0  Mercedes     X  2011  1050.15
1   Renault     Y  2010  1000.40
2      Ford     Z  2009  2000.30
3  Mercedes     X  2010  1000.00
4  Mercedes     X  2012  1100.30
5  Mercedes     X  2020  3000.50
6   Renault     Q  2011  1000.40

这也处理了所有列。


这解决了你的问题吗? :) - user17242583
谢谢您的回答,但它似乎不正确。例如,雷诺Q型的价格应该为null,因为这个品牌和型号没有其他观察结果。此外,数据可能包含同一品牌和型号的许多年份。例如,梅赛德斯X型的年份字段可以是2009年、2008年等,但我只需要年份-1、年份和年份+1。 - E. Zeytinci
@richardec 抱歉,我想我没有理解 interpolate 方法。您能否请稍微解释一下这个方法呢? - E. Zeytinci
哇,好建议 @d.b!已融入我的回答中。 - user17242583
@E.Zeytinci请现在检查答案 :) - user17242583
显示剩余3条评论

2

基于@richardec的解决方案,但加入了一些内容以纠正下一年价格已知的情况。不确定它是否比您的原始解决方案更快。

# Make an interpolated average 
df_out = df.sort_values(['brand', 'year']).groupby('brand').apply(lambda g: g.interpolate(limit_area='inside'))

# Make an average per brand/year/model
df1 = df.sort_values(['brand', 'year']).groupby(['brand','year','model']).mean().reset_index()

# Check if the next line has the same brand and model. If so, take the next average price when the price isNa
mask1 = df1["model"] == df1["model"].shift(-1)
mask2 = df1["brand"] == df1["brand"].shift(-1)
mask3 = df1["price"].isna()
df1["priceCorr"] = np.where(mask1 & mask2 & mask3 ,df1["price"].shift(-1),df1["price"] )

# Merge everything together
df_out = df_out.merge(df1[["brand", "year", "model","priceCorr"]], on=["brand", "year", "model"])
df_out["price"] = np.where(df_out["price"].isna(),df_out["priceCorr"], df_out["price"])

2

这里提供一种看起来更简单的解决方案:

  1. Sort values in the original dataframe:

    df = df.sort_values(["brand", "model", "year"])
    
  2. Group by "brand" and "model", and store the groups in a variable (to calculate only once):

    groups = df.groupby(["brand", "model"])
    
  3. Fill nan values using the average of the previous and next rows (Important: this assumes that you have data of consecutive years, meaning that if you're missing data for 2015 you know the values of 2014 and 2016. If you have no data for consecutive years, null values will remain null).

    df["price"] = df["price"].fillna((groups["price"].ffill(limit=1) + groups["price"].bfill(limit=1)) / 2)
    
结果代码:
df = df.sort_values(["brand", "model", "year"])
groups = df.groupby(["brand", "model"])
df["price"] = df["price"].fillna((groups["price"].ffill(limit=1) + groups["price"].bfill(limit=1)) / 2)
print(df)

输出:

      brand model  year    price
2      Ford     Z  2009  2000.30
3  Mercedes     X  2010  1000.00
0  Mercedes     X  2011  1050.15
4  Mercedes     X  2012  1100.30
5  Mercedes     X  2020  3000.50
6   Renault     Q  2011      NaN
1   Renault     Y  2010  1000.40

非常感谢您的回答。正如您所说,这假定我们有连续年份的数据,但不幸的是,对于我的真实数据并非如此。您有任何想法来应对这种情况吗? - E. Zeytinci
这要看你希望如何处理这种情况了。 - aaossa
是的,你说得对。我只想用年份-1、年份和年份+1的均值来填充空值。所以如果前面的值比年份-1低,我不想处理它。 - E. Zeytinci
目前,我的答案恰好做到了这一点。例如,如果奔驰在2011年和2012年为null,则两个空值在输出中仍将保持为null。 - aaossa
但是有2019年的数据。正如我在问题中提到的,我需要查看前一年、今年和后一年的数据。当我们处理2018年的观测数据时,我们需要查看2017年、2018年和2019年的数据。你试过我的不好的解决方案吗?你可以用这个解决方案看到正确的答案。它可以正确地工作,但速度非常慢。 - E. Zeytinci
显示剩余2条评论

1
def fill_it(x):
    return df[(df.brand==df.iat[x,0])&(df.model==df.iat[x,1])&((df.year==df.iat[x,2]-1)|(df.year==df.iat[x,2]+1))].price.mean()



df = df.apply(lambda x: x.fillna(fill_it(x.name)), axis=1)
df



Output 1:
    brand   model   year    price
0   Mercedes    X   2011    1050.15
1   Renault     Y   2010    1000.40
2   Ford        Z   2009    2000.30
3   Mercedes    X   2010    1000.00
4   Mercedes    X   2012    1100.30
5   Mercedes    X   2020    3000.50
6   Renault     Q   2011    NaN


Output 2:
    brand   model   year    price
0   Mercedes    X   2017    NaN
1   Mercedes    X   2018    1100.25
2   Mercedes    X   2018    1100.25
3   Mercedes    X   2019    1000.00
4   Mercedes    X   2019    1200.50

这是3倍快

df.loc[df.price.isna(), 'price'] = df[df.price.isna()].apply(lambda x: x.fillna(fill_it(x.name)), axis=1)

我尝试了另一种方法,使用pd.rolling,速度更快(在包含70k行的数据帧上运行只需200ms)。输出结果仍然符合您的要求。

df.year = pd.to_datetime(df.year, format='%Y')
df.sort_values('year', inplace=True)
df.groupby(['brand', 'model']).apply(lambda x: x.fillna(x.rolling('1095D',on='year', center=True).mean())).sort_index()

它的功能是正确的,但不幸的是速度不够快。如果您尝试使用data = {'brand': ['Mercedes', 'Renault', 'Ford', 'Mercedes', 'Mercedes', 'Mercedes', 'Renault'] * 10000, 'model': ['X', 'Y', 'Z', 'X', 'X', 'X', 'Q'] * 10000, 'year': [2011, 2010, 2009, 2010, 2012, 2020, 2011] * 10000, 'price': [None, 1000.4, 2000.3, 1000.0, 1100.3, 3000.5, None] * 10000},您会发现它需要很长时间。 - E. Zeytinci
我编辑了答案并采用了不同的方法 - 使用 pd.rolling() 和 3年移动平均来加速它。 - Yolao_21

1

这不是一个很好的解决方案,但根据您的描述,我相信它可以运行并且非常快速。只需在排序数据框中使用大量的if语句和np.where即可。

import pandas as pd
import numpy as np

data = pd.DataFrame({
    'brand': ['Mercedes', 'Renault', 'Ford', 'Mercedes', 'Mercedes', 'Mercedes', 'Renault'],
    'model': ['X', 'Y', 'Z', 'X', 'X', 'X', 'Q'],
    'year': [2011, 2010, 2009, 2010, 2012, 2020, 2011],
    'price': [None, 1000.4, 2000.3, 1000.0, 1100.3, 3000.5, None]
})

data = data.sort_values(by=['brand', 'model', 'year'])
data['adjusted_price'] = np.where(data['price'].isnull() & 
                                  (data['brand']==data['brand'].shift(1)) & (data['brand']==data['brand'].shift(-1)) &
                                  (data['model']==data['model'].shift(1)) & (data['model']==data['model'].shift(-1)) & 
                                  (data['year']==(data['year'].shift(1)+1))&(data['year']==(data['year'].shift(-1)-1)),
                                  (data['price'].shift(1)+data['price'].shift(-1))/2,
                                  data['price'])
data['price'] = data['adjusted_price']
data = data.drop(['adjusted_price'], axis=1)

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