---
title: "How to Use Cross-Price Demand Model Functions"
author: "Brent Kaplan"
date: "`r Sys.Date()`"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{How to Use Cross-Price Demand Model Functions}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(
    message = FALSE,
    warning = FALSE,
    include = TRUE,
    collapse = TRUE,
    comment = "#>",
    dev = "png",
    dpi = 144,
    fig.width = 7,
    fig.height = 5
)

library(beezdemand)
library(dplyr)
library(purrr)
library(tidyr)
library(ggplot2)

# Load package datasets
data("etm", package = "beezdemand")
data("lowNicClean", package = "beezdemand")
data("cannabisCigarettes", package = "beezdemand")
data("ongoingETM", package = "beezdemand")

lnic <- lowNicClean |>
    mutate(
        target = if_else(commodity == "cigarettesAdj", "adjusting", "fixed"),
        group = "cigarettes"
    ) |>
    select(
        -commodity
    ) |>
    relocate(condition, .after = group)

can_cig <- cannabisCigarettes |>
    select(
        id,
        x,
        y,
        target = commodity
    )

ongoing_etm <- ongoingETM |>
    pivot_longer(
        -c(id, x, flavor),
        names_to = "target",
        values_to = "y",
        values_drop_na = TRUE
    ) |>
    select(id, x, y, target)
```

## Introduction

Cross-price demand analysis examines how consumption of one commodity changes
as the price of another commodity varies. This is central to understanding
economic relationships between goods:

- **Substitutes**: When the price of the target commodity increases and
  consumption of the alternative increases, the goods function as substitutes
  (e.g., e-cigarettes and combustible cigarettes).
- **Complements**: When the price of the target increases and consumption of the
  alternative *decreases*, the goods function as complements.
- **Independent**: When the price of one commodity does not meaningfully affect
  consumption of the other.

These relationships are quantified through cross-price elasticity parameters.
The `beezdemand` package provides nonlinear (`fit_cp_nls()`), linear
(`fit_cp_linear()`), and mixed-effects linear (`fit_cp_linear(type = "mixed")`)
approaches for cross-price modeling.

## Data Structure

```{r}
glimpse(etm)
```

Typical columns:
- `id`: participant identifier

- `x`: alternative product price

- `y`: consumption level

- `target`: condition type (e.g., "alt")

- `group`: product category

These are the default canonical column names. If your data uses different names,
pass `x_var`, `y_var`, `id_var`, `group_var`, or `target_var` arguments to the
fitting functions (e.g., `fit_cp_nls(data, x_var = "price", y_var = "qty")`).

## Complete Cross-Price Analysis Workflow

This section demonstrates a complete analysis workflow using data that contains multiple
experimental conditions: target consumption when the alternative is absent (`alone`),
target consumption when the alternative is present (`own`), and alternative consumption
as a function of target price (`alt`).

### Loading the cp Dataset

```{r load-cp}
# Load the cross-price example dataset
data("cp", package = "beezdemand")

# Examine structure
glimpse(cp)

# View conditions
table(cp$target)
```

The `cp` dataset contains:

- **alone**: Target commodity (cigarettes) consumption when alternative is *not* available
- **own**: Target commodity consumption when alternative *is* available
- **alt**: Alternative commodity (e-cigarettes) consumption as a function of target price

Note that `x` represents the **target commodity price** throughout all conditions.

### Step 1: Fit Target Demand (Alone Condition)

First, we fit a standard demand curve to the target commodity when the alternative is absent:

```{r fit-alone}
# Filter to alone condition
alone_data <- cp |>
    dplyr::filter(target == "alone")

# Fit demand curve (modern interface)
fit_alone <- fit_demand_fixed(
    data = alone_data,
    equation = "koff",
    k = 2
)

# View results
fit_alone
```

### Step 2: Fit Target Demand (Own Condition)

Next, fit the same demand model to target consumption when the alternative is present:

```{r fit-own}
# Filter to own condition
own_data <- cp |>
    dplyr::filter(target == "own")

# Fit demand curve
fit_own <- fit_demand_fixed(
    data = own_data,
    equation = "koff",
    k = 2
)

