Python-数据分析入门指南-二-

91 阅读1小时+

Python 数据分析入门指南(二)

原文:Get Started with Python Data Analysis

协议:CC BY-NC-SA 4.0

五、时间序列

时间序列通常由一系列数据点组成,这些数据点来自随时间进行的测量。这种数据非常常见,出现在许多领域。

企业高管对股票价格、商品和服务价格或月销售额感兴趣。气象学家一天测量几次温度,并记录下降雨量、湿度、风向和风力。神经科医生可以使用脑电图来测量大脑沿头皮的电活动。一个社会学家可以利用竞选捐款数据来了解政党及其支持者,并将这些见解作为论证的辅助手段。时间序列数据的更多例子几乎可以无穷无尽地列举出来。

时间序列引物

一般来说,时间序列有两个目的。首先,它们帮助我们了解生成数据的底层过程。另一方面,我们希望能够使用现有数据预测相同或相关系列的未来值。当我们测量温度、降水或风时,我们希望更多地了解更复杂的事情,例如天气或一个地区的气候,以及各种因素如何相互作用。同时,我们可能对天气预报感兴趣。

在这一章中,我们将探索 Pandas 的时间序列能力。除了强大的核心数据结构——系列和数据框架——Pandas 还附带了处理时间相关数据的辅助功能。凭借其广泛的内置优化,Pandas 能够轻松处理包含数百万个数据点的大型时间序列。

我们将逐步接近时间序列,从日期和时间对象的基本构件开始。

处理日期和时间对象

Python 支持标准库中日期时间模块中的日期和时间处理:

>>> import datetime
>>> datetime.datetime(2000, 1, 1)
datetime.datetime(2000, 1, 1, 0, 0)

有时,日期是作为字符串给出或预期的,因此从字符串到字符串的转换是必要的,这分别通过两个函数实现:strptimestrftime:

>>> datetime.datetime.strptime("2000/1/1", "%Y/%m/%d")
datetime.datetime(2000, 1, 1, 0, 0)
>>> datetime.datetime(2000, 1, 1, 0, 0).strftime("%Y%m%d")
'20000101'

现实世界的数据通常有各种各样的形状,如果我们不需要记住解析时指定的确切日期格式,那就太好了。谢天谢地,Pandas 在处理代表日期或时间的字符串时,消除了很多摩擦。这些辅助功能之一是to_datetime:

>>> import pandas as pd
>>> import numpy as np
>>> pd.to_datetime("4th of July")
Timestamp('2015-07-04 
>>> pd.to_datetime("13.01.2000")
Timestamp('2000-01-13 00:00:00')
>>> pd.to_datetime("7/8/2000")
Timestamp('2000-07-08 00:00:00')

最后一个可以指 8 月 7 日或 7 月 8 日,视地区而定。为了消除这种情况的歧义,可以给to_datetime传递一个关键字参数dayfirst:

>>> pd.to_datetime("7/8/2000", dayfirst=True)
Timestamp('2000-08-07 00:00:00')

时间戳对象可以被视为 Pandas 版本的datetime对象,事实上,Timestamp类是datetime的子类:

>>> issubclass(pd.Timestamp, datetime.datetime)
True

这意味着它们在许多情况下可以互换使用:

>>> ts = pd.to_datetime(946684800000000000)
>>> ts.year, ts.month, ts.day, ts.weekday()
(2000, 1, 1, 5)

时间戳对象是 Pandas 时间序列功能的重要组成部分,因为时间戳是DateTimeIndex对象的构造块:

>>> index = [pd.Timestamp("2000-01-01"),
 pd.Timestamp("2000-01-02"),
 pd.Timestamp("2000-01-03")]
>>> ts = pd.Series(np.random.randn(len(index)), index=index)
>>> ts
2000-01-01    0.731897
2000-01-02    0.761540
2000-01-03   -1.316866
dtype: float64
>>> ts.indexDatetime
Index(['2000-01-01', '2000-01-02', '2000-01-03'],
dtype='datetime64[ns]', freq=None, tz=None)

这里有几点需要注意:我们创建一个时间戳对象列表,并将其作为索引传递给系列构造器。这个时间戳列表被动态转换成DatetimeIndex。如果我们只通过了日期字符串,我们就不会得到一个DatetimeIndex,只是一个index:

>>> ts = pd.Series(np.random.randn(len(index)), index=[
 "2000-01-01", "2000-01-02", "2000-01-03"])
>>> ts.index
Index([u'2000-01-01', u'2000-01-02', u'2000-01-03'], dtype='object')

然而,to_datetime函数足够灵活,如果我们只有一个日期字符串列表,那么它会有所帮助:

>>> index = pd.to_datetime(["2000-01-01", "2000-01-02", "2000-01-03"])
>>> ts = pd.Series(np.random.randn(len(index)), index=index)
>>> ts.index
DatetimeIndex(['2000-01-01', '2000-01-02', '2000-01-03'], dtype='datetime64[ns]', freq=None, tz=None))

另外需要注意的是,虽然我们有一个DatetimeIndex,但是freqtz属性都是None。我们将在本章后面学习这两种属性的效用。

借助to_datetime我们能够将各种字符串甚至字符串列表转换成时间戳或DatetimeIndex对象。有时,我们没有明确地得到关于一个序列的所有信息,我们必须自己生成固定间隔的时间戳序列。

Pandas 为这个任务提供了另一个巨大的实用功能:date_range

date_range功能有助于在开始日期和结束日期之间生成固定频率的datetime索引。还可以指定开始或结束日期以及要生成的时间戳数量。

频率可由freq参数指定,该参数支持多个偏移。您可以使用典型的时间间隔,如小时、分钟和秒钟:

>>> pd.date_range(start="2000-01-01", periods=3, freq='H')
DatetimeIndex(['2000-01-01 00:00:00', '2000-01-01 01:00:00', '2000-01-01 02:00:00'], dtype='datetime64[ns]', freq='H', tz=None)
>>> pd.date_range(start="2000-01-01", periods=3, freq='T')
DatetimeIndex(['2000-01-01 00:00:00', '2000-01-01 00:01:00', '2000-01-01 00:02:00'], dtype='datetime64[ns]', freq='T', tz=None)
>>> pd.date_range(start="2000-01-01", periods=3, freq='S')
DatetimeIndex(['2000-01-01 00:00:00', '2000-01-01 00:00:01', '2000-01-01 00:00:02'], dtype='datetime64[ns]', freq='S', tz=None)

freq属性允许我们指定多个选项。Pandas 已经成功地应用于金融和经济领域,不仅仅是因为处理商务约会非常简单。例如,要获得千禧年前三个工作日的指数,可以使用B偏移别名:

>>> pd.date_range(start="2000-01-01", periods=3, freq='B')
DatetimeIndex(['2000-01-03', '2000-01-04', '2000-01-05'], dtype='datetime64[ns]', freq='B', tz=None)

下表显示了可用的偏移别名,也可以在 Pandas 文档下的时间序列中查找:

|

别名

|

描述

| | --- | --- | | B | 工作日频率 | | C | 自定义工作日频率 | | D | 日历日频率 | | W | 每周频率 | | M | 月末频率 | | 医学学士 | 营业月末频率 | | 大陆弹道导弹(Continental Ballistic Missile) | 自定义业务月末频率 | | 女士 | 月份开始频率 | | BachelorofMarineScience 海洋科学学士 | 营业月开始频率 | | CBMS | 自定义业务月开始频率 | | Q | 四分之一结束频率 | | 贝克勒尔 | 业务季度频率 | | QS | 四分之一开始频率 | | BQS | 业务季度开始频率 | | A | 年终频率 | | 钡 | 营业年度结束频率 | | 如同 | 年度开始频率 | | 停下 | 业务年度开始频率 | | 钅波 | 营业时间频率 | | H | 每小时频率 | | T | 微小频率 | | S | 其次是频率 | | L | 毫秒 | | U | 微秒 | | 普通 | 纳秒 |

此外,偏移别名也可以组合使用。这里,我们生成一个datetime索引,包含五个元素,每一个相隔一天、一小时、一分钟和一秒钟:

>>> pd.date_range(start="2000-01-01", periods=5, freq='1D1h1min10s')
DatetimeIndex(['2000-01-01 00:00:00', '2000-01-02 01:01:10', '2000-01-03 02:02:20', '2000-01-04 03:03:30', '2000-01-05 04:04:40'], dtype='datetime64[ns]', freq='90070S', tz=None)

如果我们想在每 12 个小时的工作时间内索引数据,默认情况下从上午 9 点开始,下午 5 点结束,我们只需在BH别名前加上前缀:

>>> pd.date_range(start="2000-01-01", periods=5, freq='12BH')
DatetimeIndex(['2000-01-03 09:00:00', '2000-01-04 13:00:00', '2000-01-06 09:00:00', '2000-01-07 13:00:00', '2000-01-11 09:00:00'], dtype='datetime64[ns]', freq='12BH', tz=None)

业务时间的自定义定义也是可能的:

>>> ts.index
DatetimeIndex(['2000-01-01', '2000-01-02', '2000-01-03'], dtype='datetime64[ns]', freq=None, tz=None)

我们也可以使用这个定制的工作时间来构建索引:

>>> pd.date_range(start="2000-01-01", periods=5, freq=12 * bh)
DatetimeIndex(['2000-01-03 07:00:00', '2000-01-03 19:00:00', '2000-01-04 07:00:00', '2000-01-04 19:00:00', '2000-01-05 07:00:00', '2000-01-05 19:00:00', '2000-01-06 07:00:00'], dtype='datetime64[ns]', freq='12BH', tz=None)

一些频率允许我们指定锚定后缀,这允许我们表达间隔,例如每个星期五或每月的第二个星期二:

>>> pd.date_range(start="2000-01-01", periods=5, freq='W-FRI')
DatetimeIndex(['2000-01-07', '2000-01-14', '2000-01-21', '2000-01-28', '2000-02-04'], dtype='datetime64[ns]', freq='W-FRI', tz=None)
>>> pd.date_range(start="2000-01-01", periods=5, freq='WOM-2TUE')
DatetimeIndex(['2000-01-11', '2000-02-08', '2000-03-14', '2000-04-11', '2000-05-09'], dtype='datetime64[ns]', freq='WOM-2TUE', tz=None)

最后,我们可以合并不同频率的各种指标。可能性是无穷的。我们仅举一个例子,其中我们结合了两个指数——每个指数都超过十年——一个指向一年中的每个第一个工作日,一个指向二月的最后一天:

>>> s = pd.date_range(start="2000-01-01", periods=10, freq='BAS-JAN')
>>> t = pd.date_range(start="2000-01-01", periods=10, freq='A-FEB')
>>> s.union(t)
DatetimeIndex(['2000-01-03', '2000-02-29', '2001-01-01', '2001-02-28', '2002-01-01', '2002-02-28', '2003-01-01', '2003-02-28','2004-01-01', '2004-02-29', '2005-01-03', '2005-02-28', '2006-01-02', '2006-02-28', '2007-01-01', '2007-02-28','2008-01-01', '2008-02-29', '2009-01-01', '2009-02-28'], dtype='datetime64[ns]', freq=None, tz=None)

我们看到,2000 年和 2005 年不是从一个工作日开始的,2000 年、2004 年和 2008 年是闰年。

到目前为止,我们已经看到了两个强大的功能,to_datetimedate_range。现在,我们想深入时间序列,首先展示如何创建和绘制只有几行的时间序列数据。在本节的剩余部分,我们将展示访问和切片时间序列数据的各种方法。

Pandas 的时间序列数据很容易上手。可以创建一个随机游走,并绘制成几行:

>>> index = pd.date_range(start='2000-01-01', periods=200, freq='B')
>>> ts = pd.Series(np.random.randn(len(index)), index=index)
>>> walk = ts.cumsum()
>>> walk.plot()

下图显示了该图的可能输出:

Working with date and time objects

与通常的系列对象一样,您可以选择零件并分割索引:

>>> ts.head()
2000-01-03    1.464142
2000-01-04    0.103077
2000-01-05    0.762656
2000-01-06    1.157041
2000-01-07   -0.427284
Freq: B, dtype: float64
>>> ts[0]
1.4641415817112928
>>> ts[1:3]
2000-01-04    0.103077
2000-01-05    0.762656

我们可以使用日期字符串作为键,尽管我们的系列有一个DatetimeIndex:

>>> ts['2000-01-03']
1.4641415817112928

即使DatetimeIndex是由时间戳对象组成的,我们也可以使用datetime对象作为密钥:

>>> ts[datetime.datetime(2000, 1, 3)]
1.4641415817112928

访问类似于字典或列表中的查找,但功能更强大。例如,我们可以用字符串甚至混合对象进行切片:

>>> ts['2000-01-03':'2000-01-05']
2000-01-03    1.464142
2000-01-04    0.103077
2000-01-05    0.762656
Freq: B, dtype: float64
>>> ts['2000-01-03':datetime.datetime(2000, 1, 5)]
2000-01-03    1.464142
2000-01-04    0.103077
2000-01-05    0.762656
Freq: B, dtype: float64
>>> ts['2000-01-03':datetime.date(2000, 1, 5)]
2000-01-03   -0.807669
2000-01-04    0.029802
2000-01-05   -0.434855
Freq: B, dtype: float64 

甚至可以使用部分字符串来选择条目组。如果我们只对二月感兴趣,我们可以简单地写道:

>>> ts['2000-02']
2000-02-01    0.277544
2000-02-02   -0.844352
2000-02-03   -1.900688
2000-02-04   -0.120010
2000-02-07   -0.465916
2000-02-08   -0.575722
2000-02-09    0.426153
2000-02-10    0.720124
2000-02-11    0.213050
2000-02-14   -0.604096
2000-02-15   -1.275345
2000-02-16   -0.708486
2000-02-17   -0.262574
2000-02-18    1.898234
2000-02-21    0.772746
2000-02-22    1.142317
2000-02-23   -1.461767
2000-02-24   -2.746059
2000-02-25   -0.608201
2000-02-28    0.513832
2000-02-29   -0.132000

查看从 3 月到 5 月的所有条目,包括:

>>> ts['2000-03':'2000-05']
2000-03-01    0.528070
2000-03-02    0.200661
 ...
2000-05-30    1.206963
2000-05-31    0.230351
Freq: B, dtype: float64 

时间序列可以在时间上向前或向后移动。索引保持不变,值移动:

>>> small_ts = ts['2000-02-01':'2000-02-05']
>>> small_ts
2000-02-01    0.277544
2000-02-02   -0.844352
2000-02-03   -1.900688
2000-02-04   -0.120010
Freq: B, dtype: float64
>>> small_ts.shift(2)
2000-02-01         NaN
2000-02-02         NaN
2000-02-03    0.277544
2000-02-04   -0.844352
Freq: B, dtype: float64

为了在时间上向后移动,我们简单地使用负值:

>>> small_ts.shift(-2)
2000-02-01   -1.900688
2000-02-02   -0.120010
2000-02-03         NaN
2000-02-04         NaN
Freq: B, dtype: float64

重新采样时间序列

重采样描述时间序列数据的频率转换过程。在各种情况下,这是一种有用的技术,因为它通过将数据分组和聚合来促进理解。有可能根据每日温度数据创建一个新的时间序列,显示每周或每月的平均温度。另一方面,现实世界的数据可能不是以统一的时间间隔获取的,需要将观测值映射到统一的时间间隔,或者填充某些时间点的缺失值。这是重采样的两个主要使用方向:宁滨和聚集,以及填充缺失的数据。下采样和上采样也发生在其他领域,例如数字信号处理。在那里,下采样的过程通常被称为抽取,并执行采样率的降低。逆过程称为 插值,采样速率增加。我们将从数据分析的角度来看两个方向。

下采样时间序列数据

下采样减少数据中的样本数量。在这个缩减过程中,我们能够在数据点上应用聚合。让我们想象一个繁忙的机场,每小时有成千上万的人经过。机场管理局在主要区域安装了一个访客柜台,以便准确了解他们的机场有多繁忙。

他们每分钟都在从计数器接收数据。以下是一天的假设测量值,从 08:00 开始,600 分钟后的 18:00 结束:

>>> rng = pd.date_range('4/29/2015 8:00', periods=600, freq='T')
>>> ts = pd.Series(np.random.randint(0, 100, len(rng)), index=rng)
>>> ts.head()
2015-04-29 08:00:00     9
2015-04-29 08:01:00    60
2015-04-29 08:02:00    65
2015-04-29 08:03:00    25
2015-04-29 08:04:00    19

为了更好地了解一天的情况,我们可以将这个时间序列下采样到更大的间隔,例如 10 分钟。我们也可以选择聚合函数。默认聚合是取所有值并计算平均值:

>>> ts.resample('10min').head()
2015-04-29 08:00:00    49.1
2015-04-29 08:10:00    56.0
2015-04-29 08:20:00    42.0
2015-04-29 08:30:00    51.9
2015-04-29 08:40:00    59.0
Freq: 10T, dtype: float64

在我们的机场示例中,我们还对值的总和感兴趣,即给定时间范围内的游客总数。我们可以通过向how参数传递一个函数或函数名来选择聚合函数:

>>> ts.resample('10min', how='sum').head()
2015-04-29 08:00:00    442
2015-04-29 08:10:00    409
2015-04-29 08:20:00    532
2015-04-29 08:30:00    433
2015-04-29 08:40:00    470
Freq: 10T, dtype: int64

或者,我们可以通过重新采样到每小时一次的间隔来进一步缩短采样间隔:

>>> ts.resample('1h', how='sum').head()
2015-04-29 08:00:00    2745
2015-04-29 09:00:00    2897
2015-04-29 10:00:00    3088
2015-04-29 11:00:00    2616
2015-04-29 12:00:00    2691
Freq: H, dtype: int64

我们也可以要求其他东西。例如,一小时内通过我们机场的最大人数是多少:

>>> ts.resample('1h', how='max').head()
2015-04-29 08:00:00    97
2015-04-29 09:00:00    98
2015-04-29 10:00:00    99
2015-04-29 11:00:00    98
2015-04-29 12:00:00    99
Freq: H, dtype: int64

或者,如果我们对更不寻常的度量感兴趣,我们可以定义一个自定义函数。例如,我们可能有兴趣为每个小时选择一个随机样本:

>>> import random
>>> ts.resample('1h', how=lambda m: random.choice(m)).head()
2015-04-29 08:00:00    28
2015-04-29 09:00:00    14
2015-04-29 10:00:00    68
2015-04-29 11:00:00    31
2015-04-29 12:00:00     5 

如果通过字符串指定函数,Pandas 会使用高度优化的版本。

可以作为how参数的内置函数有:summeanstd, semmaxminmedianfirstlastohlcohlc指标在金融界很流行。它代表开-高-低-关。OHLC 图表是说明金融工具价格随时间变化的典型方法。

虽然在我们的机场,这个指标可能没有那么有价值,但我们仍然可以计算出来:

>>> ts.resample('1h', how='ohlc').head()
 open  high  low  close
2015-04-29 08:00:00     9    97    0     14
2015-04-29 09:00:00    68    98    3     12
2015-04-29 10:00:00    71    99    1      1
2015-04-29 11:00:00    59    98    0      4
2015-04-29 12:00:00    56    99    3
 55

对时间序列数据进行上采样

在上采样中,时间序列的频率增加。因此,我们的样本点比数据点多。其中一个主要问题是如何解释我们没有度量的系列中的条目。

让我们从一天中每小时的数据开始:

>>> rng = pd.date_range('4/29/2015 8:00', periods=10, freq='H')
>>> ts = pd.Series(np.random.randint(0, 100, len(rng)), index=rng)
>>> ts.head()
2015-04-29 08:00:00    30
2015-04-29 09:00:00    27
2015-04-29 10:00:00    54
2015-04-29 11:00:00     9
2015-04-29 12:00:00    48
Freq: H, dtype: int64

如果我们将向上采样到每 15 分钟采集一次的数据点,我们的时间序列将扩展为NaN值:

>>> ts.resample('15min')
>>> ts.head()
2015-04-29 08:00:00    30
2015-04-29 08:15:00   NaN
2015-04-29 08:30:00   NaN
2015-04-29 08:45:00   NaN
2015-04-29 09:00:00    27

处理缺失值的方法有多种,可以通过fill_method关键字参数控制重新采样。值可以向前或向后填充:

>>> ts.resample('15min', fill_method='ffill').head()
2015-04-29 08:00:00    30
2015-04-29 08:15:00    30
2015-04-29 08:30:00    30
2015-04-29 08:45:00    30
2015-04-29 09:00:00    27
Freq: 15T, dtype: int64
>>> ts.resample('15min', fill_method='bfill').head()
2015-04-29 08:00:00    30
2015-04-29 08:15:00    27
2015-04-29 08:30:00    27
2015-04-29 08:45:00    27
2015-04-29 09:00:00    27

使用limit参数,可以控制要填充的缺失值的数量:

>>> ts.resample('15min', fill_method='ffill', limit=2).head()
2015-04-29 08:00:00    30
2015-04-29 08:15:00    30
2015-04-29 08:30:00    30
2015-04-29 08:45:00   NaN
2015-04-29 09:00:00    27
Freq: 15T, dtype: float64

如果要在重采样过程中调整标签,可以使用loffset关键字参数:

>>> ts.resample('15min', fill_method='ffill', limit=2, loffset='5min').head()
2015-04-29 08:05:00    30
2015-04-29 08:20:00    30
2015-04-29 08:35:00    30
2015-04-29 08:50:00   NaN
2015-04-29 09:05:00    27
Freq: 15T, dtype: float64

还有另一种方式填写缺失值。我们可以使用一种算法来构建新的数据点,以某种方式适合现有的点,以某种方式定义。这个过程叫做插值。

我们可以让 Pandas 为我们插入一个时间序列:

>>> tsx = ts.resample('15min')
>>> tsx.interpolate().head()
2015-04-29 08:00:00    30.00
2015-04-29 08:15:00    29.25
2015-04-29 08:30:00    28.50
2015-04-29 08:45:00    27.75
2015-04-29 09:00:00    27.00
Freq: 15T, dtype: float64

我们看到默认的interpolate方法——线性插值——正在运行。Pandas 在两个现存点之间呈现线性关系。

Pandas 支持十几个interpolation功能,其中一些需要安装scipy库。我们不会在本章介绍interpolation方法,但我们鼓励您自己探索各种方法。正确的interpolation方法将取决于您的应用要求。

时区处理

虽然默认情况下,Pandas 对象不知道时区,但许多现实世界的应用程序会使用时区。和使用时间一样,时区也不是小事:你知道哪些国家有夏令时吗?你知道这些国家的时区是什么时候转换的吗?令人欣慰的是,Pandas 基于两个广受欢迎且经过验证的实用程序库的时区功能来处理时间和日期:pytzdateutil:

>>> t = pd.Timestamp('2000-01-01')
>>> t.tz is None
True

要提供时区信息,可以使用tz关键字参数:

>>> t = pd.Timestamp('2000-01-01', tz='Europe/Berlin')
>>> t.tz
<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>

这同样适用于ranges:

>>> rng = pd.date_range('1/1/2000 00:00', periods=10, freq='D', tz='Europe/London')
>>> rng
DatetimeIndex(['2000-01-01', '2000-01-02', '2000-01-03', '2000-01-04', '2000-01-05', '2000-01-06', '2000-01-07', '2000-01-08','2000-01-09', '2000-01-10'], dtype='datetime64[ns]', freq='D', tz='Europe/London')

时区对象也可以预先构建:

>>> import pytz
>>> tz = pytz.timezone('Europe/London')
>>> rng = pd.date_range('1/1/2000 00:00', periods=10, freq='D', tz=tz)
>>> rng
DatetimeIndex(['2000-01-01', '2000-01-02', '2000-01-03', '2000-01-04', '2000-01-05', '2000-01-06', '2000-01-07', '2000-01-08', '2000-01-09', '2000-01-10'], dtype='datetime64[ns]', freq='D', tz='Europe/London')

有时,您已经有一个时区不知道的时间序列对象,您想让时区知道。tz_localize功能有助于在时区感知对象和时区不感知对象之间切换:

>>> rng = pd.date_range('1/1/2000 00:00', periods=10, freq='D')
>>> ts = pd.Series(np.random.randn(len(rng)), rng)
>>> ts.index.tz is None
True
>>> ts_utc = ts.tz_localize('UTC')
>>> ts_utc.index.tz
<UTC>

要将时区感知对象移动到其他时区,可以使用tz_convert方法:

>>> ts_utc.tz_convert('Europe/Berlin').index.tz
<DstTzInfo 'Europe/Berlin' LMT+0:53:00 STD>

最后,要从对象中分离任何时区信息,可以将None传递给tz_converttz_localize:

>>> ts_utc.tz_convert(None).index.tz is None
True
>>> ts_utc.tz_localize(None).index.tz
 is None
True

超时

除了作为DatetimeIndex构建模块的强大的时间戳对象,Pandas 0.15 中还引入了另一个有用的数据结构——时间增量。时间增量也可以作为指数的基础,在本例中为TimedeltaIndex

时间增量是以不同单位表示的时间差异。Pandas 中的Timedelta类是 Python 标准库中datetime.timedelta的子类。与其他 Pandas 数据结构一样,时间增量可以由多种输入构建:

>>> pd.Timedelta('1 days')
Timedelta('1 days 00:00:00')
>>> pd.Timedelta('-1 days 2 min 10s 3us')
Timedelta('-2 days +23:57:49.999997')
>>> pd.Timedelta(days=1,seconds=1)
Timedelta('1 days 00:00:01')

如您所料,Timedeltas允许基本运算:

>>> pd.Timedelta(days=1) + pd.Timedelta(seconds=1)
Timedelta('1 days 00:00:01')

to_datetime类似,有一个to_timedelta函数可以将字符串或字符串列表解析成 Timedelta 结构或TimedeltaIndices:

>>> pd.to_timedelta('20.1s')
Timedelta('0 days 00:00:20.100000')

我们可以创建一个timedeltas的索引来代替绝对日期。例如,想象一下从火山测量。我们可能想进行测量,但从给定的日期开始索引,例如最后一次喷发的日期。我们可以创建一个以过去七天为条目的timedelta索引:

>>> pd.to_timedelta(np.arange(7), unit='D')
TimedeltaIndex(['0 days', '1 days', '2 days', '3 days', '4 days', '5 days', '6 days'], dtype='timedelta64[ns]', freq=None)

然后,我们可以使用从上次喷发开始编制索引的时间序列数据。如果我们有许多火山爆发的测量数据(可能来自多个火山),我们将有一个指数,使这些数据的比较和分析更加容易。例如,我们可以问在火山爆发后的第三天到第五天之间是否有典型的模式。这个问题用DatetimeIndex来回答不是不可能的,但是TimedeltaIndex让这种探索变得方便多了。

时间序列标绘

Pandas 对绘图有很大的支持,时间序列数据也是如此。

作为第一个示例,让我们获取一些月度数据并绘制出来:

>>> rng = pd.date_range(start='2000', periods=120, freq='MS')
>>> ts = pd.Series(np.random.randint(-10, 10, size=len(rng)), rng).cumsum()
>>> ts.head()
2000-01-01    -4
2000-02-01    -6
2000-03-01   -16
2000-04-01   -26
2000-05-01   -24
Freq: MS, dtype: int64

由于 matplotlib 是在引擎盖下使用的,我们可以传递一个熟悉的参数来绘图,例如 c 表示颜色,或者 title 表示图表标题:

>>> ts.plot(c='k', title='Example time series')
>>> plt.show()

下图显示了一个示例时间序列图:

Time series plotting

我们可以在 2 年和 5 年内叠加一个总图:

>>> ts.resample('2A').plot(c='0.75', ls='--')
>>> ts.resample('5A').plot(c='0.25', ls='-.')

下图显示了重新采样的 2 年图:

Time series plotting

下图显示了 5 年的重采样图:

Time series plotting

我们也可以将这种图表传递给plot方法。plot方法的返回值是一个AxesSubplot,可以让我们自定义剧情的很多方面。这里我们将X轴上的标签值设置为时间序列中的年份值:

>>> plt.clf()
>>> tsx = ts.resample('1A')
>>> ax = tsx.plot(kind='bar', color='k')
>>> ax.set_xticklabels(tsx.index.year)

Time series plotting

让我们想象一下我们有四个我们想同时绘制的时间序列。我们生成一个由 1000 × 4 个随机值组成的矩阵,并将每一列视为一个独立的时间序列:

>>> plt.clf()
>>> ts = pd.Series(np.random.randn(1000), index=pd.date_range('1/1/2000', periods=1000))
>>> df = pd.DataFrame(np.random.randn(1000, 4), index=ts.index, columns=['A', 'B', 'C', 'D'])
>>> df = df.cumsum()>>> df.plot(color=['k', '0.75', '0.5', '0.25'], ls='--')

Time series plotting

总结

在本章中,我们展示了如何在 Pandas 中处理时间序列。我们介绍了两种索引类型DatetimeIndexTimedeltaIndex,并深入探讨了它们的构建模块。Pandas 自带多功能助手功能,可以消除解析各种格式的日期或生成固定频率序列的痛苦。对数据进行重采样有助于获得更为精简的数据,或者有助于将不同频率的各种数据集相互对齐。Pandas 的明确目标之一是使处理缺失数据变得容易,这也与上采样相关。

最后,我们展示了时间序列是如何可视化的。由于 matplotlib 和 Pandas 是天然的伙伴,我们发现我们也可以将之前关于 matplotlib 的知识重用到时间序列数据中。

在下一章中,我们将探索在文本文件和数据库中加载和存储数据的方法。

练习练习

练习 1 :找到一两个数据集的真实例子,可以合理地分配给以下几组:

  • 固定频率数据
  • 可变频率数据
  • 频率通常以秒为单位的数据
  • 频率以纳秒为单位的数据
  • 数据,其中TimedeltaIndex更可取

创建各种固定频率范围:

  • 2000 年 1 月 1 日凌晨 1 时至 2 时每分钟
  • 从 2000-01-01 开始,每周两小时
  • 2000 年每周六和周日的条目
  • 2000 年、2001 年和 2002 年,如果是工作日,每个月的每个星期一都有一个条目

六、与数据库交互

数据分析从数据开始。因此,使用易于设置、操作且数据访问本身不会成为问题的数据存储系统是有益的。简而言之,我们希望拥有易于嵌入到我们的数据分析流程和工作流中的数据库系统。在本书中,我们主要关注数据库交互的 Python 方面,我们将学习如何将数据放入和取出 Pandas 数据结构。

有许多方法可以存储数据。在本章中,我们将学习与三个主要类别交互:文本格式、二进制格式和数据库。我们将重点介绍两种存储解决方案,MongoDB 和 Redis。MongoDB 是一个面向文档的数据库,这很容易开始,因为我们可以存储 JSON 文档,并且不需要预先定义模式。Redis 是一种流行的内存数据结构存储,在此基础上可以构建许多应用程序。使用 Redis 作为快速键值存储是可能的,但是 Redis 也支持列表、集合、散列、位数组,甚至像 HyperLogLog 这样的高级数据结构。

与文本格式的数据交互

文本是一种很好的媒介,也是一种简单的信息交流方式。以下陈述摘自道格·麦克洛伊的名言:编写程序来处理文本流,因为那是通用接口。

