Skip to content

Commit a5e292f

Browse files
authored
include bin counts and edges in tablereport json output (#2164)
1 parent c40c6a6 commit a5e292f

8 files changed

Lines changed: 69 additions & 36 deletions

File tree

CHANGES.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ New Features
4040
Additionally, negative numbers indicated with parentheses can be converted to the
4141
regular numeric format (``(432)`` becomes ``-432``). :pr:`1772` by :user:`Gabriela
4242
Gómez Jiménez <gabrielapgomezji>`.
43+
- :meth:`TableReport.json` now includes histogram data for numeric and datetime
44+
columns (the bin count and edges, and numbers of low and high outliers). Now
45+
``json()`` contains all the information shown in the report html rendering,
46+
including the plots. :pr:`2164` by :user:`Jérôme Dockès <jeromedockes>`.
4347

4448
Changes
4549
-------

skrub/_reporting/_data/templates/column-summary.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
data-name-repr="{{ column.name.__repr__() }}"
55
data-column-name="{{ column.name }}"
66
data-column-idx="{{ column.idx }}"
7-
{% if column['n_low_outliers'] %}
7+
{% if column['histogram_data'] and column['histogram_data']['n_low_outliers'] %}
88
data-has-low-outliers
99
{% endif %}
10-
{% if column['n_high_outliers'] %}
10+
{% if column['histogram_data'] and column['histogram_data']['n_high_outliers'] %}
1111
data-has-high-outliers
1212
{% endif %}
1313
data-manager="FilterableColumn {% if in_sample_tab %}SampleColumnSummary{% endif %}"

skrub/_reporting/_plotting.py

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
import numpy as np
1212
from matplotlib import pyplot as plt
1313

14-
from skrub import _dataframe as sbd
15-
14+
from .. import _dataframe as sbd
15+
from .. import _datetime_encoder
1616
from . import _utils
1717

1818
__all__ = ["COLORS", "COLOR_0", "histogram", "line", "value_counts"]
@@ -193,15 +193,40 @@ def _get_range(values, frac=0.2, factor=3.0):
193193
return low, high
194194

195195

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
197216
low, high = _get_range(values)
198-
inliers = values[(low <= values) & (values <= high)]
217+
inlier_mask = (low <= values) & (values <= high)
199218
n_low_outliers = (values < low).sum()
200219
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])
202227
n_out = n_low_outliers + n_high_outliers
203228
if not n_out:
204-
return 0, 0
229+
return result
205230
width = bins[1] - bins[0]
206231
start, stop = bins[0], bins[-1]
207232
line_params = dict(color=_RED, linestyle="--", ymax=0.95)
@@ -230,28 +255,25 @@ def _robust_hist(values, ax, color):
230255
color=_RED,
231256
)
232257
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)
234263

235264

236265
@_plot
237266
def histogram(col, duration_unit=None, color=COLOR_0):
238267
"""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)
246268
fig, ax = plt.subplots()
247269
_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)
249271
if duration_unit is not None:
250272
ax.set_xlabel(f"{duration_unit.capitalize()}s")
251273
if sbd.is_any_date(col):
252274
_rotate_ticklabels(ax)
253275
_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
255277

256278

257279
@_plot

skrub/_reporting/_summarize.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -255,9 +255,12 @@ def _add_datetime_summary(summary, column, with_plots):
255255
if with_plots:
256256
(
257257
summary["histogram_plot"],
258-
summary["n_low_outliers"],
259-
summary["n_high_outliers"],
258+
summary["histogram_data"],
260259
) = _plotting.histogram(column, color=_plotting.COLORS[0])
260+
else:
261+
# besides the plots, the bin counts and edges are always stored and
262+
# available in the json output.
263+
summary["histogram_data"] = _plotting.histogram_data(column)
261264

262265

263266
def _add_numeric_summary(
@@ -289,13 +292,12 @@ def _add_numeric_summary(
289292
summary["value_is_constant"] = False
290293
summary["quantiles"] = quantiles
291294
if not with_plots:
295+
# besides the plots, the bin counts and edges are always stored and
296+
# available in the json output.
297+
summary["histogram_data"] = _plotting.histogram_data(column)
292298
return
293299
if order_by_column is None:
294-
(
295-
summary["histogram_plot"],
296-
summary["n_low_outliers"],
297-
summary["n_high_outliers"],
298-
) = _plotting.histogram(
300+
summary["histogram_plot"], summary["histogram_data"] = _plotting.histogram(
299301
column, duration_unit=duration_unit, color=_plotting.COLORS[0]
300302
)
301303
else:

skrub/_reporting/_utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ def default(self, value):
115115
return int(value)
116116
if isinstance(value, np.floating):
117117
return float(value)
118+
if isinstance(value, np.ndarray):
119+
return value.tolist()
118120
raise
119121

120122

skrub/_reporting/tests/test_plotting.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,21 @@ def test_histogram():
1010
o = rng.uniform(-100, 100, size=10)
1111

1212
data = pd.Series(np.concatenate([x, o]))
13-
_, n_low, n_high = _plotting.histogram(data)
14-
assert (n_low, n_high) == (5, 4)
13+
_, hist = _plotting.histogram(data)
14+
assert (hist["n_low_outliers"], hist["n_high_outliers"]) == (5, 4)
1515

1616
data = pd.Series(np.concatenate([x, o - 1000]))
17-
_, n_low, n_high = _plotting.histogram(data)
18-
assert (n_low, n_high) == (10, 0)
17+
_, hist = _plotting.histogram(data)
18+
assert (hist["n_low_outliers"], hist["n_high_outliers"]) == (10, 0)
1919

2020
data = pd.Series(np.concatenate([x, o + 1000]))
21-
_, n_low, n_high = _plotting.histogram(data)
22-
assert (n_low, n_high) == (0, 10)
21+
_, hist = _plotting.histogram(data)
22+
assert (hist["n_low_outliers"], hist["n_high_outliers"]) == (0, 10)
2323

2424
data = pd.Series(x)
25-
_, n_low, n_high = _plotting.histogram(data)
26-
assert (n_low, n_high) == (0, 0)
25+
_, hist = _plotting.histogram(data)
26+
assert (hist["n_low_outliers"], hist["n_high_outliers"]) == (0, 0)
2727

2828
data = pd.Series([0.0])
29-
_, n_low, n_high = _plotting.histogram(data)
30-
assert (n_low, n_high) == (0, 0)
29+
_, hist = _plotting.histogram(data)
30+
assert (hist["n_low_outliers"], hist["n_high_outliers"]) == (0, 0)

skrub/_reporting/tests/test_summarize.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ def test_summarize(
8484
0.75: 33.6,
8585
1.0: 78.3,
8686
}
87+
if order_by is None:
88+
assert len(summary["columns"][5]["histogram_data"]["bin_counts"]) == 10
89+
assert len(summary["columns"][5]["histogram_data"]["bin_edges"]) == 11
8790
assert summary["columns"][7]["null_count"] == 9
8891
assert summary["columns"][7]["nulls_level"] == "warning"
8992
assert summary["columns"][8]["null_count"] == 17

skrub/_reporting/tests/test_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ def test_json_encoder():
104104
d = {"a": x[0], "b": y[0]}
105105
assert json.dumps(d, cls=_utils.JSONEncoder) == '{"a": 1, "b": 1.0}'
106106
with pytest.raises(TypeError, match=".*JSON serializable"):
107-
json.dumps({"a": x}, cls=_utils.JSONEncoder)
107+
json.dumps({"a": np}, cls=_utils.JSONEncoder)
108108

109109

110110
def test_svg_to_img_src():

0 commit comments

Comments
 (0)