Pandas 秘籍:6~11

您所在的位置:网站首页 端口33888 Pandas 秘籍:6~11

Pandas 秘籍:6~11

#Pandas 秘籍:6~11| 来源: 网络整理| 查看: 265

原文:Pandas Cookbook

协议:CC BY-NC-SA 4.0

译者:飞龙

六、索引对齐

在本章中,我们将介绍以下主题:

检查索引对象 生成笛卡尔积 索引爆炸 用不相等的索引填充值 追加来自不同数据帧的列 突出显示每一列的最大值 用方法链复制idxmax 寻找最常见的最大值 介绍

当以某种方式组合多个序列或数据帧时,在进行任何计算之前,数据的每个维度会首先自动在每个轴上对齐。 轴的这种无声且自动的对齐会给初学者造成极大的困惑,但它为超级用户提供了极大的灵活性。 本章将深入探讨索引对象,然后展示利用其自动对齐功能的各种秘籍。

检查索引对象

如第 1 章,“Pandas 基础”中所讨论的,序列和数据帧的每个轴都有一个索引对象,用于标记值。 有许多不同类型的索引对象,但是它们都具有相同的共同行为。 除特殊的多重索引之外,所有索引对象都是一维数据结构,结合了 Python 集和 NumPy ndarrays的功能和实现。

准备

在本秘籍中,我们将检查大学数据集的列索引并探索其许多功能。

操作步骤 读入大学数据集,为列索引分配一个变量,然后输出: >>> college = pd.read_csv('data/college.csv') >>> columns = college.columns >>> columns Index(['INSTNM', 'CITY', 'STABBR', 'HBCU', ...], dtype='object') 使用values属性访问基础的 NumPy 数组: >>> columns.values array(['INSTNM', 'CITY', 'STABBR', 'HBCU', ...], dtype=object) 通过带有标量,列表或切片的整数位置从索引中选择项目: >>> columns[5] 'WOMENONLY' >>> columns[[1,8,10]] Index(['CITY', 'SATMTMID', 'UGDS'], dtype='object') >>> columns[-7:-4] Index(['PPTUG_EF', 'CURROPER', 'PCTPELL'], dtype='object') 索引与序列和数据帧共享许多相同的方法: >>> columns.min(), columns.max(), columns.isnull().sum() ('CITY', 'WOMENONLY', 0) 直接在Index对象上使用基本算术和比较运算符: >>> columns + '_A' Index(['INSTNM_A', 'CITY_A', 'STABBR_A', 'HBCU_A', ...], dtype='object') >>> columns > 'G' array([ True, False, True, True, ...], dtype=bool) 创建索引后尝试直接更改索引值失败。 索引是不可变的对象: >>> columns[1] = 'city' TypeError: Index does not support mutable operations 工作原理

从许多索引对象操作中可以看到,它与序列和ndarrays似乎有很多共同点。 最大的差异之一来自第 6 步。索引是不可变的,创建后就无法更改它们的值。

更多

索引支持集合运算,并集,交集,差和对称差:

>>> c1 = columns[:4] >>> c1 Index(['INSTNM', 'CITY', 'STABBR', 'HBCU'], dtype='object') >>> c2 = columns[2:6] >>> c2 Index(['STABBR', 'HBCU', 'MENONLY'], dtype='object') >>> c1.union(c2) # or `c1 | c2` Index(['CITY', 'HBCU', 'INSTNM', 'MENONLY', 'RELAFFIL', 'STABBR'], dtype='object') >>> c1.symmetric_difference(c2) # or `c1 ^ c2` Index(['CITY', 'INSTNM', 'MENONLY'], dtype='object')

索引与 Python 集共享一些相同的操作。 索引在另一重要方面类似于 Python 集。 它们(通常)是使用哈希表实现的,当从数据帧中选择行或列时,哈希表的访问速度非常快。 当使用哈希表实现它们时,索引对象的值必须是不可变的,例如字符串,整数或元组,就像 Python 字典中的键一样。

索引支持重复值,并且如果在任何索引中碰巧有重复项,则哈希表将无法再用于其实现,并且对象访问会变得很慢。

另见 Pandas Index的官方文档 生成笛卡尔积

每当两个序列或数据帧与另一个序列或数据帧一起操作时,每个对象的索引(行索引和列索引)都首先对齐,然后再开始任何操作。 这种索引对齐方式是无声的,对于那些刚接触 Pandas 的人来说可能是非常令人惊讶的。 除非索引相同,否则这种对齐方式总是在索引之间创建笛卡尔积。

笛卡尔积是一个数学术语,通常出现在集合论中。 两个集之间的笛卡尔积是两个集的偶对的所有组合。 例如,标准纸牌中的 52 张纸牌代表 13 个等级(A, 2, 3,..., Q, K)和四个花色之间的笛卡尔积。

准备

生成笛卡尔积并非总是预期的结果,但是了解发生的方式和时间以避免意外后果至关重要。 在此秘籍中,将具有重叠但不相同的索引的两个序列相加在一起,产生了令人惊讶的结果。

操作步骤

请按照以下步骤创建笛卡尔积:

构造两个具有不同索引但包含一些相同值的序列: >>> s1 = pd.Series(index=list('aaab'), data=np.arange(4)) >>> s1 a 0 a 1 a 2 b 3 dtype: int64 >>> s2 = pd.Series(index=list('cababb'), data=np.arange(6)) >>> s2 c 0 a 1 b 2 a 3 b 4 b 5 dtype: int64 将两个序列加在一起以生成笛卡尔积: >>> s1 + s2 a 1.0 a 3.0 a 2.0 a 4.0 a 3.0 a 5.0 b 5.0 b 7.0 b 8.0 c NaN dtype: float64 工作原理

每个序列都是使用类构造器创建的,该类构造器接受各种各样的输入,最简单的是每个参数index和数据的值序列。

笛卡尔数学乘积与对两个 Pandas 对象进行运算的结果略有不同。s1中的每个a标签与s2中的每个a标签配对。 该配对在所得序列中产生六个a标签,三个b标签和一个c标签。 笛卡尔积在所有相同的索引标签之间发生。

由于带有标签c的元素是序列s2所特有的,因此 pandas 默认将其值设置为 missing,因为s1中没有标签可以对齐。 每当索引标签对于一个对象唯一时,Pandas 默认为缺少值。 不幸的结果是,将序列的数据类型更改为float,而每个序列仅具有整数作为值。 发生这种情况是因为 NumPy 缺少值对象。np.nan仅对于浮点数存在,而对于整数不存在。序列和数据帧的列必须具有齐次数值数据类型; 因此,每个值都转换为浮点数。 对于这个小的数据集,这几乎没有什么区别,但是对于较大的数据集,这可能会对内存产生重大影响。

更多

当索引以相同顺序包含相同的完全相同的元素时,将发生上述示例的异常。 发生这种情况时,不会发生笛卡尔积,而是按其位置对齐索引。 请注意,每个元素均按位置精确对齐,并且数据类型仍为整数:

>>> s1 = pd.Series(index=list('aaabb'), data=np.arange(5)) >>> s2 = pd.Series(index=list('aaabb'), data=np.arange(5)) >>> s1 + s2 a 0 a 2 a 4 b 6 b 8 dtype: int64

如果索引的元素相同,但是序列之间的顺序不同,则会产生笛卡尔积。 让我们在s2中更改索引的顺序,然后重新运行相同的操作:

>>> s1 = pd.Series(index=list('aaabb'), data=np.arange(5)) >>> s2 = pd.Series(index=list('bbaaa'), data=np.arange(5)) >>> s1 + s2 a 2 a 3 a 4 a 3 a 4 a 5 a 4 a 5 a 6 b 3 b 4 b 4 b 5 dtype: int64

有趣的是,Pandas 在同一项操作中有两个截然不同的结果。 如果笛卡尔积是 Pandas 的唯一选择,那么将数据帧的列加在一起这样的简单操作将使返回的元素数量激增。

在此秘籍中,每个序列具有不同数量的元素。 通常,当操作维中不包含相同数量的元素时,Python 和其他语言中的类似数组的数据结构将不允许进行操作。 Pandas 可以通过在完成操作之前先对齐索引来实现此目的。

另见 第 3 章,“开始数据分析”中的“通过更改数据类型来减少内存”秘籍 索引爆炸

先前的秘籍中有一个琐碎的示例,其中将两个小序列与不相等的索引一起添加。 处理较大的数据时,此问题可能会产生可笑的错误结果。

准备

在此秘籍中,我们添加了两个较大的序列,它们的索引只有几个唯一值,但顺序不同。 结果将使索引中的值数量爆炸。

操作步骤 读入员工数据并将索引设置为与race列相等: >>> employee = pd.read_csv('data/employee.csv', index_col='RACE') >>> employee.head()

选择BASE_SALARY列作为两个不同的序列。 检查此操作是否确实创建了两个新对象: >>> salary1 = employee['BASE_SALARY'] >>> salary2 = employee['BASE_SALARY'] >>> salary1 is salary2 True salary1和salary2变量实际上是指同一对象。 这意味着对一个的任何更改都会更改另一个。 为确保您收到数据的全新副本,请使用copy方法: >>> salary1 = employee['BASE_SALARY'].copy() >>> salary2 = employee['BASE_SALARY'].copy() >>> salary1 is salary2 False 让我们通过对其中一个序列进行排序来更改其索引顺序: >>> salary1 = salary1.sort_index() >>> salary1.head() RACE American Indian or Alaskan Native 78355.0 American Indian or Alaskan Native 81239.0 American Indian or Alaskan Native 60347.0 American Indian or Alaskan Native 68299.0 American Indian or Alaskan Native 26125.0 Name: BASE_SALARY, dtype: float64 >>> salary2.head() RACE Hispanic/Latino 121862.0 Hispanic/Latino 26125.0 White 45279.0 White 63166.0 White 56347.0 Name: BASE_SALARY, dtype: float64 让我们将这些salary序列加在一起: >>> salary_add = salary1 + salary2 >>> salary_add.head() RACE American Indian or Alaskan Native 138702.0 American Indian or Alaskan Native 156710.0 American Indian or Alaskan Native 176891.0 American Indian or Alaskan Native 159594.0 American Indian or Alaskan Native 127734.0 Name: BASE_SALARY, dtype: float64 操作成功完成。 让我们再添加一个salary1序列,然后输出每个序列的长度。 我们只是将该指数从 2,000 个值分解为超过 100 万个值: >>> salary_add1 = salary1 + salary1 >>> len(salary1), len(salary2), len(salary_add), len(salary_add1) (2000, 2000, 1175424, 2000) 工作原理

首先出现步骤 2,以创建两个唯一的对象,但实际上,它创建了一个由两个不同的变量名称引用的对象。 表达式employee['BASE_SALARY']从技术上讲创建的是视图,而不是全新的副本。 使用is运算符对此进行了验证。

在熊猫中,视图不是新对象,而只是对另一个对象的引用,通常是数据帧的某些子集。 此共享对象可能导致许多问题。

为了确保两个变量都引用完全不同的对象,我们使用copy序列方法,并再次使用is运算符验证它们是否是不同的对象。 步骤 4 使用sort_index方法按种族对序列进行排序。 第 5 步将这些不同的序列加在一起以产生一些结果。 仅检查头部,仍不清楚产生了什么。

步骤 6 向其自身添加salary1,以显示两个不同序列添加之间的比较。 此秘籍中所有序列的长度都已输出,我们清楚地看到series_add现已爆炸超过一百万个值。 索引中的每个唯一值都会产生笛卡尔积,因为索引并不完全相同。 此秘籍显着显示了将多个序列或数据帧组合在一起时索引可能产生的影响。

更多

通过做一些数学运算,我们可以验证salary_add的值的数量。 当笛卡尔积在所有相同的索引值之间发生时,我们可以求和它们各自计数的平方。 索引中甚至缺少的值也会与它们自身产生笛卡尔积:

>>> index_vc = salary1.index.value_counts(dropna=False) >>> index_vc Black or African American 700 White 665 Hispanic/Latino 480 Asian/Pacific Islander 107 NaN 35 American Indian or Alaskan Native 11 Others 2 Name: RACE, dtype: int64 >>> index_vc.pow(2).sum() 1175424 用不相等的索引填充值

当使用加法运算符将两个序列加在一起并且一个索引标签没有出现在另一个索引标签中时,结果值始终会丢失。 Pandas 提供了add方法,该方法提供了一种填充缺失值的选项。

准备

在本秘籍中,我们使用add方法的fill_value参数将baseball数据集中具有不等索引的多个序列合并在一起,以确保结果中没有缺失值。

操作步骤 读取三个baseball数据集,并将索引设置为playerID: >>> baseball_14 = pd.read_csv('data/baseball14.csv', index_col='playerID') >>> baseball_15 = pd.read_csv('data/baseball15.csv', index_col='playerID') >>> baseball_16 = pd.read_csv('data/baseball16.csv', index_col='playerID') >>> baseball_14.head()

使用索引方法difference发现baseball_14中而不是baseball_15中的索引标签,反之亦然: >>> baseball_14.index.difference(baseball_15.index) Index(['corpoca01', 'dominma01', 'fowlede01', 'grossro01', 'guzmaje01', 'hoeslj01', 'krausma01', 'preslal01', 'singljo02'], dtype='object', name='playerID') >>> baseball_14.index.difference(baseball_16.index) Index(['congeha01', 'correca01', 'gattiev01', 'gomezca01', 'lowrije01', 'rasmuco01', 'tuckepr01', 'valbulu01'], dtype='object', name='playerID') 每个索引都有很多独特的参与者。 让我们找出三年内每个玩家的总点击数。H列包含匹配数: >>> hits_14 = baseball_14['H'] >>> hits_15 = baseball_15['H'] >>> hits_16 = baseball_16['H'] >>> hits_14.head() playerID altuvjo01 225 cartech02 115 castrja01 103 corpoca01 40 dominma01 121 Name: H, dtype: int64 我们首先使用加法运算符将两个序列相加: >>> (hits_14 + hits_15).head() playerID altuvjo01 425.0 cartech02 193.0 castrja01 174.0 congeha01 NaN corpoca01 NaN Name: H, dtype: float64 即使玩家congeha01和corpoca01记录了 2015 年的热门歌曲,但他们的成绩仍然缺失。 让我们使用add方法及其参数fill_value来避免丢失值: >>> hits_14.add(hits_15, fill_value=0).head() playerID altuvjo01 425.0 cartech02 193.0 castrja01 174.0 congeha01 46.0 corpoca01 40.0 Name: H, dtype: float64 我们通过再次链接add方法来添加 2016 年的匹配: >>> hits_total = hits_14.add(hits_15, fill_value=0) \ .add(hits_16, fill_value=0) >>> hits_total.head() playerID altuvjo01 641.0 bregmal01 53.0 cartech02 193.0 castrja01 243.0 congeha01 46.0 Name: H, dtype: float64 检查结果中是否缺少值: >>> hits_total.hasnans False 工作原理

add方法的工作方式与加法运算符相似,但通过提供fill_value参数代替不匹配的索引,可以提供更大的灵活性。 在此问题中,将不匹配的索引值默认设置为 0 是有意义的,但是您可以使用其他任何数字。

有时每个序列都包含与缺失值相对应的索引标签。 在此特定实例中,当添加两个序列时,无论是否使用fill_value参数,索引标签仍将对应于缺失值。 为了澄清这一点,请看以下示例,其中索引标签a对应于每个序列中的缺失值:

>>> s = pd.Series(index=['a', 'b', 'c', 'd'], data=[np.nan, 3, np.nan, 1]) >>> s a NaN b 3.0 c NaN d 1.0 dtype: float64 >>> s1 = pd.Series(index=['a', 'b', 'c'], data=[np.nan, 6, 10]) >>> s1 a NaN b 6.0 c 10.0 dtype: float64 >>> s.add(s1, fill_value=5) a NaN b 9.0 c 15.0 d 6.0 dtype: float64 更多

此秘籍展示了如何仅将单个索引添加到序列中。 也完全可以将数据帧一起添加。 将数据帧加在一起将在计算之前对齐索引和列,并产生不匹配索引的缺失值。 首先,从 2014 年棒球数据集中选择一些列。

>>> df_14 = baseball_14[['G','AB', 'R', 'H']] >>> df_14.head()

Let's also select a few of the same and a few different columns from the 2015 baseball dataset:

>>> df_15 = baseball_15[['AB', 'R', 'H', 'HR']] >>> df_15.head()

如果行或列标签无法对齐,则将两个数据帧一起添加会丢失值。 使用style属性访问highlight_null方法可轻松查看缺失值的位置:

>>> (df_14 + df_15).head(10).style.highlight_null('yellow')

只有两个数据帧中都出现playerID的行才会丢失。 类似地,AB,H和R列是两个数据帧中唯一出现的列。 即使我们在指定fill_value参数的情况下使用add方法,我们仍然缺少值。 这是因为在我们的输入数据中从来没有行和列的某些组合。 例如,playerID congeha01和列G的交集。 他只出现在 2015 年没有G列的数据集中。 因此,没有任何值被填充:

>>> df_14.add(df_15, fill_value=0).head(10) \ .style.highlight_null('yellow')

追加来自不同数据帧的列

所有数据帧都可以向自己添加新列。 但是,像往常一样,每当一个数据帧从另一个数据帧或序列添加一个新列时,索引都将在创建新列之前首先对齐。

准备

此秘籍使用employee数据集添加一个新列,其中包含该员工部门的最高薪水。

操作步骤 导入employee数据,然后在新的数据帧中选择DEPARTMENT和BASE_SALARY列: >>> employee = pd.read_csv('data/employee.csv') >>> dept_sal = employee[['DEPARTMENT', 'BASE_SALARY']] 将此较小的数据帧按每个部门内的薪水排序: >>> dept_sal = dept_sal.sort_values(['DEPARTMENT', 'BASE_SALARY'], ascending=[True, False]) 使用drop_duplicates方法保留每个DEPARTMENT的第一行: >>> max_dept_sal = dept_sal.drop_duplicates(subset='DEPARTMENT') >>> max_dept_sal.head()

将DEPARTMENT列放入每个数据帧的索引中: >>> max_dept_sal = max_dept_sal.set_index('DEPARTMENT') >>> employee = employee.set_index('DEPARTMENT') 现在索引包含匹配的值,我们可以将新列追加到employee数据帧: >>> employee['MAX_DEPT_SALARY'] = max_dept_sal['BASE_SALARY'] >>> employee.head()

我们可以使用query方法验证结果,以检查是否存在BASE_SALARY大于MAX_DEPT_SALARY的行: >>> employee.query('BASE_SALARY > MAX_DEPT_SALARY')

工作原理

步骤 2 和 3 找到每个部门的最高工资。 为了使索引自动对齐正常工作,我们将每个数据帧索引设置为部门。 步骤 5 之所以有效,是因为左侧的数据帧中的每行索引;employee与来自右侧数据帧max_dept_sal的一个且仅一个索引对齐。 如果max_dept_sal在其索引中重复了任何部门,则该操作将失败。

例如,让我们看看当我们在具有重复索引值的等式的右侧使用数据帧时会发生什么。 我们使用数据帧的sample方法随机选择十行而不进行替换:

>>> np.random.seed(1234) >>> random_salary = dept_sal.sample(n=10).set_index('DEPARTMENT') >>> random_salary

注意索引中有几个重复的部门。 现在,当我们尝试创建新列时,将引发一个错误,警告我们有重复项。employee数据帧中的至少一个索引标签与random_salary中的两个或多个索引标签结合在一起:

>>> employee['RANDOM_SALARY'] = random_salary['BASE_SALARY'] ValueError: cannot reindex from a duplicate axis 更多

并非等号左侧的所有索引都需要匹配,但最多只能有一个匹配。 如果左对齐的数据帧索引没有任何内容,则将缺少结果值。 让我们创建一个发生这种情况的示例。 我们将仅使用max_dept_sal序列的前三行来创建新列:

>>> employee['MAX_SALARY2'] = max_dept_sal['BASE_SALARY'].head(3) >>> employee.MAX_SALARY2.value_counts() 140416.0 29 100000.0 11 64251.0 5 Name: MAX_SALARY2, dtype: int64 >>> employee.MAX_SALARY2.isnull().mean() .9775

该操作成功完成,但仅为三个部门的填充了薪水。 没有出现在max_dept_sal序列的前三行中的所有其他部门导致值丢失。

另见 第 3 章“开始数据分析”中的“从最大值中选择最小值”秘籍 突出显示每一列的最大值

college数据集有许多数字列,它们描述了有关每所学校的不同指标。 许多人都对在某些指标上表现最好的学校感兴趣。

准备

此秘籍发现每个数字列具有最大值的学校,并设置数据帧的样式以突出显示信息,以便用户轻松使用。

操作步骤 阅读以机构名称作为索引的大学数据集: >>> college = pd.read_csv('data/college.csv', index_col='INSTNM') >>> college.dtypes CITY object STABBR object HBCU float64 MENONLY float64 ... PCTFLOAN float64 UG25ABV float64 MD_EARN_WNE_P10 object GRAD_DEBT_MDN_SUPP object Length: 26, dtype: object CITY和STABBR以外的所有其他列似乎都是数字。 检查上一步中的数据类型会意外显示MD_EARN_WNE_P10和GRAD_DEBT_MDN_SUPP列属于对象类型,而不是数字类型。 为了更好地了解这些列中的值是什么,让我们检查它们的第一个值: >>> college.MD_EARN_WNE_P10.iloc[0] '30300' >>> college.GRAD_DEBT_MDN_SUPP.iloc[0] '33888' 这些值是字符串,但我们希望它们是数字。 这意味着在序列的其他地方可能会出现非数字字符。 一种检查方法是按降序对这些列进行排序并检查前几行: >>> college.MD_EARN_WNE_P10.sort_values(ascending=False).head() INSTNM Sharon Regional Health System School of Nursing PrivacySuppressed Northcoast Medical Training Academy PrivacySuppressed Success Schools PrivacySuppressed Louisiana Culinary Institute PrivacySuppressed Bais Medrash Toras Chesed PrivacySuppressed Name: MD_EARN_WNE_P10, dtype: object 罪魁祸首似乎是一些学校对这两列数据存在隐私问题。 要将这些列强制为数字,请使用 pandas 函数to_numeric: >>> cols = ['MD_EARN_WNE_P10', 'GRAD_DEBT_MDN_SUPP'] >>> for col in cols: college[col] = pd.to_numeric(college[col], errors='coerce') >>> college.dtypes.loc[cols] MD_EARN_WNE_P10 float64 GRAD_DEBT_MDN_SUPP float64 dtype: object 使用select_dtypes方法仅过滤数字列。 这将排除STABBR和CITY列,列,其中最大值对于此问题没有意义: >>> college_n = college.select_dtypes(include=[np.number]) >>> college_n.head()

通过利用数据字典,有几列仅具有二进制(0/1)值,不会提供有用的信息。 为了以编程方式找到这些列,我们可以创建布尔序列并使用nunique方法找到具有两个唯一值的所有列: >>> criteria = college_n.nunique() == 2 >>> criteria.head() HBCU True MENONLY True WOMENONLY True RELAFFIL True SATVRMID False dtype: bool 将此布尔序列传递给列索引对象的索引运算符,并创建二进制列的列表: >>> binary_cols = college_n.columns[criteria].tolist() >>> binary_cols ['HBCU', 'MENONLY', 'WOMENONLY', 'RELAFFIL', 'DISTANCEONLY', 'CURROPER'] 使用drop方法删除二进制列: >>> college_n2 = college_n.drop(labels=binary_cols, axis='columns') >>> college_n2.head()

使用idxmax方法查找每一列的最大值的索引标签: >>> max_cols = college_n2.idxmax() >>> max_cols SATVRMID California Institute of Technology SATMTMID California Institute of Technology UGDS University of Phoenix-Arizona UGDS_WHITE Mr Leon's School of Hair Design-Moscow ... PCTFLOAN ABC Beauty College Inc UG25ABV Dongguk University-Los Angeles MD_EARN_WNE_P10 Medical College of Wisconsin GRAD_DEBT_MDN_SUPP Southwest University of Visual Arts-Tucson Length: 18, dtype: object 在max_cols序列上调用unique方法。 这将返回唯一列名称的ndarray: >>> unique_max_cols = max_cols.unique() >>> unique_max_cols[:5] array(['California Institute of Technology', 'University of Phoenix-Arizona', "Mr Leon's School of Hair Design-Moscow", 'Velvatex College of Beauty Culture', 'Thunderbird School of Global Management'], dtype=object) 使用max_cols的值选择仅具有最大值的学校的行,然后使用style属性突出显示这些值: >>> college_n2.loc[unique_max_cols].style.highlight_max()

工作原理

idxmax方法非常强大,当索引被有意义地标记时,它变得非常有用。 出乎意料的是,MD_EARN_WNE_P10和GRAD_DEBT_MDN_SUPP均为object数据类型。 导入时,如果列中至少包含一个字符串,则 pandas 将列的所有数值强制转换为字符串。

通过检查步骤 2 中的特定列值,我们可以清楚地看到 在这些列中有字符串。 在第 3 步中,我们以降序排序,因为数字字符首先出现。 这会将所有字母值提升到该序列的顶部。 我们发现PrivacySuppressed字符串造成严重破坏。 Pandas 可以使用to_numeric函数将仅包含数字字符的所有字符串强制转换为实际的数字数据类型。 要覆盖在to_numeric遇到无法转换的字符串时引发错误的默认行为,必须将coerce传递给errors参数。 这将强制所有非数字字符串变为缺失值(np.nan)。

几列没有有用或有意义的最大值。 在第 4 步到第 6 步中已将它们删除。select_dtypes对于具有许多列的非常宽的数据帧极为有用。

在步骤 7 中,idxmax遍历所有列以找到每个列的最大值的索引。 它将结果作为序列输出。 SAT 数学和口语成绩均最高的学校是加利福尼亚理工学院。 洛杉矶东国大学的 25 岁以上的学生人数最多。

尽管idxmax提供的信息很好,但它不会产生相应的最大值。 为此,我们从max_cols序列的值中收集所有唯一的学校名称。

最后,在步骤 8 中,我们使用.loc索引器根据索引标签选择行,在第一步中将其作为学校名称。 此过滤器仅适用于具有最大值的学校。数据帧具有实验性style属性,该属性本身具有一些方法来更改显示的数据帧的外观。 突出显示最大值可使结果更加清晰。

更多

默认情况下,highlight_max方法突出显示每列的最大值。 我们可以使用axis参数突出显示每行的最大值。 在这里,我们只选择college数据集的种族百分比列,并突出显示每所学校百分比最高的种族:

>>> college = pd.read_csv('data/college.csv', index_col='INSTNM') >>> college_ugds = college.filter(like='UGDS_').head() >>> college_ugds.style.highlight_max(axis='columns')

尝试在大型数据帧上应用样式会导致 Jupyter 崩溃,这就是为什么仅将样式应用于数据帧的头部的原因。

另见 Pandas 数据帧样式的官方文档 使用方法链接复制idxmax

尝试自行实现内置数据帧方法可能是一个很好的练习。 这种复制可以使您对通常不会遇到的其他 Pandas 方法有更深入的了解。idxmax是仅使用本书到目前为止介绍的方法进行复制的一种挑战性方法。

准备

此秘籍将基本方法缓慢地链接在一起,以最终找到包含最大列值的所有行索引值。

操作步骤 加载大学数据集并执行与上一个秘籍相同的操作,以仅获取感兴趣的数字列: >>> college = pd.read_csv('data/college.csv', index_col='INSTNM') >>> cols = ['MD_EARN_WNE_P10', 'GRAD_DEBT_MDN_SUPP'] >>> for col in cols: college[col] = pd.to_numeric(college[col], errors='coerce') >>> college_n = college.select_dtypes(include=[np.number]) >>> criteria = college_n.nunique() == 2 >>> binary_cols = college_n.columns[criteria].tolist() >>> college_n = college_n.drop(labels=binary_cols, axis='columns') 使用max方法查找每列的最大值: >>> college_n.max().head() SATVRMID 765.0 SATMTMID 785.0 UGDS 151558.0 UGDS_WHITE 1.0 UGDS_BLACK 1.0 dtype: float64 使用数据帧的eq方法使用其max列测试每个值。 默认情况下,eq方法将列数据帧的列与传递的序列索引的标签对齐: >>> college_n.eq(college_n.max()).head()

此数据帧中所有具有至少一个True值的行都必须包含最大列数。 让我们使用any方法查找具有至少一个True值的所有此类行: >>> has_row_max = college_n.eq(college_n.max()).any(axis='columns') >>> has_row_max.head() INSTNM Alabama A & M University False University of Alabama at Birmingham False Amridge University False University of Alabama in Huntsville False Alabama State University False dtype: bool 只有 18 列,这意味着has_row_max中最多只能有 18 个True值。 让我们找出实际有多少个: >>> college_n.shape (7535, 18) >>> has_row_max.sum() 401 这有点出乎意料,但是事实证明,有些列的许多行等于最大值。 这对于许多最大值为 1 的百分比列很常见。idxmax返回第一次出现的最大值。 让我们备份一下,删除any方法,然后看一下步骤 3 的输出。让我们运行cumsum方法来累积所有True值。 显示了前三行: >>> college_n.eq(college_n.max()).cumsum()

有些列具有一个唯一的最大值,例如SATVRMID和SATMTMID,而另一些列则具有UGDS_WHITE。 109 所学校的本科生中有 100% 是白人。 如果我们再链接一次cumsum方法,则值 1 在每一列中只会出现一次,并且它将是最大值的第一次出现: >>> college_n.eq(college_n.max()).cumsum().cumsum()

