在本章中,我们希望使您熟悉典型的数据准备任务和分析技术,因为流利地准备,分组和重塑数据是成功进行数据分析的重要基础。
尽管准备数据似乎是一项平凡的任务,而且通常是这样,但这是我们不能跳过的一步,尽管我们可以通过使用诸如 Pandas 之类的工具来简化数据。
为什么根本没有必要做准备? 因为最有用的数据将来自现实世界,并且会存在缺陷,包含错误或零散的信息。
数据准备有用的原因还有很多:它使您与原材料保持紧密联系。 了解您的输入有助于您尽早发现潜在错误并建立对结果的信心。
以下是一些数据准备方案:
- 客户将三个文件交给您,每个文件包含有关单个地质现象的时间序列数据,但观察到的数据记录在不同的时间间隔上,并使用不同的分隔符
- 机器学习算法只能处理数字数据,但您的输入仅包含文本标签
- 您将获得即将启动的服务的 Web 服务器的原始日志,您的任务是根据现有访问者的行为为增长策略提出建议
用于数据处理的工具库庞大,尽管我们将重点介绍 Python,但我们也想提及一些有用的工具。 如果它们在您的系统上可用,并且您希望处理大量数据,则值得学习。
一组工具属于 Unix 传统,它强调文本处理,因此,在过去的 40 年中,开发了许多高性能和经过考验的用于处理文本的工具。 一些常见的工具是:sed
,grep
,awk
,sort
,uniq
,tr
,cut
,tail
和head
。 它们执行非常基本的操作,例如从文件中过滤出行(grep
或列(cut
),替换文本(sed
,tr
)或仅显示文件的一部分(head
,tail
)。
我们仅想通过一个示例来演示这些工具的功能。
想象一下,您已经收到了 Web 服务器的日志文件,并且对 IP 地址的分配感兴趣。
日志文件的每一行均包含通用日志服务器格式的条目(您可以从这个页面下载此数据集) ):
$ cat epa-html.txt
wpbfl2-45.gate.net [29:23:56:12] "GET /Access/ HTTP/1.0" 200 2376ebaca.icsi.net [30:00:22:20] "GET /Info.html HTTP/1.0" 200 884
例如,我们想知道某些用户访问我们网站的频率。
我们仅对第一列感兴趣,因为可以在其中找到 IP 地址或主机名。 之后,我们需要计算每个主机的出现次数,最后以友好的方式显示结果。
sort
| uniq -c
节是我们的主力军:它将首先对数据进行排序,uniq -c
将保存出现次数和值。 sort -nr | head -15
是我们的格式化部分; 我们按数字(-n
)和反向(-r
)进行排序,并且仅保留前 15 个条目。
将所有内容与管道放在一起:
$ cut -d ' ' -f 1 epa-http.txt | sort | uniq -c | sort -nr | head -15
294 sandy.rtptok1.epa.gov
292 e659229.boeing.com
266 wicdgserv.wic.epa.gov
263 keyhole.es.dupont.com
248 dwilson.pr.mcs.net
176 oea4.r8stw56.epa.gov
174 macip26.nacion.co.cr
172 dcimsd23.dcimsd.epa.gov
167 www-b1.proxy.aol.com
158 piweba3y.prodigy.com
152 wictrn13.dcwictrn.epa.gov
151 nntp1.reach.com
151 inetg1.arco.com
149 canto04.nmsu.edu
146 weisman.metrokc.gov
使用一个命令,我们可以将顺序服务器日志转换为访问我们站点的最常见主机的有序列表。 我们还看到,我们的最高用户之间的访问次数似乎没有太大差异。
有用的工具很少,以下只是其中的很小一部分:
csvkit
:这是用于处理 CSV(表格文件格式之王)的实用程序套件jq
:这是一个轻巧灵活的命令行 JSON 处理器xmlstarlet
:这是一个工具,它支持 XPath 等 XML 查询q
:此操作在文本文件上运行 SQL
Unix 命令行结束处,轻量级语言接管了。 您可能只能从文本中获得印象,但是您的同事可能会更喜欢由 matplotlib 生成的可视化表示形式,例如图表或漂亮的图形。
Python 及其数据工具生态系统比命令行更具通用性,但是对于首次探索和简单操作而言,命令行的有效性通常是无与伦比的。
大多数真实世界的数据都会存在一些缺陷,因此需要首先进行清洁步骤。 我们从一个小文件开始。 尽管此文件仅包含四行,但它将使我们能够演示经过清理的数据集的过程:
$ cat small.csv
22,6.1
41,5.7
18,5.3*
29,NA
请注意,此文件存在一些问题。 包含值的行均以逗号分隔,但缺少(NA)值,可能还有不干净的(5.3 *)值。 尽管如此,我们可以将该文件加载到数据帧中:
>>> import pandas as pd
>>> df = pd.read_csv("small.csv")
>>> df
22 6.1
0 41 5.7
1 18 5.3*
2 29 NaN
Pandas 将第一行用作header
,但这不是我们想要的:
>>> df = pd.read_csv("small.csv", header=None)
>>> df
0 1
0 22 6.1
1 41 5.7
2 18 5.3*
3 29 NaN
这样做更好,但是我们希望提供自己的列名称,而不是数字值:
>>> df = pd.read_csv("small.csv", names=["age", "height"])
>>> df
age height
0 22 6.1
1 41 5.7
2 18 5.3*
3 29 NaN
age
列看起来不错,因为 Pandas 已经推断出了预期的类型,但是height
尚不能解析为数值:
>>> df.age.dtype
dtype('int64')
>>> df.height.dtype
dtype('O')
如果我们尝试将height
列强制转换为浮点值,则 Pandas 将报告异常:
>>> df.height.astype('float')
ValueError: invalid literal for float(): 5.3*
我们可以将任何可解析的值用作浮点数,并使用convert_objects
方法丢弃其余值:
>>> df.height.convert_objects(convert_numeric=True)
0 6.1
1 5.7
2 NaN
3 NaN
Name: height, dtype: float64
如果我们事先知道数据集中的不良字符,则可以使用自定义转换器函数来扩展read_csv
方法:
>>> remove_stars = lambda s: s.replace("*", "")
>>> df = pd.read_csv("small.csv", names=["age", "height"],
converters={"height": remove_stars})
>>> df
age height
0 22 6.1
1 41 5.7
2 18 5.3
3 29 NA
现在,我们终于可以使 height 列更有用了。 我们可以为其分配更新的版本,该版本具有首选类型:
>>> df.height = df.height.convert_objects(convert_numeric=True)
>>> df
age height
0 22 6.1
1 41 5.7
2 18 5.3
3 29 NaN
如果我们只想保留完整的条目,则可以删除任何包含未定义值的行:
>>> df.dropna()
age height
0 22 6.1
1 41 5.7
2 18 5.3
我们可以使用默认的高度,也许是一个固定值:
>>> df.fillna(5.0)
age height
0 22 6.1
1 41 5.7
2 18 5.3
3 29 5.0
另一方面,我们也可以使用现有值的平均值:
>>> df.fillna(df.height.mean())
age height
0 22 6.1
1 41 5.7
2 18 5.3
3 29 5.7
最后三个数据帧是完整且正确的,具体取决于您在处理缺失值时对正确的定义。 特别是,这些列具有所需的类型,并准备进行进一步分析。 哪个数据帧最合适取决于当前的任务。
即使我们具有干净的和可能正确的数据,我们也可能只希望使用其中的一部分,或者可能要检查异常值。 离群点是由于可变性或测量误差而与其他观测值相距较远的观测点。 在这两种情况下,我们都希望减少数据集中的元素数量,以使其与进一步处理更加相关。
在此示例中,我们将尝试查找潜在的异常值。 我们将使用美国能源信息署记录的欧洲布伦特原油现货价格。 原始 Excel 数据可从这个页面获得(可以在第二个工作表中找到)。 我们对数据进行了轻微的清理(清理过程是本章末尾练习的一部分),并将使用以下数据框架,其中包含 7160 个条目,范围从 1987 年到 2015 年:
>>> df.head()
date price
0 1987-05-20 18.63
1 1987-05-21 18.45
2 1987-05-22 18.55
3 1987-05-25 18.60
4 1987-05-26 18.63
>>> df.tail()
date price
7155 2015-08-04 49.08
7156 2015-08-05 49.04
7157 2015-08-06 47.80
7158 2015-08-07 47.54
7159 2015-08-10 48.30
尽管很多人都知道油价(无论是从新闻还是从加油站来的),但让我们暂时忘记我们对它的任何了解。 我们首先可以要求极端:
>>> df[df.price==df.price.min()]
date price
2937 1998-12-10 9.1
>>> df[df.price==df.price.max()]
date price
5373 2008-07-03 143.95
查找潜在离群值的另一种方法是要求提供与均值最不相符的值。 我们可以使用np.abs
函数首先计算与均值的偏差:
>>> np.abs(df.price - df.price.mean())
0 26.17137 1 26.35137 7157 2.99863
7158 2.73863 7159 3.49863
现在,我们可以将这个偏差与标准偏差的倍数(我们选择 2.5)进行比较:
>>> import numpy as np
>>> df[np.abs(df.price - df.price.mean()) > 2.5 * df.price.std()]
date price5354 2008-06-06 132.81
5355 2008-06-09 134.43
5356 2008-06-10 135.24
5357 2008-06-11 134.52
5358 2008-06-12 132.11
5359 2008-06-13 134.29
5360 2008-06-16 133.90
5361 2008-06-17 131.27
5363 2008-06-19 131.84
5364 2008-06-20 134.28
5365 2008-06-23 134.54
5366 2008-06-24 135.37
5367 2008-06-25 131.59
5368 2008-06-26 136.82
5369 2008-06-27 139.38
5370 2008-06-30 138.40
5371 2008-07-01 140.67
5372 2008-07-02 141.24
5373 2008-07-03 143.95
5374 2008-07-07 139.62
5375 2008-07-08 134.15
5376 2008-07-09 133.91
5377 2008-07-10 135.81
5378 2008-07-11 143.68
5379 2008-07-14 142.43
5380 2008-07-15 136.02
5381 2008-07-16 133.31
5382 2008-07-17 134.16
我们看到,2008 年夏季的那几天一定很特别。 确实,找到标题为和 2007-08 年石油危机的原因和后果之类的文章和文章并不难。 我们仅通过查看数据就发现了这些事件的踪迹。
我们可以每十年分别问前面的问题。 我们首先使数据框看起来更像一个时间序列:
>>> df.index = df.date
>>> del df["date"]
>>> df.head()
pricedate
1987-05-20 18.63 1987-05-21 18.45
1987-05-22 18.55 1987-05-25 18.60
1987-05-26 18.63
我们可以过滤出八十年代:
>>> decade = df["1980":"1989"]
>>> decade[np.abs(decade.price - decade.price.mean()) > 2.5 * decade.price.std()]
price
date
1988-10-03 11.60 1988-10-04 11.65 1988-10-05 11.20 1988-10-06 11.30 1988-10-07 11.35
我们观察到,在可得的数据(1987-1989 年)中,1988 年秋季的油价略有上涨。 同样,在 90 年代,我们看到 1990 年秋我们的偏差更大:
>>> decade = df["1990":"1999"]
>>> decade[np.abs(decade.price - decade.price.mean()) > 5 * decade.price.std()]
pricedate
1990-09-24 40.75 1990-09-26 40.85 1990-09-27 41.45 1990-09-28 41.00 1990-10-09 40.90 1990-10-10 40.20 1990-10-11 41.15
还有更多的用例来过滤数据。 空间和时间是典型的单位:您可能希望按州或城市过滤普查数据,或者按季度过滤经济数据。 可能性是无止境的,将由您的项目来驱动。
情况很常见:您有多个数据源,但是为了对内容进行陈述,您宁愿将它们组合在一起。 幸运的是,当合并,联接或对齐数据时,Pandas 的串联和合并功能消除了大部分麻烦。 它也以高度优化的方式进行。
在两个数据帧具有相似形状的情况下,将一个接一个添加到另一个可能很有用。 也许A
和B
是产品,并且一个数据框包含商店中每种产品售出的商品数量:
>>> df1 = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]})
>>> df1
A B
0 1 4
1 2 5
2 3 6
>>> df2 = pd.DataFrame({'A': [4, 5, 6], 'B': [7, 8, 9]})
>>> df2
A B
0 4 7
1 5 8
2 6 9
>>> df1.append(df2)
A B
0 1 4
1 2 5
2 3 6
0 4 7
1 5 8
2 6 9
有时,我们不会关心原始数据帧的索引:
>>> df1.append(df2, ignore_index=True)
A B
0 1 4
1 2 5
2 3 6
3 4 7
4 5 8
5 6 9
pd.concat
函数提供了一种更灵活的组合对象的方法,该函数将任意数量的序列,数据帧或面板作为输入。 默认行为类似于附加:
>>> pd.concat([df1, df2])
A B
0 1 4
1 2 5
2 3 6
0 4 7
1 5 8
2 6 9
默认的concat
操作沿行或索引附加两个帧,它们对应于轴 0。要沿列连接,我们可以传入 axis 关键字参数:
>>> pd.concat([df1, df2], axis=1)
A B A B
0 1 4 4 7
1 2 5 5 8
2 3 6 6 9
我们可以添加键来创建层次结构索引。
>>> pd.concat([df1, df2], keys=['UK', 'DE'])
A B
UK 0 1 4
1 2 5
2 3 6
DE 0 4 7
1 5 8
2 6 9
如果您想稍后再参考数据框架的某些部分,这将很有用。 我们使用ix
索引器:
>>> df3 = pd.concat([df1, df2], keys=['UK', 'DE'])
>>> df3.ix["UK"]
A B
0 1 4
1 2 5
2 3 6
数据帧类似于数据库表。 因此,Pandas 对它们执行类似 SQL 的联接操作就不足为奇了。 令人惊讶的是,这些操作是高度优化且极其快速的:
>>> import numpy as np
>>> df1 = pd.DataFrame({'key': ['A', 'B', 'C', 'D'],
'value': range(4)})
>>> df1
key value
0 A 0
1 B 1
2 C 2
3 D 3
>>> df2 = pd.DataFrame({'key': ['B', 'D', 'D', 'E'],'value': range(10, 14)})
>>> df2
key value
0 B 10
1 D 11
2 D 12
3 E 13
如果我们在key
上合并,则会得到一个内部联接。 这通过基于连接谓词组合原始数据帧的列值来创建新数据帧,此处使用key
属性:
>>> df1.merge(df2, on='key')
key value_x value_y
0 B 1 10
1 D 3 11
2 D 3 12
左,右和完全联接可以通过how
参数指定:
>>> df1.merge(df2, on='key', how='left')
key value_x value_y
0 A 0 NaN
1 B 1 10
2 C 2 NaN
3 D 3 11
4 D 3 12
>>> df1.merge(df2, on='key', how='right')
key value_x value_y
0 B 1 10
1 D 3 11
2 D 3 12
3 E NaN 13
>>> df1.merge(df2, on='key', how='outer')
key value_x value_y
0 A 0 NaN
1 B 1 10
2 C 2 NaN
3 D 3 11
4 D 3 12
5 E NaN 13
合并方法可以通过 how 参数指定。 下表显示了与 SQL 相比的方法:
|合并方法
|
SQL 连接名称
|
描述
|
| --- | --- | --- |
| left
| 左外连接 | 仅使用左框中的键。 |
| right
| 右外连接 | 仅使用右框架中的键。 |
| outer
| 全外连接 | 使用两个框架中的键并集。 |
| inner
| 内部联接 | 使用两个框架中关键点的交集。 |
我们看到了如何组合数据帧,但是有时我们在单个数据结构中拥有所有正确的数据,但是对于某些任务而言,这种格式是不切实际的。 我们从一些人工天气数据开始:
>>> df
date city value
0 2000-01-03 London 6
1 2000-01-04 London 3
2 2000-01-05 London 4
3 2000-01-03 Mexico 3
4 2000-01-04 Mexico 9
5 2000-01-05 Mexico 8
6 2000-01-03 Mumbai 12
7 2000-01-04 Mumbai 9
8 2000-01-05 Mumbai 8
9 2000-01-03 Tokyo 5
10 2000-01-04 Tokyo 5
11 2000-01-05 Tokyo 6
如果要计算每个城市的最高温度,我们可以按城市对数据进行分组,然后使用max
函数:
>>> df.groupby('city').max()
date value
city
London 2000-01-05 6
Mexico 2000-01-05 9
Mumbai 2000-01-05 12
Tokyo 2000-01-05 6
但是,如果我们每次都要将数据整理成表格,则可以通过首先创建一个经过重塑的数据框(将日期作为索引,将城市作为列)来提高效率。
我们可以使用pivot
功能创建这样的数据帧。 参数是索引(我们使用日期),列(我们使用城市)和值(存储在原始数据框的 value 列中):
>>> pv = df.pivot("date", "city", "value")
>>> pv
city London Mexico Mumbai Tokyodate
2000-01-03 6 3 12 5
2000-01-04 3 9 9 5
2000-01-05 4 8 8 6
我们可以在此新数据帧上直接使用max
函数:
>>> pv.max()
city
London 6
Mexico 9
Mumbai 12
Tokyo 6
dtype: int64
具有更合适的形状,其他操作也变得更加容易。 例如,要查找每天的最高温度,我们可以简单地提供一个附加的轴参数:
>>> pv.max(axis=1)
date
2000-01-03 12
2000-01-04 9
2000-01-05 8
dtype: int64
作为最后的主题,我们将研究通过聚合获得精简数据视图的方法。 pandas 内置了许多聚合功能。 我们已经在第 3 章,“使用 Pandas”进行数据分析中看到了describe
函数。 这也适用于部分数据。 我们再次从一些人工数据开始,其中包含有关每个城市和日期的日照小时数的度量:
>>> df.head()
country city date hours
0 Germany Hamburg 2015-06-01 8
1 Germany Hamburg 2015-06-02 10
2 Germany Hamburg 2015-06-03 9
3 Germany Hamburg 2015-06-04 7
4 Germany Hamburg 2015-06-05 3
要查看每个city
的摘要,我们对分组的数据集使用describe
函数:
>>> df.groupby("city").describe()
hours
city
Berlin count 10.000000
mean 6.000000
std 3.741657
min 0.000000
25% 4.000000
50% 6.000000
75% 9.750000
max 10.000000
Birmingham count 10.000000
mean 5.100000
std 2.078995
min 2.000000
25% 4.000000
50% 5.500000
75% 6.750000
max 8.000000
在某些数据集上,按多个属性分组可能很有用。 通过传递两个列名称,我们可以大致了解每个国家和日期的晴天:
>>> df.groupby(["country", "date"]).describe()
hourscountry date
France 2015-06-01 count 5.000000
mean 6.200000
std 1.095445
min 5.000000
25% 5.000000
50% 7.000000
75% 7.000000
max 7.000000
2015-06-02 count 5.000000
mean 3.600000
std 3.577709
min 0.000000
25% 0.000000
50% 4.000000
75% 6.000000
max 8.000000
UK 2015-06-07 std 3.872983
min 0.000000
25% 2.000000
50% 6.000000
75% 8.000000
max 9.000000
我们也可以计算单个统计信息:
>>> df.groupby("city").mean()
hours
cityBerlin 6.0
Birmingham 5.1
Bordeax 4.7
Edinburgh 7.5
Frankfurt 5.8
Glasgow 4.8
Hamburg 5.5
Leipzig 5.0
London 4.8
Lyon 5.0
Manchester 5.2
Marseille 6.2
Munich 6.6
Nice 3.9
Paris 6.3
最后,我们可以使用agg
方法定义要应用于组的任何函数。 前面的代码可能是用agg
编写的,如下所示:
>>> df.groupby("city").agg(np.mean)
hours
city
Berlin 6.0
Birmingham 5.1
Bordeax 4.7
Edinburgh 7.5
Frankfurt 5.8
Glasgow 4.8
...
但是任意功能都是可能的。 在最后一个示例中,我们定义一个custom
函数,该函数获取一系列对象的输入并计算最小元素和最大元素之间的差:
>>> df.groupby("city").agg(lambda s: abs(min(s) - max(s)))
hours
city
Berlin 10
Birmingham 6
Bordeax 10
Edinburgh 8
Frankfurt 9
Glasgow 10
Hamburg 10
Leipzig 9
London 10
Lyon 8
Manchester 10
Marseille 10
Munich 9
Nice 10
Paris 9
数据探索期间的一种典型工作流程如下:
-
您找到要用于分组数据的条件。 也许您拥有该大陆以及每个国家/地区的 GDP 数据,并且想问有关这些大陆的问题。 这些问题通常会带来一些功能应用-您可能需要计算每个大陆的平均 GDP。 最后,您想要存储此数据以在新的数据结构中进行进一步处理。
-
我们在这里使用一个更简单的示例。 想象一下有关每天和城市晴天小时数的一些虚构的天气数据:
>>> df date city value 0 2000-01-03 London 6 1 2000-01-04 London 3 2 2000-01-05 London 4 3 2000-01-03 Mexico 3 4 2000-01-04 Mexico 9 5 2000-01-05 Mexico 8 6 2000-01-03 Mumbai 12 7 2000-01-04 Mumbai 9 8 2000-01-05 Mumbai 8 9 2000-01-03 Tokyo 5 10 2000-01-04 Tokyo 5 11 2000-01-05 Tokyo 6
groups
属性返回一个字典,其中包含唯一组和相应的值作为轴标签:>>> df.groupby("city").groups {'London': [0, 1, 2], 'Mexico': [3, 4, 5], 'Mumbai': [6, 7, 8], 'Tokyo': [9, 10, 11]}
-
尽管的结果是 GroupBy 对象,而不是 DataFrame,但我们可以使用常规的索引符号来引用列:
>>> grouped = df.groupby(["city", "value"]) >>> grouped["value"].max() city London 6 Mexico 9 Mumbai 12 Tokyo 6 Name: value, dtype: int64 >>> grouped["value"].sum() city London 13 Mexico 20 Mumbai 29 Tokyo 16 Name: value, dtype: int64
-
根据我们的数据集,我们看到孟买似乎是一个阳光明媚的城市。 实现上述代码的另一种方法(更详细)是:
>>> df['value'].groupby(df['city']).sum() city London 13 Mexico 20 Mumbai 29 Tokyo 16 Name: value, dtype: int64