## Copyright (C) 2023 Mohammed Azmat Khan <azmat.dev0@gmail.com>
## Copyright (C) 2024 Ruchika Sonagote <ruchikasonagote2003@gmail.com>
## Copyright (C) 2023-2026 Andreas Bertsatos <abertsatos@biol.uoa.gr>
##
## This file is part of the statistics package for GNU Octave.
##
## This program is free software; you can redistribute it and/or modify it under
## the terms of the GNU General Public License as published by the Free Software
## Foundation; either version 3 of the License, or (at your option) any later
## version.
##
## This program is distributed in the hope that it will be useful, but WITHOUT
## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
## details.
##
## You should have received a copy of the GNU General Public License along with
## this program; if not, see <http://www.gnu.org/licenses/>.

classdef ClassificationKNN
  ## -*- texinfo -*-
  ## @deftp {statistics} ClassificationKNN
  ##
  ## K-nearest neighbors classification
  ##
  ## The @code{ClassificationKNN} class implements a K-nearest neighbor
  ## classifier object, which can predict responses for new data using the
  ## @code{predict} method.  The implemented algorithm allows you choose a range
  ## of different distance metrics, the number of nearest neighbors, as well as
  ## the searching algorithm.
  ##
  ## The K-nearest neighbors (k-NN) classifier is a simple, non-parametric
  ## machine learning algorithm used for classification tasks.  It classifies a
  ## data point based on the majority class of its k closest neighbors in the
  ## feature space.
  ##
  ## Create a @code{ClassificationKNN} object by using the @code{fitcknn}
  ## function or the class constructor.
  ##
  ## @seealso{fitcknn}
  ## @end deftp

  properties (GetAccess = public, SetAccess = protected)

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} W
    ##
    ## Observation weights
    ##
    ## A numeric column vector with one entry per observation used for fitting.
    ## Each class carries its prior spread evenly over its own
    ## observations, so an observation of class @math{k} weighs
    ## @qcode{Prior(k)} divided by the number of observations in that class.  This property is read-only.
    ##
    ## Each class carries its prior spread evenly over its own observations,
    ## so an observation of a class weighs @qcode{Prior} for that class
    ## divided by the number of observations it holds.
    ##
    ## @end deftp
    W               = [];
    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} X
    ##
    ## Predictor data
    ##
    ## A numeric matrix containing the unstandardized predictor data.  Each
    ## column of @var{X} represents one predictor (variable), and each row
    ## represents one observation.  This property is read-only.
    ##
    ## @end deftp
    X = [];

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} Y
    ##
    ## Class labels
    ##
    ## Specified as a logical or numeric column vector, or as a character array
    ## or a cell array of character vectors with the same number of rows as the
    ## predictor data.  Each row in @var{Y} is the observed class label for
    ## the corresponding row in @var{X}.  This property is read-only.
    ##
    ## @end deftp
    Y = [];

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} NumObservations
    ##
    ## Number of observations
    ##
    ## A positive integer value specifying the number of observations in the
    ## training dataset used for training the ClassificationKNN model.
    ## This property is read-only.
    ##
    ## @end deftp
    NumObservations = [];

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} RowsUsed
    ##
    ## Rows used for fitting
    ##
    ## A logical column vector with the same length as the observations in the
    ## original predictor data @var{X}, true for each row that was used for
    ## fitting the ClassificationKNN model.  It is empty, @qcode{[]},
    ## when every observation was used, so a non-empty value means that rows
    ## holding missing values were dropped.  This property is read-only.
    ##
    ## @end deftp
    RowsUsed        = [];

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} NumPredictors
    ##
    ## Number of predictors
    ##
    ## A positive integer value specifying the number of predictors in the
    ## training dataset used for training the ClassificationKNN model.
    ## This property is read-only.
    ##
    ## @end deftp
    NumPredictors   = [];

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} PredictorNames
    ##
    ## Names of predictor variables
    ##
    ## A cell array of character vectors specifying the names of the predictor
    ## variables.  The names are in the order in which they appear in the
    ## training dataset.  This property is read-only.
    ##
    ## @end deftp
    PredictorNames  = [];

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} CategoricalPredictors
    ##
    ## Indices of the categorical predictors
    ##
    ## A numeric vector of column indices into @code{X} naming the predictors
    ## treated as categorical, and empty when none is.  This property is
    ## read-only.
    ##
    ## @end deftp
    CategoricalPredictors = [];

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} ExpandedPredictorNames
    ##
    ## Names of the predictors as the model expanded them
    ##
    ## A cell array of character vectors.  It matches @code{PredictorNames}
    ## unless a categorical predictor was expanded into indicator variables.
    ## This property is read-only.
    ##
    ## @end deftp
    ExpandedPredictorNames = {};

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} ResponseName
    ##
    ## Response variable name
    ##
    ## A character vector specifying the name of the response variable @var{Y}.
    ## This property is read-only.
    ##
    ## @end deftp
    ResponseName    = [];

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} ClassNames
    ##
    ## Names of classes in the response variable
    ##
    ## An array of unique values of the response variable @var{Y}, which has the
    ## same data types as the data in @var{Y}.  This property is read-only.
    ## @qcode{ClassNames} can have any of the following datatypes:
    ##
    ## @itemize
    ## @item Cell array of character vectors
    ## @item Character array
    ## @item Logical vector
    ## @item Numeric vector
    ## @end itemize
    ##
    ## @end deftp
    ClassNames      = [];

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} Sigma
    ##
    ## Predictor standard deviations
    ##
    ## A numeric vector of the same length as the columns in @var{X} with the
    ## standard deviations corresponding to each predictor.  If the predictor
    ## variables have not been standardized, then @qcode{'obj.Sigma'} is empty.
    ## This property is read-only.
    ##
    ## Each predictor is summarized from every observation where that
    ## predictor is present, so a row holding a missing value in another
    ## predictor still contributes to this one.
    ##
    ## @end deftp
    Sigma           = [];

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} Mu
    ##
    ## Predictor means
    ##
    ## A numeric vector of the same length as the columns in @var{X} with the
    ## mean values corresponding to each predictor.  If the predictor variables
    ## have not been standardized, then @qcode{'obj.Mu'} is empty.  This
    ## property is read-only.
    ##
    ## Each predictor is summarized from every observation where that
    ## predictor is present, so a row holding a missing value in another
    ## predictor still contributes to this one.
    ##
    ## @end deftp
    Mu              = [];

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} NSMethod
    ##
    ## Nearest neighbor search method
    ##
    ## A character vector specified as either @qcode{'kdtree'}, which creates
    ## and uses a Kd-tree to find nearest neighbors, or @qcode{'exhaustive'},
    ## which uses the exhaustive search algorithm by computing the distance
    ## values from all points in @var{X} to find nearest neighbors.
    ##
    ## Change the @qcode{NSMethod} property using dot notation as in:
    ## @itemize
    ## @item @qcode{@var{obj}.NSMethod = @var{newNSMethod}}
    ## @end itemize
    ##
    ## @end deftp
    NSMethod        = [];

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} BucketSize
    ##
    ## Maximum data points in each node
    ##
    ## A positive integer scalar specifying the maximum number of data points in
    ## the leaf node of the Kd-tree.  @qcode{BucketSize} only applies when the
    ## @qcode{NSMethod} property is @qcode{'kdtree'}.
    ##
    ## Change the @qcode{BucketSize} property using dot notation as in:
    ## @itemize
    ## @item @qcode{@var{obj}.BucketSize = @var{maxnum}}
    ## @end itemize
    ##
    ## @end deftp
    BucketSize      = [];
    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} BinEdges
    ##
    ## Bin edges of the predictors
    ##
    ## A cell array with one entry per predictor, holding that predictor's bin
    ## edges where the learner discretized it before fitting.  It is empty here
    ## and stays empty: this learner fits the predictors as they are, and
    ## MATLAB's reports an empty cell for it as well.
    ##
    ## This property is read-only.
    ##
    ## @end deftp
    BinEdges        = {};

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} ModelParameters
    ##
    ## Fitting options, as they were given
    ##
    ## A structure holding the parameters of the fit: @qcode{NumNeighbors},
    ## @qcode{NSMethod}, @qcode{Distance}, @qcode{BucketSize},
    ## @qcode{IncludeTies}, @qcode{DistanceWeight}, @qcode{BreakTies},
    ## @qcode{Exponent}, @qcode{Cov}, @qcode{Scale}, @qcode{StandardizeData},
    ## and the @qcode{Version}, @qcode{Method} and @qcode{Type} tags.
    ##
    ## Each of the three distance parameters belongs to one metric and is
    ## empty under the others: @qcode{Exponent} to @qcode{'minkowski'},
    ## @qcode{Cov} to @qcode{'mahalanobis'} and @qcode{Scale} to
    ## @qcode{'seuclidean'}.  @qcode{Cov} and @qcode{Scale} hold what was
    ## passed and stay empty otherwise, while @qcode{Exponent} carries its
    ## default of 2 for a @qcode{'minkowski'} fit that did not name one.
    ## What the fit used in every case is the @qcode{DistParameter} property.
    ## @qcode{BucketSize} is likewise empty unless the search is
    ## @qcode{'kdtree'}, the only method that reads it.  This property is
    ## read-only.
    ##
    ## @end deftp
    ModelParameters = [];

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} HyperparameterOptimizationResults
    ##
    ## Results of the hyperparameter optimization
    ##
    ## @strong{Always empty.}  It is declared for MATLAB compatibility, where
    ## it holds what an automatic search over the hyperparameters found.  This
    ## class fits the parameters it is given and runs no such search, so there
    ## is nothing to report.  This property is read-only.
    ##
    ## @end deftp
    HyperparameterOptimizationResults = [];

  endproperties

  ## Properties a user may set after the model is built.  Each one is
  ## validated by its set method below.
  properties (GetAccess = public, SetAccess = public)

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} NumNeighbors
    ##
    ## Number of nearest neighbors
    ##
    ## A positive integer value specifyingNumber of nearest neighbors in @var{X}
    ## used to classify each point during prediction.  Change the
    ## @qcode{NumNeighbors} property using dot notation as in:
    ## @itemize
    ## @item @qcode{@var{obj}.NumNeighbors = @var{newNumNeighbors}}
    ## @end itemize
    ##
    ##
    ## This property may be assigned after fitting.  A value larger than
    ## @qcode{NumObservations} is reduced to it rather than refused.
    ## @end deftp
    NumNeighbors    = [];

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} Distance
    ##
    ## Distance metric
    ##
    ## A character vector specifying the distance metric used by the
    ## neighbor-searcher method, or a function handle to a custom distance
    ## function.  See the available distance metrics in @code{knnsearch} for
    ## more info.  A custom distance function must have the form
    ## @qcode{@var{D2} = @var{distfun} (@var{ZI}, @var{ZJ})}, where
    ## @var{ZI} is a @math{1*N} vector containing one row of the predictor
    ## data, @var{ZJ} is an @math{M2*N} matrix containing multiple rows of the
    ## predictor data, and @var{D2} is an @math{M2*1} vector of distances
    ## whose @math{k}-th element is the distance between the observations
    ## @var{ZI} and @qcode{@var{ZJ}(@var{k},:)}.  A custom distance function
    ## carries no @qcode{DistParameter}.  Change the @qcode{Distance}
    ## property using dot notation as in:
    ## @itemize
    ## @item @qcode{@var{obj}.Distance = @var{newDistance}}
    ## @end itemize
    ##
    ##
    ## This property may be assigned after fitting.  @qcode{NSMethod} is
    ## read-only and constrains it: a @qcode{'kdtree'} model takes
    ## @qcode{'euclidean'}, @qcode{'cityblock'}, @qcode{'chebychev'} and
    ## @qcode{'minkowski'} only, and never a function handle.  Assigning a
    ## different metric recomputes @qcode{DistParameter}, since a parameter
    ## belonging to one metric means nothing under another.
    ## @end deftp
    Distance        = [];

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} DistanceWeight
    ##
    ## Distance weighting function
    ##
    ## A character vector or a function handle specifying the distance weighting
    ## function, which can be any of the following values:
    ##
    ## @itemize
    ## @item @qcode{'equal'}, which corresponds to @code{@(d) d}.
    ## @item @qcode{'inverse'}, which corresponds to @code{@(d) 1/d}.
    ## @item @qcode{'squaredinverse'}, which corresponds to @code{@(d) 1/d.^2}.
    ## @item @qcode{@fcn}, which is a function handle that accepts a matrix of
    ## nonnegative distances, and returns a matrix the same size containing
    ## nonnegative distance weights.
    ## @end itemize
    ##
    ## Change the @qcode{DistanceWeight} property
    ## using dot notation as in:
    ## @itemize
    ## @item @qcode{@var{obj}.DistanceWeight = @var{newDistanceWeight}}
    ## @end itemize
    ##
    ##
    ## A character vector naming the weight, or the @code{func2str} form of a
    ## supplied handle.  This property may be assigned after fitting.
    ## @end deftp
    DistanceWeight  = [];

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} BreakTies
    ##
    ## Tie-breaking algorithm
    ##
    ## A character vector specifying the tie-breaking algorithm used by the
    ## @code{predict} method, when multiple classes have the same smallest cost.
    ## It can be one of the following:
    ##
    ## @itemize
    ## @item @qcode{'smallest'} (default), which favors the class with the
    ## smallest index among the tied groups, i.e. the one that appears first in
    ## the training labelled data.
    ## @item @qcode{'nearest'}, which favors the class with the nearest neighbor
    ## among the tied groups, i.e. the class with the closest member point
    ## according to the distance metric used.
    ## @item @qcode{'random'}, which randomly picks one class among the tied
    ## groups.
    ## @end itemize
    ##
    ## The tie-breaking algorithm is only used when @qcode{IncludeTies} is
    ## @qcode{false}.  Change the @qcode{BreakTies} property using dot notation
    ## as in:
    ## @itemize
    ## @item @qcode{@var{obj}.BreakTies = @var{algorithm}}
    ## @end itemize
    ##
    ##
    ## This property may be assigned after fitting.  It decides the label
    ## when two classes hold the same weight among the neighbours, and it
    ## applies whether or not @qcode{IncludeTies} is set.
    ## @end deftp
    BreakTies       = [];

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} IncludeTies
    ##
    ## Flag for handling ties
    ##
    ## A logical scalar specifying whether prediction includes all the neighbors
    ## whose distance values are equal to the @math{k^th} smallest distance.  If
    ## @qcode{IncludeTies} is @qcode{true}, prediction includes all of these
    ## neighbors.  Otherwise, prediction uses exactly @math{k} neighbors.
    ##
    ## Change the @qcode{IncludeTies} property using dot notation as in:
    ## @itemize
    ## @item @qcode{@var{obj}.IncludeTies = @var{flag}}
    ## @end itemize
    ##
    ##
    ## This property may be assigned after fitting.
    ## @end deftp
    IncludeTies     = [];

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} DistParameter
    ##
    ## Parameter for distance metric
    ##
    ## A positive definite covariance matrix, a positive scalar, or a vector of
    ## positive scale values specifying the parameter for the corresponding
    ## distance metric as shown below:
    ##
    ## @itemize
    ## @item @qcode{'mahalanobis'} accepts a positive definite covariance
    ## matrix.
    ## @item @qcode{'minkowski'} accepts a positive scalar as the Minkowski
    ## distance exponent.
    ## @item @qcode{'seuclidean'} accepts a vector of positive scale values of
    ## equal length as the number of predictors in @var{X}.
    ## @end itemize
    ##
    ## For any other distance metric, @qcode{DistParameter} is empty
    ## @qcode{([])}.  Change the @qcode{DistParameter} property using dot
    ## notation as in:
    ## @itemize
    ## @item @qcode{@var{obj}.DistParameter = @var{distParam}}
    ## @end itemize
    ##
    ##
    ## This property may be assigned after fitting, but only under the three
    ## metrics that carry one: @qcode{'minkowski'}, @qcode{'seuclidean'} and
    ## @qcode{'mahalanobis'}.  Under any other metric there is nothing for it
    ## to mean and the assignment is refused.
    ##
    ## @strong{Deviation from MATLAB.}  A @qcode{'seuclidean'} scale of zeros
    ## is refused here.  MATLAB accepts it, then warns from inside its distance
    ## routine at predict time and answers anyway, which contradicts its own
    ## message that the scale must hold positive values.  A zero scale divides
    ## that predictor by nothing, so it is rejected where it is given rather
    ## than surfacing later as a warning attached to an answer.
    ## @end deftp
    DistParameter   = [];

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} Cost
    ##
    ## Cost of Misclassification
    ##
    ## A square matrix specifying the cost of misclassification of a point.
    ## @qcode{Cost(i,j)} is the cost of classifying a point into class @qcode{j}
    ## if its true class is @qcode{i} (that is, the rows correspond to the true
    ## class and the columns correspond to the predicted class).  The order of
    ## the rows and columns in @qcode{Cost} corresponds to the order of the
    ## classes in @qcode{ClassNames}.  The number of rows and columns in
    ## @qcode{Cost} is the number of unique classes in the response.  By
    ## default, @qcode{Cost(i,j) = 1} if @qcode{i != j}, and
    ## @qcode{Cost(i,j) = 0} if @qcode{i = j}.  In other words, the cost is 0
    ## for correct classification and 1 for incorrect classification.
    ##
    ## Add or change the @qcode{Cost} property using dot notation as in:
    ## @itemize
    ## @item @qcode{@var{obj}.Cost = @var{costMatrix}}
    ## @end itemize
    ##
    ##
    ## A cost may also be given as a struct with the fields
    ## @qcode{ClassNames} and @qcode{ClassificationCosts}, which names the
    ## order its own matrix is written in.  That matrix is permuted into the
    ## order of @qcode{ClassNames} above, so a caller need not know which
    ## order the classes were sorted into.  It must name every class.
    ##
    ## A cost must be floating point, not sparse, not complex, non-negative
    ## and zero down its diagonal, and must hold no @qcode{NaN} or
    ## @qcode{Inf}.  A @code{single} is widened to @code{double}.
    ## @end deftp
    Cost            = [];

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} Prior
    ##
    ## Prior probability for each class
    ##
    ## A numeric vector specifying the prior probabilities for each class.  The
    ## order of the elements in @qcode{Prior} corresponds to the order of the
    ## classes in @qcode{ClassNames}.
    ##
    ## Add or change the @qcode{Prior} property using dot notation as in:
    ## @itemize
    ## @item @qcode{@var{obj}.Prior = @var{priorVector}}
    ## @end itemize
    ##
    ## Specified as a row vector with one entry per class, in the order of
    ## @qcode{ClassNames}, and rescaled to sum to one.  It may be given as
    ## @qcode{'empirical'}, @qcode{'uniform'}, a numeric vector, or a
    ## structure with @qcode{ClassNames} and @qcode{ClassProbs} fields, which
    ## assigns each probability by class name rather than by position.
    ##
    ## @end deftp
    Prior           = [];

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} ScoreTransform
    ##
    ## Transformation function for classification scores
    ##
    ## Specified as a function handle for transforming the classification
    ## scores.  Add or change the @qcode{ScoreTransform} property using dot
    ## notation as in:
    ##
    ## @itemize
    ## @item @qcode{@var{obj}.ScoreTransform = 'function_name'}
    ## @item @qcode{@var{obj}.ScoreTransform = @@function_handle}
    ## @end itemize
    ##
    ## When specified as a character vector, it can be any of the following
    ## built-in functions.  Nevertheless, the @qcode{ScoreTransform} property
    ## always stores their function handle equivalent.
    ##
    ## @multitable @columnfractions 0.2 0.75
    ## @headitem @var{Value} @tab @var{Description}
    ## @item @qcode{'doublelogit'} @tab @math{1 ./ (1 + exp (-2 * x))}
    ## @item @qcode{'invlogit'} @tab @math{log (x ./ (1 - x))}
    ## @item @qcode{'ismax'} @tab Sets the score for the class with the
    ## largest score to 1, and for all other classes to 0
    ## @item @qcode{'logit'} @tab @math{1 ./ (1 + exp (-x))}
    ## @item @qcode{'none'} @tab @math{x} (no transformation)
    ## @item @qcode{'identity'} @tab @math{x} (no transformation)
    ## @item @qcode{'sign'} @tab
    ## @math{-1 for x < 0, 0 for x = 0, 1 for x >
    ## 0}
    ## @item @qcode{'symmetric'} @tab @math{2 * x - 1}
    ## @item @qcode{'symmetricismax'} @tab Sets the score for the class
    ## with the largest score to 1, and for all other classes to -1
    ## @item @qcode{'symmetriclogit'} @tab @math{2 ./ (1 + exp (-x)) - 1}
    ## @end multitable
    ##
    ## @end deftp
    ScoreTransform  = 'none';

    ## -*- texinfo -*-
    ## @deftp {ClassificationKNN} {property} CacheSize
    ##
    ## Size of the Gram matrix cache
    ##
    ## A positive scalar giving the cache size in megabytes, 1000 by default.
    ## Change the @qcode{CacheSize} property using dot notation as in:
    ## @itemize
    ## @item @qcode{@var{obj}.CacheSize = @var{newCacheSize}}
    ## @end itemize
    ##
    ## This property is stored and reported for compatibility and
    ## @strong{does not affect the fit or any prediction}.  A nearest-neighbour
    ## model keeps no Gram matrix to cache: it holds the training data and
    ## computes each distance when asked.  Assigning it changes nothing but
    ## the value read back.
    ##
    ## MATLAB carries the same property and hides it from @code{properties},
    ## where this package reports it, so that a value a user may set is a
    ## value a user can find.
    ## @end deftp
    CacheSize       = 1000;

  endproperties

  ## Readable by the counterpart class, which copies it, and kept out of
  ## the documented surface.
  properties (GetAccess = public, SetAccess = protected, Hidden)

    ## The callable that DistanceWeight names.  The property itself holds the
    ## text, as MATLAB reports it, and predict reaches for this.
    DWfun = @(d) ones (size (d));

    ## Raised once the constructor is done.  The set methods validate a user's
    ## assignment; the values the fit computes for itself are already checked
    ## where they are built, and one of them, the Mahalanobis covariance, is
    ## legitimately singular on data whose predictors are collinear.
    Fitted = false;
    STfun = @(x) x;
  endproperties

  ## Set methods for the properties a user may assign.
  methods (Hidden)

    function this = set.Cost (this, val)
      gnY = uniqueLabels (this.Y);
      if (isempty (val))
        this.Cost = cast (! eye (classCount (gnY)), 'double');
      else
        ## Everything a cost must be, and the struct form, which
        ## is permuted into this model's class order.
        [val, errmsg] = costMatrix (val, gnY);
        if (! isempty (errmsg))
          error ("ClassificationKNN: %s", errmsg);
        endif
        this.Cost = val;
      endif
    endfunction

    function this = set.Prior (this, val)
      [~, gnY, gY] = uniqueLabels (this.Y);
      if (isstruct (val))
        val = priorFromStruct (val, this.ClassNames, 'ClassificationKNN');
      endif
      if (strcmpi ('uniform', val))
        this.Prior = ones (1, numel (gnY)) ./ numel (gnY);
      elseif (isempty (val) || strcmpi ('empirical', val))
        pr = [];
        for i = 1:numel (gnY)
          pr = [pr; sum(gY==i)];
        endfor
        this.Prior = pr(:)' ./ sum (pr);
      elseif (isnumeric (val))
        if (numel (gnY) != numel (val))
          error (strcat ("ClassificationKNN: the elements in 'Prior' do", ...
                         " not correspond to the selected classes in Y."));
        endif
        this.Prior = val(:)' ./ sum (val);
      endif
      ## The weights follow the prior, so they are rebuilt with it
      this.W = priorWeights (this.Prior, gY, numel (gY));
    endfunction

    ## The six properties a fitted model may be reassigned.  All of them are
    ## live: a model whose Distance or NumNeighbors is changed predicts exactly
    ## as one refitted with the new value would, because a lazy learner stores
    ## its training data and does the work at predict time.

    function this = set.NumNeighbors (this, val)
      if (! (isnumeric (val) && isscalar (val) && val > 0
             && fix (val) == val))
        error (strcat ("ClassificationKNN: 'NumNeighbors' must be a", ...
                       " positive integer."));
      endif
      ## More neighbours than there are observations is reduced rather than
      ## refused, which is what the fit does with the same value.
      if (! isempty (this.X))
        val = min (val, rows (this.X));
      endif
      this.NumNeighbors = val;
    endfunction

    function this = set.Distance (this, val)
      ## The same five the constructor builds a kd-tree for.  'manhattan' is
      ## a synonym of 'cityblock' and was missing here, so a kd-tree model
      ## fitted with it could be neither reassigned nor reloaded.
      kdt = {'euclidean', 'cityblock', 'manhattan', 'chebychev', ...
             'minkowski'};
      all = [kdt, {'seuclidean', 'mahalanobis', 'manhattan', 'cosine', ...
                   'correlation', 'spearman', 'hamming', 'jaccard'}];
      ## A kd-tree can only be searched with the metrics it was built for, and
      ## NSMethod is read-only, so the metric is what has to give way.
      if (strcmpi (this.NSMethod, 'kdtree'))
        if (! (ischar (val) && any (strcmpi (kdt, val))))
          error (strcat ("ClassificationKNN: 'Distance' for a kd-tree", ...
                         " model can only be 'euclidean', 'cityblock',", ...
                         " 'manhattan', 'chebychev', or 'minkowski'."));
        endif
      elseif (! (is_function_handle (val)
                 || (ischar (val) && any (strcmpi (all, val)))))
        error ("ClassificationKNN: unsupported distance metric.");
      endif
      ## A custom metric is probed here as the constructor probes it: called
      ## on one observation against the training data, it must return one
      ## distance per row.  Taken unchecked, it would defer the failure to
      ## predict, which names neither the property nor the user's function.
      if (is_function_handle (val) && ! isempty (this.X))
        try
          D2 = val (this.X(1,:), this.X);
        catch
          error (strcat ("ClassificationKNN: invalid function", ...
                         " handle for distance metric."));
        end_try_catch
        if (! isequal (size (D2), [rows(this.X), 1]))
          error (strcat ("ClassificationKNN: custom distance", ...
                         " function produces wrong output size."));
        endif
      endif
      if (ischar (val) && ischar (this.Distance)
          && strcmpi (val, this.Distance))
        return;   # unchanged, so the parameter it carries is kept
      endif
      this.Distance = val;
      ## The parameter belongs to the metric, so it is recomputed and whatever
      ## the previous metric held is discarded.
      if (! isempty (this.X))
        this.DistParameter = knndistparam (val, this.X, ! isempty (this.Mu));
      endif
    endfunction

    function this = set.DistanceWeight (this, val)
      [f, dw] = parseDistanceWeight (val, 'ClassificationKNN');
      this.DistanceWeight = dw;
      this.DWfun = f;
    endfunction

    function this = set.BreakTies (this, val)
      if (! (ischar (val) && any (strcmpi ({'smallest', 'random', ...
                                            'nearest'}, val))))
        error (strcat ("ClassificationKNN: 'BreakTies' must be", ...
                       " 'smallest', 'random' or 'nearest'."));
      endif
      this.BreakTies = lower (val);
    endfunction

    function this = set.IncludeTies (this, val)
      if (! (islogical (val) && isscalar (val)))
        error (strcat ("ClassificationKNN: 'IncludeTies' must be a", ...
                       " logical scalar."));
      endif
      this.IncludeTies = val;
    endfunction

    function this = set.DistParameter (this, val)
      ## An empty value is the state a metric without a parameter is in, so it
      ## is always allowed; anything else has to belong to the current metric.
      if (! isempty (val) && this.Fitted)
        if (! (ischar (this.Distance)
               && any (strcmpi ({'minkowski', 'seuclidean', ...
                                 'mahalanobis'}, this.Distance))))
          error (strcat ("ClassificationKNN: 'DistParameter' can only be", ...
                         " provided when 'Distance' is 'minkowski',", ...
                         " 'mahalanobis', or 'seuclidean'."));
        endif
        switch (lower (this.Distance))
          case 'minkowski'
            if (! (isnumeric (val) && isscalar (val) && val > 0))
              error (strcat ("ClassificationKNN: the exponent for the", ...
                             " Minkowski distance must be a positive", ...
                             " scalar."));
            endif
          case 'seuclidean'
            if (! (isnumeric (val) && isvector (val) && all (val > 0)
                   && numel (val) == columns (this.X)))
              error (strcat ("ClassificationKNN: the scale for the", ...
                             " standardized Euclidean distance must be a", ...
                             " vector of positive values, with length", ...
                             " equal to the number of columns in X."));
            endif
          case 'mahalanobis'
            if (! (isnumeric (val) && issquare (val)
                   && columns (val) == columns (this.X)))
              error (strcat ("ClassificationKNN: the covariance for the", ...
                             " Mahalanobis distance must be a square", ...
                             " matrix with the same number of columns", ...
                             " as X."));
            endif
            if (! (isequal (val, val') && all (eig (val) > 0)))
              error (strcat ("ClassificationKNN: the covariance for the", ...
                             " Mahalanobis distance must be symmetric and", ...
                             " positive definite."));
            endif
        endswitch
      endif
      this.DistParameter = val;
    endfunction

    function this = set.CacheSize (this, val)
      if (! (isnumeric (val) && isscalar (val) && isreal (val)
             && isfinite (val) && val > 0))
        error (strcat ("ClassificationKNN: 'CacheSize' must be a", ...
                       " positive finite scalar."));
      endif
      this.CacheSize = val;
    endfunction

    function this = set.ScoreTransform (this, val)
      [f, nm] = parseScoreTransform (val, 'ClassificationKNN');
      this.ScoreTransform = nm;
      this.STfun = f;
    endfunction

    ## Custom display
    function display (this)
      in_name = inputname (1);
      if (! isempty (in_name))
        fprintf ('%s =\n', in_name);
      endif
      disp (this);
    endfunction

    ## Custom display
    function disp (this)
      fprintf ("\n  ClassificationKNN\n\n");
      ## Print selected properties
      fprintf ("%+25s: '%s'\n", 'ResponseName', this.ResponseName);
      if (iscellstr (this.ClassNames))
        str = repmat ({'''%s'''}, 1, numel (this.ClassNames));
        str = strcat ('{', strjoin (str, ' '), '}');
        str = sprintf (str, this.ClassNames{:});
      elseif (ischar (this.ClassNames))
        str = repmat ({'''%s'''}, 1, rows (this.ClassNames));
        str = strcat ('[', strjoin (str, ' '), ']');
        str = sprintf (str, cellstr (this.ClassNames){:});
      else # single, double, logical
        str = repmat ({'%d'}, 1, numel (this.ClassNames));
        str = strcat ('[', strjoin (str, ' '), ']');
        str = sprintf (str, this.ClassNames);
      endif
      fprintf ("%+25s: %s\n", 'ClassNames', str);
      fprintf ("%+25s: '%s'\n", 'ScoreTransform', this.ScoreTransform);
      fprintf ("%+25s: %d\n", 'NumObservations', this.NumObservations);
      fprintf ("%+25s: %d\n", 'NumPredictors', this.NumPredictors);
      if (is_function_handle (this.Distance))
        fprintf ("%+25s: %s\n", 'Distance', func2str (this.Distance));
      else
        fprintf ("%+25s: '%s'\n", 'Distance', this.Distance);
      endif
      fprintf ("%+25s: '%s'\n", 'NSMethod', this.NSMethod);
      fprintf ("%+25s: %d\n", 'NumNeighbors', this.NumNeighbors);
    endfunction

  endmethods

  methods (Access = public)

    ## -*- texinfo -*-
    ## @deftypefn  {statistics} {@var{obj} =} ClassificationKNN (@var{X}, @var{Y})
    ## @deftypefnx {statistics} {@var{obj} =} ClassificationKNN (@dots{}, @var{name}, @var{value})
    ##
    ## Create a @qcode{ClassificationKNN} class object containing a k-Nearest
    ## Neighbor classification model.
    ##
    ## @code{@var{obj} = ClassificationKNN (@var{X}, @var{Y})} returns a
    ## ClassificationKNN object, with @var{X} as the predictor data and @var{Y}
    ## containing the class labels of observations in @var{X}.
    ##
    ## @itemize
    ## @item
    ## @code{X} must be a @math{N*P} numeric matrix of input data where rows
    ## correspond to observations and columns correspond to features or
    ## variables.  @var{X} will be used to train the kNN model.
    ## @item
    ## @code{Y} is @math{N*1} matrix or cell matrix containing the class labels
    ## of corresponding predictor data in @var{X}.  @var{Y} can contain any type
    ## of categorical data.  @var{Y} must have same numbers of Rows as @var{X}.
    ## @end itemize
    ##
    ## @code{@var{obj} = ClassificationKNN (@dots{}, @var{name}, @var{value})}
    ## returns a ClassificationKNN object with parameters specified by the
    ## following @qcode{@var{name}, @var{value}} paired input arguments:
    ##
    ## @multitable @columnfractions 0.18 0.8
    ## @headitem @var{Name} @tab @var{Value}
    ##
    ## @item @qcode{'PredictorNames'} @tab A cell array of character
    ## vectors specifying the names of the predictors. The length of this array
    ## must match the number of columns in @var{X}.
    ##
    ## @item @qcode{'ResponseName'} @tab A character vector specifying the
    ## name of the response variable.
    ##
    ## @item @qcode{'ClassNames'} @tab Names of the classes in the class
    ## labels, @var{Y}, used for fitting the GAM model.
    ## @qcode{ClassNames} are of the same type as the class labels in @var{Y}.
    ##
    ## @item @qcode{'Cost'} @tab An @math{N*R} numeric matrix containing
    ## misclassification cost for the corresponding instances in @var{X}, where
    ## @math{R} is the number of unique categories in @var{Y}.  If an instance
    ## is correctly classified into its category the cost is calculated to be 1,
    ## otherwise 0. The cost matrix can be altered by using
    ## @code{@var{Mdl}.cost = somecost}.  By default, its value is
    ## @qcode{@var{cost} = ones (rows (X), numel (unique (Y)))}.
    ##
    ## @item @qcode{'Prior'} @tab A numeric vector specifying the prior
    ## probabilities for each class.  The order of the elements in @qcode{Prior}
    ## corresponds to the order of the classes in @qcode{ClassNames}.
    ## Alternatively, you can specify @qcode{'empirical'} to use the empirical
    ## class probabilities or @qcode{'uniform'} to assume equal class
    ## probabilities.
    ##
    ## @item @qcode{'ScoreTransform'} @tab A user-defined function handle
    ## or a character vector specifying one of the following builtin functions
    ## specifying the transformation applied to predicted classification scores.
    ## Supported values include @qcode{'doublelogit'}, @qcode{'invlogit'},
    ## @qcode{'ismax'}, @qcode{'logit'}, @qcode{'none'}, @qcode{'identity'},
    ## @qcode{'sign'}, @qcode{'symmetric'}, @qcode{'symmetricismax'}, and
    ## @qcode{'symmetriclogit'}.
    ##
    ## @item @qcode{'BreakTies'} @tab A character vector specifying the
    ## tie-breaking algorithm used by @code{predict} method, when multiple
    ## classes have the same smallest cost.  Available options are
    ## @qcode{'smallest'} (default), which uses the smallest index among tied
    ## groups, @qcode{'nearest'}, which uses the class with the nearest neighbor
    ## among tied groups, and @qcode{'random'}, which randomly selects one of
    ## the tied groups.
    ##
    ## @item @qcode{'NumNeighbors'} @tab A positive integer value that
    ## specifies the number of nearest neighbors to be found in the kNN search
    ## algorithm for classifying each point during prediction.  By default,
    ## it is 1.
    ##
    ## @item @qcode{'Distance'} @tab Any valid distance metric supported by
    ## the @code{pdist2} function.  Note that the allowable distance metrics
    ## depend on the selected nearest neighbor search method.
    ##
    ## @item @qcode{'DistanceWeight'} @tab Either a distance weighting
    ## function, specified either as a function handle, which accepts a matrix
    ## of nonnegative distances and returns a matrix the same size containing
    ## nonnegative distance weights, or a character vector with one of the
    ## following values: @qcode{'equal'}, which corresponds to no weighting;
    ## @qcode{'inverse'}, which corresponds to a weight equal to
    ## @math{1/distance}; @qcode{'squaredinverse'}, which corresponds to a
    ## weight equal to @math{1/distance^2}.
    ##
    ## @item @qcode{'Cov'} @tab A square matrix with the same number of
    ## columns @var{X} specifying the covariance matrix for computing the
    ## mahalanobis distance.  This must be a positive definite matrix matching.
    ## This argument is only valid when the selected distance metric is
    ## @qcode{'mahalanobis'}.
    ##
    ## @item @qcode{'Exponent'} @tab A positive scalar (usually an integer)
    ## specifying the Minkowski distance exponent.  This argument is only valid
    ## when the selected distance metric is @qcode{'minkowski'}.  By default,
    ## it is 2.
    ##
    ## @item @qcode{'Scale'} @tab A nonnegative numeric vector specifying
    ## the scale parameters for the standardized Euclidean distance.  The vector
    ## length must be equal to the number of columns in @var{X}.  This argument
    ## is only valid when the selected distance metric is @qcode{'seuclidean'},
    ## in which case each coordinate of @var{X} is scaled by the corresponding
    ## element of @qcode{'scale'}, as is each query point in @var{Y}.  By
    ## default, the scale parameter is the standard deviation of each coordinate
    ## in @var{X}.  If a variable in @var{X} is constant, i.e. zero variance,
    ## this value is forced to 1 to avoid division by zero.  This is the
    ## equivalent of this variable not being standardized.
    ##
    ## @item @qcode{'NSMethod'} @tab A character vector specifying the
    ## nearest neighbor search method used by @code{knnsearch}, which can be
    ## @qcode{'kdtree'} or @qcode{'exhaustive'}.  See @code{knnsearch} for more
    ## information about default values and allowable distance metrics for each
    ## search method.
    ##
    ## @item @qcode{'BucketSize'} @tab A positive integer value specifying
    ## the maximum number of data points in the leaf node of the Kd-tree.  This
    ## argument is meaningful only when the selected nearest neighbor search
    ## method is @qcode{'kdtree'}.  By default, it is 50.
    ## @end multitable
    ##
    ## @seealso{fitcknn, knnsearch, rangesearch, pdist2}
    ## @end deftypefn
    function this = ClassificationKNN (X, Y, varargin)
      ## Check for sufficient number of input arguments
      if (nargin < 2)
        error ("ClassificationKNN: too few input arguments.");
      endif

      ## Check X and Y have the same number of observations
      if (rows (X) != rows (Y))
        error ("ClassificationKNN: number of rows in X and Y must be equal.");
      endif

      ## Assign original X and Y data to the ClassificationKNN object
      this.X = X;
      this.Y = Y;

      ## Get groups in Y
      [gY, gnY, glY] = grp2idx (Y);

      ## Set default values before parsing optional parameters
      Standardize     = false;
      PredictorNames  = [];
      ResponseName    = [];
      ClassNames      = [];
      Prior           = [];
      Cost            = [];
      Scale           = [];     # Distance scale for 'seuclidean'
      Cov             = [];     # Covariance matrix for 'mahalanobis'
      Exponent        = [];     # Exponent for 'minkowski'
      BreakTies       = [];
      NumNeighbors    = [];
      Distance        = [];
      DistanceWeight  = [];
      DistParameter   = [];
      NSMethod        = [];
      IncludeTies     = false;
      BucketSize      = 50;
      CacheSize       = 1000;

      ## Number of parameters for Standardize, Scale, Cov (maximum 1 allowed)
      SSC = 0;

      ## Parse extra parameters
      while (numel (varargin) > 0)
        switch (tolower (varargin {1}))

          case 'standardize'
            if (SSC < 1)
              Standardize = varargin{2};
              if (! (Standardize == true || Standardize == false))
                error (strcat ("ClassificationKNN: 'Standardize' must", ...
                               " be either true or false."));
              endif
              SSC += 1;
            else
              error (strcat ("ClassificationKNN: 'Standardize' cannot", ...
                             " simultaneously be specified with either", ...
                             " Scale or Cov."));
            endif

          case 'predictornames'
            PredictorNames = varargin{2};
            if (! iscellstr (PredictorNames))
              error (strcat ("ClassificationKNN: 'PredictorNames' must", ...
                             " be supplied as a cellstring array."));
            elseif (columns (PredictorNames) != columns (X))
              error (strcat ("ClassificationKNN: 'PredictorNames' must", ...
                             " have the same number of columns as X."));
            endif

          case 'responsename'
            ResponseName = varargin{2};
            if (! ischar (ResponseName))
              error (strcat ("ClassificationKNN: 'ResponseName'", ...
                             " must be a character vector."));
            endif

          case 'classnames'
            ClassNames = varargin{2};
            if (! (iscellstr (ClassNames) || isnumeric (ClassNames) ||
                   islogical (ClassNames) || ischar (ClassNames)))
              error (strcat ("ClassificationKNN: 'ClassNames' must be a", ...
                             " cell array of character vectors, a logical", ...
                             " vector, a numeric vector, or a character array."));
            endif
            ## Check that all class names are available in gnY
            if (iscellstr (ClassNames) || ischar (ClassNames))
              ClassNames = cellstr (ClassNames);
              if (! all (cell2mat (cellfun (@(x) any (strcmp (x, gnY)),
                                   ClassNames, 'UniformOutput', false))))
                error (strcat ("ClassificationKNN: not all 'ClassNames'", ...
                               " are present in Y."));
              endif
            else
              if (! all (cell2mat (arrayfun (@(x) any (x == glY),
                                   ClassNames, 'UniformOutput', false))))
                error (strcat ("ClassificationKNN: not all 'ClassNames'", ...
                               " are present in Y."));
              endif
            endif

          case 'prior'
            Prior = varargin{2};
            if (! (isstruct (Prior) || (isnumeric (Prior) && isvector (Prior))
                   || (ischar (Prior) && (strcmpi (Prior, 'empirical')
                       || strcmpi (Prior, 'uniform')))))
              error (strcat ("ClassificationKNN: 'Prior' must be either", ...
                             " a numeric vector or a character vector."));
            endif

          case 'cost'
            Cost = varargin{2};
            ## A struct carrying its own class order is a cost too,
            ## and is resolved by the property's own set method.
            if (! (isstruct (Cost)
                   || (isnumeric (Cost) && issquare (Cost))))
              error (strcat ("ClassificationKNN: 'Cost' must be", ...
                             " a numeric square matrix."));
            endif

          case 'scoretransform'
            name = 'ClassificationKNN';
            this.ScoreTransform = varargin{2};

          case 'breakties'
            BreakTies = varargin{2};
            if (! ischar (BreakTies))
              error (strcat ("ClassificationKNN: 'BreakTies'", ...
                             " must be a character vector."));
            endif
            ## Check that all class names are available in gnY
            BTs = {'smallest', 'nearest', 'random'};
            if (! any (strcmpi (BTs, BreakTies)))
              error ("ClassificationKNN: invalid value for 'BreakTies'.");
            endif

          case 'numneighbors'
            NumNeighbors = varargin{2};
            if (! (isnumeric (NumNeighbors) && isscalar (NumNeighbors) &&
                   NumNeighbors > 0 && fix (NumNeighbors) == NumNeighbors))
              error (strcat ("ClassificationKNN: 'NumNeighbors'", ...
                             " must be a positive integer."));
            endif

          case 'distance'
            Distance = varargin{2};
            DMs = {'euclidean', 'seuclidean', 'mahalanobis', 'minkowski', ...
                   'cityblock', 'manhattan', 'chebychev', 'cosine', ...
                   'correlation', 'spearman', 'hamming', 'jaccard'};
            if (ischar (Distance))
              if (! any (strcmpi (DMs, Distance)))
                error ("ClassificationKNN: unsupported distance metric.");
              endif
            elseif (is_function_handle (Distance))
              ## Check the input output sizes of the user function
              D2 = [];
              try
                D2 = Distance(X(1,:), X);
              catch ME
                error (strcat ("ClassificationKNN: invalid function", ...
                               " handle for distance metric."));
              end_try_catch
              Xrows = rows (X);
              if (! isequal (size (D2), [Xrows, 1]))
                error (strcat ("ClassificationKNN: custom distance", ...
                               " function produces wrong output size."));
              endif
            else
              error ("ClassificationKNN: invalid distance metric.");
            endif

          case 'distanceweight'
            [this.DWfun, this.DistanceWeight] = ...
                parseDistanceWeight (varargin{2}, 'ClassificationKNN');
            DistanceWeight = this.DistanceWeight;

          case 'scale'
            if (SSC < 1)
              Scale = varargin{2};
              if (! (isnumeric (Scale) && isvector (Scale)))
                error ("ClassificationKNN: 'Scale' must be a numeric vector.");
              endif
              SSC += 1;
            else
              error (strcat ("ClassificationKNN: 'Scale' cannot", ...
                             " simultaneously be specified with either", ...
                             " 'Standardize' or 'Cov'."));
            endif

          case 'cov'
            if (SSC < 1)
              Cov = varargin{2};
              [~, p] = chol (Cov);
              if (p != 0)
                error (strcat ("ClassificationKNN: 'Cov' must be a", ...
                               " symmetric positive definite matrix."));
              endif
              SSC += 1;
            else
              error (strcat ("ClassificationKNN: 'Cov' cannot", ...
                             " simultaneously be specified with either", ...
                             " 'Standardize' or 'Scale'."));
            endif

          case 'exponent'
            Exponent = varargin{2};
            if (! (isnumeric (Exponent) && isscalar (Exponent) &&
                           Exponent > 0 && fix (Exponent) == Exponent))
              error (strcat ("ClassificationKNN: 'Exponent'", ...
                             " must be a positive integer."));
            endif

          case 'nsmethod'
            NSMethod = varargin{2};
            NSM = {'kdtree', 'exhaustive'};
            if (! ischar (NSMethod))
              error (strcat ("ClassificationKNN: 'NSMethod' must", ...
                             " be a character vector."));
            endif
            if (! any (strcmpi (NSM, NSMethod)))
              error (strcat ("ClassificationKNN: 'NSMethod' must", ...
                             " be either 'kdtree' or 'exhaustive'."));
            endif

          case 'cachesize'
            CacheSize = varargin{2};
            if (! (isnumeric (CacheSize) && isscalar (CacheSize)
                   && isreal (CacheSize) && isfinite (CacheSize)
                   && CacheSize > 0))
              error (strcat ("ClassificationKNN: 'CacheSize' must be a", ...
                             " positive finite scalar."));
            endif

          case 'includeties'
            IncludeTies = varargin{2};
            if (! (IncludeTies == true || IncludeTies == false))
              error (strcat ("ClassificationKNN: 'IncludeTies'", ...
                             " must be either true or false."));
            endif

          case 'bucketsize'
            BucketSize = varargin{2};
            if (! (isnumeric (BucketSize) && isscalar (BucketSize) &&
                           BucketSize > 0 && fix (BucketSize) == BucketSize))
              error (strcat ("ClassificationKNN: 'BucketSize'", ...
                             " must be a positive integer."));
            endif

          otherwise
            error (strcat ("ClassificationKNN: invalid parameter",...
                           " name in optional pair arguments."));

        endswitch
        varargin(1:2) = [];
      endwhile

      ## Generate default predictors and response variable names (if necessary)
      NumPredictors = columns (X);
      if (isempty (PredictorNames))
        for i = 1:NumPredictors
          PredictorNames {i} = strcat ("x", num2str (i));
        endfor
      endif
      if (isempty (ResponseName))
        ResponseName = 'Y';
      endif

      ## Assign predictors and response variable names
      this.NumPredictors  = NumPredictors;
      this.PredictorNames = PredictorNames;
      this.CategoricalPredictors = [];
      this.ExpandedPredictorNames = PredictorNames;
      this.ResponseName   = ResponseName;

      ## Handle class names
      if (! isempty (ClassNames))
        ## Anything textual is matched as whole names, gnY being grp2idx's
        ## own cellstr of them.  A character matrix is not a cellstr, and
        ## ismember between two of them compares character by character, so
        ## it would answer a question nobody asked.
        if (iscellstr (ClassNames) || ischar (ClassNames))
          ru = find (! ismember (gnY, cellstr (ClassNames)));
        else
          ru = find (! ismember (glY, ClassNames));
        endif
        for i = 1:numel (ru)
          gY(gY == ru(i)) = NaN;
        endfor
      endif

      ## An observation is dropped only when its response is missing.  A row
      ## whose predictors hold missing values is kept and reported as used,
      ## while the fit below draws on the complete observations alone.
      RowsUsed  = ! isnan (gY);
      Yret      = Y(RowsUsed, :);
      Xret      = X(RowsUsed, :);
      this.X    = Xret;
      this.Y    = Yret;
      cobs      = ! any (isnan (Xret), 2);
      Y         = Yret(cobs, :);
      X         = Xret(cobs, :);

      ## Renew groups in Y over the retained observations, so a class held
      ## only by a row with missing predictors is still a class of the model
      [this.ClassNames, gnY, gret] = uniqueLabels (Yret);
      gY = gret(cobs);

      ## Check X contains valid data
      if (! (isnumeric (X) && isfinite (X)))
        error ("ClassificationKNN: invalid values in X.");
      endif

      ## Assign the number of observations and their corresponding indices
      ## on the original data, which will be used for training the model,
      ## to the ClassificationKNN object
      this.NumObservations = rows (this.X);
      ## RowsUsed is left empty when every observation was used, as in MATLAB
      if (all (RowsUsed))
        this.RowsUsed = [];
      else
        this.RowsUsed = RowsUsed;
      endif

      ## Handle the Standardize option
      if (Standardize)
        ## A lazy learner standardizes at predict time, so each predictor is
        ## summarized from every observation where it is present, whatever is
        ## missing elsewhere in the row.  This is what MATLAB reports.
        this.Mu    = zeros (1, columns (this.X));
        this.Sigma = zeros (1, columns (this.X));
        for j = 1:columns (this.X)
          xj = this.X(:,j);
          xj = xj(! isnan (xj));
          this.Mu(j)    = mean (xj);
          this.Sigma(j) = std (xj);
        endfor
        this.Sigma(this.Sigma == 0) = 1;  # predictor is constant
      else
        this.Sigma = [];
        this.Mu = [];
      endif

      ## Handle BreakTies
      if (isempty (BreakTies))
        this.BreakTies = 'smallest';
      else
        this.BreakTies = BreakTies;
      endif

      ## Handle Cost and Prior
      this.Cost  = Cost;
      this.Prior = Prior;
      ## Each class carries its prior, spread over its own observations
      this.W = priorWeights (this.Prior, gY, this.NumObservations);

      ## Get number of neighbors
      if (isempty (NumNeighbors))
        this.NumNeighbors = 1;
      else
        ## There are only as many neighbors available as there are training
        ## samples, so a larger request is capped at that.  Asking for more
        ## raised an internal nonconformant operator error at prediction.
        this.NumNeighbors = min (NumNeighbors, rows (X));
      endif

      ## Get distance metric
      if (isempty (Distance))
        Distance = 'euclidean';
      endif
      this.Distance = Distance;

      ## Get distance weight
      if (isempty (DistanceWeight))
        [this.DWfun, this.DistanceWeight] = ...
            parseDistanceWeight ('equal', 'ClassificationKNN');
      endif

      ## Handle distance metric parameters (Scale, Cov, Exponent)
      if (! isempty (Scale))
        if (! strcmpi (Distance, 'seuclidean'))
          error (strcat ("ClassificationKNN: 'Scale' is only valid", ...
                         " when distance metric is seuclidean."));
        endif
        if (numel (Scale) != NumPredictors)
          error (strcat ("ClassificationKNN: 'Scale' vector must have", ...
                         " equal length to the number of columns in X."));
        endif
        if (any (Scale < 0))
          error (strcat ("ClassificationKNN: 'Scale' vector must", ...
                         " contain nonnegative scalar values."));
        endif
        this.DistParameter = Scale;
      else
        if (strcmpi (Distance, 'seuclidean'))
          if (Standardize)
            this.DistParameter = ones (1, NumPredictors);
          else
            this.DistParameter = std (X, [], 1);
          endif
        endif
      endif
      if (! isempty (Cov))
        if (! strcmpi (Distance, 'mahalanobis'))
          error (strcat ("ClassificationKNN: 'Cov' is only valid", ...
                         " when distance metric is 'mahalanobis'."));
        endif
        if (columns (Cov) != NumPredictors)
          error (strcat ("ClassificationKNN: 'Cov' matrix", ...
                         " must have equal columns as X."));
        endif
        this.DistParameter = Cov;
      else
        if (strcmpi (Distance, 'mahalanobis'))
          this.DistParameter = cov (X);
        endif
      endif
      if (! isempty (Exponent))
        if (! strcmpi (Distance, 'minkowski'))
          error (strcat ("ClassificationKNN: 'Exponent' is only", ...
                         " valid when distance metric is 'minkowski'."));
        endif
        this.DistParameter = Exponent;
      else
        if (strcmpi (Distance, 'minkowski'))
          this.DistParameter = 2;
        endif
      endif

      ## Get Nearest neighbor search method
      kdm = {'euclidean', 'cityblock', 'manhattan', 'minkowski', 'chebychev'};
      if (! isempty (NSMethod))
        if (strcmpi ('kdtree', NSMethod) && (! any (strcmpi (kdm, Distance))))
          error (strcat ("ClassificationKNN: 'kdtree' method is only va", ...
                         "lid for 'euclidean', 'cityblock', 'manhattan',", ...
                         " 'minkowski', and 'chebychev' distance metrics."));
        endif
        this.NSMethod = NSMethod;
      else
        if (any (strcmpi (kdm, Distance)) && NumPredictors <= 10)
          this.NSMethod = 'kdtree';
        else
          this.NSMethod = 'exhaustive';
        endif
      endif

      ## Assign IncludeTies, BucketSize and CacheSize properties
      this.IncludeTies = IncludeTies;
      this.BucketSize = BucketSize;
      this.CacheSize = CacheSize;

      ## The fit as it was asked for, not as it was resolved: Exponent, Cov
      ## and Scale hold what was passed and nothing else, DistParameter
      ## holding what was used, and BucketSize applies to the kd-tree alone.
      if (strcmpi (this.NSMethod, 'kdtree'))
        BucketSizeIn = this.BucketSize;
      else
        BucketSizeIn = [];
      endif
      ## Exponent is the exception among the three: MATLAB fills its default
      ## of 2 for a 'minkowski' fit, where Cov and Scale stay empty until
      ## they are passed.  Measured on R2024a.
      if (strcmpi (this.Distance, 'minkowski'))
        ExponentIn = this.DistParameter;
      else
        ExponentIn = [];
      endif
      this.ModelParameters = struct ( ...
        'NumNeighbors', this.NumNeighbors, ...
        'NSMethod', this.NSMethod, ...
        'Distance', this.Distance, ...
        'BucketSize', BucketSizeIn, ...
        'IncludeTies', this.IncludeTies, ...
        'DistanceWeight', this.DistanceWeight, ...
        'BreakTies', this.BreakTies, ...
        'Exponent', ExponentIn, ...
        'Cov', Cov, ...
        'Scale', Scale, ...
        'StandardizeData', logical (Standardize), ...
        'Version', 1, ...
        'Method', 'KNN', ...
        'Type', 'classification');

      this.Fitted = true;

    endfunction

    ## -*- texinfo -*-
    ## @deftypefn  {ClassificationKNN} {@var{labels} =} predict (@var{obj}, @var{XC})
    ## @deftypefnx {ClassificationKNN} {[@var{labels}, @var{scores}, @var{cost}] =} predict (@var{obj}, @var{XC})
    ##
    ## Classify new data points into categories using the kNN algorithm from a
    ## k-Nearest Neighbor classification model.
    ##
    ## @code{@var{labels} = predict (@var{obj}, @var{XC})} returns the matrix of
    ## labels predicted for the corresponding instances in @var{XC}, using the
    ## predictor data in @code{obj.X} and corresponding labels, @code{obj.Y},
    ## stored in the k-Nearest Neighbor classification model, @var{obj}.
    ##
    ## @itemize
    ## @item
    ## @var{obj} must be a @qcode{ClassificationKNN} class object.
    ## @item
    ## @var{XC} must be an @math{M*P} numeric matrix with the same number of
    ## features @math{P} as the corresponding predictors of the SVM model in
    ## @var{obj}.
    ## @end itemize
    ##
    ## @code{[@var{labels}, @var{scores}, @var{cost}] = predict (@var{obj},
    ## @var{XC})} also returns @var{scores}, which contains the predicted class
    ## scores or posterior probabilities for each instance of the corresponding
    ## unique classes, and @var{cost}, which is a matrix containing the expected
    ## cost of the classifications.  By default, @var{scores} returns the
    ## posterior probabilities for KNN models, unless a specific ScoreTransform
    ## function has been specified.  See @code{fitcknn} for more info.
    ##
    ## Note! @code{predict} is explicitly using @qcode{'exhaustive'} as the
    ## nearest search method due to the very slow implementation of
    ## @qcode{'kdtree'} in the @code{knnsearch} function.
    ##
    ## @seealso{fitcknn, ClassificationKNN, knnsearch}
    ## @end deftypefn
    function [labels, scores, cost] = predict (this, XC)

      ## Check for sufficient input arguments
      if (nargin < 2)
        error ("ClassificationKNN.predict: too few input arguments.");
      endif

      ## Check for valid XC
      if (isempty (XC))
        error ("ClassificationKNN.predict: XC is empty.");
      elseif (this.NumPredictors != columns (XC))
        error (strcat ("ClassificationKNN.predict:", ...
                       " XC must have the same number of", ...
                       " predictors as the trained model."));
      endif

      ## Get training data and labels
      used = true (rows (this.X), 1);
      X = this.X(used,:);
      Y = this.Y(used,:);

      ## Standardize (if necessary)
      if (! isempty (this.Mu))
        X = (X - this.Mu) ./ this.Sigma;
        XC = (XC - this.Mu) ./ this.Sigma;
      endif

      ## Train kNN
      if (strcmpi (this.Distance, 'seuclidean'))
        [idx, dist] = knnsearch (X, XC, 'k', this.NumNeighbors, ...
                      'NSMethod', this.NSMethod, 'Distance', 'seuclidean', ...
                      'Scale', this.DistParameter, 'sortindices', true, ...
                      'includeties', this.IncludeTies, ...
                      'bucketsize', this.BucketSize);

      elseif (strcmpi (this.Distance, 'mahalanobis'))
        [idx, dist] = knnsearch (X, XC, 'k', this.NumNeighbors, ...
                      'NSMethod', this.NSMethod, 'Distance', 'mahalanobis', ...
                      'cov', this.DistParameter, 'sortindices', true, ...
                      'includeties', this.IncludeTies, ...
                      'bucketsize', this.BucketSize);

      elseif (strcmpi (this.Distance, 'minkowski'))
        [idx, dist] = knnsearch (X, XC, 'k', this.NumNeighbors, ...
                      'NSMethod', this.NSMethod, 'Distance', 'minkowski', ...
                      'P', this.DistParameter, 'sortindices', true, ...
                      'includeties',this.IncludeTies, ...
                      'bucketsize', this.BucketSize);

      else
        [idx, dist] = knnsearch (X, XC, 'k', this.NumNeighbors, ...
                      'NSMethod', this.NSMethod, 'Distance', this.Distance, ...
                      'sortindices', true, 'includeties', this.IncludeTies, ...
                      'bucketsize', this.BucketSize);
      endif

      ## Make prediction
      if (iscellstr (this.ClassNames))
        labels = {};
      elseif (ischar (this.ClassNames))
        labels = '';
      else
        labels = [];
      endif
      scores = [];
      cost  = [];

      ## Get IDs of labels for each point in training data
      [~, ~, gY] = uniqueLabels (Y);

      ## Evaluate the K nearest neighbours for each new point
      for i = 1:rows (idx)

        ## Get K nearest neighbours
        if (this.IncludeTies)
          NN_idx = idx{i};
          NNdist = dist{i};
        else
          NN_idx = idx(i,:);
          NNdist = dist(i,:);
        endif
        k = numel (NN_idx);
        kNNgY = gY(NN_idx);

        ## Weight each neighbour by its distance and normalize, which is what
        ## DistanceWeight selects.  Equal weighting reduces to the vote count
        ## this used to be, and is the default.
        kNNgY = kNNgY(:)';
        w = this.DWfun (NNdist(:)');
        if (any (isinf (w)))
          ## A neighbour sitting on the query takes the whole vote rather than
          ## turning every share into a ratio of infinities.
          w = double (isinf (w));
        endif
        for c = 1:rows (this.ClassNames)
          freq(c) = sum (w(kNNgY == c));
        endfor
        freq = freq ./ sum (freq);

        ## Get labels according to BreakTies
        if (strcmpi (this.BreakTies, 'smallest'))
          [~, idl] = max (freq);
        else
          idl = find (freq == max (freq));
          tgn = numel (idl);
          if (tgn > 1)
            if (strcmpi (this.BreakTies, 'nearest'))
              for t = 1:tgn
                tgs(t) = find (gY(NN_idx) == idl(t));
              endfor
              [~, idm] = min (tgs);
              idl = idl(idm);
            else      # "random"
              idl = idl(randperm (numel (idl))(1));
            endif
          endif
        endif
        labels = [labels; this.ClassNames(idl,:)];

        ## The expected cost of assigning to each class is the posterior
        ## weighted by the cost matrix, sum_j P(j) * Cost(j,k).  It was
        ## 1 - freq, which is that product only for the default matrix and
        ## ignored any other.
        scores = [scores; freq];
        cost = [cost; freq * this.Cost];

      endfor

      ## Apply ScoreTransform once to the whole matrix.  Inside the loop it
      ## was applied to everything accumulated so far, so observation i of n
      ## came back transformed n-i+1 times.
      scores = this.STfun (scores);

      ## Convert double to logical if ClassNames are logical
      if (islogical (this.ClassNames))
        labels = logical (labels);
      endif

    endfunction

    ## -*- texinfo -*-
    ## @deftypefn  {ClassificationKNN} {@var{L} =} loss (@var{obj}, @var{X}, @var{Y})
    ## @deftypefnx {ClassificationKNN} {@var{L} =} loss (@dots{}, @var{name}, @var{value})
    ##
    ## Compute loss for a trained ClassificationKNN object.
    ##
    ## @code{@var{L} = loss (@var{obj}, @var{X}, @var{Y})} computes the loss,
    ## @var{L}, using the default loss function @qcode{'mincost'}.
    ##
    ## @itemize
    ## @item
    ## @code{obj} is a @var{ClassificationKNN} object trained on @code{X} and
    ## @code{Y}.
    ## @item
    ## @code{X} must be a @math{N*P} numeric matrix of input data where rows
    ## correspond to observations and columns correspond to features or
    ## variables.
    ## @item
    ## @code{Y} is @math{N*1} matrix or cell matrix containing the class labels
    ## of corresponding predictor data in @var{X}.  @var{Y} must have same
    ## numbers of Rows as @var{X}.
    ## @end itemize
    ##
    ## @code{@var{L} = loss (@dots{}, @var{name}, @var{value})} allows
    ## additional options specified by @var{name}-@var{value} pairs:
    ##
    ## @multitable @columnfractions 0.18 0.8
    ## @headitem @var{Name} @tab @var{Value}
    ##
    ## @item @qcode{'LossFun'} @tab Specifies the loss function to use.
    ## Can be a function handle with four input arguments (C, S, W, Cost)
    ## which returns a scalar value or one of:
    ## 'binodeviance', 'classifcost', 'classiferror', 'exponential',
    ## 'hinge', 'logit','mincost', 'quadratic'.
    ## @itemize
    ## @item
    ## @code{C} is a logical matrix of size @math{N*K}, where @math{N} is the
    ## number of observations and @math{K} is the number of classes.
    ## The element @code{C(i,j)} is true if the class label of the i-th
    ## observation is equal to the j-th class.
    ## @item
    ## @code{S} is a numeric matrix of size @math{N*K}, where each element
    ## represents the classification score for the corresponding class.
    ## @item
    ## @code{W} is a numeric vector of length @math{N}, representing
    ## the observation weights.
    ## @item
    ## @code{Cost} is a @math{K*K} matrix representing the misclassification
    ## costs.
    ## @end itemize
    ##
    ## @item @qcode{'Weights'} @tab Specifies observation weights, must be
    ## a numeric vector of length equal to the number of rows in X.
    ## Default is @code{ones (size (X, 1))}. loss normalizes the weights so that
    ## observation weights in each class sum to the prior probability of that
    ## class. When you supply Weights, loss computes the weighted
    ## classification loss.
    ##
    ## @end multitable
    ##
    ## @seealso{fitcknn, ClassificationKNN}
    ## @end deftypefn
    function L = loss (this, X, Y, varargin)

      ## Check for sufficient input arguments
      if (nargin < 3)
        error ("ClassificationKNN.loss: too few input arguments.");
      elseif (mod (nargin - 3, 2) != 0)
        error (strcat ("ClassificationKNN.loss: name-value", ...
                       " arguments must be in pairs."));
      elseif (nargin > 7)
        error ("ClassificationKNN.loss: too many input arguments.");
      endif

      ## Check for valid X
      if (isempty (X))
        error ("ClassificationKNN.loss: X is empty.");
      elseif (columns (this.X) != columns (X))
        error (strcat ("ClassificationKNN.loss: X must have the same", ...
                       " number of predictors as the trained model."));
      endif

      ## Default values
      LossFun = 'mincost';
      Weights = [];

      ## Validate Y
      valid_types = {'char', 'string', 'logical', 'single', 'double', 'cell'};
      if (! (any (strcmp (class (Y), valid_types))))
        error ("ClassificationKNN.loss: Y must be of a valid type.");
      endif

      ## Validate size of Y
      if (size (Y, 1) != size (X, 1))
        error (strcat ("ClassificationKNN.loss: Y must have", ...
                       " the same number of rows as X."));
      endif

      ## Parse name-value arguments
      while (numel (varargin) > 0)
        Value = varargin{2};
        switch (tolower (varargin{1}))
          case 'lossfun'
            if (isa (Value, 'function_handle'))
              ## Check if the loss function is valid
              if (nargin (Value) != 4)
                error (strcat ("ClassificationKNN.loss: custom loss function", ...
                               " must accept exactly four input arguments."));
              endif
              try
                n = 1;
                K = 2;
                C_test = false (n, K);
                S_test = zeros (n, K);
                W_test = ones (n, 1);
                Cost_test = ones (K) - eye (K);
                test_output = Value(C_test, S_test, W_test, Cost_test);
                if (! isscalar (test_output))
                  error (strcat ("ClassificationKNN.loss: custom loss", ...
                                 " function must return a scalar value."));
                endif
              catch
                error (strcat ("ClassificationKNN.loss: custom loss", ...
                               " function is not valid or does not", ...
                               " produce correct output."));
              end_try_catch
              LossFun = Value;
            elseif (ischar (Value) && any (strcmpi (Value, {'binodeviance', ...
                'classifcost', 'classiferror', 'exponential', 'hinge', ...
                'logit', 'mincost', 'quadratic'})))
              LossFun = Value;
            else
              error ("ClassificationKNN.loss: invalid loss function.");
            endif
          case 'weights'
            if (isnumeric (Value) && isvector (Value))
              if (numel (Value) != size (X ,1))
                error ("ClassificationKNN.loss: size of Weights must", ...
                       ' be equal to the number of rows in X.');
              elseif (numel (Value) == size (X, 1))
                Weights = Value;
              endif
            else
              error ("ClassificationKNN.loss: invalid Weights.");
            endif
          otherwise
            error ("ClassificationKNN.loss: invalid name-value arguments.");
        endswitch
        varargin(1:2) = [];
      endwhile

      ## Check for missing values in X
      if (! isa (LossFun, 'function_handle'))
        lossfun = tolower (LossFun);
        if (! strcmp (lossfun, 'mincost') && ! strcmp (lossfun, ...
         'classiferror') && ! strcmp (lossfun, 'classifcost') ...
          && any (isnan (X(:))))
            L = NaN;
            return;
        endif
      endif

      ## If Y is a char array convert it to a cell array of character vectors
      classes = this.ClassNames;
      if (ischar (Y) && ischar (classes))
        Y = cellstr (Y);
        classes = cellstr (classes);
      endif

      ## Check that Y is of the same type as ClassNames
      if (! strcmp (class (Y), class (classes)))
        error (strcat ("ClassificationKNN.loss: Y must be the", ...
                       " same data type as the model's ClassNames."));
      endif

      ## Check if Y contains correct classes
      if (! labelsKnown (Y, this.ClassNames))
        error (strcat ("ClassificationKNN.loss: Y must contain only", ...
                       " the classes in model's ClassNames."));
      endif

      ## Set default weights if not specified
      if (isempty (Weights))
        Weights = ones (size (X, 1), 1);
      endif

      ## Normalize Weights
      K = classCount (classes);
      class_prior_probs = this.Prior;
      norm_weights = zeros (size (Weights));
      for i = 1:K
        class_idx = ismember (Y, classes(i));
        if (sum (Weights(class_idx)) > 0)
          norm_weights(class_idx) = ...
          Weights(class_idx) * class_prior_probs(i) / sum (Weights(class_idx));
        endif
      endfor
      Weights = norm_weights / sum (norm_weights);

      ## Number of observations
      n = size (X, 1);

      ## Predict classification scores
      [label, scores] = predict (this, X);
      if (ischar (label))
        label = cellstr (label);
      endif

      ## C is vector of K-1 zeros, with 1 in the
      ## position corresponding to the true class
      C = false (n, K);
      ## Resolve every observation's class once, rather than once per
      ## iteration: the lookup does not depend on i.
      [gYidx, ~] = labelIndices (classes, Y);
      for i = 1:n
        class_idx = gYidx(i);
        C(i, class_idx) = true;
      endfor
      Y_new = C';

      ## Compute the loss using custom loss function
      if (isa (LossFun, 'function_handle'))
        L = LossFun(C, scores, Weights, this.Cost);
        return;
      endif

      ## Compute the scalar classification score for each observation
      m_j = zeros (n, 1);
      for i = 1:n
        m_j(i) = scores(i,:) * Y_new(:,i);
      endfor

      ## Compute the loss
      switch (tolower (LossFun))
        case 'binodeviance'
          b = log (1 + exp (-2 * m_j));
          L = (Weights') * b;
        case 'hinge'
          h = max (0, 1 - m_j);
          L = (Weights') * h;
        case 'exponential'
          e = exp (-m_j);
          L = (Weights') * e;
        case 'logit'
          l = log (1 + exp (-m_j));
          L = (Weights') * l;
        case 'quadratic'
          q = (1 - m_j) .^ 2;
          L = (Weights') * q;
        case 'classiferror'
          L = 0;
          for i = 1:n
            L = L + Weights(i) * (! isequal (Y(i), label(i)));
          endfor
        case 'mincost'
          Cost = this.Cost;
          L = 0;
          ## Resolve every observation's class once, rather than once per
          ## iteration: the lookup does not depend on i.
          [gYidx, ~] = labelIndices (classes, Y);
          for i = 1:n
            f_Xj = scores(i, :);
            gamma_jk = f_Xj * Cost;
            [~, min_cost_class] = min (gamma_jk);
            cj = Cost(gYidx(i), min_cost_class);
            L = L + Weights(i) * cj;
          endfor
        case 'classifcost'
          Cost = this.Cost;
          L = 0;
          ## Resolve every observation's class once, rather than once per
          ## iteration: the lookup does not depend on i.
          [gYidx, ~] = labelIndices (classes, Y);
          for i = 1:n
            y_idx = gYidx(i);
            y_hat_idx = find (ismember (classes, label(i)));
            L = L + Weights(i) * Cost(y_idx, y_hat_idx);
          endfor
        otherwise
          error ("ClassificationKNN.loss: invalid loss function.");
      endswitch

    endfunction

    ## -*- texinfo -*-
    ## @deftypefn {ClassificationKNN} {@var{m} =} margin (@var{obj}, @var{X}, @var{Y})
    ##
    ## @code{@var{m} = margin (@var{obj}, @var{X}, @var{Y})} returns
    ## the classification margins for @var{obj} with data @var{X} and
    ## classification @var{Y}. @var{m} is a numeric vector of length size (X,1).
    ##
    ## @itemize
    ## @item
    ## @code{obj} is a @var{ClassificationKNN} object trained on @code{X}
    ## and @code{Y}.
    ## @item
    ## @code{X} must be a @math{N*P} numeric matrix of input data where rows
    ## correspond to observations and columns correspond to features or
    ## variables.
    ## @item
    ## @code{Y} is @math{N*1} matrix or cell matrix containing the class labels
    ## of corresponding predictor data in @var{X}. @var{Y} must have same
    ## numbers of Rows as @var{X}.
    ## @end itemize
    ##
    ## The classification margin for each observation is the difference between
    ## the classification score for the true class and the maximal
    ## classification score for the false classes.
    ##
    ## @seealso{fitcknn, ClassificationKNN}
    ## @end deftypefn
    function m = margin (this, X, Y)

      ## Check for sufficient input arguments
      if (nargin < 3)
        error ("ClassificationKNN.margin: too few input arguments.");
      endif

      ## Check for valid X
      if (isempty (X))
        error ("ClassificationKNN.margin: X is empty.");
      elseif (columns (this.X) != columns (X))
        error (strcat ("ClassificationKNN.margin: X must have the same", ...
                       " number of predictors as the trained model."));
      endif

      ## Validate Y
      valid_types = {'char', 'string', 'logical', 'single', 'double', 'cell'};
      if (! (any (strcmp (class (Y), valid_types))))
        error ("ClassificationKNN.margin: Y must be of a valid type.");
      endif

      ## Validate X
      valid_types = {'single', 'double'};
      if (! (any (strcmp (class (X), valid_types))))
        error ("ClassificationKNN.margin: X must be of a valid type.");
      endif

      ## Validate size of Y
      if (size (Y, 1) != size (X, 1))
        error (strcat ("ClassificationKNN.margin: Y must have", ...
                       " the same number of rows as X."));
      endif

      ## If Y is a char array convert it to a cell array of character vectors
      classes = this.ClassNames;
      if (ischar (Y) && ischar (classes))
        Y = cellstr (Y);
        classes = cellstr (classes);
      endif

      ## Check that Y is of the same type as ClassNames
      if (! strcmp (class (Y), class (classes)))
        error (strcat ("ClassificationKNN.margin: Y must be the", ...
                       " same data type as the model's ClassNames."));
      endif

      ## Check if Y contains correct classes
      if (! labelsKnown (Y, classes))
        error (strcat ("ClassificationKNN.margin: Y must contain", ...
                       " only the classes in model's ClassNames."));
      endif

      ## Number of Observations
      n = size (X, 1);

      ## Initialize the margin vector
      m = zeros (n, 1);

      ## Calculate the classification scores
      [~, scores] = predict (this, X);

      ## Loop over each observation to compute the margin
      ## Resolve every observation's class once, rather than once per
      ## iteration: the lookup does not depend on i.
      [gYidx, ~] = labelIndices (classes, Y);
      for i = 1:n
        ## True class index
        true_class_idx = gYidx(i);

        ## Score for the true class
        true_class_score = scores(i, true_class_idx);

        ## Get the maximal score for the false classes
        scores(i, true_class_idx) = -Inf;              ## Temporarily
        max_false_class_score = max (scores(i, :));
        if (max_false_class_score == -Inf)
          m = NaN;
          return;
        endif
        scores(i, true_class_idx) = true_class_score;  ## Restore

        ## Calculate the margin
        m(i) = true_class_score - max_false_class_score;
      endfor

    endfunction

    ## -*- texinfo -*-
    ## @deftypefn  {ClassificationKNN} {@var{[pd, x, y]} =} partialDependence (@var{obj}, @var{Vars}, @var{Labels})
    ## @deftypefnx {ClassificationKNN} {@var{[pd, x, y]} =} partialDependence (@dots{}, @var{Data})
    ## @deftypefnx {ClassificationKNN} {@var{[pd, x, y]} =} partialDependence (@dots{}, @var{name}, @var{value})
    ##
    ## Compute partial dependence for a trained ClassificationKNN object.
    ##
    ## @code{@var{[pd, x, y]} = partialDependence (@var{obj}, @var{Vars},
    ## @var{Labels})}
    ## computes the partial dependence of the classification scores on the
    ## variables @var{Vars} for the specified class @var{Labels}.
    ##
    ## @itemize
    ## @item
    ## @code{obj} is a trained @var{ClassificationKNN} object.
    ## @item
    ## @code{Vars} is a vector of positive integers, character vector,
    ## string array, or cell array of character
    ## vectors representing predictor variables (it can be indices of
    ## predictor variables in @var{obj.X}).
    ## @item
    ## @code{Labels} is a character vector, logical vector, numeric vector,
    ## or cell array of character vectors representing class
    ## labels. (column vector)
    ## @end itemize
    ##
    ## @code{@var{[pd, x, y]} = partialDependence (@dots{}, @var{Data})}
    ## specifies new predictor data to use for computing the partial dependence.
    ##
    ## @code{@var{[pd, x, y]} = partialDependence (@dots{}, @var{name},
    ## @var{value})} allows additional options specified by name-value pairs:
    ##
    ## @multitable @columnfractions 0.32 0.7
    ## @headitem @var{Name} @tab @var{Value}
    ##
    ## @item @qcode{'NumObservationsToSample'} @tab Number of
    ## observations to sample. Must be a positive integer. Defaults to the
    ## number of observations in the training data.
    ## @item @qcode{'QueryPoints'} @tab Points at which to evaluate
    ## the partial dependence.
    ## Must be a numeric column vector, numeric two-column matrix, or
    ## cell array of character column vectors.
    ## @item @qcode{'UseParallel'} @tab Logical value indicating
    ## whether to perform computations in parallel.
    ## Defaults to @code{false}.
    ## @end multitable
    ##
    ## @subheading Return Values
    ## @itemize
    ## @item @code{pd}: Partial dependence values.
    ## @item @code{x}: Query points for the first predictor variable in Vars.
    ## @item @code{y}: Query points for the second predictor variable in
    ## Vars (if applicable).
    ## @end itemize
    ##
    ## @seealso{fitcknn, ClassificationKNN}
    ## @end deftypefn

    function [pd, x, y] = partialDependence (this, Vars, Labels, varargin)
      if (nargin < 3)
        error ("ClassificationKNN.partialDependence: too few input arguments.");
      endif

      ## Validate Vars
      if (isnumeric (Vars))
        if (! all (Vars > 0) || ! (numel (Vars) == 1 || numel (Vars) == 2))
          error ("ClassificationKNN.partialDependence: VARS must be a", ...
                 ' positive integer or vector of two positive integers.');
        endif
      elseif (iscellstr (Vars))
        if (! (numel (Vars) == 1 || numel (Vars) == 2))
          error (strcat ("ClassificationKNN.partialDependence: VARS must", ...
                         " be a string array or cell array of one or two", ...
                         " character vectors."));
        endif
        Vars = cellfun (@(v) find (strcmp (this.PredictorNames, v)), Vars);
      elseif (ischar (Vars))
        Vars = find (strcmp (this.PredictorNames, Vars));
        if (isempty (Vars))
          error (strcat ("ClassificationKNN.partialDependence: VARS", ...
                         " must match one of the predictor names."));
        endif
      else
        error (strcat ("ClassificationKNN.partialDependence: VARS", ...
                       " must be a string, or cell array."));
      endif

      ## Validate Labels
      if (! (ischar (Labels) || islogical (Labels) || ...
          isnumeric (Labels) || iscellstr (Labels) || islogical (Labels)))
        error ("ClassificationKNN.partialDependence: invalid type for LABELS.");
      endif

      ## If Labels is a char array convert it to a cell array of character vectors
      classes = this.ClassNames;
      if (ischar (Labels) && ischar (classes))
        Labels = cellstr (Labels);
        classes = cellstr (classes);
      endif

      ## Check that Y is of the same type as ClassNames
      if (! strcmp (class (Labels), class (classes)))
        error (strcat ("ClassificationKNN.margin: LABELS must be the", ...
                       " same data type as the model's ClassNames."));
      endif

      ## Additional validation to match ClassNames
      if (! all (ismember (Labels, classes)))
        error (strcat ("ClassificationKNN.partialDependence: LABELS must", ...
                       " match the class names in the model's ClassNames."));
      endif

      ## Default values
      Data = this.X;
      UseParallel = false;
      NumObservationsToSample = size (Data, 1);
      QueryPoints = [];

      ## Check for Data and other optional arguments
      if (nargin > 3)
        if (size (varargin{1}) == size (this.X))
          Data = varargin{1};
          ## Ensure Data consistency
          if (! all (size (Data, 2) == numel (this.PredictorNames)))
            error (strcat ("ClassificationKNN.partialDependence: DATA must", ...
                           " have the same number and order of columns as", ...
                           " the predictor variables."));
          endif

          ## Ensure Name-Value pairs are even length
          if (mod (nargin - 4, 2) != 0)
            error (strcat ("ClassificationKNN.partialDependence:", ...
                           " name-value arguments must be in pairs."));
          endif

          ## Set the number of observations to sample
          NumObservationsToSample = size (Data, 1);
          idx = 2;
        else
          ## Ensure Name-Value pairs are even length
          if (mod (nargin - 3, 2) != 0)
            error (strcat ("ClassificationKNN.partialDependence:", ...
                           " name-value arguments must be in pairs."));
          endif
          idx = 1;
        endif

        ## Handle name-value pair arguments
        for i = idx:2:length (varargin)
          if (! ischar (varargin{i}))
            error (strcat ("ClassificationKNN.partialDependence: name", ...
                           " arguments must be strings."));
          endif
          Value = varargin{i+1};
          ## Parse name-value pairs
          switch (lower (varargin{i}))
            case 'numobservationstosample'
              if (! isnumeric (Value) || Value <= 0 || Value != round (Value))
                error (strcat ("ClassificationKNN.partialDependence:", ...
                               " NumObservationsToSample must be a", ...
                               " positive integer."));
              endif
              NumObservationsToSample = Value;
              if (Value > size (Data, 1))
                NumObservationsToSample = size (Data, 1);
              endif
            case 'querypoints'
              if (! isnumeric (Value) && ! iscell (Value))
                error (strcat ("ClassificationKNN.partialDependence:", ...
                               " QueryPoints must be a numeric column", ...
                               " vector, numeric two-column matrix, or", ...
                               " cell array of character column vectors."));
              endif
              QueryPoints = Value;
            case 'useparallel'
              if (! islogical (UseParallel))
                error (strcat ("ClassificationKNN.partialDependence:", ...
                               " UseParallel must be a logical value."));
              endif
              UseParallel = Value;
            otherwise
              error (strcat ("ClassificationKNN.partialDependence:", ...
                             " name-value pair argument not recognized."));
          endswitch
        endfor
      endif

      ## Sample observations if needed
      if (NumObservationsToSample < size (Data, 1))
        Data = datasample (Data, NumObservationsToSample, 'Replace', false);
      endif

      ## Generate QueryPoints if not specified
      if (isempty (QueryPoints))
        if (numel (Vars) == 1)
          if (isnumeric (Data(:, Vars)))
            QueryPoints = linspace (min (Data(:, Vars)), ...
                                max (Data(:, Vars)), 100)';
          else
            QueryPoints = unique (Data(:, Vars));
          endif
        else
          QueryPoints = cell (1, numel (Vars));
          for j = 1:numel (Vars)
            if (isnumeric (Data(:, Vars(j))))
              QueryPoints{j} = linspace (min (Data(:, Vars(j))), ...
                                max (Data(:, Vars(j))), 100)';
            else
              QueryPoints{j} = unique (Data(:, Vars(j)));
            endif
          endfor
        endif
      endif

      ## Prepare grid points for predictions
      if (numel (Vars) == 1)
        gridPoints = QueryPoints;
      else
        if (ischar (QueryPoints))
          [X1, X2] = meshgrid (QueryPoints(1), QueryPoints(2));
        else
          [X1, X2] = meshgrid (QueryPoints{1}, QueryPoints{2});
        endif
        gridPoints = [X1(:), X2(:)];
      endif

      ## Predict responses for the grid points
      numClasses = classCount (classes);
      numQueryPoints = size (gridPoints, 1);
      predictions = zeros (numQueryPoints, numClasses);

      if (UseParallel)
        parfor i = 1:numQueryPoints
          tempData = Data;
          for j = 1:numel (Vars)
            tempData(:, Vars(j)) = repmat (gridPoints(i, j), ...
                                    NumObservationsToSample, 1);
          endfor
          [~, scores] = predict (this, tempData);
          predictions(i, :) = mean (scores, 1);
        endparfor
      else
        for i = 1:numQueryPoints
          tempData = Data;
          for j = 1:numel (Vars)
            tempData(:, Vars(j)) = repmat (gridPoints(i, j), ...
                                    NumObservationsToSample, 1);
          endfor
          [~, scores] = predict (this, tempData);
          predictions(i, :) = mean (scores, 1);
        endfor
      endif

      ## Compute partial dependence
      if (numel (Vars) == 1)
        if (numel (Labels) == 1)
          classIndex = find (ismember (classes, Labels));
          pd = predictions(:, classIndex)';
        else
          pd = zeros (numel (Labels), numel (QueryPoints));
          for j = 1:numel (Labels)
            classIndex = find (ismember (classes, Labels(j)));
            pd(j, :) = predictions(:, classIndex)';
          endfor
        endif
        x = QueryPoints;
        y = [];
      else
        if (numel (Labels) == 1)
          classIndex = find (ismember (classes, Labels));
          pd = reshape (predictions(:, classIndex), numel (QueryPoints{1}), ...
                        numel (QueryPoints{2}));
        else
          pd = zeros (numel (Labels), numel (QueryPoints{1}), ...
                      numel (QueryPoints{2}));
          for j = 1:numel (Labels)
            classIndex = find (ismember (classes, Labels(j)));
            pd(j, :, :) = reshape (predictions(:, classIndex), ...
                                   numel (QueryPoints{1}), ...
                                   numel (QueryPoints{2}));
          endfor
        endif
        x = QueryPoints{1};
        y = QueryPoints{2};
      endif

    endfunction

    ## -*- texinfo -*-
    ## @deftypefn  {ClassificationKNN} {@var{CVMdl} =} crossval (@var{obj})
    ## @deftypefnx {ClassificationKNN} {@var{CVMdl} =} crossval (@dots{}, @var{Name}, @var{Value})
    ##
    ## Cross Validate a ClassificationKNN object.
    ##
    ## @code{@var{CVMdl} = crossval (@var{obj})} returns a cross-validated model
    ## object, @var{CVMdl}, from a trained model, @var{obj}, using 10-fold
    ## cross-validation by default.
    ##
    ## @code{@var{CVMdl} = crossval (@var{obj}, @var{name}, @var{value})}
    ## specifies additional name-value pair arguments to customize the
    ## cross-validation process.
    ##
    ## @multitable @columnfractions 0.28 0.7
    ## @headitem @var{Name} @tab @var{Value}
    ##
    ## @item @qcode{'KFold'} @tab Specify the number of folds to use in
    ## k-fold cross-validation.  @code{"KFold", @var{k}}, where @var{k} is an
    ## integer greater than 1.
    ##
    ## @item @qcode{'Holdout'} @tab Specify the fraction of the data to
    ## hold out for testing.  @code{"Holdout", @var{p}}, where @var{p} is a
    ## scalar in the range @math{(0,1)}.
    ##
    ## @item @qcode{'Leaveout'} @tab Specify whether to perform
    ## leave-one-out cross-validation.  @code{"Leaveout", @var{Value}}, where
    ## @var{Value} is 'on' or 'off'.
    ##
    ## @item @qcode{'CVPartition'} @tab Specify a @qcode{cvpartition}
    ## object used for cross-validation.  @code{"CVPartition", @var{cv}}, where
    ## @code{isa (@var{cv}, "cvpartition")} = 1.
    ##
    ## @end multitable
    ##
    ## @seealso{fitcknn, ClassificationKNN, cvpartition,
    ## ClassificationPartitionedModel}
    ## @end deftypefn

    function CVMdl = crossval (this, varargin)
      ## Check input
      if (nargin < 1)
        error ("ClassificationKNN.crossval: too few input arguments.");
      endif

      if (numel (varargin) == 1)
        error (strcat ("ClassificationKNN.crossval: Name-Value", ...
                       " arguments must be in pairs."));
      elseif (numel (varargin) > 2)
        error (strcat ("ClassificationKNN.crossval: specify only one", ...
                       " of the optional Name-Value paired arguments."));
      endif

      ## Add default values
      if (this.NumObservations < 10)
        numFolds  = this.NumObservations;
      else
        numFolds  = 10;
      endif
      Holdout     = [];
      Leaveout    = 'off';
      CVPartition = [];

      ## Parse extra parameters
      while (numel (varargin) > 0)
        switch (tolower (varargin {1}))

          case 'kfold'
            numFolds = varargin{2};
            if (! (isnumeric (numFolds) && isscalar (numFolds)
                   && (numFolds == fix (numFolds)) && numFolds > 1))
              error (strcat ("ClassificationKNN.crossval: 'KFold' must", ...
                             " be an integer value greater than 1."));
            endif

          case 'holdout'
            Holdout = varargin{2};
            if (! (isnumeric (Holdout) && isscalar (Holdout) && Holdout > 0
                   && Holdout < 1))
              error (strcat ("ClassificationKNN.crossval: 'Holdout' must", ...
                             " be a numeric value between 0 and 1."));
            endif

          case 'leaveout'
            Leaveout = varargin{2};
            if (! (ischar (Leaveout)
                   && (strcmpi (Leaveout, 'on') || strcmpi (Leaveout, 'off'))))
              error (strcat ("ClassificationKNN.crossval: 'Leaveout'", ...
                             " must be either 'on' or 'off'."));
            endif

          case 'cvpartition'
            CVPartition = varargin{2};
            if (! (isa (CVPartition, 'cvpartition')))
              error (strcat ("ClassificationKNN.crossval: 'CVPartition'",...
                             " must be a 'cvpartition' object."));
            endif

          otherwise
            error (strcat ("ClassificationKNN.crossval: invalid",...
                           " parameter name in optional paired arguments."));
          endswitch
        varargin(1:2) = [];
      endwhile

      ## Determine the cross-validation method to use.  The partition covers
      ## the observations actually trained on: a row dropped for a missing
      ## value is not one the folds can use, and including it would leave the
      ## partition, the stored data and NumObservations disagreeing.  The
      ## response is passed rather than a count so the folds stay stratified.
      Yused = this.Y;
      if (! isempty (CVPartition))
        partition = CVPartition;
      elseif (! isempty (Holdout))
        partition = cvpartition (Yused, 'Holdout', Holdout);
      elseif (strcmpi (Leaveout, 'on'))
        partition = cvpartition (this.NumObservations, 'LeaveOut');
      else
        partition = cvpartition (Yused, 'KFold', numFolds);
      endif

      ## Create a cross-validated model object
      CVMdl = ClassificationPartitionedModel (this, partition);

    endfunction

    ## -*- texinfo -*-
    ## @deftypefn  {ClassificationKNN} {@var{e} =} edge (@var{obj}, @var{X}, @var{Y})
    ## @deftypefnx {ClassificationKNN} {@var{e} =} edge (@dots{}, @qcode{"Weights"}, @var{w})
    ##
    ## Classification edge, the mean of the classification margins.
    ##
    ## @code{@var{e} = edge (@var{obj}, @var{X}, @var{Y})} reduces the vector
    ## that @code{margin} returns to a single number, the mean margin over the
    ## rows of @var{X}.  It says how far the model puts the true class ahead of
    ## its nearest rival on average, so a larger edge is a better model, and
    ## unlike a loss it is not bounded above and rewards confidence rather than
    ## bare correctness.
    ##
    ## @code{@var{e} = edge (@dots{}, @qcode{"Weights"}, @var{w})} takes the
    ## weighted mean instead, with one weight per row of @var{X}.
    ##
    ## @end deftypefn
    function e = edge (this, X, Y, varargin)

      if (nargin < 3)
        error ("ClassificationKNN.edge: too few input arguments.");
      endif
      if (mod (numel (varargin), 2) != 0)
        error (strcat ("ClassificationKNN.edge: Name-Value", ...
                       " arguments must be in pairs."));
      endif

      ## The weights are parsed before anything is computed, so a bad
      ## Name-Value pair is reported as such rather than after a margin.
      W = edgeWeights (varargin, Y, this.ClassNames, this.Prior, ...
                       "ClassificationKNN", "edge");
      m = margin (this, X, Y);
      e = sum (W .* m(:)) / sum (W);

    endfunction

    ## -*- texinfo -*-
    ## @deftypefn  {ClassificationKNN} {@var{label} =} resubPredict (@var{obj})
    ## @deftypefnx {ClassificationKNN} {[@var{label}, @var{score}, @var{cost}] =} resubPredict (@var{obj})
    ##
    ## Classify the training data with the model fitted to it.
    ##
    ## @code{@var{label} = resubPredict (@var{obj})} is @code{predict} applied
    ## to the observations the model was fitted on, which it holds in
    ## @qcode{X}.  Handing them over yourself is not the same thing: a row
    ## dropped for a missing response is not in @qcode{X}, so the original
    ## matrix and the model's own are different data.
    ##
    ## The result measures fit and not generalization, and is optimistic by
    ## construction.  @code{crossval} is what estimates performance on data the
    ## model has not seen.
    ##
    ## @end deftypefn
    function [label, score, cost] = resubPredict (this)
      [label, score, cost] = predict (this, this.X);
    endfunction

    ## -*- texinfo -*-
    ## @deftypefn {ClassificationKNN} {@var{m} =} resubMargin (@var{obj})
    ##
    ## Classification margins of the model on its own training data.
    ##
    ## @code{@var{m} = resubMargin (@var{obj})} is @code{margin} applied to the
    ## observations the model was fitted on, one number per observation.  Being
    ## a resubstitution quantity it is optimistic by construction.
    ##
    ## @end deftypefn
    function m = resubMargin (this)
      m = margin (this, this.X, this.Y);
    endfunction

    ## -*- texinfo -*-
    ## @deftypefn {ClassificationKNN} {@var{e} =} resubEdge (@var{obj})
    ##
    ## Classification edge of the model on its own training data.
    ##
    ## @code{@var{e} = resubEdge (@var{obj})} is @code{edge} applied to the
    ## observations the model was fitted on, the mean of @code{resubMargin}.
    ##
    ## @end deftypefn
    function e = resubEdge (this)
      e = edge (this, this.X, this.Y);
    endfunction

    ## -*- texinfo -*-
    ## @deftypefn  {ClassificationKNN} {@var{L} =} resubLoss (@var{obj})
    ## @deftypefnx {ClassificationKNN} {@var{L} =} resubLoss (@dots{}, @var{name}, @var{value})
    ##
    ## Classification loss of the model on its own training data.
    ##
    ## @code{@var{L} = resubLoss (@var{obj})} is @code{loss} applied to the
    ## observations the model was fitted on, defaulting to
    ## @qcode{'mincost'}, and it accepts the same @qcode{Name-Value} pairs.
    ##
    ## Being a resubstitution quantity it is a lower bound on the error rather
    ## than an estimate of it.  It is worth least on a lazy learner: a
    ## one-neighbour @code{ClassificationKNN} has a resubstitution loss of
    ## exactly zero, every training point being its own nearest neighbour.
    ##
    ## @end deftypefn
    function L = resubLoss (this, varargin)
      L = loss (this, this.X, this.Y, varargin{:});
    endfunction

    ## -*- texinfo -*-
    ## @deftypefn  {ClassificationKNN} {} savemodel (@var{obj}, @var{filename})
    ##
    ## Save a ClassificationKNN object.
    ##
    ## @code{savemodel (@var{obj}, @var{filename})} saves each property of a
    ## ClassificationKNN object into an Octave binary file, the name of which is
    ## specified in @var{filename}, along with an extra variable, which defines
    ## the type classification object these variables constitute.  Use
    ## @code{loadmodel} in order to load a classification object into Octave's
    ## workspace.
    ##
    ## @seealso{loadmodel, fitcknn, ClassificationKNN}
    ## @end deftypefn

    function savemodel (this, fname)
      if (nargin < 2)
        error ("ClassificationKNN.savemodel: too few input arguments.");
      endif
      if (! (ischar (fname) && isrow (fname) && ! isempty (fname)))
        error ("ClassificationKNN.savemodel: FNAME must be a character vector.");
      endif
      ## Generate variable for class name
      classdef_name = 'ClassificationKNN';

      ## Create variables from model properties
      X = this.X;
      Y = this.Y;
      NumObservations = this.NumObservations;
      W               = this.W;
      RowsUsed        = this.RowsUsed;
      Sigma           = this.Sigma;
      BinEdges        = this.BinEdges;
      Mu              = this.Mu;
      NumPredictors   = this.NumPredictors;
      PredictorNames  = this.PredictorNames;
      ResponseName    = this.ResponseName;
      ClassNames      = this.ClassNames;
      Prior           = this.Prior;
      Cost            = this.Cost;
      ScoreTransform  = this.ScoreTransform;
      BreakTies       = this.BreakTies;
      NumNeighbors    = this.NumNeighbors;
      Distance        = this.Distance;
      DistanceWeight  = this.DistanceWeight;
      DWfun           = this.DWfun;
      DistParameter   = this.DistParameter;
      NSMethod        = this.NSMethod;
      IncludeTies     = this.IncludeTies;
      BucketSize      = this.BucketSize;
      CacheSize       = this.CacheSize;
      ModelParameters = this.ModelParameters;
      STfun          = this.STfun;
      CategoricalPredictors  = this.CategoricalPredictors;
      ExpandedPredictorNames = this.ExpandedPredictorNames;

      ## Save classdef name and all model properties as individual variables
      HyperparameterOptimizationResults = this.HyperparameterOptimizationResults;
      save ('-binary', fname, 'classdef_name', 'X', 'Y', 'NumObservations', ...
            'W', 'RowsUsed', 'Sigma', 'Mu', 'NumPredictors', ...
            'PredictorNames', 'BinEdges', 'ResponseName', 'ClassNames', ...
            'Prior', 'Cost', ...
            'ScoreTransform', 'BreakTies', 'NumNeighbors', 'Distance', ...
            'DistanceWeight', 'DWfun', 'DistParameter', 'NSMethod', ...
            'IncludeTies', ...
            'BucketSize', 'CacheSize', 'ModelParameters', ...
            'CategoricalPredictors', ...
            'ExpandedPredictorNames', 'STfun', ...
            'HyperparameterOptimizationResults');
    endfunction

  endmethods

  methods(Static, Hidden)

    function mdl = load_model (filename, data)
      ## Create a ClassificationKNN object
      mdl = ClassificationKNN (1, 1);

      ## Copy the saved data into the object.  Iterate over what was
      ## saved rather than over fieldnames (mdl): a private property such
      ## as STfun is written out by savemodel but is not reported by
      ## fieldnames, so comparing the two sets could never match and every
      ## load failed.  Assignment is legal here because this is a method of
      ## the class itself.
      names = fieldnames (data);
      ## set.Distance validates the metric against NSMethod, which the model
      ## built above still holds at its default of 'kdtree', so a saved model
      ## searched exhaustively with a metric no kd-tree can take was refused
      ## by its own loader until NSMethod is in place first.
      early = ismember (names, {'NSMethod'});
      ## The set methods for these read other properties, and one of them
      ## rebuilds Coeffs, so they are assigned once everything else is in
      ## place rather than in the order the file happens to list them.
      late = ismember (names, {'Cost', 'Prior', 'ScoreTransform', ...
                               'ResponseTransform'});
      names = [names(early); names(! early & ! late); names(late)];
      for i = 1:numel (names)
        try
          mdl.(names{i}) = data.(names{i});
        catch
          error ("ClassificationKNN.load_model: invalid model in '%s'.", filename)
        end_try_catch
      endfor
    endfunction

  endmethods

  methods(Access = private)



  endmethods

endclassdef

## Helper functions for ScoreTransform
function out = ismax (score)
  out = score;
  out(score == max (score)) = 1;
  out(score != max (score)) = 0;
endfunction

function out = symmetricismax (score)
  out = score;
  out(score == max (score)) = 1;
  out(score != max (score)) = -1;
endfunction

%!demo
%! ## Create a k-nearest neighbor classifier for Fisher's iris data with k = 5.
%! ## Evaluate some model predictions on new data.
%!
%! load fisheriris
%! x = meas;
%! y = species;
%! xc = [min(x); mean(x); max(x)];
%! obj = fitcknn (x, y, 'NumNeighbors', 5, 'Standardize', 1);
%! [label, score, cost] = predict (obj, xc)

%!demo
%! load fisheriris
%! x = meas;
%! y = species;
%! obj = fitcknn (x, y, 'NumNeighbors', 5, 'Standardize', 1);
%!
%! ## Create a cross-validated model
%! CVMdl = crossval (obj)

%!demo
%! load fisheriris
%! x = meas;
%! y = species;
%! covMatrix = cov (x);
%!
%! ## Fit the k-NN model using the 'mahalanobis' distance
%! ## and the custom covariance matrix
%! obj = fitcknn (x, y, 'NumNeighbors', 5, 'Distance','mahalanobis', ...
%! 'Cov', covMatrix);
%!
%! ## Create a partition model using cvpartition
%! Partition = cvpartition (size (x, 1), 'kfold', 12);
%!
%! ## Create cross-validated model using 'cvPartition' name-value argument
%! CVMdl = crossval (obj, 'cvPartition', Partition)
%!
%! ## Access the trained model from first fold of cross-validation
%! CVMdl.Trained{1}

%!demo
%! X = [1, 2; 3, 4; 5, 6];
%! Y = {'A'; 'B'; 'A'};
%! model = fitcknn (X, Y);
%! customLossFun = @(C, S, W, Cost) sum (W .* sum (abs (C - S), 2));
%! ## Calculate loss using custom loss function
%! L = loss (model, X, Y, 'LossFun', customLossFun)

%!demo
%! X = [1, 2; 3, 4; 5, 6];
%! Y = {'A'; 'B'; 'A'};
%! model = fitcknn (X, Y);
%! ## Calculate loss using 'mincost' loss function
%! L = loss (model, X, Y, 'LossFun', 'mincost')

%!demo
%! X = [1, 2; 3, 4; 5, 6];
%! Y = ['1'; '2'; '3'];
%! model = fitcknn (X, Y);
%! X_test = [3, 3; 5, 7];
%! Y_test = ['1'; '2'];
%! ## Specify custom Weights
%! W = [1; 2];
%! L = loss (model, X_test, Y_test, 'LossFun', 'logit', 'Weights', W);

%!demo
%! load fisheriris
%! mdl = fitcknn (meas, species);
%! X = mean (meas);
%! Y = {'versicolor'};
%! m = margin (mdl, X, Y)

%!demo
%! X = [1, 2; 4, 5; 7, 8; 3, 2];
%! Y = [2; 1; 3; 2];
%! ## Train the model
%! mdl = fitcknn (X, Y);
%! ## Specify Vars and Labels
%! Vars = 1;
%! Labels = 2;
%! ## Calculate partialDependence
%! [pd, x, y] = partialDependence (mdl, Vars, Labels);

%!demo
%! X = [1, 2; 4, 5; 7, 8; 3, 2];
%! Y = [2; 1; 3; 2];
%! ## Train the model
%! mdl = fitcknn (X, Y);
%! ## Specify Vars and Labels
%! Vars = 1;
%! Labels = 1;
%! queryPoints = [linspace(0, 1, 3)', linspace(0, 1, 3)'];
%! ## Calculate partialDependence using queryPoints
%! [pd, x, y] = partialDependence (mdl, Vars, Labels, 'QueryPoints', ...
%! queryPoints)

## Test constructor with custom Distance as function_handle (issue #459)
%!test
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'b'; 'b'];
%! f = @(d1,d2) sqrt (sum ((d1 - d2) .^ 2, 2));
%! a = ClassificationKNN (x, y, 'Distance', f);
%!error<ClassificationKNN: invalid function handle for distance metric.> ...
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'b'; 'b'];
%! f = @(d1,d2) sqrt (dot (d1, d2));
%! a = ClassificationKNN (x, y, 'Distance', f);
%!error<ClassificationKNN: custom distance function produces wrong output size.> ...
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'b'; 'b'];
%! f = @(d1,d2) sqrt (sum ((d1 - d2) .^ 2, 1));
%! a = ClassificationKNN (x, y, 'Distance', f);
%!test
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'b'; 'b'];
%! f = @(d1,d2) sqrt (sum ((d1 - d2) .^ 2, 2));
%! a = ClassificationKNN (x, y, 'NSMethod', 'exhaustive', 'Distance', ...
%! 'cityblock');
%! a.Distance = f;
%! assert_equal (a.Distance, f);
%! assert_equal (isempty (a.DistParameter), true);
%!error<ClassificationKNN: invalid function handle for distance metric.> ...
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'b'; 'b'];
%! a = ClassificationKNN (x, y, 'NSMethod', 'exhaustive');
%! a.Distance = @(d1,d2) sqrt (dot (d1, d2));
%!error<ClassificationKNN: custom distance function produces wrong output size.> ...
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'b'; 'b'];
%! a = ClassificationKNN (x, y, 'NSMethod', 'exhaustive');
%! a.Distance = @(d1,d2) sqrt (sum ((d1 - d2) .^ 2, 1));
%!error<ClassificationKNN: 'Distance' for a kd-tree model can only be 'euclidean', 'cityblock', 'manhattan', 'chebychev', or 'minkowski'.> ...
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'b'; 'b'];
%! a = ClassificationKNN (x, y);
%! a.Distance = @(d1,d2) sqrt (sum ((d1 - d2) .^ 2, 2));

## Test constructor with NSMethod and NumNeighbors parameters
%!test
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'b'; 'b'];
%! a = ClassificationKNN (x, y);
%! assert_equal (class (a), "ClassificationKNN");
%! assert_equal ({a.X, a.Y, a.NumNeighbors}, {x, y, 1})
%! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'})
%! assert_equal ({a.BucketSize}, {50})
%!test
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'b'; 'b'];
%! a = ClassificationKNN (x, y, 'NSMethod', 'exhaustive');
%! assert_equal (class (a), "ClassificationKNN");
%! assert_equal ({a.X, a.Y, a.NumNeighbors}, {x, y, 1})
%! assert_equal ({a.NSMethod, a.Distance}, {'exhaustive', 'euclidean'})
%! assert_equal ({a.BucketSize}, {50})
%!test
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'b'; 'b'];
%! k = 10;
%! a = ClassificationKNN (x, y, 'NumNeighbors' ,k);
%! assert_equal (class (a), "ClassificationKNN");
%! assert_equal ({a.X, a.Y, a.NumNeighbors}, {x, y, 4})
%! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'})
%! assert_equal ({a.BucketSize}, {50})
%!test
%! x = ones (4, 11);
%! y = ['a'; 'a'; 'b'; 'b'];
%! k = 10;
%! a = ClassificationKNN (x, y, 'NumNeighbors' ,k);
%! assert_equal (class (a), "ClassificationKNN");
%! assert_equal ({a.X, a.Y, a.NumNeighbors}, {x, y, 4})
%! assert_equal ({a.NSMethod, a.Distance}, {'exhaustive', 'euclidean'})
%! assert_equal ({a.BucketSize}, {50})
%!test
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'b'; 'b'];
%! k = 10;
%! a = ClassificationKNN (x, y, 'NumNeighbors' ,k, 'NSMethod', 'exhaustive');
%! assert_equal (class (a), "ClassificationKNN");
%! assert_equal ({a.X, a.Y, a.NumNeighbors}, {x, y, 4})
%! assert_equal ({a.NSMethod, a.Distance}, {'exhaustive', 'euclidean'})
%! assert_equal ({a.BucketSize}, {50})
%!test
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'b'; 'b'];
%! k = 10;
%! a = ClassificationKNN (x, y, 'NumNeighbors' ,k, 'Distance', 'hamming');
%! assert_equal (class (a), "ClassificationKNN");
%! assert_equal ({a.X, a.Y, a.NumNeighbors}, {x, y, 4})
%! assert_equal ({a.NSMethod, a.Distance}, {'exhaustive', 'hamming'})
%! assert_equal ({a.BucketSize}, {50})

