Machine-Learning-Mastery-Python-教程-四-

238 阅读1小时+

Machine Learning Mastery Python 教程(四)

原文:Machine Learning Mastery

协议:CC BY-NC-SA 4.0

如何学习 Python 以用于机器学习

原文:machinelearningmastery.com/how-to-learn-python-for-machine-learning/

Python 已成为机器学习的事实上的通用语言。它并不是一门难学的语言,但如果你对这门语言不是特别熟悉,有一些技巧可以帮助你更快或更好地学习。

在这篇文章中,你将发现学习编程语言的正确方法以及如何获得帮助。阅读完这篇文章后,你将知道:

  • 学习 Python 以用于机器学习的正确心态

  • 学习 Python 的好资源

  • 如何找到与 Python 相关问题的答案

通过我的新书 Python for Machine Learning 启动你的项目,其中包括 逐步教程 和所有示例的 Python 源代码 文件。

让我们开始吧。

如何学习 Python 以用于机器学习

图片由 Federico Di Dio 提供,部分权利保留。

如何学习 Python

学习语言有很多方式,无论是自然语言如英语,还是编程语言如 Python。婴儿通过听和模仿来学习语言。随着他们逐渐学会模式和一些词汇,他们可以自己组成句子。相反,当大学生学习拉丁语时,他们可能从语法规则开始——单数和复数,直陈式和虚拟式,主格和宾格。然后,他们可以逐步构建拉丁语句子。

同样地,当学习 Python 或任何编程语言时,你可以选择阅读他人的代码,试图理解,并在此基础上进行修改。或者你可以学习语言规则,从头开始构建程序。如果你的最终目标是处理该语言,例如编写 Python 解释器,那么后者会更有利。但通常情况下,前者的方法能更快地得到一些结果。

我的建议是首先从例子中学习。但是通过时不时地回顾语言规则,来加强你对语言的基础理解。让我们看看 维基百科上的一个例子

def secant_method(f, x0, x1, iterations):
    """Return the root calculated using the secant method."""
    for i in range(iterations):
        x2 = x1 - f(x1) * (x1 - x0) / float(f(x1) - f(x0))
        x0, x1 = x1, x2
    return x2

def f_example(x):
    return x ** 2 - 612

root = secant_method(f_example, 10, 30, 5)

print("Root: {}".format(root))  # Root: 24.738633748750722

这段 Python 代码实现了割线法以寻找函数的根。如果你是 Python 新手,你应该查看这个例子,看看你能理解多少。如果你有其他编程语言的知识,你可能会猜到 def 定义了一个函数。但如果没有,你可能会感到困惑。如果是这样,最好从一本初学者编程书籍开始,了解函数、变量、循环等概念。

接下来你可能会想到的是修改函数。例如,如果我们不使用割线法来找根,而是使用牛顿法,你可以猜测如何修改第 4 行的方程来实现。那么二分法呢?你需要添加一个语句if f(x2)>0来决定我们应该走哪条路。如果我们看一下函数f_example,我们会看到符号**。这是指数运算符,表示xx的平方。但它应该是x2612x² – 612还是x2612x^{2-612}呢?你需要回去查看语言手册,看看操作符优先级的层次结构。

因此,即使像这样的简短示例,你也可以学到很多语言特性。通过学习更多的例子,你可以推断语法,习惯性地编写代码,并进行一些工作,即使你不能详细解释它。

避免的事情

如果你决定学习 Python,你肯定会想要从一本书上学习。只需从你当地的图书馆拿起任何一本 Python 的初学者书籍即可。但在阅读时,请牢记你学习目标的大局观。边读书边做一些练习,尝试书中的代码,并创造出你自己的代码。跳过一些页面并不是个坏主意。逐页阅读可能不是学习效率最高的方式。你应该避免深入一个单一主题,因为这会使你忘记使用 Python 做有用事情的更大目标。像多线程、网络套接字和面向对象编程这样的主题可以作为以后的高级主题来处理。

Python 是一种与其解释器或编译器解耦的语言。因此,不同的解释器可能会有所不同。来自 python.org 的标准解释器是 CPython,也称为参考实现。一个常见的替代方案是 PyPy。无论你使用哪一种,你应该学习 Python 3 而不是 Python 2,因为后者是一种已经过时的方言。但请记住,Python 在 Python 2 时期获得了它的势头,你可能仍然会看到很多 Python 2 编程的内容。

资源

阅读资源

如果你不能去图书馆拿一本印刷书,你可以利用一些在线资源。我强烈推荐初学者阅读Python 教程。它很短,但会引导你了解语言的不同方面。它让你一窥 Python 的能力和如何使用它的方法。

教程结束后,你可能需要随时保持Python 语言参考Python 库参考在手边。你会时不时地参考它们,检查语法并查找函数用法。不要强迫自己记住每个函数。

编程环境

Python 在 macOS 中是内置的,但你可能想安装一个更新版本。在 Windows 中,人们通常使用 Anaconda 而不是仅安装 Python 解释器。但如果你觉得安装 IDE 和 Python 编程环境太麻烦,你可以考虑使用 Google Colab。这允许你以“笔记本”格式编写 Python 程序。事实上,许多机器学习项目都是在 Jupyter 笔记本中开发的,因为它允许我们快速探索解决问题的不同方法并通过视觉验证结果。

你也可以在 www.python.org/shell/ 使用在线 shell 尝试一个简短的代码片段。与 Google Colab 相比的缺点是你无法保存你的工作。

寻求帮助

当你从书本中看到一个例子并对其进行修改时,可能会导致代码出错,使其无法运行。在机器学习示例中尤其如此,其中有许多行代码涵盖数据收集、预处理、模型构建、训练、验证、预测,最后以可视化方式呈现结果。当你看到代码出错时,首先要做的事情是确定导致错误的几行代码。尝试检查每个步骤的输出以确保其格式正确。或者尝试回滚你的代码,看看哪个修改引入了错误。

犯错误并从错误中学习是很重要的。当你尝试语法并通过这种方式学习时,你可能会时不时地遇到错误消息。如果你试图理解它,那么找出错误的原因就会更容易。几乎总是,如果错误来自于你正在使用的库,确保通过库的文档再次确认你的语法是很重要的。

如果你仍然感到困惑,试着在互联网上搜索。如果你使用的是 Google,你可以使用一个技巧,就是将整个错误消息用双引号括起来搜索。或者有时,在 StackOverflow 上搜索可能会给你更好的答案。

进一步阅读

在这里,我列出了一些初学者的要点。如上所述,Python 教程是一个很好的开始。特别是在撰写此文时,Python 3.9 最近发布,并引入了一些新的语法。印刷版的书籍通常没有官方在线教程更新得及时。

Python 有许多入门级书籍。我知道一些简短的书籍,比如:

对于稍微进阶的学习者,你可能希望看更多的例子来完成某些任务。像烹饪书式的书籍可能会帮助很多,因为你不仅可以学习语法和语言技巧,还可以了解可以完成任务的不同库。

  • Python Cookbook,第三版,由 David Beazley 和 Brian K. Jones,2013 年。

概要

在本文中,你学到了如何学习 Python 以及可以帮助你入门的资源。目标导向的学习方法可以帮助你更快地获得结果。然而,一如既往,你需要在掌握之前投入相当多的时间。

Python 中的日志记录

原文:machinelearningmastery.com/logging-in-python/

日志记录是一种存储有关脚本的信息并跟踪发生事件的方式。在编写任何复杂的 Python 脚本时,日志记录对于在开发过程中调试软件至关重要。没有日志记录,找到代码中的问题源可能会非常耗时。

完成本教程后,你将知道:

  • 为什么我们要使用日志记录模块

  • 如何使用日志记录模块

  • 如何自定义日志记录机制

用我的新书启动你的项目 《Python 机器学习》,包括一步步的教程和所有示例的Python 源代码文件。

让我们开始吧!

Python 中的日志记录

图片由ilaria88提供。一些权利保留。

教程概述

本教程分为四个部分;它们是:

  • 日志记录的好处

  • 基本日志记录

  • 高级日志记录配置

  • 一个使用日志记录的示例

日志记录的好处

你可能会问:“为什么不直接使用打印?”

当你运行一个算法并想确认它是否按预期运行时,通常会在关键位置添加一些print()语句以显示程序的状态。打印可以帮助调试较简单的脚本,但随着代码变得越来越复杂,打印缺乏日志记录所具有的灵活性和鲁棒性。

使用日志记录,你可以精确地找出日志调用的来源,区分消息的严重级别,并将信息写入文件,这是打印无法做到的。例如,我们可以打开或关闭来自大型程序中特定模块的消息。我们还可以增加或减少日志消息的详细程度,而无需修改大量代码。

基本日志记录

Python 有一个内置库logging,用于此目的。创建一个“记录器”以记录你希望看到的消息或信息非常简单。

Python 中的日志系统在一个层级命名空间和不同的严重级别下运行。Python 脚本可以在命名空间下创建一个记录器,每次记录消息时,脚本必须指定其严重级别。记录的消息可以根据我们为命名空间设置的处理程序发送到不同的位置。最常见的处理程序是简单地在屏幕上打印,比如无处不在的print()函数。当我们启动程序时,可以注册一个新处理程序,并设置该处理程序将响应的严重级别。

有 5 种不同的日志记录级别,表示日志的严重程度,按严重程度递增排列:

  1. DEBUG

  2. INFO

  3. WARNING

  4. ERROR

  5. CRITICAL

下面是一个非常简单的日志记录示例,使用默认记录器或根记录器:

import logging

logging.debug('Debug message')
logging.info('Info message')
logging.warning('Warning message')
logging.error('Error message')
logging.critical('Critical message')

这些将发出不同严重性的日志消息。虽然有五行日志记录,如果你运行这个脚本,你可能只会看到三行输出,如下所示:

WARNING:root:This is a warning message
ERROR:root:This is an error message
CRITICAL:root:This is a critical message

这是因为根日志记录器默认只打印警告级别或以上的日志消息。然而,以这种方式使用根日志记录器与使用 print()函数没有太大区别。

根日志记录器的设置不是一成不变的。我们可以将根日志记录器配置为输出到特定的文件,更改其默认的严重性级别,并格式化输出。以下是一个示例:

import logging

logging.basicConfig(filename = 'file.log',
                    level = logging.DEBUG,
                    format = '%(asctime)s:%(levelname)s:%(name)s:%(message)s')

logging.debug('Debug message')
logging.info('Info message')
logging.warning('Warning message')
logging.error('Error message')
logging.critical('Critical message')

运行这个脚本不会在屏幕上产生输出,但会在新创建的文件file.log中产生以下内容:

2022-03-22 20:41:08,151:DEBUG:root:Debug message
2022-03-22 20:41:08,152:INFO:root:Info message
2022-03-22 20:41:08,152:WARNING:root:Warning message
2022-03-22 20:41:08,152:ERROR:root:Error message
2022-03-22 20:41:08,152:CRITICAL:root:Critical message

logging.basicConfig()的调用是为了更改根日志记录器。在我们的示例中,我们将处理程序设置为输出到文件而不是屏幕,调整日志级别,以便处理所有级别为 DEBUG 或以上的日志消息,并且还更改日志消息输出的格式以包含时间。

请注意,现在所有五条消息都已输出,因此根日志记录器现在的默认级别为“DEBUG”。可以用来格式化输出的日志记录属性(例如%(asctime)s)可以在日志记录文档中找到。

尽管有一个默认日志记录器,但我们通常希望创建和使用其他可以单独配置的日志记录器。这是因为我们可能希望不同的日志记录器有不同的严重性级别或格式。可以使用以下代码创建新的日志记录器:

logger = logging.getLogger("logger_name")

内部日志记录器是以层级方式组织的。使用以下代码创建的日志记录器:

logger = logging.getLogger("parent.child")

将会是一个在名为“parent”的日志记录器下创建的子日志记录器,而“parent”又在根日志记录器下。字符串中的点表示子日志记录器是父日志记录器的子日志记录器。在上面的情况下,将创建一个名为“parent.child”的日志记录器,以及一个名为parent的日志记录器(隐式创建)。

创建时,子日志记录器具有其父日志记录器的所有属性,直到重新配置。我们可以通过以下示例来演示这一点:

import logging

# Create `parent.child` logger
logger = logging.getLogger("parent.child")

# Emit a log message of level INFO, by default this is not print to the screen
logger.info("this is info level")

# Create `parent` logger
parentlogger = logging.getLogger("parent")

# Set parent's level to INFO and assign a new handler
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter("%(asctime)s:%(name)s:%(levelname)s:%(message)s"))
parentlogger.setLevel(logging.INFO)
parentlogger.addHandler(handler)

# Let child logger emit a log message again
logger.info("this is info level again")

这段代码只会输出一行:

2022-03-28 19:23:29,315:parent.child:INFO:this is info level again

这是由 StreamHandler 对象创建的,具有自定义格式字符串。这仅在我们重新配置parent的日志记录器后发生,否则根日志记录器的配置会占主导地位,并且不会打印级别为 INFO 的消息。

高级日志记录配置

正如我们在上一个示例中看到的,我们可以配置我们创建的日志记录器。

级别阈值

与根日志记录器的基本配置一样,我们也可以配置日志记录器的输出目标、严重性级别和格式。以下是如何将日志记录器的阈值级别设置为 INFO:

parent_logger = logging.getLogger("parent")
parent_logger.setLevel(logging.INFO)

现在,严重性级别为 INFO 及以上的命令将由 parent_logger 记录。但如果你仅做了这些,你将不会看到来自parent_logger.info("messages")的任何信息,因为没有为这个 logger 分配处理器。实际上,根 logger 也没有处理器,除非你通过logging.basicConfig()设置了一个。

日志处理器

我们可以通过处理器配置 logger 的输出目的地。处理器负责将日志消息发送到正确的目的地。有几种类型的处理器;最常见的是StreamHandlerFileHandler。使用StreamHandler时,logger 会输出到终端,而使用FileHandler时,logger 会输出到特定的文件。

下面是使用StreamHandler将日志输出到终端的示例:

import logging

# Set up root logger, and add a file handler to root logger
logging.basicConfig(filename = 'file.log',
                    level = logging.WARNING,
                    format = '%(asctime)s:%(levelname)s:%(name)s:%(message)s')

# Create logger, set level, and add stream handler
parent_logger = logging.getLogger("parent")
parent_logger.setLevel(logging.INFO)
parent_shandler = logging.StreamHandler()
parent_logger.addHandler(parent_shandler)

# Log message of severity INFO or above will be handled
parent_logger.debug('Debug message')
parent_logger.info('Info message')
parent_logger.warning('Warning message')
parent_logger.error('Error message')
parent_logger.critical('Critical message')

在上面的代码中,创建了两个处理器:一个由logging.basicConfig()为根 logger 创建的FileHandler,和一个为parent logger 创建的StreamHandler

请注意,即使存在一个将日志发送到终端的StreamHandler,来自parent logger 的日志仍然被发送到file.log,因为它是根 logger 的子 logger,根 logger 的处理器也对子 logger 的日志消息有效。我们可以看到,发送到终端的日志包括 INFO 级别及以上的消息:

Info message
Warning message
Error message
Critical message

但是终端的输出没有格式化,因为我们还没有使用Formatter。然而,file.log的日志已经设置了Formatter,它将像下面这样:

