Python-真实世界的数据科学-六-

47 阅读26分钟

Python 真实世界的数据科学(六)

原文:Python: Real-World Data Science

协议:CC BY-NC-SA 4.0

十八、数据可视化

数据可视化与图形或图形形式的数据表示有关。 这是数据分析中最重要的任务之一,因为它使我们能够查看分析结果,检测异常值并为模型构建做出决策。 有许多用于可视化的 Python 库,其中 matplotlib,seaborn,bokeh 和 ggplot 最受欢迎。 但是,在本章中,我们主要关注 matplotlib 库,该库在许多不同的上下文中被许多人使用。

Matplotlib 生成各种格式的出版物质量的数字,以及跨 Python 平台的交互式环境。 另一个优点是,pandas 附带了一些围绕 matplotlib 绘制例程的有用包装器,可以快速方便地绘制 Series 和 DataFrame 对象。

IPython 包起初是对标准交互式 Python Shell 的替代,但从那时起,它已发展成为用于数据探索,可视化和快速原型制作的必不可少的工具。 可以通过各种选项来使用 IPython 中 matplotlib 提供的图形功能,其中最简单的方法是pylab标志:

$ ipython --pylab

该标志将预加载matplotlibnumpy与默认的 matplotlib 后端进行交互使用。 IPython 可以在各种环境中运行:在终端中作为Qt应用运行,或者在浏览器内部运行。 这些选项值得探索,因为 IPython 已在许多用例中得到广泛采用,例如原型设计,用于使会议演讲或讲座更具吸引力的交互式幻灯片,以及作为共享研究的工具。

matplotlib API 入门

使用 matplotlib 进行绘图的最简单的方法通常是使用软件包支持的 MATLAB API:

>>> import matplotlib.pyplot as plt
>>> from numpy import *
>>> x = linspace(0, 3, 6)
>>> x
array([0., 0.6, 1.2, 1.8, 2.4, 3.])
>>> y = power(x,2)
>>> y
array([0., 0.36, 1.44, 3.24, 5.76, 9.])
>>> figure()
>>> plot(x, y, 'r')
>>> xlabel('x')
>>> ylabel('y')
>>> title('Data visualization in MATLAB-like API')
>>> plt.show()

先前命令的输出如下:

The matplotlib API primer

但是,除非有充分的理由,否则不应使用星级导入。 对于 matplotlib,我们可以使用规范导入:

>>> import matplotlib.pyplot as plt

然后,可以将前面的示例编写如下:

>>> plt.plot(x, y)
>>> plt.xlabel('x')
>>> plt.ylabel('y')
>>> plt.title('Data visualization using Pyplot of Matplotlib')
>>> plt.show()

先前命令的输出如下:

The matplotlib API primer

如果我们仅向 plot 函数提供单个参数,它将自动将其用作y值并生成从0N-1x值,其中N等于值的数量 :

>>> plt.plot(y)
>>> plt.xlabel('x')
>>> plt.ylabel('y')
>>> plt.title('Plot y value without given x values')
>>> plt.show()

先前命令的输出如下:

The matplotlib API primer

默认情况下,轴的范围受输入xy数据的范围限制。 如果要指定轴的viewport,则可以使用axis()方法设置自定义范围。 例如,在先前的可视化中,我们可以通过编写以下命令来将x轴的范围从[0, 5]增大到[0, 6],并将y轴的范围从[0, 9]增大到[0, 10]。 :

>>> plt.axis([0, 6, 0, 12])

线属性

当我们在 matplotlib 中绘制数据时,默认线格式是蓝色实线,缩写为b-。 要更改此设置,我们只需要向plot功能添加符号代码,其中包括字母作为颜色字符串和符号作为线条样式字符串。 让我们考虑一下具有不同格式样式的多条线的图:

>>> plt.plot(x*2, 'g^', x*3, 'rs', x**x, 'y-')
>>> plt.axis([0, 6, 0, 30])
>>> plt.show()

上一条命令的输出如下:

Line properties

我们可以选择许多线型和属性,例如颜色,线宽和破折号,以控制图形的外观。 以下示例说明了设置线属性的几种方法:

>>> line = plt.plot(y, color='red', linewidth=2.0)
>>> line.set_linestyle('--')
>>> plt.setp(line, marker='o')
>>> plt.show()

上一条命令的输出如下:

Line properties

下表列出了line2d绘图的一些常见属性:

|

财产

|

值类型

