paint-brush
为什么我们教 Pandas 而不是 SQL?经过@vladpublish
19,538 讀數
19,538 讀數

为什么我们教 Pandas 而不是 SQL?

经过 Vlad Gheorghe2m2022/05/20
Read on Terminal Reader
Read this story w/o Javascript

太長; 讀書

大多数数据训练营的课程都对 pandas (以及 Jupyter 笔记本)进行了大量投资,而 SQL 充其量只是事后的想法。他们应该做相反的事情。 Pandas 在复杂性、低效率、特殊性和混淆机会方面引入了大量开销。 Pandas 在某些方面做得更好,但总的来说,在纯分析方面,SQL 很难被击败。

People Mentioned

Mention Thumbnail

Companies Mentioned

Mention Thumbnail
Mention Thumbnail

Coins Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - 为什么我们教 Pandas 而不是 SQL?
Vlad Gheorghe HackerNoon profile picture


佩德罗·冈萨雷斯 ( Pedro Gonzalez ) 在Unsplash上拍摄的照片


曾几何时,有一个学生渴望学习数据科学。


他问人们他应该做什么,他们告诉他学习熊猫。他在网上搜索数据课程,所有课程都以熊猫为特色。所以学生学习了熊猫,然后在学术界找到了一份工作,每个人都在和熊猫一起工作。


因此,他与 pandas 一起辛勤工作了很多个月,直到他可以在睡梦中分割数据帧。完成后,他加入了一个数据训练营:你瞧,他们正在教熊猫。当他完成后,训练营聘请了他——教熊猫。


然后是学生进入他的第一家公司的时候。他的主人要他处理一些数据。


“我要用熊猫”学生说。


“你会的!”主人说。 “我们在这里使用 S-Q-L”他反驳道,用他的棍子一击强调每个字母。


“但是……但是……冗长……丑陋……缺乏功能……无休止的嵌套……而连接,连接太可怕了!……”


“如果你看不到森林,那么你就不应该碰树木。”大师说着打了他的头。


学生顿悟了。


或者他是这么想的;事实上,师父的这一击,已经把他震得厉害,让他的判断力暂时受损。


许多个月后,在痛苦的退缩之后,学生理解了 SQL。从此他觉得没有再用熊猫的必要,师父也没有再给过他一击。

SQL >> 熊猫


上面的公案是自传式的,尽管我应该说我的上司从来没有打过我(即使他们应该打过)。


自从我开始以来并没有太大变化。大多数数据训练营的课程都对 pandas(以及 Jupyter 笔记本)进行了大量投资,而 SQL 充其量只是事后的想法。


在 Udemy 上搜索“数据科学”,你会看到提到 Python(不可避免地包括 pandas)和 R,有时甚至是 Scala 的顶级课程。他们中很少有人提到 SQL。


我认为这很糟糕,无论是从价值的角度还是从教学的角度来看。

价值问题

如果您进行标准分析,SQL 是比 pandas 更好的工具。它更简单、更清晰、更强大、更高效。它也更容易理解、分享和复制。几十年来它一直是数据分析的通用语是有原因的。


如果您以牺牲 SQL 为代价专注于 Pandas,那么您就错过了成为更好、更有效的数据从业者的机会。


当然,pandas 也有一些做得更好的事情,我会在文末简要探讨。但总的来说,当涉及到纯分析时,SQL 很难被击败。


人们常常没有注意到 pandas 在复杂性、低效率、特殊性和混淆机会方面引入了显着的开销。

学习问题


http://stream1.gifsoup.com/view2/1321405/angry-panda-o.gif


我怀疑过度强调 pandas 会伤害数据学生。根据我的经验,做更多的熊猫会导致反学习,即随着时间的推移,这个人会变得更加困惑。


尤其是新学生,他们会遇到让他们感到困惑的奇怪问题,并且他们会用盲目的记忆来弥补这些问题。


他们还养成了以后很难改掉的坏习惯,比如当他们可以使用表操作时循环遍历行;创建具有混合类型的列;以 CSV 格式保存数据;就地修改数据;用同一数据帧的多个副本和切片阻塞内存......我可以继续。


