Skip to content

Latest commit

 

History

History
1333 lines (916 loc) · 66.1 KB

File metadata and controls

1333 lines (916 loc) · 66.1 KB

八、将数据重组为整齐的表格

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

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

介绍

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

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

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

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

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

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

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

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

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

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

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

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

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

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

准备

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

操作步骤

  1. 首先,请注意,状态名称位于数据帧的索引中。 这些状态正确地垂直放置,不需要重组。 问题是列名。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
  1. 注意,我们现在有了一个带有多重索引的序列。 现在索引中有两个级别。 原始索引已被推到左侧,以便为旧的列名腾出空间。 使用这一命令,我们现在基本上有了整洁的数据。 每个变量,状态,水果和重量都是垂直的。 让我们使用reset_index方法将结果转换为数据帧:
>>> state_fruit_tidy = state_fruit.stack().reset_index()
>>> state_fruit_tidy

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

  1. 可以直接使用鲜为人知的序列方法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
  1. 从这里,我们可以简单地将reset_index方法与name参数链接起来,以重现步骤 3 的输出:
>>> state_fruit.stack()\
               .rename_axis(['state', 'fruit'])\
               .reset_index(name='weight')

工作原理

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

在步骤 2 中重置索引后,pandas 将我们的数据帧的列默认设置为level_0level_10。 这是因为调用此方法的序列具有两个未正式命名的索引级别。 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()

另见

使用melt将变量值​​整理为列名

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

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

准备

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

操作步骤

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

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

  1. 这一步为我们创建了整洁的数据。 默认情况下,melt将转换后的前列名称称为变量,并将相应的值称为。 方便地,melt有两个附加参数var_namevalue_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')

另见

同时堆叠多组变量

一些数据集包含多组变量作为列名,需要同时堆叠到自己的列中。 以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 点赞数,那么我们将需要独立地堆叠两组列,而仅通过一次调用stackmelt

准备

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

操作步骤

  1. 我们将使用通用的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
  1. 将此函数传递给方法以转换所有列名:
>>> actor2 = actor.rename(columns=change_col_name)
>>> actor2.head()

  1. 使用wide_to_long函数可同时堆叠actoractor_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,它是一个字符串列表。 每个字符串代表一个列分组。 以该字符串开头的所有列都将被堆叠到一个列中。 在此秘籍中,有两列列:actoractor_facebook_likes。 默认情况下,这些列的每个组都需要以数字结尾。 此数字随后将用于标记整形数据。 每个列组都有一个下划线字符,将stubname与结尾数字分开。 为此,必须使用sep参数。

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

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

现在,我们已经正确地创建了两个独立的列组,即以actoractor_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

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

>>> 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='_')

另见

反转堆叠数据

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

准备

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

操作步骤

  1. 读取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()

  1. 使用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
  1. 使用unstack序列方法将堆叠的数据转换回原始格式:
>>> college_stacked.unstack()
  1. 可以先执行melt,然后执行pivot,然后执行类似的操作序列。 首先,读入数据而不将机构名称放在索引中:
>>> college2 = pd.read_csv('data/college.csv', 
                          usecols=usecol_func)
>>> college2.head()

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

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

  1. 请注意,机构名称现在已转移到索引中,而不是按其原始顺序排列。 列名称不是按其原始顺序。 要从第 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属性来转置不需要stackunstack的数据帧:

>>> college.T
>>> college.transpose()

另见

  • 请参阅第 4 章,“选择数据子集”中的“同时选择数据帧的行和列”秘籍
  • Pandas unstackpivot方法的官方文档

groupby聚合后解除堆叠

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

准备

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

操作步骤

  1. 读取员工数据集,并按种族找到平均工资:
>>> 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
  1. 这是一个非常简单的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
  1. 这种聚合更加复杂,可以进行重塑以简化不同的比较。 例如,如果每个种族并排而不是像现在这样垂直,则比较男性和女性的工资会更容易。 让我们解开性别索引级别:
>>> agg.unstack('GENDER')

  1. 同样,我们可以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列将产生多重索引列。 从这里开始,您可以继续使用unstackstack方法交换行和列级别,直到获得所需的数据结构为止:

>>> agg2.unstack('GENDER')

另见

  • 请参阅第 7 章,“分组和多列聚合”的“进行聚集,过滤和转换的分组函数”秘籍

使用groupby聚合复制pivot_table

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

准备

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

操作步骤

  1. 读取航班数据集,并使用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()

  1. groupby聚合无法直接复制此表。 诀窍是首先对indexcolumns参数中的所有列进行分组:
>>> 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
  1. 使用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参数中的列如何聚合。 它默认为均值,在此示例中,我们将其更改为计算总和。 此外,AIRLINEORG_AIR的某些唯一组合不存在。 这些缺失的组合将默认为结果数据帧中的缺失值。 在这里,我们使用fill_value参数将其更改为零。

步骤 2 使用indexcolumns参数中的所有列作为分组列开始复制过程。 这是使此秘籍生效的关键。 数据透视表只是分组列的所有唯一组合的交集。 步骤 3 通过使用unstack方法将最里面的索引级别转换为列名来完成复制。 就像pivot_table一样,并非AIRLINEORG_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聚合复制此代码,只需遵循秘籍中的相同模式,并将indexcolumns参数中的所有列放入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方法将数据显着重塑为所需的形式。

操作步骤

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

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

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

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

  1. 通过使用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)

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

  1. 一次堆叠所有列以返回序列:
>>> 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方法被调用两次,并对每个级别的实际值进行排序。 请注意,列级别的值是列名SATMTMIDUGDS

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

更多

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

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

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

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

准备

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

操作步骤

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

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

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

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

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

  1. 使用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()

  1. 可以使用以下方法创建相同的结果:
>>> 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列,将其删除。 最后,将两个整洁的数据帧相互比较,发现它们是等效的。

另见

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

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

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

准备

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

操作步骤

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

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

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

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

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

  1. 通过使用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不同,它对与indexcolumns参数中的列之间的交点相对应的所有值执行汇总。 由于此交点中可能存在多个值,因此pivot_table要求用户向其传递一个汇总函数,以便输出单个值。 我们使用first汇总函数,该函数采用组中的第一个值。 在此特定示例中,每个交叉点都只有一个值,因此没有任何要累加的值。 默认的聚合函数是均值,在这里会产生错误,因为某些值是字符串:

>>> inspections.pivot_table(index=['Name', 'Date'], 
                            columns='Info', 
                            values='Value', 
                            aggfunc='first') \
               .reset_index() \
               .rename_axis(None, axis='columns')

另见

在同一单元格中存储两个或多个值时进行整理

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

准备

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

操作步骤

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

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

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

工作原理

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

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

总共进行了三个拆分,得到了四列。 步骤 2 的第二行为其提供了有意义的名称。 即使所得的latitudelongitude列似乎是浮点数,也并非如此。 它们最初是从对象列进行解析的,因此仍然是对象数据类型。 步骤 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)

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

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

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

准备

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

操作步骤

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

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

  1. 这解决了我们的问题之一。 让我们使用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来旋转数据帧。 旋转后,GroupYear变量卡在索引中。 我们将它们以列的形式推出。pivot_table方法将columns参数中使用的列名称保留为列索引的名称。 重置索引后,该名称变得毫无意义,我们使用rename_axis将其删除。

更多

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

>>> sensors.set_index(['Group', 'Property']) \
           .stack() \
           .unstack('Property') \
           .rename_axis(['Group', 'Year'], axis='index') \
           .rename_axis(None, axis='columns') \
           .reset_index()

将多个观测单位存储在同一表中时进行整理

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

准备

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

操作步骤

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

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

  1. 让我们尝试使用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)

  1. 现在可以将数据集拆分为多个较小的表:
>>> 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']]

   

  1. 这些表仍然存在几个问题。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)

   

  1. 现在,我们已将观测单位分为各自的表,让我们将原始数据集的内存与这三个表进行比较:
>>> 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
  1. 实际上,我们的新整理数据会占用更多的内存。 这是可以预期的,因为原始列中的所有数据都被简单地散布到新表中。 新表还每个都有索引,并且其中两个表都有一个额外的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)

  

  1. 我们可以使用这些表形成中间表和唯一的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)

    

  1. 让我们对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)

   

  1. 让我们找出我们的新表消耗了多少内存:
>>> 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
  1. 现在我们已经标准化了表,我们可以构建一个实体关系图,显示所有表(实体),列和关系。 此图是使用易于使用的 ERDPlus 创建的:

工作原理

导入数据并识别这三个实体后,我们必须为每个观察创建一个唯一的标识符,以便在将电影,演员和导演分成不同的表格后,可以将它们链接在一起。 在第 2 步中,我们只需将 ID 列设置为从零开始的行号。 在第 3 步中,我们使用wide_to_long函数同时meltactordirector列。 它使用列的整数后缀垂直对齐数据,并将此整数后缀放置在索引中。 参数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 通过选择两个唯一标识符来创建关联表。 现在,我们可以将actordirector表简化为唯一的名称和 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

另见