现在,我们可以使用eq方法测试每个值是否等于 1,然后使用any方法查找具有至少一个True值的行: >>> has_row_max2 = college_n.eq(college_n.max()) \ .cumsum() \ .cumsum() \ .eq(1) \ .any(axis='columns') >>> has_row_max2.head() INSTNM Alabama A & M University False University of Alabama at Birmingham False Amridge University False University of Alabama in Huntsville False Alabama State University False dtype: bool 测试has_row_max2的True值不超过列数: >>> has_row_max2.sum() 16 我们需要has_row_max2为True的所有机构。 我们可以简单地在序列本身上使用布尔索引: >>> idxmax_cols = has_row_max2[has_row_max2].index >>> idxmax_cols Index(['Thunderbird School of Global Management', 'Southwest University of Visual Arts-Tucson', 'ABC Beauty College Inc', 'Velvatex College of Beauty Culture', 'California Institute of Technology', 'Le Cordon Bleu College of Culinary Arts-San Francisco', 'MTI Business College Inc', 'Dongguk University-Los Angeles', 'Mr Leon's School of Hair Design-Moscow', 'Haskell Indian Nations University', 'LIU Brentwood', 'Medical College of Wisconsin', 'Palau Community College', 'California University of Management and Sciences', 'Cosmopolitan Beauty and Tech School', 'University of Phoenix-Arizona'], dtype='object', name='INSTNM') 这些机构中的所有 16 个都是至少其中一列的第一个最大出现次数的索引。 我们可以检查它们是否与idxmax方法中找到的相同: >>> set(college_n.idxmax().unique()) == set(idxmax_cols) True 工作原理

第一步通过将两列转换为数字并消除二进制列来复制上一个秘籍的工作。 我们在步骤 2 中找到每列的最大值。在这里,需要谨慎,因为 Pandas 会默默地丢弃无法产生最大值的列。 如果发生这种情况,则第 3 步仍将完成,但将为每列生成所有False值,而没有可用的最大值。

步骤 4 使用any方法在每一行中进行扫描,以搜索至少一个True值。 具有至少一个True值的任何行都包含一列的最大值。 我们在步骤 5 中对所得的布尔序列求和,以确定多少行包含最大值。 出乎意料的是,行多于列。 步骤 6 深入说明了为什么会发生这种情况。 我们对步骤 3 的输出进行累计,并检测等于每列最大值的总行数。

许多大学只有一个种族就拥有 100% 的学生人数。 到目前为止,这是最大的多个行的最大贡献者。 如您所见,SAT 成绩栏和大学本科生只有一排具有最大值的行,但是某些种族栏有最大值。

我们的目标是找到具有最大值的第一行。 我们需要再次取累加总和,以使每一列只有一行等于 1。步骤 8 将代码格式化为每行只有一个方法,并完全按照步骤 4 的方式运行any方法。 此步骤成功后,则True值应不超过列数。 步骤 9 断言这是真的。

为了验证我们是否在前几列中找到与idxmax相同的列,我们对has_row_max2本身使用了布尔选择。 列将以不同的顺序排列,因此我们将列名称的顺序转换为集合,这些集合固有地无序比较相等性。

更多

可以用一长串代码将索引运算符与匿名函数链接起来,从而完成此秘籍。 这个小技巧使您无需执行第 10 步。在此秘籍中,我们可以估算直接idxmax方法与我们的手动工作之间的差异:

>>> %timeit college_n.idxmax().values 1.12 ms ± 28.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) >>> %timeit college_n.eq(college_n.max()) \ .cumsum() \ .cumsum() \ .eq(1) \ .any(axis='columns') \ [lambda x: x].index 5.35 ms ± 55.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

不幸的是,我们的工作速度是 Pandas idxmax内置方法的五倍,但是不管其性能如何下降,许多创新且实用的解决方案都使用布尔序列和cumsum累积方法来查找条纹或一个轴的特定模式。

寻找最常见的最大值

大学数据集包含超过 7,500 所大学的 8 个不同种族的本科人口百分比。 找到每所学校本科生人数最多的种族,然后为整个数据集找到此结果的分布将是很有趣的。 我们将能够回答一个类似“哪个机构的白人学生比其他任何种族都要多”的问题。

准备

在此秘籍中,我们使用idxmax方法找到每所学校的本科生百分比最高的种族,然后找到这些最大值的分布。

操作步骤 阅读大学数据集,然后仅选择那些包含大学种族百分比信息的列: >>> college = pd.read_csv('data/college.csv', index_col='INSTNM') >>> college_ugds = college.filter(like='UGDS_') >>> college_ugds.head()

使用idxmax方法获取每一行具有最高竞争百分比的列名称: >>> highest_percentage_race = college_ugds.idxmax(axis='columns') >>> highest_percentage_race.head() INSTNM Alabama A & M University UGDS_BLACK University of Alabama at Birmingham UGDS_WHITE Amridge University UGDS_BLACK University of Alabama in Huntsville UGDS_WHITE Alabama State University UGDS_BLACK dtype: object 使用value_counts方法返回最大出现次数的分布: >>> highest_percentage_race.value_counts(normalize=True) UGDS_WHITE 0.670352 UGDS_BLACK 0.151586 UGDS_HISP 0.129473 UGDS_UNKN 0.023422 UGDS_ASIAN 0.012074 UGDS_AIAN 0.006110 UGDS_NRA 0.004073 UGDS_NHPI 0.001746 UGDS_2MOR 0.001164 dtype: float64 工作原理

此秘籍的关键是要认识到所有列都代表相同的信息单元。 我们可以将这些列相互比较,通常是而不是情况。 例如,直接将 SAT 口语成绩与大学生人数进行比较是没有意义的。 由于数据是以这种方式构造的,因此我们可以将idxmax方法应用于数据的每一行,以找到具有最大值的列。 我们需要使用axis参数更改其默认行为。

第 2 步完成此操作并返回一个序列,我们现在可以简单地对其应用value_counts方法以返回分布。 我们将True传递给normalize参数,因为我们对分布(相对频率)感兴趣,而不是原始计数。

更多

我们可能想探索更多并回答这个问题:对于黑人学生多于其他种族的学校,第二高种族百分比的分布是什么?

>>> college_black = college_ugds[highest_percentage_race == 'UGDS_BLACK'] >>> college_black = college_black.drop('UGDS_BLACK', axis='columns') >>> college_black.idxmax(axis='columns').value_counts(normalize=True) UGDS_WHITE 0.661228 UGDS_HISP 0.230326 UGDS_UNKN 0.071977 UGDS_NRA 0.018234 UGDS_ASIAN 0.009597 UGDS_2MOR 0.006718 UGDS_AIAN 0.000960 UGDS_NHPI 0.000960 dtype: float64

在此秘籍中应用相同的方法之前,我们需要删除UGDS_BLACK列。 有趣的是,这些黑人人口较多的学校似乎倾向于拥有较高的西班牙裔人口。

七、分组以进行汇总,过滤和转换

在本章中,我们将介绍以下主题:

定义聚合 使用函数对多个列执行分组和聚合 分组后删除多重索引 自定义聚合函数 使用*args和**kwargs自定义聚合函数 检查groupby对象 筛选少数人群居多的州 转换减肥赌注 计算每个州的 SAT 加权平均成绩 按连续变量分组 计算城市之间的航班总数 寻找最长的准时航班 介绍

数据分析过程中最基本的任务之一是在对每个组执行计算之前将数据分成独立的组。 该方法已经存在了相当长的时间,但是最近被称为拆分应用组合。 本章介绍了功能强大的groupby方法,该方法可让您以可想象的任何方式对数据进行分组,并在返回单个数据集之前将任何类型的函数独立地应用于每个组。

Hadley Wickham 创造了术语“拆分应用组合”,用于描述将数据分为独立的可管理块,将函数独立应用于这些块,然后将结果组合在一起的通用数据分析模式。 可以在他的论文中找到更多详细信息。

在开始使用秘籍之前,我们只需要了解一些术语。 所有基本的分组操作都有分组列,这些列中值的每个唯一组合代表数据的独立分组。 语法如下所示:

>>> df.groupby(['list', 'of', 'grouping', 'columns']) >>> df.groupby('single_column') # when grouping by a single column

该操作的结果返回一个分组对象。 正是这个分组对象将成为驱动整个整章所有计算的引擎。 在通过对象创建此分组时,Pandas 实际上很少执行,仅验证了分组是可能的。 您必须在该分组对象上链接方法,以释放其潜能。

从技术上讲,该操作的结果将是DataFrameGroupBy或SeriesGroupBy,但为简单起见,在整章中将其称为分组对象。

定义聚合

groupby方法最常见的用途是执行聚合。 实际是什么聚合? 在我们的数据分析世界中,当许多输入的序列被汇总或组合为单个值输出时,就会发生汇总。 例如,对一列的所有值求和或求其最大值是应用于单个数据序列的常见聚合。 聚合仅获取许多值,然后将其转换为单个值。

除了介绍中定义的分组列外,大多数聚合还有两个其他组件,聚合列和聚合函数。 汇总列是其值将被汇总的列。 聚合函数定义聚集的方式。 主要的聚合函数包括sum,min,max,mean,count,variance和std等。

准备

在此秘籍中,我们检查航班数据集,并执行最简单的可能的汇总,仅涉及单个分组列,单个汇总列和单个汇总函数。 我们将找到每家航空公司的平均到达延误时间。 Pandas 具有相当多种不同的语法来产生聚合,本秘籍涵盖了它们。

操作步骤 读取飞行数据集,并定义分组列(AIRLINE),聚合列(ARR_DELAY)和聚合函数(mean): >>> flights = pd.read_csv('data/flights.csv') >>> flights.head()

将分组列放在groupby方法中,然后通过字典在agg方法中将聚集列及其聚合函数配对: >>> flights.groupby('AIRLINE').agg({'ARR_DELAY':'mean'}).head()

或者,您可以将汇总列放在索引运算符中,然后将汇总函数作为字符串传递给agg: >>> flights.groupby('AIRLINE')['ARR_DELAY'].agg('mean').head() AIRLINE AA 5.542661 AS -0.833333 B6 8.692593 DL 0.339691 EV 7.034580 Name: ARR_DELAY, dtype: float64 上一步中使用的字符串名称是 Pandas 提供的一种便捷功能,可让您引用特定的聚合函数。 您可以将任何聚合函数直接传递给agg方法,例如 NumPy mean函数。 输出与上一步相同: >>> flights.groupby('AIRLINE')['ARR_DELAY'].agg(np.mean).head() 在这种情况下,可以完全跳过agg方法,而直接使用mean方法。 此输出也与步骤 3 相同: >>> flights.groupby('AIRLINE')['ARR_DELAY'].mean().head() 工作原理

groupby方法的语法不像其他方法那么简单。 让我们通过将groupby方法的结果存储为自己的变量来拦截步骤 2 中的方法链

>>> grouped = flights.groupby('AIRLINE') >>> type(grouped) pandas.core.groupby.DataFrameGroupBy

首先使用其自己独特的属性和方法来生产一个全新的中间对象。 在此阶段没有任何计算。 Pandas 仅验证分组列。 该分组对象具有agg方法来执行聚合。 使用此方法的一种方法是向其传递一个字典,该字典将聚合列映射到聚合函数,如步骤 2 所示。

有几种不同的语法产生相似的结果,而步骤 3 显示了另一种方法。 与其标识字典中的聚合列,不如将其放在索引运算符中,就如同您从数据帧中将其选择为列一样。 然后,将函数字符串名称作为标量传递给agg方法。

您可以将任何汇总函数传递给agg方法。 为了简单起见,Pandas 允许您使用字符串名称,但是您也可以像在步骤 4 中一样明确地调用一个聚合函数。NumPy 提供了许多聚合值的函数。

步骤 5 显示了最后一种语法风格。 如本例所示,当仅应用单个聚合函数时,通常可以直接将其作为对分组对象本身的方法进行调用,而无需使用agg。 并非所有聚合函数都具有等效的方法,但是许多基本函数都有。 以下是几个聚合函数的列表,这些函数可以作为字符串传递给agg或作为方法直接链接到分组对象:

min max mean median sum count std var size describe nunique idxmin idxmax 更多

如果您不对agg使用聚合函数,则 pandas 会引发异常。 例如,让我们看看将平方根函数应用于每个组会发生什么:

>>> flights.groupby('AIRLINE')['ARR_DELAY'].agg(np.sqrt) ValueError: function does not reduce 另见 Pandas 聚合的官方文档 使用函数对多个列执行分组和聚合

可以对多列进行分组和聚合。 语法仅与使用单个列进行分组和聚合时稍有不同。 与任何分组操作一样,它有助于识别三个组成部分:分组列,聚合列和聚合函数。

准备

在本秘籍中,我们通过回答以下查询来展示数据帧的groupby方法的灵活性:

查找每个工作日每个航空公司的已取消航班的数量 查找每个航空公司在工作日内已取消和改航航班的数量和百分比 对于每个始发地和目的地,查找航班总数,已取消航班的数量和百分比,以及通话时间的平均值和方差 操作步骤 读取航班数据集,并通过定义分组列(AIRLINE, WEEKDAY),聚合列(CANCELLED)和聚合函数(sum)回答第一个查询: >>> flights.groupby(['AIRLINE', 'WEEKDAY'])['CANCELLED'] \ .agg('sum').head(7) AIRLINE WEEKDAY AA 1 41 2 9 3 16 4 20 5 18 6 21 7 29 Name: CANCELLED, dtype: int64 通过使用每对分组和聚集列的列表来回答第二个查询。 另外,对聚合函数使用列表: >>> flights.groupby(['AIRLINE', 'WEEKDAY']) \ ['CANCELLED', 'DIVERTED'].agg(['sum', 'mean']).head(7)

使用agg方法中的字典来回答第三个查询,以将特定的聚合列映射到特定的聚合函数: >>> group_cols = ['ORG_AIR', 'DEST_AIR'] >>> agg_dict = {'CANCELLED':['sum', 'mean', 'size'], 'AIR_TIME':['mean', 'var']} >>> flights.groupby(group_cols).agg(agg_dict).head()

工作原理

要像步骤 1 一样按多列分组,我们将字符串名称列表传递给groupby方法。AIRLINE和WEEKDAY的每个唯一组合均形成一个独立的组。 在每个组中,找到已取消航班的总数,然后将其作为序列返回。

步骤 2,再次按AIRLINE和WEEKDAY分组,但这一次汇总了两列。 它将两个聚合函数sum和mean中的每一个应用于每个列,从而每组返回四个列。

步骤 3 进一步进行,并使用字典将特定的聚合列映射到不同的聚合函数。 请注意,size聚合函数返回每个组的总行数。 这与count汇总函数不同,后者会返回每组非缺失值的数量。

更多

执行聚合时,会遇到几种主要的语法。 以下四个伪代码块总结了使用groupby方法执行聚合的主要方式:

将agg与字典一起使用是最灵活的方法,它允许您为每一列指定聚合函数: >>> df.groupby(['grouping', 'columns']) \ .agg({'agg_cols1':['list', 'of', 'functions'], 'agg_cols2':['other', 'functions']}) 将agg与聚合函数列表一起使用,会将每个函数应用于每个聚合列: >>> df.groupby(['grouping', 'columns'])['aggregating', 'columns'] \ .agg([aggregating, functions]) 直接使用紧随汇总列之后的方法而不是agg,仅将该方法应用于每个汇总列。 这种方式不允许多种聚合函数: >>> df.groupby(['grouping', 'columns'])['aggregating', 'columns'] \ .aggregating_method() 如果您未指定汇总列,则汇总方法将应用于所有非分组列: >>> df.groupby(['grouping', 'columns']).aggregating_method()

在前面的四个代码块中,当按单个列进行分组或聚合时,可以用字符串代替任何列表。

分组后删除多重索引

不可避免地,当使用groupby时,您可能会在列或行或两者中都创建多重索引。 具有多重索引的数据帧更加难以导航,并且有时列名称也令人困惑。

准备

在本秘籍中,我们使用groupby方法执行聚合,以创建具有行和列多重索引的数据帧,然后对其进行处理,以使索引为单个级别,并且列名具有描述性。

操作步骤 读取航班数据集; 编写声明以查找飞行的总里程和平均里程; 以及每个航空公司在每个工作日的最大和最小到达延误: >>> flights = pd.read_csv('data/flights.csv') >>> airline_info = flights.groupby(['AIRLINE', 'WEEKDAY'])\ .agg({'DIST':['sum', 'mean'], 'ARR_DELAY':['min', 'max']}) \ .astype(int) >>> airline_info.head(7)

行和列均由具有两个级别的多重索引标记。 让我们将其压缩到单个级别。 为了解决这些列,我们使用多重索引方法get_level_values。 让我们显示每个级别的输出,然后将两个级别连接起来,然后再将其设置为新的列值: >>> level0 = airline_info.columns.get_level_values(0) Index(['DIST', 'DIST', 'ARR_DELAY', 'ARR_DELAY'], dtype='object') >>> level1 = airline_info.columns.get_level_values(1) Index(['sum', 'mean', 'min', 'max'], dtype='object') >>> airline_info.columns = level0 + '_' + level1 >>> airline_info.head(7)

使用reset_index将行标签返回到单个级别: >>> airline_info.reset_index().head(7)

工作原理

当使用agg方法对多个列执行聚合时,pandas 将创建一个具有两个级别的索引对象。 聚合列变为顶层,聚合函数变为底层。 Pandas 显示的多重索引级别与单级别的列不同。 除了最里面的级别以外,屏幕上不会显示重复的索引值。 您可以检查第 1 步中的数据帧以进行验证。 例如,DIST列仅显示一次,但它引用了前两列。

最里面的多重索引级别是最接近数据的级别。 这将是最底部的列级别和最右边的索引级别。

步骤 2 通过首先使用多重索引方法get_level_values.检索每个级别的基础值来定义新列。此方法接受一个整数,该整数标识索引级别。 它们从顶部/左侧以零开始编号。 索引支持向量化操作,因此我们将两个级别与下划线分开。 我们将这些新值分配给columns属性。

在第 3 步中,我们将两个索引级别都设为reset_index作为列。 我们可以像在第 2 步中那样将级别连接在一起,但是将它们保留为单独的列更有意义。

更多

默认情况下,在分组操作结束时,pandas 将所有分组列放入索引中。 可以将groupby方法中的as_index参数设置为False,以避免此行为。 您可以在分组后将reset_index方法链接起来,以获得与步骤 3 中相同的效果。让我们看一下其中的一个示例,该示例通过查找每个航空公司从每个航班出发的平均距离来得出:

>>> flights.groupby(['AIRLINE'], as_index=False)['DIST'].agg('mean') \ .round(0)

看一下先前结果中航空公司的顺序。 默认情况下,pandas 对分组列进行排序。sort参数存在于groupby方法中,并且默认为True。 您可以将其设置为False,以使分组列的顺序与在数据集中遇到分组列的顺序相同。 通过不对数据进行排序,您还将获得较小的性能提升。

自定义聚合函数

Pandas 提供了许多最常见的聚合函数,供您与分组对象一起使用。 在某些时候,您将需要编写自己的自定义用户定义函数,而这些函数在 pandas 或 NumPy 中不存在。

准备

在此秘籍中,我们使用大学数据集来计算每个州的本科生人数的均值和标准差。 然后,我们使用此信息从每个状态的任何单一总体值的均值中找到最大标准差数。

操作步骤 读取大学数据集,并按州找到本科人口的均值和标准差: >>> college = pd.read_csv('data/college.csv') >>> college.groupby('STABBR')['UGDS'].agg(['mean', 'std']) \ .round(0).head()

这个输出不是我们想要的。 我们不是在寻找整个组的均值和标准差,而是寻找任何一个机构的均值的最大标准差数。 为了计算这一点,我们需要从每个机构的本科生人数中减去各州的本科生平均人数,然后除以标准差。 这使每个群体的本科生人数标准化。 然后,我们可以利用这些分数的绝对值的最大值来找到距离均值最远的那个。 Pandas 不提供能够执行此操作的函数。 相反,我们将需要创建一个自定义函数: >>> def max_deviation(s): std_score = (s - s.mean()) / s.std() return std_score.abs().max() 定义函数后,将其直接传递给agg方法以完成聚合: >>> college.groupby('STABBR')['UGDS'].agg(max_deviation) \ .round(1).head() STABBR AK 2.6 AL 5.8 AR 6.3 AS NaN AZ 9.9 Name: UGDS, dtype: float64 工作原理

不存在预定义的 Pandas 函数来计算偏离均值的最大标准差数。 我们被迫在步骤 2 中构造一个自定义函数。请注意,此自定义函数max_deviation接受单个参数s。 展望第 3 步,您会注意到函数名称位于agg方法内,而没有直接调用。 参数s没有明确传递给max_deviation的地方。 相反,Pandas 将UGDS列作为序列隐式传递给max_deviation。

每个组都会调用一次max_deviation函数。 由于s是序列,因此所有常规的序列方法均可用。 在称为标准化的过程中,从组中的每个值中减去该特定组的平均值,然后再除以标准差。

标准化是一种常见的统计过程,用于了解各个值与平均值之间的差异。 对于正态分布,数据的 99.7% 位于平均值的三个标准差之内。

由于我们对均值的绝对偏差感兴趣,因此我们从所有标准化得分中获取绝对值并返回最大值。agg方法必须从我们的自定义函数中返回单个标量值,否则将引发异常。 Pandas 默认使用样本标准差,该样本标准差对于只有单个值的任何组均未定义。 例如,州缩写AS(美属萨摩亚)返回了缺失值,因为它在数据集中只有一个机构。

更多

可以将我们的自定义函数应用于多个聚合列。 我们只需将更多列名称添加到索引运算符。max_deviation函数仅适用于数字列:

>>> college.groupby('STABBR')['UGDS', 'SATVRMID', 'SATMTMID'] \ .agg(max_deviation).round(1).head()

您还可以将自定义的聚合函数与预构建函数一起使用。 以下是按国家和宗教派别进行的分组:

>>> college.groupby(['STABBR', 'RELAFFIL']) \ ['UGDS', 'SATVRMID', 'SATMTMID'] \ .agg([max_deviation, 'mean', 'std']).round(1).head()

请注意,pandas 使用函数名称作为返回列的名称。 您可以使用重命名方法直接更改列名称,也可以修改特殊功能属性__name__:

>>> max_deviation.__name__ 'max_deviation' >>> max_deviation.__name__ = 'Max Deviation' >>> college.groupby(['STABBR', 'RELAFFIL']) \ ['UGDS', 'SATVRMID', 'SATMTMID'] \ .agg([max_deviation, 'mean', 'std']).round(1).head()

使用*args和**kwargs自定义聚合函数

在编写自己的用户定义的自定义聚合函数时,pandas 隐式地将每个聚合列作为一个序列一次传递给它。 有时,您将需要向函数传递的参数不仅仅是序列本身。 为此,您需要了解 Python 将任意数量的参数传递给函数的能力。 在inspect模块的帮助下,让我们看一下分组对象的agg方法的签名:

>>> college = pd.read_csv('data/college.csv') >>> grouped = college.groupby(['STABBR', 'RELAFFIL']) >>> import inspect >>> inspect.signature(grouped.agg)

参数*args允许您将任意数量的非关键字参数传递给自定义的聚合函数。 同样,**kwargs允许您传递任意数量的关键字参数。

准备

在此秘籍中,我们为大学数据集构建了一个自定义函数,该函数可按州和宗教隶属关系找到本科生人口在两个值之间的学校所占的百分比。

操作步骤 定义一个函数,该函数返回大学人口在 1000 至 3,000 之间的学校的百分比: >>> def pct_between_1_3k(s): return s.between(1000, 3000).mean() 计算按州和宗教归属分类的百分比: >>> college.groupby(['STABBR', 'RELAFFIL'])['UGDS'] \ .agg(pct_between_1_3k).head(9) STABBR RELAFFIL AK 0 0.142857 1 0.000000 AL 0 0.236111 1 0.333333 AR 0 0.279412 1 0.111111 AS 0 1.000000 AZ 0 0.096774 1 0.000000 Name: UGDS, dtype: float64 该函数可以正常工作,但不能给用户提供选择上下限的灵活性。 让我们创建一个新函数,该函数允许用户定义以下范围: >>> def pct_between(s, low, high): return s.between(low, high).mean() 将此上限和下限传递给agg方法: >>> college.groupby(['STABBR', 'RELAFFIL'])['UGDS'] \ .agg(pct_between, 1000, 10000).head(9) STABBR RELAFFIL AK 0 0.428571 1 0.000000 AL 0 0.458333 1 0.375000 AR 0 0.397059 1 0.166667 AS 0 1.000000 AZ 0 0.233871 1 0.111111 Name: UGDS, dtype: float64 工作原理

步骤 1 创建一个不接受任何额外参数的函数。 上下限必须硬编码到函数本身中,这不是很灵活。 步骤 2 显示了此聚合的结果。

我们在第 3 步中创建了一个更加灵活的函数,该函数允许用户动态定义上下限。 步骤 4 是*args和**kwargs的魔力发挥作用的地方。 在此特定示例中,我们将两个非关键字参数 1,000 和 10,000 传递给agg方法。 Pandas 分别将这两个参数传递给pct_between的low和high参数。

在步骤 4 中,有几种方法可以达到相同的结果。我们可以在以下命令中明确使用参数名称来产生相同的结果:

>>> college.groupby(['STABBR', 'RELAFFIL'])['UGDS'] \ .agg(pct_between, high=10000, low=1000).head(9)

关键字参数的顺序并不重要,只要它们位于函数名称之后即可。 更进一步,我们可以混合使用非关键字和关键字参数,只要关键字参数排在最后即可:

>>> college.groupby(['STABBR', 'RELAFFIL'])['UGDS'] \ .agg(pct_between, 1000, high=10000).head(9)

为了便于理解,最好按函数签名中定义的顺序包含所有参数名称。

从技术上讲,当调用agg时,所有非关键字参数都收集到名为args的元组中,而所有关键字参数都收集到名为kwargs的字典中。

更多

不幸的是,当同时使用多个聚合函数时,Pandas 没有直接使用这些附加参数的方法。 例如,如果您希望使用pct_between和mean函数进行汇总,则会出现以下异常:

>>> college.groupby(['STABBR', 'RELAFFIL'])['UGDS'] \ .agg(['mean', pct_between], low=100, high=1000) TypeError: pct_between() missing 2 required positional arguments: 'low' and 'high'

Pandas 无法理解需要将额外的参数传递给pct_between。 为了将我们的自定义函数与其他内置函数甚至其他自定义函数一起使用,我们可以定义一种称为闭包的特殊类型的嵌套函数。 我们可以使用通用闭包来构建所有自定义函数:

>>> def make_agg_func(func, name, *args, **kwargs): def wrapper(x): return func(x, *args, **kwargs) wrapper.__name__ = name return wrapper >>> my_agg1 = make_agg_func(pct_between, 'pct_1_3k', low=1000, high=3000) >>> my_agg2 = make_agg_func(pct_between, 'pct_10_30k', 10000, 30000) >>> college.groupby(['STABBR', 'RELAFFIL'])['UGDS'] \ .agg(['mean', my_agg1, my_agg2]).head()

make_agg_func函数充当创建自定义聚合函数的工厂。 它接受您已经构建的自定义聚合函数(在这种情况下为pct_between),name参数以及任意数量的额外参数。 它返回一个已经设置了额外参数的函数。 例如,my_agg1是一个特定的定制聚合函数,可以找到大学人口在一千到三千之间的学校所占的百分比。 额外的参数(*args和**kwargs)为您的自定义函数(pct_between)指定了一组精确的参数 )。name参数非常重要,每次调用make_agg_func时必须唯一。 它将最终用于重命名聚合列。

闭包是一个在其中包含一个函数(一个嵌套函数),并返回此嵌套函数的函数。 此嵌套函数必须引用外部函数范围内的变量才能成为闭包。 在此示例中,make_agg_func是外部函数,并返回嵌套函数wrapper,该函数从外部函数访问变量func,args和kwargs。

另见 Python 任意参数列表文档 Python 闭包教程 检查分组对象

在数据帧上使用groupby方法的直接结果将是一个分组对象。 通常,我们将继续对该对象进行操作以进行聚合或转换,而无需将其保存到变量中。 在中,检查此分组对象的主要目的是检查单个组。

准备

在本秘籍中,我们通过直接在其上调用方法以及遍历其每个组来检查分组对象本身。

操作步骤 首先,将大学数据集中的州和宗教隶属关序列进行分组,然后将结果保存到变量中并确认其类型: >>> college = pd.read_csv('data/college.csv') >>> grouped = college.groupby(['STABBR', 'RELAFFIL']) >>> type(grouped) pandas.core.groupby.DataFrameGroupBy 使用dir函数发现其所有可用函数: >>> print([attr for attr in dir(grouped) if not attr.startswith('_')]) ['CITY', 'CURROPER', 'DISTANCEONLY', 'GRAD_DEBT_MDN_SUPP', 'HBCU', 'INSTNM', 'MD_EARN_WNE_P10', 'MENONLY', 'PCTFLOAN', 'PCTPELL', 'PPTUG_EF', 'RELAFFIL', 'SATMTMID', 'SATVRMID', 'STABBR', 'UG25ABV', 'UGDS', 'UGDS_2MOR', 'UGDS_AIAN', 'UGDS_ASIAN', 'UGDS_BLACK', 'UGDS_HISP', 'UGDS_NHPI', 'UGDS_NRA', 'UGDS_UNKN', 'UGDS_WHITE', 'WOMENONLY', 'agg', 'aggregate', 'all', 'any', 'apply', 'backfill', 'bfill', 'boxplot', 'corr', 'corrwith', 'count', 'cov', 'cumcount', 'cummax', 'cummin', 'cumprod', 'cumsum', 'describe', 'diff', 'dtypes', 'expanding', 'ffill', 'fillna', 'filter', 'first', 'get_group', 'groups', 'head', 'hist', 'idxmax', 'idxmin', 'indices', 'last', 'mad', 'max', 'mean', 'median', 'min', 'ndim', 'ngroup', 'ngroups', 'nth', 'nunique', 'ohlc', 'pad', 'pct_change', 'plot', 'prod', 'quantile', 'rank', 'resample', 'rolling', 'sem', 'shift', 'size', 'skew', 'std', 'sum', 'tail', 'take', 'transform', 'tshift', 'var'] 查找具有ngroups属性的组数: >>> grouped.ngroups 112 要查找每个组的唯一标识标签,请查看groups属性,该属性包含映射到该组的所有相应索引标签的每个唯一组的字典: >>> groups = list(grouped.groups.keys()) >>> groups[:6] [('AK', 0), ('AK', 1), ('AL', 0), ('AL', 1), ('AR', 0), ('AR', 1)] 通过将get_group方法传递给一个确切的组标签的元组来检索单个组。 例如,要获得佛罗里达州的所有宗教附属学校,请执行以下操作: >>> grouped.get_group(('FL', 1)).head()

您可能想看看每个单独的组。 这是可能的,因为分组对象是可迭代的: >>> from IPython.display import display >>> for name, group in grouped: print(name) display(group.head(3))

您还可以在分组对象上调用head方法,以在单个数据帧中将每个组的第一行放在一起。 >>> grouped.head(2).head(6)

工作原理

步骤 1 正式创建了分组对象。 显示所有公共属性和方法以揭示所有可能的函数(如在步骤 2 中所做的那样)很有用。每个组由元组唯一标识,该元组包含分组列中值的唯一组合。 Pandas 允许您使用第 5 步中显示的get_group方法选择特定的组作为数据帧。

很少需要遍历整个组,通常,如果有必要,应避免这样做,因为这样做可能会很慢。 有时候,您别无选择。 当通过对象遍历分组时,将为您提供一个元组,其中包含组名和数据帧,而没有分组列。 在步骤 6 中,此元组在for循环中解包为变量name和group。

在遍历组时可以做的一件有趣的事情是直接在笔记本中显示每个组的几行。 为此,可以使用IPython.display模块中的打印函数或display函数。 使用print函数可得到纯文本格式的数据帧,而没有任何不错的 HTML 格式。 使用display函数将以其常规的易于阅读的格式生成数据帧。

更多

在步骤 2 的列表中没有探索几种有用的方法。例如nth方法,当给定一个整数列表时,该方法从每个组中选择那些特定的行。 例如,以下操作从每个组中选择第一行和最后一行:

>>> grouped.nth([1, -1]).head(8)

另见 IPython display函数的官方文档 筛选少数人群居多的州

在第 4 章,“选择数据子集”中,我们在过滤掉False行之前将每一行标记为True或False。 以类似的方式,可以在过滤掉False组之前将整个数据组标记为True或False。 为此,我们首先使用groupby方法形成组,然后应用filter方法。filter方法接受必须返回True或False来指示是否保留组的函数。

在调用groupby方法之后应用的filter方法,与第 2 章“基本数据帧操作”中的数据帧filter方法完全不同。

准备

在此秘籍中,我们使用大学数据集查找非白人大学生比白人多的所有州。 由于这是来自美国的数据集,因此白人占多数,因此,我们正在寻找少数居多的州。

操作步骤 读取大学数据集,按州分组,并显示分组总数。 这应该等于从nunique序列方法检索的唯一状态数: >>> college = pd.read_csv('data/college.csv', index_col='INSTNM') >>> grouped = college.groupby('STABBR') >>> grouped.ngroups 59 >>> college['STABBR'].nunique() # verifying the same number 59 grouped变量具有filter方法,该方法接受一个自定义函数来确定是否保留组。 自定义函数将隐式传递给当前组的数据帧,并且需要返回一个布尔值。 我们定义一个函数来计算少数民族学生的总百分比,如果该百分比大于用户定义的阈值,则返回True: >>> def check_minority(df, threshold): minority_pct = 1 - df['UGDS_WHITE'] total_minority = (df['UGDS'] * minority_pct).sum() total_ugds = df['UGDS'].sum() total_minority_pct = total_minority / total_ugds return total_minority_pct > threshold 使用check_minority函数传递的filter方法和 50% 的阈值来查找具有少数多数的所有状态: >>> college_filtered = grouped.filter(check_minority, threshold=.5) >>> college_filtered.head()

仅查看输出可能并不表示实际发生了什么。数据帧以状态亚利桑那(AZ)而不是阿拉斯加(AK)开头,因此我们可以从视觉上确认某些更改。 让我们将此过滤后的数据帧的shape与原始数据进行比较。 查看结果,大约 60% 的行已被过滤,仅剩下 20 个州占少数: >>> college.shape (7535, 26) >>> college_filtered.shape (3028, 26) >>> college_filtered['STABBR'].nunique() 20 工作原理

此秘籍以州为单位查看所有机构的总人口。 目标是保留所有州中总体上占少数的所有行。 这要求我们按状态对数据进行分组,这是在步骤 1 中完成的。我们发现有 59 个独立的组。

filter分组方法将所有行保留在一个组中或将其过滤掉。 它不会更改列数。filter分组方法通过用户定义的函数(例如此秘籍中的check_minority)执行此关守。 要过滤的一个非常重要的方面是它将特定组的整个数据帧传递给用户定义的函数,并为每个组返回一个布尔值。

在check_minority函数内部,首先计算每个机构的非白人学生的百分比和总数,然后找到所有学生的总数。 最后,根据给定的阈值检查整个州的非白人学生百分比,这会产生布尔值。

最终结果是一个数据帧,其列与原始列相同,但过滤掉了不符合阈值的状态中的行。 由于过滤后的数据帧的标题可能与原始标题相同,因此您需要进行一些检查以确保操作成功完成。 我们通过检查行数和唯一状态数来验证这一点。

更多

我们的函数check_minority是灵活的,并接受参数以降低或提高少数群体阈值的百分比。 让我们检查几个其他阈值的唯一状态的形状和数量:

>>> college_filtered_20 = grouped.filter(check_minority, threshold=.2) >>> college_filtered_20.shape (7461, 26) >>> college_filtered_20['STABBR'].nunique() 57 >>> college_filtered_70 = grouped.filter(check_minority, threshold=.7) >>> college_filtered_70.shape (957, 26) >>> college_filtered_70['STABBR'].nunique() 10 另见 Pandas 过滤的官方文档 转换减肥赌注

增加减肥动机的一种方法是与他人打赌。 此秘籍中的方案将跟踪四个月内两个人的减肥情况,并确定获胜者。

准备

在此秘籍中,我们使用来自两个人的模拟数据来跟踪四个月内减肥的百分比。 在每个月底,将根据当月体重百分比最高的个人宣布获胜者。 要跟踪减肥,我们将数据按月和人分组,然后调用transform方法以查找从月初起每周每周的减肥百分比。

操作步骤 读取原始weight_loss数据集,并检查两个人Amy和Bob的第一个月数据。 每月总共有四个称量: >>> weight_loss = pd.read_csv('data/weight_loss.csv') >>> weight_loss.query('Month == "Jan"')

要确定每个月的赢家,我们只需要比较每月第一周到最后一周的减肥效果即可。 但是,如果我们想每周更新一次,我们还可以计算从当前周到每月第一周的减肥。 让我们创建一个能够提供每周更新的函数: >>> def find_perc_loss(s): return (s - s.iloc[0]) / s.iloc[0] 让我们在一月份为Bob测试此函数。 >>> bob_jan = weight_loss.query('Name=="Bob" and Month=="Jan"') >>> find_perc_loss(bob_jan['Weight']) 0 0.000000 2 -0.010309 4 -0.027491 6 -0.027491 Name: Weight, dtype: float64

您应该忽略最后一个输出中的索引值。 0、2、4 和 6 只是引用数据帧的原始行标签,与星期无关。

第一周后,鲍勃减肥了 1% 。 他在第二周继续减肥,但在最后一周没有任何进展。 我们可以将此函数应用于人和周的每个单一组合,以获得相对于每月第一周的每周减肥。 为此,我们需要将数据按Name和Month分组,然后使用transform方法应用此自定义函数: >>> pcnt_loss = weight_loss.groupby(['Name', 'Month'])['Weight'] \ .transform(find_perc_loss) >>> pcnt_loss.head(8) 0 0.000000 1 0.000000 2 -0.010309 3 -0.040609 4 -0.027491 5 -0.040609 6 -0.027491 7 -0.035533 Name: Weight, dtype: float64 transform方法必须返回与调用数据帧具有相同行数的对象。 让我们将此结果作为新列添加到原始数据帧中。 为了帮助缩短输出,我们将选择Bob的前两个月的数据: >>> weight_loss['Perc Weight Loss'] = pcnt_loss.round(3) >>> weight_loss.query('Name=="Bob" and Month in ["Jan", "Feb"]')

请注意,减肥百分比在新月后重新设置。 通过这个新的专栏,我们可以手动确定获胜者,但让我们看看是否可以找到一种自动执行此操作的方法。 由于唯一重要的一周是最后一周,所以我们选择第 4 周: >>> week4 = weight_loss.query('Week == "Week 4"') >>> week4

这缩小了周数,但仍然不会自动找出每个月的赢家。 让我们使用pivot方法重塑此数据,以便Bob和Amy每月的减肥百分比并排: >>> winner = week4.pivot(index='Month', columns='Name', values='Perc Weight Loss') >>> winner

此输出使每个月的获胜者更加清楚,但我们仍然可以走得更远。 NumPy 具有一个称为where的向量化if-then-else函数,该函数可以将序列或布尔数组映射到其他值。 让我们为获奖者的名字创建一个列,并突出显示每个月的获奖百分比: >>> winner['Winner'] = np.where(winner['Amy'] < winner['Bob'], 'Amy', 'Bob') >>> winner.style.highlight_min(axis=1)

使用value_counts方法以赢得的月份数返回最终分数: >>> winner.Winner.value_counts() Amy 3 Bob 1 Name: Winner, dtype: int64 工作原理