## Test constructor with Standardize and DistParameter parameters
%!test
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'b'; 'b'];
%! weights = ones (4,1);
%! a = ClassificationKNN (x, y, 'Standardize', 1);
%! assert_equal (class (a), "ClassificationKNN");
%! assert_equal ({a.X, a.Y, a.NumNeighbors}, {x, y, 1})
%! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'})
%! assert_equal ({a.Sigma}, {std(x, [], 1)})
%! assert_equal ({a.Mu}, {[3.75, 4.25, 4.75]})
%!test
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'b'; 'b'];
%! weights = ones (4,1);
%! a = ClassificationKNN (x, y, 'Standardize', false);
%! assert_equal (class (a), "ClassificationKNN");
%! assert_equal ({a.X, a.Y, a.NumNeighbors}, {x, y, 1})
%! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'})
%! assert_equal ({a.Sigma}, {[]})
%! assert_equal ({a.Mu}, {[]})
%!test
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'b'; 'b'];
%! s = ones (1, 3);
%! a = ClassificationKNN (x, y, 'Scale' , s, 'Distance', 'seuclidean');
%! assert_equal (class (a), "ClassificationKNN");
%! assert_equal ({a.DistParameter}, {s})
%! assert_equal ({a.NSMethod, a.Distance}, {'exhaustive', 'seuclidean'})
%! assert_equal ({a.BucketSize}, {50})
%!test
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'b'; 'b'];
%! a = ClassificationKNN (x, y, 'Exponent' , 5, 'Distance', 'minkowski');
%! assert_equal (class (a), "ClassificationKNN");
%! assert_equal (a.DistParameter, 5)
%! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'minkowski'})
%!test
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'b'; 'b'];
%! a = ClassificationKNN (x, y, 'Exponent' , 5, 'Distance', 'minkowski', ...
%!                        'NSMethod', 'exhaustive');
%! assert_equal (class (a), "ClassificationKNN");
%! assert_equal (a.DistParameter, 5)
%! assert_equal ({a.NSMethod, a.Distance}, {'exhaustive', 'minkowski'})

