Last active
May 10, 2026 00:46
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #' 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