部分原因在于 Python 是一种极其宽容的语言,这给用户带来了不要做坏事的负担(比如具有混合类型的 pandas Series )。部分原因在于 pandas 接受(尽管不一定鼓励)命令式方法。


例如,如果一个学生想要合并两个表中的数据,没有什么能阻止她使用这个算法:


  1. 循环遍历table1

  2. 对于每个table1行,扫描所有table2以进行查找

  3. 根据table2中的数据更新table1中的行


是的,这显然非常糟糕。但是初学者并不知道更好。

相比之下,SQL 的声明式方法使得做坏事变得更加困难。


声明式编程迫使用户考虑他们想要看到的结果,而不是如何产生结果。这为他们提供了专注于分析背后的逻辑所需的空间,而不是不断地被问题和奇怪的错误所困扰。


SQL 还迫使学生在表(即关系模型)和列级操作中进行思考,这在他们的第一直觉是循环遍历所有内容时非常有益。


最后,学习 SQL 因其通用性和可移植性而产生更大的投资回报。

免责声明

我不讨厌熊猫。这是我两年来的基本分析工具,我仍然将它用于我的个人项目。我很高兴看到人们学习它。


但我试图看到更大的图景。我认为以牺牲 SQL 为代价过分强调 pandas弊大于利。尤其是对于初学者来说,他们在学习 pandas MultiIndex之后才能进行适当的GROUP BY

展望

在下一节中,我将分析 pandas 的一些最奇怪的方面,并直接与 SQL 进行比较。


再一次,我们的目标不是让 pandas 失望,而是扩大读者对他们可以使用的工具的看法。


让我们深入研究。


 SELECT * FROM opinionated_comparison WHERE sql > pandas

比较

https://www.reddit.com/r/funny/comments/5ysm1t/pandas_on_slide/


选择列

在单个语句中,SQL 的SELECT允许您:


  1. 选择您想要的列以及它们应该返回的顺序。


  2. 根据现有列的组合和转换创建新列。


  3. 重命名列。


此示例选择除一列之外的所有列,然后基于其他两列的线性组合创建一个新列:


 SELECT * EXCEPT (first_name), equity_comp / total_comp * 100 AS percent_equity FROM salaries


pandas 列选择只允许选择和排序列。如果要重命名或转换某些语句,则需要多个语句,并且许多人会因就地转换数据而犯错误(请参阅下面的不变性)。


初学者会感到困惑,因为选择单列需要一组括号( df[”first_name”] ),而选择多列需要一组括号( df[[”first_name”, "last_name"]] )。


我在这里遇到的 pandas 最大的问题是点表示法:您可以选择这样的列: df.first_name


这比使用方括号和引号要容易得多,因此人们最终出于纯粹的懒惰而更喜欢它。这至少发生在我身上:我仍然自动使用点符号,即使我知道它很糟糕。


当您有一个名为countshapediff的列或数据框的许多标准属性中的任何其他列时,就会出现问题(您可以使用dir(df)查看它们)。


当您尝试使用点符号访问它们时,您将获得属性而不是列,并且您的代码将中断。


因此,pandas 有三种选择列的方法:两种用于获取单列(其中一种不好但更有吸引力!),第三种用于选择多列。

选择行

在 SQL 中,要选择特定的行,只需使用WHERE语句(请参阅下面的过滤)。


在 Pandas 中选择行很复杂。要查看复杂程度,请查看用户入门指南。或者深入研究一个典型的30 分钟教程


我将把自己限制在一个例子中。每个 DataFrame 都有一个Index 。默认索引是一个整数序列: [0,1,2,3,4,5...]


自然,大多数人认为索引代表行的基数。实际上,数字只是分类标签!它们也可能是随机字母,例如['x', 'z', 'a'] 。没有隐含的基数。


要通过索引获取行,请使用df.loc 。但是要按行的基数进行选择,请使用df.iloc使用默认索引,这两种方法给出相同的结果。


这只会增加混乱,因为在任何时候你的索引都可能变成完全随机的东西,比如[7, 2, 2, 'c', True, None] 。是的,这一切都是允许的!并且没有任何限制可以阻止它(请参阅下面的限制)。