2022-03-22 23:07:12,533:INFO:parent:Info message
2022-03-22 23:07:12,533:WARNING:parent:Warning message
2022-03-22 23:07:12,533:ERROR:parent:Error message
2022-03-22 23:07:12,533:CRITICAL:parent:Critical message

另外,我们可以在上述parent_logger的示例中使用FileHandler

import logging

# Set up root logger, and add a file handler to root logger
logging.basicConfig(filename = 'file.log',
                    level = logging.WARNING,
                    format = '%(asctime)s:%(levelname)s:%(name)s:%(message)s')

# Create logger, set level, and add stream handler
parent_logger = logging.getLogger("parent")
parent_logger.setLevel(logging.INFO)
parent_fhandler = logging.FileHandler('parent.log')
parent_fhandler.setLevel(logging.WARNING)
parent_logger.addHandler(parent_fhandler)

# Log message of severity INFO or above will be handled
parent_logger.debug('Debug message')
parent_logger.info('Info message')
parent_logger.warning('Warning message')
parent_logger.error('Error message')
parent_logger.critical('Critical message')

上面的例子演示了你还可以设置处理器的级别。parent_fhandler的级别会过滤掉非 WARNING 级别或更高级别的日志。然而,如果你将处理器的级别设置为 DEBUG,那将等同于未设置级别,因为 DEBUG 日志会被 logger 的级别(即 INFO)过滤掉。

在这种情况下,输出到parent.log的是:

Warning message
Error message
Critical message

file.log的输出与之前相同。总之,当 logger 记录一个日志消息时,

  1. logger 的级别将决定消息是否足够严重以被处理。如果 logger 的级别未设置,则将使用其父级别(最终是根 logger)进行考虑。

  2. 如果日志消息会被处理,所有在 logger 层次结构中添加过的处理器都会收到该消息的副本。每个处理器的级别将决定这个特定的处理器是否应该处理这个消息。

格式化器

要配置 logger 的格式,我们使用Formatter。它允许我们设置日志的格式,类似于我们在根 logger 的basicConfig()中所做的。这是我们如何将 formatter 添加到处理器中的:

import logging

# Set up root logger, and add a file handler to root logger
logging.basicConfig(filename = 'file.log',
                    level = logging.WARNING,
                    format = '%(asctime)s:%(levelname)s:%(name)s:%(message)s')

# Create logger, set level, and add stream handler
parent_logger = logging.getLogger("parent")
parent_logger.setLevel(logging.INFO)
parent_fhandler = logging.FileHandler('parent.log')
parent_fhandler.setLevel(logging.WARNING)
parent_formatter = logging.Formatter('%(asctime)s:%(levelname)s:%(message)s')
parent_fhandler.setFormatter(parent_formatter)
parent_logger.addHandler(parent_fhandler)

# Log message of severity INFO or above will be handled
parent_logger.debug('Debug message')
parent_logger.info('Info message')
parent_logger.warning('Warning message')
parent_logger.error('Error message')
parent_logger.critical('Critical message')

首先,我们创建一个 formatter,然后设置我们的处理器使用那个 formatter。如果我们愿意,我们可以创建多个不同的 loggers、handlers 和 formatters,以便根据我们的喜好进行组合。

在这个示例中,parent.log将会有:

2022-03-23 13:28:31,302:WARNING:Warning message
2022-03-23 13:28:31,302:ERROR:Error message
2022-03-23 13:28:31,303:CRITICAL:Critical message

与根记录器相关联的file.log将会有:

2022-03-23 13:28:31,302:INFO:parent:Info message
2022-03-23 13:28:31,302:WARNING:parent:Warning message
2022-03-23 13:28:31,302:ERROR:parent:Error message
2022-03-23 13:28:31,303:CRITICAL:parent:Critical message

以下是日志模块文档中日志记录器、处理程序和格式化程序的流程可视化:

日志模块中记录器和处理程序的流程图

使用日志记录的示例

让我们以Nadam 算法为例:

# gradient descent optimization with nadam for a two-dimensional test function
from math import sqrt
from numpy import asarray
from numpy.random import rand
from numpy.random import seed

# objective function
def objective(x, y):
	return x**2.0 + y**2.0

# derivative of objective function
def derivative(x, y):
	return asarray([x * 2.0, y * 2.0])

# gradient descent algorithm with nadam
def nadam(objective, derivative, bounds, n_iter, alpha, mu, nu, eps=1e-8):
	# generate an initial point
	x = bounds[:, 0] + rand(len(bounds)) * (bounds[:, 1] - bounds[:, 0])
	score = objective(x[0], x[1])
	# initialize decaying moving averages
	m = [0.0 for _ in range(bounds.shape[0])]
	n = [0.0 for _ in range(bounds.shape[0])]
	# run the gradient descent
	for t in range(n_iter):
		# calculate gradient g(t)
		g = derivative(x[0], x[1])
		# build a solution one variable at a time
		for i in range(bounds.shape[0]):
			# m(t) = mu * m(t-1) + (1 - mu) * g(t)
			m[i] = mu * m[i] + (1.0 - mu) * g[i]
			# n(t) = nu * n(t-1) + (1 - nu) * g(t)²
			n[i] = nu * n[i] + (1.0 - nu) * g[i]**2
			# mhat = (mu * m(t) / (1 - mu)) + ((1 - mu) * g(t) / (1 - mu))
			mhat = (mu * m[i] / (1.0 - mu)) + ((1 - mu) * g[i] / (1.0 - mu))
			# nhat = nu * n(t) / (1 - nu)
			nhat = nu * n[i] / (1.0 - nu)
			# x(t) = x(t-1) - alpha / (sqrt(nhat) + eps) * mhat
			x[i] = x[i] - alpha / (sqrt(nhat) + eps) * mhat
		# evaluate candidate point
		score = objective(x[0], x[1])
		# report progress
		print('>%d f(%s) = %.5f' % (t, x, score))
	return [x, score]

# seed the pseudo random number generator
seed(1)
# define range for input
bounds = asarray([[-1.0, 1.0], [-1.0, 1.0]])
# define the total iterations
n_iter = 50
# steps size
alpha = 0.02
# factor for average gradient
mu = 0.8
# factor for average squared gradient
nu = 0.999
# perform the gradient descent search with nadam
best, score = nadam(objective, derivative, bounds, n_iter, alpha, mu, nu)
print('Done!')
print('f(%s) = %f' % (best, score))

最简单的用例是使用日志记录替换print()函数。我们可以进行如下更改:首先,在运行任何代码之前创建一个名为nadam的记录器,并分配一个StreamHandler

...

import logging

...

# Added: Create logger and assign handler
logger = logging.getLogger("nadam")
handler  = logging.StreamHandler()
handler.setFormatter(logging.Formatter("%(asctime)s|%(levelname)s|%(name)s|%(message)s"))
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)
# seed the pseudo random number generator
seed(1)
... # rest of the code

我们必须分配一个处理程序,因为我们从未配置根记录器,这将是唯一创建的处理程序。然后,在函数nadam()中,我们重新创建了一个记录器nadam,但由于它已经被设置过,级别和处理程序得以保留。在nadam()的每个外层 for 循环结束时,我们用logger.info()替换了print()函数,这样消息将由日志系统处理:

...

# gradient descent algorithm with nadam
def nadam(objective, derivative, bounds, n_iter, alpha, mu, nu, eps=1e-8):
    # Create a logger
    logger = logging.getLogger("nadam")
    # generate an initial point
    x = bounds[:, 0] + rand(len(bounds)) * (bounds[:, 1] - bounds[:, 0])
    score = objective(x[0], x[1])
    # initialize decaying moving averages
    m = [0.0 for _ in range(bounds.shape[0])]
    n = [0.0 for _ in range(bounds.shape[0])]
    # run the gradient descent
    for t in range(n_iter):
        # calculate gradient g(t)
        g = derivative(x[0], x[1])
        # build a solution one variable at a time
        for i in range(bounds.shape[0]):
            # m(t) = mu * m(t-1) + (1 - mu) * g(t)
            m[i] = mu * m[i] + (1.0 - mu) * g[i]
            # n(t) = nu * n(t-1) + (1 - nu) * g(t)²
            n[i] = nu * n[i] + (1.0 - nu) * g[i]**2
            # mhat = (mu * m(t) / (1 - mu)) + ((1 - mu) * g(t) / (1 - mu))
            mhat = (mu * m[i] / (1.0 - mu)) + ((1 - mu) * g[i] / (1.0 - mu))
            # nhat = nu * n(t) / (1 - nu)
            nhat = nu * n[i] / (1.0 - nu)
            # x(t) = x(t-1) - alpha / (sqrt(nhat) + eps) * mhat
            x[i] = x[i] - alpha / (sqrt(nhat) + eps) * mhat
        # evaluate candidate point
        score = objective(x[0], x[1])
        # report progress using logger
        logger.info('>%d f(%s) = %.5f' % (t, x, score))
    return [x, score]

...

如果我们对 Nadam 算法的更深层次机制感兴趣,我们可以添加更多日志。以下是完整的代码:

# gradient descent optimization with nadam for a two-dimensional test function
import logging
from math import sqrt
from numpy import asarray
from numpy.random import rand
from numpy.random import seed

# objective function
def objective(x, y):
    return x**2.0 + y**2.0

# derivative of objective function
def derivative(x, y):
    return asarray([x * 2.0, y * 2.0])

# gradient descent algorithm with nadam
def nadam(objective, derivative, bounds, n_iter, alpha, mu, nu, eps=1e-8):
    logger = logging.getLogger("nadam")
    # generate an initial point
    x = bounds[:, 0] + rand(len(bounds)) * (bounds[:, 1] - bounds[:, 0])
    score = objective(x[0], x[1])
    # initialize decaying moving averages
    m = [0.0 for _ in range(bounds.shape[0])]
    n = [0.0 for _ in range(bounds.shape[0])]
    # run the gradient descent
    for t in range(n_iter):
        iterlogger = logging.getLogger("nadam.iter")
        # calculate gradient g(t)
        g = derivative(x[0], x[1])
        # build a solution one variable at a time
        for i in range(bounds.shape[0]):
            # m(t) = mu * m(t-1) + (1 - mu) * g(t)
            m[i] = mu * m[i] + (1.0 - mu) * g[i]
            # n(t) = nu * n(t-1) + (1 - nu) * g(t)²
            n[i] = nu * n[i] + (1.0 - nu) * g[i]**2
            # mhat = (mu * m(t) / (1 - mu)) + ((1 - mu) * g(t) / (1 - mu))
            mhat = (mu * m[i] / (1.0 - mu)) + ((1 - mu) * g[i] / (1.0 - mu))
            # nhat = nu * n(t) / (1 - nu)
            nhat = nu * n[i] / (1.0 - nu)
            # x(t) = x(t-1) - alpha / (sqrt(nhat) + eps) * mhat
            x[i] = x[i] - alpha / (sqrt(nhat) + eps) * mhat
            iterlogger.info("Iteration %d variable %d: mhat=%f nhat=%f", t, i, mhat, nhat)
        # evaluate candidate point
        score = objective(x[0], x[1])
        # report progress
        logger.info('>%d f(%s) = %.5f' % (t, x, score))
    return [x, score]

# Create logger and assign handler
logger = logging.getLogger("nadam")
handler  = logging.StreamHandler()
handler.setFormatter(logging.Formatter("%(asctime)s|%(levelname)s|%(name)s|%(message)s"))
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)
logger = logging.getLogger("nadam.iter")
logger.setLevel(logging.INFO)
# seed the pseudo random number generator
seed(1)
# define range for input
bounds = asarray([[-1.0, 1.0], [-1.0, 1.0]])
# define the total iterations
n_iter = 50
# steps size
alpha = 0.02
# factor for average gradient
mu = 0.8
# factor for average squared gradient
nu = 0.999
# perform the gradient descent search with nadam
best, score = nadam(objective, derivative, bounds, n_iter, alpha, mu, nu)
print('Done!')
print('f(%s) = %f' % (best, score))

我们准备了两个级别的记录器,nadamnadam.iter,并将它们设置在不同的级别。在nadam()的内层循环中,我们使用子记录器来打印一些内部变量。当你运行这个脚本时,它将打印如下内容:

2022-03-29 12:24:59,421|INFO|nadam.iter|Iteration 0 variable 0: mhat=-0.597442 nhat=0.110055
2022-03-29 12:24:59,421|INFO|nadam.iter|Iteration 0 variable 1: mhat=1.586336 nhat=0.775909
2022-03-29 12:24:59,421|INFO|nadam|>0 f([-0.12993798  0.40463097]) = 0.18061
2022-03-29 12:24:59,421|INFO|nadam.iter|Iteration 1 variable 0: mhat=-0.680200 nhat=0.177413
2022-03-29 12:24:59,421|INFO|nadam.iter|Iteration 1 variable 1: mhat=2.020702 nhat=1.429384
2022-03-29 12:24:59,421|INFO|nadam|>1 f([-0.09764012  0.37082777]) = 0.14705
2022-03-29 12:24:59,421|INFO|nadam.iter|Iteration 2 variable 0: mhat=-0.687764 nhat=0.215332
2022-03-29 12:24:59,421|INFO|nadam.iter|Iteration 2 variable 1: mhat=2.304132 nhat=1.977457
2022-03-29 12:24:59,421|INFO|nadam|>2 f([-0.06799761  0.33805721]) = 0.11891
...
2022-03-29 12:24:59,449|INFO|nadam.iter|Iteration 49 variable 0: mhat=-0.000482 nhat=0.246709
2022-03-29 12:24:59,449|INFO|nadam.iter|Iteration 49 variable 1: mhat=-0.018244 nhat=3.966938
2022-03-29 12:24:59,449|INFO|nadam|>49 f([-5.54299505e-05 -1.00116899e-03]) = 0.00000
Done!
f([-5.54299505e-05 -1.00116899e-03]) = 0.000001

设置不同的日志记录器不仅允许我们设置不同的级别或处理程序,还可以通过查看打印消息中的记录器名称来区分日志消息的来源。

事实上,一个方便的技巧是创建一个日志装饰器,并将其应用于一些函数。我们可以跟踪每次调用该函数的情况。例如,我们下面创建了一个装饰器,并将其应用于objective()derivative()函数:

...

# A Python decorator to log the function call and return value
def loggingdecorator(name):
    logger = logging.getLogger(name)
    def _decor(fn):
        function_name = fn.__name__
        def _fn(*args, **kwargs):
            ret = fn(*args, **kwargs)
            argstr = [str(x) for x in args]
            argstr += [key+"="+str(val) for key,val in kwargs.items()]
            logger.debug("%s(%s) -> %s", function_name, ", ".join(argstr), ret)
            return ret
        return _fn
    return _decor

# objective function
@loggingdecorator("nadam.function")
def objective(x, y):
    return x**2.0 + y**2.0

# derivative of objective function
@loggingdecorator("nadam.function")
def derivative(x, y):
    return asarray([x * 2.0, y * 2.0])

然后我们将看到日志中出现如下内容:

2022-03-29 13:14:07,542|DEBUG|nadam.function|objective(-0.165955990594852, 0.4406489868843162) -> 0.22171292045649288
2022-03-29 13:14:07,542|DEBUG|nadam.function|derivative(-0.165955990594852, 0.4406489868843162) -> [-0.33191198  0.88129797]
2022-03-29 13:14:07,542|INFO|nadam.iter|Iteration 0 variable 0: mhat=-0.597442 nhat=0.110055
2022-03-29 13:14:07,542|INFO|nadam.iter|Iteration 0 variable 1: mhat=1.586336 nhat=0.775909
2022-03-29 13:14:07,542|DEBUG|nadam.function|objective(-0.12993797816930272, 0.4046309737819536) -> 0.18061010311445824
2022-03-29 13:14:07,543|INFO|nadam|>0 f([-0.12993798  0.40463097]) = 0.18061
2022-03-29 13:14:07,543|DEBUG|nadam.function|derivative(-0.12993797816930272, 0.4046309737819536) -> [-0.25987596  0.80926195]
2022-03-29 13:14:07,543|INFO|nadam.iter|Iteration 1 variable 0: mhat=-0.680200 nhat=0.177413
2022-03-29 13:14:07,543|INFO|nadam.iter|Iteration 1 variable 1: mhat=2.020702 nhat=1.429384
2022-03-29 13:14:07,543|DEBUG|nadam.function|objective(-0.09764011794760165, 0.3708277653552375) -> 0.14704682419118062
2022-03-29 13:14:07,543|INFO|nadam|>1 f([-0.09764012  0.37082777]) = 0.14705
2022-03-29 13:14:07,543|DEBUG|nadam.function|derivative(-0.09764011794760165, 0.3708277653552375) -> [-0.19528024  0.74165553]
2022-03-29 13:14:07,543|INFO|nadam.iter|Iteration 2 variable 0: mhat=-0.687764 nhat=0.215332
...

在日志消息中,我们可以看到每次调用这两个函数的参数和返回值,这些信息由nadam.function记录器记录。

想要开始使用 Python 进行机器学习吗?

立即参加我的免费 7 天电子邮件速成课程(附有示例代码)。

点击注册并获得课程的免费 PDF 电子书版本。

随着日志信息越来越多,终端屏幕将变得非常繁忙。为了更容易地监视问题,可以使用colorama模块来高亮显示日志。你需要首先安装这个模块:

pip install colorama

下面是一个如何使用colorama模块与logging模块结合,以改变日志颜色和文本亮度的示例:

import logging
import colorama
from colorama import Fore, Back, Style

# Initialize the terminal for color
colorama.init(autoreset = True)

# Set up logger as usual
logger = logging.getLogger("color")
logger.setLevel(logging.DEBUG)
shandler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s:%(levelname)s:%(name)s:%(message)s')
shandler.setFormatter(formatter)
logger.addHandler(shandler)

# Emit log message with color
logger.debug('Debug message')
logger.info(Fore.GREEN + 'Info message')
logger.warning(Fore.BLUE + 'Warning message')
logger.error(Fore.YELLOW + Style.BRIGHT + 'Error message')
logger.critical(Fore.RED + Back.YELLOW + Style.BRIGHT + 'Critical message')

从终端中,你将看到如下内容:

其中,colorama模块中的ForeBackStyle控制文本的前景、背景和亮度样式。这利用了 ANSI 转义字符,仅在支持 ANSI 的终端上有效。因此,这不适用于日志记录到文本文件。

实际上,我们可以通过以下方式派生Formatter类:

...
colors = {"DEBUG":Fore.BLUE, "INFO":Fore.CYAN,
          "WARNING":Fore.YELLOW, "ERROR":Fore.RED, "CRITICAL":Fore.MAGENTA}
class ColoredFormatter(logging.Formatter):
    def format(self, record):
        msg = logging.Formatter.format(self, record)
        if record.levelname in colors:
            msg = colors[record.levelname] + msg + Fore.RESET
        return msg

并使用它来替代logging.Formatter。以下是我们如何进一步修改 Nadam 示例以添加颜色:

# gradient descent optimization with nadam for a two-dimensional test function
import logging
import colorama
from colorama import Fore

from math import sqrt
from numpy import asarray
from numpy.random import rand
from numpy.random import seed

def loggingdecorator(name):
    logger = logging.getLogger(name)
    def _decor(fn):
        function_name = fn.__name__
        def _fn(*args, **kwargs):
            ret = fn(*args, **kwargs)
            argstr = [str(x) for x in args]
            argstr += [key+"="+str(val) for key,val in kwargs.items()]
            logger.debug("%s(%s) -> %s", function_name, ", ".join(argstr), ret)
            return ret
        return _fn
    return _decor

# objective function
@loggingdecorator("nadam.function")
def objective(x, y):
    return x**2.0 + y**2.0

# derivative of objective function
@loggingdecorator("nadam.function")
def derivative(x, y):
    return asarray([x * 2.0, y * 2.0])

# gradient descent algorithm with nadam
def nadam(objective, derivative, bounds, n_iter, alpha, mu, nu, eps=1e-8):
    logger = logging.getLogger("nadam")
    # generate an initial point
    x = bounds[:, 0] + rand(len(bounds)) * (bounds[:, 1] - bounds[:, 0])
    score = objective(x[0], x[1])
    # initialize decaying moving averages
    m = [0.0 for _ in range(bounds.shape[0])]
    n = [0.0 for _ in range(bounds.shape[0])]
    # run the gradient descent
    for t in range(n_iter):
        iterlogger = logging.getLogger("nadam.iter")
        # calculate gradient g(t)
        g = derivative(x[0], x[1])
        # build a solution one variable at a time
        for i in range(bounds.shape[0]):
            # m(t) = mu * m(t-1) + (1 - mu) * g(t)
            m[i] = mu * m[i] + (1.0 - mu) * g[i]
            # n(t) = nu * n(t-1) + (1 - nu) * g(t)²
            n[i] = nu * n[i] + (1.0 - nu) * g[i]**2
            # mhat = (mu * m(t) / (1 - mu)) + ((1 - mu) * g(t) / (1 - mu))
            mhat = (mu * m[i] / (1.0 - mu)) + ((1 - mu) * g[i] / (1.0 - mu))
            # nhat = nu * n(t) / (1 - nu)
            nhat = nu * n[i] / (1.0 - nu)
            # x(t) = x(t-1) - alpha / (sqrt(nhat) + eps) * mhat
            x[i] = x[i] - alpha / (sqrt(nhat) + eps) * mhat
            iterlogger.info("Iteration %d variable %d: mhat=%f nhat=%f", t, i, mhat, nhat)
        # evaluate candidate point
        score = objective(x[0], x[1])
        # report progress
        logger.warning('>%d f(%s) = %.5f' % (t, x, score))
    return [x, score]

# Prepare the colored formatter
colorama.init(autoreset = True)
colors = {"DEBUG":Fore.BLUE, "INFO":Fore.CYAN,
          "WARNING":Fore.YELLOW, "ERROR":Fore.RED, "CRITICAL":Fore.MAGENTA}
class ColoredFormatter(logging.Formatter):
    def format(self, record):
        msg = logging.Formatter.format(self, record)
        if record.levelname in colors:
            msg = colors[record.levelname] + msg + Fore.RESET
        return msg

# Create logger and assign handler
logger = logging.getLogger("nadam")
handler  = logging.StreamHandler()
handler.setFormatter(ColoredFormatter("%(asctime)s|%(levelname)s|%(name)s|%(message)s"))
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)
logger = logging.getLogger("nadam.iter")
logger.setLevel(logging.DEBUG)
# seed the pseudo random number generator
seed(1)
# define range for input
bounds = asarray([[-1.0, 1.0], [-1.0, 1.0]])
# define the total iterations
n_iter = 50
# steps size
alpha = 0.02
# factor for average gradient
mu = 0.8
# factor for average squared gradient
nu = 0.999
# perform the gradient descent search with nadam
best, score = nadam(objective, derivative, bounds, n_iter, alpha, mu, nu)
print('Done!')
print('f(%s) = %f' % (best, score))

如果我们在支持的终端上运行它,我们将看到以下输出:

注意,多彩的输出可以帮助我们更容易地发现任何异常行为。日志记录有助于调试,并且允许我们通过仅更改几行代码轻松控制我们想要查看的详细程度。

进一步阅读

本节提供了更多关于该主题的资源,如果你想深入了解。

API

文章

总结

在本教程中,你学习了如何在脚本中实现日志记录技术。

具体来说,你学到了:

  • 基本和高级日志记录技术

  • 如何将日志记录应用于脚本以及这样做的好处

管理机器学习项目的数据

原文:machinelearningmastery.com/managing-data-for-machine-learning-project/

大数据、标记数据、噪声数据。机器学习项目都需要查看数据。数据是机器学习项目的一个关键方面,我们如何处理这些数据是我们项目的重要考虑因素。当数据量增长,需要管理这些数据,或让它们服务于多个项目,或者只是需要更好的数据检索方式时,考虑使用数据库系统是很自然的。这可以是关系型数据库或平面文件格式。它可以是本地的,也可以是远程的。

在这篇文章中,我们探讨了可以用于在 Python 中存储和检索数据的不同格式和库。

完成本教程后,你将学会:

  • 使用 SQLite、Python dbm 库、Excel 和 Google Sheets 管理数据

  • 如何使用外部存储的数据来训练你的机器学习模型

  • 在机器学习项目中使用数据库的优缺点

启动你的项目,可以参考我新书 《Python 机器学习》,其中包含 一步一步的教程 和所有示例的 Python 源代码 文件。

让我们开始吧!

使用 Python 管理数据

图片由 比尔·本宗 提供。一些权利保留。

概述

本教程分为七个部分;它们是:

  • 在 SQLite 中管理数据

  • SQLite 的实际应用

  • 在 dbm 中管理数据

  • 在机器学习流程中使用 dbm 数据库

  • 在 Excel 中管理数据

  • 在 Google Sheets 中管理数据

  • 数据库的其他用途

在 SQLite 中管理数据

当我们提到数据库时,它通常指的是以表格格式存储数据的关系型数据库。

首先,让我们从 sklearn.dataset 获取一个表格数据集(要了解更多关于获取机器学习数据集的信息,请查看我们的 上一篇文章)。

# Read dataset from OpenML
from sklearn.datasets import fetch_openml
dataset = fetch_openml("diabetes", version=1, as_frame=True, return_X_y=False)["frame"]

上述代码读取了来自 OpenML 的“皮马印第安人糖尿病数据集”并创建了一个 pandas DataFrame。这是一个具有多个数值特征和一个二元类别标签的分类数据集。我们可以使用以下方法探索 DataFrame:

print(type(dataset))
print(dataset.head())

这为我们提供了:

<class 'pandas.core.frame.DataFrame'>
   preg   plas  pres  skin   insu  mass   pedi   age            class
0   6.0  148.0  72.0  35.0    0.0  33.6  0.627  50.0  tested_positive
1   1.0   85.0  66.0  29.0    0.0  26.6  0.351  31.0  tested_negative
2   8.0  183.0  64.0   0.0    0.0  23.3  0.672  32.0  tested_positive
3   1.0   89.0  66.0  23.0   94.0  28.1  0.167  21.0  tested_negative
4   0.0  137.0  40.0  35.0  168.0  43.1  2.288  33.0  tested_positive

这不是一个非常大的数据集,但如果它太大,我们可能无法将其放入内存。关系数据库是帮助我们高效管理表格数据的工具,而不必将所有内容保留在内存中。通常,关系数据库将理解 SQL 的一个方言,这是一种描述对数据进行操作的语言。SQLite 是一种无服务器数据库系统,不需要任何设置,在 Python 中有内置库支持。接下来,我们将演示如何利用 SQLite 来管理数据,但使用其他数据库如 MariaDB 或 PostgreSQL 也是非常相似的。

现在,让我们从在 SQLite 中创建一个内存数据库开始,并获取一个游标对象,以便我们可以执行对我们新数据库的查询:

import sqlite3

conn = sqlite3.connect(":memory:")
cur = conn.cursor()

如果我们想把数据存储在硬盘上,以便稍后重复使用或与另一个程序共享,我们可以将数据库存储在数据库文件中,而不是像上面的代码片段中替换魔术字符串:memory:一样,可以用文件名(例如example.db)来替换,如下所示:

conn = sqlite3.connect("example.db")

现在,让我们继续为我们的糖尿病数据创建一个新表。

...
create_sql = """
    CREATE TABLE diabetes(
        preg NUM,
        plas NUM, 
        pres NUM,
        skin NUM,
        insu NUM,
        mass NUM,
        pedi NUM,
        age NUM,
        class TEXT
    )
"""
cur.execute(create_sql)

cur.execute()方法执行我们传递给它作为参数的 SQL 查询。在本例中,SQL 查询创建了diabetes表,其中包含不同的列及其各自的数据类型。SQL 的语言不在此描述,但您可以从许多数据库书籍和课程中了解更多。

接下来,我们可以继续将存储在 pandas DataFrame 中的糖尿病数据集中的数据插入到我们新创建的糖尿病表中,这个表位于我们的内存 SQL 数据库中。

# Prepare a parameterized SQL for insert
insert_sql = "INSERT INTO diabetes VALUES (?,?,?,?,?,?,?,?,?)"
# execute the SQL multiple times with each element in dataset.to_numpy().tolist()
cur.executemany(insert_sql, dataset.to_numpy().tolist())

让我们分解上述代码:dataset.to_numpy().tolist()给我们提供了dataset中数据的行列表,我们将其作为参数传递给cur.executemany()。然后,cur.executemany()多次运行 SQL 语句,每次使用从dataset.to_numpy().tolist()得到的数据行,这是从dataset中获取的数据行。参数化的 SQL 期望每次传递一个值列表,因此我们应该将列表的列表传递给executemany(),这就是dataset.to_numpy().tolist()创建的内容。

现在,我们可以检查确认所有数据都存储在数据库中:

import pandas as pd

def cursor2dataframe(cur):
    """Read the column header from the cursor and then the rows of
    data from it. Afterwards, create a DataFrame"""
    header = [x[0] for x in cur.description]
    # gets data from the last executed SQL query
    data = cur.fetchall()
    # convert the data into a pandas DataFrame
    return pd.DataFrame(data, columns=header)

# get 5 random rows from the diabetes table
select_sql = "SELECT * FROM diabetes ORDER BY random() LIMIT 5"
cur.execute(select_sql)
sample = cursor2dataframe(cur)
print(sample)

在上述代码中,我们使用 SQL 中的SELECT语句来查询diabetes表的 5 行随机数据。结果将作为元组列表返回(每行一个元组)。然后,我们通过为每列分配一个名称,将元组列表转换为 pandas DataFrame。运行上面的代码片段,我们会得到这个输出:

   preg  plas  pres  skin  insu  mass   pedi  age            class
0     2    90    68    42     0  38.2  0.503   27  tested_positive
1     9   124    70    33   402  35.4  0.282   34  tested_negative
2     7   160    54    32   175  30.5  0.588   39  tested_positive
3     7   105     0     0     0   0.0  0.305   24  tested_negative
4     1   107    68    19     0  26.5  0.165   24  tested_negative

这里是使用sqlite3创建、插入和检索糖尿病数据集中样本的完整代码:

import sqlite3

import pandas as pd
from sklearn.datasets import fetch_openml

# Read dataset from OpenML
dataset = fetch_openml("diabetes", version=1, as_frame=True, return_X_y=False)["frame"]
print("Data from OpenML:")
print(type(dataset))
print(dataset.head())

