classdef FeatureSelectionNCARegression < classreg.learning.fsutils.FeatureSelectionNCAModel
%FeatureSelectionNCARegression Feature selection for regression using neighborhood component analysis (NCA).
%   FeatureSelectionNCARegression learns feature weights using a diagonal
%   adaptation of neighborhood component analysis (NCA). Feature selection
%   is achieved by regularizing the feature weights.
%
%   FeatureSelectionNCARegression properties:
%       NumObservations       - Number of observations in the training data.
%       ModelParameters       - Model parameters passed in for training this model.
%       Lambda                - Regularization parameter.
%       FitMethod             - Method used to fit this model.
%       Solver                - Solver used to fit this model.
%       GradientTolerance     - Tolerance on gradient norm for solvers 'lbfgs' and 'minibatch-lbfgs'.
%       IterationLimit        - Maximum number of iterations for optimization.
%       PassLimit             - Maximum number of passes for solvers 'sgd' and 'minibatch-lbfgs'.
%       InitialLearningRate   - Initial learning rate for solvers 'sgd' and 'minibatch-lbfgs'.
%       Verbose               - Verbosity level: 0, 1 or >1.
%       InitialFeatureWeights - Initial feature weights to start optimization.
%       FeatureWeights        - Fitted feature weights for this model.
%       FitInfo               - Fitting information from training this model.
%       Mu                    - A vector of predictor means if using predictor standardization.
%       Sigma                 - A vector of predictor standard deviations if using predictor standardization.
%       X                     - Matrix of predictors used to train this model.
%       Y                     - Observed response used to train this model.
%       W                     - Weights of observations used to train this model.
%
%   FeatureSelectionNCARegression methods:
%       predict        - Predicted response using NCA regression model.
%       loss           - Compute loss on new data.
%       refit          - Refit this model.
%       selectFeatures - Select important features.
%
%   Example:
%       % 1. Example data where first 2 predictors are relevant.
%       N  = 300;
%       y  = unifrnd(0,20,N,1);
%       x1 = y.*sin(y) + randn(N,1);
%       x2 = y.*cos(y) + randn(N,1);
%       X  = [x1,x2];
%       % 2. Add irrelevant predictors and scale all predictors from 0 to 1.
%       Xrnd = randn(N,1000);
%       Xall = [X,Xrnd];
%       Xall = bsxfun(@rdivide,bsxfun(@minus,Xall,min(Xall)),range(Xall,1));
%       % 3. Use fsrnca to do feature selection using one value of Lambda.
%       nca = fsrnca(Xall,y,'Solver','lbfgs','Lambda',0.06,'Verbose',1,'LossFunction','mad','Standardize',1);
%       % 4. Plot feature weights.
%       semilogx(nca.FeatureWeights,'ro');
%       grid on;
%       xlabel('Feature index');
%       ylabel('Feature weight');
%
%   See also fsrnca, fscnca, FeatureSelectionNCAClassification.

%   Copyright 2015-2023 The MathWorks, Inc.

%% Constants.

    %%
    % _Supported loss functions for loss method_
    properties(Constant,Hidden)
        LossFunctionMSE = 'mse';
        LossFunctionMAD = 'mad';        
        BuiltInLossFunctions = {FeatureSelectionNCARegression.LossFunctionMSE,...
                                FeatureSelectionNCARegression.LossFunctionMAD};        
    end
    
%% Properties holding inputs.    
    properties(GetAccess=public,SetAccess=protected,Dependent)
        %Y - True response vector used to train this model.
        %   The Y property is a numeric vector of continuous response
        %   values used to train this model.
        Y;        
    end    
        
    properties(GetAccess=public,SetAccess=protected,Hidden,Dependent)
        %PrivY - True response vector used to train this model.
        %   The PrivY property is a numeric vector of continuous response
        %   values used to train this model.
        PrivY;        
    end
    
    properties(Hidden)
        %Impl - Implementation class for fitting and prediction.
        %   The Impl property is a FeatureSelectionNCARegressionImpl
        %   object.
        Impl;
    end
        
    methods
        function y = get.Y(this)
            y = this.PrivY;
        end
        
        function privY = get.PrivY(this)
            privY = this.Impl.PrivY;
        end
    end
    
%% Constructor.
    methods(Hidden)
        function this = FeatureSelectionNCARegression(X,Y,varargin)
            this = doFit(this,X,Y,varargin{:});            
        end
    end

%% predict and loss methods.    
    methods        
        function ypred = predict(this,XTest)
%predict - Make predictions on test data.
%   YPRED = PREDICT(MODEL,X) returns the predicted response YPRED for 
%   FeatureSelectionNCARegression object MODEL and predictors X. X must
%   be a table if MODEL was originally trained on a table, or a numeric 
%   matrix if MODEL was originally trained on a matrix. If X is a table, 
%   it must contain all the predictors used for training this model. 
%   If X is a matrix, it must have P columns, where P is the number 
%   of predictors used for training. YPRED is a numeric vector with M 
%   elements, where M is the number of rows in X.


            % 1. Validate XTest.
            isok = FeatureSelectionNCARegression.checkXType(XTest);
            if ~isok
                error(message('stats:classreg:learning:fsutils:FeatureSelectionNCAModel:BadXType'));
            end

            % Convert to matrix X if necessary
            if this.Impl.DataSummary.TableInput || istable(XTest)
                XTest = classreg.learning.internal.table2PredictMatrix(XTest,[],[],...
                    this.Impl.DataSummary.VariableRange,...
                    this.CategoricalPredictors,this.PredictorNames);
            end

            % Encode categorical predictors in matrix X if necessary
            if ~isempty(this.Impl.DataSummary.CategoricalPredictors) & ~this.Impl.DataSummary.TableInput
               XTest = classreg.learning.internal.encodeCategorical(XTest,this.Impl.DataSummary.VariableRange);
            end
            
            % 2. Get the size of XTest.
            [M,P] = size(XTest);
            
            % 3. Ensure that XTest has the right number of columns.
            if ( P ~= this.NumFeatures )
                error(message('stats:FeatureSelectionNCA:FeatureSelectionNCARegression:BadX',this.NumFeatures));
            end
            
            % 4. Bad rows in XTest - badrows is a M-by-1 logical vector.
            badrows = any(isnan(XTest),2);
            
            % 5. Remove bad rows from XTest.
            XTest(badrows,:) = [];
            
            % 6. If XTest is empty. we are done.
            if ( isempty(XTest) )
                error(message('stats:FeatureSelectionNCA:FeatureSelectionNCARegression:NoObservationsInX'));
            end
            
            % 7. Dispatch to the right predict method from the Impl class.
            % This method will account for predictor standardization if
            % required.
            ypred = nan(M,1);
            computationMode = this.ModelParams.ComputationMode;
            usemex          = strcmpi(computationMode,classreg.learning.fsutils.FeatureSelectionNCAModel.ComputationModeMex) && ~issparse(XTest);
            if usemex
                ypredNotBad = predictNCAMex(this.Impl,XTest);
            else
                ypredNotBad = predictNCA(this.Impl,XTest);            
            end
            ypred(~badrows) = ypredNotBad;
        end
        
        function L = loss(this,XTest,varargin)
