命令行上的数据科学第二版-全-
命令行上的数据科学第二版(全)
一、简介
原文:https://datascienceatthecommandline.com/2e/chapter-1-introduction.html
贡献者:Ting-xin
这本书是关于如何利用命令行做数据科学的。我的目标是通过教你使用命令行的力量,让自己成为一名更高效和多产的数据科学家。
在标题中同时使用的术语数据科学和命令行需要解释一下,命令行作为一项超过 50 年历史的技术,怎么会对一个只有几年历史的领域有帮助呢?
今天,数据科学家有大量激动人心的技术和编程语言选择,比如 Python、R、Julia 和 Apache Spark 等等,你可能已经有使用过一个或多个这方面技术的经验了。如果是,那你为什么还要在意命令行能在数据科学领域做一些什么呢?命令行是不是有什么东西是其他技术和编程语言所没有的呢?
这些都是真实存在的问题。在第一章,我将如下回答这些问题。首先,我提出了数据科学的实用定义,它将成为本书的指南。其次,我将列出命令行的 5 个优点。在这一章结束时,我希望能够说服你:对于数据科学这个领域来说命令行是值得学习的。
1.1 数据科学是 OSEMN
数据科学领域仍处于起步阶段,因此,它包含这各种各样的定义。在本书中,我借用了 Hilary Mason 和 Chris H. Wiggins 提出的非常实用的定义,他们按照以下 5 个步骤定义数据科学:(1)获取数据,(2)清理数据,(3)探索数据,(4)数据建模,(5)解释数据,这些步骤共同构成了 OSEMN 模型(发音为awesome)。这个模型是本书的指南,因为每一步(除了第 5 步,解释数据,我将在下面解释)都有属于自己的章节。
虽然这五个步骤是以线性和递增的方式讨论的,但实际上这些步骤之间是来回重复执行或多个步骤同时进行的。图 1.1 说明了做数据科学是一个迭代的非线性过程。比如说,假如你对你的数据进行了建模,你看了看结果,你可能会决定回到清理数据的步骤,以调整数据集的特征。

图 1.1: 数据科学是一个迭代的非线性过程
接下来我将会解释每一步具体做什么。
1.1.1 获取数据
没有数据就没有数据科学。所以我们第一步都是获取数据,除非你足够幸运已经拥有了所有的数据,否则你可能需要执行以下一项或多项操作:
- 从其它地方(例如,网页或服务器)下载数据
- 从数据库或 API(如 MySQL 或 Twitter)中查询数据
- 从另一个文件(例如,HTML 文件或电子表格)中提取数据
- 自己生成的数据(例如,读取传感器或进行调查)
在第三章中,我讨论了几种使用命令行获取数据的方法。获得的数据很可能是纯文本、CSV、JSON、HTML 或 XML 等格式,所以下一步是清理这些数据。
1.1.2 清理数据
获得的数据可能会存在缺失值、数据不一致、错误和奇怪的字符或者有不感兴趣的列等情况。在这些情况下,你必须先清理数据,然后才能对它做任何有趣的事情。常见的清理操作包括:
- 过滤数据
- 提取某些列
- 替换值
- 提取值
- 处理缺失值和重复值
- 将数据从一种格式转换为另一种格式
虽然我们数据科学家都喜欢创建令人兴奋的数据可视化图表和有洞察力的模型(步骤 3 和 4),但我们通常需要先花费大量精力来获取和清理所需的数据(步骤 1 和 2)。在《Data Jujitsu》中, DJ Patil 指出“在任何数据项目中,80% 的工作都是在清理数据。”在第五章,我演示了命令行如何帮助清理数据的工作。
1.1.3 探索数据
清理完数据后,你就可以开始探索它了。这也是数据科学有趣的地方,因为当你在探索时,你将真正的了解你的数据。在第七章中,我们将使用命令行做一下工作:
- 查看你的数据
- 从数据中获取统计数据
- 创建有洞察力的可视化信息
第七章中介绍的命令行工具有:csvstat和rush。
1.1.4 建模数据
如果你想要解释数据或预测将会发生什么,你可能想为你的数据创建一个统计模型。创建模型的技术包括但不限于聚类、分类、回归和降维等。命令行不适合从头开始编程一个新类型的模型。然而,从命令行构建一个模型是非常有用的。在第九章中,我将介绍几个命令行工具,它们要么在本地构建模型,要么使用 API 在云中执行计算。
1.1.5 解释数据
OSEMN 模式中最后也可能是最重要的一步就是解释数据,这一步骤包括:
- 从你的数据中得出结论
- 评估你的结果意味着什么
- 沟通你的结果
说实话,电脑在这一步的用处不大,同样命令行在这个阶段也没有什么作用。一旦你走到这一步,一切就取决于你了。这也是是 OSEMN 模型中唯一没有自己章节的步骤。取而代之的是,我向你推荐 Max Shron 的《Thinking with Data》这本书。
1.2 其它章节
除了涵盖 OSEMN 步骤的章节外,还有四个其它的章节。它们每一篇都讨论了一个关于数据科学的常见的主题,以及如何使用命令行来实现这个目标。这些主题适用于数据科学过程中的任何步骤。
在第四章中,我们将讨论如何为命令行创建可重用的工具。这些工具既可以来自于你在命令行上输入的长命令,也可以来自于你用 Python 或 R 编写的现有代码。创建属于自己的工具会让你变得更加高效和多产。
由于命令行是一个进行数据科学的交互式环境,因此要跟踪你的工作流程可能会变得很有挑战性。在第六章中,我演示了一个叫做make的命令行工具,它允许你用任务和任务之间的依赖关系来定义你数据科学的工作流。这个工具可以提高你工作流程的可重复性,不仅对你,对你的同事和同行也是如此。
在第八章,我解释了如何通过并行运行来加快你的命令行和工具的速度。使用一个叫做 GNU Parallel 的命令行工具,你可以将命令行工具应用于非常大的数据集,并在多个核心甚至是远程机器上运行它们。
在第十章,我们将讨论如何在其他环境和编程语言中使用命令行的强大功能,比如 R、RStudio、Python、Jupyter Notebooks,甚至是 Apache Spark。
1.3 什么是命令行?
在讨论“为什么你应该将命令行用于数据科学”之前,让我们先来看看“命令行实际上是什么样子”(你可能已经很熟悉了)。图 1.2 和图 1.3 分别显示了 macOS 和 Ubuntu 上默认出现的命令行工具的屏幕截图。Ubuntu 是 GNU/Linux 的一个特殊发行版,也是我在本书中要使用的一个版本。

图 1.2:maxOS 上的命令行工具

图 1.3:Ubuntu 上的命令行工具
两张截图中显示的窗口称为终端。这是使你能够与 Shell 进行交互的程序。执行我输入的命令的就是 Shell。在第二章中,我会更详细地解释这两个术语。
我没有展示 MicrosoftWindows 的终端(它被叫做命令提示符或 PowerShell),因为它与本书介绍的命令有本质的不同,它们之间不兼容。好消息是,你可以在 Microsoft 上安装一个 Docker 镜像,这样你就能跟着做了。如何安装 Docker 镜像将在第二章讲解。
与通过图形用户界面(GUI)相比,输入命令是一种非常不同的与计算机交互的方式。如果你通常习惯于在 Microsoft Excel 中处理数据,那么这种方法一开始可能看起来有些吓人。不要害怕。相信我,你会很快习惯在命令行上工作。
在本书中,我所输入的命令和它们所产生的输出都以文本形式显示。例如,两张截图中终端的内容会是这样的。:
$ whoami
dst
$ date
Thu Mar 3 10:38:33 AM CET 2022
$ echo 'The command line is awesome!' | cowsay -f tux
______________________________
< The command line is awesome! >
------------------------------
\
\
.--.
|o_o |
|:_/ |
// \ \
(| | )
/'\_ _/`\
\___)=(___/
$
你还会注意到每个命令前面都有一个美元符号($),这个就是一个提示符。这两张截图中显示了很多信息:用户名、日期和一只企鹅。在示例中显示美元符号是一种惯例,提示符有以下特点:(1)在会话中会改变(当你进入不同的目录),(2)可以由用户定制(例如,它还可以显示时间或你正在处理的当前git分支),(3)与命令本身无关。
在下一章,我将更多地解释基本的命令行概念。现在是时候首先解释一下“为什么你应该学习使用命令行进行数据科学研究”了。
1.4 为什么在命令行中进行数据科学?
命令行有许多巨大的优势,它可以使你真正成为一个更有效率和生产力的数据科学家。粗略地对这些优势进行分组,命令行是:灵活的、不断增强的、可重复的、可扩展的和无处不在的。下面我将对每个优势进行阐述。
1.4.1 灵活的命令行
命令行的第一个优点是用起来灵活。数据科学具有很强的交互性和探索性,你的工作环境需要考虑到这一点,命令行通过两种方式实现这一点。
首先,命令行提供了一个所谓的读取-求值-打印循环(REPL,交互式解释器)。这意味着当你输入命令,按下Enter,命令就会立即被执行。与脚本、大型程序和 Hadoop 作业相关的编辑-编译-运行-调试周期相比,REPL 通常更便于进行数据科学研究。你的命令是立即执行的,可以随意停止,可以快速更改。这个短的迭代周期可以真正的让你玩转你的数据。
第二,命令行可以很方便的操作文件系统。因为数据是数据科学的主要组成部分,所以能够轻松地处理包含你的数据集的文件是很重要的。命令行为此提供了许多方便的工具。
1.4.2 不断增强的命令行
命令行与其他技术集成得很好。无论目前你现在的数据科学工作流包含什么技术(无论是 R、Python 还是 Excel),请知道我并不是建议你放弃该工作流。相反,把命令行看作是一种辅助的技术,它可以增强你目前正在使用的技术的功能,主要通过以下三种方式:
首先,命令行可以充当许多不同数据科学工具之间的粘合剂。粘合工具的一种方法是将第一个工具的输出连接到第二个工具的输入。在第二章中,我解释了这是如何工作的。
其次,你通常可以从自己的环境中将任务委派给命令行。例如,Python、R 和 Apache Spark 允许你运行命令行工具并捕获它们的输出。我在第十章中用例子证明了这一点。
第三,你可以将你的代码(例如,Python 或 R 脚本)转换成可重用的命令行工具。这样,用什么语言写就不再重要了。现在,可以从命令行直接使用它,或者从前面提到的与命令行集成的任何环境中使用它。我在第四章.中解释了如何做到这一点。
最后,每种技术都有其优势和劣势,多了解几种技术并使用最适合手头任务的技术才是正道。有时这应该使用 R,有时是命令行,有时甚至是笔和纸。到本书结束时,你将对何时应该使用命令行、何时继续使用你最喜欢的编程语言或统计计算的环境有一个基本的理解。
1.4.3 可重复的命令行
正如我以前说过的,在命令行上工作与使用 GUI 有很大的不同。在命令行上,你通过输入来做事情,而在 GUI 上,你通过点击鼠标来做事情。
你在命令行上手动输入的所有内容也可以通过脚本和工具实现自动化。很多时候我们需要重新运行自己的命令行,比如我犯了一个错误、输入数据变了或者我的同事想要进行同样的分析,通过自动化脚本或者工具我们可以很容易的重新运行我们曾经输入的内容。此外,你的命令可以以一个特定的时间间隔运行,在远程服务器上运行,并在许多数据块上并行运行(更多信息请参见第八章)。
因为命令行是自动化的,所以它变得可伸缩和可重复。但是自动化 GUI 中的指向和点击并不简单,这使得 GUI 成为一个不太适合做可扩展和可重复的数据科学的环境。
1.4.4 可扩展的命令行
命令行本身是 50 多年前发明的,但是它的核心功能基本保持不变,但是它的工具,命令行的主力,每天都在被开发。
命令行本身是语言无关的,这允许用许多不同的编程语言来编写命令行工具。开源社区正在开发许多免费的高质量命令行工具,我们可以将它们用于数据科学。
这些命令行工具可以一起工作,这使得命令行非常灵活。你还可以创建自己的工具,让你扩展命令行的有效功能。
1.4.5 无处不在的命令行
因为命令行是任何类 Unix 的操作系统自带的,包括 Ubuntu Linux 和 macOS,所以在很多地方都可以找到。另外,排名前 500 的超级计算机 100% 都运行 Linux。因此,如果你曾经经手过一台超级计算机(或者如果你曾经发现自己在侏罗纪公园里门锁坏了),你最好知道如何使用命令行!
但是 Linux 不仅仅运行在超级计算机上。它也可以在服务器、笔记本电脑和嵌入式系统上运行。如今,许多公司都提供云计算,你可以很容易地在网络上启动新机器。如果你曾经登录过这样的机器(或一般的服务器),几乎可以肯定的是你将碰到命令行。
值得注意的是,命令行并不只是一种炒作。这项技术已经存在了五十多年,而且我相信它还会继续存在五十年。因此,学习如何使用命令行(对于数据科学和平时使用而言)是一项值得的投资。
1.5 总结
在这一章中,我向你介绍了做数据科学的 OSEMN 模型,并且我把它作为本书的指南。同时我也提供了一些关于 Unix 命令行的背景知识,希望能让你相信这是一个适合从事数据科学的环境。在下一章,我将向你展示如何开始安装数据集和工具,并解释它们的基本概念。
1.6 探索参考
- Brian W. Kernighan 所著的《UNIX: A History and a Memoir》讲述了 UNIX 的故事,解释了它是什么,它是如何开发的,以及它为什么重要
- 2018 年,我在 Strata London 做了一个题为《50 个学习 Shell 做数据科学的理由》的演讲。如果你需要更多的说服力,你可以看看它的幻灯片。
- 由 Max Shron 撰写的《Thinking with Data》一书简短而温馨,它关注的是 "为什么 "而不是 "怎么做",并提供了一个定义数据科学项目的框架,这将帮助你提出正确的问题并正确的解决问题。
二、开始
原文:https://datascienceatthecommandline.com/2e/chapter-2-getting-started.html
贡献者:Ting-xin
在这一章中,我需要确定你能够利用命令行做数据科学,为此你需要能满足一些条件。条件主要分为三个部分:(1)拥有与我在本书中使用的相同的数据集,(2)拥有一个适当的环境,拥有我在本书中使用的所有命令行工具,(3)了解使用命令行时的基本概念。
首先,我描述了如何下载数据集。其次,我解释了如何安装 Docker 镜像,它是一个基于 Ubuntu Linux 的虚拟环境,包含所有必要的命令行工具。随后,我通过例子介绍了基本的 Unix 概念。
在本章结束时,你将掌握进行数据科学的第一步,也就是获取数据。
2.1 获取数据
数据集下载步骤:
- 数据集压缩包下载地址:https://www.datascienceatthecommandline.com/2e/data.zip
- 创建一个新目录:你可以给这个目录起任何你想要的名字,但是我建议你使用小写字母、数字,可能还有连字符或下划线,以便更容易在命令行中使用。比如:
dsatcl2e-data,然后记住这个目录在哪里 - 将 ZIP 文件移动到新的目录中,并将其解压
- 这个目录下每章都有一个对应的子目录
接下来我将介绍如何安装包含处理这些数据的环境,它包含的所有必要的命令行工具。
2.2 安装 Docker 镜像
在本书中,我们使用了许多不同的命令行工具。Unix 通常预装了许多命令行工具,并提供了许多包含相关工具的包。自己独立安装这些包通常不会太难。然而,我们也会使用那些不能以包的形式提供的工具,这也需要更多涉及安装的手动操作。为了获得必要的命令行工具而不必经历每个工具的安装过程,我建议安装专门为本书创建的 Docker 镜像,无论你的操作系统是 Windows、macOS 还是 Linux 。
Docker 镜像是一个或多个应用及其所有依赖项的包。Docker 容器是一个运行镜像的隔离环境,你可以使用docker命令行工具(这也是你下面要做的)或 Docker GUI 来管理 Docker 镜像和容器。在某种程度上,Docker 容器就像一个虚拟机,只是 Docker 容器使用的资源要少得多。在本章的最后,我会推荐了一些资源来学习更多关于 Docker 的知识。
如果你仍然喜欢在本地而不是在 Docker 容器中运行命令行工具,那么你当然可以自己单独安装这些命令行工具。请注意,这是一个非常耗时的过程。附录中列出了本书中使用的所有命令行工具。安装说明仅适用于 Ubuntu。本书中使用的脚本和数据集可以通过克隆本书的 GitHub 仓库得。
为了安装 Docker 镜像,首先需要从 Docker 网站中下载并安装 Docker 本身。安装 Docker 后,你就可以在终端或命令提示符下调用以下命令来下载 Docker 镜像(不要输入入美元符号):
$ docker pull datasciencetoolbox/dsatcl2e
然后你可以运行 Docker 镜像,如下所示:
$ docker run --rm -it datasciencetoolbox/dsatcl2e
现在你处于一个称为 Docker 容器的隔离环境中,它安装了所有必要的命令行工具。如果下面的命令绘制了一头热情的牛,那么这就表示一切工作正常:
$ cowsay "Let's moove\!"
______________
< Let's moove! >
--------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
如果你想在容器和机器中交换数据,你可以为容器可以添加一个卷,这意味着机器中的本地目录将被映射到容器内的一个目录。所以我建议你首先创建一个新目录,然后进入这个新目录,然后在 macOS 或 Linux 上运行以下命令:
$ docker run --rm -it -v "$(pwd)":/data datasciencetoolbox/dsatcl2e
或者在 Windows 上使用命令提示符(也称为cmd)上运行以下命令:
C:\> docker run --rm -it -v "%cd%":/data datasciencetoolbox/dsatcl2e
或者当你使用 Windows PowerShell 上运行以下命令:
PS C:\> docker run --rm -it -v ${PWD}:/data datasciencetoolbox/dsatcl2e
在上面的命令中,选项-v指示docker将当前目录映射到容器内的/data目录,因此这也是 Docker 容器和机器交换数据的地方。
如果你想要知道更多关于 Docker 镜像的知识,请访问该 网址
当这些都完成后,你可以通过输入exit命令来关闭 Docker 容器。
2.3 基本的 Unix 概念
在第一章中,我简单的给大家展示了命令行是什么。如果现在你正在运行 Docker 镜像,那么我们就可以真正开始了。在这一节中,我将讨论几个概念和工具,为了能在命令行中轻松地进行数据科学研究,你需要了解这些概念和工具。如果到目前为止,你主要用的都是图形用户界面,那么这次可能是一个相当大的改变。但是不要担心,我会从头开始,然后逐渐进入更高级的主题。
本节不是一个完整的 Unix 课程。我将只解释与做数据科学有关的概念和工具。Docker 镜像的优势之一是很多东西都已经设置好了。如果你想了解更多,请参考本章末尾的进一步阅读部分。
2.3.1 环境
现在我们刚刚进入了一个全新的环境,因此在做任何事情之前,我们都有必要对这个环境有一个大体的了解。该环境大致定义为四层,我将简单的的从上到下的介绍它们。
命令行工具
首先,也是最重要的是我们使用的命令行工具。我们通过输入相应的命令来使用它们。命令行工具有许多种类型(这个将在下一节讨论),常见例子有:ls,cat,jq。
终端
终端是第二个概念,它是我们输入命令的应用。如果你看到书中提到的以下文字:
$ seq 3
1
2
3
然后你也可以跟着在你的终端上输入seq 3,按下Enter,结果就会生成一个数字序列。不要输入美元符号$,它只是告诉你这是一个你可以在终端输入的命,这个美元符号被称为提示符。
Shell
第三层是 Shell。一旦我们输入命令并按下Enter,终端就将命令发送给 Shell, Shell 是一个解释命令的程序。我使用的是 ZShell,还有许多其他可用的 Shell,比如 Bash 和 Fish。
操作系统
第四层是操作系统,在我们的例子中是 GNU/Linux。Linux 是内核的名字,它是操作系统的核心。内核直接与 CPU、磁盘和其他硬件接触,内核还执行我们的命令行工具。GNU,代表 GNU's Not UNIX,指的是一套基本工具。Docker 镜像是基于一个特定的 GNU/Linux 发行版,该发行版称为 Ubuntu。
2.3.2 执行命令行工具
现在你已经对环境有了基本的了解,是时候尝试一些命令了。在你的终端中键入以下内容(不带美元符号),然后按Enter:
$ pwd
/home/dst
你刚刚执行了一个包含单个命令行工具的命令。工具pwd输出你当前所在目录的名称。默认情况下,你登录的是你的主目录。
ZShell 种内置的命令行工具cd允许你导航到不同的目录:
$ cd /data/ch02 # ➊
$ pwd # ➋
/data/ch02
$ cd .. # ➌
$ pwd # ➍
/data
$ cd ch02 # ➎
➊ 导航到目录/data/ch02。
➋ 打印当前目录。
➌ 导航到父目录。
➍ 再次打印当前目录。
➎ 导航到子目录ch02。
cd之后的部分指定你想要去的那个目录。命令后面的值被称为命令行参数或选项。两个点表示父目录。顺便说一下,一个点指的是当前目录。虽然cd .不会有任何影响,但你仍然会看到一个点被用在其他地方。接下来让我们尝试一个不同的命令:
$ head -n 3 movies.txt
Matrix
Star Wars
Home Alone
这里我们将三个命令行参数传递给head。第一个是选项。这里我使用了短选项-n。有时一个短的选项有一个长的变量的意思,现在这种情况下就是--lines,第二个是属于选项的值,第三个是文件名。这个特定的命令的意思是输出文件/data/ch02/movies.txt的前三行内容。
2.3.3 命令行工具的 5 种类型
我们一直在说术语命令行工具,但是到目前为止也没有解释它的真正含义。我把它作为一个总称,指的是任何可以从命令行执行的东西(图 2.1)。实际上,每个命令行工具都是以下五种类型之一:
- 二进制的可执行文件
- Shell 内置程序
- 解释脚本
- Shell 函数
- 别名

图 2.1:命令行工具作为一个总称
我们需要知道命令行之间的区别。Docker 镜像预安装的命令行工具主要包括前两种类型(二进制可执行文件和 Shell 内置程序)。其他三种类型(解释脚本、Shell 函数和别名)允许我们进一步构建我们的数据科学工具箱,从而成为更高效、更高产的数据科学家。
二进制可执行文件
二进制可执行文件是传统意义上的程序,它是通过将源代码编译为机器代码而产生的。这意味着当你在文本编辑器中打开文件时是一个乱码。
Shell 内置工具
Shell 内置工具是 Shell 提供的命令行工具,在我们的例子中是 ZShell(或zsh),它的内置工具包括cd和pwd。不同 Shell 的内置工具可能不同。Shell 内置工具像二进制可执行文件一样不容易检查或更改。
解释脚本
解释脚本是一个可以由二进制可执行文件执行的文本文件。常用的脚本包括:Python、R 和 Bash 脚本。解释脚本的一个很大的优点就是你可以阅读和修改它。下面的脚本可以用 Python 执行,之所以可以被执行,不是因为它的文件扩展名是.py,而是因为脚本的第一行定义了应该执行它的二进制。
$ bat fac.py
───────┬──────────────────────────────────────────────────────────────
│ File: fac.py
───────┼──────────────────────────────────────────────────────────────
1 │ #!/usr/bin/env python
2 │
3 │ def factorial(x):
4 │ result = 1
5 │ for i in range(2, x + 1):
6 │ result *= i
7 │ return result
8 │
9 │ if __name__ == "__main__":
10 │ import sys
11 │ x = int(sys.argv[1])
12 │ sys.stdout.write(f"{factorial(x)}\n")
───────┴──────────────────────────────────────────────────────────────
这个脚本的作用是计算整数的阶乘,我们可以从命令行调用它,如下所示:
$ ./fac.py 5
120
在第四章中,我们将详细讨论如何使用解释脚本创建可重用的命令行工具。
Shell 函数
在我们的例子中,Shell 函数是由zsh执行的函数。它们提供了与脚本相似的功能,但是它们通常(但不一定)比脚本小,也更倾向于个人化。下面的命令定义了一个名为fac的函数,就像上面解释的 Python 脚本一样,它计算我们作为参数传递的整数的阶乘。它通过使用seq生成一个数字列表,使用paste将这些数字放在一行中作为分隔符,并将该等式传递给bc,后者对其求值并输出结果。
$ fac() { (echo 1; seq $1_ | paste -s -d\* - | bc; }
$ fac 5
120
文件~/.zshrc是 ZShell 的配置文件,也是定义 Shell 函数的好地方,在这里定义之后就一直可用了。
别名
别名就像宏一样。如果你发现自己经常用相同的参数(或部分参数)执行某个命令,你就可以为它定义一个别名来节省时间。当你不断拼错某个命令时,别名也非常有用(Chris Wiggins 维护了一个有用的别名列表)。下面的命令就定义了这样一个别名:
$ alias l='ls --color -lhF --group-directories-first'
$ alias les=less
现在,如果你在命令行上输入以下内容,Shell 将用它的值替换它发现的每个别名:
$ cd /data
$ l
total 40K
drwxr-xr-x 2 dst dst 4.0K Mar 3 10:38 ch02/
drwxr-xr-x 2 dst dst 4.0K Mar 3 10:38 ch03/
drwxr-xr-x 3 dst dst 4.0K Mar 3 10:38 ch04/
drwxr-xr-x 2 dst dst 4.0K Mar 3 10:38 ch05/
drwxr-xr-x 2 dst dst 4.0K Mar 3 10:38 ch06/
drwxr-xr-x 2 dst dst 4.0K Mar 3 10:38 ch07/
drwxr-xr-x 2 dst dst 4.0K Mar 3 10:38 ch08/
drwxr-xr-x 2 dst dst 4.0K Mar 3 10:38 ch09/
drwxr-xr-x 4 dst dst 4.0K Mar 3 10:38 ch10/
drwxr-xr-x 3 dst dst 4.0K Mar 3 10:38 csvconf/
$ cd ch02
别名比 Shell 函数简单,因为它们不允许参数。由于参数的原因,无法使用别名定义函数fac。尽管如此,别名可以让你节省大量的击键次数。像 Shell 函数一样,别名通常在文件.zshrc中定义。该文件位于你的主目录下,要查看当前定义的所有别名,可以不带参数地运行alias。试试看,你看到了什么?
在本书中,我们将主要关注最后三种类型的命令行工具:解释脚本、Shell 函数和别名,因为这些类型很容易改变。命令行工具的目的是使你的生活更加轻松,并使你成为更有生产力和效率的数据科学家。你可以通过type找到命令行工具的类型(它本身是一个 Shell 内置的工具):
$ type -a pwd
pwd is a shell builtin
pwd is /usr/bin/pwd
pwd is /bin/pwd
$ type -a cd
cd is a shell builtin
$ type -a fac
fac is a shell function
$ type -a l
l is an alias for ls --color -lhF --group-directories-first
type为pwd返回了三个命令行工具。在这种情况下,当你输入pwd时,将使用第一个命令行工具。在下一节中,我们将学习如何组合命令行工具。
2.3.4 组合命令行工具
因为大多数命令行工具都遵循 Unix 哲学,它们被设计成只做一件事,但是做得非常好。例如,命令行工具grep用来过滤行数据,wc用来计数行数据,sort可以排序行数据。命令行的强大之处在于它能够组合这些小而强大的命令行工具。
命令行的能力是通过管理这些工具的通信流实现的。每个工具都有三个标准通信流:标准输入、标准输出和标准错误。这些通常被简写为stdin``stdout``stderr。
默认情况下,标准输出和标准错误都被重定向到终端,因此正常输出和任何错误信息都被打印在屏幕上。图 2.2 对pwd和rev都进行了说明,如果你运行rev,你会看到什么都没有发生。这是因为rev期望有输入,默认情况下,就是在键盘上按下任何键。试着输入一个句子并按下回车键,rev就会立即对你的输入进行反向响应。你可以按Ctrl+D来停止发送输入,然后rev就会停止。

图 2.2:工具的通信流:标准输入(stdin)、标准输出(stdout)、标准误差(stderr)
但是实际上,我们不会使用键盘作为输入源,而是使用其他工具产生的输出和文件的内容。例如,通过curl,我们可以下载 Lewis Carrol 写的《Alice's Adventures in Wonderland》这本书,然后用管道把它送到下一个工具(curl 将再第三章讨论)。管道通过用管道操作符|完成的。

图 2.3:一个工具的输出通过管道传输到另一个工具
我们可以用管道将curl的输出连接到grep,以过滤每行的数据。想象一下,如果我们想看看目录中列出的章节,我们就可以将curl和grep结合起来使用,如下所示:
$ curl -s "https://www.gutenberg.org/files/11/11-0.txt" | grep " CHAPTER"
CHAPTER I. Down the Rabbit-Hole
CHAPTER II. The Pool of Tears
CHAPTER III. A Caucus-Race and a Long Tale
CHAPTER IV. The Rabbit Sends in a Little Bill
CHAPTER V. Advice from a Caterpillar
CHAPTER VI. Pig and Pepper
CHAPTER VII. A Mad Tea-Party
CHAPTER VIII. The Queen’s Croquet-Ground
CHAPTER IX. The Mock Turtle’s Story
CHAPTER X. The Lobster Quadrille
CHAPTER XI. Who Stole the Tarts?
CHAPTER XII. Alice’s Evidence
如果我们想知道这本书有多少章节,我们可以使用wc,它非常擅长计数:
$ curl -s "https://www.gutenberg.org/files/11/11-0.txt" |
> grep " CHAPTER" |
> wc -l # ➊
12
➊ 选项-l指定wc应该只输出传递给它的行数。默认情况下,它还返回字符数和字数。
你可以把管道操作看成是一种自动的复制和粘贴。一旦你掌握了使用管道操作符组合工具的技巧,你会发现它几乎没有任何限制。
2.3.5 重定向输入和输出
除了将一个工具的输出输送到另一个工具外,你还可以将其保存到一个文件中。该文件将被保存在当前目录下,除非给出完整的路径。这被称为输出重定向,其工作原理如下:
$ curl "https://www.gutenberg.org/files/11/11-0.txt" | grep " CHAPTER" > chapter
s.txt
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 170k 100 170k 0 0 183k 0 --:--:-- --:--:-- --:--:-- 184k
$ cat chapters.txt
CHAPTER I. Down the Rabbit-Hole
CHAPTER II. The Pool of Tears
CHAPTER III. A Caucus-Race and a Long Tale
CHAPTER IV. The Rabbit Sends in a Little Bill
CHAPTER V. Advice from a Caterpillar
CHAPTER VI. Pig and Pepper
CHAPTER VII. A Mad Tea-Party
CHAPTER VIII. The Queen’s Croquet-Ground
CHAPTER IX. The Mock Turtle’s Story
CHAPTER X. The Lobster Quadrille
CHAPTER XI. Who Stole the Tarts?
CHAPTER XII. Alice’s Evidence
在这里,我们将grep的输出保存在/data/ch02目录下一个名为chapters.txt的文件中。如果这个文件还不存在,它将被创建。如果这个文件已经存在,其内容将被覆盖。图 2.4 说明了输出重定向在概念上是如何工作的。注意,标准错误仍然被重定向到终端:

图 2.4:工具的输出可以重定向到一个文件
你还可以使用>>将输出附加到文件中,这意味着输出被添加到原始内容之后:
$ echo -n "Hello" > greeting.txt
$ echo " World" >> greeting.txt
工具echo输出你指定的值。代表换行符的-n选项指定echo不输出尾随换行符\n。
如果你需要存储中间结果,将输出保存到文件中是非常有用的,例如在以后的阶段继续分析。要再次使用文件greeting.txt的内容,我们可以使用cat,它读取一个文件并打印它。
$ cat greeting.txt
Hello World
$ cat greeting.txt | wc -w # ➊
2
➊ -w选项表示wc只统计字数。
使用小于号(<)可以获得相同的结果:
$ < greeting.txt wc -w
2
通过小于号(<)这种方式,你直接将文件传递给wc的标准输入,而不需要运行一个额外的进程。图 2.5 说明了这两种方式的工作原理。同样,最终的输出也是一样的。

图 2.5:使用文件内容作为输入的两种方式
像许多命令行工具一样,wc允许将一个或多个文件名指定为参数。例如:
$ wc -w greeting.txt movies.txt
2 greeting.txt
11 movies.txt
13 total
注意,在这种情况下,wc也输出文件的名称。
你可以通过将任何工具的输出重定向到一个名为/dev/null的特殊文件来保留它。我经常这样做来保留错误消息(见图 2.6 的说明)。下面的内容将导致cat产生一个错误信息,因为它找不到404.txt这个文件:
$ cat movies.txt 404.txt
Matrix
Star Wars
Home Alone
Indiana Jones
Back to the Future
/usr/bin/cat: 404.txt: No such file or directory
你可以将标准错误重定向到/dev/null,如下所示:
$ cat movies.txt 404.txt 2> /dev/null # ➊
Matrix
Star Wars
Home Alone
Indiana Jones
Back to the Future
➊ 2指标准错误

图 2.6:将stderr重定向到/dev/null
注意不要从同一个文件中读出和写入。如果你这样做,你会得到一个空文件。这是因为输出被重定向的工具会立即打开该文件进行写入,从而将其清空。这有两个解决办法:(1)写到一个不同的文件,然后用mv重命名;(2)使用sponge,它在写到一个文件之前吸收了所有的输入。图 2.7 说明了这是如何工作的:

图 2.7:除非你使用sponge,否则你不能在一个管道中读取和写入同一个文件
例如,假设你已经使用dseq生成了一个文件dates.txt,现在你想使用nl添加行号。如果运行下面的代码,文件dates.txt将会是空的。
$ dseq 5 > dates.txt
$ < dates.txt nl > dates.txt
$ bat dates.txt
───────┬────────────────────────────────────────────────────────────────────────
│ File: dates.txt <EMPTY>
───────┴────────────────────────────────────────────────────────────────────────
所以说你可以使用我刚刚描述的解决方法之一:
$ dseq 5 > dates.txt
$ < dates.txt nl > dates-nl.txt
$ bat dates-nl.txt
───────┬────────────────────────────────────────────────────────────────────────
│ File: dates-nl.txt
───────┼────────────────────────────────────────────────────────────────────────
1 │ 1 2022-03-04
2 │ 2 2022-03-05
3 │ 3 2022-03-06
4 │ 4 2022-03-07
5 │ 5 2022-03-08
───────┴────────────────────────────────────────────────────────────────────────
$ dseq 5 > dates.txt
$ < dates.txt nl | sponge dates.txt
$ bat dates.txt
───────┬────────────────────────────────────────────────────────────────────────
│ File: dates.txt
───────┼────────────────────────────────────────────────────────────────────────
1 │ 1 2022-03-04
2 │ 2 2022-03-05
3 │ 3 2022-03-06
4 │ 4 2022-03-07
5 │ 5 2022-03-08
───────┴────────────────────────────────────────────────────────────────────────
2.3.6 使用文件和目录
作为数据科学家,我们处理大量数据。这些数据通常存储在文件中。了解如何在命令行上处理文件(以及它们所在的目录)是很重要的。使用 GUI 可以做的每一个动作,都可以用命令行工具来完成(等等)。在这一节中,我将介绍列举、创建、移动、复制、重命名和删除文件和目录的最重要的方法。
用ls可以列出一个目录的内容。如果不指定目录,它会列出当前目录的内容。我更喜欢ls有一个长列表格式,并且目录和文件分组,目录在前。我使用别名l,而不是每次都输入相应的选项。
$ ls /data/ch10
alice.txt count.py count.R __pycache__ Untitled1337.ipynb
$ alias l
l='ls --color -lhF --group-directories-first'
$ l /data/ch10
total 180K
drwxr-xr-x 2 dst dst 4.0K Mar 3 10:38 __pycache__/
-rw-r--r-- 1 dst dst 164K Mar 3 10:38 alice.txt
-rwxr--r-- 1 dst dst 408 Mar 3 10:38 count.py*
-rw-r--r-- 1 dst dst 460 Mar 3 10:38 count.R
-rw-r--r-- 1 dst dst 1.7K Mar 3 10:38 Untitled1337.ipynb
你已经看到了我们如何通过使用>或>>重定向输出来创建新文件。如果你需要将文件移动到不同的目录,你可以使用mv:
$ mv hello.txt /data/ch02
你也可以使用mv重命名文件:
$ cd data
$ mv hello.txt bye.txt
你也可以重命名或移动整个目录。如果你不再需要一个文件,你用rm删除它:
$ rm bye.txt
如果你想要删除整个目录及其所有内容,请指定-r选项,它代表递归:
$ rm -r /data/ch02/old
如果要复制一个文件,使用cp。这对于创建备份非常有用:
$ cp server.log server.log.bak
你可以使用mkdir创建目录:
$ cd /data
$ mkdir logs
$ l
total 44K
drwxr-xr-x 2 dst dst 4.0K Mar 3 10:39 ch02/
drwxr-xr-x 2 dst dst 4.0K Mar 3 10:38 ch03/
drwxr-xr-x 3 dst dst 4.0K Mar 3 10:38 ch04/
drwxr-xr-x 2 dst dst 4.0K Mar 3 10:38 ch05/
drwxr-xr-x 2 dst dst 4.0K Mar 3 10:38 ch06/
drwxr-xr-x 2 dst dst 4.0K Mar 3 10:38 ch07/
drwxr-xr-x 2 dst dst 4.0K Mar 3 10:38 ch08/
drwxr-xr-x 2 dst dst 4.0K Mar 3 10:38 ch09/
drwxr-xr-x 4 dst dst 4.0K Mar 3 10:38 ch10/
drwxr-xr-x 3 dst dst 4.0K Mar 3 10:38 csvconf/
drwxr-xr-x 2 dst dst 4.0K Mar 3 10:39 logs/
使用命令行工具来管理你的文件,一开始可能很可怕,因为你没有文件系统的图形概览来提供即时反馈。有一些可视化的文件管理器可以帮助解决这个问题,比如 GNU Midnight Commander、Ranger 和 Vifm。这些都没有安装在 Docker 镜像中,但你可以通过运行 sudo apt install,然后选择 mc、ranger 或 vifm,自己安装一个。
上面所有的命令行工具都接受代表 verbose 的-v选项,这样它们就可以输出正在发生的事情。例如:
$ mkdir -v backup
/usr/bin/mkdir: created directory 'backup'
$ cp -v * backup
/usr/bin/cp: -r not specified; omitting directory 'backup'
/usr/bin/cp: -r not specified; omitting directory 'ch02'
/usr/bin/cp: -r not specified; omitting directory 'ch03'
/usr/bin/cp: -r not specified; omitting directory 'ch04'
/usr/bin/cp: -r not specified; omitting directory 'ch05'
/usr/bin/cp: -r not specified; omitting directory 'ch06'
/usr/bin/cp: -r not specified; omitting directory 'ch07'
/usr/bin/cp: -r not specified; omitting directory 'ch08'
/usr/bin/cp: -r not specified; omitting directory 'ch09'
/usr/bin/cp: -r not specified; omitting directory 'ch10'
/usr/bin/cp: -r not specified; omitting directory 'csvconf'
/usr/bin/cp: -r not specified; omitting directory 'logs'
除了mkdir之外的所有工具也接受-i选项,它代表“交互式”,这样工具就会要求你确认。例如:
$ rm -i *
zsh: sure you want to delete all 12 files in /data [yn]? n
2.3.7 管理输出
有时,一个工具或工具序列产生了太多的输出,无法包含在书中。与其手动改变这样的输出,我更喜欢通过一个辅助工具的管道将其透明化。你不一定要这样做,尤其是如果你对完整的输出感兴趣。
以下是我用来管理输出的工具:
我们可以使用trim来限制输出给定的高度和宽度,默认情况下,输出被修剪为 10 行和终端的宽度,但也可以传递一个负数以禁止修剪高度和/或宽度。例如:
$ cat /data/ch07/tips.csv | trim 5 25
bill,tip,sex,smoker,day,…
16.99,1.01,Female,No,Sun…
10.34,1.66,Male,No,Sun,D…
21.01,3.5,Male,No,Sun,Di…
23.68,3.31,Male,No,Sun,D…
… with 240 more lines
我用来管理输出的其他工具有:head、tail、fold、paste和column,附录中包含了每种方法的示例。
如果输出是逗号分隔的值,我通常通过csvlook将它转换成一个好看的表格。如果你运行csvlook,你将看到完整的表格。我通过trim重新定义了csvlook,这样表格就缩短了:
$ which csvlook
csvlook() {
/usr/bin/csvlook "$@" | trim | sed 's/- | -/──┼──/g;s/| -/├──/g;s/- |/──
┤/;s/|/│/g;2s/-/─/g'
}
$ csvlook /data/ch07/tips.csv
│ bill │ tip │ sex │ smoker │ day │ time │ size │
├───────┼───────┼────────┼────────┼──────┼────────┼──────┤
│ 16.99 │ 1.01 │ Female │ False │ Sun │ Dinner │ 2 │
│ 10.34 │ 1.66 │ Male │ False │ Sun │ Dinner │ 3 │
│ 21.01 │ 3.50 │ Male │ False │ Sun │ Dinner │ 3 │
│ 23.68 │ 3.31 │ Male │ False │ Sun │ Dinner │ 2 │
│ 24.59 │ 3.61 │ Female │ False │ Sun │ Dinner │ 4 │
│ 25.29 │ 4.71 │ Male │ False │ Sun │ Dinner │ 4 │
│ 8.77 │ 2.00 │ Male │ False │ Sun │ Dinner │ 2 │
│ 26.88 │ 3.12 │ Male │ False │ Sun │ Dinner │ 4 │
… with 236 more lines
我使用bat来显示文件的内容,其中行号和语法会突出显示。例如源代码:
$ bat /data/ch04/stream.py
───────┬────────────────────────────────────────────────────────────────────────
│ File: /data/ch04/stream.py
───────┼────────────────────────────────────────────────────────────────────────
1 │ #!/usr/bin/env python
2 │ from sys import stdin, stdout
3 │ while True:
4 │ line = stdin.readline()
5 │ if not line:
6 │ break
7 │ stdout.write("%d\n" % int(line)**2)
8 │ stdout.flush()
───────┴────────────────────────────────────────────────────────────────────────
有时,当我想明确指出文件中的空格、制表符和换行符时,我会添加-A选项。
有时将中间输出写到文件中很有用。这允许你在管道中的任何步骤完成后对其进行检查。你可以在你的管道中插入工具tee。我经常用它来检查最终输出的一部分,同时将完整的输出写入文件(见图 2.8)。在这个例子中,完整的输出被写入even.txt,前 5 行被使用trim打印:
$ seq 0 2 100 | tee even.txt | trim 5
0
2
4
6
8
… with 46 more lines

图 2.8:使用tee将中间输出写入文件
最后,为了插入由命令行工具生成的图片(除了屏幕截图和图表之外的每张图片),我使用了display。但是如果你运行display,你会发现它不起作用。在第七章中,我介绍了四个选项,让你从命令行中显示生成的图像。
2.3.8 帮助
当你在命令行中摸索时,可能会需要帮助,即使是最有经验的用户在某些时候也需要帮助。我们不可能记住所有不同的命令行工具及其可能的参数。幸运的是,命令行提供了几种获得帮助的方法。
获得帮助最重要的命令或许是man,是手动的简称。它包含大多数命令行工具的信息。如果我忘记了工具tar的选项,这种情况经常发生,我只需使用以下命令访问它的手册页:
$ man tar | trim 20
TAR(1) GNU TAR Manual TAR(1)
NAME
tar - an archiving utility
SYNOPSIS
Traditional usage
tar {A|c|d|r|t|u|x}[GnSkUWOmpsMBiajJzZhPlRvwo] [ARG...]
UNIX-style usage
tar -A [OPTIONS] ARCHIVE ARCHIVE
tar -c [-f ARCHIVE] [OPTIONS] [FILE...]
tar -d [-f ARCHIVE] [OPTIONS] [FILE...]
tar -t [-f ARCHIVE] [OPTIONS] [MEMBER...]
tar -r [-f ARCHIVE] [OPTIONS] [FILE...]
… with 1147 more lines
并非每个命令行工具都有手册页。以cd为例:
$ man cd
No manual entry for cd
对于像cd这样的 Shell 内置,你可以参考zshbuildins手册页:
$ man zshbuiltins | trim
ZSHBUILTINS(1) General Commands Manual ZSHBUILTINS(1)
NAME
zshbuiltins - zsh built-in commands
SHELL BUILTIN COMMANDS
Some shell builtin commands take options as described in individual en‐
tries; these are often referred to in the list below as `flags' to
avoid confusion with shell options, which may also have an effect on
the behaviour of builtin commands. In this introductory section, `op‐
… with 2735 more lines
按/可以搜索,按q可以退出。尝试为cd找到合适的部分。
较新的命令行工具通常也没有手册页。在这种情况下,最好的办法是使用--help(或-h)选项调用工具。例如:
$ jq --help | trim
jq - commandline JSON processor [version 1.6]
Usage: /usr/bin/jq [options] <jq filter> [file...]
/usr/bin/jq [options] --args <jq filter> [strings...]
/usr/bin/jq [options] --jsonargs <jq filter> [JSON_TEXTS...]
jq is a tool for processing JSON inputs, applying the given filter to
its JSON text inputs and producing the filter's results as JSON on
standard output.
… with 37 more lines
指定--help选项也适用于命令行工具,比如cat。但是,相应的手册页通常会提供更多信息。如果在尝试了这三种方法后,你仍然有不明白的地方,那为啥不 Google 一下呢。在附录中,列出了本书中使用的所有命令行工具。除了如何安装每个命令行工具,它还显示了如何获得帮助。
手册页可能非常冗长,难以阅读。工具tldr是一个由社区维护的命令行工具帮助页面的集合,旨在成为传统手册页面的一个更简单、更易用的补充。下面是一个显示tar的tldr页面的示例:
$ tldr tar | trim 20
tar
Archiving utility.
Often combined with a compression method, such as gzip or bzip2.
More information: https://www.gnu.org/software/tar.
- [c]reate an archive and write it to a [f]ile:
tar cf target.tar file1 file2 file3
- [c]reate a g[z]ipped archive and write it to a [f]ile:
tar czf target.tar.gz file1 file2 file3
- [c]reate a g[z]ipped archive from a directory using relative paths:
tar czf target.tar.gz --directory=path/to/directory .
- E[x]tract a (compressed) archive [f]ile into the current directory [v]erbos…
tar xvf source.tar[.gz|.bz2|.xz]
- E[x]tract a (compressed) archive [f]ile into the target directory:
… with 12 more lines
如你所见,tldr没有像man经常做的那样按字母顺序列出许多选项,而是通过给你一个实际例子。
2.4 总结
在本章中,你学习了如何通过安装 Docker 镜像来获得所有需要的命令行工具。我还介绍了一些基本的命令行概念以及如何获得帮助。现在你已经具备了所有必要的要素,也已经为 OSEMN 数据科学模型的第一步做好了准备:获取数据。
2.5 进一步探索
- 本书的副标题是向 Jerry Peek、Shelley Powers、Tim O'Reilly 和 Mike Loukides 的史诗般的书《Unix Power Tools》致敬。在该书 51 个章节和一千多页中,它几乎涵盖了关于 Unix 的所有知识,它的重量超过 4 磅,所以你可以考虑买本电子书。
- 网站 explainshell 解析了一条命令或一连串的命令,并对每个部分提供了简短的解释。这对快速理解一个新的命令或选项很有用,而不必粗略地阅读相关的手册页面。
- Docker 确实是一个出色的软件。在本章中,我简要介绍了如何下载 Docker 镜像和运行 Docker 容器,但学习如何创建自己的 Docker 镜像可能是值得的。Sean Kane 和 Karl Matthias 的《Docker: Up & Running》一书是一个很好的资源。
三、获取数据
原文:https://datascienceatthecommandline.com/2e/chapter-3-obtaining-data.html
贡献者:Ting-xin
本章讨论 OSEMN 模型的第一步:获取数据。毕竟,没有任何数据,我们就没有多少数据科学可以做。我假设你已经有了解决数据科学问题所需的数据,第一步你需要把这些数据放到你的电脑上(也可能放到 Docker 容器里)。
在 Unix 哲学中,文本是一个通用接口。几乎每个命令行工具都将文本作为输入,或者以文本作为输出,或者两者都有。这就是为什么命令行工具可以很好地协同工作的主要原因。然而,正如我们将看到的,即使只是文本也可以有多种形式。
我们可以通过多种方式获取数据,例如从服务器下载数据、查询数据库或连接到 Web API。有时,数据以压缩的形式或二进制格式出现,如 Microsoft Excel 电子表格。在这一章中,我们将讨论了几个有助于从命令行解决这个问题的工具,包括:curl,in2csv,sql2csv,以及tar。
3.1 概述
在本章中,你将学习如何:
- 将本地文件复制到 Docker 镜像
- 从互联网下载数据
- 解压缩文件
- 从电子表格中提取数据
- 查询关系数据库
- 调用 Web API
首先打开第三章的目录:
$ cd /data/ch03
$ l
total 924K
-rw-r--r-- 1 dst dst 627K Mar 3 10:41 logs.tar.gz
-rw-r--r-- 1 dst dst 189K Mar 3 10:41 r-datasets.db
-rw-r--r-- 1 dst dst 149 Mar 3 10:41 tmnt-basic.csv
-rw-r--r-- 1 dst dst 148 Mar 3 10:41 tmnt-missing-newline.csv
-rw-r--r-- 1 dst dst 181 Mar 3 10:41 tmnt-with-header.csv
-rw-r--r-- 1 dst dst 91K Mar 3 10:41 top2000.xlsx
获取这些文件已经在第二章中做过了。任何其他文件都是使用命令行工具下载或生成的。
3.2 将本地文件复制到 Docker 容器
一种常见的情况是,你自己的计算机上已经有了需要的文件,本节介绍了如何将这些文件放入 Docker 容器。
我在第二章提到 Docker 容器是一个隔离的虚拟环境。但是有一个例外:文件可以在 Docker 容器中进出传输。运行docker run的本地目录会被映射到 Docker 容器中的一个目录。这个目录叫做/data。注意这不是主目录,主目录是/home/dst。
如果你的本地计算机上有一个或多个文件,并且你想对它们应用一些命令行工具,那么你需要将这些文件复制或移动到那个映射的目录中。假设你的下载目录中有一个名为logs.csv的文件,现在我们来复制文件。
如果你正在运行 Windows,请打开命令提示符或 PowerShell 并运行以下两个命令:
> cd %UserProfile%\Downloads
> copy logs.csv MyDataScienceToolbox\
如果你运行的是 Linux 或 macOS,请打开一个终端并在你的操作系统上执行以下命令(而不是在 Docker 容器中):
$ cp ~/Downloads/logs.csv ~/my-data-science-toolbox
你也可以使用图形文件管理器(如 Windows Explorer 或 macOS Finder)将文件拖放到正确的目录中。
3.3 从互联网上下载数据
毫无疑问,互联网已经成为了数据的最大来源。当从互联网下载数据时,命令行工具curl被认为是命令行中的瑞士军刀。
3.3.1 curl介绍
当你浏览到一个代表统一资源定位符的 URL 时,你的浏览器会渲染它下载的数据。例如,浏览器会呈现 HTML 文件,自动播放视频文件,显示 PDF 文件。然而,当你使用curl来访问一个 URL 时,它会下载数据,并在默认情况下将其打印出来。curl不会做任何解释和渲染,但幸运的是可以使用其他命令行工具来进一步处理数据。
最简单的调用curl是指定一个 URL 作为命令行参数。现在让我们试着从维基百科下载一篇文章:
$ curl "https://en.wikipedia.org/wiki/List_of_windmills_in_the_Netherlands" |
> trim # ➊
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0<!
DOCTYPE html>
<html class="client-nojs" lang="en" dir="ltr">
<head>
<meta charset="UTF-8"/>
<title>List of windmills in the Netherlands - Wikipedia</title>
<script>document.documentElement.className="client-js";RLCONF={"wgBreakFrames":…
"wgRelevantPageName":"List_of_windmills_in_the_Netherlands","wgRelevantArticleI…
,"site.styles":"ready","user.styles":"ready","ext.globalCssJs.user":"ready","us…
"ext.growthExperiments.SuggestedEditSession"];</script>
<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.implement("user.options@…
100 250k 100 250k 0 0 866k 0 --:--:-- --:--:-- --:--:-- 891k
… with 1760 more lines
➊ 记住,trim只是用来让输出很好地适应终端
如你所见,curl下载维基百科的服务器返回的原始 HTML 不进行任何解释,所有内容立即打印在标准输出上。因为这个 URL,你会认为这篇文章会列出荷兰所有的风车。然而,显然有太多的风车留下来,每个省都有自己的网页。令人着迷。
默认情况下,curl会输出一个进度条,显示下载速度和预期完成时间。这个输出不是写到标准输出,而是一个单独的通道,称为标准错误,所以当你在管道中添加另一个工具时,它们之间不会干扰。虽然这些信息在下载非常大的文件时会很有用,但它通常会分散我们的注意力,所以我指定了-s选项来让忽略这个输出。
$ curl -s "https://en.wikipedia.org/wiki/List_of_windmills_in_Friesland" |
> pup -n 'table.wikitable tr' # ➊
234
➊ 我会讨论pup,一个方便的抓取网站的工具,更详细的内容在第五章。
你知道吗,仅在弗里斯兰省就有 234 座风车!(译者也不懂为啥突然来这么一句
3.3.2 保存
你可以通过添加-O选项将curl的输出保存到文件中,文件名将基于 URL 的最后一部分。
$ curl -s "https://en.wikipedia.org/wiki/List_of_windmills_in_Friesland" -O
$ l
total 1.4M
-rw-r--r-- 1 dst dst 432K Mar 3 10:41 List_of_windmills_in_Friesland
-rw-r--r-- 1 dst dst 627K Mar 3 10:41 logs.tar.gz
-rw-r--r-- 1 dst dst 189K Mar 3 10:41 r-datasets.db
-rw-r--r-- 1 dst dst 149 Mar 3 10:41 tmnt-basic.csv
-rw-r--r-- 1 dst dst 148 Mar 3 10:41 tmnt-missing-newline.csv
-rw-r--r-- 1 dst dst 181 Mar 3 10:41 tmnt-with-header.csv
-rw-r--r-- 1 dst dst 91K Mar 3 10:41 top2000.xlsx
如果你不喜欢这个文件名,那么你可以选择使用-o选项和一个文件名来保存文件,或者将输出重定向到一个新的文件:
$ curl -s "https://en.wikipedia.org/wiki/List_of_windmills_in_Friesland" > fries
land.html
3.3.3 其他协议
curl总共支持 20 多种协议。从 FTP 服务器(代表文件传输协议)下载文件同样也可以使用curl,下面显示从ftp.gnu.org下载文件welcome.msg:
$ curl -s "ftp://ftp.gnu.org/welcome.msg" | trim
NOTICE (Updated October 15 2021):
If you maintain scripts used to access ftp.gnu.org over FTP,
we strongly encourage you to change them to use HTTPS instead.
Eventually we hope to shut down FTP protocol access, but plan
to give notice here and other places for several months ahead
of time.
--
… with 19 more lines
如果指定的 URL 是一个 DICT 协议,curl将列出该目录的内容。当 URL 受密码保护时,你可以使用-u选项指定用户名和密码。DICT 协议允许你访问各种字典和查找定义,根据《国际英语协作词典》,下面是“风车”的定义:
$ curl -s "dict://dict.org/d:windmill" | trim
220 dict.dict.org dictd 1.12.1/rf on Linux 4.19.0-10-amd64 <auth.mime> <9813708…
250 ok
150 1 definitions retrieved
151 "Windmill" gcide "The Collaborative International Dictionary of English v.0…
Windmill \Wind"mill`\, n.
A mill operated by the power of the wind, usually by the
action of the wind upon oblique vanes or sails which radiate
from a horizontal shaft. --Chaucer.
[1913 Webster]
.
… with 2 more lines
然而,当从互联网下载数据时,协议很可能是 HTTP,因此 URL 将以http://或https://开头。
3.3.4 重定向设置
当你访问一个缩短的网址时,比如以http://bit.ly/或http://t.co/开头的网址,你的浏览器会自动将你重定向到正确的位置。然而,使用curl,你需要指定-L或--location选项以便被重定向。如果没有,你可能会得到这样的结果:
$ curl -s "https://bit.ly/2XBxvwK"
<html>
<head><title>Bitly</title></head>
<body><a href="https://youtu.be/dQw4w9WgXcQ">moved here</a></body>
</html>%
有时候你什么也得不到,就像我们按照上面提到的网址:
$ curl -s "https://youtu.be/dQw4w9WgXcQ"
通过指定-I或--head选项,curl只获取响应的 HTTP 头,这允许你检查服务器返回的状态代码和其他信息。
$ curl -sI "https://youtu.be/dQw4w9WgXcQ" | trim
HTTP/2 303
content-type: application/binary
x-content-type-options: nosniff
cache-control: no-cache, no-store, max-age=0, must-revalidate
pragma: no-cache
expires: Mon, 01 Jan 1990 00:00:00 GMT
date: Thu, 03 Mar 2022 09:41:56 GMT
location: https://www.youtube.com/watch?v=dQw4w9WgXcQ&feature=youtu.be
content-length: 0
x-frame-options: SAMEORIGIN
… with 11 more lines
信息中第一行显示了 HTTP 协议和状态码,在本例中是 303。你还可以看到该 URL 重定向到的位置。如果curl没有给你预期的结果,检查标题并获得状态码是一个有用的调试工具。其他常见的 HTTP 状态代码包括 404(未找到)和 403(禁止)。维基百科列出了所有 HTTP 状态码。
总之,curl是一个有用的从互联网下载数据的命令行工具。它的三个最常见的选项是-s忽略进度条、-u指定用户名和密码、-L自动跟随重定向。请参阅其手册页了解更多信息:
$ man curl | trim 20
curl(1) Curl Manual curl(1)
NAME
curl - transfer a URL
SYNOPSIS
curl [options / URLs]
DESCRIPTION
curl is a tool to transfer data from or to a server, using one of the
supported protocols (DICT, FILE, FTP, FTPS, GOPHER, HTTP, HTTPS, IMAP,
IMAPS, LDAP, LDAPS, MQTT, POP3, POP3S, RTMP, RTMPS, RTSP, SCP, SFTP,
SMB, SMBS, SMTP, SMTPS, TELNET and TFTP). The command is designed to
work without user interaction.
curl offers a busload of useful tricks like proxy support, user authen‐
tication, FTP upload, HTTP post, SSL connections, cookies, file trans‐
fer resume, Metalink, and more. As you will see below, the number of
features will make your head spin!
… with 3986 more lines
3.4 解压文件
如果原始数据集非常大,或者它是许多文件的集合,则该文件可能是压缩文件。包含许多重复值的数据集(如文本文件中的单词或 JSON 文件中的键)特别适合压缩。
压缩文件常见的文件扩展名有:.tar.gz、.zip和.rar。要解压缩这些文件,你可以使用命令行工具tar、unzip和unrar。(当然还有一些不太常见的文件扩展名,这些可能需要其他工具来处理。)
让我们以tar.gz(读作“gzipped tarball”)为例。为了提取名为logs.tar.gz的档案,你将使用以下命令行:
$ tar -xzf logs.tar.gz # ➊
➊ 压缩的时候将xzf这三个短选项组合在一起是很常见的,就像我在这里做的一样。当然你也可以将它们分别指定为-x -z -f,事实上,许多命令工具都允许你由单个字符组合成选项。
tar因其众多的命令行参数而臭名昭著。在这种情况下,三个选项-x、-z和-f表明tar将会用gzip作为解压缩算法从压缩文件中提取文件。
但是,由于我们还不熟悉这个压缩文件,所以最好先检查一下它的内容,这可以通过-t选项(而不是-x选项)来完成:
$ tar -tzf logs.tar.gz | trim
E1FOSPSAYDNUZI.2020-09-01-00.0dd00628
E1FOSPSAYDNUZI.2020-09-01-00.b717c457
E1FOSPSAYDNUZI.2020-09-01-01.05f904a4
E1FOSPSAYDNUZI.2020-09-01-02.36588daf
E1FOSPSAYDNUZI.2020-09-01-02.6cea8b1d
E1FOSPSAYDNUZI.2020-09-01-02.be4bc86d
E1FOSPSAYDNUZI.2020-09-01-03.16f3fa32
E1FOSPSAYDNUZI.2020-09-01-03.1c0a370f
E1FOSPSAYDNUZI.2020-09-01-03.76df64bf
E1FOSPSAYDNUZI.2020-09-01-04.0a1ade1b
… with 2427 more lines
这个压缩文件中包含了很多文件,并且它们不在一个目录中,因此为了保持当前目录的整洁,最好首先使用mkdir创建一个新目录,然后使用-C选项提取其中的文件。
$ mkdir logs
$ tar -xzf logs.tar.gz -C logs
让我们来看一下文件的数量及其部分内容:
$ ls logs | wc -l
2437
$ cat logs/* | trim
#Version: 1.0
#Fields: date time x-edge-location sc-bytes c-ip cs-method cs(Host) cs-uri-stem…
2020-09-01 00:51:54 SEA19-C1 391 206.55.174.150 GET …
2020-09-01 00:54:59 CPH50-C2 384 82.211.213.95 GET …
#Version: 1.0
#Fields: date time x-edge-location sc-bytes c-ip cs-method cs(Host) cs-uri-stem…
2020-09-01 00:04:28 DFW50-C1 391 2a03:2880:11ff:9::face:…
#Version: 1.0
#Fields: date time x-edge-location sc-bytes c-ip cs-method cs(Host) cs-uri-stem…
2020-09-01 01:04:14 ATL56-C4 385 2600:1700:2760:da20:548…
… with 10279 more lines
非常好。现在,我知道你想清理和研究这些日志文件,但那是以后在第五章和第七章中讨论的事情。
随着时间的推移,你会习惯这些选项,但我想给你看一个比较方便的替代脚本,它不需要记住不同的命令行工具和它们的选项,这个方便的脚本叫做unpack,它可以解压缩许多不同的格式。unpack查看你想要解压缩的文件的扩展名,并调用适当的命令行工具。现在,为了解压缩这个文件,你可以运行:
$ unpack logs.tar.gz
3.5 将 Microsoft Excel 电子表格转换为 CSV 格式
对于许多人来说,Microsoft Excel 提供了一种直观的方式来处理小型数据集并对其执行计算。因此,大量数据被嵌入到 Microsoft Excel 电子表格中。根据文件名的扩展名,这些电子表格要么以专有的二进制格式(.xls)存储,要么以压缩的 XML 文件的集合(.xlsx)存储。但这两种情况下都不利于大多数命令行工具使用这些数据。如果仅仅因为这些有价值的数据集以这种方式存储,我们就不能使用它们了,那这将是一种耻辱。
特别是当你刚开始使用命令行时,你可能会尝试通过在 Microsoft Excel 或开源版本(如 LibreOffice Calc)中打开电子表格,然后手动将其导出为 CSV 格式,从而将电子表格转换为 CSV 格式。虽然这也是一个解决方案,但缺点是它不能很好地扩展到多个文件,并且不能自动化。此外,当你在服务器上工作时,很可能没有这样的应用可用。相信我,命令行是一个更好的解决方案。
幸运的是,我们有一个名为in2csv的命令行工具,它可以将 Microsoft Excel 电子表格转换成 CSV 文件。CSV 指的是逗号分隔的数值,使用 CSV 文件可能会很棘手,因为它缺乏正式的规范。Yakov Shafranovich 将 CSV 格式定义为以下三点:
- 每条记录位于单独的一行,由换行符(
LF)分隔。例如,下面的 CSV 文件包含了关于忍者神龟的重要信息:
$ bat -A tmnt-basic.csv # ➊
───────┬────────────────────────────────────────────────────────────────────────
│ File: tmnt-basic.csv
───────┼────────────────────────────────────────────────────────────────────────
1 │ Leonardo,Leo,blue,two·ninjakens␊
2 │ Raphael,Raph,red,pair·of·sai␊
3 │ Michelangelo,Mikey·or·Mike,orange,pair·of·nunchaku␊
4 │ Donatello,Donnie·or·Don,purple,staff␊
───────┴────────────────────────────────────────────────────────────────────────
➊ -A选项使bat显示所有不可打印的字符,如空格、制表符和换行符。
- 文件中的最后一条记录可能有也可能没有结束换行符,例如:
$ bat -A tmnt-missing-newline.csv
───────┬────────────────────────────────────────────────────────────────────────
│ File: tmnt-missing-newline.csv
───────┼────────────────────────────────────────────────────────────────────────
1 │ Leonardo,Leo,blue,two·ninjakens␊
2 │ Raphael,Raph,red,pair·of·sai␊
3 │ Michelangelo,Mikey·or·Mike,orange,pair·of·nunchaku␊
4 │ Donatello,Donnie·or·Don,purple,staff
───────┴────────────────────────────────────────────────────────────────────────
- 文件的第一行可能会出现一个标题行,其格式与普通记录行相同。该标题将包含与文件中的字段相对应的名称,并且应该包含与文件其余部分中的记录相同数量的字段。例如:
$ bat -A tmnt-with-header.csv
───────┬────────────────────────────────────────────────────────────────────────
│ File: tmnt-with-header.csv
───────┼────────────────────────────────────────────────────────────────────────
1 │ name,nickname,mask_color,weapon␊
2 │ Leonardo,Leo,blue,two·ninjakens␊
3 │ Raphael,Raph,red,pair·of·sai␊
4 │ Michelangelo,Mikey·or·Mike,orange,pair·of·nunchaku␊
5 │ Donatello,Donnie·or·Don,purple,staff␊
───────┴────────────────────────────────────────────────────────────────────────
如你所见,默认情况下,CSV 不太可读。你可以通过管道将数据传输到一个名为csvlook的工具,它会很好地将数据格式化成表格。如果 CSV 数据没有头,比如tmnt-missing-newline.csv,那么你需要添加-H选项,否则第一行将被解释为头。
$ csvlook tmnt-with-header.csv
│ name │ nickname │ mask_color │ weapon │
├──────────────┼───────────────┼────────────┼──────────────────┤
│ Leonardo │ Leo │ blue │ two ninjakens │
│ Raphael │ Raph │ red │ pair of sai │
│ Michelangelo │ Mikey or Mike │ orange │ pair of nunchaku │
│ Donatello │ Donnie or Don │ purple │ staff │
$ csvlook tmnt-basic.csv
│ Leonardo │ Leo │ blue │ two ninjakens │
├──────────────┼───────────────┼────────┼──────────────────┤
│ Raphael │ Raph │ red │ pair of sai │
│ Michelangelo │ Mikey or Mike │ orange │ pair of nunchaku │
│ Donatello │ Donnie or Don │ purple │ staff │
$ csvlook -H tmnt-missing-newline.csv # ➊
│ a │ b │ c │ d │
├──────────────┼───────────────┼────────┼──────────────────┤
│ Leonardo │ Leo │ blue │ two ninjakens │
│ Raphael │ Raph │ red │ pair of sai │
│ Michelangelo │ Mikey or Mike │ orange │ pair of nunchaku │
│ Donatello │ Donnie or Don │ purple │ staff │
➊ -H选项表示 CSV 文件中没有标题行。
让我们用一个电子表格来演示一下in2csv,这个表格包含了一年一度的荷兰马拉松广播节目 2000 强中 2000 首最流行的歌曲。要提取它的数据,你可以如下调用in2csv:
$ in2csv top2000.xlsx | tee top2000.csv | trim
NR.,ARTIEST,TITEL,JAAR
1,Danny Vera,Roller Coaster,2019
2,Queen,Bohemian Rhapsody,1975
3,Eagles,Hotel California,1977
4,Billy Joel,Piano Man,1974
5,Led Zeppelin,Stairway To Heaven,1971
6,Pearl Jam,Black,1992
7,Boudewijn de Groot,Avond,1997
8,Coldplay,Fix You,2005
9,Pink Floyd,Wish You Were Here,1975
… with 1991 more lines
Danny Vera 是谁?当然,最受欢迎的歌曲应该是波西米亚狂想曲。嗯,至少 Queen 在前 2000 名中出现了很多次,所以我也不应该抱怨:
$ csvgrep top2000.csv --columns ARTIEST --regex '^Queen$' | csvlook -I # ➊
│ NR. │ ARTIEST │ TITEL │ JAAR │
├──────┼─────────┼─────────────────────────────────┼──────┤
│ 2 │ Queen │ Bohemian Rhapsody │ 1975 │
│ 11 │ Queen │ Love Of My Life │ 1975 │
│ 46 │ Queen │ Innuendo │ 1991 │
│ 55 │ Queen │ Don't Stop Me Now │ 1979 │
│ 70 │ Queen │ Somebody To Love │ 1976 │
│ 85 │ Queen │ Who Wants To Live Forever │ 1986 │
│ 89 │ Queen │ The Show Must Go On │ 1991 │
│ 131 │ Queen │ Killer Queen │ 1974 │
… with 24 more lines
➊ --regex选项后的值是一个正则表达式(或 regex)。这是一种定义模式的特殊语法。因为我只想匹配与Queen完全匹配的艺术家,所以我使用插入符号(^)和美元符号($)来匹配列ARTIEST中值的开始和结束。
顺便说一下,工具in2csv、csvgrep和csvlook都是 CSVkit 的一部分,CSVkit 是处理 CSV 数据的命令行工具的集合。
文件的格式是由扩展名自动决定的,本例中是.xlsx。如果你要将数据导入in2csv,你必须明确指定格式。
一个电子表格可以包含多个工作表。默认情况下,in2csv提取第一个工作表。如果要提取不同的工作表,那么需要将工作表的名称传递给--sheet选项。如果你不确定工作表叫什么,你可以使用--names选项查看,它会打印出所有工作表的名称。这里我们看到top2000.xlsx只有一张表,名为Blad1(荷兰语,意思是Sheet1)。
$ in2csv --names top2000.xlsx
Blad1
3.6 查询关系数据库
许多公司将他们的数据存储在关系数据库中。就像电子表格一样,如果我们可以从命令行获得这些数据,那就太好了。
关系数据库的例子有 MySQL、PostgreSQL 和 SQLite。这些数据库的接口方式稍微有些不同。有些提供命令行工具或命令行界面,有些则不提供。此外,当涉及到它们的使用和输出时,格式不是很一致。
幸运的是,有一个名为sql2csv的命令行工具专门用来做这个事,它也是 CSVkit 的一部分。它通过一个公共接口与许多不同的数据库协同工作,包括 MySQL、Oracle、PostgreSQL、SQLite、Microsoft SQL Server 和 Sybase。sql2csv的输出,顾名思义,就是 CSV 格式的。
我们可以通过对关系数据库执行SELECT查询来获取数据。(sql2csv也支持INSERT、UPDATE和DELETE查询,但这不是本章的目的。)
sql2csv需要两个参数:--db,指定数据库 URL,形式一般是:dialect+driver://username:password@host:port/database;--query,包含SELECT查询。例如,指定一个包含来自 R 的标准数据集的 SQLite 数据库,我可以从表mtcars中选择所有行,并按mpg列对它们进行排序,如下所示:
$ sql2csv --db 'sqlite:///r-datasets.db' \
> --query 'SELECT row_names AS car, mpg FROM mtcars ORDER BY mpg' | csvlook
│ car │ mpg │
├─────────────────────┼──────┤
│ Cadillac Fleetwood │ 10.4 │
│ Lincoln Continental │ 10.4 │
│ Camaro Z28 │ 13.3 │
│ Duster 360 │ 14.3 │
│ Chrysler Imperial │ 14.7 │
│ Maserati Bora │ 15.0 │
│ Merc 450SLC │ 15.2 │
│ AMC Javelin │ 15.2 │
… with 24 more lines
这个例子中 SQLite 数据库是一个本地文件,所以在这里我不需要指定任何用户名、密码或主机。如果你想查询你雇主的数据库,你当然需要知道如何访问它,并且你需要得到权限。
3.7 调用 Web API
在上一节中,我解释了如何从互联网上下载文件。从互联网上拿数据的另一种方式是通过 Web API,它代表应用编程接口,API 数量正在以越来越快的速度增长,这对我们数据科学家来说意味着大量有趣的数据。
Web API 并不意味着要以漂亮的布局呈现,比如网站。相反,大多数 Web API 以结构化格式返回数据,比如 JSON 或 XML。以结构化的形式保存数据的好处是数据可以很容易地被其他工具处理,比如jq。例如,例子中的 API 包含大量关于 George R.R. Martin 虚构的世界的信息,而《权力的游戏》一书和电视剧就发生在这个虚构世界中,它以下面的 JSON 结构返回数据:
$ curl -s "https://anapioficeandfire.com/api/characters/583" | jq '.'
{
"url": "https://anapioficeandfire.com/api/characters/583",
"name": "Jon Snow",
"gender": "Male",
"culture": "Northmen",
"born": "In 283 AC",
"died": "", # ➊
"titles": [
"Lord Commander of the Night's Watch"
],
"aliases": [
"Lord Snow",
"Ned Stark's Bastard",
"The Snow of Winterfell",
"The Crow-Come-Over",
"The 998th Lord Commander of the Night's Watch",
"The Bastard of Winterfell",
"The Black Bastard of the Wall",
"Lord Crow"
],
"father": "",
"mother": "",
"spouse": "",
"allegiances": [
"https://anapioficeandfire.com/api/houses/362"
],
"books": [
"https://anapioficeandfire.com/api/books/5"
],
"povBooks": [
"https://anapioficeandfire.com/api/books/1",
"https://anapioficeandfire.com/api/books/2",
"https://anapioficeandfire.com/api/books/3",
"https://anapioficeandfire.com/api/books/8"
],
"tvSeries": [
"Season 1",
"Season 2",
"Season 3",
"Season 4",
"Season 5",
"Season 6"
],
"playedBy": [
"Kit Harington"
]
}
➊ 剧透:这个数据并不完全是最新的。
数据通过管道传输到命令行工具jq,这只是为了以一种漂亮的方式显示它。jq有更多清理和探索的可能性,我们将在第五章和第七章中探索。
3.7.1 认证
一些 Web API 要求你在请求它们的输出之前进行身份验证(即证明你的身份)。有几种方法可以做到这一点。一些 Web API 使用 API 密匙,而另一些使用 OAuth 协议。在这里,News API,一个独立的标题和新闻文章来源,就是一个很好的例子。让我们看看当你试图在没有 API 键的情况下访问这个 API 时会发生什么:
$ curl -s "http://newsapi.org/v2/everything?q=linux" | jq .
{
"status": "error", "code": "apiKeyMissing",
"message": "Your API key is missing. Append this to the URL with the apiKey pa
ram, or use the x-api-key HTTP header."
}
嗯,那是意料之中的。顺便说一下,问号后面的部分是我们传递查询参数的地方,这也是你需要指定 API 密匙的地方。但是我想对自己的 API 密匙保密,所以我通过读取文件的方式将信息插入进去。
$ curl -s "http://newsapi.org/v2/everything?q=linux&apiKey=$(< /data/.secret/new
sapi.org_apikey)" |
> jq '.' | trim 30
{
"status": "ok",
"totalResults": 9178,
"articles": [
{
"source": {
"id": "the-verge",
"name": "The Verge"
},
"author": "Sean Hollister",
"title": "Bungie rejects Steam Deck, threatens to ban Destiny 2 players t…
"description": "Not only is Bungie apparently not making Destiny 2 compat…
"url": "https://www.theverge.com/22957294/bungie-destiny-2-steam-deck-gam…
"urlToImage": "https://cdn.vox-cdn.com/thumbor/PTFqHfu8ezNz4QrLf_ugVteHNV…
"publishedAt": "2022-03-01T23:45:56Z",
"content": "Image: Bungie\r\n\n \n\n When will Bungie let Destiny 2 come …
},
{
"source": {
"id": "ars-technica",
"name": "Ars Technica"
},
"author": "Scharon Harding",
"title": "System76 Linux workstation looks ready for gaming, too",
"description": "A 144 Hz screen, colorful keyboard, and Ethernet can appe…
"url": "https://arstechnica.com/gadgets/2022/02/system76-linux-laptop-uni…
"urlToImage": "https://cdn.arstechnica.net/wp-content/uploads/2022/02/lis…
"publishedAt": "2022-02-03T16:49:47Z",
"content": "13 with 13 posters participating\r\nSystem76's latest Linux l…
},
… with 236 more lines
你可以在 News API 的网站获得自己的 API 密匙。
3.7.2 流式 API
一些 Web API 以流的方式返回数据。这意味着一旦你连接到它,数据将继续涌入,直到连接被关闭。一个众所周知的例子是 Twitter “fire hose”,它不断地向世界各地发送所有的推文。幸运的是,大多数命令行工具也以流方式运行。
例如,让我们来看一个 10 秒钟的 Wikimedia 流媒体 API 示例:
$ curl -s "https://stream.wikimedia.org/v2/stream/recentchange" |
> sample -s 10 > wikimedia-stream-sample
这个特定的 API 返回对 Wikipedia 和 Wikimedia 的其他属性所做的所有更改。命令行工具sample用于在 10 秒后关闭连接,我们也可以通过按下Ctrl-C发送中断来手动关闭连接。输出被保存到文件wikimedia-stream-sample,让我们用trim来一窥究竟:
$ < wikimedia-stream-sample trim
:ok
event: message
id: [{"topic":"eqiad.mediawiki.recentchange","partition":0,"timestamp":16101133…
data: {"$schema":"/mediawiki/recentchange/1.0.0","meta":{"uri":"https://en.wiki…
event: message
id: [{"topic":"eqiad.mediawiki.recentchange","partition":0,"timestamp":16101133…
data: {"$schema":"/mediawiki/recentchange/1.0.0","meta":{"uri":"https://www.wik…
… with 1078 more lines
通过sed和jq,我们就可以清理这些数据,从而看到英文版维基百科发生的变化:
$ < wikimedia-stream-sample sed -n 's/^data: //p' | # ➊
> jq 'select(.type == "edit" and .server_name == "en.wikipedia.org") | .title' # ➋
"Odion Ighalo"
"Hold Up (song)"
"Talk:Royal Bermuda Yacht Club"
"Jenna Ushkowitz"
"List of films released by Yash Raj Films"
"SP.A"
"Odette (musician)"
"Talk:Pierre Avoi"
"User:Curlymanjaro/sandbox3"
"List of countries by electrification rate"
"Grieg (crater)"
"Gorman, Edmonton"
"Khabza"
"QAnon"
"Khaw Boon Wan"
"Draft:Oggy and the Cockroaches (1975 TV series)"
"Renzo Reggiardo"
"Greer, Arizona"
"National Curriculum for England"
"Mod DB"
"Jordanian Pro League"
"List of foreign Serie A players"
➊ 这个sed表达式表示只打印以data:开头的行,打印分号后的部分,恰好是 JSON。
➋ 这个jq表达式打印具有某个type和server_name的 JSON 对象的title键。
说到流媒体,你知道你可以使用telnet免费播放《星球大战:第四集——新的希望》吗?
$ telnet towel.blinkenlights.nl
过了一会儿,我们看到 Han Solo 先开枪了!
-=== `"',
I'll bet you ""o o O O|)
have! _\ -/_ _\o/ _
|| || |* /|\ / |\
\\ || *** //| | |\\
\\o=*********** // | | | ||
|\(#'***\\ -==# | | | ||
|====|* ') '\ |====| /#
|/|| | | || | "
( )( ) | || |
|-||-| | || |
| || | | || |
________________[_][__\________________/__)(_)_____________________
当然,这可能不是一个好的数据来源,但在训练你的机器学习模型的同时欣赏一部老经典也没什么错。
3.8 总结
恭喜你,你已经完成了 OSEMN 模型的第一步。你已经学习了各种获取数据的方法,从下载到查询关系数据库。在下一章,也是中间章节,我将教你如何创建你自己的命令行工具。如果你迫不及待地想了解数据清理,可以跳过这一步,直接进入第五章(OSEMN 模型的第二步)。
3.9 进一步探索
- 寻找一个数据集来练习?GitHub 知识库 Awesome Public Datasets 列出了数百个公开可用的高质量数据集
- 或者你更愿意用 API 来练习?GitHub 库 Public API 列出了很多免费 API。City Bikes 和 The One API 是我的最爱
- 编写 SQL 查询从关系数据库中获取数据是一项重要的技能。Ben Forta 的《SQL in 10 Minutes a Day》一书的前 15 课讲了 SELECT 语句及其过滤、分组和排序功能
四、创建命令行工具
原文:https://datascienceatthecommandline.com/2e/chapter-4-creating-command-line-tools.html
在整本书中,我将向您介绍许多基本上适合一行的命令和管道。这些被称为一行程序或管道。能够只用一行程序执行复杂的任务是命令行的强大之处。这是一种与编写和使用传统程序截然不同的体验。
有些任务你只执行一次,有些任务你执行得更频繁。有些任务非常具体,有些则可以概括。如果您需要定期重复某个命令行程序,那么将它变成自己的命令行工具是值得的。因此,一行程序和命令行工具都有它们的用途。识别机会需要练习和技巧。命令行工具的优点是您不必记住整个一行程序,并且如果您将它包含到其他管道中,它会提高可读性。在这个意义上,你可以把命令行工具想象成类似于编程语言中的一个函数。
然而,使用编程语言的好处是代码在一个或多个文件中。这意味着您可以轻松地编辑和重用这些代码。如果代码有参数,它甚至可以被一般化,并重新应用于遵循类似模式的问题。
命令行工具具有两个世界的优点:它们可以从命令行使用,接受参数,并且只需创建一次。在这一章中,你将熟悉用两种方式创建命令行工具。首先,我解释了如何将这些一行程序转换成可重用的命令行工具。通过在命令中添加参数,您可以增加编程语言提供的灵活性。随后,我将演示如何从用编程语言编写的代码中创建可重用的命令行工具。遵循 Unix 的理念,您的代码可以与其他命令行工具结合使用,这些工具可能是用完全不同的语言编写的。在这一章中,我将重点介绍三种编程语言:Bash、Python 和 R。
我相信,从长远来看,创建可重用的命令行工具会使您成为一名更高效的数据科学家。您将逐步构建自己的数据科学工具箱,从中可以提取现有工具,并将其应用于您之前遇到的问题。它需要实践来识别将一行程序或现有代码转化为命令行工具的机会。
为了将命令行变为 Shell 脚本, 我们会用一点 Shell 脚本语言. 这本书仅仅会展示一些较少的 Shell 变成概念, 包括变量, 判断和循环。完整的 Shell 变成教程应有一本专门的书来讲述它, 所以超出了这本书的范围. 如果你想更深入的了解 Shell 编程, 我推荐 Arnold Robbins 和 Nelson H. F. Beebe 写的《Shell 编程经典》这本书。
4.1 概述
在本章中,您将学习如何:
- 将一行程序转换成参数化的 Shell 脚本
- 将现有的 Python 和 R 代码转换成可重用的命令行工具
本章从以下文件开始:
$ cd /data/ch04
$ l
total 32K
-rwxr-xr-x 1 dst dst 400 Mar 3 10:42 fizzbuzz.py*
-rwxr-xr-x 1 dst dst 391 Mar 3 10:42 fizzbuzz.R*
-rwxr-xr-x 1 dst dst 182 Mar 3 10:42 stream.py*
-rwxr-xr-x 1 dst dst 147 Mar 3 10:42 stream.R*
-rwxr-xr-x 1 dst dst 105 Mar 3 10:42 top-words-4.sh*
-rwxr-xr-x 1 dst dst 128 Mar 3 10:42 top-words-5.sh*
-rwxr-xr-x 1 dst dst 647 Mar 3 10:42 top-words.py*
-rwxr-xr-x 1 dst dst 584 Mar 3 10:42 top-words.R*
获取这些文件的说明在第二章中。任何其他文件都是使用命令行工具下载或生成的。
4.2 将一行程序转换成 Shell 脚本
在这一节中,我将解释如何把一行程序变成一个可重用的命令行工具。比方说,您想获得一段文本中使用频率最高的单词。以刘易斯·卡罗尔的《爱丽丝漫游仙境》为例,这本书和许多其他伟大的书籍一样,可以在古腾堡计划上免费获得。
$ curl -sL "https://www.gutenberg.org/files/11/11-0.txt" | trim
The Project Gutenberg eBook of Alice’s Adventures in Wonderland, by Lewis …
This eBook is for the use of anyone anywhere in the United States and
most other parts of the world at no cost and with almost no restrictions
whatsoever. You may copy it, give it away or re-use it under the terms
of the Project Gutenberg License included with this eBook or online at
www.gutenberg.org. If you are not located in the United States, you
will have to check the laws of the country where you are located before
using this eBook.
… with 3751 more lines
以下顺序的工具或管道应该可以完成这项工作:
$ curl -sL "https://www.gutenberg.org/files/11/11-0.txt" | # ➊
> tr '[:upper:]' '[:lower:]' | # ➋
> grep -oE "[a-z\']{2,}" | # ➌
> sort | # ➍
> uniq -c | # ➎
> sort -nr | # ➏
> head -n 10 # ➐
1839 the
942 and
811 to
638 of
610 it
553 she
486 you
462 said
435 in
403 alice
➊ 使用curl下载电子书。
➋ 使用tr将整个文本转换成小写。
➌ 使用grep提取所有单词,并将每个单词放在单独的行上。
➍ 用sort将这些单词按字母顺序排序。
➎ 去掉所有重复的,用uniq统计每个单词在列表中出现的频率。
➏ 使用sort按计数降序排列这个独特单词列表。
使用head只保留前 10 行(即单词)。
这些词确实在文章中出现得最多。因为这些单词(除了单词alice)在许多英语文本中出现得非常频繁,所以它们没有什么意义。事实上,这些被称为停用词。如果我们去掉这些,我们会保留与这篇文章相关的最常用的词。
以下是我找到的停用词列表:
$ curl -sL "https://raw.githubusercontent.com/stopwords-iso/stopwords-en/master/
stopwords-en.txt" |
> sort | tee stopwords | trim 20
10
39
a
able
ableabout
about
above
abroad
abst
accordance
according
accordingly
across
act
actually
ad
added
adj
adopted
ae
… with 1278 more lines
使用grep,我们可以在开始计数之前过滤掉停用词:
$ curl -sL "https://www.gutenberg.org/files/11/11-0.txt" |
> tr '[:upper:]' '[:lower:]' |
> grep -oE "[a-z\']{2,}" |
> sort |
> grep -Fvwf stopwords | # ➊
> uniq -c |
> sort -nr |
> head -n 10
403 alice
98 gutenberg
88 project
76 queen
71 time
63 king
60 turtle
57 mock
56 hatter
55 gryphon
➊ 从一个文件中获取模式(在我们的例子中是停用词),每行一个,用-f。用-F将那些模式解释为固定字符串。只选择那些包含与-w构成完整单词的匹配的行。用-v选择不匹配的行。
每一个命令行都提供了一个帮助说明. 所以如果你想知道更多, 比如说, grep, 你可以运行man grep命令. 命令tr, grep, uniq, 和sort会在下章中讨论更加详细的用法。
只运行一次这个一行程序没有任何问题。然而,想象一下,如果你想拥有古腾堡计划中每本电子书的前 10 个单词。或者想象一下,你想要一个新闻网站每小时的前 10 个单词。在这种情况下,最好将这个一行程序作为一个单独的构建块,可以成为更大的东西的一部分。为了在参数方面给这个一行程序增加一些灵活性,让我们把它变成一个 Shell 脚本。
这允许我们以一行程序为起点,并逐步对其进行改进。为了将这个一行程序变成一个可重用的命令行工具,我将带您完成以下六个步骤:
- 将一行程序复制并粘贴到一个文件中。
- 添加执行权限。
- 定义一个所谓的 Shebang。
- 移除固定输入部分。
- 添加一个参数。
- 选择性地扩展您的路径。
4.2.1 第一步:创建文件
第一步是创建一个新文件。您可以打开您最喜欢的文本编辑器,复制并粘贴这个一行程序。让我们将这个文件命名为top-words-1.sh,以表明这是我们新的命令行工具的第一步。如果您喜欢呆在命令行,您可以使用内置的fc,它代表“修复命令”,并允许您修复或编辑上次运行的命令。
$ fc
运行fc调用默认的文本编辑器,它存储在环境变量编辑器中。在 Docker 容器中,这被设置为nano,一个简单的文本编辑器。如您所见,该文件包含我们的一行程序:
GNU nano 5.4 /tmp/zshxzOKMw curl -sL "https://www.gutenberg.org/files/11/11-0.txt" |
tr '[:upper:]' '[:lower:]' |
grep -oE "[a-z\']{2,}" |
sort |
grep -Fvwf stopwords |
uniq -c |
sort -nr |
head -n 10
[ Read 8 lines ]
^G Help ^O Write Out ^W Where Is ^K Cut ^T Execute ^C Location
^X Exit ^R Read File ^\ Replace ^U Paste ^J Justify ^_ Go To Line
让我们通过按下Ctrl-O,删除临时文件名,并键入top-words-1.sh来给这个临时文件一个合适的名称:
GNU nano 5.4 /tmp/zshxzOKMw curl -sL "https://www.gutenberg.org/files/11/11-0.txt" |
tr '[:upper:]' '[:lower:]' |
grep -oE "[a-z\']{2,}" |
sort |
grep -Fvwf stopwords |
uniq -c |
sort -nr |
head -n 10
File Name to Write: top-words-1.sh
^G Help M-D DOS Format M-A Append M-B Backup File
^C Cancel M-M Mac Format M-P Prepend ^T Browse
按下Enter :
GNU nano 5.4 /tmp/zshxzOKMw curl -sL "https://www.gutenberg.org/files/11/11-0.txt" |
tr '[:upper:]' '[:lower:]' |
grep -oE "[a-z\']{2,}" |
sort |
grep -Fvwf stopwords |
uniq -c |
sort -nr |
head -n 10
Save file under DIFFERENT NAME?
Y Yes
N No ^C Cancel
按下Y确认您要以不同的文件名保存:
GNU nano 5.4 top-words-1.sh curl -sL "https://www.gutenberg.org/files/11/11-0.txt" |
tr '[:upper:]' '[:lower:]' |
grep -oE "[a-z\']{2,}" |
sort |
grep -Fvwf stopwords |
uniq -c |
sort -nr |
head -n 10
[ Wrote 8 lines ]
^G Help ^O Write Out ^W Where Is ^K Cut ^T Execute ^C Location
^X Exit ^R Read File ^\ Replace ^U Paste ^J Justify ^_ Go To Line
按下Ctrl-X退出nano,回到你来的地方。
我们正在使用文件扩展名.sh说明我们正在创建一个 Shell 脚本。然而,命令行工具不需要有扩展。事实上,命令行工具很少有扩展。
确认文件的内容:
$ pwd
/data/ch04
$ l
total 44K
-rwxr-xr-x 1 dst dst 400 Mar 3 10:42 fizzbuzz.py*
-rwxr-xr-x 1 dst dst 391 Mar 3 10:42 fizzbuzz.R*
-rw-r--r-- 1 dst dst 7.5K Mar 3 10:42 stopwords
-rwxr-xr-x 1 dst dst 182 Mar 3 10:42 stream.py*
-rwxr-xr-x 1 dst dst 147 Mar 3 10:42 stream.R*
-rw-r--r-- 1 dst dst 173 Mar 3 10:42 top-words-1.sh
-rwxr-xr-x 1 dst dst 105 Mar 3 10:42 top-words-4.sh*
-rwxr-xr-x 1 dst dst 128 Mar 3 10:42 top-words-5.sh*
-rwxr-xr-x 1 dst dst 647 Mar 3 10:42 top-words.py*
-rwxr-xr-x 1 dst dst 584 Mar 3 10:42 top-words.R*
$ bat top-words-1.sh ───────┬────────────────────────────────────────────────────────────────────────
│ File: top-words-1.sh
───────┼────────────────────────────────────────────────────────────────────────
1 │ curl -sL "https://www.gutenberg.org/files/11/11-0.txt" |
2 │ tr '[:upper:]' '[:lower:]' |
3 │ grep -oE "[a-z\']{2,}" |
4 │ sort |
5 │ grep -Fvwf stopwords |
6 │ uniq -c |
7 │ sort -nr |
8 │ head -n 10
───────┴────────────────────────────────────────────────────────────────────────
你现在可以使用bash来解释和执行文件中的命令:
$ bash top-words-1.sh
403 alice
98 gutenberg
88 project
76 queen
71 time
63 king
60 turtle
57 mock
56 hatter
55 gryphon
这可以避免您下次再次输入一行程序。
然而,因为该文件不能独立执行,所以它还不是一个真正的命令行工具。让我们在下一步中改变这一点。
4.2.2 第二步:给予执行权限
我们不能直接执行文件的原因是我们没有正确的访问权限。特别是,作为用户,您需要拥有执行该文件的权限。在本节中,我们将更改文件的访问权限。
为了比较步骤之间的差异,使用cp -v top-words-{1,2}.sh将文件复制到top-words-2.sh。
如果你想验证括号扩展或者其他形式的文件扩展会导致什么, 用echo代替命令把结果打印出来. 比如, echo book_{draft,final}.md or echo agent-{001..007}.
要更改文件的访问权限,我们需要使用一个名为chmod的命令行工具,代表更改模式。它改变特定文件的文件模式位。以下命令授予用户你执行top-words-2.sh的权限:
$ cp -v top-words-{1,2}.sh
'top-words-1.sh' -> 'top-words-2.sh'
$ chmod u+x top-words-2.sh
参数u+x由三个字符组成:(1)u表示我们要为拥有该文件的用户,也就是您,更改权限,因为您创建了该文件;(2)+表明我们要添加一个权限;以及(3)x,其指示执行的权限。
现在让我们来看看这两个文件的访问权限:
$ l top-words-{1,2}.sh
-rw-r--r-- 1 dst dst 173 Mar 3 10:42 top-words-1.sh
-rwxr--r-- 1 dst dst 173 Mar 3 10:42 top-words-2.sh*
第一列显示每个文件的访问权限。对于top-words-2.sh,这里是-rwxrw-r--。第一个字符- (连字符)表示文件类型。一个-表示常规文件,一个d表示目录。接下来的三个字符,rwx,表示拥有该文件的用户的访问权限。r和w分别表示读和写 。(你可以看到,top-words-1.sh有一个-而不是一个x,这意味着我们不能执行那个文件。)接下来的三个字符rw-表示拥有该文件的组的所有成员的访问权限。最后,列中的最后三个字符,r--,表示所有其他用户的访问权限。
现在,您可以执行该文件,如下所示:
$ ./top-words-2.sh
403 alice
98 gutenberg
88 project
76 queen
71 time
63 king
60 turtle
57 mock
56 hatter
55 gryphon
如果您试图执行一个您没有正确访问权限的文件,如top-words-1.sh,您将看到以下错误消息:
$ ./top-words-1.sh
zsh: permission denied: ./top-words-1.sh
4.2.3 第三步:定义 Shebang
虽然我们已经可以单独执行文件,但是我们应该在文件中添加一个所谓的 Shebang。 Shebang 是脚本中的一个特殊行,它指示系统应该使用哪个可执行文件来解释命令。
Shebang 这个名字来源于前两个字:一个井号(She)和一个感叹号(Bang):#!。就像我们在上一步中所做的那样,忽略它并不是一个好主意,因为每个 Shell 都有不同的默认可执行文件。如果没有定义 Shebang,我们在整本书中使用的 ZShell 默认使用可执行文件/bin/sh。在这种情况下,我希望bash将命令解释为比sh给我们更多的功能。
同样,你可以随意使用任何你喜欢的编辑器,但我将坚持使用nano,它安装在 Docker 映像中。
$ cp -v top-words-{2,3}.sh
'top-words-2.sh' -> 'top-words-3.sh'
$ nano top-words-3.sh
GNU nano 5.4 top-words-3.sh curl -sL "https://www.gutenberg.org/files/11/11-0.txt" |
tr '[:upper:]' '[:lower:]' |
grep -oE "[a-z\']{2,}" |
sort |
grep -Fvwf stopwords |
uniq -c |
sort -nr |
head -n 10
[ Read 8 lines ]
^G Help ^O Write Out ^W Where Is ^K Cut ^T Execute ^C Location
^X Exit ^R Read File ^\ Replace ^U Paste ^J Justify ^_ Go To Line
继续输入#!/usr/bin/env/bash,然后按Enter。准备好后,按Ctrl-X保存并退出。
GNU nano 5.4 top-words-3.sh * #!/usr/bin/env bash curl -sL "https://www.gutenberg.org/files/11/11-0.txt" |
tr '[:upper:]' '[:lower:]' |
grep -oE "[a-z\']{2,}" |
sort |
grep -Fvwf stopwords |
uniq -c |
sort -nr |
head -n 10
Save modified buffer?
Y Yes
N No ^C Cancel
按下Y以表示您想要保存文件。
GNU nano 5.4 top-words-3.sh * #!/usr/bin/env bash curl -sL "https://www.gutenberg.org/files/11/11-0.txt" |
tr '[:upper:]' '[:lower:]' |
grep -oE "[a-z\']{2,}" |
sort |
grep -Fvwf stopwords |
uniq -c |
sort -nr |
head -n 10
File Name to Write: top-words-3.sh
^G Help M-D DOS Format M-A Append M-B Backup File
^C Cancel M-M Mac Format M-P Prepend ^T Browse
让我们确认一下top-words-3.sh是什么样子的:
$ bat top-words-3.sh ───────┬────────────────────────────────────────────────────────────────────────
│ File: top-words-3.sh
───────┼────────────────────────────────────────────────────────────────────────
1 │ #!/usr/bin/env bash
2 │ curl -sL "https://www.gutenberg.org/files/11/11-0.txt" |
3 │ tr '[:upper:]' '[:lower:]' |
4 │ grep -oE "[a-z\']{2,}" |
5 │ sort |
6 │ grep -Fvwf stopwords |
7 │ uniq -c |
8 │ sort -nr |
9 │ head -n 10
───────┴────────────────────────────────────────────────────────────────────────
这正是我们所需要的:我们的原始管道,前面有一个 Shebang。
有时,您会遇到以!/usr/bin/bash或!/usr/bin/python形式出现的脚本(对于 Python,我们将在下一节中看到)。虽然这通常是可行的,但是如果将bash或python可执行文件安装在与/usr/bin不同的位置,那么该脚本将不再有效。最好使用我这里呈现的形式,即!/usr/bin/env bash和!/usr/bin/env python,因为env可执行文件知道bash和python安装在哪里。简而言之,使用env使您的脚本更具可移植性。
4.2.4 第四步:移除固定输入
我们知道有一个有效的命令行工具,我们可以从命令行执行。但是我们可以做得更好。我们可以使我们的命令行工具更加可重用。我们文件中的第一个命令是curl,它下载我们希望从中获得前 10 个最常用单词的文本。所以,数据和操作合二为一。
如果我们想从另一本电子书或任何其他文本中获得 10 个最常用的单词,会怎么样呢?输入数据在工具本身中是固定的。最好将数据从命令行工具中分离出来。
如果我们假设命令行工具的用户将提供文本,那么该工具将变得普遍适用。因此,解决方案是从脚本中删除curl命令。下面是名为top-words-4.sh的更新脚本:
$ cp -v top-words-{3,4}.sh
'top-words-3.sh' -> 'top-words-4.sh'
$ sed -i '2d' top-words-4.sh
$ bat top-words-4.sh ───────┬────────────────────────────────────────────────────────────────────────
│ File: top-words-4.sh
───────┼────────────────────────────────────────────────────────────────────────
1 │ #!/usr/bin/env bash
2 │ tr '[:upper:]' '[:lower:]' |
3 │ grep -oE "[a-z\']{2,}" |
4 │ sort |
5 │ grep -Fvwf stopwords |
6 │ uniq -c |
7 │ sort -nr |
8 │ head -n 10
───────┴────────────────────────────────────────────────────────────────────────
这是因为如果一个脚本从一个需要来自标准输入的数据的命令开始,比如tr,它将接受提供给命令行工具的输入。例如:
$ curl -sL 'https://www.gutenberg.org/files/11/11-0.txt' | ./top-words-4.sh
403 alice
98 gutenberg
88 project
76 queen
71 time
63 king
60 turtle
57 mock
56 hatter
55 gryphon
$ curl -sL 'https://www.gutenberg.org/files/12/12-0.txt' | ./top-words-4.sh
469 alice
189 queen
98 gutenberg
88 project
72 time
71 red
70 white
67 king
63 head
59 knight
$ man bash | ./top-words-4.sh
585 command
332 set
313 word
313 option
304 file
300 variable
298 bash
258 list
257 expansion
238 history
虽然我们没有在脚本里这样做, 但是我们仍然应该保存数据. 通常, 让用户使用输出重定向比在脚本里写明输出到哪个文件好. 当然, 如果你打算只在里自己的项目里使用命令行工具, 那么是否清楚的写明文件路径就没有什么限制了.
4.2.5 第五步:添加参数
为了使我们的命令行工具更加可重用,还有一个步骤:参数。在我们的命令行工具中,有许多固定的命令行参数,例如用-nr代表sort,用-n 10代表head。最好保持前一个论点不变。然而,允许head命令有不同的值是非常有用的。这将允许最终用户设置输出最常用的单词的数量。下面显示了我们的文件top-words-5.sh的样子:
$ bat top-words-5.sh ───────┬────────────────────────────────────────────────────────────────────────
│ File: top-words-5.sh
───────┼────────────────────────────────────────────────────────────────────────
1 │ #!/usr/bin/env bash
2 │
3 │ NUM_WORDS="${1:-10}"
4 │
5 │ tr '[:upper:]' '[:lower:]' |
6 │ grep -oE "[a-z\']{2,}" |
7 │ sort |
8 │ grep -Fvwf stopwords |
9 │ uniq -c |
10 │ sort -nr |
11 │ head -n "${NUM_WORDS}"
───────┴────────────────────────────────────────────────────────────────────────
- 变量
NUM_WORDS被设置为$1的值,这是 Bash 中的一个特殊变量。它保存传递给我们的命令行工具的第一个命令行参数的值。下表列出了 Bash 提供的其他特殊变量。如果没有指定值,它将采用值10 - 注意,为了使用
$NUM_WORDS变量的值,您需要在它前面放一个美元符号。当你设置它的时候,你并没有写一个美元符号。
我们也可以直接使用$1作为head的参数,而不必费心创建一个额外的变量,比如NUM_WORDS。然而,有了更大的脚本和更多的命令行参数,如$2和$3,当您使用命名变量时,您的代码变得更具可读性。
现在,如果您想查看我们文本中最常用的 20 个单词,我们将调用我们的命令行工具,如下所示:
$ curl -sL "https://www.gutenberg.org/files/11/11-0.txt" > alice.txt
$ < alice.txt ./top-words-5.sh 20
403 alice
98 gutenberg
88 project
76 queen
71 time
63 king
60 turtle
57 mock
56 hatter
55 gryphon
53 rabbit
50 head
48 voice
45 looked
44 mouse
42 duchess
40 tone
40 dormouse
37 cat
34 march
如果用户没有指定数字,那么我们的脚本将显示前 10 个最常用的单词:
$ < alice.txt ./top-words-5.sh
403 alice
98 gutenberg
88 project
76 queen
71 time
63 king
60 turtle
57 mock
56 hatter
55 gryphon
4.2.6 第六步:拓展你的人生道路
经过前面的五个步骤,我们终于完成了构建一个可重用的命令行工具。然而,还有一个非常有用的步骤。在这个可选步骤中,我们将确保您可以从任何地方执行您的命令行工具。
目前,当您想要执行您的命令行工具时,您要么必须导航到它所在的目录,要么包括完整的路径名,如步骤 2 所示。如果命令行工具是专门为某个项目而构建的,这是没问题的。然而,如果你的命令行工具可以应用于多种情况,那么从任何地方执行它都是有用的,就像 Ubuntu 自带的命令行工具一样。
为了实现这一点,Bash 需要知道在哪里可以找到您的命令行工具。它通过遍历存储在名为PATH的环境变量中的目录列表来实现这一点。在一个新的 Docker 容器中,PATH如下所示:
$ echo $PATH
/usr/local/lib/R/site-library/rush/exec:/usr/bin/dsutils:/home/dst/.local/bin:/u
sr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
目录由冒号分隔。我们可以通过将冒号转换为换行符,将其打印为目录列表:
$ echo $PATH | tr ':' '\n'
/usr/local/lib/R/site-library/rush/exec
/usr/bin/dsutils
/home/dst/.local/bin
/usr/local/sbin
/usr/local/bin
/usr/sbin
/usr/bin
/sbin
/bin
要永久更改PATH,您需要编辑位于您的主目录中的.bashrc或.bash_profile。如果您将所有自定义命令行工具放在一个目录中,比如说,~/tools,那么您只需更改一次PATH。现在,您不再需要添加./,但也可以只用文件名。此外,您不再需要记住命令行工具的位置。
$ cp -v top-words{-5.sh,}
'top-words-5.sh' -> 'top-words'
$ export PATH="${PATH}:/data/ch04"
$ echo $PATH
/usr/local/lib/R/site-library/rush/exec:/usr/bin/dsutils:/home/dst/.local/bin:/u
sr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/data/ch04
$ curl "https://www.gutenberg.org/files/11/11-0.txt" |
> top-words 10
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 170k 100 170k 0 0 145k 0 0:00:01 0:00:01 --:--:-- 145k
403 alice
98 gutenberg
88 project
76 queen
71 time
63 king
60 turtle
57 mock
56 hatter
55 gryphon
4.3 用 Python 和 R 创建命令行工具
我们在上一节创建的命令行工具是用 Bash 编写的。(当然,并不是 Bash 编程语言的所有特性都被采用了,但是解释器仍然是bash。)正如您现在所知道的,命令行是语言不可知的,所以我们不一定要使用 Bash 来创建命令行工具。
在这一节中,我将演示命令行工具也可以用其他编程语言创建。我将重点介绍 Python 和 R,因为这是数据科学社区中最流行的两种编程语言。我无法提供这两种语言的完整介绍,所以我假设您对 Python 或 R 有一定的了解。其他编程语言,如 Java、Go 和 Julia,在创建命令行工具时也遵循类似的模式。
用不同于 Bash 的另一种编程语言创建命令行工具有三个主要原因。首先,您可能已经有了一些希望能够从命令行使用的代码。其次,命令行工具最终会包含一百多行 Bash 代码。第三,命令行工具需要更加安全和健壮(Bash 缺少许多特性,比如类型检查)。
我在上一节中讨论的六个步骤也大致适用于用其他编程语言创建命令行工具。然而,第一步不是从命令行复制粘贴,而是将相关代码复制粘贴到一个新文件中。用 Python 和 R 写的命令行工具需要分别指定python和Rscript,作为 Shebang 之后的解释器。
当使用 Python 和 R 创建命令行工具时,还有两个方面需要特别注意。首先,处理标准输入(这是 Shell 脚本的天性)必须在 Python 和 R 中明确处理。其次,由于用 Python 和 R 编写的命令行工具往往更复杂,我们可能还希望为用户提供指定更复杂的命令行参数的能力。
4.3.1 移植 Shell 脚本
首先,让我们看看如何将刚刚创建的 Shell 脚本移植到 Python 和 R 中。换句话说,哪些 Python 和 R 代码为我们提供了标准输入中最常用的单词?我们将首先显示两个文件top-words.py和top-words.R然后讨论与 Shell 代码的区别。在 Python 中,代码如下所示:
$ cd /data/ch04
$ bat top-words.py
───────┬────────────────────────────────────────────────────────────────────────
│ File: top-words.py
───────┼────────────────────────────────────────────────────────────────────────
1 │ #!/usr/bin/env python
2 │ import re
3 │ import sys
4 │
5 │ from collections import Counter
6 │ from urllib.request import urlopen
7 │
8 │ def top_words(text, n):
9 │ with urlopen("https://raw.githubusercontent.com/stopwords-iso/stopw
│ ords-en/master/stopwords-en.txt") as f:
10 │ stopwords = f.read().decode("utf-8").split("\n")
11 │
12 │ words = re.findall("[a-z']{2,}", text.lower())
13 │ words = (w for w in words if w not in stopwords)
14 │
15 │ for word, count in Counter(words).most_common(n):
16 │ print(f"{count:>7} {word}")
17 │
18 │
19 │ if __name__ == "__main__":
20 │ text = sys.stdin.read()
21 │
22 │ try:
23 │ n = int(sys.argv[1])
24 │ except:
25 │ n = 10
26 │
27 │ top_words(text, n)
───────┴────────────────────────────────────────────────────────────────────────
注意,这个 Python 例子没有使用任何第三方包。如果你想做高级文本处理,那么我推荐你去看看 NLTK 包 。如果你要处理大量的数字数据,那么我推荐你使用 Pandas 包 。
在 R 语言中,代码看起来像这样:
$ bat top-words.R
───────┬────────────────────────────────────────────────────────────────────────
│ File: top-words.R
───────┼────────────────────────────────────────────────────────────────────────
1 │ #!/usr/bin/env Rscript
2 │ n <- as.integer(commandArgs(trailingOnly = TRUE))
3 │ if (length(n) == 0) n <- 10
4 │
5 │ f_stopwords <- url("https://raw.githubusercontent.com/stopwords-iso/sto
│ pwords-en/master/stopwords-en.txt")
6 │ stopwords <- readLines(f_stopwords, warn = FALSE)
7 │ close(f_stopwords)
8 │
9 │ f_text <- file("stdin")
10 │ lines <- tolower(readLines(f_text))
11 │
12 │ words <- unlist(regmatches(lines, gregexpr("[a-z']{2,}", lines)))
13 │ words <- words[is.na(match(words, stopwords))]
14 │
15 │ counts <- sort(table(words), decreasing = TRUE)
16 │ cat(sprintf("%7d %s\n", counts[1:n], names(counts[1:n])), sep = "")
17 │ close(f_text)
───────┴────────────────────────────────────────────────────────────────────────
让我们检查所有三个实现(即 Bash、Python 和 R)是否返回了相同计数的前 5 个单词:
$ time < alice.txt top-words 5
403 alice
98 gutenberg
88 project
76 queen
71 time
top-words 5 < alice.txt 0.56s user 0.04s system 139% cpu 0.427 total
$ time < alice.txt top-words.py 5
403 alice
98 gutenberg
88 project
76 queen
71 time
top-words.py 5 < alice.txt 2.15s user 0.03s system 97% cpu 2.232 total
$ time < alice.txt top-words.R 5
403 alice
98 gutenberg
88 project
76 queen
71 time
top-words.R 5 < alice.txt 1.67s user 0.10s system 94% cpu 1.886 total
精彩!当然,输出本身并不令人兴奋。令人兴奋的是,我们可以用多种语言完成同样的任务。让我们看看这两种方法之间的区别。
首先,显而易见的是代码量的差异。对于这个特定的任务,Python 和 R 都比 Bash 需要更多的代码。这说明,对于某些任务,使用命令行更好。对于其他任务,您最好使用编程语言。随着您在命令行上获得更多的经验,您将开始认识到何时使用哪种方法。当一切都是命令行工具时,您甚至可以将任务拆分成子任务,并将 Bash 命令行工具与 Python 命令行工具结合使用。无论哪种方法最适合手头的任务。
4.3.2 处理来自标准输入的流数据
在前面的两个代码片段中,Python 和 R 都一次性读取了完整的标准输入。在命令行上,大多数工具以流的方式将数据传输到下一个命令行工具。有一些命令行工具在将数据写入标准输出之前需要完整的数据,比如sort。这意味着管道被这样的命令行工具阻塞了。当输入数据是有限的,比如一个文件时,这并不是一个问题。但是,当输入数据是一个不间断的流时,这样的阻塞命令行工具是没有用的。
幸运的是 Python 和 R 支持处理流数据。例如,您可以逐行应用函数。下面是两个最小的例子,分别演示了这在 Python 和 R 中是如何工作的。
Python 和 R 工具都解决了现在已经臭名昭著的 Fizz Buzz 问题,该问题定义如下:打印从 1 到 100 的数字,除非该数字能被 3 整除,否则打印Fizz;如果数字能被 5 整除,则改为打印buzz;如果这个数字能被 15 整除,就打印fizzbuzz。下面是 Python 代码 :
$ bat fizzbuzz.py
───────┬────────────────────────────────────────────────────────────────────────
│ File: fizzbuzz.py
───────┼────────────────────────────────────────────────────────────────────────
1 │ #!/usr/bin/env python
2 │ import sys
3 │
4 │ CYCLE_OF_15 = ["fizzbuzz", None, None, "fizz", None,
5 │ "buzz", "fizz", None, None, "fizz",
6 │ "buzz", None, "fizz", None, None]
7 │
8 │ def fizz_buzz(n: int) -> str:
9 │ return CYCLE_OF_15[n % 15] or str(n)
10 │
11 │ if __name__ == "__main__":
12 │ try:
13 │ while (n:= sys.stdin.readline()):
14 │ print(fizz_buzz(int(n)))
15 │ except:
16 │ pass
───────┴────────────────────────────────────────────────────────────────────────
这是 R 代码:
$ bat fizzbuzz.R
───────┬────────────────────────────────────────────────────────────────────────
│ File: fizzbuzz.R
───────┼────────────────────────────────────────────────────────────────────────
1 │ #!/usr/bin/env Rscript
2 │ cycle_of_15 <- c("fizzbuzz", NA, NA, "fizz", NA,
3 │ "buzz", "fizz", NA, NA, "fizz",
4 │ "buzz", NA, "fizz", NA, NA)
5 │
6 │ fizz_buzz <- function(n) {
7 │ word <- cycle_of_15[as.integer(n) %% 15 + 1]
8 │ ifelse(is.na(word), n, word)
9 │ }
10 │
11 │ f <- file("stdin")
12 │ open(f)
13 │ while(length(n <- readLines(f, n = 1)) > 0) {
14 │ write(fizz_buzz(n), stdout())
15 │ }
16 │ close(f)
───────┴────────────────────────────────────────────────────────────────────────
让我们测试这两个工具(为了节省空间,我将输出传送到column):
$ seq 30 | fizzbuzz.py | column -x
1 2 fizz 4 buzz
fizz 7 8 fizz buzz
11 fizz 13 14 fizzbuzz
16 17 fizz 19 buzz
fizz 22 23 fizz buzz
26 fizz 28 29 fizzbuzz
$ seq 30 | fizzbuzz.R | column -x
1 2 fizz 4 buzz
fizz 7 8 fizz buzz
11 fizz 13 14 fizzbuzz
16 17 fizz 19 buzz
fizz 22 23 fizz buzz
26 fizz 28 29 fizzbuzz
这个输出在我看来是正确的!很难证明这两个工具实际上以流的方式工作。在将输入数据传输到 Python 或 R 工具之前,您可以通过将输入数据传输到sample -d 100来验证这一点。这样,您将在每一行之间添加一个小的延迟,以便更容易确认工具不会等待所有的输入数据,而是逐行操作。
4.4 总结
在 intermezzo 这一章中,我向您展示了如何构建自己的命令行工具。只需要六个步骤就可以将您的代码变成可重用的构建块。你会发现这会让你更有效率。我建议你留意创造自己工具的机会。下一章将介绍 OSEMN 数据科学模型的第二步,即清理数据。
4.5 进一步探索
- 当工具需要记住许多选项时,向工具中添加帮助文档就变得非常重要,尤其是当您希望与他人共享您的工具时。是一个语言无关的框架,提供帮助并定义您的工具可以接受的可能选项。几乎任何编程语言都有可用的实现,包括 Bash、Python 和 R。
- 如果你想学习更多关于 Bash 编程的知识,我推荐 Arnold Robbins 和 Nelson Beebe 的经典 Shell 编程和 Carl Albing 和 JP Vossen 的 Bash 食谱。
- 编写一个健壮且安全的 Bash 脚本相当棘手。ShellCheck 是一个在线工具,可以检查你的 Bash 代码中的错误和漏洞。还有一个命令行工具可用。
- Joel Grus 的《Fizz Buzz 的十篇文章》一书是一个很有见地和有趣的收藏,收集了用 Python 解决 Fizz Buzz 的十种不同方法。
五、清理数据
原文:https://datascienceatthecommandline.com/2e/chapter-5-scrubbing-data.html
两章前,在 OSEMN 数据科学模型的第一步,我们看到了从各种来源获取数据。这一章讲的都是第二步:清理数据。你看,你很少能立即继续探索甚至建模数据。您的数据首先需要清理或清理的原因有很多。
首先,数据可能不是期望的格式。例如,您可能已经从一个 API 获得了一些 JSON 数据,但是您需要以 CSV 格式创建可视化。其他常见的格式包括纯文本、HTML 和 XML。大多数命令行工具只能处理一种或两种格式,因此将数据从一种格式转换成另一种格式非常重要。
一旦数据采用了所需的格式,仍然可能会出现丢失值、不一致、奇怪的字符或不必要的部分等问题。您可以通过应用过滤器、替换值以及合并多个文件来解决这些问题。命令行特别适合这类转换,因为有许多专门的工具可用,其中大多数可以处理大量数据。在本章中,我将讨论经典工具,如grep和awk,以及更新的工具,如jq和pup。
有时,您可以使用同一个命令行工具来执行多个操作,或者使用多个工具来执行同一个操作。本章的结构更像一本食谱,重点是问题或食谱,而不是深入探究命令行工具本身。
5.1 概述
在本章中,您将学习如何:
- 将数据从一种格式转换成另一种格式
- 将 SQL 查询直接应用于 CSV
- 过滤一行
- 提取和替换值
- 拆分、合并和提取列
- 合并多个文件
本章从以下文件开始:
$ cd /data/ch05
$ l
total 200K
-rw-r--r-- 1 dst dst 164K Mar 3 10:43 alice.txt
-rw-r--r-- 1 dst dst 4.5K Mar 3 10:43 iris.csv
-rw-r--r-- 1 dst dst 179 Mar 3 10:43 irismeta.csv
-rw-r--r-- 1 dst dst 160 Mar 3 10:43 names-comma.csv
-rw-r--r-- 1 dst dst 129 Mar 3 10:43 names.csv
-rw-r--r-- 1 dst dst 7.8K Mar 3 10:43 tips.csv
-rw-r--r-- 1 dst dst 5.1K Mar 3 10:43 users.json
获取这些文件的说明在第二章中。任何其他文件都是使用命令行工具下载或生成的。
在我深入实际的转换之前,我想演示一下在命令行工作时它们的普遍性。
5.2 变换,变换无处不在
在第一章中,我提到过,在实践中,OSEMN 模型的步骤很少是线性的。在这种情况下,虽然清理是 OSEMN 模型的第二步,但我希望您知道,需要清理的不仅仅是获得的数据。您将在本章中学习的转换在您的管道的任何部分以及 OSEMN 模型的任何步骤中都是有用的。一般来说,如果一个命令行工具生成的输出可以被下一个工具立即使用,您可以使用管道操作符(|)将这两个工具链接在一起。否则,首先需要通过在管道中插入一个中间工具来对数据进行转换。
让我通过一个例子让你更具体。假设您已经获得了一个fizzbuzz序列的前 100 个条目(参见第四章,并且您想要使用条形图来可视化词语fizz、buzz和fizzbuzz出现的频率。如果这个例子使用了您可能还不熟悉的工具,请不要担心,稍后会更详细地介绍它们。
首先,通过生成序列获得数据,并将其写入fb.seq:
$ seq 100 |
> /data/ch04/fizzbuzz.py | # ➊
> tee fb.seq | trim
1
2
fizz
4
buzz
fizz
7
8
fizz
buzz
… with 90 more lines
➊ 自定义工具fizzbuzz.py来自第四章。
然后你使用grep来保存匹配模式fizz或buzz的行,并使用sort和uniq来计算每个单词出现的频率:
$ grep -E "fizz|buzz" fb.seq | # ➊
> sort | uniq -c | sort -nr > fb.cnt # ➋
$ bat -A fb.cnt
───────┬────────────────────────────────────────────────────────────────────────
│ File: fb.cnt
───────┼────────────────────────────────────────────────────────────────────────
1 │ ·····27·fizz␊
2 │ ·····14·buzz␊
3 │ ······6·fizzbuzz␊
───────┴────────────────────────────────────────────────────────────────────────
➊ 这个正则表达式也匹配fizzbuzz。
➋ 使用sort和uniq这种方式是一种常见的行计数和降序排序方式。是-c选项增加了计数。
请注意,sort使用了两次:第一次是因为uniq假设其输入数据被排序,第二次是对计数进行数字排序。在某种程度上,这是一个中间的转变,尽管很微妙。
下一步是使用rush来可视化计数。然而,由于rush期望输入数据是 CSV 格式的,这首先需要一个不太微妙的转换。awk可以添加一个标题,翻转两个字段,并在一个咒语中插入逗号:
$ < fb.cnt awk 'BEGIN { print "value,count" } { print $2","$1 }' > fb.csv
$ bat fb.csv
───────┬────────────────────────────────────────────────────────────────────────
│ File: fb.csv
───────┼────────────────────────────────────────────────────────────────────────
1 │ value,count
2 │ fizz,27
3 │ buzz,14
4 │ fizzbuzz,6
───────┴────────────────────────────────────────────────────────────────────────
$ csvlook fb.csv
│ value │ count │
├──────────┼───────┤
│ fizz │ 27 │
│ buzz │ 14 │
│ fizzbuzz │ 6 │
现在您已经准备好使用rush来创建一个条形图。结果见图 5.1。(我会在第七章中详细讲述rush的这个语法。)
$ rush plot -x value -y count --geom col --height 2 fb.csv > fb.png
$ display fb.png

图 5.1:计数嘶嘶声、嗡嗡声和嘶嘶声
虽然这个例子有点做作,但它揭示了在命令行工作时的一种常见模式。关键工具,例如获取数据、创建可视化或训练模型的工具,通常需要中间转换才能链接到管道中。从这个意义上说,编写管道就像解决一个难题,其中的关键部分通常需要辅助部分来配合。
现在您已经看到了清理数据的重要性,您已经准备好了解一些实际的转换。
5.3 纯文本
从形式上来说,纯文本是指一系列人类可读的字符,也可以是一些特定类型的控制字符,如制表符和换行符 。例如日志、电子书、电子邮件和源代码。纯文本比二进制数据有很多好处,包括:
- 可以使用任何文本编辑器打开、编辑和保存它
- 它是自描述的,并且独立于创建它的应用
- 它将比其他形式的数据寿命长,因为不需要额外的知识或应用来处理它
但最重要的是,Unix 哲学认为纯文本是命令行工具之间的通用接口。也就是说,大多数工具接受纯文本作为输入,生成纯文本作为输出。
这足以让我从纯文本开始。我在本章中讨论的其他格式,CSV、JSON、XML 和 HTML 也确实是纯文本。目前,我假设纯文本没有清晰的表格结构(像 CSV 那样)或嵌套结构(像 JSON、XML 和 HTML 那样)。在本章的后面,我将介绍一些专门用于处理这些格式的工具。
5.3.1 过滤一行
第一个清理操作是过滤行。这意味着从输入数据中,将评估每一行是被保留还是被丢弃。
5.3.1.1 基于位置
过滤一行的最直接方法是基于它们的位置。当您想要检查某个文件的前 10 行时,或者当您从另一个命令行工具的输出中提取特定行时,这可能会很有用。为了说明如何基于位置进行过滤,让我们创建一个包含 10 行的虚拟文件:
$ seq -f "Line %g" 10 | tee lines
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10
您可以使用head、sed或awk打印前 3 行:
$ < lines head -n 3
Line 1
Line 2
Line 3
$ < lines sed -n '1,3p'
Line 1
Line 2
Line 3
$ < lines awk 'NR <= 3' # ➊
Line 1
Line 2
Line 3
awk、NR中的 ➊ 是指到目前为止看到的输入记录的总数。
同样,您可以使用tail打印最后 3 行:
$ < lines tail -n 3
Line 8
Line 9
Line 10
你也可以使用sed和awk来实现,但是tail要快得多。删除前 3 行如下所示:
$ < lines tail -n +4
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10
$ < lines sed '1,3d' Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10
$ < lines sed -n '1,3!p'
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10
注意,用tail你必须指定行数加 1。把它想象成你要开始打印的那一行。使用head可以删除最后 3 行:
$ < lines head -n -3
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
您可以使用sed、awk或head和tail的组合来打印特定的行。这里我打印了第 4、5 和 6 行:
$ < lines sed -n '4,6p'
Line 4
Line 5
Line 6
$ < lines awk '(NR>=4) && (NR<=6)'
Line 4
Line 5
Line 6
$ < lines head -n 6 | tail -n 3
Line 4
Line 5
Line 6
您可以通过指定起点和步长,用sed打印奇数行,或者通过使用模运算符,用awk打印奇数行:
$ < lines sed -n '1~2p'
Line 1
Line 3
Line 5
Line 7
Line 9
$ < lines awk 'NR%2' Line 1
Line 3
Line 5
Line 7
Line 9
打印偶数行的工作方式类似:
$ < lines sed -n '0~2p'
Line 2
Line 4
Line 6
Line 8
Line 10
$ < lines awk '(NR+1)%2'
Line 2
Line 4
Line 6
Line 8
Line 10
这些许多例子以小于(<)号开头,后面跟着文件的名称. 我这样做的原因是这样可以让我从左到右的读取命令行. 这只是我个人的习惯. 你也可以使用cat去传递文件的内容. 同样, 许多命令行工具也接受文件的名称作为一个参数.
5.3.1.2 基于模式
有时,您希望根据行的内容保留或丢弃行。使用用于过滤行的规范命令行工具grep,您可以打印匹配特定模式或正则表达式的每一行。例如,从《爱丽丝漫游仙境》中提取所有章节标题:
$ < alice.txt grep -i chapter # ➊
CHAPTER I. Down the Rabbit-Hole
CHAPTER II. The Pool of Tears
CHAPTER III. A Caucus-Race and a Long Tale
CHAPTER IV. The Rabbit Sends in a Little Bill
CHAPTER V. Advice from a Caterpillar
CHAPTER VI. Pig and Pepper
CHAPTER VII. A Mad Tea-Party
CHAPTER VIII. The Queen's Croquet-Ground
CHAPTER IX. The Mock Turtle's Story
CHAPTER X. The Lobster Quadrille
CHAPTER XI. Who Stole the Tarts?
CHAPTER XII. Alice's Evidence
➊ -i选项指定匹配应该不区分大小写。
您也可以指定正则表达式。例如,如果您只想打印以The开头的标题:
$ < alice.txt grep -E '^CHAPTER (.*)\. The'
CHAPTER II. The Pool of Tears
CHAPTER IV. The Rabbit Sends in a Little Bill
CHAPTER VIII. The Queen's Croquet-Ground
CHAPTER IX. The Mock Turtle's Story
CHAPTER X. The Lobster Quadrille
注意,为了启用正则表达式,您必须指定-E选项。否则,grep将模式解释为一个字符串,这很可能导致根本没有匹配:
$ < alice.txt grep '^CHAPTER (.*)\. The'
使用-v选项,您可以反转匹配,这样grep会打印出The不匹配的行。下面的正则表达式只匹配包含空格的行。所以用逆运算,并使用wc -l,你可以计算非空行的数量:
$ < alice.txt grep -Ev '^\s$' | wc -l
2790
5.3.1.3 基于随机性
当您在制定数据管道的过程中,并且拥有大量数据时,调试管道可能会很麻烦。在这种情况下,从数据中生成较小的样本可能会有用。这就是sample派上用场的地方。sample的主要目的是通过逐行输出输入的特定百分比来获得数据的子集。
$ seq -f "Line %g" 1000 | sample -r 1%
Line 5
Line 137
Line 323
Line 385
Line 391
Line 554
Line 580
Line 613
Line 803
Line 841
Line 899
在这里,每个输入行都有百分之一的机会被打印。这个百分比也可以指定为分数(即1/100)或概率(即0.01)。
有另外两个用途,在调试管道时会很有用。首先,可以给输出增加一些延迟。当输入是一个持续的流(例如,我们在第三章中看到的维基百科流),并且数据进来得太快以至于看不到发生了什么时,这就很方便了。其次,可以在sample上放一个定时器,这样就不用手动杀死正在进行的进程了。例如,要在打印的每一行之间添加 1 秒钟的延迟,并且只运行 5 秒钟,您可以键入:
$ seq -f "Line %g" 1000 | sample -r 1% -d 1000 -s 5 | ts # ➊
Mar 03 10:43:52 Line 38
Mar 03 10:43:53 Line 117
Mar 03 10:43:54 Line 455
Mar 03 10:43:55 Line 569
Mar 03 10:43:56 Line 613
Mar 03 10:43:57 Line 895
➊ 工具ts在每一行前面添加一个时间戳。
为了防止不必要的计算,请尽量将sample放在您的管道中。事实上,这个观点适用于任何减少数据的命令行工具,比如head和tail。一旦你确信你的管道工作正常,你就把它从管道中取出来。
5.3.2 提取数值
为了从前面的例子中提取实际的章节标题,您可以采用一种简单的方法,将grep的输出通过管道传输到cut:
$ grep -i chapter alice.txt | cut -d ' ' -f 3-
Down the Rabbit-Hole
The Pool of Tears
A Caucus-Race and a Long Tale
The Rabbit Sends in a Little Bill
Advice from a Caterpillar
Pig and Pepper
A Mad Tea-Party
The Queen's Croquet-Ground
The Mock Turtle's Story
The Lobster Quadrille
Who Stole the Tarts?
Alice's Evidence
在这里,传递给cut的每一行都被分隔成多个字段,然后打印第三个字段到最后一个字段。每个输入行的字段总数可以不同。有了sed,你可以用更复杂的方式完成同样的任务:
$ sed -rn 's/^CHAPTER ([IVXLCDM]{1,})\. (.*)$/\2/p' alice.txt | trim 3
Down the Rabbit-Hole
The Pool of Tears
A Caucus-Race and a Long Tale
… with 9 more lines
(因为输出是相同的,所以它被调整为三行。)这种方法使用正则表达式和反向引用。这里,sed也接管了grep所做的工作。我只建议在简单的方法不起作用时使用复杂的方法。例如,如果CHAPTER曾经是文本本身的一部分,而不仅仅是用来表示一个新章节的开始。当然,有许多复杂的层次可以解决这个问题,但这是为了说明一个非常严格的方法。在实践中,面临的挑战是提出一个在复杂性和灵活性之间取得良好平衡的管道。
值得注意的是cut也可以在人物位置上分割。当您希望在每个输入行提取(或删除)相同的字符集时,这很有用:
$ grep -i chapter alice.txt | cut -c 9-
I. Down the Rabbit-Hole
II. The Pool of Tears
III. A Caucus-Race and a Long Tale
IV. The Rabbit Sends in a Little Bill
V. Advice from a Caterpillar
VI. Pig and Pepper
VII. A Mad Tea-Party
VIII. The Queen's Croquet-Ground
IX. The Mock Turtle's Story
X. The Lobster Quadrille
XI. Who Stole the Tarts?
XII. Alice's Evidence
grep有一个很棒的特性,它使用-o选项将每场比赛输出到单独的一行上:
$ < alice.txt grep -oE '\w{2,}' | trim
Project
Gutenberg
Alice
Adventures
in
Wonderland
by
Lewis
Carroll
This
… with 28615 more lines
但是,如果你想创建一个包含所有以a开头,以e结尾的单词的数据集呢?当然,这也有一个管道:
$ < alice.txt tr '[:upper:]' '[:lower:]' | # ➊
> grep -oE '\w{2,}' |
> grep -E '^a.*e$' |
> sort | uniq | sort -nr | trim
available
ate
assistance
askance
arise
argue
are
archive
applicable
apple
… with 25 more lines
➊ 这里我用tr使文本小写。我们将在下一节更仔细地看看tr。
两个grep命令可能被合并成一个,但是在这种情况下,我认为重用和修改之前的管道会更容易。为了完成工作而务实并不可耻!
5.3.3 替换和删除数值
您可以使用命令行工具tr(代表翻译)来替换或删除个别字符。例如,空格可以替换为下划线,如下所示:
$ echo 'hello world!' | tr ' ' '_'
hello_world!
如果需要替换多个字符,您可以组合使用:
$ echo 'hello world!' | tr ' !' '_?'
hello_world?
tr也可以通过指定参数-d来删除单个字符:
$ echo 'hello world!' | tr -d ' !'
helloworld
$ echo 'hello world!' | tr -d -c '[a-z]' helloworld%
在这种情况下,这两个命令完成相同的事情。然而,第二个命令使用了两个额外的特性:它使用方括号和破折号([-])指定了字符的范围 (全部是小写字母),并且-c选项指示应该使用它的补码。换句话说,这个命令只保留小写字母。你甚至可以使用tr将文本转换成大写:
$ echo 'hello world!' | tr '[a-z]' '[A-Z]'
HELLO WORLD!
$ echo 'hello world!' | tr '[:lower:]' '[:upper:]'
HELLO WORLD!
但是,如果需要翻译非 ASCII 字符,那么tr可能无法工作,因为它只对单字节字符进行操作。在这些情况下,您应该使用sed来代替:
$ echo 'hello world!' | tr '[a-z]' '[A-Z]'
HELLO WORLD!
$ echo 'hallo wêreld!' | tr '[a-z]' '[A-Z]'
HALLO WêRELD!
$ echo 'hallo wêreld!' | tr '[:lower:]' '[:upper:]'
HALLO WêRELD!
$ echo 'hallo wêreld!' | sed 's/[[:lower:]]*/\U&/g'
HALLO WÊRELD!
$ echo 'helló világ' | tr '[:lower:]' '[:upper:]'
HELLó VILáG
$ echo 'helló világ' | sed 's/[[:lower:]]*/\U&/g'
HELLÓ VILÁG
如果您需要对多个字符进行操作,那么您可能会发现sed非常有用。你已经看到了一个从alice.txt中提取章节标题的例子。在sed中,提取、删除和替换实际上都是相同的操作。你只需要指定不同的正则表达式。例如,要更改一个单词,删除重复的空格,并删除前导空格:
$ echo ' hello world!' |
> sed -re 's/hello/bye/' | # ➊
> sed -re 's/\s+/ /g' | # ➋
> sed -re 's/\s+//' # ➌
bye world!
➊ 把hello换成bye。
➋ 用一个空格替换任何空格。标志g代表全局,意味着同一替换可以在同一行上应用多次。
➌ 这只删除了前导空格,因为我没有在这里指定标志g。
同样,正如前面的grep示例一样,这三个sed命令可以合并成一个:
$ echo ' hello world!' |
> sed -re 's/hello/bye/;s/\s+/ /g;s/\s+//'
bye world!
但是告诉我,你觉得什么更容易读?
5.4 CSV
5.4.1 正文、标题和列,天哪!
我用来清理纯文本的命令行工具,比如tr和grep,并不总是适用于 CSV。原因是这些命令行工具没有标题、主体和列的概念。如果您想使用grep过滤行,但总是在输出中包含标题,该怎么办?或者,如果您只想使用tr大写特定列的值,而不改变其他列的值,该怎么办?
有多步骤的解决方法,但是非常麻烦。我有更好的东西。为了利用 CSV 的普通命令行工具,我将向您介绍三个命令行工具,它们被恰当地命名为:body,header,以及cols。
让我们从第一个命令行工具body开始。使用body,您可以将任何命令行工具应用于 CSV 文件的主体,即除了文件头之外的所有内容。例如:
$ echo -e "value\n7\n2\n5\n3" | body sort -n
value
2
3
5
7
它假设 CSV 文件的文件头只跨越一行。它是这样工作的:
- 从标准输入中取出一行,并将其存储为名为
$header的变量。 - 打印出标题。
- 对标准输入中的剩余数据执行传递给
body的所有命令行参数。
这是另一个例子。假设您想要计算以下 CSV 文件的行数:
$ seq 5 | header -a count
count
1
2
3
4
5
使用wc -l,您可以计算所有行的数量:
$ seq 5 | header -a count | wc -l
6
如果您只想考虑正文中的行(所以除了标题之外的所有内容),您可以添加body:
$ seq 5 | header -a count | body wc -l
count
5
请注意,在输出中没有使用标题,也再次打印了标题。
第二个命令行工具header允许您操作 CSV 文件的文件头。如果没有提供参数,将打印 CSV 文件的文件头:
$ < tips.csv header
bill,tip,sex,smoker,day,time,size
这个和head -n 1一样。如果标题跨越多行,这是不推荐的,您可以指定-n 2。您也可以向 CSV 文件添加标题:
$ seq 5 | header -a count
count
1
2
3
4
5
这相当于echo "count" | cat - <(seq 5)。使用-d选项删除标题:
$ < iris.csv header -d | trim
5.1,3.5,1.4,0.2,Iris-setosa
4.9,3.0,1.4,0.2,Iris-setosa
4.7,3.2,1.3,0.2,Iris-setosa
4.6,3.1,1.5,0.2,Iris-setosa
5.0,3.6,1.4,0.2,Iris-setosa
5.4,3.9,1.7,0.4,Iris-setosa
4.6,3.4,1.4,0.3,Iris-setosa
5.0,3.4,1.5,0.2,Iris-setosa
4.4,2.9,1.4,0.2,Iris-setosa
4.9,3.1,1.5,0.1,Iris-setosa
… with 140 more lines
这个和tail -n +2差不多,但是更容易记一点。替换一个头,如果你看上面的源代码,基本上就是先删除一个头,然后再添加一个头,这是通过指定-r选项来完成的。这里,我们结合body:
$ seq 5 | header -a line | body wc -l | header -r count
count
5
最后但并非最不重要的一点是,您可以只对标题应用命令,类似于body命令行工具对正文所做的。例如:
$ seq 5 | header -a line | header -e "tr '[a-z]' '[A-Z]'"
LINE
1
2
3
4
5
第三个命令行工具叫做cols,它允许您将某个命令只应用于列的一个子集。例如,如果您想要大写tips数据集中的day列中的值(不影响其他列和标题),您可以将cols与body结合使用,如下所示:
$ < tips.csv cols -c day body "tr '[a-z]' '[A-Z]'" | head -n 5 | csvlook
│ day │ bill │ tip │ sex │ smoker │ time │ size │
├────────────┼───────┼──────┼────────┼────────┼────────┼──────┤
│ 0001-01-07 │ 16.99 │ 1.01 │ Female │ False │ Dinner │ 2 │
│ 0001-01-07 │ 10.34 │ 1.66 │ Male │ False │ Dinner │ 3 │
│ 0001-01-07 │ 21.01 │ 3.50 │ Male │ False │ Dinner │ 3 │
│ 0001-01-07 │ 23.68 │ 3.31 │ Male │ False │ Dinner │ 2 │
请注意,将多个命令行工具和参数作为命令传递给header -e、body和cols会导致复杂的引用。如果您遇到这样的问题,最好为此创建一个单独的命令行工具,并将其作为命令传递。
总之,虽然通常最好使用专门为 CSV 数据制作的命令行工具,但是如果需要的话,body、header和cols也允许您将经典的命令行工具应用于 CSV 文件。
5.4.2 对 CSV 执行 SQL 查询
如果本章提到的命令行工具不能提供足够的灵活性,那么还有另一种方法可以从命令行清除数据。工具csvsql可以让你直接对 CSV 文件执行 SQL 查询。SQL 是定义清理数据操作的强大语言;这是一种与使用单独的命令行工具非常不同的方式。
如果你的数据来自于关系数据库, 并且, 如果可以, 尝试在那个数据库上执行 SQL 查询并且接着将数据导出成 csv 格式. 像我在第三章讨论的那样, 你可以用命令行sql2csv来做这件事.当你第一次从数据库导出 CSV 文件, 并且接着执行 SQL 的时候, 它不仅仅会慢, 还有可能列的数据类型不能从 CSV 文件中推断出来.
在下面的清理任务中,我将包括几个涉及csvsql的解决方案。一个基本命令是这样的:
$ seq 5 | header -a val | csvsql --query "SELECT SUM(val) AS sum FROM stdin"
sum
15.0
如果将标准输入传递给csvsql,那么这个表就被命名为stdin。列的类型是从数据中自动推断出来的。正如您将在后面看到的,在合并 CSV 文件部分,您还可以指定多个 CSV 文件。请记住csvsql使用了 SQL 的 SQLite 方言,这与 SQL 标准有一些细微的差别。虽然 SQL 通常比其他解决方案更冗长,但它也更灵活。如果您已经知道如何用 SQL 解决清理问题,那么为什么不在命令行中使用它呢?
5.4.3 提取和重新排序列
可以使用命令行工具对列进行提取和重新排序:csvcut。例如,为了只保留鸢尾花数据集中包含数值的列,对中间的两列进行重新排序:
$ < iris.csv csvcut -c sepal_length,petal_length,sepal_width,petal_width | csvlo
ok
│ sepal_length │ petal_length │ sepal_width │ petal_width │
├──────────────┼──────────────┼─────────────┼─────────────┤
│ 5.1 │ 1.4 │ 3.5 │ 0.2 │
│ 4.9 │ 1.4 │ 3.0 │ 0.2 │
│ 4.7 │ 1.3 │ 3.2 │ 0.2 │
│ 4.6 │ 1.5 │ 3.1 │ 0.2 │
│ 5.0 │ 1.4 │ 3.6 │ 0.2 │
│ 5.4 │ 1.7 │ 3.9 │ 0.4 │
│ 4.6 │ 1.4 │ 3.4 │ 0.3 │
│ 5.0 │ 1.5 │ 3.4 │ 0.2 │
… with 142 more lines
或者,您也可以用-C选项指定您想要省略的列,它代表补充:
$ < iris.csv csvcut -C species | csvlook │ sepal_length │ sepal_width │ petal_length │ petal_width │
├──────────────┼─────────────┼──────────────┼─────────────┤
│ 5.1 │ 3.5 │ 1.4 │ 0.2 │
│ 4.9 │ 3.0 │ 1.4 │ 0.2 │
│ 4.7 │ 3.2 │ 1.3 │ 0.2 │
│ 4.6 │ 3.1 │ 1.5 │ 0.2 │
│ 5.0 │ 3.6 │ 1.4 │ 0.2 │
│ 5.4 │ 3.9 │ 1.7 │ 0.4 │
│ 4.6 │ 3.4 │ 1.4 │ 0.3 │
│ 5.0 │ 3.4 │ 1.5 │ 0.2 │
… with 142 more lines
这里,包含的列保持相同的顺序。除了列名,您还可以指定列的索引,从 1 开始。例如,这允许您只选择奇数列(如果您需要的话!):
$ echo 'a,b,c,d,e,f,g,h,i\n1,2,3,4,5,6,7,8,9' |
> csvcut -c $(seq 1 2 9 | paste -sd,)
a,c,e,g,i
1,3,5,7,9
如果您确定任何值中都没有逗号,那么您也可以使用cut来提取列。请注意cut不会对列进行重新排序,如以下命令所示:
$ echo 'a,b,c,d,e,f,g,h,i\n1,2,3,4,5,6,7,8,9' | cut -d, -f 5,1,3
a,c,e
1,3,5
如您所见,用-f选项指定列的顺序并不重要;使用cut,它们将总是以原始顺序出现。为了完整起见,我们还来看一下提取和重新排序鸢尾花数据集的数字列的 SQL 方法:
$ < iris.csv csvsql --query "SELECT sepal_length, petal_length, "\
> "sepal_width, petal_width FROM stdin" | head -n 5 | csvlook
│ sepal_length │ petal_length │ sepal_width │ petal_width │
├──────────────┼──────────────┼─────────────┼─────────────┤
│ 5.1 │ 1.4 │ 3.5 │ 0.2 │
│ 4.9 │ 1.4 │ 3.0 │ 0.2 │
│ 4.7 │ 1.3 │ 3.2 │ 0.2 │
│ 4.6 │ 1.5 │ 3.1 │ 0.2 │
5.4.4 过滤行
过滤 CSV 文件中的行与过滤纯文本文件中的行之间的区别在于,您可能只希望根据特定列中的值进行过滤。基于位置的过滤本质上是相同的,但是您必须考虑到 CSV 文件的第一行通常是文件头。请记住,如果您想保留标题,您可以随时使用body命令行工具:
$ seq 5 | sed -n '3,5p'
3
4
5
$ seq 5 | header -a count | body sed -n '3,5p'
count
3
4
5
当要对某一列中的某一模式进行过滤时,可以使用csvgrep``awk,当然,也可以使用csvsql。例如,要排除交易方规模小于 5 的所有账单:
$ csvgrep -c size -i -r "[1-4]" tips.csv bill,tip,sex,smoker,day,time,size
29.8,4.2,Female,No,Thur,Lunch,6
34.3,6.7,Male,No,Thur,Lunch,6
41.19,5.0,Male,No,Thur,Lunch,5
27.05,5.0,Female,No,Thur,Lunch,6
29.85,5.14,Female,No,Sun,Dinner,5
48.17,5.0,Male,No,Sun,Dinner,6
20.69,5.0,Male,No,Sun,Dinner,5
30.46,2.0,Male,Yes,Sun,Dinner,5
28.15,3.0,Male,Yes,Sat,Dinner,5
awk和csvsql都可以做数值比较。例如,要在周六或周日获得 40 美元以上的所有账单:
$ < tips.csv awk -F, 'NR==1 || ($1 > 40.0) && ($5 ~ /^S/)'
bill,tip,sex,smoker,day,time,size
48.27,6.73,Male,No,Sat,Dinner,4
44.3,2.5,Female,Yes,Sat,Dinner,3
48.17,5.0,Male,No,Sun,Dinner,6
50.81,10.0,Male,Yes,Sat,Dinner,3
45.35,3.5,Male,Yes,Sun,Dinner,3
40.55,3.0,Male,Yes,Sun,Dinner,2
48.33,9.0,Male,No,Sat,Dinner,4
csvsql解决方案更冗长,但也更健壮,因为它使用列名而不是它们的索引:
$ csvsql --query "SELECT * FROM tips WHERE bill > 40 AND day LIKE 'S%'" tips.csv
bill,tip,sex,smoker,day,time,size
48.27,6.73,Male,0,Sat,Dinner,4.0
44.3,2.5,Female,1,Sat,Dinner,3.0
48.17,5.0,Male,0,Sun,Dinner,6.0
50.81,10.0,Male,1,Sat,Dinner,3.0
45.35,3.5,Male,1,Sun,Dinner,3.0
40.55,3.0,Male,1,Sun,Dinner,2.0
48.33,9.0,Male,0,Sat,Dinner,4.0
请注意,SQL 查询中的WHERE子句的灵活性不容易与其他命令行工具相匹配,因为 SQL 可以对日期和集合进行操作,并形成复杂的子句组合。
5.4.5 合并列
当感兴趣的值分布在多个列中时,合并列非常有用。日期(其中年、月和日可以是单独的列)或姓名(其中名和姓是单独的列)可能会出现这种情况。让我们考虑第二种情况。
输入 CSV 是作曲家列表。想象你的任务是把名和姓组合成一个全名。我将为这个任务提供四种不同的方法:sed、awk、cols + tr和csvsql。让我们看看输入 CSV:
$ csvlook -I names.csv
│ id │ last_name │ first_name │ born │
├────┼───────────┼────────────┼──────┤
│ 1 │ Williams │ John │ 1932 │
│ 2 │ Elfman │ Danny │ 1953 │
│ 3 │ Horner │ James │ 1953 │
│ 4 │ Shore │ Howard │ 1946 │
│ 5 │ Zimmer │ Hans │ 1957 │
第一种方法sed,使用了两条语句。第一个是替换标题,第二个是将反向引用应用于第二行以后的正则表达式:
$ < names.csv sed -re '1s/.*/id,full_name,born/g;2,$s/(.*),(.*),(.*),(.*)/\1,\3
\2,\4/g' |
> csvlook -I
│ id │ full_name │ born │
├────┼───────────────┼──────┤
│ 1 │ John Williams │ 1932 │
│ 2 │ Danny Elfman │ 1953 │
│ 3 │ James Horner │ 1953 │
│ 4 │ Howard Shore │ 1946 │
│ 5 │ Hans Zimmer │ 1957 │
awk方法如下所示:
$ < names.csv awk -F, 'BEGIN{OFS=","; print "id,full_name,born"} {if(NR > 1) {pr
int $1,$3" "$2,$4}}' |
> csvlook -I
│ id │ full_name │ born │
├────┼───────────────┼──────┤
│ 1 │ John Williams │ 1932 │
│ 2 │ Danny Elfman │ 1953 │
│ 3 │ James Horner │ 1953 │
│ 4 │ Howard Shore │ 1946 │
│ 5 │ Hans Zimmer │ 1957 │
与tr相结合的cols方法:
$ < names.csv |
> cols -c first_name,last_name tr \",\" \" \" |
> header -r full_name,id,born |
> csvcut -c id,full_name,born |
> csvlook -I
│ id │ full_name │ born │
├────┼───────────────┼──────┤
│ 1 │ John Williams │ 1932 │
│ 2 │ Danny Elfman │ 1953 │
│ 3 │ James Horner │ 1953 │
│ 4 │ Howard Shore │ 1946 │
│ 5 │ Hans Zimmer │ 1957 │
请注意,csvsql使用 SQLite 作为数据库来执行查询,而||代表串联:
$ < names.csv csvsql --query "SELECT id, first_name || ' ' || last_name "\
> "AS full_name, born FROM stdin" | csvlook -I
│ id │ full_name │ born │
├─────┼───────────────┼────────┤
│ 1.0 │ John Williams │ 1932.0 │
│ 2.0 │ Danny Elfman │ 1953.0 │
│ 3.0 │ James Horner │ 1953.0 │
│ 4.0 │ Howard Shore │ 1946.0 │
│ 5.0 │ Hans Zimmer │ 1957.0 │
如果last_name包含逗号会怎样?为了清楚起见,让我们看一下原始输入 CSV:
$ cat names-comma.csv
id,last_name,first_name,born
1,Williams,John,1932
2,Elfman,Danny,1953
3,Horner,James,1953
4,Shore,Howard,1946
5,Zimmer,Hans,1957
6,"Beethoven, van",Ludwig,1770
看起来前三种方法都失败了。都是以不同的方式。只有csvsql能够组合名字和全名:
$ < names-comma.csv sed -re '1s/.*/id,full_name,born/g;2,$s/(.*),(.*),(.*),(.*)/
\1,\3 \2,\4/g' | tail -n 1
6,"Beethoven,Ludwig van",1770
$ < names-comma.csv awk -F, 'BEGIN{OFS=","; print "id,full_name,born"} {if(NR >
1) {print $1,$3" "$2,$4}}' | tail -n 1
6, van" "Beethoven,Ludwig
$ < names-comma.csv | cols -c first_name,last_name tr \",\" \" \" |
> header -r full_name,id,born | csvcut -c id,full_name,born | tail -n 1
6,"Ludwig ""Beethoven van""",1770
$ < names-comma.csv csvsql --query "SELECT id, first_name || ' ' || last_name AS
full_name, born FROM stdin" | tail -n 1
6.0,"Ludwig Beethoven, van",1770.0
$ < names-comma.csv rush run -t 'unite(df, full_name, first_name, last_name, sep
= " ")' - | tail -n 1
6,"Ludwig Beethoven, van",1770
等一下!最后一个命令是什么?那是 R 吗?嗯,事实上,是的。它是通过名为rush的命令行工具评估的 R 代码。此刻我所能说的是,这种方法也成功地合并了两列。稍后我将讨论这个漂亮的命令行工具。
5.4.6 合并多个 CSV 文件
5.4.6.1 横向连接
假设您有三个想要并排放置的 CSV 文件。我们用tee保存流水线中间csvcut的结果:
$ < tips.csv csvcut -c bill,tip | tee bills.csv | head -n 3 | csvlook
│ bill │ tip │
├───────┼──────┤
│ 16.99 │ 1.01 │
│ 10.34 │ 1.66 │
$ < tips.csv csvcut -c day,time | tee datetime.csv |
> head -n 3 | csvlook -I
│ day │ time │
├─────┼────────┤
│ Sun │ Dinner │
│ Sun │ Dinner │
$ < tips.csv csvcut -c sex,smoker,size | tee customers.csv |
> head -n 3 | csvlook
│ sex │ smoker │ size │
├────────┼────────┼──────┤
│ Female │ False │ 2 │
│ Male │ False │ 3 │
假设这些行排成一行,你可以将paste这些文件放在一起:
$ paste -d, {bills,customers,datetime}.csv | head -n 3 | csvlook -I
│ bill │ tip │ sex │ smoker │ size │ day │ time │
├───────┼──────┼────────┼────────┼──────┼─────┼────────┤
│ 16.99 │ 1.01 │ Female │ No │ 2 │ Sun │ Dinner │
│ 10.34 │ 1.66 │ Male │ No │ 3 │ Sun │ Dinner │
这里,命令行参数-d指示paste使用逗号作为分隔符。
5.4.6.2 连接
有时数据不能通过垂直或水平连接来组合。在某些情况下,尤其是在关系数据库中,数据分布在多个表(或文件)中,以尽量减少冗余。假设您想用更多关于三种鸢尾花的信息来扩展鸢尾花数据集,即 USDA 标识符。碰巧我有一个单独的 CSV 文件,包含这些标识符:
$ csvlook irismeta.csv
│ species │ wikipedia_url │ usda_id │
├─────────────────┼──────────────────────────────────────────────┼─────────┤
│ Iris-versicolor │ http://en.wikipedia.org/wiki/Iris_versicolor │ IRVE2 │
│ Iris-virginica │ http://en.wikipedia.org/wiki/Iris_virginica │ IRVI │
│ Iris-setosa │ │ IRSE │
这个数据集和鸢尾花数据集的共同点是species列。您可以使用csvjoin来连接两个数据集:
$ csvjoin -c species iris.csv irismeta.csv | csvcut -c sepal_length,sepal_width,
species,usda_id | sed -n '1p;49,54p' | csvlook
│ sepal_length │ sepal_width │ species │ usda_id │
├──────────────┼─────────────┼─────────────────┼─────────┤
│ 4.6 │ 3.2 │ Iris-setosa │ IRSE │
│ 5.3 │ 3.7 │ Iris-setosa │ IRSE │
│ 5.0 │ 3.3 │ Iris-setosa │ IRSE │
│ 7.0 │ 3.2 │ Iris-versicolor │ IRVE2 │
│ 6.4 │ 3.2 │ Iris-versicolor │ IRVE2 │
│ 6.9 │ 3.1 │ Iris-versicolor │ IRVE2 │
当然,您也可以使用 SQL 方法,使用csvsql,这通常会更长一点(但可能更灵活):
$ csvsql --query 'SELECT i.sepal_length, i.sepal_width, i.species, m.usda_id FRO
M iris i JOIN irismeta m ON (i.species = m.species)' iris.csv irismeta.csv | sed
-n '1p;49,54p' | csvlook
│ sepal_length │ sepal_width │ species │ usda_id │
├──────────────┼─────────────┼─────────────────┼─────────┤
│ 4.6 │ 3.2 │ Iris-setosa │ IRSE │
│ 5.3 │ 3.7 │ Iris-setosa │ IRSE │
│ 5.0 │ 3.3 │ Iris-setosa │ IRSE │
│ 7.0 │ 3.2 │ Iris-versicolor │ IRVE2 │
│ 6.4 │ 3.2 │ Iris-versicolor │ IRVE2 │
│ 6.9 │ 3.1 │ Iris-versicolor │ IRVE2 │
5.5 使用 XML/HTML 和 JSON
在这一节中,我将演示几个可以将数据从一种格式转换为另一种格式的命令行工具。转换数据有两个原因。
首先,数据经常需要表格形式,就像数据库表或电子表格一样,因为许多可视化和机器学习算法都依赖于它。CSV 本质上是表格形式,但是 JSON 和 HTML/XML 数据可以有深度嵌套的结构。
第二,许多命令行工具,尤其是经典的工具,如cut和grep,是对纯文本进行操作的。这是因为文本被视为命令行工具之间的通用接口。此外,其他格式更年轻。这些格式中的每一种都可以被视为纯文本,这使得我们也可以将这样的命令行工具应用于其他格式。
有时,您可以将经典工具应用于结构化数据。例如,通过将下面的 JSON 数据视为纯文本,您可以使用sed将属性gender更改为sex:
$ sed -e 's/"gender":/"sex":/g' users.json | jq | trim
{
"results": [
{
"sex": "male",
"name": {
"title": "mr",
"first": "leevi",
"last": "kivisto"
},
"location": {
… with 260 more lines
像许多其他命令行工具一样,sed不利用数据的结构。最好是使用利用数据结构的工具(比如我下面讨论的jq),或者首先将数据转换成表格格式,比如 CSV,然后应用适当的命令行工具。
我将通过一个真实的用例来演示如何将 XML/HTML 和 JSON 转换成 CSV。我将在这里使用的命令行工具有:curl、pup、、、jq和json2csv、、、。
维基百科拥有丰富的信息。这些信息中的大部分都以表格的形式排列,这些表格可以被视为数据集。例如,这个页面包含一个国家和地区的列表,以及它们的边界长度、面积和两者之间的比例。
假设您对分析这些数据感兴趣。在本节中,我将带您浏览所有必要的步骤及其相应的命令。我不会涉及每一个细节,所以可能你不会马上理解所有的事情。别担心,我相信你会明白它的要点。请记住,本节的目的是演示命令行。本节(及更多)中使用的所有工具和概念将在后续章节中解释。
您感兴趣的数据集嵌入在 HTML 中。您的目标是最终得到一个您可以使用的数据集的表示。第一步是使用curl下载 HTML:
$ curl -sL 'http://en.wikipedia.org/wiki/List_of_countries_and_territories_by_bo
rder/area_ratio' > wiki.html
HTML 被保存到一个名为wiki.html的文件中。让我们看看前 10 行是什么样子的:
$ < wiki.html trim
<!DOCTYPE html>
<html class="client-nojs" lang="en" dir="ltr">
<head>
<meta charset="UTF-8"/>
<title>List of countries and territories by border/area ratio - Wikipedia</titl…
<script>document.documentElement.className="client-js";RLCONF={"wgBreakFrames":…
"Border-related lists"],"wgPageContentLanguage":"en","wgPageContentModel":"wiki…
"wgGEAskQuestionEnabled":false,"wgGELinkRecommendationsFrontendEnabled":false};…
"ext.centralNotice.startUp","ext.centralauth.centralautologin","ext.popups","ex…
<script>(RLQ=window.RLQ||[]).push(function(){mw.loader.implement("user.options@…
… with 2578 more lines
那似乎是合乎规程的。您已经能够确定我们感兴趣的根 HTML 元素是一个带有类<table>。这允许您使用grep查看您感兴趣的部分(-A选项指定您想要在匹配行之后打印的行数):
$ grep wikitable -A 21 wiki.html
<style data-mw-deduplicate="TemplateStyles:r1049062077">.mw-parser-output table.
static-row-numbers{counter-reset:rowNumber}body.skin-minerva .mw-parser-output .
static-row-numbers2.sortable{counter-reset:rowNumber -1}body.skin-minerva .mw-pa
rser-output .static-row-numbers2.sortable.static-row-header-two{counter-reset:ro
wNumber -2}.mw-parser-output table.static-row-numbers tr::before{display:table-c
ell;padding:0 0.5em;text-align:right}.mw-parser-output table.static-row-numbers
tr::before{content:""}body:not(.skin-minerva) .mw-parser-output .static-row-numb
ers.sortable tbody tr:not(.static-row-header):not(.static-row-numbers-norank)::b
efore,body.skin-minerva .mw-parser-output .static-row-numbers tbody tr:not(:firs
t-child):not(.static-row-header):not(.static-row-numbers-norank)::before,.mw-par
ser-output table.static-row-numbers:not(.sortable) tbody tr:not(:first-child):no
t(.static-row-header):not(.static-row-numbers-norank)::before{counter-increment:
rowNumber;content:counter(rowNumber);vertical-align:inherit}.mw-parser-output .s
tatic-row-header-hash thead tr::before{content:"#"}.mw-parser-output .static-row
-header-row thead tr::before{content:"Row"}.mw-parser-output .static-row-numbers
.wikitable tr::before{background-color:#eaecf0}body:not(.skin-minerva) .mw-parse
r-output .static-row-numbers.mw-datatable:not(.wikitable) tr::before{background-
color:#eaeeff}.mw-parser-output table.static-row-numbers.wikitable tbody tr:not(
.static-row-header)::before,body:not(.skin-minerva) .mw-parser-output .static-ro
w-numbers.mw-datatable:not(.wikitable) tbody tr::before{border:1px solid #a2a9b1
}.mw-parser-output .static-row-numbers-period2 tbody tr::before{content:counter(
rowNumber)"."}.mw-parser-output .srn-white-background{background:#fff}.mw-parser
-output .static-row-numbers tr:hover{background:#eaf3ff}</style>
<table class="wikitable sortable static-row-numbers">
<tbody><tr>
<th>Country or territory</th>
<th>Total length of land borders (km)</th>
<th>Total surface area (km<sup>2</sup>)</th>
<th>Border/area ratio (km/km<sup>2</sup>)
</th></tr>
<tr>
<td>Vatican City
</td>
<td>3.2
</td>
<td>0.44
</td>
<td>7.2727273
</td></tr>
<tr>
<td>Monaco
</td>
<td>4.4
</td>
<td>2
你现在可以看到这些国家和他们的价值观。下一步是从 HTML 文件中提取必要的元素。为此,您可以使用pup:
$ < wiki.html pup 'table.wikitable tbody' | tee table.html | trim
<tbody>
<tr>
<th>
Country or territory
</th>
<th>
Total length of land borders (km)
</th>
<th>
Total surface area (km
… with 3458 more lines
传递给pup的表达式是一个 CSS 选择器。该语法通常用于样式化网页,但是您也可以使用它从 HTML 中选择某些元素。在这种情况下,您想要选择具有wikitable类的table的tbody。接下来是xml2json,它将 XML(和 HTML)转换成 JSON。
$ < table.html xml2json > table.json
$ jq . table.json | trim 20
{
"tbody": {
"tr": [
{
"th": [
{
"$t": "Country or territory"
},
{
"$t": "Total length of land borders (km)"
},
{
"$t": [
"Total surface area (km",
")"
],
"sup": {
"$t": "2"
}
},
… with 3950 more lines
我将 HTML 转换成 JSON 的原因是因为有一个非常强大的工具叫做jq来处理 JSON 数据。以下命令提取 JSON 数据的某些部分,并将其重新整形为我们可以使用的形式:
$ < table.json jq -r '.tbody.tr[1:][] | [.td[]["$t"]] | @csv' | header -a rank,c
ountry,border,surface,ratio > countries.csv
数据现在处于您可以使用的形式。从维基百科页面到 CSV 数据集需要很多步骤。然而,当你把上面所有的命令组合成一个时,你会发现它实际上非常简洁和有表现力。
$ csvlook --max-column-width 28 countries.csv
│ rank │ country │ border │ surface │ ratio │
├──────────────────────────────┼───────────┼───────────────┼─────────┼───────┤
│ Vatican City │ 3.20 │ 0.44 │ 7.273… │ │
│ Monaco │ 4.40 │ 2.00 │ 2.200… │ │
│ San Marino │ 39.00 │ 61.00 │ 0.639… │ │
│ Liechtenstein │ 76.00 │ 160.00 │ 0.465… │ │
│ Sint Maarten (Netherlands) │ 10.20 │ 34.00 │ 0.300… │ │
│ Andorra │ 120.30 │ 468.00 │ 0.257… │ │
│ Gibraltar (United Kingdom) │ 1.20 │ 6.00 │ 0.200… │ │
│ Saint Martin (France) │ 10.20 │ 54.00 │ 0.189… │ │
… with 238 more lines
XML/HTML 到 JSON 再到 CSV 的转换演示到此结束。虽然jq可以执行更多的操作,并且有专门的工具来处理 XML 数据,但是根据我的经验,尽可能快地将数据转换成 CSV 格式会更好。这样,您可以花更多的时间来精通通用的命令行工具,而不是非常特殊的工具。
5.6 总结
在这一章中,我们已经了解了数据的清理。如您所见,没有一种工具可以神奇地摆脱所有杂乱的数据;您通常需要结合多种不同的工具来获得想要的结果。请记住,经典的命令行工具如cut和sort不能解释结构化数据。幸运的是,有一些工具可以将一种数据格式(比如 JSON 和 XML)转换成另一种数据格式(比如 CSV)。在下一章,也是间奏曲章节,我将向你展示如何使用make来管理你的项目。如果你迫不及待地想在第七章开始探索和可视化你的数据,你可以跳过这一章。
5.7 进一步探索
- 我希望我能解释更多关于
awk的事情。它是如此强大的工具和编程语言。我强烈建议你花时间去学习它。两个很好的资源是多尔蒂和罗宾斯的书《sed & awk》和在线的 GNU Awk 用户指南。 - 在这一章中,我在几个地方使用了正则表达式。不幸的是,关于它们的教程超出了本书的范围。因为正则表达式可以在许多不同的工具中使用,所以我建议您了解一下它们。一本好书是 Jan Goyvaerts 和 Steven Levithan 写的正则表达式食谱。
六、项目管理与make
原文:https://datascienceatthecommandline.com/2e/chapter-6-project-management-with-make.html
我希望现在您已经开始认识到命令行是一个非常方便的数据处理环境。您可能已经注意到,由于使用了命令行,我们:
- 调用许多不同的命令。
- 在不同的目录中工作。
- 开发我们自己的命令行工具。
- 获取并生成许多(中间)文件。
由于这是一个探索性的过程,我们的工作流程往往相当混乱,这使得我们很难跟踪我们已经做了什么。重要的是,我们的步骤可以被自己或他人复制。当您继续以前的项目时,您可能已经忘记了运行了哪些命令、从哪个目录运行了哪些文件、使用了哪些参数以及运行的顺序。想象一下与合作者分享项目的挑战。
您可以通过挖掘history命令的输出来恢复一些命令,但是这当然不是一种可靠的方法。更好的方法是将命令保存到 Shell 脚本中。至少这允许你和你的合作者复制这个项目。然而,Shell 脚本也是一种次优方法,因为:
- 很难阅读和维护。
- 步骤之间的依赖关系不清楚。
- 每一步每次都要执行,这是低效的,有时也是不可取的。
这就是make真正闪耀的地方 。make是一个命令行工具,允许您:
- 根据输入和输出依赖关系形式化您的数据工作流步骤。
- 运行工作流程的特定步骤。
- 使用内联代码。
- 从外部来源存储和检索数据。
在第一版, 这章用drake代替make. Drake 在处理数据方面有很多新增的特性,本来应该是make很好的继承者. 然而, Drake 在 2016 年的时候被它的创造者放弃了,因为有很多没有解决 bug. 所以我决定使用make.
一个重要的相关主题是版本控制 ,它允许您跟踪项目的变更,将项目备份到服务器,与其他人协作,并在出现问题时检索早期版本。一个流行的做版本控制的命令行工具是git。它经常与 GitHub 结合使用,GitHub 是一种分布式版本控制的在线服务。很多开源项目,包括这本书,都托管在 GitHub 上。版本控制的主题已经超出了本书的范围,但是我强烈建议您研究一下,尤其是当您开始与他人合作的时候。在本章的最后,我推荐了一些资源来了解更多。
6.1 概述
使用make管理您的数据工作流是本章的主题。因此,您将了解:
- 用一个
Makefile定义你的工作流。 - 从输入和输出依赖关系的角度思考工作流。
- 运行任务和构建目标。
$ cd /data/ch06
$ l
total 28K
-rw-r--r-- 1 dst dst 37 Mar 3 10:45 Makefile.test
-rw-r--r-- 1 dst dst 16 Mar 3 10:45 numbers.make
-rw-r--r-- 1 dst dst 26 Mar 3 10:45 numbers-write.make
-rw-r--r-- 1 dst dst 21 Mar 3 10:45 numbers-write-var.make
-rw-r--r-- 1 dst dst 432 Mar 3 10:45 starwars.make
-rw-r--r-- 1 dst dst 263 Mar 3 10:45 tasks.make
-rw-r--r-- 1 dst dst 27 Mar 3 10:45 template.make
获取这些文件的说明在第二章中。任何其他文件都是使用命令行工具下载或生成的。
6.2 make介绍
make围绕数据及其依赖关系组织命令执行。您的数据处理步骤在一个单独的文本文件(工作流)中被正式化。每一步都有输入和输出。make自动解析它们的依赖关系,并确定需要运行哪些命令以及运行的顺序。
这意味着,如果您有一个耗时 10 分钟的 SQL 查询,那么只有在结果丢失或查询后来发生变化时,才需要执行该查询。此外,如果您想要(重新)运行一个特定的步骤,make只会重新运行该步骤所依赖的步骤。这可以节省你很多时间。
拥有一个正式的工作流程可以让你在几个星期后轻松地拿起你的项目并与其他人合作。我强烈建议您这样做,即使您认为这将是一次性项目,因为您永远不知道何时需要再次运行某些步骤,或者在另一个项目中重用它们。
6.3 运行任务
默认情况下,make在当前目录中搜索名为Makefile的配置文件。它也可以被命名为makefile(小写),但是我建议将您的文件命名为Makefile,因为它更常见,而且这样它会出现在目录列表的顶部。通常每个项目只有一个配置文件。因为这一章讨论了许多不同的文件,所以我没有使用扩展名给它们分别命名。让我们从下面的Makefile开始:
$ bat -A numbers.make
───────┬────────────────────────────────────────────────────────────────────────
│ File: numbers.make
───────┼────────────────────────────────────────────────────────────────────────
1 │ numbers:␊
2 │ ├──────┤seq·7␊
───────┴────────────────────────────────────────────────────────────────────────
这个Makefile包含一个目标叫做numbers。一个目标就像一个任务。它通常是您想要创建的文件的名称,但也可以比它更通用。下面这条线,seq 7,被称为规则 。把一个规则想象成一个食谱;一个或多个指定如何构建目标的命令。
规则前面的空格是一个制表符。对空格很挑剔。当心一些编辑在你按下TAB键时插入空格,称为软标签,这将导致make产生错误。以下代码通过将选项卡扩展到八个空格来说明这一点:
$ < numbers.make expand > spaces.make
$ bat -A spaces.make ───────┬────────────────────────────────────────────────────────────────────────
│ File: spaces.make
───────┼────────────────────────────────────────────────────────────────────────
1 │ numbers:␊
2 │ ········seq·7␊
───────┴────────────────────────────────────────────────────────────────────────
$ make -f spaces.make # ➊
spaces.make:2: *** missing separator (did you mean TAB instead of 8 spaces?). S
top. # ➋
$ rm spaces.make
➊ 我需要添加-f选项(简称--makefile选项),因为配置文件不叫Makefile,这是默认的。
➋ 你可以在命令行找到的更有用的错误信息之一!
从现在开始,我将把适当的文件重命名为Makefile,因为这样更符合现实世界的使用。所以,如果我运行make:
$ cp numbers.make Makefile
$ make
seq 7
1
2
3
4
5
6
7
然后我们看到make首先打印规则本身(seq 7),然后是规则生成的输出。这个过程被称为构建目标。如果你不指定一个目标的名字,那么make将构建第一个在Makefile中指定的目标。但是在实践中,您通常会指定您想要构建的目标:
$ make numbers
seq 7
1
2
3
4
5
6
7
make本来是为了协助进行源码汇编的, 解释了一些像target,rule和building的术语.
在这种情况下,我们实际上没有构建任何东西,因为我们没有创建任何新文件。make将愉快地再次构建我们的目标numbers,因为它没有找到一个叫做*的文件编号 *。在下一节中,我将深入探讨这一点。
有时,不管同名文件是否存在,都构建一个目标是很有用的。想想作为项目的一部分,您需要执行的任务。在你的Makefile的顶部使用一个名为.PHONY的特殊目标,后跟虚假目标的名字,这是一个很好的做法。这里有一个例子Makefile来说明如何使用假目标:
$ bat tasks.make
───────┬────────────────────────────────────────────────────────────────────────
│ File: tasks.make
───────┼────────────────────────────────────────────────────────────────────────
1 │ .PHONY: clean publish docker-run
2 │
3 │ clean:
4 │ rm book/2e/book.md book/2e/render*.rds
5 │
6 │ publish:
7 │ (cd www && hugo) && netlify deploy --prod --dir www/public
8 │
9 │ docker-run:
10 │ docker run -it --rm -v $$(pwd)/book/2e/data:/data -p 8000:8000
│ datasciencetoolbox/dsatcl2e:latest # ➊
───────┴────────────────────────────────────────────────────────────────────────
➊ 注意$(pwd)前面多出来的美元符号。这是必要的,因为make使用一个美元符号来表示各种特殊变量,我将在后面解释。
以上摘自我写这本书时使用的Makefile。你可以说我把make作为一个荣耀的任务运行者。虽然这不是make的主要目的,但它仍然提供了很多价值,因为我不需要记住或查找我使用了什么咒语。相反,我输入make publish,这本书的最新版本就出版了。将长时间运行的命令放在一个Makefile中是非常好的。
并且可以为我们做更多的事情!
6.4 构建实战
让我们修改我们的Makefile,这样规则的输出被写到一个文件numbers。
$ cp numbers-write.make Makefile
$ bat Makefile
───────┬────────────────────────────────────────────────────────────────────────
│ File: Makefile
───────┼────────────────────────────────────────────────────────────────────────
1 │ numbers:
2 │ seq 7 > numbers
───────┴────────────────────────────────────────────────────────────────────────
$ make numbers
seq 7 > numbers
$ bat numbers
───────┬────────────────────────────────────────────────────────────────────────
│ File: numbers
───────┼────────────────────────────────────────────────────────────────────────
1 │ 1
2 │ 2
3 │ 3
4 │ 4
5 │ 5
6 │ 6
7 │ 7
───────┴────────────────────────────────────────────────────────────────────────
现在我们可以说make实际上是在构建什么东西。此外,如果我们再次运行它,我们会看到make报告目标numbers是最新的。
$ make numbers
make: 'numbers' is up to date.
没有必要重建目标numbers,因为文件numbers已经存在。这很好,因为make通过不重复工作节省了我们的时间。
在make里,都是关于文件的。但是要记住make只关心目标的名称 。它不检查规则是否实际创建了同名文件。如果我们要写入一个名为nummers的文件,它在荷兰语中是“数字”的意思,而目标仍然名为numbers,那么make将总是构建这个目标。反之亦然,如果文件numbers是由其他进程创建的,不管是自动的还是手动的,那么make仍然会认为那个目标是最新的。
我们可以通过使用自动变量$@来避免一些重复,该变量被扩展为目标的名称:
$ cp numbers-write-var.make Makefile
$ bat Makefile
───────┬────────────────────────────────────────────────────────────────────────
│ File: Makefile
───────┼────────────────────────────────────────────────────────────────────────
1 │ numbers:
2 │ seq 7 > $@
───────┴────────────────────────────────────────────────────────────────────────
让我们通过删除文件numbers并再次调用make来验证这是否可行:
$ rm numbers
$ make numbers
seq 7 > numbers
$ bat numbers
───────┬────────────────────────────────────────────────────────────────────────
│ File: numbers
───────┼────────────────────────────────────────────────────────────────────────
1 │ 1
2 │ 2
3 │ 3
4 │ 4
5 │ 5
6 │ 6
7 │ 7
───────┴────────────────────────────────────────────────────────────────────────
make重建目标的另一个原因是它的依赖性,所以接下来让我们讨论一下。
6.5 添加依赖关系
到目前为止,我们已经研究了孤立存在的目标。在典型的数据科学工作流中,许多步骤都依赖于其他步骤。为了恰当地讨论 Makefile 中的依赖关系,让我们考虑两个与星战角色数据集相关的任务。
以下是该数据集的摘录:
$ curl -sL 'https://raw.githubusercontent.com/tidyverse/dplyr/master/data-raw/st
arwars.csv' |
> xsv select name,height,mass,homeworld,species |
> csvlook
│ name │ height │ mass │ homeworld │ species │
├───────────────────────┼────────┼─────────┼────────────────┼────────────────┤
│ Luke Skywalker │ 172 │ 77.0 │ Tatooine │ Human │
│ C-3PO │ 167 │ 75.0 │ Tatooine │ Droid │
│ R2-D2 │ 96 │ 32.0 │ Naboo │ Droid │
│ Darth Vader │ 202 │ 136.0 │ Tatooine │ Human │
│ Leia Organa │ 150 │ 49.0 │ Alderaan │ Human │
│ Owen Lars │ 178 │ 120.0 │ Tatooine │ Human │
│ Beru Whitesun lars │ 165 │ 75.0 │ Tatooine │ Human │
│ R5-D4 │ 97 │ 32.0 │ Tatooine │ Droid │
… with 79 more lines
第一个任务计算十个最高的人:
$ curl -sL 'https://raw.githubusercontent.com/tidyverse/dplyr/master/data-raw/st
arwars.csv' |
> grep Human | # ➊
> cut -d, -f 1,2 | # ➋
> sort -t, -k2 -nr | # ➌
> head # ➍
Darth Vader,202
Qui-Gon Jinn,193
Dooku,193
Bail Prestor Organa,191
Raymus Antilles,188
Mace Windu,188
Anakin Skywalker,188
Gregar Typho,185
Jango Fett,183
Cliegg Lars,183
➊ 只保留包含图案Human的行。
➋ 提取前两列。
➌ 按第二列的数字顺序对行进行反向排序。
➍ 默认情况下,head打印前 10 行。您可以用-n选项覆盖它。
第二个任务是创建一个显示每个物种高度分布的箱线图(见图 6.1):
$ curl -sL 'https://raw.githubusercontent.com/tidyverse/dplyr/master/data-raw/st
arwars.csv' |
> rush plot --x height --y species --geom boxplot > heights.png
$ display heights.png

图 6.1:星球大战中每个物种的身高分布
让我们把这两个任务放到一个Makefile中。我不想一步一步地做,我想先展示一个完整的Makefile是什么样子,然后一步一步地解释所有的语法。
$ cp starwars.make Makefile
$ bat Makefile
───────┬────────────────────────────────────────────────────────────────────────
│ File: Makefile
───────┼────────────────────────────────────────────────────────────────────────
1 │ SHELL := bash
2 │ .ONESHELL:
3 │ .SHELLFLAGS := -eu -o pipefail -c
4 │
5 │ URL = "https://raw.githubusercontent.com/tidyverse/dplyr/master/data-ra
│ w/starwars.csv"
6 │
7 │ .PHONY: all top10
8 │
9 │ all: top10 heights.png
10 │
11 │ data:
12 │ mkdir $@
13 │
14 │ data/starwars.csv: data
15 │ curl -sL $(URL) > $@
16 │
17 │ top10: data/starwars.csv
18 │ grep Human $< |
19 │ cut -d, -f 1,2 |
20 │ sort -t, -k2 -nr |
21 │ head
22 │
23 │ heights.png: data/starwars.csv
24 │ < $< rush plot --x height --y species --geom boxplot > $@
───────┴────────────────────────────────────────────────────────────────────────
让我们一步一步地看这个Makefile。前三行用于更改与make本身相关的一些默认设置:
- 所有规则都在 Shell 中执行,默认情况下,Shell 是
sh。用SHELL变量我们可以把它改成另一个 Shell,就像bash。这样我们就可以使用 Bash 提供的所有东西,比如for循环。 - 默认情况下,规则中的每一行都单独发送到 Shell。对于特殊的目标
.ONESHELL,我们可以覆盖它,这样目标top10的规则就起作用了。 .SHELLFLAGS线使得 Bash 更加严格,这被认为是最佳实践。例如,由于这个原因,现在一旦出现错误,目标top10的规则中的管道就会停止。
我们定义一个自定义变量叫做URL。尽管这仅使用一次,但我发现将这样的信息放在文件的开头很有帮助,这样您就可以很容易地对这些设置进行更改。
使用特殊目标.PHONY我们可以指出哪些目标没有被文件表示。在我们的例子中,目标为all``top10。无论目录中是否包含同名文件,这些目标都将被执行。
有五个目标:all``data``data/starwars.csv``top10``heights.png。图 6.1 概述了这些目标以及它们之间的依赖关系。

图 6.2:目标之间的依赖关系
让我们依次讨论每个目标:
- 目标
all有两个依赖项,但没有规则。这就像是按指定顺序执行一个或多个目标的快捷方式。在这种情况下:top10``heights.png。目标all作为第一个目标出现在Makefile中,这意味着如果我们运行make,这个目标将被构建。 - 目标
data创建目录data。之前我说过make都是关于文件的。嗯,也是关于目录的。只有当目录data尚不存在时,才会执行该目标。 - 目标
data/starwars.csv取决于目标data。如果没有data目录,它将首先被创建。一旦满足了所有的依赖关系,就会执行规则,包括下载一个文件,并将其保存到与目标同名的文件中。 - 目标
top10被标记为冒牌货,所以如果指定,它将始终被构建。这取决于data/starwars.csv目标。它使用了一个特殊的变量$<,该变量扩展为第一个先决条件的名称,即data/starwars.csv。 - 目标
heights.png,与目标top10一样,依赖于data/starwars.csv,并且利用了我们在本章中看到的两个自动变量。如果您想了解其他自动变量,请参见在线文档。
最后但同样重要的是,让我们验证这个Makefile是否有效:
$ make
mkdir data
curl -sL "https://raw.githubusercontent.com/tidyverse/dplyr/master/data-raw/star
wars.csv" > data/starwars.csv
grep Human data/starwars.csv |
cut -d, -f 1,2 |
sort -t, -k2 -nr |
head
Darth Vader,202
Qui-Gon Jinn,193
Dooku,193
Bail Prestor Organa,191
Raymus Antilles,188
Mace Windu,188
Anakin Skywalker,188
Gregar Typho,185
Jango Fett,183
Cliegg Lars,183
< data/starwars.csv rush plot --x height --y species --geom boxplot > heights.pn
g
这里没有惊喜。因为我们没有指定任何目标,所以将构建all目标,这又会导致构建top10和heights.png目标。前者的输出被打印成标准输出,后者创建一个文件heights.png。data目录只创建一次,就像 CSV 文件只下载一次一样。
没有什么比只是玩你的数据而忘记其他一切更有趣的了。但是当我说使用Makefile来记录你所做的事情是值得的时候,你必须相信我。这不仅会让您的生活变得更轻松(双关语),而且您还会开始按照步骤来考虑您的数据工作流。正如您自己的命令行工具箱一样,它会随着时间的推移而扩展,这同样适用于make工作流。您定义的步骤越多,就越容易继续做下去,因为通常您可以重用某些步骤。我希望你会习惯make,它会让你的生活更轻松。
6.6 总结
命令行的一个优点是它允许您处理数据。您可以轻松地执行不同的命令和处理不同的数据文件。这是一个非常互动和迭代的过程。过一段时间后,很容易忘记你采取了哪些步骤来获得想要的结果。因此,每隔一段时间记录你的步骤是非常重要的。这样,如果您或您的同事在一段时间后重新开始您的项目,通过执行相同的步骤可以再次产生相同的结果。
在这一章中,我已经向您展示了仅仅将每个命令放在一个 Bash 脚本中并不是最理想的。相反,我建议使用make作为命令行工具来管理您的数据工作流。下一章涵盖了 OSEMN 数据科学模型的第三步,即探索数据。
6.7 进一步探索
- 罗伯特·梅克伦堡的《用 GNU Make 管理项目》一书和在线的《GNU Make 手册》提供了对
make的全面和高级的概述。 - 除了
make之外,还有很多其他的工作流管理器。尽管它们在语法和功能上有所不同,但它们也使用诸如目标、规则和依赖关系等概念。例子包括 Luigi、ApacheAirflow 和 Nextflow。 - 要了解更多关于版本控制的知识,特别是
git和 GitHub,我推荐斯科特·沙孔和本·施特劳布的书《Pro Git》。免费提供的在线 GitHub 文档也是一个很好的起点。
七、探索数据
原文:https://datascienceatthecommandline.com/2e/chapter-7-exploring-data.html
在所有这些艰苦的工作之后(除非你已经有了干净的数据),是时候享受一些乐趣了。现在您已经获得并清理了数据,您可以继续进行 OSEMN 模型的第三步,即探索数据。
探索是你熟悉数据的步骤。当您想要从中提取任何价值时,熟悉数据是必不可少的。例如,知道数据具有哪种特征,意味着您知道哪些特征值得进一步探索,哪些特征可以用来回答您的任何问题。
可以从三个角度探索您的数据。第一个视角是检查数据及其属性。在这里,您希望了解诸如原始数据的样子、数据集有多少个数据点以及数据集有哪些特征之类的信息。
第二是计算描述性统计。这种视角有助于了解更多关于单个特性的信息。输出通常是简短的文本,因此可以在命令行上打印。
第三个视角是创建数据的可视化。从这个角度,您可以深入了解多个功能是如何交互的。我将讨论一种创建可以在命令行上打印的可视化效果的方法。然而,可视化最适合在图形用户界面上显示。数据可视化优于描述性统计的一个优点是,它们更灵活,可以传达更多的信息。
7.1 概述
在本章中,您将学习如何:
- 检查数据及其属性
- 计算描述性统计量
- 在命令行内外创建数据可视化
本章从以下文件开始:
$ cd /data/ch07
$ l
total 104K
-rw-r--r-- 1 dst dst 125 Mar 3 10:46 datatypes.csv
-rw-r--r-- 1 dst dst 7.8K Mar 3 10:46 tips.csv
-rw-r--r-- 1 dst dst 83K Mar 3 10:46 venture.csv
-rw-r--r-- 1 dst dst 4.6K Mar 3 10:46 venture-wide.csv
获取这些文件的说明在第二章中。任何其他文件都是使用命令行工具下载或生成的。
7.2 检查数据及其属性
在本节中,我将演示如何检查数据集及其属性。因为即将到来的可视化和建模技术期望数据是矩形的,所以我假设数据是 CSV 格式的。如有必要,您可以使用第五章中描述的技术将您的数据转换成 CSV 格式。
为了简单起见,我还假设您的数据有一个头。在第一小节中,我将展示一种方法来确定是否是这样。一旦你知道你有一个标题,你可以继续回答下列问题:
- 数据集有多少个数据点和特征?
- 原始数据是什么样的?
- 数据集有什么样的特征?
- 这些特征中的一些可以被视为绝对的吗?
7.2.1 不管有没有head,我来了
您可以通过使用head打印前几行来检查您的文件是否有标题:
$ head -n 5 venture.csv
FREQ,TIME_FORMAT,TIME_PERIOD,EXPEND,UNIT,GEO,OBS_STATUS,OBS_VALUE,FREQ_DESC,TIME
_FORMAT_DESC,TIME_PERIOD_DESC,OBS_STATUS_DESC,EXPEND_DESC,UNIT_DESC,GEO_DESC
A,P1Y,2015,INV_VEN,PC_GDP,CZ,,0.002,Annual,Annual,Year 2015,No data,"Venture cap
ital investment (seed, start-up and later stage) ",Percentage of GDP,Czechia
A,P1Y,2007,INV_VEN,PC_GDP,DE,,0.034,Annual,Annual,Year 2007,No data,"Venture cap
ital investment (seed, start-up and later stage) ",Percentage of GDP,Germany
A,P1Y,2008,INV_VEN,PC_GDP,DE,,0.039,Annual,Annual,Year 2008,No data,"Venture cap
ital investment (seed, start-up and later stage) ",Percentage of GDP,Germany
A,P1Y,2009,INV_VEN,PC_GDP,DE,,0.029,Annual,Annual,Year 2009,No data,"Venture cap
ital investment (seed, start-up and later stage) ",Percentage of GDP,Germany
如果这些行换行,使用nl添加行号:
$ head -n 3 venture.csv | nl
1 FREQ,TIME_FORMAT,TIME_PERIOD,EXPEND,UNIT,GEO,OBS_STATUS,OBS_VALUE,FREQ_D
ESC,TIME_FORMAT_DESC,TIME_PERIOD_DESC,OBS_STATUS_DESC,EXPEND_DESC,UNIT_DESC,GEO_
DESC
2 A,P1Y,2015,INV_VEN,PC_GDP,CZ,,0.002,Annual,Annual,Year 2015,No data,"Ven
ture capital investment (seed, start-up and later stage) ",Percentage of GDP,Cze
chia
3 A,P1Y,2007,INV_VEN,PC_GDP,DE,,0.034,Annual,Annual,Year 2007,No data,"Ven
ture capital investment (seed, start-up and later stage) ",Percentage of GDP,Ger
many
或者,您可以使用trim:
$ < venture.csv trim 5
FREQ,TIME_FORMAT,TIME_PERIOD,EXPEND,UNIT,GEO,OBS_STATUS,OBS_VALUE,FREQ_DESC,TIM…
A,P1Y,2015,INV_VEN,PC_GDP,CZ,,0.002,Annual,Annual,Year 2015,No data,"Venture ca…
A,P1Y,2007,INV_VEN,PC_GDP,DE,,0.034,Annual,Annual,Year 2007,No data,"Venture ca…
A,P1Y,2008,INV_VEN,PC_GDP,DE,,0.039,Annual,Annual,Year 2008,No data,"Venture ca…
A,P1Y,2009,INV_VEN,PC_GDP,DE,,0.029,Annual,Annual,Year 2009,No data,"Venture ca…
… with 536 more lines
在这种情况下,很明显,第一行是一个标题,因为它只包含大写的名称,后面的行包含数字。这确实是一个相当主观的过程,由您决定第一行是标题还是已经是第一个数据点。当数据集不包含标题时,你最好使用header工具(在第五章中讨论)来纠正它。
7.2.2 检查所有数据
如果你想按照自己的节奏检查原始数据,那么使用cat可能不是一个好主意,因为那样所有的数据都会被一次性打印出来。我推荐使用less,它允许您在命令行中交互式地检查您的数据。您可以通过指定-S选项来防止长行(如venture.csv)换行:
$ less -S venture.csv
,GEO,OBS_STATUS,OBS_VALUE,FREQ_DESC,TIME_FORMAT_DESC,TIME_PERIOD_DESC,OBS_STATU> al,Annual,Year 2015,No data,"Venture capital investment (seed, start-up and lat> al,Annual,Year 2007,No data,"Venture capital investment (seed, start-up and lat> al,Annual,Year 2008,No data,"Venture capital investment (seed, start-up and lat> al,Annual,Year 2009,No data,"Venture capital investment (seed, start-up and lat> al,Annual,Year 2010,No data,"Venture capital investment (seed, start-up and lat> al,Annual,Year 2011,No data,"Venture capital investment (seed, start-up and lat> al,Annual,Year 2012,No data,"Venture capital investment (seed, start-up and lat> al,Annual,Year 2013,No data,"Venture capital investment (seed, start-up and lat> al,Annual,Year 2014,No data,"Venture capital investment (seed, start-up and lat> al,Annual,Year 2015,No data,"Venture capital investment (seed, start-up and lat> al,Annual,Year 2007,No data,"Venture capital investment (seed, start-up and lat> al,Annual,Year 2008,No data,"Venture capital investment (seed, start-up and lat> al,Annual,Year 2009,No data,"Venture capital investment (seed, start-up and lat> al,Annual,Year 2010,No data,"Venture capital investment (seed, start-up and lat> :
右边的大于号表示您可以水平滚动。按Up和Down可以上下滚动。按下Space向下滚动整个屏幕。水平滚动通过按Left和Right完成。按下g和G分别转到文件的开始和结束。按下q即可退出less。手册页列出了所有可用的键绑定。
less的一个优点是它不会将整个文件加载到内存中,这意味着它即使在查看大文件时也很快。
7.2.3 特征名称和数据类型
列(或特征)名称可以指示特征的含义。为此,您可以使用以下head和tr组合:
$ < venture.csv head -n 1 | tr , '\n'
FREQ
TIME_FORMAT
TIME_PERIOD
EXPEND
UNIT
GEO
OBS_STATUS
OBS_VALUE
FREQ_DESC
TIME_FORMAT_DESC
TIME_PERIOD_DESC
OBS_STATUS_DESC
EXPEND_DESC
UNIT_DESC
GEO_DESC
这个基本命令假设文件由逗号分隔。更健壮的方法是使用csvcut:
$ csvcut -n venture.csv
1: FREQ
2: TIME_FORMAT
3: TIME_PERIOD
4: EXPEND
5: UNIT
6: GEO
7: OBS_STATUS
8: OBS_VALUE
9: FREQ_DESC
10: TIME_FORMAT_DESC
11: TIME_PERIOD_DESC
12: OBS_STATUS_DESC
13: EXPEND_DESC
14: UNIT_DESC
15: GEO_DESC
除了打印列名之外,您还可以更进一步。除了列名之外,了解每列包含什么类型的值也非常有用,比如字符串、数值或日期。假设您有以下玩具数据集:
$ bat -A datatypes.csv
───────┬────────────────────────────────────────────────────────────────────────
│ File: datatypes.csv
───────┼────────────────────────────────────────────────────────────────────────
1 │ a,b,c,d,e,f␊
2 │ 1,0.0,FALSE,"""Yes!""",2011-11-11·11:00,2012-09-08␊
3 │ 42,3.1415,TRUE,"OK,·good",2014-09-15,12/6/70␊
4 │ 66,,False,2198,,␊
───────┴────────────────────────────────────────────────────────────────────────
其csvlook解释如下:
$ csvlook datatypes.csv
│ a │ b │ c │ d │ e │ f │
├────┼────────┼───────┼──────────┼─────────────────────┼────────────┤
│ 1 │ 0.000… │ False │ "Yes!" │ 2011-11-11 11:00:00 │ 2012-09-08 │
│ 42 │ 3.142… │ True │ OK, good │ 2014-09-15 00:00:00 │ 1970-12-06 │
│ 66 │ │ False │ 2198 │ │ │
我已经在第五章中使用了csvsql来直接对 CSV 数据执行 SQL 查询。当没有传递命令行参数时,它会生成必要的 SQL 语句,如果要将这些数据插入到实际的数据库中,就需要用到这些语句。您还可以使用输出来检查推断的列类型。如果一列在数据类型后打印了NOT NULL字符串,那么该列不包含缺失值。
$ csvsql datatypes.csv
CREATE TABLE datatypes (
a DECIMAL NOT NULL,
b DECIMAL,
c BOOLEAN NOT NULL,
d VARCHAR NOT NULL,
e TIMESTAMP,
f DATE
);
当您使用csvkit套件中的其他工具时,例如csvgrep、csvsort和csvsql,这个输出特别有用。对于venture.csv,各列推断如下:
$ csvsql venture.csv CREATE TABLE venture (
"FREQ" VARCHAR NOT NULL,
"TIME_FORMAT" VARCHAR NOT NULL,
"TIME_PERIOD" DECIMAL NOT NULL,
"EXPEND" VARCHAR NOT NULL,
"UNIT" VARCHAR NOT NULL,
"GEO" VARCHAR NOT NULL,
"OBS_STATUS" BOOLEAN,
"OBS_VALUE" DECIMAL NOT NULL,
"FREQ_DESC" VARCHAR NOT NULL,
"TIME_FORMAT_DESC" VARCHAR NOT NULL,
"TIME_PERIOD_DESC" VARCHAR NOT NULL,
"OBS_STATUS_DESC" VARCHAR NOT NULL,
"EXPEND_DESC" VARCHAR NOT NULL,
"UNIT_DESC" VARCHAR NOT NULL,
"GEO_DESC" VARCHAR NOT NULL
);
7.2.4 唯一标识符、连续变量和因子
仅仅知道每个特性的数据类型是不够的。了解每个特性代表什么也很重要。了解这个领域非常有用,但是我们也可以通过查看数据本身来获得一些上下文。
字符串和整数都可以是唯一的标识符,也可以代表一个类别。在后一种情况下,这可以用来为您的可视化指定一种颜色。但是如果一个整数代表一个邮政编码,那么计算平均值就没有意义了。
要确定某个特征是否应被视为唯一标识符或分类变量,您可以计算特定列的唯一值的数量:
$ wc -l tips.csv
245 tips.csv
$ < tips.csv csvcut -c day | header -d | sort | uniq | wc -l
4
您可以使用csvstat (它是csvkit的一部分)来获取每列的唯一值的数量:
$ csvstat tips.csv --unique
1\. bill: 229
2\. tip: 123
3\. sex: 2
4\. smoker: 2
5\. day: 4
6\. time: 2
7\. size: 6
$ csvstat venture.csv --unique
1\. FREQ: 1
2\. TIME_FORMAT: 1
3\. TIME_PERIOD: 9
4\. EXPEND: 1
5\. UNIT: 3
6\. GEO: 20
7\. OBS_STATUS: 1
8\. OBS_VALUE: 286
9\. FREQ_DESC: 1
10\. TIME_FORMAT_DESC: 1
11\. TIME_PERIOD_DESC: 9
12\. OBS_STATUS_DESC: 1
13\. EXPEND_DESC: 1
14\. UNIT_DESC: 3
15\. GEO_DESC: 20
如果只有一个惟一的值(比如用OBS_STATUS,那么有可能您可以丢弃该列,因为它不提供任何值。如果您想自动丢弃所有这样的列,那么您可以使用以下管道:
$ < venture.csv csvcut -C $( # ➊
> csvstat venture.csv --unique | # ➋
> grep ': 1$' | # ➌
> cut -d. -f 1 | # ➍
> tr -d ' ' | # ➎
> paste -sd, # ➏
> ) | trim # ➐
TIME_PERIOD,UNIT,GEO,OBS_VALUE,TIME_PERIOD_DESC,UNIT_DESC,GEO_DESC
2015,PC_GDP,CZ,0.002,Year 2015,Percentage of GDP,Czechia
2007,PC_GDP,DE,0.034,Year 2007,Percentage of GDP,Germany
2008,PC_GDP,DE,0.039,Year 2008,Percentage of GDP,Germany
2009,PC_GDP,DE,0.029,Year 2009,Percentage of GDP,Germany
2010,PC_GDP,DE,0.029,Year 2010,Percentage of GDP,Germany
2011,PC_GDP,DE,0.029,Year 2011,Percentage of GDP,Germany
2012,PC_GDP,DE,0.021,Year 2012,Percentage of GDP,Germany
2013,PC_GDP,DE,0.023,Year 2013,Percentage of GDP,Germany
2014,PC_GDP,DE,0.021,Year 2014,Percentage of GDP,Germany
… with 531 more lines
➊ -C选项取消选择给定位置(或名称)的列,该选项提供了命令替换
➋ 获取venture.csv
➌ 仅保留包含一个唯一值的列
➍ 提取列位置
➎ 修剪任何空白区域
➏ 放置所有列位置
说到这里,我打算暂时保留这些专栏。
一般来说,如果唯一值的数量与总行数相比较少,那么该特征可能会被视为分类特征(例如在venture.csv中的GEO)。如果数字等于行数,它可能是唯一标识符,但也可能是数值。只有一个方法可以找到答案:我们需要更深入。
7.3 计算描述性统计量
7.3.1 列的统计量
命令行工具csvstat给出了很多信息。对于每个特征(列),它显示:
- 数据类型
- 它是否有任何缺失值(空值)
- 唯一值的数量
- 适用于这些特征的各种描述性统计数据(最小值、最大值、总和、平均值、标准差和中值)
如下调用csvstat:
$ csvstat venture.csv | trim 32
1\. "FREQ"
Type of data: Text
Contains null values: False
Unique values: 1
Longest value: 1 characters
Most common values: A (540x)
2\. "TIME_FORMAT"
Type of data: Text
Contains null values: False
Unique values: 1
Longest value: 3 characters
Most common values: P1Y (540x)
3\. "TIME_PERIOD"
Type of data: Number
Contains null values: False
Unique values: 9
Smallest value: 2,007
Largest value: 2,015
Sum: 1,085,940
Mean: 2,011
Median: 2,011
StDev: 2.584
Most common values: 2,015 (60x)
2,007 (60x)
2,008 (60x)
2,009 (60x)
2,010 (60x)
… with 122 more lines
我只显示了前 32 行,因为这会产生大量输出。您可能想通过less来处理这个问题。如果您只对特定的统计数据感兴趣,也可以使用以下选项之一:
--max(最大)--min(最小值)--sum(总和)--mean(均值)--median(中值)--stdev(标准差)--nulls(列是否包含空值)--unique(唯一值)--freq(频繁值)--len(最大值长度)
例如:
$ csvstat venture.csv --freq | trim
1\. FREQ: { "A": 540 }
2\. TIME_FORMAT: { "P1Y": 540 }
3\. TIME_PERIOD: { "2015": 60, "2007": 60, "2008": 60, "2009": 60, "2010": 60 }
4\. EXPEND: { "INV_VEN": 540 }
5\. UNIT: { "PC_GDP": 180, "NR_COMP": 180, "MIO_EUR": 180 }
6\. GEO: { "CZ": 27, "DE": 27, "DK": 27, "EL": 27, "ES": 27 }
7\. OBS_STATUS: { "None": 540 }
8\. OBS_VALUE: { "0": 28, "1": 19, "2": 14, "0.002": 10, "0.034": 7 }
9\. FREQ_DESC: { "Annual": 540 }
10\. TIME_FORMAT_DESC: { "Annual": 540 }
… with 5 more lines
您可以使用-c选项选择一个特征子集,该选项接受整数和列名:
$ csvstat venture.csv -c 3,GEO
3\. "TIME_PERIOD"
Type of data: Number
Contains null values: False
Unique values: 9
Smallest value: 2,007
Largest value: 2,015
Sum: 1,085,940
Mean: 2,011
Median: 2,011
StDev: 2.584
Most common values: 2,015 (60x)
2,007 (60x)
2,008 (60x)
2,009 (60x)
2,010 (60x)
6\. "GEO"
Type of data: Text
Contains null values: False
Unique values: 20
Longest value: 2 characters
Most common values: CZ (27x)
DE (27x)
DK (27x)
EL (27x)
ES (27x)
Row count: 540
记住csvstat, 就像csvsql, 采用启发式的方法去决定数据类型, 并且不一定毁正确. 我鼓励你在采用了前述的分组后,保持采取人工的检查. 进一步, 即使数据类型是一个字符串或者整型, 也没有指明应该如何应用它.
作为一个很好的附加功能,csvstat在最后输出数据点(行)的数量。正确处理值中的换行符和逗号。要只看到最后一行,您可以使用tail。或者,您可以使用xsv,它只返回实际的行数。
$ csvstat venture.csv | tail -n 1
Row count: 540
$ xsv count venture.csv
540
注意,这两个选项不同于使用wc -l,它计算新行的数量(因此也计算标题)。
7.3.2 Shell 上的 R 单行代码
在本节中,我将向您介绍一个名为rush的命令行工具,它使您能够直接从命令行利用统计编程环境R。在我解释rush做什么以及它为什么存在之前,让我们先谈谈R本身。
R是一个非常强大的做数据科学的统计软件包。它是一种解释型编程语言,有大量的软件包,并提供自己的 REPL,类似于命令行,允许您处理数据。注意,一旦启动 R,您就处于一个独立于 Unix 命令行的交互式会话中。
假设您有一个名为tips.csv的 CSV 文件,您想要计算小费的百分比,并保存结果。为了在R中完成这一点,您将首先运行R:
$ R --quiet # ➊
>
➊ 我在这里使用--quiet选项来抑制相当长的启动消息
然后运行下面的代码:
> library(tidyverse) # ➊
── Attaching packages ─────────────────────────────────────── tidyverse 1.3.0 ──
✔ ggplot2 3.3.3 ✔ purrr 0.3.4
✔ tibble 3.0.6 ✔ dplyr 1.0.4
✔ tidyr 1.1.2 ✔ stringr 1.4.0
✔ readr 1.4.0 ✔ forcats 0.5.1
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag() masks stats::lag()
> df <- read_csv("tips.csv") # ➋
── Column specification ──────────────────────────────────────────────────────── cols(
bill = col_double(),
tip = col_double(),
sex = col_character(),
smoker = col_character(),
day = col_character(),
time = col_character(),
size = col_double()
)
> df <- mutate(df, percent = tip / bill * 100) # ➌
> write_csv(df, "percent.csv") # ➍
> q("no") # ➎
$
➊ 加载任何需要的包
➋ 读入 CSV 文件并将其赋给变量
➌ 计算新列percent
➍ 将结果保存到磁盘
➎ 退出R
之后,您可以在命令行上继续使用保存的文件percent.csv。
$ < percent.csv trim 5
bill,tip,sex,smoker,day,time,size,percent
16.99,1.01,Female,No,Sun,Dinner,2,5.9446733372572105
10.34,1.66,Male,No,Sun,Dinner,3,16.054158607350097
21.01,3.5,Male,No,Sun,Dinner,3,16.658733936220845
23.68,3.31,Male,No,Sun,Dinner,2,13.97804054054054
… with 240 more lines
请注意,只有第三行与您具体想要完成的任务相关联。其他行是必要的样板。为了完成某件简单的事情而输入这种样板文件是很麻烦的,而且会破坏你的工作流程。有时,您只想一次对数据做一两件事。如果您能驾驭R的力量并从命令行使用它,那不是很好吗?
这就是rush的用武之地。让我们像以前一样执行相同的任务,但是现在使用rush:
$ rm percent.csv
$ rush run -t 'mutate(df, percent = tip / bill * 100)' tips.csv > percent.csv
$ < percent.csv trim 5
bill,tip,sex,smoker,day,time,size,percent
16.99,1.01,Female,No,Sun,Dinner,2,5.9446733372572105
10.34,1.66,Male,No,Sun,Dinner,3,16.054158607350097
21.01,3.5,Male,No,Sun,Dinner,3,16.658733936220845
23.68,3.31,Male,No,Sun,Dinner,2,13.97804054054054
… with 240 more lines
这些小的一行程序是可能的,因为rush处理所有的样板文件。在这种情况下,我使用的是run子命令。还有plot子命令,我将在下一节中使用它来快速生成数据可视化。如果您正在传递任何输入数据,那么默认情况下,rush假设它是 CSV 格式的,带有一个头和一个逗号作为分隔符。此外,对列名进行了清理,以便更容易使用。您可以分别使用--no-header(或-H)、--delimiter(或-d)和--no-clean-names(或-C)选项来覆盖这些默认值。该帮助很好地概述了run子命令的可用选项:
$ rush run --help
rush: Run an R expression
Usage:
rush run [options] <expression> [--] [<file>...]
Reading options:
-d, --delimiter <str> Delimiter [default: ,].
-C, --no-clean-names No clean names.
-H, --no-header No header.
Setup options:
-l, --library <name> Libraries to load.
-t, --tidyverse Enter the Tidyverse.
Saving options:
--dpi <str|int> Plot resolution [default: 300].
--height <int> Plot height.
-o, --output <str> Output file.
--units <str> Plot size units [default: in].
-w, --width <int> Plot width.
General options:
-n, --dry-run Only print generated script.
-h, --help Show this help.
-q, --quiet Be quiet.
--seed <int> Seed random number generator.
-v, --verbose Be verbose.
--version Show version.
在幕后,rush生成一个R脚本并随后执行它。您可以通过指定--dry-run(或-n)选项来查看这个生成的脚本:
$ rush run -n --tidyverse 'mutate(df, percent = tip / bill * 100)' tips.csv
#!/usr/bin/env Rscript
library(tidyverse)
library(glue)
df <- janitor::clean_names(readr::read_delim("tips.csv", delim = ",", col_names
= TRUE))
mutate(df, percent = tip/bill * 100)
这个生成的脚本:
- 写出了 Shebang(
#!);参见从命令行运行R脚本所需的第四章。 - 导入
tidyverse和glue包。 - 加载
tips.csv作为数据帧,清除列名,并将其赋给变量df。 - 运行指定的表达式。
- 将结果打印到标准输出。
您可以将这个生成的脚本重定向到一个文件,并通过 Shebang 轻松地将它变成一个新的命令行工具。
rush的输出本身不一定是 CSV 格式的。在这里,我计算平均小费百分比、最大聚会规模、时间列的唯一值、账单和小费之间的相关性。最后,我提取整个列(但只显示前 10 个值)。
$ < percent.csv rush run 'mean(df$percent)' -
16.0802581722505
$ < percent.csv rush run 'max(df$size)' -
6
$ < percent.csv rush run 'unique(df$time)' -
Dinner
Lunch
$ < percent.csv rush run 'cor(df$bill, df$tip)' -
0.675734109211365
$ < percent.csv rush run 'df$tip' - | trim
1.01
1.66
3.5
3.31
3.61
4.71
2
3.12
1.96
3.23
… with 234 more lines
最后一个破折号意味着rush应该从标准输入中读取。
所以现在,如果你想用R对你的数据集做一两件事,你可以把它指定为一行程序,然后继续在命令行上工作。您已经掌握的关于R的所有知识现在都可以从命令行使用了。使用rush,你甚至可以创建复杂的可视化效果,我将在下一节向你展示。
7.4 创建可视化效果
在这一节中,我将向您展示如何在命令行创建数据可视化。我将使用rush plot创建条形图、散点图和箱线图。不过,在我们开始之前,我想先解释一下如何显示可视化效果。
7.4.1 从命令行显示图像
让我们以tips.png的图像为例。看一下图 7.1,这是使用rush和tips.csv数据集创建的数据可视化。(一会儿我会解释一下rush的语法。)我使用display工具将图片插入书中,但是如果你运行display你会发现它不起作用。这是因为从命令行显示图像实际上相当棘手。

图 7.1:自己显示这个图像可能有些棘手
根据您的设置,有不同的选项可用于显示图像。我知道有四种选择,每种都有自己的优缺点:(1)作为文本表示,(2)作为内嵌图像,(3)使用图像查看器,以及(4)使用浏览器。让我们快速浏览一遍。

图 7.2:通过 ASCII 字符和 ANSI 转义序列(上图)和 iTerm2 内嵌图像协议(下图)在终端显示图像
选项 1 是显示终端内的图像,如图 7.2 顶部所示。当标准输出没有重定向到文件时,此输出由rush生成。它基于 ASCII 字符和 ANSI 转义序列,因此在每个终端中都可用。根据你阅读本书的方式,当你运行这段代码时,你得到的输出可能与图 7.2 中的截图相符,也可能不相符。
$ rush plot --x bill --y tip --color size --facets '~day' tips.csv
Fri Sat 10.0 * # 7.5 * # * 5.0 # * ### * # # # ### ## ## #####*####+ * ** # 2.5 # %### % #########*#*## # # t size i Sun Thur 6 p 10.0 1 7.5 = * ** * # * 5.0 ## # +#*# +* * # = ##* = = # +* 2.5 ######## # * ### # # ## #####* # ## ####* * #+ ###### # 10 20 30 40 50 10 20 30 40 50 bill
如果你只看到 ASCII 字符,这意味着你阅读这本书的媒介不支持负责颜色的 ANSI 转义序列。幸运的是,如果您自己运行上面的命令,它看起来就像截图一样。
如图 7.2 底部所示,选项 2 也显示终端内的图像。这是 iTerm2 终端,仅适用于 macOS,通过一个小脚本(我将其命名为display)使用内嵌图像协议。Docker 映像不包含该脚本,但是您可以轻松地安装它:
$ curl -s "https://iterm2.com/utilities/imgcat" > display && chmod u+x display
如果您没有在 macOS 上使用 iTerm2,可能有其他选项可以内联显示图像。请咨询你喜欢的搜索引擎。

图 7.3:通过文件浏览器和图像浏览器(左)以及通过网络服务器和浏览器(右)在外部显示图像
选项 3 是在图像查看器中手动打开图像(本例中为tips.csv)。图 7.3 在左边显示了 macOS 上的文件浏览器(Finder)和图像浏览器(Preview)。当你在本地工作时,这个选项总是有效的。当您在 Docker 容器中工作时,只有当您使用-v选项映射了一个本地目录时,才能从您的操作系统访问生成的映像。参见第二章了解如何操作的说明。此选项的一个优点是,当图像发生变化时,大多数图像查看器会自动更新显示,这允许您在微调可视化时进行快速迭代。
选项 4 是在浏览器中打开图像。图 7.3 右侧是火狐显示http://localhost:8000/tips.png的截图。任何浏览器都可以,但是您需要另外两个先决条件才能使用这个选项。首先,您需要使用-p选项在 Docker 容器上创建一个可访问的端口(本例中为端口 8000)。(同样,参见第二章了解如何操作的说明。)其次,你需要启动一个 Webserver。为此,Docker 容器有一个名为servewd的小工具,使用 Python 服务于当前工作目录:
$ bat $(which servewd)
───────┬────────────────────────────────────────────────────────────────────────
│ File: /usr/bin/dsutils/servewd
───────┼────────────────────────────────────────────────────────────────────────
1 │ #!/usr/bin/env bash
2 │ ARGS="$@"
3 │ python3 -m http.server ${ARGS} 2>/dev/null &
───────┴────────────────────────────────────────────────────────────────────────
你只需要从一个目录运行servewd一次(比如/data/),它就会愉快地在后台运行。一旦你绘制了一些东西,你就可以在你的浏览器中访问localhost:8000并访问该目录及其所有子目录的内容。默认端口是 8000,但是您可以通过将它指定为servewd的参数来更改它:
$ servewd 9999
只要确保这个端口是可访问的。因为servewd在后台运行,所以需要按如下方式停止它:
$ pkill -f http.server
选项 4 也可以在远程机器上工作。
既然我们已经介绍了显示图像的四个选项,让我们继续实际创建一些。
7.4.2 使用rush绘图
当谈到创建数据可视化时,有太多的选择。就我个人而言,我是一个坚定的支持者ggplot2,这是一个 R 的可视化包。图形的底层语法伴随着一个一致的 API,允许您快速迭代地创建不同类型的漂亮数据可视化,而很少需要查阅文档。探索数据时一组受欢迎的属性。
我们并不真的着急,但我们也不想过多地摆弄任何单一的可视化。此外,我们希望尽可能多地使用命令行。幸运的是,我们还有rush,它允许我们从命令行ggplot2。图 7.1 中的数据可视化可以创建如下:
$ rush run --library ggplot2 'ggplot(df, aes(x = bill, y = tip, color = size)) +
geom_point() + facet_wrap(~day)' tips.csv > tips.png
然而,你可能已经注意到了,我使用了一个非常不同的命令来创建tips.png:
$ rush plot --x bill --y tip --color size --facets '~day' tips.csv > tips.png
虽然ggplot2的语法相对简洁,特别是考虑到它所提供的灵活性,但是有一个快速创建基本绘图的捷径。该快捷方式可通过rush的plot子命令获得。这允许你创建漂亮的基本绘图,而不需要学习 R 和图形的语法。
在引擎盖下,rush plot使用ggplot2包中的功能qplot。这是文件的第一部分:
$ R -q -e '?ggplot2::qplot' | trim 14
> ?ggplot2::qplot
qplot package:ggplot2 R Documentation
Quick plot
Description:
‘qplot()’ is a shortcut designed to be familiar if you're used to
base ‘plot()’. It's a convenient wrapper for creating a number of
different types of plots using a consistent calling scheme. It's
great for allowing you to produce plots quickly, but I highly
recommend learning ‘ggplot()’ as it makes it easier to create
complex graphics.
… with 108 more lines
我同意这个建议;一旦你读完这本书,学习ggplot2将是值得的,尤其是如果你想将任何探索性的数据可视化升级为适合交流的可视化。现在,当我们在命令行时,让我们走捷径。
如图 7.2 所示,rush plot可以用相同的语法创建图形可视化(由像素组成)和文本可视化(由 ASCII 字符和 ANSI 转义序列组成)。当rush检测到其输出通过管道传输到另一个命令(如display或重定向到一个文件,如tips.png时,它将产生一个图形可视化;否则它将产生文本可视化。
让我们花点时间通读一下rush plot的绘图和保存选项:
$ rush plot --help
rush: Quick plot
Usage:
rush plot [options] [--] [<file>|-]
Reading options:
-d, --delimiter <str> Delimiter [default: ,].
-C, --no-clean-names No clean names.
-H, --no-header No header.
Setup options:
-l, --library <name> Libraries to load.
-t, --tidyverse Enter the Tidyverse.
Plotting options:
--aes <key=value> Additional aesthetics.
-a, --alpha <name> Alpha column.
-c, --color <name> Color column.
--facets <formula> Facet specification.
-f, --fill <name> Fill column.
-g, --geom <geom> Geometry [default: auto].
--group <name> Group column.
--log <x|y|xy> Variables to log transform.
--margins Display marginal facets.
--post <code> Code to run after plotting.
--pre <code> Code to run before plotting.
--shape <name> Shape column.
--size <name> Size column.
--title <str> Plot title.
-x, --x <name> X column.
--xlab <str> X axis label.
-y, --y <name> Y column.
--ylab <str> Y axis label.
-z, --z <name> Z column.
Saving options:
--dpi <str|int> Plot resolution [default: 300].
--height <int> Plot height.
-o, --output <str> Output file.
--units <str> Plot size units [default: in].
-w, --width <int> Plot width.
General options:
-n, --dry-run Only print generated script.
-h, --help Show this help.
-q, --quiet Be quiet.
--seed <int> Seed random number generator.
-v, --verbose Be verbose.
--version Show version.
最重要的选项是以<name>作为参数的绘图选项。例如,--x选项允许您指定应该使用哪一列来确定对象应该沿 x 轴放置在哪里。这同样适用于--y选项。--color和--fill选项用于指定您想要使用哪一列进行着色。你大概能猜到--size和--alpha选项是关于什么的。在我创建各种可视化效果时,其他常见选项将在各节中解释。注意,对于每个可视化,我首先显示其文本表示(ASCII 和 ANSI 字符),然后显示其视觉表示(像素)。
7.4.3 创建条形图
条形图对于显示分类特征的值计数特别有用。以下是tips数据集中的time特征的文本可视化:
$ rush plot --x time tips.csv
******************************** ******************************** 150 ******************************** ******************************** ******************************** ******************************** 100 ******************************** ******************************** ******************************** ******************************** ******************************** ******************************** 50 ******************************** ******************************** ******************************** ******************************** ******************************** ******************************** ******************************** ******************************** 0 ******************************** ******************************** Dinner Lunch time
图 7.4 显示了图形可视化,当输出被重定向到一个文件时,由rush plot创建。
$ rush plot --x time tips.csv > plot-bar.png
$ display plot-bar.png

图 7.4:条形图
从这个柱状图我们可以得出的结论很简单:晚餐的数据点是午餐的两倍多。
7.4.4 创建直方图
连续变量的计数可以用直方图显示。这里,我使用了时间特性来设置填充颜色。因此,rush plot方便地创建了一个堆叠直方图。
$ rush plot --x tip --fill time tips.csv
=== === === 40 === === === === === === 30 === === === === time ===== === Dinner 20 ==+++ === ===== Lunch ==+++==== ===== === + ==+++==== ===== === === 10 +++++========== === === ====+++++++++=+++====+++== ===== ==+++++++++++++++++==+++++=+++====== ====== 0 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2.5 5.0 7.5 10.0 tip
图 7.5 显示了可视化图形。
允许我展现你可以会觉得有用的两种快捷语法. 两个惊叹号(!!)把前述的命令行代替. 惊叹号和美元符号(!$)把前面命令的最后部分代替, 就是文件名plot-histogram.png. 你可以看到, 更新后的命令首先被 ZShell 打印,所以你可以知道它到底执行了什么. 这两种快捷语法可以节省打字时间, 但它们不太容易被记住.
$ !! > plot-histogram.png
rush plot --x tip --fill time tips.csv > plot-histogram.png
$ display !$
display plot-histogram.png

图 7.5:直方图
该直方图显示,大多数小费在 2.5 美元左右。因为晚餐组和午餐组这两个组是相互叠加的,并且显示绝对计数,所以很难对它们进行比较。也许密度图可以对此有所帮助。
7.4.5 创建密度图
密度图对于可视化连续变量的分布非常有用。rush plot使用试探法来确定合适的几何图形,但您可以用geom选项覆盖它:
$ rush plot --x tip --fill time --geom density tips.csv
0.5 @@@ @@+@@ 0.4 @+++@@ @@++++@ @+++++@@ @@ 0.3 @++++++@@@@=@@ @++++++++@@===@@ time @+++++++++@@====@ Dinner 0.2 @+++++++++++@@@===@@ Lunch @+++++++++++++@@@==@@ @ @++++++++++++++++@@@@@@@ 0.1 @@+++++++++++++++++++++@@@@@@@ ++++++++++++++++++++++++++@++@@@@ ++++++++++++++++++++++++++++++++@@@@@@@@@@@ 0.0 ++++++++++++++++++++++++++++++++++++++++++@@@@@@@@@@@@@@@@@@@@@@ 2.5 5.0 7.5 10.0 tip
在这种情况下,与图 7.6 中的视觉表示相比,文字表示确实显示了其局限性。
$ rush plot --x tip --fill time --geom density tips.csv > plot-density.png
$ display plot-density.png

图 7.6:密度图
7.4.6 快乐的小意外
你已经看到了三种类型的可视化。在ggplot2中,这些对应于功能geom_bar、geom_histogram和geom_density。geom是geometry的缩写,表示实际绘制的内容。这个ggplot2的备忘单很好地概述了可用的几何类型。可以使用的几何图形类型取决于您指定的柱(及其类型)。不是每个组合都有意义。以这个线图为例。
$ rush plot --x tip --y bill --color size --size day --geom path tips.csv
50 #* * ##### # == #*** ****##### # ### =**+ #*** *****#### 40 # ##** ####***+====== *****### #*# ###******###+#** ======****### day ###################*****##*+*##*****####=# Fri b 30 # *+++########**##==****+****####### Sat i # #** *##++####**#===*%=====####*##**# Sun l # # ######***#=####==########## ** Thur l 20 # #########**#####****#####++ ########*########*###### # * size ###################### ## * 5 10 ########### #### ##* %%## ## # % 2.5 5.0 7.5 10.0 tip
这个快乐的小意外在图 7.7 的视觉表现中变得更加清晰。
$ rush plot --x tip --y bill --color size --size day --geom path tips.csv > plot
-accident.png
$ display plot-accident.png

图 7.7:一个快乐的小意外
tips.csv中的行是独立的观察值,而在数据点之间画一条线假设它们是相连的。最好用散点图来形象化tip和bill之间的关系。
7.4.7 创建散点图
在指定两个连续特征时,几何图形为点的散点图恰好是默认设置:
$ rush plot --x bill --y tip --color time tips.csv
10.0 = = 7.5 = = = + = t + = = time i = = = == + Dinner p 5.0 = = = + =+ = =+ + = Lunch = = =+==+++= = = += + = = += =++= ======== = = = = == =====+=====+=++ === = == = = 2.5 ++=++++=+=+==== == === = = == =+ ===+ + == =++ = = = = = = = 10 20 30 40 50 bill
注意每个点的颜色是用--color选项指定的(而不是用--fill选项)。直观表示见图 7.8。
$ rush plot --x bill --y tip --color time tips.csv > plot-scatter.png
$ display plot-scatter.png

图 7.8:散点图
从这个散点图中我们可以得出结论,账单金额和小费之间有关系。也许我们可以通过绘制趋势线从更高的层面来审视这些数据。
7.4.8 创建趋势线
如果您用smooth覆盖默认几何图形,您可以可视化趋势线。这些对于看到更大的画面是有用的。
$ rush plot --x bill --y tip --color time --geom smooth tips.csv
== ==== 7.5 ====== ======== ================== =======+++++++++====== t 5.0 ====+++++++++========== time i ====++++++================= Dinner p ===+++++++============= Lunch == ==+++++++===== = 2.5 ==============++++==== ======++++++++=== ========== ===== 0.0 == 10 20 30 40 50 bill
rush plot不能处理透明,所以在这种情况下,可视化表示(见图 7.9)要好得多。
$ rush plot --x bill --y tip --color time --geom smooth tips.csv > plot-trend.pn
g
$ display plot-trend.png

图 7.9:趋势线
如果你喜欢将原始点与趋势线一起可视化,你需要借助于用rush run编写ggplot2代码(见图 7.10)。
$ rush run --library ggplot2 'ggplot(df, aes(x = bill, y = tip, color = time)) +
geom_point() + geom_smooth()' tips.csv > plot-trend-points.png
$ display plot-trend-points.png

图 7.10:趋势线和原始点的组合
7.4.9 创建箱线图
对于一个或多个特征,箱线图显示五个数字的摘要:最小值、最大值、样本中值以及第一个和第三个四分位数。在这种情况下,我们需要使用factor()函数将size特征转换为分类特征,否则bill特征的所有值将被集中在一起。
$ rush plot --x 'factor(size)' --y bill --geom boxplot tips.csv
50 % % % % % % % 40 % % % % % %%%%%%%%%% %%%%%%%%%% % % % % %%%%%%%%%% b 30 % % % %%%%%%%%%% %%%%%%%%%% i % %%%%%%%%%%% %%%%%%%%%% l % % % %%%%%%%%%% l 20 %%%%%%%%%% %%%%%%%%%%% % % %%%%%%%%%% %%%%%%%%%%% %%%%%%%%%% % 10 %%%%%%%%%% % %%%%%%%%%% 1 2 3 4 5 6 factor(size)
虽然文字表现不算太差,但视觉表现要清晰得多(见图 7.11)。
$ rush plot --x 'factor(size)' --y bill --geom boxplot tips.csv > plot-boxplot.p
ng
$ display plot-boxplot.png

图 7.11:箱形图
不出所料,这个方框图显示,平均而言,聚会规模越大,费用越高。
7.4.10 添加标签
默认标签基于列名(或规范)。在之前的图片中,标签factor(size)应该有所改进。使用--xlab和--ylab选项,您可以覆盖 x 轴和 y 轴的标签。可以使用--title选项添加标题。这里有一个小提琴图(这是一个盒子图和密度图的混搭)演示了这一点(另见图 7.12)。
$ rush plot --x 'factor(size)' --y bill --geom violin --title 'Distribution of b
ill amount per party size' --xlab 'Party size' --ylab 'Bill (USD)' tips.csv
Distribution of bill amount per party size 50 % % %% %% % %% %% 40 % % %%% %%%%%% %% B % % % % % %%%% i % %%% % % %%%%%%%%%%%% % %% l 30 %% % % % %% %%%%% %%%%%% %%%%% l %%% % % % % %% %% % %% %% % % %%%%%% ( 20 %% %% % % %%%% U % %% %% %% S 10 %%%%%%%%%%%% %%% %% %%% D %%%%% %%%%% %%% ) %%%%%% 1 2 3 4 5 6 Party size
$ rush plot --x 'factor(size)' --y bill --geom violin --title 'Distribution of b
ill amount per party size' --xlab 'Party size' --ylab 'Bill (USD)' tips.csv > pl
ot-labels.png
$ display plot-labels.png

图 7.12:带有标题和标签的小提琴图
如果你想与他人(或你未来的自己)分享,用适当的标签和标题来注释你的可视化特别有用,以便更容易理解正在显示的内容。
7.4.11 超越基本绘图
虽然rush plot适合于在探索数据时创建基本的图表,但它肯定有其局限性。有时您需要更多的灵活性和复杂的选项,如多种几何图形、坐标转换和主题化。在这种情况下,可能值得了解更多关于rush plot利用其功能的底层包,即用于 R 的ggplot2包。当你对 Python 比对 R 更感兴趣时,还有plotnine包,它是用于 Python 的ggplot2的重新实现。
7.5 总结
在这一章中,我们已经研究了探索数据的各种方法。文本和图形数据可视化各有利弊。图形的质量显然要高得多,但是在命令行中查看可能有些棘手。这就是文本可视化派上用场的地方。由于有了R和ggplot2,至少rush有了创建这两种类型的一致语法。
下一章又是一个间奏曲章节,在这一章中,我将讨论如何提高命令和管道的速度。如果您迫不及待地想在第九章中开始对数据建模,请稍后阅读该章。
7.6 进一步探索
- 不幸的是,一本合适的教程超出了本书的范围。如果你想更好地可视化你的数据,我强烈建议你花一些时间去理解图形语法的力量和美丽。由 Hadley Wickham 和 Garrett Grolemund 所著的《面向数据科学的 R》一书的第三章和第 28 章是很好的参考资料。
** 说到第三章和第 28 章,我用 Plotnine 和 Pandas 把它们翻译成了 Python,以防你对 Python 比对 R 更感兴趣。
八、并行管道
原文:https://datascienceatthecommandline.com/2e/chapter-8-parallel-pipelines.html
在前面的章节中,我们一直在处理一次性处理整个任务的命令和管道。然而,在实践中,您可能会发现自己面临一个需要多次运行相同命令或管道的任务。例如,您可能需要:
- 抓取数百个网页
- 进行几十次 API 调用并转换它们的输出
- 为一系列参数值训练分类器
- 为数据集中的每对特征生成散点图
在上述任何一个例子中,都包含了某种形式的重复。使用您最喜欢的脚本或编程语言,您可以使用for循环或while循环来处理这个问题。在命令行上,您可能倾向于做的第一件事是按下Up来恢复之前的命令,如果需要的话对其进行修改,然后按下Enter来再次运行该命令。这样做两三次没问题,但是想象一下这样做几十次。这种方法很快变得繁琐、低效,并且容易出错。好消息是,您也可以在命令行上编写这样的循环。这就是本章的全部内容。
有时候,一个接一个地重复快速命令(以序列的方式)就足够了。当您拥有多个内核(甚至可能是多台机器)时,如果您能够利用这些内核就好了,尤其是当您面临数据密集型任务时。使用多个内核或机器时,总运行时间可能会显著减少。在这一章中,我将介绍一个非常强大的工具,叫做parallel,它可以处理好这一切。它使您能够对一系列参数(如数字、行和文件)应用命令或管道。另外,顾名思义,它允许您在并行中运行命令。
8.1 概述
本章讨论了几种加速需要多次运行命令和管道的任务的方法。我的主要目标是向你展示parallel的灵活性和力量。因为该工具可以与本书中讨论的任何其他工具相结合,所以它将积极地改变您使用命令行进行数据科学的方式。在本章中,您将了解:
- 对一系列数字、行和文件串行运行命令
- 将一个大任务分成几个小任务
- 并行运行管道
- 将管道分发到多台机器
本章从以下文件开始:
$ cd /data/ch08
$ l
total 20K
-rw-r--r-- 1 dst dst 126 Mar 3 10:51 emails.txt
-rw-r--r-- 1 dst dst 61 Mar 3 10:51 movies.txt
-rwxr-xr-x 1 dst dst 125 Mar 3 10:51 slow.sh*
-rw-r--r-- 1 dst dst 5.1K Mar 3 10:51 users.json
获取这些文件的说明在第二章中。任何其他文件都是使用命令行工具下载或生成的。
8.2 串行处理
在深入研究并行化之前,我将简要讨论串行循环。知道如何做到这一点是值得的,因为这个功能总是可用的,语法非常类似于其他编程语言中的循环,并且它将真正使您欣赏parallel。
从本章介绍中提供的例子中,我们可以提取三种类型的项目进行循环:数字、行和文件。这三种类型的项目将在接下来的三个小节中分别讨论。
8.2.1 数字上的循环
假设您需要计算 0 到 100 之间的每个偶数的平方。有一个叫做bc的工具,这是一个基本计算器,你可以用管道把一个方程。计算 4 的平方的命令如下所示:
$ echo "4^2" | bc
16
对于一次性计算,这就可以了。但是,正如介绍中提到的,你需要疯狂地按下Up,改变数字,并按下Enter 50 次!在这种情况下,最好让 Shell 通过使用for循环来为您完成困难的工作:
$ for i in {0..100..2} # ➊
> do
> echo "$i^2" | bc # ➋
> done | trim
0
4
16
36
64
100
144
196
256
324
… with 41 more lines
➊ ZShell 有一个特性叫做大括号扩展,将{0..100..2}转换成一个由空格分隔的列表: 0 2 4 … 98 100。变量i在第一次迭代中赋值0,在第二次迭代中赋值1,依此类推。
➌ 这个变量的值可以通过在它前面加一个美元符号($)来使用。Shell 将在执行echo之前用它的值替换$i。注意在do和done之间可以有多个命令。
虽然与您最喜欢的编程语言相比,语法可能显得有点奇怪,但是值得记住这一点,因为它在 Shell 中总是可用的。稍后我将介绍一种更好、更灵活的重复命令的方式。
8.2.2 行上的循环
第二种可以循环的项目是行。这些行可以来自文件或标准输入。这是一种非常通用的方法,因为这些行可以包含任何内容,包括:数字、日期和电子邮件地址。
假设你想给你所有的联系人发一封电子邮件。让我们首先使用免费的随机用户生成器 API 生成一些假用户:
$ curl -s "https://randomuser.me/api/1.2/?results=5&seed=dsatcl2e" > users.json
$ < users.json jq -r '.results[].email' > emails
$ bat emails
───────┬────────────────────────────────────────────────────────────────────────
│ File: emails
───────┼────────────────────────────────────────────────────────────────────────
1 │ selma.andersen@example.com
2 │ kent.clark@example.com
3 │ ditmar.niehaus@example.com
4 │ benjamin.robinson@example.com
5 │ paulo.muller@example.com
───────┴────────────────────────────────────────────────────────────────────────
你可以用while循环遍历来自emails的行:
$ while read line # ➊
> do
> echo "Sending invitation to ${line}." # ➋
> done < emails # ➌
Sending invitation to selma.andersen@example.com.
Sending invitation to kent.clark@example.com.
Sending invitation to ditmar.niehaus@example.com.
Sending invitation to benjamin.robinson@example.com.
Sending invitation to paulo.muller@example.com.
➊ 在这种情况下,您需要使用while循环,因为 ZShell 事先不知道输入包含多少行。
尽管在这种情况下line变量周围的花括号是不必要的(因为变量名不能包含句点),但这仍然是一个好的做法。
➌ 这个重定向也可以放在while之前。
您还可以通过指定特殊的文件标准输入/dev/stdin,以交互方式向while循环提供输入。完成后按Ctrl-D。
$ while read line; do echo "You typed: ${line}."; done < /dev/stdin
one
You typed: one.
two
You typed: two.
three
You typed: three.
但是这种方法有一个缺点,就是一旦你按下Enter,那一行输入的do和done之间的命令会立即运行。没有回头路了。
8.2.3 文件上的循环
在这一节中,我将讨论我们经常需要循环的第三种类型的项目:文件。
为了处理特殊字符,使用globbing(即路径名扩展)代替ls :
$ for chapter in /data/*
> do
> echo "Processing Chapter ${chapter}."
> done
Processing Chapter /data/ch02.
Processing Chapter /data/ch03.
Processing Chapter /data/ch04.
Processing Chapter /data/ch05.
Processing Chapter /data/ch06.
Processing Chapter /data/ch07.
Processing Chapter /data/ch08.
Processing Chapter /data/ch09.
Processing Chapter /data/ch10.
Processing Chapter /data/csvconf.
就像大括号展开一样,表达式/data/在被for循环处理之前,首先被 ZShell 展开成一个列表。
清单文件的一个更详细的替代是find,其中:
- 可以向下遍历目录
- 允许对诸如大小、访问时间和权限等属性进行详细搜索
- 处理特殊字符,如空格和换行符
例如,下面的find调用列出了目录/data下扩展名为csv且小于 2kb 的所有文件:
$ find /data -type f -name '*.csv' -size -2k
/data/ch03/tmnt-basic.csv
/data/ch03/tmnt-missing-newline.csv
/data/ch03/tmnt-with-header.csv
/data/ch05/irismeta.csv
/data/ch05/names-comma.csv
/data/ch05/names.csv
/data/ch07/datatypes.csv
8.3 并行处理
假设您有一个运行时间很长的工具,如下所示:
$ bat slow.sh
───────┬────────────────────────────────────────────────────────────────────────
│ File: slow.sh
───────┼────────────────────────────────────────────────────────────────────────
1 │ #!/bin/bash
2 │ echo "Starting job $1" | ts # ➊
3 │ duration=$((1+RANDOM%5)) # ➋
4 │ sleep $duration # ➌
5 │ echo "Job $1 took ${duration} seconds" | ts
───────┴────────────────────────────────────────────────────────────────────────
➊ ts增加一个时间戳。
➋ 魔法变量RANDOM调用一个内部 Bash 函数,返回一个 0 到 32767 之间的伪随机整数。将该整数除以 5 的余数加上 1 确保了duration在 1 和 5 之间。
➌ sleep暂停执行给定的秒数。
这个过程可能不会占用所有可用的资源。碰巧你需要运行这个命令很多次。例如,您需要下载一系列文件。
一种简单的并行化方法是在后台运行命令。让我们运行slow.sh三次:
$ for i in {A..C}; do
> ./slow.sh $i & # ➊
> done
[2] 385 # ➋
[3] 387
[4] 390
$ Mar 03 10:52:01 Starting job A
Mar 03 10:52:01 Starting job B
Mar 03 10:52:01 Starting job C
Mar 03 10:52:02 Job A took 1 seconds
[2] done ./slow.sh $i
$ Mar 03 10:52:04 Job C took 3 seconds
[4] + done ./slow.sh $i
$ Mar 03 10:52:05 Job B took 4 seconds
[3] + done ./slow.sh $i
$
➊ “与”号(&)将命令发送到后台,允许for循环立即继续下一次迭代。
➋ 这一行显示了 ZShell 给定的作业号和进程 ID,可以用于更细粒度的作业控制。这个话题虽然强大,但超出了本书的范围。
记住并不是所有的东西都可以并行化. API 函数可能只有一个特定的数字, 或者一些命令,只可能有 1 个实例。.
图 8.1 从概念层面上说明了串行处理、简单并行处理和使用 GNU Parallel 的并行处理在并发进程数量和运行所有事务所花费的总时间方面的区别。

图 8.1:串行处理、简单并行处理和使用 GNU Parallel 的并行处理
这种幼稚的方法有两个问题。首先,没有办法控制您同时运行多少个进程。如果您一次启动太多的作业,它们可能会竞争相同的资源,如 CPU、内存、磁盘访问和网络带宽。这可能会导致运行所有程序需要更长的时间。第二,很难区分哪个输出属于哪个输入。让我们看看更好的方法。
8.3.1 GNU Parallel 简介
请允许我介绍一下parallel,这是一个命令行工具,允许您并行化和分发命令和管道。这个工具的美妙之处在于,现有的工具可以原样使用;它们不需要修改。
有 2 个命令行工具有相同的名字parallel. 如果你使用 Docker 镜像那么你已经安装了正确的命令行工具了. 否则, 你可能要运行parallel --version检查下是否安装了正确的版本. 结果应该为GNU parallel。
在我深入讨论parallel的细节之前,这里有一个小笑话向你展示替换之前的for循环是多么容易:
$ seq 0 2 100 | parallel "echo {}^2 | bc" | trim
0
4
16
36
64
100
144
196
256
324
… with 41 more lines
这是parallel最简单的形式:要循环的项目通过标准输入传递,除了parallel需要运行的命令之外,没有任何参数。参见图 8.2 了解parallel如何在进程间并发分配输入并收集它们的输出。

图 8.2: GNU Parallel 同时在进程间分配输入并收集它们的输出
正如你所看到的,它基本上是一个for循环。这是另一个笑话,它取代了上一节中的for循环。
$ parallel --jobs 2 ./slow.sh ::: {A..C}
Mar 03 10:52:12 Starting job A
Mar 03 10:52:13 Job A took 1 seconds
Mar 03 10:52:12 Starting job B
Mar 03 10:52:16 Job B took 4 seconds
Mar 03 10:52:13 Starting job C
Mar 03 10:52:18 Job C took 4 seconds
这里,使用--jobs选项,我指定parallel最多可以同时运行两个作业。slow.sh的参数被指定为一个参数,而不是通过标准输入。
凭借多达 159 种不同的选项,parallel提供了大量的功能。(也许太多了。幸运的是,你只需要知道一小部分就能有效。如果您需要使用一个不常用的选项,手册页提供了很多信息。
8.3.2 指定输入
parallel最重要的参数是您希望为每个输入运行的命令或管道。问题是:输入项应该插入命令行的什么位置?如果不指定任何内容,那么输入项将被追加到管道的末尾。
$ seq 3 | parallel cowsay
___
< 1 >
---
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
___
< 2 >
---
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
___
< 3 >
---
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
以上与跑步相同:
$ cowsay 1 > /dev/null # ➊
$ cowsay 2 > /dev/null
$ cowsay 3 > /dev/null
➊ 因为输出和之前一样,所以我把它重定向到/dev/null来抑制它。
虽然这通常是可行的,但我建议您通过使用占位符来明确输入项应该插入到命令中的什么位置。在这种情况下,因为您想一次使用整个输入行(一个数字),所以您只需要一个占位符。您用一对花括号({})指定占位符,换句话说,指定输入项的位置:
$ seq 3 | parallel cowsay {} > /dev/null
有其他的方法提供parallel的输入. 我提倡用管道(就像我在整章中做的那样)因为那是大多数命令行工具串联在一起的工具. 另外一个方法是用不常见的语法. 不得不说的是, 它们确实增加了新的功能, 比如遍历多个数组的所有组合, 所以如果想了解更多,读下parallel的帮助手册
当输入项是文件名时,有几个修饰符可以只使用文件名的一部分。例如,使用{/},将只使用文件名的基本名称:
$ find /data/ch03 -type f | parallel echo '{#}\) \"{}\" has basename \"{/}\"' # ➊
1) "/data/ch03/tmnt-basic.csv" has basename "tmnt-basic.csv"
2) "/data/ch03/logs.tar.gz" has basename "logs.tar.gz"
3) "/data/ch03/tmnt-missing-newline.csv" has basename "tmnt-missing-newline.csv"
4) "/data/ch03/r-datasets.db" has basename "r-datasets.db"
5) "/data/ch03/top2000.xlsx" has basename "top2000.xlsx"
6) "/data/ch03/tmnt-with-header.csv" has basename "tmnt-with-header.csv"
➊ 括号())和引号(")等字符在 Shell 中有特殊的含义。要按字面意思使用它们,你要在它们前面加一个反斜杠\。这叫转义。
如果输入行有多个由分隔符分隔的部分,您可以向占位符添加数字。例如:
$ < input.csv parallel --colsep , "mv {2} {1}" > /dev/null
在这里,您可以应用相同的占位符修饰符。也可以重用相同的输入项。如果parallel的输入是一个带标题的 CSV 文件,那么您可以使用列名作为占位符:
$ < input.csv parallel -C, --header : "invite {name} {email}"
如果你想知道你的占位符是否设置正确, 你可以加上--dryrun选项. parallel将会打印出所有它将要执行的命令而不是真正的执行它们.
8.3.3 控制并发作业的数量
默认情况下,parallel在每个 CPU 内核上运行一个作业。您可以使用--jobs或-j选项控制同时运行的任务数量。指定一个数字意味着许多作业将同时运行。如果你在数字前面加一个加号,那么parallel将运行N个任务加上 CPU 核心的数量。如果你在数字前面加一个减号,那么parallel将运行N-M个任务。其中N是 CPU 内核的数量。您还可以指定一个百分比,默认值为 CPU 核心数的 100%。并发运行的作业的最佳数量取决于您正在运行的实际命令。
$ seq 5 | parallel -j0 "echo Hi {}"
Hi 1
Hi 3
Hi 2
Hi 4
Hi 5
$ seq 5 | parallel -j200% "echo Hi {}"
Hi 1
Hi 2
Hi 3
Hi 4
Hi 5
如果您指定-j1,那么命令将串行运行。即使这不做正义的工具的名称,它仍然有它的用途。例如,当您需要访问一个一次只允许一个连接的 API 时。如果您指定了-j0,那么parallel将会并行运行尽可能多的作业。这可以与您的带&符号的循环相比较。这是不可取的。
8.3.4 日志和输出
为了保存每个命令的输出,您可能会尝试以下操作:
$ seq 5 | parallel "echo \"Hi {}\" > hi-{}.txt"
这将把输出保存到单独的文件中。或者,如果您想将所有内容保存到一个大文件中,您可以执行以下操作:
$ seq 5 | parallel "echo Hi {}" >> one-big-file.txt
然而,parallel提供了--results选项,它将输出存储在单独的文件中。对于每个作业,parallel创建三个文件: seq,保存作业编号,stdout,包含作业产生的输出,stderr,包含作业产生的任何错误。这三个文件根据输入值放在子目录中。
parallel仍然打印所有的输出,在这种情况下是多余的。您可以将标准输入和标准输出重定向到/dev/null,如下所示:
$ seq 10 | parallel --results outdir "curl 'https://anapioficeandfire.com/api/ch
aracters/{}' | jq -r '.aliases[0]'" 2>/dev/null 1>&2
$ tree outdir | trim outdir
└── 1
├── 1
│ ├── seq
│ ├── stderr
│ └── stdout
├── 10
│ ├── seq
│ ├── stderr
│ └── stdout
… with 34 more lines
参见图 8.3 了解--results选项如何工作的图示概述。

图 8.3: GNU Parallel 使用--results选项将输出存储在单独的文件中
当您并行运行多个作业时,作业运行的顺序可能与输入的顺序不一致。因此,工作的产出也是混杂的。要保持相同的顺序,请指定--keep-order选项或-k选项。
有时,记录哪个输入生成了哪个输出是很有用的。parallel允许您用--tag选项标记输出,这将为每一行添加输入项。
$ seq 5 | parallel --tag "echo 'sqrt({})' | bc -l"
1 1
2 1.41421356237309504880
3 1.73205080756887729352
4 2.00000000000000000000
5 2.23606797749978969640
$ parallel --tag --keep-order "echo '{1}*{2}' | bc -l" ::: 3 4 ::: 5 6 7
3 5 15
3 6 18
3 7 21
4 5 20
4 6 24
4 7 28
8.3.5 创建并行工具
我在本章开始时使用的bc工具本身并不是并行的。但是,您可以使用parallel将其并行化。Docker 图像包含一个名为pbc的工具。它的代码如下所示:
$ bat $(which pbc)
───────┬────────────────────────────────────────────────────────────────────────
│ File: /usr/bin/dsutils/pbc
───────┼────────────────────────────────────────────────────────────────────────
1 │ #!/bin/bash
2 │ # pbc: parallel bc. First column of input CSV is mapped to {1}, second
│ to {2}, and so forth.
3 │ #
4 │ # Example usage: paste -d, <(seq 100) <(seq 100 -1 1) | ./pbc 'sqrt({1}
│ *{2})'
5 │ #
6 │ # Dependency: GNU parallel
7 │ #
8 │ # Author: http://jeroenjanssens.com
9 │
10 │ parallel -C, -k -j100% "echo '$1' | bc -l"
───────┴────────────────────────────────────────────────────────────────────────
这个工具也允许我们简化本章开头使用的代码。它可以同时处理逗号分隔的值:
$ seq 100 | pbc '{1}^2' | trim
1
4
9
16
25
36
49
64
81
100
… with 90 more lines
$ paste -d, <(seq 4) <(seq 4) <(seq 4) | pbc 'sqrt({1}+{2})^{3}'
1.41421356237309504880
4.00000000000000000000
14.69693845669906858905
63.99999999999999999969
8.4 分布式处理
有时你需要比你的本地机器更多的能量,即使它有所有的核心。幸运的是,parallel还可以利用远程机器的能力,这真的可以让你加快流水线的速度。
最棒的是parallel不必安装在远程机器上。所需要的就是你可以用安全 Shell 协议(或 SSH)连接到远程机器,这也是parallel用来分发你的管道的。(安装parallel很有帮助,因为它可以决定在每台远程机器上使用多少内核;稍后将详细介绍。)
首先,我将获得正在运行的 AWS EC2 实例的列表。如果您没有任何远程机器,也不用担心,您可以用--sshlogin :替换任何出现的--slf hostnames,它告诉parallel使用哪个远程机器。这样,您仍然可以遵循本节中的示例。
一旦您知道要接管哪些远程机器,我们将考虑三种类型的分布式处理:
- 在远程机器上运行普通命令
- 在远程机器之间直接分发本地数据
- 将文件发送到远程机器,处理它们,并检索结果
8.4.1 获取正在运行的 AWS EC2 实例列表
在本节中,我们将创建一个名为hostnames的文件,其中每行包含一个远程机器的主机名。我以亚马逊网络服务(AWS)为例。我假设您有一个 AWS 帐户,并且知道如何启动实例。如果你正在使用不同的云计算服务(比如谷歌云平台或微软 Azure),或者如果你有自己的服务器,请确保在继续下一部分之前,你自己创建了一个hostnames文件。
您可以使用 AWS API 的命令行接口aws获得正在运行的 AWS EC2 实例的列表。有了aws,你几乎可以用在线 AWS 管理控制台做所有你能做的事情。
命令aws ec2 describe-instances以 JSON 格式返回关于所有 EC2 实例的大量信息(更多信息请参见在线文档)。您可以使用jq提取相关字段:
$ aws ec2 describe-instances | jq '.Reservations[].Instances[] | {public_dns: .P
ublicDnsName, state: .State.Name}'
EC2 实例的可能状态有: pending,running,shutting-down,terminated,stopping和stopped。因为您只能将管道分发到正在运行的实例,所以您可以按如下方式过滤掉未运行的实例:
> aws ec2 describe-instances | jq -r '.Reservations[].Instances[] | select(.Stat
e.Name=="running") | .PublicDnsName' | tee hostnames
ec2-54-88-122-140.compute-1.amazonaws.com
ec2-54-88-89-208.compute-1.amazonaws.com
(如果没有-r或--raw-output选项,主机名就会被双引号括起来。)输出被保存到hostnames,以便我稍后可以将它传递给parallel。
如上所述,parallel采用了ssh来连接到远程机器。如果您想连接到 EC2 实例,而不是每次都键入凭证,那么您可以将类似下面的文本添加到文件~/.ssh/config中。
$ bat ~/.ssh/config
───────┬────────────────────────────────────────────────────────────────────────
│ File: /home/dst/.ssh/config
───────┼────────────────────────────────────────────────────────────────────────
1 │ Host *.amazonaws.com
2 │ IdentityFile ~/.ssh/MyKeyFile.pem
3 │ User ubuntu
───────┴────────────────────────────────────────────────────────────────────────
根据您运行的发行版,您的用户名可能不同于ubuntu。
8.4.2 在远程机器上运行命令
分布式处理的第一种风格是在远程机器上运行普通命令。让我们首先通过在每个 EC2 实例上运行工具hostname来仔细检查一下parallel是否在工作:
$ parallel --nonall --sshloginfile hostnames hostname
ip-172-31-23-204
ip-172-31-23-205
这里,--sshloginfile或--slf选项用于引用文件hostnames。--nonall选项指示parallel在不使用任何参数的情况下,在hostnames文件中的每台远程机器上执行相同的命令。记住,如果您没有任何远程机器可以利用,您可以用--sshlogin :替换--slf hostnames,这样命令就可以在您的本地机器上运行:
$ parallel --nonall --sshlogin : hostname
data-science-toolbox
在每台远程机器上运行相同的命令一次,每台机器只需要一个内核。如果您想将传入的参数列表分发给parallel,那么它可能会使用多个内核。如果没有明确指定核心的数量,parallel将尝试确定这一点。
$ seq 2 | parallel --slf hostnames echo 2>&1
bash: parallel: command not found
parallel: Warning: Could not figure out number of cpus on ec2-54-88-122-140.comp
ute-1.amazonaws.com (). Using 1.
1
2
在本例中,我在两台远程机器中的一台上安装了parallel。我收到一条警告消息,指出在其中一个上找不到parallel。因此,parallel无法确定核心的数量,将默认使用一个核心。当您收到此警告消息时,您可以执行以下四项操作之一:
- 不要担心,每台机器使用一个内核会让您很开心
- 通过
--jobs或-j选项指定每台机器的工作数量 - 指定每台机器要使用的内核数量,例如,如果您想要两个内核,可以在
hostnames文件中的每个主机名前面加上2/ - 使用软件包管理器安装
parallel。例如,如果远程机器都运行 Ubuntu:
$ parallel --nonall --slf hostnames "sudo apt-get install -y parallel"
8.4.3 在远程机器间分发本地数据
分布式处理的第二种风格是在远程机器之间直接分发本地数据。假设您有一个非常大的数据集,您想使用多台远程机器来处理它。为了简单起见,让我们对 1 到 1000 之间的所有整数求和。首先,让我们通过使用wc打印远程机器的主机名和它接收到的输入的长度,来仔细检查您的输入实际上是被分发的:
$ seq 1000 | parallel -N100 --pipe --slf hostnames "(hostname; wc -l) | paste -s
d:"
ip-172-31-23-204:100
ip-172-31-23-205:100
ip-172-31-23-205:100
ip-172-31-23-204:100
ip-172-31-23-205:100
ip-172-31-23-204:100
ip-172-31-23-205:100
ip-172-31-23-204:100
ip-172-31-23-205:100
ip-172-31-23-204:100
非常好。您可以看到您的 1000 个数字平均分布在 100 个子集上(由-N100指定)。现在,您可以对所有这些数字求和了:
$ seq 1000 | parallel -N100 --pipe --slf hostnames "paste -sd+ | bc" | paste -sd
500500
在这里,您还可以立即对从远程机器上获得的 10 笔金额进行求和。让我们通过在没有parallel的情况下进行相同的计算来检查答案是否正确:
$ seq 1000 | paste -sd+ | bc
500500
很好,这很有效。如果您有一个想要在远程机器上执行的更大的管道,您也可以将它放在一个单独的脚本中,并用parallel上传。我将通过创建一个名为add的非常简单的命令行工具来演示这一点:
$ echo '#!/usr/bin/env bash' > add
$ echo 'paste -sd+ | bc' >> add
$ bat add
───────┬────────────────────────────────────────────────────────────────────────
│ File: add
───────┼────────────────────────────────────────────────────────────────────────
1 │ #!/usr/bin/env bash
2 │ paste -sd+ | bc
───────┴────────────────────────────────────────────────────────────────────────
$ chmod u+x add
$ seq 1000 | ./add
500500
使用--basefile选项,parallel首先将文件上传到所有远程机器,然后运行作业:
$ seq 1000 |
> parallel -N100 --basefile add --pipe --slf hostnames './add' |
> ./add
500500
对 1000 个数求和当然只是一个玩具例子。另外,在本地进行会快得多。尽管如此,我还是希望从这里可以清楚地看到parallel可以变得无比强大。
8.4.4 在远程机器上处理文件
分布式处理的第三种风格是将文件发送到远程机器,处理它们,并检索结果。假设您想统计纽约市每个区接到 311 服务电话的频率。您的本地机器上还没有这些数据,所以让我们首先从免费的 NYC 开放数据 API 中获取这些数据:
$ seq 0 100 900 | parallel "curl -sL 'http://data.cityofnewyork.us/resource/erm
2-nwe9.json?\$limit=100&\$offset={}' | jq -c '.[]' | gzip > nyc-{#}.json.gz"
现在有 10 个包含压缩 JSON 数据的文件:
$ l nyc*json.gz
-rw-r--r-- 1 dst dst 16K Mar 3 10:55 nyc-10.json.gz
-rw-r--r-- 1 dst dst 14K Mar 3 10:53 nyc-1.json.gz
-rw-r--r-- 1 dst dst 15K Mar 3 10:53 nyc-2.json.gz
-rw-r--r-- 1 dst dst 16K Mar 3 10:54 nyc-3.json.gz
-rw-r--r-- 1 dst dst 15K Mar 3 10:54 nyc-4.json.gz
-rw-r--r-- 1 dst dst 15K Mar 3 10:53 nyc-5.json.gz
-rw-r--r-- 1 dst dst 15K Mar 3 10:54 nyc-6.json.gz
-rw-r--r-- 1 dst dst 15K Mar 3 10:54 nyc-7.json.gz
-rw-r--r-- 1 dst dst 15K Mar 3 10:54 nyc-8.json.gz
-rw-r--r-- 1 dst dst 16K Mar 3 10:54 nyc-9.json.gz
注意,jq -c '.[]'用于展平 JSON 对象的数组,这样每行有一个对象,每个文件总共有 100 行。使用zcat,你直接打印一个压缩文件的内容:
$ zcat nyc-1.json.gz | trim
{"unique_key":"53497809","created_date":"2022-03-02T01:59:41.000","agency":"EDC…
{"unique_key":"53496727","created_date":"2022-03-02T01:59:28.000","agency":"NYP…
{"unique_key":"53501332","created_date":"2022-03-02T01:58:14.000","agency":"NYP…
{"unique_key":"53502331","created_date":"2022-03-02T01:58:12.000","agency":"NYP…
{"unique_key":"53496515","created_date":"2022-03-02T01:56:51.000","agency":"NYP…
{"unique_key":"53501441","created_date":"2022-03-02T01:56:44.000","agency":"NYP…
{"unique_key":"53502239","created_date":"2022-03-02T01:54:11.000","agency":"NYP…
{"unique_key":"53495487","created_date":"2022-03-02T01:54:07.000","agency":"NYP…
{"unique_key":"53497370","created_date":"2022-03-02T01:53:59.000","agency":"NYP…
{"unique_key":"53502342","created_date":"2022-03-02T01:53:01.000","agency":"NYP…
… with 90 more lines
让我们看看一行 JSON 看起来像什么:
$ zcat nyc-1.json.gz | head -n 1
{"unique_key":"53497809","created_date":"2022-03-02T01:59:41.000","agency":"EDC"
,"agency_name":"Economic Development Corporation","complaint_type":"Noise - Heli
copter","descriptor":"Other","location_type":"Above Address","incident_zip":"100
03","incident_address":"103 2 AVENUE","street_name":"2 AVENUE","cross_street_1":
"EAST 6 STREET","cross_street_2":"NICHOLAS FIGUEROA WAY","intersection_street
_1":"EAST 6 STREET","intersection_street_2":"NICHOLAS FIGUEROA WAY","address_
type":"ADDRESS","city":"NEW YORK","landmark":"2 AVENUE","status":"In Progress","
community_board":"03 MANHATTAN","bbl":"1004620030","borough":"MANHATTAN","x_coor
dinate_state_plane":"987442","y_coordinate_state_plane":"204322","open_data_chan
nel_type":"ONLINE","park_facility_name":"Unspecified","park_borough":"MANHATTAN"
,"latitude":"40.7274928080516","longitude":"-73.98848345588063","location":{"lat
itude":"40.7274928080516","longitude":"-73.98848345588063","human_address":"{\"a
ddress\": \"\", \"city\": \"\", \"state\": \"\", \"zip\": \"\"}"},":@computed_re
gion_efsh_h5xi":"11724",":@computed_region_f5dn_yrer":"70",":@computed_region_ye
ji_bk3q":"4",":@computed_region_92fq_4b7q":"50",":@computed_region_sbqj_enih":"5
"}
如果您要获得本地机器上每个区的服务呼叫总数,您可以运行以下命令:
$ zcat nyc*json.gz | # ➊
> jq -r '.borough' | # ➋
> tr '[A-Z] ' '[a-z]_' | # ➌
> sort | uniq -c | sort -nr | # ➍
> awk '{print $2","$1}' | # ➎
> header -a borough,count | # ➏
> csvlook
│ borough │ count │
├───────────────┼───────┤
│ brooklyn │ 300 │
│ queens │ 235 │
│ manhattan │ 235 │
│ bronx │ 191 │
│ staten_island │ 38 │
│ unspecified │ 1 │
➊ 使用zcat展开所有压缩文件。
➋ 对于每个呼叫,使用jq提取行政区的名称。
➌ 将区名转换成小写,并用下划线替换空格(因为awk默认情况下会在空格上拆分)。
➍ 用sort和uniq统计每个区的出现次数。
➎ 反转两列,用逗号分隔,用awk分隔。
➏ 使用header添加表头。
想象一下,您自己的机器非常慢,您根本无法在本地执行这个管道。您可以使用parallel在远程机器之间分发本地文件,让它们进行处理,并检索结果:
$ ls *.json.gz | # ➊
> parallel -v --basefile jq \ # ➋
> --trc {.}.csv \ # ➌
> --slf hostnames \ # ➍
> "zcat {} | ./jq -r '.borough' | tr '[A-Z] ' '[a-z]_' | sort | uniq -c | awk '{
print \$2\",\"\$1}' > {.}.csv" # ➎
➊ 打印文件列表,并通过管道将其输入parallel
➋ 将jq二进制传输到每个远程机器。幸运的是,jq没有附属国。这个文件随后将从远程机器上删除,因为我指定了--trc选项(这意味着--cleanup选项)。注意流水线用的是./jq而不仅仅是jq。这是因为管道需要使用上传的版本,而不是可能在或可能不在搜索路径上的版本。
➌ 命令行参数--trc {.}.csv是--transfer --return {.}.csv --cleanup的简称。(替换字符串{.}被没有最后扩展名的输入文件名替换。)在这里,这意味着 JSON 文件被传输到远程机器,CSV 文件被返回到本地机器,并且这两个文件都将在远程机器的每个作业之后被删除
➍ 指定一个主机名列表。记住,如果你想在本地尝试一下,你可以指定--sshlogin :而不是--slf hostnames
➎ 注意awk表达式中的转义。引用有时会很棘手。在这里,美元符号和双引号被转义。如果引用变得太混乱,记得你把管道放到一个单独的命令行工具中,就像我用add做的那样
在这个过程中,如果您在一台远程机器上运行ls,您会看到parallel确实传输(并清理)了二进制文件jq、JSON 文件和 CSV 文件:
$ ssh $(head -n 1 hostnames) ls
每个 CSV 文件看起来都像这样:
> cat nyc-1.json.csv
bronx,3
brooklyn,5
manhattan,24
queens,3
staten_island,2
您可以使用rush和 tidyverse 对每个 CSV 文件中的计数求和:
$ cat nyc*csv | header -a borough,count |
> rush run -t 'group_by(df, borough) %>% summarize(count = sum(count))' - |
> csvsort -rc count | csvlook
│ borough │ count │
├───────────────┼───────┤
│ brooklyn │ 300 │
│ manhattan │ 235 │
│ queens │ 235 │
│ bronx │ 191 │
│ staten_island │ 38 │
│ unspecified │ 1 │
或者,如果您喜欢使用 SQL 来汇总结果,您可以使用第五章中讨论的csvsql:
$ cat nyc*csv | header -a borough,count |
> csvsql --query 'SELECT borough, SUM(count) AS count FROM stdin GROUP BY boroug
h ORDER BY count DESC' |
> csvlook
│ borough │ count │
├───────────────┼───────┤
│ brooklyn │ 300 │
│ queens │ 235 │
│ manhattan │ 235 │
│ bronx │ 191 │
│ staten_island │ 38 │
│ unspecified │ 1 │
8.5 总结
作为一名数据科学家,您需要处理数据,有时会处理大量数据。这意味着有时您需要多次运行一个命令,或者将数据密集型命令分布到多个内核上。在本章中,我已经向您展示了并行化命令是多么容易。是一个非常强大和灵活的工具,可以加速普通命令行工具并分发它们。它提供了许多功能,在这一章中,我只能够触及表面。在下一章中,我将介绍 OSEMN 模型的第四步:数据建模。
8.6 进一步探索
- 一旦你对
parallel及其最重要的选项有了基本的了解,我推荐你看看在线教程。您将学习如何指定不同的输入方式,保存所有作业的日志,以及如何超时、恢复和重试作业。正如本教程中 Ole Tange 的创建者所说,“你的命令行会喜欢它的”。
九、建模数据
原文:https://datascienceatthecommandline.com/2e/chapter-9-modeling-data.html
在本章中,我们将执行 OSEMN 模型的第四步:数据建模。一般来说,模型是对数据的抽象或更高层次的描述。建模有点像创建可视化,因为我们从单个数据点后退一步来看更大的画面。
可视化以形状、位置和颜色为特征:我们可以通过观察来解释它们。另一方面,模型的内在特征是数字,这意味着计算机可以使用它们来做一些事情,比如对新的数据点进行预测。(我们仍然可以将模型可视化,以便我们可以尝试理解它们,并了解它们的表现。)
在本章中,我将考虑三种常用于数据建模的算法:
- 降维
- 回归
- 分类
这些算法来自统计学和机器学习领域,所以我要稍微改变一下词汇。假设我有一个 CSV 文件,也称为数据集 。除了标题,每一行都被认为是一个数据点 。每个数据点都有一个或多个特征 ,或者已经被测量的属性。有时候,一个数据点也有一个标签 ,一般来说就是一个判断或者结果。当我在下面介绍葡萄酒数据集时,这变得更加具体。
第一种类型的算法(维度缩减)通常是无监督的,这意味着它们仅基于数据集的特征来创建模型。最后两种类型的算法(回归和分类)根据定义是监督算法,这意味着它们也将标签合并到模型中。
这章不是介绍机器学习,那意味着我会跳过很多细节. 我的建议是你在使用到自己的数据集之前熟悉下算法. 这章的末尾我推荐了一些关于机器学习的书籍.
9.1 概述
在本章中,您将学习如何:
- 使用
tapkee减少数据集的维数。 - 使用
vw预测白酒质量。 - 使用
skll将葡萄酒分类为红葡萄酒或白葡萄酒。
本章从以下文件开始:
$ cd /data/ch09
$ l
total 4.0K
-rw-r--r-- 1 dst dst 503 Mar 3 10:55 classify.cfg
获取这些文件的说明在第二章中。任何其他文件都是使用命令行工具下载或生成的。
请再来点酒!
在这一章中,我将使用一组品酒师对名为 vinho verde 的葡萄牙红酒和白酒的记录。每个数据点代表一种葡萄酒。每种葡萄酒都有 11 个理化特性:(1)固定酸度,(2)挥发性酸度,(3)柠檬酸,(4)残糖,(5)氯化物,(6)游离二氧化硫,(7)总二氧化硫,(8)密度,(9)pH 值,(10)硫酸盐,和(11)酒精。还有一个总体质量分数在 0(很差)到 10(优秀)之间,这是葡萄酒专家至少三次评价的中位数。关于这个数据集的更多信息可以在 UCI 机器学习库获得。
数据集被分成两个文件:一个用于白葡萄酒,一个用于红葡萄酒。第一步是使用curl获得这两个文件(当然还有parallel,因为我没有一整天的时间):
$ parallel "curl -sL http://archive.ics.uci.edu/ml/machine-learning-databases/wi
ne-quality/winequality-{}.csv > wine-{}.csv" ::: red white
三重冒号只是将数据传递给parallel的另一种方式。
让我们检查这两个文件,并计算行数:
$ < wine-red.csv nl | # ➊
> fold | # ➋
> trim
1 "fixed acidity";"volatile acidity";"citric acid";"residual sugar";"chlor
ides";"free sulfur dioxide";"total sulfur dioxide";"density";"pH";"sulphates";"a
lcohol";"quality"
2 7.4;0.7;0;1.9;0.076;11;34;0.9978;3.51;0.56;9.4;5
3 7.8;0.88;0;2.6;0.098;25;67;0.9968;3.2;0.68;9.8;5
4 7.8;0.76;0.04;2.3;0.092;15;54;0.997;3.26;0.65;9.8;5
5 11.2;0.28;0.56;1.9;0.075;17;60;0.998;3.16;0.58;9.8;6
6 7.4;0.7;0;1.9;0.076;11;34;0.9978;3.51;0.56;9.4;5
7 7.4;0.66;0;1.8;0.075;13;40;0.9978;3.51;0.56;9.4;5
8 7.9;0.6;0.06;1.6;0.069;15;59;0.9964;3.3;0.46;9.4;5
… with 1592 more lines
$ < wine-white.csv nl | fold | trim
1 "fixed acidity";"volatile acidity";"citric acid";"residual sugar";"chlor
ides";"free sulfur dioxide";"total sulfur dioxide";"density";"pH";"sulphates";"a
lcohol";"quality"
2 7;0.27;0.36;20.7;0.045;45;170;1.001;3;0.45;8.8;6
3 6.3;0.3;0.34;1.6;0.049;14;132;0.994;3.3;0.49;9.5;6
4 8.1;0.28;0.4;6.9;0.05;30;97;0.9951;3.26;0.44;10.1;6
5 7.2;0.23;0.32;8.5;0.058;47;186;0.9956;3.19;0.4;9.9;6
6 7.2;0.23;0.32;8.5;0.058;47;186;0.9956;3.19;0.4;9.9;6
7 8.1;0.28;0.4;6.9;0.05;30;97;0.9951;3.26;0.44;10.1;6
8 6.2;0.32;0.16;7;0.045;30;136;0.9949;3.18;0.47;9.6;6
… with 4891 more lines
$ wc -l wine-{red,white}.csv
1600 wine-red.csv
4899 wine-white.csv
6499 total
➊ 为了清晰起见,我使用nl来添加行号。
➋ 为了看到整个标题,我用了fold。
乍一看,这些数据似乎很干净。尽管如此,让我们清理它,使它更符合大多数命令行工具的期望。具体来说,我将:
- 将标题转换为小写。
- 用逗号替换分号。
- 用下划线替换空格。
- 删除不必要的引号。
工具tr可以处理所有这些事情。看在过去的份上,这次让我们使用for循环来处理这两个文件:
$ for COLOR in red white; do
> < wine-$COLOR.csv tr '[A-Z]; ' '[a-z],_' | tr -d \" > wine-${COLOR}-clean.csv
> done
让我们通过合并这两个文件来创建一个数据集。我将使用csvstack添加一个名为type的列,第一个文件的行将为red,第二个文件的行为white:
$ csvstack -g red,white -n type wine-{red,white}-clean.csv | # ➊
> xsv select 2-,1 > wine.csv # ➋
➊ 新列type由csvstack放置在开头。
➋ 有些算法假设标签是最后一列,所以我用xsv把列type移到最后。
检查该数据集中是否有任何缺失值是一种很好的做法,因为大多数机器学习算法都无法处理它们:
$ csvstat wine.csv --nulls
1\. fixed_acidity: False
2\. volatile_acidity: False
3\. citric_acid: False
4\. residual_sugar: False
5\. chlorides: False
6\. free_sulfur_dioxide: False
7\. total_sulfur_dioxide: False
8\. density: False
9\. ph: False
10\. sulphates: False
11\. alcohol: False
12\. quality: False
13\. type: False
太棒了。如果有任何缺失值,我们可以用该特性的平均值或最常见值来填充它们。另一种不太微妙的方法是删除至少有一个缺失值的数据点。出于好奇,我们来看看红葡萄酒和白葡萄酒的质量分布是什么样的。
$ rush run -t 'ggplot(df, aes(x = quality, fill = type)) + geom_density(adjust =
3, alpha = 0.5)' wine.csv > wine-quality.png
$ display wine-quality.png

使用密度图比较红葡萄酒和白葡萄酒的质量
从密度图中,你可以看到白葡萄酒的质量越来越高。这是否意味着白葡萄酒总体上比红葡萄酒好,或者白葡萄酒专家比红葡萄酒专家更容易给出更高的分数?这是数据没有告诉我们的。或者酒精和质量之间可能有某种关系?让我们用rush来了解一下:
$ rush plot --x alcohol --y quality --color type --geom smooth wine.csv > wine-a
lcohol-vs-quality.png
$ display wine-alcohol-vs-quality.png

葡萄酒的酒精含量与其质量之间的关系
找到了。咳咳,我们继续做模特吧,好吗?
9.3 将 Tapkee 用于降维
降维的目标是将高维数据点映射到更低维的映射上。挑战是在低维映射上保持相似的数据点紧密地在一起。正如我们在上一节中看到的,我们的葡萄酒数据集包含 13 个特征。我将坚持二维,因为这是直观的。
降维通常被认为是探索的一部分。当有太多的特征需要绘制时,这很有用。你可以做一个散点图矩阵,但是一次只能显示两个特征。它也可以作为其他机器学习算法的预处理步骤。
大多数降维算法是无监督的。这意味着他们不使用数据点的标签来构建低维映射。
在本节中,我将介绍两种技术:PCA,代表主成分分析,和 T-SNE,代表 t-分布式随机邻居嵌入 。
9.3.1 Tapkee 介绍
Tapkee 是一个降维的 C++模板库 。该库包含许多降维算法的实现,包括:
- 局部线性嵌入
- Isomap
- 多维排列
- PCA
- T-SNE
关于这些算法的更多信息可以在 Tapkee 的网站上找到。虽然 Tapkee 主要是一个可以包含在其他应用中的库,但它也提供了一个命令行工具tapkee。我将使用它对我们的葡萄酒数据集进行降维。
9.3.2 线性和非线性映射
首先,我将使用标准化来扩展特性,使得每个特性都同等重要。当应用机器学习算法时,这通常会导致更好的结果。
为了扩展,我使用了rush和tidyverse包。
$ rush run --tidyverse --output wine-scaled.csv \
> 'select(df, -type) %>% # ➊
> scale() %>% # ➋
> as_tibble() %>% # ➌
> mutate(type = df$type)' wine.csv # ➍
$ csvlook wine-scaled.csv
│ fixed_acidity │ volatile_acidity │ citric_acid │ residual_sugar │ chlorides │…
├───────────────┼──────────────────┼─────────────┼────────────────┼───────────┤…
│ 0.142… │ 2.189… │ -2.193… │ -0.745… │ …
│ 0.451… │ 3.282… │ -2.193… │ -0.598… │ …
│ 0.451… │ 2.553… │ -1.917… │ -0.661… │ …
│ 3.074… │ -0.362… │ 1.661… │ -0.745… │ …
│ 0.142… │ 2.189… │ -2.193… │ -0.745… │ …
│ 0.142… │ 1.946… │ -2.193… │ -0.766… │ …
│ 0.528… │ 1.581… │ -1.780… │ -0.808… │ …
│ 0.065… │ 1.885… │ -2.193… │ -0.892… │ …
… with 6489 more lines
➊ 我需要临时删除列type,因为scale()只对数字列有效。
➋ scale()函数接受数据帧,但返回一个矩阵。
➌ 函数as_tibble()将矩阵转换回数据帧。
➍ 最后,我把type一栏加回去。
现在,我们应用两种降维技术,并使用Rio-scatter可视化映射:
$ xsv select '!type' wine-scaled.csv | # ➊
> header -d | # ➋
> tapkee --method pca | # ➌
> tee wine-pca.txt | trim
-0.568882,3.34818
-1.19724,3.22835
-0.952507,3.23722
-1.60046,1.67243
-0.568882,3.34818
-0.556231,3.15199
-0.53894,2.28288
1.104,2.56479
0.231315,2.86763
-1.18363,1.81641
… with 6487 more lines
➊ 取消选择列type
➋ 删除标题
➌ 应用 PCA
$ < wine-pca.txt header -a pc1,pc2 | # ➊
> paste -d, - <(xsv select type wine-scaled.csv) | # ➋
> tee wine-pca.csv | csvlook
│ pc1 │ pc2 │ type │
├──────────┼─────────┼───────┤
│ -0.569… │ 3.348… │ red │
│ -1.197… │ 3.228… │ red │
│ -0.953… │ 3.237… │ red │
│ -1.600… │ 1.672… │ red │
│ -0.569… │ 3.348… │ red │
│ -0.556… │ 3.152… │ red │
│ -0.539… │ 2.283… │ red │
│ 1.104… │ 2.565… │ red │
… with 6489 more lines
➊ 加回表头pc1和pc2
➋ 加回列type
现在我们可以创建一个散点图:
$ rush plot --x pc1 --y pc2 --color type --shape type wine-pca.csv > wine-pca.pn
g
$ display wine-pca.png

图 9.1:使用 PCA 进行线性降维
让我们用同样的方法执行 T-SNE:
$ xsv select '!type' wine-scaled.csv | # ➊
> header -d | # ➋
> tapkee --method T-SNE | # ➌
> header -a x,y | # ➍
> paste -d, - <(xsv select type wine-scaled.csv) | # ➎
> rush plot --x x --y y --color type --shape type > wine-tsne.png # ➏
➊ 取消选择列type
➋ 删除表头
➌ 应用 T-SNE
➍ 添加回表头与列x``y
➎ 添加回列type
➏ 创建散点图
$ display wine-tsne.png

图 9.2:使用 T-SNE 进行非线性降维
我们可以看到,在根据红葡萄酒和白葡萄酒的物理化学特性来区分红葡萄酒和白葡萄酒方面,T-SNE 比 PCA 做得更好。这些散点图验证了数据集具有一定的结构;特征和标签之间是有关系的。知道了这一点,我很乐意通过应用监督机器学习来前进。我将从回归任务开始,然后继续分类任务。
9.4 将 Vowpal Wabbit 用于回归
在这一部分,我将创建一个模型,根据白葡萄酒的物理化学性质来预测白葡萄酒的质量。因为质量是一个介于 0 和 10 之间的数字,所以我们可以将此视为一个回归任务。
为此,我将使用 Vowpal Wabbit,或vw。
9.4.1 准备数据
与 CSV 不同,vw有自己的数据格式。工具csv2vw顾名思义,可以将 CSV 转换成这种格式。--label选项用于指示哪一列包含标签。让我们来看看结果:
$ csv2vw wine-white-clean.csv --label quality | trim
6 | alcohol:8.8 chlorides:0.045 citric_acid:0.36 density:1.001 fixed_acidity:7 …
6 | alcohol:9.5 chlorides:0.049 citric_acid:0.34 density:0.994 fixed_acidity:6.…
6 | alcohol:10.1 chlorides:0.05 citric_acid:0.4 density:0.9951 fixed_acidity:8.…
6 | alcohol:9.9 chlorides:0.058 citric_acid:0.32 density:0.9956 fixed_acidity:7…
6 | alcohol:9.9 chlorides:0.058 citric_acid:0.32 density:0.9956 fixed_acidity:7…
6 | alcohol:10.1 chlorides:0.05 citric_acid:0.4 density:0.9951 fixed_acidity:8.…
6 | alcohol:9.6 chlorides:0.045 citric_acid:0.16 density:0.9949 fixed_acidity:6…
6 | alcohol:8.8 chlorides:0.045 citric_acid:0.36 density:1.001 fixed_acidity:7 …
6 | alcohol:9.5 chlorides:0.049 citric_acid:0.34 density:0.994 fixed_acidity:6.…
6 | alcohol:11 chlorides:0.044 citric_acid:0.43 density:0.9938 fixed_acidity:8.…
… with 4888 more lines
在这种格式中,每行是一个数据点。该行以标签开始,后跟管道符号,然后是由空格分隔的特征名称/值对。虽然与 CSV 格式相比,这种格式可能显得过于冗长,但它确实提供了更多的灵活性,例如权重、标签、名称空间和稀疏的特征表示。对于葡萄酒数据集,我们不需要这种灵活性,但在将vw应用于更复杂的问题时,这可能会很有用。这篇文章更详细地解释了vw格式。
一个是我们创建的,或者说是训练的回归模型,它可以用来预测新的、看不见的数据点。换句话说,如果我们给模型一种它从未见过的酒,它可以预测,或者测试,它的质量。为了正确评估这些预测的准确性,我们需要留出一些不会用于训练的数据。通常将完整数据集的 80% 用于训练,剩下的 20% 用于测试。
我可以这样做,首先使用split将完整的数据集分成五个相等的部分。我使用wc验证每个部分的数据点数量。
$ csv2vw wine-white-clean.csv --label quality |
> shuf | # ➊
> split -d -n r/5 - wine-part-
$ wc -l wine-part-*
980 wine-part-00
980 wine-part-01
980 wine-part-02
979 wine-part-03
979 wine-part-04
4898 total
➊ 工具shuf对数据集进行随机化,以确保训练和测试都具有相似的质量分布。
现在,我可以将第一部分(20%)用于测试集wine-test.vw,并将其余四部分(80%)合并到训练集wine-train.vw:
$ mv wine-part-00 wine-test.vw
$ cat wine-part-* > wine-train.vw
$ rm wine-part-*
$ wc -l wine-*.vw
980 wine-test.vw
3918 wine-train.vw
4898 total
现在我们准备使用vw训练一个模型。
9.4.2 训练模型
工具vw接受许多不同的选项(将近 400 个!).幸运的是,你不需要全部都有效。为了注释我在这里使用的选项,我将把每个选项放在单独的一行上:
$ vw \
> --data wine-train.vw \ # ➊
> --final_regressor wine.model \ # ➋
> --passes 10 \ # ➌
> --cache_file wine.cache \ # ➍
> --nn 3 \ # ➎
> --quadratic :: \ # ➏
> --l2 0.000005 \ # ➐
> --bit_precision 25 ➑
creating quadratic features for pairs: ::
WARNING: any duplicate namespace interactions will be removed
You can use --leave_duplicate_interactions to disable this behaviour.
using l2 regularization = 5e-06
final_regressor = wine.model
Num weight bits = 25
learning rate = 0.5
initial_t = 0
power_t = 0.5
decay_learning_rate = 1
creating cache_file = wine.cache
Reading datafile = wine-train.vw
num sources = 1
Enabled reductions: gd, generate_interactions, nn, scorer
average since example example current current current
loss last counter weight label predict features
16.000000 16.000000 1 1.0 4.0000 0.0000 78
16.640850 17.281700 2 2.0 5.0000 0.8429 78
19.862713 23.084575 4 4.0 7.0000 1.4240 78
17.263957 14.665201 8 8.0 5.0000 2.1202 78
14.425717 11.587478 16 16.0 6.0000 2.8387 78
11.653072 8.880427 32 32.0 6.0000 3.9516 78
7.180206 2.707339 64 64.0 7.0000 4.8593 78
4.043363 0.906520 128 128.0 6.0000 5.5736 78
2.438232 0.833101 256 256.0 6.0000 5.8213 78
1.600178 0.762125 512 512.0 6.0000 6.0160 78
1.165957 0.731735 1024 1024.0 6.0000 6.0026 78
0.968015 0.770074 2048 2048.0 5.0000 6.0260 78
0.802674 0.802674 4096 4096.0 5.0000 5.3271 78 h
0.736162 0.669649 8192 8192.0 5.0000 5.4833 78 h
0.687028 0.637895 16384 16384.0 8.0000 5.5832 78 h
0.639125 0.591222 32768 32768.0 5.0000 5.8400 78 h
finished run
number of examples per pass = 3527
passes used = 10
weighted example sum = 35270.000000
weighted label sum = 207590.000000
average loss = 0.590422 h
best constant = 5.885738
total feature number = 2749620
➊ 文件wine-train.vw用于训练模型。
➋ 模型,或回归值,将存储在文件wine.model中。
➌ 培训次数。
➍ 进行多遍时需要缓存。
➎ 使用一个有三个隐藏单元的神经网络。
➏ 基于所有输入特征创建并使用二次特征。任何重复都将被vw删除。
➐ 使用 l2 正则化。
➑ 使用 25 位来存储特征。
现在我已经训练了一个回归模型,让我们用它来做预测。
9.4.3 测试模型
该模型存储在文件wine.model中。为了使用该模型进行预测,我再次运行vw,但是现在使用了一组不同的选项:
$ vw \
> --data wine-test.vw \ # ➊
> --initial_regressor wine.model \ # ➋
> --testonly \ # ➌
> --predictions predictions \ # ➍
> --quiet # ➎
$ bat predictions | trim
5.638143
5.072958
4.776097
5.619643
4.331424
5.306047
5.895846
6.500000
5.143466
6.041055
… with 970 more lines
➊ 文件wine-test.vw用于测试模型。
➋ 使用存储在文件wine.model中的模式。
➌ 忽略标签信息,只进行测试。
➍ 这些预测存储在一个名为预测的文件中。
➎ 不输出诊断和进度更新。
让我们使用paste将文件预测中的预测与文件wine-test.vw中的真实值或观察值相结合。使用awk,我可以将预测值与观察值进行比较,并计算平均绝对误差(MAE)。当预测白葡萄酒的质量时,MAE 告诉我们vw平均有多远。
$ paste -d, predictions <(cut -d '|' -f 1 wine-test.vw) |
> tee results.csv |
> awk -F, '{E+=sqrt(($1-$2)^2)} END {print "MAE: " E/NR}' |
> cowsay # ➊
____________
< MAE: 0.603 >
------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
因此,这些预测平均误差约为 0.6 个百分点。让我们用rush plot可视化观察值和预测值之间的关系:
$ < results.csv header -a "predicted,observed" |
> rush plot --x observed --y predicted --geom jitter > wine-regression.png
$ display wine-regression.png

图 9.3:用 Vowpal Wabbit 进行回归
我可以想象,用于训练模型的选项可能有点多。让我们看看当我使用所有默认值时vw的表现如何:
$ vw -d wine-train.vw -f wine2.model --quiet # ➊
$ vw -data wine-test.vw -i wine2.model -t -p predictions --quiet # ➋
$ paste -d, predictions <(cut -d '|' -f 1 wine-test.vw) | # ➌
> awk -F, '{E+=sqrt(($1-$2)^2)} END {print "MAE: " E/NR}'
MAE: 0.615643
➊ 训练回归模型
➋检验回归模型
➌计算平均绝对误差
显然,使用默认值时,MAE 要高 0.04,这意味着预测稍微差一些。
在这一部分,我只能触及vw所能做的事情的表面。它接受如此多的选项是有原因的。除了回归之外,它还支持二元分类、多类分类、强化学习和潜在的狄利克雷分配。其网站包含许多教程和文章,以了解更多信息。
9.5 将 SciKit-Learn Laboratory 用于分类
在这一部分,我将训练一个分类模型,或者说分类器 ,它可以预测一款葡萄酒是红葡萄酒还是白葡萄酒。虽然我们可以使用vw来做这件事,但我想演示另一个工具:SciKit-Learn Laboratory(SKLL)。顾名思义,它构建在 SciKit-Learn 之上,SciKit-Learn 是一个流行的 Python 机器学习包。SKLL 本身是一个 Python 包,它提供了run_experiment工具,这使得从命令行使用 SciKit-Learn 成为可能。我使用别名skll而不是run_experiment,因为我发现它更容易记住,因为它对应于包名:
$ alias skll=run_experiment
$ skll
usage: run_experiment [-h] [-a NUM_FEATURES] [-A] [-k] [-l] [-m MACHINES]
[-q QUEUE] [-r] [-v] [--version]
config_file [config_file ...]
run_experiment: error: the following arguments are required: config_file
9.5.1 准备数据
skll期望位于不同目录中的训练和测试数据集具有相同的文件名。因为它的预测不一定与原始数据集的顺序相同,所以我添加了一个列id,它包含一个惟一的标识符,这样我就可以将预测与正确的数据点匹配起来。让我们创建一个平衡的数据集:
$ NUM_RED="$(< wine-red-clean.csv wc -l)" # ➊
$ csvstack -n type -g red,white \ # ➋
> wine-red-clean.csv \
> <(< wine-white-clean.csv body shuf | head -n $NUM_RED) |
> body shuf |
> nl -s, -w1 -v0 | # ➌
> sed '1s/0,/id,/' | # ➍
> tee wine-balanced.csv | csvlook
│ id │ type │ fixed_acidity │ volatile_acidity │ citric_acid │ residual_sug…
├───────┼───────┼───────────────┼──────────────────┼─────────────┼─────────────…
│ 1 │ red │ 6.00 │ 0.500 │ 0.04 │ 2.…
│ 2 │ white │ 8.00 │ 0.270 │ 0.57 │ 10.…
│ 3 │ red │ 10.10 │ 0.280 │ 0.46 │ 1.…
│ 4 │ red │ 8.90 │ 0.840 │ 0.34 │ 1.…
│ 5 │ red │ 8.70 │ 0.780 │ 0.51 │ 1.…
│ 6 │ red │ 12.80 │ 0.300 │ 0.74 │ 2.…
│ 7 │ red │ 6.60 │ 0.695 │ 0.00 │ 2.…
│ 8 │ red │ 6.40 │ 0.560 │ 0.15 │ 1.…
… with 3190 more lines
➊ 在变量NUM_RED中存储红酒的数量。
➋ 将所有红酒与随机抽取的白葡萄酒混合。
➌ 在每行前使用nl添加“行号”。
➍ 将第一行的0替换为id,这样它就是一个正确的列名。
让我们将这个平衡的数据集分成一个训练集和一个测试集:
$ mkdir -p {train,test}
$ HEADER="$(< wine-balanced.csv header)"
$ < wine-balanced.csv header -d | shuf | split -d -n r/5 - wine-part-
$ wc -l wine-part-*
640 wine-part-00
640 wine-part-01
640 wine-part-02
639 wine-part-03
639 wine-part-04
3198 total
$ cat wine-part-00 | header -a $HEADER > test/features.csv && rm wine-part-00
$ cat wine-part-* | header -a $HEADER > train/features.csv && rm wine-part-*
$ wc -l t*/features.csv
641 test/features.csv
2559 train/features.csv
3200 total
现在我有了一个平衡的训练数据集和一个平衡的测试数据集,我可以继续构建一个分类器。
9.5.2 运行实验
在skll中训练一个分类器是通过在配置文件中定义一个实验来完成的。它由几个部分组成,例如,指定在哪里寻找数据集,哪些分类器这里是我将使用的配置文件classify.cfg:
$ bat classify.cfg
───────┬────────────────────────────────────────────────────────────────────────
│ File: classify.cfg
───────┼────────────────────────────────────────────────────────────────────────
1 │ [General]
2 │ experiment_name = wine
3 │ task = evaluate
4 │
5 │ [Input]
6 │ train_directory = train
7 │ test_directory = test
8 │ featuresets = [["features"]]
9 │ feature_scaling = both
10 │ label_col = type
11 │ id_col = id
12 │ shuffle = true
13 │ learners = ["KNeighborsClassifier", "LogisticRegression", "DecisionTree
│ Classifier", "RandomForestClassifier"]
14 │ suffix = .csv
15 │
16 │ [Tuning]
17 │ grid_search = false
18 │ objectives = ["neg_mean_squared_error"]
19 │ param_grids = [{}, {}, {}, {}]
20 │
21 │ [Output]
22 │ logs = output
23 │ results = output
24 │ predictions = output
25 │ models = output
───────┴────────────────────────────────────────────────────────────────────────
我使用skll运行实验:
$ skll -l classify.cfg 2>/dev/null
选项-l指定在本地模式下运行。skll还提供了在集群上运行实验的可能性。运行一个实验所需的时间取决于所选算法的复杂性和数据的大小。
9.5.3 解析结果
一旦所有分类器被训练和测试,结果可以在目录output中找到:
$ ls -1 output
wine_features_DecisionTreeClassifier.log
wine_features_DecisionTreeClassifier.model
wine_features_DecisionTreeClassifier_predictions.tsv
wine_features_DecisionTreeClassifier.results
wine_features_DecisionTreeClassifier.results.json
wine_features_KNeighborsClassifier.log
wine_features_KNeighborsClassifier.model
wine_features_KNeighborsClassifier_predictions.tsv
wine_features_KNeighborsClassifier.results
wine_features_KNeighborsClassifier.results.json
wine_features_LogisticRegression.log
wine_features_LogisticRegression.model
wine_features_LogisticRegression_predictions.tsv
wine_features_LogisticRegression.results
wine_features_LogisticRegression.results.json
wine_features_RandomForestClassifier.log
wine_features_RandomForestClassifier.model
wine_features_RandomForestClassifier_predictions.tsv
wine_features_RandomForestClassifier.results
wine_features_RandomForestClassifier.results.json
wine.log
wine_summary.tsv
skll为每个分类器生成四个文件:一个日志,两个结果,一个预测。我提取算法名称,并使用以下 SQL 查询按其准确性对它们进行排序:
$ < output/wine_summary.tsv csvsql --query "SELECT learner_name, accuracy FROM s
tdin ORDER BY accuracy DESC" | csvlook -I
│ learner_name │ accuracy │
├────────────────────────┼───────────┤
│ LogisticRegression │ 0.9953125 │
│ RandomForestClassifier │ 0.9953125 │
│ KNeighborsClassifier │ 0.99375 │
│ DecisionTreeClassifier │ 0.984375 │
这里的相关列是accuracy,表示被正确分类的数据点的百分比。由此我们看到,实际上所有的算法都表现得非常好。RandomForestClassifier是性能最好的算法,紧随其后的是KNeighborsClassifier。
每个 JSON 文件都包含一个混淆矩阵,让您进一步了解每个分类器的性能。混淆矩阵是一个表格,其中列指的是真实标签(红色和白色),行指的是预测标签。对角线上的数字越大,意味着预测越正确。使用jq,我可以打印每个分类器的名称,并提取相关的混淆矩阵:
$ jq -r '.[] | "\(.learner_name):\n\(.result_table)\n"' output/*.json
DecisionTreeClassifier:
+-------+-------+---------+-------------+----------+-------------+
| | red | white | Precision | Recall | F-measure |
+=======+=======+=========+=============+==========+=============+
| red | [317] | 2 | 0.975 | 0.994 | 0.984 |
+-------+-------+---------+-------------+----------+-------------+
| white | 8 | [313] | 0.994 | 0.975 | 0.984 |
+-------+-------+---------+-------------+----------+-------------+
(row = reference; column = predicted)
KNeighborsClassifier:
+-------+-------+---------+-------------+----------+-------------+
| | red | white | Precision | Recall | F-measure |
+=======+=======+=========+=============+==========+=============+
| red | [318] | 1 | 0.991 | 0.997 | 0.994 |
+-------+-------+---------+-------------+----------+-------------+
| white | 3 | [318] | 0.997 | 0.991 | 0.994 |
+-------+-------+---------+-------------+----------+-------------+
(row = reference; column = predicted)
LogisticRegression:
+-------+-------+---------+-------------+----------+-------------+
| | red | white | Precision | Recall | F-measure |
+=======+=======+=========+=============+==========+=============+
| red | [317] | 2 | 0.997 | 0.994 | 0.995 |
+-------+-------+---------+-------------+----------+-------------+
| white | 1 | [320] | 0.994 | 0.997 | 0.995 |
+-------+-------+---------+-------------+----------+-------------+
(row = reference; column = predicted)
RandomForestClassifier:
+-------+-------+---------+-------------+----------+-------------+
| | red | white | Precision | Recall | F-measure |
+=======+=======+=========+=============+==========+=============+
| red | [317] | 2 | 0.997 | 0.994 | 0.995 |
+-------+-------+---------+-------------+----------+-------------+
| white | 1 | [320] | 0.994 | 0.997 | 0.995 |
+-------+-------+---------+-------------+----------+-------------+
(row = reference; column = predicted)
当您有两个以上的类别时,混淆矩阵特别有用,这样您就可以看到发生了哪种错误分类,以及错误分类的成本对于每个类别来说是不同的。
从使用的角度来看,有趣的是考虑到vw和skll采用两种不同的方法。vw使用命令行选项,而skll需要一个单独的文件。这两种方法各有利弊。虽然命令行选项支持更多的特别用法,但配置文件可能更容易复制。然后,正如我们已经看到的,用任意数量的选项调用vw可以很容易地放在脚本或Makefile中。相反,让skll接受不需要配置文件的选项就不那么简单了。
9.6 总结
在这一章中,我们已经研究了建模数据。通过例子,我深入研究了三种不同的机器学习任务,即无监督的降维以及有监督的回归和分类。不幸的是,一本合适的机器学习教程超出了本书的范围。在下一节中,如果你想了解更多关于机器学习的知识,我有一些建议。这是我在本书中涉及的 OSEMN 数据科学模型的第四步,也是最后一步。下一章是最后一个间奏曲章节,将是关于在其他地方利用命令行。
9.7 进一步探索
- Sebastian Raschka 和 Vahid Mirjalili 所著的《Python 机器学习》一书全面概述了机器学习以及如何使用 Python 来应用它。
- Jared Lander 的《R for everybody》后面的章节解释了如何使用 R 完成各种机器学习任务。
- 如果你想更深入地了解机器学习,我强烈推荐你阅读克里斯托弗·毕晓普的《模式识别和机器学习》和大卫·麦凯的《信息论、推理和学习算法》。
- 如果您有兴趣了解更多关于 T-SNE 算法的信息,我推荐关于它的原始文章:Laurens van der Maaten 和 Geoffrey Hinton 撰写的《使用 T-SNE 可视化数据》。
十、多语言数据科学
原文:https://datascienceatthecommandline.com/2e/chapter-10-polyglot-data-science.html
通晓多种语言的人就是能讲多种语言的人。在我看来,通晓多种语言的数据科学家是指使用多种编程语言、工具和技术来获取、清理、探索和建模数据的人。
命令行刺激了多语言方法。命令行并不关心工具是用哪种编程语言编写的,只要它们遵循 Unix 的理念。我们在第四章中非常清楚地看到了这一点,在那里我们用 Bash、Python 和 R 创建了命令行工具。此外,我们直接在 CSV 文件上执行 SQL 查询,并从命令行执行 R 表达式。简而言之,在没有完全意识到的情况下,我们已经在做多语言数据科学了!
在这一章中,我将进一步翻转它。我将向您展示如何在各种编程语言和环境中利用命令行。因为说实话,我们不会把整个数据科学生涯都花在命令行上。对于我来说,当我分析一些数据时,我经常使用 RStudio IDE,当我实现一些东西时,我经常使用 Python。我利用一切有助于我完成工作的东西。
我感到欣慰的是,命令行通常触手可及,无需切换到不同的应用。它允许我快速运行命令,而无需切换到单独的应用,也不会中断我的工作流程。比如用curl下载文件,用head检查一段数据,用git创建备份,用make编译一个网站。一般来说,没有命令行,通常需要大量代码或者根本无法完成的任务。
10.1 概述
在本章中,您将学习如何:
- 在 JupyterLab 和 RStudio IDE 中运行终端
- 在 Python 和 R 中与任意命令行工具交互
- 在 Apache Spark 中使用 Shell 命令转换数据
本章从以下文件开始:
$ cd /data/ch10
$ l
total 180K
drwxr-xr-x 2 dst dst 4.0K Mar 3 11:02 __pycache__/
-rw-r--r-- 1 dst dst 164K Mar 3 11:02 alice.txt
-rwxr--r-- 1 dst dst 408 Mar 3 11:02 count.py*
-rw-r--r-- 1 dst dst 460 Mar 3 11:02 count.R
-rw-r--r-- 1 dst dst 1.7K Mar 3 11:02 Untitled1337.ipynb
获取这些文件的说明在第二章中。任何其他文件都是使用命令行工具下载或生成的。
10.2 Jupyter
Project Jupyter 是一个开源项目,诞生于 2014 年的 IPython 项目,因为它发展到支持跨所有编程语言的交互式数据科学和科学计算。Jupyter 支持 40 多种编程语言,包括 Python、R、Julia 和 Scala。在这一节中,我将重点介绍 Python。
该项目包括 JupyterLab、Jupyter 笔记本和 Jupyter 控制台。我将从 Jupyter 控制台开始,因为它是以交互方式使用 Python 的最基本的控制台。这里有一个 Jupyter 控制台会话,演示了利用命令行的几种方法。
$ jupyter console
Jupyter console 6.4.0
Python 3.9.4 (default, Apr 4 2021, 19:38:44)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.23.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]: ! date # ➊
Sun May 2 01:45:06 PM CEST 2021
In [2]: ! pip install --upgrade requests
Requirement already satisfied: requests in /home/dst/.local/lib/python3.9/site-p
ackages (2.25.1)
Collecting requests
Using cached requests-2.25.1-py2.py3-none-any.whl (61 kB)
Downloading requests-2.25.0-py2.py3-none-any.whl (61 kB)
|████████████████████████████████| 61 kB 2.1 MB/s
Requirement already satisfied: urllib3<1.27,>=1.21.1 in /home/dst/.local/lib/pyt
hon3.9/site-packages (from requests) (1.26.4)
Requirement already satisfied: certifi>=2017.4.17 in /home/dst/.local/lib/python
3.9/site-packages (from requests) (2020.12.5)
Requirement already satisfied: chardet<5,>=3.0.2 in /usr/lib/python3/dist-packag
es (from requests) (4.0.0)
Requirement already satisfied: idna<3,>=2.5 in /home/dst/.local/lib/python3.9/si
te-packages (from requests) (2.10)
In [3]: ! head alice.txt
Project Gutenberg's Alice's Adventures in Wonderland, by Lewis Carroll
This eBook is for the use of anyone anywhere at no cost and with
almost no restrictions whatsoever. You may copy it, give it away or
re-use it under the terms of the Project Gutenberg License included
with this eBook or online at www.gutenberg.org
Title: Alice's Adventures in Wonderland
In [4]: len(open("alice.txt").read().strip().split("\n")) # ➋
Out[4]: 3735
In [5]: total_lines = ! < alice.txt wc -l
In [6]: total_lines
Out[6]: ['3735']
In [7]: int(total_lines[0]) # ➌
Out[7]: 3735
In [8]: url = "https://www.gutenberg.org/files/11/old/11.txt"
In [9]: import requests # ➍
In [10]: with open("alice2.txt", "wb") as f:
...: response = requests.get(url)
...: f.write(response.content)
...:
In [11]: ! curl '{url}' > alice3.txt # ➎
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 163k 100 163k 0 0 211k 0 --:--:-- --:--:-- --:--:-- 211k
In [12]: ! ls alice*txt
alice2.txt alice3.txt alice.txt
In [13]: ! rm -v alice{2,3}.txt # ➏
zsh:1: no matches found: alice(2, 3).txt
In [14]: ! rm -v alice{{2,3}}.txt
removed 'alice2.txt'
removed 'alice3.txt'
In [15]: lower = ["foo", "bar", "baz"]
In [16]: upper = ! echo '{"\n".join(lower)}' | tr '[a-z]' '[A-Z]' # ➐
In [17]: upper
Out[17]: ['FOO', 'BAR', 'BAZ']
In [18]: exit
Shutting down kernel
➊ 你可以运行任意的 Shell 命令和管道比如date或者pip来安装一个 Python 包。
➋ 对比这一行 Pyton 代码,统计alice.txt中的行数与其下面wc的调用数。
➌ 注意,标准输出是以字符串列表的形式返回的,所以为了使用total_lines的值,获取第一项并将其转换为整数。
➍ 比较这个单元格和下一个要下载文件的单元格,调用它下面的curl。
➎ 你可以用花括号将 Python 变量作为 Shell 命令的一部分。
➏ :如果你想用字面上的花括号,就打两次。
➐ 使用 Python 变量作为标准输入是可以做到的,但是正如你所看到的,变得相当棘手。
Jupyter Notebook 本质上是一个基于浏览器的 Jupyter 控制台版本。它支持利用命令行的相同方式,包括感叹号和 bash 魔术。最大的区别是,笔记本不仅可以包含代码,还可以包含标记文本、等式和数据可视化。由于这个原因,它在数据科学家中非常受欢迎。Jupyter Notebook 是一个独立的项目和环境,但我想使用 JupyterLab 来处理笔记本,因为它提供了一个更完整的 IDE。
图 10.1 是 JupyterLab 的截图,显示了文件浏览器(左)、代码编辑器(中)、笔记本(右)、终端(下)。后三者都展示了利用命令行的方法。代码是我将在下一节讨论的内容。这个特殊的笔记本与我刚才讨论的控制台会话非常相似。终端为您运行命令行工具提供了一个完整的 Shell。请注意,这个终端、代码和笔记本之间不可能有交互。因此,这个终端与打开一个单独的终端应用没有什么不同,但是当您在 Docker 容器内或远程服务器上工作时,它仍然很有帮助。

图 10.1:带有文件浏览器、代码编辑器、笔记本和终端的 JupyterLab 屏幕截图的这个笔记本也包含了个叫做%%bash的单元,它可以让你写多行的 Bash 脚本. 因为使用 Python 变量更难, 我不推荐你使用这个方法. 你最好用一个单独的文件创建 Bash 脚本,并且用感叹号来执行它 (!).
10.3 Python
subprocess模块允许您从 Python 运行命令行工具,并连接到它们的标准输入和输出。相对于旧的os.system()功能,推荐使用该模块。默认情况下,它不在 Shell 中运行,但是可以用run()函数的shell参数来改变它。
$ bat count.py
───────┬────────────────────────────────────────────────────────────────────────
│ File: count.py
───────┼────────────────────────────────────────────────────────────────────────
1 │ #!/usr/bin/env python
2 │
3 │ from subprocess import run # ➊
4 │ from sys import argv
5 │
6 │ if __name__ == "__main__":
7 │
8 │ _, filename, pattern = argv
9 │
10 │ with open(filename) as f: # ➋
11 │ alice = f.read()
12 │
13 │ words = "\n".join(alice.split()) # ➌
14 │
15 │ grep = run(["grep", "-i", pattern], # ➍
16 │ input = words,
17 │ capture_output=True,
18 │ text=True)
19 │
20 │ print(len(grep.stdout.strip().split("\n"))) # ➎
───────┴────────────────────────────────────────────────────────────────────────
➊ 利用命令行的推荐方式是使用subprocess模块的run()功能。
➋ 打开文件fliename
➌ 将整个文本拆分成单词
➍ 运行命令行工具grep,其中words作为标准输入传递。
➎ 标准输出为一个长字符串。在这里,我将它拆分到每个换行符上,以计算pattern出现的次数。
这个命令行工具的用法如下:
$ ./count.py alice.txt alice
403
注意,第 15 行的run调用的第一个参数是一个字符串列表,其中第一项是命令行工具的名称,其余项是参数。这不同于传递单个字符串。这也意味着您没有任何其他的 Shell 语法来支持诸如重定向和管道之类的事情。
10.4 R
在 R 中,有几种方法可以利用命令行。
在下面的例子中,我启动了一个 R 会话,并使用system2()函数计算字符串alice在书《爱丽丝漫游仙境》中出现的次数。
$ R --quiet
> lines <- readLines("alice.txt") # ➊
> head(lines)
[1] "Project Gutenberg's Alice's Adventures in Wonderland, by Lewis Carroll"
[2] ""
[3] "This eBook is for the use of anyone anywhere at no cost and with"
[4] "almost no restrictions whatsoever. You may copy it, give it away or"
[5] "re-use it under the terms of the Project Gutenberg License included"
[6] "with this eBook or online at www.gutenberg.org"
> words <- unlist(strsplit(lines, " ")) # ➋
> head(words)
[1] "Project" "Gutenberg's" "Alice's" "Adventures" "in"
[6] "Wonderland,"
> alice <- system2("grep", c("-i", "alice"), input = words, stdout = TRUE) # ➌
> head(alice)
[1] "Alice's" "Alice's" "ALICE'S" "ALICE'S" "Alice" "Alice"
> length(alice) # ➍
➊ 读入文件alice.txt
➋ 将文本拆分成单词
➌ 调用命令行工具grep只保留与字符串alice匹配的行。字符向量words作为标准输入传递。
➍ 统计字符向量alice中的元素个数
system2()的一个缺点是,它首先将字符向量写入一个文件,然后将其作为标准输入传递给命令行工具。当处理大量数据和大量调用时,这可能会有问题。
最好使用命名管道,因为这样就不会有数据写入磁盘,这样效率会高得多。这可以通过pipe()和fifo()功能完成。感谢吉姆·海斯特的建议。下面的代码演示了这一点:
> out_con <- fifo("out", "w+") # ➊
> in_con <- pipe("grep b > out") # ➋
> writeLines(c("foo", "bar"), in_con) # ➌
> readLines(out_con) # ➍
[1] "bar"
➊ 函数fifo()创建一个特殊的先进先出文件,称为out。这只是对管道连接的引用(就像stdin和stdout一样)。实际上没有数据写入磁盘。
➋ 工具grep将只保留包含b的行,并将它们写入命名管道out。
➌ 将两个值写入 Shell 命令的标准输入。
➍ 读取grep产生的标准输出作为字符向量。
➎ 清理连接并删除特殊文件。
因为这需要相当多的样板代码(创建连接、写、读、清理),所以我写了一个助手函数sh()。使用magrittr包中的管道操作符(%>%,我将多个 Shell 命令链接在一起。
> library(magrittr)
>
> sh <- function(.data, command) {
+ temp_file <- tempfile()
+ out_con <- fifo(temp_file, "w+")
+ in_con <- pipe(paste0(command, " > ", temp_file))
+ writeLines(as.character(.data), in_con)
+ result <- readLines(out_con)
+ close(out_con)
+ close(in_con)
+ unlink(temp_file)
+ result
+ }
>
> lines <- readLines("alice.txt")
> words <- unlist(strsplit(lines, " "))
>
> sh(words, "grep -i alice") %>%
+ sh("wc -l") %>%
+ sh("cowsay") %>%
+ cli::cat_boxx()
┌──────────────────────────────────┐
│ │
│ _____ │
│ < 403 > │
│ ----- │
│ \ ^__^ │
│ \ (oo)\_______ │
│ (__)\ )\/\ │
│ ||----w | │
│ || || │
│ │
└──────────────────────────────────┘
>
> q("no")
10.5 RStudio
RStudio IDE 可以说是使用 R 的最流行的环境。当您打开 RStudio 时,您将首先看到 console 选项卡:

图 10.2:打开控制台选项卡时的 RStudio IDE
“终端”选项卡紧挨着“控制台”选项卡。如果提供完整的 Shell:

图 10.3:打开“终端”选项卡的 RStudio IDE
注意,就像 JupyterLab 一样,这个终端没有连接到控制台或任何 R 脚本。
10.6 Apache Spark
Apache Spark 是一个集群计算框架。当无法将数据存储在内存中时,你会求助于这只 800 磅重的大猩猩。Spark 本身是用 Scala 编写的,但是你也可以从 Python 使用 PySpark 和从 R 使用 SparkR 或 sparklyr 与它交互。
数据处理和机器学习管道是通过一系列转换和一个最终动作来定义的。其中一个转换是pipe()转换,它允许您通过 Shell 命令(比如 Bash 或 Perl 脚本)运行整个数据集。数据集中的项被写入标准输入,标准输出作为字符串的 RDD 返回。
在下面的会话中,我启动了一个 Spark Shell,并再次计算了《爱丽丝漫游仙境》中alice出现的次数。
$ spark-shell --master local[6]
Spark context Web UI available at http://3d1bec8f2543:4040
Spark context available as 'sc' (master = local[6], app id = local-16193763).
Spark session available as 'spark'.
Welcome to
____ __
/ __/__ ___ _____/ /__
_\ \/ _ \/ _ `/ __/ '_/
/___/ .__/\_,_/_/ /_/\_\ version 3.1.1
/_/
Using Scala version 2.12.10 (OpenJDK 64-Bit Server VM, Java 11.0.10)
Type in expressions to have them evaluated.
Type :help for more information.
scala> val lines = sc.textFile("alice.txt") # ➊
lines: org.apache.spark.rdd.RDD[String] = alice.txt MapPartitionsRDD[1] at textF
ile at <console>:24
scala> lines.first()
res0: String = Project Gutenberg's Alice's Adventures in Wonderland, by Lewis Ca
rroll
scala> val words = lines.flatMap(line => line.split(" ")) # ➋
words: org.apache.spark.rdd.RDD[String] = MapPartitionsRDD[2] at flatMap at <con
sole>:25
scala> words.take(5)
res1: Array[String] = Array(Project, Gutenberg's, Alice's, Adventures, in)
scala> val alice = words.pipe("grep -i alice") # ➌
alice: org.apache.spark.rdd.RDD[String] = PipedRDD[3] at pipe at <console>:25
scala> alice.take(5)
res2: Array[String] = Array(Alice's, Alice's, ALICE'S, ALICE'S, Alice)
scala> val counts = alice.pipe("wc -l") # ➍
counts: org.apache.spark.rdd.RDD[String] = PipedRDD[4] at pipe at <console>:25
scala> counts.collect()
res3: Array[String] = Array(64, 72, 94, 93, 67, 13) # ➎
scala> counts.map(x => x.toInt).reduce(_ + _) # ➏
res4: Int = 403
scala> sc.textFile("alice.txt").flatMap(line => line.split(" ")).pipe("grep -i a
lice").pipe("wc -l").map(x => x.toInt).reduce(_ + _)
res5: Int = 403 # ➐
➊ 读取alice.txt使得每一行都是一个元素。
➋ 在空格上拆分各个元素。换句话说,每一行都被拆分成单词。
➌ 通过grep管道传输每个分区,只保留与字符串alice匹配的元素。
➍ 管每个分区通过wc来统计元素的数量。
➎ 每个分区有一个计数。
➏ 将所有的计数相加得到最终的计数。注意,元素首先需要从字符串转换成整数。
➐ 将上述步骤组合成一个单一命令。
pipe()转换也在 PySpark, SparkR, 和 SparklyR 中提供。
如果您想在管道中使用定制的命令行工具,那么您需要确保它存在于集群中的所有节点上(称为执行器)。一种方法是在使用spark-submit提交 Spark 应用时,用--files选项指定文件名。
Matei Zaharia 和 Bill Chambers(Apache Spark 的原作者)在他们的书《Spark 权威指南》中提到,这个pipe方法可能是 Spark 更有趣的方法之一。”那是相当的赞美!我认为 Apache Spark 的开发者增加了利用一项 50 年前的技术的能力,这太棒了。
10.7 总结
在本章中,你学习了在其他情况下使用命令行的几种方法,包括编程语言和其他环境。重要的是要认识到命令行并不存在于真空中。最重要的是你使用工具,有时结合使用,可靠地完成工作。
既然我们已经学完了所有的四个奥赛门章节和四个间奏曲章节,是时候总结一下了,在最后一章中结束。
10.8 进一步探索
- 也有不使用命令行直接集成两种编程语言的方法。例如,R 中的
reticulate包允许你直接与 Python 交互。
十一、总结
原文:https://datascienceatthecommandline.com/2e/chapter-11-conclusion.html
在这最后一章,这本书接近尾声。我将首先回顾我在前面十章中讨论的内容,然后给你三条建议,并提供一些资源来进一步探索我们触及的相关主题。最后,如果您有任何问题、评论或新的命令行工具要分享,我提供了一些与我联系的方法。
11.1 让我们回顾一下
这本书探索了使用命令行进行数据科学的能力。我发现这是一个有趣的观察,这个相对年轻的领域提出的挑战可以通过这样一个经过时间考验的技术来解决。我希望您现在看到了命令行的能力。许多命令行工具提供了各种可能性,非常适合包括数据科学在内的各种任务。
数据科学有许多可用的定义。在第一章中,我介绍了 Mason 和 Wiggins 定义的 OSEMN 模型,因为它非常实用,可以转化为非常具体的任务。首字母缩写 OSEMN 代表获取、清理、探索、建模和解释数据。第一章也解释了为什么命令行非常适合做这些数据科学任务。
在第二章中,我解释了如何获得本书中使用的所有工具。第二章还介绍了命令行的基本工具和概念。
OSEMN 模型的四个章节着重于使用命令行执行这些实际任务。我没有专门用一章来讨论第五步,解释数据,因为,坦率地说,计算机,更不用说命令行,在这里几乎没有用处。然而,我提供了一些关于这个主题的进一步阅读的提示。
在 intermezzo 的四个章节中,我们讨论了在命令行中进行数据科学的一些更广泛的主题,这些主题并不真正特定于某个特定的步骤。在第四章中,我解释了如何将一行程序和现有代码转化为可重用的命令行工具。在第六章中,我描述了如何使用名为make的工具管理数据工作流。在第八章中,我演示了如何使用 GNU Parallel 并行运行普通的命令行工具和管道。在第十章中,我展示了命令行并不存在于真空中,而是可以在其他编程语言和环境中使用。这些 intermezzo 章节中讨论的主题可以应用于数据工作流中的任何一点。
不可能展示所有可用的与数据科学相关的命令行工具。每天都有新的工具产生。现在你可能已经明白,这本书更多的是关于使用命令行的思想,而不是给你一个详尽的工具列表。
11.2 三条建议
你可能已经花了相当多的时间阅读了这些章节,并且可能还阅读了代码示例。希望它能最大化这项投资的回报,并增加您继续将命令行整合到您的数据科学工作流中的可能性,我想向您提供三条建议:(1)耐心,(2)创新,以及(3)务实。在接下来的三个小节中,我将详细阐述每一条建议。
11.2.1 耐心
我能给的第一条建议是要有耐心。在命令行上处理数据不同于使用编程语言,因此需要不同的思维方式。
此外,命令行工具本身也有一些奇怪和不一致的地方。这部分是因为它们是由许多不同的人在几十年的时间里开发出来的。如果你发现自己对这些令人眼花缭乱的选项不知所措,别忘了使用--help、man、tldr或你最喜欢的搜索引擎来了解更多。
然而,尤其是在开始的时候,这可能是一次令人沮丧的经历。相信我,随着您练习使用命令行及其工具,您会变得更加熟练。命令行已经存在了几十年,并且还会继续存在下去。这是一项值得的投资。
11.2.2 创新
第二个相关的建议是要有创造性。命令行非常灵活。通过组合命令行工具,您可以完成比您想象的更多的事情。
我鼓励你不要马上回到你的编程语言。当你不得不使用一种编程语言时,考虑一下代码是否可以以某种方式通用化或重用。如果是这样,考虑使用我在第四章中讨论的步骤,用这些代码创建你自己的命令行工具。如果你认为你的工具对其他人有益,你甚至可以更进一步,把它开源。也许您知道如何在命令行执行某个步骤,但是您不愿意离开您正在工作的主要编程语言或环境。也许你可以使用第十章中列出的方法之一。
11.2.3 务实
第三条建议是要务实。务实与创新相关,但值得单独解释。在前面的小节中,我提到过你不应该马上退回到编程语言。当然,命令行有其局限性。在整本书中,我一直强调命令行应该被看作是进行数据科学的一种伴随方法。
我已经讨论了在命令行中进行数据科学的四个步骤。实际上,命令行对于步骤 1 的适用性比步骤 4 更高。你应该使用最适合手头任务的方法。在工作流程中的任何一点上混合搭配方法都是非常好的。正如我在第十章中所展示的,命令行在与其他方法、编程语言和统计环境的集成方面非常出色。每种方法都有一定的权衡,精通命令行的一部分是学习何时使用哪种方法。
总之,当您有耐心、有创造力并且务实时,命令行将使您成为更高效和多产的数据科学家。
11.3 何去何从?
由于这本书是关于命令行和数据科学的交集,许多相关的主题只是被触及。现在,由您来进一步探索这些主题。以下小节提供了主题列表和建议参考的资源。
11.4 命令行
- 《Linux 命令行:完全介绍》,第二版,作者:小威廉·E·肖特(No Starch 出版社,2019 年)
- Jerry Peek、Shelley Powers、Tim O'Reilly 和 Mike Loukides 编写的 《Unix Power Tools》,第三版(O'Reilly Media,2002 年)
- Arnold Robbins、Elbert Hannah 和 Linda Lamb 编写的《学习 Vi 和 Vim 编辑器》第 7 版(O'Reilly Media,2008 年)
11.4.1 Shell 编程
- Arnold Robbins 和 Nelson H.F. Beebe (O'Reilly Media,2005)编写的《经典 Shell 脚本》
- 戴维·泰勒和布兰登·佩里的《邪恶酷壳剧本》第二版(No Starch 出版社,2017)
- Carl Albing JP Vossen 的《Bash Cookbook》(O'Reilly Media,2018 年)
11.4.2 Python、R 和 SQL
- Zed A. Shaw(Addison-Wesley Professional,2017 年)的《本办法学 Python 3》
- 《Python 数据分析》,第二版作者 Wes McKinne(O'Reilly Media,2017 年)
- 《从零开始的数据科学》,第二版乔尔·格鲁什著(O'Reilly Media,2019 年)
- Garrett Grolemund 和 Hadley Wickham (O'Reilly Media,2016 年)
- 杰瑞德·兰德(Addison-Wesley Professional,2017 年出版)的《人人有责》第二版
- 《Sams 每天 10 分钟自学 SQL》,第五版本·福塔著(Sams,2020 年)
11.4.3 API
- Matthew A. Russell 和 Mikhail Klassen 的《社交网络挖掘》第三版(O'Reilly Media,2019)
- 皮特·沃顿的《数据源手册》(O'Reilly Media,2011 年)
11.4.4 机器学习
- 《Python 机器学习》,第三版 Sebastian rasch ka 和 Vahid Mirjalili 著(Packt 出版社,2019 年)
- 克里斯托弗·M·毕晓普(施普林格,2006 年)的《模式识别和机器学习》
- 《信息理论、推理和学习算法》(剑桥大学出版社,2003 年)
11.5 取得联系
如果没有创建命令行和众多工具的许多人,这本书是不可能的。可以肯定地说,当前用于数据科学的命令行工具生态系统是一个社区成果。我只能让您对许多可用的命令行工具有所了解。每天都有新的创造,也许有一天你自己也会创造一个。那样的话,我很乐意收到你的来信。如果您有任何问题、意见或建议,都可以给我写信,我将不胜感激。有几种联系方式:
- 电子邮件:jeroen@jeranssens.com
- Twitter:@jerenjanssens
- 图书网站:https://datascienceatthecommandline.com/
- 预订 GitHub 资源库:https://gitHub.com/jeroenjanssens/data-science-at-the-command-line
谢谢你。
附录:命令行工具列表
原文:https://datascienceatthecommandline.com/2e/list-of-command-line-tools.html
这是本书中讨论的所有命令行工具的概述。这包括二进制可执行文件、解释脚本和 ZShell 内置文件和关键字。对于每个命令行工具,如果可用且适当,将提供以下信息:
- 要在命令行中键入的实际命令
- 描述
- 书中使用的版本
- 该版本发布的年份
- 主要作者
- 查找更多信息的网站
- 如何获得帮助
- 一个用法示例
这里列出的所有命令行工具都包含在 Docker 映像中。有关如何设置的说明,请参见第二章。请注意,引用开源软件不是小事,一些信息可能会丢失或不正确。
alias
定义或显示别名。alias是一个 ZShell 内置。
$ type alias
alias is a shell builtin
$ man zshbuiltins | grep -A 10 alias
$ alias l
l='ls --color -lhF --group-directories-first'
$ alias python=python3
awk
模式扫描和文本处理语言。awk(版本 1.3.4)作者:迈克·D·布伦南和托马斯·E·迪基(2019)。更多信息:https://invisible-island.net/mawk。
$ type awk
awk is /usr/bin/awk
$ man awk
$ seq 5 | awk '{sum+=$1} END {print sum}'
15
aws
管理 AWS 服务的统一工具。aws(版本 2.1.32)由亚马逊网络服务(2021)提供。更多信息:https://aws.amazon.com/cli。
$ type aws
aws is /usr/local/bin/aws
$ aws --help
bash
GNU Bourne-再次壳。bash(5.0.17 版)布莱恩·福克斯和切特·雷米(2019)。更多信息:https://www.gnu.org/software/bash。
$ type bash
bash is /usr/bin/bash
$ man bash
bat
一个带有语法高亮和 Git 集成的cat克隆。bat(版本 0.18.0)作者大卫·彼得(2021)。更多信息:https://github.com/sharkdp/bat。
$ type bat
bat is an alias for bat --tabs 8 --paging never
$ man bat
bc
任意精度计算器语言。bc(版本 1.07.1)作者菲利普·A·纳尔逊(2017)。更多信息:https://www.gnu.org/software/bc。
$ type bc
bc is /usr/bin/bc
$ man bc
$ bc -l <<< 'e(1)'
2.71828182845904523536
body
对除第一行以外的所有行应用命令。body(0.1 版)作者耶鲁安·扬森斯(2021)。更多信息:https://github.com/jeroenjanssens/dsutils。
$ type body
body is /usr/bin/dsutils/body
$ seq 10 | header -a 'values' | body shuf
values
9
6
1
3
2
7
5
10
8
4
cat
连接文件并在标准输出上打印。托尔比约恩·格兰隆德和理查德·M·斯托曼(2018 年)著cat(版本 8.30)。更多信息:https://www.gnu.org/software/coreutils。
$ type cat
cat is /usr/bin/cat
$ man cat
$ cat *.log > all.log
cd
更改 Shell 工作目录。cd是一个 ZShell 内置。
$ type cd
cd is a shell builtin
$ man zshbuiltins | grep -A 10 cd
$ cd ~
$ pwd
/home/dst
$ cd ..
$ pwd
/home
$ cd /data/ch01
cd: no such file or directory: /data/ch01
chmod
更改文件模式位。chmod(8.30 版)作者大卫·麦肯齐和吉姆·迈耶林(2018)。我使用第四章中的chmod来制作一个可执行的工具。更多信息:https://www.gnu.org/software/coreutils。
$ type chmod
chmod is /usr/bin/chmod
$ man chmod
$ chmod u+x script.sh
cols
将命令应用于列的子集。cols(0.1 版)作者耶鲁安·扬森斯(2021)。更多信息:https://github.com/jeroenjanssens/dsutils。
$ type cols
cols is /usr/bin/dsutils/cols
column
column(版本 2.36.1)作者卡雷尔·扎克(2021)。更多信息:https://www.kernel.org/pub/linux/utils/util-linux。
$ type column
column is /usr/bin/column
cowsay
可配置的说话牛。托尼·门罗(1999 年)的作品。更多信息:https://github.com/tnalpgge/rank-amateur-cowsay。
$ type cowsay
cowsay is /usr/bin/cowsay
$ man cowsay
$ echo 'The command line is awesome!' | cowsay
______________________________
< The command line is awesome! >
------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
cp
复制文件和目录。托尔比约恩·格兰伦德、大卫·麦肯齐和吉姆·迈耶林(2018 年)。更多信息:https://www.gnu.org/software/coreutils。
$ type cp
cp is /usr/bin/cp
$ man cp
$ cp -r ~/Downloads/*.xlsx /data
csv2vw
将 CSV 转换为 Vowpal Wabbit 格式。csv2vw(0.1 版)作者耶鲁安·扬森斯(2021)。更多信息:https://github.com/jeroenjanssens/dsutils。
$ type csv2vw
csv2vw is /usr/bin/dsutils/csv2vw
csvcut
过滤和截断 CSV 文件。克里斯托弗·格罗斯科夫(Christopher Groskopf)著(2020 年)。更多信息:https://csvkit.rtfd.org。
$ type csvcut
csvcut is /usr/bin/csvcut
$ csvcut --help
$ csvcut -c bill,tip /data/ch05/tips.csv | trim
bill,tip
16.99,1.01
10.34,1.66
21.01,3.5
23.68,3.31
24.59,3.61
25.29,4.71
8.77,2.0
26.88,3.12
15.04,1.96
… with 235 more lines
csvgrep
搜索 CSV 文件。克里斯托弗·格罗斯科夫(Christopher Groskopf)著(2020 年)。更多信息:https://csvkit.rtfd.org。
$ type csvgrep
csvgrep is /usr/bin/csvgrep
$ csvgrep --help
csvjoin
执行类似 SQL 的联接,以合并指定列上的 CSV 文件。克里斯托弗·格罗斯科夫(Christopher Groskopf)著(2020 年)。更多信息:https://csvkit.rtfd.org。
$ type csvjoin
csvjoin is /usr/bin/csvjoin
$ csvjoin --help
csvlook
在控制台中将 CSV 文件呈现为与 Markdown 兼容的固定宽度表格。克里斯托弗·格罗斯科夫(Christopher Groskopf)著(2020 年)。更多信息:https://csvkit.rtfd.org。
$ type csvlook
csvlook is a shell function
$ csvlook --help
$ csvlook /data/ch05/tips.csv
│ bill │ tip │ sex │ smoker │ day │ time │ size │
├───────┼───────┼────────┼────────┼──────┼────────┼──────┤
│ 16.99 │ 1.01 │ Female │ False │ Sun │ Dinner │ 2 │
│ 10.34 │ 1.66 │ Male │ False │ Sun │ Dinner │ 3 │
│ 21.01 │ 3.50 │ Male │ False │ Sun │ Dinner │ 3 │
│ 23.68 │ 3.31 │ Male │ False │ Sun │ Dinner │ 2 │
│ 24.59 │ 3.61 │ Female │ False │ Sun │ Dinner │ 4 │
│ 25.29 │ 4.71 │ Male │ False │ Sun │ Dinner │ 4 │
│ 8.77 │ 2.00 │ Male │ False │ Sun │ Dinner │ 2 │
│ 26.88 │ 3.12 │ Male │ False │ Sun │ Dinner │ 4 │
… with 236 more lines
csvquote
使常见的 unix 工具能够正确处理 CSV 数据。csvquote(0.1 版)作者丹·布朗(2018)。更多信息:https://github.com/dbro/csvquote。
$ type csvquote
csvquote is /usr/local/bin/csvquote
csvsort
对 CSV 文件排序。克里斯托弗·格罗斯科夫(Christopher Groskopf)著(2020 年)。更多信息:https://csvkit.rtfd.org。
$ type csvsort
csvsort is /usr/bin/csvsort
$ csvsort --help
csvsql
对 CSV 文件执行 SQL 语句。克里斯托弗·格罗斯科夫(Christopher Groskopf)著(2020 年)。更多信息:https://csvkit.rtfd.org。
$ type csvsql
csvsql is /usr/bin/csvsql
$ csvsql --help
csvstack
堆叠多个 CSV 文件中的行。克里斯托弗·格罗斯科夫(Christopher Groskopf)著(2020 年)。更多信息:https://csvkit.rtfd.org。
$ type csvstack
csvstack is /usr/bin/csvstack
$ csvstack --help
csvstat
打印 CSV 文件中每一列的描述性统计数据。克里斯托弗·格罗斯科夫(Christopher Groskopf)著(2020 年)。更多信息:https://csvkit.rtfd.org。
$ type csvstat
csvstat is /usr/bin/csvstat
$ csvstat --help
curl
传输一个 URL。curl(版本 7.68.0)作者丹尼尔·斯坦伯格(2016)。更多信息: https://curl.haxx.se 。
$ type curl
curl is /usr/bin/curl
$ man curl
cut
从文件的每一行中删除节。cut(8.30 版)作者大卫·m·伊纳特、大卫·麦肯齐、吉姆·迈耶林(2019)。更多信息:https://www.gnu.org/software/coreutils。
$ type cut
cut is /usr/bin/cut
$ man cut
display
在任何 X 服务器上显示图像或图像序列。display(版本 6.9.10-23)作者 ImageMagick Studio LLC (2019)。更多信息:https://imagemagick.org。
$ type display
display is a shell function
dseq
生成日期序列。dseq(0.1 版)作者耶鲁安·扬森斯(2021)。更多信息:https://github.com/jeroenjanssens/dsutils。
$ type dseq
dseq is /usr/bin/dsutils/dseq
$ dseq 3
2022-03-04
2022-03-05
2022-03-06
echo
显示一行文本。echo(8.30 版)布莱恩·福克斯和切特·雷米(2019)。对于使用文字文本作为下一个工具的标准输入非常有用。更多信息:https://www.gnu.org/software/coreutils。
$ type echo
echo is a shell builtin
$ man echo
$ echo Hippopotomonstrosesquippedaliophobia | cowsay
______________________________________
< Hippopotomonstrosesquippedaliophobia >
--------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
$ echo -n Hippopotomonstrosesquippedaliophobia | wc -c
36
env
在修改后的环境中运行程序。理查德·姆林纳里克、大卫·麦肯齐和阿萨夫·戈登(2018 年)。更多信息:https://www.gnu.org/software/coreutils。
$ type env
env is /usr/bin/env
$ man env
export
设置 Shell 变量的导出属性。有助于使 Shell 变量对其他命令行工具可用..export是一个 ZShell 内置。
$ type export
export is a reserved word
$ man zshbuiltins | grep -A 10 export
$ export PATH="$PATH:$HOME/bin"
fc
控制交互历史机制。fc是一个 ZShell 内置。我使用第四章中的fc来编辑nano中的命令。
$ type fc
fc is a shell builtin
$ man zshbuiltins | grep -A 10 '^ *fc '
find
在目录层次结构中搜索文件。find(版本 4.7.0)作者埃里克·b·德克尔、詹姆斯·杨曼、凯文·达利(2019)。更多信息:https://www.gnu.org/software/findutils。
$ type find
find is /usr/bin/find
$ man find
$ find /data -type f -name '*.csv' -size -3
/data/ch03/tmnt-basic.csv
/data/ch03/tmnt-missing-newline.csv
/data/ch03/tmnt-with-header.csv
/data/ch05/irismeta.csv
/data/ch05/names-comma.csv
/data/ch05/names.csv
/data/ch07/datatypes.csv
fold
将每个输入行换行以适合指定的宽度。大卫·麦肯齐(David MacKenzie)著(2020 年)。更多信息:https://www.gnu.org/software/coreutils。
$ type fold
fold is /usr/bin/fold
$ man fold
for
对列表中的每个成员执行命令。for是一个 ZShell 内置。在第八章中,我讨论了用parallel代替for的优势。
$ type for
for is a reserved word
$ man zshmisc | grep -EA 10 '^ *for '
$ for i in {A..C} "It's easy as" {1..3}; do echo $i; done
A
B
C
It's easy as
1
2
3
fx
交互式 JSON 查看器。fx(20.0.2 版),安东·梅德韦杰夫(2020 年)。更多信息:https://github.com/antonmedv/fx。
$ type fx
fx is /usr/local/bin/fx
$ fx --help
$ echo '[1,2,3]' | fx 'this.map(x => x * 2)'
[
2,
4,
6
]
git
愚蠢的内容跟踪器。Linus Torvalds 和 Junio C. Hamano (2021 年)编写的git(版本 2.25.1)。更多信息:https://git-scm.com。
$ type git
git is /usr/bin/git
$ man git
grep
打印与图案匹配的行。grep(3.4 版)作者吉姆·迈耶林(2019)。更多信息:https://www.gnu.org/software/grep。
$ type grep
grep is /usr/bin/grep
$ man grep
$ seq 100 | grep 3 | wc -l
19
gron
使 JSON greppable。gron(版本 0.6.1)作者汤姆·哈德森(2021)。更多信息:https://github.com/TomNomNom/gron。
$ type gron
gron is /usr/bin/gron
$ man gron
head
输出文件的第一部分。head(8.30 版)作者大卫·麦肯齐和吉姆·迈耶林(2019)。更多信息:https://www.gnu.org/software/coreutils。
$ type head
head is /usr/bin/head
$ man head
$ seq 100 | head -n 5
1
2
3
4
5
header
添加、替换和删除标题行。header(0.1 版)作者耶鲁安·扬森斯(2021)。更多信息:https://github.com/jeroenjanssens/dsutils。
$ type header
header is /usr/bin/dsutils/header
history
GNU 历史图书馆。布莱恩·福克斯和切特·雷米(2020 年)。更多信息:https://www.gnu.org/software/bash。
$ type history
history is a shell builtin
hostname
显示或设置系统的主机名。彼得·托拜厄斯、贝恩德·艾肯费尔斯和迈克尔·梅斯克斯(2021 年)。更多信息:https://sourceforge.net/projects/net-tools/。
$ type hostname
hostname is /usr/bin/hostname
$ man hostname
$ hostname
b213d759a26c
$ hostname -i
172.17.0.2
in2csv
将常见但不太好的表格数据格式转换为 CSV 格式。克里斯托弗·格罗斯科夫(Christopher Groskopf)著(2020 年)。更多信息:https://csvkit.rtfd.org。
$ type in2csv
in2csv is /usr/bin/in2csv
$ in2csv --help
jq
命令行 JSON 处理器。jq(1.6 版)斯蒂芬·多兰(2021)。更多信息:https://stedolan.github.com/jq。
$ type jq
jq is /usr/bin/jq
$ man jq
json2csv
将 JSON 转换为 CSV。json2csv(版本 1.2.1)作者 Jehiah Czebotar (2019)。更多信息:https://github.com/jehiah/json2csv。
$ type json2csv
json2csv is /usr/bin/json2csv
$ json2csv --help
l
以长格式列出目录内容,目录在文件、可读文件大小和访问权限之前分组。l由未知(1999 年)。
$ type l
l is an alias for ls --color -lhF --group-directories-first
$ cd /data/ch03
$ ls
logs.tar.gz tmnt-basic.csv tmnt-with-header.csv
r-datasets.db tmnt-missing-newline.csv top2000.xlsx
$ l
total 924K
-rw-r--r-- 1 dst dst 627K Mar 3 11:02 logs.tar.gz
-rw-r--r-- 1 dst dst 189K Mar 3 11:02 r-datasets.db
-rw-r--r-- 1 dst dst 149 Mar 3 11:02 tmnt-basic.csv
-rw-r--r-- 1 dst dst 148 Mar 3 11:02 tmnt-missing-newline.csv
-rw-r--r-- 1 dst dst 181 Mar 3 11:02 tmnt-with-header.csv
-rw-r--r-- 1 dst dst 91K Mar 3 11:02 top2000.xlsx
less
更多的反义词。马克·努德尔曼(2019)著less(第 551 版)。更多信息:https://www.greenwoodsoftware.com/less。
$ type less
less is an alias for less -R
$ man less
$ less README
ls
列出目录内容。ls(8.30 版)作者理查德·M·斯托曼和大卫·麦肯齐(2019)。更多信息:https://www.gnu.org/software/coreutils。
$ type ls
ls is /usr/bin/ls
$ man ls
$ ls /data
ch02 ch03 ch04 ch05 ch06 ch07 ch08 ch09 ch10 csvconf
make
维护计算机程序的程序。斯图尔特·费尔德曼(Stuart I. Feldman)著(2020 年)。更多信息:https://www.gnu.org/software/make。
$ type make
make is /usr/bin/make
$ man make
$ make sandwich
man
系统参考手册的界面。man(版本 2.9.1)作者约翰·w·伊顿和科林·沃森(2020)。更多信息:https://nongnu.org/man-db。
$ type man
man is /usr/bin/man
$ man man
$ man excel
No manual entry for excel
mkdir
制作目录。mkdir(8.30 版)作者大卫·麦肯齐(2019)。更多信息:https://www.gnu.org/software/coreutils。
$ type mkdir
mkdir is /usr/bin/mkdir
$ man mkdir
$ mkdir -p /data/ch{01..10}
mv
移动(重命名)文件。迈克·帕克,大卫·麦肯齐和吉姆·迈耶林(2020 年)。更多信息:https://www.gnu.org/software/coreutils。
$ type mv
mv is /usr/bin/mv
$ man mv
$ mv results{,.bak}
nano
Nano 的另一个编辑器,灵感来自 Pico。Benno Schulenberg,David Lawrence Ramsey,Jordi Mallach,Chris Allegretta,Robert Siemborski 和 Adam Rogoyski (2020)的作品。更多信息:https://nano-editor.org。
$ type nano
nano is /usr/bin/nano
nl
文件的行数。斯科特·巴特拉姆和大卫·麦肯齐(2020 年)。更多信息:https://www.gnu.org/software/coreutils。
$ type nl
nl is /usr/bin/nl
$ man nl
$ nl /data/ch05/alice.txt | head
1 Project Gutenberg's Alice's Adventures in Wonderland, by Lewis Carroll
2
3 This eBook is for the use of anyone anywhere at no cost and with
4 almost no restrictions whatsoever. You may copy it, give it away or
5 re-use it under the terms of the Project Gutenberg License included
6 with this eBook or online at www.gutenberg.org
7
8
9 Title: Alice's Adventures in Wonderland
10
parallel
从标准输入并行构建和执行 Shell 命令行。奥勒·葛覃(2016 年版)。更多信息:https://www.gnu.org/software/parallel。
$ type parallel
parallel is /usr/bin/parallel
$ man parallel
$ seq 3 | parallel "echo Processing file {}.csv"
Processing file 1.csv
Processing file 2.csv
Processing file 3.csv
paste
合并文件行。paste(8.30 版)作者大卫·m·伊纳特和大卫·麦肯齐(2019)。更多信息:https://www.gnu.org/software/coreutils。
$ type paste
paste is /usr/bin/paste
$ man paste
$ paste -d, <(seq 5) <(dseq 5)
1,2022-03-04
2,2022-03-05
3,2022-03-06
4,2022-03-07
5,2022-03-08
$ seq 5 | paste -sd+
1+2+3+4+5
pbc
平行公元前。耶鲁安·扬森斯(2021)。更多信息:https://github.com/jeroenjanssens/dsutils。
$ type pbc
pbc is /usr/bin/dsutils/pbc
$ seq 3 | pbc '{1}^2'
1
4
9
pip
安装和管理 Python 包的工具。PyPA (2020 年)的pip(版本 20.0.2)。更多信息: https://pip.pypa.io 。
$ type pip
pip is /usr/bin/pip
$ man pip
$ pip install pandas
$ pip freeze | grep sci
scikit-learn==0.24.2
scipy==1.7.0
pup
在命令行解析 HTML。pup(版本 0.4.0)作者 Eric Chiang (2016)。更多信息:https://github.com/EricChiang/pup。
$ type pup
pup is /usr/bin/pup
$ pup --help
pwd
打印工作目录的名称。pwd(8.30 版)作者吉姆·迈耶林(2019)。更多信息:https://www.gnu.org/software/coreutils。
$ type pwd
pwd is a shell builtin
$ man pwd
$ cd ~
$ pwd
/home/dst
python
一种解释性的、交互式的、面向对象的编程语言。python(版本 3.8.5)由 Python 软件基金会(2021)提供。更多信息:https://www.python.org。
$ type python
python is an alias for python3
$ man python
R
统计计算的语言和环境。R(版本 4.0.4)由 R 统计计算基金会(2021)提供。更多信息:https://www.r-project.org。
$ type R
R is /usr/bin/R
$ man R
rev
逐字符反转行。rev(版本 2.36.1)作者卡雷尔·扎克(2021)。更多信息:https://www.kernel.org/pub/linux/utils/util-linux。
$ type rev
rev is /usr/bin/rev
$ echo 'Satire: Veritas' | rev
satireV :eritaS
$ echo 'Ça va?' | rev | cut -c 2- | rev
Ça va
rm
删除文件或目录。保罗·鲁宾、大卫·麦肯齐、理查德·M·斯托曼和吉姆·迈耶林(2019 年)。更多信息:https://www.gnu.org/software/coreutils。
$ type rm
rm is /usr/bin/rm
$ man rm
$ rm *.old
rush
来自 Shell 的 R 一行程序。rush(0.1 版)作者耶鲁安·扬森斯(2021)。更多信息:https://github.com/jeroenjanssens/rush。
$ type rush
rush is /usr/local/lib/R/site-library/rush/exec/rush
$ rush --help
$ rush run '6*7'
42
$ rush run --tidyverse 'filter(starwars, species == "Human") %>% select(name)'
# A tibble: 35 x 1
name
<chr>
1 Luke Skywalker
2 Darth Vader
3 Leia Organa
4 Owen Lars
5 Beru Whitesun lars
6 Biggs Darklighter
7 Obi-Wan Kenobi
8 Anakin Skywalker
9 Wilhuff Tarkin
10 Han Solo
# … with 25 more rows
sample
在给定的延迟和一定的持续时间内,根据某种概率从标准输入中过滤掉行。耶鲁安·扬森斯著(2021 年)。更多信息:https://github.com/jeroenjanssens/sample。
$ type sample
sample is /usr/local/bin/sample
$ sample --help
$ seq 1000 | sample -r 0.01 | trim 5
893
912
scp
OpenSSH 安全文件复制。提莫·铃音和塔图·伊洛宁(2019 年)。更多信息:https://www.openssh.com。
$ type scp
scp is /usr/bin/scp
$ man scp
sed
用于过滤和转换文本的流编辑器。杰伊·芬拉森、汤姆·洛德、肯·皮齐尼和保罗·邦奇尼(2018 年)。更多信息:https://www.gnu.org/software/sed。
$ type sed
sed is /usr/bin/sed
$ man sed
seq
打印一系列数字。seq(8.30 版)作者:乌尔里希·德雷珀(2019)。更多信息:https://www.gnu.org/software/coreutils。
$ type seq
seq is /usr/bin/seq
$ man seq
$ seq 3
1
2
3
$ seq 10 5 20
10
15
20
servewd
使用简单的 HTTP 服务器提供当前工作目录。servewd(0.1 版)作者耶鲁安·扬森斯(2021)。更多信息:https://github.com/jeroenjanssens/dsutils。
$ type servewd
servewd is /usr/bin/dsutils/servewd
$ servewd --help
$ cd /data && servewd 8000
shuf
生成随机排列。保罗·埃格特著(2019 年)。更多信息:https://www.gnu.org/software/coreutils。
$ type shuf
shuf is /usr/bin/shuf
$ man shuf
$ echo {a..z} | tr ' ' '\n' | shuf | trim 5
m
v
l
h
j
… with 21 more lines
$ shuf -i 1-100 | trim 5
56
8
7
88
4
… with 95 more lines
skll
sci kit-学习实验室。skll(版本 2.5.0)由教育测试服务(2021)提供。实际工具是run_experiment。我使用别名skll,因为我觉得更容易记住。更多信息:https://skll.readthedocs.org。
$ type skll
skll is an alias for run_experiment
$ skll --help
sort
对文本文件行进行排序。迈克·哈尔特尔和保罗·埃格特(2019 年)。更多信息:https://www.gnu.org/software/coreutils。
$ type sort
sort is /usr/bin/sort
$ man sort
$ echo '3\n7\n1\n3' | sort
1
3
3
7
split
把文件分成几部分。托尔比约恩·格兰隆德和理查德·M·斯托曼(2019 年)著split(版本 8.30)。更多信息:https://www.gnu.org/software/coreutils。
$ type split
split is /usr/bin/split
$ man split
sponge
吸收标准输入并写入文件。科林·沃森和托勒夫·福格·希恩(2021 年)。如果您希望在单个管道中读取和写入同一个文件,这很有用。更多信息:https://joeyh.name/code/moreutils。
$ type sponge
sponge is /usr/bin/sponge
sql2csv
对数据库执行 SQL 查询,并将结果输出到 CSV 文件。克里斯托弗·格罗斯科夫(Christopher Groskopf)著(2020 年)。更多信息:https://csvkit.rtfd.org。
$ type sql2csv
sql2csv is /usr/bin/sql2csv
$ sql2csv --help
ssh
OpenSSH 远程登录客户端。ssh(版本 1:8.2p1-4ubuntu0.2)作者:塔图·伊洛宁、亚伦·坎贝尔、鲍勃·贝克、马库斯·弗里德尔、尼尔斯·普罗沃斯、西奥·拉阿德、达格·宋和马库斯·弗里德尔(2020)。更多信息:https://www.openssh.com。
$ type ssh
ssh is /usr/bin/ssh
$ man ssh
sudo
作为另一个用户执行命令。sudo(版本 1.8.31)作者托德·c·米勒(2019)。更多信息: https://www.sudo.ws 。
$ type sudo
sudo is /usr/bin/sudo
$ man sudo
tail
输出文件的最后一部分。保罗·鲁宾、大卫·麦肯齐、伊恩·兰斯·泰勒和吉姆·迈耶林(2019 年)。更多信息:https://www.gnu.org/software/coreutils。
$ type tail
tail is /usr/bin/tail
$ man tail
tapkee
一个高效的降维库。Sergey Lisitsyn、Christian Widmer 和 Fernando J. Iglesias Garcia (2013 年)撰写的tapkee(版本 1.2)。更多信息:http://tapkee.lisitsyn.me。
$ type tapkee
tapkee is /usr/bin/tapkee
$ tapkee --help
tar
存档工具。约翰·吉尔摩和杰伊·芬拉森(2014 年)。更多信息:https://www.gnu.org/software/tar。
$ type tar
tar is /usr/bin/tar
$ man tar
tee
从标准输入读取并写入标准输出和文件。迈克·帕克,理查德·M·斯托曼和大卫·麦肯齐(2019 年)。更多信息:https://www.gnu.org/software/coreutils。
$ type tee
tee is /usr/bin/tee
$ man tee
telnet
TELNET 协议的用户界面。Mats Erik Andersson,Andreas Henriksson 和 Christoph Biedl (1999 年)。更多信息:http://www.hcs.harvard.edu/~dholland/computers/netkit.html。
$ type telnet
telnet is /usr/bin/telnet
tldr
控制台命令的协作清单。tldr(版本 3.3.7)作者欧文·沃克(2021)。更多信息: https://tldr.sh 。
$ type tldr
tldr is /usr/local/bin/tldr
$ tldr --help
$ tldr tar | trim
✔ Page not found. Updating cache...
✔ Creating index...
tar
Archiving utility.
Often combined with a compression method, such as gzip or bzip2.
More information: https://www.gnu.org/software/tar.
- [c]reate an archive and write it to a [f]ile:
tar cf target.tar file1 file2 file3
… with 22 more lines
tr
翻译或删除字符。tr(8.30 版)作者吉姆·迈耶林(2018)。更多信息:https://www.gnu.org/software/coreutils。
$ type tr
tr is /usr/bin/tr
$ man tr
tree
以树状格式列出目录的内容。tree(版本 1.8.0)作者史蒂夫·贝克(2018)。更多信息:https://launchpad.net/ubuntu/+source/tree。
$ type tree
tree is /usr/bin/tree
$ man tree
$ tree / | trim
/
├── bin -> usr/bin
├── boot
├── data
│ ├── ch02
│ │ ├── fac.py
│ │ └── movies.txt
│ ├── ch03
│ │ ├── logs.tar.gz
│ │ ├── r-datasets.db
… with 121442 more lines
trim
将输出修剪到给定的高度和宽度。耶鲁安·扬森斯(2021)。更多信息:https://github.com/jeroenjanssens/dsutils。
$ type trim
trim is /usr/bin/dsutils/trim
$ echo {a..z}-{0..9} | fold | trim 5 60
a-0 a-1 a-2 a-3 a-4 a-5 a-6 a-7 a-8 a-9 b-0 b-1 b-2 b-3 b-4…
c-0 c-1 c-2 c-3 c-4 c-5 c-6 c-7 c-8 c-9 d-0 d-1 d-2 d-3 d-4…
e-0 e-1 e-2 e-3 e-4 e-5 e-6 e-7 e-8 e-9 f-0 f-1 f-2 f-3 f-4…
g-0 g-1 g-2 g-3 g-4 g-5 g-6 g-7 g-8 g-9 h-0 h-1 h-2 h-3 h-4…
i-0 i-1 i-2 i-3 i-4 i-5 i-6 i-7 i-8 i-9 j-0 j-1 j-2 j-3 j-4…
… with 8 more lines
ts
时间戳输入。乔伊·赫斯(Joey Hess)著(2021)。更多信息:https://joeyh.name/code/moreutils。
$ type ts
ts is /usr/bin/ts
$ echo seq 5 | sample -d 500 | ts
Mar 03 11:07:09 seq 5
type
显示命令行工具的类型和位置。type是一个 ZShell 内置。
$ type type
type is a shell builtin
$ man zshbuiltins | grep -A 10 '^ *type '
uniq
报告或省略重复的行。uniq(8.30 版)作者理查德·M·斯托曼和大卫·麦肯齐(2019)。更多信息:https://www.gnu.org/software/coreutils。
$ type uniq
uniq is /usr/bin/uniq
$ man uniq
unpack
提取常见的文件格式。帕特里克·布里斯班(2013 年)。更多信息:https://github.com/jeroenjanssens/dsutils。
$ type unpack
unpack is /usr/bin/dsutils/unpack
unrar
从 rar 档案中提取文件。本·阿瑟斯汀、克里斯蒂安·舍勒和约翰内斯·温克尔曼(2014 年)。更多信息:http://home.gna.org/unrar。
$ type unrar
unrar is /usr/bin/unrar
$ man unrar
unzip
在 ZIP 存档中列出、测试和提取压缩文件。(版本 6.0)作者:塞缪尔·h·史密斯、埃德·戈登、克里斯蒂安·斯皮勒、翁诺·林登、麦克·怀特、凯·乌维·隆美尔、史蒂文·m·施韦达、保罗·基尼茨、克里斯·赫博斯、乔纳森·哈德森、塞尔吉奥·莫内西、哈拉尔德·登克、约翰·布什、亨特·戈特利、史蒂夫·索尔兹伯里、史蒂夫·米勒和戴夫·史密斯(2009)。更多信息:http://www.info-zip.org/pub/infozip。
$ type unzip
unzip is /usr/bin/unzip
$ man unzip
vw
在线学习的快速机器学习库。约翰·兰福德(2021 年)。更多信息:https://vowpalwabbit.org。
$ type vw
vw is /usr/local/bin/vw
$ vw --help --quiet
wc
打印每个文件的换行、字数和字节数。wc(8.30 版)作者保罗·鲁宾和大卫·麦肯齐(2019)。更多信息:https://www.gnu.org/software/coreutils。
$ type wc
wc is /usr/bin/wc
$ man wc
which
找到一个命令。which(0.1 版本)由未知(2016)提供。更多信息:。
$ type which
which is a shell builtin
$ man which
xml2json
使用 xml 映射将 XML 输入转换为 JSON 输出。弗朗索瓦·帕门蒂尔(2016 年)。更多信息:https://github.com/parmentf/xml2json。
$ type xml2json
xml2json is /usr/local/bin/xml2json
xmlstarlet
命令行 XML/XSLT 工具包。xmlstarlet(版本 1.6.1)作者:Dagobert Michelsen、Noam Postavsky 和 Mikhail Grushinskiy (2019)。更多信息:https://sourceforge.net/projects/xmlstar。
$ type xmlstarlet
xmlstarlet is /usr/bin/xmlstarlet
$ man xmlstarlet
xsv
用 Rust 编写的快速 CSV 命令行工具包。xsv(0 . 13 . 0 版本)作者安德鲁·格兰特(2018)。更多信息:https://github.com/BurntSushi/xsv。
$ type xsv
xsv is /usr/bin/xsv
$ xsv --help
zcat
解压缩并连接文件到标准输出。保罗·埃格特著(2021)。更多信息:https://www.nongnu.org/zutils/zutils.html。
$ type zcat
zcat is /usr/bin/zcat
$ man zcat
zsh
ZShell。保罗·法尔斯塔德和彼得·斯蒂芬森(2020 年)。更多信息:https://www.zsh.org。
$ type zsh
zsh is /usr/bin/zsh
$ man zsh
序言
原文:https://datascienceatthecommandline.com/2e/foreword.html
贡献者:Ting-xin
这是一见钟情。
大概是在 1981 年或 1982 年左右,我第一次接触到了 Unix。它的命令行界面 Shell,用同样的语言来处理单个命令和复杂的程序的方式深刻的改变了我的世界,从此我再也没有回头。
我是一个作家,同时也沉迷于计算机的一些有趣之处,正则表达式是我的碰到的第一个乐趣。我第一次是在 HP 的 RTE 操作系统的文本编辑器中尝试了正则表达式,但是当我接触到 Unix ,了解到 Unix 利用 Shell 将许多的小型实用工具链接在一起的哲学观念时,我才真正的完全理解了它的威力(译者注:Shell 中使用正则表达式启发了作者)。毫无疑问,ed、ex、vi(现在是vim)和emacs中的正则表达式也很强大,但是直到我看到在 Unix 输入ex的脚本命令后如何启动了 Unix 流编辑器sed时;利用 AWK 将正则表达式的简单脚本应用到文件上时;以及 Shell 脚本如何让你不仅从现有的工具中,而且从你自己编写的新工具中构建管道的时候,我才真正明白了正则表达式和 Shell 的强大。编程是你与计算机交流的一种方式,通过编程你能告诉计算机你想让它们做什么,通常我们想干的事不是一次性的,而是想要以持久的方式,所以编程语言通常像人类语言一样是可以变化的,有可重复的结构,有不同的动词和对象。
作为一个初学者,其他形式的编程似乎更像是需要严格遵守的食谱、小心翼翼的咒语或者像是在等待老师给你写的论文打分,因此你必须把每件事都做对。但是使用 Shell 编程就没有编译和等待。这更像是和一个朋友之间的谈话。当朋友不理解时,你可以很容易地再试一次。更重要的是,如果你只是有简单的事情要说,你可以只用一个词来说。而且,你可能想说的一大堆话可能都已经有了对应的说法。如果没有,你可以很容易地创造出新的单词。最终你可以把你学到的单词和你编造的单词串成逐渐复杂的句子、段落,并最终达到说服力的文章。
几乎所有其他的编程语言都比 Shell 及其相关工具更加强大,但至少对我来说,没有一种语言能比 Shell 提供更容易进入编程思维的途径,也没有一种语言能提供更好的环境来与我们工作的机器进行日常对话。正如 AWK 的创始人之一、也是《Unix 编程环境》这本了不起的书的作者之一布莱恩·柯尼根(Brian Kernighan)在 2019 年接受莱克斯·弗里德曼(Lex Fridman)采访时所说,“Unix 本来就是一个非常容易编写程序的环境”。【00:23:10】他继续解释为什么他在探索数据时仍然经常使用 AWK 而不是编写 Python 程序:“它不能扩展成大的程序,但是它在那些你想看到一些东西的小事情上做得非常好。”【00:37:01】
在《命令行上的数据科学》一书中,耶鲁安·扬森斯(Jeroen Janssens)展示了即使在现在, Unix/Linux 的命令行方式也是非常强大。如果耶鲁安(Jeroen)还没有这样做,我也会在这里写一篇文章,解释为什么命令行对于数据科学中经常遇到的各种任务是如此的亲近和匹配。但是他已经在他的书的开头解释了这一点。所以我只想说:你越是使用命令行,你就会发现自己越是青睐于使用它,因为它是完成大部分工作的最简单方法。无论你是一个 Shell 新手,还是一个没有考虑过 Shell 编程对数据科学有多大帮助的人,这本书都会让你受益匪浅。Jeroen 是一位伟大的老师,他所涉及的材料是无价的。
—Tim O’Reilly
May 2021
原文:https://datascienceatthecommandline.com/2e/preface.html
贡献者:Ting-xin
数据科学是一个令人兴奋的工作领域,相对而言它还比较年轻。不幸的是,许多人和许多公司都认为,你需要新技术来解决数据科学带来的问题。然而,正如这本书所展示的,许多事情可以通过使用命令行来完成,有时它是一种更有效的方式。
在我读博士期间,我逐渐从使用微软 Windows 转向使用 Linux。因为这种过渡一开始有点吓人,所以我先把两个操作系统安装在一起(称为双启动)。在 Microsoft Windows 和 Linux 之间来回切换的冲动最终小时了,甚至在某个时候我开始摆弄 Arch Linux 了,它允许你从头开始构建自己的 Linux 机器。从 Linux 能得到的只是命令行,至于怎么做完全就看你自己了。出于需要,我很快就能非常自如地使用命令行了。最终,随着空闲时间变得越来越少,并且由于 Linux 发行版 Ubuntu 的易用性和庞大的社区,我最终选择了它作为我的操作系统,但是命令行仍然是我花费时间最多的地方。
事实上在不久之前,我就意识到命令行不仅仅是用来安装软件、配置系统和搜索文件的。我开始学习诸如cut、sort和sed之类的工具。这些都是命令行工具的例子,它们将数据作为输入,对其进行处理,并打印结果。Ubuntu 自带了其中一些小工具,当我意识到了利用这些小工具的潜力,我就被深深地吸引住了。
在获得博士学位后,我成为了一名数据科学家,我就想尽可能多地使用命令行这种方法来做数据科学。多亏了一些新的开源命令行工具,包括xml2json、jq和json2csv,我甚至能够使用命令行来完成抓取网站和处理大量 JSON 数据等任务。
2013 年 9 月,我决定写一篇名为数据科学的七个命令行工具的博客(译者:相关博客的内容是几个命令行工具的简要用法)。令我惊讶的是,这篇博文引起了相当多的反响,并且我收到了很多关于其他命令行工具的建议。于是我开始有了一个想法,我是不是可以把这篇博文扩充成一本书?大约 10 个月后,我在许多有才华的人的帮助下(见致谢),给出了这个问题的答案——YES!
我分享这个故事并不是因为我想让你该知道这本书是怎么来的,而是因为我想表明一个事实:我也必须学习命令行。因为命令行与使用图形用户界面有很大的不同,所以它一开始看起来很吓人。但如果我能学会,那你应该也可以。不管你当前的操作系统是什么,也不管你当前如何处理数据,当你读完这本书后,你将能够以命令行的方式开展数据科学。如果您已经熟悉了命令行,即使说你熟的做梦都是 Shell 编程了,但是你仍然有可能发现一些有趣的技巧或命令行工具,用于你的下一个数据科学项目。
对这本书有什么期待
在本书中,我们将获取、清理、探索和建模大量的数据。这本书并不是关于如何让这些数据科学任务中变得更合理。这个领域现在已经有了很好的资源了,例如,何时应用哪种统计测试、如何更好地将数据可视化。相反,这本书旨在通过教你如何在命令行中执行这些数据科学任务,从而使你更有效率和生产力。
虽然这本书讨论了 90 多种命令行工具,但最重要的不是工具本身。一些命令行工具已经存在了很长时间,而另一些将被更好的工具所取代。就在你阅读这篇文章的时候,新的命令行工具也许正在诞生。这些年来,我发现了许多令人惊奇的命令行工具。不幸的是,其中一些发现得太晚,没有收入书中。简而言之,命令行工具来来去去,但是没关系,随它去吧。
最重要的是如何使用工具、管道和数据的基本理念。大多数命令行工具只做一件事并把它做得很好。这句话是 Unix 哲学的一部分,也将在整本书中多次出现。一旦你熟悉了命令行,知道如何组合使用命令行工具,你甚至可以创建新的命令行工具,这也意味着你已经发展了一项宝贵的技能。
第二版的变化
虽然命令行作为一项技术和一种工作方式是不过时的,但第一版中讨论的一些工具要么已经被更新的工具取代(例如,csvkit几乎已经被xsv取代),要么被它们的开发人员放弃(例如,drake),或者说它们已经是次优的选择(例如,weka)。自 2014 年 10 月第一版出版以来,我学到了很多东西,要么是通过我自己本身的经历,要么是来自读者有用的反馈。尽管这本书非常的小众,因为它位于两个学科的交叉点,但数据科学界仍然对它保持着稳定的兴趣,我几乎每天都收到的许多积极的消息就证明了这一点。通过更新第一版,我希望能让这本书至少再保持五年的相关性。以下是我所做的非详尽的改变清单:
- 我尽可能把
csvkit换成了xsv,xsv是处理 CSV 文件的一种更快的替代方式 - 在 2.2 和 3.2 节中,我用 Docker 映像替换了 VirtualBox 映像,Docker 是一种比 VirtualBox 更快、更轻量级的运行隔离环境的方式
- 我现在使用
pup而不是scrape来处理 HTMLscrape是我自己创造的一个 Python 工具,pup速度更快,功能更多,更易于安装 - 第六章已经从头重写,我现在用
make而不是drake来做项目管理,drake不再维护,make更加成熟,并且非常受开发者欢迎 - 我把
Rio换成了rush,Rio是我自己创建的一个笨拙的 Bash 脚本,rush是一个 R 包,它是从命令行使用 R 的一种更加稳定和灵活的方式 - 在第九章中,我用 Vowpal Wabbit(
vw)替换了 Weka 和 BigML,Weka 很旧,从命令行使用它的方式也很笨拙。BigML 是一个商业 API,我不想再依赖它。Vowpal Wabbit 是一个非常成熟的机器学习工具,它开始在雅虎开发,现在在微软。 - 第十章是关于将命令行集成到现有工作流的全新章节,包括 Python、R 和 Apache Spark。在第一版中,我提到过命令行可以很容易地与现有的工作流集成,但我从未深入探讨过,本章解决了这个问题
如何阅读这本书
总的来说,我建议你以线性方式阅读这本书。一旦我在前面介绍了一个概念或命令行工具,我就有可能在后面的章节中用到它。例如,在第九章中,我大量使用了在第八章中广泛介绍的并行。
数据科学是一个广泛的领域,与许多其他领域都都有交叉,比如编程、数据可视化和机器学习。因此,这本书触及了许多有趣的话题,但遗憾的是,这些话题无法得到充分的讨论。在全书中,每一章的结尾都有进一步探索的建议。为了跟上本书的进度,并不要求阅读这些材料,但当你有兴趣时,你就知道还有很多东西要学。
这本书是写给谁的
本书只对你做了一个假设:你在与数据打交道。你目前正在使用哪种编程语言或统计计算环境并不重要。本书从一开始就解释了所有必要的概念。
无论你的操作系统是微软的 Windows,macOS,还是某种类型的 Linux,这些都无所谓。本书附带一个 Docker 镜像,这是一个易于安装的虚拟环境,它允许你在与本书相同的环境中运行命令行工具并跟随代码示例。你不必浪费时间去弄清楚如何安装所有的命令行工具和它们的依赖关系。
这本书包含了一些 Bash、Python 和 R 的代码,所以如果你有一些编程经验的话会很有帮助,你也不一定非要跟着例子走。
本书中使用的惯例
本书使用了以下排版惯例:
Italic
表示新的术语、URL、目录名和文件名。
Constant width
用于代码和命令,以及在段落中引用命令行工具及其选项。
Constant width bold
显示应由用户按字面意思输入的命令或其他文本。
This element signifies a tip or suggestion.
This element signifies a general note.
This element indicates a warning or caution.
致谢
第二版致谢(2021)
自第一版问世以来,已经过去了七年。在这段时间里,特别是在过去 13 个月里,许多人帮助了我。没有他们,我就永远无法写出第二版。
我再次有幸与 O'Reilly 公司中的三位出色的编辑合作。我要感谢 "拥抱最后期限 "的 Sarah Grey、"全力以赴 "的 Jess Haberman 和 "顺其自然 "的 Kate Galloway。在他们不可思议的帮助下,我能够最后期限前完成,并且在关键时刻心无旁骛,最终放手其它事。我还要感谢他们的同事:Angela Rufino, Arthur Johnson, Cassandra Furtado, David Futato, Helen Monroe, Karen Montgomery, Kate Dullea, Kristen Brown, Marie Beaugureau, Marsee Henon, Nick Adams, Regina Wilkinson, Shannon Cutt, Shannon Turlington, and Yasmina Greco, 是他们让我与 O'Reilly 公司的合作变得如此愉快。
尽管有一个自动化的程序来执行代码并将结果粘贴回来(感谢 R Markdown 和 Docker),但我能够犯的错误数量还是令人印象深刻。感谢 Aaditya Maruthi、Brian Eoff、Caitlin Hudon、Julia Silge、Mike Dewar 和 Shane Reustle 极大地减少了这个数字。当然,留下的任何错误都是我的责任。
Marc Canaleta 值得我们特别感谢。2014 年 10 月,在第一版问世后不久,马克邀请我为他在巴塞罗那 Social Point 的团队举办了为期一天的关于命令行的数据科学的研讨会。我们都不知道,接下来会有很多研讨会。这最终促使我成立了自己的公司:数据科学研讨会。每次授课,我都能学到新东西。他们可能不知道,但每个学生都以这样或那样的方式对这本书产生了影响。我对他们说:谢谢你们。我希望我可以在很长一段时间内教书。
吸引人的对话、精彩的建议和热情的拉动请求。我非常感谢以下慷慨人士的每一个贡献:Adam Johnson, Andre Manook, Andrea Borruso, Andres Lowrie, Andrew Berisha, Andrew Gallant, Andrew Sanchez, Anicet Ebou, Anthony Egerton, Ben Isenhart, Chris Wiggins, Chrys Wu, Dan Nguyen, Darryl Amatsetam, Dmitriy Rozhkov, Doug Needham, Edgar Manukyan, Erik Swan, Felienne Hermans, George Kampolis, Giel van Lankveld, Greg Wilson, Hay Kranen, Ioannis Cherouvim, Jake Hofman, Jannes Muenchow, Jared Lander, Jay Roaf, Jeffrey Perkel, Jim Hester, Joachim Hagege, Joel Grus, John Cook, John Sandall, Joost Helberg, Joost van Dijk, Joyce Robbins, Julian Hatwell。Karlo Guidoni, Karthik Ram, Lissa Hyacinth, Longhow Lam, Lui Pillmann, Lukas Schmid, Luke Reding, Maarten van Gompel, Martin Braun, Max Schelker, Max Shron, Nathan Furnal, Noah Chase, Oscar Chic, Paige Bailey, Peter Saalbrink, Rich Pauloo, Richard Groot, Rico Huijbers, Rob Doherty, Robbert van Vlijmen, Russell Scudder, Sylvain Lapoix, TJ Lavelle, Tan Long, Thomas Stone, Tim O'Reilly, Vincent Warmerdam, and Yihui X。
在本书中,特别是在脚注和附录中,你会发现有数百个名字。这些名字属于许多工具、书籍和其他资源的作者,而本书正是基于这些资源。我非常感谢他们的辛勤工作,无论这些工作是在 50 年前还是 50 天前完成的。
最重要的是,我要感谢我的妻子 Esther、我的女儿 Florien 和我的儿子 Olivier,他们每天都在提醒我什么才是真正重要的。我保证要过几年才会开始写第三版。
第一版致谢(2014)
首先,我要感谢 Mike Dewar 和 Mike Loukides,他们相信我在 2013 年 9 月写的博客文章七个数据科学命令行工具可以扩展成一本书。
特别感谢我的技术审查员 Mike Dewar、Brian Eoff 和 Shane Reustle 阅读了各种草稿,仔细测试了所有命令,并提供了宝贵的反馈。他们的努力极大地改进了这本书,留下的任何错误都是我的责任。
我有幸与三位了不起的编辑一起工作:Ann Spencer, Julie Steele, 和 Marie Beaugureau。感谢你们的指导,感谢你们成为 O'Reilly 公司众多优秀人才的联络人。这些人包括 Laura Baldwin, Huguette Barriere, Sophia DeMartini, Yasmina Greco, Rachel James, Ben Lorica, Mike Loukides, and Christopher Pappas。还有许多人我没有见过,因为他们在幕后操作。他们共同确保了与 O'Reilly 公司的合作确实是一种乐趣。
本书讨论了 80 多个命令行工具。不用说,没有这些工具,这本书首先就不会存在。因此,我非常感谢所有创造和贡献这些工具的作者。遗憾的是,完整的作者名单太长了,无法在此列出;在附录中提到了他们。特别感谢 Aaron Crow、Jehiah Czebotar、Christoph Groskopf、Dima Kogan、Sergey Lisitsyn、Francisco J. Martin 和 Ole Tange 提供的帮助,他们的命令行工具令人惊叹。
Eric Postma 和 Jaap van den Herik 在我的博士课程期间指导过我,值得特别感谢。在五年的时间里,他们给我上了很多课。尽管写一本技术书与写一篇博士论文有很大的不同,但其中许多教训在过去九个月里也被证明是非常有帮助的。
最后,我要感谢我在 YPlan 的同事,我的朋友,我的家人,特别是我的妻子 Esther,感谢他们支持我,并在适当的时候把我从命令行的工作中拉出来。


浙公网安备 33010602011771号