三、处理表格数据
原文:DS-100/textbook/notebooks/ch03
译者:飞龙
协议:CC BY-NC-SA 4.0
自豪地采用谷歌翻译
索引、切片和排序
起步
在本章的每一节中,我们将使用第一章中的婴儿名称数据集。我们将提出一个问题,将问题分解为大体步骤,然后使用pandas DataFrame将每个步骤转换为 Python 代码。 我们从导入pandas开始:
# pd is a common shorthand for pandas
import pandas as pd现在我们可以使用pd.read_csv读取数据。
baby = pd.read_csv('babynames.csv')
babyName  | Sex  | Count  | Year  | 
0  | Mary  | F  | 9217  | 
1  | Anna  | F  | 3860  | 
2  | Emma  | F  | 2587  | 
…  | …  | …  | …  | 
1891891  | Verna  | M  | 5  | 
1891892  | Winnie  | M  | 5  | 
1891893  | Winthrop  | M  | 5  | 
1891894 行 × 4 列
请注意,为了使上述代码正常工作,babynames.csv文件必须位于这个笔记本的相同目录中。 通过在笔记本单元格中运行ls,我们可以检查当前文件夹中的文件:
ls
# babynames.csv                  indexes_slicing_sorting.ipynb当我们使用熊猫来读取数据时,我们得到一个DataFrame。 DataFrame是一个表格数据结构,其中每列都有标签(这里是'Name', 'Sex', 'Count', 'Year'),并且每一行都有标签(这里是0,1,2, ..., 1891893)。 然而,Data8 中引入的表格仅包含列标签。
DataFrame的标签称为DataFrame的索引,并使许多数据操作更容易。
索引、切片和排序
让我们使用pandas来回答以下问题:
2016 年的五个最受欢迎的婴儿名字是?
拆分问题
我们可以将这个问题分解成以下更简单的表格操作:
- 分割出 2016 年的行。
 - 按照计数对行降序排序。
 
