--- a
+++ b/exseek/scripts/feature_selectors.py
@@ -0,0 +1,706 @@
+from sklearn.base import BaseEstimator, ClassifierMixin, TransformerMixin, MetaEstimatorMixin, is_classifier
+from sklearn.base import clone
+from sklearn.feature_selection.base import SelectorMixin
+from sklearn.model_selection import GridSearchCV, check_cv
+from sklearn.feature_selection import RFE, RFECV, SelectFromModel
+from sklearn.neural_network import MLPClassifier
+import numpy as np
+import pandas as pd
+from sklearn.utils.validation import check_is_fitted
+from sklearn.utils import check_X_y
+from tqdm import tqdm
+import os
+import subprocess
+import shutil
+from copy import deepcopy
+import logging
+from utils import search_dict, get_feature_importances, python_args_to_r_args
+logging.basicConfig(level=logging.INFO, format='[%(asctime)s] [%(levelname)s] %(name)s: %(message)s')
+logger = logging.getLogger(__name__)
+
+def get_feature_ranking(feature_importances):
+    '''Calculate ranking from feature importances
+    Feature importances are sorted in descendig order and then converted to ranks
+    Smaller values indicate higher importance
+
+    Parameters
+    ----------
+    arrays: list of array-like objects
+        feature importances
+    
+    Returns
+    -------
+    arrays: array-like
+        Feature ranking
+    '''
+    ranking = np.zeros(len(feature_importances), dtype='int')
+    ranking[np.argsort(-feature_importances)] = np.arange(len(feature_importances))
+    return ranking
+
+class MaxFeaturesSelector(BaseEstimator, SelectorMixin):
+    '''Select given number of features from model
+
+    Parameters:
+    ----------
+
+    estimator: object
+        A classifier that provides feature importances through feature_importances_ or coef_ attribute after calling the fit method.
+    
+    grid_search: bool
+        Whether to optimize hyper-parameters of the estimator by grid search
+    
+    grid_search_params: dict
+        Parameters passed to GridSearchCV
+    
+    n_features_to_select: int
+        Maximum number of features to select
+    
+    Attributes:
+    ----------
+
+    support_: array-like, shape (n_features,)
+        Boolean mask indicating features selected
+    
+    feature_importances_: array-like, shape (n_features,)
+        Average feature importances across resampling runs
+    '''
+    def __init__(self, estimator, n_features_to_select=10, 
+        grid_search=False, grid_search_params=None):
+        self.estimator = estimator
+        self.n_features_to_select = n_features_to_select
+        self.grid_search = grid_search
+        self.grid_search_params = grid_search_params
+    
+    def fit(self, X, y, sample_weight=None):
+        if self.grid_search is not None:
+            grid_search = GridSearchCV(self.estimator,
+                **self.grid_search_params)
+            grid_search.fit(X, y, sample_weight=sample_weight)
+            self.estimator_ = grid_search.best_estimator_
+            self.best_classifier_params_ = grid_search.best_params_
+            self.estimator_.set_params(**self.best_classifier_params_)
+        self.estimator_.fit(X, y, sample_weight=sample_weight)
+        self.feature_importances_ = get_feature_importances(self.estimator)
+        self.support_ = np.zeros(X.shape[1], dtype='bool')
+        self.support_[np.argsort(-self.feature_importances_)][:self.n_features_to_select] = True
+    
+    def _get_support_mask(self):
+        check_is_fitted(self, 'support_')
+        return self.support_
+    
+class RobustSelector(BaseEstimator, SelectorMixin):
+    '''Feature selection based on recurrence
+
+    Parameters:
+    ----------
+
+    estimator: object
+        A classifier that provides feature importances through feature_importances_ or coef_ attribute after calling the fit method.
+    
+    cv: int or splitter object
+        Specifies how to subsample the original dataset
+    
+    n_features_to_select: int
+        Maximum number of features to select
+    
+    Attributes:
+    ----------
+    
+    support_: array-like, shape (n_features,)
+        Boolean mask indicating features selected
+    
+    ranking_: array-like, shape (n_features,)
+        Ranking of feature importances starting from 0 to n_features - 1.
+        Smaller ranks indicates higher importance.
+    
+    feature_recurrence_: array-like, shape (n_features,)
+        Number of times each feature is selected across resampling runs divided by total number of resampling runs.
+    
+    feature_selection_matrix_: array-like, shape (n_splits, n_features)
+        A boolean matrix indicates features selected in each resampling run
+    
+    feature_rank_matrix_: array-like, shape (n_splits, n_features)
+        Feature ranks in each resampling run
+    
+    feature_importances_matrix_: array-like, shape (n_splits, n_features)
+        Feature importances in each resampling run
+    
+    feature_importances_: array-like, shape (n_features,)
+        Average feature importances across resampling runs
+    '''
+    def __init__(self, estimator, cv=None, n_features_to_select=10, verbose=0):
+        self.estimator = estimator
+        self.cv = cv
+        self.n_features_to_select = n_features_to_select
+        self.verbose = verbose
+    
+    def fit(self, X, y, sample_weight=None):
+        n_samples, n_features = X.shape
+        # compute sample weight
+        if sample_weight is None:
+            sample_weight = np.ones(n_samples)
+        feature_rank_matrix = []
+        feature_importances_matrix = []
+        for train_index, _ in self.cv.split(X, y, sample_weight):
+            estimator = clone(self.estimator)
+            estimator.fit(X[train_index], y[train_index], sample_weight=sample_weight[train_index])
+            feature_importances = get_feature_importances(estimator)
+            feature_importances_matrix.append(feature_importances)
+            feature_rank_matrix.append(get_feature_ranking(feature_importances))
+        self.feature_rank_matrix_ = np.vstack(feature_rank_matrix)
+        self.feature_importances_matrix_ = np.vstack(feature_importances_matrix)
+        self.feature_selection_matrix_ = (self.feature_rank_matrix_ < self.n_features_to_select).astype(np.int32)
+        self.feature_recurrence_ = np.mean(self.feature_selection_matrix_, axis=0)
+
+        self.feature_importances_ = self.feature_importances_matrix_.mean(axis=0)
+        self.ranking_ = get_feature_ranking(-self.feature_rank_matrix_.mean(axis=0))
+        self.support_ = np.zeros(n_features, dtype='bool')
+        self.support_[np.argsort(-self.feature_recurrence_)][:self.n_features_to_select] = True
+        return self
+
+    def _get_support_mask(self):
+        check_is_fitted(self, 'support_')
+        return self.support_
+
+class RpkmFilter(BaseEstimator, SelectorMixin):
+    def __init__(self, threshold=1, below=False, pseudo_count=0.01):
+        self.threshold = threshold
+        self.below = below
+        self.pseudo_count = pseudo_count
+    
+    def set_gene_lengths(self, gene_lengths):
+        self.gene_lengths = gene_lengths
+
+    def fit(self, X, y=None, **kwargs):
+        if getattr(self, 'gene_lengths') is None:
+            raise ValueError('gene_lengths is required for RpkmFilter')
+        rpkm = 1e3*X/self.gene_lengths.reshape((1, -1))
+        self.rpkm_mean_ = np.exp(np.mean(np.log(rpkm + self.pseudo_count), axis=0)) - self.pseudo_count
+        if self.below:
+            self.support_ = self.rpkm_mean_ < self.threshold
+        else:
+            self.support_ = self.rpkm_mean_ > self.threshold
+        return self
+    
+    def _get_support_mask(self):
+        check_is_fitted(self, 'support_')
+        return self.support_
+        
+class RpmFilter(BaseEstimator, SelectorMixin):
+    '''Feature selection based on geometric mean expression value across samples (RPM)
+
+    Parameters:
+    ----------
+
+    threshold: float
+        Expression value threshold
+    
+    below: bool
+        True if select features with expression value below threshold
+    
+    pseudo_count: float
+        Pseudo-count to add to input expression matrix during calculating the geometric mean
+    
+    Attributes:
+    ----------
+    
+    support_: bool | array-like, shape (n_features,)
+        Boolean mask indicating features selected
+
+    '''
+    def __init__(self, threshold=1, below=False, pseudo_count=0.01):
+        self.threshold = threshold
+        self.below = below
+        self.pseudo_count = pseudo_count
+
+    def fit(self, X, y=None, **kwargs):
+        self.rpm_mean_ = np.exp(np.mean(np.log(X + self.pseudo_count), axis=0)) - self.pseudo_count
+        if self.below:
+            self.support_ = self.rpm_mean_ < self.threshold
+        else:
+            self.support_ = self.rpm_mean_ > self.threshold
+        return self
+    
+    def _get_support_mask(self):
+        check_is_fitted(self, 'support_')
+        return self.support_
+
+class FoldChangeFilter(BaseEstimator, SelectorMixin):
+    '''Feature selection based on fold change
+
+    Parameters:
+    ----------
+
+    threshold: float
+        Fold change threshold
+    
+    direction: str
+        'both': fold change in both direction (up-regulated or down-regulated)
+        'up': up-regulated
+        'down': down-regulated
+    
+    below: bool
+        True if select features with fold change below threshold
+    
+    pseudo_count: float
+        Pseudo-count to add to input expression matrix
+
+    '''
+    def __init__(self, threshold=1, direction='any', below=False, pseudo_count=0.01):
+        if threshold <= 0:
+            raise ValueError('fold change threshold should be > 0')
+        self.direction = direction
+        self.threshold = threshold
+        self.pseudo_count = pseudo_count
+        self.below = below
+    
+    def fit(self, X, y, **kwargs):
+        unique_classes = np.sort(np.unique(y))
+        if len(unique_classes) != 2:
+            raise ValueError('FoldChangeSelector requires exactly 2 classes, but found {} classes'.format(len(unique_classes)))
+        # calculate geometric mean
+        X = X + self.pseudo_count
+        X_log = np.log2(X)
+        log_mean = np.zeros((2, X.shape[1]))
+        for i, c in enumerate(unique_classes):
+            log_mean[i] = np.mean(X_log[y == c], axis=0)
+        logfc = log_mean[1] - log_mean[0]
+        if self.direction == 'any':
+            self.logfc_ = np.abs(logfc)
+        elif self.direction == 'down':
+            self.logfc_ = -logfc
+        elif self.direction == 'up':
+            self.logfc_ = logfc
+        else:
+            raise ValueError('unknown fold change direction: {}'.format(self.direction))
+        
+        if self.below:
+            self.support_ = self.logfc_ < np.log(self.threshold)
+        else:
+            self.support_ = self.logfc_ > np.log(self.threshold)
+        return self
+    
+    def _get_support_mask(self):
+        check_is_fitted(self, 'support_')
+        return self.support_
+
+class ZeroFractionFilter(BaseEstimator, SelectorMixin):
+    '''Feature selection based on fraction of zero values
+
+    Parameters:
+    ----------
+
+    threshold: float
+        Features with zero values above this fraction will be filtered out
+    
+    eps: float
+        Define zero values as values below this number
+
+    '''
+    def __init__(self, threshold=0.8, eps=0.0):
+        self.threshold = threshold
+        self.eps = eps
+    
+    def fit(self, X, y=None, **kwargs):
+        self.zero_fractions_ = np.mean(X <= self.eps, axis=0)
+        self.support_ = self.zero_fractions_ < self.threshold
+        return self
+
+    def _get_support_mask(self):
+        check_is_fitted(self, 'support_')
+        return self.support_
+
+class HvgFilter(BaseEstimator, SelectorMixin):
+    def __init__(self, threshold=0):
+        self.threshold = threshold
+    
+    def fit(self, X, y=None, **kwargs):
+        mean = np.mean(X, axis=0)
+        std = np.std(X, axis=0)
+
+class FisherDiscriminantRatioFilter(BaseEstimator, SelectorMixin):
+    '''Feature selection based on fraction of zero values
+
+    Parameters:
+    ----------
+
+    threshold: float
+        Features with zero values above this fraction will be filtered out
+    
+    eps: float
+        Define zero values as values below this number
+    
+    Attributes:
+    ----------
+
+    support_: array-like, shape (n_features,)
+        Boolean mask indicating features selected
+    
+    scores_: array-like, shape (n_features,)
+        Fisher's discriminant ratios
+    '''
+    def __init__(self, threshold=0):
+        self.threshold = threshold
+    
+    def fit(self, X, y, **kwargs):
+        unique_classes = np.unique(y)
+        if len(unique_classes) != 2:
+            raise ValueError('Fisher discriminant ratio requires 2 classes')
+        unique_classes = np.sort(unique_classes)
+        mean = np.zeros(2)
+        var = np.zeros(2)
+        for i in range(2):
+            mean[i] = np.mean(X[y == unique_classes[i]], axis=0, ddof=1)
+            var[i] = np.var(X[y == unique_classes[i]], axis=0, ddof=1)
+        self.scores_ = (mean[1] - mean[0])/(var[0] + var[1])
+        self.support_ = self.scores_ > self.threshold
+    
+    def _get_support_mask(self):
+        check_is_fitted(self, 'support_')
+        return self.support_
+
+class DiffExpFilter(BaseEstimator, SelectorMixin):
+    '''Feature selection based on differential expression
+
+    Parameters:
+    ----------
+
+    threshold: float
+        Features with zero values above this fraction will be filtered out
+    
+    max_features: int
+        Maximum number of features to select
+    
+    score_type: str
+        Scores for ranking features.
+        Allowed values: 'neglogp', 'logfc', 'pi_score'
+        'pi_score'[2]: | log FC - log (padj) |
+    
+    method: str
+        Differential expression method to use.
+        Available methods: 'deseq2', 'edger_glmlrt', 'edger_glmqlf', 'edger_exact', 'wilcox'.
+    
+    temp_dir: str
+        Temporary directory for storing input and output files for differential expression
+    
+    script: str
+        Path of the script (to differential_expression.R)
+        The script takes two input files: matrix.txt and sample_classes.txt and outputs a table named results.txt.
+        The output file contains at least two columns: log2FoldChange, padj
+    
+    fold_change_direction: str
+        Direction of fold change filter
+        Allowed values: up, down or any.
+    
+    fold_change_threshold: float
+        Threshold for absolute fold change
+    
+    References:
+    ----------
+
+    1. Rosario, S.R., Long, M.D., Affronti, H.C., Rowsam, A.M., Eng, K.H., and Smiraglia, D.J. (2018). 
+        Pan-cancer analysis of transcriptional metabolic dysregulation using The Cancer Genome Atlas. Nature Communications 9, 5330.
+    2. Xiao, Y., Hsiao, T.-H., Suresh, U., Chen, H.-I.H., Wu, X., Wolf, S.E., and Chen, Y. (2014). 
+        A novel significance score for gene selection and ranking. Bioinformatics 30, 801–807.
+
+    
+    Attributes:
+    ----------
+
+    support_: array-like, shape (n_features,)
+        Boolean mask indicating features selected
+    
+    padj_: array-like, shape (n_features,)
+        Adjusted p-values for each feature
+    
+    logfc_: array-like, shape (n_features,)
+        Log2 fold change
+
+    '''
+    def __init__(self, 
+            rscript_path='Rscript',
+            threshold=0, 
+            max_features=None, 
+            score_type='adjusted_pvalue',
+            temp_dir=None,
+            script=None, method='deseq2',
+            fold_change_direction='any',
+            fold_change_threshold=1):
+        self.rscript_path = rscript_path
+        self.threshold = threshold
+        self.max_features = max_features
+        self.score_type = score_type
+        self.temp_dir = temp_dir
+        self.method = method
+        self.script = script
+        self.fold_change_direction = fold_change_direction
+        self.fold_change_threshold = fold_change_threshold
+    
+    def fit(self, X, y, **kwargs):
+        if self.temp_dir is None:
+            raise ValueError('parameter temp_dir is required for DiffExpFilter')
+        if self.script is None:
+            raise ValueError('parameter script is required for DiffExpFilter')
+        # save expression matrix to file
+        matrix = pd.DataFrame(X.T, 
+            index=['F%d'%i for i in range(X.shape[1])],
+            columns=['S%d'%i for i in range(X.shape[0])])
+        if not os.path.isdir(self.temp_dir):
+            logger.debug('create diffexp dir: {}'.format(self.temp_dir))
+            os.makedirs(self.temp_dir)
+        try:
+            matrix_file = os.path.join(self.temp_dir, 'matrix.txt')
+            logger.debug('write expression matrix to : {}'.format(matrix_file))
+            matrix.to_csv(matrix_file, sep='\t', na_rep='NA', index=True, header=True)
+            # save sample_classes to file
+            sample_classes = pd.Series(y, index=matrix.columns.values)
+            sample_classes = sample_classes.map({0: 'negative', 1: 'positive'})
+            sample_classes.name = 'label'
+            sample_classes.index.name = 'sample_id'
+            sample_classes_file = os.path.join(self.temp_dir, 'sample_classes.txt')
+            logger.debug('write sample classes to: {}'.format(sample_classes_file))
+            sample_classes.to_csv(sample_classes_file, sep='\t', na_rep='NA', index=True, header=True)
+            output_file = os.path.join(self.temp_dir, 'results.txt')
+            logger.debug('run differential expression script: {}'.format(self.script))
+            subprocess.check_call([self.rscript_path, self.script, 
+                '--matrix', matrix_file, 
+                '--classes', sample_classes_file,
+                '--method', self.method,
+                '--positive-class', 'positive', '--negative-class', 'negative',
+                '-o', output_file])
+            # read results
+            logger.debug('read differential expression results: {}'.format(output_file))
+            results = pd.read_table(output_file, sep='\t', index_col=0)
+        finally:
+            # remove temp_dir after reading the output file or an exception occurs
+            logger.debug('remove diffexp directory: {}'.format(self.temp_dir))
+            shutil.rmtree(self.temp_dir, ignore_errors=True)
+        self.logfc_ = results.loc[:, 'log2FoldChange']
+        self.padj_ = results.loc[:, 'padj']
+        if self.score_type == 'neglogp':
+            self.scores_ = -np.log10(self.padj_)
+        elif self.score_type == 'logfc':
+            self.scores_ = np.abs(self.logfc_)
+        elif self.score_type == 'pi_value':
+            self.scores_ = np.abs(self.logfc_)*(-np.log10(self.padj_))
+        else:
+            raise ValueError('unknown differential expression score type: {}'.format(self.score_type))
+        # apply fold change filter
+        fc_support = np.ones(X.shape[1], dtype='bool')
+        if self.fold_change_direction != 'any':
+            logger.debug('apply fold change filter: {} > {:.2f}'.format(self.fold_change_direction, self.fold_change_threshold))
+            if self.fold_change_threshold == 'up':
+                fc_support = self.logfc_ > np.log2(self.fold_change_threshold)
+            elif self.fold_change_threshold == 'down':
+                fc_support = self.logfc_ < -np.log2(self.fold_change_threshold)
+            else:
+                raise ValueError('unknown fold change direction: {}'.format(self.fold_change_direction))
+        # compute support mask
+        if self.max_features is not None:
+            # sort feature scores in descending order and get top features
+            indices = np.nonzero(fc_support)[0]
+            indices = indices[np.argsort(-self.scores_[indices])][:self.max_features]
+            self.support_ = np.zeros(X.shape[1], dtype='bool')
+            self.support_[indices] = True
+        else:
+            # select features with scores above a given threshold
+            self.support_ = (self.scores_ > self.threshold) & fc_support
+        return self
+    
+    def _get_support_mask(self):
+        check_is_fitted(self, 'support_')
+        return self.support_
+
+class SIS(BaseEstimator, SelectorMixin):
+    '''Sure Independence Screening
+
+    Original R function:
+
+    SIS(x, y, family = c("gaussian", "binomial", "poisson", "cox"),
+        penalty = c("SCAD", "MCP", "lasso"), concavity.parameter = switch(penalty,
+        SCAD = 3.7, 3), tune = c("bic", "ebic", "aic", "cv"), nfolds = 10,
+        type.measure = c("deviance", "class", "auc", "mse", "mae"),
+        gamma.ebic = 1, nsis = NULL, iter = TRUE, iter.max = ifelse(greedy ==
+        FALSE, 10, floor(nrow(x)/log(nrow(x)))), varISIS = c("vanilla", "aggr",
+        "cons"), perm = FALSE, q = 1, greedy = FALSE, greedy.size = 1,
+        seed = 0, standardize = TRUE)
+
+    Parameters:
+    ----------
+
+    temp_dir: str
+        directory for storing temporary files
+    
+    sis_args: dict
+        Arguments to pass to SIS function
+    
+
+    '''
+    def __init__(self, rscript_path='Rscript', temp_dir=None, n_features_to_select=None, sis_params=None):
+        self.rscript_path = rscript_path
+        self.temp_dir = temp_dir
+        self.n_features_to_select = n_features_to_select
+        self.sis_params = sis_params
+        if n_features_to_select is not None:
+            self.sis_params['nsis'] = n_features_to_select
+        if self.sis_params is None:
+            self.sis_params.update(deepcopy(sis_params))
+        if self.temp_dir is None:
+            raise ValueError('temp_dir is required for SIS')
+    
+    def fit(self, X, y):
+        if not os.path.isdir(self.temp_dir):
+            os.makedirs(self.temp_dir)
+        try:
+            pd.DataFrame(X).to_csv(os.path.join(self.temp_dir, 'matrix.txt'), 
+                sep='\t', header=False, index=False, na_rep='NA')
+            pd.Series(y).to_csv(os.path.join(self.temp_dir, 'labels.txt'), header=False, index=False)
+            #print(y[:10])
+            r_script = r'''
+library(SIS)
+X <- read.table('{temp_dir}/matrix.txt', header=FALSE, check.names=FALSE)
+X <- as.matrix(X)
+y <- read.table('{temp_dir}/labels.txt')[,1]
+y <- as.numeric(y)
+model <- SIS(X, y, {sis_params})
+write.table(model$coef.est, '{temp_dir}/coef.txt', sep='\t', col.names=FALSE, row.names=TRUE, quote=FALSE)
+write.table(model$ix, '{temp_dir}/ix.txt', col.names=FALSE, row.names=FALSE, quote=FALSE)
+    '''
+            print(self.sis_params)
+            r_script = r_script.format(temp_dir=self.temp_dir, sis_params=python_args_to_r_args(self.sis_params))
+            print(python_args_to_r_args(self.sis_params))
+            script_file = os.path.join(self.temp_dir, 'run_SIS.R')
+            with open(script_file, 'w') as f:
+                f.write(r_script)
+            logger.debug('execute R script: ' + r_script)
+            subprocess.check_call([self.rscript_path, script_file], shell=False)
+            # read outputs
+            #coef = pd.read_table(os.path.join(self.temp_dir, 'coef.txt'), sep='\t', index=True, header=None)
+            # read indices of selected features
+            ix = pd.read_table(os.path.join(self.temp_dir, 'ix.txt'), sep='\t', header=None)
+            indices = ix.iloc[:, 0].values - 1
+            self.support_ = np.zeros(X.shape[1], dtype='bool')
+            self.support_[indices] = True
+        finally:
+            pass
+            #shutil.rmtree(self.temp_dir)
+    
+    def _get_support_mask(self):
+        check_is_fitted(self, 'support_')
+        return self.support_
+
+class RandomSubsetSelector(BaseEstimator, SelectorMixin):
+    '''Large scale feature selection based on max feature weights on feature subsets
+
+    Parameters:
+    ----------
+
+    estimator: BaseEstimator object
+        Internal estimator to use for feature selection
+    
+    n_subsets: int
+        Number of random feature subsets
+    
+    subset_size: int
+        Number of features in each subset
+    
+    n_features_to_select: int
+        Maximum number of features to select
+    
+    random_state: RandomState
+        Random number generator
+    '''
+    def __init__(self, estimator, n_subsets=40, subset_size=50, n_features_to_select=10, random_state=None):
+        self.estimator = estimator
+        self.n_subsets = n_subsets
+        self.subset_size = subset_size
+        self.n_features_to_select = n_features_to_select
+        self.random_state = random_state
+        
+    def fit(self, X, y, sample_weight=None):
+        n_features = X.shape[1]
+        feature_weights = np.zeros((self.n_subsets, n_features))
+        rng = np.random.RandomState(self.random_state)
+        for subset_index in range(self.n_subsets):
+            subset = rng.choice(n_features, size=self.subset_size, replace=False)
+            estimator = clone(self.estimator)
+            estimator.fit(X[:, subset], y, sample_weight=sample_weight)
+            feature_weights[subset_index, subset] = get_feature_importances(estimator)
+        # get local maximum
+        feature_weights = np.max(feature_weights, axis=0)
+        self.features_ = np.argsort(-feature_weights)[:self.n_features_to_select]
+        self.support_ = np.zeros(n_features, dtype='bool')
+        self.support_[self.features_] = True
+    
+    def _get_support_mask(self):
+        check_is_fitted(self, 'support_')
+        return self.support_
+
+class NullSelector(BaseEstimator, SelectorMixin):
+    '''A null selector that select all features
+
+    Attributes:
+    ----------
+
+    support_: array-like, shape (n_features,)
+        Boolean mask indicating features selected
+    '''
+    def fit(self, X, y=None, **kwargs):
+        self.support_ = np.ones(X.shape[1], dtype='bool')
+        return self
+    
+    def _get_support_mask(self):
+        check_is_fitted(self, 'support_')
+        return self.support_
+    
+def get_selector(name, estimator=None, n_features_to_select=None, **params):
+    if name == 'RobustSelector':
+        return RobustSelector(estimator, n_features_to_select=n_features_to_select, **search_dict(params,
+         ('cv', 'verbose')))
+    elif name == 'MaxFeatures':
+        return SelectFromModel(estimator, threshold=-np.inf, max_features=n_features_to_select)
+    elif name == 'RandomSubsetSelector':
+        return RandomSubsetSelector(estimator, n_features_to_select=n_features_to_select, **search_dict(params,
+        ('n_subsets', 'subset_size', 'random_state')))
+    elif name == 'FeatureImportanceThreshold':
+        return SelectFromModel(estimator, **search_dict(params, 'threshold'))
+    elif name == 'RFE':
+        return RFE(estimator, n_features_to_select=n_features_to_select, **search_dict(params, 
+        ('step', 'verbose')))
+    elif name == 'RFECV':
+        return RFECV(estimator, n_features_to_select=n_features_to_select, **search_dict(params,
+         ('step', 'cv', 'verbose')))
+    elif name == 'FoldChangeFilter':
+        return FoldChangeFilter(**search_dict(params,
+        ('threshold', 'direction', 'below', 'pseudo_count')))
+    elif name == 'ZeroFractionFilter':
+        return ZeroFractionFilter(**search_dict(params,
+        ('threshold',)))
+    elif name == 'RpkmFilter':
+        return RpkmFilter(**search_dict(params,
+        ('threshold',)))
+    elif name == 'RpmFilter':
+        return RpmFilter(**search_dict(params,
+        ('threshold',)))
+    elif name == 'DiffExpFilter':
+        return DiffExpFilter(max_features=n_features_to_select, **search_dict(params,
+        ('rscript_path', 'threshold', 'script', 'temp_dir', 'score_type', 'method')))
+    elif name == 'ReliefF':
+        from skrebate import ReliefF
+        return ReliefF(n_features_to_select=n_features_to_select,
+            **search_dict(params, ('n_jobs', 'n_neighbors', 'discrete_limit')))
+    elif name == 'SURF':
+        from skrebate import SURF
+        return SURF(n_features_to_select=n_features_to_select,
+            **search_dict(params, ('n_jobs', 'discrete_limit')))
+    elif name == 'MultiSURF':
+        from skrebate import MultiSURF
+        return MultiSURF(n_features_to_select=n_features_to_select,
+            **search_dict(params, ('n_jobs', 'discrete_limit')))
+    elif name == 'SIS':
+        return SIS(n_features_to_select=n_features_to_select, 
+            **search_dict(params, ('rscript_path', 'temp_dir', 'sis_params')))
+    elif name == 'NullSelector':
+        return NullSelector()
+    else:
+        raise ValueError('unknown selector: {}'.format(name))
\ No newline at end of file