Questions or feedback?

API Comparison#

OpenDP has two APIs:

  • The Context API is simpler and helps enforce best practices. Currently available only for Python, it is used in the Getting Started documentation.

  • The Framework API is lower-level. Available for Python, R, and Rust, it directly implements the OpenDP Programming Framework.

Because the Context API is a wrapper around the Framework API, it is easier to use but less flexible: All calls ultimately pass through the Framework API.

A side-by-side comparison may make the differences more clear.

1. Identify the Unit of Privacy#

>>> import opendp.prelude as dp
>>> dp.enable_features("contrib")

>>> privacy_unit = dp.unit_of(contributions=1)
>>> privacy_unit
(SymmetricDistance(), 1)

>>> import opendp.prelude as dp
>>> dp.enable_features("contrib")

>>> d_in = 1  # neighboring data set distance is at most d_in...
>>> input_metric = (
...     dp.symmetric_distance()
... )  # ...in terms of additions/removals
>>> input_domain = dp.vector_domain(dp.atom_domain(T=float))

library(opendp)
enable_features("contrib")

d_in <- 1L # neighboring data set distance is at most d_in...
input_metric <- symmetric_distance() # ...in terms of additions/removals
input_domain <- vector_domain(atom_domain(.T = f64, nan = FALSE))

2. Set Privacy Loss Parameters#

>>> privacy_loss = dp.loss_of(epsilon=1.0)
>>> privacy_loss
(MaxDivergence, 1.0)

>>> d_out = 1.0  # output distributions have distance at most d_out (ε)...
>>> privacy_measure = (
...     dp.max_divergence()
... )  # ...in terms of pure-DP

d_out <- 1.0 # output distributions have distance at most d_out (ε)...
privacy_measure <- max_divergence() # ...in terms of pure-DP

3. Collect Public Information#

>>> bounds = (0.0, 100.0)
>>> imputed_value = 50.0

>>> bounds = (0.0, 100.0)
>>> imputed_value = 50.0

bounds <- c(0.0, 100.0)
imputed_value <- 50.0

4. Mediate Access to Data#

>>> from random import randint
>>> data = [float(randint(0, 100)) for _ in range(100)]

>>> context = dp.Context.compositor(
...     data=data,
...     privacy_unit=privacy_unit,
...     privacy_loss=privacy_loss,
...     split_evenly_over=3,
... )

dp.Context.compositor creates an adaptive composition measurement. You can now submit up to three queries to context, in the form of measurements.

>>> from random import randint
>>> data = [float(randint(0, 100)) for _ in range(100)]

>>> m_sc = dp.c.make_adaptive_composition(
...     input_domain=input_domain,
...     input_metric=input_metric,
...     output_measure=privacy_measure,
...     d_in=d_in,
...     d_mids=[d_out / 3] * 3,
... )

>>> # Call measurement with data to create a queryable:
>>> queryable = m_sc(data)

dp.c.make_adaptive_composition creates an adaptive composition measurement. You can now submit up to three queries to queryable, in the form of Measurements.

data <- runif(100L, min = 0.0, max = 100.0)

m_sc <- make_adaptive_composition(
  input_domain = input_domain,
  input_metric = input_metric,
  output_measure = privacy_measure,
  d_in = d_in,
  d_mids = rep(d_out / 3L, 3L)
)

# Call measurement with data to create a queryable:
queryable <- m_sc(arg = data) # Different from Python, which does not require "arg".

make_adaptive_composition creates an adaptive composition measurement. You can now submit up to three queries to queryable, in the form of measurements.

5. Submit DP Queries#

>>> mean_query = (
...     context.query()
...     .impute_constant(imputed_value)
...     .clamp(bounds)
...     .resize(size=dp_count, constant=imputed_value)
...     .mean()
...     .laplace()
... )

>>> dp_mean = mean_query.release()

>>> mean_transformation = (
...     dp.t.make_impute_constant(
...         input_domain, input_metric, 0.0
...     )
...     >> dp.t.then_clamp(bounds)
...     >> dp.t.then_resize(
...         size=dp_count, constant=imputed_value
...     )
...     >> dp.t.then_mean()
... )

>>> mean_measurement = dp.binary_search_chain(
...     lambda scale: mean_transformation
...     >> dp.m.then_laplace(scale),
...     d_in,
...     d_out / 3,
... )

>>> dp_mean = queryable(mean_measurement)

mean_transformation <- (
  make_clamp(input_domain, input_metric, bounds)
  |> then_resize(size = dp_count, constant = imputed_value)
  |> then_mean()
)

mean_measurement <- binary_search_chain(
  function(scale) mean_transformation |> then_laplace(scale), d_in, d_out / 3L
)

dp_mean <- queryable(query = mean_measurement) # Different from Python, which does not require "query".
cat("dp_mean:", dp_mean, "\n")