假设您编写代码时假设索引表示行基数。现在:


  • df.loc[7]将返回第一行
  • df.loc[2]将返回一个数据帧切片而不是一行(因为它在索引中多次出现)
  • df.loc[None]将返回一个实际的行!

我不哭你哭。



是的:相同的方法可以返回标量值、行或数据帧切片,具体取决于索引的组成方式。 pandas 文档承认这种疯狂:


其他方法,如索引,可以产生非常令人惊讶的结果。通常使用标量进行索引会降低维度。使用标量切片DataFrame将返回一个Series 。用标量切片Series将返回一个标量。但是对于 [index] 重复,情况并非如此。



请记住,没有任何约束可以防止索引包含重复项。我无法告诉你这给我带来了多少头痛。


(除了我们提到的所有选择方法之外,pandas 还具有用于单个值的df.atdf.iat 。另一件事要记住并避免混淆。)

过滤

在 SQL 中,过滤很简单。编写WHERE ,根据需要插入尽可能多的语句,并将它们与逻辑运算符链接在一起。括号使您可以更好地控制结构表达式。


例如,以下查询筛选出 30 岁以上且至少满足以下两个条件之一的人:任期超过 5 年或股权薪酬低于 50:


 SELECT * from salaries WHERE age > 30 AND (tenure > 5 OR equity_comp < 50)


这在 Pandas 中看起来如何?


 new_df = df[(df["age"] > 30) & ((df["tenure"] > 5) | (df["equity_comp"] < 50))]


啊。向初学者解释这一点很有趣。


当然,你可能不会这样写,因为它太丑了。您将在多个语句中执行过滤:这意味着更多的代码行、变量和重复。


Pandas 过滤器基于一种称为布尔索引的方法。每个过滤操作都分两个步骤进行:


  1. 您获取一个Series (即列对象)并通过布尔测试运行每个元素。从而将其转换为由布尔值(真或假)组成的新Series


  2. 您选择具有此列的数据框,最终排除布尔Series包含错误值的行。


注意到这里隐藏的假设了吗?用于过滤的Series和被过滤的数据框需要以相同的顺序共享相同的索引。这并不总是得到保证。


在实践中,布尔索引意味着当您过滤时,您总是必须重复数据框变量,例如salaries[salaries["cash_comp"] > 20] 。当您编写大量代码时,这非常烦人!见上面的例子:dataframe 变量被引用了 4 次。


我也可以从经验中说,布尔索引对于初学者来说并不容易理解。有些人根本没有得到底层机制。他们只是记住编码模式。


df.query()方法似乎提供了一种更好的过滤方法。)

分组

这里没有大的抱怨。但是 SQL 肯定更接近英语。这两个是等价的:


 SELECT AVG(cash_comp), SUM(tenure) FROM salaries GROUP BY department
 grouped_df = df.groupby('department').agg({"cash_comp": np.mean, "tenure": np.sum})

加入

SQL 有一种连接类型。它被称为JOIN 。当然,它可以是左/右和内/外,但使用非常简单。


Pandas 有两种方法: joinmerge 。我一直不明白为什么我们需要两个。 join应该适用于索引, merge应该适用于任何列。


但是,如果您查看文档 [ 1 ][ 2 ],它们似乎都支持索引和列。我很困惑。 (如果您有疑问,我建议始终选择merge ,因为join更像是一种遗留方法。)


SQL 使得基于一系列逻辑条件的 JOIN 变得非常容易,例如:按角色加入,但前提是伦敦的薪水比华盛顿的高,或者这个人的任期更长。


 SELECT * FROM london_hq lhq JOIN washington_hq whq ON lhq.role = whq.role AND (lhq.salary > whq.salary OR lhq.tenure > whq.tenure)


据我所知,熊猫不可能做到这一点。加入(或合并)时,您只能使用相等条件。


因此,您必须首先在role上执行 JOIN,跟踪每列的来源,然后过滤结果。


但是,我认为这些条件理所当然地属于 JOIN 并且与不使用相等比较的相关性并没有降低。

打字

SQL 的主要优点之一是每一列都有明确定义的类型。此外,列不允许有混合类型。从长远来看,这可以节省很多错误和麻烦。


