第四章

零、练一练

练一练

请在learn_pandas数据集上按学校分组统计体重的均值。

df = pd.read_csv('data/learn_pandas.csv')
df.groupby("School")["Weight"].mean()
School
A    56.442308
B    55.666667
C    54.000000
D    54.223881
Name: Weight, dtype: float64

练一练

请根据0.25分位数和0.75分位数进行分割,将体重分为high、normal和low这3组,统计身高的均值。

q25 = df.Weight.quantile(0.25)
q75 = df.Weight.quantile(0.75)
w_dict = {0:"low", 1:"normal", 2:"high"}
condition = ((df.Weight > q25)*1 + (df.Weight > q75)*1).replace(w_dict)
df.groupby(condition)["Height"].mean()
Weight
high      174.935714
low       155.891071
normal    162.255294
Name: Height, dtype: float64

练一练

在learn_pandas数据集中,Transfer列的元素为“N”时表示该名同学不是转系生,请按照学校和年级两列分组,找出所有不含转系生的组对应的学校和年级。

res = (df.Transfer=="N").groupby([df.School, df.Grade]).all()
res[~res].index
MultiIndex([('A',    'Senior'),
            ('B',  'Freshman'),
            ('B',    'Junior'),
            ('B',    'Senior'),
            ('B', 'Sophomore'),
            ('C',    'Junior'),
            ('C',    'Senior'),
            ('D',  'Freshman'),
            ('D',    'Junior'),
            ('D',    'Senior'),
            ('D', 'Sophomore')],
           names=['School', 'Grade'])

练一练

请使用传入字典的方法完成与gb.agg(['max', 'min'])等价的聚合任务。

gb = df.groupby('Gender')[['Height', 'Weight']]
gb.agg({"Height": ['max', 'min'], "Weight": ['max', 'min']})
Height Weight
max min max min
Gender
Female 170.2 145.4 63.0 34.0
Male 193.9 155.7 89.0 51.0

练一练

在groupby对象上可以使用describe()方法进行统计信息汇总,请同时使用多个聚合函数,完成与该方法相同的功能。

gb = df.groupby('Gender')[["Height", "Weight"]]
gb.agg(["count", "mean", "std", "min",
       ("25%", lambda x: x.quantile(0.25)),
       ("50%", lambda x: x.quantile(0.5)),
       ("75%", lambda x: x.quantile(0.75)), "max"])
Height Weight
count mean std min 25% 50% 75% max count mean std min 25% 50% 75% max
Gender
Female 132 159.19697 5.053982 145.4 155.675 159.6 162.825 170.2 135 47.918519 5.405983 34.0 44.0 48.0 52.00 63.0
Male 51 173.62549 7.048485 155.7 168.900 173.4 177.150 193.9 54 72.759259 7.772557 51.0 69.0 73.0 78.75 89.0

练一练

在agg()中能够使用聚合字符串的地方,我们都可以传入返回标量值的自定义函数,请自行构造一个相关的例子。

gb = df.groupby('Gender')[["Height", "Weight"]]
gb.agg(lambda x: x.skew())
Height Weight
Gender
Female -0.219253 -0.268482
Male 0.437535 -0.332393

练一练

在groupby对象中,rank()方法也是一个实用的变换函数,请在官方文档中查阅它的功能并给出1个使用的例子。

groupby上的rank()函数用于组内排名,一个典型的例子是全年级某次数学期末考试,需要计算每个学生在班级内的排名:

df = pd.DataFrame({
    "Student ID": ["ID-%02d"%i for i in range(1, 151)],
    "Class": np.random.randint(1,6,150), # 5个班级
    "Score": np.random.randint(40,101,150) # 分数
})
r = df.groupby("Class")["Score"].rank(method="min")
r.head()
0     3.0
1     3.0
2    29.0
3     6.0
4    37.0
Name: Score, dtype: float64

练一练

transform()方法无法像agg()一样,通过传入字典来对指定列使用特定的变换,如果需要在一次transform()的调用中实现这种功能,请给出解决方案。

由于Series在传入函数时是自带名字的,因此可以在函数内部进行名字的判断,再进行不同操作。例如:

def transform_helper(x):
    if x.name == "A":
        return x + 1
    elif x.name == "B":
        return x - 1
df = pd.DataFrame({"A":[1,2,3,4], "B":[5,6,7,8], "C":list("aabb")})
df.groupby("C").transform(transform_helper)
A B
0 2 4
1 3 5
2 4 6
3 5 7

练一练

从概念上说,索引功能是组过滤功能的子集,请使用groupby对象上的filter()方法完成loc[...]的功能,这里假设“...”是元素的列表。

df = pd.DataFrame({"A":[1,2,3,4,5,6]}, index=list("aabbcd"))
df
A
a 1
a 2
b 3
b 4
c 5
d 6
items_list = ["b", "d"]
df.groupby(df.index).filter(lambda x: x.index[0] in items_list)
A
b 3
b 4
d 6

练一练

在groupby对象中还定义了cov()和corr()函数,从概念上说也属于跨列的分组处理。请利用本节定义的gb对象,使用apply()函数实现与gb.cov()同样的功能。

df = pd.DataFrame(np.random.rand(12, 5), columns=list("ABCDE"))
df["F"] = list("aaaabbbbcccc")
apply_method = df.groupby("F").apply(lambda x: x.cov())
inner_method = df.groupby("F").cov()
apply_method.equals(inner_method)
True

一、汽车数据的分组分析

data/ch4/car.csv是一份有关汽车的数据集,其中Brand、Disp.和HP分别代表汽车品牌、发动机蓄量、发动机输出。

  • 按照如下要求,逐步对表格数据进行操作:

    • 筛选出所属Country数超过2个的汽车,即若该汽车的Country在总体数据集中出现次数不超过2则剔除。

    • 按Country分组计算价格均值、价格变异系数以及该Country的汽车数量,其中变异系数的计算方法是标准差除以均值,并在结果中把变异系数重命名为CoV。

  • 按照表中位置的前三分之一、中间三分之一和后三分之一分组,统计Price的均值。

  • 按照类型Type分组,解决如下问题:

    • 对Price和HP分别计算最大值和最小值,结果会产生多级列索引,请用下划线连接的方式把多级列索引合并为单层索引。

    • 对HP进行组内的min-max归一化,即每个元素减去组内HP的最小值后,再除以组内HP的极差。

【解答】
df = pd.read_csv("data/ch4/car.csv")
  • 1.1

res = df.Brand[df.groupby("Country")["Country"].transform(lambda x: x.shape[0] > 2)]
res.values
array(['Eagle Summit 4', 'Ford Escort   4', 'Ford Festiva 4',
       'Honda Civic 4', 'Mazda Protege 4', 'Nissan Sentra 4',
       'Pontiac LeMans 4', 'Subaru Loyale 4', 'Subaru Justy 3',
       'Toyota Corolla 4', 'Toyota Tercel 4', 'Chevrolet Camaro V8',
       'Dodge Daytona', 'Ford Mustang V8', 'Ford Probe',
       'Honda Civic CRX Si 4', 'Honda Prelude Si 4WS 4', 'Nissan 240SX 4',
       'Plymouth Laser', 'Subaru XT 4', 'Buick Skylark 4',
       'Chevrolet Beretta 4', 'Chrysler Le Baron V6', 'Ford Tempo 4',
       'Honda Accord 4', 'Mazda 626 4', 'Mitsubishi Galant 4',
       'Mitsubishi Sigma V6', 'Nissan Stanza 4', 'Oldsmobile Calais 4',
       'Subaru Legacy 4', 'Toyota Camry 4', 'Acura Legend V6',
       'Buick Century 4', 'Chrysler Le Baron Coupe',
       'Chrysler New Yorker V6', 'Eagle Premier V6', 'Ford Taurus V6',
       'Ford Thunderbird V6', 'Hyundai Sonata 4', 'Mazda 929 V6',
       'Nissan Maxima V6', 'Oldsmobile Cutlass Ciera 4',
       'Oldsmobile Cutlass Supreme V6', 'Toyota Cressida 6',
       'Buick Le Sabre V6', 'Chevrolet Caprice V8',
       'Ford LTD Crown Victoria V8', 'Chevrolet Lumina APV V6',
       'Dodge Grand Caravan V6', 'Ford Aerostar V6', 'Mazda MPV V6',
       'Mitsubishi Wagon 4', 'Nissan Axxess 4', 'Nissan Van 4'],
      dtype=object)
  • 1.2

