如何将具有连续数据的序列或数据框列转换为基于极坐标中的一组中断的分类列?



在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       │
└───────────┴──────────┴──────────┘

希望以上内容更有帮助。

最新更新