%loss - Evaluate accuracy of learned feature weights on test data.
%
%   ERR = LOSS(MODEL,X,Y) returns the mean squared error for MODEL computed 
%   using learned feature weights, predictors X and observed response Y. 
%   X must be a table if MODEL was originally trained on a table, or a 
%   numeric matrix if MODEL was originally trained on a matrix. If X is
%   a table, it must contain all the predictors used for training this model.
%   If X is a matrix, it must have P columns, where P is the number of 
%   predictors used for training.Y must be a vector of floating-point 
%   numbers with N elements, where N is the number of rows in X. Y can be 
%   omitted if it appears in the table X.
%
%   ERR = loss(MODEL,X,Y,'PARAM1',val1,...) accepts additional name value
%   pairs and returns a different measure of accuracy of the learned
%   feature weights (smaller the better).
%
%       Parameter        Value
%       'LossFunction' - A character vector or string specifying the
%                        loss type. Choices are 'mse' and 'mad'. As
%                        described below, choice 'mse' returns L2 and 'mad'
%                        returns L1. Default is 'mse'.
%
%   X is an M-by-P matrix where M is the number of observations and P is
%   the number of predictors. Y is a numeric real vector with M elements.
%   Element i of Y is the true response for row i of X. L2 is computed as
%   follows:
%
%       L2 = mean((T-Y).^2)
%
%   where T is a M-by-1 vector containing the predicted response using NCA
%   regression model for rows of X. L1 is computed as follows:
%
%       L1 = mean(abs(T-Y))
            [varargin{:}] = convertStringsToChars(varargin{:});

            [YTest,varargin] = classreg.learning.internal.inferResponse(this.ResponseName,XTest,varargin{:});

            % 1. Figure out the loss type.
                % 1.1 Set parameter defaults.
                dfltLossFunction = FeatureSelectionNCARegression.LossFunctionMSE;
                % 1.2 Parse optional name/value pairs.
                paramNames = {  'LossFunction'};
                paramDflts = {dfltLossFunction};
                lossType   = internal.stats.parseArgs(paramNames,paramDflts,varargin{:});
                % 1.3 Validate optional parameters.
                lossType = internal.stats.getParamVal(lossType,this.BuiltInLossFunctions,'LossFunction');

            % 2. Validate XTest.
            isok = FeatureSelectionNCARegression.checkXType(XTest);
            if ~isok
                error(message('stats:classreg:learning:fsutils:FeatureSelectionNCAModel:BadXType'));
            end

            % Convert to matrix X if necessary
            if this.Impl.DataSummary.TableInput || istable(XTest)
                [XTest,YTest] = classreg.learning.internal.table2PredictMatrix(XTest,YTest,[],...
                    this.Impl.DataSummary.VariableRange,...
                    this.CategoricalPredictors,this.PredictorNames);
            end

            % Encode categorical predictors in matrix X if necessary
            if ~isempty(this.Impl.DataSummary.CategoricalPredictors) & ~this.Impl.DataSummary.TableInput
               XTest = classreg.learning.internal.encodeCategorical(XTest,this.Impl.DataSummary.VariableRange);
            end
            
            % 3. Get the size of XTest.
            [M,P] = size(XTest);
            
            % 4. Ensure that XTest has the right number of columns.
            if ( P ~= this.NumFeatures )
                error(message('stats:FeatureSelectionNCA:FeatureSelectionNCARegression:BadX',this.NumFeatures));
            end
            
            % 5. Validate YTest and convert it to a column vector.
            isok = FeatureSelectionNCARegression.checkYType(YTest);
            if ~isok
                error(message('stats:FeatureSelectionNCA:FeatureSelectionNCARegression:BadYType'));
            end
            YTest = YTest(:);
            
            % 6. Ensure that XTest and YTest have the same number of rows.
            if ( M ~= length(YTest) )
                error(message('stats:FeatureSelectionNCA:FeatureSelectionNCARegression:BadY',M));
            end
            
            % 7. Remove bad rows from XTest and YTest.
            [XTest,YTest] = FeatureSelectionNCARegression.removeBadRows(XTest,YTest,[]);            
            if ( isempty(XTest) || isempty(YTest) )
                error(message('stats:FeatureSelectionNCA:FeatureSelectionNCARegression:NoObservationsInXY'));
            end
            MNew = size(XTest,1);
            
            % 8. Get predicted values for rows of XTest.
            computationMode = this.ModelParams.ComputationMode;
            usemex          = strcmpi(computationMode,classreg.learning.fsutils.FeatureSelectionNCAModel.ComputationModeMex) && ~issparse(XTest);
            if usemex
                ypred = predictNCAMex(this.Impl,XTest);
            else
                ypred = predictNCA(this.Impl,XTest);
            end
                        
            % 9. Compute loss. 
            if ( strcmpi(lossType,FeatureSelectionNCARegression.LossFunctionMSE) )
                r = ypred - YTest;
                L = (r'*r)/MNew;
            else
                r = ypred - YTest;
                L = sum(abs(r))/MNew;
            end
        end        
    end
  
%% Object display.
    methods(Hidden)
        function s = propsForDisp(this,s)
        %propsForDisp - Create a structure with properties for display.
        %   s = propsForDisp(this,s) takes an object this of type
        %   FeatureSelectionNCARegression, and an optional struct s and
        %   adds fields to s for display purposes. s can be empty.
            
            % 1. Invoke super-class method.
            s = propsForDisp@classreg.learning.fsutils.FeatureSelectionNCAModel(this,s);
            
            % 2. Add additional properties specific to regression to s.
            s.Y = this.Y;
            s.W = this.W;
        end
    end
            
%% Utilities for setting up input data.    
    methods(Hidden)        
        function [X,Y,W,labels,labelsOrig,dataSummary,modelParamArgs] = setupXYW(~,X,Y,varargin)

            [X,Y,vrange,wastable,varargin] = classreg.learning.internal.table2FitMatrix(X,Y,varargin{:});

            % 1. Check X.
            X = FeatureSelectionNCARegression.validateX(X);

            % Extract observation weights.
            args = {'weights'};
            defs = {[]};
            [W,~,remainingArgs] = ...
                    internal.stats.parseArgs(args,defs,varargin{:},'VariableRange',vrange,'TableInput',wastable);
            
            % 2. Check Y.
            Y = FeatureSelectionNCARegression.validateY(Y);
            
            % 3. Check W.  
            if ( isempty(W) )
                W = ones(size(X,1),1);
            else
                W = FeatureSelectionNCARegression.validateW(W);    
            end
            
            % 4. Number of rows in X must match the length of Y and W.
            N = size(X,1);
            if ( length(Y) ~= N )
                error(message('stats:FeatureSelectionNCA:FeatureSelectionNCARegression:BadYLength'));
            end
            
            if ( length(W) ~= N )
                error(message('stats:FeatureSelectionNCA:FeatureSelectionNCARegression:BadWeights',N));
            end

            % 5. Populate DataSummary and encode categorical predictors
            %    in the case of matrix input
            args = {'predictornames' 'responsename' ...
                        'categoricalpredictors' 'variablerange' 'tableinput' 'ordinalpredictors'}; 
            defs = {       []               []             [] ...
                              []              []             []};

            [predictornames,responsename,catpreds,vrange,wastable,...
                        ordpreds,~,modelParamArgs] = ...
                        internal.stats.parseArgs(args,defs,remainingArgs{:});                         
           
            [X,Y,W,dataSummary] = classreg.learning.FullClassificationRegressionModel.prepareDataCR(X,Y,'PredictorNames',predictornames,...
                                             'ResponseName',responsename,'CategoricalPredictors',catpreds,'VariableRange',vrange,'TableInput',wastable,...
                                             'OrdinalPredictors',ordpreds,'Weights',W);

            if ~dataSummary.TableInput
                    X = classreg.learning.internal.encodeCategorical(X,dataSummary.VariableRange);
            end
            
            % 6. Remove unusable rows from X, Y and W.
            [X,Y,W] = FeatureSelectionNCARegression.removeBadRows(X,Y,W);
            
            % 7. If X, Y or W become empty after removing bad rows, stop.
            if ( isempty(X) || isempty(Y) || isempty(W) )
                error(message('stats:FeatureSelectionNCA:FeatureSelectionNCARegression:NoObservationsInXY'));
            end
            
            % 8. Return X, Y and W. For regression, labels and labelsOrig
            % are set to [].
            labels     = [];
            labelsOrig = [];
        end
    end
    
%% Utilities for input validation.    
    methods(Static,Hidden) 

        function this = loadobj(obj)
            % custom load method for backwards compatibility
            if isempty(obj.Impl.DataSummary)
                % populate fields CategoricalPredictors,
                % ExpandedPredictorsNames, PredictorNames
                dataSummary = struct;
                dataSummary.PredictorNames = obj.NumFeatures;
                dataSummary.CategoricalPredictors = [];
                dataSummary.OrdinalPredictors = false(1,obj.NumFeatures);
                dataSummary.TableInput = false;
                dataSummary.VariableRange = cell(1,obj.NumFeatures);
                dataSummary.ResponseName = 'Y';
                obj.Impl.DataSummary = dataSummary;
                this = obj;
            else
                this = obj;
            end
        end

        function Y = validateY(Y)
        %validateY - Validate the Y input.
        %   Y = validateY(Y) ensures that Y is a numeric, real vector. If
        %   not an error is thrown. Y is returned as a column vector.
        
            % 1. Check the type of Y.
            isok = FeatureSelectionNCARegression.checkYType(Y);            
            if ~isok
                error(message('stats:FeatureSelectionNCA:FeatureSelectionNCARegression:BadYType'));
            end
            
            % 2. Ensure that Y is a column vector.
            Y = Y(:);
        end
        
        function isok = checkYType(Y)
            % Y must be a numeric, real vector.
            isok = isfloat(Y) && isreal(Y) && isvector(Y);
        end        
    end
end

% LocalWords:  xall
