Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@
^LICENSE\.md$
^doc$
^Meta$
^\.positai$
^\.claude$
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,4 @@ rsconnect/
inst/doc
/doc/
/Meta/
.positai
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ Description: Provides tools for estimating and visualizing common estimands
License: MIT + file LICENSE
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.2
Depends: R (>= 4.2.0)
Imports:
broom,
Expand All @@ -31,3 +30,4 @@ Suggests:
Config/testthat/edition: 3
LazyData: true
VignetteBuilder: knitr
Config/roxygen2/version: 8.0.0
6 changes: 4 additions & 2 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ S3method(summary,marginal_means)
export(amce)
export(cjlm)
export(conditional_amce)
export(conditional_marginal_means)
export(marginal_means)
export(conditional_mm)
export(mm)
importFrom(ggplot2,autoplot)
importFrom(stats,complete.cases)
importFrom(stats,gaussian)
importFrom(stats,pnorm)
importFrom(stats,reformulate)
importFrom(stats,terms)
10 changes: 5 additions & 5 deletions R/amce.r
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
#' Preference Experiments. \emph{Political Analysis}, 22(1), 1--30.
#' \doi{10.1093/pan/mpt024}
#'
#' @seealso \code{\link{conditional_amce}}, \code{\link{marginal_means}}
#' @seealso \code{\link{conditional_amce}}, \code{\link{mm}}
#'
#' @examples
#' library(conjest)
Expand Down Expand Up @@ -66,10 +66,10 @@
amce <- function(data, formula = NULL, outcome = NULL, attributes = NULL, id = NULL, wts = NULL, design = NULL) {

if (!is.null(formula)) {
outcome <- deparse(rlang::f_lhs(formula))
outcome <- deparse(rlang::f_lhs(formula))
attributes <- labels(terms(formula))
}

assert_fct(data, attributes)

results <- lapply(attributes, function(attr) {
Expand Down Expand Up @@ -131,7 +131,7 @@ amce <- function(data, formula = NULL, outcome = NULL, attributes = NULL, id = N
#' thus do not provide direct insight about patterns of preferences
#' between groups (Leeper, Hobolt & Tilley, 2020). For comparing
#' absolute levels of favorability across subgroups, use
#' \code{\link{conditional_marginal_means}} instead.
#' \code{\link{conditional_mm}} instead.
#'
#' @param data A data frame containing the conjoint data.
#' @param formula A formula of the form \code{outcome ~ attr1 + attr2 + ...}.
Expand All @@ -157,7 +157,7 @@ amce <- function(data, formula = NULL, outcome = NULL, attributes = NULL, id = N
#' Subgroup Preferences in Conjoint Experiments. \emph{Political Analysis},
#' 28(2), 207--221. \doi{10.1017/pan.2019.30}
#'
#' @seealso \code{\link{amce}}, \code{\link{conditional_marginal_means}}
#' @seealso \code{\link{amce}}, \code{\link{conditional_mm}}
#'
#' @examples
#' library(conjest)
Expand Down
34 changes: 22 additions & 12 deletions R/marginal_means.r
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@
#'
#' @return A data frame of class `marginal_means`
#'
#' @seealso \code{\link{amce}}, \code{\link{conditional_marginal_means}}
#' @seealso \code{\link{amce}}, \code{\link{conditional_mm}}
#'
#' @examples
#' library(conjest)
#' data("trust")
#'
#' res <- marginal_means(trust, selected ~ group + sex + age, id = ~uuid)
#' res <- mm(trust, selected ~ group + sex + age, id = ~uuid)
#'
#' # Custom methods for inspecting & visualizing results:
#' summary(res)
Expand All @@ -43,14 +43,14 @@
#'
#' cj_design <- svydesign(id = ~uuid, weights = ~weight, data = trust)
#'
#' marginal_means(
#' mm(
#' trust,
#' selected ~ group + sex + age,
#' design = cj_design
#' )
#'
#' @export
marginal_means <- function(data, formula = NULL, outcome = NULL, attributes = NULL, id = NULL, wts = NULL, design = NULL) {
mm <- function(data, formula = NULL, outcome = NULL, attributes = NULL, id = NULL, wts = NULL, design = NULL) {

if (!is.null(formula)) {
outcome <- deparse(rlang::f_lhs(formula))
Expand Down Expand Up @@ -79,9 +79,14 @@ marginal_means <- function(data, formula = NULL, outcome = NULL, attributes = NU
results$lower <- results$estimate - results$std.error
results$upper <- results$estimate + results$std.error

# correct z-test for H_0: MM = 0.5
adj_stats <- z_adj(results$estimate, results$std.error, h_0 = .5)
results$z <- adj_stats$z
results$p <- adj_stats$p

results <- tibble::tibble(results[, c(
"attribute", "level", "term", "estimate",
"std.error", "lower", "upper"
"attribute", "level", "term", "estimate",
"std.error", "lower", "upper", "z", "p"
)])

class(results) <- c("marginal_means", class(results))
Expand All @@ -92,7 +97,7 @@ marginal_means <- function(data, formula = NULL, outcome = NULL, attributes = NU

#' Estimate Conditional Marginal Means for a Conjoint Experiment
#'
#' Conditional version of \code{\link{marginal_means}}. Computes marginal means separately for each
#' Conditional version of \code{\link{mm}}. Computes marginal means separately for each
#' level of a respondent-level grouping variable, allowing comparisons of conjoint preferences across
#' subgroups. This approach is preferred over conditional AMCEs for subgroup analysis, as conditional
#' marginal means do not require a baseline, and are thus not sensitive to the choice of a baseline
Expand All @@ -113,7 +118,7 @@ marginal_means <- function(data, formula = NULL, outcome = NULL, attributes = NU
#'
#' @return A data frame of class `marginal_means`
#'
#' @seealso \code{\link{marginal_means}}, \code{\link{conditional_amce}}
#' @seealso \code{\link{mm}}, \code{\link{conditional_amce}}
#'
#' @references Leeper, T. J., Hobolt, S. B., and Tilley, J. (2020). Measuring
#' Subgroup Preferences in Conjoint Experiments. \emph{Political Analysis},
Expand All @@ -127,7 +132,7 @@ marginal_means <- function(data, formula = NULL, outcome = NULL, attributes = NU
#' # respondent characteristics, e.g.
#' # trust in men vs. women, by sex of
#' # the respondent:
#' res <- conditional_marginal_means(
#' res <- conditional_mm(
#' trust,
#' selected ~ sex,
#' group = resp_sex,
Expand All @@ -147,22 +152,22 @@ marginal_means <- function(data, formula = NULL, outcome = NULL, attributes = NU
#' # outgroup profiles vary depending on the
#' # sex of the profile?
#'
#' conditional_marginal_means(
#' conditional_mm(
#' trust,
#' selected ~ group,
#' group = sex,
#' id = ~uuid
#' )
#'
#' @export
conditional_marginal_means <- function(data, formula = NULL, outcome = NULL, attributes = NULL, id = NULL, group = NULL, wts = NULL, design = NULL) {
conditional_mm <- function(data, formula = NULL, outcome = NULL, attributes = NULL, id = NULL, group = NULL, wts = NULL, design = NULL) {
conditional_estimates(
data, formula, outcome, attributes,
groupvar = rlang::as_name(rlang::ensym(group)),
id = id,
wts = wts,
design = design,
.estimator = marginal_means,
.estimator = mm,
.class = "conditional_marginal_means"
)
}
Expand Down Expand Up @@ -220,13 +225,16 @@ summary.marginal_means <- function(object, ...) {
`Estimate` = formatC(subset$estimate, format = "f", digits = 4),
`Std. Error` = formatC(subset$std.error, format = "f", digits = 4),
` ` = ifelse(subset$estimate < .5, "-", "+"),
` ` = make_stars(subset$p),
check.names = FALSE
)

print(out, row.names = FALSE)
cat("\n")

}

cat("\nSignif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1\n")
}

#' @export
Expand Down Expand Up @@ -257,6 +265,7 @@ summary.conditional_marginal_means <- function(object, ...) {
`Estimate` = formatC(subset$estimate, format = "f", digits = 4),
`Std. Error` = formatC(subset$std.error, format = "f", digits = 4),
` ` = ifelse(subset$estimate >= 0.5, "+", "-"),
` ` = make_stars(subset$p),
check.names = FALSE
)

Expand All @@ -269,6 +278,7 @@ summary.conditional_marginal_means <- function(object, ...) {

}

cat("\nSignif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1\n")
}

#' @export
Expand Down
2 changes: 1 addition & 1 deletion R/package.r
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#' @importFrom stats terms reformulate gaussian
#' @importFrom stats terms reformulate gaussian complete.cases pnorm
#' @importFrom ggplot2 autoplot
NULL

Expand Down
68 changes: 61 additions & 7 deletions R/utils.r
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,31 @@ validate_design <- function(data, design, id, wts) {

}

# Check if all variables from the formula actually exist:
#
#' @noRd
assert_exist <- function(data, variables) {

dne <- variables[!variables %in% colnames(data)]

if (length(dne) > 0) {
cli::cli_abort(c(
"Some variables do not exist in {.var data}:",
"x" = "{.var {dne}}"
), call = rlang::caller_env())
}

}


# Check if all provided attribute are factors, throw if not (used for AMCE
# and Marginal Means to ensure coherent behavior).
#
#' @noRd
assert_fct <- function(data, attributes) {

assert_exist(data, attributes)

non_factors <- attributes[!sapply(attributes, function(x) is.factor(data[[x]]))]

if (length(non_factors) > 0) {
Expand All @@ -68,6 +87,27 @@ assert_fct <- function(data, attributes) {

}

# Drop rows with missing values; otherwise they might yield non-descript errors
# later in svyglm ("no missing values in weights allowed")
#
#' @noRd
check_data <- function(data, variables) {

assert_exist(data, variables)

n_before <- nrow(data)
new_data <- data[complete.cases(data[, variables]), ]
n_dropped <- n_before - nrow(new_data)

if (n_dropped > 0) {
cli::cli_warn(
"Dropping {n_dropped} row{?s} with missing values in variable{?s} {.var {variables}}."
)
}

new_data
}

# Get stars for significance levels for printouts. Can be called like
# `p_values |> make_stars(c(.01 = "***", .05 = "**", .1 = "*"))`
#
Expand Down Expand Up @@ -95,14 +135,27 @@ format_number <- function(x, thres = 1e-4) {
)
}

#' Marginal means should really be tested for whether they are different from
#' 0.5. Using this internally to adjust Z to some H_0; in this case: H_0 is
#' MM = 0.5.
#'
#' @noRd
z_adj <- function(est, se, h_0) {

z <- (est - h_0) / se
p <- 2L * pnorm(-abs(z))

list(p = p, z = z)
}

# `cjlm` is basically the backend that fits the actual model and that `amce` &
# `marginal_means` call into. It just wraps `survey::svyglm` which is used here
# `mm` call into. It just wraps `survey::svyglm` which is used here
# to estimate a linear model adjusting for design features.
#
#' Fit a Custom Linear Model for Conjoint Data
#'
#' Fit arbitrary linear models to conjoint data. Unlike \code{\link{amce}}
#' and \code{\link{marginal_means}}, which handle formula parsing, baseline
#' and \code{\link{mm}}, which handle formula parsing, baseline
#' reconstruction, and result formatting automatically, \code{cjlm} is intended
#' for custom model specifications that do not fit the other functions in this package,
#' such as models with interactions, continuous predictors, or
Expand All @@ -125,8 +178,8 @@ format_number <- function(x, thres = 1e-4) {
#' attribute of the result and can be retrieved via \code{attr(result, "model")}
#' if needed.
#'
#' @seealso \code{\link{amce}}, \code{\link{marginal_means}},
#' \code{\link{conditional_amce}}, \code{\link{conditional_marginal_means}}
#' @seealso \code{\link{amce}}, \code{\link{mm}},
#' \code{\link{conditional_amce}}, \code{\link{conditional_mm}}
#'
#' @examples
#' library(conjest)
Expand All @@ -151,13 +204,14 @@ format_number <- function(x, thres = 1e-4) {
#' @export
cjlm <- function(data, formula = NULL, id = NULL, wts = NULL, design = NULL) {

data <- check_data(data, all.vars(formula))
design <- validate_design(data, design, id, wts)

model <- survey::svyglm(formula, design, family = gaussian())
res <- broom::tidy(model)
res <- broom::tidy(model)

class(res) <- c("cjlm", class(res))
attr(res, "id") <- id
class(res) <- c("cjlm", class(res))
attr(res, "id") <- id
attr(res, "model") <- model

res
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ Provides tools for estimating and visualizing common estimands from conjoint sur
This package implements the methods described in [Hainmueller, Hopkins, and Yamamoto (2014)](https://doi.org/10.1093/pan/mpt024) and [Leeper, Hobolt, and Tilley (2020)](https://doi.org/10.1017/pan.2019.30), allowing you to estimate & visualize:

* Average Marginal Component Effects (AMCEs), via `amce()`.
* Marginal Means, via `marginal_means()`.
* Conditional versions of both, via `conditional_amce()` and `conditional_marginal_means()`.
* Marginal Means, via `mm()`.
* Conditional versions of both, via `conditional_amce()` and `conditional_mm()`.
* Arbitrary models with design-specific adjustments, via `cjlm()`.

Custom S3 methods for summarizing and visualizing results are included for `summary()` and `ggplot2::autoplot()`.
Expand Down Expand Up @@ -56,13 +56,13 @@ summary(amce_res)
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
```

Results also work with `ggplot2::autoplot()`, both for `marginal_means()`, as well as for `amce()`:
Results also work with `ggplot2::autoplot()`, both for `mm()`, as well as for `amce()`:

```r
library(ggplot2)

trust |>
marginal_means(selected ~ sex + age + class, id = ~uuid) |>
mm(selected ~ sex + age + class, id = ~uuid) |>
autoplot()
```
<div align="center">
Expand Down
2 changes: 1 addition & 1 deletion man/amce.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions man/cjlm.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading