Repository: Toloka/crowd-kit

Issue:
[BUG] `OneCoinDawidSkene` is broken after vesion `1.4.0`. 
### Observed behavior

In crowd-kit 1.4.0 and later, the following error occurs when running `fit_predict()` on `OneCoinDawidSkene`.

Code (Same as the example in docs):
```
from crowdkit.aggregation import OneCoinDawidSkene
from crowdkit.datasets import load_dataset
df, gt = load_dataset('relevance-2')
hds = OneCoinDawidSkene(100)
result = hds.fit_predict(df)
```
Error:
```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
[<ipython-input-2-bc24fa88a4e5>](https://localhost:8080/#) in <cell line: 5>()
      3 df, gt = load_dataset('relevance-2')
      4 hds = OneCoinDawidSkene(100)
----> 5 result = hds.fit_predict(df)

[/usr/local/lib/python3.10/dist-packages/crowdkit/aggregation/classification/dawid_skene.py](https://localhost:8080/#) in fit_predict(self, data, true_labels, initial_error)
    378         """
    379 
--> 380         self.fit(data, true_labels, initial_error)
    381         assert self.labels_ is not None, "no labels_"
    382         return self.labels_

TypeError: OneCoinDawidSkene.fit() takes 2 positional arguments but 4 were given
```

I believe the following commits are related to this issue:
https://github.com/Toloka/crowd-kit/commit/e67d3a73bcec8898addf15da324da54b2e74629d

### Expected behavior

The argument problem is fixed and the algorithm runs successfully.

In `1.3.0.post0`, this issue is not caused.

Thank you for reading my issue. 
I appreciate the all contributors of the crowd-kit.

### Python Version

3.1

### Crowd-Kit Version

1.4.1

### Other Packages Versions

_No response_

### Example code

```Python
from crowdkit.aggregation import OneCoinDawidSkene
from crowdkit.datasets import load_dataset
df, gt = load_dataset('relevance-2')
hds = OneCoinDawidSkene(100)
result = hds.fit_predict(df)
```


### Relevant log output

```shell
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
[<ipython-input-2-bc24fa88a4e5>](https://localhost:8080/#) in <cell line: 5>()
      3 df, gt = load_dataset('relevance-2')
      4 hds = OneCoinDawidSkene(100)
----> 5 result = hds.fit_predict(df)

[/usr/local/lib/python3.10/dist-packages/crowdkit/aggregation/classification/dawid_skene.py](https://localhost:8080/#) in fit_predict(self, data, true_labels, initial_error)
    378         """
    379 
--> 380         self.fit(data, true_labels, initial_error)
    381         assert self.labels_ is not None, "no labels_"
    382         return self.labels_

TypeError: OneCoinDawidSkene.fit() takes 2 positional arguments but 4 were given
```

Discussion:
@shenxiangzhuang: Hi @takumi1001, thanks for reporting this issue. I'll try to fix it later.

Actually, there is a quick fix: override `fit_predict` and `fit_predict_proba` methods in `OneCoinDawidSkene`. And the implementation is the same as the old methods used in `DawidSkene` class. With this approach, we can keep `OneCoinDawidSkene` behavior as before without sacrificing readability. @dustalov What do you think about this?@dustalov: Thank you for the report!

@shenxiangzhuang, please override the necessary methods and add corresponding tests to address potential inheritance issues in the future. Once that's done, I'll proceed with shipping the release.@takumi1001: Thank you for your quick response and bugfix 🙇 

Solution patch:
diff --git a/crowdkit/aggregation/classification/dawid_skene.py b/crowdkit/aggregation/classification/dawid_skene.py
index 65bca6b..07fe1f5 100644
--- a/crowdkit/aggregation/classification/dawid_skene.py
+++ b/crowdkit/aggregation/classification/dawid_skene.py
@@ -578,3 +578,31 @@ def fit(self, data: pd.DataFrame) -> "OneCoinDawidSkene":  # type: ignore[overri
         self.labels_ = get_most_probable_labels(probas)
 
         return self
+
+    def fit_predict_proba(self, data: pd.DataFrame) -> pd.DataFrame:  # type: ignore[override]
+        """Fits the model to the training data and returns probability distributions of labels for each task.
+        Args:
+            data (DataFrame): The training dataset of workers' labeling results
+                which is represented as the `pandas.DataFrame` data containing `task`, `worker`, and `label` columns.
+        Returns:
+            DataFrame: Probability distributions of task labels.
+                The `pandas.DataFrame` data is indexed by `task` so that `result.loc[task, label]` is the probability that the `task` true label is equal to `label`.
+                Each probability is in the range from 0 to 1, all task probabilities must sum up to 1.
+        """
+
+        self.fit(data)
+        assert self.probas_ is not None, "no probas_"
+        return self.probas_
+
+    def fit_predict(self, data: pd.DataFrame) -> "pd.Series[Any]":  # type: ignore[override]
+        """Fits the model to the training data and returns the aggregated results.
+        Args:
+            data (DataFrame): The training dataset of workers' labeling results
+                which is represented as the `pandas.DataFrame` data containing `task`, `worker`, and `label` columns.
+        Returns:
+            Series: Task labels. The `pandas.Series` data is indexed by `task` so that `labels.loc[task]` is the most likely true label of tasks.
+        """
+
+        self.fit(data)
+        assert self.labels_ is not None, "no labels_"
+        return self.labels_


Test patch:
diff --git a/tests/aggregation/test_ds_aggregation.py b/tests/aggregation/test_ds_aggregation.py
index 8fac92d..3e447d0 100644
--- a/tests/aggregation/test_ds_aggregation.py
+++ b/tests/aggregation/test_ds_aggregation.py
@@ -404,6 +404,16 @@ def test_aggregate_hds_on_toy_ysda(
         toy_ground_truth_df.sort_index(),
     )
 
+    assert_series_equal(
+        OneCoinDawidSkene(n_iter=n_iter, tol=tol)
+        .fit_predict(toy_answers_df)
+        .sort_index(),
+        toy_ground_truth_df.sort_index(),
+    )
+
+    probas = OneCoinDawidSkene(n_iter=n_iter, tol=tol).fit_predict_proba(toy_answers_df)
+    assert ((probas >= 0) & (probas <= 1)).all().all()
+
 
 @pytest.mark.parametrize("n_iter, tol", [(10, 0), (100500, 1e-5)])
 def test_aggregate_ds_on_simple(
@@ -432,6 +442,18 @@ def test_aggregate_hds_on_simple(
         simple_ground_truth.sort_index(),
     )
 
+    assert_series_equal(
+        OneCoinDawidSkene(n_iter=n_iter, tol=tol)
+        .fit_predict(simple_answers_df)
+        .sort_index(),
+        simple_ground_truth.sort_index(),
+    )
+
+    probas = OneCoinDawidSkene(n_iter=n_iter, tol=tol).fit_predict_proba(
+        simple_answers_df
+    )
+    assert ((probas >= 0) & (probas <= 1)).all().all()
+
 
 def _make_probas(data: List[List[Any]]) -> pd.DataFrame:
     # TODO: column should not be an index!