classdef FeatureTransformer
    % FeatureTransformer Learned feature transformations.
    %     FeatureTransformer contains information required to apply learned
    %     transformations to new data. Use GENCFEATURES/GENRFEATURES to 
    %     automatically generate engineered features that might improve the 
    %     accuracy or interpretability of a model.
    %  
    %     FeatureTransformer properties:
    %  
    %        Type                    -  'classification' or 'regression'
    %        TargetLearner           -  Target learner for which features are
    %                                    engineered,  specified as 'linear',
    %                                   'bag', or 'gaussian-svm'.
    %        NumEngineeredFeatures   -  Number of engineered features
    %                                   available in the transformer.
    %        NumOriginalFeatures     -  Number of features available in the
    %                                   transformer that are taken directly 
    %                                   from the table originally passed to 
    %                                   GENCFEATURES/GENRFEATURES. Original 
    %                                   features include those cast to a new 
    %                                   data type.  
    %        TotalNumFeatures        -  Total number of features available in
    %                                   the transformer. TotalNumFeatures is the 
    %                                   sum of NumEngineeredFeatures and 
    %                                   NumOriginalFeatures.
    %                                   
    %  
    %     FeatureTransformer methods:
    %  
    %        transform - Transform new data.
    %        describe - Describe engineered features.
    
