Repository: Zeroto521/my-data-toolkit

Issue:
EHN: New accessor `fillna_regresssion`
<!--
Thanks for contributing a pull request!

Please follow these standard acronyms to start the commit message:

- ENH: enhancement
- BUG: bug fix
- DOC: documentation
- TYP: type annotations
- TST: addition or modification of tests
- MAINT: maintenance commit (refactoring, typos, etc.)
- BLD: change related to building
- REL: related to releasing
- API: an (incompatible) API change
- DEP: deprecate something, or remove a deprecated object
- DEV: development tool or utility
- REV: revert an earlier commit
- PERF: performance improvement
- BOT: always commit via a bot
- CI: related to CI or CD
- CLN: Code cleanup
-->

- [ ] closes #xxxx
- [x] whatsnew entry

Fill na value with regression method



Discussion:
# [Codecov](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/556?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Zero) Report
> Merging [#556](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/556?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Zero) (74df406) into [main](https://codecov.io/gh/Zeroto521/my-data-toolkit/commit/09c19784745620291edf92403c1b2b26dcc6a65a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Zero) (09c1978) will **decrease** coverage by `0.10%`.
> The diff coverage is `95.00%`.

```diff
@@            Coverage Diff             @@
##             main     #556      +/-   ##
==========================================
- Coverage   99.64%   99.53%   -0.11%     
==========================================
  Files          60       61       +1     
  Lines         840      860      +20     
  Branches       96      100       +4     
==========================================
+ Hits          837      856      +19     
  Misses          1        1              
- Partials        2        3       +1     
```


| [Impacted Files](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/556?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Zero) | Coverage Δ | |
|---|---|---|
| [dtoolkit/accessor/dataframe/fillna\_regression.py](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/556/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Zero#diff-ZHRvb2xraXQvYWNjZXNzb3IvZGF0YWZyYW1lL2ZpbGxuYV9yZWdyZXNzaW9uLnB5) | `94.73% <94.73%> (ø)` | |
| [dtoolkit/accessor/dataframe/\_\_init\_\_.py](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/556/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Zero#diff-ZHRvb2xraXQvYWNjZXNzb3IvZGF0YWZyYW1lL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/556?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Zero).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Zero)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/556?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Zero). Last update [09c1978...74df406](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/556?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Zero). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Zero).


Resolving patch:
diff --git a/dtoolkit/accessor/dataframe/fillna_regression.py b/dtoolkit/accessor/dataframe/fillna_regression.py
--- a/dtoolkit/accessor/dataframe/fillna_regression.py
+++ b/dtoolkit/accessor/dataframe/fillna_regression.py
@@ -15,8 +15,7 @@
 def fillna_regression(
     df: pd.DataFrame,
     method: RegressorMixin,
-    X: IntOrStr | list[IntOrStr] | pd.Index,
-    y: IntOrStr,
+    columns: dict[IntOrStr, IntOrStr | list[IntOrStr] | pd.Index],
     how: str = "na",
     **kwargs,
 ) -> pd.DataFrame:
@@ -28,11 +27,9 @@ def fillna_regression(
     method : RegressorMixin
         Regression transformer.
 
-    X : int or str, list of int or str, Index
-        Feature columns.
-
-    y : int or str
-        Target column.
+    columns : dict, ``{y: X}``
+        A series of column names pairs. The key is the y (or target) column name, and
+        values are X (or feature) column names.
 
     how : {'na', 'all'}, default 'na'
         Only fill na value or apply regression to entire target column.
@@ -41,6 +38,10 @@ def fillna_regression(
         See the documentation for ``method`` for complete details on
         the keyword arguments.
 
+    Returns
+    -------
+    DataFrame
+
     See Also
     --------
     sklearn.kernel_ridge
@@ -94,7 +95,7 @@ def fillna_regression(
 
     Use 'x1' and 'x2' columns to fit 'y' column and fill the value.
 
-    >>> df.fillna_regression(LinearRegression, ['x1', 'x2'], 'y')
+    >>> df.fillna_regression(LinearRegression, {'y': ['x1', 'x2']})
        x1  x2     y
     0   1   1   6.0
     1   1   2   8.0
@@ -106,6 +107,22 @@ def fillna_regression(
     if how not in {"na", "all"}:
         raise ValueError(f"invalid inf option: {how!r}")
 
+    for y, X in columns.items():
+        df = _fillna_regression(df, method, y, X, how=how, **kwargs)
+
+    return df
+
+
+def _fillna_regression(
+    df: pd.DataFrame,
+    method: RegressorMixin,
+    y: IntOrStr,
+    X: IntOrStr | list[IntOrStr] | pd.Index,
+    how: str = "na",
+    **kwargs,
+):
+    """Fill single na column at once."""
+
     if isinstance(X, (str, int)):
         X = [X]
 


Test patch:
diff --git a/test/accessor/dataframe/test_fillna_regression.py b/test/accessor/dataframe/test_fillna_regression.py
--- a/test/accessor/dataframe/test_fillna_regression.py
+++ b/test/accessor/dataframe/test_fillna_regression.py
@@ -7,7 +7,7 @@
 
 
 @pytest.mark.parametrize(
-    "df, method, X, y, how, kwargs, expected",
+    "df, method, columns, how, kwargs, expected",
     [
         (
             pd.DataFrame(
@@ -21,8 +21,7 @@
                 columns=["x1", "x2", "y"],
             ),
             linear_model.LinearRegression,
-            ["x1", "x2"],
-            "y",
+            {"y": ["x1", "x2"]},
             "na",
             {},
             pd.DataFrame(
@@ -48,8 +47,7 @@
                 columns=["x1", "x2", "y"],
             ),
             linear_model.Ridge,
-            ["x1", "x2"],
-            "y",
+            {"y": ["x1", "x2"]},
             "na",
             {},
             pd.DataFrame(
@@ -75,8 +73,7 @@
                 columns=["x1", "x2", "y"],
             ),
             linear_model.LinearRegression,
-            ["x1", "x2"],
-            "y",
+            {"y": ["x1", "x2"]},
             "all",
             {},
             pd.DataFrame(
@@ -102,8 +99,7 @@
                 columns=["x1", "x2", "y"],
             ),
             linear_model.LinearRegression,
-            "x1",
-            "y",
+            {"y": "x1"},
             "na",
             {},
             pd.DataFrame(
@@ -129,8 +125,7 @@
                 columns=["x1", "x2", "y"],
             ),
             linear_model.LinearRegression,
-            "x2",
-            "y",
+            {"y": "x2"},
             "na",
             {},
             pd.DataFrame(
@@ -156,8 +151,7 @@
                 columns=["x1", "x2", "y"],
             ),
             linear_model.LinearRegression,
-            ["x1"],
-            "y",
+            {"y": ["x1"]},
             "all",
             {},
             pd.DataFrame(
@@ -183,8 +177,7 @@
                 columns=["x1", "x2", "y"],
             ),
             linear_model.LinearRegression,
-            pd.Index(["x1"]),
-            "y",
+            {"y": pd.Index(["x1"])},
             "all",
             {},
             pd.DataFrame(
@@ -210,8 +203,7 @@
                 columns=["x1", "x2", "y"],
             ),
             tree.DecisionTreeRegressor,
-            ["x1", "x2"],
-            "y",
+            {"y": ["x1", "x2"]},
             "na",
             dict(criterion="friedman_mse", splitter="best"),
             pd.DataFrame(
@@ -225,10 +217,62 @@
                 columns=["x1", "x2", "y"],
             ),
         ),
+        (
+            pd.DataFrame(
+                [
+                    [1, 1, None],
+                    [1, 2, 8],
+                    [2, 2, 9],
+                    [2, 3, 11],
+                    [3, 5, None],
+                ],
+                columns=["x1", "x2", "y"],
+            ),
+            tree.DecisionTreeRegressor,
+            {"y": "x2", "y": ["x1"], "y": ["x1", "x2"]},  # noqa: F601
+            "na",
+            {},
+            pd.DataFrame(
+                [
+                    [1, 1, 6],
+                    [1, 2, 8],
+                    [2, 2, 9],
+                    [2, 3, 11],
+                    [3, 5, 11],
+                ],
+                columns=["x1", "x2", "y"],
+            ),
+        ),
+        (
+            pd.DataFrame(
+                [
+                    [1, 1, None, 6],
+                    [1, 2, 8, 8],
+                    [2, 2, 9, 9],
+                    [2, 3, 11, 11],
+                    [3, 5, None, None],
+                ],
+                columns=["x1", "x2", "y1", "y2"],
+            ),
+            tree.DecisionTreeRegressor,
+            {"y2": ["x1"], "y1": ["x1", "x2"]},
+            "na",
+            {},
+            pd.DataFrame(
+                [
+                    [1, 1, 6, 6],
+                    [1, 2, 8, 8],
+                    [2, 2, 9, 9],
+                    [2, 3, 11, 11],
+                    [3, 5, 11, 11],
+                ],
+                columns=["x1", "x2", "y1", "y2"],
+            ),
+        ),
     ],
 )
-def test_work(df, method, X, y, how, kwargs, expected):
-    result = df.fillna_regression(method, X, y, how=how, **kwargs)
+def test_work(df, method, columns, how, kwargs, expected):
+    result = df.fillna_regression(method, columns, how=how, **kwargs)
 
     assert result.equals(result)
 
@@ -245,4 +289,4 @@ def test_error():
         columns=["x1", "x2", "y"],
     )
     with pytest.raises(ValueError):
-        df.fillna_regression(linear_model.MultiTaskElasticNet, "x1", "y", how="blah")
+        df.fillna_regression(linear_model.MultiTaskElasticNet, {"y": "x1"}, how="blah")