diff --git a/404.html b/404.html index d06d117..eea4a84 100644 --- a/404.html +++ b/404.html @@ -39,6 +39,12 @@ + + + + + + @@ -272,7 +278,7 @@
  • - + @@ -293,7 +299,7 @@
  • - + @@ -314,7 +320,7 @@
  • - + diff --git a/css/ansi-colours.css b/css/ansi-colours.css new file mode 100644 index 0000000..42301ef --- /dev/null +++ b/css/ansi-colours.css @@ -0,0 +1,174 @@ +/*! +* +* IPython notebook +* +*/ +/* CSS font colors for translated ANSI escape sequences */ +/* The color values are a mix of + http://www.xcolors.net/dl/baskerville-ivorylight and + http://www.xcolors.net/dl/euphrasia */ +.ansi-black-fg { + color: #3E424D; +} +.ansi-black-bg { + background-color: #3E424D; +} +.ansi-black-intense-fg { + color: #282C36; +} +.ansi-black-intense-bg { + background-color: #282C36; +} +.ansi-red-fg { + color: #E75C58; +} +.ansi-red-bg { + background-color: #E75C58; +} +.ansi-red-intense-fg { + color: #B22B31; +} +.ansi-red-intense-bg { + background-color: #B22B31; +} +.ansi-green-fg { + color: #00A250; +} +.ansi-green-bg { + background-color: #00A250; +} +.ansi-green-intense-fg { + color: #007427; +} +.ansi-green-intense-bg { + background-color: #007427; +} +.ansi-yellow-fg { + color: #DDB62B; +} +.ansi-yellow-bg { + background-color: #DDB62B; +} +.ansi-yellow-intense-fg { + color: #B27D12; +} +.ansi-yellow-intense-bg { + background-color: #B27D12; +} +.ansi-blue-fg { + color: #208FFB; +} +.ansi-blue-bg { + background-color: #208FFB; +} +.ansi-blue-intense-fg { + color: #0065CA; +} +.ansi-blue-intense-bg { + background-color: #0065CA; +} +.ansi-magenta-fg { + color: #D160C4; +} +.ansi-magenta-bg { + background-color: #D160C4; +} +.ansi-magenta-intense-fg { + color: #A03196; +} +.ansi-magenta-intense-bg { + background-color: #A03196; +} +.ansi-cyan-fg { + color: #60C6C8; +} +.ansi-cyan-bg { + background-color: #60C6C8; +} +.ansi-cyan-intense-fg { + color: #258F8F; +} +.ansi-cyan-intense-bg { + background-color: #258F8F; +} +.ansi-white-fg { + color: #C5C1B4; +} +.ansi-white-bg { + background-color: #C5C1B4; +} +.ansi-white-intense-fg { + color: #A1A6B2; +} +.ansi-white-intense-bg { + background-color: #A1A6B2; +} +.ansi-default-inverse-fg { + color: #FFFFFF; +} +.ansi-default-inverse-bg { + background-color: #000000; +} +.ansi-bold { + font-weight: bold; +} +.ansi-underline { + text-decoration: underline; +} +/* The following styles are deprecated an will be removed in a future version */ +.ansibold { + font-weight: bold; +} +.ansi-inverse { + outline: 0.5px dotted; +} +/* use dark versions for foreground, to improve visibility */ +.ansiblack { + color: black; +} +.ansired { + color: darkred; +} +.ansigreen { + color: darkgreen; +} +.ansiyellow { + color: #c4a000; +} +.ansiblue { + color: darkblue; +} +.ansipurple { + color: darkviolet; +} +.ansicyan { + color: steelblue; +} +.ansigray { + color: gray; +} +/* and light for background, for the same reason */ +.ansibgblack { + background-color: black; +} +.ansibgred { + background-color: red; +} +.ansibggreen { + background-color: green; +} +.ansibgyellow { + background-color: yellow; +} +.ansibgblue { + background-color: blue; +} +.ansibgpurple { + background-color: magenta; +} +.ansibgcyan { + background-color: cyan; +} +.ansibggray { + background-color: gray; +} \ No newline at end of file diff --git a/css/jupyter-cells.css b/css/jupyter-cells.css new file mode 100644 index 0000000..46def9f --- /dev/null +++ b/css/jupyter-cells.css @@ -0,0 +1,10 @@ +/* Input cells */ +.input code, .input pre { + background-color: #3333aa11; +} + +/* Output cells */ +.output pre { + background-color: #ececec80; + padding: 10px; +} diff --git a/css/pandas-dataframe.css b/css/pandas-dataframe.css new file mode 100644 index 0000000..2c18015 --- /dev/null +++ b/css/pandas-dataframe.css @@ -0,0 +1,36 @@ +/* Pretty Pandas Dataframes */ +/* Supports mkdocs-material color variables */ +.dataframe { + border: 0; + font-size: smaller; +} +.dataframe tr { + border: none; + background: var(--md-code-bg-color, #ffffff); +} +.dataframe tr:nth-child(even) { + background: var(--md-default-bg-color, #f5f5f5); +} +.dataframe tr:hover { + background-color: var(--md-footer-bg-color--dark, #e1f5fe); +} + +.dataframe thead th { + background: var(--md-default-bg-color, #ffffff); + border-bottom: 1px solid #aaa; + font-weight: bold; +} +.dataframe th { + border: none; + padding-left: 10px; + padding-right: 10px; +} + +.dataframe td{ + /* background: #fff; */ + border: none; + text-align: right; + min-width:5em; + padding-left: 10px; + padding-right: 10px; +} diff --git a/_examples/azcausal/index.html b/examples/azcausal/index.html similarity index 84% rename from _examples/azcausal/index.html rename to examples/azcausal/index.html index a5fa087..6a10b0a 100644 --- a/_examples/azcausal/index.html +++ b/examples/azcausal/index.html @@ -8,7 +8,7 @@ - + @@ -43,6 +43,12 @@ + + + + + + @@ -448,12 +454,52 @@ + + +
    +
    +

    AZCausal Integration

    Amazon's AZCausal library provides the functionality to fit synthetic control and difference-in-difference models to your data. Integrating the synthetic data generating process of causal_validation with AZCausal is trivial, as we show in this notebook. To start, we'll simulate a toy dataset.

    +
    +
    +
    +
    +
    + +
    from azcausal.estimators.panel.sdid import SDID
     import scipy.stats as st
     
    @@ -469,6 +515,14 @@ 

    AZCausal Integration

    ) from causal_validation.transforms.parameter import UnitVaryingParameter
    + + +
    +
    +
    +
    + +
    cfg = Config(
         n_control_units=10,
         n_pre_intervention_timepoints=60,
    @@ -480,18 +534,65 @@ 

    AZCausal Integration

    data = linear_trend(simulate(cfg)) plot(data)
    -
    <Axes: xlabel='Time', ylabel='Observed'>
    -
    -

    png

    + + +
    +
    +
    +
    +
    +
    +<Axes: xlabel='Time', ylabel='Observed'>
    +
    +
    +
    +
    +
    +No description has been provided for this image +
    +
    +
    +
    +
    +
    +
    +

    will inflate the treated group's observations in the post-intervention window.

    +
    +
    +
    +
    +
    + +
    TRUE_EFFECT = 0.05
     effect = StaticEffect(effect=TRUE_EFFECT)
     inflated_data = effect(data)
     plot(inflated_data)
     
    -
    <Axes: xlabel='Time', ylabel='Observed'>
    -
    -

    png

    + + +
    +
    +
    +
    +
    +
    +<Axes: xlabel='Time', ylabel='Observed'>
    +
    +
    +
    +
    +
    +No description has been provided for this image +
    +
    +
    +
    +
    +
    +
    +

    Fitting a model

    We now have some very toy data on which we may apply a model. For this demonstration we shall use the Synthetic Difference-in-Differences model implemented in AZCausal; @@ -500,13 +601,28 @@

    Fitting a model

    AZCausal. Through the .to_azcausal() method implemented here, this is straightforward to achieve. Once we have a AZCausal compatible dataset, the modelling is very simple by virtue of the clean design of AZCausal.

    +
    +
    +
    +
    +
    + +
    panel = inflated_data.to_azcausal()
     model = SDID()
     result = model.fit(panel)
     print(f"Delta: {TRUE_EFFECT - result.effect.percentage().value / 100}")
     print(result.summary(title="Synthetic Data Experiment"))
     
    -
    Delta: -2.3592239273284576e-16
    +
    +
    +
    +
    +
    +
    +
    +
    +Delta: -2.3592239273284576e-16
     ╭──────────────────────────────────────────────────────────────────────────────╮
     |                          Synthetic Data Experiment                           |
     ├──────────────────────────────────────────────────────────────────────────────┤
    @@ -529,7 +645,16 @@ 

    Fitting a model

    | Observed: 747.03 | | Counter Factual: 711.46 | ╰──────────────────────────────────────────────────────────────────────────────╯ -
    +
    + +
    +
    +
    +
    +
    +
    +
    +

    effect. However, given the simplicity of the data, this is not surprising. With the functionality within this package though we can easily construct more complex datasets in effort to fully stress-test any new model and identify its limitations.

    @@ -545,6 +670,13 @@

    Fitting a model

    $\delta_{t, n}$ is 5% when $n=1$ and $t\geq 60$ and 0 otherwise. Meanwhile, $\mathbf{Y}$ is the matrix of observations, long in the number of time points and wide in the number of units.

    +
    +
    +
    +
    +
    + +
    cfg = Config(
         n_control_units=10,
         n_pre_intervention_timepoints=60,
    @@ -565,18 +697,53 @@ 

    Fitting a model

    data = effect(periodic(linear_trend(simulate(cfg)))) plot(data)
    -
    <Axes: xlabel='Time', ylabel='Observed'>
    -
    -

    png

    + + +
    +
    +
    +
    +
    +
    +<Axes: xlabel='Time', ylabel='Observed'>
    +
    +
    +
    +
    +
    +No description has been provided for this image +
    +
    +
    +
    +
    +
    +
    +

    time we see that the delta between the estaimted and true effect is much larger than before.

    +
    +
    +
    +
    +
    + +
    panel = data.to_azcausal()
     model = SDID()
     result = model.fit(panel)
     print(f"Delta: {100*(TRUE_EFFECT - result.effect.percentage().value / 100): .2f}%")
     print(result.summary(title="Synthetic Data Experiment"))
     
    -
    Delta:  1.71%
    +
    +
    +
    +
    +
    +
    +
    +
    +Delta:  1.71%
     ╭──────────────────────────────────────────────────────────────────────────────╮
     |                          Synthetic Data Experiment                           |
     ├──────────────────────────────────────────────────────────────────────────────┤
    @@ -599,7 +766,13 @@ 

    Fitting a model

    | Observed: 686.44 | | Counter Factual: 664.59 | ╰──────────────────────────────────────────────────────────────────────────────╯ -
    +
    + +
    +
    +
    +
    +
    diff --git a/_examples/azcausal_files/azcausal_2_1.png b/examples/azcausal/output_2_1.png similarity index 100% rename from _examples/azcausal_files/azcausal_2_1.png rename to examples/azcausal/output_2_1.png diff --git a/_examples/azcausal_files/azcausal_4_1.png b/examples/azcausal/output_4_1.png similarity index 100% rename from _examples/azcausal_files/azcausal_4_1.png rename to examples/azcausal/output_4_1.png diff --git a/_examples/azcausal_files/azcausal_8_1.png b/examples/azcausal/output_8_1.png similarity index 100% rename from _examples/azcausal_files/azcausal_8_1.png rename to examples/azcausal/output_8_1.png diff --git a/_examples/basic/index.html b/examples/basic/index.html similarity index 74% rename from _examples/basic/index.html rename to examples/basic/index.html index f8f75ac..881fd30 100644 --- a/_examples/basic/index.html +++ b/examples/basic/index.html @@ -8,7 +8,7 @@ - + @@ -45,6 +45,12 @@ + + + + + + @@ -570,12 +576,52 @@ + + +
    +
    +

    Data Synthesis

    In this notebook we'll demonstrate how causal-validation can be used to simulate synthetic datasets. We'll start with very simple data to which a static treatment effect may be applied. From there, we'll build up to complex datasets. Along the way, we'll show how reproducibility can be ensured, plots can be generated, and unit-level parameters may be specified.

    +
    +
    +
    +
    +
    + +
    from itertools import product
     
     import matplotlib.pyplot as plt
    @@ -597,9 +643,29 @@ 

    Data Synthesis

    ) from causal_validation.transforms.parameter import UnitVaryingParameter
    + + +
    +
    +
    +
    +

    Simulating a Dataset

    +
    +
    +
    +
    +
    +

    then invoking the simulate function. Once simulated, we may visualise the data through the plot function.

    +
    +
    +
    +
    +
    + +
    cfg = Config(
         n_control_units=10,
         n_pre_intervention_timepoints=60,
    @@ -610,14 +676,41 @@ 

    Simulating a Dataset

    data = simulate(cfg) plot(data)
    -
    <Axes: xlabel='Time', ylabel='Observed'>
    -
    -

    png

    + + +
    +
    +
    +
    +
    +
    +<Axes: xlabel='Time', ylabel='Observed'>
    +
    +
    +
    +
    +
    +No description has been provided for this image +
    +
    +
    +
    +
    +
    +
    +

    Controlling baseline behaviour

    We observe that we have 10 control units, each of which were sampled from a Gaussian distribution with mean 20 and scale 0.2. Had we wished for our underlying observations to have more or less noise, or to have a different global mean, then we can simply specify that through the config file.

    +
    +
    +
    +
    +
    + +
    means = [10, 50]
     scales = [0.1, 0.5]
     
    @@ -633,12 +726,34 @@ 

    Controlling baseline behaviour

    data = simulate(cfg) plot(data, ax=ax, title=f"Mean: {m}, Scale: {s}")
    -

    png

    + + +
    +
    +
    +
    +
    +No description has been provided for this image +
    +
    +
    +
    +
    +
    +
    +

    Reproducibility

    In the above four panels, we can see that whilst the mean and scale of the underlying data generating process is varying, the functional form of the data is the same. This is by design to ensure that data sampling is reproducible. To sample a new dataset, you may either change the underlying seed in the config file.

    +
    +
    +
    +
    +
    + +
    cfg = Config(
         n_control_units=10,
         n_pre_intervention_timepoints=60,
    @@ -646,14 +761,50 @@ 

    Reproducibility

    seed=42, )
    + + +
    +
    +
    +
    +

    Reusing the same config file across simulations

    +
    +
    +
    +
    +
    + +
    fig, axes = plt.subplots(ncols=2, figsize=(10, 3))
     for ax in axes:
         data = simulate(cfg)
         plot(data, ax=ax)
     
    -

    png

    + + +
    +
    +
    +
    +
    +No description has been provided for this image +
    +
    +
    +
    +
    +
    +
    +

    Or manually specifying and passing your own pseudorandom number generator key

    +
    +
    +
    +
    +
    + +
    
     rng = np.random.RandomState(42)
     
    @@ -662,67 +813,232 @@ 

    Reproducibility

    data = simulate(cfg, key=rng) plot(data, ax=ax)
    -

    png

    + + +
    +
    +
    +
    +
    +No description has been provided for this image +
    +
    +
    +
    +
    +
    +
    +

    Simulating an effect

    In the data we have seen up until now, the treated unit has been drawn from the same data generating process as the control units. However, it can be helpful to also inflate the treated unit to observe how well our model can recover the the true treatment effect. To do this, we simply compose our dataset with an Effect object. In the below, we shall inflate our data by 2%.

    +
    +
    +
    +
    +
    + +
    effect = StaticEffect(effect=0.02)
     inflated_data = effect(data)
     fig, (ax0, ax1) = plt.subplots(ncols=2, figsize=(10, 3))
     plot(data, ax=ax0, title="Original data")
     plot(inflated_data, ax=ax1, title="Inflated data")
     
    -
    <Axes: title={'center': 'Inflated data'}, xlabel='Time', ylabel='Observed'>
    -
    -

    png

    + + +
    +
    +
    +
    +
    +
    +<Axes: title={'center': 'Inflated data'}, xlabel='Time', ylabel='Observed'>
    +
    +
    +
    +
    +
    +No description has been provided for this image +
    +
    +
    +
    +
    +
    +
    +

    More complex generation processes

    The example presented above shows a very simple stationary data generation process. However, we may make our example more complex by including a non-stationary trend to the data.

    +
    +
    +
    +
    +
    + +
    trend_term = Trend(degree=1, coefficient=0.1)
     data_with_trend = effect(trend_term(data))
     plot(data_with_trend)
     
    -
    <Axes: xlabel='Time', ylabel='Observed'>
    -
    -

    png

    + + +
    +
    +
    +
    +
    +
    +<Axes: xlabel='Time', ylabel='Observed'>
    +
    +
    +
    +
    +
    +No description has been provided for this image +
    +
    +
    +
    +
    +
    +
    + +
    trend_term = Trend(degree=2, coefficient=0.0025)
     data_with_trend = effect(trend_term(data))
     plot(data_with_trend)
     
    -
    <Axes: xlabel='Time', ylabel='Observed'>
    -
    -

    png

    + + +
    +
    +
    +
    +
    +
    +<Axes: xlabel='Time', ylabel='Observed'>
    +
    +
    +
    +
    +
    +No description has been provided for this image +
    +
    +
    +
    +
    +
    +
    +

    We may also include periodic components in our data

    +
    +
    +
    +
    +
    + +
    periodicity = Periodic(amplitude=2, frequency=6)
     perioidic_data = effect(periodicity(trend_term(data)))
     plot(perioidic_data)
     
    -
    <Axes: xlabel='Time', ylabel='Observed'>
    -
    -

    png

    + + +
    +
    +
    +
    +
    +
    +<Axes: xlabel='Time', ylabel='Observed'>
    +
    +
    +
    +
    +
    +No description has been provided for this image +
    +
    +
    +
    +
    +
    +
    +

    Unit-level parameterisation

    +
    +
    +
    +
    +
    + +
    sampling_dist = norm(0.0, 1.0)
     intercept = UnitVaryingParameter(sampling_dist=sampling_dist)
     trend_term = Trend(degree=1, intercept=intercept, coefficient=0.1)
     data_with_trend = effect(trend_term(data))
     plot(data_with_trend)
     
    -
    <Axes: xlabel='Time', ylabel='Observed'>
    -
    -

    png

    + + +
    +
    +
    +
    +
    +
    +<Axes: xlabel='Time', ylabel='Observed'>
    +
    +
    +
    +
    +
    +No description has been provided for this image +
    +
    +
    +
    +
    +
    +
    + +
    sampling_dist = poisson(2)
     frequency = UnitVaryingParameter(sampling_dist=sampling_dist)
     
     p = Periodic(frequency=frequency)
     plot(p(data))
     
    -
    <Axes: xlabel='Time', ylabel='Observed'>
    -
    -

    png

    + + +
    +
    +
    +
    +
    +
    +<Axes: xlabel='Time', ylabel='Observed'>
    +
    +
    +
    +
    +
    +No description has been provided for this image +
    +
    +
    +
    +
    +
    +
    +

    Conclusions

    In this notebook we have shown how one can define their model's true underlying data generating process, starting from simple white-noise samples through to more complex @@ -731,6 +1047,9 @@

    Conclusions

    Amazon's own AZCausal library to compare the effect estimated by a model with the true effect of the underlying data generating process. A link to this notebook is here.

    +
    +
    +
    diff --git a/_examples/basic_files/basic_10_0.png b/examples/basic/output_10_0.png similarity index 100% rename from _examples/basic_files/basic_10_0.png rename to examples/basic/output_10_0.png diff --git a/_examples/basic_files/basic_12_0.png b/examples/basic/output_12_0.png similarity index 100% rename from _examples/basic_files/basic_12_0.png rename to examples/basic/output_12_0.png diff --git a/_examples/basic_files/basic_14_1.png b/examples/basic/output_14_1.png similarity index 100% rename from _examples/basic_files/basic_14_1.png rename to examples/basic/output_14_1.png diff --git a/_examples/basic_files/basic_16_1.png b/examples/basic/output_16_1.png similarity index 100% rename from _examples/basic_files/basic_16_1.png rename to examples/basic/output_16_1.png diff --git a/_examples/basic_files/basic_17_1.png b/examples/basic/output_17_1.png similarity index 100% rename from _examples/basic_files/basic_17_1.png rename to examples/basic/output_17_1.png diff --git a/_examples/basic_files/basic_19_1.png b/examples/basic/output_19_1.png similarity index 100% rename from _examples/basic_files/basic_19_1.png rename to examples/basic/output_19_1.png diff --git a/_examples/basic_files/basic_21_1.png b/examples/basic/output_21_1.png similarity index 100% rename from _examples/basic_files/basic_21_1.png rename to examples/basic/output_21_1.png diff --git a/_examples/basic_files/basic_22_1.png b/examples/basic/output_22_1.png similarity index 100% rename from _examples/basic_files/basic_22_1.png rename to examples/basic/output_22_1.png diff --git a/_examples/basic_files/basic_4_1.png b/examples/basic/output_4_1.png similarity index 100% rename from _examples/basic_files/basic_4_1.png rename to examples/basic/output_4_1.png diff --git a/_examples/basic_files/basic_6_0.png b/examples/basic/output_6_0.png similarity index 100% rename from _examples/basic_files/basic_6_0.png rename to examples/basic/output_6_0.png diff --git a/_examples/placebo_test/index.html b/examples/placebo_test/index.html similarity index 72% rename from _examples/placebo_test/index.html rename to examples/placebo_test/index.html index c06471c..a48f7b7 100644 --- a/_examples/placebo_test/index.html +++ b/examples/placebo_test/index.html @@ -8,7 +8,7 @@ - + @@ -45,6 +45,12 @@ + + + + + + @@ -504,6 +510,39 @@ + + +
    +
    +

    Placebo Testing

    A placebo test is an approach to assess the validity of a causal model by checking if the effect can truly be attributed to the treatment, or to other spurious factors. A @@ -518,6 +557,13 @@

    Placebo Testing

    reliable. Placebo testing is thus a critical step to ensure the robustness of findings in RCTs. In this notebook, we demonstrate how a placebo test can be conducted in causal-validation.

    +
    +
    +
    +
    +
    + +
    from azcausal.core.error import JackKnife
     from azcausal.estimators.panel.did import DID
     from azcausal.estimators.panel.sdid import SDID
    @@ -531,13 +577,37 @@ 

    Placebo Testing

    from causal_validation.plotters import plot from causal_validation.validation.placebo import PlaceboTest
    -
    /home/runner/.local/share/hatch/env/virtual/causal-validation/CYBYs5D-/docs/lib/python3.10/site-packages/pandera/engines/pandas_engine.py:67: UserWarning: Using typeguard < 3. Generic types like List[TYPE], Dict[TYPE, TYPE] will only validate the first element in the collection.
    +
    +
    +
    +
    +
    +
    +
    +
    +/home/runner/.local/share/hatch/env/virtual/causal-validation/CYBYs5D-/docs/lib/python3.10/site-packages/pandera/engines/pandas_engine.py:67: UserWarning: Using typeguard < 3. Generic types like List[TYPE], Dict[TYPE, TYPE] will only validate the first element in the collection.
       warnings.warn(
    -
    +
    + +
    +
    +
    +
    +
    +
    +
    +

    Data simulation

    To demonstrate a placebo test, we must first simulate some data. For the purposes of illustration, we'll simulate a very simple dataset containing 10 control units where each unit has 60 pre-intervention observations, and 30 post-intervention observations.

    +
    +
    +
    +
    +
    + +
    cfg = Config(
         n_control_units=10,
         n_pre_intervention_timepoints=60,
    @@ -550,16 +620,50 @@ 

    Data simulation

    data = effect(simulate(cfg)) plot(data)
    -
    <Axes: xlabel='Time', ylabel='Observed'>
    -
    -

    png

    + + +
    +
    +
    +
    +
    +
    +<Axes: xlabel='Time', ylabel='Observed'>
    +
    +
    +
    +
    +
    +No description has been provided for this image +
    +
    +
    +
    +
    +
    +
    +

    Model

    We'll now define our model. To do this, we'll use the synthetic difference-in-differences implementation of AZCausal. This implementation, along with any other model from AZCausal, can be neatly wrapped up in our AZCausalWrapper to make fitting and effect estimation simpler.

    +
    +
    +
    +
    +
    + +
    model = AZCausalWrapper(model=SDID(), error_estimator=JackKnife())
     
    + + +
    +
    +
    +
    +

    Placebo Test Results

    Now that we have a dataset and model defined, we may conduct our placebo test. With 10 control units, the test will estimate 10 individual effects; 1 per control unit when @@ -571,32 +675,91 @@

    Placebo Test Results

    Accordingly, the p-value attains a value of 0.5, indicating that we have insufficient evidence to reject the null hypothesis and we, therefore, have no evidence to suggest that there is bias within this particular setup.

    +
    +
    +
    +
    +
    + +
    result = PlaceboTest(model, data).execute()
     result.summary()
     
    -
    Output()
    -
    -
    
     
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    
    +
    +
    +
    +
                                                                      
     | Model | Effect | Standard Deviation | Standard Error | p-value |
     |-------|--------|--------------------|----------------|---------|
     | SDID  | 0.0851 | 0.4079             | 0.129          | 0.5472  |
     
     
    - +
    +
    +
    +
    +
    +
    +
    +

    Model Comparison

    We can also use the results of a placebo test to compare two or more models. Using causal-validation, this is as simple as supplying a series of models to the placebo test and comparing their outputs. To demonstrate this, we will compare the previously used synthetic difference-in-differences model with regular difference-in-differences.

    +
    +
    +
    +
    +
    + +
    did_model = AZCausalWrapper(model=DID())
     PlaceboTest([model, did_model], data).execute().summary()
     
    -
    Output()
    -
    -
    
     
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    
    +
    +
    +
    +
                                                                      
     | Model | Effect | Standard Deviation | Standard Error | p-value |
     |-------|--------|--------------------|----------------|---------|
    @@ -604,6 +767,14 @@ 

    Model Comparison

    | DID | 0.0002 | 0.2818 | 0.0891 | 0.9982 |
    +
    +
    +
    +
    +
    + diff --git a/_examples/placebo_test_files/placebo_test_3_1.png b/examples/placebo_test/output_3_1.png similarity index 100% rename from _examples/placebo_test_files/placebo_test_3_1.png rename to examples/placebo_test/output_3_1.png diff --git a/index.html b/index.html index a2a20fd..bf09321 100644 --- a/index.html +++ b/index.html @@ -12,7 +12,7 @@ - + @@ -43,6 +43,12 @@ + + + + + + @@ -339,7 +345,7 @@
  • - + @@ -360,7 +366,7 @@
  • - + @@ -381,7 +387,7 @@
  • - + diff --git a/scripts/gen_examples.py b/scripts/gen_examples.py deleted file mode 100644 index 65fd132..0000000 --- a/scripts/gen_examples.py +++ /dev/null @@ -1,95 +0,0 @@ -from argparse import ArgumentParser -from pathlib import Path -import subprocess -from concurrent.futures import ThreadPoolExecutor, as_completed -import shutil -from dataclasses import dataclass - -EXCLUDE = ["utils.py"] - - -def process_file(file: Path, out_file: Path | None = None, execute: bool = False): - """Converts a python file to markdown using jupytext and nbconvert.""" - - out_dir = out_file.parent - command = f"cd {out_dir.as_posix()} && " - - out_file = out_file.relative_to(out_dir).as_posix() - - if execute: - command += f"jupytext --to ipynb {file} --output - " - command += ( - f"| jupyter nbconvert --to markdown --execute --stdin --output {out_file}" - ) - else: - command += f"jupytext --to markdown {file} --output {out_file}" - - subprocess.run(command, shell=True, check=False) - - -def is_modified(file: Path, out_file: Path): - """Check if the output file is older than the input file.""" - return out_file.exists() and out_file.stat().st_mtime < file.stat().st_mtime - - -def main(args): - # project root directory - wdir = Path(__file__).parents[2] - - # output directory - out_dir: Path = args.outdir - out_dir.mkdir(exist_ok=True, parents=True) - - # copy directories in "examples" to output directory - for dir in wdir.glob("examples/*"): - if dir.is_dir(): - (out_dir / dir.name).mkdir(exist_ok=True, parents=True) - for file in dir.glob("*"): - # copy, not move! - shutil.copy(file, out_dir / dir.name / file.name) - - # list of files to be processed - files = [f for f in wdir.glob("examples/*.py") if f.name not in EXCLUDE] - print(files) - - # process only modified files - if args.only_modified: - files = [f for f in files if is_modified(f, out_dir / f"{f.stem}.md")] - - # process files in parallel - if args.parallel: - with ThreadPoolExecutor(max_workers=args.max_workers) as executor: - futures = [] - for file in files: - out_file = out_dir / f"{file.stem.replace('.pct', '')}.md" - futures.append( - executor.submit( - process_file, file, out_file=out_file, execute=args.execute - ) - ) - - for future in as_completed(futures): - try: - future.result() - except Exception as e: - print(f"Error processing file: {e}") - else: - for file in files: - out_file = out_dir / f"{file.stem.replace('.pct', '')}.md" - process_file(file, out_file=out_file, execute=args.execute) - - -@dataclass -class GeneratorArgs: - max_workers: int = 4 - execute: bool = True - only_modified: bool = False - project_root: Path = Path(__file__).parents[2] - parallel: bool = False - - def __post_init__(self): - self.outdir: Path = self.project_root / "docs" / "_examples" - - -args = GeneratorArgs() -main(args) diff --git a/search/search_index.json b/search/search_index.json index 9749a8e..c1baf18 100644 --- a/search/search_index.json +++ b/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Welcome to Causal Validation","text":"

    Causal Validation is a library designed to validate and test your causal models. To achieve this, we provide functionality to simulate causal data, and vaildate your model through a placebo test.

    "},{"location":"#data-synthesis","title":"Data Synthesis","text":"

    Data Synthesis in Causal Validation is a fully composable process whereby a set of functions are sequentially applied to a dataset. At some point in this process we also induce a treatment effect. Any of these functions can be parameterised to either have constant parameter values across all control units, or a value that varies across parameters. To see this, consider the below example where we simulate a dataset whose trend varies across each of the 10 control units.

    from causal_validation import Config, simulate\nfrom causal_validation.effects import StaticEffect\nfrom causal_validation.plotters import plot\nfrom causal_validation.transforms import Trend, Periodic\nfrom causal_validation.transforms.parameter import UnitVaryingParameter\nfrom scipy.stats import norm\n\ncfg = Config(\n    n_control_units=10,\n    n_pre_intervention_timepoints=60,\n    n_post_intervention_timepoints=30,\n)\n\n# Simulate the base observation\nbase_data = simulate(cfg)\n\n# Apply a linear trend with unit-varying intercept\nintercept = UnitVaryingParameter(sampling_dist = norm(0, 1))\ntrend_component = Trend(degree=1, coefficient=0.1, intercept=intercept)\ntrended_data = trend_component(base_data)\n\n# Simulate a 5% lift in the treated unit's post-intervention data\neffect = StaticEffect(0.05)\ninflated_data = effect(trended_data)\n

    "},{"location":"#model-validation","title":"Model Validation","text":"

    Once a dataset has been synthesised, we may wish to validate our model using a placebo test. In Causal Validation this is straightforward and can be accomplished in combination with AZCausal by the following.

    from azcausal.estimators.panel.sdid import SDID\nfrom causal_validation.validation.placebo import PlaceboTest\n\nmodel = AZCausalWrapper(model=SDID())\nresult = PlaceboTest(model, inflated_data).execute()\nresult.summary()\n
    "},{"location":"_examples/azcausal/","title":"AZCausal Integration","text":"

    Amazon's AZCausal library provides the functionality to fit synthetic control and difference-in-difference models to your data. Integrating the synthetic data generating process of causal_validation with AZCausal is trivial, as we show in this notebook. To start, we'll simulate a toy dataset.

    from azcausal.estimators.panel.sdid import SDID\nimport scipy.stats as st\n\nfrom causal_validation import (\n    Config,\n    simulate,\n)\nfrom causal_validation.effects import StaticEffect\nfrom causal_validation.plotters import plot\nfrom causal_validation.transforms import (\n    Periodic,\n    Trend,\n)\nfrom causal_validation.transforms.parameter import UnitVaryingParameter\n
    cfg = Config(\n    n_control_units=10,\n    n_pre_intervention_timepoints=60,\n    n_post_intervention_timepoints=30,\n    seed=123,\n)\n\nlinear_trend = Trend(degree=1, coefficient=0.05)\ndata = linear_trend(simulate(cfg))\nplot(data)\n
    <Axes: xlabel='Time', ylabel='Observed'>\n

    will inflate the treated group's observations in the post-intervention window.

    TRUE_EFFECT = 0.05\neffect = StaticEffect(effect=TRUE_EFFECT)\ninflated_data = effect(data)\nplot(inflated_data)\n
    <Axes: xlabel='Time', ylabel='Observed'>\n

    "},{"location":"_examples/azcausal/#fitting-a-model","title":"Fitting a model","text":"

    We now have some very toy data on which we may apply a model. For this demonstration we shall use the Synthetic Difference-in-Differences model implemented in AZCausal; however, the approach shown here will work for any model implemented in AZCausal. To achieve this, we must first coerce the data into a format that is digestible for AZCausal. Through the .to_azcausal() method implemented here, this is straightforward to achieve. Once we have a AZCausal compatible dataset, the modelling is very simple by virtue of the clean design of AZCausal.

    panel = inflated_data.to_azcausal()\nmodel = SDID()\nresult = model.fit(panel)\nprint(f\"Delta: {TRUE_EFFECT - result.effect.percentage().value / 100}\")\nprint(result.summary(title=\"Synthetic Data Experiment\"))\n
    Delta: -2.3592239273284576e-16\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n|                          Synthetic Data Experiment                           |\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n|                                    Panel                                     |\n|  Time Periods: 90 (60/30)                                  total (pre/post)  |\n|  Units: 11 (10/1)                                       total (contr/treat)  |\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n|                                     ATT                                      |\n|  Effect: 1.1858                                                              |\n|  Observed: 24.90                                                             |\n|  Counter Factual: 23.72                                                      |\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n|                                  Percentage                                  |\n|  Effect: 5.0000                                                              |\n|  Observed: 105.00                                                            |\n|  Counter Factual: 100.00                                                     |\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n|                                  Cumulative                                  |\n|  Effect: 35.57                                                               |\n|  Observed: 747.03                                                            |\n|  Counter Factual: 711.46                                                     |\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n

    effect. However, given the simplicity of the data, this is not surprising. With the functionality within this package though we can easily construct more complex datasets in effort to fully stress-test any new model and identify its limitations.

    To achieve this, we'll simulate 10 control units, 60 pre-intervention time points, and 30 post-intervention time points according to the following process: $$ \\begin{align} \\mu_{n, t} & \\sim\\mathcal{N}(20, 0.5^2)\\ \\alpha_{n} & \\sim \\mathcal{N}(0, 1^2)\\ \\beta_{n} & \\sim \\mathcal{N}(0.05, 0.01^2)\\ \\nu_n & \\sim \\mathcal{N}(1, 1^2)\\ \\gamma_n & \\sim \\operatorname{Student-t}{10}(1, 1^2)\\ \\mathbf{Y}{n, t} & = \\mu_{n, t} + \\alpha_{n} + \\beta_{n}t + \\nu_n\\sin\\left(3\\times 2\\pi t + \\gamma\\right) + \\delta_{t, n} \\end{align} $$ where the true treatment effect $\\delta_{t, n}$ is 5% when $n=1$ and $t\\geq 60$ and 0 otherwise. Meanwhile, $\\mathbf{Y}$ is the matrix of observations, long in the number of time points and wide in the number of units.

    cfg = Config(\n    n_control_units=10,\n    n_pre_intervention_timepoints=60,\n    n_post_intervention_timepoints=30,\n    global_mean=20,\n    global_scale=1,\n    seed=123,\n)\n\nintercept = UnitVaryingParameter(sampling_dist=st.norm(loc=0.0, scale=1))\ncoefficient = UnitVaryingParameter(sampling_dist=st.norm(loc=0.05, scale=0.01))\nlinear_trend = Trend(degree=1, coefficient=coefficient, intercept=intercept)\n\namplitude = UnitVaryingParameter(sampling_dist=st.norm(loc=1.0, scale=2))\nshift = UnitVaryingParameter(sampling_dist=st.t(df=10))\nperiodic = Periodic(amplitude=amplitude, shift=shift, frequency=3)\n\ndata = effect(periodic(linear_trend(simulate(cfg))))\nplot(data)\n
    <Axes: xlabel='Time', ylabel='Observed'>\n

    time we see that the delta between the estaimted and true effect is much larger than before.

    panel = data.to_azcausal()\nmodel = SDID()\nresult = model.fit(panel)\nprint(f\"Delta: {100*(TRUE_EFFECT - result.effect.percentage().value / 100): .2f}%\")\nprint(result.summary(title=\"Synthetic Data Experiment\"))\n
    Delta:  1.71%\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n|                          Synthetic Data Experiment                           |\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n|                                    Panel                                     |\n|  Time Periods: 90 (60/30)                                  total (pre/post)  |\n|  Units: 11 (10/1)                                       total (contr/treat)  |\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n|                                     ATT                                      |\n|  Effect: 0.728265                                                            |\n|  Observed: 22.88                                                             |\n|  Counter Factual: 22.15                                                      |\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n|                                  Percentage                                  |\n|  Effect: 3.2874                                                              |\n|  Observed: 103.29                                                            |\n|  Counter Factual: 100.00                                                     |\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n|                                  Cumulative                                  |\n|  Effect: 21.85                                                               |\n|  Observed: 686.44                                                            |\n|  Counter Factual: 664.59                                                     |\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n
    "},{"location":"_examples/basic/","title":"Data Synthesis","text":"

    In this notebook we'll demonstrate how causal-validation can be used to simulate synthetic datasets. We'll start with very simple data to which a static treatment effect may be applied. From there, we'll build up to complex datasets. Along the way, we'll show how reproducibility can be ensured, plots can be generated, and unit-level parameters may be specified.

    from itertools import product\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom scipy.stats import (\n    norm,\n    poisson,\n)\n\nfrom causal_validation import (\n    Config,\n    simulate,\n)\nfrom causal_validation.effects import StaticEffect\nfrom causal_validation.plotters import plot\nfrom causal_validation.transforms import (\n    Periodic,\n    Trend,\n)\nfrom causal_validation.transforms.parameter import UnitVaryingParameter\n
    "},{"location":"_examples/basic/#simulating-a-dataset","title":"Simulating a Dataset","text":"

    then invoking the simulate function. Once simulated, we may visualise the data through the plot function.

    cfg = Config(\n    n_control_units=10,\n    n_pre_intervention_timepoints=60,\n    n_post_intervention_timepoints=30,\n    seed=123,\n)\n\ndata = simulate(cfg)\nplot(data)\n
    <Axes: xlabel='Time', ylabel='Observed'>\n

    "},{"location":"_examples/basic/#controlling-baseline-behaviour","title":"Controlling baseline behaviour","text":"

    We observe that we have 10 control units, each of which were sampled from a Gaussian distribution with mean 20 and scale 0.2. Had we wished for our underlying observations to have more or less noise, or to have a different global mean, then we can simply specify that through the config file.

    means = [10, 50]\nscales = [0.1, 0.5]\n\nfig, axes = plt.subplots(ncols=2, nrows=2, figsize=(10, 6), tight_layout=True)\nfor (m, s), ax in zip(product(means, scales), axes.ravel(), strict=False):\n    cfg = Config(\n        n_control_units=10,\n        n_pre_intervention_timepoints=60,\n        n_post_intervention_timepoints=30,\n        global_mean=m,\n        global_scale=s,\n    )\n    data = simulate(cfg)\n    plot(data, ax=ax, title=f\"Mean: {m}, Scale: {s}\")\n

    "},{"location":"_examples/basic/#reproducibility","title":"Reproducibility","text":"

    In the above four panels, we can see that whilst the mean and scale of the underlying data generating process is varying, the functional form of the data is the same. This is by design to ensure that data sampling is reproducible. To sample a new dataset, you may either change the underlying seed in the config file.

    cfg = Config(\n    n_control_units=10,\n    n_pre_intervention_timepoints=60,\n    n_post_intervention_timepoints=30,\n    seed=42,\n)\n

    Reusing the same config file across simulations

    fig, axes = plt.subplots(ncols=2, figsize=(10, 3))\nfor ax in axes:\n    data = simulate(cfg)\n    plot(data, ax=ax)\n

    Or manually specifying and passing your own pseudorandom number generator key

    \nrng = np.random.RandomState(42)\n\nfig, axes = plt.subplots(ncols=2, figsize=(10, 3))\nfor ax in axes:\n    data = simulate(cfg, key=rng)\n    plot(data, ax=ax)\n

    "},{"location":"_examples/basic/#simulating-an-effect","title":"Simulating an effect","text":"

    In the data we have seen up until now, the treated unit has been drawn from the same data generating process as the control units. However, it can be helpful to also inflate the treated unit to observe how well our model can recover the the true treatment effect. To do this, we simply compose our dataset with an Effect object. In the below, we shall inflate our data by 2%.

    effect = StaticEffect(effect=0.02)\ninflated_data = effect(data)\nfig, (ax0, ax1) = plt.subplots(ncols=2, figsize=(10, 3))\nplot(data, ax=ax0, title=\"Original data\")\nplot(inflated_data, ax=ax1, title=\"Inflated data\")\n
    <Axes: title={'center': 'Inflated data'}, xlabel='Time', ylabel='Observed'>\n

    "},{"location":"_examples/basic/#more-complex-generation-processes","title":"More complex generation processes","text":"

    The example presented above shows a very simple stationary data generation process. However, we may make our example more complex by including a non-stationary trend to the data.

    trend_term = Trend(degree=1, coefficient=0.1)\ndata_with_trend = effect(trend_term(data))\nplot(data_with_trend)\n
    <Axes: xlabel='Time', ylabel='Observed'>\n

    trend_term = Trend(degree=2, coefficient=0.0025)\ndata_with_trend = effect(trend_term(data))\nplot(data_with_trend)\n
    <Axes: xlabel='Time', ylabel='Observed'>\n

    We may also include periodic components in our data

    periodicity = Periodic(amplitude=2, frequency=6)\nperioidic_data = effect(periodicity(trend_term(data)))\nplot(perioidic_data)\n
    <Axes: xlabel='Time', ylabel='Observed'>\n

    "},{"location":"_examples/basic/#unit-level-parameterisation","title":"Unit-level parameterisation","text":"
    sampling_dist = norm(0.0, 1.0)\nintercept = UnitVaryingParameter(sampling_dist=sampling_dist)\ntrend_term = Trend(degree=1, intercept=intercept, coefficient=0.1)\ndata_with_trend = effect(trend_term(data))\nplot(data_with_trend)\n
    <Axes: xlabel='Time', ylabel='Observed'>\n
    sampling_dist = poisson(2)\nfrequency = UnitVaryingParameter(sampling_dist=sampling_dist)\n\np = Periodic(frequency=frequency)\nplot(p(data))\n
    <Axes: xlabel='Time', ylabel='Observed'>\n
    "},{"location":"_examples/basic/#conclusions","title":"Conclusions","text":"

    In this notebook we have shown how one can define their model's true underlying data generating process, starting from simple white-noise samples through to more complex example with periodic and temporal components, perhaps containing unit-level variation. In a follow-up notebook, we show how these datasets may be integrated with Amazon's own AZCausal library to compare the effect estimated by a model with the true effect of the underlying data generating process. A link to this notebook is here.

    "},{"location":"_examples/placebo_test/","title":"Placebo Testing","text":"

    A placebo test is an approach to assess the validity of a causal model by checking if the effect can truly be attributed to the treatment, or to other spurious factors. A placebo test is conducted by iterating through the set of control units and at each iteration, replacing the treated unit by one of the control units and measuring the effect. If the model detects a significant effect, then it suggests potential bias or omitted variables in the analysis, indicating that the causal inference is flawed.

    A successful placebo test will show no statistically significant results and we may then conclude that the estimated effect can be attributed to the treatment and not driven by confounding factors. Conversely, a failed placebo test, which shows significant results, suggests that the identified treatment effect may not be reliable. Placebo testing is thus a critical step to ensure the robustness of findings in RCTs. In this notebook, we demonstrate how a placebo test can be conducted in causal-validation.

    from azcausal.core.error import JackKnife\nfrom azcausal.estimators.panel.did import DID\nfrom azcausal.estimators.panel.sdid import SDID\n\nfrom causal_validation import (\n    Config,\n    simulate,\n)\nfrom causal_validation.effects import StaticEffect\nfrom causal_validation.models import AZCausalWrapper\nfrom causal_validation.plotters import plot\nfrom causal_validation.validation.placebo import PlaceboTest\n
    /home/runner/.local/share/hatch/env/virtual/causal-validation/CYBYs5D-/docs/lib/python3.10/site-packages/pandera/engines/pandas_engine.py:67: UserWarning: Using typeguard < 3. Generic types like List[TYPE], Dict[TYPE, TYPE] will only validate the first element in the collection.\n  warnings.warn(\n
    "},{"location":"_examples/placebo_test/#data-simulation","title":"Data simulation","text":"

    To demonstrate a placebo test, we must first simulate some data. For the purposes of illustration, we'll simulate a very simple dataset containing 10 control units where each unit has 60 pre-intervention observations, and 30 post-intervention observations.

    cfg = Config(\n    n_control_units=10,\n    n_pre_intervention_timepoints=60,\n    n_post_intervention_timepoints=30,\n    seed=123,\n)\n\nTRUE_EFFECT = 0.05\neffect = StaticEffect(effect=TRUE_EFFECT)\ndata = effect(simulate(cfg))\nplot(data)\n
    <Axes: xlabel='Time', ylabel='Observed'>\n

    "},{"location":"_examples/placebo_test/#model","title":"Model","text":"

    We'll now define our model. To do this, we'll use the synthetic difference-in-differences implementation of AZCausal. This implementation, along with any other model from AZCausal, can be neatly wrapped up in our AZCausalWrapper to make fitting and effect estimation simpler.

    model = AZCausalWrapper(model=SDID(), error_estimator=JackKnife())\n
    "},{"location":"_examples/placebo_test/#placebo-test-results","title":"Placebo Test Results","text":"

    Now that we have a dataset and model defined, we may conduct our placebo test. With 10 control units, the test will estimate 10 individual effects; 1 per control unit when it is mocked as the treated group. With those 10 effects, the routine will then produce the mean estimated effect, along with the standard deviation across the estimated effect, the effect's standard error, and the p-value that corresponds to the null-hypothesis test that the effect is 0.

    In the below, we see that expected estimated effect is small at just 0.08. Accordingly, the p-value attains a value of 0.5, indicating that we have insufficient evidence to reject the null hypothesis and we, therefore, have no evidence to suggest that there is bias within this particular setup.

    result = PlaceboTest(model, data).execute()\nresult.summary()\n
    Output()\n
     
                                                                      \n| Model | Effect | Standard Deviation | Standard Error | p-value |\n|-------|--------|--------------------|----------------|---------|\n| SDID  | 0.0851 | 0.4079             | 0.129          | 0.5472  |\n\n
    "},{"location":"_examples/placebo_test/#model-comparison","title":"Model Comparison","text":"

    We can also use the results of a placebo test to compare two or more models. Using causal-validation, this is as simple as supplying a series of models to the placebo test and comparing their outputs. To demonstrate this, we will compare the previously used synthetic difference-in-differences model with regular difference-in-differences.

    did_model = AZCausalWrapper(model=DID())\nPlaceboTest([model, did_model], data).execute().summary()\n
    Output()\n
     
                                                                      \n| Model | Effect | Standard Deviation | Standard Error | p-value |\n|-------|--------|--------------------|----------------|---------|\n| SDID  | 0.0851 | 0.4079             | 0.129          | 0.5472  |\n| DID   | 0.0002 | 0.2818             | 0.0891         | 0.9982  |\n\n
    "}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Welcome to Causal Validation","text":"

    Causal Validation is a library designed to validate and test your causal models. To achieve this, we provide functionality to simulate causal data, and vaildate your model through a placebo test.

    "},{"location":"#data-synthesis","title":"Data Synthesis","text":"

    Data Synthesis in Causal Validation is a fully composable process whereby a set of functions are sequentially applied to a dataset. At some point in this process we also induce a treatment effect. Any of these functions can be parameterised to either have constant parameter values across all control units, or a value that varies across parameters. To see this, consider the below example where we simulate a dataset whose trend varies across each of the 10 control units.

    from causal_validation import Config, simulate\nfrom causal_validation.effects import StaticEffect\nfrom causal_validation.plotters import plot\nfrom causal_validation.transforms import Trend, Periodic\nfrom causal_validation.transforms.parameter import UnitVaryingParameter\nfrom scipy.stats import norm\n\ncfg = Config(\n    n_control_units=10,\n    n_pre_intervention_timepoints=60,\n    n_post_intervention_timepoints=30,\n)\n\n# Simulate the base observation\nbase_data = simulate(cfg)\n\n# Apply a linear trend with unit-varying intercept\nintercept = UnitVaryingParameter(sampling_dist = norm(0, 1))\ntrend_component = Trend(degree=1, coefficient=0.1, intercept=intercept)\ntrended_data = trend_component(base_data)\n\n# Simulate a 5% lift in the treated unit's post-intervention data\neffect = StaticEffect(0.05)\ninflated_data = effect(trended_data)\n

    "},{"location":"#model-validation","title":"Model Validation","text":"

    Once a dataset has been synthesised, we may wish to validate our model using a placebo test. In Causal Validation this is straightforward and can be accomplished in combination with AZCausal by the following.

    from azcausal.estimators.panel.sdid import SDID\nfrom causal_validation.validation.placebo import PlaceboTest\n\nmodel = AZCausalWrapper(model=SDID())\nresult = PlaceboTest(model, inflated_data).execute()\nresult.summary()\n
    "},{"location":"examples/azcausal/","title":"AZCausal Integration","text":"
    from azcausal.estimators.panel.sdid import SDID\nimport scipy.stats as st\n\nfrom causal_validation import (\n    Config,\n    simulate,\n)\nfrom causal_validation.effects import StaticEffect\nfrom causal_validation.plotters import plot\nfrom causal_validation.transforms import (\n    Periodic,\n    Trend,\n)\nfrom causal_validation.transforms.parameter import UnitVaryingParameter\n
    cfg = Config(\n    n_control_units=10,\n    n_pre_intervention_timepoints=60,\n    n_post_intervention_timepoints=30,\n    seed=123,\n)\n\nlinear_trend = Trend(degree=1, coefficient=0.05)\ndata = linear_trend(simulate(cfg))\nplot(data)\n
    \n<Axes: xlabel='Time', ylabel='Observed'>\n

    will inflate the treated group's observations in the post-intervention window.

    TRUE_EFFECT = 0.05\neffect = StaticEffect(effect=TRUE_EFFECT)\ninflated_data = effect(data)\nplot(inflated_data)\n
    \n<Axes: xlabel='Time', ylabel='Observed'>\n
    panel = inflated_data.to_azcausal()\nmodel = SDID()\nresult = model.fit(panel)\nprint(f\"Delta: {TRUE_EFFECT - result.effect.percentage().value / 100}\")\nprint(result.summary(title=\"Synthetic Data Experiment\"))\n
    \nDelta: -2.3592239273284576e-16\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n|                          Synthetic Data Experiment                           |\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n|                                    Panel                                     |\n|  Time Periods: 90 (60/30)                                  total (pre/post)  |\n|  Units: 11 (10/1)                                       total (contr/treat)  |\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n|                                     ATT                                      |\n|  Effect: 1.1858                                                              |\n|  Observed: 24.90                                                             |\n|  Counter Factual: 23.72                                                      |\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n|                                  Percentage                                  |\n|  Effect: 5.0000                                                              |\n|  Observed: 105.00                                                            |\n|  Counter Factual: 100.00                                                     |\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n|                                  Cumulative                                  |\n|  Effect: 35.57                                                               |\n|  Observed: 747.03                                                            |\n|  Counter Factual: 711.46                                                     |\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n\n

    effect. However, given the simplicity of the data, this is not surprising. With the functionality within this package though we can easily construct more complex datasets in effort to fully stress-test any new model and identify its limitations.

    To achieve this, we'll simulate 10 control units, 60 pre-intervention time points, and 30 post-intervention time points according to the following process: $$ \\begin{align} \\mu_{n, t} & \\sim\\mathcal{N}(20, 0.5^2)\\ \\alpha_{n} & \\sim \\mathcal{N}(0, 1^2)\\ \\beta_{n} & \\sim \\mathcal{N}(0.05, 0.01^2)\\ \\nu_n & \\sim \\mathcal{N}(1, 1^2)\\ \\gamma_n & \\sim \\operatorname{Student-t}{10}(1, 1^2)\\ \\mathbf{Y}{n, t} & = \\mu_{n, t} + \\alpha_{n} + \\beta_{n}t + \\nu_n\\sin\\left(3\\times 2\\pi t + \\gamma\\right) + \\delta_{t, n} \\end{align} $$ where the true treatment effect $\\delta_{t, n}$ is 5% when $n=1$ and $t\\geq 60$ and 0 otherwise. Meanwhile, $\\mathbf{Y}$ is the matrix of observations, long in the number of time points and wide in the number of units.

    cfg = Config(\n    n_control_units=10,\n    n_pre_intervention_timepoints=60,\n    n_post_intervention_timepoints=30,\n    global_mean=20,\n    global_scale=1,\n    seed=123,\n)\n\nintercept = UnitVaryingParameter(sampling_dist=st.norm(loc=0.0, scale=1))\ncoefficient = UnitVaryingParameter(sampling_dist=st.norm(loc=0.05, scale=0.01))\nlinear_trend = Trend(degree=1, coefficient=coefficient, intercept=intercept)\n\namplitude = UnitVaryingParameter(sampling_dist=st.norm(loc=1.0, scale=2))\nshift = UnitVaryingParameter(sampling_dist=st.t(df=10))\nperiodic = Periodic(amplitude=amplitude, shift=shift, frequency=3)\n\ndata = effect(periodic(linear_trend(simulate(cfg))))\nplot(data)\n
    \n<Axes: xlabel='Time', ylabel='Observed'>\n

    time we see that the delta between the estaimted and true effect is much larger than before.

    panel = data.to_azcausal()\nmodel = SDID()\nresult = model.fit(panel)\nprint(f\"Delta: {100*(TRUE_EFFECT - result.effect.percentage().value / 100): .2f}%\")\nprint(result.summary(title=\"Synthetic Data Experiment\"))\n
    \nDelta:  1.71%\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n|                          Synthetic Data Experiment                           |\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n|                                    Panel                                     |\n|  Time Periods: 90 (60/30)                                  total (pre/post)  |\n|  Units: 11 (10/1)                                       total (contr/treat)  |\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n|                                     ATT                                      |\n|  Effect: 0.728265                                                            |\n|  Observed: 22.88                                                             |\n|  Counter Factual: 22.15                                                      |\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n|                                  Percentage                                  |\n|  Effect: 3.2874                                                              |\n|  Observed: 103.29                                                            |\n|  Counter Factual: 100.00                                                     |\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n|                                  Cumulative                                  |\n|  Effect: 21.85                                                               |\n|  Observed: 686.44                                                            |\n|  Counter Factual: 664.59                                                     |\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n\n
    "},{"location":"examples/azcausal/#azcausal-integration","title":"AZCausal Integration","text":"

    Amazon's AZCausal library provides the functionality to fit synthetic control and difference-in-difference models to your data. Integrating the synthetic data generating process of causal_validation with AZCausal is trivial, as we show in this notebook. To start, we'll simulate a toy dataset.

    "},{"location":"examples/azcausal/#fitting-a-model","title":"Fitting a model","text":"

    We now have some very toy data on which we may apply a model. For this demonstration we shall use the Synthetic Difference-in-Differences model implemented in AZCausal; however, the approach shown here will work for any model implemented in AZCausal. To achieve this, we must first coerce the data into a format that is digestible for AZCausal. Through the .to_azcausal() method implemented here, this is straightforward to achieve. Once we have a AZCausal compatible dataset, the modelling is very simple by virtue of the clean design of AZCausal.

    "},{"location":"examples/basic/","title":"Data Synthesis","text":"
    from itertools import product\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom scipy.stats import (\n    norm,\n    poisson,\n)\n\nfrom causal_validation import (\n    Config,\n    simulate,\n)\nfrom causal_validation.effects import StaticEffect\nfrom causal_validation.plotters import plot\nfrom causal_validation.transforms import (\n    Periodic,\n    Trend,\n)\nfrom causal_validation.transforms.parameter import UnitVaryingParameter\n

    then invoking the simulate function. Once simulated, we may visualise the data through the plot function.

    cfg = Config(\n    n_control_units=10,\n    n_pre_intervention_timepoints=60,\n    n_post_intervention_timepoints=30,\n    seed=123,\n)\n\ndata = simulate(cfg)\nplot(data)\n
    \n<Axes: xlabel='Time', ylabel='Observed'>\n
    means = [10, 50]\nscales = [0.1, 0.5]\n\nfig, axes = plt.subplots(ncols=2, nrows=2, figsize=(10, 6), tight_layout=True)\nfor (m, s), ax in zip(product(means, scales), axes.ravel(), strict=False):\n    cfg = Config(\n        n_control_units=10,\n        n_pre_intervention_timepoints=60,\n        n_post_intervention_timepoints=30,\n        global_mean=m,\n        global_scale=s,\n    )\n    data = simulate(cfg)\n    plot(data, ax=ax, title=f\"Mean: {m}, Scale: {s}\")\n
    cfg = Config(\n    n_control_units=10,\n    n_pre_intervention_timepoints=60,\n    n_post_intervention_timepoints=30,\n    seed=42,\n)\n

    Reusing the same config file across simulations

    fig, axes = plt.subplots(ncols=2, figsize=(10, 3))\nfor ax in axes:\n    data = simulate(cfg)\n    plot(data, ax=ax)\n

    Or manually specifying and passing your own pseudorandom number generator key

    \nrng = np.random.RandomState(42)\n\nfig, axes = plt.subplots(ncols=2, figsize=(10, 3))\nfor ax in axes:\n    data = simulate(cfg, key=rng)\n    plot(data, ax=ax)\n
    effect = StaticEffect(effect=0.02)\ninflated_data = effect(data)\nfig, (ax0, ax1) = plt.subplots(ncols=2, figsize=(10, 3))\nplot(data, ax=ax0, title=\"Original data\")\nplot(inflated_data, ax=ax1, title=\"Inflated data\")\n
    \n<Axes: title={'center': 'Inflated data'}, xlabel='Time', ylabel='Observed'>\n
    trend_term = Trend(degree=1, coefficient=0.1)\ndata_with_trend = effect(trend_term(data))\nplot(data_with_trend)\n
    \n<Axes: xlabel='Time', ylabel='Observed'>\n
    trend_term = Trend(degree=2, coefficient=0.0025)\ndata_with_trend = effect(trend_term(data))\nplot(data_with_trend)\n
    \n<Axes: xlabel='Time', ylabel='Observed'>\n

    We may also include periodic components in our data

    periodicity = Periodic(amplitude=2, frequency=6)\nperioidic_data = effect(periodicity(trend_term(data)))\nplot(perioidic_data)\n
    \n<Axes: xlabel='Time', ylabel='Observed'>\n
    sampling_dist = norm(0.0, 1.0)\nintercept = UnitVaryingParameter(sampling_dist=sampling_dist)\ntrend_term = Trend(degree=1, intercept=intercept, coefficient=0.1)\ndata_with_trend = effect(trend_term(data))\nplot(data_with_trend)\n
    \n<Axes: xlabel='Time', ylabel='Observed'>\n
    sampling_dist = poisson(2)\nfrequency = UnitVaryingParameter(sampling_dist=sampling_dist)\n\np = Periodic(frequency=frequency)\nplot(p(data))\n
    \n<Axes: xlabel='Time', ylabel='Observed'>\n
    "},{"location":"examples/basic/#data-synthesis","title":"Data Synthesis","text":"

    In this notebook we'll demonstrate how causal-validation can be used to simulate synthetic datasets. We'll start with very simple data to which a static treatment effect may be applied. From there, we'll build up to complex datasets. Along the way, we'll show how reproducibility can be ensured, plots can be generated, and unit-level parameters may be specified.

    "},{"location":"examples/basic/#simulating-a-dataset","title":"Simulating a Dataset","text":""},{"location":"examples/basic/#controlling-baseline-behaviour","title":"Controlling baseline behaviour","text":"

    We observe that we have 10 control units, each of which were sampled from a Gaussian distribution with mean 20 and scale 0.2. Had we wished for our underlying observations to have more or less noise, or to have a different global mean, then we can simply specify that through the config file.

    "},{"location":"examples/basic/#reproducibility","title":"Reproducibility","text":"

    In the above four panels, we can see that whilst the mean and scale of the underlying data generating process is varying, the functional form of the data is the same. This is by design to ensure that data sampling is reproducible. To sample a new dataset, you may either change the underlying seed in the config file.

    "},{"location":"examples/basic/#simulating-an-effect","title":"Simulating an effect","text":"

    In the data we have seen up until now, the treated unit has been drawn from the same data generating process as the control units. However, it can be helpful to also inflate the treated unit to observe how well our model can recover the the true treatment effect. To do this, we simply compose our dataset with an Effect object. In the below, we shall inflate our data by 2%.

    "},{"location":"examples/basic/#more-complex-generation-processes","title":"More complex generation processes","text":"

    The example presented above shows a very simple stationary data generation process. However, we may make our example more complex by including a non-stationary trend to the data.

    "},{"location":"examples/basic/#unit-level-parameterisation","title":"Unit-level parameterisation","text":""},{"location":"examples/basic/#conclusions","title":"Conclusions","text":"

    In this notebook we have shown how one can define their model's true underlying data generating process, starting from simple white-noise samples through to more complex example with periodic and temporal components, perhaps containing unit-level variation. In a follow-up notebook, we show how these datasets may be integrated with Amazon's own AZCausal library to compare the effect estimated by a model with the true effect of the underlying data generating process. A link to this notebook is here.

    "},{"location":"examples/placebo_test/","title":"Placebo Testing","text":"
    from azcausal.core.error import JackKnife\nfrom azcausal.estimators.panel.did import DID\nfrom azcausal.estimators.panel.sdid import SDID\n\nfrom causal_validation import (\n    Config,\n    simulate,\n)\nfrom causal_validation.effects import StaticEffect\nfrom causal_validation.models import AZCausalWrapper\nfrom causal_validation.plotters import plot\nfrom causal_validation.validation.placebo import PlaceboTest\n
    \n/home/runner/.local/share/hatch/env/virtual/causal-validation/CYBYs5D-/docs/lib/python3.10/site-packages/pandera/engines/pandas_engine.py:67: UserWarning: Using typeguard < 3. Generic types like List[TYPE], Dict[TYPE, TYPE] will only validate the first element in the collection.\n  warnings.warn(\n\n
    cfg = Config(\n    n_control_units=10,\n    n_pre_intervention_timepoints=60,\n    n_post_intervention_timepoints=30,\n    seed=123,\n)\n\nTRUE_EFFECT = 0.05\neffect = StaticEffect(effect=TRUE_EFFECT)\ndata = effect(simulate(cfg))\nplot(data)\n
    \n<Axes: xlabel='Time', ylabel='Observed'>\n
    model = AZCausalWrapper(model=SDID(), error_estimator=JackKnife())\n
    result = PlaceboTest(model, data).execute()\nresult.summary()\n
     
                                                                      \n| Model | Effect | Standard Deviation | Standard Error | p-value |\n|-------|--------|--------------------|----------------|---------|\n| SDID  | 0.0851 | 0.4079             | 0.129          | 0.5472  |\n\n
    did_model = AZCausalWrapper(model=DID())\nPlaceboTest([model, did_model], data).execute().summary()\n
     
                                                                      \n| Model | Effect | Standard Deviation | Standard Error | p-value |\n|-------|--------|--------------------|----------------|---------|\n| SDID  | 0.0851 | 0.4079             | 0.129          | 0.5472  |\n| DID   | 0.0002 | 0.2818             | 0.0891         | 0.9982  |\n\n
    "},{"location":"examples/placebo_test/#placebo-testing","title":"Placebo Testing","text":"

    A placebo test is an approach to assess the validity of a causal model by checking if the effect can truly be attributed to the treatment, or to other spurious factors. A placebo test is conducted by iterating through the set of control units and at each iteration, replacing the treated unit by one of the control units and measuring the effect. If the model detects a significant effect, then it suggests potential bias or omitted variables in the analysis, indicating that the causal inference is flawed.

    A successful placebo test will show no statistically significant results and we may then conclude that the estimated effect can be attributed to the treatment and not driven by confounding factors. Conversely, a failed placebo test, which shows significant results, suggests that the identified treatment effect may not be reliable. Placebo testing is thus a critical step to ensure the robustness of findings in RCTs. In this notebook, we demonstrate how a placebo test can be conducted in causal-validation.

    "},{"location":"examples/placebo_test/#data-simulation","title":"Data simulation","text":"

    To demonstrate a placebo test, we must first simulate some data. For the purposes of illustration, we'll simulate a very simple dataset containing 10 control units where each unit has 60 pre-intervention observations, and 30 post-intervention observations.

    "},{"location":"examples/placebo_test/#model","title":"Model","text":"

    We'll now define our model. To do this, we'll use the synthetic difference-in-differences implementation of AZCausal. This implementation, along with any other model from AZCausal, can be neatly wrapped up in our AZCausalWrapper to make fitting and effect estimation simpler.

    "},{"location":"examples/placebo_test/#placebo-test-results","title":"Placebo Test Results","text":"

    Now that we have a dataset and model defined, we may conduct our placebo test. With 10 control units, the test will estimate 10 individual effects; 1 per control unit when it is mocked as the treated group. With those 10 effects, the routine will then produce the mean estimated effect, along with the standard deviation across the estimated effect, the effect's standard error, and the p-value that corresponds to the null-hypothesis test that the effect is 0.

    In the below, we see that expected estimated effect is small at just 0.08. Accordingly, the p-value attains a value of 0.5, indicating that we have insufficient evidence to reject the null hypothesis and we, therefore, have no evidence to suggest that there is bias within this particular setup.

    "},{"location":"examples/placebo_test/#model-comparison","title":"Model Comparison","text":"

    We can also use the results of a placebo test to compare two or more models. Using causal-validation, this is as simple as supplying a series of models to the placebo test and comparing their outputs. To demonstrate this, we will compare the previously used synthetic difference-in-differences model with regular difference-in-differences.

    "}]} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml index 844099f..ebb287f 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -5,15 +5,15 @@ 2024-09-06 - https://amazon-science.github.io/causal-validation/_examples/azcausal/ + https://amazon-science.github.io/causal-validation/examples/azcausal/ 2024-09-06 - https://amazon-science.github.io/causal-validation/_examples/basic/ + https://amazon-science.github.io/causal-validation/examples/basic/ 2024-09-06 - https://amazon-science.github.io/causal-validation/_examples/placebo_test/ + https://amazon-science.github.io/causal-validation/examples/placebo_test/ 2024-09-06 \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz index a1735ed..e6345f7 100644 Binary files a/sitemap.xml.gz and b/sitemap.xml.gz differ