Repository: pyjanitor-devs/pyjanitor
Pull request #1083 (2022-05-02 20:04:28)

[DOC/ENH] Add doctests to `math`
# PR Description

Please describe the changes proposed in the pull request:

- Add doctests to `math` module
- Fix unnecessary restriction in `logit()`
- Update tests for `logit()`

**This PR relates to #972.**

# PR Checklist

Please ensure that you have done the following:

1. [x] PR in from a fork off your branch. Do not PR from `<your_username>`:`dev`, but rather from `<your_username>`:`<feature-branch_name>`.
<!-- Doing this helps us keep the commit history much cleaner than it would otherwise be. -->
2. [x] If you're not on the contributors list, add yourself to `AUTHORS.md`.
<!-- We'd like to acknowledge your contributions! -->
3. [x] Add a line to `CHANGELOG.md` under the latest version header (i.e. the one that is "on deck") describing the contribution.
    - Do use some discretion here; if there are multiple PRs that are related, keep them in a single line.

# Automatic checks

There will be automatic checks run on the PR. These include:

- Building a preview of the docs on Netlify
- Automatically linting the code
- Making sure the code is documented
- Making sure that all tests are passed
- Making sure that code coverage doesn't go down.

# Relevant Reviewers

<!-- Finally, please tag relevant maintainers to review. -->

Please tag maintainers to review.

- @ericmjl


