我最近决定在使用数据框的 Python 项目中从 Pandas[1] 转向 Polars[2] 。我是在上周参加一个关于 Polars 的 工作坊[3] 时做出这个决定的:我发现它的语法如此直观,以至于我无法再为继续尝试"更好地"使用 Pandas 找到理由,尽管 Pandas 是更成熟的库。令人惊讶的是,Polars 更快(这是它的主要卖点)并不是我做出这个决定的因素。
R 语言最近也发生了类似的转变。在 R 的大部分历史中,只有一种与数据框交互的方式:Base R。然后 Tidyverse 出现了,它提供了性能改进和更简单的语法。最终,Tidyverse 成为许多人与数据框交互的主要方式。我认为 Tidyverse 更简单的语法导致了它的广泛采用,我认为 Polars 也可能会发生类似的情况。
这很大程度上可以用 布鲁姆分类法[4] 来解释。我第一次了解布鲁姆分类法是在多年前参加"培训师培训"课程时。该分类法列出了人们从初学者到专家的过程中所经历的阶段。这里有一个关键点:金字塔的基础是"记忆"。如果你无法记住如何执行一个基本任务(比如说子集化一个数据框),那么你就无法将其应用到你的工作中,评估他人的代码,或为该语言/库贡献你自己的扩展。
我认为 Polars 和 Tidyverse 都比它们之前的库有更容易记忆的语法。对于 Tidyverse 来说,这可能促使它成为许多用户的主要数据框库。我预计 Polars 也会发生类似的情况。
理解这些语法差异可以帮助我们都成为更好的程序员。虽然我们中很少有人开发下载量达数百万的库,但我们大多数人都会编写供他人使用的代码。弄清楚是什么使某些 API 比其他 API 更容易掌握,可以帮助我们的下一个项目更加成功。为了帮助实现这一点,下面我用 Polars 和 Tidyverse 以及它们之前的数据框库(Pandas 和 Base R)解决了同一个简单问题。这个问题是:
- 将美国县的 CSV 文件读入数据框。
- 将行子集化为名为"Washington"的县。
- 将列子集化为"county.name"和"state.name"。
(选择这个例子是因为对行和列进行子集化是最基本的操作之一,但它仍然能说明我的观点。而且当我在做一个美国县数据的项目时,我发现很多州都有一个名为"Washington"的县,这很有趣。)
本文中使用的代码也可以在 github[5] 上找到。欢迎将其作为起点来自行探索这些库。
Polars vs. Pandas
Polars
在 Polars 中,你可以使用 filter 函数对行进行子集化。你可以使用 select 函数对列进行子集化。这些函数都是数据框类的方法。Python 用户通过将长方法"链"放在 () 内来设置样式。所以在读入数据后,代码看起来像这样:
( df .filter(pl.col('county.name')=='washington') .select(['county.name','state.name']) )
当我第一次读到这段代码时,我的第一个想法是,你对数据框执行的每个操作都有一个描述性的函数名。虽然这听起来很明显,但 Pandas 和 Base R 经常使用运算符/符号而不是函数,而且运算符可能会根据输入做不同的事情。这可能会使人难以记住如何使用该库。
R 程序员会注意到 filter 和 select 与 dplyr 用于相同任务的名称完全相同。当我看到这一点时,我以为 Ritchie Vink(Polars 的创始人)只是复制了 Tidyverse 的做法。但当我在 LinkedIn 上 问[6] 他时,他说他不编写 R 程序,实际上并不知道这一点!他幽默地称之为"趋同进化"。
当我第一次看到这种语法时,我很高兴,因为我认为它很容易记住:每个任务(对行进行子集化、对列进行子集化、按名称提取列)都有一个与之相关的函数,而且函数的命名方式使其易于记忆。正如我们将在下面看到的,这与 tidyverse 非常相似,而与 Pandas 和 Base R 都非常不同。
Pandas
当我尝试使用 pandas 编写这段代码时,我想:"啊,我应该在这里使用 .loc (而不是 .iloc),对吧?但是那个函数是使用 [] 还是 ()?我总是忘记。让我问问 Copilot。"
Copilot 的回答让我感到惊讶。它没有提到 .loc 或 .iloc。相反,它说使用普通的 []:
df[df['county.name'] == 'washington']
这种语法的优点是非常简洁。所以如果你知道你在做什么,那么读写都很快。但作为新手,它可能会令人困惑。一种令人困惑的方式是你使用的是运算符/符号而不是明确命名的函数(所以你必须记住 [] 的作用)。其次,[] 实际上在同一行代码中做了两件不同的事情。在内部表达式中,你给它一个列名,它返回该列的值。但在外部表达式中,你给它一个逻辑 Series,它返回数据框中相应的行。
由于我想看到 .loc 版本,我告诉 Copilot "展示另一种方式"。它返回了这个:
df.query('`county.name` == "washington"')
当我第一次学习 Pandas 时,我对"query" API 感到兴奋,因为它似乎很容易使用。但后来一个我仰慕的"高级" Pandas 用户告诉我,他从不使用它。所以我决定自己也不使用它。于是我再次告诉 Copilot "展示另一种方式"。它返回了:
df.loc[df['county.name'] == 'washington']
正是我在寻找的。所以我在这里使用 .loc 的直觉是正确的。尽管我不确定它是使用 [] 还是 (),但我能够记住这是需要注意的事情。加入选择列的代码,我们得到这个解决方案:
( df .loc[df['county.name'] == 'washington'] [['county.name', 'state.name']] )
有趣的是,列选择代码又增加了两对 []。而且它们再次表示不同的含义(内部的表示"Python 列表",外部的表示"子集列")。
对我来说,在攀登布鲁姆分类法方面,关于 Pandas 有两个障碍。第一个是知道我应该使用许多可能的方法中的哪一种来解决一个任务。这实际上让我想起了 Python 之禅[7] 中的一句话:"应该有一种 - 最好只有一种 - 明显的方法来做到这一点。"第二个是记住语法的细节。
Tidyverse vs. Base R
Tidyverse
tidyverse 有一个原则,即代码应该 为人类设计[8] 。在实践中,这意味着创建具有清晰名称的函数,并让每个函数只做一件事。它还意味着使用管道运算符 |> (读作"然后")来组合这些函数。这意味着我们的简单分析可以这样完成:
df |> filter(county.name == "washington") |> select(county.name, state.name)
这段代码与等效的 Polars 代码非常相似。7 月份,我教授了一个"R 入门"工作坊。我们同时涵盖了 Tidyverse 和 Base R。学生们使用 Tidyverse 解决简单问题的速度比使用 Base R 解决类似问题的速度快得多。我将这归因于 Tidyverse 依赖于具有明确名称且只做一件事的函数。
这段代码的另一个特点是它使用了 非标准评估(NSE)[9] 。在调用 filter 时,我们可以通过不加引号写列名来引用列的内容(例如 county.name)。在 Polars 中,我们需要写 pl.col('county.name'),在 Pandas 中,我们需要写类似 df['county.name'] 的内容。NSE 非常有用,并且会产生如此简洁的代码,以至于我不确定为什么 Pandas 和 Polars 都没有采用它。[更新 2024-09-05:在 LinkedIn 上,Ritchie Vink 很友好地 回应[10] 了这一点:"注意,NSE 在 Python 中是不可能的。这意味着一些 DSL 在 Python 中无法表达,需要像 pl.col(..)
这样的实用对象。"]
Base R
上述代码的"Base R"版本非常不同。与 Pandas 类似,没有明确的函数调用。相反,你使用运算符/符号 [] , 和 $:
df[df$county.name == "washington", c("county.name", "state.name")]
作为一个长期的 R 用户,我发现这样的代码非常容易读写。但我工作坊中的学生在编写它时遇到了困难。他们能够轻松地单独编写向量化的逻辑测试(df$county.name == "washington")。但他们很难将该测试放入 [] 中。
这段代码暴露的另一个问题是,运算符通常可以被重载,这可能会进一步混淆新手。例如,在上面的代码中,df$county.name == "washington" 被用作下标。这没问题,一旦你理解了布尔索引,你就可以继续了。但是 R 中有 五种下标[11] ,新手需要学习所有这些。使用明确的函数时,这个问题就不那么严重了,事实上,Tidyverse 对于特殊情况有很多明确命名的函数(例如 starts_with 和 ends_with)。
总结
去年 12 月,当我第一次开始学习如何在 Python 中处理表格数据时,我选择学习 Pandas,因为它是 Python 中最流行的数据框库。现在我认为新手最好学习 Polars,我正在把精力放在那里。我的主要原因是我发现 Polars 的语法更容易记忆。根据布鲁姆分类法,我认为一个更容易记住如何使用的库反过来会使我能够更快地做出重大贡献。作为额外的好处,Polars 的性能比 Pandas 更好。
"Polars vs. Pandas"的争论让我想起了大约十年前开始的"Tidyverse vs. Base R"的争论。当时,有经验的 R 用户(比如我自己)嘲笑那些认为"学习 Tidyverse"在某种程度上等同于"学习 R"的人。回想起来,我们错了。我们低估了人们采用更简单、更明确的数据框 API 的速度。我认为今天对 Polars 持类似立场的有经验的 Pandas 用户也可能被证明是错误的,原因也类似。当然,只有时间才能证明一切。
Polars和Tidyverse的创建让我想起了我第一份工作时一位首席工程师告诉我的话:"第一次构建某样东西时,要专注于让它能够工作。第二次构建时,要专注于让它变得漂亮。"Pandas和Base R都是统计计算领域的重大贡献。它们工作得很好,非常好。而后来出现的Polars和Tidyverse则有奢侈的机会可以专注于变得"漂亮"。
正如俗话所说:"历史不会重复,但总会押韵。"
参考链接
1. Pandas: https://pandas.pydata.org/
2. Polars: https://pola.rs/
3. 工作坊: https://store.lerner.co.il/polars
4. 布鲁姆分类法: https://cft.vanderbilt.edu/guides-sub-pages/blooms-taxonomy/
5. github: https://github.com/arilamstein/polars-pandas/tree/main
6. 问: https://www.linkedin.com/feed/update/urn:li:activity:7236435167987294209?commentUrn=urn%3Ali%3Acomment%3A(activity%3A7236435167987294209%2C7236446743855288320)&replyUrn=urn%3Ali%3Acomment%3A(activity%3A7236435167987294209%2C7236765479673974784)&dashCommentUrn=urn%3Ali%3Afsd_comment%3A(7236446743855288320%2Curn%3Ali%3Aactivity%3A7236435167987294209)&dashReplyUrn=urn%3Ali%3Afsd_comment%3A(7236765479673974784%2Curn%3Ali%3Aactivity%3A7236435167987294209)
7. Python 之禅: https://en.wikipedia.org/wiki/Zen_of_Python
8. 为人类设计: https://tidyverse.tidyverse.org/articles/manifesto.html#design-for-humans
9. 非标准评估(NSE): https://adv-r.hadley.nz/metaprogramming.html
10. 回应: https://www.linkedin.com/feed/update/urn:li:activity:7237182297102295041?commentUrn=urn%3Ali%3Acomment%3A(activity%3A7237182297102295041%2C7237485477031759872)&dashCommentUrn=urn%3Ali%3Afsd_comment%3A(7237485477031759872%2Curn%3Ali%3Aactivity%3A7237182297102295041)
11. 五种下标: https://www.johndcook.com/blog/2008/10/23/five-kinds-of-r-language-subscripts/