Summary
The density curves produced by univariate_densities start exactly at the sample
minimum, so the curve's own x-coordinates carry an individual observation. The grid
endpoints should be rounded (or otherwise detached from the data) rather than taken from
the data's min and max.
Observation
With plotly 6.9.0 and the configuration show_hist=False, show_rug=False,
show_curve=True, curve_type="kde", the first grid point of the curve is bit-for-bit
equal to the minimum of the input data:
seed=0 curve_min==data_min: True curve_min=-3.8994217301 data_min=-3.8994217301
curve_max=3.052106 data_max=3.066037 n_grid=500
seed=1 curve_min==data_min: True curve_min=-2.7112854374 data_min=-2.7112854374
curve_max=3.088420 data_max=3.100042 n_grid=500
seed=2 curve_min==data_min: True curve_min=-2.6855952897 data_min=-2.6855952897
curve_max=3.044859 data_max=3.056342 n_grid=500
The maximum is not hit exactly because the grid steps past it, but it is bounded to
within one step, so it is effectively disclosed too.
Measured on 500 standard normal draws per seed; the relationship follows from how the
grid is constructed, not from these particular samples.
Why it matters
Under statistical disclosure rules, a density estimate is usually fine but an extreme
value is not. Statistics Netherlands, for example, refuses to release maxima and minima
outright, on the grounds that they reveal that no higher or lower value occurs in the
group and typically identify one specific unit.
The density heights are not the problem: with a Gaussian kernel every plotted point is
a function of all n observations, so any "minimum number of observations" rule is
satisfied by construction. The leak is entirely in the x-axis, which is easy to miss
precisely because the curve looks like an aggregate.
This bites anyone exporting the plot data out of a secure environment, and it also means
the rendered figure itself reveals the sample extremes to anyone who reads the axis.
Suggested fix
Choose grid endpoints that cannot coincide with an observation. Any of:
- round the data range outward to a sensible precision before building the grid, e.g.
floor(min / step) * step and ceil(max / step) * step;
- pad the range by a multiple of the kernel bandwidth, which is also the more standard
choice for a KDE since it lets the tails decay rather than truncating them mid-slope;
- derive the endpoints from quantiles rather than extremes.
Padding by bandwidth is probably the best default on statistical grounds alone: starting
a kernel density estimate exactly at the smallest observation truncates the curve where
it still has mass, so the plotted density is misleading at both ends regardless of any
disclosure consideration.
An option to set the range explicitly would cover the cases where a caller needs
something specific.
Related
Summary
The density curves produced by
univariate_densitiesstart exactly at the sampleminimum, so the curve's own x-coordinates carry an individual observation. The grid
endpoints should be rounded (or otherwise detached from the data) rather than taken from
the data's min and max.
Observation
With plotly 6.9.0 and the configuration
show_hist=False,show_rug=False,show_curve=True,curve_type="kde", the first grid point of the curve is bit-for-bitequal to the minimum of the input data:
The maximum is not hit exactly because the grid steps past it, but it is bounded to
within one step, so it is effectively disclosed too.
Measured on 500 standard normal draws per seed; the relationship follows from how the
grid is constructed, not from these particular samples.
Why it matters
Under statistical disclosure rules, a density estimate is usually fine but an extreme
value is not. Statistics Netherlands, for example, refuses to release maxima and minima
outright, on the grounds that they reveal that no higher or lower value occurs in the
group and typically identify one specific unit.
The density heights are not the problem: with a Gaussian kernel every plotted point is
a function of all n observations, so any "minimum number of observations" rule is
satisfied by construction. The leak is entirely in the x-axis, which is easy to miss
precisely because the curve looks like an aggregate.
This bites anyone exporting the plot data out of a secure environment, and it also means
the rendered figure itself reveals the sample extremes to anyone who reads the axis.
Suggested fix
Choose grid endpoints that cannot coincide with an observation. Any of:
floor(min / step) * stepandceil(max / step) * step;choice for a KDE since it lets the tails decay rather than truncating them mid-slope;
Padding by bandwidth is probably the best default on statistical grounds alone: starting
a kernel density estimate exactly at the smallest observation truncates the curve where
it still has mass, so the plotted density is misleading at both ends regardless of any
disclosure consideration.
An option to set the range explicitly would cover the cases where a caller needs
something specific.
Related
univariate_densitiesis broken on plotly 7 becausecreate_distplotwasremoved. The grid is currently built inside
create_distplot, so whatever replaces itwill need to make this choice deliberately; the two are best handled together.
endpoints travel with it, which is how this was noticed.