# View results
fit_own
```

### Step 3: Fit Cross-Price Model (Alt Condition)

Finally, fit the cross-price model to alternative consumption as a function of target price:

```{r fit-alt-cp}
# Filter to alt condition
alt_data <- cp |>
    dplyr::filter(target == "alt")

# Fit cross-price model
fit_alt <- fit_cp_nls(
    data = alt_data,
    equation = "exponentiated",
    return_all = TRUE
)

# View results
summary(fit_alt)
```

`fit_cp_nls()` uses a log10-parameterized optimizer internally (for numerical
stability), but `predict()` returns `y_pred` on the natural `y` scale.
For the `"exponential"` form, predictions may also include `y_pred_log10`.

### Comparing Results Across Conditions

```{r compare-conditions}
# Extract key parameters for each condition
coef_alone <- coef(fit_alone)
Q0_alone <- coef_alone$estimate[coef_alone$term == "q0"]
Alpha_alone <- coef_alone$estimate[coef_alone$term == "alpha"]

coef_own <- coef(fit_own)
Q0_own <- coef_own$estimate[coef_own$term == "q0"]
Alpha_own <- coef_own$estimate[coef_own$term == "alpha"]

comparison <- data.frame(
    Condition = c("Alone (Target)", "Own (Target)", "Alt (Cross-Price)"),
    Q0_or_Qalone = c(
        Q0_alone,
        Q0_own,
        coef(fit_alt)["qalone"]
    ),
    Alpha_or_I = c(
        Alpha_alone,
        Alpha_own,
        coef(fit_alt)["I"]
    )
)

comparison
```

**Interpretation:**

- Comparing `Q0` between alone and own conditions shows how the presence of an alternative
  affects baseline consumption of the target commodity
- The `I` parameter from the cross-price model indicates whether the products are
  substitutes (I < 0) or complements (I > 0)

### Combined Visualization

```{r combined-plot, fig.width=8, fig.height=4}
# Create prediction data
x_seq <- seq(0.01, max(cp$x), length.out = 100)

# Get demand predictions for each condition
pred_alone <- predict(fit_alone, newdata = data.frame(x = x_seq))$.fitted
pred_own <- predict(fit_own, newdata = data.frame(x = x_seq))$.fitted

# Cross-price model predictions (always on the natural y scale)
pred_alt <- predict(fit_alt, newdata = data.frame(x = x_seq))$y_pred

# Combine into plot data
plot_data <- data.frame(
    x = rep(x_seq, 3),
    y = c(pred_alone, pred_own, pred_alt),
    Condition = rep(c("Target (Alone)", "Target (Own)", "Alternative (Cross-Price)"), each = length(x_seq))
)

# Plot
ggplot() +
    geom_point(data = cp, aes(x = x, y = y, color = target), alpha = 0.6) +
    geom_line(data = plot_data, aes(x = x, y = y, color = Condition), linewidth = 1) +
    scale_x_log10() +
    scale_y_log10() +
    labs(
        x = "Target Price",
        y = "Consumption",
        title = "Cross-Price Analysis: All Conditions",
        color = "Condition"
    ) +
    theme_bw()
```

## Checking Unsystematic Data

```{r}
etm |>
    dplyr::filter(group %in% "E-Cigarettes" & id %in% 1)


unsys_one <- etm |>
    filter(group %in% "E-Cigarettes" & id %in% 1) |>
    check_systematic_cp()

unsys_one$results

unsys_one_lnic <- lnic |>
    filter(
        target == "adjusting",
        id == "R_00Q12ahGPKuESBT"
    ) |>
    check_systematic_cp()

unsys_one_lnic$results
```

```{r compute-unsys-etm, results='hide'}
unsys_all <- etm |>
    group_by(id, group) |>
    nest() |>
    mutate(
        sys = map(data, check_systematic_cp),
        results = map(sys, ~ dplyr::select(.x$results, -id))
    ) |>
    select(-data, -sys) |>
    unnest(results)
