Skip to content

BUG: Preserve NumPy complex scalars in object data - #982

Merged
bashtage merged 3 commits into
mainfrom
wip-pr976-complex-object
Oct 29, 2026
Merged

bashtage merged 3 commits into
mainfrom
wip-pr976-complex-object

Conversation

@bashtage

Copy link
Copy Markdown
Owner

closes #976

Takes over #976 by @galafis, whose commit is kept unchanged, and implements the review comments on it.

The bug (from #976). as_float64 converted object data with astype(float64) first. NumPy complex scalars (np.complex128(1 + 2j), np.complex64) in object data were cast to their real parts with a ComplexWarning. The imaginary parts were therefore lost with allow_complex=True, and complex data were accepted with allow_complex=False. Python complex elements were already handled, since casting them to float64 raises.

The change (review). All object data now go through one helper, _object_values. It is used for object ndarrays (as_float64, check_array) and for the object columns of a Series or DataFrame (ensure1d, ensure2d).

  • The values are cast to complex128 in one NumPy call. An element that is not a number raises the existing TypeError.
  • If every imaginary part is 0, the real parts are returned as float64. Otherwise the data are complex128 if allow_complex, and a TypeError if not.
  • There is no Python loop over the elements (the isinstance scans in BUG: Preserve NumPy complex scalars in object data #976 are gone), and no ComplexWarning.
  • None is still rejected in an ndarray and still becomes NaN in pandas data, since pandas treats it as missing. It is found before the cast, since NumPy casts None to nan+nanj, which would look like complex data.
  • In a DataFrame, if an object column turns out to be complex, every column is converted to complex128, as for a complex dtype column. Columns are matched by position, so duplicated labels and a duplicated index are kept.

Behaviour changes against main.

  • NumPy complex scalars with a nonzero imaginary part become complex128 with allow_complex=True; on main they became float64 with a ComplexWarning. Without allow_complex they now raise a TypeError; on main they were accepted.
  • Complex elements whose imaginary parts are all 0 (2 + 0j, np.complex128(1)) are real data: float64 for both values of allow_complex. On main, such Python complex values were rejected without allow_complex and became complex128 with it, and the NumPy ones became float64 with a ComplexWarning.
  • The message for rejected complex object data is now "my_data must contain real numbers that can be converted to float64; got complex numbers in an object array with shape (2,) (ndarray)".

Everything else is unchanged on 21 kinds of object data (ndarray, Series and DataFrame, both values of allow_complex): numeric strings, Decimal/Fraction, large ints, longdouble and float32 scalars, empty arrays, NaN and None.

Tests. arch/tests/utility/test_dtype_policy.py adds:

  • NumPy complex64 and complex128 scalars, 2-d object data and a complex NaN;
  • zero imaginary parts, for the four helpers and three containers, with warnings as errors;
  • None in pandas object data;
  • a DataFrame with real, complex-object and real-object columns and duplicated labels.

The new tests fail on main, and the zero-imaginary ones also fail on the head of #976.

  • test_dtype_policy.py: 519 passed with -W error, and also on Python 3.10 with NumPy 1.23.5 and pandas 1.5.3.
  • Fast suite: 27,041 passed, 19 skipped (numba not installed, and inapplicable parametrizations).
  • After merging main: arch/tests/utility and test_dtype_inputs.py, 1,487 passed.
  • flake8, ruff, black, isort and numpydoc lint are clean, and mypy has no new errors.

🤖 Generated with Claude Code

galafis and others added 3 commits October 24, 2026 14:02
Object arrays and the object columns of a Series or DataFrame are converted
by _object_values. The values are cast to complex128 in one step, which
accepts every real and complex number and rejects elements that are not
numbers, and the real parts are returned as float64 if every imaginary part
is 0. Complex data are complex128 if allowed and rejected otherwise. This
replaces the element-by-element searches for complex scalars, and NumPy no
longer issues a ComplexWarning.

None is still rejected in an ndarray and is NaN in pandas data. It is found
before the cast, since NumPy casts None to a complex NaN.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@bashtage
bashtage marked this pull request as ready for review October 29, 2026 14:54
@codecov

codecov Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.72727% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.96%. Comparing base (00eb3d1) to head (25d11c2).

Files with missing lines Patch % Lines
arch/utility/array.py 93.33% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #982   +/-   ##
=======================================
  Coverage   98.95%   98.96%           
=======================================
  Files         138      138           
  Lines       30148    30221   +73     
  Branches     2339     2345    +6     
=======================================
+ Hits        29834    29907   +73     
- Misses        256      257    +1     
+ Partials       58       57    -1     
Flag Coverage Δ
adder 98.90% <97.72%> (+<0.01%) ⬆️
subtractor 98.90% <97.72%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@bashtage
bashtage merged commit afbe2c2 into main Oct 29, 2026
25 of 26 checks passed
@bashtage
bashtage deleted the wip-pr976-complex-object branch October 29, 2026 16:04
jngod2011 pushed a commit to jngod2011/arch that referenced this pull request Oct 31, 2026
- Highlights and new features: NGARCH (bashtage#966), the VaR backtests (bashtage#965), the
  local block bootstrap (bashtage#967) and bandwidth_method (bashtage#964, and
  phillips_ouliaris here).
- Changes that alter results: the automatic bandwidth entry of bashtage#983 now
  also describes the bandwidth of the kernels with bounded support, the
  fractional bandwidths and the Tukey-Parzen constant of bashtage#964, its S&P 500
  bandwidths are those of the bandwidth attribute (30.96 and 34.50, where it
  said the 31.96 and 35.50 of opt_bandwidth), and it links phillips_ouliaris
  in arch.unitroot.cointegration. The ADF-Z tables of bashtage#972, bashtage#973 and bashtage#980.
- Backward-incompatible changes: strings in object data.
- Bug fixes: bashtage#954, bashtage#970, bashtage#974, bashtage#975, bashtage#977, bashtage#978, bashtage#982, and the constant
  data of the Newey-West bandwidth and mackinnonp at -inf and inf.
- Maintenance: the CI changes and the version file.
- Contributors: galafis, jackiectl2, mkzung and wavebyrd.
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.

2 participants