import itertools

import numpy as np
from sklearn.neighbors import NearestNeighbors
from sklearn.externals.joblib import delayed, Parallel
from sklearn.utils import gen_even_slices

from .base import BaseOutlierDetector

__all__ = ['FastABOD']


def _abof(X, X_train, neigh_ind):
    with np.errstate(invalid='raise'):
        return np.var([
            [
                (diff_a @ diff_b) / (diff_a @ diff_a) / (diff_b @ diff_b)
                for diff_a, diff_b in itertools.combinations(
                    X_neigh - query_point, 2
                )
            ] for query_point, X_neigh in zip(X, X_train[neigh_ind])
        ], axis=1)


class FastABOD(BaseOutlierDetector):
    """Fast Angle-Based Outlier Detector (FastABOD).

    Parameters
    ----------
    algorithm : str, default 'auto'
        Tree algorithm to use. Valid algorithms are
        ['kd_tree'|'ball_tree'|'auto'].

    contamination : float, default 0.1
        Proportion of outliers in the data set. Used to define the threshold.

    leaf_size : int, default 30
        Leaf size of the underlying tree.

    metric : str or callable, default 'minkowski'
        Distance metric to use.

    novelty : bool, default False
        By default, FastABOD is only meant to be used for outlier detection.
        Set novelty to True if you want to use FastABOD for novelty detection.
        In this case be aware that that you should only use predict,
        decision_function and anomaly_score on new unseen data and not on the
        training data.

    n_jobs : int, default 1
        Number of jobs to run in parallel. If -1, then the number of jobs is
        set to the number of CPU cores.

    n_neighbors : int, default 20
        Number of neighbors.

    p : int, default 2
        Power parameter for the Minkowski metric.

    verbose : bool, default False
        Enable verbose output.

    metric_params : dict, default None
        Additioal parameters passed to the requested metric.

    Attributes
    ----------
    anomaly_score_ : array-like of shape (n_samples,)
        Anomaly score for each training data.

    fit_time_ : float
        Time spent for fitting in seconds.

    threshold_ : float
        Threshold.

    X_ : array-like of shape (n_samples, n_features)
        Training data.

    References
    ----------
    H.-P. Kriegel, M. Schubert and A. Zimek,
    "Angle-based outlier detection in high-dimensional data,"
    In Proceedings of SIGKDD'08, pp. 444-452, 2008.

    H.-P. Kriegel, P. Kroger, E. Schubert and A. Zimek,
    "Interpreting and unifying outlier scores,"
    In Proceedings of SDM'11, pp. 13-24, 2011.
    """

    @property
    def X_(self):
        return self._estimator._fit_X

    def __init__(
        self, algorithm='auto', contamination=0.1, leaf_size=30,
        metric='minkowski', novelty=False, n_jobs=1, n_neighbors=20,
        p=2, verbose=False, metric_params=None
    ):
        super().__init__(contamination=contamination, verbose=verbose)

        self.algorithm     = algorithm
        self.leaf_size     = leaf_size
        self.metric        = metric
        self.novelty       = novelty
        self.n_jobs        = n_jobs
        self.n_neighbors   = n_neighbors
        self.p             = p
        self.metric_params = metric_params

    def _fit(self, X):
        self._estimator   = NearestNeighbors(
            algorithm     = self.algorithm,
            leaf_size     = self.leaf_size,
            metric        = self.metric,
            n_jobs        = self.n_jobs,
            n_neighbors   = self.n_neighbors,
            p             = self.p,
            metric_params = self.metric_params
        ).fit(X)
        self._abof_max    = np.max(self._anomaly_score(X, regularize=False))

        return self

    def _anomaly_score(self, X, regularize=True):
        abof = self._abof(X)

        if regularize:
            return -np.log(abof / self._abof_max)
        else:
            return abof

    def _abof(self, X):
        """Compute the Angle-Based Outlier Factor (ABOF) for each sample."""

        if X is self.X_:
            neigh_ind = self._estimator.kneighbors(return_distance=False)
        else:
            neigh_ind = self._estimator.kneighbors(X, return_distance=False)

        n_samples, _  = X.shape
        result        = Parallel(n_jobs=self.n_jobs)(
            delayed(_abof)(
                X[s], self.X_, neigh_ind[s]
            ) for s in gen_even_slices(n_samples, self.n_jobs)
        )

        return np.concatenate(result)

@@ -1,14 +1,12 @@
-import
+from
  itertoo
@@ -7,16 +7,36 @@
 tertools
+ import combinations
 %0A%0Aimport
@@ -99,567 +99,70 @@
 ors%0A
-from sklearn.externals.joblib import delayed, Parallel%0Afrom sklearn.utils import gen_even_slices%0A%0Afrom .base import BaseOutlierDetector%0A%0A__all__ = %5B'FastABOD'%5D%0A%0A%0Adef _abof(X, X_train, neigh_ind):%0A    with np.errstate(invalid='raise'):%0A        return np.var(%5B%0A            %5B%0A                (diff_a @ diff_b) / (diff_a @ diff_a) / (diff_b @ diff_b)%0A                for diff_a, diff_b in itertools.combinations(%0A                    X_neigh - query_point, 2%0A                )%0A            %5D for query_point, X_neigh in zip(X, X_train%5Bneigh_ind%5D)%0A        %5D, axis=1)
+%0Afrom .base import BaseOutlierDetector%0A%0A__all__ = %5B'FastABOD'%5D
 %0A%0A%0Ac
@@ -732,211 +732,24 @@
     
-By default, FastABOD is only meant to be used for outlier detection.%0A        Set novelty to
+If
  True
- if
+,
  you 
-want to use FastABOD for novelty detection.%0A        In this case be aware that that you should only
+can
  use
@@ -757,24 +757,16 @@
 predict,
-%0A       
  decisio
@@ -796,16 +796,24 @@
 score on
+%0A       
  new uns
@@ -835,24 +835,16 @@
 t on the
-%0A       
  trainin
@@ -3603,31 +3603,37 @@
     
-n_samples, _  = X.shape
+return np.var(%5B%0A            %5B
 %0A   
@@ -3641,22 +3641,16 @@
     
-result
         
 = Pa
@@ -3649,220 +3649,217 @@
     
-= Parallel(n_jobs=self.n_jobs)(%0A            delayed(_abof)(%0A                X%5Bs%5D, self.X_, neigh_ind%5Bs%5D%0A            ) for s in gen_even_slices(n_samples, self.n_jobs)%0A        )%0A%0A        return np.concatenate(result
+(pa @ pb) / (pa @ pa) / (pb @ pb) for pa, pb in combinations(%0A                    X_neigh - query_point, 2%0A                )%0A            %5D for query_point, X_neigh in zip(X, self.X_%5Bneigh_ind%5D)%0A        %5D, axis=1
 )%0A

fix _anomaly_score method