Repository: Zeroto521/my-data-toolkit

Issue:
ENH: New geoaccessor `geocentroid` to get the center point of points
<!--
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

```python
    >>> import dtoolkit.geoaccessor
    >>> import geopandas as gpd
    >>> from shapely import Point
    >>> df = gpd.GeoDataFrame(
    ...     geometry=[
    ...         Point(100, 32),
    ...         Point(120, 50),
    ...         Point(122, 55)
    ...     ],
    ...     crs=4326,
    ... )
    >>> df
                         geometry
    0  POINT (100.00000 32.00000)
    1  POINT (120.00000 50.00000)
    2  POINT (122.00000 55.00000)
    >>> df.geocentroid()
    <POINT (112.213 44.119)>
```


Discussion:
# [Codecov](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/832?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=40%25) Report
> Merging [#832](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/832?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=40%25) (c50a826) into [main](https://codecov.io/gh/Zeroto521/my-data-toolkit/commit/03ae0f06e6e754d4de2c49941ec7d57aac1b98cd?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=40%25) (03ae0f0) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

```diff
@@           Coverage Diff           @@
##             main     #832   +/-   ##
=======================================
  Coverage   99.70%   99.71%           
=======================================
  Files         147      149    +2     
  Lines        2053     2091   +38     
  Branches      462      470    +8     
=======================================
+ Hits         2047     2085   +38     
  Misses          1        1           
  Partials        5        5           
```


| [Impacted Files](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/832?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=40%25) | Coverage Δ | |
|---|---|---|
| [dtoolkit/geoaccessor/geodataframe/\_\_init\_\_.py](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/832?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=40%25#diff-ZHRvb2xraXQvZ2VvYWNjZXNzb3IvZ2VvZGF0YWZyYW1lL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | |
| [dtoolkit/geoaccessor/geodataframe/geocentroid.py](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/832?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=40%25#diff-ZHRvb2xraXQvZ2VvYWNjZXNzb3IvZ2VvZGF0YWZyYW1lL2dlb2NlbnRyb2lkLnB5) | `100.00% <100.00%> (ø)` | |
| [dtoolkit/geoaccessor/geoseries/\_\_init\_\_.py](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/832?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=40%25#diff-ZHRvb2xraXQvZ2VvYWNjZXNzb3IvZ2Vvc2VyaWVzL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | |
| [dtoolkit/geoaccessor/geoseries/geocentroid.py](https://codecov.io/gh/Zeroto521/my-data-toolkit/pull/832?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=40%25#diff-ZHRvb2xraXQvZ2VvYWNjZXNzb3IvZ2Vvc2VyaWVzL2dlb2NlbnRyb2lkLnB5) | `100.00% <100.00%> (ø)` | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=40%25)


Resolving patch:
diff --git a/dtoolkit/geoaccessor/geodataframe/geocentroid.py b/dtoolkit/geoaccessor/geodataframe/geocentroid.py
--- a/dtoolkit/geoaccessor/geodataframe/geocentroid.py
+++ b/dtoolkit/geoaccessor/geodataframe/geocentroid.py
@@ -18,7 +18,7 @@ def geocentroid(
     /,
     weights: Hashable | pd.Series = None,
     max_iter: int = 300,
-    tol: float = 1e-4,
+    tol: float = 1e-5,
 ) -> Point:
     if weights is not None and isinstance(weights, Hashable):
         weights = df[weights]
diff --git a/dtoolkit/geoaccessor/geoseries/geocentroid.py b/dtoolkit/geoaccessor/geoseries/geocentroid.py
--- a/dtoolkit/geoaccessor/geoseries/geocentroid.py
+++ b/dtoolkit/geoaccessor/geoseries/geocentroid.py
@@ -14,11 +14,21 @@ def geocentroid(
     /,
     weights: pd.Series = None,
     max_iter: int = 300,
-    tol: float = 1e-4,
+    tol: float = 1e-5,
 ) -> Point:
-    """
+    r"""
     Return the centroid of all points via the center of gravity method.
 
+    .. math::
+
+        \left\{\begin{matrix}
+            d_i &=& D(P(\bar{x}_n, \bar{y}_n), P(x_i, y_i))  \\
+            \bar{x}_0 &=& \frac{\sum w_i x_i}{\sum w_i} \\
+            \bar{y}_0 &=& \frac{\sum w_i y_i}{\sum w_i} \\
+            \bar{x}_{n+1} &=& \frac{\sum w_i x_i / d_i}{\sum w_i / d_i} \\
+            \bar{y}_{n+1} &=& \frac{\sum w_i y_i / d_i}{\sum w_i / d_i} \\
+        \end{matrix}\right.
+
     Parameters
     ----------
     weights : Hashable or 1d array-like, optional
@@ -29,18 +39,13 @@ def geocentroid(
     max_iter : int, default 300
         Maximum number of iterations to perform.
 
-    tol : float, default 1e-4
+    tol : float, default 1e-5
         Tolerance for convergence.
 
     Returns
     -------
     Point
 
-    Raises
-    ------
-    ValueError
-        If the CRS is not ``ESGP:4326``.
-
     See Also
     --------
     geopandas.GeoSeries.centroid
@@ -65,24 +70,21 @@ def geocentroid(
     1        2  POINT (120.00000 50.00000)
     2        3  POINT (122.00000 55.00000)
     >>> df.geocentroid()
-    <POINT (112.375 44.276)>
+    <POINT (120 50)>
 
     Set weights for each point.
 
     >>> df.geocentroid("weights")
-    <POINT (114.516 46.675)>
+    <POINT (121.999 54.998)>
     >>> df.geocentroid([1, 2, 3])
-    <POINT (114.516 46.675)>
+    <POINT (121.999 54.998)>
     """
 
-    if s.crs != 4326:
-        raise ValueError(f"Only support 'EPSG:4326' CRS, but got {s.crs!r}.")
-
     weights = np.asarray(weights) if weights is not None else 1
     coord = xy(s)
     X = coord.mean()
     for _ in range(max_iter):
-        dis = geodistance(s, Point(*X.tolist())).mul(weights, axis=0)
+        dis = geodistance(s, Point(*X.tolist())).rdiv(1).mul(weights, axis=0)
         Xt = coord.mul(dis, axis=0).sum() / dis.sum()
 
         if ((X - Xt).abs() <= tol).all():


Test patch:
diff --git a/test/geoaccessor/geoseries/test_geocentroid.py b/test/geoaccessor/geoseries/test_geocentroid.py
deleted file mode 100644
--- a/test/geoaccessor/geoseries/test_geocentroid.py
+++ /dev/null
@@ -1,32 +0,0 @@
-import geopandas as gpd
-import pytest
-from shapely import Point
-
-from dtoolkit.geoaccessor.geoseries import geocentroid
-
-
-@pytest.mark.parametrize(
-    "s, error",
-    [
-        (
-            gpd.GeoSeries(
-                [
-                    Point(100, 32),
-                    Point(120, 50),
-                    Point(122, 55),
-                ],
-            ),
-            ValueError,
-        ),
-    ],
-)
-def test_error(s, error):
-    with pytest.raises(error):
-        geocentroid(s)
-
-
-def test_tol():
-    # test `tol` parameter
-    result = gpd.GeoSeries([Point(100, 50), Point(120, 50)], crs=4326).geocentroid()
-
-    assert result.equals(Point(110, 50))