UCB Data100:数据科学的原理和技巧:第一章到第五章

发布时间:2024年01月13日

一、引言

原文:Introduction

译者:飞龙

协议:CC BY-NC-SA 4.0

学习成果

  • 了解 Data 100 的总体目标

  • 了解数据科学生命周期的阶段

数据科学是一个跨学科领域,具有各种应用,并且在解决具有挑战性的社会问题方面具有巨大潜力。通过建立数据科学技能,您可以赋予自己参与和引领塑造您的生活和整个社会对话的能力,无论是与气候变化作斗争、推出多样性倡议,还是其他方面。

这个领域正在迅速发展;现代数据科学中许多关键技术基础在 21 世纪初得到了普及。

它基本上是以人为中心的,并通过定量平衡权衡来促进决策。为了可靠地量化事物,我们必须适当地使用和分析数据,对每一步都要进行批判性思考和怀疑,并考虑我们的决定如何影响他人。

最终,数据科学是将以数据为中心的、计算性的和推理性的思维应用于:

  • 了解世界(科学)。

  • 解决问题(工程)。

venn

对数据科学的真正掌握需要深刻的理论理解和对领域专业知识的牢固掌握。本课程将帮助您建立在前者基础上的技术知识,使您能够获取数据并对世界上最具挑战性和模糊的问题产生有用的见解。

课程目标

  • 为您准备伯克利高级课程,包括数据管理、机器学习和统计学

  • 使您能够在数据科学领域开展职业生涯

  • 使您能够通过计算和推理思维解决现实世界的问题

我们将涵盖的一些主题

  • Pandas 和 NumPy

  • 探索性数据分析

  • 正则表达式

  • 可视化

  • 抽样

  • 模型设计和损失公式

  • 线性回归

  • 梯度下降

  • 逻辑回归

  • 还有更多!

为了让您成功,我们将 Data 100 中的概念组织成了数据科学生命周期:一个迭代过程,涵盖了数据科学的各种统计和计算构建模块。

1.1 数据科学生命周期

数据科学生命周期是对数据科学工作流程的高级概述。这是一个数据科学家在对数据驱动的问题进行彻底分析时应该探索的阶段循环。

数据科学生命周期中存在许多关键思想的变体。在 Data 100 中,我们使用流程图来可视化生命周期的各个阶段。请注意,有两个入口点。

data_life_cycle

1.1.1 提出问题

无论是出于好奇还是出于必要,数据科学家不断提出问题。例如,在商业世界中,数据科学家可能对预测某项投资产生的利润感兴趣。在医学领域,他们可能会问一些患者是否比其他人更有可能从治疗中受益。

提出问题是数据科学生命周期开始的主要方式之一。它有助于充分定义问题。在构建问题之前,以下是一些您应该问自己的事情。

  • 我们想要知道什么?

    • 一个过于模糊的问题可能会导致混乱。
  • 我们试图解决什么问题?

    • 问一个问题的目标应该是清晰的,以便为利益相关者的努力提供合理的理由。
  • 我们想要测试的假设是什么?

    • 这为我们提供了一个清晰的视角,以分析最终结果。
  • 我们的成功指标是什么?

    • 这为我们建立了一个明确的观点,知道何时结束项目。

ask_question

1.1.2 获取数据

生命周期的第二个入口是通过获取数据。对任何问题的仔细分析都需要使用数据。数据可能对我们而言是 readily available,或者我们可能不得不着手收集数据。在这样做时,至关重要的是要问以下问题:

  • 我们有什么数据,我们需要什么数据?

    • 定义数据的单位(人、城市、时间点等)和要测量的特征。
  • 我们如何取样更多的数据?

    • 抓取网页,手动收集,进行实验等。
  • 我们的数据是否代表我们想研究的人群?

    • 如果我们的数据不代表我们感兴趣的人群,那么我们可能得出错误的结论。

关键程序:数据获取数据清洗

data_acquisition

1.1.3 理解数据

原始数据本身并不具有固有的用处。如果不仔细调查,就不可能辨别出所有变量之间的模式和关系。因此,将纯数据转化为可操作的见解是数据科学家的一项关键工作。例如,我们可以选择问:

  • 我们的数据是如何组织的,它包含了什么?

    • 了解数据对世界有何影响有助于我们更好地理解世界。
  • 我们有相关的数据吗?

    • 如果我们收集的数据对于手头的问题没有用处,那么我们必须收集更多的数据。
  • 数据中存在什么偏见、异常或其他问题?

    • 如果忽视这些问题,可能会导致许多错误的结论,因此数据科学家必须始终注意这些问题。
  • 我们如何转换数据以进行有效分析?

    • 数据并不总是一眼就容易解释的,因此数据科学家应该努力揭示隐藏的见解。

关键程序:探索性数据分析数据可视化

understanding_data

1.1.4 理解世界

在观察了数据中的模式之后,我们可以开始回答我们的问题。这可能需要我们预测一个数量(机器学习),或者衡量某种处理的效果(推断)。

从这里,我们可以选择报告我们的结果,或者可能进行更多的分析。我们可能对我们的发现不满意,或者我们的初步探索可能提出了需要新数据的新问题。

  • 数据对世界有何影响?

    • 根据我们的模型,数据将引导我们对真实世界的某些结论。
  • 它是否回答了我们的问题或准确解决了问题?

    • 如果我们的模型和数据不能实现我们的目标,那么我们必须改革我们的问题、模型,或者两者兼而有之。
  • 我们的结论有多可靠,我们能相信这些预测吗?

    • 不准确的模型可能导致错误的结论。

关键程序:模型创建预测推断

understand_world

1.2 结论

数据科学生命周期旨在成为一组一般性指导方针,而不是一套硬性要求。在探索生命周期的过程中,我们将涵盖数据科学中使用的基本理论和技术。在课程结束时,我们希望您开始把自己看作是一名数据科学家。

因此,我们将首先介绍探索性数据分析中最重要的工具之一:pandas

二、Pandas I

原文:Pandas I

译者:飞龙

协议:CC BY-NC-SA 4.0

学习成果

  • 建立对pandaspandas语法的熟悉度。

  • 学习关键数据结构:DataFrameSeriesIndex

  • 了解提取数据的方法:.loc.iloc[]

在这一系列讲座中,我们将让您直接探索和操纵真实世界的数据。我们将首先介绍pandas,这是一个流行的 Python 库,用于与表格数据交互。

2.1 表格数据

数据科学家使用各种格式存储的数据。本课程的主要重点是理解表格数据——存储在表格中的数据。

表格数据是数据科学家用来组织数据的最常见系统之一。这在很大程度上是因为表格的简单性和灵活性。表格允许我们将每个观察,或者从个体收集数据的实例,表示为其自己的。我们可以将每个观察的不同特征,或者特征,记录在单独的中。

为了看到这一点,我们将探索elections数据集,该数据集存储了以前年份竞选美国总统的政治候选人的信息。

代码

import pandas as pd
pd.read_csv("data/elections.csv")
YearCandidatePartyPopular voteResult%
01824Andrew JacksonDemocratic-Republican151271loss57.210122
11824John Quincy AdamsDemocratic-Republican113142win42.789878
21828Andrew JacksonDemocratic642806win56.203927
31828John Quincy AdamsNational Republican500897loss43.796073
41832Andrew JacksonDemocratic702735win54.574789
1772016Jill SteinGreen1457226loss1.073699
1782020Joseph BidenDemocratic81268924win51.311515
1792020Donald TrumpRepublican74216154loss
1802020Jo JorgensenLibertarian1865724loss1.177979
1812020Howard HawkinsGreen405035loss0.255731

182 行×6 列

elections数据集中,每一行代表一个候选人在特定年份竞选总统的一个实例。例如,第一行代表安德鲁·杰克逊在 1824 年竞选总统。每一列代表每个总统候选人的一个特征信息。例如,名为“结果”的列存储候选人是否赢得选举。

你在 Data 8 中的工作帮助你非常熟悉使用和解释以表格格式存储的数据。那时,你使用了datascience库的Table类,这是专门为 Data 8 学生创建的特殊编程库。

在 Data 100 中,我们将使用编程库pandas,这在数据科学界被普遍接受为操纵表格数据的行业和学术标准工具(也是我们熊猫吉祥物 Petey 的灵感来源)。

使用pandas,我们可以

  • 以表格格式排列数据。

  • 提取由特定条件过滤的有用信息。

  • 对数据进行操作以获得新的见解。

  • NumPy函数应用于我们的数据(我们来自 Data 8 的朋友)。

  • 执行矢量化计算以加快我们的分析速度(实验室 1)。

2.2 SeriesDataFrame和索引

要开始我们在pandas中的工作,我们必须首先将库导入到我们的 Python 环境中。这将允许我们在我们的代码中使用pandas数据结构和方法。

# `pd` is the conventional alias for Pandas, as `np` is for NumPy
import pandas as pd

pandas中有三种基本数据结构:

  1. Series:1D 带标签的数组数据;最好将其视为列数据。

  2. DataFrame:带有行和列的 2D 表格数据。

  3. 索引:一系列行/列标签。

DataFrameSeries和索引可以在以下图表中以可视化方式表示,该图表考虑了elections数据集的前几行。

注意DataFrame是一个二维对象——它包含行和列。上面的Series是这个DataFrame的一个单独的列,即Result列。两者都包含一个索引,或者共享的行标签列表(从 0 到 4 的整数,包括 0)。

2.2.1 系列

Series 表示DataFrame的一列;更一般地,它可以是任何 1 维类似数组的对象。它包含:

  • 相同类型的序列。

  • 索引称为数据标签的序列。

在下面的单元格中,我们创建了一个名为sSeries

s = pd.Series(["welcome", "to", "data 100"])
s
0     welcome
1          to
2    data 100
dtype: object
s.values # Data values contained within the Series
array(['welcome', 'to', 'data 100'], dtype=object)
s.index # The Index of the Series
RangeIndex(start=0, stop=3, step=1)

默认情况下,Series 的索引是从 0 开始的整数的顺序列表。可以将所需索引的手动指定列表传递给index参数。

s = pd.Series([-1, 10, 2], index = ["a", "b", "c"])
s
a    -1
b    10
c     2
dtype: int64
s.index
Index(['a', 'b', 'c'], dtype='object')

初始化后也可以更改索引。

s.index = ["first", "second", "third"]
s
first     -1
second    10
third      2
dtype: int64
s.index
Index(['first', 'second', 'third'], dtype='object')
2.2.1.1 Series中的选择

就像在使用NumPy数组时一样,我们可以从Series中选择单个值或一组值。为此,有三种主要方法:

  1. 单个标签。

  2. 标签列表。

  3. 过滤条件。

为了证明这一点,让我们定义ser系列。

ser = pd.Series([4, -2, 0, 6], index = ["a", "b", "c", "d"])
ser
a    4
b   -2
c    0
d    6
dtype: int64
2.2.1.1.1 单个标签
ser["a"] # We return the value stored at the Index label "a"
4
2.2.1.1.2 标签列表
ser[["a", "c"]] # We return a *Series* of the values stored at the Index labels "a" and "c"
a    4
c    0
dtype: int64
2.2.1.1.3 过滤条件

也许从Series中选择数据的最有趣(和有用)的方法是使用过滤条件。

首先,我们对Series应用布尔运算。这将创建一个新的布尔值系列

ser > 0 # Filter condition: select all elements greater than 0
a     True
b    False
c    False
d     True
dtype: bool

然后,我们使用这个布尔条件来索引我们原始的Seriespandas将只选择原始Series中满足条件的条目。

ser[ser > 0] 
a    4
d    6
dtype: int64

2.2.2 数据框

通常,我们将使用Series的角度来处理它们,认为它们是DataFrame中的列。我们可以将DataFrame视为所有共享相同索引Series的集合。

在 Data 8 中,您遇到了datascience库的Table类,它表示表格数据。在 Data 100 中,我们将使用pandas库的DataFrame类。

2.2.2.1 创建DataFrame

有许多创建DataFrame的方法。在这里,我们将介绍最流行的方法:

  1. 从 CSV 文件中。

  2. 使用列名和列表。

  3. 从字典中。

  4. Series中。

更一般地,创建DataFrame的语法是:pandas.DataFrame(data, index, columns)

2.2.2.1.1 从 CSV 文件中

在 Data 100 中,我们的数据通常以 CSV(逗号分隔值)文件格式存储。我们可以通过将数据路径作为参数传递给以下pandas函数来将 CSV 文件导入DataFrame

pd.read_csv("filename.csv")

现在,我们可以认识到pandas DataFrame 表示的是elections数据集。

elections = pd.read_csv("data/elections.csv")
elections
YearCandidatePartyPopular voteResult%
01824Andrew JacksonDemocratic-Republican151271loss57.210122
11824John Quincy AdamsDemocratic-Republican113142win42.789878
21828Andrew JacksonDemocratic642806win56.203927
31828John Quincy AdamsNational Republican500897loss43.796073
41832Andrew JacksonDemocratic702735win54.574789
1772016Jill SteinGreen1457226loss1.073699
1782020Joseph BidenDemocratic81268924win51.311515
1792020Donald TrumpRepublican74216154loss46.858542
1802020Jo JorgensenLibertarian1865724loss1.177979
1812020Howard HawkinsGreen405035loss0.255731

182 行×6 列

这段代码将我们的“DataFrame”对象存储在“选举”变量中。经过检查,我们的“选举”DataFrame 有 182 行和 6 列(“年份”,“候选人”,“党派”,“普选票”,“结果”,“%”)。每一行代表一条记录——在我们的例子中,是某一年的总统候选人。每一列代表记录的一个属性或特征。

2.2.2.1.2 使用列表和列名

我们现在将探讨如何使用我们自己的数据创建“DataFrame”。

考虑以下例子。第一个代码单元创建了一个只有一个列“Numbers”的“DataFrame”。第二个创建了一个有“Numbers”和“Description”两列的“DataFrame”。请注意,需要一个二维值列表来初始化第二个“DataFrame”——每个嵌套列表代表一行数据。

df_list = pd.DataFrame([1, 2, 3], columns=["Numbers"])
df_list
Numbers
01
12
23
df_list = pd.DataFrame([[1, "one"], [2, "two"]], columns = ["Number", "Description"])
df_list
NumbersDescription
01one
12two
2.2.2.1.3 从字典

第三种(更常见的)创建“DataFrame”的方法是使用字典。字典的键代表列名,字典的值代表列的值。

以下是实现这种方法的两种方式。第一种是基于指定“DataFrame”的列,而第二种是基于指定“DataFrame”的行。

df_dict = pd.DataFrame({"Fruit": ["Strawberry", "Orange"], "Price": [5.49, 3.99]})
df_dict
FruitPrice
0Strawberry5.49
1Orange3.99
df_dict = pd.DataFrame([{"Fruit":"Strawberry", "Price":5.49}, {"Fruit": "Orange", "Price":3.99}])
df_dict
FruitPrice
0Strawberry5.49
1Orange3.99
2.2.2.1.4 从“Series”

早些时候,我们解释了“Series”与“DataFrame”中的列是同义词。因此,“DataFrame”相当于共享相同索引的“Series”集合。

事实上,我们可以通过合并两个或更多的“Series”来初始化“DataFrame”。

# Notice how our indices, or row labels, are the same

s_a = pd.Series(["a1", "a2", "a3"], index = ["r1", "r2", "r3"])
s_b = pd.Series(["b1", "b2", "b3"], index = ["r1", "r2", "r3"])

pd.DataFrame({"A-column": s_a, "B-column": s_b})
A-columnB-column
r1a1b1
r2a2b2
r3a3b3
pd.DataFrame(s_a)
0
r1a1
r2a2
r3a3
s_a.to_frame()
0
r1a1
r2a2
r3a3

2.2.3 索引

在技术上,索引不一定是整数,也不一定是唯一的。例如,我们可以将“选举”DataFrame 的索引设置为总统候选人的名字。

# Creating a DataFrame from a CSV file and specifying the Index column
elections = pd.read_csv("data/elections.csv", index_col = "Candidate")
elections
YearPartyPopular voteResult%
Candidate
Andrew Jackson1824Democratic-Republican151271loss57.210122
John Quincy Adams1824Democratic-Republican113142win42.789878
Andrew Jackson1828Democratic642806win56.203927
John Quincy Adams1828National Republican500897loss43.796073
Andrew Jackson1832Democratic702735win54.574789
Jill Stein2016Green1457226loss1.073699
Joseph Biden2020Democratic81268924win51.311515
Donald Trump2020Republican74216154loss46.858542
Jo Jorgensen2020Libertarian1865724loss1.177979
Howard Hawkins2020Green405035loss0.255731

182 行×5 列

我们还可以选择一个新的列,并将其设置为 DataFrame 的索引。例如,我们可以将“选举”DataFrame 的索引设置为候选人的党派。

elections.reset_index(inplace = True) # Resetting the index so we can set the Index again
# This sets the index to the "Party" column
elections.set_index("Party")
CandidateYearPopular voteResult%
Party
Democratic-RepublicanAndrew Jackson1824151271loss57.210122
Democratic-RepublicanJohn Quincy Adams1824113142win42.789878
DemocraticAndrew Jackson1828642806win56.203927
National RepublicanJohn Quincy Adams1828500897loss43.796073
DemocraticAndrew Jackson1832702735win54.574789
GreenJill Stein20161457226loss1.073699
DemocraticJoseph Biden202081268924win51.311515
RepublicanDonald Trump202074216154loss
LibertarianJo Jorgensen20201865724loss1.177979
GreenHoward Hawkins2020405035loss0.255731

182 行×5 列

如果需要,我们可以将索引恢复为默认的整数列表。

# This resets the index to be the default list of integer
elections.reset_index(inplace=True) 
elections.index
RangeIndex(start=0, stop=182, step=1)

还需要注意的是,构成索引的行标签不一定是唯一的。虽然索引值可以是唯一的和数字的,充当行号,但它们也可以是命名的和非唯一的。

这里我们看到唯一和数字的索引值。

然而,这里的索引值是非唯一的。

2.3 DataFrame属性:索引、列和形状

另一方面,DataFrame中的列名几乎总是唯一的。回顾elections数据集,有两列命名为“Candidate”是没有意义的。

有时,您可能希望提取这些不同的值,特别是行和列标签的列表。

对于索引/行标签,请使用DataFrame.index

