Metadata-Version: 2.4
Name: dpate
Version: 0.0.1
Summary: differentially private average treatment effect estimation
Project-URL: Homepage, https://github.com/dstewtes/dpate
Project-URL: Issues, https://github.com/dstewtes/dpate
Author-email: Duncan Stewardson <dstewtes@gmail.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.12
Requires-Dist: numpy>=1.26.4
Requires-Dist: scikit-learn>=1.8.0
Description-Content-Type: text/markdown

# dpate

A Python package for differentially private average treatment effect estimation.

[Github repo](https://github.com/dstewtes/dpate)

Based on this [paper]() <- link to be added when available

## Dependencies

dpate requires:

- Python (>= 3.12)
- NumPy (>= 1.26.4)
- SciKit-Learn (>= 1.8.0)

While this package _may_ work with other versions of Python, NumPy, or SciKit-Learn, the package has been tested with the versions listed above.

## Install

```
pip install -U dpate
```

## Usage

This package includes basic functions to estimate average treatment effect while preserving differential privacy. For all differentially private methods, we require the outcomes to be bound by some value such that `|Y_i| <= Cy` for all `i`. Further, unless otherwise specified, we require the covariates to fall within the unit ball. If the covariates do not fall within the unit ball, it is necessary to **privately** scale them to the unit ball. One way of doing so would be to have known, public, bounds on the covariates, and scale them down by the norm of those bounds.



## Documentation

### `estimators.py`

```py
def non_private_ate(
    Xs: NDArray,
    Ws: NDArray,
    Ys: NDArray,
    estimator: str = 'NIPW',
    crossfit: bool = True, 
    log_model_args: dict = {},
    lin_model_args: dict = {},
    **kwargs) -> float:
```
INPUTS:

- `Xs`: NDArray of shape `(n,features)` that holds the covariates
- `Ws`: NDArray of shape `(n,)` that holds the treatment assignments
- `Ys`: NDArray of shape `(n,)` that holds the outcome values.
- `estimator`: Which estimator to use: `'AIPW'`, `'NIPW'`, `'IPW'`, `'Reg'` are the options
- `crossfit`: Whether you wish to crossfit the data
- `log_model_args`: any extra parameters for the Scikit-Learn logistic regression model
- `lin_model_args`: any extra parameters for the Scikit-Learn linear regression model

OUT:

A non-private estimation of the average treatment effect using IPW, AIPW, NIPW, or G-formula


```py
def np_prop_blocking(Xs: NDArray,
    Ws: NDArray,
    Ys: NDArray,
    m = 5,
    **kwargs) -> float:
```

INPUTS:

- `Xs`: NDArray of shape `(n,features)` that holds the covariates
- `Ws`: NDArray of shape `(n,)` that holds the treatment assignments
- `Ys`: NDArray of shape `(n,)` that holds the outcome values.
- `m`: an integer or function (with input `n`) to determine the number of bins

OUT:

A non-private estimate of the ATE using blocking on the propensity score.


```py
def dp_ipw_sequential(
    Xs: NDArray, 
    Ws: NDArray, 
    Ys: NDArray, 
    Y_bound: float | int, 
    eps: float, 
    prop_clip: float = 0.1, 
    alpha: float = 0.2, 
    delta: float = 0.0, 
    distr: str = 'laplacian',
    C: float = 1.0,
    **kwargs) -> float:
```

INPUTS:

- `Xs`: NDArray of shape `(n,features)` that holds the covariates
- `Ws`: NDArray of shape `(n,)` that holds the treatment assignments
- `Ys`: NDArray of shape `(n,)` that holds the outcome values.
- `Y_bound`: a float or integer for a bound on the outcomes such that `|Ys[i]| <= Y_bound` for all `i`
- `eps`: A float value for the privacy budget
- `prop_clip`: a float value for where to clip the propensity score. Must be greater than 0 to preserve privacy.
- `alpha`: how much privacy budget to spend on the propensity score model
- `delta`: A float value for the chance of failure of privacy
- `distr`: A string of the name of the distribution from which to draw the noise. `'laplace'` and `'gaussain'` are the options.
- `C`: The inverse of the regularization constant for logistic regression. (same as the `C` parameter in the SciKit-Learn `LogisticRegression` model).
- `**kwargs`: any other desired parameters for the base SciKit-Learn `LogisticRegression` model

OUT:

An (eps,delta)-DP estimation of the average treatment effect using inverse probability weighting with sequential composition.


```py
def dp_ipw_split(
    Xs: NDArray, 
    Ws: NDArray, 
    Ys: NDArray, 
    Y_bound: float | int, 
    eps: float, 
    prop_clip: float = 0.1, 
    alpha: float = 0.5, 
    delta: float = 0.0,
    distr: str = 'laplacian',
    C: float = 1.0,
    **kwargs) -> float:
```

This function is an implementation and generalization of Lee et al. 2019's algorithm

INPUTS:

- `Xs`: NDArray of shape `(n,features)` that holds the covariates
- `Ws`: NDArray of shape `(n,)` that holds the treatment assignments
- `Ys`: NDArray of shape `(n,)` that holds the outcome values.
- `Y_bound`: a float or integer for a bound on the outcomes such that `|Ys[i]| <= Y_bound` for all `i`
- `eps`: A float value for the privacy budget
- `prop_clip`: a float value for where to clip the propensity score. Must be greater than 0 to preserve privacy.
- `alpha`: what percentage of the dataset to use for training the propensity model
- `delta`: A float value for the chance of failure of privacy
- `distr`: A string of the name of the distribution from which to draw the noise. `'laplace'` and `'gaussain'` are the options.
- `C`: The inverse of the regularization constant for logistic regression. (same as the `C` parameter in the SciKit-Learn `LogisticRegression` model).
- `**kwargs`: any other desired parameters for the base SciKit-Learn `LogisticRegression` model

OUT:

An (eps,delta)-DP estimation of the average treatment effect using inverse probability weighting with parallel composition.

```py
def dp_prop_blocking(
    Xs: NDArray, 
    Ws: NDArray, 
    Ys: NDArray, 
    Y_bound: float | int, 
    eps: float, 
    m = lambda n,eps : min(max(5,int(eps*20)), max(3,np.floor((n/1000+((eps-0.25)/0.25))/2))),
    T: float = 1.5,
    a1: float = 0.1, 
    a2: float = 0.35, 
    a3: float = 0.55, 
    delta: float = 0.0,
    distr: str = 'laplacian',
    C: float = 1.0,
    **kwargs) -> float:
```

INPUTS:

- `Xs`: NDArray of shape `(n,features)` that holds the covariates
- `Ws`: NDArray of shape `(n,)` that holds the treatment assignments
- `Ys`: NDArray of shape `(n,)` that holds the outcome values.
- `Y_bound`: a float or integer for a bound on the outcomes such that `|Ys[i]| <= Y_bound` for all `i`
- `eps`: A float value for the privacy budget
- `m`: an integer or function (with input `n`) to determine the number of bins
- `T`: a float for the threshold for minimum noisy count for a bin.
- `a1`: how much of the privacy budget to use on training the propensity model
- `a2`: how much of the privacy budget to use on the noisy bin counts
- `a3`: how much of the privacy budget to use on the noisy bin summations
- `delta`: A float value for the chance of failure of privacy
- `distr`: A string of the name of the distribution from which to draw the noise. `'laplace'` and `'gaussain'` are the options.
- `C`: The inverse of the regularization constant for logistic regression. (same as the `C` parameter in the SciKit-Learn `LogisticRegression` model).
- `**kwargs`: any other desired parameters for the base SciKit-Learn `LogisticRegression` model

OUT:

An (eps,delta)-DP estimation of the average treatment effect using blocking-on-the-propensity score


```py
def dp_ate_by_aggregation(
    Xs: NDArray,
    Ws: NDArray,
    Ys: NDArray,
    Y_bound: float | int,
    eps: float,
    delta: float = 0.0,
    K = lambda n: int(sqrt(n)),
    distr: str = 'laplacian',
    priv_step: str = 'end',
    log_model_args: dict = {},
    *args) -> float:
```

This function is an implementation and generalization of the point estimation part of Guha and Reiter 2025's algorithm.

INPUTS:

- `Xs`: NDArray of shape `(n,features)` that holds the covariates
- `Ws`: NDArray of shape `(n,)` that holds the treatment assignments
- `Ys`: NDArray of shape `(n,)` that holds the outcome values.
- `Y_bound`: a float or integer for a bound on the outcomes such that `|Ys[i]| <= Y_bound` for all `i`
- `eps`: A float value for the privacy budget
- `delta`: A float value for the chance of failure of privacy
- `K`: the number of subsamples to compute tau hat on.
- `distr`: A string of the name of the distribution from which to draw the noise. `'laplace'` and `'gaussain'` are the options.
- `priv_step`: when to add noise. Either `middle` (aggregate noisy tau hats) or `end` (add noise to the aggregate tau hat value)
- `log_model_args`: any extra parameters for the Scikit-Learn logistic regression model

If `priv_step` is `'end'`, then `Xs` do not need to fall within the unit ball.

OUT:

An (eps,delta)-DP estimation of the average treatment effect using subsample and aggregate


```py
def dp_nuisance_aggregation(
    Xs: NDArray,
    Ws: NDArray,
    Ys: NDArray,
    Y_bound: float | int,
    eps: float,
    delta: float = 0.0,
    prop_clip: float = 0.1,
    K = lambda n: int(sqrt(n)),
    estimator = 'AIPW',
    distr: str = 'laplacian',
    log_model_args: dict = {},
    lin_model_args: dict = {},
    *args) -> float:
```

This function is an implementation and generalization of Lebeda et al. 2025's algorithm.

INPUTS:

- `Xs`: NDArray of shape `(n,features)` that holds the covariates
- `Ws`: NDArray of shape `(n,)` that holds the treatment assignments
- `Ys`: NDArray of shape `(n,)` that holds the outcome values.
- `Y_bound`: a float or integer for a bound on the outcomes such that `|Ys[i]| <= Y_bound` for all `i`
- `eps`: A float value for the privacy budget
- `delta`: A float value for the chance of failure of privacy
- `prop_clip`: a float value for where to clip the propensity score. Must be greater than 0 to preserve privacy.
- `K`: the number of subsamples to train nuisance models on.
- `estimator`: Which estimator to use: `'AIPW'`, `'IPW'`, or `'Reg'` are the options
- `distr`: A string of the name of the distribution from which to draw the noise. `'laplace'` and `'gaussain'` are the options.
- `log_model_args`: any extra parameters for the Scikit-Learn logistic regression model
- `lin_model_args`: any extra parameters for the Scikit-Learn linear regression model

The `Xs` do not need to fall within the unit ball for this function.

OUT:

An (eps,delta)-DP estimation of the average treatment effect using subsample and aggregate on nuisance estimators


```py
def dp_nipw_sequential(
    Xs: NDArray, 
    Ws: NDArray, 
    Ys: NDArray, 
    Y_bound: float | int, 
    eps: float, 
    prop_clip: float = 0.1, 
    alpha: float = 0.2, 
    beta: float = 0.5,
    delta: float = 0.0, 
    distr: str = 'laplacian',
    C: float = 1.0,
    **kwargs) -> float:
```

This function is an implementation and generalization of the point estimation part of Ohnishi and Awan 2025's algorithm.

INPUTS:

- `Xs`: NDArray of shape `(n,features)` that holds the covariates
- `Ws`: NDArray of shape `(n,)` that holds the treatment assignments
- `Ys`: NDArray of shape `(n,)` that holds the outcome values.
- `Y_bound`: a float or integer for a bound on the outcomes such that `|Ys[i]| <= Y_bound` for all `i`
- `eps`: A float value for the privacy budget
- `prop_clip`: a float value for where to clip the propensity score. Must be greater than 0 to preserve privacy.
- `alpha`: how much privacy budget to spend on the propensity score model
- `beta`: how to split the remaining privacy budget between the numerators and denominators of NIPW
- `delta`: A float value for the chance of failure of privacy
- `distr`: A string of the name of the distribution from which to draw the noise. `'laplace'` and `'gaussain'` are the options.
- `C`: The inverse of the regularization constant for logistic regression. (same as the `C` parameter in the SciKit-Learn `LogisticRegression` model).
- `**kwargs`: any other desired parameters for the base SciKit-Learn `LogisticRegression` model

OUT:

An (eps,delta)-DP estimation of the average treatment effect using inverse probability weighting with normalized weights using sequential composition.

### `models.py`


```py
class PrivPropScoreModel(LogisticRegression)
```

This is a class for a differentially private logistic-regression based propensity score model based on the output perturbation method described by Chaudhuri et al. 2009. The three following functions are methods of this class.


```py
def __init__(
        self,
        eps: float,
        delta: float = 0.0,
        clip: float = 0.0,
        distr: str = 'laplacian',
        C: float = 1.0,
        **kwargs) -> None:
```

INPUTS:

- `eps`: A float value for the privacy budget
- `delta`: A float value for the chance of failure of privacy
- `clip`: a float value for where to clip the propensity score. Must be greater than 0 to preserve privacy.
- `distr`: A string of the name of the distribution from which to draw the noise. `'laplace'` and `'gaussain'` are the options.
- `C`: The inverse of the regularization constant for logistic regression. (same as the `C` parameter in the SciKit-Learn `LogisticRegression` model).
- `**kwargs`: any other desired parameters for the base SciKit-Learn `LogisticRegression` model

The attributes of this class are the same as the inputs to `__init__`

```py
def fit(self, Xs: NDArray, Ws: NDArray):
```

Fits and then privatizes the model weights.

INPUTS:

- `Xs`: an array of shape `(n, d)` where each entry is an individual's `d`-dimensional covariates
- `Ws`: a 1D array of length `n` of binary treatment assignments for each individual.

```py
def get_prop_score(self, Xs: NDArray):
```

Outputs a `(2,n)` array where the first entry is each individual's probability of being in the control group, and the second entry is each individual's probability of treatment.

INPUTS:

- `Xs`: an array of shape `(n, d)` where each entry is an individual's `d`-dimensional covariates



### `utils.py`

```py
def calc_gaussian_noise(eps: float, delta: float, sensitivity: float) -> float:
```

This function calculates the standard deviation of the Gaussian noise needed to preserve (`eps`, `delta`)-Differential Privacy for a function with sensitivity of `sensitivity`

INPUTS:

- `eps`: A float value for the privacy budget
- `delta`: A float value for the chance of failure of privacy
- `sensitivity`: A float value for the sensitivity of the function you wish to privatize via Gaussian noise.



```py
def get_noise(
    eps: float,
    sensitivity: float,
    delta: float = 0.0,
    size: int | tuple = None,
    distr: str = 'laplacian') -> float | NDArray:
```

This function outputs a float or array of noise for privacy by output perturbation.

INPUTS:

- `eps`: A float value for the privacy budget
- `sensitivity`: A float value for the sensitivity of the function you wish to privatize.
- `delta`: A float value for the chance of failure of privacy
- `size`: The size/shape of the output array. If a float is desired, leave as `None`
- `distr`: A string of the name of the distribution from which to draw the noise. `'laplace'` and `'gaussain'` are the options.


```py
def calc_ate(
    Ws: NDArray, 
    Ys: NDArray, 
    e0: NDArray, 
    e1: NDArray, 
    g0: NDArray, 
    g1:NDArray, 
    estimator: str) -> float:
```

This function outputs a _non-private_ estimate of the ATE.

INPUTS:

- `Ws`: binary treatment assignments
- `Ys`: observed outcomes
- `e0`: estimated probability of being in the control group
- `e1`: estimated probability of being in the treated group (o/w known as the propensity score)
- `g0`: regression scores of E[Y(0)|X]
- `g1`: regression scores of E[Y(1)|X]
- `estimator`: which estimator to use. `'IPW'`, `'NIPW'`, `'AIPW'`, `'Reg'` are the options




## Citation Details

If you use this code, please cite the following paper:

**To be included once arxiv link is up**

Further, code in this repository implements algorithms from (or inspired by) the following sources:

```
@article{chaudhuri_differentially_2009,
  title={Differentially private empirical risk minimization.},
  author={Chaudhuri, Kamalika and Monteleoni, Claire and Sarwate, Anand D},
  journal={Journal of Machine Learning Research},
  volume={12},
  number={3},
  year={2011}
}
```

```
@misc{lebeda_model_2025,
	title = {Model {Agnostic} {Differentially} {Private} {Causal} {Inference}},
	url = {http://arxiv.org/abs/2505.19589},
	doi = {10.48550/arXiv.2505.19589},
	urldate = {2025-09-21},
	publisher = {arXiv},
	author = {Lebeda, Christian and Even, Mathieu and Bellet, Aurélien and Josse, Julie},
	month = may,
	year = {2025},
	note = {arXiv:2505.19589 [cs]},
	keywords = {Computer Science - Machine Learning, Statistics - Machine Learning}
}
```

```
@misc{ohnishi_differentially_2025,
	title = {Differentially {Private} {Covariate} {Balancing} {Causal} {Inference}},
	url = {http://arxiv.org/abs/2410.14789},
	doi = {10.48550/arXiv.2410.14789},
	urldate = {2025-09-21},
	publisher = {arXiv},
	author = {Ohnishi, Yuki and Awan, Jordan},
	month = aug,
	year = {2025},
	note = {arXiv:2410.14789 [stat]},
	keywords = {Computer Science - Cryptography and Security, Computer Science - Machine Learning, Statistics - Methodology}
}
```

```
@article{guha2025differentially,
  title={Differentially private estimation of weighted average treatment effects for binary outcomes},
  author={Guha, Sharmistha and Reiter, Jerome P},
  journal={Computational Statistics \& Data Analysis},
  volume={207},
  pages={108145},
  year={2025},
  publisher={Elsevier}
}
```

```
@misc{lee_privacy-preserving_2019,
	title = {Privacy-{Preserving} {Causal} {Inference} via {Inverse} {Probability} {Weighting}},
	url = {http://arxiv.org/abs/1905.12592},
	doi = {10.48550/arXiv.1905.12592},
	urldate = {2025-09-21},
	publisher = {arXiv},
	author = {Lee, Si Kai and Gresele, Luigi and Park, Mijung and Muandet, Krikamol},
	month = nov,
	year = {2019},
	note = {arXiv:1905.12592 [cs]},
	keywords = {Computer Science - Machine Learning, Statistics - Machine Learning},
	annote = {eps,delta dp},
}
```


## Final Remarks

If you have any issues installing or running this code, by all means please open an issue on the [Github repo](https://github.com/dstewtes/dpate/issues), and I will do my best to look into it as soon as possible :).