在本节中,我们将开始从文本文件读取数据和向文本文件写入数据。

从文本格式读取数据

通常,一个系统的原始数据日志存储在多个文本文件中,随着时间的推移,这些文本文件会积累大量信息。谢天谢地,在 Python 中与这类文件交互很简单。

Pandas 支持许多将数据从文本文件读入数据框对象的功能。最简单的一个就是read_csv()功能。让我们从一个小示例文件开始:

$ cat example_data/ex_06-01.txt
Name,age,major_id,sex,hometown
Nam,7,1,male,hcm
Mai,11,1,female,hcm
Lan,25,3,female,hn
Hung,42,3,male,tn
Nghia,26,3,male,dn
Vinh,39,3,male,vl
Hong,28,4,female,dn

型式

cat是 Unix shell 命令,可用于将文件内容打印到屏幕上。

在上面的示例文件中,每列由逗号分隔,第一行是标题行,包含列名。要将数据文件读入 DataFrame 对象,我们键入以下命令:

>>> df_ex1 = pd.read_csv('example_data/ex_06-01.txt')
>>> df_ex1
 Name  age  major_id     sex hometown
0    Nam    7         1    male      hcm
1    Mai   11         1  female      hcm
2    Lan   25         3  female       hn
3   Hung   42         3    male       tn
4  Nghia   26         3    male       dn
5   Vinh   39         3    male       vl
6   Hong   28         4  female       dn

我们看到read_csv函数使用逗号作为文本文件中列之间的默认分隔符,第一行自动用作列的标题。如果我们想更改此设置,我们可以使用sep参数更改分隔符号,并在示例文件没有标题行的情况下设置header=None

请参见下面的示例:

$ cat example_data/ex_06-02.txt
Nam     7       1       male    hcm
Mai     11      1       female  hcm
Lan     25      3       female  hn
Hung    42      3       male    tn
Nghia   26      3       male    dn
Vinh    39      3       male    vl
Hong    28      4       female  dn

>>> df_ex2 = pd.read_csv('example_data/ex_06-02.txt',
 sep = '\t', header=None)
>>> df_ex2
 0   1  2       3    4
0    Nam   7  1    male  hcm
1    Mai  11  1  female  hcm
2    Lan  25  3  female   hn
3   Hung  42  3    male   tn
4  Nghia  26  3    male   dn
5   Vinh  39  3    male   vl
6   Hong  28  4  female   dn

我们也可以使用等于所选行索引的header将特定行设置为标题行。同样,当我们想要使用数据文件中的任何一列作为 DataFrame 的列索引时,我们将index_col设置为该列的名称或索引。我们再用第二个数据文件example_data/ex_06-02.txt来说明这一点:

>>> df_ex3 = pd.read_csv('example_data/ex_06-02.txt',
 sep = '\t', header=None,
 index_col=0)
>>> df_ex3
 1  2       3    4
0
Nam     7  1    male  hcm
Mai    11  1  female  hcm
Lan    25  3  female   hn
Hung   42  3    male   tn
Nghia  26  3    male   dn
Vinh   39  3    male   vl
Hong   28  4  female   dn

除了这些参数,我们还有很多有用的参数可以帮助我们更有效地将数据文件加载到 Pandas 对象中。下表显示了一些常见参数:

|

参数

|

价值

|

描述

| | --- | --- | --- | | dtype | 列类型的类型名称或字典 | 设置数据或列的数据类型。默认情况下,它会尝试推断最合适的数据类型。 | | skiprows | 列表式或整数式 | 要跳过的行数(从 0 开始)。 | | na_values | 列表式或字典式,默认无 | 要识别为NA / NaN的值。如果 dict 被通过,这可以在每列的基础上设置。 | | true_values | 目录 | 要转换为布尔真值的值列表。 | | false_values | 目录 | 要转换为布尔假的值列表。 | | keep_default_na | Booldefault True | 如果存在na_values参数并且keep_default_naFalse,则默认的 NaN 值被忽略,否则它们被附加到 | | thousands | Strdefault None | 千位分隔符 | | nrows | Intdefault None | 限制从文件中读取的行数。 | | error_bad_lines | Booleandefault True | 如果设置为真,将返回一个数据帧,即使在解析过程中出现错误。 |

除了read_csv()功能,我们在 Pandas 中还有一些其他的解析功能:

|

功能

|

描述

| | --- | --- | | read_table | 将常规分隔文件读入数据框 | | read_fwf | 将固定宽度格式化行的表格读入数据框 | | read_clipboard | 从剪贴板中读取文本并传递到read_table。它对于从网页转换表格很有用 |

在某些情况下,我们无法使用这些函数自动解析磁盘中的数据文件。在这种情况下,我们也可以打开文件,通过阅读器进行迭代,标准库中的 CSV 模块支持:

$ cat example_data/ex_06-03.txt
Nam     7       1       male    hcm
Mai     11      1       female  hcm
Lan     25      3       female  hn
Hung    42      3       male    tn      single
Nghia   26      3       male    dn      single
Vinh    39      3       male    vl
Hong    28      4       female  dn

>>> import csv
>>> f = open('data/ex_06-03.txt')
>>> r = csv.reader(f, delimiter='\t')
>>> for line in r:
>>>    print(line)
['Nam', '7', '1', 'male', 'hcm']
['Mai', '11', '1', 'female', 'hcm']
['Lan', '25', '3', 'female', 'hn']
['Hung', '42', '3', 'male', 'tn', 'single']
['Nghia', '26', '3', 'male', 'dn', 'single']
['Vinh', '39', '3', 'male', 'vl']
['Hong', '28', '4', 'female', 'dn']

将数据写入文本格式

我们看到了如何将数据从文本文件加载到 Pandas 的数据结构中。现在,我们将学习如何将数据从程序的数据对象导出到文本文件。对应read_csv()功能,我们还有to_csv()功能,Pandas 支持。让我们看看下面的例子:

>>> df_ex3.to_csv('example_data/ex_06-02.out', sep = ';')

结果将如下所示:

$ cat example_data/ex_06-02.out
0;1;2;3;4
Nam;7;1;male;hcm
Mai;11;1;female;hcm
Lan;25;3;female;hn
Hung;42;3;male;tn
Nghia;26;3;male;dn
Vinh;39;3;male;vl
Hong;28;4;female;dn

如果我们想在将数据写入磁盘文件时跳过标题行或索引列,我们可以为标题和索引参数设置一个False值:

>>> import sys
>>> df_ex3.to_csv(sys.stdout, sep='\t',
 header=False, index=False)
7       1       male    hcm
11      1       female  hcm
25      3       female  hn
42      3       male    tn
26      3       male    dn
39      3       male    vl
28      4       female  dn

我们也可以通过在columns参数中指定数据帧的列的子集来将它们写入文件:

>>> df_ex3.to_csv(sys.stdout, columns=[3,1,4],
 header=False, sep='\t')
Nam     male    7       hcm
Mai     female  11      hcm
Lan     female  25      hn
Hung    male    42      tn
Nghia   male    26      dn
Vinh    male    39      vl
Hong    female  28      dn

有了 series 对象,我们可以使用相同的函数将数据写入文本文件,参数大多与上面相同。

与二进制格式的数据交互

我们可以用 pickle 模块读写 Python 对象的二进制序列化,可以在标准库中找到。如果您使用需要很长时间才能创建的对象,比如一些机器学习模型,对象序列化可能会很有用。通过酸洗这些对象,可以更快地访问该模型。它还允许您以标准化的方式分发 Python 对象。

Pandas 包括支持开箱腌制。相关的方法是read_pickle()to_pickle()功能,可以轻松地读写文件中的数据。这些方法将以 pickle 格式将数据写入磁盘,这是一种方便的短期存储格式:

>>> df_ex3.to_pickle('example_data/ex_06-03.out')
>>> pd.read_pickle('example_data/ex_06-03.out')
 1  2       3    4
0
Nam     7  1    male  hcm
Mai    11  1  female  hcm
Lan    25  3  female   hn
Hung   42  3    male   tn
Nghia  26  3    male   dn
Vinh   39  3    male   vl
Hong   28  4  female   dn

HDF5

HDF5 不是数据库,而是数据模型和文件格式。它适用于一写多读数据集。HDF5 文件包括两种对象:数据集和组,数据集是类似数组的数据集合,组是类似文件夹的容器,保存数据集和其他组。Python 中有一些与 HDF5 格式交互的接口,比如h5py使用大家熟悉的 NumPy 和 Python 构造,比如字典和 NumPy 数组语法。有了h5py,我们有了 HDF5 API 的高级接口,这有助于我们开始。但是,在本书中,我们将介绍另一个用于这种格式的库,称为 PyTables,它可以很好地处理 Pandas 对象:

>>> store = pd.HDFStore('hdf5_store.h5')
>>> store
<class 'pandas.io.pytables.HDFStore'>
File path: hdf5_store.h5
Empty

我们创建了一个空的 HDF5 文件,名为hdf5_store.h5。现在,我们可以向文件中写入数据,就像向dict添加键值对一样:

>>> store['ex3'] = df_ex3
>>> store['name'] = df_ex2[0]
>>> store['hometown'] = df_ex3[4]
>>> store
<class 'pandas.io.pytables.HDFStore'>
File path: hdf5_store.h5
/ex3                  frame        (shape->[7,4])
/hometown             series       (shape->[1])
/name                 series       (shape->[1])

存储在 HDF5 文件中的对象可以通过指定对象键来检索:

>>> store['name']
0      Nam
1      Mai
2      Lan
3     Hung
4    Nghia
5     Vinh
6     Hong
Name: 0, dtype: object

一旦我们完成了与 HDF5 文件的交互,我们就关闭它以释放文件句柄:

>>> store.close()
>>> store
<class 'pandas.io.pytables.HDFStore'>
File path: hdf5_store.h5
File is CLOSED

对于使用 HDF5 格式,还有其他受支持的有用功能。如果需要处理大量数据,您应该更详细地探索两个库-pytablesh5py

与 MongoDB 中的数据交互

许多应用程序需要比文本文件更强大的存储系统,这就是为什么许多应用程序使用数据库来存储数据。数据库有很多种,但有两大类:关系数据库,它支持一种称为 SQL 的标准声明性语言,以及所谓的 NoSQL 数据库,它们通常能够在没有预定义模式的情况下工作,并且数据实例更适合描述为文档,而不是行。

MongoDB 是一种 NoSQL 数据库,它将数据存储为文档,这些文档在集合中组合在一起。文档被表示为 JSON 对象。它存储数据的速度快、可扩展,查询数据也很灵活。要在 Python 中使用 MongoDB,我们需要导入pymongo包,并通过传递主机名和端口打开到数据库的连接。假设我们有一个 MongoDB 实例,运行在默认主机(localhost)和端口(27017)上:

>>> import pymongo
>>> conn = pymongo.MongoClient(host='localhost', port=27017)

如果我们不把任何参数放入pymongo.MongoClient()功能,它会自动使用默认的主机和端口。

在下一步中,我们将与 MongoDB 实例中的数据库进行交互。我们可以列出实例中可用的所有数据库:

>>> conn.database_names()
['local']
>>> lc = conn.local
>>> lc
Database(MongoClient('localhost', 27017), 'local')

上面的片段说我们的 MongoDB 实例只有一个数据库,名为‘local’。如果我们指向的数据库和集合不存在,MongoDB 将根据需要创建它们:

>>> db = conn.db
>>> db
Database(MongoClient('localhost', 27017), 'db')

每个数据库都包含文档组,称为集合。我们可以将它们理解为关系数据库中的表。要列出数据库中所有现有的集合,我们使用collection_names()函数:

>>> lc.collection_names()
['startup_log', 'system.indexes']
>>> db.collection_names()
[]

我们的db数据库还没有任何收藏。让我们创建一个名为person的集合,并将数据框对象中的数据插入其中:

>>> collection = db.person
>>> collection
Collection(Database(MongoClient('localhost', 27017), 'db'), 'person')
>>> # insert df_ex2 DataFrame into created collection
>>> import json
>>> records = json.load(df_ex2.T.to_json()).values()
>>> records
dict_values([{'2': 3, '3': 'male', '1': 39, '4': 'vl', '0': 'Vinh'}, {'2': 3, '3': 'male', '1': 26, '4': 'dn', '0': 'Nghia'}, {'2': 4, '3': 'female', '1': 28, '4': 'dn', '0': 'Hong'}, {'2': 3, '3': 'female', '1': 25, '4': 'hn', '0': 'Lan'}, {'2': 3, '3': 'male', '1': 42, '4': 'tn', '0': 'Hung'}, {'2': 1, '3':'male', '1': 7, '4': 'hcm', '0': 'Nam'}, {'2': 1, '3': 'female', '1': 11, '4': 'hcm', '0': 'Mai'}])
>>> collection.insert(records)
[ObjectId('557da218f21c761d7c176a40'),
 ObjectId('557da218f21c761d7c176a41'),
 ObjectId('557da218f21c761d7c176a42'),
 ObjectId('557da218f21c761d7c176a43'),
 ObjectId('557da218f21c761d7c176a44'),
 ObjectId('557da218f21c761d7c176a45'),
 ObjectId('557da218f21c761d7c176a46')]

df_ex2在加载到字典之前被转置并转换为 JSON 字符串。insert()函数从df_ex2接收我们创建的词典,并将其保存到集合中。

如果我们想要列出集合中的所有数据,我们可以执行以下命令:

>>> for cur in collection.find():
>>>     print(cur)
{'4': 'vl', '2': 3, '3': 'male', '1': 39, '_id': ObjectId('557da218f21c761d7c176
a40'), '0': 'Vinh'}
{'4': 'dn', '2': 3, '3': 'male', '1': 26, '_id': ObjectId('557da218f21c761d7c176
a41'), '0': 'Nghia'}
{'4': 'dn', '2': 4, '3': 'female', '1': 28, '_id': ObjectId('557da218f21c761d7c1
76a42'), '0': 'Hong'}
{'4': 'hn', '2': 3, '3': 'female', '1': 25, '_id': ObjectId('557da218f21c761d7c1
76a43'), '0': 'Lan'}
{'4': 'tn', '2': 3, '3': 'male', '1': 42, '_id': ObjectId('557da218f21c761d7c176
a44'), '0': 'Hung'}
{'4': 'hcm', '2': 1, '3': 'male', '1': 7, '_id': ObjectId('557da218f21c761d7c176
a45'), '0': 'Nam'}
{'4': 'hcm', '2': 1, '3': 'female', '1': 11, '_id': ObjectId('557da218f21c761d7c
176a46'), '0': 'Mai'}

如果我们想要在某些条件下从创建的集合中查询数据,我们可以使用find()函数,并传入描述我们想要检索的文档的字典。返回的结果是游标类型,它支持迭代器协议:

>>> cur = collection.find({'3' : 'male'})
>>> type(cur)
pymongo.cursor.Cursor
>>> result = pd.DataFrame(list(cur))
>>> result
 0   1  2     3    4                       _id
0   Vinh  39  3  male   vl  557da218f21c761d7c176a40
1  Nghia  26  3  male   dn  557da218f21c761d7c176a41
2   Hung  42  3  male   tn  557da218f21c761d7c176a44
3    Nam   7  1  male  hcm  557da218f21c761d7c176a45

有时候,我们希望删除 MongdoDB 中的数据。我们需要做的就是向集合上的remove()方法传递一个查询:

>>> # before removing data
>>> pd.DataFrame(list(collection.find()))
 0   1  2       3    4                       _id
0   Vinh  39  3    male   vl  557da218f21c761d7c176a40
1  Nghia  26  3    male   dn  557da218f21c761d7c176a41
2   Hong  28  4  female   dn  557da218f21c761d7c176a42
3    Lan  25  3  female   hn  557da218f21c761d7c176a43
4   Hung  42  3    male   tn  557da218f21c761d7c176a44
5    Nam   7  1    male  hcm  557da218f21c761d7c176a45
6    Mai  11  1  female  hcm  557da218f21c761d7c176a46

>>> # after removing records which have '2' column as 1 and '3' column as 'male'
>>> collection.remove({'2': 1, '3': 'male'})
{'n': 1, 'ok': 1}
>>> cur_all = collection.find();
>>> pd.DataFrame(list(cur_all))
 0   1  2       3    4                       _id
0   Vinh  39  3    male   vl  557da218f21c761d7c176a40
1  Nghia  26  3    male   dn  557da218f21c761d7c176a41
2   Hong  28  4  female   dn  557da218f21c761d7c176a42
3    Lan  25  3  female   hn  557da218f21c761d7c176a43
4   Hung  42  3    male   tn  557da218f21c761d7c176a44
5    Mai  11  1  female  hcm  557da218f21c761d7c176a46

我们逐步学习了如何在集合中插入、查询和删除数据。现在,我们将展示如何在 MongoDB 中更新集合中的现有数据:

>>> doc = collection.find_one({'1' : 42})
>>> doc['4'] = 'hcm'
>>> collection.save(doc)
ObjectId('557da218f21c761d7c176a44')
>>> pd.DataFrame(list(collection.find()))
 0   1  2       3    4                       _id
0   Vinh  39  3    male   vl  557da218f21c761d7c176a40
1  Nghia  26  3    male   dn  557da218f21c761d7c176a41
2   Hong  28  4  female   dn  557da218f21c761d7c176a42
3    Lan  25  3  female   hn  557da218f21c761d7c176a43
4   Hung  42  3    male  hcm  557da218f21c761d7c176a44
5    Mai  11  1  female  hcm  557da218f21c761d7c176a46

下表显示了为在 MongoDB 中操作文档提供快捷方式的方法:

|

更新方法

|

描述

| | --- | --- | | inc() | 递增数字字段 | | set() | 将某些字段设置为新值 | | unset() | 从文档中删除字段 | | push() | 将值附加到文档中的数组上 | | pushAll() | 将几个值追加到文档的数组中 | | addToSet() | 仅当数组不存在时,才将值添加到数组中 | | pop() | 移除数组的最后一个值 | | pull() | 从数组中移除所有出现的值 | | pullAll() | 从数组中移除任何值集的所有匹配项 | | rename() | 重命名字段 | | bit() | 通过按位运算更新值 |

与 Redis 中的数据交互

Redis 是一种高级的键值存储,其中的值可以是不同的类型:字符串、列表、集合、排序集合或散列。Redis 像 memcached 一样将数据存储在内存中,但它可以持久存储在磁盘上,不像 memcached 那样没有这样的选项。Redis 支持快速读写,大约每秒 100,000 次 set 或 get 操作。

要与 Redis 交互,我们需要将Redis-py模块安装到 Python 中,Python 在pypi上有,可以用pip安装:

$ pip install redis

现在,我们可以通过数据库服务器的主机和端口连接到 Redis。我们假设已经安装了一个 Redis 服务器,该服务器使用默认主机(localhost)和端口(6379)参数运行:

>>> import redis
>>> r = redis.StrictRedis(host='127.0.0.1', port=6379)
>>> r
StrictRedis<ConnectionPool<Connection<host=localhost,port=6379,db=0>>>

作为在 Redis 中存储数据的第一步,我们需要定义哪种数据结构适合我们的需求。在本节中,我们将介绍 Redis 中常用的四种数据结构:简单值、列表、集合和有序集。尽管数据以许多不同的数据结构存储在 Redis 中,但每个值都必须与一个键相关联。

简单值

这是 Redis 中最基本的一种价值。对于 Redis 中的每个键,我们还有一个可以有数据类型的值,比如字符串、整数或双精度。让我们从一个设置和获取 Redis 数据的例子开始:

>>> r.set('gender:An', 'male')
True
>>> r.get('gender:An')
b'male'

在这个例子中,我们希望将一个名为An的人的性别信息存储到 Redis 中。我们的关键是gender:An,我们的价值是male。它们都是一种字符串。

set()函数接收两个参数:键和值。第一个参数是键,第二个参数是值。如果我们想更新这个键的值,我们只需要再次调用函数并更改第二个参数的值。Redis 会自动更新它。

get()函数将检索我们的键的值,该值作为参数传递。在这种情况下,我们要获取关键gender:An的性别信息。

在第二个例子中,我们向您展示了另一种值类型,一个整数:

>>> r.set('visited_time:An', 12)
True
>>> r.get('visited_time:An')
b'12'
>>> r.incr('visited_time:An', 1)
13
>>> r.get('visited_time:An')
b'13'

我们看到了一个新的函数,incr(),用来将 key 的值增加一个给定量。如果我们的键不存在,RedisDB 将以给定的增量作为值创建该键。

列表

我们有一些方法来与 Redis 中的列表值进行交互。以下示例使用rpush()lrange()函数将列表数据放入数据库和从数据库获取列表数据:

>>> r.rpush('name_list', 'Tom')
1L
>>> r.rpush('name_list', 'John')
2L
>>> r.rpush('name_list', 'Mary')
3L
>>> r.rpush('name_list', 'Jan')
4L
>>> r.lrange('name_list', 0, -1)
[b'Tom', b'John', b'Mary', b'Jan']
>>> r.llen('name_list')
4
>>> r.lindex('name_list', 1)
b'John'

除了我们在示例中使用的rpush()lrange()函数之外,我们还想介绍另外两个函数。首先,llen()函数用于获取给定键在 Redis 中的列表长度。lindex()功能是检索列表项目的另一种方式。我们需要向函数传递两个参数:一个键和列表中项目的索引。下表列出了用 Redis 处理列表数据结构的其他一些强大功能:

|

功能

|

描述

| | --- | --- | | rpushx(name, value) | 如果名称存在,将值推到列表名称的尾部 | | rpop(name) | 移除并返回列表名称的最后一项 | | lset(name, index, value) | 将列表名称索引位置的项目设置为输入值 | | lpushx(name,value) | 如果名称存在,将值推送到列表名称的开头 | | lpop(name) | 移除并返回列表名称的第一项 |

设置

这个数据结构也类似于列表类型。但是,与列表相反,我们不能在集合中存储重复的值:

>>> r.sadd('country', 'USA')
1
>>> r.sadd('country', 'Italy')
1
>>> r.sadd('country', 'Singapore')
1
>>> r.sadd('country', 'Singapore')
0
>>> r.smembers('country')
{b'Italy', b'Singapore', b'USA'}
>>> r.srem('country', 'Singapore')
1
>>> r.smembers('country')
{b'Italy', b'USA'}

对应列表数据结构,我们还有很多功能可以获取、设置、更新或删除集合中的项目。它们列在支持的集合数据结构函数中,如下表所示:

|

功能

|

描述

| | --- | --- | | sadd(name, values) | 用键名向集合中添加值 | | scard(name) | 用键名返回集合中的元素数 | | smembers(name) | 用键名返回集合的所有成员 | | srem(name, values) | 用键名从集合中移除值 |

有序集

当我们将数据添加到名为分数的集合中时,有序集合数据结构具有额外的属性。有序集合将使用分数来确定集合中元素的顺序:

>>> r.zadd('person:A', 10, 'sub:Math')
1
>>> r.zadd('person:A', 7, 'sub:Bio')
1
>>> r.zadd('person:A', 8, 'sub:Chem')
1
>>> r.zrange('person:A', 0, -1)
[b'sub:Bio', b'sub:Chem', b'sub:Math']
>>> r.zrange('person:A', 0, -1, withscores=True)
[(b'sub:Bio', 7.0), (b'sub:Chem', 8.0), (b'sub:Math', 10.0)]

通过使用zrange(name, start, end)函数,我们可以从排序的集合中获得一个范围内的值,该范围位于默认情况下按升序排序的开始和结束分数之间。如果要改变way的排序方式,可以将desc参数设置为Truewithscore参数用于我们想要获得分数和返回值的情况。返回类型是一个(值、分数)对的列表,如您在上面的示例中所见。

有关有序集合的更多可用功能,请参见下表:

|

功能

|

描述

| | --- | --- | | zcard(name) | 用关键字名称返回排序集中的元素数量 | | zincrby(name, value, amount=1) | 将排序后的具有关键字名称的集合中的值的分数按数量递增 | | zrangebyscore(name, min, max, withscores=False, start=None, num=None) | 从排序集中返回一个值范围,关键字名称的得分在最小值和最大值之间。如果withscorestrue,则返回分数和数值。如果给定了开始和num,返回范围的一部分 | | zrank(name, value) | 返回一个从 0 开始的值,该值指示带键名的排序集中的值的等级 | | zrem(name, values) | 从具有关键字名称的排序集中移除成员值 |

总结

我们已经完成了在不同的常用存储机制中与数据交互的基础知识,从简单的存储机制(如文本文件)到更结构化的存储机制(如 HDF5),再到更复杂的数据存储系统(如 MongoDB 和 Redis)。最合适的存储类型将取决于您的用例。数据存储层技术的选择在数据处理系统的总体设计中起着重要的作用。有时,我们需要组合各种数据库系统来存储我们的数据,例如数据的复杂性、系统的性能或计算需求。

练习练习

  • 选择一组您选择的数据,并为其设计存储选项。考虑文本文件、HDF5、文档数据库和数据结构存储作为可能的持久选项。还要评估更新或删除一个特定项目有多困难(例如,通过某种度量,多少行代码)。哪种存储类型最容易设置?哪种存储类型支持最灵活的查询?
  • 第 3 章Pandas 数据分析中,我们看到可以用 Pandas 创建分级索引。举个例子,假设你有超过 100 万居民的每个城市的数据,我们有一个两级指数,所以我们可以处理单个城市,也可以处理整个国家。您将如何用本章中介绍的各种存储选项来表示这种层次关系:文本文件、HDF5、MongoDB 和 Redis?从长远来看,你认为什么最方便?

七、数据分析应用示例

在本章中,我们希望让您熟悉典型的数据准备任务和分析技术,因为熟练准备、分组和重塑数据是成功数据分析的重要组成部分。

虽然准备数据似乎是一项平凡的任务——通常也是如此——但这是我们不能跳过的一步,尽管我们可以通过使用 Pandas 等工具来努力简化它。

为什么准备是必要的?因为大多数有用的数据将来自现实世界,并且会有缺陷、包含错误或者是零碎的。

数据准备之所以有用,还有更多原因:它让你与原材料密切接触。了解您的输入有助于您及早发现潜在的错误,并对结果建立信心。

以下是一些数据准备场景:

  • 一个客户交给你三个文件,每个文件包含一个单一地质现象的时间序列数据,但是观察到的数据被记录在不同的时间间隔,并使用不同的分隔符
  • 机器学习算法只能处理数字数据,但您的输入只包含文本标签
  • 你拿到了一个新兴服务的网络服务器的原始日志,你的任务是根据现有的访问者行为,对增长策略提出建议

数据收集

用于数据收集的工具库非常庞大,虽然我们将重点讨论 Python,但我们也想提一些有用的工具。如果它们在您的系统上可用,并且您希望大量处理数据,那么它们值得学习。

有一组工具属于 UNIX 传统,它强调文本处理,因此在过去的四十年中,开发了许多高性能和经过战斗考验的工具来处理文本。常见的工具有:sedgrepawksortuniqtrcuttailhead。它们做的是非常基础的事情,比如从文件中过滤掉行(grep)或列(cut),替换文本(sedtr)或只显示文件的一部分(headtail)。

我们只想用一个例子来证明这些工具的力量。

假设你拿到了一个网络服务器的日志文件,你对 IP 地址的分布感兴趣。

日志文件的每一行都包含一个常用日志服务器格式的条目(您可以从ita.ee.lbl.gov/html/contri…下载该数据集):

$ 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

现在我们终于可以让高度栏更有用一点了。我们可以为其分配更新的版本,该版本具有喜欢的类型:

>>> 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 数据可从www.eia.gov/dnav/pet/hi…获得(可在第二张工作表中找到)。我们稍微清理了一下数据(清理过程是本章末尾练习的一部分),并将使用以下数据框,包含 1987 年至 2015 年的 7160 个条目:

>>> 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   price
5354 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()
 price
date
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 年的秋天显示出石油价格的轻微上涨。同样,在九十年代,我们看到我们有一个更大的偏差,在 1990 年秋天:

>>> decade = df["1990":"1999"]
>>> decade[np.abs(decade.price - decade.price.mean()) > 5 * decade.price.std()]
 price
date
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 的连接和合并功能在组合、连接或对齐数据时,抽象掉了大部分痛苦。它也以高度优化的方式做到了这一点。

在两个数据帧具有相似形状的情况下,一个接一个地追加可能是有用的。也许AB是产品,一个数据框包含商店中每种产品售出的商品数量:

>>> 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函数创建这样一个数据帧。参数是索引(我们使用日期)、列(我们使用城市)和值(存储在原始数据框的值列中):

>>> pv = df.pivot("date", "city", "value")
>>> pv
city date         London  Mexico  Mumbai  Tokyo
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 自带很多内置的聚合功能。我们已经在第三章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

在某些data sets上,按多个属性分组会很有用。通过输入两个列名,我们可以大致了解每个国家和日期的日照时间:

>>> df.groupby(["country", "date"]).describe()
 hours country 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
city
Berlin        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 数据,你想问关于大陆的问题。这些问题通常会导致一些函数应用——你可能想计算每个大陆的平均国内生产总值。最后,您希望将这些数据存储在新的数据结构中,以便进一步处理。

  • 这里我们用一个更简单的例子。想象一些关于每天晴天小时数和城市的虚构天气数据:

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

总结

在这一章中,我们已经看到了处理数据帧的方法,从清理和过滤,到分组、聚合和整形。Pandas 使许多常见的操作变得非常容易,而更复杂的操作,如按多个属性进行旋转或分组,也经常可以表示为一行。清理和准备数据是数据探索和分析的重要组成部分。

下一章将简要介绍机器学习算法,该算法正在应用数据分析结果来做出决策或构建有用的产品。

练习练习

练习 1: 清洗:在关于过滤的部分,我们使用了欧洲布伦特原油现货价格,在网上可以找到一个 Excel 文档。拿着这个 Excel 电子表格,试着把它转换成一个 CSV 文档,准备和 Pandas 一起导入。

提示:有很多方法可以做到。我们使用了一个名为xls2csv.py的小工具,并且我们能够用一个辅助方法加载生成的 CSV 文件:

import datetime
import pandas as pd
def convert_date(s):
    parts = s.replace("(", "").replace(")", "").split(",")
	if len(parts) < 6:
	return datetime.date(1970, 1, 1)
	return datetime.datetime(*[int(p) for p in parts])
	df = pd.read_csv("RBRTEd.csv", sep=',', names=["date", "price"], converters={"date": convert_date}).dropna()

拿一个对你的工作很重要的数据集来说,或者如果你手头没有,那就拿一个你感兴趣的并且可以在网上找到的数据集来说。提前问一两个关于数据的问题。然后使用清理、过滤、分组和绘图技术来回答你的问题。

八、基于 scikit-learn 的机器学习模型

在前一章中,我们看到了如何执行数据管理、数据聚合和分组。在这一章中,我们将简要地看到不同 scikit-learn 模块对不同模型的工作,scikit-learn 中的数据表示,使用示例理解监督和非监督学习,以及测量预测性能。

机器学习模型概述

机器学习是人工智能的一个子领域,探索机器如何从数据中学习来分析结构、帮助决策和做出预测。1959 年,阿瑟·塞缪尔将机器学习定义为“赋予计算机学习能力而无需明确编程的研究领域。”

大量应用采用机器学习方法,如垃圾邮件过滤、光学字符识别、计算机视觉、语音识别、信用审批、搜索引擎和推荐系统。

机器学习的一个重要驱动因素是,所有部门的数据生成速度都在加快;无论是网络流量、文本或图像,还是传感器数据或科学数据集。更大的数据量给存储和处理系统带来了许多新的挑战。另一方面,许多学习算法会产生更好的结果,有更多的数据可以学习。近年来,由于各种硬任务(如语音识别或图像中的对象检测)的性能显著提高,该领域受到了广泛关注。没有智能算法的帮助,理解大量数据似乎是不可能的。

学习问题通常使用一组样本(通常用 N 或 N 表示)来构建模型,然后对模型进行验证,并用于预测看不见的数据的属性。

每个样本可能由单个或多个值组成。在机器学习的背景下,数据的属性被称为特征。

机器学习可以根据输入数据的性质来安排:

  • 监督学习
  • 无监督学习

在监督学习中,输入数据(通常用 x 表示)与目标标签(y)相关联,而在无监督学习中,我们只有未标记的输入数据。

监督学习可以进一步分解为以下问题:

  • 分类问题
  • 回归问题

分类问题有一组固定的目标标签、类或类别,而回归问题有一个或多个连续的输出变量。将电子邮件分类为垃圾邮件还是非垃圾邮件是一项具有两个目标标签的分类任务。预测房价——考虑到房屋的数据,如大小、年龄和一氧化氮浓度——是一项回归任务,因为价格是连续的。

无监督学习处理不带标签的数据集。一个典型的例子是聚类或自动分类。目标是将相似的项目组合在一起。相似性意味着什么将取决于上下文,在这样的任务中有许多相似性度量可以使用。

不同型号的 scikit-learn 模块

scikit-learn 库被组织成子模块。每个子模块包含用于某类机器学习模型和方法的算法和辅助方法。

以下是这些子模块的示例,包括一些示例模型:

|

子模块

|

描述

|

示例模型

| | --- | --- | --- | | 串 | 这就是无监督聚类 | KMeans 和 Ward | | 分解 | 这就是降维 | 常设仲裁法院和 NMF | | 全体 | 这涉及到基于集成的方法 | AdaBoostClassifier,AdaBoostRegressor,随机应变分类器,随机森林回归器 | | 皱胃向左移 | 这代表潜在鉴别分析 | 皱胃向左移 | | 线性模型 | 这是广义线性模型 | 线性回归,物流回归,套索和感知器 | | 混合 | 这是混合物模型 | GMM 和维也纳国际中心 | | 朴素贝叶斯 | 这包括基于贝叶斯定理的监督学习 | BaseNB 和 BernoulliNB,GaussianNB | | 邻居 | 这些是 k 最近的邻居 | KNeighborsClassifier,LSHForest | | 神经网络 | 这包括基于神经网络的模型 | BernoulliRBM | | 树 | 决策树 | 决定反对分类者,决定反对者 |

虽然这些方法是多种多样的,但 scikit-learn 库通过向大多数这些算法公开一个常规接口,抽象出了许多不同之处。表中列出的所有示例算法都实现了一种fit方法,并且大多数算法也实现了预测。这些方法代表了机器学习的两个阶段。首先,使用fit方法在现有数据上训练模型。一旦经过训练,就可以使用模型来预测未知数据的类别或值。在接下来的部分中,我们将看到这两种方法都在发挥作用。

scikit-learn 库是 PyData 生态系统的一部分。它的代码库在过去的六年里稳步增长,拥有超过 100 个贡献者,是 scikit 工具包中最活跃和最受欢迎的一个。

sci kit-learn 中的数据表示

与机器学习的异构领域和应用相反,scikit-learn 中的数据表示没有那么多样化,许多算法期望的基本格式很简单——样本和特征的矩阵。

底层数据结构是numpyndarray。矩阵中的每一行对应一个样本,每一列对应一个特征的值。

在机器学习数据集的世界中也有类似于Hello World的东西;例如,起源于 1936 年的 Iris 数据集。使用 scikit-learn 的标准安装,您已经可以访问几个数据集,包括由 150 个样本组成的鸢尾,每个样本由从三种不同鸢尾花物种获取的四个测量值组成:

>>> import numpy as np
>>> from sklearn import datasets
>>> iris = datasets.load_iris()

数据集被打包成一堆,它只是字典的一个薄薄的包装:

>>> type(iris)
sklearn.datasets.base.Bunch
>>> iris.keys()
['target_names', 'data', 'target', 'DESCR', 'feature_names']

data键下,我们可以找到样本和特征的矩阵,并可以确认其形状:

>>> type(iris.data)
numpy.ndarray
>>> iris.data.shape
(150, 4)

data矩阵中的每个条目都已被标记,这些标记可以在target属性中查找:

>>> type(iris.target)
numpy.ndarray
>>> iris.target.shape
(150,)
>>> iris.target[:10]
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
>>> np.unique(iris.target)
array([0, 1, 2])

目标名称已编码。我们可以在target_names属性中查找对应的名称:

>>> iris.target_names
>>> array(['setosa', 'versicolor', 'virginica'], dtype='|S10')

这是许多数据集的基本结构,例如示例数据、目标值和目标名称。

该数据集中的单个条目有哪些特征?:

>>> iris.data[0]
array([ 5.1,  3.5,  1.4,  0.2])

这四个特征是对真花的测量:它们的萼片长度和宽度,和花瓣长度和宽度。已经检查了三种不同的品种:鸢尾-濑户鸢尾鸢尾-云芝鸢尾-北美鸢尾

机器学习试图回答以下问题:如果只给定花的萼片和花瓣长度的测量值,我们能预测花的种类吗?

在下一节中,我们将看到如何用 scikit-learn 来回答这个问题。

除了有关花卉的数据之外,scikit-learn 发行版中还包括一些其他数据集,如下所示:

  • 波士顿房价数据集(506 个样本和 13 个属性)
  • 手写数字数据集的光学识别(5620 个样本和 64 个属性)
  • 鸢尾植物数据库(150 个样本和 4 个属性)
  • Linnerud 数据集(30 个样本和 3 个属性)

一些数据集没有包括在内,但是它们可以很容易地按需获取(因为这些数据集通常稍大一些)。在这些数据集中,您可以找到一个房地产数据集和一个新闻语料库:

>>> ds = datasets.fetch_california_housing()
downloading Cal. housing from http://lib.stat.cmu.edu/modules.php?op=...
>>> ds.data.shape
(20640, 8)
>>> ds = datasets.fetch_20newsgroups()
>>> len(ds.data)
11314
>>> ds.data[0][:50]
u"From: lerxst@wam.umd.edu (where's my thing)\nSubjec"
>>> sum([len([w for w in sample.split()]) for sample in ds.data])
3252437

这些数据集是开始使用 scikit-learn 库的好方法,它们还将帮助您测试自己的算法。最后,scikit-learn 还包括创建人工数据集的函数(前缀为datasets.make_)。

如果你使用自己的数据集,你将不得不把它们做成 scikit-learn 所期望的形状,这可能是它自己的任务。Pandas 之类的工具让这个任务变得简单多了,Pandas 数据帧可以通过数据帧上的as_matrix()方法轻松导出到numpy.ndarray

监督学习——分类和回归

在这个部分,我们将展示分类和回归的简短示例。

分类问题无处不在:文档分类、欺诈检测、商业智能中的市场细分、生物信息学中的蛋白质功能预测。

尽管手工规则可能会为新数据分配一个类别或标签,但使用算法从现有数据中学习和归纳会更快。

我们将继续使用 Iris 数据集。在我们应用学习算法之前,我们希望通过查看一些值和图来获得数据的直觉。

所有测量值共享相同的维度,这有助于可视化各种箱线图中的差异:

Supervised learning – classification and regression

我们看到花瓣长度(第三个特征)表现出最大的变化,这可以表明这个特征在分类过程中的重要性。用两个维度绘制数据点,每个轴使用一个特征,这也很有见地。而且,事实上,我们之前的观察强化了花瓣长度可能是区分不同物种的一个很好的指标。鸢尾似乎也比其他两个物种更容易分离:

Supervised learning – classification and regression

从的可视化中,我们获得了解决问题的直觉。我们将使用一种被称为支持向量机 ( SVM )的监督方法来学习虹膜数据的分类器。应用编程接口将模型和数据分开,因此,第一步是实例化模型。在这种情况下,我们传递一个可选的关键字参数,以便以后能够查询模型的概率:

>>> from sklearn.svm import SVC
>>> clf = SVC(probability=True)

下一步是根据我们的训练数据拟合模型:

>>> clf.fit(iris.data, iris.target)
SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0,
 degree=3, gamma=0.0, kernel='rbf', max_iter=-1,
 probability=True, random_state=None, shrinking=True,
 tol=0.001, verbose=False)

有了这条线,我们已经在数据集上训练了我们的第一个机器学习模型。这个模型现在可以用来预测未知数据的种类。如果给定一些我们以前从未见过的测量值,我们可以在模型上使用预测方法:

>>> unseen = [6.0, 2.0, 3.0, 2.0]
>>> clf.predict(unseen)
array([1])
>>> iris.target_names[clf.predict(unseen)]
array(['versicolor'],
 dtype='|S10')

我们看到分类器给测量赋予了versicolor标签。如果我们想象我们的情节中的未知点,我们会发现这似乎是一个明智的预测:

Supervised learning – classification and regression

其实分类器对这个标签是比较确定的,我们可以用分类器上的predict_proba方法来查询:

>>> clf.predict_proba(unseen)
array([[ 0.03314121,  0.90920125,  0.05765754]])

我们的示例由四个特征组成,但是许多问题涉及更高维的数据集,并且许多算法在这些数据集上也运行良好。

我们想展示监督学习问题的另一种算法:线性回归。在线性回归中,我们试图预测一个或多个连续的输出变量,称为回归和,给定一个三维输入向量。回归意味着输出是连续的。之所以称之为线性,是因为输出将以参数的线性函数建模。

我们首先创建一个示例数据集,如下所示:

>>> import matplotlib.pyplot as plt
>>> X = [[1], [2], [3], [4], [5], [6], [7], [8]]
>>> y = [1, 2.5, 3.5, 4.8, 3.9, 5.5, 7, 8]
>>> plt.scatter(X, y, c='0.25')
>>> plt.show()

给定这些数据,我们希望学习一个线性函数来逼近数据并最小化预测误差,预测误差定义为观察响应和预测响应之间的平方和:

>>> from sklearn.linear_model import LinearRegression
>>> clf = LinearRegression()
>>> clf.fit(X, y)

许多模型将在训练过程中学习参数。这些参数在属性名称的末尾用一个下划线标记。在该模型中,coef_属性将保存线性回归问题的估计系数:

>>> clf.coef_
array([ 0.91190476])

我们也可以根据我们的数据绘制预测:

>>> plt.plot(X, clf.predict(X), '--', color='0.10', linewidth=1)

该图的输出如下:

Supervised learning – classification and regression

上图是人工数据的简单例子,但是线性回归有广泛的应用。如果给定房地产对象的特征,我们就可以学会预测价格。如果给出星系的特征,如大小、颜色或亮度,就有可能预测它们的距离。如果给出家庭收入和父母受教育程度的数据,我们可以说说他们孩子的成绩。

线性回归的应用无处不在,一个或多个自变量可能与一个或多个因变量相关联。

无监督学习——聚类和降维

很多已有数据没有标注。用无监督模型从没有标签的数据中学习仍然是可能的。探索性数据分析期间的典型任务是找到相关的项目或集群。我们可以想象 Iris 数据集,但是没有标签:

Unsupervised learning – clustering and dimensionality reduction

虽然没有标签的任务看起来要困难得多,但是一组测量值(在左下方)似乎与分开了。聚类算法的目标是识别这些组。

我们将在虹膜数据集上使用 K-Means 聚类(没有标签)。该算法期望预先指定聚类的数量,这可能是一个缺点。K-Means 将尝试通过最小化聚类内的平方和来将数据集划分为组。

例如,我们实例化n_clusters等于3KMeans模型:

>>> from sklearn.cluster import KMeans
>>> km = KMeans(n_clusters=3)

类似于监督算法,我们可以使用fit方法训练模型,但我们只传递数据,不传递目标标签:

>>> km.fit(iris.data)
KMeans(copy_x=True, init='k-means++', max_iter=300, n_clusters=3, n_init=10, n_jobs=1, precompute_distances='auto', random_state=None, tol=0.0001, verbose=0)

我们已经看到属性以下划线结尾。在这种情况下,算法为训练数据分配了一个标签,可以用labels_属性进行检查:

>>> km.labels_
array([1, 1, 1, 1, 1, 1, ..., 0, 2, 0, 0, 2], dtype=int32)

我们已经可以将这些算法的结果与我们已知的目标标签进行比较:

>>> iris.target
array([0, 0, 0, 0, 0, 0, ..., 2, 2, 2, 2, 2])

我们快速relabel结果来简化预测误差计算:

>>> tr = {1: 0, 2: 1, 0: 2}
>>> predicted_labels = np.array([tr[i] for i in km.labels_])
>>> sum([p == t for (p, t) in zip(predicted_labels, iris.target)])
134

从 150 个样本中,K-Mean 为 134 个样本分配了正确的标签,准确率约为 90%。下图用灰色显示了正确预测的算法点,用红色显示了错误标记的点:

Unsupervised learning – clustering and dimensionality reduction

作为无监督算法的另一个例子,我们来看看主成分分析 ( 主成分分析)。主成分分析的目的是寻找高维数据中最大方差的方向。一个目标是通过将更高维度的空间投影到更低维的子空间,同时保留大部分信息,从而减少维度的数量。

问题出现在各个领域。您已经收集了许多样本,每个样本包含数百或数千个特征。并不是手头现象的所有属性都同样重要。在我们的 Iris 数据集中,我们看到花瓣长度本身似乎是各种物种的一个很好的鉴别器。主成分分析旨在找到解释数据中大多数变化的主成分。如果我们对我们的分量进行相应的排序(技术上,我们通过特征值对协方差矩阵的特征向量进行排序),我们可以保留解释大部分数据的特征向量,忽略剩余的特征向量,从而降低数据的维数。

用 scikit-learn 运行 PCA 很简单。我们将不讨论实现细节,而是尝试通过在 Iris 数据集上运行 PCA 来给你一个 PCA 的直觉,以便给你另一个角度。

流程与我们到目前为止实施的流程相似。首先,我们实例化我们的模型;这一次,PCA 来自分解子模块。我们还引入了一种标准化方法,称为StandardScaler,它将从我们的数据中移除平均值,并换算成单位方差。这一步是许多机器学习算法的共同要求:

>>> from sklearn.decomposition import PCA
>>> from sklearn.preprocessing import StandardScaler

首先,我们用一个参数实例化我们的模型(该参数指定要减少到的维数),标准化我们的输入,并运行fit_transform函数,该函数将处理主成分分析的机制:

>>> pca = PCA(n_components=2)
>>> X = StandardScaler().fit_transform(iris.data)
>>> Y = pca.fit_transform(X)

结果是虹膜数据集中的维度从四个维度(萼片和花瓣的宽度和长度)减少到两个维度。需要注意的是,这个投影并不在现有的两个维度上,所以我们的新数据集并不仅仅由花瓣的长度和宽度组成。相反,两个新的维度将代表现有特征的混合。

以下散点图显示了转换后的数据集;从图表上看,看起来我们仍然保留了数据集的本质,尽管我们将维度的数量减半:

Unsupervised learning – clustering and dimensionality reduction

降维只是处理高维数据集的一种方式,有时会受到所谓降维诅咒的影响。

测量预测性能

我们已经看到机器学习过程由以下步骤组成:

  • 模型选择:我们首先为我们的数据选择一个合适的模型。我们有标签吗?有多少样品?数据可分离吗?我们有几个维度?由于这一步不重要,选择将取决于实际问题。截至 2015 年秋季,scikit-learn 文档包含一个非常受欢迎的流程图,名为选择正确的评估者。它很短,但信息量很大,值得仔细看看。
  • 训练:我们要把模型和数据放在一起,这通常发生在 scikit-learn 中模型的拟合方法中。
  • 应用:一旦我们训练好我们的模型,我们就能够对看不见的数据做出预测。

到目前为止,我们忽略了在训练和应用之间发生的一个重要步骤:模型测试和验证。在这一步中,我们要评估我们的模型学习得有多好。

学习的一个目标,特别是机器学习,是泛化。有限的一组观察是否足以对任何可能的观察做出陈述,这是一个更深层次的理论问题,在机器学习的专用资源中有所回答。

一个模型概括的好不好也是可以检验的。然而,重要的是训练和测试输入是分开的。模型在训练输入上表现良好,但在看不见的测试输入上失败的情况称为过拟合,这种情况并不少见。

基本方法是将可用数据分割成一个训练和测试集,scikit-learn 通过train_test_split功能帮助创建这种分割。

我们回到 Iris 数据集,再次执行 SVC。这次我们将在训练集上评估算法的性能。我们留出 40%的数据用于测试:

>>> from sklearn.cross_validation import train_test_split
>>> X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target, test_size=0.4, random_state=0)
>>> clf = SVC()
>>> clf.fit(X_train, y_train)

score 函数返回给定数据和标签的平均精度。我们通过测试集进行评估:

>>> clf.score(X_test, y_test)
 0.94999999999999996

该模型似乎表现良好,对看不见的数据的准确率约为 94%。我们现在可以开始调整模型参数(也称为超参数)以提高预测性能。这个周期会带来过度拟合的问题。一种解决方案是将输入数据分成三组:一组用于训练、验证和测试。超参数调整的迭代模型将发生在训练集和验证集之间,而最终评估将在测试集上完成。将数据集分为三个也减少了我们可以学习的样本数量。

交叉验证 ( CV )是一种不需要验证集,但仍能抵消过度拟合的技术。数据集被分割成k部分(称为折叠)。对于每个折叠,模型在k-1折叠上训练,并在剩余折叠上测试。精度被视为折叠的平均值。

我们将在 Iris 数据集上展示五重交叉验证,再次使用 SVC:

>>> from sklearn.cross_validation import cross_val_score
>>> clf = SVC()
>>> scores = cross_val_score(clf, iris.data, iris.target, cv=5)
>>> scores
array([ 0.96666667,  1\.    ,  0.96666667,  0.96666667,  1\.    ])
>>> scores.mean()
0.98000000000000009

有不同类实现的各种策略来拆分数据集进行交叉验证:KFoldStratifiedKFoldLeaveOneOutLeavePOutLeaveOneLabelOutLeavePLableOutShuffleSplitStratifiedShuffleSplitPredefinedSplit

模型验证是一个重要的步骤,对于开发健壮的机器学习解决方案是必要的。

总结

在这一章中,我们旋风般地浏览了最流行的 Python 机器学习库之一:scikit-learn。我们看到了这个库期望什么样的数据。真实世界的数据很少会马上被输入到估计器中。有了强大的库,比如 Numpy,尤其是 Pandas,您已经看到了如何检索、组合和形成数据。可视化库,如 matplotlib,有助于直观地了解数据集、问题和解决方案。

在这一章中,我们看了一个规范数据集,Iris 数据集。我们也从不同的角度来看它:作为有监督和无监督学习中的一个问题,以及作为模型验证的一个例子。

总之,我们已经研究了四种不同的算法:支持向量机、线性回归、K-Means 聚类和主成分分析。其中的每一个都值得探索,尽管我们只用了几行 Python 就实现了所有的算法,但我们几乎没有触及表面。

有许多方法可以让您进一步了解数据分析过程。已经出版了数百本关于机器学习的书,所以我们在这里只想强调几本:用 Python 构建机器学习系统**里歇特科埃略将更深入地探讨 scikit-learn,这是我们在本章中无法做到的。从数据中学习阿布-穆斯塔法、马格东-伊斯梅尔组成,是学习一般问题的坚实理论基础的巨大资源。

最有趣的应用将会出现在你自己的领域。然而,如果你想获得一些灵感,我们建议你看看www.kaggle.com网站,该网站运行预测建模和分析竞赛,既有趣又有洞察力。

练习练习

以下问题是有人监督还是无人监督?回归还是分类问题?:

  • 识别自动售货机内的硬币
  • 识别手写数字
  • 如果给定一些关于人和经济的事实,我们想要估计消费者支出
  • 如果给定地理、政治和历史事件的数据,我们希望预测侵犯人权行为最终将在何时何地发生
  • 如果考虑到鲸鱼及其物种的声音,我们想给尚未标记的鲸鱼录音贴上标签

查找最早的机器学习模型和算法之一:感知器。在 Iris 数据集上尝试感知器,并估计模型的准确性。感知器与本章中的 SVC 相比如何?