当您将数据加载到 pandas 中时,大多数列都会自动键入为object 。这可能意味着以下三件事之一:


  1. 该列仅包含字符串

  2. 该列包含不是原始数据类型的 Python 对象,例如列表或字典

  3. 该列包含混合类型


当您看到object数据类型时,您永远不知道是哪种情况。我觉得这很烦人。


与 SQL 不同,您可以在 pandas 中加载混合类型的数据:它们将被简单地键入为object


Pandas 不会强迫您指定模式并坚持使用它。当您开始时,这会为您提供速度优势,但您通常会在未来出现错误和混乱时为此付出高昂的代价。


这对于不注意常见陷阱的初学者来说尤其成问题。例如,当我使用 Pandas 时,我经常尝试进行 datetime 操作,结果发现 datetime 列是由字符串组成的(因此归类为object )。我会天真地这样做:


 df['Date'] = df['Date'].astype('datetime64[ns]')


然后继续前进,直到很久以后才发现 Pandas 的日期解析器误读了我的字符串并且日期没有意义。

文件和 CSV

http://www.reddit.com/r/Panda_Gifs/comments/32x49o/keep_rollin_rollin_rollin_rollin/


老实说:大多数人将他们的数据帧存储为 CSV。熊猫学生受到欢迎,不鼓励这样做。这是一个坏主意!


当然,CSV 是人类可读的,而且……它们的优势到此为止。它们的缺点是:


  • 转换为 CSV 时,您会丢失有关架构和列类型的所有信息。一切都恢复为文本。


  • CSV 容易出现格式错误、损坏和解析错误。


  • CSV 很难压缩,这会增加存储成本。


  • CSV 格式未指定,这意味着不同的程序以不同的方式创建 CSV,而用户有责任弄清楚如何解析它们。正如任何数据专家都会证明的那样,这很快就会变成地狱般的体验。


对于在 pandas 工作的人来说,模式的丢失通常是最大的问题。这是一种常见的情况:


  1. 您的工作从 CSV 开始。如果您已经弄清楚了正确的格式、编码、引号字符规范以及 pandas 的read_csv许多参数的其余部分,您将在数据框中加载它。现在您需要花时间探索列,将每列转换为正确的类型,处理过程中出现的任何错误并验证最终结果是否有意义。 (或者您可以立即开始工作,然后再面对很多错误)。


  2. 完成工作后,您将拥有一个新的数据框。你打算用它做什么?为什么,将其保存为 CSV。现在,您之前在架构定义方面所做的所有工作都已消失,因为数据框已转储为文本。


  3. 您需要为另一个工作流加载新的数据框。这意味着加载您刚刚转储的 CSV。我希望您编写的函数可以成功恢复架构,否则您将不得不重新完成这项工作(前提是您记得每列应该做什么)。


  4. 想要与朋友分享 CSV 文件或将其发布到 GitHub 上?您最好共享可以重新估算模式的代码,并希望他们愿意并且能够运行它。或者他们将留下一团文本,并且必须从头开始重复所有模式插补工作。


听起来很荒谬?我已经看过无数次了。我自己做过这个!但现在我想知道:我们为什么要教人们这样工作?是什么证明了这种疯狂和残忍?


这里有两种解决方案。


如果您真的需要使用 pandas,请在 Parquet 中导出您的数据框。

或者你可以在 SQL 中工作,省去所有的麻烦。毕竟,数据库是存储数据的最佳场所。


问问自己:为什么我需要一个文件层?如果您只是读取一些数据,对其进行处理然后存储结果,那么您可能不会。从数据库加载,在数据库中工作,在数据库中保存。就是这么简单。需要与外部共享数据?在 Parquet 中导出。


世界不需要更多的 CSV。

注意:有些人试图通过腌制他们的数据框来解决架构问题。这是一个可怕的想法


腌制效率低下且不安全(切勿打开您不信任的腌菜!)。腌制数据框只能在 Python 中打开,并且必须在同一个库环境中进行(用户可能对此一无所知)。如果读取 pickle 的 pandas 与编写它的 pandas 版本不同,则该文件可能无法读取!


pandas 用户共享 CSV 文件


空值

