///|
/// Feature family exposed by the production feature pipeline.
pub(all) enum ProductionFeatureKind {
LevelFeature
SpreadFeature
TrendFeature
VolatilityFeature
SkewFeature
KurtosisFeature
AutocorrelationFeature
DifferenceFeature
QuantileFeature
DistributionEntropyFeature
MissingRatioFeature
OutlierRatioFeature
SeasonalStrengthFeature
}
///|
pub fn production_feature_kind_name(kind : ProductionFeatureKind) -> String {
match kind {
LevelFeature => "level"
SpreadFeature => "spread"
TrendFeature => "trend"
VolatilityFeature => "volatility"
SkewFeature => "skew"
KurtosisFeature => "kurtosis"
AutocorrelationFeature => "autocorrelation"
DifferenceFeature => "difference"
QuantileFeature => "quantile"
DistributionEntropyFeature => "distribution-entropy"
MissingRatioFeature => "missing-ratio"
OutlierRatioFeature => "outlier-ratio"
SeasonalStrengthFeature => "seasonal-strength"
}
}
///|
/// One named feature value with quality and provenance metadata.
pub struct ProductionFeature {
name : String
kind : ProductionFeatureKind
value : Double
valid : Bool
sample_count : Int
source_window : Int
}
///|
pub fn ProductionFeature::new(
kind : ProductionFeatureKind,
value : Double,
sample_count : Int,
source_window : Int,
) -> ProductionFeature {
{
name: production_feature_kind_name(kind),
kind,
value: if is_finite(value) {
value
} else {
0.0
},
valid: is_finite(value),
sample_count: if sample_count < 0 {
0
} else {
sample_count
},
source_window: if source_window < 0 {
0
} else {
source_window
},
}
}
///|
pub fn ProductionFeature::name(self : ProductionFeature) -> String {
self.name
}
///|
pub fn ProductionFeature::kind(
self : ProductionFeature,
) -> ProductionFeatureKind {
self.kind
}
///|
pub fn ProductionFeature::value(self : ProductionFeature) -> Double {
self.value
}
///|
pub fn ProductionFeature::valid(self : ProductionFeature) -> Bool {
self.valid
}
///|
pub fn ProductionFeature::sample_count(self : ProductionFeature) -> Int {
self.sample_count
}
///|
pub fn ProductionFeature::source_window(self : ProductionFeature) -> Int {
self.source_window
}
///|
/// A fixed-order feature vector suitable for a model or report.
pub struct ProductionFeatureVector {
features : Array[ProductionFeature]
values : Array[Double]
valid_count : Int
missing_count : Int
}
///|
pub fn ProductionFeatureVector::new(
features : Array[ProductionFeature],
) -> ProductionFeatureVector {
let values : Array[Double] = []
let mut valid_count = 0
let mut missing_count = 0
for feature in features {
values.push(feature.value())
if feature.valid() {
valid_count += 1
} else {
missing_count += 1
}
}
{ features, values, valid_count, missing_count }
}
///|
pub fn ProductionFeatureVector::features(
self : ProductionFeatureVector,
) -> Array[ProductionFeature] {
let result : Array[ProductionFeature] = []
for feature in self.features {
result.push(feature)
}
result
}
///|
pub fn ProductionFeatureVector::values(
self : ProductionFeatureVector,
) -> Array[Double] {
let result : Array[Double] = []
for value in self.values {
result.push(value)
}
result
}
///|
pub fn ProductionFeatureVector::valid_count(
self : ProductionFeatureVector,
) -> Int {
self.valid_count
}
///|
pub fn ProductionFeatureVector::missing_count(
self : ProductionFeatureVector,
) -> Int {
self.missing_count
}
///|
pub fn ProductionFeatureVector::valid_ratio(
self : ProductionFeatureVector,
) -> Double {
if self.features.length() == 0 {
1.0
} else {
self.valid_count.to_double() / self.features.length().to_double()
}
}
///|
pub fn ProductionFeatureVector::get(
self : ProductionFeatureVector,
name : String,
) -> ProductionFeature? {
for feature in self.features {
if feature.name() == name {
return Some(feature)
}
}
None
}
///|
pub fn ProductionFeatureVector::distance(
self : ProductionFeatureVector,
other : ProductionFeatureVector,
) -> Double {
let n = if self.values.length() < other.values.length() {
self.values.length()
} else {
other.values.length()
}
let mut total = 0.0
for i in 0.. String {
let entries : Array[String] = []
for feature in self.features {
entries.push(feature.name() + "=" + feature.value().to_string())
}
entries.join(",")
}
///|
/// Robust location and scale parameters learned from feature vectors.
pub struct ProductionFeatureScaler {
centers : Array[Double]
scales : Array[Double]
mut fitted : Bool
}
///|
pub fn ProductionFeatureScaler::new(
dimension? : Int = 1,
) -> ProductionFeatureScaler {
let size = if dimension < 1 { 1 } else { dimension }
{
centers: Array::make(size, 0.0),
scales: Array::make(size, 1.0),
fitted: false,
}
}
///|
pub fn ProductionFeatureScaler::dimension(
self : ProductionFeatureScaler,
) -> Int {
self.centers.length()
}
///|
pub fn ProductionFeatureScaler::fit(
self : ProductionFeatureScaler,
vectors : Array[ProductionFeatureVector],
) -> Bool {
if vectors.length() == 0 {
return false
}
let dimension = self.dimension()
for i in 0.. ()
Some(value) => if is_finite(value) { values.push(value) }
}
}
if values.length() == 0 {
self.centers[i] = 0.0
self.scales[i] = 1.0
} else {
self.centers[i] = median(values)
let deviation = median_absolute_deviation(values)
self.scales[i] = if deviation < 1.0e-12 {
let fallback = standard_deviation(values)
if fallback < 1.0e-12 {
1.0
} else {
fallback
}
} else {
deviation
}
}
}
self.fitted = true
true
}
///|
pub fn ProductionFeatureScaler::is_fitted(
self : ProductionFeatureScaler,
) -> Bool {
self.fitted
}
///|
pub fn ProductionFeatureScaler::centers(
self : ProductionFeatureScaler,
) -> Array[Double] {
let result : Array[Double] = []
for center in self.centers {
result.push(center)
}
result
}
///|
pub fn ProductionFeatureScaler::scales(
self : ProductionFeatureScaler,
) -> Array[Double] {
let result : Array[Double] = []
for scale in self.scales {
result.push(scale)
}
result
}
///|
pub fn ProductionFeatureScaler::transform(
self : ProductionFeatureScaler,
vector : ProductionFeatureVector,
) -> ProductionFeatureVector {
let result : Array[ProductionFeature] = []
for i, feature in vector.features {
let value = if i < self.centers.length() {
(feature.value() - self.centers[i]) / self.scales[i]
} else {
feature.value()
}
result.push(
ProductionFeature::new(
feature.kind(),
value,
feature.sample_count(),
feature.source_window(),
),
)
}
ProductionFeatureVector::new(result)
}
///|
pub fn ProductionFeatureScaler::inverse(
self : ProductionFeatureScaler,
values : Array[Double],
) -> Array[Double] {
let result : Array[Double] = []
let n = if values.length() < self.centers.length() {
values.length()
} else {
self.centers.length()
}
for i in 0.. ProductionFeatureConfig {
{
window_size: if window_size < 2 {
2
} else {
window_size
},
seasonal_period: if seasonal_period < 0 {
0
} else {
seasonal_period
},
include_distribution,
include_autocorrelation,
outlier_threshold: if outlier_threshold < 0.0 {
0.0
} else {
outlier_threshold
},
}
}
///|
pub fn ProductionFeatureConfig::window_size(
self : ProductionFeatureConfig,
) -> Int {
self.window_size
}
///|
pub fn ProductionFeatureConfig::seasonal_period(
self : ProductionFeatureConfig,
) -> Int {
self.seasonal_period
}
///|
pub fn ProductionFeatureConfig::include_distribution(
self : ProductionFeatureConfig,
) -> Bool {
self.include_distribution
}
///|
pub fn ProductionFeatureConfig::include_autocorrelation(
self : ProductionFeatureConfig,
) -> Bool {
self.include_autocorrelation
}
///|
pub fn ProductionFeatureConfig::outlier_threshold(
self : ProductionFeatureConfig,
) -> Double {
self.outlier_threshold
}
///|
/// Feature pipeline used before a model or detector call.
pub struct ProductionFeaturePipeline {
config : ProductionFeatureConfig
windows : Array[ProductionTimeWindow]
mut extracted : Int
mut invalid : Int
}
///|
pub fn ProductionFeaturePipeline::new(
config? : ProductionFeatureConfig = ProductionFeatureConfig::new(),
) -> ProductionFeaturePipeline {
{ config, windows: [], extracted: 0, invalid: 0 }
}
///|
pub fn ProductionFeaturePipeline::config(
self : ProductionFeaturePipeline,
) -> ProductionFeatureConfig {
self.config
}
///|
pub fn ProductionFeaturePipeline::extracted(
self : ProductionFeaturePipeline,
) -> Int {
self.extracted
}
///|
pub fn ProductionFeaturePipeline::invalid(
self : ProductionFeaturePipeline,
) -> Int {
self.invalid
}
///|
fn production_feature_skew(values : Array[Double]) -> Double {
if values.length() < 2 {
return 0.0
}
let center = mean(values)
let deviation = standard_deviation(values)
if deviation < 1.0e-12 {
return 0.0
}
let mut total = 0.0
for value in values {
total += (value - center) /
deviation *
((value - center) / deviation) *
((value - center) / deviation)
}
total / values.length().to_double()
}
///|
fn production_feature_kurtosis(values : Array[Double]) -> Double {
if values.length() < 2 {
return 0.0
}
let center = mean(values)
let deviation = standard_deviation(values)
if deviation < 1.0e-12 {
return 0.0
}
let mut total = 0.0
for value in values {
let standardized = (value - center) / deviation
total += standardized * standardized * standardized * standardized
}
total / values.length().to_double() - 3.0
}
///|
fn production_feature_outlier_ratio(
values : Array[Double],
threshold : Double,
) -> Double {
if values.length() == 0 {
return 0.0
}
let center = median(values)
let scale = median_absolute_deviation(values)
if scale < 1.0e-12 {
return 0.0
}
let mut count = 0
for value in values {
if absolute(value - center) / scale > threshold {
count += 1
}
}
count.to_double() / values.length().to_double()
}
///|
fn production_feature_entropy(
values : Array[Double],
bins? : Int = 8,
) -> Double {
if values.length() == 0 {
return 0.0
}
let count = if bins < 2 { 2 } else { bins }
let low = array_minimum(values)
let high = array_maximum(values)
if high <= low {
return 0.0
}
let counts = Array::make(count, 0)
for value in values {
let index = if value >= high {
count - 1
} else {
((value - low) / (high - low) * count.to_double()).to_int()
}
counts[index] += 1
}
let mut entropy = 0.0
for item in counts {
if item > 0 {
let probability = item.to_double() / values.length().to_double()
entropy -= probability * @math.ln(probability)
}
}
entropy
}
///|
pub fn ProductionFeaturePipeline::extract(
self : ProductionFeaturePipeline,
values : Array[Double],
) -> ProductionFeatureVector {
let valid = remove_invalid(values)
if valid.length() == 0 {
self.invalid += 1
}
let width = self.config.window_size()
let sample = if valid.length() <= width {
valid
} else {
let result : Array[Double] = []
for i in (valid.length() - width).. 1 {
seasonal_strength(sample, self.config.seasonal_period())
} else {
0.0
}
let missing_ratio = if values.length() == 0 {
0.0
} else {
1.0 - valid.length().to_double() / values.length().to_double()
}
let features : Array[ProductionFeature] = [
ProductionFeature::new(LevelFeature, center, n, width),
ProductionFeature::new(SpreadFeature, spread, n, width),
ProductionFeature::new(TrendFeature, trend, n, width),
ProductionFeature::new(VolatilityFeature, volatility, n, width),
ProductionFeature::new(
SkewFeature,
production_feature_skew(sample),
n,
width,
),
ProductionFeature::new(
KurtosisFeature,
production_feature_kurtosis(sample),
n,
width,
),
ProductionFeature::new(AutocorrelationFeature, autocorrelation, n, width),
ProductionFeature::new(DifferenceFeature, difference, n, width),
ProductionFeature::new(QuantileFeature, quantile(sample, 0.9), n, width),
ProductionFeature::new(DistributionEntropyFeature, entropy, n, width),
ProductionFeature::new(
MissingRatioFeature,
missing_ratio,
values.length(),
width,
),
ProductionFeature::new(
OutlierRatioFeature,
production_feature_outlier_ratio(sample, self.config.outlier_threshold()),
n,
width,
),
ProductionFeature::new(SeasonalStrengthFeature, seasonal, n, width),
]
self.extracted += 1
ProductionFeatureVector::new(features)
}
///|
pub fn ProductionFeaturePipeline::extract_from_window(
self : ProductionFeaturePipeline,
window : ProductionTimeWindow,
) -> ProductionFeatureVector {
self.extract(window.values())
}
///|
pub fn ProductionFeaturePipeline::extract_batch(
self : ProductionFeaturePipeline,
batches : Array[Array[Double]],
) -> Array[ProductionFeatureVector] {
let result : Array[ProductionFeatureVector] = []
for batch in batches {
result.push(self.extract(batch))
}
result
}
///|
pub fn ProductionFeaturePipeline::feature_names(
self : ProductionFeaturePipeline,
) -> Array[String] {
ignore(self.config.window_size())
let names : Array[String] = []
for
kind in [
LevelFeature,
SpreadFeature,
TrendFeature,
VolatilityFeature,
SkewFeature,
KurtosisFeature,
AutocorrelationFeature,
DifferenceFeature,
QuantileFeature,
DistributionEntropyFeature,
MissingRatioFeature,
OutlierRatioFeature,
SeasonalStrengthFeature,
] {
names.push(production_feature_kind_name(kind))
}
names
}
///|
/// Builds a compact feature-to-score summary used for explanations.
pub fn production_feature_contributions(
vector : ProductionFeatureVector,
weights : Array[Double],
) -> Array[EvidenceContribution] {
let result : Array[EvidenceContribution] = []
let features = vector.features()
let n = if features.length() < weights.length() {
features.length()
} else {
weights.length()
}
for i in 0.. Double {
let pipeline = ProductionFeaturePipeline::new(config~)
pipeline.extract(left).distance(pipeline.extract(right))
}