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

三、处理表格数据

原文: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
NameSexCountYear
0MaryF9217
1AnnaF3860
2EmmaF2587
1891891VernaM5
1891892WinnieM5
1891893WinthropM5

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
NameSexCountYear
0MaryF9217
1AnnaF3860
2EmmaF2587
1891891VernaM5
1891892WinnieM5
1891893WinthropM5

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']
NameSexCount
1AnnaF
2EmmaF
3ElizabethF
4MinnieF
5MargaretF

我们通常需要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']]
NameYear
0Mary
1Anna
2Emma
1891891Verna
1891892Winnie
1891893Winthrop

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']]
NameCount
0Mary
1Anna
2Emma
1891891Verna
1891892Winnie
1891893Winthrop

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
NameSexCountYear
1850880EmmaF19414
1850881OliviaF19246
1850882AvaF16237
1883745ZyahirM5
1883746ZyelM5
1883747ZylynM5

32868 行 × 4 列

对行排序

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

sorted_2016 = baby_2016.sort_values('Count', ascending=False)
sorted_2016
NameSexCountYear
1850880EmmaF19414
1850881OliviaF19246
1869637NoahM19015
1868752MikaelynF5
1868751MietteF5
1883747ZylynM5

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]
NameSexCountYear
1850880EmmaF19414
1850881OliviaF19246
1869637NoahM19015
1869638LiamM18138
1850882AvaF16237

总结

我们现在拥有了 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
NameSexCountYear
0MaryF9217
1AnnaF3860
2EmmaF2587
3ElizabethF2549
4MinnieF2243

拆分问题

我们应该首先注意到,上一节中的问题与这个问题有相似之处;上一节中的问题将名称限制为 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)
NameSexCount
Year
188020002000
188119351935
188221272127
20143320633206
20153306333063
20163286832868

137 行 × 3 列

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

baby.groupby('Year').agg(len)
NameSexCount
Year
188020002000
188119351935
188221272127
20143320633206
20153306333063
20163286832868

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_counts
Count
YearSex
1880F
M110491
1881F
2015M
2016F
M1880674

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
NameCount
YearSex
1880FMary
MJohn9655
1881FMary
2015MNoah
2016FEmma
MNoah19015

274 行 × 2 列

注意,多列分组会导致每行有多个标签。 这被称为“多级索引”,并且很难处理。 需要知道的重要事情是,.loc接受行索引的元组,而不是单个值:

baby_pop.loc[(2000, 'F'), 'Name']
# 'Emily'

.iloc的行为与往常一样,因为它使用索引而不是标签:

baby_pop.iloc[10:15, :]
NameCount
YearSex
1885FMary
MJohn8756
1886FMary
MJohn9026
1887FMary
透视

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

为了透视,使用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
SexFM
Year
1880MaryJohn
1881MaryJohn
1882MaryJohn
2014EmmaNoah
2015EmmaNoah
2016EmmaNoah

137 行 × 2 列

将此结果与我们使用.groupby()计算的baby_pop表进行比较。 我们可以看到baby_pop中的Sex索引成为了数据透视表的列。

baby_pop
NameCount
YearSex
1880FMary
MJohn9655
1881FMary
2015MNoah
2016FEmma
MNoah19015

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
NameSexCountYear
0MaryF9217
1AnnaF3860
2EmmaF2587
3ElizabethF2549
4MinnieF2243

拆解问题

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

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

应用

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
NameSexCountYearLast
0MaryF92171884
1AnnaF38601884
2EmmaF25871884
1891891VernaM51883
1891892WinnieM51883
1891893WinthropM51883

1891894 行 × 5 列

分组

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

# Shorthand for baby.groupby(['Last', 'Sex']).agg(np.sum)
baby.groupby(['Last', 'Sex']).sum()
CountYear
LastSex
aF58079486
M193163053566324
bF17376
yM18569388
zF142023
M1201239649274

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
LastSex
aF
M1931630
bF
yM
zF
M120123

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文档中查看绘图,我们了解到pandasDataFrame的一行中的列绘制为一组条形,并将每列显示为不同颜色的条形。 这意味着letter_dist表的透视版本将具有正确的格式。

letter_pivot = pd.pivot_table(
    baby, index='Last', columns='Sex', values='Count', aggfunc='sum'
)
letter_pivot
SexFM
Last
a580794861931630
b173761435939
c302621672407
x37381644092
y2487763818569388
z142023120123

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_pivot
SexFMF propM prop
Last
a5807948619316300.9678120.032188
b1737614359390.0119560.988044
c3026216724070.0177730.982227
x373816440920.0548530.945147
y24877638185693880.5725970.427403
z1420231201230.5417710.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中表达以下操作:

操作pandas
逐元素应用函数series.apply(func)
字符串操作series.str.func()
绘图df.plot.func()
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值