-
Notifications
You must be signed in to change notification settings - Fork 0
fix(matplotlib): corriger le rendu des histogrammes adaptatifs #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7e4549f
13830b5
f7c14d4
63afdbd
731d2d4
aa94a3b
2355d70
c40fc12
fbdb9d3
89f47ab
a9e7844
6f687db
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,26 +6,75 @@ | |
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING, Any | ||
| from typing import TYPE_CHECKING, Any, Literal, overload | ||
|
|
||
| import numpy as np | ||
| from matplotlib.axes import Axes | ||
|
|
||
| from khisto.histogram import histogram as khisto_histogram | ||
|
|
||
| if TYPE_CHECKING: | ||
| from numpy.typing import ArrayLike | ||
| from matplotlib.axes import Axes | ||
| from matplotlib.container import BarContainer | ||
| from matplotlib.patches import Polygon | ||
| from numpy.typing import ArrayLike, NDArray | ||
|
|
||
|
|
||
| @overload | ||
| def hist( | ||
| x: ArrayLike, | ||
| range: tuple[float, float] | None = None, | ||
| max_bins: int | None = None, | ||
| density: bool = True, | ||
| *, | ||
| ax: Axes | None = None, | ||
| histtype: Literal["bar"] = "bar", | ||
| **kwargs: Any, | ||
| ) -> tuple[np.ndarray, np.ndarray, Any]: | ||
| ) -> tuple[NDArray[np.float64], NDArray[np.float64], BarContainer]: ... | ||
|
|
||
|
|
||
| @overload | ||
| def hist( | ||
| x: ArrayLike, | ||
| range: tuple[float, float] | None = None, | ||
| max_bins: int | None = None, | ||
| density: bool = True, | ||
| *, | ||
| ax: Axes | None = None, | ||
| histtype: Literal["step", "stepfilled"], | ||
| **kwargs: Any, | ||
| ) -> tuple[NDArray[np.float64], NDArray[np.float64], list[Polygon]]: ... | ||
|
|
||
|
|
||
| @overload | ||
| def hist( | ||
| x: ArrayLike, | ||
| range: tuple[float, float] | None = None, | ||
| max_bins: int | None = None, | ||
| density: bool = True, | ||
| *, | ||
| ax: Axes | None = None, | ||
| histtype: str, | ||
| **kwargs: Any, | ||
| ) -> tuple[ | ||
| NDArray[np.float64], | ||
| NDArray[np.float64], | ||
| BarContainer | list[Polygon], | ||
| ]: ... | ||
|
|
||
|
|
||
| def hist( | ||
| x: ArrayLike, | ||
| range: tuple[float, float] | None = None, | ||
| max_bins: int | None = None, | ||
| density: bool = True, | ||
| *, | ||
| ax: Axes | None = None, | ||
| **kwargs: Any, | ||
| ) -> tuple[ | ||
| NDArray[np.float64], | ||
| NDArray[np.float64], | ||
| BarContainer | list[Polygon], | ||
| ]: | ||
| """Compute and plot an optimal histogram. | ||
|
|
||
| Parameters | ||
|
|
@@ -49,7 +98,8 @@ def hist( | |
| Axes object to plot on. If not provided, the current axes will be used. | ||
| **kwargs : | ||
| other keyword arguments are described in ``matplotlib.pyplot.hist``. The ``bins``, | ||
| ``weights``, and stacked/multiple dataset features are not supported. | ||
| ``weights``, ``stacked``, ``histtype="barstacked"``, and multiple dataset | ||
| features are not supported. | ||
|
|
||
| Returns | ||
| ------- | ||
|
|
@@ -58,7 +108,7 @@ def hist( | |
| bins : ndarray | ||
| Bin edges. | ||
| patches | ||
| Container with the bar patches. | ||
| Container with the bar patches, or a list containing the step polygon. | ||
|
|
||
| .. note:: | ||
| Khiops bins are left-open and right-closed, ``(lower, upper]``, unlike | ||
|
|
@@ -70,6 +120,11 @@ def hist( | |
| matplotlib.pyplot.hist : Matplotlib's histogram function. | ||
| khisto.histogram : Underlying histogram computation. | ||
| """ | ||
| # optional dependency; only import if strictly needed. | ||
| import matplotlib.pyplot as plt | ||
| from matplotlib.container import BarContainer | ||
| from matplotlib.patches import Polygon | ||
|
|
||
| unsupported_kwargs = { | ||
| "bins": "Use max_bins to limit the number of bins.", | ||
| "stacked": "Stacked histograms are not supported.", | ||
|
|
@@ -79,16 +134,51 @@ def hist( | |
| if name in kwargs: | ||
| raise TypeError(f"{name} is not supported. {hint}") | ||
|
|
||
| # Compute histogram using khisto | ||
| _, bin_edges = khisto_histogram(x, range=range, max_bins=max_bins, density=density) | ||
| histtype = kwargs.get("histtype", "bar") | ||
| if histtype == "barstacked": | ||
| raise ValueError( | ||
| "histtype='barstacked' is not supported. Khisto only accepts a single dataset." | ||
| ) | ||
|
|
||
| if ax is None: | ||
| # optional dependency; only import if strictly needed. | ||
| import matplotlib.pyplot as plt | ||
| # Use frequencies so Matplotlib applies density and cumulative only once. | ||
| frequencies, bin_edges = khisto_histogram( | ||
| x, | ||
| range=range, | ||
| max_bins=max_bins, | ||
| density=False, | ||
| ) | ||
|
|
||
| if ax is None: | ||
| ax = plt.gca() | ||
|
|
||
| # 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) | ||
| # Weighted left edges preserve Khiops' right-closed bins and [-1e100, 1e100] | ||
| # clamping when Matplotlib renders its left-closed bins. | ||
| cumulative = kwargs.get("cumulative", False) | ||
| plot_weights = ( | ||
| frequencies / frequencies.sum() if density and cumulative else frequencies | ||
| ) | ||
| values, edges, patches = ax.hist( | ||
| x=bin_edges[:-1], | ||
| bins=bin_edges.tolist(), | ||
| weights=plot_weights, | ||
| density=density and not cumulative, | ||
| **kwargs, | ||
| ) | ||
| if isinstance(values, list): | ||
| raise TypeError("Matplotlib unexpectedly returned multiple histograms.") | ||
| if isinstance(patches, BarContainer): | ||
| histogram_patches: BarContainer | list[Polygon] = patches | ||
| elif isinstance(patches, list): | ||
| histogram_patches = [patch for patch in patches if isinstance(patch, Polygon)] | ||
| if len(histogram_patches) != len(patches): | ||
| raise TypeError("Matplotlib returned unexpected histogram patches.") | ||
| else: | ||
| raise TypeError("Matplotlib returned unexpected histogram patches.") | ||
|
|
||
| if histtype == "bar" and not {"edgecolor", "ec"} & kwargs.keys(): | ||
| if not isinstance(histogram_patches, BarContainer): | ||
| raise TypeError("Matplotlib unexpectedly returned non-bar patches.") | ||
| for patch in histogram_patches.patches: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Selon ma compréhension, tu forces un
Davantage commenter le code: sans le contexte, on ne comprend pas l'intention.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oui, il y a des parametrages qui feront qu'on ne verra plus les bin fines. Mais là je n'ai pas vraiment de solution...
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Le changement de edge color est effectivement fait après le ax.hist. Ce qui s'affiche à l'écran est l'objet retourné par ax.hist, ce n'ai pas ax.hist qui affiche. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok: merci pour l'éclaircissement. |
||
| patch.set_edgecolor(patch.get_facecolor()) | ||
|
|
||
| return values, edges, histogram_patches | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would put this Python snippet in a separate script for easier maintenance.