classdef ClassificationNaiveBayes < ...
        classreg.learning.classif.FullClassificationModel & classreg.learning.classif.CompactClassificationNaiveBayes
    %ClassificationNaiveBayes Naive Bayes classification model.
    %   ClassificationNaiveBayes is a naive Bayes classification model. This model
    %   can predict responses for new data. This model also stores data used for
    %   training and can compute resubstitution predictions.
    %
    %   An object of this class cannot be created by calling the constructor.
    %   Use FITCNB to create a ClassificationNaiveBayes object by fitting a
    %   naive Bayes model to training data.
    %
    %   This class is derived from CompactClassificationNaiveBayes.
    %
    %   ClassificationNaiveBayes properties:
    %       Y                       - True class labels used to train this model.
    %       X                       - Matrix of predictors used to train this model.
    %       W                       - Weights of observations used to train this model.
    %       ModelParameters         - Input parameters to the fitting function.
    %       NumObservations         - Number of observations.
    %       PredictorNames          - Names of predictors used for this model.
    %       ExpandedPredictorNames  - Names of expanded predictors.
    %       CategoricalPredictors   - Indices of categorical predictors.
    %       ResponseName            - Name of the response variable.
    %       ClassNames              - Names of classes in Y.
    %       Prior                   - Prior class probabilities.
    %       Cost                    - Misclassification costs.
    %       ScoreTransform          - Transformation applied to predicted classification scores.
    %       DistributionNames       - The distributions used to model each predictor.
    %       DistributionParameters  - Estimated parameters for the individual distributions.
    %       CategoricalLevels       - The levels of predictors that use the 'mvmn' distribution.
    %       Kernel                  - The type of smoothing kernel for each kernel distribution.
    %       Support                 - The density support for each kernel distribution.
    %       Width                   - The width for each kernel distribution.
    %       RowsUsed                - Logical index for rows used in fit.
    %       HyperparameterOptimizationResults - An object or table describing the results of hyperparameter optimization.
    %
    %   ClassificationNaiveBayes methods:
    %       compact                 - Compact this model.
    %       compareHoldout          - Compare two models using test data.
    %       crossval                - Cross-validate this model.
    %       edge                    - Classification edge.
    %       incrementalLearner      - Convert naive Bayes classification model to incremental learner.
    %       loss                    - Classification loss.
    %       margin                  - Classification margins.
    %       partialDependence       - Partial Dependence values.
    %       plotPartialDependence   - Plot Partial Dependence.
    %       predict                 - Predicted response of this model.
    %       logp                    - Log probability of data according to this model.
    %       resubEdge               - Resubstitution classification edge.
    %       resubLoss               - Resubstitution classification loss.
    %       resubMargin             - Resubstitution classification margins.
    %       resubPredict            - Resubstitution predicted response.
    %
    %   Example: Train a naive Bayes model on Fisher iris data
    %       t = readtable('fisheriris.csv','format','%f%f%f%f%C');
    %       nb = fitcnb(t, 'Species');
    %
    %   See also fitcnb, classreg.learning.classif.CompactClassificationNaiveBayes.

    %   Copyright 2014-2023 The MathWorks, Inc.

    properties (GetAccess=protected, SetAccess=protected)
        % PrivW stores the original weights so that we don't lose them if
        % the user temporarily sets a prior component to 0.
        PrivW = [];
    end

    methods(Hidden)
        function this = ClassificationNaiveBayes (X,Y,W,modelParams,...
                                                  dataSummary,classSummary,scoreTransform,sumWeights)            
            this = this@classreg.learning.classif.FullClassificationModel(...
                X,Y,W,modelParams,dataSummary,classSummary,scoreTransform);

            this = this@classreg.learning.classif.CompactClassificationNaiveBayes(...
                dataSummary,classSummary,scoreTransform,[],[],[],[],[],[]);          


            % Set and expand DistributionNames. (We can't do it in the
            % compact constructor, even though it's called above.)
            this.DistributionNames = this.ModelParameters.DistributionNames;
            if ischar(this.DistributionNames) && ~strcmp('mn',this.DistributionNames)
                this.DistributionNames = repmat({this.DistributionNames},1,this.NumDims);
            end

            KernelDims = strcmp(this.DistributionNames,'kernel');

            % Standardize the data if requested. Note that this is done
            % before computing data-dependent default Cov & Scale parameters.
            if this.ModelParameters.StandardizeData
                % Standardize data. Uses this.W, which has been normalized by the base constructor.                
                this = standardize(this,KernelDims);
            end

            % Warn if we find any 'mvmn' predictors that weren't declared
            % categorical, and add them to the 'CategoricalPredictors'
            % property.
            mvmns = find(strcmp('mvmn',this.DistributionNames));
            if any(~ismember(mvmns, this.CategoricalPredictors))
                warning(message('stats:ClassificationNaiveBayes:ClassificationNaiveBayes:SomeMvmnNotCat'));
                this.DataSummary.CategoricalPredictors = union(this.CategoricalPredictors(:)', mvmns);
            end

            % Store W into PrivW:
            this.PrivW = W;
            this.SumWeights = sumWeights;

            % Find and store unique Categorical levels for use in training and prediction
            this.CategoricalLevels = classreg.learning.nbutils.findCategoricalLevels(this.DistributionNames, this.PrivX, 'sorted');

            % Train the model on this.X, this.Y and this.W, to get DistributionParameters
            if ~all(strcmp(this.DistributionNames,'mvmn')) && ~isfloat(this.PrivX) % no integers allowed
                internal.stats.checkSupportedNumeric('X',this.PrivX);
            end
            if strcmp(this.DistributionNames, 'mn')
                % 'mn' must be treated as a special case because it uses a
                % single distribution to model all predictor dimensions.
                [this,this.DistributionParameters] = fitMNDist(this);
            else
                this.DistributionParameters = fitNonMNDists(this);
            end
        end
    end

    methods
        function cmp = compact (this)
        %COMPACT Compact naive Bayes model.
        %   CMP=COMPACT(MODEL) returns an object of class
        %   CompactClassificationNaiveBayes holding the structure of
        %   the trained naive Bayes classifier. The compact object does
        %   not contain X and Y used for training.
        %
        %   See also fitcnb, ClassificationNaiveBayes,
        %   classreg.learning.classif.CompactClassificationNaiveBayes.
            dataSummary = this.DataSummary;
            dataSummary.RowsUsed = [];
            cmp = classreg.learning.classif.CompactClassificationNaiveBayes(...
                dataSummary, this.ClassSummary, this.PrivScoreTransform, this.PrivScoreType, ...
                this.DistributionNames, this.DistributionParameters, this.CategoricalLevels,this.Mu,this.Sigma);
        end
        
        function incrmdl = incrementalLearner(this,varargin)
        %INCREMENTALLEARNER Convert naive Bayes classification model to incremental learner.
        %   INCREMENTALMODEL=INCREMENTALLEARNER(MODEL) returns an incremental 
        %   classification naive Bayes model INCREMENTALMODEL initialized
        %   using the parameters provided in MODEL.
        %
        %   INCREMENTALLEARNER uses these properties of MODEL to initialize
        %   INCREMENTALMODEL:
        %       'ClassNames'
        %       'Cost'
        %       'DistributionNames'
        %       'DistributionParameters'  
        %       'NumPredictors'
        %       'Prior'
        %       'ScoreTransform'
        %       'Weights'
        %       'Y'                       - To compute per class weights
        %
        %   INCREMENTALMODEL=INCREMENTALLEARNER(MODEL,'PARAM1',val1,'PARAM2',val2,...)
        %   specifies optional parameter name/value arguments:
        %       'Metrics'                 - String vector representing built-in
        %                                   metrics, cell array of function handles
        %                                   or a struct of function handles specifying
        %                                   metrics that are tracked during incremental
        %                                   training. 'Metrics' can be a
        %                                   combination of strings, function
        %                                   handles or struct of function handles
        %                                   passed as a cell array.
        %                                   Built-in metrics for classification:
        %                                   'binodeviance', 'classiferror', 'exponential',
        %                                   'hinge', 'logit', and 'quadratic'.
        %                                   If you pass a function handle FUN, it
        %                                   is called as shown below:
        %                                       FUN(C,S)
        %                                   where C is an N-by-K logical matrix for N
        %                                   elements in Y and K classes in the ClassNames
        %                                   property, S is an N-by-K numeric matrix.
        %                                   C has one true per row for the true class.
        %                                   S is a matrix of predicted scores for classes
        %                                   with one row per observation, similar to score
        %                                   output from predict.
        %                                   Metric name will be extracted from provided function
        %                                   handle if it's not an anonymous
        %                                   function handle. If it is an anonymous
        %                                   function handle, Metric will be named
        %                                   as 'CustomMetric' and it will be post-fixed
        %                                   with an underscore and an integer
        %                                   identifying its relative position in
        %                                   the Metrics list.
        %                                   If you pass a struct, fieldnames will
        %                                   be treated as the metric names and
        %                                   values must be function handles.
        %                                   Incremental models track two values for
        %                                   each metric; a 'Cumulative' value and a
        %                                   'Window' value. 'MetricsWindowSize' determines
        %                                   the update frequency of
        %                                   'Window' metrics.
        %                                   Default: 'classiferror'.
        %       'MetricsWarmupPeriod'     - Nonnegative integer specifying the number of
        %                                   observations that will be used for
        %                                   training the model prior to metrics
        %                                   update. Default is 0.
        %       'MetricsWindowSize'       - Positive integer specifying the window
        %                                   size that is used for metrics
        %                                   update. Default value is 200.
        %
        %   Example: Initialize an incremental naive Bayes model using a traditionally trained model
        %   and continue incremental training.
        %
        %       ttModel = fitcnb(Xoffline,Yoffline);
        %       mdl = incrementalLearner(ttModel);
        %       mdl = updateMetricsAndFit(mdl,Xnew,Ynew,'Weights',Wnew);
        %
        %   See also incrementalClassificationNaiveBayes.
            if ~(isa(this,'classreg.learning.FullClassificationRegressionModel') && isa(this,'ClassificationNaiveBayes'))
                error(message('stats:incremental:Offline:NotFullModel'));
            end
            if istable(this.X) && ~isempty(this.CategoricalPredictors)
                error(message('stats:incremental:Offline:TableWithCatPreds'));
            end
            [args,additionalargs] = incremental.utils.offlineNaiveBayesParameters(this,true,varargin{:});
            [args,additionalargs] = incremental.utils.offlineClassificationParameters(this,args,additionalargs);
            mergedargs = incremental.utils.mergeOfflineParameters(args,additionalargs);
            incrmdl = incrementalClassificationNaiveBayes(mergedargs);
        end
    end

    methods(Static, Hidden)
        %% A static fit method
        function this = fit (X,Y,varargin)
            temp = classreg.learning.FitTemplate.make(...
                'NaiveBayes','type','classification',varargin{:});
            this = fit(temp,X,Y);
        end

        %% A template method
        function temp = template (varargin)
            temp = classreg.learning.FitTemplate.make(...,
            'NaiveBayes','type','classification',varargin{:});
        end
        function [X,Y,W,dataSummary,classSummary,scoreTransform,sumWeights] = ...
                prepareData(X,Y,varargin)
            [X,Y,W,dataSummary,classSummary,scoreTransform,sumWeights] = ...
                prepareData@classreg.learning.classif.FullClassificationModel(X,Y,varargin{:},'OrdinalIsCategorical',true);
        end
    end

    %% Model-specific methods:
    methods (Access=protected)
        % Set the Prior
        function this = setPrior (this, prior)
            this = setPrior@classreg.learning.classif.CompactClassificationNaiveBayes(this,prior);
            % make the weights sum to the prior within classes
            C = classreg.learning.internal.classCount(this.ClassSummary.ClassNames, this.PrivY);
            WC = bsxfun(@times,C,this.PrivW);
            Wj = sum(WC,1);
            gt0 = Wj>0;
            this.W = sum(bsxfun(@times,WC(:,gt0),this.Prior(gt0)./Wj(gt0)),2);
        end

        function [this,distParams] = fitMNDist (this)
        % Fit a 'mn' to each class. distParams{c,d} is P(d|c).
            classreg.learning.nbutils.checkMNData(this.PrivX);  % This is in the compact class because it needs to do the check during prediction, too.
            distParams = cell(this.NumClasses, this.NumDims);
            badRows = ~isfinite(sum(this.PrivX,2));
            if any(badRows)
                X = this.PrivX(~badRows,:);
                W = this.W(~badRows);
                Y = this.PrivY(~badRows);
            else
                X = this.PrivX;
                W = this.W;
                Y = this.PrivY;
            end
            ClassCount = classreg.learning.internal.classCount(this.ClassSummary.ClassNames, Y);
            WCC                = W.*ClassCount;
            f                  = (sum(ClassCount)./sum(WCC))';
            WCCX               = WCC'*X;                            % class x feature
            this.WeightedCounts = WCCX;                            % required for incremental learning
            posteriorWtdCounts = 1 + f.*WCCX;                       % add-1 smoothing
            posteriorProbs     = posteriorWtdCounts./sum(posteriorWtdCounts,2);
            for c = 1:this.NumClasses
                if this.NonzeroProbClasses(c)
                    distParams(c,:) = num2cell(posteriorProbs(c,:));    % This row is a vector of Multinomial probabilities.
                else
                    distParams(c,:) = {[]};
                end
            end
        end

        function distParams = fitNonMNDists (this)
        % Fit a non-'mn' distribution to each {class, dimension}
        % combination. distParams{c,d} will contain the parameters for
        % the distribution modeling P(X_d|c). An error is thrown if
        % there is not enough data for a [c,d] combination.
        % Check all (c,d) combinations for sufficient data
            classCount = classreg.learning.internal.classCount(this.ClassSummary.ClassNames, this.PrivY);

            if issparse(this.PrivX)&& all(strcmp(this.DistributionNames,'mvmn'))
                [NoDataCombos,distParams] = classreg.learning.nbutils.findNoDataCombosSparseMVMN(this.PrivX,...
                                                                  this.W,this.NumDims,this.NumClasses,classCount,this.CategoricalLevels,this.NonzeroProbClasses, sum(classCount));
                if any(NoDataCombos(:))
                    [row, col] = find(NoDataCombos);
                    errorStr = sprintf('\n');
                    errorStr = [errorStr,sprintf('  %14s   %14s\n','Class Name','Predictor Name')];
                    for n=1:numel(col)
                        errorStr = [errorStr,sprintf('  %14s   %14s\n',...
                                char(this.ClassSummary.ClassNames(row(n))), this.PredictorNames{col(n)} )];
                    end
                    error(message('stats:ClassificationNaiveBayes:ClassificationNaiveBayes:NoDataForUniFit',errorStr));
                end
            else
                count =0;
                errorStr = sprintf('\n');
                for i=1:this.NumClasses
                    if(isempty(this.X(classCount(:, i),:)))
                        noDataIdx =[];
                    else
                        noDataIdx = find(all(ismissing(this.X(classCount(:, i),:)),1));
                    end
                    if any(noDataIdx)
                        count =1;
                        errorStr = [errorStr,sprintf('  %14s   %14s\n','Class Name','Predictor Name')];
                        errorStr = [errorStr,sprintf('  %14s   %14s\n',...
                                char(this.ClassSummary.ClassNames(i)), this.PredictorNames{noDataIdx})];
                    end
                end
                if (count ==1)
                    error(message('stats:ClassificationNaiveBayes:ClassificationNaiveBayes:NoDataForUniFit',errorStr));
                end
                distParams = findNoDataCombos(this);
            end

        end

        function distParams = findNoDataCombos(this)
            ClassCount = classreg.learning.internal.classCount(this.ClassSummary.ClassNames, this.PrivY);
            distParams = cell(this.NumClasses, this.NumDims);
            cRows = ClassCount(:, 1:this.NumClasses);
            for c = 1:this.NumClasses
                x = this.PrivX(cRows(:,c),1:this.NumDims);
                w = this.W(cRows(:,c));
                if this.NonzeroProbClasses(c)
                    distributionNames = this.DistributionNames;
                    categoricalLevels = this.CategoricalLevels;
                    for d = 1:this.NumDims
                        % Get data for this class and dimension

                        wlocal = w;
                        xlocal = x(:,d);

                        % Remove NaNs
                        nanRows = isnan(xlocal);
                        if any(nanRows)
                            xlocal(nanRows) = [];
                            wlocal(nanRows) = [];
                        end
                        if strcmp(this.DistributionNames{d},'normal') && var(xlocal)==0
                            error(message('stats:ClassificationNaiveBayes:ClassificationNaiveBayes:ZeroVarianceForUniFit', ...
                                          char(this.ClassSummary.ClassNames(c)), this.PredictorNames{d}));
                        end
                        [Kernel, Support, Width] = getKernelInputParams(this, c, d);

                        distParams{c,d} = fitUnivariateDist(xlocal, wlocal, distributionNames{d}, ...
                                                            Kernel, Support, Width, categoricalLevels{d});
                    end
                else
                    distParams(c,:) = {[]};
                end
            end
        end

        function L = examplesInClass (this, Y, classNum)
        % Returns a logical array indicating which elements of Y are
        % instances of class number classNum.
            L = ismember(Y, this.ClassSummary.ClassNames(classNum));

        end

        function [Kernel, Support, Width] = getKernelInputParams (this, c, d)
        % Return kernel parameters for class c and dimension d
            kernelDims = this.KernelDims;
            if isempty(kernelDims) || ~kernelDims(d)
                Kernel = [];
                Support = [];
                Width = [];
            else
                MP = this.ModelParameters;
                % Kernel may be a string or cell
                if ischar(MP.Kernel)
                    Kernel = MP.Kernel;
                else
                    Kernel = MP.Kernel{d};
                end
                % Support may be a numeric vector, string, or cell.
                if isnumeric(MP.Support) || ischar(MP.Support)
                    Support = MP.Support;
                else
                    Support = MP.Support{d};
                end
                % Width may be a numeric scalar, vector or matrix.
                if isscalar(MP.Width)
                    Width = MP.Width;
                elseif size(MP.Width,1)==1
                    Width = MP.Width(d);
                elseif size(MP.Width,2)==1
                    Width = MP.Width(c);
                else
                    Width = MP.Width(c,d);
                end
            end
        end

        function this = standardize(this,KernelDims)
           NumFeatures = size(this.PrivX,2);
           isCat = ismember(1:NumFeatures,this.CategoricalPredictors);
           validDims = KernelDims & ~isCat;
           this.Mu = zeros(1,NumFeatures);
           this.Sigma = ones(1,NumFeatures);
           [this.PrivX(:,validDims), this.Mu(:,validDims), this.Sigma(:,validDims)] = classreg.learning.internal.wnanzscore(this.PrivX(:,validDims), this.W);
        end


        %% Properties to display
        function s = propsForDisp (this,s)
            s = propsForDisp@classreg.learning.classif.FullClassificationModel(this,s);
            s = propsForDisp@classreg.learning.classif.CompactClassificationNaiveBayes(this,s);
        end

    end

end

%% local functions

function distParams = fitUnivariateDist (x, w, distName, kernel, support, width, categoricalLevels)
% Fit a non-'mn' univariate distribution to column vector x, weighted
% by w.  Preconditions: (1) There is at least 1 data point. (2) x and w
% contain no NaNs. (3) w need not sum to 1.

    w = w/sum(w);
    % Do the fits
    switch distName     % 'kernel','mvmn','normal'
      case 'kernel'
        if isnan(width)
            width = [];
        end
        distParams = prob.KernelDistribution.fit(x, ...
                                                 'frequency',w*length(w), ...                            % Pass effective number of counts.
                                                 'kernel',kernel, 'support',support, 'width',width);     % distParams is a prob.KernelDistribution object.
      case 'mvmn'
        % Do weighted add-1 smoothing
        posteriorCounts = ones(length(categoricalLevels),1);                % init to the prior for add-1 smoothing.
        [~, levelIndices]= matlab.internal.math.ismemberhelper(x,categoricalLevels);   % Get mvmn Level indices for x. (Indices into categoricalLevels).
        if any(levelIndices)
            wCounts = accumarray(full(levelIndices), w, size(categoricalLevels))*length(levelIndices);% Get the effective number of counts for each level.
        else
            wCounts =0;
        end
        posteriorCounts = posteriorCounts + wCounts;
        distParams = posteriorCounts/sum(posteriorCounts);          % distParams is a vector of Multinomial probabilities.
      case 'normal'
        %             distParams = prob.NormalDistribution.fit(x, 'frequency',w); % CAN NOT USE. Rejects real weights.
        %             [m,s] = normfit(x,0.05,zeros(size(x)),w*length(x));         % CAN NOT USE. Gives different answer. Probably uses weights differently.
        mu = x'*w;
        sigma = sqrt(classreg.learning.internal.wnanvar(x,w,1));    % 1=Unbiased std estimate. (The NaN part is unnecessary.)
        distParams = [mu; sigma];                                   % distParams is [mean, std] of the gaussian fit.
    end
end

% LocalWords:  INCREMENTALLEARNER INCREMENTALMODEL Warmup tt Xoffline Yoffline Xnew Ynew Wnew