|

描述

| | --- | --- | --- | | colorc | 任何 matplotlib 颜色 | 设置图中线条的颜色 | | dashes | 开关 | 这样可以设置墨点的顺序 | | data | nparray xdatanp.array ydata | 设置用于可视化的数据 | | linestylels | ['-' | '—' | '-.' | ':' | ...] | 这将在图中设置线条样式 | | linewidthlw | 浮点价值 | 这将设置图中的线宽 | | marker | 任何符号 | 这将在图中的数据点设置样式 |

图和子图

默认情况下,所有绘图命令均适用于当前图形和轴。 在某些情况下,我们希望可视化多个图形和轴上的数据,以比较不同的图或更有效地使用页面上的空间。 在绘制数据之前,需要完成两个步骤。 首先,我们必须定义要绘制的图形。 其次,我们需要弄清楚子图在图中的位置:

>>> plt.figure('a')    # define a figure, named 'a'
>>> plt.subplot(221)    # the first position of 4 subplots in 2x2 figure
>>> plt.plot(y+y, 'r--')
>>> plt.subplot(222)    # the second position of 4 subplots
>>> plt.plot(y*3, 'ko')
>>> plt.subplot(223)    # the third position of 4 subplots
>>> plt.plot(y*y, 'b^')
>>> plt.subplot(224)
>>> plt.show()

上一条命令的输出如下:

Figures and subplots

在这种情况下,我们当前具有图a。 如果要修改图形a中的任何子图,我们首先调用命令以选择图形和子图,然后执行该函数以修改子图。 例如,在这里,我们更改了四图图形的第二个图形的标题:

>>> plt.figure('a')
>>> plt.subplot(222)
>>> plt.title('visualization of y*3')
>>> plt.show()

前面的命令的输出如下:

Figures and subplots

注意

如果我们不使用逗号分隔索引,则整数子图规范必须为三位数。 因此,plt.subplot(221)等于plt.subplot(2,2,1)命令。

有便捷方法plt.subplots()来创建包含给定数量的子图的图形。 与前面的示例一样,我们可以使用plt.subplots(2,2)命令创建由四个子图组成的2x2图形。

我们还可以使用plt.axes([left, bottom, width, height])命令手动创建轴,而不是矩形网格,其中所有输入参数都在小数[0, 1]坐标中:

>>> plt.figure('b')    # create another figure, named 'b'
>>> ax1 = plt.axes([0.05, 0.1, 0.4, 0.32])
>>> ax2 = plt.axes([0.52, 0.1, 0.4, 0.32])
>>> ax3 = plt.axes([0.05, 0.53, 0.87, 0.44])
>>> plt.show()

先前命令的输出如下:

Figures and subplots

但是,当您手动创建轴时,需要花费更多时间来平衡子图之间的坐标和大小,才能获得比例合理的图形。

Figures and subplots

探索地块类型

到目前为止,我们已经研究了如何创建简单的线图。 matplotlib 库支持更多可用于数据可视化的图类型。 但是,我们的目标是提供基本知识,以帮助您理解和使用该库在最常见的情况下可视化数据。 因此,我们仅关注四种图类型:散点图条形图等高线图直方图

散点图

散点图用于可视化同一数据集中测得的变量之间的关系。 使用plt.scatter()函数可以很容易地绘制一个简单的散点图,该散点图需要xy轴都使用数字列:

Scatter plots

让我们看一下前面输出的命令:

>>> X = np.random.normal(0, 1, 1000)
>>> Y = np.random.normal(0, 1, 1000)
>>> plt.scatter(X, Y, c = ['b', 'g', 'k', 'r', 'c'])
>>> plt.show()

条形图

条形图用于以矩形条形显示分组的数据,矩形条可以是垂直或水平的,且条形的长度对应于其值。 我们使用plt.bar()命令显示垂直条,使用plt.barh()命令显示其他条形:

Bar plots

上一个输出的命令如下:

>>> X = np.arange(5)
>>> Y = 3.14 + 2.71 * np.random.rand(5)
>>> plt.subplots(2)
>>> # the first subplot
>>> plt.subplot(211)
>>> plt.bar(X, Y, align='center', alpha=0.4, color='y')
>>> plt.xlabel('x')
>>> plt.ylabel('y')
>>> plt.title('bar plot in vertical')
>>> # the second subplot
>>> plt.subplot(212)
>>> plt.barh(X, Y, align='center', alpha=0.4, color='c')
>>> plt.xlabel('x')
>>> plt.ylabel('y')
>>> plt.title('bar plot in horizontal')
>>> plt.show()

