%% KNN Model for Iris Data
% Load Data
load fisheriris
tbl = array2table(meas(:,:));
tbl.Y = species;
% RNG - Seed Initialization
rng(10);

%% Stratified (default) split into train and test sets
sample_cnt = size(tbl, 1);
hpartition = cvpartition(sample_cnt, 'Holdout', 0.3); 
% Training Set
idx_train = training(hpartition);
tbl_train = tbl(idx_train,:);
% Test Set
idx_test = test(hpartition);
tbl_test = tbl(idx_test, :);

%% Train Model
knn_mdl = fitcknn(tbl_train, 'Y', 'NumNeighbors', 5);
train_error = resubLoss(knn_mdl);
fprintf('Training Error: %4.3f', train_error)

%% Confusion Matrix
[predict_class, ~] = predict(knn_mdl, tbl_train);
conf_matrix = confusionmat (tbl_train.Y, predict_class)

%% Cross Validation
cv_mdl = crossval(knn_mdl);
fold_accuracy = 1 - kfoldLoss(cv_mdl, 'Mode', 'individual');
figure()
bar(fold_accuracy);
title('Fold Accuracy')
xlabel('Fold')
ylabel('Accuracy')
grid
kfold_error = kfoldLoss(cv_mdl);
fprintf('10-Fold Cross Validation Error: %4.3f', kfold_error)

%% Test Set - Before Tuning
[predict_class, scores] = predict(knn_mdl, tbl_test);
figure()
confuion_matrix = confusionchart (tbl_test.Y, predict_class);
tst_error = loss(knn_mdl, tbl_test, 'Y');
tstPerf = 1 - tst_error;
fprintf('Test Set Accuracy = %4.3f', tstPerf)

%% Hyperparameter Tuning
k_vals = 3:15;
metric = {'euclidean', 'cosine', 'correlation'};
error_vals = zeros(length(k_vals), length(metric));

for i = 1:length(k_vals)
    for j = 1:length(metric)
        knn = fitcknn(tbl_train, 'Y', 'NumNeighbors', k_vals(i), ...
                                      'Distance', metric{j} );
        cv = crossval(knn);
        error = kfoldLoss(cv);
        error_vals(i, j) = error;
        fprintf('CV Error for k=%d (%s) is  %4.3f \n', ...
	 			k_vals(i), metric{j}, error);
    end
end

%% Plot of parameters performance  
%
figure()
bar(k_vals, error_vals)
legend({'Euclidean', 'Cosine', 'Correlation'})
title('KNN Hyperparameter Tuning')
xlabel('K')
ylabel('CV Error')
grid

%% optimal values for K and distance metric
[k, d] = find(error_vals == min(min(error_vals)))
rnd_indx = randi(length(k))
k_opt = k_vals(k(rnd_indx)); 
metric_opt = metric{d(rnd_indx)};
fprintf ('Optimal value for k=%d. Optimal metric = %s', ...
              k_opt, metric_opt);
%% Final Model - 
knn_opt_mdl = fitcknn(tbl_train, 'Y', ...
                    'NumNeighbors', k_opt, ...
                    'Distance', metric_opt)

train_error = resubLoss(knn_mdl);
sprintf('Optimal Model - Training Error: %4.3f', train_error)

%% Test Results -- After Tuning
[predict_class, scores] = predict(knn_opt_mdl, tbl_test);
tst_error = loss(knn_opt_mdl, tbl_test, 'Y');
tstPerf = 1 - tst_error;
sprintf('Optimal Model - Accuracy = %4.3f', tstPerf)
figure()
confuion_matrix = confusionchart (tbl_test.Y, predict_class);

%% Plot ROC Curve
rocObj = rocmetrics (tbl_test.Y, scores, knn_mdl.ClassNames)
figure()
plot(rocObj)