## Test constructor with BucketSize and IncludeTies parameters
%!test
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'b'; 'b'];
%! a = ClassificationKNN (x, y, 'BucketSize' , 20, 'distance', 'mahalanobis');
%! assert_equal (class (a), "ClassificationKNN");
%! assert_equal ({a.NSMethod, a.Distance}, {'exhaustive', 'mahalanobis'})
%! assert_equal ({a.BucketSize}, {20})
%!test
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'b'; 'b'];
%! a = ClassificationKNN (x, y, 'IncludeTies', true);
%! assert_equal (class (a), "ClassificationKNN");
%! assert_equal (a.IncludeTies, true);
%! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'})
%!test
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'b'; 'b'];
%! a = ClassificationKNN (x, y);
%! assert_equal (class (a), "ClassificationKNN");
%! assert_equal (a.IncludeTies, false);
%! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'})

## Test constructor with Prior and Cost parameters
%!test
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'b'; 'b'];
%! a = ClassificationKNN (x, y);
%! assert_equal (class (a), "ClassificationKNN")
%! assert_equal (a.Prior, [0.5, 0.5])
%! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'})
%! assert_equal ({a.BucketSize}, {50})
%!test
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'b'; 'b'];
%! prior = [0.5, 0.5];
%! a = ClassificationKNN (x, y, 'Prior', 'empirical');
%! assert_equal (class (a), "ClassificationKNN")
%! assert_equal (a.Prior, prior)
%! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'})
%! assert_equal ({a.BucketSize}, {50})
%!test
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'a'; 'b'];
%! prior = [0.75, 0.25];
%! a = ClassificationKNN (x, y, 'Prior', 'empirical');
%! assert_equal (class (a), "ClassificationKNN")
%! assert_equal (a.Prior, prior)
%! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'})
%! assert_equal ({a.BucketSize}, {50})
%!test
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'a'; 'b'];
%! prior = [0.5, 0.5];
%! a = ClassificationKNN (x, y, 'Prior', 'uniform');
%! assert_equal (class (a), "ClassificationKNN")
%! assert_equal (a.Prior, prior)
%! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'})
%! assert_equal ({a.BucketSize}, {50})
%!test
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'b'; 'b'];
%! cost = [0, 1; 1, 0];
%! a = ClassificationKNN (x, y, 'Cost', cost);
%! assert_equal (class (a), "ClassificationKNN")
%! assert_equal (a.Cost, [0, 1; 1, 0])
%! assert_equal ({a.NSMethod, a.Distance}, {'kdtree', 'euclidean'})
%! assert_equal ({a.BucketSize}, {50})
%!test
%! x = [1, 2, 3; 4, 5, 6; 7, 8, 9; 3, 2, 1];
%! y = ['a'; 'a'; 'b'; 'b'];
%! cost = [0, 1; 1, 0];
%! a = ClassificationKNN (x, y, 'Cost', cost, 'Distance', 'hamming' );
%! assert_equal (class (a), "ClassificationKNN")
%! assert_equal (a.Cost, [0, 1; 1, 0])
%! assert_equal ({a.NSMethod, a.Distance}, {'exhaustive', 'hamming'})
%! assert_equal ({a.BucketSize}, {50})
%!test
%! x = [1, 2; 3, 4; 5,6; 5, 8];
%! y = {'9'; '9'; '6'; '7'};
%! a = ClassificationKNN (x, y);
%! assert_equal (a.Prior, [0.25, 0.25, 0.5])

