///|
fn fit_standard(data : Dataset) -> FeatureScaler {
let columns = data.feature_count()
let rows = data.features()
let offsets = Array::make(columns, 0.0)
let scales = Array::make(columns, 0.0)
let constants = Array::make(columns, false)
for column = 0; column < columns; column = column + 1 {
let mut total = 0.0
for row in rows {
total = total + row[column]
}
let mean = total / rows.length().to_double()
offsets[column] = mean
let mut squared_total = 0.0
for row in rows {
let difference = row[column] - mean
squared_total = squared_total + difference * difference
}
let deviation = (squared_total / rows.length().to_double()).sqrt()
if deviation == 0.0 {
scales[column] = 1.0
constants[column] = true
} else {
scales[column] = deviation
}
}
{
scaler_kind: StandardScale,
offset_values: offsets,
scale_values: scales,
constant_columns: constants,
input_columns: columns,
}
}
///|
fn fit_minmax(data : Dataset, lower : Double, upper : Double) -> FeatureScaler {
let columns = data.feature_count()
let rows = data.features()
let offsets = Array::make(columns, 0.0)
let scales = Array::make(columns, 1.0)
let constants = Array::make(columns, false)
for column = 0; column < columns; column = column + 1 {
let mut minimum = rows[0][column]
let mut maximum = rows[0][column]
for row = 1; row < rows.length(); row = row + 1 {
minimum = minimum.min(rows[row][column])
maximum = maximum.max(rows[row][column])
}
offsets[column] = minimum
let width = maximum - minimum
if width == 0.0 {
constants[column] = true
} else {
scales[column] = width
}
}
{
scaler_kind: MinMaxScale(lower, upper),
offset_values: offsets,
scale_values: scales,
constant_columns: constants,
input_columns: columns,
}
}
///|
/// Fits preprocessing parameters from a validated training dataset.
pub fn fit_scaler(
data : Dataset,
kind : ScalerKind,
) -> Result[FeatureScaler, SvmError] {
match kind {
StandardScale => Ok(fit_standard(data))
MinMaxScale(lower, upper) => {
if !finite_double(lower) {
return Err(InvalidConfiguration("minmax_lower", lower))
}
if !finite_double(upper) || upper <= lower {
return Err(InvalidConfiguration("minmax_upper", upper))
}
Ok(fit_minmax(data, lower, upper))
}
}
}
///|
pub fn FeatureScaler::feature_count(self : FeatureScaler) -> Int {
self.input_columns
}
///|
pub fn FeatureScaler::kind(self : FeatureScaler) -> ScalerKind {
self.scaler_kind
}
///|
pub fn FeatureScaler::offsets(self : FeatureScaler) -> Array[Double] {
self.offset_values.copy()
}
///|
pub fn FeatureScaler::scales(self : FeatureScaler) -> Array[Double] {
self.scale_values.copy()
}
///|
fn FeatureScaler::validate_row(
self : FeatureScaler,
row : Array[Double],
) -> Result[Unit, SvmError] {
if row.length() != self.input_columns {
return Err(PredictionDimensionMismatch(self.input_columns, row.length()))
}
for column, value in row {
if !finite_double(value) {
return Err(NonFiniteFeature(0, column))
}
}
Ok(())
}
///|
pub fn FeatureScaler::transform_row(
self : FeatureScaler,
row : Array[Double],
) -> Result[Array[Double], SvmError] {
match self.validate_row(row) {
Err(error) => return Err(error)
Ok(_) => ()
}
let transformed = Array::make(self.input_columns, 0.0)
for column = 0; column < self.input_columns; column = column + 1 {
if self.constant_columns[column] {
transformed[column] = 0.0
} else {
let normalized = (row[column] - self.offset_values[column]) /
self.scale_values[column]
transformed[column] = match self.scaler_kind {
StandardScale => normalized
MinMaxScale(lower, upper) => lower + normalized * (upper - lower)
}
}
}
Ok(transformed)
}
///|
pub fn FeatureScaler::inverse_row(
self : FeatureScaler,
row : Array[Double],
) -> Result[Array[Double], SvmError] {
match self.validate_row(row) {
Err(error) => return Err(error)
Ok(_) => ()
}
let restored = Array::make(self.input_columns, 0.0)
for column = 0; column < self.input_columns; column = column + 1 {
if self.constant_columns[column] {
restored[column] = self.offset_values[column]
} else {
let normalized = match self.scaler_kind {
StandardScale => row[column]
MinMaxScale(lower, upper) => (row[column] - lower) / (upper - lower)
}
restored[column] = self.offset_values[column] +
normalized * self.scale_values[column]
}
}
Ok(restored)
}
///|
/// Applies fitted parameters to every row while preserving class labels.
pub fn FeatureScaler::transform_dataset(
self : FeatureScaler,
data : Dataset,
name : String,
) -> Result[Dataset, SvmError] {
if data.feature_count() != self.input_columns {
return Err(
PredictionDimensionMismatch(self.input_columns, data.feature_count()),
)
}
let transformed : Array[Array[Double]] = []
for row in data.features() {
match self.transform_row(row) {
Err(error) => return Err(error)
Ok(value) => transformed.push(value)
}
}
dataset(transformed, data.labels(), name)
}