Пример #1
0
class OWDiscretize(widget.OWWidget):
    # pylint: disable=too-many-instance-attributes
    name = "Discretize"
    description = "Discretize the numeric data features."
    icon = "icons/Discretize.svg"
    keywords = ["bin", "categorical", "nominal", "ordinal"]

    class Inputs:
        data = Input("Data", Orange.data.Table, doc="Input data table")

    class Outputs:
        data = Output("Data", Orange.data.Table, doc="Table with discretized features")

    settingsHandler = settings.DomainContextHandler()
    settings_version = 2
    saved_var_states = settings.ContextSetting({})

    #: The default method name
    default_method_name = settings.Setting(Methods.EqualFreq.name)
    #: The k for Equal{Freq,Width}
    default_k = settings.Setting(3)
    #: The default cut points for custom entry
    default_cutpoints: Tuple[float, ...] = settings.Setting(())
    autosend = settings.Setting(True)

    #: Discretization methods
    Default, Leave, MDL, EqualFreq, EqualWidth, Remove, Custom = list(Methods)

    want_main_area = False
    resizing_enabled = False

    def __init__(self):
        super().__init__()

        #: input data
        self.data = None
        self.class_var = None
        #: Current variable discretization state
        self.var_state = {}
        #: Saved variable discretization settings (context setting)
        self.saved_var_states = {}

        self.method = Methods.Default
        self.k = 5
        self.cutpoints = ()

        box = gui.vBox(self.controlArea, self.tr("Default Discretization"))
        self._default_method_ = 0
        self.default_bbox = rbox = gui.radioButtons(
            box, self, "_default_method_", callback=self._default_disc_changed)
        self.default_button_group = bg = rbox.findChild(QButtonGroup)
        bg.buttonClicked[int].connect(self.set_default_method)

        rb = gui.hBox(rbox)
        self.left = gui.vBox(rb)
        right = gui.vBox(rb)
        rb.layout().setStretch(0, 1)
        rb.layout().setStretch(1, 1)
        self.options = [
            (Methods.Default, self.tr("Default")),
            (Methods.Leave, self.tr("Leave numeric")),
            (Methods.MDL, self.tr("Entropy-MDL discretization")),
            (Methods.EqualFreq, self.tr("Equal-frequency discretization")),
            (Methods.EqualWidth, self.tr("Equal-width discretization")),
            (Methods.Remove, self.tr("Remove numeric variables")),
            (Methods.Custom, self.tr("Manual")),
        ]

        for id_, opt in self.options[1:]:
            t = gui.appendRadioButton(rbox, opt)
            bg.setId(t, id_)
            t.setChecked(id_ == self.default_method)
            [right, self.left][opt.startswith("Equal")].layout().addWidget(t)

        def _intbox(parent, attr, callback):
            box = gui.indentedBox(parent)
            s = gui.spin(
                box, self, attr, minv=2, maxv=10, label="Num. of intervals:",
                callback=callback)
            s.setMaximumWidth(60)
            s.setAlignment(Qt.AlignRight)
            gui.rubber(s.box)
            return box.box

        self.k_general = _intbox(self.left, "default_k",
                                 self._default_disc_changed)
        self.k_general.layout().setContentsMargins(0, 0, 0, 0)

        def manual_cut_editline(text="", enabled=True) -> QLineEdit:
            edit = QLineEdit(
                text=text,
                placeholderText="e.g. 0.0, 0.5, 1.0",
                toolTip="Enter fixed discretization cut points (a comma "
                        "separated list of strictly increasing numbers e.g. "
                        "0.0, 0.5, 1.0).",
                enabled=enabled,
            )
            @edit.textChanged.connect
            def update():
                validator = edit.validator()
                if validator is not None:
                    state, _, _ = validator.validate(edit.text(), 0)
                else:
                    state = QValidator.Acceptable
                palette = edit.palette()
                colors = {
                    QValidator.Intermediate: (Qt.yellow, Qt.black),
                    QValidator.Invalid: (Qt.red, Qt.black),
                }.get(state, None)
                if colors is None:
                    palette = QPalette()
                else:
                    palette.setColor(QPalette.Base, colors[0])
                    palette.setColor(QPalette.Text, colors[1])

                cr = edit.cursorRect()
                p = edit.mapToGlobal(cr.bottomRight())
                edit.setPalette(palette)
                if state != QValidator.Acceptable and edit.isVisible():
                    show_tip(edit, p, edit.toolTip(), textFormat=Qt.RichText)
                else:
                    show_tip(edit, p, "")
            return edit

        self.manual_cuts_edit = manual_cut_editline(
            text=", ".join(map(str, self.default_cutpoints)),
            enabled=self.default_method == Methods.Custom,
        )

        def set_manual_default_cuts():
            text = self.manual_cuts_edit.text()
            self.default_cutpoints = tuple(
                float(s.strip()) for s in text.split(",") if s.strip())
            self._default_disc_changed()
        self.manual_cuts_edit.editingFinished.connect(set_manual_default_cuts)

        validator = IncreasingNumbersListValidator()
        self.manual_cuts_edit.setValidator(validator)
        ibox = gui.indentedBox(right, orientation=Qt.Horizontal)
        ibox.layout().addWidget(self.manual_cuts_edit)

        right.layout().addStretch(10)
        self.left.layout().addStretch(10)

        self.connect_control(
            "default_cutpoints",
            lambda values: self.manual_cuts_edit.setText(", ".join(map(str, values)))
        )
        vlayout = QHBoxLayout()
        box = gui.widgetBox(
            self.controlArea, "Individual Attribute Settings",
            orientation=vlayout, spacing=8
        )

        # List view with all attributes
        self.varview = ListViewSearch(
            selectionMode=QListView.ExtendedSelection,
            uniformItemSizes=True,
        )
        self.varview.setItemDelegate(DiscDelegate())
        self.varmodel = itemmodels.VariableListModel()
        self.varview.setModel(self.varmodel)
        self.varview.selectionModel().selectionChanged.connect(
            self._var_selection_changed
        )

        vlayout.addWidget(self.varview)
        # Controls for individual attr settings
        self.bbox = controlbox = gui.radioButtons(
            box, self, "method", callback=self._disc_method_changed
        )
        vlayout.addWidget(controlbox)
        self.variable_button_group = bg = controlbox.findChild(QButtonGroup)
        for id_, opt in self.options[:5]:
            b = gui.appendRadioButton(controlbox, opt)
            bg.setId(b, id_)

        self.k_specific = _intbox(controlbox, "k", self._disc_method_changed)

        gui.appendRadioButton(controlbox, "Remove attribute", id=Methods.Remove)
        b = gui.appendRadioButton(controlbox, "Manual", id=Methods.Custom)

        self.manual_cuts_specific = manual_cut_editline(
            text=", ".join(map(str, self.cutpoints)),
            enabled=self.method == Methods.Custom
        )
        self.manual_cuts_specific.setValidator(validator)
        b.toggled[bool].connect(self.manual_cuts_specific.setEnabled)

        def set_manual_cuts():
            text = self.manual_cuts_specific.text()
            points = [t for t in text.split(",") if t.split()]
            self.cutpoints = tuple(float(t) for t in points)
            self._disc_method_changed()
        self.manual_cuts_specific.editingFinished.connect(set_manual_cuts)

        self.connect_control(
            "cutpoints",
            lambda values: self.manual_cuts_specific.setText(", ".join(map(str, values)))
        )
        ibox = gui.indentedBox(controlbox, orientation=Qt.Horizontal)
        self.copy_current_to_manual_button = b = FixedSizeButton(
            text="CC", toolTip="Copy the current cut points to manual mode",
            enabled=False
        )
        b.clicked.connect(self._copy_to_manual)
        ibox.layout().addWidget(self.manual_cuts_specific)
        ibox.layout().addWidget(b)

        gui.rubber(controlbox)
        controlbox.setEnabled(False)
        bg.button(self.method)
        self.controlbox = controlbox

        gui.auto_apply(self.buttonsArea, self, "autosend")

        self._update_spin_positions()

    @property
    def default_method(self) -> Methods:
        return Methods[self.default_method_name]

    @default_method.setter
    def default_method(self, method):
        self.set_default_method(method)

    def set_default_method(self, method: Methods):
        if isinstance(method, int):
            method = Methods(method)
        else:
            method = Methods.from_method(method)

        if method != self.default_method:
            self.default_method_name = method.name
            self.default_button_group.button(method).setChecked(True)
            self._default_disc_changed()
        self.manual_cuts_edit.setEnabled(method == Methods.Custom)

    @Inputs.data
    def set_data(self, data):
        self.closeContext()
        self.data = data
        if self.data is not None:
            self._initialize(data)
            self.openContext(data)
            # Restore the per variable discretization settings
            self._restore(self.saved_var_states)
            # Complete the induction of cut points
            self._update_points()
        else:
            self._clear()
        self.commit.now()

    def _initialize(self, data):
        # Initialize the default variable states for new data.
        self.class_var = data.domain.class_var
        cvars = [var for var in data.domain.variables
                 if var.is_continuous]
        self.varmodel[:] = cvars

        has_disc_class = data.domain.has_discrete_class

        def set_enabled(box: QWidget, id_: Methods, state: bool):
            bg = box.findChild(QButtonGroup)
            b = bg.button(id_)
            b.setEnabled(state)

        set_enabled(self.default_bbox, self.MDL, has_disc_class)
        bg = self.bbox.findChild(QButtonGroup)
        b = bg.button(Methods.MDL)
        b.setEnabled(has_disc_class)
        set_enabled(self.bbox, self.MDL, has_disc_class)

        # If the newly disabled MDL button is checked then change it
        if not has_disc_class and self.default_method == self.MDL:
            self.default_method = Methods.Leave
        if not has_disc_class and self.method == self.MDL:
            self.method = Methods.Default

        # Reset (initialize) the variable discretization states.
        self._reset()

    def _restore(self, saved_state):
        # Restore variable states from a saved_state dictionary.
        def_method = self._current_default_method()
        for i, var in enumerate(self.varmodel):
            key = variable_key(var)
            if key in saved_state:
                state = saved_state[key]
                if isinstance(state.method, Default):
                    state = DState(Default(def_method), None, None)
                self._set_var_state(i, state)

    def _reset(self):
        # restore the individual variable settings back to defaults.
        def_method = self._current_default_method()
        self.var_state = {}
        for i in range(len(self.varmodel)):
            state = DState(Default(def_method), None, None)
            self._set_var_state(i, state)

    def _set_var_state(self, index, state):
        # set the state of variable at `index` to `state`.
        self.var_state[index] = state
        self.varmodel.setData(self.varmodel.index(index), state, Qt.UserRole)

    def _clear(self):
        self.data = None
        self.varmodel[:] = []
        self.var_state = {}
        self.saved_var_states = {}
        self.default_button_group.button(self.MDL).setEnabled(True)
        self.variable_button_group.button(self.MDL).setEnabled(True)

    def _update_points(self):
        """
        Update the induced cut points.
        """
        if self.data is None:
            return

        def induce_cuts(method, data, var):
            dvar = _dispatch[type(method)](method, data, var)
            if dvar is None:
                # removed
                return [], None
            elif dvar is var:
                # no transformation took place
                return None, var
            elif is_discretized(dvar):
                return dvar.compute_value.points, dvar
            raise ValueError

        for i, var in enumerate(self.varmodel):
            state = self.var_state[i]
            if state.points is None and state.disc_var is None:
                points, dvar = induce_cuts(state.method, self.data, var)
                new_state = state._replace(points=points, disc_var=dvar)
                self._set_var_state(i, new_state)

    def _current_default_method(self):
        method = self.default_method
        k = self.default_k
        if method == Methods.Leave:
            def_method = Leave()
        elif method == Methods.MDL:
            def_method = MDL()
        elif method == Methods.EqualFreq:
            def_method = EqualFreq(k)
        elif method == Methods.EqualWidth:
            def_method = EqualWidth(k)
        elif method == Methods.Remove:
            def_method = Remove()
        elif method == Methods.Custom:
            def_method = Custom(self.default_cutpoints)
        else:
            assert False
        return def_method

    def _current_method(self):
        if self.method == Methods.Default:
            method = Default(self._current_default_method())
        elif self.method == Methods.Leave:
            method = Leave()
        elif self.method == Methods.MDL:
            method = MDL()
        elif self.method == Methods.EqualFreq:
            method = EqualFreq(self.k)
        elif self.method == Methods.EqualWidth:
            method = EqualWidth(self.k)
        elif self.method == Methods.Remove:
            method = Remove()
        elif self.method == Methods.Custom:
            method = Custom(self.cutpoints)
        else:
            assert False
        return method

    def _update_spin_positions(self):
        kmethods = [Methods.EqualFreq, Methods.EqualWidth]
        self.k_general.setDisabled(self.default_method not in kmethods)
        if self.default_method == Methods.EqualFreq:
            self.left.layout().insertWidget(1, self.k_general)
        elif self.default_method == Methods.EqualWidth:
            self.left.layout().insertWidget(2, self.k_general)

        self.k_specific.setDisabled(self.method not in kmethods)
        if self.method == Methods.EqualFreq:
            self.bbox.layout().insertWidget(4, self.k_specific)
        elif self.method == Methods.EqualWidth:
            self.bbox.layout().insertWidget(5, self.k_specific)

    def _default_disc_changed(self):
        self._update_spin_positions()
        method = self._current_default_method()
        state = DState(Default(method), None, None)
        for i, _ in enumerate(self.varmodel):
            if isinstance(self.var_state[i].method, Default):
                self._set_var_state(i, state)
        self._update_points()
        self.commit.deferred()

    def _disc_method_changed(self):
        self._update_spin_positions()
        indices = self.selected_indices()
        method = self._current_method()
        state = DState(method, None, None)
        for idx in indices:
            self._set_var_state(idx, state)
        self._update_points()
        self._copy_to_manual_update_enabled()
        self.commit.deferred()

    def _copy_to_manual(self):
        indices = self.selected_indices()
        # set of all methods for the current selection
        if len(indices) != 1:
            return
        index = indices[0]
        state = self.var_state[index]
        var = self.varmodel[index]
        fmt = var.repr_val
        points = state.points
        if points is None:
            points = ()
        else:
            points = tuple(state.points)
        state = state._replace(method=Custom(points), points=None, disc_var=None)
        self._set_var_state(index, state)
        self.method = Methods.Custom
        self.cutpoints = points
        self.manual_cuts_specific.setText(", ".join(map(fmt, points)))
        self._update_points()
        self.commit.deferred()

    def _copy_to_manual_update_enabled(self):
        indices = self.selected_indices()
        methods = [self.var_state[i].method for i in indices]
        self.copy_current_to_manual_button.setEnabled(
            len(indices) == 1 and not isinstance(methods[0], Custom))

    def _var_selection_changed(self, *_):
        self._copy_to_manual_update_enabled()
        indices = self.selected_indices()
        # set of all methods for the current selection
        methods = [self.var_state[i].method for i in indices]

        def key(method):
            if isinstance(method, Default):
                return Default, (None, )
            return type(method), tuple(method)

        mset = list(unique_everseen(methods, key=key))

        self.controlbox.setEnabled(len(mset) > 0)
        if len(mset) == 1:
            method = mset.pop()
            self.method = Methods.from_method(method)
            if isinstance(method, (EqualFreq, EqualWidth)):
                self.k = method.k
            elif isinstance(method, Custom):
                self.cutpoints = method.points
        else:
            # deselect the current button
            self.method = -1
            bg = self.controlbox.group
            button_group_reset(bg)
        self._update_spin_positions()

    def selected_indices(self):
        rows = self.varview.selectionModel().selectedRows()
        return [index.row() for index in rows]

    def method_for_index(self, index):
        state = self.var_state[index]
        return state.method

    def discretized_var(self, index):
        # type: (int) -> Optional[Orange.data.DiscreteVariable]
        state = self.var_state[index]
        if state.disc_var is not None and state.points == []:
            # Removed by MDL Entropy
            return None
        else:
            return state.disc_var

    def discretized_domain(self):
        """
        Return the current effective discretized domain.
        """
        if self.data is None:
            return None

        # a mapping of all applied changes for variables in `varmodel`
        mapping = {var: self.discretized_var(i)
                   for i, var in enumerate(self.varmodel)}

        def disc_var(source):
            return mapping.get(source, source)

        # map the full input domain to the new variables (where applicable)
        attributes = [disc_var(v) for v in self.data.domain.attributes]
        attributes = [v for v in attributes if v is not None]

        class_vars = [disc_var(v) for v in self.data.domain.class_vars]
        class_vars = [v for v in class_vars if v is not None]

        domain = Orange.data.Domain(
            attributes, class_vars, metas=self.data.domain.metas
        )
        return domain

    @gui.deferred
    def commit(self):
        output = None
        if self.data is not None:
            domain = self.discretized_domain()
            output = self.data.transform(domain)
        self.Outputs.data.send(output)

    def storeSpecificSettings(self):
        super().storeSpecificSettings()
        self.saved_var_states = {
            variable_key(var):
                self.var_state[i]._replace(points=None, disc_var=None)
            for i, var in enumerate(self.varmodel)
        }

    def send_report(self):
        self.report_items((
            ("Default method", self.options[self.default_method][1]),))
        if self.varmodel:
            self.report_items("Thresholds", [
                (var.name,
                 DiscDelegate.cutsText(self.var_state[i], var.repr_val) or "leave numeric")
                for i, var in enumerate(self.varmodel)])

    @classmethod
    def migrate_settings(cls, settings, version):  # pylint: disable=redefined-outer-name
        if version is None or version < 2:
            # was stored as int indexing Methods (but offset by 1)
            default = settings.pop("default_method", 0)
            default = Methods(default + 1)
            settings["default_method_name"] = default.name
