Pandas-秘籍第三版-四-

102 阅读47分钟

Pandas 秘籍第三版(四)

原文:annas-archive.org/md5/dbf45b033e25cfae0fd6c82aa3a4578a

译者:飞龙

协议:CC BY-NC-SA 4.0

第十章:一般使用和性能优化建议

到目前为止,我们已经覆盖了 pandas 库的相当大一部分,同时通过示例应用来强化良好的使用习惯。掌握了这些知识后,你现在已经做好准备,踏入实际工作,并将所学的内容应用到数据分析问题中。

本章将提供一些你在独立工作时应牢记的小窍门和建议。本章介绍的食谱是我在多种经验水平的 pandas 用户中经常看到的常见错误。尽管这些做法出发点良好,但不当使用 pandas 构造会浪费很多性能。当数据集较小时,这可能不是大问题,但数据通常是增长的,而不是缩小。使用正确的惯用法并避免低效代码带来的维护负担,可以为你的组织节省大量时间和金钱。

本章将涵盖以下食谱:

  • 避免 dtype=object

  • 注意数据大小

  • 使用矢量化函数代替循环

  • 避免修改数据

  • 使用字典编码低基数数据

  • 测试驱动开发功能

避免使用 dtype=object

在 pandas 中使用 dtype=object 来存储字符串是最容易出错且效率低下的做法之一。不幸的是,在很长一段时间里,dtype=object 是处理字符串数据的唯一方法;直到 1.0 版本发布之前,这个问题才“得到解决”。

我故意将“解决”放在引号中,因为尽管 pandas 1.0 确实引入了 pd.StringDtype(),但直到 3.0 版本发布之前,许多构造和 I/O 方法默认并未使用它。实际上,除非你明确告诉 pandas 否则,在 2.x 版本中,你所有的字符串数据都会使用 dtype=object。值得一提的是,1.0 引入的 pd.StringDtype() 帮助确保你存储字符串,但直到 3.0 版本发布之前,它并未进行性能优化。

如果你使用的是 pandas 3.0 版本及更高版本,可能仍然会遇到一些旧代码,如 ser = ser.astype(object)。通常情况下,这类调用应该被替换为 ser = ser.astype(pd.StringDtype()),除非你确实需要在 pd.Series 中存储 Python 对象。不幸的是,我们无法真正了解原始意图,所以作为开发者,你应该意识到使用 dtype=object 的潜在问题,并学会判断是否可以用 pd.StringDtype() 适当地替代它。

如何实现

我们在第三章《数据类型》中已经讨论过使用 dtype=object 的一些问题,但在此重申并扩展这些问题是值得的。

为了做一个简单的比较,我们来创建两个 pd.Series 对象,它们的数据相同,一个使用 object 数据类型,另一个使用 pd.StringDtype

`ser_obj = pd.Series(["foo", "bar", "baz"] * 10_000, dtype=object) ser_str = pd.Series(["foo", "bar", "baz"] * 10_000, dtype=pd.StringDtype())` 

尝试为 ser_str 分配一个非字符串值将会失败:

`ser_str.iloc[0] = False` 
`TypeError: Cannot set non-string value 'False' into a StringArray.` 

相比之下,pd.Series 类型的对象会很乐意接受我们的 Boolean 值:

`ser_obj.iloc[0] = False` 

反过来,这只会让你更难发现数据中的问题。当我们尝试分配非字符串数据时,使用pd.StringDtype时,失败的地方是非常明显的。而在使用对象数据类型时,直到你在代码的后面尝试做一些字符串操作(比如大小写转换)时,才可能发现问题:

`ser_obj.str.capitalize().head()` 
`0    NaN 1    Bar 2    Baz 3    Foo 4    Bar dtype: object` 

pandas 并没有抛出错误,而是决定将我们在第一行的False条目设置为缺失值。这样的默认行为可能并不是你想要的,但使用对象数据类型时,你对数据质量的控制会大大减弱。

如果你使用的是 pandas 3.0 及更高版本,当安装了 PyArrow 时,你还会发现pd.StringDtype变得显著更快。让我们重新创建我们的pd.Series对象来测量这一点:

`ser_obj = pd.Series(["foo", "bar", "baz"] * 10_000, dtype=object) ser_str = pd.Series(["foo", "bar", "baz"] * 10_000, dtype=pd.StringDtype())` 

为了快速比较执行时间,让我们使用标准库中的timeit模块:

`import timeit timeit.timeit(ser_obj.str.upper, number=1000)` 
`2.2286621460007154` 

将该运行时间与使用正确的pd.StringDtype的情况进行比较:

`timeit.timeit(ser_str.str.upper, number=1000)` 
`2.7227514309997787` 

不幸的是,3.0 版本之前的用户无法看到任何性能差异,但单单是数据验证的改进就足以让你远离dtype=object

那么,避免使用dtype=object的最简单方法是什么?如果你有幸使用 pandas 3.0 及以上版本,你会发现这个数据类型不再那么常见,因为这是该库的自然发展。即便如此,对于仍然使用 pandas 2.x 系列的用户,我建议在 I/O 方法中使用dtype_backend="numpy_nullable"参数:

`import io data = io.StringIO("int_col,string_col\n0,foo\n1,bar\n2,baz") data.seek(0) pd.read_csv(data, dtype_backend="numpy_nullable").dtypes` 
`int_col                Int64 string_col    string[python] dtype: object` 

如果你手动构建一个pd.DataFrame,可以使用pd.DataFrame.convert_dtypes方法,并配合使用相同的dtype_backend="numpy_nullable"参数:

`df = pd.DataFrame([     [0, "foo"],     [1, "bar"],     [2, "baz"], ], columns=["int_col", "string_col"]) df.convert_dtypes(dtype_backend="numpy_nullable").dtypes` 
`int_col                Int64 string_col    string[python] dtype: object` 

请注意,numpy_nullable这个术语有些误导。这个参数本来应该被命名为pandas_nullable,甚至直接命名为pandasnullable也更合适,但当它首次引入时,它与 NumPy 系统密切相关。随着时间的推移,numpy_nullable这个术语保留下来了,但类型已经不再依赖于 NumPy。在本书出版后,可能会有更合适的值来实现相同的行为,基本上是为了找到支持缺失值的 pandas 最佳数据类型。

虽然dtype=object最常被误用来处理字符串,但它在处理日期时间时也暴露了一些问题。我常常看到新用户写出这样的代码,试图创建一个他们认为是日期的pd.Series

`import datetime ser = pd.Series([     datetime.date(2024, 1, 1),     datetime.date(2024, 1, 2),     datetime.date(2024, 1, 3), ]) ser` 
`0    2024-01-01 1    2024-01-02 2    2024-01-03 dtype: object` 

虽然这是一种逻辑上可行的方法,但问题在于 pandas 并没有一个真正的日期类型。相反,这些数据会使用 Python 标准库中的datetime.date类型存储在一个object数据类型的数组中。Python 对象的这种不幸用法掩盖了你正在处理日期的事实,因此,尝试使用pd.Series.dt访问器时会抛出错误:

`ser.dt.year` 
`AttributeError: Can only use .dt accessor with datetimelike values` 

第三章数据类型中,我们简要讨论了 PyArrow 的date32类型,它会是解决这个问题的一个更原生的方案:

`import datetime ser = pd.Series([     datetime.date(2024, 1, 1),     datetime.date(2024, 1, 2),     datetime.date(2024, 1, 3), ], dtype=pd.ArrowDtype(pa.date32())) ser` 
`0    2024-01-01 1    2024-01-02 2    2024-01-03 dtype: date32[day][pyarrow]` 

这样就解锁了pd.Series.dt属性,可以使用了:

`ser.dt.year` 
`0    2024 1    2024 2    2024 dtype: int64[pyarrow]` 

我觉得这个细节有些遗憾,希望未来的 pandas 版本能够抽象化这些问题,但无论如何,它们在当前版本中存在,并且可能会存在一段时间。

