StochTree 0.5.0.9000
Loading...
Searching...
No Matches
bcf.h
1
5#ifndef STOCHTREE_BCF_H_
6#define STOCHTREE_BCF_H_
7
8#include <memory>
9#include <vector>
10#include <stochtree/container.h>
11#include <stochtree/log.h>
12#include <stochtree/meta.h>
13#include <stochtree/random_effects.h>
14
15namespace StochTree {
16
17enum class BCFRFXModelSpec {
18 Custom,
19 InterceptOnly,
20 InterceptPlusTreatment
21};
22
23struct BCFData {
24 // Train set covariates
25 double* X_train = nullptr;
26 int n_train = 0;
27 int p = 0;
28
29 // Test set covariates
30 double* X_test = nullptr;
31 int n_test = 0;
32
33 // Treatment
34 double* treatment_train = nullptr;
35 double* treatment_test = nullptr;
36 int treatment_dim = 0;
37
38 // Train set outcome
39 double* y_train = nullptr;
40
41 // Observation weights
42 double* obs_weights_train = nullptr;
43 double* obs_weights_test = nullptr;
44
45 // Random effects
46 int* rfx_group_ids_train = nullptr;
47 int* rfx_group_ids_test = nullptr;
48 double* rfx_basis_train = nullptr;
49 double* rfx_basis_test = nullptr;
50 int rfx_num_groups = 0;
51 int rfx_basis_dim = 0;
52};
53
54struct BCFConfig {
55 // High level parameters
56 bool standardize_outcome = true; // whether to standardize the outcome before fitting and unstandardize predictions after
57 int num_threads = 1; // number of threads to use for sampling
58 bool verbose = false; // whether to print sampler progress to the console
59 int cutpoint_grid_size = 100; // number of cutpoints to consider for each covariate when sampling splits
60 std::vector<FeatureType> feature_types; // feature types for each covariate (should be same length as number of covariates in the dataset), where 0 = continuous, 1 = categorical
61 LinkFunction link_function = LinkFunction::Identity; // link function to use (Identity, Probit, Cloglog)
62 OutcomeType outcome_type = OutcomeType::Continuous; // type of the outcome variable (Continuous, Binary, Ordinal)
63 int random_seed = -1; // random seed for reproducibility (if negative, a random seed will be generated)
64 bool keep_gfr = true; // whether or not to keep GFR samples or simply use them to warm-start an MCMC chain
65 bool keep_burnin = false; // whether or not to keep "burn-in" MCMC samples (largely a debugging flag)
66 bool adaptive_coding = false; // whether or not to use adaptive coding for the BCF model
67 double b_0_init = 0.0; // initial value for the b_0 parameter in the adaptive coding scheme (only relevant if adaptive_coding=true)
68 double b_1_init = 1.0; // initial value for the b_1 parameter in the adaptive coding scheme (only relevant if adaptive_coding=true)
69
70 // Global error variance parameters
71 double a_sigma2_global = 0.0; // shape parameter for inverse gamma prior on global error variance
72 double b_sigma2_global = 0.0; // scale parameter for inverse gamma prior on global error variance
73 double sigma2_global_init = 1.0; // initial value for global error variance
74 bool sample_sigma2_global = true; // whether to sample global error variance (if false, it will be fixed at sigma2_global_init)
75
76 // Prognostic forest parameters
77 int num_trees_mu = 200; // number of trees in the prognostic forest
78 double alpha_mu = 0.95; // alpha parameter for prognostic forest tree prior
79 double beta_mu = 2.0; // beta parameter for prognostic forest tree prior
80 int min_samples_leaf_mu = 5; // minimum number of samples per leaf for prognostic forest
81 int max_depth_mu = -1; // maximum depth for prognostic forest trees (-1 means no maximum)
82 bool leaf_constant_mu = true; // whether to use constant leaf model for prognostic forest
83 int leaf_dim_mu = 1; // dimension of the leaf for prognostic forest
84 bool exponentiated_leaf_mu = false; // whether to exponentiate leaf predictions for prognostic forest
85 int num_features_subsample_mu = 0; // number of features to subsample for each prognostic forest split (0 means no subsampling)
86 double a_sigma2_mu = 3.0; // shape parameter for inverse gamma prior on prognostic forest leaf scale
87 double b_sigma2_mu = -1.0; // scale parameter for inverse gamma prior on prognostic forest leaf scale (-1 is a sentinel value that triggers a data-informed calibration based on the variance of the outcome and the number of trees)
88 double sigma2_mu_init = -1.0; // initial value of prognostic forest leaf scale (-1 is a sentinel value that triggers a data-informed calibration based on the variance of the outcome and the number of trees)
89 std::vector<double> var_weights_mu; // variable weights for prognostic forest splits (should be same length as number of covariates in the dataset)
90 bool sample_sigma2_leaf_mu = false; // whether to sample prognostic forest leaf scale (if false, it will be fixed at sigma2_mu_init)
91 std::vector<int> sweep_update_indices_mu; // indices of trees to update in a given sweep (should be subset of [0, num_trees - 1])
92
93 // Treatment effect forest parameters
94 int num_trees_tau = 50; // number of trees in the treatment effect forest
95 double alpha_tau = 0.95; // alpha parameter for treatment effect forest tree prior
96 double beta_tau = 2.0; // beta parameter for treatment effect forest tree prior
97 int min_samples_leaf_tau = 5; // minimum number of samples per leaf for treatment effect forest
98 int max_depth_tau = -1; // maximum depth for treatment effect forest trees (-1 means no maximum)
99 bool leaf_constant_tau = false; // whether to use constant leaf model for treatment effect forest (false for univariate/multivariate regression leaf, true for constant leaf)
100 int leaf_dim_tau = 1; // dimension of the leaf for treatment effect forest
101 bool exponentiated_leaf_tau = false; // whether to exponentiate leaf predictions for treatment effect forest
102 int num_features_subsample_tau = 0; // number of features to subsample for each treatment effect forest split (0 means no subsampling)
103 double a_sigma2_tau = 3.0; // shape parameter for inverse gamma prior on treatment effect forest leaf scale
104 double b_sigma2_tau = -1.0; // scale parameter for inverse gamma prior on treatment effect forest leaf scale (-1 is a sentinel value that triggers a data-informed calibration based on the variance of the outcome and the number of trees)
105 double sigma2_tau_init = -1.0; // initial value of treatment effect forest leaf scale (-1 is a sentinel value that triggers a data-informed calibration based on the variance of the outcome and the number of trees)
106 std::vector<double> var_weights_tau; // variable weights for treatment effect forest splits (should be same length as number of covariates in the dataset)
107 std::vector<double> sigma2_leaf_tau_matrix; // prior covariance matrix Sigma_0 for multivariate leaf regression, stored column-major (size leaf_dim_tau^2); empty = use sigma2_tau_init * I
108 bool sample_sigma2_leaf_tau = false; // whether to sample treatment effect forest leaf scale (if false, it will be fixed at sigma2_tau_init)
109 std::vector<int> sweep_update_indices_tau; // indices of trees to update in a given sweep (should be subset of [0, num_trees - 1])
110 MeanLeafModelType tau_leaf_model_type = MeanLeafModelType::GaussianUnivariateRegression; // leaf model type for treatment effect forest
111 bool sample_tau_0 = true; // whether or not to sample an intercept term on the treatment, additive to the covariate-dependent treatment effect forest
112 double tau_0_prior_var_scalar = -1.0; // scalar-valued prior variance for treatment intercept (only relevant when sample_tau_0=true; -1 is a sentinel value that triggers a data-informed calibration)
113 std::vector<double> tau_0_prior_var_multivariate; // vector-valued prior variance for treatment intercept in multivariate treatment case (only relevant when sample_tau_0=true; should be of length treatment_dim; empty = use data-informed calibration)
114
115 // Variance forest parameters
116 int num_trees_variance = 0; // number of trees in the variance forest
117 double leaf_prior_calibration_param = 1.5; // calibration parameter for variance forest leaf prior
118 double shape_variance_forest = -1.0; // shape parameter for variance forest leaf model (calibrated internally based on leaf_prior_calibration_param if set to sentinel value of -1)
119 double scale_variance_forest = -1.0; // scale parameter for variance forest leaf model (calibrated internally based on leaf_prior_calibration_param if set to sentinel value of -1)
120 double variance_forest_leaf_init = -1.0; // initial (raw-scale) root value for the variance forest; each leaf starts at log(value)/num_trees_variance. Sentinel <= 0 = calibrate internally
121 double alpha_variance = 0.5; // alpha parameter for variance forest tree prior
122 double beta_variance = 2.0; // beta parameter for variance forest tree prior
123 int min_samples_leaf_variance = 5; // minimum number of samples per leaf for variance forest
124 int max_depth_variance = -1; // maximum depth for variance forest trees (-1 means no maximum)
125 bool leaf_constant_variance = true; // whether to use constant leaf model for variance forest
126 int leaf_dim_variance = 1; // dimension of the leaf for variance forest (should be 1 if leaf_constant_variance=true)
127 bool exponentiated_leaf_variance = true; // whether to exponentiate leaf predictions for variance forest
128 int num_features_subsample_variance = 0; // number of features to subsample for each variance forest split (0 means no subsampling)
129 std::vector<double> var_weights_variance; // variable weights for variance forest splits (should be same length as number of covariates in the dataset)
130 std::vector<int> sweep_update_indices_variance; // indices of trees to update in a given sweep (should be subset of [0, num_trees - 1])
131
132 // Random effects parameters
133 bool has_random_effects = false; // whether or not a model includes a random effects term
134 BCFRFXModelSpec rfx_model_spec = BCFRFXModelSpec::Custom; // specification for the random effects model; custom relies on a user-provided basis while intercept-only constructs a varying intercept model without needing a user-provided basis
135 std::vector<double> rfx_working_parameter_mean_prior; // vector of dimension num_basis; empty = use zeros
136 std::vector<double> rfx_group_parameter_mean_prior; // matrix of dimension num_basis x num_groups, stored column-major; empty = use zeros
137 std::vector<double> rfx_working_parameter_cov_prior; // matrix of dimension num_basis x num_basis, stored column-major; empty = use identity matrix
138 std::vector<double> rfx_group_parameter_cov_prior; // matrix of dimension num_basis x num_basis, stored column-major; empty = use identity matrix
139 double rfx_variance_prior_shape = 1.0; // shape parameter for variance prior in random effects model
140 double rfx_variance_prior_scale = 1.0; // scale parameter for variance prior in random effects model
141
142 // TODO: Other parameters ...
143};
144
146 // Posterior samples of training set outcome predictions (n_train x num_samples, stored column-major)
147 std::vector<double> y_hat_train;
148
149 // Posterior samples of training set prognostic forest predictions (n_train x num_samples, stored column-major)
150 std::vector<double> mu_forest_predictions_train;
151
152 // Posterior samples of training set treatment effect forest predictions (n_train x num_samples, stored column-major)
153 std::vector<double> tau_forest_predictions_train;
154
155 // Posterior samples of training set variance forest predictions (n_train x num_samples, stored column-major)
156 std::vector<double> variance_forest_predictions_train;
157
158 // Posterior samples of test set outcome predictions (n_test x num_samples, stored column-major)
159 std::vector<double> y_hat_test;
160
161 // Posterior samples of test set prognostic forest predictions (n_test x num_samples, stored column-major)
162 std::vector<double> mu_forest_predictions_test;
163
164 // Posterior samples of test set treatment effect forest predictions (n_test x num_samples, stored column-major)
165 std::vector<double> tau_forest_predictions_test;
166
167 // Posterior samples of test set variance forest predictions (n_test x num_samples, stored column-major)
168 std::vector<double> variance_forest_predictions_test;
169
170 // Posterior samples of global error variance (num_samples)
171 std::vector<double> global_error_variance_samples;
172
173 // Posterior samples of leaf scale (num_samples)
174 std::vector<double> leaf_scale_mu_samples;
175 std::vector<double> leaf_scale_tau_samples;
176
177 // Pointer to sampled prognostic forests
178 std::unique_ptr<ForestContainer> mu_forests;
179
180 // Pointer to sampled treatment effect forests
181 std::unique_ptr<ForestContainer> tau_forests;
182
183 // Pointer to sampled variance forests
184 std::unique_ptr<ForestContainer> variance_forests;
185
186 // Posterior samples of training set RFX predictions (n_train x num_samples, stored column-major)
187 std::vector<double> rfx_predictions_train;
188
189 // Posterior samples of test set RFX predictions (n_test x num_samples, stored column-major)
190 std::vector<double> rfx_predictions_test;
191
192 // Treatment intercept samples (treatment_dim x num_samples, stored column-major; only populated when sample_tau_0=true)
193 std::vector<double> tau_0_samples;
194
195 // Adaptive coding parameter samples
196 std::vector<double> b0_samples;
197 std::vector<double> b1_samples;
198
199 // Pointer to random effects sample container and label mapping
200 std::unique_ptr<RandomEffectsContainer> rfx_container;
201 std::unique_ptr<LabelMapper> rfx_label_mapper;
202
203 // Metadata about the samples (e.g., number of samples, burn-in, etc.) could be added here as needed
204 int num_samples = 0;
205 int num_train = 0;
206 int num_test = 0;
207 int treatment_dim = 0;
208 double y_bar = 0.0;
209 double y_std = 0.0;
210
211 // Serialize the samples-owned subtree (forests + parameter traces + intrinsic scalars) into a JSON
212 // object. BCF mirror of BARTSamples::ToJson -- see that method for the design notes (shared C++
213 // source of truth, byte-identical key layout, presence inferred from structure, nlohmann sorts
214 // keys so insertion order is irrelevant). Forests use the BCF named keys; the extra BCF parameter
215 // traces (leaf_scale_mu/tau, tau_0, b0/b1) go under the same "parameters" subfolder.
216 // NOTE: parameter traces are serialized verbatim (identity). The tau_0 user-facing scaling
217 // (x y_std) and its multivariate (treatment_dim>1) ravel-order are reconciled at the postprocess /
218 // wiring boundary per the locked scale decision, not here. Random effects are not yet routed
219 // through this path (guarded to avoid silent drops).
220 void AppendToJson(nlohmann::json& obj) const {
221 // Forests, under the BCF self-describing named keys, with the num_forests counter
222 nlohmann::json forests = nlohmann::json::object();
223 int num_forests = 0;
224 if (mu_forests != nullptr) {
225 forests.emplace("prognostic_forest", mu_forests->to_json());
226 num_forests++;
227 }
228 if (tau_forests != nullptr) {
229 forests.emplace("treatment_forest", tau_forests->to_json());
230 num_forests++;
231 }
232 if (variance_forests != nullptr) {
233 forests.emplace("variance_forest", variance_forests->to_json());
234 num_forests++;
235 }
236 obj["forests"] = forests;
237 obj["num_forests"] = num_forests;
238 // Parameter traces, under the "parameters" subfolder (presence inferred from non-empty vectors)
239 nlohmann::json parameters = nlohmann::json::object();
240 if (!global_error_variance_samples.empty()) {
241 parameters.emplace("sigma2_global_samples", global_error_variance_samples);
242 }
243 if (!leaf_scale_mu_samples.empty()) {
244 parameters.emplace("sigma2_leaf_mu_samples", leaf_scale_mu_samples);
245 }
246 if (!leaf_scale_tau_samples.empty()) {
247 parameters.emplace("sigma2_leaf_tau_samples", leaf_scale_tau_samples);
248 }
249 if (!b0_samples.empty()) {
250 parameters.emplace("b0_samples", b0_samples);
251 }
252 if (!b1_samples.empty()) {
253 parameters.emplace("b1_samples", b1_samples);
254 }
255 if (!tau_0_samples.empty()) {
256 parameters.emplace("tau_0_samples", tau_0_samples);
257 obj.emplace("tau_0_dim", treatment_dim);
258 }
259 if (!parameters.empty()) {
260 obj["parameters"] = parameters;
261 }
262 // Intrinsic scalars (stored in user-facing scale, matching the existing wire format)
263 obj.emplace("outcome_mean", y_bar);
264 obj.emplace("outcome_scale", y_std);
265 obj.emplace("num_samples", num_samples);
266 obj.emplace("treatment_dim", treatment_dim);
267 // Random effects
268 int num_random_effects = 0;
269 nlohmann::json rfx = nlohmann::json::object();
270 if (rfx_container != nullptr && rfx_label_mapper != nullptr) {
271 rfx.emplace("random_effect_container_0", rfx_container->to_json());
272 rfx.emplace("random_effect_label_mapper_0", rfx_label_mapper->to_json());
273 rfx.emplace("random_effect_groupids_0", rfx_label_mapper->Keys());
275 }
276 obj["random_effects"] = rfx;
277 obj["num_random_effects"] = num_random_effects;
278 }
279
280 // Populate this BCFSamples from the samples-owned subtree of a parsed JSON object. Inverse of
281 // ToJson(); presence inferred from structure rather than envelope booleans. See ToJson() re: scope.
282 void FromJson(const nlohmann::json& obj) {
283 // Unpack forests if present, checking for the expected keys
284 if (obj.contains("forests")) {
285 const nlohmann::json& forests = obj.at("forests");
286 if (forests.contains("prognostic_forest")) {
287 mu_forests = std::make_unique<ForestContainer>(0, 0, false, false);
288 mu_forests->from_json(forests.at("prognostic_forest"));
289 }
290 if (forests.contains("treatment_forest")) {
291 tau_forests = std::make_unique<ForestContainer>(0, 0, false, false);
292 tau_forests->from_json(forests.at("treatment_forest"));
293 }
294 if (forests.contains("variance_forest")) {
295 variance_forests = std::make_unique<ForestContainer>(0, 0, false, false);
296 variance_forests->from_json(forests.at("variance_forest"));
297 }
298 }
299 // Unpack parameters if present, checking for expected keys
300 if (obj.contains("parameters")) {
301 const nlohmann::json& parameters = obj.at("parameters");
302 if (parameters.contains("sigma2_global_samples")) {
303 global_error_variance_samples = parameters.at("sigma2_global_samples").get<std::vector<double>>();
304 }
305 if (parameters.contains("sigma2_leaf_mu_samples")) {
306 leaf_scale_mu_samples = parameters.at("sigma2_leaf_mu_samples").get<std::vector<double>>();
307 }
308 if (parameters.contains("sigma2_leaf_tau_samples")) {
309 leaf_scale_tau_samples = parameters.at("sigma2_leaf_tau_samples").get<std::vector<double>>();
310 }
311 if (parameters.contains("b0_samples")) {
312 b0_samples = parameters.at("b0_samples").get<std::vector<double>>();
313 }
314 if (parameters.contains("b1_samples")) {
315 b1_samples = parameters.at("b1_samples").get<std::vector<double>>();
316 }
317 if (parameters.contains("tau_0_samples")) {
318 tau_0_samples = parameters.at("tau_0_samples").get<std::vector<double>>();
319 }
320 }
321 // Unpack random effects if present, checking for expected keys
322 if (obj.contains("num_random_effects") && obj.at("num_random_effects").get<int>() > 0) {
323 rfx_container = std::make_unique<RandomEffectsContainer>();
324 rfx_label_mapper = std::make_unique<LabelMapper>();
325 rfx_container->from_json(obj.at("random_effects").at("random_effect_container_0"));
326 rfx_label_mapper->from_json(obj.at("random_effects").at("random_effect_label_mapper_0"));
327 }
328 // Unpack outcome statistics
329 if (obj.contains("outcome_mean")) y_bar = obj.at("outcome_mean").get<double>();
330 if (obj.contains("outcome_scale")) y_std = obj.at("outcome_scale").get<double>();
331 if (obj.contains("num_samples")) num_samples = obj.at("num_samples").get<int>();
332 if (obj.contains("treatment_dim")) treatment_dim = obj.at("treatment_dim").get<int>();
333 }
334
335 // Append another chain's draws onto this one (multi-chain combine). BCF mirror of
336 // BARTSamples::Merge -- `this` must already be populated, `other` must match model structure
337 // (same forests present, same standardization, same treatment_dim). Forests are deep-copied
338 // sample-by-sample and parameter traces concatenated, preserving draw order.
339 void Merge(const BCFSamples& other) {
340 // Runtime checks for samples objects to be combined
341 if (y_bar != other.y_bar || y_std != other.y_std) {
342 Log::Fatal("Cannot merge BCFSamples with different outcome standardization");
343 }
344 if (rfx_container != nullptr && other.rfx_container != nullptr) {
345 if (rfx_container->NumComponents() != other.rfx_container->NumComponents() ||
346 rfx_container->NumGroups() != other.rfx_container->NumGroups()) {
347 Log::Fatal("Cannot merge BARTSamples with different random effects structure");
348 }
349 if (rfx_label_mapper->Keys() != other.rfx_label_mapper->Keys()) {
350 Log::Fatal("Cannot merge BARTSamples with different random effects label mapping");
351 }
352 if (rfx_label_mapper->Map() != other.rfx_label_mapper->Map()) {
353 Log::Fatal("Cannot merge BARTSamples with different random effects label mapping");
354 }
355 }
356 if (treatment_dim != other.treatment_dim) {
357 Log::Fatal("Cannot merge BCFSamples with different treatment_dim");
358 }
359 // Append forests if they exist in the samples object
360 AppendForestContainerSamples(mu_forests, other.mu_forests, "prognostic");
361 AppendForestContainerSamples(tau_forests, other.tau_forests, "treatment");
362 AppendForestContainerSamples(variance_forests, other.variance_forests, "variance");
363 // Append random effects if they exist in the samples object
364 AppendRandomEffectsContainerSamples(rfx_container, other.rfx_container);
365 // Append parameters samples
366 auto append = [](std::vector<double>& dst, const std::vector<double>& src, const std::string& name = "") {
367 if ((!dst.empty() && src.empty()) || (dst.empty() && !src.empty())) {
368 Log::Fatal("Cannot merge BARTSamples objects: %s samples present in one chain but not the other", name.c_str());
369 }
370 dst.insert(dst.end(), src.begin(), src.end());
371 };
372 append(global_error_variance_samples, other.global_error_variance_samples, "global error variance");
373 append(leaf_scale_mu_samples, other.leaf_scale_mu_samples, "leaf scale mu");
374 append(leaf_scale_tau_samples, other.leaf_scale_tau_samples, "leaf scale tau");
375 append(tau_0_samples, other.tau_0_samples, "tau_0");
376 append(b0_samples, other.b0_samples, "b0");
377 append(b1_samples, other.b1_samples, "b1");
378 num_samples += other.num_samples;
379 }
380};
381
382} // namespace StochTree
383
384#endif // STOCHTREE_BCF_H_
static void GFRSampleOneIter(TreeEnsemble &active_forest, ForestTracker &tracker, ForestContainer &forests, LeafModel &leaf_model, ForestDataset &dataset, ColumnVector &residual, TreePrior &tree_prior, std::mt19937 &gen, std::vector< double > &variable_weights, std::vector< int > &sweep_update_indices, double global_variance, std::vector< FeatureType > &feature_types, int cutpoint_grid_size, bool keep_forest, bool pre_initialized, bool backfitting, int num_features_subsample, int num_threads, LeafSuffStatConstructorArgs &... leaf_suff_stat_args)
Definition tree_sampler.h:816
A collection of random number generation utilities.
Definition bart.h:15
void AppendRandomEffectsContainerSamples(std::unique_ptr< RandomEffectsContainer > &dst, const std::unique_ptr< RandomEffectsContainer > &src)
Append every retained random effects sample from src onto the end of dst (deep copy)....
Definition random_effects.h:384
void AppendForestContainerSamples(std::unique_ptr< ForestContainer > &dst, const std::unique_ptr< ForestContainer > &src, const char *name)
Append every retained forest sample from src onto the end of dst (deep copy). Used by BARTSamples::Me...
Definition container.h:229
Definition bcf.h:54
Definition bcf.h:23
Definition bcf.h:145