StochTree 0.5.0.9000
Loading...
Searching...
No Matches
bart.h
1
5#ifndef STOCHTREE_BART_H_
6#define STOCHTREE_BART_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 BARTRFXModelSpec {
18 Custom,
19 InterceptOnly
20};
21
22struct BARTData {
23 // Train set covariates
24 double* X_train = nullptr;
25 int n_train = 0;
26 int p = 0;
27
28 // Test set covariates
29 double* X_test = nullptr;
30 int n_test = 0;
31
32 // Train set outcome
33 double* y_train = nullptr;
34
35 // Basis for leaf regression
36 double* basis_train = nullptr;
37 double* basis_test = nullptr;
38 int basis_dim = 0;
39
40 // Observation weights
41 double* obs_weights_train = nullptr;
42 double* obs_weights_test = nullptr;
43
44 // Random effects
45 int* rfx_group_ids_train = nullptr;
46 int* rfx_group_ids_test = nullptr;
47 double* rfx_basis_train = nullptr;
48 double* rfx_basis_test = nullptr;
49 int rfx_num_groups = 0;
50 int rfx_basis_dim = 0;
51};
52
53struct BARTConfig {
54 // High level parameters
55 bool standardize_outcome = true; // whether to standardize the outcome before fitting and unstandardize predictions after
56 int num_threads = 1; // number of threads to use for sampling
57 bool verbose = false; // whether to print sampler progress to the console
58 int cutpoint_grid_size = 100; // number of cutpoints to consider for each covariate when sampling splits
59 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
60 LinkFunction link_function = LinkFunction::Identity; // link function to use (Identity, Probit, Cloglog)
61 OutcomeType outcome_type = OutcomeType::Continuous; // type of the outcome variable (Continuous, Binary, Ordinal)
62 int random_seed = -1; // random seed for reproducibility (if negative, a random seed will be generated)
63 bool keep_gfr = true; // whether or not to keep GFR samples or simply use them to warm-start an MCMC chain
64 bool keep_burnin = false; // whether or not to keep "burn-in" MCMC samples (largely a debugging flag)
65
66 // Global error variance parameters
67 double a_sigma2_global = 0.0; // shape parameter for inverse gamma prior on global error variance
68 double b_sigma2_global = 0.0; // scale parameter for inverse gamma prior on global error variance
69 double sigma2_global_init = 1.0; // initial value for global error variance
70 bool sample_sigma2_global = true; // whether to sample global error variance (if false, it will be fixed at sigma2_global_init)
71
72 // Mean forest parameters
73 int num_trees_mean = 200; // number of trees in the mean forest
74 double alpha_mean = 0.95; // alpha parameter for mean forest tree prior
75 double beta_mean = 2.0; // beta parameter for mean forest tree prior
76 int min_samples_leaf_mean = 5; // minimum number of samples per leaf for mean forest
77 int max_depth_mean = -1; // maximum depth for mean forest trees (-1 means no maximum)
78 bool leaf_constant_mean = true; // whether to use constant leaf model for mean forest
79 int leaf_dim_mean = 1; // dimension of the leaf for mean forest
80 bool exponentiated_leaf_mean = false; // whether to exponentiate leaf predictions for mean forest
81 int num_features_subsample_mean = 0; // number of features to subsample for each mean forest split (0 means no subsampling)
82 double a_sigma2_mean = 3.0; // shape parameter for inverse gamma prior on mean forest leaf scale
83 double b_sigma2_mean = -1.0; // scale parameter for inverse gamma prior on mean 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)
84 double sigma2_mean_init = -1.0; // initial value of mean 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)
85 std::vector<double> var_weights_mean; // variable weights for mean forest splits (should be same length as number of covariates in the dataset)
86 std::vector<double> sigma2_leaf_mean_matrix; // prior covariance matrix Sigma_0 for multivariate leaf regression, stored column-major (size leaf_dim_mean^2); empty = use sigma2_mean_init * I
87 bool sample_sigma2_leaf_mean = false; // whether to sample mean forest leaf scale (if false, it will be fixed at sigma2_mean_init)
88 std::vector<int> sweep_update_indices_mean; // indices of trees to update in a given sweep (should be subset of [0, num_trees - 1])
89 MeanLeafModelType mean_leaf_model_type = MeanLeafModelType::GaussianConstant; // leaf model type for mean forest
90 int num_classes_cloglog = 0; // number of classes for cloglog ordinal leaf model (should be set if mean_leaf_model_type = CloglogOrdinal)
91 double cloglog_leaf_prior_shape = 2.0; // shape parameter for cloglog ordinal leaf model prior
92 double cloglog_leaf_prior_scale = 2.0; // scale parameter for cloglog ordinal leaf model prior
93 double cloglog_cutpoint_0 = 0.0; // Fixed value of the first log-scale cutpoint for the cloglog model (defaults to 0 for identifiability)
94
95 // Variance forest parameters
96 int num_trees_variance = 0; // number of trees in the variance forest
97 double leaf_prior_calibration_param = 1.5; // calibration parameter for variance forest leaf prior
98 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)
99 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)
100 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
101 double alpha_variance = 0.5; // alpha parameter for variance forest tree prior
102 double beta_variance = 2.0; // beta parameter for variance forest tree prior
103 int min_samples_leaf_variance = 5; // minimum number of samples per leaf for variance forest
104 int max_depth_variance = -1; // maximum depth for variance forest trees (-1 means no maximum)
105 bool leaf_constant_variance = true; // whether to use constant leaf model for variance forest
106 int leaf_dim_variance = 1; // dimension of the leaf for variance forest (should be 1 if leaf_constant_variance=true)
107 bool exponentiated_leaf_variance = true; // whether to exponentiate leaf predictions for variance forest
108 int num_features_subsample_variance = 0; // number of features to subsample for each variance forest split (0 means no subsampling)
109 std::vector<double> var_weights_variance; // variable weights for variance forest splits (should be same length as number of covariates in the dataset)
110 std::vector<int> sweep_update_indices_variance; // indices of trees to update in a given sweep (should be subset of [0, num_trees - 1])
111
112 // Random effects parameters
113 bool has_random_effects = false; // whether or not a model includes a random effects term
114 BARTRFXModelSpec rfx_model_spec = BARTRFXModelSpec::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
115 std::vector<double> rfx_working_parameter_mean_prior; // vector of dimension num_basis; empty = use zeros
116 std::vector<double> rfx_group_parameter_mean_prior; // matrix of dimension num_basis x num_groups, stored column-major; empty = use zeros
117 std::vector<double> rfx_working_parameter_cov_prior; // matrix of dimension num_basis x num_basis, stored column-major; empty = use identity matrix
118 std::vector<double> rfx_group_parameter_cov_prior; // matrix of dimension num_basis x num_basis, stored column-major; empty = use identity matrix
119 double rfx_variance_prior_shape = 1.0; // shape parameter for variance prior in random effects model
120 double rfx_variance_prior_scale = 1.0; // scale parameter for variance prior in random effects model
121
122 // TODO: Other parameters ...
123};
124
126 // Posterior samples of training set mean forest predictions (num_samples x n_train, stored column-major)
127 std::vector<double> mean_forest_predictions_train;
128
129 // Posterior samples of training set variance forest predictions (num_samples x n_train, stored column-major)
130 std::vector<double> variance_forest_predictions_train;
131
132 // Posterior samples of test set mean forest predictions (num_samples x n_test, stored column-major)
133 std::vector<double> mean_forest_predictions_test;
134
135 // Posterior samples of test set variance forest predictions (num_samples x n_test, stored column-major)
136 std::vector<double> variance_forest_predictions_test;
137
138 // Posterior samples of global error variance (num_samples)
139 std::vector<double> global_error_variance_samples;
140
141 // Posterior samples of leaf scale (num_samples)
142 std::vector<double> leaf_scale_samples;
143
144 // Pointer to sampled mean forests
145 std::unique_ptr<ForestContainer> mean_forests;
146
147 // Pointer to sampled variance forests
148 std::unique_ptr<ForestContainer> variance_forests;
149
150 // Posterior samples of cloglog cutpoint parameters (num_samples x num_classes - 1, stored column-major)
151 std::vector<double> cloglog_cutpoint_samples;
152
153 // Posterior samples of training set RFX predictions (num_samples x n_train, stored column-major)
154 std::vector<double> rfx_predictions_train;
155
156 // Posterior samples of test set RFX predictions (num_samples x n_test, stored column-major)
157 std::vector<double> rfx_predictions_test;
158
159 // Pointer to random effects sample container and label mapping
160 std::unique_ptr<RandomEffectsContainer> rfx_container;
161 std::unique_ptr<LabelMapper> rfx_label_mapper;
162
163 // Metadata about the samples (e.g., number of samples, burn-in, etc.) could be added here as needed
164 int num_samples = 0;
165 int num_train = 0;
166 int num_test = 0;
167 double y_bar = 0.0;
168 double y_std = 0.0;
169
170 void AppendToJson(nlohmann::json& obj) const {
171 // Forests, under self-describing named keys, with the num_forests counter
172 nlohmann::json forests = nlohmann::json::object();
173 int num_forests = 0;
174 if (mean_forests != nullptr) {
175 forests.emplace("mean_forest", mean_forests->to_json());
176 num_forests++;
177 }
178 if (variance_forests != nullptr) {
179 forests.emplace("variance_forest", variance_forests->to_json());
180 num_forests++;
181 }
182 // NOTE: use operator[] assignment (not emplace) for the top-level keys below. The R CppJson
183 // envelope pre-creates empty "forests"/"random_effects"/"num_forests"/"num_random_effects"
184 // keys, and nlohmann::json::emplace is a no-op when the key already exists (like std::map),
185 // which would silently drop the forests/rfx. Assignment overwrites whether or not the key exists.
186 obj["forests"] = forests;
187 obj["num_forests"] = num_forests;
188 // Parameter traces, under the "parameters" subfolder (presence inferred from non-empty vectors)
189 nlohmann::json parameters = nlohmann::json::object();
190 if (!global_error_variance_samples.empty()) {
191 parameters.emplace("sigma2_global_samples", global_error_variance_samples);
192 }
193 if (!leaf_scale_samples.empty()) {
194 parameters.emplace("sigma2_leaf_samples", leaf_scale_samples);
195 }
196 if (!cloglog_cutpoint_samples.empty()) {
197 parameters.emplace("cloglog_cutpoint_samples", cloglog_cutpoint_samples);
198 }
199 if (!parameters.empty()) {
200 obj["parameters"] = parameters;
201 }
202 // Intrinsic scalars (stored in user-facing scale, matching the existing wire format)
203 obj["outcome_mean"] = y_bar;
204 obj["outcome_scale"] = y_std;
205 obj["num_samples"] = num_samples;
206 // Random effects
207 int num_random_effects = 0;
208 nlohmann::json rfx = nlohmann::json::object();
209 if (rfx_container != nullptr && rfx_label_mapper != nullptr) {
210 rfx.emplace("random_effect_container_0", rfx_container->to_json());
211 rfx.emplace("random_effect_label_mapper_0", rfx_label_mapper->to_json());
212 rfx.emplace("random_effect_groupids_0", rfx_label_mapper->Keys());
213 num_random_effects = 1;
214 }
215 obj["random_effects"] = rfx;
216 obj["num_random_effects"] = num_random_effects;
217 }
218
219 // Populate this BARTSamples from a parsed JSON object
220 void FromJson(const nlohmann::json& obj) {
221 // Unpack forests if present, checking for the expected keys
222 if (obj.contains("forests")) {
223 const nlohmann::json& forests = obj.at("forests");
224 if (forests.contains("mean_forest")) {
225 mean_forests = std::make_unique<ForestContainer>(0, 0, false, false);
226 mean_forests->from_json(forests.at("mean_forest"));
227 }
228 if (forests.contains("variance_forest")) {
229 variance_forests = std::make_unique<ForestContainer>(0, 0, false, false);
230 variance_forests->from_json(forests.at("variance_forest"));
231 }
232 }
233 // Unpack parameters if present, checking for expected keys
234 if (obj.contains("parameters")) {
235 const nlohmann::json& parameters = obj.at("parameters");
236 if (parameters.contains("sigma2_global_samples")) {
237 global_error_variance_samples = parameters.at("sigma2_global_samples").get<std::vector<double>>();
238 }
239 if (parameters.contains("sigma2_leaf_samples")) {
240 leaf_scale_samples = parameters.at("sigma2_leaf_samples").get<std::vector<double>>();
241 }
242 if (parameters.contains("cloglog_cutpoint_samples")) {
243 cloglog_cutpoint_samples = parameters.at("cloglog_cutpoint_samples").get<std::vector<double>>();
244 }
245 }
246 // Unpack random effects if present, checking for expected keys
247 if (obj.contains("num_random_effects") && obj.at("num_random_effects").get<int>() > 0) {
248 rfx_container = std::make_unique<RandomEffectsContainer>();
249 rfx_label_mapper = std::make_unique<LabelMapper>();
250 rfx_container->from_json(obj.at("random_effects").at("random_effect_container_0"));
251 rfx_label_mapper->from_json(obj.at("random_effects").at("random_effect_label_mapper_0"));
252 }
253 // Unpack outcome statistics
254 if (obj.contains("outcome_mean")) y_bar = obj.at("outcome_mean").get<double>();
255 if (obj.contains("outcome_scale")) y_std = obj.at("outcome_scale").get<double>();
256 if (obj.contains("num_samples")) num_samples = obj.at("num_samples").get<int>();
257 }
258
259 // Append another chain's draws onto this one (multi-chain combine). `this` must already be
260 // populated (e.g. via FromJson on the first chain); `other` must have matching model structure
261 // (same forests present, same outcome standardization). Forests are deep-copied sample-by-sample
262 // and parameter traces concatenated, so draw order is preserved (this's draws, then other's).
263 void Merge(const BARTSamples& other) {
264 // Runtime checks for samples objects to be combined
265 if (y_bar != other.y_bar || y_std != other.y_std) {
266 Log::Fatal("Cannot merge BARTSamples with different outcome standardization");
267 }
268 if (rfx_container != nullptr && other.rfx_container != nullptr) {
269 if (rfx_container->NumComponents() != other.rfx_container->NumComponents() ||
270 rfx_container->NumGroups() != other.rfx_container->NumGroups()) {
271 Log::Fatal("Cannot merge BARTSamples with different random effects structure");
272 }
273 if (rfx_label_mapper->Keys() != other.rfx_label_mapper->Keys()) {
274 Log::Fatal("Cannot merge BARTSamples with different random effects label mapping");
275 }
276 if (rfx_label_mapper->Map() != other.rfx_label_mapper->Map()) {
277 Log::Fatal("Cannot merge BARTSamples with different random effects label mapping");
278 }
279 }
280 // Append forests if they exist in the samples object
281 AppendForestContainerSamples(mean_forests, other.mean_forests, "mean");
282 AppendForestContainerSamples(variance_forests, other.variance_forests, "variance");
283 // Append random effects if they exist in the samples object
284 // Note that the LabelMapper is not sample-specific so we do not need to append to it
285 AppendRandomEffectsContainerSamples(rfx_container, other.rfx_container);
286 // Append parameter samples
287 if ((!global_error_variance_samples.empty() && other.global_error_variance_samples.empty()) ||
288 (global_error_variance_samples.empty() && !other.global_error_variance_samples.empty())) {
289 Log::Fatal("Cannot merge BARTSamples objects: global error variance samples present in one chain but not the other");
290 }
291 global_error_variance_samples.insert(global_error_variance_samples.end(),
292 other.global_error_variance_samples.begin(), other.global_error_variance_samples.end());
293 if ((!leaf_scale_samples.empty() && other.leaf_scale_samples.empty()) ||
294 (leaf_scale_samples.empty() && !other.leaf_scale_samples.empty())) {
295 Log::Fatal("Cannot merge BARTSamples objects: leaf scale samples present in one chain but not the other");
296 }
297 leaf_scale_samples.insert(leaf_scale_samples.end(),
298 other.leaf_scale_samples.begin(), other.leaf_scale_samples.end());
299 if ((!cloglog_cutpoint_samples.empty() && other.cloglog_cutpoint_samples.empty()) ||
300 (cloglog_cutpoint_samples.empty() && !other.cloglog_cutpoint_samples.empty())) {
301 Log::Fatal("Cannot merge BARTSamples objects: cloglog cutpoint samples present in one chain but not the other");
302 }
303 cloglog_cutpoint_samples.insert(cloglog_cutpoint_samples.end(),
304 other.cloglog_cutpoint_samples.begin(), other.cloglog_cutpoint_samples.end());
305 num_samples += other.num_samples;
306 }
307
308 std::vector<double> OutcomePredictionsTrain() const {
309 std::vector<double> predictions(num_train * num_samples, 0.0);
310 for (int i = 0; i < num_train * num_samples; ++i) {
311 if (!mean_forest_predictions_train.empty()) {
312 predictions[i] += mean_forest_predictions_train[i];
313 }
314 if (!rfx_predictions_train.empty()) {
315 predictions[i] += rfx_predictions_train[i]; // Add random effects contribution if present
316 }
317 }
318 return predictions;
319 }
320
321 std::vector<double> OutcomePredictionsTest() const {
322 std::vector<double> predictions(num_test * num_samples, 0.0);
323 for (int i = 0; i < num_test * num_samples; ++i) {
324 if (!mean_forest_predictions_test.empty()) {
325 predictions[i] += mean_forest_predictions_test[i];
326 }
327 if (!rfx_predictions_test.empty()) {
328 predictions[i] += rfx_predictions_test[i]; // Add random effects contribution if present
329 }
330 }
331 return predictions;
332 }
333};
334
335} // namespace StochTree
336
337#endif // STOCHTREE_BART_H_
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 bart.h:53
Definition bart.h:22
Definition bart.h:125