Пример #2
0
class OWImpute(OWWidget):
    name = "Impute"
    description = "Impute missing values in the data table."
    icon = "icons/Impute.svg"
    priority = 2130
    keywords = ["substitute", "missing"]

    class Inputs:
        data = Input("Data", Orange.data.Table)
        learner = Input("Learner", Learner)

    class Outputs:
        data = Output("Data", Orange.data.Table)

    class Error(OWWidget.Error):
        imputation_failed = Msg("Imputation failed for '{}'")
        model_based_imputer_sparse = \
            Msg("Model based imputer does not work for sparse data")

    class Warning(OWWidget.Warning):
        cant_handle_var = Msg("Default method can not handle '{}'")

    settingsHandler = settings.DomainContextHandler()

    _default_method_index = settings.Setting(int(Method.Leave))  # type: int
    # Per-variable imputation state (synced in storeSpecificSettings)
    _variable_imputation_state = settings.ContextSetting(
        {})  # type: VariableState

    autocommit = settings.Setting(True)

    want_main_area = False
    resizing_enabled = False

    def __init__(self):
        super().__init__()
        self.data = None  # type: Optional[Orange.data.Table]
        self.learner = None  # type: Optional[Learner]
        self.default_learner = SimpleTreeLearner(min_instances=10,
                                                 max_depth=10)
        self.modified = False
        self.executor = qconcurrent.ThreadExecutor(self)
        self.__task = None

        main_layout = QVBoxLayout()
        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)
        main_layout.addWidget(box)

        button_group = QButtonGroup()
        button_group.buttonClicked[int].connect(self.set_default_method)

        for i, (method, _) in enumerate(list(METHODS.items())[1:-1]):
            imputer = self.create_imputer(method)
            button = QRadioButton(imputer.name)
            button.setChecked(method == self.default_method_index)
            button_group.addButton(button, method)
            box_layout.addWidget(button, i % 3, i // 3)

        self.default_button_group = button_group

        box = QGroupBox(title=self.tr("Individual Attribute Settings"),
                        flat=False)
        main_layout.addWidget(box)

        horizontal_layout = QHBoxLayout(box)
        main_layout.addWidget(box)

        self.varview = ListViewSearch(
            selectionMode=QListView.ExtendedSelection, uniformItemSizes=True)
        self.varview.setItemDelegate(DisplayFormatDelegate())
        self.varmodel = itemmodels.VariableListModel()
        self.varview.setModel(self.varmodel)
        self.varview.selectionModel().selectionChanged.connect(
            self._on_var_selection_changed)
        self.selection = self.varview.selectionModel()

        horizontal_layout.addWidget(self.varview)

        method_layout = QVBoxLayout()
        horizontal_layout.addLayout(method_layout)

        button_group = QButtonGroup()
        for method in Method:
            imputer = self.create_imputer(method)
            button = QRadioButton(text=imputer.name)
            button_group.addButton(button, method)
            method_layout.addWidget(button)

        self.value_combo = QComboBox(
            minimumContentsLength=8,
            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength,
            activated=self._on_value_selected)
        self.value_double = QDoubleSpinBox(
            editingFinished=self._on_value_selected,
            minimum=-1000.,
            maximum=1000.,
            singleStep=.1,
            decimals=3,
        )
        self.value_stack = value_stack = QStackedWidget()
        value_stack.addWidget(self.value_combo)
        value_stack.addWidget(self.value_double)
        method_layout.addWidget(value_stack)

        button_group.buttonClicked[int].connect(
            self.set_method_for_current_selection)

        method_layout.addStretch(2)

        reset_button = QPushButton("Restore All to Default",
                                   checked=False,
                                   checkable=False,
                                   clicked=self.reset_variable_state,
                                   default=False,
                                   autoDefault=False)
        method_layout.addWidget(reset_button)

        self.variable_button_group = button_group

        box = gui.auto_apply(self.controlArea, self, "autocommit")
        box.button.setFixedWidth(180)
        box.layout().insertStretch(0)

        self.info.set_input_summary(self.info.NoInput)
        self.info.set_output_summary(self.info.NoOutput)

    def create_imputer(self, method, *args):
        # type: (Method, ...) -> impute.BaseImputeMethod
        if method == Method.Model:
            if self.learner is not None:
                return impute.Model(self.learner)
            else:
                return impute.Model(self.default_learner)
        elif method == Method.AsAboveSoBelow:
            assert self.default_method_index != Method.AsAboveSoBelow
            default = self.create_imputer(Method(self.default_method_index))
            m = AsDefault()
            m.method = default
            return m
        else:
            return METHODS[method](*args)

    @property
    def default_method_index(self):
        return self._default_method_index

    @default_method_index.setter
    def default_method_index(self, index):
        if self._default_method_index != index:
            assert index != Method.AsAboveSoBelow
            self._default_method_index = index
            self.default_button_group.button(index).setChecked(True)
            # update variable view
            self.update_varview()
            self._invalidate()

    def set_default_method(self, index):
        """Set the current selected default imputation method.
        """
        self.default_method_index = index

    @Inputs.data
    @check_sql_input
    def set_data(self, data):
        self.cancel()
        self.closeContext()
        self.varmodel[:] = []
        self._variable_imputation_state = {}  # type: VariableState
        self.modified = False
        self.data = data

        if data is not None:
            self.varmodel[:] = data.domain.variables
            self.openContext(data.domain)
            # restore per variable imputation state
            self._restore_state(self._variable_imputation_state)

        summary = len(data) if data else self.info.NoInput
        details = format_summary_details(data) if data else ""
        self.info.set_input_summary(summary, details)

        self.update_varview()
        self.unconditional_commit()

    @Inputs.learner
    def set_learner(self, learner):
        self.cancel()
        self.learner = learner or self.default_learner
        imputer = self.create_imputer(Method.Model)
        button = self.default_button_group.button(Method.Model)
        button.setText(imputer.name)

        variable_button = self.variable_button_group.button(Method.Model)
        variable_button.setText(imputer.name)

        if learner is not None:
            self.default_method_index = Method.Model

        self.update_varview()
        self.commit()

    def get_method_for_column(self, column_index):
        # type: (int) -> impute.BaseImputeMethod
        """
        Return the imputation method for column by its index.
        """
        assert 0 <= column_index < len(self.varmodel)
        idx = self.varmodel.index(column_index, 0)
        state = idx.data(StateRole)
        if state is None:
            state = (Method.AsAboveSoBelow, ())
        return self.create_imputer(state[0], *state[1])

    def _invalidate(self):
        self.modified = True
        if self.__task is not None:
            self.cancel()
        self.commit()

    def commit(self):
        self.cancel()
        self.warning()
        self.Error.imputation_failed.clear()
        self.Error.model_based_imputer_sparse.clear()
        summary = len(self.data) if self.data else self.info.NoOutput
        detail = format_summary_details(self.data) if self.data else ""
        self.info.set_output_summary(summary, detail)

        if not self.data or not self.varmodel.rowCount():
            self.Outputs.data.send(self.data)
            self.modified = False
            return

        data = self.data
        impute_state = [(i, var, self.get_method_for_column(i))
                        for i, var in enumerate(self.varmodel)]
        # normalize to the effective method bypasing AsDefault
        impute_state = [(i, var, m.method if isinstance(m, AsDefault) else m)
                        for i, var, m in impute_state]

        def impute_one(method, var, data):
            # Readability counts, pylint: disable=no-else-raise
            # type: (impute.BaseImputeMethod, Variable, Table) -> Any
            if isinstance(method, impute.Model) and data.is_sparse():
                raise SparseNotSupported()
            elif isinstance(method, impute.DropInstances):
                return RowMask(method(data, var))
            elif not method.supports_variable(var):
                raise VariableNotSupported(var)
            else:
                return method(data, var)

        futures = []
        for _, var, method in impute_state:
            f = self.executor.submit(impute_one, copy.deepcopy(method), var,
                                     data)
            futures.append(f)

        w = qconcurrent.FutureSetWatcher(futures)
        w.doneAll.connect(self.__commit_finish)
        w.progressChanged.connect(self.__progress_changed)
        self.__task = Task(futures, w)
        self.progressBarInit()
        self.setInvalidated(True)

    @Slot()
    def __commit_finish(self):
        assert QThread.currentThread() is self.thread()
        assert self.__task is not None
        futures = self.__task.futures
        assert len(futures) == len(self.varmodel)
        assert self.data is not None

        def get_variable(variable, future, drop_mask) \
                -> Optional[List[Orange.data.Variable]]:
            # Returns a (potentially empty) list of variables,
            # or None on failure that should interrupt the imputation
            assert future.done()
            try:
                res = future.result()
            except SparseNotSupported:
                self.Error.model_based_imputer_sparse()
                return []  # None?
            except VariableNotSupported:
                self.Warning.cant_handle_var(variable.name)
                return []
            except Exception:  # pylint: disable=broad-except
                log = logging.getLogger(__name__)
                log.info("Error for %s", variable.name, exc_info=True)
                self.Error.imputation_failed(variable.name)
                return None
            if isinstance(res, RowMask):
                drop_mask |= res.mask
                newvar = variable
            else:
                newvar = res
            if isinstance(newvar, Orange.data.Variable):
                newvar = [newvar]
            return newvar

        def create_data(attributes, class_vars):
            domain = Orange.data.Domain(attributes, class_vars,
                                        self.data.domain.metas)
            try:
                return self.data.from_table(domain, self.data[~drop_mask])
            except Exception:  # pylint: disable=broad-except
                log = logging.getLogger(__name__)
                log.info("Error", exc_info=True)
                self.Error.imputation_failed("Unknown")
                return None

        self.__task = None
        self.setInvalidated(False)
        self.progressBarFinished()

        attributes = []
        class_vars = []
        drop_mask = np.zeros(len(self.data), bool)
        for i, (var, fut) in enumerate(zip(self.varmodel, futures)):
            newvar = get_variable(var, fut, drop_mask)
            if newvar is None:
                data = None
                break
            if i < len(self.data.domain.attributes):
                attributes.extend(newvar)
            else:
                class_vars.extend(newvar)
        else:
            data = create_data(attributes, class_vars)

        self.Outputs.data.send(data)
        self.modified = False
        summary = len(data) if data else self.info.NoOutput
        details = format_summary_details(data) if data else ""
        self.info.set_output_summary(summary, details)

    @Slot(int, int)
    def __progress_changed(self, n, d):
        assert QThread.currentThread() is self.thread()
        assert self.__task is not None
        self.progressBarSet(100. * n / d)

    def cancel(self):
        self.__cancel(wait=False)

    def __cancel(self, wait=False):
        if self.__task is not None:
            task, self.__task = self.__task, None
            task.cancel()
            task.watcher.doneAll.disconnect(self.__commit_finish)
            task.watcher.progressChanged.disconnect(self.__progress_changed)
            if wait:
                concurrent.futures.wait(task.futures)
                task.watcher.flush()
            self.progressBarFinished()
            self.setInvalidated(False)

    def onDeleteWidget(self):
        self.__cancel(wait=True)
        super().onDeleteWidget()

    def send_report(self):
        specific = []
        for i, var in enumerate(self.varmodel):
            method = self.get_method_for_column(i)
            if not isinstance(method, AsDefault):
                specific.append("{} ({})".format(var.name, str(method)))

        default = self.create_imputer(Method.AsAboveSoBelow)
        if specific:
            self.report_items((("Default method", default.name),
                               ("Specific imputers", ", ".join(specific))))
        else:
            self.report_items((("Method", default.name), ))

    def _on_var_selection_changed(self):
        # Method is well documented, splitting it is not needed for readability,
        # thus pylint: disable=too-many-branches
        indexes = self.selection.selectedIndexes()
        defmethod = (Method.AsAboveSoBelow, ())
        methods = [index.data(StateRole) for index in indexes]
        methods = [m if m is not None else defmethod for m in methods]
        methods = set(methods)
        selected_vars = [self.varmodel[index.row()] for index in indexes]
        has_discrete = any(var.is_discrete for var in selected_vars)
        fixed_value = None
        value_stack_enabled = False
        current_value_widget = None

        if len(methods) == 1:
            method_type, parameters = methods.pop()
            for m in Method:
                if method_type == m:
                    self.variable_button_group.button(m).setChecked(True)

            if method_type == Method.Default:
                (fixed_value, ) = parameters

        elif self.variable_button_group.checkedButton() is not None:
            # Uncheck the current button
            self.variable_button_group.setExclusive(False)
            self.variable_button_group.checkedButton().setChecked(False)
            self.variable_button_group.setExclusive(True)
            assert self.variable_button_group.checkedButton() is None

        # Update variable methods GUI enabled state based on selection.
        for method in Method:
            # use a default constructed imputer to query support
            imputer = self.create_imputer(method)
            enabled = all(
                imputer.supports_variable(var) for var in selected_vars)
            button = self.variable_button_group.button(method)
            button.setEnabled(enabled)

        # Update the "Value" edit GUI.
        if not has_discrete:
            # no discrete variables -> allow mass edit for all (continuous vars)
            value_stack_enabled = True
            current_value_widget = self.value_double
        elif len(selected_vars) == 1:
            # single discrete var -> enable and fill the values combo
            value_stack_enabled = True
            current_value_widget = self.value_combo
            self.value_combo.clear()
            self.value_combo.addItems(selected_vars[0].values)
        else:
            # mixed type selection -> disable
            value_stack_enabled = False
            current_value_widget = None
            self.variable_button_group.button(Method.Default).setEnabled(False)

        self.value_stack.setEnabled(value_stack_enabled)
        if current_value_widget is not None:
            self.value_stack.setCurrentWidget(current_value_widget)
            if fixed_value is not None:
                # set current value
                if current_value_widget is self.value_combo:
                    self.value_combo.setCurrentIndex(fixed_value)
                elif current_value_widget is self.value_double:
                    self.value_double.setValue(fixed_value)
                else:
                    assert False

    def set_method_for_current_selection(self, method_index):
        # type: (Method) -> None
        indexes = self.selection.selectedIndexes()
        self.set_method_for_indexes(indexes, method_index)

    def set_method_for_indexes(self, indexes, method_index):
        # type: (List[QModelIndex], Method) -> None
        if method_index == Method.AsAboveSoBelow:
            for index in indexes:
                self.varmodel.setData(index, None, StateRole)
        elif method_index == Method.Default:
            current = self.value_stack.currentWidget()
            if current is self.value_combo:
                value = self.value_combo.currentIndex()
            else:
                value = self.value_double.value()
            for index in indexes:
                state = (int(Method.Default), (value, ))
                self.varmodel.setData(index, state, StateRole)
        else:
            state = (int(method_index), ())
            for index in indexes:
                self.varmodel.setData(index, state, StateRole)

        self.update_varview(indexes)
        self._invalidate()

    def update_varview(self, indexes=None):
        if indexes is None:
            indexes = map(self.varmodel.index, range(len(self.varmodel)))

        for index in indexes:
            self.varmodel.setData(index,
                                  self.get_method_for_column(index.row()),
                                  DisplayMethodRole)

    def _on_value_selected(self):
        # The fixed 'Value' in the widget has been changed by the user.
        self.variable_button_group.button(Method.Default).setChecked(True)
        self.set_method_for_current_selection(Method.Default)

    def reset_variable_state(self):
        indexes = list(map(self.varmodel.index, range(len(self.varmodel))))
        self.set_method_for_indexes(indexes, Method.AsAboveSoBelow)
        self.variable_button_group.button(
            Method.AsAboveSoBelow).setChecked(True)

    def _store_state(self):
        # type: () -> VariableState
        """
        Save the current variable imputation state
        """
        state = {}  # type: VariableState
        for i, var in enumerate(self.varmodel):
            index = self.varmodel.index(i)
            m = index.data(StateRole)
            if m is not None:
                state[var_key(var)] = m
        return state

    def _restore_state(self, state):
        # type: (VariableState) -> None
        """
        Restore the variable imputation state from the saved state
        """
        def check(state):
            # check if state is a proper State
            if isinstance(state, tuple) and len(state) == 2:
                m, p = state
                if isinstance(m, int) and isinstance(p, tuple) and \
                        0 <= m < len(Method):
                    return True
            return False

        for i, var in enumerate(self.varmodel):
            m = state.get(var_key(var), None)
            if check(m):
                self.varmodel.setData(self.varmodel.index(i), m, StateRole)

    def storeSpecificSettings(self):
        self._variable_imputation_state = self._store_state()
        super().storeSpecificSettings()