classdef ClassificationModel < incremental.SimpleLearner
    %CLASSIFICATIONMODEL Incremental classification model.
    %   ClassificationModel is the super class for incremental
    %   classification models.

    %   Copyright 2020-2023 The MathWorks, Inc.

    properties(Dependent, SetAccess = protected)
        %CLASSNAMES Names of classes in Y.
        %   The ClassNames property is an array containing the class names for the
        %   response variable Y.
        %
        %   See also incremental.classif.ClassificationModel.
        ClassNames
    end

    properties(SetAccess = protected)
        %PRIOR Prior class probabilities.
        %   The Prior property is a vector with prior probabilities for classes.
        %
        %   See also incremental.classif.ClassificationModel.
        Prior
    end

    properties(Dependent, SetAccess = protected)
        %SCORETRANSFORM Transformation applied to predicted classification scores.
        %   The ScoreTransform property is a string describing how raw
        %   classification scores predicted by the model are transformed. You can
        %   assign a function handle or one of the following strings to this
        %   property: 'none', 'doublelogit', 'identity', 'invlogit', 'ismax',
        %   'logit', 'sign', 'symmetricismax', 'symmetriclogit', and 'symmetric'.
        %   You can use either 'identity' or 'none' for the identity
        %   transformation.
        %
        %   See also incremental.classif.ClassificationModel.
        ScoreTransform
    end

    properties(Access = protected)
        pCost
    end

    properties(Abstract,Constant,Access = protected)
        IsBinary
        IsAdaptive
        LabelPredictor
        HasCost
        HasAdditionalPredictionInputs
    end

    properties(Constant,Abstract,Access = protected)
        DefaultLoss
    end

    properties(Dependent, Access = protected)
        NumClasses
    end

    properties(Access = private)
        pClassNames
        pScoreTransform
        pClassType
        pAllowedClassifMetrics = {'classiferror','hinge','logit','quadratic','exponential',...
            'binodeviance'}
    end

    properties(Access = protected)
        pPriorisState(1,1) logical = false
        PriorDefined(1,1) logical = false
        PriorReset(1,1) logical = false
        IgnoreXNaNs = false
        MaxNumClasses {mustBeNumeric,mustBeReal,mustBeNonsparse}
        WeightsPerClass {mustBeNonsparse}
    end

    methods(Abstract,Access = protected)
        score(this,X,obsInRows,prior,cost,varargin)
    end

    methods
        function [varargout] = predict(this,X,varargin)
            %PREDICT Predict responses of the model.
            %   [LABEL,SCORE]=PREDICT(MODEL,X) returns predicted class labels and
            %   scores for model MODEL and predictors X. Pass X as a matrix with
            %   N observations from P predictors.
            %   Classification labels LABEL are an N-by-1 array of the same type
            %   as Y used for training, where N is the number of observations (rows in
            %   X). Scores SCORE are an N-by-K numeric matrix for N observations
            %   and K classes. The predicted label is assigned to the class with
            %   the largest score.
            %
            %   [LABEL,SCORE]=PREDICT(MODEL,X,'PARAM1',val1,'PARAM2',val2,...) specifies
            %   optional parameter name/value pairs:
            %       'ObservationsIn'    - String specifying the data orientation,
            %                             either 'rows' or 'columns'. Default: 'rows'.
            %
            validatePredictor(this);
            if ~this.HasAdditionalPredictionInputs
                if ~ObservationsInSupported(this)
                    narginchk(2,2);
                else
                    narginchk(2,4);
                end
            end            
            [predictionArgs,remainingArgs] = this.parsePredictionArgs(varargin{:});
            obsInRows = this.parseObservationsInInput(remainingArgs);
            [prior,cost,scoreTransform] = validatePredictArgs(this,predictionArgs);
            [varargout{1:nargout}] = predictWithoutValidation(this,X,obsInRows,prior,cost,scoreTransform);
        end

        function numclasses = get.NumClasses(this)
            numclasses = numel(this.pClassNames);
        end

        function classnames = get.ClassNames(this)
            classnames = labels(this.pClassNames);
        end

        function st = get.ScoreTransform(this)
            st = classreg.learning.internal.convertScoreTransform(...
                this.pScoreTransform,'string',[]);
        end

        function this = set.MaxNumClasses(this,maxnumclasses)
            this.MaxNumClasses = maxnumclasses;
        end
    end

    methods (Access = protected)
        function this = ClassificationModel(varargin)
            % Constructor
            this = this@incremental.SimpleLearner(varargin{:});
        end

        function [classArgs,remainingArgs] = parseDataParameters(~,params)
            params = incremental.utils.convertStructToCell(params);
            p = inputParser;
            p.StructExpand = true;
            p.KeepUnmatched = true;
            defaultclassnames = classreg.learning.internal.ClassLabel([]);
            defaultprior = 'empirical';
            addParameter(p,'ClassNames',defaultclassnames);
            addParameter(p,'Prior',defaultprior);
            addParameter(p,'ScoreTransform','identity');
            defaultnumclasses = [];
            addParameter(p,'MaxNumClasses',defaultnumclasses);
            defaultcost = [];
            addParameter(p,'Cost',defaultcost);
            defaultsw = [];
            addParameter(p,'WeightsPerClass',defaultsw);
            parse(p,params{:});
            classArgs = p.Results;
            remainingArgs = p.Unmatched;
        end

        function this = fillDataParameters(this,~,classParams)
            this = setClassNames(this,classParams.ClassNames);
            this = setPrior(this,classParams.Prior);
            this = setScoreTransform(this,classParams.ScoreTransform);
            this = setMaxNumClasses(this,classParams.MaxNumClasses);
            this = setCost(this,classParams.Cost);
            this = setWeightsPerClass(this,classParams.WeightsPerClass);
        end

        function this = setCost(this,cost)
            if this.HasCost
                if isempty(cost)
                    this.pCost = 1-eye(this.MaxNumClasses);
                else
                    this.pCost = validateCost(this,cost);
                end
            elseif ~isempty(cost)
                error(message('stats:incremental:Learner:UnrecognizedNV','Cost'));
            end
        end

        function processedCost = validateCost(this,cost)
            validateattributes(cost,{'numeric','struct'},{'real'},mfilename,'Cost');
            if isempty(this.ClassNames)
                error(message('stats:incremental:classif:ClassificationModel:CostWithoutClassNames'));
            end
            processedCost = classreg.learning.classif.FullClassificationModel.processCost(...
                cost,this.Prior,this.pClassNames,this.pClassNames);
            % Necessary for empirical prior case. The above internal method
            % does not remove NaN and Inf in cost when prior is unknown
            % (NaN). This works fine for offline learning, but for online
            % learning, prior would be NaNs in the beginning when it is empirical,
            % though it will later be filled in to actual values. We must still
            % remove costs with NaN and Inf values in such a case.
            badcost = any(isnan(processedCost)) | any(isinf(processedCost));
            if any(badcost,'all')
                cn = find(badcost,1);
                % Cost can be NaN/Inf if the corresponding class has 0 prior.
                % This can happen either when these values come from an
                % offline model or when the user supplies prior as NVP.
                if this.Prior(cn) ~= 0
                    classname = cellstr(classreg.learning.internal.ClassLabel(this.ClassNames(cn)));
                    error(message('stats:classreg:learning:classif:FullClassificationModel:checkNanCostForGoodPrior:BadCostInColumnForGoodClass',...
                        classname{1}));
                end
            end
        end

        function this = setScoreTransform(this,st)
            st = convertStringsToChars(st);
            this.pScoreTransform = ...
                classreg.learning.internal.convertScoreTransform(st,...
                'handle',this.NumClasses);
        end

        function this = setWeightsPerClass(this,sumWts)
            % initialize WeightsPerClass to all 0's for incrementally trained models
            if nargin>1 && ~isempty(sumWts)
                this.WeightsPerClass = zeros(1,this.NumClasses);
                % assign non-zero values
                this.WeightsPerClass(this.Prior~=0) = sumWts;
            else
                n = max(this.NumClasses,this.MaxNumClasses);
                this.WeightsPerClass = zeros(1,n);
            end
        end

        function this = setMaxNumClasses(this,numclasses)
            if this.IsBinary
                this.MaxNumClasses = 2;
                return;
            end
            if isnumeric(numclasses) ...
                    && isscalar(numclasses) ...
                    && numclasses > 0 ...
                    && numclasses == floor(numclasses) ...
                    && ~isinf(numclasses) ...
                    && ~isnan(numclasses)
                if (numclasses < this.NumClasses)
                    error(message('stats:incremental:classif:ClassificationModel:SmallMaxNumClasses'));
                elseif ~isempty(this.ClassNames) ...
                        && numclasses~=this.NumClasses
                    error(message('stats:incremental:classif:ClassificationModel:ClassNamesMaxNumClassesMismatch'));
                end
                this.MaxNumClasses = numclasses;
            elseif ~isempty(numclasses)
                error(message('stats:incremental:classif:ClassificationModel:WrongMaxNumClasses'));
            elseif ~isempty(this.ClassNames)
                this.MaxNumClasses = this.NumClasses;
            else
                error(message('stats:incremental:classif:ClassificationModel:NoMaxNumClassesAndClassNames'));
            end
        end

        function [this,Xraw,Yraw,Wraw] = prepareRawDataAndUpdateState(this,Xraw,Yraw,Wraw,obsInRows)
            % prepare data and update ClassNames
            this = addClasses(this,Yraw);
            % clean nan X and W for training
            removeNaNWX = true;
            % remove zero prior and zero cost observations for training
            removeNaNIndicesEarly = true;
            % remove observations with a column of zeros and row of zeros
            removeZeroColumnCost = true;
            [this,Xraw,Yraw,~,Wraw]  = prepareDataRaw(this,Xraw,Yraw,Wraw,obsInRows,removeNaNWX,removeNaNIndicesEarly,removeZeroColumnCost);
        end

        function [this,X,Y,Wraw,WisRaw,C] = prepareDataAndUpdateState(this,Xraw,Yraw,Wraw,obsInRows,numObservations)
            % update Prior, ClassNames and Weights if needed
            % clean nan X and W for training
            removeNaNWX = true;
            % remove zero prior and zero cost observations for training
            removeNaNIndicesEarly = true;
            % remove observations with a column of zeros and row of zeros
            removeZeroColumnCost = true;         
            [this,X,Y,C,Wraw] = prepareData(this,Xraw,Yraw,Wraw,obsInRows,removeNaNWX,removeNaNIndicesEarly,removeZeroColumnCost);
            WisRaw = true;
            this = updatePriorAndWeightsPerClass(this,C,Wraw,numObservations);
        end

        function this = updatePriorAndWeightsPerClass(this,C,W,numObservations)
            % update prior and WeightsPerClass

            % if prior is not empirical, do not modify prior and increase
            % per class Weights using raw Weights
            if ~this.pPriorisState
                if ~isempty(W)
                    batchWeightsPerClass = this.BatchWeightsPerClass(C,W);
                    if isempty(this.WeightsPerClass)
                        this.WeightsPerClass = batchWeightsPerClass;
                    else
                        this.WeightsPerClass = this.WeightsPerClass + batchWeightsPerClass;
                    end
                end
            else % if prior is empirical, update prior and increase
                % per class Weights using raw Weights
                if ~isempty(W)
                    batchWeightsPerClass = this.BatchWeightsPerClass(C,W);
                    % when prior is not fully defined, we need to
                    % temporarily adjust prior and batchWeightsPerClass
                    % sizes
                    % This is needed when all classes are not defined
                    % and we see new classes.
                    if ~this.PriorDefined
                        bwpcpriorsizediff =   numel(this.Prior) - numel(batchWeightsPerClass);
                        wpcpriorsizediff =   numel(this.WeightsPerClass) - numel(this.Prior);
                        % In order to avoid matrix size issues, we append zeros
                        % to batchWeightsPerClass if numel(batchWeightsPerClass) < numel(Prior)
                        if 0 < bwpcpriorsizediff
                            batchWeightsPerClass = [batchWeightsPerClass zeros(1,bwpcpriorsizediff)];
                        end
                        if 0 < wpcpriorsizediff
                            batchWeightsPerClass = [batchWeightsPerClass zeros(1,wpcpriorsizediff)];
                        end
                        if numObservations == 0
                            this.WeightsPerClass = batchWeightsPerClass;
                        elseif 0 < numObservations
                            % In order to avoid matrix size issues, we further append zeros
                            % to batchWeightsPerClass if numel(Prior) < numel(WeightsPerClass)
                            this.WeightsPerClass = this.WeightsPerClass + batchWeightsPerClass;
                        end
                        tempPrior = this.WeightsPerClass./sum(this.WeightsPerClass);
                        this.Prior = tempPrior(1:this.NumClasses);
                        % PriorDefined becomes true only when all classes
                        % are known and at least one instance of each class
                        % is seen.
                        if numel(this.Prior) == this.MaxNumClasses...
                                && nnz(this.Prior) == this.MaxNumClasses
                            this.PriorDefined = true;
                        end
                    else
                        if isempty(this.WeightsPerClass)
                            this.WeightsPerClass = batchWeightsPerClass;
                        else
                            this.WeightsPerClass = this.WeightsPerClass + batchWeightsPerClass;
                        end
                        this.Prior = this.WeightsPerClass./sum(this.WeightsPerClass);
                    end
                end
            end
        end

        function Y = mapYToIndex(this,Y)
            % map data to indices
            Y = grp2idx(Y,this.pClassNames);
            if this.IsBinary
                Y(Y==1) = -1;
                Y(Y==2) = 1;
            end
        end

        function [this,xout,yout,C,W,indsToExclude] = prepareData(this,X,Y,W,obsInRows,removeNaNWX,removeZeroPriorZeroCost,removeZeroColumnCost,prior,cost)
            % prepare data with mapping for labels
            
            if nargin == 8
                prior = this.Prior;
                cost = this.pCost;
            end
            
            [this,xout,yout,C,W,indsToExclude] = prepareDataRaw(this,X,Y,W,obsInRows,removeNaNWX,removeZeroPriorZeroCost,removeZeroColumnCost,prior,cost);
            yout = mapYToIndex(this,yout);
        end

        function [xout,yout,C,W] = prepareDataForMetrics(this,X,Y,W,obsInRows)
            % prepare data for metrics calculation with mapping for labels
            
            % keep nan valued features for metrics calculation
            removeNaNWX = false;
            % remove zero prior and zero cost observations for metrics calculation
            removeNaNIndicesEarly = true;
            % remove observations with a row of zeros
            removeZeroColumnCost = false; 
            [~,xout,yout,C,W] = prepareData(this,X,Y,W,obsInRows,removeNaNWX,removeNaNIndicesEarly,removeZeroColumnCost);
        end

        function [xout,yout,C,W] = prepareDataForLoss(this,X,Y,W,obsInRows,prior,cost)
            % prepare data for loss with mapping for labels

            % keep nan valued features for loss calculation
            removeNaNWX = false;
            % remove zero prior and zero cost observations for loss calculation
            removeNaNIndicesEarly = true;
            % remove observations with a row of zeros
            removeZeroColumnCost = false; 
            [~,xout,yout,C,W] = prepareData(this,X,Y,W,obsInRows,removeNaNWX,removeNaNIndicesEarly,removeZeroColumnCost,prior,cost);
        end
        

        function [windowMetrics,Wadjusted] = adjustWeightAndCalculateMetrics(this,X,Y,W,obsInRows,metricsFun)
            % prepare data, adjust weights using prior, calculate window loss

            [C,Wadjusted,s] = classifLossForMetrics(this,X,Y,W,obsInRows);
            metricsFun = cellfun(@(c)makeMetricsFunWithDefaultClass(c,this.Prior,this.HasCost),metricsFun,'UniformOutput',false);
            if this.HasCost                
                windowMetrics = cellfun(@(c) c(C,s,this.pCost),metricsFun,'UniformOutput',false);
            else
                windowMetrics = cellfun(@(c) c(C,s),metricsFun,'UniformOutput',false);
            end
        end

        function l = modelLoss(this,X,Y,W,obsInRows,lossFun,additionalArgs)
            % Get additional arguments
            [predArgs,~] = this.parsePredictionArgs(additionalArgs);
            % Model loss for incremental classification models.
            funloss = classreg.learning.internal.lossCheck(lossFun,'classification'); 
            [prior,cost,scoreTransform] = validatePredictArgs(this,predArgs);
            funloss = makeLossFunWithDefaultClass(funloss,prior,this.HasCost);
            [C,W,s] = classifLoss(this,X,Y,W,obsInRows,prior,cost,scoreTransform);
            if this.HasCost && ~isscalar(cost)
                l = funloss(C,s,W,cost);
            else
                l = funloss(C,s,W);
            end
        end

        function l = modelPerObservationLoss(this,X,Y,obsInRows,lossFun)
            % Model perObservationLoss for incremental classification models.
            funloss = classreg.learning.internal.lossCheck(lossFun,'classification',true,this.HasCost);
            funloss = makeMetricsFunWithDefaultClass(funloss,this.Prior,this.HasCost);

            % keep nan valued features for loss calculation
            removeNaNWX = false;
            % remove zero prior and zero cost observations for metrics calculation
            removeNaNIndicesEarly = false;
            % remove observations with a row of zeros
            removeZeroColumnCost = false; 
            
            [~,X,~,C,~,indsToExclude] = prepareData(this,X,Y,[],obsInRows,removeNaNWX,removeNaNIndicesEarly,removeZeroColumnCost);
            [~,sreduced,~] = predictWithoutValidation(this,X,obsInRows,this.Prior,this.pCost,this.pScoreTransform);
            skipWeights = true;
            if this.HasCost
                classreg.learning.internal.classifCheck(C,sreduced,[],this.pCost,skipWeights);
                lreduced = funloss(C,sreduced,this.pCost);
            else
                classreg.learning.internal.classifCheck(C,sreduced,[],[],skipWeights);
                lreduced = funloss(C,sreduced);
            end
            if islogical(lreduced)
                lreduced = cast(lreduced,'like',X);
            end
            l = nan(size(indsToExclude),'like',lreduced);
            l(~indsToExclude,:) = lreduced;
        end

        function metrics = allowedMetrics(this)
            % allowed metrics for incremental classification models
            metrics = this.pAllowedClassifMetrics;
        end

        function [predArgs,remainingArgs] = parsePredictionArgs(this,varargin)
            args = incremental.utils.convertStructToCell(varargin{:});
            p = inputParser;
            p.StructExpand = true;
            p.KeepUnmatched = true;
            if this.HasAdditionalPredictionInputs
                addParameter(p,'ScoreTransform',[]);
            end
            parse(p,args{:});
            predArgs = p.Results;
            remainingArgs = p.Unmatched;

            if ~this.HasAdditionalPredictionInputs
                % No parameters should remain other than potentially the
                % ObservationsIn parameter for models that support it for
                % models that do not support additional prediction inputs
                f = fieldnames(remainingArgs);
                f(ismember(f,'ObservationsIn')) = [];
                if ~isempty(f)
                    error(message('stats:incremental:Learner:UnrecognizedNV',f{1}));
                end

                % if no additional prediction inputs are expected, set predArgs
                % to empty
                predArgs = [];
            end
        end

        function [metricsnames,metricshandles] = parseMetrics(this,metricsnames,metrics)
            % Parsing metrics for the incremental classification models
            n = numel(metrics);
            metricshandles = cell(n,1);
            for ii = 1 : n
                if ischar(metrics{ii})
                    metricshandles{ii} = classreg.learning.internal.lossCheck(metrics{ii},'classification',true,this.HasCost);
                    metricsnames{ii} = incremental.internal.mapToMetricName(metricsnames{ii});
                else
                    metricshandles{ii} = classreg.learning.internal.lossCheck(metrics{ii},'classification',true,this.HasCost);
                end
            end
        end

        function this = resetModel(this,varargin)
            % reset states for incremental classification models
            if this.pPriorisState
                this.Prior = nan(size(this.Prior),'like',this.Prior);
                this.PriorReset = true;
                this.PriorDefined = false;
            end
            this.WeightsPerClass = zeros(size(this.WeightsPerClass),'like',this.WeightsPerClass);
        end
        function canpredict = canPredict(this)
            % can classification model predict
            if ~isempty(this.ClassNames)
                canpredict = true;
            else
                canpredict = false;
            end
            canpredict = canPredict@incremental.SimpleLearner(this) && canpredict;
        end

        function cancalculateloss = canCalculateLoss(this)
            % can classification model calculate loss

            % If PriorDefined is true, all classes are known and seen
            % at least once; then, classification model can calculate loss.
            % If all classes are known but not seen, we can still
            % calculate loss if all classes are known and Prior does not
            % have any NaN values.
            calculateloss = false;
            if this.PriorDefined || (this.NumClasses == this.MaxNumClasses && ~any(isnan(this.Prior)))
                calculateloss = true;
            end
            cancalculateloss = canPredict(this) && calculateloss;
        end

        function cancalculatemetrics = canCalculateMetrics(this)
            % can classification model calculate metrics

            % If PriorDefined is true, all classes are known and seen
            % at least once; then, classification model can calculate
            % metrics. Otherwise, do not calculate metrics.

            cancalculatemetrics = canPredict(this) && this.PriorDefined;
        end

        function defaultmetric = defaultMetric(this)
            % Default metric for the incremental classification model
            defaultmetric = this.DefaultLoss;
        end

        function defaultloss = defaultLoss(this)
            % Default loss for the incremental classification model
            defaultloss = this.DefaultLoss;
        end

        function defaultPerObservationLoss = defaultPerObservationLoss(this)
            % Default loss for the incremental classification model
            defaultPerObservationLoss = this.DefaultLoss;
        end


        function allowedPerObservationLosses = allowedPerObservationLosses(this)
            % allowed per observations losses incremental classification models
            allowedPerObservationLosses = this.pAllowedClassifMetrics;
        end

        function C = classCount(this,Yraw)
            C = classreg.learning.internal.classCount(this.pClassNames,Yraw);
        end

        function W = adjustWeights(this,C,W,prior) %#ok<INUSL>
            % adjust weights using prior
            if isempty(W) || isempty(C)
                return;
            end
            WC = C.*W;
            Wj = sum(WC,1);
            adjWFactor = zeros(1,numel(Wj),'like',Wj);
            zeroprior = Wj==0;
            adjWFactor(~zeroprior) = prior(~zeroprior)./Wj(~zeroprior);
            W = sum(WC.*adjWFactor,2);
        end

    end

    methods (Access = private)
        function [varargout] = predictWithoutValidation(this,X,obsInRows,prior,cost,scoreTransform)
            scores = score(this,X,obsInRows,prior,cost);
            [varargout{1:nargout}] = this.LabelPredictor(this.ClassNames, ...
                prior,cost,scores,scoreTransform);
        end

        function [tPrior,tCost,tScoreTransform] = validatePredictArgs(this,predArgs)
            if ~isempty(predArgs)
                if this.HasAdditionalPredictionInputs ...
                        && ~isempty(predArgs.Cost)
                    tCost = validateCost(this,predArgs.Cost);
                else
                    tCost = this.pCost;
                end
                if this.HasAdditionalPredictionInputs ...
                        && ~isempty(predArgs.ScoreTransform)
                    st = convertStringsToChars(predArgs.ScoreTransform);
                    tScoreTransform = classreg.learning.internal.convertScoreTransform(st,...
                        'handle',this.NumClasses);
                else
                    tScoreTransform = this.pScoreTransform;
                end
                if this.HasAdditionalPredictionInputs ...
                        && ~isempty(predArgs.Prior)
                    tPrior = predArgs.Prior;
                    validatePrior(this,tPrior);
                    if strcmp('uniform',tPrior)
                        tPrior = ones(1,this.NumClasses)/this.NumClasses;
                    else
                        tPrior = tPrior(:)'/sum(tPrior(:));
                    end
                else
                    tPrior = this.Prior;
                end
            else
                tCost = this.pCost;
                tScoreTransform = this.pScoreTransform;
                tPrior = this.Prior;
            end
            % When not all classes are seen (for example, when ClassNames
            % is provided, hence NumClasses=MaxNumClasses, but not all
            % classes are seen), this.WeightsPerClass is 0 for the classes which
            % aren't seen yet. When not all classes are seen, we must
            % replace the columns corresponding to unknown classes to NaNs
            % and rows to 0s to make sure they don't get predicted into.
            % This is a temporary change just for prediction.
            % If ClassNames are known and Prior is known (provided by user),
            % then we can predict into such a class with 0 observations
            % seen during training, but known non-zero prior
            cond1 = nnz(this.WeightsPerClass) ~= this.MaxNumClasses && length(tPrior)~=this.MaxNumClasses;
            cond2 = nnz(tPrior) ~= this.MaxNumClasses;
            % It will be either cond1 or cond2
            if cond1 || cond2
                if cond1
                    % Some classes have no observations seen and we don't know
                    % Prior for all classes (which means Prior was not provided
                    % by the user), then we replace these classes cost
                    idx = this.WeightsPerClass;
                else
                    % Prior is known, but some classes have 0 Prior. Replace
                    % such costs corresponding to 0 prior.
                    idx = tPrior;
                end
                tCost(~idx,:) = 0;
                tCost(:,~idx) = NaN;
                tCost(1:1+size(tCost,1):end) = 0;
            end
        end

        function [C,Wadjusted,s] = classifLossForMetrics(this,X,Y,W,obsInRows)
            % Return per observation predictions and adjusted weights for
            % metrics calculation

            [X,~,C,W] = prepareDataForMetrics(this,X,Y,W,obsInRows);
            [~,s,~] = predictWithoutValidation(this,X,obsInRows,this.Prior,this.pCost,this.pScoreTransform);
            % Remove all nan rows from s, C and W for metrics calculation.
            % W won't have any nan values.
            % Inf values are not filtered out.
            indsToExclude = all(isnan(s),2) | isnan(W);
            W = W(~indsToExclude,:);
            C = C(~indsToExclude,:);
            s = s(~indsToExclude,:);
            % adjust weights using prior.
            Wadjusted = adjustWeights(this,C,W,this.Prior);
            % Wadjusted should not have any 0 values or nan values. Such
            % weights are filtered out for metrics calculations.
            % Inf values are not filtered out.            
            indsToExclude = Wadjusted == 0 | isnan(Wadjusted);
            Wadjusted = Wadjusted(~indsToExclude,:);
            C = C(~indsToExclude,:);
            s = s(~indsToExclude,:);            

            if this.HasCost
                classreg.learning.internal.classifCheck(C,s,Wadjusted,this.pCost);
            else
                classreg.learning.internal.classifCheck(C,s,Wadjusted,[]);
            end
        end

        function [C,Wadjusted,s] = classifLoss(this,X,Y,W,obsInRows,prior,cost,scoreTransform)
            % Return per observation predictions and adjusted weights

            % Remove all nan values from C and W for loss calculation
            % All zero prior and zero cost entries are removed.
            [X,~,C,W] = prepareDataForLoss(this,X,Y,W,obsInRows,prior,cost);
            % scores may have nan values.
            [~,s,~] = predictWithoutValidation(this,X,obsInRows,prior,cost,scoreTransform);

            % adjust weights returns normalized weights
            Wadjusted = adjustWeights(this,C,W,prior);

            if this.HasCost
                classreg.learning.internal.classifCheck(C,s,Wadjusted,cost);
            else
                classreg.learning.internal.classifCheck(C,s,Wadjusted,[]);
            end
        end

        function this = addClasses(this,Y)
            yout = classreg.learning.internal.ClassLabel(Y);
            if ~isempty(this.pClassNames)
                if ~isa(labels(yout),this.pClassType)
                    error(message('stats:incremental:classif:ClassificationModel:ClassTypeMismatch'));
                end
            else
                this = setClassType(this,yout);
            end

            newclassindices = ~ismember(yout,this.pClassNames) & ~ismissing(yout);
            newclasses = levels(yout(newclassindices));
            if any(newclassindices)
                totalnumclasses = this.NumClasses + numel(newclasses);
                if this.MaxNumClasses < totalnumclasses
                    if this.IsBinary
                        if this.NumClasses == 0
                            % Binary models can't have more than two classes
                            error(message('stats:incremental:classif:ClassificationModel:BinaryMustHaveTwoClasses'));
                        else
                            error(message('stats:incremental:classif:ClassificationModel:CannotAddClass'));
                        end
                    else
                        if this.NumClasses == 0
                            mnc = num2str(this.MaxNumClasses);
                            error(message('stats:incremental:classif:ClassificationModel:MaxNumClassesExceeded',mnc));
                        else
                            error(message('stats:incremental:classif:ClassificationModel:CannotAddClass'));
                        end
                    end
                end
                if isempty(this.pClassNames)
                    this.pClassNames = newclasses;
                else
                    this.pClassNames = vertcat(this.pClassNames,newclasses);
                end
                if ~isempty(this.Prior) && ~any(isnan(this.Prior))
                    this.Prior = [this.Prior zeros(1,numel(newclasses))];
                    this.PriorReset = true;
                else % if Prior is all NaNs
                    this.Prior = [this.Prior NaN(1,numel(newclasses))];
                end
            end
        end

        function C = extractClassCounts(this,Y,errorForNewClass)
            % extract class counts for given vector of labels

            % if errorForNewClass is true, error for new classes. Else,
            % check canCalculateLoss.
            % if canCalculateLoss is true, error out for unknown
            % classes.
            % PriorDefined being true is not sufficient since
            % PriorDefined will be false if all classes are known but
            % not seen.
            C = classCounts(Y,this.pClassNames, errorForNewClass || canCalculateLoss(this));
        end

        function [this,X,yout,C,W,indsToExclude] = prepareDataRaw(this,X,Y,W,obsInRows,removeNaNWX,removeNaNIndicesEarly,removeZeroColumnCost,prior,cost)
            % prepare data without mapping labels to categories
            
            if nargin == 8
                prior = this.Prior;
                cost = this.pCost;
            end

            % W will be [] for perObservationLoss
            ignoreW = isequal(W,[]);
            ignoreXNaNs = this.IgnoreXNaNs;

            yout = classreg.learning.internal.ClassLabel(Y);
            indsToExclude = ismissing(yout);         
            if removeNaNWX
                if obsInRows
                    if ignoreXNaNs
                        inds = all(isnan(X),2);
                    else
                        inds = any(isnan(X),2);
                    end
                else
                    if ignoreXNaNs
                        inds =  all(isnan(X));
                    else
                        inds = any(isnan(X));
                    end
                end
                indsToExclude = inds(:) | indsToExclude;
            end
            if ~ignoreW
                if removeNaNWX
                    indsToExclude = indsToExclude | W == 0 | isnan(W);
                end
            end

            % If removeNaNIndicesEarly is true, reduce yout, xout, and W initially. Else, do not reduce
            % them yet. C will have reduced size as well after call to classCounts.
            if removeNaNIndicesEarly
                yout = yout(~indsToExclude,:);
                if obsInRows
                    X = X(~indsToExclude,:);
                else
                    X = X(:,~indsToExclude);
                end
                if ~ignoreW
                    W = W(~indsToExclude,:);
                end
            end

            % if Prior is defined, do not allow unknown classes
            C = classCounts(yout,this.pClassNames, this.PriorDefined);

            % If true, further reduce yout, xout, and W using Prior and
            % Cost.
            if removeNaNIndicesEarly
                % Remove indices for observations with 
                % 0 prior and 0 cost
                if this.PriorReset
                    this.PriorReset = false;
                else
                    if  this.PriorDefined
                        zeroprior = prior==0;
                        if any(zeroprior)
                            t = any(C(:,zeroprior),2);
                            C(t,:) = [];
                            yout = yout(~t,:);
                            if obsInRows
                                X = X(~t,:);
                            else
                                X = X(:,~t);
                            end
                            %remove Weights with zero prior
                            if ~ignoreW
                                W = W(~t,:);
                            end
                        end
                    end
                end
                % remove zero cost classes if model has cost
                if this.HasCost
                    zerocost = all(cost==0,2)';
                    if removeZeroColumnCost
                        zerocost = zerocost & all(cost==0,1);
                    end
                    if any(zerocost)
                        t = any(C(:,zerocost),2);
                        C(t,:) = [];
                        yout = yout(~t,:);
                        if obsInRows
                            X = X(~t,:);
                        else
                            X = X(:,~t);
                        end
                        %remove Weights with zero cost
                        if ~ignoreW
                            W = W(~t,:);
                        end
                    end
                end
            else
                yout = yout(~indsToExclude,:);
                if obsInRows
                    X = X(~indsToExclude,:);
                else
                    X = X(:,~indsToExclude);
                end
                if ~ignoreW
                    W = W(~indsToExclude,:);
                end
                C = C(~indsToExclude,:);
            end
        end

        function this = setPrior(this,prior)
            C = this.NumClasses;
            if ischar(prior) || isstring(prior)
                prior = validatestring(prior,{'empirical','uniform'},mfilename,'Prior');
                if isequal(prior,'empirical')
                    this.pPriorisState = true;
                    % If NumClasses is 1, Prior is defined and equal to 1
                    % This happens if a single class is provided for a
                    % multi-class model
                    if C == 1
                        this.Prior = 1;
                        this.PriorDefined = true;
                    else
                        this.Prior = NaN(1,C);
                        this.PriorDefined = false;
                    end
                else
                    if C == 0
                        error(message('stats:incremental:classif:ClassificationModel:PriorMustBeEmpirical'));
                    end
                    this.pPriorisState = false;
                    this.Prior = ones(1,C)./C;
                    this.PriorDefined = true;
                end
            else
                validatePrior(this,prior);
                this.pPriorisState = false;
                this.Prior = prior(:)'/sum(prior(:));
                this.PriorDefined = true;
            end
        end

        function validatePrior(this,prior)
            C = this.NumClasses;
            if isstring(prior) || ischar(prior)
                if strcmp(prior,'empirical')
                    error(message('stats:classreg:learning:classif:ClassificationModel:setPrivatePrior:EmpiricalOrEmptyPrior'));
                end
                prior = validatestring(prior,{'uniform'},mfilename,'Prior');
            end
            if C == 0
                error(message('stats:incremental:classif:ClassificationModel:PriorMustBeEmpirical'));
            end
            if ~isfloat(prior) || any(prior<0) || all(prior==0) || isempty(prior)
                error(message('stats:classreg:learning:classif:FullClassificationModel:processPrior:BadNumericPrior'));
            end
            if any(isnan(prior)) || any(isinf(prior))
                error(message('stats:classreg:learning:classif:FullClassificationModel:processPrior:NaNInfPrior'));
            end
            if length(prior)~=C
                error(message('stats:classreg:learning:classif:FullClassificationModel:processPrior:BadPriorLength', C));
            end
            validateattributes(prior,{'numeric'},{'real'},mfilename,'Prior');
            % check for nonsparse prior
            internal.stats.checkSupportedNumeric('Prior',prior);
        end

        function this = setClassType(this,classes)
            if isnumeric(labels(classes))
                this.pClassType = 'numeric';
            else
                this.pClassType = class(labels(classes));
            end
        end

        function this = setClassNames(this,classnames)
            classlabels = classreg.learning.internal.ClassLabel(classnames);
            uniquelabels = levels(classlabels);
            if numel(classlabels)~=numel(uniquelabels)
                error(message('stats:incremental:classif:ClassificationModel:NonUniqueClassNames'));
            end
            if ~isempty(classlabels)
                if this.IsBinary
                    if numel(classlabels)~=2
                        error(message('stats:incremental:classif:ClassificationModel:BinaryMustHaveTwoClasses'));
                    end
                else
                    if numel(uniquelabels)==0
                        error(message('stats:incremental:classif:ClassificationModel:EmptyLabel'));
                    end
                end
            end
            this.pClassNames = classlabels;
            if ~isempty(this.pClassNames)
                this = setClassType(this,classlabels);
            end
        end
    end

    methods(Static, Access = protected)
        function [labels,scores,classnum] = maxScore(classnames,prior,~,scores,scoreTransform)
            scores = scoreTransform(scores);
            N = size(scores,1);
            notNaN = ~all(isnan(scores),2);
            [~,cls] = max(prior);
            labels = repmat(classnames(cls,:),N,1);
            [~,classnum] = max(scores(notNaN,:),[],2);
            labels(notNaN,:) = classnames(classnum,:);
            if N>size(classnum,1)
                temp = NaN(N,1,'like',scores);
                temp(notNaN,:) = classnum;
                classnum = temp;
            end
        end

        function [labels,scores,cost,classnum] = ...
                minCost(classnames,Prior,Cost,posterior,scoreTransform)
            cost = posterior*Cost;
            N = size(posterior,1);
            notNaN = ~all(isnan(cost),2);
            [~,cls] = max(Prior);
            labels = repmat(classnames(cls,:),N,1);
            [~,classnum] = min(cost(notNaN,:),[],2);
            labels(notNaN,:) = classnames(classnum,:);
            scores = scoreTransform(posterior);
            scores = scores(:,1:numel(Prior));
        end

        function WC = BatchWeightsPerClass(C,W)
            WC = C.*W;
            WC = sum(WC,1);
        end
    end

    methods (Access = protected)
        function propList = propertiesForDisplay(this,propList)
            cnames = this.ClassNames;
            if ischar(cnames)
                propList.ClassNames = cnames;
            else
                propList.ClassNames = cnames';
            end
            propList.ScoreTransform = this.ScoreTransform;
        end

        function [readyForCodegen,undefinedParameters] = validateForCoder(this)
            % check whether incremental ClassificationModel is ready for code
            %   generation
            [readyForCodegen,undefinedParameters] = validateForCoder@incremental.SimpleLearner(this);
            % For code generation, ClassNames must be fully defined
            if this.NumClasses ~= this.MaxNumClasses
                readyForCodegen = false;
                undefinedParameters{end+1} = 'ClassNames';
            end
        end

    end

    methods(Hidden)
        function mincostsupported = MinCostSupported(this)
            mincostsupported = ismember('mincost',allowedMetrics(this));
        end
    end    

    methods (Hidden)
        function this = deserialize(this,deserializeableStr)
            % construct incremental classification model from a given deserializable struct
            this = deserialize@incremental.SimpleLearner(this,deserializeableStr);
            assert(isfield(deserializeableStr,'ClassificationParameters'),...
                                                  'Deserializable classification struct must have ''ClassificationParameters'' field.');
            deserializeableStr = deserializeableStr.ClassificationParameters;
            this.Prior = deserializeableStr.Prior;
            this.pPriorisState = deserializeableStr.pPriorisState;
            this.PriorDefined = deserializeableStr.PriorDefined;
            this.PriorReset = deserializeableStr.PriorReset;
            this.pClassNames = deserializeableStr.pClassNames;
            this.pClassType = deserializeableStr.pClassType;
            this.pScoreTransform = deserializeableStr.pScoreTransform;

            % Backwards compatibility
            if isfield(deserializeableStr,'IgnoreXNaNs')
                this.IgnoreXNaNs = deserializeableStr.IgnoreXNaNs;
            end    
            
            if ~isfield(deserializeableStr,'MaxNumClasses') %if MaxNumClasses is not a field, struct is saved prior to R2021a
                this = setMaxNumClasses(this);
            else
                this.MaxNumClasses = deserializeableStr.MaxNumClasses;
            end

            if ~isfield(deserializeableStr,'WeightsPerClass')  %if WeightsPerClass is not a field, struct is saved prior to R2021a
                this = setWeightsPerClass(this);
            else
                this.WeightsPerClass = deserializeableStr.WeightsPerClass;
            end

            if ~isfield(deserializeableStr,'Cost') %if WeightsPerClass is not a field, struct is saved prior to R2021a
                this.pCost = [];
            else
                this.pCost = deserializeableStr.Cost;
            end
        end        
    end
    methods (Static,Hidden)
        function str = serialize(this,str)
            % Convert incremental classification object to deserializeable
            % struct. 
            str.ClassificationParameters = serializeHelper(this);
        end         
    end

    methods (Hidden) %methods used for code generation
        function [tunableStr, nontunableStr] = toStruct(this,tunableStr,nontunableStr)
            [tunableStr,nontunableStr] = toStruct@incremental.SimpleLearner(this,tunableStr,nontunableStr);
            tunableStr.PriorDefined = this.PriorDefined;
            % if Prior is not defined, assign 0 values instead of nan for
            % code generation
            if this.PriorDefined
                tunableStr.Prior = this.Prior;
            else
                tunableStr.Prior = cast(-1*ones(1,this.MaxNumClasses),'like',this.Prior);
                tunableStr.Prior(1:this.NumClasses) = this.Prior;
                tunableStr.Prior(isnan(tunableStr.Prior)) = cast(-1,'like',this.Prior);
            end
            tunableStr.WeightsPerClass = this.WeightsPerClass;
            tunableStr.PriorReset = this.PriorReset;
            nontunableStr.PriorisState = this.pPriorisState;
            nontunableStr.MaxNumClasses = this.MaxNumClasses;
            nontunableStr.Cost = this.pCost;
            [nontunableStr.ClassNames,nontunableStr.ClassType,nontunableStr.ClassNamesLength,nontunableStr.IsOrdinal] = labelTypeLength(this.pClassType,this.ClassNames);
            nontunableStr.NumClasses = this.NumClasses;
            [tunableStr,nontunableStr] = toStructScoreTransform(this,tunableStr,nontunableStr);
            nontunableStr.IgnoreXNaNs = this.IgnoreXNaNs;
            nontunableStr.IsBinary = this.IsBinary;
            nontunableStr.IsAdaptive = this.IsAdaptive;
            function [classnames,type,length,ordinal] = labelTypeLength(type,classnames)
                ordinal = false;
                if  isequal(type,'numeric')
                    type = int8(1);
                    length = 1;
                elseif isequal(type,'logical')
                    type = int8(2);
                    length = 1;
                elseif isequal(type,'char')
                    type = int8(3);
                    length = size(type,2);
                elseif isequal(type,'cell')
                    type = int8(4);
                    length = cellfun(@(x)numel(x), classnames);
                    classnames = char(classnames);
                else % isequal(label,'categorical') is true
                    if ~isordinal(classnames)
                        type = int8(5);
                    else
                        type = int8(6);
                        ordinal = true;
                    end
                    length = cellfun(@(x)numel(x), cellstr(classnames));
                    classnames = char(classnames);
                end
            end
        end

        function [tunableStr,nontunableStr] = toStructScoreTransform(this,tunableStr,nontunableStr)
            % extract the name of the transform
            nontunableStr.ScoreTransform = func2str(this.pScoreTransform);
            isincremental = true;
            doclass = true;
            nontunableStr = classreg.learning.coderutils.transformToCoderStruct(nontunableStr,doclass,this.pScoreTransform,this.NumClasses,isincremental);
        end

        function this = resetTemplateProperties(this)
            % Reset properties that are irrelevant in template learners            
            
            this = setClassNames(this,[]);
            this = setPrior(this,'empirical');
            this = setScoreTransform(this,'none');
        end
    end
    methods (Static, Hidden) %static methods used for code generation
        function params = mergeScoreTransformStructs(params,~,nontunableStr)

            if nontunableStr.CustomScoreTransform
                rt = str2func(nontunableStr.ScoreTransformFull);
                pScoreTransform = classreg.learning.internal.convertScoreTransform(rt,...
                    'handle',1);
            else
                rt = lower(char(nontunableStr.ScoreTransform));
                pScoreTransform = classreg.learning.internal.convertScoreTransform(rt,...
                    'handle',1);
            end
            params.ClassificationParameters.pScoreTransform = pScoreTransform;
        end

        function params = mergeClassifStructs(tunableStr,nontunableStr)
            % merge necessary parameters from tunable and nontunable
            % structs
            params = incremental.SimpleLearner.mergeLearnerStructs(tunableStr,nontunableStr,true);

            fields = {'Prior','PriorDefined','PriorReset','MaxNumClasses','Cost','IgnoreXNaNs','WeightsPerClass'};
            params.ClassificationParameters = incremental.coderutils.mergeStructs(params,fields,tunableStr,nontunableStr);
            % if Prior is not defined, assign nan values
            if ~params.ClassificationParameters.PriorDefined
                params.ClassificationParameters.Prior = params.ClassificationParameters.Prior;
                params.ClassificationParameters.Prior(params.ClassificationParameters.Prior==-1) = cast(NaN,'like',params.ClassificationParameters.Prior);
            end

            switch nontunableStr.ClassType
                case 1
                    classnames = nontunableStr.ClassNames;
                    classtype = 'numeric';
                case 2
                    classnames = nontunableStr.ClassNames;
                    classtype = 'logical';
                case 3
                    classnames = nontunableStr.ClassNames;
                    classtype = 'char';
                case 4
                    classnames = cellstr(nontunableStr.ClassNames);
                    classtype = 'cell';
                otherwise
                    classnames = categorical(cellstr(nontunableStr.ClassNames),'Ordinal',nontunableStr.IsOrdinal);
                    classtype = 'categorical';
            end

            params.ClassificationParameters.pClassNames = classreg.learning.internal.ClassLabel(classnames);
            params.ClassificationParameters.pClassType = classtype;
            params.ClassificationParameters.pPriorisState = nontunableStr.PriorisState;

        end         
    end
