Skip to content

从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中没有.lociloc方法 - 并且在Polars中也没有SettingWithCopyWarning

然而,在Polars中选择数据的最佳方式是使用表达式API。例如,如果你想在pandas中选择一列,你可以执行以下操作之一:

df['a']
df.loc[:,'a']

但在Polars中,你会使用.select方法:

df.select('a')

如果你想根据值选择行,那么在Polars中使用.filter方法:

df.filter(pl.col('a') < 10)

如下面的表达式部分所述,Polars 可以在 .selectfilter 中并行运行操作,并且 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 通过识别只有 id1v1 列是相关的来优化此查询,因此只会从 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,其中包含列 abc。我们希望根据条件重新分配列 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中,一个常见的用法是利用pipeDataFrame应用某些函数。将这种编码风格复制到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),
)

编写返回表达式的函数的另一个好处是,这些函数是可组合的,因为表达式可以被链式调用和部分应用,从而在设计上提供了更大的灵活性。