numops provides dependency-free helpers for common numerical operations on vectors, matrices, and arrays. This vignette shows how to clamp, wrap, interpolate, remap, divide, and normalize numeric data with consistent input validation and shape preservation.
Many numerical tasks in R require short but easily repeated
expressions. For example, clamping a value to an interval requires
nested pmin() and pmax() calls, while row
normalization requires combining rowSums() and
sweep(). numops gives these operations concise names and
consistent behavior.
The package has no runtime dependencies. Functions that combine inputs use strict scalar recycling, and operations on matrices and arrays preserve their dimensions and dimnames.
| Problem | Function |
|---|---|
| Restrict values to an interval | clamp() |
Restrict probabilities to [0, 1] |
clamp01() |
| Test whether values are within bounds | in_range() |
| Wrap periodic values | wrap() |
| Interpolate between endpoints | lerp() |
| Find a relative position between endpoints | inv_lerp() |
| Map values between intervals | remap() |
| Calculate a midpoint | midpoint() |
| Divide with a fallback for zero | divide_or() |
| Calculate Euclidean length | l2_norm() |
| Normalize vectors or array slices | normalize_l2() |
| Calculate length-preserving differences | adjacent_difference() |
clamp() restricts each value to the closed interval
[lower, upper]. Its element-wise formula is
min(max(x, lower), upper)
Use clamp01() when the interval is [0, 1],
as is common for probabilities and proportions.
Missing values in x remain missing. Bounds may be
infinite, but the lower bound cannot exceed the upper bound.
in_range() performs the inclusive test
x >= lower & x <= upper.
lerp() implements linear interpolation:
a + t * (b - a)
At t = 0, the result is a; at
t = 1, it is b. Values of t
outside [0, 1] extrapolate beyond the endpoints.
inv_lerp() performs the inverse calculation. It returns
the position of x relative to a and
b:
(x - a) / (b - a)
Results below zero or above one indicate that x lies
outside the endpoints.
remap() combines inverse interpolation and
interpolation. It maps x from the interval
from to the interval to:
to[1] + (x - from[1]) / (from[2] - from[1]) * (to[2] - to[1])
Values outside from are extrapolated. Call
clamp() separately when the output must remain inside the
destination interval.
divide_or() evaluates x / y except where
y is zero. At those positions, it returns
default.
Only a zero denominator triggers the fallback. A missing denominator produces a missing result according to ordinary R division.
For values \(x_1, \ldots, x_n\), the Euclidean or L2 norm is
\[ \lVert x \rVert_2 = \sqrt{\sum_{i = 1}^{n} x_i^2}. \]
The calculation is scaled internally to avoid unnecessary overflow and underflow.
normalize_l2() divides a vector by its L2 norm.
The margin argument identifies the dimensions that index
separate slices. For a matrix, margin = 1 operates on rows
and margin = 2 operates on columns.
x <- matrix(
c(3, 4, 0, 1, 2, 2),
nrow = 2,
byrow = TRUE,
dimnames = list(c("a", "b"), c("x", "y", "z"))
)
l2_norm(x, margin = 1)
#> a b
#> 5 3
normalize_l2(x, margin = 1)
#> x y z
#> a 0.6000000 0.8000000 0.0000000
#> b 0.3333333 0.6666667 0.6666667Column normalization uses the same interface.
The approach extends to higher-dimensional arrays by supplying one or
more dimensions in margin.
A zero vector cannot be scaled to unit length. The zero
argument controls the result:
"keep" leaves the zero slice unchanged."na" replaces the slice with missing values."error" stops the calculation.adjacent_difference() keeps the first value and then
records the change from each preceding value:
result[1] = x[1]
result[i] = x[i] - x[i - 1]
values <- c(10, 13, 12, 18)
changes <- adjacent_difference(values)
changes
#> [1] 10 3 -1 6
cumsum(changes)
#> [1] 10 13 12 18Unlike diff(), the result has the same length as the
input. Applying cumsum() reconstructs the original values
when ordinary arithmetic is reversible.
Functions with multiple numeric inputs use strict scalar recycling. Every input must have length one or a shared length. Length-one inputs are recycled; other length combinations produce an error.
Names, dimensions, and dimnames come from the first input already having the shared length.
x <- matrix(
1:4,
nrow = 2,
dimnames = list(c("first", "second"), c("a", "b"))
)
clamp(x, lower = 2, upper = 3)
#> a b
#> first 2 3
#> second 2 3This rule avoids the partial recycling that base R permits for some length combinations.
Consider three sensor readings containing temperature, relative humidity, and wind direction. The variables use different units and need separate numerical transformations before they can be combined as features.
sensors <- data.frame(
temperature = c(18, 22, 30),
humidity = c(45, 0, 75),
wind_direction = c(-10, 360, 450)
)
sensors
#> temperature humidity wind_direction
#> 1 18 45 -10
#> 2 22 0 360
#> 3 30 75 450Map temperatures from 0–40 degrees to [0, 1], convert
percentage humidity to a proportion, and wrap wind directions to
[0, 360).
temperature_score <- remap(
sensors$temperature,
from = c(0, 40),
to = c(0, 1)
)
humidity_score <- clamp01(
divide_or(sensors$humidity, 100, default = NA_real_)
)
wind_direction <- wrap(
sensors$wind_direction,
lower = 0,
upper = 360
)
transformed <- data.frame(
temperature_score,
humidity_score,
wind_direction
)
transformed
#> temperature_score humidity_score wind_direction
#> 1 0.45 0.45 350
#> 2 0.55 0.00 0
#> 3 0.75 0.75 90The two unitless scores can then be combined and normalized by row.
features <- as.matrix(
transformed[c("temperature_score", "humidity_score")]
)
normalize_l2(features, margin = 1)
#> temperature_score humidity_score
#> [1,] 0.7071068 0.7071068
#> [2,] 1.0000000 0.0000000
#> [3,] 0.7071068 0.7071068This workflow makes each numerical step explicit: remapping changes units, clamping enforces valid proportions, wrapping handles periodic values, and L2 normalization scales feature vectors to unit length.
numops supplies a compact vocabulary for recurring numerical tasks in R. The functions are dependency-free, work across vectors, matrices, and arrays, and share consistent rules for validation, recycling, missing values, and output shape. See the individual function help pages for complete edge-case behavior.