Repository: BayesWitnesses/m2cgen (language: rust)

Issue:
Add support for OneClassSVM
https://scikit-learn.org/stable/modules/generated/sklearn.svm.OneClassSVM.html


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index a2bba683..0a1aa586 100644
--- a/README.md
+++ b/README.md
@@ -47,7 +47,7 @@ pip install m2cgen
 |  | Classification | Regression |
 | --- | --- | --- |
 | **Linear** | <ul><li>scikit-learn<ul><li>LogisticRegression</li><li>LogisticRegressionCV</li><li>PassiveAggressiveClassifier</li><li>Perceptron</li><li>RidgeClassifier</li><li>RidgeClassifierCV</li><li>SGDClassifier</li></ul></li><li>lightning<ul><li>AdaGradClassifier</li><li>CDClassifier</li><li>FistaClassifier</li><li>SAGAClassifier</li><li>SAGClassifier</li><li>SDCAClassifier</li><li>SGDClassifier</li></ul></li></ul> | <ul><li>scikit-learn<ul><li>ARDRegression</li><li>BayesianRidge</li><li>ElasticNet</li><li>ElasticNetCV</li><li>GammaRegressor</li><li>HuberRegressor</li><li>Lars</li><li>LarsCV</li><li>Lasso</li><li>LassoCV</li><li>LassoLars</li><li>LassoLarsCV</li><li>LassoLarsIC</li><li>LinearRegression</li><li>OrthogonalMatchingPursuit</li><li>OrthogonalMatchingPursuitCV</li><li>PassiveAggressiveRegressor</li><li>PoissonRegressor</li><li>RANSACRegressor(only supported regression estimators can be used as a base estimator)</li><li>Ridge</li><li>RidgeCV</li><li>SGDRegressor</li><li>TheilSenRegressor</li><li>TweedieRegressor</li></ul><li>StatsModels<ul><li>Generalized Least Squares (GLS)</li><li>Generalized Least Squares with AR Errors (GLSAR)</li><li>Generalized Linear Models (GLM)</li><li>Ordinary Least Squares (OLS)</li><li>[Gaussian] Process Regression Using Maximum Likelihood-based Estimation (ProcessMLE)</li><li>Quantile Regression (QuantReg)</li><li>Weighted Least Squares (WLS)</li></ul><li>lightning<ul><li>AdaGradRegressor</li><li>CDRegressor</li><li>FistaRegressor</li><li>SAGARegressor</li><li>SAGRegressor</li><li>SDCARegressor</li><li>SGDRegressor</li></ul></li></ul> |
-| **SVM** | <ul><li>scikit-learn<ul><li>LinearSVC</li><li>NuSVC</li><li>SVC</li></ul></li><li>lightning<ul><li>KernelSVC</li><li>LinearSVC</li></ul></li></ul> | <ul><li>scikit-learn<ul><li>LinearSVR</li><li>NuSVR</li><li>SVR</li></ul></li><li>lightning<ul><li>LinearSVR</li></ul></li></ul> |
+| **SVM** | <ul><li>scikit-learn<ul><li>LinearSVC</li><li>NuSVC</li><li>OneClassSVM</li><li>SVC</li></ul></li><li>lightning<ul><li>KernelSVC</li><li>LinearSVC</li></ul></li></ul> | <ul><li>scikit-learn<ul><li>LinearSVR</li><li>NuSVR</li><li>SVR</li></ul></li><li>lightning<ul><li>LinearSVR</li></ul></li></ul> |
 | **Tree** | <ul><li>DecisionTreeClassifier</li><li>ExtraTreeClassifier</li></ul> | <ul><li>DecisionTreeRegressor</li><li>ExtraTreeRegressor</li></ul> |
 | **Random Forest** | <ul><li>ExtraTreesClassifier</li><li>LGBMClassifier(rf booster only)</li><li>RandomForestClassifier</li><li>XGBRFClassifier</li></ul> | <ul><li>ExtraTreesRegressor</li><li>LGBMRegressor(rf booster only)</li><li>RandomForestRegressor</li><li>XGBRFRegressor</li></ul> |
 | **Boosting** | <ul><li>LGBMClassifier(gbdt/dart/goss booster only)</li><li>XGBClassifier(gbtree(including boosted forests)/gblinear booster only)</li><ul> | <ul><li>LGBMRegressor(gbdt/dart/goss booster only)</li><li>XGBRegressor(gbtree(including boosted forests)/gblinear booster only)</li></ul> |
