///|
/// Normalize a measured column to the unit interval.
pub fn normalize_column(
values : ArrayView[Double],
) -> Array[Double] raise @domain.ExtractionError {
if values.length() == 0 {
raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
}
for value in values {
@domain.require_finite("feature value", value)
}
let mut lower = values[0]
let mut upper = values[0]
for value in values {
lower = lower.min(value)
upper = upper.max(value)
}
if upper <= lower {
[
for _ in values => 0.0
]
} else {
[
for value in values => (value - lower) / (upper - lower)
]
}
}
///|
pub fn standardize_column(
values : ArrayView[Double],
) -> Array[Double] raise @domain.ExtractionError {
if values.length() == 0 {
raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
}
let mean = values.fold(init=0.0, (sum, value) => sum + value) /
values.length().to_double()
let variance = values.fold(init=0.0, (sum, value) => {
sum + (value - mean) * (value - mean)
}) /
values.length().to_double()
let scale = variance.sqrt().max(1.0e-12)
[
for value in values => (value - mean) / scale
]
}
///|
pub fn winsorize_column(
values : ArrayView[Double],
lower_probability~ : Double,
upper_probability~ : Double,
) -> Array[Double] raise @domain.ExtractionError {
if lower_probability < 0.0 ||
upper_probability > 1.0 ||
lower_probability > upper_probability {
raise @domain.ExtractionError::InvalidArgument(
message="winsorization probabilities are invalid",
)
}
let sorted = values.to_owned()
sorted.sort()
let lower = sorted[(lower_probability * (sorted.length() - 1).to_double())
.floor()
.to_int()]
let upper = sorted[(upper_probability * (sorted.length() - 1).to_double())
.ceil()
.to_int()]
[
for value in values => value.max(lower).min(upper)
]
}
///|
pub fn first_difference(values : ArrayView[Double]) -> Array[Double] {
if values.length() < 2 {
[]
} else {
[
for index in 1.. values[index] - values[index - 1]
]
}
}
///|
pub fn relative_difference(values : ArrayView[Double]) -> Array[Double] {
if values.length() < 2 {
[]
} else {
[
for index in 1.. {
(values[index] - values[index - 1]) /
values[index - 1].abs().max(1.0e-12)
}
]
}
}
///|
pub(all) struct FeatureVector {
id : String
features : Array[Double]
target : Double
} derive(Debug, Eq, ToJson)
///|
pub fn table_features(table : MeasurementTable) -> Array[FeatureVector] {
let xs = normalize_column(table.x_values()) catch { _ => table.x_values() }
let ys = normalize_column(table.y_values()) catch { _ => table.y_values() }
[
for index, row in table.rows() => {
{
id: row.id,
features: [
xs[index],
ys[index],
xs[index] * xs[index],
xs[index] * ys[index],
],
target: row.y,
}
}
]
}
///|
pub fn feature_matrix(table : MeasurementTable) -> Array[Array[Double]] {
[
for vector in table_features(table) => vector.features
]
}
///|
pub fn aggregate_replicates(
table : MeasurementTable,
) -> MeasurementTable raise @domain.ExtractionError {
let sorted = table.sorted_by_x().rows()
let result : Array[MeasurementRow] = []
let mut index = 0
while index < sorted.length() {
let first = sorted[index]
let mut cursor = index + 1
let mut sum_y = first.y
let mut sum_temperature = first.temperature_kelvin
let mut sum_pressure = first.pressure_pascal
while cursor < sorted.length() && sorted[cursor].id == first.id {
sum_y += sorted[cursor].y
sum_temperature += sorted[cursor].temperature_kelvin
sum_pressure += sorted[cursor].pressure_pascal
cursor += 1
}
let count = cursor - index
result.push(
MeasurementRow::new(
id=first.id,
x=first.x,
y=sum_y / count.to_double(),
temperature_kelvin=sum_temperature / count.to_double(),
pressure_pascal=sum_pressure / count.to_double(),
replicate=count,
),
)
index = cursor
}
MeasurementTable::new(name=table.name(), rows=result)
}
///|
pub fn rescale_column(
values : ArrayView[Double],
source_lower~ : Double,
source_upper~ : Double,
target_lower~ : Double,
target_upper~ : Double,
) -> Array[Double] raise @domain.ExtractionError {
if source_upper <= source_lower {
raise @domain.ExtractionError::InvalidArgument(
message="source scale is invalid",
)
}
[
for value in values => {
target_lower +
(value - source_lower) /
(source_upper - source_lower) *
(target_upper - target_lower)
}
]
}
///|
pub fn rolling_variance(
values : ArrayView[Double],
window~ : Int,
) -> Array[Double] raise @domain.ExtractionError {
if window < 1 || window > values.length() {
raise @domain.ExtractionError::InvalidArgument(
message="variance window is invalid",
)
}
[
for index in 0.. {
let start = (index + 1 - window).max(0)
let count = index - start + 1
let mut sum = 0.0
for cursor in start..<=index {
sum += values[cursor]
}
let mean = sum / count.to_double()
let mut variance = 0.0
for cursor in start..<=index {
let difference = values[cursor] - mean
variance += difference * difference
}
variance / count.to_double()
}
]
}
///|
pub fn detect_plateau(
values : ArrayView[Double],
tolerance~ : Double,
) -> Array[Bool] {
[
for index in 0.. {
if index == 0 {
false
} else {
(values[index] - values[index - 1]).abs() <= tolerance
}
}
]
}
///|
pub fn monotone_direction(values : ArrayView[Double]) -> Int {
let mut positive = 0
let mut negative = 0
for index in 1.. values[index - 1] {
positive += 1
}
if values[index] < values[index - 1] {
negative += 1
}
}
if positive > negative {
1
} else if negative > positive {
-1
} else {
0
}
}