Source code for synthpriv.privacy.budget
"""Split of the DP budget between training and marginals.
The ``dp-gan`` synthesizer consumes ``ecdf_epsilon`` on the marginals ECDFs and
``epsilon`` on DP-SGD training; the total guarantee is
``epsilon + ecdf_epsilon`` (additive and exact). This module helps split a total
budget into those two items coherently.
"""
from __future__ import annotations
from dataclasses import dataclass
[docs]
@dataclass(frozen=True)
class BudgetSplit:
"""Result of the split: ``train`` and ``margins`` items for ``dp-gan``.
``total`` (final composed guarantee) = ``train + margins``. For ``dp-copula``
use ``DPCopulaGenerator`` directly with its internal fractions
(``margins_fraction``/``corr_fraction``), which split the total automatically.
"""
total: float
margins_fraction: float
train: float
margins: float
def describe(self) -> str:
return (
f"Total budget {self.total:.3f} -> training (DP-SGD) "
f"{self.train:.3f} + marginals (DP-ECDF) {self.margins:.3f} "
f"({self.margins_fraction:.0%} to marginals). "
"for dp-copula the split is internal via margins/corr_fraction."
)
[docs]
def split_budget(total_epsilon: float, margins_fraction: float = 0.3) -> BudgetSplit:
"""Divide ``total_epsilon`` into training and marginals for ``dp-gan``.
A low ``margins_fraction`` (0.1-0.4) is usually enough for marginals with many
rows; increase it on small datasets (Laplace histogram variance grows with less
data) or when tails matter a lot and KS drops.
"""
if total_epsilon <= 0:
raise ValueError(f"total_epsilon must be > 0, got {total_epsilon!r}")
if not 0 < margins_fraction < 1:
raise ValueError(f"margins_fraction must be in (0, 1), got {margins_fraction!r}")
train = total_epsilon * (1.0 - margins_fraction)
margins = total_epsilon - train
return BudgetSplit(total=float(total_epsilon),
margins_fraction=float(margins_fraction),
train=float(train), margins=float(margins))