///|
/// Input audit for a numeric stream before it is handed to a detector.
pub struct SeriesAudit {
length : Int
finite_values : Int
non_finite_values : Int
repeated_adjacent_values : Int
increasing_steps : Int
decreasing_steps : Int
zero_steps : Int
minimum : Double?
maximum : Double?
}
///|
pub fn audit_series(values : Array[Double]) -> SeriesAudit {
let mut finite_values = 0
let mut non_finite_values = 0
let mut repeated = 0
let mut increasing = 0
let mut decreasing = 0
let mut zero = 0
let mut minimum : Double? = None
let mut maximum : Double? = None
let mut previous : Double? = None
for value in values {
if !finite(value) {
non_finite_values = non_finite_values + 1
} else {
finite_values = finite_values + 1
match minimum {
None => minimum = Some(value)
Some(current) => if value < current { minimum = Some(value) }
}
match maximum {
None => maximum = Some(value)
Some(current) => if value > current { maximum = Some(value) }
}
match previous {
None => ()
Some(current) =>
if value > current {
increasing = increasing + 1
} else if value < current {
decreasing = decreasing + 1
} else {
repeated = repeated + 1
zero = zero + 1
}
}
previous = Some(value)
}
}
{
length: values.length(),
finite_values,
non_finite_values,
repeated_adjacent_values: repeated,
increasing_steps: increasing,
decreasing_steps: decreasing,
zero_steps: zero,
minimum,
maximum,
}
}
///|
pub fn SeriesAudit::is_usable(self : SeriesAudit) -> Bool {
self.length > 0 && self.finite_values == self.length
}
///|
pub fn SeriesAudit::is_constant(self : SeriesAudit) -> Bool {
self.finite_values > 0 &&
self.increasing_steps == 0 &&
self.decreasing_steps == 0
}
///|
pub fn SeriesAudit::monotonic_direction(self : SeriesAudit) -> String {
if self.increasing_steps > 0 && self.decreasing_steps == 0 {
"increasing"
} else if self.decreasing_steps > 0 && self.increasing_steps == 0 {
"decreasing"
} else if self.increasing_steps == 0 && self.decreasing_steps == 0 {
"constant"
} else {
"mixed"
}
}
///|
pub fn SeriesAudit::range(self : SeriesAudit) -> Double? {
match (self.minimum, self.maximum) {
(Some(lower), Some(upper)) => Some(upper - lower)
_ => None
}
}
///|
pub fn SeriesAudit::summary(self : SeriesAudit) -> String {
"length=" +
self.length.to_string() +
",finite=" +
self.finite_values.to_string() +
",non_finite=" +
self.non_finite_values.to_string() +
",repeated_adjacent=" +
self.repeated_adjacent_values.to_string() +
",direction=" +
self.monotonic_direction()
}
///|
pub fn validate_audit(audit : SeriesAudit) -> Result[Unit, DetectError] {
if audit.length == 0 {
return Err(InvalidRunLengthLimit)
}
if audit.non_finite_values > 0 {
return Err(NonFiniteObservation)
}
Ok(())
}
///|
pub fn audit_difference(
values : Array[Double],
) -> Result[SeriesAudit, DetectError] {
let series = match NumericSeries::new(values) {
Ok(value) => value
Err(error) => return Err(error)
}
Ok(audit_series(series.difference()))
}
///|
pub struct RunLengthAudit {
observed_steps : Int
retained_states : Int
truncation_ratio : Double
}
///|
pub fn audit_run_length_capacity(
updates : Array[Update],
max_run_length : Int,
) -> Result[RunLengthAudit, DetectError] {
if updates.length() == 0 || max_run_length < 2 {
return Err(InvalidRunLengthLimit)
}
let mut retained = 0
for update in updates {
retained = retained + update.retained_run_lengths
}
Ok({
observed_steps: updates.length(),
retained_states: retained,
truncation_ratio: retained.to_double() /
(updates.length() * max_run_length).to_double(),
})
}
///|
pub fn SeriesAudit::quality_score(self : SeriesAudit) -> Int {
if self.length == 0 {
return 0
}
let mut score = 100
score = score - self.non_finite_values * 100 / self.length
if self.is_constant() {
score = score - 20
}
if self.repeated_adjacent_values * 2 > self.length {
score = score - 10
}
if score < 0 {
0
} else {
score
}
}
///|
pub fn SeriesAudit::recommendation(self : SeriesAudit) -> String {
if self.length == 0 {
"provide at least one observation"
} else if self.non_finite_values > 0 {
"remove or impute non-finite observations before detection"
} else if self.is_constant() {
"constant series is valid but cannot demonstrate a change point"
} else if self.quality_score() < 80 {
"inspect repeated or irregular observations before calibration"
} else {
"series is suitable for detector calibration"
}
}