|
11 | 11 | import numpy as np |
12 | 12 | from matplotlib import pyplot as plt |
13 | 13 |
|
14 | | -from skrub import _dataframe as sbd |
15 | | - |
| 14 | +from .. import _dataframe as sbd |
| 15 | +from .. import _datetime_encoder |
16 | 16 | from . import _utils |
17 | 17 |
|
18 | 18 | __all__ = ["COLORS", "COLOR_0", "histogram", "line", "value_counts"] |
@@ -193,15 +193,40 @@ def _get_range(values, frac=0.2, factor=3.0): |
193 | 193 | return low, high |
194 | 194 |
|
195 | 195 |
|
196 | | -def _robust_hist(values, ax, color): |
| 196 | +def _robust_hist(col, ax=None, color=None): |
| 197 | + col = sbd.drop_nulls(col) |
| 198 | + if sbd.is_float(col): |
| 199 | + # avoid any issues with pandas nullable dtypes |
| 200 | + # (to_numpy can yield a numpy array with object dtype in old pandas |
| 201 | + # version if there are inf or nan) |
| 202 | + col = sbd.to_float32(col) |
| 203 | + values = sbd.to_numpy(col) |
| 204 | + if sbd.is_any_date(col): |
| 205 | + # numpy histogram does not handle datetimes but matplotlib does, so we |
| 206 | + # convert to the total number of seconds since epoch (a float) |
| 207 | + # |
| 208 | + # note that the dtype cannot be duration (timedelta) here as they are |
| 209 | + # handled higher in the call stack and converted to floats with |
| 210 | + # _utils.duration_to_numeric |
| 211 | + np_histogram_values = sbd.to_numpy( |
| 212 | + _datetime_encoder.DatetimeEncoder(resolution=None).fit_transform(col) |
| 213 | + ).ravel() |
| 214 | + else: |
| 215 | + np_histogram_values = values |
197 | 216 | low, high = _get_range(values) |
198 | | - inliers = values[(low <= values) & (values <= high)] |
| 217 | + inlier_mask = (low <= values) & (values <= high) |
199 | 218 | n_low_outliers = (values < low).sum() |
200 | 219 | n_high_outliers = (high < values).sum() |
201 | | - n, bins, patches = ax.hist(inliers) |
| 220 | + result = {"n_low_outliers": n_low_outliers, "n_high_outliers": n_high_outliers} |
| 221 | + result["bin_counts"], result["bin_edges"] = np.histogram( |
| 222 | + np_histogram_values[inlier_mask] |
| 223 | + ) |
| 224 | + if ax is None: |
| 225 | + return result |
| 226 | + n, bins, patches = ax.hist(values[inlier_mask]) |
202 | 227 | n_out = n_low_outliers + n_high_outliers |
203 | 228 | if not n_out: |
204 | | - return 0, 0 |
| 229 | + return result |
205 | 230 | width = bins[1] - bins[0] |
206 | 231 | start, stop = bins[0], bins[-1] |
207 | 232 | line_params = dict(color=_RED, linestyle="--", ymax=0.95) |
@@ -230,28 +255,25 @@ def _robust_hist(values, ax, color): |
230 | 255 | color=_RED, |
231 | 256 | ) |
232 | 257 | ax.set_xlim(start, stop) |
233 | | - return n_low_outliers, n_high_outliers |
| 258 | + return result |
| 259 | + |
| 260 | + |
| 261 | +def histogram_data(col): |
| 262 | + return _robust_hist(col, ax=None, color=None) |
234 | 263 |
|
235 | 264 |
|
236 | 265 | @_plot |
237 | 266 | def histogram(col, duration_unit=None, color=COLOR_0): |
238 | 267 | """Histogram for a numeric column.""" |
239 | | - col = sbd.drop_nulls(col) |
240 | | - if sbd.is_float(col): |
241 | | - # avoid any issues with pandas nullable dtypes |
242 | | - # (to_numpy can yield a numpy array with object dtype in old pandas |
243 | | - # version if there are inf or nan) |
244 | | - col = sbd.to_float32(col) |
245 | | - values = sbd.to_numpy(col) |
246 | 268 | fig, ax = plt.subplots() |
247 | 269 | _despine(ax) |
248 | | - n_low_outliers, n_high_outliers = _robust_hist(values, ax, color=color) |
| 270 | + histogram_data = _robust_hist(col, ax=ax, color=color) |
249 | 271 | if duration_unit is not None: |
250 | 272 | ax.set_xlabel(f"{duration_unit.capitalize()}s") |
251 | 273 | if sbd.is_any_date(col): |
252 | 274 | _rotate_ticklabels(ax) |
253 | 275 | _adjust_fig_size(fig, ax, 2.0, 1.0) |
254 | | - return _serialize(fig), n_low_outliers, n_high_outliers |
| 276 | + return _serialize(fig), histogram_data |
255 | 277 |
|
256 | 278 |
|
257 | 279 | @_plot |
|
0 commit comments