在整个秘籍中,query方法用于过滤数据,而不是布尔索引。 有关更多信息,请参阅第 5 章,“布尔索引”的“查询方法”秘籍,以提高布尔索引的可读性。

我们的目标是找到每个人每个月的减肥百分比。 一种完成此任务的方法是计算相对于每个月初的每周减肥。 此特定任务非常适合transform分组方法。transform方法接受一个函数作为其必需的参数。 该函数隐式地传递给每个非分组列(或仅使用在索引运算符中指定的列,如在此秘籍中使用Weight所做的那样)。 它必须返回与传递的组长度相同的值序列,否则将引发异常。 本质上,原始数据帧中的所有值都在转换。 没有聚集或过滤发生。

第 2 步创建一个函数,该函数从其所有值中减去传递的序列的第一个值,然后将该结果除以第一个值。 这将计算相对于第一个值的百分比损失(或收益)。 在第 3 步中,我们在一个月内对一个人测试了此函数。

在步骤 4 中,我们在人和周的每个组合上以相同的方式使用此函数。 从字面上看,我们正在将Weight列转换为当前一周的体重损失百分比。 为每个人输出第一个月的数据。 Pandas 将新数据作为序列返回。 该序列本身并没有什么用处,并且更有意义地作为新列附加到原始数据帧中。 我们在步骤 5 中完成此操作。

要确定获胜者,只需每月的第 4 周。 我们可以在这里停下来,手动确定获胜者,但 Pandas 提供了自动执行此功能的函数。 第 7 步中的pivot函数通过将一列的唯一值转换为新的列名称来重塑我们的数据集。index参数用于您不想旋转的列。 传递给values参数的列将平铺在index和columns参数中列的每个唯一组合上。

只有在index和columns参数中的列的每种唯一组合仅出现一次时,pivot方法才有效。 如果唯一的组合不止一个,则会引发异常。 在这种情况下,您可以使用pivot_table方法,该方法允许您将多个值聚合在一起。

枢纽化之后,我们利用高效且快速的 NumPy where函数,该函数的第一个参数是产生布尔序列的条件。True值映射到Amy,False值映射到Bob。我们突出显示每个月的获胜者,并使用value_counts方法统计最终得分。

更多

看一下第 7 步中的数据帧输出。您是否注意到月份是按字母顺序而不是按时间顺序排列的? 不幸的是,至少在这种情况下,Pandas 按字母顺序为我们排序了几个月。 我们可以通过将Month的数据类型更改为分类变量来解决此问题。 分类变量将每列的所有值映射为一个整数。 我们可以选择此映射为月份的正常时间顺序。 Pandas 在pivot方法期间使用此基础整数映射按时间顺序排列月份:

>>> week4a = week4.copy() >>> month_chron = week4a['Month'].unique() # or use drop_duplicates >>> month_chron array(['Jan', 'Feb', 'Mar', 'Apr'], dtype=object) >>> week4a['Month'] = pd.Categorical(week4a['Month'], categories=month_chron, ordered=True) >>> week4a.pivot(index='Month', columns='Name', values='Perc Weight Loss')

要转换Month列,请使用Categorical构造器。 将原始列作为序列传递,并将所有类别的唯一序列按所需顺序传递给categories参数。 由于Month列已经按时间顺序排列,因此我们可以简单地使用unique方法,该方法保留了获取所需数组的顺序。 通常,要按字母顺序以外的其他方式对对象数据类型的列进行排序,请将其转换为类别。

另见 Pandas groupby转换的官方文档 NumPy where函数的官方文档 计算每个州的 SAT 加权平均成绩

分组对象具有四个接受一个或多个函数以对每个组执行计算的方法。 这四种方法是agg,filter,transform和apply。 这些方法的前三个方法中的每个方法都有一个非常特定的输出,函数必须返回该输出。agg必须返回标量值,filter必须返回布尔值,transform必须返回与传递的组长度相同的序列。 但是,apply方法可能返回标量值,序列或什至任何形状的数据帧,因此使其非常灵活。 每个组也仅将其称为 ,这与对每个非分组列调用一次的transform和agg形成对比。apply方法能够同时对多个列进行操作时返回单个对象的能力,使得此秘籍中的计算成为可能。

准备

在此秘籍中,我们从大学数据集中计算每个州的数学和口头 SAT 分数的加权平均值。 我们根据每个学校的本科生人数对分数进行加权。

操作步骤 读取大学数据集,并在UGDS,SATMTMID或SATVRMID列中删除所有缺少值的行。 这三列中的每一列都必须具有非缺失值: >>> college = pd.read_csv('data/college.csv') >>> subset = ['UGDS', 'SATMTMID', 'SATVRMID'] >>> college2 = college.dropna(subset=subset) >>> college.shape (7535, 27) >>> college2.shape (1184, 27) 绝大多数机构没有我们三个必填列的数据,但这仍然足够继续。 接下来,创建一个用户定义的函数以仅计算 SAT 数学分数的加权平均值: >>> def weighted_math_average(df): weighted_math = df['UGDS'] * df['SATMTMID'] return int(weighted_math.sum() / df['UGDS'].sum()) 按状态分组,然后将此函数传递给apply方法: >>> college2.groupby('STABBR').apply(weighted_math_average).head() STABBR AK 503 AL 536 AR 529 AZ 569 CA 564 dtype: int64 我们成功为每个组返回了一个标量值。 让我们绕个小弯路,将相同的函数传递给agg方法,看看结果如何: >>> college2.groupby('STABBR').agg(weighted_math_average).head()

weighted_math_average函数将应用于数据帧中的每个非聚合列。 如果尝试将列限制为SATMTMID,则将出现错误,因为您将无法访问UGDS。 因此,完成对多列操作的最佳方法是使用apply: >>> college2.groupby('STABBR')['SATMTMID'] \ .agg(weighted_math_average) KeyError: 'UGDS' apply的一个不错的功能是您可以通过返回一个序列来创建多个新列。 此返回的序列的索引将是新的列名。 让我们修改一下函数,以计算两个 SAT 分数的加权平均值和算术平均值,以及每个组中机构数量的计数。 我们以序列返回以下五个值: >>> from collections import OrderedDict >>> def weighted_average(df): data = OrderedDict() weight_m = df['UGDS'] * df['SATMTMID'] weight_v = df['UGDS'] * df['SATVRMID'] wm_avg = weight_m.sum() / df['UGDS'].sum() wv_avg = weight_v.sum() / df['UGDS'].sum() data['weighted_math_avg'] = wm_avg data['weighted_verbal_avg'] = wv_avg data['math_avg'] = df['SATMTMID'].mean() data['verbal_avg'] = df['SATVRMID'].mean() data['count'] = len(df) return pd.Series(data, dtype='int') >>> college2.groupby('STABBR').apply(weighted_average).head(10)

工作原理

为了正确完成此秘籍,我们需要首先过滤没有UGDS,SATMTMID和SATVRMID值缺失的机构。 默认情况下,dropna方法删除具有一个或多个缺失值的行。 我们必须使用subset参数来限制其查找缺少值的列。

在第 2 步中,我们定义一个仅计算SATMTMID列的加权平均值的函数。 加权平均值与算术平均值的不同之处在于,每个值都乘以一定的权重。 然后将这个数量相加并除以权重之和。 在这种情况下,我们的体重就是在校学生人数。

在第 3 步中,我们将此函数传递给apply方法。 我们的函数weighted_math_average传递了每个组所有原始列的数据帧。 它返回单个标量值,即SATMTMID的加权平均值。 此时,您可能认为可以使用agg方法进行此计算。 用agg直接替换apply不起作用,因为agg返回其每个聚合列的值。

实际上,可以通过预先计算UGDS和SATMTMID的乘法来间接使用agg。

步骤 6 确实显示了apply的多功能性。 我们构建了一个新函数,该函数计算两个 SAT 列的加权平均值和算术平均值以及每个组的行数。 为了使apply创建多个列,您必须返回一个序列。 索引值用作结果数据帧中的列名。 您可以使用此方法返回任意多个值。

请注意,OrderedDict类是从collections模块导入的,该模块是标准库的一部分。 该有序字典用于存储数据。 普通的 Python 字典不能用来存储数据,因为它不保留插入顺序。

构造器pd.Series确实具有一个索引参数,您可以使用它来指定顺序,但是使用OrderedDict会更干净。

更多

在此秘籍中,我们为每个组返回一行作为序列。 通过返回数据帧,可以为每个组返回任意数量的行和列。 除了查找算术和加权均值之外,我们还查找两个 SAT 列的几何和谐波均值,然后将结果作为数据帧返回,其中数据行是均值类型的名称,列是 SAT 类型。 为了减轻我们的负担,我们使用 NumPy 函数average来计算加权平均值,并使用 SciPy 函数gmean和hmean来计算几何和调和平均值:

>>> from scipy.stats import gmean, hmean >>> def calculate_means(df): df_means = pd.DataFrame(index=['Arithmetic', 'Weighted', 'Geometric', 'Harmonic']) cols = ['SATMTMID', 'SATVRMID'] for col in cols: arithmetic = df[col].mean() weighted = np.average(df[col], weights=df['UGDS']) geometric = gmean(df[col]) harmonic = hmean(df[col]) df_means[col] = [arithmetic, weighted, geometric, harmonic] df_means['count'] = len(df) return df_means.astype(int) >>> college2.groupby('STABBR').apply(calculate_means).head(12)

另见 Pandas apply和groupby方法的官方文档 Python OrderedDict类的官方文档 SciPy stats模块的官方文档 按连续变量分组

在对 Pandas 进行分组时,通常使用具有离散重复值的列。 如果没有重复的值,则分组将毫无意义,因为每个组只有一行。 连续数字列通常具有很少的重复值,并且通常不用于形成组。 但是,如果我们可以将具有连续值的列转换为离散列,方法是将每个值放入一个桶中,四舍五入或使用其他映射,则将它们分组是有意义的。

准备

在此秘籍中,我们探索航班数据集以发现不同旅行距离的航空公司分布。 例如,这使我们能够找到在 500 到 1,000 英里之间飞行最多的航空公司。 为此,我们使用 Pandascut函数离散化每次飞行的距离。

操作步骤 读取航班数据集,并输出前五行: >>> flights = pd.read_csv('data/flights.csv') >>> flights.head()

如果要查找一定距离范围内的航空公司分布,则需要将DIST列的值放入离散的桶中。 让我们使用 pandas cut函数将数据分为五个桶: >>> bins = [-np.inf, 200, 500, 1000, 2000, np.inf] >>> cuts = pd.cut(flights['DIST'], bins=bins) >>> cuts.head() 0 (500.0, 1000.0] 1 (1000.0, 2000.0] 2 (500.0, 1000.0] 3 (1000.0, 2000.0] 4 (1000.0, 2000.0] Name: DIST, dtype: category Categories (5, interval[float64]): [(-inf, 200.0] < (200.0, 500.0] < (500.0, 1000.0] < (1000.0, 2000.0] < (2000.0, inf]] 创建有序的分类序列。 为了帮助您了解发生了什么,让我们计算每个类别的值: >>> cuts.value_counts() (500.0, 1000.0] 20659 (200.0, 500.0] 15874 (1000.0, 2000.0] 14186 (2000.0, inf] 4054 (-inf, 200.0] 3719 Name: DIST, dtype: int64 cuts序列现在可以用于形成组。 Pandas 允许您以任何希望的方式来分组。 将cuts序列传递到groupby方法,然后在AIRLINE列上调用value_counts方法以查找每个距离组的分布。 请注意,SkyWest(OO)组成了少于 200 英里的航班的 33%,但仅占 200 到 500 英里之间的航班的 16%: >>> flights.groupby(cuts)['AIRLINE'].value_counts(normalize=True) \ .round(3).head(15) DIST AIRLINE (-inf, 200.0] OO 0.326 EV 0.289 MQ 0.211 DL 0.086 AA 0.052 UA 0.027 WN 0.009 (200.0, 500.0] WN 0.194 DL 0.189 OO 0.159 EV 0.156 MQ 0.100 AA 0.071 UA 0.062 VX 0.028 Name: AIRLINE, dtype: float64 工作原理

在步骤 2 中,cut函数将DIST列的每个值放入五个仓位之一。 箱由定义边缘的六个数字序列创建。 您总是需要比容器数多一个边缘。 您可以为bins参数传递一个整数,该整数将自动创建该数目的等宽槽。 NumPy 中提供了负无穷大对象和正无穷大对象,并确保将所有值放置在桶中。 如果您的值在箱边缘之外,则将使它们丢失并且不会放置在箱中。

cuts变量现在是五个有序类别的序列。 它具有所有常规的序列方法,在步骤 3 中,使用value_counts方法来了解其分布。

非常有趣的是,pandas 允许您将groupby方法传递给任何对象。 这意味着您可以从与当前数据帧完全无关的内容中形成组。 在这里,我们将cuts变量中的值分组。 对于每个分组,我们通过将normalize设置为True,以value_counts查找每个航空公司的航班百分比。

从这个结果可以得出一些有趣的见解。 从全部结果来看,SkyWest 是领先的航空公司,飞行距离不到 200 英里,但没有超过 2,000 英里的航班。 相比之下,美国航空公司在 200 英里以下的航班中排名第五,但到目前为止,在 1,000 到 2,000 英里之间的航班最多。

更多

当按cuts变量分组时,我们可以找到更多结果。 例如,我们可以为每个距离分组找到第 25、50 和 75% 的通话时间。 由于通话时间以分钟为单位,因此我们可以除以 60 得到小时:

>>> flights.groupby(cuts)['AIR_TIME'].quantile(q=[.25, .5, .75]) \ .div(60).round(2) DIST (-inf, 200.0] 0.25 0.43 0.50 0.50 0.75 0.57 (200.0, 500.0] 0.25 0.77 0.50 0.92 0.75 1.05 (500.0, 1000.0] 0.25 1.43 0.50 1.65 0.75 1.92 (1000.0, 2000.0] 0.25 2.50 0.50 2.93 0.75 3.40 (2000.0, inf] 0.25 4.30 0.50 4.70 0.75 5.03 Name: AIR_TIME, dtype: float64

当使用cut函数时,我们可以使用此信息来创建内容丰富的字符串标签。 这些标签代替了间隔符号。 我们还可以链接unstack方法,该方法将内部索引级别转换为列名称:

>>> labels=['Under an Hour', '1 Hour', '1-2 Hours', '2-4 Hours', '4+ Hours'] >>> cuts2 = pd.cut(flights['DIST'], bins=bins, labels=labels) >>> flights.groupby(cuts2)['AIRLINE'].value_counts(normalize=True) \ .round(3) \ .unstack() \ .style.highlight_max(axis=1)

另见 Pandas cut函数的官方文档 更多秘籍请参考第 8 章, “将数据整理为整齐的格式” 计算城市之间的航班总数

在航班数据集中,我们具有始发地和目的地机场的数据。 例如,计算从休斯敦出发并降落在亚特兰大的航班数量是微不足道的。 更困难的是计算两个城市之间的航班总数,而不管始发地或目的地是哪一个。

准备

在此秘籍中,我们计算两个城市之间的航班总数,而不管始发地或目的地是哪个。 为此,我们按字母顺序对始发和目的地机场进行排序,以使机场的每种组合始终以相同的顺序出现。 然后,我们可以使用这种新的列安排来形成组,然后进行计数。

操作步骤 读取航班数据集,并找到每个始发地与目的地机场之间的航班总数: >>> flights = pd.read_csv('data/flights.csv') >>> flights_ct = flights.groupby(['ORG_AIR', 'DEST_AIR']).size() >>> flights_ct.head() ORG_AIR DEST_AIR ATL ABE 31 ABQ 16 ABY 19 ACY 6 AEX 40 dtype: int64 选择在两个方向上的休斯顿(IAH)和亚特兰大(ATL)之间的航班总数: >>> flights_ct.loc[[('ATL', 'IAH'), ('IAH', 'ATL')]] ORG_AIR DEST_AIR ATL IAH 121 IAH ATL 148 dtype: int64 我们可以简单地将这两个数字相加得出城市之间的总航班,但是有一种更有效,更自动化的解决方案可以适用于所有航班。 让我们按照字母顺序对每一行的起点和终点城市进行独立排序: >>> flights_sort = flights[['ORG_AIR', 'DEST_AIR']] \ .apply(sorted, axis=1) >>> flights_sort.head()

现在,每行都已独立排序,列名不正确。 让我们将其重命名为更通用的名称,然后再次找到所有城市之间的航班总数: >>> rename_dict = {'ORG_AIR':'AIR1', 'DEST_AIR':'AIR2'} >>> flights_sort = flights_sort.rename(columns=rename_dict) >>> flights_ct2 = flights_sort.groupby(['AIR1', 'AIR2']).size() >>> flights_ct2.head() AIR1 AIR2 ABE ATL 31 ORD 24 ABI DFW 74 ABQ ATL 16 DEN 46 dtype: int64 让我们选择亚特兰大和休斯顿之间的所有航班,并验证其是否与步骤 2 中的值之和匹配: >>> flights_ct2.loc[('ATL', 'IAH')] 269 如果我们尝试选择休斯顿和亚特兰大的航班,则会出现错误: >>> flights_ct2.loc[('IAH', 'ATL')] IndexingError: Too many indexers 工作原理

在第 1 步中,我们按起点和目的地机场列形成分组,然后将size方法应用于分组对象,该对象仅返回每个组的总行数。 请注意,我们可以将字符串size传递给agg方法以获得相同的结果。 在步骤 2 中,选择了亚特兰大和休斯顿之间每个方向的航班总数。 序列flights_count具有两个级别的多重索引。 从多重索引中选择行的一种方法是将loc索引运算符传递给精确级别值的元组。 在这里,我们实际上选择了两个不同的行('ATL', 'HOU')和('HOU', 'ATL')。 我们使用元组列表来正确执行此操作。

步骤 3 是秘籍中最相关的步骤。 对于亚特兰大和休斯顿之间的所有航班,我们只希望有一个标签,到目前为止,我们有两个标签。 如果我们按字母顺序对出发地和目的地机场的每种组合进行排序,那么我们将为机场之间的航班使用一个标签。 为此,我们使用数据帧的apply方法。 这与分组的apply方法不同。 在步骤 3 中没有形成组。

必须向数据帧的apply方法传递一个函数。 在这种情况下,它是内置的sorted函数。 默认情况下,此函数作为序列应用于每个列。 我们可以使用axis=1(或axis='index')来改变计算方向。sorted函数将每行数据隐式地作为序列传递给它。 它返回已排序的机场代码的列表。 这是将第一行作为序列传递给排序函数的示例:

>>> sorted(flights.loc[0, ['ORG_AIR', 'DEST_AIR']]) ['LAX', 'SLC']

apply方法以这种确切的方式使用sorted遍历所有行。 完成此操作后,将对每一行进行独立排序。 列名现在已无意义。 我们在下一步中对列名称进行重命名,然后执行与步骤 2 中相同的分组和汇总。这次,亚特兰大和休斯顿之间的所有航班都属于同一标签。

更多

您可能想知道为什么我们不能使用更简单的sort_values序列方法。 此方法不是独立进行排序,而是将行或列保留为单个记录,就像在进行数据分析时所期望的那样。 步骤 3 是非常昂贵的操作,需要几秒钟才能完成。 只有大约 60,000 行,因此该解决方案无法很好地扩展到更大的数据。

步骤 3 是非常昂贵的操作,需要几秒钟才能完成。 只有大约 60,000 行,因此该解决方案无法很好地扩展到更大的数据。 在所有 Pandas 中,用axis=1调用apply方法是性能最低的操作之一。 在内部,Pandas 在每行上循环,不会因 NumPy 提供任何速度提升。 如果可能,请避免将apply与axis=1一起使用。

使用 NumPy sort函数可以大大提高速度。 让我们继续使用此函数并分析其输出。 默认情况下,它将对每一行进行独立排序:

>>> data_sorted = np.sort(flights[['ORG_AIR', 'DEST_AIR']]) >>> data_sorted[:10] array([['LAX', 'SLC'], ['DEN', 'IAD'], ['DFW', 'VPS'], ['DCA', 'DFW'], ['LAX', 'MCI'], ['IAH', 'SAN'], ['DFW', 'MSY'], ['PHX', 'SFO'], ['ORD', 'STL'], ['IAH', 'SJC']], dtype=object)

返回二维 NumPy 数组。 NumPy 并不容易进行分组操作,因此让我们使用数据帧构造器创建一个新的数据帧并检查它是否等于步骤 3 中的flights_sorted数据帧:

>>> flights_sort2 = pd.DataFrame(data_sorted, columns=['AIR1', 'AIR2']) >>> fs_orig = flights_sort.rename(columns={'ORG_AIR':'AIR1', 'DEST_AIR':'AIR2'}) >>> flights_sort2.equals(fs_orig) True

由于数据帧相同,因此您可以将第 3 步替换为先前的更快排序例程。 我们来计时一下每种不同的排序方法之间的区别:

>>> %%timeit >>> flights_sort = flights[['ORG_AIR', 'DEST_AIR']] \ .apply(sorted, axis=1) 7.41 s ± 189 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) >>> %%timeit >>> data_sorted = np.sort(flights[['ORG_AIR', 'DEST_AIR']]) >>> flights_sort2 = pd.DataFrame(data_sorted, columns=['AIR1', 'AIR2']) 10.6 ms ± 453 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

NumPy 解决方案的速度比对 Pandas 使用apply快 700 倍。

另见 NumPy sort函数的官方文档 寻找最长的准时航班

对于航空公司而言,最重要的指标之一是其准时飞行表现。 美国联邦航空管理局认为,航班在比原定抵达时间至少晚 15 分钟后才抵达。 Pandas 有直接的方法来计算每个航空公司的准时航班总数和百分比。 尽管这些基本摘要统计数据是一个重要的指标,但是还有其他一些重要的计算很有趣,例如,找出每个航空公司在其始发机场的连续准点飞行时间。

准备

在此秘籍中,我们找到了每个始发机场的每家航空公司的最长连续航班准点率。 这要求列中的每个值都必须知道紧随其后的值。 为了将条纹应用到每个组之前,我们巧妙地使用了diff和cumsum方法来发现条纹。

操作步骤 在开始实际的航班数据集之前,让我们练习计算带有少量样本序列的航班的条纹: >>> s = pd.Series([0, 1, 1, 0, 1, 1, 1, 0]) >>> s 0 0 1 1 2 1 3 0 4 1 5 1 6 1 7 0 dtype: int64 我们对 1 的条纹的最终表示将是与原始序列相同长度的序列,每个条纹从 1 开始独立计数。 首先,我们使用cumsum方法: >>> s1 = s.cumsum() >>> s1 0 0 1 1 2 2 3 2 4 3 5 4 6 5 7 5 dtype: int64 现在,我们已经积累了序列中的所有值。 让我们将此序列乘以原始序列: >>> s.mul(s1) 0 0 1 1 2 2 3 0 4 3 5 4 6 5 7 0 dtype: int64 我们最初只有一个非零值。 这个结果非常接近我们的期望。 我们只需要重新开始每个连胜,而不是从累加的总和开始。 让我们链接diff方法,该方法从当前值中减去前一个值: >>> s.mul(s1).diff() 0 NaN 1 1.0 2 1.0 3 -2.0 4 3.0 5 1.0 6 1.0 7 -5.0 dtype: float64 负值表示条纹结束。 我们需要将负值向下传播到序列上,并使用它们减去步骤 2 中多余的累加。为此,我们将使用where方法使所有非负值都丢失: >>> s.mul(s1).diff().where(lambda x: x < 0) 0 NaN 1 NaN 2 NaN 3 -2.0 4 NaN 5 NaN 6 NaN 7 -5.0 dtype: float64 现在,我们可以使用ffill方法向下传播这些值: >>> s.mul(s1).diff().where(lambda x: x < 0).ffill() 0 NaN 1 NaN 2 NaN 3 -2.0 4 -2.0 5 -2.0 6 -2.0 7 -5.0 dtype: float64 最后,我们可以将此序列添加回s1,以清除多余的累积量: >>> s.mul(s1).diff().where(lambda x: x < 0).ffill() \ .add(s1, fill_value=0) 0 0.0 1 1.0 2 2.0 3 0.0 4 1.0 5 2.0 6 3.0 7 0.0 dtype: float64 现在我们有了一个连续工作的条纹查找器,我们可以找到每个航空公司和始发机场最长的条纹。 让我们读入航班数据集并创建一列以表示准时到达: >>> flights = pd.read_csv('data/flights.csv') >>> flights['ON_TIME'] = flights['ARR_DELAY'].lt(15).astype(int) >>> flights[['AIRLINE', 'ORG_AIR', 'ON_TIME']].head(10)

使用前七个步骤中的逻辑来定义一个函数,该函数返回给定序列的最大连胜数: >>> def max_streak(s): s1 = s.cumsum() return s.mul(s1).diff().where(lambda x: x < 0) \ .ffill().add(s1, fill_value=0).max() 找出每个航空公司和始发机场的最大准点到达率,以及航班总数和准点到达率。 首先,对一年中的日期和预定的出发时间进行排序: >>> flights.sort_values(['MONTH', 'DAY', 'SCHED_DEP']) \ .groupby(['AIRLINE', 'ORG_AIR'])['ON_TIME'] \ .agg(['mean', 'size', max_streak]).round(2).head()

工作原理

在 Pandas 中查找数据中的条纹并不是一项简单的操作,需要先行或后行的方法,例如diff或shift,或记住当前状态的方法,例如cumsum。 前七个步骤的最终结果是序列的长度与原始序列的长度相同,可以跟踪所有连续的序列。 在这些步骤中,我们使用mul和add方法代替它们的等效运算符(*)和(+)。 我认为,这样可以使计算从左到右的过程更加简洁。 您当然可以将它们替换为实际的运算符。

理想情况下,我们希望告诉 Pandas 在每个条纹开始时都应用cumsum方法,并在每个条纹结束后重新设置自身。 要将此信息传达给 Pandas,需要采取许多步骤。 第 2 步将整个序列中的所有结果累积起来。 其余步骤将慢慢清除所有多余的累积。 为了识别这种多余的累积,我们需要找到每个条纹的末尾并从下一个条纹的开始减去该值。

要找到每个条纹的结尾,请通过在步骤 3 中将s1乘以原始零序列和 1 来巧妙地使所有值不属于条纹零。 条纹。 很好,但是同样,我们需要消除多余的累积。 知道条纹结束的地方并不能使我们到达那里。

在第 4 步中,我们使用diff方法来查找此多余部分。diff方法获取当前值与位于距离其一定行数的任何值之间的差。 默认情况下,返回当前值与前一个值之间的差。

在步骤 4 中,只有负值才有意义。那些是连续结束后的值。 这些值需要向下传播,直到后续条纹结束。 为了消除(丢失)所有我们不关心的值,我们使用where方法,该方法采用与调用序列大小相同的条件序列。 默认情况下,所有True值保持不变,而False值丢失。where方法允许您通过将函数作为第一个参数来将调用序列用作条件的一部分。 使用一个匿名函数,该函数隐式传递给调用序列,并检查每个值是否小于零。 第 5 步的结果是一个序列,其中仅保留负值,其余更改为缺失值。

步骤 6 中的ffill方法将缺失值替换为在序列中前进/后退的最后一个非缺失值。 由于前三个值不跟随非缺失值,因此它们仍然丢失。 我们终于有了消除多余积蓄的序列。 我们将累加序列添加到步骤 6 的结果中,以使条纹全部从零开始。add方法允许我们用fill_value参数替换缺少的值。 这样就完成了在数据集中查找条纹的过程。 当执行这样的复杂逻辑时,最好使用一个小的数据集,在此您可以知道最终的输出是什么。 从第 8 步开始并在分组时建立这种寻路逻辑将是非常困难的任务。

在步骤 8 中,我们创建ON_TIME列。 值得注意的一项是,已取消的排期缺少ARR_DELAY的值,该值未通过布尔条件,因此ON_TIME列的值为零。 取消的航班与延迟的航班一样。

第 9 步将我们的逻辑从前七个步骤转变为一个函数,并链接max方法以返回最长的条纹。 由于我们的函数返回单个值,因此它正式是一个聚合函数,可以按照步骤 10 的操作传递给agg方法。为确保我们正在查看实际的连续航班,我们使用sort_values方法按日期和预定的出发时间进行排序。

更多

既然我们找到了准点到达时间最长的条纹,我们可以轻松地找到相反的地方-延迟到达的最长条纹。 以下函数为传递给它的每个组返回两行。 第一行是条纹的起点,最后一行是条纹的终点。 每行包含开始/结束条纹的月份和日期,以及条纹的总长度:

>>> def max_delay_streak(df): df = df.reset_index(drop=True) s = 1 - df['ON_TIME'] s1 = s.cumsum() streak = s.mul(s1).diff().where(lambda x: x < 0) \ .ffill().add(s1, fill_value=0) last_idx = streak.idxmax() first_idx = last_idx - streak.max() + 1 df_return = df.loc[[first_idx, last_idx], ['MONTH', 'DAY']] df_return['streak'] = streak.max() df_return.index = ['first', 'last'] df_return.index.name='type' return df_return >>> flights.sort_values(['MONTH', 'DAY', 'SCHED_DEP']) \ .groupby(['AIRLINE', 'ORG_AIR']) \ .apply(max_delay_streak) \ .sort_values('streak', ascending=False).head(10)

当我们使用分组的apply方法时,每个组的数据帧都传递给max_delay_streak函数。 在此函数内部,删除了数据帧的索引并用RangeIndex代替,以便我们轻松找到条纹的第一行和最后一行。 反转ON_TIME列,然后使用相同的逻辑查找延迟飞行的条纹。 条纹的第一行和最后一行的索引存储为变量。 然后,这些索引用于选择条纹结束的月份和日期。 我们使用数据帧返回结果。 我们标记并命名索引以使最终结果更清晰。

我们的最终结果显示了最长的延迟条纹以及第一和最后一个日期。 让我们进行调查,看看是否可以找出导致这些延迟的原因。 天气恶劣是航班延误或取消的常见原因。 从第一行开始,美国航空(AA)从达拉斯沃思堡(DFW)机场开始拥有连续 38 班延误航班,从 2015 年 2 月 26 日至 2015 年 3 月 1 日。查看 2015 年 2 月 27 日的历史天气数据 ,降雪量为 2 英寸,这是当天的记录。 这是 DFW 的主要天气事件,并给整个城市造成了严重问题。 请注意,DFW 出现了第三次最长的连胜纪录,但这次是几天前,并且是另一家航空公司。

另见 请参阅第 1 章,“Pandas 基础”的“将运算符与序列一起使用”秘籍 Pandas ffill的官方文档 八、将数据重组为整齐的表格

在本章中,我们将介绍以下主题:

使用stack将变量值整理为列名 使用melt将变量值整理为列名 同时堆叠多组变量 反转堆叠数据 在groupby聚合后解除堆叠 使用用groupby聚合复制pivot_table 重命名轴级别以方便重塑 将多个变量存储为列名时进行整理 将多个变量存储为列值时进行整理 在同一单元格中存储两个或多个值时进行整理 在列名和值中存储变量时进行整理 将多个观测单位存储在同一表中时进行整理 介绍

前几章中使用的所有数据集都没有做太多或做任何工作来更改其结构。 我们立即开始以原始形状处理数据集。 在开始更详细的分析之前,许多野外的数据集将需要大量的重组。 在某些情况下,整个项目可能只关心格式化数据,以便其他人可以轻松处理它。

有许多术语用于描述数据重组的过程,其中整齐的数据是数据科学家最常用的。 整洁的数据是 Hadley Wickham 创造的一个术语,用于描述使分析变得容易进行的数据形式。 本章将涵盖 Hadley 提出的许多想法以及如何用 Pandas 来实现它们。 要了解有关整理数据的更多信息,请阅读 Hadley 的论文。

什么是整洁的数据? Hadley 提出了三个简单的指导原则来确定数据集是否整洁:

每个变量组成一列 每个观测结果排成一行 每种观测单位组成一个表格

任何不符合这些准则的数据集都被认为是混乱的。 一旦开始将数据重组为整齐的格式,此定义将变得更有意义,但是现在,我们需要知道什么是变量,观测值和观测单位。

要获得关于变量实际含义的直觉,最好考虑一下变量名称和变量值之间的区别。 变量名称是标签,例如性别,种族,薪水和职位。 变量值是指每次观察都可能发生变化的事物,例如性别中的男性/女性或种族中的白色/黑色。 单个观测值是单个观测单位的所有变量值的集合。 为了帮助了解观察单位可能是什么,请考虑零售商店,该商店具有有关每个交易,员工,客户,物品和商店本身的数据。 这些中的每一个都可以视为观察单位,并且需要自己的表格。 将员工信息(例如,工作时间)与客户信息(例如,花费的金额)组合在同一张表中,将破坏这一整洁的原则。

解决杂乱数据的第一步是在存在杂乱数据时对其进行识别,并且存在无限可能。 Hadley 明确提到了五种最常见的混乱数据类型:

列名是值,不是变量名 多个变量存储在列名中 变量存储在行和列中 多种观测单位存储在同一表中 一个观测单位存储在多个表中

重要的是要了解,整理数据通常不涉及更改数据集的值,填写缺失的值或进行任何类型的分析。 整理数据涉及更改数据的形状或结构以符合整理原则。 整洁的数据类似于将所有工具都放在工具箱中,而不是随机散布在整个房屋中。 在工具箱中正确放置工具可以轻松完成所有其他任务。 数据格式正确后,进行进一步分析变得容易得多。

一旦发现混乱的数据,您将使用 Pandas 工具来重组数据,使数据整洁。 Pandas 提供给您的主要整洁工具是数据帧方法stack,melt,unstack和pivot。 较复杂的整理工作涉及撕裂文本,这需要str访问器。 其他辅助方法,例如rename,rename_axis,reset_index和set_index,将有助于对整洁的数据进行最终处理。

使用stack将变量值整理为列名

为了帮助理解整洁数据和混乱数据之间的差异,让我们看一下一个简单的表格,该表格可能是也可能不是整齐的:

>>> state_fruit = pd.read_csv('data/state_fruit.csv', index_col=0) >>> state_fruit

该表似乎没有任何混乱,并且该信息很容易消耗。 但是,按照整洁的原则,它实际上并不是整洁的。 每个列名称实际上是变量的值。 实际上,数据帧中甚至都没有变量名。 将凌乱的数据集转换为整洁的数据的第一步之一就是识别所有变量。 在此特定数据集中,我们具有州和水果的变量。 在问题的背景下,还没有找到任何数字数据。 我们可以将此变量标记为权重或其他任何明智的名称。

准备

这个特定的混乱数据集包含变量值作为列名。 我们将需要将这些列名称转换为列值。 在本秘籍中,我们使用stack方法将数据帧重组为整齐的形式。

操作步骤 首先,请注意,状态名称位于数据帧的索引中。 这些状态正确地垂直放置,不需要重组。 问题是列名。stack方法采用所有列名,并将其整形为垂直,作为单个索引级别: >>> state_fruit.stack() Texas Apple 12 Orange 10 Banana 40 Arizona Apple 9 Orange 7 Banana 12 Florida Apple 0 Orange 14 Banana 190 dtype: int64 注意,我们现在有了一个带有多重索引的序列。 现在索引中有两个级别。 原始索引已被推到左侧,以便为旧的列名腾出空间。 使用这一命令,我们现在基本上有了整洁的数据。 每个变量,状态,水果和重量都是垂直的。 让我们使用reset_index方法将结果转换为数据帧: >>> state_fruit_tidy = state_fruit.stack().reset_index() >>> state_fruit_tidy

现在我们的结构是正确的,但是列名没有意义。 让我们用适当的标识符替换它们: >>> state_fruit_tidy.columns = ['state', 'fruit', 'weight'] >>> state_fruit_tidy

可以直接使用鲜为人知的序列方法rename_axis来设置索引级别的名称,而不是直接更改columns属性,然后再使用reset_index: >>> state_fruit.stack()\ .rename_axis(['state', 'fruit']) state fruit Texas Apple 12 Orange 10 Banana 40 Arizona Apple 9 Orange 7 Banana 12 Florida Apple 0 Orange 14 Banana 190 dtype: int64 从这里,我们可以简单地将reset_index方法与name参数链接起来,以重现步骤 3 的输出: >>> state_fruit.stack()\ .rename_axis(['state', 'fruit'])\ .reset_index(name='weight') 工作原理

stack方法功能强大,需要花费一些时间才能完全理解和欣赏。 它接受所有列名并转置它们,因此它们成为新的最里面的索引级别。 请注意,每个旧列名称仍如何通过与每个状态配对来标记其原始值。3 x 3数据帧中有 9 个原始值,这些值被转换为具有相同数量值的单个序列。 原始的第一行数据成为结果序列中的前三个值。

在步骤 2 中重置索引后,pandas 将我们的数据帧的列默认设置为level_0,level_1和0。 这是因为调用此方法的序列具有两个未正式命名的索引级别。 Pandas 还从外部从零开始按整数引用索引。

步骤 3 显示了一种重命名列的简单直观的方法。 您可以通过将columns属性设置为等于列表来简单地为整个数据帧设置新列。

或者,可以通过链接rename_axis方法在一个步骤中设置列名称,该方法在将列表作为第一个参数传递时,将这些值用作索引级别名称。 重置索引时,Pandas 使用这些索引级别名称作为新的列名称。 此外,reset_index方法具有一个name参数,该参数对应于序列值的新列名称。

所有序列都有一个name属性,可以直接设置或使用rename方法设置。 当使用reset_index时,这个属性成为列名。

更多

使用stack的关键之一是将所有不希望转换的列都放在索引中。 最初使用索引中的状态读取此秘籍中的数据集。 让我们看一下如果不将状态读入索引,将会发生什么:

>>> state_fruit2 = pd.read_csv('data/state_fruit2.csv') >>> state_fruit2

由于状态名称不在索引中,因此在此数据帧上使用stack可将所有值整形为一个长值序列:

>>> state_fruit2.stack() 0 State Texas Apple 12 Orange 10 Banana 40 1 State Arizona Apple 9 Orange 7 Banana 12 2 State Florida Apple 0 Orange 14 Banana 190 dtype: object

这个命令将重塑所有列,这次包括状态,而这根本不是我们所需要的。 为了正确地重塑此数据,您需要首先使用set_index方法将所有未重塑的列放入索引中,然后使用stack。 下面的代码与步骤 1 产生相似的结果:

>>> state_fruit2.set_index('State').stack() 另见 Pandas 重塑和数据透视表的官方文档 Pandas stack方法的官方文档 使用melt将变量值​​整理为列名

像大多数大型 Python 库一样,Pandas 也有许多不同的方式来完成同一任务-区别通常是可读性和性能。 Pandas 包含一个名为melt的数据帧方法,该的工作原理与先前秘籍中介绍的stack方法相似,但灵活性更高。

在 Pandas 版本 0.20 之前,melt仅作为必须通过pd.melt访问的函数提供。 Pandas 仍然是一个不断发展的库,您需要期待每个新版本的变化。 Pandas 一直在推动将只能在数据帧上运行的所有函数移至方法上,例如它们对melt所做的一样。 这是使用melt的首选方法,也是本秘籍使用它的方式。 查看 Pandas 文档的“新增功能”部分,以了解所有更改的最新信息。

准备

在本秘籍中,我们使用melt方法来整理一个简单的数据帧,以变量值作为列名。

操作步骤 读取state_fruit2数据集,并确定哪些列需要转换,哪些列不需要转换: >>> state_fruit2 = pd.read_csv('data/state_fruit2.csv') >>> state_fruit2

通过将适当的列传递给id_vars和value_vars参数来使用melt方法: >>> state_fruit2.melt(id_vars=['State'], value_vars=['Apple', 'Orange', 'Banana'])

这一步为我们创建了整洁的数据。 默认情况下,melt将转换后的前列名称称为变量,并将相应的值称为值。 方便地,melt有两个附加参数var_name和value_name,它们使您能够重命名这两列: >>> state_fruit2.melt(id_vars=['State'], value_vars=['Apple', 'Orange', 'Banana'], var_name='Fruit', value_name='Weight')

工作原理

melt方法功能强大,可以显着重塑您的数据帧。 它最多包含五个参数,其中两个参数对于理解如何正确重塑数据至关重要:

id_vars是您要保留为列且不重塑形状的列名列表 value_vars是您想要重整为单个列的列名列表

id_vars或标识变量保留在同一列中,但对于传递给value_vars的每列重复一次。melt的一个关键方面是它忽略索引中的值,实际上,它默默地删除了您的索引并用默认的RangeIndex代替了它。 这意味着,如果您确实希望保留索引中的值,那么在使用melt之前,需要先重置索引。

将水平列名称转换为垂直列值的某些通用术语是“融化”,“解除堆叠”或“取消旋转”。

更多

melt方法的所有参数都是可选的,并且如果您希望所有值都位于单个列中,而它们的旧列标签位于另一个列中,则可以使用其默认值调用melt:

>>> state_fruit2.melt()

实际上,您可能有很多需要融合的变量,并且只想指定标识变量。 在这种情况下,以以下方式调用melt会产生与步骤 2 相同的结果。在融化单个列时,实际上甚至不需要列表,只需传递其字符串值即可:

>>> state_fruit2.melt(id_vars='State') 另见 Pandas melt方法的官方文档 Pandas 开发人员讨论了将melt和其他类似函数转换为方法的问题 同时堆叠多组变量

一些数据集包含多组变量作为列名,需要同时堆叠到自己的列中。 以movie数据集为例可以帮助阐明这一点。 首先,选择包含演员姓名及其对应的 Facebook 点赞的所有列:

>>> movie = pd.read_csv('data/movie.csv') >>> actor = movie[['movie_title', 'actor_1_name', 'actor_2_name', 'actor_3_name', 'actor_1_facebook_likes', 'actor_2_facebook_likes', 'actor_3_facebook_likes']] >>> actor.head()

如果我们将变量定义为电影的标题,演员名称和 Facebook 点赞数,那么我们将需要独立地堆叠两组列,而仅通过一次调用stack或melt。

准备

在本秘籍中,我们将通过同时堆叠演员名称及其与wide_to_long函数相对应的 Facebook 点赞来整理actor数据帧。

操作步骤 我们将使用通用的wide_to_long函数将数据重整为整齐的形式。 要使用此函数,我们将需要更改要堆叠的列名,以使它们以数字结尾。 我们首先创建一个用户定义的函数来更改列名: >>> def change_col_name(col_name): col_name = col_name.replace('_name', '') if 'facebook' in col_name: fb_idx = col_name.find('facebook') col_name = col_name[:5] + col_name[fb_idx - 1:] \ + col_name[5:fb_idx-1] return col_name 将此函数传递给方法以转换所有列名: >>> actor2 = actor.rename(columns=change_col_name) >>> actor2.head()

使用wide_to_long函数可同时堆叠actor和actor_facebook_likes列集: >>> stubs = ['actor', 'actor_facebook_likes'] >>> actor2_tidy = pd.wide_to_long(actor2, stubnames=stubs, i=['movie_title'], j='actor_num', sep='_') >>> actor2_tidy.head()

工作原理

wide_to_long函数以相当特定的方式工作。 它的主要参数是stubnames,它是一个字符串列表。 每个字符串代表一个列分组。 以该字符串开头的所有列都将被堆叠到一个列中。 在此秘籍中,有两列列:actor和actor_facebook_likes。 默认情况下,这些列的每个组都需要以数字结尾。 此数字随后将用于标记整形数据。 每个列组都有一个下划线字符,将stubname与结尾数字分开。 为此,必须使用sep参数。

原始列名称与wide_to_long工作所需的模式不匹配。 可以通过使用列表精确指定列名称来手动更改列名称。 这很快就会成为很多类型的输入,因此,我们定义了一个函数,该函数自动将我们的列转换为有效的格式。change_col_name函数从参与者列中删除_name,并重新排列facebook列,以便现在它们都以数字结尾。

要实际完成列重命名,我们在步骤 2 中使用rename方法。它接受许多不同类型的参数,其中之一是函数。 将其传递给函数时,每个列名都会一次隐式传递给它。

现在,我们已经正确地创建了两个独立的列组,即以actor和actor_facebook_likes开头的列,它们将被堆叠。 除此之外,wide_to_long还需要一个唯一列,即参数i,用作不会堆叠的标识变量。 还需要参数j,该参数仅重命名从原始列名的末尾去除的标识数字。 默认情况下,prefix参数包含搜索一个或多个数字的正则表达式,\d+。\d是与数字 0-9 匹配的特殊令牌。 加号+使表达式与这些数字中的一个或多个匹配。

要成为str方法的强大用户,您将需要熟悉正则表达式,这是与某些文本中的特定模式匹配的字符序列。 它们由具有特殊含义的“元字符”和“字面值”字符组成。 要使自己对正则表达式有用,请查看 Regular-Expressions.info 中的简短教程。

更多

当所有变量分组具有相同的数字结尾(如此秘籍中的数字)时,函数wide_to_long起作用。 当您的变量没有相同的结尾或不是以数字结尾时,您仍然可以使用wide_to_long同时进行列堆叠。 例如,让我们看一下以下数据集:

>>> df = pd.read_csv('data/stackme.csv') >>> df

假设我们希望将a1和b1列以及d和e列堆叠在一起。 另外,我们想使用a1和b1作为行的标签。 要完成此任务,我们需要重命名列,以便它们以所需的标签结尾:

>>> df2 = df.rename(columns = {'a1':'group1_a1', 'b2':'group1_b2', 'd':'group2_a1', 'e':'group2_b2'}) >>> df2

然后,我们需要修改后缀参数,该参数通常默认为选择数字的正则表达式。 在这里,我们只是简单地告诉它找到任意数量的字符:

>>> pd.wide_to_long(df2, stubnames=['group1', 'group2'], i=['State', 'Country', 'Test'], j='Label', suffix='.+', sep='_')

另见 Pandas wide_to_long的官方文档 反转堆叠数据

数据帧具有两种相似的方法stack和melt,用于将水平列名称转换为垂直列值。数据帧分别具有分别通过unstack和pivot方法直接反转这两个操作的能力。stack/unstack是更简单的方法,仅允许控制列/行索引,而melt/pivot提供更大的灵活性来选择要重塑的列。

准备

在此秘籍中,我们将stack/melt一个数据集,并立即将unstack/pivot的操作转换回其原始形式。

操作步骤 读取college数据集,以机构名称作为索引,并且仅包含大学生种族栏目: >>> usecol_func = lambda x: 'UGDS_' in x or x == 'INSTNM' >>> college = pd.read_csv('data/college.csv', index_col='INSTNM', usecols=usecol_func) >>> college.head()

使用stack方法将每个水平列名称转换为垂直索引级别: >>> college_stacked = college.stack() >>> college_stacked.head(18) INSTNM Alabama A ; M University UGDS_WHITE 0.0333 UGDS_BLACK 0.9353 UGDS_HISP 0.0055 UGDS_ASIAN 0.0019 UGDS_AIAN 0.0024 UGDS_NHPI 0.0019 UGDS_2MOR 0.0000 UGDS_NRA 0.0059 UGDS_UNKN 0.0138 University of Alabama at Birmingham UGDS_WHITE 0.5922 UGDS_BLACK 0.2600 UGDS_HISP 0.0283 UGDS_ASIAN 0.0518 UGDS_AIAN 0.0022 UGDS_NHPI 0.0007 UGDS_2MOR 0.0368 UGDS_NRA 0.0179 UGDS_UNKN 0.0100 dtype: float64 使用unstack序列方法将堆叠的数据转换回原始格式: >>> college_stacked.unstack() 可以先执行melt,然后执行pivot,然后执行类似的操作序列。 首先,读入数据而不将机构名称放在索引中: >>> college2 = pd.read_csv('data/college.csv', usecols=usecol_func) >>> college2.head()

使用melt方法将所有竞速列转置为单列: >>> college_melted = college2.melt(id_vars='INSTNM', var_name='Race', value_name='Percentage') >>> college_melted.head()

使用pivot方法来反转之前的结果: >>> melted_inv = college_melted.pivot(index='INSTNM', columns='Race', values='Percentage') >>> melted_inv.head()

请注意,机构名称现在已转移到索引中,而不是按其原始顺序排列。 列名称不是按其原始顺序。 要从第 4 步中完全复制起始数据帧,请使用.loc索引运算符同时选择行和列,然后重置索引: >>> college2_replication = melted_inv.loc[college2['INSTNM'], college2.columns[1:]]\ .reset_index() >>> college2.equals(college2_replication) True 工作原理

在步骤 1 中,有多种方法可以完成相同的任务。在这里,我们展示read_csv函数的多功能性。usecols参数接受我们要导入的列的列表或动态确定它们的函数。 我们使用匿名函数来检查列名是否包含UGDS_或等于INSTNM。 该函数以字符串的形式传递给每个列名,并且必须返回一个布尔值。 通过这种方式可以节省大量的内存。

步骤 2 中的stack方法将所有列名称放入最里面的索引级别,并返回一个序列。 在步骤 3 中,unstack方法通过获取最里面的索引级别中的所有值将它们转换为列名来反转此操作。

步骤 3 的结果与步骤 1 不太完全相同。 整行都缺少值,默认情况下,stack方法在步骤 2 中将其删除。 为了保留这些丢失的值并创建精确的副本,请在stack方法中使用dropna=False。

步骤 4 读取与步骤 1 相同的数据集,但没有将机构名称放入索引中,因为melt方法无法访问它。 步骤 5 使用melt方法转置所有Race列。 它通过将value_vars参数保留为其默认值None来执行此操作。 如果未指定,则id_vars参数中不存在的所有列都将转置。

步骤 6 用pivot方法反转了步骤 5 的操作,该方法接受三个参数。 每个参数都将一列作为字符串。index参数引用的列保持垂直并成为新索引。columns参数引用的列的值成为列名。values参数引用的值将平铺以对应于其先前索引和列标签的交集。

要使用pivot进行精确复制,我们需要按照与原始顺序完全相同的顺序对行和列进行排序。 由于机构名称在索引中,因此我们使用.loc索引运算符作为通过其原始索引对数据帧进行排序的方式。

更多

为了帮助进一步理解stack/unstack,让我们将它们用于转置college数据帧。

在这种情况下,我们使用矩阵转置的精确数学定义,其中新行是原始数据矩阵的旧列。

如果您看一下步骤 2 的输出,您会注意到有两个索引级别。 默认情况下,unstack方法使用最里面的索引级别作为新的列值。 索引级别从外部从零开始编号。 Pandas 默认将unstack方法的level参数设置为-1,这是指最里面的索引。 我们可以使用level=0代替unstack最外面的列:

>>> college.stack().unstack(0)

实际上,有一种非常简单的方法可以通过使用transpose方法或T属性来转置不需要stack或unstack的数据帧:

>>> college.T >>> college.transpose() 另见 请参阅第 4 章,“选择数据子集”中的“同时选择数据帧的行和列”秘籍 Pandas unstack和pivot方法的官方文档 在groupby聚合后解除堆叠

按单个列对数据进行分组并在单个列上执行聚合将返回简单易用的结果,并且易于使用。 当按多个列进行分组时,可能不会以使消耗变得容易的方式来构造结果聚合。 由于默认情况下groupby操作将唯一的分组列放在索引中,因此unstack方法对于重新排列数据非常有用,以便以对解释更有用的方式显示数据。

准备

在此秘籍中,我们使用employee数据集执行聚合,并按多列分组。 然后,我们使用unstack方法将结果重塑为一种格式,以便于比较不同组。

操作步骤 读取员工数据集,并按种族找到平均工资: >>> employee = pd.read_csv('data/employee.csv') >>> employee.groupby('RACE')['BASE_SALARY'].mean().astype(int) RACE American Indian or Alaskan Native 60272 Asian/Pacific Islander 61660 Black or African American 50137 Hispanic/Latino 52345 Others 51278 White 64419 Name: BASE_SALARY, dtype: int64 这是一个非常简单的groupby操作,可产生易于阅读且无需重塑的序列。 现在让我们按性别查找所有种族的平均工资: >>> agg = employee.groupby(['RACE', 'GENDER'])['BASE_SALARY'] \ .mean().astype(int) >>> agg RACE GENDER American Indian or Alaskan Native Female 60238 Male 60305 Asian/Pacific Islander Female 63226 Male 61033 Black or African American Female 48915 Male 51082 Hispanic/Latino Female 46503 Male 54782 Others Female 63785 Male 38771 White Female 66793 Male 63940 Name: BASE_SALARY, dtype: int64 这种聚合更加复杂,可以进行重塑以简化不同的比较。 例如,如果每个种族并排而不是像现在这样垂直,则比较男性和女性的工资会更容易。 让我们解开性别索引级别: >>> agg.unstack('GENDER')

同样,我们可以unstack竞赛索引级别: >>> agg.unstack('RACE')

工作原理

第 1 步使用单个分组列(RACE),单个聚合列(BASE_SALARY)和单个聚合函数(mean)进行最简单的聚合。 此结果易于使用,不需要任何其他处理即可求值。 第 2 步通过将种族和性别分组在一起,稍微增加了复杂性。 生成的多重索引序列在一个维中包含所有值,这使得比较更加困难。 为了使信息更易于使用,我们使用unstack方法将一个(或多个)级别中的值转换为列。

默认情况下,unstack使用最里面的索引级别作为新列。 您可以使用level参数指定要取消堆叠的确切级别,该参数接受级别名称作为字符串或级别整数位置。 最好在整数位置上使用级别名称,以避免产生歧义。 第 3 步和第 4 步将每个级别拆栈,这将导致数据帧具有单级索引。 现在,按性别比较每个种族的薪水要容易得多。

更多

如果有多个分组和聚合列,则直接结果将是数据帧而不是序列。 例如,让我们计算除平均值以外的更多聚合,如步骤 2 所示:

>>> agg2 = employee.groupby(['RACE', 'GENDER'])['BASE_SALARY'] \ .agg(['mean', 'max', 'min']).astype(int) >>> agg2

堆叠GENDER列将产生多重索引列。 从这里开始,您可以继续使用unstack和stack方法交换行和列级别,直到获得所需的数据结构为止:

>>> agg2.unstack('GENDER')

另见 请参阅第 7 章,“分组和多列聚合”的“进行聚集,过滤和转换的分组函数”秘籍 使用groupby聚合复制pivot_table

乍一看,pivot_table方法似乎提供了一种独特的数据分析方法。 但是,在进行少量按摩之后,可以使用groupby聚合完全复制其功能。 知道这种等效性可以帮助缩小 Pandas 功能的范围。

准备

在此秘籍中,我们使用flights数据集创建数据透视表,然后使用groupby操作重新创建它。

操作步骤 读取航班数据集,并使用pivot_table方法查找每个航空公司每个始发机场已取消航班的总数: >>> flights = pd.read_csv('data/flights.csv') >>> fp = flights.pivot_table(index='AIRLINE', columns='ORG_AIR', values='CANCELLED', aggfunc='sum', fill_value=0).round(2) >>> fp.head()

groupby聚合无法直接复制此表。 诀窍是首先对index和columns参数中的所有列进行分组: >>> fg = flights.groupby(['AIRLINE', 'ORG_AIR'])['CANCELLED'].sum() >>> fg.head() AIRLINE ORG_AIR AA ATL 3 DEN 4 DFW 86 IAH 3 LAS 3 Name: CANCELLED, dtype: int64 使用unstack方法将ORG_AIR索引级别转换为列名称: >>> fg_unstack = fg.unstack('ORG_AIR', fill_value=0) >>> fp.equals(fg_unstack) True 工作原理

pivot_table方法非常通用且灵活,但是执行与groupby聚合相当相似的操作,其中步骤 1 显示了一个简单示例。index参数采用一列(或多列),该列将不会被透视,并且其唯一值将放置在索引中。columns参数采用一列(或多列),该列将被透视,并且其唯一值将作为列名称。values参数采用将汇总的一列(或多列)。

还存在一个aggfunc参数,该参数带有一个或多个聚合函数,这些函数确定values参数中的列如何聚合。 它默认为均值,在此示例中,我们将其更改为计算总和。 此外,AIRLINE和ORG_AIR的某些唯一组合不存在。 这些缺失的组合将默认为结果数据帧中的缺失值。 在这里,我们使用fill_value参数将其更改为零。

步骤 2 使用index和columns参数中的所有列作为分组列开始复制过程。 这是使此秘籍生效的关键。 数据透视表只是分组列的所有唯一组合的交集。 步骤 3 通过使用unstack方法将最里面的索引级别转换为列名来完成复制。 就像pivot_table一样,并非AIRLINE和ORG_AIR的所有组合都存在。 我们再次使用fill_value参数将这些缺失的交集强制为零。

更多

可以使用groupby聚合复制更复杂的数据透视表。 例如,从pivot_table中获得以下结果:

>>> flights.pivot_table(index=['AIRLINE', 'MONTH'], columns=['ORG_AIR', 'CANCELLED'], values=['DEP_DELAY', 'DIST'], aggfunc=[np.sum, np.mean], fill_value=0)

要使用groupby聚合复制此代码,只需遵循秘籍中的相同模式,并将index和columns参数中的所有列放入groupby方法中,然后将unstack列中:

>>> flights.groupby(['AIRLINE', 'MONTH', 'ORG_AIR', 'CANCELLED']) \ ['DEP_DELAY', 'DIST'] \ .agg(['mean', 'sum']) \ .unstack(['ORG_AIR', 'CANCELLED'], fill_value=0) \ .swaplevel(0, 1, axis='columns')

有一些区别。 当像agg分组方法那样作为列表传递时,pivot_table方法不接受聚合函数作为字符串。 相反,您必须使用 NumPy 函数。 列级别的顺序也有所不同,其中pivot_table将聚合函数置于values参数中列之前的级别。 这与swaplevel方法相等,在这种情况下,该方法将切换前两个级别的顺序。

截至撰写本书时,将多个列堆叠在一起时存在一个错误,即忽略fill_value参数。 要解决此错误,请将.fillna(0)链接到代码末尾。

重命名轴级别以方便重塑

当每个轴(索引/列)级别具有名称时,使用stack/unstack方法进行重塑要容易得多。 Pandas 允许用户按整数位置或名称引用每个轴级别。 由于整数位置是隐式的而不是显式的,因此应尽可能考虑使用级别名称。 此建议来自“Python 之禅”,这是 Python 的指导原则的简短列表,一个是“显式优于隐式”。

准备

当用多列进行分组或聚合时,所得的 Pandas 对象将在一个或两个轴上具有多个级别。 在本秘籍中,我们将命名每个轴的每个级别,然后使用stack/unstack方法将数据显着重塑为所需的形式。

操作步骤 阅读大学数据集,并按机构和宗教信仰找到一些关于大学人口和 SAT 数学成绩的基本摘要统计数据: >>> college = pd.read_csv('data/college.csv') >>> cg = college.groupby(['STABBR', 'RELAFFIL']) \ ['UGDS', 'SATMTMID'] \ .agg(['size', 'min', 'max']).head(6)

请注意,两个索引级别都有名称,并且都是旧的列名称。 另一方面,列级别没有名称。 使用rename_axis方法为它们提供级别名称: >>> cg = cg.rename_axis(['AGG_COLS', 'AGG_FUNCS'], axis='columns') >>> cg

现在每个轴级别都有一个名称,重塑变得轻而易举。 使用stack方法将AGG_FUNCS列移至索引级别: >>> cg.stack('AGG_FUNCS').head()

默认情况下,堆叠会将新的列级别放置在最里面的位置。 使用swaplevel方法切换电平的位置: >>> cg.stack('AGG_FUNCS').swaplevel('AGG_FUNCS', 'STABBR', axis='index').head()

通过使用sort_index方法对级别进行排序,我们可以继续使用轴级别名称: >>> cg.stack('AGG_FUNCS') \ .swaplevel('AGG_FUNCS', 'STABBR', axis='index') \ .sort_index(level='RELAFFIL', axis='index') \ .sort_index(level='AGG_COLS', axis='columns').head(6)

为了完全重塑数据,您可能需要堆叠一些列,同时堆叠其他列。 在单个命令中将两个方法链接在一起: >>> cg.stack('AGG_FUNCS').unstack(['RELAFFIL', 'STABBR'])

一次堆叠所有列以返回序列: >>> cg.stack(['AGG_FUNCS', 'AGG_COLS']).head(12) STABBR RELAFFIL AGG_FUNCS AGG_COLS AK 0 count UGDS 7.0 SATMTMID 0.0 min UGDS 109.0 max UGDS 12865.0 1 count UGDS 3.0 SATMTMID 1.0 min UGDS 27.0 SATMTMID 503.0 max UGDS 275.0 SATMTMID 503.0 AL 0 count UGDS 71.0 SATMTMID 13.0 dtype: float64 工作原理

groupby聚合的结果通常会产生具有多个轴级别的数据帧或序列。 步骤 1 中groupby操作的结果数据帧每个轴具有多个级别。 列级别未命名,这将要求我们仅按其整数位置引用它们。 为了大大简化我们引用列级别的能力,我们使用rename_axis方法对其进行了重命名。

rename_axis方法有点奇怪,因为它可以根据传递给它的第一个参数的类型来修改级别名称和级别值。 向其传递一个列表(如果只有一个级别,则为标量)会更改级别的名称。 向其传递字典或函数会更改级别的值。 在第 2 步中,我们向rename_axis方法传递一个列表,并返回一个具有所有轴级别命名的数据帧。

一旦所有轴级别都有名称,我们就可以轻松明确地控制数据的结构。 步骤 3 将AGG_FUNCS列堆叠到最里面的索引级别。 步骤 4 中的swaplevel方法接受要交换的级别的名称或位置作为前两个参数。sort_index方法被调用两次,并对每个级别的实际值进行排序。 请注意,列级别的值是列名SATMTMID和UGDS。

通过步骤 6 进行堆叠和拆栈,我们可以得到截然不同的输出。也可以将每个单独的列级别堆叠到索引中以产生一个序列。

更多

如果您希望完全丢弃电平值,可以将它们设置为None。 当需要减少数据帧的可视输出中的混乱情况,或者很明显列级别代表什么并且不进行进一步处理时,可以采取这种措施:

>>> cg.rename_axis([None, None], axis='index') \ .rename_axis([None, None], axis='columns')

将多个变量存储为列名时进行整理

每当列名称本身包含多个不同的变量时,就会出现一种特殊的混乱数据。 当年龄和性别连接在一起时,便会出现这种情况的常见示例。 要整理这样的数据集,我们必须使用 pandas str访问器来操作列,该访问器包含用于字符串处理的其他方法。

准备

在本秘籍中,我们将首先确定所有变量,其中一些变量将被连接在一起作为列名。 然后,我们对数据进行整形并解析文本以提取正确的变量值。

操作步骤 读取男士的weightlifting数据集,并标识变量: >>> weightlifting = pd.read_csv('data/weightlifting_men.csv') >>> weightlifting

变量是体重类别,性别/年龄类别和合格总数。 年龄和性别变量已合并为一个单元格。 在将它们分开之前,让我们使用melt方法将age和sex列名称转置为单个垂直列: >>> wl_melt = weightlifting.melt(id_vars='Weight Category', var_name='sex_age', value_name='Qual Total') >>> wl_melt.head()

选择sex_age列,然后使用str访问器中可用的split方法将该列分为两个不同的列: >>> sex_age = wl_melt['sex_age'].str.split(expand=True) >>> sex_age.head()

此操作返回了一个完全独立的数据帧,具有无意义的列名。 让我们重命名列,以便我们可以显式访问它们: >>> sex_age.columns = ['Sex', 'Age Group'] >>> sex_age.head()

在str访问器之后直接使用索引运算符从Sex列中选择第一个字符: >>> sex_age['Sex'] = sex_age['Sex'].str[0] >>> sex_age.head()

使用pd.concat函数将此数据帧与wl_melt连接在一起,以生成整洁的数据集: >>> wl_cat_total = wl_melt[['Weight Category', 'Qual Total']] >>> wl_tidy = pd.concat([sex_age, wl_cat_total], axis='columns') >>> wl_tidy.head()

可以使用以下方法创建相同的结果: >>> cols = ['Weight Category', 'Qual Total'] >>> sex_age[cols] = wl_melt[cols] 工作原理

weightlifting数据集与许多数据集一样,具有原始格式的易于消化的信息,但是从技术上讲,它很混乱,因为除一个列名之外,所有其他列都包含性别和年龄信息。 一旦确定了变量,就可以开始整理数据集。 只要列名称包含变量,就需要使用melt(或stack)方法。Weight Category变量已经在正确的位置,因此我们通过将其传递给id_vars参数来将其保留为标识变量。 请注意,我们不需要明确地命名要与value_vars融合的所有列。 默认情况下,id_vars中不存在的所有列都会融化。

sex_age列需要解析,并分为两个变量。 为此,我们转向str访问器提供的额外函数,该函数仅适用于序列(单个数据帧的列)。 在这种情况下,split方法是较常见的方法之一,因为它可以将字符串的不同部分分成各自的列。 默认情况下,它在空白处分割,但是您也可以使用pat参数指定字符串或正则表达式。 当expand参数设置为True时,将为每个独立的分割字符段形成一个新列。 当False时,返回单个列,其中包含所有段的列表。

在第 4 步中重命名列之后,我们需要再次使用str访问器。 有趣的是,索引运算符可用于选择或分割字符串段。 在这里,我们选择第一个字符,这是性别变量。 我们可以更进一步,将年龄分为最小年龄和最大年龄两个单独的列,但是通常以这种方式指代整个年龄组,因此我们将其保持不变。

步骤 6 显示了将所有数据连接在一起的两种不同方法之一。concat函数接受数据帧的集合,并将它们垂直(axis='index')或水平(axis='columns')连接。 由于两个数据帧的索引相同,因此可以像第 7 步中那样将一个数据帧的值分配给另一列中的新列。

更多

从步骤 2 开始,完成此秘籍的另一种方法是直接从sex_age列中分配新列,而无需使用split方法。assign方法可用于动态添加以下新列:

>>> age_group = wl_melt.sex_age.str.extract('(\d{2}[-+](?:\d{2})?)', expand=False) >>> sex = wl_melt.sex_age.str[0] >>> new_cols = {'Sex':sex, 'Age Group': age_group} >>> wl_tidy2 = wl_melt.assign(**new_cols) \ .drop('sex_age',axis='columns') >>> wl_tidy2.sort_index(axis=1).equals(wl_tidy.sort_index(axis=1)) True

以与步骤 5 完全相同的方式找到Sex列。由于我们没有使用split,因此必须以不同的方式提取Age Group列。extract方法使用复杂的正则表达式来提取字符串的非常特定的部分。 为了正确使用extract,您的图案必须包含捕获组。 通过将圆括号括在图案的一部分周围来形成捕获组。 在此示例中,整个表达式是一个大捕获组。 它以\d{2}开头,它精确地搜索两位数,然后是字面的正负号,或者是可选的后两位。 尽管表达式的最后部分(?:\d{2})?被括号括起来,但是?:表示它实际上不是捕获组。 从技术上讲,它是一个非捕获组,用于同时表示两个数字(可选)。 不再需要sex_age列,将其删除。 最后,将两个整洁的数据帧相互比较,发现它们是等效的。

另见 有关非捕获组的更多信息,请参见网站 Regular-Expressions.info 将多个变量存储为列值时进行整理

整洁的数据集每个变量必须有一个单独的列。 有时,多个变量名放在一列中,而其对应的值放在另一列中。 这种杂乱数据的一般格式如下:

在此示例中,前三行和后三行表示两个不同的观察值,每个观察值应为行。 需要对数据进行透视,使其最终如下所示:

准备

在此秘籍中,我们确定包含结构错误的变量的列,并将其旋转以创建整洁的数据。

操作步骤 读取餐厅inspections数据集,然后将Date列数据类型转换为datetime64: >>> inspections = pd.read_csv('data/restaurant_inspections.csv', parse_dates=['Date']) >>> inspections.head()

该数据集具有两个变量Name和Date,它们分别正确地包含在单个列中。Info列本身具有五个不同的变量:Borough,Cuisine,Description,Grade和Score。 让我们尝试使用pivot方法使Name和Date列保持垂直,从Info列中的所有值中创建新列,并使用Value列作为它们的交集: >>> inspections.pivot(index=['Name', 'Date'], columns='Info', values='Value') NotImplementedError: > 1 ndim Categorical are not supported at this time 不幸的是,Pandas 开发人员尚未为我们实现此功能。 将来,这行代码很有可能会起作用。 幸运的是,在大多数情况下,Pandas 有多种完成同一任务的方法。 让我们将Name,Date和Info放入索引中: >>> inspections.set_index(['Name','Date', 'Info']).head(10)

使用unstack方法来旋转Info列中的所有值: >>> inspections.set_index(['Name','Date', 'Info']) \ .unstack('Info').head()

使用reset_index方法将索引级别分为几列: >>> insp_tidy = inspections.set_index(['Name','Date', 'Info']) \ .unstack('Info') \ .reset_index(col_level=-1) >>> insp_tidy.head()