SQL 使用 NULL 值来指示缺失数据。您可以轻松过滤掉空值。


 SELECT * FROM salaries WHERE equity_comp IS NOT NULL AND cash_comp IS NOT NULL


在 Pandas 中,缺失值可以是以下任何一种:


  • Python 的原生None (Pandas 显示为None但视为nan

  • numpy.nan

  • pandas.NA

  • pandas.NaT (用于日期时间)


让我们关注最常见的numpy.nan


  • 这个对象的类型是float ,所以忘记用类型检查来检测它。

  • 这是真的,所以忘记布尔测试。 bool(np.nan)True

  • 它没有通过相等性测试,因为numpy.nan == numpy.nan是假的。 nan不等于自己!

  • 在操作中使用nan不会引发异常,它只是意味着结果是nan


这不是很有趣吗?



检测nan的唯一方法是使用pandas.isna() 。没关系,一旦您阅读了文档并忘记了所有的 Python 直觉。尽管如此,这种行为对于初学者来说还是非常令人困惑的。


以下是在 Pandas 中复制上述查询的方法:


 new_df = df.dropna(subset=["equity_comp", "cash_comp"])

约束

约束在 SQL 中很重要。它们允许您指定保持数据安全和一致的规则。例如,用作每一行的唯一标识符的主键必须是唯一的并且不能为空。


熊猫没有这样的东西。


pandas 中最接近主键的是索引。不幸的是,索引值既可以是重复的,也可以是空的(是的,你可以有一个None值的索引)。


用户经常使用索引是主键的隐含假设,这是由默认索引由整数组成的事实所强制执行的: [0,1,2,3,4...] 。因此人们倾向于使用索引来引用特定的行,例如df.loc[2]


这是一种愚蠢的信仰行为。这在连接或合并不同的数据帧时变得很明显。经常发生类似的索引混合在一起的情况,您会得到一个如下所示的索引: [0,1,2,2,2,3...]


Pandas 不会对此发出任何警告,因此您一开始并没有意识到这一点。但是下次您使用df.loc[2]时,您的代码将中断,因为它现在将返回一个包含三行的数据框,而不是单行。


在您发现需要在合并的数据帧上运行reset_index()以使每一行再次获得唯一值之前,很多人都会流泪。


此外,SQL 约束允许您在编写时运行检查。如果您尝试将空值插入带有NOT NULL约束的列中,则会出现异常并且不会发生错误写入。 Pandas 只允许在读取时运行检查。也就是说,如果你记得这样做。

矢量化操作

这主要是一个教学点。众所周知,Pandas 允许甚至鼓励向量化操作(其中一个系列的所有元素都被并行访问)。


但是很多使用 Python 工作的人不会自动这样想。他们从学习循环开始,现在, 通过 Guido ,他们想使用这些循环。


当他们开始使用 pandas 时,他们很快发现了iterrowsitertuples方法,它们允许他们逐行循环数据帧。


几乎不可避免地,它们再次陷入循环模式,因为没有什么能迫使它们用向量来思考。这导致他们编写运行速度非常慢的糟糕代码。


在长期使用 pandas 之后,我开始专注于 SQL。每次遇到 SQL 问题时,我的直觉就是想出一个循环的解决方案。令人沮丧的是,SQL 不允许我这样做。


它的声明式语法迫使我从列操作、JOIN 和窗口函数的角度进行思考。渐渐地,我建立了一套新的心智模型,使我成为了一个更好的分析师。


我认为学习者应该在开始使用 pandas 之前建立在 SQL 中操作数据的信心。他们将更好地理解何时按行循环是不可避免的,这很少见。

不变性

您在内存中加载数据帧。您需要修改该数据框。您是在原地更改它,还是创建一个副本?您应该更新现有列还是创建新列?


如果您需要创建多个数据帧切片,然后在每个切片上做一些工作怎么办?您应该将每个切片存储在单独的变量中还是使用相同的变量依次保存不同的切片?


当人们在 pandas 中工作时,他们倾向于同时做所有这些事情。很快就很难跟踪包含数据帧、数据帧切片和切片切片的所有变量,以及如何添加或修改数据。


(我并不总是写熊猫,但当我这样做时,我会得到带有复制警告的设置。)


而且由于大多数人将 pandas 与笔记本一起使用,这些问题与典型的笔记本陷阱相得益彰,最终导致巨大的头痛。

这就是为什么我认为 pandas 不是数据分析的最佳选择的原因之一。


在 SQL 中处理数据时,您不会更改原始数据。 UPDATE语句不用于分析。相反,您创建代表不同选择的表和视图的管道。


当您需要保存结果时,您可以创建新表(或向现有目标表添加行,但不修改或删除以前的行)。这尊重了不变性的原则:永远不要修改或删除源数据。它使您的流程安全、透明且易于复制。


是的,你可以尊重 pandas 的不变性,但你应该这样做并不明显,而且很多人从未学会这样做。您通常看到的是文件级别的不变性:人们通常会加载 CSV 并输出新的 CSV。但是对于介于两者之间的工作呢?什么都行。


(大多数 pandas 方法在理论上是“纯”的,因为它们返回一个新的数据帧,而不是修改前一个数据帧。但它们都提供了就地更改数据帧的inplace选项。人们乐于使用它。)

当熊猫还不够时

如果你认真地使用 pandas,你最终会遇到性能障碍。您正在分析的数据太大,或者您的处理需求太高。


我在研究熊猫时经常看到这一点。当它发生时,我和我的同事会在谷歌上搜索“让 pandas 更快”,并找到无数关于这个热门主题的文章,这些文章反过来又提出了无数的黑客、优化和 PyPI 库,它们承诺可以做到这一点。


如果您遇到这种情况,请务必查看可用资源。尤其是那些解释如何更好地使用 pandas的。但不要抱太大希望。熊猫可以做的事情是有严格限制的。没关系:它的设计初衷不是为了全部数据分析。


当您需要扩展数据工作负载时,两个最佳选择是:


  • 派斯帕克。 Spark 是用于大规模分析和并行数据处理的引擎。 PySpark 允许您在 Python 中使用它,并使用类似于 pandas 的语法。它甚至有 pandas API。


  • 数据仓库。用于存储和分析超大规模数据的系统(想想 TB 和 PB)。现代数据仓库在云中运行,因此您无需管理任何服务器即可利用分布式系统的强大功能。 Google Cloud 的数据仓库解决方案 BigQuery可以在 30 秒内处理 1000 亿行或 7 TB 的数据。数据仓库通常使用 SQL。 (如果你想免费试用 BigQuery, 我在这里写过。

熊猫什么时候更好?

我不想让你避开熊猫。这是一个很棒的工具,绝对值得学习。


某些情况下,pandas 是比 SQL 更好的选择。我不会在这里详细介绍,但这里有一个快速列表:


  • 当与其他 Python 工作流集成时,例如你正在做机器学习或者你想使用 Python 可视化库。


  • 当您需要一些快速统计时。 describe()方法非常有用。


  • 当您需要进行快速分析时,无需担心规模或重现性。尽管 Excel 或 Google 表格也可以正常工作。


  • 如果您正在构建 Python 应用程序。 Pandas 可能是从任意数据结构到表的最快方式,反之亦然。


  • 当您真正需要命令式工作流和循环时。例如构建马尔可夫模拟。


  • 当您需要编写或重用不寻常的功能时。 Pandas 擅长应用任意 Python 函数。


  • 如果您有一个高度动态和参数化的工作流程。

结论

不要叫我放弃熊猫


我希望这篇文章能激发您更深入地思考 SQL 和 pandas 及其相对优势和劣势。


我相信当前的数据趋势已经过分倾向于 pandas,而忽略 SQL 后果自负。


这是我的建议:


  • 如果您是学习者:学习 SQL 并学习将其用于您的分析。你不会后悔的。


  • 如果您是课程设计者:让您的学生沉浸在 SQL 中,直到他们在表格中做梦并且大写。这是艰难的爱,他们会因此而恨你,但到时候他们会明白的。 (不过不要打他们的头。)


  • 如果你是一名导师:试着让你的学生逐渐远离 Pandas,并鼓励他们尝试 SQL 中的问题。


我很想进行对话。随意发表评论,给我写电子邮件或在LinkedIn上添加我。