尽管我已经指出了dtype=object的一些缺点,但在处理凌乱数据时,它仍然有其用处。有时,你可能对数据一无所知,需要先检查它,才能做出进一步的决策。对象数据类型为你提供了加载几乎任何数据的灵活性,并且可以应用相同的 pandas 算法。虽然这些算法可能效率不高,但它们仍然为你提供了一种一致的方式来与数据交互和探索数据,最终为你争取了时间,帮助你找出如何最好地清理数据并将其存储为更合适的格式。因此,我认为dtype=object最好作为一个暂存区——我不建议将类型保存在其中,但它为你争取时间,以便对数据类型做出断言,可能是一个资产。

留意数据大小

随着数据集的增长,你可能会发现必须选择更合适的数据类型,以确保pd.DataFrame仍然能适应内存。

第三章数据类型中,我们讨论了不同的整数类型,以及它们在内存使用和容量之间的权衡。当处理像 CSV 和 Excel 文件这样的无类型数据源时,pandas 会倾向于使用过多的内存,而不是选择错误的容量。这种保守的做法可能导致系统内存的低效使用,因此,了解如何优化内存使用可能是加载文件和收到OutOfMemory错误之间的关键差异。

如何做

为了说明选择合适的数据类型的影响,我们从一个相对较大的pd.DataFrame开始,这个 DataFrame 由 Python 整数组成:

`df = pd.DataFrame({     "a": [0] * 100_000,     "b": [2 ** 8] * 100_000,     "c": [2 ** 16] * 100_000,     "d": [2 ** 32] * 100_000, }) df = df.convert_dtypes(dtype_backend="numpy_nullable") df.head()` 
 `a    b       c          d 0   0  256  65536  4294967296 1   0  256  65536  4294967296 2   0  256  65536  4294967296 3   0  256  65536  4294967296 4   0  256  65536  4294967296` 

对于整数类型,确定每个pd.Series需要多少内存是一个相对简单的过程。对于pd.Int64Dtype,每条记录是一个 64 位整数,需要 8 个字节的内存。每条记录旁边,pd.Series还会关联一个字节,值为 0 或 1,用来表示记录是否缺失。因此,每条记录总共需要 9 个字节,对于每个pd.Series中的 100,000 条记录,我们的内存使用量应该为 900,000 字节。pd.DataFrame.memory_usage确认这个计算是正确的:

`df.memory_usage()` 
`Index       128 a        900000 b        900000 c        900000 d        900000 dtype: int64` 

如果你知道应该使用什么类型,可以通过.astype显式地为pd.DataFrame的列选择更合适的大小:

`df.assign(     a=lambda x: x["a"].astype(pd.Int8Dtype()),     b=lambda x: x["b"].astype(pd.Int16Dtype()),     c=lambda x: x["c"].astype(pd.Int32Dtype()), ).memory_usage()` 
`Index       128 a        200000 b        300000 c        500000 d        900000 dtype: int64` 

作为一种便捷方式,pandas 可以通过调用 pd.to_numeric 来推断更合适的大小。传递 downcast="signed" 参数将确保我们继续使用带符号整数,并且我们将继续传递 dtype_backend="numpy_nullable" 来确保我们获得适当的缺失值支持:

`df.select_dtypes("number").assign(     **{x: pd.to_numeric(          y, downcast="signed", dtype_backend="numpy_nullable"     ) for x, y in df.items()} ).memory_usage()` 
`Index       128 a        200000 b        300000 c        500000 d        900000 dtype: int64` 

使用向量化函数代替循环

Python 作为一门语言,以其强大的循环能力而著称。无论你是在处理列表还是字典,循环遍历 Python 对象通常是一个相对容易完成的任务,并且能够编写出非常简洁、清晰的代码。

尽管 pandas 是一个 Python 库,但这些相同的循环结构反而成为编写符合 Python 编程习惯且高效代码的障碍。与循环相比,pandas 提供了向量化计算,即对 pd.Series 中的所有元素进行计算,而无需显式地循环。

如何实现

让我们从一个简单的 pd.Series 开始,这个 pd.Series 是由一个范围构造的:

`ser = pd.Series(range(100_000), dtype=pd.Int64Dtype())` 

我们可以使用内置的 pd.Series.sum 方法轻松计算求和:

`ser.sum()` 
`4999950000` 

遍历 pd.Series 并积累自己的结果会得到相同的数字:

`result = 0 for x in ser:     result += x result` 
`4999950000` 

然而,两个代码示例完全不同。使用 pd.Series.sum 时,pandas 在低级语言(如 C)中执行元素求和,避免了与 Python 运行时的任何交互。在 pandas 中,我们称之为向量化函数。

相反,for 循环由 Python 运行时处理,正如你可能知道的那样,Python 比 C 慢得多。

为了提供一些具体的数字,我们可以使用 Python 的 timeit 模块进行简单的时间基准测试。我们先从 pd.Series.sum 开始:

`timeit.timeit(ser.sum, number=1000)` 
`0.04479526499926578` 

我们可以将其与 Python 循环进行比较:

`def loop_sum():     result = 0     for x in ser:         result += x timeit.timeit(loop_sum, number=1000)` 
`5.392715779991704` 

使用循环会导致巨大的性能下降!

通常情况下,你应该使用 pandas 内置的向量化函数来满足大多数分析需求。对于更复杂的应用,使用 .agg.transform.map.apply 方法,这些方法已经在第五章,算法及其应用中讲解过。你应该能避免在 99.99%的分析中使用 for 循环;如果你发现自己更频繁地使用它们,可能需要重新考虑你的设计,通常可以通过仔细重读第五章,算法及其应用来解决。

这个规则的唯一例外情况是当处理 pd.GroupBy 对象时,使用 for 循环可能更合适,因为它可以像字典一样高效地进行迭代:

`df = pd.DataFrame({     "column": ["a", "a", "b", "a", "b"],     "value": [0, 1, 2, 4, 8], }) df = df.convert_dtypes(dtype_backend="numpy_nullable") for label, group in df.groupby("column"):     print(f"The group for label {label} is:\n{group}\n")` 
`The group for label a is:  column  value 0      a      0 1      a      1 3      a      4 The group for label b is:  column  value 2      b      2 4      b      8` 

避免修改数据

尽管 pandas 允许你修改数据,但修改的成本会根据数据类型有所不同。在某些情况下,这可能代价高昂,因此你应该尽可能地减少任何必须执行的修改操作。

如何实现

在考虑数据变更时,应该尽量在加载数据到 pandas 结构之前进行变更。我们可以通过比较加载到 pd.Series 后修改记录所需的时间,轻松地说明性能差异:

`def mutate_after():     data = ["foo", "bar", "baz"]     ser = pd.Series(data, dtype=pd.StringDtype())     ser.iloc[1] = "BAR" timeit.timeit(mutate_after, number=1000)` 
`0.041951814011554234` 

如果变异事先执行,所需的时间:

`def mutate_before():     data = ["foo", "bar", "baz"]     data[1] = "BAR"     ser = pd.Series(data, dtype=pd.StringDtype()) timeit.timeit(mutate_before, number=1000)` 
`0.019495725005981512` 

还有更多...

你可能会陷入一个技术性的深坑,试图解读在不同版本的 pandas 中变异不同数据类型的影响。然而,从 pandas 3.0 开始,行为变得更加一致,这是由于引入了按需写入(Copy-on-Write),这一点是 PDEP-07 提案的一部分。简单来说,每次你尝试变异pd.Seriespd.DataFrame时,都会得到原始数据的一个副本。

尽管这种行为现在更容易预测,但也意味着变异可能非常昂贵,特别是如果你尝试变异一个大的pd.Seriespd.DataFrame

字典编码低基数数据

第三章数据类型中,我们讨论了分类数据类型,它可以通过将字符串(或任何数据类型)替换为更小的整数代码来减少内存使用。虽然第三章数据类型提供了一个很好的技术深度讲解,但考虑到在处理低基数数据时,这可以带来显著的节省,因此在这里再强调一次作为最佳实践是值得的。低基数数据是指唯一值与总记录数的比率相对较低的数据。

如何做

为了进一步强调内存节省的观点,假设我们创建一个低基数pd.Series。我们的pd.Series将有 300,000 行数据,但只有三个唯一值"foo""bar""baz"

`values = ["foo", "bar", "baz"] ser = pd.Series(values * 100_000, dtype=pd.StringDtype()) ser.memory_usage()` 
`2400128` 

仅仅将其更改为分类数据类型,就能大幅提升内存性能:

`cat = pd.CategoricalDtype(values) ser = pd.Series(values * 100_000, dtype=cat) ser.memory_usage()` 
`300260` 

测试驱动开发特点

测试驱动开发(简称TDD)是一种流行的软件开发实践,旨在提高代码质量和可维护性。总体上,TDD 从开发者编写测试开始,测试描述了对变更的预期功能。测试从失败状态开始,开发者在测试最终通过时,可以确信他们的实现是正确的。

测试通常是代码评审者在考虑代码变更时首先查看的内容(在贡献 pandas 时,测试是必须的!)。在接受了一个有测试的变更后,后续的任何代码变更都会重新运行该测试,确保你的代码库随着时间的推移继续按预期工作。通常,正确构造的测试可以帮助你的代码库扩展,同时在开发新特性时减轻回归的风险。

pandas 库提供了工具,使得你可以通过pd.testing模块为你的pd.Seriespd.DataFrame对象编写测试,我们将在本教程中进行回顾。

它是如何工作的

Python 标准库提供了unittest模块,用于声明和自动执行测试。创建测试时,通常会创建一个继承自unittest.TestCase的类,并在该类中创建方法来对程序行为进行断言。

在下面的代码示例中,MyTests.test_42 方法将调用 unittest.TestCase.assertEqual,并传入两个参数,21 * 242。由于这两个参数在逻辑上是相等的,测试执行将通过:

`import unittest class MyTests(unittest.TestCase):     def test_42(self):         self.assertEqual(21 * 2, 42) def suite():     suite = unittest.TestSuite()     suite.addTest(MyTests("test_42"))     return suite runner = unittest.TextTestRunner() runner.run(suite())` 
`. ---------------------------------------------------------------------- Ran 1 test in 0.001s OK <unittest.runner.TextTestResult run=1 errors=0 failures=0>` 

现在让我们尝试使用 pandas 遵循相同的执行框架,不过这次我们不是比较 21 * 242,而是尝试比较两个 pd.Series 对象:

`def some_cool_numbers():     return pd.Series([42, 555, pd.NA], dtype=pd.Int64Dtype()) class MyTests(unittest.TestCase):     def test_cool_numbers(self):         result = some_cool_numbers()         expected = pd.Series([42, 555, pd.NA], dtype=pd.Int64Dtype())         self.assertEqual(result, expected) def suite():     suite = unittest.TestSuite()     suite.addTest(MyTests("test_cool_numbers"))     return suite runner = unittest.TextTestRunner() runner.run(suite())` 
`E ====================================================================== ERROR: test_cool_numbers (__main__.MyTests) ---------------------------------------------------------------------- Traceback (most recent call last):  File "/tmp/ipykernel_79586/2361126771.py", line 9, in test_cool_numbers    self.assertEqual(result, expected)  File "/usr/lib/python3.9/unittest/case.py", line 837, in assertEqual    assertion_func(first, second, msg=msg)  File "/usr/lib/python3.9/unittest/case.py", line 827, in _baseAssertEqual    if not first == second:  File "/home/willayd/clones/Pandas-Cookbook-Third-Edition/lib/python3.9/site-packages/pandas/core/generic.py", line 1577, in __nonzero__    raise ValueError( ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all(). ---------------------------------------------------------------------- Ran 1 test in 0.004s FAILED (errors=1) <unittest.runner.TextTestResult run=1 errors=1 failures=0>` 

哦……这真是让人吃惊!

这里的根本问题是调用 self.assertEqual(result, expected) 执行表达式 result == expected。如果该表达式的结果是 True,测试将通过;返回 False 的表达式将使测试失败。

然而,pandas 重载了 pd.Series 的等于运算符,因此它不会返回 TrueFalse,而是返回另一个进行逐元素比较的 pd.Series

`result = some_cool_numbers() expected = pd.Series([42, 555, pd.NA], dtype=pd.Int64Dtype()) result == expected` 
`0    True 1    True 2    <NA> dtype: boolean` 

由于测试框架不知道如何处理这个问题,你需要使用 pd.testing 命名空间中的自定义函数。对于 pd.Series 的比较,pd.testing.assert_series_equal 是最合适的工具:

`import pandas.testing as tm def some_cool_numbers():     return pd.Series([42, 555, pd.NA], dtype=pd.Int64Dtype()) class MyTests(unittest.TestCase):     def test_cool_numbers(self):         result = some_cool_numbers()         expected = pd.Series([42, 555, pd.NA], dtype=pd.Int64Dtype())         tm.assert_series_equal(result, expected) def suite():     suite = unittest.TestSuite()     suite.addTest(MyTests("test_cool_numbers"))     return suite runner = unittest.TextTestRunner() runner.run(suite())` 
`. ---------------------------------------------------------------------- Ran 1 test in 0.001s   OK <unittest.runner.TextTestResult run=1 errors=0 failures=0>` 

为了完整性,让我们触发一个故意的失败并查看输出:

`def some_cool_numbers():     return pd.Series([42, 555, pd.NA], dtype=pd.Int64Dtype()) class MyTests(unittest.TestCase):     def test_cool_numbers(self):         result = some_cool_numbers()         expected = pd.Series([42, 555, pd.NA], dtype=pd.Int32Dtype())         tm.assert_series_equal(result, expected) def suite():     suite = unittest.TestSuite()     suite.addTest(MyTests("test_cool_numbers"))     return suite runner = unittest.TextTestRunner() runner.run(suite())` 
`F ====================================================================== FAIL: test_cool_numbers (__main__.MyTests) ---------------------------------------------------------------------- Traceback (most recent call last):  File "/tmp/ipykernel_79586/2197259517.py", line 9, in test_cool_numbers    tm.assert_series_equal(result, expected)  File "/home/willayd/clones/Pandas-Cookbook-Third-Edition/lib/python3.9/site-packages/pandas/_testing/asserters.py", line 975, in assert_series_equal    assert_attr_equal("dtype", left, right, obj=f"Attributes of {obj}")  File "/home/willayd/clones/Pandas-Cookbook-Third-Edition/lib/python3.9/site-packages/pandas/_testing/asserters.py", line 421, in assert_attr_equal    raise_assert_detail(obj, msg, left_attr, right_attr)  File "/home/willayd/clones/Pandas-Cookbook-Third-Edition/lib/python3.9/site-packages/pandas/_testing/asserters.py", line 614, in raise_assert_detail    raise AssertionError(msg) AssertionError: Attributes of Series are different Attribute "dtype" are different [left]:  Int64 [right]: Int32 ---------------------------------------------------------------------- Ran 1 test in 0.003s FAILED (failures=1) <unittest.runner.TextTestResult run=1 errors=0 failures=1>` 

在测试失败的追踪信息中,pandas 告诉我们,比较对象的数据类型不同。调用 some_cool_numbers 返回一个带有 pd.Int64Dtypepd.Series,而我们的期望是 pd.Int32Dtype

虽然这些示例集中在使用 pd.testing.assert_series_equal,但对于 pd.DataFrame,等效的方法是 pd.testing.assert_frame_equal。这两个函数知道如何处理可能不同的行索引、列索引、值和缺失值语义,并且如果期望不符合,它们会向测试运行器报告有用的错误信息。

还有更多……

这个示例使用了 unittest 模块,因为它是 Python 语言自带的。然而,许多大型 Python 项目,特别是在科学 Python 领域,使用 pytest 库来编写和执行单元测试。

unittest 不同,pytest 放弃了基于类的测试结构(包括 setUptearDown 方法),转而采用基于测试夹具的方法。关于这两种不同测试范式的比较,可以在 pytest 文档中找到。

pytest 库还提供了一套丰富的插件。有些插件可能旨在改善与第三方库的集成(比如 pytest-djangopytest-sqlalchemy),而其他插件则可能专注于扩展测试套件,利用系统的所有资源(例如 pytest-xdist)。介于两者之间,还有无数的插件使用场景,因此我强烈建议你了解 pytest 及其插件生态系统,以便测试你的 Python 代码库。

加入我们的 Discord 社区

加入我们社区的 Discord 空间,与作者和其他读者一起讨论:

packt.link/pandas

第十一章:pandas 生态系统

虽然 pandas 库提供了大量令人印象深刻的功能,但它的流行很大程度上得益于大量与之互补工作的第三方库。我们不能在本章中覆盖所有这些库,也无法深入探讨每个单独库的工作原理。然而,仅仅知道这些工具的存在并理解它们提供的功能,就能为未来的学习提供很大的启发。

虽然 pandas 是一个令人惊叹的工具,但它也有自己的缺陷,我们在本书中已经尽力突出了这些缺陷;pandas 不可能解决所有分析问题。我强烈建议你熟悉本章中提到的工具,并在寻找新工具和专用工具时,参考 pandas 生态系统的文档(pandas.pydata.org/about/)。

本章的技术说明是,随着库的新版本发布,这些代码块可能会中断或行为发生变化。虽然我们在本书中尽力编写“未来可用”的 pandas 代码,但随着我们讨论第三方依赖库(及其依赖项),要保证这一点变得更加困难。如果你在运行本章代码时遇到问题,请确保参考与本书代码示例一起提供的 requirements.txt 文件。该文件将包含已知与本章兼容的依赖项和版本的列表。

本章将涵盖以下内容:

  • 基础库

  • 探索性数据分析

  • 数据验证

  • 可视化

  • 数据科学

  • 数据库

  • 其他 DataFrame 库

基础库

和许多开源库一样,pandas 在其他基础库的基础上构建了功能,使得它们可以处理低层次的细节,而 pandas 则提供了更友好的功能。如果你希望深入研究比在 pandas 中学到的更技术性的细节,那么这些库就是你需要关注的重点。

NumPy

NumPy 自称为Python 科学计算的基础包,它是 pandas 最初构建之上的库。实际上,NumPy 是一个n维度的库,因此你不仅仅局限于像 pd.DataFrame(pandas 实际上曾经提供过 3 维和 4 维的面板结构,但现在这些已不再使用)那样的二维数据。

在本书中,我们向你展示了如何从 NumPy 对象构建 pandas 对象,如以下 pd.DataFrame 构造函数所示:

`arr = np.arange(1, 10).reshape(3, -1) df = pd.DataFrame(arr) df` 
 `0    1    2 0    1    2    3 1    4    5    6 2    7    8    9` 

然而,你也可以通过使用 pd.DataFrame.to_numpy 方法,从 pd.DataFrame 对象创建 NumPy 数组:

`df.to_numpy()` 
`array([[1, 2, 3],       [4, 5, 6],       [7, 8, 9]])` 

许多 NumPy 函数接受 pd.DataFrame 作为参数,甚至仍然会返回一个 pd.DataFrame

`np.log(df)` 
 `0           1           2 0    0.000000    0.693147    1.098612 1    1.386294    1.609438    1.791759 2    1.945910    2.079442    2.197225` 

需要记住的主要一点是,NumPy 与 pandas 的互操作性会在你需要处理非浮动类型的缺失值时下降,或者更一般地,当你尝试使用既非整数也非浮动点类型的数据时。

这一点的具体规则过于复杂,无法在本书中列出,但通常,我建议除非是浮动点或整数数据,否则不要调用 pd.Series.to_numpypd.DataFrame.to_numpy

PyArrow

pandas 的另一个主要依赖库是 Apache Arrow,Apache Arrow 自称是一个 跨语言的内存分析开发平台。这个项目由 pandas 的创建者 Wes McKinney 启动,并在他具有影响力的博客文章《Apache Arrow 与 我讨厌 pandas 的 10 件事》中宣布 (wesmckinney.com/blog/apache-arrow-pandas-internals/)。Apache Arrow 项目为一维数据结构定义了内存布局,允许不同的语言、程序和库共享相同的数据。除了定义这些结构外,Apache Arrow 项目还提供了一整套工具,供库实现 Apache Arrow 的规范。

本书中在特定场景下使用了 Apache Arrow 的 Python 实现 PyArrow。虽然 pandas 没有提供将 pd.DataFrame 转换为 PyArrow 的方法,但 PyArrow 库提供了一个 pa.Table.from_pandas 方法,专门用于此目的:

`tbl = pa.Table.from_pandas(df) tbl` 
`pyarrow.Table 0: int64 1: int64 2: int64 ---- 0: [[1,4,7]] 1: [[2,5,8]] 2: [[3,6,9]]` 

PyArrow 同样提供了一个 pa.Table.to_pandas 方法,可以将 pa.Table 转换为 pd.DataFrame

`tbl.to_pandas()` 
 `0    1    2 0    1    2    3 1    4    5    6 2    7    8    9` 

通常,PyArrow 被认为是比 pandas 更低级的库。它主要旨在为其他库的开发者提供服务,而非为寻求 DataFrame 库的一般用户提供服务,因此,除非你正在编写库,否则你可能不需要经常将 pd.DataFrame 转换为 PyArrow。然而,随着 Apache Arrow 生态系统的发展,pandas 和 PyArrow 可以互操作这一事实为 pandas 与许多其他分析库和数据库的集成提供了无限的可能性。

探索性数据分析

很多时候,你会遇到一个对数据几乎不了解的数据集。在本书中,我们展示了手动筛选数据的方法,但也有一些工具可以帮助自动化这些潜在的繁琐任务,帮助你更短时间内掌握数据。

YData Profiling

YData Profiling 自称是“领先的数据概况分析包,能够自动化并标准化生成详细报告,包含统计信息和可视化”。虽然我们在可视化章节中已经学会了如何手动探索数据,但这个包可以作为一个快速启动工具,自动生成许多有用的报告和特性。

为了将其与我们在那些章节中做的工作进行对比,我们再来看一看车辆数据集。现在,我们只挑选一个小子集的列,以保持我们的 YData Profiling 尽量简洁;对于大型数据集,性能往往会下降:

`df = pd.read_csv(     "data/vehicles.csv.zip",     dtype_backend="numpy_nullable",     usecols=[         "id",         "engId",         "make",         "model",         "cylinders",         "city08",         "highway08",         "year",         "trany",     ] ) df.head()` 
 `city08   cylinders   engId   …   model               trany           year 0   19       4           9011    …   Spider Veloce 2000  Manual 5-spd    1985 1   9        12          22020   …   Testarossa          Manual 5-spd    1985 2   23       4           2100    …   Charger             Manual 5-spd    1985 3   10       8           2850    …   B150/B250 Wagon 2WD Automatic 3-spd 1985 4   17       4           66031   …   Legacy AWD Turbo    Manual 5-spd    1993 5 rows × 9 columns` 

YData Profiling 使你能够轻松创建一个概况报告,该报告包含许多常见的可视化内容,并有助于描述你在pd.DataFrame中工作的各列。

本书是使用ydata_profiling版本 4.9.0 编写的。要创建概况报告,只需运行:

`from ydata_profiling import ProfileReport profile = ProfileReport(df, title="Vehicles Profile Report")` 

如果在 Jupyter notebook 中运行代码,你可以直接在 notebook 中看到输出,方法是调用:

`profile.to_widgets()` 