# Create database
conn = sqlite3.connect(":memory:")
cur = conn.cursor()
create_sql = """
    CREATE TABLE diabetes(
        preg NUM,
        plas NUM,
        pres NUM,
        skin NUM,
        insu NUM,
        mass NUM,
        pedi NUM,
        age NUM,
        class TEXT
    )
"""
cur.execute(create_sql)

# Insert data into the table using a parameterized SQL
insert_sql = "INSERT INTO diabetes VALUES (?,?,?,?,?,?,?,?,?)"
rows = dataset.to_numpy().tolist()
cur.executemany(insert_sql, rows)

def cursor2dataframe(cur):
    """Read the column header from the cursor and then the rows of
    data from it. Afterwards, create a DataFrame"""
    header = [x[0] for x in cur.description]
    # gets data from the last executed SQL query
    data = cur.fetchall()
    # convert the data into a pandas DataFrame
    return pd.DataFrame(data, columns=header)

# get 5 random rows from the diabetes table
select_sql = "SELECT * FROM diabetes ORDER BY random() LIMIT 5"
cur.execute(select_sql)
sample = cursor2dataframe(cur)
print("Data from SQLite database:")
print(sample)

# close database connection
conn.commit()
conn.close()

使用数据库的好处在于,当数据集不是从互联网获取,而是由你随时间收集时,会更加明显。例如,你可能会在多天内从传感器收集数据。你可以通过自动化任务将每小时收集的数据写入数据库。然后,你的机器学习项目可以使用数据库中的数据集运行,你可能会看到随着数据的积累而不同的结果。

让我们看看如何将我们的关系数据库构建到机器学习流程中!

SQLite 实战

现在我们已经探讨了如何使用 sqlite3 存储和检索数据,我们可能会对如何将其集成到我们的机器学习流程中感兴趣。

通常,在这种情况下,我们会有一个过程来收集数据并将其写入数据库(例如,从传感器读取数据多天)。这将类似于前一节中的代码,只不过我们更愿意将数据库写入磁盘以便持久存储。然后,我们将从数据库中读取数据用于机器学习过程,无论是训练还是预测。根据模型的不同,有不同的方式来使用数据。让我们考虑 Keras 中的一个二分类模型用于糖尿病数据集。我们可以构建一个生成器,从数据库中读取随机批次的数据:

def datagen(batch_size):
    conn = sqlite3.connect("diabetes.db", check_same_thread=False)
    cur = conn.cursor()
    sql = f"""
        SELECT preg, plas, pres, skin, insu, mass, pedi, age, class
        FROM diabetes
        ORDER BY random()
        LIMIT {batch_size}
    """
    while True:
        cur.execute(sql)
        data = cur.fetchall()
        X = [row[:-1] for row in data]
        y = [1 if row[-1]=="tested_positive" else 0 for row in data]
        yield np.asarray(X), np.asarray(y)

上述代码是一个生成器函数,它从 SQLite 数据库中获取batch_size数量的行,并将其作为 NumPy 数组返回。我们可以使用来自这个生成器的数据在我们的分类网络中进行训练:

from keras.models import Sequential
from keras.layers import Dense

# create binary classification model
model = Sequential()
model.add(Dense(16, input_dim=8, activation='relu'))
model.add(Dense(8, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])

# train model
history = model.fit(datagen(32), epochs=5, steps_per_epoch=2000)

运行上述代码会给我们以下输出:

Epoch 1/5
2000/2000 [==============================] - 6s 3ms/step - loss: 2.2360 - accuracy: 0.6730
Epoch 2/5
2000/2000 [==============================] - 5s 2ms/step - loss: 0.5292 - accuracy: 0.7380
Epoch 3/5
2000/2000 [==============================] - 5s 2ms/step - loss: 0.4936 - accuracy: 0.7564
Epoch 4/5
2000/2000 [==============================] - 5s 2ms/step - loss: 0.4751 - accuracy: 0.7662
Epoch 5/5
2000/2000 [==============================] - 5s 2ms/step - loss: 0.4487 - accuracy: 0.7834

请注意,我们在生成器函数中只读取一个批次,而不是全部数据。我们依赖数据库提供数据,而不关心数据库中数据集的大小。虽然 SQLite 不是一个客户端-服务器数据库系统,因此不适合网络扩展,但还有其他数据库系统可以做到这一点。因此,你可以想象在只提供有限内存的情况下使用异常庞大的数据集进行机器学习应用。

以下是完整的代码,从准备数据库到使用实时读取的数据训练 Keras 模型:

import sqlite3

import numpy as np
from sklearn.datasets import fetch_openml
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense

# Create database
conn = sqlite3.connect("diabetes.db")
cur = conn.cursor()
cur.execute("DROP TABLE IF EXISTS diabetes")
create_sql = """
    CREATE TABLE diabetes(
        preg NUM,
        plas NUM,
        pres NUM,
        skin NUM,
        insu NUM,
        mass NUM,
        pedi NUM,
        age NUM,
        class TEXT
    )
"""
cur.execute(create_sql)

# Read data from OpenML, insert data into the table using a parameterized SQL
dataset = fetch_openml("diabetes", version=1, as_frame=True, return_X_y=False)["frame"]
insert_sql = "INSERT INTO diabetes VALUES (?,?,?,?,?,?,?,?,?)"
rows = dataset.to_numpy().tolist()
cur.executemany(insert_sql, rows)

# Commit to flush change to disk, then close connection
conn.commit()
conn.close()

# Create data generator for Keras classifier model
def datagen(batch_size):
    """A generator to produce samples from database
    """
    # Tensorflow may run in different thread, thus needs check_same_thread=False
    conn = sqlite3.connect("diabetes.db", check_same_thread=False)
    cur = conn.cursor()
    sql = f"""
        SELECT preg, plas, pres, skin, insu, mass, pedi, age, class
        FROM diabetes
        ORDER BY random()
        LIMIT {batch_size}
    """
    while True:
        # Read rows from database
        cur.execute(sql)
        data = cur.fetchall()
        # Extract features
        X = [row[:-1] for row in data]
        # Extract targets, encode into binary (0 or 1)
        y = [1 if row[-1]=="tested_positive" else 0 for row in data]
        yield np.asarray(X), np.asarray(y)

# create binary classification model
model = Sequential()
model.add(Dense(16, input_dim=8, activation='relu'))
model.add(Dense(8, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])

# train model
history = model.fit(datagen(32), epochs=5, steps_per_epoch=2000)

在进入下一部分之前,我们应该强调所有数据库都有些许不同。我们使用的 SQL 语句在其他数据库实现中可能并不最优。此外,请注意,SQLite 并不非常先进,因为它的目标是成为一个不需要服务器设置的数据库。使用大型数据库及其优化使用是一个重要话题,但这里演示的概念仍然适用。

想要开始使用 Python 进行机器学习吗?

立即参加我的免费 7 天电子邮件速成课程(附样例代码)。

点击注册并获得课程的免费 PDF 电子书版本。

在 dbm 中管理数据

关系型数据库非常适合表格数据,但并非所有数据集都采用表格结构。有时,数据最适合存储在类似于 Python 字典的结构中,即键值对存储。存在许多键值对数据存储。MongoDB 可能是最著名的一种,它需要像 PostgreSQL 一样进行服务器部署。GNU dbm 是一种无服务器存储,类似于 SQLite,并且几乎每个 Linux 系统中都已安装。在 Python 的标准库中,我们有dbm模块来处理它。

让我们来探索一下 Python 的 dbm 库。该库支持两种不同的 dbm 实现:GNU dbm 和 ndbm。如果系统中未安装这两种实现,则有 Python 自己的实现作为备用。无论底层的 dbm 实现如何,我们在 Python 程序中使用相同的语法。

这次,我们将演示如何使用 scikit-learn 的数字数据集:

import sklearn.datasets

# get digits dataset (8x8 images of digits)
digits = sklearn.datasets.load_digits()

dbm库使用类似字典的接口来存储和检索来自 dbm 文件的数据,将键映射到值,其中键和值都是字符串。将数字数据集存储在文件 digits.dbm 中的代码如下:

import dbm
import pickle

# create file if not exists, otherwise open for read/write
with dbm.open("digits.dbm", "c") as db:
    for idx in range(len(digits.target)):
        db[str(idx)] = pickle.dumps((digits.images[idx], digits.target[idx]))

上述代码片段会在 digits.dbm 文件不存在时创建一个新文件。然后我们从 digits.images 中选择每个数字图像,从 digits.target 中选择标签,并创建一个元组。我们使用数据的偏移量作为键,将元组的 pickle 字符串作为值存储在数据库中。与 Python 的字典不同,dbm 只允许字符串键和序列化值。因此,我们使用 str(idx) 将键转换为字符串,并仅存储 pickle 数据。

你可以在我们的上一篇文章中了解更多关于序列化的内容。

以下是如何从数据库中读取数据:

import random
import numpy as np

# number of images that we want in our sample
batchsize = 4
images = []
targets = []

# open the database and read a sample
with dbm.open("digits.dbm", "r") as db:
    # get all keys from the database
    keys = db.keys()
    # randomly samples n keys
    for key in random.sample(keys, batchsize):
        # go through each key in the random sample
        image, target = pickle.loads(db[key])
        images.append(image)
        targets.append(target)
    print(np.asarray(images), np.asarray(targets))

在上述代码片段中,我们从数据库中获取 4 个随机键,然后获取它们的对应值,并使用 pickle.loads() 进行反序列化。正如我们所知,反序列化的数据将是一个元组;我们将其分配给变量 imagetarget,然后将每个随机样本收集到列表 imagestargets 中。为了方便在 scikit-learn 或 Keras 中进行训练,我们通常更喜欢将整个批次作为 NumPy 数组。

运行上述代码会得到以下输出:

[[[ 0\.  0\.  1\.  9\. 14\. 11\.  1\.  0.]
  [ 0\.  0\. 10\. 15\.  9\. 13\.  5\.  0.]
  [ 0\.  3\. 16\.  7\.  0\.  0\.  0\.  0.]
  [ 0\.  5\. 16\. 16\. 16\. 10\.  0\.  0.]
  [ 0\.  7\. 16\. 11\. 10\. 16\.  5\.  0.]
  [ 0\.  2\. 16\.  5\.  0\. 12\.  8\.  0.]
  [ 0\.  0\. 10\. 15\. 13\. 16\.  5\.  0.]
  [ 0\.  0\.  0\.  9\. 12\.  7\.  0\.  0.]]
...
] [6 8 7 3]

综合起来,这就是检索数字数据集的代码,然后创建、插入和从 dbm 数据库中采样的代码:

import dbm
import pickle
import random

import numpy as np
import sklearn.datasets

# get digits dataset (8x8 images of digits)
digits = sklearn.datasets.load_digits()

# create file if not exists, otherwise open for read/write
with dbm.open("digits.dbm", "c") as db:
    for idx in range(len(digits.target)):
        db[str(idx)] = pickle.dumps((digits.images[idx], digits.target[idx]))

# number of images that we want in our sample
batchsize = 4
images = []
targets = []

# open the database and read a sample
with dbm.open("digits.dbm", "r") as db:
    # get all keys from the database
    keys = db.keys()
    # randomly samples n keys
    for key in random.sample(keys, batchsize):
        # go through each key in the random sample
        image, target = pickle.loads(db[key])
        images.append(image)
        targets.append(target)
    print(np.array(images), np.array(targets))

接下来,让我们看看如何在我们的机器学习管道中使用新创建的 dbm 数据库!

在机器学习管道中使用 dbm 数据库

在这里,你可能意识到我们可以为数字分类创建生成器和 Keras 模型,就像在 SQLite 数据库示例中所做的那样。以下是如何修改代码的步骤。首先是我们的生成器函数。我们只需在循环中选择一个随机批次的键并从 dbm 存储中获取数据:

def datagen(batch_size):
    """A generator to produce samples from database
    """
    with dbm.open("digits.dbm", "r") as db:
        keys = db.keys()
        while True:
            images = []
            targets = []
            for key in random.sample(keys, batch_size):
                image, target = pickle.loads(db[key])
                images.append(image)
                targets.append(target)
            yield np.array(images).reshape(-1,64), np.array(targets)

然后,我们可以为数据创建一个简单的 MLP 模型:

import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense

model = Sequential()
model.add(Dense(32, input_dim=64, activation='relu'))
model.add(Dense(32, activation='relu'))
model.add(Dense(10, activation='softmax'))
model.compile(loss="sparse_categorical_crossentropy",
              optimizer="adam",
              metrics=["sparse_categorical_accuracy"])

history = model.fit(datagen(32), epochs=5, steps_per_epoch=1000)

运行上述代码会产生以下输出:

Epoch 1/5
1000/1000 [==============================] - 3s 2ms/step - loss: 0.6714 - sparse_categorical_accuracy: 0.8090
Epoch 2/5
1000/1000 [==============================] - 2s 2ms/step - loss: 0.1049 - sparse_categorical_accuracy: 0.9688
Epoch 3/5
1000/1000 [==============================] - 2s 2ms/step - loss: 0.0442 - sparse_categorical_accuracy: 0.9875
Epoch 4/5
1000/1000 [==============================] - 2s 2ms/step - loss: 0.0484 - sparse_categorical_accuracy: 0.9850
Epoch 5/5
1000/1000 [==============================] - 2s 2ms/step - loss: 0.0245 - sparse_categorical_accuracy: 0.9935

这就是我们如何使用 dbm 数据库来训练 MLP 以处理数字数据集的。使用 dbm 训练模型的完整代码在这里:

import dbm
import pickle
import random

import numpy as np
import sklearn.datasets
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense

# get digits dataset (8x8 images of digits)
digits = sklearn.datasets.load_digits()

# create file if not exists, otherwise open for read/write
with dbm.open("digits.dbm", "c") as db:
    for idx in range(len(digits.target)):
        db[str(idx)] = pickle.dumps((digits.images[idx], digits.target[idx]))

# retrieving data from database for model
def datagen(batch_size):
    """A generator to produce samples from database
    """
    with dbm.open("digits.dbm", "r") as db:
        keys = db.keys()
        while True:
            images = []
            targets = []
            for key in random.sample(keys, batch_size):
                image, target = pickle.loads(db[key])
                images.append(image)
                targets.append(target)
            yield np.array(images).reshape(-1,64), np.array(targets)

# Classification model in Keras
model = Sequential()
model.add(Dense(32, input_dim=64, activation='relu'))
model.add(Dense(32, activation='relu'))
model.add(Dense(10, activation='softmax'))
model.compile(loss="sparse_categorical_crossentropy",
              optimizer="adam",
              metrics=["sparse_categorical_accuracy"])

# Train with data from dbm store
history = model.fit(datagen(32), epochs=5, steps_per_epoch=1000)

在更高级的系统如 MongoDB 或 Couchbase 中,我们可以简单地要求数据库系统为我们读取随机记录,而不是从所有键的列表中选择随机样本。但思想仍然是相同的;我们可以依赖外部存储来保存我们的数据并管理数据集,而不是在我们的 Python 脚本中完成。

在 Excel 中管理数据

有时候,内存不是我们将数据保存在机器学习脚本之外的原因。原因是有更好的工具来处理数据。也许我们希望拥有能够在屏幕上显示所有数据并允许我们滚动、添加格式和高亮等工具。或者我们希望与不关心我们 Python 程序的其他人分享数据。在需要使用关系数据库的情况下,人们常常使用 Excel 来管理数据。虽然 Excel 可以读取和导出 CSV 文件,但我们可能希望直接处理 Excel 文件。

在 Python 中,有几个库可以处理 Excel 文件,OpenPyXL 是其中最著名的一个。在使用之前,我们需要安装这个库:

pip install openpyxl

目前,Excel 使用的格式是“Open XML Spreadsheet”,文件名以 .xlsx 结尾。较旧的 Excel 文件是以二进制格式保存,文件名后缀为 .xls,这不被 OpenPyXL 支持(在这种情况下,你可以使用 xlrdxlwt 模块进行读写)。

让我们考虑一下在 SQLite 的情况下使用的相同示例。我们可以打开一个新的 Excel 工作簿,将我们的糖尿病数据集作为工作表写入:

import pandas as pd
from sklearn.datasets import fetch_openml
import openpyxl

# Read dataset from OpenML
dataset = fetch_openml("diabetes", version=1, as_frame=True, return_X_y=False)["frame"]
header = list(dataset.columns)
data = dataset.to_numpy().tolist()

# Create Excel workbook and write data into the default worksheet
wb = openpyxl.Workbook()
sheet = wb.active # use the default worksheet
sheet.title = "Diabetes"
for n,colname in enumerate(header):
    sheet.cell(row=1, column=1+n, value=colname)
for n,row in enumerate(data):
    for m,cell in enumerate(row):
        sheet.cell(row=2+n, column=1+m, value=cell)
# Save
wb.save("MLM.xlsx")

上述代码用于为工作表中的每个单元格准备数据(由行和列指定)。当我们创建一个新的 Excel 文件时,默认会有一个工作表。然后,单元格通过行和列偏移来标识,从 1 开始。我们使用以下语法向单元格写入数据:

sheet.cell(row=3, column=4, value="my data")

要从单元格中读取数据,我们使用:

sheet.cell(row=3, column=4).value

一次一个单元格地向 Excel 写入数据是繁琐的,实际上我们可以逐行添加数据。以下是我们如何修改上述代码以逐行操作而不是逐单元格操作:

import pandas as pd
from sklearn.datasets import fetch_openml
import openpyxl

# Read dataset from OpenML
dataset = fetch_openml("diabetes", version=1, as_frame=True, return_X_y=False)["frame"]
header = list(dataset.columns)
data = dataset.to_numpy().tolist()

# Create Excel workbook and write data into the default worksheet
wb = openpyxl.Workbook()
sheet = wb.create_sheet("Diabetes")  # or wb.active for default sheet
sheet.append(header)
for row in data:
    sheet.append(row)
# Save
wb.save("MLM.xlsx")

一旦我们将数据写入文件后,我们可以使用 Excel 直观地浏览数据、添加格式等:

将其用于机器学习项目并不比使用 SQLite 数据库更难。以下是 Keras 中相同的二分类模型,但生成器从 Excel 文件中读取数据:

import random

import numpy as np
import openpyxl
from sklearn.datasets import fetch_openml
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense

# Read data from OpenML
dataset = fetch_openml("diabetes", version=1, as_frame=True, return_X_y=False)["frame"]
header = list(dataset.columns)
rows = dataset.to_numpy().tolist()

# Create Excel workbook and write data into the default worksheet
wb = openpyxl.Workbook()
sheet = wb.active
sheet.title = "Diabetes"
sheet.append(header)
for row in rows:
    sheet.append(row)
# Save
wb.save("MLM.xlsx")

# Create data generator for Keras classifier model
def datagen(batch_size):
    """A generator to produce samples from database
    """
    wb = openpyxl.load_workbook("MLM.xlsx", read_only=True)
    sheet = wb.active
    maxrow = sheet.max_row
    while True:
        # Read rows from Excel file
        X = []
        y = []
        for _ in range(batch_size):
            # data starts at row 2
            row_num = random.randint(2, maxrow)
            rowdata = [cell.value for cell in sheet[row_num]]
            X.append(rowdata[:-1])
            y.append(1 if rowdata[-1]=="tested_positive" else 0)
        yield np.asarray(X), np.asarray(y)

# create binary classification model
model = Sequential()
model.add(Dense(16, input_dim=8, activation='relu'))
model.add(Dense(8, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])

# train model
history = model.fit(datagen(32), epochs=5, steps_per_epoch=20)

在上述操作中,我们故意给 fit() 函数传递 steps_per_epoch=20 参数,因为上面的代码会非常慢。这是因为 OpenPyXL 是用 Python 实现的,以最大化兼容性,但牺牲了编译模块所能提供的速度。因此,最好避免每次都逐行读取 Excel 数据。如果我们需要使用 Excel,较好的选择是一次性将所有数据读入内存,然后直接使用:

import random

import numpy as np
import openpyxl
from sklearn.datasets import fetch_openml
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense

# Read data from OpenML
dataset = fetch_openml("diabetes", version=1, as_frame=True, return_X_y=False)["frame"]
header = list(dataset.columns)
rows = dataset.to_numpy().tolist()

# Create Excel workbook and write data into the default worksheet
wb = openpyxl.Workbook()
sheet = wb.active
sheet.title = "Diabetes"
sheet.append(header)
for row in rows:
    sheet.append(row)
# Save
wb.save("MLM.xlsx")

# Read entire worksheet from the Excel file
wb = openpyxl.load_workbook("MLM.xlsx", read_only=True)
sheet = wb.active
X = []
y = []
for i, row in enumerate(sheet.rows):
    if i==0:
        continue # skip the header row
    rowdata = [cell.value for cell in row]
    X.append(rowdata[:-1])
    y.append(1 if rowdata[-1]=="tested_positive" else 0)
X, y = np.asarray(X), np.asarray(y)

# create binary classification model
model = Sequential()
model.add(Dense(16, input_dim=8, activation='relu'))
model.add(Dense(8, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])

# train model
history = model.fit(X, y, epochs=5)

在 Google Sheets 中管理数据

除了 Excel 工作簿,有时我们会发现 Google Sheets 更方便处理数据,因为它是在“云端”中。我们也可以使用与 Excel 类似的逻辑来管理数据。但首先,我们需要安装一些模块才能在 Python 中访问它:

pip install google-api-python-client google-auth-httplib2 google-auth-oauthlib

假设你有一个 Gmail 账户,并且创建了一个 Google Sheet。你在地址栏看到的 URL,在 /edit 部分之前的部分,告诉你这个表格的 ID,我们将稍后使用这个 ID:

要从 Python 程序访问这个表格,最好为你的代码创建一个服务账户。这是一个通过密钥进行身份验证的机器可操作账户,但由账户所有者管理。你可以控制这个服务账户的权限和过期时间。你也可以随时撤销服务账户,因为它与 Gmail 账户是分开的。

要创建服务账户,首先,你需要前往 Google 开发者控制台,console.developers.google.com,并通过点击“创建项目”按钮来创建一个项目:

你需要提供一个名称,然后点击“创建”:

它会将你带回控制台,但你的项目名称会出现在搜索框旁边。下一步是通过点击搜索框下方的“启用 API 和服务”来启用 API:

由于我们需要创建一个服务账户来使用 Google Sheets,我们在搜索框中搜索“sheets”:

然后点击 Google Sheets API:

并启用它

之后,我们将被送回控制台主屏幕,我们可以点击右上角的“创建凭据”来创建服务账户:

有不同类型的凭据,我们选择“服务账户”:

我们需要提供一个名称(供我们参考)、一个帐户 ID(作为项目的唯一标识符)和一个描述。显示在“服务帐户 ID”框下方的电子邮件地址是该服务帐户的电子邮件。复制它,稍后我们将将其添加到我们的 Google 表中。创建所有这些之后,我们可以跳过其余部分,然后点击“完成”:

当我们完成后,将被送回到主控制台屏幕,并且如果我们在“服务帐户”部分下看到它,就知道服务帐户已经创建:

接下来,我们需要点击帐户右侧的铅笔图标,这将带我们到以下屏幕:

我们需要为此帐户创建一个密钥,而不是密码。我们点击页面顶部的“键”,然后点击“添加键”并选择“创建新键”:

键有两种不同的格式,JSON 是首选格式。选择 JSON 并在底部点击“创建”将会下载 JSON 文件的键:

JSON 文件将如下所示:

{
  "type": "service_account",
  "project_id": "mlm-python",
  "private_key_id": "3863a6254774259a1249",
  "private_key": "-----BEGIN PRIVATE KEY-----\n
                  MIIEvgIBADANBgkqh...
                  -----END PRIVATE KEY-----\n",
  "client_email": "ml-access@mlm-python.iam.gserviceaccount.com",
  "client_id": "11542775381574",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/ml-access%40mlm-python.iam.gserviceaccount.com"
}

保存 JSON 文件后,我们可以返回到我们的 Google 表,并与我们的服务帐户共享该表。点击右上角的“共享”按钮,输入服务帐户的电子邮件地址。您可以跳过通知,直接点击“共享”。然后我们就准备好了!

此时,我们已准备好使用来自我们 Python 程序的服务帐户访问此特定 Google 表格。要向 Google 表格写入内容,我们可以使用 Google 的 API。我们首先依赖于刚刚下载的服务帐户的 JSON 文件(在本示例中为 mlm-python.json)来创建连接:

from oauth2client.service_account import ServiceAccountCredentials
from googleapiclient.discovery import build
from httplib2 import Http

cred_file = "mlm-python.json"
scopes = ['https://www.googleapis.com/auth/spreadsheets']
cred = ServiceAccountCredentials.from_json_keyfile_name(cred_file, scopes)
service = build("sheets", "v4", http=cred.authorize(Http()))
sheet = service.spreadsheets()

如果我们刚刚创建它,文件中应该只有一个工作表,并且其 ID 为 0。使用 Google 的 API 的所有操作都是以 JSON 格式进行的。例如,以下是我们如何使用刚刚创建的连接删除整个工作表上的所有内容:

...

sheet_id = '12Pc2_pX3HOSltcRLHtqiq3RSOL9RcG72CZxRqsMeRul'
body = {
    "requests": [{
        "deleteRange": {
            "range": {
                "sheetId": 0
            },
            "shiftDimension": "ROWS"
        }
    }]
}
action = sheet.batchUpdate(spreadsheetId=sheet_id, body=body)
action.execute()

假设我们像上面的第一个示例那样将糖尿病数据集读入一个 DataFrame 中。然后,我们可以一次性将整个数据集写入 Google 表格中。为此,我们需要创建一个列表的列表来反映表格中单元格的二维数组结构,然后将数据放入 API 查询中:

...
rows = [list(dataset.columns)]
rows += dataset.to_numpy().tolist()
maxcol = max(len(row) for row in rows)
maxcol = chr(ord("A") - 1 + maxcol)
action = sheet.values().append(
    spreadsheetId = sheet_id,
    body = {"values": rows},
    valueInputOption = "RAW",
    range = "Sheet1!A1:%s" % maxcol
)
action.execute()

在上述内容中,我们假设表格的名称为“Sheet1”(默认名称,您可以在屏幕底部看到)。我们将数据写入到左上角对齐,从单元格 A1(左上角)开始填充。我们使用 dataset.to_numpy().tolist() 将所有数据收集到一个列表的列表中,但我们还将列标题作为额外的行添加在开头。

从 Google 表格中读取数据的方式类似。以下是我们如何读取随机一行数据:

...
# Check the sheets
sheet_properties = sheet.get(spreadsheetId=sheet_id).execute()["sheets"]
print(sheet_properties)
# Read it back
maxrow = sheet_properties[0]["properties"]["gridProperties"]["rowCount"]
maxcol = sheet_properties[0]["properties"]["gridProperties"]["columnCount"]
maxcol = chr(ord("A") - 1 + maxcol)
row = random.randint(1, maxrow)
readrange = f"A{row}:{maxcol}{row}"
data = sheet.values().get(spreadsheetId=sheet_id, range=readrange).execute()

首先,我们可以通过检查其属性来了解表中的行数。上面的print()语句将产生如下结果:

[{'properties': {'sheetId': 0, 'title': 'Sheet1', 'index': 0,
'sheetType': 'GRID', 'gridProperties': {'rowCount': 769, 'columnCount': 9}}}]

由于我们只有一个表单,列表只包含一个属性字典。使用这些信息,我们可以选择一行随机行并指定读取范围。上面的变量data将是如下所示的字典,数据将以列表的形式存在,可以使用data["values"]访问:

{'range': 'Sheet1!A536:I536',
 'majorDimension': 'ROWS',
 'values': [['1',
   '77',
   '56',
   '30',
   '56',
   '33.3',
   '1.251',
   '24',
   'tested_negative']]}

