knitr::opts_chunk$set( echo = TRUE, collapse = TRUE, comment = "#>", dev = "png", dpi = 144, fig.width = 7, fig.height = 5, warning = FALSE, message = FALSE ) library(beezdemand) library(dplyr) data(apt)
The beezdemand package provides multiple approaches for fitting behavioral
economic demand curves. This guide helps you choose the right modeling approach
for your data and research questions.
Demand analysis is appropriate when you want to: - Quantify the value of a reinforcer (e.g., drugs, food, activities) - Compare demand elasticity across conditions or groups - Estimate key parameters like intensity (Q0), elasticity (alpha), and breakpoint - Compute derived metrics like Pmax (price at maximum expenditure) and Omax (maximum expenditure)
| Your Situation | Recommended Approach | Function |
|----------------|---------------------|----------|
| Individual curves, quick exploration | Fixed-effects NLS | fit_demand_fixed() |
| Group comparisons, repeated measures | Mixed-effects | fit_demand_mixed() |
| Many zeros, two-part modeling | Hurdle model | fit_demand_hurdle() |
| Cross-commodity substitution | Cross-price models | fit_cp_*() |
Before fitting any model, always check your data for systematic responding.
# Check for systematic demand systematic_check <- check_systematic_demand(apt) head(systematic_check$results) # Filter to systematic data only (those that pass all criteria) systematic_ids <- systematic_check$results |> filter(systematic) |> dplyr::pull(id) apt_clean <- apt |> filter(as.character(id) %in% systematic_ids) cat("Systematic participants:", n_distinct(apt_clean$id), "of", n_distinct(apt$id), "\n")
The check_systematic_demand() function applies criteria from Stein et al. (2015)
to identify nonsystematic responding patterns including:
Use fit_demand_fixed() when you want:
# Fit individual demand curves using the Hursh & Silberberg equation fit_fixed <- fit_demand_fixed( data = apt, equation = "hs", k = 2 ) # Print summary print(fit_fixed) # Get tidy coefficient table tidy(fit_fixed) |> head() # Get model-level statistics glance(fit_fixed)
# Plot individual curves plot(fit_fixed, type = "individual", ids = c("19", "51"))
# Basic diagnostics check_demand_model(fit_fixed)
Use fit_demand_mixed() when you want:
For the mixed-effects approach with the zben equation form, transform your
consumption data using the LL4 (log-log with 4-parameter adjustment):
# Transform consumption using LL4 apt_ll4 <- apt |> dplyr::mutate(y_ll4 = ll4(y)) # View the transformation apt_ll4 |> dplyr::filter(id == 19) |> dplyr::select(id, x, y, y_ll4)
# Fit mixed-effects model fit_mixed <- fit_demand_mixed( data = apt_ll4, y_var = "y_ll4", x_var = "x", id_var = "id", equation_form = "zben" ) # Print summary print(fit_mixed) summary(fit_mixed) # Basic diagnostics check_demand_model(fit_mixed) plot_residuals(fit_mixed)$fitted
For experimental designs with factors, you can use emmeans for post-hoc comparisons:
# For a model with factors (example with ko dataset): data(ko) # Prepare data with LL4 transformation # (Note: the ko dataset already includes a y_ll4 column, but we # recreate it here to demonstrate the transformation workflow) ko_ll4 <- ko |> dplyr::mutate(y_ll4 = ll4(y)) fit <- fit_demand_mixed( data = ko_ll4, y_var = "y_ll4", x_var = "x", id_var = "monkey", factors = c("drug", "dose"), equation_form = "zben" ) # Get estimated marginal means for Q0 and alpha across drug levels emms <- get_demand_param_emms(fit, factors_in_emm = "drug", include_ev = TRUE) # Pairwise comparisons of drug conditions comps <- get_demand_comparisons(fit, compare_specs = ~drug, contrast_type = "pairwise")
Use fit_demand_hurdle() when you have:
The hurdle model separates:
# Fit hurdle model with 3 random effects fit_hurdle <- fit_demand_hurdle( data = apt, y_var = "y", x_var = "x", id_var = "id", random_effects = c("zeros", "q0", "alpha") ) # Summary summary(fit_hurdle) # Population demand curve plot(fit_hurdle, type = "demand") # Probability of zero consumption plot(fit_hurdle, type = "probability") # Basic diagnostics check_demand_model(fit_hurdle) plot_residuals(fit_hurdle)$fitted plot_qq(fit_hurdle)
Compare nested models using likelihood ratio tests:
# Fit full model (3 random effects) fit_hurdle3 <- fit_demand_hurdle( data = apt, y_var = "y", x_var = "x", id_var = "id", random_effects = c("zeros", "q0", "alpha") ) # Fit simplified model (2 random effects) fit_hurdle2 <- fit_demand_hurdle( data = apt, y_var = "y", x_var = "x", id_var = "id", random_effects = c("zeros", "q0") # No alpha random effect ) # Compare models compare_hurdle_models(fit_hurdle3, fit_hurdle2) # Unified model comparison (AIC/BIC + LRT when appropriate) compare_models(fit_hurdle3, fit_hurdle2)
The equation argument determines the functional form of the demand curve.
Each equation has trade-offs in terms of flexibility, zero handling, and
comparability across studies.
| Equation | Function | Handles Zeros | k Required | Best For |
|----------|----------|:-------------:|:----------:|----------|
| "hs" | Hursh & Silberberg (2008) | No | Yes | Traditional analyses, compatibility with older literature |
| "koff" | Koffarnus et al. (2015) | No | Yes | Modified exponential, widely used in applied research |
| "simplified" | Rzeszutek et al. (2025) | Yes | No | Modern analyses; avoids k-dependency and zero issues |
Recommendations:
"simplified" (also called SND) as it
handles zeros natively and does not require specifying k, making results
more comparable across studies."hs" or
"koff" with the same k specification as the original study."hs" or "koff", zeros in consumption data are incompatible
with the log transformation and will be dropped with a warning.The scaling constant k controls the asymptotic range of the demand curve:
k = 2: Good default for most purchase task datak = "ind": Calculate k individually for each participantk = "fit": Estimate k as a free parameterk = "share": Fit a single k shared across all participants# Different k specifications fit_k2 <- fit_demand_fixed(apt, k = 2) # Fixed at 2 fit_kind <- fit_demand_fixed(apt, k = "ind") # Individual fit_kfit <- fit_demand_fixed(apt, k = "fit") # Free parameter fit_kshare <- fit_demand_fixed(apt, k = "share") # Shared across participants
| Parameter | Interpretation | Typical Range | |-----------|---------------|---------------| | Q0 (Intensity) | Consumption at zero price | Dataset-dependent | | alpha (Elasticity) | Rate of consumption decline | 0.0001 - 0.1 | | Pmax | Price at maximum expenditure | Dataset-dependent | | Omax | Maximum expenditure | Dataset-dependent | | Breakpoint | First price with zero consumption | Dataset-dependent |
Problem: Model fails to converge or produces unreasonable estimates.
Solutions:
check_systematic_demand()Problem: Many zeros in consumption data.
Solutions:
fit_demand_hurdle() which explicitly models zerosProblem: Comparing alpha values between different model types.
Solution: Be aware of parameterization differences:
tidy(fit, report_space = "log10") for comparable outputlog10(alpha) = log(alpha) / log(10)| Approach | Best For | Key Features | Handles Zeros |
|----------|----------|--------------|---------------|
| fit_demand_fixed() | Individual curves, quick analysis | Simple, per-subject estimates | Excludes |
| fit_demand_mixed() | Group comparisons, repeated measures | Random effects, emmeans integration | LL4 transform |
| fit_demand_hurdle() | Data with many zeros | Two-part model, TMB backend | Explicitly models |
vignette("beezdemand") for basic usagevignette("fixed-demand") for individual demand curve fittingvignette("group-comparisons") for extra sum-of-squares F-testvignette("mixed-demand") for mixed-effects examplesvignette("mixed-demand-advanced") for factors, EMMs, covariatesvignette("hurdle-demand-models") for hurdle model detailsvignette("cross-price-models") for substitution analysesvignette("migration-guide") for migrating from FitCurves()Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.