## Test constructor with ClassNames parameter
%!test
%! load fisheriris
%! x = meas;
%! y = species;
%! ClassNames = {'setosa', 'versicolor', 'virginica'};
%! a = ClassificationKNN (x, y, 'ClassNames', ClassNames);
%! assert_equal (a.ClassNames, ClassNames')

## Test input validation for constructor
## A response naming its classes in the rows of a character matrix is one
## of the documented types and MATLAB accepts it on every classifier.  The
## whole surface below was broken and untested, which is why it stayed so.
%!test
%! load fisheriris
%! bch = ! strcmp (species, "setosa");
%! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell);
%! rand ("state", 1); randn ("state", 1); Mc = fitcknn (Xch, Ych);
%! rand ("state", 1); randn ("state", 1); Ms = fitcknn (Xch, Ycell);
%! assert_equal (size (Mc.ClassNames), [2, 10]);
%! assert_equal (cellstr (Mc.ClassNames), Ms.ClassNames);

## predict returns whole names, not their first letters.
%!test
%! load fisheriris
%! bch = ! strcmp (species, "setosa");
%! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell);
%! rand ("state", 1); randn ("state", 1); Mc = fitcknn (Xch, Ych);
%! rand ("state", 1); randn ("state", 1); Ms = fitcknn (Xch, Ycell);
%! pch = predict (Mc, Xch);
%! assert_equal (columns (pch), 10);
%! assert_equal (cellstr (pch), predict (Ms, Xch));