等高线图

我们使用等高线图来表示二维的三个数值变量之间的关系。 沿xy轴绘制了两个变量,第三个变量z用于以不同颜色绘制为曲线的轮廓级别:

>>> x = np.linspace(-1, 1, 255)
>>> y = np.linspace(-2, 2, 300)
>>> z = np.sin(y[:, np.newaxis]) * np.cos(x)
>>> plt.contour(x, y, z, 255, linewidth=2)
>>> plt.show()

让我们看一下下图中的轮廓图:

Contour plots

注意

如果要绘制轮廓线和填充轮廓,可以使用plt.contourf()方法代替plt.contour()。 与 MATLAB 相比,matplotlib 的contourf()不会绘制多边形的边缘。

直方图

直方图以图形方式表示数值数据的分布。 通常,值范围被划分为大小相等的 bin,每个 bin 的高度对应于该 bin 中值的频率:

Histogram plots

先前输出的命令如下:

>>> mu, sigma = 100, 25
>>> fig, (ax0, ax1) = plt.subplots(ncols=2)
>>> x = mu + sigma * np.random.randn(1000)
>>> ax0.hist(x,20, normed=1, histtype='stepfilled', 
 facecolor='g', alpha=0.75)
>>> ax0.set_title('Stepfilled histogram')
>>> ax1.hist(x, bins=[100,150, 165, 170, 195] normed=1, 
 histtype='bar', rwidth=0.8)
>>> ax1.set_title('uniquel bins histogram')
>>> # automatically adjust subplot parameters to give specified padding
>>> plt.tight_layout()
>>> plt.show()

Histogram plots

图例和注释

图例是一个重要元素,用于标识图中的plot元素。 在图中显示图例的最简单方法是使用plot函数的label参数,并通过调用plt.legend()方法显示标签:

>>> x = np.linspace(0, 1, 20) 
>>> y1 = np.sin(x)
>>> y2 = np.cos(x)
>>> y3 = np.tan(x)
>>> plt.plot(x, y1, 'c', label='y=sin(x)')
>>> plt.plot(x, y2, 'y', label='y=cos(x)')
>>> plt.plot(x, y3, 'r', label='y=tan(x)')
>>> plt.lengend(loc='upper left')
>>> plt.show()

前面命令的输出如下:

Legends and annotations

图例命令中的loc自变量用于确定标签框的位置。 有几个有效的位置选项:lower leftrightupper leftlower centerupper rightcenterlower rightupper rightcenter rightbestupper centercenter left。 默认位置设置为upper right。 但是,当我们设置了上面列表中不存在的无效位置选项时,该功能将自动退回到最佳选项。

如果要将图例分成多个图框,可以手动设置绘图线的预期标签,如下图所示:

Legends and annotations

上一条命令的输出如下:

>>> p1 = plt.plot(x, y1, 'c', label='y=sin(x)')
>>> p2 = plt.plot(x, y2, 'y', label='y=cos(x)')
>>> p3 = plt.plot(x, y3, 'r', label='y=tan(x)')
>>> lsin = plt.legend(handles=p1, loc='lower right')
>>> lcos = plt.legend(handles=p2, loc='upper left')
>>> ltan = plt.legend(handles=p3, loc='upper right')
>>> # with above code, only 'y=tan(x)' legend appears in the figure
>>> # fix: add lsin, lcos as separate artists to the axes
>>> plt.gca().add_artist(lsin)
>>> plt.gca().add_artist(lcos)
>>> # automatically adjust subplot parameters to specified padding
>>> plt.tight_layout()
>>> plt.show()

我们要介绍的图形中的另一个元素是注释,可以由文本,箭头或其他形状组成,以详细说明图形的各个部分或强调一些特殊的数据点。 有多种显示注释的方法,例如textarrowannotation

  • text方法在绘图上的给定坐标(x, y)处绘制文本; (可选)具有自定义属性。 该函数中有一些常见的参数:xy,标签文本和可以通过fontdict传递的字体相关属性,例如familyfontsizestyle
  • annotate方法可以绘制适当排列的文本和箭头。 该函数的参数是s(标签文本),xy(要注释的元素的位置),xytext(标签s的位置),xycoords(指示什么类型的 坐标xy是)和arrowprops(连接注释的箭头的线属性字典)。

