├── README.md
├── 第01章 准备工作.md
├── 第02章 Python语法基础,IPython和Jupyter Notebooks.md
├── 第03章 Python的数据结构、函数和文件.md
├── 第04章 NumPy基础:数组和矢量计算.md
├── 第05章 pandas入门.md
├── 第06章 数据加载、存储与文件格式.md
├── 第07章 数据清洗和准备.md
├── 第08章 数据规整:聚合、合并和重塑.md
├── 第09章 绘图和可视化.md
├── 第10章 数据聚合与分组运算.md
├── 第11章 时间序列.md
├── 第12章 pandas高级应用.md
├── 第13章 Python建模库介绍.md
├── 第14章 数据分析案例.md
├── 附录A NumPy高级应用.md
└── 附录B 更多关于IPython的内容(完).md
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | 支持免费测试!Swiftproxy 提供全球高质量纯净住宅IP,包括动态住宅代理和静态住宅代理,其最大的特点是其动态流量不过期,流量用完为止,静态IP购买后30天内不限制流量。越来越多的企业选择 Swiftproxy 来满足其住宅代理需求,使其成为网络爬虫和社交媒体管理的首选。Swiftproxy 提供超过 9000 万个 IP 地址,并配备免费的精准定位功能,非常适合社交媒体管理、市场调研和公共数据采集。
4 |
5 |
6 | https://www.swiftproxy.net
7 |
8 |
9 |
10 | Swiftproxy具备哪些优势?
11 | - 1.庞大的 IP 池和精确定位: 覆盖全球 220 个地区,可将代理服务器定位到城市级别,可适用于网页抓取、数据收集、社交媒体营销、广告验证、品牌保护、SEO等业务。同时,Swiftproxy提供庞大的IP池,具有超9000万个真实住宅IP,确保IP的纯净可靠。
12 | - 2.灵活会话: 提供轮转会话和粘性会话,允许用户在不同 IP 之间切换或长期保持同一 IP。
13 | - 3.多协议支持: 支持 HTTP/HTTPS 和 SOCKS5 协议,可以与 650 多种工具和软件选项轻松集成。
14 | - 4.购买的流量永不过期: 用户可以按照自己的节奏使用购买的数据,无需担心流量过期。
15 |
16 | ***
17 |
18 |
78 |
79 | ***
80 |
81 | > 新版《利用Python进行数据分析》上市后,我一直在寻找一本Python数据分析的进阶书。经过漫长的搜索和等待,总算找到了,书名是《Fast Python》(中文书名极速Python)。
82 |
83 |
111 |
112 |
113 |
技术链条:Pandas > Arrow > Ray > ChatGPT > ?
114 |
115 |
116 | 这本书在Amazon上市后,评论并不多,只有两个5星评价。但是,看完目录后,我立即就想认真读一读。《利用Python进行数据分析》主要围绕NumPy、Pandas、Matplotlib,内容比较偏基础。而工作中要处理的数据量变得越来越大,对技术的要求越来越高,基础方法已经不够用了,必须使用能处理大规模数据集的新方法。《极速Python》从软件到硬件,从单机到分布式,对Python高性能编程和大数据分析优化进行了系统性讲解。内容亮点包括Python代码分析、数据结构优化、内存优化、高并发编程、NumPy编程、Cython代码重构、pandas进阶、数据存储。针对当下最热点的技术领域,本书还重点讲解了Arrow、GPU编程和分布式数据处理。《Fast Python》比另一本优秀的《High Performance Python》的技术栈还要全!
117 |
118 | 结合大语言模型研究热潮,阅读《极速Python》可以更好地理解最新的Python数据分析技术对技术界产生了巨大的影响。Pandas的作者Wes近年来主要开发了Arrow。Arrow提供了一种高效的数据格式和交换方式,使得在不同的计算框架和编程语言之间进行数据交换和分析变得更加容易和高效。通过将Arrow和Pandas结合使用,可以获得高性能的数据处理和分析能力。Arrow提供了快速的数据传输和交换机制,而Pandas提供了丰富的数据操作和分析功能。这使得在大规模数据集上进行数据处理和分析变得更加高效和便捷。进而,分布式机器学习框架Ray在Datasets组件中使用了Arrow,而22年底爆火的ChatGPT就是用Ray训练而成的。
119 |
120 | ***
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 | 【Python数据分析群】,拉你入群 |
146 | 用三个月完成了翻译,工作砌码回家码字,手竟然脱皮了 :joy: |
147 |
148 |
149 |
150 |
151 |
152 | |
153 |
154 |
155 | |
156 |
159 |
160 |
161 |
162 |
163 |
164 |
--------------------------------------------------------------------------------
/第01章 准备工作.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
5 | ***
6 |
7 | # 第1章 准备工作
8 |
9 | ## 1.1 本书的内容
10 |
11 | 本书讲的是利用Python进行数据控制、处理、整理、分析等方面的具体细节和基本要点。我的目标是介绍Python编程和用于数据处理的库和工具环境,掌握这些,可以让你成为一个数据分析专家。虽然本书的标题是“数据分析”,重点却是Python编程、库,以及用于数据分析的工具。这就是数据分析要用到的Python编程。
12 |
13 | ### 什么样的数据?
14 |
15 | 当书中出现“数据”时,究竟指的是什么呢?主要指的是结构化数据(structured data),这个故意含糊其辞的术语代指了所有通用格式的数据,例如:
16 |
17 | * 表格型数据,其中各列可能是不同的类型(字符串、数值、日期等)。比如保存在关系型数据库中或以制表符/逗号为分隔符的文本文件中的那些数据。
18 | * 多维数组(矩阵)。
19 | * 通过关键列(对于SQL用户而言,就是主键和外键)相互联系的多个表。
20 | * 间隔平均或不平均的时间序列。
21 |
22 | 这绝不是一个完整的列表。大部分数据集都能被转化为更加适合分析和建模的结构化形式,虽然有时这并不是很明显。如果不行的话,也可以将数据集的特征提取为某种结构化形式。例如,一组新闻文章可以被处理为一张词频表,而这张词频表就可以用于情感分析。
23 |
24 | 大部分电子表格软件(比如Microsoft Excel,它可能是世界上使用最广泛的数据分析工具了)的用户不会对此类数据感到陌生。
25 |
26 | ## 1.2 为什么要使用Python进行数据分析
27 |
28 | 许许多多的人(包括我自己)都很容易爱上Python这门语言。自从1991年诞生以来,Python现在已经成为最受欢迎的动态编程语言之一,其他还有Perl、Ruby等。由于拥有大量的Web框架(比如Rails(Ruby)和Django(Python)),自从2005年,使用Python和Ruby进行网站建设工作非常流行。这些语言常被称作脚本(scripting)语言,因为它们可以用于编写简短而粗糙的小程序(也就是脚本)。我个人并不喜欢“脚本语言”这个术语,因为它好像在说这些语言无法用于构建严谨的软件。在众多解释型语言中,由于各种历史和文化的原因,Python发展出了一个巨大而活跃的科学计算(scientific computing)社区。在过去的10年,Python从一个边缘或“自担风险”的科学计算语言,成为了数据科学、机器学习、学界和工业界软件开发最重要的语言之一。
29 |
30 | 在数据分析、交互式计算以及数据可视化方面,Python将不可避免地与其他开源和商业的领域特定编程语言/工具进行对比,如R、MATLAB、SAS、Stata等。近年来,由于Python的库(例如pandas和scikit-learn)不断改良,使其成为数据分析任务的一个优选方案。结合其在通用编程方面的强大实力,我们完全可以只使用Python这一种语言构建以数据为中心的应用。
31 |
32 | ### Python作为胶水语言
33 |
34 | Python成为成功的科学计算工具的部分原因是,它能够轻松地集成C、C++以及Fortran代码。大部分现代计算环境都利用了一些Fortran和C库来实现线性代数、优选、积分、快速傅里叶变换以及其他诸如此类的算法。许多企业和国家实验室也利用Python来“粘合”那些已经用了多年的遗留软件系统。
35 |
36 | 大多数软件都是由两部分代码组成的:少量需要占用大部分执行时间的代码,以及大量不经常执行的“胶水代码”。大部分情况下,胶水代码的执行时间是微不足道的。开发人员的精力几乎都是花在优化计算瓶颈上面,有时更是直接转用更低级的语言(比如C)。
37 |
38 | ### 解决“两种语言”问题
39 |
40 | 很多组织通常都会用一种类似于领域特定的计算语言(如SAS和R)对新想法做研究、原型构建和测试,然后再将这些想法移植到某个更大的生产系统中去(可能是用Java、C\#或C++编写的)。人们逐渐意识到,Python不仅适用于研究和原型构建,同时也适用于构建生产系统。为什么一种语言就够了,却要使用两个语言的开发环境呢?我相信越来越多的企业也会这样看,因为研究人员和工程技术人员使用同一种编程工具将会给企业带来非常显著的组织效益。
41 |
42 | ### 为什么不选Python
43 |
44 | 虽然Python非常适合构建分析应用以及通用系统,但它对不少应用场景适用性较差。
45 |
46 | 由于Python是一种解释型编程语言,因此大部分Python代码都要比用编译型语言(比如Java和C++)编写的代码运行慢得多。由于程序员的时间通常都比CPU时间值钱,因此许多人也愿意对此做一些取舍。但是,在那些延迟要求非常小或高资源利用率的应用中(例如高频交易系统),耗费时间使用诸如C++这样更低级、更低生产率的语言进行编程也是值得的。
47 |
48 | 对于高并发、多线程的应用程序而言(尤其是拥有许多计算密集型线程的应用程序),Python并不是一种理想的编程语言。这是因为Python有一个叫做全局解释器锁(Global Interpreter Lock,GIL)的组件,这是一种防止解释器同时执行多条Python字节码指令的机制。有关“为什么会存在GIL”的技术性原因超出了本书的范围。虽然很多大数据处理应用程序为了能在较短的时间内完成数据集的处理工作都需要运行在计算机集群上,但是仍然有一些情况需要用单进程多线程系统来解决。
49 |
50 | 这并不是说Python不能执行真正的多线程并行代码。例如,Python的C插件使用原生的C或C++的多线程,可以并行运行而不被GIL影响,只要它们不频繁地与Python对象交互。
51 |
52 | ## 1.3 重要的Python库
53 |
54 | 考虑到那些还不太了解Python科学计算生态系统和库的读者,下面我先对各个库做一个简单的介绍。
55 |
56 | ### NumPy
57 |
58 | NumPy(Numerical Python的简称)是Python科学计算的基础包。本书大部分内容都基于NumPy以及构建于其上的库。它提供了以下功能(不限于此):
59 |
60 | * 快速高效的多维数组对象ndarray。
61 | * 用于对数组执行元素级计算以及直接对数组执行数学运算的函数。
62 | * 用于读写硬盘上基于数组的数据集的工具。
63 | * 线性代数运算、傅里叶变换,以及随机数生成。
64 |
65 | -成熟的C API, 用于Python插件和原生C、C++、Fortran代码访问NumPy的数据结构和计算工具。
66 |
67 | 除了为Python提供快速的数组处理能力,NumPy在数据分析方面还有另外一个主要作用,即作为在算法和库之间传递数据的容器。对于数值型数据,NumPy数组在存储和处理数据时要比内置的Python数据结构高效得多。此外,由低级语言(比如C和Fortran)编写的库可以直接操作NumPy数组中的数据,无需进行任何数据复制工作。因此,许多Python的数值计算工具要么使用NumPy数组作为主要的数据结构,要么可以与NumPy进行无缝交互操作。
68 |
69 | ### pandas
70 |
71 | pandas提供了快速便捷处理结构化数据的大量数据结构和函数。自从2010年出现以来,它助使Python成为强大而高效的数据分析环境。本书用得最多的pandas对象是DataFrame,它是一个面向列(column-oriented)的二维表结构,另一个是Series,一个一维的标签化数组对象。
72 |
73 | pandas兼具NumPy高性能的数组计算功能以及电子表格和关系型数据库(如SQL)灵活的数据处理功能。它提供了复杂精细的索引功能,能更加便捷地完成重塑、切片和切块、聚合以及选取数据子集等操作。因为数据操作、准备、清洗是数据分析最重要的技能,pandas是本书的重点。
74 |
75 | 作为背景,我是在2008年初开始开发pandas的,那时我任职于AQR Capital Management,一家量化投资管理公司,我有许多工作需求都不能用任何单一的工具解决:
76 |
77 | * 有标签轴的数据结构,支持自动或清晰的数据对齐。这可以防止由于数据不对齐,或处理来源不同的索引不同的数据,所造成的错误。
78 | * 集成时间序列功能。
79 | * 相同的数据结构用于处理时间序列数据和非时间序列数据。
80 | * 保存元数据的算术运算和压缩。
81 | * 灵活处理缺失数据。
82 | * 合并和其它流行数据库(例如基于SQL的数据库)的关系操作。
83 |
84 | 我想只用一种工具就实现所有功能,并使用通用软件开发语言。Python是一个不错的候选语言,但是此时没有集成的数据结构和工具来实现。我一开始就是想把pandas设计为一款适用于金融和商业分析的工具,pandas专注于深度时间序列功能和工具,适用于时间索引化的数据。
85 |
86 | 对于使用R语言进行统计计算的用户,肯定不会对DataFrame这个名字感到陌生,因为它源自于R的data.frame对象。但与Python不同,data frames是构建于R和它的标准库。因此,pandas的许多功能不属于R或它的扩展包。
87 |
88 | pandas这个名字源于panel data(面板数据,这是多维结构化数据集在计量经济学中的术语)以及Python data analysis(Python数据分析)。
89 |
90 | ### matplotlib
91 |
92 | matplotlib是最流行的用于绘制图表和其它二维数据可视化的Python库。它最初由John D.Hunter(JDH)创建,目前由一个庞大的开发团队维护。它非常适合创建出版物上用的图表。虽然还有其它的Python可视化库,matplotlib却是使用最广泛的,并且它和其它生态工具配合也非常完美。我认为,可以使用它作为默认的可视化工具。
93 |
94 | ### IPython和Jupyter
95 |
96 | IPython项目起初是Fernando Pérez在2001年的一个用以加强和Python交互的子项目。在随后的16年中,它成为了Python数据栈最重要的工具之一。虽然IPython本身没有提供计算和数据分析的工具,它却可以大大提高交互式计算和软件开发的生产率。IPython鼓励“执行-探索”的工作流,区别于其它编程软件的“编辑-编译-运行”的工作流。它还可以方便地访问系统的shell和文件系统。因为大部分的数据分析代码包括探索、试错和重复,IPython可以使工作更快。
97 |
98 | 2014年,Fernando和IPython团队宣布了Jupyter项目,一个更宽泛的多语言交互计算工具的计划。IPython web notebook变成了Jupyter notebook,现在支持40种编程语言。IPython现在可以作为Jupyter使用Python的内核(一种编程语言模式)。
99 |
100 | IPython变成了Jupyter庞大开源项目(一个交互和探索式计算的高效环境)中的一个组件。它最老也是最简单的模式,现在是一个用于编写、测试、调试Python代码的强化shell。你还可以使用通过Jupyter Notebook,一个支持多种语言的交互式网络代码“笔记本”,来使用IPython。IPython shell 和Jupyter notebooks特别适合进行数据探索和可视化。
101 |
102 | Jupyter notebooks还可以编写Markdown和HTML内容,它提供了一种创建代码和文本的富文本方法。其它编程语言也在Jupyter中植入了内核,好让在Jupyter中可以使用Python以外的语言。
103 |
104 | 对我个人而言,我的大部分Python工作都要用到IPython,包括运行、调试和测试代码。
105 |
106 | 在本书的GitHub页面,你可以找到包含各章节所有代码实例的Jupyter notebooks。
107 |
108 | ### SciPy
109 |
110 | SciPy是一组专门解决科学计算中各种标准问题域的包的集合,主要包括下面这些包:
111 |
112 | * scipy.integrate:数值积分例程和微分方程求解器。
113 | * scipy.linalg:扩展了由numpy.linalg提供的线性代数例程和矩阵分解功能。
114 | * scipy.optimize:函数优化器(最小化器)以及根查找算法。
115 | * scipy.signal:信号处理工具。
116 | * scipy.sparse:稀疏矩阵和稀疏线性系统求解器。
117 | * scipy.special:SPECFUN(这是一个实现了许多常用数学函数(如伽玛函数)的Fortran库)的包装器。
118 | * scipy.stats:标准连续和离散概率分布(如密度函数、采样器、连续分布函数等)、各种统计检验方法,以及更好的描述统计法。
119 |
120 | NumPy和SciPy结合使用,便形成了一个相当完备和成熟的计算平台,可以处理多种传统的科学计算问题。
121 |
122 | ### scikit-learn
123 |
124 | 2010年诞生以来,scikit-learn成为了Python的通用机器学习工具包。仅仅七年,就汇聚了全世界超过1500名贡献者。它的子模块包括:
125 |
126 | * 分类:SVM、近邻、随机森林、逻辑回归等等。
127 | * 回归:Lasso、岭回归等等。
128 | * 聚类:k-均值、谱聚类等等。
129 | * 降维:PCA、特征选择、矩阵分解等等。
130 | * 选型:网格搜索、交叉验证、度量。
131 | * 预处理:特征提取、标准化。
132 |
133 | 与pandas、statsmodels和IPython一起,scikit-learn对于Python成为高效数据科学编程语言起到了关键作用。虽然本书不会详细讲解scikit-learn,我会简要介绍它的一些模型,以及用其它工具如何使用这些模型。
134 |
135 | ### statsmodels
136 |
137 | statsmodels是一个统计分析包,起源于斯坦福大学统计学教授Jonathan Taylor,他设计了多种流行于R语言的回归分析模型。Skipper Seabold和Josef Perktold在2010年正式创建了statsmodels项目,随后汇聚了大量的使用者和贡献者。受到R的公式系统的启发,Nathaniel Smith发展出了Patsy项目,它提供了statsmodels的公式或模型的规范框架。
138 |
139 | 与scikit-learn比较,statsmodels包含经典统计学和经济计量学的算法。包括如下子模块:
140 |
141 | * 回归模型:线性回归,广义线性模型,健壮线性模型,线性混合效应模型等等。
142 | * 方差分析(ANOVA)。
143 | * 时间序列分析:AR,ARMA,ARIMA,VAR和其它模型。
144 | * 非参数方法: 核密度估计,核回归。
145 | * 统计模型结果可视化。
146 |
147 | statsmodels更关注与统计推断,提供不确定估计和参数p-值。相反的,scikit-learn注重预测。
148 |
149 | 同scikit-learn一样,我也只是简要介绍statsmodels,以及如何用NumPy和pandas使用它。
150 |
151 | ## 1.4 安装和设置
152 |
153 | 由于人们用Python所做的事情不同,所以没有一个普适的Python及其插件包的安装方案。由于许多读者的Python科学计算环境都不能完全满足本书的需要,所以接下来我将详细介绍各个操作系统上的安装方法。我推荐免费的Anaconda安装包。写作本书时,Anaconda提供Python 2.7和3.6两个版本,以后可能发生变化。本书使用的是Python 3.6,因此推荐选择Python 3.6或更高版本。
154 |
155 | ### Windows
156 |
157 | 要在Windows上运行,先下载[Anaconda安装包](https://www.anaconda.com/download/)。推荐跟随Anaconda下载页面的Windows安装指导,安装指导在写作本书和读者看到此文的的这段时间内可能发生变化。
158 |
159 | 现在,来确认设置是否正确。打开命令行窗口(`cmd.exe`),输入`python`以打开Python解释器。可以看到类似下面的Anaconda版本的输出:
160 |
161 | ```text
162 | C:\Users\wesm>python
163 | Python 3.5.2 |Anaconda 4.1.1 (64-bit)| (default, Jul 5 2016, 11:41:13)
164 | [MSC v.1900 64 bit (AMD64)] on win32
165 | >>>
166 | ```
167 |
168 | 要退出shell,按Ctrl-D(Linux或macOS上),Ctrl-Z(Windows上),或输入命令`exit()`,再按Enter。
169 |
170 | ### Apple \(OS X, macOS\)
171 |
172 | 下载OS X Anaconda安装包,它的名字类似Anaconda3-4.1.0-MacOSX-x86\_64.pkg。双击.pkg文件,运行安装包。安装包运行时,会自动将Anaconda执行路径添加到`.bash_profile`文件,它位于`/Users/$USER/.bash_profile`。
173 |
174 | 为了确认成功,在系统shell打开IPython:
175 |
176 | ```text
177 | $ ipython
178 | ```
179 |
180 | 要退出shell,按Ctrl-D,或输入命令`exit()`,再按Enter。
181 |
182 | ### GNU/Linux
183 |
184 | Linux版本很多,这里给出Debian、Ubantu、CentOS和Fedora的安装方法。安装包是一个脚本文件,必须在shell中运行。取决于系统是32位还是64位,要么选择x86 \(32位\)或x86\_64 \(64位\)安装包。随后你会得到一个文件,名字类似于`Anaconda3-4.1.0-Linux-x86_64.sh`。用bash进行安装:
185 |
186 | ```text
187 | $ bash Anaconda3-4.1.0-Linux-x86_64.sh
188 | ```
189 |
190 | > 笔记:某些Linux版本在包管理器中有满足需求的Python包,只需用类似apt的工具安装就行。这里讲的用Anaconda安装,适用于不同的Linux安装包,也很容易将包升级到最新版本。
191 |
192 | 接受许可之后,会向你询问在哪里放置Anaconda的文件。我推荐将文件安装到默认的home目录,例如`/home/$USER/anaconda`。
193 |
194 | Anaconda安装包可能会询问你是否将`bin/`目录添加到`$PATH`变量。如果在安装之后有任何问题,你可以修改文件`.bashrc`(或`.zshrc`,如果使用的是zsh shell)为类似以下的内容:
195 |
196 | ```text
197 | export PATH=/home/$USER/anaconda/bin:$PATH
198 | ```
199 |
200 | 做完之后,你可以开启一个新窗口,或再次用`~/.bashrc`执行`.bashrc`。
201 |
202 | ### 安装或升级Python包
203 |
204 | 在你阅读本书的时候,你可能想安装另外的不在Anaconda中的Python包。通常,可以用以下命令安装:
205 |
206 | ```text
207 | conda install package_name
208 | ```
209 |
210 | 如果这个命令不行,也可以用pip包管理工具:
211 |
212 | ```text
213 | pip install package_name
214 | ```
215 |
216 | 你可以用`conda update`命令升级包:
217 |
218 | ```text
219 | conda update package_name
220 | ```
221 |
222 | pip可以用`--upgrade`升级:
223 |
224 | ```text
225 | pip install --upgrade package_name
226 | ```
227 |
228 | 本书中,你有许多机会尝试这些命令。
229 |
230 | > 注意:当你使用conda和pip二者安装包时,千万不要用pip升级conda的包,这样会导致环境发生问题。当使用Anaconda或Miniconda时,最好首先使用conda进行升级。
231 |
232 | Python 2 和 Python 3
233 |
234 | 第一版的Python 3.x出现于2008年。它有一系列的变化,与之前的Python 2.x代码有不兼容的地方。因为从1991年Python出现算起,已经过了17年,Python 3 的出现被视为吸取一些列教训的更优结果。
235 |
236 | 2012年,因为许多包还没有完全支持Python 3,许多科学和数据分析社区还是在使用Python 2.x。因此,本书第一版使用的是Python 2.7。现在,用户可以在Python 2.x和Python 3.x间自由选择,二者都有良好的支持。
237 |
238 | 但是,Python 2.x在2020年就会到期(包括重要的安全补丁),因此再用Python 2.7就不是好的选择了。因此,本书使用了Python 3.6,这一广泛使用、支持良好的稳定版本。我们已经称Python 2.x为“遗留版本”,简称Python 3.x为“Python”。我建议你也是如此。
239 |
240 | 本书基于Python 3.6。你的Python版本也许高于3.6,但是示例代码应该是向前兼容的。一些示例代码可能在Python 2.7上有所不同,或完全不兼容。
241 |
242 | ### 集成开发环境(IDEs)和文本编辑器
243 |
244 | 当被问到我的标准开发环境,我几乎总是回答“IPython加文本编辑器”。我通常在编程时,反复在IPython或Jupyter notebooks中测试和调试每条代码。也可以交互式操作数据,和可视化验证数据操作中某一特殊集合。在shell中使用pandas和NumPy也很容易。
245 |
246 | 但是,当创建软件时,一些用户可能更想使用特点更为丰富的IDE,而不仅仅是原始的Emacs或Vim的文本编辑器。以下是一些IDE:
247 |
248 | * PyDev(免费),基于Eclipse平台的IDE;
249 | * JetBrains的PyCharm(商业用户需要订阅,开源开发者免费);
250 | * Visual Studio(Windows用户)的Python Tools;
251 | * Spyder(免费),Anaconda附带的IDE;
252 | * Komodo IDE(商业)。
253 |
254 | 因为Python的流行,大多数文本编辑器,比如Atom和Sublime Text 3,对Python的支持也非常好。
255 |
256 | ## 1.5 社区和会议
257 |
258 | 除了在网上搜索,各式各样的科学和数据相关的Python邮件列表是非常有帮助的,很容易获得回答。包括:
259 |
260 | * pydata:一个Google群组列表,用以回答Python数据分析和pandas的问题;
261 | * pystatsmodels: statsmodels或pandas相关的问题;
262 | * scikit-learn和Python机器学习邮件列表,scikit-learn@python.org;
263 | * numpy-discussion:和NumPy相关的问题;
264 | * scipy-user:SciPy和科学计算的问题;
265 |
266 | 因为这些邮件列表的URLs可以很容易搜索到,但因为可能发生变化,所以没有给出。
267 |
268 | 每年,世界各地会举办许多Python开发者大会。如果你想结识其他有相同兴趣的人,如果可能的话,我建议你去参加一个。许多会议会对无力支付入场费和差旅费的人提供财力帮助。下面是一些会议:
269 |
270 | * PyCon和EuroPython:北美和欧洲的两大Python会议;
271 | * SciPy和EuroSciPy:北美和欧洲两大面向科学计算的会议;
272 | * PyData:世界范围内,一些列的地区性会议,专注数据科学和数据分析;
273 | * 国际和地区的PyCon会议([http://pycon.org有完整列表)](http://pycon.org有完整列表)) 。
274 |
275 | ## 1.6 本书导航
276 |
277 | 如果之前从未使用过Python,那你可能需要先看看本书的第2章和第3章,我简要介绍了Python的特点,IPython和Jupyter notebooks。这些知识是为本书后面的内容做铺垫。如果你已经掌握Python,可以选择跳过。
278 |
279 | 接下来,简单地介绍了NumPy的关键特性,附录A中是更高级的NumPy功能。然后,我介绍了pandas,本书剩余的内容全部是使用pandas、NumPy和matplotlib处理数据分析的问题。我已经尽量让全书的结构循序渐进,但偶尔会有章节之间的交叉,有时用到的概念还没有介绍过。
280 |
281 | 尽管读者各自的工作任务不同,大体可以分为几类:
282 |
283 | * 与外部世界交互
284 |
285 | 阅读编写多种文件格式和数据存储;
286 |
287 | * 数据准备
288 |
289 | 清洗、修改、结合、标准化、重塑、切片、切割、转换数据,以进行分析;
290 |
291 | * 转换数据
292 |
293 | 对旧的数据集进行数学和统计操作,生成新的数据集(例如,通过各组变量聚类成大的表);
294 |
295 | * 建模和计算
296 |
297 | 将数据绑定统计模型、机器学习算法、或其他计算工具;
298 |
299 | * 展示
300 |
301 | 创建交互式和静态的图表可视化和文本总结。
302 |
303 | ### 代码示例
304 |
305 | 本书大部分代码示例的输入形式和输出结果都会按照其在IPython shell或Jupyter notebooks中执行时的样子进行排版:
306 |
307 | ```text
308 | In [5]: CODE EXAMPLE
309 | Out[5]: OUTPUT
310 | ```
311 |
312 | 但你看到类似的示例代码,就是让你在`in`的部分输入代码,按Enter键执行(Jupyter中是按Shift-Enter)。然后就可以在`out`看到输出。
313 |
314 | ### 示例数据
315 |
316 | 各章的示例数据都存放在GitHub上:[http://github.com/pydata/pydata-book。](http://github.com/pydata/pydata-book。) 下载这些数据的方法有二:使用git版本控制命令行程序;直接从网站上下载该GitHub库的zip文件。如果遇到了问题,可以到我的个人主页,[http://wesmckinney.com/,](http://wesmckinney.com/,) 获取最新的指导。
317 |
318 | 为了让所有示例都能重现,我已经尽我所能使其包含所有必需的东西,但仍然可能会有一些错误或遗漏。如果出现这种情况的话,请给我发邮件:wesmckinn@gmail.com。报告本书错误的最好方法是O’Reilly的errata页面,[http://www.bit.ly/pyDataAnalysis\_errata。](http://www.bit.ly/pyDataAnalysis_errata。)
319 |
320 | ### 引入惯例
321 |
322 | Python社区已经广泛采取了一些常用模块的命名惯例:
323 |
324 | ```python
325 | import numpy as np
326 | import matplotlib.pyplot as plt
327 | import pandas as pd
328 | import seaborn as sns
329 | import statsmodels as sm
330 | ```
331 |
332 | 也就是说,当你看到np.arange时,就应该想到它引用的是NumPy中的arange函数。这样做的原因是:在Python软件开发过程中,不建议直接引入类似NumPy这种大型库的全部内容(from numpy import \*)。
333 |
334 | ### 行话
335 |
336 | 由于你可能不太熟悉书中使用的一些有关编程和数据科学方面的常用术语,所以我在这里先给出其简单定义:
337 |
338 | 数据规整(Munge/Munging/Wrangling) 指的是将非结构化和(或)散乱数据处理为结构化或整洁形式的整个过程。这几个词已经悄悄成为当今数据黑客们的行话了。Munge这个词跟Lunge押韵。
339 |
340 | 伪码(Pseudocode) 算法或过程的“代码式”描述,而这些代码本身并不是实际有效的源代码。
341 |
342 | 语法糖(Syntactic sugar) 这是一种编程语法,它并不会带来新的特性,但却能使代码更易读、更易写。
343 |
344 |
--------------------------------------------------------------------------------
/第02章 Python语法基础,IPython和Jupyter Notebooks.md:
--------------------------------------------------------------------------------
1 | ***
2 |
3 | 🌟 大模型时代,开发模型需要海量数据,采集数据又离不开代理IP。推荐一家好用的数据集和代理IP服务 🌐💡【[bright.cn](https://get.brightdata.com/bigdataresource)】💡🌐,根植海外亮数据BrightData团队,数据集和代理市场领导者,覆盖全球的7200万IP,🌍🔥有轮动真人住宅IP跟机房IP,亲测稳定不易被封。有多种套餐可选,📦🔒还有不限流量的套餐。需要高质量代理IP的可以注册后联系中文客服,开通后赠送💵💰5美元试用和教程指引,报我名字【SeanCheney】可再得💵💰150美元套餐折扣。
4 |
5 | 💡数据集和代理IP推荐💡【[bright.cn](https://get.brightdata.com/bigdataresource)】💡,进入网站注册账号可免费获得诸如 亚马逊,沃尔玛,领英,抖音,Glassdoor,Airbnb,谷歌地图商家等数据集样本。
6 |
7 | ***
8 |
9 | # 第2章 Python语法基础,IPython和Jupyter Notebooks
10 |
11 | 当我在2011年和2012年写作本书的第一版时,可用的学习Python数据分析的资源很少。这部分上是一个鸡和蛋的问题:我们现在使用的库,比如pandas、scikit-learn和statsmodels,那时相对来说并不成熟。2017年,数据科学、数据分析和机器学习的资源已经很多,原来通用的科学计算拓展到了计算机科学家、物理学家和其它研究领域的工作人员。学习Python和成为软件工程师的优秀书籍也有了。
12 |
13 | 因为这本书是专注于Python数据处理的,对于一些Python的数据结构和库的特性难免不足。因此,本章和第3章的内容只够你能学习本书后面的内容。
14 |
15 | 在我来看,没有必要为了数据分析而去精通Python。我鼓励你使用IPython shell和Jupyter试验示例代码,并学习不同类型、函数和方法的文档。虽然我已尽力让本书内容循序渐进,但读者偶尔仍会碰到没有之前介绍过的内容。
16 |
17 | 本书大部分内容关注的是基于表格的分析和处理大规模数据集的数据准备工具。为了使用这些工具,必须首先将混乱的数据规整为整洁的表格(或结构化)形式。幸好,Python是一个理想的语言,可以快速整理数据。Python使用得越熟练,越容易准备新数据集以进行分析。
18 |
19 | 最好在IPython和Jupyter中亲自尝试本书中使用的工具。当你学会了如何启动Ipython和Jupyter,我建议你跟随示例代码进行练习。与任何键盘驱动的操作环境一样,记住常见的命令也是学习曲线的一部分。
20 |
21 | > 笔记:本章没有介绍Python的某些概念,如类和面向对象编程,你可能会发现它们在Python数据分析中很有用。 为了加强Python知识,我建议你学习官方Python教程,[https://docs.python.org/3/,或是通用的Python教程书籍,比如:](https://docs.python.org/3/,或是通用的Python教程书籍,比如:)
22 | >
23 | > * Python Cookbook,第3版,David Beazley和Brian K. Jones著(O’Reilly)
24 | > * 流畅的Python,Luciano Ramalho著 \(O’Reilly\)
25 | > * 高效的Python,Brett Slatkin著 \(Pearson\)
26 |
27 | ## 2.1 Python解释器
28 |
29 | Python是解释性语言。Python解释器同一时间只能运行一个程序的一条语句。标准的交互Python解释器可以在命令行中通过键入`python`命令打开:
30 |
31 | ```text
32 | $ python
33 | Python 3.6.0 | packaged by conda-forge | (default, Jan 13 2017, 23:17:12)
34 | [GCC 4.8.2 20140120 (Red Hat 4.8.2-15)] on linux
35 | Type "help", "copyright", "credits" or "license" for more information.
36 | >>> a = 5
37 | >>> print(a)
38 | 5
39 | ```
40 |
41 | `>>>`提示输入代码。要退出Python解释器返回终端,可以输入`exit()`或按Ctrl-D。
42 |
43 | 运行Python程序只需调用Python的同时,使用一个`.py`文件作为它的第一个参数。假设创建了一个`hello_world.py`文件,它的内容是:
44 |
45 | ```python
46 | print('Hello world')
47 | ```
48 |
49 | 你可以用下面的命令运行它(`hello_world.py`文件必须位于终端的工作目录):
50 |
51 | ```python
52 | $ python hello_world.py
53 | Hello world
54 | ```
55 |
56 | 一些Python程序员总是这样执行Python代码的,从事数据分析和科学计算的人却会使用IPython,一个强化的Python解释器,或Jupyter notebooks,一个网页代码笔记本,它原先是IPython的一个子项目。在本章中,我介绍了如何使用IPython和Jupyter,在附录A中有更深入的介绍。当你使用`%run`命令,IPython会同样执行指定文件中的代码,结束之后,还可以与结果交互:
57 |
58 | ```text
59 | $ ipython
60 | Python 3.6.0 | packaged by conda-forge | (default, Jan 13 2017, 23:17:12)
61 | Type "copyright", "credits" or "license" for more information.
62 |
63 | IPython 5.1.0 -- An enhanced Interactive Python.
64 | ? -> Introduction and overview of IPython's features.
65 | %quickref -> Quick reference.
66 | help -> Python's own help system.
67 | object? -> Details about 'object', use 'object??' for extra details.
68 |
69 | In [1]: %run hello_world.py
70 | Hello world
71 |
72 | In [2]:
73 | ```
74 |
75 | IPython默认采用序号的格式`In [2]:`,与标准的`>>>`提示符不同。
76 |
77 | ## 2.2 IPython基础
78 |
79 | 在本节中,我们会教你打开运行IPython shell和jupyter notebook,并介绍一些基本概念。
80 |
81 | ### 运行IPython Shell
82 |
83 | 你可以用`ipython`在命令行打开IPython Shell,就像打开普通的Python解释器:
84 |
85 | ```text
86 | $ ipython
87 | Python 3.6.0 | packaged by conda-forge | (default, Jan 13 2017, 23:17:12)
88 | Type "copyright", "credits" or "license" for more information.
89 |
90 | IPython 5.1.0 -- An enhanced Interactive Python.
91 | ? -> Introduction and overview of IPython's features.
92 | %quickref -> Quick reference.
93 | help -> Python's own help system.
94 | object? -> Details about 'object', use 'object??' for extra details.
95 |
96 | In [1]: a = 5
97 | In [2]: a
98 | Out[2]: 5
99 | ```
100 |
101 | 你可以通过输入代码并按Return(或Enter),运行任意Python语句。当你只输入一个变量,它会显示代表的对象:
102 |
103 | ```python
104 | In [5]: import numpy as np
105 |
106 | In [6]: data = {i : np.random.randn() for i in range(7)}
107 |
108 | In [7]: data
109 | Out[7]:
110 | {0: -0.20470765948471295,
111 | 1: 0.47894333805754824,
112 | 2: -0.5194387150567381,
113 | 3: -0.55573030434749,
114 | 4: 1.9657805725027142,
115 | 5: 1.3934058329729904,
116 | 6: 0.09290787674371767}
117 | ```
118 |
119 | 前两行是Python代码语句;第二条语句创建一个名为`data`的变量,它引用一个新创建的Python字典。最后一行打印`data`的值。
120 |
121 | 许多Python对象被格式化为更易读的形式,或称作`pretty-printed`,它与普通的`print`不同。如果在标准Python解释器中打印上述`data`变量,则可读性要降低:
122 |
123 | ```text
124 | >>> from numpy.random import randn
125 | >>> data = {i : randn() for i in range(7)}
126 | >>> print(data)
127 | {0: -1.5948255432744511, 1: 0.10569006472787983, 2: 1.972367135977295,
128 | 3: 0.15455217573074576, 4: -0.24058577449429575, 5: -1.2904897053651216,
129 | 6: 0.3308507317325902}
130 | ```
131 |
132 | IPython还支持执行任意代码块(通过一个华丽的复制-粘贴方法)和整段Python脚本的功能。你也可以使用Jupyter notebook运行大代码块,接下来就会看到。
133 |
134 | ### 运行Jupyter Notebook
135 |
136 | notebook是Jupyter项目的重要组件之一,它是一个代码、文本(有标记或无标记)、数据可视化或其它输出的交互式文档。Jupyter Notebook需要与内核互动,内核是Jupyter与其它编程语言的交互编程协议。Python的Jupyter内核是使用IPython。要启动Jupyter,在命令行中输入`jupyter notebook`:
137 |
138 | ```text
139 | $ jupyter notebook
140 | [I 15:20:52.739 NotebookApp] Serving notebooks from local directory:
141 | /home/wesm/code/pydata-book
142 | [I 15:20:52.739 NotebookApp] 0 active kernels
143 | [I 15:20:52.739 NotebookApp] The Jupyter Notebook is running at:
144 | http://localhost:8888/
145 | [I 15:20:52.740 NotebookApp] Use Control-C to stop this server and shut down
146 | all kernels (twice to skip confirmation).
147 | Created new window in existing browser session.
148 | ```
149 |
150 | 在多数平台上,Jupyter会自动打开默认的浏览器(除非指定了`--no-browser`)。或者,可以在启动notebook之后,手动打开网页`http://localhost:8888/`。图2-1展示了Google Chrome中的notebook。
151 |
152 | > 笔记:许多人使用Jupyter作为本地的计算环境,但它也可以部署到服务器上远程访问。这里不做介绍,如果需要的话,鼓励读者自行到网上学习。
153 |
154 | 
155 |
156 | 要新建一个notebook,点击按钮New,选择“Python3”或“conda\[默认项\]”。如果是第一次,点击空格,输入一行Python代码。然后按Shift-Enter执行。
157 |
158 | 
159 |
160 | 当保存notebook时(File目录下的Save and Checkpoint),会创建一个后缀名为`.ipynb`的文件。这是一个自包含文件格式,包含当前笔记本中的所有内容(包括所有已评估的代码输出)。可以被其它Jupyter用户加载和编辑。要加载存在的notebook,把它放到启动notebook进程的相同目录内。你可以用本书的示例代码练习,见图2-3。
161 |
162 | 虽然Jupyter notebook和IPython shell使用起来不同,本章中几乎所有的命令和工具都可以通用。
163 |
164 | 
165 |
166 | ### Tab补全
167 |
168 | 从外观上,IPython shell和标准的Python解释器只是看起来不同。IPython shell的进步之一是具备其它IDE和交互计算分析环境都有的tab补全功能。在shell中输入表达式,按下Tab,会搜索已输入变量(对象、函数等等)的命名空间:
169 |
170 | ```text
171 | In [1]: an_apple = 27
172 |
173 | In [2]: an_example = 42
174 |
175 | In [3]: an
176 | an_apple and an_example any
177 | ```
178 |
179 | 在这个例子中,IPython呈现出了之前两个定义的变量和Python的关键字和内建的函数`any`。当然,你也可以补全任何对象的方法和属性:
180 |
181 | ```text
182 | In [3]: b = [1, 2, 3]
183 |
184 | In [4]: b.
185 | b.append b.count b.insert b.reverse
186 | b.clear b.extend b.pop b.sort
187 | b.copy b.index b.remove
188 | ```
189 |
190 | 同样也适用于模块:
191 |
192 | ```text
193 | In [1]: import datetime
194 |
195 | In [2]: datetime.
196 | datetime.date datetime.MAXYEAR datetime.timedelta
197 | datetime.datetime datetime.MINYEAR datetime.timezone
198 | datetime.datetime_CAPI datetime.time datetime.tzinfo
199 | ```
200 |
201 | 在Jupyter notebook和新版的IPython(5.0及以上),自动补全功能是下拉框的形式。
202 |
203 | > 笔记:注意,默认情况下,IPython会隐藏下划线开头的方法和属性,比如魔术方法和内部的“私有”方法和属性,以避免混乱的显示(和让新手迷惑!)这些也可以tab补全,但是你必须首先键入一个下划线才能看到它们。如果你喜欢总是在tab补全中看到这样的方法,你可以IPython配置中进行设置。可以在IPython文档中查找方法。
204 |
205 | 除了补全命名、对象和模块属性,Tab还可以补全其它的。当输入看似文件路径时(即使是Python字符串),按下Tab也可以补全电脑上对应的文件信息:
206 |
207 | ```text
208 | In [7]: datasets/movielens/
209 | datasets/movielens/movies.dat datasets/movielens/README
210 | datasets/movielens/ratings.dat datasets/movielens/users.dat
211 |
212 | In [7]: path = 'datasets/movielens/
213 | datasets/movielens/movies.dat datasets/movielens/README
214 | datasets/movielens/ratings.dat datasets/movielens/users.dat
215 | ```
216 |
217 | 结合`%run`,tab补全可以节省许多键盘操作。
218 |
219 | 另外,tab补全可以补全函数的关键词参数(包括等于号=)。见图2-4。
220 |
221 | 
222 |
223 | 后面会仔细地学习函数。
224 |
225 | ### 自省
226 |
227 | 在变量前后使用问号?,可以显示对象的信息:
228 |
229 | ```python
230 | In [8]: b = [1, 2, 3]
231 |
232 | In [9]: b?
233 | Type: list
234 | String Form:[1, 2, 3]
235 | Length: 3
236 | Docstring:
237 | list() -> new empty list
238 | list(iterable) -> new list initialized from iterable's items
239 |
240 | In [10]: print?
241 | Docstring:
242 | print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)
243 |
244 | Prints the values to a stream, or to sys.stdout by default.
245 | Optional keyword arguments:
246 | file: a file-like object (stream); defaults to the current sys.stdout.
247 | sep: string inserted between values, default a space.
248 | end: string appended after the last value, default a newline.
249 | flush: whether to forcibly flush the stream.
250 | Type: builtin_function_or_method
251 | ```
252 |
253 | 这可以作为对象的自省。如果对象是一个函数或实例方法,定义过的文档字符串,也会显示出信息。假设我们写了一个如下的函数:
254 |
255 | ```python
256 | def add_numbers(a, b):
257 | """
258 | Add two numbers together
259 |
260 | Returns
261 | -------
262 | the_sum : type of arguments
263 | """
264 | return a + b
265 | ```
266 |
267 | 然后使用?符号,就可以显示如下的文档字符串:
268 |
269 | ```python
270 | In [11]: add_numbers?
271 | Signature: add_numbers(a, b)
272 | Docstring:
273 | Add two numbers together
274 |
275 | Returns
276 | -------
277 | the_sum : type of arguments
278 | File:
279 | Type: function
280 | ```
281 |
282 | 使用??会显示函数的源码:
283 |
284 | ```python
285 | In [12]: add_numbers??
286 | Signature: add_numbers(a, b)
287 | Source:
288 | def add_numbers(a, b):
289 | """
290 | Add two numbers together
291 |
292 | Returns
293 | -------
294 | the_sum : type of arguments
295 | """
296 | return a + b
297 | File:
298 | Type: function
299 | ```
300 |
301 | ?还有一个用途,就是像Unix或Windows命令行一样搜索IPython的命名空间。字符与通配符结合可以匹配所有的名字。例如,我们可以获得所有包含load的顶级NumPy命名空间:
302 |
303 | ```python
304 | In [13]: np.*load*?
305 | np.__loader__
306 | np.load
307 | np.loads
308 | np.loadtxt
309 | np.pkgload
310 | ```
311 |
312 | ### %run命令
313 |
314 | 你可以用`%run`命令运行所有的Python程序。假设有一个文件`ipython_script_test.py`:
315 |
316 | ```python
317 | def f(x, y, z):
318 | return (x + y) / z
319 |
320 | a = 5
321 | b = 6
322 | c = 7.5
323 |
324 | result = f(a, b, c)
325 | ```
326 |
327 | 可以如下运行:
328 |
329 | ```python
330 | In [14]: %run ipython_script_test.py
331 | ```
332 |
333 | 这段脚本运行在空的命名空间(没有import和其它定义的变量),因此结果和普通的运行方式`python script.py`相同。文件中所有定义的变量(import、函数和全局变量,除非抛出异常),都可以在IPython shell中随后访问:
334 |
335 | ```python
336 | In [15]: c
337 | Out [15]: 7.5
338 |
339 | In [16]: result
340 | Out[16]: 1.4666666666666666
341 | ```
342 |
343 | 如果一个Python脚本需要命令行参数(在`sys.argv`中查找),可以在文件路径之后传递,就像在命令行上运行一样。
344 |
345 | > 笔记:如果想让一个脚本访问IPython已经定义过的变量,可以使用`%run -i`。
346 |
347 | 在Jupyter notebook中,你也可以使用`%load`,它将脚本导入到一个代码格中:
348 |
349 | ```text
350 | >>> %load ipython_script_test.py
351 |
352 | def f(x, y, z):
353 | return (x + y) / z
354 | a = 5
355 | b = 6
356 | c = 7.5
357 |
358 | result = f(a, b, c)
359 | ```
360 |
361 | ### 中断运行的代码
362 |
363 | 代码运行时按Ctrl-C,无论是%run或长时间运行命令,都会导致`KeyboardInterrupt`。这会导致几乎所有Python程序立即停止,除非一些特殊情况。
364 |
365 | > 警告:当Python代码调用了一些编译的扩展模块,按Ctrl-C不一定将执行的程序立即停止。在这种情况下,你必须等待,直到控制返回Python解释器,或者在更糟糕的情况下强制终止Python进程。
366 |
367 | ### 从剪贴板执行程序
368 |
369 | 如果使用Jupyter notebook,你可以将代码复制粘贴到任意代码格执行。在IPython shell中也可以从剪贴板执行。假设在其它应用中复制了如下代码:
370 |
371 | ```python
372 | x = 5
373 | y = 7
374 | if x > 5:
375 | x += 1
376 |
377 | y = 8
378 | ```
379 |
380 | 最简单的方法是使用`%paste`和`%cpaste`函数。`%paste`可以直接运行剪贴板中的代码:
381 |
382 | ```python
383 | In [17]: %paste
384 | x = 5
385 | y = 7
386 | if x > 5:
387 | x += 1
388 |
389 | y = 8
390 | ## -- End pasted text --
391 | ```
392 |
393 | `%cpaste`功能类似,但会给出一条提示:
394 |
395 | ```python
396 | In [18]: %cpaste
397 | Pasting code; enter '--' alone on the line to stop or use Ctrl-D.
398 | :x = 5
399 | :y = 7
400 | :if x > 5:
401 | : x += 1
402 | :
403 | : y = 8
404 | :--
405 | ```
406 |
407 | 使用`%cpaste`,你可以粘贴任意多的代码再运行。你可能想在运行前,先看看代码。如果粘贴了错误的代码,可以用Ctrl-C中断。
408 |
409 | ### 键盘快捷键
410 |
411 | IPython有许多键盘快捷键进行导航提示(类似Emacs文本编辑器或UNIX bash Shell)和交互shell的历史命令。表2-1总结了常见的快捷键。图2-5展示了一部分,如移动光标。
412 |
413 | 
414 |
415 | 
416 |
417 | Jupyter notebooks有另外一套庞大的快捷键。因为它的快捷键比IPython的变化快,建议你参阅Jupyter notebook的帮助文档。
418 |
419 | ### 魔术命令
420 |
421 | IPython中特殊的命令(Python中没有)被称作“魔术”命令。这些命令可以使普通任务更便捷,更容易控制IPython系统。魔术命令是在指令前添加百分号%前缀。例如,可以用`%timeit`(这个命令后面会详谈)测量任何Python语句,例如矩阵乘法,的执行时间:
422 |
423 | ```python
424 | In [20]: a = np.random.randn(100, 100)
425 |
426 | In [20]: %timeit np.dot(a, a)
427 | 10000 loops, best of 3: 20.9 µs per loop
428 | ```
429 |
430 | 魔术命令可以被看做IPython中运行的命令行。许多魔术命令有“命令行”选项,可以通过?查看:
431 |
432 | ```text
433 | In [21]: %debug?
434 | Docstring:
435 | ::
436 |
437 | %debug [--breakpoint FILE:LINE] [statement [statement ...]]
438 |
439 | Activate the interactive debugger.
440 |
441 | This magic command support two ways of activating debugger.
442 | One is to activate debugger before executing code. This way, you
443 | can set a break point, to step through the code from the point.
444 | You can use this mode by giving statements to execute and optionally
445 | a breakpoint.
446 |
447 | The other one is to activate debugger in post-mortem mode. You can
448 | activate this mode simply running %debug without any argument.
449 | If an exception has just occurred, this lets you inspect its stack
450 | frames interactively. Note that this will always work only on the last
451 | traceback that occurred, so you must call this quickly after an
452 | exception that you wish to inspect has fired, because if another one
453 | occurs, it clobbers the previous one.
454 |
455 | If you want IPython to automatically do this on every exception, see
456 | the %pdb magic for more details.
457 |
458 | positional arguments:
459 | statement Code to run in debugger. You can omit this in cell
460 | magic mode.
461 |
462 | optional arguments:
463 | --breakpoint , -b
464 | Set break point at LINE in FILE.
465 | ```
466 |
467 | 魔术函数默认可以不用百分号,只要没有变量和函数名相同。这个特点被称为“自动魔术”,可以用`%automagic`打开或关闭。
468 |
469 | 一些魔术函数与Python函数很像,它的结果可以赋值给一个变量:
470 |
471 | ```text
472 | In [22]: %pwd
473 | Out[22]: '/home/wesm/code/pydata-book
474 |
475 | In [23]: foo = %pwd
476 |
477 | In [24]: foo
478 | Out[24]: '/home/wesm/code/pydata-book'
479 | ```
480 |
481 | IPython的文档可以在shell中打开,我建议你用`%quickref`或`%magic`学习下所有特殊命令。表2-2列出了一些可以提高生产率的交互计算和Python开发的IPython指令。
482 |
483 | 
484 |
485 | ### 集成Matplotlib
486 |
487 | IPython在分析计算领域能够流行的原因之一是它非常好的集成了数据可视化和其它用户界面库,比如matplotlib。不用担心以前没用过matplotlib,本书后面会详细介绍。`%matplotlib`魔术函数配置了IPython shell和Jupyter notebook中的matplotlib。这点很重要,其它创建的图不会出现(notebook)或获取session的控制,直到结束(shell)。
488 |
489 | 在IPython shell中,运行`%matplotlib`可以进行设置,可以创建多个绘图窗口,而不会干扰控制台session:
490 |
491 | ```text
492 | In [26]: %matplotlib
493 | Using matplotlib backend: Qt4Agg
494 | ```
495 |
496 | 在JUpyter中,命令有所不同(图2-6):
497 |
498 | ```text
499 | In [26]: %matplotlib inline
500 | ```
501 |
502 | 
503 |
504 | ## 2.3 Python语法基础
505 |
506 | 在本节中,我将概述基本的Python概念和语言机制。在下一章,我将详细介绍Python的数据结构、函数和其它内建工具。
507 |
508 | ### 语言的语义
509 |
510 | Python的语言设计强调的是可读性、简洁和清晰。有些人称Python为“可执行的伪代码”。
511 |
512 | ### 使用缩进,而不是括号
513 |
514 | Python使用空白字符(tab和空格)来组织代码,而不是像其它语言,比如R、C++、JAVA和Perl那样使用括号。看一个排序算法的`for`循环:
515 |
516 | ```python
517 | for x in array:
518 | if x < pivot:
519 | less.append(x)
520 | else:
521 | greater.append(x)
522 | ```
523 |
524 | 冒号标志着缩进代码块的开始,冒号之后的所有代码的缩进量必须相同,直到代码块结束。不管是否喜欢这种形式,使用空白符是Python程序员开发的一部分,在我看来,这可以让python的代码可读性大大优于其它语言。虽然期初看起来很奇怪,经过一段时间,你就能适应了。
525 |
526 | > 笔记:我强烈建议你使用四个空格作为默认的缩进,可以使用tab代替四个空格。许多文本编辑器的设置是使用制表位替代空格。某些人使用tabs或不同数目的空格数,常见的是使用两个空格。大多数情况下,四个空格是大多数人采用的方法,因此建议你也这样做。
527 |
528 | 你应该已经看到,Python的语句不需要用分号结尾。但是,分号却可以用来给同在一行的语句切分:
529 |
530 | ```python
531 | a = 5; b = 6; c = 7
532 | ```
533 |
534 | Python不建议将多条语句放到一行,这会降低代码的可读性。
535 |
536 | ### 万物皆对象
537 |
538 | Python语言的一个重要特性就是它的对象模型的一致性。每个数字、字符串、数据结构、函数、类、模块等等,都是在Python解释器的自有“盒子”内,它被认为是Python对象。每个对象都有类型(例如,字符串或函数)和内部数据。在实际中,这可以让语言非常灵活,因为函数也可以被当做对象使用。
539 |
540 | ### 注释
541 |
542 | 任何前面带有井号\#的文本都会被Python解释器忽略。这通常被用来添加注释。有时,你会想排除一段代码,但并不删除。简便的方法就是将其注释掉:
543 |
544 | ```python
545 | results = []
546 | for line in file_handle:
547 | # keep the empty lines for now
548 | # if len(line) == 0:
549 | # continue
550 | results.append(line.replace('foo', 'bar'))
551 | ```
552 |
553 | 也可以在执行过的代码后面添加注释。一些人习惯在代码之前添加注释,前者这种方法有时也是有用的:
554 |
555 | ```python
556 | print("Reached this line") # Simple status report
557 | ```
558 |
559 | ### 函数和对象方法调用
560 |
561 | 你可以用圆括号调用函数,传递零个或几个参数,或者将返回值给一个变量:
562 |
563 | ```python
564 | result = f(x, y, z)
565 | g()
566 | ```
567 |
568 | 几乎Python中的每个对象都有附加的函数,称作方法,可以用来访问对象的内容。可以用下面的语句调用:
569 |
570 | ```python
571 | obj.some_method(x, y, z)
572 | ```
573 |
574 | 函数可以使用位置和关键词参数:
575 |
576 | ```python
577 | result = f(a, b, c, d=5, e='foo')
578 | ```
579 |
580 | 后面会有更多介绍。
581 |
582 | ### 变量和参数传递
583 |
584 | 当在Python中创建变量(或名字),你就在等号右边创建了一个对这个变量的引用。考虑一个整数列表:
585 |
586 | ```python
587 | In [8]: a = [1, 2, 3]
588 | ```
589 |
590 | 假设将a赋值给一个新变量b:
591 |
592 | ```python
593 | In [9]: b = a
594 | ```
595 |
596 | 在有些方法中,这个赋值会将数据\[1, 2, 3\]也复制。在Python中,a和b实际上是同一个对象,即原有列表\[1, 2, 3\](见图2-7)。你可以在a中添加一个元素,然后检查b:
597 |
598 | ```python
599 | In [10]: a.append(4)
600 |
601 | In [11]: b
602 | Out[11]: [1, 2, 3, 4]
603 | ```
604 |
605 | 
606 |
607 | 理解Python的引用的含义,数据是何时、如何、为何复制的,是非常重要的。尤其是当你用Python处理大的数据集时。
608 |
609 | > 笔记:赋值也被称作绑定,我们是把一个名字绑定给一个对象。变量名有时可能被称为绑定变量。
610 |
611 | 当你将对象作为参数传递给函数时,新的局域变量创建了对原始对象的引用,而不是复制。如果在函数里绑定一个新对象到一个变量,这个变动不会反映到上一层。因此可以改变可变参数的内容。假设有以下函数:
612 |
613 | ```python
614 | def append_element(some_list, element):
615 | some_list.append(element)
616 | ```
617 |
618 | 然后有:
619 |
620 | ```python
621 | In [27]: data = [1, 2, 3]
622 |
623 | In [28]: append_element(data, 4)
624 |
625 | In [29]: data
626 | Out[29]: [1, 2, 3, 4]
627 | ```
628 |
629 | ### 动态引用,强类型
630 |
631 | 与许多编译语言(如JAVA和C++)对比,Python中的对象引用不包含附属的类型。下面的代码是没有问题的:
632 |
633 | ```python
634 | In [12]: a = 5
635 |
636 | In [13]: type(a)
637 | Out[13]: int
638 |
639 | In [14]: a = 'foo'
640 |
641 | In [15]: type(a)
642 | Out[15]: str
643 | ```
644 |
645 | 变量是在特殊命名空间中的对象的名字,类型信息保存在对象自身中。一些人可能会说Python不是“类型化语言”。这是不正确的,看下面的例子:
646 |
647 | ```text
648 | In [16]: '5' + 5
649 | ---------------------------------------------------------------------------
650 | TypeError Traceback (most recent call last)
651 | in ()
652 | ----> 1 '5' + 5
653 | TypeError: must be str, not int
654 | ```
655 |
656 | 在某些语言中,例如Visual Basic,字符串‘5’可能被默许转换(或投射)为整数,因此会产生10。但在其它语言中,例如JavaScript,整数5会被投射成字符串,结果是联结字符串‘55’。在这个方面,Python被认为是强类型化语言,意味着每个对象都有明确的类型(或类),默许转换只会发生在特定的情况下,例如:
657 |
658 | ```text
659 | In [17]: a = 4.5
660 |
661 | In [18]: b = 2
662 |
663 | # String formatting, to be visited later
664 | In [19]: print('a is {0}, b is {1}'.format(type(a), type(b)))
665 | a is , b is
666 |
667 | In [20]: a / b
668 | Out[20]: 2.25
669 | ```
670 |
671 | 知道对象的类型很重要,最好能让函数可以处理多种类型的输入。你可以用`isinstance`函数检查对象是某个类型的实例:
672 |
673 | ```text
674 | In [21]: a = 5
675 |
676 | In [22]: isinstance(a, int)
677 | Out[22]: True
678 | ```
679 |
680 | `isinstance`可以用类型元组,检查对象的类型是否在元组中:
681 |
682 | ```text
683 | In [23]: a = 5; b = 4.5
684 |
685 | In [24]: isinstance(a, (int, float))
686 | Out[24]: True
687 |
688 | In [25]: isinstance(b, (int, float))
689 | Out[25]: True
690 | ```
691 |
692 | ### 属性和方法
693 |
694 | Python的对象通常都有属性(其它存储在对象内部的Python对象)和方法(对象的附属函数可以访问对象的内部数据)。可以用`obj.attribute_name`访问属性和方法:
695 |
696 | ```text
697 | In [1]: a = 'foo'
698 |
699 | In [2]: a.
700 | a.capitalize a.format a.isupper a.rindex a.strip
701 | a.center a.index a.join a.rjust a.swapcase
702 | a.count a.isalnum a.ljust a.rpartition a.title
703 | a.decode a.isalpha a.lower a.rsplit a.translate
704 | a.encode a.isdigit a.lstrip a.rstrip a.upper
705 | a.endswith a.islower a.partition a.split a.zfill
706 | a.expandtabs a.isspace a.replace a.splitlines
707 | a.find a.istitle a.rfind a.startswith
708 | ```
709 |
710 | 也可以用`getattr`函数,通过名字访问属性和方法:
711 |
712 | ```text
713 | In [27]: getattr(a, 'split')
714 | Out[27]:
715 | ```
716 |
717 | 在其它语言中,访问对象的名字通常称作“反射”。本书不会大量使用`getattr`函数和相关的`hasattr`和`setattr`函数,使用这些函数可以高效编写原生的、可重复使用的代码。
718 |
719 | ### 鸭子类型
720 |
721 | 经常地,你可能不关心对象的类型,只关心对象是否有某些方法或用途。这通常被称为“鸭子类型”,来自“走起来像鸭子、叫起来像鸭子,那么它就是鸭子”的说法。例如,你可以通过验证一个对象是否遵循迭代协议,判断它是可迭代的。对于许多对象,这意味着它有一个`__iter__`魔术方法,其它更好的判断方法是使用`iter`函数:
722 |
723 | ```python
724 | def isiterable(obj):
725 | try:
726 | iter(obj)
727 | return True
728 | except TypeError: # not iterable
729 | return False
730 | ```
731 |
732 | 这个函数会返回字符串以及大多数Python集合类型为`True`:
733 |
734 | ```text
735 | In [29]: isiterable('a string')
736 | Out[29]: True
737 |
738 | In [30]: isiterable([1, 2, 3])
739 | Out[30]: True
740 |
741 | In [31]: isiterable(5)
742 | Out[31]: False
743 | ```
744 |
745 | 我总是用这个功能编写可以接受多种输入类型的函数。常见的例子是编写一个函数可以接受任意类型的序列(list、tuple、ndarray)或是迭代器。你可先检验对象是否是列表(或是NUmPy数组),如果不是的话,将其转变成列表:
746 |
747 | ```python
748 | if not isinstance(x, list) and isiterable(x):
749 | x = list(x)
750 | ```
751 |
752 | ### 引入
753 |
754 | 在Python中,模块就是一个有`.py`扩展名、包含Python代码的文件。假设有以下模块:
755 |
756 | ```python
757 | # some_module.py
758 | PI = 3.14159
759 |
760 | def f(x):
761 | return x + 2
762 |
763 | def g(a, b):
764 | return a + b
765 | ```
766 |
767 | 如果想从同目录下的另一个文件访问`some_module.py`中定义的变量和函数,可以:
768 |
769 | ```python
770 | import some_module
771 | result = some_module.f(5)
772 | pi = some_module.PI
773 | ```
774 |
775 | 或者:
776 |
777 | ```python
778 | from some_module import f, g, PI
779 | result = g(5, PI)
780 | ```
781 |
782 | 使用`as`关键词,你可以给引入起不同的变量名:
783 |
784 | ```python
785 | import some_module as sm
786 | from some_module import PI as pi, g as gf
787 |
788 | r1 = sm.f(pi)
789 | r2 = gf(6, pi)
790 | ```
791 |
792 | ### 二元运算符和比较运算符
793 |
794 | 大多数二元数学运算和比较都不难想到:
795 |
796 | ```python
797 | In [32]: 5 - 7
798 | Out[32]: -2
799 |
800 | In [33]: 12 + 21.5
801 | Out[33]: 33.5
802 |
803 | In [34]: 5 <= 2
804 | Out[34]: False
805 | ```
806 |
807 | 表2-3列出了所有的二元运算符。
808 |
809 | 要判断两个引用是否指向同一个对象,可以使用`is`方法。`is not`可以判断两个对象是不同的:
810 |
811 | ```python
812 | In [35]: a = [1, 2, 3]
813 |
814 | In [36]: b = a
815 |
816 | In [37]: c = list(a)
817 |
818 | In [38]: a is b
819 | Out[38]: True
820 |
821 | In [39]: a is not c
822 | Out[39]: True
823 | ```
824 |
825 | 因为`list`总是创建一个新的Python列表(即复制),我们可以断定c是不同于a的。使用`is`比较与`==`运算符不同,如下:
826 |
827 | ```python
828 | In [40]: a == c
829 | Out[40]: True
830 | ```
831 |
832 | `is`和`is not`常用来判断一个变量是否为`None`,因为只有一个`None`的实例:
833 |
834 | ```python
835 | In [41]: a = None
836 |
837 | In [42]: a is None
838 | Out[42]: True
839 | ```
840 |
841 | 
842 |
843 | ### 可变与不可变对象
844 |
845 | Python中的大多数对象,比如列表、字典、NumPy数组,和用户定义的类型(类),都是可变的。意味着这些对象或包含的值可以被修改:
846 |
847 | ```python
848 | In [43]: a_list = ['foo', 2, [4, 5]]
849 |
850 | In [44]: a_list[2] = (3, 4)
851 |
852 | In [45]: a_list
853 | Out[45]: ['foo', 2, (3, 4)]
854 | ```
855 |
856 | 其它的,例如字符串和元组,是不可变的:
857 |
858 | ```python
859 | In [46]: a_tuple = (3, 5, (4, 5))
860 |
861 | In [47]: a_tuple[1] = 'four'
862 | ---------------------------------------------------------------------------
863 | TypeError Traceback (most recent call last)
864 | in ()
865 | ----> 1 a_tuple[1] = 'four'
866 | TypeError: 'tuple' object does not support item assignment
867 | ```
868 |
869 | 记住,可以修改一个对象并不意味就要修改它。这被称为副作用。例如,当写一个函数,任何副作用都要在文档或注释中写明。如果可能的话,我推荐避免副作用,采用不可变的方式,即使要用到可变对象。
870 |
871 | ### 标量类型
872 |
873 | Python的标准库中有一些内建的类型,用于处理数值数据、字符串、布尔值,和日期时间。这些单值类型被称为标量类型,本书中称其为标量。表2-4列出了主要的标量。日期和时间处理会另外讨论,因为它们是标准库的`datetime`模块提供的。
874 |
875 | 
876 |
877 | ### 数值类型
878 |
879 | Python的主要数值类型是`int`和`float`。`int`可以存储任意大的数:
880 |
881 | ```python
882 | In [48]: ival = 17239871
883 |
884 | In [49]: ival ** 6
885 | Out[49]: 26254519291092456596965462913230729701102721
886 | ```
887 |
888 | 浮点数使用Python的`float`类型。每个数都是双精度(64位)的值。也可以用科学计数法表示:
889 |
890 | ```python
891 | In [50]: fval = 7.243
892 |
893 | In [51]: fval2 = 6.78e-5
894 | ```
895 |
896 | 不能得到整数的除法会得到浮点数:
897 |
898 | ```python
899 | In [52]: 3 / 2
900 | Out[52]: 1.5
901 | ```
902 |
903 | 要获得C-风格的整除(去掉小数部分),可以使用底除运算符//:
904 |
905 | ```python
906 | In [53]: 3 // 2
907 | Out[53]: 1
908 | ```
909 |
910 | ### 字符串
911 |
912 | 许多人是因为Python强大而灵活的字符串处理而使用Python的。你可以用单引号或双引号来写字符串:
913 |
914 | ```python
915 | a = 'one way of writing a string'
916 | b = "another way"
917 | ```
918 |
919 | 对于有换行符的字符串,可以使用三引号,'''或"""都行:
920 |
921 | ```python
922 | c = """
923 | This is a longer string that
924 | spans multiple lines
925 | """
926 | ```
927 |
928 | 字符串`c`实际包含四行文本,"""后面和lines后面的换行符。可以用`count`方法计算`c`中的新的行:
929 |
930 | ```python
931 | In [55]: c.count('\n')
932 | Out[55]: 3
933 | ```
934 |
935 | Python的字符串是不可变的,不能修改字符串:
936 |
937 | ```python
938 | In [56]: a = 'this is a string'
939 |
940 | In [57]: a[10] = 'f'
941 | ---------------------------------------------------------------------------
942 | TypeError Traceback (most recent call last)
943 | in ()
944 | ----> 1 a[10] = 'f'
945 | TypeError: 'str' object does not support item assignment
946 |
947 | In [58]: b = a.replace('string', 'longer string')
948 |
949 | In [59]: b
950 | Out[59]: 'this is a longer string'
951 | ```
952 |
953 | 经过以上的操作,变量`a`并没有被修改:
954 |
955 | ```python
956 | In [60]: a
957 | Out[60]: 'this is a string'
958 | ```
959 |
960 | 许多Python对象使用`str`函数可以被转化为字符串:
961 |
962 | ```python
963 | In [61]: a = 5.6
964 |
965 | In [62]: s = str(a)
966 |
967 | In [63]: print(s)
968 | 5.6
969 | ```
970 |
971 | 字符串是一个序列的Unicode字符,因此可以像其它序列,比如列表和元组(下一章会详细介绍两者)一样处理:
972 |
973 | ```python
974 | In [64]: s = 'python'
975 |
976 | In [65]: list(s)
977 | Out[65]: ['p', 'y', 't', 'h', 'o', 'n']
978 |
979 | In [66]: s[:3]
980 | Out[66]: 'pyt'
981 | ```
982 |
983 | 语法`s[:3]`被称作切片,适用于许多Python序列。后面会更详细的介绍,本书中用到很多切片。
984 |
985 | 反斜杠是转义字符,意思是它备用来表示特殊字符,比如换行符\n或Unicode字符。要写一个包含反斜杠的字符串,需要进行转义:
986 |
987 | ```python
988 | In [67]: s = '12\\34'
989 |
990 | In [68]: print(s)
991 | 12\34
992 | ```
993 |
994 | 如果字符串中包含许多反斜杠,但没有特殊字符,这样做就很麻烦。幸好,可以在字符串前面加一个r,表明字符就是它自身:
995 |
996 | ```python
997 | In [69]: s = r'this\has\no\special\characters'
998 |
999 | In [70]: s
1000 | Out[70]: 'this\\has\\no\\special\\characters'
1001 | ```
1002 |
1003 | r表示raw。
1004 |
1005 | 将两个字符串合并,会产生一个新的字符串:
1006 |
1007 | ```python
1008 | In [71]: a = 'this is the first half '
1009 |
1010 | In [72]: b = 'and this is the second half'
1011 |
1012 | In [73]: a + b
1013 | Out[73]: 'this is the first half and this is the second half'
1014 | ```
1015 |
1016 | 字符串的模板化或格式化,是另一个重要的主题。Python 3拓展了此类的方法,这里只介绍一些。字符串对象有`format`方法,可以替换格式化的参数为字符串,产生一个新的字符串:
1017 |
1018 | ```python
1019 | In [74]: template = '{0:.2f} {1:s} are worth US${2:d}'
1020 | ```
1021 |
1022 | 在这个字符串中,
1023 |
1024 | * `{0:.2f}`表示格式化第一个参数为带有两位小数的浮点数。
1025 | * `{1:s}`表示格式化第二个参数为字符串。
1026 | * `{2:d}`表示格式化第三个参数为一个整数。
1027 |
1028 | 要替换参数为这些格式化的参数,我们传递`format`方法一个序列:
1029 |
1030 | ```python
1031 | In [75]: template.format(4.5560, 'Argentine Pesos', 1)
1032 | Out[75]: '4.56 Argentine Pesos are worth US$1'
1033 | ```
1034 |
1035 | 字符串格式化是一个很深的主题,有多种方法和大量的选项,可以控制字符串中的值是如何格式化的。推荐参阅Python官方文档。
1036 |
1037 | 这里概括介绍字符串处理,第8章的数据分析会详细介绍。
1038 |
1039 | ### 字节和Unicode
1040 |
1041 | 在Python 3及以上版本中,Unicode是一级的字符串类型,这样可以更一致的处理ASCII和Non-ASCII文本。在老的Python版本中,字符串都是字节,不使用Unicode编码。假如知道字符编码,可以将其转化为Unicode。看一个例子:
1042 |
1043 | ```python
1044 | In [76]: val = "español"
1045 |
1046 | In [77]: val
1047 | Out[77]: 'español'
1048 | ```
1049 |
1050 | 可以用`encode`将这个Unicode字符串编码为UTF-8:
1051 |
1052 | ```python
1053 | In [78]: val_utf8 = val.encode('utf-8')
1054 |
1055 | In [79]: val_utf8
1056 | Out[79]: b'espa\xc3\xb1ol'
1057 |
1058 | In [80]: type(val_utf8)
1059 | Out[80]: bytes
1060 | ```
1061 |
1062 | 如果你知道一个字节对象的Unicode编码,用`decode`方法可以解码:
1063 |
1064 | ```python
1065 | In [81]: val_utf8.decode('utf-8')
1066 | Out[81]: 'español'
1067 | ```
1068 |
1069 | 虽然UTF-8编码已经变成主流,但因为历史的原因,你仍然可能碰到其它编码的数据:
1070 |
1071 | ```python
1072 | In [82]: val.encode('latin1')
1073 | Out[82]: b'espa\xf1ol'
1074 |
1075 | In [83]: val.encode('utf-16')
1076 | Out[83]: b'\xff\xfee\x00s\x00p\x00a\x00\xf1\x00o\x00l\x00'
1077 |
1078 | In [84]: val.encode('utf-16le')
1079 | Out[84]: b'e\x00s\x00p\x00a\x00\xf1\x00o\x00l\x00'
1080 | ```
1081 |
1082 | 工作中碰到的文件很多都是字节对象,盲目地将所有数据编码为Unicode是不可取的。
1083 |
1084 | 虽然用的不多,你可以在字节文本的前面加上一个b:
1085 |
1086 | ```python
1087 | In [85]: bytes_val = b'this is bytes'
1088 |
1089 | In [86]: bytes_val
1090 | Out[86]: b'this is bytes'
1091 |
1092 | In [87]: decoded = bytes_val.decode('utf8')
1093 |
1094 | In [88]: decoded # this is str (Unicode) now
1095 | Out[88]: 'this is bytes'
1096 | ```
1097 |
1098 | ### 布尔值
1099 |
1100 | Python中的布尔值有两个,True和False。比较和其它条件表达式可以用True和False判断。布尔值可以与and和or结合使用:
1101 |
1102 | ```python
1103 | In [89]: True and True
1104 | Out[89]: True
1105 |
1106 | In [90]: False or True
1107 | Out[90]: True
1108 | ```
1109 |
1110 | ### 类型转换
1111 |
1112 | str、bool、int和float也是函数,可以用来转换类型:
1113 |
1114 | ```python
1115 | In [91]: s = '3.14159'
1116 |
1117 | In [92]: fval = float(s)
1118 |
1119 | In [93]: type(fval)
1120 | Out[93]: float
1121 |
1122 | In [94]: int(fval)
1123 | Out[94]: 3
1124 |
1125 | In [95]: bool(fval)
1126 | Out[95]: True
1127 |
1128 | In [96]: bool(0)
1129 | Out[96]: False
1130 | ```
1131 |
1132 | ### None
1133 |
1134 | None是Python的空值类型。如果一个函数没有明确的返回值,就会默认返回None:
1135 |
1136 | ```python
1137 | In [97]: a = None
1138 |
1139 | In [98]: a is None
1140 | Out[98]: True
1141 |
1142 | In [99]: b = 5
1143 |
1144 | In [100]: b is not None
1145 | Out[100]: True
1146 | ```
1147 |
1148 | None也常常作为函数的默认参数:
1149 |
1150 | ```python
1151 | def add_and_maybe_multiply(a, b, c=None):
1152 | result = a + b
1153 |
1154 | if c is not None:
1155 | result = result * c
1156 |
1157 | return result
1158 | ```
1159 |
1160 | 另外,None不仅是一个保留字,还是唯一的NoneType的实例:
1161 |
1162 | ```python
1163 | In [101]: type(None)
1164 | Out[101]: NoneType
1165 | ```
1166 |
1167 | ### 日期和时间
1168 |
1169 | Python内建的`datetime`模块提供了`datetime`、`date`和`time`类型。`datetime`类型结合了`date`和`time`,是最常使用的:
1170 |
1171 | ```python
1172 | In [102]: from datetime import datetime, date, time
1173 |
1174 | In [103]: dt = datetime(2011, 10, 29, 20, 30, 21)
1175 |
1176 | In [104]: dt.day
1177 | Out[104]: 29
1178 |
1179 | In [105]: dt.minute
1180 | Out[105]: 30
1181 | ```
1182 |
1183 | 根据`datetime`实例,你可以用`date`和`time`提取出各自的对象:
1184 |
1185 | ```python
1186 | In [106]: dt.date()
1187 | Out[106]: datetime.date(2011, 10, 29)
1188 |
1189 | In [107]: dt.time()
1190 | Out[107]: datetime.time(20, 30, 21)
1191 | ```
1192 |
1193 | `strftime`方法可以将datetime格式化为字符串:
1194 |
1195 | ```python
1196 | In [108]: dt.strftime('%m/%d/%Y %H:%M')
1197 | Out[108]: '10/29/2011 20:30'
1198 | ```
1199 |
1200 | `strptime`可以将字符串转换成`datetime`对象:
1201 |
1202 | ```python
1203 | In [109]: datetime.strptime('20091031', '%Y%m%d')
1204 | Out[109]: datetime.datetime(2009, 10, 31, 0, 0)
1205 | ```
1206 |
1207 | 表2-5列出了所有的格式化命令。
1208 |
1209 | 
1210 |
1211 | 当你聚类或对时间序列进行分组,替换datetimes的time字段有时会很有用。例如,用0替换分和秒:
1212 |
1213 | ```python
1214 | In [110]: dt.replace(minute=0, second=0)
1215 | Out[110]: datetime.datetime(2011, 10, 29, 20, 0)
1216 | ```
1217 |
1218 | 因为`datetime.datetime`是不可变类型,上面的方法会产生新的对象。
1219 |
1220 | 两个datetime对象的差会产生一个`datetime.timedelta`类型:
1221 |
1222 | ```python
1223 | In [111]: dt2 = datetime(2011, 11, 15, 22, 30)
1224 |
1225 | In [112]: delta = dt2 - dt
1226 |
1227 | In [113]: delta
1228 | Out[113]: datetime.timedelta(17, 7179)
1229 |
1230 | In [114]: type(delta)
1231 | Out[114]: datetime.timedelta
1232 | ```
1233 |
1234 | 结果`timedelta(17, 7179)`指明了`timedelta`将17天、7179秒的编码方式。
1235 |
1236 | 将`timedelta`添加到`datetime`,会产生一个新的偏移`datetime`:
1237 |
1238 | ```python
1239 | In [115]: dt
1240 | Out[115]: datetime.datetime(2011, 10, 29, 20, 30, 21)
1241 |
1242 | In [116]: dt + delta
1243 | Out[116]: datetime.datetime(2011, 11, 15, 22, 30)
1244 | ```
1245 |
1246 | ### 控制流
1247 |
1248 | Python有若干内建的关键字进行条件逻辑、循环和其它控制流操作。
1249 |
1250 | ### if、elif和else
1251 |
1252 | if是最广为人知的控制流语句。它检查一个条件,如果为True,就执行后面的语句:
1253 |
1254 | ```python
1255 | if x < 0:
1256 | print('It's negative')
1257 | ```
1258 |
1259 | `if`后面可以跟一个或多个`elif`,所有条件都是False时,还可以添加一个`else`:
1260 |
1261 | ```python
1262 | if x < 0:
1263 | print('It's negative')
1264 | elif x == 0:
1265 | print('Equal to zero')
1266 | elif 0 < x < 5:
1267 | print('Positive but smaller than 5')
1268 | else:
1269 | print('Positive and larger than or equal to 5')
1270 | ```
1271 |
1272 | 如果某个条件为True,后面的`elif`就不会被执行。当使用and和or时,复合条件语句是从左到右执行:
1273 |
1274 | ```python
1275 | In [117]: a = 5; b = 7
1276 |
1277 | In [118]: c = 8; d = 4
1278 |
1279 | In [119]: if a < b or c > d:
1280 | .....: print('Made it')
1281 | Made it
1282 | ```
1283 |
1284 | 在这个例子中,`c > d`不会被执行,因为第一个比较是True:
1285 |
1286 | 也可以把比较式串在一起:
1287 |
1288 | ```python
1289 | In [120]: 4 > 3 > 2 > 1
1290 | Out[120]: True
1291 | ```
1292 |
1293 | ### for循环
1294 |
1295 | for循环是在一个集合(列表或元组)中进行迭代,或者就是一个迭代器。for循环的标准语法是:
1296 |
1297 | ```python
1298 | for value in collection:
1299 | # do something with value
1300 | ```
1301 |
1302 | 你可以用continue使for循环提前,跳过剩下的部分。看下面这个例子,将一个列表中的整数相加,跳过None:
1303 |
1304 | ```python
1305 | sequence = [1, 2, None, 4, None, 5]
1306 | total = 0
1307 | for value in sequence:
1308 | if value is None:
1309 | continue
1310 | total += value
1311 | ```
1312 |
1313 | 可以用`break`跳出for循环。下面的代码将各元素相加,直到遇到5:
1314 |
1315 | ```python
1316 | sequence = [1, 2, 0, 4, 6, 5, 2, 1]
1317 | total_until_5 = 0
1318 | for value in sequence:
1319 | if value == 5:
1320 | break
1321 | total_until_5 += value
1322 | ```
1323 |
1324 | break只中断for循环的最内层,其余的for循环仍会运行:
1325 |
1326 | ```python
1327 | In [121]: for i in range(4):
1328 | .....: for j in range(4):
1329 | .....: if j > i:
1330 | .....: break
1331 | .....: print((i, j))
1332 | .....:
1333 | (0, 0)
1334 | (1, 0)
1335 | (1, 1)
1336 | (2, 0)
1337 | (2, 1)
1338 | (2, 2)
1339 | (3, 0)
1340 | (3, 1)
1341 | (3, 2)
1342 | (3, 3)
1343 | ```
1344 |
1345 | 如果集合或迭代器中的元素序列(元组或列表),可以用for循环将其方便地拆分成变量:
1346 |
1347 | ```python
1348 | for a, b, c in iterator:
1349 | # do something
1350 | ```
1351 |
1352 | ### While循环
1353 |
1354 | while循环指定了条件和代码,当条件为False或用break退出循环,代码才会退出:
1355 |
1356 | ```python
1357 | x = 256
1358 | total = 0
1359 | while x > 0:
1360 | if total > 500:
1361 | break
1362 | total += x
1363 | x = x // 2
1364 | ```
1365 |
1366 | ### pass
1367 |
1368 | pass是Python中的非操作语句。代码块不需要任何动作时可以使用(作为未执行代码的占位符);因为Python需要使用空白字符划定代码块,所以需要pass:
1369 |
1370 | ```python
1371 | if x < 0:
1372 | print('negative!')
1373 | elif x == 0:
1374 | # TODO: put something smart here
1375 | pass
1376 | else:
1377 | print('positive!')
1378 | ```
1379 |
1380 | ### range
1381 |
1382 | range函数返回一个迭代器,它产生一个均匀分布的整数序列:
1383 |
1384 | ```python
1385 | In [122]: range(10)
1386 | Out[122]: range(0, 10)
1387 |
1388 | In [123]: list(range(10))
1389 | Out[123]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1390 | ```
1391 |
1392 | range的三个参数是(起点,终点,步进):
1393 |
1394 | ```python
1395 | In [124]: list(range(0, 20, 2))
1396 | Out[124]: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
1397 |
1398 | In [125]: list(range(5, 0, -1))
1399 | Out[125]: [5, 4, 3, 2, 1]
1400 | ```
1401 |
1402 | 可以看到,range产生的整数不包括终点。range的常见用法是用序号迭代序列:
1403 |
1404 | ```python
1405 | seq = [1, 2, 3, 4]
1406 | for i in range(len(seq)):
1407 | val = seq[i]
1408 | ```
1409 |
1410 | 可以使用list来存储range在其他数据结构中生成的所有整数,默认的迭代器形式通常是你想要的。下面的代码对0到99999中3或5的倍数求和:
1411 |
1412 | ```python
1413 | sum = 0
1414 | for i in range(100000):
1415 | # % is the modulo operator
1416 | if i % 3 == 0 or i % 5 == 0:
1417 | sum += i
1418 | ```
1419 |
1420 | 虽然range可以产生任意大的数,但任意时刻耗用的内存却很小。
1421 |
1422 | ### 三元表达式
1423 |
1424 | Python中的三元表达式可以将if-else语句放到一行里。语法如下:
1425 |
1426 | ```python
1427 | value = true-expr if condition else false-expr
1428 | ```
1429 |
1430 | `true-expr`或`false-expr`可以是任何Python代码。它和下面的代码效果相同:
1431 |
1432 | ```python
1433 | if condition:
1434 | value = true-expr
1435 | else:
1436 | value = false-expr
1437 | ```
1438 |
1439 | 下面是一个更具体的例子:
1440 |
1441 | ```python
1442 | In [126]: x = 5
1443 |
1444 | In [127]: 'Non-negative' if x >= 0 else 'Negative'
1445 | Out[127]: 'Non-negative'
1446 | ```
1447 |
1448 | 和if-else一样,只有一个表达式会被执行。因此,三元表达式中的if和else可以包含大量的计算,但只有True的分支会被执行。因此,三元表达式中的if和else可以包含大量的计算,但只有True的分支会被执行。
1449 |
1450 | 虽然使用三元表达式可以压缩代码,但会降低代码可读性。
1451 |
1452 |
--------------------------------------------------------------------------------
/第06章 数据加载、存储与文件格式.md:
--------------------------------------------------------------------------------
1 | 访问数据是使用本书所介绍的这些工具的第一步。我会着重介绍pandas的数据输入与输出,虽然别的库中也有不少以此为目的的工具。
2 |
3 | 输入输出通常可以划分为几个大类:读取文本文件和其他更高效的磁盘存储格式,加载数据库中的数据,利用Web API操作网络资源。
4 |
5 | # 6.1 读写文本格式的数据
6 | pandas提供了一些用于将表格型数据读取为DataFrame对象的函数。表6-1对它们进行了总结,其中read_csv和read_table可能会是你今后用得最多的。
7 |
8 | 
9 |
10 | 我将大致介绍一下这些函数在将文本数据转换为DataFrame时所用到的一些技术。这些函数的选项可以划分为以下几个大类:
11 |
12 | - 索引:将一个或多个列当做返回的DataFrame处理,以及是否从文件、用户获取列名。
13 | - 类型推断和数据转换:包括用户定义值的转换、和自定义的缺失值标记列表等。
14 | - 日期解析:包括组合功能,比如将分散在多个列中的日期时间信息组合成结果中的单个列。
15 | - 迭代:支持对大文件进行逐块迭代。
16 | - 不规整数据问题:跳过一些行、页脚、注释或其他一些不重要的东西(比如由成千上万个逗号隔开的数值数据)。
17 |
18 | 因为工作中实际碰到的数据可能十分混乱,一些数据加载函数(尤其是read_csv)的选项逐渐变得复杂起来。面对不同的参数,感到头痛很正常(read_csv有超过50个参数)。pandas文档有这些参数的例子,如果你感到阅读某个文件很难,可以通过相似的足够多的例子找到正确的参数。
19 |
20 | 其中一些函数,比如pandas.read_csv,有类型推断功能,因为列数据的类型不属于数据类型。也就是说,你不需要指定列的类型到底是数值、整数、布尔值,还是字符串。其它的数据格式,如HDF5、Feather和msgpack,会在格式中存储数据类型。
21 |
22 | 日期和其他自定义类型的处理需要多花点工夫才行。首先我们来看一个以逗号分隔的(CSV)文本文件:
23 | ```python
24 | In [8]: !cat examples/ex1.csv
25 | a,b,c,d,message
26 | 1,2,3,4,hello
27 | 5,6,7,8,world
28 | 9,10,11,12,foo
29 | ```
30 |
31 | >笔记:这里,我用的是Unix的cat shell命令将文件的原始内容打印到屏幕上。如果你用的是Windows,你可以使用type达到同样的效果。
32 |
33 | 由于该文件以逗号分隔,所以我们可以使用read_csv将其读入一个DataFrame:
34 | ```python
35 | In [9]: df = pd.read_csv('examples/ex1.csv')
36 |
37 | In [10]: df
38 | Out[10]:
39 | a b c d message
40 | 0 1 2 3 4 hello
41 | 1 5 6 7 8 world
42 | 2 9 10 11 12 foo
43 | ```
44 |
45 | 我们还可以使用read_table,并指定分隔符:
46 | ```python
47 | In [11]: pd.read_table('examples/ex1.csv', sep=',')
48 | Out[11]:
49 | a b c d message
50 | 0 1 2 3 4 hello
51 | 1 5 6 7 8 world
52 | 2 9 10 11 12 foo
53 | ```
54 |
55 | 并不是所有文件都有标题行。看看下面这个文件:
56 | ```python
57 | In [12]: !cat examples/ex2.csv
58 | 1,2,3,4,hello
59 | 5,6,7,8,world
60 | 9,10,11,12,foo
61 | ```
62 |
63 | 读入该文件的办法有两个。你可以让pandas为其分配默认的列名,也可以自己定义列名:
64 | ```python
65 | In [13]: pd.read_csv('examples/ex2.csv', header=None)
66 | Out[13]:
67 | 0 1 2 3 4
68 | 0 1 2 3 4 hello
69 | 1 5 6 7 8 world
70 | 2 9 10 11 12 foo
71 |
72 | In [14]: pd.read_csv('examples/ex2.csv', names=['a', 'b', 'c', 'd', 'message'])
73 | Out[14]:
74 | a b c d message
75 | 0 1 2 3 4 hello
76 | 1 5 6 7 8 world
77 | 2 9 10 11 12 foo
78 | ```
79 |
80 | 假设你希望将message列做成DataFrame的索引。你可以明确表示要将该列放到索引4的位置上,也可以通过index_col参数指定"message":
81 | ```python
82 | In [15]: names = ['a', 'b', 'c', 'd', 'message']
83 |
84 | In [16]: pd.read_csv('examples/ex2.csv', names=names, index_col='message')
85 | Out[16]:
86 | a b c d
87 | message
88 | hello 1 2 3 4
89 | world 5 6 7 8
90 | foo 9 10 11 12
91 | ```
92 |
93 | 如果希望将多个列做成一个层次化索引,只需传入由列编号或列名组成的列表即可:
94 | ```python
95 | In [17]: !cat examples/csv_mindex.csv
96 | key1,key2,value1,value2
97 | one,a,1,2
98 | one,b,3,4
99 | one,c,5,6
100 | one,d,7,8
101 | two,a,9,10
102 | two,b,11,12
103 | two,c,13,14
104 | two,d,15,16
105 |
106 | In [18]: parsed = pd.read_csv('examples/csv_mindex.csv',
107 | ....: index_col=['key1', 'key2'])
108 |
109 | In [19]: parsed
110 | Out[19]:
111 | value1 value2
112 | key1 key2
113 | one a 1 2
114 | b 3 4
115 | c 5 6
116 | d 7 8
117 | two a 9 10
118 | b 11 12
119 | c 13 14
120 | d 15 16
121 | ```
122 |
123 | 有些情况下,有些表格可能不是用固定的分隔符去分隔字段的(比如空白符或其它模式)。看看下面这个文本文件:
124 | ```python
125 | In [20]: list(open('examples/ex3.txt'))
126 | Out[20]:
127 | [' A B C\n',
128 | 'aaa -0.264438 -1.026059 -0.619500\n',
129 | 'bbb 0.927272 0.302904 -0.032399\n',
130 | 'ccc -0.264273 -0.386314 -0.217601\n',
131 | 'ddd -0.871858 -0.348382 1.100491\n']
132 | ```
133 |
134 | 虽然可以手动对数据进行规整,这里的字段是被数量不同的空白字符间隔开的。这种情况下,你可以传递一个正则表达式作为read_table的分隔符。可以用正则表达式表达为\s+,于是有:
135 | ```python
136 | In [21]: result = pd.read_table('examples/ex3.txt', sep='\s+')
137 |
138 | In [22]: result
139 | Out[22]:
140 | A B C
141 | aaa -0.264438 -1.026059 -0.619500
142 | bbb 0.927272 0.302904 -0.032399
143 | ccc -0.264273 -0.386314 -0.217601
144 | ddd -0.871858 -0.348382 1.100491
145 | ```
146 |
147 | 这里,由于列名比数据行的数量少,所以read_table推断第一列应该是DataFrame的索引。
148 |
149 | 这些解析器函数还有许多参数可以帮助你处理各种各样的异形文件格式(表6-2列出了一些)。比如说,你可以用skiprows跳过文件的第一行、第三行和第四行:
150 | ```python
151 | In [23]: !cat examples/ex4.csv
152 | # hey!
153 | a,b,c,d,message
154 | # just wanted to make things more difficult for you
155 | # who reads CSV files with computers, anyway?
156 | 1,2,3,4,hello
157 | 5,6,7,8,world
158 | 9,10,11,12,foo
159 | In [24]: pd.read_csv('examples/ex4.csv', skiprows=[0, 2, 3])
160 | Out[24]:
161 | a b c d message
162 | 0 1 2 3 4 hello
163 | 1 5 6 7 8 world
164 | 2 9 10 11 12 foo
165 | ```
166 |
167 | 缺失值处理是文件解析任务中的一个重要组成部分。缺失数据经常是要么没有(空字符串),要么用某个标记值表示。默认情况下,pandas会用一组经常出现的标记值进行识别,比如NA及NULL:
168 | ```python
169 | In [25]: !cat examples/ex5.csv
170 | something,a,b,c,d,message
171 | one,1,2,3,4,NA
172 | two,5,6,,8,world
173 | three,9,10,11,12,foo
174 | In [26]: result = pd.read_csv('examples/ex5.csv')
175 |
176 | In [27]: result
177 | Out[27]:
178 | something a b c d message
179 | 0 one 1 2 3.0 4 NaN
180 | 1 two 5 6 NaN 8 world
181 | 2 three 9 10 11.0 12 foo
182 |
183 | In [28]: pd.isnull(result)
184 | Out[28]:
185 | something a b c d message
186 | 0 False False False False False True
187 | 1 False False False True False False
188 | 2 False False False False False False
189 | ```
190 |
191 | na_values可以用一个列表或集合的字符串表示缺失值:
192 | ```python
193 | In [29]: result = pd.read_csv('examples/ex5.csv', na_values=['NULL'])
194 |
195 | In [30]: result
196 | Out[30]:
197 | something a b c d message
198 | 0 one 1 2 3.0 4 NaN
199 | 1 two 5 6 NaN 8 world
200 | 2 three 9 10 11.0 12 foo
201 | ```
202 |
203 | 字典的各列可以使用不同的NA标记值:
204 | ```python
205 | In [31]: sentinels = {'message': ['foo', 'NA'], 'something': ['two']}
206 |
207 | In [32]: pd.read_csv('examples/ex5.csv', na_values=sentinels)
208 | Out[32]:
209 | something a b c d message
210 | 0 one 1 2 3.0 4 NaN
211 | 1 NaN 5 6 NaN 8 world
212 | 2 three 9 10 11.0 12 NaN
213 | ```
214 |
215 | 表6-2列出了pandas.read_csv和pandas.read_table常用的选项。
216 |
217 | 
218 |
219 | 
220 |
221 | 
222 |
223 | ## 逐块读取文本文件
224 | 在处理很大的文件时,或找出大文件中的参数集以便于后续处理时,你可能只想读取文件的一小部分或逐块对文件进行迭代。
225 |
226 | 在看大文件之前,我们先设置pandas显示地更紧些:
227 | ```python
228 | In [33]: pd.options.display.max_rows = 10
229 | ```
230 |
231 | 然后有:
232 | ```python
233 | In [34]: result = pd.read_csv('examples/ex6.csv')
234 |
235 | In [35]: result
236 | Out[35]:
237 | one two three four key
238 | 0 0.467976 -0.038649 -0.295344 -1.824726 L
239 | 1 -0.358893 1.404453 0.704965 -0.200638 B
240 | 2 -0.501840 0.659254 -0.421691 -0.057688 G
241 | 3 0.204886 1.074134 1.388361 -0.982404 R
242 | 4 0.354628 -0.133116 0.283763 -0.837063 Q
243 | ... ... ... ... ... ..
244 | 9995 2.311896 -0.417070 -1.409599 -0.515821 L
245 | 9996 -0.479893 -0.650419 0.745152 -0.646038 E
246 | 9997 0.523331 0.787112 0.486066 1.093156 K
247 | 9998 -0.362559 0.598894 -1.843201 0.887292 G
248 | 9999 -0.096376 -1.012999 -0.657431 -0.573315 0
249 | [10000 rows x 5 columns]
250 | If you want to only read a small
251 | ```
252 |
253 | 如果只想读取几行(避免读取整个文件),通过nrows进行指定即可:
254 | ```python
255 | In [36]: pd.read_csv('examples/ex6.csv', nrows=5)
256 | Out[36]:
257 | one two three four key
258 | 0 0.467976 -0.038649 -0.295344 -1.824726 L
259 | 1 -0.358893 1.404453 0.704965 -0.200638 B
260 | 2 -0.501840 0.659254 -0.421691 -0.057688 G
261 | 3 0.204886 1.074134 1.388361 -0.982404 R
262 | 4 0.354628 -0.133116 0.283763 -0.837063 Q
263 | ```
264 |
265 | 要逐块读取文件,可以指定chunksize(行数):
266 | ```python
267 | In [874]: chunker = pd.read_csv('ch06/ex6.csv', chunksize=1000)
268 |
269 | In [875]: chunker
270 | Out[875]:
271 | ```
272 |
273 | read_csv所返回的这个TextParser对象使你可以根据chunksize对文件进行逐块迭代。比如说,我们可以迭代处理ex6.csv,将值计数聚合到"key"列中,如下所示:
274 | ```python
275 | chunker = pd.read_csv('examples/ex6.csv', chunksize=1000)
276 |
277 | tot = pd.Series([])
278 | for piece in chunker:
279 | tot = tot.add(piece['key'].value_counts(), fill_value=0)
280 |
281 | tot = tot.sort_values(ascending=False)
282 | ```
283 |
284 | 然后有:
285 | ```python
286 | In [40]: tot[:10]
287 | Out[40]:
288 | E 368.0
289 | X 364.0
290 | L 346.0
291 | O 343.0
292 | Q 340.0
293 | M 338.0
294 | J 337.0
295 | F 335.0
296 | K 334.0
297 | H 330.0
298 | dtype: float64
299 | ```
300 |
301 | TextParser还有一个get_chunk方法,它使你可以读取任意大小的块。
302 |
303 | ## 将数据写出到文本格式
304 | 数据也可以被输出为分隔符格式的文本。我们再来看看之前读过的一个CSV文件:
305 | ```python
306 | In [41]: data = pd.read_csv('examples/ex5.csv')
307 |
308 | In [42]: data
309 | Out[42]:
310 | something a b c d message
311 | 0 one 1 2 3.0 4 NaN
312 | 1 two 5 6 NaN 8 world
313 | 2 three 9 10 11.0 12 foo
314 | ```
315 |
316 | 利用DataFrame的to_csv方法,我们可以将数据写到一个以逗号分隔的文件中:
317 | ```python
318 | In [43]: data.to_csv('examples/out.csv')
319 |
320 | In [44]: !cat examples/out.csv
321 | ,something,a,b,c,d,message
322 | 0,one,1,2,3.0,4,
323 | 1,two,5,6,,8,world
324 | 2,three,9,10,11.0,12,foo
325 | ```
326 |
327 | 当然,还可以使用其他分隔符(由于这里直接写出到sys.stdout,所以仅仅是打印出文本结果而已):
328 | ```python
329 | In [45]: import sys
330 |
331 | In [46]: data.to_csv(sys.stdout, sep='|')
332 | |something|a|b|c|d|message
333 | 0|one|1|2|3.0|4|
334 | 1|two|5|6||8|world
335 | 2|three|9|10|11.0|12|foo
336 | ```
337 |
338 | 缺失值在输出结果中会被表示为空字符串。你可能希望将其表示为别的标记值:
339 | ```python
340 | In [47]: data.to_csv(sys.stdout, na_rep='NULL')
341 | ,something,a,b,c,d,message
342 | 0,one,1,2,3.0,4,NULL
343 | 1,two,5,6,NULL,8,world
344 | 2,three,9,10,11.0,12,foo
345 | ```
346 |
347 | 如果没有设置其他选项,则会写出行和列的标签。当然,它们也都可以被禁用:
348 | ```python
349 | In [48]: data.to_csv(sys.stdout, index=False, header=False)
350 | one,1,2,3.0,4,
351 | two,5,6,,8,world
352 | three,9,10,11.0,12,foo
353 | ```
354 |
355 | 此外,你还可以只写出一部分的列,并以你指定的顺序排列:
356 | ```python
357 | In [49]: data.to_csv(sys.stdout, index=False, columns=['a', 'b', 'c'])
358 | a,b,c
359 | 1,2,3.0
360 | 5,6,
361 | 9,10,11.0
362 | ```
363 |
364 | Series也有一个to_csv方法:
365 | ```python
366 | In [50]: dates = pd.date_range('1/1/2000', periods=7)
367 |
368 | In [51]: ts = pd.Series(np.arange(7), index=dates)
369 |
370 | In [52]: ts.to_csv('examples/tseries.csv')
371 |
372 | In [53]: !cat examples/tseries.csv
373 | 2000-01-01,0
374 | 2000-01-02,1
375 | 2000-01-03,2
376 | 2000-01-04,3
377 | 2000-01-05,4
378 | 2000-01-06,5
379 | 2000-01-07,6
380 | ```
381 |
382 | ## 处理分隔符格式
383 | 大部分存储在磁盘上的表格型数据都能用pandas.read_table进行加载。然而,有时还是需要做一些手工处理。由于接收到含有畸形行的文件而使read_table出毛病的情况并不少见。为了说明这些基本工具,看看下面这个简单的CSV文件:
384 | ```python
385 | In [54]: !cat examples/ex7.csv
386 | "a","b","c"
387 | "1","2","3"
388 | "1","2","3"
389 | ```
390 |
391 | 对于任何单字符分隔符文件,可以直接使用Python内置的csv模块。将任意已打开的文件或文件型的对象传给csv.reader:
392 | ```python
393 | import csv
394 | f = open('examples/ex7.csv')
395 |
396 | reader = csv.reader(f)
397 | ```
398 |
399 | 对这个reader进行迭代将会为每行产生一个元组(并移除了所有的引号):
400 | ```python
401 | In [56]: for line in reader:
402 | ....: print(line)
403 | ['a', 'b', 'c']
404 | ['1', '2', '3']
405 | ['1', '2', '3']
406 | ```
407 |
408 | 现在,为了使数据格式合乎要求,你需要对其做一些整理工作。我们一步一步来做。首先,读取文件到一个多行的列表中:
409 | ```python
410 | In [57]: with open('examples/ex7.csv') as f:
411 | ....: lines = list(csv.reader(f))
412 | ```
413 |
414 | 然后,我们将这些行分为标题行和数据行:
415 | ```python
416 | In [58]: header, values = lines[0], lines[1:]
417 | ```
418 |
419 | 然后,我们可以用字典构造式和zip(*values),后者将行转置为列,创建数据列的字典:
420 | ```python
421 | In [59]: data_dict = {h: v for h, v in zip(header, zip(*values))}
422 |
423 | In [60]: data_dict
424 | Out[60]: {'a': ('1', '1'), 'b': ('2', '2'), 'c': ('3', '3')}
425 | ```
426 |
427 | CSV文件的形式有很多。只需定义csv.Dialect的一个子类即可定义出新格式(如专门的分隔符、字符串引用约定、行结束符等):
428 | ```python
429 | class my_dialect(csv.Dialect):
430 | lineterminator = '\n'
431 | delimiter = ';'
432 | quotechar = '"'
433 | quoting = csv.QUOTE_MINIMAL
434 | reader = csv.reader(f, dialect=my_dialect)
435 | ```
436 |
437 | 各个CSV语支的参数也可以用关键字的形式提供给csv.reader,而无需定义子类:
438 | ```python
439 | reader = csv.reader(f, delimiter='|')
440 | ```
441 |
442 | 可用的选项(csv.Dialect的属性)及其功能如表6-3所示。
443 |
444 | 
445 |
446 | >笔记:对于那些使用复杂分隔符或多字符分隔符的文件,csv模块就无能为力了。这种情况下,你就只能使用字符串的split方法或正则表达式方法re.split进行行拆分和其他整理工作了。
447 |
448 | 要手工输出分隔符文件,你可以使用csv.writer。它接受一个已打开且可写的文件对象以及跟csv.reader相同的那些语支和格式化选项:
449 | ```python
450 | with open('mydata.csv', 'w') as f:
451 | writer = csv.writer(f, dialect=my_dialect)
452 | writer.writerow(('one', 'two', 'three'))
453 | writer.writerow(('1', '2', '3'))
454 | writer.writerow(('4', '5', '6'))
455 | writer.writerow(('7', '8', '9'))
456 | ```
457 |
458 | ## JSON数据
459 | JSON(JavaScript Object Notation的简称)已经成为通过HTTP请求在Web浏览器和其他应用程序之间发送数据的标准格式之一。它是一种比表格型文本格式(如CSV)灵活得多的数据格式。下面是一个例子:
460 | ```python
461 | obj = """
462 | {"name": "Wes",
463 | "places_lived": ["United States", "Spain", "Germany"],
464 | "pet": null,
465 | "siblings": [{"name": "Scott", "age": 30, "pets": ["Zeus", "Zuko"]},
466 | {"name": "Katie", "age": 38,
467 | "pets": ["Sixes", "Stache", "Cisco"]}]
468 | }
469 | """
470 | ```
471 | 除其空值null和一些其他的细微差别(如列表末尾不允许存在多余的逗号)之外,JSON非常接近于有效的Python代码。基本类型有对象(字典)、数组(列表)、字符串、数值、布尔值以及null。对象中所有的键都必须是字符串。许多Python库都可以读写JSON数据。我将使用json,因为它是构建于Python标准库中的。通过json.loads即可将JSON字符串转换成Python形式:
472 | ```python
473 | In [62]: import json
474 |
475 | In [63]: result = json.loads(obj)
476 |
477 | In [64]: result
478 | Out[64]:
479 | {'name': 'Wes',
480 | 'pet': None,
481 | 'places_lived': ['United States', 'Spain', 'Germany'],
482 | 'siblings': [{'age': 30, 'name': 'Scott', 'pets': ['Zeus', 'Zuko']},
483 | {'age': 38, 'name': 'Katie', 'pets': ['Sixes', 'Stache', 'Cisco']}]}
484 | ```
485 |
486 | json.dumps则将Python对象转换成JSON格式:
487 | ```python
488 | In [65]: asjson = json.dumps(result)
489 | ```
490 |
491 | 如何将(一个或一组)JSON对象转换为DataFrame或其他便于分析的数据结构就由你决定了。最简单方便的方式是:向DataFrame构造器传入一个字典的列表(就是原先的JSON对象),并选取数据字段的子集:
492 | ```python
493 | In [66]: siblings = pd.DataFrame(result['siblings'], columns=['name', 'age'])
494 |
495 | In [67]: siblings
496 | Out[67]:
497 | name age
498 | 0 Scott 30
499 | 1 Katie 38
500 | ```
501 |
502 | pandas.read_json可以自动将特别格式的JSON数据集转换为Series或DataFrame。例如:
503 | ```python
504 | In [68]: !cat examples/example.json
505 | [{"a": 1, "b": 2, "c": 3},
506 | {"a": 4, "b": 5, "c": 6},
507 | {"a": 7, "b": 8, "c": 9}]
508 | ```
509 |
510 | pandas.read_json的默认选项假设JSON数组中的每个对象是表格中的一行:
511 | ```python
512 | In [69]: data = pd.read_json('examples/example.json')
513 |
514 | In [70]: data
515 | Out[70]:
516 | a b c
517 | 0 1 2 3
518 | 1 4 5 6
519 | 2 7 8 9
520 | ```
521 |
522 | 第7章中关于USDA Food Database的那个例子进一步讲解了JSON数据的读取和处理(包括嵌套记录)。
523 |
524 | 如果你需要将数据从pandas输出到JSON,可以使用to_json方法:
525 | ```python
526 | In [71]: print(data.to_json())
527 | {"a":{"0":1,"1":4,"2":7},"b":{"0":2,"1":5,"2":8},"c":{"0":3,"1":6,"2":9}}
528 |
529 | In [72]: print(data.to_json(orient='records'))
530 | [{"a":1,"b":2,"c":3},{"a":4,"b":5,"c":6},{"a":7,"b":8,"c":9}]
531 | ```
532 |
533 | ## XML和HTML:Web信息收集
534 |
535 | Python有许多可以读写常见的HTML和XML格式数据的库,包括lxml、Beautiful Soup和html5lib。lxml的速度比较快,但其它的库处理有误的HTML或XML文件更好。
536 |
537 | pandas有一个内置的功能,read_html,它可以使用lxml和Beautiful Soup自动将HTML文件中的表格解析为DataFrame对象。为了进行展示,我从美国联邦存款保险公司下载了一个HTML文件(pandas文档中也使用过),它记录了银行倒闭的情况。首先,你需要安装read_html用到的库:
538 | ```
539 | conda install lxml
540 | pip install beautifulsoup4 html5lib
541 | ```
542 |
543 | 如果你用的不是conda,可以使用``pip install lxml``。
544 |
545 | pandas.read_html有一些选项,默认条件下,它会搜索、尝试解析标签内的的表格数据。结果是一个列表的DataFrame对象:
546 | ```python
547 | In [73]: tables = pd.read_html('examples/fdic_failed_bank_list.html')
548 |
549 | In [74]: len(tables)
550 | Out[74]: 1
551 |
552 | In [75]: failures = tables[0]
553 |
554 | In [76]: failures.head()
555 | Out[76]:
556 | Bank Name City ST CERT \
557 | 0 Allied Bank Mulberry AR 91
558 | 1 The Woodbury Banking Company Woodbury GA 11297
559 | 2 First CornerStone Bank King of Prussia PA 35312
560 | 3 Trust Company Bank Memphis TN 9956
561 | 4 North Milwaukee State Bank Milwaukee WI 20364
562 | Acquiring Institution Closing Date Updated Date
563 | 0 Today's Bank September 23, 2016 November 17, 2016
564 | 1 United Bank August 19, 2016 November 17, 2016
565 | 2 First-Citizens Bank & Trust Company May 6, 2016 September 6, 2016
566 | 3 The Bank of Fayette County April 29, 2016 September 6, 2016
567 | 4 First-Citizens Bank & Trust Company March 11, 2016 June 16, 2016
568 | ```
569 |
570 | 因为failures有许多列,pandas插入了一个换行符\。
571 |
572 | 这里,我们可以做一些数据清洗和分析(后面章节会进一步讲解),比如计算按年份计算倒闭的银行数:
573 | ```python
574 | In [77]: close_timestamps = pd.to_datetime(failures['Closing Date'])
575 |
576 | In [78]: close_timestamps.dt.year.value_counts()
577 | Out[78]:
578 | 2010 157
579 | 2009 140
580 | 2011 92
581 | 2012 51
582 | 2008 25
583 | ...
584 | 2004 4
585 | 2001 4
586 | 2007 3
587 | 2003 3
588 | 2000 2
589 | Name: Closing Date, Length: 15, dtype: int64
590 | ```
591 |
592 | ## 利用lxml.objectify解析XML
593 | XML(Extensible Markup Language)是另一种常见的支持分层、嵌套数据以及元数据的结构化数据格式。本书所使用的这些文件实际上来自于一个很大的XML文档。
594 |
595 | 前面,我介绍了pandas.read_html函数,它可以使用lxml或Beautiful Soup从HTML解析数据。XML和HTML的结构很相似,但XML更为通用。这里,我会用一个例子演示如何利用lxml从XML格式解析数据。
596 |
597 | 纽约大都会运输署发布了一些有关其公交和列车服务的数据资料(http://www.mta.info/developers/download.html)。这里,我们将看看包含在一组XML文件中的运行情况数据。每项列车或公交服务都有各自的文件(如Metro-North Railroad的文件是Performance_MNR.xml),其中每条XML记录就是一条月度数据,如下所示:
598 | ```xml
599 |
600 | 373889
601 |
602 | Metro-North Railroad
603 | Escalator Availability
604 | Percent of the time that escalators are operational
605 | systemwide. The availability rate is based on physical observations performed
606 | the morning of regular business days only. This is a new indicator the agency
607 | began reporting in 2009.
608 | 2011
609 | 12
610 | Service Indicators
611 | M
612 | U
613 | %
614 | 1
615 | 97.00
616 |
617 | 97.00
618 |
619 |
620 | ```
621 |
622 | 我们先用lxml.objectify解析该文件,然后通过getroot得到该XML文件的根节点的引用:
623 | ```python
624 | from lxml import objectify
625 |
626 | path = 'datasets/mta_perf/Performance_MNR.xml'
627 | parsed = objectify.parse(open(path))
628 | root = parsed.getroot()
629 | ```
630 |
631 | root.INDICATOR返回一个用于产生各个XML元素的生成器。对于每条记录,我们可以用标记名(如YTD_ACTUAL)和数据值填充一个字典(排除几个标记):
632 | ```python
633 | data = []
634 |
635 | skip_fields = ['PARENT_SEQ', 'INDICATOR_SEQ',
636 | 'DESIRED_CHANGE', 'DECIMAL_PLACES']
637 |
638 | for elt in root.INDICATOR:
639 | el_data = {}
640 | for child in elt.getchildren():
641 | if child.tag in skip_fields:
642 | continue
643 | el_data[child.tag] = child.pyval
644 | data.append(el_data)
645 | ```
646 |
647 | 最后,将这组字典转换为一个DataFrame:
648 | ```python
649 | In [81]: perf = pd.DataFrame(data)
650 |
651 | In [82]: perf.head()
652 | Out[82]:
653 | Empty DataFrame
654 | Columns: []
655 | Index: []
656 | ```
657 |
658 | XML数据可以比本例复杂得多。每个标记都可以有元数据。看看下面这个HTML的链接标签(它也算是一段有效的XML):
659 | ```python
660 | from io import StringIO
661 | tag = 'Google'
662 | root = objectify.parse(StringIO(tag)).getroot()
663 | ```
664 |
665 | 现在就可以访问标签或链接文本中的任何字段了(如href):
666 | ```python
667 | In [84]: root
668 | Out[84]:
669 |
670 | In [85]: root.get('href')
671 | Out[85]: 'http://www.google.com'
672 |
673 | In [86]: root.text
674 | Out[86]: 'Google'
675 | ```
676 |
677 | # 6.2 二进制数据格式
678 |
679 | 实现数据的高效二进制格式存储最简单的办法之一是使用Python内置的pickle序列化。pandas对象都有一个用于将数据以pickle格式保存到磁盘上的to_pickle方法:
680 | ```python
681 | In [87]: frame = pd.read_csv('examples/ex1.csv')
682 |
683 | In [88]: frame
684 | Out[88]:
685 | a b c d message
686 | 0 1 2 3 4 hello
687 | 1 5 6 7 8 world
688 | 2 9 10 11 12 foo
689 |
690 | In [89]: frame.to_pickle('examples/frame_pickle')
691 | ```
692 |
693 | 你可以通过pickle直接读取被pickle化的数据,或是使用更为方便的pandas.read_pickle:
694 | ```python
695 | In [90]: pd.read_pickle('examples/frame_pickle')
696 | Out[90]:
697 | a b c d message
698 | 0 1 2 3 4 hello
699 | 1 5 6 7 8 world
700 | 2 9 10 11 12 foo
701 | ```
702 |
703 | >注意:pickle仅建议用于短期存储格式。其原因是很难保证该格式永远是稳定的;今天pickle的对象可能无法被后续版本的库unpickle出来。虽然我尽力保证这种事情不会发生在pandas中,但是今后的某个时候说不定还是得“打破”该pickle格式。
704 |
705 | pandas内置支持两个二进制数据格式:HDF5和MessagePack。下一节,我会给出几个HDF5的例子,但我建议你尝试下不同的文件格式,看看它们的速度以及是否适合你的分析工作。pandas或NumPy数据的其它存储格式有:
706 |
707 | - bcolz:一种可压缩的列存储二进制格式,基于Blosc压缩库。
708 | - Feather:我与R语言社区的Hadley Wickham设计的一种跨语言的列存储文件格式。Feather使用了Apache Arrow的列式内存格式。
709 |
710 | ## 使用HDF5格式
711 |
712 | HDF5是一种存储大规模科学数组数据的非常好的文件格式。它可以被作为C标准库,带有许多语言的接口,如Java、Python和MATLAB等。HDF5中的HDF指的是层次型数据格式(hierarchical data format)。每个HDF5文件都含有一个文件系统式的节点结构,它使你能够存储多个数据集并支持元数据。与其他简单格式相比,HDF5支持多种压缩器的即时压缩,还能更高效地存储重复模式数据。对于那些非常大的无法直接放入内存的数据集,HDF5就是不错的选择,因为它可以高效地分块读写。
713 |
714 | 虽然可以用PyTables或h5py库直接访问HDF5文件,pandas提供了更为高级的接口,可以简化存储Series和DataFrame对象。HDFStore类可以像字典一样,处理低级的细节:
715 | ```python
716 | In [92]: frame = pd.DataFrame({'a': np.random.randn(100)})
717 |
718 | In [93]: store = pd.HDFStore('mydata.h5')
719 |
720 | In [94]: store['obj1'] = frame
721 |
722 | In [95]: store['obj1_col'] = frame['a']
723 |
724 | In [96]: store
725 | Out[96]:
726 |
727 | File path: mydata.h5
728 | /obj1 frame (shape->[100,1])
729 |
730 | /obj1_col series (shape->[100])
731 |
732 | /obj2 frame_table (typ->appendable,nrows->100,ncols->1,indexers->
733 | [index])
734 | /obj3 frame_table (typ->appendable,nrows->100,ncols->1,indexers->
735 | [index])
736 | ```
737 |
738 | HDF5文件中的对象可以通过与字典一样的API进行获取:
739 | ```python
740 | In [97]: store['obj1']
741 | Out[97]:
742 | a
743 | 0 -0.204708
744 | 1 0.478943
745 | 2 -0.519439
746 | 3 -0.555730
747 | 4 1.965781
748 | .. ...
749 | 95 0.795253
750 | 96 0.118110
751 | 97 -0.748532
752 | 98 0.584970
753 | 99 0.152677
754 | [100 rows x 1 columns]
755 | ```
756 |
757 | HDFStore支持两种存储模式,'fixed'和'table'。后者通常会更慢,但是支持使用特殊语法进行查询操作:
758 | ```python
759 | In [98]: store.put('obj2', frame, format='table')
760 |
761 | In [99]: store.select('obj2', where=['index >= 10 and index <= 15'])
762 | Out[99]:
763 | a
764 | 10 1.007189
765 | 11 -1.296221
766 | 12 0.274992
767 | 13 0.228913
768 | 14 1.352917
769 | 15 0.886429
770 |
771 | In [100]: store.close()
772 | ```
773 |
774 | put是store['obj2'] = frame方法的显示版本,允许我们设置其它的选项,比如格式。
775 |
776 | pandas.read_hdf函数可以快捷使用这些工具:
777 | ```python
778 | In [101]: frame.to_hdf('mydata.h5', 'obj3', format='table')
779 |
780 | In [102]: pd.read_hdf('mydata.h5', 'obj3', where=['index < 5'])
781 | Out[102]:
782 | a
783 | 0 -0.204708
784 | 1 0.478943
785 | 2 -0.519439
786 | 3 -0.555730
787 | 4 1.965781
788 | ```
789 |
790 | >笔记:如果你要处理的数据位于远程服务器,比如Amazon S3或HDFS,使用专门为分布式存储(比如Apache Parquet)的二进制格式也许更加合适。Python的Parquet和其它存储格式还在不断的发展之中,所以这本书中没有涉及。
791 |
792 | 如果需要本地处理海量数据,我建议你好好研究一下PyTables和h5py,看看它们能满足你的哪些需求。。由于许多数据分析问题都是IO密集型(而不是CPU密集型),利用HDF5这样的工具能显著提升应用程序的效率。
793 |
794 | >注意:HDF5不是数据库。它最适合用作“一次写多次读”的数据集。虽然数据可以在任何时候被添加到文件中,但如果同时发生多个写操作,文件就可能会被破坏。
795 |
796 | ## 读取Microsoft Excel文件
797 |
798 | pandas的ExcelFile类或pandas.read_excel函数支持读取存储在Excel 2003(或更高版本)中的表格型数据。这两个工具分别使用扩展包xlrd和openpyxl读取XLS和XLSX文件。你可以用pip或conda安装它们。
799 |
800 | 要使用ExcelFile,通过传递xls或xlsx路径创建一个实例:
801 | ```python
802 | In [104]: xlsx = pd.ExcelFile('examples/ex1.xlsx')
803 | ```
804 |
805 | 存储在表单中的数据可以read_excel读取到DataFrame(原书这里写的是用parse解析,但代码中用的是read_excel,是个笔误:只换了代码,没有改文字):
806 | ```python
807 | In [105]: pd.read_excel(xlsx, 'Sheet1')
808 | Out[105]:
809 | a b c d message
810 | 0 1 2 3 4 hello
811 | 1 5 6 7 8 world
812 | 2 9 10 11 12 foo
813 | ```
814 |
815 | 如果要读取一个文件中的多个表单,创建ExcelFile会更快,但你也可以将文件名传递到pandas.read_excel:
816 | ```python
817 | In [106]: frame = pd.read_excel('examples/ex1.xlsx', 'Sheet1')
818 |
819 | In [107]: frame
820 | Out[107]:
821 | a b c d message
822 | 0 1 2 3 4 hello
823 | 1 5 6 7 8 world
824 | 2 9 10 11 12 foo
825 | ```
826 |
827 | 如果要将pandas数据写入为Excel格式,你必须首先创建一个ExcelWriter,然后使用pandas对象的to_excel方法将数据写入到其中:
828 | ```python
829 | In [108]: writer = pd.ExcelWriter('examples/ex2.xlsx')
830 |
831 | In [109]: frame.to_excel(writer, 'Sheet1')
832 |
833 | In [110]: writer.save()
834 | ```
835 |
836 | 你还可以不使用ExcelWriter,而是传递文件的路径到to_excel:
837 | ```python
838 | In [111]: frame.to_excel('examples/ex2.xlsx')
839 | ```
840 |
841 | # 6.3 Web APIs交互
842 | 许多网站都有一些通过JSON或其他格式提供数据的公共API。通过Python访问这些API的办法有不少。一个简单易用的办法(推荐)是requests包(http://docs.python-requests.org)。
843 |
844 | 为了搜索最新的30个GitHub上的pandas主题,我们可以发一个HTTP GET请求,使用requests扩展库:
845 | ```python
846 | In [113]: import requests
847 |
848 | In [114]: url = 'https://api.github.com/repos/pandas-dev/pandas/issues'
849 |
850 | In [115]: resp = requests.get(url)
851 |
852 | In [116]: resp
853 | Out[116]:
854 | ```
855 |
856 | 响应对象的json方法会返回一个包含被解析过的JSON字典,加载到一个Python对象中:
857 | ```python
858 | In [117]: data = resp.json()
859 |
860 | In [118]: data[0]['title']
861 | Out[118]: 'Period does not round down for frequencies less that 1 hour'
862 | ```
863 |
864 | data中的每个元素都是一个包含所有GitHub主题页数据(不包含评论)的字典。我们可以直接传递数据到DataFrame,并提取感兴趣的字段:
865 | ```python
866 | In [119]: issues = pd.DataFrame(data, columns=['number', 'title',
867 | .....: 'labels', 'state'])
868 |
869 | In [120]: issues
870 | Out[120]:
871 | number title \
872 | 0 17666 Period does not round down for frequencies les...
873 | 1 17665 DOC: improve docstring of function where
874 | 2 17664 COMPAT: skip 32-bit test on int repr
875 | 3 17662 implement Delegator class
876 | 4 17654 BUG: Fix series rename called with str alterin...
877 | .. ... ...
878 | 25 17603 BUG: Correctly localize naive datetime strings...
879 | 26 17599 core.dtypes.generic --> cython
880 | 27 17596 Merge cdate_range functionality into bdate_range
881 | 28 17587 Time Grouper bug fix when applied for list gro...
882 | 29 17583 BUG: fix tz-aware DatetimeIndex + TimedeltaInd...
883 | labels state
884 | 0 [] open
885 | 1 [{'id': 134699, 'url': 'https://api.github.com... open
886 | 2 [{'id': 563047854, 'url': 'https://api.github.... open
887 | 3 [] open
888 | 4 [{'id': 76811, 'url': 'https://api.github.com/... open
889 | .. ... ...
890 | 25 [{'id': 76811, 'url': 'https://api.github.com/... open
891 | 26 [{'id': 49094459, 'url': 'https://api.github.c... open
892 | 27 [{'id': 35818298, 'url': 'https://api.github.c... open
893 | 28 [{'id': 233160, 'url': 'https://api.github.com... open
894 | 29 [{'id': 76811, 'url': 'https://api.github.com/... open
895 | [30 rows x 4 columns]
896 | ```
897 |
898 | 花费一些精力,你就可以创建一些更高级的常见的Web API的接口,返回DataFrame对象,方便进行分析。
899 |
900 | # 6.4 数据库交互
901 |
902 | 在商业场景下,大多数数据可能不是存储在文本或Excel文件中。基于SQL的关系型数据库(如SQL Server、PostgreSQL和MySQL等)使用非常广泛,其它一些数据库也很流行。数据库的选择通常取决于性能、数据完整性以及应用程序的伸缩性需求。
903 |
904 | 将数据从SQL加载到DataFrame的过程很简单,此外pandas还有一些能够简化该过程的函数。例如,我将使用SQLite数据库(通过Python内置的sqlite3驱动器):
905 | ```python
906 | In [121]: import sqlite3
907 |
908 | In [122]: query = """
909 | .....: CREATE TABLE test
910 | .....: (a VARCHAR(20), b VARCHAR(20),
911 | .....: c REAL, d INTEGER
912 | .....: );"""
913 |
914 | In [123]: con = sqlite3.connect('mydata.sqlite')
915 |
916 | In [124]: con.execute(query)
917 | Out[124]:
918 |
919 | In [125]: con.commit()
920 | ```
921 |
922 | 然后插入几行数据:
923 | ```python
924 | In [126]: data = [('Atlanta', 'Georgia', 1.25, 6),
925 | .....: ('Tallahassee', 'Florida', 2.6, 3),
926 | .....: ('Sacramento', 'California', 1.7, 5)]
927 |
928 | In [127]: stmt = "INSERT INTO test VALUES(?, ?, ?, ?)"
929 |
930 | In [128]: con.executemany(stmt, data)
931 | Out[128]:
932 | ```
933 |
934 | 从表中选取数据时,大部分Python SQL驱动器(PyODBC、psycopg2、MySQLdb、pymssql等)都会返回一个元组列表:
935 | ```python
936 | In [130]: cursor = con.execute('select * from test')
937 |
938 | In [131]: rows = cursor.fetchall()
939 |
940 | In [132]: rows
941 | Out[132]:
942 | [('Atlanta', 'Georgia', 1.25, 6),
943 | ('Tallahassee', 'Florida', 2.6, 3),
944 | ('Sacramento', 'California', 1.7, 5)]
945 | ```
946 |
947 | 你可以将这个元组列表传给DataFrame构造器,但还需要列名(位于光标的description属性中):
948 | ```python
949 | In [133]: cursor.description
950 | Out[133]:
951 | (('a', None, None, None, None, None, None),
952 | ('b', None, None, None, None, None, None),
953 | ('c', None, None, None, None, None, None),
954 | ('d', None, None, None, None, None, None))
955 |
956 | In [134]: pd.DataFrame(rows, columns=[x[0] for x in cursor.description])
957 | Out[134]:
958 | a b c d
959 | 0 Atlanta Georgia 1.25 6
960 | 1 Tallahassee Florida 2.60 3
961 | 2 Sacramento California 1.70 5
962 | ```
963 |
964 | 这种数据规整操作相当多,你肯定不想每查一次数据库就重写一次。[SQLAlchemy项目](http://www.sqlalchemy.org/)是一个流行的Python SQL工具,它抽象出了SQL数据库中的许多常见差异。pandas有一个read_sql函数,可以让你轻松的从SQLAlchemy连接读取数据。这里,我们用SQLAlchemy连接SQLite数据库,并从之前创建的表读取数据:
965 | ```python
966 | In [135]: import sqlalchemy as sqla
967 |
968 | In [136]: db = sqla.create_engine('sqlite:///mydata.sqlite')
969 |
970 | In [137]: pd.read_sql('select * from test', db)
971 | Out[137]:
972 | a b c d
973 | 0 Atlanta Georgia 1.25 6
974 | 1 Tallahassee Florida 2.60 3
975 | 2 Sacramento California 1.70 5
976 | ```
977 |
978 | # 6.5 总结
979 |
980 | 访问数据通常是数据分析的第一步。在本章中,我们已经学了一些有用的工具。在接下来的章节中,我们将深入研究数据规整、数据可视化、时间序列分析和其它主题。
981 |
--------------------------------------------------------------------------------
/第07章 数据清洗和准备.md:
--------------------------------------------------------------------------------
1 | 在数据分析和建模的过程中,相当多的时间要用在数据准备上:加载、清理、转换以及重塑。这些工作会占到分析师时间的80%或更多。有时,存储在文件和数据库中的数据的格式不适合某个特定的任务。许多研究者都选择使用通用编程语言(如Python、Perl、R或Java)或UNIX文本处理工具(如sed或awk)对数据格式进行专门处理。幸运的是,pandas和内置的Python标准库提供了一组高级的、灵活的、快速的工具,可以让你轻松地将数据规整为想要的格式。
2 |
3 | 如果你发现了一种本书或pandas库中没有的数据操作方式,请在邮件列表或GitHub网站上提出。实际上,pandas的许多设计和实现都是由真实应用的需求所驱动的。
4 |
5 | 在本章中,我会讨论处理缺失数据、重复数据、字符串操作和其它分析数据转换的工具。下一章,我会关注于用多种方法合并、重塑数据集。
6 |
7 | # 7.1 处理缺失数据
8 | 在许多数据分析工作中,缺失数据是经常发生的。pandas的目标之一就是尽量轻松地处理缺失数据。例如,pandas对象的所有描述性统计默认都不包括缺失数据。
9 |
10 | 缺失数据在pandas中呈现的方式有些不完美,但对于大多数用户可以保证功能正常。对于数值数据,pandas使用浮点值NaN(Not a Number)表示缺失数据。我们称其为哨兵值,可以方便的检测出来:
11 | ```python
12 | In [10]: string_data = pd.Series(['aardvark', 'artichoke', np.nan, 'avocado'])
13 |
14 | In [11]: string_data
15 | Out[11]:
16 | 0 aardvark
17 | 1 artichoke
18 | 2 NaN
19 | 3 avocado
20 | dtype: object
21 |
22 | In [12]: string_data.isnull()
23 | Out[12]:
24 | 0 False
25 | 1 False
26 | 2 True
27 | 3 False
28 | dtype: bool
29 | ```
30 |
31 | 在pandas中,我们采用了R语言中的惯用法,即将缺失值表示为NA,它表示不可用not available。在统计应用中,NA数据可能是不存在的数据或者虽然存在,但是没有观察到(例如,数据采集中发生了问题)。当进行数据清洗以进行分析时,最好直接对缺失数据进行分析,以判断数据采集的问题或缺失数据可能导致的偏差。
32 |
33 | Python内置的None值在对象数组中也可以作为NA:
34 | ```python
35 | In [13]: string_data[0] = None
36 |
37 | In [14]: string_data.isnull()
38 | Out[14]:
39 | 0 True
40 | 1 False
41 | 2 True
42 | 3 False
43 | dtype: bool
44 | ```
45 |
46 | pandas项目中还在不断优化内部细节以更好处理缺失数据,像用户API功能,例如pandas.isnull,去除了许多恼人的细节。表7-1列出了一些关于缺失数据处理的函数。
47 |
48 | 
49 |
50 | ## 滤除缺失数据
51 | 过滤掉缺失数据的办法有很多种。你可以通过pandas.isnull或布尔索引的手工方法,但dropna可能会更实用一些。对于一个Series,dropna返回一个仅含非空数据和索引值的Series:
52 | ```python
53 | In [15]: from numpy import nan as NA
54 |
55 | In [16]: data = pd.Series([1, NA, 3.5, NA, 7])
56 |
57 | In [17]: data.dropna()
58 | Out[17]:
59 | 0 1.0
60 | 2 3.5
61 | 4 7.0
62 | dtype: float64
63 | ```
64 |
65 | 这等价于:
66 | ```python
67 | In [18]: data[data.notnull()]
68 | Out[18]:
69 | 0 1.0
70 | 2 3.5
71 | 4 7.0
72 | dtype: float64
73 | ```
74 |
75 | 而对于DataFrame对象,事情就有点复杂了。你可能希望丢弃全NA或含有NA的行或列。dropna默认丢弃任何含有缺失值的行:
76 | ```python
77 | In [19]: data = pd.DataFrame([[1., 6.5, 3.], [1., NA, NA],
78 | ....: [NA, NA, NA], [NA, 6.5, 3.]])
79 |
80 | In [20]: cleaned = data.dropna()
81 |
82 | In [21]: data
83 | Out[21]:
84 | 0 1 2
85 | 0 1.0 6.5 3.0
86 | 1 1.0 NaN NaN
87 | 2 NaN NaN NaN
88 | 3 NaN 6.5 3.0
89 |
90 | In [22]: cleaned
91 | Out[22]:
92 | 0 1 2
93 | 0 1.0 6.5 3.0
94 | ```
95 |
96 | 传入how='all'将只丢弃全为NA的那些行:
97 | ```python
98 | In [23]: data.dropna(how='all')
99 | Out[23]:
100 | 0 1 2
101 | 0 1.0 6.5 3.0
102 | 1 1.0 NaN NaN
103 | 3 NaN 6.5 3.0
104 | ```
105 |
106 | 用这种方式丢弃列,只需传入axis=1即可:
107 | ```python
108 | In [24]: data[4] = NA
109 |
110 | In [25]: data
111 | Out[25]:
112 | 0 1 2 4
113 | 0 1.0 6.5 3.0 NaN
114 | 1 1.0 NaN NaN NaN
115 | 2 NaN NaN NaN NaN
116 | 3 NaN 6.5 3.0 NaN
117 |
118 | In [26]: data.dropna(axis=1, how='all')
119 | Out[26]:
120 | 0 1 2
121 | 0 1.0 6.5 3.0
122 | 1 1.0 NaN NaN
123 | 2 NaN NaN NaN
124 | 3 NaN 6.5 3.0
125 | ```
126 |
127 | 另一个滤除DataFrame行的问题涉及时间序列数据。假设你只想留下一部分观测数据,可以用thresh参数实现此目的:
128 | ```python
129 | In [27]: df = pd.DataFrame(np.random.randn(7, 3))
130 |
131 | In [28]: df.iloc[:4, 1] = NA
132 |
133 | In [29]: df.iloc[:2, 2] = NA
134 |
135 | In [30]: df
136 | Out[30]:
137 | 0 1 2
138 | 0 -0.204708 NaN NaN
139 | 1 -0.555730 NaN NaN
140 | 2 0.092908 NaN 0.769023
141 | 3 1.246435 NaN -1.296221
142 | 4 0.274992 0.228913 1.352917
143 | 5 0.886429 -2.001637 -0.371843
144 | 6 1.669025 -0.438570 -0.539741
145 |
146 | In [31]: df.dropna()
147 | Out[31]:
148 | 0 1 2
149 | 4 0.274992 0.228913 1.352917
150 | 5 0.886429 -2.001637 -0.371843
151 | 6 1.669025 -0.438570 -0.539741
152 |
153 | In [32]: df.dropna(thresh=2)
154 | Out[32]:
155 | 0 1 2
156 | 2 0.092908 NaN 0.769023
157 | 3 1.246435 NaN -1.296221
158 | 4 0.274992 0.228913 1.352917
159 | 5 0.886429 -2.001637 -0.371843
160 | 6 1.669025 -0.438570 -0.539741
161 | ```
162 |
163 | ## 填充缺失数据
164 | 你可能不想滤除缺失数据(有可能会丢弃跟它有关的其他数据),而是希望通过其他方式填补那些“空洞”。对于大多数情况而言,fillna方法是最主要的函数。通过一个常数调用fillna就会将缺失值替换为那个常数值:
165 | ```python
166 | In [33]: df.fillna(0)
167 | Out[33]:
168 | 0 1 2
169 | 0 -0.204708 0.000000 0.000000
170 | 1 -0.555730 0.000000 0.000000
171 | 2 0.092908 0.000000 0.769023
172 | 3 1.246435 0.000000 -1.296221
173 | 4 0.274992 0.228913 1.352917
174 | 5 0.886429 -2.001637 -0.371843
175 | 6 1.669025 -0.438570 -0.539741
176 | ```
177 |
178 | 若是通过一个字典调用fillna,就可以实现对不同的列填充不同的值:
179 | ```python
180 | In [34]: df.fillna({1: 0.5, 2: 0})
181 | Out[34]:
182 | 0 1 2
183 | 0 -0.204708 0.500000 0.000000
184 | 1 -0.555730 0.500000 0.000000
185 | 2 0.092908 0.500000 0.769023
186 | 3 1.246435 0.500000 -1.296221
187 | 4 0.274992 0.228913 1.352917
188 | 5 0.886429 -2.001637 -0.371843
189 | 6 1.669025 -0.438570 -0.539741
190 | ```
191 |
192 | fillna默认会返回新对象,但也可以对现有对象进行就地修改:
193 | ```python
194 | In [35]: _ = df.fillna(0, inplace=True)
195 |
196 | In [36]: df
197 | Out[36]:
198 | 0 1 2
199 | 0 -0.204708 0.000000 0.000000
200 | 1 -0.555730 0.000000 0.000000
201 | 2 0.092908 0.000000 0.769023
202 | 3 1.246435 0.000000 -1.296221
203 | 4 0.274992 0.228913 1.352917
204 | 5 0.886429 -2.001637 -0.371843
205 | 6 1.669025 -0.438570 -0.539741
206 | ```
207 |
208 | 对reindexing有效的那些插值方法也可用于fillna:
209 | ```python
210 | In [37]: df = pd.DataFrame(np.random.randn(6, 3))
211 |
212 | In [38]: df.iloc[2:, 1] = NA
213 |
214 | In [39]: df.iloc[4:, 2] = NA
215 |
216 | In [40]: df
217 | Out[40]:
218 | 0 1 2
219 | 0 0.476985 3.248944 -1.021228
220 | 1 -0.577087 0.124121 0.302614
221 | 2 0.523772 NaN 1.343810
222 | 3 -0.713544 NaN -2.370232
223 | 4 -1.860761 NaN NaN
224 | 5 -1.265934 NaN NaN
225 |
226 | In [41]: df.fillna(method='ffill')
227 | Out[41]:
228 | 0 1 2
229 | 0 0.476985 3.248944 -1.021228
230 | 1 -0.577087 0.124121 0.302614
231 | 2 0.523772 0.124121 1.343810
232 | 3 -0.713544 0.124121 -2.370232
233 | 4 -1.860761 0.124121 -2.370232
234 | 5 -1.265934 0.124121 -2.370232
235 |
236 | In [42]: df.fillna(method='ffill', limit=2)
237 | Out[42]:
238 | 0 1 2
239 | 0 0.476985 3.248944 -1.021228
240 | 1 -0.577087 0.124121 0.302614
241 | 2 0.523772 0.124121 1.343810
242 | 3 -0.713544 0.124121 -2.370232
243 | 4 -1.860761 NaN -2.370232
244 | 5 -1.265934 NaN -2.370232
245 | ```
246 |
247 | 只要有些创新,你就可以利用fillna实现许多别的功能。比如说,你可以传入Series的平均值或中位数:
248 | ```python
249 | In [43]: data = pd.Series([1., NA, 3.5, NA, 7])
250 |
251 | In [44]: data.fillna(data.mean())
252 | Out[44]:
253 | 0 1.000000
254 | 1 3.833333
255 | 2 3.500000
256 | 3 3.833333
257 | 4 7.000000
258 | dtype: float64
259 | ```
260 | 表7-2列出了fillna的参考。
261 |
262 | 
263 |
264 | 
265 |
266 | # 7.2 数据转换
267 | 本章到目前为止介绍的都是数据的重排。另一类重要操作则是过滤、清理以及其他的转换工作。
268 |
269 | ## 移除重复数据
270 |
271 | DataFrame中出现重复行有多种原因。下面就是一个例子:
272 | ```python
273 | In [45]: data = pd.DataFrame({'k1': ['one', 'two'] * 3 + ['two'],
274 | ....: 'k2': [1, 1, 2, 3, 3, 4, 4]})
275 |
276 | In [46]: data
277 | Out[46]:
278 | k1 k2
279 | 0 one 1
280 | 1 two 1
281 | 2 one 2
282 | 3 two 3
283 | 4 one 3
284 | 5 two 4
285 | 6 two 4
286 | ```
287 |
288 | DataFrame的duplicated方法返回一个布尔型Series,表示各行是否是重复行(前面出现过的行):
289 | ```python
290 | In [47]: data.duplicated()
291 | Out[47]:
292 | 0 False
293 | 1 False
294 | 2 False
295 | 3 False
296 | 4 False
297 | 5 False
298 | 6 True
299 | dtype: bool
300 | ```
301 |
302 | 还有一个与此相关的drop_duplicates方法,它会返回一个DataFrame,重复的数组会标为False:
303 | ```python
304 | In [48]: data.drop_duplicates()
305 | Out[48]:
306 | k1 k2
307 | 0 one 1
308 | 1 two 1
309 | 2 one 2
310 | 3 two 3
311 | 4 one 3
312 | 5 two 4
313 | ```
314 |
315 | 这两个方法默认会判断全部列,你也可以指定部分列进行重复项判断。假设我们还有一列值,且只希望根据k1列过滤重复项:
316 | ```python
317 | In [49]: data['v1'] = range(7)
318 |
319 | In [50]: data.drop_duplicates(['k1'])
320 | Out[50]:
321 | k1 k2 v1
322 | 0 one 1 0
323 | 1 two 1 1
324 | ```
325 |
326 | duplicated和drop_duplicates默认保留的是第一个出现的值组合。传入keep='last'则保留最后一个:
327 | ```python
328 | In [51]: data.drop_duplicates(['k1', 'k2'], keep='last')
329 | Out[51]:
330 | k1 k2 v1
331 | 0 one 1 0
332 | 1 two 1 1
333 | 2 one 2 2
334 | 3 two 3 3
335 | 4 one 3 4
336 | 6 two 4 6
337 | ```
338 |
339 | ## 利用函数或映射进行数据转换
340 | 对于许多数据集,你可能希望根据数组、Series或DataFrame列中的值来实现转换工作。我们来看看下面这组有关肉类的数据:
341 | ```python
342 | In [52]: data = pd.DataFrame({'food': ['bacon', 'pulled pork', 'bacon',
343 | ....: 'Pastrami', 'corned beef', 'Bacon',
344 | ....: 'pastrami', 'honey ham', 'nova lox'],
345 | ....: 'ounces': [4, 3, 12, 6, 7.5, 8, 3, 5, 6]})
346 |
347 | In [53]: data
348 | Out[53]:
349 | food ounces
350 | 0 bacon 4.0
351 | 1 pulled pork 3.0
352 | 2 bacon 12.0
353 | 3 Pastrami 6.0
354 | 4 corned beef 7.5
355 | 5 Bacon 8.0
356 | 6 pastrami 3.0
357 | 7 honey ham 5.0
358 | 8 nova lox 6.0
359 | ```
360 |
361 | 假设你想要添加一列表示该肉类食物来源的动物类型。我们先编写一个不同肉类到动物的映射:
362 | ```python
363 | meat_to_animal = {
364 | 'bacon': 'pig',
365 | 'pulled pork': 'pig',
366 | 'pastrami': 'cow',
367 | 'corned beef': 'cow',
368 | 'honey ham': 'pig',
369 | 'nova lox': 'salmon'
370 | }
371 | ```
372 |
373 | Series的map方法可以接受一个函数或含有映射关系的字典型对象,但是这里有一个小问题,即有些肉类的首字母大写了,而另一些则没有。因此,我们还需要使用Series的str.lower方法,将各个值转换为小写:
374 | ```python
375 | In [55]: lowercased = data['food'].str.lower()
376 |
377 | In [56]: lowercased
378 | Out[56]:
379 | 0 bacon
380 | 1 pulled pork
381 | 2 bacon
382 | 3 pastrami
383 | 4 corned beef
384 | 5 bacon
385 | 6 pastrami
386 | 7 honey ham
387 | 8 nova lox
388 | Name: food, dtype: object
389 |
390 | In [57]: data['animal'] = lowercased.map(meat_to_animal)
391 |
392 | In [58]: data
393 | Out[58]:
394 | food ounces animal
395 | 0 bacon 4.0 pig
396 | 1 pulled pork 3.0 pig
397 | 2 bacon 12.0 pig
398 | 3 Pastrami 6.0 cow
399 | 4 corned beef 7.5 cow
400 | 5 Bacon 8.0 pig
401 | 6 pastrami 3.0 cow
402 | 7 honey ham 5.0 pig
403 | 8 nova lox 6.0 salmon
404 | ```
405 |
406 | 我们也可以传入一个能够完成全部这些工作的函数:
407 | ```python
408 | In [59]: data['food'].map(lambda x: meat_to_animal[x.lower()])
409 | Out[59]:
410 | 0 pig
411 | 1 pig
412 | 2 pig
413 | 3 cow
414 | 4 cow
415 | 5 pig
416 | 6 cow
417 | 7 pig
418 | 8 salmon
419 | Name: food, dtype: object
420 | ```
421 |
422 | 使用map是一种实现元素级转换以及其他数据清理工作的便捷方式。
423 |
424 | ## 替换值
425 | 利用fillna方法填充缺失数据可以看做值替换的一种特殊情况。前面已经看到,map可用于修改对象的数据子集,而replace则提供了一种实现该功能的更简单、更灵活的方式。我们来看看下面这个Series:
426 | ```python
427 | In [60]: data = pd.Series([1., -999., 2., -999., -1000., 3.])
428 |
429 | In [61]: data
430 | Out[61]:
431 | 0 1.0
432 | 1 -999.0
433 | 2 2.0
434 | 3 -999.0
435 | 4 -1000.0
436 | 5 3.0
437 | ```
438 |
439 | -999这个值可能是一个表示缺失数据的标记值。要将其替换为pandas能够理解的NA值,我们可以利用replace来产生一个新的Series(除非传入inplace=True):
440 | ```python
441 | In [62]: data.replace(-999, np.nan)
442 | Out[62]:
443 | 0 1.0
444 | 1 NaN
445 | 2 2.0
446 | 3 NaN
447 | 4 -1000.0
448 | 5 3.0
449 | dtype: float64
450 | ```
451 |
452 | 如果你希望一次性替换多个值,可以传入一个由待替换值组成的列表以及一个替换值::
453 | ```python
454 | In [63]: data.replace([-999, -1000], np.nan)
455 | Out[63]:
456 | 0 1.0
457 | 1 NaN
458 | 2 2.0
459 | 3 NaN
460 | 4 NaN
461 | 5 3.0
462 | dtype: float64
463 | ```
464 |
465 | 要让每个值有不同的替换值,可以传递一个替换列表:
466 | ```python
467 | In [64]: data.replace([-999, -1000], [np.nan, 0])
468 | Out[64]:
469 | 0 1.0
470 | 1 NaN
471 | 2 2.0
472 | 3 NaN
473 | 4 0.0
474 | 5 3.0
475 | dtype: float64
476 | ```
477 |
478 | 传入的参数也可以是字典:
479 | ```python
480 | In [65]: data.replace({-999: np.nan, -1000: 0})
481 | Out[65]:
482 | 0 1.0
483 | 1 NaN
484 | 2 2.0
485 | 3 NaN
486 | 4 0.0
487 | 5 3.0
488 | dtype: float64
489 | ```
490 |
491 | >笔记:data.replace方法与data.str.replace不同,后者做的是字符串的元素级替换。我们会在后面学习Series的字符串方法。
492 |
493 | ## 重命名轴索引
494 | 跟Series中的值一样,轴标签也可以通过函数或映射进行转换,从而得到一个新的不同标签的对象。轴还可以被就地修改,而无需新建一个数据结构。接下来看看下面这个简单的例子:
495 | ```python
496 | In [66]: data = pd.DataFrame(np.arange(12).reshape((3, 4)),
497 | ....: index=['Ohio', 'Colorado', 'New York'],
498 | ....: columns=['one', 'two', 'three', 'four'])
499 | ```
500 |
501 | 跟Series一样,轴索引也有一个map方法:
502 | ```python
503 | In [67]: transform = lambda x: x[:4].upper()
504 |
505 | In [68]: data.index.map(transform)
506 | Out[68]: Index(['OHIO', 'COLO', 'NEW '], dtype='object')
507 | ```
508 |
509 | 你可以将其赋值给index,这样就可以对DataFrame进行就地修改:
510 | ```python
511 | In [69]: data.index = data.index.map(transform)
512 |
513 | In [70]: data
514 | Out[70]:
515 | one two three four
516 | OHIO 0 1 2 3
517 | COLO 4 5 6 7
518 | NEW 8 9 10 11
519 | ```
520 |
521 | 如果想要创建数据集的转换版(而不是修改原始数据),比较实用的方法是rename:
522 | ```python
523 | In [71]: data.rename(index=str.title, columns=str.upper)
524 | Out[71]:
525 | ONE TWO THREE FOUR
526 | Ohio 0 1 2 3
527 | Colo 4 5 6 7
528 | New 8 9 10 11
529 | ```
530 |
531 | 特别说明一下,rename可以结合字典型对象实现对部分轴标签的更新:
532 | ```python
533 | In [72]: data.rename(index={'OHIO': 'INDIANA'},
534 | ....: columns={'three': 'peekaboo'})
535 | Out[72]:
536 | one two peekaboo four
537 | INDIANA 0 1 2 3
538 | COLO 4 5 6 7
539 | NEW 8 9 10 11
540 | ```
541 |
542 | rename可以实现复制DataFrame并对其索引和列标签进行赋值。如果希望就地修改某个数据集,传入inplace=True即可:
543 | ```python
544 | In [73]: data.rename(index={'OHIO': 'INDIANA'}, inplace=True)
545 |
546 | In [74]: data
547 | Out[74]:
548 | one two three four
549 | INDIANA 0 1 2 3
550 | COLO 4 5 6 7
551 | NEW 8 9 10 11
552 | ```
553 |
554 | ## 离散化和面元划分
555 | 为了便于分析,连续数据常常被离散化或拆分为“面元”(bin)。假设有一组人员数据,而你希望将它们划分为不同的年龄组:
556 | ```python
557 | In [75]: ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]
558 | ```
559 |
560 | 接下来将这些数据划分为“18到25”、“26到35”、“35到60”以及“60以上”几个面元。要实现该功能,你需要使用pandas的cut函数:
561 | ```python
562 | In [76]: bins = [18, 25, 35, 60, 100]
563 |
564 | In [77]: cats = pd.cut(ages, bins)
565 |
566 | In [78]: cats
567 | Out[78]:
568 | [(18, 25], (18, 25], (18, 25], (25, 35], (18, 25], ..., (25, 35], (60, 100], (35,60], (35, 60], (25, 35]]
569 | Length: 12
570 | Categories (4, interval[int64]): [(18, 25] < (25, 35] < (35, 60] < (60, 100]]
571 | ```
572 |
573 | pandas返回的是一个特殊的Categorical对象。结果展示了pandas.cut划分的面元。你可以将其看做一组表示面元名称的字符串。它的底层含有一个表示不同分类名称的类型数组,以及一个codes属性中的年龄数据的标签:
574 | ```python
575 | In [79]: cats.codes
576 | Out[79]: array([0, 0, 0, 1, 0, 0, 2, 1, 3, 2, 2, 1], dtype=int8)
577 |
578 | In [80]: cats.categories
579 | Out[80]:
580 | IntervalIndex([(18, 25], (25, 35], (35, 60], (60, 100]]
581 | closed='right',
582 | dtype='interval[int64]')
583 |
584 | In [81]: pd.value_counts(cats)
585 | Out[81]:
586 | (18, 25] 5
587 | (35, 60] 3
588 | (25, 35] 3
589 | (60, 100] 1
590 | dtype: int64
591 | ```
592 |
593 | pd.value_counts(cats)是pandas.cut结果的面元计数。
594 |
595 | 跟“区间”的数学符号一样,圆括号表示开端,而方括号则表示闭端(包括)。哪边是闭端可以通过right=False进行修改:
596 | ```python
597 | In [82]: pd.cut(ages, [18, 26, 36, 61, 100], right=False)
598 | Out[82]:
599 | [[18, 26), [18, 26), [18, 26), [26, 36), [18, 26), ..., [26, 36), [61, 100), [36,
600 | 61), [36, 61), [26, 36)]
601 | Length: 12
602 | Categories (4, interval[int64]): [[18, 26) < [26, 36) < [36, 61) < [61, 100)]
603 | ```
604 |
605 | 你可 以通过传递一个列表或数组到labels,设置自己的面元名称:
606 | ```python
607 | In [83]: group_names = ['Youth', 'YoungAdult', 'MiddleAged', 'Senior']
608 |
609 | In [84]: pd.cut(ages, bins, labels=group_names)
610 | Out[84]:
611 | [Youth, Youth, Youth, YoungAdult, Youth, ..., YoungAdult, Senior, MiddleAged, Mid
612 | dleAged, YoungAdult]
613 | Length: 12
614 | Categories (4, object): [Youth < YoungAdult < MiddleAged < Senior]
615 | ```
616 |
617 | 如果向cut传入的是面元的数量而不是确切的面元边界,则它会根据数据的最小值和最大值计算等长面元。下面这个例子中,我们将一些均匀分布的数据分成四组:
618 | ```python
619 | In [85]: data = np.random.rand(20)
620 |
621 | In [86]: pd.cut(data, 4, precision=2)
622 | Out[86]:
623 | [(0.34, 0.55], (0.34, 0.55], (0.76, 0.97], (0.76, 0.97], (0.34, 0.55], ..., (0.34
624 | , 0.55], (0.34, 0.55], (0.55, 0.76], (0.34, 0.55], (0.12, 0.34]]
625 | Length: 20
626 | Categories (4, interval[float64]): [(0.12, 0.34] < (0.34, 0.55] < (0.55, 0.76] <
627 | (0.76, 0.97]]
628 | ```
629 |
630 | 选项precision=2,限定小数只有两位。
631 |
632 | qcut是一个非常类似于cut的函数,它可以根据样本分位数对数据进行面元划分。根据数据的分布情况,cut可能无法使各个面元中含有相同数量的数据点。而qcut由于使用的是样本分位数,因此可以得到大小基本相等的面元:
633 | ```python
634 | In [87]: data = np.random.randn(1000) # Normally distributed
635 |
636 | In [88]: cats = pd.qcut(data, 4) # Cut into quartiles
637 |
638 | In [89]: cats
639 | Out[89]:
640 | [(-0.0265, 0.62], (0.62, 3.928], (-0.68, -0.0265], (0.62, 3.928], (-0.0265, 0.62]
641 | , ..., (-0.68, -0.0265], (-0.68, -0.0265], (-2.95, -0.68], (0.62, 3.928], (-0.68,
642 | -0.0265]]
643 | Length: 1000
644 | Categories (4, interval[float64]): [(-2.95, -0.68] < (-0.68, -0.0265] < (-0.0265,
645 | 0.62] <
646 | (0.62, 3.928]]
647 |
648 | In [90]: pd.value_counts(cats)
649 | Out[90]:
650 | (0.62, 3.928] 250
651 | (-0.0265, 0.62] 250
652 | (-0.68, -0.0265] 250
653 | (-2.95, -0.68] 250
654 | dtype: int64
655 | ```
656 |
657 | 与cut类似,你也可以传递自定义的分位数(0到1之间的数值,包含端点):
658 | ```python
659 | In [91]: pd.qcut(data, [0, 0.1, 0.5, 0.9, 1.])
660 | Out[91]:
661 | [(-0.0265, 1.286], (-0.0265, 1.286], (-1.187, -0.0265], (-0.0265, 1.286], (-0.026
662 | 5, 1.286], ..., (-1.187, -0.0265], (-1.187, -0.0265], (-2.95, -1.187], (-0.0265,
663 | 1.286], (-1.187, -0.0265]]
664 | Length: 1000
665 | Categories (4, interval[float64]): [(-2.95, -1.187] < (-1.187, -0.0265] < (-0.026
666 | 5, 1.286] <
667 | (1.286, 3.928]]
668 | ```
669 |
670 | 本章稍后在讲解聚合和分组运算时会再次用到cut和qcut,因为这两个离散化函数对分位和分组分析非常重要。
671 |
672 | ## 检测和过滤异常值
673 | 过滤或变换异常值(outlier)在很大程度上就是运用数组运算。来看一个含有正态分布数据的DataFrame:
674 | ```python
675 | In [92]: data = pd.DataFrame(np.random.randn(1000, 4))
676 |
677 | In [93]: data.describe()
678 | Out[93]:
679 | 0 1 2 3
680 | count 1000.000000 1000.000000 1000.000000 1000.000000
681 | mean 0.049091 0.026112 -0.002544 -0.051827
682 | std 0.996947 1.007458 0.995232 0.998311
683 | min -3.645860 -3.184377 -3.745356 -3.428254
684 | 25% -0.599807 -0.612162 -0.687373 -0.747478
685 | 50% 0.047101 -0.013609 -0.022158 -0.088274
686 | 75% 0.756646 0.695298 0.699046 0.623331
687 | max 2.653656 3.525865 2.735527 3.366626
688 | ```
689 |
690 | 假设你想要找出某列中绝对值大小超过3的值:
691 | ```python
692 | In [94]: col = data[2]
693 |
694 | In [95]: col[np.abs(col) > 3]
695 | Out[95]:
696 | 41 -3.399312
697 | 136 -3.745356
698 | Name: 2, dtype: float64
699 | ```
700 |
701 | 要选出全部含有“超过3或-3的值”的行,你可以在布尔型DataFrame中使用any方法:
702 | ```python
703 | In [96]: data[(np.abs(data) > 3).any(1)]
704 | Out[96]:
705 | 0 1 2 3
706 | 41 0.457246 -0.025907 -3.399312 -0.974657
707 | 60 1.951312 3.260383 0.963301 1.201206
708 | 136 0.508391 -0.196713 -3.745356 -1.520113
709 | 235 -0.242459 -3.056990 1.918403 -0.578828
710 | 258 0.682841 0.326045 0.425384 -3.428254
711 | 322 1.179227 -3.184377 1.369891 -1.074833
712 | 544 -3.548824 1.553205 -2.186301 1.277104
713 | 635 -0.578093 0.193299 1.397822 3.366626
714 | 782 -0.207434 3.525865 0.283070 0.544635
715 | 803 -3.645860 0.255475 -0.549574 -1.907459
716 | ```
717 |
718 | 根据这些条件,就可以对值进行设置。下面的代码可以将值限制在区间-3到3以内:
719 | ```python
720 | In [97]: data[np.abs(data) > 3] = np.sign(data) * 3
721 |
722 | In [98]: data.describe()
723 | Out[98]:
724 | 0 1 2 3
725 | count 1000.000000 1000.000000 1000.000000 1000.000000
726 | mean 0.050286 0.025567 -0.001399 -0.051765
727 | std 0.992920 1.004214 0.991414 0.995761
728 | min -3.000000 -3.000000 -3.000000 -3.000000
729 | 25% -0.599807 -0.612162 -0.687373 -0.747478
730 | 50% 0.047101 -0.013609 -0.022158 -0.088274
731 | 75% 0.756646 0.695298 0.699046 0.623331
732 | max 2.653656 3.000000 2.735527 3.000000
733 | ```
734 |
735 | 根据数据的值是正还是负,np.sign(data)可以生成1和-1:
736 | ```python
737 | In [99]: np.sign(data).head()
738 | Out[99]:
739 | 0 1 2 3
740 | 0 -1.0 1.0 -1.0 1.0
741 | 1 1.0 -1.0 1.0 -1.0
742 | 2 1.0 1.0 1.0 -1.0
743 | 3 -1.0 -1.0 1.0 -1.0
744 | 4 -1.0 1.0 -1.0 -1.0
745 | ```
746 |
747 | ## 排列和随机采样
748 | 利用numpy.random.permutation函数可以轻松实现对Series或DataFrame的列的排列工作(permuting,随机重排序)。通过需要排列的轴的长度调用permutation,可产生一个表示新顺序的整数数组:
749 | ```python
750 | In [100]: df = pd.DataFrame(np.arange(5 * 4).reshape((5, 4)))
751 |
752 | In [101]: sampler = np.random.permutation(5)
753 |
754 | In [102]: sampler
755 | Out[102]: array([3, 1, 4, 2, 0])
756 | ```
757 |
758 | 然后就可以在基于iloc的索引操作或take函数中使用该数组了:
759 | ```python
760 | In [103]: df
761 | Out[103]:
762 | 0 1 2 3
763 | 0 0 1 2 3
764 | 1 4 5 6 7
765 | 2 8 9 10 11
766 | 3 12 13 14 15
767 | 4 16 17 18 19
768 |
769 | In [104]: df.take(sampler)
770 | Out[104]:
771 | 0 1 2 3
772 | 3 12 13 14 15
773 | 1 4 5 6 7
774 | 4 16 17 18 19
775 | 2 8 9 10 11
776 | 0 0 1 2 3
777 | ```
778 |
779 | 如果不想用替换的方式选取随机子集,可以在Series和DataFrame上使用sample方法:
780 | ```python
781 | In [105]: df.sample(n=3)
782 | Out[105]:
783 | 0 1 2 3
784 | 3 12 13 14 15
785 | 4 16 17 18 19
786 | 2 8 9 10 11
787 | ```
788 |
789 | 要通过替换的方式产生样本(允许重复选择),可以传递replace=True到sample:
790 | ```python
791 | In [106]: choices = pd.Series([5, 7, -1, 6, 4])
792 |
793 | In [107]: draws = choices.sample(n=10, replace=True)
794 |
795 | In [108]: draws
796 | Out[108]:
797 | 4 4
798 | 1 7
799 | 4 4
800 | 2 -1
801 | 0 5
802 | 3 6
803 | 1 7
804 | 4 4
805 | 0 5
806 | 4 4
807 | dtype: int64
808 | ```
809 |
810 | ## 计算指标/哑变量
811 | 另一种常用于统计建模或机器学习的转换方式是:将分类变量(categorical variable)转换为“哑变量”或“指标矩阵”。
812 |
813 | 如果DataFrame的某一列中含有k个不同的值,则可以派生出一个k列矩阵或DataFrame(其值全为1和0)。pandas有一个get_dummies函数可以实现该功能(其实自己动手做一个也不难)。使用之前的一个DataFrame例子:
814 | ```python
815 | In [109]: df = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'b'],
816 | .....: 'data1': range(6)})
817 |
818 | In [110]: pd.get_dummies(df['key'])
819 | Out[110]:
820 | a b c
821 | 0 0 1 0
822 | 1 0 1 0
823 | 2 1 0 0
824 | 3 0 0 1
825 | 4 1 0 0
826 | 5 0 1 0
827 | ```
828 |
829 | 有时候,你可能想给指标DataFrame的列加上一个前缀,以便能够跟其他数据进行合并。get_dummies的prefix参数可以实现该功能:
830 | ```python
831 | In [111]: dummies = pd.get_dummies(df['key'], prefix='key')
832 |
833 | In [112]: df_with_dummy = df[['data1']].join(dummies)
834 |
835 | In [113]: df_with_dummy
836 | Out[113]:
837 | data1 key_a key_b key_c
838 | 0 0 0 1 0
839 | 1 1 0 1 0
840 | 2 2 1 0 0
841 | 3 3 0 0 1
842 | 4 4 1 0 0
843 | 5 5 0 1 0
844 | ```
845 |
846 | 如果DataFrame中的某行同属于多个分类,则事情就会有点复杂。看一下MovieLens 1M数据集,14章会更深入地研究它:
847 | ```python
848 | In [114]: mnames = ['movie_id', 'title', 'genres']
849 |
850 | In [115]: movies = pd.read_table('datasets/movielens/movies.dat', sep='::',
851 | .....: header=None, names=mnames)
852 |
853 | In [116]: movies[:10]
854 | Out[116]:
855 | movie_id title genres
856 | 0 1 Toy Story (1995) Animation|Children's|Comedy
857 | 1 2 Jumanji (1995) Adventure|Children's|Fantasy
858 | 2 3 Grumpier Old Men (1995) Comedy|Romance
859 | 3 4 Waiting to Exhale (1995) Comedy|Drama
860 | 4 5 Father of the Bride Part II (1995) Comedy
861 | 5 6 Heat (1995) Action|Crime|Thriller
862 | 6 7 Sabrina (1995) Comedy|Romance
863 | 7 8 Tom and Huck (1995) Adventure|Children's
864 | 8 9 Sudden Death (1995)
865 | Action
866 | 9 10 GoldenEye (1995) Action|Adventure|Thriller
867 | ```
868 |
869 | 要为每个genre添加指标变量就需要做一些数据规整操作。首先,我们从数据集中抽取出不同的genre值:
870 | ```python
871 | In [117]: all_genres = []
872 |
873 | In [118]: for x in movies.genres:
874 | .....: all_genres.extend(x.split('|'))
875 |
876 | In [119]: genres = pd.unique(all_genres)
877 | ```
878 |
879 | 现在有:
880 | ```python
881 | In [120]: genres
882 | Out[120]:
883 | array(['Animation', "Children's", 'Comedy', 'Adventure', 'Fantasy',
884 | 'Romance', 'Drama', 'Action', 'Crime', 'Thriller','Horror',
885 | 'Sci-Fi', 'Documentary', 'War', 'Musical', 'Mystery', 'Film-Noir',
886 | 'Western'], dtype=object)
887 | ```
888 |
889 | 构建指标DataFrame的方法之一是从一个全零DataFrame开始:
890 | ```python
891 | In [121]: zero_matrix = np.zeros((len(movies), len(genres)))
892 |
893 | In [122]: dummies = pd.DataFrame(zero_matrix, columns=genres)
894 | ```
895 |
896 | 现在,迭代每一部电影,并将dummies各行的条目设为1。要这么做,我们使用dummies.columns来计算每个类型的列索引:
897 | ```python
898 | In [123]: gen = movies.genres[0]
899 |
900 | In [124]: gen.split('|')
901 | Out[124]: ['Animation', "Children's", 'Comedy']
902 |
903 | In [125]: dummies.columns.get_indexer(gen.split('|'))
904 | Out[125]: array([0, 1, 2])
905 | ```
906 |
907 | 然后,根据索引,使用.iloc设定值:
908 | ```python
909 | In [126]: for i, gen in enumerate(movies.genres):
910 | .....: indices = dummies.columns.get_indexer(gen.split('|'))
911 | .....: dummies.iloc[i, indices] = 1
912 | .....:
913 | ```
914 |
915 | 然后,和以前一样,再将其与movies合并起来:
916 | ```python
917 | In [127]: movies_windic = movies.join(dummies.add_prefix('Genre_'))
918 |
919 | In [128]: movies_windic.iloc[0]
920 | Out[128]:
921 | movie_id 1
922 | title Toy Story (1995)
923 | genres Animation|Children's|Comedy
924 | Genre_Animation 1
925 | Genre_Children's 1
926 | Genre_Comedy 1
927 | Genre_Adventure 0
928 | Genre_Fantasy 0
929 | Genre_Romance 0
930 | Genre_Drama 0
931 | ...
932 | Genre_Crime 0
933 | Genre_Thriller 0
934 | Genre_Horror 0
935 | Genre_Sci-Fi 0
936 | Genre_Documentary 0
937 | Genre_War 0
938 | Genre_Musical 0
939 | Genre_Mystery 0
940 | Genre_Film-Noir 0
941 | Genre_Western 0
942 | Name: 0, Length: 21, dtype: object
943 | ```
944 |
945 | >笔记:对于很大的数据,用这种方式构建多成员指标变量就会变得非常慢。最好使用更低级的函数,将其写入NumPy数组,然后结果包装在DataFrame中。
946 |
947 | 一个对统计应用有用的秘诀是:结合get_dummies和诸如cut之类的离散化函数:
948 | ```python
949 | In [129]: np.random.seed(12345)
950 |
951 | In [130]: values = np.random.rand(10)
952 |
953 | In [131]: values
954 | Out[131]:
955 | array([ 0.9296, 0.3164, 0.1839, 0.2046, 0.5677, 0.5955, 0.9645,
956 | 0.6532, 0.7489, 0.6536])
957 |
958 | In [132]: bins = [0, 0.2, 0.4, 0.6, 0.8, 1]
959 |
960 | In [133]: pd.get_dummies(pd.cut(values, bins))
961 | Out[133]:
962 | (0.0, 0.2] (0.2, 0.4] (0.4, 0.6] (0.6, 0.8] (0.8, 1.0]
963 | 0 0 0 0 0 1
964 | 1 0 1 0 0 0
965 | 2 1 0 0 0 0
966 | 3 0 1 0 0 0
967 | 4 0 0 1 0 0
968 | 5 0 0 1 0 0
969 | 6 0 0 0 0 1
970 | 7 0 0 0 1 0
971 | 8 0 0 0 1 0
972 | 9 0 0 0 1 0
973 | ```
974 |
975 | 我们用numpy.random.seed,使这个例子具有确定性。本书后面会介绍pandas.get_dummies。
976 |
977 | # 7.3 字符串操作
978 |
979 | Python能够成为流行的数据处理语言,部分原因是其简单易用的字符串和文本处理功能。大部分文本运算都直接做成了字符串对象的内置方法。对于更为复杂的模式匹配和文本操作,则可能需要用到正则表达式。pandas对此进行了加强,它使你能够对整组数据应用字符串表达式和正则表达式,而且能处理烦人的缺失数据。
980 |
981 | ## 字符串对象方法
982 |
983 | 对于许多字符串处理和脚本应用,内置的字符串方法已经能够满足要求了。例如,以逗号分隔的字符串可以用split拆分成数段:
984 | ```python
985 | In [134]: val = 'a,b, guido'
986 | In [135]: val.split(',')
987 | Out[135]: ['a', 'b', ' guido']
988 | ```
989 |
990 | split常常与strip一起使用,以去除空白符(包括换行符):
991 | ```python
992 | In [136]: pieces = [x.strip() for x in val.split(',')]
993 |
994 | In [137]: pieces
995 | Out[137]: ['a', 'b', 'guido']
996 | ```
997 |
998 | 利用加法,可以将这些子字符串以双冒号分隔符的形式连接起来:
999 | ```python
1000 | In [138]: first, second, third = pieces
1001 |
1002 | In [139]: first + '::' + second + '::' + third
1003 | Out[139]: 'a::b::guido'
1004 | ```
1005 |
1006 | 但这种方式并不是很实用。一种更快更符合Python风格的方式是,向字符串"::"的join方法传入一个列表或元组:
1007 | ```python
1008 | In [140]: '::'.join(pieces)
1009 | Out[140]: 'a::b::guido'
1010 | ```
1011 |
1012 | 其它方法关注的是子串定位。检测子串的最佳方式是利用Python的in关键字,还可以使用index和find:
1013 | ```python
1014 | In [141]: 'guido' in val
1015 | Out[141]: True
1016 |
1017 | In [142]: val.index(',')
1018 | Out[142]: 1
1019 |
1020 | In [143]: val.find(':')
1021 | Out[143]: -1
1022 | ```
1023 |
1024 | 注意find和index的区别:如果找不到字符串,index将会引发一个异常(而不是返回-1):
1025 | ```python
1026 | In [144]: val.index(':')
1027 | ---------------------------------------------------------------------------
1028 | ValueError Traceback (most recent call last)
1029 | in ()
1030 | ----> 1 val.index(':')
1031 | ValueError: substring not found
1032 | ```
1033 |
1034 | 与此相关,count可以返回指定子串的出现次数:
1035 | ```python
1036 | In [145]: val.count(',')
1037 | Out[145]: 2
1038 | ```
1039 |
1040 | replace用于将指定模式替换为另一个模式。通过传入空字符串,它也常常用于删除模式:
1041 | ```python
1042 | In [146]: val.replace(',', '::')
1043 | Out[146]: 'a::b:: guido'
1044 |
1045 | In [147]: val.replace(',', '')
1046 | Out[147]: 'ab guido'
1047 | ```
1048 |
1049 | 表7-3列出了Python内置的字符串方法。
1050 |
1051 | 这些运算大部分都能使用正则表达式实现(马上就会看到)。
1052 |
1053 | 
1054 |
1055 | 
1056 |
1057 | casefold 将字符转换为小写,并将任何特定区域的变量字符组合转换成一个通用的可比较形式。
1058 |
1059 | ## 正则表达式
1060 |
1061 | 正则表达式提供了一种灵活的在文本中搜索或匹配(通常比前者复杂)字符串模式的方式。正则表达式,常称作regex,是根据正则表达式语言编写的字符串。Python内置的re模块负责对字符串应用正则表达式。我将通过一些例子说明其使用方法。
1062 |
1063 | >笔记:正则表达式的编写技巧可以自成一章,超出了本书的范围。从网上和其它书可以找到许多非常不错的教程和参考资料。
1064 |
1065 | re模块的函数可以分为三个大类:模式匹配、替换以及拆分。当然,它们之间是相辅相成的。一个regex描述了需要在文本中定位的一个模式,它可以用于许多目的。我们先来看一个简单的例子:假设我想要拆分一个字符串,分隔符为数量不定的一组空白符(制表符、空格、换行符等)。描述一个或多个空白符的regex是\s+:
1066 | ```python
1067 | In [148]: import re
1068 |
1069 | In [149]: text = "foo bar\t baz \tqux"
1070 |
1071 | In [150]: re.split('\s+', text)
1072 | Out[150]: ['foo', 'bar', 'baz', 'qux']
1073 | ```
1074 |
1075 | 调用re.split('\s+',text)时,正则表达式会先被编译,然后再在text上调用其split方法。你可以用re.compile自己编译regex以得到一个可重用的regex对象:
1076 | ```python
1077 | In [151]: regex = re.compile('\s+')
1078 |
1079 | In [152]: regex.split(text)
1080 | Out[152]: ['foo', 'bar', 'baz', 'qux']
1081 | ```
1082 |
1083 | 如果只希望得到匹配regex的所有模式,则可以使用findall方法:
1084 | ```python
1085 | In [153]: regex.findall(text)
1086 | Out[153]: [' ', '\t ', ' \t']
1087 | ```
1088 |
1089 | >笔记:如果想避免正则表达式中不需要的转义(\),则可以使用原始字符串字面量如r'C:\x'(也可以编写其等价式'C:\\x')。
1090 |
1091 | 如果打算对许多字符串应用同一条正则表达式,强烈建议通过re.compile创建regex对象。这样将可以节省大量的CPU时间。
1092 |
1093 | match和search跟findall功能类似。findall返回的是字符串中所有的匹配项,而search则只返回第一个匹配项。match更加严格,它只匹配字符串的首部。来看一个小例子,假设我们有一段文本以及一条能够识别大部分电子邮件地址的正则表达式:
1094 | ```python
1095 | text = """Dave dave@google.com
1096 | Steve steve@gmail.com
1097 | Rob rob@gmail.com
1098 | Ryan ryan@yahoo.com
1099 | """
1100 | pattern = r'[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}'
1101 |
1102 | # re.IGNORECASE makes the regex case-insensitive
1103 | regex = re.compile(pattern, flags=re.IGNORECASE)
1104 | ```
1105 |
1106 | 对text使用findall将得到一组电子邮件地址:
1107 | ```python
1108 | In [155]: regex.findall(text)
1109 | Out[155]:
1110 | ['dave@google.com',
1111 | 'steve@gmail.com',
1112 | 'rob@gmail.com',
1113 | 'ryan@yahoo.com']
1114 | ```
1115 |
1116 | search返回的是文本中第一个电子邮件地址(以特殊的匹配项对象形式返回)。对于上面那个regex,匹配项对象只能告诉我们模式在原字符串中的起始和结束位置:
1117 | ```python
1118 | In [156]: m = regex.search(text)
1119 |
1120 | In [157]: m
1121 | Out[157]: <_sre.SRE_Match object; span=(5, 20), match='dave@google.com'>
1122 |
1123 | In [158]: text[m.start():m.end()]
1124 | Out[158]: 'dave@google.com'
1125 | ```
1126 |
1127 | regex.match则将返回None,因为它只匹配出现在字符串开头的模式:
1128 | ```python
1129 | In [159]: print(regex.match(text))
1130 | None
1131 | ```
1132 |
1133 | 相关的,sub方法可以将匹配到的模式替换为指定字符串,并返回所得到的新字符串:
1134 | ```python
1135 | In [160]: print(regex.sub('REDACTED', text))
1136 | Dave REDACTED
1137 | Steve REDACTED
1138 | Rob REDACTED
1139 | Ryan REDACTED
1140 | ```
1141 |
1142 | 假设你不仅想要找出电子邮件地址,还想将各个地址分成3个部分:用户名、域名以及域后缀。要实现此功能,只需将待分段的模式的各部分用圆括号包起来即可:
1143 | ```python
1144 | In [161]: pattern = r'([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})'
1145 |
1146 | In [162]: regex = re.compile(pattern, flags=re.IGNORECASE)
1147 | ```
1148 |
1149 | 由这种修改过的正则表达式所产生的匹配项对象,可以通过其groups方法返回一个由模式各段组成的元组:
1150 | ```python
1151 | In [163]: m = regex.match('wesm@bright.net')
1152 |
1153 | In [164]: m.groups()
1154 | Out[164]: ('wesm', 'bright', 'net')
1155 | ```
1156 |
1157 | 对于带有分组功能的模式,findall会返回一个元组列表:
1158 | ```python
1159 | In [165]: regex.findall(text)
1160 | Out[165]:
1161 | [('dave', 'google', 'com'),
1162 | ('steve', 'gmail', 'com'),
1163 | ('rob', 'gmail', 'com'),
1164 | ('ryan', 'yahoo', 'com')]
1165 | ```
1166 |
1167 | sub还能通过诸如\1、\2之类的特殊符号访问各匹配项中的分组。符号\1对应第一个匹配的组,\2对应第二个匹配的组,以此类推:
1168 | ```python
1169 | In [166]: print(regex.sub(r'Username: \1, Domain: \2, Suffix: \3', text))
1170 | Dave Username: dave, Domain: google, Suffix: com
1171 | Steve Username: steve, Domain: gmail, Suffix: com
1172 | Rob Username: rob, Domain: gmail, Suffix: com
1173 | Ryan Username: ryan, Domain: yahoo, Suffix: com
1174 | ```
1175 |
1176 | Python中还有许多的正则表达式,但大部分都超出了本书的范围。表7-4是一个简要概括。
1177 |
1178 | 
1179 |
1180 | ## pandas的矢量化字符串函数
1181 |
1182 | 清理待分析的散乱数据时,常常需要做一些字符串规整化工作。更为复杂的情况是,含有字符串的列有时还含有缺失数据:
1183 | ```python
1184 | In [167]: data = {'Dave': 'dave@google.com', 'Steve': 'steve@gmail.com',
1185 | .....: 'Rob': 'rob@gmail.com', 'Wes': np.nan}
1186 |
1187 | In [168]: data = pd.Series(data)
1188 |
1189 | In [169]: data
1190 | Out[169]:
1191 | Dave dave@google.com
1192 | Rob rob@gmail.com
1193 | Steve steve@gmail.com
1194 | Wes NaN
1195 | dtype: object
1196 |
1197 | In [170]: data.isnull()
1198 | Out[170]:
1199 | Dave False
1200 | Rob False
1201 | Steve False
1202 | Wes True
1203 | dtype: bool
1204 | ```
1205 |
1206 | 通过data.map,所有字符串和正则表达式方法都能被应用于(传入lambda表达式或其他函数)各个值,但是如果存在NA(null)就会报错。为了解决这个问题,Series有一些能够跳过NA值的面向数组方法,进行字符串操作。通过Series的str属性即可访问这些方法。例如,我们可以通过str.contains检查各个电子邮件地址是否含有"gmail":
1207 | ```python
1208 | In [171]: data.str.contains('gmail')
1209 | Out[171]:
1210 | Dave False
1211 | Rob True
1212 | Steve True
1213 | Wes NaN
1214 | dtype: object
1215 | ```
1216 |
1217 | 也可以使用正则表达式,还可以加上任意re选项(如IGNORECASE):
1218 | ```python
1219 | In [172]: pattern
1220 | Out[172]: '([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\\.([A-Z]{2,4})'
1221 |
1222 | In [173]: data.str.findall(pattern, flags=re.IGNORECASE)
1223 | Out[173]:
1224 | Dave [(dave, google, com)]
1225 | Rob [(rob, gmail, com)]
1226 | Steve [(steve, gmail, com)]
1227 | Wes NaN
1228 | dtype: object
1229 | ```
1230 |
1231 | 有两个办法可以实现矢量化的元素获取操作:要么使用str.get,要么在str属性上使用索引:
1232 | ```python
1233 | In [174]: matches = data.str.match(pattern, flags=re.IGNORECASE)
1234 |
1235 | In [175]: matches
1236 | Out[175]:
1237 | Dave True
1238 | Rob True
1239 | Steve True
1240 | Wes NaN
1241 | dtype: object
1242 | ```
1243 |
1244 | 要访问嵌入列表中的元素,我们可以传递索引到这两个函数中:
1245 | ```python
1246 | In [176]: matches.str.get(1)
1247 | Out[176]:
1248 | Dave NaN
1249 | Rob NaN
1250 | Steve NaN
1251 | Wes NaN
1252 | dtype: float64
1253 |
1254 | In [177]: matches.str[0]
1255 | Out[177]:
1256 | Dave NaN
1257 | Rob NaN
1258 | Steve NaN
1259 | Wes NaN
1260 | dtype: float64
1261 | ```
1262 |
1263 | 你可以利用这种方法对字符串进行截取:
1264 | ```python
1265 | In [178]: data.str[:5]
1266 | Out[178]:
1267 | Dave dave@
1268 | Rob rob@g
1269 | Steve steve
1270 | Wes NaN
1271 | dtype: object
1272 | ```
1273 |
1274 | 表7-5介绍了更多的pandas字符串方法。
1275 |
1276 | 
1277 |
1278 |
1279 | # 7.4 总结
1280 |
1281 | 高效的数据准备可以让你将更多的时间用于数据分析,花较少的时间用于准备工作,这样就可以极大地提高生产力。我们在本章中学习了许多工具,但覆盖并不全面。下一章,我们会学习pandas的聚合与分组。
1282 |
--------------------------------------------------------------------------------
/第09章 绘图和可视化.md:
--------------------------------------------------------------------------------
1 | 信息可视化(也叫绘图)是数据分析中最重要的工作之一。它可能是探索过程的一部分,例如,帮助我们找出异常值、必要的数据转换、得出有关模型的idea等。另外,做一个可交互的数据可视化也许是工作的最终目标。Python有许多库进行静态或动态的数据可视化,但我这里重要关注于matplotlib(http://matplotlib.org/)和基于它的库。
2 |
3 | matplotlib是一个用于创建出版质量图表的桌面绘图包(主要是2D方面)。该项目是由John Hunter于2002年启动的,其目的是为Python构建一个MATLAB式的绘图接口。matplotlib和IPython社区进行合作,简化了从IPython shell(包括现在的Jupyter notebook)进行交互式绘图。matplotlib支持各种操作系统上许多不同的GUI后端,而且还能将图片导出为各种常见的矢量(vector)和光栅(raster)图:PDF、SVG、JPG、PNG、BMP、GIF等。除了几张,本书中的大部分图都是用它生成的。
4 |
5 | 随着时间的发展,matplotlib衍生出了多个数据可视化的工具集,它们使用matplotlib作为底层。其中之一是seaborn(http://seaborn.pydata.org/),本章后面会学习它。
6 |
7 | 学习本章代码案例的最简单方法是在Jupyter notebook进行交互式绘图。在Jupyter notebook中执行下面的语句:
8 | ```python
9 | %matplotlib notebook
10 | ```
11 |
12 | # 9.1 matplotlib API入门
13 |
14 | matplotlib的通常引入约定是:
15 |
16 | ```python
17 | In [11]: import matplotlib.pyplot as plt
18 | ```
19 |
20 | 在Jupyter中运行%matplotlib notebook(或在IPython中运行%matplotlib),就可以创建一个简单的图形。如果一切设置正确,会看到图9-1:
21 |
22 | ```python
23 | In [12]: import numpy as np
24 |
25 | In [13]: data = np.arange(10)
26 |
27 | In [14]: data
28 | Out[14]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
29 |
30 | In [15]: plt.plot(data)
31 | ```
32 |
33 | 
34 |
35 | 虽然seaborn这样的库和pandas的内置绘图函数能够处理许多普通的绘图任务,但如果需要自定义一些高级功能的话就必须学习matplotlib API。
36 |
37 | >笔记:虽然本书没有详细地讨论matplotlib的各种功能,但足以将你引入门。matplotlib的示例库和文档是学习高级特性的最好资源。
38 |
39 | ## Figure和Subplot
40 |
41 | matplotlib的图像都位于Figure对象中。你可以用plt.figure创建一个新的Figure:
42 |
43 | ```python
44 | In [16]: fig = plt.figure()
45 | ```
46 |
47 | 如果用的是IPython,这时会弹出一个空窗口,但在Jupyter中,必须再输入更多命令才能看到。plt.figure有一些选项,特别是figsize,它用于确保当图片保存到磁盘时具有一定的大小和纵横比。
48 |
49 | 不能通过空Figure绘图。必须用add_subplot创建一个或多个subplot才行:
50 |
51 | ```python
52 | In [17]: ax1 = fig.add_subplot(2, 2, 1)
53 | ```
54 |
55 | 这条代码的意思是:图像应该是2×2的(即最多4张图),且当前选中的是4个subplot中的第一个(编号从1开始)。如果再把后面两个subplot也创建出来,最终得到的图像如图9-2所示:
56 |
57 | ```python
58 | In [18]: ax2 = fig.add_subplot(2, 2, 2)
59 |
60 | In [19]: ax3 = fig.add_subplot(2, 2, 3)
61 | ```
62 |
63 | 
64 |
65 | >提示:使用Jupyter notebook有一点不同,即每个小窗重新执行后,图形会被重置。因此,对于复杂的图形,,你必须将所有的绘图命令存在一个小窗里。
66 |
67 | 这里,我们运行同一个小窗里的所有命令:
68 |
69 | ```python
70 | fig = plt.figure()
71 | ax1 = fig.add_subplot(2, 2, 1)
72 | ax2 = fig.add_subplot(2, 2, 2)
73 | ax3 = fig.add_subplot(2, 2, 3)
74 | ```
75 |
76 | 如果这时执行一条绘图命令(如plt.plot([1.5, 3.5, -2, 1.6])),matplotlib就会在最后一个用过的subplot(如果没有则创建一个)上进行绘制,隐藏创建figure和subplot的过程。因此,如果我们执行下列命令,你就会得到如图9-3所示的结果:
77 |
78 | ```python
79 | In [20]: plt.plot(np.random.randn(50).cumsum(), 'k--')
80 | ```
81 |
82 | 
83 |
84 | "k--"是一个线型选项,用于告诉matplotlib绘制黑色虚线图。上面那些由fig.add_subplot所返回的对象是AxesSubplot对象,直接调用它们的实例方法就可以在其它空着的格子里面画图了,如图9-4所示:
85 |
86 | ```python
87 | In [21]: ax1.hist(np.random.randn(100), bins=20, color='k', alpha=0.3)
88 |
89 | In [22]: ax2.scatter(np.arange(30), np.arange(30) + 3 * np.random.randn(30))
90 | ```
91 |
92 | 
93 |
94 | 你可以在matplotlib的文档中找到各种图表类型。
95 |
96 | 创建包含subplot网格的figure是一个非常常见的任务,matplotlib有一个更为方便的方法plt.subplots,它可以创建一个新的Figure,并返回一个含有已创建的subplot对象的NumPy数组:
97 |
98 | ```python
99 | In [24]: fig, axes = plt.subplots(2, 3)
100 |
101 | In [25]: axes
102 | Out[25]:
103 | array([[,
104 | ,
105 | ],
106 | [,
107 | ,
108 | ]], dtype
109 | =object)
110 | ```
111 |
112 | 这是非常实用的,因为可以轻松地对axes数组进行索引,就好像是一个二维数组一样,例如axes[0,1]。你还可以通过sharex和sharey指定subplot应该具有相同的X轴或Y轴。在比较相同范围的数据时,这也是非常实用的,否则,matplotlib会自动缩放各图表的界限。有关该方法的更多信息,请参见表9-1。
113 |
114 | 
115 |
116 | ## 调整subplot周围的间距
117 |
118 | 默认情况下,matplotlib会在subplot外围留下一定的边距,并在subplot之间留下一定的间距。间距跟图像的高度和宽度有关,因此,如果你调整了图像大小(不管是编程还是手工),间距也会自动调整。利用Figure的subplots_adjust方法可以轻而易举地修改间距,此外,它也是个顶级函数:
119 |
120 | ```python
121 | subplots_adjust(left=None, bottom=None, right=None, top=None,
122 | wspace=None, hspace=None)
123 | ```
124 |
125 | wspace和hspace用于控制宽度和高度的百分比,可以用作subplot之间的间距。下面是一个简单的例子,其中我将间距收缩到了0(如图9-5所示):
126 |
127 | ```python
128 | fig, axes = plt.subplots(2, 2, sharex=True, sharey=True)
129 | for i in range(2):
130 | for j in range(2):
131 | axes[i, j].hist(np.random.randn(500), bins=50, color='k', alpha=0.5)
132 | plt.subplots_adjust(wspace=0, hspace=0)
133 | ```
134 |
135 | 
136 |
137 | 不难看出,其中的轴标签重叠了。matplotlib不会检查标签是否重叠,所以对于这种情况,你只能自己设定刻度位置和刻度标签。后面几节将会详细介绍该内容。
138 |
139 | ## 颜色、标记和线型
140 |
141 | matplotlib的plot函数接受一组X和Y坐标,还可以接受一个表示颜色和线型的字符串缩写。例如,要根据x和y绘制绿色虚线,你可以执行如下代码:
142 |
143 | ```python
144 | ax.plot(x, y, 'g--')
145 | ```
146 |
147 | 这种在一个字符串中指定颜色和线型的方式非常方便。在实际中,如果你是用代码绘图,你可能不想通过处理字符串来获得想要的格式。通过下面这种更为明确的方式也能得到同样的效果:
148 |
149 | ```python
150 | ax.plot(x, y, linestyle='--', color='g')
151 | ```
152 |
153 | 常用的颜色可以使用颜色缩写,你也可以指定颜色码(例如,'#CECECE')。你可以通过查看plot的文档字符串查看所有线型的合集(在IPython和Jupyter中使用plot?)。
154 |
155 | 线图可以使用标记强调数据点。因为matplotlib可以创建连续线图,在点之间进行插值,因此有时可能不太容易看出真实数据点的位置。标记也可以放到格式字符串中,但标记类型和线型必须放在颜色后面(见图9-6):
156 |
157 | ```python
158 | In [30]: from numpy.random import randn
159 |
160 | In [31]: plt.plot(randn(30).cumsum(), 'ko--')
161 | ```
162 |
163 | 
164 |
165 | 还可以将其写成更为明确的形式:
166 |
167 | ```python
168 | plot(randn(30).cumsum(), color='k', linestyle='dashed', marker='o')
169 | ```
170 |
171 | 在线型图中,非实际数据点默认是按线性方式插值的。可以通过drawstyle选项修改(见图9-7):
172 |
173 | ```python
174 | In [33]: data = np.random.randn(30).cumsum()
175 |
176 | In [34]: plt.plot(data, 'k--', label='Default')
177 | Out[34]: []
178 |
179 | In [35]: plt.plot(data, 'k-', drawstyle='steps-post', label='steps-post')
180 | Out[35]: []
181 |
182 | In [36]: plt.legend(loc='best')
183 | ```
184 |
185 | 
186 |
187 | 你可能注意到运行上面代码时有输出。matplotlib会返回引用了新添加的子组件的对象。大多数时候,你可以放心地忽略这些输出。这里,因为我们传递了label参数到plot,我们可以创建一个plot图例,指明每条使用plt.legend的线。
188 |
189 | >笔记:你必须调用plt.legend(或使用ax.legend,如果引用了轴的话)来创建图例,无论你绘图时是否传递label标签选项。
190 |
191 | ## 刻度、标签和图例
192 |
193 | 对于大多数的图表装饰项,其主要实现方式有二:使用过程型的pyplot接口(例如,matplotlib.pyplot)以及更为面向对象的原生matplotlib API。
194 |
195 | pyplot接口的设计目的就是交互式使用,含有诸如xlim、xticks和xticklabels之类的方法。它们分别控制图表的范围、刻度位置、刻度标签等。其使用方式有以下两种:
196 |
197 | - 调用时不带参数,则返回当前的参数值(例如,plt.xlim()返回当前的X轴绘图范围)。
198 | - 调用时带参数,则设置参数值(例如,plt.xlim([0,10])会将X轴的范围设置为0到10)。
199 |
200 | 所有这些方法都是对当前或最近创建的AxesSubplot起作用的。它们各自对应subplot对象上的两个方法,以xlim为例,就是ax.get_xlim和ax.set_xlim。我更喜欢使用subplot的实例方法(因为我喜欢明确的事情,而且在处理多个subplot时这样也更清楚一些)。当然你完全可以选择自己觉得方便的那个。
201 |
202 | ## 设置标题、轴标签、刻度以及刻度标签
203 |
204 | 为了说明自定义轴,我将创建一个简单的图像并绘制一段随机漫步(如图9-8所示):
205 |
206 | ```python
207 | In [37]: fig = plt.figure()
208 |
209 | In [38]: ax = fig.add_subplot(1, 1, 1)
210 |
211 | In [39]: ax.plot(np.random.randn(1000).cumsum())
212 | ```
213 |
214 | 
215 |
216 | 要改变x轴刻度,最简单的办法是使用set_xticks和set_xticklabels。前者告诉matplotlib要将刻度放在数据范围中的哪些位置,默认情况下,这些位置也就是刻度标签。但我们可以通过set_xticklabels将任何其他的值用作标签:
217 |
218 | ```python
219 | In [40]: ticks = ax.set_xticks([0, 250, 500, 750, 1000])
220 |
221 | In [41]: labels = ax.set_xticklabels(['one', 'two', 'three', 'four', 'five'],
222 | ....: rotation=30, fontsize='small')
223 | ```
224 |
225 | rotation选项设定x刻度标签倾斜30度。最后,再用set_xlabel为X轴设置一个名称,并用set_title设置一个标题(见图9-9的结果):
226 |
227 | ```python
228 | In [42]: ax.set_title('My first matplotlib plot')
229 | Out[42]:
230 |
231 | In [43]: ax.set_xlabel('Stages')
232 | ```
233 |
234 | 
235 |
236 | Y轴的修改方式与此类似,只需将上述代码中的x替换为y即可。轴的类有集合方法,可以批量设定绘图选项。前面的例子,也可以写为:
237 |
238 | ```python
239 | props = {
240 | 'title': 'My first matplotlib plot',
241 | 'xlabel': 'Stages'
242 | }
243 | ax.set(**props)
244 | ```
245 |
246 | ## 添加图例
247 |
248 | 图例(legend)是另一种用于标识图表元素的重要工具。添加图例的方式有多种。最简单的是在添加subplot的时候传入label参数:
249 |
250 | ```python
251 | In [44]: from numpy.random import randn
252 |
253 | In [45]: fig = plt.figure(); ax = fig.add_subplot(1, 1, 1)
254 |
255 | In [46]: ax.plot(randn(1000).cumsum(), 'k', label='one')
256 | Out[46]: []
257 |
258 | In [47]: ax.plot(randn(1000).cumsum(), 'k--', label='two')
259 | Out[47]: []
260 |
261 | In [48]: ax.plot(randn(1000).cumsum(), 'k.', label='three')
262 | Out[48]: []
263 | ```
264 |
265 | 在此之后,你可以调用ax.legend()或plt.legend()来自动创建图例(结果见图9-10):
266 |
267 | ```python
268 | In [49]: ax.legend(loc='best')
269 | ```
270 |
271 | 
272 |
273 | legend方法有几个其它的loc位置参数选项。请查看文档字符串(使用ax.legend?)。
274 |
275 | loc告诉matplotlib要将图例放在哪。如果你不是吹毛求疵的话,"best"是不错的选择,因为它会选择最不碍事的位置。要从图例中去除一个或多个元素,不传入label或传入label='_nolegend_'即可。(中文第一版这里把best错写成了beat)
276 |
277 | ## 注解以及在Subplot上绘图
278 |
279 | 除标准的绘图类型,你可能还希望绘制一些子集的注解,可能是文本、箭头或其他图形等。注解和文字可以通过text、arrow和annotate函数进行添加。text可以将文本绘制在图表的指定坐标(x,y),还可以加上一些自定义格式:
280 |
281 | ```python
282 | ax.text(x, y, 'Hello world!',
283 | family='monospace', fontsize=10)
284 | ```
285 |
286 | 注解中可以既含有文本也含有箭头。例如,我们根据最近的标准普尔500指数价格(来自Yahoo!Finance)绘制一张曲线图,并标出2008年到2009年金融危机期间的一些重要日期。你可以在Jupyter notebook的一个小窗中试验这段代码(图9-11是结果):
287 |
288 | ```python
289 | from datetime import datetime
290 |
291 | fig = plt.figure()
292 | ax = fig.add_subplot(1, 1, 1)
293 |
294 | data = pd.read_csv('examples/spx.csv', index_col=0, parse_dates=True)
295 | spx = data['SPX']
296 |
297 | spx.plot(ax=ax, style='k-')
298 |
299 | crisis_data = [
300 | (datetime(2007, 10, 11), 'Peak of bull market'),
301 | (datetime(2008, 3, 12), 'Bear Stearns Fails'),
302 | (datetime(2008, 9, 15), 'Lehman Bankruptcy')
303 | ]
304 |
305 | for date, label in crisis_data:
306 | ax.annotate(label, xy=(date, spx.asof(date) + 75),
307 | xytext=(date, spx.asof(date) + 225),
308 | arrowprops=dict(facecolor='black', headwidth=4, width=2,
309 | headlength=4),
310 | horizontalalignment='left', verticalalignment='top')
311 |
312 | # Zoom in on 2007-2010
313 | ax.set_xlim(['1/1/2007', '1/1/2011'])
314 | ax.set_ylim([600, 1800])
315 |
316 | ax.set_title('Important dates in the 2008-2009 financial crisis')
317 | ```
318 |
319 | 
320 |
321 | 这张图中有几个重要的点要强调:ax.annotate方法可以在指定的x和y坐标轴绘制标签。我们使用set_xlim和set_ylim人工设定起始和结束边界,而不使用matplotlib的默认方法。最后,用ax.set_title添加图标标题。
322 |
323 | 更多有关注解的示例,请访问matplotlib的在线示例库。
324 |
325 | 图形的绘制要麻烦一些。matplotlib有一些表示常见图形的对象。这些对象被称为块(patch)。其中有些(如Rectangle和Circle),可以在matplotlib.pyplot中找到,但完整集合位于matplotlib.patches。
326 |
327 | 要在图表中添加一个图形,你需要创建一个块对象shp,然后通过ax.add_patch(shp)将其添加到subplot中(如图9-12所示):
328 |
329 | ```python
330 | fig = plt.figure()
331 | ax = fig.add_subplot(1, 1, 1)
332 |
333 | rect = plt.Rectangle((0.2, 0.75), 0.4, 0.15, color='k', alpha=0.3)
334 | circ = plt.Circle((0.7, 0.2), 0.15, color='b', alpha=0.3)
335 | pgon = plt.Polygon([[0.15, 0.15], [0.35, 0.4], [0.2, 0.6]],
336 | color='g', alpha=0.5)
337 |
338 | ax.add_patch(rect)
339 | ax.add_patch(circ)
340 | ax.add_patch(pgon)
341 | ```
342 |
343 | 
344 |
345 |
346 | 如果查看许多常见图表对象的具体实现代码,你就会发现它们其实就是由块patch组装而成的。
347 |
348 | ## 将图表保存到文件
349 |
350 | 利用plt.savefig可以将当前图表保存到文件。该方法相当于Figure对象的实例方法savefig。例如,要将图表保存为SVG文件,你只需输入:
351 |
352 | ```python
353 | plt.savefig('figpath.svg')
354 | ```
355 |
356 | 文件类型是通过文件扩展名推断出来的。因此,如果你使用的是.pdf,就会得到一个PDF文件。我在发布图片时最常用到两个重要的选项是dpi(控制“每英寸点数”分辨率)和bbox_inches(可以剪除当前图表周围的空白部分)。要得到一张带有最小白边且分辨率为400DPI的PNG图片,你可以:
357 |
358 | ```python
359 | plt.savefig('figpath.png', dpi=400, bbox_inches='tight')
360 | ```
361 |
362 | savefig并非一定要写入磁盘,也可以写入任何文件型的对象,比如BytesIO:
363 |
364 | ```python
365 | from io import BytesIO
366 | buffer = BytesIO()
367 | plt.savefig(buffer)
368 | plot_data = buffer.getvalue()
369 | ```
370 |
371 | 表9-2列出了savefig的其它选项。
372 |
373 | 
374 |
375 | ## matplotlib配置
376 |
377 | matplotlib自带一些配色方案,以及为生成出版质量的图片而设定的默认配置信息。幸运的是,几乎所有默认行为都能通过一组全局参数进行自定义,它们可以管理图像大小、subplot边距、配色方案、字体大小、网格类型等。一种Python编程方式配置系统的方法是使用rc方法。例如,要将全局的图像默认大小设置为10×10,你可以执行:
378 |
379 | ```python
380 | plt.rc('figure', figsize=(10, 10))
381 | ```
382 |
383 | rc的第一个参数是希望自定义的对象,如'figure'、'axes'、'xtick'、'ytick'、'grid'、'legend'等。其后可以跟上一系列的关键字参数。一个简单的办法是将这些选项写成一个字典:
384 |
385 | ```python
386 | font_options = {'family' : 'monospace',
387 | 'weight' : 'bold',
388 | 'size' : 'small'}
389 | plt.rc('font', **font_options)
390 | ```
391 |
392 | 要了解全部的自定义选项,请查阅matplotlib的配置文件matplotlibrc(位于matplotlib/mpl-data目录中)。如果对该文件进行了自定义,并将其放在你自己的.matplotlibrc目录中,则每次使用matplotlib时就会加载该文件。
393 |
394 | 下一节,我们会看到,seaborn包有若干内置的绘图主题或类型,它们使用了matplotlib的内部配置。
395 |
396 | # 9.2 使用pandas和seaborn绘图
397 |
398 | matplotlib实际上是一种比较低级的工具。要绘制一张图表,你组装一些基本组件就行:数据展示(即图表类型:线型图、柱状图、盒形图、散布图、等值线图等)、图例、标题、刻度标签以及其他注解型信息。
399 |
400 | 在pandas中,我们有多列数据,还有行和列标签。pandas自身就有内置的方法,用于简化从DataFrame和Series绘制图形。另一个库seaborn(https://seaborn.pydata.org/),由Michael Waskom创建的静态图形库。Seaborn简化了许多常见可视类型的创建。
401 |
402 | >提示:引入seaborn会修改matplotlib默认的颜色方案和绘图类型,以提高可读性和美观度。即使你不使用seaborn API,你可能也会引入seaborn,作为提高美观度和绘制常见matplotlib图形的简化方法。
403 |
404 | ## 线型图
405 |
406 | Series和DataFrame都有一个用于生成各类图表的plot方法。默认情况下,它们所生成的是线型图(如图9-13所示):
407 |
408 | ```python
409 | In [60]: s = pd.Series(np.random.randn(10).cumsum(), index=np.arange(0, 100, 10))
410 |
411 | In [61]: s.plot()
412 | ```
413 |
414 | 
415 |
416 | 该Series对象的索引会被传给matplotlib,并用以绘制X轴。可以通过use_index=False禁用该功能。X轴的刻度和界限可以通过xticks和xlim选项进行调节,Y轴就用yticks和ylim。plot参数的完整列表请参见表9-3。我只会讲解其中几个,剩下的就留给读者自己去研究了。
417 |
418 |
419 | 
420 |
421 | 
422 |
423 | pandas的大部分绘图方法都有一个可选的ax参数,它可以是一个matplotlib的subplot对象。这使你能够在网格布局中更为灵活地处理subplot的位置。
424 |
425 | DataFrame的plot方法会在一个subplot中为各列绘制一条线,并自动创建图例(如图9-14所示):
426 | ```python
427 | In [62]: df = pd.DataFrame(np.random.randn(10, 4).cumsum(0),
428 | ....: columns=['A', 'B', 'C', 'D'],
429 | ....: index=np.arange(0, 100, 10))
430 |
431 | In [63]: df.plot()
432 | ```
433 |
434 | 
435 |
436 | plot属性包含一批不同绘图类型的方法。例如,df.plot()等价于df.plot.line()。后面会学习这些方法。
437 |
438 | >笔记:plot的其他关键字参数会被传给相应的matplotlib绘图函数,所以要更深入地自定义图表,就必须学习更多有关matplotlib API的知识。
439 |
440 | DataFrame还有一些用于对列进行灵活处理的选项,例如,是要将所有列都绘制到一个subplot中还是创建各自的subplot。详细信息请参见表9-4。
441 |
442 | 
443 |
444 | >注意: 有关时间序列的绘图,请见第11章。
445 |
446 | ## 柱状图
447 |
448 | plot.bar()和plot.barh()分别绘制水平和垂直的柱状图。这时,Series和DataFrame的索引将会被用作X(bar)或Y(barh)刻度(如图9-15所示):
449 |
450 | ```python
451 | In [64]: fig, axes = plt.subplots(2, 1)
452 |
453 | In [65]: data = pd.Series(np.random.rand(16), index=list('abcdefghijklmnop'))
454 |
455 | In [66]: data.plot.bar(ax=axes[0], color='k', alpha=0.7)
456 | Out[66]:
457 |
458 | In [67]: data.plot.barh(ax=axes[1], color='k', alpha=0.7)
459 | ```
460 |
461 | 
462 |
463 | color='k'和alpha=0.7设定了图形的颜色为黑色,并使用部分的填充透明度。对于DataFrame,柱状图会将每一行的值分为一组,并排显示,如图9-16所示:
464 |
465 | ```python
466 | In [69]: df = pd.DataFrame(np.random.rand(6, 4),
467 | ....: index=['one', 'two', 'three', 'four', 'five', 'six'],
468 | ....: columns=pd.Index(['A', 'B', 'C', 'D'], name='Genus'))
469 |
470 | In [70]: df
471 | Out[70]:
472 | Genus A B C D
473 | one 0.370670 0.602792 0.229159 0.486744
474 | two 0.420082 0.571653 0.049024 0.880592
475 | three 0.814568 0.277160 0.880316 0.431326
476 | four 0.374020 0.899420 0.460304 0.100843
477 | five 0.433270 0.125107 0.494675 0.961825
478 | six 0.601648 0.478576 0.205690 0.560547
479 |
480 | In [71]: df.plot.bar()
481 | ```
482 |
483 | 
484 |
485 | 注意,DataFrame各列的名称"Genus"被用作了图例的标题。
486 |
487 | 设置stacked=True即可为DataFrame生成堆积柱状图,这样每行的值就会被堆积在一起(如图9-17所示):
488 |
489 | ```python
490 | In [73]: df.plot.barh(stacked=True, alpha=0.5)
491 | ```
492 |
493 | 
494 |
495 | >笔记:柱状图有一个非常不错的用法:利用value_counts图形化显示Series中各值的出现频率,比如s.value_counts().plot.bar()。
496 |
497 | 再以本书前面用过的那个有关小费的数据集为例,假设我们想要做一张堆积柱状图以展示每天各种聚会规模的数据点的百分比。我用read_csv将数据加载进来,然后根据日期和聚会规模创建一张交叉表:
498 |
499 | ```python
500 | In [75]: tips = pd.read_csv('examples/tips.csv')
501 |
502 | In [76]: party_counts = pd.crosstab(tips['day'], tips['size'])
503 |
504 | In [77]: party_counts
505 | Out[77]:
506 | size 1 2 3 4 5 6
507 | day
508 | Fri 1 16 1 1 0 0
509 | Sat 2 53 18 13 1 0
510 | Sun 0 39 15 18 3 1
511 | Thur 1 48 4 5 1 3
512 |
513 | # Not many 1- and 6-person parties
514 | In [78]: party_counts = party_counts.loc[:, 2:5]
515 | ```
516 |
517 | 然后进行规格化,使得各行的和为1,并生成图表(如图9-18所示):
518 |
519 | ```python
520 | # Normalize to sum to 1
521 | In [79]: party_pcts = party_counts.div(party_counts.sum(1), axis=0)
522 |
523 | In [80]: party_pcts
524 | Out[80]:
525 | size 2 3 4 5
526 | day
527 | Fri 0.888889 0.055556 0.055556 0.000000
528 | Sat 0.623529 0.211765 0.152941 0.011765
529 | Sun 0.520000 0.200000 0.240000 0.040000
530 | Thur 0.827586 0.068966 0.086207 0.017241
531 |
532 | In [81]: party_pcts.plot.bar()
533 | ```
534 |
535 | 
536 |
537 | 于是,通过该数据集就可以看出,聚会规模在周末会变大。
538 |
539 | 对于在绘制一个图形之前,需要进行合计的数据,使用seaborn可以减少工作量。用seaborn来看每天的小费比例(图9-19是结果):
540 |
541 | ```python
542 | In [83]: import seaborn as sns
543 |
544 | In [84]: tips['tip_pct'] = tips['tip'] / (tips['total_bill'] - tips['tip'])
545 |
546 | In [85]: tips.head()
547 | Out[85]:
548 | total_bill tip smoker day time size tip_pct
549 | 0 16.99 1.01 No Sun Dinner 2 0.063204
550 | 1 10.34 1.66 No Sun Dinner 3 0.191244
551 | 2 21.01 3.50 No Sun Dinner 3 0.199886
552 | 3 23.68 3.31 No Sun Dinner 2 0.162494
553 | 4 24.59 3.61 No Sun Dinner 4 0.172069
554 |
555 | In [86]: sns.barplot(x='tip_pct', y='day', data=tips, orient='h')
556 | ```
557 |
558 | 
559 |
560 | seaborn的绘制函数使用data参数,它可能是pandas的DataFrame。其它的参数是关于列的名字。因为一天的每个值有多次观察,柱状图的值是tip_pct的平均值。绘制在柱状图上的黑线代表95%置信区间(可以通过可选参数配置)。
561 |
562 | seaborn.barplot有颜色选项,使我们能够通过一个额外的值设置(见图9-20):
563 |
564 | ```python
565 | In [88]: sns.barplot(x='tip_pct', y='day', hue='time', data=tips, orient='h')
566 | ```
567 |
568 | 
569 |
570 | 注意,seaborn已经自动修改了图形的美观度:默认调色板,图形背景和网格线的颜色。你可以用seaborn.set在不同的图形外观之间切换:
571 |
572 | ```python
573 | In [90]: sns.set(style="whitegrid")
574 | ```
575 |
576 | ## 直方图和密度图
577 |
578 | 直方图(histogram)是一种可以对值频率进行离散化显示的柱状图。数据点被拆分到离散的、间隔均匀的面元中,绘制的是各面元中数据点的数量。再以前面那个小费数据为例,通过在Series使用plot.hist方法,我们可以生成一张“小费占消费总额百分比”的直方图(如图9-21所示):
579 | ```python
580 | In [92]: tips['tip_pct'].plot.hist(bins=50)
581 | ```
582 |
583 | 
584 |
585 | 与此相关的一种图表类型是密度图,它是通过计算“可能会产生观测数据的连续概率分布的估计”而产生的。一般的过程是将该分布近似为一组核(即诸如正态分布之类的较为简单的分布)。因此,密度图也被称作KDE(Kernel Density Estimate,核密度估计)图。使用plot.kde和标准混合正态分布估计即可生成一张密度图(见图9-22):
586 | ```python
587 | In [94]: tips['tip_pct'].plot.density()
588 | ```
589 |
590 | 
591 |
592 | seaborn的distplot方法绘制直方图和密度图更加简单,还可以同时画出直方图和连续密度估计图。作为例子,考虑一个双峰分布,由两个不同的标准正态分布组成(见图9-23):
593 |
594 | ```python
595 | In [96]: comp1 = np.random.normal(0, 1, size=200)
596 |
597 | In [97]: comp2 = np.random.normal(10, 2, size=200)
598 |
599 | In [98]: values = pd.Series(np.concatenate([comp1, comp2]))
600 |
601 | In [99]: sns.distplot(values, bins=100, color='k')
602 | ```
603 |
604 | 
605 |
606 | ## 散布图或点图
607 |
608 | 点图或散布图是观察两个一维数据序列之间的关系的有效手段。在下面这个例子中,我加载了来自statsmodels项目的macrodata数据集,选择了几个变量,然后计算对数差:
609 |
610 | ```python
611 | In [100]: macro = pd.read_csv('examples/macrodata.csv')
612 |
613 | In [101]: data = macro[['cpi', 'm1', 'tbilrate', 'unemp']]
614 |
615 | In [102]: trans_data = np.log(data).diff().dropna()
616 |
617 | In [103]: trans_data[-5:]
618 | Out[103]:
619 | cpi m1 tbilrate unemp
620 | 198 -0.007904 0.045361 -0.396881 0.105361
621 | 199 -0.021979 0.066753 -2.277267 0.139762
622 | 200 0.002340 0.010286 0.606136 0.160343
623 | 201 0.008419 0.037461 -0.200671 0.127339
624 | 202 0.008894 0.012202 -0.405465 0.042560
625 | ```
626 |
627 | 然后可以使用seaborn的regplot方法,它可以做一个散布图,并加上一条线性回归的线(见图9-24):
628 |
629 | ```python
630 | In [105]: sns.regplot('m1', 'unemp', data=trans_data)
631 | Out[105]:
632 |
633 | In [106]: plt.title('Changes in log %s versus log %s' % ('m1', 'unemp'))
634 | ```
635 |
636 | 
637 |
638 | 在探索式数据分析工作中,同时观察一组变量的散布图是很有意义的,这也被称为散布图矩阵(scatter plot matrix)。纯手工创建这样的图表很费工夫,所以seaborn提供了一个便捷的pairplot函数,它支持在对角线上放置每个变量的直方图或密度估计(见图9-25):
639 |
640 | ```python
641 | In [107]: sns.pairplot(trans_data, diag_kind='kde', plot_kws={'alpha': 0.2})
642 | ```
643 |
644 | 
645 |
646 | 你可能注意到了plot_kws参数。它可以让我们传递配置选项到非对角线元素上的图形使用。对于更详细的配置选项,可以查阅seaborn.pairplot文档字符串。
647 |
648 |
649 | ##分面网格(facet grid)和类型数据
650 | 要是数据集有额外的分组维度呢?有多个分类变量的数据可视化的一种方法是使用小面网格。seaborn有一个有用的内置函数factorplot,可以简化制作多种分面图(见图9-26):
651 | ```python
652 | In [108]: sns.factorplot(x='day', y='tip_pct', hue='time', col='smoker',
653 | .....: kind='bar', data=tips[tips.tip_pct < 1])
654 | ```
655 |
656 | 
657 |
658 | 除了在分面中用不同的颜色按时间分组,我们还可以通过给每个时间值添加一行来扩展分面网格:
659 |
660 | ```python
661 | In [109]: sns.factorplot(x='day', y='tip_pct', row='time',
662 | .....: col='smoker',
663 | .....: kind='bar', data=tips[tips.tip_pct < 1])
664 | ```
665 |
666 | 
667 |
668 | factorplot支持其它的绘图类型,你可能会用到。例如,盒图(它可以显示中位数,四分位数,和异常值)就是一个有用的可视化类型(见图9-28):
669 |
670 | ```python
671 | In [110]: sns.factorplot(x='tip_pct', y='day', kind='box',
672 | .....: data=tips[tips.tip_pct < 0.5])
673 | ```
674 |
675 | 
676 |
677 | 使用更通用的seaborn.FacetGrid类,你可以创建自己的分面网格。请查阅seaborn的文档(https://seaborn.pydata.org/)。
678 |
679 | # 9.3 其它的Python可视化工具
680 | 与其它开源库类似,Python创建图形的方式非常多(根本罗列不完)。自从2010年,许多开发工作都集中在创建交互式图形以便在Web上发布。利用工具如Boken(https://bokeh.pydata.org/en/latest/)和Plotly(https://github.com/plotly/plotly.py),现在可以创建动态交互图形,用于网页浏览器。
681 |
682 | 对于创建用于打印或网页的静态图形,我建议默认使用matplotlib和附加的库,比如pandas和seaborn。对于其它数据可视化要求,学习其它的可用工具可能是有用的。我鼓励你探索绘图的生态系统,因为它将持续发展。
683 |
684 | # 9.4 总结
685 |
686 | 本章的目的是熟悉一些基本的数据可视化操作,使用pandas,matplotlib,和seaborn。如果视觉显示数据分析的结果对你的工作很重要,我鼓励你寻求更多的资源来了解更高效的数据可视化。这是一个活跃的研究领域,你可以通过在线和纸质的形式学习许多优秀的资源。
687 |
688 | 下一章,我们将重点放在pandas的数据聚合和分组操作上。
689 |
--------------------------------------------------------------------------------
/第12章 pandas高级应用.md:
--------------------------------------------------------------------------------
1 | 前面的章节关注于不同类型的数据规整流程和NumPy、pandas与其它库的特点。随着时间的发展,pandas发展出了更多适合高级用户的功能。本章就要深入学习pandas的高级功能。
2 |
3 | # 12.1 分类数据
4 |
5 | 这一节介绍的是pandas的分类类型。我会向你展示通过使用它,提高性能和内存的使用率。我还会介绍一些在统计和机器学习中使用分类数据的工具。
6 |
7 | ## 背景和目的
8 |
9 | 表中的一列通常会有重复的包含不同值的小集合的情况。我们已经学过了unique和value_counts,它们可以从数组提取出不同的值,并分别计算频率:
10 | ```python
11 | In [10]: import numpy as np; import pandas as pd
12 |
13 | In [11]: values = pd.Series(['apple', 'orange', 'apple',
14 | ....: 'apple'] * 2)
15 |
16 | In [12]: values
17 | Out[12]:
18 | 0 apple
19 | 1 orange
20 | 2 apple
21 | 3 apple
22 | 4 apple
23 | 5 orange
24 | 6 apple
25 | 7 apple
26 | dtype: object
27 |
28 | In [13]: pd.unique(values)
29 | Out[13]: array(['apple', 'orange'], dtype=object)
30 |
31 | In [14]: pd.value_counts(values)
32 | Out[14]:
33 | apple 6
34 | orange 2
35 | dtype: int64
36 | ```
37 |
38 | 许多数据系统(数据仓库、统计计算或其它应用)都发展出了特定的表征重复值的方法,以进行高效的存储和计算。在数据仓库中,最好的方法是使用所谓的包含不同值的维表(Dimension Table),将主要的参数存储为引用维表整数键:
39 | ```python
40 | In [15]: values = pd.Series([0, 1, 0, 0] * 2)
41 |
42 | In [16]: dim = pd.Series(['apple', 'orange'])
43 |
44 | In [17]: values
45 | Out[17]:
46 | 0 0
47 | 1 1
48 | 2 0
49 | 3 0
50 | 4 0
51 | 5 1
52 | 6 0
53 | 7 0
54 | dtype: int64
55 |
56 | In [18]: dim
57 | Out[18]:
58 | 0 apple
59 | 1 orange
60 | dtype: object
61 | ```
62 |
63 | 可以使用take方法存储原始的字符串Series:
64 | ```python
65 | In [19]: dim.take(values)
66 | Out[19]:
67 | 0 apple
68 | 1 orange
69 | 0 apple
70 | 0 apple
71 | 0 apple
72 | 1 orange
73 | 0 apple
74 | 0 apple
75 | dtype: object
76 | ```
77 |
78 | 这种用整数表示的方法称为分类或字典编码表示法。不同值得数组称为分类、字典或数据级。本书中,我们使用分类的说法。表示分类的整数值称为分类编码或简单地称为编码。
79 |
80 | 分类表示可以在进行分析时大大的提高性能。你也可以在保持编码不变的情况下,对分类进行转换。一些相对简单的转变例子包括:
81 |
82 | - 重命名分类。
83 | - 加入一个新的分类,不改变已经存在的分类的顺序或位置。
84 |
85 | ## pandas的分类类型
86 |
87 | pandas有一个特殊的分类类型,用于保存使用整数分类表示法的数据。看一个之前的Series例子:
88 | ```python
89 | In [20]: fruits = ['apple', 'orange', 'apple', 'apple'] * 2
90 |
91 | In [21]: N = len(fruits)
92 |
93 | In [22]: df = pd.DataFrame({'fruit': fruits,
94 | ....: 'basket_id': np.arange(N),
95 | ....: 'count': np.random.randint(3, 15, size=N),
96 | ....: 'weight': np.random.uniform(0, 4, size=N)},
97 | ....: columns=['basket_id', 'fruit', 'count', 'weight'])
98 |
99 | In [23]: df
100 | Out[23]:
101 | basket_id fruit count weight
102 | 0 0 apple 5 3.858058
103 | 1 1 orange 8 2.612708
104 | 2 2 apple 4 2.995627
105 | 3 3 apple 7 2.614279
106 | 4 4 apple 12 2.990859
107 | 5 5 orange 8 3.845227
108 | 6 6 apple 5 0.033553
109 | 7 7 apple 4 0.425778
110 | ```
111 |
112 | 这里,df['fruit']是一个Python字符串对象的数组。我们可以通过调用它,将它转变为分类:
113 | ```python
114 | In [24]: fruit_cat = df['fruit'].astype('category')
115 |
116 | In [25]: fruit_cat
117 | Out[25]:
118 | 0 apple
119 | 1 orange
120 | 2 apple
121 | 3 apple
122 | 4 apple
123 | 5 orange
124 | 6 apple
125 | 7 apple
126 | Name: fruit, dtype: category
127 | Categories (2, object): [apple, orange]
128 | ```
129 |
130 | fruit_cat的值不是NumPy数组,而是一个pandas.Categorical实例:
131 | ```python
132 | In [26]: c = fruit_cat.values
133 |
134 | In [27]: type(c)
135 | Out[27]: pandas.core.categorical.Categorical
136 | ```
137 |
138 | 分类对象有categories和codes属性:
139 | ```python
140 | In [28]: c.categories
141 | Out[28]: Index(['apple', 'orange'], dtype='object')
142 |
143 | In [29]: c.codes
144 | Out[29]: array([0, 1, 0, 0, 0, 1, 0, 0], dtype=int8)
145 | ```
146 |
147 | 你可将DataFrame的列通过分配转换结果,转换为分类:
148 | ```python
149 | In [30]: df['fruit'] = df['fruit'].astype('category')
150 |
151 | In [31]: df.fruit
152 | Out[31]:
153 | 0 apple
154 | 1 orange
155 | 2 apple
156 | 3 apple
157 | 4 apple
158 | 5 orange
159 | 6 apple
160 | 7 apple
161 | Name: fruit, dtype: category
162 | Categories (2, object): [apple, orange]
163 | ```
164 |
165 | 你还可以从其它Python序列直接创建pandas.Categorical:
166 | ```python
167 | In [32]: my_categories = pd.Categorical(['foo', 'bar', 'baz', 'foo', 'bar'])
168 |
169 | In [33]: my_categories
170 | Out[33]:
171 | [foo, bar, baz, foo, bar]
172 | Categories (3, object): [bar, baz, foo]
173 | ```
174 |
175 | 如果你已经从其它源获得了分类编码,你还可以使用from_codes构造器:
176 | ```python
177 | In [34]: categories = ['foo', 'bar', 'baz']
178 |
179 | In [35]: codes = [0, 1, 2, 0, 0, 1]
180 |
181 | In [36]: my_cats_2 = pd.Categorical.from_codes(codes, categories)
182 |
183 | In [37]: my_cats_2
184 | Out[37]:
185 | [foo, bar, baz, foo, foo, bar]
186 | Categories (3, object): [foo, bar, baz]
187 | ```
188 |
189 | 与显示指定不同,分类变换不认定指定的分类顺序。因此取决于输入数据的顺序,categories数组的顺序会不同。当使用from_codes或其它的构造器时,你可以指定分类一个有意义的顺序:
190 | ```python
191 | In [38]: ordered_cat = pd.Categorical.from_codes(codes, categories,
192 | ....: ordered=True)
193 |
194 | In [39]: ordered_cat
195 | Out[39]:
196 | [foo, bar, baz, foo, foo, bar]
197 | Categories (3, object): [foo < bar < baz]
198 | ```
199 |
200 | 输出[foo < bar < baz]指明‘foo’位于‘bar’的前面,以此类推。无序的分类实例可以通过as_ordered排序:
201 | ```python
202 | In [40]: my_cats_2.as_ordered()
203 | Out[40]:
204 | [foo, bar, baz, foo, foo, bar]
205 | Categories (3, object): [foo < bar < baz]
206 | ```
207 |
208 | 最后要注意,分类数据不需要字符串,尽管我仅仅展示了字符串的例子。分类数组可以包括任意不可变类型。
209 |
210 | ## 用分类进行计算
211 |
212 | 与非编码版本(比如字符串数组)相比,使用pandas的Categorical有些类似。某些pandas组件,比如groupby函数,更适合进行分类。还有一些函数可以使用有序标志位。
213 |
214 | 来看一些随机的数值数据,使用pandas.qcut面元函数。它会返回pandas.Categorical,我们之前使用过pandas.cut,但没解释分类是如何工作的:
215 | ```python
216 | In [41]: np.random.seed(12345)
217 |
218 | In [42]: draws = np.random.randn(1000)
219 |
220 | In [43]: draws[:5]
221 | Out[43]: array([-0.2047, 0.4789, -0.5194, -0.5557, 1.9658])
222 | ```
223 |
224 | 计算这个数据的分位面元,提取一些统计信息:
225 | ```python
226 | In [44]: bins = pd.qcut(draws, 4)
227 |
228 | In [45]: bins
229 | Out[45]:
230 | [(-0.684, -0.0101], (-0.0101, 0.63], (-0.684, -0.0101], (-0.684, -0.0101], (0.63,
231 | 3.928], ..., (-0.0101, 0.63], (-0.684, -0.0101], (-2.95, -0.684], (-0.0101, 0.63
232 | ], (0.63, 3.928]]
233 | Length: 1000
234 | Categories (4, interval[float64]): [(-2.95, -0.684] < (-0.684, -0.0101] < (-0.010
235 | 1, 0.63] <
236 | (0.63, 3.928]]
237 | ```
238 |
239 | 虽然有用,确切的样本分位数与分位的名称相比,不利于生成汇总。我们可以使用labels参数qcut,实现目的:
240 | ```python
241 | In [46]: bins = pd.qcut(draws, 4, labels=['Q1', 'Q2', 'Q3', 'Q4'])
242 |
243 | In [47]: bins
244 | Out[47]:
245 | [Q2, Q3, Q2, Q2, Q4, ..., Q3, Q2, Q1, Q3, Q4]
246 | Length: 1000
247 | Categories (4, object): [Q1 < Q2 < Q3 < Q4]
248 |
249 | In [48]: bins.codes[:10]
250 | Out[48]: array([1, 2, 1, 1, 3, 3, 2, 2, 3, 3], dtype=int8)
251 | ```
252 |
253 | 加上标签的面元分类不包含数据面元边界的信息,因此可以使用groupby提取一些汇总信息:
254 | ```python
255 | In [49]: bins = pd.Series(bins, name='quartile')
256 |
257 | In [50]: results = (pd.Series(draws)
258 | ....: .groupby(bins)
259 | ....: .agg(['count', 'min', 'max'])
260 | ....: .reset_index())
261 |
262 | In [51]: results
263 | Out[51]:
264 | quartile count min max
265 | 0 Q1 250 -2.949343 -0.685484
266 | 1 Q2 250 -0.683066 -0.010115
267 | 2 Q3 250 -0.010032 0.628894
268 | 3 Q4 250 0.634238 3.927528
269 | ```
270 |
271 | 分位数列保存了原始的面元分类信息,包括排序:
272 | ```python
273 | In [52]: results['quartile']
274 | Out[52]:
275 | 0 Q1
276 | 1 Q2
277 | 2 Q3
278 | 3 Q4
279 | Name: quartile, dtype: category
280 | Categories (4, object): [Q1 < Q2 < Q3 < Q4]
281 | ```
282 |
283 | ## 用分类提高性能
284 |
285 | 如果你是在一个特定数据集上做大量分析,将其转换为分类可以极大地提高效率。DataFrame列的分类使用的内存通常少的多。来看一些包含一千万元素的Series,和一些不同的分类:
286 | ```python
287 | In [53]: N = 10000000
288 |
289 | In [54]: draws = pd.Series(np.random.randn(N))
290 |
291 | In [55]: labels = pd.Series(['foo', 'bar', 'baz', 'qux'] * (N // 4))
292 | ```
293 |
294 | 现在,将标签转换为分类:
295 | ```python
296 | In [56]: categories = labels.astype('category')
297 | ```
298 |
299 | 这时,可以看到标签使用的内存远比分类多:
300 | ```python
301 | In [57]: labels.memory_usage()
302 | Out[57]: 80000080
303 |
304 | In [58]: categories.memory_usage()
305 | Out[58]: 10000272
306 | ```
307 |
308 | 转换为分类不是没有代价的,但这是一次性的代价:
309 | ```python
310 | In [59]: %time _ = labels.astype('category')
311 | CPU times: user 490 ms, sys: 240 ms, total: 730 ms
312 | Wall time: 726 ms
313 | ```
314 |
315 | GroupBy使用分类操作明显更快,是因为底层的算法使用整数编码数组,而不是字符串数组。
316 |
317 | ## 分类方法
318 |
319 | 包含分类数据的Series有一些特殊的方法,类似于Series.str字符串方法。它还提供了方便的分类和编码的使用方法。看下面的Series:
320 | ```python
321 | In [60]: s = pd.Series(['a', 'b', 'c', 'd'] * 2)
322 |
323 | In [61]: cat_s = s.astype('category')
324 |
325 | In [62]: cat_s
326 | Out[62]:
327 | 0 a
328 | 1 b
329 | 2 c
330 | 3 d
331 | 4 a
332 | 5 b
333 | 6 c
334 | 7 d
335 | dtype: category
336 | Categories (4, object): [a, b, c, d]
337 | ```
338 |
339 | 特别的cat属性提供了分类方法的入口:
340 | ```python
341 | In [63]: cat_s.cat.codes
342 | Out[63]:
343 | 0 0
344 | 1 1
345 | 2 2
346 | 3 3
347 | 4 0
348 | 5 1
349 | 6 2
350 | 7 3
351 | dtype: int8
352 |
353 | In [64]: cat_s.cat.categories
354 | Out[64]: Index(['a', 'b', 'c', 'd'], dtype='object')
355 | ```
356 |
357 | 假设我们知道这个数据的实际分类集,超出了数据中的四个值。我们可以使用set_categories方法改变它们:
358 | ```python
359 | In [65]: actual_categories = ['a', 'b', 'c', 'd', 'e']
360 |
361 | In [66]: cat_s2 = cat_s.cat.set_categories(actual_categories)
362 |
363 | In [67]: cat_s2
364 | Out[67]:
365 | 0 a
366 | 1 b
367 | 2 c
368 | 3 d
369 | 4 a
370 | 5 b
371 | 6 c
372 | 7 d
373 | dtype: category
374 | Categories (5, object): [a, b, c, d, e]
375 | ```
376 |
377 | 虽然数据看起来没变,新的分类将反映在它们的操作中。例如,如果有的话,value_counts表示分类:
378 | ```python
379 | In [68]: cat_s.value_counts()
380 | Out[68]:
381 | d 2
382 | c 2
383 | b 2
384 | a 2
385 | dtype: int64
386 |
387 | In [69]: cat_s2.value_counts()
388 | Out[69]:
389 | d 2
390 | c 2
391 | b 2
392 | a 2
393 | e 0
394 | dtype: int64
395 | ```
396 |
397 | 在大数据集中,分类经常作为节省内存和高性能的便捷工具。过滤完大DataFrame或Series之后,许多分类可能不会出现在数据中。我们可以使用remove_unused_categories方法删除没看到的分类:
398 | ```python
399 | In [70]: cat_s3 = cat_s[cat_s.isin(['a', 'b'])]
400 |
401 | In [71]: cat_s3
402 | Out[71]:
403 | 0 a
404 | 1 b
405 | 4 a
406 | 5 b
407 | dtype: category
408 | Categories (4, object): [a, b, c, d]
409 |
410 | In [72]: cat_s3.cat.remove_unused_categories()
411 | Out[72]:
412 | 0 a
413 | 1 b
414 | 4 a
415 | 5 b
416 | dtype: category
417 | Categories (2, object): [a, b]
418 | ```
419 |
420 | 表12-1列出了可用的分类方法。
421 |
422 | 
423 |
424 |
425 | ## 为建模创建虚拟变量
426 |
427 | 当你使用统计或机器学习工具时,通常会将分类数据转换为虚拟变量,也称为one-hot编码。这包括创建一个不同类别的列的DataFrame;这些列包含给定分类的1s,其它为0。
428 |
429 | 看前面的例子:
430 | ```python
431 | In [73]: cat_s = pd.Series(['a', 'b', 'c', 'd'] * 2, dtype='category')
432 | ```
433 |
434 | 前面的第7章提到过,pandas.get_dummies函数可以转换这个分类数据为包含虚拟变量的DataFrame:
435 | ```python
436 | In [74]: pd.get_dummies(cat_s)
437 | Out[74]:
438 | a b c d
439 | 0 1 0 0 0
440 | 1 0 1 0 0
441 | 2 0 0 1 0
442 | 3 0 0 0 1
443 | 4 1 0 0 0
444 | 5 0 1 0 0
445 | 6 0 0 1 0
446 | 7 0 0 0 1
447 | ```
448 |
449 | # 12.2 GroupBy高级应用
450 |
451 | 尽管我们在第10章已经深度学习了Series和DataFrame的Groupby方法,还有一些方法也是很有用的。
452 |
453 | ## 分组转换和“解封”GroupBy
454 |
455 | 在第10章,我们在分组操作中学习了apply方法,进行转换。还有另一个transform方法,它与apply很像,但是对使用的函数有一定限制:
456 |
457 | - 它可以产生向分组形状广播标量值
458 | - 它可以产生一个和输入组形状相同的对象
459 | - 它不能修改输入
460 |
461 | 来看一个简单的例子:
462 | ```python
463 | In [75]: df = pd.DataFrame({'key': ['a', 'b', 'c'] * 4,
464 | ....: 'value': np.arange(12.)})
465 |
466 | In [76]: df
467 | Out[76]:
468 | key value
469 | 0 a 0.0
470 | 1 b 1.0
471 | 2 c 2.0
472 | 3 a 3.0
473 | 4 b 4.0
474 | 5 c 5.0
475 | 6 a 6.0
476 | 7 b 7.0
477 | 8 c 8.0
478 | 9 a 9.0
479 | 10 b 10.0
480 | 11 c 11.0
481 | ```
482 |
483 | 按键进行分组:
484 | ```python
485 | In [77]: g = df.groupby('key').value
486 |
487 | In [78]: g.mean()
488 | Out[78]:
489 | key
490 | a 4.5
491 | b 5.5
492 | c 6.5
493 | Name: value, dtype: float64
494 | ```
495 |
496 | 假设我们想产生一个和df['value']形状相同的Series,但值替换为按键分组的平均值。我们可以传递函数lambda x: x.mean()进行转换:
497 | ```python
498 | In [79]: g.transform(lambda x: x.mean())
499 | Out[79]:
500 | 0 4.5
501 | 1 5.5
502 | 2 6.5
503 | 3 4.5
504 | 4 5.5
505 | 5 6.5
506 | 6 4.5
507 | 7 5.5
508 | 8 6.5
509 | 9 4.5
510 | 10 5.5
511 | 11 6.5
512 | Name: value, dtype: float64
513 | ```
514 |
515 | 对于内置的聚合函数,我们可以传递一个字符串假名作为GroupBy的agg方法:
516 | ```python
517 | In [80]: g.transform('mean')
518 | Out[80]:
519 | 0 4.5
520 | 1 5.5
521 | 2 6.5
522 | 3 4.5
523 | 4 5.5
524 | 5 6.5
525 | 6 4.5
526 | 7 5.5
527 | 8 6.5
528 | 9 4.5
529 | 10 5.5
530 | 11 6.5
531 | Name: value, dtype: float64
532 | ```
533 |
534 | 与apply类似,transform的函数会返回Series,但是结果必须与输入大小相同。举个例子,我们可以用lambda函数将每个分组乘以2:
535 | ```python
536 | In [81]: g.transform(lambda x: x * 2)
537 | Out[81]:
538 | 0 0.0
539 | 1 2.0
540 | 2 4.0
541 | 3 6.0
542 | 4 8.0
543 | 5 10.0
544 | 6 12.0
545 | 7 14.0
546 | 8 16.0
547 | 9 18.0
548 | 10 20.0
549 | 11 22.0
550 | Name: value, dtype: float64
551 | ```
552 |
553 | 再举一个复杂的例子,我们可以计算每个分组的降序排名:
554 | ```python
555 | In [82]: g.transform(lambda x: x.rank(ascending=False))
556 | Out[82]:
557 | 0 4.0
558 | 1 4.0
559 | 2 4.0
560 | 3 3.0
561 | 4 3.0
562 | 5 3.0
563 | 6 2.0
564 | 7 2.0
565 | 8 2.0
566 | 9 1.0
567 | 10 1.0
568 | 11 1.0
569 | Name: value, dtype: float64
570 | ```
571 |
572 | 看一个由简单聚合构造的的分组转换函数:
573 | ```python
574 | def normalize(x):
575 | return (x - x.mean()) / x.std()
576 | ```
577 |
578 | 我们用transform或apply可以获得等价的结果:
579 | ```python
580 | In [84]: g.transform(normalize)
581 | Out[84]:
582 | 0 -1.161895
583 | 1 -1.161895
584 | 2 -1.161895
585 | 3 -0.387298
586 | 4 -0.387298
587 | 5 -0.387298
588 | 6 0.387298
589 | 7 0.387298
590 | 8 0.387298
591 | 9 1.161895
592 | 10 1.161895
593 | 11 1.161895
594 | Name: value, dtype: float64
595 |
596 | In [85]: g.apply(normalize)
597 | Out[85]:
598 | 0 -1.161895
599 | 1 -1.161895
600 | 2 -1.161895
601 | 3 -0.387298
602 | 4 -0.387298
603 | 5 -0.387298
604 | 6 0.387298
605 | 7 0.387298
606 | 8 0.387298
607 | 9 1.161895
608 | 10 1.161895
609 | 11 1.161895
610 | Name: value, dtype: float64
611 | ```
612 |
613 | 内置的聚合函数,比如mean或sum,通常比apply函数快,也比transform快。这允许我们进行一个所谓的解封(unwrapped)分组操作:
614 | ```python
615 | In [86]: g.transform('mean')
616 | Out[86]:
617 | 0 4.5
618 | 1 5.5
619 | 2 6.5
620 | 3 4.5
621 | 4 5.5
622 | 5 6.5
623 | 6 4.5
624 | 7 5.5
625 | 8 6.5
626 | 9 4.5
627 | 10 5.5
628 | 11 6.5
629 | Name: value, dtype: float64
630 |
631 | In [87]: normalized = (df['value'] - g.transform('mean')) / g.transform('std')
632 |
633 | In [88]: normalized
634 | Out[88]:
635 | 0 -1.161895
636 | 1 -1.161895
637 | 2 -1.161895
638 | 3 -0.387298
639 | 4 -0.387298
640 | 5 -0.387298
641 | 6 0.387298
642 | 7 0.387298
643 | 8 0.387298
644 | 9 1.161895
645 | 10 1.161895
646 | 11 1.161895
647 | Name: value, dtype: float64
648 | ```
649 |
650 | 解封分组操作可能包括多个分组聚合,但是矢量化操作还是会带来收益。
651 |
652 | ## 分组的时间重采样
653 |
654 | 对于时间序列数据,resample方法从语义上是一个基于内在时间的分组操作。下面是一个示例表:
655 | ```python
656 | In [89]: N = 15
657 |
658 | In [90]: times = pd.date_range('2017-05-20 00:00', freq='1min', periods=N)
659 |
660 | In [91]: df = pd.DataFrame({'time': times,
661 | ....: 'value': np.arange(N)})
662 |
663 | In [92]: df
664 | Out[92]:
665 | time value
666 | 0 2017-05-20 00:00:00 0
667 | 1 2017-05-20 00:01:00 1
668 | 2 2017-05-20 00:02:00 2
669 | 3 2017-05-20 00:03:00 3
670 | 4 2017-05-20 00:04:00 4
671 | 5 2017-05-20 00:05:00 5
672 | 6 2017-05-20 00:06:00 6
673 | 7 2017-05-20 00:07:00 7
674 | 8 2017-05-20 00:08:00 8
675 | 9 2017-05-20 00:09:00 9
676 | 10 2017-05-20 00:10:00 10
677 | 11 2017-05-20 00:11:00 11
678 | 12 2017-05-20 00:12:00 12
679 | 13 2017-05-20 00:13:00 13
680 | 14 2017-05-20 00:14:00 14
681 | ```
682 |
683 | 这里,我们可以用time作为索引,然后重采样:
684 | ```python
685 | In [93]: df.set_index('time').resample('5min').count()
686 | Out[93]:
687 | value
688 | time
689 | 2017-05-20 00:00:00 5
690 | 2017-05-20 00:05:00 5
691 | 2017-05-20 00:10:00 5
692 | ```
693 |
694 | 假设DataFrame包含多个时间序列,用一个额外的分组键的列进行标记:
695 | ```python
696 | In [94]: df2 = pd.DataFrame({'time': times.repeat(3),
697 | ....: 'key': np.tile(['a', 'b', 'c'], N),
698 | ....: 'value': np.arange(N * 3.)})
699 |
700 | In [95]: df2[:7]
701 | Out[95]:
702 | key time value
703 | 0 a 2017-05-20 00:00:00 0.0
704 | 1 b 2017-05-20 00:00:00 1.0
705 | 2 c 2017-05-20 00:00:00 2.0
706 | 3 a 2017-05-20 00:01:00 3.0
707 | 4 b 2017-05-20 00:01:00 4.0
708 | 5 c 2017-05-20 00:01:00 5.0
709 | 6 a 2017-05-20 00:02:00 6.0
710 | ```
711 |
712 | 要对每个key值进行相同的重采样,我们引入pandas.TimeGrouper对象:
713 | ```python
714 | In [96]: time_key = pd.TimeGrouper('5min')
715 | ```
716 |
717 | 我们然后设定时间索引,用key和time_key分组,然后聚合:
718 | ```python
719 | In [97]: resampled = (df2.set_index('time')
720 | ....: .groupby(['key', time_key])
721 | ....: .sum())
722 |
723 | In [98]: resampled
724 | Out[98]:
725 | value
726 | key time
727 | a 2017-05-20 00:00:00 30.0
728 | 2017-05-20 00:05:00 105.0
729 | 2017-05-20 00:10:00 180.0
730 | b 2017-05-20 00:00:00 35.0
731 | 2017-05-20 00:05:00 110.0
732 | 2017-05-20 00:10:00 185.0
733 | c 2017-05-20 00:00:00 40.0
734 | 2017-05-20 00:05:00 115.0
735 | 2017-05-20 00:10:00 190.0
736 |
737 | In [99]: resampled.reset_index()
738 | Out[99]:
739 | key time value
740 | 0 a 2017-05-20 00:00:00 30.0
741 | 1 a 2017-05-20 00:05:00 105.0
742 | 2 a 2017-05-20 00:10:00 180.0
743 | 3 b 2017-05-20 00:00:00 35.0
744 | 4 b 2017-05-20 00:05:00 110.0
745 | 5 b 2017-05-20 00:10:00 185.0
746 | 6 c 2017-05-20 00:00:00 40.0
747 | 7 c 2017-05-20 00:05:00 115.0
748 | 8 c 2017-05-20 00:10:00 190.0
749 | ```
750 |
751 | 使用TimeGrouper的限制是时间必须是Series或DataFrame的索引。
752 |
753 | # 12.3 链式编程技术
754 |
755 | 当对数据集进行一系列变换时,你可能发现创建的多个临时变量其实并没有在分析中用到。看下面的例子:
756 | ```python
757 | df = load_data()
758 | df2 = df[df['col2'] < 0]
759 | df2['col1_demeaned'] = df2['col1'] - df2['col1'].mean()
760 | result = df2.groupby('key').col1_demeaned.std()
761 | ```
762 |
763 | 虽然这里没有使用真实的数据,这个例子却指出了一些新方法。首先,DataFrame.assign方法是一个df[k] = v形式的函数式的列分配方法。它不是就地修改对象,而是返回新的修改过的DataFrame。因此,下面的语句是等价的:
764 | ```python
765 | # Usual non-functional way
766 | df2 = df.copy()
767 | df2['k'] = v
768 |
769 | # Functional assign way
770 | df2 = df.assign(k=v)
771 | ```
772 |
773 | 就地分配可能会比assign快,但是assign可以方便地进行链式编程:
774 | ```python
775 | result = (df2.assign(col1_demeaned=df2.col1 - df2.col2.mean())
776 | .groupby('key')
777 | .col1_demeaned.std())
778 | ```
779 |
780 | 我使用外括号,这样便于添加换行符。
781 |
782 | 使用链式编程时要注意,你可能会需要涉及临时对象。在前面的例子中,我们不能使用load_data的结果,直到它被赋值给临时变量df。为了这么做,assign和许多其它pandas函数可以接收类似函数的参数,即可调用对象(callable)。为了展示可调用对象,看一个前面例子的片段:
783 | ```python
784 | df = load_data()
785 | df2 = df[df['col2'] < 0]
786 | ```
787 |
788 | 它可以重写为:
789 | ```python
790 | df = (load_data()
791 | [lambda x: x['col2'] < 0])
792 | ```
793 |
794 | 这里,load_data的结果没有赋值给某个变量,因此传递到[ ]的函数在这一步被绑定到了对象。
795 |
796 | 我们可以把整个过程写为一个单链表达式:
797 | ```python
798 | result = (load_data()
799 | [lambda x: x.col2 < 0]
800 | .assign(col1_demeaned=lambda x: x.col1 - x.col1.mean())
801 | .groupby('key')
802 | .col1_demeaned.std())
803 | ```
804 |
805 | 是否将代码写成这种形式只是习惯而已,将它分开成若干步可以提高可读性。
806 |
807 | ## 管道方法
808 |
809 | 你可以用Python内置的pandas函数和方法,用带有可调用对象的链式编程做许多工作。但是,有时你需要使用自己的函数,或是第三方库的函数。这时就要用到管道方法。
810 |
811 | 看下面的函数调用:
812 | ```python
813 | a = f(df, arg1=v1)
814 | b = g(a, v2, arg3=v3)
815 | c = h(b, arg4=v4)
816 | ```
817 |
818 | 当使用接收、返回Series或DataFrame对象的函数式,你可以调用pipe将其重写:
819 | ```python
820 | result = (df.pipe(f, arg1=v1)
821 | .pipe(g, v2, arg3=v3)
822 | .pipe(h, arg4=v4))
823 | ```
824 |
825 | f(df)和df.pipe(f)是等价的,但是pipe使得链式声明更容易。
826 |
827 | pipe的另一个有用的地方是提炼操作为可复用的函数。看一个从列减去分组方法的例子:
828 | ```python
829 | g = df.groupby(['key1', 'key2'])
830 | df['col1'] = df['col1'] - g.transform('mean')
831 | ```
832 |
833 | 假设你想转换多列,并修改分组的键。另外,你想用链式编程做这个转换。下面就是一个方法:
834 | ```python
835 | def group_demean(df, by, cols):
836 | result = df.copy()
837 | g = df.groupby(by)
838 | for c in cols:
839 | result[c] = df[c] - g[c].transform('mean')
840 | return result
841 | ```
842 |
843 | 然后可以写为:
844 | ```python
845 | result = (df[df.col1 < 0]
846 | .pipe(group_demean, ['key1', 'key2'], ['col1']))
847 | ```
848 |
849 | # 12.4 总结
850 |
851 | 和其它许多开源项目一样,pandas仍然在不断的变化和进步中。和本书中其它地方一样,这里的重点是放在接下来几年不会发生什么改变且稳定的功能。
852 |
853 | 为了深入学习pandas的知识,我建议你学习官方文档,并阅读开发团队发布的文档更新。我们还邀请你加入pandas的开发工作:修改bug、创建新功能、完善文档。
854 |
--------------------------------------------------------------------------------
/第13章 Python建模库介绍.md:
--------------------------------------------------------------------------------
1 | 本书中,我已经介绍了Python数据分析的编程基础。因为数据分析师和科学家总是在数据规整和准备上花费大量时间,这本书的重点在于掌握这些功能。
2 |
3 | 开发模型选用什么库取决于应用本身。许多统计问题可以用简单方法解决,比如普通的最小二乘回归,其它问题可能需要复杂的机器学习方法。幸运的是,Python已经成为了运用这些分析方法的语言之一,因此读完此书,你可以探索许多工具。
4 |
5 | 本章中,我会回顾一些pandas的特点,在你胶着于pandas数据规整和模型拟合和评分时,它们可能派上用场。然后我会简短介绍两个流行的建模工具,statsmodels和scikit-learn。这二者每个都值得再写一本书,我就不做全面的介绍,而是建议你学习两个项目的线上文档和其它基于Python的数据科学、统计和机器学习的书籍。
6 |
7 | # 13.1 pandas与模型代码的接口
8 |
9 | 模型开发的通常工作流是使用pandas进行数据加载和清洗,然后切换到建模库进行建模。开发模型的重要一环是机器学习中的“特征工程”。它可以描述从原始数据集中提取信息的任何数据转换或分析,这些数据集可能在建模中有用。本书中学习的数据聚合和GroupBy工具常用于特征工程中。
10 |
11 | 优秀的特征工程超出了本书的范围,我会尽量直白地介绍一些用于数据操作和建模切换的方法。
12 |
13 | pandas与其它分析库通常是靠NumPy的数组联系起来的。将DataFrame转换为NumPy数组,可以使用.values属性:
14 | ```python
15 | In [10]: import pandas as pd
16 |
17 | In [11]: import numpy as np
18 |
19 | In [12]: data = pd.DataFrame({
20 | ....: 'x0': [1, 2, 3, 4, 5],
21 | ....: 'x1': [0.01, -0.01, 0.25, -4.1, 0.],
22 | ....: 'y': [-1.5, 0., 3.6, 1.3, -2.]})
23 |
24 | In [13]: data
25 | Out[13]:
26 | x0 x1 y
27 | 0 1 0.01 -1.5
28 | 1 2 -0.01 0.0
29 | 2 3 0.25 3.6
30 | 3 4 -4.10 1.3
31 | 4 5 0.00 -2.0
32 |
33 | In [14]: data.columns
34 | Out[14]: Index(['x0', 'x1', 'y'], dtype='object')
35 |
36 | In [15]: data.values
37 | Out[15]:
38 | array([[ 1. , 0.01, -1.5 ],
39 | [ 2. , -0.01, 0. ],
40 | [ 3. , 0.25, 3.6 ],
41 | [ 4. , -4.1 , 1.3 ],
42 | [ 5. , 0. , -2. ]])
43 | ```
44 |
45 | 要转换回DataFrame,可以传递一个二维ndarray,可带有列名:
46 | ```python
47 | In [16]: df2 = pd.DataFrame(data.values, columns=['one', 'two', 'three'])
48 |
49 | In [17]: df2
50 | Out[17]:
51 | one two three
52 | 0 1.0 0.01 -1.5
53 | 1 2.0 -0.01 0.0
54 | 2 3.0 0.25 3.6
55 | 3 4.0 -4.10 1.3
56 | 4 5.0 0.00 -2.0
57 | ```
58 |
59 | >笔记:最好当数据是均匀的时候使用.values属性。例如,全是数值类型。如果数据是不均匀的,结果会是Python对象的ndarray:
60 | >```python
61 | >In [18]: df3 = data.copy()
62 | >
63 | >In [19]: df3['strings'] = ['a', 'b', 'c', 'd', 'e']
64 | >
65 | >In [20]: df3
66 | >Out[20]:
67 | > x0 x1 y strings
68 | >0 1 0.01 -1.5 a
69 | >1 2 -0.01 0.0 b
70 | >2 3 0.25 3.6 c
71 | >3 4 -4.10 1.3 d
72 | >4 5 0.00 -2.0 e
73 | >
74 | >In [21]: df3.values
75 | >Out[21]:
76 | >array([[1, 0.01, -1.5, 'a'],
77 | > [2, -0.01, 0.0, 'b'],
78 | > [3, 0.25, 3.6, 'c'],
79 | > [4, -4.1, 1.3, 'd'],
80 | > [5, 0.0, -2.0, 'e']], dtype=object)
81 | >```
82 |
83 | 对于一些模型,你可能只想使用列的子集。我建议你使用loc,用values作索引:
84 | ```python
85 | In [22]: model_cols = ['x0', 'x1']
86 |
87 | In [23]: data.loc[:, model_cols].values
88 | Out[23]:
89 | array([[ 1. , 0.01],
90 | [ 2. , -0.01],
91 | [ 3. , 0.25],
92 | [ 4. , -4.1 ],
93 | [ 5. , 0. ]])
94 | ```
95 |
96 | 一些库原生支持pandas,会自动完成工作:从DataFrame转换到NumPy,将模型的参数名添加到输出表的列或Series。其它情况,你可以手工进行“元数据管理”。
97 |
98 | 在第12章,我们学习了pandas的Categorical类型和pandas.get_dummies函数。假设数据集中有一个非数值列:
99 | ```python
100 | In [24]: data['category'] = pd.Categorical(['a', 'b', 'a', 'a', 'b'],
101 | ....: categories=['a', 'b'])
102 |
103 | In [25]: data
104 | Out[25]:
105 | x0 x1 y category
106 | 0 1 0.01 -1.5 a
107 | 1 2 -0.01 0.0 b
108 | 2 3 0.25 3.6 a
109 | 3 4 -4.10 1.3 a
110 | 4 5 0.00 -2.0 b
111 | ```
112 |
113 | 如果我们想替换category列为虚变量,我们可以创建虚变量,删除category列,然后添加到结果:
114 | ```python
115 | In [26]: dummies = pd.get_dummies(data.category, prefix='category')
116 |
117 | In [27]: data_with_dummies = data.drop('category', axis=1).join(dummies)
118 |
119 | In [28]: data_with_dummies
120 | Out[28]:
121 | x0 x1 y category_a category_b
122 | 0 1 0.01 -1.5 1 0
123 | 1 2 -0.01 0.0 0 1
124 | 2 3 0.25 3.6 1 0
125 | 3 4 -4.10 1.3 1 0
126 | 4 5 0.00 -2.0 0 1
127 | ```
128 |
129 | 用虚变量拟合某些统计模型会有一些细微差别。当你不只有数字列时,使用Patsy(下一节的主题)可能更简单,更不容易出错。
130 |
131 | # 13.2 用Patsy创建模型描述
132 |
133 | Patsy是Python的一个库,使用简短的字符串“公式语法”描述统计模型(尤其是线性模型),可能是受到了R和S统计编程语言的公式语法的启发。
134 |
135 | Patsy适合描述statsmodels的线性模型,因此我会关注于它的主要特点,让你尽快掌握。Patsy的公式是一个特殊的字符串语法,如下所示:
136 | ```python
137 | y ~ x0 + x1
138 | ```
139 |
140 | a+b不是将a与b相加的意思,而是为模型创建的设计矩阵。patsy.dmatrices函数接收一个公式字符串和一个数据集(可以是DataFrame或数组的字典),为线性模型创建设计矩阵:
141 | ```python
142 | In [29]: data = pd.DataFrame({
143 | ....: 'x0': [1, 2, 3, 4, 5],
144 | ....: 'x1': [0.01, -0.01, 0.25, -4.1, 0.],
145 | ....: 'y': [-1.5, 0., 3.6, 1.3, -2.]})
146 |
147 | In [30]: data
148 | Out[30]:
149 | x0 x1 y
150 | 0 1 0.01 -1.5
151 | 1 2 -0.01 0.0
152 | 2 3 0.25 3.6
153 | 3 4 -4.10 1.3
154 | 4 5 0.00 -2.0
155 |
156 | In [31]: import patsy
157 |
158 | In [32]: y, X = patsy.dmatrices('y ~ x0 + x1', data)
159 | ```
160 |
161 | 现在有:
162 | ```python
163 | In [33]: y
164 | Out[33]:
165 | DesignMatrix with shape (5, 1)
166 | y
167 | -1.5
168 | 0.0
169 | 3.6
170 | 1.3
171 | -2.0
172 | Terms:
173 | 'y' (column 0)
174 |
175 | In [34]: X
176 | Out[34]:
177 | DesignMatrix with shape (5, 3)
178 | Intercept x0 x1
179 | 1 1 0.01
180 | 1 2 -0.01
181 | 1 3 0.25
182 | 1 4 -4.10
183 | 1 5 0.00
184 | Terms:
185 | 'Intercept' (column 0)
186 | 'x0' (column 1)
187 | 'x1' (column 2)
188 | ```
189 |
190 | 这些Patsy的DesignMatrix实例是NumPy的ndarray,带有附加元数据:
191 | ```python
192 | In [35]: np.asarray(y)
193 | Out[35]:
194 | array([[-1.5],
195 | [ 0. ],
196 | [ 3.6],
197 | [ 1.3],
198 | [-2. ]])
199 |
200 | In [36]: np.asarray(X)
201 | Out[36]:
202 | array([[ 1. , 1. , 0.01],
203 | [ 1. , 2. , -0.01],
204 | [ 1. , 3. , 0.25],
205 | [ 1. , 4. , -4.1 ],
206 | [ 1. , 5. , 0. ]])
207 | ```
208 |
209 | 你可能想Intercept是哪里来的。这是线性模型(比如普通最小二乘回归)的惯例用法。添加 +0 到模型可以不显示intercept:
210 | ```python
211 | In [37]: patsy.dmatrices('y ~ x0 + x1 + 0', data)[1]
212 | Out[37]:
213 | DesignMatrix with shape (5, 2)
214 | x0 x1
215 | 1 0.01
216 | 2 -0.01
217 | 3 0.25
218 | 4 -4.10
219 | 5 0.00
220 | Terms:
221 | 'x0' (column 0)
222 | 'x1' (column 1)
223 | ```
224 |
225 | Patsy对象可以直接传递到算法(比如numpy.linalg.lstsq)中,它执行普通最小二乘回归:
226 | ```python
227 | In [38]: coef, resid, _, _ = np.linalg.lstsq(X, y)
228 | ```
229 |
230 | 模型的元数据保留在design_info属性中,因此你可以重新附加列名到拟合系数,以获得一个Series,例如:
231 | ```python
232 | In [39]: coef
233 | Out[39]:
234 | array([[ 0.3129],
235 | [-0.0791],
236 | [-0.2655]])
237 |
238 | In [40]: coef = pd.Series(coef.squeeze(), index=X.design_info.column_names)
239 |
240 | In [41]: coef
241 | Out[41]:
242 | Intercept 0.312910
243 | x0 -0.079106
244 | x1 -0.265464
245 | dtype: float64
246 | ```
247 |
248 | ## 用Patsy公式进行数据转换
249 |
250 | 你可以将Python代码与patsy公式结合。在评估公式时,库将尝试查找在封闭作用域内使用的函数:
251 | ```python
252 | In [42]: y, X = patsy.dmatrices('y ~ x0 + np.log(np.abs(x1) + 1)', data)
253 |
254 | In [43]: X
255 | Out[43]:
256 | DesignMatrix with shape (5, 3)
257 | Intercept x0 np.log(np.abs(x1) + 1)
258 | 1 1 0.00995
259 | 1 2 0.00995
260 | 1 3 0.22314
261 | 1 4 1.62924
262 | 1 5 0.00000
263 | Terms:
264 | 'Intercept' (column 0)
265 | 'x0' (column 1)
266 | 'np.log(np.abs(x1) + 1)' (column 2)
267 | ```
268 |
269 | 常见的变量转换包括标准化(平均值为0,方差为1)和中心化(减去平均值)。Patsy有内置的函数进行这样的工作:
270 | ```python
271 | In [44]: y, X = patsy.dmatrices('y ~ standardize(x0) + center(x1)', data)
272 |
273 | In [45]: X
274 | Out[45]:
275 | DesignMatrix with shape (5, 3)
276 | Intercept standardize(x0) center(x1)
277 | 1 -1.41421 0.78
278 | 1 -0.70711 0.76
279 | 1 0.00000 1.02
280 | 1 0.70711 -3.33
281 | 1 1.41421 0.77
282 | Terms:
283 | 'Intercept' (column 0)
284 | 'standardize(x0)' (column 1)
285 | 'center(x1)' (column 2)
286 | ```
287 |
288 | 作为建模的一步,你可能拟合模型到一个数据集,然后用另一个数据集评估模型。另一个数据集可能是剩余的部分或是新数据。当执行中心化和标准化转变,用新数据进行预测要格外小心。因为你必须使用平均值或标准差转换新数据集,这也称作状态转换。
289 |
290 | patsy.build_design_matrices函数可以使用原始样本数据集的保存信息,来转换新数据,:
291 | ```python
292 | In [46]: new_data = pd.DataFrame({
293 | ....: 'x0': [6, 7, 8, 9],
294 | ....: 'x1': [3.1, -0.5, 0, 2.3],
295 | ....: 'y': [1, 2, 3, 4]})
296 |
297 | In [47]: new_X = patsy.build_design_matrices([X.design_info], new_data)
298 |
299 | In [48]: new_X
300 | Out[48]:
301 | [DesignMatrix with shape (4, 3)
302 | Intercept standardize(x0) center(x1)
303 | 1 2.12132 3.87
304 | 1 2.82843 0.27
305 | 1 3.53553 0.77
306 | 1 4.24264 3.07
307 | Terms:
308 | 'Intercept' (column 0)
309 | 'standardize(x0)' (column 1)
310 | 'center(x1)' (column 2)]
311 | ```
312 |
313 | 因为Patsy中的加号不是加法的意义,当你按照名称将数据集的列相加时,你必须用特殊I函数将它们封装起来:
314 | ```python
315 | In [49]: y, X = patsy.dmatrices('y ~ I(x0 + x1)', data)
316 |
317 | In [50]: X
318 | Out[50]:
319 | DesignMatrix with shape (5, 2)
320 | Intercept I(x0 + x1)
321 | 1 1.01
322 | 1 1.99
323 | 1 3.25
324 | 1 -0.10
325 | 1 5.00
326 | Terms:
327 | 'Intercept' (column 0)
328 | 'I(x0 + x1)' (column 1)
329 | ```
330 |
331 | Patsy的patsy.builtins模块还有一些其它的内置转换。请查看线上文档。
332 |
333 | 分类数据有一个特殊的转换类,下面进行讲解。
334 |
335 | ## 分类数据和Patsy
336 |
337 | 非数值数据可以用多种方式转换为模型设计矩阵。完整的讲解超出了本书范围,最好和统计课一起学习。
338 |
339 | 当你在Patsy公式中使用非数值数据,它们会默认转换为虚变量。如果有截距,会去掉一个,避免共线性:
340 | ```python
341 | In [51]: data = pd.DataFrame({
342 | ....: 'key1': ['a', 'a', 'b', 'b', 'a', 'b', 'a', 'b'],
343 | ....: 'key2': [0, 1, 0, 1, 0, 1, 0, 0],
344 | ....: 'v1': [1, 2, 3, 4, 5, 6, 7, 8],
345 | ....: 'v2': [-1, 0, 2.5, -0.5, 4.0, -1.2, 0.2, -1.7]
346 | ....: })
347 |
348 | In [52]: y, X = patsy.dmatrices('v2 ~ key1', data)
349 |
350 | In [53]: X
351 | Out[53]:
352 | DesignMatrix with shape (8, 2)
353 | Intercept key1[T.b]
354 | 1 0
355 | 1 0
356 | 1 1
357 | 1 1
358 | 1 0
359 | 1 1
360 | 1 0
361 | 1 1
362 | Terms:
363 | 'Intercept' (column 0)
364 | 'key1' (column 1)
365 | ```
366 |
367 | 如果你从模型中忽略截距,每个分类值的列都会包括在设计矩阵的模型中:
368 | ```python
369 | In [54]: y, X = patsy.dmatrices('v2 ~ key1 + 0', data)
370 |
371 | In [55]: X
372 | Out[55]:
373 | DesignMatrix with shape (8, 2)
374 | key1[a] key1[b]
375 | 1 0
376 | 1 0
377 | 0 1
378 | 0 1
379 | 1 0
380 | 0 1
381 | 1 0
382 | 0 1
383 | Terms:
384 | 'key1' (columns 0:2)
385 | ```
386 |
387 | 使用C函数,数值列可以截取为分类量:
388 | ```python
389 | In [56]: y, X = patsy.dmatrices('v2 ~ C(key2)', data)
390 |
391 | In [57]: X
392 | Out[57]:
393 | DesignMatrix with shape (8, 2)
394 | Intercept C(key2)[T.1]
395 | 1 0
396 | 1 1
397 | 1 0
398 | 1 1
399 | 1 0
400 | 1 1
401 | 1 0
402 | 1 0
403 | Terms:
404 | 'Intercept' (column 0)
405 | 'C(key2)' (column 1)
406 | ```
407 |
408 | 当你在模型中使用多个分类名,事情就会变复杂,因为会包括key1:key2形式的相交部分,它可以用在方差(ANOVA)模型分析中:
409 | ```python
410 | In [58]: data['key2'] = data['key2'].map({0: 'zero', 1: 'one'})
411 |
412 | In [59]: data
413 | Out[59]:
414 | key1 key2 v1 v2
415 | 0 a zero 1 -1.0
416 | 1 a one 2 0.0
417 | 2 b zero 3 2.5
418 | 3 b one 4 -0.5
419 | 4 a zero 5 4.0
420 | 5 b one 6 -1.2
421 | 6 a zero 7 0.2
422 | 7 b zero 8 -1.7
423 |
424 | In [60]: y, X = patsy.dmatrices('v2 ~ key1 + key2', data)
425 |
426 | In [61]: X
427 | Out[61]:
428 | DesignMatrix with shape (8, 3)
429 | Intercept key1[T.b] key2[T.zero]
430 | 1 0 1
431 | 1 0 0
432 | 1 1 1
433 | 1 1 0
434 | 1 0 1
435 | 1 1 0
436 | 1 0 1
437 | 1 1 1
438 | Terms:
439 | 'Intercept' (column 0)
440 | 'key1' (column 1)
441 | 'key2' (column 2)
442 |
443 | In [62]: y, X = patsy.dmatrices('v2 ~ key1 + key2 + key1:key2', data)
444 |
445 | In [63]: X
446 | Out[63]:
447 | DesignMatrix with shape (8, 4)
448 | Intercept key1[T.b] key2[T.zero]
449 | key1[T.b]:key2[T.zero]
450 | 1 0 1 0
451 | 1 0 0 0
452 | 1 1 1 1
453 | 1 1 0 0
454 | 1 0 1 0
455 | 1 1 0 0
456 | 1 0 1 0
457 | 1 1 1 1
458 | Terms:
459 | 'Intercept' (column 0)
460 | 'key1' (column 1)
461 | 'key2' (column 2)
462 | 'key1:key2' (column 3)
463 | ```
464 |
465 | Patsy提供转换分类数据的其它方法,包括以特定顺序转换。请参阅线上文档。
466 |
467 | # 13.3 statsmodels介绍
468 |
469 | statsmodels是Python进行拟合多种统计模型、进行统计试验和数据探索可视化的库。Statsmodels包含许多经典的统计方法,但没有贝叶斯方法和机器学习模型。
470 |
471 | statsmodels包含的模型有:
472 |
473 | - 线性模型,广义线性模型和健壮线性模型
474 | - 线性混合效应模型
475 | - 方差(ANOVA)方法分析
476 | - 时间序列过程和状态空间模型
477 | - 广义矩估计
478 |
479 | 下面,我会使用一些基本的statsmodels工具,探索Patsy公式和pandasDataFrame对象如何使用模型接口。
480 |
481 | ## 估计线性模型
482 |
483 | statsmodels有多种线性回归模型,包括从基本(比如普通最小二乘)到复杂(比如迭代加权最小二乘法)的。
484 |
485 | statsmodels的线性模型有两种不同的接口:基于数组和基于公式。它们可以通过API模块引入:
486 | ```python
487 | import statsmodels.api as sm
488 | import statsmodels.formula.api as smf
489 | ```
490 |
491 | 为了展示它们的使用方法,我们从一些随机数据生成一个线性模型:
492 | ```python
493 | def dnorm(mean, variance, size=1):
494 | if isinstance(size, int):
495 | size = size,
496 | return mean + np.sqrt(variance) * np.random.randn(*size)
497 |
498 | # For reproducibility
499 | np.random.seed(12345)
500 |
501 | N = 100
502 | X = np.c_[dnorm(0, 0.4, size=N),
503 | dnorm(0, 0.6, size=N),
504 | dnorm(0, 0.2, size=N)]
505 | eps = dnorm(0, 0.1, size=N)
506 | beta = [0.1, 0.3, 0.5]
507 |
508 | y = np.dot(X, beta) + eps
509 | ```
510 |
511 | 这里,我使用了“真实”模型和可知参数beta。此时,dnorm可用来生成正态分布数据,带有特定均值和方差。现在有:
512 | ```python
513 | In [66]: X[:5]
514 | Out[66]:
515 | array([[-0.1295, -1.2128, 0.5042],
516 | [ 0.3029, -0.4357, -0.2542],
517 | [-0.3285, -0.0253, 0.1384],
518 | [-0.3515, -0.7196, -0.2582],
519 | [ 1.2433, -0.3738, -0.5226]])
520 |
521 | In [67]: y[:5]
522 | Out[67]: array([ 0.4279, -0.6735, -0.0909, -0.4895,-0.1289])
523 | ```
524 |
525 | 像之前Patsy看到的,线性模型通常要拟合一个截距。sm.add_constant函数可以添加一个截距的列到现存的矩阵:
526 | ```python
527 | In [68]: X_model = sm.add_constant(X)
528 |
529 | In [69]: X_model[:5]
530 | Out[69]:
531 | array([[ 1. , -0.1295, -1.2128, 0.5042],
532 | [ 1. , 0.3029, -0.4357, -0.2542],
533 | [ 1. , -0.3285, -0.0253, 0.1384],
534 | [ 1. , -0.3515, -0.7196, -0.2582],
535 | [ 1. , 1.2433, -0.3738, -0.5226]])
536 | ```
537 |
538 | sm.OLS类可以拟合一个普通最小二乘回归:
539 | ```python
540 | In [70]: model = sm.OLS(y, X)
541 | ```
542 |
543 | 这个模型的fit方法返回了一个回归结果对象,它包含估计的模型参数和其它内容:
544 | ```python
545 | In [71]: results = model.fit()
546 |
547 | In [72]: results.params
548 | Out[72]: array([ 0.1783, 0.223 , 0.501 ])
549 | ```
550 |
551 | 对结果使用summary方法可以打印模型的详细诊断结果:
552 | ```python
553 | In [73]: print(results.summary())
554 | OLS Regression Results
555 | ==============================================================================
556 | Dep. Variable: y R-squared: 0.430
557 | Model: OLS Adj. R-squared: 0.413
558 | Method: Least Squares F-statistic: 24.42
559 | Date: Mon, 25 Sep 2017 Prob (F-statistic): 7.44e-12
560 | Time: 14:06:15 Log-Likelihood: -34.305
561 | No. Observations: 100 AIC: 74.61
562 | Df Residuals: 97 BIC: 82.42
563 | Df Model: 3
564 | Covariance Type: nonrobust
565 | ==============================================================================
566 | coef std err t P>|t| [0.025 0.975]
567 | ------------------------------------------------------------------------------
568 | x1 0.1783 0.053 3.364 0.001 0.073 0.283
569 | x2 0.2230 0.046 4.818 0.000 0.131 0.315
570 | x3 0.5010 0.080 6.237 0.000 0.342 0.660
571 | ==============================================================================
572 | Omnibus: 4.662 Durbin-Watson: 2.201
573 | Prob(Omnibus): 0.097 Jarque-Bera (JB): 4.098
574 | Skew: 0.481 Prob(JB): 0.129
575 | Kurtosis: 3.243 Cond. No.
576 | 1.74
577 | ==============================================================================
578 | Warnings:
579 | [1] Standard Errors assume that the covariance matrix of the errors is correctly
580 | specified.
581 | ```
582 |
583 | 这里的参数名为通用名x1, x2等等。假设所有的模型参数都在一个DataFrame中:
584 | ```python
585 | In [74]: data = pd.DataFrame(X, columns=['col0', 'col1', 'col2'])
586 |
587 | In [75]: data['y'] = y
588 |
589 | In [76]: data[:5]
590 | Out[76]:
591 | col0 col1 col2 y
592 | 0 -0.129468 -1.212753 0.504225 0.427863
593 | 1 0.302910 -0.435742 -0.254180 -0.673480
594 | 2 -0.328522 -0.025302 0.138351 -0.090878
595 | 3 -0.351475 -0.719605 -0.258215 -0.489494
596 | 4 1.243269 -0.373799 -0.522629 -0.128941
597 | ```
598 |
599 | 现在,我们使用statsmodels的公式API和Patsy的公式字符串:
600 | ```python
601 | In [77]: results = smf.ols('y ~ col0 + col1 + col2', data=data).fit()
602 |
603 | In [78]: results.params
604 | Out[78]:
605 | Intercept 0.033559
606 | col0 0.176149
607 | col1 0.224826
608 | col2 0.514808
609 | dtype: float64
610 |
611 | In [79]: results.tvalues
612 | Out[79]:
613 | Intercept 0.952188
614 | col0 3.319754
615 | col1 4.850730
616 | col2 6.303971
617 | dtype: float64
618 | ```
619 |
620 | 观察下statsmodels是如何返回Series结果的,附带有DataFrame的列名。当使用公式和pandas对象时,我们不需要使用add_constant。
621 |
622 | 给出一个样本外数据,你可以根据估计的模型参数计算预测值:
623 | ```python
624 | In [80]: results.predict(data[:5])
625 | Out[80]:
626 | 0 -0.002327
627 | 1 -0.141904
628 | 2 0.041226
629 | 3 -0.323070
630 | 4 -0.100535
631 | dtype: float64
632 | ```
633 |
634 | statsmodels的线性模型结果还有其它的分析、诊断和可视化工具。除了普通最小二乘模型,还有其它的线性模型。
635 |
636 | ## 估计时间序列过程
637 |
638 | statsmodels的另一模型类是进行时间序列分析,包括自回归过程、卡尔曼滤波和其它态空间模型,和多元自回归模型。
639 |
640 | 用自回归结构和噪声来模拟一些时间序列数据:
641 | ```python
642 | init_x = 4
643 |
644 | import random
645 | values = [init_x, init_x]
646 | N = 1000
647 |
648 | b0 = 0.8
649 | b1 = -0.4
650 | noise = dnorm(0, 0.1, N)
651 | for i in range(N):
652 | new_x = values[-1] * b0 + values[-2] * b1 + noise[i]
653 | values.append(new_x)
654 | ```
655 |
656 | 这个数据有AR(2)结构(两个延迟),参数是0.8和-0.4。拟合AR模型时,你可能不知道滞后项的个数,因此可以用较多的滞后量来拟合这个模型:
657 | ```python
658 | In [82]: MAXLAGS = 5
659 |
660 | In [83]: model = sm.tsa.AR(values)
661 |
662 | In [84]: results = model.fit(MAXLAGS)
663 | ```
664 |
665 | 结果中的估计参数首先是截距,其次是前两个参数的估计值:
666 | ```python
667 | In [85]: results.params
668 | Out[85]: array([-0.0062, 0.7845, -0.4085, -0.0136, 0.015 , 0.0143])
669 | ```
670 |
671 | 更多的细节以及如何解释结果超出了本书的范围,可以通过statsmodels文档学习更多。
672 |
673 | # 13.4 scikit-learn介绍
674 |
675 | scikit-learn是一个广泛使用、用途多样的Python机器学习库。它包含多种标准监督和非监督机器学习方法和模型选择和评估、数据转换、数据加载和模型持久化工具。这些模型可以用于分类、聚合、预测和其它任务。
676 |
677 | 机器学习方面的学习和应用scikit-learn和TensorFlow解决实际问题的线上和纸质资料很多。本节中,我会简要介绍scikit-learn API的风格。
678 |
679 | 写作此书的时候,scikit-learn并没有和pandas深度结合,但是有些第三方包在开发中。尽管如此,pandas非常适合在模型拟合前处理数据集。
680 |
681 | 举个例子,我用一个Kaggle竞赛的经典数据集,关于泰坦尼克号乘客的生还率。我们用pandas加载测试和训练数据集:
682 | ```python
683 | In [86]: train = pd.read_csv('datasets/titanic/train.csv')
684 |
685 | In [87]: test = pd.read_csv('datasets/titanic/test.csv')
686 |
687 | In [88]: train[:4]
688 | Out[88]:
689 | PassengerId Survived Pclass \
690 | 0 1 0 3
691 | 1 2 1 1
692 | 2 3 1 3
693 | 3 4 1 1
694 | Name Sex Age SibSp \
695 | 0 Braund, Mr. Owen Harris male 22.0 1
696 | 1 Cumings, Mrs. John Bradley (Florence Briggs Th... female 38.0 1
697 | 2 Heikkinen, Miss. Laina female 26.0 0
698 | 3 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1
699 | Parch Ticket Fare Cabin Embarked
700 | 0 0 A/5 21171 7.2500 NaN S
701 | 1 0 PC 17599 71.2833 C85 C
702 | 2 0 STON/O2. 3101282 7.9250 NaN S
703 | 3 0 113803 53.1000 C123 S
704 | ```
705 |
706 | statsmodels和scikit-learn通常不能接收缺失数据,因此我们要查看列是否包含缺失值:
707 | ```python
708 | In [89]: train.isnull().sum()
709 | Out[89]:
710 | PassengerId 0
711 | Survived 0
712 | Pclass 0
713 | Name 0
714 | Sex 0
715 | Age 177
716 | SibSp 0
717 | Parch 0
718 | Ticket 0
719 | Fare 0
720 | Cabin 687
721 | Embarked 2
722 | dtype: int64
723 |
724 | In [90]: test.isnull().sum()
725 | Out[90]:
726 | PassengerId 0
727 | Pclass 0
728 | Name 0
729 | Sex 0
730 | Age 86
731 | SibSp 0
732 | Parch 0
733 | Ticket 0
734 | Fare 1
735 | Cabin 327
736 | Embarked 0
737 | dtype: int64
738 | ```
739 |
740 | 在统计和机器学习的例子中,根据数据中的特征,一个典型的任务是预测乘客能否生还。模型现在训练数据集中拟合,然后用样本外测试数据集评估。
741 |
742 | 我想用年龄作为预测值,但是它包含缺失值。缺失数据补全的方法有多种,我用的是一种简单方法,用训练数据集的中位数补全两个表的空值:
743 | ```python
744 | In [91]: impute_value = train['Age'].median()
745 |
746 | In [92]: train['Age'] = train['Age'].fillna(impute_value)
747 |
748 | In [93]: test['Age'] = test['Age'].fillna(impute_value)
749 | ```
750 |
751 | 现在我们需要指定模型。我增加了一个列IsFemale,作为“Sex”列的编码:
752 | ```python
753 | In [94]: train['IsFemale'] = (train['Sex'] == 'female').astype(int)
754 |
755 | In [95]: test['IsFemale'] = (test['Sex'] == 'female').astype(int)
756 | ```
757 |
758 | 然后,我们确定一些模型变量,并创建NumPy数组:
759 | ```python
760 | In [96]: predictors = ['Pclass', 'IsFemale', 'Age']
761 |
762 | In [97]: X_train = train[predictors].values
763 |
764 | In [98]: X_test = test[predictors].values
765 |
766 | In [99]: y_train = train['Survived'].values
767 |
768 | In [100]: X_train[:5]
769 | Out[100]:
770 | array([[ 3., 0., 22.],
771 | [ 1., 1., 38.],
772 | [ 3., 1., 26.],
773 | [ 1., 1., 35.],
774 | [ 3., 0., 35.]])
775 |
776 | In [101]: y_train[:5]
777 | Out[101]: array([0, 1, 1, 1, 0])
778 | ```
779 |
780 | 我不能保证这是一个好模型,但它的特征都符合。我们用scikit-learn的LogisticRegression模型,创建一个模型实例:
781 | ```python
782 | In [102]: from sklearn.linear_model import LogisticRegression
783 |
784 | In [103]: model = LogisticRegression()
785 | ```
786 |
787 | 与statsmodels类似,我们可以用模型的fit方法,将它拟合到训练数据:
788 | ```python
789 | In [104]: model.fit(X_train, y_train)
790 | Out[104]:
791 | LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
792 | intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1,
793 | penalty='l2', random_state=None, solver='liblinear', tol=0.0001,
794 | verbose=0, warm_start=False)
795 | ```
796 |
797 | 现在,我们可以用model.predict,对测试数据进行预测:
798 | ```python
799 | In [105]: y_predict = model.predict(X_test)
800 |
801 | In [106]: y_predict[:10]
802 | Out[106]: array([0, 0, 0, 0, 1, 0, 1, 0, 1, 0])
803 | ```
804 |
805 | 如果你有测试数据集的真是值,你可以计算准确率或其它错误度量值:
806 | ```python
807 | (y_true == y_predict).mean()
808 | ```
809 |
810 | 在实际中,模型训练经常有许多额外的复杂因素。许多模型有可以调节的参数,有些方法(比如交叉验证)可以用来进行参数调节,避免对训练数据过拟合。这通常可以提高预测性或对新数据的健壮性。
811 |
812 | 交叉验证通过分割训练数据来模拟样本外预测。基于模型的精度得分(比如均方差),可以对模型参数进行网格搜索。有些模型,如logistic回归,有内置的交叉验证的估计类。例如,logisticregressioncv类可以用一个参数指定网格搜索对模型的正则化参数C的粒度:
813 | ```python
814 | In [107]: from sklearn.linear_model import LogisticRegressionCV
815 |
816 | In [108]: model_cv = LogisticRegressionCV(10)
817 |
818 | In [109]: model_cv.fit(X_train, y_train)
819 | Out[109]:
820 | LogisticRegressionCV(Cs=10, class_weight=None, cv=None, dual=False,
821 | fit_intercept=True, intercept_scaling=1.0, max_iter=100,
822 | multi_class='ovr', n_jobs=1, penalty='l2', random_state=None,
823 | refit=True, scoring=None, solver='lbfgs', tol=0.0001, verbose=0)
824 | ```
825 |
826 | 要手动进行交叉验证,你可以使用cross_val_score帮助函数,它可以处理数据分割。例如,要交叉验证我们的带有四个不重叠训练数据的模型,可以这样做:
827 | ```python
828 | In [110]: from sklearn.model_selection import cross_val_score
829 |
830 | In [111]: model = LogisticRegression(C=10)
831 |
832 | In [112]: scores = cross_val_score(model, X_train, y_train, cv=4)
833 |
834 | In [113]: scores
835 | Out[113]: array([ 0.7723, 0.8027, 0.7703, 0.7883])
836 | ```
837 |
838 | 默认的评分指标取决于模型本身,但是可以明确指定一个评分。交叉验证过的模型需要更长时间来训练,但会有更高的模型性能。
839 |
840 | # 13.5 继续学习
841 |
842 | 我只是介绍了一些Python建模库的表面内容,现在有越来越多的框架用于各种统计和机器学习,它们都是用Python或Python用户界面实现的。
843 |
844 | 这本书的重点是数据规整,有其它的书是关注建模和数据科学工具的。其中优秀的有:
845 |
846 | - Andreas Mueller and Sarah Guido (O’Reilly)的 《Introduction to Machine Learning with Python》
847 | - Jake VanderPlas (O’Reilly)的 《Python Data Science Handbook》
848 | - Joel Grus (O’Reilly) 的 《Data Science from Scratch: First Principles》
849 | - Sebastian Raschka (Packt Publishing) 的《Python Machine Learning》
850 | - Aurélien Géron (O’Reilly) 的《Hands-On Machine Learning with Scikit-Learn and TensorFlow》
851 |
852 | 虽然书是学习的好资源,但是随着底层开源软件的发展,书的内容会过时。最好是不断熟悉各种统计和机器学习框架的文档,学习最新的功能和API。
853 |
--------------------------------------------------------------------------------
/附录B 更多关于IPython的内容(完).md:
--------------------------------------------------------------------------------
1 | 第2章中,我们学习了IPython shell和Jupyter notebook的基础。本章中,我们会探索IPython更深层次的功能,可以从控制台或在jupyter使用。
2 |
3 | # B.1 使用命令历史
4 |
5 | Ipython维护了一个位于磁盘的小型数据库,用于保存执行的每条指令。它的用途有:
6 |
7 | - 只用最少的输入,就能搜索、补全和执行先前运行过的指令;
8 | - 在不同session间保存命令历史;
9 | - 将日志输入/输出历史到一个文件
10 |
11 | 这些功能在shell中,要比notebook更为有用,因为notebook从设计上是将输入和输出的代码放到每个代码格子中。
12 |
13 | ## 搜索和重复使用命令历史
14 |
15 | Ipython可以让你搜索和执行之前的代码或其他命令。这个功能非常有用,因为你可能需要重复执行同样的命令,例如%run命令,或其它代码。假设你必须要执行:
16 | ```python
17 | In[7]: %run first/second/third/data_script.py
18 | ```
19 |
20 | 运行成功,然后检查结果,发现计算有错。解决完问题,然后修改了data_script.py,你就可以输入一些%run命令,然后按Ctrl+P或上箭头。这样就可以搜索历史命令,匹配输入字符的命令。多次按Ctrl+P或上箭头,会继续搜索命令。如果你要执行你想要执行的命令,不要害怕。你可以按下Ctrl-N或下箭头,向前移动历史命令。这样做了几次后,你可以不假思索地按下这些键!
21 |
22 | Ctrl-R可以带来如同Unix风格shell(比如bash shell)的readline的部分增量搜索功能。在Windows上,readline功能是被IPython模仿的。要使用这个功能,先按Ctrl-R,然后输入一些包含于输入行的想要搜索的字符:
23 | ```python
24 | In [1]: a_command = foo(x, y, z)
25 |
26 | (reverse-i-search)`com': a_command = foo(x, y, z)
27 | ```
28 |
29 | Ctrl-R会循环历史,找到匹配字符的每一行。
30 |
31 | ## 输入和输出变量
32 |
33 | 忘记将函数调用的结果分配给变量是非常烦人的。IPython的一个session会在一个特殊变量,存储输入和输出Python对象的引用。前面两个输出会分别存储在 _(一个下划线)和 __(两个下划线)变量:
34 | ```python
35 | In [24]: 2 ** 27
36 | Out[24]: 134217728
37 |
38 | In [25]: _
39 | Out[25]: 134217728
40 | ```
41 |
42 | 输入变量是存储在名字类似_iX的变量中,X是输入行的编号。对于每个输入变量,都有一个对应的输出变量_X。因此在输入第27行之后,会有两个新变量_27 (输出)和_i27(输入):
43 | ```python
44 | In [26]: foo = 'bar'
45 |
46 | In [27]: foo
47 | Out[27]: 'bar'
48 |
49 | In [28]: _i27
50 | Out[28]: u'foo'
51 |
52 | In [29]: _27
53 | Out[29]: 'bar'
54 | ```
55 |
56 | 因为输入变量是字符串,它们可以用Python的exec关键字再次执行:
57 | ```python
58 | In [30]: exec(_i27)
59 | ```
60 |
61 | 这里,_i27是在In [27]输入的代码。
62 |
63 | 有几个魔术函数可以让你利用输入和输出历史。%hist可以打印所有或部分的输入历史,加上或不加上编号。%reset可以清理交互命名空间,或输入和输出缓存。%xdel魔术函数可以去除IPython中对一个特别对象的所有引用。对于关于这些魔术方法的更多内容,请查看文档。
64 |
65 | >警告:当处理非常大的数据集时,要记住IPython的输入和输出的历史会造成被引用的对象不被垃圾回收(释放内存),即使你使用del关键字从交互命名空间删除变量。在这种情况下,小心使用xdel %和%reset可以帮助你避免陷入内存问题。
66 |
67 | # B.2 与操作系统交互
68 |
69 | IPython的另一个功能是无缝连接文件系统和操作系统。这意味着,在同时做其它事时,无需退出IPython,就可以像Windows或Unix使用命令行操作,包括shell命令、更改目录、用Python对象(列表或字符串)存储结果。它还有简单的命令别名和目录书签功能。
70 |
71 | 表B-1总结了调用shell命令的魔术函数和语法。我会在下面几节介绍这些功能。
72 |
73 | 
74 |
75 | ## Shell命令和别名
76 | 用叹号开始一行,是告诉IPython执行叹号后面的所有内容。这意味着你可以删除文件(取决于操作系统,用rm或del)、改变目录或执行任何其他命令。
77 |
78 | 通过给变量加上叹号,你可以在一个变量中存储命令的控制台输出。例如,在我联网的基于Linux的主机上,我可以获得IP地址为Python变量:
79 | ```python
80 | In [1]: ip_info = !ifconfig wlan0 | grep "inet "
81 |
82 | In [2]: ip_info[0].strip()
83 | Out[2]: 'inet addr:10.0.0.11 Bcast:10.0.0.255 Mask:255.255.255.0'
84 | ```
85 |
86 | 返回的Python对象ip_info实际上是一个自定义的列表类型,它包含着多种版本的控制台输出。
87 |
88 | 当使用!,IPython还可以替换定义在当前环境的Python值。要这么做,可以在变量名前面加上$符号:
89 | ```python
90 | In [3]: foo = 'test*'
91 |
92 | In [4]: !ls $foo
93 | test4.py test.py test.xml
94 | ```
95 |
96 | %alias魔术函数可以自定义shell命令的快捷方式。看一个简单的例子:
97 | ```python
98 | In [1]: %alias ll ls -l
99 |
100 | In [2]: ll /usr
101 | total 332
102 | drwxr-xr-x 2 root root 69632 2012-01-29 20:36 bin/
103 | drwxr-xr-x 2 root root 4096 2010-08-23 12:05 games/
104 | drwxr-xr-x 123 root root 20480 2011-12-26 18:08 include/
105 | drwxr-xr-x 265 root root 126976 2012-01-29 20:36 lib/
106 | drwxr-xr-x 44 root root 69632 2011-12-26 18:08 lib32/
107 | lrwxrwxrwx 1 root root 3 2010-08-23 16:02 lib64 -> lib/
108 | drwxr-xr-x 15 root root 4096 2011-10-13 19:03 local/
109 | drwxr-xr-x 2 root root 12288 2012-01-12 09:32 sbin/
110 | drwxr-xr-x 387 root root 12288 2011-11-04 22:53 share/
111 | drwxrwsr-x 24 root src 4096 2011-07-17 18:38 src/
112 | ```
113 |
114 | 你可以执行多个命令,就像在命令行中一样,只需用分号隔开:
115 | ```python
116 | In [558]: %alias test_alias (cd examples; ls; cd ..)
117 |
118 | In [559]: test_alias
119 | macrodata.csv spx.csv tips.csv
120 | ```
121 |
122 | 当session结束,你定义的别名就会失效。要创建恒久的别名,需要使用配置。
123 |
124 | ## 目录书签系统
125 |
126 | IPython有一个简单的目录书签系统,可以让你保存常用目录的别名,这样在跳来跳去的时候会非常方便。例如,假设你想创建一个书签,指向本书的补充内容:
127 | ```python
128 | In [6]: %bookmark py4da /home/wesm/code/pydata-book
129 | ```
130 |
131 | 这么做之后,当使用%cd魔术命令,就可以使用定义的书签:
132 | ```python
133 | In [7]: cd py4da
134 | (bookmark:py4da) -> /home/wesm/code/pydata-book
135 | /home/wesm/code/pydata-book
136 | ```
137 |
138 | 如果书签的名字,与当前工作目录的一个目录重名,你可以使用-b标志来覆写,使用书签的位置。使用%bookmark的-l选项,可以列出所有的书签:
139 | ```python
140 | In [8]: %bookmark -l
141 | Current bookmarks:
142 | py4da -> /home/wesm/code/pydata-book-source
143 | ```
144 |
145 | 书签,和别名不同,在session之间是保持的。
146 |
147 | # B.3 软件开发工具
148 |
149 | 除了作为优秀的交互式计算和数据探索环境,IPython也是有效的Python软件开发工具。在数据分析中,最重要的是要有正确的代码。幸运的是,IPython紧密集成了和加强了Python内置的pdb调试器。第二,需要快速的代码。对于这点,IPython有易于使用的代码计时和分析工具。我会详细介绍这些工具。
150 |
151 | ## 交互调试器
152 |
153 | IPython的调试器用tab补全、语法增强、逐行异常追踪增强了pdb。调试代码的最佳时间就是刚刚发生错误。异常发生之后就输入%debug,就启动了调试器,进入抛出异常的堆栈框架:
154 |
155 | ```python
156 | In [2]: run examples/ipython_bug.py
157 | ---------------------------------------------------------------------------
158 | AssertionError Traceback (most recent call last)
159 | /home/wesm/code/pydata-book/examples/ipython_bug.py in ()
160 | 13 throws_an_exception()
161 | 14
162 | ---> 15 calling_things()
163 |
164 | /home/wesm/code/pydata-book/examples/ipython_bug.py in calling_things()
165 | 11 def calling_things():
166 | 12 works_fine()
167 | ---> 13 throws_an_exception()
168 | 14
169 | 15 calling_things()
170 |
171 | /home/wesm/code/pydata-book/examples/ipython_bug.py in throws_an_exception()
172 | 7 a = 5
173 | 8 b = 6
174 | ----> 9 assert(a + b == 10)
175 | 10
176 | 11 def calling_things():
177 |
178 | AssertionError:
179 |
180 | In [3]: %debug
181 | > /home/wesm/code/pydata-book/examples/ipython_bug.py(9)throws_an_exception()
182 | 8 b = 6
183 | ----> 9 assert(a + b == 10)
184 | 10
185 |
186 | ipdb>
187 | ```
188 |
189 | 一旦进入调试器,你就可以执行任意的Python代码,在每个堆栈框架中检查所有的对象和数据(解释器会保持它们活跃)。默认是从错误发生的最低级开始。通过u(up)和d(down),你可以在不同等级的堆栈踪迹切换:
190 | ```python
191 | ipdb> u
192 | > /home/wesm/code/pydata-book/examples/ipython_bug.py(13)calling_things()
193 | 12 works_fine()
194 | ---> 13 throws_an_exception()
195 | 14
196 | ```
197 |
198 | 执行%pdb命令,可以在发生任何异常时让IPython自动启动调试器,许多用户会发现这个功能非常好用。
199 |
200 | 用调试器帮助开发代码也很容易,特别是当你希望设置断点或在函数和脚本间移动,以检查每个阶段的状态。有多种方法可以实现。第一种是使用%run和-d,它会在执行传入脚本的任何代码之前调用调试器。你必须马上按s(step)以进入脚本:
201 | ```python
202 | In [5]: run -d examples/ipython_bug.py
203 | Breakpoint 1 at /home/wesm/code/pydata-book/examples/ipython_bug.py:1
204 | NOTE: Enter 'c' at the ipdb> prompt to start your script.
205 | > (1)()
206 |
207 | ipdb> s
208 | --Call--
209 | > /home/wesm/code/pydata-book/examples/ipython_bug.py(1)()
210 | 1---> 1 def works_fine():
211 | 2 a = 5
212 | 3 b = 6
213 | ```
214 |
215 | 然后,你就可以决定如何工作。例如,在前面的异常,我们可以设置一个断点,就在调用works_fine之前,然后运行脚本,在遇到断点时按c(continue):
216 | ```python
217 | ipdb> b 12
218 | ipdb> c
219 | > /home/wesm/code/pydata-book/examples/ipython_bug.py(12)calling_things()
220 | 11 def calling_things():
221 | 2--> 12 works_fine()
222 | 13 throws_an_exception()
223 | ```
224 |
225 | 这时,你可以step进入works_fine(),或通过按n(next)执行works_fine(),进入下一行:
226 | ```python
227 | ipdb> n
228 | > /home/wesm/code/pydata-book/examples/ipython_bug.py(13)calling_things()
229 | 2 12 works_fine()
230 | ---> 13 throws_an_exception()
231 | 14
232 | ```
233 |
234 | 然后,我们可以进入throws_an_exception,到达发生错误的一行,查看变量。注意,调试器的命令是在变量名之前,在变量名前面加叹号!可以查看内容:
235 | ```python
236 | ipdb> s
237 | --Call--
238 | > /home/wesm/code/pydata-book/examples/ipython_bug.py(6)throws_an_exception()
239 | 5
240 | ----> 6 def throws_an_exception():
241 | 7 a = 5
242 |
243 | ipdb> n
244 | > /home/wesm/code/pydata-book/examples/ipython_bug.py(7)throws_an_exception()
245 | 6 def throws_an_exception():
246 | ----> 7 a = 5
247 | 8 b = 6
248 |
249 | ipdb> n
250 | > /home/wesm/code/pydata-book/examples/ipython_bug.py(8)throws_an_exception()
251 | 7 a = 5
252 | ----> 8 b = 6
253 | 9 assert(a + b == 10)
254 |
255 | ipdb> n
256 | > /home/wesm/code/pydata-book/examples/ipython_bug.py(9)throws_an_exception()
257 | 8 b = 6
258 | ----> 9 assert(a + b == 10)
259 | 10
260 |
261 | ipdb> !a
262 | 5
263 | ipdb> !b
264 | 6
265 | ```
266 |
267 | 提高使用交互式调试器的熟练度需要练习和经验。表B-2,列出了所有调试器命令。如果你习惯了IDE,你可能觉得终端的调试器在一开始会不顺手,但会觉得越来越好用。一些Python的IDEs有很好的GUI调试器,选择顺手的就好。
268 |
269 | 
270 |
271 | ## 使用调试器的其它方式
272 |
273 | 还有一些其它工作可以用到调试器。第一个是使用特殊的set_trace函数(根据pdb.set_trace命名的),这是一个简装的断点。还有两种方法是你可能想用的(像我一样,将其添加到IPython的配置):
274 | ```python
275 | from IPython.core.debugger import Pdb
276 |
277 | def set_trace():
278 | Pdb(color_scheme='Linux').set_trace(sys._getframe().f_back)
279 |
280 | def debug(f, *args, **kwargs):
281 | pdb = Pdb(color_scheme='Linux')
282 | return pdb.runcall(f, *args, **kwargs)
283 | ```
284 | 第一个函数set_trace非常简单。如果你想暂时停下来进行仔细检查(比如发生异常之前),可以在代码的任何位置使用set_trace:
285 | ```python
286 | In [7]: run examples/ipython_bug.py
287 | > /home/wesm/code/pydata-book/examples/ipython_bug.py(16)calling_things()
288 | 15 set_trace()
289 | ---> 16 throws_an_exception()
290 | 17
291 | ```
292 |
293 | 按c(continue)可以让代码继续正常行进。
294 |
295 | 我们刚看的debug函数,可以让你方便的在调用任何函数时使用调试器。假设我们写了一个下面的函数,想逐步分析它的逻辑:
296 | ```python
297 | def f(x, y, z=1):
298 | tmp = x + y
299 | return tmp / z
300 | ```
301 |
302 | 普通地使用f,就会像f(1, 2, z=3)。而要想进入f,将f作为第一个参数传递给debug,再将位置和关键词参数传递给f:
303 | ```python
304 | In [6]: debug(f, 1, 2, z=3)
305 | > (2)f()
306 | 1 def f(x, y, z):
307 | ----> 2 tmp = x + y
308 | 3 return tmp / z
309 |
310 | ipdb>
311 | ```
312 |
313 | 这两个简单方法节省了我平时的大量时间。
314 |
315 | 最后,调试器可以和%run一起使用。脚本通过运行%run -d,就可以直接进入调试器,随意设置断点并启动脚本:
316 | ```python
317 | In [1]: %run -d examples/ipython_bug.py
318 | Breakpoint 1 at /home/wesm/code/pydata-book/examples/ipython_bug.py:1
319 | NOTE: Enter 'c' at the ipdb> prompt to start your script.
320 | > (1)()
321 |
322 | ipdb>
323 | ```
324 |
325 | 加上-b和行号,可以预设一个断点:
326 | ```python
327 | In [2]: %run -d -b2 examples/ipython_bug.py
328 |
329 | Breakpoint 1 at /home/wesm/code/pydata-book/examples/ipython_bug.py:2
330 | NOTE: Enter 'c' at the ipdb> prompt to start your script.
331 | > (1)()
332 |
333 | ipdb> c
334 | > /home/wesm/code/pydata-book/examples/ipython_bug.py(2)works_fine()
335 | 1 def works_fine():
336 | 1---> 2 a = 5
337 | 3 b = 6
338 |
339 | ipdb>
340 | ```
341 |
342 | ## 代码计时:%time 和 %timeit
343 |
344 | 对于大型和长时间运行的数据分析应用,你可能希望测量不同组件或单独函数调用语句的执行时间。你可能想知道哪个函数占用的时间最长。幸运的是,IPython可以让你开发和测试代码时,很容易地获得这些信息。
345 |
346 | 手动用time模块和它的函数time.clock和time.time给代码计时,既单调又重复,因为必须要写一些无趣的模板化代码:
347 | ```python
348 | import time
349 | start = time.time()
350 | for i in range(iterations):
351 | # some code to run here
352 | elapsed_per = (time.time() - start) / iterations
353 | ```
354 |
355 | 因为这是一个很普通的操作,IPython有两个魔术函数,%time和%timeit,可以自动化这个过程。
356 |
357 | %time会运行一次语句,报告总共的执行时间。假设我们有一个大的字符串列表,我们想比较不同的可以挑选出特定开头字符串的方法。这里有一个含有600000字符串的列表,和两个方法,用以选出foo开头的字符串:
358 | ```python
359 | # a very large list of strings
360 | strings = ['foo', 'foobar', 'baz', 'qux',
361 | 'python', 'Guido Van Rossum'] * 100000
362 |
363 | method1 = [x for x in strings if x.startswith('foo')]
364 |
365 | method2 = [x for x in strings if x[:3] == 'foo']
366 | ```
367 |
368 | 看起来它们的性能应该是同级别的,但事实呢?用%time进行一下测量:
369 | ```python
370 | In [561]: %time method1 = [x for x in strings if x.startswith('foo')]
371 | CPU times: user 0.19 s, sys: 0.00 s, total: 0.19 s
372 | Wall time: 0.19 s
373 |
374 | In [562]: %time method2 = [x for x in strings if x[:3] == 'foo']
375 | CPU times: user 0.09 s, sys: 0.00 s, total: 0.09 s
376 | Wall time: 0.09 s
377 | ```
378 |
379 | Wall time(wall-clock time的简写)是主要关注的。第一个方法是第二个方法的两倍多,但是这种测量方法并不准确。如果用%time多次测量,你就会发现结果是变化的。要想更准确,可以使用%timeit魔术函数。给出任意一条语句,它能多次运行这条语句以得到一个更为准确的时间:
380 | ```python
381 | In [563]: %timeit [x for x in strings if x.startswith('foo')]
382 | 10 loops, best of 3: 159 ms per loop
383 |
384 | In [564]: %timeit [x for x in strings if x[:3] == 'foo']
385 | 10 loops, best of 3: 59.3 ms per loop
386 | ```
387 |
388 | 这个例子说明了解Python标准库、NumPy、pandas和其它库的性能是很有价值的。在大型数据分析中,这些毫秒的时间就会累积起来!
389 |
390 | %timeit特别适合分析执行时间短的语句和函数,即使是微秒或纳秒。这些时间可能看起来毫不重要,但是一个20微秒的函数执行1百万次就比一个5微秒的函数长15秒。在上一个例子中,我们可以直接比较两个字符串操作,以了解它们的性能特点:
391 | ```python
392 | In [565]: x = 'foobar'
393 |
394 | In [566]: y = 'foo'
395 |
396 | In [567]: %timeit x.startswith(y)
397 | 1000000 loops, best of 3: 267 ns per loop
398 |
399 | In [568]: %timeit x[:3] == y
400 | 10000000 loops, best of 3: 147 ns per loop
401 | ```
402 |
403 | ## 基础分析:%prun和%run -p
404 |
405 | 分析代码与代码计时关系很紧密,除了它关注的是“时间花在了哪里”。Python主要的分析工具是cProfile模块,它并不局限于IPython。cProfile会执行一个程序或任意的代码块,并会跟踪每个函数执行的时间。
406 |
407 | 使用cProfile的通常方式是在命令行中运行一整段程序,输出每个函数的累积时间。假设我们有一个简单的在循环中进行线型代数运算的脚本(计算一系列的100×100矩阵的最大绝对特征值):
408 | ```python
409 | import numpy as np
410 | from numpy.linalg import eigvals
411 |
412 | def run_experiment(niter=100):
413 | K = 100
414 | results = []
415 | for _ in xrange(niter):
416 | mat = np.random.randn(K, K)
417 | max_eigenvalue = np.abs(eigvals(mat)).max()
418 | results.append(max_eigenvalue)
419 | return results
420 | some_results = run_experiment()
421 | print 'Largest one we saw: %s' % np.max(some_results)
422 | ```
423 |
424 | 你可以用cProfile运行这个脚本,使用下面的命令行:
425 | ```
426 | python -m cProfile cprof_example.py
427 | ```
428 |
429 | 运行之后,你会发现输出是按函数名排序的。这样要看出谁耗费的时间多有点困难,最好用-s指定排序:
430 | ```python
431 | $ python -m cProfile -s cumulative cprof_example.py
432 | Largest one we saw: 11.923204422
433 | 15116 function calls (14927 primitive calls) in 0.720 seconds
434 |
435 | Ordered by: cumulative time
436 |
437 | ncalls tottime percall cumtime percall filename:lineno(function)
438 | 1 0.001 0.001 0.721 0.721 cprof_example.py:1()
439 | 100 0.003 0.000 0.586 0.006 linalg.py:702(eigvals)
440 | 200 0.572 0.003 0.572 0.003 {numpy.linalg.lapack_lite.dgeev}
441 | 1 0.002 0.002 0.075 0.075 __init__.py:106()
442 | 100 0.059 0.001 0.059 0.001 {method 'randn')
443 | 1 0.000 0.000 0.044 0.044 add_newdocs.py:9()
444 | 2 0.001 0.001 0.037 0.019 __init__.py:1()
445 | 2 0.003 0.002 0.030 0.015 __init__.py:2()
446 | 1 0.000 0.000 0.030 0.030 type_check.py:3()
447 | 1 0.001 0.001 0.021 0.021 __init__.py:15()
448 | 1 0.013 0.013 0.013 0.013 numeric.py:1()
449 | 1 0.000 0.000 0.009 0.009 __init__.py:6()
450 | 1 0.001 0.001 0.008 0.008 __init__.py:45()
451 | 262 0.005 0.000 0.007 0.000 function_base.py:3178(add_newdoc)
452 | 100 0.003 0.000 0.005 0.000 linalg.py:162(_assertFinite)
453 | ```
454 |
455 | 只显示出前15行。扫描cumtime列,可以容易地看出每个函数用了多少时间。如果一个函数调用了其它函数,计时并不会停止。cProfile会记录每个函数的起始和结束时间,使用它们进行计时。
456 |
457 | 除了在命令行中使用,cProfile也可以在程序中使用,分析任意代码块,而不必运行新进程。Ipython的%prun和%run -p,有便捷的接口实现这个功能。%prun使用类似cProfile的命令行选项,但是可以分析任意Python语句,而不用整个py文件:
458 | ```python
459 | In [4]: %prun -l 7 -s cumulative run_experiment()
460 | 4203 function calls in 0.643 seconds
461 |
462 | Ordered by: cumulative time
463 | List reduced from 32 to 7 due to restriction <7>
464 | ncalls tottime percall cumtime percall filename:lineno(function)
465 | 1 0.000 0.000 0.643 0.643 :1()
466 | 1 0.001 0.001 0.643 0.643 cprof_example.py:4(run_experiment)
467 | 100 0.003 0.000 0.583 0.006 linalg.py:702(eigvals)
468 | 200 0.569 0.003 0.569 0.003 {numpy.linalg.lapack_lite.dgeev}
469 | 100 0.058 0.001 0.058 0.001 {method 'randn'}
470 | 100 0.003 0.000 0.005 0.000 linalg.py:162(_assertFinite)
471 | 200 0.002 0.000 0.002 0.000 {method 'all' of 'numpy.ndarray'}
472 | ```
473 |
474 | 相似的,调用``%run -p -s cumulative cprof_example.py``有和命令行相似的作用,只是你不用离开Ipython。
475 |
476 | 在Jupyter notebook中,你可以使用%%prun魔术方法(两个%)来分析一整段代码。这会弹出一个带有分析输出的独立窗口。便于快速回答一些问题,比如“为什么这段代码用了这么长时间”?
477 |
478 | 使用IPython或Jupyter,还有一些其它工具可以让分析工作更便于理解。其中之一是SnakeViz(https://github.com/jiffyclub/snakeviz/),它会使用d3.js产生一个分析结果的交互可视化界面。
479 |
480 | ## 逐行分析函数
481 |
482 | 有些情况下,用%prun(或其它基于cProfile的分析方法)得到的信息,不能获得函数执行时间的整个过程,或者结果过于复杂,加上函数名,很难进行解读。对于这种情况,有一个小库叫做line_profiler(可以通过PyPI或包管理工具获得)。它包含IPython插件,可以启用一个新的魔术函数%lprun,可以对一个函数或多个函数进行逐行分析。你可以通过修改IPython配置(查看IPython文档或本章后面的配置小节)加入下面这行,启用这个插件:
483 | ```python
484 | # A list of dotted module names of IPython extensions to load.
485 | c.TerminalIPythonApp.extensions = ['line_profiler']
486 | ```
487 |
488 | 你还可以运行命令:
489 | ```python
490 | %load_ext line_profiler
491 | ```
492 |
493 | line_profiler也可以在程序中使用(查看完整文档),但是在IPython中使用是最为强大的。假设你有一个带有下面代码的模块prof_mod,做一些NumPy数组操作:
494 | ```python
495 | from numpy.random import randn
496 |
497 | def add_and_sum(x, y):
498 | added = x + y
499 | summed = added.sum(axis=1)
500 | return summed
501 |
502 | def call_function():
503 | x = randn(1000, 1000)
504 | y = randn(1000, 1000)
505 | return add_and_sum(x, y)
506 | ```
507 |
508 | 如果想了解add_and_sum函数的性能,%prun可以给出下面内容:
509 | ```python
510 | In [569]: %run prof_mod
511 |
512 | In [570]: x = randn(3000, 3000)
513 |
514 | In [571]: y = randn(3000, 3000)
515 |
516 | In [572]: %prun add_and_sum(x, y)
517 | 4 function calls in 0.049 seconds
518 | Ordered by: internal time
519 | ncalls tottime percall cumtime percall filename:lineno(function)
520 | 1 0.036 0.036 0.046 0.046 prof_mod.py:3(add_and_sum)
521 | 1 0.009 0.009 0.009 0.009 {method 'sum' of 'numpy.ndarray'}
522 | 1 0.003 0.003 0.049 0.049 :1()
523 | ```
524 |
525 | 上面的做法启发性不大。激活了IPython插件line_profiler,新的命令%lprun就能用了。使用中的不同点是,我们必须告诉%lprun要分析的函数是哪个。语法是:
526 | ```python
527 | %lprun -f func1 -f func2 statement_to_profile
528 | ```
529 |
530 | 我们想分析add_and_sum,运行:
531 | ```python
532 | In [573]: %lprun -f add_and_sum add_and_sum(x, y)
533 | Timer unit: 1e-06 s
534 | File: prof_mod.py
535 | Function: add_and_sum at line 3
536 | Total time: 0.045936 s
537 | Line # Hits Time Per Hit % Time Line Contents
538 | ==============================================================
539 | 3 def add_and_sum(x, y):
540 | 4 1 36510 36510.0 79.5 added = x + y
541 | 5 1 9425 9425.0 20.5 summed = added.sum(axis=1)
542 | 6 1 1 1.0 0.0 return summed
543 | ```
544 |
545 | 这样就容易诠释了。我们分析了和代码语句中一样的函数。看之前的模块代码,我们可以调用call_function并对它和add_and_sum进行分析,得到一个完整的代码性能概括:
546 | ```python
547 | In [574]: %lprun -f add_and_sum -f call_function call_function()
548 | Timer unit: 1e-06 s
549 | File: prof_mod.py
550 | Function: add_and_sum at line 3
551 | Total time: 0.005526 s
552 | Line # Hits Time Per Hit % Time Line Contents
553 | ==============================================================
554 | 3 def add_and_sum(x, y):
555 | 4 1 4375 4375.0 79.2 added = x + y
556 | 5 1 1149 1149.0 20.8 summed = added.sum(axis=1)
557 | 6 1 2 2.0 0.0 return summed
558 | File: prof_mod.py
559 | Function: call_function at line 8
560 | Total time: 0.121016 s
561 | Line # Hits Time Per Hit % Time Line Contents
562 | ==============================================================
563 | 8 def call_function():
564 | 9 1 57169 57169.0 47.2 x = randn(1000, 1000)
565 | 10 1 58304 58304.0 48.2 y = randn(1000, 1000)
566 | 11 1 5543 5543.0 4.6 return add_and_sum(x, y)
567 | ```
568 |
569 | 我的经验是用%prun (cProfile)进行宏观分析,%lprun (line_profiler)做微观分析。最好对这两个工具都了解清楚。
570 |
571 | >笔记:使用%lprun必须要指明函数名的原因是追踪每行的执行时间的损耗过多。追踪无用的函数会显著地改变结果。
572 |
573 | # B.4 使用IPython高效开发的技巧
574 |
575 | 方便快捷地写代码、调试和使用是每个人的目标。除了代码风格,流程细节(比如代码重载)也需要一些调整。
576 |
577 | 因此,这一节的内容更像是门艺术而不是科学,还需要你不断的试验,以达成高效。最终,你要能结构优化代码,并且能省时省力地检查程序或函数的结果。我发现用IPython设计的软件比起命令行,要更适合工作。尤其是当发生错误时,你需要检查自己或别人写的数月或数年前写的代码的错误。
578 |
579 | ## 重载模块依赖
580 |
581 | 在Python中,当你输入import some_lib,some_lib中的代码就会被执行,所有的变量、函数和定义的引入,就会被存入到新创建的some_lib模块命名空间。当下一次输入some_lib,就会得到一个已存在的模块命名空间的引用。潜在的问题是当你%run一个脚本,它依赖于另一个模块,而这个模块做过修改,就会产生问题。假设我在test_script.py中有如下代码:
582 | ```python
583 | import some_lib
584 |
585 | x = 5
586 | y = [1, 2, 3, 4]
587 | result = some_lib.get_answer(x, y)
588 | ```
589 |
590 | 如果你运行过了%run test_script.py,然后修改了some_lib.py,下一次再执行%run test_script.py,还会得到旧版本的some_lib.py,这是因为Python模块系统的“一次加载”机制。这一点区分了Python和其它数据分析环境,比如MATLAB,它会自动传播代码修改。解决这个问题,有多种方法。第一种是在标准库importlib模块中使用reload函数:
591 | ```python
592 | import some_lib
593 | import importlib
594 |
595 | importlib.reload(some_lib)
596 | ```
597 |
598 | 这可以保证每次运行test_script.py时可以加载最新的some_lib.py。很明显,如果依赖更深,在各处都使用reload是非常麻烦的。对于这个问题,IPython有一个特殊的dreload函数(它不是魔术函数)重载深层的模块。如果我运行过some_lib.py,然后输入dreload(some_lib),就会尝试重载some_lib和它的依赖。不过,这个方法不适用于所有场景,但比重启IPython强多了。
599 |
600 | ## 代码设计技巧
601 |
602 | 对于这单,没有简单的对策,但是有一些原则,是我在工作中发现很好用的。
603 |
604 | ## 保持相关对象和数据活跃
605 |
606 | 为命令行写一个下面示例中的程序是很少见的:
607 | ```python
608 | from my_functions import g
609 |
610 | def f(x, y):
611 | return g(x + y)
612 |
613 | def main():
614 | x = 6
615 | y = 7.5
616 | result = x + y
617 |
618 | if __name__ == '__main__':
619 | main()
620 | ```
621 |
622 | 在IPython中运行这个程序会发生问题,你发现是什么了吗?运行之后,任何定义在main函数中的结果和对象都不能在IPython中被访问到。更好的方法是将main中的代码直接在模块的命名空间中执行(或者在``__name__ == '__main__':``中,如果你想让这个模块可以被引用)。这样,当你%rundiamante,就可以查看所有定义在main中的变量。这等价于在Jupyter notebook的代码格中定义一个顶级变量。
623 |
624 | ## 扁平优于嵌套
625 |
626 | 深层嵌套的代码总让我联想到洋葱皮。当测试或调试一个函数时,你需要剥多少层洋葱皮才能到达目标代码呢?“扁平优于嵌套”是Python之禅的一部分,它也适用于交互式代码开发。尽量将函数和类去耦合和模块化,有利于测试(如果你是在写单元测试)、调试和交互式使用。
627 |
628 | ## 克服对大文件的恐惧
629 |
630 | 如果你之前是写JAVA(或者其它类似的语言),你可能被告知要让文件简短。在多数语言中,这都是合理的建议:太长会让人感觉是坏代码,意味着重构和重组是必要的。但是,在用IPython开发时,运行10个相关联的小文件(小于100行),比起两个或三个长文件,会让你更头疼。更少的文件意味着重载更少的模块和更少的编辑时在文件中跳转。我发现维护大模块,每个模块都是紧密组织的,会更实用和Pythonic。经过方案迭代,有时会将大文件分解成小文件。
631 |
632 | 我不建议极端化这条建议,那样会形成一个单独的超大文件。找到一个合理和直观的大型代码模块库和封装结构往往需要一点工作,但这在团队工作中非常重要。每个模块都应该结构紧密,并且应该能直观地找到负责每个功能领域功能和类。
633 |
634 | # B.5 IPython高级功能
635 |
636 | 要全面地使用IPython系统需要用另一种稍微不同的方式写代码,或深入IPython的配置。
637 |
638 | ## 让类是对IPython友好的
639 |
640 | IPython会尽可能地在控制台美化展示每个字符串。对于许多对象,比如字典、列表和元组,内置的pprint模块可以用来美化格式。但是,在用户定义的类中,你必自己生成字符串。假设有一个下面的简单的类:
641 | ```python
642 | class Message:
643 | def __init__(self, msg):
644 | self.msg = msg
645 | ```
646 |
647 | 如果这么写,就会发现默认的输出不够美观:
648 | ```python
649 | In [576]: x = Message('I have a secret')
650 |
651 | In [577]: x
652 | Out[577]: <__main__.Message instance at 0x60ebbd8>
653 | ```
654 |
655 | IPython会接收__repr__魔术方法返回的字符串(通过output = repr(obj)),并在控制台打印出来。因此,我们可以添加一个简单的__repr__方法到前面的类中,以得到一个更有用的输出:
656 | ```python
657 | class Message:
658 | def __init__(self, msg):
659 | self.msg = msg
660 |
661 | def __repr__(self):
662 | return 'Message: %s' % self.msg
663 | In [579]: x = Message('I have a secret')
664 |
665 | In [580]: x
666 | Out[580]: Message: I have a secret
667 | ```
668 |
669 | ## 文件和配置
670 |
671 | 通过扩展配置系统,大多数IPython和Jupyter notebook的外观(颜色、提示符、行间距等等)和动作都是可以配置的。通过配置,你可以做到:
672 |
673 | - 改变颜色主题
674 | - 改变输入和输出提示符,或删除输出之后、输入之前的空行
675 | - 执行任意Python语句(例如,引入总是要使用的代码或者每次加载IPython都要运行的内容)
676 | - 启用IPython总是要运行的插件,比如line_profiler中的%lprun魔术函数
677 | - 启用Jupyter插件
678 | - 定义自己的魔术函数或系统别名
679 |
680 | IPython的配置存储在特殊的ipython_config.py文件中,它通常是在用户home目录的.ipython/文件夹中。配置是通过一个特殊文件。当你启动IPython,就会默认加载这个存储在profile_default文件夹中的默认文件。因此,在我的Linux系统,完整的IPython配置文件路径是:
681 | ```python
682 | /home/wesm/.ipython/profile_default/ipython_config.py
683 | ```
684 |
685 | 要启动这个文件,运行下面的命令:
686 | ```python
687 | ipython profile create
688 | ```
689 |
690 | 这个文件中的内容留给读者自己探索。这个文件有注释,解释了每个配置选项的作用。另一点,可以有多个配置文件。假设你想要另一个IPython配置文件,专门是为另一个应用或项目的。创建一个新的配置文件很简单,如下所示:
691 | ```python
692 | ipython profile create secret_project
693 | ```
694 |
695 | 做完之后,在新创建的profile_secret_project目录便捷配置文件,然后如下启动IPython:
696 | ```python
697 | $ ipython --profile=secret_project
698 | Python 3.5.1 | packaged by conda-forge | (default, May 20 2016, 05:22:56)
699 | Type "copyright", "credits" or "license" for more information.
700 |
701 | IPython 5.1.0 -- An enhanced Interactive Python.
702 | ? -> Introduction and overview of IPython's features.
703 | %quickref -> Quick reference.
704 | help -> Python's own help system.
705 | object? -> Details about 'object', use 'object??' for extra details.
706 |
707 | IPython profile: secret_project
708 | ```
709 |
710 | 和之前一样,IPython的文档是一个极好的学习配置文件的资源。
711 |
712 | 配置Jupyter有些不同,因为你可以使用除了Python的其它语言。要创建一个类似的Jupyter配置文件,运行:
713 | ```python
714 | jupyter notebook --generate-config
715 | ```
716 |
717 | 这样会在home目录的.jupyter/jupyter_notebook_config.py创建配置文件。编辑完之后,可以将它重命名:
718 | ```python
719 | $ mv ~/.jupyter/jupyter_notebook_config.py ~/.jupyter/my_custom_config.py
720 | ```
721 |
722 | 打开Jupyter之后,你可以添加--config参数:
723 | ```python
724 | jupyter notebook --config=~/.jupyter/my_custom_config.py
725 | ```
726 |
727 | # B.6 总结
728 | 学习过本书中的代码案例,你的Python技能得到了一定的提升,我建议你持续学习IPython和Jupyter。因为这两个项目的设计初衷就是提高生产率的,你可能还会发现一些工具,可以让你更便捷地使用Python和计算库。
729 |
730 | 你可以在nbviewer(https://nbviewer.jupyter.org/)上找到更多有趣的Jupyter notebooks。
731 |
--------------------------------------------------------------------------------