Repository: scikit-learn/scikit-learn
File: sklearn/ensemble/_hist_gradient_boosting/tests/test_binning.py (Python)

Code before:

    # Adding more bins to the mapper yields the same results (same thresholds)
    mapper_2 = _BinMapper(n_bins=min(256, n_distinct * 3) + 1)
    binned_2 = mapper_2.fit_transform(data)

    assert_allclose(mapper_1.bin_thresholds_[0], mapper_2.bin_thresholds_[0])
    assert_array_equal(binned_1, binned_2)


def test_binmapper_weighted_vs_repeated_equivalence(global_random_seed):
    rng = np.random.RandomState(global_random_seed)

    n_samples = 500
    X = rng.rand(n_samples, 10)
    sw = rng.randint(0, 10, size=n_samples)

    X_repeated = np.repeat(X, sw, axis=0)

    est_weighted = _BinMapper(n_bins=50).fit(X, sample_weight=sw)

    est_repeated = _BinMapper(n_bins=50).fit(X_repeated, sample_weight=None)

    bin_thresholds_weighted = est_weighted.transform(X)
    bin_thresholds_repeated = est_repeated.transform(X)

    assert_array_equal(bin_thresholds_weighted, bin_thresholds_repeated)


@pytest.mark.parametrize(
    "max_bins, scale, offset",
    [
        (3, 2, -1),
        (42, 1, 0),
        (255, 0.3, 42),
    ],
)
def test_bin_mapper_identity_small(max_bins, scale, offset):
    data = np.arange(max_bins).reshape(-1, 1) * scale + offset
    # max_bins is the number of bins for non-missing values
    n_bins = max_bins + 1
    binned = _BinMapper(n_bins=n_bins).fit_transform(data)
    assert_array_equal(binned, np.arange(max_bins).reshape(-1, 1))


@pytest.mark.parametrize(
    "max_bins_small, max_bins_large",
    [
        (2, 2),
        (3, 3),
        (4, 4),
        (42, 42),

Review comment (bug):
I think we should directly compare the `bin_thresholds_` attributes instead of transformed data. It's more explicit to debug in case of failures.

Code after:
    sw = rng.randint(0, 5, size=n_samples)
    X_repeated = np.repeat(X, sw, axis=0)

    # Collect estimated bins thresholds on the weighted/repeated datasets for
    # `n_resampling_iterations` subsampling. `n_resampling_iterations` is large
    # enough to ensure a well-powered statistical test.
    n_resampling_iterations = 500
    bins_weighted = []
    bins_repeated = []
    for _ in range(n_resampling_iterations):
        params = dict(n_bins=n_bins, subsample=300, random_state=rng)
        est_weighted = _BinMapper(**params).fit(X, sample_weight=sw)
        est_repeated = _BinMapper(**params).fit(X_repeated, sample_weight=None)
        bins_weighted.append(np.hstack(est_weighted.bin_thresholds_))
        bins_repeated.append(np.hstack(est_repeated.bin_thresholds_))

    bins_weighted = np.stack(bins_weighted).T
    bins_repeated = np.stack(bins_repeated).T
    # bins_weighted and bins_weighted of shape (n_thresholds, n_subsample)
    # kstest_pval of shape (n_thresholds,)
    kstest_pval = np.asarray(
        [
            kstest(bin_weighted, bin_repeated).pvalue
            for bin_weighted, bin_repeated in zip(bins_weighted, bins_repeated)
        ]
    )
    # We should not be able to reject the null hypothesis that the two samples
    # come from the same distribution for all bins at level 5% with Bonferroni
    # correction.
    assert np.min(kstest_pval) > 0.05 / len(kstest_pval)


@pytest.mark.parametrize(
    "max_bins, scale, offset",
    [
        (3, 2, -1),
        (42, 1, 0),
        (255, 0.3, 42),
    ],
)
def test_bin_mapper_identity_small(max_bins, scale, offset):
    data = np.arange(max_bins).reshape(-1, 1) * scale + offset
    # max_bins is the number of bins for non-missing values
    n_bins = max_bins + 1
    binned = _BinMapper(n_bins=n_bins).fit_transform(data)
    assert_array_equal(binned, np.arange(max_bins).reshape(-1, 1))


@pytest.mark.parametrize(
    "max_bins_small, max_bins_large",
    [