diff --git a/Orange/widgets/data/owimpute.py b/Orange/widgets/data/owimpute.py index 16c4ea3023a..a173488a391 100644 --- a/Orange/widgets/data/owimpute.py +++ b/Orange/widgets/data/owimpute.py @@ -11,10 +11,11 @@ from AnyQt.QtWidgets import ( QGroupBox, QRadioButton, QPushButton, QHBoxLayout, QGridLayout, QVBoxLayout, QStackedWidget, QComboBox, - QButtonGroup, QStyledItemDelegate, QListView, QDoubleSpinBox -) -from AnyQt.QtCore import Qt, QThread, QModelIndex + QButtonGroup, QStyledItemDelegate, QListView, QDoubleSpinBox, QLabel) +from AnyQt.QtCore import Qt, QThread, QModelIndex, QDateTime from AnyQt.QtCore import pyqtSlot as Slot +from AnyQt.QtGui import QDoubleValidator + from orangewidget.utils.listview import ListViewSearch import Orange.data @@ -154,6 +155,8 @@ class Warning(OWWidget.Warning): _variable_imputation_state = settings.ContextSetting({}) # type: VariableState autocommit = settings.Setting(True) + default_numeric = settings.Setting("") + default_time = settings.Setting(0) want_main_area = False resizing_enabled = False @@ -171,11 +174,13 @@ def __init__(self): main_layout.setContentsMargins(10, 10, 10, 10) self.controlArea.layout().addLayout(main_layout) - box = QGroupBox(title=self.tr("Default Method"), flat=False) - box_layout = QGridLayout(box) - box_layout.setContentsMargins(5, 0, 0, 0) + box = gui.vBox(None, "Default Method") main_layout.addWidget(box) + box_layout = QGridLayout(box) + box_layout.setSpacing(8) + box.layout().addLayout(box_layout) + button_group = QButtonGroup() button_group.buttonClicked[int].connect(self.set_default_method) @@ -186,6 +191,37 @@ def __init__(self): button_group.addButton(button, method) box_layout.addWidget(button, i % 3, i // 3) + def set_to_fixed_value(): + self.set_default_method(Method.Default) + + def set_default_time(datetime): + self.default_time = datetime.toMSecsSinceEpoch() + if self.default_method_index != Method.Default: + set_to_fixed_value() + else: + self._invalidate() + + hlayout = QHBoxLayout() + box.layout().addLayout(hlayout) + button = QRadioButton("Fixed values; numeric variables:") + button_group.addButton(button, Method.Default) + button.setChecked(Method.Default == self.default_method_index) + hlayout.addWidget(button) + + le = gui.lineEdit( + None, self, "default_numeric", valueType=float, + validator=QDoubleValidator(), alignment=Qt.AlignRight, + callback=self._invalidate, focusInCallback=set_to_fixed_value) + hlayout.addWidget(le) + + hlayout.addWidget(QLabel(", time:")) + + self.time_widget = gui.DateTimeEditWCalendarTime(self) + self.time_widget.setContentsMargins(0, 0, 0, 0) + self.default_time = QDateTime.currentDateTime().toMSecsSinceEpoch() + self.time_widget.dateTimeChanged.connect(set_default_time) + hlayout.addWidget(self.time_widget) + self.default_button_group = button_group box = QGroupBox(title=self.tr("Individual Attribute Settings"), @@ -267,6 +303,11 @@ def create_imputer(self, method, *args): m = AsDefault() m.method = default return m + elif method == Method.Default and not args: # global default values + return impute.FixedValueByType( + default_continuous=float(self.default_numeric or np.nan), + default_time=self.default_time or np.nan + ) else: return METHODS[method](*args) @@ -302,6 +343,8 @@ def set_data(self, data): if data is not None: self.varmodel[:] = data.domain.variables self.openContext(data.domain) + self.time_widget.set_datetime( + QDateTime.fromMSecsSinceEpoch(self.default_time)) # restore per variable imputation state self._restore_state(self._variable_imputation_state) @@ -660,5 +703,19 @@ def storeSpecificSettings(self): super().storeSpecificSettings() +def __sample_data(): # pragma: no cover + domain = Orange.data.Domain( + [Orange.data.ContinuousVariable(f"c{i}") for i in range(3)] + + [Orange.data.TimeVariable(f"t{i}") for i in range(3)], + []) + n = np.nan + x = np.array([ + [1, 2, n, 1000, n, n], + [2, n, 1, n, 2000, 2000] + ]) + return Orange.data.Table(domain, x, np.empty((2, 0))) + + if __name__ == "__main__": # pragma: no cover + # WidgetPreview(OWImpute).run(__sample_data()) WidgetPreview(OWImpute).run(Orange.data.Table("brown-selected")) diff --git a/Orange/widgets/data/tests/test_owimpute.py b/Orange/widgets/data/tests/test_owimpute.py index 6398a067033..1481281b9e6 100644 --- a/Orange/widgets/data/tests/test_owimpute.py +++ b/Orange/widgets/data/tests/test_owimpute.py @@ -6,7 +6,7 @@ from AnyQt.QtCore import Qt, QItemSelection from AnyQt.QtTest import QTest -from Orange.data import Table, Domain +from Orange.data import Table, Domain, ContinuousVariable, TimeVariable from Orange.preprocess import impute from Orange.widgets.data.owimpute import OWImpute, AsDefault, Learner, Method from Orange.widgets.tests.base import WidgetTest @@ -119,6 +119,31 @@ def test_select_method(self): self.assertIsInstance(widget.get_method_for_column(0), AsDefault) self.assertIsInstance(widget.get_method_for_column(2), AsDefault) + def test_overall_default(self): + domain = Domain( + [ContinuousVariable(f"c{i}") for i in range(3)] + + [TimeVariable(f"t{i}") for i in range(3)], + []) + n = np.nan + x = np.array([ + [1, 2, n, 1000, n, n], + [2, n, 1, n, 2000, 2000] + ]) + data = Table(domain, x, np.empty((2, 0))) + + widget = self.widget + widget.default_numeric = 3.14 + widget.default_time = 42 + widget.default_method_index = Method.Default + + self.send_signal(self.widget.Inputs.data, data) + imp_data = self.get_output(self.widget.Outputs.data) + np.testing.assert_almost_equal( + imp_data.X, + [[1, 2, 3.14, 1000, 42, 42], + [2, 3.14, 1, 42, 2000, 2000]] + ) + def test_value_edit(self): data = Table("heart_disease")[::10] self.send_signal(self.widget.Inputs.data, data)