philiprehberger-math_kit 0.2.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +45 -0
- data/lib/philiprehberger/math_kit/numeric.rb +79 -0
- data/lib/philiprehberger/math_kit/stats.rb +65 -0
- data/lib/philiprehberger/math_kit/version.rb +1 -1
- data/lib/philiprehberger/math_kit.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a73db6abf0e0f987688786bd18c0b959182dc5290c52ac52e04a55926361436e
|
|
4
|
+
data.tar.gz: 85c7d136bf63030f7009b736aa0d44c81429e383973bb8bd65ca58f0aa08c927
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 63bbbb1ca5f8c31f476dbb87f9df33569a4cdd420e49b736f2a5289c5f7e3839cff4cc3353af2c8db87f5bf909feb7b7047088e29167ea36d7318752c271848c
|
|
7
|
+
data.tar.gz: 183f0a9fec1904a98d37520df268f260e7f6e4dd9e32403fc5ce1ef3e3fdf1d346947d1751e757a4d81de0eae73cd215b393c19faa82a3b7510d7ff4b23d6c9a
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-04-15
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `Numeric` module with common integer and value helpers
|
|
14
|
+
- `Numeric.factorial(n)` for non-negative integer factorial (arbitrary precision)
|
|
15
|
+
- `Numeric.fibonacci(n)` for the n-th Fibonacci number via iterative O(n)/O(1) computation
|
|
16
|
+
- `Numeric.gcd(a, b)` for greatest common divisor (accepts negative inputs)
|
|
17
|
+
- `Numeric.lcm(a, b)` for least common multiple (accepts negative inputs)
|
|
18
|
+
- `Numeric.clamp(value, min, max)` for constraining a value to a range
|
|
19
|
+
|
|
20
|
+
## [0.3.0] - 2026-04-10
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- `Stats.describe(values)` for summary statistics (count, mean, median, min, max, stddev, variance, percentiles)
|
|
24
|
+
- `Stats.histogram(values, bins:)` for frequency distribution into equal-width bins
|
|
25
|
+
- `Stats.weighted_mean(values, weights:)` for weighted arithmetic mean
|
|
26
|
+
|
|
10
27
|
## [0.2.3] - 2026-04-08
|
|
11
28
|
|
|
12
29
|
### Changed
|
|
@@ -59,3 +76,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
59
76
|
- Linear interpolation between sorted points with extrapolation
|
|
60
77
|
- Rounding modes: bankers (round half to even), ceiling, floor, truncate with precision
|
|
61
78
|
- Simple moving average and exponential moving average
|
|
79
|
+
|
|
80
|
+
[Unreleased]: https://github.com/philiprehberger/rb-math-kit/compare/v0.4.0...HEAD
|
|
81
|
+
[0.4.0]: https://github.com/philiprehberger/rb-math-kit/compare/v0.3.0...v0.4.0
|
|
82
|
+
[0.3.0]: https://github.com/philiprehberger/rb-math-kit/compare/v0.2.3...v0.3.0
|
|
83
|
+
[0.2.3]: https://github.com/philiprehberger/rb-math-kit/compare/v0.2.2...v0.2.3
|
|
84
|
+
[0.2.2]: https://github.com/philiprehberger/rb-math-kit/compare/v0.2.1...v0.2.2
|
|
85
|
+
[0.2.1]: https://github.com/philiprehberger/rb-math-kit/compare/v0.2.0...v0.2.1
|
|
86
|
+
[0.2.0]: https://github.com/philiprehberger/rb-math-kit/compare/v0.1.2...v0.2.0
|
|
87
|
+
[0.1.2]: https://github.com/philiprehberger/rb-math-kit/compare/v0.1.1...v0.1.2
|
|
88
|
+
[0.1.1]: https://github.com/philiprehberger/rb-math-kit/compare/v0.1.0...v0.1.1
|
|
89
|
+
[0.1.0]: https://github.com/philiprehberger/rb-math-kit/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -43,6 +43,28 @@ Philiprehberger::MathKit::Stats.sum([1, 2, 3]) # => 6
|
|
|
43
43
|
Philiprehberger::MathKit::Stats.range([1, 5, 3, 9, 2]) # => 8
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
### Summary Statistics
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
Philiprehberger::MathKit::Stats.describe([1, 2, 3, 4, 5])
|
|
50
|
+
# => { count: 5, mean: 3.0, median: 3.0, min: 1.0, max: 5.0,
|
|
51
|
+
# stddev: 1.58..., variance: 2.5, p25: 2.0, p50: 3.0, p75: 4.0 }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Weighted Mean
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
Philiprehberger::MathKit::Stats.weighted_mean([10, 20, 30], weights: [3, 1, 1])
|
|
58
|
+
# => 16.0
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Histogram
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
Philiprehberger::MathKit::Stats.histogram([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], bins: 5)
|
|
65
|
+
# => [{ min: 1.0, max: 2.8, count: 2 }, { min: 2.8, max: 4.6, count: 2 }, ...]
|
|
66
|
+
```
|
|
67
|
+
|
|
46
68
|
### Skewness and Kurtosis
|
|
47
69
|
|
|
48
70
|
```ruby
|
|
@@ -116,6 +138,16 @@ Philiprehberger::MathKit::MovingAverage.simple([1, 2, 3, 4, 5], window: 3)
|
|
|
116
138
|
Philiprehberger::MathKit::MovingAverage.exponential([1, 2, 3, 4, 5], alpha: 0.5) # => [1.0, 1.5, 2.25, 3.125, 4.0625]
|
|
117
139
|
```
|
|
118
140
|
|
|
141
|
+
### Numeric Helpers
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
Philiprehberger::MathKit::Numeric.factorial(5) # => 120
|
|
145
|
+
Philiprehberger::MathKit::Numeric.fibonacci(10) # => 55
|
|
146
|
+
Philiprehberger::MathKit::Numeric.gcd(12, 18) # => 6
|
|
147
|
+
Philiprehberger::MathKit::Numeric.lcm(4, 6) # => 12
|
|
148
|
+
Philiprehberger::MathKit::Numeric.clamp(42, 0, 10) # => 10
|
|
149
|
+
```
|
|
150
|
+
|
|
119
151
|
## API
|
|
120
152
|
|
|
121
153
|
### `Stats`
|
|
@@ -140,6 +172,9 @@ Philiprehberger::MathKit::MovingAverage.exponential([1, 2, 3, 4, 5], alpha: 0.5)
|
|
|
140
172
|
| `.median_absolute_deviation(values)` | Median absolute deviation |
|
|
141
173
|
| `.trimmed_mean(values, trim: 0.1)` | Trimmed mean (remove fraction from each end) |
|
|
142
174
|
| `.winsorized_mean(values, trim: 0.1)` | Winsorized mean (clamp extremes) |
|
|
175
|
+
| `.describe(values)` | Summary statistics hash (count, mean, median, min, max, stddev, percentiles) |
|
|
176
|
+
| `.histogram(values, bins: 10)` | Frequency distribution as array of bin hashes |
|
|
177
|
+
| `.weighted_mean(values, weights:)` | Weighted arithmetic mean |
|
|
143
178
|
|
|
144
179
|
### `Regression`
|
|
145
180
|
|
|
@@ -173,6 +208,16 @@ Philiprehberger::MathKit::MovingAverage.exponential([1, 2, 3, 4, 5], alpha: 0.5)
|
|
|
173
208
|
| `.simple(values, window:)` | Simple moving average |
|
|
174
209
|
| `.exponential(values, alpha:)` | Exponential moving average |
|
|
175
210
|
|
|
211
|
+
### `Numeric`
|
|
212
|
+
|
|
213
|
+
| Method | Description |
|
|
214
|
+
|--------|-------------|
|
|
215
|
+
| `.factorial(n)` | Factorial of a non-negative integer |
|
|
216
|
+
| `.fibonacci(n)` | N-th Fibonacci number (0-indexed) |
|
|
217
|
+
| `.gcd(a, b)` | Greatest common divisor of two integers |
|
|
218
|
+
| `.lcm(a, b)` | Least common multiple of two integers |
|
|
219
|
+
| `.clamp(value, min, max)` | Clamp a numeric value between min and max |
|
|
220
|
+
|
|
176
221
|
## Development
|
|
177
222
|
|
|
178
223
|
```bash
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Philiprehberger
|
|
4
|
+
module MathKit
|
|
5
|
+
# Numeric helpers for common integer and value operations
|
|
6
|
+
module Numeric
|
|
7
|
+
class << self
|
|
8
|
+
# Factorial of a non-negative integer (n!)
|
|
9
|
+
#
|
|
10
|
+
# @param n [Integer] a non-negative integer
|
|
11
|
+
# @return [Integer] the factorial of n
|
|
12
|
+
# @raise [ArgumentError] if n is negative or not an Integer
|
|
13
|
+
def factorial(n)
|
|
14
|
+
raise ArgumentError, 'factorial requires an Integer' unless n.is_a?(Integer)
|
|
15
|
+
raise ArgumentError, 'factorial requires a non-negative integer' if n.negative?
|
|
16
|
+
|
|
17
|
+
(1..n).reduce(1, :*)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# N-th Fibonacci number (0-indexed: fibonacci(0) = 0, fibonacci(1) = 1)
|
|
21
|
+
#
|
|
22
|
+
# Uses an iterative algorithm for O(n) time and O(1) space.
|
|
23
|
+
#
|
|
24
|
+
# @param n [Integer] a non-negative index
|
|
25
|
+
# @return [Integer] the n-th Fibonacci number
|
|
26
|
+
# @raise [ArgumentError] if n is negative or not an Integer
|
|
27
|
+
def fibonacci(n)
|
|
28
|
+
raise ArgumentError, 'fibonacci requires an Integer' unless n.is_a?(Integer)
|
|
29
|
+
raise ArgumentError, 'fibonacci requires a non-negative integer' if n.negative?
|
|
30
|
+
|
|
31
|
+
a = 0
|
|
32
|
+
b = 1
|
|
33
|
+
n.times { a, b = b, a + b }
|
|
34
|
+
a
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Greatest common divisor of two integers (Euclidean algorithm)
|
|
38
|
+
#
|
|
39
|
+
# @param a [Integer] first integer
|
|
40
|
+
# @param b [Integer] second integer
|
|
41
|
+
# @return [Integer] the non-negative greatest common divisor
|
|
42
|
+
# @raise [ArgumentError] if either argument is not an Integer
|
|
43
|
+
def gcd(a, b)
|
|
44
|
+
raise ArgumentError, 'gcd requires Integer arguments' unless a.is_a?(Integer) && b.is_a?(Integer)
|
|
45
|
+
|
|
46
|
+
a.abs.gcd(b.abs)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Least common multiple of two integers
|
|
50
|
+
#
|
|
51
|
+
# @param a [Integer] first integer
|
|
52
|
+
# @param b [Integer] second integer
|
|
53
|
+
# @return [Integer] the non-negative least common multiple (0 if either is 0)
|
|
54
|
+
# @raise [ArgumentError] if either argument is not an Integer
|
|
55
|
+
def lcm(a, b)
|
|
56
|
+
raise ArgumentError, 'lcm requires Integer arguments' unless a.is_a?(Integer) && b.is_a?(Integer)
|
|
57
|
+
|
|
58
|
+
a.abs.lcm(b.abs)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Clamp a numeric value between a minimum and maximum
|
|
62
|
+
#
|
|
63
|
+
# @param value [Numeric] the value to clamp
|
|
64
|
+
# @param min [Numeric] lower bound
|
|
65
|
+
# @param max [Numeric] upper bound
|
|
66
|
+
# @return [Numeric] the clamped value
|
|
67
|
+
# @raise [ArgumentError] if min is greater than max
|
|
68
|
+
def clamp(value, min, max)
|
|
69
|
+
raise ArgumentError, 'min must not be greater than max' if min > max
|
|
70
|
+
|
|
71
|
+
return min if value < min
|
|
72
|
+
return max if value > max
|
|
73
|
+
|
|
74
|
+
value
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -291,6 +291,71 @@ module Philiprehberger
|
|
|
291
291
|
mean(winsorized)
|
|
292
292
|
end
|
|
293
293
|
|
|
294
|
+
# Summary statistics for a dataset
|
|
295
|
+
#
|
|
296
|
+
# @param values [Array<Numeric>] the input values
|
|
297
|
+
# @return [Hash] with :count, :mean, :median, :min, :max, :stddev, :variance, :p25, :p50, :p75
|
|
298
|
+
# @raise [ArgumentError] if values is empty
|
|
299
|
+
def describe(values)
|
|
300
|
+
raise ArgumentError, 'values must not be empty' if values.empty?
|
|
301
|
+
|
|
302
|
+
{
|
|
303
|
+
count: values.size,
|
|
304
|
+
mean: mean(values),
|
|
305
|
+
median: median(values),
|
|
306
|
+
min: values.min.to_f,
|
|
307
|
+
max: values.max.to_f,
|
|
308
|
+
stddev: values.size >= 2 ? stddev(values, population: false) : 0.0,
|
|
309
|
+
variance: values.size >= 2 ? variance(values, population: false) : 0.0,
|
|
310
|
+
p25: percentile(values, 25),
|
|
311
|
+
p50: percentile(values, 50),
|
|
312
|
+
p75: percentile(values, 75)
|
|
313
|
+
}
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Frequency distribution (histogram)
|
|
317
|
+
#
|
|
318
|
+
# @param values [Array<Numeric>] the input values
|
|
319
|
+
# @param bins [Integer] number of bins (default: 10)
|
|
320
|
+
# @return [Array<Hash>] array of { min:, max:, count: } hashes
|
|
321
|
+
# @raise [ArgumentError] if values is empty or bins < 1
|
|
322
|
+
def histogram(values, bins: 10)
|
|
323
|
+
raise ArgumentError, 'values must not be empty' if values.empty?
|
|
324
|
+
raise ArgumentError, 'bins must be at least 1' if bins < 1
|
|
325
|
+
|
|
326
|
+
min_val = values.min.to_f
|
|
327
|
+
max_val = values.max.to_f
|
|
328
|
+
width = max_val == min_val ? 1.0 : (max_val - min_val) / bins.to_f
|
|
329
|
+
|
|
330
|
+
result = Array.new(bins) do |i|
|
|
331
|
+
{ min: min_val + (i * width), max: min_val + ((i + 1) * width), count: 0 }
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
values.each do |v|
|
|
335
|
+
idx = width.zero? ? 0 : ((v - min_val) / width).floor
|
|
336
|
+
idx = bins - 1 if idx >= bins
|
|
337
|
+
result[idx][:count] += 1
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
result
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Weighted arithmetic mean
|
|
344
|
+
#
|
|
345
|
+
# @param values [Array<Numeric>] the input values
|
|
346
|
+
# @param weights [Array<Numeric>] the corresponding weights
|
|
347
|
+
# @return [Float] the weighted mean
|
|
348
|
+
# @raise [ArgumentError] if arrays differ in size, are empty, or weights sum to zero
|
|
349
|
+
def weighted_mean(values, weights:)
|
|
350
|
+
raise ArgumentError, 'values must not be empty' if values.empty?
|
|
351
|
+
raise ArgumentError, 'values and weights must have the same size' if values.size != weights.size
|
|
352
|
+
|
|
353
|
+
total_weight = weights.sum.to_f
|
|
354
|
+
raise ArgumentError, 'weights must not sum to zero' if total_weight.zero?
|
|
355
|
+
|
|
356
|
+
values.zip(weights).sum { |v, w| v * w } / total_weight
|
|
357
|
+
end
|
|
358
|
+
|
|
294
359
|
private
|
|
295
360
|
|
|
296
361
|
# T-distribution critical values for common confidence levels
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-math_kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philip Rehberger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Descriptive statistics, linear interpolation, rounding modes, and moving
|
|
14
14
|
averages. Lightweight math toolkit with zero dependencies.
|
|
@@ -24,6 +24,7 @@ files:
|
|
|
24
24
|
- lib/philiprehberger/math_kit.rb
|
|
25
25
|
- lib/philiprehberger/math_kit/interpolation.rb
|
|
26
26
|
- lib/philiprehberger/math_kit/moving_average.rb
|
|
27
|
+
- lib/philiprehberger/math_kit/numeric.rb
|
|
27
28
|
- lib/philiprehberger/math_kit/regression.rb
|
|
28
29
|
- lib/philiprehberger/math_kit/round.rb
|
|
29
30
|
- lib/philiprehberger/math_kit/stats.rb
|