"""Class to perform under-sampling by removing Tomek's links."""
# Authors: Guillaume Lemaitre <g.lemaitre58@gmail.com>
# Fernando Nogueira
# Christos Aridas
# License: MIT
from __future__ import division, print_function
import numpy as np
from sklearn.neighbors import NearestNeighbors
from ..base import BaseCleaningSampler
[docs]class TomekLinks(BaseCleaningSampler):
"""Class to perform under-sampling by removing Tomek's links.
Parameters
----------
ratio : str, dict, or callable, optional (default='auto')
Ratio to use for resampling the data set.
- If ``str``, has to be one of: (i) ``'minority'``: resample the
minority class; (ii) ``'majority'``: resample the majority class,
(iii) ``'not minority'``: resample all classes apart of the minority
class, (iv) ``'all'``: resample all classes, and (v) ``'auto'``:
correspond to ``'all'`` with for over-sampling methods and ``'not
minority'`` for under-sampling methods. The classes targeted will be
over-sampled or under-sampled to achieve an equal number of sample
with the majority or minority class.
- If ``dict``, the keys correspond to the targeted classes. The values
correspond to the desired number of samples.
- If callable, function taking ``y`` and returns a ``dict``. The keys
correspond to the targeted classes. The values correspond to the
desired number of samples.
return_indices : bool, optional (default=False)
Whether or not to return the indices of the samples randomly
selected from the majority class.
random_state : int, RandomState instance or None, optional (default=None)
If int, ``random_state`` is the seed used by the random number
generator; If ``RandomState`` instance, random_state is the random
number generator; If ``None``, the random number generator is the
``RandomState`` instance used by ``np.random``.
n_jobs : int, optional (default=1)
The number of threads to open if possible.
Notes
-----
This method is based on [1]_.
Supports mutli-class resampling.
Examples
--------
>>> from collections import Counter
>>> from sklearn.datasets import make_classification
>>> from imblearn.under_sampling import \
TomekLinks # doctest: +NORMALIZE_WHITESPACE
>>> X, y = make_classification(n_classes=2, class_sep=2,
... weights=[0.1, 0.9], n_informative=3, n_redundant=1, flip_y=0,
... n_features=20, n_clusters_per_class=1, n_samples=1000, random_state=10)
>>> print('Original dataset shape {}'.format(Counter(y)))
Original dataset shape Counter({1: 900, 0: 100})
>>> tl = TomekLinks(random_state=42)
>>> X_res, y_res = tl.fit_sample(X, y)
>>> print('Resampled dataset shape {}'.format(Counter(y_res)))
Resampled dataset shape Counter({1: 897, 0: 100})
References
----------
.. [1] I. Tomek, "Two modifications of CNN," In Systems, Man, and
Cybernetics, IEEE Transactions on, vol. 6, pp 769-772, 2010.
"""
[docs] def __init__(self, ratio='auto', return_indices=False,
random_state=None, n_jobs=1):
super(TomekLinks, self).__init__(ratio=ratio,
random_state=random_state)
self.return_indices = return_indices
self.n_jobs = n_jobs
@staticmethod
[docs] def is_tomek(y, nn_index, class_type):
"""is_tomek uses the target vector and the first neighbour of every
sample point and looks for Tomek pairs. Returning a boolean vector with
True for majority Tomek links.
Parameters
----------
y : ndarray, shape (n_samples, )
Target vector of the data set, necessary to keep track of whether a
sample belongs to minority or not
nn_index : ndarray, shape (len(y), )
The index of the closes nearest neighbour to a sample point.
class_type : int or str
The label of the minority class.
Returns
-------
is_tomek : ndarray, shape (len(y), )
Boolean vector on len( # samples ), with True for majority samples
that are Tomek links.
"""
links = np.zeros(len(y), dtype=bool)
# find which class to not consider
class_excluded = [c for c in np.unique(y) if c not in class_type]
# there is a Tomek link between two samples if they are both nearest
# neighbors of each others.
for index_sample, target_sample in enumerate(y):
if target_sample in class_excluded:
continue
if y[nn_index[index_sample]] != target_sample:
if nn_index[nn_index[index_sample]] == index_sample:
links[index_sample] = True
return links
def _sample(self, X, y):
"""Resample the dataset.
Parameters
----------
X : ndarray, shape (n_samples, n_features)
Matrix containing the data which have to be sampled.
y : ndarray, shape (n_samples, )
Corresponding label for each sample in X.
Returns
-------
X_resampled : ndarray, shape (n_samples_new, n_features)
The array containing the resampled data.
y_resampled : ndarray, shape (n_samples_new)
The corresponding label of `X_resampled`
idx_under : ndarray, shape (n_samples, )
If `return_indices` is `True`, a boolean array will be returned
containing the which samples have been selected.
"""
# Find the nearest neighbour of every point
nn = NearestNeighbors(n_neighbors=2, n_jobs=self.n_jobs)
nn.fit(X)
nns = nn.kneighbors(X, return_distance=False)[:, 1]
links = self.is_tomek(y, nns, self.ratio_)
if self.return_indices:
return (X[np.logical_not(links)], y[np.logical_not(links)],
np.flatnonzero(np.logical_not(links)))
else:
return X[np.logical_not(links)], y[np.logical_not(links)]