Python数据分析 - PolarsBook中文版: https://www.pythondataanalysis.com/docs/polars_book_cn/ - Polars快速入门: https://www.pythondataanalysis.com/docs/polars_book_cn/quickstart/ - Polars表达式: https://www.pythondataanalysis.com/docs/polars_book_cn/dsl/ - Polars表达式: https://www.pythondataanalysis.com/docs/polars_book_cn/dsl/expressions/ - Polars上下文: https://www.pythondataanalysis.com/docs/polars_book_cn/dsl/contexts/ - Polars分组: https://www.pythondataanalysis.com/docs/polars_book_cn/dsl/groupby/ - Polars折叠: https://www.pythondataanalysis.com/docs/polars_book_cn/dsl/folds/ - Polars自定义函数: https://www.pythondataanalysis.com/docs/polars_book_cn/dsl/custom_functions/ - Polars实例: https://www.pythondataanalysis.com/docs/polars_book_cn/dsl/introduction_polars/ - Polars表达式方法: https://www.pythondataanalysis.com/docs/polars_book_cn/dsl/api/ - Polars视频介绍: https://www.pythondataanalysis.com/docs/polars_book_cn/dsl/video_intro/ - Polars与Numpy交互: https://www.pythondataanalysis.com/docs/polars_book_cn/dsl/numpy/ - Polars窗口函数: https://www.pythondataanalysis.com/docs/polars_book_cn/dsl/window_functions/ - Polars索引: https://www.pythondataanalysis.com/docs/polars_book_cn/indexing/ - Polars数据类型: https://www.pythondataanalysis.com/docs/polars_book_cn/datatypes/ - 来自Pandas: https://www.pythondataanalysis.com/docs/polars_book_cn/coming_from_pandas/ - 来自ApacheSpark: https://www.pythondataanalysis.com/docs/polars_book_cn/coming_from_spark/ - Polars性能: https://www.pythondataanalysis.com/docs/polars_book_cn/performance/ - 字符串: https://www.pythondataanalysis.com/docs/polars_book_cn/performance/strings/ - Polars优化: https://www.pythondataanalysis.com/docs/polars_book_cn/optimizations/ - Polars惰性方法: https://www.pythondataanalysis.com/docs/polars_book_cn/optimizations/lazy/ - 谓词下推: https://www.pythondataanalysis.com/docs/polars_book_cn/optimizations/lazy/predicate-pushdown/ - 投影下推: https://www.pythondataanalysis.com/docs/polars_book_cn/optimizations/lazy/projection-pushdown/ - 其它优化: https://www.pythondataanalysis.com/docs/polars_book_cn/optimizations/lazy/other-optimizations/ - Polars参考指南: https://www.pythondataanalysis.com/docs/polars_book_cn/references/ - Polars时间序列: https://www.pythondataanalysis.com/docs/polars_book_cn/timeseries/ - Polars时间序列实例: https://www.pythondataanalysis.com/docs/polars_book_cn/timeseries/time-series/ - Polars使用范围: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/ - IO: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/io/ - Polars操作CSV文件: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/io/csv/ - Polars操作Parquet文件: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/io/parquet/ - Polars处理多个文件: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/io/multiple_files/ - Polars读取数据库: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/io/read_db/ - Polars与AWS交互: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/io/aws/ - Polars与Google BigQuery交互: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/io/google-big-query/ - Polars与Postgres交互: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/io/postgres/ - 互通性: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/interop/ - Arrow: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/interop/arrow/ - Numpy: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/interop/numpy/ - 数据: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/data/ - 字符串: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/data/strings/ - 时间戳: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/data/timestamps/ - 数据帧: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/df/ - 选中行或列: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/df/row_col_selection/ - 常用操作: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/df/common-manipulations/ - 聚合: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/df/aggregate/ - 分组: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/df/groupby/ - 过滤: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/df/filter/ - 连接: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/df/join/ - 重塑: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/df/melt/ - 条件应用: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/df/conditionally-apply/ - 排序: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/df/sorting/ - 透视: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/df/pivot/ - 应用: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/apply/ - Polars自定义函数: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/apply/udfs/ - Polars窗口函数: https://www.pythondataanalysis.com/docs/polars_book_cn/howcani/apply/window-functions/ - Python数据分析 第二版: https://www.pythondataanalysis.com/docs/Python_Data_Analysis_2nd_Editon/ - 第 1 章 准备工作: https://www.pythondataanalysis.com/docs/Python_Data_Analysis_2nd_Editon/Chapter-01/ - 第 2 章 Python 语法基础: https://www.pythondataanalysis.com/docs/Python_Data_Analysis_2nd_Editon/Chapter-02/ - 第 3 章 Python 的数据结构、函数和文件: https://www.pythondataanalysis.com/docs/Python_Data_Analysis_2nd_Editon/Chapter-03/ - 第 4 章 NumPy 基础:数组和向量计算: https://www.pythondataanalysis.com/docs/Python_Data_Analysis_2nd_Editon/Chapter-04/ - 第 5 章 Pandas 入门: https://www.pythondataanalysis.com/docs/Python_Data_Analysis_2nd_Editon/Chapter-05/ - 第 6 章 数据加载、存储与文件格式: https://www.pythondataanalysis.com/docs/Python_Data_Analysis_2nd_Editon/Chapter-06/ - 第 7 章 数据清洗和准备: https://www.pythondataanalysis.com/docs/Python_Data_Analysis_2nd_Editon/Chapter-07/ - 第 10 章 数据聚合与分组运算: https://www.pythondataanalysis.com/docs/Python_Data_Analysis_2nd_Editon/Chapter-10/ - 第 11 章 时间序列: https://www.pythondataanalysis.com/docs/Python_Data_Analysis_2nd_Editon/Chapter-11/ - 第 12 章 pandas 高级应用: https://www.pythondataanalysis.com/docs/Python_Data_Analysis_2nd_Editon/Chapter-12/ - 第 13 章 Python 建模库介绍: https://www.pythondataanalysis.com/docs/Python_Data_Analysis_2nd_Editon/Chapter-13/ - 第 14 章 数据分析案例: https://www.pythondataanalysis.com/docs/Python_Data_Analysis_2nd_Editon/Chapter-14/ - 附录 A NumPy 高级应用: https://www.pythondataanalysis.com/docs/Python_Data_Analysis_2nd_Editon/Appendix-A/ - 附录 B 更多关于 IPython 的内容: https://www.pythondataanalysis.com/docs/Python_Data_Analysis_2nd_Editon/Appendix-B/ - 第 8 章 数据规整:聚合、合并和重塑: https://www.pythondataanalysis.com/docs/Python_Data_Analysis_2nd_Editon/Chapter-08/ - 第 9 章 绘图和可视化: https://www.pythondataanalysis.com/docs/Python_Data_Analysis_2nd_Editon/Chapter-09/ - Polars用户指南: https://www.pythondataanalysis.com/docs/Polars_user_guide/ - Polars入门: https://www.pythondataanalysis.com/docs/Polars_user_guide/polars_getting_started/ - 安装Polars: https://www.pythondataanalysis.com/docs/Polars_user_guide/polars_installation/ - Polars核心概念: https://www.pythondataanalysis.com/docs/Polars_user_guide/concepts/ - Polars数据类型和结构: https://www.pythondataanalysis.com/docs/Polars_user_guide/concepts/data-types-and-structures/ - Polars表达式和上下文: https://www.pythondataanalysis.com/docs/Polars_user_guide/concepts/expressions-and-contexts/ - Polars延迟API: https://www.pythondataanalysis.com/docs/Polars_user_guide/concepts/lazy-api/ - Streaming: https://www.pythondataanalysis.com/docs/Polars_user_guide/concepts/_streaming/ - Polars表达式: https://www.pythondataanalysis.com/docs/Polars_user_guide/expressions/ - Polars基本操作: https://www.pythondataanalysis.com/docs/Polars_user_guide/expressions/basic-operations/ - Aggregation: https://www.pythondataanalysis.com/docs/Polars_user_guide/expressions/aggregation/ - Casting: https://www.pythondataanalysis.com/docs/Polars_user_guide/expressions/casting/ - Categorical Data and Enums: https://www.pythondataanalysis.com/docs/Polars_user_guide/expressions/categorical-data-and-enums/ - Expression Expansion: https://www.pythondataanalysis.com/docs/Polars_user_guide/expressions/expression-expansion/ - Folds: https://www.pythondataanalysis.com/docs/Polars_user_guide/expressions/folds/ - Lists and Arrays: https://www.pythondataanalysis.com/docs/Polars_user_guide/expressions/lists-and-arrays/ - Missing Data: https://www.pythondataanalysis.com/docs/Polars_user_guide/expressions/missing-data/ - Numpy Functions: https://www.pythondataanalysis.com/docs/Polars_user_guide/expressions/numpy-functions/ - Strings: https://www.pythondataanalysis.com/docs/Polars_user_guide/expressions/strings/ - Structs: https://www.pythondataanalysis.com/docs/Polars_user_guide/expressions/structs/ - User Defined Python Functions: https://www.pythondataanalysis.com/docs/Polars_user_guide/expressions/user-defined-python-functions/ - Window Functions: https://www.pythondataanalysis.com/docs/Polars_user_guide/expressions/window-functions/ - Reference: https://www.pythondataanalysis.com/docs/Polars_user_guide/api/reference/ - Index: https://www.pythondataanalysis.com/docs/Polars_user_guide/development/contributing/ - Versioning: https://www.pythondataanalysis.com/docs/Polars_user_guide/development/versioning/ - Index: https://www.pythondataanalysis.com/docs/Polars_user_guide/polars-cloud/ - Ecosystem: https://www.pythondataanalysis.com/docs/Polars_user_guide/user-guide/ecosystem/ - Gpu Support: https://www.pythondataanalysis.com/docs/Polars_user_guide/user-guide/gpu-support/ - Index: https://www.pythondataanalysis.com/docs/Polars_user_guide/user-guide/io/ - Index: https://www.pythondataanalysis.com/docs/Polars_user_guide/user-guide/lazy/ - Pandas: https://www.pythondataanalysis.com/docs/Polars_user_guide/user-guide/migration/pandas/ - Spark: https://www.pythondataanalysis.com/docs/Polars_user_guide/user-guide/migration/spark/ - Arrow: https://www.pythondataanalysis.com/docs/Polars_user_guide/user-guide/misc/arrow/ - Comparison: https://www.pythondataanalysis.com/docs/Polars_user_guide/user-guide/misc/comparison/ - Multiprocessing: https://www.pythondataanalysis.com/docs/Polars_user_guide/user-guide/misc/multiprocessing/ - Polars Llms: https://www.pythondataanalysis.com/docs/Polars_user_guide/user-guide/misc/polars_llms/ - Styling: https://www.pythondataanalysis.com/docs/Polars_user_guide/user-guide/misc/styling/ - Visualization: https://www.pythondataanalysis.com/docs/Polars_user_guide/user-guide/misc/visualization/ - Index: https://www.pythondataanalysis.com/docs/Polars_user_guide/user-guide/plugins/ - Create: https://www.pythondataanalysis.com/docs/Polars_user_guide/user-guide/sql/create/ - Cte: https://www.pythondataanalysis.com/docs/Polars_user_guide/user-guide/sql/cte/ - Intro: https://www.pythondataanalysis.com/docs/Polars_user_guide/user-guide/sql/intro/ - Select: https://www.pythondataanalysis.com/docs/Polars_user_guide/user-guide/sql/select/ - Show: https://www.pythondataanalysis.com/docs/Polars_user_guide/user-guide/sql/show/ - Index: https://www.pythondataanalysis.com/docs/Polars_user_guide/user-guide/transformations/ # 附录 A NumPy 高级应用 在这篇附录中,我会深入 NumPy 库的数组计算。这会包括`ndarray`更内部的细节,和更高级的数组操作和算法。 本章包括了一些杂乱的章节,不需要仔细研究。 # A.1 `ndarray`对象的内部机理 NumPy 的`ndarray`提供了一种将同质数据块(可以是连续或跨越)解释为多维数组对象的方式。正如你之前所看到的那样,数据类型(`dtype`)决定了数据的解释方式,比如浮点数、整数、布尔值等。 `ndarray`如此强大的部分原因是所有数组对象都是数据块的一个跨度视图(strided view)。你可能想知道数组视图`arr[::2,::-1]`不复制任何数据的原因是什么。简单地说,`ndarray`不只是一块内存和一个`dtype`,它还有跨度信息,这使得数组能以各种步幅(step size)在内存中移动。更准确地讲,`ndarray`内部由以下内容组成: - 一个指向数据(内存或内存映射文件中的一块数据)的指针。 - 数据类型或`dtype`,描述在数组中的固定大小值的格子。 - 一个表示数组形状(`shape`)的元组。 - 一个跨度元组(`stride`),其中的整数指的是为了前进到当前维度下一个元素需要“跨过”的字节数。 图 A-1 简单地说明了`ndarray` 的内部结构。 ![图 A-1 Numpy 的`ndarray`对象](/imgs2/7178691-43452f2f413e5094.png) 例如,一个`10×5`的数组,其形状为`(10,5)`: ```python In [10]: np.ones((10, 5)).shape Out[10]: (10, 5) ``` 一个典型的(C 顺序,稍后将详细讲解)`3×4×5`的`float64`(8 个字节)数组,其跨度为`(160,40,8)` —— 知道跨度是非常有用的,通常,跨度在一个轴上越大,沿这个轴进行计算的开销就越大: ```python In [11]: np.ones((3, 4, 5), dtype=np.float64).strides Out[11]: (160, 40, 8) ``` 虽然 NumPy 用户很少会对数组的跨度信息感兴趣,但它们却是构建非复制式数组视图的重要因素。跨度甚至可以是负数,这样会使数组在内存中后向移动,比如在切片`obj[::-1]`或`obj[:,::-1]`中就是这样的。 ## NumPy 数据类型体系 你可能偶尔需要检查数组中所包含的是否是整数、浮点数、字符串或 Python 对象。因为浮点数的种类很多(从`float16`到`float128`),判断`dtype`是否属于某个大类的工作非常繁琐。幸运的是,`dtype`都有一个超类(比如`np.integer`和`np.floating`),它们可以跟`np.issubdtype`函数结合使用: ```python In [12]: ints = np.ones(10, dtype=np.uint16) In [13]: floats = np.ones(10, dtype=np.float32) In [14]: np.issubdtype(ints.dtype, np.integer) Out[14]: True In [15]: np.issubdtype(floats.dtype, np.floating) Out[15]: True ``` 调用`dtype`的`mro`方法即可查看其所有的父类: ```python In [16]: np.float64.mro() Out[16]: [numpy.float64, numpy.floating, numpy.inexact, numpy.number, numpy.generic, float, object] ``` 然后得到: ```python In [17]: np.issubdtype(ints.dtype, np.number) Out[17]: True ``` 大部分 NumPy 用户完全不需要了解这些知识,但是这些知识偶尔还是能派上用场的。图 A-2 说明了`dtype`体系以及父子类关系。 ![图 A-2 NumPy 的`dtype`体系](/imgs2/7178691-b8996bf943a06ab9.png) # A.2 高级数组操作 除花式索引、切片、布尔条件取子集等操作之外,数组的操作方式还有很多。虽然 pandas 中的高级函数可以处理数据分析工作中的许多重型任务,但有时你还是需要编写一些在现有库中找不到的数据算法。 ## 数组重塑 多数情况下,你可以无需复制任何数据,就将数组从一个形状转换为另一个形状。只需向数组的实例方法`reshape`传入一个表示新形状的元组即可实现该目的。例如,假设有一个一维数组,我们希望将其重新排列为一个矩阵(结果见图 A-3): ```python In [18]: arr = np.arange(8) In [19]: arr Out[19]: array([0, 1, 2, 3, 4, 5, 6, 7]) In [20]: arr.reshape((4, 2)) Out[20]: array([[0, 1], [2, 3], [4, 5], [6, 7]]) ``` ![图 A-3 按 C 顺序(按行)和按 Fortran 顺序(按列)进行重塑](/imgs2/7178691-95bbca6d8d04e4c7.png) 多维数组也能被重塑: ```python In [21]: arr.reshape((4, 2)).reshape((2, 4)) Out[21]: array([[0, 1, 2, 3], [4, 5, 6, 7]]) ``` 作为参数的形状的其中一维可以是 -1,它表示该维度的大小由数据本身推断而来: ```python In [22]: arr = np.arange(15) In [23]: arr.reshape((5, -1)) Out[23]: array([[ 0, 1, 2], [ 3, 4, 5], [ 6, 7, 8], [ 9, 10, 11], [12, 13, 14]]) ``` 与`reshape`将一维数组转换为多维数组的运算过程相反的运算通常称为扁平化(flattening)或散开(raveling): ```python In [27]: arr = np.arange(15).reshape((5, 3)) In [28]: arr Out[28]: array([[ 0, 1, 2], [ 3, 4, 5], [ 6, 7, 8], [ 9, 10, 11], [12, 13, 14]]) In [29]: arr.ravel() Out[29]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]) ``` 如果结果中的值与原始数组相同,`ravel`不会产生源数据的副本。`flatten`方法的行为类似于`ravel`,只不过它总是返回数据的副本: ```python In [30]: arr.flatten() Out[30]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]) ``` 数组可以被重塑或散开为别的顺序。这对 NumPy 新手来说是一个比较微妙的问题,所以在下一小节中我们将专门讲解这个问题。 ## C 和 Fortran 顺序 NumPy 允许你更为灵活地控制数据在内存中的布局。默认情况下,NumPy 数组是按行优先顺序创建的。在空间方面,这就意味着,对于一个二维数组,每行中的数据项是被存放在相邻内存位置上的。另一种顺序是列优先顺序,它意味着每列中的数据项是被存放在相邻内存位置上的。 由于一些历史原因,行和列优先顺序又分别称为 C 和 Fortran 顺序。在 FORTRAN 77 中,矩阵全都是列优先的。 像`reshape`和`reval`这样的函数,都可以接受一个表示数组数据存放顺序的`order`参数。一般可以是`'C'`或`'F'`(还有`'A'`和`'K'`等不常用的选项,具体请参考 NumPy 的文档)。图 A-3 对此进行了说明: ```python In [31]: arr = np.arange(12).reshape((3, 4)) In [32]: arr Out[32]: array([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]]) In [33]: arr.ravel() Out[33]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) In [34]: arr.ravel('F') Out[34]: array([ 0, 4, 8, 1, 5, 9, 2, 6, 10, 3, 7, 11]) ``` ![图 A-3 按 C(行优先)或 Fortran(列优先)顺序进行重塑](/imgs2/7178691-f486e7c41d7e0eec.png) 二维或更高维数组的重塑过程比较令人费解(见图 A-3)。C 和 Fortran 顺序的关键区别就是维度的行进顺序: - C /行优先顺序:先经过更高的维度(例如,轴 1 会先于轴 0 被处理)。 - Fortran /列优先顺序:后经过更高的维度(例如,轴 0 会先于轴 1 被处理)。 ## 数组的合并和拆分 `numpy.concatenate`可以按指定轴将一个由数组组成的序列(如元组、列表等)连接到一起: ```python In [35]: arr1 = np.array([[1, 2, 3], [4, 5, 6]]) In [36]: arr2 = np.array([[7, 8, 9], [10, 11, 12]]) In [37]: np.concatenate([arr1, arr2], axis=0) Out[37]: array([[ 1, 2, 3], [ 4, 5, 6], [ 7, 8, 9], [10, 11, 12]]) In [38]: np.concatenate([arr1, arr2], axis=1) Out[38]: array([[ 1, 2, 3, 7, 8, 9], [ 4, 5, 6, 10, 11, 12]]) ``` 对于常见的连接操作,NumPy 提供了一些比较方便的方法(如`vstack`和`hstack`)。因此,上面的运算还可以表达为: ```python In [39]: np.vstack((arr1, arr2)) Out[39]: array([[ 1, 2, 3], [ 4, 5, 6], [ 7, 8, 9], [10, 11, 12]]) In [40]: np.hstack((arr1, arr2)) Out[40]: array([[ 1, 2, 3, 7, 8, 9], [ 4, 5, 6, 10, 11, 12]]) ``` 与此相反,`split`用于将一个数组沿指定轴拆分为多个数组: ```python In [41]: arr = np.random.randn(5, 2) In [42]: arr Out[42]: array([[-0.2047, 0.4789], [-0.5194, -0.5557], [ 1.9658, 1.3934], [ 0.0929, 0.2817], [ 0.769 , 1.2464]]) In [43]: first, second, third = np.split(arr, [1, 3]) In [44]: first Out[44]: array([[-0.2047, 0.4789]]) In [45]: second Out[45]: array([[-0.5194, -0.5557], [ 1.9658, 1.3934]]) In [46]: third Out[46]: array([[ 0.0929, 0.2817], [ 0.769 , 1.2464]]) ``` 传入到`np.split`的值`[1,3]`指示在哪个索引处分割数组。 表 A-1 中列出了所有关于数组连接和拆分的函数,其中有些是专门为了方便常见的连接运算而提供的。 ![表 A-1 数组连接函数](/imgs2/7178691-c597246722a6bb01.png) ## 堆叠辅助类:`r_`和`c_` NumPy 命名空间中有两个特殊的对象——`r_`和`c_`,它们可以使数组的堆叠操作更为简洁: ```python In [47]: arr = np.arange(6) In [48]: arr1 = arr.reshape((3, 2)) In [49]: arr2 = np.random.randn(3, 2) In [50]: np.r_[arr1, arr2] Out[50]: array([[ 0. , 1. ], [ 2. , 3. ], [ 4. , 5. ], [ 1.0072, -1.2962], [ 0.275 , 0.2289], [ 1.3529, 0.8864]]) In [51]: np.c_[np.r_[arr1, arr2], arr] Out[51]: array([[ 0. , 1. , 0. ], [ 2. , 3. , 1. ], [ 4. , 5. , 2. ], [ 1.0072, -1.2962, 3. ], [ 0.275 , 0.2289, 4. ], [ 1.3529, 0.8864, 5. ]]) ``` 它还可以将切片转换成数组: ```python In [52]: np.c_[1:6, -10:-5] Out[52]: array([[ 1, -10], [ 2, -9], [ 3, -8], [ 4, -7], [ 5, -6]]) ``` `r_`和`c_`的具体功能请参考其文档。 ## 元素的重复操作:`tile`和`repeat` 对数组进行重复以产生更大数组的工具主要是`repeat`和`tile`这两个函数。`repeat`会将数组中的各个元素重复一定次数,从而产生一个更大的数组: ```python In [53]: arr = np.arange(3) In [54]: arr Out[54]: array([0, 1, 2]) In [55]: arr.repeat(3) Out[55]: array([0, 0, 0, 1, 1, 1, 2, 2, 2]) ``` > 笔记:跟其他流行的数组编程语言(如 MATLAB)不同,NumPy 中很少需要对数组进行重复(replicate)。这主要是因为广播(broadcasting,我们将在下一节中讲解该技术)能更好地满足该需求。 默认情况下,如果传入的是一个整数,则各元素就都会重复那么多次。如果传入的是一组整数,则各元素就可以重复不同的次数: ```python In [56]: arr.repeat([2, 3, 4]) Out[56]: array([0, 0, 1, 1, 1, 2, 2, 2, 2]) ``` 对于多维数组,还可以让它们的元素沿指定轴重复: ```python In [57]: arr = np.random.randn(2, 2) In [58]: arr Out[58]: array([[-2.0016, -0.3718], [ 1.669 , -0.4386]]) In [59]: arr.repeat(2, axis=0) Out[59]: array([[-2.0016, -0.3718], [-2.0016, -0.3718], [ 1.669 , -0.4386], [ 1.669 , -0.4386]]) ``` 注意,如果没有设置轴向,则数组会被扁平化,这可能不会是你想要的结果。同样,在对多维进行重复时,也可以传入一组整数,这样就会使各切片重复不同的次数: ```python In [60]: arr.repeat([2, 3], axis=0) Out[60]: array([[-2.0016, -0.3718], [-2.0016, -0.3718], [ 1.669 , -0.4386], [ 1.669 , -0.4386], [ 1.669 , -0.4386]]) In [61]: arr.repeat([2, 3], axis=1) Out[61]: array([[-2.0016, -2.0016, -0.3718, -0.3718, -0.3718], [ 1.669 , 1.669 , -0.4386, -0.4386, -0.4386]]) ``` `tile`的功能是沿指定轴向堆叠数组的副本。你可以形象地将其想象成“铺瓷砖”: ```python In [62]: arr Out[62]: array([[-2.0016, -0.3718], [ 1.669 , -0.4386]]) In [63]: np.tile(arr, 2) Out[63]: array([[-2.0016, -0.3718, -2.0016, -0.3718], [ 1.669 , -0.4386, 1.669 , -0.4386]]) ``` 第二个参数是瓷砖的数量。对于标量,瓷砖是水平铺设的,而不是垂直铺设。它可以是一个表示“铺设”布局的元组: ```python In [64]: arr Out[64]: array([[-2.0016, -0.3718], [ 1.669 , -0.4386]]) In [65]: np.tile(arr, (2, 1)) Out[65]: array([[-2.0016, -0.3718], [ 1.669 , -0.4386], [-2.0016, -0.3718], [ 1.669 , -0.4386]]) In [66]: np.tile(arr, (3, 2)) Out[66]: array([[-2.0016, -0.3718, -2.0016, -0.3718], [ 1.669 , -0.4386, 1.669 , -0.4386], [-2.0016, -0.3718, -2.0016, -0.3718], [ 1.669 , -0.4386, 1.669 , -0.4386], [-2.0016, -0.3718, -2.0016, -0.3718], [ 1.669 , -0.4386, 1.669 , -0.4386]]) ``` ## 花式索引的等价函数:`take`和`put` 在第 4 章中我们讲过,获取和设置数组子集的一个办法是通过整数数组使用花式索引: ```python In [67]: arr = np.arange(10) * 100 In [68]: inds = [7, 1, 2, 6] In [69]: arr[inds] Out[69]: array([700, 100, 200, 600]) ``` `ndarray`还有其它方法用于获取单个轴向上的选区: ```python In [70]: arr.take(inds) Out[70]: array([700, 100, 200, 600]) In [71]: arr.put(inds, 42) In [72]: arr Out[72]: array([ 0, 42, 42, 300, 400, 500, 42, 42,800, 900]) In [73]: arr.put(inds, [40, 41, 42, 43]) In [74]: arr Out[74]: array([ 0, 41, 42, 300, 400, 500, 43, 40, 800, 900]) ``` 要在其它轴上使用`take`,只需传入`axis`关键字即可: ```python In [75]: inds = [2, 0, 2, 1] In [76]: arr = np.random.randn(2, 4) In [77]: arr Out[77]: array([[-0.5397, 0.477 , 3.2489, -1.0212], [-0.5771, 0.1241, 0.3026, 0.5238]]) In [78]: arr.take(inds, axis=1) Out[78]: array([[ 3.2489, -0.5397, 3.2489, 0.477 ], [ 0.3026, -0.5771, 0.3026, 0.1241]]) ``` `put`不接受`axis`参数,它只会在数组的扁平化版本(一维,C 顺序)上进行索引。因此,在需要用其他轴向的索引设置元素时,最好还是使用花式索引。 # A.3 广播 广播(broadcasting)指的是不同形状的数组之间的算术运算的执行方式。它是一种非常强大的功能,但也容易令人误解,即使是经验丰富的老手也是如此。将标量值跟数组合并时就会发生最简单的广播: ```python In [79]: arr = np.arange(5) In [80]: arr Out[80]: array([0, 1, 2, 3, 4]) In [81]: arr * 4 Out[81]: array([ 0, 4, 8, 12, 16]) ``` 这里我们说:在这个乘法运算中,标量值 4 被广播到了其他所有的元素上。 看一个例子,我们可以通过减去列平均值的方式对数组的每一列进行距平化处理。这个问题解决起来非常简单: ```python In [82]: arr = np.random.randn(4, 3) In [83]: arr.mean(0) Out[83]: array([-0.3928, -0.3824, -0.8768]) In [84]: demeaned = arr - arr.mean(0) In [85]: demeaned Out[85]: array([[ 0.3937, 1.7263, 0.1633], [-0.4384, -1.9878, -0.9839], [-0.468 , 0.9426, -0.3891], [ 0.5126, -0.6811, 1.2097]]) In [86]: demeaned.mean(0) Out[86]: array([-0., 0., -0.]) ``` 图 A-4 形象地展示了该过程。用广播的方式对行进行距平化处理会稍微麻烦一些。幸运的是,只要遵循一定的规则,低维度的值是可以被广播到数组的任意维度的(比如对二维数组各列减去行平均值)。 ![图 A-4 一维数组在轴 0 上的广播](/imgs2/7178691-6aaf022ab88452a9.png) 于是就得到了: ![](/imgs2/7178691-fcaba8455960862a.png) 虽然我是一名经验丰富的 NumPy 老手,但经常还是得停下来画张图并想想广播的原则。再来看一下最后那个例子,假设你希望对各行减去那个平均值。由于`arr.mean(0)`的长度为 3,所以它可以在 0 轴向上进行广播:因为`arr`的后缘维度是 3,所以它们是兼容的。根据该原则,要在 1 轴向上做减法(即各行减去行平均值),较小的那个数组的形状必须是`(4,1)`: ```python In [87]: arr Out[87]: array([[ 0.0009, 1.3438, -0.7135], [-0.8312, -2.3702, -1.8608], [-0.8608, 0.5601, -1.2659], [ 0.1198, -1.0635, 0.3329]]) In [88]: row_means = arr.mean(1) In [89]: row_means.shape Out[89]: (4,) In [90]: row_means.reshape((4, 1)) Out[90]: array([[ 0.2104], [-1.6874], [-0.5222], [-0.2036]]) In [91]: demeaned = arr - row_means.reshape((4, 1)) In [92]: demeaned.mean(1) Out[92]: array([ 0., -0., 0., 0.]) ``` 图 A-5 说明了该运算的过程。 ![图 A-5 二维数组在轴 1 上的广播](/imgs2/7178691-9b0310d6773c3d38.png) 图 A-6 展示了另外一种情况,这次是在一个三维数组上沿 0 轴向加上一个二维数组。 ![图 A-6 三维数组在轴 0 上的广播](/imgs2/7178691-965eb28b60046cd9.png) ## 沿其它轴向广播 高维度数组的广播似乎更难以理解,而实际上它也是遵循广播原则的。如果不然,你就会得到下面这样一个错误: ```python In [93]: arr - arr.mean(1) --------------------------------------------------------------------------- ValueError Traceback (most recent call last) in () ----> 1 arr - arr.mean(1) ValueError: operands could not be broadcast together with shapes (4,3) (4,) ``` 人们经常需要通过算术运算过程将较低维度的数组在除 0 轴以外的其他轴向上广播。根据广播的原则,较小数组的“广播维”必须为 1。在上面那个行距平化的例子中,这就意味着要将行平均值的形状变成`(4,1)`而不是`(4,)`: ```python In [94]: arr - arr.mean(1).reshape((4, 1)) Out[94]: array([[-0.2095, 1.1334, -0.9239], [ 0.8562, -0.6828, -0.1734], [-0.3386, 1.0823, -0.7438], [ 0.3234, -0.8599, 0.5365]]) ``` 对于三维的情况,在三维中的任何一维上广播其实也就是将数据重塑为兼容的形状而已。图 A-7 说明了要在三维数组各维度上广播的形状需求。 ![图 A-7:能在该三维数组上广播的二维数组的形状](/imgs2/7178691-b40936aab8e757d0.png) 于是就有了一个非常普遍的问题(尤其是在通用算法中),即专门为了广播而添加一个长度为 1 的新轴。虽然`reshape`是一个办法,但插入轴需要构造一个表示新形状的元组。这是一个很郁闷的过程。因此,NumPy 数组提供了一种通过索引机制插入轴的特殊语法。下面这段代码通过特殊的`np.newaxis`属性以及“全”切片来插入新轴: ```python In [95]: arr = np.zeros((4, 4)) In [96]: arr_3d = arr[:, np.newaxis, :] In [97]: arr_3d.shape Out[97]: (4, 1, 4) In [98]: arr_1d = np.random.normal(size=3) In [99]: arr_1d[:, np.newaxis] Out[99]: array([[-2.3594], [-0.1995], [-1.542 ]]) In [100]: arr_1d[np.newaxis, :] Out[100]: array([[-2.3594, -0.1995, -1.542 ]]) ``` 因此,如果我们有一个三维数组,并希望对轴 2 进行距平化,那么只需要编写下面这样的代码就可以了: ```python In [101]: arr = np.random.randn(3, 4, 5) In [102]: depth_means = arr.mean(2) In [103]: depth_means Out[103]: array([[-0.4735, 0.3971, -0.0228, 0.2001], [-0.3521, -0.281 , -0.071 , -0.1586], [ 0.6245, 0.6047, 0.4396, -0.2846]]) In [104]: depth_means.shape Out[104]: (3, 4) In [105]: demeaned = arr - depth_means[:, :, np.newaxis] In [106]: demeaned.mean(2) Out[106]: array([[ 0., 0., -0., -0.], [ 0., 0., -0., 0.], [ 0., 0., -0., -0.]]) ``` 有些读者可能会想,在对指定轴进行距平化时,有没有一种既通用又不牺牲性能的方法呢?实际上是有的,但需要一些索引方面的技巧: ```python def demean_axis(arr, axis=0): means = arr.mean(axis) # This generalizes things like [:, :, np.newaxis] to N dimensions indexer = [slice(None)] * arr.ndim indexer[axis] = np.newaxis return arr - means[indexer] ``` ## 通过广播设置数组的值 算术运算所遵循的广播原则同样也适用于通过索引机制设置数组值的操作。对于最简单的情况,我们可以这样做: ```python In [107]: arr = np.zeros((4, 3)) In [108]: arr[:] = 5 In [109]: arr Out[109]: array([[ 5., 5., 5.], [ 5., 5., 5.], [ 5., 5., 5.], [ 5., 5., 5.]]) ``` 但是,假设我们想要用一个一维数组来设置目标数组的各列,只要保证形状兼容就可以了: ```python In [110]: col = np.array([1.28, -0.42, 0.44, 1.6]) In [111]: arr[:] = col[:, np.newaxis] In [112]: arr Out[112]: array([[ 1.28, 1.28, 1.28], [-0.42, -0.42, -0.42], [ 0.44, 0.44, 0.44], [ 1.6 , 1.6 , 1.6 ]]) In [113]: arr[:2] = [[-1.37], [0.509]] In [114]: arr Out[114]: array([[-1.37 , -1.37 , -1.37 ], [ 0.509, 0.509, 0.509], [ 0.44 , 0.44 , 0.44 ], [ 1.6 , 1.6 , 1.6 ]]) ``` # A.4 `ufunc`高级应用 虽然许多 NumPy 用户只会用到通用函数所提供的快速的元素级运算,但通用函数实际上还有一些高级用法能使我们丢开循环而编写出更为简洁的代码。 ## `ufunc`实例方法 NumPy 的各个二元`ufunc`都有一些用于执行特定向量化运算的特殊方法。表 A-2 汇总了这些方法,下面我将通过几个具体的例子对它们进行说明。 `reduce`接受一个数组参数,并通过一系列的二元运算对其值进行聚合(可指明轴向)。例如,我们可以用`np.add.reduce`对数组中各个元素进行求和: ```python In [115]: arr = np.arange(10) In [116]: np.add.reduce(arr) Out[116]: 45 In [117]: arr.sum() Out[117]: 45 ``` 起始值取决于`ufunc`(对于`add`的情况,就是 0)。如果设置了轴号,约简运算就会沿该轴向执行。这就使你能用一种比较简洁的方式得到某些问题的答案。在下面这个例子中,我们用`np.logical_and`检查数组各行中的值是否是有序的: ```python In [118]: np.random.seed(12346) # for reproducibility In [119]: arr = np.random.randn(5, 5) In [120]: arr[::2].sort(1) # sort a few rows In [121]: arr[:, :-1] < arr[:, 1:] Out[121]: array([[ True, True, True, True], [False, True, False, False], [ True, True, True, True], [ True, False, True, True], [ True, True, True, True]], dtype=bool) In [122]: np.logical_and.reduce(arr[:, :-1] < arr[:, 1:], axis=1) Out[122]: array([ True, False, True, False, True], dtype=bool) ``` 注意,`logical_and.reduce`跟`all`方法是等价的。 `ccumulate`跟`reduce`的关系就像`cumsum`跟`sum`的关系那样。它产生一个跟原数组大小相同的中间“累计”值数组: ```python In [123]: arr = np.arange(15).reshape((3, 5)) In [124]: np.add.accumulate(arr, axis=1) Out[124]: array([[ 0, 1, 3, 6, 10], [ 5, 11, 18, 26, 35], [10, 21, 33, 46, 60]]) ``` `outer`用于计算两个数组的叉积: ```python In [125]: arr = np.arange(3).repeat([1, 2, 2]) In [126]: arr Out[126]: array([0, 1, 1, 2, 2]) In [127]: np.multiply.outer(arr, np.arange(5)) Out[127]: array([[0, 0, 0, 0, 0], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 2, 4, 6, 8], [0, 2, 4, 6, 8]]) ``` `outer`输出结果的维度是两个输入数据的维度之和: ```python In [128]: x, y = np.random.randn(3, 4), np.random.randn(5) In [129]: result = np.subtract.outer(x, y) In [130]: result.shape Out[130]: (3, 4, 5) ``` 最后一个方法`reduceat`用于计算“局部约简”,其实就是一个对数据各切片进行聚合的`groupby`运算。它接受一组用于指示如何对值进行拆分和聚合的“面元边界”: ```python In [131]: arr = np.arange(10) In [132]: np.add.reduceat(arr, [0, 5, 8]) Out[132]: array([10, 18, 17]) ``` 最终结果是在`arr[0:5]`、`arr[5:8]`以及`arr[8:]`上执行的约简。跟其他方法一样,这里也可以传入一个`axis`参数: ```python In [133]: arr = np.multiply.outer(np.arange(4), np.arange(5)) In [134]: arr Out[134]: array([[ 0, 0, 0, 0, 0], [ 0, 1, 2, 3, 4], [ 0, 2, 4, 6, 8], [ 0, 3, 6, 9, 12]]) In [135]: np.add.reduceat(arr, [0, 2, 4], axis=1) Out[135]: array([[ 0, 0, 0], [ 1, 5, 4], [ 2, 10, 8], [ 3, 15, 12]]) ``` 表 A-2 总结了部分的`ufunc`方法。 ![表 A `ufunc`方法](/imgs2/7178691-c997bd45000f7b72.png) ## 编写新的`ufunc` 有多种方法可以让你编写自己的 NumPy `ufunc`。最常见的是使用 NumPy C API,但它超越了本书的范围。在本节,我们讲纯粹的 Python `ufunc`。 `numpy.frompyfunc`接受一个 Python 函数以及两个分别表示输入输出参数数量的参数。例如,下面是一个能够实现元素级加法的简单函数: ```python In [136]: def add_elements(x, y): .....: return x + y In [137]: add_them = np.frompyfunc(add_elements, 2, 1) In [138]: add_them(np.arange(8), np.arange(8)) Out[138]: array([0, 2, 4, 6, 8, 10, 12, 14], dtype=object) ``` 用`frompyfunc`创建的函数总是返回 Python 对象数组,这一点很不方便。幸运的是,还有另一个办法,即`numpy.vectorize`。虽然没有`frompyfunc`那么强大,但可以让你指定输出类型: ```python In [139]: add_them = np.vectorize(add_elements, otypes=[np.float64]) In [140]: add_them(np.arange(8), np.arange(8)) Out[140]: array([ 0., 2., 4., 6., 8., 10., 12., 14.]) ``` 虽然这两个函数提供了一种创建`ufunc`型函数的手段,但它们非常慢,因为它们在计算每个元素时都要执行一次 Python 函数调用,这就会比 NumPy 自带的基于 C 的`ufunc`慢很多: ```python In [141]: arr = np.random.randn(10000) In [142]: %timeit add_them(arr, arr) 4.12 ms +- 182 us per loop (mean +- std. dev. of 7 runs, 100 loops each) In [143]: %timeit np.add(arr, arr) 6.89 us +- 504 ns per loop (mean +- std. dev. of 7 runs, 100000 loops each) ``` 本章的后面,我会介绍使用 [Numba](http://numba.pydata.org/),创建快速 Python ufuncs。 # A.5 结构化和记录式数组 你可能已经注意到了,到目前为止我们所讨论的`ndarray`都是一种同质数据容器,也就是说,在它所表示的内存块中,各元素占用的字节数相同(具体根据`dtype`而定)。从表面上看,它似乎不能用于表示异质或表格型的数据。结构化数组是一种特殊的`ndarray`,其中的各个元素可以被看做 C 语言中的结构体(`struct`,这就是“结构化”的由来)或 SQL 表中带有多个命名字段的行: ```python In [144]: dtype = [('x', np.float64), ('y', np.int32)] In [145]: sarr = np.array([(1.5, 6), (np.pi, -2)], dtype=dtype) In [146]: sarr Out[146]: array([( 1.5 , 6), ( 3.1416, -2)], dtype=[('x', ' ``` 刚开始使用`lexsort`的时候可能会比较容易头晕,这是因为键的应用顺序是从最后一个传入的算起的。不难看出,`last_name`是先于`first_name`被应用的。 > 笔记:`Series`和`DataFrame`的`sort_index`以及`Series`的`order`方法就是通过这些函数的变体(它们还必须考虑缺失值)实现的。 ## 其他排序算法 稳定的(stable)排序算法会保持等价元素的相对位置。对于相对位置具有实际意义的那些间接排序而言,这一点非常重要: ```python In [189]: values = np.array(['2:first', '2:second', '1:first', '1:second', .....: '1:third']) In [190]: key = np.array([2, 2, 1, 1, 1]) In [191]: indexer = key.argsort(kind='mergesort') In [192]: indexer Out[192]: array([2, 3, 4, 0, 1]) In [193]: values.take(indexer) Out[193]: array(['1:first', '1:second', '1:third', '2:first', '2:second'], dtype='