Skip to content

Instantly share code, notes, and snippets.

@oousmane
Last active May 10, 2026 00:46
Show Gist options
  • Select an option

  • Save oousmane/31ff1efbe925e30eeba0f419eaf182b5 to your computer and use it in GitHub Desktop.

Select an option

Save oousmane/31ff1efbe925e30eeba0f419eaf182b5 to your computer and use it in GitHub Desktop.
This function streamline computation of of weighted circular mean. Useful for data described by direction and module like wind.
#' Round to a given multiple
#'
#' @param x Numeric scalar or vector.
#' @param to Target multiple. Default: `5`.
#' @param direction Rounding direction: `"nearest"` (default), `"up"`, or
#' `"down"`.
#'
#' @return Numeric vector rounded to the nearest multiple of `to`.
#' @export
#'
#' @examples
#' round_to(173) # 175
#' round_to(173, to = 10) # 170
#' round_to(173, to = 45, "up") # 180
#' round_to(c(1, 7, 13, 18, 22), to = 5) # 0 5 15 20 20
#' round_to(3.14, to = 0.25) # 3.25
round_to <- function(x, to = 5, direction = c("nearest", "up", "down")) {
# ── Validation ────────────────────────────────────────────────────────────
if (!is.numeric(x)) stop("`x` must be numeric.")
if (!is.numeric(to) || length(to) != 1 || to <= 0)
stop("`to` must be a single positive number.")
direction <- match.arg(direction)
switch(direction,
nearest = round(x / to) * to,
up = ceiling(x / to) * to,
down = floor(x / to) * to
)
}
#' Weighted circular mean of directions
#'
#' Computes the mean direction and mean resultant length of a set of vectors
#' (magnitude + direction), correctly handling angular circularity.
#'
#' @param module Numeric vector of magnitudes (e.g. wind speeds, ≥ 0).
#' @param angle Numeric vector of directions, same length as `module`.
#' @param degrees Logical. If `TRUE` (default), `angle` is in degrees;
#' otherwise in radians.
#' @param to Rounding multiple applied to the mean direction. Default: `5`.
#'
#' @return A named list:
#' \describe{
#' \item{`module`}{Mean resultant magnitude, rounded to 1 decimal place.}
#' \item{`angle`}{Mean direction in degrees in \[0, 360\[,
#' rounded to the nearest multiple of `to`.}
#' }
#' @export
#'
#' @examples
#' # Winds at 350° and 10° should average to ~0°, not 180°
#' circular_mean(c(5, 10, 8), c(350, 10, 5))
#'
#' # Round to nearest 10°
#' circular_mean(c(5, 10, 8), c(350, 10, 5), to = 10)
circular_mean <- function(module, angle, degrees = TRUE, to = 5) {
# ── Validation ────────────────────────────────────────────────────────────
if (!is.numeric(module)) stop("`module` must be numeric.")
if (!is.numeric(angle)) stop("`angle` must be numeric.")
if (length(module) != length(angle))
stop("`module` and `angle` must have the same length.")
if (any(module < 0, na.rm = TRUE))
warning("Negative values in `module` — are you sure?")
if (!is.logical(degrees) || length(degrees) != 1)
stop("`degrees` must be a single logical value.")
if (all(is.na(module)) || all(is.na(angle)))
return(list(module = NA_real_, angle = NA_real_))
# ── Computation ───────────────────────────────────────────────────────────
if (degrees) angle <- angle * pi / 180
u <- mean(module * sin(angle), na.rm = TRUE)
v <- mean(module * cos(angle), na.rm = TRUE)
mean_module <- round(sqrt(u^2 + v^2), 1)
mean_angle <- round_to(atan2(u, v) * 180 / pi, to = to, direction = "nearest")
# Bring into [0, 360[
mean_angle <- mean_angle %% 360
list(module = mean_module, angle = mean_angle)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment