数据科学的原理与技巧 三、处理表格数据

三、处理表格数据

原文: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')
baby
Name Sex Count Year
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 行 × 4 列

请注意,为了使上述代码正常工作,babynames.csv文件必须位于这个笔记本的相同目录中。 通过在笔记本单元格中运行ls,我们可以检查当前文件夹中的文件:

ls
# babynames.csv                  indexes_slicing_sorting.ipynb

当我们使用熊猫来读取数据时,我们得到一个DataFrameDataFrame是一个表格数据结构,其中每列都有标签(这里是'Name', 'Sex', 'Count', 'Year'),并且每一行都有标签(这里是0,1,2, ..., 1891893)。 然而,Data8 中引入的表格仅包含列标签。

DataFrame的标签称为DataFrame的索引,并使许多数据操作更容易。

索引、切片和排序

让我们使用pandas来回答以下问题:

2016 年的五个最受欢迎的婴儿名字是?

拆分问题

我们可以将这个问题分解成以下更简单的表格操作:

  • 分割出 2016 年的行。
  • 按照计数对行降序排序。

现在,我们可以在pandas中表达这些步骤。

使用.loc切片

为了选择DataFrame的子集,我们使用.loc切片语法。 第一个参数是行标签,第二个参数是列标签:

baby
Name Sex Count Year
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 行 × 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 3860
2 Emma F 2587
3 Elizabeth F 2549
4 Minnie F 2243
5 Margaret F 2142

我们通常需要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 1884
1 Anna 1884
2 Emma 1884
... ... ...
1891891 Verna 1883
1891892 Winnie 1883
1891893 Winthrop 1883

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 9217
1 Anna 3860
2 Emma 2587
... ... ...
1891891 Verna 5
1891892 Winnie 5
1891893 Winthrop 5

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
'''

一旦我们有了这个TrueFalse的序列,我们就可以将它传递给.loc

# We are slicing rows, so the boolean Series goes in the first
# argument to .loc
baby_2016 = baby.loc[baby['Year'] == 2016, :]
baby_2016
Name Sex Count Year
1850880 Emma F 19414 2016
1850881 Olivia F 19246 2016
1850882 Ava F 16237 2016
... ... ... ... ...
1883745 Zyahir M 5 2016
1883746 Zyel M 5 2016
1883747 Zylyn M 5 2016

32868 行 × 4 列

对行排序

下一步是按'Count'对行降序排序。 我们可以使用sort_values()函数。

sorted_2016 = baby_2016.sort_values('Count', ascending=False)
sorted_2016
Name Sex Count Year
1850880 Emma F 19414 2016
1850881 Olivia F 19246 2016
1869637 Noah M 19015 2016
... ... ... ... ...
1868752 Mikaelyn F 5 2016
1868751 Miette F 5 2016
1883747 Zylyn M 5 2016

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 2016
1850881 Olivia F 19246 2016
1869637 Noah M 19015 2016
1869638 Liam M 18138 2016
1850882 Ava F 16237 2016

总结

我们现在拥有了 2016 年的五个最受欢迎的婴儿名称,并且学会了在pandas中表达以下操作:

操作 pandas
读取 CSV 文件 pd.read_csv()
使用标签或索引来切片 .loc.iloc
使用谓词对行切片 .loc中使用布尔值的序列
对行排序 .sort_values()

分组和透视

在本节中,我们将回答这个问题:

每年最受欢迎的男性和女性名称是什么?

这里再次展示了婴儿名称数据集:

baby = pd.read_csv('babynames.csv')
baby.head()
# the .head() method outputs the first five rows of the DataFrame
Name Sex Count Year
0 Mary F 9217 1884
1 Anna F 3860 1884
2 Emma F 2587 1884
3 Elizabeth F 2549 1884
4 Minnie F 2243 1884

拆分问题

我们应该首先注意到,上一节中的问题与这个问题有相似之处;上一节中的问题将名称限制为 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 2000
1881 1935 1935 1935
1882 2127 2127 2127
... ... ... ...
2014 33206 33206 33206
2015 33063 33063 33063
2016 32868 32868 32868

137 行 × 3 列

你可能会注意到length函数只是简单调用了len函数,所以我们可以简化上面的代码。

baby.groupby('Year').agg(len)

| Name | Sex | Count |
| --- | --- | --- | --- |
| Year | | | |
| 1880 | 2000 | 2000 | 2000 |
| 1881 | 1935 | 1935 | 1935 |
| 1882 | 2127 | 2127 | 2127 |
| ... | ... | ... | ... |
| 2014 | 33206 | 33206 | 33206 |
| 2015 | 33063 | 33063 | 33063 |
| 2016 | 32868 | 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 2000
1881 1935
1882 2127
... ...
2014 33206
2015 33063
2016 32868

137 行 × 1 列

请注意,生成的DataFrame的索引现在包含特定年份,因此我们可以像以前一样,使用.loc分割出年份的子集:

# Every twentieth year starting at 1880
year_rows.loc[1880:2016:20, :]
Count
Year
1880 2000
1900 3730
1920 10755
1940 8961
1960 11924
1980 19440
2000 29764

多个列的分组

我们在 Data8 中看到,我们可以按照多个列分组,基于唯一值来获取分组。 为此,请将列标签列表传递到.groupby()

grouped_counts = baby.groupby(['Year', 'Sex']).sum()
grouped_counts
Count
Year Sex
1880 F 90992
M 110491
1881 F 91953
... ... ...
2015 M 1907211
2016 F 1756647
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_pop
Name Count
Year Sex
1880 F Mary 7065
M John 9655
1881 F Mary 6919
... ... ... ...
2015 M Noah 19594
2016 F Emma 19414
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 9128
M John 8756
1886 F Mary 9889
M John 9026
1887 F Mary 9888

透视

如果按两列分组,则通常可以使用数据透视表,以更方便的格式显示数据。 数据透视表可以使用一组分组标签,作为结果表的列。

为了透视,使用pd.pivot_table()函数。

pd.pivot_table(baby,
               index='Year',         # Index for rows
               columns='Sex',        # Columns
               values='Name',        # Values in table
               aggfunc=most_popular) # Aggregation function
Sex 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_pop
Name Count
Year Sex
1880 F Mary 7065
M John 9655
1881 F Mary 6919
... ... ... ...
2015 M Noah 19594
2016 F Emma 19414
M Noah 19015

274 行 × 2 列

总结

我们现在有了数据集中每个性别和年份的最受欢迎的婴儿名称,并学会了在pandas中表达以下操作:

操作 pandas
分组 df.groupby(label)
多列分组 df.groupby([label1, label2])
分组和聚合 df.groupby(label).agg(func)
透视 pd.pivot_table()

应用、字符串和绘图

在本节中,我们将回答这个问题:

我们可以用名字的最后一个字母来预测婴儿的性别吗?

这里再次展示了婴儿名称数据集:

baby = pd.read_csv('babynames.csv')
baby.head()
# the .head() method outputs the first five rows of the DataFrame
Name Sex Count Year
0 Mary F 9217 1884
1 Anna F 3860 1884
2 Emma F 2587 1884
3 Elizabeth F 2549 1884
4 Minnie F 2243 1884

拆解问题

虽然有很多方法可以预测是否可能,但我们将在本节中使用绘图。 我们可以将这个问题分解为两个步骤:

  • 计算每个名称的最后一个字母。
  • 按照最后一个字母和性别分组,使用计数来聚合。
  • 绘制每个性别和字母的计数。

应用

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]
baby
Name Sex Count Year Last
0 Mary F 9217 1884 y
1 Anna F 3860 1884 a
2 Emma F 2587 1884 a
... ... ... ... ... ...
1891891 Verna M 5 1883 a
1891892 Winnie M 5 1883 e
1891893 Winthrop M 5 1883 p

1891894 行 × 5 列

分组

为了计算每个最后一个字母的性别分布,我们需要按LastSex分组。

# Shorthand for baby.groupby(['Last', 'Sex']).agg(np.sum)
baby.groupby(['Last', 'Sex']).sum()
Count Year
Last Sex
a F 58079486 915565667
M 1931630 53566324
b F 17376 1092953
... ... ... ...
y M 18569388 114394474
z F 142023 4268028
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_dist
Count
Last Sex
a F 58079486
M 1931630
b F 17376
... ... ...
y M 18569388
z F 142023
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>
image

虽然这个绘图显示了字母和性别的分布,但是男性和女性的条形很难分开。 通过在pandas文档中查看绘图,我们了解到pandasDataFrame的一行中的列绘制为一组条形,并将每列显示为不同颜色的条形。 这意味着letter_dist表的透视版本将具有正确的格式。

letter_pivot = pd.pivot_table(
    baby, index='Last', columns='Sex', values='Count', aggfunc='sum'
)
letter_pivot
Sex 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>
image

请注意,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_pivot
Sex 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>
image

总结

我们可以看到几乎所有以'p'结尾的名字都是男性,以'a'结尾的名字都是女性! 一般来说,许多字母的条形长度之间的差异意味着,如果我们只知道他们的名字的最后一个字母,我们往往可以准确猜测一个人的性别。

我们已经学会在pandas中表达以下操作:

操作 pandas
逐元素应用函数 series.apply(func)
字符串操作 series.str.func()
绘图 df.plot.func()
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,451评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,172评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,782评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,709评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,733评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,578评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,320评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,241评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,686评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,878评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,992评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,715评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,336评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,912评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,040评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,173评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,947评论 2 355

推荐阅读更多精彩内容