end


function  C = classCounts(Y,classNames, errorCondition)
knownclasses = ismember(levels(Y),classNames);
if ~all(knownclasses)
    ylevels = levels(Y);
    unknownyclasses = ylevels(~knownclasses);
    if errorCondition
        str = char(unknownyclasses(1));
        cls = class(labels(unknownyclasses(1)));
        error(message('stats:classreg:learning:internal:classCount:UnknownClass', str, cls));
    end
else
    % C corresponds to non-missing labels
    C = classreg.learning.internal.classCount(classNames,Y);
end

end


function funloss = makeLossFunWithDefaultClass(funloss,prior,hascost)

if isempty(prior)
    return;
end

[~,nancls] = max(prior);

if isequal(funloss,@classreg.learning.loss.classiferror) || ...
        isequal(funloss,@classreg.learning.loss.mincost) || ...
        isequal(funloss,@classreg.learning.loss.classifcost)
    if hascost
        funloss = @(C,S,w,cost) funloss(C,S,w,cost,nancls);
    else
        funloss = @(C,S,w) funloss(C,S,w,nancls);
    end
end
end

function metricsfun = makeMetricsFunWithDefaultClass(metricsfun,prior,hascost)

if isempty(prior)
    return;
end

[~,nancls] = max(prior);

if isequal(metricsfun,@classreg.learning.loss.unnormalized.classiferror) || ...
        isequal(metricsfun,@classreg.learning.loss.unnormalized.mincost) || ...
        isequal(metricsfun,@classreg.learning.loss.unnormalized.classifcost)
    if hascost
        metricsfun = @(C,S,cost) metricsfun(C,S,cost,nancls);
    else
        metricsfun = @(C,S) metricsfun(C,S,nancls);
    end
end
end

function str = serializeHelper(this)
% serialize helper function
    str = struct;
    inputIsStruct = isstruct(this);
    str.Prior = this.Prior;
    str.pPriorisState = this.pPriorisState;
    str.PriorDefined = this.PriorDefined;
    str.PriorReset = this.PriorReset;
    str.pClassNames = this.pClassNames;
    str.pClassType = this.pClassType;
    str.pScoreTransform = this.pScoreTransform;
    if inputIsStruct
        if isfield(this,'MaxNumClasses')
            str.MaxNumClasses = this.MaxNumClasses;
        end
        if isfield(this,'pCost')
            str.Cost = this.pCost;
        end
        if isfield(this,'MaxNumClasses')
            str.MaxNumClasses = this.MaxNumClasses;
        end   
        if isfield(this,'IgnoreXNaNs')
            str.IgnoreXNaNs = this.IgnoreXNaNs;
        end
        if isfield(this,'WeightsPerClass')
            str.WeightsPerClass = this.WeightsPerClass;
        end
    else
        str.MaxNumClasses = this.MaxNumClasses;
        str.Cost = this.pCost;
        str.IgnoreXNaNs = this.IgnoreXNaNs;
        str.WeightsPerClass = this.WeightsPerClass;
    end  
end