more equastion refactoring
This commit is contained in:
@@ -2,33 +2,42 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Stats
|
||||
class AxisRange < T::ImmutableStruct
|
||||
class Axis
|
||||
extend T::Sig
|
||||
|
||||
const :min, Float
|
||||
const :max, Float
|
||||
extend T::Helpers
|
||||
abstract!
|
||||
|
||||
sig { returns(Float) }
|
||||
attr_reader :min
|
||||
|
||||
sig { returns(Float) }
|
||||
attr_reader :max
|
||||
|
||||
sig(:final) { params(min: Float, max: Float).void }
|
||||
def initialize(min:, max:)
|
||||
@min = min
|
||||
@max = max
|
||||
end
|
||||
|
||||
sig(:final) { returns(Float) }
|
||||
def scale
|
||||
max - min
|
||||
end
|
||||
|
||||
sig { returns(T::Range[Float]) }
|
||||
sig(:final) { returns(T::Range[Float]) }
|
||||
def range
|
||||
min..max
|
||||
end
|
||||
|
||||
sig { params(value: Float).returns(Float) }
|
||||
sig { abstract.params(value: Float).returns(Float) }
|
||||
def normalize(value)
|
||||
(value - min) / scale
|
||||
end
|
||||
|
||||
sig { params(value: Float).returns(Float) }
|
||||
sig { abstract.params(value: Float).returns(Float) }
|
||||
def denormalize(value)
|
||||
value * scale + min
|
||||
end
|
||||
|
||||
sig do
|
||||
sig(:final) do
|
||||
params(
|
||||
mapper: T.nilable(T.proc.params(arg: Float).returns(String)),
|
||||
).returns(String)
|
||||
@@ -8,18 +8,6 @@ module Stats
|
||||
extend T::Helpers
|
||||
abstract!
|
||||
|
||||
sig(:final) { returns(T::Array[Float]) }
|
||||
attr_reader :x_values
|
||||
|
||||
sig(:final) { returns(T::Array[Float]) }
|
||||
attr_reader :y_values
|
||||
|
||||
sig(:final) { returns(Stats::AxisRange) }
|
||||
attr_reader :x
|
||||
|
||||
sig(:final) { returns(Stats::AxisRange) }
|
||||
attr_reader :y
|
||||
|
||||
sig(:final) { params(records: T::Array[Domain::FaFavIdAndDate]).void }
|
||||
def initialize(records)
|
||||
data_points =
|
||||
@@ -38,17 +26,23 @@ module Stats
|
||||
x_minmax = T.cast(@x_values.minmax, [Float, Float])
|
||||
y_minmax = T.cast(@y_values.minmax, [Float, Float])
|
||||
@x =
|
||||
T.let(
|
||||
Stats::AxisRange.new(min: x_minmax[0], max: x_minmax[1]),
|
||||
Stats::AxisRange,
|
||||
)
|
||||
T.let(axis_class.new(min: x_minmax[0], max: x_minmax[1]), Stats::Axis)
|
||||
@y =
|
||||
T.let(
|
||||
Stats::AxisRange.new(min: y_minmax[0], max: y_minmax[1]),
|
||||
Stats::AxisRange,
|
||||
)
|
||||
T.let(axis_class.new(min: y_minmax[0], max: y_minmax[1]), Stats::Axis)
|
||||
end
|
||||
|
||||
sig(:final) { returns(T::Array[Float]) }
|
||||
attr_reader :x_values
|
||||
|
||||
sig(:final) { returns(T::Array[Float]) }
|
||||
attr_reader :y_values
|
||||
|
||||
sig(:final) { returns(Stats::Axis) }
|
||||
attr_reader :x
|
||||
|
||||
sig(:final) { returns(Stats::Axis) }
|
||||
attr_reader :y
|
||||
|
||||
sig(:final) { returns(String) }
|
||||
def x_range
|
||||
@x.as_string
|
||||
@@ -59,8 +53,8 @@ module Stats
|
||||
@y.as_string { |x| Time.at(x) }
|
||||
end
|
||||
|
||||
# Convert raw data to normalized [0,1] scale for Rumale
|
||||
sig(:final) { returns(T::Array[T::Array[Float]]) }
|
||||
# Default transformation matrix (identity for linear/quadratic)
|
||||
sig { returns(T::Array[T::Array[Float]]) }
|
||||
def normalized_x_matrix
|
||||
@x_values.map { |x| [@x.normalize(x)] }
|
||||
end
|
||||
@@ -77,12 +71,6 @@ module Stats
|
||||
@x.range.step(step_size).to_a
|
||||
end
|
||||
|
||||
# Default transformation matrix (identity for linear/quadratic)
|
||||
sig { returns(T::Array[T::Array[Float]]) }
|
||||
def transformed_x_matrix
|
||||
normalized_x_matrix
|
||||
end
|
||||
|
||||
# Abstract method for denormalizing regression results
|
||||
sig do
|
||||
abstract
|
||||
@@ -91,5 +79,11 @@ module Stats
|
||||
end
|
||||
def denormalize_regression(regression_x, weight_vec)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
sig { abstract.returns(T.class_of(Stats::Axis)) }
|
||||
def axis_class
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -47,11 +47,23 @@ module Stats
|
||||
def coefficients
|
||||
end
|
||||
|
||||
# Evaluate the equation for a given x value
|
||||
sig(:final) { params(x_value: Float).returns(Float) }
|
||||
def evaluate(x_value)
|
||||
normalizer.y.denormalize(
|
||||
evaluate_normalized(normalizer.x.normalize(x_value)),
|
||||
)
|
||||
end
|
||||
|
||||
sig { returns(Stats::DataNormalizer) }
|
||||
attr_reader :normalizer
|
||||
|
||||
protected
|
||||
|
||||
sig { abstract.params(x_value: Float).returns(Float) }
|
||||
def evaluate_normalized(x_value)
|
||||
end
|
||||
|
||||
sig { abstract.returns(String) }
|
||||
def format_equation
|
||||
end
|
||||
|
||||
16
app/lib/stats/linear_axis.rb
Normal file
16
app/lib/stats/linear_axis.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Stats
|
||||
class LinearAxis < Axis
|
||||
sig { override.params(value: Float).returns(Float) }
|
||||
def normalize(value)
|
||||
(value - min) / scale
|
||||
end
|
||||
|
||||
sig { override.params(value: Float).returns(Float) }
|
||||
def denormalize(value)
|
||||
value * scale + min
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -30,5 +30,12 @@ module Stats
|
||||
|
||||
[intercept_orig, slope_orig]
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
sig { override.returns(T.class_of(Stats::Axis)) }
|
||||
def axis_class
|
||||
Stats::LinearAxis
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
16
app/lib/stats/logarithmic_axis.rb
Normal file
16
app/lib/stats/logarithmic_axis.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Stats
|
||||
class LogarithmicAxis < Axis
|
||||
sig { override.params(value: Float).returns(Float) }
|
||||
def normalize(value)
|
||||
Math.log(value)
|
||||
end
|
||||
|
||||
sig { override.params(value: Float).returns(Float) }
|
||||
def denormalize(value)
|
||||
Math.exp(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -9,39 +9,16 @@ module Stats
|
||||
EPSILON = 1e-8
|
||||
|
||||
# Convert x values to log-transformed matrix for logarithmic regression
|
||||
sig { returns(T::Array[T::Array[Float]]) }
|
||||
def transformed_x_matrix
|
||||
@x_values.map { |x| [transform_normalized_x(@x.normalize(x) + EPSILON)] }
|
||||
end
|
||||
|
||||
# Common denormalization logic using the transformation function
|
||||
sig do
|
||||
override
|
||||
.params(regression_x: T::Array[Float], weight_vec: T::Array[Float])
|
||||
.returns(T::Array[Float])
|
||||
end
|
||||
def denormalize_regression(regression_x, weight_vec)
|
||||
norm_slope = T.cast(weight_vec[1], Float)
|
||||
norm_intercept = T.cast(weight_vec[0], Float)
|
||||
regression_x.map do |x|
|
||||
x_norm = @x.normalize(x) + EPSILON
|
||||
y_norm = norm_slope * transform_normalized_x(x_norm) + norm_intercept
|
||||
@y.denormalize(y_norm)
|
||||
end
|
||||
sig { override.returns(T::Array[T::Array[Float]]) }
|
||||
def normalized_x_matrix
|
||||
@x_values.map { |x| [@x.normalize(x) + EPSILON] }
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Apply logarithmic transformation to normalized x values
|
||||
sig { params(x_norm: Float).returns(Float) }
|
||||
def transform_normalized_x(x_norm)
|
||||
Math.log(x_norm)
|
||||
end
|
||||
|
||||
# Apply logarithmic transformation to raw x values (for backward compatibility)
|
||||
sig { override.params(x: Float).returns(Float) }
|
||||
def transform_x(x)
|
||||
Math.log(x)
|
||||
sig { override.returns(T.class_of(Stats::Axis)) }
|
||||
def axis_class
|
||||
Stats::LogarithmicAxis
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,7 +13,7 @@ module Stats
|
||||
end
|
||||
def initialize(normalizer, coefficients)
|
||||
super(normalizer)
|
||||
@coefficients = coefficients
|
||||
@coefficients = T.let(coefficients, T::Array[Float])
|
||||
end
|
||||
|
||||
# Public method to get coefficients
|
||||
@@ -22,6 +22,20 @@ module Stats
|
||||
@coefficients
|
||||
end
|
||||
|
||||
# Evaluate the polynomial equation for a given x value
|
||||
sig(:final) { override.params(x_value: Float).returns(Float) }
|
||||
def evaluate_normalized(x_value)
|
||||
return 0.0 if @coefficients.empty?
|
||||
|
||||
# Use Horner's method for efficient polynomial evaluation
|
||||
result = T.let(@coefficients.last, T.untyped)
|
||||
T
|
||||
.must(@coefficients[0...-1])
|
||||
.reverse
|
||||
.each { |coeff| result = result * x_value + coeff }
|
||||
result
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
sig { override.returns(String) }
|
||||
|
||||
@@ -35,5 +35,12 @@ module Stats
|
||||
|
||||
[c_orig, b_orig, a_orig]
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
sig { override.returns(T.class_of(Stats::Axis)) }
|
||||
def axis_class
|
||||
Stats::LinearAxis
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -68,7 +68,7 @@ module Stats
|
||||
)
|
||||
|
||||
# Fit the pipeline on training data
|
||||
training_x_matrix = training_normalizer.transformed_x_matrix
|
||||
training_x_matrix = training_normalizer.normalized_x_matrix
|
||||
training_y_vector = training_normalizer.normalized_y_vector
|
||||
pipeline.fit(training_x_matrix, training_y_vector)
|
||||
|
||||
@@ -76,7 +76,7 @@ module Stats
|
||||
training_r_squared = pipeline.score(training_x_matrix, training_y_vector)
|
||||
|
||||
# Score on evaluation data
|
||||
evaluation_x_matrix = evaluation_normalizer.transformed_x_matrix
|
||||
evaluation_x_matrix = evaluation_normalizer.normalized_x_matrix
|
||||
evaluation_y_vector = evaluation_normalizer.normalized_y_vector
|
||||
evaluation_r_squared =
|
||||
pipeline.score(evaluation_x_matrix, evaluation_y_vector)
|
||||
|
||||
16
app/lib/stats/square_root_axis.rb
Normal file
16
app/lib/stats/square_root_axis.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Stats
|
||||
class SquareRootAxis < Axis
|
||||
sig { override.params(value: Float).returns(Float) }
|
||||
def normalize(value)
|
||||
Math.sqrt(value)
|
||||
end
|
||||
|
||||
sig { override.params(value: Float).returns(Float) }
|
||||
def denormalize(value)
|
||||
value * value
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,42 +4,11 @@
|
||||
module Stats
|
||||
# Square root regression specific normalizer
|
||||
class SquareRootNormalizer < TransformedNormalizer
|
||||
extend T::Sig
|
||||
|
||||
# Convert x values to square root transformed matrix for square root regression
|
||||
sig { returns(T::Array[T::Array[Float]]) }
|
||||
def transformed_x_matrix
|
||||
@x_values.map { |x| [transform_normalized_x(@x.normalize(x))] }
|
||||
end
|
||||
|
||||
# Common denormalization logic using the transformation function
|
||||
sig do
|
||||
override
|
||||
.params(regression_x: T::Array[Float], weight_vec: T::Array[Float])
|
||||
.returns(T::Array[Float])
|
||||
end
|
||||
def denormalize_regression(regression_x, weight_vec)
|
||||
norm_slope = T.cast(weight_vec[1], Float)
|
||||
norm_intercept = T.cast(weight_vec[0], Float)
|
||||
regression_x.map do |x|
|
||||
x_norm = @x.normalize(x)
|
||||
y_norm = norm_slope * transform_normalized_x(x_norm) + norm_intercept
|
||||
@y.denormalize(y_norm)
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Apply square root transformation to normalized x values
|
||||
sig { params(x_norm: Float).returns(Float) }
|
||||
def transform_normalized_x(x_norm)
|
||||
Math.sqrt(x_norm)
|
||||
end
|
||||
|
||||
# Apply square root transformation to raw x values (for backward compatibility)
|
||||
sig { override.params(x: Float).returns(Float) }
|
||||
def transform_x(x)
|
||||
Math.sqrt(x)
|
||||
sig { override.returns(T.class_of(Stats::Axis)) }
|
||||
def axis_class
|
||||
Stats::SquareRootAxis
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -29,6 +29,12 @@ module Stats
|
||||
|
||||
protected
|
||||
|
||||
# Evaluate the transformed equation for a given x value
|
||||
sig(:final) { override.params(x_value: Float).returns(Float) }
|
||||
def evaluate_normalized(x_value)
|
||||
@norm_slope * x_value + @norm_intercept
|
||||
end
|
||||
|
||||
sig { override.returns(String) }
|
||||
def format_equation
|
||||
slope_orig = @norm_slope * @normalizer.y.scale
|
||||
|
||||
@@ -16,12 +16,11 @@ module Stats
|
||||
def denormalize_coefficients(norm_intercept, norm_slope)
|
||||
slope_orig = norm_slope * @y.scale
|
||||
intercept_orig = norm_intercept * @y.scale + @y.min
|
||||
|
||||
[intercept_orig, slope_orig]
|
||||
end
|
||||
|
||||
# Common denormalization logic using the transformation function
|
||||
sig do
|
||||
sig(:final) do
|
||||
override
|
||||
.params(regression_x: T::Array[Float], weight_vec: T::Array[Float])
|
||||
.returns(T::Array[Float])
|
||||
@@ -31,16 +30,9 @@ module Stats
|
||||
norm_intercept = T.cast(weight_vec[0], Float)
|
||||
regression_x.map do |x|
|
||||
# y = a * f(x) + b, where coefficients are in normalized space
|
||||
y_norm = norm_slope * transform_x(x) + norm_intercept
|
||||
y_norm = norm_slope * @x.normalize(x) + norm_intercept
|
||||
@y.denormalize(y_norm)
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Abstract method for applying the transformation function
|
||||
sig { abstract.params(x: Float).returns(Float) }
|
||||
def transform_x(x)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -21,7 +21,6 @@ namespace :stats do
|
||||
|
||||
# Create base normalizer for display ranges
|
||||
base_normalizer = Stats::LinearNormalizer.new(records_array)
|
||||
|
||||
puts "📈 X-axis range (fav_fa_id): #{base_normalizer.x_range}"
|
||||
puts "📈 Y-axis range (date): #{base_normalizer.y_range}"
|
||||
|
||||
|
||||
5
spec/lib/stats/equation_spec.rb
Normal file
5
spec/lib/stats/equation_spec.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
# typed: false
|
||||
require "rails_helper"
|
||||
|
||||
describe Stats::Equation do
|
||||
end
|
||||
Reference in New Issue
Block a user