这是一个简单的示例来说明annotatetext功能:

>>> x = np.linspace(-2.4, 0.4, 20)
>>> y = x*x + 2*x + 1
>>> plt.plot(x, y, 'c', linewidth=2.0)
>>> plt.text(-1.5, 1.8, 'y=x^2 + 2*x + 1',
 fontsize=14, style='italic')
>>> plt.annotate('minima point', xy=(-1, 0),
 xytext=(-1, 0.3),
 horizontalalignment='center', 
 verticalalignment='top', 
 arrowprops=dict(arrowstyle='->', 
 connectionstyle='arc3'))
>>> plt.show()

上一条命令的输出如下:

Legends and annotations

用 Pandas 绘图功能

我们已使用 matplotlib 覆盖了图中的大多数重要组件。 在本节中,我们将介绍另一种强大的绘图方法,该方法可从通常用于处理数据的 Pandas 数据对象直接创建标准可视化效果。

对于 Pandas 中的 Series 或 DataFrame 对象,支持大多数绘图类型,例如折线图,条形图,框形图,直方图和散点图以及饼图。 要选择绘图类型,我们使用plot函数的kind参数。 没有指定任何类型的绘图,默认情况下plot函数将生成线型可视化效果,如以下示例所示:

>>> s = pd.Series(np.random.normal(10, 8, 20))
>>> s.plot(style='ko—', alpha=0.4, label='Series plotting')
>>> plt.legend()
>>> plt.show()

上一条命令的输出如下:

Plotting functions with pandas

另一个示例将可视化包含多列的 DataFrame 对象的数据:

>>> data = {'Median_Age': [24.2, 26.4, 28.5, 30.3],
 'Density': [244, 256, 268, 279]}
>>> index_label = ['2000', '2005', '2010', '2014'];
>>> df1 = pd.DataFrame(data, index=index_label)
>>> df1.plot(kind='bar', subplots=True, sharex=True)
>>> plt.tight_layout();
>>> plt.show()

上一条命令的输出如下:

Plotting functions with pandas

DataFrame 的绘图方法具有许多选项,使我们能够处理列的绘图。 例如,在前面的 DataFrame 可视化中,我们选择将列绘制在单独的子图中。 下表列出了更多选项:

|

争论

|

价值

|

描述

| | --- | --- | --- | | subplots | True / False | 该图在单独的子图中绘制每个数据列 | | logy | True / False | 获取对数刻度y轴 | | secondary_y | True / False | 在辅助y轴上绘制数据 | | sharexsharey | True / False | 共享相同的xy轴,链接杆和限制 |

Plotting functions with pandas

其他 Python 数据可视化工具

除了 matplotlib 之外,还有其他基于 Python 的强大数据可视化工具包。 尽管我们无法更深入地研究这些库,但我们希望至少在本节中简要介绍它们。

散景

Bokeh 是,由 Peter Wang,Hugo Shi 和其他人在 Continuum Analytics 中进行。 它旨在以D3.js的样式提供优雅且引人入胜的可视化。 该库可以快速轻松地创建交互式绘图,仪表板和数据应用。 这是 matplotlib 和 Bokeh 之间的一些差异:

  • Bokeh 通过 IPython 的浏览器内客户端渲染新模型实现了跨平台的普遍性
  • Bokeh 使用 R 和 ggplot 用户熟悉的语法,而 matplotlib 对 Matlab 用户更熟悉
  • Bokeh 具有一致的构想,以构建受 ggplot 启发的浏览器内交互式可视化工具,而 Matplotlib 具有一致的构想,即专注于 2D 跨平台图形。

使用散景创建图的基本步骤如下:

  • 准备列表,系列和数据框中的一些数据
  • 告诉 Bokeh 您想在哪里生成输出
  • 调用figure()创建带有一些总体选项的绘图,类似于前面讨论的 matplotlib 选项
  • 使用颜色,图例和宽度等视觉自定义添加数据的渲染器
  • 要求 Bokeh show()save()结果

MayaVi

MayaVi 是一个用于交互式科学数据可视化和 3D 绘图的库,建立在屡获殊荣的可视化工具包VTK)的基础上, 开源可视化库的基于特征的包装器。 它提供以下内容:

  • 通过对话框与可视化中的数据和对象进行交互的可能性。
  • Python 中的接口,用于编写脚本。 MayaVi 可以与 Numpy 和 scipy 一起使用以进行现成的 3D 绘制,并且可以在 IPython 笔记本中使用,类似于 matplotlib。
  • VTK 的抽象,提供了更简单的编程模型。

让我们查看基于 VTK 示例及其提供的数据完全使用 MayaVi 制作的插图:

MayaVi

MayaVi

MayaVi

MayaVi

MayaVi

十九、时间序列

时间序列通常由一系列数据点组成,这些数据点来自随时间推移而进行的测量。 这种数据非常普遍,并且发生在多个字段中。

业务主管对股票价格,商品和服务价格或每月销售数字感兴趣。 气象学家每天要进行几次温度测量,并保留降水,湿度,风向和力的记录。 神经科医生可以使用脑电图来测量沿头皮的大脑电活动。 社会学家可以使用竞选捐款数据来了解政党及其支持者,并将这些见解用作论证的辅助手段。 几乎可以无休止地列举时间序列数据的更多示例。

时间序列入门

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

在本章中,我们将探讨 Pandas 的时间序列功能。 除了强大的核心数据结构(系列和 DataFrame)之外,pandas 还具有用于处理与时间相关的数据的辅助功能。 凭借其广泛的内置优化功能,pandas 能够轻松处理具有数百万个数据点的大型时间序列。

我们将从日期和时间对象的基本构造块开始逐步处理时间序列。

处理日期和时间对象

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

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

时间戳记对象可以看作是datetime对象的 Pandas 版本,实际上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 文档查找:

|

别名

|

描述

| | --- | --- | | 乙 | 工作日频率 | | C | 自定义工作日频率 | | d | 日历日频率 | | 在 | 每周频率 | | 中号 | 月末频率 | | BM | 营业月结束频率 | | 煤层气 | 自定义营业月结束频率 | | 小姐 | 月开始频率 | | BMS | 营业月份开始频率 | | 中央管理系统 | 自定义营业月开始频率 | | 问 | 四分之一结束频率 | | BQ | 业务季度频率 | | 质量体系 | 季度开始频率 | | BQS | 业务季度开始频率 | | 一种 | 年末频率 | | BA | 营业年度结束频率 | | 作为 | 年开始频率 | | 低的 | 营业年度开始频率 | | BH | 营业时间频率 | | H | 每小时频率 | | Ť | 分频 | | 小号 | 其次频率 | | 大号 | 毫秒 | | ü | 微秒 | | ñ | 纳秒 |

此外,偏移别名也可以组合使用。 在这里,我们正在生成一个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 AM 开始到 5 PM 结束)对数据建立索引,则只需为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 年是 the 年。

到目前为止,我们已经看到了两个强大的功能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

要查看从三月到五月的所有条目,包括:

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

Working with date and time objects

重采样时间序列

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

下采样时间序列数据

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