如果你没有使用 Jupyter,你还可以将该概况导出为本地 HTML 文件,然后从那里打开:

`profile.to_file("vehicles_profile.html")` 

在查看概况时,你首先会看到一个高层次的概述部分,其中列出了缺失数据的单元格数、重复行数等:

一张计算机屏幕截图 说明自动生成

图 11.1:YData Profiling 提供的概述

每一列来自pd.DataFrame的数据都会被详细列出。如果某列包含连续值,YData Profiling 会为你创建一个直方图:

一张图表截图 说明自动生成

图 11.2:YData Profiling 生成的直方图

对于分类变量,该工具将生成一个词云可视化图:

一张计算机屏幕截图 说明自动生成

图 11.3:YData Profiling 生成的词云

为了理解你的连续变量是否存在相关性,概况报告中包含了一个简洁的热图,根据每一对变量之间的关系着色:

一张颜色图表截图 说明自动生成

图 11.4:YData Profiling 生成的热图

尽管你可能仍然需要深入分析数据集,超出该库所提供的功能,但它是一个很好的起点,并可以帮助自动生成那些本来可能是繁琐的图表。

数据验证

计算中的“垃圾进,垃圾出”原则指出,无论你的代码多么出色,如果从质量差的数据开始,分析结果也会是质量差的。数据从业者经常面临诸如意外缺失数据、重复值和建模实体之间断裂关系等问题。

幸运的是,有一些工具可以帮助你自动化输入到模型和从模型输出的数据,这样可以确保你所执行工作的可信度。在这篇文章中,我们将介绍 Great Expectations。

Great Expectations

本书是使用 Great Expectations 版本 1.0.2 编写的。为了开始,让我们再次看看我们的车辆数据集:

`df = pd.read_csv(     "data/vehicles.csv.zip",     dtype_backend="numpy_nullable",     dtype={         "rangeA": pd.StringDtype(),         "mfrCode": pd.StringDtype(),         "c240Dscr": pd.StringDtype(),         "c240bDscr": pd.StringDtype()     } ) df.head()` 
 `barrels08  barrelsA08  charge120  …  phevCity  phevHwy  phevComb 0  14.167143  0.0         0.00         0        0 1  27.046364  0.0         0.00         0        0 2  11.018889  0.0         0.00         0        0 3  27.046364  0.0         0.00         0        0 4  15.658421  0.0         0.00         0        0 5 rows × 84 columns` 

使用 Great Expectations 的方式有几种,不是所有的方式都能在本手册中记录。为了展示一个自包含的例子,我们将在内存中设置并处理所有的期望。

为了实现这一点,我们将导入great_expectations库,并为我们的测试创建一个context

`import great_expectations as gx context = gx.get_context()` 

在此上下文中,你可以创建数据源和数据资产。对于像 SQL 这样的非 DataFrame 源,数据源通常会包含连接凭证,而对于存在内存中的pd.DataFrame,则无需做太多工作。数据资产是用于存储结果的分组机制。这里我们只创建了一个数据资产,但在实际应用中,你可能会决定创建多个资产来存储和组织 Great Expectations 输出的验证结果:

`datasource = context.data_sources.add_pandas(name="pandas_datasource") data_asset = datasource.add_dataframe_asset(name="vehicles")` 

从这里,你可以在 Great Expectations 中创建一个批次定义。对于非 DataFrame 源,批次定义会告诉库如何从源中获取数据。对于 pandas,批次定义将简单地从关联的pd.DataFrame中检索所有数据:

`batch_definition_name = "dataframe_definition" batch_definition = data_asset.add_batch_definition_whole_dataframe(     batch_definition_name ) batch = batch_definition.get_batch(batch_parameters={     "dataframe": df })` 

到此时,你可以开始对数据进行断言。例如,你可以使用 Great Expectations 来确保某一列不包含任何空值:

`city_exp = gx.expectations.ExpectColumnValuesToNotBeNull(     column="city08" ) result = batch.validate(city_exp) print(result)` 
`{   "success": true,   "expectation_config": {     "type": "expect_column_values_to_not_be_null",     "kwargs": {       "batch_id": "pandas_datasource-vehicles",       "column": "city08"     },     "meta": {}   },   "result": {     "element_count": 48130,     "unexpected_count": 0,     "unexpected_percent": 0.0,     "partial_unexpected_list": [],     "partial_unexpected_counts": [],     "partial_unexpected_index_list": []   },   "meta": {},   "exception_info": {     "raised_exception": false,     "exception_traceback": null,     "exception_message": null   } }` 

同样应用于cylinders列的期望将不会成功:

`cylinders_exp = gx.expectations.ExpectColumnValuesToNotBeNull(     column="cylinders" ) result = batch.validate(cylinders_exp) print(result)` 
`{   "success": false,   "expectation_config": {     "type": "expect_column_values_to_not_be_null",     "kwargs": {       "batch_id": "pandas_datasource-vehicles",       "column": "cylinders"     },     "meta": {}   },   "result": {     "element_count": 48130,     "unexpected_count": 965,     "unexpected_percent": 2.0049864949096197,     "partial_unexpected_list": [       null,       null,       ...       null,       null     ],     "partial_unexpected_counts": [       {         "value": null,         "count": 20       }     ],     "partial_unexpected_index_list": [       7138,       7139,       8143,       ...       23022,       23023,       23024     ]   },   "meta": {},   "exception_info": {     "raised_exception": false,     "exception_traceback": null,     "exception_message": null   } }` 

为了简洁起见,我们只展示了如何设置关于空值的期望,但你可以在greatexpectations.io/expectations/中找到完整的期望库,供你用于其他断言。Great Expectations 还与其他工具(如 Spark、PostgreSQL 等)兼容,因此你可以在数据转换管道中的多个点应用你的期望。

可视化

在第6 章可视化中,我们详细讨论了使用 matplotlib 进行可视化,并且还讨论了使用 Seaborn 进行高级图表的绘制。这些工具非常适合生成静态图表,但当你想要加入一些交互性时,你需要选择其他的库。

对于这个例子,我们将加载来自车辆数据集的数据,这是我们在第6 章散点图例子中使用过的:

`df = pd.read_csv(     "data/vehicles.csv.zip",     dtype_backend="numpy_nullable",     dtype={         "rangeA": pd.StringDtype(),         "mfrCode": pd.StringDtype(),         "c240Dscr": pd.StringDtype(),         "c240bDscr": pd.StringDtype()     } ) df.head()` 
 `barrels08  barrelsA08  charge120  …  phevCity  phevHwy  phevComb 0  14.167143  0.0         0.00         0        0 1  27.046364  0.0         0.00         0        0 2  11.018889  0.0         0.00         0        0 3  27.046364  0.0         0.00         0        0 4  15.658421  0.0         0.00         0        0 5 rows × 84 columns` 

Plotly

让我们先来看看 Plotly,它可以用来创建具有高交互性的可视化图表,因此在 Jupyter 笔记本中非常受欢迎。使用它时,只需将plotly作为backend=参数传递给pd.DataFrame.plot。我们还将添加一个hover_data=参数,Plotly 可以用它为每个数据点添加标签:

`df.plot(     kind="scatter",     x="city08",     y="highway08",     backend="plotly",     hover_data={"make": True, "model": True, "year": True}, )` 

如果你在 Jupyter 笔记本或 HTML 页面中查看此内容,你将看到你可以将鼠标悬停在任何数据点上,查看更多细节:

图 11.5: 使用 Plotly 悬停在数据点上

你甚至可以选择图表的某个区域,以便放大数据点:

图 11.6: 使用 Plotly 进行缩放

如您所见,Plotly 与您在本书中看到的相同的 pandas API 配合使用,非常简单。如果您希望图表具有互动性,这是一个非常适合使用的工具。

PyGWalker