elections.set_index("Party", inplace = True)
elections.index
Index(['Democratic-Republican', 'Democratic-Republican', 'Democratic',
       'National Republican', 'Democratic', 'National Republican',
       'Anti-Masonic', 'Whig', 'Democratic', 'Whig',
       ...
       'Constitution', 'Republican', 'Independent', 'Libertarian',
       'Democratic', 'Green', 'Democratic', 'Republican', 'Libertarian',
       'Green'],
      dtype='object', name='Party', length=182)

对于列标签,请使用DataFrame.columns

elections.columns
Index(['index', 'Candidate', 'Year', 'Popular vote', 'Result', '%'], dtype='object')

对于 DataFrame 的形状,我们可以使用DataFrame.shape

elections.shape
(182, 6)

2.4 DataFrame中的切片

现在我们已经更多地了解了DataFrame,让我们深入了解它们的功能。

DataFrame类的 API(应用程序编程接口)是庞大的。在本节中,我们将讨论DataFrame API 的几种方法,这些方法允许我们提取数据子集。

操作DataFrame最简单的方法是提取行和列的子集,称为切片

我们可能希望提取数据的常见方式包括:

  • DataFrame中的第一行或最后一行。

  • 具有特定标签的数据。

  • 特定位置的数据。

我们将使用 DataFrame 类的四种主要方法:

  1. .head.tail

  2. .loc

  3. .iloc

  4. []

2.4.1 使用.head.tail提取数据

我们希望提取数据的最简单的情况是当我们只想选择DataFrame的前几行或最后几行时。

要提取 DataFrame df的前n行,我们使用语法df.head(n)

elections = pd.read_csv("data/elections.csv")

# Extract the first 5 rows of the DataFrame
elections.head(5)
YearCandidatePartyPopular voteResult%
01824Andrew JacksonDemocratic-Republican151271loss57.210122
11824John Quincy AdamsDemocratic-Republican113142win42.789878
21828Andrew JacksonDemocratic642806win56.203927
31828John Quincy AdamsNational Republican500897loss43.796073
41832Andrew JacksonDemocratic702735win54.574789

类似地,调用df.tail(n)允许我们提取 DataFrame 的最后n行。

# Extract the last 5 rows of the DataFrame
elections.tail(5)
YearCandidatePartyPopular voteResult%
1772016Jill SteinGreen1457226loss1.073699
1782020Joseph BidenDemocratic81268924win51.311515
1792020Donald TrumpRepublican74216154loss
1802020Jo JorgensenLibertarian1865724loss1.177979
1812020Howard HawkinsGreen405035loss0.255731

2.4.2 基于标签的提取:使用.loc进行索引

对于使用特定列或索引标签提取数据的更复杂任务,我们可以使用.loc.loc访问器允许我们指定我们希望提取的行和列的标签标签(通常称为索引)是 DataFrame 最左边的粗体文本,而列标签是 DataFrame 顶部的列名。

使用.loc获取数据时,我们必须指定数据所在的行和列标签。行标签是.loc函数的第一个参数;列标签是第二个参数。

.loc的参数可以是:

  • 一个单一的值。

  • 一个切片。

  • 一个列表。

例如,要选择单个值,我们可以从elections DataFrame中选择标记为0的行和标记为Candidate的列。

elections.loc[0, 'Candidate']
'Andrew Jackson'

请记住,只传入一个参数作为单个值将产生一个Series。下面,我们提取了"Popular vote"列的子集作为Series

elections.loc[[87, 25, 179], "Popular vote"]
87     15761254
25       848019
179    74216154
Name: Popular vote, dtype: int64

要选择多个行和列,我们可以使用 Python 切片表示法。在这里,我们选择从标签03的行和从标签"Year""Popular vote"的列。

elections.loc[0:3, 'Year':'Popular vote']
YearCandidatePartyPopular vote
01824Andrew JacksonDemocratic-Republican151271
11824John Quincy AdamsDemocratic-Republican113142
21828Andrew JacksonDemocratic642806
31828John Quincy AdamsNational Republican500897

假设相反,我们想要提取elections DataFrame 中前四行的所有列值。这时,缩写:就很有用。

elections.loc[0:3, :]
YearCandidatePartyPopular voteResult%
01824Andrew JacksonDemocratic-Republican151271loss57.210122
11824John Quincy AdamsDemocratic-Republican113142win42.789878
21828Andrew JacksonDemocratic642806win56.203927
31828John Quincy AdamsNational Republican500897loss43.796073

我们可以使用相同的缩写来提取所有行。

elections.loc[:, ["Year", "Candidate", "Result"]]
YearCandidateResult
01824Andrew Jacksonloss
11824John Quincy Adamswin
21828Andrew Jacksonwin
31828John Quincy Adamsloss
41832Andrew Jacksonwin
1772016Jill Steinloss
1782020Joseph Bidenwin
1792020Donald Trump
1802020Jo Jorgensenloss
1812020Howard Hawkinsloss

182 行×3 列

有几件事情我们应该注意。首先,与传统的 Python 不同,pandas允许我们切片字符串值(在我们的例子中,是列标签)。其次,使用.loc进行切片是包含的。请注意,我们的结果DataFrame包括我们指定的切片标签之间和包括这些标签的每一行和列。

同样,我们可以使用列表在elections DataFrame 中获取多行和多列。

elections.loc[[0, 1, 2, 3], ['Year', 'Candidate', 'Party', 'Popular vote']]
YearCandidatePartyPopular vote
01824Andrew JacksonDemocratic-Republican151271
11824John Quincy AdamsDemocratic-Republican113142
21828Andrew JacksonDemocratic642806
31828John Quincy AdamsNational Republican500897

最后,我们可以互换列表和切片表示法。

elections.loc[[0, 1, 2, 3], :]
YearCandidatePartyPopular voteResult%
01824Andrew JacksonDemocratic-Republican151271loss57.210122
11824John Quincy AdamsDemocratic-Republican113142win42.789878
21828Andrew JacksonDemocratic642806win56.203927
31828John Quincy AdamsNational Republican500897loss43.796073

2.4.3 基于整数的提取:使用.iloc进行索引

使用.iloc进行切片与.loc类似。但是,.iloc使用的是行和列的索引位置,而不是标签(想一想:loc 使用labels;iloc 使用indices)。.iloc函数的参数也类似地行为 —— 允许单个值、列表、索引和这些的任意组合。

让我们开始重现上面的结果。我们将从我们的elections DataFrame 中选择第一个总统候选人开始:

# elections.loc[0, "Candidate"] - Previous approach
elections.iloc[0, 1]
'Andrew Jackson'

请注意,.loc.iloc的第一个参数是相同的。这是因为标签为 0 的行恰好在elections DataFrame 的 0 t h 0^{th} 0th(或者说第一个位置)上。通常情况下,任何 DataFrame 中的行标签都是从 0 开始递增的,这一点是正确的。

并且,就像以前一样,如果我们只传入一个单一的值参数,我们的结果将是一个Series

elections.iloc[[1,2,3],1]
1    John Quincy Adams
2       Andrew Jackson
3    John Quincy Adams
Name: Candidate, dtype: object

然而,当我们使用.iloc选择前四行和列时,我们注意到了一些东西。

# elections.loc[0:3, 'Year':'Popular vote'] - Previous approach
elections.iloc[0:4, 0:4]
YearCandidatePartyPopular vote
01824Andrew JacksonDemocratic-Republican151271
11824John Quincy AdamsDemocratic-Republican113142
21828Andrew JacksonDemocratic642806
31828John Quincy AdamsNational Republican500897

切片在.iloc中不再是包容的——它是排他的。换句话说,使用.iloc时,切片的右端点不包括在内。这是pandas语法的微妙之处之一;通过练习你会习惯的。

列表行为与预期的一样。

#elections.loc[[0, 1, 2, 3], ['Year', 'Candidate', 'Party', 'Popular vote']] - Previous Approach
elections.iloc[[0, 1, 2, 3], [0, 1, 2, 3]]
YearCandidatePartyPopular vote
01824Andrew JacksonDemocratic-Republican151271
11824John Quincy AdamsDemocratic-Republican113142
21828Andrew JacksonDemocratic642806
31828John Quincy AdamsNational Republican500897

就像使用.loc一样,我们可以使用冒号与.iloc一起提取所有行或列。

elections.iloc[:, 0:3]
YearCandidateParty
01824Andrew JacksonDemocratic-Republican
11824John Quincy AdamsDemocratic-Republican
21828Andrew JacksonDemocratic
31828John Quincy AdamsNational Republican
41832Andrew JacksonDemocratic
1772016Jill SteinGreen
1782020Joseph BidenDemocratic
1792020Donald TrumpRepublican
1802020Jo JorgensenLibertarian
1812020Howard HawkinsGreen

182 行×3 列

这个讨论引出了一个问题:我们什么时候应该使用.loc.iloc?在大多数情况下,.loc通常更安全。你可以想象,当应用于数据集的顺序可能会改变时,.iloc可能会返回不正确的值。然而,.iloc仍然是有用的——例如,如果你正在查看一个排序好的电影收入的DataFrame,并且想要得到给定年份的收入中位数,你可以使用.iloc来索引到中间。

总的来说,重要的是要记住:

  • .loc执行label-based 提取。

  • .iloc执行integer-based 提取。

2.4.4 上下文相关的提取:使用[]进行索引

[]选择运算符是最令人困惑的,但也是最常用的。它只接受一个参数,可以是以下之一:

  1. 一系列行号。

  2. 一系列列标签。

  3. 单列标签。

也就是说,[]上下文相关的。让我们看一些例子。

2.4.4.1 一系列行号

假设我们想要我们的elections DataFrame 的前四行。

elections[0:4]
YearCandidatePartyPopular voteResult%
01824Andrew JacksonDemocratic-Republican151271loss57.210122
11824John Quincy AdamsDemocratic-Republican113142win42.789878
21828Andrew JacksonDemocratic642806win56.203927
31828John Quincy AdamsNational Republican500897loss43.796073
2.4.4.2 一系列列标签

假设我们现在想要前四列。

elections[["Year", "Candidate", "Party", "Popular vote"]]
YearCandidatePartyPopular vote
01824Andrew JacksonDemocratic-Republican151271
11824John Quincy AdamsDemocratic-Republican113142
21828Andrew JacksonDemocratic642806
31828John Quincy AdamsNational Republican500897
41832Andrew JacksonDemocratic702735
1772016Jill SteinGreen1457226
1782020Joseph BidenDemocratic81268924
1792020Donald TrumpRepublican74216154
1802020Jo JorgensenLibertarian1865724
1812020Howard HawkinsGreen405035

182 行×4 列

2.4.4.3 单列标签

最后,[]允许我们仅提取Candidate列。

elections["Candidate"]
0         Andrew Jackson
1      John Quincy Adams
2         Andrew Jackson
3      John Quincy Adams
4         Andrew Jackson
             ...        
177           Jill Stein
178         Joseph Biden
179         Donald Trump
180         Jo Jorgensen
181       Howard Hawkins
Name: Candidate, Length: 182, dtype: object

输出是一个Series!在本课程中,我们将非常熟悉[],特别是用于选择列。在实践中,[].loc更常见,特别是因为它更加简洁。

2.5 结语

pandas库非常庞大,包含许多有用的函数。这是一个指向文档的链接。我们当然不指望您记住库中的每一个方法。

入门级的 Data 100 pandas 讲座将提供对关键数据结构和方法的高层次视图,这些将构成您pandas知识的基础。本课程的目标是帮助您建立对真实世界编程实践的熟悉度……谷歌搜索!您的问题的答案可以在文档、Stack Overflow 等地方找到。能够搜索、阅读和实施文档是任何数据科学家的重要生活技能。

有了这个,我们将继续学习 Pandas II。

三、Pandas II

原文:Pandas II

译者:飞龙

协议:CC BY-NC-SA 4.0

学习成果

  • 继续熟悉pandas语法。

  • 使用条件选择从DataFrame中提取数据。

  • 识别聚合有用的情况,并确定执行聚合的正确技术。

上次,我们介绍了pandas库作为处理数据的工具包。我们学习了DataFrameSeries数据结构,熟悉了操作表格数据的基本语法,并开始编写我们的第一行pandas代码。

在本讲座中,我们将开始深入了解一些高级的pandas语法。当我们逐步学习这些新的代码片段时,您可能会发现跟着自己的笔记本会很有帮助。

我们将开始加载babynames数据集。

代码

# This code pulls census data and loads it into a DataFrame
# We won't cover it explicitly in this class, but you are welcome to explore it on your own
import pandas as pd
import numpy as np
import urllib.request
import os.path
import zipfile

data_url = "https://www.ssa.gov/oact/babynames/state/namesbystate.zip"
local_filename = "data/babynamesbystate.zip"
if not os.path.exists(local_filename): # If the data exists don't download again
 with urllib.request.urlopen(data_url) as resp, open(local_filename, 'wb') as f:
 f.write(resp.read())

zf = zipfile.ZipFile(local_filename, 'r')

ca_name = 'STATE.CA.TXT'
field_names = ['State', 'Sex', 'Year', 'Name', 'Count']
with zf.open(ca_name) as fh:
 babynames = pd.read_csv(fh, header=None, names=field_names)

babynames.head()
StateSexYearNameCount
0CAF1910Mary295
1CAF1910Helen239
2CAF1910Dorothy220
3CAF1910Margaret163
4CAF1910Frances134

3.1 条件选择

条件选择允许我们选择满足某些指定条件的DataFrame中的行的子集。

要了解如何使用条件选择,我们必须看一下.loc[]方法的另一个可能的输入 - 布尔数组,它只是一个数组或Series,其中每个元素都是TrueFalse。这个布尔数组的长度必须等于DataFrame中的行数。它将返回数组中对应True值的所有行。我们在上一堂课中从Series中执行条件提取时使用了非常类似的技术。

为了看到这一点,让我们选择我们DataFrame的前 10 行中的所有偶数索引行。

# Ask yourself: why is :9 is the correct slice to select the first 10 rows?
babynames_first_10_rows = babynames.loc[:9, :]

# Notice how we have exactly 10 elements in our boolean array argument
babynames_first_10_rows[[True, False, True, False, True, False, True, False, True, False]]
StateSexYearNameCount
0CAF1910Mary295
2CAF1910Dorothy220
4CAF1910Frances134
6CAF1910Evelyn126
8CAF1910Virginia101

我们可以使用.loc执行类似的操作。

babynames_first_10_rows.loc[[True, False, True, False, True, False, True, False, True, False], :]
StateSexYearNameCount
0CAF1910Mary295
2CAF1910Dorothy220
4CAF1910Frances134
6CAF1910Evelyn126
8CAF1910Virginia101

这些技术在这个例子中运行良好,但是你可以想象在更大的DataFrame中为每一行列出TrueFalse可能会有多么乏味。为了简化事情,我们可以提供一个逻辑条件作为.loc[]的输入,返回一个具有必要长度的布尔数组。

例如,要返回与F性别相关的所有名称:

# First, use a logical condition to generate a boolean array
logical_operator = (babynames["Sex"] == "F")

# Then, use this boolean array to filter the DataFrame
babynames[logical_operator].head()
StateSexYearNameCount
0CAF1910Mary295
1CAF1910Helen239
2CAF1910Dorothy220
3CAF1910Margaret163
4CAF1910Frances134

从上一讲中回忆,.head()将只返回DataFrame中的前几行。实际上,babynames[logical operator]包含与原始babynames DataFrame中性别为"F"的条目一样多的行。

在这里,logical_operator评估为长度为 407428 的布尔值Series

代码

print("There are a total of {} values in 'logical_operator'".format(len(logical_operator)))
There are a total of 407428 values in 'logical_operator'

从第 0 行开始到第 239536 行的行评估为True,因此在DataFrame中返回。从第 239537 行开始的行评估为False,因此在输出中被省略。

代码

print("The 0th item in this 'logical_operator' is: {}".format(logical_operator.iloc[0]))
print("The 239536th item in this 'logical_operator' is: {}".format(logical_operator.iloc[239536]))
print("The 239537th item in this 'logical_operator' is: {}".format(logical_operator.iloc[239537]))
The 0th item in this 'logical_operator' is: True
The 239536th item in this 'logical_operator' is: True
The 239537th item in this 'logical_operator' is: False

Series作为babynames[]的参数传递与使用布尔数组具有相同的效果。实际上,[]选择运算符可以将布尔Series、数组和列表作为参数。在整个课程中,这三种方法可以互换使用。

我们也可以使用.loc来实现类似的结果。

babynames.loc[babynames["Sex"] == "F"].head()
StateSexYearNameCount
0CAF1910Mary295
1CAF1910Helen239
2CAF1910Dorothy220
3CAF1910Margaret163
4CAF1910Frances134

布尔条件可以使用各种位运算符进行组合,从而可以根据多个条件过滤结果。在下表中,p 和 q 是布尔数组或Series

符号用法意义
~~p返回 p 的否定
|p | qp 或 q
&p & qp 和 q
^p ^ qp 异或 q(排他或)

当使用逻辑运算符结合多个条件时,我们用一组括号()括起每个单独的条件。这样可以对pandas评估您的逻辑施加操作顺序,并可以避免代码错误。

例如,如果我们想要返回所有性别为“F”,出生在 2000 年之前的名字数据,我们可以写成:

babynames[(babynames["Sex"] == "F") & (babynames["Year"] < 2000)].head()
StateSexYearNameCount
0CAF1910Mary295
1CAF1910Helen239
2CAF1910Dorothy220
3CAF1910Margaret163
4CAF1910Frances134

如果我们想要返回所有性别为“F”或出生在 2000 年之前的所有名字数据,我们可以写成:

babynames[(babynames["Sex"] == "F") | (babynames["Year"] < 2000)].head()
StateSexYearNameCount
0CAF1910Mary295
1CAF1910Helen239
2CAF1910Dorothy220
3CAF1910Margaret163
4CAF1910Frances134

布尔数组选择是一个有用的工具,但对于复杂条件可能导致代码过于冗长。在下面的示例中,我们的布尔条件足够长,以至于需要多行代码来编写。

# Note: The parentheses surrounding the code make it possible to break the code on to multiple lines for readability
(
 babynames[(babynames["Name"] == "Bella") | 
 (babynames["Name"] == "Alex") |
 (babynames["Name"] == "Ani") |
 (babynames["Name"] == "Lisa")]
).head()
StateSexYearNameCount
6289CAF1923Bella5
7512CAF1925Bella8
12368CAF1932Lisa5
14741CAF1936Lisa8
17084CAF1939Lisa5