## loss, margin and edge read a character response as the same response.
%!test
%! load fisheriris
%! bch = ! strcmp (species, "setosa");
%! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell);
%! rand ("state", 1); randn ("state", 1); Mc = fitcknn (Xch, Ych);
%! rand ("state", 1); randn ("state", 1); Ms = fitcknn (Xch, Ycell);
%! assert_equal (loss (Mc, Xch, Ych), loss (Ms, Xch, Ycell), 1e-12);
%! assert_equal (margin (Mc, Xch, Ych), margin (Ms, Xch, Ycell), 1e-12);
%! assert_equal (edge (Mc, Xch, Ych), edge (Ms, Xch, Ycell), 1e-12);

## A character matrix pads its rows out to the longest name, and the padding
## is part of the name: R2024a reports ClassNames of ['ab  '; 'abcd'].
%!test
%! Xpad = [1 2; 3 4; 1.1 2.1; 3.1 4.1; 1.2 2.2; 3.2 4.2];
%! Ypad = char ({"ab", "abcd", "ab", "abcd", "ab", "abcd"});
%! rand ("state", 1); randn ("state", 1);
%! Mp = fitcknn (Xpad, Ypad);
%! assert_equal (size (Mp.ClassNames), [2, 4]);
%! assert_equal (Mp.ClassNames(1,:), "ab  ");

## A row dropped for a missing predictor is the only case that exercises
## indexing the response by row rather than by element.
%!test
%! load fisheriris
%! bch = ! strcmp (species, "setosa");
%! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell);
%! Xmiss = Xch; Xmiss(3,2) = NaN;
%! rand ("state", 1); randn ("state", 1); Md = fitcknn (Xmiss, Ych);
%! rand ("state", 1); randn ("state", 1); Ms = fitcknn (Xmiss, Ycell);
%! assert_equal (size (Md.ClassNames), [2, 10]);
%! assert_equal (cellstr (Md.ClassNames), Ms.ClassNames);

## ClassNames may itself be given as a character matrix, which selects the
## classes by whole name: ismember between two character matrices compares
## them character by character and would select by letter.
%!test
%! load fisheriris
%! rand ("state", 1); randn ("state", 1);
%! Mf = fitcknn (meas, char (species), ...
%!               "ClassNames", char ({"versicolor", "virginica"}));
%! assert_equal (rows (Mf.ClassNames), 2);
%! assert_equal (cellstr (Mf.ClassNames), {"versicolor"; "virginica"});

## A model fitted from a character response comes back off disk unchanged.
%!test
%! load fisheriris
%! bch = ! strcmp (species, "setosa");
%! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell);
%! rand ("state", 1); randn ("state", 1); Mc = fitcknn (Xch, Ych);
%! fname = tempname ();
%! savemodel (Mc, fname);
%! M2 = loadmodel (fname);
%! delete (fname);
%! assert_equal (M2.ClassNames, Mc.ClassNames);
%! assert_equal (predict (M2, Xch), predict (Mc, Xch));

## crossval carries a character response through cvpartition and back.
%!test
%! load fisheriris
%! bch = ! strcmp (species, "setosa");
%! Xch = meas(bch,:); Ycell = species(bch); Ych = char (Ycell);
%! rand ("state", 1); randn ("state", 1); Mc = fitcknn (Xch, Ych);
%! rand ("state", 1); randn ("state", 1); Ms = fitcknn (Xch, Ycell);
%! rand ("state", 2); cvc = crossval (Mc, "KFold", 3);
%! rand ("state", 2); cvs = crossval (Ms, "KFold", 3);
%! assert_equal (cellstr (kfoldPredict (cvc)), kfoldPredict (cvs));

%!test
%! ## CacheSize defaults to 1000 and is reported, where MATLAB hides it.
%! X = [1, 2; 3, 4; 5, 6; 7, 8; 2, 2];
%! Mdl = fitcknn (X, [1; 1; 2; 2; 1]);
%! assert_equal (Mdl.CacheSize, 1000);
%! assert_equal (any (strcmp (properties (Mdl), 'CacheSize')), true);

%!test
%! ## It is settable after fitting and through fitcknn alike.
%! X = [1, 2; 3, 4; 5, 6; 7, 8; 2, 2];
%! Mdl = fitcknn (X, [1; 1; 2; 2; 1]);
%! Mdl.CacheSize = 5000;
%! assert_equal (Mdl.CacheSize, 5000);
%! Mdl2 = fitcknn (X, [1; 1; 2; 2; 1], "CacheSize", 250);
%! assert_equal (Mdl2.CacheSize, 250);

%!test
%! ## It changes nothing: the model keeps no cache to size.
%! X = [1, 2; 3, 4; 5, 6; 7, 8; 2, 2];
%! y = [1; 1; 2; 2; 1];
%! Mdl = fitcknn (X, y, "CacheSize", 1);
%! Mdl2 = fitcknn (X, y, "CacheSize", 1e6);
%! [l1, s1] = predict (Mdl, X);
%! [l2, s2] = predict (Mdl2, X);
%! assert_equal (l1, l2);
%! assert_equal (s1, s2);

%!error<ClassificationKNN: 'CacheSize' must be a positive finite scalar.> ...
%! fitcknn (ones (5, 2), [1; 1; 1; 2; 2], "CacheSize", 0)
%!error<ClassificationKNN: 'CacheSize' must be a positive finite scalar.> ...
%! fitcknn (ones (5, 2), [1; 1; 1; 2; 2], "CacheSize", [1, 2])
%!error<ClassificationKNN: 'CacheSize' must be a positive finite scalar.> ...
%! fitcknn (ones (5, 2), [1; 1; 1; 2; 2], "CacheSize", Inf)
%!test
%! Mdl = fitcknn (ones (5, 2), [1; 1; 1; 2; 2]);
%! fail ("Mdl.CacheSize = -1", ...
%!       "ClassificationKNN: 'CacheSize' must be a positive finite scalar.");

%!error<ClassificationKNN: too few input arguments.> ClassificationKNN ()
%!error<ClassificationKNN: too few input arguments.> ...
%! ClassificationKNN (ones (4, 1))
%!error<ClassificationKNN: number of rows in X and Y must be equal.> ...
%! ClassificationKNN (ones (4,2), ones (1,4))
%!error<ClassificationKNN: 'Standardize' must be either true or false.> ...
%! ClassificationKNN (ones (5,3), ones (5,1), 'standardize', 'a')
%!error<ClassificationKNN: 'Standardize' cannot simultaneously be specified with> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'scale', [1 1], 'standardize', true)
%!error<ClassificationKNN: 'PredictorNames' must be supplied as a cellstring array.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'PredictorNames', ['A'])
%!error<ClassificationKNN: 'PredictorNames' must be supplied as a cellstring array.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'PredictorNames', 'A')
%!error<ClassificationKNN: 'PredictorNames' must have the same number of columns as X.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'PredictorNames', {'A', 'B', 'C'})
%!error<ClassificationKNN: 'ResponseName' must be a character vector.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'ResponseName', {'Y'})
%!error<ClassificationKNN: 'ResponseName' must be a character vector.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'ResponseName', 1)
%!error<ClassificationKNN: 'ClassNames' must be a cell array of character vectors, a logical vector, a numeric vector, or a character array.> ...
%! ClassificationKNN (ones (10,2), ones (10,1), 'ClassNames', @(x)x)
%!error<ClassificationKNN: 'ClassNames' must be a cell array of character vectors, a logical vector, a numeric vector, or a character array.> ...
%! ClassificationKNN (ones (10,2), ones (10,1), 'ClassNames', {1})
%!error<ClassificationKNN: not all 'ClassNames' are present in Y.> ...
%! ClassificationKNN (ones (10,2), ones (10,1), 'ClassNames', [1, 2])
%!error<ClassificationKNN: not all 'ClassNames' are present in Y.> ...
%! ClassificationKNN (ones (5,2), ['a';'b';'a';'a';'b'], 'ClassNames', ['a';'c'])
%!error<ClassificationKNN: not all 'ClassNames' are present in Y.> ...
%! ClassificationKNN (ones (5,2), {'a';'b';'a';'a';'b'}, 'ClassNames', {'a','c'})
%!error<ClassificationKNN: not all 'ClassNames' are present in Y.> ...
%! ClassificationKNN (ones (10,2), logical (ones (10,1)), 'ClassNames', [true, false])
%!error<ClassificationKNN: 'BreakTies' must be a character vector.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'BreakTies', 1)
%!error<ClassificationKNN: 'BreakTies' must be a character vector.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'BreakTies', {'1'})
%!error<ClassificationKNN: invalid value for 'BreakTies'.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'BreakTies', 'some')
%!error<ClassificationKNN: 'Prior' must be either a numeric vector or a character vector.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Prior', {'1', '2'})
%!error<ClassificationKNN: 'Cost' must be a numeric square matrix.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Cost', [1, 2])
%!error<ClassificationKNN: the number of rows and columns in 'Cost' must correspond to selected classes in Y.> ...
%! Mdl = fitcknn ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]);
%! Mdl.Cost = 1:4;
%!error<ClassificationKNN: 'Cost' must be a numeric square matrix.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Cost', 'string')
%!error<ClassificationKNN: 'Cost' must be a numeric square matrix.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Cost', {eye(2)})
%!error<ClassificationKNN: 'NumNeighbors' must be a positive integer.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'NumNeighbors', 0)
%!error<ClassificationKNN: 'NumNeighbors' must be a positive integer.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'NumNeighbors', 15.2)
%!error<ClassificationKNN: 'NumNeighbors' must be a positive integer.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'NumNeighbors', 'asd')
%!error<ClassificationKNN: unsupported distance metric.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Distance', 'somemetric')
%!error<ClassificationKNN: invalid function handle for distance metric.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Distance', ...
%!                    @(v,m)sqrt (repmat (v,rows (m),1)-m,2))
%!error<ClassificationKNN: custom distance function produces wrong output size.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Distance', ...
%!                    @(v,m)sqrt (sum (sumsq (repmat (v,rows (m),1)-m,2))))
%!error<ClassificationKNN: invalid distance metric.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Distance', [1 2 3])
%!error<ClassificationKNN: invalid distance metric.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Distance', {'mahalanobis'})
%!error<ClassificationKNN: invalid distance metric.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Distance', logical (5))
%!error<ClassificationKNN: function handle for 'DistanceWeight' must return the same size as its input.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'DistanceWeight', @(x)sum (x))
%!error<ClassificationKNN: 'DistanceWeight' must be 'equal', 'inverse', 'squaredinverse', or a function handle.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'DistanceWeight', 'text')
%!error<ClassificationKNN: 'DistanceWeight' must be a character vector or a function handle.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'DistanceWeight', [1 2 3])
%!error<ClassificationKNN: 'Scale' must be a numeric vector.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Scale', 'scale')
%!error<ClassificationKNN: 'Scale' must be a numeric vector.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Scale', {[1 2 3]})
%!error<ClassificationKNN: 'Scale' cannot simultaneously be specified with> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'standardize', true, 'scale', [1 1])
%!error<ClassificationKNN: 'Cov' must be a symmetric positive definite matrix.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Cov', ones (2), 'Distance', 'mahalanobis')
%!error<ClassificationKNN: 'Cov' cannot simultaneously be specified with> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'scale', [1 1], 'Cov', ones (2))
%!error<ClassificationKNN: 'Exponent' must be a positive integer.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Exponent', 12.5)
%!error<ClassificationKNN: 'Exponent' must be a positive integer.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Exponent', -3)
%!error<ClassificationKNN: 'Exponent' must be a positive integer.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Exponent', 'three')
%!error<ClassificationKNN: 'Exponent' must be a positive integer.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Exponent', {3})
%!error<ClassificationKNN: 'NSMethod' must be a character vector.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'NSMethod', {'kdtree'})
%!error<ClassificationKNN: 'NSMethod' must be a character vector.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'NSMethod', 3)
%!error<ClassificationKNN: 'NSMethod' must be either 'kdtree' or 'exhaustive'.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'NSMethod', 'some')
%!error<ClassificationKNN: 'IncludeTies' must be either true or false.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'IncludeTies', 'some')
%!error<ClassificationKNN: 'BucketSize' must be a positive integer.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'BucketSize', 42.5)
%!error<ClassificationKNN: 'BucketSize' must be a positive integer.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'BucketSize', -50)
%!error<ClassificationKNN: 'BucketSize' must be a positive integer.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'BucketSize', 'some')
%!error<ClassificationKNN: 'BucketSize' must be a positive integer.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'BucketSize', {50})
%!error<ClassificationKNN: invalid parameter name in optional pair arguments.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'some', 'some')
%!error<ClassificationKNN: invalid values in X.> ...
%! ClassificationKNN ([1;2;3;'a';4], ones (5,1))
%!error<ClassificationKNN: invalid values in X.> ...
%! ClassificationKNN ([1;2;3;Inf;4], ones (5,1))
%!error<ClassificationKNN: the elements in 'Prior' do not correspond to the selected classes in Y.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Prior', [1 2])
%!error<ClassificationKNN: the number of rows and columns in 'Cost' must correspond to selected classes in Y.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Cost', [1 2; 1 3])
%!error<ClassificationKNN: 'Scale' is only valid when distance metric is seuclidean.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Scale', [1 1])
%!error<ClassificationKNN: 'Scale' vector must have equal length to the number of columns in X.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Scale', [1 1 1], 'Distance', 'seuclidean')
%!error<ClassificationKNN: 'Scale' vector must contain nonnegative scalar values.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Scale', [1 -1], 'Distance', 'seuclidean')
%!error<ClassificationKNN: 'Cov' is only valid when distance metric is 'mahalanobis'.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Cov', eye (2))
%!error<ClassificationKNN: 'Cov' matrix must have equal columns as X.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Cov', eye (3), 'Distance', 'mahalanobis')
%!error<ClassificationKNN: 'Exponent' is only valid when distance metric is 'minkowski'.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Exponent', 3)
%!error<ClassificationKNN: 'kdtree' method is only valid for 'euclidean', 'cityblock', 'manhattan', 'minkowski', and 'chebychev' distance metrics.> ...
%! ClassificationKNN (ones (5,2), ones (5,1), 'Distance', 'hamming', 'NSMethod', 'kdtree')

## Test output for predict method
%!shared x, y
%! load fisheriris
%! x = meas;
%! y = species;
%!test
%! xc = [min(x); mean(x); max(x)];
%! obj = fitcknn (x, y, 'NumNeighbors', 5);
%! [l, s, c] = predict (obj, xc);
%! assert_equal (l, {'setosa'; 'versicolor'; 'virginica'})
%! assert_equal (s, [1, 0, 0; 0, 1, 0; 0, 0, 1])
%! assert_equal (c, [0, 1, 1; 1, 0, 1; 1, 1, 0])
%!test
%! xc = [min(x); mean(x); max(x)];
%! obj = fitcknn (x, y, 'NumNeighbors', 5, 'Standardize', 1);
%! [l, s, c] = predict (obj, xc);
%! assert_equal (l, {'versicolor'; 'versicolor'; 'virginica'})
%! assert_equal (s, [0.4, 0.6, 0; 0, 1, 0; 0, 0, 1])
%! assert_equal (c, [0.6, 0.4, 1; 1, 0, 1; 1, 1, 0])
%!test
%! xc = [min(x); mean(x); max(x)];
%! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'mahalanobis');
%! [l, s, c] = predict (obj, xc);
%! assert_equal (s, [0.3, 0.7, 0; 0, 0.9, 0.1; 0.2, 0.2, 0.6], 1e-4)
%! assert_equal (c, [0.7, 0.3, 1; 1, 0.1, 0.9; 0.8, 0.8, 0.4], 1e-4)
%!test
%! xc = [min(x); mean(x); max(x)];
%! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'cosine');
%! [l, s, c] = predict (obj, xc);
%! assert_equal (l, {'setosa'; 'versicolor'; 'virginica'})
%! assert_equal (s, [1, 0, 0; 0, 1, 0; 0, 0.3, 0.7], 1e-4)
%! assert_equal (c, [0, 1, 1; 1, 0, 1; 1, 0.7, 0.3], 1e-4)
%!test
%! xc = [5.2, 4.1, 1.5, 0.1; 5.1, 3.8, 1.9, 0.4; ...
%!         5.1, 3.8, 1.5, 0.3; 4.9, 3.6, 1.4, 0.1];
%! obj = fitcknn (x, y, 'NumNeighbors', 5);
%! [l, s, c] = predict (obj, xc);
%! assert_equal (l, {'setosa'; 'setosa'; 'setosa'; 'setosa'})
%! assert_equal (s, [1, 0, 0; 1, 0, 0; 1, 0, 0; 1, 0, 0])
%! assert_equal (c, [0, 1, 1; 0, 1, 1; 0, 1, 1; 0, 1, 1])
%!test
%! xc = [5, 3, 5, 1.45];
%! obj = fitcknn (x, y, 'NumNeighbors', 5);
%! [l, s, c] = predict (obj, xc);
%! assert_equal (l, {'versicolor'})
%! assert_equal (s, [0, 0.6, 0.4], 1e-4)
%! assert_equal (c, [1, 0.4, 0.6], 1e-4)
%!test
%! xc = [5, 3, 5, 1.45];
%! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'minkowski', 'Exponent', 5);
%! [l, s, c] = predict (obj, xc);
%! assert_equal (l, {'versicolor'})
%! assert_equal (s, [0, 0.5, 0.5], 1e-4)
%! assert_equal (c, [1, 0.5, 0.5], 1e-4)
%!test
%! xc = [5, 3, 5, 1.45];
%! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'jaccard');
%! [l, s, c] = predict (obj, xc);
%! assert_equal (l, {'setosa'})
%! assert_equal (s, [0.9, 0.1, 0], 1e-4)
%! assert_equal (c, [0.1, 0.9, 1], 1e-4)
%!test
%! xc = [5, 3, 5, 1.45];
%! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'mahalanobis');
%! [l, s, c] = predict (obj, xc);
%! assert_equal (l, {'versicolor'})
%! assert_equal (s, [0.1000, 0.5000, 0.4000], 1e-4)
%! assert_equal (c, [0.9000, 0.5000, 0.6000], 1e-4)
%!test
%! xc = [5, 3, 5, 1.45];
%! obj = fitcknn (x, y, 'NumNeighbors', 5, 'distance', 'jaccard');
%! [l, s, c] = predict (obj, xc);
%! assert_equal (l, {'setosa'})
%! assert_equal (s, [0.8, 0.2, 0], 1e-4)
%! assert_equal (c, [0.2, 0.8, 1], 1e-4)
%!test
%! xc = [5, 3, 5, 1.45];
%! obj = fitcknn (x, y, 'NumNeighbors', 5, 'distance', 'seuclidean');
%! [l, s, c] = predict (obj, xc);
%! assert_equal (l, {'versicolor'})
%! assert_equal (s, [0, 1, 0], 1e-4)
%! assert_equal (c, [1, 0, 1], 1e-4)
%!test
%! xc = [5, 3, 5, 1.45];
%! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'chebychev');
%! [l, s, c] = predict (obj, xc);
%! assert_equal (l, {'versicolor'})
%! assert_equal (s, [0, 0.7, 0.3], 1e-4)
%! assert_equal (c, [1, 0.3, 0.7], 1e-4)
%!test
%! xc = [5, 3, 5, 1.45];
%! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'cityblock');
%! [l, s, c] = predict (obj, xc);
%! assert_equal (l, {'versicolor'})
%! assert_equal (s, [0, 0.6, 0.4], 1e-4)
%! assert_equal (c, [1, 0.4, 0.6], 1e-4)
%!test
%! xc = [5, 3, 5, 1.45];
%! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'cosine');
%! [l, s, c] = predict (obj, xc);
%! assert_equal (l, {'virginica'})
%! assert_equal (s, [0, 0.1, 0.9], 1e-4)
%! assert_equal (c, [1, 0.9, 0.1], 1e-4)
%!test
%! xc = [5, 3, 5, 1.45];
%! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'correlation');
%! [l, s, c] = predict (obj, xc);
%! assert_equal (l, {'virginica'})
%! assert_equal (s, [0, 0.1, 0.9], 1e-4)
%! assert_equal (c, [1, 0.9, 0.1], 1e-4)
%!test
%! xc = [5, 3, 5, 1.45];
%! obj = fitcknn (x, y, 'NumNeighbors', 30, 'distance', 'spearman');
%! [l, s, c] = predict (obj, xc);
%! assert_equal (l, {'versicolor'})
%! assert_equal (s, [0, 1, 0], 1e-4)
%! assert_equal (c, [1, 0, 1], 1e-4)
%!test
%! xc = [5, 3, 5, 1.45];
%! obj = fitcknn (x, y, 'NumNeighbors', 30, 'distance', 'hamming');
%! [l, s, c] = predict (obj, xc);
%! assert_equal (l, {'setosa'})
%! assert_equal (s, [0.4333, 0.3333, 0.2333], 1e-4)
%! assert_equal (c, [0.5667, 0.6667, 0.7667], 1e-4)
%!test
%! xc = [5, 3, 5, 1.45];
%! obj = fitcknn (x, y, 'NumNeighbors', 5, 'distance', 'hamming');
%! [l, s, c] = predict (obj, xc);
%! assert_equal (l, {'setosa'})
%! assert_equal (s, [0.8, 0.2, 0], 1e-4)
%! assert_equal (c, [0.2, 0.8, 1], 1e-4)
%!test
%! xc = [min(x); mean(x); max(x)];
%! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'correlation');
%! [l, s, c] = predict (obj, xc);
%! assert_equal (l, {'setosa'; 'versicolor'; 'virginica'})
%! assert_equal (s, [1, 0, 0; 0, 1, 0; 0, 0.4, 0.6], 1e-4)
%! assert_equal (c, [0, 1, 1; 1, 0, 1; 1, 0.6, 0.4], 1e-4)
%!test
%! xc = [min(x); mean(x); max(x)];
%! obj = fitcknn (x, y, 'NumNeighbors', 10, 'distance', 'hamming');
%! [l, s, c] = predict (obj, xc);
%! assert_equal (l, {'setosa';'setosa';'setosa'})
%! assert_equal (s, [0.9, 0.1, 0; 1, 0, 0; 0.5, 0, 0.5], 1e-4)
%! assert_equal (c, [0.1, 0.9, 1; 0, 1, 1; 0.5, 1, 0.5], 1e-4)

## Test input validation for predict method
%!error<ClassificationKNN.predict: too few input arguments.> ...
%! predict (ClassificationKNN (ones (4,2), ones (4,1)))
%!error<ClassificationKNN.predict: XC is empty.> ...
%! predict (ClassificationKNN (ones (4,2), ones (4,1)), [])
%!error<ClassificationKNN.predict: XC must have the same number of predictors as the trained model.> ...
%! predict (ClassificationKNN (ones (4,2), ones (4,1)), 1)