@@ -65,6 +65,8 @@ Vector value; signed distance of the sample to the hyperplane per each class.
 The output is consistent with the output of ```LinearClassifierMixin.decision_function```.
 
 ### SVM
+#### Outlier detection
+Scalar value; signed distance of the sample to the separating hyperplane: positive for an inlier and negative for an outlier.
 #### Binary
 Scalar value; signed distance of the sample to the hyperplane for the second class.
 #### Multiclass
diff --git a/m2cgen/assemblers/__init__.py b/m2cgen/assemblers/__init__.py
index 86e24693..a817d331 100644
--- a/m2cgen/assemblers/__init__.py
+++ b/m2cgen/assemblers/__init__.py
@@ -52,6 +52,7 @@
     "sklearn_LinearSVR": SklearnLinearModelAssembler,
     "sklearn_NuSVC": SklearnSVMModelAssembler,
     "sklearn_NuSVR": SklearnSVMModelAssembler,
+    "sklearn_OneClassSVM": SklearnSVMModelAssembler,
     "sklearn_SVC": SklearnSVMModelAssembler,
     "sklearn_SVR": SklearnSVMModelAssembler,
 
diff --git a/m2cgen/assemblers/svm.py b/m2cgen/assemblers/svm.py
index dcf90348..7b7e12bf 100644
--- a/m2cgen/assemblers/svm.py
+++ b/m2cgen/assemblers/svm.py
@@ -141,8 +141,7 @@ def _rbf_kernel(self, support_vector):
             )
             for i, support_element in enumerate(support_vector)
         ]
-        kernel = utils.apply_op_to_expressions(ast.BinNumOpType.ADD,
-                                               *elem_wise)
+        kernel = utils.apply_op_to_expressions(ast.BinNumOpType.ADD, *elem_wise)
         kernel = utils.mul(self._neg_gamma_expr, kernel)
         return ast.ExpExpr(kernel)
 


Test patch:
diff --git a/tests/e2e/test_e2e.py b/tests/e2e/test_e2e.py
index 44eb9e02..c984f74e 100644
--- a/tests/e2e/test_e2e.py
+++ b/tests/e2e/test_e2e.py
@@ -266,12 +266,15 @@ def classification_binary_random_w_missing_values(model, test_fraction=0.02):
 
         # Sklearn SVM
         regression(svm.NuSVR(kernel="rbf")),
+        regression(svm.OneClassSVM(kernel="rbf")),
         regression(svm.SVR(kernel="rbf")),
 
         classification(svm.NuSVC(kernel="rbf", **SVC_PARAMS)),
+        classification(svm.OneClassSVM(kernel="rbf")),
         classification(svm.SVC(kernel="rbf", **SVC_PARAMS)),
 
         classification_binary(svm.NuSVC(kernel="rbf", **SVC_PARAMS)),
+        classification_binary(svm.OneClassSVM(kernel="rbf")),
         classification_binary(svm.SVC(kernel="linear", **SVC_PARAMS)),
         classification_binary(svm.SVC(kernel="poly", C=1.5, degree=2, gamma=0.1, coef0=2.0, **SVC_PARAMS)),
         classification_binary(svm.SVC(kernel="rbf", **SVC_PARAMS)),
diff --git a/tests/utils.py b/tests/utils.py
index e7a0a578..aa1d503d 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -17,7 +17,7 @@
 from sklearn.ensemble._forest import BaseForest, ForestClassifier
 from sklearn.linear_model._base import LinearClassifierMixin
 from sklearn.model_selection import train_test_split
