From 71db9b1d526488ffb8736252e8b80ad50cf65f1f Mon Sep 17 00:00:00 2001 From: NicklasXYZ <18580183+NicklasXYZ@users.noreply.github.com> Date: Wed, 10 Apr 2024 23:28:28 +0200 Subject: [PATCH 1/6] fix link --- src/gleam_community/maths/metrics.gleam | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/gleam_community/maths/metrics.gleam b/src/gleam_community/maths/metrics.gleam index d5ddc69..237b900 100644 --- a/src/gleam_community/maths/metrics.gleam +++ b/src/gleam_community/maths/metrics.gleam @@ -38,6 +38,7 @@ //// * [`sorensen_dice_coefficient`](#sorensen_dice_coefficient) //// * [`tversky_index`](#tversky_index) //// * [`overlap_coefficient`](#overlap_coefficient) +//// * [`levenshtein_distance`](#levenshtein_distance) //// * **Basic statistical measures** //// * [`mean`](#mean) //// * [`median`](#median) @@ -1014,8 +1015,8 @@ pub fn cosine_similarity( /// - deletions /// - substitutions /// -/// Note: The implementation is primarily based on the elixir implementation -/// [https://hex.pm/packages/levenshtein](levenshtein). +/// Note: The implementation is primarily based on the Elixir implementation +/// [levenshtein](https://hex.pm/packages/levenshtein). /// ///
/// Example: From d9c642062a1c3cc1007f41c47b268f902b0de8a2 Mon Sep 17 00:00:00 2001 From: NicklasXYZ <18580183+NicklasXYZ@users.noreply.github.com> Date: Wed, 10 Apr 2024 23:32:42 +0200 Subject: [PATCH 2/6] Run gleam update --- manifest.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.toml b/manifest.toml index 6c5a157..4a7c920 100644 --- a/manifest.toml +++ b/manifest.toml @@ -3,7 +3,7 @@ packages = [ { name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" }, - { name = "gleeunit", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D364C87AFEB26BDB4FB8A5ABDE67D635DC9FA52D6AB68416044C35B096C6882D" }, + { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, ] [requirements] From b7f7b29e48274b98136519e4b076812eb06acda8 Mon Sep 17 00:00:00 2001 From: NicklasXYZ <18580183+NicklasXYZ@users.noreply.github.com> Date: Sat, 13 Apr 2024 23:38:39 +0200 Subject: [PATCH 3/6] Add canberra and bary-curtis distance --- src/gleam_community/maths/arithmetics.gleam | 77 +++-- src/gleam_community/maths/metrics.gleam | 309 +++++++++++++----- .../maths/arithmetics_test.gleam | 19 +- test/gleam_community/maths/metrics_test.gleam | 165 ++++++++-- 4 files changed, 432 insertions(+), 138 deletions(-) diff --git a/src/gleam_community/maths/arithmetics.gleam b/src/gleam_community/maths/arithmetics.gleam index dbd3ffe..b37a067 100644 --- a/src/gleam_community/maths/arithmetics.gleam +++ b/src/gleam_community/maths/arithmetics.gleam @@ -44,6 +44,9 @@ import gleam/int import gleam/list +import gleam/option +import gleam/pair +import gleam/result import gleam_community/maths/conversion import gleam_community/maths/elementary import gleam_community/maths/piecewise @@ -289,29 +292,32 @@ pub fn proper_divisors(n: Int) -> List(Int) { /// /// /// -/// Calculate the sum of the elements in a list: +/// Calculate the (weighted) sum of the elements in a list: /// /// \\[ -/// \sum_{i=1}^n x_i +/// \sum_{i=1}^n w_i x_i /// \\] /// -/// In the formula, $$n$$ is the length of the list and $$x_i \in \mathbb{R}$$ is the value in the input list indexed by $$i$$. +/// In the formula, $$n$$ is the length of the list and $$x_i \in \mathbb{R}$$ is +/// the value in the input list indexed by $$i$$, while $$w_i \in \mathbb{R}$$ is +/// a corresponding weight ($$w_i = 1.0\;\forall i=1...n$$ by default). /// ///
/// Example: /// /// import gleeunit/should +/// import gleam/option /// import gleam_community/maths/arithmetics /// /// pub fn example () { /// // An empty list returns an error /// [] -/// |> arithmetics.float_sum() +/// |> arithmetics.float_sum(option.None) /// |> should.equal(0.0) /// /// // Valid input returns a result /// [1.0, 2.0, 3.0] -/// |> arithmetics.float_sum() +/// |> arithmetics.float_sum(option.None) /// |> should.equal(6.0) /// } ///
@@ -322,12 +328,18 @@ pub fn proper_divisors(n: Int) -> List(Int) { /// /// /// -pub fn float_sum(arr: List(Float)) -> Float { - case arr { - [] -> 0.0 - _ -> +pub fn float_sum(arr: List(Float), weights: option.Option(List(Float))) -> Float { + case arr, weights { + [], _ -> 0.0 + _, option.None -> arr |> list.fold(0.0, fn(acc: Float, a: Float) -> Float { a +. acc }) + _, option.Some(warr) -> { + list.zip(arr, warr) + |> list.fold(0.0, fn(acc: Float, a: #(Float, Float)) -> Float { + pair.first(a) *. pair.second(a) +. acc + }) + } } } @@ -385,29 +397,32 @@ pub fn int_sum(arr: List(Int)) -> Int { /// /// /// -/// Calculate the product of the elements in a list: +/// Calculate the (weighted) product of the elements in a list: /// /// \\[ -/// \prod_{i=1}^n x_i +/// \prod_{i=1}^n x_i^{w_i} /// \\] /// -/// In the formula, $$n$$ is the length of the list and $$x_i \in \mathbb{R}$$ is the value in the input list indexed by $$i$$. -/// +/// In the formula, $$n$$ is the length of the list and $$x_i \in \mathbb{R}$$ is +/// the value in the input list indexed by $$i$$, while $$w_i \in \mathbb{R}$$ is +/// a corresponding weight ($$w_i = 1.0\;\forall i=1...n$$ by default). +/// ///
/// Example: /// /// import gleeunit/should +/// import gleam/option /// import gleam_community/maths/arithmetics /// /// pub fn example () { /// // An empty list returns 0.0 /// [] -/// |> arithmetics.float_product() +/// |> arithmetics.float_product(option.None) /// |> should.equal(0.0) /// /// // Valid input returns a result /// [1.0, 2.0, 3.0] -/// |> arithmetics.float_product() +/// |> arithmetics.float_product(option.None) /// |> should.equal(6.0) /// } ///
@@ -418,12 +433,36 @@ pub fn int_sum(arr: List(Int)) -> Int { /// /// /// -pub fn float_product(arr: List(Float)) -> Float { - case arr { - [] -> 1.0 - _ -> +pub fn float_product( + arr: List(Float), + weights: option.Option(List(Float)), +) -> Result(Float, String) { + case arr, weights { + [], _ -> + 1.0 + |> Ok + _, option.None -> arr |> list.fold(1.0, fn(acc: Float, a: Float) -> Float { a *. acc }) + |> Ok + _, option.Some(warr) -> { + let results = + list.zip(arr, warr) + |> list.map(fn(a: #(Float, Float)) -> Result(Float, String) { + pair.first(a) + |> elementary.power(pair.second(a)) + }) + |> result.all + case results { + Ok(prods) -> + prods + |> list.fold(1.0, fn(acc: Float, a: Float) -> Float { a *. acc }) + |> Ok + Error(msg) -> + msg + |> Error + } + } } } diff --git a/src/gleam_community/maths/metrics.gleam b/src/gleam_community/maths/metrics.gleam index 237b900..84cb8b2 100644 --- a/src/gleam_community/maths/metrics.gleam +++ b/src/gleam_community/maths/metrics.gleam @@ -33,6 +33,8 @@ //// * [`chebyshev_distance`](#chebyshev_distance) //// * [`minkowski_distance`](#minkowski_distance) //// * [`cosine_similarity`](#cosine_similarity) +//// * [`canberra_distance`](#canberra_distance) +//// * [`braycurtis_distance`](#braycurtis_distance) //// * **Set & string similarity measures** //// * [`jaccard_index`](#jaccard_index) //// * [`sorensen_dice_coefficient`](#sorensen_dice_coefficient) @@ -57,6 +59,48 @@ import gleam/set import gleam/float import gleam/int import gleam/string +import gleam/option + +/// Utility function that checks all lists have the expected length +/// Primarily used by all distance measures taking List(Float) as input +fn check_lists( + xarr: List(Float), + yarr: List(Float), + weights: option.Option(List(Float)), +) -> Result(Bool, String) { + case xarr, yarr { + [], _ -> + "Invalid input argument: The list xarr is empty." + |> Error + _, [] -> + "Invalid input argument: The list yarr is empty." + |> Error + _, _ -> { + let xlen: Int = list.length(xarr) + let ylen: Int = list.length(yarr) + case xlen == ylen, weights { + False, _ -> + "Invalid input argument: length(xarr) != length(yarr). Valid input is when length(xarr) == length(yarr)." + |> Error + True, option.None -> { + True + |> Ok + } + True, option.Some(warr) -> { + let wlen: Int = list.length(warr) + case xlen == wlen { + True -> + True + |> Ok + False -> + "Invalid input argument: length(weights) != length(xarr) and length(weights) != length(yarr). Valid input is when length(weights) == length(xarr) == length(yarr)." + |> Error + } + } + } + } + } +} ///
/// @@ -169,8 +213,9 @@ pub fn norm(arr: List(Float), p: Float) -> Float { pub fn manhattan_distance( xarr: List(Float), yarr: List(Float), + weights: option.Option(List(Float)), ) -> Result(Float, String) { - minkowski_distance(xarr, yarr, 1.0) + minkowski_distance(xarr, yarr, 1.0, weights) } ///
@@ -231,34 +276,24 @@ pub fn minkowski_distance( xarr: List(Float), yarr: List(Float), p: Float, + weights: option.Option(List(Float)), ) -> Result(Float, String) { - case xarr, yarr { - [], _ -> - "Invalid input argument: The list xarr is empty." + case check_lists(xarr, yarr, weights) { + Error(msg) -> + msg |> Error - _, [] -> - "Invalid input argument: The list yarr is empty." - |> Error - _, _ -> { - let xlen: Int = list.length(xarr) - let ylen: Int = list.length(yarr) - case xlen == ylen { - False -> - "Invalid input argument: length(xarr) != length(yarr). Valid input is when length(xarr) == length(yarr)." - |> Error + Ok(_) -> { + case p <. 1.0 { True -> - case p <. 1.0 { - True -> - "Invalid input argument: p < 1. Valid input is p >= 1." - |> Error - False -> - list.zip(xarr, yarr) - |> list.map(fn(tuple: #(Float, Float)) -> Float { - pair.first(tuple) -. pair.second(tuple) - }) - |> norm(p) - |> Ok - } + "Invalid input argument: p < 1. Valid input is p >= 1." + |> Error + False -> + list.zip(xarr, yarr) + |> list.map(fn(tuple: #(Float, Float)) -> Float { + pair.first(tuple) -. pair.second(tuple) + }) + |> norm(p) + |> Ok } } } @@ -314,8 +349,9 @@ pub fn minkowski_distance( pub fn euclidean_distance( xarr: List(Float), yarr: List(Float), + weights: option.Option(List(Float)), ) -> Result(Float, String) { - minkowski_distance(xarr, yarr, 2.0) + minkowski_distance(xarr, yarr, 2.0, weights) } ///
@@ -364,32 +400,21 @@ pub fn euclidean_distance( pub fn chebyshev_distance( xarr: List(Float), yarr: List(Float), + weights: option.Option(List(Float)), ) -> Result(Float, String) { - case xarr, yarr { - [], _ -> - "Invalid input argument: The list xarr is empty." + case check_lists(xarr, yarr, weights) { + Error(msg) -> + msg |> Error - _, [] -> - "Invalid input argument: The list yarr is empty." - |> Error - _, _ -> { - let xlen: Int = list.length(xarr) - let ylen: Int = list.length(yarr) - case xlen == ylen { - False -> - "Invalid input argument: length(xarr) != length(yarr). Valid input is when length(xarr) == length(yarr)." - |> Error - True -> { - let differences = - list.zip(xarr, yarr) - |> list.map(fn(tuple: #(Float, Float)) -> Float { - { pair.first(tuple) -. pair.second(tuple) } - |> piecewise.float_absolute_value() - }) - differences - |> piecewise.list_maximum(float.compare) - } - } + Ok(_) -> { + let differences = + list.zip(xarr, yarr) + |> list.map(fn(tuple: #(Float, Float)) -> Float { + { pair.first(tuple) -. pair.second(tuple) } + |> piecewise.float_absolute_value() + }) + differences + |> piecewise.list_maximum(float.compare) } } } @@ -441,7 +466,7 @@ pub fn mean(arr: List(Float)) -> Result(Float, String) { |> Error _ -> arr - |> arithmetics.float_sum() + |> arithmetics.float_sum(option.None) |> fn(a: Float) -> Float { a /. conversion.int_to_float(list.length(arr)) } @@ -579,7 +604,7 @@ pub fn variance(arr: List(Float), ddof: Int) -> Result(Float, String) { let assert Ok(result) = elementary.power(a -. mean, 2.0) result }) - |> arithmetics.float_sum() + |> arithmetics.float_sum(option.None) |> fn(a: Float) -> Float { a /. { @@ -969,34 +994,23 @@ pub fn overlap_coefficient(xset: set.Set(a), yset: set.Set(a)) -> Float { pub fn cosine_similarity( xarr: List(Float), yarr: List(Float), + weights: option.Option(List(Float)), ) -> Result(Float, String) { - case xarr, yarr { - [], _ -> - "Invalid input argument: The list xarr is empty." - |> Error - _, [] -> - "Invalid input argument: The list yarr is empty." + case check_lists(xarr, yarr, weights) { + Error(msg) -> + msg |> Error - _, _ -> { - let xlen: Int = list.length(xarr) - let ylen: Int = list.length(yarr) - case xlen == ylen { - False -> - "Invalid input argument: length(xarr) != length(yarr). Valid input is when length(xarr) == length(yarr)." - |> Error - True -> { - list.fold( - list.zip(xarr, yarr), - 0.0, - fn(acc: Float, a: #(Float, Float)) -> Float { - let result: Float = pair.first(a) *. pair.second(a) - result +. acc - }, - ) - /. { norm(xarr, 2.0) *. norm(yarr, 2.0) } - |> Ok - } - } + Ok(_) -> { + list.fold( + list.zip(xarr, yarr), + 0.0, + fn(acc: Float, a: #(Float, Float)) -> Float { + let result: Float = pair.first(a) *. pair.second(a) + result +. acc + }, + ) + /. { norm(xarr, 2.0) *. norm(yarr, 2.0) } + |> Ok } } } @@ -1119,3 +1133,140 @@ fn distance_list_helper( } } } + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths/metrics +/// +/// pub fn example () { +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +/// +pub fn canberra_distance( + xarr: List(Float), + yarr: List(Float), + weights: option.Option(List(Float)), +) -> Result(Float, String) { + case check_lists(xarr, yarr, weights) { + Error(msg) -> + msg + |> Error + Ok(_) -> { + let arr: List(Float) = + list.zip(xarr, yarr) + |> list.map(canberra_distance_helper) + case weights { + option.None -> { + arr + |> arithmetics.float_sum(option.None) + |> Ok + } + _ -> { + arr + |> arithmetics.float_sum(weights) + |> Ok + } + } + } + } +} + +fn canberra_distance_helper(tuple: #(Float, Float)) -> Float { + let numerator: Float = + piecewise.float_absolute_value({ pair.first(tuple) -. pair.second(tuple) }) + let denominator: Float = { + piecewise.float_absolute_value(pair.first(tuple)) + +. piecewise.float_absolute_value(pair.second(tuple)) + } + numerator /. denominator +} + +///
+/// +/// Spot a typo? Open an issue! +/// +///
+/// +///
+/// Example: +/// +/// import gleeunit/should +/// import gleam_community/maths/metrics +/// +/// pub fn example () { +/// } +///
+/// +///
+/// +/// Back to top ↑ +/// +///
+/// +/// +pub fn braycurtis_distance( + xarr: List(Float), + yarr: List(Float), + weights: option.Option(List(Float)), +) -> Result(Float, String) { + case check_lists(xarr, yarr, weights) { + Error(msg) -> + msg + |> Error + Ok(_) -> { + let zipped_arr: List(#(Float, Float)) = list.zip(xarr, yarr) + let numerator_elements: List(Float) = + zipped_arr + |> list.map(fn(tuple: #(Float, Float)) -> Float { + piecewise.float_absolute_value({ + pair.first(tuple) -. pair.second(tuple) + }) + }) + let denominator_elements: List(Float) = + zipped_arr + |> list.map(fn(tuple: #(Float, Float)) -> Float { + piecewise.float_absolute_value({ + pair.first(tuple) +. pair.second(tuple) + }) + }) + + case weights { + option.None -> { + let numerator = + numerator_elements + |> arithmetics.float_sum(option.None) + let denominator = + denominator_elements + |> arithmetics.float_sum(option.None) + { numerator /. denominator } + |> Ok + } + _ -> { + let numerator = + numerator_elements + |> arithmetics.float_sum(weights) + let denominator = + denominator_elements + |> arithmetics.float_sum(weights) + { numerator /. denominator } + |> Ok + } + } + } + } +} diff --git a/test/gleam_community/maths/arithmetics_test.gleam b/test/gleam_community/maths/arithmetics_test.gleam index aa694b7..7ec5ad5 100644 --- a/test/gleam_community/maths/arithmetics_test.gleam +++ b/test/gleam_community/maths/arithmetics_test.gleam @@ -1,5 +1,6 @@ import gleam_community/maths/arithmetics import gleeunit/should +import gleam/option pub fn int_gcd_test() { arithmetics.gcd(1, 1) @@ -100,16 +101,16 @@ pub fn int_divisors_test() { pub fn float_list_sum_test() { // An empty list returns 0 [] - |> arithmetics.float_sum() + |> arithmetics.float_sum(option.None) |> should.equal(0.0) // Valid input returns a result [1.0, 2.0, 3.0] - |> arithmetics.float_sum() + |> arithmetics.float_sum(option.None) |> should.equal(6.0) [-2.0, 4.0, 6.0] - |> arithmetics.float_sum() + |> arithmetics.float_sum(option.None) |> should.equal(8.0) } @@ -132,17 +133,17 @@ pub fn int_list_sum_test() { pub fn float_list_product_test() { // An empty list returns 0 [] - |> arithmetics.float_product() - |> should.equal(1.0) + |> arithmetics.float_product(option.None) + |> should.equal(Ok(1.0)) // Valid input returns a result [1.0, 2.0, 3.0] - |> arithmetics.float_product() - |> should.equal(6.0) + |> arithmetics.float_product(option.None) + |> should.equal(Ok(6.0)) [-2.0, 4.0, 6.0] - |> arithmetics.float_product() - |> should.equal(-48.0) + |> arithmetics.float_product(option.None) + |> should.equal(Ok(-48.0)) } pub fn int_list_product_test() { diff --git a/test/gleam_community/maths/metrics_test.gleam b/test/gleam_community/maths/metrics_test.gleam index e5888a0..3ccf758 100644 --- a/test/gleam_community/maths/metrics_test.gleam +++ b/test/gleam_community/maths/metrics_test.gleam @@ -3,6 +3,7 @@ import gleam_community/maths/metrics import gleam_community/maths/predicates import gleeunit/should import gleam/set +import gleam/option pub fn float_list_norm_test() { let assert Ok(tol) = elementary.power(-10.0, -6.0) @@ -54,15 +55,16 @@ pub fn float_list_manhattan_test() { let assert Ok(tol) = elementary.power(-10.0, -6.0) // Empty lists returns an error - metrics.manhattan_distance([], []) + metrics.manhattan_distance([], [], option.None) |> should.be_error() // Differing lengths returns error - metrics.manhattan_distance([], [1.0]) + metrics.manhattan_distance([], [1.0], option.None) |> should.be_error() // manhattan distance (p = 1) - let assert Ok(result) = metrics.manhattan_distance([0.0, 0.0], [1.0, 2.0]) + let assert Ok(result) = + metrics.manhattan_distance([0.0, 0.0], [1.0, 2.0], option.None) result |> predicates.is_close(3.0, 0.0, tol) |> should.be_true() @@ -72,53 +74,53 @@ pub fn float_list_minkowski_test() { let assert Ok(tol) = elementary.power(-10.0, -6.0) // Empty lists returns an error - metrics.minkowski_distance([], [], 1.0) + metrics.minkowski_distance([], [], 1.0, option.None) |> should.be_error() // Differing lengths returns error - metrics.minkowski_distance([], [1.0], 1.0) + metrics.minkowski_distance([], [1.0], 1.0, option.None) |> should.be_error() // Test order < 1 - metrics.minkowski_distance([0.0, 0.0], [0.0, 0.0], -1.0) + metrics.minkowski_distance([0.0, 0.0], [0.0, 0.0], -1.0, option.None) |> should.be_error() // Check that the function agrees, at some arbitrary input // points, with known function values let assert Ok(result) = - metrics.minkowski_distance([1.0, 1.0], [1.0, 1.0], 1.0) + metrics.minkowski_distance([1.0, 1.0], [1.0, 1.0], 1.0, option.None) result |> predicates.is_close(0.0, 0.0, tol) |> should.be_true() let assert Ok(result) = - metrics.minkowski_distance([0.0, 0.0], [1.0, 1.0], 10.0) + metrics.minkowski_distance([0.0, 0.0], [1.0, 1.0], 10.0, option.None) result |> predicates.is_close(1.0717734625362931, 0.0, tol) |> should.be_true() let assert Ok(result) = - metrics.minkowski_distance([0.0, 0.0], [1.0, 1.0], 100.0) + metrics.minkowski_distance([0.0, 0.0], [1.0, 1.0], 100.0, option.None) result |> predicates.is_close(1.0069555500567189, 0.0, tol) |> should.be_true() let assert Ok(result) = - metrics.minkowski_distance([0.0, 0.0], [1.0, 1.0], 10.0) + metrics.minkowski_distance([0.0, 0.0], [1.0, 1.0], 10.0, option.None) result |> predicates.is_close(1.0717734625362931, 0.0, tol) |> should.be_true() // Euclidean distance (p = 2) let assert Ok(result) = - metrics.minkowski_distance([0.0, 0.0], [1.0, 2.0], 2.0) + metrics.minkowski_distance([0.0, 0.0], [1.0, 2.0], 2.0, option.None) result |> predicates.is_close(2.23606797749979, 0.0, tol) |> should.be_true() // Manhattan distance (p = 1) let assert Ok(result) = - metrics.minkowski_distance([0.0, 0.0], [1.0, 2.0], 1.0) + metrics.minkowski_distance([0.0, 0.0], [1.0, 2.0], 1.0, option.None) result |> predicates.is_close(3.0, 0.0, tol) |> should.be_true() @@ -128,15 +130,16 @@ pub fn float_list_euclidean_test() { let assert Ok(tol) = elementary.power(-10.0, -6.0) // Empty lists returns an error - metrics.euclidean_distance([], []) + metrics.euclidean_distance([], [], option.None) |> should.be_error() // Differing lengths returns error - metrics.euclidean_distance([], [1.0]) + metrics.euclidean_distance([], [1.0], option.None) |> should.be_error() // Euclidean distance (p = 2) - let assert Ok(result) = metrics.euclidean_distance([0.0, 0.0], [1.0, 2.0]) + let assert Ok(result) = + metrics.euclidean_distance([0.0, 0.0], [1.0, 2.0], option.None) result |> predicates.is_close(2.23606797749979, 0.0, tol) |> should.be_true() @@ -266,65 +269,69 @@ pub fn overlap_coefficient_test() { pub fn cosine_similarity_test() { // Empty lists returns an error - metrics.cosine_similarity([], []) + metrics.cosine_similarity([], [], option.None) |> should.be_error() // One empty list returns an error - metrics.cosine_similarity([1.0, 2.0, 3.0], []) + metrics.cosine_similarity([1.0, 2.0, 3.0], [], option.None) |> should.be_error() // One empty list returns an error - metrics.cosine_similarity([], [1.0, 2.0, 3.0]) + metrics.cosine_similarity([], [1.0, 2.0, 3.0], option.None) |> should.be_error() // Different sized lists returns an error - metrics.cosine_similarity([1.0, 2.0], [1.0, 2.0, 3.0, 4.0]) + metrics.cosine_similarity([1.0, 2.0], [1.0, 2.0, 3.0, 4.0], option.None) |> should.be_error() // Two orthogonal vectors (represented by lists) - metrics.cosine_similarity([-1.0, 1.0, 0.0], [1.0, 1.0, -1.0]) + metrics.cosine_similarity([-1.0, 1.0, 0.0], [1.0, 1.0, -1.0], option.None) |> should.equal(Ok(0.0)) // Two identical (parallel) vectors (represented by lists) - metrics.cosine_similarity([1.0, 2.0, 3.0], [1.0, 2.0, 3.0]) + metrics.cosine_similarity([1.0, 2.0, 3.0], [1.0, 2.0, 3.0], option.None) |> should.equal(Ok(1.0)) // Two parallel, but oppositely oriented vectors (represented by lists) - metrics.cosine_similarity([-1.0, -2.0, -3.0], [1.0, 2.0, 3.0]) + metrics.cosine_similarity([-1.0, -2.0, -3.0], [1.0, 2.0, 3.0], option.None) |> should.equal(Ok(-1.0)) } pub fn chebyshev_distance_test() { // Empty lists returns an error - metrics.chebyshev_distance([], []) + metrics.chebyshev_distance([], [], option.None) |> should.be_error() // One empty list returns an error - metrics.chebyshev_distance([1.0, 2.0, 3.0], []) + metrics.chebyshev_distance([1.0, 2.0, 3.0], [], option.None) |> should.be_error() // One empty list returns an error - metrics.chebyshev_distance([], [1.0, 2.0, 3.0]) + metrics.chebyshev_distance([], [1.0, 2.0, 3.0], option.None) |> should.be_error() // Different sized lists returns an error - metrics.chebyshev_distance([1.0, 2.0], [1.0, 2.0, 3.0, 4.0]) + metrics.chebyshev_distance([1.0, 2.0], [1.0, 2.0, 3.0, 4.0], option.None) |> should.be_error() // Try different types of valid input - metrics.chebyshev_distance([1.0, 0.0], [0.0, 2.0]) + metrics.chebyshev_distance([1.0, 0.0], [0.0, 2.0], option.None) |> should.equal(Ok(2.0)) - metrics.chebyshev_distance([1.0, 0.0], [2.0, 0.0]) + metrics.chebyshev_distance([1.0, 0.0], [2.0, 0.0], option.None) |> should.equal(Ok(1.0)) - metrics.chebyshev_distance([1.0, 0.0], [-2.0, 0.0]) + metrics.chebyshev_distance([1.0, 0.0], [-2.0, 0.0], option.None) |> should.equal(Ok(3.0)) - metrics.chebyshev_distance([-5.0, -10.0, -3.0], [-1.0, -12.0, -3.0]) + metrics.chebyshev_distance( + [-5.0, -10.0, -3.0], + [-1.0, -12.0, -3.0], + option.None, + ) |> should.equal(Ok(4.0)) - metrics.chebyshev_distance([1.0, 2.0, 3.0], [1.0, 2.0, 3.0]) + metrics.chebyshev_distance([1.0, 2.0, 3.0], [1.0, 2.0, 3.0], option.None) |> should.equal(Ok(0.0)) } @@ -367,3 +374,99 @@ pub fn levenshtein_distance_test() { ) |> should.equal(10) } + +pub fn canberra_distance_test() { + // Empty lists returns an error + metrics.canberra_distance([], [], option.None) + |> should.be_error() + + // One empty list returns an error + metrics.canberra_distance([1.0, 2.0, 3.0], [], option.None) + |> should.be_error() + + // One empty list returns an error + metrics.canberra_distance([], [1.0, 2.0, 3.0], option.None) + |> should.be_error() + + // Different sized lists returns an error + metrics.canberra_distance([1.0, 2.0], [1.0, 2.0, 3.0, 4.0], option.None) + |> should.be_error() + + // Try different types of valid input + metrics.canberra_distance([0.0, 0.0], [0.0, 0.0], option.None) + |> should.equal(Ok(0.0)) + + metrics.canberra_distance([1.0, 2.0], [-2.0, -1.0], option.None) + |> should.equal(Ok(2.0)) + + metrics.canberra_distance([1.0, 0.0], [0.0, 2.0], option.None) + |> should.equal(Ok(2.0)) + + metrics.canberra_distance([1.0, 0.0], [2.0, 0.0], option.None) + |> should.equal(Ok(1.0 /. 3.0)) + + metrics.canberra_distance([1.0, 0.0], [0.0, 2.0], option.Some([1.0, 1.0])) + |> should.equal(Ok(2.0)) + + metrics.canberra_distance([1.0, 0.0], [0.0, 2.0], option.Some([1.0, 0.5])) + |> should.equal(Ok(1.5)) + + metrics.canberra_distance([1.0, 0.0], [0.0, 2.0], option.Some([0.5, 0.5])) + |> should.equal(Ok(1.0)) + + // Different sized lists (weights) returns an error + metrics.canberra_distance( + [1.0, 2.0, 3.0], + [1.0, 2.0, 3.0], + option.Some([1.0]), + ) + |> should.be_error() +} + +pub fn braycurtis_distance_test() { + // Empty lists returns an error + metrics.braycurtis_distance([], [], option.None) + |> should.be_error() + + // One empty list returns an error + metrics.braycurtis_distance([1.0, 2.0, 3.0], [], option.None) + |> should.be_error() + + // One empty list returns an error + metrics.braycurtis_distance([], [1.0, 2.0, 3.0], option.None) + |> should.be_error() + + // Different sized lists returns an error + metrics.braycurtis_distance([1.0, 2.0], [1.0, 2.0, 3.0, 4.0], option.None) + |> should.be_error() + + // Try different types of valid input + metrics.braycurtis_distance([0.0, 0.0], [0.0, 0.0], option.None) + |> should.equal(Ok(0.0)) + + metrics.braycurtis_distance([1.0, 2.0], [-2.0, -1.0], option.None) + |> should.equal(Ok(3.0)) + + metrics.braycurtis_distance([1.0, 0.0], [0.0, 2.0], option.None) + |> should.equal(Ok(1.0)) + + metrics.braycurtis_distance([1.0, 2.0], [3.0, 4.0], option.None) + |> should.equal(Ok(0.4)) + + metrics.braycurtis_distance([1.0, 2.0], [3.0, 4.0], option.Some([1.0, 1.0])) + |> should.equal(Ok(0.4)) + + metrics.braycurtis_distance([1.0, 2.0], [3.0, 4.0], option.Some([0.5, 1.0])) + |> should.equal(Ok(0.375)) + + metrics.braycurtis_distance([1.0, 2.0], [3.0, 4.0], option.Some([0.25, 0.25])) + |> should.equal(Ok(0.4)) + + // Different sized lists (weights) returns an error + metrics.braycurtis_distance( + [1.0, 2.0, 3.0], + [1.0, 2.0, 3.0], + option.Some([1.0]), + ) + |> should.be_error() +} From 2313363a9e921f11df2675f64226d566b81d1af2 Mon Sep 17 00:00:00 2001 From: NicklasXYZ <18580183+NicklasXYZ@users.noreply.github.com> Date: Sun, 14 Apr 2024 16:54:10 +0200 Subject: [PATCH 4/6] Allow passing weights in distance calculations --- src/gleam_community/maths/arithmetics.gleam | 91 +++-- src/gleam_community/maths/combinatorics.gleam | 6 +- src/gleam_community/maths/conversion.gleam | 6 +- src/gleam_community/maths/elementary.gleam | 6 +- src/gleam_community/maths/metrics.gleam | 343 +++++++++++++----- src/gleam_community/maths/piecewise.gleam | 6 +- src/gleam_community/maths/predicates.gleam | 36 +- src/gleam_community/maths/sequences.gleam | 24 +- src/gleam_community/maths/special.gleam | 4 +- .../maths/arithmetics_test.gleam | 6 +- test/gleam_community/maths/metrics_test.gleam | 263 ++++++++++++-- 11 files changed, 584 insertions(+), 207 deletions(-) diff --git a/src/gleam_community/maths/arithmetics.gleam b/src/gleam_community/maths/arithmetics.gleam index b37a067..ebffe30 100644 --- a/src/gleam_community/maths/arithmetics.gleam +++ b/src/gleam_community/maths/arithmetics.gleam @@ -1,6 +1,6 @@ -//// -//// -//// +//// +//// +//// //// -//// +//// +//// +//// //// -//// +//// +//// +//// //// -//// +//// +//// +//// //// -//// +//// +//// +//// //// -//// +//// +//// +//// ////