diff --git a/rulekit/_helpers.py b/rulekit/_helpers.py index 56fa4d1..4f6aceb 100644 --- a/rulekit/_helpers.py +++ b/rulekit/_helpers.py @@ -24,6 +24,40 @@ from rulekit.rules import BaseRule +def _missing_to_none(value: Any) -> Any: + """Map pandas/numpy missing values to None for the Java DataTable. + + Args: + value: A single cell value. + + Returns: + None if the value is missing, otherwise the original value. + """ + if value is None: + return None + try: + if pd.isna(value): + return None + except (TypeError, ValueError): + pass + return value + + +def _to_java_object_array(values: np.ndarray) -> np.ndarray: + """Copy an array to dtype=object with missing cells as None. + + Args: + values: Array produced from a pandas frame or series. + + Returns: + Object array safe to pass through JPype into DataTable. + """ + result = np.empty(values.shape, dtype=object) + for index, value in np.ndenumerate(values): + result[index] = _missing_to_none(value) + return result + + def get_rule_generator(expert: bool = False) -> Any: """Factory for Java RuleGenerator class object @@ -259,7 +293,7 @@ def _sanitize_y(self, y: Union[pd.Series, np.ndarray, list]): elif isinstance(y, pd.Series): self._label_name = y.name self._attributes_names.append(self._label_name) - self._y = y.to_numpy() + self._y = _to_java_object_array(y.to_numpy()) elif isinstance(y, list): self._label_name = self.DEFAULT_LABEL_ATTRIBUTE_NAME self._attributes_names.append(self._label_name) @@ -281,9 +315,7 @@ def _sanitize_X( ) -> tuple[np.ndarray, np.ndarray]: if isinstance(X, pd.DataFrame): self._attributes_names = X.columns.tolist() - # replace nan values with None - X = X.where(pd.notnull(X), None) - self._X = X.to_numpy() + self._X = _to_java_object_array(X.astype(object).to_numpy()) elif isinstance(X, np.ndarray): self._attributes_names = [ f"{self.AUTOMATIC_ATTRIBUTES_NAMES_PREFIX}{index + 1}" diff --git a/rulekit/_operator.py b/rulekit/_operator.py index 8fe6e43..a44fdc0 100644 --- a/rulekit/_operator.py +++ b/rulekit/_operator.py @@ -1,5 +1,5 @@ -"""Contains base classes for rule induction operators -""" +"""Contains base classes for rule induction operators""" + from __future__ import annotations from abc import ABC @@ -79,9 +79,7 @@ def fit( # pylint: disable=missing-function-docstring self.model = RuleSet[BaseRule](java_model) return self.model - def predict( - self, values: Data - ) -> np.ndarray: # pylint: disable=missing-function-docstring + def predict(self, values: Data) -> np.ndarray: # pylint: disable=missing-function-docstring if self.model is None: raise ValueError('"fit" method must be called before calling this method') example_set = ExampleSetFactory(self._get_problem_type()).make(values) @@ -90,7 +88,8 @@ def predict( ) def get_params( - self, deep: bool = True # pylint: disable=unused-argument + self, + deep: bool = True, # pylint: disable=unused-argument ) -> dict[str, Any]: """ Parameters @@ -146,6 +145,17 @@ def get_coverage_matrix(self, values: Data) -> np.ndarray: if self.model is None: raise ValueError('"fit" method must be called before calling this method') example_set = ExampleSetFactory(self._get_problem_type()).make(values) + + # update mapping for nominal attributes + header = self.model._java_object.getTrainingHeader() + if header is not None: + try: + example_set = example_set.updateMapping(header) + except Exception as e: + raise ValueError( + "Error updating mapping. Check if the given set agrees with the training set." + ) from e + covering_info = self.model.covering(example_set) if isinstance(values, (pd.Series, pd.DataFrame)): values = values.to_numpy() diff --git a/rulekit/jar/rulekit-2.1.24-all.jar b/rulekit/jar/rulekit-2.1.26-all.jar similarity index 99% rename from rulekit/jar/rulekit-2.1.24-all.jar rename to rulekit/jar/rulekit-2.1.26-all.jar index 5aab357..5034d7c 100644 Binary files a/rulekit/jar/rulekit-2.1.24-all.jar and b/rulekit/jar/rulekit-2.1.26-all.jar differ diff --git a/rulekit/main.py b/rulekit/main.py index 9703e91..6d437c2 100644 --- a/rulekit/main.py +++ b/rulekit/main.py @@ -14,7 +14,7 @@ from rulekit._logging import _RuleKitJavaLoggerConfig -__RULEKIT_RELEASE_VERSION__ = "2.1.24" +__RULEKIT_RELEASE_VERSION__ = "2.1.26" __VERSION__ = f"{__RULEKIT_RELEASE_VERSION__}.1" diff --git a/setup.py b/setup.py index 544c215..6c104c2 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setuptools.setup( name="rulekit", - version='2.1.24.2', + version='2.1.26.1', author="Cezary Maszczyk", author_email="cezary.maszczyk@gmail.com", description="Comprehensive suite for rule-based learning", @@ -34,7 +34,7 @@ python_requires='>=3.9', install_requires=[ 'numpy>=1.24', - 'pandas>=1.5', + 'pandas>=1.5,<=3.0.5', 'scipy>=1.11', 'scikit-learn>=1.1', 'JPype1==1.5.2', diff --git a/tests/test_survival.py b/tests/test_survival.py index bda5f44..9ede103 100644 --- a/tests/test_survival.py +++ b/tests/test_survival.py @@ -135,9 +135,7 @@ def test_passing_survival_time_column_to_fit_method(self): ) X, y = test_case.example_set.values, test_case.example_set.labels survival_time_col: pd.Series = X[test_case.survival_time] - X_without_time_col: pd.DataFrame = X.drop( - columns=[test_case.survival_time], axis=1 - ) + X_without_time_col: pd.DataFrame = X.drop(columns=[test_case.survival_time]) surv1.fit(X_without_time_col, y, survival_time=survival_time_col) surv2.fit(X, y) @@ -154,9 +152,7 @@ def test_ibs_calculation(self): ) X, y = test_case.example_set.values, test_case.example_set.labels survival_time_col: pd.Series = X[test_case.survival_time] - X_without_time_col: pd.DataFrame = X.drop( - columns=[test_case.survival_time], axis=1 - ) + X_without_time_col: pd.DataFrame = X.drop(columns=[test_case.survival_time]) surv.fit(X, y) ibs: float = surv.score(X, y)