数据集很整齐,但是有一些烦人的剩余 Pandas 残骸需要清除。 让我们使用多重索引方法droplevel删除顶部的列级别,然后将索引级别重命名为None: >>> insp_tidy.columns = insp_tidy.columns.droplevel(0) \ .rename(None) >>> insp_tidy.head()

通过使用squeeze方法将该列数据帧转换为序列,可以避免在步骤 4 中创建多重索引列。 以下代码产生与上一步相同的结果: >>> inspections.set_index(['Name','Date', 'Info']) \ .squeeze() \ .unstack('Info') \ .reset_index() \ .rename_axis(None, axis='columns') 工作原理

在第 1 步中,我们注意到在Info列中垂直放置了五个变量,在Value列中有相应的值。 因为我们需要将这五个变量中的每一个作为水平列名进行透视,所以pivot方法似乎可以工作。 不幸的是,当有多个非枢轴列时,Pandas 开发人员尚未实现这种特殊情况。 我们被迫使用另一种方法。

unstack方法还枢转垂直数据,但仅适用于索引中的数据。 第 3 步通过使用set_index方法移动将和不会旋转到索引中的两个列来开始此过程。 这些列进入索引后,即可像在步骤 3 中一样操作unstack。

请注意,当我们拆开数据帧时,pandas 会保留原始的列名(在这里,它只是一个列Value),并创建一个以旧列名为上层的多重索引。 数据集现在基本上是整齐的,但是我们继续使用reset_index方法将无枢轴的列设置为普通列。 因为我们有多重索引列,所以我们可以使用col_level参数选择新列名称所属的级别。 默认情况下,名称会插入到最高级别(级别 0)。 我们使用-1表示最底层。

毕竟,我们还有一些多余的数据帧名称和索引需要丢弃。 不幸的是,没有可以删除级别的数据帧方法,因此我们必须进入索引并使用其droplevel方法。 在这里,我们用单级列覆盖了旧的多重索引列。 这些列仍具有无用的名称属性Info,该属性已重命名为None。

通过将步骤 3 中的结果数据帧强制为序列,可以避免清理多重索引列。squeeze方法仅适用于单列数据帧,并将其转换为序列。

更多

实际上,可以使用pivot_table方法,该方法对允许多少个非透视列没有限制。pivot_table方法与pivot不同,它对与index和columns参数中的列之间的交点相对应的所有值执行汇总。 由于此交点中可能存在多个值,因此pivot_table要求用户向其传递一个汇总函数,以便输出单个值。 我们使用first汇总函数,该函数采用组中的第一个值。 在此特定示例中,每个交叉点都只有一个值,因此没有任何要累加的值。 默认的聚合函数是均值,在这里会产生错误,因为某些值是字符串:

>>> inspections.pivot_table(index=['Name', 'Date'], columns='Info', values='Value', aggfunc='first') \ .reset_index() \ .rename_axis(None, axis='columns') 另见 Pandas droplevel和squeeze方法的官方文档 在同一单元格中存储两个或多个值时进行整理

表格数据本质上是二维的,因此,可以在单个单元格中显示的信息量有限。 解决方法是,您偶尔会看到在同一单元格中存储了多个值的数据集。 整洁的数据可为每个单元格精确地提供一个值。 为了纠正这些情况,通常需要使用str序列访问器中的方法将字符串数据解析为多列。

准备

在本秘籍中,我们检查一个数据集,该数据集的每个列中都有一个包含多个不同变量的列。 我们使用str访问器将这些字符串解析为单独的列以整理数据。

操作步骤 读取texas_cities数据集,并标识变量: >>> cities = pd.read_csv('data/texas_cities.csv') >>> cities

City列看起来不错,并且仅包含一个值。 另一方面,Geolocation列包含四个变量:latitude,latitude direction,longitude和longitude direction。 让我们将Geolocation列分为四个单独的列: >>> geolocations = cities.Geolocation.str.split(pat='. ', expand=True) >>> geolocations.columns = ['latitude', 'latitude direction', 'longitude', 'longitude direction'] >>> geolocations

因为Geolocation的原始数据类型是对象,所以所有新列也是对象。 让我们将latitude和longitude更改为浮点数: >>> geolocations = geolocations.astype({'latitude':'float', 'longitude':'float'}) >>> geolocations.dtypes latitude float64 latitude direction object longitude float64 longitude direction object dtype: object 将这些新列与原始的City列连接在一起: >>> cities_tidy = pd.concat([cities['City'], geolocations], axis='columns') >>> cities_tidy

工作原理

读取数据后,我们决定数据集中有多少个变量。 在这里,我们选择将Geolocation列分为四个变量,但是我们可以只选择两个作为纬度和经度,并使用负号来区分西/东和南/北。

有几种方法可以使用str访问器中的方法来解析Geolocation列。 最简单的方法是使用split方法。 我们为它传递一个由任何字符(句点)和空格定义的简单正则表达式。 当空格跟随任何字符时,将进行分割,并形成一个新列。 该模式的首次出现在纬度的尽头。 空格紧跟度数字符,并形成分割。 分割字符将被丢弃,而不保留在结果列中。 下一个分割与逗号和空格匹配,紧跟在纬度方向之后。

总共进行了三个拆分,得到了四列。 步骤 2 的第二行为其提供了有意义的名称。 即使所得的latitude和longitude列似乎是浮点数,也并非如此。 它们最初是从对象列进行解析的,因此仍然是对象数据类型。 步骤 3 使用字典将列名称映射到其新类型。

您可以使用函数to_numeric尝试将每一列转换为整数或浮点数,而不是使用字典,如果字典有很多列名,则需要大量输入。 要在每列上迭代应用此函数,请对以下内容使用apply方法:

>>> geolocations.apply(pd.to_numeric, errors='ignore')

步骤 4 将城市连接到此新数据帧的前面,以完成整理数据的过程。

更多

split方法在此示例中使用简单的正则表达式非常有效。 对于其他示例,某些列可能会要求您根据几种不同的模式创建拆分。 要搜索多个正则表达式,请使用竖线字符|。 例如,如果我们只想分割度数符号和逗号,并在其后跟一个空格,则可以执行以下操作:

>>> cities.Geolocation.str.split(pat='° |, ', expand=True)

这将从步骤 2 返回相同的数据帧。可以使用管道字符将任意数量的其他拆分模式附加到前面的字符串模式。

extract方法是另一种出色的方法,它允许您提取每个单元格中的特定组。 这些捕获组必须用括号括起来。 结果中不存在任何括号外匹配的内容。 下一行产生与步骤 2 相同的输出:

>>> cities.Geolocation.str.extract('([0-9.]+). (N|S), ([0-9.]+). (E|W)', expand=True)

此正则表达式具有四个捕获组。 第一组和第三组至少搜索一个或多个带小数的连续数字。 第二和第四组搜索单个字符(方向)。 第一个和第三个捕获组由任何字符分隔,后跟一个空格。 第二个捕获组用逗号分隔,然后用空格隔开。

在列名和值中存储变量时进行整理

每当变量在列名称中水平存储并且在列值垂直向下存储时,就会出现一种特别难以诊断的混乱数据形式。 通常,您会遇到这种类型的数据集,而不是在数据库中,而是从其他人已经生成的汇总报告中遇到。

准备

在此秘籍中,变量在垂直和水平方向都可以识别,并通过melt和pivot_table方法重新整理为整齐的数据。

操作步骤 读取sensors数据集并标识变量: >>> sensors = pd.read_csv('data/sensors.csv') >>> sensors

正确放置在垂直列中的唯一变量是Group。Property列似乎具有三个唯一变量Pressure,Temperature和Flow。2012至2016列的其余部分本身都是一个变量,我们可以明智地将其命名为Year。 用单个数据帧方法不可能重组这种混乱的数据。 让我们从melt方法开始,将年份分为自己的专栏: >>> sensors.melt(id_vars=['Group', 'Property'], var_name='Year') \ .head(6)

这解决了我们的问题之一。 让我们使用pivot_table方法将Property列转换为新的列名称: >>> sensors.melt(id_vars=['Group', 'Property'], var_name='Year') \ .pivot_table(index=['Group', 'Year'], columns='Property', values='value') \ .reset_index() \ .rename_axis(None, axis='columns')

工作原理

一旦在步骤 1 中确定了变量,就可以开始重组。 Pandas 没有同时旋转,列的方法,因此我们必须一次完成这一任务。 我们通过将Property列传递给melt方法中的id_vars参数来保持年份垂直。

现在,结果中还有混乱的数据部分。如前面的秘籍“将多个变量存储为列值时进行整理”秘籍所述,当在index参数中使用多个列时,我们必须使用pivot_table来旋转数据帧。 旋转后,Group和Year变量卡在索引中。 我们将它们以列的形式推出。pivot_table方法将columns参数中使用的列名称保留为列索引的名称。 重置索引后,该名称变得毫无意义,我们使用rename_axis将其删除。

更多

每当涉及melt,pivot_table或pivot的解决方案时,您都可以确定存在使用stack和unstack的替代方法。 诀窍是首先将当前未旋转到索引中的列移动:

>>> sensors.set_index(['Group', 'Property']) \ .stack() \ .unstack('Property') \ .rename_axis(['Group', 'Year'], axis='index') \ .rename_axis(None, axis='columns') \ .reset_index() 将多个观测单位存储在同一表中时进行整理

当每个表包含来自单个观察单位的信息时,通常更容易维护数据。 另一方面,当所有数据都在单个表中时,更容易发现见解;对于机器学习,所有数据都必须在单个表中。 整洁的数据的重点不是直接进行分析。 相反,它正在对数据进行结构化处理,以便更轻松地进行分析,并且在一个表中有多个观察单位时,可能需要将其分成各自的表。

准备

在本秘籍中,我们使用movie数据集来识别三个观察单位(电影,演员和导演),并分别为每个观察单位创建表格。 制定此秘籍的关键之一是了解演员和导演的 Facebook 点赞与电影无关。 每个演员和导演都映射到一个表示他们的 Facebook 点赞数的单一值。 由于这种独立性,我们可以将电影,导演和演员的数据分离到各自的表中。 数据库人员将此过程标准化,这可以提高数据完整性并减少冗余。

操作步骤 读入更改后的movie数据集,并输出前五行: >>> movie = pd.read_csv('data/movie_altered.csv') >>> movie.head()

该数据集包含有关电影本身,导演和演员的信息。 这三个实体可以视为观测单位。 在开始之前,让我们使用insert方法创建一列来唯一标识每个电影: >>> movie.insert(0, 'id', np.arange(len(movie))) >>> movie.head()

让我们尝试使用wide_to_long函数整理此数据集,以将所有演员放在一列中,并将其对应的 Facebook 点赞放在另一列中,并为导演做同样的事情,即使每部电影只有一个 : >>> stubnames = ['director', 'director_fb_likes', 'actor', 'actor_fb_likes'] >>> movie_long = pd.wide_to_long(movie, stubnames=stubnames, i='id', j='num', sep='_').reset_index() >>> movie_long['num'] = movie_long['num'].astype(int) >>> movie_long.head(9)

现在可以将数据集拆分为多个较小的表: >>> movie_table = movie_long[['id', 'year', 'duration', 'rating']] >>> director_table = movie_long[['id', 'num', 'director', 'director_fb_likes']] >>> actor_table = movie_long[['id', 'num', 'actor', 'actor_fb_likes']]

   

这些表仍然存在几个问题。movie表将每个电影重复三遍,导演表的每个 ID 都有两行缺失,而一些电影的某些演员有缺失值。 让我们来照顾这​​些问题: >>> movie_entity = movie_entity.drop_duplicates() \ .reset_index(drop=True) >>> director_entity = director_entity.dropna() \ .reset_index(drop=True) >>> actor_table = actor_table.dropna() \ .reset_index(drop=True)

   

现在,我们已将观测单位分为各自的表,让我们将原始数据集的内存与这三个表进行比较: >>> movie.memory_usage(deep=True).sum() 2318234 >>> movie_table.memory_usage(deep=True).sum() + \ director_table.memory_usage(deep=True).sum() + \ actor_table.memory_usage(deep=True).sum() 2627306 实际上,我们的新整理数据会占用更多的内存。 这是可以预期的,因为原始列中的所有数据都被简单地散布到新表中。 新表还每个都有索引,并且其中两个表都有一个额外的num列,这些列占了额外的内存。 但是,我们可以利用以下事实:Facebook 点赞数与电影无关,这意味着每个演员和导演在所有电影中都有一个 Facebook 点赞数。 在执行此操作之前,我们需要创建另一个表,将每个电影映射到每个演员/导演。 首先,创建特定于演员和导演表的id列,以唯一标识每个演员/导演: >>> director_cat = pd.Categorical(director_table['director']) >>> director_table.insert(1, 'director_id', director_cat.codes) >>> actor_cat = pd.Categorical(actor_table['actor']) >>> actor_table.insert(1, 'actor_id', actor_cat.codes)

  

我们可以使用这些表形成中间表和唯一的actor/director表。 我们首先使用director表执行此操作: >>> director_associative = director_table[['id', 'director_id', 'num']] >>> dcols = ['director_id', 'director', 'director_fb_likes'] >>> director_unique = director_table[dcols].drop_duplicates() \ .reset_index(drop=True)

    

让我们对actor表做同样的事情: >>> actor_associative = actor_table[['id', 'actor_id', 'num']] >>> acols = ['actor_id', 'actor', 'actor_fb_likes'] >>> actor_unique = actor_table[acols].drop_duplicates() \ .reset_index(drop=True)

   

让我们找出我们的新表消耗了多少内存: >>> movie_table.memory_usage(deep=True).sum() + \ director_associative.memory_usage(deep=True).sum() + \ director_unique.memory_usage(deep=True).sum() + \ actor_associative.memory_usage(deep=True).sum() + \ actor_unique.memory_usage(deep=True).sum() 1833402 现在我们已经标准化了表,我们可以构建一个实体关系图,显示所有表(实体),列和关系。 此图是使用易于使用的 ERDPlus 创建的: 工作原理

导入数据并识别这三个实体后,我们必须为每个观察创建一个唯一的标识符,以便在将电影,演员和导演分成不同的表格后,可以将它们链接在一起。 在第 2 步中,我们只需将 ID 列设置为从零开始的行号。 在第 3 步中,我们使用wide_to_long函数同时melt,actor和director列。 它使用列的整数后缀垂直对齐数据,并将此整数后缀放置在索引中。 参数j用于控制其名称。 重复stubnames列表中不在列中的值以与已熔化的列对齐。

在第 4 步中,我们创建三个新表,并在每个表中保留id列。 我们还保留num列以标识确切的director/actor列。 步骤 5 通过删除重复项和缺失值来压缩每个表。

在第 5 步之后,这三个观测单位在各自的表中,但它们仍然包含与原始相同的数据量(还有更多),如步骤 6 所示。要返回memory_usage方法从object数据类型列中获得正确的字节数,必须将deep参数设置为True。

每个演员/导演在其各自的表中仅需要一个条目。 我们不能简单地列出演员姓名和 Facebook 点赞的表格,因为无法将演员链接回原始电影。 电影和演员之间的关系称为多对多关系。 每个电影与多个演员相关联,每个演员可以出现在多个电影中。 为了解决此关系,创建了一个中间表或关联表,该表包含电影和演员的唯一标识符(主键)。

要创建关联表,我们必须唯一地标识每个演员/导演。 一种技巧是使用pd.Categorical从每个演员/导演姓名中创建一个分类数据类型。 分类数据类型具有从每个值到整数的内部映射。 在codes属性中可以找到该整数,该属性用作唯一 ID。 要设置关联表的创建,我们将此唯一 ID 添加到actor/director表中。

步骤 8 和步骤 9 通过选择两个唯一标识符来创建关联表。 现在,我们可以将actor和director表简化为唯一的名称和 Facebook 点赞的名称。 这种新的表安排使用的内存比原始表少 20% 。 正式的关系数据库具有实体关系图以可视化表格。 在第 10 步中,我们使用简单的 ERDPlus 工具进行可视化,这大大简化了对表之间关系的理解。

更多

通过将所有表重新结合在一起,可以重新创建原始的movie表。 首先,将关联表连接到actor/director表。 然后旋转num列,并向后添加列前缀:

>>> actors = actor_associative.merge(actor_unique, on='actor_id') \ .drop('actor_id', 1) \ .pivot_table(index='id', columns='num', aggfunc='first') >>> actors.columns = actors.columns.get_level_values(0) + '_' + \ actors.columns.get_level_values(1).astype(str) >>> directors = director_associative.merge(director_unique, on='director_id') \ .drop('director_id', 1) \ .pivot_table(index='id', columns='num', aggfunc='first') >>> directors.columns = directors.columns.get_level_values(0) + '_' + \ directors.columns.get_level_values(1) \ .astype(str)

这些表现在可以与movie_table结合在一起:

>>> movie2 = movie_table.merge(directors.reset_index(), on='id', how='left') \ .merge(actors.reset_index(), on='id', how='left') >>> movie.equals(movie2[movie.columns]) True 另见 有关数据库规范化,关联表以及主键和外键 有关wide_to_long函数的更多信息,请参阅本章中的“同时堆叠多组变量”秘籍 九、组合 Pandas 对象

在本章中,我们将介绍以下主题:

将新行追加到数据帧 将多个数据帧连接在一起 比较特朗普总统和奥巴马总统的支持率 了解concat,join和merge之间的区别 连接到 SQL 数据库 介绍

可以使用多种选项将两个或多个数据帧或序列组合在一起。append方法最不灵活,仅允许将新行附加到数据帧。concat方法非常通用,可以在任一轴上组合任意数量的数据帧或序列。join方法通过将一个数据帧的列与其他数据帧的索引对齐来提供快速查找。merge方法提供了类似 SQL 的功能,可以将两个数据帧结合在一起。

将新行追加到数据帧

在执行数据分析时,创建新列比创建新行更为常见。 这是因为新的数据行通常代表新的观察结果,而作为分析人员,连续捕获新数据通常不是您的工作。 数据捕获通常留给其他平台,如关系数据库管理系统。 但是,这是一个必不可少的功能,因为它会不时出现。

准备

在本秘籍中,我们将首先使用.loc索引器将行追加到小型数据集,然后过渡到使用append方法。

操作步骤 读入名称数据集,并将其输出: >>> names = pd.read_csv('data/names.csv') >>> names

让我们创建一个包含一些新数据的列表,并使用.loc索引器设置一个等于该新数据的行标签: >>> new_data_list = ['Aria', 1] >>> names.loc[4] = new_data_list >>> names

.loc索引器使用标签来引用行。 在这种情况下,行标签与整数位置完全匹配。 可以使用非整数标签附加更多行: >>> names.loc['five'] = ['Zach', 3] >>> names

为了更明确地将变量与值相关联,可以使用字典。 同样,在这一步中,我们可以动态选择新的索引标签作为数据帧的长度: >>> names.loc[len(names)] = {'Name':'Zayd', 'Age':2} >>> names

序列还可以保存新数据,并且与字典完全相同: >>> names.loc[len(names)] = pd.Series({'Age':32, 'Name':'Dean'}) >>> names

前面的操作全部使用.loc索引运算符就地更改names数据帧。 没有返回的数据帧的单独副本。 在接下来的几个步骤中,我们将研究append方法,该方法不会修改调用数据帧的方法。 而是返回带有附加行的数据帧的新副本。 让我们从原始的names数据帧开始,并尝试追加一行。append的第一个参数必须是另一个数据帧,序列,字典或它们的列表,但不能是步骤 2 中的列表。让我们看看当尝试将字典与append一起使用时会发生什么: >>> names = pd.read_csv('data/names.csv') >>> names.append({'Name':'Aria', 'Age':1}) TypeError: Can only append a Series if ignore_index=True or if the Series has a name 此错误消息似乎有点不正确。 我们正在传递一个数据帧而不是一个序列,但是它为我们提供了如何更正它的说明: >>> names.append({'Name':'Aria', 'Age':1}, ignore_index=True)

这有效,但是ignore_index是一个偷偷摸摸的参数。 当设置为True时,旧索引将被完全删除并替换为 0 至n-1之间的RangeIndex。 例如,让我们为names数据帧指定一个索引: >>> names.index = ['Canada', 'Canada', 'USA', 'USA'] >>> names

重新运行步骤 7 中的代码,您将获得相同的结果。 原始索引被完全忽略。 让我们继续使用在索引中包含这些国家/地区字符串的names数据集,并通过append方法使用具有name属性的序列: >>> s = pd.Series({'Name': 'Zach', 'Age': 3}, name=len(names)) >>> s Age 3 Name Zach Name: 4, dtype: object >>> names.append(s)

append方法比.loc索引器更灵活。 它支持同时添加多行。 实现此目的的一种方法是使用序列的列表: >>> s1 = pd.Series({'Name': 'Zach', 'Age': 3}, name=len(names)) >>> s2 = pd.Series({'Name': 'Zayd', 'Age': 2}, name='USA') >>> names.append([s1, s2])

仅具有两列的小型数据帧非常简单,可以手动写出所有列名称和值。 当它们变大时,此过程将非常痛苦。 例如,让我们看一下 2016 年棒球数据集: >>> bball_16 = pd.read_csv('data/baseball16.csv') >>> bball_16.head()

该数据集包含 22 列,如果您手动输入新的数据行,则很容易输错列名称或完全忘记其中的一个。 为了帮助避免这些错误,让我们选择一行作为序列,并将to_dict方法链接到该行,以获取示例行作为字典: >>> data_dict = bball_16.iloc[0].to_dict() >>> print(data_dict) {'playerID': 'altuvjo01', 'yearID': 2016, 'stint': 1, 'teamID': 'HOU', 'lgID': 'AL', 'G': 161, 'AB': 640, 'R': 108, 'H': 216, '2B': 42, '3B': 5, 'HR': 24, 'RBI': 96.0, 'SB': 30.0, 'CS': 10.0, 'BB': 60, 'SO': 70.0, 'IBB': 11.0, 'HBP': 7.0, 'SH': 3.0, 'SF': 7.0, 'GIDP': 15.0} 用字典理解清除旧值,将任何先前的字符串值分配为空字符串,将所有其他字符串值分配为缺失值。 现在,该词典可以用作您要输入的任何新数据的模板: >>> new_data_dict = {k: '' if isinstance(v, str) else np.nan for k, v in data_dict.items()} >>> print(new_data_dict) {'playerID': '', 'yearID': nan, 'stint': nan, 'teamID': '', 'lgID': '', 'G': nan, 'AB': nan, 'R': nan, 'H': nan, '2B': nan, '3B': nan, 'HR': nan, 'RBI': nan, 'SB': nan, 'CS': nan, 'BB': nan, 'SO': nan, 'IBB': nan, 'HBP': nan, 'SH': nan, 'SF': nan, 'GIDP': nan} 工作原理

.loc索引运算符用于根据行和列标签选择和分配数据。 传递给它的第一个值表示行标签。 在步骤 2 中,names.loc[4]引用带有等于整数 4 的标签的行。此标签当前在数据帧中不存在。 赋值语句使用列表提供的数据创建新行。 如秘籍中所述,此操作将修改names数据帧本身。 如果以前存在标签等于整数 4 的行,则该命令将覆盖该行。 与append方法相比,就地进行此修改使此索引运算符的使用风险更高,该方法从未修改原始调用数据帧。

任何有效的标签都可以与.loc索引运算符一起使用,如步骤 3 所示。不管实际的新标签值是多少,新行始终将附加在最后。 即使使用列表分配也可以,但为清楚起见,最好使用字典,以便我们准确地知道与每个值关联的列,如步骤 4 所示。

步骤 5 显示了一个小技巧,可以动态地将新标签设置为数据帧中的当前行数。 只要索引标签与列名匹配,存储在序列中的数据也将得到正确分配。

其余步骤使用append方法,这是一种仅将新行追加到数据帧的简单方法。 大多数数据帧方法都允许通过axis参数进行行和列操作。append是一个例外,它只能将行追加到数据帧。

如步骤 6 中的错误消息所示,使用映射到值的列名字典不足以进行追加操作,如步骤 6 中的错误消息所示。要正确地追加没有行名的字典,您必须将ignore_index参数设置为True。 步骤 10 向您展示如何通过简单地将字典转换为序列来保持旧索引。 确保使用name参数,该参数随后将用作新的索引标签。 通过将序列列表作为第一个参数传递,可以用append方法添加任意数量的行。

当想要以更大的数据帧以这种方式附加行时,可以通过使用to_dict方法将单行转换为字典,然后使用字典推导式和一些默认值来清除所有旧值,从而避免大量键入和错误。

更多

将单行添加到数据帧是相当昂贵的操作,如果您发现自己编写了将单行数据附加到数据帧的循环,那么您做错了。 让我们首先创建 1,000 行新数据作为序列列表:

>>> random_data = [] >>> for i in range(1000): d = dict() for k, v in data_dict.items(): if isinstance(v, str): d[k] = np.random.choice(list('abcde')) else: d[k] = np.random.randint(10) random_data.append(pd.Series(d, name=i + len(bball_16))) >>> random_data[0].head() 2B 3 3B 9 AB 3 BB 9 CS 4 Name: 16, dtype: object

让我们花时间遍历每个项目一次添加一个附件需要花费多长时间:

>>> %%timeit >>> bball_16_copy = bball_16.copy() >>> for row in random_data: bball_16_copy = bball_16_copy.append(row) 4.88 s ± 190 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

仅花了 1,000 排就花了将近五秒钟。 如果我们改为通过整个序列列表,则速度会大大提高:

>>> %%timeit >>> bball_16_copy = bball_16.copy() >>> bball_16_copy = bball_16_copy.append(random_data) 78.4 ms ± 6.2 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

通过传递序列列表,时间已减少到十分之一秒以下。 在内部,pandas 将序列列表转换为单个数据帧,然后进行追加。

将多个数据帧连接在一起

通用的concat函数可将两个或多个数据帧(或序列)垂直和水平连接在一起。 通常,当同时处理多个 Pandas 对象时,连接并不是偶然发生的,而是通过它们的索引对齐每个对象。

准备

在此秘籍中,我们将水平和垂直方向的数据帧与concat函数结合在一起,然后更改参数值以产生不同的结果。

操作步骤 读取 2016 年和 2017 年的股票数据集,并将其股票代码作为索引: >>> stocks_2016 = pd.read_csv('data/stocks_2016.csv', index_col='Symbol') >>> stocks_2017 = pd.read_csv('data/stocks_2017.csv', index_col='Symbol')

    

将所有stock数据集放在一个列表中,然后调用concat函数将它们连接在一起: >>> s_list = [stocks_2016, stocks_2017] >>> pd.concat(s_list)

默认情况下,concat函数垂直连接数据帧,一个接一个。 前面的数据帧的一个问题是无法识别每一行的年份。concat函数允许使用keys参数标记每个结果数据帧。 该标签将显示在级联框架的最外层索引级别中,并强制创建多重索引。 同样,为了清楚起见,names参数还可以重命名每个索引级别: >>> pd.concat(s_list, keys=['2016', '2017'], names=['Year', 'Symbol'])

也可以通过将axis参数更改为column或 1 来水平连接: >>> pd.concat(s_list, keys=['2016', '2017'], axis='columns', names=['Year', None])

请注意,当一年中存在股票代号而另一年不存在时,会出现缺失值。 默认情况下,concat函数使用外连接,将列表中每个数据帧的所有行保留在列表中。 但是,它为我们提供了仅在两个数据帧中保留具有相同索引值的行的选项。 这称为内连接。 我们将join参数设置为inner,以更改行为: >>> pd.concat(s_list, join='inner', keys=['2016', '2017'], axis='columns', names=['Year', None])

工作原理

第一个参数是concat函数所需的唯一参数,它必须是 Pandas 对象的列表,通常是数据帧或序列的列表或字典。 默认情况下,所有这些对象将垂直堆叠在另一个之上。 在此秘籍中,仅连接了两个数据帧,但是任何数量的 Pandas 对象都可以工作。 当我们垂直连接时,数据帧通过其列名称对齐。

在此数据集中,所有列名称均相同,因此 2017 年数据中的每个列均在 2016 年数据中的同一列名称下精确对齐。 但是,如步骤 4 所示,将它们水平连接时,只有两个年份的索引标签相匹配 - AAPL和TSLA。 因此,这些股票代号在任何一年中都没有缺失值。 可以使用concat进行两种对齐方式,join参数引用的outer(默认)和inner。

更多

append方法是concat的精简版本,只能将新行附加到数据帧。 在内部,append仅调用concat函数。 例如,此秘籍中的第 2 步可以复制以下内容:

>>> stocks_2016.append(stocks_2017) 比较特朗普总统和奥巴马总统的支持率

现任美国总统的公众支持是一个经常成为新闻头条的话题,并通过民意测验进行正式衡量。 近年来,这些民意调查的频率迅速增加,并且每周都有大量新的数据发布。 有许多不同的民意测验者都有各自的问题和方法来捕获其数据,因此,数据之间存在相当多的可变性。 来自加利福尼亚大学圣塔芭芭拉分校的美国总统职位项目每天提供的总批准评级低至单个数据点。

与本书中的大多数秘籍不同,该数据在 CSV 文件中不易获得。 通常,作为数据分析师,您将需要在 Web 上查找数据,并使用可以将其抓取为可通过本地工作站解析的格式的工具。

准备

在本秘籍中,我们将使用read_html函数,该函数功能强大,可以在线从表中抓取数据并将其转换为数据帧。 您还将学习如何检查网页以查找某些元素的基础 HTML。 我使用 Google Chrome 浏览器作为浏览器,建议您将其或 Firefox 用于基于 Web 的步骤。

操作步骤 导航至唐纳德·特朗普总统的美国总统职位批准页。 您应该获得一个包含时间序列图的页面,该页面紧随其后的是表格中的数据:

read_html函数能够从网页上抓取表格并将其数据放入数据帧中。 它最适合简单的 HTML 表,并提供一些有用的参数来选择所需的确切表,以防同一页上有多个表。 让我们继续使用read_html作为其默认值,它将以列表形式将所有表作为数据帧返回: >>> base_url = 'http://www.presidency.ucsb.edu/data/popularity.php?pres={}' >>> trump_url = base_url.format(45) >>> df_list = pd.read_html(trump_url) >>> len(df_list) 14 该函数返回了 14 个表,乍一看似乎很荒谬,因为该网页似乎只显示了大多数人会识别为表的单个元素。read_html函数正式搜索以>> df0 = df_list[0] >>> df0.shape (308, 1794) >>> df0.head(7)

回顾网页,从 2017 年 1 月 22 日开始直到批准数据收集的那一天(即 2017 年 9 月 25 日),几乎每天都有批准表存在。这是八个多月或 250 行数据,该数据在某种程度上接近第一个表中的 308 行。 扫描其余的表,您会发现发现了许多空的,毫无意义的表,以及网页中实际上与表不相似的不同部分的表。 让我们使用read_html函数的一些参数来帮助我们选择所需的表。 我们可以使用match参数在表中搜索特定的字符串。 让我们搜索其中带有单词Start Date的表: >>> df_list = pd.read_html(trump_url, match='Start Date') >>> len(df_list) 3 通过在表中搜索特定的字符串,我们将表的数量减少到只有三个。 另一个有用的参数是attrs,它接受 HTML 属性及其值配对的字典。 我们想为我们的特定表找到一些独特的属性。 为此,让我们再次在数据表中单击鼠标右键。 这次,请确保单击表格标题之一的最上方。 例如,右键单击President,然后再次选择检查或检查元素:

您选择的元素应突出显示。 实际上,这不是我们感兴趣的元素。继续查看,直到遇到以>> df_list = pd.read_html(trump_url, match='Start Date', attrs={'align':'center'}) >>> len(df_list) 1 >>> trump = df_list[0] >>> trump.shape (249, 19) >>> trump.head(8)

我们仅与一个表匹配,并且行数非常接近起始日期和最后日期之间的总天数。 查看数据,似乎我们确实找到了要查找的表。 六个列的名称似乎在第 4 行。我们可以走得更远,更精确地选择要跳过的行以及要使用skiprows和header参数的列名称。 我们还可以使用parse_dates参数确保将开始日期和结束日期正确地强制为正确的数据类型: >>> df_list = pd.read_html(trump_url, match='Start Date', attrs={'align':'center'}, header=0, skiprows=[0,1,2,3,5], parse_dates=['Start Date', 'End Date']) >>> trump = df_list[0] >>> trump.head()

这几乎是我们想要的,除了缺少值的列。 让我们使用dropna方法删除缺少所有值的列: >>> trump = trump.dropna(axis=1, how='all') >>> trump.head()

让我们用ffill方法向前填充President列中的缺失值。 首先让我们检查其他列中是否缺少任何值: >>> trump.isnull().sum() President 242 Start Date 0 End Date 0 Approving 0 Disapproving 0 unsure/no data 0 dtype: int64 >>> trump = trump.ffill() trump.head()

最后,检查数据类型以确保它们正确是很重要的: >>> trump.dtypes President object Start Date datetime64[ns] End Date datetime64[ns] Approving int64 Disapproving int64 unsure/no data int64 dtype: object 让我们构建一个将所有步骤组合在一起的函数,以自动化检索任何总裁的批准数据的过程: >>> def get_pres_appr(pres_num): base_url = 'http://www.presidency.ucsb.edu/data/popularity.php?pres={}' pres_url = base_url.format(pres_num) df_list = pd.read_html(pres_url, match='Start Date', attrs={'align':'center'}, header=0, skiprows=[0,1,2,3,5], parse_dates=['Start Date', 'End Date']) pres = df_list[0].copy() pres = pres.dropna(axis=1, how='all') pres['President'] = pres['President'].ffill() return pres.sort_values('End Date') \ .reset_index(drop=True) 唯一的参数pres_num表示每个总统的批准号。 巴拉克·奥巴马是美国第 44 任总统; 将 44 传递给get_pres_appr函数以获取其批准号: >>> obama = get_pres_appr(44) >>> obama.head()

在总统富兰克林·罗斯福第三任期期间,有总统支持率的评级数据可追溯到 1941 年。 通过我们的自定义函数以及concat函数,可以从该站点获取所有总统批准评级数据。 现在,让我们获取最后五位总统的支持率数据,并输出每位总统的前三行: >>> pres_41_45 = pd.concat([get_pres_appr(x) for x in range(41,46)], ignore_index=True) >>> pres_41_45.groupby('President').head(3)

在继续之前,让我们确定是否有多个批准评级的日期: >>> pres_41_45['End Date'].value_counts().head(8) 1990-08-26 2 1990-03-11 2 1999-02-09 2 2013-10-10 2 1990-08-12 2 1992-11-22 2 1990-05-22 2 1991-09-30 1 Name: End Date, dtype: int64 只有几天有重复的值。 为了简化分析,让我们仅保留重复日期存在的第一行: >>> pres_41_45 = pres_41_45.drop_duplicates(subset='End Date') 让我们获得一些关于数据的摘要统计信息: >>> pres_41_45.shape (3679, 6) >>> pres_41_45['President'].value_counts() Barack Obama 2786 George W. Bush 270 Donald J. Trump 243 William J. Clinton 227 George Bush 153 Name: President, dtype: int64 >>> pres_41_45.groupby('President', sort=False) \ .median().round(1)

让我们在同一张图表上绘制每个总裁的支持率。 为此,我们将按每位总裁分组,遍历每组,并分别绘制每个日期的批准等级: >>> from matplotlib import cm >>> fig, ax = plt.subplots(figsize=(16,6)) >>> styles = ['-.', '-', ':', '-', ':'] >>> colors = [.9, .3, .7, .3, .9] >>> groups = pres_41_45.groupby('President', sort=False) >>> for style, color, (pres, df) in zip(styles, colors, groups): df.plot('End Date', 'Approving', ax=ax, label=pres, style=style, color=cm.Greys(color), title='Presedential Approval Rating')

此图表将所有总统依次排列。 通过将批准等级与在职天数作图,我们可以更简单地比较它们。 让我们创建一个新变量来代表上班天数: >>> days_func = lambda x: x - x.iloc[0] >>> pres_41_45['Days in Office'] = pres_41_45.groupby('President') \ ['End Date'] \ .transform(days_func) >>> pres_41_45.groupby('President').head(3)

自总统任期以来,我们已经成功地为每一行分配了相对天数。 有趣的是,新列Days in Office具有其值的字符串表示形式。 让我们检查其数据类型: >>> pres_41_45.dtypes ... Days in Office timedelta64[ns] dtype: object Days in Office列是具有纳秒精度的timedelta64对象。 这比所需的精度要高得多。 让我们通过仅获取日期将数据类型更改为整数: >>> pres_41_45['Days in Office'] = pres_41_45['Days in Office'] \ .dt.days >>> pres_41_45['Days in Office'].head() 0 0 1 32 2 35 3 43 4 46 Name: Days in Office, dtype: int64 我们可以按照与步骤 19 中相似的方式来绘制此数据,但是存在一种完全不涉及任何循环的方法。 默认情况下,在数据帧上调用plot方法时,pandas 尝试将数据的每一列绘制为线图,并使用索引作为 x 轴。 知道了这一点之后,我们就来讨论一下数据,以便每位总裁都拥有自己的专栏以进行审批: >>> pres_pivot = pres_41_45.pivot(index='Days in Office', columns='President', values='Approving') >>> pres_pivot.head()

现在,每个总裁都有自己的批准等级列,我们可以直接对每个列进行绘制而无需分组。 为了减少剧情中的混乱情况,我们将仅绘制巴拉克·奥巴马和唐纳德·特朗普: >>> plot_kwargs = dict(figsize=(16,6), color=cm.gray([.3, .7]), style=['-', '--'], title='Approval Rating') >>> pres_pivot.loc[:250, ['Donald J. Trump', 'Barack Obama']] \ .ffill().plot(**plot_kwargs)

工作原理

通常在到达所需的一个或多个表之前多次调用read_html。 您可以使用两个主要参数来指定表match和attrs。 提供给match的字符串用于查找表中实际文本的精确匹配。 这是将显示在网页本身上的文本。 另一方面,attrs参数搜索在表标记>> pres_rm = pres_41_45.groupby('President', sort=False) \ .rolling('90D', on='End Date')['Approving'] \ .mean() >>> pres_rm.head() President End Date George Bush 1989-01-26 51.000000 1989-02-27 55.500000 1989-03-02 57.666667 1989-03-10 58.750000 1989-03-13 58.200000 Name: Approving, dtype: float64

在这里,我们可以使用unstack方法重新构造数据,使其看起来与步骤 23 的输出类似,然后进行绘制:

>>> styles = ['-.', '-', ':', '-', ':'] >>> colors = [.9, .3, .7, .3, .9] >>> color = cm.Greys(colors) >>> title='90 Day Approval Rating Rolling Average' >>> plot_kwargs = dict(figsize=(16,6), style=styles, color = color, title=title) >>> correct_col_order = pres_41_45.President.unique() >>> pres_rm.unstack('President')[correct_col_order].plot(**plot_kwargs)

另见 matplotlib 的调色板参考 所有日期偏移量及其别名的列表 请参阅第 11 章,“使用 Matplotlib,Pandas 和 Seaborn 进行可视化” 了解concat,join和merge之间的区别

merge和join数据帧(而不是序列)方法以及concat函数都提供了非常相似的功能,可以将多个 Pandas 对象组合在一起。 由于它们是如此相似,并且它们在某些情况下可以相互复制,因此何时以及如何正确使用它们会变得非常混乱。 为了帮助弄清它们之间的差异,请查看以下概述:

concat: Pandas 函数 垂直或水平组合两个或多个 Pandas 对象 仅在索引上对齐 每当索引中出现重复项时发生错误 默认为外连接,带有内连接选项 join: 数据帧方法 水平组合两个或多个 Pandas 对象 将调用的数据帧的列或索引与其他对象的索引(而不是列)对齐 通过执行笛卡尔积来处理连接列/索引上的重复值 默认为左连接,带有内,外和右选项 merge: 数据帧方法 准确地水平合并两个数据帧 将调用的数据帧的列/索引与其他数据帧的列/索引对齐 通过执行笛卡尔积来处理连接列/索引上的重复值 默认为内连接,带有左,外和右选项

join方法的第一个参数是other,它可以是单个数据帧/序列,也可以是任意数量的数据帧/序列的列表。

准备

在此秘籍中,我们将执行组合数据帧所需的。 第一种情况使用concat更简单,而第二种情况使用merge更简单。

操作步骤 让我们使用循环而不是对read_csv函数的三个不同调用将 2016 年,2017 年和 2018 年的股票数据读入数据帧的列表中。 Jupyter 笔记本当前仅允许将一个数据帧显示在一行上。 但是,有一种方法可以在IPython库的帮助下自定义 HTML 输出。 用户定义的display_frames函数接受数据帧的列表并将它们全部输出到一行: >>> from IPython.display import display_html >>> years = 2016, 2017, 2018 >>> stock_tables = [pd.read_csv('data/stocks_{}.csv'.format(year), index_col='Symbol') for year in years] >>> def display_frames(frames, num_spaces=0): t_style = '>> sql_string1 = ''' select Name, time(avg(Milliseconds) / 1000, 'unixepoch') as avg_time from ( select g.Name, t.Milliseconds from genres as g join tracks as t on g.genreid == t.genreid ) group by Name order by avg_time ''' >>> pd.read_sql_query(sql_string1, engine)

要重现步骤 6 的答案,请使用以下 SQL 查询:

>>> sql_string2 = ''' select c.customerid, c.FirstName, c.LastName, sum(ii.quantity * ii.unitprice) as Total from customers as c join invoices as i on c.customerid = i.customerid join invoice_items as ii on i.invoiceid = ii.invoiceid group by c.customerid, c.FirstName, c.LastName order by Total desc ''' >>> pd.read_sql_query(sql_string2, engine)

另见 SQLAlchemy 的所有引擎配置 Pandas SQL 查询的官方文档 十、时间序列分析

在本章中,我们将介绍以下主题:

了解 Python 和 Pandas 日期工具之间的区别 智能分割时间序列 使用仅适用于日期时间索引的方法 计算每周的犯罪数量 分别汇总每周犯罪和交通事故 按工作日和年份衡量犯罪 使用日期时间索引和匿名函数进行分组 按时间戳和另一列分组 使用merge_asof,发现上次犯罪率降低了 20% 介绍

Pandas 的根源在于分析金融时间序列数据。 作者 Wes McKinney 当时对可用的 Python 工具并不满意,因此决定在他工作的对冲基金中建立 Pandas 来满足自己的需求。 从广义上讲,时间序列只是随时间推移收集的数据点。 最典型地,时间在每个数据点之间平均间隔。 Pandas 在处理日期,在不同时间段内进行汇总,对不同时间段进行采样等方面具有出色的功能。

了解 Python 和 Pandas 日期工具之间的区别

在介绍 Pandas 之前,了解并了解 Python 核心的日期和时间功能可能会有所帮助。datetime模块提供了三种不同的数据类型,date,time和datetime。 正式而言,date是一个由年,月和日组成的时刻。 例如,2013 年 6 月 7 日为日期。time由小时,分钟,秒和微秒(百万分之一秒)组成,并且未附加到任何日期。 时间的示例是 12 小时 30 分钟。datetime由日期和时间这两个元素共同组成。

另一方面,Pandas 有一个封装日期和时间的对象,称为Timestamp。 它具有纳秒级(十亿分之一秒)的精度,并且源自 NumPy 的datetime64数据类型。 Python 和 Pandas 都具有timedelta对象,在进行日期加/减时很有用。

准备

在本秘籍中,我们将首先探索 Python 的datetime模块,然后转向 Pandas 中相应的高级日期工具。

操作步骤 首先,将datetime模块导入我们的名称空间并创建date,time和datetime对象: >>> import datetime >>> date = datetime.date(year=2013, month=6, day=7) >>> time = datetime.time(hour=12, minute=30, second=19, microsecond=463198) >>> dt = datetime.datetime(year=2013, month=6, day=7, hour=12, minute=30, second=19, microsecond=463198) >>> print("date is ", date) >>> print("time is", time) >>> print("datetime is", dt) date is 2013-06-07 time is 12:30:19.463198 datetime is 2013-06-07 12:30:19.463198 让我们构造并打印出timedelta对象,这是datetime模块中的另一种主要数据类型: >>> td = datetime.timedelta(weeks=2, days=5, hours=10, minutes=20, seconds=6.73, milliseconds=99, microseconds=8) >>> print(td) 19 days, 10:20:06.829008 将此timedelta添加/减去到步骤 1 中的date和datetime对象中: >>> print('new date is', date + td) >>> print('new datetime is', dt + td) new date is 2013-06-26 new datetime is 2013-06-26 22:50:26.292206 尝试将timedelta添加到time对象是不可能的: >>> time + td TypeError: unsupported operand type(s) for +: 'datetime.time' and 'datetime.timedelta' 让我们看一下 Pandas 及其Timestamp对象,这是具有纳秒精度的时间片刻。Timestamp构造器非常灵活,可以处理各种输入: >>> pd.Timestamp(year=2012, month=12, day=21, hour=5, minute=10, second=8, microsecond=99) Timestamp('2012-12-21 05:10:08.000099') >>> pd.Timestamp('2016/1/10') Timestamp('2016-01-10 00:00:00') >>> pd.Timestamp('2014-5/10') Timestamp('2014-05-10 00:00:00') >>> pd.Timestamp('Jan 3, 2019 20:45.56') Timestamp('2019-01-03 20:45:33') >>> pd.Timestamp('2016-01-05T05:34:43.123456789') Timestamp('2016-01-05 05:34:43.123456789') 也可以将单个整数或浮点数传递给Timestamp构造器,该构造器返回的日期等于 Unix 纪元(即 1970 年 1 月 1 日)之后的纳秒数: >>> pd.Timestamp(500) Timestamp('1970-01-01 00:00:00.000000500') >>> pd.Timestamp(5000, unit='D') Timestamp('1983-09-10 00:00:00') Pandas 提供了to_datetime函数,其功能与Timestamp构造器非常相似,但在特殊情况下带有一些不同的参数。 请参阅以下示例: >>> pd.to_datetime('2015-5-13') Timestamp('2015-05-13 00:00:00') >>> pd.to_datetime('2015-13-5', dayfirst=True) Timestamp('2015-05-13 00:00:00') >>> pd.to_datetime('Start Date: Sep 30, 2017 Start Time: 1:30 pm', format='Start Date: %b %d, %Y Start Time: %I:%M %p') Timestamp('2017-09-30 13:30:00') >>> pd.to_datetime(100, unit='D', origin='2013-1-1') Timestamp('2013-04-11 00:00:00') to_datetime函数具有更多功能。 它能够将整个列表或字符串序列或整数转换为时间戳。 由于我们更可能与序列或数据帧交互,而不是与单个标量值交互,因此您比Timestamp更可能使用to_datetime: >>> s = pd.Series([10, 100, 1000, 10000]) >>> pd.to_datetime(s, unit='D') 0 1970-01-11 1 1970-04-11 2 1972-09-27 3 1997-05-19 dtype: datetime64[ns] >>> s = pd.Series(['12-5-2015', '14-1-2013', '20/12/2017', '40/23/2017']) >>> pd.to_datetime(s, dayfirst=True, errors='coerce') 0 2015-05-12 1 2013-01-14 2 2017-12-20 3 NaT dtype: datetime64[ns] >>> pd.to_datetime(['Aug 3 1999 3:45:56', '10/31/2017']) DatetimeIndex(['1999-08-03 03:45:56', '2017-10-31 00:00:00'], dtype='datetime64[ns]', freq=None) 类似于Timestamp构造器和to_datetime函数,pandas 具有Timedelta和to_timedelta来表示时间量。Timedelta构造器和to_timedelta函数都可以创建一个Timedelta对象。 与to_datetime一样,to_timedelta具有更多功能,可以将整个列表或序列转换为Timedelta对象。 >>> pd.Timedelta('12 days 5 hours 3 minutes 123456789 nanoseconds') Timedelta('12 days 05:03:00.123456') >>> pd.Timedelta(days=5, minutes=7.34) Timedelta('5 days 00:07:20.400000') >>> pd.Timedelta(100, unit='W') Timedelta('700 days 00:00:00') >>> pd.to_timedelta('67:15:45.454') Timedelta('2 days 19:15:45.454000') >>> s = pd.Series([10, 100]) >>> pd.to_timedelta(s, unit='s') 0 00:00:10 1 00:01:40 dtype: timedelta64[ns] >>> time_strings = ['2 days 24 minutes 89.67 seconds', '00:45:23.6'] >>> pd.to_timedelta(time_strings) TimedeltaIndex(['2 days 00:25:29.670000', '0 days 00:45:23.600000'], dtype='timedelta64[ns]', freq=None) 可以将时间戳添加到时间戳中或从时间戳中减去。 它们甚至可以彼此分开以返回浮点数: >>> pd.Timedelta('12 days 5 hours 3 minutes') * 2 Timedelta('24 days 10:06:00') >>> pd.Timestamp('1/1/2017') + \ pd.Timedelta('12 days 5 hours 3 minutes') * 2 Timestamp('2017-01-25 10:06:00') >>> td1 = pd.to_timedelta([10, 100], unit='s') >>> td2 = pd.to_timedelta(['3 hours', '4 hours']) >>> td1 + td2 TimedeltaIndex(['03:00:10', '04:01:40'], dtype='timedelta64[ns]', freq=None) >>> pd.Timedelta('12 days') / pd.Timedelta('3 days') 4.0 时间戳和时间增量都有大量可用作属性和方法的功能。 让我们采样其中的一些: >>> ts = pd.Timestamp('2016-10-1 4:23:23.9') >>> ts.ceil('h') Timestamp('2016-10-01 05:00:00' >>> ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second (2016, 10, 1, 4, 23, 23) >>> ts.dayofweek, ts.dayofyear, ts.daysinmonth (5, 275, 31) >>> ts.to_pydatetime() datetime.datetime(2016, 10, 1, 4, 23, 23, 900000) >>> td = pd.Timedelta(125.8723, unit='h') >>> td Timedelta('5 days 05:52:20.280000') >>> td.round('min') Timedelta('5 days 05:52:00') >>> td.components Components(days=5, hours=5, minutes=52, seconds=20, milliseconds=280, microseconds=0, nanoseconds=0) >>> td.total_seconds() 453140.28 工作原理

datetime模块是 Python 标准库的一部分,非常流行并且被广泛使用。 因此,最好对它有所了解,因为您可能会跨过它。datetime模块实际上非常简单,总共只有六种类型的对象:date,time,datetime和timedelta以及时区上的其他两个对象。 Pandas Timestamp和Timedelta对象具有datetime模块对应物的所有功能以及更多功能。 在处理时间序列时,将有可能完全保留在 Pandas 中。

步骤 1 显示了如何使用datetime模块创建日期时间,日期,时间和时间增量。 只有整数可以用作日期或时间的每个组成部分,并作为单独的参数传递。 将此与第 5 步进行比较,在第 5 步中,pandas Timestamp构造器可以接受与参数相同的组件,以及各种日期字符串。 除了整数部分和字符串,第 6 步还显示了如何将单个数字标量用作日期。 此标量的单位默认为纳秒(ns),但在第二条语句中将其更改为天(D),其他选项为小时(h),分钟(m),秒(s),毫秒(ms)和微秒(µs)。

步骤 2 详细说明了datetime模块的timedelta对象及其所有参数的构造。 再次,将其与步骤 9 中显示的 pandas Timedelta构造器进行比较,该构造器接受这些相同的参数以及字符串和标量数字。

除了仅能创建单个对象的Timestamp和Timedelta构造器之外,to_datetime和to_timedelta函数还可以将整数或字符串的整个序列转换为所需的类型 。 这些函数还提供了构造器不可用的其他几个参数。 这些参数之一是errors,默认为字符串值raise,但也可以设置为ignore或coerce。 每当无法转换字符串日期时,errors参数都会确定要采取的措施。 当设置为raise时,引发异常并且程序执行停止。 当设置为ignore时,将返回原始序列,就像进入函数之前一样。 当设置为coerce时,NaT(不是时间)对象用于表示新值。 步骤 8 的第二条语句将所有值正确转换为Timestamp,最后一个被强制变为NaT。

仅可用于to_datetime的这些参数中的另一个参数是format,当字符串包含 Pandas 无法自动识别的特定日期模式时,该参数特别有用。 在步骤 7 的第三条语句中,我们在其他一些字符中嵌入了日期时间。 我们用它们各自的格式指令替换字符串的日期和时间。

日期格式指令以单个百分号%开头,后跟单个字符。 每个指令都指定日期或时间的某些部分。 有关所有指令的表格,请参见 Python 官方文档。

更多

当将大量字符串转换为时间戳时,日期格式指令实际上可以产生很大的不同。 每当 Pandas 使用to_datetime将字符串序列转换为时间戳时,它都会搜索代表日期的大量不同字符串组合。 即使所有字符串都具有相同的格式,也是如此。 通过format参数,我们可以指定确切的日期格式,这样 Pandas 不必每次都搜索正确的日期格式。 让我们创建一个日期列表作为字符串,并使用和不使用格式指令将它们转换为时间戳的时间:

>>> date_string_list = ['Sep 30 1984'] * 10000 >>> %timeit pd.to_datetime(date_string_list, format='%b %d %Y') 35.6 ms ± 1.47 ms per loop (mean ± std. dev. of 7 runs, 10 loops each) >>> %timeit pd.to_datetime(date_string_list) 1.31 s ± 63.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

提供格式化指令可使性能提高 40 倍。

另见 Python datetime模块的官方文档 Pandas 时间序列的官方文档 Pandas 时间增量官方文档 智能分割时间序列

在第 4 章,“选择数据子集”中,彻底介绍了数据帧的选择和切片。 当数据帧具有DatetimeIndex时,将出现更多选择和切片的机会。

准备

在本秘籍中,我们将使用部分日期匹配来选择和切片带有DatetimeIndex的数据帧。

操作步骤 从hdf5文件crimes.h5读取丹佛crimes数据集,并输出列数据类型和前几行。hdf5文件格式允许有效地存储大量科学数据,并且与 CSV 文本文件完全不同。 >>> crime = pd.read_hdf('data/crime.h5', 'crime') >>> crime.dtypes OFFENSE_TYPE_ID category OFFENSE_CATEGORY_ID category REPORTED_DATE datetime64[ns] GEO_LON float64 GEO_LAT float64 NEIGHBORHOOD_ID category IS_CRIME int64 IS_TRAFFIC int64 dtype: object 请注意,有三个类别列和一个Timestamp(由 NumPy 的datetime64对象表示)。 这些数据类型是在创建数据文件时存储的,这与仅存储原始文本的 CSV 文件不同。 设置REPORTED_DATE列作为索引,以便进行智能时间戳切片: >>> crime = crime.set_index('REPORTED_DATE') >>> crime.head()

像往常一样,可以通过将值传递给.loc索引运算符来选择等于单个索引的所有行: >>> crime.loc['2016-05-12 16:45:00']

在索引中使用Timestamp时,可以选择部分匹配索引值的所有行。 例如,如果我们要获取 2016 年 5 月 5 日以后的所有罪行,则只需选择以下内容: >>> crime.loc['2016-05-12']

您不仅可以选择不正确的日期,而且可以选择整个月,一年甚至一天的小时: >>> crime.loc['2016-05'].shape (8012, 7) >>> crime.loc['2016'].shape (91076, 7) >>> crime.loc['2016-05-12 03'].shape (4, 7) 选择字符串还可以包含月份名称: >>> crime.loc['Dec 2015'].sort_index()

包含月份名称的许多其他字符串模式也可以使用: >>> crime.loc['2016 Sep, 15'].shape (252, 7) >>> crime.loc['21st October 2014 05'].shape (4, 7) 除了选择之外,您还可以使用切片符号来选择精确的数据范围: >>> crime.loc['2015-3-4':'2016-1-1'].sort_index()

请注意,无论何时何地,在结束日期实现的所有犯罪都包含在返回的结果中。 对于使用基于标签的.loc索引器的任何结果,都是如此。 您可以为切片的任何开始或结束部分提供尽可能多的精度(或缺乏精度): >>> crime.loc['2015-3-4 22':'2016-1-1 11:45:00'].sort_index()

工作原理

hdf5文件的许多不错的功能之一是它们保留每一列的数据类型的能力,从而大大减少了所需的内存。 在这种情况下,这些列中的三列存储为 pandas 类别而不是对象。 将它们存储为对象将导致内存使用量增加四倍:

>>> mem_cat = crime.memory_usage().sum() >>> mem_obj = crime.astype({'OFFENSE_TYPE_ID':'object', 'OFFENSE_CATEGORY_ID':'object', 'NEIGHBORHOOD_ID':'object'}) \ .memory_usage(deep=True).sum() >>> mb = 2 ** 20 >>> round(mem_cat / mb, 1), round(mem_obj / mb, 1) (29.4, 122.7)

为了使用索引运算符按日期智能地选择和切片行,索引必须包含日期值。 在步骤 2 中,我们将REPORTED_DATE列移到索引中,并正式创建DatetimeIndex作为新索引:

>>> crime.index[:2] DatetimeIndex(['2014-06-29 02:01:00', '2014-06-29 01:54:00'], dtype='datetime64[ns]', name='REPORTED_DATE', freq=None)

使用DatetimeIndex时,可以使用.loc索引器使用多种字符串选择行。 实际上,所有可以发送到 pandas Timestamp构造器的字符串都将在这里工作。 出乎意料的是,对于该秘籍中的任何选择或切片,实际上都没有必要使用.loc索引器。 索引运算符本身将以完全相同的方式工作。 例如,步骤 6 的第二条语句可以写为crime['21st October 2014 05']。 索引运算符通常为列保留,但只要存在DatetimeIndex,就可以灵活地使用时间戳。

就个人而言,我更喜欢在选择行时使用.loc索引器,并且始终将其本身用于索引运算符。.loc索引器是显式的,传递给它的第一个值始终用于选择行。

步骤 8 和 9 显示切片的工作方式与从先前步骤中选择的相同。 结果中将包括与片段的开始或结束值部分匹配的任何日期。

更多

我们原始的犯罪数据帧未排序,并且切片仍按预期工作。 对索引进行排序将导致性能大幅提高。 让我们看一下与第 8 步完成的切片的区别:

>>> %timeit crime.loc['2015-3-4':'2016-1-1'] 39.6 ms ± 2.77 ms per loop (mean ± std. dev. of 7 runs, 10 loops each) >>> crime_sort = crime.sort_index() >>> %timeit crime_sort.loc['2015-3-4':'2016-1-1'] 758 µs ± 42.1 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

排序后的数据帧与原始数据相比,性能提高了 50 倍。

另见 请参阅第 4 章,“选择数据子集” 使用仅适用于日期时间索引的方法

有许多仅适用于日期时间索引的数据帧/序列方法。 如果索引为任何其他类型,则这些方法将失败。

准备

在本秘籍中,我们将首先使用方法按照时间成分选择数据行。 然后,我们将学习功能强大的日期偏移对象及其别名。

操作步骤 读取犯罪 HDF5 数据集,将索引设置为REPORTED_DATE,并确保我们具有日期时间索引: >>> crime = pd.read_hdf('data/crime.h5', 'crime') \ .set_index('REPORTED_DATE') >>> print(type(crime.index)) 使用between_time方法选择在凌晨 2 点到凌晨 5 点之间发生的所有犯罪,无论日期如何: >>> crime.between_time('2:00', '5:00', include_end=False).head()

使用at_time选择特定时间的所有日期: >>> crime.at_time('5:47').head()

first方法提供了一种选择前n个时间段的优雅方法,其中n是整数。 这些时间段由可以在pd.offsets模块中的DateOffset对象正式表示。 必须按其索引对数据帧进行排序,以确保此方法可以工作。 让我们选择犯罪数据的前六个月: >>> crime_sort = crime.sort_index() >>> crime_sort.first(pd.offsets.MonthBegin(6))

这捕获了从 1 月到 6 月的数据,但令人惊讶的是,在 7 月选择了一行。 原因是 Pandas 实际上使用了索引中第一个元素的时间分量,在此示例中为6分钟。 让我们使用MonthEnd,这是一个稍微不同的偏移量: >>> crime_sort.first(pd.offsets.MonthEnd(6))

这捕获了几乎相同数量的数据,但是如果仔细观察,仅捕获了 6 月 30 日以来的一行。 同样,这是因为保留了第一个索引的时间部分。 确切的搜索结果为2012-06-30 00:06:00。 那么,我们如何才能准确地获得六个月的数据呢? 有两种方法。 所有DateOffset都有一个normalize参数,当设置为True时,会将所有时间分量设置为零。 以下应该使我们非常接近我们想要的: >>> crime_sort.first(pd.offsets.MonthBegin(6, normalize=True))

此方法已成功捕获了一年前六个月的所有数据。 在将normalize设置为True的情况下,搜索到2012-07-01 00:00:00,它实际上将包括该日期和时间确切报告的任何犯罪。 实际上,无法使用第一种方法来确保仅捕获从一月到六月的数据。 以下非常简单的切片将产生准确的结果: >>> crime_sort.loc[:'2012-06'] 有十二个日期偏移对象,可以非常精确地向前或向后移动到下一个最近的偏移量。 您可以使用名为偏移别名的字符串代替在pd.offsets中查找日期偏移对象。 例如,月末的字符串是M,月初的字符串是MS。 要表示这些偏移别名的数量,只需在其前面放置一个整数。 使用此表查找所有别名。 让我们看一下偏移别名的一些示例,其中包含对注释中所选内容的描述: >>> crime_sort.first('5D') # 5 days >>> crime_sort.first('5B') # 5 business days >>> crime_sort.first('7W') # 7 weeks, with weeks ending on Sunday >>> crime_sort.first('3QS') # 3rd quarter start >>> crime_sort.first('A') # one year end 工作原理

一旦确保索引为日期时间索引,就可以利用本秘籍中的所有方法。 使用.loc索引器无法仅根据Timestamp的时间成分进行选择或切片。 要按时间范围选择所有日期,必须使用between_time方法,或者要选择确切的时间,请使用at_time。 确保为开始时间和结束时间传递的字符串至少包含小时和分钟。 也可以使用datetime模块中的time对象。 例如,以下命令将产生与步骤 2 相同的结果:

>>> import datetime >>> crime.between_time(datetime.time(2,0), datetime.time(5,0), include_end=False)

在第 4 步中,我们开始使用简单的first方法,但使用复杂的参数offset。 它必须是日期偏移对象,也可以是字符串的偏移别名。 为了帮助理解日期偏移对象,最好查看它们对单个Timestamp的作用。 例如,让我们采用索引的第一个元素,并以两种不同的方式为其添加六个月的时间:

>>> first_date = crime_sort.index[0] >>> first_date Timestamp('2012-01-02 00:06:00') >>> first_date + pd.offsets.MonthBegin(6) Timestamp('2012-07-01 00:06:00') >>> first_date + pd.offsets.MonthEnd(6) Timestamp('2012-06-30 00:06:00')

MonthBegin和MonthEnd偏移量都不会增加或减少确切的时间量,而是有效地向上舍入到下个月的下一个月初或下个月,而不管它是在哪一天。 在内部,first方法使用数据帧的第一个索引元素,并添加传递给它的日期偏移。 然后切成片直到这个新日期。 例如,步骤 4 等效于以下内容:

>>> step4 = crime_sort.first(pd.offsets.MonthEnd(6)) >>> end_dt = crime_sort.index[0] + pd.offsets.MonthEnd(6) >>> step4_internal = crime_sort[:end_dt] >>> step4.equals(step4_internal) True

步骤 5 至 7 直接从前面的等效操作开始。 在步骤 8 中,偏移别名使引用 DateOffsets 的方法更加紧凑。

与first方法相对应的是last方法,该方法从给定日期偏移的数据帧中选择最后n个时间段。分组对象具有两个名称完全相同但功能完全不同的方法。 它们返回每个组的第一个或最后一个元素,与拥有日期时间索引无关。

更多

当可用的那些不能完全满足您的需求时,可以构建一个自定义的日期偏移:

>>> dt = pd.Timestamp('2012-1-16 13:40') >>> dt + pd.DateOffset(months=1) Timestamp('2012-02-16 13:40:00')

请注意,此自定义日期偏移使Timestamp精确增加了一个月。 让我们再看一个使用更多日期和时间组件的示例:

>>> do = pd.DateOffset(years=2, months=5, days=3, hours=8, seconds=10) >>> pd.Timestamp('2012-1-22 03:22') + do Timestamp('2014-06-25 11:22:10') 另见 Pandas DateOffsets对象的官方文档 计算每周的犯罪数量

原始的丹佛犯罪数据集非常庞大,有 460,000 多行标记有报告日期。 计算每周犯罪的数量是可以通过根据一段时间进行分组来回答的许多查询之一。resample方法提供了一个简单的接口,可以按任何可能的时间跨度进行分组。

准备

在本秘籍中,我们将同时使用resample和groupby方法来计算每周犯罪的数量。

操作步骤 读取犯罪 HDF5 数据集,将索引设置为REPORTED_DATE,然后对其进行排序以提高其余秘籍的性能: >>> crime_sort = pd.read_hdf('data/crime.h5', 'crime') \ .set_index('REPORTED_DATE') \ .sort_index() 为了计算每周的犯罪数量,我们需要每周组成一个小组。resample方法采用日期偏移对象或别名,并返回准备对所有组执行操作的对象。 从resample方法返回的对象与调用groupby方法后产生的对象非常相似: >>> crime_sort.resample('W') DatetimeIndexResampler [freq=, axis=0, closed=right, label=right, convention=start, base=0] 偏移别名W, 用来通知 Pandas 我们要按周分组。 在上一步中没有发生太多事情。 Pandas 只是简单地验证了我们的偏移量,并返回了一个对象,该对象准备好每周作为一组执行操作。 调用resample返回一些数据后,可以链接几种方法。 让我们链接size方法以计算每周犯罪数量: >>> weekly_crimes = crime_sort.resample('W').size() >>> weekly_crimes.head() REPORTED_DATE 2012-01-08 877 2012-01-15 1071 2012-01-22 991 2012-01-29 988 2012-02-05 888 Freq: W-SUN, dtype: int64 现在,我们将每周犯罪计数列为一个序列,而新索引一次增加一周。 默认情况下,有些事情是很重要的,要理解。 选择周日作为一周的最后一天,并且该日期也是用来标记所得序列中每个元素的日期。 例如,第一个索引值 2012 年 1 月 8 日是星期日。 在截至 8 日的那一周内,共发生了 877 起犯罪。 1 月 9 日星期一至 1 月 15 日星期日这周记录了 1,071 起犯罪。 让我们做一些健全性检查,并确保我们的重采样正是这样做的: >>> len(crime_sort.loc[:'2012-1-8']) 877 >>> len(crime_sort.loc['2012-1-9':'2012-1-15']) 1071 让我们选择除周日之外的另一天,以固定偏移结束一周: >>> crime_sort.resample('W-THU').size().head() REPORTED_DATE 2012-01-05 462 2012-01-12 1116 2012-01-19 924 2012-01-26 1061 2012-02-02 926 Freq: W-THU, dtype: int64 resample的几乎所有功能都可以通过groupby方法再现。 唯一的区别是必须在pd.Grouper对象中传递偏移量: >>> weekly_crimes_gby = crime_sort.groupby(pd.Grouper(freq='W')) \ .size() >>> weekly_crimes_gby.head() REPORTED_DATE 2012-01-08 877 2012-01-15 1071 2012-01-22 991 2012-01-29 988 2012-02-05 888 Freq: W-SUN, dtype: int64 >>> weekly_crimes.equal(weekly_crimes_gby) True 工作原理

默认情况下,resample方法与日期时间索引隐式工作,这就是为什么我们在步骤 1 中将其设置为REPORTED_DATE的原因。在步骤 2 中,我们创建了一个中间对象,可帮助我们了解如何在数据内形成组。resample的第一个参数是rule,用于确定如何对索引中的时间戳进行分组。 在这种情况下,我们使用偏移别名W来形成长度为一周的组,该组在周日结束。 默认的结束日期是星期日,但可以通过在星期几的前面加上破折号和前三个字母来更改锚定的偏移量。

一旦我们与resample组成了小组,我们就必须链接一个方法以对每个小组采取行动。 在第 3 步中,我们使用size方法来计算每周的犯罪数量。 您可能想知道调用resample之后可以使用哪些所有可能的属性和方法。 下面检查resample对象并输出它们:

>>> r = crime_sort.resample('W') >>> resample_methods = [attr for attr in dir(r) if attr[0].islower()] >>> print(resample_methods) ['agg', 'aggregate', 'apply', 'asfreq', 'ax', 'backfill', 'bfill', 'count', 'ffill', 'fillna', 'first', 'get_group', 'groups', 'indices', 'interpolate', 'last', 'max', 'mean', 'median', 'min', 'ndim', 'ngroups', 'nunique', 'obj', 'ohlc', 'pad', 'plot', 'prod', 'sem', 'size', 'std', 'sum', 'transform', 'var']

步骤 4 通过按周手动切片数据并计算行数来验证步骤 3 中计数的准确性。 实际上,甚至不需要按Timestamp分组resample方法,因为该功能可以直接从groupby方法本身获得。 但是,必须使用freq参数将偏移量pd.Grouper的实例传递给groupby方法,如步骤 6 所示。

一个非常类似的名为pd.TimeGrouper的对象能够按照与pd.Grouper完全相同的方式按时间进行分组,但是从熊猫 0.21 版本开始,它已被弃用,不应使用。 不幸的是,在线上有很多使用pd.TimeGrouper的例子,但不要让它们诱惑您。

更多

即使索引不包含Timestamp,也可以使用resample。 您可以使用on参数选择带有时间戳的列,这些列将用于形成组:

>>> crime = pd.read_hdf('data/crime.h5', 'crime') >>> weekly_crimes2 = crime.resample('W', on='REPORTED_DATE').size() >>> weekly_crimes2.equals(weekly_crimes) True

同样,通过选择key参数的Timestamp列,可以将groupby与pd.Grouper结合使用:

>>> weekly_crimes_gby2 = crime.groupby(pd.Grouper(key='REPORTED_DATE', freq='W')).size() >>> weekly_crimes_gby2.equals(weekly_crimes_gby) True

通过调用每周犯罪序列中的plot方法,我们还可以轻松地绘制丹佛所有犯罪(包括交通事故)的线图:

>>> weekly_crimes.plot(figsize=(16, 4), title='All Denver Crimes')

另见 Pandas 重采样的官方文档 所有锚定偏移量的表 分别汇总每周犯罪和交通事故

丹佛犯罪数据集将所有犯罪和交通事故汇总在一个表格中,并通过二进制列IS_CRIME和IS_TRAFFIC将它们分开。resample方法允许您按一段时间分组并分别汇总特定的列。

准备

在本秘籍中,我们将使用resample方法对一年中的每个季度进行分组,然后分别汇总犯罪和交通事故的数量。

操作步骤 读取犯罪 HDF5 数据集,将索引设置为REPORTED_DATE,然后对其进行排序以提高其余秘籍的性能: >>> crime_sort = pd.read_hdf('data/crime.h5', 'crime') \ .set_index('REPORTED_DATE') \ .sort_index() 使用resample方法按一年中的每个季度进行分组,然后将各组的IS_CRIME和IS_TRAFFIC列求和: >>> crime_quarterly = crime_sort.resample('Q')['IS_CRIME', 'IS_TRAFFIC'].sum() >>> crime_quarterly.head()

请注意,所有日期均显示为该季度的最后一天。 这是因为偏移别名Q代表该季度末。 让我们使用偏移别名QS代表季度的开始: >>> crime_sort.resample('QS')['IS_CRIME', 'IS_TRAFFIC'].sum().head()

让我们通过检查第二季度的数据是否正确来验证这些结果: >>> crime_sort.loc['2012-4-1':'2012-6-30', ['IS_CRIME', 'IS_TRAFFIC']].sum() IS_CRIME 9641 IS_TRAFFIC 5255 dtype: int64 可以使用groupby方法复制此操作: >>> crime_quarterly2 = crime_sort.groupby(pd.Grouper(freq='Q')) \ ['IS_CRIME', 'IS_TRAFFIC'].sum() >>> crime_quarterly2.equals(crime_quarterly) True 让我们作图以更好地分析一段时间内犯罪和交通事故的趋势: >>> plot_kwargs = dict(figsize=(16,4), color=['black', 'lightgrey'], title='Denver Crimes and Traffic Accidents') >>> crime_quarterly.plot(**plot_kwargs)

工作原理

在第 1 步中读取并准备好数据后,我们在第 2 步中开始分组和聚合。调用resample方法后,我们可以通过链接方法或选择一组要聚合的列来继续进行操作。 我们选择选择IS_CRIME和IS_TRAFFIC列进行汇总。 如果我们不只是选择这两个,那么所有数字列的总和将具有以下结果:

>>> crime_sort.resample('Q').sum().head()

默认情况下,偏移别名Q在技术上使用 12 月 31 日作为一年的最后一天。 代表一个季度的日期范围全部使用此结束日期计算。 汇总结果使用该季度的最后一天作为标签。 步骤 3 使用偏移别名QS,默认情况下,它使用 1 月 1 日作为一年的第一天来计算季度。

大多数公共企业都报告季度收入,但是从一月开始,它们都没有相同的日历年。 例如,如果我们希望季度开始于 3 月 1 日,则可以使用QS-MAR来锚定偏移别名:

>>> crime_sort.resample('QS-MAR')['IS_CRIME', 'IS_TRAFFIC'] \ .sum().head()

与前面的秘籍一样,我们通过手动切片来验证结果,并使用pd.Grouper使用groupby方法复制结果以设置组长。 在第 6 步中,我们仅调用数据帧的plot方法。 默认情况下,为每列数据绘制一条线。 该图清楚地表明,在今年的前三个季度,报告的犯罪数量急剧增加。 犯罪和贩运似乎都是季节性因素,在较冷的月份数字较低,在较暖的月份数字较高。

更多

为了获得不同的视觉角度,我们可以绘制犯罪和交通增加百分比,而不是原始计数。 让我们将所有数据除以第一行并再次绘图:

>>> crime_begin = crime_quarterly.iloc[0] >>> crime_begin IS_CRIME 7882 IS_TRAFFIC 4726 Name: 2012-03-31 00:00:00, dtype: int64 >>> crime_quarterly.div(crime_begin) \ .sub(1) \ .round(2) \ .plot(**plot_kwargs)

按工作日和年份衡量犯罪

通过按工作日和按年衡量犯罪的同时,必须具有直接从时间戳中提取此信息的函数。 值得庆幸的是,此函数内置于任何包含dt访问器的时间戳组成的列中。

准备

在本秘籍中,我们将使用dt访问器为我们提供每个犯罪的工作日名称和年份(序列)。 我们通过使用这两个序列的小组来计算所有犯罪。 最后,我们在创建犯罪总量热图之前,调整数据以考虑部分年份和人口。

操作步骤 读入丹佛犯罪 HDF5 数据集,将REPORTED_DATE保留为一列: >>> crime = pd.read_hdf('data/crime.h5', 'crime') >>> crime.head()

所有“时间戳”列均具有称为dt访问器的特殊属性,该属性可访问为它们专门设计的各种其他属性和方法。 让我们找到每个REPORTED_DATE的工作日名称,然后计算这些值: >>> wd_counts = crime['REPORTED_DATE'].dt.weekday_name \ .value_counts() >>> wd_counts Monday 70024 Friday 69621 Wednesday 69538 Thursday 69287 Tuesday 68394 Saturday 58834 Sunday 55213 Name: REPORTED_DATE, dtype: int64 周末看来,犯罪和交通事故的发生率大大降低。 让我们按正确的工作日顺序排列此数据,并绘制水平条形图: >>> days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] >>> title = 'Denver Crimes and Traffic Accidents per Weekday' >>> wd_counts.reindex(days).plot(kind='barh', title=title)

我们可以执行非常类似的过程来按年份绘制计数: >>> title = 'Denver Crimes and Traffic Accidents per Year' >>> crime['REPORTED_DATE'].dt.year.value_counts() \ .sort_index() \ .plot(kind='barh', title=title)

我们需要按工作日和年份分组。 一种方法是将工作日和年份序列保存为单独的变量,然后将这些变量与groupby方法一起使用: >>> weekday = crime['REPORTED_DATE'].dt.weekday_name >>> year = crime['REPORTED_DATE'].dt.year >>> crime_wd_y = crime.groupby([year, weekday]).size() >>> crime_wd_y.head(10) REPORTED_DATE REPORTED_DATE 2012 Friday 8549 Monday 8786 Saturday 7442 Sunday 7189 Thursday 8440 Tuesday 8191 Wednesday 8440 2013 Friday 10380 Monday 10627 Saturday 8875 dtype: int64 我们已经正确汇总了数据,但是结构并不完全有利于轻松进行比较。 让我们先重命名那些无意义的索引级别名称,然后再将unstack重命名为工作日级别,以使我们的表更具可读性: >>> crime_table = crime_wd_y.rename_axis(['Year', 'Weekday']) \ .unstack('Weekday') >>> crime_table

现在,我们有了更好的表示形式,更易于阅读,但值得注意的是,2017 年的数字并不完整。 为了更公平地进行比较,我们可以进行简单的线性外推法来估算犯罪的最终数量。 首先让我们找到 2017 年数据的最后一天: >>> criteria = crime['REPORTED_DATE'].dt.year == 2017 >>> crime.loc[criteria, 'REPORTED_DATE'].dt.dayofyear.max() 272 天真的估计是假设全年犯罪率保持不变,并将 2017 年表中的所有值乘以 365/272。 但是,我们可以做得更好,查看历史数据并计算在一年的前 272 天中发生的犯罪的平均百分比: >>> round(272 / 365, 3) .745 >>> crime_pct = crime['REPORTED_DATE'].dt.dayofyear.le(272) \ .groupby(year) \ .mean() \ .round(3) >>> crime_pct REPORTED_DATE 2012 0.748 2013 0.725 2014 0.751 2015 0.748 2016 0.752 2017 1.000 Name: REPORTED_DATE, dtype: float64 >>> crime_pct.loc[2012:2016].median() .748 事实证明,也许非常巧合的是,在一年的前 272 天发生的犯罪百分比几乎与该年过去的天数百分比成正比。 现在让我们更新 2017 年的行,并更改列顺序以匹配工作日顺序: >>> crime_table.loc[2017] = crime_table.loc[2017].div(.748) \ .astype('int') >>> crime_table = crime_table.reindex(columns=days) >>> crime_table

我们可以绘制条形图或折线图,但这对于热图也是一个很好的情况,seaborn 库中提供了该图: >>> import seaborn as sns >>> sns.heatmap(crime_table, cmap='Greys')

犯罪似乎每年都在增加,但是该数据并未说明人口的增长。 让我们读一下有数据的每年丹佛人口的表格: >>> denver_pop = pd.read_csv('data/denver_pop.csv', index_col='Year') >>> denver_pop

据报告,许多犯罪指标是每 100,000 名居民的比率。 让我们将人口除以 100,000,然后将原始犯罪计数除以该数字即可得出每 100,000 居民的犯罪率: >>> den_100k = denver_pop.div(100000).squeeze() >>> crime_table2 = crime_table.div(den_100k, axis='index') \ .astype('int') >>> crime_table2

再一次,我们可以制作一个热图,即使在调整了人口增长之后,该热图看起来也几乎与第一个相同: >>> sns.heatmap(crime_table2, cmap='Greys')

工作原理

所有包含时间戳的数据帧的列都可以使用dt访问器访问许多其他属性和方法。 实际上,从dt访问器可用的所有这些方法和属性也可以直接从单个时间戳对象获得。

在第 2 步中,我们使用仅适用于序列的dt访问器来提取工作日名称并简单地计算发生次数。 在执行步骤 3 之前,我们使用reindex方法手动重新排列索引的顺序,在最基本的使用情况下,该方法接受包含所需顺序的列表。 也可以使用.loc索引器完成此任务,如下所示:

>>> wd_counts.loc[days] Monday 70024 Tuesday 68394 Wednesday 69538 Thursday 69287 Friday 69621 Saturday 58834 Sunday 55213 Name: REPORTED_DATE, dtype: int64

与.loc相比,reindex方法实际上性能更高,并且在更多情况下具有许多参数。 然后,我们使用dt访问器的weekday_name属性检索一周中每一天的名称,并在制作水平条形图之前对出现的次数进行计数。

在第 4 步中,我们执行一个非常相似的过程,并再次使用dt访问器检索年份,然后使用value_counts方法对发生次数进行计数。 在这种情况下,我们使用sort_index而不是reindex,因为年份自然会按所需顺序排序。

秘籍的目标是将工作日和年份进行分组,因此这正是我们在第 5 步中所做的。groupby方法非常灵活,可以通过多种方式进行分组。 在此秘籍中,我们将两个序列year和weekday传递给它们,所有唯一的组合从中组成一个组。 然后,我们将size方法链接到该方法,该方法返回单个值,即每个组的长度。

在第 5 步之后,我们的序列很长,只有一列数据,这使得很难按年和工作日进行比较。 为了简化可读性,我们将工作日级别使用unstack旋转为水平列名称。

在步骤 7 中,我们使用布尔索引来仅选择 2017 年的犯罪,然后再次使用dt访问器中的dayofyear查找从年初开始经过的总天数。 该序列的最大值应告诉我们 2017 年有多少天的数据。

步骤 8 非常复杂。 我们首先通过使用crime['REPORTED_DATE'].dt.dayofyear.le(272)测试每个犯罪是否在每年的第 272 天或之前犯下来创建布尔值序列。 从这里开始,我们再次使用灵活的groupby方法按照先前计算的year序列来分组,然后使用mean方法来查找每年第 272 天或之前的犯罪百分比。

.loc索引器在步骤 9 中选择整个 2017 年数据行。我们用该行除以在步骤 8 中找到的中位数百分比来调整该行。

许多犯罪的可视化都是通过热图完成的,其中一个步骤是在第 10 步借助seaborn可视化库完成的。cmap参数采用几十个可用 matplotlib 调色板的字符串名称。

在第 12 步中,我们将100k居民的犯罪率除以该年的人口。 这实际上是一个相当棘手的操作。 通常,将一个数据帧除以另一个时,它们在其列和索引上对齐。 但是,在此步骤中,crime_table没有公用的denver_pop列,因此,如果我们尝试对它们进行划分,则没有值会对齐。 要解决此问题,我们使用squeeze方法创建了den_100k序列。 我们仍然不能简单地划分这两个对象,因为默认情况下,数据帧和序列之间的划分会将数据帧的列与序列的索引对齐,如下所示:

>>> crime_table / den_100k

我们需要数据帧的索引与序列的索引对齐,并且为此,我们使用div方法,该方法允许我们使用axis参数更改对齐方向。 在步骤 13 中绘制已调整犯罪率的heatmap。

更多

让我们通过编写一个函数来一次完成此秘籍的所有步骤并添加选择特定类型犯罪的功能来完成此分析的完成:

>>> ADJ_2017 = .748 >>> def count_crime(df, offense_cat): df = df[df['OFFENSE_CATEGORY_ID'] == offense_cat] weekday = df['REPORTED_DATE'].dt.weekday_name year = df['REPORTED_DATE'].dt.year ct = df.groupby([year, weekday]).size().unstack() ct.loc[2017] = ct.loc[2017].div(ADJ_2017).astype('int') pop = pd.read_csv('data/denver_pop.csv', index_col='Year') pop = pop.squeeze().div(100000) ct = ct.div(pop, axis=0).astype('int') ct = ct.reindex(columns=days) sns.heatmap(ct, cmap='Greys') return ct >>> count_crime(crime, 'auto-theft')

另见 Pandas reindex方法的官方文档 Seaborn heatmap函数的官方文档 使用日期时间索引和匿名函数进行分组

将数据帧与DatetimeIndex一起使用将为许多新的和不同的操作打开一扇门,如本章中的几个秘籍所示。

准备

在本秘籍中,我们将展示对具有DatetimeIndex的数据帧使用groupby方法的多功能性。

操作步骤 读入丹佛crime hdf5文件,将REPORTED_DATE列放在索引中,然后对其进行排序: >>> crime_sort = pd.read_hdf('data/crime.h5', 'crime') \ .set_index('REPORTED_DATE') \ .sort_index() DatetimeIndex本身具有许多与 Pandas Timestamp相同的属性和方法。 让我们看一下它们的共同点: >>> common_attrs = set(dir(crime_sort.index)) & \ set(dir(pd.Timestamp)) >>> print([attr for attr in common_attrs if attr[0] != '_']) ['to_pydatetime', 'normalize', 'day', 'dayofyear', 'freq', 'ceil', 'microsecond', 'tzinfo', 'weekday_name', 'min', 'quarter', 'month', 'tz_convert', 'tz_localize', 'is_month_start', 'nanosecond', 'tz', 'to_datetime', 'dayofweek', 'year', 'date', 'resolution', 'is_quarter_end', 'weekofyear', 'is_quarter_start', 'max', 'is_year_end', 'week', 'round', 'strftime', 'offset', 'second', 'is_leap_year', 'is_year_start', 'is_month_end', 'to_period', 'minute', 'weekday', 'hour', 'freqstr', 'floor', 'time', 'to_julian_date', 'days_in_month', 'daysinmonth'] 然后,我们可以使用索引来查找工作日名称,类似于上一秘籍的步骤 2 中所做的操作: >>> crime_sort.index.weekday_name.value_counts() Monday 70024 Friday 69621 Wednesday 69538 Thursday 69287 Tuesday 68394 Saturday 58834 Sunday 55213 Name: REPORTED_DATE, dtype: int64 令人惊讶的是,groupby方法具有接受函数作为参数的能力。 该函数将隐式传递给索引,并且其返回值用于形成组。 让我们通过使用将索引转换为工作日名称的函数进行分组,然后分别计算犯罪和交通事故的数量,来了解这一点: >>> crime_sort.groupby(lambda x: x.weekday_name) \ ['IS_CRIME', 'IS_TRAFFIC'].sum()

您可以使用函数列表按年中的小时和年进行分组,然后对表进行整形以使其更具可读性: >>> funcs = [lambda x: x.round('2h').hour, lambda x: x.year] >>> cr_group = crime_sort.groupby(funcs) \ ['IS_CRIME', 'IS_TRAFFIC'].sum() >>> cr_final = cr_group.unstack() >>> cr_final.style.highlight_max(color='lightgrey')

工作原理

在第 1 步中,我们读入数据并将一列时间戳放入索引中以创建日期时间索引。 在第 2 步中,我们看到日期时间索引具有许多与单个时间戳对象相同的函数。 在第 3 步中,我们直接使用日期时间索引的这些额外函数提取工作日名称。

在步骤 4 中,我们利用groupby方法的特殊功能来接受通过日期时间索引传递的函数。 匿名函数中的x实际上是日期时间索引,我们使用它来检索工作日名称。 可以传递groupby任意数量的自定义函数的列表,如步骤 5 所示。这里,第一个函数使用日期时间索引的round方法将每个值四舍五入到最接近的第二小时。 第二个函数检索年份。 在分组和汇总之后,我们将unstack年作为列。 然后,我们突出显示每列的最大值。 犯罪率最高的报告时间是下午 3 点至 5 点。 大多数交通事故发生在下午 5 点之间。 晚上 7 点

更多

此秘籍的最终结果是带有多重索引列的数据帧。 使用此数据帧,可以仅选择犯罪或交通事故。xs方法允许您从任何索引级别中选择一个值。 让我们看一个示例,其中我们仅选择处理流量的数据部分:

>>> cr_final.xs('IS_TRAFFIC', axis='columns', level=0).head()

这称为在 Pandas 中截取的横截面。 我们必须使用axis和level参数专门表示我们的值所在的位置。 让我们再次使用xs仅选择 2016 年中处于不同级别的数据:

>>> cr_final.xs(2016, axis='columns', level=1).head()

另见 Pandas 横截面方法xs的官方文档 按时间戳和另一列分组

resample方法本身无法按时间段进行分组。 但是,groupby方法可以按时间段和其他列进行分组。

准备

在此秘籍中,我们将展示两种非常相似但不同的方法来按时间戳分组,并在另一列中进行。

操作步骤 读取employee数据集,并使用HIRE_DATE列创建日期时间索引: >>> employee = pd.read_csv('data/employee.csv', parse_dates=['JOB_DATE', 'HIRE_DATE'], index_col='HIRE_DATE') >>> employee.head()

首先,让我们按性别进行简单分组,然后找到每个分组的平均工资: >>> employee.groupby('GENDER')['BASE_SALARY'].mean().round(-2) GENDER Female 52200.0 Male 57400.0 Name: BASE_SALARY, dtype: float64 让我们根据租用日期找到平均薪水,然后将每个人归类为 10 年: >>> employee.resample('10AS')['BASE_SALARY'].mean().round(-2) HIRE_DATE 1958-01-01 81200.0 1968-01-01 106500.0 1978-01-01 69600.0 1988-01-01 62300.0 1998-01-01 58200.0 2008-01-01 47200.0 Freq: 10AS-JAN, Name: BASE_SALARY, dtype: float64 如果我们想按性别和五年时间跨度分组,可以在致电groupby之后直接致电resample: >>> employee.groupby('GENDER').resample('10AS')['BASE_SALARY'] \ .mean().round(-2) GENDER HIRE_DATE Female 1975-01-01 51600.0 1985-01-01 57600.0 1995-01-01 55500.0 2005-01-01 51700.0 2015-01-01 38600.0 Male 1958-01-01 81200.0 1968-01-01 106500.0 1978-01-01 72300.0 1988-01-01 64600.0 1998-01-01 59700.0 2008-01-01 47200.0 Name: BASE_SALARY, dtype: float64 现在,这已经完成了我们打算要做的工作,但是每当我们要比较男性和女性的工资时,我们都会遇到一个小问题。 让我们unstack性别级别,看看会发生什么: >>> sal_avg.unstack('GENDER')

男性和女性的 10 年期限不在同一日期开始。 发生这种情况的原因是,数据首先按性别分组,然后在每种性别内,根据雇用日期组成了更多的组。 让我们验证一下第一位雇用的男性是 1958 年,第一位雇用的女性是 1975 年: >>> employee[employee['GENDER'] == 'Male'].index.min() Timestamp('1958-12-29 00:00:00') >>> employee[employee['GENDER'] == 'Female'].index.min() Timestamp('1975-06-09 00:00:00') 要解决此问题,我们必须将日期与性别一起分组,并且只有通过groupby方法才能做到这一点: >>> sal_avg2 = employee.groupby(['GENDER', pd.Grouper(freq='10AS')]) \ ['BASE_SALARY'].mean().round(-2) >>> sal_avg2 GENDER HIRE_DATE Female 1968-01-01 NaN 1978-01-01 57100.0 1988-01-01 57100.0 1998-01-01 54700.0 2008-01-01 47300.0 Male 1958-01-01 81200.0 1968-01-01 106500.0 1978-01-01 72300.0 1988-01-01 64600.0 1998-01-01 59700.0 2008-01-01 47200.0 Name: BASE_SALARY, dtype: float64 现在我们可以unstack性别,使行完美对齐: >>> sal_final = sal_avg2.unstack('GENDER') >>> sal_final

工作原理

步骤 1 中的read_csv函数允许将列都转换为时间戳,并同时将它们放入索引中,以创建日期时间索引。 第 2 步使用单个分组列GENDER执行简单的groupby操作。 步骤 3 使用resample方法和偏移别名10AS以 10 年的时间增量形成组。A是年份的别名,S通知我们该时期的开始用作标签。 例如,标签1988-01-01的数据跨越该日期,直到 1997 年 12 月 31 日为止。

有趣的是,从对groupby方法的调用返回的对象具有其自己的resample方法,但反之则不成立:

>>> 'resample' in dir(employee.groupby('GENDER')) True >>> 'groupby' in dir(employee.resample('10AS')) False

在第 4 步中,根据最早雇用的员工,计算出男女的 10 年完全不同的开始日期。 步骤 6 验证每种性别最早雇用的雇员的年份与步骤 4 的输出相匹配。步骤 5 显示了当我们尝试将女性的工资与男性的工资进行比较时,这如何导致不一致。 他们没有相同的 10 年期限。

要缓解此问题,我们必须将“性别”和“时间戳”归为一组。resample方法仅能按单个时间戳分组。 我们只能使用groupby方法完成此操作。 使用pd.Grouper,我们可以复制resample的功能。 我们只需将偏移别名传递给freq参数,然后将对象与我们希望分组的所有其他列一起放在列表中,如步骤 7 所示。由于现在男性和女性的开始日期都相同 10 年期间,步骤 8 中的重塑数据将针对每种性别进行调整,从而使比较变得更加容易。 看起来,随着工作时间的延长,男性的工资往往会更高,尽管在 10 年以下的工作中,男性和女性的平均工资相同。

更多

从局外人的角度来看,步骤 8 中输出的行代表 10 年的间隔并不明显。 改善索引标签的一种方法是显示每个时间间隔的开始和结束。 我们可以通过将当前索引年份与自身添加的 9 连接来实现此目的:

>>> years = sal_final.index.year >>> years_right = years + 9 >>> sal_final.index = years.astype(str) + '-' + years_right.astype(str) >>> sal_final

实际上,有一种完全不同的方法来制作此秘籍。 我们可以使用cut函数根据每位员工的受聘年限并从中形成组来创建等宽间隔:

>>> cuts = pd.cut(employee.index.year, bins=5, precision=0) >>> cuts.categories.values array([Interval(1958.0, 1970.0, closed='right'), Interval(1970.0, 1981.0, closed='right'), Interval(1981.0, 1993.0, closed='right'), Interval(1993.0, 2004.0, closed='right'), Interval(2004.0, 2016.0, closed='right')], dtype=object) >>> employee.groupby([cuts, 'GENDER'])['BASE_SALARY'] \ .mean().unstack('GENDER').round(-2)

使用merge_asof查找相比上次降低了 20% 的犯罪率

很多时候,我们想知道上一次发生什么事情的时间。 例如,我们可能对上一次失业率低于 5% 或上一次股市连续五天上涨或上一次睡眠八个小时感兴趣。merge_asof函数为这些类型的问题提供答案。

准备

在此秘籍中,我们将找到每种犯罪类别当月的犯罪总数,然后找到上次发生率降低 20% 的时间。

操作步骤 读入丹佛犯罪数据集,将REPORTED_DATE放在索引中,然后对其进行排序: >>> crime_sort = pd.read_hdf('data/crime.h5', 'crime') \ .set_index('REPORTED_DATE') \ .sort_index() 查找最近一个月的数据: >>> crime_sort.index.max() Timestamp('2017-09-29 06:16:00') 由于我们没有 9 月份的全部数据,因此将其从数据集中删除: >>> crime_sort = crime_sort[:'2017-8'] >>> crime_sort.index.max() Timestamp('2017-08-31 23:52:00') 让我们计算每个月的犯罪和交通事故数量: >>> all_data = crime_sort.groupby([pd.Grouper(freq='M'), 'OFFENSE_CATEGORY_ID']).size() >>> all_data.head() REPORTED_DATE OFFENSE_CATEGORY_ID 2012-01-31 aggravated-assault 113 all-other-crimes 124 arson 5 auto-theft 275 burglary 343 dtype: int64 尽管merge_asof函数可以使用索引,但重置它会更容易: >>> all_data = all_data.sort_values().reset_index(name='Total') >>> all_data.head()

让我们获取当前月份的犯罪计数,并新建一个列来表示目标: >>> goal = all_data[all_data['REPORTED_DATE'] == '2017-8-31'] \ .reset_index(drop=True) >>> goal['Total_Goal'] = goal['Total'].mul(.8).astype(int) >>> goal.head()

现在使用merge_asof函数查找每个犯罪类别的每月犯罪总数上一次小于Total_Goal列的时间: >>> pd.merge_asof(goal, all_data, left_on='Total_Goal', right_on='Total', by='OFFENSE_CATEGORY_ID', suffixes=('_Current', '_Last'))

工作原理

读完我们的数据后,我们决定不包括 2017 年 9 月的数据,因为它不是一个完整的月份。 我们使用部分日期字符串对直至 2017 年 8 月的所有犯罪进行分割,在第 4 步中,我们统计每月每个犯罪类别的所有犯罪,在第 5 步中,我们按此总数进行排序,这对于merge_asof是必需的。

在第 6 步中,我们将最新数据选择到单独的数据帧中。 我们将以 8 月的这个月为基准,并创建Total_Goal列,该列比当前少 20% 。 在第 7 步中,我们使用merge_asof查找上一次每月犯罪计数少于Total_Goal列的时间。

更多

除了时间戳和时间增量数据类型外,pandas 还提供了时间段类型来表示确切的时间段。 例如,2012-05代表 2012 年 5 月的整个月份。您可以通过以下方式手动构建时间段:

>>> pd.Period(year=2012, month=5, day=17, hour=14, minute=20, freq='T') Period('2012-05-17 14:20', 'T')

该对象表示 2012 年 5 月 17 日下午 2:20 的整个分钟。 可以在步骤 4 中使用这些期间,而不用pd.Grouper按日期分组。 具有日期时间索引的数据帧具有to_period方法,可以将时间戳转换为期间。 它接受偏移别名来确定时间段的确切长度。

>>> ad_period = crime_sort.groupby([lambda x: x.to_period('M'), 'OFFENSE_CATEGORY_ID']).size() >>> ad_period = ad_period.sort_values() \ .reset_index(name='Total') \ .rename(columns={'level_0':'REPORTED_DATE'}) >>> ad_period.head()

让我们验证此数据帧的最后两列是否等效于步骤 5 中的all_data:

>>> cols = ['OFFENSE_CATEGORY_ID', 'Total'] >>> all_data[cols].equals(ad_period[cols]) True

现在,可以使用以下代码以几乎完全相同的方式复制步骤 6 和 7:

>>> aug_2018 = pd.Period('2017-8', freq='M') >>> goal_period = ad_period[ad_period['REPORTED_DATE'] == aug_2018] \ .reset_index(drop=True) >>> goal_period['Total_Goal'] = goal_period['Total'].mul(.8).astype(int) >>> pd.merge_asof(goal_period, ad_period, left_on='Total_Goal', right_on='Total', by='OFFENSE_CATEGORY_ID', suffixes=('_Current', '_Last')).head()

十一、Pandas,Matplotlib 和 Seaborn 的可视化

在本章中,我们将介绍以下主题:

matplotlib 入门 使用 matplotlib 可视化数据 Pandas 绘图的基础知识 可视化航班数据集 堆叠面积图以发现新兴趋势 了解 Pandas 与 Pandas 的区别 使用 Seaborn 网格进行多元分析 在 Seaborn 钻石数据集中发现辛普森悖论 介绍

可视化是探索性数据分析以及演示和应用中的关键组成部分。 在探索性数据分析过程中,您通常是一个人或成小组工作,需要快速创建绘图以帮助您更好地理解数据。 它可以帮助您识别异常值和丢失的数据,也可以引发其他令人感兴趣的问题,这些问题将导致进一步的分析和更直观的显示。 通常不会在考虑最终用户的情况下完成这种类型的可视化。 严格来说是为了帮助您更好地了解当前情况。 绘图不一定是完美的。

在为报表或应用准备可视化文件时,必须使用其他方法。 注意小细节。 此外,通常您必须将所有可能的可视化范围缩小到仅最能代表您数据的少数几个。 良好的数据可视化使观看者享受提取信息的体验。 就像使观众迷失的电影一样,好的可视化效果将包含大量真正引起人们兴趣的信息。

Python 中主要的数据可视化库是 matplotlib,该项目始于 2000 年代初期,旨在模仿 Matlab 的绘图函数。 Matplotlib 具有极大的能力来绘制您可以想象的大多数事物,它为用户提供了强大的功能来控制绘制表面的各个方面。 也就是说,对于初学者来说,它并不是最友好的库。 值得庆幸的是,Pandas 使我们对数据的可视化变得非常容易,并且通常只需单击plot方法即可绘制出我们想要的内容。 Pandas 实际上并没有独自策划。 它在内部调用 matplotlib 函数来创建图。 我认为,Pandas 还添加了自己的样式,该样式比 matplotlib 中的默认样式好一些。

Seaborn 还是一个可视化库,它在内部调用 matplotlib 函数,并且自身不进行任何实际绘制。 Seaborn 可以轻松轻松地制作漂亮的绘图,并允许创建许多新类型的绘图,而这些新绘图无法直接从 matplotlib 或 Pandas 获得。 Seaborn 处理整洁(长)数据,而 Pandas 处理汇总(宽)数据效果最佳。 Seaborn 在其绘图函数中还接受了 Pandas 数据帧对象。

尽管可以在不直接运行任何 matplotlib 代码的情况下创建图,但有时仍需要使用它来手动调整更精细的图细节。 因此,前两个秘籍将介绍 matplotlib 的一些基础知识,如果您需要直接使用它,将非常有用。 除了前两个秘籍外,所有绘图示例都将使用 Pandas 或海生豆。

Python 中的可视化不一定必须依赖于 matplotlib。 Bokeh 迅速成为针对 Web 的非常流行的交互式可视化库。 它完全独立于 matplotlib,并且能够生成整个应用。

matplotlib 入门

对于许多数据科学家而言,他们绝大部分的绘图命令将直接来自 Pandas 或海生动物,它们都完全依赖于 matplotlib 进行实际的绘图。 但是,pandas 和 seaborn 都不提供 matplotlib 的完整替代品,有时您需要直接使用它。 因此,本秘籍将简要介绍 matplotlib 的最关键方面。

准备

让我们从下图中的 matplotlib 图的解剖开始我们的介绍:

Matplotlib 使用对象层次结构在输出中显示其所有绘图项。 该层次结构是了解有关 matplotlib 的一切的关键。 图形和轴对象是层次结构的两个主要组成部分。 图形对象位于层次结构的顶部。 它是将要绘制的所有内容的容器。 图中包含一个或多个轴对象。 轴是使用 matplotlib 时将与之交互的主要对象,通常可以将其视为实际的绘图表面。 轴包含 x/y 轴,点,线,标记,标签,图例以及其他任何绘制的有用项目。

2017 年初,matplotlib 在发布版本 2.0 时进行了重大更改。 许多默认的绘图参数已更改。 解剖图实际上来自版本 1 的文档,但与版本 2 中更新的解剖图相比,在区分图形和轴方面做得更好。

需要在轴对象和轴之间进行非常明显的区分。 它们是完全独立的对象。 使用 matplotlib 术语的轴域对象不是轴的复数,而是如前所述,该对象创建并控制了大多数有用的绘图元素。 轴仅指图的 x 或 y (甚至 z)轴。

不幸的是,matplotlib 选择使用轴域(Axes,即单词轴的复数)来指代完全不同的对象,但是它对于库来说是至关重要的,因此目前不太可能更改。

由轴域对象创建的所有这些有用的绘图元素都称为艺术家。 甚至图形和轴域对象本身也是艺术家。 对艺术家的这种区分对本秘籍而言并不重要,但在进行更高级的 matplotlib 绘图时,尤其是在阅读文档时,将很有用。

Matplotlib 的面向对象指南

Matplotlib 为用户提供了两个不同的接口来进行绘图。 有状态接口直接通过pyplot模块进行所有调用。 此接口称为有状态,因为 matplotlib 隐式跟踪绘图环境的当前状态。 每当在有状态接口中创建图时,matplotlib 都会找到当前图形或当前轴并对其进行更改。 这种方法可以快速绘制一些东西,但是当处理多个图形和轴时可能变得笨拙。

Matplotlib 还提供了无状态或面向对象的接口,您可以在其中显式使用引用特定绘图对象的变量。 然后可以使用每个变量来更改绘图的某些属性。 面向对象的方法是显式的,您始终清楚地知道要修改的对象。

不幸的是,同时使用这两个选项会导致很多混乱,并且 matplotlib 以难以学习而著称。 该文档提供了使用这两种方法的示例。 教程,博客文章, Stack Overflow 文章在网络上比比皆是,这使这种混乱永久化。 本秘籍仅专注于面向对象的方法,因为它具有更多的 Python 风格,并且与我们与 Pandas 互动的方式更加相似。

如果您不熟悉 matplotlib,则可能不知道如何识别每种方法之间的差异。 通过有状态接口,所有命令将直接从pyplot发出,通常是别名plt。 制作简单的线图并在每个轴上添加一些标签如下所示:

>>> import matplotlib.pyplot as plt >>> x = [-3, 5, 7] >>> y = [10, 2, 5] >>> plt.figure(figsize=(15,3)) >>> plt.plot(x, y) >>> plt.xlim(0, 10) >>> plt.ylim(-3, 8) >>> plt.xlabel('X Axis') >>> plt.ylabel('Y axis') >>> plt.title('Line Plot') >>> plt.suptitle('Figure Title', size=20, y=1.03)

面向对象的方法仍然使用pyplot,但是通常,它只是在第一步中创建图形和轴域对象。 创建后,将直接调用这些对象的方法来更改绘图。 以下代码使用面向对象的方法对上一个图进行精确复制:

>>> fig, ax = plt.subplots(figsize=(15,3)) >>> ax.plot(x, y) >>> ax.set_xlim(0, 10) >>> ax.set_ylim(-3, 8) >>> ax.set_xlabel('X axis') >>> ax.set_ylabel('Y axis') >>> ax.set_title('Line Plot') >>> fig.suptitle('Figure Title', size=20, y=1.03)

在这个简单的示例中,我们仅直接使用两个对象,即图形和轴,但是通常,图可以包含数百个对象; 可以使用每一种都以非常精细的方式进行修改,而使用状态接口则不容易做到。 在本章中,我们将构建一个空图并使用面向对象的接口修改其一些基本属性。

操作步骤 要使用面向对象的方法开始使用 matplotlib,您将需要导入pyplot模块和别名plt: >>> import matplotlib.pyplot as plt 通常,当使用面向对象的方法时,我们将创建一个图形和一个或多个轴域对象。 让我们使用subplots函数创建具有单个轴的图形: >>> fig, ax = plt.subplots(nrows=1, ncols=1)

subplots函数返回一个包含图形和一个或多个轴域对象(这里只是一个)的两个项目元组对象,这些对象被解包到变量fig和ax中。 从现在开始,我们将通过常规的面向对象方法调用方法来直接使用这些对象。 让我们看一下每个对象的类型,以确保我们实际使用的是图形和轴域: >>> type(fig) matplotlib.figure.Figure >>> type(ax) matplotlib.axes._subplots.AxesSubplot 尽管您将调用比图形方法更多的轴域,但您可能仍需要与它们交互。 让我们找到图的大小,然后将其放大: >>> fig.get_size_inches() array([ 6., 4.]) >>> fig.set_size_inches(14, 4) >>> fig

在开始绘制之前,让我们检查一下 matplotlib 层次结构。 您可以使用axes属性收集图中的所有轴: >>> fig.axes [] 此命令返回所有轴对象的列表。 但是,我们已经将轴对象存储在ax变量中。 让我们确认它们实际上是同一对象: >>> fig.axes[0] is ax True 为了明显地将图形与轴区分开,我们可以给每个图形一个唯一的facecolor。 Matplotlib 接受各种不同的颜色输入类型。 字符串名称支持大约 140 种 HTML 颜色(请参见此列表)。 您还可以使用包含从零到一的浮点数的字符串来表示灰色阴影: >>> fig.set_facecolor('.9') >>> ax.set_facecolor('.7') >>> fig

现在我们已经区分了图形和轴域,让我们用get_children方法查看轴域的所有直接子代: >>> ax_children = ax.get_children() >>> ax_children [, , , , , , ...] 每个基本图都有四个刺和两个轴对象。 脊线代表数据边界,是您看到的与较深的灰色矩形(“轴”)接壤的四根物理线。 x 和 y 轴对象包含更多的绘图对象,例如刻度和它们的标签以及整个轴的标签。 我们可以从该列表中选择刺,但这通常不是这样做的。 我们可以使用spines属性直接访问它们: >>> spines = ax.spines >>> spines OrderedDict([('left', ), ('right', ), ('bottom', ), ('top', )]) 刺包含在有序字典中。 让我们选择左侧的脊椎,并更改其位置和宽度,使其更加突出,并使底部的脊椎不可见: >>> spine_left = spines['left'] >>> spine_left.set_position(('outward', -100)) >>> spine_left.set_linewidth(5) >>> spine_bottom = spines['bottom'] >>> spine_bottom.set_visible(False) >>> fig

现在,让我们集中讨论轴对象。 我们可以通过xaxis和yaxis属性直接访问每个轴。Axes对象也可以直接使用某些轴属性。 在此步骤中,我们以两种方式更改每个轴的某些属性: >>> ax.xaxis.grid(True, which='major', linewidth=2, color='black', linestyle='--') >>> ax.xaxis.set_ticks([.2, .4, .55, .93]) >>> ax.xaxis.set_label_text('X Axis', family='Verdana', fontsize=15) >>> ax.set_ylabel('Y Axis', family='Calibri', fontsize=20) >>> ax.set_yticks([.1, .9]) >>> ax.set_yticklabels(['point 1', 'point 9'], rotation=45) >>> fig

工作原理

面向对象方法要掌握的关键思想之一是每个绘图元素都具有获取器和设置器方法。 获取器方法均以get_开头,并检索特定属性或检索其他绘图对象。 例如,ax.get_yscale()检索绘制 y 轴以字符串形式绘制的比例类型(默认为linear),而ax.get_xticklabels()检索 matplotlib 文本对象列表,每个都有自己的获取器和设置器方法。 设置方法修改特定的属性或整个对象组。 许多 matplotlib 归结为锁存到特定的绘图元素上,然后通过获取器和设置器方法进行检查和修改。

把 matplotlib 层次结构类比为家可能是有用的。 家及其所有内容将是图形。 每个房间都是轴域,房间的内容是艺术家。

开始使用面向对象接口的最简单方法是使用pyplot模块,该模块通常是步骤 1 中的别名plt和。步骤 2 显示了面向对象的方法,是最常见的启动方法之一。plt.subplots函数创建一个图形,以及一个轴域对象网格。 前两个参数nrows和ncols和定义了统一的轴对象网格。 例如,plt.subplots(2,4)在一个图形中创建了八个相同大小的轴对象。

plt.subplots函数有点奇怪,因为它返回一个两个项的元组。 第一个元素是图形,第二个元素是轴域对象。 该元组被解压缩为两个不同的变量fig和ax。 如果您不习惯于拆开元组,则可能会看到步骤 2 如下所示:

>>> plot_objects = plt.subplots(nrows=1, ncols=1) >>> type(plot_objects) tuple >>> fig = plot_objects[0] >>> ax = plot_objects[1]

如果使用plt.subplots和创建多个轴,则元组中的第二项是包含所有轴的 NumPy 数组。 让我们在这里演示一下:

>>> plot_objects = plt.subplots(2, 4)

plot_objects变量是一个元组,其中包含一个数字作为其第一个元素,并包含一个 Numpy 数组作为其第二个元素:

>>> plot_objects[1] array([[, , , ], [, , , ]], dtype=object)

步骤 3 验证我们确实有适当变量引用的图形和轴域对象。 在第 4 步中,我们遇到了获取器和设置器方法的第一个示例。 Matplotlib 将所有图形的默认宽度设置为 6 英寸乘以 4 英寸高,这不是屏幕上的实际大小,但是如果将图形保存到文件中,则将是确切大小。

步骤 5 显示,除了获取器方法之外,有时您还可以通过其属性直接访问另一个绘图对象。 通常,同时存在属性和获取方法来检索同一对象。 例如,查看以下示例:

>>> fig.axes == fig.get_axes() True >>> ax.xaxis == ax.get_xaxis() True >>> ax.yaxis == ax.get_yaxis() True

许多美术师都具有facecolor属性,可以将其设置为覆盖一种特定颜色的整个表面,如步骤 7 所示。在步骤 8 中,可以使用get_children方法更好地了解对象层次。 返回轴正下方所有对象的列表。 可以从此列表中选择所有对象,然后开始使用设置器方法来修改属性,但这不是惯例。 通常,我们通常直接从属性或获取器方法中收集对象。

通常,在检索绘图对象时,它们会在列表或字典之类的容器中返回。 这就是在步骤 9 中收集刺时发生的情况。您必须从它们各自的容器中选择单个对象,以便在它们上使用获取器或设置器方法,如在步骤 10 中所做的那样。通常也使用for循环一次迭代一个。

步骤 11 以特殊方式添加网格线。 我们期望有get_grid和set_grid方法,但是,只有grid方法,该方法接受布尔值作为打开/关闭网格线的第一个参数。 每个轴都有主刻度和次刻度,但默认情况下,副刻度是关闭的。which参数用于选择带有网格线的刻度线类型。

请注意,步骤 11 的前三行选择xaxis属性并从中调用方法,而后三行直接从轴域对象本身调用等效方法。 第二组方法是 matplotlib 提供的一种方便方式,可以节省一些击键。 通常,大多数对象只能设置自己的属性,而不能设置其子级的属性。 无法通过轴设置许多轴级属性,但是在此步骤中,可以设置一些属性。 两种方法都可以接受。

在步骤 11 中将网格线与第一行添加在一起时,我们设置属性linewidth,color,,和linestyle。 这些都是 matplotlib 线(正式为Line2D对象)的所有属性。 您可以在此处查看所有可用属性。set_ticks方法接受一个浮点序列,并仅在那些位置绘制刻度线。 使用空列表将完全删除所有刻度。

每个轴可能都标有一些文本,为此 matplotlib 正式使用了Text对象。 所有可用文本属性中仅更改了几个。set_yticklabels轴方法接收一个字符串列表,用作每个刻度的标签。 您可以设置任意数量的文本属性。

更多

为了帮助找到每个绘图对象的所有可能的属性,只需调用properties方法,该方法会将所有它们显示为字典。 让我们看一下轴对象的属性的精选列表:

>>> ax.xaxis.properties() {'alpha': None, 'gridlines': , 'label': Text(0.5,22.2,'X Axis'), 'label_position': 'bottom', 'label_text': 'X Axis', 'tick_padding': 3.5, 'tick_space': 26, 'ticklabels': , 'ticklocs': array([ 0.2 , 0.4 , 0.55, 0.93]), 'ticks_position': 'bottom', 'visible': True} 另见 Matplotlib 使用指南的官方文档 轴域对象的所有方法的分类列表 《Matplotlib 剖析》,主要贡献者 Ben Root Matplotlib 有状态pyplot模块和面向对象方法的官方文档 Matplotlib 官方文档中的艺术家教程 使用 matplotlib 可视化数据

Matplotlib 有几十种绘图方法,几乎​​可以想象任何一种绘图。 线,条,直方图,散点图,方格,小提琴,轮廓,饼图以及许多其他图都可以从“轴”对象中用作方法。 只有在 1.5 版(2015 年发布)中,matplotlib 才开始接受来自 Pandas 数据帧的数据。 在此之前,必须将数据从 NumPy 数组或 Python 列表传递给它。

准备

在本秘籍中,我们将通过将 Pandas 数据帧中的数据减少到 NumPy 数组来可视化电影预算随时间的趋势,然后将其传递给 matplotlib 绘图函数。

操作步骤 既然我们知道如何选择绘图元素并更改其属性,那么让我们实际创建数据可视化。 让我们阅读电影数据集,计算每年的预算中位数,然后找到五年滚动平均值以使数据平滑: >>> movie = pd.read_csv('data/movie.csv') >>> med_budget = movie.groupby('title_year')['budget'].median() / 1e6 >>> med_budget_roll = med_budget.rolling(5, min_periods=1).mean() >>> med_budget_roll.tail() title_year 2012.0 20.893 2013.0 19.893 2014.0 19.100 2015.0 17.980 2016.0 17.780 Name: budget, dtype: float64 让我们将数据放入 NumPy 数组中: >>> years = med_budget_roll.index.values >>> years[-5:] array([ 2012., 2013., 2014., 2015., 2016.]) >>> budget = med_budget_roll.values >>> budget[-5:] array([ 20.893, 19.893, 19.1 , 17.98 , 17.78 ]) plot方法用于创建折线图。 让我们用它在新图中绘制预算随时间推移的滚动中位数: >>> fig, ax = plt.subplots(figsize=(14,4), linewidth=5, edgecolor='.5') >>> ax.plot(years, budget, linestyle='--', linewidth=3, color='.2', label='All Movies') >>> text_kwargs=dict(fontsize=20, family='cursive') >>> ax.set_title('Median Movie Budget', **text_kwargs) >>> ax.set_ylabel('Millions of Dollars', **text_kwargs)

有趣的是,电影预算中位数在 2000 年达到顶峰,随后又下降了。 也许这只是数据集的人工产物,其中近年来我们拥有的所有电影的数据都更多,而不仅仅是最受欢迎的电影。 让我们找出每年的电影数量: >>> movie_count = movie.groupby('title_year')['budget'].count() >>> movie_count.tail() title_year 2012.0 191 2013.0 208 2014.0 221 2015.0 192 2016.0 86 Name: budget, dtype: int64 一个轴上可以放置任意数量的图,这些计数可以直接用中位数预算作为条形图绘制。 由于两个图的单位完全不同(美元与计数),因此我们可以创建辅助 y 轴,也可以将计数缩放到与预算相同的范围内。 我们选择后者,并在其前面直接将每个条的值标记为文本。 由于绝大多数数据都包含在最近几年中,因此我们也可以将数据限制为从 1970 年开始拍摄的电影: >>> ct = movie_count.values >>> ct_norm = ct / ct.max() * budget.max() >>> fifth_year = (years % 5 == 0) & (years >= 1970) >>> years_5 = years[fifth_year] >>> ct_5 = ct[fifth_year] >>> ct_norm_5 = ct_norm[fifth_year] >>> ax.bar(years_5, ct_norm_5, 3, facecolor='.5', alpha=.3, label='Movies per Year') >>> ax.set_xlim(1968, 2017) >>> for x, y, v in zip(years _5, ct_norm_5, ct_5): ax.text(x, y + .5, str(v), ha='center') >>> ax.legend() >>> fig

如果仅查看每年预算最高的 10 部电影,这种趋势可能不会成立。 让我们找出每年仅前十部电影的五年滚动中位数: >>> top10 = movie.sort_values('budget', ascending=False) \ .groupby('title_year')['budget'] \ .apply(lambda x: x.iloc[:10].median() / 1e6) >>> top10_roll = top10.rolling(5, min_periods=1).mean() >>> top10_roll.tail() title_year 2012.0 192.9 2013.0 195.9 2014.0 191.7 2015.0 186.8 2016.0 189.1 Name: budget, dtype: float64 对于所有数据,这些数字表示一个比在步骤 13 中发现的数字高一个数量级。 以相同的比例绘制两条线看起来并不好。 让我们创建一个带有两个子图(轴)的全新图形,并在第二个轴中绘制上一步的数据: >>> fig2, ax_array = plt.subplots(2, 1, figsize=(14,8), sharex=True) >>> ax1 = ax_array[0] >>> ax2 = ax_array[1] >>> ax1.plot(years, budget, linestyle='--', linewidth=3, color='.2', label='All Movies') >>> ax1.bar(years_5, ct_norm_5, 3, facecolor='.5', alpha=.3, label='Movies per Year') >>> ax1.legend(loc='upper left') >>> ax1.set_xlim(1968, 2017) >>> plt.setp(ax1.get_xticklines(), visible=False) >>> for x, y, v in zip(years_5, ct_norm_5, ct_5): ax1.text(x, y + .5, str(v), ha='center') >>> ax2.plot(years, top10_roll.values, color='.2', label='Top 10 Movies') >>> ax2.legend(loc='upper left') >>> fig2.tight_layout() >>> fig2.suptitle('Median Movie Budget', y=1.02, **text_kwargs) >>> fig2.text(0, .6, 'Millions of Dollars', rotation='vertical', ha='center', **text_kwargs) >>> import os >>> path = os.path.expanduser('~/Desktop/movie_budget.png') >>> fig2.savefig(path, bbox_inches='tight')

工作原理

在第 1 步中,我们开始寻求分析电影预算的方法,方法是找出每年的预算中位数(百万美元)。 找到每年的预算中位数后,我们决定对其进行平滑处理,因为每年之间会有很大的差异。 我们选择对数据进行平滑处理是因为我们正在寻找一个总体趋势,而不必对任何一年的确切值感兴趣。

在此步骤中,我们使用rolling方法根据最近五年数据的平均值来计算每年的新值。 例如,将 2011 年至 2015 年的预算中位数进行分组并取平均值。 结果是 2015 年的新值。rolling方法唯一需要的参数是窗口的大小,默认情况下,窗口的大小将在当年结束。

rolling方法返回一个类似分组的对象,该对象必须使其组与另一个函数共同作用才能产生结果。 让我们手动验证rolling方法是否能像往年一样工作:

>>> med_budget.loc[2012:2016].mean() 17.78 >>> med_budget.loc[2011:2015].mean() 17.98 >>> med_budget.loc[2010:2014].mean() 19.1

这些值与步骤 1 的输出相同。在步骤 2 中,通过将数据放入 NumPy 数组中,我们准备使用 matplotlib。 在第 3 步中,我们创建图形和轴以设置面向对象的接口。plt.subplots方法支持大量输入。 请参阅此文档以查看此函数和figure函数的所有可能参数。

plot方法中的前两个参数表示折线图的 x 和 y 值。 所有行属性都可以在plot的调用中进行更改。轴域的set_title方法提供标题,并可以在其调用内设置所有可用的文本属性。set_ylablel方法也是如此。 如果要为许多对象设置相同的属性,则可以将它们打包在一起作为字典,然后将该字典作为参数之一传递,如**text_kwargs一样。

在第 4 步中,我们注意到 2000 年左右开始的预算中值出现意外下降的趋势,并怀疑每年收集的电影数量可能起到解释作用。 我们选择通过从 1970 年开始每隔五年创建一个条形图来向图表添加此维度。我们对 NumPy 数据数组使用布尔选择的方式与在步骤 5 中对 Pandas 序列的处理方式相同。

bar方法将 x 值的高度和条形的宽度作为其前三个参数,并将条形的中心直接放在每个 x 值处。 条形高度是从电影计数中得出的,电影计数首先被缩小到零到一之间,然后乘以最大中位数预算。 这些钢筋高度存储在变量ct_norm_5中。 为了正确标记每个条形图,我们首先将条形图中心,其高度和实际影片数压缩在一起。 然后,我们遍历此压缩对象,并使用text方法将计数放在小节之前,该方法接受 x 值,y 值和字符串。 我们将 y 值略微向上调整,并使用水平对齐参数ha将文本居中。

回顾步骤 3,您会注意到label参数等于All Movies的plot方法。 这是为绘图创建图例时 matplotlib 使用的值。 调用legend Axes 方法会将所有带有指定标签的图放置在图例中。

为了调查预算中位数的意外下降,我们可以仅关注每年预算最高的 10 部电影。 在按年份分组后,第 6 步使用自定义聚合函数,然后以与以前相同的方式对结果进行平滑处理。 这些结果可以直接绘制在同一张图上,但是由于值要大得多,因此我们选择创建一个带有两个轴的全新图形。

我们通过在两个两行一列的网格中创建具有两个子图的图形来开始执行步骤 7。 请记住,当创建多个子图时,所有轴都存储在 NumPy 数组中。 步骤 5 的最终结果将在顶部轴中重新创建。 我们在底部的轴上绘制预算最高的 10 部电影。 请注意,年份与底部和顶部轴都对齐,因为在图形创建中sharex参数设置为True。 共享轴时,matplotlib 会删除所有刻度线的标签,但会保留每个刻度线的细小垂直线。 要删除这些刻度线,我们使用pyplot的setp函数。 尽管这不是直接面向对象的,但是当我们要为整个绘制对象序列设置属性时,它是显式的并且非常有用。 通过此有用的函数,我们将所有刻度线设置为不可见。

最后,我们然后多次调用图形方法。 这与我们通常调用的轴域方法不同。tight_layout方法通过删除多余的空间并确保不同的轴不会重叠来将子图调整为更好的外观。suptitle方法为整个图形创建标题,而set_title轴方法则为单个轴创建标题。 它接受 x 和 y 位置来表示图形坐标系中的位置,其中(0, 0)表示左下,而(1, 1)表示右上。 默认情况下,y 值为 0.98,但我们将其上移了几个点至 1.02。

每个轴域还具有一个坐标系,其中(0, 0)用于左下角,而(1, 1)用于右上角。 除了那些坐标系之外,每个轴还具有一个数据坐标系,这对于大多数人来说更自然,并表示 x 和 y 轴的边界。 这些界限可以分别通过ax.get_xlim()和ax.get_ylim()获取。 在此之前的所有绘图均使用数据坐标系。 请参阅“变换教程”以了解有关坐标系的更多信息。

由于两个轴的 y 轴使用相同的单位,因此我们使用图形的text方法使用图形坐标系将自定义 y 轴标签直接放置在每个轴之间。 最后,我们将图形保存到桌面。 路径中的波浪符号~代表主目录,但是savefig方法无法理解这意味着什么。 您必须使用os库中的expanduser函数来创建完整路径。 例如,path变量在我的机器上变为:

>>> os.path.expanduser('~/Desktop/movie_budget.png') '/Users/Ted/Desktop/movie_budget.png'

savefig方法现在可以在正确的位置创建文件。 默认情况下,savefig将仅保存在图形坐标系的(0, 0))至(1, 1)中绘制的内容。 由于我们的标题略微超出该区域,因此其中一些将被裁剪。 将bbox_inches参数设置为yight,matplotlib 将包含扩展到该区域之外的所有标题或标签。

更多

在 1.5 版发布之后,Matplotlib 开始接受其所有绘图函数的 pandas 数据帧。数据帧通过data参数传递给绘图方法。 这样做使您可以引用具有字符串名称的列。 以下脚本创建了从 2000 年开始随机选择的 100 部电影的 IMDB 分数与年份的散点图。 每个点的大小与预算成比例:

>>> cols = ['budget', 'title_year', 'imdb_score', 'movie_title'] >>> m = movie[cols].dropna() >>> m['budget2'] = m['budget'] / 1e6 >>> np.random.seed(0) >>> movie_samp = m.query('title_year >= 2000').sample(100) >>> fig, ax = plt.subplots(figsize=(14,6)) >>> ax.scatter(x='title_year', y='imdb_score', s='budget2', data=movie_samp) >>> idx_min = movie_samp['imdb_score'].idxmin() >>> idx_max = movie_samp['imdb_score'].idxmax() >>> for idx, offset in zip([idx_min, idx_max], [.5, -.5]): year = movie_samp.loc[idx, 'title_year'] score = movie_samp.loc[idx, 'imdb_score'] title = movie_samp.loc[idx, 'movie_title'] ax.annotate(xy=(year, score), xytext=(year + 1, score + offset), s=title + ' ({})'.format(score), ha='center', size=16, arrowprops=dict(arrowstyle="fancy")) >>> ax.set_title('IMDB Score by Year', size=25) >>> ax.grid(True)

创建散点图后,最高得分的电影和最低得分的电影都用annotate方法标记。xy参数是我们要注释的点的元组。xytext参数是文本位置的另一个元组坐标。 由于ha设置为center,因此文本居中。

另见 Matplotlib 官方图例指南 Matplotlib scatter方法的官方文档 Matplotlib 官方标注指南 Pandas 绘图的基础知识

Pandas 通过自动执行许多步骤使绘制过程变得非常容易。 所有 Pandas 绘图均由 matplotlib 内部处理,并通过数据帧或序列的plot方法公开访问。 我们说 Pandasplot方法是围绕 matplotlib 的包装器。 在 Pandas 中创建图时,将返回 matplotlib 轴或图。 您可以使用 matplotlib 的全部函数来修改该对象,直到获得所需的结果。

Pandas 仅能生成 matplotlib 可用的一小部分图,例如线图,条形图,方框图和散点图,以及核密度估计值(KDE)和直方图。 Pandas 通过使过程变得非常简单和高效而擅长于其创建的绘图,通常只需要一行代码,从而节省了探索数据的大量时间。

准备

了解 Pandas 绘图的关键之一就是要知道绘图方法是否需要一个或两个变量来进行绘图。 例如,线图和散点图需要两个变量来绘制每个点。 对于条形图也是如此,后者需要一些 x 坐标来定位条形,并需要另一个变量来设置条形的高度。 箱线图,直方图和 KDE 仅使用一个变量进行绘制。

默认情况下,两变量线图和散点图使用索引作为 x 轴,将列的值用作 y 轴。 单变量图忽略索引,并对每个变量应用转换或聚合以制作其图。 在本秘籍中,我们将考察 Pandas 中两变量和一变量绘图之间的差异。

操作步骤 创建一个具有有意义索引的小型数据帧: >>> df = pd.DataFrame(index=['Atiya', 'Abbas', 'Cornelia', 'Stephanie', 'Monte'], data={'Apples':[20, 10, 40, 20, 50], 'Oranges':[35, 40, 25, 19, 33]})

条形图使用 x 轴的标签索引,并将列值用作条形高度。 在kind参数设置为bar的情况下,使用plot方法: >>> color = ['.2', '.7'] >>> df.plot(kind='bar', color=color, figsize=(16,4))

KDE 图忽略索引,并将每列的值用作 x 轴,并计算 y 值的概率密度: >>> df.plot(kind='kde', color=color, figsize=(16,4))

让我们将所有两个变量图一起绘制在一个图中。 散点图是唯一需要您为 x 和 y 值指定列的散点图。 如果希望使用散点图的索引,则必须使用reset_index方法使其成为一列。 其他两个图使用 x 轴的索引,并为每个数字列创建一组新的线/条: >>> fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(16,4)) >>> fig.suptitle('Two Variable Plots', size=20, y=1.02) >>> df.plot(kind='line', color=color, ax=ax1, title='Line plot') >>> df.plot(x='Apples', y='Oranges', kind='scatter', color=color, ax=ax2, title='Scatterplot') >>> df.plot(kind='bar', color=color, ax=ax3, title='Bar plot')

让我们也将所有一变量图放在同一张图中: >>> fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(16,4)) >>> fig.suptitle('One Variable Plots', size=20, y=1.02) >>> df.plot(kind='kde', color=color, ax=ax1, title='KDE plot') >>> df.plot(kind='box', ax=ax2, title='Boxplot') >>> df.plot(kind='hist', color=color, ax=ax3, title='Histogram')

工作原理

第 1 步创建了一个小的样本数据帧,它将帮助我们说明使用 Pandas 进行的两个变量绘制和一变量绘制之间的差异。 默认情况下,Pandas 将使用数据帧的每个数字列制作一组新的条形,线形,KDE,盒形图或直方图,并在将其作为两变量图时将索引用作 x 值。 散点图是例外之一,必须明确为 x 和 y 值指定一列。

pandas plot方法非常通用,并具有大量参数,可让您根据自己的喜好自定义结果。 例如,您可以设置图形大小,打开和关闭网格线,设置 x 和 y 轴的范围,为图形着色,旋转刻度线,以及更多。

您还可以使用特定 matplotlib 绘图方法可用的任何参数。 多余的参数将由plot方法的**kwds参数收集,并正确传递给基础的 matplotlib 函数。 例如,在第 2 步中,我们创建一个条形图。 这意味着我们可以使用 matplotlib bar函数中可用的所有参数,以及 Pandas plot方法中可用的参数。

在第 3 步中,我们创建一个单变量 KDE 图,该图将为数据帧中的每个数字列创建一个密度估计。 步骤 4 将所有两个变量图放置在同一图中。 同样,第 5 步将所有一变量图放置在一起。 第 4 步和第 5 步中的每个步骤都会创建一个具有三个轴对象的图形。 命令plt.subplots(1, 3)创建一个图形,该图形具有分布在一行和三列上的三个轴。 它返回一个由图和包含轴的一维 NumPy 数组组成的两元组。 元组的第一项被解包到变量fig中。 元组的第二个项目被解包为另外三个变量,每个变量一个。 Pandasplot方法方便地带有ax参数,使我们可以将绘图结果放入图中的特定轴中。

更多

除散点图外,所有图均未指定要使用的列。 Pandas 默认使用每一个数字列,并且在使用双变量图的情况下默认使用索引。 当然,您可以指定要用于每个 x 或 y 值的确切列:

>>> fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(16,4)) >>> df.sort_values('Apples').plot(x='Apples', y='Oranges', kind='line', ax=ax1) >>> df.plot(x='Apples', y='Oranges', kind='bar', ax=ax2) >>> df.plot(x='Apples', kind='kde', ax=ax3)

另见 Pandas 可视化的官方文档 可视化航班数据集

探索性数据分析主要由可视化指导,而 Pandas 为快速,轻松地创建它们提供了一个很好的接口。 开始可视化任何数据集时的一种简单策略是仅关注单变量图。 最受欢迎的单变量图往往是用于分类数据(通常是字符串)的条形图,以及用于连续数据(总是数字)的直方图,箱形图或 KDE。 直接在项目开始时尝试同时分析多个变量可能会很困难。

准备

在本秘籍中,我们通过直接用 Pandas 创建单变量和多变量图来对航班数据集进行一些基本的探索性数据分析。

操作步骤 读取航班数据集,并输出前五行: >>> flights = pd.read_csv('data/flights.csv') >>> flights.head()

在开始绘制之前,让我们计算转向,取消,延迟和准时飞行的数量。 我们已经有用于转移和取消的二进制列。 只要航班到达时间晚于预定时间 15 分钟或更长时间,便视为航班延误。 让我们创建两个新的二进制列来跟踪延迟到达和准时到达: >>> flights['DELAYED'] = flights['ARR_DELAY'].ge(15).astype(int) >>> cols = ['DIVERTED', 'CANCELLED', 'DELAYED'] >>> flights['ON_TIME'] = 1 - flights[cols].any(axis=1) >>> cols.append('ON_TIME') >>> status = flights[cols].sum() >>> status DIVERTED 137 CANCELLED 881 DELAYED 11685 ON_TIME 45789 dtype: int64 现在,让我们在同一图上为分类列和连续列绘制几个图: >>> fig, ax_array = plt.subplots(2, 3, figsize=(18,8)) >>> (ax1, ax2, ax3), (ax4, ax5, ax6) = ax_array >>> fig.suptitle('2015 US Flights - Univariate Summary', size=20) >>> ac = flights['AIRLINE'].value_counts() >>> ac.plot(kind='barh', ax=ax1, title='Airline') >>> oc = flights['ORG_AIR'].value_counts() >>> oc.plot(kind='bar', ax=ax2, rot=0, title='Origin City') >>> dc = flights['DEST_AIR'].value_counts().head(10) >>> dc.plot(kind='bar', ax=ax3, rot=0, title='Destination City') >>> status.plot(kind='bar', ax=ax4, rot=0, log=True, title='Flight Status') >>> flights['DIST'].plot(kind='kde', ax=ax5, xlim=(0, 3000), title='Distance KDE') >>> flights['ARR_DELAY'].plot(kind='hist', ax=ax6, title='Arrival Delay', range=(0,200))

这不是对所有单变量统计信息的详尽研究,但为我们提供了一些变量的详细信息。 在继续进行多变量图绘制之前,让我们绘制出每周的飞行次数。 使用带有 x 轴上日期的时间序列图的正确情况。 不幸的是,我们在任何列中都没有 Pandas 时间戳,但确实有月和日。to_datetime函数有一个巧妙的技巧,可以识别与时间戳组件匹配的列名。 例如,如果您有一个数据帧架,其中的标题栏正好为三列year,month,和day,,则将该数据帧传递给to_datetime函数将返回时间戳序列。 要准备我们当前的数据帧,我们需要为年份添加一列,并使用计划的出发时间来获取小时和分钟: >>> hour = flights['SCHED_DEP'] // 100 >>> minute = flights['SCHED_DEP'] % 100 >>> df_date = flights[['MONTH', 'DAY']].assign(YEAR=2015, HOUR=hour, MINUTE=minute) >>> df_date.head()

然后,几乎可以用to_datetime函数将这个数据帧转换为适当的时间戳序列: >>> flight_dep = pd.to_datetime(df_date) >>> flight_dep.head() 0 2015-01-01 16:25:00 1 2015-01-01 08:23:00 2 2015-01-01 13:05:00 3 2015-01-01 15:55:00 4 2015-01-01 17:20:00 dtype: datetime64[ns] 让我们将此结果用作新索引,然后使用resample方法查找每周的航班计数: >>> flights.index = flight_dep >>> fc = flights.resample('W').size() >>> fc.plot(figsize=(12,3), title='Flights per Week', grid=True)

这个绘图很有启发性。 看来我们没有十月份的数据。 由于缺少这些数据,如果存在趋势,则很难通过视觉分析任何趋势。 前几周和后几周也低于正常水平,可能是因为没有整周的数据。 让我们每周进行一次缺少少于 1,000 个航班的数据。 然后,我们可以使用interpolate方法填写此丢失的数据: >>> fc_miss = fc.where(fc > 1000) >>> fc_intp = fc_miss.interpolate(limit_direction='both') >>> ax = fc_intp.plot(color='black', figsize=(16,4)) >>> fc_intp[fc < 500].plot(linewidth=10, grid=True, color='.8', ax=ax) >>> ax.annotate(xy=(.8, .55), xytext=(.8, .77), xycoords='axes fraction', s='missing data', ha='center', size=20, arrowprops=dict()) >>> ax.set_title('Flights per Week (Interpolated Missing Data)')

让我们改变方向,专注于多变量绘图。 让我们找到以下 10 个机场: 入境航班旅行的平均距离最长 至少有 100 个航班: >>> flights.groupby('DEST_AIR')['DIST'] \ .agg(['mean', 'count']) \ .query('count > 100') \ .sort_values('mean') \ .tail(10) \ .plot(kind='bar', y='mean', rot=0, legend=False, title='Average Distance per Destination')

头两个目的地机场在夏威夷也就不足为奇了。 现在,让我们通过对 2,000 英里以下的所有航班的距离和通话时间进行散点图来同时分析两个变量: >>> fs = flights.reset_index(drop=True)[['DIST', 'AIR_TIME']] \ .query('DIST


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3