///|
/// Transformation kinds available in the preprocessing chain.
pub(all) enum ProductionTransformKind {
IdentityTransform
ClipTransform
DifferenceTransform
Log1pTransform
SqrtTransform
ZScoreTransform
RobustZTransform
DetrendTransform
SmoothTransform
WinsorizeTransform
SeasonalRemoveTransform
RateTransform
}
///|
pub fn production_transform_kind_name(kind : ProductionTransformKind) -> String {
match kind {
IdentityTransform => "identity"
ClipTransform => "clip"
DifferenceTransform => "difference"
Log1pTransform => "log1p"
SqrtTransform => "sqrt"
ZScoreTransform => "z-score"
RobustZTransform => "robust-z"
DetrendTransform => "detrend"
SmoothTransform => "smooth"
WinsorizeTransform => "winsorize"
SeasonalRemoveTransform => "seasonal-remove"
RateTransform => "rate"
}
}
///|
/// One configured preprocessing operation.
pub struct ProductionTransformSpec {
kind : ProductionTransformKind
parameter_a : Double
parameter_b : Double
integer_parameter : Int
}
///|
pub fn ProductionTransformSpec::new(
kind : ProductionTransformKind,
parameter_a? : Double = 0.0,
parameter_b? : Double = 1.0,
integer_parameter? : Int = 3,
) -> ProductionTransformSpec {
{
kind,
parameter_a,
parameter_b,
integer_parameter: if integer_parameter < 1 {
1
} else {
integer_parameter
},
}
}
///|
pub fn ProductionTransformSpec::kind(
self : ProductionTransformSpec,
) -> ProductionTransformKind {
self.kind
}
///|
pub fn ProductionTransformSpec::parameter_a(
self : ProductionTransformSpec,
) -> Double {
self.parameter_a
}
///|
pub fn ProductionTransformSpec::parameter_b(
self : ProductionTransformSpec,
) -> Double {
self.parameter_b
}
///|
pub fn ProductionTransformSpec::integer_parameter(
self : ProductionTransformSpec,
) -> Int {
self.integer_parameter
}
///|
pub fn ProductionTransformSpec::summary(
self : ProductionTransformSpec,
) -> String {
production_transform_kind_name(self.kind) +
"(" +
self.parameter_a.to_string() +
"," +
self.parameter_b.to_string() +
"," +
self.integer_parameter.to_string() +
")"
}
///|
/// Diagnostics from one preprocessing pass.
pub struct ProductionPreprocessingReport {
mut input_count : Int
mut output_count : Int
mut invalid_input : Int
mut invalid_output : Int
mut clipped : Int
mut imputed : Int
mut transformed : Int
mut finite : Bool
}
///|
pub fn ProductionPreprocessingReport::empty() -> ProductionPreprocessingReport {
{
input_count: 0,
output_count: 0,
invalid_input: 0,
invalid_output: 0,
clipped: 0,
imputed: 0,
transformed: 0,
finite: true,
}
}
///|
pub fn ProductionPreprocessingReport::input_count(
self : ProductionPreprocessingReport,
) -> Int {
self.input_count
}
///|
pub fn ProductionPreprocessingReport::output_count(
self : ProductionPreprocessingReport,
) -> Int {
self.output_count
}
///|
pub fn ProductionPreprocessingReport::invalid_input(
self : ProductionPreprocessingReport,
) -> Int {
self.invalid_input
}
///|
pub fn ProductionPreprocessingReport::invalid_output(
self : ProductionPreprocessingReport,
) -> Int {
self.invalid_output
}
///|
pub fn ProductionPreprocessingReport::clipped(
self : ProductionPreprocessingReport,
) -> Int {
self.clipped
}
///|
pub fn ProductionPreprocessingReport::imputed(
self : ProductionPreprocessingReport,
) -> Int {
self.imputed
}
///|
pub fn ProductionPreprocessingReport::transformed(
self : ProductionPreprocessingReport,
) -> Int {
self.transformed
}
///|
pub fn ProductionPreprocessingReport::finite(
self : ProductionPreprocessingReport,
) -> Bool {
self.finite
}
///|
pub fn ProductionPreprocessingReport::quality(
self : ProductionPreprocessingReport,
) -> Double {
if self.input_count == 0 {
1.0
} else {
clamp_probability(
1.0 - self.invalid_output.to_double() / self.input_count.to_double(),
)
}
}
///|
pub fn ProductionPreprocessingReport::summary(
self : ProductionPreprocessingReport,
) -> String {
"input=" +
self.input_count.to_string() +
",output=" +
self.output_count.to_string() +
",invalid_input=" +
self.invalid_input.to_string() +
",invalid_output=" +
self.invalid_output.to_string() +
",clipped=" +
self.clipped.to_string() +
",imputed=" +
self.imputed.to_string() +
",transformed=" +
self.transformed.to_string() +
",quality=" +
self.quality().to_string()
}
///|
/// A transform chain that can be reused across batches.
pub struct ProductionPreprocessor {
specs : Array[ProductionTransformSpec]
missing : MissingValueStrategy
mut batches : Int
mut values : Int
mut failed : Int
}
///|
pub fn ProductionPreprocessor::new(
missing? : MissingValueStrategy = DropValue,
) -> ProductionPreprocessor {
{ specs: [], missing, batches: 0, values: 0, failed: 0 }
}
///|
pub fn ProductionPreprocessor::add(
self : ProductionPreprocessor,
spec : ProductionTransformSpec,
) -> Unit {
self.specs.push(spec)
}
///|
pub fn ProductionPreprocessor::clear(self : ProductionPreprocessor) -> Unit {
self.specs.clear()
}
///|
pub fn ProductionPreprocessor::specs(
self : ProductionPreprocessor,
) -> Array[ProductionTransformSpec] {
let result : Array[ProductionTransformSpec] = []
for spec in self.specs {
result.push(spec)
}
result
}
///|
pub fn ProductionPreprocessor::batches(self : ProductionPreprocessor) -> Int {
self.batches
}
///|
pub fn ProductionPreprocessor::values(self : ProductionPreprocessor) -> Int {
self.values
}
///|
pub fn ProductionPreprocessor::failed(self : ProductionPreprocessor) -> Int {
self.failed
}
///|
fn production_safe_values(
values : Array[Double],
strategy : MissingValueStrategy,
) -> (Array[Double], Int) {
let output : Array[Double] = []
let invalid = values.length() - remove_invalid(values).length()
let valid = remove_invalid(values)
let fallback = match strategy {
ImputeLast =>
if valid.length() == 0 {
0.0
} else {
valid[valid.length() - 1]
}
ImputeMean => mean(valid)
ImputeZero => 0.0
_ => 0.0
}
let mut last = fallback
for value in values {
if is_finite(value) {
output.push(value)
last = value
} else {
match strategy {
DropValue => ()
MarkUnknown => output.push(0.0)
ImputeLast => output.push(last)
ImputeMean => output.push(fallback)
ImputeZero => output.push(0.0)
}
}
}
(output, invalid)
}
///|
fn production_transform_clip(
values : Array[Double],
lower : Double,
upper : Double,
) -> (Array[Double], Int) {
let low = if lower > upper { upper } else { lower }
let high = if lower > upper { lower } else { upper }
let output : Array[Double] = []
let mut clipped = 0
for value in values {
let result = if value < low {
clipped += 1
low
} else if value > high {
clipped += 1
high
} else {
value
}
output.push(result)
}
(output, clipped)
}
///|
fn production_transform_difference(
values : Array[Double],
lag : Int,
) -> Array[Double] {
let output : Array[Double] = []
let safe_lag = if lag < 1 { 1 } else { lag }
for i in safe_lag.. Array[Double] {
let output : Array[Double] = []
for value in values {
let safe = if value <= -1.0 { -0.999999999999 } else { value }
output.push(@math.ln(1.0 + safe))
}
output
}
///|
fn production_transform_sqrt(values : Array[Double]) -> Array[Double] {
let output : Array[Double] = []
for value in values {
output.push(if value < 0.0 { 0.0 } else { value.sqrt() })
}
output
}
///|
fn production_transform_zscore(values : Array[Double]) -> Array[Double] {
let center = mean(values)
let scale = standard_deviation(values)
let safe_scale = if scale < 1.0e-12 { 1.0 } else { scale }
let output : Array[Double] = []
for value in values {
output.push((value - center) / safe_scale)
}
output
}
///|
fn production_transform_robust_z(values : Array[Double]) -> Array[Double] {
let center = median(values)
let scale = median_absolute_deviation(values)
let safe_scale = if scale < 1.0e-12 { 1.0 } else { scale }
let output : Array[Double] = []
for value in values {
output.push((value - center) / safe_scale)
}
output
}
///|
fn production_transform_detrend(values : Array[Double]) -> Array[Double] {
let slope = linear_slope(values)
let center = mean(values)
let output : Array[Double] = []
for i, value in values {
output.push(
value -
(
center +
slope * (i.to_double() - (values.length() - 1).to_double() / 2.0)
),
)
}
output
}
///|
fn production_transform_smooth(
values : Array[Double],
width : Int,
) -> Array[Double] {
let safe_width = if width < 1 { 1 } else { width }
let output : Array[Double] = []
for i in 0.. Array[Double] {
let lower = quantile(values, lower_probability)
let upper = quantile(values, upper_probability)
production_transform_clip(values, lower, upper).0
}
///|
fn production_transform_seasonal_remove(
values : Array[Double],
period : Int,
) -> Array[Double] {
if period < 2 || values.length() < period {
return values
}
deseasonalize(values, period)
}
///|
fn production_transform_rate(values : Array[Double]) -> Array[Double] {
production_percent_changes(values)
}
///|
fn production_apply_spec(
values : Array[Double],
spec : ProductionTransformSpec,
) -> (Array[Double], Int) {
match spec.kind() {
IdentityTransform => (values, 0)
ClipTransform =>
production_transform_clip(values, spec.parameter_a(), spec.parameter_b()).0
|> fn(result) { (result, 0) }
DifferenceTransform =>
(production_transform_difference(values, spec.integer_parameter()), 0)
Log1pTransform => (production_transform_log1p(values), 0)
SqrtTransform => (production_transform_sqrt(values), 0)
ZScoreTransform => (production_transform_zscore(values), 0)
RobustZTransform => (production_transform_robust_z(values), 0)
DetrendTransform => (production_transform_detrend(values), 0)
SmoothTransform =>
(production_transform_smooth(values, spec.integer_parameter()), 0)
WinsorizeTransform =>
(
production_transform_winsorize(
values,
spec.parameter_a(),
spec.parameter_b(),
),
0,
)
SeasonalRemoveTransform =>
(
production_transform_seasonal_remove(values, spec.integer_parameter()),
0,
)
RateTransform => (production_transform_rate(values), 0)
}
}
///|
pub fn ProductionPreprocessor::transform(
self : ProductionPreprocessor,
values : Array[Double],
) -> (Array[Double], ProductionPreprocessingReport) {
let safe = production_safe_values(values, self.missing)
let mut current = safe.0
let report = ProductionPreprocessingReport::empty()
report.input_count = values.length()
report.invalid_input = safe.1
report.imputed = if self.missing is DropValue { 0 } else { safe.1 }
let mut transformed = 0
let mut clipped = 0
for spec in self.specs {
let next = production_apply_spec(current, spec)
current = next.0
clipped += next.1
transformed += 1
}
report.output_count = current.length()
report.clipped = clipped
report.transformed = transformed
report.invalid_output = current.length() - remove_invalid(current).length()
report.finite = report.invalid_output == 0
self.batches += 1
self.values += values.length()
if !report.finite {
self.failed += 1
}
(current, report)
}
///|
pub fn ProductionPreprocessor::transform_points(
self : ProductionPreprocessor,
points : Array[SignalPoint],
) -> (Array[SignalPoint], ProductionPreprocessingReport) {
let values : Array[Double] = []
for point in points {
values.push(point.value)
}
let transformed = self.transform(values)
let output : Array[SignalPoint] = []
let n = if transformed.0.length() < points.length() {
transformed.0.length()
} else {
points.length()
}
for i in 0.. Array[Array[Double]] {
let result : Array[Array[Double]] = []
for batch in batches {
result.push(self.transform(batch).0)
}
result
}
///|
pub fn production_transform_values(
values : Array[Double],
specs : Array[ProductionTransformSpec],
missing? : MissingValueStrategy = DropValue,
) -> Array[Double] {
let preprocessor = ProductionPreprocessor::new(missing~)
for spec in specs {
preprocessor.add(spec)
}
preprocessor.transform(values).0
}
///|
pub fn production_clip_percentiles(
values : Array[Double],
lower? : Double = 0.01,
upper? : Double = 0.99,
) -> Array[Double] {
production_transform_winsorize(
values,
clamp_probability(lower),
clamp_probability(upper),
)
}
///|
pub fn production_normalize_range(
values : Array[Double],
lower? : Double = 0.0,
upper? : Double = 1.0,
) -> Array[Double] {
if values.length() == 0 {
return []
}
let source_low = array_minimum(values)
let source_high = array_maximum(values)
if source_high <= source_low {
return Array::make(values.length(), lower)
}
let result : Array[Double] = []
for value in values {
result.push(
lower +
(value - source_low) * (upper - lower) / (source_high - source_low),
)
}
result
}
///|
pub fn production_center_scale(values : Array[Double]) -> Array[Double] {
production_transform_robust_z(values)
}
///|
pub fn production_residuals_from_trend(values : Array[Double]) -> Array[Double] {
production_transform_detrend(values)
}
///|
pub fn production_smoothed_values(
values : Array[Double],
width? : Int = 5,
) -> Array[Double] {
production_transform_smooth(values, width)
}