my_math_gem 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2e0a3e729a12b6f393f4e517d1aa7ea6fd1b323f17ce0d24323eb3e7e8994cb0
4
+ data.tar.gz: 64db298a15dda3aa7a95abf877568d3d18b58846830062ca13e01b3791153b59
5
+ SHA512:
6
+ metadata.gz: becf314c18f3ac4f52b4eab65b1d1380fff5e6a985c34e40b2689e0203d8f4e7aec5d7b6cb7c3011ab0ee812ed7fa1a45b2b201dbe3163550e9f98e7413ba67b
7
+ data.tar.gz: 7dfffd172439642ab57bcab54e50d0e5fd2e6403dd89e1ea9f884399e1b997e34e176bb31667f194f89ee9debd9aaae3d12dc944f2f978238aaf87b704559184
data/LICENSE ADDED
@@ -0,0 +1,11 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Andri
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+ ...
data/README.md ADDED
File without changes
@@ -0,0 +1,113 @@
1
+ module MyMathGem
2
+ module Calculus
3
+ # Turunan orde n menggunakan finite difference (central difference)
4
+ def self.derivative_n(f, x, n=1, h=1e-5)
5
+ raise ArgumentError, "n harus positif integer" unless n.is_a?(Integer) && n > 0
6
+ raise ArgumentError, "f harus callable" unless f.respond_to?(:call)
7
+
8
+ # Turunan orde 1
9
+ if n == 1
10
+ return derivative(f, x, h, :central)
11
+ end
12
+
13
+ # Rekursif turunan orde lebih tinggi
14
+ lower_order = ->(t) { derivative_n(f, t, n - 1, h) }
15
+ derivative(lower_order, x, h, :central)
16
+ end
17
+
18
+ # Turunan parsial fungsi multivariabel f, terhadap variabel index var_idx (0-based)
19
+ # args = array nilai input [x,y,z,...]
20
+ def self.partial_derivative(f, args, var_idx, h=1e-5)
21
+ raise ArgumentError, "f harus callable" unless f.respond_to?(:call)
22
+ raise ArgumentError, "args harus array" unless args.is_a?(Array)
23
+ raise ArgumentError, "var_idx di luar jangkauan" unless var_idx.between?(0, args.length-1)
24
+
25
+ args_forward = args.dup
26
+ args_backward = args.dup
27
+ args_forward[var_idx] += h
28
+ args_backward[var_idx] -= h
29
+ (f.call(*args_forward) - f.call(*args_backward)) / (2 * h)
30
+ end
31
+
32
+ # Integral adaptif sederhana menggunakan recursive Simpson's rule
33
+ def self.adaptive_integral(f, a, b, tol=1e-6, max_recursion=20)
34
+ raise ArgumentError, "f harus callable" unless f.respond_to?(:call)
35
+
36
+ simpson = ->(fa, fm, fb, h) { h * (fa + 4*fm + fb) / 6.0 }
37
+
38
+ recursive_adaptive = lambda do |f, a, b, fa, fm, fb, integral, tol, depth|
39
+ mid = (a + b) / 2.0
40
+ f1 = f.call((a + mid) / 2.0)
41
+ f2 = f.call((mid + b) / 2.0)
42
+ left = simpson.call(fa, f1, fm, (mid - a))
43
+ right = simpson.call(fm, f2, fb, (b - mid))
44
+ if depth >= max_recursion || (left + right - integral).abs < 15 * tol
45
+ return left + right + (left + right - integral) / 15.0
46
+ else
47
+ left_int = recursive_adaptive.call(f, a, mid, fa, f1, fm, left, tol/2, depth + 1)
48
+ right_int = recursive_adaptive.call(f, mid, b, fm, f2, fb, right, tol/2, depth + 1)
49
+ left_int + right_int
50
+ end
51
+ end
52
+
53
+ fa = f.call(a)
54
+ fb = f.call(b)
55
+ mid = (a + b) / 2.0
56
+ fm = f.call(mid)
57
+ initial = simpson.call(fa, fm, fb, (b - a))
58
+
59
+ recursive_adaptive.call(f, a, b, fa, fm, fb, initial, tol, 0)
60
+ end
61
+
62
+ # Turunan numerik standar fungsi f di titik x
63
+ # Metode: :central (default), :forward, :backward
64
+ def self.derivative(f, x, h=1e-5, method=:central)
65
+ raise ArgumentError, "f harus callable" unless f.respond_to?(:call)
66
+
67
+ case method
68
+ when :central
69
+ (f.call(x + h) - f.call(x - h)) / (2 * h)
70
+ when :forward
71
+ (f.call(x + h) - f.call(x)) / h
72
+ when :backward
73
+ (f.call(x) - f.call(x - h)) / h
74
+ else
75
+ raise ArgumentError, "Method tidak valid. Gunakan :central, :forward, atau :backward"
76
+ end
77
+ end
78
+
79
+ # Integral numerik fungsi f dari a ke b
80
+ # Metode: :trapezoid (default) atau :simpson
81
+ # n = jumlah segmen, genap jika pakai Simpson
82
+ def self.integral(f, a, b, n=1000, method=:trapezoid)
83
+ raise ArgumentError, "f harus callable" unless f.respond_to?(:call)
84
+ raise ArgumentError, "n harus positif integer" unless n.is_a?(Integer) && n > 0
85
+ raise ArgumentError, "Untuk Simpson, n harus genap" if method == :simpson && n.odd?
86
+
87
+ case method
88
+ when :trapezoid
89
+ dx = (b - a).to_f / n
90
+ total = 0.0
91
+ n.times do |i|
92
+ x0 = a + i * dx
93
+ x1 = x0 + dx
94
+ total += 0.5 * (f.call(x0) + f.call(x1)) * dx
95
+ end
96
+ total
97
+
98
+ when :simpson
99
+ dx = (b - a).to_f / n
100
+ sum = f.call(a) + f.call(b)
101
+ (1...n).each do |i|
102
+ x = a + i * dx
103
+ weight = i.odd? ? 4 : 2
104
+ sum += weight * f.call(x)
105
+ end
106
+ (dx / 3) * sum
107
+
108
+ else
109
+ raise ArgumentError, "Method tidak valid. Gunakan :trapezoid atau :simpson"
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,85 @@
1
+ module MyMathGem
2
+ module ComplexNumber
3
+ Complex = Struct.new(:re, :im) do
4
+ TOLERANCE = 1e-12
5
+
6
+ def +(other)
7
+ Complex.new(self.re + other.re, self.im + other.im)
8
+ end
9
+
10
+ def -(other)
11
+ Complex.new(self.re - other.re, self.im - other.im)
12
+ end
13
+
14
+ def -@
15
+ Complex.new(-self.re, -self.im)
16
+ end
17
+
18
+ def *(other)
19
+ Complex.new(
20
+ self.re * other.re - self.im * other.im,
21
+ self.re * other.im + self.im * other.re
22
+ )
23
+ end
24
+
25
+ def /(other)
26
+ denom = other.re**2 + other.im**2
27
+ raise ZeroDivisionError, "Pembagian dengan nol" if denom == 0
28
+ Complex.new(
29
+ (self.re * other.re + self.im * other.im) / denom.to_f,
30
+ (self.im * other.re - self.re * other.im) / denom.to_f
31
+ )
32
+ end
33
+
34
+ def modulus
35
+ Math.sqrt(re**2 + im**2)
36
+ end
37
+
38
+ # Argumen (fase) bilangan kompleks, hasil dalam radian (-π sampai π)
39
+ def arg
40
+ Math.atan2(im, re)
41
+ end
42
+
43
+ def conjugate
44
+ Complex.new(re, -im)
45
+ end
46
+
47
+ # Buat bilangan kompleks dari polar form: magnitude r, angle theta (radian)
48
+ def self.from_polar(r, theta)
49
+ new(r * Math.cos(theta), r * Math.sin(theta))
50
+ end
51
+
52
+ def to_s
53
+ if im >= 0
54
+ "#{re} + #{im}i"
55
+ else
56
+ "#{re} - #{-im}i"
57
+ end
58
+ end
59
+
60
+ def to_a
61
+ [re, im]
62
+ end
63
+
64
+ # Cek kesamaan dengan toleransi floating point
65
+ def ==(other)
66
+ return false unless other.is_a?(Complex)
67
+ (self.re - other.re).abs < TOLERANCE && (self.im - other.im).abs < TOLERANCE
68
+ end
69
+
70
+ # Eksponensial bilangan kompleks (e^(a+bi))
71
+ def exp
72
+ e_to_re = Math.exp(re)
73
+ Complex.new(
74
+ e_to_re * Math.cos(im),
75
+ e_to_re * Math.sin(im)
76
+ )
77
+ end
78
+
79
+ # Logaritma bilangan kompleks (natural log)
80
+ def log
81
+ Complex.new(Math.log(modulus), arg)
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,176 @@
1
+ module MyMathGem
2
+ module DataProcessing
3
+ # Buang nil atau NaN dari data
4
+ def self.clean_data(data)
5
+ raise ArgumentError, "Data harus array" unless data.is_a?(Array)
6
+ data.compact.reject { |x| x.respond_to?(:nan?) && x.nan? }
7
+ end
8
+
9
+ def self.mean(data)
10
+ data = clean_data(data)
11
+ raise ArgumentError, "Data harus tidak kosong" if data.empty?
12
+ data.sum.to_f / data.size
13
+ end
14
+
15
+ def self.weighted_mean(data, weights)
16
+ data = clean_data(data)
17
+ raise ArgumentError, "Data dan weights harus array sama panjang dan tidak kosong" if data.size == 0 || data.size != weights.size
18
+ total_weight = weights.sum.to_f
19
+ raise ArgumentError, "Total bobot tidak boleh nol" if total_weight == 0
20
+ weighted_sum = data.zip(weights).sum { |v, w| v * w }
21
+ weighted_sum / total_weight
22
+ end
23
+
24
+ def self.median(data)
25
+ data = clean_data(data)
26
+ raise ArgumentError, "Data harus tidak kosong" if data.empty?
27
+ sorted = data.sort
28
+ mid = sorted.size / 2
29
+ sorted.size.odd? ? sorted[mid] : (sorted[mid - 1] + sorted[mid]).to_f / 2
30
+ end
31
+
32
+ def self.trimmed_mean(data, trim_ratio=0.1)
33
+ data = clean_data(data)
34
+ raise ArgumentError, "trim_ratio harus antara 0 dan 0.5" unless trim_ratio.is_a?(Numeric) && trim_ratio >= 0 && trim_ratio <= 0.5
35
+ raise ArgumentError, "Data harus cukup besar untuk trimming" if data.size < 2
36
+ sorted = data.sort
37
+ trim_count = (trim_ratio * sorted.size).floor
38
+ trimmed = sorted[trim_count...-trim_count] || []
39
+ raise ArgumentError, "Trimmed data kosong" if trimmed.empty?
40
+ trimmed.sum.to_f / trimmed.size
41
+ end
42
+
43
+ def self.mode(data)
44
+ data = clean_data(data)
45
+ raise ArgumentError, "Data harus tidak kosong" if data.empty?
46
+ freq = data.each_with_object(Hash.new(0)) { |v, h| h[v] += 1 }
47
+ max_freq = freq.values.max
48
+ freq.select { |_, v| v == max_freq }.keys
49
+ end
50
+
51
+ def self.variance(data)
52
+ data = clean_data(data)
53
+ raise ArgumentError, "Data harus minimal 2 elemen" if data.size < 2
54
+ m = mean(data)
55
+ sum_sq = data.sum { |x| (x - m)**2 }
56
+ sum_sq.to_f / (data.size - 1)
57
+ end
58
+
59
+ def self.standard_deviation(data)
60
+ Math.sqrt(variance(data))
61
+ end
62
+
63
+ def self.standard_error_mean(data)
64
+ sd = standard_deviation(data)
65
+ Math.sqrt(sd.to_f / clean_data(data).size)
66
+ end
67
+
68
+ def self.min_max_normalize(data)
69
+ data = clean_data(data)
70
+ raise ArgumentError, "Data harus tidak kosong" if data.empty?
71
+ min = data.min
72
+ max = data.max
73
+ range = max - min
74
+ raise ArgumentError, "Range data 0, normalisasi gagal" if range == 0
75
+ data.map { |x| (x - min).to_f / range }
76
+ end
77
+
78
+ def self.z_score_normalize(data)
79
+ data = clean_data(data)
80
+ raise ArgumentError, "Data harus minimal 2 elemen" if data.size < 2
81
+ m = mean(data)
82
+ sd = standard_deviation(data)
83
+ raise ArgumentError, "Standar deviasi 0, normalisasi gagal" if sd == 0
84
+ data.map { |x| (x - m) / sd.to_f }
85
+ end
86
+
87
+ def self.robust_scale(data)
88
+ # Skala menggunakan median dan IQR
89
+ data = clean_data(data)
90
+ raise ArgumentError, "Data harus minimal 4 elemen" if data.size < 4
91
+ med = median(data)
92
+ q1 = percentile(data.sort, 25)
93
+ q3 = percentile(data.sort, 75)
94
+ iqr = q3 - q1
95
+ raise ArgumentError, "IQR 0, robust scaling gagal" if iqr == 0
96
+ data.map { |x| (x - med).to_f / iqr }
97
+ end
98
+
99
+ def self.moving_average(data, w=3)
100
+ data = clean_data(data)
101
+ raise ArgumentError, "Window harus integer > 0" unless w.is_a?(Integer) && w > 0
102
+ return data if w == 1
103
+ smoothed = []
104
+ data.each_cons(w) { |window| smoothed << (window.sum.to_f / w) }
105
+ smoothed
106
+ end
107
+
108
+ def self.filter_outliers(data)
109
+ data = clean_data(data)
110
+ raise ArgumentError, "Data harus minimal 4 elemen" if data.size < 4
111
+ sorted = data.sort
112
+ q1 = percentile(sorted, 25)
113
+ q3 = percentile(sorted, 75)
114
+ iqr = q3 - q1
115
+ lower_bound = q1 - 1.5 * iqr
116
+ upper_bound = q3 + 1.5 * iqr
117
+ data.select { |x| x >= lower_bound && x <= upper_bound }
118
+ end
119
+
120
+ def self.skewness(data)
121
+ data = clean_data(data)
122
+ n = data.size
123
+ raise ArgumentError, "Data minimal 3 elemen" if n < 3
124
+ m = mean(data)
125
+ sd = standard_deviation(data)
126
+ return 0 if sd == 0
127
+ sum_cubed = data.sum { |x| (x - m)**3 }
128
+ (n.to_f / ((n-1)*(n-2))) * (sum_cubed / (sd**3))
129
+ end
130
+
131
+ def self.kurtosis(data)
132
+ data = clean_data(data)
133
+ n = data.size
134
+ raise ArgumentError, "Data minimal 4 elemen" if n < 4
135
+ m = mean(data)
136
+ sd = standard_deviation(data)
137
+ return 0 if sd == 0
138
+ sum_quad = data.sum { |x| (x - m)**4 }
139
+ numerator = (n*(n+1)*sum_quad) / ((n-1)*(n-2)*(n-3)*(sd**4))
140
+ denominator = (3*((n-1)**2)) / ((n-2)*(n-3))
141
+ numerator - denominator
142
+ end
143
+
144
+ def self.percentile(sorted_data, p)
145
+ raise ArgumentError, "p harus antara 0 dan 100" unless (0..100).include?(p)
146
+ return sorted_data.first if p == 0
147
+ return sorted_data.last if p == 100
148
+
149
+ rank = (p.to_f / 100) * (sorted_data.size - 1)
150
+ lower_idx = rank.floor
151
+ upper_idx = rank.ceil
152
+ if lower_idx == upper_idx
153
+ sorted_data[lower_idx]
154
+ else
155
+ lower_value = sorted_data[lower_idx]
156
+ upper_value = sorted_data[upper_idx]
157
+ fraction = rank - lower_idx
158
+ lower_value + fraction * (upper_value - lower_value)
159
+ end
160
+ end
161
+
162
+ def self.pearson_correlation(x, y)
163
+ x = clean_data(x)
164
+ y = clean_data(y)
165
+ raise ArgumentError, "x dan y harus array sama panjang dan > 1" if x.size <= 1 || x.size != y.size
166
+ mx = mean(x)
167
+ my = mean(y)
168
+ numerator = x.zip(y).sum { |xi, yi| (xi - mx) * (yi - my) }
169
+ denom_x = Math.sqrt(x.sum { |xi| (xi - mx)**2 })
170
+ denom_y = Math.sqrt(y.sum { |yi| (yi - my)**2 })
171
+ denom = denom_x * denom_y
172
+ return 0 if denom == 0
173
+ numerator.to_f / denom
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,115 @@
1
+ module MyMathGem
2
+ module Fourier
3
+ PI2 = 2 * Math::PI
4
+
5
+ # Window functions
6
+ module Window
7
+ def self.hamming(n)
8
+ (0...n).map { |i| 0.54 - 0.46 * Math.cos(PI2 * i / (n - 1)) }
9
+ end
10
+
11
+ def self.hanning(n)
12
+ (0...n).map { |i| 0.5 * (1 - Math.cos(PI2 * i / (n - 1))) }
13
+ end
14
+
15
+ def self.blackman(n)
16
+ (0...n).map { |i| 0.42 - 0.5 * Math.cos(PI2 * i / (n - 1)) + 0.08 * Math.cos(4 * Math::PI * i / (n - 1)) }
17
+ end
18
+ end
19
+
20
+ # Zero-pad input to next power of two length
21
+ def self.zero_pad_to_power_of_two(signal)
22
+ n = signal.size
23
+ target = 1
24
+ target <<= 1 while target < n
25
+ padded = signal.dup
26
+ padded += Array.new(target - n, 0)
27
+ padded
28
+ end
29
+
30
+ # FFT with zero-padding if input size not power of two
31
+ def self.fft(signal)
32
+ n = signal.size
33
+ padded_signal = n & (n - 1) == 0 ? signal : zero_pad_to_power_of_two(signal)
34
+ fft_recursive(padded_signal)
35
+ end
36
+
37
+ def self.fft_recursive(signal)
38
+ n = signal.size
39
+ return [signal[0].is_a?(Complex) ? signal[0] : Complex(signal[0], 0)] if n == 1
40
+
41
+ even = fft_recursive(signal.each_slice(2).map(&:first))
42
+ odd = fft_recursive(signal.each_slice(2).map(&:last))
43
+
44
+ (0...n/2).flat_map do |k|
45
+ t = Complex.polar(1, -PI2 * k / n) * odd[k]
46
+ [even[k] + t, even[k] - t]
47
+ end
48
+ end
49
+
50
+ # IFFT with zero-padding support
51
+ def self.ifft(spectrum)
52
+ n = spectrum.size
53
+ raise ArgumentError, "Spektrum harus array" unless spectrum.is_a?(Array)
54
+ conjugated = spectrum.map(&:conj)
55
+ fft_res = fft_recursive(conjugated)
56
+ fft_res.map { |c| c.conj / n }
57
+ end
58
+
59
+ # Magnitude dari spektrum
60
+ def self.magnitude(spectrum)
61
+ spectrum.map(&:abs)
62
+ end
63
+
64
+ # Fase spektrum (radian)
65
+ def self.phase(spectrum)
66
+ spectrum.map(&:phase)
67
+ end
68
+
69
+ # Power Spectral Density (PSD)
70
+ def self.power_spectral_density(spectrum)
71
+ magnitude(spectrum).map { |mag| mag**2 / spectrum.size }
72
+ end
73
+
74
+ # Filter frekuensi range (indeks)
75
+ def self.filter_frequency(spectrum, low_idx, high_idx)
76
+ n = spectrum.size
77
+ raise ArgumentError, "Indeks frekuensi tidak valid" if low_idx < 0 || high_idx >= n || low_idx > high_idx
78
+ spectrum.each_with_index.map { |v, i| (i >= low_idx && i <= high_idx) ? v : Complex(0, 0) }
79
+ end
80
+
81
+ # Rekonstruksi sinyal real dari spectrum
82
+ def self.reconstruct_signal(spectrum)
83
+ idft_res = ifft(spectrum)
84
+ idft_res.map(&:real)
85
+ end
86
+
87
+ # Spectrogram: bagi sinyal jadi jendela, hitung FFT tiap jendela
88
+ # window_size dan hop_size dalam sample count
89
+ # window_func: symbol :hamming, :hanning, :blackman atau nil (no window)
90
+ # Return array of spectra arrays
91
+ def self.spectrogram(signal, window_size:, hop_size:, window_func: nil)
92
+ window = case window_func
93
+ when :hamming then Window.hamming(window_size)
94
+ when :hanning then Window.hanning(window_size)
95
+ when :blackman then Window.blackman(window_size)
96
+ else Array.new(window_size, 1.0) # no window
97
+ end
98
+
99
+ spectrogram = []
100
+ (0..signal.size - window_size).step(hop_size) do |start_idx|
101
+ segment = signal[start_idx, window_size]
102
+ windowed = segment.zip(window).map { |v, w| v * w }
103
+ spectrum = fft(windowed)
104
+ spectrogram << spectrum
105
+ end
106
+ spectrogram
107
+ end
108
+
109
+ # Konversi indeks spektrum ke frekuensi (Hz)
110
+ # sample_rate dalam Hz
111
+ def self.index_to_frequency(index, spectrum_size, sample_rate)
112
+ index * sample_rate.to_f / spectrum_size
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,126 @@
1
+ module MyMathGem
2
+ module LinearAlgebra
3
+ # Validasi matriks (array of arrays) persegi
4
+ def self.square_matrix?(m)
5
+ return false unless m.is_a?(Array) && !m.empty?
6
+ n = m.length
7
+ m.all? { |row| row.is_a?(Array) && row.length == n }
8
+ end
9
+
10
+ # Dot product dua vektor (array angka)
11
+ def self.dot(v1, v2)
12
+ raise ArgumentError, "Vektor harus sama panjang" unless v1.length == v2.length
13
+ v1.zip(v2).map { |a, b| a * b }.sum
14
+ end
15
+
16
+ # Norm (magnitude) vektor
17
+ def self.norm(v)
18
+ Math.sqrt(v.map { |x| x**2 }.sum)
19
+ end
20
+
21
+ # Normalisasi vektor (unit vector)
22
+ def self.normalize(v)
23
+ n = norm(v)
24
+ raise ArgumentError, "Vektor nol tidak dapat dinormalisasi" if n == 0
25
+ v.map { |x| x.to_f / n }
26
+ end
27
+
28
+ # Cross product dua vektor 3D
29
+ def self.cross(v1, v2)
30
+ raise ArgumentError, "Cross product hanya untuk vektor 3 dimensi" unless v1.length == 3 && v2.length == 3
31
+ [
32
+ v1[1]*v2[2] - v1[2]*v2[1],
33
+ v1[2]*v2[0] - v1[0]*v2[2],
34
+ v1[0]*v2[1] - v1[1]*v2[0]
35
+ ]
36
+ end
37
+
38
+ # Perkalian matriks (mxn) dengan matriks (nxp) atau vektor (n)
39
+ def self.matrix_multiply(m1, m2)
40
+ # Jika m2 adalah vektor
41
+ if m2.is_a?(Array) && !m2.empty? && !m2[0].is_a?(Array)
42
+ raise ArgumentError, "Jumlah kolom m1 harus sama dengan panjang vektor m2" unless m1[0].length == m2.length
43
+
44
+ m1.map do |row|
45
+ dot(row, m2)
46
+ end
47
+ else
48
+ raise ArgumentError, "Jumlah kolom m1 harus sama dengan jumlah baris m2" unless m1[0].length == m2.length
49
+
50
+ result = Array.new(m1.length) { Array.new(m2[0].length, 0) }
51
+ m1.length.times do |i|
52
+ m2[0].length.times do |j|
53
+ sum = 0
54
+ m2.length.times do |k|
55
+ sum += m1[i][k] * m2[k][j]
56
+ end
57
+ result[i][j] = sum
58
+ end
59
+ end
60
+ result
61
+ end
62
+ end
63
+
64
+ # Transpose matriks
65
+ def self.transpose(m)
66
+ raise ArgumentError, "Input harus matriks (array of arrays)" unless m.is_a?(Array) && m.all? { |row| row.is_a?(Array) }
67
+ m[0].length.times.map { |i| m.map { |row| row[i] } }
68
+ end
69
+
70
+ # Determinan matriks 2x2
71
+ def self.determinant_2x2(m)
72
+ raise ArgumentError, "Matriks harus 2x2" unless m.length == 2 && m.all? { |row| row.length == 2 }
73
+ m[0][0]*m[1][1] - m[0][1]*m[1][0]
74
+ end
75
+
76
+ # Determinan matriks 3x3
77
+ def self.determinant_3x3(m)
78
+ raise ArgumentError, "Matriks harus 3x3" unless m.length == 3 && m.all? { |row| row.length == 3 }
79
+ a, b, c = m[0]
80
+ d, e, f = m[1]
81
+ g, h, i = m[2]
82
+ a*(e*i - f*h) - b*(d*i - f*g) + c*(d*h - e*g)
83
+ end
84
+
85
+ # Invers matriks 2x2
86
+ def self.inverse_2x2(m)
87
+ det = determinant_2x2(m)
88
+ raise ArgumentError, "Matriks singular, determinan = 0" if det == 0
89
+ inv_det = 1.0 / det
90
+ [
91
+ [ m[1][1]*inv_det, -m[0][1]*inv_det ],
92
+ [ -m[1][0]*inv_det, m[0][0]*inv_det ]
93
+ ]
94
+ end
95
+
96
+ # Invers matriks 3x3 (menggunakan metode kofaktor)
97
+ def self.inverse_3x3(m)
98
+ det = determinant_3x3(m)
99
+ raise ArgumentError, "Matriks singular, determinan = 0" if det == 0
100
+ inv_det = 1.0 / det
101
+
102
+ cofactors = Array.new(3) { Array.new(3, 0) }
103
+
104
+ # Hitung kofaktor untuk tiap elemen
105
+ (0..2).each do |row|
106
+ (0..2).each do |col|
107
+ minor = minor_2x2(m, row, col)
108
+ cofactors[row][col] = ((row + col).even? ? 1 : -1) * determinant_2x2(minor)
109
+ end
110
+ end
111
+
112
+ # Transpose kofaktor untuk dapat adjoin matrix
113
+ adjugate = transpose(cofactors)
114
+
115
+ # Kalikan dengan 1/det
116
+ adjugate.map { |row| row.map { |val| val * inv_det } }
117
+ end
118
+
119
+ # Helper: matriks minor 2x2 dengan menghilangkan baris row_to_remove dan kolom col_to_remove
120
+ def self.minor_2x2(m, row_to_remove, col_to_remove)
121
+ m.each_with_index
122
+ .reject { |_, r| r == row_to_remove }
123
+ .map { |row, _| row.each_with_index.reject { |_, c| c == col_to_remove }.map(&:first) }
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,142 @@
1
+ module MyMathGem
2
+ module ODESolver
3
+ # Metode Euler untuk ODE y' = f(t, y)
4
+ def self.euler(t0:, y0:, t_end:, steps:, f:, callback: nil, return_separate: false)
5
+ validate_params!(t0, y0, t_end, steps, f)
6
+
7
+ dt = (t_end - t0).to_f / steps
8
+ ts = [t0]
9
+ ys = [deep_copy(y0)]
10
+
11
+ steps.times do |i|
12
+ t, y = ts[-1], ys[-1]
13
+ dy = f.call(t, y)
14
+ y_next = vector_add(y, scalar_multiply(dy, dt))
15
+ t_next = t + dt
16
+
17
+ ts << t_next
18
+ ys << y_next
19
+
20
+ callback.call(t_next, y_next) if callback.is_a?(Proc)
21
+ end
22
+
23
+ return_separate ? { times: ts, values: ys } : ts.zip(ys)
24
+ end
25
+
26
+ # Metode Runge-Kutta orde 4 (RK4)
27
+ def self.runge_kutta4(t0:, y0:, t_end:, steps:, f:, callback: nil, return_separate: false)
28
+ validate_params!(t0, y0, t_end, steps, f)
29
+
30
+ dt = (t_end - t0).to_f / steps
31
+ ts = [t0]
32
+ ys = [deep_copy(y0)]
33
+
34
+ steps.times do
35
+ t, y = ts[-1], ys[-1]
36
+
37
+ k1 = f.call(t, y)
38
+ k2 = f.call(t + dt/2.0, vector_add(y, scalar_multiply(k1, dt/2.0)))
39
+ k3 = f.call(t + dt/2.0, vector_add(y, scalar_multiply(k2, dt/2.0)))
40
+ k4 = f.call(t + dt, vector_add(y, scalar_multiply(k3, dt)))
41
+
42
+ increment = scalar_multiply(
43
+ vector_add(
44
+ vector_add(k1, scalar_multiply(k2, 2)),
45
+ vector_add(scalar_multiply(k3, 2), k4)
46
+ ),
47
+ dt / 6.0
48
+ )
49
+ y_next = vector_add(y, increment)
50
+ t_next = t + dt
51
+
52
+ ts << t_next
53
+ ys << y_next
54
+
55
+ callback.call(t_next, y_next) if callback.is_a?(Proc)
56
+ end
57
+
58
+ return_separate ? { times: ts, values: ys } : ts.zip(ys)
59
+ end
60
+
61
+ # Metode Heun (Improved Euler)
62
+ def self.heun(t0:, y0:, t_end:, steps:, f:, callback: nil, return_separate: false)
63
+ validate_params!(t0, y0, t_end, steps, f)
64
+
65
+ dt = (t_end - t0).to_f / steps
66
+ ts = [t0]
67
+ ys = [deep_copy(y0)]
68
+
69
+ steps.times do
70
+ t, y = ts[-1], ys[-1]
71
+
72
+ k1 = f.call(t, y)
73
+ y_predict = vector_add(y, scalar_multiply(k1, dt))
74
+ k2 = f.call(t + dt, y_predict)
75
+ increment = scalar_multiply(vector_add(k1, k2), dt / 2.0)
76
+ y_next = vector_add(y, increment)
77
+ t_next = t + dt
78
+
79
+ ts << t_next
80
+ ys << y_next
81
+
82
+ callback.call(t_next, y_next) if callback.is_a?(Proc)
83
+ end
84
+
85
+ return_separate ? { times: ts, values: ys } : ts.zip(ys)
86
+ end
87
+
88
+ private
89
+
90
+ def self.validate_params!(t0, y0, t_end, steps, f)
91
+ raise ArgumentError, "steps harus integer positif" unless steps.is_a?(Integer) && steps > 0
92
+ raise ArgumentError, "t_end harus > t0" unless t_end > t0
93
+ raise ArgumentError, "f harus callable" unless f.respond_to?(:call)
94
+ raise ArgumentError, "y0 harus scalar, array, atau nested array" unless valid_y0?(y0)
95
+ end
96
+
97
+ def self.valid_y0?(y0)
98
+ scalar?(y0) || vector?(y0) || matrix?(y0)
99
+ end
100
+
101
+ def self.scalar?(x)
102
+ x.is_a?(Numeric)
103
+ end
104
+
105
+ def self.vector?(v)
106
+ v.is_a?(Array) && v.all? { |e| scalar?(e) }
107
+ end
108
+
109
+ def self.matrix?(m)
110
+ m.is_a?(Array) && m.all? { |row| vector?(row) }
111
+ end
112
+
113
+ def self.vector_add(v1, v2)
114
+ if scalar?(v1) && scalar?(v2)
115
+ v1 + v2
116
+ elsif vector?(v1) && vector?(v2) && v1.length == v2.length
117
+ v1.zip(v2).map { |a,b| a + b }
118
+ elsif matrix?(v1) && matrix?(v2) && v1.length == v2.length
119
+ v1.zip(v2).map { |row1, row2| vector_add(row1, row2) }
120
+ else
121
+ raise ArgumentError, "v1 dan v2 harus tipe yang sama dan ukuran cocok"
122
+ end
123
+ end
124
+
125
+ def self.scalar_multiply(v, scalar)
126
+ if scalar?(v)
127
+ v * scalar
128
+ elsif vector?(v)
129
+ v.map { |x| x * scalar }
130
+ elsif matrix?(v)
131
+ v.map { |row| scalar_multiply(row, scalar) }
132
+ else
133
+ raise ArgumentError, "v harus scalar, vector, atau matrix"
134
+ end
135
+ end
136
+
137
+ # Deep copy nilai y0 supaya aman dari modifikasi luar
138
+ def self.deep_copy(obj)
139
+ Marshal.load(Marshal.dump(obj))
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,162 @@
1
+ module MyMathGem
2
+ module Optimization
3
+ class Optimizer
4
+ attr_reader :x, :fx, :history
5
+
6
+ def initialize(f:, grad: nil, x_init:, learning_rate: 0.01, max_iter: 1000, tol: 1e-6,
7
+ method: :gd, momentum: 0.9, beta1: 0.9, beta2: 0.999, epsilon: 1e-8,
8
+ lr_decay: nil, decay_rate: 0.99, verbose: false, early_stopping: true)
9
+ @f = f
10
+ @grad = grad
11
+ @x = x_init.dup
12
+ @learning_rate = learning_rate.to_f
13
+ @max_iter = max_iter
14
+ @tol = tol
15
+ @method = method
16
+ @momentum = momentum
17
+ @beta1 = beta1
18
+ @beta2 = beta2
19
+ @epsilon = epsilon
20
+ @lr_decay = lr_decay # :linear, :exponential, or nil
21
+ @decay_rate = decay_rate
22
+ @verbose = verbose
23
+ @early_stopping = early_stopping
24
+
25
+ @v = Array.new(@x.size, 0.0) # for momentum
26
+ @m = Array.new(@x.size, 0.0) # for Adam
27
+ @s = Array.new(@x.size, 0.0) # for Adam
28
+ @t = 0 # timestep for Adam
29
+
30
+ @grad ||= ->(x) { numerical_gradient(@f, x) }
31
+
32
+ @history = []
33
+ end
34
+
35
+ def optimize
36
+ prev_fx = nil
37
+
38
+ @max_iter.times do |i|
39
+ @t += 1
40
+ g = @grad.call(@x)
41
+ raise ArgumentError, "Gradien harus array dengan panjang sama seperti x" unless valid_gradient?(g)
42
+
43
+ case @method
44
+ when :gd
45
+ step = scalar_multiply(g, @learning_rate)
46
+ @x = vector_subtract(@x, step)
47
+ when :momentum
48
+ @v = vector_add(scalar_multiply(@v, @momentum), scalar_multiply(g, 1 - @momentum))
49
+ step = scalar_multiply(@v, @learning_rate)
50
+ @x = vector_subtract(@x, step)
51
+ when :adam
52
+ update_adam(g)
53
+ else
54
+ raise ArgumentError, "Metode optimasi tidak dikenal: #{@method}"
55
+ end
56
+
57
+ @fx = @f.call(@x)
58
+ @history << { iteration: i + 1, x: @x.dup, fx: @fx }
59
+
60
+ if @verbose
61
+ puts "Iterasi #{i+1}: f(x) = #{@fx}, ||grad|| = #{vector_norm(g)}"
62
+ end
63
+
64
+ if prev_fx && @early_stopping
65
+ change = (prev_fx - @fx).abs
66
+ break if change < @tol
67
+ end
68
+ prev_fx = @fx
69
+
70
+ apply_lr_decay(i)
71
+ end
72
+
73
+ @x
74
+ end
75
+
76
+ private
77
+
78
+ def update_adam(g)
79
+ @m = vector_add(scalar_multiply(@m, @beta1), scalar_multiply(g, 1 - @beta1))
80
+ @s = vector_add(scalar_multiply(@s, @beta2), scalar_multiply(elementwise_square(g), 1 - @beta2))
81
+
82
+ m_hat = scalar_multiply(@m, 1.0 / (1 - @beta1**@t))
83
+ s_hat = scalar_multiply(@s, 1.0 / (1 - @beta2**@t))
84
+
85
+ update = m_hat.zip(s_hat).map do |mi, si|
86
+ mi / (Math.sqrt(si) + @epsilon)
87
+ end
88
+
89
+ step = scalar_multiply(update, @learning_rate)
90
+ @x = vector_subtract(@x, step)
91
+ end
92
+
93
+ def apply_lr_decay(iter)
94
+ return unless @lr_decay
95
+ case @lr_decay
96
+ when :linear
97
+ @learning_rate = [@learning_rate - @decay_rate * iter, 0].max
98
+ when :exponential
99
+ @learning_rate *= @decay_rate
100
+ else
101
+ # no decay
102
+ end
103
+ end
104
+
105
+ def numerical_gradient(f, x, h=1e-8)
106
+ grad = Array.new(x.size, 0.0)
107
+ x_copy = x.dup
108
+ x.size.times do |i|
109
+ x_copy[i] = x[i] + h
110
+ fxh1 = f.call(x_copy)
111
+ x_copy[i] = x[i] - h
112
+ fxh2 = f.call(x_copy)
113
+ grad[i] = (fxh1 - fxh2) / (2 * h)
114
+ x_copy[i] = x[i]
115
+ end
116
+ grad
117
+ end
118
+
119
+ def vector_subtract(v1, v2)
120
+ raise ArgumentError, "Vektor harus sama panjang" unless v1.size == v2.size
121
+ v1.zip(v2).map { |a, b| a - b }
122
+ end
123
+
124
+ def vector_add(v1, v2)
125
+ raise ArgumentError, "Vektor harus sama panjang" unless v1.size == v2.size
126
+ v1.zip(v2).map { |a, b| a + b }
127
+ end
128
+
129
+ def scalar_multiply(v, scalar)
130
+ v.map { |x| x * scalar }
131
+ end
132
+
133
+ def elementwise_square(v)
134
+ v.map { |x| x**2 }
135
+ end
136
+
137
+ def vector_norm(v)
138
+ Math.sqrt(v.map { |x| x**2 }.sum)
139
+ end
140
+
141
+ def valid_gradient?(g)
142
+ g.is_a?(Array) && g.size == @x.size && g.all? { |v| v.is_a?(Numeric) }
143
+ end
144
+ end
145
+
146
+ # Helper method singkat untuk pengguna yg ingin cepat pakai GD tanpa class
147
+ def self.gradient_descent(**kwargs)
148
+ opt = Optimizer.new(**kwargs.merge(method: :gd))
149
+ opt.optimize
150
+ end
151
+
152
+ def self.gradient_descent_momentum(**kwargs)
153
+ opt = Optimizer.new(**kwargs.merge(method: :momentum))
154
+ opt.optimize
155
+ end
156
+
157
+ def self.adam(**kwargs)
158
+ opt = Optimizer.new(**kwargs.merge(method: :adam))
159
+ opt.optimize
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,298 @@
1
+ module MyMathGem
2
+ module Statistics
3
+ # Mean (rata-rata)
4
+ def self.mean(data)
5
+ data = validate_numeric_array(data)
6
+ data.sum.to_f / data.size
7
+ end
8
+
9
+ # Median
10
+ def self.median(data)
11
+ data = validate_numeric_array(data)
12
+ sorted = data.sort
13
+ mid = sorted.size / 2
14
+ sorted.size.odd? ? sorted[mid] : (sorted[mid - 1] + sorted[mid]).to_f / 2
15
+ end
16
+
17
+ # Mode (bisa banyak)
18
+ def self.mode(data)
19
+ data = validate_numeric_array(data)
20
+ freq = data.each_with_object(Hash.new(0)) { |v, h| h[v] += 1 }
21
+ max_freq = freq.values.max
22
+ freq.select { |_, v| v == max_freq }.keys
23
+ end
24
+
25
+ # Varians sampel
26
+ def self.variance(data)
27
+ data = validate_numeric_array(data)
28
+ raise ArgumentError, "Data harus >= 2 elemen" if data.size < 2
29
+ m = mean(data)
30
+ sum_sq = data.sum { |x| (x - m)**2 }
31
+ sum_sq.to_f / (data.size - 1)
32
+ end
33
+
34
+ # Standar deviasi sampel
35
+ def self.standard_deviation(data)
36
+ Math.sqrt(variance(data))
37
+ end
38
+
39
+ # Quantile (persentil p 0..1)
40
+ def self.quantile(data, p)
41
+ data = validate_numeric_array(data)
42
+ raise ArgumentError, "p harus antara 0 dan 1" unless p.is_a?(Numeric) && p.between?(0, 1)
43
+ sorted = data.sort
44
+ rank = p * (sorted.size - 1)
45
+ lower_idx = rank.floor
46
+ upper_idx = rank.ceil
47
+ if lower_idx == upper_idx
48
+ sorted[lower_idx]
49
+ else
50
+ lower_value = sorted[lower_idx]
51
+ upper_value = sorted[upper_idx]
52
+ lower_value + (rank - lower_idx) * (upper_value - lower_value)
53
+ end
54
+ end
55
+
56
+ # Skewness (kemiringan)
57
+ def self.skewness(data)
58
+ data = validate_numeric_array(data)
59
+ raise ArgumentError, "Data harus >= 3 elemen" if data.size < 3
60
+ m = mean(data)
61
+ sd = standard_deviation(data)
62
+ return 0 if sd == 0
63
+ n = data.size
64
+ sum_cubed = data.sum { |x| (x - m)**3 }
65
+ (n.to_f / ((n-1)*(n-2))) * (sum_cubed / (sd**3))
66
+ end
67
+
68
+ # Kurtosis (keruncingan)
69
+ def self.kurtosis(data)
70
+ data = validate_numeric_array(data)
71
+ raise ArgumentError, "Data harus >= 4 elemen" if data.size < 4
72
+ m = mean(data)
73
+ sd = standard_deviation(data)
74
+ return 0 if sd == 0
75
+ n = data.size
76
+ sum_quad = data.sum { |x| (x - m)**4 }
77
+ numerator = (n*(n+1)*sum_quad) / ((n-1)*(n-2)*(n-3)*(sd**4))
78
+ denominator = (3*((n-1)**2)) / ((n-2)*(n-3))
79
+ numerator - denominator
80
+ end
81
+
82
+ # Pearson correlation
83
+ def self.pearson_correlation(x, y)
84
+ x, y = validate_numeric_array(x), validate_numeric_array(y)
85
+ raise ArgumentError, "x dan y harus sama panjang >= 2" unless x.size == y.size && x.size >= 2
86
+ mx, my = mean(x), mean(y)
87
+ numerator = x.zip(y).sum { |xi, yi| (xi - mx) * (yi - my) }
88
+ denom_x = Math.sqrt(x.sum { |xi| (xi - mx)**2 })
89
+ denom_y = Math.sqrt(y.sum { |yi| (yi - my)**2 })
90
+ denom = denom_x * denom_y
91
+ return 0 if denom == 0
92
+ numerator.to_f / denom
93
+ end
94
+
95
+ # Spearman rank correlation
96
+ def self.spearman_correlation(x, y)
97
+ x, y = validate_numeric_array(x), validate_numeric_array(y)
98
+ raise ArgumentError, "x dan y harus sama panjang >= 2" unless x.size == y.size && x.size >= 2
99
+ rank_x = rank_array(x)
100
+ rank_y = rank_array(y)
101
+ pearson_correlation(rank_x, rank_y)
102
+ end
103
+
104
+ # Covariance sample
105
+ def self.covariance(x, y)
106
+ x, y = validate_numeric_array(x), validate_numeric_array(y)
107
+ raise ArgumentError, "x dan y harus sama panjang >= 2" unless x.size == y.size && x.size >= 2
108
+ mx, my = mean(x), mean(y)
109
+ sum = x.zip(y).sum { |xi, yi| (xi - mx) * (yi - my) }
110
+ sum.to_f / (x.size - 1)
111
+ end
112
+
113
+ # Z-score untuk data
114
+ def self.z_scores(data)
115
+ data = validate_numeric_array(data)
116
+ m = mean(data)
117
+ sd = standard_deviation(data)
118
+ raise ArgumentError, "Stddev 0, z-score tidak dapat dihitung" if sd == 0
119
+ data.map { |x| (x - m) / sd }
120
+ end
121
+
122
+ # Confidence interval untuk mean (default distribusi normal)
123
+ # level contoh 0.95 untuk 95% CI
124
+ def self.confidence_interval_mean(data, level = 0.95)
125
+ data = validate_numeric_array(data)
126
+ n = data.size
127
+ raise ArgumentError, "Data harus >= 2 elemen" if n < 2
128
+ m = mean(data)
129
+ sd = standard_deviation(data)
130
+ alpha = 1 - level
131
+ # t-distribution critical value (approximation)
132
+ t_crit = t_distribution_critical_value(n - 1, alpha / 2)
133
+ margin = t_crit * sd / Math.sqrt(n)
134
+ [m - margin, m + margin]
135
+ end
136
+
137
+ # t-distribution critical value (two-tailed), menggunakan inverse CDF aproksimasi
138
+ # Implementasi sederhana menggunakan inverse normal approximation (baik untuk n>30)
139
+ def self.t_distribution_critical_value(df, alpha)
140
+ # gunakan distribusi normal z untuk aproksimasi sederhana (lebih kompleks bisa pakai gem statistik)
141
+ # untuk df>30 pendekatan z cukup baik
142
+ z = inverse_normal_cdf(1 - alpha)
143
+ z
144
+ end
145
+
146
+ # Inverse normal CDF aproksimasi (probit), menggunakan algoritma Peter John Acklam
147
+ def self.inverse_normal_cdf(p)
148
+ raise ArgumentError, "p harus di (0,1)" unless p > 0 && p < 1
149
+ # koefisien dan algoritma Acklam (singkat)
150
+ a1 = -39.6968302866538; a2 = 220.946098424521; a3 = -275.928510446969
151
+ a4 = 138.357751867269; a5 = -30.6647980661472; a6 = 2.50662827745924
152
+
153
+ b1 = -54.4760987982241; b2 = 161.585836858041; b3 = -155.698979859887
154
+ b4 = 66.8013118877197; b5 = -13.2806815528857
155
+
156
+ c1 = -0.00778489400243029; c2 = -0.322396458041136; c3 = -2.40075827716184
157
+ c4 = -2.54973253934373; c5 = 4.37466414146497; c6 = 2.93816398269878
158
+
159
+ d1 = 0.00778469570904146; d2 = 0.32246712907004; d3 = 2.445134137143
160
+ d4 = 3.75440866190742
161
+
162
+ plow = 0.02425
163
+ phigh = 1 - plow
164
+
165
+ if p < plow
166
+ q = Math.sqrt(-2 * Math.log(p))
167
+ (((((c1 * q + c2) * q + c3) * q + c4) * q + c5) * q + c6) /
168
+ ((((d1 * q + d2) * q + d3) * q + d4) * q + 1)
169
+ elsif p > phigh
170
+ q = Math.sqrt(-2 * Math.log(1 - p))
171
+ -(((((c1 * q + c2) * q + c3) * q + c4) * q + c5) * q + c6) /
172
+ ((((d1 * q + d2) * q + d3) * q + d4) * q + 1)
173
+ else
174
+ q = p - 0.5
175
+ r = q * q
176
+ (((((a1 * r + a2) * r + a3) * r + a4) * r + a5) * r + a6) * q /
177
+ (((((b1 * r + b2) * r + b3) * r + b4) * r + b5) * r + 1)
178
+ end
179
+ end
180
+
181
+ # One sample t-test: apakah mean data berbeda signifikan dari nilai mu0
182
+ # Return nilai t-statistik dan p-value (two-tailed)
183
+ def self.t_test_one_sample(data, mu0)
184
+ data = validate_numeric_array(data)
185
+ n = data.size
186
+ raise ArgumentError, "Data harus >= 2 elemen" if n < 2
187
+ m = mean(data)
188
+ sd = standard_deviation(data)
189
+ t_stat = (m - mu0) / (sd / Math.sqrt(n))
190
+ p_val = 2 * (1 - t_cdf(t_stat.abs, n - 1))
191
+ [t_stat, p_val]
192
+ end
193
+
194
+ # Two sample t-test (independen), equal variance assumed
195
+ def self.t_test_two_sample(x, y)
196
+ x, y = validate_numeric_array(x), validate_numeric_array(y)
197
+ nx, ny = x.size, y.size
198
+ raise ArgumentError, "Data harus >= 2 elemen" if nx < 2 || ny < 2
199
+ mx, my = mean(x), mean(y)
200
+ varx, vary = variance(x), variance(y)
201
+ pooled_var = ((nx -1) * varx + (ny - 1) * vary) / (nx + ny - 2)
202
+ se = Math.sqrt(pooled_var * (1.0/nx + 1.0/ny))
203
+ t_stat = (mx - my) / se
204
+ df = nx + ny - 2
205
+ p_val = 2 * (1 - t_cdf(t_stat.abs, df))
206
+ [t_stat, p_val]
207
+ end
208
+
209
+ # Mann-Whitney U test (Wilcoxon rank sum) non-parametrik dua sampel
210
+ def self.mann_whitney_u_test(x, y)
211
+ x, y = validate_numeric_array(x), validate_numeric_array(y)
212
+ nx, ny = x.size, y.size
213
+ combined = x.zip(Array.new(nx, :x)) + y.zip(Array.new(ny, :y))
214
+ sorted = combined.sort_by(&:first)
215
+ ranks = rank_array(sorted.map(&:first))
216
+ rank_x = []
217
+ rank_y = []
218
+ sorted.each_with_index do |(_, label), i|
219
+ (label == :x ? rank_x : rank_y) << ranks[i]
220
+ end
221
+ ux = nx * ny + (nx * (nx + 1)) / 2.0 - rank_x.sum
222
+ uy = nx * ny - ux
223
+ u = [ux, uy].min
224
+ # Approximate p-value for large samples (normal approx)
225
+ mu = nx * ny / 2.0
226
+ sigma = Math.sqrt(nx * ny * (nx + ny + 1) / 12.0)
227
+ z = (u - mu) / sigma
228
+ p_val = 2 * (1 - normal_cdf(z.abs))
229
+ [u, p_val]
230
+ end
231
+
232
+ # Simple linear regression y = a + bx
233
+ # Return hash {intercept: a, slope: b, r_squared: r2}
234
+ def self.linear_regression(x, y)
235
+ x, y = validate_numeric_array(x), validate_numeric_array(y)
236
+ raise ArgumentError, "x dan y harus sama panjang >= 2" unless x.size == y.size && x.size >= 2
237
+ mx, my = mean(x), mean(y)
238
+ cov = covariance(x, y)
239
+ varx = variance(x)
240
+ slope = cov / varx
241
+ intercept = my - slope * mx
242
+
243
+ # R squared
244
+ ss_tot = y.sum { |yi| (yi - my)**2 }
245
+ ss_res = y.zip(x).sum { |yi, xi| (yi - (intercept + slope * xi))**2 }
246
+ r_squared = 1 - ss_res.to_f / ss_tot
247
+
248
+ { intercept: intercept, slope: slope, r_squared: r_squared }
249
+ end
250
+
251
+ private
252
+
253
+ def self.validate_numeric_array(data)
254
+ raise ArgumentError, "Data harus array tidak kosong" unless data.is_a?(Array) && !data.empty?
255
+ numeric_data = data.map do |v|
256
+ if v.is_a?(Numeric)
257
+ v.to_f
258
+ else
259
+ raise ArgumentError, "Semua elemen data harus numerik"
260
+ end
261
+ end
262
+ numeric_data
263
+ end
264
+
265
+ def self.rank_array(data)
266
+ sorted = data.each_with_index.sort_by(&:first)
267
+ ranks = Array.new(data.size)
268
+ i = 0
269
+ while i < sorted.size
270
+ j = i
271
+ while j + 1 < sorted.size && sorted[j+1][0] == sorted[i][0]
272
+ j += 1
273
+ end
274
+ avg_rank = (i + j + 2) / 2.0
275
+ (i..j).each { |k| ranks[sorted[k][1]] = avg_rank }
276
+ i = j + 1
277
+ end
278
+ ranks
279
+ end
280
+
281
+ def self.normal_cdf(z)
282
+ 0.5 * (1 + erf(z / Math.sqrt(2)))
283
+ end
284
+
285
+ def self.erf(x)
286
+ t = 1.0 / (1.0 + 0.3275911 * x.abs)
287
+ a1, a2, a3, a4, a5 = 0.254829592, -0.284496736, 1.421413741, -1.453152027, 1.061405429
288
+ ans = 1 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-x*x)
289
+ x >= 0 ? ans : -ans
290
+ end
291
+
292
+ # t distribution CDF (gunakan normal approx untuk simplifikasi)
293
+ def self.t_cdf(t, df)
294
+ # bisa diganti dengan implementasi distribusi t yg lebih akurat
295
+ normal_cdf(t)
296
+ end
297
+ end
298
+ end
@@ -0,0 +1,3 @@
1
+ module MyMathGem
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,23 @@
1
+ # lib/my_math_gem.rb
2
+
3
+ # Versi gem
4
+ require_relative 'my_math_gem/version'
5
+
6
+ module MyMathGem
7
+ # Autoload modul-modul agar dimuat saat pertama kali digunakan (lazy loading)
8
+ autoload :Calculus, 'my_math_gem/calculus'
9
+ autoload :LinearAlgebra, 'my_math_gem/linear_algebra'
10
+ autoload :Statistics, 'my_math_gem/statistics'
11
+ autoload :OdeSolver, 'my_math_gem/ode_solver'
12
+ autoload :Optimization, 'my_math_gem/optimization'
13
+ autoload :ComplexNumber, 'my_math_gem/complex_number'
14
+ autoload :Fourier, 'my_math_gem/fourier'
15
+ autoload :DataProcessing, 'my_math_gem/data_processing'
16
+
17
+ # Modul utama yang berisi kumpulan fungsi analisa matematika
18
+ #
19
+ # Contoh pemakaian:
20
+ # MyMathGem::Calculus.integrate(...)
21
+ # MyMathGem::Statistics.mean([...])
22
+ # MyMathGem::Fourier.fft([...])
23
+ end
@@ -0,0 +1,30 @@
1
+ # my_math_gem.gemspec
2
+
3
+ require_relative 'lib/my_math_gem/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "my_math_gem"
7
+ spec.version = MyMathGem::VERSION
8
+ spec.authors = ["Andri"]
9
+ spec.email = ["ikuide@gmail.com"]
10
+ spec.summary = "Gem untuk analisa matematika lengkap"
11
+ spec.description = <<-DESC
12
+ MyMathGem adalah kumpulan modul Ruby untuk analisa matematika,
13
+ termasuk kalkulus, aljabar linier, statistik, pemrosesan data,
14
+ solusi ODE, optimasi, bilangan kompleks, dan transformasi Fourier.
15
+ DESC
16
+
17
+ spec.homepage = "" # Kosongkan jika tidak punya homepage
18
+ spec.license = "MIT"
19
+
20
+ # File-file yang disertakan di dalam gem
21
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
+ Dir.glob("lib/**/*.rb") + ["README.md", "LICENSE", "my_math_gem.gemspec"]
23
+ end
24
+
25
+ spec.require_paths = ["lib"]
26
+ spec.required_ruby_version = ">= 2.5"
27
+
28
+ # Metadata bisa dikosongkan jika tidak punya homepage
29
+ spec.metadata = {}
30
+ end
metadata ADDED
@@ -0,0 +1,56 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: my_math_gem
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Andri
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: |
13
+ MyMathGem adalah kumpulan modul Ruby untuk analisa matematika,
14
+ termasuk kalkulus, aljabar linier, statistik, pemrosesan data,
15
+ solusi ODE, optimasi, bilangan kompleks, dan transformasi Fourier.
16
+ email:
17
+ - ikuide@gmail.com
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - LICENSE
23
+ - README.md
24
+ - lib/my_math_gem.rb
25
+ - lib/my_math_gem/calculus.rb
26
+ - lib/my_math_gem/complex_number.rb
27
+ - lib/my_math_gem/data_processing.rb
28
+ - lib/my_math_gem/fourier.rb
29
+ - lib/my_math_gem/linear_algebra.rb
30
+ - lib/my_math_gem/ode_solver.rb
31
+ - lib/my_math_gem/optimization.rb
32
+ - lib/my_math_gem/statistics.rb
33
+ - lib/my_math_gem/version.rb
34
+ - my_math_gem.gemspec
35
+ homepage: ''
36
+ licenses:
37
+ - MIT
38
+ metadata: {}
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '2.5'
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubygems_version: 3.6.7
54
+ specification_version: 4
55
+ summary: Gem untuk analisa matematika lengkap
56
+ test_files: []