more equastion refactoring

This commit is contained in:
Dylan Knutson
2025-07-10 21:59:31 +00:00
parent 163418c8cc
commit 5f81edea92
16 changed files with 154 additions and 115 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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) }

View File

@@ -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

View File

@@ -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)

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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}"

View File

@@ -0,0 +1,5 @@
# typed: false
require "rails_helper"
describe Stats::Equation do
end