现在,我们可以在pandas中表达这些步骤。
使用.loc切片
为了选择DataFrame的子集,我们使用.loc切片语法。 第一个参数是行标签,第二个参数是列标签:
babyName  | Sex  | Count  | Year  | 
0  | Mary  | F  | 9217  | 
1  | Anna  | F  | 3860  | 
2  | Emma  | F  | 2587  | 
…  | …  | …  | …  | 
1891891  | Verna  | M  | 5  | 
1891892  | Winnie  | M  | 5  | 
1891893  | Winthrop  | M  | 5  | 
1891894 行 × 4 列
baby.loc[1, 'Name'] # Row labeled 1, Column labeled 'Name'
# 'Anna'要分割出多行或多列,我们可以使用:。 请注意.loc切片是包容性的,与 Python 的切片不同。
# Get rows 1 through 5, columns Name through Count inclusive
baby.loc[1:5, 'Name':'Count']Name  | Sex  | Count  | 
1  | Anna  | F  | 
2  | Emma  | F  | 
3  | Elizabeth  | F  | 
4  | Minnie  | F  | 
5  | Margaret  | F  | 
我们通常需要DataFrame中的单个列:
baby.loc[:, 'Year']
'''
0          1884
1          1884
2          1884
           ... 
1891891    1883
1891892    1883
1891893    1883
Name: Year, Length: 1891894, dtype: int64
'''请注意,当我们选择一列时,我们会得到一个pandas序列。 序列就像一维 NumPy 数组,因为我们可以一次在所有元素上执行算术运算。
baby.loc[:, 'Year'] * 2
'''
0          3768
1          3768
2          3768
           ... 
1891891    3766
1891892    3766
1891893    3766
Name: Year, Length: 1891894, dtype: int64
'''为了选择特定的列,我们可以将列表传递给.loc切片:
# This is a DataFrame again
baby.loc[:, ['Name', 'Year']]Name  | Year  | 
0  | Mary  | 
1  | Anna  | 
2  | Emma  | 
…  | …  | 
1891891  | Verna  | 
1891892  | Winnie  | 
1891893  | Winthrop  | 
1891894 行 × 2 列
选择列很常见,所以存在简写。
# Shorthand for baby.loc[:, 'Name']
baby['Name']
'''
0              Mary
1              Anna
2              Emma
             ...   
1891891       Verna
1891892      Winnie
1891893    Winthrop
Name: Name, Length: 1891894, dtype: object
'''# Shorthand for baby.loc[:, ['Name', 'Count']]
baby[['Name', 'Count']]Name  | Count  | 
0  | Mary  | 
1  | Anna  | 
2  | Emma  | 
…  | …  | 
1891891  | Verna  | 
1891892  | Winnie  | 
1891893  | Winthrop  | 
1891894 行 × 2 列
使用谓词对行切片
为了分割出 2016 年的行,我们将首先创建一个序列,其中每个想要保留的行为True,每个想要删除的行为False。 这很简单,因为序列上的数学和布尔运算符,应用于序列中的每个元素。
# Series of years
baby['Year']
'''
0          1884
1          1884
2          1884
           ... 
1891891    1883
1891892    1883
1891893    1883
Name: Year, Length: 1891894, dtype: int64
'''# Compare each year with 2016
baby['Year'] == 2016
'''
0          False
1          False
2          False
           ...  
1891891    False
1891892    False
1891893    False
Name: Year, Length: 1891894, dtype: bool
'''一旦我们有了这个True和False的序列,我们就可以将它传递给.loc。
# We are slicing rows, so the boolean Series goes in the first
# argument to .loc
baby_2016 = baby.loc[baby['Year'] == 2016, :]
baby_2016Name  | Sex  | Count  | Year  | 
1850880  | Emma  | F  | 19414  | 
1850881  | Olivia  | F  | 19246  | 
1850882  | Ava  | F  | 16237  | 
…  | …  | …  | …  | 
1883745  | Zyahir  | M  | 5  | 
1883746  | Zyel  | M  | 5  | 
1883747  | Zylyn  | M  | 5  | 
32868 行 × 4 列
对行排序
下一步是按'Count'对行降序排序。 我们可以使用sort_values()函数。
sorted_2016 = baby_2016.sort_values('Count', ascending=False)
sorted_2016Name  | Sex  | Count  | Year  | 
1850880  | Emma  | F  | 19414  | 
1850881  | Olivia  | F  | 19246  | 
1869637  | Noah  | M  | 19015  | 
…  | …  | …  | …  | 
1868752  | Mikaelyn  | F  | 5  | 
1868751  | Miette  | F  | 5  | 
1883747  | Zylyn  | M  | 5  | 
32868 行 × 4 列
最后,我们将使用.iloc分割出DataFrame的前五行。 .iloc的工作方式类似.loc,但接受数字索引而不是标签。 它的切片中没有包含右边界,就像 Python 的列表切片。
# Get the value in the zeroth row, zeroth column
sorted_2016.iloc[0, 0]
# Get the first five rows
sorted_2016.iloc[0:5]Name  | Sex  | Count  | Year  | 
1850880  | Emma  | F  | 19414  | 
1850881  | Olivia  | F  | 19246  | 
1869637  | Noah  | M  | 19015  | 
1869638  | Liam  | M  | 18138  | 
1850882  | Ava  | F  | 16237  | 
总结
我们现在拥有了 2016 年的五个最受欢迎的婴儿名称,并且学会了在pandas中表达以下操作:
操作  | 
  | 
读取 CSV 文件  | 
  | 
使用标签或索引来切片  | 
  | 
使用谓词对行切片  | 在  | 
对行排序  | 
  | 
分组和透视
在本节中,我们将回答这个问题:
每年最受欢迎的男性和女性名称是什么?
这里再次展示了婴儿名称数据集:
baby = pd.read_csv('babynames.csv')
baby.head()
# the .head() method outputs the first five rows of the DataFrameName  | Sex  | Count  | Year  | 
0  | Mary  | F  | 9217  | 
1  | Anna  | F  | 3860  | 
2  | Emma  | F  | 2587  | 
3  | Elizabeth  | F  | 2549  | 
4  | Minnie  | F  | 2243  | 
拆分问题
我们应该首先注意到,上一节中的问题与这个问题有相似之处;上一节中的问题将名称限制为 2016 年出生的婴儿,而这个问题要求所有年份的名称。
我们再次将这个问题分解成更简单的表格操作。
- 将
baby表按'Year'和'Sex'分组。 - 对于每一组,计算最流行的名称。
 
认识到每个问题需要哪种操作,有时很棘手。通常,一系列复杂的步骤会告诉你,可能有更简单的方式来表达你想要的东西。例如,如果我们没有立即意识到需要分组,我们可能会编写如下步骤:
- 遍历每个特定的年份。
 - 对于每一年,遍历每个特定的性别。
 - 对于每一个特定年份和性别,找到最常见的名字。
 
几乎总是有一种更好的替代方法,用于遍历pandas DataFrame。特别是,遍历DataFrame的特定值,通常应该替换为分组。
分组
为了在pandas中进行分组。 我们使用.groupby()方法。
baby.groupby('Year')
# <pandas.core.groupby.DataFrameGroupBy object at 0x1a14e21f60>.groupby()返回一个奇怪的DataFrameGroupBy对象。 我们可以使用聚合函数,在该对象上调用.agg()来获得熟悉的输出:
# The aggregation function takes in a series of values for each group
# and outputs a single value
def length(series):
    return len(series)
# Count up number of values for each year. This is equivalent to
# counting the number of rows where each year appears.
baby.groupby('Year').agg(length)Name  | Sex  | Count  | 
Year  | ||
1880  | 2000  | 2000  | 
1881  | 1935  | 1935  | 
1882  | 2127  | 2127  | 
…  | …  | …  | 
2014  | 33206  | 33206  | 
2015  | 33063  | 33063  | 
2016  | 32868  | 32868  | 
137 行 × 3 列
你可能会注意到length函数只是简单调用了len函数,所以我们可以简化上面的代码。
baby.groupby('Year').agg(len)Name  | Sex  | Count  | 
Year  | ||
1880  | 2000  | 2000  | 
1881  | 1935  | 1935  | 
1882  | 2127  | 2127  | 
…  | …  | …  | 
2014  | 33206  | 33206  | 
2015  | 33063  | 33063  | 
2016  | 32868  | 32868  | 
137 行 × 3 列
聚合应用于DataFrame的每一列,从而产生冗余信息。 我们可以在分组之前使用切片限制输出列。
year_rows = baby[['Year', 'Count']].groupby('Year').agg(len)
year_rows
# A further shorthand to accomplish the same result:
#
# year_counts = baby[['Year', 'Count']].groupby('Year').count()
#
# pandas has shorthands for common aggregation functions, including
# count, sum, and mean.Count  | 
Year  | 
1880  | 
1881  | 
1882  | 
…  | 
2014  | 
2015  | 
2016  | 
137 行 × 1 列
请注意,生成的DataFrame的索引现在包含特定年份,因此我们可以像以前一样,使用.loc分割出年份的子集:
# Every twentieth year starting at 1880
year_rows.loc[1880:2016:20, :]Count  | 
Year  | 
1880  | 
1900  | 
1920  | 
1940  | 
1960  | 
1980  | 
2000  | 
多个列的分组
我们在 Data8 中看到,我们可以按照多个列分组,基于唯一值来获取分组。 为此,请将列标签列表传递到.groupby()。
grouped_counts = baby.groupby(['Year', 'Sex']).sum()
grouped_countsCount  | |
Year  | Sex  | 
1880  | F  | 
M  | 110491  | 
1881  | F  | 
…  | …  | 
2015  | M  | 
2016  | F  | 
M  | 1880674  | 
274 行 × 1 列
上面的代码计算每年每个性别出生的婴儿总数。 现在让我们使用多列分组,来计算每年和每个性别的最流行的名称。 由于数据已按照年和性别的递减顺序排序,因此我们可以定义一个聚合函数,该函数返回每个序列中的第一个值。 (如果数据没有排序,我们可以先调用sort_values()。)
# The most popular name is simply the first one that appears in the series
def most_popular(series):
    return series.iloc[0]
baby_pop = baby.groupby(['Year', 'Sex']).agg(most_popular)
baby_popName  | Count  | |
Year  | Sex  | |
1880  | F  | Mary  | 
M  | John  | 9655  | 
1881  | F  | Mary  | 
…  | …  | …  | 
2015  | M  | Noah  | 
2016  | F  | Emma  | 
M  | Noah  | 19015  | 
274 行 × 2 列
注意,多列分组会导致每行有多个标签。 这被称为“多级索引”,并且很难处理。 需要知道的重要事情是,.loc接受行索引的元组,而不是单个值:
baby_pop.loc[(2000, 'F'), 'Name']
# 'Emily'但.iloc的行为与往常一样,因为它使用索引而不是标签:
baby_pop.iloc[10:15, :]Name  | Count  | |
Year  | Sex  | |
1885  | F  | Mary  | 
M  | John  | 8756  | 
1886  | F  | Mary  | 
M  | John  | 9026  | 
1887  | F  | Mary  | 
透视
如果按两列分组,则通常可以使用数据透视表,以更方便的格式显示数据。 数据透视表可以使用一组分组标签,作为结果表的列。
为了透视,使用pd.pivot_table()函数。
pd.pivot_table(baby,
               index='Year',         # Index for rows
               columns='Sex',        # Columns
               values='Name',        # Values in table
               aggfunc=most_popular) # Aggregation functionSex  | F  | M  | 
Year  | ||
1880  | Mary  | John  | 
1881  | Mary  | John  | 
1882  | Mary  | John  | 
…  | …  | …  | 
2014  | Emma  | Noah  | 
2015  | Emma  | Noah  | 
2016  | Emma  | Noah  | 
137 行 × 2 列
将此结果与我们使用.groupby()计算的baby_pop表进行比较。 我们可以看到baby_pop中的Sex索引成为了数据透视表的列。
baby_popName  | Count  | |
Year  | Sex  | |
1880  | F  | Mary  | 
M  | John  | 9655  | 
1881  | F  | Mary  | 
…  | …  | …  | 
2015  | M  | Noah  | 
2016  | F  | Emma  | 
M  | Noah  | 19015  | 
274 行 × 2 列
总结
我们现在有了数据集中每个性别和年份的最受欢迎的婴儿名称,并学会了在pandas中表达以下操作:
操作  | 
  | 
分组  | 
  | 
多列分组  | 
  | 
分组和聚合  | 
  | 
透视  | 
  | 
应用、字符串和绘图
在本节中,我们将回答这个问题:
我们可以用名字的最后一个字母来预测婴儿的性别吗?
这里再次展示了婴儿名称数据集:
baby = pd.read_csv('babynames.csv')
baby.head()
# the .head() method outputs the first five rows of the DataFrameName  | Sex  | Count  | Year  | 
0  | Mary  | F  | 9217  | 
1  | Anna  | F  | 3860  | 
2  | Emma  | F  | 2587  | 
3  | Elizabeth  | F  | 2549  | 
4  | Minnie  | F  | 2243  | 
拆解问题
虽然有很多方法可以预测是否可能,但我们将在本节中使用绘图。 我们可以将这个问题分解为两个步骤:
- 计算每个名称的最后一个字母。
 - 按照最后一个字母和性别分组,使用计数来聚合。
 - 绘制每个性别和字母的计数。
 
应用
pandas序列包含.apply()方法,它接受一个函数并将其应用于序列中的每个值。
names = baby['Name']
names.apply(len)
'''
0          4
1          4
2          4
          ..
1891891    5
1891892    6
1891893    8
Name: Name, Length: 1891894, dtype: int64
'''为了提取每个名字的最后一个字母,我们可以定义我们自己的函数来传入.apply():
def last_letter(string):
    return string[-1]
