从Pandas迁移
在这里,我们列出了任何有pandas经验并想尝试Polars的人应该知道的关键点。我们包括了这些库所基于的概念上的差异,以及你应该如何编写Polars代码与pandas代码相比的差异。
Polars 和 pandas 在概念上的差异
Polars 没有多索引/索引
pandas 使用索引为每一行赋予一个标签。Polars 不使用索引,每一行通过其在表中的整数位置进行索引。
Polars 旨在提供可预测的结果和可读的查询,因此我们认为索引并不能帮助我们实现这一目标。我们相信查询的语义不应因索引的状态或 reset_index 调用而改变。
在Polars中,DataFrame始终是一个包含异构数据类型的二维表格。数据类型可能具有嵌套,但表格本身不会。像重采样这样的操作将通过专门的函数或方法来完成,这些函数或方法就像表格上的“动词”,明确指定这些“动词”操作的列。因此,我们坚信没有索引会使事情更简单、更明确、更易读且更不容易出错。
请注意,Polars将使用数据库中的“索引”数据结构作为优化技术。
Polars 遵循 Apache Arrow 内存格式来表示内存中的数据,而 pandas 使用 NumPy 数组
Polars 根据 Arrow 内存规范在内存中表示数据,而 pandas 使用 NumPy 数组在内存中表示数据。Apache Arrow 是一种新兴的内存列式分析标准,可以加速数据加载时间,减少内存使用并加速计算。
Polars 可以使用 to_numpy 方法将数据转换为 NumPy 格式。
Polars 比 pandas 支持更多的并行操作
Polars 利用 Rust 对并发的强大支持来并行运行许多操作。虽然 pandas 中的一些操作是多线程的,但该库的核心是单线程的,必须使用额外的库(如 Dask)来并行化操作。
Polars 可以延迟评估查询并应用查询优化
急切求值是指代码在运行时立即被求值。惰性求值是指运行一行代码意味着底层逻辑被添加到查询计划中,而不是被立即求值。
Polars 支持即时评估和惰性评估,而 pandas 仅支持即时评估。 惰性评估模式非常强大,因为 Polars 在检查查询计划时会自动进行查询优化,寻找加速查询或减少内存使用的方法。
Dask 在生成查询计划时也支持惰性求值。
关键语法差异
来自pandas的用户通常需要知道一件事...
polars != pandas
如果你的Polars代码看起来像pandas代码,它可能会运行,但很可能运行得比应有的速度慢。
让我们通过一些典型的pandas代码,看看我们如何在Polars中重写它。
选择数据
由于Polars中没有索引,因此在Polars中没有.loc或iloc方法 - 并且在Polars中也没有SettingWithCopyWarning。
然而,在Polars中选择数据的最佳方式是使用表达式API。例如,如果你想在pandas中选择一列,你可以执行以下操作之一:
df['a']
df.loc[:,'a']
但在Polars中,你会使用.select方法:
df.select('a')
如果你想根据值选择行,那么在Polars中使用.filter方法:
df.filter(pl.col('a') < 10)
如下面的表达式部分所述,Polars 可以在 .select 和 filter 中并行运行操作,并且 Polars 可以对完整的数据选择标准集进行查询优化。
懒惰
在惰性评估模式下工作非常简单,应该是你在Polars中的默认选择,因为惰性模式允许Polars进行查询优化。
我们可以通过使用隐式惰性函数(如scan_csv)或显式使用lazy方法来以惰性模式运行。
以下是一个简单的例子,我们从磁盘读取一个CSV文件并进行分组操作。CSV文件有许多列,但我们只想对其中一个ID列(id1)进行分组,然后对一个值列(v1)进行求和。在pandas中,这将是:
df = pd.read_csv(csv_file, usecols=['id1','v1'])
grouped_df = df.loc[:,['id1','v1']].groupby('id1').sum('v1')
在Polars中,您可以在惰性模式下构建此查询并进行查询优化,并通过将热切的pandas函数read_csv替换为隐式惰性的Polars函数scan_csv来评估它:
df = pl.scan_csv(csv_file)
grouped_df = df.group_by('id1').agg(pl.col('v1').sum()).collect()
Polars 通过识别只有 id1 和 v1 列是相关的来优化此查询,因此只会从 CSV 中读取这些列。通过在第二行末尾调用 .collect 方法,我们指示 Polars 立即评估查询。
如果您确实希望在急切模式下运行此查询,您只需在Polars代码中将scan_csv替换为read_csv。
了解更多关于惰性求值的内容,请参阅惰性API部分。
表达你自己
一个典型的pandas脚本由多个按顺序执行的数据转换组成。 然而,在Polars中,这些转换可以使用表达式并行执行。
列分配
我们有一个数据框 df,其中有一个名为 value 的列。我们想要添加两个新列,一个名为 tenXValue 的列,其中 value 列的值乘以 10,另一个名为 hundredXValue 的列,其中 value 列的值乘以 100。
在pandas中,这将是:
df.assign(
tenXValue=lambda df_: df_.value * 10,
hundredXValue=lambda df_: df_.value * 100
)
这些列分配是按顺序执行的。
在Polars中,我们使用.with_columns方法向df添加列:
df.with_columns(
tenXValue=pl.col("value") * 10,
hundredXValue=pl.col("value") * 100,
)
这些列分配是并行执行的。
基于谓词的列赋值
在这种情况下,我们有一个数据框 df,其中包含列 a、b 和 c。我们希望根据条件重新分配列 a 中的值。当列 c 中的值等于 2 时,我们将 a 中的值替换为 b 中的值。
在pandas中,这将是:
df.assign(a=lambda df_: df_.a.where(df_.c != 2, df_.b))
而在Polars中,这将是:
df.with_columns(
pl.when(pl.col("c") == 2)
.then(pl.col("b"))
.otherwise(pl.col("a")).alias("a")
)
Polars 可以并行计算 if -> then -> otherwise 的每个分支。当分支的计算成本增加时,这一点非常有价值。
过滤
我们想要根据一些标准过滤包含住房数据的df数据框。
在pandas中,您可以通过将布尔表达式传递给query方法来过滤数据框:
df.query("m2_living > 2500 and price < 300000")
或通过直接评估掩码:
df[(df["m2_living"] > 2500) & (df["price"] < 300000)]
在Polars中,你调用filter方法:
df.filter(
(pl.col("m2_living") > 2500) & (pl.col("price") < 300000)
)
Polars中的查询优化器还可以检测到你是否分别编写了多个过滤器,并将它们组合成优化计划中的单个过滤器。
pandas 转换
pandas 文档展示了一个名为 transform 的分组操作。在这个例子中,我们有一个数据框 df,我们想要一个新列来显示每个组中的行数。
在pandas中我们有:
df = pd.DataFrame({
"c": [1, 1, 1, 2, 2, 2, 2],
"type": ["m", "n", "o", "m", "m", "n", "n"],
})
df["size"] = df.groupby("c")["type"].transform(len)
这里pandas对"c"进行分组,取列"type",计算组的长度,然后将结果连接回原始的DataFrame,生成:
c type size
0 1 m 3
1 1 n 3
2 1 o 3
3 2 m 4
4 2 m 4
5 2 n 4
6 2 n 4
在Polars中,可以使用window函数实现相同的功能:
df.with_columns(
pl.col("type").count().over("c").alias("size")
)
shape: (7, 3)
┌─────┬──────┬──────┐
│ c ┆ type ┆ size │
│ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ u32 │
╞═════╪══════╪══════╡
│ 1 ┆ m ┆ 3 │
│ 1 ┆ n ┆ 3 │
│ 1 ┆ o ┆ 3 │
│ 2 ┆ m ┆ 4 │
│ 2 ┆ m ┆ 4 │
│ 2 ┆ n ┆ 4 │
│ 2 ┆ n ┆ 4 │
└─────┴──────┴──────┘
因为我们可以将整个操作存储在单个表达式中,所以我们可以组合多个window函数,甚至可以组合不同的组!
Polars 会缓存应用于同一组的窗口表达式,因此将它们存储在单个 with_columns 中既方便 又 最优。在以下示例中,我们查看一个计算 "c" 两次的组统计信息的情况:
df.with_columns(
pl.col("c").count().over("c").alias("size"),
pl.col("c").sum().over("type").alias("sum"),
pl.col("type").reverse().over("c").alias("reverse_type")
)
shape: (7, 5)
┌─────┬──────┬──────┬─────┬──────────────┐
│ c ┆ type ┆ size ┆ sum ┆ reverse_type │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ u32 ┆ i64 ┆ str │
╞═════╪══════╪══════╪═════╪══════════════╡
│ 1 ┆ m ┆ 3 ┆ 5 ┆ o │
│ 1 ┆ n ┆ 3 ┆ 5 ┆ n │
│ 1 ┆ o ┆ 3 ┆ 1 ┆ m │
│ 2 ┆ m ┆ 4 ┆ 5 ┆ n │
│ 2 ┆ m ┆ 4 ┆ 5 ┆ n │
│ 2 ┆ n ┆ 4 ┆ 5 ┆ m │
│ 2 ┆ n ┆ 4 ┆ 5 ┆ m │
└─────┴──────┴──────┴─────┴──────────────┘
缺失数据
pandas 使用 NaN 和/或 None 值来表示缺失值,具体取决于列的 dtype。此外,pandas 的行为会根据是否使用默认的 dtypes 或可选的 nullable 数组而有所不同。在 Polars 中,缺失数据对应于所有数据类型的 null 值。
对于浮点数列,Polars 允许使用 NaN 值。这些 NaN 值不被视为缺失数据,而是一种特殊的浮点数值。
在pandas中,带有缺失值的整数列会被转换为浮点数列,缺失值用NaN表示(除非使用可选的nullable整数类型)。在Polars中,整数列中的任何缺失值都只是null值,并且该列仍然保持为整数列。
请参阅缺失数据部分以获取更多详细信息。
管道乱扔垃圾
在pandas中,一个常见的用法是利用pipe对DataFrame应用某些函数。将这种编码风格复制到Polars中是不符合习惯的,并且会导致次优的查询计划。
下面的代码片段展示了pandas中的一个常见模式。
def add_foo(df: pd.DataFrame) -> pd.DataFrame:
df["foo"] = ...
return df
def add_bar(df: pd.DataFrame) -> pd.DataFrame:
df["bar"] = ...
return df
def add_ham(df: pd.DataFrame) -> pd.DataFrame:
df["ham"] = ...
return df
(df
.pipe(add_foo)
.pipe(add_bar)
.pipe(add_ham)
)
如果我们在polars中这样做,我们将创建3个with_columns上下文,这将迫使Polars按顺序运行这3个管道,无法利用任何并行性。
在polars中获得类似抽象的方法是创建生成表达式的函数。下面的代码片段创建了3个表达式,它们在单个上下文中运行,因此可以并行运行。
def get_foo(input_column: str) -> pl.Expr:
return pl.col(input_column).some_computation().alias("foo")
def get_bar(input_column: str) -> pl.Expr:
return pl.col(input_column).some_computation().alias("bar")
def get_ham(input_column: str) -> pl.Expr:
return pl.col(input_column).some_computation().alias("ham")
# This single context will run all 3 expressions in parallel
df.with_columns(
get_ham("col_a"),
get_bar("col_b"),
get_foo("col_c"),
)
如果你在生成表达式的函数中需要模式,你可以使用一个单一的pipe:
from collections import OrderedDict
def get_foo(input_column: str, schema: OrderedDict) -> pl.Expr:
if "some_col" in schema:
# branch_a
...
else:
# branch b
...
def get_bar(input_column: str, schema: OrderedDict) -> pl.Expr:
if "some_col" in schema:
# branch_a
...
else:
# branch b
...
def get_ham(input_column: str) -> pl.Expr:
return pl.col(input_column).some_computation().alias("ham")
# Use pipe (just once) to get hold of the schema of the LazyFrame.
lf.pipe(lambda lf: lf.with_columns(
get_ham("col_a"),
get_bar("col_b", lf.schema),
get_foo("col_c", lf.schema),
)
编写返回表达式的函数的另一个好处是,这些函数是可组合的,因为表达式可以被链式调用和部分应用,从而在设计上提供了更大的灵活性。