Repository: ctlab/ITMO_FS (language: python)

Issue:
Add logging and refactor code by PEP 8



Discussion / hints:

Fix patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 0e036aad..54abe8c8 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -26,8 +26,10 @@ jobs:
             conda create -n testenv --yes --quiet python=3
             source activate testenv
             pip install -r requirements.txt
-            pip install pandas sphinx_rtd_theme
+            pip install pandas sphinx_rtd_theme dvc
             pip install sphinx-gallery
+            pip install dvc
+            pip install pydrive2
             pip install .
             cd docs
             make html
@@ -50,8 +52,9 @@ jobs:
             pip install -r requirements.txt
             pip install pandas sphinx_rtd_theme
             pip install pandas pandas
+            pip install dvc
             pip install pytest
-            pytest .
+            pytest --cov=ITMO_FS test/
 
       - store_artifacts:
           path: test-reports
diff --git a/.github/.codecov.yml b/.github/.codecov.yml
index 25bf0258..c23e2c84 100644
--- a/.github/.codecov.yml
+++ b/.github/.codecov.yml
@@ -1,6 +1,6 @@
 codecov:
   require_ci_to_pass: yes
-
+  token: f8baf545-d745-4d99-81e7-ef7e019a1d1c
 coverage:
   precision: 2
   round: down
diff --git a/ITMO_FS/__about__.py b/ITMO_FS/__about__.py
new file mode 100644
index 00000000..2b287de3
--- /dev/null
+++ b/ITMO_FS/__about__.py
@@ -0,0 +1,6 @@
+__all__ = ["__title__", "__uri__", "__version__"]
+
+__title__ = "ITMO_FS"
+__uri__ = "https://github.com/ctlab/ITMO_FS"
+
+__version__ = "0.3.4"
diff --git a/ITMO_FS/_version.py b/ITMO_FS/_version.py
deleted file mode 100644
index f9aa3e11..00000000
--- a/ITMO_FS/_version.py
+++ /dev/null
@@ -1,1 +0,0 @@
-__version__ = "0.3.2"
diff --git a/ITMO_FS/embedded/MOS.py b/ITMO_FS/embedded/MOS.py
index bb20a37a..49fa9202 100644
--- a/ITMO_FS/embedded/MOS.py
+++ b/ITMO_FS/embedded/MOS.py
@@ -1,152 +1,132 @@
-import random as rnd
+from logging import getLogger
 
 import numpy as np
 from imblearn.over_sampling import SMOTE
-from sklearn.linear_model import SGDClassifier
-
-from ..utils import augmented_rvalue, DataChecker, generate_features
-
-
-class MOS(DataChecker):
+from sklearn.base import clone
+
+from ..utils import augmented_rvalue, BaseTransformer
+
+
+class MOS(BaseTransformer):
+    """Perform Minimizing Overlapping Selection under SMOTE (MOSS) or under
+    No-Sampling (MOSNS) algorithm.
+
+    Parameters
+    ----------
+    model : object
+        The model that should have a fit(X, y) method and a field corresponding
+        to feature weights. Currently only SGDClassifier should be passed,
+        other models would not work.
+    weight_func : callable
+        The function to extract weights from the model.
+    loss : str, 'log' or 'hinge'
+        Loss function to use in the algorithm. 'log' gives a logistic
+        regression, while 'hinge' gives a support vector machine.
+    seed : int, optional
+        Seed for python random.
+    l1_ratio : float
+        The value used to balance the L1 and L2 penalties in elastic-net.
+    threshold : float
+        The threshold value for feature dropout. Instead of comparing them to
+        zero, they are normalized and values with absolute value lower than the
+        threshold are dropped out.
+    epochs : int
+        The number of epochs to perform in the algorithm.
+    alphas : array-like, shape (n_alphas,), optional
+        The range of lambdas that should form the regularization path.
+    sampling : bool
+        Bool value that control whether MOSS (True) or MOSNS (False) should be
+        executed.
+    k_neighbors : int
+        Amount of nearest neighbors to use in SMOTE if MOSS is used.
+
+    Notes
+    -----
+    For more details see `this paper
+    <https://www.sciencedirect.com/science/article/pii/S0169743919306070/>`_.
+
+    Examples
+    --------
+    >>> from ITMO_FS.embedded import MOS
+    >>> from sklearn.linear_model import SGDClassifier
+    >>> import numpy as np
+    >>> from sklearn.datasets import make_classification
+    >>> from sklearn.linear_model import LogisticRegression
+    >>> dataset = make_classification(n_samples=100, n_features=10,
+    ... n_informative=5, n_redundant=0, weights=[0.85, 0.15], random_state=42,
+    ... shuffle=False)
+    >>> X, y = np.array(dataset[0]), np.array(dataset[1])
+    >>> m = MOS(model=SGDClassifier(),
+    ... weight_func=lambda model: np.square(model.coef_).sum(axis=0)).fit(X, y)
+    >>> m.selected_features_
+    array([1, 3, 4], dtype=int64)
+    >>> m = MOS(model=SGDClassifier(), sampling=True,
+    ... weight_func=lambda model: np.square(model.coef_).sum(axis=0)).fit(X, y)
+    >>> m.selected_features_
+    array([1, 3, 4, 6], dtype=int64)
     """
-        Performs Minimizing Overlapping Selection under SMOTE (MOSS) or under No-Sampling (MOSNS) algorithm.
-
-        Parameters
-        ----------
-        model : constructor
-            The constructor of the model that will be used. Currently only SGDClassifier should be passed, 
-            other models would not work.
-        loss : str, 'log' or 'hinge'
-            Loss function to use in the algorithm. 'log' gives a logistic regression, while 'hinge'
-            gives a support vector machine. 
-        seed : int
-            Seed for python random.
-
-        Notes
-        -----
-        For more details see `this paper <https://www.sciencedirect.com/science/article/pii/S0169743919306070/>`_.
-
-        Examples
-        --------
-        >>> from ITMO_FS.embedded import MOS
-        >>> import numpy as np
-        >>> from sklearn.datasets import make_classification
-        >>> from sklearn.linear_model import LogisticRegression
-        >>> dataset = make_classification(n_samples=100, n_features=20)
-        >>> data, target = np.array(dataset[0]), np.array(dataset[1])
-        >>> for i in range(50):  # create imbalance between classes
-        ...     target[i] = 0
-        >>> m=MOS()
-        >>> m.fit(data, target)
-        >>> m.transform(data).shape[0]
-        100
-    """
-
-    def __init__(self, model=SGDClassifier, loss='log',
-                 seed=42):  # TODO Add wrapper function which will take weights from module
-        if loss not in ['hinge', 'log']:
-            raise KeyError("Loss should be 'hinge' or 'log', %r was passed" % loss)
+    def __init__(self, model, weight_func, loss='log', seed=42, l1_ratio=0.5,
+                 threshold=1e-3, epochs=1000, alphas=np.arange(0.01, 0.2, 0.01),
+                 sampling=False, k_neighbors=2):
         self.model = model
+        self.weight_func = weight_func
         self.loss = loss
-        rnd.seed = seed
-        self.selected_features = None
+        self.seed = seed
+        self.l1_ratio = l1_ratio
+        self.threshold = threshold
+        self.epochs = epochs
+        self.alphas = alphas
+        self.sampling = sampling
+        self.k_neighbors = k_neighbors
 
-    def fit(self, X, y, l1_ratio=0.5, threshold=10e-4, epochs=1000, alphas=np.arange(0.0002, 0.02, 0.0002),
-            sampling=True, feature_names=None):
-        """
-            Runs the MOS algorithm on the specified dataset.
-
-            Parameters
-            ----------
-            X : array-like, shape (n_samples,n_features)
-                The input samples.
-            y : array-like, shape (n_samples)
-                The classes for the samples.
-            l1_ratio : float, optional
-                The value used to balance the L1 and L2 penalties in elastic-net.
-            threshold : float, optional
-                The threshold value for feature dropout. Instead of comparing them to zero, they are normalized 
-                and values with absolute value lower than the threshold are dropped out.
-            epochs : int, optional
-                The number of epochs to perform in the algorithm.
-            alphas : array-like, shape (n_alphas), optional
-                The range of lambdas that should form the regularization path.
-            sampling : bool, optional
-                Bool value that control whether MOSS (True) or MOSNS (False) should be executed.
-            feature_names : list of strings, optional
-                    In case you want to define feature names
+    def _fit(self, X, y):
+        """Run the MOS algorithm on the specified dataset.
 
-            Returns
-            ------
-            None
+        Parameters
+        ----------
+        X : array-like, shape (n_samples, n_features)
+            The input samples.
+        y : array-like, shape (n_samples,)
+            The classes for the samples.
+
+        Returns
+        -------
+        None
         """
+        if self.loss not in ['hinge', 'log']:
+            getLogger(__name__).error(
+                "Loss should be 'hinge' or 'log', %s was passed", self.loss)
+            raise KeyError(
+                "Loss should be 'hinge' or 'log', %s was passed" % self.loss)
+
+        if self.sampling:
+            try:
+                X, y = SMOTE(
+                    random_state=self.seed,
+                    k_neighbors=self.k_neighbors).fit_resample(X, y)
+            except ValueError:
+                getLogger(__name__).warning(
+                    "Couldn't perform SMOTE because k_neighbors is bigger "
+                    "than amount of instances in one of the classes; MOSNS "
+                    "would be performed instead")
 
-        features = generate_features(X)
-        X, y, feature_names = self._check_input(X, y, feature_names)
-        self.feature_names = dict(zip(features, feature_names))
-        if sampling:
-            X, y = SMOTE(random_state=rnd.seed).fit_resample(X, y)
         min_rvalue = 1
         min_b = []
-        for a in alphas:  # TODO: do a little more research on the range of lambdas
-            model = self.model(loss=self.loss, random_state=rnd.seed, penalty='elasticnet',
-                               alpha=a, l1_ratio=l1_ratio, max_iter=epochs)  # TODO Change that to more abstract type
+        model = clone(self.model)
+        for a in self.alphas:  # TODO: do a little more
+            # research on the range of lambdas
+            model = model.set_params(
+                loss=self.loss, random_state=self.seed, penalty='elasticnet',
+                alpha=a, l1_ratio=self.l1_ratio, max_iter=self.epochs)
             model.fit(X, y)
-            b = model.coef_[0]
-            rvalue = augmented_rvalue(X[:, [i for i in range(X.shape[1]) if np.abs(b[i]) > threshold]], y)
+            b = self.weight_func(model)
+            rvalue = augmented_rvalue(
+                X[:, np.flatnonzero(np.abs(b) > self.threshold)], y)
+            getLogger(__name__).info(
+                "For alpha %f: rvalue = %f, weight vector = %s", a, rvalue, b)
             if min_rvalue > rvalue:
                 min_rvalue = rvalue
                 min_b = b
-        self.selected_features = features[[i for i in range(X.shape[1]) if np.abs(min_b[i]) > threshold]]
-
-    def transform(self, X):
-        """
-            Transform given data by slicing it with selected features.
-
-            Parameters
-            ----------
-            X : array-like, shape (n_samples, n_features)
-                The training input samples.
-
-            Returns
-            ------
-            Transformed 2D numpy array
-        """
-
-        if type(X) is np.ndarray:
-            return X[:, self.selected_features.astype(int)]
-        else:
-            return X[self.selected_features]
-
-    def fit_transform(self, X, y, l1_ratio=0.5, threshold=10e-4, epochs=1000, alphas=np.arange(0.0002, 0.02, 0.0002),
-                      sampling=True, feature_names=None):
-        """
-            Fits the algorithm and transforms given dataset X.
-
-            Parameters
-            ----------
-            X : array-like, shape (n_features, n_samples)
-                The training input samples.
-            y : array-like, shape (n_samples, )
-                The target values.
-            l1_ratio : float, optional
-                The value used to balance the L1 and L2 penalties in elastic-net.
-            threshold : float, optional
-                The threshold value for feature dropout. Instead of comparing them to zero, they are normalized 
-                and values with absolute value lower than the threshold are dropped out.
-            epochs : int, optional
-                The number of epochs to perform in gradient descent.
-            alphas : array-like, shape (n_alphas), optional
-                The range of lambdas that should form the regularization path.
-            sampling : bool, optional
-                Bool value that control whether MOSS (True) or MOSNS (False) should be executed.
-            feature_names : list of strings, optional
-                In case you want to define feature names
-
-            Returns
-            -------
-            X dataset sliced with features selected by the algorithm
-        """
-
-        self.fit(X, y, feature_names=feature_names)
-        return self.transform(X)
+                getLogger(__name__).info("New minimum rvalue: %f", rvalue)
+                getLogger(__name__).info("New weight vector: %s", b)
+        self.selected_features_ = np.flatnonzero(np.abs(min_b) > self.threshold)
diff --git a/ITMO_FS/ensembles/measure_based/WeightBased.py b/ITMO_FS/ensembles/measure_based/WeightBased.py
index 72808b70..ae9f21d1 100644
--- a/ITMO_FS/ensembles/measure_based/WeightBased.py
+++ b/ITMO_FS/ensembles/measure_based/WeightBased.py
@@ -1,85 +1,119 @@
-from collections import defaultdict
+from logging import getLogger
 
-from numpy import ndarray, ones
-from sklearn.base import TransformerMixin
+import numpy as np
+from sklearn.base import clone
 
-from ITMO_FS.utils.data_check import *
 from .fusion_functions import *
+from ...utils import BaseTransformer, apply_cr, check_filters
 
 
-class WeightBased(TransformerMixin):
-    __filters = []
+class WeightBased(BaseTransformer):
+    """Weight-based filter ensemble. The ensemble first computes all filter
+    scores for the dataset and then aggregates them using a selected fusion
+    function.
 
-    def __init__(self, filters):
-        """
-        TODO comments
-        :param filters:
+    Parameters
+    ----------
+    filters : collection
+        Collection of filter objects. Filters should have a fit(X, y) method
+        and a feature_scores_ field that contains scores for all features.
+    cutting_rule : string or callable
+        A cutting rule name defined in GLOB_CR or a callable with signature
+        cutting_rule (features), which should return a list features ranked by
+        some rule.
+    fusion_function : callable
+        A function with signature (filter_scores (array-like, shape
+        (n_filters, n_features)), weights (array-like, shape (n_filters,)))
+        that should return the aggregated weights for all features.
+    weights : array-like
+        An array of shape (n_filters,) defining the weights for input filters.
+
+    See Also
+    --------
+
+    Examples
+    --------
+    >>> from ITMO_FS.ensembles import WeightBased
+    >>> from ITMO_FS.filters.univariate import UnivariateFilter
+    >>> import numpy as np
+    >>> filters = [UnivariateFilter('GiniIndex'),
+    ... UnivariateFilter('FechnerCorr'),
+    ... UnivariateFilter('SpearmanCorr'),
+    ... UnivariateFilter('PearsonCorr')]
+    >>> x = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1],
+    ... [3, 1, 4, 3, 1], [3, 1, 2, 3, 1]])
+    >>> y = np.array([1, 3, 2, 1, 2])
+    >>> wb = WeightBased(filters, ("K best", 2)).fit(x, y)
+    >>> wb.selected_features_
+    array([4, 1], dtype=int64)
+    """
+    def __init__(self, filters, cutting_rule=("K best", 2),
+                 fusion_function=weight_fusion, weights=None):
+        self.filters = filters
+        self.cutting_rule = cutting_rule
+        self.fusion_function = fusion_function
+        self.weights = weights
+
+    def get_scores(self, X, y):
+        """Return the normalized feature scores for all filters.
+
+        Parameters
+        ----------
+        X : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like, shape (n_samples,)
+            The target values.
+
+        Returns
+        -------
+        array-like, shape (n_filters, n_features) : feature scores
         """
-        check_filters(filters)
-        self.__filters = filters
-        self.selected_features = None
-        self.feature_scores = None
+        scores = np.vectorize(
+            lambda f: clone(f).fit(X, y).feature_scores_,
+            signature='()->(1)')(self.filters)
+        getLogger(__name__).info("Scores for all filters: %s", scores)
+        mins = np.min(scores, axis=1).reshape(-1, 1)
+        maxs = np.max(scores, axis=1).reshape(-1, 1)
+        return (scores - mins) / (maxs - mins)
 
     def __len__(self):
-        return len(self.__filters)
-
-    def score(self, X, y, feature_names=None):
-        self.feature_scores = defaultdict(list)
-        for _filter in self.__filters:
-            _filter.fit(X, y, feature_names=feature_names, store_scores=True)
-            _min = min(_filter.feature_scores.values())
-            _max = max(_filter.feature_scores.values())
-            for key, value in _filter.feature_scores.items():
-                self.feature_scores[key].append((value - _min) / (_max - _min))
-        return self.feature_scores
-
-    def fit(self, X, y, feature_names=None):
-        """
-        TODO comments
-        :param X:
-        :param y:
-        :param feature_names:
-        :return:
-        """
-        feature_names = generate_features(X, feature_names)
-        check_shapes(X, y)
-        # check_features(features_names)
-        _feature_names = feature_names
-        _X = X
-        _y = y
-        self.feature_scores = self.score(_X, _y, _feature_names)
-
-    def transform(self, x, cutting_rule, fusion_function=weight_fusion, weights=None):
-        """
-        Transfrom dataset
-        :param x:
-        :param cutting_rule:
-        :param fusion_function:
-        :param weights:
-        :return:
-        """
-        if weights is None:
-            weights = ones(len(self.__filters)) / len(self.__filters)
-        self.selected_features = cutting_rule(fusion_function(self.feature_scores, weights))
-        if type(x) is ndarray:
-            return x[:, self.selected_features]
-        else:
-            return x[self.selected_features]
+        """Return the number of filters used in the ensemble.
+
+        Parameters
+        ----------
 
-    def fit_transform(self, X, y=None, **fit_params):
+        Returns
+        -------
+        int : number of filters
         """
-        TODO comments
-        :param X:
-        :param y:
-        :param fit_params:
-        :return:
+        return len(self.filters)
+
+    def _fit(self, X, y):
+        """Fit the ensemble.
+
+        Parameters
+        ----------
+        X : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like, shape (n_samples,)
+            The target values.
+
+        Returns
+        -------
+        None
         """
-        cutting_rule, feature_names, fusion_function, weights = fit_params
-        self.fit(X, y, feature_names)
-        return self.transform(X, cutting_rule, fusion_function, weights)
-
-    def __repr__(self):
-        result = 'Filter weight based ensemble with: \n'
-        for f in self.__filters:
-            result += str(f) + ' filter \n'
-        return result
+        check_filters(self.filters)
+        getLogger(__name__).info(
+            "Running WeightBased with filters: %s", self.filters)
+        filter_scores = self.get_scores(X, y)
+        getLogger(__name__).info(
+            "Normalized scores for all filters: %s", filter_scores)
+        if self.weights is None:
+            weights = np.ones(len(self.filters)) / len(self.filters)
+        else:
+            weights = self.weights
+        getLogger(__name__).info("Weights vector: %s", weights)
+        self.feature_scores_ = self.fusion_function(filter_scores, weights)
+        getLogger(__name__).info("Feature scores: %s", self.feature_scores_)
+        self.selected_features_ = apply_cr(self.cutting_rule)(
+            self.feature_scores_)
diff --git a/ITMO_FS/ensembles/measure_based/fusion_functions.py b/ITMO_FS/ensembles/measure_based/fusion_functions.py
index d793b06b..7bb25f89 100644
--- a/ITMO_FS/ensembles/measure_based/fusion_functions.py
+++ b/ITMO_FS/ensembles/measure_based/fusion_functions.py
@@ -1,8 +1,18 @@
 from numpy import dot
 
 
-def weight_fusion(filter_results, weights):
-    result = {}
-    for key, value in filter_results.items():
-        result[key] = dot(value, weights)
-    return result
+def weight_fusion(filter_scores, weights):
+    """Calculate the weighted score of each feature.
+
+    Parameters
+    ----------
+    filter_scores : array-like, shape (n_filters, n_features)
+        Scores for all filters.
+    weights : array-like, shape (n_filters,)
+        Filter weights.
+
+    Returns
+    -------
+    array-like, shape (n_features,) : feature scores
+    """
+    return filter_scores.T.dot(weights)
diff --git a/ITMO_FS/ensembles/model_based/best_sum.py b/ITMO_FS/ensembles/model_based/best_sum.py
index 0362e610..c02293dd 100644
--- a/ITMO_FS/ensembles/model_based/best_sum.py
+++ b/ITMO_FS/ensembles/model_based/best_sum.py
@@ -1,34 +1,101 @@
 import numpy as np
+from sklearn.base import clone
+from sklearn.model_selection import cross_val_score
+from logging import getLogger
+from ...utils import BaseTransformer, apply_cr
 
 
-class BestSum:  ## TODO refactor , not stable
+class BestSum(BaseTransformer):
+    """Best weighted sum ensemble. The ensemble fits the input models and
+    computes the feature scores as the weighted sum of the models' feature
+    scores and some performance metric (e.g. accuracy)
 
-    def __init__(self, models, cutting_rule):
+    Parameters
+    ----------
+    models : collection
+        Collection of model objects. Models should have a fit(X, y) method and
+        a field corresponding to feature weights.
+    cutting_rule : string or callable
+        A cutting rule name defined in GLOB_CR or a callable with signature
+        cutting_rule (features), which should return a list features ranked by
+        some rule.
+    weight_func : callable
+        The function to extract weights from the model.
+    metric : string or callable
+        A standard estimator metric (e.g. 'f1' or 'roc_auc') or a callable
+        object / function with signature measure(estimator, X, y) which
+        should return only a single value.
+    cv : int
+        Number of folds in cross-validation.
+
+    See Also
+    --------
+    Jeon, H.; Oh, S. Hybrid-Recursive Feature Elimination for Efficient
+    Feature Selection. Appl. Sci. 2020, 10, 3211.
+
+        Examples
+        --------
+        >>> from ITMO_FS.ensembles import BestSum
+        >>> from sklearn.svm import SVC
+        >>> from sklearn.linear_model import LogisticRegression
+        >>> from sklearn.linear_model import RidgeClassifier
+        >>> import numpy as np
+        >>> models = [SVC(kernel='linear'),
+        ... LogisticRegression(),
+        ... RidgeClassifier()]
+        >>> x = np.array([[3, 3, 3, 2, 2],
+        ...               [3, 3, 1, 2, 3],
+        ...               [1, 3, 5, 1, 1],
+        ...               [3, 1, 4, 3, 1],
+        ...               [3, 1, 2, 3, 1]])
+        >>> y = np.array([1, 2, 2, 1, 2])
+        >>> bs = BestSum(models, ("K best", 2),
+        ... lambda model: np.square(model.coef_).sum(axis=0), cv=2).fit(x, y)
+        >>> bs.selected_features_
+        array([0, 2], dtype=int64)
+    """
+
+    def __init__(self, models, cutting_rule, weight_func, metric='f1_micro',
+                 cv=3):
+        super().__init__()
         self.models = models
         self.cutting_rule = cutting_rule
-        self.features = None
-
-    def fit(self, x, y, feature_names=None):
-        try:
-            feature_names = x.columns
-        except AttributeError:
-            if feature_names is None:
-                feature_names = list(range(x.shape[1]))
-        self.features = dict(zip(feature_names, np.zeros(len(feature_names))))
-        for model in self.models:
-            model.fit(x, y)
-            for i, k in enumerate(model.selected_features):
-                self.features[k] += (model.best_score - self.features[k]) / (i + 1)
-
-    def cut(self, cutting_rule=None):
-        if cutting_rule is None:
-            return self.cutting_rule(self.features)
-        return cutting_rule(self.features)
-
-    def predict(self, X):
-        new = self.cut(self.features)
-        n = len(self.models)
-        result = np.zeros((X.shape[0], n))
-        for i, model in enumerate(self.models):
-            result[:, i] = model.predict(X[:, new])
-        return np.array([1 if i else 0 for i in result.sum(axis=1) / n > 0.5])
+        self.weight_func = weight_func
+        self.metric = metric
+        self.cv = cv
+
+    def _fit(self, X, y):
+        """
+            Fits the ensemble.
+
+            Parameters
+            ----------
+            X : array-like, shape (n_samples, n_features)
+                The training input samples.
+            y : array-like, shape (n_samples, )
+                The target values.
+
+        Returns
+        -------
+        None
+        """
+
+        def __get_weights(model):
+            _model = clone(model).fit(X, y)
+            weights = self.weight_func(_model)
+            perf = cross_val_score(_model, X, y, cv=self.cv,
+                                   scoring=self.metric).mean()
+            return weights * perf
+
+        if len(self.models) == 0:
+            getLogger(__name__).error("No models are set")
+            raise ValueError("No models are set")
+
+        model_scores = np.vectorize(
+            lambda model: __get_weights(model),
+            signature='()->(1)')(self.models)
+        getLogger(__name__).info("Weighted model scores: %s", model_scores)
+        self.feature_scores_ = model_scores.sum(axis=0)
+        getLogger(__name__).info("Feature scores: %s", self.feature_scores_)
+        self.selected_features_ = apply_cr(self.cutting_rule)(
+            self.feature_scores_)
diff --git a/ITMO_FS/ensembles/ranking_based/Mixed.py b/ITMO_FS/ensembles/ranking_based/Mixed.py
index 01016d99..08bbacca 100644
--- a/ITMO_FS/ensembles/ranking_based/Mixed.py
+++ b/ITMO_FS/ensembles/ranking_based/Mixed.py
@@ -1,42 +1,70 @@
+from logging import getLogger
+
+import numpy as np
+
 from .fusion_functions import *
+from ...utils import BaseTransformer
 
 
-class Mixed:
-    """
-    Performs feature selection based on several filters, selecting features this way:
-    Get ranks from every filter from input. Then loops through, on every iteration=i 
-    selects features on i position on every filter then shuffles them, then adds to result list
-    without duplication, continues until specified number of features
-    
+class Mixed(BaseTransformer):
+    """Perform feature selection based on several filters, selecting features
+    this way:
+        Get ranks from every filter from input.
+        Then loops through, on every iteration=i
+            selects features on i position on every filter
+            then shuffles them, then adds to result list without
+            duplication,
+        continues until specified number of features
+
     Parameters
     ----------
-    filters: list of filter functions
+    filters : collection
+        Collection of measure functions with signature measure(X, y) that
+        should return an array of importance values for each feature.
+    n_features : int
+        Amount of features to select.
+    fusion_function : callable
+        A function with signature (filter_ranks (array-like, shape
+        (n_filters, n_features), k (int)) that should return the indices of k
+        selected features based on the filter rankings.
 
     Examples
     --------
-    >>> from ITMO_FS.filters.univariate.measures import spearman_corr,pearson_corr
+    >>> from ITMO_FS.filters.univariate.measures import *
     >>> from ITMO_FS.ensembles.ranking_based.Mixed import Mixed
-    >>> from sklearn.datasets import make_classification
-    >>> x, y = make_classification(1000, 50, n_informative = 5, n_redundant = 3, n_repeated = 2, shuffle = True)
-    >>> mixed = Mixed([spearman_corr, pearson_corr])
-    >>> mixed.fit(x, y)
-    >>> mixed.transform(x, 20).shape
-    (1000, 20)
-    
+    >>> import numpy as np
+    >>> x = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1],
+    ... [3, 1, 4, 3, 1], [3, 1, 2, 3, 1]])
+    >>> y = np.array([1, 3, 2, 1, 2])
+    >>> mixed = Mixed([gini_index, chi2_measure], 2).fit(x, y)
+    >>> mixed.selected_features_
+    array([2, 4], dtype=int64)
     """
+    def __init__(self, filters, n_features,
+                 fusion_function=best_goes_first_fusion):
+        self.filters = filters
+        self.n_features = n_features
+        self.fusion_function = fusion_function
 
-    _filters = []
-
-    def __init__(self, filters):
-        self._filters = filters
-        self._selected_features = []
-        self._filter_results = []
+    def _fit(self, X, y):
+        """Fit the ensemble.
 
-    def fit(self, X, y, bagging=False):
-        self._filter_results = list(
-            map(lambda fn: sorted(dict(enumerate(fn(X, y))).items(), key=lambda kv: kv[1], reverse=True),
-                self._filters))  # call every filter on input data, then select k best for each of them
+        Parameters
+        ----------
+        X : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like, shape (n_samples,)
+            The target values.
 
-    def transform(self, X, k, fusion_function=best_goes_first_fusion):
-        self._selected_features = fusion_function(self._filter_results, k)
-        return X[:, self._selected_features]
+        Returns
+        -------
+        None
+        """
+        #TODO: some measures are 'lower is better', a simple argsort would not
+        #work there - need to call a different ranking function
+        self.filter_ranks_ = np.vectorize(
+            lambda f: np.argsort(f(X, y))[::-1],
+            signature='()->(1)')(self.filters)
+        getLogger(__name__).info("Filter ranks: %s", self.filter_ranks_)
+        self.selected_features_ = self.fusion_function(
+            self.filter_ranks_, self.n_features)
diff --git a/ITMO_FS/ensembles/ranking_based/fusion_functions.py b/ITMO_FS/ensembles/ranking_based/fusion_functions.py
index 39913e4d..a921f1e8 100644
--- a/ITMO_FS/ensembles/ranking_based/fusion_functions.py
+++ b/ITMO_FS/ensembles/ranking_based/fusion_functions.py
@@ -1,23 +1,49 @@
 import random
+import numpy as np
 
 
-def best_goes_first_fusion(filter_results, k):
-    result = []
+def best_goes_first_fusion(filter_ranks, k):
+    """
+        Fusion function mixes filter results according feature appearance in
+        range of each filter. Selects first k of them.
+
+    Parameters
+    ----------
+    filter_ranks : array-like, shape (n_filters, n_features)
+        Feature ranking for all filters.
+    k : int
+        Amount of features to select.
+
+    Returns
+    -------
+    array-like, shape (k,) : selected features
+    """
+    result = np.array([], dtype='int')
     place = 0
-    filter_results = [[r[0] for r in result] for result in filter_results]
-    while (len(result) < k) and (place < k):
-        placed_features = list(map(list, zip(*filter_results)))[
-            place]  # take only features at index=place in filter array
+    while len(result) < k:
+        placed_features = np.setdiff1d(filter_ranks[:, place], result)
         random.shuffle(placed_features)
-        [result.append(z) for z in list(set(placed_features)) if z not in result]
+        result = np.append(result, placed_features)
         place += 1
     return result[:k]
 
 
-def borda_fusion(filter_results, k):
-    filter_results = [{r[0]: p for r, p in zip(result, range(1, len(result) + 1))} for result in filter_results]
-    result = filter_results[0]
-    for filter_ in filter_results[1:]:
-        for key in filter_.keys():
-            result[key] += filter_[key]
-    return list(dict(sorted(result.items(), key=lambda kv: kv[1], reverse=True)).keys())[:k]
+def borda_fusion(filter_ranks, k):
+    """Select features according to borda function.
+
+    Parameters
+    ----------
+    filter_ranks : array-like, shape (n_filters, n_features)
+        Feature ranking for all filters.
+    k : int
+        Amount of features to select.
+
+    Returns
+    -------
+    array-like, shape (k,) : selected features
+    """
+    n_features = filter_ranks.shape[1]
+    scores = np.zeros(n_features)
+    for f in filter_ranks:
+        scores[f] += np.arange(1, n_features + 1)
+    return np.argsort(scores)[:k]
diff --git a/ITMO_FS/filters/__init__.py b/ITMO_FS/filters/__init__.py
index 7770b15d..ef255d61 100644
--- a/ITMO_FS/filters/__init__.py
+++ b/ITMO_FS/filters/__init__.py
@@ -1,4 +1,3 @@
 from .multivariate import *
 from .univariate import *
 from .unsupervised import *
-from .sparse import *
diff --git a/ITMO_FS/filters/multivariate/DISRwithMassive.py b/ITMO_FS/filters/multivariate/DISRwithMassive.py
index 0e0a2cc1..93cb2fa3 100644
--- a/ITMO_FS/filters/multivariate/DISRwithMassive.py
+++ b/ITMO_FS/filters/multivariate/DISRwithMassive.py
@@ -1,162 +1,124 @@
+from logging import getLogger
+
 import numpy as np
+from sklearn.metrics import pairwise_distances
+
+from ...utils import BaseTransformer, generate_features
+from ...utils.information_theory import (entropy, joint_entropy,
+                                         mutual_information)
 
-from ITMO_FS.utils.information_theory import entropy
-from ITMO_FS.utils.information_theory import mutual_information
-from ...utils import DataChecker, generate_features
 
 def _complementarity(x_i, x_j, y):
-    return entropy(x_i) + entropy(x_j) + entropy(y) - entropy(list(zip(x_i, x_j))) - \
-           entropy(list(zip(x_i, y))) - entropy(list(zip(x_j, y))) + entropy(list(zip(x_i, x_j, y)))
+    return (entropy(x_i) + entropy(x_j) + entropy(y) - joint_entropy(x_i, x_j)
+            - joint_entropy(x_i, y) - joint_entropy(x_j, y)
+            + joint_entropy(x_i, x_j, y))
 
 
 def _chained_information(x_i, x_j, y):
-    return mutual_information(x_i, y) + mutual_information(x_j, y) + _complementarity(x_i, x_j, y)
-
-class DISRWithMassive(DataChecker):
+    return (mutual_information(x_i, y) + mutual_information(x_j, y)
+            + _complementarity(x_i, x_j, y))
+
+
+class DISRWithMassive(BaseTransformer):
+    """Create DISR (Double Input Symmetric Relevance) feature selection filter
+    based on kASSI criterin for feature selection which aims at maximizing the
+    mutual information avoiding, meanwhile, large multivariate density
+    estimation. Its a kASSI criterion with approximation of the information of
+    a set of variables by counting average information of subset on combination
+    of two features. This formulation thus deals with feature complementarity
+    up to order two by preserving the same computational complexity of the
+    MRMR and CMIM criteria The DISR calculation is done using graph based
+    solution.
+
+    Parameters
+    ----------
+    n_features : int
+        Number of features to select.
+
+    Notes
+    -----
+    For more details see `this paper
+    <http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.318.6576&rep=rep1&type=pdf/>`_.
+
+    Examples
+    --------
+    >>> from ITMO_FS.filters.multivariate import DISRWithMassive
+    >>> import numpy as np
+    >>> X = np.array([[1, 2, 3, 3, 1], [2, 2, 3, 3, 2], [1, 3, 3, 1, 3],
+    ... [3, 1, 3, 1, 4], [4, 4, 3, 1, 5]])
+    >>> y = np.array([1, 2, 3, 4, 5])
+    >>> disr = DISRWithMassive(3).fit(X, y)
+    >>> disr.selected_features_
+    array([0, 1, 4], dtype=int64)
     """
-        Creates DISR (Double Input Symmetric Relevance) feature selection filter
-        based on kASSI criterin for feature selection
-        which aims at maximizing the mutual information avoiding, meanwhile, large multivariate density estimation.
-        Its a kASSI criterion with approximation of the information of a set of variables
-        by counting average information of subset on combination of two features.
-        This formulation thus deals with feature complementarity up to order two
-        by preserving the same computational complexity of the MRMR and CMIM criteria
-        The DISR calculation is done using graph based solution.
+    def __init__(self, n_features):
+        self.n_features = n_features
+
+    def _fit(self, x, y):
+        """Fit the filter.
 
         Parameters
         ----------
-        expected_size : int
-            Expected size of subset of features.
-
-        Notes
-        -----
-        For more details see `this paper
-        <http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.318.6576&rep=rep1&type=pdf/>`_.
-
-        Examples
-        --------
-        >>> from ITMO_FS.filters.multivariate import DISRWithMassive
-        >>> import numpy as np
-        >>> X = np.array([[1, 2, 3, 3, 1],[2, 2, 3, 3, 2], [1, 3, 3, 1, 3],\
-[3, 1, 3, 1, 4],[4, 4, 3, 1, 5]], dtype = np.integer)
-        >>> y = np.array([1, 2, 3, 4, 5], dtype=np.integer)
-        >>> disr = DISRWithMassive(3)
-        >>> disr.fit_transform(X, y)
-        array([[1, 2, 1],
-               [2, 2, 2],
-               [1, 3, 3],
-               [3, 1, 4],
-               [4, 4, 5]])
-    """
-
-    def __init__(self, expected_size=None):
-        self.expected_size = expected_size
-        self.n_features = None
-        self._vertices = None
-        self._edges = None
-        self.selected_features = None
-
-    def __count_weight(self, i):
-        return np.sum(2 * self._vertices[i] * np.multiply(self._edges[i], self._vertices))
-
-    def fit(self, X, y, feature_names=None):
-        """
-            Fits filter
-
-            Parameters
-            ----------
-            X : array-like, shape (n_samples, n_features)
-                The training input samples.
-            y : array-like, shape (n_samples, )
-                The target values.
-            feature_names : list of strings, optional
-                In case you want to define feature names
-
-            Returns
-            -------
-            None
+        x : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like, shape (n_samples,)
+            The target values.
+
+        Returns
+        -------
+        None
         """
-        
-        features = generate_features(X)
-        X, y, feature_names = self._check_input(X, y, feature_names)
-        self.feature_names = dict(zip(features, feature_names))
-        self.n_features = X.shape[1]
-        if self.expected_size is None:
-            self.expected_size = self.n_features // 3
-        free_features = np.array([], dtype='object')
-        self.selected_features = generate_features(X)
-        self._vertices = np.ones(self.n_features)
-        self._edges = np.zeros((self.n_features, self.n_features))
-        for i in range(self.n_features):
-            for j in range(self.n_features):
-                entropy_pair = entropy(list(zip(X[:, i], X[:, j])))
-                if entropy_pair != 0.:
-                    self._edges[i][j] = _chained_information(X[:, i], X[:, j], y) / entropy_pair
-
-        # TODO apply vectorize to selected_features and not arange(n_features)?
-        while self.selected_features.size != self.expected_size:
-            min_index = np.argmin(np.vectorize(lambda x: self.__count_weight(x))(self.selected_features))
-            self._vertices[min_index] = 0
-            free_features = np.append(free_features, min_index)
-            self.selected_features = np.delete(self.selected_features, min_index)
-
-        change = True
-        while change:
-            change = False
-            swap_index = (-1, -1)
-            max_difference = 0
-            for i in range(len(free_features)):
-                for j in range(len(self.selected_features)):
-                    temp_difference = self.__count_weight(free_features[i]) - self.__count_weight(
-                        self.selected_features[j])
-                    if temp_difference > max_difference:
-                        max_difference = temp_difference
-                        swap_index = (i, j)
-            if max_difference > 0:
-                change = True
-                new_selected, new_free = swap_index
-                free_features = np.append(free_features, new_free)
-                free_features = np.delete(free_features, new_selected)
-                self.selected_features = np.append(self.selected_features, new_selected)
-                self.selected_features = np.delete(self.selected_features, new_free)
-        self.selected_features = features[self.selected_features]
-
-    def transform(self, X):
-        """
-            Transform given data by slicing it with selected features.
-
-            Parameters
-            ----------
-            X : array-like, shape (n_samples, n_features)
-                The training input samples.
-
-            Returns
-            -------
-            Transformed 2D numpy array
-        """
-
-        if type(X) is np.ndarray:
-            return X[:, self.selected_features]
-        else:
-            return X[self.selected_features]
-
-    def fit_transform(self, X, y, feature_names=None):
-        """
-            Fits the filter and transforms given dataset X.
-
-            Parameters
-            ----------
-            X : array-like, shape (n_features, n_samples)
-                The training input samples.
-            y : array-like, shape (n_samples, )
-                The target values.
-            feature_names : list of strings, optional
-                In case you want to define feature names
-
-            Returns
-            -------
-            X dataset sliced with features selected by the filter
-        """
-
-        self.fit(X, y, feature_names)
-        return self.transform(X)
+        free_features = np.array([], dtype='int')
+        self.selected_features_ = generate_features(x)
+        self._edges = pairwise_distances(
+            x.T, x.T, lambda xi, xj: (_chained_information(xi, xj, y)
+                                      / (joint_entropy(xi, xj) + 1e-15)))
+        np.fill_diagonal(self._edges, 0)
+        getLogger(__name__).info("Graph weights: %s", self._edges)
+    
+        while len(self.selected_features_) != self.n_features:
+            min_index = np.argmin(
+                np.sum(self._edges[np.ix_(self.selected_features_,
+                                          self.selected_features_)], axis=0))
+            getLogger(__name__).info(
+                "Removing feature %d from selected set",
+                self.selected_features_[min_index])
+            free_features = np.append(
+                free_features, self.selected_features_[min_index])
+            self.selected_features_ = np.delete(
+                self.selected_features_, min_index)
+
+        getLogger(__name__).info(
+            "Selected set: %s, free set: %s", self.selected_features_,
+            free_features)
+
+        while True:
+            selected_weights = np.sum(
+                self._edges[np.ix_(self.selected_features_,
+                                   self.selected_features_)], axis=0)
+            getLogger(__name__).info(
+                "Graph of selected set: %s", selected_weights)
+
+            free_weights = np.sum(self._edges[np.ix_(self.selected_features_,
+                                                     free_features)], axis=0)
+            getLogger(__name__).info(
+                "Free weights that would be added: %s", free_weights)
+
+            difference = (
+                free_weights.reshape(-1, 1)
+                - self._edges[np.ix_(free_features, self.selected_features_)]
+                - selected_weights)
+            getLogger(__name__).info("Difference matrix: %s", difference)
+
+            if np.all(difference <= 0):
+                getLogger(__name__).info(
+                    "All differences are non-positive, terminating")
+                break
+            index_add, index_del = np.unravel_index(
+                np.argmax(difference), difference.shape)
+            getLogger(__name__).info(
+                "Maximum difference found at index (%d, %d), swapping those "
+                "features", index_add, index_del)
+
+            self.selected_features_[index_del], free_features[index_add] = (
+                free_features[index_add], self.selected_features_[index_del])
diff --git a/ITMO_FS/filters/multivariate/FCBF.py b/ITMO_FS/filters/multivariate/FCBF.py
index fc5f8ec3..319e800c 100644
--- a/ITMO_FS/filters/multivariate/FCBF.py
+++ b/ITMO_FS/filters/multivariate/FCBF.py
@@ -1,115 +1,94 @@
-import numpy as np
-
-from ...utils.information_theory import matrix_mutual_information
-from ...utils import DataChecker, generate_features
+from logging import getLogger
 
+import numpy as np
 
-class FCBFDiscreteFilter(DataChecker):
+from ...utils import BaseTransformer, generate_features
+from ...utils.information_theory import entropy, conditional_entropy
+
+
+class FCBFDiscreteFilter(BaseTransformer):
+    """Create FCBF (Fast Correlation Based filter) feature selection filter
+    based on mutual information criteria for data with discrete features. This
+    filter finds best set of features by searching for a feature, which
+    provides the most information about classification problem on given dataset
+    at each step and then eliminating features which are less relevant than
+    redundant.
+
+    Parameters
+    ----------
+    delta : float
+        Symmetric uncertainty value threshold.
+
+    Notes
+    -----
+    For more details see `this paper
+    <https://www.aaai.org/Papers/ICML/2003/ICML03-111.pdf/>`_.
+
+    Examples
+    --------
+    >>> from ITMO_FS.filters.multivariate import FCBFDiscreteFilter
+    >>> import numpy as np
+    >>> X = np.array([[1, 2, 3, 3, 1], [2, 2, 3, 3, 2], [1, 3, 3, 1, 3],
+    ... [3, 1, 3, 1, 4], [4, 4, 3, 1, 5]])
+    >>> y = np.array([1, 2, 3, 4, 5])
+    >>> fcbf = FCBFDiscreteFilter().fit(X, y)
+    >>> fcbf.selected_features_
+    array([4], dtype=int64)
     """
-        Creates FCBF (Fast Correlation Based filter) feature selection filter
-        based on mutual information criteria for data with discrete features
-        This filter finds best set of features by searching for a feature, which provides
-        the most information about classification problem on given dataset at each step
-        and then eliminating features which are less relevant than redundant
+    def __init__(self, delta=0.1):
+        self.delta = delta
+
+    def _fit(self, x, y):
+        """Fit the filter.
 
         Parameters
         ----------
-
-        Notes
-        -----
-        For more details see `this paper <https://www.aaai.org/Papers/ICML/2003/ICML03-111.pdf/>`_.
-
-        Examples
-        --------
-        >>> from ITMO_FS.filters.multivariate import FCBFDiscreteFilter
-        >>> import numpy as np
-        >>> X = np.array([[1, 2, 3, 3, 1],[2, 2, 3, 3, 2], [1, 3, 3, 1, 3],[3, 1, 3, 1, 4],[4, 4, 3, 1, 5]], dtype = np.integer)
-        >>> y = np.array([1, 2, 3, 4, 5], dtype=np.integer)
-        >>> fcbf = FCBFDiscreteFilter()
-        >>> fcbf.fit_transform(X, y)
-        array([[1],
-               [2],
-               [3],
-               [4],
-               [5]])
-    """
-
-    def __init__(self):
-        self.selected_features = None
-
-    def fit(self, X, y, feature_names=None):
-        """
-            Fits filter
-
-            Parameters
-            ----------
-            X : array-like, shape (n_samples, n_features)
-                The training input samples.
-            y : array-like, shape (n_samples, )
-                The target values.
-            feature_names : list of strings, optional
-                In case you want to define feature names
-
-            Returns
-            -------
-            None
-        """
-
-        features = generate_features(X)
-        X, y, feature_names = self._check_input(X, y, feature_names)
-        free_features = generate_features(X)
-        self.feature_names = dict(zip(features, feature_names))
-        self.selected_features = np.array([], dtype='int')
-        # TODO Add exit of the loop when all differences are positive and are not updated
-        #  (e.g. it happens when we get same max_index twice).
-        max_index = -1
-        while len(free_features) != 0:
-            if max_index == np.argmax(matrix_mutual_information(X[:, free_features], y)):
-                break
-            max_index = np.argmax(matrix_mutual_information(X[:, free_features], y))
-            self.selected_features = np.append(self.selected_features, max_index)
-            relevance = matrix_mutual_information(X[:, free_features], y)
-            redundancy = matrix_mutual_information(X[:, free_features], X[:, max_index])
-            difference = relevance - redundancy
-            free_features = np.delete(free_features, np.where(difference <= 0.)[0])
-        self.selected_features = features[self.selected_features]
-
-    def transform(self, X):
-        """
-            Transform given data by slicing it with selected features.
-
-            Parameters
-            ----------
-            X : array-like, shape (n_samples, n_features)
-                The training input samples.
-
-            Returns
-            -------
-            Transformed 2D numpy array
-        """
-
-        if type(X) is np.ndarray:
-            return X[:, self.selected_features]
-        else:
-            return X[self.selected_features]
-
-    def fit_transform(self, X, y, feature_names=None):
-        """
-            Fits the filter and transforms given dataset X.
-
-            Parameters
-            ----------
-            X : array-like, shape (n_features, n_samples)
-                The training input samples.
-            y : array-like, shape (n_samples, )
-                The target values.
-            feature_names : list of strings, optional
-                In case you want to define feature names
-
-            Returns
-            -------
-            X dataset sliced with features selected by the filter
+        x : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like, shape (n_samples,)
+            The target values.
+
+        Returns
+        -------
+        None
         """
-        
-        self.fit(X, y, feature_names)
-        return self.transform(X)
+        def __SU(x, y, entropy_y):
+            entropy_x = entropy(x)
+            return 2 * ((entropy_x - conditional_entropy(y, x))
+                        / (entropy_x + entropy_y))
+
+        free_features = generate_features(x)
+        self.selected_features_ = np.array([], dtype='int')
+        entropy_y = entropy(y)
+        getLogger(__name__).info("Entropy of y: %f", entropy_y)
+
+        su_class = np.apply_along_axis(__SU, 0, x, y, entropy_y)
+        getLogger(__name__).info("SU values against y: %s", su_class)
+        self.selected_features_ = np.argsort(su_class)[::-1][:
+            np.count_nonzero(su_class > self.delta)]
+        getLogger(__name__).info("Selected set: %s", self.selected_features_)
+
+        index = 1
+        while index < self.selected_features_.shape[0]:
+            feature = self.selected_features_[index - 1]
+            getLogger(__name__).info("Leading feature: %d", feature)
+            entropy_feature = entropy(x[:, feature])
+            getLogger(__name__).info(
+                "Leading feature entropy: %f", entropy_feature)
+            su_classes = su_class[self.selected_features_[index:]]
+            getLogger(__name__).info(
+                "SU values against y for the remaining features: %s",
+                su_classes)
+            su_feature = np.apply_along_axis(
+                __SU, 0, x[:, self.selected_features_[index:]], x[:, feature],
+                entropy_feature)
+            getLogger(__name__).info(
+                "SU values against leading feature for the remaining features: "
+                "%s", su_feature)
+            to_delete = np.flatnonzero(su_feature >= su_classes) + index
+            getLogger(__name__).info(
+                "Deleting those features from the selected set: %s",
+                self.selected_features_[to_delete])
+            self.selected_features_ = np.delete(
+                self.selected_features_, to_delete)
+            index += 1
diff --git a/ITMO_FS/filters/multivariate/MultivariateFilter.py b/ITMO_FS/filters/multivariate/MultivariateFilter.py
index 258da308..e86fc679 100644
--- a/ITMO_FS/filters/multivariate/MultivariateFilter.py
+++ b/ITMO_FS/filters/multivariate/MultivariateFilter.py
@@ -1,138 +1,118 @@
+from logging import getLogger
+
 import numpy as np
 from sklearn.base import TransformerMixin
 
-from .measures import GLOB_MEASURE
-from ...utils import DataChecker, generate_features
-
-# TODO Test interface!!!!
-class MultivariateFilter(TransformerMixin, DataChecker):
+from .measures import (MEASURE_NAMES, mutual_information,
+                       matrix_mutual_information)
+from ...utils import BaseTransformer, generate_features
+
+
+class MultivariateFilter(BaseTransformer):
+    """Provides basic functionality for multivariate filters.
+
+    Parameters
+    ----------
+    measure : string or callable
+        A metric name defined in GLOB_MEASURE or a callable with signature
+        measure(selected_features, free_features, dataset, labels) which
+        should return a list of metric values for each feature in the dataset.
+    n_features : int
+        Number of features to select.
+    beta : float, optional
+        Initialize only in case you run MIFS or generalizedCriteria metrics.
+    gamma : float, optional
+        Initialize only in case you run generalizedCriteria metric.
+
+    See Also
+    --------
+
+    Examples
+    --------
+    >>> from ITMO_FS.filters.multivariate import MultivariateFilter
+    >>> from sklearn.preprocessing import KBinsDiscretizer
+    >>> import numpy as np
+    >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
+    >>> x = np.array([[1, 2, 3, 3, 1], [2, 2, 3, 3, 2], [1, 3, 3, 1, 3],
+    ... [3, 1, 3, 1, 4], [4, 4, 3, 1, 5]])
+    >>> y = np.array([1, 2, 3, 4, 5])
+    >>> data = est.fit_transform(x)
+    >>> model = MultivariateFilter('JMI', 3).fit(x, y)
+    >>> model.selected_features_
+    array([4, 0, 1], dtype=int64)
     """
-        Provides basic functionality for multivariate filters.
+    def __init__(self, measure, n_features, beta=None, gamma=None):
+        self.measure = measure
+        self.n_features = n_features
+        self.beta = beta
+        self.gamma = gamma
+
+    def _fit(self, X, y, **kwargs):
+        """Fit the filter.
 
         Parameters
         ----------
-        measure : string or callable
-            A metric name defined in GLOB_MEASURE or a callable with signature measure(selected_features, free_features, dataset, labels)
-            which should return a list of metric values for each feature in the dataset.
-        n_features : int
-            Number of features to select.
-        beta : float, optional
-            Initialize only in case you run MIFS or generalizedCriteria metrics.
-        gamma : float, optional
-            Initialize only in case you run generalizedCriteria metric.
-        
-        See Also
-        --------
-        
-        Examples
-        --------
-        >>> from ITMO_FS.filters.multivariate import MultivariateFilter
-        >>> from sklearn.preprocessing import KBinsDiscretizer
-        >>> import numpy as np
-
-        >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
-        >>> X = np.array([[1, 2, 3, 3, 1],[2, 2, 3, 3, 2], [1, 3, 3, 1, 3],\
-[3, 1, 3, 1, 4],[4, 4, 3, 1, 5]], dtype = np.integer)
-        >>> y = np.array([1, 2, 3, 4, 5], dtype=np.integer)
-        >>> est.fit(X)
-        KBinsDiscretizer(encode='ordinal', n_bins=10)
-        >>> data = est.transform(X)
-        >>> model = MultivariateFilter('MIM', 3)
-        >>> model.fit(X, y)
-        >>> model.selected_features
-        array([4, 0, 1])
-    """
-
-    def __init__(self, measure, n_features, beta=None, gamma=None):
-        if type(measure) is str:
+        X : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like, shape (n_samples,)
+            The target values.
+        **kwargs
+
+        Returns
+        -------
+        None
+        """
+        if isinstance(self.measure, str):
             try:
-                self.measure = GLOB_MEASURE[measure]
+                measure = MEASURE_NAMES[self.measure]
             except KeyError:
-                raise KeyError("No %r measure yet" % measure)
-        else:
-            self.measure = measure
-        self.__n_features = n_features
-        self.selected_features = np.array([], dtype='int')
-        self.beta = beta
-        self.gamma = gamma
+                getLogger(__name__).error("No %r measure yet", self.measure)
+                raise KeyError("No %r measure yet" % self.measure)
 
-    def fit(self, X, y, feature_names=None):
-        """
-            Fits the filter.
+        getLogger(__name__).info(
+            "Using MultivariateFilter with %s measure", measure)
+        free_features = generate_features(X)
+        self.selected_features_ = np.array([], dtype='int')
 
-            Parameters
-            ----------
-            X : array-like, shape (n_samples, n_features)
-                The training input samples.
-            y : array-like, shape (n_samples, )
-                The target values.
-            feature_names : list of strings, optional
-                In case you want to define feature names
+        relevance = np.apply_along_axis(
+            mutual_information, 0, X[:, free_features], y)
+        getLogger(__name__).info("Relevance vector: %s", relevance)
 
-            Returns
-            ------
-            None
-        """
+        redundancy = np.vectorize(
+            lambda free_feature: matrix_mutual_information(
+                X[:, free_features], X[:, free_feature]),
+            signature='()->(1)')(free_features)
+        getLogger(__name__).info("Redundancy vector: %s", redundancy)
 
-        features = generate_features(X)
-        X, y, feature_names = self._check_input(X, y, feature_names)
-        if self.__n_features > X.shape[1]:
-            raise ValueError("Cannot select %d features out of %d" % (self.__n_features, X.shape[1]))
-        free_features = generate_features(X)
-        self.feature_names = dict(zip(features, feature_names))
-        while len(self.selected_features) != self.__n_features:
+        while len(self.selected_features_) != self.n_features:
             if self.beta is None:
-                values = self.measure(self.selected_features, free_features, X, y)
+                values = measure(
+                    self.selected_features_, free_features, X, y,
+                    relevance=relevance[free_features],
+                    redundancy=np.sum(
+                        redundancy[self.selected_features_],
+                        axis=0)[free_features])
             else:
                 if self.gamma is not None:
-                    values = self.measure(self.selected_features, free_features, X, y, self.beta, self.gamma)
+                    values = measure(
+                        self.selected_features_, free_features, X, y, self.beta,
+                        self.gamma, relevance=relevance[free_features],
+                        redundancy=np.sum(
+                            redundancy[self.selected_features_],
+                            axis=0)[free_features])
                 else:
-                    values = self.measure(self.selected_features, free_features, X, y, self.beta)
+                    values = measure(
+                        self.selected_features_,free_features, X, y, self.beta,
+                        relevance=relevance[free_features],
+                        redundancy=np.sum(
+                            redundancy[self.selected_features_],
+                            axis=0)[free_features])
+
+            getLogger(__name__).info("Free features: %s", free_features)
+            getLogger(__name__).info("Measure values: %s", values)
             to_add = np.argmax(values)
-            self.selected_features = np.append(self.selected_features, free_features[to_add])
+            getLogger(__name__).info(
+                "Adding feature %d to the selected set", free_features[to_add])
+            self.selected_features_ = np.append(
+                self.selected_features_, free_features[to_add])
             free_features = np.delete(free_features, to_add)
-        self.selected_features = features[self.selected_features]
-
-    def transform(self, X):
-        """
-            Transform given data by slicing it with selected features.
-
-            Parameters
-            ----------
-            X : array-like, shape (n_samples, n_features)
-                The training input samples.
-
-            Returns
-            ------
-
-            Transformed 2D numpy array
-        """
-
-        if type(X) is np.ndarray:
-            return X[:, self.selected_features]
-        else:
-            return X[self.selected_features]
-
-    def fit_transform(self, X, y=None, feature_names=None, **fit_params):
-        """
-            Fits the filter and transforms given dataset X.
-
-            Parameters
-            ----------
-            X : array-like, shape (n_features, n_samples)
-                The training input samples.
-            y : array-like, shape (n_samples, ), optional
-                The target values.
-            feature_names : list of strings, optional
-                In case you want to define feature names
-            **fit_params :
-                dictonary of measure parameter if needed.
-
-            Returns
-            ------
-
-            X dataset sliced with features selected by the filter
-        """
-
-        self.fit(X, y, feature_names)
-        return self.transform(X)
diff --git a/ITMO_FS/filters/multivariate/STIR.py b/ITMO_FS/filters/multivariate/STIR.py
index 3f78fa8b..811f4796 100644
--- a/ITMO_FS/filters/multivariate/STIR.py
+++ b/ITMO_FS/filters/multivariate/STIR.py
@@ -1,229 +1,106 @@
+from logging import getLogger
+
 import numpy as np
 from sklearn.metrics import pairwise_distances
-from ...utils import generate_features, DataChecker
-
-
-class STIR(DataChecker):
+from sklearn.preprocessing import MinMaxScaler
+
+from ...utils import knn_from_class, BaseTransformer
+
+
+class STIR(BaseTransformer):
+    """Feature selection using STIR algorithm.
+
+    Parameters
+    ----------
+    n_features : int
+        Number of features to select.
+    metric : str or callable
+        Distance metric to use in kNN. If str, should be one of the standard
+        distance metrics (e.g. 'euclidean' or 'manhattan'). If callable, should
+        have the signature metric(x1 (array-like, shape (n,)), x2 (array-like,
+        shape (n,))) that should return the distance between two vectors.
+    k : int
+        Number of constant nearest hits/misses.
+
+    Notes
+    -----
+    For more details see `this paper <https://academic.oup.com/bioinformatics/article/35/8/1358/5100883>`_.
+
+    Examples
+    --------
+    >>> from ITMO_FS.filters.multivariate import STIR
+    >>> import numpy as np
+    >>> X = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1],
+    ... [3, 1, 4, 3, 1], [3, 1, 2, 3, 1]])
+    >>> y = np.array([1, 2, 2, 1, 2])
+    >>> model = STIR(2).fit(X, y)
+    >>> model.selected_features_
+    array([2, 0], dtype=int64)
     """
-        Feature selection using STIR algorithm.
-
-        Algorithm taken from paper:
-
-        STatistical Inference Relief (STIR) feature selection
-        (https://academic.oup.com/bioinformatics/article/35/8/1358/5100883).
-    """
-
-    def __init__(self, n_features_to_keep=10):
-        """
-            Sets up STIR to perform feature selection.
-        """
-
-        self.n_features_to_keep = n_features_to_keep
-        self.feature_scores = None
-        self.top_features = None
-
-    def max_diff(self, X):
-        """
-            Computes max difference in each column.
-
-            Parameters
-            ----------
-            X : array-like, shape (n_samples, n_features)
-                matrix to compute column difference of.
-
-            Returns
-            -------
-            diff_vector : array-like, shape (n_features)
-                column difference vector.
-        """
-
-        diff_vector = np.max(X, axis=0) - np.min(X, axis=0)
-
-        return diff_vector
-
-    def distance_matrix(self, X):
-        """
-            Computes the distance matrix.
-
-            Before calculating distance we center
-            matrix and normalize it.
-
-            Parameters
-            ----------
-            X : array-like, shape (n_samples, n_features) 
-                matrix to compute column difference of.
-
-            Returns
-            -------
-            X_distances : array-like, shape (n_samples, n_samples)
-                distance matrix.
-        """
-
-        #max_diff_vec = self.max_diff(X) # TODO: probably should convert to float64 and add division by zero check
-        max_diff_vec = self.max_diff(X).astype('float64')
-        min_vec = X.min(axis=0)
-
-        X_centered = X - min_vec
-        max_diff_vec[max_diff_vec == 0] = 1e-14
-        X_scaled = X_centered / max_diff_vec
-        X_distances = pairwise_distances(X_scaled)
-
-        return X_distances
-
-    def find_neighbors(self, X, y, k=1):
-        """
-            Finds the nearest hit/miss matrices.
-
-            Parameters
-            ----------
-            X : array-like, shape (n_samples, n_features)
-                matrix to compute neighbors of.
-            y : array-like, shape (n_samples, )
-                vector of binary class status (usually -1/1).
-            k : int, optional
-                number of constant nearest hits/misses.
-
-            Returns
-            -------
-            hitmiss : array-like, shape (2, )
-                hitmiss[1] (hits) and hitmiss[2] (misses). 
-                Each list has two columns: index is the first column (instances) 
-                in both lists. The second column is hit_index (nearest hits for 
-                the first column instance) for list [1] and miss_index 
-                (nearest misses) for list [2].
+    def __init__(self, n_features, metric='manhattan', k=1):
+        self.n_features = n_features
+        self.metric = metric
+        self.k = k
+
+    def _fit(self, X, y):
+        """Fit the filter.
+
+        Parameters
+        ----------
+        X : array-like, shape (n_samples, n_features)
+            The input samples.
+        y : array-like, shape (n_samples,)
+            The classes for the samples.
+
+        Returns
+        -------
+        None
         """
-
-        X_distances = self.distance_matrix(X)
-        num_samples = X.shape[0]
-
-        indexes = []
-        hits = []
-        misses = []
-
-        for i in range(num_samples):
-            distances = X_distances[i]
-            nearest = np.argsort(distances)
-            nearest_matrix = np.column_stack((nearest, [y[j] for j in nearest]))
-            nearest_hits = np.array(list(filter(lambda row: row[1] == nearest_matrix[0, 1],
-                                                nearest_matrix)))
-            nearest_misses = np.array(list(filter(lambda row: row[1] != nearest_matrix[0, 1],
-                                                  nearest_matrix)))
-            k_nearest_hits = [row[0] for row in nearest_hits[1: (k + 1)]]
-            k_nearest_misses = [row[0] for row in nearest_misses[:k]]
-
-            indexes += [i for j in range(k)]
-            hits += k_nearest_hits
-            misses += k_nearest_misses
-
-        # TODO: this sometimes fails when all class labels are different or when k != 1 is used
-        hitmiss = np.array([np.column_stack((indexes, hits)),
-                            np.column_stack((indexes, misses))])
-
-        return hitmiss
-
-    def fit(self, X, y, feature_names=None, k=1):
-        """
-            Computes the feature importance scores from the training data.
-
-            Parameters
-            ----------
-            X : array-like, shape (n_samples, n_features) 
-                Training instances to compute the feature importance scores from.
-            y : array-like, shape (n_samples, )
-                Training labels.
-            feature_names : list of strings, optional
-                In case you want to define feature names
-            k : int, optional
-                number of constant nearest hits/misses.
-
-            Returns
-            -------
-            None
-        """
-
-        features = generate_features(X)
-        X, y, feature_names = self._check_input(X, y, feature_names)
-        new_features = generate_features(X)
-        self.feature_names = dict(zip(features, feature_names))
         n_samples = X.shape[0]
-        n_features = X.shape[1]
-        weights = np.zeros(n_features)
-        neighbors_index = self.find_neighbors(X, y, k)
-        #range_vec = np.array(self.max_diff(X)) # TODO: probably should convert to float64 and add division by zero check
-        range_vec = np.array(self.max_diff(X)).astype('float64')
-        range_vec[range_vec == 0] = 1e-14
-        one_over_range = 1 / range_vec
-        one_over_m = 1 / n_samples
-
-        hit_index, hits = np.hsplit(neighbors_index[0], 2)
-        miss_index, misses = np.hsplit(neighbors_index[1], 2)
-
-        hit_index = hit_index.flatten()
-        hits = hits.flatten()
-        miss_index = miss_index.flatten()
-        misses = misses.flatten()
-
-        for feature_index in new_features:
-            attr_values = X.T[feature_index]
-
-            hit_neighbors = np.array([attr_values[i] for i in hits])
-            miss_neighbors = np.array([attr_values[i] for i in misses])
-
-            hit_values = np.array([attr_values[i] for i in hit_index])
-            miss_values = np.array([attr_values[i] for i in miss_index])
-
-            attr_diff_hits = np.abs(hit_neighbors - hit_values) * one_over_range[feature_index]
-            attr_diff_misses = np.abs(miss_neighbors - miss_values) * one_over_range[feature_index]
-
-            mu_hits = np.sum(attr_diff_hits) / n_samples
-            mu_misses = np.sum(attr_diff_misses) / n_samples
-
-            weights[feature_index] = mu_misses - mu_hits
-
-        self.feature_scores = weights * one_over_m * 1000
-        self.selected_features = features[np.argsort(self.feature_scores)[::-1][:self.n_features_to_keep]]
-
-    def transform(self, X):
-        """
-            Reduces the feature set down to the top `n_features_to_keep` features.
-
-            Parameters
-            ----------
-            X : array-like, shape (n_samples, n_features) 
-                Feature matrix to perform feature selection on.
-
-            Returns
-            -------
-            Transformed 2D numpy array
-        """
-
-        if type(X) is np.ndarray:
-            return X[:, self.selected_features]
-        else:
-            return X[self.selected_features]
-
-    def fit_transform(self, X, y, feature_names=None, k=1):
-        """
-            Fits and transforms data.
-
-            Computes the feature importance scores from the training data, then
-            reduces the feature set down to the top 'n_features_to_keep' features.
-
-            Parameters
-            ----------
-            X : array-like, shape (n_samples, n_features) 
-                Training instances to compute the feature importance scores from.
-            y : array-like, shape (n_samples, )
-                Training labels.
-            feature_names : list of strings, optional
-                In case you want to define feature names
-            k : int, optional
-                number of constant nearest hits/misses.             
-
-            Returns
-            -------
-            Transformed 2D numpy array
-        """
+        classes, counts = np.unique(y, return_counts=True)
+
+        if np.any(counts <= self.k):
+            getLogger(__name__).error(
+                "Cannot select %d nearest neighbors because one of the classes "
+                "has less than %d samples", self.k, self.k + 1)
+            raise ValueError(
+                "Cannot select %d nearest neighbors because one of the classes "
+                "has less than %d samples" % (self.k, self.k + 1))
+
+        x_normalized = MinMaxScaler().fit_transform(X)
+        dm = pairwise_distances(x_normalized, x_normalized, self.metric)
+        getLogger(__name__).info("Distance matrix: %s", dm)
+
+        indices = np.arange(n_samples)
+        hits_diffs = np.abs(
+            np.vectorize(
+                lambda index: (
+                    x_normalized[index]
+                    - x_normalized[knn_from_class(
+                        dm, y, index, self.k, y[index])]),
+                signature='()->(n,m)')(indices))
+        getLogger(__name__).info("Hit differences matrix: %s", hits_diffs)
+        misses_diffs = np.abs(
+            np.vectorize(
+                lambda index: (
+                    x_normalized[index]
+                    - x_normalized[knn_from_class(
+                        dm, y, index, self.k, y[index], anyOtherClass=True)]),
+                signature='()->(n,m)')(indices))
+        getLogger(__name__).info("Miss differences matrix: %s", misses_diffs)
+
+        H = np.mean(hits_diffs, axis=(0,1))
+        getLogger(__name__).info("H: %s", H)
+        M = np.mean(misses_diffs, axis=(0,1))
+        getLogger(__name__).info("M: %s", M)
+        var_H = np.var(hits_diffs, axis=(0,1))
+        var_M = np.var(misses_diffs, axis=(0,1))
+
+        # the 1 / (1 / |M| + 1 / |H|) ^ (1/2) multiplier is constant, we omit it
+        self.feature_scores_ = (
+            (M - H) * np.sqrt(2 * self.k * n_samples - 2)
+            / (np.sqrt((self.k * n_samples - 1) * (var_H + var_M)) + 1e-15))
+        getLogger(__name__).info("Feature scores: %s", self.feature_scores_)
+        self.selected_features_ = np.argsort(self.feature_scores_)[::-1][
+            :self.n_features]
 
-        self.fit(X, y, feature_names, k)
-        return self.transform(X)
diff --git a/ITMO_FS/filters/multivariate/TraceRatioFisher.py b/ITMO_FS/filters/multivariate/TraceRatioFisher.py
index 5a9dbd3d..098de2cf 100644
--- a/ITMO_FS/filters/multivariate/TraceRatioFisher.py
+++ b/ITMO_FS/filters/multivariate/TraceRatioFisher.py
@@ -1,140 +1,88 @@
+from logging import getLogger
+
 import numpy as np
 from sklearn.metrics.pairwise import pairwise_distances
-from scipy.sparse import *
-from ...utils import DataChecker, generate_features
-
 
-# TODO requests changes for MultivariateFilter to be used there
-class TraceRatioFisher(DataChecker):
+from ...utils import BaseTransformer, generate_features
+
+class TraceRatioFisher(BaseTransformer):
+    """Creates TraceRatio(similarity based) feature selection filter
+    performed in supervised way, i.e. fisher version
+
+    Parameters
+    ----------
+    n_features : int
+        Number of features to select.
+    epsilon : float
+        Lambda change threshold.
+
+    Notes
+    -----
+    For more details see `this paper
+    <https://www.aaai.org/Papers/AAAI/2008/AAAI08-107.pdf/>`_.
+
+    Examples
+    --------
+    >>> from ITMO_FS.filters.multivariate import TraceRatioFisher
+    >>> x = np.array([[1, 2, 3, 3, 1], [2, 2, 3, 3, 2], [1, 3, 3, 1, 3],
+    ... [3, 1, 3, 1, 4], [4, 4, 3, 1, 5]])
+    >>> y = np.array([1, 2, 1, 1, 2])
+    >>> tracer = TraceRatioFisher(3).fit(x, y)
+    >>> tracer.selected_features_
+    array([0, 1, 3], dtype=int64)
     """
-        Creates TraceRatio(similarity based) feature selection filter
-        performed in supervised way, i.e fisher version
+    def __init__(self, n_features, epsilon=1e-3):
+        self.n_features = n_features
+        self.epsilon = epsilon
+
+    def _fit(self, X, y):
+        """Fit the filter.
 
         Parameters
         ----------
-        n_selected_features : int
-            Amount of features to filter
-
-        Notes
-        -----
-        For more details see `this paper <https://www.aaai.org/Papers/AAAI/2008/AAAI08-107.pdf/>`_.
-
-        Examples
-        --------
-        >>> from ITMO_FS.filters.multivariate import TraceRatioFisher
-        >>> X = np.array([[1, 2, 3, 3, 1],[2, 2, 3, 3, 2], [1, 3, 3, 1, 3],\
-[3, 1, 3, 1, 4],[4, 4, 3, 1, 5]], dtype = np.integer)
-        >>> y = np.array([1, 2, 3, 4, 5], dtype=np.integer)
-        >>> tracer = TraceRatioFisher(3)
-        >>> tracer.fit_transform(X, y)
-        array([[1, 1, 2],
-               [2, 2, 2],
-               [3, 1, 3],
-               [4, 3, 1],
-               [5, 4, 4]])
-    """
-
-    def __init__(self, n_selected_features):
-        self.n_selected_features = n_selected_features
-
-    def fit(self, X, y, feature_names=None):
+        X : array-like, shape (n_samples, n_features)
+            The training input samples
+        y : array-like, shape (n_samples,)
+            The target values
+
+        Returns
+        -------
+        None
         """
-            Fits filter
-
-            Parameters
-            ----------
-            X : numpy array, shape (n_samples, n_features)
-              The training input samples
-            y : numpy array, shape (n_samples, )
-              The target values
-            feature_names : list of strings, optional
-                In case you want to define feature names
-
-            Returns
-            ----------
-            None
-
-            See Also
-            --------
-
-            Examples
-            --------
-
-        """
-
-        features = generate_features(X)
-        X, y, feature_names = self._check_input(X, y, feature_names)
-        self.feature_names = dict(zip(features, feature_names))
         n_samples = X.shape[0]
-        A_within = np.zeros((n_samples, n_samples))
-        labels = np.unique(y)
-        n_classes = labels.size
-        for i in range(n_classes):
-            sample_from_class = (y == labels[i])
-            cross_samples = (sample_from_class[:, np.newaxis] & sample_from_class[np.newaxis, :])
-            A_within[cross_samples] = 1.0 / np.count_nonzero(sample_from_class)
+        classes, counts = np.unique(y, return_counts=True)
+        counts_d = {cl: counts[idx] for idx, cl in enumerate(classes)}
+        getLogger(__name__).info("Class counts: %s", counts_d)
+
+        A_within = pairwise_distances(
+            y.reshape(-1, 1), metric=lambda x, y: (
+                (x[0] == y[0]) / counts_d[x[0]]))
         L_within = np.eye(n_samples) - A_within
-        L_between = np.ones((n_samples, n_samples)) / n_samples - A_within
+        getLogger(__name__).info("A_w: %s", A_within)
+        getLogger(__name__).info("L_w: %s", L_within)
+
+        L_between = A_within - np.ones((n_samples, n_samples)) / n_samples
+        getLogger(__name__).info("L_b: %s", L_between)
 
-        L_within = (L_within.T + L_within) / 2
-        L_between = (L_between.T + L_between) / 2
         E = X.T.dot(L_within).dot(X)
         B = X.T.dot(L_between).dot(X)
-        E = (E.T + E) / 2
-        B = (B.T + B) / 2
 
         # we need only diagonal elements for trace calculation
-        e = np.absolute(np.diag(E))
-        b = np.absolute(np.diag(B))
-        #b[b == 0] = 1e-14 # TODO: probably should be e[e == 0] = 1e-14?
-        e[e == 0] = 1e-14
-        features_indices = np.argsort(np.divide(b, e))[::-1][0:self.n_selected_features]
-        lam = np.sum(b[features_indices]) / np.sum(e[features_indices])
-        prev_lam = 0
-        while (lam - prev_lam >= 1e-3):
+        e = np.array(np.diag(E))
+        b = np.array(np.diag(B))
+        getLogger(__name__).info("E: %s", e)
+        getLogger(__name__).info("B: %s", b)
+        lam = 0
+        prev_lam = -1
+        while (lam - prev_lam >= self.epsilon):  # TODO: optimize
             score = b - lam * e
-            features_indices = np.argsort(score)[::-1][0:self.n_selected_features]
+            getLogger(__name__).info("Score: %s", score)
+            self.selected_features_ = np.argsort(score)[::-1][:self.n_features]
+            getLogger(__name__).info(
+                "New selected set: %s", self.selected_features_)
             prev_lam = lam
-            lam = np.sum(b[features_indices]) / np.sum(e[features_indices])
-        self.selected_features = features[features_indices]
-        #return features_indices, score, lam
-
-    def transform(self, X):
-        """
-            Transform given data by slicing it with selected features.
-
-            Parameters
-            ----------
-            X : array-like, shape (n_samples, n_features)
-                The training input samples.
-
-            Returns
-            ------
-            Transformed 2D numpy array
-        """
-
-        if type(X) is np.ndarray:
-            return X[:, self.selected_features]
-        else:
-            return X[self.selected_features]
-
-    def fit_transform(self, X, y, feature_names=None):
-        """
-            Fits the filter and transforms given dataset X.
-
-            Parameters
-            ----------
-            X : array-like, shape (n_features, n_samples)
-                The training input samples.
-            y : array-like, shape (n_samples, )
-                The target values.
-            feature_names : list of strings, optional
-                In case you want to define feature names
-
-            Returns
-            ------
-            X dataset sliced with features selected by the filter
-        """
-
-        self.fit(X, y, feature_names)
-        return self.transform(X)
+            lam = (np.sum(b[self.selected_features_])
+                   / np.sum(e[self.selected_features_]))
+            getLogger(__name__).info("New lambda: %d", lam)
+        self.score_ = score
+        self.lam_ = lam
diff --git a/ITMO_FS/filters/multivariate/measures.py b/ITMO_FS/filters/multivariate/measures.py
index 3a3463b2..f3da4c8d 100644
--- a/ITMO_FS/filters/multivariate/measures.py
+++ b/ITMO_FS/filters/multivariate/measures.py
@@ -1,613 +1,867 @@
 from ...utils.information_theory import *
 
 
-def MIM(selected_features, free_features, X, y):
-    """
-        Mutual Information Maximization feature scoring criterion.
-        This criterion focuses only on increase of relevance.
-        Given set of already selected features and set of remaining features on dataset X
-        with labels y selects next feature.
-
-        Parameters
-        ----------
-        selected_features : list of ints,
-            already selected features
-        free_features : list of ints
-            free features
-        X : array-like, shape (n_samples, n_features)
-            The training input samples.
-        y : array-like, shape (n_samples, )
-            The target values.
+def MIM(selected_features, free_features, x, y, **kwargs):
+    """Mutual Information Maximization feature scoring criterion. This
+    criterion focuses only on increase of relevance. Given set of already
+    selected features and set of remaining features on dataset X with labels
+    y selects next feature.
+
+    Parameters
+    ----------
+    selected_features : list of ints
+        already selected features
+    free_features : list of ints
+        free features
+    x : array-like, shape (n_samples, n_features)
+        The training input samples.
+    y : array-like, shape (n_samples,)
+        The target values.
+    kwargs : dict, optional
+        Additional parameters to pass to generalizedCriteria.
+
+    Returns
+    -------
+    array-like, shape (n_features,) : feature scores
         
-        Notes
-        -----
-        For more details see `this paper <http://www.jmlr.org/papers/volume13/brown12a/brown12a.pdf/>`_.
-
-
-        Examples
-        --------
-
-        >>> from ITMO_FS.filters.multivariate import MIM
-        >>> from sklearn.preprocessing import KBinsDiscretizer
-        >>> import numpy as np
-        >>> X = np.array([[1, 2, 3, 3, 1],[2, 2, 3, 3, 2], [1, 3, 3, 1, 3],\
-[3, 1, 3, 1, 4],[4, 4, 3, 1, 5]], dtype = np.integer)
-        >>> y = np.array([1, 2, 3, 4, 5], dtype=np.integer)
-        >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
-        >>> est.fit(X)
-        KBinsDiscretizer(encode='ordinal', n_bins=10)
-        >>> X = est.transform(X)
-        >>> selected_features = [1, 2]
-        >>> other_features = [i for i in range(0, X.shape[1]) if i not in selected_features]
-        >>> MIM(np.array(selected_features), np.array(other_features), X, y)
-        array([1.33217904, 0.67301167, 1.60943791])
+    Notes
+    -----
+    For more details see `this paper
+    <http://www.jmlr.org/papers/volume13/brown12a/brown12a.pdf/>`_.
+
+    Examples
+    --------
+    >>> from ITMO_FS.filters.multivariate import MIM
+    >>> from sklearn.preprocessing import KBinsDiscretizer
+    >>> import numpy as np
+    >>> x = np.array([[1, 2, 3, 3, 1], [2, 2, 3, 3, 2], [1, 3, 3, 1, 3],
+    ... [3, 1, 3, 1, 4], [4, 4, 3, 1, 5]])
+    >>> y = np.array([1, 2, 3, 4, 5])
+    >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
+    >>> x = est.fit_transform(x)
+    >>> selected_features = [1, 2]
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> MIM(np.array(selected_features), np.array(other_features), x, y)
+    array([1.33217904, 0.67301167, 1.60943791])
     """
-    return matrix_mutual_information(X[:, free_features], y)
-
-
-def MRMR(selected_features, free_features, X, y):
+    return matrix_mutual_information(x[:, free_features], y)
+
+
+def MRMR(selected_features, free_features, x, y, **kwargs):
+    """Minimum-Redundancy Maximum-Relevance feature scoring criterion. Given
+    set of already selected features and set of remaining features on
+    dataset X with labels y selects next feature.
+
+    Parameters
+    ----------
+    selected_features : list of ints
+        already selected features
+    free_features : list of ints
+        free features
+    x : array-like, shape (n_samples, n_features)
+        The training input samples.
+    y : array-like, shape (n_samples,)
+        The target values.
+    kwargs : dict, optional
+        Additional parameters to pass to generalizedCriteria.
+
+    Returns
+    -------
+    array-like, shape (n_features,) : feature scores
+
+    Notes
+    -----
+    For more details see `this paper
+    <http://www.jmlr.org/papers/volume13/brown12a/brown12a.pdf/>`_.
+
+    Examples
+    --------
+    >>> from ITMO_FS.filters.multivariate import MRMR
+    >>> from sklearn.preprocessing import KBinsDiscretizer
+    >>> import numpy as np
+    >>> x = np.array([[1, 2, 3, 3, 1], [2, 2, 3, 3, 2], [1, 3, 3, 1, 3],
+    ... [3, 1, 3, 1, 4], [4, 4, 3, 1, 5]])
+    >>> y = np.array([1, 2, 3, 4, 5])
+    >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
+    >>> x = est.fit_transform(x)
+    >>> selected_features = []
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> MRMR(np.array(selected_features), np.array(other_features), x, y)
+    array([1.33217904, 1.33217904, 0.        , 0.67301167, 1.60943791])
+    >>> selected_features = [1, 2]
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> MRMR(np.array(selected_features), np.array(other_features), x, y)
+    array([0.80471896, 0.33650583, 0.94334839])
     """
-        Minimum-Redundancy Maximum-Relevance feature scoring criterion.
-        Given set of already selected features and set of remaining features on dataset X
-        with labels y selects next feature.
-
-        Parameters
-        ----------
-        selected_features : list of ints,
-            already selected features
-        free_features : list of ints
-            free features
-        X : array-like, shape (n_samples, n_features)
-            The training input samples.
-        y : array-like, shape (n_samples, )
-            The target values.
+    if selected_features.size == 0:
+        return matrix_mutual_information(x, y)
+    return generalizedCriteria(
+        selected_features, free_features, x, y, 1 / selected_features.size, 0,
+        **kwargs)
+
+
+def JMI(selected_features, free_features, x, y, **kwargs):
+    """Joint Mutual Information feature scoring criterion. Given set of already
+    selected features and set of remaining features on dataset X with labels
+    y selects next feature.
+
+    Parameters
+    ----------
+    selected_features : list of ints
+        already selected features
+    free_features : list of ints
+        free features
+    x : array-like, shape (n_samples, n_features)
+        The training input samples.
+    y : array-like, shape (n_samples,)
+        The target values.
+    kwargs : dict, optional
+        Additional parameters to pass to generalizedCriteria.
+
+    Returns
+    -------
+    array-like, shape (n_features,) : feature scores
         
-        Notes
-        -----
-        For more details see `this paper <http://www.jmlr.org/papers/volume13/brown12a/brown12a.pdf/>`_.
-
-
-        Examples
-        --------
-
-        >>> from ITMO_FS.filters.multivariate import MRMR
-        >>> from sklearn.preprocessing import KBinsDiscretizer
-        >>> import numpy as np
-        >>> X = np.array([[1, 2, 3, 3, 1],[2, 2, 3, 3, 2], [1, 3, 3, 1, 3],\
-[3, 1, 3, 1, 4],[4, 4, 3, 1, 5]], dtype = np.integer)
-        >>> y = np.array([1, 2, 3, 4, 5], dtype=np.integer)
-        >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
-        >>> est.fit(X)
-        KBinsDiscretizer(encode='ordinal', n_bins=10)
-        >>> X = est.transform(X)
-        >>> selected_features = [1, 2]
-        >>> other_features = [i for i in range(0, X.shape[1]) if i not in selected_features]
-        >>> MRMR(np.array(selected_features), np.array(other_features), X, y)
-        array([0.80471896, 0.33650583, 0.94334839])
+    Notes
+    -----
+    For more details see `this paper
+    <http://www.jmlr.org/papers/volume13/brown12a/brown12a.pdf/>`_.
+
+    Examples
+    --------
+    >>> from ITMO_FS.filters.multivariate import JMI
+    >>> from sklearn.preprocessing import KBinsDiscretizer
+    >>> import numpy as np
+    >>> x = np.array([[1, 2, 3, 3, 1], [2, 2, 3, 3, 2], [1, 3, 3, 1, 3],
+    ... [3, 1, 3, 1, 4], [4, 4, 3, 1, 5]])
+    >>> y = np.array([1, 2, 3, 4, 5])
+    >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
+    >>> x = est.fit_transform(x)
+    >>> selected_features = []
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> JMI(np.array(selected_features), np.array(other_features), x, y)
+    array([1.33217904, 1.33217904, 0.        , 0.67301167, 1.60943791])
+    >>> selected_features = [1, 2]
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> JMI(np.array(selected_features), np.array(other_features), x, y)
+    array([0.80471896, 0.33650583, 0.94334839])
     """
     if selected_features.size == 0:
-        return matrix_mutual_information(X, y)
-    return generalizedCriteria(selected_features, free_features, X, y, 1 / selected_features.size, 0)
-
-
-def JMI(selected_features, free_features, X, y):
-    """
-        Joint Mutual Information feature scoring criterion.
-        Given set of already selected features and set of remaining features on dataset X
-        with labels y selects next feature.
-
-        Parameters
-        ----------
-        selected_features : list of ints,
-            already selected features
-        free_features : list of ints
-            free features
-        X : array-like, shape (n_samples, n_features)
-            The training input samples.
-        y : array-like, shape (n_samples, )
-            The target values.
-        
-        Notes
-        -----
-        For more details see `this paper <http://www.jmlr.org/papers/volume13/brown12a/brown12a.pdf/>`_.
-
-
-        Examples
-        --------
-
-        >>> from ITMO_FS.filters.multivariate import JMI
-        >>> from sklearn.preprocessing import KBinsDiscretizer
-        >>> import numpy as np
-        >>> X = np.array([[1, 2, 3, 3, 1],[2, 2, 3, 3, 2], [1, 3, 3, 1, 3],\
-[3, 1, 3, 1, 4],[4, 4, 3, 1, 5]], dtype = np.integer)
-        >>> y = np.array([1, 2, 3, 4, 5], dtype=np.integer)
-        >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
-        >>> est.fit(X)
-        KBinsDiscretizer(encode='ordinal', n_bins=10)
-        >>> X = est.transform(X)
-        >>> selected_features = [1, 2]
-        >>> other_features = [i for i in range(0, X.shape[1]) if i not in selected_features]
-        >>> JMI(np.array(selected_features), np.array(other_features), X, y)
-        array([0.80471896, 0.33650583, 0.94334839])
+        return matrix_mutual_information(x, y)
+    return generalizedCriteria(
+        selected_features, free_features, x, y, 1 / selected_features.size,
+        1 / selected_features.size, **kwargs)
+
+def JMIM(selected_features, free_features, x, y, **kwargs):
+    """Joint Mutual Information Maximization feature scoring criterion. Given
+    set of already selected features and set of remaining features on
+    dataset X with labels y selects next feature.
+
+    Parameters
+    ----------
+    selected_features : list of ints
+        already selected features
+    free_features : list of ints
+        free features
+    x : array-like, shape (n_samples, n_features)
+        The training input samples.
+    y : array-like, shape (n_samples,)
+        The target values.
+    kwargs : dict, optional
+        Additional parameters to pass to generalizedCriteria.
+
+    Returns
+    -------
+    array-like, shape (n_features,) : feature scores
+
+    Notes
+    -----
+    For more details see `this paper
+    <https://www.sciencedirect.com/science/article/pii/S0957417415004674/>`_.
+
+    Examples
+    --------
+    >>> from ITMO_FS.filters.multivariate import JMIM
+    >>> from sklearn.preprocessing import KBinsDiscretizer
+    >>> import numpy as np
+    >>> x = np.array([[1, 2, 3, 3, 1], [2, 2, 3, 3, 2], [1, 3, 3, 1, 3],
+    ... [3, 1, 3, 1, 4], [4, 4, 3, 1, 5]])
+    >>> y = np.array([1, 2, 3, 4, 5])
+    >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
+    >>> x = est.fit_transform(x)
+    >>> selected_features = []
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> JMIM(np.array(selected_features), np.array(other_features), x, y)
+    array([1.33217904, 1.33217904, 0.        , 0.67301167, 1.60943791])
+    >>> selected_features = [1, 2]
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> JMIM(np.array(selected_features), np.array(other_features), x, y)
+    array([1.33217904, 0.67301167, 1.60943791])
     """
+    relevance = matrix_mutual_information(x[:, free_features], y)
     if selected_features.size == 0:
-        return matrix_mutual_information(X, y)
-    return generalizedCriteria(selected_features, free_features, X, y, 1 / selected_features.size,
-                               1 / selected_features.size)
-
-
-def CIFE(selected_features, free_features, X, y):
+        return relevance
+    cond_information = np.vectorize(
+        lambda free_feature: np.apply_along_axis(
+            conditional_mutual_information, 0, x[:, selected_features],
+            y, x[:, free_feature]),
+        signature='()->(1)')(free_features)
+    return np.min(cond_information.T + relevance, axis=0)
+
+def NJMIM(selected_features, free_features, x, y, **kwargs):
+    """Normalized Joint Mutual Information Maximization feature scoring
+    criterion. Given set of already selected features and set of
+    remaining features on dataset X with labels y selects next feature.
+
+    Parameters
+    ----------
+    selected_features : list of ints
+        already selected features
+    free_features : list of ints
+        free features
+    x : array-like, shape (n_samples, n_features)
+        The training input samples.
+    y : array-like, shape (n_samples,)
+        The target values.
+    kwargs : dict, optional
+        Additional parameters to pass to generalizedCriteria.
+
+    Returns
+    -------
+    array-like, shape (n_features,) : feature scores
+
+    Notes
+    -----
+    For more details see `this paper
+    <https://www.sciencedirect.com/science/article/pii/S0957417415004674/>`_.
+
+    Examples
+    --------
+    >>> from ITMO_FS.filters.multivariate import NJMIM
+    >>> from sklearn.preprocessing import KBinsDiscretizer
+    >>> import numpy as np
+    >>> x = np.array([[1, 2, 3, 3, 1], [2, 2, 3, 3, 2], [1, 3, 3, 1, 3],
+    ... [3, 1, 3, 1, 4], [4, 4, 3, 1, 5]])
+    >>> y = np.array([1, 2, 3, 4, 5])
+    >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
+    >>> x = est.fit_transform(x)
+    >>> selected_features = []
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> NJMIM(np.array(selected_features), np.array(other_features), x, y)
+    array([1.33217904, 1.33217904, 0.        , 0.67301167, 1.60943791])
+    >>> selected_features = [1, 2]
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> NJMIM(np.array(selected_features), np.array(other_features), x, y)
+    array([0.82772938, 0.41816566, 1.        ])
     """
-        Conditional Infomax Feature Extraction feature scoring criterion.
-        Given set of already selected features and set of remaining features on dataset X
-        with labels y selects next feature.
-
-        Parameters
-        ----------
-        selected_features : list of ints,
-            already selected features
-        free_features : list of ints
-            free features
-        X : array-like, shape (n_samples, n_features)
-            The training input samples.
-        y : array-like, shape (n_samples, )
-            The target values.
+    if selected_features.size == 0:
+        return matrix_mutual_information(x, y)
+    sym_relevance = np.vectorize(
+        lambda selected_feature: np.apply_along_axis(
+            symmetrical_relevance, 0, x[:, free_features],
+            x[:, selected_feature], y),
+        signature='()->(1)')(selected_features)
+    return np.min(sym_relevance, axis=0)
+
+
+def CIFE(selected_features, free_features, x, y, **kwargs):
+    """Conditional Infomax Feature Extraction feature scoring criterion. Given
+    set of already selected features and set of remaining features on
+    dataset X with labels y selects next feature.
+
+    Parameters
+    ----------
+    selected_features : list of ints
+        already selected features
+    free_features : list of ints
+        free features
+    x : array-like, shape (n_samples, n_features)
+        The training input samples.
+    y : array-like, shape (n_samples,)
+        The target values.
+    kwargs : dict, optional
+        Additional parameters to pass to generalizedCriteria.
+
+    Returns
+    -------
+    array-like, shape (n_features,) : feature scores
         
-                Notes
-        -----
-        For more details see `this paper <http://www.jmlr.org/papers/volume13/brown12a/brown12a.pdf/>`_.
-
-        Examples
-        --------
-
-        >>> from ITMO_FS.filters.multivariate import CIFE
-        >>> from sklearn.preprocessing import KBinsDiscretizer
-        >>> import numpy as np
-        >>> X = np.array([[1, 2, 3, 3, 1],[2, 2, 3, 3, 2], [1, 3, 3, 1, 3],\
-[3, 1, 3, 1, 4],[4, 4, 3, 1, 5]], dtype = np.integer)
-        >>> y = np.array([1, 2, 3, 4, 5], dtype=np.integer)
-        >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
-        >>> est.fit(X)
-        KBinsDiscretizer(encode='ordinal', n_bins=10)
-        >>> X = est.transform(X)
-        >>> selected_features = [1, 2]
-        >>> other_features = [i for i in range(0, X.shape[1]) if i not in selected_features]
-        >>> CIFE(np.array(selected_features), np.array(other_features), X, y)
-        array([0.27725887, 0.        , 0.27725887])
+    Notes
+    -----
+    For more details see `this paper
+    <http://www.jmlr.org/papers/volume13/brown12a/brown12a.pdf/>`_.
+
+    Examples
+    --------
+    >>> from ITMO_FS.filters.multivariate import CIFE
+    >>> from sklearn.preprocessing import KBinsDiscretizer
+    >>> import numpy as np
+    >>> x = np.array([[1, 2, 3, 3, 1], [2, 2, 3, 3, 2], [1, 3, 3, 1, 3],
+    ... [3, 1, 3, 1, 4], [4, 4, 3, 1, 5]])
+    >>> y = np.array([1, 2, 3, 4, 5])
+    >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
+    >>> x = est.fit_transform(x)
+    >>> selected_features = []
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> CIFE(np.array(selected_features), np.array(other_features), x, y)
+    array([1.33217904, 1.33217904, 0.        , 0.67301167, 1.60943791])
+    >>> selected_features = [1, 2]
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> CIFE(np.array(selected_features), np.array(other_features), x, y)
+    array([0.27725887, 0.        , 0.27725887])
     """
-    return generalizedCriteria(selected_features, free_features, X, y, 1, 1)
-
-
-def MIFS(selected_features, free_features, X, y, beta):
-    """
-        Mutual Information Feature Selection feature scoring criterion.
-        This criterion includes the I(X;Y) term to ensure feature relevance, but introduces a 
-        penalty to enforce low correlations with features already selected in set.
-        Given set of already selected features and set of remaining features on dataset X
-        with labels y selects next feature.
-
-        Parameters
-        ----------
-        selected_features : list of ints,
-            already selected features
-        free_features : list of ints
-            free features
-        X : array-like, shape (n_samples, n_features)
-            The training input samples.
-        y : array-like, shape (n_samples, )
-            The target values.
-        beta : float,
-            coeficient for redundancy term
+    return generalizedCriteria(
+        selected_features, free_features, x, y, 1, 1, **kwargs)
+
+
+def MIFS(selected_features, free_features, x, y, beta, **kwargs):
+    """Mutual Information Feature Selection feature scoring criterion. This
+    criterion includes the I(X;Y) term to ensure feature relevance,
+    but introduces a penalty to enforce low correlations with features
+    already selected in set. Given set of already selected features and set
+    of remaining features on dataset X with labels y selects next feature.
+
+    Parameters
+    ----------
+    selected_features : list of ints
+        already selected features
+    free_features : list of ints
+        free features
+    x : array-like, shape (n_samples, n_features)
+        The training input samples.
+    y : array-like, shape (n_samples,)
+        The target values.
+    beta : float
+        Coefficient for redundancy term.
+    kwargs : dict, optional
+        Additional parameters to pass to generalizedCriteria.
+
+    Returns
+    -------
+    array-like, shape (n_features,) : feature scores
         
-                Notes
-        -----
-        For more details see `this paper <http://www.jmlr.org/papers/volume13/brown12a/brown12a.pdf/>`_.
-
-        Examples
-        --------
-
-        >>> from ITMO_FS.filters.multivariate import MIFS
-        >>> from sklearn.preprocessing import KBinsDiscretizer
-        >>> import numpy as np
-        >>> X = np.array([[1, 2, 3, 3, 1],[2, 2, 3, 3, 2], [1, 3, 3, 1, 3],\
-[3, 1, 3, 1, 4],[4, 4, 3, 1, 5]], dtype = np.integer)
-        >>> y = np.array([1, 2, 3, 4, 5], dtype=np.integer)
-        >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
-        >>> est.fit(X)
-        KBinsDiscretizer(encode='ordinal', n_bins=10)
-        >>> X = est.transform(X)
-        >>> selected_features = [1, 2]
-        >>> other_features = [i for i in range(0, X.shape[1]) if i not in selected_features]
-        >>> MIFS(np.array(selected_features), np.array(other_features), X, y, 0.4)
-        array([0.91021097, 0.403807  , 1.0765663 ])
-    """
-    return generalizedCriteria(selected_features, free_features, X, y, beta, 0)
-
-
-def CMIM(selected_features, free_features, X, y):
+    Notes
+    -----
+    For more details see `this paper
+    <http://www.jmlr.org/papers/volume13/brown12a/brown12a.pdf/>`_.
+
+    Examples
+    --------
+    >>> from ITMO_FS.filters.multivariate import MIFS
+    >>> from sklearn.preprocessing import KBinsDiscretizer
+    >>> import numpy as np
+    >>> x = np.array([[1, 2, 3, 3, 1], [2, 2, 3, 3, 2], [1, 3, 3, 1, 3],
+    ... [3, 1, 3, 1, 4], [4, 4, 3, 1, 5]])
+    >>> y = np.array([1, 2, 3, 4, 5])
+    >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
+    >>> x = est.fit_transform(x)
+    >>> selected_features = []
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> MIFS(np.array(selected_features), np.array(other_features), x, y, 0.4)
+    array([1.33217904, 1.33217904, 0.        , 0.67301167, 1.60943791])
+    >>> selected_features = [1, 2]
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> MIFS(np.array(selected_features), np.array(other_features), x, y, 0.4)
+    array([0.91021097, 0.403807  , 1.0765663 ])
     """
-        Conditional Mutual Info Maximisation feature scoring criterion.
-        Given set of already selected features and set of remaining features on dataset X
-        with labels y selects next feature.
-
-        Parameters
-        ----------
-        selected_features : list of ints,
-            already selected features
-        free_features : list of ints
-            free features
-        X : array-like, shape (n_samples, n_features)
-            The training input samples.
-        y : array-like, shape (n_samples, )
-            The target values.
+    return generalizedCriteria(
+        selected_features, free_features, x, y, beta, 0, **kwargs)
+
+
+def CMIM(selected_features, free_features, x, y, **kwargs):
+    """Conditional Mutual Info Maximisation feature scoring criterion. Given
+    set of already selected features and set of remaining features on
+    dataset X with labels y selects next feature.
+
+    Parameters
+    ----------
+    selected_features : list of ints
+        already selected features
+    free_features : list of ints
+        free features
+    x : array-like, shape (n_samples, n_features)
+        The training input samples.
+    y : array-like, shape (n_samples,)
+        The target values.
+    kwargs : dict, optional
+        Additional parameters to pass to generalizedCriteria.
+
+    Returns
+    -------
+    array-like, shape (n_features,) : feature scores
         
-        Notes
-        -----
-        For more details see `this paper <http://www.jmlr.org/papers/volume13/brown12a/brown12a.pdf/>`_.
-
-
-        Examples
-        --------
-
-        >>> from ITMO_FS.filters.multivariate import CMIM
-        >>> from sklearn.preprocessing import KBinsDiscretizer
-        >>> import numpy as np
-        >>> X = np.array([[1, 2, 3, 3, 1],[2, 2, 3, 3, 2], [1, 3, 3, 1, 3],\
-[3, 1, 3, 1, 4],[4, 4, 3, 1, 5]], dtype = np.integer)
-        >>> y = np.array([1, 2, 3, 4, 5], dtype=np.integer)
-        >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
-        >>> est.fit(X)
-        KBinsDiscretizer(encode='ordinal', n_bins=10)
-        >>> X = est.transform(X)
-        >>> selected_features = [1, 2]
-        >>> other_features = [i for i in range(0, X.shape[1]) if i not in selected_features]
-        >>> CMIM(np.array(selected_features), np.array(other_features), X, y)
-        array([0.27725887, 0.        , 0.27725887])
+    Notes
+    -----
+    For more details see `this paper
+    <http://www.jmlr.org/papers/volume13/brown12a/brown12a.pdf/>`_.
+
+    Examples
+    --------
+    >>> from ITMO_FS.filters.multivariate import CMIM
+    >>> from sklearn.preprocessing import KBinsDiscretizer
+    >>> import numpy as np
+    >>> x = np.array([[1, 2, 3, 3, 1], [2, 2, 3, 3, 2], [1, 3, 3, 1, 3],
+    ... [3, 1, 3, 1, 4], [4, 4, 3, 1, 5]])
+    >>> y = np.array([1, 2, 3, 4, 5])
+    >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
+    >>> x = est.fit_transform(x)
+    >>> selected_features = []
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> CMIM(np.array(selected_features), np.array(other_features), x, y)
+    array([1.33217904, 1.33217904, 0.        , 0.67301167, 1.60943791])
+    >>> selected_features = [1, 2]
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> CMIM(np.array(selected_features), np.array(other_features), x, y)
+    array([0.27725887, 0.        , 0.27725887])
     """
     if selected_features.size == 0:
-        return matrix_mutual_information(X, y)
-    vectorized_function = lambda free_feature: \
-        min(np.vectorize(
-            lambda selected_feature: conditional_mutual_information(X[:, free_feature], y, X[:, selected_feature]))(
-            selected_features))
+        return matrix_mutual_information(x, y)
+    vectorized_function = lambda free_feature: min(
+        np.vectorize(
+            lambda selected_feature: conditional_mutual_information(
+                x[:, free_feature], y,
+                x[:, selected_feature]))(selected_features))
     return np.vectorize(vectorized_function)(free_features)
 
 
-def ICAP(selected_features, free_features, X, y):
-    """
-        Interaction Capping feature scoring criterion.
-        Given set of already selected features and set of remaining features on dataset X
-        with labels y selects next feature.
-
-        Parameters
-        ----------
-        selected_features : list of ints,
-            already selected features
-        free_features : list of ints
-            free features
-        X : array-like, shape (n_samples, n_features)
-            The training input samples.
-        y : array-like, shape (n_samples, )
-            The target values.
+def ICAP(selected_features, free_features, x, y, **kwargs):
+    """Interaction Capping feature scoring criterion. Given set of already
+    selected features and set of remaining features on dataset X with labels
+    y selects next feature.
+
+    Parameters
+    ----------
+    selected_features : list of ints
+        already selected features
+    free_features : list of ints
+        free features
+    x : array-like, shape (n_samples, n_features)
+        The training input samples.
+    y : array-like, shape (n_samples,)
+        The target values.
+    kwargs : dict, optional
+        Additional parameters to pass to generalizedCriteria.
+
+    Returns
+    -------
+    array-like, shape (n_features,) : feature scores
         
-        Notes
-        -----
-        For more details see `this paper <http://www.jmlr.org/papers/volume13/brown12a/brown12a.pdf/>`_.
-
-
-
-        Examples
-        --------
-
-        >>> from ITMO_FS.filters.multivariate import ICAP
-        >>> from sklearn.preprocessing import KBinsDiscretizer
-        >>> import numpy as np
-        >>> X = np.array([[1, 2, 3, 3, 1],[2, 2, 3, 3, 2], [1, 3, 3, 1, 3],\
-[3, 1, 3, 1, 4],[4, 4, 3, 1, 5]], dtype = np.integer)
-        >>> y = np.array([1, 2, 3, 4, 5], dtype=np.integer)
-        >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
-        >>> est.fit(X)
-        KBinsDiscretizer(encode='ordinal', n_bins=10)
-        >>> X = est.transform(X)
-        >>> selected_features = [1, 2]
-        >>> other_features = [i for i in range(0, X.shape[1]) if i not in selected_features]
-        >>> ICAP(np.array(selected_features), np.array(other_features), X, y)
-        array([0.27725887, 0.        , 0.27725887])
+    Notes
+    -----
+    For more details see `this paper
+    <http://www.jmlr.org/papers/volume13/brown12a/brown12a.pdf/>`_.
+
+    Examples
+    --------
+    >>> from ITMO_FS.filters.multivariate import ICAP
+    >>> from sklearn.preprocessing import KBinsDiscretizer
+    >>> import numpy as np
+    >>> x = np.array([[1, 2, 3, 3, 1], [2, 2, 3, 3, 2], [1, 3, 3, 1, 3],
+    ... [3, 1, 3, 1, 4], [4, 4, 3, 1, 5]])
+    >>> y = np.array([1, 2, 3, 4, 5])
+    >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
+    >>> x = est.fit_transform(x)
+    >>> selected_features = []
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> ICAP(np.array(selected_features), np.array(other_features), x, y)
+    array([1.33217904, 1.33217904, 0.        , 0.67301167, 1.60943791])
+    >>> selected_features = [1, 2]
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> ICAP(np.array(selected_features), np.array(other_features), x, y)
+    array([0.27725887, 0.        , 0.27725887])
     """
-    if selected_features.size == 0:
-        return matrix_mutual_information(X, y)
-    relevance = matrix_mutual_information(X[:, free_features], y)
-    redundancy = np.vectorize(
-        lambda free_feature: np.sum(matrix_mutual_information(X[:, selected_features], X[:, free_feature])))(
-        free_features)
-    cond_dependency = np.vectorize(lambda free_feature: \
-                                       np.sum(np.apply_along_axis(conditional_mutual_information, 0,
-                                                                  X[:, selected_features], X[:, free_feature], y)))(
-        free_features)
-    return relevance - np.maximum(redundancy - cond_dependency, 0.)
+    if "relevance" in kwargs:
+        relevance = kwargs["relevance"]
+    else:
+        relevance = matrix_mutual_information(x[:, free_features], y)
 
+    if selected_features.size == 0:
+        return relevance
 
-def DCSF(selected_features, free_features, X, y):
-    """
-        Dynamic change of selected feature with the class scoring criterion.
-        DCSF employs both mutual information and conditional mutual information 
-        to find an optimal subset of features.
-        Given set of already selected features and set of remaining features on dataset X
-        with labels y selects next feature.
-
-        Parameters
-        ----------
-        selected_features : list of ints,
-            already selected features
-        free_features : list of ints
-            free features
-        X : array-like, shape (n_samples, n_features)
-            The training input samples.
-        y : array-like, shape (n_samples, )
-            The target values.
-        
-        Notes
-        -----
-        For more details see `this paper <https://www.sciencedirect.com/science/article/abs/pii/S0031320318300736/>`_.
+    redundancy = np.vectorize(
+        lambda free_feature: matrix_mutual_information(
+            x[:, selected_features], 
+            x[:, free_feature]),
+        signature='()->(1)')(free_features)
+    cond_dependency = np.vectorize(
+        lambda free_feature: np.apply_along_axis(
+            conditional_mutual_information, 0,
+            x[:, selected_features],
+            x[:, free_feature], y),
+        signature='()->(1)')(free_features)
+    return relevance - np.sum(
+        np.maximum(redundancy - cond_dependency, 0.), axis=1)
+
+
+def DCSF(selected_features, free_features, x, y, **kwargs):
+    """Dynamic change of selected feature with the class scoring criterion.
+    DCSF employs both mutual information and conditional mutual information
+    to find an optimal subset of features. Given set of already selected
+    features and set of remaining features on dataset X with labels y
+    selects next feature.
+
+    Parameters
+    ----------
+    selected_features : list of ints
+        already selected features
+    free_features : list of ints
+        free features
+    x : array-like, shape (n_samples, n_features)
+        The training input samples.
+    y : array-like, shape (n_samples,)
+        The target values.
+    kwargs : dict, optional
+        Additional parameters to pass to generalizedCriteria.
+
+    Returns
+    -------
+    array-like, shape (n_features,) : feature scores
         
-
+    Notes
+    -----
+    For more details see `this paper
+    <https://www.sciencedirect.com/science/article/abs/pii/S0031320318300736/>`_.
         
-        Examples
-        --------
-
-        >>> from ITMO_FS.filters.multivariate import DCSF
-        >>> from sklearn.preprocessing import KBinsDiscretizer
-        >>> import numpy as np
-        >>> X = np.array([[1, 2, 3, 3, 1],[2, 2, 3, 3, 2], [1, 3, 3, 1, 3],\
-[3, 1, 3, 1, 4],[4, 4, 3, 1, 5]], dtype = np.integer)
-        >>> y = np.array([1, 2, 3, 4, 5], dtype=np.integer)
-        >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
-        >>> est.fit(X)
-        KBinsDiscretizer(encode='ordinal', n_bins=10)
-        >>> X = est.transform(X)
-        >>> selected_features = [1, 2]
-        >>> other_features = [i for i in range(0, X.shape[1]) if i not in selected_features]
-        >>> DCSF(np.array(selected_features), np.array(other_features), X, y)
-        array([0.83177662, 0.65916737, 0.55451774])
+    Examples
+    --------
+    >>> from ITMO_FS.filters.multivariate import DCSF
+    >>> from sklearn.preprocessing import KBinsDiscretizer
+    >>> import numpy as np
+    >>> x = np.array([[1, 2, 3, 3, 1], [2, 2, 3, 3, 2], [1, 3, 3, 1, 3],
+    ... [3, 1, 3, 1, 4], [4, 4, 3, 1, 5]])
+    >>> y = np.array([1, 2, 3, 4, 5])
+    >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
+    >>> x = est.fit_transform(x)
+    >>> selected_features = []
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> DCSF(np.array(selected_features), np.array(other_features), x, y)
+    array([0., 0., 0., 0., 0.])
+    >>> selected_features = [1, 2]
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> DCSF(np.array(selected_features), np.array(other_features), x, y)
+    array([0.83177662, 0.65916737, 0.55451774])
     """
     if selected_features.size == 0:
         return np.zeros(len(free_features))
     vectorized_function = lambda free_feature: np.sum(
-        np.apply_along_axis(lambda z, x, y: conditional_mutual_information(x, y, z), 0, X[:, selected_features],
-                            X[:, free_feature], y) +
-        np.apply_along_axis(conditional_mutual_information, 0, X[:, selected_features], y, X[:, free_feature]) -
-        matrix_mutual_information(X[:, selected_features], X[:, free_feature]))
+        np.apply_along_axis(
+            lambda z, a, b: conditional_mutual_information(a, b, z), 0,
+            x[:, selected_features],
+            x[:, free_feature], y)
+        + np.apply_along_axis(
+            conditional_mutual_information, 0, x[:, selected_features], y,
+            x[:, free_feature])
+        - matrix_mutual_information(
+            x[:, selected_features], x[:, free_feature]))
     return np.vectorize(vectorized_function)(free_features)
 
 
-def CFR(selected_features, free_features, X, y):
-    """
-        The criterion of CFR maximizes the correlation and minimizes the redundancy.
-        Given set of already selected features and set of remaining features on dataset X
-        with labels y selects next feature.
-
-        Parameters
-        ----------
-        selected_features : list of ints,
-            already selected features
-        free_features : list of ints
-            free features
-        X : array-like, shape (n_samples, n_features)
-            The training input samples.
-        y : array-like, shape (n_samples, )
-            The target values.
-        
-        Notes
-        -----
-        For more details see `this paper <https://www.sciencedirect.com/science/article/pii/S2210832719302522/>`_.
-
+def CFR(selected_features, free_features, x, y, **kwargs):
+    """The criterion of CFR maximizes the correlation and minimizes the
+    redundancy. Given set of already selected features and set of remaining
+    features on dataset X with labels y selects next feature.
+
+    Parameters
+    ----------
+    selected_features : list of ints
+        already selected features
+    free_features : list of ints
+        free features
+    x : array-like, shape (n_samples, n_features)
+        The training input samples.
+    y : array-like, shape (n_samples,)
+        The target values.
+    kwargs : dict, optional
+        Additional parameters to pass to generalizedCriteria.
+
+    Returns
+    -------
+    array-like, shape (n_features,) : feature scores
         
-        Examples
-        --------
-
-        >>> from ITMO_FS.filters.multivariate import CFR
-        >>> from sklearn.preprocessing import KBinsDiscretizer
-        >>> import numpy as np
-        >>> X = np.array([[1, 2, 3, 3, 1],[2, 2, 3, 3, 2], [1, 3, 3, 1, 3],\
-[3, 1, 3, 1, 4],[4, 4, 3, 1, 5]], dtype = np.integer)
-        >>> y = np.array([1, 2, 3, 4, 5], dtype=np.integer)
-        >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
-        >>> est.fit(X)
-        KBinsDiscretizer(encode='ordinal', n_bins=10)
-        >>> X = est.transform(X)
-        >>> selected_features = [1, 2]
-        >>> other_features = [i for i in range(0, X.shape[1]) if i not in selected_features]
-        >>> CFR(np.array(selected_features), np.array(other_features), X, y)
-        array([0.55451774, 0.        , 0.55451774])
-
+    Notes
+    -----
+    For more details see `this paper
+    <https://www.sciencedirect.com/science/article/pii/S2210832719302522/>`_.
+
+    Examples
+    --------
+    >>> from ITMO_FS.filters.multivariate import CFR
+    >>> from sklearn.preprocessing import KBinsDiscretizer
+    >>> import numpy as np
+    >>> x = np.array([[1, 2, 3, 3, 1], [2, 2, 3, 3, 2], [1, 3, 3, 1, 3],
+    ... [3, 1, 3, 1, 4], [4, 4, 3, 1, 5]])
+    >>> y = np.array([1, 2, 3, 4, 5])
+    >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
+    >>> x = est.fit_transform(x)
+    >>> selected_features = []
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> CFR(np.array(selected_features), np.array(other_features), x, y)
+    array([0., 0., 0., 0., 0.])
+    >>> selected_features = [1, 2]
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> CFR(np.array(selected_features), np.array(other_features), x, y)
+    array([0.55451774, 0.        , 0.55451774])
     """
     if selected_features.size == 0:
         return np.zeros(len(free_features))
     vectorized_function = lambda free_feature: np.sum(
-        np.apply_along_axis(lambda z, x, y: conditional_mutual_information(x, y, z), 0, X[:, selected_features],
-                            X[:, free_feature], y) +
-        np.apply_along_axis(conditional_mutual_information, 0, X[:, selected_features], X[:, free_feature], y) -
-        matrix_mutual_information(X[:, selected_features], X[:, free_feature]))
+        np.apply_along_axis(
+            lambda z, a, b: conditional_mutual_information(a, b, z), 0,
+            x[:, selected_features],
+            x[:, free_feature], y)
+        + np.apply_along_axis(
+            conditional_mutual_information, 0, x[:, selected_features],
+            x[:, free_feature], y)
+        - matrix_mutual_information(
+            x[:, selected_features], x[:, free_feature]))
     return np.vectorize(vectorized_function)(free_features)
 
 
-def MRI(selected_features, free_features, X, y):
-    """
-        Max-Relevance and Max-Independence feature scoring criteria.
-        Given set of already selected features and set of remaining features on dataset X
-        with labels y selects next feature.
-
-        Parameters
-        ----------
-        selected_features : list of ints,
-            already selected features
-        free_features : list of ints
-            free features
-        X : array-like, shape (n_samples, n_features)
-            The training input samples.
-        y : array-like, shape (n_samples, )
-            The target values.
-        
-        Notes
-        -----
-        For more details see `this paper <https://link.springer.com/article/10.1007/s10489-019-01597-z/>`_.
-
+def MRI(selected_features, free_features, x, y, **kwargs):
+    """Max-Relevance and Max-Independence feature scoring criteria. Given set
+    of already selected features and set of remaining features on dataset X
+    with labels y selects next feature.
+
+    Parameters
+    ----------
+    selected_features : list of ints
+        already selected features
+    free_features : list of ints
+        free features
+    x : array-like, shape (n_samples, n_features)
+        The training input samples.
+    y : array-like, shape (n_samples,)
+        The target values.
+    kwargs : dict, optional
+        Additional parameters to pass to generalizedCriteria.
+
+    Returns
+    -------
+    array-like, shape (n_features,) : feature scores
         
-        Examples
-        --------
-
-        >>> from ITMO_FS.filters.multivariate import MRI
-        >>> from sklearn.preprocessing import KBinsDiscretizer
-        >>> import numpy as np
-        >>> X = np.array([[1, 2, 3, 3, 1],[2, 2, 3, 3, 2], [1, 3, 3, 1, 3],\
-[3, 1, 3, 1, 4],[4, 4, 3, 1, 5]], dtype = np.integer)
-        >>> y = np.array([1, 2, 3, 4, 5], dtype=np.integer)
-        >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
-        >>> est.fit(X)
-        KBinsDiscretizer(encode='ordinal', n_bins=10)
-        >>> X = est.transform(X)
-        >>> selected_features = [1, 2]
-        >>> other_features = [i for i in range(0, X.shape[1]) if i not in selected_features]
-        >>> MRI(np.array(selected_features), np.array(other_features), X, y)
-        array([0.62889893, 0.22433722, 0.72131855])
+    Notes
+    -----
+    For more details see `this paper
+    <https://link.springer.com/article/10.1007/s10489-019-01597-z/>`_.
+
+    Examples
+    --------
+    >>> from ITMO_FS.filters.multivariate import MRI
+    >>> from sklearn.preprocessing import KBinsDiscretizer
+    >>> import numpy as np
+    >>> x = np.array([[1, 2, 3, 3, 1], [2, 2, 3, 3, 2], [1, 3, 3, 1, 3],
+    ... [3, 1, 3, 1, 4], [4, 4, 3, 1, 5]])
+    >>> y = np.array([1, 2, 3, 4, 5])
+    >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
+    >>> x = est.fit_transform(x)
+    >>> selected_features = []
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> MRI(np.array(selected_features), np.array(other_features), x, y)
+    array([1.33217904, 1.33217904, 0.        , 0.67301167, 1.60943791])
+    >>> selected_features = [1, 2]
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> MRI(np.array(selected_features), np.array(other_features), x, y)
+    array([0.62889893, 0.22433722, 0.72131855])
     """
-    return generalizedCriteria(selected_features, free_features, X, y, 2 / (selected_features.size + 1),
-                               2 / (selected_features.size + 1))
-
-
-def __information_weight(Xk, Xj, y):
-    return 1 + (joint_mutual_information(Xk, Xj, y) + mutual_information(Xk, y) + mutual_information(Xj, y)) / (
-            entropy(Xk) + entropy(Xj))
-
-
-def __SU(Xk, Xj):
-    return 2 * mutual_information(Xk, Xj) / (entropy(Xk) + entropy(Xj))
-
-
-def IWFS(selected_features, free_features, X, y):
-    """
-        Interaction Weight base feature scoring criteria.
-        IWFS is good at identifyng
-        Given set of already selected features and set of remaining features on dataset X
-        with labels y selects next feature.
-
-        Parameters
-        ----------
-        selected_features : list of ints,
-            already selected features
-        free_features : list of ints
-            free features
-        X : array-like, shape (n_samples, n_features)
-            The training input samples.
-        y : array-like, shape (n_samples, )
-            The target values.
-        
-        Notes
-        -----
-        For more details see `this paper <https://www.sciencedirect.com/science/article/abs/pii/S0031320315000850/>`_.
+    return generalizedCriteria(
+        selected_features, free_features, x, y,
+        2 / (selected_features.size + 1), 2 / (selected_features.size + 1),
+        **kwargs)
+
+
+def __information_weight(xk, xj, y):
+    return 1 + (joint_mutual_information(xk, xj, y)
+                - mutual_information(xk, y)
+                - mutual_information(xj, y)) / (entropy(xk) + entropy(xj))
+
+
+def __SU(xk, xj):
+    return 2 * mutual_information(xk, xj) / (entropy(xk) + entropy(xj))
+
+
+def IWFS(selected_features, free_features, x, y, **kwargs):
+    """Interaction Weight base feature scoring criteria. IWFS is good at
+    identifyng Given set of already selected features and set of remaining
+    features on dataset X with labels y selects next feature.
+
+    Parameters
+    ----------
+    selected_features : list of ints
+        already selected features
+    free_features : list of ints
+        free features
+    x : array-like, shape (n_samples, n_features)
+        The training input samples.
+    y : array-like, shape (n_samples,)
+        The target values.
+    kwargs : dict, optional
+        Additional parameters to pass to generalizedCriteria.
+
+    Returns
+    -------
+    array-like, shape (n_features,) : feature scores
         
-
-
-        Examples
-        --------
-
-        >>> from ITMO_FS.filters.multivariate import IWFS
-        >>> from sklearn.preprocessing import KBinsDiscretizer
-        >>> import numpy as np
-        >>> X = np.array([[1, 2, 3, 3, 1],[2, 2, 3, 3, 2], [1, 3, 3, 1, 3],\
-[3, 1, 3, 1, 4],[4, 4, 3, 1, 5]], dtype = np.integer)
-        >>> y = np.array([1, 2, 3, 4, 5], dtype=np.integer)
-        >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
-        >>> est.fit(X)
-        KBinsDiscretizer(encode='ordinal', n_bins=10)
-        >>> X = est.transform(X)
-        >>> selected_features = [1, 2]
-        >>> other_features = [i for i in range(0, X.shape[1]) if i not in selected_features]
-        >>> IWFS(np.array(selected_features), np.array(other_features), X, y)
-        array([13.9984669 , 13.35861706, 14.56253175])
+    Notes
+    -----
+    For more details see `this paper
+    <https://www.sciencedirect.com/science/article/abs/pii/S0031320315000850/>`_.
+
+    Examples
+    --------
+    >>> from ITMO_FS.filters.multivariate import IWFS
+    >>> from sklearn.preprocessing import KBinsDiscretizer
+    >>> import numpy as np
+    >>> x = np.array([[1, 2, 3, 3, 1], [2, 2, 3, 3, 2], [1, 3, 3, 1, 3],
+    ... [3, 1, 3, 1, 4], [4, 4, 3, 1, 5]])
+    >>> y = np.array([1, 2, 3, 4, 5])
+    >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
+    >>> x = est.fit_transform(x)
+    >>> selected_features = []
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> IWFS(np.array(selected_features), np.array(other_features), x, y)
+    array([0., 0., 0., 0., 0.])
+    >>> selected_features = [1, 2]
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> IWFS(np.array(selected_features), np.array(other_features), x, y)
+    array([1.0824043 , 1.11033338, 1.04268505])
     """
     if selected_features.size == 0:
         return np.zeros(len(free_features))
     vectorized_function = lambda free_feature: np.prod(
-        np.apply_along_axis(lambda Xj, Xk, y: __information_weight(Xk, Xj, y), 0, X[:, selected_features],
-                            X[:, free_feature], y) *
-        (np.apply_along_axis(__SU, 0, X[:, selected_features], X[:, free_feature]) + 1))
+        np.apply_along_axis(
+            lambda Xj, Xk, y: __information_weight(Xk, Xj, y),
+            0, x[:, selected_features], x[:, free_feature], y)
+        * (np.apply_along_axis(
+            __SU, 0, x[:, selected_features], x[:, free_feature]) + 1))
     return np.vectorize(vectorized_function)(free_features)
 
 
-# Ask question what should happen if number of features user want is less than useful number of features
-def generalizedCriteria(selected_features, free_features, X, y, beta, gamma):
-    """
-        This feature scoring criteria is a linear combination of all relevance,
-        redundancy, conditional dependency
-        Given set of already selected features and set of remaining features on dataset X
-        with labels y selects next feature.
-
-        Parameters
-        ----------
-        selected_features : list of ints,
-            already selected features
-        free_features : list of ints
-            free features
-        X : array-like, shape (n_samples, n_features)
-            The training input samples.
-        y : array-like, shape (n_samples, )
-            The target values.
-        beta : float,
-            coeficient for redundancy term
-        gamma : float,
-            coeficient for conditional dependancy term
-
-        Notes
-        -----
-        See the original paper [1]_ for more details.
-
-        References
-        ----------
-        .. [1] Brown, Gavin et al. "Conditional Likelihood Maximisation: A Unifying Framework for Information
-        Theoretic Feature Selection." JMLR 2012.
-
-        Examples
-        --------
-
-        >>> from ITMO_FS.filters.multivariate import CFR
-        >>> from sklearn.preprocessing import KBinsDiscretizer
-        >>> import numpy as np
-        >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
-        >>> X = np.array([[1, 2, 3, 3, 1],[2, 2, 3, 3, 2], [1, 3, 3, 1, 3],\
-[3, 1, 3, 1, 4],[4, 4, 3, 1, 5]], dtype = np.integer)
-        >>> y = np.array([1, 2, 3, 4, 5], dtype=np.integer)
-        >>> est.fit(X)
-        KBinsDiscretizer(encode='ordinal', n_bins=10)
-        >>> X = est.transform(X)
-        >>> selected_features = [1, 2]
-        >>> other_features = [i for i in range(0, X.shape[1]) if i not in selected_features]
-        >>> generalizedCriteria(np.array(selected_features), np.array(other_features), X, y, 0.4, 0.3)
-        array([0.91021097, 0.403807  , 1.0765663 ])
+# Ask question what should happen if number of features user want is less
+# than useful number of features
+def generalizedCriteria(selected_features, free_features, x, y, beta, gamma,
+                        **kwargs):
+    """This feature scoring criteria is a linear combination of all relevance,
+    redundancy, conditional dependency Given set of already selected
+    features and set of remaining features on dataset X with labels y
+    selects next feature.
+
+    Parameters
+    ----------
+    selected_features : list of ints
+        already selected features
+    free_features : list of ints
+        free features
+    x : array-like, shape (n_samples, n_features)
+        The training input samples.
+    y : array-like, shape (n_samples,)
+        The target values.
+    beta : float
+        Coefficient for redundancy term.
+    gamma : float
+        Coefficient for conditional dependancy term.
+
+    Returns
+    -------
+    array-like, shape (n_features,) : feature scores
+
+    Notes
+    -----
+    See the original paper [1]_ for more details.
+
+    References
+    ----------
+    .. [1] Brown, Gavin et al. "Conditional
+    Likelihood Maximisation: A Unifying Framework for Information
+    Theoretic Feature Selection." JMLR 2012.
+        
+    Examples
+    --------
+    >>> from ITMO_FS.filters.multivariate import CFR
+    >>> from sklearn.preprocessing import KBinsDiscretizer
+    >>> import numpy as np
+    >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
+    >>> x = np.array([[1, 2, 3, 3, 1], [2, 2, 3, 3, 2], [1, 3, 3, 1, 3],
+    ... [3, 1, 3, 1, 4], [4, 4, 3, 1, 5]])
+    >>> y = np.array([1, 2, 3, 4, 5])
+    >>> x = est.fit_transform(x)
+    >>> selected_features = []
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> generalizedCriteria(np.array(selected_features),
+    ... np.array(other_features), x, y, 0.4, 0.3)
+    array([1.33217904, 1.33217904, 0.        , 0.67301167, 1.60943791])
+    >>> selected_features = [1, 2]
+    >>> other_features = [i for i in range(0, x.shape[1]) if i
+    ... not in selected_features]
+    >>> generalizedCriteria(np.array(selected_features),
+    ... np.array(other_features), x, y, 0.4, 0.3)
+    array([0.91021097, 0.403807  , 1.0765663 ])
     """
+    if "relevance" in kwargs:
+        relevance = kwargs["relevance"]
+    else:
+        relevance = matrix_mutual_information(x[:, free_features], y)
+
     if selected_features.size == 0:
-        return matrix_mutual_information(X, y)
-    relevance = np.apply_along_axis(mutual_information, 0, X[:, free_features], y)
-    redundancy = np.vectorize(
-        lambda free_feature: np.sum(matrix_mutual_information(X[:, selected_features], X[:, free_feature])))(
-        free_features)
-
-    cond_dependency = np.vectorize(lambda free_feature: np.sum(np.apply_along_axis(conditional_mutual_information, 0,
-                                                                                   X[:, selected_features],
-                                                                                   X[:, free_feature], y)))(
-        free_features)
-    return relevance - beta * redundancy + gamma * cond_dependency
-
-
-GLOB_MEASURE = {"MIM": MIM,
-                "MRMR": MRMR,
-                "JMI": JMI,
-                "CIFE": CIFE,
-                "MIFS": MIFS,
-                "CMIM": CMIM,
-                "ICAP": ICAP,
-                "DCSF": DCSF,
-                "CFR": CFR,
-                "MRI": MRI,
-                "IWFS": IWFS,
-                "generalizedCriteria": generalizedCriteria}
+        return relevance
+
+    if beta != 0:
+        if "redundancy" in kwargs:
+            redundancy = kwargs["redundancy"]
+        else:
+            redundancy = np.vectorize(
+                lambda free_feature: np.sum(
+                    matrix_mutual_information(
+                        x[:, selected_features],
+                        x[:, free_feature])))(free_features)
+    else:
+        redundancy = 0
+
+    if gamma != 0:
+        cond_dependency = np.vectorize(
+            lambda free_feature: np.sum(
+                np.apply_along_axis(
+                    conditional_mutual_information, 0, x[:, selected_features],
+                    x[:, free_feature], y)))(free_features)
+    else:
+        cond_dependency = 0
+    return relevance - beta*redundancy + gamma*cond_dependency
+
+
+MEASURE_NAMES = {"MIM": MIM,
+                 "MRMR": MRMR,
+                 "JMI": JMI,
+                 "CIFE": CIFE,
+                 "MIFS": MIFS,
+                 "CMIM": CMIM,
+                 "ICAP": ICAP,
+                 "DCSF": DCSF,
+                 "CFR": CFR,
+                 "MRI": MRI,
+                 "IWFS": IWFS,
+                 "generalizedCriteria": generalizedCriteria}
diff --git a/ITMO_FS/filters/multivariate/mimaga.py b/ITMO_FS/filters/multivariate/mimaga.py
index cd7e716b..753094dd 100644
--- a/ITMO_FS/filters/multivariate/mimaga.py
+++ b/ITMO_FS/filters/multivariate/mimaga.py
@@ -7,6 +7,9 @@
 from sklearn.model_selection import train_test_split
 from functools import partial
 from ...utils.information_theory import *
+from ...utils import BaseTransformer
+
+# TODO fix docs
 
 
 def genes_mutual_information(genes):
@@ -40,7 +43,14 @@ def decode_genes(mapping, chromosome, train, test):
     return np.array(filtered_train), np.array(filtered_test)
 
 
-def population_fitness(mapping, population, train, train_cl, test, test_cl, measure):
+def population_fitness(
+        mapping,
+        population,
+        train,
+        train_cl,
+        test,
+        test_cl,
+        measure):
     """
     :param population: vector of chromosomes
     :return: vector of (chromosome code, chromosome fitness), max fitness, average fitness
@@ -48,7 +58,8 @@ def population_fitness(mapping, population, train, train_cl, test, test_cl, meas
     code_fitness = []
     f_sum = 0
     for i in range(len(population)):
-        filtered_train, filtered_test = decode_genes(mapping, population[i], train, test)
+        filtered_train, filtered_test = decode_genes(
+            mapping, population[i], train, test)
         clf = make_pipeline(StandardScaler(), SVC(gamma='auto'))
         if len(filtered_train) == 0:
             continue
@@ -67,7 +78,7 @@ def crossover(x, y):
     """ simple one-point crossover """
     random_point = random.randint(1, len(x) - 1)
     return x[0:random_point] + y[random_point:len(x)], \
-           y[0:random_point] + x[random_point:len(x)]
+        y[0:random_point] + x[random_point:len(x)]
 
 
 def mutation(x):
@@ -95,14 +106,19 @@ def cross_and_mutate(pc, pm, population):
         new_population.extend([child1, child2])
         max_parent_f = max([max_parent_f, f1, f2])
     for i in range(mutate_number):
-        mutant = mutation(population[random.randint(0, len(population) - 1)][0])
+        mutant = mutation(
+            population[random.randint(0, len(population) - 1)][0])
         new_population.append(mutant)
     return new_population, max_parent_f
 
 
-class MIMAGA(object):
+class MIMAGA(BaseTransformer):
+
+    def _fit(self, X, y, param):
+        pass
 
-    def __init__(self, mim_size, pop_size, max_iter, f_target, k1, k2, k3, k4):
+    def __init__(self, mim_size, pop_size, max_iter=20, f_target=0.8, k1=0.6,
+                 k2=0.3, k3=0.9, k4=0.001):
         """
         :param mim_size: desirable number of filtered features after MIM
         :param pop_size: initial population size
@@ -117,14 +133,14 @@ def __init__(self, mim_size, pop_size, max_iter, f_target, k1, k2, k3, k4):
         --------
         https://www.sciencedirect.com/science/article/abs/pii/S0925231217304150
         """
-        self._mim_size = mim_size
-        self._pop_size = pop_size
-        self._max_iter = max_iter
-        self._f_target = f_target
-        self._k1 = k1
-        self._k2 = k2
-        self._k3 = k3
-        self._k4 = k4
+        self.mim_size = mim_size
+        self.pop_size = pop_size
+        self.max_iter = max_iter
+        self.f_target = f_target
+        self.k1 = k1
+        self.k2 = k2
+        self.k3 = k3
+        self.k4 = k4
 
     # MIM
 
@@ -136,7 +152,8 @@ def _mim_filter(self, genes):
         g_num, _ = genes.shape
         mi_vector = genes_mutual_information(genes)
         seq_nums = [i for i in range(g_num)]
-        target_sequence = list(map(lambda p: p[1], sorted(zip(mi_vector, seq_nums))))[:self._mim_size]
+        target_sequence = list(map(lambda p: p[1], sorted(zip(mi_vector, seq_nums))))[
+            :self.mim_size]
         return target_sequence
 
     # AGA
@@ -146,29 +163,38 @@ def _initial_population(self):
         P.S. each individual corresponds to chromosome
         """
         population = []
-        for _ in range(self._pop_size):
-            individual_num = random.randint(1, 2 << self._mim_size - 1)
-            individual_code = list(map(int, bin(individual_num)[2:].zfill(self._mim_size)))
+        for _ in range(self.pop_size):
+            individual_num = random.randint(1, 2 << self.mim_size - 1)
+            individual_code = list(
+                map(int, bin(individual_num)[2:].zfill(self.mim_size)))
             population.append(individual_code)
         return population
 
     def _crossover_probability(self, f_max, f_avg, f_par):
         """ probability of crossover in population """
         if f_par >= f_avg:
-            return self._k1 * ((f_max - f_par) / (f_max - f_avg)) \
+            return self.k1 * ((f_max - f_par) / (f_max - f_avg)) \
                 if f_max != f_avg else 1
         else:
-            return self._k2
+            return self.k2
 
     def _mutation_probability(self, f_max, f_avg, f_par):
         """ probability of mutation in population """
         if f_par >= f_avg:
-            return self._k3 * ((f_max - f_par) / (f_max - f_avg)) \
+            return self.k3 * ((f_max - f_par) / (f_max - f_avg)) \
                 if f_max != f_avg else 1
         else:
-            return self._k4
+            return self.k4
 
-    def _aga_filter(self, max_size, mapping, population, train, train_cl, test, test_cl):
+    def _aga_filter(
+            self,
+            max_size,
+            mapping,
+            population,
+            train,
+            train_cl,
+            test,
+            test_cl):
         """
         :param max_size: maximum size of population (if population becomes bigger,
                          the worst individuals are killed)
@@ -183,13 +209,18 @@ def _aga_filter(self, max_size, mapping, population, train, train_cl, test, test
         f_par = f_max = 0
         counter = 0
         best_individual = [1 for _ in range(len(population[0]))]
-        while counter < self._max_iter and f_max < self._f_target:
-            code_fitness, f_max, f_avg = population_fitness(mapping, population, train, train_cl, test, test_cl, partial(f1_score, average='macro'))
+        while counter < self.max_iter and f_max < self.f_target:
+            code_fitness, f_max, f_avg = population_fitness(
+                mapping, population, train, train_cl, test, test_cl, partial(
+                    f1_score, average='macro'))
             if len(code_fitness) > max_size:
                 code_fitness = code_fitness[:max_size]
                 population = list(map(lambda x: x[0], code_fitness))
 
-            highly_fitted = list(filter(lambda x: x[1] >= f_max / 2, code_fitness))
+            highly_fitted = list(
+                filter(
+                    lambda x: x[1] >= f_max / 2,
+                    code_fitness))
             if len(highly_fitted) == 0:
                 highly_fitted = code_fitness
             best_individual = code_fitness[0][0]
@@ -208,14 +239,17 @@ def mimaga_filter(self, genes, classes):
         :param classes: distribution pf initial dataset
         :return: filtered with MIMAGA dataset, fitness value
         """
-        train_set, test_set, train_classes, test_classes = train_test_split(genes, classes, test_size=0.33)
+        train_set, test_set, train_classes, test_classes = train_test_split(
+            genes, classes, test_size=0.33)
         filtered_indexes = self._mim_filter(train_set.transpose())
-        index_map = dict(zip([i for i in range(self._mim_size)], filtered_indexes))
+        index_map = dict(
+            zip([i for i in range(self.mim_size)], filtered_indexes))
 
         first_population = self._initial_population()
-        best, max_fitness = self._aga_filter(self._pop_size * 2, index_map, first_population,
+        best, max_fitness = self._aga_filter(self.pop_size * 2, index_map, first_population,
                                              train_set.transpose(), train_classes, test_set.transpose(), test_classes)
-        result_genes, _ = decode_genes(index_map, best, train_set.transpose(), test_set.transpose())
+        result_genes, _ = decode_genes(
+            index_map, best, train_set.transpose(), test_set.transpose())
         return result_genes, max_fitness
 
 # TODO: optimize everything bcs this works for hours
diff --git a/ITMO_FS/filters/sparse/MCFS.py b/ITMO_FS/filters/sparse/MCFS.py
deleted file mode 100644
index 73894933..00000000
--- a/ITMO_FS/filters/sparse/MCFS.py
+++ /dev/null
@@ -1,125 +0,0 @@
-import numpy as np
-from sklearn.neighbors import NearestNeighbors
-from sklearn.linear_model import Lars
-from scipy.linalg import eigh
-
-
-class MCFS(object):
-    """
-        Performs the Unsupervised Feature Selection for Multi-Cluster Data algorithm.
-
-        Parameters
-        ----------
-        d : int
-            Number of features to select.
-        k : int, optional
-            Amount of clusters to find.
-        p : int, optional
-            Amount of nearest neighbors to use while building the graph.
-        scheme : str, either '0-1', 'heat' or 'dot', optional
-            Weighting scheme to use while building the graph.
-        sigma : float, optional
-            Parameter for heat weighting scheme. Ignored if scheme is not 'heat'.
-
-        Notes
-        -----
-        For more details see `this paper
-        <http://www.cad.zju.edu.cn/home/dengcai/Publication/Conference/2010_KDD-MCFS.pdf/>`_.
-
-        Examples
-        --------
-
-    """
-
-    def __scheme_01(self, x1, x2):
-        return 1
-
-    def __scheme_heat(self, x1, x2):
-        return np.exp(-np.linalg.norm(x1 - x2) ** 2 / (self.sigma ** 2))
-
-    def ___scheme_dot(self, x1, x2):
-        return (x1 / np.linalg.norm(x1)).dot(x2 / np.linalg.norm(x2))
-
-    def __init__(self, d, k=5, p=5, scheme='dot', sigma=1):
-        if scheme not in ['0-1', 'heat', 'dot']:
-            raise KeyError('scheme should be either 0-1, heat or dot; %r passed' % scheme)
-        if scheme == '0-1':
-            self.scheme = self.__scheme_01
-        elif scheme == 'heat':
-            self.scheme = self.__scheme_heat
-        else:
-            self.scheme = self.__scheme_dot
-        self.d = d
-        self.k = k
-        self.p = p
-        self.sigma = sigma
-
-    def run(self, X, y=None):
-        """
-            Fits filter
-
-            Parameters
-            ----------
-            X : numpy array, shape (n_samples, n_features)
-                The training input samples.
-            y : numpy array, optional
-                The target values (ignored).
-
-            Returns
-            ----------
-            W : array-like, shape (n_features, k)
-                Feature weight matrix.
-
-            See Also
-            --------
-
-            examples
-            --------
-            from ITMO_FS.filters.sparse import MCFS
-            from sklearn.datasets import make_classification
-            import numpy as np
-
-            dataset = make_classification(n_samples=100, n_features=20, n_informative=4, n_redundant=0, shuffle=False)
-            data, target = np.array(dataset[0]), np.array(dataset[1])
-            model = MCFS(d=5, k=2, scheme='heat')
-            weights = model.run(data, target)
-            print(model.feature_ranking(weights))
-
-        """
-        n_samples, n_features = X.shape
-        graph = NearestNeighbors(n_neighbors=self.p + 1, algorithm='ball_tree').fit(X).kneighbors_graph(X).toarray()
-        graph = graph + graph.T
-
-        indices = [[(i, j) for j in range(n_samples)] for i in range(n_samples)]
-        func = np.vectorize(lambda xy: graph[xy[0]][xy[1]] * self.scheme(X[xy[0]], X[xy[1]]), signature='(1)->()')
-        W = func(indices)
-
-        D = np.diag(W.sum(axis=0))
-        L = D - W
-        eigvals, Y = eigh(type=1, a=L, b=D, eigvals=(0, self.k - 1))
-
-        weights = np.zeros((n_features, self.k))
-        for i in range(self.k):
-            clf = Lars(n_nonzero_coefs=self.d)
-            clf.fit(X, Y[:, i])
-            weights[:, i] = clf.coef_
-
-        return weights
-
-    def feature_ranking(self, W):
-        """
-            Calculate the MCFS score for a feature weight matrix.
-
-            Parameters
-            ----------
-            W : array-like, shape (n_features, k)
-                Feature weight matrix.
-
-            Returns
-            -------
-            indices : array-like, shape (d)
-                Indices of d selected features.
-        """
-        mcfs_score = W.max(axis=1)
-        ranking = np.argsort(mcfs_score)[::-1]
-        return ranking[:self.d]
diff --git a/ITMO_FS/filters/sparse/NDFS.py b/ITMO_FS/filters/sparse/NDFS.py
deleted file mode 100644
index f62b474c..00000000
--- a/ITMO_FS/filters/sparse/NDFS.py
+++ /dev/null
@@ -1,154 +0,0 @@
-import numpy as np
-from sklearn.cluster import KMeans
-from sklearn.neighbors import NearestNeighbors
-from sklearn.preprocessing import OneHotEncoder
-
-from ...utils import l21_norm, matrix_norm, power_neg_half
-
-
-class NDFS(object):
-    """
-        Performs the Nonnegative Discriminative Feature Selection algorithm.
-
-        Parameters
-        ----------
-        p : int
-            Number of features to select.
-        c : int, optional
-            Amount of clusters to find.
-        k : int, optional
-            Amount of nearest neighbors to use while building the graph.
-        alpha : float, optional
-            Parameter in the objective function.
-        beta : float, optional
-            Regularization parameter in the objective function.
-        gamma : float, optional
-            Parameter in the objective function that controls the orthogonality condition.
-        sigma : float, optional
-            Parameter for the weighting scheme.
-        max_iterations : int, optional
-            Maximum amount of iterations to perform.
-        epsilon : positive float, optional
-            Specifies the needed residual between the target functions from consecutive iterations. If the residual
-            is smaller than epsilon, the algorithm is considered to have converged.
-
-        See Also
-        --------
-        http://www.nlpr.ia.ac.cn/2012papers/gjhy/gh27.pdf
-
-        Examples
-        --------
-
-    """
-
-    def __init__(self, p, c=5, k=5, alpha=1, beta=1, gamma=10e8, sigma=1, max_iterations=1000, epsilon=1e-5):
-        self.p = p
-        self.c = c
-        self.k = k
-        self.alpha = alpha
-        self.beta = beta
-        self.gamma = gamma
-        self.sigma = sigma
-        self.max_iterations = max_iterations
-        if epsilon < 0:
-            raise ValueError("Epsilon should be positive, %d passed" % epsilon)
-        self.epsilon = epsilon
-
-    def __scheme(self, x1, x2):
-        return np.exp(-np.linalg.norm(x1 - x2) ** 2 / (self.sigma ** 2))
-
-    def run(self, X, y=None):
-        """
-            Fits filter
-
-            Parameters
-            ----------
-            X : numpy array, shape (n_samples, n_features)
-                The training input samples.
-            y : numpy array, shape (n_samples) or (n_samples, n_classes), optional
-                The target values or their one-hot encoding that are used to compute F. If not present, a k-means clusterization algorithm is used.
-                If present, n_classes should be equal to c.
-
-            Returns
-            ----------
-            W : array-like, shape (n_features, c)
-                Feature weight matrix.
-
-            See Also
-            --------
-
-            Examples
-            --------
-            >>> from ITMO_FS.filters.sparse import NDFS
-            >>> import numpy as np
-            >>> X = np.array([[1, 2, 3, 3, 1],[2, 2, 3, 3, 2], [1, 3, 3, 1, 3],\
-[3, 1, 3, 1, 4],[4, 4, 3, 1, 5]], dtype = np.integer)
-            >>> y = np.array([1, 2, 3, 4, 5], dtype=np.integer)
-            >>> model = NDFS(p=5, c=2)
-            >>> weights = model.run(X)
-            >>> model.feature_ranking(weights)
-        """
-
-        n_samples, n_features = X.shape
-        graph = NearestNeighbors(n_neighbors=self.p + 1, algorithm='ball_tree').fit(X).kneighbors_graph(X).toarray()
-        graph = graph + graph.T
-
-        indices = [[(i, j) for j in range(n_samples)] for i in range(n_samples)]
-        func = np.vectorize(lambda xy: graph[xy[0]][xy[1]] * self.__scheme(X[xy[0]], X[xy[1]]), signature='(1)->()')
-        S = func(indices)
-
-        A = np.diag(S.sum(axis=0))
-        L = power_neg_half(A).dot(A - S).dot(power_neg_half(A))
-
-        if y is not None:
-            if len(y.shape) == 2:
-                Y = y
-            else:
-                Y = OneHotEncoder().fit_transform(y.reshape(-1, 1)).toarray()
-        else:
-            Y = self.__run_kmeans(X)
-        F = Y.dot(power_neg_half(Y.T.dot(Y)))
-        D = np.eye(n_features)
-        I = np.eye(n_samples)
-
-        previous_target = 0
-        W=np.zeros(n_features)
-        for step in range(self.max_iterations):
-            M = L + self.alpha * (I - X.dot(np.linalg.inv(X.T.dot(X) + self.beta * D)).dot(X.T))
-            F = F * ((self.gamma * F) / (M.dot(F) + self.gamma * F.dot(F.T).dot(F)))
-            W = np.linalg.inv(X.T.dot(X) + self.beta * D).dot(X.T.dot(F))
-            diag = 2 * matrix_norm(W)
-            diag[diag < 1e-10] = 1e-10  # prevents division by zero
-            D = np.diag(1 / diag)
-
-            target = np.trace(F.T.dot(L).dot(F)) + self.alpha * (np.linalg.norm(X.dot(W) - F) + self.beta * l21_norm(W))
-            if step > 0 and abs(target - previous_target) < self.epsilon:
-                break
-            previous_target = target
-
-        return W
-
-    def feature_ranking(self, W):
-        """
-            Calculate the NDFS score for a feature weight matrix.
-
-            Parameters
-            ----------
-            W : array-like, shape (n_features, c)
-                Feature weight matrix.
-
-            Returns
-            -------
-            indices : array-like, shape(p)
-                Indices of p selected features.
-        """
-        ndfs_score = matrix_norm(W)
-        ranking = np.argsort(ndfs_score)[::-1]
-        return ranking[:self.p]
-
-    def __run_kmeans(self, X):
-        n_samples, n_features = X.shape
-        kmeans = KMeans(n_clusters=self.c, copy_x=True)
-        kmeans.fit(X)
-        labels = kmeans.labels_
-        return OneHotEncoder().fit_transform(labels.reshape(-1, 1)).toarray()
diff --git a/ITMO_FS/filters/sparse/RFS.py b/ITMO_FS/filters/sparse/RFS.py
deleted file mode 100644
index cdba9b87..00000000
--- a/ITMO_FS/filters/sparse/RFS.py
+++ /dev/null
@@ -1,112 +0,0 @@
-import numpy as np
-from sklearn.preprocessing import OneHotEncoder
-from ...utils import l21_norm, matrix_norm
-
-
-class RFS(object):
-    """
-        Performs the Robust Feature Selection via Joint L2,1-Norms Minimization algorithm.
-
-        Parameters
-        ----------
-        p : int
-            Number of features to select.
-        gamma : float, optional
-            Regularization parameter.
-        max_iterations : int, optional
-            Maximum amount of iterations to perform.
-        epsilon : positive float, optional
-            Specifies the needed residual between the target functions from consecutive iterations. If the residual
-            is smaller than epsilon, the algorithm is considered to have converged.
-
-        Notes
-        -----
-        For more details see `this paper
-        <https://papers.nips.cc/paper/3988-efficient-and-robust-feature-selection-via-joint-l21-norms-minimization.pdf/>`_.
-
-
-        Examples
-        --------
-        >>> from ITMO_FS.filters.sparse import RFS
-        >>> from sklearn.datasets import make_classification
-        >>> import numpy as np
-        >>> dataset = make_classification(n_samples=100, n_features=20, n_informative=4, n_redundant=0, shuffle=False)
-        >>> data, target = np.array(dataset[0]), np.array(dataset[1])
-        >>> model = RFS(gamma=15, epsilon=1e-12)
-        >>> print(model.run(data, target))
-    """
-
-    def __init__(self, p, gamma=1, max_iterations=1000, epsilon=1e-5):
-        self.p = p
-        self.gamma = gamma
-        self.max_iterations = max_iterations
-        if epsilon < 0:
-            raise ValueError("Epsilon should be positive, %d passed" % epsilon)
-        self.epsilon = epsilon
-
-    def run(self, X, y):
-        """
-            Fits the algorithm.
-
-            Parameters
-            ----------
-            X : array-like, shape (n_samples, n_features)
-                The training input samples.
-            y : array-like, shape (n_samples) or (n_samples, n_classes)
-                The target values or their one-hot encoding.
-
-            Returns
-            ----------
-            W : array-like, shape (n_features, n_classes)
-                Feature weight matrix.
-
-            See Also
-            --------
-
-            Examples
-            --------
-
-        """
-
-        if len(y.shape) == 2:
-            Y = y
-        else:
-            Y = OneHotEncoder().fit_transform(y.reshape(-1, 1)).toarray()
-
-        n_samples, n_features = X.shape
-        A = np.append(X, self.gamma * np.eye(n_samples), axis=1)
-        D = np.eye(n_samples + n_features)
-
-        previous_target = 0
-        for step in range(self.max_iterations):
-            D_inv = np.linalg.inv(D)
-            U = D_inv.dot(A.T).dot(np.linalg.inv(A.dot(D_inv).dot(A.T))).dot(Y)
-            U = np.dot(np.dot(np.dot(D_inv, A.T), np.linalg.inv(np.dot(np.dot(A, D_inv), A.T))), Y)
-            diag = 2 * matrix_norm(U)
-            diag[diag < 1e-10] = 1e-10  # prevents division by zero
-            D = np.diag(1 / diag)
-
-            target = l21_norm(X.dot(U[:n_features]) - Y) + self.gamma * l21_norm(U[:n_features])
-            if step > 0 and abs(target - previous_target) < self.epsilon:
-                break
-            previous_target = target
-
-        return U[:n_features]
-
-    def feature_ranking(self, W):
-        """
-            Calculate the RFS score for a feature weight matrix.
-
-            Parameters
-            ----------
-            W : array-like, shape (n_features, c)
-                Feature weight matrix.
-
-            Returns
-            -------
-            indices : array-like, shape(p)
-                Indices of p selected features.
-        """
-        ndfs_score = matrix_norm(W)
-        ranking = np.argsort(ndfs_score)[::-1]
-        return ranking[:self.p]
diff --git a/ITMO_FS/filters/sparse/SPEC.py b/ITMO_FS/filters/sparse/SPEC.py
deleted file mode 100644
index 2cae1b03..00000000
--- a/ITMO_FS/filters/sparse/SPEC.py
+++ /dev/null
@@ -1,136 +0,0 @@
-import numpy as np
-from scipy.linalg import eigh
-from scipy.spatial.distance import cosine
-from ...utils import knn, l21_norm, matrix_norm, power_neg_half
-
-
-class SPEC(object):
-    """
-        Performs the Spectral Feature Selection algorithm.
-
-        Parameters
-        ----------
-        p : int
-            Number of features to select.
-        k : int, optional
-            Amount of clusters to find.
-        gamma : callable, optional
-            An "increasing function that penalizes high frequency components". Default is gamma(x) = x^2.
-        sigma : float, optional
-            Parameter for the weighting scheme.
-        phi_type : int (1, 2 or 3), optional
-            Type of feature ranking function to use.
-
-        Notes
-        -----
-        For more details see `this paper <https://www.ijcai.org/Proceedings/11/Papers/267.pdf/>`_.
-
-
-        Examples
-        --------
-
-    """
-
-    def __init__(self, p, k=5, gamma=(lambda x: x ** 2), sigma=0.5, phi_type=1):
-        self.p = p
-        self.k = k
-        self.gamma = gamma
-        self.sigma = sigma
-        if phi_type == 1:
-            self.phi = self.__phi1
-        elif phi_type == 2:
-            self.phi = self.__phi2
-        elif phi_type == 3:
-            self.phi = self.__phi3
-        else:
-            raise ValueError("phi_type should be 1, 2 or 3, %d passed" % phi_type)
-
-    def __scheme(self, x1, x2):
-        return np.exp(-np.linalg.norm(x1 - x2) ** 2 / (4 * self.sigma ** 2))
-
-    def __phi1(self, cosines, eigvals):
-        return np.sum(cosines * cosines * self.gamma(eigvals))
-
-    def __phi2(self, cosines, eigvals):
-        return np.sum(cosines[1:] * cosines[1:] * self.gamma(eigvals[1:])) / np.sum(cosines[1:] * cosines[1:])
-
-    def __phi3(self, cosines, eigvals):
-        return np.sum(cosines * cosines * (self.gamma(2) - self.gamma(eigvals)))
-
-    def run(self, X, y=None):
-        """
-            Fits filter
-
-            Parameters
-            ----------
-            X : numpy array, shape (n_samples, n_features)
-                The training input samples.
-            y : numpy array, optional
-                The target values. If present, label values are used to construct the similarity graph and the amount of classes overrides k.
-
-            Returns
-            ----------
-            W : array-like, shape (n_features)
-                Feature weight matrix.
-
-            See Also
-            --------
-
-            Examples
-            --------
-            >>> from ITMO_FS.filters.sparse import SPEC
-            >>> from sklearn.datasets import make_classification
-            >>> import numpy as np
-            >>> dataset = make_classification(n_samples=100, n_features=20, n_informative=4, n_redundant=0, shuffle=False)
-            >>> data, target = np.array(dataset[0]), np.array(dataset[1])
-            >>> model = SPEC(p=5, k=2)
-            >>> weights = model.run(data, target)
-            >>> print(model.feature_ranking(weights))
-        """
-
-        def calc_weight(f):
-            f_norm = np.sqrt(D).dot(f)
-            f_norm /= np.linalg.norm(f_norm)
-
-            cosines = np.apply_along_axis(lambda vec: 1 - cosine(f_norm, vec), 0, eigvectors)
-            return self.phi(cosines, eigvals)
-
-        n_samples, n_features = X.shape
-        if (y == None).any():
-            graph = np.ones((n_samples, n_samples))
-            indices = [[(i, j) for j in range(n_samples)] for i in range(n_samples)]
-            func = np.vectorize(lambda xy: graph[xy[0]][xy[1]] * self.__scheme(X[xy[0]], X[xy[1]]), signature='(1)->()')
-            W = func(indices)
-        else:
-            values, counts = np.unique(y, return_counts=True)
-            values_dict = dict(zip(values, counts))
-            self.k = len(values)
-            W = np.array(
-                [[(lambda i, j: 1 / values_dict[y[i]] if y[i] == y[j] else 0)(i, j) for j in range(n_samples)] for i in
-                 range(n_samples)])
-
-        D = np.diag(W.sum(axis=1))
-        L = D - W
-        L_norm = power_neg_half(D).dot(L).dot(power_neg_half(D))
-        eigvals, eigvectors = eigh(a=L_norm)
-
-        return np.apply_along_axis(lambda f: calc_weight(f), 0, X)
-
-    def feature_ranking(self, W):
-        """
-            Calculate the SPEC score for a feature weight matrix.
-
-            Parameters
-            ----------
-            W : array-like, shape (n_features)
-                Feature weight matrix.
-
-            Returns
-            -------
-            indices : array-like, shape(p)
-                Indices of p selected features.
-        """
-        ranking = np.argsort(W)
-        if self.phi == self.__phi3:
-            ranking = ranking[::-1]
-        return ranking[:self.p]
diff --git a/ITMO_FS/filters/sparse/UDFS.py b/ITMO_FS/filters/sparse/UDFS.py
deleted file mode 100644
index ccbc6e8b..00000000
--- a/ITMO_FS/filters/sparse/UDFS.py
+++ /dev/null
@@ -1,131 +0,0 @@
-import numpy as np
-from scipy.linalg import eigh
-from ...utils import knn, l21_norm, matrix_norm
-
-
-class UDFS(object):
-    """
-        Performs the Unsupervised Discriminative Feature Selection algorithm.
-
-        Parameters
-        ----------
-        p : int
-            Number of features to select.
-        c : int, optional
-            Amount of clusters to find.
-        k : int, optional
-            Amount of nearest neighbors to use while building the graph.
-        gamma : float, optional
-            Regularization term in the target function.
-        l : float, optional
-            Parameter that controls the invertibility of the matrix used in computing of B.
-        max_iterations : int, optional
-            Maximum amount of iterations to perform.
-        epsilon : positive float, optional
-            Specifies the needed residual between the target functions from consecutive iterations. If the residual
-            is smaller than epsilon, the algorithm is considered to have converged.
-
-        Notes
-        -----
-        For more details see `this paper <https://www.ijcai.org/Proceedings/11/Papers/267.pdf/>`_.
-
-
-        Examples
-        --------
-
-    """
-
-    def __init__(self, p, c=5, k=5, gamma=1, l=1e-6, max_iterations=1000, epsilon=1e-5):
-        self.p = p
-        self.c = c
-        self.k = k
-        self.gamma = gamma
-        self.l = l
-        self.max_iterations = max_iterations
-        if epsilon < 0:
-            raise ValueError("Epsilon should be positive, %d passed" % epsilon)
-        self.epsilon = epsilon
-
-    def run(self, X, y=None):
-        """
-            Fits filter
-
-            Parameters
-            ----------
-            X : numpy array, shape (n_samples, n_features)
-                The training input samples.
-            y : numpy array, optional
-                The target values (ignored).
-
-            Returns
-            ----------
-            W : array-like, shape (n_features, c)
-                Feature weight matrix.
-
-            See Also
-            --------
-
-            Examples
-            --------
-            >>> from ITMO_FS.filters.sparse import UDFS
-            >>> from sklearn.datasets import make_classification
-            >>> import numpy as np
-            >>> dataset = make_classification(n_samples=100, n_features=20, \
-n_informative=4, n_redundant=0, shuffle=False)
-            >>> data, target = np.array(dataset[0]), np.array(dataset[1])
-            >>> model = UDFS(p=5, c=2)
-            >>> weights = model.run(data)
-            >>> print(model.feature_ranking(weights))
-        """
-
-        def construct_S(arr):
-            S = np.zeros((n_samples, self.k + 1))
-            for idx in range(self.k + 1):
-                S[arr[idx], idx] = 1
-            return S
-
-        n_samples, n_features = X.shape
-        indices = list(range(n_samples))
-        H = np.eye(self.k + 1) - np.ones((self.k + 1, self.k + 1)) / (self.k + 1)
-        I = np.eye(self.k + 1)
-        neighbors = np.vectorize(lambda idx: np.append([idx], knn(X, y, idx, self.k)), signature='()->(1)')(indices)
-        X_centered = np.apply_along_axis(lambda arr: X[arr].T.dot(H), 1, neighbors)
-        S = np.apply_along_axis(lambda arr: construct_S(arr), 1, neighbors)
-        B = np.vectorize(lambda idx: np.linalg.inv(X_centered[idx].T.dot(X_centered[idx]) + self.l * I),
-                         signature='()->(1,1)')(indices)
-        Mi = np.vectorize(lambda idx: S[idx].dot(H).dot(B[idx]).dot(H).dot(S[idx].T), signature='()->(1,1)')(indices)
-        M = X.T.dot(Mi.sum(axis=0)).dot(X)
-
-        D = np.eye(n_features)
-        previous_target = 0
-        for step in range(self.max_iterations):
-            P = M + self.gamma * D
-            _, W = eigh(a=P, eigvals=(0, self.c - 1))
-            diag = 2 * matrix_norm(W)
-            diag[diag < 1e-10] = 1e-10  # prevents division by zero
-            D = np.diag(1 / diag)
-
-            target = np.trace(W.T.dot(M).dot(W)) + self.gamma * l21_norm(W)
-            if step > 0 and abs(target - previous_target) < self.epsilon:
-                break
-            previous_target = target
-
-        return W
-
-    def feature_ranking(self, W):
-        """
-            Calculate the UDFS score for a feature weight matrix.
-
-            Parameters
-            ----------
-            W : array-like, shape (n_features, c)
-                Feature weight matrix.
-
-            Returns
-            -------
-            indices : array-like, shape(p)
-                Indices of p selected features.
-        """
-        udfs_score = matrix_norm(W)
-        ranking = np.argsort(udfs_score)[::-1]
-        return ranking[:self.p]
diff --git a/ITMO_FS/filters/sparse/__init__.py b/ITMO_FS/filters/sparse/__init__.py
deleted file mode 100644
index 8cd7b3bb..00000000
--- a/ITMO_FS/filters/sparse/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from .RFS import RFS
-from .MCFS import MCFS
-from .NDFS import NDFS
-from .UDFS import UDFS
-from .SPEC import SPEC
diff --git a/ITMO_FS/filters/univariate/NDFS.py b/ITMO_FS/filters/univariate/NDFS.py
new file mode 100644
index 00000000..e5353306
--- /dev/null
+++ b/ITMO_FS/filters/univariate/NDFS.py
@@ -0,0 +1,169 @@
+from logging import getLogger
+
+import numpy as np
+from sklearn.cluster import KMeans
+from sklearn.metrics.pairwise import pairwise_distances
+from sklearn.neighbors import NearestNeighbors
+from sklearn.preprocessing import OneHotEncoder
+
+from ...utils import l21_norm, matrix_norm, power_neg_half, BaseTransformer
+
+
+class NDFS(BaseTransformer):
+    """Nonnegative Discriminative Feature Selection algorithm.
+
+    Parameters
+    ----------
+    n_features : int
+        Number of features to select.
+    c : int
+        Amount of clusters to find.
+    k : int
+        Amount of nearest neighbors to use while building the graph.
+    alpha : float
+        Parameter in the objective function.
+    beta : float
+        Regularization parameter in the objective function.
+    gamma : float
+        Parameter in the objective function that controls the orthogonality
+        condition.
+    sigma : float
+        Parameter for the weighting scheme.
+    max_iterations : int
+        Maximum amount of iterations to perform.
+    epsilon : positive float
+        Specifies the needed residual between the target functions from
+        consecutive iterations. If the residual is smaller than epsilon, the
+        algorithm is considered to have converged.
+
+    See Also
+    --------
+    http://www.nlpr.ia.ac.cn/2012papers/gjhy/gh27.pdf
+
+    Examples
+    --------
+    >>> from ITMO_FS.filters.univariate import NDFS
+    >>> import numpy as np
+    >>> X = np.array([[1, 2, 3, 3, 1], [2, 2, 3, 3, 2], [1, 3, 3, 1, 3],
+    ... [1, 1, 3, 1, 4], [2, 4, 3, 1, 5]])
+    >>> y = np.array([1, 2, 1, 1, 2])
+    >>> model = NDFS(3).fit(X, y)
+    >>> model.selected_features_
+    array([0, 3, 4], dtype=int64)
+    >>> model = NDFS(3).fit(X)
+    >>> model.selected_features_
+    array([3, 4, 1], dtype=int64)
+    """
+    def __init__(self, n_features, c=2, k=3, alpha=1, beta=1, gamma=10e8,
+                 sigma=1, max_iterations=1000, epsilon=1e-5):
+        self.n_features = n_features
+        self.c = c
+        self.k = k
+        self.alpha = alpha
+        self.beta = beta
+        self.gamma = gamma
+        self.sigma = sigma
+        self.max_iterations = max_iterations
+        self.epsilon = epsilon
+
+    def __scheme(self, x1, x2):
+        return np.exp(-np.linalg.norm(x1 - x2) ** 2 / (self.sigma ** 2))
+
+    def _fit(self, X, y, **kwargs):
+        """Fit the filter.
+
+        Parameters
+        ----------
+        X : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like, shape (n_samples,) or (n_samples, n_classes)
+            The target values or their one-hot encoding that are used to
+            compute F. If not present, a k-means clusterization algorithm
+            is used. If present, n_classes should be equal to c.
+
+        Returns
+        -------
+        None
+        """
+        n_samples = X.shape[0]
+
+        if self.k >= n_samples:
+            getLogger(__name__).error(
+                "Cannot select %d nearest neighbors with n_samples = %d",
+                self.k, n_samples)
+            raise ValueError(
+                "Cannot select %d nearest neighbors with n_samples = %d"
+                % (self.k, n_samples))
+
+        graph = NearestNeighbors(
+            n_neighbors=self.k,
+            algorithm='ball_tree').fit(X).kneighbors_graph().toarray()
+        graph = np.minimum(1, graph + graph.T)
+        getLogger(__name__).info("Nearest neighbors graph: %s", graph)
+
+        S = graph * pairwise_distances(
+            X, metric=lambda x, y: self.__scheme(x, y))
+        getLogger(__name__).info("S: %s", S)
+        A = np.diag(S.sum(axis=0))
+        getLogger(__name__).info("A: %s", A)
+        L = power_neg_half(A).dot(A - S).dot(power_neg_half(A))
+        getLogger(__name__).info("L: %s", L)
+
+        if y is not None:
+            if len(y.shape) == 2:
+                Y = y
+            else:
+                Y = OneHotEncoder().fit_transform(y.reshape(-1, 1)).toarray()
+        else:
+            if self.c > n_samples:
+                getLogger(__name__).error(
+                    "Cannot find %d clusters with n_samples = %d", self.c,
+                    n_samples)
+                raise ValueError(
+                    "Cannot find %d clusters with n_samples = %d"
+                    % (self.c, n_samples))
+            Y = self.__run_kmeans(X)
+        getLogger(__name__).info("Transformed Y: %s", Y)
+        F = Y.dot(power_neg_half(Y.T.dot(Y)))
+        getLogger(__name__).info("F: %s", F)
+        D = np.eye(self.n_features_)
+        In = np.eye(n_samples)
+        Ic = np.eye(Y.shape[1])
+
+        previous_target = -1
+        for _ in range(self.max_iterations):
+            M = (L + self.alpha
+                * (In - X.dot(
+                    np.linalg.inv(X.T.dot(X) + self.beta * D)).dot(X.T)))
+            getLogger(__name__).info("M: %s", M)
+            F = (F * ((self.gamma * F)
+                       / (M.dot(F) + self.gamma * F.dot(F.T).dot(F))))
+            getLogger(__name__).info("F: %s", F)
+            W = np.linalg.inv(X.T.dot(X) + self.beta * D).dot(X.T.dot(F))
+            getLogger(__name__).info("W: %s", W)
+            diag = 2 * matrix_norm(W)
+            diag[diag < 1e-10] = 1e-10  # prevents division by zero
+            D = np.diag(1 / diag)
+            getLogger(__name__).info("D: %s", D)
+
+            target = (np.trace(F.T.dot(L).dot(F))
+                + self.alpha * (np.linalg.norm(X.dot(W) - F) ** 2
+                    + self.beta * l21_norm(W))
+                + self.gamma * (np.linalg.norm(F.T.dot(F) - Ic) ** 2) / 2)
+            getLogger(__name__).info("New target value: %d", target)
+            if abs(target - previous_target) < self.epsilon:
+                break
+            previous_target = target
+
+        getLogger(__name__).info("Ended up with W: %s", W)
+        self.feature_scores_ = matrix_norm(W)
+        getLogger(__name__).info("Feature scores: %s", self.feature_scores_)
+        ranking = np.argsort(self.feature_scores_)[::-1]
+        self.selected_features_ = ranking[:self.n_features]
+
+    def __run_kmeans(self, X):
+        kmeans = KMeans(n_clusters=self.c, copy_x=True)
+        kmeans.fit(X)
+        labels = kmeans.labels_
+        getLogger(__name__).info("Labels from KMeans: %s", labels)
+        return OneHotEncoder().fit_transform(labels.reshape(-1, 1)).toarray()
diff --git a/ITMO_FS/filters/univariate/RFS.py b/ITMO_FS/filters/univariate/RFS.py
new file mode 100644
index 00000000..d52e6eb1
--- /dev/null
+++ b/ITMO_FS/filters/univariate/RFS.py
@@ -0,0 +1,92 @@
+from logging import getLogger
+
+import numpy as np
+from sklearn.preprocessing import OneHotEncoder
+
+from ...utils import l21_norm, matrix_norm, BaseTransformer
+
+
+class RFS(BaseTransformer):
+    """Robust Feature Selection via Joint L2,1-Norms Minimization algorithm.
+
+    Parameters
+    ----------
+    n_features : int
+        Number of features to select.
+    gamma : float
+        Regularization parameter.
+    max_iterations : int
+        Maximum amount of iterations to perform.
+    epsilon : positive float
+        Specifies the needed residual between the target functions from
+        consecutive iterations. If the residual is smaller than epsilon, the
+        algorithm is considered to have converged.
+
+    Notes
+    -----
+    For more details see `this paper
+    <https://papers.nips.cc/paper/3988-efficient-and-robust-feature-selection-via-joint-l21-norms-minimization.pdf/>`_.
+
+    Examples
+    --------
+    >>> from ITMO_FS.filters.univariate import RFS
+    >>> import numpy as np
+    >>> X = np.array([[1, 2, 3, 3, 1], [2, 2, 3, 3, 2], [1, 3, 3, 1, 3],
+    ... [1, 1, 3, 1, 4], [2, 4, 3, 1, 5]])
+    >>> y = np.array([1, 2, 1, 1, 2])
+    >>> model = RFS(2).fit(X, y)
+    >>> model.selected_features_
+    array([0, 3], dtype=int64)
+    """
+    def __init__(self, n_features, gamma=1, max_iterations=1000, epsilon=1e-5):
+        self.n_features = n_features
+        self.gamma = gamma
+        self.max_iterations = max_iterations
+        self.epsilon = epsilon
+
+    def _fit(self, X, y):
+        """Fit the filter.
+
+        Parameters
+        ----------
+        X : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like, shape (n_samples,) or (n_samples, n_classes)
+            The target values or their one-hot encoding.
+
+        Returns
+        -------
+        None
+        """
+        if len(y.shape) == 2:
+            Y = y
+        else:
+            Y = OneHotEncoder().fit_transform(y.reshape(-1, 1)).toarray()
+
+        getLogger(__name__).info("Transformed Y: %s", Y)
+        n_samples = X.shape[0]
+        A = np.append(X, self.gamma * np.eye(n_samples), axis=1)
+        getLogger(__name__).info("A: %s", A)
+        D = np.eye(n_samples + self.n_features_)
+
+        previous_target = -1
+        for _ in range(self.max_iterations):
+            D_inv = np.linalg.inv(D)
+            U = D_inv.dot(A.T).dot(np.linalg.inv(A.dot(D_inv).dot(A.T))).dot(Y)
+            getLogger(__name__).info("U: %s", U)
+            diag = 2 * matrix_norm(U)
+            diag[diag < 1e-10] = 1e-10  # prevents division by zero
+            D = np.diag(1 / diag)
+            getLogger(__name__).info("D: %s", D)
+
+            target = l21_norm(U)
+            getLogger(__name__).info("New target value: %d", target)
+            if abs(target - previous_target) < self.epsilon:
+                break
+            previous_target = target
+
+        getLogger(__name__).info("Ended up with U: %s", U)
+        self.feature_scores_ = matrix_norm(U[:self.n_features_])
+        getLogger(__name__).info("Feature scores: %s", self.feature_scores_)
+        ranking = np.argsort(self.feature_scores_)[::-1]
+        self.selected_features_ = ranking[:self.n_features]
diff --git a/ITMO_FS/filters/univariate/SPEC.py b/ITMO_FS/filters/univariate/SPEC.py
new file mode 100644
index 00000000..34c70b8c
--- /dev/null
+++ b/ITMO_FS/filters/univariate/SPEC.py
@@ -0,0 +1,145 @@
+from logging import getLogger
+
+import numpy as np
+from scipy.linalg import eigh
+from sklearn.metrics.pairwise import pairwise_distances
+
+from ...utils import l21_norm, matrix_norm, power_neg_half, BaseTransformer
+
+
+class SPEC(BaseTransformer):
+    """Spectral Feature Selection algorithm.
+
+    Parameters
+    ----------
+    n_features : int
+        Number of features to select.
+    k : int
+        Amount of clusters to find.
+    gamma : callable
+        An "increasing function that penalizes high frequency components".
+        Default is gamma(x) = x^2.
+    sigma : float
+        Parameter for the weighting scheme.
+    phi_type : int (1, 2 or 3)
+        Type of feature ranking function to use.
+
+    Notes
+    -----
+    For more details see `this paper <http://www.public.asu.edu/~huanliu/papers/icml07.pdf/>`_.
+
+    Examples
+    --------
+    >>> from ITMO_FS.filters.univariate import SPEC
+    >>> import numpy as np
+    >>> X = np.array([[1, 2, 3, 3, 1], [2, 2, 3, 3, 2], [1, 3, 3, 1, 3],
+    ... [1, 1, 3, 1, 4], [2, 4, 3, 1, 5]])
+    >>> y = np.array([1, 2, 1, 1, 2])
+    >>> model = SPEC(3).fit(X, y)
+    >>> model.selected_features_
+    array([0, 1, 4], dtype=int64)
+    >>> model = SPEC(3).fit(X)
+    >>> model.selected_features_
+    array([3, 4, 1], dtype=int64)
+    """
+    def __init__(self, n_features, k=2, gamma=(lambda x: x ** 2), sigma=0.5,
+                 phi_type=3):
+        self.n_features = n_features
+        self.k = k
+        self.gamma = gamma
+        self.sigma = sigma
+        self.phi_type = phi_type
+
+    def __scheme(self, x1, x2):
+        return np.exp(-np.linalg.norm(x1 - x2) ** 2 / (2 * self.sigma ** 2))
+
+    def __phi1(self, cosines, eigvals, k):
+        return np.sum(cosines * cosines * self.gamma(eigvals))
+
+    def __phi2(self, cosines, eigvals, k):
+        return (np.sum(cosines[1:] * cosines[1:] * self.gamma(eigvals[1:]))
+                / np.sum(cosines[1:] * cosines[1:]))
+
+    def __phi3(self, cosines, eigvals, k):
+        return np.sum(cosines[1:k] * cosines[1:k]
+                      * (self.gamma(2) - self.gamma(eigvals[1:k])))
+
+    def _fit(self, X, y):
+        """Fit the filter.
+
+        Parameters
+        ----------
+        X : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like, shape (n_samples,), optional
+            The target values. If present, label values are used to
+            construct the similarity graph and the amount of classes
+            overrides k.
+
+        Returns
+        -------
+        None
+        """
+        def calc_weight(f):
+            f_norm = np.sqrt(D).dot(f)
+            f_norm /= np.linalg.norm(f_norm)
+
+            cosines = np.apply_along_axis(
+                lambda vec: np.dot(vec / np.linalg.norm(vec), f_norm), 0,
+                eigvectors)
+            return phi(cosines, eigvals, k)
+
+        if self.phi_type == 1:
+            phi = self.__phi1
+        elif self.phi_type == 2:
+            phi = self.__phi2
+        elif self.phi_type == 3:
+            phi = self.__phi3
+        else:
+            getLogger(__name__).error(
+                "phi_type should be 1, 2 or 3, %d passed", self.phi_type)
+            raise ValueError(
+                "phi_type should be 1, 2 or 3, %d passed" % self.phi_type)
+
+        n_samples = X.shape[0]
+
+        if y is None:
+            if self.k > n_samples:
+                getLogger(__name__).error(
+                    "Cannot find %d clusters with n_samples = %d",
+                    self.k, n_samples)
+                raise ValueError(
+                    "Cannot find %d clusters with n_samples = %d"
+                    % (self.k, n_samples))
+            k = self.k
+            graph = np.ones((n_samples, n_samples))
+            W = graph * pairwise_distances(
+                X, metric=lambda x, y: self.__scheme(x, y))
+        else:
+            values, counts = np.unique(y, return_counts=True)
+            values_dict = dict(zip(values, counts))
+            k = len(values)
+            W = pairwise_distances(
+                y.reshape(-1, 1),
+                metric=lambda x, y: (x[0] == y[0]) / values_dict[x[0]])
+
+        getLogger(__name__).info("W: %s", W)
+
+        D = np.diag(W.sum(axis=1))
+        getLogger(__name__).info("D: %s", D)
+        L = D - W
+        getLogger(__name__).info("L: %s", L)
+        L_norm = power_neg_half(D).dot(L).dot(power_neg_half(D))
+        getLogger(__name__).info("Normalized L: %s", L_norm)
+        eigvals, eigvectors = eigh(a=L_norm)
+        getLogger(__name__).info(
+            "Eigenvalues for normalized L: %s, eigenvectors: %s",
+            eigvals, eigvectors)
+
+        self.feature_scores_ = np.apply_along_axis(
+            lambda f: calc_weight(f), 0, X)
+        getLogger(__name__).info("Feature scores: %s", self.feature_scores_)
+        ranking = np.argsort(self.feature_scores_)
+        if self.phi_type == 3:
+            ranking = ranking[::-1]
+        self.selected_features_ = ranking[:self.n_features]
diff --git a/ITMO_FS/filters/univariate/UnivariateFilter.py b/ITMO_FS/filters/univariate/UnivariateFilter.py
index 549cfc89..f186e7e8 100644
--- a/ITMO_FS/filters/univariate/UnivariateFilter.py
+++ b/ITMO_FS/filters/univariate/UnivariateFilter.py
@@ -1,182 +1,98 @@
-from numpy import ndarray
-from sklearn.base import TransformerMixin, BaseEstimator
-
-from .measures import GLOB_CR, GLOB_MEASURE
-from ...utils import DataChecker, generate_features, check_restrictions
-
-
-class UnivariateFilter(BaseEstimator, TransformerMixin, DataChecker):  # TODO ADD LOGGING
+from logging import getLogger
+
+import numpy as np
+
+from .measures import CR_NAMES, MEASURE_NAMES
+from ...utils import (BaseTransformer, generate_features, check_restrictions,
+                      apply_cr)
+
+
+class UnivariateFilter(BaseTransformer):
+    """Basic interface for using univariate measures for feature selection.
+    List of available measures is in ITMO_FS.filters.univariate.measures, also
+    you can provide your own measure but it should suit the argument scheme for
+    measures, i.e. take two arguments x,y and return scores for all the
+    features in dataset x. Same applies to cutting rules.
+
+    Parameters
+    ----------
+    measure : string or callable
+        A metric name defined in GLOB_MEASURE or a callable with signature
+        measure (sample dataset, labels of dataset samples) which should
+        return a list of metric values for each feature in the dataset.
+    cutting_rule : string or callables
+        A cutting rule name defined in GLOB_CR or a callable with signature
+        cutting_rule (features) which should return a list of features ranked by
+        some rule.
+
+    See Also
+    --------
+
+    Examples
+    --------
+
+    >>> import numpy as np
+    >>> from ITMO_FS.filters.univariate import select_k_best
+    >>> from ITMO_FS.filters.univariate import UnivariateFilter
+    >>> from ITMO_FS.filters.univariate import f_ratio_measure
+    >>> x = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1],
+    ... [3, 1, 4, 3, 1], [3, 1, 2, 3, 1]])
+    >>> y = np.array([1, 3, 2, 1, 2])
+    >>> filter = UnivariateFilter(f_ratio_measure,
+    ... select_k_best(2)).fit(x, y)
+    >>> filter.selected_features_
+    array([4, 2], dtype=int64)
+    >>> filter.feature_scores_
+    array([0.6 , 0.2 , 1.  , 0.12, 5.4 ])
     """
-        Basic interface for using univariate measures for feature selection.
-        List of available measures is in ITMO_FS.filters.univariate.measures, also you can
-        provide your own measure but it should suit the argument scheme for measures,
-        i.e. take two arguments x,y and return scores for all the features in dataset x.
-        Same applies to cutting rules.
-
-        Parameters
-        ----------
-        measure : string or callable
-            A metric name defined in GLOB_MEASURE or a callable with signature
-            measure (sample dataset, labels of dataset samples)
-            which should return a list of metric values for each feature in the dataset.
-        cutting_rule : string or callables
-            A cutting rule name defined in GLOB_CR or a callable with signature
-            cutting_rule (features),
-            which should return a list features ranked by some rule.
-
-        See Also
-        --------
-
-        Examples
-        --------
-
-        >>> from sklearn.datasets import make_classification
-        >>> from ITMO_FS.filters.univariate import select_k_best
-        >>> from ITMO_FS.filters.univariate import UnivariateFilter
-        >>> from ITMO_FS.filters.univariate import f_ratio_measure
-        >>> x, y = make_classification(1000, 100, n_informative = 10, n_redundant = 30, \
-n_repeated = 10, shuffle = False)
-        >>> ufilter = UnivariateFilter(f_ratio_measure, select_k_best(10))
-        >>> ufilter.fit(x, y)
-    """
-
-
-    def __init__(self, measure, cutting_rule=("Best by percentage",  0.2)):
-        # TODO Check measure and cutting_rule
-        super().__init__()
-        if type(measure) is str:
-            try:
-                self.measure = GLOB_MEASURE[measure]
-            except KeyError:
-                raise KeyError("No %r measure yet" % measure)
-        elif hasattr(measure, '__call__'):
-            self.measure = measure
-        else:
-            raise KeyError("%r isn't a measure function or string" % measure)
-
-    def __apply_cr(self):
-        if type(self.cutting_rule) is tuple:
-            cutting_rule_name = self.cutting_rule[0]
-            cutting_rule_value = self.cutting_rule[1]
-            try:
-                self.cutting_rule = GLOB_CR[cutting_rule_name](cutting_rule_value)
-            except KeyError:
-                raise KeyError("No %r cutting rule yet" % cutting_rule_name)
-        elif hasattr(self.cutting_rule, '__call__'):
-            self.cutting_rule = self.cutting_rule
-        else:
-            raise KeyError("%r isn't a cutting rule function or string" % self.cutting_rule)
-
-        check_restrictions(self.measure.__name__, self.cutting_rule.__name__)
+    def __init__(self, measure, cutting_rule=("Best by percentage", 1.0)):
+        self.measure = measure
+        self.cutting_rule = cutting_rule
 
     def __apply_ms(self):
-        if type(self.measure) is str:
+        if isinstance(self.measure, str):
             try:
-                self.measure = GLOB_MEASURE[self.measure]
+                measure = MEASURE_NAMES[self.measure]
             except KeyError:
-                raise KeyError("No %r measure yet" % self.measure)
+                getLogger(__name__).error("No %s measure yet", self.measure)
+                raise KeyError("No %s measure yet" % self.measure)
         elif hasattr(self.measure, '__call__'):
-            self.measure = self.measure
+            measure = self.measure
         else:
-            raise KeyError("%r isn't a measure function or string" % self.measure)
-
-    def get_scores(self, X, y, feature_names):
-        """
-            Counts feature scores on given data.
-
-            Parameters
-            ----------
-            X : array-like, shape (n_features, n_samples)
-                The training input samples.
-            y : array-like, shape (n_samples, )
-                The target values.
-            feature_names : list of strings
-                In case you want to define feature names
-
-            Returns
-            ------
-            dictionary of format: key - feature_names, values - feature scores
+            getLogger(__name__).error(
+                "%s isn't a measure function or string", self.measure)
+            raise KeyError(
+                "%s isn't a measure function or string" % self.measure)
+        return measure
 
-        """
-        self.__apply_ms()
-        return dict(zip(feature_names, self.measure(X, y)))
-
-    def fit_transform(self, X, y=None, feature_names=None, store_scores=False, **fit_params):
-        """
-            Fits the filter and transforms given dataset X.
-
-            Parameters
-            ----------
-            X : array-like, shape (n_features, n_samples)
-                The training input samples.
-            y : array-like, shape (n_samples, ), optional
-                The target values.
-            feature_names : list of strings, optional
-                In case you want to define feature names
-            store_scores : boolean, optional (by default False)
-                In case you want to store the scores of features
-                for future calls to Univariate filter
-            **fit_params :
-                dictonary of measure parameter if needed.
-
-            Returns
-            ------
-            X dataset sliced with features selected by the filter
-        """
-        self.fit(X, y, feature_names, store_scores)
-        return self.transform(X)
-
-    def fit(self, X, y, feature_names=None, store_scores=True):
-        """
-            Fits the filter.
+    def _fit(self, X, y, store_scores=True):
+        """Fit the filter.
 
-            Parameters
-            ----------
-            X : array-like, shape (n_features, n_samples)
-                The training input samples.
-            y : array-like, shape (n_samples, )
-                The target values.
-            feature_names : list of strings, optional
-                In case you want to define feature names
-            store_scores : boolean, optional (by default False)
-                In case you want to store the scores of features
-                for future calls to Univariate filter
-
-            Returns
-            ------
-            None
+        Parameters
+        ----------
+        X : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like, shape (n_samples,)
+            The target values.
+        store_scores : boolean, optional
+            In case you want to store the scores of features
+            for future calls to Univariate filter; default True
+
+        Returns
+        -------
+        None
         """
+        measure = self.__apply_ms()
+        cutting_rule = apply_cr(self.cutting_rule)
+        getLogger(__name__).info(
+            "Using UnivariateFilter with measure %s and cutting rule %s",
+            measure, cutting_rule)
 
-        self.__apply_ms()
-        self.__apply_cr()
+        check_restrictions(measure.__name__, cutting_rule.__name__)
 
-        X, y, feature_names = self._check_input(X, y, feature_names)
-        features = generate_features(X, feature_names)
-        self.feature_names = dict(zip(features, feature_names))
-        feature_scores = self.get_scores(X, y, features)
+        feature_scores = measure(X, y)
+        getLogger(__name__).info("Feature scores: %s", feature_scores)
 
         if store_scores:
-            self.feature_scores = feature_scores
-        self.selected_features = self.cutting_rule(feature_scores)
-
-    def transform(self, X):
-        """
-            Slices given dataset by previously selected features.
-
-            Parameters
-            ----------
-            X : array-like, shape (n_features, n_samples)
-                The training input samples.
-
-            Returns
-            ------
-            X dataset sliced with features selected by the filter
-        """
-        if type(X) is ndarray:
-            return X[:, self.selected_features]
-        else:
-            return X[self.selected_features]
-
-    def __repr__(self, **kwargs):
-        return "Univariate filter with measure {} and cutting rule {}".format(self.measure.__name__,
-                                                                              self.cutting_rule.__name__)
+            self.feature_scores_ = feature_scores
+        self.selected_features_ = cutting_rule(feature_scores)
diff --git a/ITMO_FS/filters/univariate/VDM.py b/ITMO_FS/filters/univariate/VDM.py
index 4e9f3355..88570763 100644
--- a/ITMO_FS/filters/univariate/VDM.py
+++ b/ITMO_FS/filters/univariate/VDM.py
@@ -1,24 +1,29 @@
 import numpy as np
 
 from ITMO_FS.utils.functions import cartesian
+from ...utils import BaseTransformer
 
 
 #  TODO some optimization and sklearn-like API
-class VDM:
+class VDM(BaseTransformer):
     """
-        Creates Value Difference Metric builder
-        http://aura.abdn.ac.uk/bitstream/handle/2164/10951/payne_ecai_98.pdf?sequence=1
-        https://www.jair.org/index.php/jair/article/view/10182
+        Creates Value Difference Metric builder.
+        For continious features discretesation requered.
 
         Parameters
         ----------
         weighted: bool
-            If weighted = False, modified version of metric which omits the weights is used
+            If weighted = False, modified version of metric which omits the
+            weights is used
+        q: int
+            Power in VDM usually 1 or 2
 
         Notes
         -----
-        For more details see papers about `Improved Heterogeneous Distance Functions
-        <https://www.jair.org/index.php/jair/article/view/10182/>`_ and `Implicit Future Selection with the VDM
+        For more details see papers about
+        `Improved Heterogeneous Distance Functions
+        <https://www.jair.org/index.php/jair/article/view/10182/>`_
+        and `Implicit Future Selection with the VDM
         <https://aura.abdn.ac.uk/bitstream/handle/2164/10951/payne_ecai_98.pdf?sequence=1/>`_.
 
         Examples
@@ -30,111 +35,146 @@ class VDM:
         ...               1,
         ...               1])
         >>> vdm = VDM()
-        >>> vdm.run(x, y)
+        >>> vdm.fit(x, y)
         array([[0.         4.35355339 4.        ]
                [4.5        0.         0.5       ]
                [4.         0.35355339 0.        ]])
     """
 
-    def __init__(self, weighted=True):
+    def __init__(self, weighted=True, q=1):
         self.weighted = weighted
+        self.q = q
 
-    def run(self, x, y):
+    def _fit(self, X, y=None, **kwargs):
         """
             Generates metric for the data
-            Complexity: O(n_features * n_samples^3) worst case, should be faster on a real data
+            Complexity: O(n_features * n_samples^3) worst case, should be
+            faster on a real data.
 
             Parameters
             ----------
-            x: array-like, shape (n_features, n_samples)
-                Input samples' parameters. Parameters among every class must be sequential integers.
+            X: array-like, shape (n_features, n_samples)
+                Input samples' parameters. Parameters among every class must be
+                sequential integers.
             y: array-like, shape (n_samples)
-                Input samples' class labels. Class labels must be sequential integers.
+                Input samples' class labels. Class labels must be sequential
+                integers.
             Returns
             -------
             result:
-                numpy.ndarray, shape=(n_samples, n_samples), dtype=np.double with selected version of metrics
+                numpy.ndarray, shape=(n_samples, n_samples), dtype=np.double
+                with selected version of metrics
             See Also
             --------
-    feature_scores = {}
-    def run(self, x, y, weighted=True):
         """
-        # TODO Fix case of y passed as DataFrame. For now y is transformed to 2D array and this causes an error.
-        #  It seems better to follow usual sklearn practice and to use check_X_y but np.asarray(y[0]) is also possible
-        x = np.asarray(x)  # Converting input data to numpy arrays
-        y = np.asarray(y)
-
+        # TODO Fix case of y passed as DataFrame. For now y is transformed
+        #  to 2D array and this causes an error. It seems better to follow
+        #  usual sklearn practice and to use check_X_y but np.asarray(y[0])
+        #  is also possible
         n_labels = np.max(y) + 1  # Number of different class labels
-        n_samples = x.shape[0]  # Number of samples
+        n_samples = X.shape[0]  # Number of samples
 
-        vdm = np.zeros((n_samples, n_samples), dtype=np.double)  # Initializing output matrix
+        vdm = np.zeros((n_samples, n_samples),
+                       dtype=np.double)  # Initializing output matrix
 
-        for feature in x.T:  # For each attribute:
+        for feature in X.T:  # For each attribute:
             # Initializing utility structures:
-            n_values = np.max(feature) + 1  # Number of different values for the feature
+            n_values = np.max(
+                feature) + 1  # Number of different values for the feature
 
-            entries_x = np.empty(n_values, dtype=object)  # Array containing list of indexes for every feature value
+            entries_x = np.empty(n_values,
+                                 dtype=object)  # Array containing list of
+            # indexes for every feature value
             entries_x[:] = [[] for _ in range(n_values)]
 
-            entries_c_x = np.array([{} for _ in range(n_labels)])  # Array of dirs of kind
+            entries_c_x = np.array(
+                [{} for _ in range(n_labels)])  # Array of dirs of kind
             # {(feature value, amount of entries) for each class label
 
             for i, value in enumerate(feature):  # For each sample:
-                entries_x[value].append(i)  # Adding sample index to entries list
-                entries_c_x[y[i]][value] = entries_c_x[y[i]].get(value, 0) + 1  # Adding entry for corresponding
+                entries_x[value].append(
+                    i)  # Adding sample index to entries list
+                entries_c_x[y[i]][value] = entries_c_x[y[i]].get(value,
+                                                                 0) + 1  #
+                # Adding entry for corresponding
                 # class label
 
-            amounts_x = np.array(list(map(len, entries_x)))  # Array containing amounts of samples
+            amounts_x = np.array(list(
+                map(len, entries_x)))  # Array containing amounts of samples
             # for every feature value
 
             # Calculating deltas:
 
-            deltas = np.zeros((n_values, n_values), dtype=np.double)  # Array for calculating deltas
+            deltas = np.zeros((n_values, n_values),
+                              dtype=np.double)  # Array for calculating deltas
 
-            # Calculating components where exactly one of probabilities is not zero:
+            # Calculating components where exactly one of probabilities is
+            # not zero:
             for c in range(n_labels):  # For each class:
-                entries = np.array(list(entries_c_x[c].keys()))  # Feature values which are presented in pairs for
+                entries = np.array(list(entries_c_x[
+                                            c].keys()))  # Feature values
+                # which are presented in pairs for
                 # the class
-                amounts = np.array(list(entries_c_x[c].values()))  # Corresponding amounts
-                non_entries = np.arange(n_values)  # Feature values which are not presented in pairs for the class
+                amounts = np.array(
+                    list(entries_c_x[c].values()))  # Corresponding amounts
+                non_entries = np.arange(
+                    n_values)  # Feature values which are not presented in pairs for the class
                 # TODO get rid of error if entries are empty, example in test
                 non_entries[entries] = -1
                 non_entries = non_entries[non_entries != -1]
 
                 for i in range(len(entries)):  # For each feature value
                     value = entries[i]  # Current value
-                    v_c_instances = amounts[i]  # Amount of instances with such value and such class
-                    v_instances = amounts_x[value]  # Amount of instances with such value
-                    target_x, target_y = cartesian([value], non_entries)  # Target indexes for deltas array
-                    deltas[target_x, target_y] += (v_c_instances / v_instances) ** 2
+                    v_c_instances = amounts[
+                        i]  # Amount of instances with such value and such class
+                    v_instances = amounts_x[
+                        value]  # Amount of instances with such value
+                    target_x, target_y = cartesian([value],
+                                                   non_entries)  # Target indexes for deltas array
+                    deltas[target_x, target_y] += (
+                                                          v_c_instances / v_instances) ** 2
             deltas += deltas.T  # As we didn't determined indexes order, for each i, j some components are
             # written to delta(i, j) while others to delta(j, i), but exactly once. Adding transposed matrix to fix this
 
             # Calculating components where both probabilities are not zero:
             for c in range(n_labels):  # For each class:
-                entries = np.array(list(entries_c_x[c].keys()))  # Feature values which are presented in pairs for
+                entries = np.array(list(entries_c_x[
+                                            c].keys()))  # Feature values which are presented in pairs for
                 # the class
-                amounts = np.array(list(entries_c_x[c].values()))  # Corresponding amounts
-                probs = amounts / amounts_x[entries]  # Conditional probabilities
-                target_x, target_y = cartesian(np.arange(len(entries)), np.arange(len(entries)))  # Target indexes
+                amounts = np.array(
+                    list(entries_c_x[c].values()))  # Corresponding amounts
+                probs = amounts / amounts_x[
+                    entries]  # Conditional probabilities
+                target_x, target_y = cartesian(np.arange(len(entries)),
+                                               np.arange(len(
+                                                   entries)))  # Target indexes
                 # for deltas array
-                deltas[entries[target_x], entries[target_y]] += (probs[target_x] - probs[target_y]) ** 2
+                deltas[entries[target_x], entries[target_y]] += (probs[
+                                                                     target_x] -
+                                                                 probs[
+                                                                     target_y]) ** 2
 
             # Updating vdm:
             if not self.weighted:  # If non-weighted version of metrics was selected
                 for i in range(n_values):  # For each value i
                     for j in range(n_values):  # For each value j
-                        if amounts_x[i] == 0 or amounts_x[j] == 0:  # If some value does not appear in current feature,
+                        if amounts_x[i] == 0 or amounts_x[
+                            j] == 0:  # If some value does not appear in current feature,
                             # skip it
                             continue
-                        vdm[cartesian(entries_x[i], entries_x[j])] += deltas[i][j]
+                        vdm[cartesian(entries_x[i], entries_x[j])] += \
+                            deltas[i][j]
             else:  # If weighted version of metrics was selected
-                weights = np.zeros(n_values, dtype=np.double)  # Initializing weights array
+                weights = np.zeros(n_values,
+                                   dtype=np.double)  # Initializing weights array
                 for c in range(n_labels):  # For each class:
-                    entries = np.array(list(entries_c_x[c].keys()))  # Feature values which are presented in pairs for
+                    entries = np.array(list(entries_c_x[
+                                                c].keys()))  # Feature values which are presented in pairs for
                     # the class
-                    amounts = np.array(list(entries_c_x[c].values()))  # Corresponding amounts
-                    probs = amounts / amounts_x[entries]  # Conditional probabilities
+                    amounts = np.array(
+                        list(entries_c_x[c].values()))  # Corresponding amounts
+                    probs = amounts / amounts_x[
+                        entries]  # Conditional probabilities
                     weights[entries] += probs ** 2
                 weights = np.sqrt(weights)
 
@@ -142,6 +182,7 @@ def run(self, x, y, weighted=True):
                     for j in range(n_values):  # For each value j
                         if amounts_x[i] == 0 or amounts_x[j] == 0:
                             continue
-                        vdm[cartesian(entries_x[i], entries_x[j])] += deltas[i][j] * weights[i]
+                        vdm[cartesian(entries_x[i], entries_x[j])] += \
+                            deltas[i][j] * weights[i]
 
         return vdm
diff --git a/ITMO_FS/filters/univariate/__init__.py b/ITMO_FS/filters/univariate/__init__.py
index 5173d2b7..c70adb59 100644
--- a/ITMO_FS/filters/univariate/__init__.py
+++ b/ITMO_FS/filters/univariate/__init__.py
@@ -1,6 +1,12 @@
 from .UnivariateFilter import UnivariateFilter
 from .VDM import VDM
-from .measures import anova, fit_criterion_measure, f_ratio_measure, gini_index, su_measure, modified_t_score, fechner_corr, \
-    information_gain, reliefF_measure, chi2_measure, spearman_corr, pearson_corr, laplacian_score, qpfs_filter, \
-    kendall_corr, select_k_best, select_k_worst, select_worst_by_value, select_best_by_value, select_best_percentage,\
+from .measures import anova, fit_criterion_measure, f_ratio_measure, \
+    gini_index, su_measure, modified_t_score, fechner_corr, \
+    information_gain, relief_measure, reliefF_measure, chi2_measure, \
+    spearman_corr, pearson_corr, laplacian_score, qpfs_filter, \
+    kendall_corr, select_k_best, select_k_worst, select_worst_by_value, \
+    select_best_by_value, select_best_percentage, \
     select_worst_percentage
+from .NDFS import NDFS
+from .RFS import RFS
+from .SPEC import SPEC
diff --git a/ITMO_FS/filters/univariate/measures.py b/ITMO_FS/filters/univariate/measures.py
index 14edc150..aea63bc2 100644
--- a/ITMO_FS/filters/univariate/measures.py
+++ b/ITMO_FS/filters/univariate/measures.py
@@ -1,16 +1,17 @@
 from functools import partial, update_wrapper
 from math import exp
-from math import log
 
 import numpy as np
-from scipy import sparse as sp
 from scipy.sparse import lil_matrix
+from scipy.stats import rankdata
 from sklearn.preprocessing import MinMaxScaler
+from sklearn.metrics.pairwise import pairwise_distances, euclidean_distances
+from sklearn.neighbors import NearestNeighbors
 
-from ITMO_FS.utils.data_check import generate_features
-from ITMO_FS.utils.information_theory import conditional_entropy
-from ITMO_FS.utils.information_theory import entropy
-from ITMO_FS.utils.qpfs_body import qpfs_body
+from ...utils.information_theory import conditional_entropy
+from ...utils.information_theory import entropy
+from ...utils.qpfs_body import qpfs_body
+from ...utils.functions import knn_from_class
 
 
 def _wrapped_partial(func, *args, **kwargs):
@@ -19,73 +20,63 @@ def _wrapped_partial(func, *args, **kwargs):
     return partial_func
 
 
-def fit_criterion_measure(X, y):
-    ##todo comments
-    x = np.asarray(X)  # Converting input data to numpy array
-    y = np.asarray(y.reshape((-1,)))
-    if len(x.shape) == 2:
-        fc = np.zeros(x.shape[1])  # Array with amounts of correct predictions for each feature
-    else:
-        fc = np.zeros(len(x))
-    tokens_n = np.unique(y)  # Number of different class tokens
-    centers = np.empty(tokens_n)  # Array with centers of sets of feature values for each class token
-    variances = np.empty(tokens_n)  # Array with variances of sets of feature values for each class token
-    # Each of arrays above will be separately calculated for each feature
-    distances = np.empty(tokens_n)  # Array with distances between sample's value and each class's center
-    # This array will be separately calculated for each feature and each sample
-    for feature_index, feature in enumerate(x.T):  # For each feature
-        # Initializing utility structures
-        class_values = [[] for _ in range(tokens_n)]  # Array with lists of feature values for each class token
-        for index, value in enumerate(y):  # Filling array
-            class_values[value].append(feature[index])
-        for token, values in enumerate(class_values):  # For each class token's list of feature values
-            tmp_arr = np.array(values)
-            centers[token] = np.mean(tmp_arr)
-            variances[token] = np.var(tmp_arr)
-        # Main calculations
-        for sample_index, value in enumerate(feature):  # For each sample value
-            for i in range(tokens_n):  # For each class token
-                # Here can be raise warnings by 0/0 division. In this case, default results
-                # are interpreted correctly
-                distances[i] = np.abs(value - centers[i]) / variances[i]
-            fc[feature_index] += np.argmin(distances) == y[sample_index]
-    fc /= y.shape[0]
-    return dict(zip(generate_features(x), fc))
-
-
-def __calculate_F_ratio(row, y_data):
-    inter_class = 0.0
-    intra_class = 0.0
-    mean_feature = np.mean(row)
-    for value in np.unique(y_data):
-        index_for_this_value = np.where(y_data == value)[0]
-        n = np.sum(row[index_for_this_value])
-        mu = np.mean(row[index_for_this_value])
-        var = np.var(row[index_for_this_value])
-        inter_class += n * np.power((mu - mean_feature), 2)
-        intra_class += (n - 1) * var
-    if inter_class == 0. and intra_class == 0.:
-        return 0.
-    elif intra_class == 0.:
-        return float('inf')
-    else:
-        return inter_class / intra_class
+def fit_criterion_measure(x, y):
+    """Calculate the FitCriterion score for features. Bigger values mean more
+    important features.
 
+    Parameters
+    ----------
+    x : array-like, shape (n_samples, n_features)
+        The training input samples.
+    y : array-like, shape (n_samples,)
+        The target values.
 
-def f_ratio_measure(X, y):
+    Returns
+    -------
+    array-like, shape (n_features,) : feature scores
+
+    See Also
+    --------
+    https://core.ac.uk/download/pdf/191234514.pdf
+
+    Examples
+    --------
+    >>> from ITMO_FS.filters.univariate import fit_criterion_measure
+    >>> import numpy as np
+    >>> x = np.array([[1, 2, 4, 1, 1], [2, 2, 2, 1, 2], [3, 5, 1, 1, 4],
+    ... [1, 1, 1, 1, 4], [2, 2, 2, 1, 5]])
+    >>> y = np.array([1, 2, 3, 1, 2])
+    >>> fit_criterion_measure(x, y)
+    array([1. , 0.8, 0.8, 0.4, 0.6])
     """
-    Calculates Fisher score for features.
+    def count_hits(feature):
+        splits = {cl: feature[y == cl] for cl in classes}
+        means = {cl: np.mean(splits[cl]) for cl in classes}
+        devs = {cl: np.var(splits[cl]) for cl in classes}
+        distances = np.vectorize(
+            lambda x_val: {cl: (
+                abs(x_val - means[cl])
+                / (devs[cl] + 1e-10)) for cl in classes})(feature)
+        return np.sum(np.vectorize(lambda d: min(d, key=d.get))(distances) == y)
+
+    classes = np.unique(y)
+    return np.apply_along_axis(count_hits, 0, x) / x.shape[0]
+
+
+def f_ratio_measure(x, y):
+    """Calculate Fisher score for features. Bigger values mean more important
+    features.
 
     Parameters
     ----------
-    X : numpy array, shape (n_samples, n_features)
-        The input samples.
-    y : numpy array, shape (n_samples, )
-        The classes for the samples.
+    x : array-like, shape (n_samples, n_features)
+        The training input samples.
+    y : array-like, shape (n_samples,)
+        The target values.
 
     Returns
     -------
-    Score for each feature as a numpy array, shape (n_features, )
+    array-like, shape (n_features,) : feature scores
 
     See Also
     --------
@@ -93,129 +84,141 @@ def f_ratio_measure(X, y):
 
     Examples
     --------
-    >>> import sklearn.datasets as datasets
     >>> from ITMO_FS.filters.univariate import f_ratio_measure
-    >>> X = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1], [3, 1, 4, 3, 1], [3, 1, 2, 3, 1]])
+    >>> import numpy as np
+    >>> x = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1],
+    ... [3, 1, 4, 3, 1], [3, 1, 2, 3, 1]])
     >>> y = np.array([1, 3, 2, 1, 2])
-    >>> scores = f_ratio_measure(X, y)
-    >>> print(scores)
-    [ 0.96        0.37333333  0.5         0.1725     13.26      ]
+    >>> f_ratio_measure(x, y)
+    array([0.6 , 0.2 , 1.  , 0.12, 5.4 ])
     """
-    return np.apply_along_axis(__calculate_F_ratio, 0, X, y)
+    def __F_ratio(feature):
+        splits = {cl: feature[y == cl] for cl in classes}
+        mean_feature = np.mean(feature)
+        inter_class = np.sum(
+            np.vectorize(lambda cl: (
+                counts_d[cl]
+                * np.power(mean_feature - np.mean(splits[cl]), 2)))(classes))
+        intra_class = np.sum(
+            np.vectorize(lambda cl: (
+                counts_d[cl]
+                * np.var(splits[cl])))(classes))
+        return inter_class / (intra_class + 1e-10)
 
+    classes, counts = np.unique(y, return_counts=True)
+    counts_d = {cl: counts[idx] for idx, cl in enumerate(classes)}
+    return np.apply_along_axis(__F_ratio, 0, x)
 
-def gini_index(X, y):
-    """
-    Gini index is a measure of statistical dispersion.
-    Note: before counting gini index data is normalized with MinMaxScaler
+
+def gini_index(x, y):
+    """Calculate Gini index for features. Bigger values mean more important
+    features. This measure works best with discrete features due to being based
+    on information theory.
 
     Parameters
     ----------
-    X : numpy array, shape (n_samples, n_features)
-        The input samples.
-    y : numpy array, shape (n_samples, )
-        The classes for the samples.
+    x : array-like, shape (n_samples, n_features)
+        The training input samples.
+    y : array-like, shape (n_samples,)
+        The target values.
 
     Returns
     -------
-    Score for each feature as a numpy array, shape (n_features, )
+    array-like, shape (n_features,) : feature scores
 
     See Also
     --------
-    https://en.wikipedia.org/wiki/Gini_coefficient
-    
+    http://lkm.fri.uni-lj.si/xaigor/slo/clanki/ijcai95z.pdf
+
     Examples
     --------
-    import sklearn.datasets as datasets
-    from ITMO_FS.filters.univariate import gini_index
-
-    X, y = datasets.make_classification(n_samples=200, n_features=7, shuffle=False)
-    scores = gini_index(X, y)
-    print(scores)
-
-    >>> import sklearn.datasets as datasets
     >>> from ITMO_FS.filters.univariate import gini_index
-    >>> X = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1], [3, 1, 4, 3, 1], [3, 1, 2, 3, 1]])
+    >>> from sklearn.preprocessing import KBinsDiscretizer
+    >>> x = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1],
+    ... [3, 1, 4, 3, 1], [3, 1, 2, 3, 1]])
     >>> y = np.array([1, 3, 2, 1, 2])
-    >>> scores = gini_index(X, y)
-    >>> print(scores)
-    [0.05555556 0.44444444 0.05555556 0.16666667 0.62962963]
+    >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
+    >>> x = est.fit_transform(x)
+    >>> gini_index(x, y)
+    array([0.14      , 0.04      , 0.64      , 0.24      , 0.37333333])
     """
+    def __gini(feature):
+        values, counts = np.unique(feature, return_counts=True)
+        counts_d = {val: counts[idx] for idx, val in enumerate(values)}
+        total_sum = np.sum(
+            np.vectorize(
+                lambda val: (
+                    np.sum(
+                        np.square(
+                            np.unique(
+                                y[feature == val], return_counts=True)[1]))
+                    / counts_d[val]))(values))
+        return total_sum / x.shape[0] - prior_prob_squared_sum
+
+    classes, counts = np.unique(y, return_counts=True)
+    prior_prob_squared_sum = np.sum(np.square(counts / x.shape[0]))
 
-    if X.shape[0] < 2:
-        raise Exception("Sample size must be greater than 1")
-    scaler = MinMaxScaler()
-    X = scaler.fit_transform(X)
-    cum_x = np.cumsum(X / np.linalg.norm(X, 1, axis=0), axis=0)
-    cum_y = np.cumsum(y / np.linalg.norm(y, 1))
-    diff_x = (cum_x[1:] - cum_x[:-1])
-    diff_y = (cum_y[1:] + cum_y[:-1])
-    return np.abs(1 - np.sum(np.multiply(diff_x.T, diff_y).T, axis=0))
+    return np.apply_along_axis(__gini, 0, x)
 
 
-def su_measure(X, y):
-    """
-    SU is a correlation measure between the features and the class
-    calculated, via formula SU(X,Y) = 2 * I(X|Y) / (H(X) + H(Y))
+def su_measure(x, y):
+    """SU is a correlation measure between the features and the class
+    calculated via formula SU(X,Y) = 2 * I(X|Y) / (H(X) + H(Y)). Bigger values
+    mean more important features. This measure works best with discrete
+    features due to being based on information theory.
 
     Parameters
     ----------
-    X : numpy array, shape (n_samples, n_features)
-        The input samples.
-    y : numpy array, shape (n_samples, )
-        The classes for the samples.
+    x : array-like, shape (n_samples, n_features)
+        The training input samples.
+    y : array-like, shape (n_samples,)
+        The target values.
 
     Returns
     -------
-    Score for each feature as a numpy array, shape (n_features, )
+    array-like, shape (n_features,) : feature scores
 
     See Also
     --------
-    https://www.matec-conferences.org/articles/matecconf/pdf/2016/05/matecconf_iccma2016_06002.pdf
+    https://pdfs.semanticscholar.org/9964/c7b42e6ab311f88e493b3fc552515e0c764a.pdf
 
     Examples
     --------
-    >>> import sklearn.datasets as datasets
     >>> from ITMO_FS.filters.univariate import su_measure
-    >>> X = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1], [3, 1, 4, 3, 1], [3, 1, 2, 3, 1]])
+    >>> from sklearn.preprocessing import KBinsDiscretizer
+    >>> import numpy as np
+    >>> x = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1],
+    ... [3, 1, 4, 3, 1], [3, 1, 2, 3, 1]])
     >>> y = np.array([1, 3, 2, 1, 2])
-    >>> scores = su_measure(X, y)
-    >>> print(scores)
-    [0.82173546 0.67908587 0.79187567 0.73717549 0.86172942]
+    >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
+    >>> x = est.fit_transform(x)
+    >>> su_measure(x, y)
+    array([0.28694182, 0.13715115, 0.79187567, 0.47435099, 0.67126949])
     """
-    entropy_y = entropy(y)
-    f_ratios = np.empty(X.shape[1])
-    for index in range(X.shape[1]):
-        entropy_x = entropy(X[:, index])
-        cond_entropy = conditional_entropy(y, X[:, index])
-        f_ratios[index] = (entropy_x + entropy_y - cond_entropy) / (entropy_x + entropy_y)
-    return f_ratios
+    def __SU(feature):
+        entropy_x = entropy(feature)
+        return (2 * (entropy_x - conditional_entropy(y, feature))
+                  / (entropy_x + entropy_y))
 
+    entropy_y = entropy(y)
+    return np.apply_along_axis(__SU, 0, x)
 
 # TODO CONCORDATION COEF
 
-def _kendall_corr(X, y):
-    k_corr = 0.0
-    for i in range(len(X)):
-        for j in range(i + 1, len(X)):
-            k_corr += np.sign(X[i] - X[j]) * np.sign(y[i] - y[j])
-    return k_corr
-
-
-def kendall_corr(X, y):
-    """
-    Calculates Sample sign correlation (Kendall correlation) for each feature.
+def kendall_corr(x, y):
+    """Calculate Sample sign correlation (Kendall correlation) for each
+    feature. Bigger absolute values mean more important features.
 
     Parameters
     ----------
-    X : numpy array, shape (n_samples, n_features) or (n_samples, )
-        The input samples.
-    y : numpy array, shape (n_samples, )
-        The classes for the samples.
+    x : array-like, shape (n_samples, n_features)
+        The training input samples.
+    y : array-like, shape (n_samples,)
+        The target values.
 
     Returns
     -------
-    Score for each feature as a numpy array, shape (n_features, )
+    array-like, shape (n_features,) : feature scores
 
     See Also
     --------
@@ -223,386 +226,393 @@ def kendall_corr(X, y):
 
     Examples
     --------
-    >>> import sklearn.datasets as datasets
     >>> from ITMO_FS.filters.univariate import kendall_corr
-    >>> X = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1], [3, 1, 4, 3, 1], [3, 1, 2, 3, 1]])
+    >>> import numpy as np
+    >>> x = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1],
+    ... [3, 1, 4, 3, 1], [3, 1, 2, 3, 1]])
     >>> y = np.array([1, 3, 2, 1, 2])
-    >>> scores = kendall_corr(X, y)
-    >>> print(scores)
-    [-0.1  0.2 -0.4 -0.2  0.2]
+    >>> kendall_corr(x, y)
+    array([-0.1,  0.2, -0.4, -0.2,  0.2])
     """
+    def __kendall_corr(feature):
+        k_corr = 0.0
+        for i in range(len(feature)):
+            k_corr += np.sum(np.sign(feature[i] - feature[i + 1:])
+                             * np.sign(y[i] - y[i + 1:]))
+        return 2 * k_corr / (feature.shape[0] * (feature.shape[0] - 1))
 
-    if len(X.shape) == 1:
-        k_corr = _kendall_corr(X, y)
-        return float(2 * k_corr) / (len(X) * (len(X) - 1))
-    else:
-        res = []
-        for var in range(X.shape[1]):
-            x = X[:, var]
-            k_corr = _kendall_corr(x, y)
-            k_corr = float(2 * k_corr) / (len(x) * (len(x) - 1))
-            res.append(k_corr)
-        return np.array(res)
+    return np.apply_along_axis(__kendall_corr, 0, x)
 
 
-def fechner_corr(X, y):
-    """
-    Calculates Sample sign correlation (Fechner correlation) for each feature.
-    
+def fechner_corr(x, y):
+    """Calculate Sample sign correlation (Fechner correlation) for each
+    feature. Bigger absolute values mean more important features.
 
     Parameters
     ----------
-    X : numpy array, shape (n_samples, n_features)
-        The input samples.
-    y : numpy array, shape (n_samples, )
-        The classes for the samples.
+    x : array-like, shape (n_samples, n_features)
+        The training input samples.
+    y : array-like, shape (n_samples,)
+        The target values.
 
     Returns
     -------
-    Score for each feature as a numpy array, shape (n_features, )
+    array-like, shape (n_features,) : feature scores
 
     See Also
     --------
 
     Examples
     --------
-    >>> import sklearn.datasets as datasets
     >>> from ITMO_FS.filters.univariate import fechner_corr
-    >>> X = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1], [3, 1, 4, 3, 1], [3, 1, 2, 3, 1]])
+    >>> import numpy as np
+    >>> x = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1],
+    ... [3, 1, 4, 3, 1], [3, 1, 2, 3, 1]])
     >>> y = np.array([1, 3, 2, 1, 2])
-    >>> scores = fechner_corr(X, y)
-    >>> print(scores)
-    [-0.2  0.2 -0.5 -0.2 -0.2]
+    >>> fechner_corr(x, y)
+    array([-0.2,  0.2, -0.4, -0.2, -0.2])
     """
-
-    if len(X.shape) == 1:
-        m = 1
-    else:
-        m = X.shape[1]
     y_dev = y - np.mean(y)
-    x_dev = X - np.mean(X, axis=0)
-    if m == 1:
-        N_plus = np.sum((x_dev >= 0) & (y_dev >= 0)) + np.sum((x_dev < 0) & (y_dev < 0))
-        N_minus = np.sum((x_dev > 0) & (y_dev < 0)) + np.sum((x_dev < 0) & (y_dev > 0))
-    else:
-        N_plus = np.sum((x_dev >= 0).T & (y_dev >= 0), axis=1) + np.sum((x_dev < 0).T & (y_dev < 0), axis=1).astype(
-            float)
-        N_minus = np.sum((x_dev > 0).T & (y_dev < 0), axis=1) + np.sum((x_dev < 0).T & (y_dev > 0), axis=1).astype(
-            float)
-    return (N_plus - N_minus) / (N_plus + N_minus)
-
-
-def __distance_matrix(X, y, n_samples):
-    dm = np.zeros((n_samples, n_samples), dtype=tuple)
-    for i in range(n_samples):
-        for j in range(i, n_samples):
-            # using the Manhattan (L1) norm rather than
-            # the Euclidean (L2) norm,
-            # although the rationale is not specified
-            value = np.linalg.norm(X[i, :] - X[j, :], 1)
-            dm[i, j] = (value, j, y[j])
-            dm[j, i] = (value, i, y[i])
-    # sort_indices = dm.argsort(1)
-    # dm.sort(1)
-    # indices = np.arange(n_samples) #[sort_indices]
-    # dm = np.dstack((dm, indices))
-    return dm
-
-    # TODO redo with np.where
-
-
-def __take_k(dm_i, k, r_index, choice_func):
-    hits = []
-    dm_i = sorted(dm_i, key=lambda x: x[0])
-    for samp in dm_i:
-        if (samp[1] != r_index) & (k > 0) & (choice_func(samp[2])):
-            hits.append(samp)
-            k -= 1
-    return np.array(hits, int)
-
-
-def reliefF_measure(X, y, k_neighbors=1):
-    """
-    Counts ReliefF measure for each feature
+    x_dev = x - np.mean(x, axis=0)
+    return np.sum(np.sign(x_dev.T * y_dev), axis=1) / x.shape[0]
+
+def reliefF_measure(x, y, k_neighbors=1):
+    """Calculate ReliefF measure for each feature. Bigger values mean more
+    important features.
 
     Note:
-    Only for complete X
-    Rather than repeating the algorithm m(TODO Ask Nikita about user defined) times,
-    implement it exhaustively (i.e. n times, once for each instance)
+    Only for complete x
+    Rather than repeating the algorithm m(TODO Ask Nikita about user defined)
+    times, implement it exhaustively (i.e. n times, once for each instance)
     for relatively small n (up to one thousand).
-    
+
     Calculates spearman correlation for each feature.
-    Spearman's correlation assesses monotonic relationships (whether linear or not).
-    If there are no repeated data values, a perfect Spearman correlation of +1 or −1
-    occurs when each of the variables is a perfect monotone function of the other.
+    Spearman's correlation assesses monotonic relationships (whether linear or
+    not). If there are no repeated data values, a perfect Spearman correlation
+    of +1 or −1 occurs when each of the variables is a perfect monotone
+    function of the other.
 
     Parameters
     ----------
-    X : numpy array, shape (n_samples, n_features)
+    x : array-like, shape (n_samples, n_features)
         The input samples.
-    y : numpy array, shape (n_samples, )
+    y : array-like, shape (n_samples,)
         The classes for the samples.
-    k_neighbors : int, optional = 1,
-        The number of neighbors to consider when assigning feature importance scores.
-        More neighbors results in more accurate scores, but takes longer.
-        Selection of k hits and misses is the basic difference to Relief
-        and ensures greater robustness of the algorithm concerning noise.
-    
+    k_neighbors : int, optional
+        The number of neighbors to consider when assigning feature importance
+        scores. More neighbors results in more accurate scores but takes
+        longer. Selection of k hits and misses is the basic difference to
+        Relief and ensures greater robustness of the algorithm concerning noise.
 
     Returns
     -------
-    Score for each feature as a numpy array, shape (n_features, )
+    array-like, shape (n_features,) : feature scores
 
     See Also
     --------
-    R.J. Urbanowicz et al. Relief-based feature selection: Introduction and review
-    Journal of Biomedical Informatics 85 (2018) 189–203
+    R.J. Urbanowicz et al. Relief-based feature selection: Introduction and
+    review. Journal of Biomedical Informatics 85 (2018) 189–203
 
     Examples
     --------
-    >>> import sklearn.datasets as datasets
     >>> from ITMO_FS.filters.univariate import reliefF_measure
-    >>> X = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1], [3, 1, 4, 3, 1], [3, 1, 2, 3, 1]])
-    >>> y = np.array([1, 3, 2, 1, 2])
-    >>> scores = reliefF_measure(X, y)
-    >>> print(scores)
-    [-0.13333333 -0.41666667  0.17916667 -0.275       0.46666667]
+    >>> import numpy as np
+    >>> x = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1],
+    ... [3, 1, 4, 3, 1], [3, 1, 2, 3, 1], [1, 2, 1, 4, 2], [4, 3, 2, 3, 1]])
+    >>> y = np.array([1, 2, 2, 1, 2, 1, 2])
+    >>> reliefF_measure(x, y)
+    array([-0.14285714, -0.57142857,  0.10714286, -0.14285714,  0.07142857])
+    >>> reliefF_measure(x, y, k_neighbors=2)
+    array([-0.07142857, -0.17857143, -0.07142857, -0.0952381 , -0.17857143])
     """
-    f_ratios = np.zeros(X.shape[1])
+    def __calc_misses(index):
+        misses_diffs_classes = np.abs(
+            np.vectorize(
+                lambda cl: (
+                        x[index]
+                        - x[knn_from_class(dm, y, index, k_neighbors, cl)])
+                    * prior_prob[cl],
+                signature='()->(n,m)')(classes[classes != y[index]]))
+        return (np.sum(np.sum(misses_diffs_classes, axis=1), axis=0)
+            / (1 - prior_prob[y[index]]))
+
     classes, counts = np.unique(y, return_counts=True)
+    if np.any(counts <= k_neighbors):
+        raise ValueError(
+            "Cannot calculate relieff measure because one of theclasses has "
+            "less than %d samples" % (k_neighbors + 1))
     prior_prob = dict(zip(classes, np.array(counts) / len(y)))
-    n_samples = X.shape[0]
-    n_features = X.shape[1]
-    dm = __distance_matrix(X, y, n_samples)
-    for i in range(n_samples):
-        r = X[i]
-        dm_i = dm[i]
-        hits = __take_k(dm_i, k_neighbors, i, lambda x: x == y[i])
-        if len(hits) != 0:
-            ind_hits = hits[:, 1]
-        else:
-            ind_hits = []
-        value_hits = X.take(ind_hits, axis=0)
-        m_c = np.empty(len(classes), np.ndarray)
-        for j in range(len(classes)):
-            if classes[j] != y[i]:
-                misses = __take_k(dm_i, k_neighbors, i, lambda x: x == classes[j])
-                ind_misses = misses[:, 1]
-                m_c[j] = X.take(ind_misses, axis=0)
-        for A in range(n_features):
-            weight_hit = np.sum(np.abs(r[A] - value_hits[:, A]))
-            weight_miss = 0
-            for j in range(len(classes)):
-                if classes[j] != y[i]:
-                    weight_miss += prior_prob[y[j]] * np.sum(np.abs(r[A] - m_c[j][:, A]))
-            f_ratios[A] += weight_miss / (1 - prior_prob[y[i]]) - weight_hit
+    n_samples = x.shape[0]
+    n_features = x.shape[1]
+    # use manhattan distance instead of euclidean
+    dm = pairwise_distances(x, x, 'manhattan')
+
+    indices = np.arange(n_samples)
+    # use abs instead of square because of manhattan distance
+    hits_diffs = np.abs(
+        np.vectorize(
+            lambda index: (
+                x[index]
+                - x[knn_from_class(dm, y, index, k_neighbors, y[index])]),
+            signature='()->(n,m)')(indices))
+    H = np.sum(hits_diffs, axis=(0,1))
+
+    misses_sum_diffs = np.vectorize(
+        lambda index: __calc_misses(index),
+        signature='()->(n)')(indices)
+    M = np.sum(misses_sum_diffs, axis=0)
+
+    weights = M - H
     # dividing by m * k guarantees that all final weights
     # will be normalized within the interval [ − 1, 1].
-    f_ratios /= n_samples * k_neighbors
+    weights /= n_samples * k_neighbors
     # The maximum and minimum values of A are determined over the entire
     # set of instances.
     # This normalization ensures that weight updates fall
     # between 0 and 1 for both discrete and continuous features.
     with np.errstate(divide='ignore', invalid="ignore"):  # todo
-        return f_ratios / (np.amax(X, axis=0) - np.amin(X, axis=0))
+        return weights / (np.amax(x, axis=0) - np.amin(x, axis=0))
 
 
-def __label_binarize(y):
-    """
-    Binarize labels in a one-vs-all fashion
-    This function makes it possible to compute this transformation for a
-    fixed set of class labels known ahead of time.
-    """
-    classes = np.unique(y)
-    n_samples = len(y)
-    n_classes = len(classes)
-    row = np.arange(n_samples)
-    col = [np.where(classes == el)[0][0] for el in y]
-    data = np.repeat(1, n_samples)
-    # TODO redo it with numpy
-    return sp.csr_matrix((data, (row, col)), shape=(n_samples, n_classes)).toarray()
-
-
-def __chisquare(f_obs, f_exp):
-    """Fast replacement for scipy.stats.chisquare.
-    Version from https://github.com/scipy/scipy/pull/2525 with additional
-    optimizations.
-    """
-    f_obs = np.asarray(f_obs, dtype=np.float64)
-    # Reuse f_obs for chi-squared statistics
-    chisq = f_obs
-    chisq -= f_exp
-    chisq **= 2
-    with np.errstate(invalid="ignore"):
-        chisq /= f_exp
-    chisq = chisq.sum(axis=0)
-    return chisq
-
-
-def chi2_measure(X, y):
-    """
-    Calculates score for the test chi-squared statistic from X.
-    Chi-squared test is a statistical hypothesis test that
-    is valid to perform when the test statistic is chi-squared 
-    distributed under the null hypothesis
-
-    Note: Input data must contain only non-negative features such
-    as booleans or frequencies (e.g., term counts in document classification),
-    relative to the classes.
-    
+def relief_measure(x, y, m=None, random_state=42):
+    """Calculate Relief measure for each feature. This measure is supposed to
+    work only with binary classification datasets; for multi-class problems use
+    the ReliefF measure. Bigger values mean more important features.
+
     Parameters
     ----------
-    X : numpy array, shape (n_samples, n_features)
+    x : array-like, shape (n_samples, n_features)
         The input samples.
-    y : numpy array, shape (n_samples, )
+    y : array-like, shape (n_samples,)
         The classes for the samples.
+    m : int, optional
+        Amount of iterations to do. If not specified, n_samples iterations
+        would be performed.
+    random_state : int, optional
+        Random state for numpy random.
 
     Returns
     -------
-    Score for each feature as a numpy array, shape (n_features, )
+    array-like, shape (n_features,) : feature scores
 
     See Also
     --------
-    https://en.wikipedia.org/wiki/Chi-squared_test
+    R.J. Urbanowicz et al. Relief-based feature selection: Introduction and
+    review. Journal of Biomedical Informatics 85 (2018) 189–203
 
     Examples
-    ##TODO examples
     --------
+    >>> from ITMO_FS.filters.univariate import relief_measure
+    >>> import numpy as np
+    >>> x = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1],
+    ... [3, 1, 4, 3, 1], [3, 1, 2, 3, 1]])
+    >>> y = np.array([1, 2, 2, 1, 2])
+    >>> relief_measure(x, y)
+    array([ 0.    , -0.6   , -0.1875, -0.15  , -0.4   ])
     """
+    weights = np.zeros(x.shape[1])
+    classes, counts = np.unique(y, return_counts=True)
+    if len(classes) == 1:
+        raise ValueError("Cannot calculate relief measure with 1 class")
+    if 1 in counts:
+        raise ValueError(
+            "Cannot calculate relief measure because one of the classes has "
+            "only 1 sample")
+
+    n_samples = x.shape[0]
+    n_features = x.shape[1]
+    if m is None:
+        m = n_samples
+
+    x_normalized = MinMaxScaler().fit_transform(x)
+    dm = euclidean_distances(x_normalized, x_normalized)
+    indices = np.random.default_rng(random_state).integers(
+        low=0, high=n_samples, size=m)
+    objects = x_normalized[indices]
+    hits_diffs = np.square(
+        np.vectorize(
+            lambda index: (
+                x_normalized[index]
+                - x_normalized[knn_from_class(dm, y, index, 1, y[index])]),
+            signature='()->(n,m)')(indices))
+    misses_diffs = np.square(
+        np.vectorize(
+            lambda index: (
+                x_normalized[index]
+                - x_normalized[knn_from_class(
+                    dm, y, index, 1, y[index], anyOtherClass=True)]),
+            signature='()->(n,m)')(indices))
+
+    H = np.sum(hits_diffs, axis=(0,1))
+    M = np.sum(misses_diffs, axis=(0,1))
+
+    weights = M - H
+
+    return weights / m
+
+
+def chi2_measure(x, y):
+    """Calculate the Chi-squared measure for each feature. Bigger values mean
+    more important features. This measure works best with discrete features due
+    to being based on statistics.
+
+    Parameters
+    ----------
+    x : array-like, shape (n_samples, n_features)
+        The training input samples.
+    y : array-like, shape (n_samples,)
+        The target values.
+
+    Returns
+    -------
+    array-like, shape (n_features,) : feature scores
 
-    if np.any(X < 0):
-        raise ValueError("Input X must be non-negative.")
-    y = __label_binarize(y)
-    # If you use sparse input
-    # you can use sklearn.utils.extmath.safe_sparse_dot instead
-    observed = np.dot(y.T, X)  # n_classes * n_features
-    feature_count = X.sum(axis=0).reshape(1, -1)
-    class_prob = y.mean(axis=0).reshape(1, -1)
-    expected = np.dot(class_prob.T, feature_count)
-    return __chisquare(observed, expected)
-
-
-def __contingency_matrix(labels_true, labels_pred):
-    """Build a contingency matrix describing the relationship between labels.
-        Parameters
-        ----------
-        labels_true : int array, shape = [n_samples]
-            Ground truth class labels to be used as a reference
-        labels_pred : array, shape = [n_samples]
-            Cluster labels to evaluate
-        Returns
-        -------
-        contingency : {array-like, sparse}, shape=[n_classes_true, n_classes_pred]
-            Matrix :math:`C` such that :math:`C_{i, j}` is the number of samples in
-            true class :math:`i` and in predicted class :math:`j`. If
-            ``eps is None``, the dtype of this array will be integer. If ``eps`` is
-            given, the dtype will be float.
-        """
-    classes, class_idx = np.unique(labels_true, return_inverse=True)
-    clusters, cluster_idx = np.unique(labels_pred, return_inverse=True)
-    n_classes = classes.shape[0]
-    n_clusters = clusters.shape[0]
-    # Using coo_matrix to accelerate simple histogram calculation,
-    # i.e. bins are consecutive integers
-    # Currently, coo_matrix is faster than histogram2d for simple cases
-    # TODO redo it with numpy
-    contingency = sp.coo_matrix((np.ones(class_idx.shape[0]),
-                                 (class_idx, cluster_idx)),
-                                shape=(n_classes, n_clusters),
-                                dtype=np.int)
-    contingency = contingency.tocsr()
-    contingency.sum_duplicates()
-    return contingency
-
-
-def __mi(U, V):
-    contingency = __contingency_matrix(U, V)
-    nzx, nzy, nz_val = sp.find(contingency)
-    contingency_sum = contingency.sum()
-    pi = np.ravel(contingency.sum(axis=1))
-    pj = np.ravel(contingency.sum(axis=0))
-    log_contingency_nm = np.log(nz_val)
-    contingency_nm = nz_val / contingency_sum
-    # Don't need to calculate the full outer product, just for non-zeroes
-    outer = (pi.take(nzx).astype(np.int64, copy=False)
-             * pj.take(nzy).astype(np.int64, copy=False))
-    log_outer = -np.log(outer) + log(pi.sum()) + log(pj.sum())
-    mi = (contingency_nm * (log_contingency_nm - log(contingency_sum)) +
-          contingency_nm * log_outer)
-    return mi.sum()
-
-
-def spearman_corr(X, y):
+    See Also
+    --------
+    http://lkm.fri.uni-lj.si/xaigor/slo/clanki/ijcai95z.pdf
+
+    Example
+    -------
+    >>> from ITMO_FS.filters.univariate import chi2_measure
+    >>> from sklearn.preprocessing import KBinsDiscretizer
+    >>> import numpy as np
+    >>> x = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1],
+    ... [3, 1, 4, 3, 1], [3, 1, 2, 3, 1]])
+    >>> y = np.array([1, 3, 2, 1, 2])
+    >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
+    >>> x = est.fit_transform(x)
+    >>> chi2_measure(x, y)
+    array([ 1.875     ,  0.83333333, 10.        ,  3.75      ,  6.66666667])
     """
-    Calculates spearman correlation for each feature.
-    Spearman's correlation assesses monotonic relationships (whether linear or not).
-    If there are no repeated data values, a perfect Spearman correlation of +1 or −1
-    occurs when each of the variables is a perfect monotone function of the other.
+    def __chi2(feature):
+        values, counts = np.unique(feature, return_counts=True)
+        values_map = {val: idx for idx, val in enumerate(values)}
+        splits = {cl: np.array([values_map[val] for val in feature[y == cl]]) 
+            for cl in classes}
+        e = np.vectorize(
+            lambda cl: prior_probs[cl] * counts,
+            signature='()->(1)')(classes)
+        n = np.vectorize(
+            lambda cl: np.bincount(splits[cl], minlength=values.shape[0]),
+            signature='()->(1)')(classes)
+        return np.sum(np.square(e - n) / e)
+
+    classes, counts = np.unique(y, return_counts=True)
+    prior_probs = {cl: counts[idx] / x.shape[0] for idx, cl
+        in enumerate(classes)}
+    
+    return np.apply_along_axis(__chi2, 0, x)
+
+
+#
+# def __contingency_matrix(labels_true, labels_pred):
+#     """Build a contingency matrix describing the relationship between labels.
+#         Parameters
+#         ----------
+#         labels_true : int array, shape = [n_samples]
+#             Ground truth class labels to be used as a reference
+#         labels_pred : array, shape = [n_samples]
+#             Cluster labels to evaluate
+#         Returns
+#         -------
+#         contingency : {array-like, sparse}, shape=[n_classes_true, n_classes_pred]
+#             Matrix :math:`C` such that :math:`C_{i, j}` is the number of samples in
+#             true class :math:`i` and in predicted class :math:`j`. If
+#             ``eps is None``, the dtype of this array will be integer. If ``eps`` is
+#             given, the dtype will be float.
+#         """
+#     classes, class_idx = np.unique(labels_true, return_inverse=True)
+#     clusters, cluster_idx = np.unique(labels_pred, return_inverse=True)
+#     n_classes = classes.shape[0]
+#     n_clusters = clusters.shape[0]
+#     # Using coo_matrix to accelerate simple histogram calculation,
+#     # i.e. bins are consecutive integers
+#     # Currently, coo_matrix is faster than histogram2d for simple cases
+#     # TODO redo it with numpy
+#     contingency = sp.coo_matrix((np.ones(class_idx.shape[0]),
+#                                  (class_idx, cluster_idx)),
+#                                 shape=(n_classes, n_clusters),
+#                                 dtype=np.int)
+#     contingency = contingency.tocsr()
+#     contingency.sum_duplicates()
+#     return contingency
+#
+#
+# def __mi(U, V):
+#     contingency = __contingency_matrix(U, V)
+#     nzx, nzy, nz_val = sp.find(contingency)
+#     contingency_sum = contingency.sum()
+#     pi = np.ravel(contingency.sum(axis=1))
+#     pj = np.ravel(contingency.sum(axis=0))
+#     log_contingency_nm = np.log(nz_val)
+#     contingency_nm = nz_val / contingency_sum
+#     # Don't need to calculate the full outer product, just for non-zeroes
+#     outer = (pi.take(nzx).astype(np.int64, copy=False)
+#              * pj.take(nzy).astype(np.int64, copy=False))
+#     log_outer = -np.log(outer) + log(pi.sum()) + log(pj.sum())
+#     mi = (contingency_nm * (log_contingency_nm - log(contingency_sum)) +
+#           contingency_nm * log_outer)
+#     return mi.sum()
+#
+
+def spearman_corr(x, y):
+    """Calculate Spearman's correlation for each feature. Bigger absolute
+    values mean more important features. This measure works best with discrete
+    features due to being based on statistics.
 
     Parameters
     ----------
-    X : numpy array, shape (n_samples, n_features)
-        The input samples.
-    y : numpy array, shape (n_samples, )
-        The classes for the samples.
+    x : array-like, shape (n_samples, n_features)
+        The training input samples.
+    y : array-like, shape (n_samples,)
+        The target values.
 
     Returns
     -------
-    Score for each feature as a numpy array, shape (n_features, )
+    array-like, shape (n_features,) : feature scores
 
     See Also
     --------
-    https://en.wikipedia.org/wiki/Spearman%27s_rank_correlation_coefficient
+    https://en.wikipedia.org/wiki/Spearman's_rank_correlation_coefficient
 
     Examples
     --------
-    >>> import sklearn.datasets as datasets
     >>> from ITMO_FS.filters.univariate import spearman_corr
-    >>> X = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1], [3, 1, 4, 3, 1], [3, 1, 2, 3, 1]])
+    >>> from sklearn.preprocessing import KBinsDiscretizer
+    >>> import numpy as np
+    >>> x = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1],
+    ... [3, 1, 4, 3, 1], [3, 1, 2, 3, 1]])
     >>> y = np.array([1, 3, 2, 1, 2])
-    >>> scores = spearman_corr(X, y)
-    >>> scores
-    array([-0.4 ,  0.3 , -0.3 , -0.2 ,  0.65])
+    >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
+    >>> x = est.fit_transform(x)
+    >>> spearman_corr(x, y)
+    array([-0.186339  ,  0.30429031, -0.52704628, -0.30555556,  0.35355339])
     """
-    n = X.shape[0]
-    c = 6 / (n * (n - 1) * (n + 1))
-
-    d = dict(zip(sorted(y), range(y.shape[0])))
-    ranks_y = np.vectorize(d.get)(y)
-    if y.shape == X.shape:
-        d = dict(zip(sorted(X), range(X.shape[0])))
-        ranks_X = np.vectorize(d.get)(X)
-
-        dif = ranks_X - ranks_y
-    else:
-        ranks_X = X.copy()
-        for i in range(X.shape[1]):
-            d = dict(zip(sorted(X[:, i]), range(X.shape[0])))
-            ranks_X[:, i] = np.vectorize(d.get)(X[:, i])
+    n = x.shape[0]
+    if n < 2:
+        raise ValueError("The input should contain more than 1 sample")
 
-        dif = ranks_X - np.repeat(ranks_y, X.shape[1]).reshape(y.shape[0], X.shape[1])
+    x_ranks = np.apply_along_axis(rankdata, 0, x)
+    y_ranks = rankdata(y)
 
-    return 1 - c * np.sum(dif * dif, axis=0)
+    return pearson_corr(x_ranks, y_ranks)
 
 
-def pearson_corr(X, y):
-    """
-    Calculates pearson correlation for each feature.
-    Pearson correleation coeficient is a statistic
-    that measures linear correlation between two variables X and Y.
-    It has a value in interval [-1, +1], where 1 is total positive linear correlation,
-    0 is no linear correlation, and −1 is total negative linear correlation
+def pearson_corr(x, y):
+    """Calculate Pearson's correlation for each feature. Bigger absolute
+    values mean more important features. This measure works best with discrete
+    features due to being based on statistics.
 
     Parameters
     ----------
-    X : numpy array, shape (n_samples, n_features)
-        The input samples.
-    y : numpy array, shape (n_samples, )
-        The classes for the samples.
+    x : array-like, shape (n_samples, n_features)
+        The training input samples.
+    y : array-like, shape (n_samples,)
+        The target values.
 
     Returns
     -------
-    Score for each feature as a numpy array, shape (n_features, )
+    array-like, shape (n_features,) : feature scores
 
     See Also
     --------
@@ -610,56 +620,57 @@ def pearson_corr(X, y):
 
     Examples
     --------
-    >>> import sklearn.datasets as datasets
     >>> from ITMO_FS.filters.univariate import pearson_corr
-    >>> X = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1], [3, 1, 4, 3, 1], [3, 1, 2, 3, 1]])
+    >>> from sklearn.preprocessing import KBinsDiscretizer
+    >>> import numpy as np
+    >>> x = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1],
+    ... [3, 1, 4, 3, 1], [3, 1, 2, 3, 1]])
     >>> y = np.array([1, 3, 2, 1, 2])
-    >>> pearson_corr(X, y)
-    array([-0.13363062,  0.32732684, -0.56694671, -0.28571429,  0.53452248])
+    >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
+    >>> x = est.fit_transform(x)
+    >>> pearson_corr(x, y)
+    array([-0.13363062,  0.32732684, -0.60631301, -0.26244533,  0.53452248])
     """
-    x_dev = X - np.mean(X, axis=0)
-    y_dev = y - np.mean(y, axis=0)
+    x_dev = x - np.mean(x, axis=0)
+    y_dev = y - np.mean(y)
     sq_dev_x = x_dev * x_dev
     sq_dev_y = y_dev * y_dev
-    if len(X.shape) == 1:
-        sum_dev = y_dev.T.dot(x_dev).reshape((1,))
-        denominators = np.array([np.sqrt(np.sum(sq_dev_y, axis=0) * np.sum(sq_dev_x, axis=0))])
-    else:
-        sum_dev = y_dev.T.dot(x_dev).reshape((X.shape[1],))
-        denominators = np.sqrt(np.sum(sq_dev_y, axis=0) * np.sum(sq_dev_x, axis=0))
+    sum_dev = y_dev.T.dot(x_dev).reshape((x.shape[1],))
+    denominators = np.sqrt(np.sum(sq_dev_y) * np.sum(sq_dev_x, axis=0))
 
     results = np.array(
-        [(sum_dev[i] / denominators[i]) if denominators[i] > 0.0 else 0 for i in range(len(denominators))])
+        [(sum_dev[i] / denominators[i]) if denominators[i] > 0.0 else 0 for i
+         in range(len(denominators))])
     return results
 
 
 # TODO need to implement unsupervised way
-def laplacian_score(X, y, k_neighbors=5, t=1, metric=np.linalg.norm, **kwargs):
-    """
-    Calculates Laplacian Score for each feature.
+def laplacian_score(x, y, k_neighbors=5, t=1, metric='euclidean', **kwargs):
+    """Calculate Laplacian Score for each feature. Smaller values mean more
+    important features.
 
     Parameters
     ----------
-    X : numpy array, shape (n_samples, n_features)
+    x : array-like, shape (n_samples, n_features)
         The input samples.
-    y : numpy array, shape (n_samples, )
+    y : array-like, shape (n_samples,)
         The classes for the samples.
-    k_neighbors : int, optional (by default k_neighbors=5)
+    k_neighbors : int, optional
         The number of neighbors to construct a nearest neighbor graph.
-    t : float, optional (by default t=1)
-        Suitable constant for weight matrix S, 
+    t : float, optional
+        Suitable constant for weight matrix S
         where Sij = exp(-(|xi - xj| ^ 2) / t).
-    metric : callable, optional (by default metric=np.linalg.norm)
-        Norm function to compute distance between two points.
-        The default distance is euclidean.
-    weights : numpy array, shape (n_samples, n_samples)
-        The weight matrix of the graph that models the local structure of the data space.
-        By default it is constructed using KNN algorithm.
+    metric : str or callable, optional
+        Norm function to compute distance between two points or one of the
+        commonly used strings ('euclidean', 'manhattan' etc.) The default
+        metric is euclidean.
+    weights : array-like, shape (n_samples, n_samples)
+        The weight matrix of the graph that models the local structure of
+        the data space. By default it is constructed using KNN algorithm.
 
     Returns
     -------
-    List of scores of each feature.
-    The smaller the laplacian score is, the more important the feature is.
+    array-like, shape (n_features,) : feature scores
 
     See Also
     --------
@@ -667,15 +678,15 @@ def laplacian_score(X, y, k_neighbors=5, t=1, metric=np.linalg.norm, **kwargs):
 
     Examples
     --------
-    >>> import sklearn.datasets as datasets
     >>> from ITMO_FS.filters.univariate import laplacian_score
-    >>> X = np.array([[1, 2, 3, 3, 1],[2, 2, 3, 3, 2], [1, 3, 3, 1, 3],[3, 1, 3, 1, 4],[4, 4, 3, 1, 5]], dtype = np.integer)
-    >>> y = np.array([1, 2, 3, 4, 5], dtype=np.integer)
-    >>> scores = laplacian_score(X, y)
-    >>> scores
+    >>> import numpy as np
+    >>> x = np.array([[1, 2, 3, 3, 1], [2, 2, 3, 3, 2], [1, 3, 3, 1, 3],
+    ... [3, 1, 3, 1, 4], [4, 4, 3, 1, 5]])
+    >>> y = np.array([1, 2, 3, 4, 5])
+    >>> laplacian_score(x, y)
     array([1.98983619, 1.22248371,        nan, 0.79710221, 1.90648048])
     """
-    n, m = X.shape
+    n, m = x.shape
     k_neighbors = min(k_neighbors, n - 1)
     if 'weights' in kwargs.keys():
         S = kwargs['weights']
@@ -684,101 +695,101 @@ def laplacian_score(X, y, k_neighbors=5, t=1, metric=np.linalg.norm, **kwargs):
             S = lil_matrix((n, n))
         else:
             S = np.zeros((n, n))
+        graph = NearestNeighbors(n_neighbors=k_neighbors, metric=metric)
+        graph.fit(x)
+        distances, neighbors = graph.kneighbors()
         for i in range(n):
-            distances = []
-            for j in range(n):
-                if i == j:
-                    continue
-                d = metric(X[i] - X[j])
-                distances.append((d, j))
-                if y[i] == y[j]:
-                    S[i, j] = exp(-d * d / t)
-            distances.sort()
             for j in range(k_neighbors):
-                S[i, distances[j][1]] = S[distances[j][1], i] = exp(-distances[j][0] * distances[j][0] / t)
+                S[i, neighbors[i][j]] = S[neighbors[i][j], i] = exp(
+                    -distances[i][j] * distances[i][j] / t)
     ONE = np.ones((n,))
     D = np.diag(S.dot(ONE))
     L = D - S
     t = D.dot(ONE)
-    F = X - X.T.dot(t) / ONE.dot(t)
+    F = x - x.T.dot(t) / ONE.dot(t)
     F = F.T.dot(L.dot(F)) / F.T.dot(D.dot(F))
     return np.diag(F)
 
 
-def information_gain(X, y):
-    """
-    Calculates mutual information for each feature by formula,
-    I(X,Y) = H(X) - H(X|Y)
+def information_gain(x, y):
+    """Calculate mutual information for each feature by formula
+    I(X,Y) = H(Y) - H(Y|X). Bigger values mean more important features. This
+    measure works best with discrete features due to being based on information
+    theory.
 
     Parameters
     ----------
-    X : numpy array, shape (n_samples, n_features)
-        The input samples.
-    y : numpy array, shape (n_samples, )
-        The classes for the samples.
+    x : array-like, shape (n_samples, n_features)
+        The training input samples.
+    y : array-like, shape (n_samples,)
+        The target values.
 
     Returns
     -------
-    Score for each feature as a numpy array, shape (n_features, )
+    array-like, shape (n_features,) : feature scores
 
     See Also
     --------
-    
+
     Examples
     --------
-    >>> import sklearn.datasets as datasets
     >>> from ITMO_FS.filters.univariate import information_gain
-    >>> X = np.array([[1, 2, 3, 3, 1],[2, 2, 3, 3, 2], [1, 3, 3, 1, 3],[3, 1, 3, 1, 4],[4, 4, 3, 1, 5]], dtype = np.integer)
-    >>> y = np.array([1, 2, 3, 4, 5], dtype=np.integer)
-    >>> scores = information_gain(X, y)
-    >>> print(scores)
-    [1.33217904 1.33217904 0.         0.67301167 1.60943791]
+    >>> import numpy as np
+    >>> from sklearn.preprocessing import KBinsDiscretizer
+    >>> x = np.array([[1, 2, 3, 3, 1], [2, 2, 3, 3, 2], [1, 3, 3, 1, 3],
+    ... [3, 1, 3, 1, 4], [4, 4, 3, 1, 5]])
+    >>> y = np.array([1, 2, 3, 4, 5])
+    >>> est = KBinsDiscretizer(n_bins=10, encode='ordinal')
+    >>> x = est.fit_transform(x)
+    >>> information_gain(x, y)
+    array([1.33217904, 1.33217904, 0.        , 0.67301167, 1.60943791])
     """
     entropy_x = entropy(y)
-    cond_entropy = np.apply_along_axis(conditional_entropy, 0, X, y)
+    cond_entropy = np.apply_along_axis(conditional_entropy, 0, x, y)
     return entropy_x - cond_entropy
 
 
-def anova(X, y):
-    """
-    Calculates anova measure for each feature.
+def anova(x, y):
+    """Calculate anova measure for each feature. Bigger values mean more
+    important features.
 
     Parameters
     ----------
-    X : numpy array, shape (n_samples, n_features)
-        The input samples.
-    y : numpy array, shape (n_samples, )
-        The classes for the samples.
+    x : array-like, shape (n_samples, n_features)
+        The training input samples.
+    y : array-like, shape (n_samples,)
+        The target values.
 
     Returns
     -------
-    Score for each feature as a numpy array, shape (n_features, )
+    array-like, shape (n_features,) : feature scores
 
     See Also
     --------
-    Lowry, Richard.  "Concepts and Applications of Inferential
-    Statistics". Chapter 14.
-    http://vassarstats.net/textbook/
+    Lowry, Richard.  "Concepts and Applications of Inferential Statistics".
+    Chapter 14. http://vassarstats.net/textbook/
 
     Note:
-    The Anova score is counted for checking hypothesis if variances of two samples are similar,
-    this measure only returns you counted F-score.
-    For understanding whether samples' variances are similar you should compare recieved result with 
-    value of F-distribution function, for example use: 
+    The Anova score is counted for checking hypothesis if variances of two
+    samples are similar, this measure only returns you counted F-score.
+    For understanding whether samples' variances are similar you should
+    compare recieved result with value of F-distribution function, for
+    example use:
     https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.fdtrc.html#scipy.special.fdtrc
 
     Examples
     --------
-    >>> import sklearn.datasets as datasets
     >>> from ITMO_FS.filters.univariate import anova
-    >>> X = np.array([[1, 2, 3, 3, 1],[2, 2, 3, 3, 2], [1, 3, 3, 1, 3],[3, 1, 3, 1, 4],[4, 4, 3, 1, 5]], dtype = np.integer)
-    >>> y = np.array([1, 2, 3, 4, 5], dtype=np.integer)
-    >>> scores = anova(X, y)
-    >>> print(scores)
+    >>> import numpy as np
+    >>> x = np.array([[1, 2, 3, 3, 1], [2, 2, 3, 3, 2], [1, 3, 3, 1, 3],
+    ... [3, 1, 3, 1, 4], [4, 4, 3, 1, 5]])
+    >>> y = np.array([1, 2, 1, 3, 3])
+    >>> anova(x, y)
+    array([12.6 ,  0.04,   nan,  1.4 ,  3.  ])
     """
-    split_by_class = [X[y == k] for k in np.unique(y)]
+    split_by_class = [x[y == k] for k in np.unique(y)]
     num_classes = len(np.unique(y))
-    num_samples = X.shape[0]
+    num_samples = x.shape[0]
     num_samples_by_class = [s.shape[0] for s in split_by_class]
     sq_sum_all = sum((s ** 2).sum(axis=0) for s in split_by_class)
     sum_group = [np.asarray(s.sum(axis=0)) for s in split_by_class]
@@ -786,7 +797,9 @@ def anova(X, y):
     sum_sq_group = [np.asarray((s ** 2).sum(axis=0)) for s in split_by_class]
     sq_sum_group = [s ** 2 for s in sum_group]
     sq_sum_total = sq_sum_all - sq_sum_combined / float(num_samples)
-    sq_sum_within = sum([sum_sq_group[i] - sq_sum_group[i] / num_samples_by_class[i] for i in range(num_classes)])
+    sq_sum_within = sum(
+        [sum_sq_group[i] - sq_sum_group[i] / num_samples_by_class[i] for i in
+         range(num_classes)])
     sq_sum_between = sq_sum_total - sq_sum_within
     deg_free_between = num_classes - 1
     deg_free_within = num_samples - num_classes
@@ -796,67 +809,65 @@ def anova(X, y):
     return np.array(f)
 
 
-def modified_t_score(X, y):
-    """
-    Calculate the Modified T-score for each feature.
-    
+def modified_t_score(x, y):
+    """Calculate the Modified T-score for each feature. Bigger values mean
+    more important features.
+
     Parameters
     ----------
-    X : numpy array, shape (n_samples, n_features)
+    x : array-like, shape (n_samples, n_features)
         The input samples.
-    y : numpy array, shape (n_samples, )
+    y : array-like, shape (n_samples,)
         The classes for the samples. There can be only 2 classes.
 
     Returns
     -------
-    Score for each feature as a numpy array, shape (n_features, ). The higher the better.
+    array-like, shape (n_features,) : feature scores
 
     See Also
     --------
-
     For more details see paper <https://dergipark.org.tr/en/download/article-file/261247>.
 
     Examples
     --------
-
-    >>> import sklearn.datasets as datasets
-    >>> import numpy as np
     >>> from ITMO_FS.filters.univariate import modified_t_score
-    >>> X = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1], [3, 1, 4, 3, 1], [3, 1, 2, 3, 1]])
+    >>> import numpy as np
+    >>> x = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1],
+    ... [3, 1, 4, 3, 1], [3, 1, 2, 3, 1]])
     >>> y = np.array([1, 1, 2, 1, 2])
-    >>> scores = modified_t_score(X, y)
-    >>> print(scores)
-    [1.68968099 0.12148022 0.39653932 0.17682997 2.04387142]
-
+    >>> modified_t_score(x, y)
+    array([1.68968099, 0.12148022, 0.39653932, 0.17682997, 2.04387142])
     """
-
     classes = np.unique(y)
 
     size_class0 = y[y == classes[0]].size
     size_class1 = y[y == classes[1]].size
 
-    mean_class0 = np.mean(X[y == classes[0]], axis=0)
+    mean_class0 = np.mean(x[y == classes[0]], axis=0)
     mean_class0 = np.nan_to_num(mean_class0)
-    mean_class1 = np.mean(X[y == classes[1]], axis=0)
+    mean_class1 = np.mean(x[y == classes[1]], axis=0)
     mean_class1 = np.nan_to_num(mean_class1)
 
-    std_class0 = np.std(X[y == classes[0]], axis=0)
+    std_class0 = np.std(x[y == classes[0]], axis=0)
     std_class0 = np.nan_to_num(std_class0)
-    std_class1 = np.std(X[y == classes[1]], axis=0)
+    std_class1 = np.std(x[y == classes[1]], axis=0)
     std_class1 = np.nan_to_num(std_class1)
 
-    corr_with_y = np.apply_along_axis(lambda feature: abs(np.corrcoef(feature, y)[0][1]), 0, X)
+    corr_with_y = np.apply_along_axis(
+        lambda feature: abs(np.corrcoef(feature, y)[0][1]), 0, x)
     corr_with_y = np.nan_to_num(corr_with_y)
 
-    corr_with_others = abs(np.corrcoef(X, rowvar=False))
+    corr_with_others = abs(np.corrcoef(x, rowvar=False))
     corr_with_others = np.nan_to_num(corr_with_others)
 
-    mean_of_corr_with_others = (corr_with_others.sum(axis=1) - corr_with_others.diagonal()) / (
-            len(corr_with_others) - 1)
+    mean_of_corr_with_others = (
+        corr_with_others.sum(axis=1)
+        - corr_with_others.diagonal()) / (len(corr_with_others) - 1)
 
     t_score_numerator = abs(mean_class0 - mean_class1)
     t_score_denominator = np.sqrt(
-        (size_class0 * np.square(std_class0) + size_class1 * np.square(std_class1)) / (size_class0 + size_class1))
+        (size_class0 * np.square(std_class0) + size_class1 * np.square(
+            std_class1)) / (size_class0 + size_class1))
     modificator = corr_with_y / mean_of_corr_with_others
 
     modified_t_score = t_score_numerator / t_score_denominator * modificator
@@ -865,20 +876,21 @@ def modified_t_score(X, y):
     return modified_t_score
 
 
-GLOB_MEASURE = {"FitCriterion": fit_criterion_measure,
-                "FRatio": f_ratio_measure,
-                "GiniIndex": gini_index,
-                "SymmetricUncertainty": su_measure,
-                "SpearmanCorr": spearman_corr,
-                "PearsonCorr": pearson_corr,
-                "FechnerCorr": fechner_corr,
-                "KendallCorr": kendall_corr,
-                "ReliefF": reliefF_measure,
-                "Chi2": chi2_measure,
-                "Anova": anova,
-                "LaplacianScore": laplacian_score,
-                "InformationGain": information_gain,
-                "ModifiedTScore": modified_t_score}
+MEASURE_NAMES = {"FitCriterion": fit_criterion_measure,
+                 "FRatio": f_ratio_measure,
+                 "GiniIndex": gini_index,
+                 "SymmetricUncertainty": su_measure,
+                 "SpearmanCorr": spearman_corr,
+                 "PearsonCorr": pearson_corr,
+                 "FechnerCorr": fechner_corr,
+                 "KendallCorr": kendall_corr,
+                 "ReliefF": reliefF_measure,
+                 "Chi2": chi2_measure,
+                 "Anova": anova,
+                 "LaplacianScore": laplacian_score,
+                 "InformationGain": information_gain,
+                 "ModifiedTScore": modified_t_score,
+                 "Relief": relief_measure}
 
 
 def select_best_by_value(value):
@@ -890,15 +902,10 @@ def select_worst_by_value(value):
 
 
 def __select_by_value(scores, value, more=True):
-    features = []
-    for key, sc_value in scores.items():
-        if more:
-            if sc_value >= value:
-                features.append(key)
-        else:
-            if sc_value <= value:
-                features.append(key)
-    return features
+    if more:
+        return np.flatnonzero(scores >= value)
+    else:
+        return np.flatnonzero(scores <= value)
 
 
 def select_k_best(k):
@@ -910,19 +917,20 @@ def select_k_worst(k):
 
 
 def __select_k(scores, k, reverse=False):
-    if type(k) != int:
+    if not isinstance(k, int):
         raise TypeError("Number of features should be integer")
-    return [keys[0] for keys in sorted(scores.items(), key=lambda kv: kv[1], reverse=reverse)[:k]]
+    if k > scores.shape[0]:
+        raise ValueError(
+            "Cannot select %d features with n_features = %d" % (k, len(scores)))
+    order = np.argsort(scores)
+    if reverse:
+        order = order[::-1]
+    return order[:k]
 
 
 def __select_percentage_best(scores, percent):
-    features = []
-    max_val = max(scores.values())
-    threshold = max_val * percent
-    for key, sc_value in scores.items():
-        if sc_value >= threshold:
-            features.append(key)
-    return features
+    return __select_k(
+        scores, k=(int)(scores.shape[0] * percent), reverse=True)
 
 
 def select_best_percentage(percent):
@@ -930,66 +938,61 @@ def select_best_percentage(percent):
 
 
 def __select_percentage_worst(scores, percent):
-    features = []
-    max_val = min(scores.values())
-    threshold = max_val * percent
-    for key, sc_value in scores.items():
-        if sc_value >= threshold:
-            features.append(key)
-    return features
+    return __select_k(
+        scores, k=(int)(scores.shape[0] * percent), reverse=False)
 
 
 def select_worst_percentage(percent):
     return _wrapped_partial(__select_percentage_worst, percent=percent)
 
 
-GLOB_CR = {"Best by value": select_best_by_value,
-           "Worst by value": select_worst_by_value,
-           "K best": select_k_best,
-           "K worst": select_k_worst,
-           "Worst by percentage": select_worst_percentage,
-           "Best by percentage": select_best_percentage}
+CR_NAMES = {"Best by value": select_best_by_value,
+            "Worst by value": select_worst_by_value,
+            "K best": select_k_best,
+            "K worst": select_k_worst,
+            "Worst by percentage": select_worst_percentage,
+            "Best by percentage": select_best_percentage}
 
 
 def qpfs_filter(X, y, r=None, sigma=None, solv='quadprog', fn=pearson_corr):
-    """
-    Performs Quadratic Programming Feature Selection algorithm.
+    """Performs Quadratic Programming Feature Selection algorithm.
     Note: this realization requires labels to start from 1 and be numerical.
-    
+
     Parameters
     ----------
-    X : array-like, shape (n_samples,n_features)
+    X : array-like, shape (n_samples, n_features)
         The input samples.
-    y : array-like, shape (n_samples)
+    y : array-like, shape (n_samples,)
         The classes for the samples.
     r : int
         The number of samples to be used in Nystrom optimization.
     sigma : double
         The threshold for eigenvalues to be used in solving QP optimization.
     solv : string, default
-        The name of qp solver according to qpsolvers(https://pypi.org/project/qpsolvers/) naming.
-        Note quadprog is used by default.
+        The name of qp solver according to
+        qpsolvers(https://pypi.org/project/qpsolvers/) naming. Note quadprog
+        is used by default.
     fn : function(array, array), default
-        The function to count correlation, for example pierson correlation or  mutual information.
-        Note mutual information is used by default.
+        The function to count correlation, for example pierson correlation or
+        mutual information. Note mutual information is used by default.
+
     Returns
-    ------
-    array-like, shape (n_features) : the ranks of features in dataset, with rank increase, feature relevance increases and redundancy decreases.
-    
+    -------
+    array-like, shape (n_features,) : the ranks of features in dataset, with
+    rank increase, feature relevance increases and redundancy decreases.
+
     See Also
     --------
     http://www.jmlr.org/papers/volume11/rodriguez-lujan10a/rodriguez-lujan10a.pdf
-    
+
     Examples
     --------
-
     >>> from ITMO_FS.filters.univariate import qpfs_filter
     >>> from sklearn.datasets import make_classification
-    >>> x = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1], [3, 1, 4, 3, 1], [3, 1, 2, 3, 1]])
+    >>> x = np.array([[3, 3, 3, 2, 2], [3, 3, 1, 2, 3], [1, 3, 5, 1, 1],
+    ... [3, 1, 4, 3, 1], [3, 1, 2, 3, 1]])
     >>> y = np.array([1, 3, 2, 1, 2])
     >>> ranks = qpfs_filter(x, y)
     >>> print(ranks)
-
     """
-
     return qpfs_body(X, y, fn, r=r, sigma=sigma, solv=solv)
diff --git a/ITMO_FS/filters/unsupervised/MCFS.py b/ITMO_FS/filters/unsupervised/MCFS.py
new file mode 100644
index 00000000..bcc32828
--- /dev/null
+++ b/ITMO_FS/filters/unsupervised/MCFS.py
@@ -0,0 +1,145 @@
+from logging import getLogger
+
+import numpy as np
+from scipy.linalg import eigh
+from sklearn.linear_model import Lars
+from sklearn.neighbors import NearestNeighbors
+from sklearn.metrics.pairwise import pairwise_distances
+
+from ...utils import BaseTransformer
+
+
+class MCFS(BaseTransformer):
+    """Unsupervised Feature Selection for Multi-Cluster Data algorithm.
+
+    Parameters
+    ----------
+    n_features : int
+        Number of features to select.
+    k : int
+        Amount of clusters to find.
+    p : int
+        Amount of nearest neighbors to use while building the graph.
+    scheme : str, either '0-1', 'heat' or 'dot'
+        Weighting scheme to use while building the graph.
+    sigma : float
+        Parameter for heat weighting scheme. Ignored if scheme is not 'heat'.
+    full_graph : boolean
+        If True, connect all vertices in the graph to each other instead of
+        running the k-nearest neighbors algorithm. Use with 'heat' or 'dot'
+        schemes.
+
+    Notes
+    -----
+    For more details see `this paper
+    <http://www.cad.zju.edu.cn/home/dengcai/Publication/Conference/2010_KDD-MCFS.pdf/>`_.
+
+    Examples
+    --------
+    >>> from ITMO_FS.filters.unsupervised import MCFS
+    >>> from sklearn.datasets import make_classification
+    >>> import numpy as np
+    >>> dataset = make_classification(n_samples=500, n_features=100,
+    ... n_informative=5, n_redundant=0, random_state=42, shuffle=False)
+    >>> X, y = np.array(dataset[0]), np.array(dataset[1])
+    >>> model = MCFS(5).fit(X)
+    >>> model.selected_features_
+    array([0, 2, 4, 1, 3], dtype=int64)
+    """
+    def __init__(self, n_features, k=2, p=3, scheme='dot', sigma=1,
+                 full_graph=False):
+        self.n_features = n_features
+        self.k = k
+        self.p = p
+        self.scheme = scheme
+        self.sigma = sigma
+        self.full_graph = full_graph
+
+    def __scheme_01(self, x1, x2):
+        return 1
+
+    def __scheme_heat(self, x1, x2):
+        return np.exp(-np.linalg.norm(x1 - x2) ** 2 / self.sigma)
+
+    def __scheme_dot(self, x1, x2):
+        return (x1 / np.linalg.norm(x1 + 1e-10)).dot(
+            x2 / np.linalg.norm(x2 + 1e-10))
+
+    def _fit(self, X, y):
+        """
+            Fits the filter.
+
+            Parameters
+            ----------
+            X : array-like, shape (n_samples, n_features)
+                The training input samples.
+            y : array-like
+                The target values (ignored).
+
+            Returns
+            ----------
+            None
+        """
+        if self.scheme == '0-1':
+            scheme = self.__scheme_01
+        elif self.scheme == 'heat':
+            scheme = self.__scheme_heat
+        elif self.scheme == 'dot':
+            scheme = self.__scheme_dot
+        else:
+            getLogger(__name__).error(
+                "scheme should be either '0-1', 'heat' or 'dot'; %s passed",
+                self.scheme)
+            raise KeyError(
+                "scheme should be either '0-1', 'heat' or 'dot'; %s passed"
+                % self.scheme)
+
+        n_samples = X.shape[0]
+
+
+        if self.k > n_samples:
+            getLogger(__name__).error(
+                "Cannot find %d clusters with n_samples = %d",
+                self.k, n_samples)
+            raise ValueError(
+                "Cannot find %d clusters with n_samples = %d"
+                % (self.k, n_samples))
+
+        if self.p >= n_samples:
+            getLogger(__name__).error(
+                "Cannot select %d nearest neighbors with n_samples = %d",
+                self.p, n_samples)
+            raise ValueError(
+                "Cannot select %d nearest neighbors with n_samples = %d"
+                % (self.p, n_samples))
+
+        if self.full_graph:
+            graph = np.ones((n_samples, n_samples))
+        else:
+            graph = NearestNeighbors(n_neighbors=self.p,
+                algorithm='ball_tree').fit(X).kneighbors_graph().toarray()
+            graph = np.minimum(1, graph + graph.T)
+
+        getLogger(__name__).info("Nearest neighbors graph: %s", graph)
+
+        W = graph * pairwise_distances(X, metric=lambda x, y: scheme(x, y))
+        getLogger(__name__).info("W: %s", W)
+        D = np.diag(W.sum(axis=0))
+        getLogger(__name__).info("D: %s", D)
+        L = D - W
+        getLogger(__name__).info("L: %s", L)
+        eigvals, Y = eigh(type=1, a=L, b=D, subset_by_index=[1, self.k])
+        getLogger(__name__).info("Eigenvalues: %s, classes: %s", eigvals, Y)
+
+        weights = np.zeros((self.n_features_, self.k))
+        for i in range(self.k):
+            clf = Lars(n_nonzero_coefs=self.n_features)
+            clf.fit(X, Y[:, i])
+            weights[:, i] = np.abs(clf.coef_)
+            getLogger(__name__).info(
+                "Weights for eigenvalue %d: %s", i, weights[:, i])
+
+        self.feature_scores_ = weights.max(axis=1)
+        getLogger(__name__).info("Feature scores: %s", self.feature_scores_)
+        ranking = np.argsort(self.feature_scores_)[::-1]
+        self.selected_features_ = ranking[:self.n_features]
diff --git a/ITMO_FS/filters/unsupervised/UDFS.py b/ITMO_FS/filters/unsupervised/UDFS.py
new file mode 100644
index 00000000..dae0d5a8
--- /dev/null
+++ b/ITMO_FS/filters/unsupervised/UDFS.py
@@ -0,0 +1,144 @@
+from logging import getLogger
+
+import numpy as np
+from scipy.linalg import eigh
+from sklearn.neighbors import NearestNeighbors
+
+from ...utils import l21_norm, matrix_norm, BaseTransformer
+
+
+class UDFS(BaseTransformer):
+    """Unsupervised Discriminative Feature Selection algorithm.
+
+    Parameters
+    ----------
+    n_features : int
+        Number of features to select.
+    c : int
+        Amount of clusters to find.
+    k : int
+        Amount of nearest neighbors to use while building the graph.
+    gamma : float
+        Regularization term in the target function.
+    l : float
+        Parameter that controls the invertibility of the matrix used in
+        computing of B.
+    max_iterations : int
+        Maximum amount of iterations to perform.
+    epsilon : positive float
+        Specifies the needed residual between the target functions from
+        consecutive iterations. If the residual is smaller than epsilon, the
+        algorithm is considered to have converged.
+
+    Notes
+    -----
+    For more details see `this paper <https://www.ijcai.org/Proceedings/11/Papers/267.pdf/>`_.
+
+    Examples
+    --------
+    >>> from ITMO_FS.filters.unsupervised import UDFS
+    >>> from sklearn.datasets import make_classification
+    >>> import numpy as np
+    >>> dataset = make_classification(n_samples=500, n_features=100,
+    ... n_informative=5, n_redundant=0, random_state=42, shuffle=False,
+    ... n_clusters_per_class=1)
+    >>> X, y = np.array(dataset[0]), np.array(dataset[1])
+    >>> model = UDFS(5).fit(X)
+    >>> model.selected_features_
+    array([ 2,  3, 19, 90, 92], dtype=int64)
+    """
+    def __init__(self, n_features, c=2, k=3, gamma=1, l=1e-6,
+                 max_iterations=1000, epsilon=1e-5):
+        self.n_features = n_features
+        self.c = c
+        self.k = k
+        self.gamma = gamma
+        self.l = l
+        self.max_iterations = max_iterations
+        self.epsilon = epsilon
+
+    def _fit(self, X, y):
+        """Fit the filter.
+
+        Parameters
+        ----------
+        X : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like
+            The target values (ignored).
+
+        Returns
+        -------
+        None
+        """
+        def construct_S(arr):
+            S = np.zeros((n_samples, self.k + 1))
+            for idx in range(self.k + 1):
+                S[arr[idx], idx] = 1
+            return S
+
+        n_samples = X.shape[0]
+
+        if self.c > n_samples:
+            getLogger(__name__).error(
+                "Cannot find %d clusters with n_samples = %d",
+                self.c, n_samples)
+            raise ValueError(
+                "Cannot find %d clusters with n_samples = %d"
+                % (self.c, n_samples))
+
+        if self.k >= n_samples:
+            getLogger(__name__).error(
+                "Cannot select %d nearest neighbors with n_samples = %d",
+                self.k, n_samples)
+            raise ValueError(
+                "Cannot select %d nearest neighbors with n_samples = %d"
+                % (self.k, n_samples))
+
+        indices = list(range(n_samples))
+        I = np.eye(self.k + 1)
+        H = I - np.ones((self.k + 1, self.k + 1)) / (self.k + 1)
+
+        neighbors = NearestNeighbors(
+            n_neighbors=self.k + 1,
+            algorithm='ball_tree').fit(X).kneighbors(X, return_distance=False)
+        getLogger(__name__).info("Neighbors graph: %s", neighbors)
+        X_centered = np.apply_along_axis(
+            lambda arr: X[arr].T.dot(H), 1, neighbors)
+
+        S = np.apply_along_axis(lambda arr: construct_S(arr), 1, neighbors)
+        getLogger(__name__).info("S: %s", S)
+        B = np.vectorize(
+            lambda idx: np.linalg.inv(X_centered[idx].T.dot(X_centered[idx])
+                        + self.l * I),
+            signature='()->(1,1)')(indices)
+        getLogger(__name__).info("B: %s", B)
+        Mi = np.vectorize(
+            lambda idx: S[idx].dot(H).dot(B[idx]).dot(H).dot(S[idx].T),
+            signature='()->(1,1)')(indices)
+        M = X.T.dot(Mi.sum(axis=0)).dot(X)
+        getLogger(__name__).info("M: %s", M)
+
+        D = np.eye(self.n_features_)
+        previous_target = -1
+        for step in range(self.max_iterations):
+            P = M + self.gamma * D
+            getLogger(__name__).info("P: %s", P)
+            _, W = eigh(a=P, subset_by_index=[0, self.c - 1])
+            getLogger(__name__).info("W: %s", W)
+            diag = 2 * matrix_norm(W)
+            diag[diag < 1e-10] = 1e-10  # prevents division by zero
+            D = np.diag(1 / diag)
+            getLogger(__name__).info("D: %s", D)
+
+            target = np.trace(W.T.dot(M).dot(W)) + self.gamma * l21_norm(W)
+            getLogger(__name__).info("New target value: %d", target)
+            if abs(target - previous_target) < self.epsilon:
+                break
+            previous_target = target
+
+        getLogger(__name__).info("Ended up with W = %s", W)
+        self.feature_scores_ = matrix_norm(W)
+        getLogger(__name__).info("Feature scores: %s", self.feature_scores_)
+        ranking = np.argsort(self.feature_scores_)[::-1]
+        self.selected_features_ = ranking[:self.n_features]
diff --git a/ITMO_FS/filters/unsupervised/__init__.py b/ITMO_FS/filters/unsupervised/__init__.py
index 0d23be4d..65715c00 100644
--- a/ITMO_FS/filters/unsupervised/__init__.py
+++ b/ITMO_FS/filters/unsupervised/__init__.py
@@ -1,1 +1,3 @@
+from .MCFS import MCFS
+from .UDFS import UDFS
 from .trace_ratio_laplacian import TraceRatioLaplacian
diff --git a/ITMO_FS/filters/unsupervised/trace_ratio_laplacian.py b/ITMO_FS/filters/unsupervised/trace_ratio_laplacian.py
index 5f95b01a..9ac00f41 100644
--- a/ITMO_FS/filters/unsupervised/trace_ratio_laplacian.py
+++ b/ITMO_FS/filters/unsupervised/trace_ratio_laplacian.py
@@ -1,104 +1,111 @@
+from logging import getLogger
+
 import numpy as np
 from sklearn.metrics.pairwise import pairwise_distances
-from scipy.sparse import *
-
-
-# TODO requests changes for MultivariateFilter to be used there
-class TraceRatioLaplacian(object):
-    """
-        Creates TraceRatio(similarity based) feature selection filter
-        performed in unsupervised way, i.e laplacian version
-
-        Parameters
-        ----------
-        n_selected_features : int
-            Amount of features to filter
-        k : int
-            number of neighbours to use for knn
-        t : int
-            constant for kernel function calculation
-
-            - Note: in laplacian case only. In fisher it uses label similarity, i.e if both samples belong to same class
-
-        Notes
-        -----
-        For more details see `this paper <https://aaai.org/Papers/AAAI/2008/AAAI08-107.pdf/>`_.
-
-        Examples
-        --------
-        >>> from ITMO_FS.filters.unsupervised.trace_ratio_laplacian import TraceRatioLaplacian
-        >>> from sklearn.datasets import make_classification
-        >>> x, y = make_classification(1000, 100, n_informative = 10, \
-n_redundant = 30, n_repeated = 10, shuffle = False)
-        >>> tracer = TraceRatioLaplacian(10)
-        >>> print(tracer.run(x, y)[0])
-
-
+from sklearn.neighbors import NearestNeighbors
+
+from ...utils import BaseTransformer
+
+class TraceRatioLaplacian(BaseTransformer):
+    """TraceRatio(similarity based) feature selection filter performed in
+    unsupervised way, i.e laplacian version
+
+    Parameters
+    ----------
+    n_features : int
+        Amount of features to select.
+    k : int
+        Amount of nearest neighbors to use while building the graph.
+    t : int
+        constant for kernel function calculation
+    epsilon : float
+        Lambda change threshold.
+
+    Notes
+    -----
+    For more details see `this paper <https://aaai.org/Papers/AAAI/2008/AAAI08-107.pdf/>`_.
+
+    Examples
+    --------
+    >>> from ITMO_FS.filters.unsupervised import TraceRatioLaplacian
+    >>> import numpy as np
+    >>> X = np.array([[1, 2, 3, 3, 1], [2, 2, 3, 3, 2], [1, 3, 3, 1, 3],
+    ... [1, 1, 3, 1, 4], [2, 4, 3, 1, 5]])
+    >>> y = np.array([1, 2, 1, 1, 2])
+    >>> tracer = TraceRatioLaplacian(2, k=2).fit(X)
+    >>> tracer.selected_features_
+    array([3, 1], dtype=int64)
     """
-
-    def __init__(self, n_selected_features, k=5, t=1):
-        self.n_selected_features = n_selected_features
+    def __init__(self, n_features, k=5, t=1, epsilon=1e-3):
+        self.n_features = n_features
         self.k = k
         self.t = t
+        self.epsilon = epsilon
 
-    def run(self, X, y):
-        """
-            Fits filter
-
-            Parameters
-            ----------
-            X : numpy array, shape (n_samples, n_features)
-              The training input samples
-            y : numpy array, shape (n_samples, )
-              The target values
-
-            Returns
-            ----------
-            feature_indices : numpy array
-                array of feature indices in X
-
-            See Also
-            --------
-
-            Examples
-            --------
+    def _fit(self, X, y):
+        """Fit the filter.
 
+        Parameters
+        ----------
+        X : array-likey, shape (n_samples, n_features)
+            The training input samples
+        y : array-like, shape (n_samples,)
+            The target values
+
+        Returns
+        -------
+        None
         """
-
         n_samples = X.shape[0]
-        Distances = pairwise_distances(X)
-        Distances **= 2
-        Distances_NN = np.sort(Distances, axis=1)[:, 0:self.k + 1]
-        Indices_NN = np.argsort(Distances, axis=1)[:, 0:self.k + 1]
-        Kernel = np.exp(-Distances_NN / self.t)
-        joined_distances = np.ravel(Kernel)
-        indices_axis_one = np.ravel(Indices_NN)
-        indices_axis_zero = np.repeat(np.arange(n_samples), self.k + 1)
-        A_within = csc_matrix((joined_distances, (indices_axis_zero, indices_axis_one)), shape=(n_samples, n_samples))
-        A_within = A_within - A_within.multiply(A_within.T > A_within) + A_within.T.multiply(A_within.T > A_within)
-        D_within = np.diag(np.ravel(A_within.sum(1)))  # check correctness
+
+        if self.k >= n_samples:
+            getLogger(__name__).error(
+                "Cannot select %d nearest neighbors with n_samples = %d",
+                self.k, n_samples)
+            raise ValueError(
+                "Cannot select %d nearest neighbors with n_samples = %d"
+                % (self.k, n_samples))
+
+        graph = NearestNeighbors(
+            n_neighbors=self.n_features,
+            algorithm='ball_tree').fit(X).kneighbors_graph().toarray()
+        graph = np.minimum(1, graph + graph.T)
+        getLogger(__name__).info("Nearest neighbors graph: %s", graph)
+
+        A_within = graph * pairwise_distances(
+            X, metric=lambda x, y: np.exp(-np.linalg.norm(x - y) ** 2 / self.t))
+        getLogger(__name__).info("A_within: %s", A_within)
+        D_within = np.diag(A_within.sum(axis=1))
+        getLogger(__name__).info("D_within: %s", D_within)
         L_within = D_within - A_within
-        A_between = D_within.dot(np.ones((n_samples, n_samples))).dot(D_within) / np.sum(D_within)
-        D_between = np.diag(A_between.sum(1))
+        getLogger(__name__).info("L_within: %s", L_within)
+        A_between = (D_within.dot(np.ones((n_samples, n_samples))).dot(D_within)
+                     / np.sum(D_within))
+        getLogger(__name__).info("A_between: %s", A_between)
+        D_between = np.diag(A_between.sum(axis=1))
+        getLogger(__name__).info("D_between: %s", D_between)
         L_between = D_between - A_between
+        getLogger(__name__).info("L_between: %s", L_between)
 
-        L_within = (L_within.T + L_within) / 2
-        L_between = (L_between.T + L_between) / 2
         E = X.T.dot(L_within).dot(X)
         B = X.T.dot(L_between).dot(X)
-        E = (E.T + E) / 2
-        B = (B.T + B) / 2
 
         # we need only diagonal elements for trace calculation
-        e = np.absolute(np.diag(E))
-        b = np.absolute(np.diag(B))
-        b[b == 0] = 1e-14
-        features_indices = np.argsort(np.divide(b, e))[::-1][0:self.n_selected_features]
-        lam = np.sum(b[features_indices]) / np.sum(e[features_indices])
-        prev_lam = 0
-        while (lam - prev_lam >= 1e-3):
+        e = np.array(np.diag(E))
+        b = np.array(np.diag(B))
+        getLogger(__name__).info("E: %s", e)
+        getLogger(__name__).info("B: %s", b)
+        lam = 0
+        prev_lam = -1
+        while lam - prev_lam >= self.epsilon:  # TODO: optimize
             score = b - lam * e
-            features_indices = np.argsort(score)[::-1][0:self.n_selected_features]
+            getLogger(__name__).info("Score: %s", score)
+            self.selected_features_ = np.argsort(score)[::-1][:self.n_features]
+            getLogger(__name__).info(
+                "New selected set: %s", self.selected_features_)
             prev_lam = lam
-            lam = np.sum(b[features_indices]) / np.sum(e[features_indices])
-        return features_indices, score, lam
+            lam = (np.sum(b[self.selected_features_])
+                   / np.sum(e[self.selected_features_]))
+            getLogger(__name__).info("New lambda: %d", lam)
+        self.score_ = score
+        self.lam_ = lam
diff --git a/ITMO_FS/hybrid/IWSSr_SFLA.py b/ITMO_FS/hybrid/IWSSr_SFLA.py
new file mode 100644
index 00000000..ed766226
--- /dev/null
+++ b/ITMO_FS/hybrid/IWSSr_SFLA.py
@@ -0,0 +1,398 @@
+from logging import getLogger
+
+import numpy as np
+from sklearn.model_selection import cross_val_score
+
+from ITMO_FS.filters.univariate.measures import su_measure, relief_measure
+from ITMO_FS.utils import BaseWrapper
+
+
+class IWSSr_SFLA(BaseWrapper):
+    """IWSSr-SFLA (Incremental Wrapper Subset Selection with replacement and
+    Shuffled Frog Leaping Algorithm).
+
+    Parameters
+    ----------
+    estimator : object
+        A supervised learning estimator that should have a fit(X, y) method and
+        a predict(X) method.
+    measure_iwssr : string or callable
+        A standard estimator metric (e.g. 'f1' or 'roc_auc') or a callable with
+        signature measure(estimator, X, y) which should return only a single
+        value that would be used in the IWSSr algorithm.
+    measure_frogs : string or callable
+        A standard estimator metric (e.g. 'f1' or 'roc_auc') or a callable with
+        signature measure(estimator, X, y) which should return only a single
+        value that would be used to measure the frog's fitness value.
+    cv : int
+        Number of folds in cross-validation.
+    relief_iterations : int
+        Amount of iterations to do in the Relief algorithm.
+    sfla_m : int
+        Amount of memplexes in the SFLA algorithm.
+    sfla_n : int
+        Amount of frogs in each memplex in the SFLA algorithm.
+    sfla_q : int
+        Amount of frogs in each submemplex in the SFLA algorithm. Should be
+        lower than sfla_n.
+    s : int
+        Maximum amount of features that can change during one leap in the SFLA
+        algorithm.
+    iterations : int
+        Total iteration number in the SFLA algorithm.
+    iterations_leaps : int
+        Total amount of leaps that each memplex would try to do during each
+        iteration.
+    seed : int
+        Python random seed.
+
+    See Also
+    --------
+    For more details see `this paper <https://www.nature.com/articles/s41598-019-54987-1/>`_.
+
+    Examples
+    --------
+    >>> from ITMO_FS.hybrid import IWSSr_SFLA
+    >>> from sklearn.datasets import make_classification
+    >>> from sklearn.preprocessing import KBinsDiscretizer
+    >>> from sklearn.linear_model import LogisticRegression
+    >>> dataset = make_classification(n_samples=100, n_features=20,
+    ... n_informative=5, n_redundant=0, shuffle=False, random_state=42)
+    >>> x, y = np.array(dataset[0]), np.array(dataset[1])
+    >>> x = KBinsDiscretizer(n_bins=10, encode='ordinal',
+    ... strategy='uniform').fit_transform(x)
+    >>> algo = IWSSr_SFLA(LogisticRegression()).fit(x, y)
+    >>> algo.selected_features_
+    array([ 1,  3,  4, 10, 13, 15, 17], dtype=int64)
+    """
+    def __init__(self, estimator, measure_iwssr='accuracy',
+                 measure_frogs='accuracy', cv=3, relief_iterations=None,
+                 sfla_m=5, sfla_n=20, sfla_q=8, s=3, iterations=20,
+                 iterations_leaps=10, seed=42):
+        self.estimator = estimator
+        self.measure_iwssr = measure_iwssr
+        self.measure_frogs = measure_frogs
+        self.cv = cv
+        self.relief_iterations = relief_iterations
+        self.sfla_m = sfla_m
+        self.sfla_n = sfla_n
+        self.sfla_q = sfla_q
+        self.s = s
+        self.iterations = iterations
+        self.iterations_leaps = iterations_leaps
+        self.seed = seed
+
+    def __generate_frog(self, weights):
+        """Generate a frog from the given feature weights.
+
+        Parameters
+        ----------
+        weights : array-like, shape (n_features,)
+            Probabilities for the features.
+
+        Returns
+        -------
+        array-like, shape (n_features,) : generated frog in the form of a
+        boolean array
+        """
+        f_num = self._rng.integers(self.n_features_) + 1
+        features = self._rng.choice(
+            self.n_features_, size=f_num, replace=False, p=weights)
+        frog = np.full(self.n_features_, False)
+        frog[features] = True
+        return frog
+
+    def __create_memplexes(self):
+        """Create sfla_m memplexes.
+
+        Parameters
+        ----------
+
+        Returns
+        -------
+        array-like, shape (sfla_m, sfla_n) : the indices of frogs in each of
+        the memplexes
+        """
+        return self._rng.permutation(
+            self.sfla_m * self.sfla_n).reshape((self.sfla_m, self.sfla_n))
+
+    def __iwssr(self, X, y, frog):
+        """Perform the IWSSr algorithm for given data.
+
+        Parameters
+        ----------
+        X : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like, shape (n_samples,)
+            The target values.
+        frog : array-like, shape (n_features,)
+            A frog (boolean array) of selected features.
+
+        Returns
+        -------
+        array-like, shape (n_selected_features,) : selected features
+        """
+        def __check_subset(subset):
+            nonlocal best_measure
+            nonlocal best_iteration_set
+
+            measure = cross_val_score(
+                self._estimator, X[:, subset], y, cv=self.cv,
+                scoring=self.measure_iwssr).mean()
+            if measure > best_measure:
+                best_measure = measure
+                best_iteration_set = subset
+
+        frog_features = np.flatnonzero(frog)
+        getLogger(__name__).info("Performing IWSSr for frog %s", frog_features)
+
+        weights = su_measure(X, y)
+        getLogger(__name__).info("Feature weights: %s", weights)
+        order = np.argsort(weights)[::-1]
+        selected_features = np.array([order[0]])
+        best_measure = cross_val_score(
+            self._estimator, X[:, selected_features], y, cv=self.cv,
+            scoring=self.measure_iwssr).mean()
+        best_iteration_set = selected_features
+        getLogger(__name__).info(
+            "Optimal feature set: %s, score: %d",
+            best_iteration_set, best_measure)
+
+        for feature in order[1:]:
+            getLogger(__name__).info(
+                "Trying to add feature %d into the set", feature)
+            if selected_features.shape[0] != 1:
+                for i in range(selected_features.shape[0]):
+                    __check_subset(
+                        np.append(
+                            np.delete(selected_features, i),
+                            feature))
+            __check_subset(np.append(selected_features, feature))
+            getLogger(__name__).info(
+                "Optimal feature set: %s, score: %d",
+                best_iteration_set, best_measure)
+            selected_features = best_iteration_set
+
+        selected_features = frog_features[selected_features]
+        getLogger(__name__).info("New selected features: %s", selected_features)
+
+        new_frog = np.full(self.n_features_, False)
+        new_frog[selected_features] = True
+        return new_frog
+
+    def __create_submemplex(self, fitness_values):
+        """Create a submemplex from the memplex of frogs.
+
+        Parameters
+        ----------
+        fitness_values : array-like, shape (sfla_n,)
+            Fitness values for all frogs in the memplex.
+
+        Returns
+        -------
+        array-like, shape (sfla_q,) : indices of frogs in the submemplex
+        """
+        order = np.argsort(fitness_values)[::-1]
+        weights = np.zeros(self.sfla_n)
+        weights[order] = np.arange(self.sfla_n)
+        weights = (2 * (self.sfla_n - weights)
+                   / (self.sfla_n * (self.sfla_n + 1)))
+        return self._rng.choice(
+            self.sfla_n, size=self.sfla_q, replace=False, p=weights)
+
+    def __leap(self, frogs, fitness_values, memplex, submemplex, X, y,
+               relief_weights):
+        """Create a new worst frog in the submemplex by trying to make it
+        leap towards better frogs.
+
+        Parameters
+        ----------
+        frogs : array-like, shape (sfla_n * sfla_m, n_features)
+            The population of frogs.
+        fitness_values : array-like, shape (sfla_n * sfla_m)
+            Fitness value for each frog.
+        memplex : array-like, shape (sfla_n,)
+                Indices of frogs in the memplex.
+        submemplex : array-like, shape (sfla_q,)
+            Indices of frogs in the submemplex.
+        X : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like, shape (n_samples,)
+            The target values.
+        relief_weights : array-like, shape (n_features,)
+            Weights for the features.
+
+        Returns
+        -------
+        tuple (int, array-like, float) : index of the worst frog in the
+        submemplex, the new worst frog and its fitness value
+        """
+        memplex_best_frog = memplex[np.argsort(fitness_values[memplex])[-1]]
+        submemplex_worst_frog = submemplex[np.argsort(
+            fitness_values[submemplex])[0]]
+        getLogger(__name__).info(
+            "Trying to evolve frog %s", submemplex_worst_frog)
+        worst_frog_fitness = fitness_values[submemplex_worst_frog]
+
+        new_worst_frog = self.__leap_towards(
+            frogs[memplex_best_frog], frogs[submemplex_worst_frog], X, y)
+        new_worst_frog_fitness = cross_val_score(
+            self._estimator, X[:, new_worst_frog], y, cv=self.cv,
+            scoring=self.measure_frogs).mean()
+        getLogger(__name__).info(
+            "After leaping to submemplex best frog: frog = %s, fitness = %d",
+            new_worst_frog, new_worst_frog_fitness)
+        if new_worst_frog_fitness > worst_frog_fitness:
+            return submemplex_worst_frog, new_worst_frog, new_worst_frog_fitness
+
+        population_best_frog = np.argsort(fitness_values)[-1]
+        new_worst_frog = self.__leap_towards(
+            frogs[population_best_frog], frogs[submemplex_worst_frog], X, y)
+        new_worst_frog_fitness = cross_val_score(
+            self._estimator, X[:, new_worst_frog], y, cv=self.cv,
+            scoring=self.measure_frogs).mean()
+        getLogger(__name__).info(
+            "After leaping to memplex best frog: frog = %s, fitness = %d",
+            new_worst_frog, new_worst_frog_fitness)
+        if new_worst_frog_fitness > worst_frog_fitness:
+            return submemplex_worst_frog, new_worst_frog, new_worst_frog_fitness
+
+        new_worst_frog = self.__generate_frog(relief_weights)
+        new_worst_frog_fitness = cross_val_score(
+            self._estimator, X[:, new_worst_frog], y, cv=self.cv,
+            scoring=self.measure_frogs).mean()
+        getLogger(__name__).info(
+            "Replacing with random frog: frog = %s, fitness = %d",
+            new_worst_frog, new_worst_frog_fitness)
+        return submemplex_worst_frog, new_worst_frog, new_worst_frog_fitness
+
+    def __leap_towards(self, frog_to, frog_from, X, y):
+        """Perform a leap from the worse frog to the better frog.
+
+        Parameters
+        ----------
+        frog_to : array-like, shape (n_features,)
+                The better frog (one to leap to).
+        frog_from : array-like, shape (n_features,)
+            The worse frog (one that leaps).
+        X : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like, shape (n_samples,)
+            The target values.
+
+        Returns
+        -------
+        array-like, shape (n_features,) : the new worst frog
+        """
+        len_to = np.count_nonzero(frog_to)
+        len_from = np.count_nonzero(frog_from)
+        if len_to > len_from:
+            to_add = frog_to & np.logical_not(frog_from)
+            weights = su_measure(X[:, to_add], y)
+            weights /= np.sum(weights)
+            features_num = min(
+                min(
+                    int(self._rng.random() * (len_to - len_from)),
+                    self.s),
+                np.count_nonzero(to_add))
+            added_features = self._rng.choice(
+                np.flatnonzero(to_add), size=features_num, replace=False,
+                p=weights)
+            new_worst_frog = np.copy(frog_from)
+            new_worst_frog[added_features] = True
+        else:
+            weights = su_measure(X[:, frog_from], y)
+            weights /= np.sum(weights)
+            features_num = min(
+                min(
+                    int(self._rng.random() * (len_from - len_to)),
+                    self.s),
+                len_from - 1)
+            left_features = self._rng.choice(
+                np.flatnonzero(frog_from), size=len_from - features_num,
+                replace=False, p=weights)
+            new_worst_frog = np.full(self.n_features_, False)
+            new_worst_frog[left_features] = True
+        return new_worst_frog
+
+    def __apply_sfla(self, frogs, X, y, relief_weights):
+        """Apply the SFLA algorithm to a population of frogs.
+
+        Parameters
+        ----------
+        frogs : array-like, shape (sfla_n * sfla_m, n_features)
+            The population of frogs.
+        X : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like, shape (n_samples,)
+            The target values.
+        relief_weights : array-like, shape (n_features,)
+            Weights for the features.
+
+        Returns
+        -------
+        array-like, shape (sfla_n * sfla_m, n_features) : the evolved population
+        of frogs
+        """
+        fitness_values = np.vectorize(
+            lambda frog: cross_val_score(
+                self._estimator, X[:, frog], y, cv=self.cv,
+                scoring=self.measure_frogs).mean(),
+            signature='(1)->()')(frogs)
+        getLogger(__name__).info("Fitness values for frogs: %s", fitness_values)
+        for _ in range(self.iterations):
+            memplexes = self.__create_memplexes()
+            for memplex in memplexes:
+                for _ in range(self.iterations_leaps):
+                    submemplex = memplex[self.__create_submemplex(
+                        fitness_values[memplex])]
+                    (worst_frog_index, new_worst_frog,
+                    new_worst_frog_fitness) = self.__leap(
+                        frogs, fitness_values, memplex, submemplex, X, y,
+                        relief_weights)
+                    frogs[worst_frog_index] = new_worst_frog
+                    fitness_values[worst_frog_index] = new_worst_frog_fitness
+        return frogs
+
+
+    def _fit(self, X, y):
+        """Fit the model.
+
+        Parameters
+        ----------
+        X : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like, shape (n_samples,)
+            The target values.
+
+        Returns
+        -------
+        None
+        """
+        self._rng = np.random.default_rng(self.seed)
+
+        #TODO: adding 1 ensures we have no negative probabilities; the paper
+        #doesn't say anything about that
+        relief_weights = relief_measure(
+            X, y, self.relief_iterations, random_state=self.seed) + 1
+        relief_weights /= np.sum(relief_weights)
+        getLogger(__name__).info("Relief feature weights: %s", relief_weights)
+
+        frogs = np.vectorize(
+            lambda _: self.__generate_frog(relief_weights),
+            signature='()->(1)')(np.arange(self.sfla_m * self.sfla_n))
+        getLogger(__name__).info("Initial frogs population: %s", frogs)
+        frogs = np.vectorize(
+            lambda frog: self.__iwssr(X[:, frog], y, frog),
+            signature='(1)->(1)')(frogs)
+        getLogger(__name__).info("Frogs population after IWSSr: %s", frogs)
+        frogs = self.__apply_sfla(frogs, X, y, relief_weights)
+        getLogger(__name__).info("Frogs population after SFLA: %s", frogs)
+
+        fitness_values = np.vectorize(
+            lambda frog: cross_val_score(
+                self._estimator, X[:, frog], y, cv=self.cv,
+                scoring=self.measure_frogs).mean(),
+            signature='(1)->()')(frogs)
+        self.best_score_ = np.max(fitness_values)
+        self.selected_features_ = frogs[np.argmax(fitness_values)]
diff --git a/ITMO_FS/hybrid/Melif.py b/ITMO_FS/hybrid/Melif.py
index 1edd7c7e..4031d008 100644
--- a/ITMO_FS/hybrid/Melif.py
+++ b/ITMO_FS/hybrid/Melif.py
@@ -1,143 +1,195 @@
-import datetime as dt
+from logging import getLogger
 
 import numpy as np
-from sklearn.model_selection import train_test_split
-from sklearn.utils.validation import check_X_y
+from sklearn.base import clone
+from sklearn.model_selection import cross_val_score
 
 from ITMO_FS.ensembles import WeightBased
+from ITMO_FS.utils import BaseWrapper, apply_cr
 from ITMO_FS.utils.data_check import *
 
 
-class Melif(object):
-
-    def __init__(self, filter_ensemble, scorer=None, verbose=False):  # TODO scorer name
-        if filter_ensemble is list:
-            filter_ensemble=WeightBased(filter_ensemble)
-        self.ensemble = filter_ensemble
-        self.__score = scorer
-        self.best_score = 0
-        self.best_point = []
-        self.best_f = {}
-        self.verbose = verbose
+class Melif(BaseWrapper):
+    """MeLiF algorithm.
+
+    Parameters
+    ----------
+    estimator : object
+        A supervised learning estimator that should have a fit(X, y) method and
+        a predict(X) method.
+    measure : string or callable
+        A standard estimator metric (e.g. 'f1' or 'roc_auc') or a callable with
+        signature measure(estimator, X, y) which should return only a single
+        value.
+    cutting_rule : string or callable
+        A cutting rule name defined in GLOB_CR or a callable with signature
+        cutting_rule (features), which should return a list features ranked by
+        some rule.
+    filter_ensemble : object
+        A filter ensemble (e.g. WeightBased) or a list of filters that will be
+        used to create a WeightBased ensemble.
+    delta : float
+        The step in coordinate descent.
+    points : array-like
+        An array of starting points in the search.
+    seed : int
+        Random seed used to initialize np.random.default_rng().
+    cv : int
+        Number of folds in cross-validation.
+
+    See Also
+    --------
+    For more details see `this paper <https://www.researchgate.net/publication/317201206_MeLiF_Filter_Ensemble_Learning_Algorithm_for_Gene_Selection>`_.
+
+    Examples
+    --------
+    >>> from ITMO_FS.hybrid import Melif
+    >>> from ITMO_FS.filters.univariate import UnivariateFilter
+    >>> from sklearn.datasets import make_classification
+    >>> from sklearn.preprocessing import KBinsDiscretizer
+    >>> from sklearn.linear_model import LogisticRegression
+    >>> dataset = make_classification(n_samples=100, n_features=20,
+    ... n_informative=5, n_redundant=0, shuffle=False, random_state=42)
+    >>> x, y = np.array(dataset[0]), np.array(dataset[1])
+    >>> x = KBinsDiscretizer(n_bins=10, encode='ordinal',
+    ... strategy='uniform').fit_transform(x)
+    >>> filters = [UnivariateFilter('GiniIndex'),
+    ... UnivariateFilter('FechnerCorr'),
+    ... UnivariateFilter('SpearmanCorr'),
+    ... UnivariateFilter('PearsonCorr')]
+    >>> algo = Melif(LogisticRegression(), 'f1_macro', ("K best", 5),
+    ... filters, delta=0.5).fit(x, y)
+    >>> algo.selected_features_
+    array([ 3,  4,  1, 13, 16], dtype=int64)
+    """
+    def __init__(self, estimator, measure, cutting_rule, filter_ensemble,
+                 delta=0.5, points=None, seed=42, cv=3):
+        self.estimator = estimator
+        self.measure = measure
+        self.cutting_rule = cutting_rule
+        self.filter_ensemble = filter_ensemble
+        self.delta = delta
+        self.points = points
+        self.seed = seed
+        self.cv = cv
+
+    def _fit(self, X, y):
+        """Run the MeLiF algorithm on the specified dataset.
+
+        Parameters
+        ----------
+        X : array-like, shape (n_samples, n_features)
+            The input samples.
+        y : array-like, shape (n_samples,)
+            The classes for the samples.
+
+        Returns
+        -------
+        None
+        """
+        self._rng = np.random.default_rng(self.seed)
+        if type(self.filter_ensemble) is list:
+            self.__ensemble = WeightBased(self.filter_ensemble)
+        else:
+            self.__ensemble = clone(self.filter_ensemble)
 
+        self.n_filters = len(self.__ensemble)
+        self.__filter_weights = np.ones(self.n_filters) / self.n_filters
 
-    def fit(self, X, y, estimator, cutting_rule, test_size=0.3, delta=0.5, feature_names=None, points=None):
-        """
+        check_cutting_rule(self.cutting_rule)
+        cutting_rule = apply_cr(self.cutting_rule)
+        getLogger(__name__).info(
+            "Using MeLiF with ensemble: %s and cutting rule: %s",
+            self.__ensemble, cutting_rule)
+        scores = self.__ensemble.get_scores(X, y)
 
-        :param X:
-        :param y:
-        :param estimator:
-        :param cutting_rule:
-        :param test_size:
-        :param delta:
-        :param feature_names:
-        :param points:
-        :return:
-        """
-        if self.verbose:
-            print('Running basic MeLiF\nEnsemble of :{}'.format(self.ensemble))
-        feature_names = generate_features(X, feature_names)
-        check_shapes(X, y)
-        # check_features(features_names)
-        self.__X, self.__y = check_X_y(X, y, dtype=np.float64,
-                                       order='C', accept_sparse='csr',
-                                       accept_large_sparse=False)
-        self.__feature_names = feature_names
-        self.__filter_weights = np.ones(len(self.ensemble)) / len(self.ensemble)
-        self.__points = points
-        self.__estimator = estimator
-        self.__cutting_rule = cutting_rule
-
-        self.__delta = delta
-        if self.verbose:
-            print('Estimator: {}'.format(estimator))
-            print("Optimizer greedy search, optimizing measure is {}".format(self.__score))
-            time = dt.datetime.now()
-            print("time:{}".format(time))
-
-        check_cutting_rule(cutting_rule)
-        self._train_x, self._test_x, self._train_y, self._test_y = train_test_split(self.__X, self.__y,
-                                                                                    test_size=test_size)
-        nu = self.ensemble.score(self.__X, self.__y, self.__feature_names)
-
-        if self.__points is None:
-            self.__points = [self.__filter_weights]
-            for i in range(len(self.ensemble)):
-                a = np.zeros(len(self.ensemble))
-                a[i] = 1
-                self.__points.append(a)
-        best_point = self.__points[0]
-        mapping = dict(zip(range(len(nu.keys())), nu.keys()))
-        n = dict(zip(nu.keys(), self.__measure(np.array(list(nu.values())), best_point)))
-
-        self.selected_features = self.__cutting_rule(n)
-        self.best_f = {i: nu[i] for i in self.selected_features}
-        for k, v in mapping.items():
-            nu[k] = nu.pop(v)
-        self.__search(self.__points, nu)
-        self.selected_features = [mapping[i] for i in self.selected_features]
-        for k in list(self.best_f.keys()):
-            self.best_f[mapping[k]] = self.best_f.pop(k)
-        if self.verbose:
-            print('Footer')
-            print("Best point:{}".format(self.best_point))
-            print("Best Score:{}".format(self.best_score))
-            print('Top features:')
-            for key, value in sorted(self.best_f.items(), key=lambda x: x[1], reverse=True):
-                print("Feature: {}, value: {}".format(key, value))
-
-    def transform(self, X):
-        if type(X) is np.ndarray:
-            return X[:, self.selected_features]
+        if self.points is None:
+            points = np.vstack((self.__filter_weights, np.eye(self.n_filters)))
         else:
-            return X[self.selected_features]
-
-    def fit_transform(self, X, y, estimator, cutting_rule, test_size=0.3, delta=0.5, feature_names=None, points=None):
-        self.fit(X, y, estimator, cutting_rule, test_size, delta, feature_names, points)
-        return self.transform(X)
-
-    def predict(self, X):
-        return self.__estimator.predict(self.transform(X))
-
-    def __search(self, points, features):
-        i = 0
-        border = len(points)
-        if self.verbose:
-            time = dt.datetime.now()
-        while i < len(points):
-            point = points[i]
-            if self.verbose:
-                print('Time:{}'.format(dt.datetime.now() - time))
-                print('point:{}'.format(point))
-            self.__values = np.array(list(features.values()))
-            n = dict(zip(features.keys(), self.__measure(self.__values, point)))
-            self.selected_features = self.__cutting_rule(n)
-            new_features = {i: features[i] for i in self.selected_features}
-            if new_features == {}:
-                break  # TODO rewrite that thing
-            self.__estimator.fit(self._train_x[:, self.selected_features], self._train_y)
-            predicted = self.__estimator.predict(self._test_x[:, self.selected_features])
-            score = self.__score(self._test_y, predicted)
-            if self.verbose:
-                print('Score at current point : {}'.format(score))
-            if score > self.best_score or i < border:
-                self.best_score = score
-                self.best_point = point
-                self.best_f = new_features
-                points += self.__get_candidates(point, self.__delta)
-            i += 1
-
-    def __get_candidates(self, point, delta=0.1):
-        candidates = np.tile(point, (len(point) * 2, 1)) + np.vstack(
-            (np.eye(len(point)) * delta, np.eye(len(point)) * -delta))
-        return list(candidates)
-
-    def __score_features(self, nu, candidate):
-        n = dict(zip(nu.keys(), self.__measure(nu, candidate)))
-        scores = self.__cutting_rule(n)
-        keys = list(scores.keys())
-        self.__estimator.fit(self._train_x[list(scores.keys())], self._train_y)
-        return self.__score(self._test_y, self.__estimator.predict(self._test_x[keys]))
-
-    def __measure(self, nu, weights):
-        return np.dot(nu, weights)
+            points = self.points
+        best_point_ = points[0]
+
+        self.best_score_ = 0
+        for point in points:
+            getLogger(__name__).info(
+                "Running coordinate descent from point %s", point)
+            new_point, new_score = self.__search(
+                X, y, point, scores, cutting_rule)
+            getLogger(__name__).info(
+                "Ended up in point %s with score %d", new_point, new_score)
+            if new_score > self.best_score_:
+                self.best_score_ = new_score
+                self.best_point_ = new_point
+        getLogger(__name__).info(
+            "Final best point: %s with score %d",
+            best_point_, self.best_score_)
+        self.selected_features_ = cutting_rule(
+            np.dot(scores.T, self.best_point_))
+        self._estimator.fit(X[:, self.selected_features_], y)
+
+    def __search(self, X, y, point, scores, cutting_rule):
+        """Perform a coordinate descent from the given point.
+
+        Parameters
+        ----------
+        X : array-like, shape (n_samples, n_features)
+            The input samples.
+        y : array-like, shape (n_samples,)
+            The classes for the samples.
+        point : array-like, shape (n_filters,)
+            The starting point.
+        scores : array-like, shape (n_filters, n_features)
+            The scores for the features from all filters.
+        cutting_rule : callable
+            The cutting rule to use.
+
+        Returns
+        -------
+        tuple (array-like, float) : the optimal point and its score
+        """
+        best_point = point
+        selected_features = cutting_rule(np.dot(scores.T, point))
+        best_score = cross_val_score(
+            self._estimator, X[:, selected_features], y, cv=self.cv,
+            scoring=self.measure).mean()
+        delta = np.eye(self.n_filters) * self.delta
+        changed = True
+        while changed:
+            #the original paper descends starting from the first filter;
+            #we randomize the order instead to avoid local maximas
+            getLogger(__name__).info(
+                "Current optimal point: %s with score = %d",
+                best_point, best_score)
+            order = self._rng.permutation(self.n_filters)
+            changed = False
+            for f in order:
+                iteration_point_plus = best_point + delta[f]
+                selected_features = cutting_rule(
+                    np.dot(scores.T, iteration_point_plus))
+                score = cross_val_score(
+                    self._estimator, X[:, selected_features], y, cv=self.cv,
+                    scoring=self.measure).mean()
+                getLogger(__name__).info(
+                    "Trying to move to point %s: score = %d",
+                    iteration_point_plus, score)
+                if score > best_score:
+                    best_score = score
+                    best_point = iteration_point_plus
+                    changed = True
+                    break
+
+                iteration_point_minus = best_point - delta[f]
+                selected_features = cutting_rule(
+                    np.dot(scores.T, iteration_point_minus))
+                score = cross_val_score(
+                    self._estimator, X[:, selected_features], y, cv=self.cv,
+                    scoring=self.measure).mean()
+                getLogger(__name__).info(
+                    "Trying to move to point %s: score = %d",
+                    iteration_point_minus, score)
+                if score > best_score:
+                    best_score = score
+                    best_point = iteration_point_minus
+                    changed = True
+                    break
+        return best_point, best_score
diff --git a/ITMO_FS/hybrid/__init__.py b/ITMO_FS/hybrid/__init__.py
index c256a1b2..c9d2b648 100644
--- a/ITMO_FS/hybrid/__init__.py
+++ b/ITMO_FS/hybrid/__init__.py
@@ -1,2 +1,3 @@
 from .filter_wrapper_hybrid import *
 from .Melif import Melif
+from .IWSSr_SFLA import IWSSr_SFLA
diff --git a/ITMO_FS/hybrid/filter_wrapper_hybrid.py b/ITMO_FS/hybrid/filter_wrapper_hybrid.py
index 677527ce..344d0adb 100644
--- a/ITMO_FS/hybrid/filter_wrapper_hybrid.py
+++ b/ITMO_FS/hybrid/filter_wrapper_hybrid.py
@@ -1,17 +1,90 @@
-class FilterWrapperHybrid:
+from logging import getLogger
 
+from sklearn.base import clone
+
+from ..utils import BaseTransformer
+
+class FilterWrapperHybrid(BaseTransformer):
+    """Perform the filter + wrapper hybrid algorithm by first running the
+    filter algorithm on the full dataset, leaving the selected features and
+    running the wrapper algorithm on the cut dataset.
+
+    Parameters
+    ----------
+    filter_ : object
+        A feature selection model that should have a fit(X, y) method and a
+        selected_features_ attribute available after fitting.
+    wrapper : object
+        A feature selection model that should have a fit(X, y) method,
+        selected_features_ and best_score_ attributes available after fitting
+        and a predict(X) method.
+
+    Notes
+    -----
+    This class doesn't require the first algorithm to be a filter (the only
+    requirements are a fit(X, y) method and a selected_features_ attribute)
+    but it is recommended to use a fast algorithm first to remove a lot of
+    unnecessary features before processing the resulting dataset with a more
+    time-consuming algorithm (e.g. a wrapper).
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from sklearn.linear_model import LogisticRegression
+    >>> from ITMO_FS.wrappers.deterministic import BackwardSelection
+    >>> from ITMO_FS.filters.univariate import UnivariateFilter
+    >>> from ITMO_FS.hybrid import FilterWrapperHybrid
+    >>> from sklearn.datasets import make_classification
+    >>> dataset = make_classification(n_samples=100, n_features=20,
+    ... n_informative=5, n_redundant=0, shuffle=False, random_state=42)
+    >>> x, y = np.array(dataset[0]), np.array(dataset[1])
+    >>> filter_ = UnivariateFilter('FRatio', ("K best", 10))
+    >>> wrapper = BackwardSelection(LogisticRegression(), 5, measure='f1_macro')
+    >>> model = FilterWrapperHybrid(filter_, wrapper).fit(x, y)
+    >>> model.selected_features_
+    array([ 1,  3,  4, 10,  7], dtype=int64)
+    """
     def __init__(self, filter_, wrapper):
-        self._filter = filter_
-        self._wrapper = wrapper
-        self.selected_features = None
-        self.best_score = None
+        self.filter_ = filter_
+        self.wrapper = wrapper
 
-    def fit(self, X, y):
-        new = self._filter.fit_transform(X, y)
-        self.selected_features = self._filter.selected_features
-        self._wrapper.fit(new, y)
-        self.best_score = self._wrapper.best_score
+    def _fit(self, X, y):
+        """Fit the model.
+
+        Parameters
+        ----------
+        X : array-like, shape (n_samples, n_features)
+            The input samples.
+        y : array-like, shape (n_samples,)
+            The classes for the samples.
+
+        Returns
+        -------
+        None
+        """
+        self._filter = clone(self.filter_)
+        self._wrapper = clone(self.wrapper)
+        getLogger(__name__).info(
+            "Running FilterWrapper with filter = %s, wrapper = %s",
+            self._filter, self._wrapper)
+
+        selected_filter = self._filter.fit(X, y).selected_features_
+        getLogger(__name__).info(
+            "Features selected by filter: %s", selected_filter)
+        self.selected_features_ = selected_filter[self._wrapper.fit(
+            X[:, selected_filter], y).selected_features_]
+        self.best_score_ = self._wrapper.best_score_
 
     def predict(self, X):
-        new = X[:, self.selected_features]
-        return self._wrapper.predict(new)
+        """Predict class labels for the input data.
+
+        Parameters
+        ----------
+        X : array-like, shape (n_samples, n_features)
+            The input samples.
+
+        Returns
+        ------
+        array-like, shape (n_samples,) : class labels
+        """
+        return self._wrapper.predict(X)
diff --git a/ITMO_FS/utils/DataChecker.py b/ITMO_FS/utils/DataChecker.py
deleted file mode 100644
index d0eecb7a..00000000
--- a/ITMO_FS/utils/DataChecker.py
+++ /dev/null
@@ -1,19 +0,0 @@
-class DataChecker:
-
-    @staticmethod
-    def _check_input(X, y=None, feature_names=None):
-        if feature_names is None:
-            if hasattr(X, 'columns'):
-                feature_names = X.columns
-            else:
-                feature_names = list(range(X.shape[1]))
-        if hasattr(X, 'values'):
-            X = X.values
-        if hasattr(y, 'values'):
-            # TODO maybe something better instead of ravel() to fix the 2D array error?
-            y = y.values.ravel()
-
-        return X, y, feature_names
-
-    def get_feature_names(self):
-        return [name for (index, name) in self.feature_names.items() if index in self.selected_features]
diff --git a/ITMO_FS/utils/__init__.py b/ITMO_FS/utils/__init__.py
index 5066a7ea..9fecb701 100644
--- a/ITMO_FS/utils/__init__.py
+++ b/ITMO_FS/utils/__init__.py
@@ -2,4 +2,5 @@
 from .functions import *
 from .information_theory import *
 from .qpfs_body import qpfs_body
-from .DataChecker import DataChecker
+from .base_transformer import BaseTransformer
+from .base_wrapper import BaseWrapper
diff --git a/ITMO_FS/utils/base_transformer.py b/ITMO_FS/utils/base_transformer.py
new file mode 100644
index 00000000..c147ea2a
--- /dev/null
+++ b/ITMO_FS/utils/base_transformer.py
@@ -0,0 +1,102 @@
+from abc import abstractmethod
+from logging import getLogger
+
+import numpy as np
+import pandas as pd
+from sklearn.base import BaseEstimator, TransformerMixin
+from sklearn.feature_selection import VarianceThreshold
+from sklearn.utils import check_X_y, check_array
+from sklearn.utils.validation import check_is_fitted
+
+
+class BaseTransformer(TransformerMixin, BaseEstimator):
+    def __init__(self):
+        pass
+
+    def fit(self, X, y=None, **fit_params):
+        """Fit the algorithm.
+
+        Parameters
+        ----------
+        X : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like, shape (n_samples,), optional
+            The class labels.
+        fit_params : dict, optional
+            Additional parameters to pass to underlying _fit function.
+
+        Returns
+        -------
+        Self, i.e. the transformer object.
+        """
+        if y is not None:
+            X, y = check_X_y(X, y, dtype='numeric')
+            if y.dtype.kind == 'O':
+                y = y.astype('int')
+        else:
+            X = check_array(X, dtype='float64', accept_large_sparse=False)
+
+        self.n_total_features_ = X.shape[1]
+        nonconst_features = VarianceThreshold().fit(X).get_support(indices=True)
+        self.n_features_ = nonconst_features.shape[0]
+
+        if self.n_features_ != self.n_total_features_:
+            getLogger(__name__).warning(
+                "Found %d constant features; they would not be used in fit")
+
+        if hasattr(self, 'n_features'):
+            if self.n_features > self.n_features_:
+                getLogger(__name__).error(
+                    "Cannot select %d features with n_features = %d",
+                    self.n_features, self.n_features_)
+                raise ValueError(
+                    "Cannot select %d features with n_features = %d"
+                    % (self.n_features, self.n_features_))
+
+        if hasattr(self, 'epsilon'):
+            if self.epsilon <= 0:
+                getLogger(__name__).error(
+                    "Epsilon should be positive, %d passed", self.epsilon)
+                raise ValueError(
+                    "Epsilon should be positive, %d passed" % self.epsilon)
+
+
+        self._fit(X[:, nonconst_features], y, **fit_params)
+
+        if hasattr(self, 'feature_scores_'):
+            scores = np.empty(self.n_total_features_)
+            scores.fill(np.nan)
+            scores[nonconst_features] = self.feature_scores_
+            self.feature_scores_ = scores
+        self.selected_features_ = nonconst_features[self.selected_features_]
+
+        return self
+
+    def transform(self, X):
+        """
+            Transform given data by slicing it with selected features.
+
+            Parameters
+            ----------
+            X : array-like, shape (n_samples, n_features)
+                The training input samples.
+
+            Returns
+            ------
+            Transformed 2D numpy array
+        """
+        check_is_fitted(self, 'selected_features_')
+        X_ = check_array(X, dtype='numeric', accept_large_sparse=False)
+        if X_.shape[1] != self.n_total_features_:
+            getLogger(__name__).error(
+                "Shape of input is different from what was seen in 'fit'")
+            raise ValueError(
+                "Shape of input is different from what was seen in 'fit'")
+        if isinstance(X, pd.DataFrame):
+            return X[X.columns[self.selected_features_]]
+        else:
+            return X_[:, self.selected_features_]
+
+    @abstractmethod
+    def _fit(self, X, y):
+        pass
diff --git a/ITMO_FS/utils/base_wrapper.py b/ITMO_FS/utils/base_wrapper.py
new file mode 100644
index 00000000..9a3d514f
--- /dev/null
+++ b/ITMO_FS/utils/base_wrapper.py
@@ -0,0 +1,67 @@
+from logging import getLogger
+
+from sklearn.base import clone
+from sklearn.utils import check_array
+from sklearn.utils.validation import check_is_fitted
+
+from . import BaseTransformer
+
+class BaseWrapper(BaseTransformer):
+    def __init__(self):
+        pass
+
+    def fit(self, X, y=None, **fit_params):
+        """Fit the algorithm.
+
+        Parameters
+        ----------
+        X : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like, shape (n_samples,), optional
+            The class labels.
+        fit_params : dict, optional
+            Additional parameters to pass to underlying _fit function.
+
+        Returns
+        -------
+        Self, i.e. the transformer object.
+        """
+        if not hasattr(self.estimator, 'fit'):
+            getLogger(__name__).error(
+                "estimator should be an estimator implementing "
+                "'fit' method, %s was passed", self.estimator)
+            raise TypeError(
+                "estimator should be an estimator implementing "
+                "'fit' method, %s was passed" % self.estimator)
+        if not hasattr(self.estimator, 'predict'):
+            getLogger(__name__).error(
+                "estimator should be an estimator implementing "
+                "'predict' method, %s was passed", self.estimator)
+            raise TypeError(
+                "estimator should be an estimator implementing "
+                "'predict' method, %s was passed" % self.estimator)
+        self._estimator = clone(self.estimator)
+
+        return super().fit(X, y, **fit_params)
+
+    def predict(self, X):
+        """Predict class labels for the input data.
+
+        Parameters
+        ----------
+        X : array-like, shape (n_samples, n_features)
+            The input samples.
+
+        Returns
+        -------
+        array-like, shape (n_samples,) : class labels
+        """
+        check_is_fitted(self, 'selected_features_')
+        X_ = check_array(X, dtype='float64', accept_large_sparse=False)
+        if X_.shape[1] != self.n_features_:
+            getLogger(__name__).error(
+                "Shape of input is different from what was seen in 'fit'")
+            raise ValueError(
+                "Shape of input is different from what was seen in 'fit'")
+
+        return self._estimator.predict(X_[:, self.selected_features_])
diff --git a/ITMO_FS/utils/data_check.py b/ITMO_FS/utils/data_check.py
index d9533875..ee4be92d 100644
--- a/ITMO_FS/utils/data_check.py
+++ b/ITMO_FS/utils/data_check.py
@@ -1,22 +1,6 @@
 from numpy import array
 
 
-def check_data(data):
-    if type(data) is array:
-        return
-    if type(data) is list:
-        return
-    raise TypeError("input isn't a list ar numpy array")
-
-
-def check_features(features, size):
-    if all(isinstance(x, str) for x in features):
-        if len(features) != size:
-            raise IndexError("Shapes mismatch {} and {}".format(len(features), size))
-    else:
-        raise TypeError("Features should be strings")
-
-
 def generate_features(X, features=None):
     if features is None:
         try:
@@ -29,36 +13,19 @@ def generate_features(X, features=None):
     return array(features)
 
 
-def check_shapes(X, y):
-    if X.shape[0] == y.shape[0]:
-        return
-    raise ValueError("Shape mismatch: {},{}".format(X.shape, y.shape))
-
-
 def check_filters(filters):
     for filter_ in filters:
+        attr = None
         if not hasattr(filter_, 'fit'):
-            raise TypeError(
-                "filters should be a list of filters each implementing 'fit' method, %r was passed" % filter_)
+            attr = 'fit'
         if not hasattr(filter_, 'transform'):
-            raise TypeError(
-                "filters should be a list of filters each implementing 'transform' method, %r was passed" % filter_)
+            attr = 'transform'
         if not hasattr(filter_, 'fit_transform'):
+            attr = 'fit_transform'
+        if not (attr is None):
             raise TypeError(
-                "filters should be a list of filters each implementing 'fit_transform' method, %r was passed" % filter_)
-
-
-def check_classifier(classifier):
-    pass  # TODO check if current object has fit and predict
-
-
-def check_scorer(scorer):
-    try:
-        result = scorer(1, 5)
-        if (result is float) or (result is int):
-            raise AttributeError("Scorer isn't fit")
-    except AttributeError:
-        pass  # todo scorer check
+                "filters should be a list of filters each implementing {0} "
+                "method, {1} was passed".format(attr, filter_))
 
 
 def check_cutting_rule(cutting_rule):
@@ -69,5 +36,8 @@ def check_cutting_rule(cutting_rule):
 
 
 def check_restrictions(measure_name, cutting_rule_name):
-    if measure_name in RESTRICTIONS.keys() and cutting_rule_name not in RESTRICTIONS[measure_name]:
-        raise KeyError('This measure %r doesn\'t support this cutting rule %r' % (measure_name, cutting_rule_name))
+    if (measure_name in RESTRICTIONS.keys() and
+            cutting_rule_name not in RESTRICTIONS[measure_name]):
+        raise KeyError(
+            "This measure %s doesn't support this cutting rule %s"
+            % (measure_name, cutting_rule_name))
diff --git a/ITMO_FS/utils/functions.py b/ITMO_FS/utils/functions.py
index 7b635bcd..b7c78f8b 100644
--- a/ITMO_FS/utils/functions.py
+++ b/ITMO_FS/utils/functions.py
@@ -1,115 +1,168 @@
 import numpy as np
-from numpy import array, abs
-
-def normalize(x):
-    x = abs(array(x))
-    max_ = max(x)
-    min_ = max(x)
-    return (x - min_) / (max_ - min_)
+from sklearn.metrics import f1_score
+from sklearn.metrics.pairwise import euclidean_distances
 
 
 def cartesian(rw, cl):  # returns cartesian product for passed numpy arrays as two paired numpy array
     tmp = np.array(np.meshgrid(rw, cl)).T.reshape(len(rw) * len(cl), 2)
     return tmp.T[0], tmp.T[1]
 
+def weight_func(model):  # weight function used in MOS testing
+    return model.coef_[0]
+
+def f1_scorer(y_true, y_pred):
+    return f1_score(y_true, y_pred, average='micro')
 
 def augmented_rvalue(X, y, k=7, theta=3):
-    """
-    Calculates the augmented R-value for a dataset with two (0, 1) classes. 
-    The original paper supposes that the majority of objects are of class 0 (negative).
-        Parameters
-        ----------
-        X : array-like, shape (n_samples,n_features)
-            The input samples.
-        y : array-like, shape (n_samples)
-            The classes for the samples.
-        k : int
-            The amount of nearest neighbors used in the calculation.
-        theta : int
-            The threshold value: if from k nearest neighbors of an object more than theta of them are of a different class, 
-            then this object is in the overlap region.
-        Returns
-        ------
-        float - the augmented R-value for the dataset; the value is in the range [-1, 1].
-
-        Notes
-        -----
-        For more details see `this paper <https://www.sciencedirect.com/science/article/pii/S0169743919306070>`_.
+    """Calculate the augmented R-value for a dataset with two classes.
 
+    Parameters
+    ----------
+    X : array-like, shape (n_samples, n_features)
+        The input samples.
+    y : array-like, shape (n_samples,)
+        The classes for the samples.
+    k : int
+        The amount of nearest neighbors used in the calculation.
+    theta : int
+        The threshold value: if from k nearest neighbors of an object more than
+        theta of them are of a different class, then this object is in the
+        overlap region.
+
+    Returns
+    -------
+    float - the augmented R-value for the dataset; the value is in the range
+    [-1, 1].
+
+    Notes
+    -----
+    For more details see `this paper <https://www.sciencedirect.com/science/article/pii/S0169743919306070>`_.
     """
-    indicesNegative = [i for i, x in enumerate(y) if x == 0]
-    indicesPositive = [i for i, x in enumerate(y) if x == 1]
-    R0, R1 = 0, 0
-    for elem in indicesNegative:
-        nearest = knn(X, y, elem, k)
-        R0 += np.sign(list(map(lambda x: y[x], nearest)).count(1) - theta)
-    R0 /= len(indicesNegative)
-    for elem in indicesPositive:
-        nearest = knn(X, y, elem, k)
-        R1 += np.sign(list(map(lambda x: y[x], nearest)).count(0) - theta)
-    R1 /= len(indicesPositive)
-    return (R0 * len(indicesPositive) + R1 * len(indicesNegative)) / len(X)
-
-
-def knn(X, y, index, k, allClasses=True):
-    """
-    Returns the indices of k nearest neighbors of X[index].
-        Parameters
-        ----------
-        X : array-like, shape (n_samples,n_features)
-            The input samples.
-        y : array-like, shape (n_samples)
-            The classes for the samples.
-        index : int
-            The index of an element.
-        k : int
-            The amount of nearest neighbors to return.
-        allClasses : bool
-            If false, returns only k nearest neighbors belonging to the same class.
-        Returns
-        ------
-        array-like, shape(k) - the indices of the nearest neighbors
+    unique, counts = np.unique(y, return_counts=True)
+    freq = sorted(list(zip(unique, counts)), key=lambda x: x[1], reverse=True)
+    dm = euclidean_distances(X, X)
+    Rs = []
+    Cs = []
+
+    for label, frequency in freq:
+        Cs.append(frequency)
+        count = 0
+        for elem in [i for i, x in enumerate(y) if x == label]:
+            nearest = knn_from_class(dm, y, elem, k, 1, anyClass=True)
+            count += np.sign(
+                k
+                - list(map(lambda x: y[x], nearest)).count(label)
+                - theta)
+        Rs.append(count / frequency)
+    Cs = Cs[::-1]
+    return np.dot(Rs, Cs) / len(X)
+
+
+def knn_from_class(distances, y, index, k, cl, anyOtherClass=False,
+                   anyClass=False):
+    """Return the indices of k nearest neighbors of X[index] from the selected
+    class.
+
+    Parameters
+    ----------
+    distances : array-like, shape (n_samples, n_samples)
+        The distance matrix of the input samples.
+    y : array-like, shape (n_samples,)
+        The classes for the samples.
+    index : int
+        The index of an element.
+    k : int
+        The amount of nearest neighbors to return.
+    cl : int
+        The class label for the nearest neighbors.
+    anyClass : bool
+        If True, returns neighbors not belonging to the same class as X[index].
+
+    Returns
+    -------
+    array-like, shape (k,) - the indices of the nearest neighbors
     """
-    distances = map(lambda x: (x[0], np.linalg.norm(X[index] - x[1])),
-                    [(i, x) for i, x in enumerate(X) if i != index and (allClasses or y[i] == y[index])])
-    nearest = sorted(distances, key=lambda x: x[1])[:k]
-    return np.array(list(map(lambda x: x[0], nearest)))
+    y_c = np.copy(y)
+    if anyOtherClass:
+        cl = y_c[index] + 1
+        y_c[y_c != y_c[index]] = cl
+    if anyClass:
+        y_c.fill(cl)
+    class_indices = np.nonzero(y_c == cl)[0]
+    distances_class = distances[index][class_indices]
+    nearest = np.argsort(distances_class)
+    if y_c[index] == cl:
+        nearest = nearest[1:]
+
+    return class_indices[nearest[:k]]
 
 def matrix_norm(M):
-    """
-    Calculates the norm of all rows in the matrix.
-        Parameters
-        ----------
-        M : array-like, shape (n, m)
-            The matrix.
-        Returns
-        -------
-        array-like, shape (n) - the norms for each row in the matrix
+    """Calculate the norm of all rows in the matrix.
+
+    Parameters
+    ----------
+    M : array-like, shape (n, m)
+        The matrix.
+
+    Returns
+    -------
+    array-like, shape (n,) : the norms for each row in the matrix
     """
     return np.sqrt((M * M).sum(axis=1))
 
 def l21_norm(M):
-    """
-    Calculates the L2,1 norm of a matrix.
-        Parameters
-        ----------
-        M : array-like, shape (n, m)
-            The matrix.
-        Returns
-        -------
-        float - the L2,1 norm of this matrix
+    """Calculate the L2,1 norm of a matrix.
+
+    Parameters
+    ----------
+    M : array-like, shape (n, m)
+        The matrix.
+
+    Returns
+    -------
+    float : the L2,1 norm of this matrix
     """
     return matrix_norm(M).sum()
 
 def power_neg_half(M):
+    """Calculate M ^ (-1/2).
+
+    Parameters
+    ----------
+    M : array-like, shape (n, m)
+        The matrix.
+
+    Returns
+    -------
+    array-like, shape (n, m) : M ^ (-1/2)
     """
-    Calculates M ^ (-1/2).
-        Parameters
-        ----------
-        M : array-like, shape (n, m)
-            The matrix.
-        Returns
-        -------
-        array-like, shape (n, m) - M ^ (-1/2)
+    return np.sqrt(np.linalg.inv(M))
+
+def apply_cr(cutting_rule):
+    """Extract the cutting rule from a tuple or callable.
+
+    Parameters
+    ----------
+    cutting_rule : tuple or callable
+        A (str, float) tuple describing a cutting rule or a callable with
+        signature cutting_rule (features) which should return a list of features
+        ranked by some rule.
+
+    Returns
+    -------
+    callable : a cutting rule callable
     """
-    return np.sqrt(np.linalg.inv(M))
\ No newline at end of file
+    from ..filters.univariate.measures import CR_NAMES, MEASURE_NAMES
+    if type(cutting_rule) is tuple:
+        cutting_rule_name = cutting_rule[0]
+        cutting_rule_value = cutting_rule[1]
+        try:
+            cr = CR_NAMES[cutting_rule_name](cutting_rule_value)
+        except KeyError:
+            raise KeyError("No %s cutting rule yet" % cutting_rule_name)
+    elif hasattr(cutting_rule, '__call__'):
+        cr = cutting_rule
+    else:
+        raise KeyError(
+            "%s isn't a cutting rule function or string" % cutting_rule)
+    return cr
diff --git a/ITMO_FS/utils/information_theory.py b/ITMO_FS/utils/information_theory.py
index 827e831a..657ac8e1 100644
--- a/ITMO_FS/utils/information_theory.py
+++ b/ITMO_FS/utils/information_theory.py
@@ -7,34 +7,286 @@
 
 
 def conditional_entropy(x_j, y):
-    # H(Y|X)
-    buf = [[e[1] for e in g] for _, g in groupby(sorted(zip(x_j, y)), itemgetter(0))]
+    """Calculate the conditional entropy (H(Y|X)) between two arrays.
+
+    Parameters
+    ----------
+    x_j : array-like, shape (n,)
+        The first array.
+    y : array-like, shape (n,)
+        The second array.
+
+    Returns
+    -------
+    float : H(Y|X) value
+
+    Examples
+    --------
+    >>> from ITMO_FS.utils.information_theory import conditional_entropy
+    >>> conditional_entropy([1,2,1,3,4], [1,2,3,4,5])
+    0.2772588722239781
+    >>> conditional_entropy([1], [2])
+    0.0
+    >>> conditional_entropy([1,2,1,3,2,4], [3,3,3,3,3,3])
+    0.0
+    >>> conditional_entropy([1,2,3,1,3,2,3,4,1], [1,2,1,3,1,4,4,1,5])
+    0.7324081924454064
+    """
+    buf = [[e[1] for e in g] for _, g in 
+           groupby(sorted(zip(x_j, y)), itemgetter(0))]
     return fsum(entropy(group) * len(group) for group in buf) / len(x_j)
 
 
+def joint_entropy(*arrs):
+    """Calculate the joint entropy (H(X;Y;...)) between multiple arrays.
+
+    Parameters
+    ----------
+    arrs : any number of array-like, all of shape (n,)
+        Any number of arrays.
+
+    Returns
+    -------
+    float : H(X;Y;...) value
+
+    Examples
+    --------
+    >>> from ITMO_FS.utils.information_theory import joint_entropy
+    >>> joint_entropy([1,2,3,4,5])
+    1.6094379124341003
+    >>> joint_entropy([1,2,3,4,4])
+    1.3321790402101221
+    >>> joint_entropy([1,2,3,4,5,6], [1,2,3,4,5,6], [1,2,3,4,5,6])
+    1.791759469228055
+    >>> joint_entropy([1,2,1,3,2], [3,3,3,3,3])
+    1.0549201679861442
+    >>> conditional_entropy([1,1], [2,2])
+    0.0
+    """
+    return entropy(list(zip(*arrs)))
+
+
 def matrix_mutual_information(x, y):
+    """Calculate the mutual information (I(X;Y) = H(Y) - H(Y|X)) between each
+    column of the matrix and an array.
+
+    Parameters
+    ----------
+    x : array-like, shape (n, n_features)
+        The matrix.
+    y : array-like, shape (n,)
+        The second array.
+
+    Returns
+    -------
+    array-like, shape (n_features,) : I(X;Y) values for all columns of the
+    matrix
+
+    Examples
+    --------
+    >>> from ITMO_FS.utils.information_theory import matrix_mutual_information
+    >>> matrix_mutual_information([[1,3,2,1], [2,2,2,1], [3,3,2,2]], [1,1,2])
+    array([0.63651417, 0.17441605, 0.        , 0.63651417])
+    """
     return np.apply_along_axis(mutual_information, 0, x, y)
 
 
 def mutual_information(x, y):
+    """Calculate the mutual information (I(X;Y) = H(Y) - H(Y|X)) between two
+    arrays.
+
+    Parameters
+    ----------
+    x : array-like, shape (n,)
+        The first array.
+    y : array-like, shape (n,)
+        The second array.
+
+    Returns
+    -------
+    float : I(X;Y) value
+
+    Examples
+    --------
+    >>> from ITMO_FS.utils.information_theory import mutual_information
+    >>> mutual_information([1,2,3,4,5], [5,4,3,2,1])
+    1.6094379124341003
+    >>> mutual_information([1,2,3,1,2,3,1,2,3], [1,1,2,2,3,3,4,4,5])
+    0.48248146150371407
+    >>> mutual_information([1,2,3], [1,1,1])
+    0.0
+    >>> mutual_information([1,2,1,3,2,4,3,1], [1,2,3,4,2,3,2,1])
+    0.9089087348987808
+    """
     return entropy(y) - conditional_entropy(x, y)
 
 
 def conditional_mutual_information(x, y, z):
-    return entropy(list(zip(x, z))) + entropy(list(zip(y, z))) - entropy(list(zip(x, y, z))) - entropy(z)
+    """Calculate the conditional mutual information (I(X;Y|Z) = H(X;Z) + H(Y;Z)
+    - H(X;Y;Z) - H(Z)) between three arrays.
+
+    Parameters
+    ----------
+    x : array-like, shape (n,)
+        The first array.
+    y : array-like, shape (n,)
+        The second array.
+    z : array-like, shape (n,)
+        The third array.
+
+    Returns
+    -------
+    float : I(X;Y|Z) value
+
+    Examples
+    --------
+    >>> from ITMO_FS.utils import conditional_mutual_information
+    >>> conditional_mutual_information([1,3,2,1], [2,2,2,1], [3,3,2,2])
+    0.3465735902799726
+    >>> conditional_mutual_information([1,1,1,1,1], [2,3,4,2,1], [1,2,1,2,1])
+    0.0
+    >>> conditional_mutual_information([1,2,3,4,1], [2,3,4,2,1], [1,1,1,1,1])
+    1.054920167986144
+    >>> conditional_mutual_information([1,2,3], [1,1,1], [3,2,2])
+    0.0
+    >>> conditional_mutual_information([1,2,3,4,1,3,2,1,4,5],
+    ... [1,3,2,4,5,4,3,2,1,2], [2,1,4,3,2,6,5,2,1,3])
+    0.27725887222397816
+    """
+    return (entropy(list(zip(x, z)))
+            + entropy(list(zip(y, z)))
+            - entropy(list(zip(x, y, z)))
+            - entropy(z))
 
 
 def joint_mutual_information(x, y, z):
+    """Calculate the joint mutual information (I(X,Y;Z) = I(X;Z) + I(Y;Z|X))
+    between three arrays.
+
+    Parameters
+    ----------
+    x : array-like, shape (n,)
+        The first array.
+    y : array-like, shape (n,)
+        The second array.
+    z : array-like, shape (n,)
+        The third array.
+
+    Returns
+    -------
+    float : I(X,Y;Z) value
+
+    Examples
+    --------
+    >>> from ITMO_FS.utils.information_theory import joint_mutual_information
+    >>> joint_mutual_information([1,3,2,1], [2,2,2,1], [3,3,2,2])
+    0.6931471805599454
+    >>> joint_mutual_information([1,1,1,1,1], [2,3,4,2,1], [1,2,1,2,1])
+    0.39575279478527814
+    >>> joint_mutual_information([1,2,3,4,1], [2,3,4,2,1], [1,1,1,1,1])
+    0.0
+    >>> joint_mutual_information([1,2,3], [1,1,1], [3,2,2])
+    0.636514168294813
+    >>> joint_mutual_information([1,2,3,4,1,3,2,1,4,5],
+    ... [1,3,2,4,5,4,3,2,1,2], [2,1,4,3,2,6,5,2,1,3])
+    1.5571130980576458
+    """
     return mutual_information(x, z) + conditional_mutual_information(y, z, x)
 
 
 def interaction_information(x, y, z):
-    return mutual_information(x, z) + mutual_information(y, z) - joint_mutual_information(x, y, z)
+    """Calculate the interaction information (I(X;Y;Z) = I(X;Y) - I(X;Y|Z))
+    between three arrays.
+
+    Parameters
+    ----------
+    x : array-like, shape (n,)
+        The first array.
+    y : array-like, shape (n,)
+        The second array.
+    z : array-like, shape (n,)
+        The third array.
 
+    Returns
+    -------
+    float : I(X;Y;Z) value
 
-def elog(x):
-    return x * log(x) if 0. < x < 1. else 0.
+    Examples
+    --------
+    >>> from ITMO_FS.utils.information_theory import interaction_information
+    >>> interaction_information([1,3,2,1], [2,2,2,1], [3,3,2,2])
+    -0.13081203594113694
+    >>> interaction_information([1,1,1,1,1], [2,3,4,2,1], [1,2,1,2,1])
+    0.0
+    >>> interaction_information([1,2,3,4,1], [2,3,4,2,1], [1,1,1,1,1])
+    0.0
+    >>> interaction_information([1,2,3], [1,1,1], [3,2,2])
+    0.0
+    >>> interaction_information([1,2,3,4,1,3,2,1,4,5],
+    ... [1,3,2,4,5,4,3,2,1,2], [2,1,4,3,2,6,5,2,1,3])
+    0.6730116670092565
+    """
+    return mutual_information(x, y) - conditional_mutual_information(x, y, z)
 
 
+def symmetrical_relevance(x, y, z):
+    """Calculate the symmetrical relevance (SR(X;Y;Z) = I(X;Y;Z) / H(X;Y|Z))
+    between three arrays.
+
+    Parameters
+    ----------
+    x : array-like, shape (n,)
+        The first array.
+    y : array-like, shape (n,)
+        The second array.
+    z : array-like, shape (n,)
+        The third array.
+
+    Returns
+    -------
+    float : SR(X;Y;Z) value
+
+    Examples
+    --------
+    >>> from ITMO_FS.utils.information_theory import symmetrical_relevance
+    >>> symmetrical_relevance([1,3,2,1], [2,2,2,1], [3,3,2,2])
+    0.5000000000000001
+    >>> symmetrical_relevance([1,1,1,1,1], [2,3,4,2,1], [1,2,1,2,1])
+    0.2458950368496943
+    >>> symmetrical_relevance([1,2,3,4,1], [2,3,4,2,1], [1,1,1,1,1])
+    0.0
+    >>> symmetrical_relevance([1,2,3], [1,1,1], [3,2,2])
+    0.5793801642856952
+    >>> symmetrical_relevance([1,2,3,4,1,3,2,1,4,5],
+    ... [1,3,2,4,5,4,3,2,1,2], [2,1,4,3,2,6,5,2,1,3])
+    0.6762456261857126
+    """
+    return joint_mutual_information(x, y, z) / joint_entropy(x, y, z)
+
 def entropy(x):
+    """Calculate the entropy (H(X)) of an array.
+
+    Parameters
+    ----------
+    x : array-like, shape (n,)
+        The array.
+
+    Returns
+    -------
+    float : H(X) value
+
+    Examples
+    --------
+    >>> from ITMO_FS.utils.information_theory import entropy
+    >>> entropy([1,1,1])
+    0.0
+    >>> entropy([1,2,3,4,5])
+    1.6094379124341003
+    >>> entropy([5,4,1,2,3])
+    1.6094379124341003
+    >>> entropy([1,2,1,2,1,2,1,2,1,2])
+    0.6931471805599456
+    >>> entropy([1,1,1,1,1,2])
+    0.4505612088663047
+    """
     return log(len(x)) - fsum(v * log(v) for v in Counter(x).values()) / len(x)
diff --git a/ITMO_FS/wrappers/deterministic/AddDelWrapper.py b/ITMO_FS/wrappers/deterministic/AddDelWrapper.py
index f4377aaa..a14a77a6 100644
--- a/ITMO_FS/wrappers/deterministic/AddDelWrapper.py
+++ b/ITMO_FS/wrappers/deterministic/AddDelWrapper.py
@@ -1,212 +1,201 @@
+from logging import getLogger
 import random as rnd
-from copy import copy
-from importlib import reload
 
 import numpy as np
-from sklearn.metrics import make_scorer
 from sklearn.model_selection import cross_val_score
 
-
-class AddDelWrapper(object):
+from ...utils import BaseWrapper, generate_features
+
+
+class AddDelWrapper(BaseWrapper):
+    """Add-Del feature wrapper.
+
+    Parameters
+    ----------
+    estimator : object
+        A supervised learning estimator that should have a fit(X, y) method and
+        a predict(X) method.
+    measure : string or callable
+        A standard estimator metric (e.g. 'f1' or 'roc_auc') or a callable with
+        signature measure(estimator, X, y) which should return only a single
+        value.
+    cv : int
+        Number of folds in cross-validation.
+    seed : int
+        Seed for python random.
+    d : int
+        Amount of consecutive iterations for add ana del procedures that can
+        have decreasing objective function before the algorithm terminates.
+
+    See Also
+    --------
+    Lecture about feature selection (ru), p.13 -
+    http://www.ccas.ru/voron/download/Modeling.pdf
+
+    Examples
+    --------
+    >>> from sklearn.datasets import make_classification
+    >>> from sklearn.linear_model import LogisticRegression
+    >>> dataset = make_classification(n_samples=100, n_features=20,
+    ... n_informative=5, n_redundant=0, shuffle=False, random_state=42)
+    >>> x, y = np.array(dataset[0]), np.array(dataset[1])
+    >>> lg = LogisticRegression(solver='lbfgs')
+    >>> add_del = AddDelWrapper(lg, 'accuracy').fit(x, y)
+    >>> add_del.selected_features_
+    array([1, 4, 3], dtype=int64)
     """
-        Creates add-del feature wrapper
+    def __init__(self, estimator, measure, cv=3, seed=42, d=1):
+        self.estimator = estimator
+        self.measure = measure
+        self.cv = cv
+        self.seed = seed
+        self.d = d
+
+    def __add(self, X, y, free_features):
+        """Add features to the selected set one by one until either all of
+        the features are added or more than d iterations pass without
+        increasing the objective function.
 
         Parameters
         ----------
-        estimator: object
-            A supervised learning estimator with a fit method
-        score : callable
-            A callable function which will be used to estimate score
-        score : boolean
-            maximize = True if bigger values are better for score function
-        seed: int
-            Seed for python random
-        best_score : float
-            The best score of given metric on the feature combination after add-del procedure
-
-        See Also
-        --------
-        Lecture about feature selection (ru), p.13 - http://www.ccas.ru/voron/download/Modeling.pdf
-
-        Examples
-        --------
-        >>> from sklearn.metrics import accuracy_score
-        >>> from sklearn import datasets,linear_model
-        >>> data = datasets.make_classification(n_samples=1000, n_features=20)
-        >>> X = np.array(data[0])
-        >>> y = np.array(data[1])
-        >>> lg = linear_model.LogisticRegression(solver='lbfgs')
-        >>> add_del = AddDelWrapper(lg, accuracy_score)
-        >>> add_del.fit(X, y)
-
-        >>> from sklearn.metrics import mean_absolute_error
-        >>> boston = datasets.load_boston()
-        >>> X = boston['data']
-        >>> y = boston['target']
-        >>> lasso = linear_model.Lasso()
-        >>> add_del = AddDelWrapper(lasso, mean_absolute_error, maximize=False)
-        >>> add_del.fit(X, y)
-    """
-
-    def __init__(self, estimator, score, maximize=True, seed=42):
-        if not hasattr(estimator, 'fit'):
-            raise TypeError("estimator should be an estimator implementing "
-                            "'fit' method, %r was passed" % estimator)
-        self._estimator = estimator
-        self.score = score
-        self.maximize = maximize
-        rnd.seed(seed)
-        self.best_score = 0.0
-
-    def __add(self, X, y, cv=3, silent=True):
-
-        prev_score = 0
-        scores = []
-
-        to_append = [i for i in range(X.shape[1])]  # list of features not used in final configuration
-        appended = []  # list of features in final configuration
-
-        for feature in to_append:
-
-            appended.append(feature)
-
-            current_score = abs(np.mean(cross_val_score(self._estimator, X[:, appended], y,
-                                                        scoring=make_scorer(self.score,
-                                                                            greater_is_better=self.maximize),
-                                                        cv=cv)))
-            scores.append(current_score)
-
-            if not silent:
-                print('feature {} (score: {})'.format(feature, current_score))
-
-            if self.maximize == True and current_score <= prev_score:
-                appended.pop()
-
-            elif self.maximize == False and current_score > prev_score:
-                appended.pop()
-
-            prev_score = current_score
-
-        if not silent:
-            if self.maximize:
-                print('max score: {}'.format(np.max(scores)))
-            elif not self.maximize:
-                print('min score: {}'.format(np.min(scores)))
-
-        return appended
-
-    def __del(self, X, y, features, cv=3, silent=True):
-
-        prev_score = abs(np.mean(cross_val_score(self._estimator, X[:, features], y,
-                                                 scoring=make_scorer(self.score, greater_is_better=self.maximize),
-                                                 cv=cv)))
-        current_score = 0
-        scores = [prev_score]
-        res_score = 0
-        if not silent:
-            print('score: {}'.format(prev_score))
-
-        iter_features = copy(features)
-
-        for feature in iter_features:
-
-            features.remove(feature)
-
-            current_score = abs(np.mean(cross_val_score(self._estimator, X[:, features], y,
-                                                        scoring=make_scorer(self.score,
-                                                                            greater_is_better=self.maximize),
-                                                        cv=cv)))
-            scores.append(current_score)
-
-            if not silent:
-                print('remove feature {} (score: {})'.format(feature, current_score))
-
-            if self.maximize and prev_score > current_score:
-                features.append(feature)
-
-            if not self.maximize and prev_score <= current_score:
-                features.append(feature)
-
-            if self.maximize and current_score > prev_score:
-                prev_score = current_score
-
-            if not self.maximize and current_score <= prev_score:
-                prev_score = current_score
-
-        if self.maximize:
-            res_score = np.max(scores)
-        elif not self.maximize:
-            res_score = np.min(scores)
-
-        if silent == 'False':
-            print('score: {}'.format(res_score))
+        X : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like, shape (n_samples,)
+            The target values.
+        free_features : array-like, shape (n_not_selected_features,)
+            The array of current free features.
+
+        Returns
+        -------
+        array-like, shape (n_new_selected_features,) : selected features;
+        array-like, shape (n_new_not_selected_features,) : new free features
+        """
+        best_score = self.best_score_
+        iteration_features = self.selected_features_
+        iteration_free_features = free_features
+        selected_features = self.selected_features_
+        getLogger(__name__).info(
+            "Trying to add features from free set %s to selected set %s",
+            free_features, selected_features)
+
+        while (iteration_features.shape[0] - selected_features.shape[0] <=
+                   self.d) & (iteration_free_features.shape[0] != 0):
+            getLogger(__name__).info(
+                "Current selected set: %s, best score: %d",
+                selected_features, best_score)
+            scores = np.vectorize(
+                lambda f: cross_val_score(
+                    self._estimator, X[:, np.append(iteration_features, f)], y,
+                    cv=self.cv, scoring=self.measure).mean())(
+            iteration_free_features)
+            getLogger(__name__).info("Scores for all free features: %s", scores)
+
+            to_add = np.argmax(scores)
+            iteration_score = scores[to_add]
+            getLogger(__name__).info(
+                "Adding feature %d, new score: %d",
+                iteration_free_features[to_add], iteration_score)
+            iteration_features = np.append(
+                iteration_features, iteration_free_features[to_add])
+            iteration_free_features = np.delete(iteration_free_features, to_add)
+
+            if iteration_score > best_score:
+                selected_features = iteration_features
+                free_features = iteration_free_features
+                best_score = iteration_score
+
+        return selected_features, free_features
+
+    def __del(self, X, y, selected_features, free_features):
+        """Delete features from the selected set one by one until either only
+        one feature is left or more than d iterations pass without
+        increasing the objective function.
 
-        return features, res_score
+        Parameters
+        ----------
+        X : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like, shape (n_samples,)
+            The target values.
+        selected_features : array-like, shape (n_selected_features,)
+            The array of current selected features.
+        free_features : array-like, shape (n_not_selected_features,)
+            The array of current free features.
+
+        Returns
+        -------
+        array-like, shape (n_new_selected_features,) : new selected features;
+        array-like, shape (n_new_not_selected_features,) : new free features;
+        float : score for the selected feature set
+        """
+        best_score = cross_val_score(
+            self._estimator, X[:, selected_features], y, scoring=self.measure,
+            cv=self.cv).mean()
+        iteration_features = selected_features
+        iteration_free_features = free_features
+        getLogger(__name__).info(
+            "Trying to delete features from selected set %s", selected_features)
+
+        while (selected_features.shape[0] - iteration_features.shape[0] <=
+                   self.d) & (iteration_features.shape[0] != 1):
+            getLogger(__name__).info(
+                "Current selected set: %s, best score: %d",
+                selected_features, best_score)
+            scores = np.vectorize(
+                lambda i: cross_val_score(
+                    self._estimator, X[:, np.delete(iteration_features, i)], y,
+                    cv=self.cv, scoring=self.measure).mean())(
+            np.arange(0, iteration_features.shape[0]))
+            getLogger(__name__).info(
+                "Scores for all selected features: %s", scores)
+
+            to_delete = np.argmax(scores)
+            iteration_score = scores[to_delete]
+            getLogger(__name__).info(
+                "Deleting feature %d, new score: %d",
+                iteration_features[to_delete], iteration_score)
+            iteration_free_features = np.append(
+                iteration_free_features, iteration_features[to_delete])
+            iteration_features = np.delete(iteration_features, to_delete)
+
+            if iteration_score > best_score:
+                selected_features = iteration_features
+                free_features = iteration_free_features
+                best_score = iteration_score
+
+        return selected_features, free_features, best_score
+
+    def _fit(self, X, y):
+        """Fit the wrapper.
 
-    def fit(self, X, y, cv=3, silent=True):  ##TODO with fit predict
+        Parameters
+        ----------
+        X : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like, shape (n_samples,)
+            The target values.
+
+        Returns
+        -------
+        None
         """
-           Fits wrapper.
-
-           Parameters
-           ----------
-           X : numpy array or pandas DataFrame, shape (n_samples, n_features)
-               The training input samples.
-           y : numpy array of pandas Series, shape (n_samples, )
-               The target values.
-           cv=3 : int
-               Number of splits in cross-validation
-           silent=True : boolean
-               If silent=False then prints all the scores during add-del procedure
-
-           Returns:
-           ----------
-           features : list
-               List of feature after add-del procedure
-
-           See Also
-           --------
-
-           examples
-           --------
-           :param silent:
-           :param y:
-           :param X:
-           :param cv:
-
-       """
-
-        return_feature_names = False
-
-        try:
-            import pandas
-
-            if isinstance(X, pandas.DataFrame):
-                return_feature_names = True
-                columns = np.array(X.columns)
-                return_feature_names = True
+        self.selected_features_ = np.array([], dtype='int')
+        free_features = generate_features(X)
+        self.best_score_ = 0
+        while True:
+            selected_features, free_features = self.__add(X, y, free_features)
+            getLogger(__name__).info(
+                "After add: selected set = %s, free set = %s",
+                selected_features, free_features)
+            selected_features, free_features, iteration_score = self.__del(
+                X, y, selected_features, free_features)
+            getLogger(__name__).info(
+                "After del: selected set = %s, free set = %s, score = %d",
+                selected_features, free_features, iteration_score)
+
+            if iteration_score > self.best_score_:
+                self.best_score_ = iteration_score
+                self.selected_features_ = selected_features
             else:
-                pandas = reload(pandas)
-        except ImportError:
-            pass
-
-        X = np.array(X)
-        y = np.array(y).ravel()
-
-        if not silent:
-            print('add trial')
-        features = self.__add(X, y, cv, silent)
-
-        if not silent:
-            print('del trial')
-
-        features, score = self.__del(X, y, features, cv, silent)
-        self.best_score = score
-
-        if return_feature_names:
-            features = list(columns[features])
-
-        self.__features__ = features
-        self._estimator.fit(X[:, features], y)
-
-    def predict(self, X):
-        return self._estimator.predict(X)
+                break
+        self._estimator.fit(X[:, self.selected_features_], y)
diff --git a/ITMO_FS/wrappers/deterministic/BackwardSelection.py b/ITMO_FS/wrappers/deterministic/BackwardSelection.py
index 5e7d2d5b..7ce9729e 100644
--- a/ITMO_FS/wrappers/deterministic/BackwardSelection.py
+++ b/ITMO_FS/wrappers/deterministic/BackwardSelection.py
@@ -1,91 +1,88 @@
-#!/usr/bin/env python
-import numpy as np
-
-from ...utils import generate_features
-
+from logging import getLogger
 
-class BackwardSelection:
+import numpy as np
+from sklearn.model_selection import cross_val_score
+
+from ...utils import generate_features, BaseWrapper
+
+
+class BackwardSelection(BaseWrapper):
+    """Backward Selection removes one feature at a time until the number of
+    features to be removed is reached. On each step, the best n-1 features
+    out of n are chosen (according to some estimator metric) and the last one
+    is removed.
+
+    Parameters
+    ----------
+    estimator : object
+        A supervised learning estimator that should have a fit(X, y) method and
+        a predict(X) method.
+    n_features : int
+        Number of features to select.
+    measure : string or callable
+        A standard estimator metric (e.g. 'f1' or 'roc_auc') or a callable with
+        signature measure(estimator, X, y) which should return only a single
+        value.
+    cv : int
+        Number of folds in cross-validation.
+
+    See Also
+    --------
+
+    Examples
+    --------
+    >>> from ITMO_FS.wrappers import BackwardSelection
+    >>> from sklearn.linear_model import LogisticRegression
+    >>> from sklearn.datasets import make_classification
+    >>> import numpy as np
+    >>> dataset = make_classification(n_samples=100, n_features=20,
+    ... n_informative=5, n_redundant=0, shuffle=False, random_state=42)
+    >>> x, y = np.array(dataset[0]), np.array(dataset[1])
+    >>> model = BackwardSelection(LogisticRegression(), 5,
+    ... measure='f1_macro').fit(x, y)
+    >>> model.selected_features_
+    array([ 0,  1,  2,  3, 13], dtype=int64)
     """
-        Backward Selection removes one feature at a time until the number of features to be removed is reached. On each step,
-        the best n-1 features out of n are chosen (according to some estimator metric) and the last one is removed.
+    def __init__(self, estimator, n_features, measure, cv=3):
+        self.estimator = estimator
+        self.n_features = n_features
+        self.measure = measure
+        self.cv = cv
+
+    def _fit(self, X, y):
+        """Fit the wrapper.
 
         Parameters
         ----------
-        estimator : object
-            A supervised learning estimator with a fit method.
-        n_features : int
-            Number of features to be removed.
-        measure : string or callable
-            A standard estimator metric (e.g. 'f1' or 'roc_auc') or a callable object / function with signature 
-            measure(estimator, X, y) which should return only a single value.
-
-        See Also
-        --------
-
-
-        Examples
-        --------
-
+        X : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like, shape (n_samples,)
+            The target values.
+
+        Returns
+        -------
+        None
         """
-
-    def __init__(self, estimator, n_features, measure):
-        # self.__class__ = type(estimator.__class__.__name__, (self.__class__, estimator.__class__), vars(estimator))
-        if not hasattr(estimator, 'fit'):
-            raise TypeError("estimator should be an estimator implementing "
-                            "'fit' method, %r was passed" % estimator)
-        self.__estimator = estimator
-        self.__n_features = n_features
-        self.__measure = measure
-        self.selected_features = None
-
-    def fit(self, train_X, train_y, val_X, val_y):
-        """
-            Fits wrapper.
-
-            Parameters
-            ----------
-            X : array-like, shape (n_samples,n_features)
-                The training input samples.
-            y : array-like, shape (n_samples,)
-                The target values.
-            cv : int
-                Number of folds in cross-validation.
-            Returns
-            ------
-            None
-
-            See Also
-            --------
-
-            Examples
-            --------
-            >>> from ITMO_FS.wrappers import BackwardSelection
-            >>> from sklearn.linear_model import LogisticRegression
-            >>> from sklearn.datasets import make_classification
-            >>> import numpy as np
-            >>> dataset = make_classification(n_samples=100, n_features=20, \
-n_informative=4, n_redundant=0, shuffle=False)
-            >>> data, target = np.array(dataset[0]), np.array(dataset[1])
-            >>> model = BackwardSelection(LogisticRegression(), 15, 'f1_macro')
-            >>> model.fit(data, target)
-            >>> print(model.selected_features)
-            [ 1  2  3 10 15]
-
-        """
-        self.selected_features = generate_features(train_X)
-        target_size = len(self.selected_features) - self.__n_features
-
-        while len(self.selected_features) != target_size:
-            max_measure = 0
-            to_delete = 0
-            for i in range(len(self.selected_features)):
-                iteration_features = np.delete(self.selected_features, i)
-                self.__estimator.fit(train_X[:, iteration_features], train_y)
-                iteration_measure = self.__measure(self.__estimator.predict(val_X[:, iteration_features]), val_y)
-                if iteration_measure > max_measure:
-                    max_measure = iteration_measure
-                    to_delete = i
-            self.selected_features = np.delete(self.selected_features, to_delete)
-
-    def predict(self, X):
-        self.__estimator.predict(X[:, self.selected_features])
+        self.selected_features_ = generate_features(X)
+
+        while self.selected_features_.shape[0] != self.n_features:
+            getLogger(__name__).info(
+                "Current selected set: %s", self.selected_features_)
+            scores = np.vectorize(
+                lambda i: cross_val_score(
+                    self._estimator,
+                    X[:, np.delete(self.selected_features_, i)], y, cv=self.cv,
+                    scoring=self.measure).mean())(
+            np.arange(0, self.selected_features_.shape[0]))
+            getLogger(__name__).info(
+                "Scores for all selected features: %s", scores)
+            to_delete = np.argmax(scores)
+            getLogger(__name__).info(
+                "Deleting feature %d", self.selected_features_[to_delete])
+
+            self.selected_features_ = np.delete(
+                self.selected_features_, to_delete)
+        self.best_score_ = cross_val_score(
+            self._estimator, X[:, self.selected_features_], y, cv=self.cv,
+            scoring=self.measure).mean()
+        self._estimator.fit(X[:, self.selected_features_], y)
diff --git a/ITMO_FS/wrappers/deterministic/RecursiveElimination.py b/ITMO_FS/wrappers/deterministic/RecursiveElimination.py
index 70540f7e..a13f4848 100644
--- a/ITMO_FS/wrappers/deterministic/RecursiveElimination.py
+++ b/ITMO_FS/wrappers/deterministic/RecursiveElimination.py
@@ -1,90 +1,90 @@
-#!/usr/bin/env python
+from logging import getLogger
 
 import numpy as np
-
-from ...utils import generate_features
-
-
-class RecursiveElimination:
+from sklearn.model_selection import cross_val_score
+
+from ...utils import generate_features, BaseWrapper
+
+
+class RecursiveElimination(BaseWrapper):
+    """Recursive feature elimination algorithm.
+
+    Parameters
+    ----------
+    estimator : object
+        A supervised learning estimator that should have a fit(X, y) method, a
+        predict(X) method and a field corresponding to feature weights.
+    n_features : int
+        Number of features to leave.
+    measure : string or callable
+        A standard estimator metric (e.g. 'f1' or 'roc_auc') or a callable with
+        signature measure(estimator, X, y) which should return only a single
+        value.
+    weight_func : callable
+        A function to extract weights from the model.
+    cv : int
+        Number of folds in cross-validation.
+            
+    See Also
+    --------
+    Guyon, I., Weston, J., Barnhill, S., & Vapnik, V., “Gene selection for
+    cancer classification using support vector machines”, Mach. Learn.,
+    46(1-3), 389–422, 2002.
+    https://link.springer.com/article/10.1023/A:1012487302797
+
+    Examples
+    --------
+    >>> from sklearn.datasets import make_classification
+    >>> from ITMO_FS.wrappers import RecursiveElimination
+    >>> from sklearn.svm import SVC
+    >>> import numpy as np
+    >>> dataset = make_classification(n_samples=100, n_features=20,
+    ... n_informative=4, n_redundant=0, shuffle=False, random_state=42)
+    >>> x, y = np.array(dataset[0]), np.array(dataset[1])
+    >>> model = SVC(kernel='linear')
+    >>> rfe = RecursiveElimination(model, 5, measure='f1_macro',
+    ... weight_func=lambda model: np.square(model.coef_).sum(axis=0)).fit(x, y)
+    >>> rfe.selected_features_
+    array([ 0,  1,  2, 11, 19], dtype=int64)
     """
-        Performs a recursive feature elimination until the required number of features is reached.
+    def __init__(self, estimator, n_features, measure, weight_func, cv=3):
+        self.estimator = estimator
+        self.n_features = n_features
+        self.measure = measure
+        self.weight_func = weight_func
+        self.cv = cv
+
+    def _fit(self, X, y):
+        """Fit the wrapper.
 
         Parameters
         ----------
-        estimator : object
-            A supervised learning estimator with a fit method that provides information about feature importance either
-            through a coef_ attribute or through a feature_importances_ attribute.
-        n_features : int
-            Number of features to leave.
-
-        See Also
-        --------
-
-        Examples
-        --------
-
-        """
-
-    def __init__(self, estimator, n_features):
-        if not hasattr(estimator, 'fit'):
-            raise TypeError("estimator should be an estimator implementing "
-                            "'fit' method, %r was passed" % estimator)
-
-        self.__estimator__ = estimator
-        self.__n_features__ = n_features
-        self.__features__ = []
-
-    def fit(self, X, y):
+        X : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like, shape (n_samples,)
+            the target values.
+
+        Returns
+        -------
+        None
         """
-            Fits wrapper.
-
-            Parameters
-            ----------
-            X : array-like, shape (n_samples,n_features)
-                The training input samples.
-            y : array-like, shape (n_samples,)
-                the target values.
-            Returns
-            ------
-            None
-
-            See Also
-            --------
-            Guyon, I., Weston, J., Barnhill, S., & Vapnik, V., “Gene selection for cancer classification using support vector machines”, Mach. Learn., 46(1-3), 389–422, 2002.
-            https://link.springer.com/article/10.1023/A:1012487302797
-
-            Examples
-            --------
-            >>> from sklearn.datasets import make_classification
-            >>> from ITMO_FS.wrappers import RecursiveElimination
-            >>> from sklearn.svm import SVC
-            >>> import numpy as np
-            >>> dataset = make_classification(n_samples=1000, n_features=20)
-            >>> data, target = np.array(dataset[0]), np.array(dataset[1])
-            >>> model = SVC(kernel='linear')
-            >>> rfe = RecursiveElimination(model, 5)
-            >>> rfe.fit(data, target)
-            >>> print("Resulting features: ", rfe.__features__)
-
-        """
-        self.__features__ = generate_features(X)
-
-        while len(self.__features__) != self.__n_features__:
-            self.__estimator__.fit(X[:, self.__features__], y)
-
-            if hasattr(self.__estimator__, 'coef_'):
-                coefs = np.square(self.__estimator__.coef_)
-            elif hasattr(self.__estimator__, 'feature_importances_'):
-                coefs = np.square(self.__estimator__.feature_importances_)
-            else:
-                raise TypeError("estimator should be an estimator with a "
-                                "'coef_' or 'feature_importances_' attribute, %r was passed" % self.__estimator__)
-            if coefs.ndim > 1:
-                coefs = coefs.sum(axis=0)
-
-            least_important = np.argmin(coefs)
-
-            self.__features__ = np.delete(self.__features__, least_important)
-
-    def predict(self, X):
-        self.__estimator__.predict(X[:, self.__features__])
+        self.selected_features_ = generate_features(X)
+
+        while self.selected_features_.shape[0] != self.n_features:
+            getLogger(__name__).info(
+                "Current selected set: %s", self.selected_features_)
+            self._estimator.fit(X[:, self.selected_features_], y)
+            weights = self.weight_func(self._estimator)
+            getLogger(__name__).info(
+                "Weights for all selected features: %s", weights)
+            least_important = np.argmin(weights)
+            getLogger(__name__).info(
+                "Deleting the least important feature %d",
+                self.selected_features_[least_important])
+            self.selected_features_ = np.delete(self.selected_features_,
+                least_important)
+
+        self.best_score_ = cross_val_score(self._estimator,
+            X[:, self.selected_features_], y, cv=self.cv,
+            scoring=self.measure).mean()
+        self._estimator.fit(X[:, self.selected_features_], y)
diff --git a/ITMO_FS/wrappers/deterministic/SequentialForwardSelection.py b/ITMO_FS/wrappers/deterministic/SequentialForwardSelection.py
index 2db8b4a9..e13fce63 100644
--- a/ITMO_FS/wrappers/deterministic/SequentialForwardSelection.py
+++ b/ITMO_FS/wrappers/deterministic/SequentialForwardSelection.py
@@ -1,88 +1,86 @@
-#!/usr/bin/env python
+from logging import getLogger
+
 import numpy as np
 from sklearn.model_selection import cross_val_score
 
-from ...utils import generate_features
-
-
-class SequentialForwardSelection:
+from ...utils import generate_features, BaseWrapper
+
+
+class SequentialForwardSelection(BaseWrapper):
+    """Sequentially add features that maximize the classifying function when
+    combined with the features already used.
+    #TODO add theory about this method
+
+    Parameters
+    ----------
+    estimator: object
+        A supervised learning estimator that should have a fit(X, y) method and
+        a predict(X) method.
+    n_features : int
+        Number of features to select.
+    measure : string or callable
+        A standard estimator metric (e.g. 'f1' or 'roc_auc') or a callable with
+        signature measure(estimator, X, y) which should return only a single
+        value.
+    cv : int
+        Number of folds in cross-validation.
+
+    See Also
+    --------
+
+    Examples
+    --------
+    >>> from ITMO_FS.wrappers import SequentialForwardSelection
+    >>> from sklearn.linear_model import LogisticRegression
+    >>> from sklearn.datasets import make_classification
+    >>> import numpy as np
+    >>> dataset = make_classification(n_samples=100, n_features=20,
+    ... n_informative=5, n_redundant=0, shuffle=False, random_state=42)
+    >>> x, y = np.array(dataset[0]), np.array(dataset[1])
+    >>> model = SequentialForwardSelection(LogisticRegression(), 5,
+    ... measure='f1_macro').fit(x, y)
+    >>> model.selected_features_
+    array([ 1,  4,  3,  5, 19], dtype=int64)
     """
-        Sequentially Adds Features that Maximises the Classifying function when combined with the features already used
-        TODO add theory about this method
+    def __init__(self, estimator, n_features, measure, cv=3):
+        self.estimator = estimator
+        self.n_features = n_features
+        self.measure = measure
+        self.cv = cv
+
+    def _fit(self, X, y):
+        """Fit the wrapper.
 
         Parameters
         ----------
-        estimator: object
-            A supervised learning estimator with a fit method that provides information about feature importance either
-            through a coef_ attribute or through a feature_importances_ attribute.
-        n_features : int
-            Number of features to select.
-        measure : string or callable
-            A standard estimator metric (e.g. 'f1' or 'roc_auc') or a callable object / function with signature
-            measure(estimator, X, y) which should return only a single value.
-
-        See Also
-        --------
-
-        Examples
-        --------
-
-        """
-
-    def __init__(self, estimator, n_features, measure):
-        self.__estimator = estimator
-        self.__n_features = n_features
-        self.__measure = measure
-        self.selected_features = None
-
-    def fit(self, X, y, cv=3):
+        X : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like, shape (n_samples,)
+            The target values.
+
+        Returns
+        -------
+        None
         """
-            Fits wrapper.
-
-            Parameters
-            ----------
-            X : array-like, shape (n_features,n_samples)
-                The training input samples.
-            y : array-like, shape (n_features,n_samples)
-                The target values.
-            cv : int
-                Number of folds in cross-validation.
-            Returns
-            ------
-            None
-
-            See Also
-            --------
-
-            Examples
-            --------
-            >>> from ITMO_FS.wrappers import SequentialForwardSelection
-            >>> from sklearn.linear_model import LogisticRegression
-            >>> from sklearn.datasets import make_classification
-            >>> import numpy as np
-            >>> dataset = make_classification(n_samples=100, n_features=20, n_informative=4, n_redundant=0, shuffle=False)
-            >>> data, target = np.array(dataset[0]), np.array(dataset[1])
-            >>> model = SequentialForwardSelection(LogisticRegression(), 5, 'f1_macro')
-            >>> model.fit(data, target)
-            >>> print(model.selected_features)
-
-
-        """
-        self.selected_features = np.array([], dtype=int)
-        features_left = generate_features(X)
-
-        while len(self.selected_features) != self.__n_features:
-            max_measure = 0
-            to_add = 0
-            for i in range(len(features_left)):
-                iteration_features = np.append(self.selected_features, features_left[i])
-                iteration_measure = cross_val_score(self.__estimator, X[:, iteration_features], y, cv=cv,
-                                                    scoring=self.__measure).mean()
-                if iteration_measure > max_measure:
-                    max_measure = iteration_measure
-                    to_add = i
-            self.selected_features = np.append(self.selected_features, features_left[to_add])
-            features_left = np.delete(features_left, to_add)
-
-    def predict(self, X):
-        self.__estimator.predict(X[:, self.selected_features])
+        self.selected_features_ = np.array([], dtype=int)
+        free_features = generate_features(X)
+
+        while self.selected_features_.shape[0] != self.n_features:
+            getLogger(__name__).info(
+                "Current selected set: %s", self.selected_features_)
+            scores = np.vectorize(
+                lambda f: cross_val_score(
+                    self._estimator,
+                    X[:, np.append(self.selected_features_, f)], y, cv=self.cv,
+                    scoring=self.measure).mean())(free_features)
+            getLogger(__name__).info("Scores for all free features: %s", scores)
+            to_add = np.argmax(scores)
+            getLogger(__name__).info("Adding feature %d", free_features[to_add])
+            self.selected_features_ = np.append(self.selected_features_,
+                free_features[to_add])
+            free_features = np.delete(free_features, to_add)
+
+        self.best_score_ = cross_val_score(self._estimator,
+            X[:, self.selected_features_], y, cv=self.cv,
+            scoring=self.measure).mean()
+        self._estimator.fit(X[:, self.selected_features_], y)
diff --git a/ITMO_FS/wrappers/deterministic/__init__.py b/ITMO_FS/wrappers/deterministic/__init__.py
index df26b7f3..f8d29c48 100644
--- a/ITMO_FS/wrappers/deterministic/__init__.py
+++ b/ITMO_FS/wrappers/deterministic/__init__.py
@@ -2,4 +2,4 @@
 from .BackwardSelection import BackwardSelection
 from .RecursiveElimination import RecursiveElimination
 from .SequentialForwardSelection import SequentialForwardSelection
-from .qpfs_wrapper import qpfs_wrapper
+from .qpfs_wrapper import QPFSWrapper
diff --git a/ITMO_FS/wrappers/deterministic/qpfs_wrapper.py b/ITMO_FS/wrappers/deterministic/qpfs_wrapper.py
index 303603cf..5b332e03 100644
--- a/ITMO_FS/wrappers/deterministic/qpfs_wrapper.py
+++ b/ITMO_FS/wrappers/deterministic/qpfs_wrapper.py
@@ -1,8 +1,8 @@
 from ITMO_FS.filters.univariate.measures import pearson_corr
 from ITMO_FS.utils.qpfs_body import qpfs_body
+from ...utils import BaseWrapper
 
-
-def qpfs_wrapper(X, y, alpha, r=None, sigma=None, solv='quadprog', fn=pearson_corr):
+class QPFSWrapper(BaseWrapper):
     """
     #TODO rewrite to the proper notation
     Performs Quadratic Programming Feature Selection algorithm.
@@ -12,20 +12,16 @@ def qpfs_wrapper(X, y, alpha, r=None, sigma=None, solv='quadprog', fn=pearson_co
 
     Parameters
     ----------
-    X : array-like, shape (n_samples,n_features)
-        The input samples.
-    y : array-like, shape (n_samples)
-        The classes for the samples.
     alpha : double value
         That represents balance between relevance and redundancy of features.
-    r : int, optional
+    r : int
         The number of samples to be used in Nystrom optimization.
-    sigma : double, optional
+    sigma : double
         The threshold for eigenvalues to be used in solving QP optimization.
-    solv : string, optional
+    solv : string
         The name of qp solver according to qpsolvers(https://pypi.org/project/qpsolvers/) naming.
         Note quadprog is used by default.
-    fn : function(array, array), optional
+    fn : function(array, array)
         The function to count correlation, for example pearson correlation or  mutual information.
         Note pearson_corr from ITMO_FS measures is used by default.
     Returns
@@ -46,4 +42,25 @@ def qpfs_wrapper(X, y, alpha, r=None, sigma=None, solv='quadprog', fn=pearson_co
     >>> print(ranks)
 
     """
-    return qpfs_body(X, y, fn, alpha=alpha, r=r, sigma=sigma, solv=solv)
+    def __init__(self, alpha, r=None, sigma=None, solv='quadprog', fn=pearson_corr):
+        self.alpha = alpha
+        self.r = r
+        self.sigma = sigma
+        self.solv = solv
+        self.fn = fn
+
+    def _fit(X, y):
+        """
+            Fits wrapper.
+
+            Parameters
+            ----------
+            X : array-like, shape (n_samples,n_features)
+                The training input samples.
+            y : array-like, shape (n_samples,)
+                The target values.
+            Returns
+            ------
+            None
+        """        
+        return qpfs_body(X, y, fn, alpha=alpha, r=r, sigma=sigma, solv=solv)
diff --git a/ITMO_FS/wrappers/randomized/HillClimbing.py b/ITMO_FS/wrappers/randomized/HillClimbing.py
index b2bcddf1..0644da82 100644
--- a/ITMO_FS/wrappers/randomized/HillClimbing.py
+++ b/ITMO_FS/wrappers/randomized/HillClimbing.py
@@ -1,29 +1,92 @@
-import random
+from logging import getLogger
 
-from sklearn.model_selection import cross_validate
+import numpy as np
+from sklearn.base import clone
+from sklearn.model_selection import cross_val_score
 
+from ...utils import generate_features, BaseWrapper
 
-class HillClimbingWrapper:
-    def __init__(self, estimator, scorer):
-        self._scorer = scorer
-        self._estimator = estimator
-        self.features = None
+class HillClimbingWrapper(BaseWrapper):
+    """Hill Climbing algorithm.
 
-    def fit(self, x, y, feature_names=None, cv=3):
-        try:
-            feature_names = x.columns
-        except AttributeError:
-            if feature_names is None:
-                feature_names = list(range(x.shape[1]))
+    Parameters
+    ----------
+    estimator : object
+        A supervised learning estimator that should have a fit(X, y) method and
+        a predict(X) method.
+    measure : string or callable
+        A standard estimator metric (e.g. 'f1' or 'roc_auc') or a callable with
+        signature measure(estimator, X, y) which should return only a single
+        value.
+    seed : int
+        Random seed used to initialize np.random.default_rng().
+    cv : int
+        Number of folds in cross-validation.
 
-        features = [random.choice(feature_names)]
-        score = cross_validate(self._estimator, x[:, features], y, cv=cv, scoring=self._scorer)
-        old_score = 0
-        while score > old_score:
-            features += [random.choice(set(feature_names) - set(features))]
+    See Also
+    --------
+
+    Examples
+    --------
+    >>> from ITMO_FS.wrappers import HillClimbingWrapper
+    >>> from sklearn.linear_model import LogisticRegression
+    >>> from sklearn.datasets import make_classification
+    >>> import numpy as np
+    >>> dataset = make_classification(n_samples=100, n_features=20,
+    ... n_informative=5, n_redundant=0, shuffle=False, random_state=42)
+    >>> x, y = np.array(dataset[0]), np.array(dataset[1])
+    >>> model = HillClimbingWrapper(LogisticRegression(),
+    ... measure='f1_macro').fit(x, y)
+    >>> model.selected_features_
+    array([ 0,  1,  2,  3,  4,  6,  7,  9, 11, 13, 14, 15], dtype=int64)
+    """
+    def __init__(self, estimator, measure, seed=42, cv=3):
+        self.estimator = estimator
+        self.measure = measure
+        self.seed = seed
+        self.cv = cv
+
+    def _fit(self, X, y):
+        """Fit the wrapper.
+
+        Parameters
+        ----------
+        X : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like, shape (n_samples,)
+            The target values.
+
+        Returns
+        -------
+        None
+        """
+        rng = np.random.default_rng(self.seed)
+
+        features = generate_features(X)
+        mask = rng.choice([True, False], self.n_features_)
+        getLogger(__name__).info("Initial feature mask: %s", mask)
+        score = cross_val_score(
+            self._estimator, X[:, features[mask]], y, cv=self.cv,
+            scoring=self.measure).mean()
+
+        while True:
+            getLogger(__name__).info("Current best score: %d", score)
             old_score = score
-            score = cross_validate(self._estimator, x[:, features], y, cv=cv, scoring=self._scorer)
-        self.features = features
+            order = rng.permutation(self.n_features_)
+            for feature in order:
+                getLogger(__name__).info("Trying to change feature %d", feature)
+                mask[feature] = not(mask[feature])
+                new_score = cross_val_score(self._estimator,
+                    X[:, features[mask]], y, cv=self.cv,
+                    scoring=self.measure).mean()
+                getLogger(__name__).info("New score: %d", new_score)
+                if new_score > score:
+                    score = new_score
+                    break
+                mask[feature] = not(mask[feature])
+            if old_score == score:
+                break
 
-    def predict(self, X):
-        return self._estimator.predict(X[:, self.features])
+        self.selected_features_ = features[mask]
+        self.best_score_ = score
+        self._estimator.fit(X[:, self.selected_features_], y)
diff --git a/ITMO_FS/wrappers/randomized/SimulatedAnnealing.py b/ITMO_FS/wrappers/randomized/SimulatedAnnealing.py
index c7abcb03..243a0c83 100644
--- a/ITMO_FS/wrappers/randomized/SimulatedAnnealing.py
+++ b/ITMO_FS/wrappers/randomized/SimulatedAnnealing.py
@@ -1,131 +1,149 @@
-import math
-import random
+from logging import getLogger
 
 import numpy as np
+from sklearn.model_selection import cross_val_score
 
+from ...utils import BaseWrapper, generate_features
 
-class SimulatedAnnealing(object):
-    """
-        Performs feature selection using simulated annealing
-
-        Parameters
-        ----------
-        seed : integer
-            Random seed used to initialize np.random.seed()
-        iteration_number : integer
-            number of iterations of algorithm
-        classifier : Classifier instance
-            ``Classifier`` used for training and testing on provided datasets.
 
-            - Note that algorithm implementation assumes that classifier has fit, predict methods. Default algorithm \
-            uses ``sklearn.neighbors.KNeighborsClassifier``
+class SimulatedAnnealing(BaseWrapper):
+    """Simulated Annealing algorithm.
 
-        c : integer
-            constant c is used t o control the rate of feature perturbation
-        init_number_of_features : float
-            number of features to initialize start features subset,
-            Note: by default (5-10) percents of number of features is used
+    Parameters
+    ----------
+    estimator : object
+        A supervised learning estimator that should have a fit(X, y) method and
+        a predict(X) method.
+    measure : string or callable
+        A standard estimator metric (e.g. 'f1' or 'roc_auc') or a callable with
+        signature measure(estimator, X, y) which should return only a single
+        value.
+    seed : int
+        Random seed used to initialize np.random.default_rng().
+    iteration_number : int
+        Number of iterations of the algorithm.
+    c : int
+        A constant that is used to control the rate of feature perturbation.
+    init_number_of_features : int
+        The number of features to initialize start features subset with, by
+        default 5-10 percents of features is used.
+    cv : int
+        Number of folds in cross-validation.
         
-        Notes
-        -----
-        For more details see `this paper <http://www.feat.engineering/simulated-annealing.html/>`_.
-
-        Examples
-        --------
-        >>> from sklearn.datasets import make_classification
-        >>> from sklearn.model_selection import KFold
-        >>> from ITMO_FS.wrappers.randomized import SimulatedAnnealing
-        >>> x, y = make_classification(1000, 100, n_informative = 10, n_redundant = 30, n_repeated = 10, shuffle = False)
-        >>> kf = KFold(n_splits=2)
-        >>> sa = SimulatedAnnealing()
-        >>> for train_index, test_index in kf.split(x):
-        ...    sa.fit(x[train_index], y[train_index], x[test_index], y[test_index])
-        ...    print(sa.selected_features)
+    Notes
+    -----
+    For more details see `this paper <http://www.feat.engineering/simulated-annealing.html/>`_.
 
-        
+    Examples
+    --------
+    >>> from sklearn.datasets import make_classification
+    >>> from sklearn.linear_model import LogisticRegression
+    >>> from ITMO_FS.wrappers.randomized import SimulatedAnnealing
+    >>> dataset = make_classification(n_samples=100, n_features=20,
+    ... n_informative=5, n_redundant=0, shuffle=False, random_state=42)
+    >>> x, y = np.array(dataset[0]), np.array(dataset[1])
+    >>> sa = SimulatedAnnealing(LogisticRegression(), measure='f1_macro',
+    ... iteration_number=50).fit(x, y)
+    >>> sa.selected_features_
+    array([ 1,  4,  3, 17, 10, 16, 11, 14,  5], dtype=int64)
     """
-
-    def __init__(self, classifier, score, seed=1, iteration_number=100, c=1, init_number_of_features=None):
+    def __init__(self, estimator, measure, seed=42, iteration_number=100, c=1,
+                 init_number_of_features=None, cv=3):
+        self.estimator = estimator
+        self.measure = measure
         self.seed = seed
         self.iteration_number = iteration_number
-        self.classifier = classifier
-        self.score = score
         self.c = c
         self.init_number_of_features = init_number_of_features
+        self.cv = cv
 
     def __acceptance(self, i, prev_score, cur_score):
-        return math.exp(-i / self.c * (prev_score - cur_score) / prev_score)
+        return np.exp((i + 1) / self.c * (cur_score - prev_score) / prev_score)
+
+    def _fit(self, X, y):
+        """Fit the wrapper.
 
-    def fit(self, train_x, train_y, test_x, test_y):
-        """
-        Runs the Simulated Annealing algorithm on the specified dataset and fits the classifier.
-        
         Parameters
         ----------
-        train_x : array-like, shape (n_samples, n_features)
-            The input training samples.
-        train_y : array-like, shape (n_samples)
-            The classes for training samples.
-        test_x : array-like, shape (n_samples, n_features)
-            The input testing samples.
-        test_y : array-like, shape (n_samples)
-            The classes for testing samples.
+        X : array-like, shape (n_samples, n_features)
+            The training input samples.
+        y : array-like, shape (n_samples,)
+            the target values.
 
-        Return
-        ------
+        Returns
+        -------
         None
         """
-        np.random.seed(self.seed)
-        random.seed(self.seed)
-        feature_number = train_x.shape[1]
+        rng = np.random.default_rng(self.seed)
+        features = generate_features(X)
+
         if self.init_number_of_features is None:
-            percentage = random.randint(5, 11)
-            self.init_number_of_features = int(feature_number * percentage / 100)
-        feature_subset = np.unique((np.random.randint(0, feature_number, self.init_number_of_features)))
-        prev_score = self.__get_score(train_x, train_y, test_x, test_y, feature_subset)
+            percentage = rng.integers(5, 11)
+            init_number_of_features = int(
+                self.n_features_ * percentage / 100) + 1
+        elif self.init_number_of_features == 0:
+            getLogger(__name__).warning(
+                "Initial number of features was set to zero; would use one "
+                "instead")
+            init_number_of_features = 1
+        else:
+            init_number_of_features = self.init_number_of_features
+
+        feature_subset = np.unique(
+            rng.integers(0, self.n_features_, init_number_of_features))
+        getLogger(__name__).info("Initial selected set: %s", feature_subset)
+        prev_score = cross_val_score(
+            self._estimator, X[:, feature_subset], y, cv=self.cv,
+            scoring=self.measure).mean()
+        getLogger(__name__).info("Initial score: %d", prev_score)
+
         for i in range(self.iteration_number):
-            operation = random.randint(0, 1)
-            percentage = random.randint(1, 5)
-            if operation == 1:
+            getLogger(__name__).info("Current best score: %d", prev_score)
+            operation = rng.integers(0, 2)
+            percentage = rng.integers(1, 5)
+            if operation == 1 & feature_subset.shape[0] != self.n_features_:
                 # inc
-                include_number = int(feature_number * (percentage / 100))
-                not_included_features = np.array([f for f in np.arange(0, feature_number) if f not in feature_subset])
-                cur_subset = np.append(feature_subset,
-                                       np.random.choice(not_included_features, size=include_number, replace=False))
+                not_included_features = np.setdiff1d(features, feature_subset)
+                include_number = min(
+                    not_included_features.shape[0],
+                    int(self.n_features_ * (percentage / 100)) + 1)
+                to_add = rng.choice(
+                    not_included_features, size=include_number, replace=False)
+                getLogger(__name__).info(
+                    "Trying to add features %s into the selected set", to_add)
+                cur_subset = np.append(feature_subset, to_add)
             else:
                 # exc
-                exclude_number = int(feature_number * (percentage / 100))
-                cur_subset = np.delete(feature_subset,
-                                       np.random.choice(feature_subset, size=exclude_number, replace=False))
-            cur_score = self.__get_score(train_x, train_y, test_x, test_y, feature_subset)
+                exclude_number = min(
+                    feature_subset.shape[0] - 1,
+                    int(self.n_features_ * (percentage / 100)) + 1)
+                to_delete = rng.choice(
+                    np.arange(feature_subset.shape[0]), size=exclude_number,
+                    replace=False)
+                getLogger(__name__).info(
+                    "Trying to delete features %s from the selected set",
+                    feature_subset[to_delete])
+                cur_subset = np.delete(feature_subset, to_delete)
+            cur_score = cross_val_score(
+                self._estimator, X[:, cur_subset], y, cv=self.cv,
+                scoring=self.measure).mean()
+            getLogger(__name__).info("New score: %d", cur_score)
             if cur_score > prev_score:
                 feature_subset = cur_subset
                 prev_score = cur_score
             else:
-                ruv = random.random()
-                if ruv > self.__acceptance(i, prev_score, cur_score):
+                getLogger(__name__).info(
+                    "Score has not improved; trying to accept the new subset "
+                    "anyway")
+                ruv = rng.random()
+                acceptance = self.__acceptance(i, prev_score, cur_score)
+                getLogger(__name__).info(
+                    "Random value = %d, acceptance = %d", ruv, acceptance)
+                if ruv < acceptance:
+                    getLogger(__name__).info("Accepting the new subset")
                     feature_subset = cur_subset
                     prev_score = cur_score
-        self.selected_features = feature_subset
-
-    def __get_score(self, train_x, train_y, test_x, test_y, subset):
-        self.classifier.fit(train_x[:, subset], train_y)
-        pred_labels = self.classifier.predict(test_x[:, subset])
-        score = self.score(pred_labels, test_y)
-        return score
-
-    def predict(self, test_x):
-        """
-        Predicts labels on test dataset
 
-        Parameters
-        ----------
-        test_x : array-like, shape (n_samples, n_features)
-            The input testing samples.
-        
-        Return
-        ------
-        array-like, shape (n_samples,n_selected_features) : array of feature numbers
-        
-        """
-        return self.classifier.predict(test_x[:, self.selected_features])
+        self.selected_features_ = feature_subset
+        self.best_score_ = prev_score
+        self._estimator.fit(X[:, self.selected_features_], y)
diff --git a/ITMO_FS/wrappers/randomized/TPhMGWO.py b/ITMO_FS/wrappers/randomized/TPhMGWO.py
index 58f2e639..8e0bacf8 100644
--- a/ITMO_FS/wrappers/randomized/TPhMGWO.py
+++ b/ITMO_FS/wrappers/randomized/TPhMGWO.py
@@ -1,180 +1,210 @@
-import math
-import random
+from logging import getLogger
 
 import numpy as np
-from sklearn.metrics import mean_squared_error
-from sklearn.model_selection import KFold
-from sklearn.neighbors import KNeighborsClassifier
-
-
-class TPhMGWO(object):
-    """
-        Performs Grey Wolf optimization with Two-Phase Mutation
-
-        Parameters
-        ----------
-        wolfNumber : integer
-            Number of search agents used to find solution for features selection problem
-        seed : integer
-            Random seed used to initialize np.random.seed()
-        alpha : float
-            weight of importance of classification accuracy
-            Note alpha is used in equation that counts fitness as fitness = alpha * score + beta * |selected_features| / |features| where alpha = 1 - beta
-        classifier : classifier used for training and testing on provided dataset
-            Note that algorithm implementation assumes that classifier has fit, predict methods
-            Default algorithm uses sklearn.neighbors.KNeighborsClassifier
-        foldNumber : integer
-            fold number to train and test classifier
-        iteration_number : integer
-            number of iterations of algorithm
-        Mp : float
-            probability of mutation
-
-        Notes
-        -----
-        For more details see `this paper <https://www.sciencedirect.com/science/article/pii/S0957417419305263/>`_.
-
-
-        Examples
-        --------
-        >>> import numpy as np
-        >>> from ITMO_FS.wrappers.randomized import TPhMGWO
-        >>> from sklearn.datasets import make_classification
-        >>> tphmgwo = TPhMGWO()
-        >>> x, y = make_classification(500, 50, n_informative = 10, \
-n_redundant = 30, n_repeated = 10, shuffle = True)
-        >>> result = tphmgwo.run(x, y)
-        >>> print(np.where(result == 1))
-
-
+from sklearn.model_selection import cross_val_score
+
+from ...utils import BaseWrapper, generate_features
+
+
+class TPhMGWO(BaseWrapper):
+    """Grey Wolf optimization with Two-Phase Mutation.
+
+    Parameters
+    ----------
+    estimator : object
+        A supervised learning estimator that should have a fit(X, y) method and
+        a predict(X) method. The original paper suggests to use the
+        k-nearest-neighbors classifier.
+    measure : string or callable
+        A standard estimator metric (e.g. 'f1' or 'roc_auc') or a callable with
+        signature measure(estimator, X, y) which should return only a single
+        value.
+    wolf_number : int
+        Number of search agents used to find a solution for feature selection
+        problem.
+    seed : int
+        Random seed used to initialize np.random.default_rng().
+    alpha : float
+        Weight of importance of classification accuracy. Alpha is used in
+        equation that counts fitness as
+        fitness = alpha * score + beta * |selected_features| / |features|
+        where alpha = 1 - beta.
+    cv : int
+        Number of folds in cross-validation.
+    iteration_number : int
+        Number of iterations of the algorithm.
+    mp : float
+        Mutation probability.
+    binarize : str
+        Transformation function to use. Currently only 'tanh' and 'sigmoid'
+        are supported.
+
+    Notes
+    -----
+    For more details see `this paper <https://www.sciencedirect.com/science/article/pii/S0957417419305263/>`_.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from sklearn.neighbors import KNeighborsClassifier
+    >>> from ITMO_FS.wrappers.randomized import TPhMGWO
+    >>> from sklearn.datasets import make_classification
+    >>> dataset = make_classification(n_samples=100, n_features=20,
+    ... n_informative=5, n_redundant=0, shuffle=False, random_state=42)
+    >>> x, y = np.array(dataset[0]), np.array(dataset[1])
+    >>> tphmgwo = TPhMGWO(KNeighborsClassifier(n_neighbors=7),
+    ... measure='accuracy').fit(x, y)
+    >>> tphmgwo.selected_features_
+    array([0, 1, 2, 4], dtype=int64)
     """
-
-    def __init__(self, wolfNumber=10, seed=1, alpha=0.01, classifier=KNeighborsClassifier(n_neighbors=10), foldNumber=5,
-                 iteration_number=30, Mp=0.5, errorRate=mean_squared_error):
-        self.wolfNumber = wolfNumber
-        self._seed = seed
+    def __init__(self, estimator, measure, wolf_number=10, seed=1, alpha=0.5,
+                 cv=5, iteration_number=30, mp=0.5, binarize='sigmoid'):
+        self.estimator = estimator
+        self.measure = measure
+        self.wolf_number = wolf_number
+        self.seed = seed
         self.alpha = alpha
-        self.beta = 1 - alpha
-        self.classifier = classifier
-        self.foldNumber = foldNumber
+        self.estimator = estimator
+        self.cv = cv
         self.iteration_number = iteration_number
-        self.errorRate = errorRate
-        self._Mp = Mp
-        self.solution = None
-
-    class ClassifierMethodsException(Exception):
-        pass
+        self.mp = mp
+        self.binarize = binarize
 
     def __sigmoid(self, x):
-        rand = random.random()
-        xs = np.where(1 / (1 - math.exp(-x)) <= rand, 1, 0)
+        rand = self._rng.random(self.n_features_)
+        xs = np.where(1 / (1 + np.exp(-x)) <= rand, 1, 0)
         return xs
 
     def __tanh(self, x):
-        rand = random.random()
+        rand = self._rng.random(self.n_features_)
         xs = np.where(abs(np.tanh(x)) <= rand, 1, 0)
         return xs
 
-    def __genRandValues(self, t, featureNumber):
+    def __genRandValues(self, t):
         a = 2 - t * 2.0 / self.iteration_number
-        rand1 = np.random.uniform(low=0.0, high=1.0, size=featureNumber)
-        rand2 = np.random.uniform(low=0.0, high=1.0, size=featureNumber)
+        rand1 = self._rng.random((3, self.n_features_))
+        rand2 = self._rng.random((3, self.n_features_))
         A = abs(2 * a * rand1 - a)
-        C = 2 * rand2
-        return a, A, C
-
-    def __calcFitness(self, wolves, X,
-                      y):  # Calc fitness function. Note that it returns an array and takes an array of search agents
-        featureNumber = X.shape[1]
-        fitnessFunc = np.zeros(self.wolfNumber, dtype=np.float)
-        for index in range(wolves.shape[0]):
-            wolf = wolves[index]
-            XFilt = X[:, np.where(wolf == 1)[0]]
-            kf = KFold(self.foldNumber, shuffle=True)
-            score = 0.0
-            for train_indices, test_indices in kf.split(X):
-                try:
-                    self.classifier.fit(XFilt[train_indices], y[train_indices])
-                except AttributeError:
-                    raise self.ClassifierMethodsException(
-                        "The classifier " + type(self.classifier).__name__ + " doesn't have expected method fit")
-                try:
-                    score += self.errorRate(y[test_indices], self.classifier.predict(XFilt[test_indices]),
-                                             squared=False)
-                except AttributeError:
-                    raise self.ClassifierMethodsException(
-                        "The classifier " + type(self.classifier).__name__ + " doesn't have expected method predict")
-            score /= self.foldNumber
-            fitnessFunc[index] = self.alpha * score + self.beta * np.count_nonzero(wolf) / featureNumber
-        return fitnessFunc
-
-    def run(self, X, y):
-        """
-        
-        Runs the TPhGWO algorithm on the specified dataset.
-        
+        C = (2 * rand2)
+        return A, C
+
+    def _fit(self, X, y):
+        """Run the TPhMGWO algorithm on the specified dataset.
+            
         Parameters
         ----------
-        X : array-like, shape (n_samples,n_features)
+        X : array-like, shape (n_samples, n_features)
             The input samples.
-        y : array-like, shape (n_samples)
+        y : array-like, shape (n_samples,)
             The classes for the samples.
 
         Returns
-        ------
-        array-like, shape (n_samples,n_selected_features) : 0-1 array where 1 means feature is selected and 0 not
-
+        -------
+        None
         """
+        def __calcScore(wolf):
+            score = 1 - cross_val_score(
+                self._estimator, X[:, np.where(wolf, True, False)], y,
+                cv=self.cv, scoring=self.measure).mean()
+            return (self.alpha * score
+                    + self._beta * np.count_nonzero(wolf) / self.n_features_)
+
+        def __updateWolf(wolf):
+            D = abs(C * best3Wolves - wolf)
+            return binarize(np.mean(best3Wolves - A * D, axis=0))
+
+        if self.binarize == 'tanh':
+            binarize = self.__tanh
+        elif self.binarize == 'sigmoid':
+            binarize = self.__sigmoid
+        else:
+            getLogger(__name__).error(
+                "binarize should be 'tanh' or 'sigmoid', %s was passed",
+                self.binarize)
+            raise ValueError(
+                "binarize should be 'tanh' or 'sigmoid', %s was passed"
+                % self.binarize)
+
+        self._beta = 1 - self.alpha
+        self._rng = np.random.default_rng(self.seed)
+        features = generate_features(X)
+
+        wolves = self._rng.choice([0, 1], (self.wolf_number, self.n_features_))
+        for wolf in wolves:
+            if wolf.sum() == 0:
+                wolf[0] = 1
+        getLogger(__name__).info("Initial wolves population: %s", wolves)
+
+        A, C = self.__genRandValues(0)
+        getLogger(__name__).info("Initial values: A = %s, C = %s", A, C)
+        classNumber = np.unique(y).shape[0]
+
+        scores = np.vectorize(
+            lambda wolf: __calcScore(wolf), signature='(1)->()')(wolves)
+        getLogger(__name__).info("Scores for all wolves: %s", scores)
+
+        alphaIndex, betaIndex, deltaIndex = np.argsort(scores)[:3]
+        best3Wolves = wolves[[alphaIndex, betaIndex, deltaIndex]]
 
-        featureNumber = X.shape[1]
-        np.random.seed(self._seed)
-        wolves = []
-        for i in range(self.wolfNumber):
-            wolves.append(np.random.randint(0, 2, featureNumber, dtype = np.integer))
-        print(wolves)
-        wolves = np.array(wolves)
-        wolves = wolves.astype(dtype=np.double)
-        a, A, C = self.__genRandValues(0, featureNumber)
-        classNumber = np.unique(y).size
-        fitnessFunc = self.__calcFitness(wolves, X, y)
-        deltaIndex, betaIndex, alphaIndex = np.argpartition(fitnessFunc, -3)[-3:]
-        best3Wolves = [wolves[deltaIndex], wolves[betaIndex], wolves[alphaIndex]]
         for t in range(self.iteration_number):
-            for index in range(len(wolves)):
-                Darray = abs(C * best3Wolves - wolves[index])
-                wolfAprox = best3Wolves - A * Darray
-                wolfNewPos = np.mean(wolfAprox, axis=0)
-                wolves[index] = wolfNewPos
-            wolves = self.__tanh(wolves)
-            a, A, C = self.__genRandValues(t, featureNumber)
-            fitnessFunc = self.__calcFitness(wolves, X, y)
-            deltaIndex, betaIndex, alphaIndex = np.argpartition(fitnessFunc, -3)[-3:]
-            best3Wolves = [np.copy(wolves[deltaIndex]), np.copy(wolves[betaIndex]), np.copy(wolves[alphaIndex])]
+            wolves = np.vectorize(
+                lambda wolf: __updateWolf(wolf), signature='(1)->(1)')(wolves)
+            for wolf in wolves:
+                if wolf.sum() == 0:
+                    wolf[0] = 1
+            getLogger(__name__).info(
+                "Wolves population after iteration %d: %s", t, wolves)
+
+            A, C = self.__genRandValues(t)
+            getLogger(__name__).info("A = %s, C = %s", A, C)
+            scores = np.vectorize(
+                lambda wolf: __calcScore(wolf), signature='(1)->()')(wolves)
+            getLogger(__name__).info("Scores for all wolves: %s", scores)
+            alphaIndex, betaIndex, deltaIndex = np.argsort(scores)[:3]
+            best3Wolves = wolves[[alphaIndex, betaIndex, deltaIndex]]
+
             alphaWolf = np.copy(wolves[alphaIndex])
-            fitnessFuncAlpha = self.__calcFitness(np.array([alphaWolf]), X, y)[0]
-            one_positions = np.where(alphaWolf == 1)[0]
-            mutatedAlphaOne = np.copy(alphaWolf)
-            for one_pos in one_positions:
-                r = random.random()
-                if r < self._Mp:
-                    mutatedAlphaOne[one_pos] = 0
-                    mutatedFitnessAlpha = self.__calcFitness(np.array([mutatedAlphaOne]), X, y)[0]
-                    if (mutatedFitnessAlpha < fitnessFuncAlpha):
-                        fitnessFuncAlpha = mutatedFitnessAlpha
-                        alphaWolf = mutatedAlphaOne
-            # mutatedAlphaOne = np.copy(wolves[alphaIndex])
-            zero_positions = np.where(alphaWolf == 0)[0]
-            mutatedAlphaZero = np.copy(alphaWolf)
-            for zero_pos in zero_positions:
-                r = random.random()
-                if r < self._Mp:
-                    mutatedAlphaZero[zero_pos] = 1
-                    mutatedFitnessAlpha = self.__calcFitness(np.array([mutatedAlphaZero]), X, y)[0]
-                    if (mutatedFitnessAlpha < fitnessFuncAlpha):
-                        fitnessFuncAlpha = mutatedFitnessAlpha
-                        alphaWolf = mutatedAlphaZero
-            # mutatedAlphaZero = np.copy(wolves[alphaIndex])
+            alphaScore = scores[alphaIndex]
+            mutated = np.copy(alphaWolf)
+
+            getLogger(__name__).info("Base alpha wolf score: %d", alphaScore)
+            for selected in self._rng.permuted(np.flatnonzero(alphaWolf)):
+                getLogger(__name__).info(
+                    "Trying to remove feature %d from the alpha wolf", selected)
+                r = self._rng.random()
+                if r < self.mp:
+                    mutated[selected] = 0
+                    if mutated.sum() == 0:
+                        break
+                    mutatedScore = __calcScore(mutated)
+                    getLogger(__name__).info("New score: %d", mutatedScore)
+                    if (mutatedScore < alphaScore):
+                        alphaScore = mutatedScore
+                        alphaWolf = mutated
+                    else:
+                        mutated[selected] = 1
+
+            mutated = np.copy(alphaWolf)
+            for free in self._rng.permuted(np.flatnonzero(alphaWolf - 1)):
+                getLogger(__name__).info(
+                    "Trying to add feature %d to the alpha wolf", free)
+                r = self._rng.random()
+                if r < self.mp:
+                    mutated[free] = 1
+                    mutatedScore = __calcScore(mutated)
+                    getLogger(__name__).info("New score: %d", mutatedScore)
+                    if (mutatedScore < alphaScore):
+                        alphaScore = mutatedScore
+                        alphaWolf = mutated
+                    else:
+                        mutated[free] = 0
+
             wolves[alphaIndex] = alphaWolf
-        self.vector_representation = alphaWolf
-        self.solution = np.where(self.vector_representation == 1)[0]
-        return self.solution
+
+        if alphaWolf.sum() == 0:
+            alphaWolf[0] = 1
+        self.selected_features_ = np.flatnonzero(alphaWolf)
+        self.best_score_ = cross_val_score(
+            self._estimator, X[:, self.selected_features_], y, cv=self.cv,
+            scoring=self.measure).mean()
+        self._estimator.fit(X[:, self.selected_features_], y)
diff --git a/ITMO_FS/wrappers/wrapper_utils.py b/ITMO_FS/wrappers/wrapper_utils.py
deleted file mode 100644
index 86ac0fce..00000000
--- a/ITMO_FS/wrappers/wrapper_utils.py
+++ /dev/null
@@ -1,79 +0,0 @@
-#!/usr/bin/env python
-
-from numpy import mean, vstack
-from numpy.random import shuffle
-
-
-def get_current_accuracy(__estimator__, X, current_features, test_x, test_y):
-    """
-    Checks the Accuracy of the Current Features
-        Parameters
-        ----------
-        X : array-like, shape (n_features,n_samples)
-            The training input samples.
-        current_features : array-like, shape (n_features,n_samples)
-            Current Set of Features
-        test_x : array-like, shape (n_features, n_samples)
-            Testing Set
-        test_y : array-like , shape(n_samples)
-            Labels
-        Returns
-        ------
-        float, Accuracy of the Current Features
-        :param X:
-        :param __estimator__:
-
-    """
-
-    correct = 0
-    for i in range(test_x.length):
-        predict = __estimator__.predict(
-            [test_x[j] for j in current_features])  # TODO: rewrite to correct matrix of shape (n_samples,n_features)
-        if predict == test_y[i]:
-            correct += 1
-    current_accuracy = correct / test_x.length
-    return current_accuracy
-
-
-def get_current_cv_accuracy(__estimator__, X, y, current_features, cv=3):
-    """
-    Checks the Accuracy of the Current Features
-        Parameters
-        ----------
-        X : array-like, shape (n_features,n_samples)
-            The training input samples.
-        current_features : array-like, shape (n_features,n_samples)
-            Current Set of Features
-        test_x : array-like, shape (n_features, n_samples)
-            Testing Set
-        test_y : array-like , shape(n_samples)
-            Labels
-        Returns
-        ------
-        float, Accuracy of the Current Features
-
-    """
-
-    accuracies = []
-    for data, target in cross_validate(X, y, k=cv):
-        predict = __estimator__.predict(data[:, current_features]).reshape(
-            target.shape)  # TODO: rewrite to correct matrix of shape (n_samples,n_features) or change docs
-        correct = sum(predict == target)
-        accuracies.append(correct / len(data))
-
-    return mean(accuracies)
-
-
-def cross_validate(X, y, random=False, k=3):
-    x_t, y_t = X, y
-    if random:
-        shuffle(x_t)
-        shuffle(y_t)
-    x_y_pairs = []
-    n = int(X.shape[0] / k)
-    for i in range(k):
-        x_y_pairs.append([x_t[i * n:(i + 1) * n], y_t[i * n:(i + 1) * n]])
-    for i in range(X.shape[0] % k):
-        x_y_pairs[i][0] = vstack((x_y_pairs[i][0], x_t[n * k + i]))
-        x_y_pairs[i][1] = vstack((x_y_pairs[i][1], y_t[n * k + i]))
-    return x_y_pairs
diff --git a/README.rst b/README.rst
index 604e0a4f..1ca9919f 100644
--- a/README.rst
+++ b/README.rst
@@ -23,29 +23,81 @@ Install with
 
 Current available algorithms:
 
-+--------------------------------------+------------------------------+-----------------+----------+-----------------+
-| Filters                              | Wrappers                     | Hybrid          | Embedded | Ensembles       |
-+======================================+==============================+=================+==========+=================+
-| Spearman correlation                 | Add Del                      | Filter Wrapper  | MOSNS    | MeLiF           |
-+--------------------------------------+------------------------------+-----------------+----------+-----------------+
-| Pearson correlation                  | Backward selection           |                 | MOSS     | Best goes first |
-+--------------------------------------+------------------------------+-----------------+----------+-----------------+
-| Fit Criterion                        | Sequential Forward Selection |                 | RFE      | Best sum        |
-+--------------------------------------+------------------------------+-----------------+----------+-----------------+
-| F ratio                              | QPFS                         |                 |          |                 |
-+--------------------------------------+------------------------------+-----------------+----------+-----------------+
-| Gini index                           | Hill climbing                |                 |          |                 |
-+--------------------------------------+------------------------------+-----------------+----------+-----------------+
-| Information Gain                     |                              |                 |          |                 |
-+--------------------------------------+------------------------------+-----------------+----------+-----------------+
-| Minimum Redundancy Maximum Relevance |                              |                 |          |                 |
-+--------------------------------------+------------------------------+-----------------+----------+-----------------+
-| VDM                                  |                              |                 |          |                 |
-+--------------------------------------+------------------------------+-----------------+----------+-----------------+
-| QPFS                                 |                              |                 |          |                 |
-+--------------------------------------+------------------------------+-----------------+----------+-----------------+
-| MIMAGA                               |                              |                 |          |                 |
-+--------------------------------------+------------------------------+-----------------+----------+-----------------+
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Supervised filters                           | Unsupervised filters                          | Wrappers                     | Hybrid          | Embedded | Ensembles       |
++==============================================+===============================================+==============================+=================+==========+=================+
+| Spearman correlation                         | Trace Ratio (Laplacian)                       | Add Del                      | Filter Wrapper  | MOSNS    | MeLiF           |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Pearson correlation                          | Multi-Cluster Feature Selection               | Backward selection           | IWSSr-SFLA      | MOSS     | Best goes first |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Fit Criterion                                | Unsupervised Discriminative Feature Selection | Sequential Forward Selection |                 | RFE      | Best sum        |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| F ratio                                      |                                               | QPFS                         |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Gini index                                   |                                               | Hill climbing                |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Symmetric Uncertainty                        |                                               | Simulated Annealing          |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Fechner correlation                          |                                               | Recursive Elimination        |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Kendall correlation                          |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Information Gain                             |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| ANOVA                                        |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Chi-squared                                  |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Relief                                       |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| ReliefF                                      |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Laplacian score                              |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Modified T-score                             |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Mutual Information Maximization              |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Minimum Redundancy Maximum Relevance         |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Joint Mutual Information                     |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Conditional Infomax Feature Extraction       |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Mutual Information Feature Selection         |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Conditional Mutual Info Maximization         |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Interaction Capping                          |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Dynamic Change of Selected Feature           |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Composition of Feature Relevancy             |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Max-Relevance and Max-Independence           |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Interaction Weight                           |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Double Input Symmetric Relevance             |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Fast Correlation                             |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Statistical Inference Relief                 |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Trace Ratio (Fisher)                         |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Nonnegative Discriminative Feature Selection |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Robust Feature Selection                     |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| Spectral Feature Selection                   |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| VDM                                          |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| QPFS                                         |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
+| MIMAGA                                       |                                               |                              |                 |          |                 |
++----------------------------------------------+-----------------------------------------------+------------------------------+-----------------+----------+-----------------+
 
 Documentation:
 
diff --git a/meta.yml b/meta.yml
new file mode 100644
index 00000000..b2637e68
--- /dev/null
+++ b/meta.yml
@@ -0,0 +1,35 @@
+{% set name = "itmo_fs" %}
+{% set version = "0.3.3" %}
+
+package:
+  name: "{{ name|lower }}"
+  version: "{{ load_setup_py_data().version }}"
+
+source:
+  git_rev:
+  git_url:
+  url: https://pypi.io/packages/source/{{ name[0] }}/{{ name }}/{{ name }}-{{ version }}.tar.gz
+      sha256: 5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7
+
+build:
+  number: 0
+  script: "{{ PYTHON }} -m pip install . --no-deps --ignore-installed -vv "
+
+
+requirements:
+  build:
+    - pip
+    - python
+    - setuptools
+
+  run:
+    - python
+
+test:
+  imports:
+    -itmo_fs
+    -pandas
+    -pytest
+
+about:
+  home:
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 57c8583e..00849916 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,10 @@
-imblearn
-numpy
-scipy
-scikit-learn
+imbalanced-learn
+numpy~=1.19.2
+scipy~=1.5.2
+scikit-learn~=0.23.2
 qpsolvers
+
+dvc~=1.11.16
+pandas~=1.1.3
+imblearn~=0.0
+setuptools~=50.3.1
\ No newline at end of file
diff --git a/setup.py b/setup.py
index a2039571..7f115072 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,12 @@
 import codecs
 
 from setuptools import find_packages, setup
+import os
+base_dir = os.path.dirname(__file__)
+
+about = {}
+with open(os.path.join(base_dir, "ITMO_FS", "__about__.py")) as f:
+    exec(f.read(), about)
 
 DISTNAME = 'ITMO_FS'
 DESCRIPTION = 'Python Feature Selection library from ITMO University.'
@@ -11,7 +17,7 @@
 URL = 'https://github.com/ctlab/ITMO_FS'
 LICENSE = 'new BSD'
 DOWNLOAD_URL = 'https://github.com/ctlab/ITMO_FS'
-VERSION = '0.3.3'
+VERSION = about["__version__"],
 INSTALL_REQUIRES = ['numpy', 'scipy', 'scikit-learn', 'imblearn', 'qpsolvers']
 CLASSIFIERS = ['Intended Audience :: Science/Research',
                'Intended Audience :: Developers',
@@ -26,7 +32,8 @@
                'Programming Language :: Python :: 2.7',
                'Programming Language :: Python :: 3.5',
                'Programming Language :: Python :: 3.6',
-               'Programming Language :: Python :: 3.7']
+               'Programming Language :: Python :: 3.7',
+               'Programming Language :: Python :: 3.8']
 EXTRAS_REQUIRE = {
     'tests': [
         'pytest',


Test patch:
diff --git a/test/Melif_test.py b/test/Melif_test.py
index 3bf4fa01..bb59d245 100644
--- a/test/Melif_test.py
+++ b/test/Melif_test.py
@@ -6,9 +6,11 @@
 from sklearn.metrics import f1_score
 from sklearn.model_selection import train_test_split
 from sklearn.svm import SVC
+from sklearn.utils.estimator_checks import check_estimator
 from ITMO_FS.ensembles import WeightBased
 from ITMO_FS.filters import *
 from ITMO_FS.hybrid.Melif import Melif
+from ITMO_FS.utils import f1_scorer
 
 
 class MyTestCase(unittest.TestCase):
@@ -20,24 +22,25 @@ class MyTestCase(unittest.TestCase):
                UnivariateFilter(pearson_corr),
                UnivariateFilter(spearman_corr)]
 
-    estimator = SVC()
-    ensemble = WeightBased(filters)
+    estimator = SVC(random_state=42)
+    ensemble = WeightBased(filters, cutting_rule=select_k_best(50))
+
+    melif = Melif(estimator, select_k_best(1500), ensemble, scorer=f1_score, verbose=True)
+
 
-    melif = Melif(ensemble, f1_score, verbose=False)
 
     def test_wide(self):
         data, target = self.wide_classification[0], self.wide_classification[1]
 
         train_data, test_data, train_target, test_target = train_test_split(data, target)
-        self.melif.fit(train_data, train_target, self.estimator, select_k_best(1500))
+        self.melif.fit(train_data, train_target)
 
         print(f1_score(test_target, self.melif.predict(test_data)))
 
     def test_wide_pd(self):
         data, target = pd.DataFrame(self.wide_classification[0]), pd.DataFrame(self.wide_classification[1])
         train_data, test_data, train_target, test_target = train_test_split(data, target)
-        self.melif.fit(train_data, train_target, self.estimator, select_k_best(1500),
-                       feature_names=[str(i) + ' column' for i in data.columns])
+        self.melif.fit(train_data, train_target)
         print(f1_score(test_target, self.melif.predict(test_data)))
 
     def test_R(self):
@@ -66,6 +69,10 @@ def test_R(self):
             f.fit(data[features], data[target])
             print('|', datetime.datetime.now() - start, '|')
 
+    def test_est(self):
+        melif = Melif(self.estimator, select_k_best(2), self.ensemble, scorer=f1_scorer)
+        check_estimator(melif)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/test/datasets/.gitignore b/test/datasets/.gitignore
new file mode 100644
index 00000000..42193807
--- /dev/null
+++ b/test/datasets/.gitignore
@@ -0,0 +1,5 @@
+/gisette.csv
+/madelon.csv
+/arcene.csv
+/dexter.csv
+/dorothea.csv
diff --git a/test/datasets/BASEHOCK.mat.dvc b/test/datasets/BASEHOCK.mat.dvc
deleted file mode 100644
index 020bcdfa..00000000
--- a/test/datasets/BASEHOCK.mat.dvc
+++ /dev/null
@@ -1,4 +0,0 @@
-outs:
-- md5: a66e8fedaea4b302ef2d206261fd687c
-  size: 279059
-  path: BASEHOCK.mat
diff --git a/test/datasets/COIL20.mat.dvc b/test/datasets/COIL20.mat.dvc
deleted file mode 100644
index b05e0573..00000000
--- a/test/datasets/COIL20.mat.dvc
+++ /dev/null
@@ -1,4 +0,0 @@
-outs:
-- md5: c940a2768c55a6edbbe697c6b93eec18
-  size: 3024549
-  path: COIL20.mat
diff --git a/test/datasets/arcene.csv.dvc b/test/datasets/arcene.csv.dvc
new file mode 100644
index 00000000..2aa58fb0
--- /dev/null
+++ b/test/datasets/arcene.csv.dvc
@@ -0,0 +1,4 @@
+outs:
+- md5: 13c55f4366f0bc89c4bc6c4bc826b0bc
+  size: 2715738
+  path: arcene.csv
diff --git a/test/datasets/dexter.csv.dvc b/test/datasets/dexter.csv.dvc
new file mode 100644
index 00000000..ae142c39
--- /dev/null
+++ b/test/datasets/dexter.csv.dvc
@@ -0,0 +1,4 @@
+outs:
+- md5: cca2fe50c6a4869948fba36d56f0ef86
+  size: 24038795
+  path: dexter.csv
diff --git a/test/datasets/dorothea.csv.dvc b/test/datasets/dorothea.csv.dvc
new file mode 100644
index 00000000..7b58e446
--- /dev/null
+++ b/test/datasets/dorothea.csv.dvc
@@ -0,0 +1,4 @@
+outs:
+- md5: 9ae080330bbe3e419b415fbbb5761252
+  size: 320002322
+  path: dorothea.csv
diff --git a/test/datasets/gisette.csv.dvc b/test/datasets/gisette.csv.dvc
new file mode 100644
index 00000000..78931173
--- /dev/null
+++ b/test/datasets/gisette.csv.dvc
@@ -0,0 +1,4 @@
+outs:
+- md5: 2a1efe73ab9eace5947d2d5cb62d16d6
+  size: 67797724
+  path: gisette.csv
diff --git a/test/datasets/madelon.csv.dvc b/test/datasets/madelon.csv.dvc
new file mode 100644
index 00000000..394f5544
--- /dev/null
+++ b/test/datasets/madelon.csv.dvc
@@ -0,0 +1,4 @@
+outs:
+- md5: 44cce2dfb74b4ade59fcf63be7c59610
+  size: 4004995
+  path: madelon.csv
diff --git a/test/datasets/orlraws10P.mat.dvc b/test/datasets/orlraws10P.mat.dvc
deleted file mode 100644
index d1552cc0..00000000
--- a/test/datasets/orlraws10P.mat.dvc
+++ /dev/null
@@ -1,4 +0,0 @@
-outs:
-- md5: c83b559478c946fe0248465b8e481bee
-  size: 951783
-  path: orlraws10P.mat
diff --git a/test/embedded_test.py b/test/embedded_test.py
index 966601b7..226c2907 100644
--- a/test/embedded_test.py
+++ b/test/embedded_test.py
@@ -1,109 +1,164 @@
 import unittest
 
 import pandas as pd
-from sklearn.linear_model import LogisticRegression
+from sklearn.linear_model import LogisticRegression, SGDClassifier
 from sklearn.pipeline import Pipeline
 import numpy as np
 
+from sklearn.utils.estimator_checks import check_estimator
+
 from ITMO_FS.embedded import *
+from ITMO_FS.utils import weight_func
 
 np.random.seed(42)
 
+
 class TestCases(unittest.TestCase):
-    data, target = np.random.randint(10, size=(100, 20)), np.random.randint(10, size=(100,))
-    feature_names = [''.join(['f', str(i)]) for i in range(data.shape[1])]
-    feature_names_override = [''.join(['g', str(i)]) for i in range(data.shape[1])]
+    data, target = np.random.randint(
+        10, size=(
+            100, 20)), np.random.randint(
+        10, size=(
+            100,))
+
+    def test_MOS_err_loss(self):
+        with self.assertRaises(KeyError):
+            MOS(model=SGDClassifier(), weight_func=weight_func,
+                sampling=True, loss="err").fit(self.data,
+                                               self.target)
+
+    def test_MOS_no_sampling(self):
+        # MOSS
+        res = MOS(
+            model=SGDClassifier(),
+            weight_func=weight_func).fit_transform(
+            self.data,
+            self.target)
+        assert self.data.shape[0] == res.shape[0]
+        print("MOSS:", self.data.shape, '--->', res.shape)
 
     def test_MOSS(self):
         # MOSS
-        res = MOS().fit_transform(self.data, self.target, sampling=True)
+        res = MOS(
+            model=SGDClassifier(),
+            weight_func=weight_func,
+            sampling=True).fit_transform(
+            self.data,
+            self.target)
+        assert self.data.shape[0] == res.shape[0]
+        print("MOSS:", self.data.shape, '--->', res.shape)
+
+    def test_MOSS_n_naigbours_err(self):
+        # MOSS
+        with self.assertRaises(ValueError):
+            MOS(
+                model=SGDClassifier(),
+                weight_func=weight_func,
+                sampling=True, k_neighbors=1000).fit_transform(
+                self.data,
+                self.target)
+
+    def test_MOSS_hinge(self):
+        # MOSS
+        res = MOS(
+            model=SGDClassifier(),
+            weight_func=weight_func,
+            sampling=True, loss="hinge").fit_transform(
+            self.data,
+            self.target)
         assert self.data.shape[0] == res.shape[0]
         print("MOSS:", self.data.shape, '--->', res.shape)
 
     def test_MOSNS(self):
         # MOSNS
-        res = MOS().fit_transform(self.data, self.target, sampling=False)
+        res = MOS(
+            model=SGDClassifier(),
+            weight_func=weight_func,
+            sampling=False).fit_transform(
+            self.data,
+            self.target)
         assert self.data.shape[0] == res.shape[0]
         print("MOSNS:", self.data.shape, '--->', res.shape)
 
     def test_losses(self):
         for loss in ['log', 'hinge']:
-            res = MOS(loss=loss).fit_transform(self.data, self.target)
+            res = MOS(
+                model=SGDClassifier(),
+                weight_func=weight_func,
+                loss=loss).fit_transform(
+                self.data,
+                self.target)
             assert self.data.shape[0] == res.shape[0]
 
     def test_df(self):
-        f = MOS()
+        f = MOS(model=SGDClassifier(), weight_func=weight_func, sampling=True)
 
-        df = f.fit_transform(pd.DataFrame(self.data), pd.DataFrame(self.target), sampling=True)
-        arr = f.fit_transform(self.data, self.target, sampling=True)
+        df = f.fit_transform(
+            pd.DataFrame(
+                self.data), pd.DataFrame(
+                self.target))
+        arr = f.fit_transform(self.data, self.target)
         np.testing.assert_array_equal(df, arr)
 
-        df = f.fit_transform(pd.DataFrame(self.data), pd.DataFrame(self.target), sampling=False)
-        arr = f.fit_transform(self.data, self.target, sampling=False)
+        f = MOS(model=SGDClassifier(), weight_func=weight_func, sampling=False)
+
+        df = f.fit_transform(
+            pd.DataFrame(
+                self.data), pd.DataFrame(
+                self.target))
+        arr = f.fit_transform(self.data, self.target)
         np.testing.assert_array_equal(df, arr)
 
     def test_pipeline(self):
         # FS
-        p = Pipeline([('FS1', MOS())])
+        p = Pipeline(
+            [('FS1', MOS(model=SGDClassifier(), weight_func=weight_func))])
         p.fit(self.data, self.target)
         res = p.transform(self.data)
         assert self.data.shape[0] == res.shape[0]
 
         # FS - estim
-        p = Pipeline([('FS1', MOS()), ('E1', LogisticRegression())])
+        p = Pipeline([('FS1', MOS(model=SGDClassifier(),
+                                  weight_func=weight_func)),
+                      ('E1', LogisticRegression())])
         p.fit(self.data, self.target)
         assert 0 <= p.score(self.data, self.target) <= 1
 
         # FS - FS
-        p = Pipeline([('FS1', MOS(loss='log')), ('FS2', MOS(loss='hinge'))])
+        p = Pipeline([('FS1',
+                       MOS(model=SGDClassifier(),
+                           weight_func=weight_func,
+                           loss='log')),
+                      ('FS2',
+                       MOS(model=SGDClassifier(),
+                           weight_func=weight_func,
+                           loss='hinge'))])
         p.fit(self.data, self.target)
         res = p.transform(self.data)
         assert self.data.shape[0] == res.shape[0]
 
         # FS - FS - estim
-        p = Pipeline([('FS1', MOS(loss='log')), ('FS2', MOS(loss='hinge')), ('E1', LogisticRegression())])
+        p = Pipeline([('FS1',
+                       MOS(model=SGDClassifier(), weight_func=weight_func,
+                           loss='log')), ('FS2', MOS(
+                               model=SGDClassifier(), weight_func=weight_func, loss='hinge')),
+                      ('E1', LogisticRegression())])
         p.fit(self.data, self.target)
         assert 0 <= p.score(self.data, self.target) <= 1
 
-    def test_feature_names_np(self):
-        f = MOS()
-
-        arr = f.fit_transform(self.data, self.target, feature_names=self.feature_names, sampling=True)
-        assert np.all([feature in self.feature_names for feature in f.get_feature_names()])
-
-        arr = f.fit_transform(self.data, self.target, feature_names=self.feature_names, sampling=False)
-        assert np.all([feature in self.feature_names for feature in f.get_feature_names()])
-
-    def test_feature_names_df(self):
-        f = MOS()
-
-        arr = f.fit_transform(pd.DataFrame(self.data), pd.DataFrame(self.target), feature_names=self.feature_names, sampling=True)
-        assert np.all([feature in self.feature_names for feature in f.get_feature_names()])
-
-        arr = f.fit_transform(pd.DataFrame(self.data), pd.DataFrame(self.target), feature_names=self.feature_names, sampling=False)
-        assert np.all([feature in self.feature_names for feature in f.get_feature_names()])
-
-    def test_feature_names_df_defined(self):
-        dfX = pd.DataFrame(self.data)
-        dfX.columns = self.feature_names
-        f = MOS()
-
-        arr = f.fit_transform(dfX, pd.DataFrame(self.target), sampling=True)
-        assert np.all([feature in self.feature_names for feature in f.get_feature_names()])
-
-        arr = f.fit_transform(dfX, pd.DataFrame(self.target), sampling=False)
-        assert np.all([feature in self.feature_names for feature in f.get_feature_names()])
-
-    def test_feature_names_df_defined_override(self):
-        dfX = pd.DataFrame(self.data)
-        dfX.columns = self.feature_names_override
-        f = MOS()
-
-        arr = f.fit_transform(dfX, pd.DataFrame(self.target), feature_names=self.feature_names, sampling=True)
-        assert np.all([feature in self.feature_names for feature in f.get_feature_names()])
-
-        arr = f.fit_transform(dfX, pd.DataFrame(self.target), feature_names=self.feature_names, sampling=False)
-        assert np.all([feature in self.feature_names for feature in f.get_feature_names()])
+    def test_est(self):
+        moss = MOS(
+            model=SGDClassifier(),
+            weight_func=weight_func,
+            sampling=True)
+        mosns = MOS(
+            model=SGDClassifier(),
+            weight_func=weight_func,
+            sampling=False)
+
+        # for some reason using local weight_func or lambda here causes it to fail with pickle errors
+        # so we're using an imported weight_func
+        check_estimator(moss)
+        check_estimator(mosns)
 
 
 if __name__ == "__main__":
diff --git a/test/ensemble_test.py b/test/ensemble_test.py
index 1c29f719..e590c7fd 100644
--- a/test/ensemble_test.py
+++ b/test/ensemble_test.py
@@ -1,7 +1,10 @@
 import time
 import unittest
 import numpy as np
-from collections import defaultdict
+from sklearn.linear_model import LogisticRegression, RidgeClassifier
+
+from ITMO_FS import BestSum
+from utils import load_dataset
 
 from sklearn.datasets import make_classification, make_regression
 from sklearn.metrics import f1_score
@@ -14,110 +17,184 @@
 
 
 class MyTestCase(unittest.TestCase):
-    wide_classification = make_classification(n_features=2000, n_informative=100, n_redundant=500)
-    tall_classification = make_classification(n_samples=50000, n_features=100, n_informative=23, n_redundant=30)
+    wide_classification = make_classification(n_features=2000,
+                                              n_informative=100,
+                                              n_redundant=500)
+    tall_classification = make_classification(n_samples=50000, n_features=100,
+                                              n_informative=23, n_redundant=30)
     wide_regression = make_regression(n_features=2000, n_informative=100)
-    tall_regression = make_regression(n_samples=50000, n_features=200, n_informative=50)
+    tall_regression = make_regression(n_samples=50000, n_features=200,
+                                      n_informative=50)
 
-    def test_ranking_based_ensemble(self):
-        data, target = self.wide_classification[0], self.wide_classification[1]
+    madelon = load_dataset("madelon.csv")
+
+    def test_ranking_based_error_mixed_ensemble(self):
+        data, target = self.madelon.drop(['target'], axis=1), self.madelon[
+            "target"]
+        filters = [gini_index,
+                   fechner_corr,
+                   spearman_corr,
+                   pearson_corr]
+        with self.assertRaises(ValueError):
+            ensemble = Mixed(filters, k=1000000, fusion_function=borda_fusion)
+            ensemble.fit(data, target)
+
+    def test_ranking_based_mixed_ensemble(self):
+        data, target = self.madelon.drop(['target'], axis=1), self.madelon[
+            "target"]
         filters = [gini_index,
                    fechner_corr,
                    spearman_corr,
                    pearson_corr]
-        ensemble = Mixed(filters)
-        ensemble.fit(data, target)
-        ensemble.transform(data, 100, borda_fusion)
-        d = [{'f' + str(i): i for i in range(100)}.items()] * 5
-        self.assertEqual(borda_fusion(d, 100), ['f' + str(i) for i in reversed(range(100))])
-        ensemble.transform(data, 100)
-        self.assertEqual(borda_fusion(d, 100), ['f' + str(i) for i in reversed(range(100))])
 
-    def test_weight_based_ensemble(self):
+        ensemble = Mixed(filters, k=100, fusion_function=borda_fusion)
+        ensemble.fit(data, target)
+        ensemble.transform(data)
+
+        d = np.array([['f' + str(i) for i in range(100)]]* 5)
+        self.assertEqual(borda_fusion(d, 100),
+                         ['f' + str(i) for i in reversed(range(100))])
+
+    def test_ranking_based_bgf_fusion_function(self):
+        random.seed(42)
+        filter_results = np.array([
+            [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+            [3, 2, 10, 5, 7, 9, 6, 4, 8, 1],
+            [10, 9, 8, 7, 6, 5, 4, 3, 2, 1],
+            [1, 10, 9, 2, 8, 3, 7, 4, 6, 5],
+            [5, 4, 6, 3, 7, 2, 8, 10, 9, 1]
+        ])
+        np.testing.assert_array_equal(
+            best_goes_first_fusion(filter_results, 10),
+            [5, 3, 10, 1, 4, 9, 2, 8, 6, 7])
+
+    def test_measure_based_weight_based_ensemble(self):
         data, target = self.wide_classification[0], self.wide_classification[1]
         filters = [UnivariateFilter(gini_index),
                    UnivariateFilter(fechner_corr),
                    UnivariateFilter(spearman_corr),
                    UnivariateFilter(pearson_corr)]
-        ensemble = WeightBased(filters)
-        ensemble.fit(data, target)
-
         weights = [0.5, 0.5, 0.5, 0.5]
-        ensemble.transform(data, select_k_best(100), weights=weights)
+        ensemble = WeightBased(filters, select_k_best(100), weights=weights)
+        ensemble.fit(data, target)
+        ensemble.transform(data)
+        assert len(ensemble) == len(filters)
 
-    def test_benching_ensembles(self):
-        datasets = [make_classification(n_samples=2000, n_features=20 * i, n_informative=i, n_redundant=5 * i) for i in
-                    [2, 10, 20, 50, 100, 200, 500, 1000]]
+    def test_measure_based_weight_based_no_weights_ensemble(self):
+        data, target = self.wide_classification[0], self.wide_classification[1]
+        filters = [UnivariateFilter(gini_index),
+                   UnivariateFilter(fechner_corr),
+                   UnivariateFilter(spearman_corr),
+                   UnivariateFilter(pearson_corr)]
+        ensemble = WeightBased(filters, select_k_best(100))
 
-        filters = [gini_index,
-                   fechner_corr,
-                   spearman_corr,
-                   pearson_corr]
+        ensemble.fit(data, target)
+        ensemble.transform(data)
+
+    # def test_benching_ensembles(self):
+    #     datasets = [make_classification(n_samples=2000, n_features=20 * i,
+    #                                     n_informative=i, n_redundant=5 * i) for
+    #                 i in
+    #                 [2, 10, 20, 50, 100, 200, 500, 1000]]
+    #
+    #     filters = [gini_index,
+    #                fechner_corr,
+    #                spearman_corr,
+    #                pearson_corr]
+    #
+    #     kfold = KFold(n_splits=10)
+    #     for dataset in datasets:
+    #         X, y = dataset
+    #         k = int(X.shape[1] * 0.1)
+    #
+    #         time_ens_start = []
+    #         time_ens_end = []
+    #
+    #         time_filter_start = defaultdict(list)
+    #         time_filter_end = defaultdict(list)
+    #
+    #         scores_ens = []
+    #         scores_filters = defaultdict(list)
+    #         scores_no_fs = []
+    #
+    #         for train_index, test_index in kfold.split(X):
+    #             svm = SVC()
+    #             svm.fit(X[train_index], y[train_index])
+    #             y_pred = svm.predict(X[test_index])
+    #             scores_no_fs.append(f1_score(y[test_index], y_pred))
+    #
+    #             time_ens_start.append(time.time())
+    #             ensemble = Mixed(filters)
+    #             ensemble.fit(X[train_index], y[train_index])
+    #             X_transformed = ensemble.transform(X, k, borda_fusion)
+    #             time_ens_end.append(time.time())
+    #
+    #             svm = SVC()
+    #             svm.fit(X_transformed[train_index], y[train_index])
+    #             y_pred = svm.predict(X_transformed[test_index])
+    #             scores_ens.append(f1_score(y[test_index], y_pred))
+    #
+    #             for filter in filters:
+    #                 time_filter_start[filter.__name__].append(time.time())
+    #                 univ_filter = UnivariateFilter(filter,
+    #                                                cutting_rule=("K best", k))
+    #                 univ_filter.fit(X[train_index], y[train_index])
+    #                 X_transformed = univ_filter.transform(X)
+    #                 time_filter_end[filter.__name__].append(time.time())
+    #
+    #                 svm = SVC()
+    #                 svm.fit(X_transformed[train_index], y[train_index])
+    #                 y_pred = svm.predict(X_transformed[test_index])
+    #                 scores_filters[filter.__name__].append(
+    #                     f1_score(y[test_index], y_pred))
+    #
+    #         print('Dataset size', X.shape)
+    #
+    #         sum_time = 0
+    #         for filter in filters:
+    #             filter_dif = np.array(
+    #                 time_filter_end[filter.__name__]) - np.array(
+    #                 time_filter_start[filter.__name__])
+    #             print('Filter ' + filter.__name__ + ' time',
+    #                   np.mean(filter_dif), np.std(filter_dif))
+    #             sum_time += np.mean(filter_dif)
+    #
+    #         ens_dif = np.array(time_ens_end) - np.array(time_ens_start)
+    #         print('Ensemble time', np.mean(ens_dif), np.std(ens_dif))
+    #         print('Sum of filter time', sum_time)
+    #
+    #         print('No fs score', np.mean(scores_no_fs), np.std(scores_no_fs))
+    #
+    #         for filter in filters:
+    #             print('Filter ' + filter.__name__ + ' time',
+    #                   np.mean(scores_filters[filter.__name__]),
+    #                   np.std(scores_filters[filter.__name__]))
+    #
+    #         print('Ensemble score', np.mean(scores_ens), np.std(scores_ens))
+    #         print()
+
+    def test_model_based_best_sum_no_models(self):
+        models = []
+        data, target = self.madelon.drop(['target'], axis=1), self.madelon[
+            "target"]
+        with self.assertRaises(ValueError):
+            m = BestSum(models, select_k_best(100), lambda x: x)
+            m.fit(data, target)
+
+    def test_model_based_best_sum(self):
+        x = np.array([[3, 3, 3, 2, 2],
+                      [3, 3, 1, 2, 3],
+                      [1, 3, 5, 1, 1],
+                      [3, 1, 4, 3, 1],
+                      [3, 1, 2, 3, 1]])
+        y = np.array([1, 2, 2, 1, 2])
+        models = [SVC(kernel='linear'), LogisticRegression(),
+                  RidgeClassifier()]
+        ensemble = BestSum(models, select_k_best(2),
+                           lambda model: np.square(model.coef_).sum(axis=0),
+                           cv=2)
+        ensemble.fit(x, y)
 
-        kfold = KFold(n_splits=10)
-        for dataset in datasets:
-            X, y = dataset
-            k = int(X.shape[1] * 0.1)
-
-            time_ens_start = []
-            time_ens_end = []
-
-            time_filter_start = defaultdict(list)
-            time_filter_end = defaultdict(list)
-
-            scores_ens = []
-            scores_filters = defaultdict(list)
-            scores_no_fs = []
-
-            for train_index, test_index in kfold.split(X):
-                svm = SVC()
-                svm.fit(X[train_index], y[train_index])
-                y_pred = svm.predict(X[test_index])
-                scores_no_fs.append(f1_score(y[test_index], y_pred))
-
-                time_ens_start.append(time.time())
-                ensemble = Mixed(filters)
-                ensemble.fit(X[train_index], y[train_index])
-                X_transformed = ensemble.transform(X, k, borda_fusion)
-                time_ens_end.append(time.time())
-
-                svm = SVC()
-                svm.fit(X_transformed[train_index], y[train_index])
-                y_pred = svm.predict(X_transformed[test_index])
-                scores_ens.append(f1_score(y[test_index], y_pred))
-
-                for filter in filters:
-                    time_filter_start[filter.__name__].append(time.time())
-                    univ_filter = UnivariateFilter(filter, cutting_rule=("K best", k))
-                    univ_filter.fit(X[train_index], y[train_index])
-                    X_transformed = univ_filter.transform(X)
-                    time_filter_end[filter.__name__].append(time.time())
-
-                    svm = SVC()
-                    svm.fit(X_transformed[train_index], y[train_index])
-                    y_pred = svm.predict(X_transformed[test_index])
-                    scores_filters[filter.__name__].append(f1_score(y[test_index], y_pred))
-
-            print('Dataset size', X.shape)
-
-            sum_time = 0
-            for filter in filters:
-                filter_dif = np.array(time_filter_end[filter.__name__]) - np.array(time_filter_start[filter.__name__])
-                print('Filter ' + filter.__name__ + ' time', np.mean(filter_dif), np.std(filter_dif))
-                sum_time += np.mean(filter_dif)
-
-            ens_dif = np.array(time_ens_end) - np.array(time_ens_start)
-            print('Ensemble time', np.mean(ens_dif), np.std(ens_dif))
-            print('Sum of filter time', sum_time)
-
-            print('No fs score', np.mean(scores_no_fs), np.std(scores_no_fs))
-
-            for filter in filters:
-                print('Filter ' + filter.__name__ + ' time', np.mean(scores_filters[filter.__name__]),
-                      np.std(scores_filters[filter.__name__]))
-
-            print('Ensemble score', np.mean(scores_ens), np.std(scores_ens))
-            print()
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/test/hybrid_test.py b/test/hybrid_test.py
new file mode 100644
index 00000000..608b9cfb
--- /dev/null
+++ b/test/hybrid_test.py
@@ -0,0 +1,22 @@
+import unittest
+
+from sklearn.linear_model import LogisticRegression
+from sklearn.utils.estimator_checks import check_estimator
+from sklearn.metrics import make_scorer
+from ITMO_FS.filters import *
+from ITMO_FS.wrappers import BackwardSelection
+from ITMO_FS.utils import f1_scorer
+from ITMO_FS.hybrid import FilterWrapperHybrid
+
+
+class MyTestCase(unittest.TestCase):
+
+    def test_est(self):
+        classifier = LogisticRegression(max_iter=1000)
+        back_selection = BackwardSelection(classifier, 2, make_scorer(f1_scorer))
+        fw = FilterWrapperHybrid(UnivariateFilter(spearman_corr, cutting_rule=('K best', 2)), back_selection)
+        check_estimator(fw)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/test/multivariate_filters_test.py b/test/multivariate_filters_test.py
index b8ab7e78..a7c435ea 100644
--- a/test/multivariate_filters_test.py
+++ b/test/multivariate_filters_test.py
@@ -8,26 +8,61 @@
 from sklearn.utils.estimator_checks import check_estimator
 
 from ITMO_FS.filters.multivariate import *
+from utils import load_dataset
 
 np.random.seed(42)
 
 
 class TestCases(unittest.TestCase):
-    data, target = np.random.randint(10, size=(100, 20)), np.random.randint(10, size=(100,))
-    feature_names = [''.join(['f', str(i)]) for i in range(data.shape[1])]
-    feature_names_override = [''.join(['g', str(i)]) for i in range(data.shape[1])]
+    data, target = np.random.randint(10, size=(100, 20)), np.random.randint(10,
+                                                                            size=(
+                                                                                100,))
+    madelon = load_dataset("madelon.csv")
 
     def test_FCBF(self):
         # FCBF
         res = FCBFDiscreteFilter().fit_transform(self.data, self.target)
         assert self.data.shape[0] == res.shape[0]
-        print("Fast Correlation Based filter:", self.data.shape, '--->', res.shape)
+        print("Fast Correlation Based filter:", self.data.shape, '--->',
+              res.shape)
 
     def test_DISR(self):
         # DISR
-        res = DISRWithMassive().fit_transform(self.data, self.target)
+        res = DISRWithMassive(10).fit_transform(self.data, self.target)
         assert self.data.shape[0] == res.shape[0]
-        print("Double Input Symmetric Relevance:", self.data.shape, '--->', res.shape)
+        print("Double Input Symmetric Relevance:", self.data.shape, '--->',
+              res.shape)
+
+    def test_JMIM_error(self):
+        # JMIM
+        data, target = self.madelon.drop(['target'], axis=1), self.madelon[
+            "target"]
+        with self.assertRaises(ValueError):
+            JMIM(10000).fit_transform(data, target)
+
+    def test_JMIM(self):
+        # JMIM
+        res = JMIM(10).fit_transform(self.data, self.target)
+        assert self.data.shape[0] == res.shape[0]
+        print("Joint Mutual Information Maximisation:", self.data.shape,
+              '--->', res.shape)
+
+    def test_JMIM_normalised(self):
+        # JMIM
+        res = JMIM(10, normalized=True).fit_transform(self.data, self.target)
+        assert self.data.shape[0] == res.shape[0]
+        print("Joint Mutual Information Maximisation:", self.data.shape,
+              '--->', res.shape)
+
+    def test_multivariate_interface_error_features(self):
+        with self.assertRaises(ValueError):
+            filter = MultivariateFilter("MRMR", 5000000)
+            filter.fit(self.data, self.target)
+
+    def test_multivariate_interface_error_name(self):
+        with self.assertRaises(KeyError):
+            filter = MultivariateFilter("asoikfhlkjfdslfh", 5)
+            filter.fit(self.data, self.target)
 
     def test_trace_ratio(self):
         # TraceRatioFisher
@@ -39,21 +74,34 @@ def test_stir(self):
         # STIR
         res = STIR(10).fit_transform(self.data, self.target)
         assert self.data.shape[0] == res.shape[0]
-        print("STatistical Inference Relief:", self.data.shape, '--->', res.shape)
+        print("Statistical Inference Relief:", self.data.shape, '--->',
+              res.shape)
 
     def test_base_multivariate(self):
-        # Multivariate
+        # Multivariate with callable
         f = MultivariateFilter(MIM, 10)
         f.fit(self.data, self.target)
         res = f.transform(self.data)
         assert self.data.shape[0] == res.shape[0]
-        print("Base multivariate:", self.data.shape, '--->', res.shape)
+        print("Multivariate with callable:", self.data.shape, '--->',
+              res.shape)
+
+        # Multivariate with string
+        f = MultivariateFilter('MRMR', 10)
+        f.fit(self.data, self.target)
+        res = f.transform(self.data)
+        assert self.data.shape[0] == res.shape[0]
+        print("Multivariate with string:", self.data.shape, '--->', res.shape)
 
     def test_k_best(self):
         for i in [5, 10, 20]:
             res = DISRWithMassive(i).fit_transform(self.data, self.target)
             assert i == res.shape[1]
 
+        for i in [5, 10, 20]:
+            res = JMIM(i).fit_transform(self.data, self.target)
+            assert i == res.shape[1]
+
         for i in [5, 10, 20]:
             f = MultivariateFilter(MIM, i)
             f.fit(self.data, self.target)
@@ -70,7 +118,7 @@ def test_k_best(self):
 
     def test_measures(self):
         # Multivariate
-        for measure in GLOB_MEASURE:
+        for measure in MEASURE_NAMES:
             beta = 0.3 if measure in ['MIFS', 'generalizedCriteria'] else None
             gamma = 0.4 if measure == 'generalizedCriteria' else None
             f = MultivariateFilter(measure, 10, beta, gamma)
@@ -79,8 +127,11 @@ def test_measures(self):
             assert self.data.shape[0] == res.shape[0] and res.shape[1] == 10
 
     def test_df(self):
-        for f in [FCBFDiscreteFilter(), DISRWithMassive(), MultivariateFilter(MIM, 10), TraceRatioFisher(10), STIR(10)]:
-            df = f.fit_transform(pd.DataFrame(self.data), pd.DataFrame(self.target))
+        for f in [FCBFDiscreteFilter(), DISRWithMassive(10), JMIM(10),
+                  MultivariateFilter(MIM, 10), \
+                  TraceRatioFisher(10), STIR(10)]:
+            df = f.fit_transform(pd.DataFrame(self.data),
+                                 pd.DataFrame(self.target))
             arr = f.fit_transform(self.data, self.target)
             np.testing.assert_array_equal(df, arr)
 
@@ -92,7 +143,8 @@ def test_pipeline(self):
         assert self.data.shape[0] == res.shape[0] and res.shape[1] == 10
 
         # FS - estim
-        p = Pipeline([('FS1', FCBFDiscreteFilter()), ('E1', LogisticRegression())])
+        p = Pipeline([('FS1', FCBFDiscreteFilter()),
+                      ('E1', LogisticRegression(max_iter=10000))])
         p.fit(self.data, self.target)
         assert 0 <= p.score(self.data, self.target) <= 1
 
@@ -103,37 +155,20 @@ def test_pipeline(self):
         assert self.data.shape[0] == res.shape[0] and res.shape[1] == 5
 
         # FS - FS - estim
-        p = Pipeline([('FS1', TraceRatioFisher(10)), ('FS2', DISRWithMassive()), ('E1', LogisticRegression())])
+        p = Pipeline(
+            [('FS1', TraceRatioFisher(10)), ('FS2', DISRWithMassive(5)),
+             ('E1', LogisticRegression(max_iter=10000))])
         p.fit(self.data, self.target)
         assert 0 <= p.score(self.data, self.target) <= 1
 
-    def test_feature_names_np(self):
-        for f in [FCBFDiscreteFilter(), DISRWithMassive(), MultivariateFilter(MIM, 10), TraceRatioFisher(10), STIR(10)]:
-            arr = f.fit_transform(self.data, self.target, feature_names=self.feature_names)
-            assert np.all([feature in self.feature_names for feature in f.get_feature_names()])
-
-    def test_feature_names_df(self):
-        for f in [FCBFDiscreteFilter(), DISRWithMassive(), MultivariateFilter(MIM, 10), TraceRatioFisher(10), STIR(10)]:
-            arr = f.fit_transform(pd.DataFrame(self.data), pd.DataFrame(self.target), feature_names=self.feature_names)
-            assert np.all([feature in self.feature_names for feature in f.get_feature_names()])
-
-    def test_feature_names_df_defined(self):
-        dfX = pd.DataFrame(self.data)
-        dfX.columns = self.feature_names
-        for f in [FCBFDiscreteFilter(), DISRWithMassive(), MultivariateFilter(MIM, 10), TraceRatioFisher(10), STIR(10)]:
-            arr = f.fit_transform(dfX, pd.DataFrame(self.target))
-            assert np.all([feature in self.feature_names for feature in f.get_feature_names()])
-
-    def test_feature_names_df_defined_override(self):
-        dfX = pd.DataFrame(self.data)
-        dfX.columns = self.feature_names_override
-        for f in [FCBFDiscreteFilter(), DISRWithMassive(), MultivariateFilter(MIM, 10), TraceRatioFisher(10), STIR(10)]:
-            arr = f.fit_transform(dfX, pd.DataFrame(self.target), feature_names=self.feature_names)
-            assert np.all([feature in self.feature_names for feature in f.get_feature_names()])
-
     def test_est(self):
-        for f in [FCBFDiscreteFilter(), DISRWithMassive(), MultivariateFilter(MIM, 10), TraceRatioFisher(10), STIR(10)]:
-            assert check_estimator(f)
+        for f in [
+            FCBFDiscreteFilter(),
+            DISRWithMassive(2),
+            MultivariateFilter(MIM, 2),
+            TraceRatioFisher(2),
+            STIR(2)]:
+            check_estimator(f)
 
 
 if __name__ == "__main__":
diff --git a/test/univariate_filters_test.py b/test/univariate_filters_test.py
index 627cdcef..c33b5112 100644
--- a/test/univariate_filters_test.py
+++ b/test/univariate_filters_test.py
@@ -1,6 +1,7 @@
 import unittest
 from math import sqrt
 
+import numpy as np
 import pandas as pd
 from scipy import stats
 from sklearn.datasets import load_iris
@@ -10,9 +11,11 @@
 from sklearn.pipeline import Pipeline
 from sklearn.utils.estimator_checks import check_estimator
 
+from ITMO_FS import MIMAGA
 from ITMO_FS.filters.univariate import *
-from ITMO_FS.filters.univariate.measures import GLOB_CR
+from ITMO_FS.filters.univariate.measures import CR_NAMES
 from ITMO_FS.utils.information_theory import *
+from utils import load_dataset
 
 np.random.seed(42)
 
@@ -26,15 +29,23 @@ def fechner_example(a, b):
 
 
 class TestCases(unittest.TestCase):
-    wide_classification = make_classification(n_features=2000, n_informative=100, n_redundant=500)
-    tall_classification = make_classification(n_samples=50000, n_features=100, n_informative=23, n_redundant=30)
+    wide_classification = make_classification(n_features=2000,
+                                              n_informative=100,
+                                              n_redundant=500)
+    tall_classification = make_classification(n_samples=50000, n_features=100,
+                                              n_informative=23, n_redundant=30)
     wide_regression = make_regression(n_features=2000, n_informative=100)
-    tall_regression = make_regression(n_samples=50000, n_features=200, n_informative=50)
+    tall_regression = make_regression(n_samples=50000, n_features=200,
+                                      n_informative=50)
+
+    madelon = load_dataset("madelon.csv")
 
     def test_filters(self):
         data, target = load_iris(True)
 
-        res = UnivariateFilter(spearman_corr, select_best_by_value(0.9999)).fit_transform(data, target)
+        res = UnivariateFilter(spearman_corr,
+                               select_best_by_value(0.9999)).fit_transform(
+            data, target)
         print("SpearmanCorr:", data.shape, '--->', res.shape)
 
         data = np.array([[0, 0], [0, 0], [0, 0]])
@@ -53,7 +64,9 @@ def test_filters(self):
     def test_k_best(self):
         data, target = self.wide_classification[0], self.wide_classification[1]
         for i in [5, 10, 20]:
-            res = UnivariateFilter(spearman_corr, select_k_best(i)).fit_transform(data, target)
+            res = UnivariateFilter(spearman_corr,
+                                   select_k_best(i)).fit_transform(data,
+                                                                   target)
             assert i == res.shape[1]
 
     def test_corr(self):
@@ -61,14 +74,17 @@ def test_corr(self):
         data, target = self.wide_classification[0], self.wide_classification[1]
         for f in [spearman_corr, pearson_corr, fechner_corr, kendall_corr]:
             # X.shape == 1 case
-            np.testing.assert_array_almost_equal(np.round(f(data[:, 0], data[:, 0]), 10), np.array([1.]))
+            np.testing.assert_array_almost_equal(
+                np.round(f(data[:, 0], data[:, 0]), 10), np.array([1.]))
             # X.shape == 2 case
             res = f(data, target)
             assert (-1 <= res).all() and (1 >= res).all()
 
     def test_corr_verification(self):
         # Values verification
-        data, target = np.array([[1, 8], [3, 2], [4, 5], [0, 7], [7, 1], [8, 4]]), np.array([9, 3, 4, 6, 2, 1])
+        data, target = np.array(
+            [[1, 8], [3, 2], [4, 5], [0, 7], [7, 1], [8, 4]]), np.array(
+            [9, 3, 4, 6, 2, 1])
         for f, verif in zip([spearman_corr, pearson_corr],
                             [stats.spearmanr, stats.pearsonr]):
             true_res = []
@@ -79,16 +95,6 @@ def test_corr_verification(self):
             # print(true_res)
             np.testing.assert_allclose(res, true_res)
 
-    # def test_filters(self):
-    #     data, target = self.wide_classification[0], self.wide_classification[1]
-    #     for f, answer in zip(
-    #             [fit_criterion_measure, f_ratio_measure, gini_index, su_measure, chi2_measure, laplacian_score,
-    #              information_gain],
-    #             [np.ones((data.shape[1],)), np.ones((data.shape[1],)), np.ones((data.shape[1],)),
-    #              np.ones((data.shape[1],)), np.ones((data.shape[1],)), np.ones((data.shape[1],)),
-    #              np.ones((data.shape[1],))]):
-    #         assert (f(data[0], data[0]) == answer).all()
-
     def test_df(self):
         # Univariate filter
         data, target = self.wide_classification[0], self.wide_classification[1]
@@ -110,18 +116,6 @@ def test_df(self):
         # arr = f.run(data, target)
         # np.testing.assert_array_equal(df, arr)
 
-    def test_df_feature_names(self):
-        # Univariate filter
-        data, target = self.wide_classification[0], self.wide_classification[1]
-        f = UnivariateFilter(pearson_corr, select_k_best(50))
-        f.fit(pd.DataFrame(data), pd.DataFrame(target), feature_names=[str(i) + ' column' for i in data.columns])
-        df = f.transform(pd.DataFrame(data))
-
-        f = UnivariateFilter(pearson_corr, select_k_best(50))
-        f.fit(data, target)
-        arr = f.transform(data)
-        np.testing.assert_array_equal(df.values, arr)
-
     def test_chi2(self):
         iris_dataset = load_iris()
         X = iris_dataset.data
@@ -143,30 +137,36 @@ def test_anova(self):
         np.testing.assert_allclose(res, true_res)
 
     def test_modified_t_score_by_hand_small(self):
-        X = np.array([[5, 1, 3, 2], [4, 2, 2, 1], [3, 3, 4, 1], [2, 2, 3, 1], [1, 1, 5, 2]])
+        X = np.array([[5, 1, 3, 2], [4, 2, 2, 1], [3, 3, 4, 1], [2, 2, 3, 1],
+                      [1, 1, 5, 2]])
         y = np.array([1, 1, 2, 2, 2])
         scores = modified_t_score(X, y)
 
         # true_scores was calculated by hand
         true_numerator = np.array([5 / 2, 1 / 2, 3 / 2, 1 / 6])
         true_denominator = np.sqrt(np.array([1 / 2, 1 / 2, 1 / 2, 7 / 30]))
-        true_modificator = np.array([(sqrt(3) / 2) / ((0 + 5 / (2 * sqrt(13)) + 0) / 3),
-                                     (sqrt(3) / (2 * sqrt(7))) / ((0 + 3 / (2 * sqrt(91)) + 4 / sqrt(21)) / 3),
-                                     (3 * sqrt(3) / (2 * sqrt(13))) / (
-                                                 (5 / (2 * sqrt(13)) + 3 / (2 * sqrt(91)) + sqrt(3) / sqrt(13)) / 3),
-                                     (1 / 6) / ((0 + 4 / sqrt(21) + sqrt(3) / sqrt(13)) / 3)])
+        true_modificator = np.array(
+            [(sqrt(3) / 2) / ((0 + 5 / (2 * sqrt(13)) + 0) / 3),
+             (sqrt(3) / (2 * sqrt(7))) / (
+                     (0 + 3 / (2 * sqrt(91)) + 4 / sqrt(21)) / 3),
+             (3 * sqrt(3) / (2 * sqrt(13))) / (
+                     (5 / (2 * sqrt(13)) + 3 / (2 * sqrt(91)) + sqrt(3) / sqrt(
+                         13)) / 3),
+             (1 / 6) / ((0 + 4 / sqrt(21) + sqrt(3) / sqrt(13)) / 3)])
         true_scores = true_numerator / true_denominator * true_modificator
 
         np.testing.assert_allclose(scores, true_scores)
 
     def test_modified_t_score_univariate_filter_small(self):
-        X = np.array([[5, 1, 3, 2], [4, 2, 2, 1], [3, 3, 4, 1], [2, 2, 3, 1], [1, 1, 5, 2]])
+        X = np.array([[5, 1, 3, 2], [4, 2, 2, 1], [3, 3, 4, 1], [2, 2, 3, 1],
+                      [1, 1, 5, 2]])
         y = np.array([1, 1, 2, 2, 2])
 
-        univ_filter = UnivariateFilter('ModifiedTScore', cutting_rule=('K best', 2))
+        univ_filter = UnivariateFilter('ModifiedTScore',
+                                       cutting_rule=('K best', 2))
         univ_filter.fit(X, y)
 
-        assert univ_filter.selected_features == [0, 2]
+        assert univ_filter.selected_features_ == [0, 2]
 
     def test_modified_t_score_univariate_filter_wide(self):
         data, target = self.wide_classification[0], self.wide_classification[1]
@@ -175,8 +175,9 @@ def test_modified_t_score_univariate_filter_wide(self):
             univ_filter = UnivariateFilter('ModifiedTScore', select_k_best(i))
 
             univ_filter.fit(data, target)
-            scores = univ_filter.feature_scores.values()
-            assert all(score >= 0 for score in scores) and all(not np.isnan(score) for score in scores)
+            scores = univ_filter.feature_scores_.values()
+            assert all(score >= 0 for score in scores) and all(
+                not np.isnan(score) for score in scores)
 
             res = univ_filter.transform(data)
             assert i == res.shape[1]
@@ -187,7 +188,7 @@ def test_igain(self):
         y = iris_dataset.target
         X = X.astype(int)
         res = information_gain(X, y)
-        true_res = mutual_info_classif(X, y)
+        true_res = mutual_info_classif(X, y, discrete_features=True)
         np.testing.assert_allclose(res, true_res, rtol=0.12)
 
     def test_mi(self):
@@ -200,20 +201,24 @@ def test_mi(self):
         eq_1 = entropy(X_j) - conditional_entropy(y, X_j)
         eq_2 = entropy(y) - conditional_entropy(X_j, y)
         eq_3 = entropy(X_j) + entropy(y) - entropy(list(zip(X_j, y)))
-        eq_4 = entropy(list(zip(X_j, y))) - conditional_entropy(X_j, y) - conditional_entropy(y, X_j)
+        eq_4 = entropy(list(zip(X_j, y))) - conditional_entropy(X_j,
+                                                                y) - conditional_entropy(
+            y, X_j)
         print(eq_1, eq_2)
         np.testing.assert_allclose(eq_1, eq_2)
         np.testing.assert_allclose(eq_2, eq_3)
         np.testing.assert_allclose(eq_3, eq_4)
 
     def test_gini_index(self):
-        X = np.array([[1, 2, 3], [-1, 2, 3], [1, 2, 2], [-1, 1, 1], [1, 2, 1], [-1, 3, 4]])
+        X = np.array([[1, 2, 3], [-1, 2, 3], [1, 2, 2], [-1, 1, 1], [1, 2, 1],
+                      [-1, 3, 4]])
         y = np.array([1, 2, 1, 3, 1, 1])
         univ_filter = UnivariateFilter('GiniIndex', cutting_rule=('K best', 2))
         univ_filter.fit(X, y)
-        # print(univ_filter.feature_scores)
-        scores = univ_filter.feature_scores.values()
-        assert all(score <= 1 for score in scores) and all(score >= 0 for score in scores)
+        # print(univ_filter.feature_scores_)
+        scores = univ_filter.feature_scores_.values()
+        assert all(score <= 1 for score in scores) and all(
+            score >= 0 for score in scores)
 
     def test_single_class(self):
         X = np.array([[1, 2, 3], [1, 2, 3], [2, 2, 2], [1, 1, 1], [1, 2, 1]])
@@ -227,21 +232,22 @@ def test_fechner(self):
         X = iris_dataset.data
         y = iris_dataset.target
         X = X.astype(int)
-        univ_filter = UnivariateFilter('FechnerCorr', cutting_rule=('K best', 10))
+        univ_filter = UnivariateFilter('FechnerCorr',
+                                       cutting_rule=('K best', 2))
         univ_filter.fit(X, y)
-        # print(univ_filter.selected_features)
-        # univ_filter = UnivariateFilter('PearsonCorr', cutting_rule=('K best', 10))
+        # print(univ_filter.selected_features_)
+        # univ_filter = UnivariateFilter('PearsonCorr', cutting_rule=('K best', 2))
         # univ_filter.fit(X, y)
-        # print(univ_filter.selected_features)
+        # print(univ_filter.selected_features_)
 
     def test_def_cr(self):
         iris_dataset = load_iris()
         X = iris_dataset.data
         y = iris_dataset.target
         X = X.astype(int)
-        univ_filter = UnivariateFilter('FechnerCorr')
+        univ_filter = UnivariateFilter('FechnerCorr', cutting_rule=('K best', 2))
         univ_filter.fit(X, y)
-        assert univ_filter.selected_features == [0, 2, 3]
+        assert univ_filter.selected_features_ == [3, 2]
 
     def test_pipeline(self):
         iris_dataset = load_iris()
@@ -249,74 +255,78 @@ def test_pipeline(self):
         y = iris_dataset.target
         X = X.astype(int)
 
-        pipeline = Pipeline([('FS1', UnivariateFilter('FechnerCorr', ('K best', 2)))])
+        pipeline = Pipeline(
+            [('FS1', UnivariateFilter('FechnerCorr', ('K best', 2)))])
         result = pipeline.fit_transform(X, y)
         assert result.shape[0] == X.shape[0] and result.shape[1] == 2
 
-        p = Pipeline([('FS1', UnivariateFilter('FechnerCorr', ('K best', 2))), ('E1', LogisticRegression())])
+        p = Pipeline([('FS1', UnivariateFilter('FechnerCorr', ('K best', 2))),
+                      ('E1', LogisticRegression())])
         p.fit(X, y)
         assert 0 <= p.score(X, y) <= 1
 
-        p = Pipeline([('FS1', UnivariateFilter('FechnerCorr', ('K best', 3))),
+        p = Pipeline([('FS1', NDFS(3)),
                       ('FS2', UnivariateFilter('PearsonCorr', ('K best', 1)))])
 
         result = p.fit_transform(X, y)
         assert result.shape[0] == X.shape[0] and result.shape[1] == 1
 
         p = Pipeline([('FS1', UnivariateFilter('FechnerCorr', ('K best', 3))),
-                      ('FS2', UnivariateFilter('PearsonCorr', ('K best', 1))), ('E1', LogisticRegression())])
+                      ('FS2', UnivariateFilter('PearsonCorr', ('K best', 1))),
+                      ('E1', LogisticRegression())])
         p.fit(X, y)
         assert 0 <= p.score(X, y) <= 1
 
-    def test_est(self):
-        univ_filter = UnivariateFilter('FechnerCorr', ('K best', 2))
-
-        assert check_estimator(univ_filter)
-
-    def test_qpfs_restrictions(self):
-        iris_dataset = load_iris()
-        X = iris_dataset.data
-        y = iris_dataset.target
-        X = X.astype(int)
-
-        univ_filter = UnivariateFilter(qpfs_filter, GLOB_CR['Best by value'](0.5))
-        self.assertRaises(KeyError, univ_filter.fit, X, y)
-
-        univ_filter = UnivariateFilter(qpfs_filter, GLOB_CR['Worst by value'](0.5))
-        self.assertRaises(KeyError, univ_filter.fit, X, y)
-
-        univ_filter = UnivariateFilter(qpfs_filter, GLOB_CR['Worst by value'](0.5))
-        self.assertRaises(KeyError, univ_filter.fit, X, y)
-
-        univ_filter = UnivariateFilter(qpfs_filter, GLOB_CR['Worst by value'](0.5))
-        self.assertRaises(KeyError, univ_filter.fit, X, y)
-
-        univ_filter = UnivariateFilter(qpfs_filter, GLOB_CR['Worst by value'](0.5))
-        self.assertRaises(KeyError, univ_filter.fit, X, y)
-
-        univ_filter = UnivariateFilter(qpfs_filter, GLOB_CR['Worst by value'](0.5))
-        self.assertRaises(KeyError, univ_filter.fit, X, y)
-
-        univ_filter = UnivariateFilter(qpfs_filter, GLOB_CR['Worst by value'](0.5))
-        self.assertRaises(KeyError, univ_filter.fit, X, y)
+    def test_NDFS(self):
+        # NDFS
+        data, target = self.wide_classification[0], self.wide_classification[1]
+        res = NDFS(10).fit_transform(data, target)
+        assert data.shape[0] == res.shape[0]
+        print("NDFS:", data.shape, '--->', res.shape)
 
-        univ_filter = UnivariateFilter(qpfs_filter, GLOB_CR['Worst by value'](0.5))
-        self.assertRaises(KeyError, univ_filter.fit, X, y)
+    def test_RFS(self):
+        # RFS
+        data, target = self.wide_classification[0], self.wide_classification[1]
+        res = RFS(10).fit_transform(data, target)
+        assert data.shape[0] == res.shape[0]
+        print("RFS:", data.shape, '--->', res.shape)
 
-        UnivariateFilter(qpfs_filter, GLOB_CR['K best'](2))
-        UnivariateFilter(qpfs_filter, GLOB_CR['K worst'](2))
+    def test_SPEC(self):
+        # SPEC
+        data, target = self.wide_classification[0], self.wide_classification[1]
+        res = SPEC(10).fit_transform(data, target)
+        assert data.shape[0] == res.shape[0]
+        print("SPEC:", data.shape, '--->', res.shape)
 
-        UnivariateFilter(qpfs_filter, ('K best', 2))
-        UnivariateFilter(qpfs_filter, ('K worst', 2))
+    def test_est(self):
+        for f in [UnivariateFilter('FechnerCorr', ('K best', 2)), NDFS(2), RFS(2), SPEC(2, gamma=np.square)]:
+            check_estimator(f)
 
-        univ_filter = UnivariateFilter(qpfs_filter, ('K best', 2))
+    def test_qpfs_restrictions(self):
         iris_dataset = load_iris()
         X = iris_dataset.data
         y = iris_dataset.target
-
-        univ_filter.fit(X, y)
-
-        print(univ_filter.selected_features)
+        for cutting_rule in [CR_NAMES['Best by value'](0.5),
+                             CR_NAMES['Worst by value'](0.5),
+                             CR_NAMES['Worst by percentage'](0.5),
+                             CR_NAMES['Best by percentage'](0.5),
+                             ('Worst by value', 0.5), ('Best by value', 0.5),
+                             ('Worst by percentage', 0.2),
+                             ('Best by percentage', 0.2)]:
+            f = UnivariateFilter(qpfs_filter, cutting_rule)
+            self.assertRaises(KeyError, f.fit, X, y)
+
+        for cutting_rule in [CR_NAMES['K best'](2), CR_NAMES['K worst'](2),
+                             ('K best', 2), ('K worst', 2)]:
+            f = UnivariateFilter(qpfs_filter, cutting_rule)
+            f.fit(X, y)
+
+    # def test_mimaga(self):
+    #     data, target = (self.madelon.drop(['target'], axis=1).values,
+    #                     self.madelon["target"].values)
+    #
+    #     filter=MIMAGA(100,500)
+    #     filter.mimaga_filter(data,target)
 
 
 if __name__ == "__main__":
diff --git a/test/univariate_measures_test.py b/test/univariate_measures_test.py
new file mode 100644
index 00000000..25a59b0c
--- /dev/null
+++ b/test/univariate_measures_test.py
@@ -0,0 +1,147 @@
+import unittest
+from ITMO_FS.filters.univariate.measures import *
+from utils import load_dataset
+
+
+class UnivariateMeasuresTest(unittest.TestCase):
+    madelon = load_dataset("madelon.csv")
+
+    def test_measures(self):
+        data = self.madelon.drop(['target'], axis=1).values
+        for f, answer in zip(
+                [su_measure,
+                 laplacian_score],
+                [1,
+                 1,
+                 1]):
+            np.testing.assert_allclose(
+                f(data[0].reshape((-1, 1)), data[0]),
+                answer, atol=1e-05)
+
+    def test_information_gain(self):
+        data = self.madelon.drop(['target'], axis=1).values
+        np.testing.assert_allclose(
+            information_gain(data[:, 0].reshape((-1, 1)), data[:, 0]),
+            0, atol=1e-05)
+
+    def test_pearson_correlation(self):
+        data = self.madelon.drop(['target'], axis=1).values
+        np.testing.assert_allclose(
+            pearson_corr(data[:, 0].reshape((-1, 1)), data[:, 0]),
+            1, atol=1e-05)
+
+    def test_pearson_correlation_1d(self):
+        data = self.madelon.drop(['target'], axis=1).values
+        np.testing.assert_allclose(
+            pearson_corr(data[0], data[0]),
+            1, atol=1e-05)
+
+    def test_spearman_measure(self):
+        data = self.madelon.drop(['target'], axis=1).values
+        np.testing.assert_allclose(
+            spearman_corr(data[:, 0].reshape((-1, 1)), data[:, 0]),
+            1, atol=1e-05)
+
+    def test_spearman_measure_1d(self):
+        data = self.madelon.drop(['target'], axis=1).values
+        np.testing.assert_allclose(
+            spearman_corr(data[:, 0], data[:, 0]),
+            1, atol=1e-05)
+
+    def test_spearman_measure_error(self):
+        with self.assertRaises(ValueError):
+            spearman_corr(np.array([-1]), [-1])
+
+    def test_chi2_measure(self):
+        data = self.madelon.drop(['target'], axis=1).values
+        np.testing.assert_allclose(
+            chi2_measure(data[:, 0].reshape((-1, 1)), data[:, 0]),
+            1, atol=1e-05)
+
+    def test_chi2_measure_error(self):
+        with self.assertRaises(ValueError):
+            chi2_measure(np.array([-1]), [-1])
+
+    def test_gini_index(self):
+        data = self.madelon.drop(['target'], axis=1).values
+        assert gini_index(data[0].reshape((-1, 1)), data[0]), 0
+        with self.assertRaises(ValueError):
+            gini_index(data[0, :1], data[0])
+
+    def test_relief_error(self):
+        data, target = (self.madelon.drop(['target'], axis=1).values,
+                        self.madelon["target"].values)
+        with self.assertRaises(ValueError):
+            relief_measure(data, target[target > 0])
+
+    def test_relief(self):
+        data, target = (self.madelon.drop(['target'], axis=1).values,
+                        self.madelon["target"].values)
+        relief_measure(data, target)
+
+    def test_reliefF_measure(self):
+        data, target = (self.madelon.drop(['target'], axis=1).values,
+                        self.madelon["target"].values)
+        reliefF_measure(data, target)
+
+    def test_laplacian_score(self):
+        data, target = (self.madelon.drop(['target'], axis=1).values,
+                        self.madelon["target"].values)
+        laplacian_score(data, target)
+
+    def test_cutting_rules(self):
+        data = dict(
+            zip(['f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10'],
+                [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]))
+        assert select_k_best(5)(data), ['f10', 'f9', 'f8', 'f7', 'f6']
+        assert select_k_worst(5)(data), ['f1', 'f2', 'f3', 'f4', 'f5']
+
+        with self.assertRaises(TypeError):
+            select_k_best(0.5)(data)
+
+        with self.assertRaises(ValueError):
+            select_k_best(100)(data)
+
+        assert select_best_by_value(5)(data), ['f10', 'f9', 'f8', 'f7', 'f6']
+        assert select_worst_by_value(5)(data), ['f1', 'f2', 'f3', 'f4', 'f5']
+
+        assert select_best_percentage(0.5)(data), ['f10', 'f9', 'f8', 'f7',
+                                                   'f6']
+        assert select_worst_percentage(0.5)(data), ['f1', 'f2', 'f3', 'f4',
+                                                    'f5']
+
+    def test_fit_criterion(self):
+        data, target = (self.madelon.drop(['target'], axis=1).values,
+                        self.madelon["target"].values)
+        fit_criterion_measure(data, target)
+
+    def test_anova(self):
+        data, target = (self.madelon.drop(['target'], axis=1).values,
+                        self.madelon["target"].values)
+        anova(data, target)
+
+    def test_modified_t_score(self):
+        data, target = (self.madelon.drop(['target'], axis=1).values,
+                        self.madelon["target"].values)
+        modified_t_score(data, target)
+
+    def test_f_ratio(self):
+        data, target = (self.madelon.drop(['target'], axis=1).values,
+                        self.madelon["target"].values)
+        f_ratio_measure(data, target)
+
+    def test_kendall(self):
+        data, target = (self.madelon.drop(['target'], axis=1).values,
+                        self.madelon["target"].values)
+        kendall_corr(data[:, 0], target)
+        kendall_corr(data, target)
+
+    def test_fechner(self):
+        data, target = (self.madelon.drop(['target'], axis=1).values,
+                        self.madelon["target"].values)
+        fechner_corr(data[:, 0], target)
+        fechner_corr(data, target)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/test/unsupervised_filters_test.py b/test/unsupervised_filters_test.py
new file mode 100644
index 00000000..887e0de6
--- /dev/null
+++ b/test/unsupervised_filters_test.py
@@ -0,0 +1,66 @@
+import unittest
+
+import pandas as pd
+from sklearn.linear_model import LogisticRegression
+from sklearn.pipeline import Pipeline
+import numpy as np
+
+from sklearn.utils.estimator_checks import check_estimator
+
+from ITMO_FS.filters.unsupervised import *
+from ITMO_FS.filters.univariate import *
+
+np.random.seed(42)
+
+
+class TestCases(unittest.TestCase):  # TODO: add TraceRatioLaplacian tests and tests without target
+    data, target = np.random.randint(10, size=(100, 20)), np.random.randint(10, size=(100,))
+
+    def test_MCFS(self):
+        # MCFS
+        res = MCFS(10).fit_transform(self.data, self.target)
+        assert self.data.shape[0] == res.shape[0]
+        print("MCFS:", self.data.shape, '--->', res.shape)
+
+    def test_UDFS(self):
+        # UDFS
+        res = UDFS(10).fit_transform(self.data, self.target)
+        assert self.data.shape[0] == res.shape[0]
+        print("UDFS:", self.data.shape, '--->', res.shape)
+
+    def test_df(self):
+        for f in [MCFS(10), UDFS(10)]:
+            df = f.fit_transform(pd.DataFrame(self.data), pd.DataFrame(self.target))
+            arr = f.fit_transform(self.data, self.target)
+            np.testing.assert_array_equal(df, arr)
+
+    def test_pipeline(self):
+        # FS
+        p = Pipeline([('FS1', MCFS(10))])
+        p.fit(self.data, self.target)
+        res = p.transform(self.data)
+        assert self.data.shape[0] == res.shape[0] and res.shape[1] == 10
+
+        # FS - estim
+        p = Pipeline([('FS1', UDFS(10)), ('E1', LogisticRegression())])
+        p.fit(self.data, self.target)
+        assert 0 <= p.score(self.data, self.target) <= 1
+
+        # FS - FS
+        p = Pipeline([('FS1', MCFS(10)), ('FS2', UDFS(5))])
+        p.fit(self.data, self.target)
+        res = p.transform(self.data)
+        assert self.data.shape[0] == res.shape[0] and res.shape[1] == 5
+
+        # FS - FS - estim
+        p = Pipeline([('FS1', UDFS(10)), ('FS2', MCFS(5)), ('E1', LogisticRegression())])
+        p.fit(self.data, self.target)
+        assert 0 <= p.score(self.data, self.target) <= 1
+
+    def test_est(self):
+        for f in [MCFS(2), UDFS(2)]:
+            check_estimator(f)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/test/utils.py b/test/utils.py
new file mode 100644
index 00000000..1274e055
--- /dev/null
+++ b/test/utils.py
@@ -0,0 +1,24 @@
+import dvc.api
+import pandas as pd
+
+datasets = ["arcene.csv",
+            "dexter.csv",
+            "dorothea.csv",
+            "gisette.csv",
+            "madelon.csv"]
+
+
+def load_dataset(name):  # todo fails to hold header
+    with dvc.api.open(
+            'test/datasets/' + name) as fd:
+        df = pd.read_csv(fd, header=None)
+        features = ['v' + str(i) for i in range(df.shape[1] - 1)] + ["target"]
+        df.columns = features
+        return df
+
+
+def load_datasets():
+    data = []
+    for d in datasets:
+        data.append(load_dataset(d))
+    return data
diff --git a/test/wrapper_test.py b/test/wrapper_test.py
index e0b0dde1..46665cf5 100644
--- a/test/wrapper_test.py
+++ b/test/wrapper_test.py
@@ -10,10 +10,13 @@
 from sklearn.model_selection import cross_val_score, KFold
 from sklearn.neighbors import KNeighborsClassifier
 from sklearn.svm import SVC
+from sklearn.utils.estimator_checks import check_estimator
+from sklearn.metrics import make_scorer
 
 from ITMO_FS import RecursiveElimination, BackwardSelection, AddDelWrapper, SequentialForwardSelection, \
     HillClimbingWrapper, SimulatedAnnealing, TPhMGWO
 from ITMO_FS.utils.information_theory import *
+from ITMO_FS.utils import test_scorer
 
 np.random.seed(42)
 
@@ -24,71 +27,70 @@ class TestCases(unittest.TestCase):
     wide_regression = make_regression(n_features=2000, n_informative=100)
     tall_regression = make_regression(n_samples=50000, n_features=200, n_informative=50)
 
-    def test_rec_elim(self):
-        classifier = LogisticRegression(max_iter=1000)
-        rec_elimination = RecursiveElimination(classifier, 10)
-        X, y = self.wide_classification
-
-        default_score = cross_val_score(classifier, X, y, cv=5, scoring='f1')
-
-        rec_elimination.fit(X, y)
-        features = rec_elimination.__features__
-        assert len(features) == 10
-
-        wrapper_score = cross_val_score(classifier, X[:, features], y, cv=5, scoring='f1')
-
-        assert all(default_score < wrapper_score)
-
-    def test_back_sel(self):
-        classifier = LogisticRegression(max_iter=1000)
-        back_selection = BackwardSelection(classifier, 10, 'f1')
-        X, y = self.wide_classification
-
-        print('start calculating the default score')
-        default_score = cross_val_score(classifier, X, y, cv=5, scoring='f1')
-        print('finish calculating the default score')
-
-        print('start backward selection')
-        # TODO backward selection works for too long
-        back_selection.fit(X, y)
-        print('finish backward selection')
-
-        features = back_selection.selected_features
-        assert len(features) == 10
-
-        wrapper_score = cross_val_score(classifier, X[:, features], y, cv=5, scoring='f1')
-
-        assert all(default_score < wrapper_score)
-
-    def test_add_del_wrapper(self):
-        classifier = LogisticRegression(max_iter=1000)
-        add_del_wrapper = AddDelWrapper(classifier, f1_score)
-        X, y = self.wide_classification
-
-        default_score = cross_val_score(classifier, X, y, cv=5, scoring='f1')
-
-        add_del_wrapper.fit(X, y)
-        features = add_del_wrapper.__features__
-        assert len(features) == 10
-
-        wrapper_score = cross_val_score(classifier, X[:, features], y, cv=5, scoring='f1')
-
-        assert all(default_score < wrapper_score)
-
-    def test_seq_forw_sel(self):
-        classifier = LogisticRegression(max_iter=1000)
-        seq_forw_sel = SequentialForwardSelection(classifier, 10, 'f1')
-        X, y = self.wide_classification
-
-        default_score = cross_val_score(classifier, X, y, cv=5, scoring='f1')
-
-        seq_forw_sel.fit(X, y)
-        features = seq_forw_sel.selected_features
-        assert len(features) == 10
+    # def test_rec_elim(self):
+    #     classifier = LogisticRegression(max_iter=1000)
+    #     rec_elimination = RecursiveElimination(classifier, 10, 'f1')
+    #     X, y = self.wide_classification
+    #
+    #     default_score = cross_val_score(classifier, X, y, cv=5, scoring='f1').mean()
+    #
+    #     rec_elimination.fit(X, y)
+    #     features = rec_elimination.selected_features_
+    #     assert len(features) == 10
+    #
+    #     wrapper_score = cross_val_score(classifier, X[:, features], y, cv=5, scoring='f1').mean()
+    #
+    #     assert default_score < wrapper_score
 
-        wrapper_score = cross_val_score(classifier, X[:, features], y, cv=5, scoring='f1')
+    # def test_back_sel(self):
+    #     classifier = LogisticRegression(max_iter=1000)
+    #     back_selection = BackwardSelection(classifier, 10, 'f1')
+    #     X, y = self.wide_classification
+    #
+    #     print('start calculating the default score')
+    #     default_score = cross_val_score(classifier, X, y, cv=5, scoring='f1').mean()
+    #     print('finish calculating the default score')
+    #
+    #     print('start backward selection')
+    #     # TODO backward selection works for too long
+    #     back_selection.fit(X, y)
+    #     print('finish backward selection')
+    #
+    #     features = back_selection.selected_features_
+    #     assert len(features) == 10
+    #
+    #     wrapper_score = cross_val_score(classifier, X[:, features], y, cv=5, scoring='f1').mean()
+    #
+    #     assert default_score < wrapper_score
 
-        assert all(default_score < wrapper_score)
+    # def test_add_del_wrapper(self):
+    #     classifier = LogisticRegression(max_iter=1000)
+    #     add_del_wrapper = AddDelWrapper(classifier, f1_score)
+    #     X, y = self.wide_classification
+    #
+    #     default_score = cross_val_score(classifier, X, y, cv=5, scoring='f1').mean()
+    #
+    #     add_del_wrapper.fit(X, y)
+    #     features = add_del_wrapper.selected_features_
+    #
+    #     wrapper_score = cross_val_score(classifier, X[:, features], y, cv=5, scoring='f1').mean()
+    #
+    #     assert default_score < wrapper_score
+    #
+    # def test_seq_forw_sel(self):
+    #     classifier = LogisticRegression(max_iter=1000)
+    #     seq_forw_sel = SequentialForwardSelection(classifier, 10, 'f1')
+    #     X, y = self.wide_classification
+    #
+    #     default_score = cross_val_score(classifier, X, y, cv=5, scoring='f1').mean()
+    #
+    #     seq_forw_sel.fit(X, y)
+    #     features = seq_forw_sel.selected_features_
+    #     assert len(features) == 10
+    #
+    #     wrapper_score = cross_val_score(classifier, X[:, features], y, cv=5, scoring='f1').mean()
+    #
+    #     assert default_score < wrapper_score
 
     # def test_qpfs_wrapper(self):
     #     classifier = LogisticRegression(max_iter=1000)
@@ -105,54 +107,57 @@ def test_seq_forw_sel(self):
     #
     #     assert all(default_score < wrapper_score)
 
-    def test_hill_climbing(self):
-        classifier = LogisticRegression(max_iter=1000)
-        hill_climbing = HillClimbingWrapper(classifier, f1_score)
-        X, y = self.wide_classification
-
-        default_score = cross_val_score(classifier, X, y, cv=5, scoring='f1')
-
-        hill_climbing.fit(X, y)
-        features = hill_climbing.features
-        # assert len(features) == 10
-
-        wrapper_score = cross_val_score(classifier, X[:, features], y, cv=5, scoring='f1')
-
-        assert all(default_score < wrapper_score)
-
-    def test_sim_annealing(self):
-        classifier = LogisticRegression(max_iter=1000)
-        sim_annealing = SimulatedAnnealing(classifier, f1_score)
-        X, y = self.wide_classification
-
-        kf = KFold(n_splits=2)
-        sa = SimulatedAnnealing(classifier, f1_score)
-
-        for train_index, test_index in kf.split(X):
-            classifier.fit(X[train_index], y[train_index])
-            default_score = f1_score(y[test_index], classifier.predict(X[test_index]))
-
-            sim_annealing.fit(X[train_index], y[train_index], X[test_index], y[test_index])
-            features = sim_annealing.selected_features
-            # assert len(features) == 10
-
-            classifier.fit(X[train_index][:, features], y[train_index])
-            wrapper_score = f1_score(y[test_index], classifier.predict(X[test_index][:, features]))
-
-            assert default_score < wrapper_score
-
-    def test_wolves(self):
-        classifier = LogisticRegression(max_iter=1000)
-        tphmgwo = TPhMGWO()
-        X, y = self.wide_classification
-
-        default_score = cross_val_score(classifier, X, y, cv=5, scoring='f1')
-
-        tphmgwo.run(X, y)
-        features = tphmgwo.solution
-        # assert len(features) == 10
-
-        wrapper_score = cross_val_score(classifier, X[:, features], y, cv=5, scoring='f1')
-
-        assert all(default_score < wrapper_score)
+    # def test_hill_climbing(self):
+    #     classifier = LogisticRegression(max_iter=1000)
+    #     hill_climbing = HillClimbingWrapper(classifier, f1_score)
+    #     X, y = self.wide_classification
+    #
+    #     default_score = cross_val_score(classifier, X, y, cv=5, scoring='f1').mean()
+    #
+    #     hill_climbing.fit(X, y)
+    #     features = hill_climbing.selected_features_
+    #     # assert len(features) == 10
+    #
+    #     wrapper_score = cross_val_score(classifier, X[:, features], y, cv=5, scoring='f1').mean()
+    #
+    #     assert default_score < wrapper_score
+    #
+    # def test_sim_annealing(self):
+    #     classifier = LogisticRegression(max_iter=1000)
+    #     sim_annealing = SimulatedAnnealing(classifier, f1_score)
+    #     X, y = self.wide_classification
+    #
+    #     sim_annealing.fit(X, y)
+    #     default_score = cross_val_score(classifier, X, y, cv=5, scoring='f1').mean()
+    #
+    #     features = sim_annealing.selected_features_
+    #     # assert len(features) == 10
+    #
+    #     wrapper_score = cross_val_score(classifier, X[:, features], y, cv=5, scoring='f1').mean()
+    #
+    #     assert default_score < wrapper_score
+    #
+    # def test_wolves(self):
+    #     classifier = LogisticRegression(max_iter=1000)
+    #     tphmgwo = TPhMGWO()
+    #     X, y = self.wide_classification
+    #
+    #     default_score = cross_val_score(classifier, X, y, cv=5, scoring='f1').mean()
+    #
+    #     tphmgwo.run(X, y)
+    #     features = tphmgwo.selected_features_
+    #     # assert len(features) == 10
+    #
+    #     wrapper_score = cross_val_score(classifier, X[:, features], y, cv=5, scoring='f1').mean()
+    #
+    #     assert default_score < wrapper_score
+    #
+    # def test_est(self):
+    #     classifier = LogisticRegression(max_iter=1000)
+    #     for f in [RecursiveElimination(classifier, 2, make_scorer(test_scorer)), BackwardSelection(classifier, 2, make_scorer(test_scorer)),
+    #     AddDelWrapper(classifier, test_scorer), SequentialForwardSelection(classifier, 2, make_scorer(test_scorer)),
+    #     HillClimbingWrapper(classifier, test_scorer), SimulatedAnnealing(classifier, test_scorer), TPhMGWO()]:
+    #         check_estimator(f)
 
+if __name__ == "__main__":
+    unittest.main()