use ndarray::{Array1, Array2, ArrayBase, Axis, Data, Ix1, Ix2, NdFloat};
use num_traits::{AsPrimitive, FromPrimitive};
pub trait Covariance<A> {
fn covariance(self, observation_axis: Axis) -> Array2<A>;
}
impl<S, A> Covariance<A> for ArrayBase<S, Ix2>
where
S: Data<Elem = A>,
A: FromPrimitive + NdFloat,
usize: AsPrimitive<A>,
{
fn covariance(self, observation_axis: Axis) -> Array2<A> {
assert!(
self.len_of(observation_axis) != 0,
"Cannot compute a covariance from zero observations"
);
let means = self.mean_axis(observation_axis).unwrap();
let mut centered = self.to_owned();
centered
.axis_iter_mut(observation_axis)
.for_each(|mut o| o -= &means);
let normalization = self.len_of(observation_axis).as_() - A::one();
if observation_axis == Axis(0) {
centered.t().dot(¢ered.map(|v| *v / normalization))
} else {
centered.dot(¢ered.t().map(|v| *v / normalization))
}
}
}
pub trait SquaredEuclideanDistance<A, D> {
type Output;
fn squared_euclidean_distance<S>(&self, other: ArrayBase<S, D>) -> Self::Output
where
S: Data<Elem = A>;
}
impl<A, S1> SquaredEuclideanDistance<A, Ix1> for ArrayBase<S1, Ix1>
where
A: NdFloat,
S1: Data<Elem = A>,
{
type Output = A;
#[allow(clippy::suspicious_operation_groupings)]
fn squared_euclidean_distance<S2>(&self, other: ArrayBase<S2, Ix1>) -> A
where
S2: Data<Elem = A>,
{
assert_eq!(
self.len(),
other.len(),
"Cannot compute (squared) euclidean distance of vectors with different lengths."
);
let self_sqn = self.dot(self);
let other_sqn = other.dot(&other);
let dp = self.dot(&other);
self_sqn + other_sqn - (dp + dp)
}
}
impl<A, S1> SquaredEuclideanDistance<A, Ix2> for ArrayBase<S1, Ix1>
where
A: NdFloat,
S1: Data<Elem = A>,
{
type Output = Array1<A>;
fn squared_euclidean_distance<S2>(&self, other: ArrayBase<S2, Ix2>) -> Self::Output
where
S2: Data<Elem = A>,
{
assert_eq!(
self.len(),
other.ncols(),
"Cannot compute (squared) euclidean distance when the number of vector components and matrix columns differ."
);
let self_sqn = self.dot(self);
let other_sqn: Array1<_> = other.outer_iter().map(|r| r.dot(&r)).collect();
let mut distances = other.dot(self);
for i in 0..distances.len() {
distances[i] = self_sqn + other_sqn[i] - (distances[i] + distances[i]);
}
distances
}
}
impl<A, S1> SquaredEuclideanDistance<A, Ix2> for ArrayBase<S1, Ix2>
where
A: NdFloat,
S1: Data<Elem = A>,
{
type Output = Array2<A>;
fn squared_euclidean_distance<S2>(&self, other: ArrayBase<S2, Ix2>) -> Self::Output
where
S2: Data<Elem = A>,
{
assert_eq!(
self.ncols(),
other.ncols(),
"Cannot compute (squared) euclidean distance of matrices with different numbers of columns."
);
let self_sqn: Array1<_> = self.outer_iter().map(|r| r.dot(&r)).collect();
let other_sqn: Array1<_> = other.outer_iter().map(|r| r.dot(&r)).collect();
let mut distances = self.dot(&other.t());
for i in 0..distances.nrows() {
for j in 0..distances.ncols() {
distances[(i, j)] =
self_sqn[i] + other_sqn[j] - (distances[(i, j)] + distances[(i, j)]);
}
}
distances
}
}
pub trait EuclideanDistance<A, D> {
type Output;
fn euclidean_distance<S>(&self, other: ArrayBase<S, D>) -> Self::Output
where
S: Data<Elem = A>;
}
impl<A, S1> EuclideanDistance<A, Ix1> for ArrayBase<S1, Ix1>
where
A: NdFloat,
S1: Data<Elem = A>,
{
type Output = A;
fn euclidean_distance<S2>(&self, other: ArrayBase<S2, Ix1>) -> A
where
S2: Data<Elem = A>,
{
self.squared_euclidean_distance(other).sqrt()
}
}
impl<A, S1> EuclideanDistance<A, Ix2> for ArrayBase<S1, Ix1>
where
A: NdFloat,
S1: Data<Elem = A>,
{
type Output = Array1<A>;
fn euclidean_distance<S>(&self, other: ArrayBase<S, Ix2>) -> Self::Output
where
S: Data<Elem = A>,
{
self.squared_euclidean_distance(other).mapv_into(A::sqrt)
}
}
impl<A, S1> EuclideanDistance<A, Ix2> for ArrayBase<S1, Ix2>
where
A: NdFloat,
S1: Data<Elem = A>,
{
type Output = Array2<A>;
fn euclidean_distance<S>(&self, other: ArrayBase<S, Ix2>) -> Self::Output
where
S: Data<Elem = A>,
{
self.squared_euclidean_distance(other).mapv_into(A::sqrt)
}
}
#[cfg(test)]
mod tests {
use ndarray::{array, Axis};
use super::{Covariance, EuclideanDistance, SquaredEuclideanDistance};
#[test]
fn covariance() {
let x = array![[0.0, 2.0], [1.0, 1.0], [2.0, 0.0]];
let cov = x.view().covariance(Axis(0));
assert_eq!(cov, array![[1., -1.], [-1., 1.]]);
let cov = x.t().covariance(Axis(1));
assert_eq!(cov, array![[1., -1.], [-1., 1.]]);
}
#[test]
fn euclidean_distance_ix1_ix1() {
let a = array![1., 2., 3.];
let b = array![0., 2., 0.];
assert_eq!(a.euclidean_distance(b), 10f32.sqrt());
}
#[test]
fn euclidean_distance_ix1_ix2() {
let a = array![1., 2., 3.];
let b = array![[2., 0., 0.], [0., 2., 0.], [0., 0., 2.]];
assert!(a
.euclidean_distance(b)
.abs_diff_eq(&array![14f32.sqrt(), 10f32.sqrt(), 6f32.sqrt()], 1e-6));
}
#[test]
fn euclidean_distance_ix2_ix2() {
let a = array![[1., 2., 3.], [3., 2., 1.]];
let b = array![[2., 0., 0.], [0., 2., 0.], [0., 0., 2.]];
assert!(a.euclidean_distance(b).abs_diff_eq(
&array![
[14f32.sqrt(), 10f32.sqrt(), 6f32.sqrt()],
[6f32.sqrt(), 10f32.sqrt(), 14f32.sqrt()]
],
1e-6
));
}
#[test]
fn squared_euclidean_distance_ix1_ix1() {
let a = array![1., 2., 3.];
let b = array![0., 2., 0.];
assert_eq!(a.squared_euclidean_distance(b), 10f32);
}
#[test]
fn squared_euclidean_distances_ix1_ix2() {
let a = array![1., 2., 3.];
let b = array![[2., 0., 0.], [0., 2., 0.], [0., 0., 2.]];
assert_eq!(a.squared_euclidean_distance(b), array![14., 10., 6.]);
}
#[test]
fn squared_euclidean_distances_ix2_ix2() {
let a = array![[1., 2., 3.], [3., 2., 1.]];
let b = array![[2., 0., 0.], [0., 2., 0.], [0., 0., 2.]];
assert_eq!(
a.squared_euclidean_distance(b),
array![[14., 10., 6.], [6.0, 10.0, 14.0]]
);
}
}