From 637a0253923c64a9f41cb9993887a81a6c4c7aa5 Mon Sep 17 00:00:00 2001 From: Elouen Ginat Date: Fri, 11 Sep 2026 18:55:01 +0200 Subject: [PATCH 1/9] fix(matplotlib): preserve Khiops right-closed bins Shift observations by one ULP before Matplotlib rendering so values on internal boundaries retain their Khiops assignments.\n\nRefs #32 --- src/khisto/matplotlib/hist.py | 13 ++++++++++++- tests/plot/test_matplotlib_histogram.py | 13 +++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/khisto/matplotlib/hist.py b/src/khisto/matplotlib/hist.py index 625baf4..e9d5190 100644 --- a/src/khisto/matplotlib/hist.py +++ b/src/khisto/matplotlib/hist.py @@ -60,6 +60,14 @@ def hist( patches Container with the bar patches. + Notes + ----- + Khiops bins are right-closed, ``(lower, upper]``, while Matplotlib bins are + left-closed, ``[lower, upper)`` (except for its final bin). This function + adjusts observations by one floating-point step before delegating to + Matplotlib so that values on internal bin edges retain their Khiops bin + assignments. The returned bin edges are not modified. + See Also -------- matplotlib.pyplot.hist : Matplotlib's histogram function. @@ -83,4 +91,7 @@ def hist( ax = plt.gca() - return ax.hist(x, bin_edges, density=density, range=range, **kwargs) + # Khiops bins are right-closed, whereas Matplotlib bins are left-closed. + # Moving each value down one ULP preserves Khiops assignments at shared edges. + plot_values = np.nextafter(np.asarray(x, dtype=np.float64), -np.inf) + return ax.hist(plot_values, bin_edges, density=density, range=range, **kwargs) diff --git a/tests/plot/test_matplotlib_histogram.py b/tests/plot/test_matplotlib_histogram.py index 82c6214..5c3fb40 100644 --- a/tests/plot/test_matplotlib_histogram.py +++ b/tests/plot/test_matplotlib_histogram.py @@ -66,6 +66,19 @@ def test_frequency_histogram(self, normal_data): assert np.sum(n) == len(normal_data) plt.close(fig) + @pytest.mark.parametrize("density", [False, True]) + def test_histogram_matches_khiops_at_internal_edges(self, density): + """Test that observations on internal edges keep their Khiops bins.""" + data = np.repeat([1, 2, 3, 4, 5, 6], [458, 82, 43, 11, 3, 2]) + expected, expected_bins = histogram(data, max_bins=100, density=density) + fig, ax = plt.subplots() + + values, bins, _ = hist(data, max_bins=100, density=density, ax=ax) + + np.testing.assert_array_equal(bins, expected_bins) + np.testing.assert_allclose(values, expected) + plt.close(fig) + def test_horizontal_orientation(self, normal_data): """Test horizontal histogram.""" fig, ax = plt.subplots() From f915c2f90868c9eb72191597ff24cc8bbe9d2de0 Mon Sep 17 00:00:00 2001 From: Elouen Ginat Date: Mon, 14 Sep 2026 15:33:59 +0200 Subject: [PATCH 2/9] =?UTF-8?q?fix(histogram):=20limiter=20la=20granularit?= =?UTF-8?q?=C3=A9=20au=20meilleur=20mod=C3=A8le?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/khisto/core/backend.py | 8 +++++++ src/khisto/histogram.py | 43 ++++++++++++++++++++++------------- tests/array/test_histogram.py | 29 +++++++++++++++++++++++ 3 files changed, 64 insertions(+), 16 deletions(-) diff --git a/src/khisto/core/backend.py b/src/khisto/core/backend.py index 1e50bef..d194ae7 100644 --- a/src/khisto/core/backend.py +++ b/src/khisto/core/backend.py @@ -105,6 +105,10 @@ def from_dict(cls, data: dict[str, Any]) -> _KhistoOutput: class HistogramResult: """Result of optimal histogram computation. + .. warning:: + Bins are right-closed: a value on an internal edge belongs to the bin + on its left, unlike with NumPy histograms. + Attributes ---------- lower_bounds : NDArray[np.float64] @@ -238,6 +242,10 @@ def _process_histogram_file(file_path: Path) -> list[HistogramResult]: def compute_histograms(x: NDArray[np.float64]) -> list[HistogramResult]: """Compute optimal histogram of an array using khisto CLI binary input. + .. warning:: + Bins are right-closed: a value on an internal edge belongs to the bin + on its left, unlike with NumPy histograms. + Parameters ---------- x : NDArray[np.float64] diff --git a/src/khisto/histogram.py b/src/khisto/histogram.py index 4290983..1a933ca 100644 --- a/src/khisto/histogram.py +++ b/src/khisto/histogram.py @@ -30,22 +30,29 @@ def _select_histogram( HistogramResult The selected histogram result. """ - if max_bins is not None: - # Find the finest granularity that respects max_bins - for r in reversed(histogram_results): - if len(r) <= max_bins: - return r - # If no histogram respects the constraint, use the coarsest one - return histogram_results[0] - - # Return the best histogram (optimal in terms of interpretability) - # There is only one best histogram, so we return the first one we find - for r in reversed(histogram_results): - if r.is_best: - return r - # Fallback to finest granularity if no best is marked - # It is assumed to be the best because it is the finest granularity - return histogram_results[-1] + + # Find the best histogram marked as is_best, + # or default to the last one if none is marked. + best_histogram = next( + (result for result in reversed(histogram_results) if result.is_best), + histogram_results[-1], + ) + + if max_bins is None: + return best_histogram + + # Select the histogram with the highest granularity + # that does not exceed max_bins. + # Histograms finer than the best interpretable one are skipped. + return next( + ( + result + for result in reversed(histogram_results) + if result.granularity <= best_histogram.granularity + and len(result) <= max_bins + ), + histogram_results[0], + ) def histogram( @@ -56,6 +63,10 @@ def histogram( ) -> tuple[NDArray[np.float64], NDArray[np.float64]]: """Compute an optimal histogram using the Khiops binning algorithm. + .. warning:: + Bins are right-closed: a value on an internal edge belongs to the bin + on its left, unlike with NumPy histograms. + Parameters ---------- a : array_like diff --git a/tests/array/test_histogram.py b/tests/array/test_histogram.py index 8205883..9d1b3e5 100644 --- a/tests/array/test_histogram.py +++ b/tests/array/test_histogram.py @@ -10,6 +10,23 @@ import pytest from khisto import histogram +from khisto.core import HistogramResult +from khisto.histogram import _select_histogram + + +def _histogram_result( + n_bins: int, granularity: int, *, is_best: bool = False +) -> HistogramResult: + edges = np.arange(n_bins + 1, dtype=np.float64) + return HistogramResult( + lower_bounds=edges[:-1], + upper_bounds=edges[1:], + frequencies=np.ones(n_bins, dtype=np.int64), + probabilities=np.full(n_bins, 1 / n_bins), + densities=np.full(n_bins, 1 / n_bins), + is_best=is_best, + granularity=granularity, + ) # Test data fixtures @@ -45,6 +62,18 @@ def bimodal_data(): class TestHistogram: """Test cases for histogram function.""" + def test_max_bins_does_not_select_finer_than_best(self): + """Test that max_bins cannot select a granularity after the best one.""" + results = [ + _histogram_result(1, granularity=1), + _histogram_result(3, granularity=2, is_best=True), + _histogram_result(4, granularity=3), + ] + + selected = _select_histogram(results, max_bins=10) + + assert selected is results[1] + def test_histogram_with_list(self, simple_data): """Test histogram with Python list input.""" hist, bin_edges = histogram(simple_data) From 07dbe58689bd0a4c9334eda8254aeb704fbce2e0 Mon Sep 17 00:00:00 2001 From: Elouen Ginat Date: Mon, 14 Sep 2026 17:24:13 +0200 Subject: [PATCH 3/9] =?UTF-8?q?docs(api):=20pr=C3=A9ciser=20la=20fermeture?= =?UTF-8?q?=20des=20bins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/khisto/core/backend.py | 8 ++++---- src/khisto/histogram.py | 4 ++-- src/khisto/matplotlib/hist.py | 11 ++++------- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/khisto/core/backend.py b/src/khisto/core/backend.py index d194ae7..6c3f2a6 100644 --- a/src/khisto/core/backend.py +++ b/src/khisto/core/backend.py @@ -106,8 +106,8 @@ class HistogramResult: """Result of optimal histogram computation. .. warning:: - Bins are right-closed: a value on an internal edge belongs to the bin - on its left, unlike with NumPy histograms. + Khiops bins are right-closed, ``(lower, upper]``, unlike Numpy bins which + are left-closed, ``[lower, upper)``. Attributes ---------- @@ -243,8 +243,8 @@ def compute_histograms(x: NDArray[np.float64]) -> list[HistogramResult]: """Compute optimal histogram of an array using khisto CLI binary input. .. warning:: - Bins are right-closed: a value on an internal edge belongs to the bin - on its left, unlike with NumPy histograms. + Khiops bins are right-closed, ``(lower, upper]``, unlike Numpy bins which + are left-closed, ``[lower, upper)``. Parameters ---------- diff --git a/src/khisto/histogram.py b/src/khisto/histogram.py index 1a933ca..c4970e5 100644 --- a/src/khisto/histogram.py +++ b/src/khisto/histogram.py @@ -64,8 +64,8 @@ def histogram( """Compute an optimal histogram using the Khiops binning algorithm. .. warning:: - Bins are right-closed: a value on an internal edge belongs to the bin - on its left, unlike with NumPy histograms. + Khiops bins are right-closed, ``(lower, upper]``, unlike Numpy bins which + are left-closed, ``[lower, upper)``. Parameters ---------- diff --git a/src/khisto/matplotlib/hist.py b/src/khisto/matplotlib/hist.py index e9d5190..4d23d84 100644 --- a/src/khisto/matplotlib/hist.py +++ b/src/khisto/matplotlib/hist.py @@ -60,13 +60,10 @@ def hist( patches Container with the bar patches. - Notes - ----- - Khiops bins are right-closed, ``(lower, upper]``, while Matplotlib bins are - left-closed, ``[lower, upper)`` (except for its final bin). This function - adjusts observations by one floating-point step before delegating to - Matplotlib so that values on internal bin edges retain their Khiops bin - assignments. The returned bin edges are not modified. + .. warning:: + Khiops bins are right-closed, ``(lower, upper]``, unlike Matplotlib bins + which are left-closed, ``[lower, upper)``, but the displayed plot is still + correct. See Also -------- From 6433b0ce0f70df984dbf889c2290c9861ce38c85 Mon Sep 17 00:00:00 2001 From: Elouen Ginat Date: Tue, 15 Sep 2026 09:31:15 +0200 Subject: [PATCH 4/9] =?UTF-8?q?docs(api):=20pr=C3=A9senter=20les=20interva?= =?UTF-8?q?lles=20sous=20forme=20de=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/khisto/core/backend.py | 4 ++-- src/khisto/histogram.py | 2 +- src/khisto/matplotlib/hist.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/khisto/core/backend.py b/src/khisto/core/backend.py index 6c3f2a6..b40217e 100644 --- a/src/khisto/core/backend.py +++ b/src/khisto/core/backend.py @@ -105,7 +105,7 @@ def from_dict(cls, data: dict[str, Any]) -> _KhistoOutput: class HistogramResult: """Result of optimal histogram computation. - .. warning:: + .. note:: Khiops bins are right-closed, ``(lower, upper]``, unlike Numpy bins which are left-closed, ``[lower, upper)``. @@ -242,7 +242,7 @@ def _process_histogram_file(file_path: Path) -> list[HistogramResult]: def compute_histograms(x: NDArray[np.float64]) -> list[HistogramResult]: """Compute optimal histogram of an array using khisto CLI binary input. - .. warning:: + .. note:: Khiops bins are right-closed, ``(lower, upper]``, unlike Numpy bins which are left-closed, ``[lower, upper)``. diff --git a/src/khisto/histogram.py b/src/khisto/histogram.py index c4970e5..cc47e51 100644 --- a/src/khisto/histogram.py +++ b/src/khisto/histogram.py @@ -63,7 +63,7 @@ def histogram( ) -> tuple[NDArray[np.float64], NDArray[np.float64]]: """Compute an optimal histogram using the Khiops binning algorithm. - .. warning:: + .. note:: Khiops bins are right-closed, ``(lower, upper]``, unlike Numpy bins which are left-closed, ``[lower, upper)``. diff --git a/src/khisto/matplotlib/hist.py b/src/khisto/matplotlib/hist.py index 4d23d84..02aba95 100644 --- a/src/khisto/matplotlib/hist.py +++ b/src/khisto/matplotlib/hist.py @@ -60,7 +60,7 @@ def hist( patches Container with the bar patches. - .. warning:: + .. note:: Khiops bins are right-closed, ``(lower, upper]``, unlike Matplotlib bins which are left-closed, ``[lower, upper)``, but the displayed plot is still correct. From 04ba3527d5321321b393a04170ecfa74c71f2674 Mon Sep 17 00:00:00 2001 From: Elouen Ginat Date: Tue, 15 Sep 2026 09:53:13 +0200 Subject: [PATCH 5/9] =?UTF-8?q?docs(histogramme):=20pr=C3=A9ciser=20le=20c?= =?UTF-8?q?alcul=20de=20la=20proportion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/counts_vs_density.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/counts_vs_density.rst b/docs/counts_vs_density.rst index a0cbf6a..09d743e 100644 --- a/docs/counts_vs_density.rst +++ b/docs/counts_vs_density.rst @@ -23,5 +23,5 @@ Reading variable-width histograms :widths: 35, 35, 30 "How many observations?", "Counts (``density=False``)", "Bar heights" - "What fraction of observations?", "Counts divided by :math:`N`, or density", "Bar areas" + "What fraction of observations?", "Counts divided by :math:`N`, or density times bin width", "Bar areas" "Where are values concentrated?", "Density (``density=True``, the Khisto default)", "Bar heights" \ No newline at end of file From 89e68cdff064e58939608fa92623217575177602 Mon Sep 17 00:00:00 2001 From: Elouen Ginat Date: Tue, 15 Sep 2026 10:43:20 +0200 Subject: [PATCH 6/9] docs(api): clarifier la fermeture des intervalles --- src/khisto/core/backend.py | 8 ++++---- src/khisto/histogram.py | 4 ++-- src/khisto/matplotlib/hist.py | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/khisto/core/backend.py b/src/khisto/core/backend.py index b40217e..d3aac25 100644 --- a/src/khisto/core/backend.py +++ b/src/khisto/core/backend.py @@ -106,8 +106,8 @@ class HistogramResult: """Result of optimal histogram computation. .. note:: - Khiops bins are right-closed, ``(lower, upper]``, unlike Numpy bins which - are left-closed, ``[lower, upper)``. + Khiops bins are left-open and right-closed, ``(lower, upper]``, unlike + NumPy bins which are left-closed and right-open, ``[lower, upper)``. Attributes ---------- @@ -243,8 +243,8 @@ def compute_histograms(x: NDArray[np.float64]) -> list[HistogramResult]: """Compute optimal histogram of an array using khisto CLI binary input. .. note:: - Khiops bins are right-closed, ``(lower, upper]``, unlike Numpy bins which - are left-closed, ``[lower, upper)``. + Khiops bins are left-open and right-closed, ``(lower, upper]``, unlike + NumPy bins which are left-closed and right-open, ``[lower, upper)``. Parameters ---------- diff --git a/src/khisto/histogram.py b/src/khisto/histogram.py index cc47e51..1cb1bcb 100644 --- a/src/khisto/histogram.py +++ b/src/khisto/histogram.py @@ -64,8 +64,8 @@ def histogram( """Compute an optimal histogram using the Khiops binning algorithm. .. note:: - Khiops bins are right-closed, ``(lower, upper]``, unlike Numpy bins which - are left-closed, ``[lower, upper)``. + Khiops bins are left-open and right-closed, ``(lower, upper]``, unlike + NumPy bins which are left-closed and right-open, ``[lower, upper)``. Parameters ---------- diff --git a/src/khisto/matplotlib/hist.py b/src/khisto/matplotlib/hist.py index 02aba95..96dcb40 100644 --- a/src/khisto/matplotlib/hist.py +++ b/src/khisto/matplotlib/hist.py @@ -61,9 +61,9 @@ def hist( Container with the bar patches. .. note:: - Khiops bins are right-closed, ``(lower, upper]``, unlike Matplotlib bins - which are left-closed, ``[lower, upper)``, but the displayed plot is still - correct. + Khiops bins are left-open and right-closed, ``(lower, upper]``, unlike + Matplotlib bins which are left-closed and right-open, ``[lower, upper)``. + The displayed plot is still correct. See Also -------- From 22404f857f09fa65d83d06b0d5ab9ceabe76aa75 Mon Sep 17 00:00:00 2001 From: Elouen Ginat Date: Tue, 15 Sep 2026 12:26:09 +0200 Subject: [PATCH 7/9] refactor(histogram): appliquer les retours de revue --- src/khisto/histogram.py | 24 +++++------ tests/array/test_histogram.py | 32 ++++----------- tests/plot/test_matplotlib_histogram.py | 54 ++++++++++--------------- 3 files changed, 41 insertions(+), 69 deletions(-) diff --git a/src/khisto/histogram.py b/src/khisto/histogram.py index 1cb1bcb..e3e93e7 100644 --- a/src/khisto/histogram.py +++ b/src/khisto/histogram.py @@ -33,10 +33,12 @@ def _select_histogram( # Find the best histogram marked as is_best, # or default to the last one if none is marked. - best_histogram = next( - (result for result in reversed(histogram_results) if result.is_best), - histogram_results[-1], - ) + for result in reversed(histogram_results): + if result.is_best: + best_histogram = result + break + else: + best_histogram = histogram_results[-1] if max_bins is None: return best_histogram @@ -44,15 +46,11 @@ def _select_histogram( # Select the histogram with the highest granularity # that does not exceed max_bins. # Histograms finer than the best interpretable one are skipped. - return next( - ( - result - for result in reversed(histogram_results) - if result.granularity <= best_histogram.granularity - and len(result) <= max_bins - ), - histogram_results[0], - ) + for result in reversed(histogram_results): + if result.granularity <= best_histogram.granularity and len(result) <= max_bins: + return result + + return histogram_results[0] def histogram( diff --git a/tests/array/test_histogram.py b/tests/array/test_histogram.py index 9d1b3e5..6b56a76 100644 --- a/tests/array/test_histogram.py +++ b/tests/array/test_histogram.py @@ -10,23 +10,6 @@ import pytest from khisto import histogram -from khisto.core import HistogramResult -from khisto.histogram import _select_histogram - - -def _histogram_result( - n_bins: int, granularity: int, *, is_best: bool = False -) -> HistogramResult: - edges = np.arange(n_bins + 1, dtype=np.float64) - return HistogramResult( - lower_bounds=edges[:-1], - upper_bounds=edges[1:], - frequencies=np.ones(n_bins, dtype=np.int64), - probabilities=np.full(n_bins, 1 / n_bins), - densities=np.full(n_bins, 1 / n_bins), - is_best=is_best, - granularity=granularity, - ) # Test data fixtures @@ -64,15 +47,16 @@ class TestHistogram: def test_max_bins_does_not_select_finer_than_best(self): """Test that max_bins cannot select a granularity after the best one.""" - results = [ - _histogram_result(1, granularity=1), - _histogram_result(3, granularity=2, is_best=True), - _histogram_result(4, granularity=3), - ] + # This distribution produces histograms finer than the best one. + data = np.repeat([1, 2, 3, 4, 5, 6], [458, 82, 43, 11, 3, 2]) + expected_values, expected_edges = histogram(data, density=False) - selected = _select_histogram(results, max_bins=10) + # A loose bin limit must still select the best interpretable histogram. + values, edges = histogram(data, max_bins=100, density=False) - assert selected is results[1] + # The limit must not expose a finer histogram after the best one. + np.testing.assert_array_equal(values, expected_values) + np.testing.assert_array_equal(edges, expected_edges) def test_histogram_with_list(self, simple_data): """Test histogram with Python list input.""" diff --git a/tests/plot/test_matplotlib_histogram.py b/tests/plot/test_matplotlib_histogram.py index 5c3fb40..eca0bf1 100644 --- a/tests/plot/test_matplotlib_histogram.py +++ b/tests/plot/test_matplotlib_histogram.py @@ -16,6 +16,13 @@ from khisto.matplotlib import hist +@pytest.fixture(autouse=True) +def close_figures(): + """Close every figure, including when a test fails.""" + yield + plt.close("all") + + class TestHistBasic: """Test basic hist functionality.""" @@ -27,7 +34,7 @@ def normal_data(self): def test_simple_array(self, normal_data): """Test hist with simple numpy array.""" - fig, ax = plt.subplots() + _fig, ax = plt.subplots() n, bins, patches = hist(normal_data, ax=ax) assert isinstance(n, np.ndarray) @@ -35,7 +42,6 @@ def test_simple_array(self, normal_data): assert patches is not None assert len(n) > 0 assert len(bins) == len(n) + 1 - plt.close(fig) def test_without_ax(self, normal_data): """Test hist without explicit ax parameter.""" @@ -44,135 +50,119 @@ def test_without_ax(self, normal_data): assert isinstance(n, np.ndarray) assert isinstance(bins, np.ndarray) assert len(n) > 0 - plt.close() def test_density_histogram(self, normal_data): """Test density histogram.""" - fig, ax = plt.subplots() + _fig, ax = plt.subplots() n, bins, _ = hist(normal_data, density=True, ax=ax) # Density should integrate to 1 bin_widths = np.diff(bins) total = np.sum(n * bin_widths) assert np.isclose(total, 1.0, rtol=1e-5) - plt.close(fig) def test_frequency_histogram(self, normal_data): """Test frequency histogram is the default behavior.""" - fig, ax = plt.subplots() + _fig, ax = plt.subplots() n, _, _ = hist(normal_data, ax=ax, density=False) # Frequencies should sum to total count assert np.sum(n) == len(normal_data) - plt.close(fig) @pytest.mark.parametrize("density", [False, True]) def test_histogram_matches_khiops_at_internal_edges(self, density): """Test that observations on internal edges keep their Khiops bins.""" data = np.repeat([1, 2, 3, 4, 5, 6], [458, 82, 43, 11, 3, 2]) expected, expected_bins = histogram(data, max_bins=100, density=density) - fig, ax = plt.subplots() + _fig, ax = plt.subplots() values, bins, _ = hist(data, max_bins=100, density=density, ax=ax) np.testing.assert_array_equal(bins, expected_bins) np.testing.assert_allclose(values, expected) - plt.close(fig) def test_horizontal_orientation(self, normal_data): """Test horizontal histogram.""" - fig, ax = plt.subplots() + _fig, ax = plt.subplots() n, _, _ = hist(normal_data, orientation="horizontal", ax=ax) assert isinstance(n, np.ndarray) assert len(n) > 0 - plt.close(fig) def test_with_max_bins(self, normal_data): """Test histogram with max_bins parameter.""" - fig, ax = plt.subplots() + _fig, ax = plt.subplots() n, _, _ = hist(normal_data, max_bins=5, ax=ax) assert len(n) <= 5 - plt.close(fig) def test_with_range(self, normal_data): """Test histogram with range parameter.""" - fig, ax = plt.subplots() + _fig, ax = plt.subplots() _, bins, _ = hist(normal_data, range=(-1, 1), ax=ax) assert bins[0] >= -1 assert bins[-1] <= 1 - plt.close(fig) def test_log_scale(self, normal_data): """Test histogram with log scale.""" - fig, ax = plt.subplots() + _fig, ax = plt.subplots() hist(normal_data, log=True, ax=ax) assert ax.get_yscale() == "log" - plt.close(fig) def test_color_parameter(self, normal_data): """Test histogram with color parameter.""" - fig, ax = plt.subplots() + _fig, ax = plt.subplots() _, _, patches = hist(normal_data, color="red", ax=ax) assert patches is not None - plt.close(fig) def test_step_histtype(self, normal_data): """Test histogram with step histtype.""" - fig, ax = plt.subplots() + _fig, ax = plt.subplots() n, _, _ = hist(normal_data, histtype="step", ax=ax) assert isinstance(n, np.ndarray) - plt.close(fig) def test_stepfilled_histtype(self, normal_data): """Test histogram with stepfilled histtype.""" - fig, ax = plt.subplots() + _fig, ax = plt.subplots() n, _, _ = hist(normal_data, histtype="stepfilled", ax=ax) assert isinstance(n, np.ndarray) - plt.close(fig) def test_cumulative_density_histogram(self, normal_data): """Test cumulative density histogram.""" - fig, ax = plt.subplots() + _fig, ax = plt.subplots() n, _, _ = hist(normal_data, density=True, cumulative=True, ax=ax) assert np.isclose(n[-1], 1.0, rtol=1e-5) - plt.close(fig) def test_cumulative_hist_matches_array_api(self, normal_data): """Test that the plotting wrapper matches cumulative histogram values.""" - fig, ax = plt.subplots() + _fig, ax = plt.subplots() n, bins, _ = hist(normal_data, density=True, cumulative=True, ax=ax) density, expected_bins = histogram(normal_data, density=True) expected = np.cumsum(density * np.diff(expected_bins)) np.testing.assert_array_equal(bins, expected_bins) np.testing.assert_allclose(n, expected) - plt.close(fig) def test_reverse_cumulative_frequency_histogram(self, normal_data): """Test reverse cumulative frequency histogram.""" - fig, ax = plt.subplots() + _fig, ax = plt.subplots() n, _, _ = hist(normal_data, density=False, cumulative=-1, ax=ax) assert np.isclose(n[0], len(normal_data)) - plt.close(fig) def test_unsupported_bins_parameter(self, normal_data): """Test that bins raises a clear error message.""" - fig, ax = plt.subplots() + _fig, ax = plt.subplots() with pytest.raises(TypeError, match="bins is not supported"): hist(normal_data, bins=10, ax=ax) - plt.close(fig) - class TestHistReturnValues: """Test return values match matplotlib.pyplot.hist interface.""" From 7b176c1b86182ebac8345c69f17cfe802105dd30 Mon Sep 17 00:00:00 2001 From: Elouen Ginat Date: Tue, 15 Sep 2026 12:26:19 +0200 Subject: [PATCH 8/9] =?UTF-8?q?chore(build):=20r=C3=A9utiliser=20le=20cach?= =?UTF-8?q?e=20CMake=20avec=20uv?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 2479c45..372ba0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -129,6 +129,7 @@ ignore = [ ] [tool.scikit-build] +build-dir = "build/{wheel_tag}" # Reuse CMake and Ninja artifacts across uv sync runs wheel.py-api = "py3" # We specify full compatibility with all Python 3 releases cmake.version = "CMakeLists.txt" # Read CMake minimal version from file From f13fe240864f14c0d09f10fd75abfb8ecb4f4fc7 Mon Sep 17 00:00:00 2001 From: Elouen Ginat Date: Tue, 15 Sep 2026 13:10:13 +0200 Subject: [PATCH 9/9] =?UTF-8?q?chore(release):=20pr=C3=A9parer=20la=20vers?= =?UTF-8?q?ion=201.0.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 21 +++++++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 663d9d4..6ad6cb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ All notable changes to Khisto are documented in this file. +## [1.0.2] - 2026-09-15 + +### Added + +- Explain counts, densities, and variable-width bins in a dedicated guide. + +### Fixed + +- Correct Matplotlib plots for observations exactly on internal bin edges. Khiops + assigns these values to right-closed bins, `(lower, upper]`, while Matplotlib's + default left-closed convention, `[lower, upper)`, previously shifted them into + the next bin and produced incorrect counts and densities. +- Prevent `max_bins` from selecting a histogram finer than the best interpretable + one. + +### Changed + +- Highlight and update links to the official Khiops histogram documentation. +- Clarify the bin-boundary semantics in the array and Matplotlib API documentation. + ## [1.0.1] - 2026-09-07 ### Fixed @@ -20,4 +40,5 @@ First stable release. - Prebuilt wheels for Linux, macOS, and Windows. - Public API documentation and usage examples. +[1.0.2]: https://github.com/KhiopsML/khisto-python/compare/v1.0.1...v1.0.2 [1.0.1]: https://github.com/KhiopsML/khisto-python/compare/v1.0.0...v1.0.1 diff --git a/pyproject.toml b/pyproject.toml index 372ba0e..869c308 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "khisto" -version = "1.0.1" +version = "1.0.2" description = "Optimal histogram visualization using the Khiops algorithm" readme = "README.md" license = "BSD-3-Clause-Clear"