-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathtest-bgmCompare.R
More file actions
718 lines (601 loc) · 26 KB
/
Copy pathtest-bgmCompare.R
File metadata and controls
718 lines (601 loc) · 26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
# ==============================================================================
# Tests for bgmCompare() - Multi-group MRF Comparison
# ==============================================================================
#
# EXTENDS: test-tolerance.R (stochastic-robust testing approach)
# PATTERN: Reproducibility, range invariants, dimension consistency
#
# These tests parallel the structure of test-bgm.R for consistency.
# Tests both reproducibility and basic output structure.
#
# INTEGRATION NOTE: Many configurations (Blume-Capel, missing data imputation,
# standardization) are tested via the parameterized fixture approach in
# test-methods.R. See:
# - helper-fixtures.R: Cached fit functions (get_bgmcompare_fit_blumecapel, etc.)
# - test-methods.R: get_bgmcompare_fixtures() loops over all configurations
#
# This file focuses on tests that require special setup or unique assertions.
# ==============================================================================
# Tiers. Structure, ordering, labels, scale and the selection branches stay
# local. Reading back a planted difference at its planted size is parameter
# recovery, so it runs nightly.
skip_unless_slow = function() {
skip_if_not(
identical(Sys.getenv("BGMS_RUN_SLOW_TESTS"), "true"),
message = "Set BGMS_RUN_SLOW_TESTS=true to run the compare recovery certification"
)
}
# ------------------------------------------------------------------------------
# Reproducibility Tests (using fixtures to save one model fit)
# ------------------------------------------------------------------------------
test_that("bgmCompare is reproducible with seed (x, y interface)", {
# Use cached fixture as fit1, run one fresh fit as fit2 with same params
fit1 = get_bgmcompare_fit_xy()
data("Wenchuan", package = "bgms")
x = Wenchuan[1:25, 1:4]
y = Wenchuan[26:50, 1:4]
fit2 = without_support_warning(bgmCompare(x = x, y = y, iter = 50, warmup = 100, chains = 2, seed = 1234, display_progress = "none"))
combine_chains = function(fit) {
pairs = do.call(rbind, fit$raw_samples$pairwise)
mains = do.call(rbind, fit$raw_samples$main)
cbind(mains, pairs)
}
expect_equal(combine_chains(fit1), combine_chains(fit2))
})
test_that("bgmCompare accepts its default update_method", {
data("Wenchuan", package = "bgms")
x = Wenchuan[1:20, 1:3]
y = Wenchuan[21:40, 1:3]
expect_error(
without_support_warning(bgmCompare(
x = x, y = y, iter = 20, warmup = 20, chains = 1, cores = 1,
seed = 1, display_progress = "none", verbose = FALSE
)),
NA
)
})
# ------------------------------------------------------------------------------
# Output Structure Tests (using saved fit)
# ------------------------------------------------------------------------------
test_that("bgmCompare output has expected structure", {
fit = get_bgmcompare_fit()
expect_s3_class(fit, "bgmCompare")
# Should have key components
expect_true("arguments" %in% names(fit))
expect_true("raw_samples" %in% names(fit))
# Raw samples should have required components
expect_true("pairwise" %in% names(fit$raw_samples))
expect_true("main" %in% names(fit$raw_samples))
})
test_that("bgmCompare stores correct number of groups", {
fit = get_bgmcompare_fit()
args = extract_arguments(fit)
expect_true("num_groups" %in% names(args))
expect_true(args$num_groups >= 2)
})
test_that("bgmCompare posterior summaries have expected format", {
fit = get_bgmcompare_fit()
# Should have baseline summaries
expect_true(!is.null(fit$posterior_summary_pairwise_baseline))
expect_true(!is.null(fit$posterior_mean_pairwise_baseline))
})
# ------------------------------------------------------------------------------
# Tolerance/Sanity Tests (Stochastic-robust)
# ------------------------------------------------------------------------------
test_that("bgmCompare outputs are numerically sane", {
fit = get_bgmcompare_fit()
args = extract_arguments(fit)
p = args$num_variables
# Check baseline pairwise
M = fit$posterior_mean_pairwise_baseline
expect_true(is.matrix(M))
expect_equal(dim(M), c(p, p))
# Symmetry check
asym = max(abs(M - t(M)), na.rm = TRUE)
expect_true(asym <= 1e-8, info = sprintf("Asymmetry too large: %g", asym))
# Values should be finite
expect_true(all(is.finite(M)))
# Check group params
group_params = extract_group_params(fit)
expect_true(all(is.finite(group_params$main_effects_groups)))
expect_true(all(is.finite(group_params$pairwise_effects_groups)))
})
# ------------------------------------------------------------------------------
# Fresh Fit Tests
# ------------------------------------------------------------------------------
test_that("bgmCompare without selection produces valid estimates", {
data = generate_grouped_test_data(n_per_group = 20, p = 3, n_groups = 2, seed = 42)
fit = bgmCompare(
x = data$x,
group_indicator = data$group_indicator,
difference_selection = FALSE,
iter = 50,
warmup = 100,
chains = 1,
display_progress = "none"
)
expect_s3_class(fit, "bgmCompare")
# Should have posterior means
expect_true(!is.null(fit$posterior_mean_pairwise_baseline))
expect_true(!is.null(fit$posterior_mean_main_baseline))
# Without difference selection there are no block allocations to summarise;
# extract_sbm() stops on the selection guard before it reaches the prior one.
expect_error(extract_sbm(fit), regexp = "difference_selection = TRUE")
})
test_that("bgmCompare with selection produces valid indicators", {
data = generate_grouped_test_data(n_per_group = 20, p = 3, n_groups = 2, seed = 123)
# A single chain exercises the single-chain path of summarize_mixture_effect
fit = bgmCompare(
x = data$x,
group_indicator = data$group_indicator,
difference_selection = TRUE,
iter = 50,
warmup = 100,
chains = 1,
display_progress = "none"
)
expect_s3_class(fit, "bgmCompare")
# Should have indicator samples
expect_true(!is.null(fit$raw_samples$indicator))
# Indicators should be binary
ind_samples = do.call(rbind, fit$raw_samples$indicator)
expect_true(all(ind_samples %in% c(0, 1)))
})
# ------------------------------------------------------------------------------
# Method Variations Tests
# ------------------------------------------------------------------------------
test_that("bgmCompare works with different update methods", {
data = generate_grouped_test_data(n_per_group = 20, p = 3, n_groups = 2, seed = 99)
methods_to_test = c("adaptive-metropolis")
# Note: Could add "hmc", "nuts" if testing more thoroughly
for(method in methods_to_test) {
fit = tryCatch(
without_support_warning(bgmCompare(
x = data$x,
group_indicator = data$group_indicator,
update_method = method,
iter = 25,
warmup = 50,
chains = 1,
display_progress = "none"
)),
error = function(e) e
)
if(!inherits(fit, "error")) {
expect_s3_class(fit, "bgmCompare")
}
}
})
# ------------------------------------------------------------------------------
# More Than Two Groups
# ------------------------------------------------------------------------------
test_that("bgmCompare handles more than 2 groups", {
data = generate_grouped_test_data(
n_per_group = 15, p = 3, n_groups = 3, seed = 456
)
# Use difference_selection = FALSE to avoid summary computation issues
# with very short chains
fit = bgmCompare(
x = data$x,
group_indicator = data$group_indicator,
difference_selection = FALSE,
iter = 25,
warmup = 50,
chains = 1,
display_progress = "none"
)
expect_s3_class(fit, "bgmCompare")
args = extract_arguments(fit)
expect_equal(args$num_groups, 3)
# Group-specific effects should have 3 columns
group_params = extract_group_params(fit)
expect_equal(ncol(group_params$main_effects_groups), 3)
expect_equal(ncol(group_params$pairwise_effects_groups), 3)
})
# ==============================================================================
# Parameter Ordering Test (p >= 4 required to detect row/column-major bugs)
# ==============================================================================
#
# See test-bgm.R header comment and helper-fixtures.R for background on
# row-major vs column-major ordering bugs.
# ==============================================================================
test_that("bgmCompare output has correct parameter ordering", {
# Ordering is structural: the chain only has to produce the vectors.
skip_on_cran()
data("Wenchuan", package = "bgms")
x = na.omit(Wenchuan[, 1:5]) # p=5 to detect row/column-major bugs
group_ind = rep(1:2, length.out = nrow(x))
fit = bgmCompare(
x = x, group_indicator = group_ind,
difference_selection = TRUE,
iter = 60, warmup = 60, chains = 1,
seed = 42,
display_progress = "none"
)
# Summary mean vector -> matrix lower triangle (same row-major order)
M = fit$posterior_mean_pairwise_baseline
expect_true(
all(abs(fit$posterior_summary_pairwise_baseline$mean - M[lower.tri(M)]) < 1e-10),
info = "bgmCompare pairwise baseline summary means do not match matrix lower triangle"
)
# Extractor column means -> matrix positions (uses named "Vi-Vj" columns)
pw_means = colMeans(extract_pairwise_interactions(fit))
expect_true(
all(check_extractor_matrix_consistency(
pw_means, fit$posterior_mean_pairwise_baseline
)),
info = "bgmCompare extract_pairwise_interactions() names do not match matrix positions"
)
})
# ------------------------------------------------------------------------------
# Stochastic-Block difference prior
# ------------------------------------------------------------------------------
test_that("bgmCompare accepts sbm_prior() and surfaces allocations", {
data("Wenchuan", package = "bgms")
x = na.omit(Wenchuan[, 1:4])
group_ind = rep(1:2, length.out = nrow(x))
fit = bgmCompare(
x = x, group_indicator = group_ind,
difference_prior = sbm_prior(),
difference_selection = TRUE,
iter = 100, warmup = 100, chains = 2,
seed = 1, display_progress = "none"
)
expect_s3_class(fit, "bgmCompare")
# Posterior allocation summaries are populated for SBM.
expect_false(is.null(fit$posterior_mean_allocations))
expect_length(fit$posterior_mean_allocations, ncol(x))
expect_length(fit$posterior_mode_allocations, ncol(x))
# Coclustering matrix is square with the right dim and 1's on the diagonal.
cm = fit$posterior_mean_coclustering_matrix
expect_equal(dim(cm), c(ncol(x), ncol(x)))
expect_equal(unname(diag(cm)), rep(1, ncol(x)))
expect_true(all(cm >= 0 & cm <= 1))
# Per-iteration allocations land in raw_samples with shape iter x p.
expect_equal(
dim(fit$raw_samples$allocations[[1]]),
c(nrow(fit$raw_samples$indicator[[1]]), ncol(x))
)
# The exported extractor is the supported route to all of the above; assert
# it hands back the same four summaries the fields carry.
sbm = extract_sbm(fit)
expect_named(
sbm,
c(
"posterior_num_blocks", "posterior_mean_allocations",
"posterior_mode_allocations", "posterior_mean_coclustering_matrix"
)
)
expect_identical(sbm$posterior_num_blocks, fit$posterior_num_blocks)
expect_identical(sbm$posterior_mean_allocations, fit$posterior_mean_allocations)
expect_identical(sbm$posterior_mode_allocations, fit$posterior_mode_allocations)
expect_identical(
sbm$posterior_mean_coclustering_matrix,
fit$posterior_mean_coclustering_matrix
)
expect_equal(dim(sbm$posterior_mean_coclustering_matrix), c(ncol(x), ncol(x)))
})
test_that("bgmCompare without sbm_prior() does not produce allocation fields", {
data("Wenchuan", package = "bgms")
x = na.omit(Wenchuan[, 1:4])
group_ind = rep(1:2, length.out = nrow(x))
fit = bgmCompare(
x = x, group_indicator = group_ind,
difference_prior = beta_bernoulli_prior(alpha = 1, beta = 1),
difference_selection = TRUE,
iter = 100, warmup = 100, chains = 1,
seed = 1, display_progress = "none"
)
expect_null(fit$posterior_mean_allocations)
expect_null(fit$posterior_mode_allocations)
expect_null(fit$posterior_mean_coclustering_matrix)
expect_null(fit$raw_samples$allocations)
# extract_sbm() names the prior it needs rather than returning four NULLs.
expect_error(extract_sbm(fit), regexp = "Stochastic-Block")
})
# ------------------------------------------------------------------------------
# Pairwise parameterization
# ------------------------------------------------------------------------------
test_that("bgmCompare pairwise effects are on the association scale", {
# simulate_mrf() is the package's reference for the association scale: a
# rest score carries 2 * omega * x. Both groups are drawn from the same
# omega, so each group's estimate must recover omega, not 2 * omega.
p = 3
omega = matrix(0, p, p)
omega[upper.tri(omega)] = c(0.5, 0.0, 0.45)
omega = omega + t(omega)
main = matrix(c(0, -0.5), nrow = p, ncol = 2, byrow = TRUE)
draw = function(seed) {
simulate_mrf(
400, p,
num_categories = 2, pairwise = omega, main = main,
variable_type = "ordinal", iter = 50, seed = seed
)
}
fit = bgmCompare(
rbind(draw(11), draw(12)),
group_indicator = rep(1:2, each = 400),
iter = 600, warmup = 300, chains = 1, seed = 1234,
difference_selection = FALSE, display_progress = "none"
)
groups = extract_group_params(fit)$pairwise_effects_groups
target = omega[t(utils::combn(p, 2))]
# The scale is read as the slope of the estimate on the target, which is
# bounded from BOTH sides: a doubled parameterization gives slope 2 and a
# halved one slope 0.5, and neither is inside the band. An rmse bound
# against 2 * target alone tests only the doubling direction, and at this
# data size it cannot see the halving one at all -- target and 0.5 * target
# are 0.194 apart in rmse while the run's own error reaches 0.19.
#
# The band comes from the estimator's spread, not from taste: over 12
# alternative data/fit seeds at this construction the slope stayed within
# [0.851, 1.369] on both groups, so [0.6, 1.6] holds it with room while
# still refusing 0.5 and 2.
#
# Both groups are drawn from the same omega, so both must recover it. Only
# group 1 was ever checked, which left a difference parameterization that
# mis-signs or mis-scales the second group's reconstruction unexamined.
slope = function(estimate) sum(estimate * target) / sum(target^2)
rmse = function(estimate, x) sqrt(mean((estimate - x)^2))
for(g in 1:2) {
estimate = groups[, g]
ctx = paste("group", g)
expect_lt(rmse(estimate, target), 0.2)
expect_gt(slope(estimate), 0.6)
expect_lt(slope(estimate), 1.6)
# And the correct scale has to fit better than the doubled one, which is
# the direction this data size does resolve.
expect_lt(rmse(estimate, target), 0.8 * rmse(estimate, 2 * target),
label = ctx
)
}
})
test_that("bgmCompare recovers a planted group difference at its planted size", {
skip_unless_slow()
skip_on_cran()
# The guard above fits two groups drawn from the SAME omega, so every
# difference in it is zero and a difference parameterization off by a factor
# would still pass. Nothing else in the every-run tier looks at the size of
# a nonzero difference. This plants one and reads it back.
#
# The two group matrices swap their two nonzero pairs, so the planted
# difference is large (+-0.5) while both groups stay in a well-identified
# coupling range -- planting a large difference by inflating one group
# instead pushes that group toward deterministic data, where the posterior
# is wide and the read-back is noisier than the difference being measured.
p = 3
pairs = t(utils::combn(p, 2))
symmetric = function(values) {
m = matrix(0, p, p)
m[upper.tri(m)] = values
m + t(m)
}
omega_1 = symmetric(c(0.60, 0.00, 0.10))
omega_2 = symmetric(c(0.10, 0.00, 0.60))
main = matrix(c(0, -0.5), nrow = p, ncol = 2, byrow = TRUE)
draw = function(omega, seed) {
simulate_mrf(
1200, p,
num_categories = 2, pairwise = omega, main = main,
variable_type = "ordinal", iter = 50, seed = seed
)
}
fit = bgmCompare(
rbind(draw(omega_1, 101), draw(omega_2, 201)),
group_indicator = rep(1:2, each = 1200),
iter = 400, warmup = 250, chains = 1, seed = 1,
difference_selection = FALSE, display_progress = "none"
)
groups = extract_group_params(fit)$pairwise_effects_groups
recovered = groups[, 2] - groups[, 1]
planted = omega_2[pairs] - omega_1[pairs]
# Tolerances from the estimator's own spread: over 12 alternative
# data/fit seeds at this construction the rmse of the recovered difference
# reached 0.089 and no single pair was off by more than 0.125, while the
# slope stayed within [0.813, 1.167]. The bounds below sit above those and
# below what a mis-scaled parameterization produces: at half or double the
# difference the slope is 0.5 or 2, and the rmse against the planted
# difference is 0.204 or 0.408.
expect_lt(sqrt(mean((recovered - planted)^2)), 0.15)
expect_lt(max(abs(recovered - planted)), 0.20)
slope = sum(recovered * planted) / sum(planted^2)
expect_gt(slope, 0.7)
expect_lt(slope, 1.4)
# The signs are the qualitative half of the same claim: the pair the second
# group loses and the pair it gains must come back with opposite signs, and
# the pair with no planted difference must not acquire one.
expect_lt(recovered[1], 0)
expect_gt(recovered[3], 0)
expect_lt(abs(recovered[2]), 0.20)
})
# Weekly certification (T2): this is the only test in the suite that runs
# bgmCompare() at its shipped defaults on a full shipped dataset, and it costs
# ~14 min on the 2-core CI runner -- by itself a quarter of the nightly budget.
# A full-defaults end-to-end fit is a product-surface check, which the tier
# contract puts in T1, but at that size it is not a smoke. The cheap end of the
# same surface stays local: the label-propagation test below fits the same data
# at iter = 50.
test_that("the shipped data's own language column works as the group indicator", {
skip_on_cran()
skip_unless_certification()
data("Boredom", package = "bgms")
fit = bgmCompare(Boredom[, -1], group_indicator = Boredom$language)
expect_s3_class(fit, "bgmCompare")
expect_equal(
tabulate(extract_arguments(fit)$group),
tabulate(match(Boredom$language, unique(Boredom$language)))
)
})
test_that("predict.bgmCompare centers Blume-Capel variables at the fit's baseline", {
fit = get_bgmcompare_fit_blumecapel()
arguments = extract_arguments(fit)
# The fit's own baseline (category 3, shifted to the 0-based scale) must be
# stored; without it predict() silently centered every Blume-Capel term at 0.
expect_equal(arguments$baseline_category, rep(2L, 4L))
data("Boredom", package = "bgms")
newdata = Boredom[c(1:4, 494:497), 2:5]
probs = predict(fit, newdata = newdata, group = 1)
# Manual reference, the sampler's own convention: category c contributes
# exp(lin*(c-ref) + quad*(c-ref)^2 + (c-ref)*rest), with the rest score
# summing 2 * (x_v - ref_v) * pairwise[v, j] over the other variables.
shift = arguments$blume_capel_shift
ref = arguments$baseline_category
x0 = sweep(data.matrix(newdata), 2, shift)
gp = extract_group_params(fit)
p = arguments$num_variables
pw = matrix(0, p, p)
pw[lower.tri(pw)] = gp$pairwise_effects_groups[, 1]
pw = pw + t(pw)
main = matrix(gp$main_effects_groups[, 1], ncol = 2, byrow = TRUE)
for(j in seq_len(p)) {
rest = as.numeric((sweep(x0[, -j, drop = FALSE], 2, ref[-j])) %*% (2 * pw[-j, j]))
cats = 0:arguments$num_categories[j] - ref[j]
expected = t(vapply(rest, function(r) {
e = exp(main[j, 1] * cats + main[j, 2] * cats^2 + cats * r)
e / sum(e)
}, numeric(length(cats))))
expect_equal(unname(probs[[j]]), unname(expected), tolerance = 1e-10)
}
})
# ---- group labels on human displays (F-072) ---------------------------------
# Groups are numbered by first appearance in the indicator and every extractor
# keys on that number; the original labels ride along so the displays a person
# reads can name the group as well as number it.
test_that("a compare fit stores the group indicator's own labels", {
data("Boredom", package = "bgms")
# The shipped data is fr-first, so first-appearance numbering makes fr group 1
# even though "en" sorts first -- exactly the confusion the labels remove.
expect_identical(Boredom$language[1], "fr")
fit = bgmCompare(
x = Boredom[, 2:5], group_indicator = Boredom$language,
iter = 50, warmup = 100, chains = 2, seed = 8, display_progress = "none"
)
arguments = extract_arguments(fit)
expect_identical(arguments$group_labels, c("fr", "en"))
expect_equal(tabulate(arguments$group), c(490L, 496L))
# print() and summary() name the groups; the numbers stay the key.
expect_output(print(fit), "groups: 1 = fr \\(n = 490\\), 2 = en \\(n = 496\\)")
expect_output(
print(summary(fit)),
"groups: 1 = fr \\(n = 490\\), 2 = en \\(n = 496\\)"
)
# Centrality labels carry them too.
expect_match(
attr(extract_centrality(fit, group = 2), "label"), "group 2 (en)",
fixed = TRUE
)
expect_match(
attr(extract_centrality(fit, group = c(1, 2)), "label"),
"(group 1 (fr) - group 2 (en))",
fixed = TRUE
)
# The extractor contract is numeric and must not move: easybgm and JASP read
# these column names.
expect_identical(
colnames(extract_group_params(fit)$pairwise_effects_groups),
c("group1", "group2")
)
})
test_that("the x/y path labels the groups x and y", {
fit = get_bgmcompare_fit_xy()
expect_identical(extract_arguments(fit)$group_labels, c("x", "y"))
# The counts are the fit's own, after listwise deletion -- which is the point
# of reporting them next to the labels.
n = tabulate(extract_arguments(fit)$group)
expect_output(
print(fit),
sprintf("groups: 1 = x \\(n = %d\\), 2 = y \\(n = %d\\)", n[1], n[2])
)
expect_match(
attr(extract_centrality(fit, group = 1), "label"), "group 1 (x)",
fixed = TRUE
)
})
test_that("a fit without the stored labels degrades to bare group numbers", {
# Fits made before the field existed have no group_labels; every display path
# goes through these two helpers, so this is the whole degradation contract.
legacy = extract_arguments(get_bgmcompare_fit())
legacy$group_labels = NULL
expect_null(compare_group_labels(legacy))
expect_null(group_mapping_line(legacy))
expect_identical(group_tag(NULL, 2L), "group 2")
expect_identical(group_tag(c("fr", "en"), 2L), "group 2 (en)")
# A label vector that cannot name every group is refused wholesale rather
# than half-applied.
expect_null(compare_group_labels(list(group_labels = "only-one"), 2L))
expect_null(compare_group_labels(list(group_labels = c("a", NA))))
})
# ------------------------------------------------------------------------------
# Difference summary layout
# ------------------------------------------------------------------------------
test_that("difference summary rows are contrast-major in both selection branches", {
skip_on_cran()
# Three groups, so contrast-major and variable-major orders differ. With two
# groups they coincide, which is why this went unnoticed.
set.seed(41)
n_groups = 3
p = 3
x = matrix(sample(0:1, n_groups * 40 * p, replace = TRUE), ncol = p)
colnames(x) = paste0("V", seq_len(p))
group_indicator = rep(seq_len(n_groups), each = 40)
fit_of = function(difference_selection) {
without_support_warning(bgmCompare(
x = x, group_indicator = group_indicator,
difference_selection = difference_selection,
iter = 50, warmup = 100, chains = 2, cores = 2, seed = 41,
display_progress = "none"
))
}
sel = fit_of(TRUE)
nosel = fit_of(FALSE)
# The selection branch used to emit rows variable-major while labelling them
# contrast-major, so the two branches disagreed on row order and the
# selection branch no longer lined up positionally with names_all or with
# posterior_mean_*_differences.
names_all = get_fit_cache(sel)$names_all
for(field in c("main_differences", "pairwise_differences")) {
key = if(field == "main_differences") "main_diff" else "pairwise_diff"
sel_tbl = sel[[paste0("posterior_summary_", field)]]
nosel_tbl = nosel[[paste0("posterior_summary_", field)]]
expect_equal(sel_tbl$parameter, names_all[[key]], info = field)
expect_equal(nosel_tbl$parameter, names_all[[key]], info = field)
expect_equal(rownames(sel_tbl), names_all[[key]], info = field)
expect_equal(rownames(nosel_tbl), names_all[[key]], info = field)
}
# Each row's statistics belong to the parameter its label names: the summary
# means must reproduce posterior_mean_*_differences read in the same order.
pw_means = unlist(lapply(
sel$posterior_mean_pairwise_differences,
function(m) m[lower.tri(m)]
))
expect_equal(
unname(pw_means),
sel$posterior_summary_pairwise_differences$mean,
tolerance = 1e-10
)
main_means = unlist(lapply(
sel$posterior_mean_main_differences,
function(m) as.vector(t(m))
))
expect_equal(
unname(main_means),
sel$posterior_summary_main_differences$mean,
tolerance = 1e-10
)
})
test_that("compare summary tables carry their parameter names as row names", {
skip_on_cran()
fit = get_bgmcompare_fit()
names_all = get_fit_cache(fit)$names_all
# Stored straight from summarize_fit_compare(), these tables kept their labels
# in a `parameter` column with default "1", "2", "3" row names -- which is
# what extract_rhat() and extract_ess() then handed back as names.
expect_equal(rownames(fit$posterior_summary_main_baseline), names_all$main_baseline)
expect_equal(rownames(fit$posterior_summary_pairwise_baseline), names_all$pairwise_baseline)
expect_equal(rownames(fit$posterior_summary_main_differences), names_all$main_diff)
expect_equal(rownames(fit$posterior_summary_pairwise_differences), names_all$pairwise_diff)
expect_equal(rownames(fit$posterior_summary_indicator), names_all$indicators)
# The `parameter` column stays: print.summary.bgmCompare() reads it by name
# and rounds df[, -1] positionally.
expect_true("parameter" %in% names(fit$posterior_summary_main_differences))
})