Files
lightgbm-org--lightgbm/tests/python_package_test/test_polars.py
T
2026-07-13 13:27:18 +08:00

414 lines
15 KiB
Python

# coding: utf-8
import filecmp
from pathlib import Path
from typing import Any, Dict, Optional
import numpy as np
import pytest
import lightgbm as lgb
from .utils import np_assert_array_equal
pl = pytest.importorskip("polars")
# ----------------------------------------------------------------------------------------------- #
# UTILITIES #
# ----------------------------------------------------------------------------------------------- #
_INTEGER_TYPES = [pl.Int8, pl.Int16, pl.Int32, pl.Int64, pl.UInt8, pl.UInt16, pl.UInt32, pl.UInt64]
_FLOAT_TYPES = [pl.Float32, pl.Float64]
def generate_simple_polars_frame() -> pl.DataFrame:
values = [1, 2, 3, 4, 5]
bool_values = [True, True, False, False, True]
columns = {f"col_{i}": pl.Series(values, dtype=dtype) for i, dtype in enumerate(_INTEGER_TYPES + _FLOAT_TYPES)}
columns[f"col_{len(columns)}"] = pl.Series(bool_values, dtype=pl.Boolean)
return pl.DataFrame(columns)
def generate_nullable_polars_frame(dtype: Any) -> pl.DataFrame:
return pl.DataFrame(
{
"col_0": pl.Series([1, None, 3, 4, 5], dtype=dtype),
"col_1": pl.Series([None, 2, 3, 4, 5], dtype=dtype),
"col_2": pl.Series([1, 2, 3, 4, None], dtype=dtype),
"col_3": pl.Series([None, None, None, None, None], dtype=dtype),
}
)
def generate_dummy_polars_frame() -> pl.DataFrame:
return pl.DataFrame(
{
"a": pl.Series([1, 2, 3, 4, 5], dtype=pl.UInt8),
"b": pl.Series([0.5, 0.6, 0.1, 0.8, 1.5], dtype=pl.Float32),
}
)
def generate_random_polars_frame(
num_columns: int,
num_datapoints: int,
seed: int,
generate_nulls: bool = True,
values: Optional[np.ndarray] = None,
) -> pl.DataFrame:
return pl.DataFrame(
{
f"col_{i}": generate_random_polars_series(
num_datapoints, seed + i, generate_nulls=generate_nulls, values=values
)
for i in range(num_columns)
}
)
def generate_random_polars_series(
num_datapoints: int,
seed: int,
generate_nulls: bool = True,
values: Optional[np.ndarray] = None,
) -> pl.Series:
generator = np.random.default_rng(seed)
data = (
generator.standard_normal(num_datapoints).astype(np.float32)
if values is None
else generator.choice(values, size=num_datapoints, replace=True)
)
series = pl.Series("col", data, dtype=pl.Float32)
if generate_nulls:
indices = generator.choice(len(data), size=num_datapoints // 10)
series = series.scatter(indices, None)
return series
def dummy_dataset_params() -> Dict[str, Any]:
return {
"min_data_in_bin": 1,
"min_data_in_leaf": 1,
}
# ----------------------------------------------------------------------------------------------- #
# UNIT TESTS #
# ----------------------------------------------------------------------------------------------- #
# ------------------------------------------- DATASET ------------------------------------------- #
def assert_datasets_equal(tmp_path: Path, lhs: lgb.Dataset, rhs: lgb.Dataset):
lhs._dump_text(tmp_path / "polars.txt")
rhs._dump_text(tmp_path / "pandas.txt")
assert filecmp.cmp(tmp_path / "polars.txt", tmp_path / "pandas.txt")
@pytest.mark.parametrize(
("polars_frame_fn", "dataset_params"),
[ # Use lambda functions here to minimize memory consumption
(generate_simple_polars_frame, dummy_dataset_params()),
(generate_dummy_polars_frame, dummy_dataset_params()),
(lambda: generate_nullable_polars_frame(pl.Float32), dummy_dataset_params()),
(lambda: generate_nullable_polars_frame(pl.Int32), dummy_dataset_params()),
(lambda: generate_random_polars_frame(3, 1000, 42), {}),
(lambda: generate_random_polars_frame(100, 10000, 43), {}),
],
)
def test_dataset_construct_fuzzy(tmp_path, polars_frame_fn, dataset_params):
polars_frame = polars_frame_fn()
polars_dataset = lgb.Dataset(polars_frame, params=dataset_params)
polars_dataset.construct()
pandas_dataset = lgb.Dataset(polars_frame.to_pandas(), params=dataset_params)
pandas_dataset.construct()
assert_datasets_equal(tmp_path, polars_dataset, pandas_dataset)
def test_dataset_construct_fuzzy_boolean(tmp_path):
boolean_data = generate_random_polars_frame(10, 10000, 42, generate_nulls=False, values=np.array([True, False]))
float_data = boolean_data.cast(pl.Float32)
polars_dataset = lgb.Dataset(boolean_data)
polars_dataset.construct()
pandas_dataset = lgb.Dataset(float_data.to_pandas())
pandas_dataset.construct()
assert_datasets_equal(tmp_path, polars_dataset, pandas_dataset)
# -------------------------------------------- FIELDS ------------------------------------------- #
def test_dataset_construct_fields_fuzzy():
polars_frame = generate_random_polars_frame(3, 1000, 42)
polars_labels = generate_random_polars_series(1000, 42, generate_nulls=False)
polars_weights = generate_random_polars_series(1000, 42, generate_nulls=False)
polars_groups = pl.Series("group", [300, 400, 50, 250], dtype=pl.Int32)
polars_dataset = lgb.Dataset(polars_frame, label=polars_labels, weight=polars_weights, group=polars_groups)
polars_dataset.construct()
pandas_dataset = lgb.Dataset(
polars_frame.to_pandas(),
label=polars_labels.to_numpy(),
weight=polars_weights.to_numpy(),
group=polars_groups.to_numpy(),
)
pandas_dataset.construct()
# Check for equality
for field in ("label", "weight", "group"):
np_assert_array_equal(polars_dataset.get_field(field), pandas_dataset.get_field(field), strict=True)
np_assert_array_equal(polars_dataset.get_label(), pandas_dataset.get_label(), strict=True)
np_assert_array_equal(polars_dataset.get_weight(), pandas_dataset.get_weight(), strict=True)
# -------------------------------------------- LABELS ------------------------------------------- #
@pytest.mark.parametrize("polars_type", _INTEGER_TYPES + _FLOAT_TYPES)
def test_dataset_construct_labels(polars_type):
data = generate_dummy_polars_frame()
labels = pl.Series("label", [0, 1, 0, 0, 1], dtype=polars_type)
dataset = lgb.Dataset(data, label=labels, params=dummy_dataset_params())
dataset.construct()
expected = np.array([0, 1, 0, 0, 1], dtype=np.float32)
np_assert_array_equal(expected, dataset.get_label(), strict=True)
def test_dataset_construct_labels_boolean():
data = generate_dummy_polars_frame()
labels = pl.Series("label", [False, True, False, False, True], dtype=pl.Boolean)
dataset = lgb.Dataset(data, label=labels, params=dummy_dataset_params())
dataset.construct()
expected = np.array([0, 1, 0, 0, 1], dtype=np.float32)
np_assert_array_equal(expected, dataset.get_label(), strict=True)
# ------------------------------------------- WEIGHTS ------------------------------------------- #
def test_dataset_construct_weights_none():
data = generate_dummy_polars_frame()
weight = pl.Series("weight", [1, 1, 1, 1, 1], dtype=pl.Float32)
dataset = lgb.Dataset(data, weight=weight, params=dummy_dataset_params())
dataset.construct()
assert dataset.get_weight() is None
assert dataset.get_field("weight") is None
@pytest.mark.parametrize("polars_type", _FLOAT_TYPES)
def test_dataset_construct_weights(polars_type):
data = generate_dummy_polars_frame()
weights = pl.Series("weight", [3, 0.7, 1.5, 0.5, 0.1], dtype=polars_type)
dataset = lgb.Dataset(data, weight=weights, params=dummy_dataset_params())
dataset.construct()
expected = np.array([3, 0.7, 1.5, 0.5, 0.1], dtype=np.float32)
np_assert_array_equal(expected, dataset.get_weight(), strict=True)
# -------------------------------------------- GROUPS ------------------------------------------- #
@pytest.mark.parametrize("polars_type", _INTEGER_TYPES)
def test_dataset_construct_groups(polars_type):
data = generate_dummy_polars_frame()
groups = pl.Series("group", [2, 3], dtype=polars_type)
dataset = lgb.Dataset(data, group=groups, params=dummy_dataset_params())
dataset.construct()
expected = np.array([0, 2, 5], dtype=np.int32)
np_assert_array_equal(expected, dataset.get_field("group"), strict=True)
# ----------------------------------------- INIT SCORES ----------------------------------------- #
@pytest.mark.parametrize("polars_type", _INTEGER_TYPES + _FLOAT_TYPES)
def test_dataset_construct_init_scores_array(polars_type):
data = generate_dummy_polars_frame()
init_scores = pl.Series("init_score", [0, 1, 2, 3, 3], dtype=polars_type)
dataset = lgb.Dataset(data, init_score=init_scores, params=dummy_dataset_params())
dataset.construct()
expected = np.array([0, 1, 2, 3, 3], dtype=np.float64)
np_assert_array_equal(expected, dataset.get_init_score(), strict=True)
def test_dataset_construct_init_scores_table():
data = generate_dummy_polars_frame()
init_scores = pl.DataFrame(
{
"a": generate_random_polars_series(5, seed=1, generate_nulls=False),
"b": generate_random_polars_series(5, seed=2, generate_nulls=False),
"c": generate_random_polars_series(5, seed=3, generate_nulls=False),
}
)
dataset = lgb.Dataset(data, init_score=init_scores, params=dummy_dataset_params())
dataset.construct()
actual = dataset.get_init_score()
expected = init_scores.to_numpy().astype(np.float64)
np_assert_array_equal(expected, actual, strict=True)
# ------------------------------------------ PREDICTION ----------------------------------------- #
def assert_equal_predict_polars_pandas(booster: lgb.Booster, data: pl.DataFrame):
pandas_data = data.to_pandas()
p_polars = booster.predict(data)
p_pandas = booster.predict(pandas_data)
np_assert_array_equal(p_polars, p_pandas, strict=True)
p_raw_polars = booster.predict(data, raw_score=True)
p_raw_pandas = booster.predict(pandas_data, raw_score=True)
np_assert_array_equal(p_raw_polars, p_raw_pandas, strict=True)
p_leaf_polars = booster.predict(data, pred_leaf=True)
p_leaf_pandas = booster.predict(pandas_data, pred_leaf=True)
np_assert_array_equal(p_leaf_polars, p_leaf_pandas, strict=True)
p_pred_contrib_polars = booster.predict(data, pred_contrib=True)
p_pred_contrib_pandas = booster.predict(pandas_data, pred_contrib=True)
np_assert_array_equal(p_pred_contrib_polars, p_pred_contrib_pandas, strict=True)
p_first_iter_polars = booster.predict(data, start_iteration=0, num_iteration=1, raw_score=True)
p_first_iter_pandas = booster.predict(pandas_data, start_iteration=0, num_iteration=1, raw_score=True)
np_assert_array_equal(p_first_iter_polars, p_first_iter_pandas, strict=True)
def test_predict_regression():
data_float = generate_random_polars_frame(10, 10000, 42)
data_bool = generate_random_polars_frame(1, 10000, 42, generate_nulls=False, values=np.array([True, False]))
data = data_float.with_columns(data_bool["col_0"].alias("col_bool"))
dataset = lgb.Dataset(
data,
label=generate_random_polars_series(10000, 43, generate_nulls=False),
params=dummy_dataset_params(),
)
booster = lgb.train(
{"objective": "regression", "num_leaves": 7},
dataset,
num_boost_round=5,
)
assert_equal_predict_polars_pandas(booster, data)
def test_predict_binary_classification():
data = generate_random_polars_frame(10, 10000, 42)
dataset = lgb.Dataset(
data,
label=generate_random_polars_series(10000, 43, generate_nulls=False, values=np.arange(2)),
params=dummy_dataset_params(),
)
booster = lgb.train(
{"objective": "binary", "num_leaves": 7},
dataset,
num_boost_round=5,
)
assert_equal_predict_polars_pandas(booster, data)
def test_predict_multiclass_classification():
data = generate_random_polars_frame(10, 10000, 42)
dataset = lgb.Dataset(
data,
label=generate_random_polars_series(10000, 43, generate_nulls=False, values=np.arange(5)),
params=dummy_dataset_params(),
)
booster = lgb.train(
{"objective": "multiclass", "num_leaves": 7, "num_class": 5},
dataset,
num_boost_round=5,
)
assert_equal_predict_polars_pandas(booster, data)
def test_predict_ranking():
data = generate_random_polars_frame(10, 10000, 42)
dataset = lgb.Dataset(
data,
label=generate_random_polars_series(10000, 43, generate_nulls=False, values=np.arange(4)),
group=np.array([1000, 2000, 3000, 4000]),
params=dummy_dataset_params(),
)
booster = lgb.train(
{"objective": "lambdarank", "num_leaves": 7},
dataset,
num_boost_round=5,
)
assert_equal_predict_polars_pandas(booster, data)
def test_polars_feature_name_auto():
data = generate_dummy_polars_frame()
dataset = lgb.Dataset(
data,
label=pl.Series("label", [0, 1, 0, 0, 1]),
params=dummy_dataset_params(),
categorical_feature=["a"],
)
booster = lgb.train({"num_leaves": 7}, dataset, num_boost_round=5)
assert booster.feature_name() == ["a", "b"]
def test_polars_feature_name_manual():
data = generate_dummy_polars_frame()
dataset = lgb.Dataset(
data,
label=pl.Series("label", [0, 1, 0, 0, 1]),
params=dummy_dataset_params(),
feature_name=["c", "d"],
categorical_feature=["c"],
)
booster = lgb.train({"num_leaves": 7}, dataset, num_boost_round=5)
assert booster.feature_name() == ["c", "d"]
def test_get_data_polars_frame():
from polars.testing import assert_frame_equal # noqa: PLC0415
original_frame = generate_simple_polars_frame()
dataset = lgb.Dataset(original_frame, free_raw_data=False)
dataset.construct()
returned_data = dataset.get_data()
assert isinstance(returned_data, pl.DataFrame)
assert returned_data.schema == original_frame.schema
assert returned_data.shape == original_frame.shape
assert_frame_equal(returned_data, original_frame)
def test_get_data_polars_frame_subset(rng):
from polars.testing import assert_frame_equal # noqa: PLC0415
original_frame = generate_random_polars_frame(num_columns=3, num_datapoints=1000, seed=42)
dataset = lgb.Dataset(original_frame, free_raw_data=False)
dataset.construct()
subset_size = 100
used_indices = rng.choice(a=original_frame.shape[0], size=subset_size, replace=False)
used_indices = sorted(used_indices)
subset_dataset = dataset.subset(used_indices).construct()
expected_subset = original_frame[used_indices]
subset_data = subset_dataset.get_data()
assert isinstance(subset_data, pl.DataFrame)
assert subset_data.schema == expected_subset.schema
assert subset_data.shape == expected_subset.shape
assert len(subset_data) == len(used_indices)
assert subset_data.shape == (subset_size, 3)
assert_frame_equal(subset_data, expected_subset)