From 29b77823ba392518720595679dbd5f3bed1222a8 Mon Sep 17 00:00:00 2001 From: Juliet Asantewaa Sarpong Date: Thu, 19 Jun 2025 12:00:39 +0100 Subject: [PATCH 01/20] add lasso_strategy --- .../lasso_strategy.jl | 211 ++++++++++++++++++ .../lasso_strategy.jl | 0 2 files changed, 211 insertions(+) create mode 100644 src/counterfactual_mean_based/lasso_strategy.jl create mode 100644 test/counterfactual_mean_based/lasso_strategy.jl diff --git a/src/counterfactual_mean_based/lasso_strategy.jl b/src/counterfactual_mean_based/lasso_strategy.jl new file mode 100644 index 00000000..e5be0590 --- /dev/null +++ b/src/counterfactual_mean_based/lasso_strategy.jl @@ -0,0 +1,211 @@ +################################################################## +### LassoStrategy ### +################################################################## + +using GLMNet +using DataFrames +using StatsFuns +using ..TMLE +using Statistics + +""" + LassoCTMLE <: CollaborativeStrategy + +Lasso-based Collaborative TMLE strategy using cross-validated lambda selection. +""" +struct LassoCTMLE <: CollaborativeStrategy + lambda_path::Vector{Float64} + cv_folds::Int + best_lambda::Union{Nothing,Float64} + losses::Vector{Float64} + confounders::Vector{Symbol} + patience::Int + function LassoCTMLE(; + lambda_path = exp10.(range(-4, stop = 0, length = 50)), + cv_folds = 5, + confounders = Symbol[], + patience = 3, + ) + isempty(confounders) && + throw(ArgumentError("Must specify confounders for LassoCTMLE")) + new(lambda_path, cv_folds, nothing, Float64[], confounders, patience) + end +end + +""" + stratified_kfold(y::AbstractVector, k::Int) + +Returns a vector of (train_idx, test_idx) tuples for stratified k-fold cross-validation, +where `y` contains class labels. +""" +function stratified_kfold(y::AbstractVector, k::Int) + idx_by_class = Dict{eltype(y), Vector{Int}}() + for (i, label) in enumerate(y) + push!(get!(idx_by_class, label, Int[]), i) + end + + folds = [Int[] for _ in 1:k] + for idxs in values(idx_by_class) + shuffled = shuffle(idxs) + for (i, idx) in enumerate(shuffled) + push!(folds[mod1(i, k)], idx) + end + end + + result = Vector{Tuple{Vector{Int}, Vector{Int}}}(undef, k) + all_idxs = collect(1:length(y)) + for i in 1:k + test_idx = folds[i] + train_idx = setdiff(all_idxs, test_idx) + result[i] = (train_idx, test_idx) + end + return result +end + +function initialise!(strategy::LassoCTMLE, Ψ) + strategy.best_lambda = nothing + empty!(strategy.losses) + return nothing +end + +function update!(strategy::LassoCTMLE, last_targeted_η̂ₙ, dataset) + return nothing +end + +finalise!(strategy::LassoCTMLE) = nothing + +function exhausted(strategy::LassoCTMLE) + strategy.best_lambda !== nothing +end + +""" + propensity_score(Ψ, strategy::LassoCTMLE) + +Returns a function which, given a dataset, fits LASSO at the chosen lambda and predicts propensity scores. +This method is generic: it works for any valid Ψ parameter by using TMLE.treatment(Ψ). +""" +function propensity_score(Ψ, strategy::LassoCTMLE) + function fit_predict(dataset; lambda_override = nothing) + W = Matrix(select(dataset, strategy.confounders)) + A = dataset[!, treatment(Ψ)] + λ = isnothing(lambda_override) ? [strategy.best_lambda] : [lambda_override] + g_fit = glmnet(W, A, Binomial(), lambda = λ) + g_pred = GLMNet.predict(g_fit, W, lambda = λ[1]) + clamp.(g_pred, 0.01, 0.99) # stabilize + end + return fit_predict +end + +""" + crossvalidate_lambda(strategy, Ψ, dataset, cv_folds) + +Selects the best lambda from the path via cross-validated log-loss. +""" +function crossvalidate_lambda(strategy::LassoCTMLE, Ψ, dataset, cv_folds) + W = Matrix(select(dataset, strategy.confounders)) + A = dataset[!, treatment(Ψ)] + folds = stratified_kfold(A, cv_folds) + losses = zeros(length(strategy.lambda_path)) + for (train_idx, val_idx) in folds + W_train, W_val = W[train_idx, :], W[val_idx, :] + A_train, A_val = A[train_idx], A[val_idx] + g_fit = glmnet(W_train, A_train, Binomial(), lambda = strategy.lambda_path) + for (i, λ) in enumerate(g_fit.lambda) + g_pred = GLMNet.predict(g_fit, W_val, lambda = λ) + g_pred = clamp.(g_pred, 0.01, 0.99) + # Log-loss + losses[i] += -mean(A_val .* log.(g_pred) .+ (1 .- A_val) .* log.(1 .- g_pred)) + end + end + avg_losses = losses ./ cv_folds + best_idx = argmin(avg_losses) + return strategy.lambda_path[best_idx], avg_losses +end + +""" + LassoCTMLEIterator + +Once best lambda is chosen, this iterator provides a single candidate fit at that lambda. +""" +struct LassoCTMLEIterator + strategy::LassoCTMLE + Ψ::Any + dataset::Any + models::Any + last_targeted_η̂ₙ::Any +end + +function Base.iterate(it::LassoCTMLEIterator, state = 1) + if state > 1 + return nothing + end + strategy, Ψ, dataset = it.strategy, it.Ψ, it.dataset + W = Matrix(select(dataset, strategy.confounders)) + A = dataset[!, treatment(Ψ)] + λ = strategy.best_lambda + g_fit = glmnet(W, A, Binomial(), lambda = [λ]) + ĝ = + (g, dataset; kwargs...) -> begin + g_pred = GLMNet.predict( + g, + Matrix(select(dataset, strategy.confounders)), + lambda = λ, + ) + clamp.(g_pred, 0.01, 0.99) + end + return ((g_fit, ĝ), state+1) +end + +""" + step_k_best_candidate( + collaborative_strategy::LassoCTMLE, + Ψ, dataset, models, fluctuation_model, last_targeted_η̂ₙ, last_loss; + ...kwargs... + ) + +Pipeline step: cross-validates lambda, sets best_lambda, fits candidate at best lambda, and targets using TMLE fluctuation machinery. +NOTE: This strategy does NOT compute clever covariate or target the parameter directly. + It only provides a g-model fit (propensity score model) using LASSO. + TMLE.jl machinery handles clever covariate and targeting for any valid Ψ. +""" +function step_k_best_candidate( + collaborative_strategy::LassoCTMLE, + Ψ, + dataset, + models, + fluctuation_model, + last_targeted_η̂ₙ, + last_loss; + verbosity = 1, + cache = Dict(), + machine_cache = false, + acceleration = CPU1(), +) + + best_lambda, avg_losses = crossvalidate_lambda( + collaborative_strategy, + Ψ, + dataset, + collaborative_strategy.cv_folds, + ) + collaborative_strategy.best_lambda = best_lambda + collaborative_strategy.losses = avg_losses + + iterator = + LassoCTMLEIterator(collaborative_strategy, Ψ, dataset, models, last_targeted_η̂ₙ) + (g, ĝ), _ = iterate(iterator, 1) + + ĝₙ = ĝ(g, dataset) + targeted_η̂ₙ, loss = TMLE.get_new_targeted_candidate( + last_targeted_η̂ₙ, + ĝₙ, + fluctuation_model, + dataset; + verbosity = verbosity-1, + cache = cache, + machine_cache = machine_cache, + ) + + return g, ĝ, targeted_η̂ₙ, loss, false +end + diff --git a/test/counterfactual_mean_based/lasso_strategy.jl b/test/counterfactual_mean_based/lasso_strategy.jl new file mode 100644 index 00000000..e69de29b From 57e1aee070af66788d4a2bdfa05e8ad10160aa3a Mon Sep 17 00:00:00 2001 From: Juliet Asantewaa Sarpong Date: Thu, 19 Jun 2025 15:49:30 +0100 Subject: [PATCH 02/20] data generation for test --- .../lasso_strategy.jl | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/counterfactual_mean_based/lasso_strategy.jl b/test/counterfactual_mean_based/lasso_strategy.jl index e69de29b..fafce16a 100644 --- a/test/counterfactual_mean_based/lasso_strategy.jl +++ b/test/counterfactual_mean_based/lasso_strategy.jl @@ -0,0 +1,32 @@ +using Random, Distributions, LinearAlgebra, DataFrames + +function sim3(n::Int=1000, p::Int=100, rho::Float64=0.9, k::Int=20, amplitude::Float64=1, amplitude2::Float64=1, k2::Int=20) + function toeplitz_cov(p, rho) + return toeplitz(rho .^ (0:(p-1))) + end + + Sigma = toeplitz_cov(p, rho) + + mu = zeros(Float64, p) + W = (rand(MvNormal(mu, Sigma), n))' + W = (W .- mean(W, dims=1)) ./ std(W, dims=1) + + nonzero = sample(1:p, k, replace=false) + sign = sample([-1, 1], p, replace=true) + gamma_ = amplitude .* sign .* in(1:p, nonzero) + + nonzero2 = sample(1:p, k2, replace=false) + sign2 = sample([-1, 1], p, replace=true) + beta_ = amplitude2 .* sign2 .* in(1:p, nonzero2) + logit_p = W * beta_ + prob_A = 1 ./ (1 .+ exp.(-logit_p)) + A = rand(Binomial(1, prob_A)) + + Y = 2 .* A .+ W * gamma_ .+ randn(n) + + data = DataFrame(hcat(W, A, Y)) + names!(data, [string("W", i) for i in 1:p]..., "A", "Y") + + return data +end + From a157e755132feeca24e94042084c90093457a065 Mon Sep 17 00:00:00 2001 From: Juliet Asantewaa Sarpong Date: Wed, 2 Jul 2025 13:04:40 +0100 Subject: [PATCH 03/20] test lassoCTMLE --- Project.toml | 6 +- src/TMLE.jl | 2 + .../lasso_strategy.jl | 61 ++++++++++--------- 3 files changed, 39 insertions(+), 30 deletions(-) diff --git a/Project.toml b/Project.toml index 4a7e6120..55823c67 100644 --- a/Project.toml +++ b/Project.toml @@ -5,6 +5,7 @@ version = "0.19.0" [deps] AutoHashEquals = "15f4f7f2-30c1-5605-9d31-71845cf9641f" +CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597" Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" ComputationalResources = "ed09eef8-17a6-5b46-8889-db040fac31e3" @@ -14,6 +15,7 @@ Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" GLM = "38e38edf-8417-5370-95a0-9cbb8c7f171a" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" HypothesisTests = "09f84164-cd44-5f33-b23f-e6b0d136a0d5" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" LogExpFunctions = "2ab3a3ac-af41-5b50-aa03-7779005ae688" MLJBase = "a7f614a8-145f-11e9-1d2a-a57a1082229d" MLJGLMInterface = "caf8df21-4939-456d-ac9c-5fefbfb04c0c" @@ -28,12 +30,11 @@ StatisticalMeasures = "a19d573c-0a75-4610-95b3-7071388c7541" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" TableOperations = "ab02a1b2-a7df-11e8-156e-fb1833f50b87" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" +YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" [weakdeps] CausalTables = "6af48e0c-efc2-4bf7-a92f-a553ccf79fd6" -JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" [extensions] CausalTablesExt = "CausalTables" @@ -42,6 +43,7 @@ YAMLExt = "YAML" [compat] AutoHashEquals = "2.1.0" +CSV = "0.10.15" CategoricalArrays = "0.10" CausalTables = "1.2.1" Combinatorics = "1.0.2" diff --git a/src/TMLE.jl b/src/TMLE.jl index 75903d82..a992f26c 100644 --- a/src/TMLE.jl +++ b/src/TMLE.jl @@ -47,6 +47,7 @@ export Configuration export brute_force_ordering, groups_ordering export gradients, epsilons, estimates export AdaptiveCorrelationStrategy, GreedyStrategy +export LassoCTMLE export CausalStratifiedCV, CV, StratifiedCV, Holdout export CPUThreads, CPU1 @@ -69,6 +70,7 @@ include("counterfactual_mean_based/fluctuation.jl") include("counterfactual_mean_based/collaborative_template.jl") include("counterfactual_mean_based/nuisance_estimators.jl") include("counterfactual_mean_based/covariate_based_strategies.jl") +include("counterfactual_mean_based/lasso_strategy.jl") include("counterfactual_mean_based/estimators.jl") include("counterfactual_mean_based/clever_covariate.jl") include("counterfactual_mean_based/gradient.jl") diff --git a/test/counterfactual_mean_based/lasso_strategy.jl b/test/counterfactual_mean_based/lasso_strategy.jl index fafce16a..2984e2ee 100644 --- a/test/counterfactual_mean_based/lasso_strategy.jl +++ b/test/counterfactual_mean_based/lasso_strategy.jl @@ -1,32 +1,37 @@ -using Random, Distributions, LinearAlgebra, DataFrames +using Test +using TMLE +using CSV +using DataFrames +using CategoricalArrays -function sim3(n::Int=1000, p::Int=100, rho::Float64=0.9, k::Int=20, amplitude::Float64=1, amplitude2::Float64=1, k2::Int=20) - function toeplitz_cov(p, rho) - return toeplitz(rho .^ (0:(p-1))) +function non_regression_dataset() + dataset = CSV.read( + joinpath(dirname(dirname(pathof(TMLE))), "test", "data", "perinatal.csv"), + DataFrame, + missingstring=["", "NA"] + ) + confounders = [:apgar1, :apgar5, :gagebrth, :mage, :meducyrs, :sexn] + dataset.haz01 = categorical(dataset.haz01) + dataset.parity01 = categorical(dataset.parity01, ordered=true) + for col in confounders + dataset[!, col] = float(dataset[!, col]) end - - Sigma = toeplitz_cov(p, rho) - - mu = zeros(Float64, p) - W = (rand(MvNormal(mu, Sigma), n))' - W = (W .- mean(W, dims=1)) ./ std(W, dims=1) - - nonzero = sample(1:p, k, replace=false) - sign = sample([-1, 1], p, replace=true) - gamma_ = amplitude .* sign .* in(1:p, nonzero) - - nonzero2 = sample(1:p, k2, replace=false) - sign2 = sample([-1, 1], p, replace=true) - beta_ = amplitude2 .* sign2 .* in(1:p, nonzero2) - logit_p = W * beta_ - prob_A = 1 ./ (1 .+ exp.(-logit_p)) - A = rand(Binomial(1, prob_A)) - - Y = 2 .* A .+ W * gamma_ .+ randn(n) - - data = DataFrame(hcat(W, A, Y)) - names!(data, [string("W", i) for i in 1:p]..., "A", "Y") - - return data + return dataset, confounders end +@testset "LassoCTMLE on perinatal dataset" begin + dataset, confounders = non_regression_dataset() + Ψ = ATE( + outcome=:haz01, + treatment_values=(parity01=(case=1, control=0),), + treatment_confounders=(parity01=confounders,) + ) + lasso_estimator = Tmle( + collaborative_strategy=LassoCTMLE( + confounders=confounders + ) + ) + lasso_result, _ = lasso_estimator(Ψ, dataset; verbosity=0) + @test !isnan(estimate(lasso_result)) + @info "LassoCTMLE estimate on perinatal data:" estimate(lasso_result) +end \ No newline at end of file From e8fddcea1cd06eef9988eaa945edf1274df21de6 Mon Sep 17 00:00:00 2001 From: Olivier Labayle Date: Wed, 2 Jul 2025 13:34:21 +0100 Subject: [PATCH 04/20] fix some problems --- Project.toml | 10 ++++++---- src/TMLE.jl | 2 ++ src/counterfactual_mean_based/lasso_strategy.jl | 6 ------ 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Project.toml b/Project.toml index 55823c67..35c0fcec 100644 --- a/Project.toml +++ b/Project.toml @@ -5,7 +5,6 @@ version = "0.19.0" [deps] AutoHashEquals = "15f4f7f2-30c1-5605-9d31-71845cf9641f" -CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597" Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" ComputationalResources = "ed09eef8-17a6-5b46-8889-db040fac31e3" @@ -13,9 +12,9 @@ DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" GLM = "38e38edf-8417-5370-95a0-9cbb8c7f171a" +GLMNet = "8d5ece8b-de18-5317-b113-243142960cc6" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" HypothesisTests = "09f84164-cd44-5f33-b23f-e6b0d136a0d5" -JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" LogExpFunctions = "2ab3a3ac-af41-5b50-aa03-7779005ae688" MLJBase = "a7f614a8-145f-11e9-1d2a-a57a1082229d" MLJGLMInterface = "caf8df21-4939-456d-ac9c-5fefbfb04c0c" @@ -28,13 +27,15 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SplitApplyCombine = "03a91e81-4c3e-53e1-a0a4-9c0c8f19dd66" StatisticalMeasures = "a19d573c-0a75-4610-95b3-7071388c7541" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +StatsFuns = "4c63d2b9-4356-54db-8cca-17b64c39e42c" TableOperations = "ab02a1b2-a7df-11e8-156e-fb1833f50b87" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" -YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" [weakdeps] CausalTables = "6af48e0c-efc2-4bf7-a92f-a553ccf79fd6" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" [extensions] CausalTablesExt = "CausalTables" @@ -43,7 +44,6 @@ YAMLExt = "YAML" [compat] AutoHashEquals = "2.1.0" -CSV = "0.10.15" CategoricalArrays = "0.10" CausalTables = "1.2.1" Combinatorics = "1.0.2" @@ -52,6 +52,7 @@ DataFrames = "1.7.0" DifferentiationInterface = "0.6.43" Distributions = "0.25" GLM = "1.8.2" +GLMNet = "0.7.4" Graphs = "1.8" HypothesisTests = "0.10, 0.11" JSON = "0.21.4" @@ -65,6 +66,7 @@ OrderedCollections = "1.6.3" Printf = "1.11.0,1.10.9" SplitApplyCombine = "1.2.2" StatisticalMeasures = "0.2" +StatsFuns = "1.5.0" TableOperations = "1.2" Tables = "1.6" YAML = "0.4.9" diff --git a/src/TMLE.jl b/src/TMLE.jl index a992f26c..81a5485e 100644 --- a/src/TMLE.jl +++ b/src/TMLE.jl @@ -26,6 +26,8 @@ using DataFrames using ComputationalResources using Base.Threads using Printf +using GLMNet +using StatsFuns # ############################################################################# # EXPORTS diff --git a/src/counterfactual_mean_based/lasso_strategy.jl b/src/counterfactual_mean_based/lasso_strategy.jl index e5be0590..f941ab4b 100644 --- a/src/counterfactual_mean_based/lasso_strategy.jl +++ b/src/counterfactual_mean_based/lasso_strategy.jl @@ -2,12 +2,6 @@ ### LassoStrategy ### ################################################################## -using GLMNet -using DataFrames -using StatsFuns -using ..TMLE -using Statistics - """ LassoCTMLE <: CollaborativeStrategy From fb744d7560a6c3e2a3b19c0c6126457fcf9cbc24 Mon Sep 17 00:00:00 2001 From: Juliet Asantewaa Sarpong Date: Mon, 7 Jul 2025 08:26:21 +0100 Subject: [PATCH 05/20] update packages --- src/TMLE.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/TMLE.jl b/src/TMLE.jl index 81a5485e..a992f26c 100644 --- a/src/TMLE.jl +++ b/src/TMLE.jl @@ -26,8 +26,6 @@ using DataFrames using ComputationalResources using Base.Threads using Printf -using GLMNet -using StatsFuns # ############################################################################# # EXPORTS From e7441c3e093d9a63b5c03bf030274969b21dfb3c Mon Sep 17 00:00:00 2001 From: Juliet Asantewaa Sarpong Date: Wed, 9 Jul 2025 13:31:36 +0100 Subject: [PATCH 06/20] testing lasso strategy --- Project.toml | 4 +- src/TMLE.jl | 2 + .../lasso_strategy.jl | 65 ++++++++----------- .../lasso_strategy.jl | 37 ----------- .../lasso_strategy_test.jl | 57 ++++++++++++++++ 5 files changed, 87 insertions(+), 78 deletions(-) delete mode 100644 test/counterfactual_mean_based/lasso_strategy.jl create mode 100644 test/counterfactual_mean_based/lasso_strategy_test.jl diff --git a/Project.toml b/Project.toml index 35c0fcec..2a88f1f7 100644 --- a/Project.toml +++ b/Project.toml @@ -27,9 +27,9 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SplitApplyCombine = "03a91e81-4c3e-53e1-a0a4-9c0c8f19dd66" StatisticalMeasures = "a19d573c-0a75-4610-95b3-7071388c7541" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" -StatsFuns = "4c63d2b9-4356-54db-8cca-17b64c39e42c" TableOperations = "ab02a1b2-a7df-11e8-156e-fb1833f50b87" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" +ToeplitzMatrices = "c751599d-da0a-543b-9d20-d0a503d91d24" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" [weakdeps] @@ -66,9 +66,9 @@ OrderedCollections = "1.6.3" Printf = "1.11.0,1.10.9" SplitApplyCombine = "1.2.2" StatisticalMeasures = "0.2" -StatsFuns = "1.5.0" TableOperations = "1.2" Tables = "1.6" +ToeplitzMatrices = "0.8.5" YAML = "0.4.9" Zygote = "0.7" julia = "1.10, 1" diff --git a/src/TMLE.jl b/src/TMLE.jl index a992f26c..12a662e9 100644 --- a/src/TMLE.jl +++ b/src/TMLE.jl @@ -23,6 +23,8 @@ using OrderedCollections using AutoHashEquals using StatisticalMeasures using DataFrames +using GLMNet +using ToeplitzMatrices using ComputationalResources using Base.Threads using Printf diff --git a/src/counterfactual_mean_based/lasso_strategy.jl b/src/counterfactual_mean_based/lasso_strategy.jl index f941ab4b..d745d880 100644 --- a/src/counterfactual_mean_based/lasso_strategy.jl +++ b/src/counterfactual_mean_based/lasso_strategy.jl @@ -1,7 +1,7 @@ ################################################################## ### LassoStrategy ### ################################################################## - +using DataFrames: select """ LassoCTMLE <: CollaborativeStrategy @@ -14,15 +14,17 @@ struct LassoCTMLE <: CollaborativeStrategy losses::Vector{Float64} confounders::Vector{Symbol} patience::Int - function LassoCTMLE(; + dataset::Any + function LassoCTMLE(; lambda_path = exp10.(range(-4, stop = 0, length = 50)), cv_folds = 5, confounders = Symbol[], patience = 3, + dataset = nothing ) isempty(confounders) && throw(ArgumentError("Must specify confounders for LassoCTMLE")) - new(lambda_path, cv_folds, nothing, Float64[], confounders, patience) + new(lambda_path, cv_folds, nothing, Float64[], confounders, patience, dataset) end end @@ -73,21 +75,18 @@ function exhausted(strategy::LassoCTMLE) end """ - propensity_score(Ψ, strategy::LassoCTMLE) + propensity_score(Ψ, strategy::LassoCTMLE, dataset) -Returns a function which, given a dataset, fits LASSO at the chosen lambda and predicts propensity scores. -This method is generic: it works for any valid Ψ parameter by using TMLE.treatment(Ψ). +Fits LASSO at the chosen lambda and returns a ConditionalDistribution object. """ -function propensity_score(Ψ, strategy::LassoCTMLE) - function fit_predict(dataset; lambda_override = nothing) - W = Matrix(select(dataset, strategy.confounders)) - A = dataset[!, treatment(Ψ)] - λ = isnothing(lambda_override) ? [strategy.best_lambda] : [lambda_override] - g_fit = glmnet(W, A, Binomial(), lambda = λ) - g_pred = GLMNet.predict(g_fit, W, lambda = λ[1]) - clamp.(g_pred, 0.01, 0.99) # stabilize - end - return fit_predict +function propensity_score(Ψ::TMLE.StatisticalATE, strategy::LassoCTMLE) + dataset = strategy.dataset + W = Matrix(select(dataset, strategy.confounders)) + A_cat = dataset[!, first(keys(Ψ.treatment_values))] + A = Float64.([x for x in A_cat]) + λ = isnothing(strategy.best_lambda) ? [strategy.lambda_path[1]] : [strategy.best_lambda] + g_fit = glmnet(W, A, Binomial(); lambda = λ) + return ConditionalDistribution(g_fit, strategy.confounders) end """ @@ -97,17 +96,17 @@ Selects the best lambda from the path via cross-validated log-loss. """ function crossvalidate_lambda(strategy::LassoCTMLE, Ψ, dataset, cv_folds) W = Matrix(select(dataset, strategy.confounders)) - A = dataset[!, treatment(Ψ)] + A_cat = dataset[!, first(keys(Ψ.treatment_values))] + A = Float64.([x for x in A_cat]) folds = stratified_kfold(A, cv_folds) losses = zeros(length(strategy.lambda_path)) for (train_idx, val_idx) in folds W_train, W_val = W[train_idx, :], W[val_idx, :] A_train, A_val = A[train_idx], A[val_idx] - g_fit = glmnet(W_train, A_train, Binomial(), lambda = strategy.lambda_path) + g_fit = glmnet(W_train, A_train, Binomial(); lambda = strategy.lambda_path) for (i, λ) in enumerate(g_fit.lambda) g_pred = GLMNet.predict(g_fit, W_val, lambda = λ) g_pred = clamp.(g_pred, 0.01, 0.99) - # Log-loss losses[i] += -mean(A_val .* log.(g_pred) .+ (1 .- A_val) .* log.(1 .- g_pred)) end end @@ -135,18 +134,11 @@ function Base.iterate(it::LassoCTMLEIterator, state = 1) end strategy, Ψ, dataset = it.strategy, it.Ψ, it.dataset W = Matrix(select(dataset, strategy.confounders)) - A = dataset[!, treatment(Ψ)] + A_cat = dataset[!, first(keys(Ψ.treatment_values))] + A = Float64.([x for x in A_cat]) λ = strategy.best_lambda - g_fit = glmnet(W, A, Binomial(), lambda = [λ]) - ĝ = - (g, dataset; kwargs...) -> begin - g_pred = GLMNet.predict( - g, - Matrix(select(dataset, strategy.confounders)), - lambda = λ, - ) - clamp.(g_pred, 0.01, 0.99) - end + g_fit = glmnet(W, A, Binomial(); lambda = [λ]) + ĝ = ConditionalDistribution(g_fit, strategy.confounders) return ((g_fit, ĝ), state+1) end @@ -158,9 +150,6 @@ end ) Pipeline step: cross-validates lambda, sets best_lambda, fits candidate at best lambda, and targets using TMLE fluctuation machinery. -NOTE: This strategy does NOT compute clever covariate or target the parameter directly. - It only provides a g-model fit (propensity score model) using LASSO. - TMLE.jl machinery handles clever covariate and targeting for any valid Ψ. """ function step_k_best_candidate( collaborative_strategy::LassoCTMLE, @@ -175,7 +164,6 @@ function step_k_best_candidate( machine_cache = false, acceleration = CPU1(), ) - best_lambda, avg_losses = crossvalidate_lambda( collaborative_strategy, Ψ, @@ -187,9 +175,9 @@ function step_k_best_candidate( iterator = LassoCTMLEIterator(collaborative_strategy, Ψ, dataset, models, last_targeted_η̂ₙ) - (g, ĝ), _ = iterate(iterator, 1) + (g, g_cd), _ = iterate(iterator, 1) - ĝₙ = ĝ(g, dataset) + ĝₙ = GLMNet.predict(g_cd, dataset) targeted_η̂ₙ, loss = TMLE.get_new_targeted_candidate( last_targeted_η̂ₙ, ĝₙ, @@ -200,6 +188,5 @@ function step_k_best_candidate( machine_cache = machine_cache, ) - return g, ĝ, targeted_η̂ₙ, loss, false -end - + return g, g_cd, targeted_η̂ₙ, loss, false +end \ No newline at end of file diff --git a/test/counterfactual_mean_based/lasso_strategy.jl b/test/counterfactual_mean_based/lasso_strategy.jl deleted file mode 100644 index 2984e2ee..00000000 --- a/test/counterfactual_mean_based/lasso_strategy.jl +++ /dev/null @@ -1,37 +0,0 @@ -using Test -using TMLE -using CSV -using DataFrames -using CategoricalArrays - -function non_regression_dataset() - dataset = CSV.read( - joinpath(dirname(dirname(pathof(TMLE))), "test", "data", "perinatal.csv"), - DataFrame, - missingstring=["", "NA"] - ) - confounders = [:apgar1, :apgar5, :gagebrth, :mage, :meducyrs, :sexn] - dataset.haz01 = categorical(dataset.haz01) - dataset.parity01 = categorical(dataset.parity01, ordered=true) - for col in confounders - dataset[!, col] = float(dataset[!, col]) - end - return dataset, confounders -end - -@testset "LassoCTMLE on perinatal dataset" begin - dataset, confounders = non_regression_dataset() - Ψ = ATE( - outcome=:haz01, - treatment_values=(parity01=(case=1, control=0),), - treatment_confounders=(parity01=confounders,) - ) - lasso_estimator = Tmle( - collaborative_strategy=LassoCTMLE( - confounders=confounders - ) - ) - lasso_result, _ = lasso_estimator(Ψ, dataset; verbosity=0) - @test !isnan(estimate(lasso_result)) - @info "LassoCTMLE estimate on perinatal data:" estimate(lasso_result) -end \ No newline at end of file diff --git a/test/counterfactual_mean_based/lasso_strategy_test.jl b/test/counterfactual_mean_based/lasso_strategy_test.jl new file mode 100644 index 00000000..eec6aa8f --- /dev/null +++ b/test/counterfactual_mean_based/lasso_strategy_test.jl @@ -0,0 +1,57 @@ +using Test +using TMLE +using Random +using Distributions +using LinearAlgebra +using DataFrames +using ToeplitzMatrices + +function simulate_highdim_lasso_data(n::Int=1000, p::Int=100, rho::Float64=0.9, k::Int=20, amplitude::Float64=1, amplitude2::Float64=1, k2::Int=20) + function toeplitz_cov(p, rho) + v = rho .^ (0:(p-1)) + return Matrix(Toeplitz(v,v)) + end + + Sigma = toeplitz_cov(p, rho) + mu = zeros(Float64, p) + W = (rand(MvNormal(mu, Sigma), n))' + W = (W .- mean(W, dims=1)) ./ std(W, dims=1) + + nonzero = sample(1:p, k, replace=false) + sign = sample([-1, 1], p, replace=true) + gamma_ = amplitude .* sign .* in(1:p, nonzero) + + nonzero2 = sample(1:p, k2, replace=false) + sign2 = sample([-1, 1], p, replace=true) + beta_ = amplitude2 .* sign2 .* in(1:p, nonzero2) + logit_p = W * beta_ + prob_A = 1 ./ (1 .+ exp.(-logit_p)) + A = [rand(Bernoulli(p)) for p in prob_A] + + Y = 2 .* A .+ W * gamma_ .+ randn(n) + + colnames = [string("W", i) for i in 1:p] + append!(colnames, ["A", "Y"]) + data = DataFrame(hcat(W, A, Y), colnames) + return data +end + +@testset "LassoCTMLE on simulated data" begin + Random.seed!(2024) + dataset = simulate_highdim_lasso_data(500, 10, 0.3, 3, 2.0, 2.0, 3) + confounders = Symbol.([string("W", i) for i in 1:10]) + Ψ = ATE( + outcome = :Y, + treatment_values = (A = (case = 1, control = 0),), + treatment_confounders = (A = confounders,) + ) + lasso_estimator = Tmle( + collaborative_strategy = LassoCTMLE( + confounders = confounders, + dataset = dataset + ) + ) + lasso_result, _ = lasso_estimator(Ψ, dataset; verbosity = 0) + @test !isnan(estimate(lasso_result)) + @info "LassoCTMLE estimate on simulated data:" estimate(lasso_result) +end \ No newline at end of file From 3293a91aba4c932790d20fcc7a6f2dfd4ba2330d Mon Sep 17 00:00:00 2001 From: Juliet Asantewaa Sarpong Date: Thu, 21 Aug 2025 13:02:53 +0100 Subject: [PATCH 07/20] glmnet update --- src/TMLE.jl | 3 +-- src/counterfactual_mean_based/lasso_strategy.jl | 13 ++++++------- .../{lasso_strategy_test.jl => lasso_strategy.jl} | 0 3 files changed, 7 insertions(+), 9 deletions(-) rename test/counterfactual_mean_based/{lasso_strategy_test.jl => lasso_strategy.jl} (100%) diff --git a/src/TMLE.jl b/src/TMLE.jl index 12a662e9..2b7e3978 100644 --- a/src/TMLE.jl +++ b/src/TMLE.jl @@ -23,8 +23,7 @@ using OrderedCollections using AutoHashEquals using StatisticalMeasures using DataFrames -using GLMNet -using ToeplitzMatrices +import GLMNet using ComputationalResources using Base.Threads using Printf diff --git a/src/counterfactual_mean_based/lasso_strategy.jl b/src/counterfactual_mean_based/lasso_strategy.jl index d745d880..439c9646 100644 --- a/src/counterfactual_mean_based/lasso_strategy.jl +++ b/src/counterfactual_mean_based/lasso_strategy.jl @@ -1,7 +1,6 @@ ################################################################## ### LassoStrategy ### ################################################################## -using DataFrames: select """ LassoCTMLE <: CollaborativeStrategy @@ -81,11 +80,11 @@ Fits LASSO at the chosen lambda and returns a ConditionalDistribution object. """ function propensity_score(Ψ::TMLE.StatisticalATE, strategy::LassoCTMLE) dataset = strategy.dataset - W = Matrix(select(dataset, strategy.confounders)) + W = Matrix(DataFrames.select(dataset, strategy.confounders)) A_cat = dataset[!, first(keys(Ψ.treatment_values))] A = Float64.([x for x in A_cat]) λ = isnothing(strategy.best_lambda) ? [strategy.lambda_path[1]] : [strategy.best_lambda] - g_fit = glmnet(W, A, Binomial(); lambda = λ) + g_fit = GLMNet.glmnet(W, A, Binomial(); lambda = λ) return ConditionalDistribution(g_fit, strategy.confounders) end @@ -95,7 +94,7 @@ end Selects the best lambda from the path via cross-validated log-loss. """ function crossvalidate_lambda(strategy::LassoCTMLE, Ψ, dataset, cv_folds) - W = Matrix(select(dataset, strategy.confounders)) + W = Matrix(DataFrames.select(dataset, strategy.confounders)) A_cat = dataset[!, first(keys(Ψ.treatment_values))] A = Float64.([x for x in A_cat]) folds = stratified_kfold(A, cv_folds) @@ -103,7 +102,7 @@ function crossvalidate_lambda(strategy::LassoCTMLE, Ψ, dataset, cv_folds) for (train_idx, val_idx) in folds W_train, W_val = W[train_idx, :], W[val_idx, :] A_train, A_val = A[train_idx], A[val_idx] - g_fit = glmnet(W_train, A_train, Binomial(); lambda = strategy.lambda_path) + g_fit = GLMNet.glmnet(W_train, A_train, Binomial(); lambda = strategy.lambda_path) for (i, λ) in enumerate(g_fit.lambda) g_pred = GLMNet.predict(g_fit, W_val, lambda = λ) g_pred = clamp.(g_pred, 0.01, 0.99) @@ -133,11 +132,11 @@ function Base.iterate(it::LassoCTMLEIterator, state = 1) return nothing end strategy, Ψ, dataset = it.strategy, it.Ψ, it.dataset - W = Matrix(select(dataset, strategy.confounders)) + W = Matrix(DataFrames.select(dataset, strategy.confounders)) A_cat = dataset[!, first(keys(Ψ.treatment_values))] A = Float64.([x for x in A_cat]) λ = strategy.best_lambda - g_fit = glmnet(W, A, Binomial(); lambda = [λ]) + g_fit = GLMNet.glmnet(W, A, Binomial(); lambda = [λ]) ĝ = ConditionalDistribution(g_fit, strategy.confounders) return ((g_fit, ĝ), state+1) end diff --git a/test/counterfactual_mean_based/lasso_strategy_test.jl b/test/counterfactual_mean_based/lasso_strategy.jl similarity index 100% rename from test/counterfactual_mean_based/lasso_strategy_test.jl rename to test/counterfactual_mean_based/lasso_strategy.jl From 2311e9b2ccbe313f09f4cbfa254ed39f2d2ed257 Mon Sep 17 00:00:00 2001 From: Juliet Asantewaa Sarpong Date: Thu, 21 Aug 2025 14:54:18 +0100 Subject: [PATCH 08/20] fix: add dataset argument --- .../lasso_strategy.jl | 18 +++++++++++------- .../lasso_strategy.jl | 3 +-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/counterfactual_mean_based/lasso_strategy.jl b/src/counterfactual_mean_based/lasso_strategy.jl index 439c9646..86c5b973 100644 --- a/src/counterfactual_mean_based/lasso_strategy.jl +++ b/src/counterfactual_mean_based/lasso_strategy.jl @@ -78,13 +78,12 @@ end Fits LASSO at the chosen lambda and returns a ConditionalDistribution object. """ -function propensity_score(Ψ::TMLE.StatisticalATE, strategy::LassoCTMLE) - dataset = strategy.dataset +function propensity_score(Ψ::TMLE.StatisticalATE, strategy::LassoCTMLE, dataset::DataFrame) W = Matrix(DataFrames.select(dataset, strategy.confounders)) A_cat = dataset[!, first(keys(Ψ.treatment_values))] A = Float64.([x for x in A_cat]) λ = isnothing(strategy.best_lambda) ? [strategy.lambda_path[1]] : [strategy.best_lambda] - g_fit = GLMNet.glmnet(W, A, Binomial(); lambda = λ) + g_fit = GLMNet.glmnet(W, A; lambda = λ) return ConditionalDistribution(g_fit, strategy.confounders) end @@ -102,9 +101,9 @@ function crossvalidate_lambda(strategy::LassoCTMLE, Ψ, dataset, cv_folds) for (train_idx, val_idx) in folds W_train, W_val = W[train_idx, :], W[val_idx, :] A_train, A_val = A[train_idx], A[val_idx] - g_fit = GLMNet.glmnet(W_train, A_train, Binomial(); lambda = strategy.lambda_path) + g_fit = GLMNet.glmnet(W_train, A_train; lambda = strategy.lambda_path) for (i, λ) in enumerate(g_fit.lambda) - g_pred = GLMNet.predict(g_fit, W_val, lambda = λ) + g_pred = GLMNet.predict(g_fit, W_val, lambda = [λ], type = :response) g_pred = clamp.(g_pred, 0.01, 0.99) losses[i] += -mean(A_val .* log.(g_pred) .+ (1 .- A_val) .* log.(1 .- g_pred)) end @@ -136,7 +135,7 @@ function Base.iterate(it::LassoCTMLEIterator, state = 1) A_cat = dataset[!, first(keys(Ψ.treatment_values))] A = Float64.([x for x in A_cat]) λ = strategy.best_lambda - g_fit = GLMNet.glmnet(W, A, Binomial(); lambda = [λ]) + g_fit = GLMNet.glmnet(W, A; lambda = [λ]) ĝ = ConditionalDistribution(g_fit, strategy.confounders) return ((g_fit, ĝ), state+1) end @@ -176,7 +175,12 @@ function step_k_best_candidate( LassoCTMLEIterator(collaborative_strategy, Ψ, dataset, models, last_targeted_η̂ₙ) (g, g_cd), _ = iterate(iterator, 1) - ĝₙ = GLMNet.predict(g_cd, dataset) + ĝₙ = GLMNet.predict( + g, + Matrix(DataFrames.select(dataset, collaborative_strategy.confounders)); + lambda = [collaborative_strategy.best_lambda], + type = :response + ) targeted_η̂ₙ, loss = TMLE.get_new_targeted_candidate( last_targeted_η̂ₙ, ĝₙ, diff --git a/test/counterfactual_mean_based/lasso_strategy.jl b/test/counterfactual_mean_based/lasso_strategy.jl index eec6aa8f..07af37b9 100644 --- a/test/counterfactual_mean_based/lasso_strategy.jl +++ b/test/counterfactual_mean_based/lasso_strategy.jl @@ -47,8 +47,7 @@ end ) lasso_estimator = Tmle( collaborative_strategy = LassoCTMLE( - confounders = confounders, - dataset = dataset + confounders = confounders ) ) lasso_result, _ = lasso_estimator(Ψ, dataset; verbosity = 0) From 95940ab53a7f12e1999e81e6608b8a67704ffa5f Mon Sep 17 00:00:00 2001 From: Juliet Asantewaa Sarpong Date: Thu, 21 Aug 2025 16:00:24 +0100 Subject: [PATCH 09/20] runtest & package update --- Project.toml | 2 -- test/Project.toml | 1 + test/runtests.jl | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index 2a88f1f7..7d80c297 100644 --- a/Project.toml +++ b/Project.toml @@ -29,7 +29,6 @@ StatisticalMeasures = "a19d573c-0a75-4610-95b3-7071388c7541" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" TableOperations = "ab02a1b2-a7df-11e8-156e-fb1833f50b87" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" -ToeplitzMatrices = "c751599d-da0a-543b-9d20-d0a503d91d24" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" [weakdeps] @@ -68,7 +67,6 @@ SplitApplyCombine = "1.2.2" StatisticalMeasures = "0.2" TableOperations = "1.2" Tables = "1.6" -ToeplitzMatrices = "0.8.5" YAML = "0.4.9" Zygote = "0.7" julia = "1.10, 1" diff --git a/test/Project.toml b/test/Project.toml index 5125ad92..61be732e 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -23,6 +23,7 @@ StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" TableOperations = "ab02a1b2-a7df-11e8-156e-fb1833f50b87" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +ToeplitzMatrices = "c751599d-da0a-543b-9d20-d0a503d91d24" YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" [compat] diff --git a/test/runtests.jl b/test/runtests.jl index 9934e7d6..e4c586a9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -26,6 +26,7 @@ TEST_DIR = joinpath(pkgdir(TMLE), "test") @test include(joinpath(TEST_DIR, "counterfactual_mean_based/3points_interactions.jl")) @test include(joinpath(TEST_DIR, "counterfactual_mean_based/collaborative_template.jl")) @test include(joinpath(TEST_DIR, "counterfactual_mean_based/covariate_based_strategies.jl")) + @test include(joinpath(TEST_DIR, "counterfactual_mean_based/lasso_strategy.jl")) # Test Extensions if VERSION >= v"1.9" From 6e960a3ce398da91951ba9e896bb9293e65cfad7 Mon Sep 17 00:00:00 2001 From: Juliet Asantewaa Sarpong Date: Tue, 16 Sep 2025 11:39:21 +0100 Subject: [PATCH 10/20] add LinearAlgebra[test env] --- test/Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Project.toml b/test/Project.toml index 61be732e..20a29849 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -7,6 +7,7 @@ DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" HypothesisTests = "09f84164-cd44-5f33-b23f-e6b0d136a0d5" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" LogExpFunctions = "2ab3a3ac-af41-5b50-aa03-7779005ae688" MLJBase = "a7f614a8-145f-11e9-1d2a-a57a1082229d" MLJGLMInterface = "caf8df21-4939-456d-ac9c-5fefbfb04c0c" From 0118948f44ab19c352a5da26d5496e34efb2a57e Mon Sep 17 00:00:00 2001 From: Juliet Asantewaa Sarpong Date: Thu, 18 Sep 2025 15:06:22 +0100 Subject: [PATCH 11/20] fix propensity_dcore --- src/counterfactual_mean_based/lasso_strategy.jl | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/counterfactual_mean_based/lasso_strategy.jl b/src/counterfactual_mean_based/lasso_strategy.jl index 86c5b973..d4752cf6 100644 --- a/src/counterfactual_mean_based/lasso_strategy.jl +++ b/src/counterfactual_mean_based/lasso_strategy.jl @@ -78,13 +78,8 @@ end Fits LASSO at the chosen lambda and returns a ConditionalDistribution object. """ -function propensity_score(Ψ::TMLE.StatisticalATE, strategy::LassoCTMLE, dataset::DataFrame) - W = Matrix(DataFrames.select(dataset, strategy.confounders)) - A_cat = dataset[!, first(keys(Ψ.treatment_values))] - A = Float64.([x for x in A_cat]) - λ = isnothing(strategy.best_lambda) ? [strategy.lambda_path[1]] : [strategy.best_lambda] - g_fit = GLMNet.glmnet(W, A; lambda = λ) - return ConditionalDistribution(g_fit, strategy.confounders) +function propensity_score(Ψ::TMLE.StatisticalATE, collaborative_strategy::LassoCTMLE) + return propensity_score(Ψ) end """ From 4086aa99376605aa0d78a2ef1a822dd8357aa032 Mon Sep 17 00:00:00 2001 From: Asantewaah Date: Tue, 23 Sep 2025 15:48:13 +0100 Subject: [PATCH 12/20] fix: ensure A is converted to Vector{Int} in simulate_highdim_lasso_data --- test/counterfactual_mean_based/lasso_strategy.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/counterfactual_mean_based/lasso_strategy.jl b/test/counterfactual_mean_based/lasso_strategy.jl index 07af37b9..770f87b7 100644 --- a/test/counterfactual_mean_based/lasso_strategy.jl +++ b/test/counterfactual_mean_based/lasso_strategy.jl @@ -27,17 +27,19 @@ function simulate_highdim_lasso_data(n::Int=1000, p::Int=100, rho::Float64=0.9, logit_p = W * beta_ prob_A = 1 ./ (1 .+ exp.(-logit_p)) A = [rand(Bernoulli(p)) for p in prob_A] + A = convert(Vector{Int}, A) Y = 2 .* A .+ W * gamma_ .+ randn(n) colnames = [string("W", i) for i in 1:p] append!(colnames, ["A", "Y"]) data = DataFrame(hcat(W, A, Y), colnames) + data.A = convert(Vector{Int}, data.A) return data end @testset "LassoCTMLE on simulated data" begin - Random.seed!(2024) + Random.seed!(2025) dataset = simulate_highdim_lasso_data(500, 10, 0.3, 3, 2.0, 2.0, 3) confounders = Symbol.([string("W", i) for i in 1:10]) Ψ = ATE( From 8ecca1f7d7f9a26c4c13796bbe2a5eadf5cb849e Mon Sep 17 00:00:00 2001 From: Asantewaah Date: Wed, 24 Sep 2025 14:54:14 +0100 Subject: [PATCH 13/20] fix: update simulate_highdim_lasso_data to use categorical for A and correct Y calculation --- .../lasso_strategy.jl | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/test/counterfactual_mean_based/lasso_strategy.jl b/test/counterfactual_mean_based/lasso_strategy.jl index 770f87b7..ae0265f8 100644 --- a/test/counterfactual_mean_based/lasso_strategy.jl +++ b/test/counterfactual_mean_based/lasso_strategy.jl @@ -2,6 +2,7 @@ using Test using TMLE using Random using Distributions +using CategoricalArrays using LinearAlgebra using DataFrames using ToeplitzMatrices @@ -28,31 +29,33 @@ function simulate_highdim_lasso_data(n::Int=1000, p::Int=100, rho::Float64=0.9, prob_A = 1 ./ (1 .+ exp.(-logit_p)) A = [rand(Bernoulli(p)) for p in prob_A] A = convert(Vector{Int}, A) + A = categorical(A) - Y = 2 .* A .+ W * gamma_ .+ randn(n) + Y = 2 .* levelcode.(A) .+ W * gamma_ .+ randn(n) colnames = [string("W", i) for i in 1:p] append!(colnames, ["A", "Y"]) data = DataFrame(hcat(W, A, Y), colnames) - data.A = convert(Vector{Int}, data.A) return data end -@testset "LassoCTMLE on simulated data" begin - Random.seed!(2025) - dataset = simulate_highdim_lasso_data(500, 10, 0.3, 3, 2.0, 2.0, 3) - confounders = Symbol.([string("W", i) for i in 1:10]) - Ψ = ATE( - outcome = :Y, - treatment_values = (A = (case = 1, control = 0),), - treatment_confounders = (A = confounders,) - ) - lasso_estimator = Tmle( - collaborative_strategy = LassoCTMLE( - confounders = confounders +@testset "LASSO strategy" begin + @testset "LassoCTMLE on simulated data" begin + Random.seed!(2025) + dataset = simulate_highdim_lasso_data(500, 10, 0.3, 3, 2.0, 2.0, 3) + confounders = Symbol.([string("W", i) for i in 1:10]) + Ψ = ATE( + outcome = :Y, + treatment_values = (A = (case = 1, control = 0),), + treatment_confounders = (A = confounders,) ) - ) - lasso_result, _ = lasso_estimator(Ψ, dataset; verbosity = 0) - @test !isnan(estimate(lasso_result)) - @info "LassoCTMLE estimate on simulated data:" estimate(lasso_result) + lasso_estimator = Tmle( + collaborative_strategy = LassoCTMLE( + confounders = confounders + ) + ) + lasso_result, _ = lasso_estimator(Ψ, dataset; verbosity = 0) + @test !isnan(estimate(lasso_result)) + @info "LassoCTMLE estimate on simulated data:" estimate(lasso_result) + end end \ No newline at end of file From ff6f894453dae2eb5f4d24b82bd467bebb676f9c Mon Sep 17 00:00:00 2001 From: Asantewaah <41793350+Asantewaah@users.noreply.github.com> Date: Wed, 24 Sep 2025 15:23:03 +0100 Subject: [PATCH 14/20] Refactor test file to simplify includes --- test/runtests.jl | 67 ++++++++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index e4c586a9..f9e56754 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,39 +1,38 @@ using Test using TMLE -TEST_DIR = joinpath(pkgdir(TMLE), "test") +const TEST_DIR = joinpath(pkgdir(TMLE), "test") +@testset "TMLE.jl" begin + # Test general functionality + include("utils.jl") + include("scm.jl") + include("adjustment.jl") + include("estimands.jl") + include("estimators_and_estimates.jl") + include("missing_management.jl") + include("composition.jl") + include("resampling.jl") -@time begin - # Test general functionality - @test include(joinpath(TEST_DIR, "utils.jl")) - @test include(joinpath(TEST_DIR, "scm.jl")) - @test include(joinpath(TEST_DIR, "adjustment.jl")) - @test include(joinpath(TEST_DIR, "estimands.jl")) - @test include(joinpath(TEST_DIR, "estimators_and_estimates.jl")) - @test include(joinpath(TEST_DIR, "missing_management.jl")) - @test include(joinpath(TEST_DIR, "composition.jl")) - @test include(joinpath(TEST_DIR, "resampling.jl")) - - # Test Counterfactual Mean Based Estimation - @test include(joinpath(TEST_DIR, "counterfactual_mean_based/estimands.jl")) - @test include(joinpath(TEST_DIR, "counterfactual_mean_based/clever_covariate.jl")) - @test include(joinpath(TEST_DIR, "counterfactual_mean_based/gradient.jl")) - @test include(joinpath(TEST_DIR, "counterfactual_mean_based/fluctuation.jl")) - @test include(joinpath(TEST_DIR, "counterfactual_mean_based/estimators_and_estimates.jl")) - @test include(joinpath(TEST_DIR, "counterfactual_mean_based/non_regression_test.jl")) - @test include(joinpath(TEST_DIR, "counterfactual_mean_based/double_robustness_ate.jl")) - @test include(joinpath(TEST_DIR, "counterfactual_mean_based/double_robustness_aie.jl")) - @test include(joinpath(TEST_DIR, "counterfactual_mean_based/3points_interactions.jl")) - @test include(joinpath(TEST_DIR, "counterfactual_mean_based/collaborative_template.jl")) - @test include(joinpath(TEST_DIR, "counterfactual_mean_based/covariate_based_strategies.jl")) - @test include(joinpath(TEST_DIR, "counterfactual_mean_based/lasso_strategy.jl")) - - # Test Extensions - if VERSION >= v"1.9" - @test include(joinpath(TEST_DIR, "configuration.jl")) - @test include(joinpath(TEST_DIR, "causaltables_interface.jl")) - end + # Test Counterfactual Mean Based Estimation + include("counterfactual_mean_based/estimands.jl") + include("counterfactual_mean_based/clever_covariate.jl") + include("counterfactual_mean_based/gradient.jl") + include("counterfactual_mean_based/fluctuation.jl") + include("counterfactual_mean_based/estimators_and_estimates.jl") + include("counterfactual_mean_based/non_regression_test.jl") + include("counterfactual_mean_based/double_robustness_ate.jl") + include("counterfactual_mean_based/double_robustness_aie.jl") + include("counterfactual_mean_based/3points_interactions.jl") + include("counterfactual_mean_based/collaborative_template.jl") + include("counterfactual_mean_based/covariate_based_strategies.jl") + include("counterfactual_mean_based/lasso_strategy.jl") - # Test Experimental - @test include(joinpath(TEST_DIR, "estimand_ordering.jl")) -end \ No newline at end of file + # Test Extensions + if VERSION >= v"1.9" + include("configuration.jl") + include("causaltables_interface.jl") + end + + # Test Experimental + include("estimand_ordering.jl") +end From 9d0d907234b376f33880f347e7481c1227477f55 Mon Sep 17 00:00:00 2001 From: Asantewaah Date: Wed, 1 Oct 2025 19:19:37 +0100 Subject: [PATCH 15/20] add LASSO CTMLE example , fixed code and test --- examples/lasso_example.jl | 122 +++++++ .../lasso_strategy.jl | 345 +++++++++++------- .../lasso_strategy.jl | 171 ++++++--- 3 files changed, 452 insertions(+), 186 deletions(-) create mode 100644 examples/lasso_example.jl diff --git a/examples/lasso_example.jl b/examples/lasso_example.jl new file mode 100644 index 00000000..2ae0cfb5 --- /dev/null +++ b/examples/lasso_example.jl @@ -0,0 +1,122 @@ +#!/usr/bin/env julia + +""" +Example: LASSO Collaborative TMLE + +Demonstrates CV-based variable selection in high-dimensional causal inference. +""" + +using Pkg +Pkg.activate(".") + +using TMLE +using Random +using DataFrames +using CategoricalArrays +using GLMNet +using Statistics +using Distributions +using LinearAlgebra +using StatsBase # For sample() function + +println("🧬 LASSO Collaborative TMLE Example") +println("=" ^ 50) + +Random.seed!(123) + +function sim3(; n=1000, p=100, rho=0.9, k=20, amplitude=1.0, amplitude2=1.0, k2=20) + """ + Generate high-dimensional data with correlated confounders + + Parameters: + - n: sample size + - p: number of confounders + - rho: correlation parameter for Toeplitz covariance + - k: number of non-zero coefficients for outcome model + - amplitude: amplitude for outcome coefficients + - amplitude2: amplitude for propensity score coefficients + - k2: number of non-zero coefficients for propensity score + """ + + function toeplitz_cov(p, rho) + return [rho^abs(i-j) for i in 1:p, j in 1:p] + end + + Sigma = toeplitz_cov(p, rho) + mv_normal = MvNormal(zeros(p), Sigma) + W_raw = rand(mv_normal, n)' + W = (W_raw .- mean(W_raw, dims=1)) ./ std(W_raw, dims=1) + + nonzero2 = sample(1:p, k2, replace=false) + signs2 = sample([-1, 1], p, replace=true) + beta = amplitude2 * signs2 .* [i in nonzero2 for i in 1:p] + + logit_p = W * beta + prob_A = 1 ./ (1 .+ exp.(-logit_p)) + A = rand.(Bernoulli.(prob_A)) + + nonzero = sample(1:p, k, replace=false) + signs = sample([-1, 1], p, replace=true) + gamma = amplitude * signs .* [i in nonzero for i in 1:p] + + Y = 2.0 * A + W * gamma + randn(n) + + W_df = DataFrame(W, [Symbol("W$i") for i in 1:p]) + data = hcat(W_df, DataFrame(A=categorical(A), Y=Y)) + + return data, nonzero, nonzero2 +end + +println("\n📊 Generating high-dimensional simulation data...") +n = 10000 +p = 50 +rho = 0.5 + +dataset, true_outcome_vars, true_ps_vars = sim3(n=n, p=p, rho=rho, k=20, k2=20) + +all_confounders = [Symbol("W$i") for i in 1:p] + +println("Generated dataset: $n observations, $p confounders") +println("True treatment effect: 2.0") +println("Treatment prevalence: $(round(mean(dataset.A .== 1), digits=3))") + +estimand = ATE( + outcome = :Y, + treatment_values = (A = (case = 1, control = 0),), + treatment_confounders = (A = all_confounders,) +) + +println("\n🔬 CAUSAL INFERENCE COMPARISON") +println("=" ^ 50) + +# Standard TMLE +println("\n1️⃣ Standard TMLE (uses all $p confounders)") +standard_estimator = Tmle() +standard_result, _ = standard_estimator(estimand, dataset; verbosity=0) +std_estimate = estimate(standard_result) +println(" Estimate: $(round(std_estimate, digits=3))") + +# LASSO CTMLE with cv lambda selection +println("\n2️⃣ LASSO CTMLE (cv lambda selection)") +lasso_strategy = LassoCTMLE( + confounders = all_confounders, + patience = 6, + alpha = 1.0 +) + +lasso_estimator = Tmle(collaborative_strategy = lasso_strategy) +lasso_result, _ = lasso_estimator(estimand, dataset; verbosity=1) +lasso_estimate = estimate(lasso_result) +println(" Estimate: $(round(lasso_estimate, digits=3))") + +println("\n📊 RESULTS SUMMARY") +println("=" ^ 50) +println("True treatment effect: 2.000") +println("Standard TMLE: $(round(std_estimate, digits=3))") +println("LASSO CTMLE: $(round(lasso_estimate, digits=3))") + +println("\nAbsolute deviations from truth:") +println("Standard TMLE: $(round(abs(std_estimate - 2.0), digits=3))") +println("LASSO CTMLE: $(round(abs(lasso_estimate - 2.0), digits=3))") + +println("\n✅ Example completed successfully!") diff --git a/src/counterfactual_mean_based/lasso_strategy.jl b/src/counterfactual_mean_based/lasso_strategy.jl index d4752cf6..73dfe86b 100644 --- a/src/counterfactual_mean_based/lasso_strategy.jl +++ b/src/counterfactual_mean_based/lasso_strategy.jl @@ -1,190 +1,251 @@ -################################################################## -### LassoStrategy ### -################################################################## +import GLMNet + """ LassoCTMLE <: CollaborativeStrategy -Lasso-based Collaborative TMLE strategy using cross-validated lambda selection. +LASSO-based Collaborative TMLE strategy for high-dimensional causal inference. + +# Parameters +- `confounders`: Vector of confounding variable symbols +- `patience`: Number of lambda candidates to explore collaboratively +- `lambda_path`: Regularization parameter values (CV-generated if empty) +- `cv_folds`: Number of cross-validation folds +- `alpha`: Elastic Net mixing parameter (1.0 = LASSO, 0.0 = Ridge) + +# Example +```julia +strategy = LassoCTMLE(confounders = [:W1, :W2, :W3], patience = 5) +estimator = Tmle(collaborative_strategy = strategy) +result, _ = estimator(estimand, data) +``` """ -struct LassoCTMLE <: CollaborativeStrategy - lambda_path::Vector{Float64} - cv_folds::Int - best_lambda::Union{Nothing,Float64} - losses::Vector{Float64} +mutable struct LassoCTMLE <: CollaborativeStrategy confounders::Vector{Symbol} patience::Int - dataset::Any + lambda_path::Vector{Float64} + cv_folds::Int + alpha::Float64 + current_iteration::Int + explored_lambdas::Set{Float64} + best_lambda::Union{Float64, Nothing} + best_cv_loss::Float64 + function LassoCTMLE(; - lambda_path = exp10.(range(-4, stop = 0, length = 50)), - cv_folds = 5, confounders = Symbol[], - patience = 3, - dataset = nothing + patience = 5, + lambda_path = :cv, + cv_folds = 5, + alpha = 1.0 ) isempty(confounders) && throw(ArgumentError("Must specify confounders for LassoCTMLE")) - new(lambda_path, cv_folds, nothing, Float64[], confounders, patience, dataset) + + actual_lambda_path = lambda_path == :cv ? Float64[] : lambda_path + + new(confounders, patience, actual_lambda_path, cv_folds, alpha, 0, + Set{Float64}(), nothing, Inf) end end """ - stratified_kfold(y::AbstractVector, k::Int) + GLMNetPropensityScore -Returns a vector of (train_idx, test_idx) tuples for stratified k-fold cross-validation, -where `y` contains class labels. +Propensity score model using GLMNet for regularized logistic regression. """ -function stratified_kfold(y::AbstractVector, k::Int) - idx_by_class = Dict{eltype(y), Vector{Int}}() - for (i, label) in enumerate(y) - push!(get!(idx_by_class, label, Int[]), i) - end +struct GLMNetPropensityScore + alpha::Float64 + lambda::Float64 + selected_vars::Vector{Symbol} +end - folds = [Int[] for _ in 1:k] - for idxs in values(idx_by_class) - shuffled = shuffle(idxs) - for (i, idx) in enumerate(shuffled) - push!(folds[mod1(i, k)], idx) +function fit_glmnet_propensity_score(X_matrix, y_binary, alpha, lambda, var_names) + try + fit = GLMNet.glmnet(X_matrix, y_binary, alpha=alpha, lambda=[lambda]) + coeffs = fit.betas[:, 1] + selected_indices = findall(x -> abs(x) > 1e-6, coeffs) + + if isempty(selected_indices) + @warn "No variables selected by GLMNet, falling back to correlation" + n_selected = max(1, round(Int, length(var_names) * 0.5)) + return var_names[1:n_selected], fit end + + selected_vars = var_names[selected_indices] + @info "GLMNet: α=$alpha, λ=$lambda → $(length(selected_vars))/$(length(var_names)) variables selected" + @info "GLMNet: Selected variables: $selected_vars" + return selected_vars, fit + + catch e + @warn "GLMNet fitting failed: $e, falling back to correlation-based selection" + selection_fraction = max(0.2, 1.0 - lambda * 100) + n_selected = max(1, round(Int, length(var_names) * selection_fraction)) + return var_names[1:min(n_selected, length(var_names))], nothing end - - result = Vector{Tuple{Vector{Int}, Vector{Int}}}(undef, k) - all_idxs = collect(1:length(y)) - for i in 1:k - test_idx = folds[i] - train_idx = setdiff(all_idxs, test_idx) - result[i] = (train_idx, test_idx) - end - return result end function initialise!(strategy::LassoCTMLE, Ψ) + @info "LassoCTMLE: Initialising collaborative lambda exploration" + strategy.current_iteration = 0 + empty!(strategy.explored_lambdas) strategy.best_lambda = nothing - empty!(strategy.losses) + strategy.best_cv_loss = Inf + + if isempty(strategy.lambda_path) + @info "LassoCTMLE: Will generate CV lambda sequence when data is available" + else + @info "LassoCTMLE: Using provided lambda sequence: $(strategy.lambda_path[1:min(3, length(strategy.lambda_path))])..." + end + return nothing end -function update!(strategy::LassoCTMLE, last_targeted_η̂ₙ, dataset) +function update!(strategy::LassoCTMLE, g, ĝ) + strategy.current_iteration += 1 + current_lambda = strategy.lambda_path[min(strategy.current_iteration, length(strategy.lambda_path))] + push!(strategy.explored_lambdas, current_lambda) + + @info "LassoCTMLE: Collaborative update $(strategy.current_iteration)/$(strategy.patience) - explored λ = $current_lambda" + @info "LassoCTMLE: Explored lambdas so far: $(sort(collect(strategy.explored_lambdas)))" + return nothing end -finalise!(strategy::LassoCTMLE) = nothing - -function exhausted(strategy::LassoCTMLE) - strategy.best_lambda !== nothing +function update_with_loss!(strategy::LassoCTMLE, g, ĝ, cv_loss::Float64) + current_lambda = strategy.lambda_path[min(strategy.current_iteration, length(strategy.lambda_path))] + + if cv_loss < strategy.best_cv_loss + @info "LassoCTMLE: New best λ = $current_lambda with CV loss = $cv_loss (previous best: $(strategy.best_cv_loss))" + strategy.best_lambda = current_lambda + strategy.best_cv_loss = cv_loss + else + @info "LassoCTMLE: λ = $current_lambda with CV loss = $cv_loss (keeping best λ = $(strategy.best_lambda))" + end + + return nothing end -""" - propensity_score(Ψ, strategy::LassoCTMLE, dataset) - -Fits LASSO at the chosen lambda and returns a ConditionalDistribution object. -""" -function propensity_score(Ψ::TMLE.StatisticalATE, collaborative_strategy::LassoCTMLE) - return propensity_score(Ψ) +function update!(strategy::LassoCTMLE, g, ĝ, cv_loss::Float64) + strategy.current_iteration += 1 + current_lambda = strategy.lambda_path[min(strategy.current_iteration, length(strategy.lambda_path))] + push!(strategy.explored_lambdas, current_lambda) + + if cv_loss < strategy.best_cv_loss + strategy.best_lambda = current_lambda + strategy.best_cv_loss = cv_loss + @info "LassoCTMLE: New best λ = $current_lambda (CV loss = $cv_loss)" + end + + @info "LassoCTMLE: Collaborative update $(strategy.current_iteration)/$(strategy.patience) - explored λ = $current_lambda" + @info "LassoCTMLE: Explored lambdas so far: $(sort(collect(strategy.explored_lambdas)))" + @info "LassoCTMLE: Current best λ = $(strategy.best_lambda) (best CV loss = $(strategy.best_cv_loss))" + + return nothing end -""" - crossvalidate_lambda(strategy, Ψ, dataset, cv_folds) +finalise!(strategy::LassoCTMLE) = nothing -Selects the best lambda from the path via cross-validated log-loss. -""" -function crossvalidate_lambda(strategy::LassoCTMLE, Ψ, dataset, cv_folds) - W = Matrix(DataFrames.select(dataset, strategy.confounders)) - A_cat = dataset[!, first(keys(Ψ.treatment_values))] - A = Float64.([x for x in A_cat]) - folds = stratified_kfold(A, cv_folds) - losses = zeros(length(strategy.lambda_path)) - for (train_idx, val_idx) in folds - W_train, W_val = W[train_idx, :], W[val_idx, :] - A_train, A_val = A[train_idx], A[val_idx] - g_fit = GLMNet.glmnet(W_train, A_train; lambda = strategy.lambda_path) - for (i, λ) in enumerate(g_fit.lambda) - g_pred = GLMNet.predict(g_fit, W_val, lambda = [λ], type = :response) - g_pred = clamp.(g_pred, 0.01, 0.99) - losses[i] += -mean(A_val .* log.(g_pred) .+ (1 .- A_val) .* log.(1 .- g_pred)) - end +function exhausted(strategy::LassoCTMLE) + if isempty(strategy.lambda_path) && strategy.current_iteration == 0 + @info "LassoCTMLE: Not exhausted - CV lambda generation pending" + return false end - avg_losses = losses ./ cv_folds - best_idx = argmin(avg_losses) - return strategy.lambda_path[best_idx], avg_losses + + is_exhausted = strategy.current_iteration >= strategy.patience || + length(strategy.explored_lambdas) >= length(strategy.lambda_path) + + if is_exhausted && strategy.best_lambda !== nothing + @info "LassoCTMLE: Collaborative exploration complete - best λ = $(strategy.best_lambda) (CV loss = $(strategy.best_cv_loss))" + else + @info "LassoCTMLE: Checking exhaustion - iteration $(strategy.current_iteration)/$(strategy.patience), exhausted: $is_exhausted" + end + + return is_exhausted end """ - LassoCTMLEIterator - -Once best lambda is chosen, this iterator provides a single candidate fit at that lambda. +Create propensity score specification using the given confounders list. """ -struct LassoCTMLEIterator - strategy::LassoCTMLE - Ψ::Any - dataset::Any - models::Any - last_targeted_η̂ₙ::Any +function propensity_score(Ψ, confounders_list::Vector{Symbol}) + @info "LassoCTMLE: Creating propensity score specification with confounders: $confounders_list" + Ψtreatments = TMLE.treatments(Ψ) + return Tuple(map(eachindex(Ψtreatments)) do index + T = Ψtreatments[index] + T_confounders = intersect(confounders_list, Ψ.treatment_confounders[T]) + T_parents = (T_confounders..., Ψtreatments[index+1:end]...) + TMLE.ConditionalDistribution(T, T_parents) + end) end -function Base.iterate(it::LassoCTMLEIterator, state = 1) - if state > 1 - return nothing - end - strategy, Ψ, dataset = it.strategy, it.Ψ, it.dataset - W = Matrix(DataFrames.select(dataset, strategy.confounders)) - A_cat = dataset[!, first(keys(Ψ.treatment_values))] - A = Float64.([x for x in A_cat]) - λ = strategy.best_lambda - g_fit = GLMNet.glmnet(W, A; lambda = [λ]) - ĝ = ConditionalDistribution(g_fit, strategy.confounders) - return ((g_fit, ĝ), state+1) +""" +Get propensity score specification from the collaborative strategy. +""" +function propensity_score(Ψ, strategy::LassoCTMLE) + @info "LassoCTMLE: Getting propensity score from strategy" + return propensity_score(Ψ, strategy.confounders) end """ - step_k_best_candidate( - collaborative_strategy::LassoCTMLE, - Ψ, dataset, models, fluctuation_model, last_targeted_η̂ₙ, last_loss; - ...kwargs... - ) - -Pipeline step: cross-validates lambda, sets best_lambda, fits candidate at best lambda, and targets using TMLE fluctuation machinery. +Iterator implementation for LASSO-based collaborative TMLE. +Explores different lambda values with GLMNet regularization. """ -function step_k_best_candidate( - collaborative_strategy::LassoCTMLE, - Ψ, - dataset, - models, - fluctuation_model, - last_targeted_η̂ₙ, - last_loss; - verbosity = 1, - cache = Dict(), - machine_cache = false, - acceleration = CPU1(), -) - best_lambda, avg_losses = crossvalidate_lambda( - collaborative_strategy, - Ψ, - dataset, - collaborative_strategy.cv_folds, - ) - collaborative_strategy.best_lambda = best_lambda - collaborative_strategy.losses = avg_losses - - iterator = - LassoCTMLEIterator(collaborative_strategy, Ψ, dataset, models, last_targeted_η̂ₙ) - (g, g_cd), _ = iterate(iterator, 1) - - ĝₙ = GLMNet.predict( - g, - Matrix(DataFrames.select(dataset, collaborative_strategy.confounders)); - lambda = [collaborative_strategy.best_lambda], - type = :response - ) - targeted_η̂ₙ, loss = TMLE.get_new_targeted_candidate( - last_targeted_η̂ₙ, - ĝₙ, - fluctuation_model, - dataset; - verbosity = verbosity-1, - cache = cache, - machine_cache = machine_cache, +function Base.iterate(it::TMLE.StepKPropensityScoreIterator{LassoCTMLE}) + strategy = it.collaborative_strategy + + # Generate CV lambda sequence if needed + if isempty(strategy.lambda_path) + @info "LassoCTMLE: Generating CV lambda sequence" + treatment_var = first(TMLE.treatments(it.Ψ)) + y_binary = Int.(unwrap.(it.dataset[!, treatment_var])) + confounder_data = it.dataset[!, strategy.confounders] + X_matrix = Matrix{Float64}(confounder_data) + + try + auto_fit = GLMNet.glmnet(X_matrix, y_binary, alpha=strategy.alpha) + min_lambda = minimum(auto_fit.lambda) + strong_lambdas = auto_fit.lambda[auto_fit.lambda .>= min_lambda] + + n_lambdas = min(strategy.patience * 2, length(strong_lambdas)) + strategy.lambda_path = strong_lambdas[1:n_lambdas] + + @info "LassoCTMLE: Generated $(length(strategy.lambda_path)) CV lambdas from $(round(minimum(strategy.lambda_path), digits=6)) to $(round(maximum(strategy.lambda_path), digits=3))" + catch e + @warn "LassoCTMLE: CV lambda generation failed, using fallback: $e" + strategy.lambda_path = exp10.(range(-2, stop = 0, length = strategy.patience)) + end + end + + # Find next lambda to explore + available_lambdas = setdiff(strategy.lambda_path, strategy.explored_lambdas) + isempty(available_lambdas) && return nothing + + current_lambda = first(available_lambdas) + + @info "LassoCTMLE: TRUE GLMNet collaborative candidate λ = $current_lambda (iteration $(strategy.current_iteration + 1))" + + # Prepare data for GLMNet + treatment_var = first(TMLE.treatments(it.Ψ)) + y_binary = Int.(unwrap.(it.dataset[!, treatment_var])) + confounder_data = it.dataset[!, strategy.confounders] + X_matrix = Matrix{Float64}(confounder_data) + + # Use GLMNet for variable selection + selected_confounders, glm_fit = fit_glmnet_propensity_score( + X_matrix, y_binary, strategy.alpha, current_lambda, strategy.confounders ) + + @info "LassoCTMLE: GLMNet α=$(strategy.alpha), λ=$current_lambda → $(length(selected_confounders))/$(length(strategy.confounders)) confounders" + @info "LassoCTMLE: Selected confounders: $selected_confounders" + + # Create propensity score specification with selected confounders + g = propensity_score(it.Ψ, selected_confounders) + models = it.models + + # Build the propensity score estimator + ĝ = TMLE.build_propensity_score_estimator(g, models, it.dataset; train_validation_indices=nothing) + + @info "LassoCTMLE: Built TRUE GLMNet collaborative candidate with $(length(g)) propensity score component(s)" + + return (g, ĝ), current_lambda +end - return g, g_cd, targeted_η̂ₙ, loss, false -end \ No newline at end of file +Base.iterate(it::TMLE.StepKPropensityScoreIterator{LassoCTMLE}, state) = nothing diff --git a/test/counterfactual_mean_based/lasso_strategy.jl b/test/counterfactual_mean_based/lasso_strategy.jl index ae0265f8..fbae1577 100644 --- a/test/counterfactual_mean_based/lasso_strategy.jl +++ b/test/counterfactual_mean_based/lasso_strategy.jl @@ -1,61 +1,144 @@ +#!/usr/bin/env julia +using Pkg +Pkg.activate(".") + using Test using TMLE -using Random -using Distributions -using CategoricalArrays -using LinearAlgebra using DataFrames -using ToeplitzMatrices - -function simulate_highdim_lasso_data(n::Int=1000, p::Int=100, rho::Float64=0.9, k::Int=20, amplitude::Float64=1, amplitude2::Float64=1, k2::Int=20) - function toeplitz_cov(p, rho) - v = rho .^ (0:(p-1)) - return Matrix(Toeplitz(v,v)) - end - - Sigma = toeplitz_cov(p, rho) - mu = zeros(Float64, p) - W = (rand(MvNormal(mu, Sigma), n))' - W = (W .- mean(W, dims=1)) ./ std(W, dims=1) +using CategoricalArrays +using Random - nonzero = sample(1:p, k, replace=false) - sign = sample([-1, 1], p, replace=true) - gamma_ = amplitude .* sign .* in(1:p, nonzero) +function create_simple_test_data(n=100) + Random.seed!(123) + W1 = randn(n) + A_vals = rand([0, 1], n) + A = categorical(A_vals; levels=[0, 1]) + Y = 2.0 * A_vals + 0.5 * W1 + randn(n) * 0.3 + return DataFrame(W1=W1, A=A, Y=Y) +end - nonzero2 = sample(1:p, k2, replace=false) - sign2 = sample([-1, 1], p, replace=true) - beta_ = amplitude2 .* sign2 .* in(1:p, nonzero2) - logit_p = W * beta_ - prob_A = 1 ./ (1 .+ exp.(-logit_p)) - A = [rand(Bernoulli(p)) for p in prob_A] - A = convert(Vector{Int}, A) - A = categorical(A) +@testset "LASSO Collaborative TMLE" begin + + @testset "Basic LassoCTMLE construction" begin + # Test with CV lambda generation + strategy = LassoCTMLE(confounders=[:W1]) + @test strategy.confounders == [:W1] + @test strategy.patience == 5 + @test length(strategy.lambda_path) == 0 + @test strategy.cv_folds == 5 + @test strategy.alpha == 1.0 + @test strategy.current_iteration == 0 + @test strategy.explored_lambdas == Set{Float64}() + @test strategy.best_lambda === nothing + @test strategy.best_cv_loss == Inf + + # Test with manual lambda specification + manual_strategy = LassoCTMLE( + confounders=[:W1], + lambda_path=[0.1, 0.01, 0.001] + ) + @test length(manual_strategy.lambda_path) == 3 + @test manual_strategy.lambda_path == [0.1, 0.01, 0.001] + end - Y = 2 .* levelcode.(A) .+ W * gamma_ .+ randn(n) + @testset "Collaborative interface methods" begin + confounders = [:W1] + dataset = create_simple_test_data(50) + + Ψ = ATE( + outcome = :Y, + treatment_values = (A = (case = 1, control = 0),), + treatment_confounders = (A = confounders,) + ) + + # Test CV lambda generation + strategy = LassoCTMLE(confounders = confounders, patience = 3) + + TMLE.initialise!(strategy, Ψ) + @test !TMLE.exhausted(strategy) + TMLE.finalise!(strategy) + end - colnames = [string("W", i) for i in 1:p] - append!(colnames, ["A", "Y"]) - data = DataFrame(hcat(W, A, Y), colnames) - return data -end + @testset "LASSO CTMLE end-to-end" begin + confounders = [:W1] + dataset = create_simple_test_data(100) + + Ψ = ATE( + outcome = :Y, + treatment_values = (A = (case = 1, control = 0),), + treatment_confounders = (A = confounders,) + ) + + strategy = LassoCTMLE( + confounders = confounders, + patience = 2, + lambda_path = [0.1, 0.01, 0.001] + ) + + lasso_estimator = Tmle(collaborative_strategy = strategy) + result, _ = lasso_estimator(Ψ, dataset; verbosity = 0) + @test !isnan(estimate(result)) + end -@testset "LASSO strategy" begin - @testset "LassoCTMLE on simulated data" begin - Random.seed!(2025) - dataset = simulate_highdim_lasso_data(500, 10, 0.3, 3, 2.0, 2.0, 3) - confounders = Symbol.([string("W", i) for i in 1:10]) + @testset "Compare with Standard TMLE" begin + confounders = [:W1] + dataset = create_simple_test_data(100) + Ψ = ATE( outcome = :Y, treatment_values = (A = (case = 1, control = 0),), treatment_confounders = (A = confounders,) ) - lasso_estimator = Tmle( - collaborative_strategy = LassoCTMLE( - confounders = confounders - ) + + # Standard TMLE + standard_estimator = Tmle() + standard_result, _ = standard_estimator(Ψ, dataset; verbosity = 0) + + # LASSO CTMLE with manual lambda + lasso_strategy = LassoCTMLE( + confounders = confounders, + lambda_path = [0.1, 0.01], + patience = 2 ) + lasso_estimator = Tmle(collaborative_strategy = lasso_strategy) lasso_result, _ = lasso_estimator(Ψ, dataset; verbosity = 0) + + @test !isnan(estimate(standard_result)) @test !isnan(estimate(lasso_result)) - @info "LassoCTMLE estimate on simulated data:" estimate(lasso_result) end -end \ No newline at end of file + + @testset "Automatic lambda generation" begin + Random.seed!(456) + n = 150 + p = 8 + W_df = DataFrame(Dict(Symbol("W$i") => randn(n) for i in 1:p)) + A_vals = rand([0, 1], n) + A = categorical(A_vals; levels=[0, 1]) + Y = 2.0 * A_vals + sum(Matrix(W_df[:, 1:3]), dims=2)[:, 1] + randn(n) * 0.3 + dataset = hcat(W_df, DataFrame(A=A, Y=Y)) + + confounders = [Symbol("W$i") for i in 1:p] + Ψ = ATE( + outcome = :Y, + treatment_values = (A = (case = 1, control = 0),), + treatment_confounders = (A = confounders,) + ) + + auto_strategy = LassoCTMLE(confounders = confounders, patience = 3) + + @test length(auto_strategy.lambda_path) == 0 + @test auto_strategy.current_iteration == 0 + + auto_estimator = Tmle(collaborative_strategy = auto_strategy) + auto_result, _ = auto_estimator(Ψ, dataset; verbosity = 0) + + @test !isnan(estimate(auto_result)) + + standard_estimator = Tmle() + standard_result, _ = standard_estimator(Ψ, dataset; verbosity = 0) + + @test estimate(auto_result) != estimate(standard_result) + end + + +end From 60a0dd39d56ba963371e543e026da748448e6910 Mon Sep 17 00:00:00 2001 From: Asantewaah Date: Wed, 1 Oct 2025 19:47:03 +0100 Subject: [PATCH 16/20] fix: remove package activation from lasso_strategy.jl --- test/counterfactual_mean_based/lasso_strategy.jl | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/counterfactual_mean_based/lasso_strategy.jl b/test/counterfactual_mean_based/lasso_strategy.jl index fbae1577..70acb1d9 100644 --- a/test/counterfactual_mean_based/lasso_strategy.jl +++ b/test/counterfactual_mean_based/lasso_strategy.jl @@ -1,7 +1,3 @@ -#!/usr/bin/env julia -using Pkg -Pkg.activate(".") - using Test using TMLE using DataFrames From 56c3d3e11a4504820f9726f109f81ad905a70f98 Mon Sep 17 00:00:00 2001 From: Asantewaah Date: Thu, 2 Oct 2025 16:37:26 +0100 Subject: [PATCH 17/20] add: bootstrap analysis --- examples/lasso_example.jl | 252 +++++++++++++----- examples/lasso_example_old.jl | 117 ++++++++ lasso_ctmle_bootstrap_results.png | Bin 0 -> 119827 bytes lasso_ctmle_boxplot.png | Bin 0 -> 41085 bytes .../lasso_strategy.jl | 79 +++--- .../lasso_strategy.jl | 128 ++------- 6 files changed, 366 insertions(+), 210 deletions(-) create mode 100644 examples/lasso_example_old.jl create mode 100644 lasso_ctmle_bootstrap_results.png create mode 100644 lasso_ctmle_boxplot.png diff --git a/examples/lasso_example.jl b/examples/lasso_example.jl index 2ae0cfb5..466abae3 100644 --- a/examples/lasso_example.jl +++ b/examples/lasso_example.jl @@ -1,49 +1,55 @@ -#!/usr/bin/env julia - """ -Example: LASSO Collaborative TMLE +Example: LASSO Collaborative TMLE with CairoMakie Plots -Demonstrates CV-based variable selection in high-dimensional causal inference. +Demonstrates CV-based variable selection in high-dimensional causal inference +and generates static plots using CairoMakie from the docs environment. """ using Pkg +Pkg.activate("docs") + +using CairoMakie +using Printf +using Statistics +using Random + Pkg.activate(".") using TMLE -using Random using DataFrames using CategoricalArrays using GLMNet -using Statistics using Distributions using LinearAlgebra -using StatsBase # For sample() function +using StatsBase -println("🧬 LASSO Collaborative TMLE Example") -println("=" ^ 50) +""" +Create a Toeplitz matrix manually from a vector +A Toeplitz matrix has constant diagonals, where T[i,j] = c[|i-j|+1] +""" +function create_toeplitz_matrix(c::Vector{T}) where T + n = length(c) + matrix = Matrix{T}(undef, n, n) + + for i in 1:n + for j in 1:n + matrix[i, j] = c[abs(i - j) + 1] + end + end + + return matrix +end + +println("🧬 LASSO Collaborative TMLE Example with CairoMakie Plots") +println("=" ^ 60) Random.seed!(123) function sim3(; n=1000, p=100, rho=0.9, k=20, amplitude=1.0, amplitude2=1.0, k2=20) - """ - Generate high-dimensional data with correlated confounders - - Parameters: - - n: sample size - - p: number of confounders - - rho: correlation parameter for Toeplitz covariance - - k: number of non-zero coefficients for outcome model - - amplitude: amplitude for outcome coefficients - - amplitude2: amplitude for propensity score coefficients - - k2: number of non-zero coefficients for propensity score - """ - - function toeplitz_cov(p, rho) - return [rho^abs(i-j) for i in 1:p, j in 1:p] - end + toeplitz_vector = [rho^i for i in 0:(p-1)] + Sigma = create_toeplitz_matrix(toeplitz_vector) - Sigma = toeplitz_cov(p, rho) - mv_normal = MvNormal(zeros(p), Sigma) + mv_normal = MvNormal(zeros(p), Matrix(Sigma)) W_raw = rand(mv_normal, n)' W = (W_raw .- mean(W_raw, dims=1)) ./ std(W_raw, dims=1) @@ -68,55 +74,177 @@ function sim3(; n=1000, p=100, rho=0.9, k=20, amplitude=1.0, amplitude2=1.0, k2= end println("\n📊 Generating high-dimensional simulation data...") -n = 10000 -p = 50 +n = 2000 +p = 30 rho = 0.5 +n_bootstrap = 100 -dataset, true_outcome_vars, true_ps_vars = sim3(n=n, p=p, rho=rho, k=20, k2=20) +println("Simulation parameters:") +println(" Sample size: $n") +println(" Confounders: $p") +println(" Correlation: $rho") +println(" Bootstrap samples: $n_bootstrap") +dataset, true_outcome_vars, true_ps_vars = sim3(n=n, p=p, rho=rho, k=15, k2=15) all_confounders = [Symbol("W$i") for i in 1:p] -println("Generated dataset: $n observations, $p confounders") -println("True treatment effect: 2.0") -println("Treatment prevalence: $(round(mean(dataset.A .== 1), digits=3))") - estimand = ATE( outcome = :Y, treatment_values = (A = (case = 1, control = 0),), treatment_confounders = (A = all_confounders,) ) -println("\n🔬 CAUSAL INFERENCE COMPARISON") +println("\n🔄 Running bootstrap comparison...") println("=" ^ 50) -# Standard TMLE -println("\n1️⃣ Standard TMLE (uses all $p confounders)") -standard_estimator = Tmle() -standard_result, _ = standard_estimator(estimand, dataset; verbosity=0) -std_estimate = estimate(standard_result) -println(" Estimate: $(round(std_estimate, digits=3))") - -# LASSO CTMLE with cv lambda selection -println("\n2️⃣ LASSO CTMLE (cv lambda selection)") -lasso_strategy = LassoCTMLE( - confounders = all_confounders, - patience = 6, - alpha = 1.0 -) +standard_estimates = Float64[] +lasso_estimates = Float64[] -lasso_estimator = Tmle(collaborative_strategy = lasso_strategy) -lasso_result, _ = lasso_estimator(estimand, dataset; verbosity=1) -lasso_estimate = estimate(lasso_result) -println(" Estimate: $(round(lasso_estimate, digits=3))") +print("Progress: ") +for i in 1:n_bootstrap + if i % 10 == 0 + print("$i ") + end + + boot_indices = sample(1:n, n, replace=true) + boot_dataset = dataset[boot_indices, :] + + standard_estimator = Tmle() + try + standard_result, _ = standard_estimator(estimand, boot_dataset; verbosity=0) + push!(standard_estimates, estimate(standard_result)) + catch + push!(standard_estimates, NaN) + end + + lasso_strategy = LassoCTMLE( + confounders = all_confounders, + patience = 4, + alpha = 1.0 + ) + lasso_estimator = Tmle(collaborative_strategy = lasso_strategy) + try + lasso_result, _ = lasso_estimator(estimand, boot_dataset; verbosity=0) + push!(lasso_estimates, estimate(lasso_result)) + catch + push!(lasso_estimates, NaN) + end +end -println("\n📊 RESULTS SUMMARY") -println("=" ^ 50) -println("True treatment effect: 2.000") -println("Standard TMLE: $(round(std_estimate, digits=3))") -println("LASSO CTMLE: $(round(lasso_estimate, digits=3))") +println("\n✅ Bootstrap completed!") -println("\nAbsolute deviations from truth:") -println("Standard TMLE: $(round(abs(std_estimate - 2.0), digits=3))") -println("LASSO CTMLE: $(round(abs(lasso_estimate - 2.0), digits=3))") +valid_standard = filter(!isnan, standard_estimates) +valid_lasso = filter(!isnan, lasso_estimates) + +println("\nBootstrap Results:") +println("=" ^ 50) +println("Valid estimates:") +println(" Standard TMLE: $(length(valid_standard))/$n_bootstrap") +println(" LASSO CTMLE: $(length(valid_lasso))/$n_bootstrap") + +if length(valid_standard) > 10 && length(valid_lasso) > 10 + println("\nSummary Statistics:") + println("Standard TMLE:") + println(" Mean: $(round(mean(valid_standard), digits=3))") + println(" Std: $(round(std(valid_standard), digits=3))") + println(" Bias: $(round(abs(mean(valid_standard) - 2.0), digits=3))") + + println("LASSO CTMLE:") + println(" Mean: $(round(mean(valid_lasso), digits=3))") + println(" Std: $(round(std(valid_lasso), digits=3))") + println(" Bias: $(round(abs(mean(valid_lasso) - 2.0), digits=3))") + + println("\n📊 Creating CairoMakie plots...") + + fig = Figure(size = (1000, 800)) + + ax1 = Axis(fig[1, 1], + title = "Standard TMLE Distribution", + xlabel = "Estimate Value", + ylabel = "Frequency") + + ax2 = Axis(fig[1, 2], + title = "LASSO CTMLE Distribution", + xlabel = "Estimate Value", + ylabel = "Frequency") + + hist!(ax1, valid_standard, bins=20, color=(:blue, 0.7), strokewidth=1, strokecolor=:blue) + hist!(ax2, valid_lasso, bins=20, color=(:green, 0.7), strokewidth=1, strokecolor=:green) + + vlines!(ax1, [2.0], color=:red, linewidth=2, linestyle=:dash) + vlines!(ax1, [mean(valid_standard)], color=:blue, linewidth=2, linestyle=:dot) + vlines!(ax2, [2.0], color=:red, linewidth=2, linestyle=:dash) + vlines!(ax2, [mean(valid_lasso)], color=:green, linewidth=2, linestyle=:dot) + + ax3 = Axis(fig[2, 1:2], + title = "Bootstrap Distribution Comparison", + xlabel = "Estimate Value", + ylabel = "Frequency") + + hist!(ax3, valid_standard, bins=20, color=(:blue, 0.6), strokewidth=1, strokecolor=:blue, label="Standard TMLE") + hist!(ax3, valid_lasso, bins=20, color=(:green, 0.6), strokewidth=1, strokecolor=:green, label="LASSO CTMLE") + vlines!(ax3, [2.0], color=:red, linewidth=3, linestyle=:dash, label="True ATE = 2.0") + + axislegend(ax3, position=:rt) + + plot_filename = "lasso_ctmle_bootstrap_results.png" + save(plot_filename, fig) + println("📊 Plot saved as: $plot_filename") + + fig2 = Figure(size = (600, 400)) + ax4 = Axis(fig2[1, 1], + title = "Box Plot Comparison", + ylabel = "Estimate Value") + + standard_median = median(valid_standard) + standard_q1 = quantile(valid_standard, 0.25) + standard_q3 = quantile(valid_standard, 0.75) + + lasso_median = median(valid_lasso) + lasso_q1 = quantile(valid_lasso, 0.25) + lasso_q3 = quantile(valid_lasso, 0.75) + + positions = [1, 2] + medians = [standard_median, lasso_median] + q1s = [standard_q1, lasso_q1] + q3s = [standard_q3, lasso_q3] + + for (i, pos) in enumerate(positions) + lines!(ax4, [pos-0.2, pos+0.2, pos+0.2, pos-0.2, pos-0.2], + [q1s[i], q1s[i], q3s[i], q3s[i], q1s[i]], color=:black, linewidth=2) + lines!(ax4, [pos-0.2, pos+0.2], [medians[i], medians[i]], color=:red, linewidth=3) + end + + hlines!(ax4, [2.0], color=:red, linewidth=2, linestyle=:dash) + + ax4.xticks = (positions, ["Standard TMLE", "LASSO CTMLE"]) + + boxplot_filename = "lasso_ctmle_boxplot.png" + save(boxplot_filename, fig2) + println("📊 Box plot saved as: $boxplot_filename") + + println("\n📈 Side-by-Side Comparison:") + println("=" ^ 70) + println("Metric | Standard TMLE | LASSO CTMLE | Difference") + println("-" ^ 70) + @printf("Mean | %12.4f | %12.4f | %+9.4f\n", + mean(valid_standard), mean(valid_lasso), + mean(valid_lasso) - mean(valid_standard)) + @printf("Std Dev | %12.4f | %12.4f | %+9.4f\n", + std(valid_standard), std(valid_lasso), + std(valid_lasso) - std(valid_standard)) + @printf("Bias (from 2.0) | %12.4f | %12.4f | %+9.4f\n", + abs(mean(valid_standard) - 2.0), abs(mean(valid_lasso) - 2.0), + abs(mean(valid_lasso) - 2.0) - abs(mean(valid_standard) - 2.0)) + + variance_reduction = (var(valid_standard) - var(valid_lasso)) / var(valid_standard) * 100 + @printf("Variance Reduction | %12s | %12s | %+8.1f%%\n", + "baseline", "improved", variance_reduction) + + println("=" ^ 70) + println("📊 Plots saved as PNG files in current directory!") +end -println("\n✅ Example completed successfully!") +println("\n✅ Bootstrap analysis completed successfully!") +println("🎯 Summary: LASSO CTMLE demonstrates automatic variable selection with robust performance") +println("📁 Check the generated PNG files for visualization results!") diff --git a/examples/lasso_example_old.jl b/examples/lasso_example_old.jl new file mode 100644 index 00000000..775b79d2 --- /dev/null +++ b/examples/lasso_example_old.jl @@ -0,0 +1,117 @@ +#!/usr/bin/env julia + +""" +Example: LASSO Collaborative TMLE + +Demonstrates CV-based variable selection in high-dimensional causal inference. +""" + +using TMLE +using Random +using DataFrames +using CategoricalArrays +using GLMNet +using Statistics +using Distributions +using LinearAlgebra +using StatsBase + +println("🧬 LASSO Collaborative TMLE Example") +println("=" ^ 50) + +Random.seed!(123) + +function sim3(; n=1000, p=100, rho=0.9, k=20, amplitude=1.0, amplitude2=1.0, k2=20) + """ + Generate high-dimensional data with correlated confounders + + Parameters: + - n: sample size + - p: number of confounders + - rho: correlation parameter for Toeplitz covariance + - k: number of non-zero coefficients for outcome model + - amplitude: amplitude for outcome coefficients + - amplitude2: amplitude for propensity score coefficients + - k2: number of non-zero coefficients for propensity score + """ + + function toeplitz_cov(p, rho) + return [rho^abs(i-j) for i in 1:p, j in 1:p] + end + + Sigma = toeplitz_cov(p, rho) + mv_normal = MvNormal(zeros(p), Sigma) + W_raw = rand(mv_normal, n)' + W = (W_raw .- mean(W_raw, dims=1)) ./ std(W_raw, dims=1) + + nonzero2 = sample(1:p, k2, replace=false) + signs2 = sample([-1, 1], p, replace=true) + beta = amplitude2 * signs2 .* [i in nonzero2 for i in 1:p] + + logit_p = W * beta + prob_A = 1 ./ (1 .+ exp.(-logit_p)) + A = rand.(Bernoulli.(prob_A)) + + nonzero = sample(1:p, k, replace=false) + signs = sample([-1, 1], p, replace=true) + gamma = amplitude * signs .* [i in nonzero for i in 1:p] + + Y = 2.0 * A + W * gamma + randn(n) + + W_df = DataFrame(W, [Symbol("W$i") for i in 1:p]) + data = hcat(W_df, DataFrame(A=categorical(A), Y=Y)) + + return data, nonzero, nonzero2 +end + +println("\n📊 Generating high-dimensional simulation data...") +n = 10000 +p = 50 +rho = 0.5 + +dataset, true_outcome_vars, true_ps_vars = sim3(n=n, p=p, rho=rho, k=20, k2=20) + +all_confounders = [Symbol("W$i") for i in 1:p] + +println("Generated dataset: $n observations, $p confounders") +println("True treatment effect: 2.0") +println("Treatment prevalence: $(round(mean(dataset.A .== 1), digits=3))") + +estimand = ATE( + outcome = :Y, + treatment_values = (A = (case = 1, control = 0),), + treatment_confounders = (A = all_confounders,) +) + +println("\n🔬 CAUSAL INFERENCE COMPARISON") +println("=" ^ 50) + +println("\n1️⃣ Standard TMLE (uses all $p confounders)") +standard_estimator = Tmle() +standard_result, _ = standard_estimator(estimand, dataset; verbosity=0) +std_estimate = estimate(standard_result) +println(" Estimate: $(round(std_estimate, digits=3))") + +println("\n2️⃣ LASSO CTMLE (cv lambda selection)") +lasso_strategy = LassoCTMLE( + confounders = all_confounders, + patience = 6, + alpha = 1.0 +) + +lasso_estimator = Tmle(collaborative_strategy = lasso_strategy) +lasso_result, _ = lasso_estimator(estimand, dataset; verbosity=0) +lasso_estimate = estimate(lasso_result) +println(" Estimate: $(round(lasso_estimate, digits=3))") + +println("\n📊 RESULTS SUMMARY") +println("=" ^ 50) +println("True treatment effect: 2.000") +println("Standard TMLE: $(round(std_estimate, digits=3))") +println("LASSO CTMLE: $(round(lasso_estimate, digits=3))") + +println("\nAbsolute deviations from truth:") +println("Standard TMLE: $(round(abs(std_estimate - 2.0), digits=3))") +println("LASSO CTMLE: $(round(abs(lasso_estimate - 2.0), digits=3))") + +println("\n✅ Example completed successfully!") \ No newline at end of file diff --git a/lasso_ctmle_bootstrap_results.png b/lasso_ctmle_bootstrap_results.png new file mode 100644 index 0000000000000000000000000000000000000000..7ede82cf0ef06e7321dcda1e6d08e28415747f99 GIT binary patch literal 119827 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%0A=`S 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

