Aggregation: Mean#
Any constructors that have not completed the proof-writing and vetting process may still be accessed if you opt-in to “contrib”. Please contact us if you are interested in proof-writing. Thank you!
[51]:
from opendp.mod import enable_features
enable_features("contrib")
Known Dataset Size#
The much easier case to consider is when the dataset size is known:
[52]:
from opendp.transformations import make_sized_bounded_mean
sb_mean_trans = make_sized_bounded_mean(size=10, bounds=(0., 10.))
sb_mean_trans([5.] * 10)
[52]:
5.0
The sensitivity of this transformation is the same as in make_sized_bounded_sum
, but divided by size
.
That is, \(map(d_{in}) = (d_{in} // 2) \cdot max(|L|, U) / size\), where \(//\) denotes integer division with truncation.
[53]:
# since we are in the bounded-DP model, d_in should be a multiple of 2,
# because it takes one removal and one addition to change one record
sb_mean_trans.map(2)
[53]:
1.0000000000000169
Note that this operation does not divide by the length of the input data, it divides by the size parameter passed to the constructor. As in any other context, it is expected that the data passed into the function is a member of the input domain, so no promises of privacy or correctness are guaranteed when the data is not in the input domain. In particular, the function may give a result with no error message.
[54]:
sb_mean_trans = make_sized_bounded_mean(size=10, bounds=(0., 10.))
sb_mean_trans([5.])
[54]:
0.5
Unknown Dataset Size#
There are several approaches for releasing the mean when the dataset size is unknown.
The first approach is to use the resize transformation. You can separately release an estimate for the dataset size, and then preprocess the dataset with a resize transformation.
[55]:
from opendp.transformations import make_count, make_clamp, make_resize
from opendp.measurements import make_base_discrete_laplace, make_base_laplace
from opendp.domains import atom_domain
data = [5.] * 10
bounds = (0., 10.)
# (where TIA stands for Atomic Input Type)
count_meas = make_count(TIA=float) >> make_base_discrete_laplace(1.)
dp_count = count_meas(data)
mean_meas = (
make_clamp(bounds) >>
make_resize(dp_count, atom_domain(bounds), constant=5.) >>
make_sized_bounded_mean(dp_count, bounds) >>
make_base_laplace(1.)
)
mean_meas(data)
[55]:
5.239477071130359
The total privacy expenditure is the composition of the count_meas
and mean_meas
releases.
[56]:
from opendp.combinators import make_basic_composition
make_basic_composition([count_meas, mean_meas]).map(1)
[56]:
2.000000000000017
Another approach is to compute the DP sum and DP count, and then postprocess the output.
[59]:
from opendp.transformations import make_bounded_sum
dp_fraction_meas = make_basic_composition([
make_clamp(bounds) >> make_bounded_sum(bounds) >> make_base_laplace(10.),
make_count(TIA=float) >> make_base_discrete_laplace(1.)
])
dp_sum, dp_count = dp_fraction_meas(data)
print("dp mean:", dp_sum / dp_count)
print("epsilon:", dp_fraction_meas.map(1))
dp mean: 4.4093953568039455
epsilon: 2.000000009313226
The same approaches are valid for the variance estimator. The Unknown Dataset Size notebook goes into greater detail on the tradeoffs of these approaches.