Repository: scikit-learn/scikit-learn
Pull request: [MRG] Fixed n_support_ attr for OneClassSVM and SVR
Closes https://github.com/scikit-learn/scikit-learn/pull/14799
Closes https://github.com/scikit-learn/scikit-learn/pull/15034

Fixes https://github.com/scikit-learn/scikit-learn/issues/14774

@amueller I think this is enough?

~~(That doesn't fix the nonsense that the attribute has size 2 (i.e. 2 classes) for OneClass and SVR but that's a different issue)~~  It does now

Problem statement:
OneClassSVM n_support_ returns incorrect value 
#### Description
 Example: OneClassSVM n_support returns incorrect values> 0

#### Steps/Code to Reproduce

Example:
```python
from sklearn.svm import OneClassSVM

X = [[0], [0.44], [0.45], [0.46], [1]]
clf = OneClassSVM().fit(X)
clf.n_support_
```

#### Expected Results
array with one integer

#### Actual Results
array([-206807307, 1070878112])

#### Versions
System:
python: 3.7.3 (default, Mar 27 2019, 17:13:21) [MSC v.1915 64 bit (AMD64)]
executable: C:\Users\franc\Anaconda3\python.exe
machine: Windows-10-10.0.17763-SP0

BLAS:
macros: SCIPY_MKL_H=None, HAVE_CBLAS=None
lib_dirs: C:/Users/franc/Anaconda3\Library\lib
cblas_libs: mkl_rt

Python deps:
pip: 19.0.3
setuptools: 40.8.0
sklearn: 0.20.1
numpy: 1.15.4
scipy: 1.1.0
Cython: 0.29.6
pandas: 0.24.2

@FranciDona

