From b0e3158cf497749e0f5ec1f394cc7d12c2f14299 Mon Sep 17 00:00:00 2001 From: Olivier Labayle Date: Thu, 14 Mar 2024 13:40:31 +0100 Subject: [PATCH] generalize methods to factorialEstimand [BREAKING] --- Project.toml | 2 +- docs/src/user_guide/estimands.md | 4 +- src/TMLE.jl | 2 +- src/counterfactual_mean_based/estimands.jl | 221 ++++++++++---------- test/counterfactual_mean_based/estimands.jl | 100 +++++++-- 5 files changed, 195 insertions(+), 134 deletions(-) diff --git a/Project.toml b/Project.toml index 8d1e539..007bde8 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "TMLE" uuid = "8afdd2fb-6e73-43df-8b62-b1650cd9c8cf" authors = ["Olivier Labayle"] -version = "0.14.2" +version = "0.15.0" [deps] AbstractDifferentiation = "c29ec348-61ec-40c8-8164-b8c60e9d9f3d" diff --git a/docs/src/user_guide/estimands.md b/docs/src/user_guide/estimands.md index fd9fcf3..82f5abb 100644 --- a/docs/src/user_guide/estimands.md +++ b/docs/src/user_guide/estimands.md @@ -121,7 +121,7 @@ statisticalΨ = ATE( - Factorial Treatments -It is possible to generate a `ComposedEstimand` containing all linearly independent IATEs from a set of treatment values or from a dataset. For that purpose, use the `factorialATE` function. +It is possible to generate a `ComposedEstimand` containing all linearly independent IATEs from a set of treatment values or from a dataset. For that purpose, use the `factorialEstimand` function. ## The Interaction Average Treatment Effect @@ -182,7 +182,7 @@ statisticalΨ = IATE( - Factorial Treatments -It is possible to generate a `ComposedEstimand` containing all linearly independent IATEs from a set of treatment values or from a dataset. For that purpose, use the `factorialIATE` function. +It is possible to generate a `ComposedEstimand` containing all linearly independent IATEs from a set of treatment values or from a dataset. For that purpose, use the `factorialEstimand` function. ## Composed Estimands diff --git a/src/TMLE.jl b/src/TMLE.jl index 3edde41..1a68c38 100644 --- a/src/TMLE.jl +++ b/src/TMLE.jl @@ -28,7 +28,7 @@ using SplitApplyCombine export SCM, StaticSCM, add_equations!, add_equation!, parents, vertices export CM, ATE, IATE export AVAILABLE_ESTIMANDS -export factorialATE, factorialIATE +export factorialEstimand, factorialEstimands export TMLEE, OSE, NAIVE export ComposedEstimand export var, estimate, pvalue, confint, emptyIC diff --git a/src/counterfactual_mean_based/estimands.jl b/src/counterfactual_mean_based/estimands.jl index e6584d7..884cd2d 100644 --- a/src/counterfactual_mean_based/estimands.jl +++ b/src/counterfactual_mean_based/estimands.jl @@ -242,36 +242,20 @@ the number of estimands passing the positivity constraint. unique_treatment_values(dataset, colnames) = (;(colname => get_treatment_values(dataset, colname) for colname in colnames)...) -get_transitive_treatments_contrasts(treatments_unique_values) = +""" +Generated from transitive treatment switches to create independent estimands. +""" +get_treatment_settings(::Union{typeof(ATE), typeof(IATE)}, treatments_unique_values) = [collect(zip(vals[1:end-1], vals[2:end])) for vals in values(treatments_unique_values)] -function generateFactorialEstimandFromContrasts( - constructor, - treatments_levels::NamedTuple{names}, - outcome; - confounders=nothing, - outcome_extra_covariates=(), - freq_table=nothing, - positivity_constraint=nothing - ) where names - treatments_contrasts = get_transitive_treatments_contrasts(treatments_levels) - components = [] - for combo ∈ Iterators.product(treatments_contrasts...) - treatments_contrast = [NamedTuple{(:control, :case)}(treatment_control_case) for treatment_control_case ∈ combo] - Ψ = constructor( - outcome=outcome, - treatment_values=NamedTuple{names}(treatments_contrast), - treatment_confounders = confounders, - outcome_extra_covariates=outcome_extra_covariates - ) - if satisfies_positivity(Ψ, freq_table; positivity_constraint=positivity_constraint) - push!(components, Ψ) - end - end - return ComposedEstimand(joint_estimand, Tuple(components)) -end +get_treatment_settings(::typeof(CM), treatments_unique_values) = + values(treatments_unique_values) + +get_treatment_setting(combo::Tuple{Vararg{Tuple}}) = [NamedTuple{(:control, :case)}(treatment_control_case) for treatment_control_case ∈ combo] + +get_treatment_setting(combo) = collect(combo) -GENERATE_DOCSTRING = """ +FACTORIAL_DOCSTRING = """ The components of this estimand are generated from the treatment variables contrasts. For example, consider two treatment variables T₁ and T₂ each taking three possible values (0, 1, 2). For each treatment variable, the marginal transitive contrasts are defined by (0 → 1, 1 → 2). Note that (0 → 2) or (1 → 0) need not @@ -295,145 +279,152 @@ A `ComposedEstimand` with causal or statistical components. If `nothing`, causal estimands are generated. - `outcome_extra_covariates=()`: The generated components will inherit these `outcome_extra_covariates`. - `positivity_constraint=nothing`: Only components that pass the positivity constraint are added to the `ComposedEstimand` +- `verbosity=1`: Verbosity level. """ """ - factorialATE( - treatments_levels::NamedTuple{names}, outcome; + factorialEstimand( + constructor::Union{typeof(ATE), typeof(IATE)}, + treatments_levels::NamedTuple{names}, + outcome; confounders=nothing, outcome_extra_covariates=(), freq_table=nothing, - positivity_constraint=nothing - ) where names + positivity_constraint=nothing, + verbosity=1 + ) where names + +Generate a `ComposedEstimand` from `treatments_levels`. $FACTORIAL_DOCSTRING -Generate a `ComposedEstimand` of ATEs from the `treatments_levels`. $GENERATE_DOCSTRING +# Examples: -# Example: +Average Treatment Effects: -To generate a causal composed estimand with 3 components: +```@example +factorialEstimand(ATE, (T₁ = (0, 1), T₂=(0, 1, 2)), :Y₁) +``` ```@example -factorialATE((T₁ = (0, 1), T₂=(0, 1, 2)), :Y₁) +factorial(ATE, (T₁ = (0, 1, 2), T₂=(0, 1, 2)), :Y₁, confounders=[:W₁, :W₂]) ``` -To generate a statistical composed estimand with 9 components: +Interactions: ```@example -factorialATE((T₁ = (0, 1, 2), T₂=(0, 1, 2)), :Y₁, confounders=[:W₁, :W₂]) +factorialEstimand(IATE, (T₁ = (0, 1), T₂=(0, 1, 2)), :Y₁) ``` + +```@example +factorialEstimand(IATE, (T₁ = (0, 1, 2), T₂=(0, 1, 2)), :Y₁, confounders=[:W₁, :W₂]) """ -function factorialATE( - treatments_levels::NamedTuple{names}, outcome; +function factorialEstimand( + constructor::Union{typeof(CM), typeof(ATE), typeof(IATE)}, + treatments_levels::NamedTuple{names}, + outcome; confounders=nothing, outcome_extra_covariates=(), freq_table=nothing, - positivity_constraint=nothing + positivity_constraint=nothing, + verbosity=1 ) where names - return generateFactorialEstimandFromContrasts( - ATE, - treatments_levels, - outcome; - confounders=confounders, - outcome_extra_covariates=outcome_extra_covariates, - freq_table=freq_table, - positivity_constraint=positivity_constraint - ) + treatments_settings = get_treatment_settings(constructor, treatments_levels) + components = [] + for combo ∈ Iterators.product(treatments_settings...) + Ψ = constructor( + outcome=outcome, + treatment_values=NamedTuple{names}(get_treatment_setting(combo)), + treatment_confounders = confounders, + outcome_extra_covariates=outcome_extra_covariates + ) + if satisfies_positivity(Ψ, freq_table; positivity_constraint=positivity_constraint) + push!(components, Ψ) + else + verbosity > 0 && @warn("Sub estimand", Ψ, " did not pass the positivity constraint, skipped.") + end + end + return ComposedEstimand(joint_estimand, Tuple(components)) end """ - factorialATE(dataset, treatments, outcome; - confounders=nothing, - outcome_extra_covariates=(), - positivity_constraint=nothing +factorialEstimand( + constructor::Union{typeof(ATE), typeof(IATE)}, + dataset, treatments, outcome; + confounders=nothing, + outcome_extra_covariates=(), + positivity_constraint=nothing, + verbosity=1 ) -Find all unique values for each treatment variable in the dataset and generate all possible ATEs from these values. +Identifies `treatment_levels` from `dataset` and construct the +factorialEstimand from it. """ -function factorialATE(dataset, treatments, outcome; +function factorialEstimand( + constructor::Union{typeof(CM), typeof(ATE), typeof(IATE)}, + dataset, treatments, outcome; confounders=nothing, outcome_extra_covariates=(), - positivity_constraint=nothing + positivity_constraint=nothing, + verbosity=1 ) treatments_levels = unique_treatment_values(dataset, treatments) freq_table = positivity_constraint !== nothing ? frequency_table(dataset, keys(treatments_levels)) : nothing - return factorialATE( + return factorialEstimand( + constructor, treatments_levels, outcome; confounders=confounders, outcome_extra_covariates=outcome_extra_covariates, freq_table=freq_table, - positivity_constraint=positivity_constraint + positivity_constraint=positivity_constraint, + verbosity=verbosity ) end """ - factorialIATE( - treatments_levels::NamedTuple{names}, outcome; - confounders=nothing, - outcome_extra_covariates=(), - freq_table=nothing, - positivity_constraint=nothing - ) where names - -Generates a `ComposedEstimand` of IATE from `treatments_levels`. $GENERATE_DOCSTRING - -# Example: - -To generate a causal composed estimand with 3 components: - -```@example -factorialIATE((T₁ = (0, 1), T₂=(0, 1, 2)), :Y₁) -``` - -To generate a statistical composed estimand with 9 components: - -```@example -factorialIATE((T₁ = (0, 1, 2), T₂=(0, 1, 2)), :Y₁, confounders=[:W₁, :W₂]) -``` -""" -function factorialIATE( - treatments_levels::NamedTuple{names}, outcome; +factorialEstimands( + constructor::Union{typeof(ATE), typeof(IATE)}, + dataset, treatments, outcomes; confounders=nothing, outcome_extra_covariates=(), - freq_table=nothing, - positivity_constraint=nothing - ) where names - return generateFactorialEstimandFromContrasts( - IATE, - treatments_levels, - outcome; - confounders=confounders, - outcome_extra_covariates=outcome_extra_covariates, - freq_table=freq_table, - positivity_constraint=positivity_constraint + positivity_constraint=nothing, + verbosity=1 ) -end +Identifies `treatment_levels` from `dataset` and a factorialEstimand +for each outcome in `outcomes`. """ - factorialIATE(dataset, treatments, outcome; - confounders=nothing, - outcome_extra_covariates=(), - positivity_constraint=nothing - ) - -Finds treatments levels from the dataset and generates a `ComposedEstimand` of IATE from them -(see [`factorialIATE(treatments_levels, outcome; confounders=nothing, outcome_extra_covariates=())`](@ref)). -""" -function factorialIATE(dataset, treatments, outcome; +function factorialEstimands( + constructor::Union{typeof(CM), typeof(ATE), typeof(IATE)}, + dataset, treatments, outcomes; confounders=nothing, outcome_extra_covariates=(), - positivity_constraint=nothing + positivity_constraint=nothing, + verbosity=1 ) + estimands = [] treatments_levels = unique_treatment_values(dataset, treatments) freq_table = positivity_constraint !== nothing ? frequency_table(dataset, keys(treatments_levels)) : nothing - return factorialIATE( - treatments_levels, - outcome; - confounders=confounders, - outcome_extra_covariates=outcome_extra_covariates, - freq_table=freq_table, - positivity_constraint=positivity_constraint - ) + for outcome in outcomes + Ψ = factorialEstimand( + constructor, + treatments_levels, + outcome; + confounders=confounders, + outcome_extra_covariates=outcome_extra_covariates, + freq_table=freq_table, + positivity_constraint=positivity_constraint, + verbosity=verbosity-1 + ) + if length(Ψ.args) > 0 + push!(estimands, Ψ) + else + verbosity > 0 && @warn(string( + "ATE for outcome, ", outcome, + " has no component passing the positivity constraint, skipped." + )) + end + end + return estimands end joint_levels(Ψ::StatisticalIATE) = Iterators.product(values(Ψ.treatment_values)...) diff --git a/test/counterfactual_mean_based/estimands.jl b/test/counterfactual_mean_based/estimands.jl index 9d74013..bbabe48 100644 --- a/test/counterfactual_mean_based/estimands.jl +++ b/test/counterfactual_mean_based/estimands.jl @@ -203,9 +203,13 @@ end @testset "Test control_case_settings" begin treatments_unique_values = (T₁=(1, 0, 2),) - @test TMLE.get_transitive_treatments_contrasts(treatments_unique_values) == [[(1, 0), (0, 2)]] + @test TMLE.get_treatment_settings(ATE, treatments_unique_values) == [[(1, 0), (0, 2)]] + @test TMLE.get_treatment_settings(IATE, treatments_unique_values) == [[(1, 0), (0, 2)]] + @test TMLE.get_treatment_settings(CM, treatments_unique_values) == ((1, 0, 2), ) treatments_unique_values = (T₁=(1, 0, 2), T₂=["AC", "CC"]) - @test TMLE.get_transitive_treatments_contrasts(treatments_unique_values) == [[(1, 0), (0, 2)], [("AC", "CC")]] + @test TMLE.get_treatment_settings(ATE, treatments_unique_values) == [[(1, 0), (0, 2)], [("AC", "CC")]] + @test TMLE.get_treatment_settings(IATE, treatments_unique_values) == [[(1, 0), (0, 2)], [("AC", "CC")]] + @test TMLE.get_treatment_settings(CM, treatments_unique_values) == ((1, 0, 2), ["AC", "CC"]) end @testset "Test unique_treatment_values" begin @@ -220,7 +224,44 @@ end ) end -@testset "Test factorialATE" begin +@testset "Test factorial CM" begin + dataset = ( + T₁ = [0, 1, 2, missing], + T₂ = ["AC", "CC", missing, "AA"], + W₁ = [1, 2, 3, 4], + W₂ = [1, 2, 3, 4], + C = [1, 2, 3, 4], + Y₁ = [1, 2, 3, 4], + Y₂ = [1, 2, 3, 4] + ) + composedCM = factorialEstimand(CM, dataset, [:T₁], :Y₁, verbosity=0) + @test composedCM == TMLE.ComposedEstimand( + TMLE.joint_estimand, + ( + TMLE.CausalCM(:Y₁, (T₁ = 0,)), + TMLE.CausalCM(:Y₁, (T₁ = 1,)), + TMLE.CausalCM(:Y₁, (T₁ = 2,)) + ) + ) + + composedCM = factorialEstimand(CM, dataset, [:T₁, :T₂], :Y₁, verbosity=0) + @test composedCM == TMLE.ComposedEstimand( + TMLE.joint_estimand, + ( + TMLE.CausalCM(:Y₁, (T₁ = 0, T₂ = "AC")), + TMLE.CausalCM(:Y₁, (T₁ = 1, T₂ = "AC")), + TMLE.CausalCM(:Y₁, (T₁ = 2, T₂ = "AC")), + TMLE.CausalCM(:Y₁, (T₁ = 0, T₂ = "CC")), + TMLE.CausalCM(:Y₁, (T₁ = 1, T₂ = "CC")), + TMLE.CausalCM(:Y₁, (T₁ = 2, T₂ = "CC")), + TMLE.CausalCM(:Y₁, (T₁ = 0, T₂ = "AA")), + TMLE.CausalCM(:Y₁, (T₁ = 1, T₂ = "AA")), + TMLE.CausalCM(:Y₁, (T₁ = 2, T₂ = "AA")) + ) + ) +end + +@testset "Test factorial ATE" begin dataset = ( T₁ = [0, 1, 2, missing], T₂ = ["AC", "CC", missing, "AA"], @@ -231,7 +272,7 @@ end Y₂ = [1, 2, 3, 4] ) # No confounders, 1 treatment, no extra covariate: 3 causal ATEs - composedATE = factorialATE(dataset, [:T₁], :Y₁) + composedATE = factorialEstimand(ATE, dataset, [:T₁], :Y₁, verbosity=0) @test composedATE == ComposedEstimand( TMLE.joint_estimand, ( @@ -240,9 +281,10 @@ end ) ) # 2 treatments - composedATE = factorialATE(dataset, [:T₁, :T₂], :Y₁; + composedATE = factorialEstimand(ATE, dataset, [:T₁, :T₂], :Y₁; confounders=[:W₁, :W₂], - outcome_extra_covariates=[:C] + outcome_extra_covariates=[:C], + verbosity=0 ) ## 4 expected different treatment settings @test composedATE == ComposedEstimand( @@ -275,15 +317,16 @@ end ) ) # positivity constraint - composedATE = factorialATE(dataset, [:T₁, :T₂], :Y₁; + composedATE = factorialEstimand(ATE, dataset, [:T₁, :T₂], :Y₁; confounders=[:W₁, :W₂], outcome_extra_covariates=[:C], - positivity_constraint=0.1 + positivity_constraint=0.1, + verbosity=0 ) @test length(composedATE.args) == 1 end -@testset "Test factorialIATE" begin +@testset "Test factorial IATE" begin dataset = ( T₁ = [0, 1, 2, missing], T₂ = ["AC", "CC", missing, "AA"], @@ -294,9 +337,10 @@ end Y₂ = [1, 2, 3, 4] ) # From dataset - composedIATE = factorialIATE(dataset, [:T₁, :T₂], :Y₁, + composedIATE = factorialEstimand(IATE, dataset, [:T₁, :T₂], :Y₁, confounders=[:W₁], - outcome_extra_covariates=[:C] + outcome_extra_covariates=[:C], + verbosity=0 ) @test composedIATE == ComposedEstimand( TMLE.joint_estimand, @@ -328,7 +372,7 @@ end ) ) # From unique values - composedIATE = factorialIATE((T₁ = (0, 1), T₂=(0, 1, 2), T₃=(0, 1, 2)), :Y₁) + composedIATE = factorialEstimand(IATE, (T₁ = (0, 1), T₂=(0, 1, 2), T₃=(0, 1, 2)), :Y₁, verbosity=0) @test composedIATE == ComposedEstimand( TMLE.joint_estimand, ( @@ -352,15 +396,41 @@ end ) # positivity constraint - composedIATE = factorialIATE(dataset, [:T₁, :T₂], :Y₁, + composedIATE = factorialEstimand(IATE, dataset, [:T₁, :T₂], :Y₁, confounders=[:W₁], outcome_extra_covariates=[:C], - positivity_constraint=0.1 + positivity_constraint=0.1, + verbosity=0 ) @test length(composedIATE.args) == 0 end - +@testset "Test factorialEstimands" begin + dataset = ( + T₁ = [0, 1, 2, missing], + T₂ = ["AC", "CC", missing, "AA"], + W₁ = [1, 2, 3, 4], + W₂ = [1, 2, 3, 4], + C = [1, 2, 3, 4], + Y₁ = [1, 2, 3, 4], + Y₂ = [1, 2, 3, 4] + ) + factorial_ates = factorialEstimands(ATE, dataset, [:T₁, :T₂], [:Y₁, :Y₂], + confounders=[:W₁, :W₂], + outcome_extra_covariates=[:C], + positivity_constraint=0.1, + verbosity=0 + ) + @test length(factorial_ates) == 2 + # Nothing passes the threshold + factorial_ates = factorialEstimands(ATE, dataset, [:T₁, :T₂], [:Y₁, :Y₂], + confounders=[:W₁, :W₂], + outcome_extra_covariates=[:C], + positivity_constraint=0.3, + verbosity=0 + ) + @test length(factorial_ates) == 0 +end end true \ No newline at end of file