414 lines
15 KiB
Python
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)
|