在R上实现此目的的标准方法是使用cut()
,在熊猫中使用pd.cut()
。这是一种将数字数据隐藏为类别的直接方法,而不是可以动态定义的类别(通常称为breaks
)。
为了不重写轮子,我将参考 pandas 文档和 R 作为示例,因为它们比我现在即兴创作的任何文档都要好得多。
更新:从 Polars 0.13.57 开始,现在有一个cut
函数。
极地版本 0.13.56 及更早
版本极地本身没有切割功能。 但是,一种简单(且性能非常高)的剪切数据方法是使用join_asof
.
让我们从这些数据开始。
import polars as pl
import numpy as np
sample_size = 10
df = pl.DataFrame(
{
"var1": np.random.default_rng(seed=0).normal(0, 1, sample_size),
}
)
df
shape: (10, 1)
┌───────────┐
│ var1 │
│ --- │
│ f64 │
╞═══════════╡
│ 0.1257 │
├╌╌╌╌╌╌╌╌╌╌╌┤
│ -0.132105 │
├╌╌╌╌╌╌╌╌╌╌╌┤
│ 0.640423 │
├╌╌╌╌╌╌╌╌╌╌╌┤
│ 0.1049 │
├╌╌╌╌╌╌╌╌╌╌╌┤
│ -0.535669 │
├╌╌╌╌╌╌╌╌╌╌╌┤
│ 0.361595 │
├╌╌╌╌╌╌╌╌╌╌╌┤
│ 1.304 │
├╌╌╌╌╌╌╌╌╌╌╌┤
│ 0.947081 │
├╌╌╌╌╌╌╌╌╌╌╌┤
│ -0.703735 │
├╌╌╌╌╌╌╌╌╌╌╌┤
│ -1.265421 │
└───────────┘
算法
步骤 1:创建包含断点和 cat 变量的简单数据集
首先,我们将创建一个包含断点以及分类变量的数据帧。假设我们需要 -1、0 和 1 的断点。 我们将在构造函数中提供这些。
我还将使用with_row_count
自动生成我们的分类值。 (如果您愿意,可以为构造函数中的分类变量分配其他内容。
请注意,断点的数据类型必须与要剪切的变量的数据类型匹配。 因此,在这个简单的示例中,我将第一个断点写为"-1.0"(以便 Polars 自动创建break_pt
作为Float64
,而不是整数。
break_df = pl.DataFrame(
{
"break_pt": [-1.0, 0, 1],
}
).with_row_count("binned")
break_df
>>> break_df
shape: (3, 2)
┌────────┬──────────┐
│ binned ┆ break_pt │
│ --- ┆ --- │
│ u32 ┆ f64 │
╞════════╪══════════╡
│ 0 ┆ -1.0 │
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 1 ┆ 0.0 │
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 2 ┆ 1.0 │
└────────┴──────────┘
第 2 步:使用join_asof
加入
现在我们可以执行join_asof
. 请注意,这两个数据集都必须按as_of键排序,因此我们需要在连接之前按连续变量 (var1
) 对随机数的数据帧进行排序。 (break_df
已排序。
(
df
.sort(["var1"])
.join_asof(
break_df,
left_on="var1",
right_on="break_pt",
strategy="forward",
)
)
shape: (10, 3)
┌───────────┬────────┬──────────┐
│ var1 ┆ binned ┆ break_pt │
│ --- ┆ --- ┆ --- │
│ f64 ┆ u32 ┆ f64 │
╞═══════════╪════════╪══════════╡
│ -1.265421 ┆ 0 ┆ -1.0 │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ -0.703735 ┆ 1 ┆ 0.0 │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ -0.535669 ┆ 1 ┆ 0.0 │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ -0.132105 ┆ 1 ┆ 0.0 │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 0.1049 ┆ 2 ┆ 1.0 │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 0.1257 ┆ 2 ┆ 1.0 │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 0.361595 ┆ 2 ┆ 1.0 │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 0.640423 ┆ 2 ┆ 1.0 │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 0.947081 ┆ 2 ┆ 1.0 │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 1.304 ┆ null ┆ null │
└───────────┴────────┴──────────┘
这给我们留下了最后一个箱(最后一个断点以上的值)作为null
. 要用适当的binned
值填充这些null
值,我们可以使用fill_null
.
(
df
.sort(["var1"])
.join_asof(
break_df,
left_on="var1",
right_on="break_pt",
strategy="forward",
)
.with_column(pl.col("binned").fill_null(pl.col("binned").max() + 1))
)
shape: (10, 3)
┌───────────┬────────┬──────────┐
│ var1 ┆ binned ┆ break_pt │
│ --- ┆ --- ┆ --- │
│ f64 ┆ i64 ┆ f64 │
╞═══════════╪════════╪══════════╡
│ -1.265421 ┆ 0 ┆ -1.0 │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ -0.703735 ┆ 1 ┆ 0.0 │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ -0.535669 ┆ 1 ┆ 0.0 │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ -0.132105 ┆ 1 ┆ 0.0 │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 0.1049 ┆ 2 ┆ 1.0 │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 0.1257 ┆ 2 ┆ 1.0 │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 0.361595 ┆ 2 ┆ 1.0 │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 0.640423 ┆ 2 ┆ 1.0 │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 0.947081 ┆ 2 ┆ 1.0 │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 1.304 ┆ 3 ┆ null │
└───────────┴────────┴──────────┘
性能
那么效果如何呢? 让我们将随机样本增加到 1 亿个值。 让我们将休息列表扩展到 ~6,000 个断点。
sample_size = 100_000_000
df = pl.DataFrame(
{
"var1": np.random.default_rng(seed=0).normal(0, 1, sample_size),
}
)
df
shape: (100000000, 1)
┌───────────┐
│ var1 │
│ --- │
│ f64 │
╞═══════════╡
│ 0.1257 │
├╌╌╌╌╌╌╌╌╌╌╌┤
│ -0.132105 │
├╌╌╌╌╌╌╌╌╌╌╌┤
│ 0.640423 │
├╌╌╌╌╌╌╌╌╌╌╌┤
│ 0.1049 │
├╌╌╌╌╌╌╌╌╌╌╌┤
│ ... │
├╌╌╌╌╌╌╌╌╌╌╌┤
│ -0.714924 │
├╌╌╌╌╌╌╌╌╌╌╌┤
│ 0.269947 │
├╌╌╌╌╌╌╌╌╌╌╌┤
│ -2.3158 │
├╌╌╌╌╌╌╌╌╌╌╌┤
│ -0.383743 │
└───────────┘
break_list = [next_val / 1000 for next_val in range(-3000, 3001)]
break_df = pl.DataFrame(
{
"break_pt": break_list,
}
).with_row_count("binned")
break_df
>>> break_df
shape: (6001, 2)
┌────────┬──────────┐
│ binned ┆ break_pt │
│ --- ┆ --- │
│ u32 ┆ f64 │
╞════════╪══════════╡
│ 0 ┆ -3.0 │
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 1 ┆ -2.999 │
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 2 ┆ -2.998 │
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 3 ┆ -2.997 │
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ ... ┆ ... │
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 5997 ┆ 2.997 │
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 5998 ┆ 2.998 │
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 5999 ┆ 2.999 │
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 6000 ┆ 3.0 │
└────────┴──────────┘
现在对算法本身进行计时...
import time
start = time.perf_counter()
(
df.sort(["var1"])
.join_asof(
break_df,
left_on="var1",
right_on="break_pt",
strategy="forward",
)
.with_column(pl.col("binned").fill_null(pl.col("binned").max() + 1))
)
print(time.perf_counter() - start)
shape: (100000000, 3)
┌───────────┬────────┬──────────┐
│ var1 ┆ binned ┆ break_pt │
│ --- ┆ --- ┆ --- │
│ f64 ┆ i64 ┆ f64 │
╞═══════════╪════════╪══════════╡
│ -5.666706 ┆ 0 ┆ -3.0 │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ -5.6048 ┆ 0 ┆ -3.0 │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ -5.428571 ┆ 0 ┆ -3.0 │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ -5.350106 ┆ 0 ┆ -3.0 │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ ... ┆ ... ┆ ... │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 5.327897 ┆ 6001 ┆ null │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 5.344677 ┆ 6001 ┆ null │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 5.386379 ┆ 6001 ┆ null │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 5.4829 ┆ 6001 ┆ null │
└───────────┴────────┴──────────┘
>>> print(time.perf_counter() - start)
3.107058142999449
刚刚超过3秒。
编辑:一些有用的补充/改进
我们可以进行的一些改进:
- 添加标签的容量,默认标签或指定的标签列表
- 将标签作为分类变量返回
- 允许将切割值列表作为整数传递
- 将算法封装到可调用函数中
from typing import List
def cut_dataframe(_df: pl.DataFrame,
var_nm: str,
bins: List[float],
labels: List[str] = None) -> pl.DataFrame:
cuts_df = pl.DataFrame([
pl.Series(
name="break_pt",
values=bins,
dtype=pl.Float64
).extend_constant(np.Inf, 1)
])
if labels:
cuts_df = cuts_df.with_column(
pl.Series(
name="category",
values=labels
)
)
else:
cuts_df = cuts_df.with_column(
pl.format(
"({}, {}]",
pl.col("break_pt").shift_and_fill(1, -np.Inf),
pl.col("break_pt"),
)
.alias("category")
)
cuts_df = cuts_df.with_column(pl.col("category").cast(pl.Categorical))
result = (
_df.sort([var_nm]).join_asof(
cuts_df,
left_on=var_nm,
right_on="break_pt",
strategy="forward",
)
)
return result
现在,我们可以通过一次调用来剪切数据帧。
下面,我们将在不指定任何标签的情况下调用该函数,允许该函数创建默认标签。 请注意,我们不必担心断点列表是浮点数 - 该函数会自动将值转换为pl.Float64
。
cut_dataframe(df, "var1", [-1, 1])
shape: (10, 3)
┌───────────┬──────────┬──────────────┐
│ var1 ┆ break_pt ┆ category │
│ --- ┆ --- ┆ --- │
│ f64 ┆ f64 ┆ cat │
╞═══════════╪══════════╪══════════════╡
│ -1.265421 ┆ -1.0 ┆ (-inf, -1.0] │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ -0.703735 ┆ 1.0 ┆ (-1.0, 1.0] │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ -0.535669 ┆ 1.0 ┆ (-1.0, 1.0] │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ -0.132105 ┆ 1.0 ┆ (-1.0, 1.0] │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 0.1049 ┆ 1.0 ┆ (-1.0, 1.0] │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 0.1257 ┆ 1.0 ┆ (-1.0, 1.0] │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 0.361595 ┆ 1.0 ┆ (-1.0, 1.0] │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 0.640423 ┆ 1.0 ┆ (-1.0, 1.0] │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 0.947081 ┆ 1.0 ┆ (-1.0, 1.0] │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 1.304 ┆ inf ┆ (1.0, inf] │
└───────────┴──────────┴──────────────┘
在这里,我们将传递一个标签列表。
cut_dataframe(df, "var1", [-1, 1], ["low", "med", "hi"])
shape: (10, 3)
┌───────────┬──────────┬──────────┐
│ var1 ┆ break_pt ┆ category │
│ --- ┆ --- ┆ --- │
│ f64 ┆ f64 ┆ cat │
╞═══════════╪══════════╪══════════╡
│ -1.265421 ┆ -1.0 ┆ low │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ -0.703735 ┆ 1.0 ┆ med │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ -0.535669 ┆ 1.0 ┆ med │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ -0.132105 ┆ 1.0 ┆ med │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 0.1049 ┆ 1.0 ┆ med │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 0.1257 ┆ 1.0 ┆ med │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 0.361595 ┆ 1.0 ┆ med │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 0.640423 ┆ 1.0 ┆ med │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 0.947081 ┆ 1.0 ┆ med │
├╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 1.304 ┆ inf ┆ hi │
└───────────┴──────────┴──────────┘
希望以上内容更有帮助。