`` - ``FixedSmoothedMaxDivergence`` - :func:`opendp.combinators.make_pureDP_to_fixed_approxDP` * - ``MaxDivergence`` - ``ZeroConcentratedDivergence`` - :func:`opendp.combinators.make_pureDP_to_zCDP` * - ``ZeroConcentratedDivergence`` - ``SmoothedMaxDivergence`` - :func:`opendp.combinators.make_zCDP_to_approxDP` * - ``SmoothedMaxDivergence`` - ``FixedSmoothedMaxDivergence`` - :func:`opendp.combinators.make_fix_delta` :func:`opendp.combinators.make_pureDP_to_fixed_approxDP` is used for casting an output measure from ``MaxDivergence`` to ``FixedSmoothedMaxDivergence``. This is useful if you want to compose pure-DP measurements with approximate-DP measurements. .. doctest:: >>> from opendp.measurements import make_base_laplace >>> from opendp.combinators import make_pureDP_to_fixed_approxDP >>> meas_pureDP = make_base_laplace(scale=10.) >>> # convert the output measure to `FixedSmoothedMaxDivergence` >>> meas_fixed_approxDP = make_pureDP_to_fixed_approxDP(meas_pureDP) ... >>> # FixedSmoothedMaxDivergence distances are (ε, δ) tuples >>> meas_fixed_approxDP.map(d_in=1.) (0.1, 0.0) Similarly, :func:`opendp.combinators.make_pureDP_to_zCDP` is used for casting an output measure from ``MaxDivergence`` to ``ZeroConcentratedDivergence``. :func:`opendp.combinators.make_zCDP_to_approxDP` is used for casting an output measure from ``ZeroConcentratedDivergence`` to ``SmoothedMaxDivergence``. .. doctest:: >>> from opendp.measurements import make_base_gaussian >>> from opendp.combinators import make_zCDP_to_approxDP >>> meas_zCDP = make_base_gaussian(scale=0.5) >>> # convert the output measure to `SmoothedMaxDivergence` >>> meas_approxDP = make_zCDP_to_approxDP(meas_zCDP) ... >>> # SmoothedMaxDivergence distances are ε(δ) curves >>> curve = meas_approxDP.map(d_in=1.) >>> curve.epsilon(delta=1e-6) 11.688596249354896 :func:`opendp.combinators.make_fix_delta` changes the output measure from ``SmoothedMaxDivergence`` to ``FixedSmoothedMaxDivergence``. It fixes the delta parameter in the curve, so that the resulting measurement can be composed with other ``FixedSmoothedMaxDivergence`` measurements. .. doctest:: >>> from opendp.combinators import make_fix_delta >>> # convert the output measure to `FixedSmoothedMaxDivergence` >>> meas_fixed_approxDP = make_fix_delta(meas_approxDP, delta=1e-8) ... >>> # FixedSmoothedMaxDivergence distances are (ε, δ) tuples >>> meas_fixed_approxDP.map(d_in=1.) (13.3861046488579, 1e-08) These last two combinators allow you to convert output distances in terms of ρ-zCDP to ε(δ)-approxDP, and then to (ε, δ)-approxDP. Amplification ------------- If your dataset is a simple sample from a larger population, you can make the privacy relation more permissive by wrapping your measurement with a privacy amplification combinator: :func:`opendp.combinators.make_population_amplification`. .. note:: The amplifier requires a looser trust model, as the population size can be set arbitrarily. .. doctest:: >>> enable_features("honest-but-curious") In order to demonstrate this API, we'll first create a measurement with a sized input domain. The resulting measurement expects the size of the input dataset to be 10. .. doctest:: >>> from opendp.transformations import make_sized_bounded_mean >>> from opendp.measurements import make_base_laplace >>> meas = make_sized_bounded_mean(size=10, bounds=(0., 10.)) >> make_base_laplace(scale=0.5) >>> print("standard mean:", amplified([1.] * 10)) # -> 1.03 # doctest: +SKIP We can now use the amplification combinator to construct an amplified measurement. The function on the amplified measurement is identical to the standard measurement. .. doctest:: >>> from opendp.combinators import make_population_amplification >>> amplified = make_population_amplification(meas, population_size=100) >>> print("amplified mean:", amplified([1.] * 10)) # -> .97 # doctest: +SKIP The privacy relation on the amplified measurement takes into account that the input dataset of size 10 is a simple sample of individuals from a theoretical larger dataset that captures the entire population, with 100 rows. .. doctest:: >>> # Where we once had a privacy utilization of ~2 epsilon... >>> assert meas.check(2, 2. + 1e-6) ... >>> # ...we now have a privacy utilization of ~.4941 epsilon. >>> assert amplified.check(2, .4941) The efficacy of this combinator improves as n gets larger. User-Defined Callbacks ---------------------- It is possible to construct Transformations, Measurements and Postprocessors on your own via Python functions. .. list-table:: :header-rows: 1 * - Component - Constructor * - Transformation - :func:`opendp.combinators.make_user_transformation` * - Measurement - :func:`opendp.combinators.make_user_measurement` * - Postprocessor - :func:`opendp.combinators.make_user_postprocessor` .. note:: This requires a looser trust model, as we cannot verify any privacy or stability properties of user-defined functions. .. doctest:: >>> enable_features("honest-but-curious") In this example, we mock the typical API of the OpenDP library: .. doctest:: >>> from opendp.combinators import make_user_transformation >>> from opendp.domains import vector_domain, atom_domain >>> from opendp.metrics import symmetric_distance >>> from opendp.typing import * ... >>> def make_repeat(multiplicity): ... """Constructs a Transformation that duplicates each record `multiplicity` times""" ... def function(arg: List[int]) -> List[int]: ... return arg * multiplicity ... ... def stability_map(d_in: int) -> int: ... # if a user could influence at most `d_in` records before, ... # they can now influence `d_in` * `multiplicity` records ... return d_in * multiplicity ... ... return make_user_transformation( ... vector_domain(atom_domain(T=int)), ... vector_domain(atom_domain(T=int)), ... function, ... symmetric_distance(), ... symmetric_distance(), ... stability_map, ... ) The resulting Transformation may be used interchangeably with those constructed via the library: .. doctest:: >>> from opendp.transformations import * >>> from opendp.measurements import make_base_discrete_laplace >>> trans = ( ... make_cast_default(TIA=str, TOA=int) ... >> make_repeat(2) # our custom transformation ... >> make_clamp((1, 2)) ... >> make_bounded_sum((1, 2)) ... >> make_base_discrete_laplace(1.0) ... ) ... >>> release = trans(["0", "1", "2", "3"]) >>> trans.map(1) # computes epsilon 4.0 The same holds for measurements and postprocessors. You can even mix computational primitives from other DP libraries!