Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: stats.moment: enable non-central moment calculation #18152

Merged
merged 12 commits into from Apr 24, 2023

Conversation

doronbehar
Copy link
Contributor

Reference issue

Closes gh-17749

What does this implement/fix?

Described in gh-17749.

Additional information

No unit tests and documentation added, just opened for reviewers to comment on the API - hence it's a draft.

Copy link
Member

@tupui tupui left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @doronbehar. An early suggestion.

scipy/stats/_mstats_basic.py Outdated Show resolved Hide resolved
@tupui tupui added scipy.stats enhancement A new feature or improvement labels Mar 15, 2023
@@ -2551,7 +2551,7 @@ def _winsorize1D(a, low_limit, up_limit, low_include, up_include,
upinc, contains_nan, nan_policy)


def moment(a, moment=1, axis=0):
def moment(a, moment=1, axis=0, *, central=None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@doronbehar mstats.moment is not deprecated, but it is essentially obsolete now that stats.moment supports masked arrays. To reduce the scope of the PR with little impact to users, let's only add this parameter to stats.moment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have totally missed the fact that I was editing the mstats.moment function and not the stats.moment function! However, Maybe it is mandatory to change moment from mstats_basic? Note my review comment in the upcoming push, that edits both mstats.moment and stats.moment similarly.

Copy link
Contributor

@mdhaber mdhaber Mar 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The call to mstats.moment in stats.moment is probably no longer necessary. The decorator on stats.moment should take care of this. Please try removing the call to mstats.moment from stats.moment and edit only stats.moment.

If you run into any test failures because of this, we'll deal with them later.

Let's also only change only stats.moment (public function). _moment is private, and it looks like changing the name of the existing parameter mean is causing a lot of problems. I guess mean is not the best name for that function either, but it already exists and it's private, so I'm not too concerned about it. We can fix that separately, if you'd like, but I'd rather not add it to the scope of this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I think I got it. I pushed again, rebased onto main, but not squashed. I had to rebase in order to make sure I changed as less as possible, as you requested. Haven't tested anything yet.

scipy/stats/_stats_py.py Show resolved Hide resolved
Comment on lines 2568 to 2569
The point about which moments are taken. This can be the sample mean, the
origin, or any other be point. If None, compute from the sample.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The point about which moments are taken. This can be the sample mean, the
origin, or any other be point. If None, compute from the sample.
The point about which moments are taken. This can be the sample mean,
the origin, or any other point. If None (default), use the mean
computed from the sample.

Need to specify what is computed from the sample. Also, we need to specify that computing the mean from the sample is the default.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to specify what is computed from the sample. Also, we need to specify that computing the mean from the sample is the default.

Done!

@@ -2551,7 +2551,7 @@ def _winsorize1D(a, low_limit, up_limit, low_include, up_include,
upinc, contains_nan, nan_policy)


def moment(a, moment=1, axis=0):
def moment(a, moment=1, axis=0, *, central=None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"central" is an adjective, which suggests that the user would pass a boolean (e.g. "yes, I want the central moment").

I suggested the noun, "center". The user passes the center about which the moment is taken. (e.g. "take the moment about 0").

In any case, this should be a noun of some sort if it expects the user to pass a real number.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"central" is an adjective, which suggests that the user would pass a boolean (e.g. "yes, I want the central moment").

I suggested the noun, "center". The user passes the center about which the moment is taken. (e.g. "take the moment about 0").

In any case, this should be a noun of some sort if it expects the user to pass a real number.

Totally agree, it was probably a mistake.

# for array_like moment input, return a value for each.
if not np.isscalar(moment):
mean = a.mean(axis, keepdims=True)
mmnt = [_moment(a, i, axis, mean=mean) for i in moment]
mmnt = [_moment(a, i, axis, mean) for i in moment]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
mmnt = [_moment(a, i, axis, mean) for i in moment]
mmnt = [_moment(a, i, axis, center) for i in moment]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed mean -> center!

return ma.array(mmnt)
else:
return _moment(a, moment, axis)
return _moment(a, moment, axis, mean)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return _moment(a, moment, axis, mean)
return _moment(a, moment, axis, center)

Comment on lines 2596 to 2599
if central is None:
mean = a.mean(axis, keepdims=True)
else:
mean = central
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if central is None:
mean = a.mean(axis, keepdims=True)
else:
mean = central
if center is None:
center = a.mean(axis, keepdims=True)

Using mean for this variable suggests that it is always the mean. It is not always the mean (which is the purpose of the PR).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using mean for this variable suggests that it is always the mean. It is not always the mean (which is the purpose of the PR).

Thanks for the suggestion! The code I wrote was probably a functional programming habit leftover 😛.

Copy link
Contributor Author

@doronbehar doronbehar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding mstats.moment and stats.moment and the associated _moment functions, perhaps could there be less code duplicacy?

@@ -2551,7 +2551,7 @@ def _winsorize1D(a, low_limit, up_limit, low_include, up_include,
upinc, contains_nan, nan_policy)


def moment(a, moment=1, axis=0):
def moment(a, moment=1, axis=0, *, central=None):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have totally missed the fact that I was editing the mstats.moment function and not the stats.moment function! However, Maybe it is mandatory to change moment from mstats_basic? Note my review comment in the upcoming push, that edits both mstats.moment and stats.moment similarly.

@@ -2551,7 +2551,7 @@ def _winsorize1D(a, low_limit, up_limit, low_include, up_include,
upinc, contains_nan, nan_policy)


def moment(a, moment=1, axis=0):
def moment(a, moment=1, axis=0, *, central=None):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"central" is an adjective, which suggests that the user would pass a boolean (e.g. "yes, I want the central moment").

I suggested the noun, "center". The user passes the center about which the moment is taken. (e.g. "take the moment about 0").

In any case, this should be a noun of some sort if it expects the user to pass a real number.

Totally agree, it was probably a mistake.

Comment on lines 2568 to 2569
The point about which moments are taken. This can be the sample mean, the
origin, or any other be point. If None, compute from the sample.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to specify what is computed from the sample. Also, we need to specify that computing the mean from the sample is the default.

Done!

Comment on lines 2596 to 2599
if central is None:
mean = a.mean(axis, keepdims=True)
else:
mean = central
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using mean for this variable suggests that it is always the mean. It is not always the mean (which is the purpose of the PR).

Thanks for the suggestion! The code I wrote was probably a functional programming habit leftover 😛.

# for array_like moment input, return a value for each.
if not np.isscalar(moment):
mean = a.mean(axis, keepdims=True)
mmnt = [_moment(a, i, axis, mean=mean) for i in moment]
mmnt = [_moment(a, i, axis, mean) for i in moment]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed mean -> center!

scipy/stats/_stats_py.py Show resolved Hide resolved
@@ -1078,19 +1084,19 @@ def moment(a, moment=1, axis=0, nan_policy='propagate'):

if contains_nan and nan_policy == 'omit':
a = ma.masked_invalid(a)
return mstats_basic.moment(a, moment, axis)
return mstats_basic.moment(a, moment, axis, center)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since mstats_basic.moment is called here, this PR requires changing it as well?

Also include review requested `mean` -> `center` argument rename.
The call to mstats.moment in stats.moment is no longer necessary.
@mdhaber mdhaber changed the title stats: Allow computing non central moments ENH: stats.moment: enable non-central moment calculation Mar 20, 2023
Copy link
Contributor

@mdhaber mdhaber left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @doronbehar, this looks much better.

As we continue, please don't rebase or force-push. We'll_ squash-merge this at the end, so there is no need to rewrite history until then.

Merging is fine, and then you can push without forcing. In fact, I'd suggest merging upstream/main, and then I think the existing CI errors will disappear.

The changes to _stats_py.py look great; just one suggestion about the description of center. I want to specify what is calculated from the sample - the sample mean.

One thing this needs is a test. Please add a new test_moment_center method to the TestMoments class of test_stats.py. I would suggest two cases:

  • Calculate the second moment about the origin, and compare against the naive implementation (i.e. np.sum(x**2)/x.size)
  • Calculate the third moment about an arbitrary point and compare against the naive implementation.

Another thing to consider is edge cases. What if the order of the moment is 1? Does this do the right thing? (I don't think so.) How about order zero? These would need to be tested, too.

scipy/stats/_stats_py.py Outdated Show resolved Hide resolved
doronbehar and others added 3 commits March 27, 2023 13:31
* main: (52 commits)
  ENH: stats.vonmises.fit: treat case in which location likelihood equation has no solution(scipy#18190)
  MAINT: stats.kendalltau: avoid overflow (scipy#18193)
  DOC: Optimize: Fix for side bar rendering on top of Hessian (scipy#18189)
  MAINT: optimize.linprog: fix bound checks for integrality > 1 (scipy#18160)
  MAINT: Windows distutils cdist/pdist shims (scipy#18169)
  BUG: interpolate: add x-y length validation for `make_smoothing_spline`. (scipy#18188)
  ENH: Added `_sf` method for anglit distribution (scipy#17832) (scipy#18178)
  DOC: Fixed missing curly bracket in scipy.css
  DOC: Improving wording and docs for legacy directive
  DOC: Move legacy directive to not be first in the file
  DOC: Legacy directive custom styling
  DOC: Add optional argument to Legacy directive
  DOC: Ignore legacy directive in refguide_check
  DOC: Documenting the usage of the legacy directive
  DOC: Add legacy directive for documentation
  MAINT: stats.ecdf: store number at risk just before events (scipy#18187)
  DOC: cite pip issue about multiple `--config-settings` (scipy#18174)
  DOC: update links for ARPACK to point to ARPACK-NG (scipy#18173)
  MAINT: stats.logistic.fit: simplify
  MAINT: optimize.root_scalar: return gracefully when callable returns NaN (scipy#18172)
  ...
Co-authored-by: Matt Haberland <mhaberla@calpoly.edu>
@doronbehar
Copy link
Contributor Author

I tried to add some tests, but most of them seem to fail, and I'm not sure why :(. For moments of order greater then 1 I get this weird ValueError:

================================================ FAILURES ================================================
_____________________________________ TestMoments.test_moment_center _____________________________________
scipy/stats/tests/test_stats.py:3012: in test_moment_center
    y = stats.moment(self.testcase, 2, center=1)
        self       = <scipy.stats.tests.test_stats.TestMoments object at 0x7fdbaccf3f70>
        y          = 7.5
scipy/stats/_axis_nan_policy.py:521: in axis_nan_policy_wrapper
    res = hypotest_fun_out(*samples, **kwds)
        _          = 'propagate'
        _no_deco   = False
        axis       = -1
        contains_nan = False
        contains_nans = [False]
        d_args     = {'a': [1, 2, 3, 4], 'moment': 2}
        default_axis = 0
        hypotest_fun_in = <function moment at 0x7fdbad5df1c0>
        hypotest_fun_out = <function moment at 0x7fdbad5df1c0>
        intersection = set()
        is_too_small = <function _axis_nan_policy_factory.<locals>.is_too_small at 0x7fdbad5df0a0>
        keepdims   = False
        kwd_samp   = []
        kwd_samples = []
        kwds       = {'center': 1, 'moment': 2}
        maxarg     = 4
        n_axes     = 1
        n_kwd_samp = 0
        n_out      = 1
        n_outputs  = <function _moment_outputs at 0x7fdbad5deef0>
        n_samp     = 1
        n_samples  = 1
        nan_policy = 'propagate'
        ndims      = array([1])
        new_shapes = [(4,)]
        override   = {'nan_propagation': True, 'vectorization': False}
        paired     = False
        params     = ['a', 'moment', 'axis', 'nan_policy', 'center']
        reduced_axes = 0
        result_to_tuple = <function <lambda> at 0x7fdbad5df010>
        sample     = array([1, 2, 3, 4])
        samples    = [array([1, 2, 3, 4])]
        sentinel   = None
        shapes     = [(4,)]
        tuple_to_result = <function _moment_result_object at 0x7fdbad5def80>
        vectorized = True
scipy/stats/_stats_py.py:1090: in moment
    return _moment(a, moment, axis, mean=center)
        a          = array([1, 2, 3, 4])
        axis       = 0
        center     = 1
        moment     = 2
        nan_policy = 'propagate'
scipy/stats/_stats_py.py:1129: in _moment
    eps = np.finfo(a_zero_mean.dtype).resolution * 10
        a          = array([1, 2, 3, 4])
        a_zero_mean = array([0, 1, 2, 3])
        axis       = 0
        current_n  = 2
        mean       = 1
        moment     = 2
        n_list     = [2]
/nix/store/0xhlg0vi0y8m5lxsm8khcqqma8rmy2jr-python3.10-numpy-1.24.2/lib/python3.10/site-packages/numpy/core/getlimits.py:492: in __new__
    raise ValueError("data type %r not inexact" % (dtype))
E   ValueError: data type <class 'numpy.int64'> not inexact
        cls        = <class 'numpy.finfo'>
        dtype      = <class 'numpy.int64'>
        dtypes     = [dtype('int64'), <class 'numpy.int64'>]
        newdtype   = <class 'numpy.int64'>
        obj        = None
======================================== short test summary info =========================================
FAILED scipy/stats/tests/test_stats.py::TestMoments::test_moment_center - ValueError: data type <class 'numpy.int64'> not inexact

And for moments of order 1, I get simply an assertion error - no matter what is the center I use, stats.moment seem to return 0...

Haven't looked into the edge cases yet, trying to get the simple ones working..

@doronbehar
Copy link
Contributor Author

This command will be handy:

python dev.py test -t scipy.stats.tests.test_stats::TestMoments

@mdhaber
Copy link
Contributor

mdhaber commented Apr 8, 2023

There are special cases in _moment when moment == 0 or moment == 1. This is appropriate when there is no center provided because the moments are taken about the mean. You'll need to change the condition under which these special cases are used.

For the ValueError, ensure that your data has floating point type. There is a little bug that I will fix separately.

from scipy.stats import moment
moment([1, 2, 3, 4, 5], moment=1)  # no ValueError
moment([1 2, 3, 4, 5], moment=1)  # ValueError

From here on, please do not rebase or force push; merging main and regular-push is fine if needed.

Copy link
Contributor

@mdhaber mdhaber left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see doronbehar#1. After merging, please address remaining comments.

scipy/stats/tests/test_stats.py Outdated Show resolved Hide resolved
scipy/stats/tests/test_stats.py Outdated Show resolved Hide resolved
@doronbehar doronbehar marked this pull request as ready for review April 8, 2023 20:56
* main: (51 commits)
  ENH: stats.ttest_ind: add degrees of freedom and confidence interval (scipy#18210)
  DOC fix link in release notes v1.7 (scipy#18258)
  BLD: fix missing build dependency on cython signature .txt files
  DOC: Clarify executable tutorials and MyST/Jupytext
  ENH: stats: Add relativistic Breit-Wigner Distribution (scipy#17505)
  MAINT: setup.sh as executable file
  DOC: remove content related to `setup.py` usage from the docs (scipy#18245)
  DOC: orthogonal_procrustes fix date of reference paper and DOI (scipy#18251)
  BLD: implement version check for minimum Cython version
  ci: touch up wheel build action
  DOC: link to info about Gitpod.
  ENH: ndimage.gaussian_filter: add `axes` keyword-only argument (scipy#18016)
  BUG: sparse: Fix LIL full-matrix assignment (scipy#18211)
  STY: Remove unnecessary semicolon
  DOC: Pin docutils and filter additional sphinx warnings
  BUG: vq.kmeans() compares signed diff to a threshold. (scipy#8727)
  MAINT: stats: consistently return NumPy numbers (scipy#18217)
  TST: stats.dunnett: fix seed and filter warnings in test_shapes
  DOC: add info to use codespaces.
  MAINT: add devcontainer configuration
  ...
@doronbehar
Copy link
Contributor Author

From some reason, I'm getting pytest errors unrelated to this PR:

================================================ FAILURES ================================================
_____________________________ TestMoments.test_constant_moments[0-1-float32] _____________________________
scipy/stats/tests/test_stats.py:3078: in test_constant_moments
    self._assert_equal(y, expect, dtype=dtype)
        dtype      = <class 'numpy.float32'>
        expect     = 0
        moment     = 1
        self       = <scipy.stats.tests.test_stats.TestMoments object at 0x7f68ff973610>
        x          = array([0.7774076 , 0.25942257, 0.37381315, 0.58759964, 0.2728219 ],
      dtype=float32)
        y          = 5.9604646e-09
scipy/stats/tests/test_stats.py:2999: in _assert_equal
    assert_array_equal(actual, expect)
        actual     = 5.9604646e-09
        dtype      = <class 'numpy.float32'>
        expect     = array(0)
        self       = <scipy.stats.tests.test_stats.TestMoments object at 0x7f68ff973610>
        shape      = None
/nix/store/syz2y6j53y5hpzbs7l0965zwxshi8iyl-python3-3.10.10/lib/python3.10/contextlib.py:79: in inner
    return func(*args, **kwds)
E   AssertionError:
E   Arrays are not equal
E
E   Mismatched elements: 1 / 1 (100%)
E   Max absolute difference: 5.96046457e-09
E   Max relative difference: inf
E    x: array(5.960465e-09, dtype=float32)
E    y: array(0)
        args       = (<built-in function eq>, 5.9604646e-09, array(0))
        func       = <function assert_array_compare at 0x7f694407dd80>
        kwds       = {'err_msg': '', 'header': 'Arrays are not equal', 'strict': False, 'verbose': True}
        self       = <contextlib._GeneratorContextManager object at 0x7f694407a740>
_____________________________ TestMoments.test_constant_moments[0-1-float64] _____________________________
scipy/stats/tests/test_stats.py:3078: in test_constant_moments
    self._assert_equal(y, expect, dtype=dtype)
        dtype      = <class 'numpy.float64'>
        expect     = 0
        moment     = 1
        self       = <scipy.stats.tests.test_stats.TestMoments object at 0x7f68ff973880>
        x          = array([0.3708528 , 0.19705428, 0.45985588, 0.0446123 , 0.79979588])
        y          = -2.2204460492503132e-17
scipy/stats/tests/test_stats.py:2999: in _assert_equal
    assert_array_equal(actual, expect)
        actual     = -2.2204460492503132e-17
        dtype      = <class 'numpy.float64'>
        expect     = array(0)
        self       = <scipy.stats.tests.test_stats.TestMoments object at 0x7f68ff973880>
        shape      = None
/nix/store/syz2y6j53y5hpzbs7l0965zwxshi8iyl-python3-3.10.10/lib/python3.10/contextlib.py:79: in inner
    return func(*args, **kwds)
E   AssertionError:
E   Arrays are not equal
E
E   Mismatched elements: 1 / 1 (100%)
E   Max absolute difference: 2.22044605e-17
E   Max relative difference: inf
E    x: array(-2.220446e-17)
E    y: array(0)
        args       = (<built-in function eq>, -2.2204460492503132e-17, array(0))
        func       = <function assert_array_compare at 0x7f694407dd80>
        kwds       = {'err_msg': '', 'header': 'Arrays are not equal', 'strict': False, 'verbose': True}
        self       = <contextlib._GeneratorContextManager object at 0x7f694407a740>
___________________________ TestMoments.test_constant_moments[0-1-complex128] ____________________________
scipy/stats/tests/test_stats.py:3078: in test_constant_moments
    self._assert_equal(y, expect, dtype=dtype)
        dtype      = <class 'numpy.complex128'>
        expect     = 0
        moment     = 1
        self       = <scipy.stats.tests.test_stats.TestMoments object at 0x7f68ff9738b0>
        x          = array([0.07695645+0.j, 0.51883515+0.j, 0.3068101 +0.j, 0.57754295+0.j,
       0.95943334+0.j])
        y          = (1.1102230246251566e-17+0j)
scipy/stats/tests/test_stats.py:2999: in _assert_equal
    assert_array_equal(actual, expect)
        actual     = (1.1102230246251566e-17+0j)
        dtype      = <class 'numpy.complex128'>
        expect     = array(0)
        self       = <scipy.stats.tests.test_stats.TestMoments object at 0x7f68ff9738b0>
        shape      = None
/nix/store/syz2y6j53y5hpzbs7l0965zwxshi8iyl-python3-3.10.10/lib/python3.10/contextlib.py:79: in inner
    return func(*args, **kwds)
E   AssertionError:
E   Arrays are not equal
E
E   Mismatched elements: 1 / 1 (100%)
E   Max absolute difference: 1.11022302e-17
E   Max relative difference: inf
E    x: array(1.110223e-17+0.j)
E    y: array(0)
        args       = (<built-in function eq>, (1.1102230246251566e-17+0j), array(0))
        func       = <function assert_array_compare at 0x7f694407dd80>
        kwds       = {'err_msg': '', 'header': 'Arrays are not equal', 'strict': False, 'verbose': True}
        self       = <contextlib._GeneratorContextManager object at 0x7f694407a740>
======================================== short test summary info =========================================
FAILED scipy/stats/tests/test_stats.py::TestMoments::test_constant_moments[0-1-float32] - AssertionError:
FAILED scipy/stats/tests/test_stats.py::TestMoments::test_constant_moments[0-1-float64] - AssertionError:
FAILED scipy/stats/tests/test_stats.py::TestMoments::test_constant_moments[0-1-complex128] - AssertionError:

Merged branch main didn't help.

@mdhaber
Copy link
Contributor

mdhaber commented Apr 8, 2023

They are related to this PR; in fact, they are related to the changes you removed from my PR. (You were right that mean was never None, but that was the problem.) I'll go ahead and push a fix here.

@doronbehar
Copy link
Contributor Author

Weird. Indeed that test failure doesn't appear on branch main, apologies for assuming it's not my fault. However none of the options that came up regarding that low moments scenarios fix the test.. It's still peculiar that the tests fail due to a mismatch of very small numbers - in the case of the conditional acting only when moment == 0 - as shown in that pytest output of my previous comment.

@mdhaber
Copy link
Contributor

mdhaber commented Apr 8, 2023

It's still peculiar that the tests fail due to a mismatch of very small numbers - in the case of the conditional acting only when moment == 0

It fails when the moment == 1, and that is not surprising to me. The failing test is checking that the first central moment is exactly 0. But because this PR (as-is) removed the special handling in _moment when moment == 1, it calculates the first central moment to be a small but nonzero number. doronbehar#2 fixes this and strengthens the tests.

Copy link
Contributor

@mdhaber mdhaber left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @doronbehar, this LGTM. The new CI failure is actually unrelated; I've seen it elswehere. Mailing list post is here.

@tirthasheshpatel, since I worked on this a fair amount, I'm not sure mine should be the only review. Would you be willing to take a look and merge if you approve?

@mdhaber mdhaber requested a review from tupui April 24, 2023 05:21
@tupui tupui added this to the 1.11.0 milestone Apr 24, 2023
Copy link
Member

@tupui tupui left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you both. Changes and tests LGTM. The review and discussion was quite intensive, I don't have more to add. CI seems unrelated. Let's get this in now.

@tupui tupui merged commit 419b589 into scipy:main Apr 24, 2023
27 of 29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement A new feature or improvement scipy.stats
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ENH: Compute non centraled moments with stats.moment?
3 participants