-from sklearn.svm import SVC, NuSVC
+from sklearn.svm import SVC, NuSVC, OneClassSVM
 from sklearn.svm._base import BaseLibSVM
 from sklearn.tree import DecisionTreeClassifier
 from sklearn.tree._classes import BaseDecisionTree
@@ -42,14 +42,11 @@ def fit(self, X, y):
             X = sm.add_constant(X)
         est = self.model(y, X, **init_params)
         if "fit_regularized" in self.params:
-            self.fitted_model_ = est.fit_regularized(
-                **self.params["fit_regularized"])
+            self.fitted_model_ = est.fit_regularized(**self.params["fit_regularized"])
         elif "iterative_fit" in self.params:
-            self.fitted_model_ = est.iterative_fit(
-                **self.params["iterative_fit"])
+            self.fitted_model_ = est.iterative_fit(**self.params["iterative_fit"])
         elif "fit_constrained" in self.params:
-            self.fitted_model_ = est.fit_constrained(
-                **self.params["fit_constrained"])
+            self.fitted_model_ = est.fit_constrained(**self.params["fit_constrained"])
         else:
             self.fitted_model_ = est.fit(**self.params.get("fit", {}))
         # mock class module and name to show appropriate model name in tests
@@ -140,11 +137,24 @@ def get_instance(cls, dataset_name, test_fraction=0.02):
     def __call__(self, estimator):
         fitted_estimator = estimator.fit(self.X_train, self.y_train)
 
-        if isinstance(estimator, (LinearClassifierMixin, SVC, NuSVC, LightBaseClassifier)):
+        if isinstance(
+                estimator,
+                (
+                    LinearClassifierMixin,
+                    SVC,
+                    NuSVC,
+                    OneClassSVM,
+                    LightBaseClassifier,
+                )):
             y_pred = estimator.decision_function(self.X_test)
         elif isinstance(
                 estimator,
-                (ForestClassifier, DecisionTreeClassifier, xgb.XGBClassifier, lgb.LGBMClassifier)):
+                (
+                    ForestClassifier,
+                    DecisionTreeClassifier,
+                    xgb.XGBClassifier,
+                    lgb.LGBMClassifier,
+                )):
             y_pred = estimator.predict_proba(self.X_test)
         else:
             y_pred = estimator.predict(self.X_test)
@@ -152,9 +162,18 @@ def __call__(self, estimator):
         # Some models force input data to be particular type
         # during prediction phase in their native Python libraries.
         # For correct comparison of testing results we mimic the same behavior
-        if isinstance(estimator, (BaseDecisionTree, BaseForest)):
+        if isinstance(
+                estimator,
+                (
+                    BaseDecisionTree,
+                    BaseForest,
+                )):
             self.X_test = self.X_test.astype(np.float32, copy=False)
-        elif isinstance(estimator, BaseLibSVM):
+        elif isinstance(
+                estimator,
+                (
+                    BaseLibSVM,
+                )):
             self.X_test = self.X_test.astype(np.float64, copy=False)
 
         return self.X_test, y_pred, fitted_estimator
@@ -180,8 +199,7 @@ def cmp_exprs(left, right):
         return True
 
     if isinstance(left, ast.Expr) and isinstance(right, ast.Expr):
-        assert isinstance(left, type(right)), (
-            f"Expected instance of {type(right)}, received {type(left)}")
+        assert isinstance(left, type(right)), f"Expected instance of {type(right)}, received {type(left)}"
 
         # Only compare attributes which don't start with __
         attrs_to_compare = filter(lambda attr_name: not attr_name.startswith('__'), dir(left))
@@ -255,8 +273,7 @@ def predict_from_commandline(exec_args):
     result = subprocess.Popen(exec_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
     stdout, stderr = result.communicate()
     if result.returncode != 0:
-        raise Exception(
-            f"Bad exit code ({result.returncode}), stderr:\n{stderr.decode('utf-8')}")
+        raise Exception(f"Bad exit code ({result.returncode}), stderr:\n{stderr.decode('utf-8')}")
 
     items = stdout.decode("utf-8").strip().split(" ")