%   Copyright 2020-2021 The MathWorks, Inc.
    
    
    properties(GetAccess=public,SetAccess=protected)
        Type;
        TargetLearner;
    end
    
    properties (Dependent)
        NumEngineeredFeatures;
        NumOriginalFeatures;
        TotalNumFeatures; 
    end
    
    properties(Hidden)
        Program  = [];
        NumericFeatures = [];
        OriginalFeatures = [];
        ResponseName = [];
        AlgorithmInfo = [];
    end
    
    methods
        function v = get.NumEngineeredFeatures(this)
            v = sum(~this.OriginalFeatures);
        end
        function v = get.NumOriginalFeatures(this)
            v = sum(this.OriginalFeatures);
        end
        function v = get.TotalNumFeatures(this)
            v = numel(this.OriginalFeatures);
        end
    end    
    
    methods (Hidden)
        function this = FeatureTransformer(program,isNumeric,type,targetLearner,responseName,algInfo)
            
            this.Program                  = program;
            this.NumericFeatures          = isNumeric;
            this.Type                     = type;
            this.TargetLearner            = targetLearner;
            this.ResponseName             = responseName;
            this.AlgorithmInfo            = algInfo;
            
            % Find which variables are from the original input, if the user
            % requested for standardized, the standardized original inputs
            % are considered to be original predictors. 
            ov = this.Program{end,1}.OutputVarNames;
            i = ~cellfun(@isempty,regexp(ov,'^[dc]\((.+)\)$','once'));
            ov(i) = regexprep(regexprep(ov(i),'^[cd]\(',''),'\)$','');
            switch algInfo.Standarization
                case 'zscore'
                    i = ~cellfun(@isempty,regexp(ov,'^zsc\((.+)\)$','once'));
                    ov(i) = regexprep(regexprep(ov(i),'^zsc\(',''),'\)$','');
                case 'mad'
                    i = ~cellfun(@isempty,regexp(ov,'^mad\((.+)\)$','once'));
                    ov(i) = regexprep(regexprep(ov(i),'^mad\(',''),'\)$','');
                case 'range'
                    i = ~cellfun(@isempty,regexp(ov,'^range\((.+)\)$','once'));
                    ov(i) = regexprep(regexprep(ov(i),'^range\(',''),'\)$','');
            end
            this.OriginalFeatures = ismember(ov,this.Program{1,1}.InputVarNames);
        end
    end
    
    methods
        
        function T = transform(this,Tin,idx)
            %             transform applies the engineered feature transformations to new data.
            %  
            %     TBL_TRAN = transform(OBJ,TBL) returns a table with transformed
            %     features as prescribed by OBJ. TBL must contain the required 
            %     input variables, whose data types must match those of the 
            %     variables originally passed to GENCFEATURES or GENRFEATURES when OBJ was created.
            %  
            %     TBL_TRAN = transform(OBJ,TBL,IND) returns a subset of the
            %     transformed features. IND can be a numeric or logical
            %     vector indicating the position of the features to return, 
            %     or a string array or cell array of character vectors indicating
            %     the names of the features to return.


            
            if nargin>2
                idx = validateIndex(this,idx);
                fe = cleanInputDependencies(this,idx(:)');
                T = transform(fe,Tin);
                [~,~,j] = unique(idx);
                T = T(:,j);
                return
            end
            
            if isempty(this.Program)
                T = Tin(:,[]);
                T.Properties.VariableDescriptions = cell(1,0);
            else
                % Initialize a cell array to hold partial transformations
                nodes = cell(this.Program{end,3},1);
                nodes{1} = Tin;
                
                nt = size(this.Program,1);
                for i = 1:nt
                    sourceNode = this.Program{i,2};
                    if isscalar(sourceNode)
                        nodes{this.Program{i,3}} = this.Program{i,1}.apply(nodes{sourceNode});
                    else
                        nodes{this.Program{i,3}} = this.Program{i,1}.apply(horzcat(nodes{sourceNode}));
                    end
                end
                
                T = nodes{end};
            end
            
            if ~isempty(this.ResponseName) && ismember(this.ResponseName,Tin.Properties.VariableNames)
                T = [T Tin(:,this.ResponseName)];
            end
     
        end
        
        function Tout = describe(this,idx)
            % DESCRIBE provides a description of the engineered features.
            %
            %   DESCRIBE(TRA) prints the description of the engineered
            %   features that the transformer TRA produces.
            %
            %   DESCRIBE(TRA,IND) prints the description of the IND 
            %   features. IND can be a numeric or logical vector indicating 
            %   the position of the features to describe, or a string array or
            %   cell array of character vectors indicating the names of the 
            %   features to describe.
            %
            %   T = DESCRIBE(...) returns the descriptions in a table. Row
            %   names correspond to the names of the features.
          
            if nargin<2 || isempty(idx)
                idx = 1:this.TotalNumFeatures;
            else
                idx = validateIndex(this,idx,true);
            end
            
            [varIn,details] = makeDetails(this);

            T = table(...
                categorical(this.NumericFeatures,[true false],{'Numeric','Categorical'})',...
                this.OriginalFeatures',...
                categorical(varIn),details,...
                'VariableNames',{'Type','IsOriginal','InputVariables','Transformations'});
            T.Properties.RowNames = string(this.Program{end,1}.OutputVarNames');
            if nargin>1
                idx = validateIndex(this,idx,true);
                T = T(idx,:);
            end
            
            if nargout>0
                transfStr = cell(size(T,1),1);
                for i = 1:size(T,1)
                    t = T.Transformations{i};
                    if numel(t) == 0
                        transfStr{i} = '';
                    else
                        transfStr{i} = t{1};
                        for j = 2:numel(t)
                            transfStr{i} = sprintf('%s -> %s',transfStr{i},t{j});
                        end
                    end
                end
                Tout = T;
                Tout.Transformations = string(transfStr);
                return
            end
            
            str = evalc('disp(T)');
            
            requiredWidth = max(cellfun(@length,[T.Transformations{:}]));
            tabToTranColumn = regexp(regexprep(str,'<[^>]+>',''),'Transformations');
            if requiredWidth>15
                str = regexprep(str,'<strong>Transformations</strong>',[blanks(floor((requiredWidth-15)./2)) '<strong>Transformations</strong>' ]);
                str = regexprep(str,['>' repmat('_',1,15) '<'] ,['>' repmat('_',1,requiredWidth) '<']);
            end
            
            for i = 1:size(T,1)
                    t = T.Transformations{i};
                    if numel(t) == 0
                        str = regexprep(str,'\s\s{\d+.\d+\scell}[^\n]*\n','\n','once');
                    else
                        s = [t{1} '\n'];
                        for j = 2:numel(t)
                            s = sprintf('%s%s%s\n',s,blanks(tabToTranColumn-1),t{j});
                        end
                        str = regexprep(str,'\s\s{\d+.\d+\scell}[^\n]*\n',s,'once'); 
                    end
            end          
            disp(str);
        end
    end
    
    methods (Access=protected)
        function idx = validateIndex(this,idx,allowDups)
            if nargin<3
                allowDups = false;
            end
            if isempty(this.Program)
                ov = {};
            else
                ov = this.Program{end,1}.OutputVarNames;
            end
            if islogical(idx)
                if numel(idx)~=numel(ov)
                    error(message('stats:featlearn:FeatureTransformer:InvalidIndex'))
                end
                idx = find(idx);
            elseif isnumeric(idx)
                try
                    validateattributes(idx,{'numeric'},{'vector','<=',numel(ov),'>',0,'integer'})
                catch
                    error(message('stats:featlearn:FeatureTransformer:InvalidIndex'))
                end
                idx = idx(:);
            elseif isstring(idx) || iscellstr(idx) || ischar(idx)
                [h,idx] = ismember(idx,ov);
                if any(~h)
                    error(message('stats:featlearn:FeatureTransformer:InvalidIndex'))
                end
            else
                error(message('stats:featlearn:FeatureTransformer:InvalidIndex'))
            end
            if ~allowDups
                if numel(idx) ~= numel(unique(idx))
                    error(message('stats:featlearn:FeatureTransformer:InvalidIndex'))
                end
            end
        end
    end
    
    methods (Hidden)
        
    
        function  [inv,det] = makeDetails(this,idx)
            % MAKEDETAILS collect the details of each engineered feature.
            %
            %    [INPUTVAR,DETAILS] = MAKEDETAILS(OBJ) returns the input variables
            %    and the details of the transformations required for each
            %    engineered feature.           
            if nargin<2 || isempty(idx)
                idx = 1:this.TotalNumFeatures;
            else
                idx = validateIndex(this,idx,true);
            end
            det = cell(numel(idx),1);
            inv = cell(numel(idx),1);
            for i = 1:numel(idx)
                t =  cleanInputDependencies(this,idx(i));
                p = t.Program;
                switch numel(p{1,1}.InputVarNames)
                    case 1                        
                        inv{i} = sprintf('%s',p{1,1}.InputVarNames{1});                        
                    case 2
                        inv{i} = sprintf('%s, %s',p{1,1}.InputVarNames{1},p{1,1}.InputVarNames{2});
                    otherwise
                        inv{i} = sprintf('all valid numeric variables');
                end
                tras = cell(0);
                k = 1;
                for j = 1:size(p,1)
                    s = details(p{j,1});
                    if  strlength(s(1))>0
                        if numel(s) == 1
                            tras{k} = sprintf('%s',s);
                            k = k+1;
                        else
                            tras{k} = sprintf('%s (%s)',s(1),s(2));
                            k = k+1;
                        end
                    end
                end
                det{i} = tras;
            end
        end
                
        function this = cleanInputDependencies(this,idx)
            % CLEANINPUTDEPENDECIES clean input dependecies.
            %
            %    OBJ = CLEANINPUTDEPENDECIES(OBJ) walk back the internal
            %    program to remove unecessary transfomations and input dependecies.
            
            if nargin<2
                idx = 1:numel(this.Program{end,1}.OutputVarNames);
            end
            % Initialize a cell array to hold partial node dependecies
            nodes = cell(this.Program{end,3},1);
            nodes{end} = this.Program{end,1}.OutputVarNames(idx);
            % walk back to clean individual transformers
            nt = size(this.Program,1);
            for i = nt:-1:1
                dest = this.Program{i,3};
                nodes{dest} = intersect(nodes{dest},this.Program{i,1}.OutputVarNames);
                tr = setdiff(this.Program{i,1}.OutputVarNames,nodes{dest});
                this.Program{i,1} = removeVars(this.Program{i,1},tr);
                srcs = this.Program{i,2};
                for j = 1:numel(srcs)
                    nodes{srcs(j)} = [nodes{srcs(j)},this.Program{i,1}.InputVarNames];
                end
            end
            % remove empty transformers
            this.Program(ismember(cell2mat(this.Program(:,3)),find(cellfun(@isempty,nodes))),:)=[];
            % remove now invalid src node pointers
            valid = [1;cell2mat(this.Program(:,3))];
            for i =1:size(this.Program,1)
                this.Program{i,2} = this.Program{i,2}(ismember(this.Program{i,2},valid));
            end
            % remapping node pointers
            map = unique(cell2mat(this.Program(size(this.Program,1)+1:end)'));
            rmap(map) = 1:numel(map);
            rmap = rmap(:);
            for i =1:size(this.Program,1)
                this.Program{i,2} = rmap(this.Program{i,2});
                this.Program{i,3} = rmap(this.Program{i,3});
            end
            this.OriginalFeatures = this.OriginalFeatures(sort(idx));
            this.NumericFeatures = this.NumericFeatures(sort(idx));
            
        end
    end
end