names.apply(last_letter)
'''
0          y
1          a
2          a
          ..
1891891    a
1891892    e
1891893    p
Name: Name, Length: 1891894, dtype: object
'''字符串操作
虽然.apply()是灵活的,但在处理文本数据时,在使用pandas内置的字符串操作函数通常会更快。
pandas通过序列的.str属性,提供字符串操作函数。
names = baby['Name']
names.str.len()
'''
0          4
1          4
2          4
          ..
1891891    5
1891892    6
1891893    8
Name: Name, Length: 1891894, dtype: int64
'''我们可以用类似的方式,直接分离出每个名字的最后一个字母。
names.str[-1]
'''
0          y
1          a
2          a
          ..
1891891    a
1891892    e
1891893    p
Name: Name, Length: 1891894, dtype: object
'''我们建议查看文档来获取字符串方法的完整列表。
我们现在可以将最后一个字母的这一列添加到我们的婴儿数据帧中。
baby['Last'] = names.str[-1]
babyName  | Sex  | Count  | Year  | Last  | 
0  | Mary  | F  | 9217  | 1884  | 
1  | Anna  | F  | 3860  | 1884  | 
2  | Emma  | F  | 2587  | 1884  | 
…  | …  | …  | …  | …  | 
1891891  | Verna  | M  | 5  | 1883  | 
1891892  | Winnie  | M  | 5  | 1883  | 
1891893  | Winthrop  | M  | 5  | 1883  | 
1891894 行 × 5 列
分组
为了计算每个最后一个字母的性别分布,我们需要按Last和Sex分组。
# Shorthand for baby.groupby(['Last', 'Sex']).agg(np.sum)
baby.groupby(['Last', 'Sex']).sum()Count  | Year  | |
Last  | Sex  | |
a  | F  | 58079486  | 
M  | 1931630  | 53566324  | 
b  | F  | 17376  | 
…  | …  | …  | 
y  | M  | 18569388  | 
z  | F  | 142023  | 
M  | 120123  | 9649274  | 
52 行 × 2 列
请注意,因为每个没有用于分组的列都传递到聚合函数中,所以也求和了年份。 为避免这种情况,我们可以在调用.groupby()之前选择所需的列。
# When lines get long, you can wrap the entire expression in parentheses
# and insert newlines before each method call
letter_dist = (
    baby[['Last', 'Sex', 'Count']]
    .groupby(['Last', 'Sex'])
    .sum()
)
letter_distCount  | |
Last  | Sex  | 
a  | F  | 
M  | 1931630  | 
b  | F  | 
…  | …  | 
y  | M  | 
z  | F  | 
M  | 120123  | 
52 行 × 1 列
绘图
pandas为大多数基本绘图提供了内置的绘图函数,包括条形图,直方图,折线图和散点图。 为了从DataFrame中绘制图形,请使用.plot属性:
# We use the figsize option to make the plot larger
letter_dist.plot.barh(figsize=(10, 10))
# <matplotlib.axes._subplots.AxesSubplot at 0x1a17af4780>虽然这个绘图显示了字母和性别的分布,但是男性和女性的条形很难分开。 通过在pandas文档中查看绘图,我们了解到pandas将DataFrame的一行中的列绘制为一组条形,并将每列显示为不同颜色的条形。 这意味着letter_dist表的透视版本将具有正确的格式。
letter_pivot = pd.pivot_table(
    baby, index='Last', columns='Sex', values='Count', aggfunc='sum'
)
letter_pivotSex  | F  | M  | 
Last  | ||
a  | 58079486  | 1931630  | 
b  | 17376  | 1435939  | 
c  | 30262  | 1672407  | 
…  | …  | …  | 
x  | 37381  | 644092  | 
y  | 24877638  | 18569388  | 
z  | 142023  | 120123  | 
26 行 × 2 列
letter_pivot.plot.barh(figsize=(10, 10))
# <matplotlib.axes._subplots.AxesSubplot at 0x1a17c36978>请注意,pandas为我们生成了图例,这很方便 但是,这仍然难以解释。 我们为每个字母和性别绘制了计数,这些计数会导致一些条形看起来很长,而另一些几乎看不见。 相反,我们应该绘制每个最后一个字母的男性和女性的比例。
total_for_each_letter = letter_pivot['F'] + letter_pivot['M']
letter_pivot['F prop'] = letter_pivot['F'] / total_for_each_letter
letter_pivot['M prop'] = letter_pivot['M'] / total_for_each_letter
letter_pivotSex  | F  | M  | F prop  | M prop  | 
Last  | ||||
a  | 58079486  | 1931630  | 0.967812  | 0.032188  | 
b  | 17376  | 1435939  | 0.011956  | 0.988044  | 
c  | 30262  | 1672407  | 0.017773  | 0.982227  | 
…  | …  | …  | …  | …  | 
x  | 37381  | 644092  | 0.054853  | 0.945147  | 
y  | 24877638  | 18569388  | 0.572597  | 0.427403  | 
z  | 142023  | 120123  | 0.541771  | 0.458229  | 
26 行 × 4 列
(letter_pivot[['F prop', 'M prop']]
 .sort_values('M prop') # Sorting orders the plotted bars
 .plot.barh(figsize=(10, 10))
)
# <matplotlib.axes._subplots.AxesSubplot at 0x1a18194b70>总结
我们可以看到几乎所有以'p'结尾的名字都是男性,以'a'结尾的名字都是女性! 一般来说,许多字母的条形长度之间的差异意味着,如果我们只知道他们的名字的最后一个字母,我们往往可以准确猜测一个人的性别。
我们已经学会在pandas中表达以下操作:
操作  | 
  | 
逐元素应用函数  | 
  | 
字符串操作  | 
  | 
绘图  | 
  | 
                