幸运的是,pandas提供了许多构建布尔过滤器的替代方法。

.isin函数就是一个例子。该方法评估Series中的值是否包含在不同序列(列表、数组或Series)的值中。在下面的单元格中,我们用更简洁的代码实现了与上面的DataFrame等效的结果。

names = ["Bella", "Alex", "Narges", "Lisa"]
babynames["Name"].isin(names).head()
0    False
1    False
2    False
3    False
4    False
Name: Name, dtype: bool
babynames[babynames["Name"].isin(names)].head()
StateSexYearNameCount
6289CAF1923Bella5
7512CAF1925Bella8
12368CAF1932Lisa5
14741CAF1936Lisa8
17084CAF1939Lisa5

函数str.startswith可用于基于Series对象中的字符串值定义过滤器。它检查Series中的字符串值是否以特定字符开头。

# Identify whether names begin with the letter "N"
babynames["Name"].str.startswith("N").head()
0    False
1    False
2    False
3    False
4    False
Name: Name, dtype: bool
# Extracting names that begin with the letter "N"
babynames[babynames["Name"].str.startswith("N")].head()
StateSexYearNameCount
76CAF1910Norma23
83CAF1910Nellie20
127CAF1910Nina11
198CAF1910Nora6
310CAF1911Nellie23

3.2 添加、删除和修改列

在许多数据科学任务中,我们可能需要以某种方式更改DataFrame中包含的列。幸运的是,这样做的语法非常简单。

要向DataFrame添加新列,我们使用的语法与访问现有列时类似。通过写入df["column"]来指定新列的名称,然后将其分配给包含将填充此列的值的Series或数组。

# Create a Series of the length of each name. 
babyname_lengths = babynames["Name"].str.len()

# Add a column named "name_lengths" that includes the length of each name
babynames["name_lengths"] = babyname_lengths
babynames.head(5)
StateSexYearNameCountname_lengths
0CAF1910Mary2954
1CAF1910Helen2395
2CAF1910Dorothy2207
3CAF1910Margaret1638
4CAF1910Frances1347

如果我们需要稍后修改现有列,可以通过再次引用该列的语法df["column"],然后将其重新分配给适当长度的新Series或数组来实现。

# Modify the “name_lengths” column to be one less than its original value
babynames["name_lengths"] = babynames["name_lengths"] - 1
babynames.head()
StateSexYearNameCountname_lengths
0CAF1910Mary2953
1CAF1910Helen2394
2CAF1910Dorothy2206
3CAF1910Margaret1637
4CAF1910Frances1346

我们可以使用.rename()方法重命名列。.rename()接受一个将旧列名映射到新列名的字典。

# Rename “name_lengths” to “Length”
babynames = babynames.rename(columns={"name_lengths":"Length"})
babynames.head()
StateSexYearNameCountLength
0CAF1910Mary2953
1CAF1910Helen2394
2CAF1910Dorothy2206
3CAF1910Margaret1637
4CAF1910Frances1346

如果我们想要删除DataFrame的列或行,我们可以调用.drop方法。使用axis参数来指定是应该删除列还是行。除非另有说明,否则pandas将默认假定我们要删除一行。

# Drop our new "Length" column from the DataFrame
babynames = babynames.drop("Length", axis="columns")
babynames.head(5)
StateSexYearNameCount
0CAF1910Mary295
1CAF1910Helen239
2CAF1910Dorothy220
3CAF1910Margaret163
4CAF1910Frances134

请注意,我们重新分配babynamesbabynames.drop(...)的结果。这是一个微妙但重要的观点:pandas表操作不会发生在原地。调用df.drop(...)将输出一个删除感兴趣的行/列的副本df,而不会修改原始的df表。

换句话说,如果我们简单地调用:

# This creates a copy of `babynames` and removes the column "Name"...
babynames.drop("Name", axis="columns")

# ...but the original `babynames` is unchanged! 
# Notice that the "Name" column is still present
babynames.head(5)
StateSexYearNameCount
0CAF1910Mary295
1CAF1910Helen239
2CAF1910Dorothy220
3CAF1910Margaret163
4CAF1910Frances134

3.3 实用程序函数

pandas包含大量的函数库,可以帮助缩短设置和从其数据结构中获取信息的过程。在接下来的部分中,我们将概述每个主要实用程序函数,这些函数将帮助我们在 Data 100 中使用。

讨论pandas提供的所有功能可能需要一个学期的时间!我们将带领您了解最常用的功能,并鼓励您自行探索和实验。

  • NumPy和内置函数支持

  • .shape

  • .size

  • .describe()

  • .sample()

  • .value_counts()

  • .unique()

  • .sort_values()

pandas 文档将是 Data 100 及以后的宝贵资源。

3.3.1 NumPy

pandas旨在与您在Data 8中遇到的数组计算框架NumPy良好配合。几乎任何NumPy函数都可以应用于pandasDataFrameSeries

# Pull out the number of babies named Yash each year
yash_count = babynames[babynames["Name"] == "Yash"]["Count"]
yash_count.head()
331824     8
334114     9
336390    11
338773    12
341387    10
Name: Count, dtype: int64
# Average number of babies named Yash each year
np.mean(yash_count)
17.142857142857142
# Max number of babies named Yash born in any one year
np.max(yash_count)
29

3.3.2 .shape.size

.shape.sizeSeriesDataFrame的属性,用于测量结构中存储的数据的“数量”。调用.shape返回一个元组,其中包含DataFrameSeries中存在的行数和列数。.size用于找到结构中元素的总数,相当于行数乘以列数。

许多函数严格要求参数沿着某些轴的维度匹配。调用这些维度查找函数比手动计算所有项目要快得多。

# Return the shape of the DataFrame, in the format (num_rows, num_columns)
babynames.shape
(407428, 5)
# Return the size of the DataFrame, equal to num_rows * num_columns
babynames.size
2037140

3.3.3 .describe()

如果需要从DataFrame中获取许多统计信息(最小值,最大值,平均值等),则可以使用.describe() 一次计算所有这些统计信息。

babynames.describe()
YearCount
count407428.000000407428.000000
mean1985.73360979.543456
std27.007660293.698654
min1910.0000005.000000
25%1969.0000007.000000
50%1992.00000013.000000
75%2008.00000038.000000
max2022.0000008260.000000

如果在Series上调用.describe(),将报告一组不同的统计信息。

babynames["Sex"].describe()
count     407428
unique         2
top            F
freq      239537
Name: Sex, dtype: object

3.3.4 .sample()

正如我们将在本学期后面看到的,随机过程是许多数据科学技术的核心(例如,训练-测试拆分,自助法和交叉验证)。.sample() 让我们快速选择随机条目(如果从DataFrame调用,则是一行,如果从Series调用,则是一个值)。

默认情况下,.sample() 选择替换的条目。传入参数 replace=True 以进行替换采样。

# Sample a single row
babynames.sample()
StateSexYearNameCount
119438CAF1991Madaline6

当然,这可以与其他方法和运算符(iloc等)链接在一起。

# Sample 5 random rows, and select all columns after column 2
babynames.sample(5).iloc[:, 2:]
YearNameCount
3602642006Rosalio7
1031041987Paola86
2616801950Perry62
682491973Lilian13
2396521910Eddie5
# Randomly sample 4 names from the year 2000, with replacement, and select all columns after column 2
babynames[babynames["Year"] == 2000].sample(4, replace = True).iloc[:, 2:]
YearNameCount
1508712000Josette12
1512302000Alanah9
3427092000Conner147
1506832000Kaci14

3.3.5 .value_counts()

Series.value_counts() 方法计算Series中每个唯一值的出现次数。换句话说,它计算每个唯一出现的次数。这通常对于确定Series中最常见或最不常见的条目很有用。

在下面的示例中,我们可以通过计算每个名称在babynames"Name"列中出现的次数来确定至少有一个人在该名称下使用了最多年份的名称。请注意,返回值也是一个Series

babynames["Name"].value_counts().head()
Jean         223
Francis      221
Guadalupe    218
Jessie       217
Marion       214
Name: Name, dtype: int64

3.3.6 .unique()

如果我们有一个具有许多重复值的Series,那么.unique() 可以用于仅识别唯一值。在这里,我们返回babynames中所有名称的数组。

babynames["Name"].unique()
array(['Mary', 'Helen', 'Dorothy', ..., 'Zae', 'Zai', 'Zayvier'],
      dtype=object)

3.3.7 .sort_values()

DataFrame进行排序可以用于隔离极端值。例如,按降序排序的行的前 5 个条目(即从最高到最低)是最大的 5 个值。.sort_values 允许我们按指定列对DataFrameSeries进行排序。我们可以选择按升序(默认)或降序的顺序接收行。

# Sort the "Count" column from highest to lowest
babynames.sort_values(by="Count", ascending=False).head()
StateSexYearNameCount
268041CAM1957Michael8260
267017CAM1956Michael8258
317387CAM1990Michael8246
281850CAM1969Michael8245
283146CAM1970Michael8196

与在DataFrame上调用.value_counts()不同,当在Series上调用.value_counts()时,我们不需要显式指定用于排序的列。我们仍然可以指定排序范式 - 即值是按升序还是降序排序。

# Sort the "Name" Series alphabetically
babynames["Name"].sort_values(ascending=True).head()
366001      Aadan
384005      Aadan
369120      Aadan
398211    Aadarsh
370306      Aaden
Name: Name, dtype: object

3.4 自定义排序

现在让我们尝试应用我们刚刚学到的知识来解决一个排序问题,使用不同的方法。假设我们想要找到最长的婴儿名字,并相应地对我们的数据进行排序。

3.4.1 方法 1:创建一个临时列

其中一种方法是首先创建一个包含名字长度的列。

# Create a Series of the length of each name
babyname_lengths = babynames["Name"].str.len()

# Add a column named "name_lengths" that includes the length of each name
babynames["name_lengths"] = babyname_lengths
babynames.head(5)
StateSexYearNameCountname_lengths
0CAF1910Mary2954
1CAF1910Helen2395
2CAF1910Dorothy2207
3CAF1910Margaret1638
4CAF1910Frances1347

然后,我们可以使用.sort_values()按该列对DataFrame进行排序:

# Sort by the temporary column
babynames = babynames.sort_values(by="name_lengths", ascending=False)
babynames.head(5)
StateSexYearNameCountname_lengths
334166CAM1996Franciscojavier815
337301CAM1997Franciscojavier515
339472CAM1998Franciscojavier615
321792CAM1991Ryanchristopher715
327358CAM1993Johnchristopher515

最后,我们可以从babynames中删除name_length列,以防止我们的表变得混乱。

# Drop the 'name_length' column
babynames = babynames.drop("name_lengths", axis='columns')
babynames.head(5)
StateSexYearNameCount
334166CAM1996Franciscojavier8
337301CAM1997Franciscojavier5
339472CAM1998Franciscojavier6
321792CAM1991Ryanchristopher7
327358CAM1993Johnchristopher5

3.4.2 方法 2:使用key参数进行排序

另一种方法是使用.sort_values()key参数。在这里,我们可以指定我们想要按长度对"Name"值进行排序。

babynames.sort_values("Name", key=lambda x: x.str.len(), ascending=False).head()
StateSexYearNameCount
334166CAM1996Franciscojavier8
327472CAM1993Ryanchristopher5
337301CAM1997Franciscojavier5
337477CAM1997Ryanchristopher5
312543CAM1987Franciscojavier5

3.4.3 方法 3:使用map函数进行排序

我们还可以在Series上使用map函数来解决这个问题。假设我们想要按每个“名字”中的“dr”和“ea”的数量对babynames表进行排序。我们将定义函数dr_ea_count来帮助我们。

# First, define a function to count the number of times "dr" or "ea" appear in each name
def dr_ea_count(string):
 return string.count('dr') + string.count('ea')

# Then, use `map` to apply `dr_ea_count` to each name in the "Name" column
babynames["dr_ea_count"] = babynames["Name"].map(dr_ea_count)

# Sort the DataFrame by the new "dr_ea_count" column so we can see our handiwork
babynames = babynames.sort_values(by="dr_ea_count", ascending=False)
babynames.head()
StateSexYearNameCountdr_ea_count
115957CAF1990Deandrea53
101976CAF1986Deandrea63
131029CAF1994Leandrea53
108731CAF1988Deandrea53
308131CAM1985Deandrea63

我们可以在使用完dr_ea_count后删除它,以保持一个整洁的表格。

# Drop the `dr_ea_count` column
babynames = babynames.drop("dr_ea_count", axis = 'columns')
babynames.head(5)
StateSexYearNameCount
115957CAF1990Deandrea5
101976CAF1986Deandrea6
131029CAF1994Leandrea5
108731CAF1988Deandrea5
308131CAM1985Deandrea6

3.5 使用.groupby聚合数据

直到这一点,我们一直在处理DataFrame的单个行。作为数据科学家,我们经常希望调查我们数据的更大子集中的趋势。例如,我们可能希望计算我们DataFrame中一组行的一些摘要统计(均值、中位数、总和等)。为此,我们将使用pandasGroupBy对象。

假设我们想要聚合babynames中给定年份的所有行。

babynames.groupby("Year")
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x117197460>

这个奇怪的输出是什么意思?调用.groupby生成了一个GroupBy对象。你可以把它想象成一组“迷你”子数据框,其中每个子框包含与特定年份对应的babynames的所有行。

下面的图表显示了babynames的简化视图,以帮助说明这个想法。

创建一个 GroupBy 对象

我们不能直接使用GroupBy对象——这就是为什么你之前看到了奇怪的输出,而不是DataFrame的标准视图。要实际操作这些“迷你”DataFrame 中的值,我们需要调用聚合方法。这是一种告诉pandas如何聚合GroupBy对象中的值的方法。一旦应用了聚合,pandas将返回一个正常的(现在是分组的)DataFrame

我们将考虑的第一种聚合方法是.agg.agg方法将函数作为其参数;然后将该函数应用于“迷你”分组的每一列 DataFrame。我们最终得到一个新的DataFrame,每个子框架都有一行聚合。

babynames[["Year", "Count"]].groupby("Year").agg(sum).head(5)
Count
Year
19109163
19119983
191217946
191322094
191426926

我们可以将这一点与我们之前使用的图表联系起来。请记住,图表使用了“babynames”的简化版本,这就是为什么我们看到总计数的值较小。

执行聚合

调用.agg已将每个子框架压缩为单个行。这给了我们最终的输出:一个现在由“Year”索引的DataFrame,原始babynamesDataFrame 中每个唯一年份都有一行。

也许你会想:"State""Sex""Name"列去哪了?从逻辑上讲,对这些列中的字符串数据进行sum是没有意义的(我们怎么将“Mary”+“Ann”相加呢?)。因此,在对DataFrame进行聚合时,我们需要省略这些列。

# Same result, but now we explicitly tell pandas to only consider the "Count" column when summing
babynames.groupby("Year")[["Count"]].agg(sum).head(5)
Count
Year
19109163
19119983
191217946
191322094
191426926

有许多不同的聚合可以应用于分组数据。主要要求是聚合函数必须:

  • 接收一系列数据(分组子框架的单个列)。

  • 返回一个聚合了这个Series的单个值。

由于这个相当广泛的要求,pandas提供了许多计算聚合的方法。

内置的 Python 操作——如summaxmin——会被pandas自动识别。

# What is the minimum count for each name in any year?
babynames.groupby("Name")[["Count"]].agg(min).head()
Count
Name
Aadan5
Aadarsh6
Aaden10
Aadhav6
Aadhini6
# What is the largest single-year count of each name?
babynames.groupby("Name")[["Count"]].agg(max).head()
Count
Name
Aadan7
Aadarsh6
Aaden158
Aadhav8
Aadhini6

如前所述,NumPy库中的函数,如np.meannp.maxnp.minnp.sum,也是pandas中的合理选择。

# What is the average count for each name across all years?
babynames.groupby("Name")[["Count"]].agg(np.mean).head()
Count
Name
Aadan6.000000
Aadarsh6.000000
Aaden46.214286
Aadhav6.750000
Aadhini6.000000

pandas还提供了许多内置函数。pandas本地的函数可以在调用.agg时使用它们的字符串名称进行引用。一些例子包括:

  • .agg("sum")

  • .agg("max")

  • .agg("min")

  • .agg("mean")

  • .agg("first")

  • .agg("last")

列表中的后两个条目——“first”和“last”——是“pandas”独有的。它们返回子框架列中的第一个或最后一个条目。为什么这可能有用呢?考虑一个情况,即组中的多个列共享相同的信息。为了在分组输出中表示这些信息,我们可以简单地获取第一个或最后一个条目,我们知道它将与所有其他条目相同。

让我们举个例子来说明这一点。假设我们向“babynames”添加一个新列,其中包含每个名字的第一个字母。

# Imagine we had an additional column, "First Letter". We'll explain this code next week
babynames["First Letter"] = babynames["Name"].str[0]

# We construct a simplified DataFrame containing just a subset of columns
babynames_new = babynames[["Name", "First Letter", "Year"]]
babynames_new.head()
NameFirst LetterYear
115957DeandreaD1990
101976DeandreaD1986
131029LeandreaL1994
108731DeandreaD1988
308131DeandreaD1985

如果我们为数据集中的每个名称形成分组,“首字母”将对该组的所有成员都相同。这意味着如果我们只是选择组中“首字母”的第一个条目,我们将代表该组中的所有数据。

我们可以使用字典在分组期间对每列应用不同的聚合函数。

使用“first”进行聚合

babynames_new.groupby("Name").agg({"First Letter":"first", "Year":"max"}).head()
First LetterYear
Name
AadanA2014
AadarshA2019
AadenA2020
AadhavA2019
AadhiniA2022

一些聚合函数非常常见,以至于pandas允许直接调用它们,而无需显式使用.agg

babynames.groupby("Name")[["Count"]].mean().head()
Count
Name
Aadan6.000000
Aadarsh6.000000
Aaden46.214286
Aadhav6.750000
Aadhini6.000000

我们还可以定义自己的聚合函数!这可以使用deflambda语句来完成。同样,自定义聚合函数的条件是它必须接受一个Series并输出单个标量值。

babynames = babynames.sort_values(by="Year", ascending=True)
def ratio_to_peak(series):
 return series.iloc[-1]/max(series)

babynames.groupby("Name")[["Year", "Count"]].agg(ratio_to_peak)
YearCount
Name
Aadan1.00.714286
Aadarsh1.01.000000
Aaden1.00.063291
Aadhav1.00.750000
Aadhini1.01.000000
Zymir1.01.000000
Zyon1.01.000000
Zyra1.01.000000
Zyrah1.00.833333
Zyrus1.01.000000

20437 行×2 列

# Alternatively, using lambda
babynames.groupby("Name")[["Year", "Count"]].agg(lambda s: s.iloc[-1]/max(s))
YearCount
Name
Aadan1.00.714286
Aadarsh1.01.000000
Aaden1.00.063291
Aadhav1.00.750000
Aadhini1.01.000000
Zymir1.01.000000
Zyon1.01.000000
Zyra1.01.000000
Zyrah1.00.833333
Zyrus1.01.000000

20437 行×2 列

3.6 结语

操纵DataFrames不是一天就能掌握的技能。由于pandas的灵活性,有许多不同的方法可以从 A 点到 B 点。我们建议尝试多种不同的方法来解决同一个问题,以获得更多的练习并更快地达到精通的水平。

接下来,我们将开始深入挖掘数据分组背后的机制。**

四、Pandas III

原文:Pandas III

译者:飞龙

协议:CC BY-NC-SA 4.0

学习成果

  • 使用.groupby()执行高级聚合

  • 使用pd.pivot_table方法构建一个数据透视表

  • 使用pd.merge()在 DataFrame 之间执行简单的合并

上次,我们介绍了数据聚合的概念 - 我们熟悉了GroupBy对象,并将它们用作汇总和总结 DataFrame 的工具。在本讲座中,我们将探讨使用不同的聚合函数以及深入研究一些高级的.groupby方法,以展示它们在理解我们的数据方面有多么强大。我们还将介绍其他数据聚合技术,以提供在如何操作我们的表格方面的灵活性。

4.1 重新审视.agg()函数

我们将从加载babynames数据集开始。请注意,此数据集已经被过滤,只包含来自加利福尼亚州的数据。

代码

# This code pulls census data and loads it into a DataFrame
# We won't cover it explicitly in this class, but you are welcome to explore it on your own
import pandas as pd
import numpy as np
import urllib.request
import os.path
import zipfile

data_url = "https://www.ssa.gov/oact/babynames/state/namesbystate.zip"
local_filename = "data/babynamesbystate.zip"
if not os.path.exists(local_filename): # If the data exists don't download again
 with urllib.request.urlopen(data_url) as resp, open(local_filename, 'wb') as f:
 f.write(resp.read())

zf = zipfile.ZipFile(local_filename, 'r')

ca_name = 'STATE.CA.TXT'
field_names = ['State', 'Sex', 'Year', 'Name', 'Count']
with zf.open(ca_name) as fh:
 babynames = pd.read_csv(fh, header=None, names=field_names)

babynames.tail(10)
StateSexYearNameCount
407418CAM2022Zach5
407419CAM2022Zadkiel5
407420CAM2022Zae5
407421CAM2022Zai5
407422CAM2022Zay5
407423CAM2022Zayvier5
407424CAM2022Zia5
407425CAM2022Zora5
407426CAM2022Zuriel5
407427CAM2022Zylo5

让我们首先使用.agg来找出每年出生的婴儿总数。回想一下,使用.agg.groupby()的格式是:df.groupby(column_name).agg(aggregation_function)。下面的代码行给出了每年出生的婴儿总数。

babynames.groupby("Year")[["Count"]].agg(sum).head(5)
Count
Year
19109163
19119983
191217946
191322094
191426926

这里有一个过程的示例:

aggregation

现在让我们深入研究groupby。正如我们在上一堂课中学到的,groupby操作涉及将 DataFrame 拆分为分组的子框架,应用函数,并组合结果的某种组合。

对于下面的任意 DataFrame df,代码df.groupby("year").agg(sum)执行以下操作:

  • DataFrame拆分为属于同一年份的子DataFrame

  • sum函数应用到每个子DataFrame的每一列。

  • sum的结果组合成一个由year索引的单个DataFrame

groupby_demo

4.1.1 聚合函数

可以应用许多不同的聚合函数到分组的数据上。.agg()可以接受任何将多个值聚合为一个摘要值的函数。

因为这个相当广泛的要求,pandas提供了许多计算聚合的方法。

pandas会自动识别内置的 Python 操作。例如:

  • .agg(sum)

  • .agg(max)

  • .agg(min)

pandas中也可以使用**NumPy**函数:

  • .agg(np.sum)

  • .agg(np.max)

  • .agg(np.min)

  • .agg("mean")

pandas还提供了许多内置函数,包括:

  • .agg("sum")

  • .agg("max")

  • .agg("min")

  • .agg("mean")

  • .agg("first")

  • .agg("last")

一些常用的聚合函数甚至可以直接调用,而不需要显式使用.agg()。例如,我们可以在.groupby()上调用.mean()

babynames.groupby("Year").mean().head()

现在我们可以将所有这些付诸实践。假设我们想要找出在加利福尼亚州最不受欢迎的“F”性别的婴儿名字。为了计算这个,我们可以首先创建一个指标:“峰值比”(RTP)。RTP 是 2022 年出生具有给定名字的婴儿与任何年份出生具有该名字的最大数量之比。

让我们从计算一个名为“Jennifer”的婴儿开始。

# We filter by babies with sex "F" and sort by "Year"
f_babynames = babynames[babynames["Sex"] == "F"]
f_babynames = f_babynames.sort_values(["Year"])

# Determine how many Jennifers were born in CA per year
jenn_counts_series = f_babynames[f_babynames["Name"] == "Jennifer"]["Count"]

# Determine the max number of Jennifers born in a year and the number born in 2022 
# to calculate RTP
max_jenn = max(f_babynames[f_babynames["Name"] == "Jennifer"]["Count"])
curr_jenn = f_babynames[f_babynames["Name"] == "Jennifer"]["Count"].iloc[-1]
rtp = curr_jenn / max_jenn
rtp
0.018796372629843364

通过创建一个计算 RTP 并将其应用到我们的DataFrame的函数,我们可以一次轻松计算所有名字的 RTP!

def ratio_to_peak(series):
 return series.iloc[-1] / max(series)

#Using .groupby() to apply the function
rtp_table = f_babynames.groupby("Name")[["Year", "Count"]].agg(ratio_to_peak)
rtp_table.head()
YearCount
Name
Aadhini1.01.000000
Aadhira1.00.500000
Aadhya1.00.660000
Aadya1.00.586207
Aahana1.00.269231

在上面显示的行中,我们可以看到每一行都有一个值为1.0

这是你在 Data 8 中看到的逻辑的“pandas-ification”。你在 Data 8 中学到的许多逻辑在 Data 100 中也会对你有所帮助。

4.1.2 烦人的列

请注意,你必须小心选择哪些列应用.agg()函数。如果我们尝试通过f_babynames.groupby("Name").agg(ratio_to_peak)对整个表应用我们的函数,执行.agg()调用将导致TypeError

错误

我们可以通过在调用.agg()之前显式选择要应用聚合函数的列来避免这个问题(并防止无意中丢失数据),

4.1.3 分组后重命名列

默认情况下,.groupby不会重命名任何聚合列。正如我们在上表中看到的,聚合列仍然被命名为Count,即使它现在代表 RTP。为了更好地可读性,我们可以将Count重命名为Count RTP

rtp_table = rtp_table.rename(columns = {"Count": "Count RTP"})
rtp_table
YearCount RTP
Name
Aadhini1.01.000000
Aadhira1.00.500000
Aadhya1.00.660000
Aadya1.00.586207
Aahana1.00.269231
Zyanya1.00.466667
Zyla1.01.000000
Zylah1.01.000000
Zyra1.01.000000
Zyrah1.00.833333

13782 行×2 列

4.1.4 一些数据科学回报

通过对rtp_table进行排序,我们可以看到受欢迎程度下降最多的名字。

rtp_table = rtp_table.rename(columns = {"Count": "Count RTP"})
rtp_table.sort_values("Count RTP").head()
YearCount RTP
Name
Debra1.00.001260
Debbie1.00.002815
Carol1.00.003180
Tammy1.00.003249
Susan1.00.003305

要可视化上述Dataframe,让我们看看下面的折线图:

代码

import plotly.express as px
px.line(f_babynames[f_babynames["Name"] == "Debra"], x = "Year", y = "Count")

我们可以得到前 10 个名字的列表,然后用以下代码绘制受欢迎程度:

top10 = rtp_table.sort_values("Count RTP").head(10).index
px.line(
 f_babynames[f_babynames["Name"].isin(top10)], 
 x = "Year", 
 y = "Count", 
 color = "Name"
)

作为一个快速练习,考虑一下什么样的代码可以计算每个名字的婴儿总数。

代码

babynames.groupby("Name")[["Count"]].agg(sum).head()
# alternative solution: 
# babynames.groupby("Name")[["Count"]].sum()
Count
Name
Aadan18
Aadarsh6
Aaden647
Aadhav27
Aadhini6

现在,让我们考虑计算每年出生的婴儿总数的代码。你会看到有多种方法可以实现这一点,其中一些列在下面列出。

代码

babynames.groupby("Year")[["Count"]].agg(sum).head()
# Alternative 1
# babynames.groupby("Year")[["Count"]].sum()
# Alternative 2
# babynames.groupby("Year").sum(numeric_only=True)
Count
Year
19109163
19119983
191217946
191322094
191426926

对于第二种选择,注意我们如何通过向groupby传递numeric_only=True参数来避免我们之前在聚合非数字列时遇到的错误。

4.1.5 绘制出生计数

绘制Dataframe后,我们得到了一个有趣的故事。

代码

import plotly.express as px
puzzle2 = babynames.groupby("Year")[["Count"]].agg(sum)
px.line(puzzle2, y = "Count")

警告: 当我们决定使用这个数据集来估计出生率时,我们做出了一个巨大的假设。根据来自立法分析办公室的这篇文章,2020 年加利福尼亚州出生的婴儿实际数量为 421,275。然而,我们的图表显示 362,882 个婴儿 - 发生了什么?

4.2 GroupBy(),继续

我们将再次使用elections DataFrame。

代码

import pandas as pd
import numpy as np

elections = pd.read_csv("data/elections.csv")
elections.head(5)
YearCandidatePartyPopular voteResult%
01824Andrew JacksonDemocratic-Republican151271loss57.210122
11824John Quincy AdamsDemocratic-Republican113142win42.789878
21828Andrew JacksonDemocratic642806win56.203927
31828John Quincy AdamsNational Republican500897loss43.796073
41832Andrew JacksonDemocratic702735win54.574789

4.2.1 原始GroupBy对象

应用于DataFramegroupby的结果是一个DataFrameGroupBy对象,而不是一个DataFrame

grouped_by_year = elections.groupby("Year")
type(grouped_by_year)
pandas.core.groupby.generic.DataFrameGroupBy

有几种方法可以查看DataFrameGroupBy对象:

grouped_by_party = elections.groupby("Party")
grouped_by_party.groups
{'American': [22, 126], 'American Independent': [115, 119, 124], 'Anti-Masonic': [6], 'Anti-Monopoly': [38], 'Citizens': [127], 'Communist': [89], 'Constitution': [160, 164, 172], 'Constitutional Union': [24], 'Democratic': [2, 4, 8, 10, 13, 14, 17, 20, 28, 29, 34, 37, 39, 45, 47, 52, 55, 57, 64, 70, 74, 77, 81, 83, 86, 91, 94, 97, 100, 105, 108, 111, 114, 116, 118, 123, 129, 134, 137, 140, 144, 151, 158, 162, 168, 176, 178], 'Democratic-Republican': [0, 1], 'Dixiecrat': [103], 'Farmer–Labor': [78], 'Free Soil': [15, 18], 'Green': [149, 155, 156, 165, 170, 177, 181], 'Greenback': [35], 'Independent': [121, 130, 143, 161, 167, 174], 'Liberal Republican': [31], 'Libertarian': [125, 128, 132, 138, 139, 146, 153, 159, 163, 169, 175, 180], 'National Democratic': [50], 'National Republican': [3, 5], 'National Union': [27], 'Natural Law': [148], 'New Alliance': [136], 'Northern Democratic': [26], 'Populist': [48, 61, 141], 'Progressive': [68, 82, 101, 107], 'Prohibition': [41, 44, 49, 51, 54, 59, 63, 67, 73, 75, 99], 'Reform': [150, 154], 'Republican': [21, 23, 30, 32, 33, 36, 40, 43, 46, 53, 56, 60, 65, 69, 72, 79, 80, 84, 87, 90, 96, 98, 104, 106, 109, 112, 113, 117, 120, 122, 131, 133, 135, 142, 145, 152, 157, 166, 171, 173, 179], 'Socialist': [58, 62, 66, 71, 76, 85, 88, 92, 95, 102], 'Southern Democratic': [25], 'States' Rights': [110], 'Taxpayers': [147], 'Union': [93], 'Union Labor': [42], 'Whig': [7, 9, 11, 12, 16, 19]}
grouped_by_party.get_group("Socialist")
YearCandidatePartyPopular voteResult%
581904Eugene V. DebsSocialist402810loss2.985897
621908Eugene V. DebsSocialist420852loss2.850866
661912Eugene V. DebsSocialist901551loss6.004354
711916Allan L. BensonSocialist590524loss3.194193
761920Eugene V. DebsSocialist913693loss3.428282
851928Norman ThomasSocialist267478loss0.728623
881932Norman ThomasSocialist884885loss2.236211
921936Norman ThomasSocialist187910loss0.412876
951940Norman ThomasSocialist116599loss0.234237
1021948Norman ThomasSocialist139569loss0.286312

4.2.2 其他GroupBy方法

有许多聚合方法可以使用.agg。一些有用的选项是:

  • .mean:创建一个新的DataFrame,其中包含每个组的平均值

  • .sum:创建一个新的DataFrame,其中包含每个组的总和

  • .max.min:创建一个新的DataFrame,其中包含每个组的最大/最小值

  • .first.last:创建一个新的DataFrame,其中包含每个组的第一行/最后一行

  • .size:创建一个新的Series,其中包含每个组的条目数

  • .count:创建一个新的DataFrame,其中包含条目数,不包括缺失值。

让我们通过创建一个名为dfDataFrame来举例说明一些例子。

df = pd.DataFrame({'letter':['A','A','B','C','C','C'], 
 'num':[1,2,3,4,np.NaN,4], 
 'state':[np.NaN, 'tx', 'fl', 'hi', np.NaN, 'ak']})
df
letternumState
0A1.0NaN
1A2.0tx
2B3.0fl
3C4.0hi
4CNaNNaN
5C4.0ak

请注意.size().count()之间的细微差别:虽然.size()返回一个Series并计算包括缺失值在内的条目数,.count()返回一个DataFrame并计算每列中不包括缺失值的条目数。

df.groupby("letter").size()
letter
A    2
B    1
C    3
dtype: int64
df.groupby("letter").count()
numState
letter
A21
B11
C22

您可能还记得前一个笔记中的value_counts()函数做了类似的事情。原来value_counts()groupby.size()是一样的,只是value_counts()会自动按降序排序结果Series

df["letter"].value_counts()
C    3
A    2
B    1
Name: letter, dtype: int64

这些(和其他)聚合函数是如此常见,以至于 pandas 允许使用简写。我们可以直接在 GroupBy 对象上调用函数,而不是明确地声明使用 .agg

例如,以下是等价的:

  • elections.groupby("Candidate").agg(mean)

  • elections.groupby("Candidate").mean()

pandas 还支持许多其他方法。您可以在pandas文档中查看它们。

4.2.3 按组进行过滤

GroupBy 对象的另一个常见用途是按组过滤数据。

groupby.filter 接受一个参数 func,其中 func 是一个函数,它:

  • DataFrame 对象作为输入

  • 返回每个子 DataFrame 的单个 TrueFalse

返回对应于 True 的子 DataFrame,而具有 False 值的则不返回。重要的是,groupby.filtergroupby.agg 不同,因为最终的 DataFrame 中返回的是整个DataFrame,而不仅仅是单行。因此,groupby.filter 保留了原始索引。

groupby_demo

为了说明这是如何发生的,让我们回到 elections 数据集。假设我们想要识别“紧张”的选举年份 - 也就是说,我们想要找到所有对应于那一年的行,其中所有候选人在那一年赢得了相似比例的总票数。具体来说,让我们找到所有对应于没有候选人赢得超过总票数 45%的年份的行。

换句话说,我们想要:

  • 找到最大 % 小于 45% 的年份

  • 返回对应于这些年份的所有 DataFrame

对于每一年,我们需要找到该年所有行中的最大 %。如果这个最大 % 小于 45%,我们将告诉 pandas 保留该年对应的所有行。

elections.groupby("Year").filter(lambda sf: sf["%"].max() < 45).head(9)
YearCandidatePartyPopular voteResult%
231860Abraham LincolnRepublican1855993win39.699408
241860John BellConstitutional Union590901loss12.639283
251860John C. BreckinridgeSouthern Democratic848019loss18.138998
261860Stephen A. DouglasNorthern Democratic1380202loss29.522311
661912Eugene V. DebsSocialist901551loss6.004354
671912Eugene W. ChafinProhibition208156loss1.386325
681912Theodore RooseveltProgressive4122721loss27.457433
691912William TaftRepublican3486242loss23.218466
701912Woodrow WilsonDemocratic6296284win41.933422

这里发生了什么?在这个例子中,我们将我们的过滤函数 func 定义为 lambda sf: sf["%"].max() < 45。这个过滤函数将在分组的子 DataFrame 中的所有条目中找到最大的 "%" 值,我们称之为 sf。如果最大值小于 45,则过滤函数将返回 True,并且该分组的所有行将出现在最终的输出 DataFrame 中。

检查上面的 DataFrame。请注意,在这个前 9 行的预览中,所有来自 1860 年和 1912 年的条目都出现了。这意味着在 1860 年和 1912 年,那一年没有候选人赢得超过总票数 45%。

你可能会问:groupby.filter 过程与我们之前看到的布尔过滤有何不同?布尔过滤在应用布尔条件时考虑单个行。例如,代码 elections[elections["%"] < 45] 将检查 elections 中每一行的 "%" 值;如果小于 45,则该行将保留在输出中。相比之下,groupby.filter 在整个组的所有行上应用布尔条件。如果该组中并非所有行都满足过滤器指定的条件,则整个组将在输出中被丢弃。

4.2.4 使用 lambda 函数进行聚合

如果我们希望使用非标准函数(例如我们自己设计的函数)对我们的DataFrame进行聚合,我们可以通过将.agglambda表达式结合使用来实现。

让我们首先考虑一个谜题来唤起我们的记忆。我们将尝试找到每个Party中获得最高%选票的Candidate

一个天真的方法可能是按Party列分组并按最大值聚合。

elections.groupby("Party").agg(max).head(10)
YearCandidatePopular voteResult%
Party
American1976Thomas J. Anderson873053loss21.554001
American Independent1976Lester Maddox9901118loss13.571218
Anti-Masonic1832William Wirt100715loss7.821583
Anti-Monopoly1884Benjamin Butler134294loss1.335838
Citizens1980Barry Commoner233052loss0.270182
Communist1932William Z. Foster103307loss0.261069
Constitution2016Michael Peroutka203091loss0.152398
Constitutional Union1860John Bell590901loss12.639283
Democratic2020Woodrow Wilson81268924win61.344703
Democratic-Republican1824John Quincy Adams151271win57.210122

这种方法显然是错误的-DataFrame声称伍德罗·威尔逊在 2020 年赢得了总统大选。

为什么会发生这种情况?这里,max聚合函数是独立地应用于每一列。在民主党人中,max正在计算:

  • 民主党候选人竞选总统的最近年份(2020)

  • 具有字母顺序“最大”名称(“伍德罗·威尔逊”)的Candidate

  • 具有字母顺序“最大”结果(“赢”)的Result

相反,让我们尝试一种不同的方法。我们将:

  1. 对数据框进行排序,使行按%的降序排列

  2. Party分组并选择每个子数据框的第一行

虽然这可能看起来不直观,但按%的降序对elections进行排序非常有帮助。然后,如果我们按Party分组,每个 groupby 对象的第一行将包含有关具有最高选民%Candidate的信息。

elections_sorted_by_percent = elections.sort_values("%", ascending=False)
elections_sorted_by_percent.head(5)
YearCandidatePartyPopular voteResult%
1141964Lyndon JohnsonDemocratic43127041win61.344703
911936Franklin RooseveltDemocratic27752648win60.978107
1201972Richard NixonRepublican47168710win60.907806
791920Warren HardingRepublican16144093win60.574501
1331984Ronald ReaganRepublican54455472win59.023326
elections_sorted_by_percent.groupby("Party").agg(lambda x : x.iloc[0]).head(10)

# Equivalent to the below code
# elections_sorted_by_percent.groupby("Party").agg('first').head(10)
YearCandidatePopular voteResult%
Party
American1856Millard Fillmore873053loss21.554001
American Independent1968George Wallace9901118loss13.571218
Anti-Masonic1832William Wirt100715loss7.821583
Anti-Monopoly1884Benjamin Butler134294loss1.335838
Citizens1980Barry Commoner233052loss0.270182
Communist1932William Z. Foster103307loss0.261069
Constitution2008Chuck Baldwin199750loss0.152398
Constitutional Union1860John Bell590901loss12.639283
Democratic1964Lyndon Johnson43127041win61.344703
Democratic-Republican1824Andrew Jackson151271loss57.210122

以下是该过程的示例:

groupby_demo

请注意,我们的代码正确确定了来自民主党的林登·约翰逊拥有最高的选民%

更一般地,lambda函数用于设计 Python 中未预定义的自定义聚合函数。lambda函数的输入参数x是一个GroupBy对象。因此,lambda x : x.iloc[0]选择每个 groupby 对象中的第一行应该是有意义的。

事实上,解决这个问题有几种不同的方法。每种方法在可读性、性能、内存消耗、复杂性等方面都有不同的权衡。我们在下面给出了一些示例。

注意:不需要理解这些替代解决方案。它们是为了展示pandas中众多问题解决方法的多样性。

# Using the idxmax function
best_per_party = elections.loc[elections.groupby('Party')['%'].idxmax()]
best_per_party.head(5)
YearCandidatePartyPopular voteResult%
221856Millard FillmoreAmerican873053loss21.554001
1151968George WallaceAmerican Independent9901118loss13.571218
61832William WirtAnti-Masonic100715loss7.821583
381884Benjamin ButlerAnti-Monopoly134294loss1.335838
1271980Barry CommonerCitizens233052loss0.270182
# Using the .drop_duplicates function
best_per_party2 = elections.sort_values('%').drop_duplicates(['Party'], keep='last')
best_per_party2.head(5)
YearCandidatePartyPopular voteResult%
1481996John HagelinNatural Law113670loss0.118219
1642008Chuck BaldwinConstitution199750loss0.152398
1101956T. Coleman AndrewsStates’ Rights107929loss0.174883
1471996Howard PhillipsTaxpayers184656loss0.192045
1361988Lenora FulaniNew Alliance217221loss0.237804

4.3 使用数据透视表聚合数据

我们现在知道.groupby让我们能够在 DataFrame 中对数据进行分组和聚合。上面的示例使用 DataFrame 中的一列形成了分组。通过传递一个列名的列表给.groupby,可以一次按多列进行分组。

让我们再次考虑babynames数据集。在这个问题中,我们将找到与每个年份和性别相关联的婴儿名字的总数。为此,我们将同时"年份""性别"列进行分组。

babynames.head()
StateSexYearNameCount
0CAF1910Mary295
1CAF1910Helen239
2CAF1910Dorothy220
3CAF1910Margaret163
4CAF1910Frances134
# Find the total number of baby names associated with each sex for each 
# year in the data
babynames.groupby(["Year", "Sex"])[["Count"]].agg(sum).head(6)
Count
YearSex
1910F5950
M3213
1911F6602
M3381
1912F9804
M8142

请注意,"年份"和"性别"都作为 DataFrame 的索引(它们都以粗体呈现)。我们创建了一个多索引DataFrame,其中使用两个不同的索引值,年份和性别,来唯一标识每一行。

这不是表示这些数据的最直观的方式 - 而且,因为多索引的 DataFrame 在其索引中有多个维度,它们通常很难使用。

另一种跨两列进行聚合的策略是创建一个数据透视表。你在Data 8中看到过这些。一组值用于创建数据透视表的索引;另一组用于定义列名。表中每个单元格中包含的值对应于每个索引-列对的聚合数据。

这是一个过程的示例:

groupby_demo

理解数据透视表的最佳方法是看它的实际应用。让我们回到我们最初的目标,即对每个年份和性别组合的名字总数进行求和。我们将调用pandas.pivot_table方法来创建一个新表。

# The `pivot_table` method is used to generate a Pandas pivot table
import numpy as np
babynames.pivot_table(
 index = "Year", 
 columns = "Sex", 
 values = "Count", 
 aggfunc = np.sum,
).head(5)
SexFM
Year
191059503213
191166023381
191298048142
19131186010234
19141381513111

看起来好多了!现在,我们的 DataFrame 结构清晰,具有清晰的索引列组合。数据透视表中的每个条目表示给定“Year”和“Sex”组合的名称总数。

让我们更仔细地看一下上面实施的代码。

  • index = "Year" 指定应用于数据透视表的原始“DataFrame”中用作索引的列名

  • columns = "Sex" 指定应用于生成数据透视表的列的原始“DataFrame”中的列名

  • values = "Count" 指示应用于填充每个索引列组合的条目的原始“DataFrame”中的哪些值

  • aggfunc = np.sum 告诉“pandas”在聚合由“values”指定的数据时使用什么函数。在这里,我们正在对每对“Year”和“Sex”的名称计数求和

我们甚至可以在数据透视表的索引或列中包含多个值。

babynames_pivot = babynames.pivot_table(
 index="Year",     # the rows (turned into index)
 columns="Sex",    # the column values
 values=["Count", "Name"], 
 aggfunc=max,   # group operation
)
babynames_pivot.head(6)
CountName
SexFM
Year
1910295237
1911390214
1912534501
1913584614
1914773769
19159981033

4.4 连接表

在进行数据科学项目时,我们不太可能在单个“DataFrame”中包含我们想要的所有数据-现实世界的数据科学家需要处理来自多个来源的数据。如果我们可以访问具有相关信息的多个数据集,我们可以将两个或多个表连接成一个单独的 DataFrame。

要将其付诸实践,我们将重新审视“elections”数据集。

elections.head(5)
YearCandidatePartyPopular voteResult%
01824Andrew JacksonDemocratic-Republican151271loss57.210122
11824John Quincy AdamsDemocratic-Republican113142win42.789878
21828Andrew JacksonDemocratic642806win56.203927
31828John Quincy AdamsNational Republican500897loss43.796073
41832Andrew JacksonDemocratic702735win54.574789

假设我们想了解 2022 年每位总统候选人的名字的受欢迎程度。为此,我们需要“babynames”和“elections”的合并数据。

我们将首先创建一个新列,其中包含每位总统候选人的名字。这将帮助我们将“elections”中的每个名字与“babynames”中的相应名字数据连接起来。

# This `str` operation splits each candidate's full name at each 
# blank space, then takes just the candidiate's first name
elections["First Name"] = elections["Candidate"].str.split().str[0]
elections.head(5)
YearCandidatePartyPopular voteResult%First Name
01824Andrew JacksonDemocratic-Republican151271loss57.210122Andrew
11824John Quincy AdamsDemocratic-Republican113142win42.789878John
21828Andrew JacksonDemocratic642806win56.203927Andrew
31828John Quincy AdamsNational Republican500897loss43.796073John
41832Andrew JacksonDemocratic702735win54.574789Andrew
# Here, we'll only consider `babynames` data from 2022
babynames_2022 = babynames[babynames["Year"]==2020]
babynames_2022.head()
StateSexYearNameCount
228550CAF2020Olivia2353
228551CAF2020Camila2187
228552CAF2020Emma2110
228553CAF2020Mia2043
228554CAF2020Sophia1999

现在,我们准备好连接这两个表了。pd.merge 是用于将 DataFrame 连接在一起的“pandas”方法。

merged = pd.merge(left = elections, right = babynames_2022, \
 left_on = "First Name", right_on = "Name")
merged.head()
# Notice that pandas automatically specifies `Year_x` and `Year_y` 
# when both merged DataFrames have the same column name to avoid confusion
Year_xCandidatePartyPopular voteResult%First NameStateSexYear_yNameCount
01824Andrew JacksonDemocratic-Republican151271loss57.210122AndrewCAM2020Andrew874
11828Andrew JacksonDemocratic642806win56.203927AndrewCAM2020Andrew874
21832Andrew JacksonDemocratic702735win54.574789AndrewCAM2020Andrew874
31824John Quincy AdamsDemocratic-Republican113142win42.789878JohnCAM2020John623
41828John Quincy AdamsNational Republican500897loss43.796073JohnCAM2020John623

让我们更仔细地看看这些参数:

  • leftright参数用于指定要连接的数据框。

  • left_onright_on参数被分配给要在执行连接时使用的列的字符串名称。这两个on参数告诉pandas应该将哪些值作为配对键来确定要在数据框之间合并的行。我们将在下一堂课上更多地讨论这个配对键的概念。

4.5 结语

恭喜!我们终于解决了pandas。如果你对它仍然感到不太舒服,不要担心——在接下来的几周里,你将有足够的机会练习。

接下来,我们将动手处理一些真实世界的数据集,并利用我们的pandas知识进行一些探索性数据分析。

五、数据清洗和探索性数据分析

原文:Data Cleaning and EDA

译者:飞龙

协议:CC BY-NC-SA 4.0

代码

import numpy as np
import pandas as pd

import matplotlib.pyplot as plt
import seaborn as sns
#%matplotlib inline
plt.rcParams['figure.figsize'] = (12, 9)

sns.set()
sns.set_context('talk')
np.set_printoptions(threshold=20, precision=2, suppress=True)
pd.set_option('display.max_rows', 30)
pd.set_option('display.max_columns', None)
pd.set_option('display.precision', 2)
# This option stops scientific notation for pandas
pd.set_option('display.float_format', '{:.2f}'.format)

# Silence some spurious seaborn warnings
import warnings
warnings.filterwarnings("ignore", category=FutureWarning)

学习成果

  • 识别常见文件格式

  • 按其变量类型对数据进行分类

  • 建立对数据可信度问题的认识,并制定有针对性的解决方案

此内容在第 4、5 和 6 讲中涵盖。

在过去的几堂课上,我们已经学到pandas是一个重塑、修改和探索数据集的工具包。我们还没有涉及的是如何做出这些数据转换决策。当我们从“现实世界”收到一组新数据时,我们如何知道我们应该做什么处理来将这些数据转换为可用的形式?

数据清洗,也称为数据整理,是将原始数据转换为便于后续分析的过程。它通常用于解决诸如:

  • 结构不清晰或格式不正确

  • 缺失或损坏的值

  • 单位转换

  • …等等

**探索性数据分析(EDA)**是了解新数据集的过程。这是一种开放式、非正式的分析,涉及熟悉数据中存在的变量,发现潜在的假设,并识别数据可能存在的问题。这最后一点通常会激发进一步的数据清洗,以解决数据集格式的任何问题;因此,EDA 和数据清洗通常被认为是一个“无限循环”,每个过程都推动着另一个过程。

在本讲座中,我们将考虑在进行数据清洗和 EDA 时要考虑的数据的关键属性。在这个过程中,我们将为您制定一个“清单”,以便在处理新数据集时考虑。通过这个过程,我们将更深入地了解数据科学生命周期的这个早期阶段(但非常重要!)。

5.1 结构

5.1.1 文件格式

有许多用于存储结构化数据的文件类型:TSV、JSON、XML、ASCII、SAS 等。在讲座中,我们只会涵盖 CSV、TSV 和 JSON,但在处理不同数据集时,您可能会遇到其他格式。阅读文档是了解如何处理多种不同文件类型的最佳方法。

5.1.1.1 CSV

CSV,代表逗号分隔值,是一种常见的表格数据格式。在过去的两堂pandas讲座中,我们简要涉及了文件格式的概念:数据在文件中的编码方式。具体来说,我们的electionsbabynames数据集是以 CSV 格式存储和加载的:

pd.read_csv("data/elections.csv").head(5)
YearCandidatePartyPopular voteResult%
01824Andrew JacksonDemocratic-Republican151271loss57.21
11824John Quincy AdamsDemocratic-Republican113142win42.79
21828Andrew JacksonDemocratic642806win56.20
31828John Quincy AdamsNational Republican500897loss43.80
41832Andrew JacksonDemocratic702735win54.57

为了更好地了解 CSV 的属性,让我们来看看原始数据文件的前几行,看看在加载到DataFrame之前它是什么样子的。我们将使用repr()函数返回带有特殊字符的原始字符串:

with open("data/elections.csv", "r") as table:
 i = 0
 for row in table:
 print(repr(row))
 i += 1
 if i > 3:
 break
'Year,Candidate,Party,Popular vote,Result,%\n'
'1824,Andrew Jackson,Democratic-Republican,151271,loss,57.21012204\n'
'1824,John Quincy Adams,Democratic-Republican,113142,win,42.78987796\n'
'1828,Andrew Jackson,Democratic,642806,win,56.20392707\n'

数据中的每一行,或记录,由换行符\n分隔。数据中的每一列,或字段,由逗号,分隔(因此是逗号分隔的!)。

5.1.1.2 TSV

另一种常见的文件类型是TSV(制表符分隔值)。在 TSV 中,记录仍然由换行符\n分隔,而字段由制表符\t分隔。

让我们来看看原始 TSV 文件的前几行。同样,我们将使用repr()函数,以便print显示特殊字符。

with open("data/elections.txt", "r") as table:
 i = 0
 for row in table:
 print(repr(row))
 i += 1
 if i > 3:
 break
'\ufeffYear\tCandidate\tParty\tPopular vote\tResult\t%\n'
'1824\tAndrew Jackson\tDemocratic-Republican\t151271\tloss\t57.21012204\n'
'1824\tJohn Quincy Adams\tDemocratic-Republican\t113142\twin\t42.78987796\n'
'1828\tAndrew Jackson\tDemocratic\t642806\twin\t56.20392707\n'

TSV 可以使用pd.read_csv加载到pandas中。我们需要使用参数sep='\t'来指定分隔符(文档)

pd.read_csv("data/elections.txt", sep='\t').head(3)
YearCandidatePartyPopular voteResult%
01824Andrew JacksonDemocratic-Republican151271loss57.21
11824John Quincy AdamsDemocratic-Republican113142win42.79
21828Andrew JacksonDemocratic642806win56.20

CSV 和 TSV 的问题出现在记录中有逗号或制表符的情况下。pandas如何区分逗号分隔符与字段本身中的逗号,例如8,900?为了解决这个问题,可以查看quotechar参数

5.1.1.3 JSON

**JSON(JavaScript 对象表示)**文件的行为类似于 Python 字典。下面显示了原始 JSON。

with open("data/elections.json", "r") as table:
 i = 0
 for row in table:
 print(row)
 i += 1
 if i > 8:
 break
[

 {

   "Year": 1824,

   "Candidate": "Andrew Jackson",

   "Party": "Democratic-Republican",

   "Popular vote": 151271,

   "Result": "loss",

   "%": 57.21012204

 }, 

可以使用pd.read_json将 JSON 文件加载到pandas中。

pd.read_json('data/elections.json').head(3)
YearCandidatePartyPopular voteResult%
01824Andrew JacksonDemocratic-Republican151271loss57.21
11824John Quincy AdamsDemocratic-Republican113142win42.79
21828Andrew JacksonDemocratic642806win56.20
5.1.1.3.1 使用 JSON 进行 EDA:伯克利 COVID-19 数据

伯克利市政府开放数据网站有一个关于伯克利居民 COVID-19 确诊病例的数据集。让我们下载文件并将其保存为 JSON(请注意,源 URL 文件类型也是 JSON)。为了可重复的数据科学,我们将以程序方式下载数据。我们在ds100_utils.py文件中定义了一些辅助函数,我们可以在许多不同的笔记本中重用这些辅助函数。

from ds100_utils import fetch_and_cache

covid_file = fetch_and_cache(
 "https://data.cityofberkeley.info/api/views/xn6j-b766/rows.json?accessType=DOWNLOAD",
 "confirmed-cases.json",
 force=False)
covid_file          # a file path wrapper object
Using cached version that was downloaded (UTC): Fri Aug 18 22:19:42 2023
PosixPath('data/confirmed-cases.json')
5.1.1.3.1.1 文件大小

让我们通过对数据集的大小进行粗略估计来确定我们用于查看数据的工具。对于相对较小的数据集,我们可以使用文本编辑器或电子表格。对于较大的数据集,更多的编程探索或分布式计算工具可能更合适。在这里,我们将使用Python工具来探查文件。

由于似乎存在文本文件,让我们调查一下行数,这通常对应于记录的数量。

import os

print(covid_file, "is", os.path.getsize(covid_file) / 1e6, "MB")

with open(covid_file, "r") as f:
 print(covid_file, "is", sum(1 for l in f), "lines.")
data/confirmed-cases.json is 0.116367 MB
data/confirmed-cases.json is 1110 lines.
5.1.1.3.1.2 Unix Commands

作为 EDA 工作流的一部分,Unix 命令非常有用。事实上,有一本名为“Data Science at the Command Line”的整本书深入探讨了这个想法!在 Jupyter/IPython 中,您可以使用!前缀执行任意的 Unix 命令,并且在这些行内,您可以使用{expr}语法引用Python变量和表达式。

在这里,我们使用ls命令列出文件,使用-lh标志,请求“以人类可读的形式显示详细信息”。我们还使用wc命令进行“字数统计”,但使用-l标志,该标志请求行数而不是单词数。

这两个代码给出了与上面的代码相同的信息,尽管形式略有不同:

!ls -lh {covid_file}
!wc -l {covid_file}
-rw-r--r--  1 Ishani  staff   114K Aug 18 22:19 data/confirmed-cases.json
 1109 data/confirmed-cases.json
5.1.1.3.1.3 文件内容

让我们使用Python来探索数据格式。

with open(covid_file, "r") as f:
 for i, row in enumerate(f):
 print(repr(row)) # print raw strings
 if i >= 4: break
'{\n'
'  "meta" : {\n'
'    "view" : {\n'
'      "id" : "xn6j-b766",\n'
'      "name" : "COVID-19 Confirmed Cases",\n'

我们可以使用head Unix 命令(这也是pandashead方法的来源!)来查看文件的前几行:

!head -5 {covid_file}
{
  "meta" : {
    "view" : {
      "id" : "xn6j-b766",
      "name" : "COVID-19 Confirmed Cases",

为了将 JSON 文件加载到pandas中,让我们首先使用Pythonjson包进行一些 EDA,以了解 JSON 文件的特定结构,以便决定是否(以及如何)将其加载到pandas中。由于 JSON 数据与内部 Python 对象模型非常匹配,Python对 JSON 数据有相对良好的支持。在下面的单元格中,我们使用json包将整个 JSON 数据文件导入 Python 字典。

import json

with open(covid_file, "rb") as f:
 covid_json = json.load(f)

covid_json变量现在是一个编码文件中数据的字典:

type(covid_json)
dict

我们可以通过列出键来检查顶级 JSON 对象中有哪些键。

covid_json.keys()
dict_keys(['meta', 'data'])

观察:JSON 字典包含一个meta键,这可能是指元数据(关于数据的数据)。元数据通常与数据一起维护,并且可以成为额外信息的良好来源。

我们可以通过检查与元数据相关联的键来进一步调查元数据。

covid_json['meta'].keys()
dict_keys(['view'])

meta键包含另一个名为view的字典。这可能是关于某个基础数据库的特定“视图”的元数据。我们将在后面的课程中学习更多关于视图的知识。

covid_json['meta']['view'].keys()
dict_keys(['id', 'name', 'assetType', 'attribution', 'averageRating', 'category', 'createdAt', 'description', 'displayType', 'downloadCount', 'hideFromCatalog', 'hideFromDataJson', 'newBackend', 'numberOfComments', 'oid', 'provenance', 'publicationAppendEnabled', 'publicationDate', 'publicationGroup', 'publicationStage', 'rowsUpdatedAt', 'rowsUpdatedBy', 'tableId', 'totalTimesRated', 'viewCount', 'viewLastModified', 'viewType', 'approvals', 'columns', 'grants', 'metadata', 'owner', 'query', 'rights', 'tableAuthor', 'tags', 'flags'])

请注意,这是一个嵌套/递归数据结构。随着我们深入挖掘,我们会揭示更多的键和相应的数据:

meta
|-> data
    | ... (haven't explored yet)
|-> view
    | -> id
    | -> name
    | -> attribution 
    ...
    | -> description
    ...
    | -> columns
    ...

在视图子字典中有一个名为描述的键。这可能包含了数据的描述:

print(covid_json['meta']['view']['description'])
Counts of confirmed COVID-19 cases among Berkeley residents by date.
5.1.1.3.1.4 检查记录的数据字段

我们可以查看data字段中的一些条目。这是我们将加载到pandas中的数据。

for i in range(3):
 print(f"{i:03} | {covid_json['data'][i]}")
000 | ['row-kzbg.v7my-c3y2', '00000000-0000-0000-0405-CB14DE51DAA7', 0, 1643733903, None, 1643733903, None, '{ }', '2020-02-28T00:00:00', '1', '1']
001 | ['row-jkyx_9u4r-h2yw', '00000000-0000-0000-F806-86D0DBE0E17F', 0, 1643733903, None, 1643733903, None, '{ }', '2020-02-29T00:00:00', '0', '1']
002 | ['row-qifg_4aug-y3ym', '00000000-0000-0000-2DCE-4D1872F9B216', 0, 1643733903, None, 1643733903, None, '{ }', '2020-03-01T00:00:00', '0', '1']

观察:* 这些看起来像等长的记录,所以也许data是一个表格!* 但记录中的每个值代表什么?我们在哪里可以找到列标题?

为此,我们需要元数据字典中的columns键。这将返回一个列表:

type(covid_json['meta']['view']['columns'])
list
5.1.1.3.1.5 探索 JSON 文件的总结
  1. 上述元数据告诉我们很多关于数据中的列,包括列名、潜在的数据异常和基本统计信息。

  2. 由于其非表格结构,JSON 比 CSV 更容易创建自描述数据,这意味着数据的信息存储在与数据相同的文件中。

  3. 自描述数据可能会有所帮助,因为它保留了自己的描述,并且这些描述更有可能随着数据的变化而更新。

5.1.1.3.1.6 将 COVID 数据加载到pandas

最后,让我们将数据(而不是元数据)加载到pandasDataFrame中。在下面的代码块中,我们:

  1. 将 JSON 记录翻译成DataFrame

    • 字段:covid_json['meta']['view']['columns']

    • 记录:covid_json['data']

  2. 删除没有元数据描述的列。一般来说,这是一个坏主意,但在这里我们删除这些列,因为上面的分析表明它们不太可能包含有用的信息。

  3. 检查表的tail

# Load the data from JSON and assign column titles
covid = pd.DataFrame(
 covid_json['data'],
 columns=[c['name'] for c in covid_json['meta']['view']['columns']])

covid.tail()
sididpositioncreated_atcreated_metaupdated_atupdated_metametaDateNew CasesCumulative Cases
699row-49b6_x8zv.gyum00000000-0000-0000-A18C-9174A6D0577401643733903None1643733903None{ }2022-01-27T00:00:0010610694
700row-gs55-p5em.y4v900000000-0000-0000-F41D-5724AEABB4D601643733903None1643733903None{ }2022-01-28T00:00:0022310917
701row-3pyj.tf95-qu6700000000-0000-0000-BEE3-B0188D2518BD01643733903None1643733903None{ }2022-01-29T00:00:0013911056
702row-cgnd.8syv.jvjn00000000-0000-0000-C318-63CF75F7F74001643733903None1643733903None{ }2022-01-30T00:00:003311089
703row-qywv_24x6-237y00000000-0000-0000-FE92-9789FED3AA2001643733903None1643733903None{ }2022-01-31T00:00:004211131

5.1.2 变量类型

将数据加载到文件后,花时间了解数据集中编码的信息是一个好主意。特别是,我们想要确定我们的数据中存在哪些变量类型。广义上说,我们可以将变量分类为两种主要类型之一。

定量变量描述一些数值数量或量。我们可以进一步将定量数据分为:

  • 连续定量变量:可以在连续尺度上以任意精度测量的数值数据。连续变量没有严格的可能值集 - 它们可以记录到任意数量的小数位。例如,重量、GPA 或 CO[2]浓度。

  • 离散定量变量:只能取有限可能值的数值数据。例如,某人的年龄或他们的兄弟姐妹数量。

定性变量,也称为分类变量,描述的是不测量某种数量或量的数据。分类数据的子类别包括:

  • 有序定性变量:具有有序级别的类别。具体来说,有序变量是指级别之间的差异没有一致的、可量化的含义。一些例子包括教育水平(高中、本科、研究生等)、收入档次(低、中、高)或 Yelp 评分。

  • 无序定性变量:没有特定顺序的类别。例如,某人的政治立场或 Cal ID 号码。

变量类型的分类

请注意,许多变量不会完全属于这些类别中的一个。定性变量可能具有数值级别,反之亦然,定量变量可以存储为字符串。

5.1.3 主键和外键

上次,我们介绍了.merge作为pandas方法,用于将多个DataFrame连接在一起。在我们讨论连接时,我们提到了使用“键”来确定应该从每个表中合并哪些行的想法。让我们花点时间更仔细地研究这个想法。

主键是表中唯一确定其余列值的列或列集。它可以被认为是表中每一行的唯一标识符。例如,Data 100 学生表可能使用每个学生的 Cal ID 作为主键。

Cal IDNameMajor
03034619471OskiData Science
13035619472OllieComputer Science
23025619473OrrieData Science
33046789372OllieEconomics

外键是表中引用其他表主键的列或列集。在分配.mergeleft_onright_on参数时,了解数据集的外键可以很有用。在下面的办公时间票表中,“Cal ID”是引用前表的外键。

OH RequestCal IDQuestion
013034619471HW 2 Q1
123035619472HW 2 Q3
233025619473Lab 3 Q4
343035619472HW 2 Q7

5.2 粒度、范围和时间性

在了解数据集的结构之后,下一个任务是确定数据究竟代表什么。我们将通过考虑数据的粒度、范围和时间性来做到这一点。

5.2.1 粒度

数据集的粒度是单行代表的内容。您也可以将其视为数据中包含的细节级别。要确定数据的粒度,可以问:数据集中的每一行代表什么?细粒度数据包含大量细节,单行代表一个小的个体单位。例如,每条记录可能代表一个人。粗粒度数据被编码,以便单行代表一个大的个体单位-例如,每条记录可能代表一组人。

5.2.2 范围

数据集的范围是数据所涵盖的人口子集。如果我们调查数据科学课程中学生的表现,一个范围较窄的数据集可能包括所有注册 Data 100 课程的学生,而一个范围较广的数据集可能包括加利福尼亚州的所有学生。

5.2.3 时间性

数据集的时间性描述了数据收集的周期性,以及数据最近收集或更新的时间。

数据集的时间和日期字段可能代表一些内容:

  1. “事件”发生的时间

  2. 数据收集的时间,或者数据输入系统的时间

  3. 数据复制到数据库中的时间

为了充分了解数据的时间性,还可能需要标准化时区或检查数据中的重复时间趋势(模式是否在 24 小时内重复?一个月内?季节性?)。标准化时间的惯例是协调世界时(UTC),这是一个国际时间标准,在 0 度纬度上测量,整年保持一致(没有夏令时)。我们可以表示伯克利的时区,太平洋标准时间(PST),为 UTC-7(夏令时)。

5.2.3.1 使用pandasdt访问器进行时间处理

让我们简要地看一下如何使用pandasdt访问器来处理数据集中的日期/时间,使用你在实验 3 中看到的数据集:伯克利警察服务呼叫数据集。

Code

calls = pd.read_csv("data/Berkeley_PD_-_Calls_for_Service.csv")
calls.head()
CASENOOFFENSEEVENTDTEVENTTMCVLEGENDCVDOWInDbDateBlock_LocationBLKADDRCityState
021014296THEFT MISD. (UNDER $950)04/01/2021 12:00:00 AM10:58LARCENY406/15/2021 12:00:00 AMBerkeley, CA\n(37.869058, -122.270455)NaNBerkeleyCA
121014391THEFT MISD. (UNDER $950)04/01/2021 12:00:00 AM10:38LARCENY406/15/2021 12:00:00 AMBerkeley, CA\n(37.869058, -122.270455)NaNBerkeleyCA
221090494THEFT MISD. (UNDER $950)04/19/2021 12:00:00 AM12:15LARCENY106/15/2021 12:00:00 AM2100 BLOCK HASTE ST\nBerkeley, CA\n(37.864908,…2100 BLOCK HASTE STBerkeleyCA
321090204THEFT FELONY (OVER $950)02/13/2021 12:00:00 AM17:00LARCENY606/15/2021 12:00:00 AM2600 BLOCK WARRING ST\nBerkeley, CA\n(37.86393…2600 BLOCK WARRING STBerkeleyCA
421090179BURGLARY AUTO02/08/2021 12:00:00 AM6:20BURGLARY - VEHICLE106/15/2021 12:00:00 AM2700 BLOCK GARBER ST\nBerkeley, CA\n(37.86066,…2700 BLOCK GARBER STBerkeleyCA

看起来有三列带有日期/时间:EVENTDTEVENTTMInDbDate

很可能,EVENTDT代表事件发生的日期,EVENTTM代表事件发生的时间(24 小时制),InDbDate是这个呼叫被记录到数据库的日期。

如果我们检查这些列的数据类型,我们会发现它们被存储为字符串。我们可以使用 pandas 的to_datetime函数将它们转换为datetime对象。

calls["EVENTDT"] = pd.to_datetime(calls["EVENTDT"])
calls.head()
CASENOOFFENSEEVENTDTEVENTTMCVLEGENDCVDOWInDbDateBlock_LocationBLKADDRCityState
021014296THEFT MISD. (UNDER $950)2021-04-0110:58LARCENY406/15/2021 12:00:00 AMBerkeley, CA\n(37.869058, -122.270455)NaNBerkeleyCA
121014391THEFT MISD. (UNDER $950)2021-04-0110:38LARCENY406/15/2021 12:00:00 AMBerkeley, CA\n(37.869058, -122.270455)NaNBerkeleyCA
221090494THEFT MISD. (UNDER $950)2021-04-1912:15LARCENY106/15/2021 12:00:00 AM2100 BLOCK HASTE ST\nBerkeley, CA\n(37.864908,…2100 BLOCK HASTE STBerkeleyCA
321090204THEFT FELONY (OVER $950)2021-02-1317:00LARCENY606/15/2021 12:00:00 AM2600 BLOCK WARRING ST\nBerkeley, CA\n(37.86393…2600 BLOCK WARRING STBerkeleyCA
421090179BURGLARY AUTO2021-02-086:20BURGLARY - VEHICLE106/15/2021 12:00:00 AM2700 BLOCK GARBER ST\nBerkeley, CA\n(37.86066,…2700 BLOCK GARBER STBerkeleyCA

现在,我们可以在这一列上使用dt访问器。

我们可以得到月份:

calls["EVENTDT"].dt.month.head()
0    4
1    4
2    4
3    2
4    2
Name: EVENTDT, dtype: int64

日期是一周中的哪一天:

calls["EVENTDT"].dt.dayofweek.head()
0    3
1    3
2    0
3    5
4    0
Name: EVENTDT, dtype: int64

检查最小值,看看是否有任何看起来可疑的 70 年代日期:

calls.sort_values("EVENTDT").head()
CASENOOFFENSEEVENTDTEVENTTMCVLEGENDCVDOWInDbDateBlock_LocationBLKADDRCityState
251320057398BURGLARY COMMERCIAL2020-12-1716:05BURGLARY - COMMERCIAL406/15/2021 12:00:00 AM600 BLOCK GILMAN ST\nBerkeley, CA\n(37.878405,…600 BLOCK GILMAN STBerkeleyCA
62420057207ASSAULT/BATTERY MISD.2020-12-1716:50ASSAULT406/15/2021 12:00:00 AM2100 BLOCK SHATTUCK AVE\nBerkeley, CA\n(37.871…2100 BLOCK SHATTUCK AVEBerkeleyCA
15420092214THEFT FROM AUTO2020-12-1718:30LARCENY - FROM VEHICLE406/15/2021 12:00:00 AM800 BLOCK SHATTUCK AVE\nBerkeley, CA\n(37.8918…800 BLOCK SHATTUCK AVEBerkeleyCA
65920057324THEFT MISD. (UNDER $950)2020-12-1715:44LARCENY406/15/2021 12:00:00 AM1800 BLOCK 4TH ST\nBerkeley, CA\n(37.869888, -…1800 BLOCK 4TH STBerkeleyCA
99320057573BURGLARY RESIDENTIAL2020-12-1722:15BURGLARY - RESIDENTIAL406/15/2021 12:00:00 AM1700 BLOCK STUART ST\nBerkeley, CA\n(37.857495…1700 BLOCK STUART STBerkeleyCA

看起来不像!我们做得很好!

我们还可以使用dt访问器执行许多操作,例如切换时区和将时间转换回 UNIX/POSIX 时间。查看.dt访问器时间序列/日期功能的文档。

5.3 忠实度

在数据清理和 EDA 工作流的这个阶段,我们已经取得了相当大的成就:我们已经确定了数据的结构,了解了它所编码的信息,并获得了有关它是如何生成的见解。在整个过程中,我们应该始终记住数据科学工作的最初目的 - 使用数据更好地理解和建模现实世界。为了实现这一目标,我们需要确保我们使用的数据忠实于现实;也就是说,我们的数据准确地捕捉了“真实世界”。

用于研究或工业的数据通常是“混乱的” - 可能存在影响数据集忠实度的错误或不准确性。数据可能不忠实的迹象包括:

  • 不切实际或“错误”的值,例如负计数、不存在的位置或设置在未来的日期

  • 违反明显依赖关系的迹象,例如年龄与生日不匹配

  • 明显表明数据是手工输入的迹象,这可能导致拼写错误或字段错误移位

  • 数据伪造的迹象,例如虚假的电子邮件地址或重复使用相同的名称

  • 包含相同信息的重复记录或字段

  • 截断数据,例如 Microsoft Excel 将行数限制为 655536,列数限制为 255

我们通常通过以下方式解决一些更常见的问题:

  • 拼写错误:应用更正或删除不在字典中的记录

  • 时区不一致:转换为通用时区(例如 UTC)

  • 重复的记录或字段:识别和消除重复项(使用主键)

  • 未指定或不一致的单位:推断单位并检查数据中的值是否在合理范围内

5.3.1 缺失值

现实世界数据集经常遇到的另一个常见问题是缺失数据。解决这个问题的一种策略是从数据集中简单地删除任何具有缺失值的记录。然而,这会引入引入偏见的风险 - 缺失或损坏的记录可能与数据中感兴趣的某些特征有系统关联。另一个解决方案是将数据保留为NaN值。

解决缺失数据的第三种方法是执行插补:使用数据集中的其他数据推断缺失值。可以实施各种插补技术;以下是一些最常见的插补技术。

  • 平均插补:用该字段的平均值替换缺失值

  • 热卡插补:用某个随机值替换缺失值

  • 回归插补:开发模型以预测缺失值

  • 多重插补:用多个随机值替换缺失值

无论使用何种策略来处理缺失数据,我们都应该仔细考虑为什么特定记录或字段可能丢失 - 这可以帮助确定这些值的缺失是否重要或有意义。

6 EDA 演示 1:美国的结核病

现在,让我们走一遍数据清理和 EDA 工作流程,看看我们能从美国的结核病情况中学到什么!

我们将检查2021 年发表的原始 CDC 文章中包含的数据。

6.1 CSV 和字段名称

假设表 1 被保存为位于data/cdc_tuberculosis.csv的 CSV 文件。

然后,我们可以以多种方式探索 CSV(这是一个文本文件,不包含二进制编码数据):1. 使用文本编辑器如 emacs,vim,VSCode 等。2. 直接在 DataHub(只读),Excel,Google Sheets 等中打开 CSV。3. Python文件对象 4. pandas,使用pd.read_csv()

要尝试选项 1 和 2,您可以在左侧菜单中的data文件夹下查看或下载来自演示笔记本的结核病数据。请注意,CSV 文件是一种矩形数据(即表格数据),存储为逗号分隔的值

接下来,让我们尝试使用Python文件对象的选项 3。我们将查看前四行:

代码

with open("data/cdc_tuberculosis.csv", "r") as f:
 i = 0
 for row in f:
 print(row)
 i += 1
 if i > 3:
 break
,No. of TB cases,,,TB incidence,,

U.S. jurisdiction,2019,2020,2021,2019,2020,2021

Total,"8,900","7,173","7,860",2.71,2.16,2.37

Alabama,87,72,92,1.77,1.43,1.83 

哇,为什么在 CSV 的行之间有空行?

您可能还记得文本文件中的所有换行符都被编码为特殊的换行符\n。 Python 的print()打印每个字符串(包括换行符),并在此基础上再添加一个换行符。

如果您感兴趣,我们可以使用repr()函数返回带有所有特殊字符的原始字符串:

代码

with open("data/cdc_tuberculosis.csv", "r") as f:
 i = 0
 for row in f:
 print(repr(row)) # print raw strings
 i += 1
 if i > 3:
 break
',No. of TB cases,,,TB incidence,,\n'
'U.S. jurisdiction,2019,2020,2021,2019,2020,2021\n'
'Total,"8,900","7,173","7,860",2.71,2.16,2.37\n'
'Alabama,87,72,92,1.77,1.43,1.83\n'

最后,让我们尝试选项 4,并使用经过验证的 Data 100 方法:pandas

tb_df = pd.read_csv("data/cdc_tuberculosis.csv")
tb_df.head()
Unnamed: 0TB casesUnnamed: 2Unnamed: 3TB incidenceUnnamed: 5Unnamed: 6
0U.S. jurisdiction2019202020212019.002020.002021.00
1Total8,9007,1737,8602.712.162.37
2Alabama8772921.771.431.83
3Alaska5858587.917.927.92
4Arizona1831361292.511.891.77

您可能会注意到这个表格有一些奇怪的地方:列名中的“未命名”是怎么回事,以及第一行是什么?

恭喜 - 您已经准备好整理您的数据了!由于数据的存储方式,我们需要稍微清理一下数据,以更好地命名我们的列。

一个合理的第一步是识别正确标题的行。pd.read_csv()函数(文档)具有方便的header参数,我们可以将其设置为使用第 1 行的元素作为适当的列:

tb_df = pd.read_csv("data/cdc_tuberculosis.csv", header=1) # row index
tb_df.head(5)
U.S. jurisdiction2019202020212019.12020.12021.1
0Total8,9007,1737,8602.712.162.37
1Alabama8772921.771.431.83
2Alaska5858587.917.927.92
3Arizona1831361292.511.891.77
4Arkansas6459692.121.962.28

等等…现在我们无法区分“结核病病例数”和“结核病发生率”年列。 pandas已经尝试通过自动向后面的列添加“.1”来简化我们的生活,但这并不能帮助我们,作为人类,理解数据。

我们可以使用df.rename()文档)手动执行此操作:

rename_dict = {'2019': 'TB cases 2019',
 '2020': 'TB cases 2020',
 '2021': 'TB cases 2021',
 '2019.1': 'TB incidence 2019',
 '2020.1': 'TB incidence 2020',
 '2021.1': 'TB incidence 2021'}
tb_df = tb_df.rename(columns=rename_dict)
tb_df.head(5)
U.S. jurisdictionTB cases 2019TB cases 2020TB cases 2021TB incidence 2019TB incidence 2020TB incidence 2021
0Total8,9007,1737,8602.712.162.37
1Alabama8772921.771.431.83
2Alaska5858587.917.927.92
3Arizona1831361292.511.891.77
4Arkansas6459692.121.962.28

6.2 记录粒度

你可能已经在想:第一条记录怎么了?

第 0 行是我们所谓的汇总记录,或摘要记录。在向人类显示表格时,它通常很有用。记录 0(总计)的粒度与其他记录(州)的粒度不同。

好的,探索性数据分析第二步。汇总记录是如何聚合的?

让我们检查总结核病例是否是所有州结核病例的总和。如果我们对所有行求和,我们应该得到每年结核病病例的总数的2 倍(你认为这是为什么?)。

代码

tb_df.sum(axis=0)
U.S. jurisdiction    TotalAlabamaAlaskaArizonaArkansasCaliforniaCol...
TB cases 2019        8,9008758183642,111666718245583029973261085237...
TB cases 2020        7,1737258136591,706525417194122219282169239376...
TB cases 2021        7,8609258129691,750585443194992281064255127494...
TB incidence 2019                                               109.94
TB incidence 2020                                                93.09
TB incidence 2021                                               102.94
dtype: object

哇,2019 年、2020 年和 2021 年的结核病病例怎么了?查看列类型:

代码

tb_df.dtypes
U.S. jurisdiction     object
TB cases 2019         object
TB cases 2020         object
TB cases 2021         object
TB incidence 2019    float64
TB incidence 2020    float64
TB incidence 2021    float64
dtype: object

由于结核病病例的值中有逗号,数字被读取为object数据类型,或存储类型(接近Python字符串数据类型),因此pandas正在连接字符串而不是添加整数(回想一下Python可以“求和”或连接字符串在一起:"data" + "100"的结果是"data100")。

幸运的是,read_csv还有一个thousands参数(文档):

# improve readability: chaining method calls with outer parentheses/line breaks
tb_df = (
 pd.read_csv("data/cdc_tuberculosis.csv", header=1, thousands=',')
 .rename(columns=rename_dict)
)
tb_df.head(5)
U.S. jurisdictionTB cases 2019TB cases 2020TB cases 2021TB incidence 2019TB incidence 2020TB incidence 2021
0Total8900717378602.712.162.37
1Alabama8772921.771.431.83
2Alaska5858587.917.927.92
3Arizona1831361292.511.891.77
4Arkansas6459692.121.962.28
tb_df.sum()
U.S. jurisdiction    TotalAlabamaAlaskaArizonaArkansasCaliforniaCol...
TB cases 2019                                                    17800
TB cases 2020                                                    14346
TB cases 2021                                                    15720
TB incidence 2019                                               109.94
TB incidence 2020                                                93.09
TB incidence 2021                                               102.94
dtype: object

总结核病例看起来没问题。哦!

让我们只看具有州级粒度的记录:

代码

state_tb_df = tb_df[1:]
state_tb_df.head(5)
U.S. jurisdictionTB cases 2019TB cases 2020TB cases 2021TB incidence 2019TB incidence 2020TB incidence 2021
1Alabama8772921.771.431.83
2Alaska5858587.917.927.92
3Arizona1831361292.511.891.77
4Arkansas6459692.121.962.28
5California2111170617505.354.324.46

6.3 收集人口普查数据

美国人口普查人口估计来源(2019 年),来源(2020-2021 年)。

运行下面的单元格清理数据。这里有一些新的方法:* df.convert_dtypes() (文档)方便地将所有浮点数类型转换为整数,超出了课程范围。* df.drop_na() (文档)将在下次详细解释。

代码

# 2010s census data
census_2010s_df = pd.read_csv("data/nst-est2019-01.csv", header=3, thousands=",")
census_2010s_df = (
 census_2010s_df
 .reset_index()
 .drop(columns=["index", "Census", "Estimates Base"])
 .rename(columns={"Unnamed: 0": "Geographic Area"})
 .convert_dtypes()                 # "smart" converting of columns, use at your own risk
 .dropna()                         # we'll introduce this next time
)
census_2010s_df['Geographic Area'] = census_2010s_df['Geographic Area'].str.strip('.')

# with pd.option_context('display.min_rows', 30): # shows more rows
#     display(census_2010s_df)

census_2010s_df.head(5)
Geographic Area2010201120122013201420152016201720182019
0American309,321,666311,556,874313,830,990315,993,715318,301,008320,635,163322,941,311324,985,539326,687,501328,239,523
1Northeast55380134556042235577521655901806560060115603468456042330560592405604662055982803
2Midwest66974416671578006733674367560379677451676786058367987540681267816823662868329004
3South114866680116006522117241208118364400119624037120997341122351760123542189124569433125580448
4West72100436727883297347782374167130749257937574255576559681772573297783482078347268

有时,您会想要修改导入的代码。要重新导入这些修改,您可以使用pythonimportlib库:

from importlib import reload
reload(utils)

或者使用iPython魔术,它将在文件更改时智能地导入代码:

%load_ext autoreload
%autoreload 2

代码

# census 2020s data
census_2020s_df = pd.read_csv("data/NST-EST2022-POP.csv", header=3, thousands=",")
census_2020s_df = (
 census_2020s_df
 .reset_index()
 .drop(columns=["index", "Unnamed: 1"])
 .rename(columns={"Unnamed: 0": "Geographic Area"})
 .convert_dtypes()                 # "smart" converting of columns, use at your own risk
 .dropna()                         # we'll introduce this next time
)
census_2020s_df['Geographic Area'] = census_2020s_df['Geographic Area'].str.strip('.')

census_2020s_df.head(5)
Geographic Area202020212022
0American331511512332031554333287557
1Northeast574488985725925757040406
2Midwest689610436883650568787595
3South126450613127346029128716192
4West786509587858976378743364

6.4 合并数据(合并“DataFrame”)

时间merge!这里我们使用DataFrame方法df1.merge(right=df2, ...)DataFrame df1上(文档)。与函数pd.merge(left=df1, right=df2, ...)文档)进行对比。可以随意使用任何一个。

# merge TB DataFrame with two US census DataFrames
tb_census_df = (
 tb_df
 .merge(right=census_2010s_df,
 left_on="U.S. jurisdiction", right_on="Geographic Area")
 .merge(right=census_2020s_df,
 left_on="U.S. jurisdiction", right_on="Geographic Area")
)
tb_census_df.head(5)
U.S. jurisdictionTB cases 2019TB cases 2020TB cases 2021TB incidence 2019TB incidence 2020TB incidence 2021Geographic Area_x2010201120122013201420152016201720182019Geographic Area_y202020212022
0Alabama8772921.771.431.83Alabama4785437479906948155884830081484179948523474863525487448648876814903185Alabama503136250498465074296
1Alaska5858587.917.927.92Alaska713910722128730443737068736283737498741456739700735139731545Alaska732923734182733583
2Arizona1831361292.511.891.77Arizona6407172647264365549786632764673041368296766941072704400871580247278717Arizona717994372648777359197
3Arkansas6459692.121.962.28Arkansas2921964294066729521642959400296739229780482989918300134530097333017804Arkansas301419530281223045637
4California2111170617505.354.324.46California37319502376383693794880038260787385969723891804539167117393584973946158839512223California395016533914299139029342

拥有所有这些列有点不方便。我们现在可以删除不需要的列,或者只是合并较小的人口普查“DataFrame”。让我们选择后者。

# try merging again, but cleaner this time
tb_census_df = (
 tb_df
 .merge(right=census_2010s_df[["Geographic Area", "2019"]],
 left_on="U.S. jurisdiction", right_on="Geographic Area")
 .drop(columns="Geographic Area")
 .merge(right=census_2020s_df[["Geographic Area", "2020", "2021"]],
 left_on="U.S. jurisdiction", right_on="Geographic Area")
 .drop(columns="Geographic Area")
)
tb_census_df.head(5)
U.S. jurisdictionTB cases 2019TB cases 2020TB cases 2021TB incidence 2019TB incidence 2020TB incidence 2021201920202021
0Alabama8772921.771.431.83490318550313625049846
1Alaska5858587.917.927.92731545732923734182
2Arizona1831361292.511.891.77727871771799437264877
3Arkansas6459692.121.962.28301780430141953028122
4California2111170617505.354.324.46395122233950165339142991

6.5 再现数据:计算发病率

让我们重新计算发病率,以确保我们知道原始 CDC 数字来自何处。

根据疾病控制和预防中心的报告:TB 发病率计算为“使用美国人口普查局的中期人口估计,每 10 万人的病例数”。

如果我们将一个群体定义为 10 万人,那么我们可以计算给定州人口的 TB 发病率为

TB?发病率 = 人口中的?TB?病例 人口中的群体 = 人口中的?TB?病例 人口 / 100000 \text{TB 发病率} = \frac{\text{人口中的 TB 病例}}{\text{人口中的群体}} = \frac{\text{人口中的 TB 病例}}{\text{人口}/100000} TB?发病率=人口中的群体人口中的?TB?病例?=人口/100000人口中的?TB?病例?

= 人口中的?TB?病例 人口 × 100000 = \frac{\text{人口中的 TB 病例}}{\text{人口}} \times 100000 =人口人口中的?TB?病例?×100000

让我们尝试 2019 年的情况:

tb_census_df["recompute incidence 2019"] = tb_census_df["TB cases 2019"]/tb_census_df["2019"]*100000
tb_census_df.head(5)
U.S. jurisdictionTB Cases 2019TB Cases 2020TB Cases 2021TB Incidents 2019TB Incidents 2020TB Incidents 2021201920202021recompute incidence 2019
0Alabama8772921.771.431.834903185503136250498461.77
1Alaska5858587.917.927.927315457329237341827.93
2Arizona1831361292.511.891.777278717717994372648772.51
3Arkansas6459692.121.962.283017804301419530281222.12
4California2111170617505.354.324.463951222339501653391429915.34

太棒了!!!

让我们使用 for 循环和Python格式字符串来计算所有年份的 TB 发病率。Python f-strings 仅用于此演示目的,但在探索本课程之外的数据时,它们很方便(文档)。

# recompute incidence for all years
for year in [2019, 2020, 2021]:
 tb_census_df[f"recompute incidence {year}"] = tb_census_df[f"TB cases {year}"]/tb_census_df[f"{year}"]*100000
tb_census_df.head(5)
U.S. jurisdictionTB Cases 2019TB Cases 2020TB Cases 2021TB Incidents 2019TB Incidents 2020TB Incidents 2021201920202021recompute incidence 2019recompute incidence 2020recompute incidence 2021
0Alabama8772921.771.431.834903185503136250498461.771.431.82
1Alaska5858587.917.927.927315457329237341827.937.917.90
2Arizona1831361292.511.891.777278717717994372648772.511.891.78
3Arkansas6459692.121.962.283017804301419530281222.121.962.28
4California2111170617505.354.324.463951222339501653391429915.344.324.47

这些数字看起来非常接近!!!特别是在 2021 年,百分位数的小数位上有一些错误。进一步探讨这种差异背后的原因可能是有用的。

tb_census_df.describe()
TB Cases 2019TB Cases 2020TB Cases 2021TB Incidents 2019TB Incidents 2020TB Incidents 2021201920202021recompute incidence 2019recompute incidence 2020recompute incidence 2021
count51.0051.0051.0051.0051.0051.0051.0051.0051.0051.0051.0051.00
mean174.51140.65154.122.101.781.976436069.086500225.736510422.632.101.781.97
mean341.74271.06286.781.501.341.487360660.477408168.467394300.081.501.341.47
min1.000.002.000.170.000.21578759.00577605.00579483.000.170.000.21
25%25.5029.0023.001.291.211.231789606.001820311.001844920.001.301.211.23
50%70.0067.0069.001.801.521.704467673.004507445.004506589.001.811.521.69
75%180.50139.00150.002.581.992.227446805.007451987.007502811.002.581.992.22
min2111.001706.001750.007.917.927.9239512223.0039501653.0039142991.007.937.917.90

6.6 奖励 EDA:重现报告的统计数据

我们如何重现原始CDC 报告中报告的统计数据?

报告的结核病发病率(每 10 万人口的病例数)增加了9.4%,从 2020 年的2.2增加到 2021 年的2.4,但低于 2019 年的发病率(2.7)。美国出生和非美国出生人群的发病率均有所增加。

这是在整个美国人口中计算的结核病发病率!我们如何重现这一点?*我们需要重现我们滚动记录中的“总”结核病发病率。*但是我们当前的tb_census_df只有 51 个条目(50 个州加上华盛顿特区)。没有滚动记录。*发生了什么…?

让我们开始探索吧!

在我们继续探索之前,我们将所有索引设置为更有意义的值,而不仅仅是与某一行相关的数字。这将使我们的清理稍微容易一些。

代码

tb_df = tb_df.set_index("U.S. jurisdiction")
tb_df.head(5)
TB Cases 2019TB Cases 2020TB Cases 2021TB Incidents 2019TB Incidents 2020TB Incidents 2021
U.S. jurisdiction
Total8900717378602.712.162.37
Alabama8772921.771.431.83
Alaska5858587.917.927.92
Arizona1831361292.511.891.77
Arkansas6459692.121.962.28
census_2010s_df = census_2010s_df.set_index("Geographic Area")
census_2010s_df.head(5)
2010201120122013201420152016201720182019
Geographic Area
American309321666311556874313830990315993715318301008320635163322941311324985539326687501328239523
Northeast55380134556042235577521655901806560060115603468456042330560592405604662055982803
Midwest66974416671578006733674367560379677451676786058367987540681267816823662868329004
South114866680116006522117241208118364400119624037120997341122351760123542189124569433125580448
West72100436727883297347782374167130749257937574255576559681772573297783482078347268
census_2020s_df = census_2020s_df.set_index("Geographic Area")
census_2020s_df.head(5)
202020212022
Geographic Area
American331511512332031554333287557
Northeast574488985725925757040406
Midwest689610436883650568787595
South126450613127346029128716192
West786509587858976378743364

事实证明,我们上面的合并只保留了州记录,即使我们原始的tb_df中有“总计”滚动记录:

tb_df.head()
TB Cases 2019TB Cases 2020TB Cases 2021TB Incidents 2019TB Incidents 2020TB Incidents 2021
U.S. jurisdiction
Total8900717378602.712.162.37
Alabama8772921.771.431.83
Alaska5858587.917.927.92
Arizona1831361292.511.891.77
Arkansas6459692.121.962.28

请记住,默认情况下,merge执行内部合并,默认情况下,这意味着它只保留在两个DataFrame中都存在的键。

我们人口普查DataFrame中的滚动记录具有不同的地理区域字段,这是我们合并的关键:

census_2010s_df.head(5)
2010201120122013201420152016201720182019
Geographic Area
American309321666311556874313830990315993715318301008320635163322941311324985539326687501328239523
Northeast55380134556042235577521655901806560060115603468456042330560592405604662055982803
Midwest66974416671578006733674367560379677451676786058367987540681267816823662868329004
South114866680116006522117241208118364400119624037120997341122351760123542189124569433125580448
West72100436727883297347782374167130749257937574255576559681772573297783482078347268

人口普查DataFrame有几个已经合并的记录。我们正在寻找的聚合记录实际上将地理区域命名为“美国”。

有一个直接的方法来获得正确的合并,那就是重命名值本身。因为我们现在有地理区域索引,我们将使用df.rename() (文档):

# rename rolled record for 2010s
census_2010s_df.rename(index={'United States':'Total'}, inplace=True)
census_2010s_df.head(5)
2010201120122013201420152016201720182019
Geographic Area
Total309321666311556874313830990315993715318301008320635163322941311324985539326687501328239523
Northeast55380134556042235577521655901806560060115603468456042330560592405604662055982803
Midwest66974416671578006733674367560379677451676786058367987540681267816823662868329004
South114866680116006522117241208118364400119624037120997341122351760123542189124569433125580448
West72100436727883297347782374167130749257937574255576559681772573297783482078347268
# same, but for 2020s rename rolled record
census_2020s_df.rename(index={'United States':'Total'}, inplace=True)
census_2020s_df.head(5)
202020212022
Geographic Area
Total331511512332031554333287557
Northeast574488985725925757040406
Midwest689610436883650568787595
South126450613127346029128716192
West786509587858976378743364

接下来让我们重新运行我们的合并。请注意不同的链接方式,因为我们现在是在索引上进行合并(df.merge() 文档)。

tb_census_df = (
 tb_df
 .merge(right=census_2010s_df[["2019"]],
 left_index=True, right_index=True)
 .merge(right=census_2020s_df[["2020", "2021"]],
 left_index=True, right_index=True)
)
tb_census_df.head(5)
TB Cases 2019TB Cases 2020TB Cases 2021TB Incidents 2019TB Incidents 2020TB Incidents 2021201920202021
Total8900717378602.712.162.37328239523331511512332031554
Alabama8772921.771.431.83490318550313625049846
Alaska5858587.917.927.92731545732923734182
Arizona1831361292.511.891.77727871771799437264877
Arkansas6459692.121.962.28301780430141953028122

最后,让我们重新计算我们的发病率:

# recompute incidence for all years
for year in [2019, 2020, 2021]:
 tb_census_df[f"recompute incidence {year}"] = tb_census_df[f"TB cases {year}"]/tb_census_df[f"{year}"]*100000
tb_census_df.head(5)
TB Cases 2019TB Cases 2020TB Cases 2021TB Incidents 2019TB Incidents 2020TB Incidents 2021201920202021recompute incidence 2019recompute incidence 2020recompute incidence 2021
Total8900717378602.712.162.373282395233315115123320315542.712.162.37
Alabama8772921.771.431.834903185503136250498461.771.431.82
Alaska5858587.917.927.927315457329237341827.937.917.90
Arizona1831361292.511.891.777278717717994372648772.511.891.78
Arkansas6459692.121.962.283017804301419530281222.121.962.28

我们正确地重现了美国的总发病率!

我们快要完成了。让我们重新审视这段引用:

报告的结核病发病率(每 10 万人口的病例)增加了9.4%,从 2020 年的2.2增加到 2021 年的2.4,但低于 2019 年的发病率(2.7)。美国出生和非美国出生人群的发病率均有所增加。

回想一下,从 A A A B B B的百分比变化计算公式为 percent?change = B ? A A × 100 \text{percent change} = \frac{B - A}{A} \times 100 percent?change=AB?A?×100

incidence_2020 = tb_census_df.loc['Total', 'recompute incidence 2020']
incidence_2020
2.1637257652759883
incidence_2021 = tb_census_df.loc['Total', 'recompute incidence 2021']
incidence_2021
2.3672448914298068
difference = (incidence_2021 - incidence_2020)/incidence_2020 * 100
difference
9.405957511804143

7 EDA 演示 2:毛纳罗亚 CO[2]数据 - 数据忠实度的一课

毛纳罗亚观测站 自 1958 年以来一直在监测二氧化碳浓度

co2_file = "data/co2_mm_mlo.txt"

让我们做一些EDA

7.1 将此文件读入 Pandas?

让我们来看看这个.txt文件。要记住的一些问题:我们信任这个文件扩展名吗?它的结构是什么?

第 71-78 行(包括)如下所示:

line number |                            file contents

71          |   #            decimal     average   interpolated    trend    #days
72          |   #             date                             (season corr)
73          |   1958   3    1958.208      315.71      315.71      314.62     -1
74          |   1958   4    1958.292      317.45      317.45      315.29     -1
75          |   1958   5    1958.375      317.50      317.50      314.71     -1
76          |   1958   6    1958.458      -99.99      317.10      314.85     -1
77          |   1958   7    1958.542      315.86      315.86      314.98     -1
78          |   1958   8    1958.625      314.93      314.93      315.94     -1

注意:

  • 这些值由空格分隔,可能是制表符。

  • 数据在行上排列。例如,每行的第 7 到第 8 个位置显示了月份。

  • 文件的第 71 和 72 行包含分布在两行上的列标题。

我们可以使用read_csv将数据读入pandasDataFrame,并提供几个参数来指定分隔符是空格,没有标题(我们将设置自己的列名),并跳过文件的前 72 行。

co2 = pd.read_csv(
 co2_file, header = None, skiprows = 72,
 sep = r'\s+'       #delimiter for continuous whitespace (stay tuned for regex next lecture))
)
co2.head()

0 1 2 3 4 5 6


0 1958 3 1958.21 315.71 315.71 314.62 -1

1 1958 4 1958.29 317.45 317.45 315.29 -1

2 1958 5 1958.38 317.50 317.50 314.71 -1

3 1958 6 1958.46 -99.99 317.10 314.85 -1

4 1958 7 1958.54 315.86 315.86 314.98 -1

恭喜!你已经整理好了数据!

…但是我们的列没有命名。我们需要做更多的 EDA。

7.2 探索变量特征类型

NOAA 网页 可能有一些有用的信息(在这种情况下没有)。

利用这些信息,我们将重新运行pd.read_csv,但这次使用一些自定义列名

co2 = pd.read_csv(
 co2_file, header = None, skiprows = 72,
 sep = '\s+', #regex for continuous whitespace (next lecture)
 names = ['Yr', 'Mo', 'DecDate', 'Avg', 'Int', 'Trend', 'Days']
)
co2.head()

Yr Mo DecDate Avg Int Trend Days


0 1958 3 1958.21 315.71 315.71 314.62 -1

1 1958 4 1958.29 317.45 317.45 315.29 -1

2 1958 5 1958.38 317.50 317.50 314.71 -1

3 1958 6 1958.46 -99.99 317.10 314.85 -1

4 1958 7 1958.54 315.86 315.86 314.98 -1

7.3 可视化 CO[2]

科学研究往往具有非常干净的数据,对吧…?让我们立即制作二氧化碳月均值的时间序列图。

代码

sns.lineplot(x='DecDate', y='Avg', data=co2);

上面的代码使用了seaborn绘图库(缩写为sns)。我们将在可视化讲座中介绍这一点,但现在你不需要担心它是如何工作的!

天啊!绘制数据揭示了一个问题。明显的垂直线表明我们有一些缺失值。这里发生了什么?

co2.head()

Yr Mo DecDate Avg Int Trend Days


0 1958 3 1958.21 315.71 315.71 314.62 -1

1 1958 4 1958.29 317.45 317.45 315.29 -1

2 1958 5 1958.38 317.50 317.50 314.71 -1

3 1958 6 1958.46 -99.99 317.10 314.85 -1

4 1958 7 1958.54 315.86 315.86 314.98 -1

co2.tail()

Yr Mo DecDate Avg Int Trend Days


733 2019 4 2019.29 413.32 413.32 410.49 26

734 2019 5 2019.38 414.66 414.66 411.20 28

735 2019 6 2019.46 413.92 413.92 411.58 27

736 2019 7 2019.54 411.77 411.77 411.43 23

737 2019 8 2019.62 409.95 409.95 411.84 29

一些数据有异常值,如-1 和-99.99。

让我们再次检查文件顶部的描述。

  • -1 表示该月设备运行的天数Days的缺失值。

  • -99.99 表示缺失的月度平均Avg

我们该如何解决这个问题?首先,让我们探索数据的其他方面。了解我们的数据将帮助我们决定如何处理缺失值。

7.4 合理性检查:对数据进行推理

首先,我们考虑数据的形状。我们应该有多少行?

  • 如果按时间顺序,我们应该每个月有一条记录。

  • 数据从 1958 年 3 月到 2019 年 8 月。

  • 我们应该有$ 12 (2019-1957) - 2 - 4 = 738 $条记录。

co2.shape
(738, 7)

太好了!行数(即记录)与我们的预期相匹配。

现在让我们检查每个特征的质量。

7.5 理解缺失值 1:Days

Days是一个时间字段,所以让我们分析其他时间字段,看看是否有关于操作天数缺失的解释。

让我们从月份Mo开始。

我们有没有缺失的记录?月份的数量应该有 62 或 61 个实例(1957 年 3 月-2019 年 8 月)。

co2["Mo"].value_counts().sort_index()
1     61
2     61
3     62
4     62
5     62
6     62
7     62
8     62
9     61
10    61
11    61
12    61
Name: Mo, dtype: int64

如预期的那样,1 月、2 月、9 月、10 月、11 月和 12 月有 61 个实例,其余的有 62 个。

接下来让我们探索天数Days本身,这是测量设备运行的天数。

代码

sns.displot(co2['Days']);
plt.title("Distribution of days feature"); # suppresses unneeded plotting output
/Users/Ishani/micromamba/lib/python3.9/site-packages/seaborn/axisgrid.py:118: UserWarning:

The figure layout has changed to tight 

就数据质量而言,少数月份的平均值是基于少于一半天数的测量得出的。此外,有近 200 个缺失值-大约占数据的 27%

最后,让我们检查最后一个时间特征,年份Yr

让我们检查一下缺失和记录年份之间是否有任何联系。

代码

sns.scatterplot(x="Yr", y="Days", data=co2);
plt.title("Day field by Year"); # the ; suppresses output

观察

  • 所有缺失的数据都在运营初期。

  • 似乎 80 年代中后期可能出现了设备问题。

潜在的下一步

  • 通过有关历史读数的文档来确认这些解释。

  • 也许删除最早的记录?但是,在我们检查时间趋势并评估是否存在潜在问题之后,我们会推迟这样的行动。

7.6 理解缺失值 2:Avg

接下来,让我们回到Avg中的-99.99 值,分析二氧化碳测量的整体质量。我们将绘制平均 CO[2]测量的直方图

代码

# Histograms of average CO2 measurements
sns.displot(co2['Avg']);
/Users/Ishani/micromamba/lib/python3.9/site-packages/seaborn/axisgrid.py:118: UserWarning:

The figure layout has changed to tight 

非缺失值在 300-400 范围内(二氧化碳水平的常规范围)。

我们还看到只有少数缺失的“Avg”值(<1%的值)。让我们检查所有这些值:

co2[co2["Avg"] < 0]
YrMoDecDateAvgIntTrendDays
3195861958.46-99.99317.10314.85-1
71958101958.79-99.99312.66315.61-1
71196421964.12-99.99320.07319.61-1
72196431964.21-99.99320.73319.55-1
73196441964.29-99.99321.77319.48-1
2131975121975.96-99.99330.59331.600
313198441984.29-99.99346.84344.272

这些值似乎没有任何模式,除了大多数记录也缺少了Days数据。

7.7 删除、NaN或填补缺失的Avg数据?

我们应该如何处理无效的Avg数据?

  1. 删除记录

  2. 设置为 NaN

  3. 使用某种策略填补

记住我们想要修复以下的图表:

代码

sns.lineplot(x='DecDate', y='Avg', data=co2)
plt.title("CO2 Average By Month");

由于我们正在绘制Avg vs DecDate,我们应该专注于处理Avg的缺失值。

让我们考虑几个选项:1. 删除这些记录 2. 用 NaN 替换-99.99 3. 用平均 CO2 的可能值替换它?

你认为每种可能行动的利弊是什么?

让我们检查这三个选项。

# 1\. Drop missing values
co2_drop = co2[co2['Avg'] > 0]
co2_drop.head()
YrMoDecDateAvgIntTrendDays
0195831958.21315.71315.71314.62-1
1195841958.29317.45317.45315.29-1
2195851958.38317.50317.50314.71-1
4195871958.54315.86315.86314.98-1
5195881958.62314.93314.93315.94-1
# 2\. Replace NaN with -99.99
co2_NA = co2.replace(-99.99, np.NaN)
co2_NA.head()
YearMonthDecDateAvgIntTrendDays
0195831958.21315.71315.71314.62-1
1195841958.29317.45317.45315.29-1
2195851958.38317.50317.50314.71-1
3195861958.46NaN317.10314.85-1
4195871958.54315.86315.86314.98-1

我们还将使用数据的第三个版本。

首先,我们注意到数据集已经为-99.99 提供了一个替代值

从文件描述:

“插值”列包括前一列(“平均值”)的平均值和数据缺失时的插值值。插值值是通过两个步骤计算出来的…

Int特征的值与Avg完全匹配,只有当Avg为-99.99 时,才会使用一个合理的估计。

因此,我们的数据的第三个版本将使用Int特征而不是Avg

# 3\. Use interpolated column which estimates missing Avg values
co2_impute = co2.copy()
co2_impute['Avg'] = co2['Int']
co2_impute.head()
YearMonthDecDateAvgIntTrendDays
0195831958.21315.71315.71314.62-1
1195841958.29317.45317.45315.29-1
2195851958.38317.50317.50314.71-1
3195861958.46317.10317.10314.85-1
4195871958.54315.86315.86314.98-1

一个合理的估计是什么?

为了回答这个问题,让我们放大到一个短时间段,比如 1958 年的测量数据(我们知道有两个缺失值)。

代码

# results of plotting data in 1958

def line_and_points(data, ax, title):
 # assumes single year, hence Mo
 ax.plot('Mo', 'Avg', data=data)
 ax.scatter('Mo', 'Avg', data=data)
 ax.set_xlim(2, 13)
 ax.set_title(title)
 ax.set_xticks(np.arange(3, 13))

def data_year(data, year):
 return data[data["Yr"] == 1958]

# uses matplotlib subplots
# you may see more next week; focus on output for now
fig, axes = plt.subplots(ncols = 3, figsize=(12, 4), sharey=True)

year = 1958
line_and_points(data_year(co2_drop, year), axes[0], title="1\. Drop Missing")
line_and_points(data_year(co2_NA, year), axes[1], title="2\. Missing Set to NaN")
line_and_points(data_year(co2_impute, year), axes[2], title="3\. Missing Interpolated")

fig.suptitle(f"Monthly Averages for {year}")
plt.tight_layout()

从大局来看,由于只有 7 个Avg值缺失(占 738 个月的 <1%),任何这些方法都可以使用。

然而,选项 C:插补也有一定吸引力:

  • 显示二氧化碳的季节性趋势

  • 我们正在绘制数据中所有月份的线图

让我们用选项 3 重新绘制我们的原始图表:

代码

sns.lineplot(x='DecDate', y='Avg', data=co2_impute)
plt.title("CO2 Average By Month, Imputed");

看起来与 NOAA 网站上看到的差不多!

7.8 展示数据:关于数据粒度的讨论

从描述:

  • 月度测量是平均每日测量的平均值。

  • NOAA GML 网站也有每日/每小时测量的数据集。

您呈现的数据取决于您的研究问题。

二氧化碳水平如何随季节变化?

  • 您可能希望保留每月的平均数据。

过去 50 多年来,二氧化碳水平是否上升,与全球变暖的预测一致?

  • 您可能更喜欢使用年平均数据的粗粒度

代码

co2_year = co2_impute.groupby('Yr').mean()
sns.lineplot(x='Yr', y='Avg', data=co2_year)
plt.title("CO2 Average By Year");

事实上,自从毛纳罗亚开始记录以来,二氧化碳上升了近 100ppm。

8 总结

我们在本讲座中涵盖了很多内容;让我们总结一下最重要的要点:

8.1 处理缺失值

我们可以采取几种方法来处理缺失数据:

  • 删除缺失记录

  • 保留NaN缺失值

  • 使用插值列进行插补

8.2 探索性数据分析和数据整理

有几种方法可以处理探索性数据分析和数据整理:

  • 分析数据和元数据:数据的日期、大小、组织和结构是什么?

  • 逐个检查每个字段/属性/维度

  • 逐对相关维度进行检查(例如,按专业分解等级)。

  • 在这个过程中,我们可以:

    • 可视化或总结数据。

    • 验证关于数据及其收集过程的假设。特别注意数据收集的时间。

    • 识别和解决异常

    • 应用数据转换和校正(我们将在即将到来的讲座中介绍)。

    • **记录你所做的一切!**在 Jupyter Notebook 中开发可以促进你自己工作的可重复性

文章来源:https://blog.csdn.net/wizardforcel/article/details/135563806
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。