Skip to content

Commit

Permalink
feat(rad): implement median reducer
Browse files Browse the repository at this point in the history
  • Loading branch information
tmpolaczyk authored and lrubiorod committed Aug 31, 2021
1 parent a1785c4 commit 54e20c3
Show file tree
Hide file tree
Showing 5 changed files with 291 additions and 6 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions rad/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ if_rust_version = "1.0.0"
json = "0.12.1"
log = "0.4.8"
num_enum = "0.4.2"
ordered-float = "1.0"
rand = "0.7.3"
serde = "1.0.111"
serde_cbor = "0.11.1"
Expand Down
5 changes: 0 additions & 5 deletions rad/src/reducers/average.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,6 @@ pub fn mean(input: &RadonArray, return_policy: MeanReturnPolicy) -> Result<Radon
}
}

pub fn median(_input: &RadonArray) -> Result<RadonTypes, RadError> {
// TODO: implement
Ok(RadonTypes::Array(RadonArray::from(vec![])))
}

#[cfg(test)]
mod tests {
use serde_cbor::Value;
Expand Down
287 changes: 287 additions & 0 deletions rad/src/reducers/median.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
use crate::{
error::RadError,
operators::array as array_operators,
reducers::{average::mean, average::MeanReturnPolicy, RadonReducers},
types::{array::RadonArray, float::RadonFloat, RadonType, RadonTypes},
ReportContext,
};
use ordered_float::NotNan;

/// The median of a list of length N is the value at position floor(N/2) if N is odd,
/// and the average of the values at positions (N/2 - 1) and (N/2) if N is even.
///
/// The input will be sorted using the ArraySort operator.
/// The input must be an array of RadonIntegers or RadonFloats. Any RadonFloats set to NaN will be
/// ignored for this operation.
pub fn median(input: &RadonArray) -> Result<RadonTypes, RadError> {
let value = input.value();
let value_len = value.len();

match value.first() {
None => Err(RadError::ModeEmpty),
Some(RadonTypes::Float(_)) => {
// Collect non-NaN values into a vector, and sort them
let mut input_not_nan: Vec<NotNan<f64>> = Vec::with_capacity(value_len);

for item in value {
match item {
RadonTypes::Float(f64_value) => {
if let Ok(not_nan) = NotNan::new(f64_value.value()) {
input_not_nan.push(not_nan)
}
}
_ => {
return Err(RadError::MismatchingTypes {
method: RadonReducers::AverageMean.to_string(),
expected: RadonFloat::radon_type_name(),
found: item.radon_type_name(),
})
}
}
}

input_not_nan.sort();

if input_not_nan.is_empty() {
// This can happen if all elements are NaN
Err(RadError::ModeEmpty)
} else if input_not_nan.len() % 2 == 1 {
// Odd number of elements: take element at floor(N/2):
let median_pos = input_not_nan.len() / 2;
let median_elem = input_not_nan[median_pos].into_inner();

Ok(RadonTypes::Float(RadonFloat::from(median_elem)))
} else {
// Even number of elements: take average of element at (N/2 - 1) and N/2
let right_pos = input_not_nan.len() / 2;
let right_elem = input_not_nan[right_pos].into_inner();
let left_pos = right_pos - 1;
let left_elem = input_not_nan[left_pos].into_inner();

// Create new array to be able to use average::mean reducer
let rl = RadonArray::from(vec![
RadonTypes::Float(RadonFloat::from(left_elem)),
RadonTypes::Float(RadonFloat::from(right_elem)),
]);
// MeanReturnPolicy only applies to integers, so this will actually return a float
mean(&rl, MeanReturnPolicy::RoundToInteger)
}
}
Some(RadonTypes::Integer(_)) => {
let sorted_input =
match array_operators::sort(input, &[], &mut ReportContext::default()) {
Ok(RadonTypes::Array(arr)) => arr.value(),
Ok(_different_type) => unreachable!(),
Err(e) => return Err(e),
};

if sorted_input.is_empty() {
// This is unreachable
Err(RadError::ModeEmpty)
} else if sorted_input.len() % 2 == 1 {
// Odd number of elements: take element at floor(N/2):
let median_pos = sorted_input.len() / 2;

Ok(sorted_input[median_pos].clone())
} else {
// Even number of elements: take average of element at (N/2 - 1) and N/2
let right_pos = sorted_input.len() / 2;
let left_pos = right_pos - 1;

// Create new array to be able to use average::mean reducer
let rl = RadonArray::from(vec![
sorted_input[left_pos].clone(),
sorted_input[right_pos].clone(),
]);
// RoundToInteger means that when the average is not an integer, it will be rounded to an
// integer. For example, the average of 1 and 2, which is 1.5, will be rounded to 1.
mean(&rl, MeanReturnPolicy::RoundToInteger)
}
}
Some(_rad_types) => Err(RadError::UnsupportedReducer {
array: input.clone(),
reducer: RadonReducers::AverageMedian.to_string(),
}),
}
}

#[cfg(test)]
mod tests {
use super::*;

use crate::types::{float::RadonFloat, integer::RadonInteger, string::RadonString};
use crate::RadError::ModeEmpty;

#[test]
fn test_operate_reduce_median_empty() {
let input = RadonArray::from(vec![]);
let output = median(&input).unwrap_err();
let expected_error = ModeEmpty;
assert_eq!(output, expected_error);
}

#[test]
fn test_operate_reduce_median_float_odd() {
let input = RadonArray::from(vec![
RadonFloat::from(1f64).into(),
RadonFloat::from(2f64).into(),
RadonFloat::from(2f64).into(),
]);
let expected = RadonTypes::from(RadonFloat::from(2f64));
let output = median(&input).unwrap();
assert_eq!(output, expected);
}

#[test]
fn test_operate_reduce_median_float_even() {
let input = RadonArray::from(vec![
RadonFloat::from(1f64).into(),
RadonFloat::from(2f64).into(),
]);

let expected = RadonTypes::from(RadonFloat::from(1.5f64));
let output = median(&input).unwrap();
assert_eq!(output, expected);
}

#[test]
fn test_operate_reduce_median_float_with_nans() {
let input = RadonArray::from(vec![
RadonFloat::from(1f64).into(),
RadonFloat::from(2f64).into(),
RadonFloat::from(f64::NAN).into(),
]);

let expected = RadonTypes::from(RadonFloat::from(1.5f64));
let output = median(&input).unwrap();
assert_eq!(output, expected);
}

#[test]
fn test_operate_reduce_median_float_all_nans() {
let input = RadonArray::from(vec![
RadonFloat::from(f64::NAN).into(),
RadonFloat::from(f64::NAN).into(),
]);

let output = median(&input).unwrap_err();
let expected_error = ModeEmpty;
assert_eq!(output, expected_error);
}

#[test]
fn test_operate_reduce_median_int_odd() {
let input = RadonArray::from(vec![
RadonInteger::from(1i128).into(),
RadonInteger::from(2i128).into(),
RadonInteger::from(2i128).into(),
]);
let expected = RadonTypes::from(RadonInteger::from(2i128));
let output = median(&input).unwrap();
assert_eq!(output, expected);
}

#[test]
fn test_operate_reduce_median_int_even() {
// The median should be 1.5, but it is rounded to 2
let input = RadonArray::from(vec![
RadonInteger::from(1i128).into(),
RadonInteger::from(2i128).into(),
]);
let output = median(&input).unwrap();
let expected = RadonTypes::from(RadonInteger::from(2i128));
assert_eq!(output, expected);
}

#[test]
fn test_operate_reduce_median_int_unsorted_input() {
let input = RadonArray::from(vec![
RadonInteger::from(1i128).into(),
RadonInteger::from(1i128).into(),
RadonInteger::from(5i128).into(),
RadonInteger::from(5i128).into(),
RadonInteger::from(3i128).into(),
]);
let expected = RadonTypes::from(RadonInteger::from(3i128));
let output = median(&input).unwrap();
assert_eq!(output, expected);
}

#[test]
fn test_operate_reduce_median_str_odd() {
let input = RadonArray::from(vec![
RadonString::from("Bye world!").into(),
RadonString::from("Hello world!").into(),
RadonString::from("Hello world!").into(),
]);
let output = median(&input).unwrap_err();
let expected_error = RadError::UnsupportedReducer {
array: input,
reducer: "RadonReducers::AverageMedian".to_string(),
};
assert_eq!(output, expected_error);
}

#[test]
fn test_operate_reduce_median_str_even() {
// When the number of elements is even, the median is element at index (N/2 - 1)
let input = RadonArray::from(vec![
RadonString::from("Bye world!").into(),
RadonString::from("Hello world!").into(),
]);
let output = median(&input).unwrap_err();
let expected_error = RadError::UnsupportedReducer {
array: input,
reducer: "RadonReducers::AverageMedian".to_string(),
};
assert_eq!(output, expected_error);
}

#[test]
fn test_operate_reduce_median_array() {
let array1 = RadonArray::from(vec![
RadonInteger::from(1i128).into(),
RadonInteger::from(2i128).into(),
RadonInteger::from(3i128).into(),
]);
let array2 = RadonArray::from(vec![
RadonInteger::from(2i128).into(),
RadonInteger::from(5i128).into(),
RadonInteger::from(4i128).into(),
]);
let array3 = RadonArray::from(vec![
RadonInteger::from(1i128).into(),
RadonInteger::from(2i128).into(),
RadonInteger::from(3i128).into(),
RadonInteger::from(4i128).into(),
]);

let input = RadonArray::from(vec![
array1.clone().into(),
array1.clone().into(),
array1.into(),
array2.into(),
array3.into(),
]);

let output = median(&input).unwrap_err();
let expected_error = RadError::UnsupportedReducer {
array: input,
reducer: "RadonReducers::AverageMedian".to_string(),
};
assert_eq!(output, expected_error);
}

#[test]
fn test_median_big_number() {
let input = RadonArray::from(vec![
RadonInteger::from(18446744073709551616).into(),
RadonInteger::from(18446744073709551616).into(),
RadonInteger::from(2).into(),
]);

let expected = RadonTypes::from(RadonInteger::from(18446744073709551616));
let output = median(&input).unwrap();
assert_eq!(output, expected);
}
}
3 changes: 2 additions & 1 deletion rad/src/reducers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use witnet_data_structures::radon_report::ReportContext;

pub mod average;
pub mod deviation;
pub mod median;
pub mod mode;

#[derive(Debug, PartialEq, TryFromPrimitive)]
Expand Down Expand Up @@ -57,7 +58,7 @@ pub fn reduce(
RadonReducers::Mode => mode::mode(input),
RadonReducers::DeviationStandard => deviation::standard(input),
RadonReducers::AverageMedian => match &context.active_wips {
Some(active_wips) if active_wips.wip0017() => average::median(input),
Some(active_wips) if active_wips.wip0017() => median::median(input),
_ => error(),
},
_ => error(),
Expand Down

0 comments on commit 54e20c3

Please sign in to comment.