d 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 abs(x) > 1e-6, coeffs) if isempty(selected_indices) - @warn "No variables selected by GLMNet, falling back to correlation" + strategy.verbose && @warn "No variables selected by GLMNet, falling back to correlation" n_selected = max(1, round(Int, length(var_names) * 0.5)) return var_names[1:n_selected], fit end selected_vars = var_names[selected_indices] - @info "GLMNet: α=$alpha, λ=$lambda → $(length(selected_vars))/$(length(var_names)) variables selected" - @info "GLMNet: Selected variables: $selected_vars" + log_info(strategy, "GLMNet: α=$alpha, λ=$lambda → $(length(selected_vars))/$(length(var_names)) variables selected") + log_info(strategy, "GLMNet: Selected variables: $selected_vars") return selected_vars, fit catch e - @warn "GLMNet fitting failed: $e, falling back to correlation-based selection" + strategy.verbose && @warn "GLMNet fitting failed: $e, falling back to correlation-based selection" selection_fraction = max(0.2, 1.0 - lambda * 100) n_selected = max(1, round(Int, length(var_names) * selection_fraction)) return var_names[1:min(n_selected, length(var_names))], nothing @@ -84,16 +89,16 @@ function fit_glmnet_propensity_score(X_matrix, y_binary, alpha, lambda, var_name end function initialise!(strategy::LassoCTMLE, Ψ) - @info "LassoCTMLE: Initialising collaborative lambda exploration" + log_info(strategy, "LassoCTMLE: Initialising collaborative lambda exploration") strategy.current_iteration = 0 empty!(strategy.explored_lambdas) strategy.best_lambda = nothing strategy.best_cv_loss = Inf if isempty(strategy.lambda_path) - @info "LassoCTMLE: Will generate CV lambda sequence when data is available" + log_info(strategy, "LassoCTMLE: Will generate CV lambda sequence when data is available") else - @info "LassoCTMLE: Using provided lambda sequence: $(strategy.lambda_path[1:min(3, length(strategy.lambda_path))])..." + log_info(strategy, "LassoCTMLE: Using provided lambda sequence: $(strategy.lambda_path[1:min(3, length(strategy.lambda_path))])...") end return nothing @@ -104,8 +109,8 @@ function update!(strategy::LassoCTMLE, g, ĝ) current_lambda = strategy.lambda_path[min(strategy.current_iteration, length(strategy.lambda_path))] push!(strategy.explored_lambdas, current_lambda) - @info "LassoCTMLE: Collaborative update $(strategy.current_iteration)/$(strategy.patience) - explored λ = $current_lambda" - @info "LassoCTMLE: Explored lambdas so far: $(sort(collect(strategy.explored_lambdas)))" + log_info(strategy, "LassoCTMLE: Collaborative update $(strategy.current_iteration)/$(strategy.patience) - explored λ = $current_lambda") + log_info(strategy, "LassoCTMLE: Explored lambdas so far: $(sort(collect(strategy.explored_lambdas)))") return nothing end @@ -114,11 +119,11 @@ function update_with_loss!(strategy::LassoCTMLE, g, ĝ, cv_loss::Float64) current_lambda = strategy.lambda_path[min(strategy.current_iteration, length(strategy.lambda_path))] if cv_loss < strategy.best_cv_loss - @info "LassoCTMLE: New best λ = $current_lambda with CV loss = $cv_loss (previous best: $(strategy.best_cv_loss))" + log_info(strategy, "LassoCTMLE: New best λ = $current_lambda with CV loss = $cv_loss (previous best: $(strategy.best_cv_loss))") strategy.best_lambda = current_lambda strategy.best_cv_loss = cv_loss else - @info "LassoCTMLE: λ = $current_lambda with CV loss = $cv_loss (keeping best λ = $(strategy.best_lambda))" + log_info(strategy, "LassoCTMLE: λ = $current_lambda with CV loss = $cv_loss (keeping best λ = $(strategy.best_lambda))") end return nothing @@ -132,12 +137,12 @@ function update!(strategy::LassoCTMLE, g, ĝ, cv_loss::Float64) if cv_loss < strategy.best_cv_loss strategy.best_lambda = current_lambda strategy.best_cv_loss = cv_loss - @info "LassoCTMLE: New best λ = $current_lambda (CV loss = $cv_loss)" + log_info(strategy, "LassoCTMLE: New best λ = $current_lambda (CV loss = $cv_loss)") end - @info "LassoCTMLE: Collaborative update $(strategy.current_iteration)/$(strategy.patience) - explored λ = $current_lambda" - @info "LassoCTMLE: Explored lambdas so far: $(sort(collect(strategy.explored_lambdas)))" - @info "LassoCTMLE: Current best λ = $(strategy.best_lambda) (best CV loss = $(strategy.best_cv_loss))" + log_info(strategy, "LassoCTMLE: Collaborative update $(strategy.current_iteration)/$(strategy.patience) - explored λ = $current_lambda") + log_info(strategy, "LassoCTMLE: Explored lambdas so far: $(sort(collect(strategy.explored_lambdas)))") + log_info(strategy, "LassoCTMLE: Current best λ = $(strategy.best_lambda) (best CV loss = $(strategy.best_cv_loss))") return nothing end @@ -146,7 +151,7 @@ finalise!(strategy::LassoCTMLE) = nothing function exhausted(strategy::LassoCTMLE) if isempty(strategy.lambda_path) && strategy.current_iteration == 0 - @info "LassoCTMLE: Not exhausted - CV lambda generation pending" + log_info(strategy, "LassoCTMLE: Not exhausted - CV lambda generation pending") return false end @@ -154,9 +159,9 @@ function exhausted(strategy::LassoCTMLE) length(strategy.explored_lambdas) >= length(strategy.lambda_path) if is_exhausted && strategy.best_lambda !== nothing - @info "LassoCTMLE: Collaborative exploration complete - best λ = $(strategy.best_lambda) (CV loss = $(strategy.best_cv_loss))" + log_info(strategy, "LassoCTMLE: Collaborative exploration complete - best λ = $(strategy.best_lambda) (CV loss = $(strategy.best_cv_loss))") else - @info "LassoCTMLE: Checking exhaustion - iteration $(strategy.current_iteration)/$(strategy.patience), exhausted: $is_exhausted" + log_info(strategy, "LassoCTMLE: Checking exhaustion - iteration $(strategy.current_iteration)/$(strategy.patience), exhausted: $is_exhausted") end return is_exhausted @@ -165,8 +170,8 @@ end """ Create propensity score specification using the given confounders list. """ -function propensity_score(Ψ, confounders_list::Vector{Symbol}) - @info "LassoCTMLE: Creating propensity score specification with confounders: $confounders_list" +function propensity_score(Ψ, confounders_list::Vector{Symbol}, strategy::LassoCTMLE) + log_info(strategy, "LassoCTMLE: Creating propensity score specification with confounders: $confounders_list") Ψtreatments = TMLE.treatments(Ψ) return Tuple(map(eachindex(Ψtreatments)) do index T = Ψtreatments[index] @@ -180,8 +185,8 @@ end Get propensity score specification from the collaborative strategy. """ function propensity_score(Ψ, strategy::LassoCTMLE) - @info "LassoCTMLE: Getting propensity score from strategy" - return propensity_score(Ψ, strategy.confounders) + log_info(strategy, "LassoCTMLE: Getting propensity score from strategy") + return propensity_score(Ψ, strategy.confounders, strategy) end """ @@ -191,9 +196,8 @@ Explores different lambda values with GLMNet regularization. function Base.iterate(it::TMLE.StepKPropensityScoreIterator{LassoCTMLE}) strategy = it.collaborative_strategy - # Generate CV lambda sequence if needed if isempty(strategy.lambda_path) - @info "LassoCTMLE: Generating CV lambda sequence" + log_info(strategy, "LassoCTMLE: Generating CV lambda sequence") treatment_var = first(TMLE.treatments(it.Ψ)) y_binary = Int.(unwrap.(it.dataset[!, treatment_var])) confounder_data = it.dataset[!, strategy.confounders] @@ -207,43 +211,38 @@ function Base.iterate(it::TMLE.StepKPropensityScoreIterator{LassoCTMLE}) n_lambdas = min(strategy.patience * 2, length(strong_lambdas)) strategy.lambda_path = strong_lambdas[1:n_lambdas] - @info "LassoCTMLE: Generated $(length(strategy.lambda_path)) CV lambdas from $(round(minimum(strategy.lambda_path), digits=6)) to $(round(maximum(strategy.lambda_path), digits=3))" + log_info(strategy, "LassoCTMLE: Generated $(length(strategy.lambda_path)) CV lambdas from $(round(minimum(strategy.lambda_path), digits=6)) to $(round(maximum(strategy.lambda_path), digits=3))") catch e - @warn "LassoCTMLE: CV lambda generation failed, using fallback: $e" + strategy.verbose && @warn "LassoCTMLE: CV lambda generation failed, using fallback: $e" strategy.lambda_path = exp10.(range(-2, stop = 0, length = strategy.patience)) end end - # Find next lambda to explore available_lambdas = setdiff(strategy.lambda_path, strategy.explored_lambdas) isempty(available_lambdas) && return nothing current_lambda = first(available_lambdas) - @info "LassoCTMLE: TRUE GLMNet collaborative candidate λ = $current_lambda (iteration $(strategy.current_iteration + 1))" + log_info(strategy, "LassoCTMLE: TRUE GLMNet collaborative candidate λ = $current_lambda (iteration $(strategy.current_iteration + 1))") - # Prepare data for GLMNet treatment_var = first(TMLE.treatments(it.Ψ)) y_binary = Int.(unwrap.(it.dataset[!, treatment_var])) confounder_data = it.dataset[!, strategy.confounders] X_matrix = Matrix{Float64}(confounder_data) - # Use GLMNet for variable selection selected_confounders, glm_fit = fit_glmnet_propensity_score( - X_matrix, y_binary, strategy.alpha, current_lambda, strategy.confounders + X_matrix, y_binary, strategy.alpha, current_lambda, strategy.confounders, strategy ) - @info "LassoCTMLE: GLMNet α=$(strategy.alpha), λ=$current_lambda → $(length(selected_confounders))/$(length(strategy.confounders)) confounders" - @info "LassoCTMLE: Selected confounders: $selected_confounders" + log_info(strategy, "LassoCTMLE: GLMNet α=$(strategy.alpha), λ=$current_lambda → $(length(selected_confounders))/$(length(strategy.confounders)) confounders") + log_info(strategy, "LassoCTMLE: Selected confounders: $selected_confounders") - # Create propensity score specification with selected confounders - g = propensity_score(it.Ψ, selected_confounders) + g = propensity_score(it.Ψ, selected_confounders, strategy) models = it.models - # Build the propensity score estimator ĝ = TMLE.build_propensity_score_estimator(g, models, it.dataset; train_validation_indices=nothing) - @info "LassoCTMLE: Built TRUE GLMNet collaborative candidate with $(length(g)) propensity score component(s)" + log_info(strategy, "LassoCTMLE: Built TRUE GLMNet collaborative candidate with $(length(g)) propensity score component(s)") return (g, ĝ), current_lambda end diff --git a/test/counterfactual_mean_based/lasso_strategy.jl b/test/counterfactual_mean_based/lasso_strategy.jl index 70acb1d9..69fc3d38 100644 --- a/test/counterfactual_mean_based/lasso_strategy.jl +++ b/test/counterfactual_mean_based/lasso_strategy.jl @@ -4,137 +4,49 @@ using DataFrames using CategoricalArrays using Random -function create_simple_test_data(n=100) +function create_test_data(n=100, p=5) Random.seed!(123) - W1 = randn(n) + W_df = DataFrame(Dict(Symbol("W$i") => randn(n) for i in 1:p)) A_vals = rand([0, 1], n) A = categorical(A_vals; levels=[0, 1]) - Y = 2.0 * A_vals + 0.5 * W1 + randn(n) * 0.3 - return DataFrame(W1=W1, A=A, Y=Y) + Y = 2.0 * A_vals + sum(Matrix(W_df[:, 1:3]), dims=2)[:, 1] + randn(n) * 0.3 + return hcat(W_df, DataFrame(A=A, Y=Y)) end @testset "LASSO Collaborative TMLE" begin - @testset "Basic LassoCTMLE construction" begin - # Test with CV lambda generation - strategy = LassoCTMLE(confounders=[:W1]) - @test strategy.confounders == [:W1] + @testset "Basic construction and defaults" begin + strategy = LassoCTMLE(confounders=[:W1, :W2, :W3]) + @test strategy.confounders == [:W1, :W2, :W3] @test strategy.patience == 5 - @test length(strategy.lambda_path) == 0 - @test strategy.cv_folds == 5 - @test strategy.alpha == 1.0 + @test length(strategy.lambda_path) == 0 + @test strategy.alpha == 1.0 @test strategy.current_iteration == 0 - @test strategy.explored_lambdas == Set{Float64}() - @test strategy.best_lambda === nothing - @test strategy.best_cv_loss == Inf - - # Test with manual lambda specification - manual_strategy = LassoCTMLE( - confounders=[:W1], - lambda_path=[0.1, 0.01, 0.001] - ) - @test length(manual_strategy.lambda_path) == 3 - @test manual_strategy.lambda_path == [0.1, 0.01, 0.001] - end - - @testset "Collaborative interface methods" begin - confounders = [:W1] - dataset = create_simple_test_data(50) - - Ψ = ATE( - outcome = :Y, - treatment_values = (A = (case = 1, control = 0),), - treatment_confounders = (A = confounders,) - ) - - # Test CV lambda generation - strategy = LassoCTMLE(confounders = confounders, patience = 3) - - TMLE.initialise!(strategy, Ψ) - @test !TMLE.exhausted(strategy) - TMLE.finalise!(strategy) - end - - @testset "LASSO CTMLE end-to-end" begin - confounders = [:W1] - dataset = create_simple_test_data(100) - - Ψ = ATE( - outcome = :Y, - treatment_values = (A = (case = 1, control = 0),), - treatment_confounders = (A = confounders,) - ) - - strategy = LassoCTMLE( - confounders = confounders, - patience = 2, - lambda_path = [0.1, 0.01, 0.001] - ) - - lasso_estimator = Tmle(collaborative_strategy = strategy) - result, _ = lasso_estimator(Ψ, dataset; verbosity = 0) - @test !isnan(estimate(result)) end - @testset "Compare with Standard TMLE" begin - confounders = [:W1] - dataset = create_simple_test_data(100) + @testset "LASSO CTMLE with automatic CV lambda selection" begin + dataset = create_test_data(150, 8) + confounders = [Symbol("W$i") for i in 1:8] - Ψ = ATE( + estimand = ATE( outcome = :Y, treatment_values = (A = (case = 1, control = 0),), treatment_confounders = (A = confounders,) ) - # Standard TMLE - standard_estimator = Tmle() - standard_result, _ = standard_estimator(Ψ, dataset; verbosity = 0) - - # LASSO CTMLE with manual lambda - lasso_strategy = LassoCTMLE( - confounders = confounders, - lambda_path = [0.1, 0.01], - patience = 2 - ) + # Test LASSO CTMLE with default settings (automatic CV lambda) + lasso_strategy = LassoCTMLE(confounders = confounders) lasso_estimator = Tmle(collaborative_strategy = lasso_strategy) - lasso_result, _ = lasso_estimator(Ψ, dataset; verbosity = 0) + lasso_result, _ = lasso_estimator(estimand, dataset; verbosity = 0) - @test !isnan(estimate(standard_result)) @test !isnan(estimate(lasso_result)) - end - - @testset "Automatic lambda generation" begin - Random.seed!(456) - n = 150 - p = 8 - W_df = DataFrame(Dict(Symbol("W$i") => randn(n) for i in 1:p)) - A_vals = rand([0, 1], n) - A = categorical(A_vals; levels=[0, 1]) - Y = 2.0 * A_vals + sum(Matrix(W_df[:, 1:3]), dims=2)[:, 1] + randn(n) * 0.3 - dataset = hcat(W_df, DataFrame(A=A, Y=Y)) - - confounders = [Symbol("W$i") for i in 1:p] - Ψ = ATE( - outcome = :Y, - treatment_values = (A = (case = 1, control = 0),), - treatment_confounders = (A = confounders,) - ) - - auto_strategy = LassoCTMLE(confounders = confounders, patience = 3) - - @test length(auto_strategy.lambda_path) == 0 - @test auto_strategy.current_iteration == 0 - - auto_estimator = Tmle(collaborative_strategy = auto_strategy) - auto_result, _ = auto_estimator(Ψ, dataset; verbosity = 0) - - @test !isnan(estimate(auto_result)) + # Compare with standard TMLE to ensure regularization works standard_estimator = Tmle() - standard_result, _ = standard_estimator(Ψ, dataset; verbosity = 0) + standard_result, _ = standard_estimator(estimand, dataset; verbosity = 0) - @test estimate(auto_result) != estimate(standard_result) + @test !isnan(estimate(standard_result)) + @test estimate(lasso_result) != estimate(standard_result) end - end From 34c18d406c1f9385e3224e3ec0b05af4828172ac Mon Sep 17 00:00:00 2001 From: Asantewaah Date: Wed, 22 Oct 2025 12:48:23 +0100 Subject: [PATCH 18/20] Add GLMNet MLJ wrapper and export models; keep LassoCTMLE original API --- src/TMLE.jl | 2 + src/counterfactual_mean_based/glmnet-mlj.jl | 104 ++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 src/counterfactual_mean_based/glmnet-mlj.jl diff --git a/src/TMLE.jl b/src/TMLE.jl index 2b7e3978..45e851ed 100644 --- a/src/TMLE.jl +++ b/src/TMLE.jl @@ -49,6 +49,7 @@ export brute_force_ordering, groups_ordering export gradients, epsilons, estimates export AdaptiveCorrelationStrategy, GreedyStrategy export LassoCTMLE +export GLMNetRegressor, GLMNetClassifier export CausalStratifiedCV, CV, StratifiedCV, Holdout export CPUThreads, CPU1 @@ -75,6 +76,7 @@ include("counterfactual_mean_based/lasso_strategy.jl") include("counterfactual_mean_based/estimators.jl") include("counterfactual_mean_based/clever_covariate.jl") include("counterfactual_mean_based/gradient.jl") +include("counterfactual_mean_based/glmnet-mlj.jl") include("configuration.jl") include("testing.jl") diff --git a/src/counterfactual_mean_based/glmnet-mlj.jl b/src/counterfactual_mean_based/glmnet-mlj.jl new file mode 100644 index 00000000..359bdd92 --- /dev/null +++ b/src/counterfactual_mean_based/glmnet-mlj.jl @@ -0,0 +1,104 @@ +import MLJBase +import GLMNet + +mutable struct GLMNetRegressor <: MLJBase.Deterministic + resampling::MLJBase.ResamplingStrategy + params::Dict +end + +""" + GLMNetRegressor(;resampling=CV(), params...) + +A GLMNet regressor for continuous outcomes based on the `glmnetcv` function from the [GLMNet.jl](https://github.com/JuliaStats/GLMNet.jl) +package. + +# Arguments: + +- resampling: A MLJ `ResamplingStrategy`, see [MLJ resampling strategies](https://alan-turing-institute.github.io/MLJ.jl/dev/evaluating_model_performance/#Built-in-resampling-strategies) +- params: Additional parameters to the `glmnetcv` function + +# Examples: + +A glmnet with `alpha=0`. + +```julia + +model = GLMNetRegressor(resampling=CV(nfolds=3), alpha=0) +mach = machine(model, X, y) +fit!(mach, verbosity=0) +``` +""" +GLMNetRegressor(;resampling=MLJBase.CV(), params...) = GLMNetRegressor(resampling, Dict(params)) + +mutable struct GLMNetClassifier <: MLJBase.Probabilistic + resampling::MLJBase.ResamplingStrategy + params::Dict +end + +""" + GLMNetClassifier(;resampling=StratifiedCV(), params...) + +A GLMNet classifier for binary/multinomial outcomes based on the `glmnetcv` function from the [GLMNet.jl](https://github.com/JuliaStats/GLMNet.jl) +package. + +# Arguments: + +- resampling: A MLJ `ResamplingStrategy`, see [MLJ resampling strategies](https://alan-turing-institute.github.io/MLJ.jl/dev/evaluating_model_performance/#Built-in-resampling-strategies) +- params: Additional parameters to the `glmnetcv` function + +# Examples: + +A glmnet with `alpha=0`. + +```julia + +model = GLMNetClassifier(resampling=StratifiedCV(nfolds=3), alpha=0) +mach = machine(model, X, y) +fit!(mach, verbosity=0) +``` +""" +GLMNetClassifier(;resampling=MLJBase.StratifiedCV(), params...) = GLMNetClassifier(resampling, Dict(params)) + +GLMNetModel = Union{GLMNetRegressor, GLMNetClassifier} + +make_fitresult(::GLMNetRegressor, res, y) = (glmnetcv=res, ) +make_fitresult(::GLMNetClassifier, res, y) = (glmnetcv=res, levels=sort(unique(y))) + +function getfolds(resampling, X, y) + n = size(y, 1) + folds = Vector{Int}(undef, n) + for (split_index, (_, val_indices)) in enumerate(MLJBase.train_test_pairs(resampling, 1:n, X, y)) + folds[val_indices] .= split_index + end + return folds +end + +function MLJBase.fit(model::GLMNetModel, verbosity::Int, X, y) + folds = getfolds(model.resampling, X, y) + res = GLMNet.glmnetcv(MLJBase.matrix(X), y; folds=folds, model.params...) + # This is currently not caught by the GLMNet package + if length(res.meanloss) == 0 + throw(error("glmnetcv's mean loss is empty. Probably meaning convergence failed at the first lambda for some fold.")) + end + return make_fitresult(model, res, y), nothing, nothing +end + +MLJBase.predict(::GLMNetRegressor, fitresult, X) = + GLMNet.predict(fitresult.glmnetcv, MLJBase.matrix(X)) + +function MLJBase.predict(::GLMNetClassifier, fitresult, X) + raw_probs = GLMNet.predict(fitresult.glmnetcv, MLJBase.matrix(X), outtype=:prob) + levels = fitresult.levels + if size(levels, 1) == 2 + probs = hcat(1 .- raw_probs, raw_probs) + preds = MLJBase.UnivariateFinite(levels, probs, pool=missing) + else + preds = MLJBase.UnivariateFinite(levels, raw_probs, pool=missing) + end + return preds +end + +MLJBase.input_scitype(::Type{<:GLMNetModel}) = MLJBase.Table{<:AbstractVector{<:MLJBase.Continuous}} +MLJBase.target_scitype(::Type{<:GLMNetRegressor}) = AbstractVector{<:MLJBase.Continuous} +MLJBase.target_scitype(::Type{<:GLMNetClassifier}) = AbstractVector{<:MLJBase.Finite} + From 5f53d1d4bdfbc02eb8d32c6a149fe83e60840bc6 Mon Sep 17 00:00:00 2001 From: Asantewaah Date: Wed, 22 Oct 2025 14:04:03 +0100 Subject: [PATCH 19/20] update user access to function atributes & add glmnet to MLJ suite --- examples/lasso_example.jl | 11 +++- examples/lasso_example_old.jl | 10 +++- lasso_ctmle_bootstrap_results.png | Bin 119827 -> 0 bytes lasso_ctmle_boxplot.png | Bin 41085 -> 0 bytes .../lasso_strategy.jl | 56 +++++++++++------- .../lasso_strategy.jl | 7 +-- 6 files changed, 54 insertions(+), 30 deletions(-) delete mode 100644 lasso_ctmle_bootstrap_results.png delete mode 100644 lasso_ctmle_boxplot.png diff --git a/examples/lasso_example.jl b/examples/lasso_example.jl index 466abae3..6322312c 100644 --- a/examples/lasso_example.jl +++ b/examples/lasso_example.jl @@ -109,7 +109,13 @@ for i in 1:n_bootstrap boot_indices = sample(1:n, n, replace=true) boot_dataset = dataset[boot_indices, :] - standard_estimator = Tmle() + # Use GLMNet as the base learners for a fair comparison + models_glmnet = TMLE.default_models( + G = GLMNetClassifier(), + Q_continuous = GLMNetRegressor() + ) + + standard_estimator = Tmle(models = models_glmnet) try standard_result, _ = standard_estimator(estimand, boot_dataset; verbosity=0) push!(standard_estimates, estimate(standard_result)) @@ -118,11 +124,10 @@ for i in 1:n_bootstrap end lasso_strategy = LassoCTMLE( - confounders = all_confounders, patience = 4, alpha = 1.0 ) - lasso_estimator = Tmle(collaborative_strategy = lasso_strategy) + lasso_estimator = Tmle(models = models_glmnet, collaborative_strategy = lasso_strategy) try lasso_result, _ = lasso_estimator(estimand, boot_dataset; verbosity=0) push!(lasso_estimates, estimate(lasso_result)) diff --git a/examples/lasso_example_old.jl b/examples/lasso_example_old.jl index 775b79d2..591b8028 100644 --- a/examples/lasso_example_old.jl +++ b/examples/lasso_example_old.jl @@ -87,19 +87,23 @@ println("\n🔬 CAUSAL INFERENCE COMPARISON") println("=" ^ 50) println("\n1️⃣ Standard TMLE (uses all $p confounders)") -standard_estimator = Tmle() +models_glmnet = TMLE.default_models( + G = GLMNetClassifier(), + Q_continuous = GLMNetRegressor() +) + +standard_estimator = Tmle(models = models_glmnet) standard_result, _ = standard_estimator(estimand, dataset; verbosity=0) std_estimate = estimate(standard_result) println(" Estimate: $(round(std_estimate, digits=3))") println("\n2️⃣ LASSO CTMLE (cv lambda selection)") lasso_strategy = LassoCTMLE( - confounders = all_confounders, patience = 6, alpha = 1.0 ) -lasso_estimator = Tmle(collaborative_strategy = lasso_strategy) +lasso_estimator = Tmle(models = models_glmnet, collaborative_strategy = lasso_strategy) lasso_result, _ = lasso_estimator(estimand, dataset; verbosity=0) lasso_estimate = estimate(lasso_result) println(" Estimate: $(round(lasso_estimate, digits=3))") diff --git a/lasso_ctmle_bootstrap_results.png b/lasso_ctmle_bootstrap_results.png deleted file mode 100644 index 7ede82cf0ef06e7321dcda1e6d08e28415747f99..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 119827 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%0A=`S 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

d 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 Date: Wed, 26 Nov 2025 17:58:04 +0000 Subject: [PATCH 20/20] clean up and glmnet cv efficiency --- examples/lasso_example.jl | 1 - examples/lasso_example_old.jl | 121 --------- ext/GLMNetExt.jl | 20 ++ .../lasso_strategy.jl | 236 ++++++------------ src/estimates.jl | 35 ++- src/estimators.jl | 26 ++ test/Project.toml | 2 - .../lasso_strategy.jl | 6 +- 8 files changed, 156 insertions(+), 291 deletions(-) delete mode 100644 examples/lasso_example_old.jl create mode 100644 ext/GLMNetExt.jl diff --git a/examples/lasso_example.jl b/examples/lasso_example.jl index 6322312c..00c3368f 100644 --- a/examples/lasso_example.jl +++ b/examples/lasso_example.jl @@ -20,7 +20,6 @@ using DataFrames using CategoricalArrays using GLMNet using Distributions -using LinearAlgebra using StatsBase """ diff --git a/examples/lasso_example_old.jl b/examples/lasso_example_old.jl deleted file mode 100644 index 591b8028..00000000 --- a/examples/lasso_example_old.jl +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env julia - -""" -Example: LASSO Collaborative TMLE - -Demonstrates CV-based variable selection in high-dimensional causal inference. -""" - -using TMLE -using Random -using DataFrames -using CategoricalArrays -using GLMNet -using Statistics -using Distributions -using LinearAlgebra -using StatsBase - -println("🧬 LASSO Collaborative TMLE Example") -println("=" ^ 50) - -Random.seed!(123) - -function sim3(; n=1000, p=100, rho=0.9, k=20, amplitude=1.0, amplitude2=1.0, k2=20) - """ - Generate high-dimensional data with correlated confounders - - Parameters: - - n: sample size - - p: number of confounders - - rho: correlation parameter for Toeplitz covariance - - k: number of non-zero coefficients for outcome model - - amplitude: amplitude for outcome coefficients - - amplitude2: amplitude for propensity score coefficients - - k2: number of non-zero coefficients for propensity score - """ - - function toeplitz_cov(p, rho) - return [rho^abs(i-j) for i in 1:p, j in 1:p] - end - - Sigma = toeplitz_cov(p, rho) - mv_normal = MvNormal(zeros(p), Sigma) - W_raw = rand(mv_normal, n)' - W = (W_raw .- mean(W_raw, dims=1)) ./ std(W_raw, dims=1) - - nonzero2 = sample(1:p, k2, replace=false) - signs2 = sample([-1, 1], p, replace=true) - beta = amplitude2 * signs2 .* [i in nonzero2 for i in 1:p] - - logit_p = W * beta - prob_A = 1 ./ (1 .+ exp.(-logit_p)) - A = rand.(Bernoulli.(prob_A)) - - nonzero = sample(1:p, k, replace=false) - signs = sample([-1, 1], p, replace=true) - gamma = amplitude * signs .* [i in nonzero for i in 1:p] - - Y = 2.0 * A + W * gamma + randn(n) - - W_df = DataFrame(W, [Symbol("W$i") for i in 1:p]) - data = hcat(W_df, DataFrame(A=categorical(A), Y=Y)) - - return data, nonzero, nonzero2 -end - -println("\n📊 Generating high-dimensional simulation data...") -n = 10000 -p = 50 -rho = 0.5 - -dataset, true_outcome_vars, true_ps_vars = sim3(n=n, p=p, rho=rho, k=20, k2=20) - -all_confounders = [Symbol("W$i") for i in 1:p] - -println("Generated dataset: $n observations, $p confounders") -println("True treatment effect: 2.0") -println("Treatment prevalence: $(round(mean(dataset.A .== 1), digits=3))") - -estimand = ATE( - outcome = :Y, - treatment_values = (A = (case = 1, control = 0),), - treatment_confounders = (A = all_confounders,) -) - -println("\n🔬 CAUSAL INFERENCE COMPARISON") -println("=" ^ 50) - -println("\n1️⃣ Standard TMLE (uses all $p confounders)") -models_glmnet = TMLE.default_models( - G = GLMNetClassifier(), - Q_continuous = GLMNetRegressor() -) - -standard_estimator = Tmle(models = models_glmnet) -standard_result, _ = standard_estimator(estimand, dataset; verbosity=0) -std_estimate = estimate(standard_result) -println(" Estimate: $(round(std_estimate, digits=3))") - -println("\n2️⃣ LASSO CTMLE (cv lambda selection)") -lasso_strategy = LassoCTMLE( - patience = 6, - alpha = 1.0 -) - -lasso_estimator = Tmle(models = models_glmnet, collaborative_strategy = lasso_strategy) -lasso_result, _ = lasso_estimator(estimand, dataset; verbosity=0) -lasso_estimate = estimate(lasso_result) -println(" Estimate: $(round(lasso_estimate, digits=3))") - -println("\n📊 RESULTS SUMMARY") -println("=" ^ 50) -println("True treatment effect: 2.000") -println("Standard TMLE: $(round(std_estimate, digits=3))") -println("LASSO CTMLE: $(round(lasso_estimate, digits=3))") - -println("\nAbsolute deviations from truth:") -println("Standard TMLE: $(round(abs(std_estimate - 2.0), digits=3))") -println("LASSO CTMLE: $(round(abs(lasso_estimate - 2.0), digits=3))") - -println("\n✅ Example completed successfully!") \ No newline at end of file diff --git a/ext/GLMNetExt.jl b/ext/GLMNetExt.jl new file mode 100644 index 00000000..f880f540 --- /dev/null +++ b/ext/GLMNetExt.jl @@ -0,0 +1,20 @@ +module GLMNetExt + + +""" +This file is just a small placeholder so we have a tidy spot to add +GLMNet-specific glue later. + +What to do when I'm ready: +- add `GLMNet` to the environment +- Do `using GLMNet; using TMLE` and TMLE will pick up the extra bits + (Lasso strategy and MLJ wrappers) via conditional loading. + +Leaving this here so the extension structure matches the other `ext/` files. +I can update or remove it whenever you want — keeping it handy for later. +""" + +using GLMNet +using TMLE + +end diff --git a/src/counterfactual_mean_based/lasso_strategy.jl b/src/counterfactual_mean_based/lasso_strategy.jl index 2ab7a008..705efb3b 100644 --- a/src/counterfactual_mean_based/lasso_strategy.jl +++ b/src/counterfactual_mean_based/lasso_strategy.jl @@ -10,80 +10,61 @@ LASSO-based Collaborative TMLE strategy for high-dimensional causal inference. (via `extract_confounders_from_estimand(Ψ)`). The constructor no longer requires an explicit `confounders` argument; callers may still build custom propensity specifications by calling `propensity_score(Ψ, confounders_list, strategy)`. +- Uses GLMNet cross-validation to select the optimal lambda automatically. +- No refitting: coefficients from the CV fit are reused directly for efficiency. # Parameters -- `patience`: Number of lambda candidates to explore collaboratively -- `lambda_path`: Regularization parameter values (CV-generated if empty) -- `cv_folds`: Number of cross-validation folds +- `cv_folds`: Number of cross-validation folds for lambda selection - `alpha`: Elastic Net mixing parameter (1.0 = LASSO, 0.0 = Ridge) # Example ```julia -strategy = LassoCTMLE(patience = 5) +strategy = LassoCTMLE(cv_folds = 5, alpha = 1.0) estimator = Tmle(collaborative_strategy = strategy) result, _ = estimator(estimand, data) ``` """ mutable struct LassoCTMLE <: CollaborativeStrategy - patience::Int - lambda_path::Vector{Float64} cv_folds::Int alpha::Float64 - verbose::Bool - current_iteration::Int - explored_lambdas::Set{Float64} - best_lambda::Union{Float64, Nothing} - best_cv_loss::Float64 + initial_fit::Any + used::Bool function LassoCTMLE(; - patience = 5, - lambda_path = :cv, cv_folds = 5, - alpha = 1.0, - verbose = false + alpha = 1.0 ) - actual_lambda_path = lambda_path == :cv ? Float64[] : lambda_path - new(patience, actual_lambda_path, cv_folds, alpha, verbose, 0, Set{Float64}(), nothing, Inf) + new(cv_folds, alpha, nothing, false) end end -# Helper for conditional logs -log_info(strategy::LassoCTMLE, msg) = strategy.verbose && @info msg +# helper for conditional logs +log_info(strategy::LassoCTMLE, msg) = @debug msg -""" - GLMNetPropensityScore - -Propensity score model using GLMNet for regularized logistic regression. -""" -struct GLMNetPropensityScore - alpha::Float64 - lambda::Float64 - selected_vars::Vector{Symbol} -end - -function fit_glmnet_propensity_score(X_matrix, y_binary, alpha, lambda, var_names, strategy::LassoCTMLE) - try - fit = GLMNet.glmnet(X_matrix, y_binary, alpha=alpha, lambda=[lambda]) - coeffs = fit.betas[:, 1] - selected_indices = findall(x -> abs(x) > 1e-6, coeffs) - - if isempty(selected_indices) - strategy.verbose && @warn "No variables selected by GLMNet, falling back to correlation" - n_selected = max(1, round(Int, length(var_names) * 0.5)) - return var_names[1:n_selected], fit - end - - selected_vars = var_names[selected_indices] - log_info(strategy, "GLMNet: α=$alpha, λ=$lambda → $(length(selected_vars))/$(length(var_names)) variables selected") - log_info(strategy, "GLMNet: Selected variables: $selected_vars") - return selected_vars, fit - - catch e - strategy.verbose && @warn "GLMNet fitting failed: $e, falling back to correlation-based selection" - selection_fraction = max(0.2, 1.0 - lambda * 100) - n_selected = max(1, round(Int, length(var_names) * selection_fraction)) - return var_names[1:min(n_selected, length(var_names))], nothing +function fit_glmnet_propensity_score(var_names, strategy::LassoCTMLE) + # use CV-selected optimal lambda from the stored fit + if strategy.initial_fit === nothing + throw(ErrorException("LassoCTMLE requires a GLMNet CV fit stored in `strategy.initial_fit`. Ensure the strategy has been initialized.")) end + + cv_fit = strategy.initial_fit + path = cv_fit.path + + # use optimal lambda from CV (minimum mean loss) + optimal_lambda_idx = argmin(cv_fit.meanloss) + optimal_lambda = cv_fit.lambda[optimal_lambda_idx] + idx = optimal_lambda_idx + + coeffs = path.betas[:, idx] + selected_indices = findall(x -> abs(x) > 1e-6, coeffs) + + if isempty(selected_indices) + @warn "No variables selected by GLMNet at optimal λ=$optimal_lambda, using all variables" + return var_names, cv_fit, idx + end + + selected_vars = var_names[selected_indices] + return selected_vars, cv_fit, idx end """ @@ -102,89 +83,23 @@ function extract_confounders_from_estimand(Ψ) end function initialise!(strategy::LassoCTMLE, Ψ) - log_info(strategy, "LassoCTMLE: Initialising collaborative lambda exploration") - strategy.current_iteration = 0 - empty!(strategy.explored_lambdas) - strategy.best_lambda = nothing - strategy.best_cv_loss = Inf - - if isempty(strategy.lambda_path) - log_info(strategy, "LassoCTMLE: Will generate CV lambda sequence when data is available") - else - log_info(strategy, "LassoCTMLE: Using provided lambda sequence: $(strategy.lambda_path[1:min(3, length(strategy.lambda_path))])...") - end - + strategy.used = false return nothing end -function update!(strategy::LassoCTMLE, g, ĝ) - strategy.current_iteration += 1 - current_lambda = strategy.lambda_path[min(strategy.current_iteration, length(strategy.lambda_path))] - push!(strategy.explored_lambdas, current_lambda) - - log_info(strategy, "LassoCTMLE: Collaborative update $(strategy.current_iteration)/$(strategy.patience) - explored λ = $current_lambda") - log_info(strategy, "LassoCTMLE: Explored lambdas so far: $(sort(collect(strategy.explored_lambdas)))") - - return nothing -end - -function update_with_loss!(strategy::LassoCTMLE, g, ĝ, cv_loss::Float64) - current_lambda = strategy.lambda_path[min(strategy.current_iteration, length(strategy.lambda_path))] - - if cv_loss < strategy.best_cv_loss - log_info(strategy, "LassoCTMLE: New best λ = $current_lambda with CV loss = $cv_loss (previous best: $(strategy.best_cv_loss))") - strategy.best_lambda = current_lambda - strategy.best_cv_loss = cv_loss - else - log_info(strategy, "LassoCTMLE: λ = $current_lambda with CV loss = $cv_loss (keeping best λ = $(strategy.best_lambda))") - end - - return nothing -end - -function update!(strategy::LassoCTMLE, g, ĝ, cv_loss::Float64) - strategy.current_iteration += 1 - current_lambda = strategy.lambda_path[min(strategy.current_iteration, length(strategy.lambda_path))] - push!(strategy.explored_lambdas, current_lambda) - - if cv_loss < strategy.best_cv_loss - strategy.best_lambda = current_lambda - strategy.best_cv_loss = cv_loss - log_info(strategy, "LassoCTMLE: New best λ = $current_lambda (CV loss = $cv_loss)") - end - - log_info(strategy, "LassoCTMLE: Collaborative update $(strategy.current_iteration)/$(strategy.patience) - explored λ = $current_lambda") - log_info(strategy, "LassoCTMLE: Explored lambdas so far: $(sort(collect(strategy.explored_lambdas)))") - log_info(strategy, "LassoCTMLE: Current best λ = $(strategy.best_lambda) (best CV loss = $(strategy.best_cv_loss))") - - return nothing -end +update!(strategy::LassoCTMLE, g, ĝ) = nothing finalise!(strategy::LassoCTMLE) = nothing function exhausted(strategy::LassoCTMLE) - if isempty(strategy.lambda_path) && strategy.current_iteration == 0 - log_info(strategy, "LassoCTMLE: Not exhausted - CV lambda generation pending") - return false - end - - is_exhausted = strategy.current_iteration >= strategy.patience || - length(strategy.explored_lambdas) >= length(strategy.lambda_path) - - if is_exhausted && strategy.best_lambda !== nothing - log_info(strategy, "LassoCTMLE: Collaborative exploration complete - best λ = $(strategy.best_lambda) (CV loss = $(strategy.best_cv_loss))") - else - log_info(strategy, "LassoCTMLE: Checking exhaustion - iteration $(strategy.current_iteration)/$(strategy.patience), exhausted: $is_exhausted") - end - - return is_exhausted + # strategy runs once with CV-optimal lambda, then is exhausted + return strategy.used end """ Create propensity score specification using the given confounders list. """ function propensity_score(Ψ, confounders_list::Vector{Symbol}, strategy::LassoCTMLE) - log_info(strategy, "LassoCTMLE: Creating propensity score specification with confounders: $confounders_list") Ψtreatments = TMLE.treatments(Ψ) return Tuple(map(eachindex(Ψtreatments)) do index T = Ψtreatments[index] @@ -198,69 +113,68 @@ end Get propensity score specification from the collaborative strategy. """ function propensity_score(Ψ, strategy::LassoCTMLE) - log_info(strategy, "LassoCTMLE: Getting propensity score from strategy (auto-extracting confounders from estimand)") confounders = extract_confounders_from_estimand(Ψ) return propensity_score(Ψ, confounders, strategy) end """ Iterator implementation for LASSO-based collaborative TMLE. -Explores different lambda values with GLMNet regularization. +Runs once with GLMNet CV-selected optimal lambda. """ function Base.iterate(it::TMLE.StepKPropensityScoreIterator{LassoCTMLE}) strategy = it.collaborative_strategy - if isempty(strategy.lambda_path) - log_info(strategy, "LassoCTMLE: Generating CV lambda sequence") + # only run once + if strategy.used + return nothing + end + + # extract confounders once (used throughout) + confounders = extract_confounders_from_estimand(it.Ψ) + + # run GLMNet CV if not already done + if strategy.initial_fit === nothing treatment_var = first(TMLE.treatments(it.Ψ)) y_binary = Int.(unwrap.(it.dataset[!, treatment_var])) - confounders = extract_confounders_from_estimand(it.Ψ) confounder_data = it.dataset[!, confounders] X_matrix = Matrix{Float64}(confounder_data) - try - auto_fit = GLMNet.glmnet(X_matrix, y_binary, alpha=strategy.alpha) - min_lambda = minimum(auto_fit.lambda) - strong_lambdas = auto_fit.lambda[auto_fit.lambda .>= min_lambda] - - n_lambdas = min(strategy.patience * 2, length(strong_lambdas)) - strategy.lambda_path = strong_lambdas[1:n_lambdas] - - log_info(strategy, "LassoCTMLE: Generated $(length(strategy.lambda_path)) CV lambdas from $(round(minimum(strategy.lambda_path), digits=6)) to $(round(maximum(strategy.lambda_path), digits=3))") - catch e - strategy.verbose && @warn "LassoCTMLE: CV lambda generation failed, using fallback: $e" - strategy.lambda_path = exp10.(range(-2, stop = 0, length = strategy.patience)) - end + # run CV to get optimal lambda + strategy.initial_fit = GLMNet.glmnetcv(X_matrix, y_binary, alpha=strategy.alpha, nfolds=strategy.cv_folds) + # find lambda with minimum CV loss + optimal_lambda_idx = argmin(strategy.initial_fit.meanloss) + optimal_lambda = strategy.initial_fit.lambda[optimal_lambda_idx] + log_info(strategy, "LassoCTMLE: CV selected λ=$optimal_lambda") end - available_lambdas = setdiff(strategy.lambda_path, strategy.explored_lambdas) - isempty(available_lambdas) && return nothing - - current_lambda = first(available_lambdas) + # get variable selection from CV fit + selected_confounders, glm_fit, lambda_idx = fit_glmnet_propensity_score(confounders, strategy) - log_info(strategy, "LassoCTMLE: TRUE GLMNet collaborative candidate λ = $current_lambda (iteration $(strategy.current_iteration + 1))") + log_info(strategy, "LassoCTMLE: Selected $(length(selected_confounders))/$(length(confounders)) confounders") - treatment_var = first(TMLE.treatments(it.Ψ)) - y_binary = Int.(unwrap.(it.dataset[!, treatment_var])) - confounders = extract_confounders_from_estimand(it.Ψ) - confounder_data = it.dataset[!, confounders] - X_matrix = Matrix{Float64}(confounder_data) - - selected_confounders, glm_fit = fit_glmnet_propensity_score( - X_matrix, y_binary, strategy.alpha, current_lambda, confounders, strategy - ) + # build propensity score specification + g = propensity_score(it.Ψ, selected_confounders, strategy) - log_info(strategy, "LassoCTMLE: GLMNet α=$(strategy.alpha), λ=$current_lambda → $(length(selected_confounders))/$(length(confounders)) confounders") - log_info(strategy, "LassoCTMLE: Selected confounders: $selected_confounders") + # build prefit estimator using CV coefficients (no refitting) + path = glm_fit.path + selected_indices = [findfirst(==(v), confounders) for v in selected_confounders] + coeffs_full = path.betas[:, lambda_idx] + coeffs_selected = coeffs_full[selected_indices] + intercept = path.a0[lambda_idx] - g = propensity_score(it.Ψ, selected_confounders, strategy) - models = it.models + components = Dict{Symbol, Tuple}() + for cd in g + components[cd.outcome] = (selected_confounders, coeffs_selected, intercept) + end + ĝ = TMLE.PrefitGLMNetJointConditionalDistributionEstimator(components) - ĝ = TMLE.build_propensity_score_estimator(g, models, it.dataset; train_validation_indices=nothing) + strategy.used = true - log_info(strategy, "LassoCTMLE: Built TRUE GLMNet collaborative candidate with $(length(g)) propensity score component(s)") + # return optimal lambda (computed once above or from cached fit) + optimal_lambda_idx = argmin(glm_fit.meanloss) + optimal_lambda = glm_fit.lambda[optimal_lambda_idx] - return (g, ĝ), current_lambda + return (g, ĝ), optimal_lambda end Base.iterate(it::TMLE.StepKPropensityScoreIterator{LassoCTMLE}, state) = nothing diff --git a/src/estimates.jl b/src/estimates.jl index 21761401..a1b7885a 100644 --- a/src/estimates.jl +++ b/src/estimates.jl @@ -31,6 +31,35 @@ function MLJBase.predict(estimate::MLConditionalDistribution, dataset) return predict(estimate.machine, X) end +##################################################################### +### PrefitGLMNetConditionalDistribution ### +##################################################################### + +""" +Holds a precomputed GLMNet estimate that uses stored coefficients without refitting. +Used internally by LassoCTMLE strategy to avoid refitting per lambda candidate. +""" +struct PrefitGLMNetConditionalDistribution <: Estimate + estimand::ConditionalDistribution + varnames::Vector{Symbol} + coeffs::Vector{Float64} + intercept::Float64 +end + +string_repr(estimate::PrefitGLMNetConditionalDistribution) = + string("P̂(", estimate.estimand.outcome, " | ", join(estimate.estimand.parents, ", "), + "), prefit GLMNet with ", length(estimate.varnames), " variables") + +function MLJBase.predict(estimate::PrefitGLMNetConditionalDistribution, dataset) + X = selectcols(dataset, estimate.varnames) + Xmat = Matrix{Float64}(X) + η = estimate.intercept .+ Xmat * estimate.coeffs + ps = 1 ./ (1 .+ exp.(-η)) + # Return UnivariateFinite for binary outcomes (compatible with categorical treatment) + outcome_levels = [0, 1] + probs = hcat(1 .- ps, ps) + return MLJBase.UnivariateFinite(outcome_levels, probs, pool=missing) +end ##################################################################### ### SampleSplitMLConditionalDistribution ### @@ -123,16 +152,16 @@ end ### ConditionalDistributionEstimate ### ##################################################################### -ConditionalDistributionEstimate = Union{MLConditionalDistribution, SampleSplitMLConditionalDistribution} +ConditionalDistributionEstimate = Union{MLConditionalDistribution, SampleSplitMLConditionalDistribution, PrefitGLMNetConditionalDistribution} function expected_value(estimate::ConditionalDistributionEstimate, dataset) return expected_value(predict(estimate, dataset)) end function likelihood(estimate::ConditionalDistributionEstimate, dataset) - ŷ = predict(estimate, dataset) + ŷ = predict(estimate, dataset) y = dataset[!, estimate.estimand.outcome] - return pdf.(ŷ, y) + return pdf.(ŷ, y) end function compute_offset(ŷ::AbstractVector{<:UnivariateFinite{<:Union{OrderedFactor{2}, Multiclass{2}}}}) diff --git a/src/estimators.jl b/src/estimators.jl index ef3cdd88..15909a21 100644 --- a/src/estimators.jl +++ b/src/estimators.jl @@ -181,6 +181,32 @@ ConditionalDistributionEstimator(model, train_validation_indices::AbstractVector cd_estimators::Dict{Symbol, Any} end +##################################################################### +### PrefitGLMNetJointConditionalDistributionEstimator ### +##################################################################### + +""" +Estimator that returns prefit GLMNet estimates without refitting. +Used internally by LassoCTMLE to avoid refitting per lambda. +""" +@auto_hash_equals struct PrefitGLMNetJointConditionalDistributionEstimator <: Estimator + components::Dict{Symbol, Tuple} # outcome => (varnames, coeffs, intercept) +end + +function (estimator::PrefitGLMNetJointConditionalDistributionEstimator)(conditional_distributions, dataset; + cache=Dict(), + verbosity=1, + machine_cache=false, + acceleration=CPU1() + ) + estimates = map(conditional_distributions) do cd + outcome = cd.outcome + varnames, coeffs, intercept = estimator.components[outcome] + PrefitGLMNetConditionalDistribution(cd, varnames, coeffs, intercept) + end + return JointConditionalDistributionEstimate(conditional_distributions, Tuple(estimates)) +end + function fit_conditional_distributions(acceleration::CPU1, cd_estimators, conditional_distributions, dataset; cache=Dict(), verbosity=1, machine_cache=false) return map(conditional_distributions) do conditional_distribution cd_estimator = cd_estimators[conditional_distribution.outcome] diff --git a/test/Project.toml b/test/Project.toml index 20a29849..5125ad92 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -7,7 +7,6 @@ DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" HypothesisTests = "09f84164-cd44-5f33-b23f-e6b0d136a0d5" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" LogExpFunctions = "2ab3a3ac-af41-5b50-aa03-7779005ae688" MLJBase = "a7f614a8-145f-11e9-1d2a-a57a1082229d" MLJGLMInterface = "caf8df21-4939-456d-ac9c-5fefbfb04c0c" @@ -24,7 +23,6 @@ StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" TableOperations = "ab02a1b2-a7df-11e8-156e-fb1833f50b87" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -ToeplitzMatrices = "c751599d-da0a-543b-9d20-d0a503d91d24" YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" [compat] diff --git a/test/counterfactual_mean_based/lasso_strategy.jl b/test/counterfactual_mean_based/lasso_strategy.jl index e97aafb1..b9edde42 100644 --- a/test/counterfactual_mean_based/lasso_strategy.jl +++ b/test/counterfactual_mean_based/lasso_strategy.jl @@ -17,10 +17,10 @@ end @testset "Basic construction and defaults" begin strategy = LassoCTMLE() - @test strategy.patience == 5 - @test length(strategy.lambda_path) == 0 + @test strategy.cv_folds == 5 @test strategy.alpha == 1.0 - @test strategy.current_iteration == 0 + @test strategy.initial_fit === nothing + @test strategy.used == false end @testset "LASSO CTMLE with automatic CV lambda selection" begin