到目前为止,您看到的所有绘图代码都是声明式的;即,您告诉 pandas 您想要一个条形图、折线图、散点图等,然后 pandas 为您生成相应的图表。然而,许多用户可能更喜欢一种“自由形式”的工具进行探索,在这种工具中,他们可以直接拖放元素,即时制作图表。

如果这正是您所追求的,那么您将希望查看 PyGWalker 库。通过一个非常简洁的 API,您可以在 Jupyter 笔记本中生成一个互动工具,您可以拖放不同的元素来生成各种图表:

`import pygwalker as pyg pyg.walk(df)` 

图 11.7:Jupyter 笔记本中的 PyGWalker

数据科学

虽然 pandas 提供了一些内建的统计算法,但它无法涵盖所有数据科学领域使用的统计和机器学习算法。幸运的是,许多专注于数据科学的库与 pandas 有很好的集成,让您能够在不同库之间无缝地移动数据。

scikit-learn

scikit-learn 是一个流行的机器学习库,能够帮助进行监督学习和无监督学习。scikit-learn 库提供了一个令人印象深刻的算法库,用于分类、预测和聚类任务,同时还提供了数据预处理和清洗工具。

我们无法涵盖所有这些功能,但为了展示一些内容,我们再次加载车辆数据集:

`df = pd.read_csv(     "data/vehicles.csv.zip",     dtype_backend="numpy_nullable",     dtype={         "rangeA": pd.StringDtype(),         "mfrCode": pd.StringDtype(),         "c240Dscr": pd.StringDtype(),         "c240bDscr": pd.StringDtype()     } ) df.head()` 
 `barrels08  barrelsA08  charge120  …  phevCity  phevHwy  phevComb 0  14.167143  0.0         0.00         0        0 1  27.046364  0.0         0.00         0        0 2  11.018889  0.0         0.00         0        0 3  27.046364  0.0         0.00         0        0 4  15.658421  0.0         0.00         0        0 5 rows × 84 columns` 

现在假设我们想创建一个算法来预测车辆的综合行驶里程,从数据中的其他属性推断出来。由于里程是一个连续变量,我们可以选择线性回归模型来进行预测。

我们将使用的线性回归模型希望使用的特征也应该是数值型的。虽然我们可以通过某些方法将部分非数值数据人工转换为数值(例如,使用*《第五章,算法及其应用》中提到的One-hot 编码与 pd.get_dummies技术),但我们现在暂时忽略任何非数值列。线性回归模型也无法处理缺失数据。我们从《第六章,探索连续数据》探索连续数据*食谱中知道,该数据集有两个缺失数据的连续变量。虽然我们可以尝试插值来填充这些值,但在这个例子中,我们还是采用简单的方法,直接删除这些数据:

`num_df = df.select_dtypes(include=["number"]) num_df = num_df.drop(columns=["cylinders", "displ"])` 

scikit-learn 模型需要知道我们想用来进行预测的 特征(通常标记为 X)和我们试图预测的目标变量(通常标记为 y)。将数据拆分为训练集和测试集也是一个好习惯,我们可以使用 train_test_split 函数来完成:

`from sklearn.model_selection import train_test_split target_col = "comb08" X = num_df.drop(columns=[target_col]) y = num_df[target_col] X_train, X_test, y_train, y_test = train_test_split(X, y)` 

使用我们这样的数据格式,我们可以继续训练线性回归模型,然后将其应用于测试数据,以生成预测结果:

`from sklearn import linear_model   regr = linear_model.LinearRegression() regr.fit(X_train, y_train) y_pred = regr.predict(X_test)` 

现在我们已经从测试数据集得到了预测结果,我们可以将其与实际的测试数据对比。这是衡量我们训练模型准确性的好方法。

管理模型准确性的方式有很多种,但现在我们可以选择一个常用且相对简单的 mean_squared_error,这是 scikit-learn 也提供的一个便捷函数:

`from sklearn.metrics import mean_squared_error mean_squared_error(y_test, y_pred)` 
`0.11414180317382835` 

如果你有兴趣了解更多,我强烈推荐你阅读 scikit-learn 网站上的文档和示例,或者阅读像《Machine Learning with PyTorch and Scikit-Learn: Develop machine learning and deep learning models with Python》这样的书籍 (www.packtpub.com/en-us/product/machine-learning-with-pytorch-and-scikit-learn-9781801819312)。

XGBoost

现在让我们转向另一个出色的机器学习库 XGBoost,它使用梯度提升算法实现了多种算法。XGBoost 性能极其出色,扩展性强,在机器学习竞赛中表现优异,且与存储在 pd.DataFrame 中的数据兼容。如果你已经熟悉 scikit-learn,那么它使用的 API 会让你感到非常熟悉。

XGBoost 可用于分类和回归。由于我们刚刚使用 scikit-learn 进行了回归分析,接下来让我们通过一个分类示例来尝试预测车辆的品牌,这个预测是基于数据集中的数值特征。

我们正在使用的车辆数据集包含 144 种不同的品牌。为了进行分析,我们将选择一小部分消费者品牌:

`brands = {"Dodge", "Toyota", "Volvo", "BMW", "Buick", "Audi", "Volkswagen", "Subaru"} df2 = df[df["make"].isin(brands)] df2 = df2.drop(columns=["cylinders", "displ"])` 

接下来,我们将把数据分为特征(X)和目标变量(y)。为了适应机器学习算法,我们还需要将目标变量转换为类别数据类型,以便算法能够预测像 012 等值,而不是像 "Dodge""Toyota""Volvo" 这样的字符串:

`X = df2.select_dtypes(include=["number"]) y = df2["make"].astype(pd.CategoricalDtype())` 

在此基础上,我们可以再次使用 scikit-learn 中的 train_test_split 函数来创建训练数据和测试数据。请注意,我们使用了 pd.Series.cat.codes 来获取分配给类别数据类型的数字值,而不是字符串:

`X_train, X_test, y_train, y_test = train_test_split(X, y.cat.codes)` 

最后,我们可以从 XGBoost 中导入 XGBClassifier,将其在我们的数据上进行训练,并应用于测试特征以生成预测结果:

`from xgboost import XGBClassifier bst = XGBClassifier() bst.fit(X_train, y_train) preds = bst.predict(X_test)` 

现在我们得到了预测结果,可以验证它们中有多少与测试数据中包含的目标变量相匹配:

`accuracy = (preds == y_test).sum() / len(y_test) print(f"Model prediction accuracy is: {accuracy:.2%}")` 
`Model prediction accuracy is: 97.07%` 

再次强调,我们只是简单地触及了像 XGBoost 这样的库所能做的皮毛。你可以通过许多不同的方法来调整模型,以提高准确性、避免过拟合/欠拟合、优化不同的结果等等。对于想要深入了解这个优秀库的用户,我建议查看 XGBoost 的文档或像 Hands-On Gradient Boosting with XGBoost and scikit-learn 这样的书籍。

数据库

数据库知识是任何数据从业人员工具包中的重要工具。虽然 pandas 是一个适用于单机内存计算的优秀工具,但数据库提供了一套非常互补的分析工具,可以帮助存储和分发分析过程。

第四章pandas I/O 系统 中,我们介绍了如何在 pandas 和理论上任何数据库之间转移数据。然而,一个相对较新的数据库叫做 DuckDB,值得额外关注,因为它可以让你更加无缝地将 DataFrame 和数据库的世界连接在一起。

DuckDB

DuckDB 是一个轻量级数据库系统,提供与 Apache Arrow 的零复制集成,这项技术也支持与 pandas 高效的数据共享和使用。它非常轻量,并且与大多数数据库系统不同,可以轻松嵌入到其他工具或流程中。最重要的是,DuckDB 针对分析型工作负载进行了优化。

DuckDB 使你可以轻松地使用 SQL 查询 pd.DataFrame 中的数据。让我们通过加载车辆数据集来实际操作一下:

`df = pd.read_csv(     "data/vehicles.csv.zip",     dtype_backend="numpy_nullable",     dtype={         "rangeA": pd.StringDtype(),         "mfrCode": pd.StringDtype(),         "c240Dscr": pd.StringDtype(),         "c240bDscr": pd.StringDtype()     } ) df.head()` 
 `barrels08  barrelsA08  charge120  …  phevCity  phevHwy  phevComb 0  14.167143  0.0         0.00         0        0 1  27.046364  0.0         0.00         0        0 2  11.018889  0.0         0.00         0        0 3  27.046364  0.0         0.00         0        0 4  15.658421  0.0         0.00         0        0 5 rows × 84 columns` 

通过将 CREATE TABLE 语句传递给 duckdb.sql,你可以将数据从 pd.DataFrame 加载到表格中:

`import duckdb duckdb.sql("CREATE TABLE vehicles AS SELECT * FROM df")` 

一旦表格创建完成,你就可以通过 SQL 查询它:

`duckdb.sql("SELECT COUNT(*) FROM vehicles WHERE make = 'Honda'")` 
`┌──────────────┐ │ count_star() │ │    int64     │ ├──────────────┤ │         1197 │ └──────────────┘` 

如果你想将结果转换回 pd.DataFrame,你可以使用 .df 方法:

`duckdb.sql(     "SELECT make, model, year, id, city08 FROM vehicles where make = 'Honda' LIMIT 5" ).df()` 
 `make    model          year   id      city08 0   Honda   Accord Wagon   1993   10145   18 1   Honda   Accord Wagon   1993   10146   20 2   Honda   Civic Del Sol  1994   10631   25 3   Honda   Civic Del Sol  1994   10632   30 4   Honda   Civic Del Sol  1994   10633   23` 

要深入了解 DuckDB,我强烈建议查看 DuckDB 的文档,并且为了更好地理解它在数据库领域的定位,阅读 Why DuckDB 文章(duckdb.org/why_duckdb)。通常,DuckDB 的重点是单用户分析,但如果你对共享的云端数据仓库感兴趣,也可以看看 MotherDuck(motherduck.com/)。

其他 DataFrame 库

pandas 开发不久后,它成为了 Python 领域事实上的 DataFrame 库。从那时起,许多新的 DataFrame 库在该领域得以开发,它们都旨在解决 pandas 的一些不足之处,同时引入自己独特的设计决策。

Ibis

Ibis 是另一个由 pandas 创始人 Wes McKinney 创建的出色分析工具。从高层次来看,Ibis 是一个 DataFrame 的“前端”,通过一个通用的 API,你可以查询多个“后端”。

为了帮助理解这一点,值得与 pandas 的设计方法进行对比。在 pandas 中,进行分组和求和的 API 或“前端”看起来是这样的:

`df.groupby("column").agg(result="sum")` 
pd.DataFrame.groupby). Behind the scenes, pandas dictates how the pd.DataFrame is stored (in memory using pandas’ own representation) and even dictates how the summation should be performed against that in-memory representation.

在 Ibis 中,类似的表达式看起来是这样的:

`df.group_by("column").agg(result=df.sum())` 

尽管暴露给用户的 API 可能没有太大不同,但 Ibis 和 pandas 之间的相似性就此为止。Ibis 不规定你查询的数据应该如何存储;它可以存储在 BigQuery、DuckDB、MySQL、PostgreSQL 等数据库中,甚至可以存储在像 pandas 这样的其他 DataFrame 库中。除了存储之外,Ibis 也不规定如何执行求和操作;它将这一过程交给执行引擎。许多 SQL 数据库有自己的执行引擎,但也有一些可能会依赖于像 Apache DataFusion (datafusion.apache.org/) 这样的第三方库。

要通过 Ibis 使用 pd.DataFrame,你需要使用 ibis.memtable 函数将其包装起来:

`import ibis df = pd.read_csv(     "data/vehicles.csv.zip",     dtype_backend="numpy_nullable",     usecols=["id", "year", "make", "model", "city08"], ) t = ibis.memtable(df)` 

完成这些后,你可以像使用 pandas 一样,使用 Ibis API 查询数据:

`t.filter(t.make == "Honda").select("make", "model", "year", "city08")` 
`r0 := InMemoryTable   data:     PandasDataFrameProxy:              city08     id        make                model  year       0          19      1  Alfa Romeo   Spider Veloce 2000  1985       1           9     10     Ferrari           Testarossa  1985       2          23    100       Dodge              Charger  1985       3          10   1000       Dodge  B150/B250 Wagon 2WD  1985       4          17  10000      Subaru     Legacy AWD Turbo  1993       ...       ...    ...         ...                  ...   ...       48125      19   9995      Subaru               Legacy  1993       48126      20   9996      Subaru               Legacy  1993       48127      18   9997      Subaru           Legacy AWD  1993       48128      18   9998      Subaru           Legacy AWD  1993       48129      16   9999      Subaru     Legacy AWD Turbo  1993         [48130 rows x 5 columns]  r1 := Filter[r0]   r0.make == 'Honda'  Project[r1]   make:   r1.make   model:  r1.model   year:   r1.year   city08: r1.city08` 

值得注意的是,前面的代码实际上并没有返回结果。与 pandas 不同,pandas 会“急切地”执行你给它的所有操作,而 Ibis 会收集你想要的所有表达式,并且直到明确要求时才执行。这种做法通常被称为 推迟执行惰性执行

推迟执行的优势在于 Ibis 可以找到优化你请求执行的查询的方法。我们的查询要求 Ibis 查找所有“make”为 Honda 的行,并选择几个列,但对于底层数据库来说,先选择列再执行过滤可能会更快。这一过程对最终用户是透明的;用户只需要告诉 Ibis 他们需要什么,Ibis 会处理如何获取这些数据。

要将其转化为 pd.DataFrame,可以链式调用 .to_pandas

`t.filter(t.make == "Honda").select("make", "model", "year", "city08").to_pandas().head()` 
 `make    model          year   city08 0    Honda   Accord Wagon   1993   18 1    Honda   Accord Wagon   1993   20 2    Honda   Civic Del Sol  1994   25 3    Honda   Civic Del Sol  1994   30 4    Honda   Civic Del Sol  1994   23` 

然而,你并不一定要返回 pd.DataFrame。如果你想要一个 PyArrow 表格,完全可以选择 .to_pyarrow

`t.filter(t.make == "Honda").select("make", "model", "year", "city08").to_pyarrow()` 
`pyarrow.Table make: string model: string year: int64 city08: int64 ---- make: [["Honda","Honda","Honda","Honda","Honda",...,"Honda","Honda","Honda","Honda","Honda"]] model: [["Accord Wagon","Accord Wagon","Civic Del Sol","Civic Del Sol","Civic Del Sol",...,"Prelude","Prelude","Prelude","Accord","Accord"]] year: [[1993,1993,1994,1994,1994,...,1993,1993,1993,1993,1993]] city08: [[18,20,25,30,23,...,21,19,19,19,21]]` 

有关 Ibis 的更多信息,请务必查看 Ibis 文档。甚至有一个专门面向来自 pandas 用户的 Ibis 教程。

Dask

另一个与 pandas 紧密相关的流行库是 Dask。Dask 是一个框架,它提供了与 pd.DataFrame 类似的 API,但将其使用扩展到并行计算和超出系统可用内存的数据集。

如果我们想将车辆数据集转换为 Dask DataFrame,可以使用 dask.dataframe.from_pandas 函数,并设置 npartitions= 参数来控制如何划分数据集:

`import dask.dataframe as dd ddf = dd.from_pandas(df, npartitions=10)` 
``/home/willayd/clones/Pandas-Cookbook-Third-Edition/lib/python3.9/site-packages/dask/dataframe/__init__.py:42: FutureWarning: Dask dataframe query planning is disabled because dask-expr is not installed. You can install it with `pip install dask[dataframe]` or `conda install dask`. This will raise in a future version.  warnings.warn(msg, FutureWarning)`` 

通过将 DataFrame 划分为不同的分区,Dask 允许你对每个分区并行执行计算,这对性能和可扩展性有极大的帮助。