## Test output for loss method
%!test
%! load fisheriris
%! model = fitcknn (meas, species, 'NumNeighbors', 5);
%! X = mean (meas);
%! Y = {'versicolor'};
%! L = loss (model, X, Y);
%! assert_equal (L, 0)
%!test
%! load fisheriris
%! model = fitcknn (meas, species, 'NumNeighbors', 5);
%! L = loss (model, meas, species, 'LossFun', 'binodeviance');
%! assert_equal (L, 0.1413, 1e-4)
%!test
%! load fisheriris
%! model = fitcknn (meas, species);
%! L = loss (model, meas, species, 'LossFun', 'binodeviance');
%! assert_equal (L, 0.1269, 1e-4)
%!test
%! X = [1, 2; 3, 4; 5, 6];
%! Y = {'A'; 'B'; 'A'};
%! model = fitcknn (X, Y);
%! X_test = [1, 6; 3, 3];
%! Y_test = {'A'; 'B'};
%! L = loss (model, X_test, Y_test);
%! assert_equal (abs (L - 0.6667) > 1e-5, true)
%!test
%! X = [1, 2; 3, 4; 5, 6];
%! Y = {'A'; 'B'; 'A'};
%! model = fitcknn (X, Y);
%! X_with_nan = [1, 2; NaN, 4];
%! Y_test = {'A'; 'B'};
%! L = loss (model, X_with_nan, Y_test);
%! assert_equal (abs (L - 0.3333) < 1e-4, true)
%!test
%! X = [1, 2; 3, 4; 5, 6];
%! Y = {'A'; 'B'; 'A'};
%! model = fitcknn (X, Y);
%! X_with_nan = [1, 2; NaN, 4];
%! Y_test = {'A'; 'B'};
%! L = loss (model, X_with_nan, Y_test, 'LossFun', 'logit');
%! assert_equal (isnan (L), true)
%!test
%! X = [1, 2; 3, 4; 5, 6];
%! Y = {'A'; 'B'; 'A'};
%! model = fitcknn (X, Y);
%! customLossFun = @(C, S, W, Cost) sum (W .* sum (abs (C - S), 2));
%! L = loss (model, X, Y, 'LossFun', customLossFun);
%! assert_equal (L, 0)
%!test
%! X = [1, 2; 3, 4; 5, 6];
%! Y = [1; 2; 1];
%! model = fitcknn (X, Y);
%! L = loss (model, X, Y, 'LossFun', 'classiferror');
%! assert_equal (L, 0)
%!test
%! X = [1, 2; 3, 4; 5, 6];
%! Y = [true; false; true];
%! model = fitcknn (X, Y);
%! L = loss (model, X, Y, 'LossFun', 'binodeviance');
%! assert_equal (abs (L - 0.1269) < 1e-4, true)
%!test
%! X = [1, 2; 3, 4; 5, 6];
%! Y = ['1'; '2'; '1'];
%! model = fitcknn (X, Y);
%! L = loss (model, X, Y, 'LossFun', 'classiferror');
%! assert_equal (L, 0)
%!test
%! X = [1, 2; 3, 4; 5, 6];
%! Y = ['1'; '2'; '3'];
%! model = fitcknn (X, Y);
%! X_test = [3, 3];
%! Y_test = ['1'];
%! L = loss (model, X_test, Y_test, 'LossFun', 'quadratic');
%! assert_equal (L, 1)
%!test
%! X = [1, 2; 3, 4; 5, 6];
%! Y = ['1'; '2'; '3'];
%! model = fitcknn (X, Y);
%! X_test = [3, 3; 5, 7];
%! Y_test = ['1'; '2'];
%! L = loss (model, X_test, Y_test, 'LossFun', 'classifcost');
%! assert_equal (L, 1)
%!test
%! X = [1, 2; 3, 4; 5, 6];
%! Y = ['1'; '2'; '3'];
%! model = fitcknn (X, Y);
%! X_test = [3, 3; 5, 7];
%! Y_test = ['1'; '2'];
%! L = loss (model, X_test, Y_test, 'LossFun', 'hinge');
%! assert_equal (L, 1)
%!test
%! X = [1, 2; 3, 4; 5, 6];
%! Y = ['1'; '2'; '3'];
%! model = fitcknn (X, Y);
%! X_test = [3, 3; 5, 7];
%! Y_test = ['1'; '2'];
%! W = [1; 2];
%! L = loss (model, X_test, Y_test, 'LossFun', 'logit', 'Weights', W);
%! assert_equal (abs (L - 0.6931) < 1e-4, true)

## Test input validation for loss method
%!error<ClassificationKNN.loss: too few input arguments.> ...
%! loss (ClassificationKNN (ones (4,2), ones (4,1)))
%!error<ClassificationKNN.loss: too few input arguments.> ...
%! loss (ClassificationKNN (ones (4,2), ones (4,1)), ones (4,2))
%!error<ClassificationKNN.loss: X is empty.> ...
%! loss (ClassificationKNN (ones (40,2), randi ([1, 2], 40, 1)), [], zeros (2))
%!error<ClassificationKNN.loss: X must have the same number of predictors as the trained model.> ...
%! loss (ClassificationKNN (ones (40,2), randi ([1, 2], 40, 1)), 1, zeros (2))
%!error<ClassificationKNN.loss: name-value arguments must be in pairs.> ...
%! loss (ClassificationKNN (ones (4,2), ones (4,1)), ones (4,2), ...
%!        ones (4,1), 'LossFun')
%!error<ClassificationKNN.loss: Y must have the same number of rows as X.> ...
%! loss (ClassificationKNN (ones (4,2), ones (4,1)), ones (4,2), ones (3,1))
%!error<ClassificationKNN.loss: invalid loss function.> ...
%! loss (ClassificationKNN (ones (4,2), ones (4,1)), ones (4,2), ...
%!        ones (4,1), 'LossFun', 'a')
%!error<ClassificationKNN.loss: invalid Weights.> ...
%! loss (ClassificationKNN (ones (4,2), ones (4,1)), ones (4,2), ...
%!        ones (4,1), 'Weights', 'w')

## Test output for margin method
%!test
%! load fisheriris
%! mdl = fitcknn (meas, species, 'NumNeighbors', 5);
%! X = mean (meas);
%! Y = {'versicolor'};
%! m = margin (mdl, X, Y);
%! assert_equal (m, 1)
%!test
%! X = [1, 2; 3, 4; 5, 6];
%! Y = [1; 2; 3];
%! mdl = fitcknn (X, Y);
%! m = margin (mdl, X, Y);
%! assert_equal (m, [1; 1; 1])
%!test
%! X = [7, 8; 9, 10];
%! Y = ['1'; '2'];
%! mdl = fitcknn (X, Y);
%! m = margin (mdl, X, Y);
%! assert_equal (m, [1; 1])
%!test
%! X = [11, 12];
%! Y = {'1'};
%! mdl = fitcknn (X, Y);
%! m = margin (mdl, X, Y);
%! assert_equal (isnan (m), true)
%!test
%! X = [1, 2; 3, 4; 5, 6];
%! Y = [1; 2; 3];
%! mdl = fitcknn (X, Y);
%! X1 = [15, 16];
%! Y1 = [1];
%! m = margin (mdl, X1, Y1);
%! assert_equal (m, -1)

## Test input validation for margin method
%!error<ClassificationKNN.margin: too few input arguments.> ...
%! margin (ClassificationKNN (ones (4,2), ones (4,1)))
%!error<ClassificationKNN.margin: too few input arguments.> ...
%! margin (ClassificationKNN (ones (4,2), ones (4,1)), ones (4,2))
%!error<ClassificationKNN.margin: X is empty.> ...
%! margin (ClassificationKNN (ones (40,2), randi ([1, 2], 40, 1)), [], zeros (2))
%!error<ClassificationKNN.margin: X must have the same number of predictors as the trained model.> ...
%! margin (ClassificationKNN (ones (40,2), randi ([1, 2], 40, 1)), 1, zeros (2))
%!error<ClassificationKNN.margin: Y must have the same number of rows as X.> ...
%! margin (ClassificationKNN (ones (4,2), ones (4,1)), ones (4,2), ones (3,1))

## Test output for partialDependence
%!shared X, Y, mdl
%! X = [1, 2; 4, 5; 7, 8; 3, 2];
%! Y = [2; 1; 3; 2];
%! mdl = fitcknn (X, Y);
%!test
%! Vars = 1;
%! Labels = 2;
%! [pd, x, y] = partialDependence (mdl, Vars, Labels);
%! pdm = [0.7500, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000];
%! assert_equal (pd, pdm)
%!test
%! Vars = 1;
%! Labels = 2;
%! [pd, x, y] = partialDependence (mdl, Vars, Labels, ...
%! 'NumObservationsToSample', 5);
%! pdm = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...
%! 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...
%! 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...
%! 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...
%! 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
%! assert_equal (all ((abs (pdm - pd) < 1)(:)), true)
%!test
%! Vars = 1;
%! Labels = 2;
%! [pd, x, y] = partialDependence (mdl, Vars, Labels, 'UseParallel', true);
%! pdm = [0.7500, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000];
%! assert_equal (pd, pdm)
%!test
%! Vars = [1, 2];
%! Labels = 1;
%! queryPoints = {linspace(0, 1, 3)', linspace(0, 1, 3)'};
%! [pd, x, y] = partialDependence (mdl, Vars, Labels, 'QueryPoints', ...
%!                            queryPoints, 'UseParallel', true);
%! pdm = [0, 0, 0; 0, 0, 0; 0, 0, 0];
%! assert_equal (pd, pdm)
%!test
%! Vars = 1;
%! Labels = [1; 2];
%! [pd, x, y] = partialDependence (mdl, Vars, Labels);
%! pdm = [0.2500, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.2500, 0.2500, 0.2500, ...
%! 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, ...
%! 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, ...
%! 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, ...
%! 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, ...
%! 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, 0.2500, ...
%! 0.2500, 0.2500; 0.7500, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, 0.5000, ...
%! 0.5000, 0.5000, 0.5000];
%! assert_equal (pd, pdm)
%!test
%! Vars = [1, 2];
%! Labels = [1; 2];
%! queryPoints = {linspace(0, 1, 3)', linspace(0, 1, 3)'};
%! [pd, x, y] = partialDependence (mdl, Vars, Labels, 'QueryPoints', queryPoints);
%! pdm(:,:,1) = [0, 0, 0; 1, 1, 1];
%! pdm(:,:,2) = [0, 0, 0; 1, 1, 1];
%! pdm(:,:,3) = [0, 0, 0; 1, 1, 1];
%! assert_equal (pd, pdm)
%!test
%! X1 = [1; 2; 4; 5; 7; 8; 3; 2];
%! X2 = ['2'; '3'; '1'; '3'; '1'; '3'; '2'; '2'];
%! X = [X1, double(X2)];
%! Y = [1; 2; 3; 3; 2; 1; 2; 1];
%! mdl = fitcknn (X, Y, 'ClassNames', {'1', '2', '3'});
%! Vars = 1;
%! Labels = 1;
%! [pd, x, y] = partialDependence (mdl, Vars, Labels);
%! pdm = [1.0000, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, ...
%! 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, ...
%! 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...
%! 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...
%! 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.3750, ...
%! 0.3750, 0.3750, 0.3750, 0.3750, 0.3750, 0.3750, 0.3750, 0.3750, 0.3750, ...
%! 0.3750, 0.3750, 0.3750, 0.3750, 0.7500, 0.7500, 0.7500, 0.7500, 0.7500, ...
%! 0.7500, 0.7500, 0.7500];
%! assert_equal (pd, pdm)
%!test
%! X1 = [1; 2; 4; 5; 7; 8; 3; 2];
%! X2 = ['2'; '3'; '1'; '3'; '1'; '3'; '2'; '2'];
%! X = [X1, double(X2)];
%! Y = [1; 2; 3; 3; 2; 1; 2; 1];
%! predictorNames = {'Feature1', 'Feature2'};
%! mdl = fitcknn (X, Y, 'PredictorNames', predictorNames);
%! Vars = 'Feature1';
%! Labels = 1;
%! [pd, x, y] = partialDependence (mdl, Vars, Labels);
%! pdm = [1.0000, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, ...
%! 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, ...
%! 0.6250, 0.6250, 0.6250, 0.6250, 0.6250, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...
%! 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...
%! 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.3750, ...
%! 0.3750, 0.3750, 0.3750, 0.3750, 0.3750, 0.3750, 0.3750, 0.3750, 0.3750, ...
%! 0.3750, 0.3750, 0.3750, 0.3750, 0.7500, 0.7500, 0.7500, 0.7500, 0.7500, ...
%! 0.7500, 0.7500, 0.7500];
%! assert_equal (pd, pdm)
%!test
%! X1 = [1; 2; 4; 5; 7; 8; 3; 2];
%! X2 = ['2'; '3'; '1'; '3'; '1'; '3'; '2'; '2'];
%! X = [X1, double(X2)];
%! Y = [1; 2; 3; 3; 2; 1; 2; 1];
%! predictorNames = {'Feature1', 'Feature2'};
%! mdl = fitcknn (X, Y, 'PredictorNames', predictorNames);
%! new_X1 = [10; 5; 6; 8; 9; 20; 35; 6];
%! new_X2 = ['2'; '2'; '1'; '2'; '1'; '3'; '3'; '2'];
%! new_X = [new_X1, double(new_X2)];
%! Vars = 'Feature1';
%! Labels = 1;
%! [pd, x, y] = partialDependence (mdl, Vars, Labels, new_X);
%! pdm = [0, 0, 0, 0, 0, 0.2500, 0.2500, 0.2500, 0.2500, 0.7500, 0.7500, ...
%! 0.7500, 0.7500, 0.7500, 0.7500, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, ...
%! 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, ...
%! 1.0000, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ...
%! 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ...
%! 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
%! assert_equal (pd, pdm)

## Test input validation for partialDependence method
%!error<ClassificationKNN.partialDependence: too few input arguments.> ...
%! partialDependence (ClassificationKNN (ones (4,2), ones (4,1)))
%!error<ClassificationKNN.partialDependence: too few input arguments.> ...
%! partialDependence (ClassificationKNN (ones (4,2), ones (4,1)), 1)
%!error<ClassificationKNN.partialDependence: name-value arguments must be in pairs.> ...
%! partialDependence (ClassificationKNN (ones (4,2), ones (4,1)), 1, ...
%! ones (4,1), 'NumObservationsToSample')
%!error<ClassificationKNN.partialDependence: name-value arguments must be in pairs.> ...
%! partialDependence (ClassificationKNN (ones (4,2), ones (4,1)), 1, ...
%! ones (4,1), 2)

## Test output for crossval method
%!shared x, y, obj
%! load fisheriris
%! x = meas;
%! y = species;
%! covMatrix = cov (x);
%! obj = fitcknn (x, y, 'NumNeighbors', 5, 'Distance', ...
%!      'mahalanobis', 'Cov', covMatrix);
%!test
%! status = warning;
%! warning ('off');
%! rand ('seed', 23);
%! CVMdl = crossval (obj);
%! warning (status);
%! assert_equal (class (CVMdl), "ClassificationPartitionedModel")
%! assert_equal ({CVMdl.X, CVMdl.Y}, {x, y})
%! assert_equal (CVMdl.KFold == 10, true)
%! assert_equal (CVMdl.ModelParameters.NumNeighbors == 5, true)
%! assert_equal (strcmp (CVMdl.ModelParameters.Distance, 'mahalanobis'), true)
%! assert_equal (class (CVMdl.Trained{1}), "ClassificationKNN")
%! assert_equal (isempty (CVMdl.Trained{1}.Mu), true)
%!test
%! status = warning;
%! warning ('off');
%! rand ('seed', 23);
%! CVMdl = crossval (obj, 'KFold', 5);
%! warning (status);
%! assert_equal (class (CVMdl), "ClassificationPartitionedModel")
%! assert_equal ({CVMdl.X, CVMdl.Y}, {x, y})
%! assert_equal (CVMdl.KFold == 5, true)
%! assert_equal (CVMdl.ModelParameters.NumNeighbors == 5, true)
%! assert_equal (strcmp (CVMdl.ModelParameters.Distance, 'mahalanobis'), true)
%! assert_equal (class (CVMdl.Trained{1}), "ClassificationKNN")
%! assert_equal (isempty (CVMdl.Trained{1}.Mu), isempty (obj.Mu))
%!test
%! status = warning;
%! warning ('off');
%! rand ('seed', 23);
%! CVMdl = crossval (obj, 'HoldOut', 0.2);
%! warning (status);
%! assert_equal (class (CVMdl), "ClassificationPartitionedModel")
%! assert_equal ({CVMdl.X, CVMdl.Y}, {x, y})
%! assert_equal (CVMdl.ModelParameters.NumNeighbors == 5, true)
%! assert_equal (strcmp (CVMdl.ModelParameters.Distance, 'mahalanobis'), true)
%! assert_equal (class (CVMdl.Trained{1}), "ClassificationKNN")
%! assert_equal (isempty (CVMdl.Trained{1}.Mu), isempty (obj.Mu))
%!test
%! obj = fitcknn (x, y, 'NumNeighbors', 10, 'Distance', 'cityblock');
%! status = warning;
%! warning ('off');
%! rand ('seed', 23);
%! CVMdl = crossval (obj, 'HoldOut', 0.2);
%! warning (status);
%! CVMdl = crossval (obj, 'LeaveOut', 'on');
%! assert_equal (class (CVMdl), "ClassificationPartitionedModel")
%! assert_equal ({CVMdl.X, CVMdl.Y}, {x, y})
%! assert_equal (CVMdl.ModelParameters.NumNeighbors == 10, true)
%! assert_equal (strcmp (CVMdl.ModelParameters.Distance, 'cityblock'), true)
%! assert_equal (class (CVMdl.Trained{1}), "ClassificationKNN")
%! assert_equal (isempty (CVMdl.Trained{1}.Mu), isempty (obj.Mu))
%!test
%! obj = fitcknn (x, y, 'NumNeighbors', 10, 'Distance', 'cityblock');
%! status = warning;
%! warning ('off');
%! rand ('seed', 23);
%! partition = cvpartition (y, 'KFold', 3);
%! warning (status);
%! CVMdl = crossval (obj, 'cvPartition', partition);
%! assert_equal (class (CVMdl), "ClassificationPartitionedModel")
%! assert_equal (CVMdl.KFold == 3, true)
%! assert_equal (CVMdl.ModelParameters.NumNeighbors == 10, true)
%! assert_equal (strcmp (CVMdl.ModelParameters.Distance, 'cityblock'), true)
%! assert_equal (class (CVMdl.Trained{1}), "ClassificationKNN")
%! assert_equal (isempty (CVMdl.Trained{1}.Mu), isempty (obj.Mu))

## Test input validation for crossval method
%!error<ClassificationKNN.crossval: Name-Value arguments must be in pairs.> ...
%! crossval (ClassificationKNN (ones (4,2), ones (4,1)), 'kfold')
%!error<ClassificationKNN.crossval: specify only one of the optional Name-Value paired arguments.>...
%! crossval (ClassificationKNN (ones (4,2), ones (4,1)), 'kfold', 12, 'holdout', 0.2)
%!error<ClassificationKNN.crossval: 'KFold' must be an integer value greater than 1.> ...
%! crossval (ClassificationKNN (ones (4,2), ones (4,1)), 'kfold', 'a')
%!error<ClassificationKNN.crossval: 'Holdout' must be a numeric value between 0 and 1.> ...
%! crossval (ClassificationKNN (ones (4,2), ones (4,1)), 'holdout', 2)
%!error<ClassificationKNN.crossval: 'Leaveout' must be either 'on' or 'off'.> ...
%! crossval (ClassificationKNN (ones (4,2), ones (4,1)), 'leaveout', 1)
%!error<ClassificationKNN.crossval: 'CVPartition' must be a 'cvpartition' object.> ...
%! crossval (ClassificationKNN (ones (4,2), ones (4,1)), 'cvpartition', 1)
%!error <ClassificationKNN.savemodel: too few input arguments.> ...
%! savemodel (ClassificationKNN ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]))
%!error <ClassificationKNN.savemodel: FNAME must be a character vector.> ...
%! savemodel (ClassificationKNN ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), 1)
%!error <ClassificationKNN.savemodel: FNAME must be a character vector.> ...
%! savemodel (ClassificationKNN ([1, 2; 2, 3; 3, 4; 4, 5], [1; 1; 2; 2]), ['ab'; 'cd'])

## There are only as many neighbors as training samples, so a larger
## NumNeighbors is capped at that.  It used to be stored as requested and then
## fail at prediction with a nonconformant operator error.
%!test
%! x = [1, 2; 3, 4; 5, 6; 7, 8; 2, 3];
%! y = [1; 2; 1; 2; 1];
%! for k = [5, 6, 20]
%!   a = fitcknn (x, y, 'NumNeighbors', k);
%!   assert_equal (a.NumNeighbors, 5);
%!   assert_equal (predict (a, [2, 2; 6, 6]), [1; 1]);
%! endfor

## An assigned ScoreTransform reaches the scores predict returns.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species, 'NumNeighbors', 5);
%! [~, s0] = predict (Mdl, meas(60,:));
%! Mdl.ScoreTransform = 'logit';
%! [~, s1] = predict (Mdl, meas(60,:));
%! assert_equal (s1, 1 ./ (1 + exp (-s0)), 1e-12);

## The transform is applied once per prediction, not once per observation.
## It used to sit inside the loop over the query rows and be applied to
## everything accumulated so far, so row i of n came back transformed n-i+1
## times and only the last row was right.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species, 'NumNeighbors', 5);
%! [~, s0] = predict (Mdl, meas(1:6,:));
%! Mdl.ScoreTransform = 'logit';
%! [~, s1] = predict (Mdl, meas(1:6,:));
%! assert_equal (s1, 1 ./ (1 + exp (-s0)), 1e-12);

## A row predicted on its own and the same row predicted among others get the
## same scores, which is what applying the transform once per row means.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species, 'ScoreTransform', 'doublelogit');
%! [~, s1] = predict (Mdl, meas(1,:));
%! [~, s6] = predict (Mdl, meas(1:6,:));
%! assert_equal (s6(1,:), s1, 1e-12);

## The values are R2024a's: a default KNN scores the first four observations
## [1 0 0] and doublelogit takes that to [0.8808 0.5 0.5], every row alike.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species, 'ScoreTransform', 'doublelogit');
%! [~, s] = predict (Mdl, meas(1:4,:));
%! assert_equal (s, repmat ([0.880797077977882, 0.5, 0.5], 4, 1), 1e-12);

## 'logit' is the logistic function and 'invlogit' its inverse, as MATLAB has
## them, and not the other way about.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species, 'NumNeighbors', 4);
%! [~, s0] = predict (Mdl, meas(60,:));
%! Mdl.ScoreTransform = 'invlogit';
%! [~, s] = predict (Mdl, meas(60,:));
%! assert_equal (s, log (s0 ./ (1 - s0)), 1e-12);

## Setting ScoreTransform to 'none' leaves predict working, the transform being
## a function handle like every other.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species, 'NumNeighbors', 5);
%! [~, s0] = predict (Mdl, meas(1:3,:));
%! Mdl.ScoreTransform = 'none';
%! assert_equal (class (Mdl.ScoreTransform), 'char');
%! [~, s1] = predict (Mdl, meas(1:3,:));
%! assert_equal (s1, s0);

## RowsUsed is empty when every observation was used.
%!test
%! load fisheriris
%! X = meas;
%! Y = grp2idx (species);
%! Mdl = fitcknn (X, Y);
%! assert_equal (Mdl.RowsUsed, []);
%! assert_equal (class (Mdl.RowsUsed), 'double');
%! assert_equal (Mdl.NumObservations, 150);
%! assert_equal (rows (Mdl.X), 150);

## A missing response drops its observation and RowsUsed marks it.
%!test
%! load fisheriris
%! X = meas;
%! Y = grp2idx (species);
%! Y(5) = NaN;
%! Mdl = fitcknn (X, Y);
%! assert_equal (class (Mdl.RowsUsed), 'logical');
%! assert_equal (size (Mdl.RowsUsed), [150, 1]);
%! assert_equal (sum (Mdl.RowsUsed), 149);
%! assert_equal (Mdl.RowsUsed(5), false);
%! assert_equal (Mdl.NumObservations, 149);
%! assert_equal (rows (Mdl.X), 149);

## A missing predictor keeps its observation, so RowsUsed stays empty.
%!test
%! load fisheriris
%! X = meas;
%! X(3,2) = NaN;
%! Y = grp2idx (species);
%! Mdl = fitcknn (X, Y);
%! assert_equal (Mdl.RowsUsed, []);
%! assert_equal (Mdl.NumObservations, 150);
%! assert_equal (rows (Mdl.X), 150);
%! assert_equal (sum (isnan (Mdl.X(:))), 1);

## Mu and Sigma are empty unless the predictors are standardized.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species);
%! assert_equal (Mdl.Mu, []);
%! assert_equal (Mdl.Sigma, []);

## Each predictor is summarized from every observation that has it.  Column 3
## is complete, so its mean spans all 150 rows.  Values from MATLAB R2024a.
%!test
%! load fisheriris
%! X = meas;
%! X(3,2) = NaN; X(17,4) = NaN; X(140,1) = NaN;
%! Mdl = fitcknn (X, species, 'Standardize', true);
%! assert_equal (Mdl.Mu, [5.8362416107382593, 3.0563758389261766, ...
%!                        3.7580000000000031, 1.2046979865771823], 1e-13);
%! assert_equal (Mdl.Sigma, [0.82627581654893234, 0.43717801242449683, ...
%!                           1.7652982332594667, 0.76196186774754338], 1e-13);
%! assert_equal (Mdl.Mu(3), mean (meas(:,3)), 1e-13);

## Each class carries its prior spread over its own observations.  Values
## from R2024a.
%!test
%! load fisheriris
%! i3 = [1:50, 51:80, 101:120];
%! Mdl = fitcknn (meas(i3,:), species(i3), 'Prior', 'uniform');
%! assert_equal (Mdl.Prior, [1, 1, 1] / 3, 1e-14);
%! assert_equal (Mdl.W(1), 1/150, 1e-14);
%! assert_equal (Mdl.W(51), 1/90, 1e-14);
%! assert_equal (Mdl.W(81), 1/60, 1e-14);

## Reassigning the prior rebuilds the weights with it.
%!test
%! load fisheriris
%! i3 = [1:50, 51:80, 101:120];
%! Mdl = fitcknn (meas(i3,:), species(i3));
%! assert_equal (Mdl.W(1), 0.01, 1e-14);
%! Mdl.Prior = [0.98, 0.01, 0.01];
%! assert_equal (Mdl.W(1), 0.0196, 1e-14);

## A fitted model survives savemodel and loadmodel: the properties come
## back as they were and it predicts the same.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species);
%! fname = tempname ();
%! savemodel (Mdl, fname);
%! M2 = loadmodel (fname);
%! delete (fname);
%! assert_equal (class (M2), 'ClassificationKNN');
%! assert_equal (M2.NumObservations, Mdl.NumObservations);
%! assert_equal (M2.PredictorNames, Mdl.PredictorNames);
%! assert_equal (class (M2.ScoreTransform), class (Mdl.ScoreTransform));
%! assert_equal (predict (M2, meas(1:5,:)), predict (Mdl, meas(1:5,:)));

## The six settable properties.  Every value below was measured on MATLAB
## R2024a; see KNN_SETTABLE_LEDGER.md for the probes that produced them.

## A lazy learner does its work at predict time, so a reassigned model
## predicts exactly as one refitted with the same options.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species, 'NumNeighbors', 1);
%! Mdl.NumNeighbors = 50;
%! Mdl.DistanceWeight = 'inverse';
%! Mdl.Distance = 'cityblock';
%! M2 = fitcknn (meas, species, 'NumNeighbors', 50, ...
%!               'DistanceWeight', 'inverse', 'Distance', 'cityblock');
%! assert_equal (predict (Mdl, meas), predict (M2, meas));

## More neighbours than observations is reduced, not refused.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species);
%! Mdl.NumNeighbors = 1000;
%! assert_equal (Mdl.NumNeighbors, 150);

## NSMethod is read-only and constrains the metric: a kd-tree can only be
## searched with the four metrics it was built for.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species);
%! assert_equal (Mdl.NSMethod, 'kdtree');
%! Mdl.Distance = 'minkowski';
%! assert_equal (Mdl.NSMethod, 'kdtree');
%! assert_equal (Mdl.DistParameter, 2);

## The parameter belongs to the metric, so changing the metric recomputes it
## and whatever the previous one held is discarded.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species, 'Distance', 'minkowski', ...
%!                'NSMethod', 'exhaustive');
%! Mdl.DistParameter = 3;
%! Mdl.Distance = 'euclidean';
%! assert_equal (Mdl.DistParameter, []);
%! Mdl.Distance = 'minkowski';
%! assert_equal (Mdl.DistParameter, 2);

## Reassigning the same metric is not a change, so the parameter survives.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species, 'Distance', 'minkowski', ...
%!                'NSMethod', 'exhaustive');
%! Mdl.DistParameter = 3;
%! Mdl.Distance = 'minkowski';
%! assert_equal (Mdl.DistParameter, 3);

%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species, 'NSMethod', 'exhaustive');
%! Mdl.Distance = 'seuclidean';
%! assert_equal (Mdl.DistParameter, std (meas), 1e-12);

%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species, 'NSMethod', 'exhaustive');
%! Mdl.Distance = 'mahalanobis';
%! assert_equal (Mdl.DistParameter, cov (meas), 1e-12);

## DistanceWeight is the text, as MATLAB reports it, and it weights the vote.
%!test
%! Mdl = fitcknn ([1, 0; 2, 0; 50, 0; 51, 0], {'b';'a';'a';'b'}, ...
%!                'NumNeighbors', 2, 'NSMethod', 'exhaustive');
%! assert_equal (Mdl.DistanceWeight, 'equal');
%! [label, score] = predict (Mdl, [0, 0]);
%! assert_equal (label, {'a'});
%! assert_equal (score, [0.5, 0.5], 1e-6);

%!test
%! Mdl = fitcknn ([1, 0; 2, 0; 50, 0; 51, 0], {'b';'a';'a';'b'}, ...
%!                'NumNeighbors', 2, 'DistanceWeight', 'inverse', ...
%!                'NSMethod', 'exhaustive');
%! [label, score] = predict (Mdl, [0, 0]);
%! assert_equal (label, {'b'});
%! assert_equal (score, [1/3, 2/3], 1e-6);

%!test
%! Mdl = fitcknn ([1, 0; 2, 0; 50, 0; 51, 0], {'b';'a';'a';'b'}, ...
%!                'NumNeighbors', 2, 'DistanceWeight', 'squaredinverse', ...
%!                'NSMethod', 'exhaustive');
%! [label, score] = predict (Mdl, [0, 0]);
%! assert_equal (label, {'b'});
%! assert_equal (score, [0.2, 0.8], 1e-6);

## BreakTies decides the label when two classes hold the same weight: the
## first class name, or the class of the nearest of the tied neighbours.
%!test
%! Mdl = fitcknn ([1, 0; 2, 0; 50, 0; 51, 0], {'b';'a';'a';'b'}, ...
%!                'NumNeighbors', 2, 'NSMethod', 'exhaustive');
%! Mdl.BreakTies = 'smallest';
%! assert_equal (predict (Mdl, [0, 0]), {'a'});
%! Mdl.BreakTies = 'nearest';
%! assert_equal (predict (Mdl, [0, 0]), {'b'});

## IncludeTies takes every neighbour sitting at the kth distance, which turns
## a single winner into a split.
%!test
%! x = [0, 0; 0, 0; 1, 1; 1, 1; 2, 2; 2, 2];
%! y = {'a'; 'b'; 'a'; 'b'; 'a'; 'b'};
%! Mdl = fitcknn (x, y, 'NumNeighbors', 1, 'NSMethod', 'exhaustive');
%! [~, score] = predict (Mdl, [0.5, 0.5]);
%! assert_equal (score, [1, 0]);
%! Mdl.IncludeTies = true;
%! [~, score] = predict (Mdl, [0.5, 0.5]);
%! assert_equal (score, [0.5, 0.5]);

## A saved model comes back with the weight it was given.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species, 'DistanceWeight', 'squaredinverse');
%! fname = tempname ();
%! savemodel (Mdl, fname);
%! M2 = loadmodel (fname);
%! delete (fname);
%! assert_equal (M2.DistanceWeight, 'squaredinverse');
%! assert_equal (predict (M2, meas), predict (Mdl, meas));

%!error<ClassificationKNN: 'NumNeighbors' must be a positive integer.> ...
%! Mdl = fitcknn (ones (5, 2), [1;1;1;2;2]); Mdl.NumNeighbors = 0;
%!error<ClassificationKNN: 'NumNeighbors' must be a positive integer.> ...
%! Mdl = fitcknn (ones (5, 2), [1;1;1;2;2]); Mdl.NumNeighbors = 2.5;
%!error<ClassificationKNN: 'BreakTies' must be 'smallest', 'random' or 'nearest'.> ...
%! Mdl = fitcknn (ones (5, 2), [1;1;1;2;2]); Mdl.BreakTies = 'bogus';
%!error<ClassificationKNN: 'IncludeTies' must be a logical scalar.> ...
%! Mdl = fitcknn (ones (5, 2), [1;1;1;2;2]); Mdl.IncludeTies = 2;
%!error<ClassificationKNN: 'DistanceWeight' must be 'equal', 'inverse', 'squaredinverse', or a function handle.> ...
%! Mdl = fitcknn (ones (5, 2), [1;1;1;2;2]); Mdl.DistanceWeight = 'bogus';
%!error<ClassificationKNN: 'Distance' for a kd-tree model can only be 'euclidean', 'cityblock', 'manhattan', 'chebychev', or 'minkowski'.> ...
%! load fisheriris; Mdl = fitcknn (meas, species); ...
%! Mdl.Distance = 'mahalanobis';
%!error<ClassificationKNN: 'DistParameter' can only be provided when 'Distance' is 'minkowski', 'mahalanobis', or 'seuclidean'.> ...
%! load fisheriris; ...
%! Mdl = fitcknn (meas, species, 'NSMethod', 'exhaustive'); ...
%! Mdl.DistParameter = 2;
%!error<ClassificationKNN: the exponent for the Minkowski distance must be a positive scalar.> ...
%! load fisheriris; ...
%! Mdl = fitcknn (meas, species, 'Distance', 'minkowski', ...
%!                'NSMethod', 'exhaustive'); ...
%! Mdl.DistParameter = 0;
%!error<ClassificationKNN: the scale for the standardized Euclidean distance must be a vector of positive values, with length equal to the number of columns in X.> ...
%! load fisheriris; ...
%! Mdl = fitcknn (meas, species, 'Distance', 'seuclidean', ...
%!                'NSMethod', 'exhaustive'); ...
%! Mdl.DistParameter = ones (1, 3);
%!error<ClassificationKNN: the covariance for the Mahalanobis distance must be symmetric and positive definite.> ...
%! load fisheriris; ...
%! Mdl = fitcknn (meas, species, 'Distance', 'mahalanobis', ...
%!                'NSMethod', 'exhaustive'); ...
%! Mdl.DistParameter = -eye (4);

## A zero scale is refused, where MATLAB accepts it and warns at predict time.
## The message MATLAB raises for a negative scale already promises positive
## values, so this keeps the property and its own contract in agreement.
%!error<ClassificationKNN: the scale for the standardized Euclidean distance must be a vector of positive values, with length equal to the number of columns in X.> ...
%! load fisheriris; ...
%! Mdl = fitcknn (meas, species, 'Distance', 'seuclidean', ...
%!                'NSMethod', 'exhaustive'); ...
%! Mdl.DistParameter = zeros (1, 4);

## edge, resubPredict, resubMargin, resubEdge and resubLoss.  Measured on
## MATLAB R2024a, whose posteriors this class reproduces exactly.

%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species, 'NumNeighbors', 5);
%! assert_equal (edge (Mdl, meas, species), 0.9253333333, 1e-9);
%! assert_equal (edge (Mdl, meas, species), ...
%!               mean (margin (Mdl, meas, species)), 1e-12);

## Weights normalized within class to the prior, not divided by their total,
## which would give 0.9006799117.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species, 'NumNeighbors', 5);
%! assert_equal (edge (Mdl, meas, species, 'Weights', (1:150)'), ...
%!               0.9265719286, 1e-9);

%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species, 'NumNeighbors', 5);
%! assert_equal (resubEdge (Mdl), 0.9253333333, 1e-9);
%! assert_equal (resubLoss (Mdl), 1/30, 1e-12);
%! assert_equal (sum (resubMargin (Mdl)), 138.8, 1e-9);

## A one-neighbour model is its own nearest neighbour everywhere, so the
## resubstitution numbers are perfect and say nothing about the model.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species);
%! assert_equal (resubLoss (Mdl), 0);
%! assert_equal (resubEdge (Mdl), 1);

%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species, 'NumNeighbors', 5);
%! [label, score, cost] = resubPredict (Mdl);
%! [l2, s2, c2] = predict (Mdl, meas);
%! assert_equal (label, l2);
%! assert_equal (score, s2);
%! assert_equal (cost, c2);

%!error<ClassificationKNN.edge: too few input arguments.> ...
%! load fisheriris; edge (fitcknn (meas, species), meas)
%!error<ClassificationKNN.edge: size of 'Weights' must equal the number of rows in X.> ...
%! load fisheriris; ...
%! edge (fitcknn (meas, species), meas, species, 'Weights', ones (3, 1))
%!error<ClassificationKNN.edge: invalid parameter name in optional paired arguments.> ...
%! load fisheriris; edge (fitcknn (meas, species), meas, species, 'Nope', 1)

## BinEdges is an empty cell, which is what MATLAB reports for this
## learner as well: it fits the predictors as they are.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species);
%! assert_equal (class (Mdl.BinEdges), 'cell');
%! assert_equal (Mdl.BinEdges, {});

## CategoricalPredictors and ExpandedPredictorNames, shapes measured on
## R2024a: an empty double and one name per predictor.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species);
%! assert_equal (Mdl.CategoricalPredictors, []);
%! assert_equal (size (Mdl.CategoricalPredictors), [0, 0]);
%! assert_equal (Mdl.ExpandedPredictorNames, Mdl.PredictorNames);
%! assert_equal (size (Mdl.ExpandedPredictorNames), [1, 4]);

%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species);
%! fname = tempname ();
%! savemodel (Mdl, fname);
%! M2 = loadmodel (fname);
%! delete (fname);
%! assert_equal (M2.CategoricalPredictors, Mdl.CategoricalPredictors);
%! assert_equal (M2.ExpandedPredictorNames, Mdl.ExpandedPredictorNames);

## The shared cost guard is in force here too, and the struct form is
## permuted into this model's class order.  The battery is on
## ClassificationDiscriminant.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species);
%! S = struct ('ClassNames', {{'virginica'; 'setosa'; 'versicolor'}}, ...
%!             'ClassificationCosts', [0, 1, 2; 3, 0, 4; 5, 6, 0]);
%! Mdl.Cost = S;
%! assert_equal (Mdl.Cost, [0, 4, 3; 6, 0, 5; 1, 2, 0]);

%!error<ClassificationKNN: 'Cost' must have zeros on its diagonal.> ...
%! load fisheriris
%! Mdl = fitcknn (meas, species);
%! Mdl.Cost = ones (3);

## HyperparameterOptimizationResults is declared for MATLAB compatibility and
## stays empty, this class running no search over its hyperparameters.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species);
%! assert_equal (isempty (Mdl.HyperparameterOptimizationResults), true);

## ModelParameters records the fit as it was asked for.  The field list and
## its order are MATLAB's, measured on R2024a.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species);
%! assert_equal (fieldnames (Mdl.ModelParameters)', {'NumNeighbors', ...
%! 'NSMethod', 'Distance', 'BucketSize', 'IncludeTies', 'DistanceWeight', ...
%! 'BreakTies', 'Exponent', 'Cov', 'Scale', 'StandardizeData', 'Version', ...
%! 'Method', 'Type'});

%!test
%! load fisheriris
%! MP = fitcknn (meas, species).ModelParameters;
%! assert_equal (MP.NumNeighbors, 1);
%! assert_equal (MP.NSMethod, 'kdtree');
%! assert_equal (MP.Distance, 'euclidean');
%! assert_equal (MP.BucketSize, 50);
%! assert_equal (MP.IncludeTies, false);
%! assert_equal (MP.DistanceWeight, 'equal');
%! assert_equal (MP.BreakTies, 'smallest');

%!test
%! load fisheriris
%! MP = fitcknn (meas, species).ModelParameters;
%! assert_equal (MP.Version, 1);
%! assert_equal (MP.Method, 'KNN');
%! assert_equal (MP.Type, 'classification');

## BucketSize belongs to the kd-tree alone, so an exhaustive search reports
## none, as MATLAB does.
%!test
%! load fisheriris
%! MP = fitcknn (meas, species, 'NSMethod', 'exhaustive').ModelParameters;
%! assert_equal (isempty (MP.BucketSize), true);

## The three distance parameters hold what was passed, never what was
## computed: a mahalanobis fit given no Cov reports none, while
## DistParameter carries the covariance the fit used.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species, 'Distance', 'mahalanobis');
%! assert_equal (isempty (Mdl.ModelParameters.Cov), true);
%! assert_equal (size (Mdl.DistParameter), [4, 4]);

%!test
%! load fisheriris
%! C = cov (meas);
%! MP = fitcknn (meas, species, 'Distance', 'mahalanobis', ...
%!               'Cov', C).ModelParameters;
%! assert_equal (MP.Cov, C);

%!test
%! load fisheriris
%! MP = fitcknn (meas, species, 'Distance', 'minkowski', ...
%!               'Exponent', 3).ModelParameters;
%! assert_equal (MP.Exponent, 3);
%! assert_equal (isempty (MP.Cov), true);

## Exponent is the one of the three that carries a default: a 'minkowski'
## fit that names none reports 2, where Cov and Scale would stay empty.
%!test
%! load fisheriris
%! MP = fitcknn (meas, species, 'Distance', 'minkowski').ModelParameters;
%! assert_equal (MP.Exponent, 2);

%!test
%! load fisheriris
%! MP = fitcknn (meas, species, 'Distance', 'seuclidean').ModelParameters;
%! assert_equal (isempty (MP.Scale), true);

## An explicit BucketSize is still not reported when the search cannot use
## it, as MATLAB does.
%!test
%! load fisheriris
%! MP = fitcknn (meas, species, 'NSMethod', 'exhaustive', ...
%!               'BucketSize', 30).ModelParameters;
%! assert_equal (isempty (MP.BucketSize), true);

%!test
%! load fisheriris
%! MP = fitcknn (meas, species, 'NSMethod', 'kdtree', ...
%!               'BucketSize', 30).ModelParameters;
%! assert_equal (MP.BucketSize, 30);

%!test
%! load fisheriris
%! S = std (meas);
%! MP = fitcknn (meas, species, 'Distance', 'seuclidean', ...
%!               'Scale', S).ModelParameters;
%! assert_equal (MP.Scale, S);

%!test
%! load fisheriris
%! MP = fitcknn (meas, species, 'Standardize', true).ModelParameters;
%! assert_equal (MP.StandardizeData, true);
%! assert_equal (class (MP.StandardizeData), 'logical');

%!test
%! load fisheriris
%! MP = fitcknn (meas, species, 'NumNeighbors', 10, ...
%!               'DistanceWeight', 'inverse', ...
%!               'BreakTies', 'nearest', 'IncludeTies', true).ModelParameters;
%! assert_equal (MP.NumNeighbors, 10);
%! assert_equal (MP.DistanceWeight, 'inverse');
%! assert_equal (MP.BreakTies, 'nearest');
%! assert_equal (MP.IncludeTies, true);

## A model searched exhaustively with a metric no kd-tree can take survives
## the round trip.  Its loader used to refuse it: the metric was assigned
## while NSMethod still held the default the empty model is built with.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species, 'Distance', 'seuclidean', ...
%!                'NSMethod', 'exhaustive');
%! fname = tempname ();
%! savemodel (Mdl, fname);
%! M2 = loadmodel (fname);
%! delete (fname);
%! assert_equal (M2.NSMethod, 'exhaustive');
%! assert_equal (M2.Distance, 'seuclidean');
%! assert_equal (M2.DistParameter, Mdl.DistParameter);
%! assert_equal (predict (M2, meas(1:5,:)), predict (Mdl, meas(1:5,:)));

%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species, 'Distance', 'mahalanobis');
%! fname = tempname ();
%! savemodel (Mdl, fname);
%! M2 = loadmodel (fname);
%! delete (fname);
%! assert_equal (M2.Distance, 'mahalanobis');
%! assert_equal (M2.DistParameter, Mdl.DistParameter);
%! assert_equal (predict (M2, meas(1:5,:)), predict (Mdl, meas(1:5,:)));

## 'manhattan' names the same metric as 'cityblock' and a kd-tree is built
## for it, so it may be assigned to one and a model fitted with it reloads.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species, 'Distance', 'manhattan');
%! assert_equal (Mdl.NSMethod, 'kdtree');
%! fname = tempname ();
%! savemodel (Mdl, fname);
%! M2 = loadmodel (fname);
%! delete (fname);
%! assert_equal (M2.Distance, 'manhattan');
%! assert_equal (predict (M2, meas(1:5,:)), predict (Mdl, meas(1:5,:)));

## The third output is the expected cost of each assignment, the posterior
## weighted by the cost matrix.  A default matrix cannot tell that apart from
## 1 - posterior, so the fixture below sets an asymmetric one.
%!test
%! load fisheriris
%! b = strcmp (species, 'setosa');
%! Mdl = fitcknn (meas, b, 'Cost', [0, 2; 5, 0]);
%! [label, ~, cost] = predict (Mdl, meas([1, 51],:));
%! assert_equal (label, [true; false]);
%! assert_equal (cost, [5, 0; 0, 2]);

%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species, 'NumNeighbors', 5);
%! [~, score, cost] = predict (Mdl, meas([1, 60, 120],:));
%! assert_equal (cost, 1 - score);

## Every documented score transform reaches the scores that are reported, and
## none of them moves the label: a transform reshapes what is reported, not
## what is decided.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species, 'NumNeighbors', 5);
%! Mdl.ScoreTransform = 'none';
%! [label, raw] = predict (Mdl, meas([1, 60, 120],:));
%! T = {'identity', @(x) x; 'doublelogit', @(x) 1 ./ (1 + exp (-2 * x)); ...
%!      'invlogit', @(x) log (x ./ (1 - x)); ...
%!      'logit', @(x) 1 ./ (1 + exp (-x)); ...
%!      'sign', @(x) sign (x); 'symmetric', @(x) 2 * x - 1; ...
%!      'symmetriclogit', @(x) 2 ./ (1 + exp (-x)) - 1};
%! for i = 1:rows (T)
%!   Mdl.ScoreTransform = T{i,1};
%!   [l, s] = predict (Mdl, meas([1, 60, 120],:));
%!   assert_equal (s, T{i,2}(raw), 1e-12);
%!   assert_equal (l, label);
%! endfor
%! ## ismax marks the largest score of each observation, ties to the first.
%! [~, k] = max (raw, [], 2);
%! e = zeros (size (raw));
%! e(sub2ind (size (raw), (1:rows (raw))', k)) = 1;
%! Mdl.ScoreTransform = 'ismax';
%! [~, s] = predict (Mdl, meas([1, 60, 120],:));
%! assert_equal (s, e);
%! Mdl.ScoreTransform = 'symmetricismax';
%! [~, s] = predict (Mdl, meas([1, 60, 120],:));
%! assert_equal (s, 2 * e - 1);

## A function handle is taken as given and applied to the scores.
%!test
%! load fisheriris
%! Mdl = fitcknn (meas, species, 'NumNeighbors', 5);
%! Mdl.ScoreTransform = 'none';
%! [label, raw] = predict (Mdl, meas([1, 60, 120],:));
%! Mdl.ScoreTransform = @(x) x .^ 2;
%! [l, s] = predict (Mdl, meas([1, 60, 120],:));
%! assert_equal (s, raw .^ 2, 1e-12);
%! assert_equal (l, label);