df.groupby("Country")["Price"].agg([
    ("价格均值", "mean"),
    ("价格变异系数", lambda x: x.std() / x.mean()),
    ("汽车数量", "count")])
价格均值 价格变异系数 汽车数量
Country
France 15930.000000 NaN 1
Germany 14447.500000 0.435839 2
Japan 13938.052632 0.387429 19
Japan/USA 10067.571429 0.240040 7
Korea 7857.333333 0.243435 3
Mexico 8672.000000 NaN 1
Sweden 18450.000000 NaN 1
USA 12543.269231 0.203344 26
  • 2

s = pd.Series(np.empty(df.shape[0]))
s.iloc[:s.shape[0]//3] = "a.前三分之一"
s.iloc[s.shape[0]//3:s.shape[0]//3*2] = "b.中间三分之一"
s.iloc[s.shape[0]//3*2:] = "c.后三分之一"
df.groupby(s.values)["Price"].mean()
a.前三分之一      9069.95
b.中间三分之一    13356.40
c.后三分之一     15420.65
Name: Price, dtype: float64
  • 3.1

res = df.groupby('Type').agg({'Price': ['max'], 'HP': ['min']})
res.columns = res.columns.map(lambda x:'_'.join(x))
res
Price_max HP_min
Type
Compact 18900 95
Large 17257 150
Medium 24760 110
Small 9995 63
Sporty 13945 92
Van 15395 106
  • 3.2

def normalize(s):
    s_min, s_max = s.min(), s.max()
    res = (s - s_min)/(s_max - s_min)
    return res
df.groupby('Type')['HP'].transform(normalize).head()
0    1.00
1    0.54
2    0.00
3    0.58
4    0.80
Name: HP, dtype: float64

二、某海洋物种在三大海域的分布研究

某科研团队从2001年1月至2020年12月,对某海洋物种在太平洋部分水域(西经120°至西经160°、赤道线至南纬40°)、印度洋部分水域(东经60°至东经100°、赤道线至南纬40°)和大西洋部分水域(0°经线至西经40°、南纬20°至南纬60°)的出现情况进行了记录。记录的数据表存储在data/ch4/marine_observation.csv中,表的每一行数据包含了该次观测的时间、经纬度坐标(东经和北纬为正,西经和南纬为负)以及海水盐度。

  • 分组计算各年份在各海域的观测次数与海水盐度均值。

  • 将三片海域各自划分为\(10\times 10\)大小相同的网格,逐月统计每个网格内的观测总次数,并将结果保存为3个\(10\times 10\times 20\times 12\)的数组,这些维度分别代表经度方向的网格划分、维度方向的网格划分、年数以及月数。

【解答】
  • 1

df = pd.read_csv("data/ch4/marine_observation.csv")
Pacific = (df.longitude > -160) & (df.longitude < -120) & (df.latitude > -40) & (df.latitude < 0)
Indian = (df.longitude > 60) & (df.longitude < 100) & (df.latitude > -40) & (df.latitude < 0)
Atlantic = (df.longitude > -40) & (df.longitude < 0) & (df.latitude > -60) & (df.latitude < -20)
df["area"] = np.nan
df.loc[Pacific, "area"] = "Pacific"
df.loc[Indian, "area"] = "Indian"
df.loc[Atlantic, "area"] = "Atlantic"
df.area.count() == df.shape[0] # 无缺失值 # 第七章可用df.area.notna().all()
df["year"] = df.date.apply(lambda x: int(x[:4])) # 第八章可用df.date.str[:4].astype("int")
res = df.groupby(["year", "area"])["salinity"].agg(["count", "mean"])
res.head()
count mean
year area
2001 Atlantic 20073 35.015785
Indian 19972 35.009664
Pacific 19880 34.988308
2002 Atlantic 20230 34.988722
Indian 20036 35.015698
  • 2

df["lon_id"] = np.nan
df["lat_id"] = np.nan
df["month"] = df.date.apply(lambda x: int(x[5:7]))
df.loc[df.area=="Pacific", "lon_id"] = (df[df.area=="Pacific"].longitude - (-160)) // 4
df.loc[df.area=="Pacific", "lat_id"] = (df[df.area=="Pacific"].latitude - (-40)) // 4
df.loc[df.area=="Indian", "lon_id"] = (df[df.area=="Indian"].longitude - 60) // 4
df.loc[df.area=="Indian", "lat_id"] = (df[df.area=="Indian"].latitude - (-40)) // 4
df.loc[df.area=="Atlantic", "lon_id"] = (df[df.area=="Atlantic"].longitude - (-40)) // 4
df.loc[df.area=="Atlantic", "lat_id"] = (df[df.area=="Atlantic"].latitude - (-60)) // 4
df.lon_id = df.lon_id.astype("int")
df.lon_id = df.lon_id.astype("int")
all_res = []
for area in ["Pacific", "Indian", "Atlantic"]:
    _df = df[df.area==area]
    count_res = _df.groupby(["year", "month", "lon_id", "lat_id"])["area"].count()
    count_res = count_res.reorder_levels([2,3,0,1]).sort_index()
    count_res = count_res.values.reshape(10, 10, 20, 12)
    all_res.append(count_res)

三、实现transform()函数

请按照如下要求实现transform()函数:

  • groupby对象的构造方法为my_groupby(df, group_cols)

  • 支持单列分组功能(group_cols为单个列名)

  • 支持多列分组功能(group_cols为列名列表)

  • 支持标量广播功能

  • 给出测试样例,并与pandas中transform()的运行结果进行对比

【解答】
class my_groupby:
    def __init__(self, _df, group_cols):
        df = _df.copy()
        self.df = df
        if isinstance(group_cols, str):
            group_cols = [group_cols]
        self.used_col = df.columns.difference(pd.Index(group_cols))
        self.groups = df[group_cols].drop_duplicates().values.tolist()
        self.groups_dict = {}
        for items in self.groups:
            condition = np.ones(df.shape[0])
            for i in range(len(items)):
                condition = condition * (df[group_cols[i]]==items[i]).values
            self.groups_dict[tuple(items)] = df.index[condition.astype("bool")]
    def __getitem__(self, col):
        if isinstance(col, str):
            col = [col]
        self.used_col = col
        return self
    def transform(self, f):
        res = []
        for col in self.used_col:
            cur_res = pd.Series(
                np.empty(self.df.shape[0], dtype="int"),
                index=self.df.index,
                name=col
            )
            for items in self.groups_dict:
                idx = self.groups_dict[items]
                cur_res[idx] = f(self.df.loc[idx, col])
            res.append(cur_res)
        return cur_res if len(self.used_col)==1 else pd.concat(res, axis=1)
df = pd.DataFrame({
    "A": list("abbbcccc"),
    "B": list("xxxyyyyz"),
    "C": [1,2,3,4,5,6,7,8],
    "D": [9,10,11,12,13,14,15,16]
})
my_res = my_groupby(df, "A")["C"].transform(lambda x: x.cumsum())
pd_res = df.groupby("A")["C"].transform("cumsum")
my_res.equals(pd_res)
True
my_res = my_groupby(df, "A")[["C", "D"]].transform(lambda x: x.cumsum())
pd_res = df.groupby("A")[["C", "D"]].transform("cumsum")
my_res.equals(pd_res)
True
my_res = my_groupby(df, ["A", "B"])[["C", "D"]].transform(lambda x: x.cumsum())
pd_res = df.groupby(["A", "B"])[["C", "D"]].transform("cumsum")
my_res.equals(pd_res)
True
# 标量广播
my_res = my_groupby(df, ["A", "B"]).transform(lambda x: x.mean())
pd_res = df.groupby(["A", "B"]).transform("mean")
my_res.equals(pd_res)
True