codecov[bot]: # [Codecov](https://codecov.io/gh/pyjanitor-devs/pyjanitor/pull/1083?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyjanitor-devs) Report
> Merging [#1083](https://codecov.io/gh/pyjanitor-devs/pyjanitor/pull/1083?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyjanitor-devs) (a98b53c) into [dev](https://codecov.io/gh/pyjanitor-devs/pyjanitor/commit/de8e33c3696f3b1af88c4428cf1c58ca138e73e5?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyjanitor-devs) (de8e33c) will **not change** coverage.
> The diff coverage is `100.00%`.

> :exclamation: Current head a98b53c differs from pull request most recent head 94391f9. Consider uploading reports for the commit 94391f9 to get more accurate results


```diff
@@           Coverage Diff           @@
##              dev    #1083   +/-   ##
=======================================
  Coverage   97.72%   97.72%           
=======================================
  Files          77       77           
  Lines        3121     3121           
=======================================
  Hits         3050     3050           
  Misses         71       71           
```


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 21b0534..2aabc30 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -29,6 +29,7 @@
 -   [DOC] Added a step in the dev guide to install `Remote Container` in VS Code. @ashenafiyb
 -   [DOC] Convert `expand_column` and `find_replace` code examples to doctests, issue #972. @gahjelle
 -   [DOC] Convert `engineering` code examples to doctests, issue #972 @ashenafiyb
+-   [DOC] Add doctests to `math`, issue #972. @gahjelle
 
 ## [v0.22.0] - 2021-11-21
 
diff --git a/janitor/math.py b/janitor/math.py
index 3bfedac..7a023c2 100644
--- a/janitor/math.py
+++ b/janitor/math.py
@@ -8,6 +8,7 @@ import pandas as pd
 import pandas_flavor as pf
 from pandas.api.types import is_numeric_dtype
 from scipy.special import expit
+from scipy.special import logit as scipy_logit
 from scipy.special import softmax as scipy_softmax
 from scipy.stats import norm
 
@@ -17,6 +18,18 @@ def log(s: pd.Series, error: str = "warn") -> pd.Series:
     """
     Take natural logarithm of the Series.
 
+    Each value in the series should be positive. Use `error` to control the
+    behavior if there are nonpositive entries in the series.
+
+        >>> import pandas as pd
+        >>> import janitor
+        >>> s = pd.Series([0, 1, 3], name="numbers")
+        >>> s.log(error="ignore")
+        0         NaN
+        1    0.000000
+        2    1.098612
+        Name: numbers, dtype: float64
+
     :param s: Input Series.
     :param error: Determines behavior when taking the log of nonpositive
         entries. If `'warn'` then a `RuntimeWarning` is thrown. If `'raise'`,
@@ -45,6 +58,15 @@ def exp(s: pd.Series) -> pd.Series:
     """
     Take the exponential transform of the series.
 
+        >>> import pandas as pd
+        >>> import janitor
+        >>> s = pd.Series([0, 1, 3], name="numbers")
+        >>> s.exp()
+        0     1.000000
+        1     2.718282
+        2    20.085537
+        Name: numbers, dtype: float64
+
     :param s: Input Series.
     :return: Transformed Series.
     """
@@ -60,6 +82,15 @@ def sigmoid(s: pd.Series) -> pd.Series:
     sigmoid(x) = 1 / (1 + exp(-x))
     ```
 
+        >>> import pandas as pd
+        >>> import janitor
+        >>> s = pd.Series([-1, 0, 4], name="numbers")
+        >>> s.sigmoid()
+        0    0.268941
+        1    0.500000
+        2    0.982014
+        Name: numbers, dtype: float64
+
     :param s: Input Series.
     :return: Transformed Series.
     """
@@ -81,6 +112,15 @@ def softmax(s: pd.Series) -> pd.Series:
     softmax(x) = exp(x)/sum(exp(x))
     ```
 
+        >>> import pandas as pd
+        >>> import janitor
+        >>> s = pd.Series([0, 1, 3], name="numbers")
+        >>> s.softmax()
+        0    0.042010
+        1    0.114195
+        2    0.843795
+        Name: numbers, dtype: float64
+
     :param s: Input Series.
     :return: Transformed Series.
     """
@@ -96,8 +136,20 @@ def logit(s: pd.Series, error: str = "warn") -> pd.Series:
     logit(p) = log(p/(1-p))
     ```
 
+    Each value in the series should be between 0 and 1. Use `error` to
+    control the behavior if any series entries are outside of (0, 1).
+
+        >>> import pandas as pd
+        >>> import janitor
+        >>> s = pd.Series([0.1, 0.5, 0.9], name="numbers")
+        >>> s.logit()
+        0   -2.197225
+        1    0.000000
+        2    2.197225
+        Name: numbers, dtype: float64
+
     :param s: Input Series.
-    :param error: Determines behavior when `s / (1-s)` is outside of `(0, 1)`.
+    :param error: Determines behavior when `s` is outside of `(0, 1)`.
         If `'warn'` then a `RuntimeWarning` is thrown. If `'raise'`, then a
         `RuntimeError` is thrown. Otherwise, nothing is thrown and `np.nan`
         is returned for the problematic entries; defaults to `'warn'`.
@@ -105,19 +157,17 @@ def logit(s: pd.Series, error: str = "warn") -> pd.Series:
     :raises RuntimeError: if `error` is set to `'raise'`.
     """
     s = s.copy()
-    odds_ratio = s / (1 - s)
-    outside_support = (odds_ratio <= 0) | (odds_ratio >= 1)
+    outside_support = (s <= 0) | (s >= 1)
     if (outside_support).any():
-        msg = f"Odds ratio for {outside_support.sum()} value(s) \
-are outside of (0, 1)"
+        msg = f"{outside_support.sum()} value(s) are outside of (0, 1)"
         if error.lower() == "warn":
             warnings.warn(msg, RuntimeWarning)
         if error.lower() == "raise":
             raise RuntimeError(msg)
         else:
             pass
-    odds_ratio[outside_support] = np.nan
-    return odds_ratio.log(error="ignore")
+    s[outside_support] = np.nan
+    return scipy_logit(s)
 
 
 @pf.register_series_method
@@ -125,6 +175,15 @@ def normal_cdf(s: pd.Series) -> pd.Series:
     """
     Transforms the Series via the CDF of the Normal distribution.
 
+        >>> import pandas as pd
+        >>> import janitor
+        >>> s = pd.Series([-1, 0, 3], name="numbers")
+        >>> s.normal_cdf()
+        0    0.158655
+        1    0.500000
+        2    0.998650
+        dtype: float64
+
     :param s: Input Series.
     :return: Transformed Series.
     """
@@ -136,6 +195,18 @@ def probit(s: pd.Series, error: str = "warn") -> pd.Series:
     """
     Transforms the Series via the inverse CDF of the Normal distribution.
 
+    Each value in the series should be between 0 and 1. Use `error` to
+    control the behavior if any series entries are outside of (0, 1).
+
+        >>> import pandas as pd
+        >>> import janitor
+        >>> s = pd.Series([0.1, 0.5, 0.8], name="numbers")
+        >>> s.probit()
+        0   -1.281552
+        1    0.000000
+        2    0.841621
+        dtype: float64
+
     :param s: Input Series.
     :param error: Determines behavior when `s` is outside of `(0, 1)`.
         If `'warn'` then a `RuntimeWarning` is thrown. If `'raise'`, then
@@ -174,6 +245,15 @@ def z_score(
     z = (s - s.mean()) / s.std()
     ```
 
+        >>> import pandas as pd
+        >>> import janitor
+        >>> s = pd.Series([0, 1, 3], name="numbers")
+        >>> s.z_score()
+        0   -0.872872
+        1   -0.218218
+        2    1.091089
+        Name: numbers, dtype: float64
+
     :param s: Input Series.
     :param moments_dict: If not `None`, then the mean and standard
         deviation used to compute the z-score transformation is
@@ -216,6 +296,15 @@ def ecdf(s: pd.Series) -> Tuple[np.ndarray, np.ndarray]:
     Also, if the `dtype` of the series is not numeric,
     a `TypeError` is raised.
 
+        >>> import pandas as pd
+        >>> import janitor
+        >>> df = pd.DataFrame({"numbers": [0, 4, 0, 1, 2, 1, 1, 3]})
+        >>> x, y = df["numbers"].ecdf()
+        >>> x
+        array([0, 0, 1, 1, 1, 2, 3, 4])
+        >>> y
+        array([0.125, 0.25 , 0.375, 0.5  , 0.625, 0.75 , 0.875, 1.   ])
+
     :param s: A pandas series. `dtype` should be numeric.
     :returns: `(x, y)`.
         `x`: sorted array of values.


Test patch:
diff --git a/tests/math/test_logit.py b/tests/math/test_logit.py
index a6f52b5..0ae84d3 100644
--- a/tests/math/test_logit.py
+++ b/tests/math/test_logit.py
@@ -5,9 +5,9 @@ import pytest
 
 @pytest.mark.functions
 def test_logit():
-    s = pd.Series([0, 0.1, 0.2, 0.3, 1, 2])
+    s = pd.Series([0, 0.1, 0.2, 0.3, 0.5, 0.9, 1, 2])
     inside = (0 < s) & (s < 1)
-    valid = np.array([0.1, 0.2, 0.3])
+    valid = np.array([0.1, 0.2, 0.3, 0.5, 0.9])
     ans = np.log(valid / (1 - valid))
 
     with pytest.raises(RuntimeError):
@@ -17,13 +17,13 @@ def test_logit():
         out = s.logit(error="warn")
 
     assert out[inside].notna().all()
-    assert (out[inside] == ans).all()
+    assert out[inside].to_numpy() == pytest.approx(ans)
     assert (out.index == s.index).all()
     assert out[~inside].isna().all()
 
     out = s.logit(error="ignore")
 
     assert out[inside].notna().all()
-    assert (out[inside] == ans).all()
+    assert out[inside].to_numpy() == pytest.approx(ans)
     assert (out.index == s.index).all()
     assert out[~inside].isna().all()