# Using alternative signal distributions with the meta-d’ model

## Introduction

The standard meta-d’ model assumes that the evidence for making type 1 decisions follows an equal-variance normal distribution and that the evidence for making type 2 decisions follows a truncated equal-variance normal distribution. However, there has been recent interest in using other distributions in signal detection theory. While not all distributions will be identifiable with the meta-d’ model (e.g., one cannot simultaneously estimate unequal variances and \(\textrm{meta-}d'\)), the `hmetad` package allows one to specify any distribution that takes a single parameter defining the location (i.e., mean, median, or mode) of the distribution.

To demonstrate this functionality, here we implement the meta-d’ model with the Gumbel-min distribution, which has been shown to provide a parsimonious explanation of recognition memory data (Meyer-Grant et al., 2026).

We begin by loading necessary packages in `R`:

    library(tidyverse)
    library(brms)
    library(tidybayes)
    library(hmetad)

## Implementing a distribution function for use with `hmetad`

In order for a distribution to be used with the `hmetad` package, one needs to implement its cumulative distribution functions in both `R` and `Stan`. Specifically, since the `hmetad` package computes the model likelihood on the logarithmic scale, one must define:

- `<distribution>_lcdf(x, mu)`: the log cumulative distribution function defining \(\textrm{log } P(X \le x)\) for a random variable \(X \sim \textrm{distribution}(\mu)\).
- `<distribution>_lccdf(x, mu)`: the log complementary cumulative distribution function defining \(\textrm{log } P(X \ge x)\) for a random variable \(X \sim \textrm{distribution}(\mu)\).

Please note that for use with the `hmetad` package, these two functions must use the above naming scheme (i.e., must be named `<distribution>_l(c)cdf`).

For example, we can write the gumbel min distribution functions in `R` as follows:

    gumbel_min_lcdf <- function(x, g) {
      log1p(-exp(-exp(x - g)))
    }
    gumbel_min_lccdf <- function(x, g) {
      -exp(x - g)
    }

One also needs to implement the two functions in `Stan` using `brms::stanvar` so that they are available to `brms` during model fitting. Fortunately, the functions themselves will usually look almost identical to their definitions in `R`, with only minor syntactic changes and/or use of more efficient helper functions. The code below implements the same two functions in `Stan`:

    gumbel_min <- stanvar(
      scode = "
    real gumbel_min_lcdf(real x, real g) {
      return log1m_exp(-exp(x - g));
    }
    real gumbel_min_lccdf(real x, real g) {
      return -exp(x - g);
    }",
      block = "functions"
    )

Again, note that the name of the functions in `Stan` must match their corresponding names in `R`. With the `lcdf` and `lccdf` functions implemented in both `R` and `Stan`, the new distribution is ready to use with the `hmetad` package!

## Data simulation

Before continuing on to model fitting, this section describes how to simulate data from the meta-d’ model with your custom distribution. This is a necessary step for parameter recovery to ensure that the meta-d’ model is well-defined with respect to your distribution.

To simulate data, you can call the `sim_metad` function supplying the optional arguments `lcdf` and `lccdf`:

    d <- sim_metad(
      N_trials = 10000, dprime = 1.5, c = .1, log_M = -.5,
      c2_0_diff = c(.25, .5, .25), c2_1_diff = c(.1, .5, .25),
      lcdf = gumbel_min_lcdf, lccdf = gumbel_min_lccdf
    )

## Model fitting

Once you have some data, fitting the model is exactly the same as with the equal-variance normal distribution, only now we also need to specify two additional arguments to `fit_metad`.

1. The `distribution` argument should be the name of the distribution as a string. This should be the part of the function names preceding `"_lcdf"` and `"_lccdf"` in both `R` and in `Stan`.
2. The `stanvars` argument should be the `stanvar` object you have created above containing the `Stan` code for your cumulative distribution functions.

Otherwise, one can call `fit_metad` just as with the equal-variance normal distribution! Please note, however, that the scale of all parameters will vary from distribution to distribution, so set priors accordingly. The code below shows how to fit the meta-d’ model with our new `gumbel_min` distribution:

    m <- fit_metad(N ~ 1,
      data = d,
      prior = prior(normal(0, 1), class = Intercept) +
        prior(normal(0, 1), class = dprime) +
        prior(normal(0, 1), class = c) +
        prior(lognormal(-1, 1), class = metac2zero1diff) +
        prior(lognormal(-1, 1), class = metac2zero2diff) +
        prior(lognormal(-1, 1), class = metac2zero3diff) +
        prior(lognormal(-1, 1), class = metac2one1diff) +
        prior(lognormal(-1, 1), class = metac2one2diff) +
        prior(lognormal(-1, 1), class = metac2one3diff),
      distribution = "gumbel_min", stanvars = gumbel_min
    )

    #>  Family: metad__4__gumbel_min__absolute__multinomial
    #>   Links: mu = log
    #> Formula: N ~ 1
    #>    Data: data.aggregated (Number of observations: 1)
    #>   Draws: 4 chains, each with iter = 2000; warmup = 1000; thin = 1;
    #>          total post-warmup draws = 4000
    #>
    #> Regression Coefficients:
    #>           Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
    #> Intercept    -0.48      0.05    -0.59    -0.37 1.00     3735     3138
    #>
    #> Further Distributional Parameters:
    #>                 Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
    #> dprime              1.44      0.03     1.39     1.50 1.00     5090     3095
    #> c                   0.12      0.01     0.10     0.15 1.00     3809     3259
    #> metac2zero1diff     0.25      0.01     0.24     0.27 1.00     5885     3153
    #> metac2zero2diff     0.50      0.01     0.48     0.53 1.00     5166     3115
    #> metac2zero3diff     0.25      0.01     0.24     0.27 1.00     6024     3155
    #> metac2one1diff      0.09      0.01     0.08     0.10 1.00     5785     3088
    #> metac2one2diff      0.49      0.01     0.47     0.52 1.00     4717     3184
    #> metac2one3diff      0.24      0.01     0.22     0.26 1.00     5861     3276
    #>
    #> Draws were sampled using sampling(NUTS). For each parameter, Bulk_ESS
    #> and Tail_ESS are effective sample size measures, and Rhat is the potential
    #> scale reduction factor on split chains (at convergence, Rhat = 1).

The model summary can be interpreted just as with any other model, however here you can see that the model family is `metad__4__gumbel_min__absolute__multinomial`, indicating that this model indeed uses the `gumbel_min` distribution with four confidence levels and \(\textrm{meta-}c = c\).

## Model estimates

Once the model is fit, it can be post-processed like any other model from the `hmetad` package. Because alternative distributions are often understood in terms of their effects on the ROC, here we will focus on plotting both pseudo-type 1 and type 2 ROCs.

Looking at the pseudo-type 1 ROC, we can see that the `gumbel_min` distribution exhibits an asymmetry:

    # psuedo type-1 ROC
    tibble(.row = 1) |>
      add_roc1_draws(m, bounds = TRUE) |>
      median_qi(p_fa, p_hit) |>
      ggplot(aes(
        x = p_fa, xmin = p_fa.lower, xmax = p_fa.upper,
        y = p_hit, ymin = p_hit.lower, ymax = p_hit.upper
      )) +
      geom_abline(slope = 1, intercept = 0, linetype = "dashed") +
      geom_errorbar(orientation = "y", width = .01) +
      geom_errorbar(orientation = "x", width = .01) +
      geom_point() +
      geom_line() +
      coord_fixed(xlim = 0:1, ylim = 0:1, expand = FALSE) +
      xlab("P(False Alarm)") +
      ylab("P(Hit)") +
      theme_bw(18)

Likewise, the `gumbel_min` distribution also has asymmetric type 2 ROCs:

    # type 2 ROC
    roc2_draws(m, tibble(.row = 1), bounds = TRUE) |>
      median_qi(p_hit2, p_fa2) |>
      mutate(response = factor(response)) |>
      ggplot(aes(
        x = p_fa2, xmin = p_fa2.lower, xmax = p_fa2.upper,
        y = p_hit2, ymin = p_hit2.lower, ymax = p_hit2.upper,
        color = response
      )) +
      geom_abline(slope = 1, intercept = 0, linetype = "dashed") +
      geom_errorbar(orientation = "y", width = .01) +
      geom_errorbar(orientation = "x", width = .01) +
      geom_point() +
      geom_line() +
      coord_fixed(xlim = 0:1, ylim = 0:1, expand = FALSE) +
      xlab("P(Type 2 False Alarm)") +
      ylab("P(Type 2 Hit)") +
      theme_bw(18)

## References

Meyer-Grant, C. G., Kellen, D., Harding, S. M., & Singmann, H. (2026). Extreme-value signal detection theory for recognition memory: The parametric road not taken. Psychological Review.