将这些内容结合起来,以下是将数据加载到 Google Sheet 并从中读取随机行的完整代码:(运行时请确保更改sheet_id

import random

from googleapiclient.discovery import build
from httplib2 import Http
from oauth2client.service_account import ServiceAccountCredentials
from sklearn.datasets import fetch_openml

# Connect to Google Sheet
cred_file = "mlm-python.json"
scopes = ['https://www.googleapis.com/auth/spreadsheets']
cred = ServiceAccountCredentials.from_json_keyfile_name(cred_file, scopes)
service = build("sheets", "v4", http=cred.authorize(Http()))
sheet = service.spreadsheets()

# Google Sheet ID, as granted access to the service account
sheet_id = '12Pc2_pX3HOSltcRLHtqiq3RSOL9RcG72CZxRqsMeRul'

# Delete everything on spreadsheet 0
body = {
    "requests": [{
        "deleteRange": {
            "range": {
                "sheetId": 0
            },
            "shiftDimension": "ROWS"
        }
    }]
}
action = sheet.batchUpdate(spreadsheetId=sheet_id, body=body)
action.execute()

# Read dataset from OpenML
dataset = fetch_openml("diabetes", version=1, as_frame=True, return_X_y=False)["frame"]
rows = [list(dataset.columns)]       # column headers
rows += dataset.to_numpy().tolist()  # rows of data

# Write to spreadsheet 0
maxcol = max(len(row) for row in rows)
maxcol = chr(ord("A") - 1 + maxcol)
action = sheet.values().append(
    spreadsheetId = sheet_id,
    body = {"values": rows},
    valueInputOption = "RAW",
    range = "Sheet1!A1:%s" % maxcol
)
action.execute()

# Check the sheets
sheet_properties = sheet.get(spreadsheetId=sheet_id).execute()["sheets"]
print(sheet_properties)

# Read a random row of data
maxrow = sheet_properties[0]["properties"]["gridProperties"]["rowCount"]
maxcol = sheet_properties[0]["properties"]["gridProperties"]["columnCount"]
maxcol = chr(ord("A") - 1 + maxcol)
row = random.randint(1, maxrow)
readrange = f"A{row}:{maxcol}{row}"
data = sheet.values().get(spreadsheetId=sheet_id, range=readrange).execute()
print(data)

不可否认,以这种方式访问 Google Sheets 过于冗长。因此,我们有一个第三方模块gspread可用于简化操作。安装模块后,我们可以像下面这样简单地检查电子表格的大小:

import gspread

cred_file = "mlm-python.json"
gc = gspread.service_account(filename=cred_file)
sheet = gc.open_by_key(sheet_id)
spreadsheet = sheet.get_worksheet(0)
print(spreadsheet.row_count, spreadsheet.col_count)

清除电子表格、写入行以及读取随机行可以如下进行:

...
# Clear all data
spreadsheet.clear()
# Write to spreadsheet
spreadsheet.append_rows(rows)
# Read a random row of data
maxcol = chr(ord("A") - 1 + spreadsheet.col_count)
row = random.randint(2, spreadsheet.row_count)
readrange = f"A{row}:{maxcol}{row}"
data = spreadsheet.get(readrange)
print(data)

因此,之前的示例可以简化为以下内容,更短:

import random

import gspread
from sklearn.datasets import fetch_openml

# Google Sheet ID, as granted access to the service account
sheet_id = '12Pc2_pX3HOSltcRLHtqiq3RSOL9RcG72CZxRqsMeRul'

# Connect to Google Sheet
cred_file = "mlm-python.json"
gc = gspread.service_account(filename=cred_file)
sheet = gc.open_by_key(sheet_id)
spreadsheet = sheet.get_worksheet(0)

# Clear all data
spreadsheet.clear()

# Read dataset from OpenML
dataset = fetch_openml("diabetes", version=1, as_frame=True, return_X_y=False)["frame"]
rows = [list(dataset.columns)]       # column headers
rows += dataset.to_numpy().tolist()  # rows of data

# Write to spreadsheet
spreadsheet.append_rows(rows)

# Check the number of rows and columns in the spreadsheet
print(spreadsheet.row_count, spreadsheet.col_count)

# Read a random row of data
maxcol = chr(ord("A") - 1 + spreadsheet.col_count)
row = random.randint(2, spreadsheet.row_count)
readrange = f"A{row}:{maxcol}{row}"
data = spreadsheet.get(readrange)
print(data)

与读取 Excel 类似,使用存储在 Google Sheet 中的数据集,最好一次性读取,而不是在训练循环中逐行读取。这是因为每次读取时,都会发送网络请求并等待 Google 服务器的回复。这不可能很快,因此最好避免。以下是我们如何将 Google Sheet 中的数据与 Keras 代码结合起来进行训练的示例:

import random

import numpy as np
import gspread
from sklearn.datasets import fetch_openml
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense

# Google Sheet ID, as granted access to the service account
sheet_id = '12Pc2_pX3HOSltcRLHtqiq3RSOL9RcG72CZxRqsMeRul'

# Connect to Google Sheet
cred_file = "mlm-python.json"
gc = gspread.service_account(filename=cred_file)
sheet = gc.open_by_key(sheet_id)
spreadsheet = sheet.get_worksheet(0)

# Clear all data
spreadsheet.clear()

# Read dataset from OpenML
dataset = fetch_openml("diabetes", version=1, as_frame=True, return_X_y=False)["frame"]
rows = [list(dataset.columns)]       # column headers
rows += dataset.to_numpy().tolist()  # rows of data

# Write to spreadsheet
spreadsheet.append_rows(rows)

# Read the entire spreadsheet, except header
maxrow = spreadsheet.row_count
maxcol = chr(ord("A") - 1 + spreadsheet.col_count)
data = spreadsheet.get(f"A2:{maxcol}{maxrow}")
X = [row[:-1] for row in data]
y = [1 if row[-1]=="tested_positive" else 0 for row in data]
X, y = np.asarray(X).astype(float), np.asarray(y)

# create binary classification model
model = Sequential()
model.add(Dense(16, input_dim=8, activation='relu'))
model.add(Dense(8, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])

# train model
history = model.fit(X, y, epochs=5)

数据库的其他用途

上面的示例向您展示了如何从电子表格访问数据库。我们假设数据集在训练循环中由机器学习模型存储和使用。这是一种使用外部数据存储的方法,但不是唯一的方法。数据库的其他一些使用案例包括:

  • 作为存储日志的工具,以记录程序的详细信息,例如某些脚本何时被执行。这对于跟踪更改特别有用,如果脚本将要更改某些内容,例如,下载某些文件并覆盖旧版本。

  • 作为收集数据的工具。就像我们可能使用来自 scikit-learn 的GridSearchCV一样,我们经常会用不同的超参数组合来评估模型性能。如果模型很大且复杂,我们可能希望将评估分布到不同的机器上,并收集结果。在程序末尾添加几行代码,将交叉验证结果写入数据库或电子表格,以便我们可以将结果与选择的超参数制成表格是很方便的。将这些数据以结构化格式存储允许我们稍后报告结论。

  • 作为配置模型的工具。我们可以将其用作提供超参数选择的工具,以运行程序,而不是编写超参数组合和验证得分。如果我们决定更改参数,可以简单地打开一个 Google Sheet,例如,进行更改,而不是修改代码。

进一步阅读

以下是一些让你深入了解的资源:

书籍

API 和库

文章
软件

总结

在本教程中,你学会了如何使用外部数据存储,包括数据库或电子表格。

具体来说,你学到了:

  • 如何使你的 Python 程序通过 SQL 语句访问像 SQLite 这样的关系数据库

  • 如何将 dbm 用作键值存储,并像使用 Python 字典一样使用它

  • 如何从 Excel 文件中读取数据并写入数据

  • 如何通过互联网访问 Google Sheet

  • 我们如何利用这些来托管数据集并在机器学习项目中使用它们

使用 Pandas 处理数据

原文:machinelearningmastery.com/massaging-data-using-pandas/

当我们谈论数据管理时,看到以表格形式呈现的数据是不可避免的。带有列头,并且有时还会有行名,使得理解数据更容易。事实上,我们经常会看到不同类型的数据混在一起。例如,我们在一个食谱的配料表中,数量是数字,名称是字符串。在 Python 中,我们可以使用 pandas 库来处理表格数据。

完成本教程后,你将学习到:

  • pandas 库提供了什么

  • 在 pandas 中什么是 DataFrame 和 Series

  • 如何超越平凡的数组操作来操纵 DataFrame 和 Series

启动你的项目,请参考我新书《Python 机器学习》,包括逐步教程所有示例的 Python 源代码文件。

让我们开始吧!!

使用 Pandas 处理数据

照片来源:Mark de Jong。保留所有权利。

概述

本教程分为五部分:

  • DataFrame 和 Series

  • DataFrame 中的基本函数

  • 操作 DataFrames 和 Series

  • DataFrames 中的聚合

  • 处理 pandas 中的时间序列数据

DataFrame 和 Series

首先,让我们从一个示例数据集开始。我们将导入 pandas 并读取美国空气污染物排放数据到一个 DataFrame 中:

import pandas as pd

URL = "https://www.epa.gov/sites/default/files/2021-03/state_tier1_caps.xlsx"

df = pd.read_excel(URL, sheet_name="State_Trends", header=1)
print(df)
      State FIPS State  Tier 1 Code      Tier 1 Description  ... emissions18  emissions19  emissions20  emissions21
0              1    AL            1  FUEL COMB. ELEC. UTIL.  ...   10.050146     8.243679     8.243679     8.243679
1              1    AL            1  FUEL COMB. ELEC. UTIL.  ...    0.455760     0.417551     0.417551     0.417551
2              1    AL            1  FUEL COMB. ELEC. UTIL.  ...   26.233104    19.592480    13.752790    11.162100
3              1    AL            1  FUEL COMB. ELEC. UTIL.  ...    2.601011     2.868642     2.868642     2.868642
4              1    AL            1  FUEL COMB. ELEC. UTIL.  ...    1.941267     2.659792     2.659792     2.659792
...          ...   ...          ...                     ...  ...         ...          ...          ...          ...
5314          56    WY           16        PRESCRIBED FIRES  ...    0.893848     0.374873     0.374873     0.374873
5315          56    WY           16        PRESCRIBED FIRES  ...    7.118097     2.857886     2.857886     2.857886
5316          56    WY           16        PRESCRIBED FIRES  ...    6.032286     2.421937     2.421937     2.421937
5317          56    WY           16        PRESCRIBED FIRES  ...    0.509242     0.208817     0.208817     0.208817
5318          56    WY           16        PRESCRIBED FIRES  ...   16.632343     6.645249     6.645249     6.645249

[5319 rows x 32 columns]

这是一张每年的污染物排放表,包含了污染物种类和每年的排放量信息。

这里我们演示了 pandas 的一个有用功能:你可以使用 read_csv() 读取 CSV 文件,或使用 read_excel() 读取 Excel 文件。文件名可以是你机器上的本地文件或可以从中下载文件的 URL。我们从美国环境保护局的网站了解了这个 URL。我们知道哪个工作表包含数据以及数据从哪个行开始,因此在 read_excel() 函数中添加了额外的参数。

上面创建的 pandas 对象是一个 DataFrame,以表格形式呈现。类似于 NumPy,Pandas 中的数据组织在数组中。但 Pandas 将数据类型分配给列,而不是整个数组。这允许不同类型的数据包含在同一个数据结构中。我们可以通过调用 DataFrame 的 info() 函数来检查数据类型:

...
df.info() # print info to screen
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 5319 entries, 0 to 5318
Data columns (total 32 columns):
 #   Column              Non-Null Count  Dtype  
---  ------              --------------  -----  
 0   State FIPS          5319 non-null   int64  
 1   State               5319 non-null   object 
 2   Tier 1 Code         5319 non-null   int64  
 3   Tier 1 Description  5319 non-null   object 
 4   Pollutant           5319 non-null   object 
 5   emissions90         3926 non-null   float64
 6   emissions96         4163 non-null   float64
 7   emissions97         4163 non-null   float64
...
 29  emissions19         5052 non-null   float64
 30  emissions20         5052 non-null   float64
 31  emissions21         5052 non-null   float64
dtypes: float64(27), int64(2), object(3)
memory usage: 1.3+ MB

或者我们也可以获取 pandas Series 类型:

...
coltypes = df.dtypes
print(coltypes)
State FIPS              int64
State                  object
Tier 1 Code             int64
Tier 1 Description     object
Pollutant              object
emissions90           float64
emissions96           float64
emissions97           float64
...
emissions19           float64
emissions20           float64
emissions21           float64
dtype: object

在 pandas 中,DataFrame 是一个表格,而 Series 是表格中的一列。这一区分很重要,因为 DataFrame 背后的数据是一个二维数组,而 Series 是一个一维数组。

类似于 NumPy 中的 fancy indexing,我们可以从一个 DataFrame 中提取列以创建另一个 DataFrame:

...
cols = ["State", "Pollutant", "emissions19", "emissions20", "emissions21"]
last3years = df[cols]
print(last3years)
     State Pollutant  emissions19  emissions20  emissions21
0       AL        CO     8.243679     8.243679     8.243679
1       AL       NH3     0.417551     0.417551     0.417551
2       AL       NOX    19.592480    13.752790    11.162100
3       AL  PM10-PRI     2.868642     2.868642     2.868642
4       AL  PM25-PRI     2.659792     2.659792     2.659792
...    ...       ...          ...          ...          ...
5314    WY       NOX     0.374873     0.374873     0.374873
5315    WY  PM10-PRI     2.857886     2.857886     2.857886
5316    WY  PM25-PRI     2.421937     2.421937     2.421937
5317    WY       SO2     0.208817     0.208817     0.208817
5318    WY       VOC     6.645249     6.645249     6.645249

[5319 rows x 5 columns]

或者,如果我们传递的是列名字符串而不是列名列表,我们可以从 DataFrame 中提取一个列作为 Series:

...
data2021 = df["emissions21"]
print(data2021)
0        8.243679
1        0.417551
2       11.162100
3        2.868642
4        2.659792
          ...    
5314     0.374873
5315     2.857886
5316     2.421937
5317     0.208817
5318     6.645249
Name: emissions21, Length: 5319, dtype: float64

DataFrame 中的基本函数

Pandas 功能丰富。对表格或列的许多基本操作都作为 DataFrame 或 Series 上的函数提供。例如,我们可以通过以下方式查看上表中覆盖的污染物列表:

...
print(df["Pollutant"].unique())
['CO' 'NH3' 'NOX' 'PM10-PRI' 'PM25-PRI' 'SO2' 'VOC']

我们还可以找到系列的均值(mean())、标准偏差(std())、最小值(min())和最大值(max()):

...
print(df["emissions21"].mean())

但事实上,我们更可能使用describe()函数来探索新的 DataFrame。由于本示例中的 DataFrame 列太多,最好是将describe()的结果转置为新的 DataFrame:

...
print(df.describe().T)
              count       mean         std      min        25%        50%        75%           max
State FIPS   5319.0  29.039481   15.667352  1.00000  16.000000  29.000000  42.000000     56.000000
Tier 1 Code  5319.0   8.213198    4.610970  1.00000   4.000000   8.000000  12.000000     16.000000
emissions90  3926.0  67.885173  373.308888  0.00000   0.474330   4.042665  20.610050  11893.764890
emissions96  4163.0  54.576353  264.951584  0.00001   0.338420   3.351860  16.804540   6890.969060
emissions97  4163.0  51.635867  249.057529  0.00001   0.335830   3.339820  16.679675   6547.791030
...
emissions19  5052.0  19.846244   98.392126  0.00000   0.125881   1.180123   7.906181   4562.151689
emissions20  5052.0  19.507828   97.515187  0.00000   0.125066   1.165284   7.737705   4562.151689
emissions21  5052.0  19.264532   96.702411  0.00000   0.125066   1.151917   7.754584   4562.151689

的确,describe()生成的 DataFrame 可以帮助我们了解数据。从那里,我们可以了解到有多少缺失数据(通过查看计数),数据如何分布,是否存在异常值等等。

想要开始学习 Python 进行机器学习吗?

现在就免费获取我的 7 天电子邮件速成课程(包含示例代码)。

点击注册并获得课程的免费 PDF 电子书版本。

操纵 DataFrame 和 Series

类似于 NumPy 中的布尔索引,我们可以从 DataFrame 中提取一部分。例如,以下是如何仅选择一氧化碳排放数据的方法:

...
df_CO = df[df["Pollutant"] == "CO"]
print(df_CO)
      State FIPS State  Tier 1 Code             Tier 1 Description  ... emissions18  emissions19  emissions20  emissions21
0              1    AL            1         FUEL COMB. ELEC. UTIL.  ...   10.050146     8.243679     8.243679     8.243679
7              1    AL            2          FUEL COMB. INDUSTRIAL  ...   19.148024    17.291741    17.291741    17.291741
14             1    AL            3               FUEL COMB. OTHER  ...   29.207209    29.201838    29.201838    29.201838
21             1    AL            4  CHEMICAL & ALLIED PRODUCT MFG  ...    2.774257     2.626484     2.626484     2.626484
28             1    AL            5              METALS PROCESSING  ...   12.534726    12.167189    12.167189    12.167189
...          ...   ...          ...                            ...  ...         ...          ...          ...          ...
5284          56    WY           11               HIGHWAY VEHICLES  ...   70.776546    69.268149    64.493724    59.719298
5291          56    WY           12                    OFF-HIGHWAY  ...   31.092228    30.594383    30.603392    30.612400
5298          56    WY           14                  MISCELLANEOUS  ...    3.269705     3.828401     3.828401     3.828401
5305          56    WY           15                      WILDFIRES  ...  302.235376    89.399972    89.399972    89.399972
5312          56    WY           16               PRESCRIBED FIRES  ...   70.578540    28.177445    28.177445    28.177445

[760 rows x 32 columns]

正如您可能期望的那样,==运算符将从系列df["Pollutant"]中比较每个元素,结果是一个布尔值系列。如果长度匹配,DataFrame 会根据布尔值选择行。实际上,我们可以使用位运算符组合布尔值。例如,这就是我们如何选择由高速公路车辆导致的一氧化碳排放行:

...
df_CO_HW = df[(df["Pollutant"] == "CO") & (df["Tier 1 Description"] == "HIGHWAY VEHICLES")]
print(df_CO_HW)
      State FIPS State  Tier 1 Code Tier 1 Description  ...  emissions18  emissions19  emissions20  emissions21
70             1    AL           11   HIGHWAY VEHICLES  ...   532.140445   518.259811   492.182583   466.105354
171            2    AK           11   HIGHWAY VEHICLES  ...    70.674008    70.674008    63.883471    57.092934
276            4    AZ           11   HIGHWAY VEHICLES  ...   433.685363   413.347655   398.958109   384.568563
381            5    AR           11   HIGHWAY VEHICLES  ...   228.213685   227.902883   215.937225   203.971567
...
5074          54    WV           11   HIGHWAY VEHICLES  ...   133.628312   126.836047   118.621857   110.407667
5179          55    WI           11   HIGHWAY VEHICLES  ...   344.340392   374.804865   342.392977   309.981089
5284          56    WY           11   HIGHWAY VEHICLES  ...    70.776546    69.268149    64.493724    59.719298

[51 rows x 32 columns]

如果您喜欢像 Python 列表一样选择行,可以通过iloc接口进行。以下是如何选择行 5 到 10(从零开始索引)或列 1 到 6 以及行 5 到 10 的方法:

...
df_r5 = df.iloc[5:11]
df_c1_r5 = df.iloc[5:11, 1:7]

如果您熟悉 Excel,可能知道其令人兴奋的功能之一是“数据透视表”。Pandas 允许您做同样的事情。让我们考虑从数据集中获取 2021 年所有州的一氧化碳污染:

...
df_all_co = df[df["Pollutant"]=="CO"][["State", "Tier 1 Description", "emissions21"]]
print(df_all_co)
      State FIPS State  Tier 1 Code             Tier 1 Description  ... emissions18  emissions19  emissions20  emissions21
0              1    AL            1         FUEL COMB. ELEC. UTIL.  ...   10.050146     8.243679     8.243679     8.243679
7              1    AL            2          FUEL COMB. INDUSTRIAL  ...   19.148024    17.291741    17.291741    17.291741
14             1    AL            3               FUEL COMB. OTHER  ...   29.207209    29.201838    29.201838    29.201838
21             1    AL            4  CHEMICAL & ALLIED PRODUCT MFG  ...    2.774257     2.626484     2.626484     2.626484
28             1    AL            5              METALS PROCESSING  ...   12.534726    12.167189    12.167189    12.167189
...          ...   ...          ...                            ...  ...         ...          ...          ...          ...
5284          56    WY           11               HIGHWAY VEHICLES  ...   70.776546    69.268149    64.493724    59.719298
5291          56    WY           12                    OFF-HIGHWAY  ...   31.092228    30.594383    30.603392    30.612400
5298          56    WY           14                  MISCELLANEOUS  ...    3.269705     3.828401     3.828401     3.828401
5305          56    WY           15                      WILDFIRES  ...  302.235376    89.399972    89.399972    89.399972
5312          56    WY           16               PRESCRIBED FIRES  ...   70.578540    28.177445    28.177445    28.177445

[760 rows x 32 columns]

通过数据透视表,我们可以将不同的一氧化碳排放方式作为列,不同的州作为行:

...
df_pivot = df_all_co.pivot_table(index="State", columns="Tier 1 Description", values="emissions21")
print(df_pivot)
Tier 1 Description  CHEMICAL & ALLIED PRODUCT MFG  FUEL COMB. ELEC. UTIL.  ...  WASTE DISPOSAL & RECYCLING    WILDFIRES
State                                                                      ...
AK                                            NaN                4.679098  ...                    0.146018  4562.151689
AL                                       2.626484                8.243679  ...                   47.241253    38.780562
AR                                       0.307811                5.027354  ...                   26.234267     3.125529
AZ                                       0.000000                4.483514  ...                    6.438484   248.713896
...
WA                                       0.116416                4.831139  ...                    2.334996   160.284327
WI                                       0.023691                7.422521  ...                   35.670128     0.911783
WV                                       0.206324                7.836174  ...                   16.012414     5.086241
WY                                      14.296860               14.617882  ...                    1.952702    89.399972

[51 rows x 15 columns]

上述pivot_table()函数不要求值在索引和列中唯一。换句话说,如果原始 DataFrame 中某个州有两行“wildfire”,该函数将对这两行进行聚合(默认为取平均值)。要撤销数据透视操作,我们使用melt()函数:

...
df_melt = df_pivot.melt(value_name="emissions 2021", var_name="Tier 1 Description", ignore_index=False)
print(df_melt)
                  Tier 1 Description  emissions 2021
State
AK     CHEMICAL & ALLIED PRODUCT MFG             NaN
AL     CHEMICAL & ALLIED PRODUCT MFG        2.626484
AR     CHEMICAL & ALLIED PRODUCT MFG        0.307811
AZ     CHEMICAL & ALLIED PRODUCT MFG        0.000000
CA     CHEMICAL & ALLIED PRODUCT MFG        0.876666
...                              ...             ...
VT                         WILDFIRES        0.000000
WA                         WILDFIRES      160.284327
WI                         WILDFIRES        0.911783
WV                         WILDFIRES        5.086241
WY                         WILDFIRES       89.399972

[765 rows x 2 columns]

DataFrame 还有更多操作可以进行。例如,我们可以对行进行排序(使用sort_values()函数)、重命名列(使用rename()函数)、移除冗余行(使用drop_duplicates()函数)等等。

在机器学习项目中,我们经常需要在使用数据之前进行一些清理工作。使用 pandas 这一点非常方便。我们刚刚创建的 df_pivot DataFrame 中有一些标记为NaN的值表示没有可用数据。我们可以用以下任何一种方法将这些值替换为零:

df_pivot.fillna(0)
df_pivot.where(df_pivot.notna(), 0)
df_pivot.mask(df_pivot.isna(), 0)

DataFrame 中的聚合

实际上,pandas 可以提供表格操作,否则只能使用数据库 SQL 语句轻松完成。重用上述示例数据集,表中每种污染物都被分解为不同的来源。如果我们想知道污染物的总排放量,我们只需将所有来源的数据相加即可。类似于 SQL,这是一种“group by”操作。我们可以用以下方式做到:

...
df_sum = df[df["Pollutant"]=="CO"].groupby("State").sum()
print(df_sum)
       State FIPS  Tier 1 Code  emissions90  emissions96  ...  emissions18  emissions19  emissions20  emissions21
State                                                     ...
AK             28          115   4502.12238    883.50805  ...  5216.369575  5218.919502  5211.711803  5204.504105
AL             15          123   3404.01163   2440.95216  ...  1574.068371  1350.711872  1324.945132  1299.178392
AR             75          123   1706.69006   1356.08524  ...  1168.110471  1055.635824  1043.724418  1031.813011
AZ             60          123   2563.04249   1876.55422  ...  1000.976184   977.916197   964.504353   951.092509
...
WA            795          123   3604.39515   2852.52146  ...  1703.948955  1286.715920  1266.983767  1247.251614
WI            825          123   2849.49820   2679.75457  ...   922.375165   872.231181   838.232783   804.234385
WV            810          123   1270.81719    941.39753  ...   424.120829   395.720171   387.565561   379.410950
WY            840          123    467.80484   1598.56712  ...   549.270377   306.461296   301.695879   296.930461

[51 rows x 29 columns]

groupby() 函数的结果将使用分组列作为行索引。它通过将具有相同分组列值的行放入一个组中来运行。然后,作为一个组,应用某些聚合函数将多行减少为一行。在上述示例中,我们正在对每列进行求和。Pandas 提供了许多其他聚合函数,例如计算均值或仅计数行数。因为我们在做sum()操作,非数值列从输出中被删除,因为它们不适用于该操作。

这使我们能够执行一些有趣的任务。假设,使用上述 DataFrame 中的数据,我们创建了一个表,其中包含 2021 年每个州中一氧化碳(CO)和二氧化硫(SO2)的总排放量。如何实现这一点的推理如下:

  1. 按“State”和“Pollutant”分组,然后对每个组进行求和。这是我们获取每个州中每种污染物的总排放量的方法。

  2. 仅选择 2021 年的列

  3. 运行数据透视表,使州成为行,污染物成为列,总排放量作为值

  4. 仅选择 CO 和 SO2 的列

在代码中,可以这样写:

...
df_2021 = ( df.groupby(["State", "Pollutant"])
              .sum()              # get total emissions of each year
              [["emissions21"]]   # select only year 2021
              .reset_index()
              .pivot(index="State", columns="Pollutant", values="emissions21")
              .filter(["CO","SO2"])
          )
print(df_2021)
Pollutant           CO         SO2
State
AK         5204.504105   32.748621
AL         1299.178392   52.698696
AR         1031.813011   55.288823
AZ          951.092509   15.281760
...
WA         1247.251614   13.178053
WI          804.234385   21.141688
WV          379.410950   49.159621
WY          296.930461   37.056612

在上述代码中,groupby() 函数后的每一步都是创建一个新的 DataFrame。由于我们使用的函数在 DataFrame 下定义,因此我们有上述功能链式调用语法。

sum()函数将创建一个 DataFrame,其中StatePollutant作为索引的分组列。因此,当我们将 DataFrame 切片为只有一列时,我们使用了 reset_index() 将索引作为列(即会有三列,StatePollutantemissions21)。由于我们需要的污染物比我们需要的多,我们使用 filter() 从结果 DataFrame 中仅选择 CO 和 SO2 的列。这类似于使用花式索引来选择列。

实际上,我们可以以不同的方式完成同样的操作:

  1. 仅选择 CO 的行并计算总排放量;仅选择 2021 年的数据

  2. 对 SO2 也是一样

  3. 将前两步的结果 DataFrame 结合在一起

在 pandas 中,DataFrame 中有一个 join() 函数,可以帮助我们通过匹配索引将列与另一个 DataFrame 结合起来。在代码中,以上步骤如下:

...
df_co = df[df["Pollutant"]=="CO"].groupby("State").sum()[["emissions21"]].rename(columns={"emissions21":"CO"})
df_so2 = df[df["Pollutant"]=="SO2"].groupby("State").sum()[["emissions21"]].rename(columns={"emissions21":"SO2"})
df_joined = df_co.join(df_so2)

join()函数仅限于索引匹配。如果你熟悉 SQL,pandas 中JOIN子句的等效函数是merge()。如果我们为 CO 和 SO2 创建的两个 DataFrame 中有一个单独的州列,我们可以按照以下方式进行相同的操作:

df_co = df[df["Pollutant"]=="CO"].groupby("State").sum()[["emissions21"]].rename(columns={"emissions21":"CO"}).reset_index()
df_so2 = df[df["Pollutant"]=="SO2"].groupby("State").sum()[["emissions21"]].rename(columns={"emissions21":"SO2"}).reset_index()
df_merged = df_co.merge(df_so2, on="State", how="outer")

pandas 中的merge()函数可以执行所有类型的 SQL 连接。我们可以匹配来自不同 DataFrame 的不同列,并可以进行左连接、右连接、内连接和外连接。这在整理项目数据时非常有用。

DataFrame 中的groupby()函数非常强大,因为它允许我们灵活地操作 DataFrame,并开启了许多复杂转换的大门。可能在groupby()之后没有内置函数可以帮助,但我们总是可以提供自己的函数。例如,以下是如何创建一个函数来操作子 DataFrame(在所有列中,除了分组列)并应用它来找出最小和最大排放年份:

...
def minmaxyear(subdf):
    sum_series = subdf.sum()
    year_indices = [x for x in sum_series if x.startswith("emissions")]
    minyear = sum_series[year_indices].astype(float).idxmin()
    maxyear = sum_series[year_indices].astype(float).idxmax()
    return pd.Series({"min year": minyear[-2:], "max year": maxyear[-2:]})

df_years = df[df["Pollutant"]=="CO"].groupby("State").apply(minmaxyear)

apply()函数是最后的手段,它为我们提供了最大的灵活性。除了 GroupBy 对象,还有 DataFrames 和 Series 中的apply()接口。

以下是演示我们上面介绍的所有操作的完整代码:

import pandas as pd

# Pollutants data from Environmental Protection Agency
URL = "https://www.epa.gov/sites/default/files/2021-03/state_tier1_caps.xlsx"

# Read the Excel file and print
df = pd.read_excel(URL, sheet_name="State_Trends", header=1)
print("US air pollutant emission data:")
print(df)

# Show info
print("\nInformation about the DataFrame:")
df.info()

# print dtyes
coltypes = df.dtypes
print("\nColumn data types of the DataFrame:")
print(coltypes)

# Get last 3 columns
cols = ["State", "Pollutant", "emissions19", "emissions20", "emissions21"]
last3years = df[cols]
print("\nDataFrame of last 3 years data:")
print(last3years)

# Get a series
data2021 = df["emissions21"]
print("\nSeries of 2021 data:")
print(data2021)

# Print unique pollutants
print("\nUnique pollutants:")
print(df["Pollutant"].unique())

# print mean emission
print("\nMean on the 2021 series:")
print(df["emissions21"].mean())

# Describe
print("\nBasic statistics about each column in the DataFrame:")
print(df.describe().T)

# Get CO only
df_CO = df[df["Pollutant"] == "CO"]
print("\nDataFrame of only CO pollutant:")
print(df_CO)

# Get CO and Highway only
df_CO_HW = df[(df["Pollutant"] == "CO") & (df["Tier 1 Description"] == "HIGHWAY VEHICLES")]
print("\nDataFrame of only CO pollutant from Highway vehicles:")
print(df_CO_HW)

# Get DF of all CO
df_all_co = df[df["Pollutant"]=="CO"][["State", "Tier 1 Description", "emissions21"]]
print("\nDataFrame of only CO pollutant, keep only essential columns:")
print(df_all_co)

# Pivot
df_pivot = df_all_co.pivot_table(index="State", columns="Tier 1 Description", values="emissions21")
print("\nPivot table of state vs CO emission source:")
print(df_pivot)

# melt
df_melt = df_pivot.melt(value_name="emissions 2021", var_name="Tier 1 Description", ignore_index=False)
print("\nMelting the pivot table:")
print(df_melt)

# all three are the same
df_filled = df_pivot.fillna(0)
df_filled = df_pivot.where(df_pivot.notna(), 0)
df_filled = df_pivot.mask(df_pivot.isna(), 0)
print("\nFilled missing value as zero:")
print(df_filled)

# aggregation
df_sum = df[df["Pollutant"]=="CO"].groupby("State").sum()
print("\nTotal CO emission by state:")
print(df_sum)

# group by
df_2021 = ( df.groupby(["State", "Pollutant"])
              .sum()              # get total emissions of each year
              [["emissions21"]]   # select only year 2021
              .reset_index()
              .pivot(index="State", columns="Pollutant", values="emissions21")
              .filter(["CO","SO2"])
          )
print("\nComparing CO and SO2 emission:")
print(df_2021)

# join
df_co = df[df["Pollutant"]=="CO"].groupby("State").sum()[["emissions21"]].rename(columns={"emissions21":"CO"})
df_so2 = df[df["Pollutant"]=="SO2"].groupby("State").sum()[["emissions21"]].rename(columns={"emissions21":"SO2"})
df_joined = df_co.join(df_so2)
print("\nComparing CO and SO2 emission:")
print(df_joined)

# merge
df_co = df[df["Pollutant"]=="CO"].groupby("State").sum()[["emissions21"]].rename(columns={"emissions21":"CO"}).reset_index()
df_so2 = df[df["Pollutant"]=="SO2"].groupby("State").sum()[["emissions21"]].rename(columns={"emissions21":"SO2"}).reset_index()
df_merged = df_co.merge(df_so2, on="State", how="outer")
print("\nComparing CO and SO2 emission:")
print(df_merged)

def minmaxyear(subdf):
    sum_series = subdf.sum()
    year_indices = [x for x in sum_series if x.startswith("emissions")]
    minyear = sum_series[year_indices].astype(float).idxmin()
    maxyear = sum_series[year_indices].astype(float).idxmax()
    return pd.Series({"min year": minyear[-2:], "max year": maxyear[-2:]})

df_years = df[df["Pollutant"]=="CO"].groupby("State").apply(minmaxyear)
print("\nYears of minimum and maximum emissions:")
print(df_years)

处理 Pandas 中的时间序列数据

如果你处理时间序列数据,你会发现 pandas 还有另一个强大的功能。首先,让我们考虑一些每日的污染数据。我们可以从 EPA 网站选择并下载一些数据:

为了演示目的,我们下载了 2021 年德克萨斯州的 PM2.5 数据。我们可以如下导入下载的 CSV 文件ad_viz_plotval_data.csv

df = pd.read_csv("ad_viz_plotval_data.csv", parse_dates=[0])
print(df)
           Date Source    Site ID  POC  ...  COUNTY_CODE    COUNTY  SITE_LATITUDE SITE_LONGITUDE
0     2021-01-01    AQS  480131090    1  ...           13  Atascosa      29.162997     -98.589158
1     2021-01-02    AQS  480131090    1  ...           13  Atascosa      29.162997     -98.589158
2     2021-01-03    AQS  480131090    1  ...           13  Atascosa      29.162997     -98.589158
3     2021-01-04    AQS  480131090    1  ...           13  Atascosa      29.162997     -98.589158
4     2021-01-05    AQS  480131090    1  ...           13  Atascosa      29.162997     -98.589158
...          ...    ...        ...  ...  ...          ...       ...            ...            ...
19695 2021-12-27    AQS  484790313    1  ...          479      Webb      27.599444     -99.533333
19696 2021-12-28    AQS  484790313    1  ...          479      Webb      27.599444     -99.533333
19697 2021-12-29    AQS  484790313    1  ...          479      Webb      27.599444     -99.533333
19698 2021-12-30    AQS  484790313    1  ...          479      Webb      27.599444     -99.533333
19699 2021-12-31    AQS  484790313    1  ...          479      Webb      27.599444     -99.533333

[19700 rows x 20 columns]

pandas 的read_csv()函数允许我们指定某些列作为日期,并将其解析为datetime对象,而不是字符串。这对于进一步处理时间序列数据至关重要。如我们所知,第一列(从零开始计数)是日期列;我们在上面提供了参数parse_dates=[0]

对于处理时间序列数据,重要的是在 DataFrame 中使用时间作为索引。我们可以通过set_index()函数将其中一列设置为索引:

...
df_pm25 = df.set_index("Date")
print(df_pm25)
           Source    Site ID  POC  Daily Mean PM2.5 Concentration  ... COUNTY_CODE    COUNTY SITE_LATITUDE  SITE_LONGITUDE
Date                                                               ...
2021-01-01    AQS  480131090    1                             4.6  ...          13  Atascosa     29.162997      -98.589158
2021-01-02    AQS  480131090    1                             3.7  ...          13  Atascosa     29.162997      -98.589158
2021-01-03    AQS  480131090    1                             6.3  ...          13  Atascosa     29.162997      -98.589158
2021-01-04    AQS  480131090    1                             6.4  ...          13  Atascosa     29.162997      -98.589158
2021-01-05    AQS  480131090    1                             7.7  ...          13  Atascosa     29.162997      -98.589158
...           ...        ...  ...                             ...  ...         ...       ...           ...             ...
2021-12-27    AQS  484790313    1                            15.7  ...         479      Webb     27.599444      -99.533333
2021-12-28    AQS  484790313    1                            17.6  ...         479      Webb     27.599444      -99.533333
2021-12-29    AQS  484790313    1                            14.1  ...         479      Webb     27.599444      -99.533333
2021-12-30    AQS  484790313    1                            18.5  ...         479      Webb     27.599444      -99.533333
2021-12-31    AQS  484790313    1                            21.5  ...         479      Webb     27.599444      -99.533333

[19700 rows x 19 columns]

如果我们检查这个 DataFrame 的索引,我们会看到以下内容:

...
print(df_pm25.index)
DatetimeIndex(['2021-01-01', '2021-01-02', '2021-01-03', '2021-01-04',
               '2021-01-05', '2021-01-06', '2021-01-07', '2021-01-08',
               '2021-01-09', '2021-01-10',
               ...
               '2021-12-22', '2021-12-23', '2021-12-24', '2021-12-25',
               '2021-12-26', '2021-12-27', '2021-12-28', '2021-12-29',
               '2021-12-30', '2021-12-31'],
              dtype='datetime64[ns]', name='Date', length=19700, freq=None)

我们知道它的类型是datetime64,这是 pandas 中的时间戳对象。

从上面的索引中,我们可以看到每个日期并非唯一。这是因为 PM2.5 浓度是在不同的地点观测到的,每个地点都会向 DataFrame 中添加一行。我们可以筛选 DataFrame,只保留一个地点,以使索引唯一。或者,我们可以使用pivot_table()来转换 DataFrame,pivot 操作可以保证结果 DataFrame 的索引是唯一的:

df_2021 = ( df[["Date", "Daily Mean PM2.5 Concentration", "Site Name"]]
            .pivot_table(index="Date",
                         columns="Site Name",
                         values="Daily Mean PM2.5 Concentration")
          )
print(df_2021)
Site Name   Amarillo A&M  Ascarate Park SE  Austin North Hills Drive  ...  Von Ormy Highway 16  Waco Mazanec  World Trade Bridge
Date                                                                  ...
2021-01-01           1.7              11.9                       3.0  ...                  4.6           2.7                 4.4
2021-01-02           2.2               7.8                       6.1  ...                  3.7           2.2                 6.1
2021-01-03           2.5               4.2                       4.3  ...                  6.3           4.2                 8.5
2021-01-04           3.7               8.1                       3.7  ...                  6.4           4.2                 5.7
2021-01-05           4.5              10.0                       5.2  ...                  7.7           6.1                 7.1
...                  ...               ...                       ...  ...                  ...           ...                 ...
2021-12-27           1.9               5.8                      11.0  ...                 13.8          10.5                15.7
2021-12-28           1.8               6.6                      14.1  ...                 17.7           9.7                17.6
2021-12-29           NaN               8.1                      21.8  ...                 28.6          12.5                14.1
2021-12-30           4.0               9.5                      13.1  ...                 20.4          13.4                18.5
2021-12-31           3.6               3.7                      16.3  ...                 18.3          11.8                21.5

[365 rows x 53 columns]

我们可以通过以下方式检查唯一性:

df_2021.index.is_unique

现在,这个数据框中的每一列都是一个时间序列。虽然 pandas 没有在时间序列上提供任何预测功能,但它提供了帮助您清洁和转换数据的工具。将 DateTimeIndex 设置为 DataFrame 将对时间序列分析项目非常有用,因为我们可以轻松提取时间间隔的数据,例如上述 DataFrame 中的 3 个月子集提取如下:

df_3month = df_2021["2021-04-01":"2021-07-01"]

在时间序列中,常用的一个功能是重新取样数据。考虑到这个数据框中的日常数据,我们可以将其转换为每周观察。我们可以指定结果数据以每周星期日为索引。但我们仍然需要告诉它我们希望重新取样后的数据是什么样子的。如果是销售数据,我们可能希望在整个周内求和以得到周收入。在这种情况下,我们可以每周取平均以平滑波动。另一种选择是每个周期取第一个观察值,如下所示:

...
df_resample = df_2021.resample("W-SUN").first()
print(df_resample)
Site Name   Amarillo A&M  Ascarate Park SE  Austin North Hills Drive  ...  Von Ormy Highway 16  Waco Mazanec  World Trade Bridge
Date                                                                  ...
2021-01-03           1.7              11.9                       3.0  ...                  4.6           2.7                 4.4
2021-01-10           3.7               8.1                       3.7  ...                  6.4           4.2                 5.7
2021-01-17           5.8               5.3                       7.0  ...                  5.4           6.9                 4.8
...
2021-12-19           3.6              13.0                       6.3  ...                  6.9           5.9                 5.5
2021-12-26           5.3              10.4                       5.7  ...                  5.5           5.4                 3.9
2022-01-02           1.9               5.8                      11.0  ...                 13.8          10.5                15.7

[53 rows x 53 columns]

字符串“W-SUN”表示每周日进行平均。这被称为“偏移别名”。您可以在以下找到所有偏移别名的列表:

在金融市场数据中,重新取样尤其有用。想象一下,如果我们有市场价格数据,其中原始数据不以常规间隔出现。我们仍然可以使用重新取样将数据转换为常规间隔。由于它非常常用,pandas 甚至为您提供了开-高-低-收盘(称为 OHLC,即周期内的第一个、最大、最小和最后观测值)的重新取样。我们以下演示如何在一个观察站上获取一周的 OHLC:

df_ohlc = df_2021["San Antonio Interstate 35"].resample("W-SUN").ohlc()
print(df_ohlc)
            open  high   low  close
Date
2021-01-03   4.2  12.6   4.2   12.6
2021-01-10   9.7   9.7   3.0    5.7
2021-01-17   5.4  13.8   3.0   13.8
2021-01-24   9.5  11.5   5.7    9.0
...
2021-12-12   5.7  20.0   5.7   20.0
2021-12-19   9.7   9.7   3.9    3.9
2021-12-26   6.1  14.7   6.0   14.7
2022-01-02  10.9  23.7  10.9   16.3

特别是,如果我们将时间序列从粗粒度频率重新取样为细粒度频率,则称为上采样。Pandas 通常在上采样时插入NaN值,因为原始时间序列在中间时间实例时没有数据。避免在上采样时出现这些NaN值的一种方法是要求 pandas 前向填充(从较早的时间传递值)或后向填充(使用较晚时间的值)数据。例如,以下是将每日 PM2.5 观测值向前填充到每小时的操作:

...
series_ffill = df_2021["San Antonio Interstate 35"].resample("H").ffill()
print(series_ffill)
Date
2021-01-01 00:00:00     4.2
2021-01-01 01:00:00     4.2
2021-01-01 02:00:00     4.2
2021-01-01 03:00:00     4.2
2021-01-01 04:00:00     4.2
                       ...
2021-12-30 20:00:00    18.2
2021-12-30 21:00:00    18.2
2021-12-30 22:00:00    18.2
2021-12-30 23:00:00    18.2
2021-12-31 00:00:00    16.3
Freq: H, Name: San Antonio Interstate 35, Length: 8737, dtype: float64

除了重新取样,我们还可以使用滑动窗口来转换数据。例如,以下是如何从时间序列中制作一个 10 天的移动平均值。这不是重新取样,因为结果数据仍然是每日的。但对于每个数据点,它是过去 10 天的平均值。类似地,我们可以通过将不同函数应用于滚动对象来找到 10 天的标准偏差或 10 天的最大值。

...
df_mean = df_2021["San Antonio Interstate 35"].rolling(10).mean()
print(df_mean)
Date
2021-01-01      NaN
2021-01-02      NaN
2021-01-03      NaN
2021-01-04      NaN
2021-01-05      NaN
              ...
2021-12-27     8.30
2021-12-28     9.59
2021-12-29    11.57
2021-12-30    12.78
2021-12-31    13.24
Name: San Antonio Interstate 35, Length: 365, dtype: float64

为了展示原始数据和滚动平均时间序列的区别,下面展示了该图。我们向 rolling() 函数中添加了参数 min_periods=5,因为原始数据在某些天有缺失数据。这在每日数据中产生了间隙,但我们要求即使在过去 10 天的窗口中只有 5 个数据点,均值仍然被计算。

...
import matplotlib.pyplot as plt

fig = plt.figure(figsize=(12,6))
plt.plot(df_2021["San Antonio Interstate 35"], label="daily")
plt.plot(df_2021["San Antonio Interstate 35"].rolling(10, min_periods=5).mean(), label="10-day MA")
plt.legend()
plt.ylabel("PM 2.5")
plt.show()

以下是演示上述时间序列操作的完整代码:

import pandas as pd
import matplotlib.pyplot as plt

# Load time series
df = pd.read_csv("ad_viz_plotval_data.csv", parse_dates=[0])
print("Input data:")
print(df)

# Set date index
df_pm25 = df.set_index("Date")
print("\nUsing date index:")
print(df_pm25)
print(df_pm25.index)

# 2021 daily
df_2021 = ( df[["Date", "Daily Mean PM2.5 Concentration", "Site Name"]]
            .pivot_table(index="Date",
                         columns="Site Name",
                         values="Daily Mean PM2.5 Concentration")
          )
print("\nUsing date index:")
print(df_2021)
print(df_2021.index.is_unique)

# Time interval
df_3mon = df_2021["2021-04-01":"2021-07-01"]
print("\nInterval selection:")
print(df_3mon)

# Resample
print("\nResampling dataframe:")
df_resample = df_2021.resample("W-SUN").first()
print(df_resample)
print("\nResampling series for OHLC:")
df_ohlc = df_2021["San Antonio Interstate 35"].resample("W-SUN").ohlc()
print(df_ohlc)
print("\nResampling series with forward fill:")
series_ffill = df_2021["San Antonio Interstate 35"].resample("H").ffill()
print(series_ffill)

# rolling
print("\nRolling mean:")
df_mean = df_2021["San Antonio Interstate 35"].rolling(10).mean()
print(df_mean)

# Plot moving average
fig = plt.figure(figsize=(12,6))
plt.plot(df_2021["San Antonio Interstate 35"], label="daily")
plt.plot(df_2021["San Antonio Interstate 35"].rolling(10, min_periods=5).mean(), label="10-day MA")
plt.legend()
plt.ylabel("PM 2.5")
plt.show()

进一步阅读

Pandas 是一个功能丰富的库,包含的细节远超我们上面所涵盖的内容。以下是一些资源,帮助你更深入地学习:

API 文档
书籍

总结

在本教程中,你了解了 pandas 提供的函数的简要概述。

具体来说,你学到了:

  • 如何使用 pandas 的 DataFrames 和 Series

  • 如何以类似于关系数据库中表操作的方式操作 DataFrames

  • 如何利用 pandas 来帮助操作时间序列数据