A common discounting question is
not “what is k?” but “does k differ between groups?” – treatment
vs. control, clinical vs. community, commodity A vs. commodity B. Both
modeling tiers in beezdiscounting answer it the same way: the discount
rate is estimated on the natural-log scale, so a between-subject design
enters as a linear model on log k (log k = X beta), and
every marginal mean or contrast is a linear combination of coefficients.
get_dd_param_emms() and get_dd_comparisons()
work on top of that shared backbone for:
fit_dd_tmb() – mixed-effects indifference-point models
(Wald inference),fit_dd_choice() – the structural trial-level choice
model (Wald inference),fit_dd_brms() and fit_dd_choice_brms() –
their Bayesian counterparts (posterior draws; see the last
section).Both the TMB and Bayesian examples below are precomputed (Stan sampling takes minutes), so the outputs shown come from real fits; copy the chunks into your session to reproduce them.
simulate_dd_ip() generates indifference points with a
known between-condition shift in log k. Here condition C2’s
k is three times C1’s (delta_k = log(3)). To
have a second factor for the grouped contrasts shown later, we also
assign each subject a (null) site.
library(beezdiscounting)
sim <- simulate_dd_ip(
n_subjects = 24, n_conditions = 2, delta_k = c(0, log(3)), seed = 42
)
# conditions alternate across subjects, so assign site in blocks of two to
# get the full condition x site crossing
subjects <- unique(sim$id)
sim$site <- factor(ifelse(
((match(sim$id, subjects) - 1) %/% 2) %% 2 == 0, "siteA", "siteB"
))
with(unique(sim[, c("id", "condition", "site")]), table(condition, site))
#> site
#> condition siteA siteB
#> C1 6 6
#> C2 6 6
head(sim)
#> # A tibble: 6 × 5
#> id condition x y site
#> <fct> <fct> <dbl> <dbl> <fct>
#> 1 1 C1 7 0.990 siteA
#> 2 1 C1 30 0.648 siteA
#> 3 1 C1 180 0.127 siteA
#> 4 1 C1 365 0.0523 siteA
#> 5 1 C1 730 0.0194 siteA
#> 6 1 C1 1460 0.0429 siteAfactors puts the design on log k; every other parameter
stays population-level. (multi_start = FALSE keeps the
vignette quick; leave the default on for real analyses.)
fit <- fit_dd_tmb(
sim,
factors = c("condition", "site"),
multi_start = FALSE, verbose = 0
)
tidy(fit)
#> # A tibble: 5 × 8
#> term estimate std.error statistic p.value component estimate_scale
#> <chr> <dbl> <dbl> <dbl> <dbl> <chr> <chr>
#> 1 k:(Intercept) 0.00923 0.00253 -17.1 1.09e-65 fixed natural
#> 2 k:conditionC2 2.97 0.813 3.98 6.90e- 5 fixed natural
#> 3 k:sitesiteB 1.47 0.403 1.41 1.57e- 1 fixed natural
#> 4 sigma_u (log1… 0.312 NA NA NA variance log10
#> 5 phi (precisio… 10.5 NA NA NA variance natural
#> # ℹ 1 more variable: term_display <chr>get_dd_param_emms() averages the fitted design over the
factor levels you are not looking at (equal weights, the emmeans
convention) and back-transforms: k = exp(k_log), with the
standard error and the Wald interval formed on the log scale.
get_dd_param_emms(fit)
#> # A tibble: 4 × 6
#> level k k_log std.error conf.low conf.high
#> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 condition=C1, site=siteA 0.00923 -4.68 0.274 0.00540 0.0158
#> 2 condition=C1, site=siteB 0.0136 -4.30 0.276 0.00791 0.0234
#> 3 condition=C2, site=siteA 0.0274 -3.60 0.277 0.0159 0.0472
#> 4 condition=C2, site=siteB 0.0404 -3.21 0.280 0.0233 0.0700Marginalize over site to get per-condition means, or
condition on a level with at:
get_dd_param_emms(fit, factors_in_emm = "condition")
#> # A tibble: 2 × 6
#> level k k_log std.error conf.low conf.high
#> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 condition=C1 0.0112 -4.49 0.224 0.00722 0.0174
#> 2 condition=C2 0.0333 -3.40 0.229 0.0213 0.0521
get_dd_param_emms(fit, at = list(site = "siteA"))
#> # A tibble: 2 × 6
#> level k k_log std.error conf.low conf.high
#> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 condition=C1, site=siteA 0.00923 -4.68 0.274 0.00540 0.0158
#> 2 condition=C2, site=siteA 0.0274 -3.60 0.277 0.0159 0.0472get_dd_comparisons() turns the same reference grid into
factor-level contrasts. Estimates are reported on the log10 scale (the
customary discounting metric) and, because differences in log k are log
ratios, also as multiplicative ratios: a ratio of 3 means one group
discounts three times as steeply. P-values are adjusted with
stats::p.adjust() ("holm" by default).
cmp <- get_dd_comparisons(fit, compare_specs = ~condition)
cmp$k$contrasts_log10
#> # A tibble: 1 × 8
#> contrast estimate std.error statistic df conf.low conf.high p.value
#> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 condition=C1 - … -0.473 0.139 -3.41 Inf -0.745 -0.201 6.46e-4
cmp$k$contrasts_ratio
#> # A tibble: 1 × 5
#> contrast ratio conf.low conf.high p.value
#> <chr> <dbl> <dbl> <dbl> <dbl>
#> 1 condition=C1 - condition=C2 0.336 0.180 0.629 0.000646The simulated truth was k(C2) = 3 k(C1), so the
displayed C1 - C2 ratio should land near 1/3 – and it does.
With more than one factor, contrast_by conditions the
contrasts on another factor’s levels (adjustment is applied per by-cell;
with no interaction in the model the per-site contrasts coincide, as
here):
cmp_by <- get_dd_comparisons(
fit,
compare_specs = ~ condition + site, contrast_by = "site"
)
cmp_by$k$contrasts_log10
#> # A tibble: 2 × 9
#> site contrast estimate std.error statistic df conf.low conf.high p.value
#> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 siteA condition… -0.473 0.139 -3.41 Inf -0.745 -0.201 6.46e-4
#> 2 siteB condition… -0.473 0.139 -3.41 Inf -0.745 -0.201 6.46e-4tidy() flattens any comparison object into one
cross-backend frame:
A forest plot makes these contrasts easier to scan than the table.
Each row is a k ratio with its confidence interval; the
dashed line marks a ratio of 1 (no difference), and contrasts whose
interval excludes 1 are highlighted. Across the condition-by-site grid,
the condition contrasts (a threefold effect by construction) are
flagged, whereas the site contrasts (no true effect) are not.
plot of chunk forest
The structural choice model estimates the same log-k design from binary smaller-sooner vs. larger-later choices, so the comparison surface is identical. (The simulator has no built-in group effect; stack two simulated groups.)
ctrl <- simulate_dd_choice(n_subjects = 12, log_k_pop = log(0.01), seed = 7)
trt <- simulate_dd_choice(n_subjects = 12, log_k_pop = log(0.04), seed = 8)
trt$id <- paste0("t", trt$id)
ctrl$group <- "ctrl"
trt$group <- "treat"
choice_data <- rbind(ctrl, trt)
fit_choice <- fit_dd_choice(
choice_data,
mode = "structural", factors = "group", verbose = 0
)
get_dd_param_emms(fit_choice)
#> # A tibble: 2 × 6
#> level k k_log std.error conf.low conf.high
#> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 group=ctrl 0.0113 -4.48 0.234 0.00716 0.0179
#> 2 group=treat 0.0369 -3.30 0.212 0.0243 0.0559
get_dd_comparisons(fit_choice)$k$contrasts_ratio
#> # A tibble: 1 × 5
#> contrast ratio conf.low conf.high p.value
#> <chr> <dbl> <dbl> <dbl> <dbl>
#> 1 group=ctrl - group=treat 0.307 0.167 0.566 0.000152fit_dd_brms() and fit_dd_choice_brms() take
the same
factors/factor_interaction/continuous_covariates
arguments, and
get_dd_param_emms()/get_dd_comparisons()
accept the fits directly:
fit_b <- fit_dd_brms(
sim,
equation = "mazur", factors = c("condition", "site"),
chains = 4, cores = 4, seed = 1
)
get_dd_param_emms(fit_b, factors_in_emm = "condition")
#> # A tibble: 2 × 6
#> level k k_log std.error conf.low conf.high
#> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 condition=C1 0.0114 -4.48 0.232 0.00729 0.0180
#> 2 condition=C2 0.0288 -3.55 0.227 0.0183 0.0450
get_dd_comparisons(fit_b, compare_specs = ~condition)
#> $k
#> $k$emmeans
#> # A tibble: 2 × 6
#> level k k_log std.error conf.low conf.high
#> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 condition=C1 0.0114 -4.48 0.232 0.00729 0.0180
#> 2 condition=C2 0.0288 -3.55 0.227 0.0183 0.0450
#>
#> $k$contrasts_log10
#> # A tibble: 1 × 9
#> contrast estimate std.error statistic df conf.low conf.high p.value
#> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 condition=C1 - … -0.409 0.136 NA NA -0.668 -0.127 NA
#> # ℹ 1 more variable: post.prob <dbl>
#>
#> $k$contrasts_ratio
#> # A tibble: 1 × 6
#> contrast ratio conf.low conf.high p.value post.prob
#> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 condition=C1 - condition=C2 0.390 0.215 0.746 NA 0.996
#>
#>
#> attr(,"class")
#> [1] "beezdiscounting_comparison"
#> attr(,"backend")
#> [1] "brms"
#> attr(,"adjustment_method")
#> [1] "none (posterior summaries; see post.prob)"
#> attr(,"compare_specs_used")
#> [1] "~condition"
#> attr(,"contrast_type_used")
#> [1] "pairwise"
#> attr(,"contrast_by_used")
#> [1] "NULL"
fit_cb <- fit_dd_choice_brms(
choice_data,
equation = "mazur", factors = "group",
chains = 4, cores = 4, seed = 1
)
get_dd_comparisons(fit_cb)
#> $k
#> $k$emmeans
#> # A tibble: 2 × 6
#> level k k_log std.error conf.low conf.high
#> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 group=ctrl 0.0120 -4.43 0.248 0.00742 0.0194
#> 2 group=treat 0.0343 -3.37 0.235 0.0212 0.0531
#>
#> $k$contrasts_log10
#> # A tibble: 1 × 9
#> contrast estimate std.error statistic df conf.low conf.high p.value
#> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 group=ctrl - gr… -0.451 0.142 NA NA -0.726 -0.168 NA
#> # ℹ 1 more variable: post.prob <dbl>
#>
#> $k$contrasts_ratio
#> # A tibble: 1 × 6
#> contrast ratio conf.low conf.high p.value post.prob
#> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 group=ctrl - group=treat 0.354 0.188 0.679 NA 0.999
#>
#>
#> attr(,"class")
#> [1] "beezdiscounting_comparison"
#> attr(,"backend")
#> [1] "brms"
#> attr(,"adjustment_method")
#> [1] "none (posterior summaries; see post.prob)"
#> attr(,"compare_specs_used")
#> [1] "all fitted factors"
#> attr(,"contrast_type_used")
#> [1] "pairwise"
#> attr(,"contrast_by_used")
#> [1] "NULL"The tables have the same shape, with draws-based summaries in place of Wald machinery:
statistic, df, and p.value
are NA. In their place post.prob reports the
posterior probability of direction: the probability that the contrast is
positive (or negative, whichever is larger). A post.prob
near 1 plays the practical role of a small p-value.adjust produces a warning
rather than an adjusted column.Use the TMB fitters for fast iteration and conventional Wald tests
with familiar adjusted p-values. Reach for the brms fitters when you
want full posterior uncertainty for derived quantities (the EMM and
ratio intervals above need no asymptotics), prior information about
plausible discounting rates, or small-sample inference you can defend
without appealing to asymptotic normality. The vignette
vignette("bayesian-discounting") covers the Bayesian tier
itself – priors, families, convergence – in more depth.