Skip to content

Commit 1558dbc

Browse files
committed
modify handling error conditions for invalid data for KNN algorithm
1 parent 4c89ee7 commit 1558dbc

4 files changed

Lines changed: 94 additions & 38 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

7+
## Unreleased
8+
### Modified
9+
- K-Nearest Neighbours algorithm, add errors conditions and tests
10+
711
## [0.1.3] - 2018-11-22
812
### Modified
913
- Linear Regression, fit with gradient descent

lib/learn_kit/knn.ex

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ defmodule LearnKit.Knn do
4545
"""
4646
@spec new(data_set) :: %Knn{data_set: data_set}
4747

48-
def new(data_set) do
48+
def new(data_set) when is_list(data_set) do
4949
%Knn{data_set: data_set}
5050
end
5151

@@ -65,7 +65,7 @@ defmodule LearnKit.Knn do
6565
"""
6666
@spec add_train_data(%Knn{data_set: data_set}, point) :: %Knn{data_set: data_set}
6767

68-
def add_train_data(%Knn{data_set: data_set}, {key, value}) do
68+
def add_train_data(%Knn{data_set: data_set}, {key, value}) when is_atom(key) and is_list(value) do
6969
features = if Keyword.has_key?(data_set, key), do: Keyword.get(data_set, key), else: []
7070
data_set = Keyword.put(data_set, key, [value | features])
7171
%Knn{data_set: data_set}
@@ -94,16 +94,21 @@ defmodule LearnKit.Knn do
9494
"""
9595
@spec classify(%Knn{data_set: data_set}, [tuple]) :: {:ok, label}
9696

97-
def classify(%Knn{data_set: data_set}, options \\ []) do
98-
try do
99-
unless Keyword.has_key?(options, :feature), do: raise "Feature option is required"
100-
# modification of options
101-
options = Keyword.merge([k: 3, algorithm: "brute", weight: "uniform"], options)
102-
# prediction
103-
{label, _} = prediction(data_set, options)
104-
{:ok, label}
105-
rescue
106-
error -> {:error, error.message}
97+
def classify(%Knn{data_set: data_set}, options) when is_list(options) do
98+
cond do
99+
!Keyword.has_key?(options, :feature) ->
100+
{:error, "Feature option is required"}
101+
102+
!is_list(Keyword.get(options, :feature)) ->
103+
{:error, "Feature option must be presented as array"}
104+
105+
Keyword.has_key?(options, :k) && (!is_integer(Keyword.get(options, :k)) || Keyword.get(options, :k) <= 0) ->
106+
{:error, "K option must be positive integer"}
107+
108+
true ->
109+
options = Keyword.merge([k: 3, algorithm: "brute", weight: "uniform"], options)
110+
{label, _} = prediction(data_set, options)
111+
{:ok, label}
107112
end
108113
end
109114
end

lib/learn_kit/knn/classify.ex

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@ defmodule LearnKit.Knn.Classify do
88
defmacro __using__(_opts) do
99
quote do
1010
defp prediction(data_set, options) do
11-
calc_distances_for_features(data_set, options)
11+
data_set
12+
|> calc_distances_for_features(options)
1213
|> sort_distances()
1314
|> select_closest_features(options)
14-
|> calc_feature_weights(options)
15-
|> accumulate_weight_of_labels([])
16-
|> sort_result()
15+
|> check_zero_distance(options)
1716
end
1817

1918
# select algorithm for prediction
@@ -32,6 +31,21 @@ defmodule LearnKit.Knn.Classify do
3231
Enum.take(features, Keyword.get(options, :k))
3332
end
3433

34+
defp check_zero_distance(closest_features, options) do
35+
{distance, label} = Enum.at(closest_features, 0)
36+
cond do
37+
distance == 0 -> {label, 0}
38+
true -> select_best_label(closest_features, options)
39+
end
40+
end
41+
42+
defp select_best_label(features, options) do
43+
features
44+
|> calc_feature_weights(options)
45+
|> accumulate_weight_of_labels([])
46+
|> sort_result()
47+
end
48+
3549
defp calc_feature_weights(features, options) do
3650
Enum.map(features, fn feature ->
3751
Tuple.append(feature, calc_feature_weight(Keyword.get(options, :weight), elem(feature, 0)))
@@ -70,7 +84,6 @@ defmodule LearnKit.Knn.Classify do
7084
defp calc_distances_in_label(features, current_feature, key) do
7185
Enum.reduce(features, [], fn feature, acc ->
7286
distance = calc_distance_between_points(0, feature, current_feature, 0, length(feature) - 1)
73-
if distance == 0, do: raise "Feature exists in train data set with label #{key}"
7487
acc = [{distance, key} | acc]
7588
end)
7689
end

test/learn_kit/knn_test.exs

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,65 @@ defmodule LearnKit.KnnTest do
33

44
alias LearnKit.Knn
55

6-
test "create new knn classificator with empty data set" do
7-
assert %Knn{data_set: data_set} = Knn.new
8-
9-
assert data_set == []
6+
setup_all do
7+
{:ok, classifier: Knn.new([{:a1, [[-1, -1], [-2, -1], [-3, -2]]}, {:b1, [[1, 1], [2, 1], [3, 2], [-2, -2]]}])}
108
end
119

12-
test "add train data to classificator" do
13-
%Knn{data_set: data_set} = Knn.new
14-
|> Knn.add_train_data({:a1, [1, 2]})
15-
|> Knn.add_train_data({:a1, [1, 3]})
16-
|> Knn.add_train_data({:b1, [2, 3]})
10+
describe "for invalid data" do
11+
test "create new classifier with invalid data" do
12+
assert_raise FunctionClauseError, fn ->
13+
Knn.new("")
14+
end
15+
end
16+
17+
test "add train data in invalid format", state do
18+
assert_raise FunctionClauseError, fn ->
19+
Knn.add_train_data(state[:classifier], {:something_valid, "invalid"})
20+
end
21+
end
22+
23+
test "classify without options", state do
24+
assert_raise FunctionClauseError, fn ->
25+
Knn.classify(state[:classifier], "")
26+
end
27+
end
28+
29+
test "classify with empty options", state do
30+
assert {:error, "Feature option is required"} = Knn.classify(state[:classifier], [])
31+
end
32+
33+
test "classify with invalid feature", state do
34+
assert {:error, "Feature option must be presented as array"} = Knn.classify(state[:classifier], [feature: "1"])
35+
end
1736

18-
assert data_set == [b1: [[2, 3]], a1: [[1, 3], [1, 2]]]
37+
test "classify with invalid k", state do
38+
assert {:error, "K option must be positive integer"} = Knn.classify(state[:classifier], [feature: [-1, -2], k: -2])
39+
end
1940
end
2041

21-
test "classify new feature" do
22-
classificator = Knn.new
23-
|> Knn.add_train_data({:a1, [-1, -1]})
24-
|> Knn.add_train_data({:a1, [-2, -1]})
25-
|> Knn.add_train_data({:a1, [-3, -2]})
26-
|> Knn.add_train_data({:a2, [1, 1]})
27-
|> Knn.add_train_data({:a2, [2, 1]})
28-
|> Knn.add_train_data({:a2, [3, 2]})
29-
|> Knn.add_train_data({:a2, [-2, -2]})
30-
31-
assert {:ok, :a1} = Knn.classify(classificator, [feature: [-1, -2], k: 3, weight: "distance"])
42+
describe "for valid data" do
43+
test "create new knn classifier with empty data set" do
44+
assert %Knn{data_set: data_set} = Knn.new
45+
46+
assert data_set == []
47+
end
48+
49+
test "add train data to classifier" do
50+
%Knn{data_set: data_set} =
51+
Knn.new
52+
|> Knn.add_train_data({:a1, [1, 2]})
53+
|> Knn.add_train_data({:a1, [1, 3]})
54+
|> Knn.add_train_data({:b1, [2, 3]})
55+
56+
assert data_set == [b1: [[2, 3]], a1: [[1, 3], [1, 2]]]
57+
end
58+
59+
test "classify new feature", state do
60+
assert {:ok, :a1} = Knn.classify(state[:classifier], [feature: [-1, -2], k: 3, weight: "distance"])
61+
end
62+
63+
test "classify new feature, for existed point", state do
64+
assert {:ok, :b1} = Knn.classify(state[:classifier], [feature: [-2, -2], k: 3, weight: "uniform"])
65+
end
3266
end
3367
end

0 commit comments

Comments
 (0)