Skip to content

Keep the float dtype in mean(), as NumPy does - #755

Open
FabianIsensee wants to merge 1 commit into
Blosc:mainfrom
FabianIsensee:mean-keep-float-dtype
Open

FabianIsensee wants to merge 1 commit into
Blosc:mainfrom
FabianIsensee:mean-keep-float-dtype

Conversation

@FabianIsensee

Copy link
Copy Markdown

Hi Francesc,

a small fix I found while looking into #752's NumPy 1.26 CI job. It is one commit on top of main (d240a49), independent of my other PRs. improved-miniexpr has the same mean(), so it applies there too.

Why: mean() of float32 data can return float64. mean() divides the sum by the count, and that division promotes the dtype:

  • With NumPy 1.x, a float32 scalar divided by a Python int becomes float64 (value-based casting). So every full a.mean() of float32 returns float64, and so do axis reductions over more than 65535 elements.
  • With where=, the count is an int64 sum, so a.mean(where=mask) of float32 returns float64 with any NumPy. complex64 with where= comes back as complex128.

NumPy returns float32 (complex64) in all these cases.

What: the quotient is cast to NumPy's dtype: the input's float dtype, or a floating dtype=. Integer and bool inputs keep their float64 mean. std() and var() take their mean from here, so they also stay float32 with ddof=0. With ddof != 0, the rescaling by n / (n - ddof) on main still makes them float64 in these cases; #752 fixes that on improved-miniexpr.

Tests:

  • New: test_mean_dtype_matches_numpy (float32/float64/complex64/int32/bool, axis, keepdims, where). Without the fix, 16 of its 40 cases fail on NumPy 1.26 and 8 on NumPy 2.5.
  • pytest tests with NumPy 2.5: 9485 passed. The 2 failures are the b2view http tests that need aiohttp, as on main. With NumPy 1.26, the only difference from main is the new test.
  • ruff passes.

AI disclosure: the analysis, fix and test were done with Claude Code; I reviewed them.

Best,
Fabian

🤖 Generated with Claude Code

mean() divides the sum by the count, and that division can promote a
float32 (or complex64) result to float64:

- With NumPy 1.x, a float32 scalar divided by a Python int becomes
  float64 (value-based casting), so every full a.mean() of float32
  returned float64; so did axis reductions over more than 65535
  elements (an array over such an int).
- With where=, the count is an int64 sum, so a.mean(where=...) of
  float32 returned float64 with any NumPy.  complex64 with where= came
  back complex128 as well, because the sum of its where() already was.

NumPy returns the input's float dtype (or a floating dtype=).  The
quotient is now cast to that dtype; integer and bool inputs keep their
float64 mean.  std() and var() take their mean from here too.

Test: test_mean_dtype_matches_numpy (float32/float64/complex64/int32/
bool, axis, keepdims, where) has 16 of 40 cases failing without this
on NumPy 1.26, and 8 on NumPy 2.5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant