--- jupytext: text_representation: format_name: myst kernelspec: display_name: Python 3 name: python3 --- ```{code-cell} ipython3 :tags: [remove_input] import numpy as np import pandas as pd import matplotlib.pyplot as plt np.random.seed(0) plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False np.set_printoptions(suppress = True) ``` # 第四章 ## 零、练一练 ```{admonition} 练一练 请在learn_pandas数据集上按学校分组统计体重的均值。 ``` ```{code-cell} ipython3 df = pd.read_csv('data/learn_pandas.csv') df.groupby("School")["Weight"].mean() ``` ```{admonition} 练一练 请根据0.25分位数和0.75分位数进行分割,将体重分为high、normal和low这3组,统计身高的均值。 ``` ```{code-cell} ipython3 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() ``` ```{admonition} 练一练 在learn_pandas数据集中,Transfer列的元素为“N”时表示该名同学不是转系生,请按照学校和年级两列分组,找出所有不含转系生的组对应的学校和年级。 ``` ```{code-cell} ipython3 res = (df.Transfer=="N").groupby([df.School, df.Grade]).all() res[~res].index ``` ```{admonition} 练一练 请使用传入字典的方法完成与gb.agg(['max', 'min'])等价的聚合任务。 ``` ```{code-cell} ipython3 gb = df.groupby('Gender')[['Height', 'Weight']] gb.agg({"Height": ['max', 'min'], "Weight": ['max', 'min']}) ``` ```{admonition} 练一练 在groupby对象上可以使用describe()方法进行统计信息汇总,请同时使用多个聚合函数,完成与该方法相同的功能。 ``` ```{code-cell} ipython3 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"]) ``` ```{admonition} 练一练 在agg()中能够使用聚合字符串的地方,我们都可以传入返回标量值的自定义函数,请自行构造一个相关的例子。 ``` ```{code-cell} ipython3 gb = df.groupby('Gender')[["Height", "Weight"]] gb.agg(lambda x: x.skew()) ``` ```{admonition} 练一练 在groupby对象中,rank()方法也是一个实用的变换函数,请在官方文档中查阅它的功能并给出1个使用的例子。 ``` groupby上的rank()函数用于组内排名,一个典型的例子是全年级某次数学期末考试,需要计算每个学生在班级内的排名: ```{code-cell} ipython3 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() ``` ```{admonition} 练一练 transform()方法无法像agg()一样,通过传入字典来对指定列使用特定的变换,如果需要在一次transform()的调用中实现这种功能,请给出解决方案。 ``` 由于Series在传入函数时是自带名字的,因此可以在函数内部进行名字的判断,再进行不同操作。例如: ```{code-cell} ipython3 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) ``` ```{admonition} 练一练 从概念上说,索引功能是组过滤功能的子集,请使用groupby对象上的filter()方法完成loc[...]的功能,这里假设“...”是元素的列表。 ``` ```{code-cell} ipython3 df = pd.DataFrame({"A":[1,2,3,4,5,6]}, index=list("aabbcd")) df ``` ```{code-cell} ipython3 items_list = ["b", "d"] df.groupby(df.index).filter(lambda x: x.index[0] in items_list) ``` ```{admonition} 练一练 在groupby对象中还定义了cov()和corr()函数,从概念上说也属于跨列的分组处理。请利用本节定义的gb对象,使用apply()函数实现与gb.cov()同样的功能。 ``` ```{code-cell} ipython3 df = pd.DataFrame(np.random.rand(12, 5), columns=list("ABCDE")) df["F"] = list("aaaabbbbcccc") ``` ```{code-cell} ipython3 apply_method = df.groupby("F").apply(lambda x: x.cov()) inner_method = df.groupby("F").cov() apply_method.equals(inner_method) ``` ## 一、汽车数据的分组分析 data/ch4/car.csv是一份有关汽车的数据集,其中Brand、Disp.和HP分别代表汽车品牌、发动机蓄量、发动机输出。 - 按照如下要求,逐步对表格数据进行操作: - 筛选出所属Country数超过2个的汽车,即若该汽车的Country在总体数据集中出现次数不超过2则剔除。 - 按Country分组计算价格均值、价格变异系数以及该Country的汽车数量,其中变异系数的计算方法是标准差除以均值,并在结果中把变异系数重命名为CoV。 - 按照表中位置的前三分之一、中间三分之一和后三分之一分组,统计Price的均值。 - 按照类型Type分组,解决如下问题: - 对Price和HP分别计算最大值和最小值,结果会产生多级列索引,请用下划线连接的方式把多级列索引合并为单层索引。 - 对HP进行组内的min-max归一化,即每个元素减去组内HP的最小值后,再除以组内HP的极差。 ```text 【解答】 ``` ```{code-cell} ipython3 df = pd.read_csv("data/ch4/car.csv") ``` - 1.1 ```{code-cell} ipython3 res = df.Brand[df.groupby("Country")["Country"].transform(lambda x: x.shape[0] > 2)] res.values ``` - 1.2 ```{code-cell} ipython3 df.groupby("Country")["Price"].agg([ ("价格均值", "mean"), ("价格变异系数", lambda x: x.std() / x.mean()), ("汽车数量", "count")]) ``` - 2 ```{code-cell} ipython3 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() ``` - 3.1 ```{code-cell} ipython3 res = df.groupby('Type').agg({'Price': ['max'], 'HP': ['min']}) res.columns = res.columns.map(lambda x:'_'.join(x)) res ``` - 3.2 ```{code-cell} ipython3 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() ``` ## 二、某海洋物种在三大海域的分布研究 某科研团队从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$的数组,这些维度分别代表经度方向的网格划分、维度方向的网格划分、年数以及月数。 ```text 【解答】 ``` - 1 ```{code-cell} ipython3 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() ``` - 2 ```{code-cell} ipython3 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") ``` ```{code-cell} ipython3 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()的运行结果进行对比 ```text 【解答】 ``` ```{code-cell} ipython3 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) ``` ```{code-cell} ipython3 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] }) ``` ```{code-cell} ipython3 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) ``` ```{code-cell} ipython3 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) ``` ```{code-cell} ipython3 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) ``` ```{code-cell} ipython3 # 标量广播 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) ```