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")