|
46 | 46 |
|
47 | 47 | # compute mode, given the range of integer values |
48 | 48 | """ |
49 | | - mode(a, [r]) |
50 | | - mode(a::AbstractArray, wv::AbstractWeights) |
| 49 | + mode(a; method=:frequency) |
| 50 | + mode(a::AbstractArray, wv::AbstractWeights; method=:frequency) |
| 51 | + mode(a::AbstractArray{T}, r::UnitRange{T}; method=:frequency) where T<:Integer |
| 52 | +
|
| 53 | +Return the mode (most common value) of `a`, optionally |
| 54 | +over a specified range `r` or weighted via a vector |
| 55 | +`wv`. |
| 56 | +
|
| 57 | +The `method` keyword argument selects the estimation method: |
| 58 | +
|
| 59 | +- `:frequency`: Frequency-based mode. Counts occurrences and returns the most common |
| 60 | + value. If several modes exist, the first one (in order of appearance) is returned. |
| 61 | + This is appropriate for discrete data. |
| 62 | +
|
| 63 | +- `:halfsample`: Half-sample mode (HSM). A robust estimator of the mode for |
| 64 | + continuous data. Repeatedly finds the contiguous half-sample with the smallest |
| 65 | + range until at most 2 points remain, then returns their midpoint. The return value |
| 66 | + is always a floating-point number and may not be an element of `a`. |
| 67 | + Throws an `ArgumentError` if `a` contains any non-finite values (`NaN`, `Inf`, |
| 68 | + or `-Inf`). |
| 69 | + This method currently does not support `r` and `wv` arguments. |
| 70 | + |
| 71 | + # References |
| 72 | +- D.R. Bickel, R. Fruehwirth (2006). On a fast, robust estimator of the mode. |
| 73 | + Computational Statistics & Data Analysis, 50(12), 3500-3530. |
| 74 | +- T. Robertson, J.D. Cryer (1974). An iterative procedure for estimating the mode. |
| 75 | + Journal of the American Statistical Association, 69(348), 1012-1016. |
51 | 76 |
|
52 | | -Return the mode (most common number) of an array, optionally |
53 | | -over a specified range `r` or weighted via a vector `wv`. |
54 | | -If several modes exist, the first one (in order of appearance) is returned. |
| 77 | +# Examples |
| 78 | +julia> mode([1, 2, 2, 3, 3, 3, 4]) |
| 79 | +3 |
| 80 | +
|
| 81 | +julia> mode([1, 2, 2, 3, 3, 3, 4], method=:frequency) |
| 82 | +3 |
| 83 | +
|
| 84 | +julia> mode([1.0, 1.1, 1.2, 5.0, 5.1], method=:halfsample) |
| 85 | +1.1 |
| 86 | +``` |
55 | 87 | """ |
56 | | -function mode(a::AbstractArray{T}, r::UnitRange{T}) where T<:Integer |
| 88 | +function mode(a; method::Symbol=:frequency) |
| 89 | + if method === :halfsample |
| 90 | + return _hsm_mode(a) |
| 91 | + elseif method === :frequency |
| 92 | + return _frequency_mode(a) |
| 93 | + else |
| 94 | + throw(ArgumentError(LazyString("`method` must be `:frequency` or `:halfsample`, got `:", method, "`"))) |
| 95 | + end |
| 96 | +end |
| 97 | + |
| 98 | +function mode(a::AbstractArray{T}, r::UnitRange{T}; method::Symbol=:frequency) where T<:Integer |
| 99 | + if method === :halfsample |
| 100 | + throw(ArgumentError("The `:halfsample` method does not support a range argument. Call `mode(a, method=:halfsample)` without a range.")) |
| 101 | + elseif method === :frequency |
| 102 | + return _frequency_mode(a, r) |
| 103 | + else |
| 104 | + throw(ArgumentError(LazyString("`method` must be `:frequency` or `:halfsample`, got `:", method, "`"))) |
| 105 | + end |
| 106 | +end |
| 107 | + |
| 108 | +function _frequency_mode(a::AbstractArray{T}, r::UnitRange{T}) where T<:Integer |
57 | 109 | isempty(a) && throw(ArgumentError("mode is not defined for empty collections")) |
58 | 110 | len = length(a) |
59 | 111 | r0 = r[1] |
@@ -107,8 +159,7 @@ function modes(a::AbstractArray{T}, r::UnitRange{T}) where T<:Integer |
107 | 159 | return ms |
108 | 160 | end |
109 | 161 |
|
110 | | -# compute mode over arbitrary iterable |
111 | | -function mode(a) |
| 162 | +function _frequency_mode(a) |
112 | 163 | isempty(a) && throw(ArgumentError("mode is not defined for empty collections")) |
113 | 164 | cnts = Dict{eltype(a),Int}() |
114 | 165 | # first element |
@@ -204,6 +255,44 @@ function modes(a::AbstractVector, wv::AbstractWeights{T}) where T <: Real |
204 | 255 | return [x for (x, w) in weights if w == mw] |
205 | 256 | end |
206 | 257 |
|
| 258 | +# Internal implementation of the Half-Sample Mode (HSM) estimator. |
| 259 | +function _hsm_mode(a) |
| 260 | + isempty(a) && throw(ArgumentError("mode is not defined for empty collections")) |
| 261 | + if !all(x -> x isa Real, a) |
| 262 | + throw(ArgumentError("mode with `method=:halfsample` is only defined " * |
| 263 | + "for collections containing real numbers")) |
| 264 | + end |
| 265 | + # Filter NaN values and sort |
| 266 | + if !all(isfinite, a) |
| 267 | + throw(ArgumentError("mode with `method=:halfsample` is not defined " * |
| 268 | + "for collections containing non-finite values")) |
| 269 | + end |
| 270 | + filteredv = sort!(collect(a)) |
| 271 | + len = length(filteredv) |
| 272 | + |
| 273 | + len == 1 && return middle(filteredv[1], filteredv[1]) |
| 274 | + len == 2 && return middle(filteredv[1], filteredv[2]) |
| 275 | + |
| 276 | + # Iteratively find the half-sample with the smallest range |
| 277 | + filteredv = @view filteredv[1:end] |
| 278 | + while len > 2 |
| 279 | + half = cld(len, 2) |
| 280 | + best_i = 1 |
| 281 | + best_width = filteredv[half] - filteredv[1] |
| 282 | + for i in 2:(len - half + 1) |
| 283 | + w = filteredv[i + half - 1] - filteredv[i] |
| 284 | + if w < best_width |
| 285 | + best_width = w |
| 286 | + best_i = i |
| 287 | + end |
| 288 | + end |
| 289 | + filteredv = @view filteredv[best_i:(best_i + half - 1)] |
| 290 | + len = length(filteredv) |
| 291 | + end |
| 292 | + |
| 293 | + return middle(filteredv[1], filteredv[len]) |
| 294 | +end |
| 295 | + |
207 | 296 | ############################# |
208 | 297 | # |
209 | 298 | # quantile and friends |
|
0 commit comments