Hints from issue discussion:
you forgot one line in your reproduction script, I think?
doesn't look like ``random_state`` exists any more also ;)
https://scikit-learn.org/dev/modules/generated/sklearn.svm.OneClassSVM.html#sklearn.svm.OneClassSV
oops, edited 
@eugeniaft i'm just being picky ;)
I'm working on this issue. WiMLDS sprint.
same issue for ``SVR``
so in ``copy_nSV`` we just ``return`` in these cases (``labels`` is ``NULL``) and we're returning uninitialized memory, when we should return ``model->l`` instead.
lol it says
```
# TODO: do only in classification
```
(and the whole room looked at me for getting a laughing fit about a 9 year old todo where we're returning uninitialized memory)

Commit message: Fixed size

Patch to review:
diff --git a/doc/whats_new/v0.22.rst b/doc/whats_new/v0.22.rst
index 24056ad94486a..b1679c670e918 100644
--- a/doc/whats_new/v0.22.rst
+++ b/doc/whats_new/v0.22.rst
@@ -532,6 +532,10 @@ Changelog
   Now in these cases, fit() will fail with an Exception.
   :pr:`14286` by :user:`Alex Shacked <alexshacked>`.
 
+- |Fix| The `n_support_` attribute of :class:`svm.SVR` and
+  :class:`svm.OneClassSVM` was previously non-initialized, and had size 2. It
+  has now size 1 with the correct value. :pr:`15099` by `Nicolas Hug`_.
+
 :mod:`sklearn.tree`
 ...................
 
diff --git a/sklearn/svm/base.py b/sklearn/svm/base.py
index a0459708f3288..5dcf529d6b64f 100644
--- a/sklearn/svm/base.py
+++ b/sklearn/svm/base.py
@@ -244,7 +244,7 @@ def _dense_fit(self, X, y, sample_weight, solver_type, kernel,
 
         # we don't pass **self.get_params() to allow subclasses to
         # add other parameters to __init__
-        self.support_, self.support_vectors_, self.n_support_, \
+        self.support_, self.support_vectors_, self._n_support, \
             self.dual_coef_, self.intercept_, self.probA_, \
             self.probB_, self.fit_status_ = libsvm.fit(
                 X, y,
@@ -268,7 +268,7 @@ def _sparse_fit(self, X, y, sample_weight, solver_type, kernel,
         libsvm_sparse.set_verbosity_wrap(self.verbose)
 
         self.support_, self.support_vectors_, dual_coef_data, \
-            self.intercept_, self.n_support_, \
+            self.intercept_, self._n_support, \
             self.probA_, self.probB_, self.fit_status_ = \
             libsvm_sparse.libsvm_sparse_train(
                 X.shape[1], X.data, X.indices, X.indptr, y, solver_type,
@@ -328,7 +328,7 @@ def _dense_predict(self, X):
         svm_type = LIBSVM_IMPL.index(self._impl)
 
         return libsvm.predict(
-            X, self.support_, self.support_vectors_, self.n_support_,
+            X, self.support_, self.support_vectors_, self._n_support,
             self._dual_coef_, self._intercept_,
             self.probA_, self.probB_, svm_type=svm_type, kernel=kernel,
             degree=self.degree, coef0=self.coef0, gamma=self._gamma,
@@ -354,7 +354,7 @@ def _sparse_predict(self, X):
             self.degree, self._gamma, self.coef0, self.tol,
             C, self.class_weight_,
             self.nu, self.epsilon, self.shrinking,
-            self.probability, self.n_support_,
+            self.probability, self._n_support,
             self.probA_, self.probB_)
 
     def _compute_kernel(self, X):
@@ -407,7 +407,7 @@ def _dense_decision_function(self, X):
             kernel = 'precomputed'
 
         return libsvm.decision_function(
-            X, self.support_, self.support_vectors_, self.n_support_,
+            X, self.support_, self.support_vectors_, self._n_support,
             self._dual_coef_, self._intercept_,
             self.probA_, self.probB_,
             svm_type=LIBSVM_IMPL.index(self._impl),
@@ -433,7 +433,7 @@ def _sparse_decision_function(self, X):
             self.degree, self._gamma, self.coef0, self.tol,
             self.C, self.class_weight_,
             self.nu, self.epsilon, self.shrinking,
-            self.probability, self.n_support_,
+            self.probability, self._n_support,
             self.probA_, self.probB_)
 
     def _validate_for_predict(self, X):
@@ -484,6 +484,17 @@ def coef_(self):
     def _get_coef(self):
         return safe_sparse_dot(self._dual_coef_, self.support_vectors_)
 
+    @property
+    def n_support_(self):
+        check_is_fitted(self)
+        svm_type = LIBSVM_IMPL.index(self._impl)
+        if svm_type in (0, 1):
+            return self._n_support
+        else:
+            # SVR and OneClass
+            # _n_support has size 2, we make it size 1
+            return np.array([self._n_support[0]])
+
 
 class BaseSVC(ClassifierMixin, BaseLibSVM, metaclass=ABCMeta):
     """ABC for LibSVM-based classifiers."""
@@ -668,7 +679,7 @@ def _dense_predict_proba(self, X):
 
         svm_type = LIBSVM_IMPL.index(self._impl)
         pprob = libsvm.predict_proba(
-            X, self.support_, self.support_vectors_, self.n_support_,
+            X, self.support_, self.support_vectors_, self._n_support,
             self._dual_coef_, self._intercept_,
             self.probA_, self.probB_,
             svm_type=svm_type, kernel=kernel, degree=self.degree,
@@ -695,7 +706,7 @@ def _sparse_predict_proba(self, X):
             self.degree, self._gamma, self.coef0, self.tol,
             self.C, self.class_weight_,
             self.nu, self.epsilon, self.shrinking,
-            self.probability, self.n_support_,
+            self.probability, self._n_support,
             self.probA_, self.probB_)
 
     def _get_coef(self):
@@ -704,7 +715,7 @@ def _get_coef(self):
             coef = safe_sparse_dot(self.dual_coef_, self.support_vectors_)
         else:
             # 1vs1 classifier
-            coef = _one_vs_one_coef(self.dual_coef_, self.n_support_,
+            coef = _one_vs_one_coef(self.dual_coef_, self._n_support,
                                     self.support_vectors_)
             if sp.issparse(coef[0]):
                 coef = sp.vstack(coef).tocsr()
diff --git a/sklearn/svm/libsvm.pyx b/sklearn/svm/libsvm.pyx
index ded79a1f8318c..9e24192a31127 100644
--- a/sklearn/svm/libsvm.pyx
+++ b/sklearn/svm/libsvm.pyx
@@ -217,10 +217,13 @@ def fit(
         support_vectors = np.empty((SV_len, X.shape[1]), dtype=np.float64)
         copy_SV(support_vectors.data, model, support_vectors.shape)
 
-    # TODO: do only in classification
     cdef np.ndarray[np.int32_t, ndim=1, mode='c'] n_class_SV
-    n_class_SV = np.empty(n_class, dtype=np.int32)
-    copy_nSV(n_class_SV.data, model)
+    if (svm_type == 0 or svm_type == 1):
+        n_class_SV = np.empty(n_class, dtype=np.int32)
+        copy_nSV(n_class_SV.data, model)
+    else:
+        # OneClass and SVR are considered to have 2 classes
+        n_class_SV = np.array([SV_len, SV_len], dtype=np.int32)
 
     cdef np.ndarray[np.float64_t, ndim=1, mode='c'] probA
     cdef np.ndarray[np.float64_t, ndim=1, mode='c'] probB
diff --git a/sklearn/svm/tests/test_svm.py b/sklearn/svm/tests/test_svm.py
index 91f6fa10f758c..da1ed54676771 100644
--- a/sklearn/svm/tests/test_svm.py
+++ b/sklearn/svm/tests/test_svm.py
@@ -1188,3 +1188,19 @@ def test_gamma_scale():
     # gamma is not explicitly set.
     X, y = [[1, 2], [3, 2 * np.sqrt(6) / 3 + 2]], [0, 1]
     assert_no_warnings(clf.fit, X, y)
+
+
+def test_n_support_oneclass_svr():
+    # Make sure n_support is not zero for oneclass and SVR
+    # non regression test for issue #14774
+    X = np.array([[0], [0.44], [0.45], [0.46], [1]])
+    clf = svm.OneClassSVM().fit(X)
+    assert clf.n_support_ == clf.support_vectors_.shape[0]
+    assert clf.n_support_.size == 1
+    assert clf.n_support_ == 3
+
+    y = np.arange(X.shape[0])
+    reg = svm.SVR().fit(X, y)
+    assert reg.n_support_ == reg.support_vectors_.shape[0]
+    assert reg.n_support_.size == 1
+    assert reg.n_support_ == 4


Code review comments:
diff_hunk: @@ -484,6 +484,17 @@ def coef_(self):
     def _get_coef(self):
         return safe_sparse_dot(self._dual_coef_, self.support_vectors_)
 
+    @property
+    def n_support_(self):
+        check_is_fitted(self)
path: sklearn/svm/base.py
text: @user2:
I think a `@user1` raising a `NotFittedError` is kinda odd. Rather have an `AttributeError` here.