classdef RegressionModel < incremental.SimpleLearner
    %REGRESSIONMODEL Incremental regression model.
    %   RegressionModel is the super class for incremental
    %   regression models.
    
    %   Copyright 2020-2022 The MathWorks, Inc.
    
    properties(Access = private)
        pResponseTransform
    end
    
    properties(Constant,Access = protected)
        DefaultPerObservationLoss = 'squarederror'
        DefaultLoss = 'mse'
        pAllowedRegrMetrics = {'mse'}
        pAllowedRegrPerObservationLoss = {'squarederror'}
    end
    
    properties(SetAccess = protected,Dependent)
        ResponseTransform
    end
    
    methods(Abstract,Access = protected)
        response(this,varargin)
    end
    
    methods
        function st = get.ResponseTransform(this)
            st = classreg.learning.internal.convertScoreTransform(...
                this.pResponseTransform,'string',[]);
        end
        
        function yfit = predict(this,X,varargin)
            %PREDICT Predict responses of the model.
            %   YFIT=PREDICT(MODEL,X) returns predicted responses for model
            %   MODEL and predictors X. Pass X as a matrix with N observations 
            %   from P predictors. YFIT is a vector with N elements, where N is
            %   the number of rows in X.
            %
            %   YFIT=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 ~ObservationsInSupported(this)
                narginchk(2,2);
            else
                narginchk(2,4);
            end
            obsInRows = this.parseObservationsInInput(varargin{:});
            yfit = predictWithoutValidation(this,X,obsInRows);
        end
        
    end
    methods (Access = protected)
        
        function this = RegressionModel(varargin)
            this = this@incremental.SimpleLearner(varargin{:});
        end
        
        function this = setResponseTransform(this,rt)
            rt = convertStringsToChars(rt);
            this.pResponseTransform = ...
                classreg.learning.internal.convertScoreTransform(rt,...
                'handle',1);
        end
        
        function this = resetModel(this,varargin)
            return
        end
        
        function [regrArgs,remainingArgs] = parseDataParameters(~,params)
            params = incremental.utils.convertStructToCell(params);
            p = inputParser;
            p.StructExpand = true;
            p.KeepUnmatched = true;
            addParameter(p,'ResponseTransform','identity');
            parse(p,params{:});
            regrArgs = p.Results;
            remainingArgs = p.Unmatched;
        end
        
        function this = fillDataParameters(this,~,regrParams)
            this = setResponseTransform(this,regrParams.ResponseTransform);
        end
        
         function [this,Xraw,Yraw,Wraw] = prepareRawDataAndUpdateState(this,Xraw,Yraw,Wraw,obsInRows)
            % prepare data
            removeNaNWX = true;
            % clean nan X and W for training
            [Xraw,Yraw,Wraw] = prepareData(this,Xraw,Yraw,Wraw,obsInRows,removeNaNWX);
         end     
        
        function [this,X,Y,W,WisRaw] = prepareDataAndUpdateState(this,Xraw,Yraw,Wraw,obsInRows,~)
            % update and prepare data and weights 
            [this,X,Y,W] = prepareRawDataAndUpdateState(this,Xraw,Yraw,Wraw,obsInRows);
            WisRaw = true;
        end


        function [X,Y,W,indsToExclude] = prepareData(~,X,Y,W,obsInRows,removeNaNWX)
            % Extract indices to be excluded from Y. These indices will be
            % modified using nan values from X and W.

            % W will be [] for perObservationLoss
            ignoreW = isequal(W,[]);

            % check if Y is a double/single column vector if Y is not
            % empty. Y can be NaN. 
            if ~isempty(Y)
                if ~isvector(Y) || ~iscolumn(Y) || ~isfloat(Y)
                    error(message('stats:incremental:regr:RegressionModel:InvalidResponse'));
                end
            end
            indsToExclude = ismissing(Y);                 
            if removeNaNWX
                if obsInRows
                    inds = any(isnan(X),2);
                else
                    inds = any(isnan(X));
                end
                inds = inds(:);
                if ~ignoreW
                    inds = inds | W == 0 | isnan(W);
                end
                indsToExclude = inds | indsToExclude;
            end
            if obsInRows
                X = X(~indsToExclude,:);
            else
                X = X(:,~indsToExclude);
            end
            if ~ignoreW
                W = W(~indsToExclude,:);
            end
            Y = Y(~indsToExclude,:);
        end        


        function [X,Y,W,indsToExclude] = prepareDataForLoss(this,X,Y,W,obsInRows)
            % prepare data for loss. Only nan Y values are used to exclude
            % entries in X, Y and W.
            removeNaNWX = false;
            [X,Y,W,indsToExclude] = prepareData(this,X,Y,W,obsInRows,removeNaNWX);
           
        end
        
        function [windowMetrics,Wnormalized] = adjustWeightAndCalculateMetrics(this,X,Y,W,obsInRows,metricsFun)
            
            [Y,Wnormalized,YFit] = regrLossForMetrics(this,X,Y,W,obsInRows);
            windowMetrics = cellfun(@(c) c(Y,YFit),metricsFun,'UniformOutput',false);
        end
        
        function l = modelLoss(this,X,Y,W,obsInRows,lossFun,~)
            
            funloss = classreg.learning.internal.lossCheck(lossFun,'regression');
            [Y,Wnormalized,YFit] = regrLoss(this,X,Y,W,obsInRows);
            l = funloss(Y,YFit,Wnormalized);
        end


        function l = modelPerObservationLoss(this,X,Y,obsInRows,lossFun)
            % Model perObservationLoss for incremental regression models.
            funloss = classreg.learning.internal.lossCheck(lossFun,'regression',true);
            [X,Y,~,indsToExclude] = prepareDataForLoss(this,X,Y,[],obsInRows);
            yfitreduced = predictWithoutValidation(this,X,obsInRows);
            lreduced = funloss(Y,yfitreduced);
            l = nan(size(indsToExclude),'like',lreduced);
            l(~indsToExclude,:) = lreduced;

        end
        
        function metrics = allowedMetrics(this)
            metrics = this.pAllowedRegrMetrics;
        end

        function allowedPerObservationLosses = allowedPerObservationLosses(this)
            % allowed per observations losses for incremental regression models
            allowedPerObservationLosses = this.pAllowedRegrPerObservationLoss;
        end        
        
        function [metricsnames,metricshandles] = parseMetrics(~,metricsnames,metrics)
            n = numel(metrics);
            metricshandles = cell(n,1);
            for ii = 1 : n
                if ischar(metrics{ii})
                    metricshandles{ii} = classreg.learning.internal.lossCheck(metrics{ii},'regression',true);
                    metricsnames{ii} = incremental.internal.mapToMetricName(metricsnames{ii});
                else
                    metricshandles{ii} = classreg.learning.internal.lossCheck(metrics{ii},'regression',true);
                end
            end
        end
        
        function defaultmetric = defaultMetric(this)
            % Default metric for the incremental regression model
            defaultmetric = this.DefaultLoss;
        end
        
        function defaultloss = defaultLoss(this)
            % Default loss for the incremental regression model
            defaultloss = this.DefaultLoss;
        end

        function defaultloss = defaultPerObservationLoss(this)
            % Default loss for the incremental regression model
            defaultloss = this.DefaultPerObservationLoss;
        end
        
        function propList = propertiesForDisplay(this,propList)
            propList.ResponseTransform = this.ResponseTransform;
        end
    end
    methods (Access = private)
        function yfit = predictWithoutValidation(this,X,obsInRows)
            yfit = this.pResponseTransform(response(this,X,obsInRows));           
        end

        function [Y,Wnormalized,Yfit] = regrLossForMetrics(this,X,Y,W,obsInRows)
            % Return per observation predictions and normalized weights for
            % metrics calculation
            
            % Remove all NaN values from X,Y and W for metrics calculation
            [X,Y,W] = prepareDataForLoss(this,X,Y,W,obsInRows);
            Yfit = predictWithoutValidation(this,X,obsInRows);  
            %Yfit can have nan values. Remove nan values for metrics
            %update.
            indsToExclude = all(isnan(Yfit),2) | isnan(W);
            Yfit = Yfit(~indsToExclude,:);
            W = W(~indsToExclude,:);
            Y = Y(~indsToExclude,:);  
            %W only has non-nan values. Normalize W.
            Wnormalized = this.normalizeWeights(W);
            % Wnormalized should not have any 0 values or nan values. Such
            % weights are filtered out for metrics calculations.
            % Inf values are not filtered out.              
            indsToExclude = Wnormalized == 0 | isnan(Wnormalized);
            Yfit = Yfit(~indsToExclude,:);
            Wnormalized = Wnormalized(~indsToExclude,:);
            Y = Y(~indsToExclude,:); 
            classreg.learning.internal.regrCheck(Y,Yfit,Wnormalized);            
        end          
        
        function [Y,Wnormalized,Yfit] = regrLoss(this,X,Y,W,obsInRows)
            % Return per observation predictions and weights

            % All X, Y and W entries corresponding to nan values are
            % removed.
            [X,Y,W] = prepareDataForLoss(this,X,Y,W,obsInRows);
            %Yfit can have nan values
            Yfit = predictWithoutValidation(this,X,obsInRows); 
            %W can have nan values.
            Wnormalized = this.normalizeWeights(W);
            classreg.learning.internal.regrCheck(Y,Yfit,Wnormalized);            
        end

    end
    methods (Hidden) 
        function this = deserialize(this,deserializeableStr)
            % construct incremental regression model from a given deserializable struct
            this = deserialize@incremental.SimpleLearner(this,deserializeableStr);
            assert(isfield(deserializeableStr,'RegressionParameters'),...
                                                  'Deserializable regression struct must have ''RegressionParameters'' field.');
            deserializeableStr = deserializeableStr.RegressionParameters;            
            this.pResponseTransform = deserializeableStr.pResponseTransform;
        end
    end
    
    methods (Hidden) %methods used for code generation
        function [tunableStr, nontunableStr] = toStruct(this,tunableStr,nontunableStr)
            [tunableStr,nontunableStr] = toStruct@incremental.SimpleLearner(this,tunableStr,nontunableStr);
            % extract the name of the transform
            nontunableStr.ResponseTransform = func2str(this.pResponseTransform);
            isincremental = true;
            doclass = false;            
            nontunableStr = classreg.learning.coderutils.transformToCoderStruct(nontunableStr,doclass,this.pResponseTransform,1,isincremental);
        end  
    end
    
    methods (Static, Hidden) %static methods used for code generation
        function params = mergeRegrStructs(tunableStr,nontunableStr)
            % merge necessary parameters from tunable and nontunable
            % structs
            params = incremental.SimpleLearner.mergeLearnerStructs(tunableStr,nontunableStr,false);

            if nontunableStr.CustomResponseTransform
                rt = str2func(nontunableStr.ResponseTransformFull);
            pResponseTransform = classreg.learning.internal.convertScoreTransform(rt,...
                'handle',1);                
            else
                rt = lower(char(nontunableStr.ResponseTransform));
                pResponseTransform = classreg.learning.internal.convertScoreTransform(rt,...
                    'handle',1);                  
            end
            params.RegressionParameters.pResponseTransform = pResponseTransform;
        end
      
       % Backwards Compatibility function
        function str = serialize(this,str)
            % Convert incremental regression object to deserializeable
            % struct. 
            str.RegressionParameters = serializeHelper(this);
        end       
    end   
end

function str = serializeHelper(this)
    str = struct;
    str.pResponseTransform = this.pResponseTransform;
end