他们每分钟都会从计数器设备接收数据。 以下是一天的假设度量,从 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, semmaxminmedianfirstlast,[ ohlcohlc指标在金融领域很流行。 它代表开-高-低-闭。 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方法将取决于您的应用要求。

Upsampling time series data

尽管默认情况下,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

Upsampling time series data

Timedeltas

除了功能强大的时间戳对象(用作DatetimeIndex的构建块)外,还有另一个有用的数据结构(已在 Pandas 0.15 中引入)– Timedelta。 Timedelta 也可以用作索引的基础,在这种情况下为TimedeltaIndex

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

>>> 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索引,该索引具有最近 7 天的条目:

>>> 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使这种探索更加方便。

Timedeltas

时间序列图

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

Time series plotting

Time series plotting

Time series plotting

二十、与数据库交互

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

有多种存储数据的方法。 在本章中,我们将学习与三个主要类别进行交互:文本格式,二进制格式和数据库。 我们将专注于两种存储解决方案,MongoDB 和 Redis。 MongoDB 是一个面向文档的数据库,易于入门,因为我们可以存储 JSON 文档,而无需预先定义架构。 Redis 是一种流行的内存数据结构存储,可以在其上构建许多应用。 可以将 Redis 用作快速键值存储,但是 Redis 也支持开箱即用的列表,集合,哈希,位数组甚至高级数据结构(例如 HyperLogLog)。

与文本格式的数据进行交互

文本是很好的媒介,它是一种交换信息的简单方法。 以下语句来自 Doug McIlroy 的引号:编写处理文本流的程序,因为这是通用接口。

在本节中,我们将开始在文本文件中读写数据。

从文本格式读取数据

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

pandas 支持多种功能,可将数据从文本文件读取到 DataFrame 对象中。 最简单的是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 | 列表 | 也要转换为布尔 True 的值列表。 | | false_values | 列表 | 也将要转换为布尔 False 的值列表。 | | keep_default_na | Booldefault True | 如果存在na_values参数,并且keep_default_naFalse,则默认 NaN 值将被忽略,否则会将它们附加到 | | thousands | Strdefault None | 千位分隔符 | | nrows | Intdefault None | 限制要从文件读取的行数。 | | error_bad_lines | Booleandefault True | 如果设置为 True,则即使解析期间发生错误,也将返回 DataFrame。 |

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

|

功能

|

描述

| | --- | --- | | read_table | 将常规定界文件读入 DataFrame | | read_fwf | 将固定宽度格式的行表读入 DataFrame | | 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()功能,我们还具有 Pandas 支持的to_csv()功能。 让我们看下面的例子:

>>> 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参数中指定它们来将 DataFrame 列的子集写入文件:

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

对于系列对象,我们可以使用相同的函数将数据写入文本文件,并且参数与之前的参数基本相同。

Writing data to text format

与二进制格式的数据进行交互

我们可以使用 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 实例只有一个数据库,名为“本地”。 如果我们指向的数据库和集合不存在,MongoDB 将根据需要创建它们:

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

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

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

我们的db数据库尚无任何集合。 让我们创建一个名为person的集合,并将数据从 DataFrame 对象插入其中:

>>> 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()函数并传入描述我们要检索的文档的字典。 返回的结果是cursor类型,它支持迭代器协议:

>>> 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 没有这样的选择,它可以保留在磁盘上。 Redis 支持每秒约 100,000 次 set 或 get 操作的快速读写。

要与 Redis 交互,我们需要将Redis-py模块安装到 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 中设置数据和从 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 将使用给定的增量作为值来创建密钥。

The simple value

列表

我们有几种方法与 Redis 中的列表值进行交互。 下面的示例使用rpush()lrange()函数将列表数据放置到 DB 中以及从 DB 中获取列表数据:

>>> 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) | 返回键名称为 set 的所有成员 | | 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参数设置为True。 如果要获取分数和返回值,请使用withscore参数。 返回类型是(值,得分)对的列表,如您在前面的示例中看到的那样。

请参阅下表以了解有序集合上可用的更多功能:

|

功能

|

描述

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

Ordered set

Ordered set

Ordered set

Ordered set

二十一、数据分析应用示例

在本章中,我们希望使您熟悉典型的数据准备任务和分析技术,因为流利地准备,分组和重塑数据是成功进行数据分析的重要基础。

尽管准备数据似乎是一项平凡的任务,而且通常是这样,但这是我们不能跳过的一步,尽管我们可以通过使用诸如 Pandas 之类的工具来简化数据。

为什么根本没有必要做准备? 因为最有用的数据将来自现实世界,并且会存在缺陷,包含错误或零散的信息。

数据准备有用的原因还有很多:它使您与原材料保持紧密联系。 了解您的输入有助于您尽早发现潜在错误并建立对结果的信心。

以下是一些数据准备方案:

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

数据处理

用于数据处理的工具库庞大,尽管我们将重点介绍 Python,但我们也想提及一些有用的工具。 如果它们在您的系统上可用,并且您希望处理大量数据,则值得学习。

一组工具属于 Unix 传统,它强调文本处理,因此,在过去的 40 年中,开发了许多高性能和经过考验的用于处理文本的工具。 一些常见的工具是:sedgrepawksortuniqtrcuttailhead。 它们执行非常基本的操作,例如从文件中过滤出行(grep或列(cut),替换文本(sedtr)或仅显示文件的一部分(headtail)。

我们仅想通过一个示例来演示这些工具的功能。

想象一下,您已经收到了 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 的串联和合并功能消除了大部分麻烦。 它也以高度优化的方式进行。

在两个数据帧具有相似形状的情况下,将一个接一个添加到另一个可能很有用。 也许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 | 内部联接 | 使用两个框架中关键点的交集。 |

Merging data

重塑数据

我们看到了如何组合数据帧,但是有时我们在单个数据结构中拥有所有正确的数据,但是对于某些任务而言,这种格式是不切实际的。 我们从一些人工天气数据开始:

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

Grouping data

Grouping data

Grouping data

Grouping data