就像 Ibis 一样,Dask 也懒惰地执行计算。如果你想强制执行计算,你需要调用 .compute 方法:

`ddf.size.compute()` 
`3991932` 

要从 Dask DataFrame 转回 pandas,只需调用 ddf.compute

`ddf.compute().head()` 
 `city08    id      make          model                 year 0    19        1       Alfa Romeo    Spider Veloce 2000    1985 1    9         10      Ferrari       Testarossa            1985 2    23        100     Dodge         Charger               1985 3    10        1000    Dodge         B150/B250 Wagon 2WD   1985 4    17        10000   Subaru        Legacy AWD Turbo      1993` 

Polars

Polars 是 DataFrame 领域的新秀,并在非常短的时间内开发出了令人印象深刻的功能,并拥有了一群忠实的追随者。Polars 库是 Apache Arrow 原生的,因此它拥有比 pandas 当前提供的更清晰的类型系统和一致的缺失值处理(关于 pandas 类型系统及其所有缺陷的历史,请务必阅读 第三章数据类型)。

除了更简洁、更清晰的类型系统外,Polars 还能够扩展到大于内存的数据集,它甚至提供了一个懒执行引擎,并配备了查询优化器,使得编写高效、可扩展的代码变得更加容易。

对于从 pandas 到 Polars 的简单转换,你可以使用 polars.from_pandas

`import polars as pl pl_df = pl.from_pandas(df) pl_df.head()` 
`shape: (5, 84) barrels08  barrelsA08  charge120  charge240  ...  phevCity  phevHwy  phevComb f64        f64         f64        f64        ...  i64       i64      i64 14.167143  0.0         0.0        0.0        ...    0         0        0 27.046364  0.0         0.0        0.0        ...    0         0        0 11.018889  0.0         0.0        0.0        ...    0         0        0 27.046364  0.0         0.0        0.0        ...    0         0        0 15.658421  0.0         0.0        0.0        ...    0         0        0` 

对于懒执行,你可能会想试试 pl.LazyFrame,它可以直接将 pd.DataFrame 作为参数:

`lz_df = pl.LazyFrame(df)` 

就像我们在 Ibis 中看到的那样,Polars 的懒执行引擎可以优化执行筛选和选择操作的最佳路径。要执行计划,你需要将 pl.LazyFrame.collect 链接起来:

`lz_df.filter(     pl.col("make") == "Honda" ).select(["make", "model", "year", "city08"]).collect().head()` 
`shape: (5, 4) make    model   year    city08 str     str     i64     i64 "Honda" "Accord Wagon"  1993    18 "Honda" "Accord Wagon"  1993    20 "Honda" "Civic Del Sol" 1994    25 "Honda" "Civic Del Sol" 1994    30 "Honda" "Civic Del Sol" 1994    23` 

如果你想从 Polars 转回 pandas,pl.DataFramepl.LazyFrame 都提供了 .to_pandas 方法:

`lz_df.filter(     pl.col("make") == "Honda" ).select(["make", "model", "year", "city08"]).collect().to_pandas().head()` 
 `make   model          year  city08 0     Honda  Accord Wagon   1993  18 1     Honda  Accord Wagon   1993  20 2     Honda  Civic Del Sol  1994  25 3     Honda  Civic Del Sol  1994  30 4     Honda  Civic Del Sol  1994  23` 

若想更详细地了解 Polars 及其所有优秀的功能,建议查看 Polars Cookbook (www.packtpub.com/en-us/product/polars-cookbook-9781805121152)。

cuDF

如果你拥有 Nvidia 设备并且已经安装了 CUDA 工具包,你可能会对 cuDF 感兴趣。理论上,cuDF 是 pandas 的“即插即用”替代品;只要你有合适的硬件和工具,就可以将 pandas 表达式运行在你的 GPU 上,只需在 pandas 之前导入 cuDF:

`import cudf.pandas cudf.pandas.install() import pandas as pd` 

鉴于现代 GPU 相较于 CPU 的强大,这个库能够为用户提供显著的性能提升,而无需改变代码的编写方式。对于拥有合适硬件的用户来说,这种即插即用的性能提升可能是无价的。

加入我们的社区 Discord

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

packt.link/pandas

留下评价!

感谢你购买这本由 Packt Publishing 出版的书籍——我们希望你喜欢它!你的反馈非常宝贵,能够帮助我们改进和成长。请花一点时间在 Amazon 上留下评论,这只需要一分钟,但对像你这样的读者来说意义重大。

扫描下方的二维码,领取你选择的免费电子书。

packt.link/NzOWQ

packt.com

订阅我们的在线数字图书馆,全面访问超过 7,000 本书籍和视频,及行业领先的工具,帮助你规划个人发展并推进职业生涯。欲了解更多信息,请访问我们的网站。

为什么订阅?

  • 通过来自 4000 多位行业专家的实用电子书和视频,减少学习时间,增加编程时间

  • 通过专为你打造的技能计划,提升你的学习效果

  • 每月免费获得一本电子书或视频

  • 完全可搜索,便于快速访问关键信息

  • 复制、粘贴、打印和收藏内容

www.packt.com上,你还可以阅读一系列免费的技术文章,注册各种免费的电子邮件通讯,并获得 Packt 图书和电子书的独家折扣和优惠。

你可能会喜欢的其他书籍

如果你喜欢这本书,你可能会对 Packt 出版的这些其他书籍感兴趣:

机器学习与 PyTorch 和 Scikit-Learn

塞巴斯蒂安·拉什卡(Sebastian Raschka)

刘宇熙(Hayden Liu)

瓦希德·米尔贾利利(Vahid Mirjalili)

ISBN:978-1-80181-931-2

  • 探索机器学习的数据框架、模型和技术

  • 使用 scikit-learn 进行机器学习,使用 PyTorch 进行深度学习

  • 在图像、文本等数据上训练机器学习分类器

  • 构建并训练神经网络、变换器和提升算法

  • 发现评估和调优模型的最佳实践

  • 使用回归分析预测连续目标结果

  • 通过情感分析深入挖掘文本和社交媒体数据

深度学习与 TensorFlow 和 Keras(第三版)

阿米塔·卡普尔(Amita Kapoor)

安东尼奥·古利(Antonio Gulli)

苏吉特·帕尔(Sujit Pal)

ISBN:978-1-80323-291-1

  • 学习如何使用流行的 GNNs 与 TensorFlow 进行图形挖掘任务

  • 探索变换器的世界,从预训练到微调再到评估

  • 将自监督学习应用于自然语言处理、计算机视觉和音频信号处理

  • 使用 TensorFlow 概率结合概率模型和深度学习模型

  • 在云端训练你的模型,并在真实环境中应用 TF

  • 使用 TensorFlow 2.x 和 Keras API 构建机器学习和深度学习系统

算法交易的机器学习(第二版)

斯特凡·詹森(Stefan Jansen)

ISBN:978-1-83921-771-5

  • 利用市场、基本面和替代性文本与图像数据

  • 使用统计学、Alphalens 和 SHAP 值研究和评估 alpha 因子

  • 实现机器学习技术解决投资和交易问题

  • 基于 Zipline 和 Backtrader 回测并评估机器学习的交易策略

  • 使用 pandas、NumPy 和 pyfolio 优化投资组合的风险和表现分析

  • 基于协整为美国股票和 ETF 创建配对交易策略

  • 使用 AlgoSeek 的高质量交易和报价数据训练梯度提升模型,以预测日内收益

Packt 正在寻找像你这样的作者

如果你有兴趣成为 Packt 的作者,请访问authors.packtpub.com并立即申请。我们与成千上万的开发者和技术专业人士合作,帮助他们与全球技术社区分享他们的见解。你可以提交一个通用申请,申请我们正在招募的具体热门话题,或者提交你自己的想法。

加入我们在 Discord 上的社区

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

packt.link/pandas