```

```{r show-unsys-etm}
knitr::kable(
    unsys_all |>
        group_by(group) |>
        summarise(
            n_subjects = n(),
            pct_systematic = round(mean(systematic, na.rm = TRUE) * 100, 1),
            .groups = "drop"
        ),
    caption = "Systematicity check by product group (ETM dataset)"
)
```

### Demonstration from Rzeszutek et al. (2025)

#### Low Nicotine Study (Kaplan et al., 2018)

```{r compute-unsys-lnic, results='hide'}
unsys_all_lnic <- lnic |>
    filter(target == "fixed") |>
    group_by(id, condition) |>
    nest() |>
    mutate(
        sys = map(
            data,
            check_systematic_cp
        )
    ) |>
    mutate(results = map(sys, ~ dplyr::select(.x$results, -id))) |>
    select(-data, -sys) |>
    unnest(results) |>
    arrange(id)
```

```{r show-unsys-lnic}
knitr::kable(
    unsys_all_lnic |>
        group_by(condition) |>
        summarise(
            n_subjects = n(),
            pct_systematic = round(mean(systematic, na.rm = TRUE) * 100, 1),
            .groups = "drop"
        ),
    caption = "Systematicity check by condition (Low Nicotine study, Kaplan et al., 2018)"
)
```


#### Unpublished Cannabis and Cigarette Data

```{r compute-unsys-can-cig, results='hide'}
unsys_all_can_cig <- can_cig |>
    filter(target %in% c("cannabisFix", "cigarettesFix")) |>
    group_by(id, target) |>
    nest() |>
    mutate(
        sys = map(data, check_systematic_cp),
        results = map(sys, ~ dplyr::select(.x$results, -id))
    ) |>
    select(-data, -sys) |>
    unnest(results) |>
    arrange(id)
```

```{r show-unsys-can-cig}
knitr::kable(
    unsys_all_can_cig |>
        group_by(target) |>
        summarise(
            n_subjects = n(),
            pct_systematic = round(mean(systematic, na.rm = TRUE) * 100, 1),
            .groups = "drop"
        ),
    caption = "Systematicity check by target (Cannabis/Cigarettes, unpublished data)"
)
```

#### In Progress Experimental Tobacco Marketplace Data

```{r compute-unsys-ongoing-etm, results='hide'}
unsys_all_ongoing_etm <- ongoing_etm |>
    # one person is doubled up
    distinct() |>
    filter(target %in% c("FixCig", "ECig")) |>
    group_by(id, target) |>
    nest() |>
    mutate(
        sys = map(data, check_systematic_cp),
        results = map(sys, ~ dplyr::select(.x$results, -id))
    ) |>
    select(-data, -sys) |>
    unnest(results)
```

```{r show-unsys-ongoing-etm}
knitr::kable(
    unsys_all_ongoing_etm |>
        group_by(target) |>
        summarise(
            n_subjects = n(),
            pct_systematic = round(mean(systematic, na.rm = TRUE) * 100, 1),
            .groups = "drop"
        ),
    caption = "Systematicity check by target (Ongoing ETM data)"
)
```

## Nonlinear Model Fitting

### Two Stage

```{r}
fit_one <- etm |>
    dplyr::filter(group %in% "E-Cigarettes" & id %in% 1) |>
    fit_cp_nls(
        equation = "exponentiated",
        return_all = TRUE
    )

summary(fit_one)

plot(fit_one, x_trans = "log10")
```

```{r fit-individual, results='hide'}
fit_all <- etm |>
    group_by(id, group) |>
    nest() |>
    mutate(
        unsys = map(data, check_systematic_cp),
        fit = map(data, fit_cp_nls, equation = "exponentiated", return_all = TRUE),
        summary = map(fit, summary),
        plot = map(fit, plot, x_trans = "log10"),
        glance = map(fit, glance),
        tidy = map(fit, tidy)
    )
```

```{r show-individual-fits}
# Show parameter estimates for first 3 subjects only
knitr::kable(
    fit_all |>
        slice(1:3) |>
        unnest(tidy) |>
        select(id, group, term, estimate, std.error),
    digits = 3,
    caption = "Example parameter estimates (first 3 subjects)"
)

# Show one example plot
fit_all$plot[[2]]
```

### Fit to Group (pooled by group)

```{r fit-pooled, results='hide'}
fit_pooled <- etm |>
    group_by(group) |>
    nest() |>
    mutate(
        unsys = map(data, check_systematic_cp),
        fit = map(data, fit_cp_nls, equation = "exponentiated", return_all = TRUE),
        summary = map(fit, summary),
        plot = map(fit, plot, x_trans = "log10"),
        glance = map(fit, glance),
        tidy = map(fit, tidy)
    )
```

```{r show-pooled-results}
# Show tidy results instead of summary
knitr::kable(
    fit_pooled |>
        unnest(tidy) |>
        select(group, term, estimate, std.error),
    digits = 3,
    caption = "Pooled model parameter estimates by product group"
)

# Show one plot example
fit_pooled |>
    dplyr::filter(group == "E-Cigarettes") |>
    dplyr::pull(plot) |>
    pluck(1)
```

### Fit to Group (mean)

```{r fit-mean, results='hide'}
fit_mean <- etm |>
    group_by(group, x) |>
    summarise(
        y = mean(y),
        .groups = "drop"
    ) |>
    group_by(group) |>
    nest() |>
    mutate(
        unsys = map(data, check_systematic_cp),
        fit = map(data, fit_cp_nls, equation = "exponentiated", return_all = TRUE),
        summary = map(fit, summary),
        plot = map(fit, plot, x_trans = "log10"),
        glance = map(fit, glance),
        tidy = map(fit, tidy)
    )
```

```{r show-mean-results}
# Show tidy results
knitr::kable(
    fit_mean |>
        unnest(tidy) |>
        select(group, term, estimate, std.error),
    digits = 3,
    caption = "Mean model parameter estimates by product group"
)

# Show parameter estimates plot
fit_mean |>
    unnest(cols = c(glance, tidy)) |>
    select(
        group,
        term,
        estimate
    ) |>
    ggplot(aes(x = group, y = estimate, group = term)) +
    geom_bar(stat = "identity") +
    geom_point() +
    facet_wrap(~term, ncol = 1, scales = "free_y")

# Show one example plot
fit_mean |>
    dplyr::filter(group %in% "E-Cigarettes") |>
    dplyr::pull(plot) |>
    pluck(1)
```


## Linear Model Fitting

```{r}
fit_one_linear <- etm |>
    dplyr::filter(group %in% "E-Cigarettes" & id %in% 1) |>
    fit_cp_linear(
        type = "fixed",
        log10x = TRUE,
        return_all = TRUE
    )

summary(fit_one_linear)
plot(fit_one_linear, x_trans = "log10")
```

## Linear Mixed-Effects Model

```{r}
fit_mixed <- fit_cp_linear(
    etm,
    type = "mixed",
    log10x = TRUE,
    group_effects = "interaction",
    random_slope = FALSE,
    return_all = TRUE
)

summary(fit_mixed)

# plot fixed effects only
plot(fit_mixed, x_trans = "log10", pred_type = "fixed")

# plot random effects only
plot(fit_mixed, x_trans = "log10", pred_type = "random")

# plot both fixed and random effects
plot(fit_mixed, x_trans = "log10", pred_type = "all")
```

## Extracting Model Coefficients

```{r}
glance(fit_one)
tidy(fit_one)
```

```{r}
extract_coefficients(fit_mixed)
```

## Post-hoc Estimated Marginal Means and Comparisons

```{r}
cp_posthoc_slopes(fit_mixed)
cp_posthoc_intercepts(fit_mixed)
```

## See Also

- `vignette("beezdemand")` -- Getting started with beezdemand
- `vignette("model-selection")` -- Choosing the right model class for your data
- `vignette("group-comparisons")` -- Extra sum-of-squares F-test for group comparisons
- `vignette("mixed-demand")` -- Mixed-effects nonlinear demand models
- `vignette("mixed-demand-advanced")` -- Advanced mixed-effects topics
- `vignette("hurdle-demand-models")` -- Two-part hurdle demand models
- `vignette("migration-guide")` -- Migrating from `FitCurves()`
