Metadata-Version: 2.4
Name: polars-optbinning
Version: 0.4.0
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: MacOS
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Rust
Classifier: Topic :: Scientific/Engineering
Classifier: Typing :: Typed
Requires-Dist: polars>=1.37.1
License-File: LICENSE
Summary: PyO3-Polars OptBinning extension with Rust-side optimization kernels
Keywords: polars,binning,optimal-binning,feature-engineering,credit-scoring,rust
Home-Page: https://gitee.com/Ky1eYang/polars-optbinning
Author-email: "kyle.yang" <ky1eyangs225@gmail.com>
Maintainer-email: "kyle.yang" <ky1eyangs225@gmail.com>
License-Expression: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Changelog, https://gitee.com/Ky1eYang/polars-optbinning/blob/master/CHANGELOG.md
Project-URL: Documentation, https://gitee.com/Ky1eYang/polars-optbinning#readme
Project-URL: Homepage, https://gitee.com/Ky1eYang/polars-optbinning
Project-URL: Issues, https://gitee.com/Ky1eYang/polars-optbinning/issues
Project-URL: Repository, https://gitee.com/Ky1eYang/polars-optbinning

# polars-optbinning
[![PyPI](https://img.shields.io/pypi/v/polars-optbinning)](https://pypi.org/project/polars-optbinning/)

基于 Rust 的 Polars DataFrame 规则最优分箱引擎。注意项目仍处于早期开发阶段，每个版本API可能会发生重大破坏性变更。

核心 API 是 `BinningTransformer`：为一个或多个特征拟合经优化的分箱规则，保留拟合后的规则组，并在 Polars 管道中复用。

## 安装

从 PyPI 安装已发布的包：

```bash
pip install polars-optbinning
```

本地开发：

```bash
uv sync --group dev
uv run maturin develop --release
```

## 用法

```python
import polars as pl
from polars_optbinning import BinningTransformer, Objective, Stat

df = pl.DataFrame(
    {
        "x1": [1.0, 1.2, 2.0, 2.2, 3.1, 3.4, 4.5, 5.0, 5.2, 6.0],
        "x2": [10.0, 9.8, 9.5, 8.9, 8.1, 7.3, 6.5, 5.9, 5.1, 4.2],
        "y": [0, 0, 0, 1, 0, 1, 1, 1, 1, 1],
    }
)

transformer = BinningTransformer(
    features=[pl.col("x1"), pl.col("x2")],
    target=pl.col("y"),
)

transformer.fit(
    df,
    objective=Objective.iv(),
    mapping=[Stat.woe(), Stat.count()],
    max_bins=4,
)

eager_out = transformer.transform(df)
pipeline_out = df.with_columns(transformer.transform_())
count_out = transformer.transform(df, extra_mapping=[Stat.count()])
print(eager_out)
print(pipeline_out)
print(count_out)
print(transformer.summary())
```

`summary()` 可以重新执行当前规则并计算与 transform 映射无关的统计量：

```python
stats = transformer.summary(
    extra_stat=[
        Stat.count(),
        Stat.feat_avg(),
        Stat.target_avg(),
        Stat.avg("x2"),
        Stat.sum("x2"),
        Stat.entropy("category"),
        Stat.woe(),
    ]
)
```

`feat_avg()`、`feat_min()` 和 `feat_max()` 始终返回列表，覆盖每个规则组所用的特征。`avg(expr)`、`min(expr)`、`max(expr)`、`sum(expr)` 和 `entropy(expr)` 返回单个 Polars 表达式的标量统计量，其中 `entropy(expr)` 用 base-2 香浓熵统计规则内固定多分类特征的类别分布。基于表达式的输出名称从表达式派生，例如 `Stat.avg("x2")` 产生 `x2_avg`。`sum()`、`woe()`、`iv()` 和 `event_rate()` 默认使用初始化时指定的目标，也接受显式表达式。

拟合后的转换器会复用其选定的训练数据。传递 `summary(extra_stat=[...], df=new_df)` 可以在另一数据集上计算统计量。通过解析或手动构造的转换器需要提供 `df`，并可以使用显式表达式统计量（如 `Stat.woe("label")`）；依赖默认目标的统计量需要 `BinningTransformer(target=...)`。`fit()` 或 `parse()` 中提供的 `mapping` 仍然是 `transform()` 和 `transform_()` 产生的显式模式；未被 mapping 选中的规则统计量不会自动展开到 transform 列中。`transform(df, extra_mapping=[...])` 和 `transform_(extra_mapping=[...])` 可以在本次输出中临时追加规则统计映射，缺失的统计量会像 `summary(extra_stat=...)` 一样先按当前数据重新聚合，且不修改转换器自身。表达式版 `transform_()` 没有显式 `df` 参数，因此会使用 `fit()` 时保存的训练视图；解析或手动构造的转换器仍需要这些统计量已存在于规则中。

`summary(..., transition_count=True)` 会按输入 DataFrame 的当前行顺序额外统计每个规则组的转入数量：当前时刻分箱为 `i` 且上一时刻分箱不为 `i` 的次数。转移率可用 `transition_count / count` 在 Polars 中直接计算。

自 `0.4.0` 起，Rust core 内部的统计描述使用 typed `StatKind` + `StatPayload`
结构，`Stat.to_json()`、`Rule.to_json()` 和 `RuleGroup.to_json()` 中的统计字段也改为
tagged JSON；旧版 `kind/value/operation` 统计 JSON 不再兼容。Python 侧仍保留
`Stat.count()`、`Stat.avg("x")`、`Stat.entropy("category")` 等字符串友好的构造接口。

`psi(df_train, df_test)` 可按每个分箱或树叶对两组数据分布计算 PSI 贡献；
对同一 `group` 的 `value` 求和即可得到该组总 PSI：

```python
psi = transformer.psi(df_train, df_test)
```

`group_summary(extra_stat=[...])` 可以在组粒度汇总可加性统计量，当前支持
`Stat.count()`、`Stat.iv()`、`Stat.gain()` 和 `Stat.sum(expr)`。

两两 2D 分箱为每个特征对创建一个特征：

```python
pairwise = BinningTransformer(
    features=[pl.col("x1"), pl.col("x2")],
    target=pl.col("y"),
    prefix="pair_",
)

pairwise.fit(
    df,
    objective=Objective.iv(),
    mapping=[Stat.index()],
    max_bins=4,
    mode="pairwise_2d",
    progress=True,
)

out = df.with_columns(pairwise.transform_())
```

连续目标回归示例见 `examples/example_linear.py`。

原生 XGBoost 和 LightGBM 树模型可以解析为可复用的规则组：

```python
model = BinningTransformer.parse(
    "examples/models/multiclass_xgb.json",
    mapping=[Stat.bin_value()],
    prefix="xgb_",
)

tree_outputs = df.with_columns(model.transform_())
raw_margin = df.with_columns(prediction=model.predict_())
probability = df.with_columns(prediction=model.predict_(output="probability"))
classes = df.with_columns(prediction=model.predict_(output="class"))
eager_prediction = model.predict(df, output="class")
print(model.format_tree(0))
print(model.node_summary(0, df=df))
```

`parse()` 接受 XGBoost `save_model()` JSON/UBJ 文件和 LightGBM `Booster.save_model()` 文本文件、路径及可读文件对象。解析后的转换器每棵树暴露一个可编辑的 `RuleGroup`，不可再次拟合。原生模型训练、序列化、解析以及与 Polars 预测对比的完整示例见 `examples/example_multiclass_models.py`。

也支持手动构建规则引擎：

```python
from polars_optbinning import Condition, Rule, RuleGroup

group = RuleGroup(
    "x",
    ["x"],
    [
        Rule(Condition.interval("x", None, 2.0), [Stat.const(0.1)]),
        Rule(Condition.interval("x", 2.0, None), [Stat.const(0.9)]),
    ],
    [Stat.const(0.0)],
)

engine = BinningTransformer(groups=[group])
out = pl.DataFrame({"x": [1.0, 2.5]}).with_columns(engine.transform_())
```

条件可以跨一个或多个特征进行集合组合：

```python
high_risk = (Condition.interval("age", 65.0, None) & Condition.is_in("region", [1.0, 2.0]))
eligible = high_risk | Condition.interval("score", 700.0, None)
excluded = eligible - Condition.is_in("region", [9.0])
assert excluded.features == ["age", "region", "score"]
excluded = excluded.simplify()

group = RuleGroup(
    "decision",
    ["age", "region", "score"],
    [
        Rule(excluded, [Stat.const(1.0)]),
        Rule(Condition.otherwise(), [Stat.const(0.0)]),
    ],
    [Stat.const(0.0)],
)
```

`otherwise` 是可选的，但存在时必须作为最后一条规则。空组以及没有 `otherwise` 规则的未匹配行输出 null。拟合后的分箱组按拟合计数的降序排列，且始终包含一条最终的 `otherwise` 规则。

规则可以编译为显式的二进制执行链：

```python
group.compile()
assert group.execution_kind == "rule_chain"
```

## 原生 GBDT

`mode="gbdt"` 使用 Rust 原生 XGBoost-like 直方图梯度提升训练器。训练器支持数值特征、原生类别特征、回归目标、二分类目标，以及
`Objective.log_loss()` / `Objective.cross_entropy_loss()` 多分类：

```python
model = BinningTransformer(features=["x1", "x2"], target="y").fit(
    df,
    mode="gbdt",
    objective=Objective.log_loss(),
    mapping=[Stat.bin_value(), Stat.gain()],
    extra_params={
        "n_estimators": 100,
        "learning_rate": 0.1,
        "max_depth": 3,
        "max_leaves": 16,
        "max_bin": 64,
        "min_child_weight": 1e-3,
        "min_samples_leaf": 2,
        # 或使用 "min_samples_fraction": 0.02 按训练样本数比例设置叶子最小样本数
        "lambda_l1": 0.0,
        "lambda_l2": 1.0,
        "gamma": 0.0,
        "boosting_type": "gbdt",
        "subsample": 0.9,
        "colsample_bytree": 1.0,
        "top_rate": 0.2,
        "other_rate": 0.1,
        "enable_bundle": False,
        "bundle_concentration_threshold": 0.95,
        "bundle_conflict_threshold": 0.05,
        "categorical_features": ["x2"],
        "monotone_constraints": {"x1": 1},
        "missing_policy": "auto",
        "seed": 42,
    },
)

probability = df.select(model.predict_(output="probability"))
importance = model.feature_importance()
```

训练器使用 `objective.rs` 中的原生 loss 枚举统一计算初始分数、梯度和海森矩阵。
数值特征按分位点构建 histogram split；类别特征先编码为稳定类别 code，再按类别
leaf score 排序并扫描有序前缀作为候选集合，避免枚举指数级子集。缺失值使用
XGBoost-like default direction；数值特征支持单调约束候选过滤和子节点预测边界传播。
`lambda_l1`/`lambda_l2` 分别控制叶权重的 L1/L2 正则；`boosting_type="goss"`
启用大梯度单边采样，此时不能同时设置 `subsample < 1.0`，并通过
`top_rate`/`other_rate` 控制大梯度保留和小梯度采样比例。`enable_bundle=True`
启用训练期 EFB/Bundle 优化：对高集中度且低冲突度的数值特征桶分组调度，
导出的树仍使用原始特征名，类别特征暂不参与 Bundle。
完整 wine 多分类示例见 `examples/example_gbdt.py`。

普通 `optbin_nd` 表达式已移除；需要多维特征切分时，建议使用原生 GBDT
训练器，让多特征交互由树分裂自动学习。

树规则暴露 `Stat.gain()`，定义为从根到叶子路径上的累积分裂增益。`feature_importance()` 则对每个内部分裂恰好计数一次，返回 `feature`、`gain`、`gain_normalized` 和 `split_count`。结构化的规则编辑会使原始树的重要性信息失效。

## 原生随机森林

`mode="random_forest"` 提供 Rust 原生随机森林后端，复用同一套树规则、预测和特征重要性接口。

```python
forest = BinningTransformer(features=["x1", "x2"], target="y").fit(
    df,
    mode="random_forest",
    objective=Objective.log_loss(),
    mapping=[Stat.bin_value(), Stat.gain()],
    extra_params={
        "n_trees": 100,
        "max_depth": 6,
        "min_samples_leaf": 2,
        "m": 4,
        "bootstrap": True,
        "categorical_features": ["x2"],
        "seed": 42,
    },
)
```

原生 RF 支持回归、二分类、多分类、缺失默认方向、类别 one-vs-rest split、bootstrap/subsample 和特征子采样。wine 数据集示例见 `examples/example_wine_random_forest.py`。

## 当前范围

- 通过 `BinningTransformer.fit` 的多特征优化 1D 分箱。
- 类型化的 `Objective` 和 `Stat` 选择器。
- 即时 `transform(df)` 和表达式友好的 `transform_()`。
- `summary()` 以 Polars DataFrame 格式返回拟合的规则统计量。
- 通过 `mode="pairwise_2d"` 的两两 2D 矩形分箱。
- XGBoost-like 原生直方图 GBDT 训练、树规则转换及模型预测。
- 原生随机森林训练、树规则转换及模型预测。
- XGBoost/LightGBM 数值树解析、叶子规则转换及模型预测。

ND 优化拟合在可复用规则引擎接口稳定期间暂时搁置。

## 发布

发布脚本会验证 `pyproject.toml` 和 `Cargo.toml` 使用相同的版本号，构建优化后的 wheel 和源码分发包，发布到 PyPI，创建对应的 `v<version>` Gitee 标签，并将文件同步到 Gitee Release。

推荐使用 Token 认证：

```shell
$env:GITEE_TOKEN = "<personal-access-token>"
$env:UV_PUBLISH_TOKEN = "<pypi-api-token>"
python scripts/gitee_release.py
```

发布脚本支持 `--phase` 参数，默认 `all` 会先构建分发包，再把 `dist` 中的产物上传到 PyPI 和同步到 Gitee。`--phase build` 仅构建产物；`--phase pypi` 和 `--phase gitee` 都只从 `dist` 读取已有文件并执行上传，适合单独重试某个上传阶段：

```shell
python scripts/gitee_release.py --phase build
python scripts/gitee_release.py --phase pypi
python scripts/gitee_release.py --phase gitee
```

`PYPI_API_TOKEN` 可作为别名使用。也支持账号/密码认证，通过 `MATURIN_USERNAME` + `MATURIN_PASSWORD` 或 `PYPI_USERNAME` + `PYPI_PASSWORD`。

PyPI 分发包是不可变的。重复运行时，`--skip-existing` 会跳过已发布的文件，同时允许上传其他平台对应的 wheel。Gitee Release 会更新，同名附件会被替换。已有 wheel 会被保留。

`WHEEL_DIR` 可设置构建和上传使用的目录，默认为 `dist`。`SKIP_PYPI=1` 和 `SKIP_GITEE=1` 仍可作为 CI 兼容开关覆盖阶段执行。所有凭证必须存储在 CI 服务的安全变量存储中。

发布检查清单：

1. 同时更新 `pyproject.toml` 和根 `Cargo.toml` 的 `[workspace.package]` 版本号（`crates/python/Cargo.toml` 继承该版本）。
2. 更新 `CHANGELOG.md`。
3. 运行 Clippy 和 Python 测试套件。
4. 在应打版本标签的提交上运行发布脚本。

项目政策请参考 [CONTRIBUTING.md](CONTRIBUTING.md)、[SECURITY.md](SECURITY.md) 和 [LICENSE](LICENSE)。

