Repository: biolab/orange3

Issue:
Feature Constructor can fail due to missing variables
When using train data including a target feature calculated with feature construction widget (log10 function) the predictions widget fails. When instead saving exactly the same dataset with the calculated field and then retrieving it from the file, applying predictions works just fine.

Here is more detail.
Target data is numerical and has to be changed to their log10 before training.
Training done using stochastic gradient descent or random forrest or adaboost on random forrest.
Test and score widget works fine.
Predictions widget fails if log10 function applied in the workflow prior to training.
Predictions succeeds if receiving directly data with target feature changed to their log10 (saved file).

I hope someone can figure this out...
Regards,
G.


Discussion:

Resolving patch:
diff --git a/Orange/widgets/data/owfeatureconstructor.py b/Orange/widgets/data/owfeatureconstructor.py
--- a/Orange/widgets/data/owfeatureconstructor.py
+++ b/Orange/widgets/data/owfeatureconstructor.py
@@ -659,12 +659,18 @@ def report_error(err):
         )
 
         try:
+            for variable in new_variables:
+                variable.compute_value.mask_exceptions = False
             data = self.data.transform(new_domain)
         # user's expression can contain arbitrary errors
         # pylint: disable=broad-except
         except Exception as err:
             report_error(err)
             return
+        finally:
+            for variable in new_variables:
+                variable.compute_value.mask_exceptions = True
+
         disc_attrs_not_ok = self.check_attrs_values(
             [var for var in attrs if var.is_discrete], data)
         if disc_attrs_not_ok:
@@ -1064,15 +1070,23 @@ def __init__(self, expression, args, extra_env=None, cast=None):
         self.func = make_lambda(ast.parse(expression, mode="eval"),
                                 [name for name, _ in args], self.extra_env)
         self.cast = cast
+        self.mask_exceptions = True
 
     def __call__(self, instance, *_):
         if isinstance(instance, Orange.data.Table):
             return [self(inst) for inst in instance]
         else:
-            args = [str(instance[var])
-                    if instance.domain[var].is_string else instance[var]
-                    for _, var in self.args]
-            y = self.func(*args)
+            try:
+                args = [str(instance[var])
+                        if instance.domain[var].is_string else instance[var]
+                        for _, var in self.args]
+                y = self.func(*args)
+            # user's expression can contain arbitrary errors
+            # this also covers missing attributes
+            except:  # pylint: disable=bare-except
+                if not self.mask_exceptions:
+                    raise
+                return np.nan
             if self.cast:
                 y = self.cast(y)
             return y


Test patch:
diff --git a/Orange/widgets/data/tests/test_owfeatureconstructor.py b/Orange/widgets/data/tests/test_owfeatureconstructor.py
--- a/Orange/widgets/data/tests/test_owfeatureconstructor.py
+++ b/Orange/widgets/data/tests/test_owfeatureconstructor.py
@@ -274,6 +274,35 @@ def test_string_casting(self):
         self.assertEqual(r, [x[0] for x in zoo.metas[:, 0]])
         self.assertEqual(f(zoo[0]), str(zoo[0, "name"])[0])
 
+    def test_missing_variable(self):
+        zoo = Table("zoo")
+        assert zoo.domain.class_var.name == "type"
+        f = FeatureFunc("type[0]",
+                        [("type", zoo.domain["type"])])
+        no_class = Domain(zoo.domain.attributes, None, zoo.domain.metas)
+        data2 = zoo.transform(no_class)
+        r = f(data2)
+        self.assertTrue(np.all(np.isnan(r)))
+        self.assertTrue(np.isnan(f(data2[0])))
+
+    def test_invalid_expression_variable(self):
+        iris = Table("iris")
+        f = FeatureFunc("1 / petal_length",
+                        [("petal_length", iris.domain["petal length"])])
+        iris[0]["petal length"] = 0
+
+        f.mask_exceptions = False
+        self.assertRaises(Exception, f, iris)
+        self.assertRaises(Exception, f, iris[0])
+        _ = f(iris[1])
+
+        f.mask_exceptions = True
+        r = f(iris)
+        self.assertTrue(np.isnan(r[0]))
+        self.assertFalse(np.isnan(r[1]))
+        self.assertTrue(np.isnan(f(iris[0])))
+        self.assertFalse(np.isnan(f(iris[1])))
+
 
 class OWFeatureConstructorTests(WidgetTest):
     def setUp(self):