Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revise pd #618

Merged
merged 11 commits into from
Jul 31, 2023
Merged
Show file tree
Hide file tree
Changes from 9 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
7 changes: 7 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# bayestestR 0.13.2

## Breaking Changes

* `pd_to_p()` now returns 1 and a warning for pds smaller than 0.5.
* `map_estimate()`, `p_direction()`, `p_map()`, and `p_significance()` now
return a data-frame when the input is a numeric vector. (making the output
consistently a data frame for all inputs.)

## Changes

* Retrieving models from the environment was improved.
Expand Down
32 changes: 31 additions & 1 deletion R/convert_pd_to_p.R
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,45 @@
#' `"two-sided"` (default, two tailed) or `"one-sided"` (one tailed).
#' @param ... Arguments passed to or from other methods.
#'
#' @details
#' Conversion is done using the following equation (see Makowski et al., 2019):
#' \cr\cr
#' When `direction = "two-sided"` -
#' \cr\cr
#' \deqn{p = 2 \times (1 - p_d)}{p = 2 * (1 - pd)}
#' When `direction = "one-sided"` -
#' \cr\cr
#' \deqn{p = 1 - p_d}{p = 1 - pd}
#' \cr\cr
#' Note that this conversion is only valid when the lowest possible values of pd
#' is 0.5 - i.e., when the posterior represents continuous parameter space (see
#' [p_direction]). If any pd < 0.5 are detected, they are converted to a p of 1,
#' and a warning is given.
#'
#' @references
#' Makowski, D., Ben-Shachar, M. S., Chen, S. H. A., and Lüdecke, D. (2019).
#' *Indices of Effect Existence and Significance in the Bayesian Framework*.
#' Frontiers in Psychology 2019;10:2767. \doi{10.3389/fpsyg.2019.02767}
#'
#' @examples
#' pd_to_p(pd = 0.95)
#' pd_to_p(pd = 0.95, direction = "one-sided")
#'
#' @export
pd_to_p <- function(pd, direction = "two-sided", ...) {
p <- 1 - pmax(pd, 1 - pd)
p <- 1 - pd
if (.get_direction(direction) == 0) {
p <- 2 * p
}

less_than_0.5 <- pd < 0.5

Check warning on line 42 in R/convert_pd_to_p.R

View workflow job for this annotation

GitHub Actions / lint-changed-files / lint-changed-files

file=R/convert_pd_to_p.R,line=42,col=3,[object_name_linter] Variable and function name style should match snake_case or symbols.
if (any(less_than_0.5)) {
insight::format_warning("pd values smaller than 0.5 detected.",
"pd-to-p conversion assumes a continious parameter space;",
"see help('p_direction') for more info.")
p[less_than_0.5] <- 1
}

p
}

Expand Down
23 changes: 11 additions & 12 deletions R/map_estimate.R
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,20 @@
#' @rdname map_estimate
#' @export
map_estimate.numeric <- function(x, precision = 2^10, method = "kernel", ...) {
d <- estimate_density(x, precision = precision, method = method, ...)

hdp_x <- d$x[which.max(d$y)]
hdp_y <- max(d$y)

out <- hdp_x
attr(out, "MAP_density") <- hdp_y

out <- map_estimate(data.frame(x = x),
precision, method = method, ...)
out[[1]] <- NULL
attr(out, "data") <- x
attr(out, "centrality") <- "map"
class(out) <- unique(c("map_estimate", "see_point_estimate", class(out)))

out
}

.map_estimate <- function(x, precision = 2^10, method = "kernel", ...) {
d <- estimate_density(x, precision = precision, method = method, ...)

out <- d$x[which.max(d$y)]
attr(out, "MAP_density") <- max(d$y)

Check warning on line 63 in R/map_estimate.R

View workflow job for this annotation

GitHub Actions / lint-changed-files / lint-changed-files

file=R/map_estimate.R,line=63,col=13,[object_name_linter] Variable and function name style should match snake_case or symbols.
out
}


# other models -----------------------
Expand Down Expand Up @@ -98,7 +97,7 @@

#' @keywords internal
.map_estimate_models <- function(x, precision, method, ...) {
l <- sapply(x, map_estimate, precision = precision, method = method, simplify = FALSE, ...)
l <- sapply(x, .map_estimate, precision = precision, method = method, simplify = FALSE, ...)

Check warning on line 100 in R/map_estimate.R

View workflow job for this annotation

GitHub Actions / lint-changed-files / lint-changed-files

file=R/map_estimate.R,line=100,col=8,[undesirable_function_linter] Function "sapply" is undesirable.

out <- data.frame(
Parameter = colnames(x),
Expand All @@ -108,7 +107,7 @@
)

out <- .add_clean_parameters_attribute(out, x)
attr(out, "MAP_density") <- sapply(l, attr, "MAP_density")

Check warning on line 110 in R/map_estimate.R

View workflow job for this annotation

GitHub Actions / lint-changed-files / lint-changed-files

file=R/map_estimate.R,line=110,col=13,[object_name_linter] Variable and function name style should match snake_case or symbols.

Check warning on line 110 in R/map_estimate.R

View workflow job for this annotation

GitHub Actions / lint-changed-files / lint-changed-files

file=R/map_estimate.R,line=110,col=31,[undesirable_function_linter] Function "sapply" is undesirable.
attr(out, "object_name") <- insight::safe_deparse_symbol(substitute(x))
attr(out, "centrality") <- "map"
class(out) <- unique(c("map_estimate", "see_point_estimate", class(out)))
Expand All @@ -118,7 +117,7 @@

#' @rdname map_estimate
#' @export
map_estimate.stanreg <- function(x, precision = 2^10, method = "kernel", effects = c("fixed", "random", "all"), component = c("location", "all", "conditional", "smooth_terms", "sigma", "distributional", "auxiliary"), parameters = NULL, ...) {

Check warning on line 120 in R/map_estimate.R

View workflow job for this annotation

GitHub Actions / lint-changed-files / lint-changed-files

file=R/map_estimate.R,line=120,col=121,[line_length_linter] Lines should not be more than 120 characters.
effects <- match.arg(effects)
component <- match.arg(component)

Expand All @@ -138,7 +137,7 @@

#' @rdname map_estimate
#' @export
map_estimate.brmsfit <- function(x, precision = 2^10, method = "kernel", effects = c("fixed", "random", "all"), component = c("conditional", "zi", "zero_inflated", "all"), parameters = NULL, ...) {

Check warning on line 140 in R/map_estimate.R

View workflow job for this annotation

GitHub Actions / lint-changed-files / lint-changed-files

file=R/map_estimate.R,line=140,col=121,[line_length_linter] Lines should not be more than 120 characters.
effects <- match.arg(effects)
component <- match.arg(component)

Expand Down
239 changes: 129 additions & 110 deletions R/p_direction.R
Original file line number Diff line number Diff line change
@@ -1,81 +1,99 @@
#' Probability of Direction (pd)
#'
#' Compute the **Probability of Direction** (***pd***, also known
#' as the Maximum Probability of Effect - *MPE*). It varies between `50%`
#' and `100%` (*i.e.*, `0.5` and `1`) and can be interpreted as
#' the probability (expressed in percentage) that a parameter (described by its
#' posterior distribution) is strictly positive or negative (whichever is the
#' most probable). It is mathematically defined as the proportion of the
#' posterior distribution that is of the median's sign. Although differently
#' expressed, this index is fairly similar (*i.e.*, is strongly correlated)
#' to the frequentist **p-value**.
#' \cr\cr
#' Note that in some (rare) cases, especially when used with model averaged
#' posteriors (see [weighted_posteriors()] or
#' `brms::posterior_average`), `pd` can be smaller than `0.5`,
#' reflecting high credibility of `0`.
#' Compute the **Probability of Direction** (***pd***, also known as the Maximum
#' Probability of Effect - *MPE*). This can be interpreted as the probability
#' that a parameter (described by its posterior distribution) is strictly
#' positive or negative (whichever is the most probable). Although differently
#' expressed, this index is fairly similar (*i.e.*, is strongly correlated) to
#' the frequentist **p-value** (see details).
#'
#' @param x Vector representing a posterior distribution. Can also be a Bayesian model (`stanreg`, `brmsfit` or `BayesFactor`).
#' @param method Can be `"direct"` or one of methods of [density estimation][estimate_density], such as `"kernel"`, `"logspline"` or `"KernSmooth"`. If `"direct"` (default), the computation is based on the raw ratio of samples superior and inferior to 0. Else, the result is based on the [Area under the Curve (AUC)][auc] of the estimated [density][estimate_density] function.
#' @param null The value considered as a "null" effect. Traditionally 0, but could also be 1 in the case of ratios.
#' @param x A vector representing a posterior distribution, a data frame of
#' posterior draws (samples be parameter). Can also be a Bayesian model.
#' @param method Can be `"direct"` or one of methods of [`estimate_density()`],
#' such as `"kernel"`, `"logspline"` or `"KernSmooth"`. See details.
#' @param null The value considered as a "null" effect. Traditionally 0, but
#' could also be 1 in the case of ratios of change (OR, IRR, ...).
#' @inheritParams hdi
#'
#' @details
#' \subsection{What is the *pd*?}{
#' ## What is the *pd*?
#' The Probability of Direction (pd) is an index of effect existence, ranging
#' from `50%` to `100%`, representing the certainty with which an effect goes in
#' a particular direction (*i.e.*, is positive or negative). Beyond its
#' simplicity of interpretation, understanding and computation, this index also
#' presents other interesting properties:
#' \itemize{
#' \item It is independent from the model: It is solely based on the posterior
#' distributions and does not require any additional information from the data
#' or the model.
#' \item It is robust to the scale of both the response variable and the predictors.
#' \item It is strongly correlated with the frequentist p-value, and can thus
#' from 0 to 1, representing the certainty with which an effect goes in a
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't it confusing to say that in ranges from 0-1 when in practice it's 0.5-1?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In your practice 😉
I've already come across several applied examples with discrete parameter spaces in my work.

So how about this:

... is an index of effect existence, representing the certainty with which an effect goes in a particular direction (i.e., is positive or negative / has a sign), typically ranging from 0.5 to 1 (but see next section for cases where it can range between 0 and 1). ...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good, I'll make the edits and merge, thanks a lot!

#' particular direction (*i.e.*, is positive or negative / has a sign). Beyond
#' its simplicity of interpretation, understanding and computation, this index
#' also presents other interesting properties:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reasons to remove:

  • It is independent from the model: It is solely based on the posterior distributions and does not require any additional information from the data or the model.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not clear to me what that means - all posterior related functions (so all but BF and CI) are objective descriptions of the posterior - divorced from the data or model.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it's not a unique feature, but it doesn't harm to mention it as well as part of the "interesting properties" (for good or for bad) 🤷

#' - It is robust to the scale of both the response variable and the predictors.
#' - It is strongly correlated with the frequentist p-value, and can thus
#' be used to draw parallels and give some reference to readers non-familiar
#' with Bayesian statistics.
#' }
#' }
#' \subsection{Relationship with the p-value}{
#' In most cases, it seems that the *pd* has a direct correspondence with the frequentist one-sided *p*-value through the formula \ifelse{html}{\out{p<sub>one&nbsp;sided</sub>&nbsp;=&nbsp;1&nbsp;-&nbsp;<sup>p(<em>d</em>)</sup>/<sub>100</sub>}}{\eqn{p_{one sided}=1-\frac{p_{d}}{100}}} and to the two-sided p-value (the most commonly reported one) through the formula \ifelse{html}{\out{p<sub>two&nbsp;sided</sub>&nbsp;=&nbsp;2&nbsp;*&nbsp;(1&nbsp;-&nbsp;<sup>p(<em>d</em>)</sup>/<sub>100</sub>)}}{\eqn{p_{two sided}=2*(1-\frac{p_{d}}{100})}}. Thus, a two-sided p-value of respectively `.1`, `.05`, `.01` and `.001` would correspond approximately to a *pd* of `95%`, `97.5%`, `99.5%` and `99.95%`. See also [pd_to_p()].
#' }
#' \subsection{Methods of computation}{
#' The most simple and direct way to compute the *pd* is to 1) look at the
#' median's sign, 2) select the portion of the posterior of the same sign and
#' 3) compute the percentage that this portion represents. This "simple" method
#' is the most straightforward, but its precision is directly tied to the
#' number of posterior draws. The second approach relies on [density
#' estimation][estimate_density]. It starts by estimating the density function
#' (for which many methods are available), and then computing the [area under
#' the curve][area_under_curve] (AUC) of the density curve on the other side of
#' 0.
#' }
#' \subsection{Strengths and Limitations}{
#' **Strengths:** Straightforward computation and interpretation. Objective
#' property of the posterior distribution. 1:1 correspondence with the
#' frequentist p-value.
#' \cr \cr
#' **Limitations:** Limited information favoring the null hypothesis.
#' }
#' with Bayesian statistics (Makowski et al., 2019).
#'
#' @return
#' Values between 0.5 and 1 corresponding to the probability of direction (pd).
#' ## Relationship with the p-value
#' In most cases, it seems that the *pd* has a direct correspondence with the
#' frequentist one-sided *p*-value through the formula (for two-sided *p*):
#' \deqn{p = 2 \times (1 - p_d)}{p = 2 * (1 - pd)}
#' Thus, a two-sided p-value of respectively `.1`, `.05`, `.01` and `.001` would
#' correspond approximately to a *pd* of `95%`, `97.5%`, `99.5%` and `99.95%`.
#' See [pd_to_p()] for details.
#'
#' ## Possible Range of Values
#' The largest value *pd* can take is 1 - the posterior is strictly directional.
#' However, the smallest value *pd* can take depends on the parameter space
#' represented by the posterior.
#' \cr\cr
#' **For a continuous parameter space**, exact values of 0 (or any point null
#' value) are not possible, and so 100% of the posterior has _some_ sign, some
#' positive, some negative. Therefore, the smallest the *pd* can be is 0.5 -
#' with an equal posterior mass of positive and negative values. Values close to
#' 0.5 _cannot_ be used to support the null hypothesis (that the parameter does
#' _not_ have a direction) is a similar why to how large p-values cannot be used
#' to support the null hypothesis (see [`pd_tp_p()`]; Makowski et al., 2019).
#' \cr\cr
#' Note that in some (rare) cases, especially when used with model averaged
#' posteriors (see [weighted_posteriors()] or
#' `brms::posterior_average`), `pd` can be smaller than `0.5`,
#' reflecting high credibility of `0`. To detect such cases, the
#' `method = "direct"` must be used.
#' **For a discrete parameter space or a parameter space that is a mixture
#' between discrete and continuous spaces**, exact values of 0 (or any point
#' null value) _are_ possible! Therefore, the smallest the *pd* can be is 0 -
#' with 100% of the posterior mass on 0. Thus values close to 0 can be used to
#' support the null hypothesis (see van den Bergh et al., 2021).
#' \cr\cr
#' Examples of posteriors representing discrete parameter space:
#' - When a parameter can only take discrete values.
#' - When a mixture prior/posterior is used (such as the spike-and-slab prior;
#' see van den Bergh et al., 2021).
#' - When conducting Bayesian model averaging (e.g., [weighted_posteriors()] or
#' `brms::posterior_average`).
#'
#' ## Methods of computation
#' The *pd* is defined as:
#' \deqn{p_d = max({Pr(\hat{\theta} < \theta_{null}), Pr(\hat{\theta} > \theta_{null})})}{pd = max(mean(x < null), mean(x > null))}

Check warning on line 66 in R/p_direction.R

View workflow job for this annotation

GitHub Actions / lint-changed-files / lint-changed-files

file=R/p_direction.R,line=66,col=121,[line_length_linter] Lines should not be more than 120 characters.
#' \cr\cr
#' The most simple and direct way to compute the *pd* is to compute the
#' proportion of positive (or larger than `null`) posterior samples, the
#' proportion of negative (or smaller than `null`) posterior samples, and take
#' the larger of the two. This "simple" method is the most straightforward, but
#' its precision is directly tied to the number of posterior draws.
#' \cr\cr
#' The second approach relies on [`density estimation()`]: It starts by
#' estimating the continuous-smooth density function (for which many methods are
#' available), and then computing the [area under the curve][area_under_curve]
#' (AUC) of the density curve on either side of `null` and taking the maximum
#' between them. Note the this approach assumes a continuous density function,
#' and so **when the posterior represents a (partially) discrete parameter
#' space, only the direct method _must_ be used** (see above).
#'
#' @return
#' Values between 0.5 and 1 *or* between 0 and 1 (see above) corresponding to
#' the probability of direction (pd).
#'
#' @seealso [pd_to_p()] to convert between Probability of Direction (pd) and p-value.
#'
#' @note There is also a [`plot()`-method](https://easystats.github.io/see/articles/bayestestR.html) implemented in the \href{https://easystats.github.io/see/}{\pkg{see}-package}.

Check warning on line 88 in R/p_direction.R

View workflow job for this annotation

GitHub Actions / lint-changed-files / lint-changed-files

file=R/p_direction.R,line=88,col=121,[line_length_linter] Lines should not be more than 120 characters.
#'
#' @references
#' Makowski D, Ben-Shachar MS, Chen SHA, Lüdecke D (2019) Indices of Effect
#' Existence and Significance in the Bayesian Framework. Frontiers in Psychology
#' 2019;10:2767. \doi{10.3389/fpsyg.2019.02767}
#' - Makowski, D., Ben-Shachar, M. S., Chen, S. A., & Lüdecke, D. (2019).
#' Indices of effect existence and significance in the Bayesian framework.
#' Frontiers in psychology, 10, 2767. \doi{10.3389/fpsyg.2019.02767}
#' - van den Bergh, D., Haaf, J. M., Ly, A., Rouder, J. N., & Wagenmakers, E. J.
#' (2021). A cautionary note on estimating effect size. Advances in Methods
#' and Practices in Psychological Science, 4(1). \doi{10.1177/2515245921992035}
#'
#' @examples
#' library(bayestestR)
Expand Down Expand Up @@ -144,52 +162,10 @@
#' @rdname p_direction
#' @export
p_direction.numeric <- function(x, method = "direct", null = 0, ...) {
if (method == "direct") {
pdir <- max(
c(
length(x[x > null]) / length(x), # pd positive
length(x[x < null]) / length(x) # pd negative
)
)
} else {
dens <- estimate_density(x, method = method, precision = 2^10, extend = TRUE, ...)
if (length(x[x > null]) > length(x[x < null])) {
dens <- dens[dens$x > null, ]
} else {
dens <- dens[dens$x < null, ]
}
pdir <- area_under_curve(dens$x, dens$y, method = "spline")
if (pdir >= 1) pdir <- 1 # Enforce bounds
}

attr(pdir, "method") <- method
attr(pdir, "data") <- x

class(pdir) <- unique(c("p_direction", "see_p_direction", class(pdir)))

pdir
}





#' @export
p_direction.parameters_model <- function(x, ...) {
out <- data.frame(
"Parameter" = x$Parameter,
"pd" = p_to_pd(p = x[["p"]]),
row.names = NULL,
stringsAsFactors = FALSE
)

if (!is.null(x$Component)) {
out$Component <- x$Component
}

attr(out, "object_name") <- insight::safe_deparse_symbol(substitute(x))
class(out) <- unique(c("p_direction", "see_p_direction", class(out)))

obj_name <- insight::safe_deparse_symbol(substitute(x))
out <- p_direction(data.frame(x = x), method = method, null = null, ...)
out[[1]] <- NULL
attr(out, "object_name") <- obj_name
out
}

Expand All @@ -201,9 +177,9 @@
x <- .select_nums(x)

if (ncol(x) == 1) {
pd <- p_direction(x[[1]], method = method, null = null, ...)
pd <- .p_direction(x[[1]], method = method, null = null, ...)
} else {
pd <- sapply(x, p_direction, method = method, null = null, simplify = TRUE, ...)
pd <- sapply(x, .p_direction, method = method, null = null, simplify = TRUE, ...)

Check warning on line 182 in R/p_direction.R

View workflow job for this annotation

GitHub Actions / lint-changed-files / lint-changed-files

file=R/p_direction.R,line=182,col=11,[undesirable_function_linter] Function "sapply" is undesirable.
}

out <- data.frame(
Expand Down Expand Up @@ -465,6 +441,49 @@
out
}

#' @export
p_direction.parameters_model <- function(x, ...) {
out <- data.frame(
"Parameter" = x$Parameter,
"pd" = p_to_pd(p = x[["p"]]),
row.names = NULL,
stringsAsFactors = FALSE
)

if (!is.null(x$Component)) {
out$Component <- x$Component
}

attr(out, "object_name") <- insight::safe_deparse_symbol(substitute(x))
class(out) <- unique(c("p_direction", "see_p_direction", class(out)))

out
}

#' @keywords internal
.p_direction <- function(x, method = "direct", null = 0, ...) {
if (method == "direct") {
pdir <- max(
length(x[x > null]), # pd positive
length(x[x < null]) # pd negative
) / length(x)
} else {
dens <- estimate_density(x, method = method, precision = 2^10, extend = TRUE, ...)
if (length(x[x > null]) > length(x[x < null])) {
dens <- dens[dens$x > null, ]
} else {
dens <- dens[dens$x < null, ]
}
pdir <- area_under_curve(dens$x, dens$y, method = "spline")
if (pdir >= 1) {
# Enforce bounds
pdir <- 1
}
}

pdir
}

# Methods -----------------------------------------------------------------


Expand Down
Loading
Loading