Skip to content

ENH: One data type rule, with complex data in the covariance estimators - #957

Merged
bashtage merged 9 commits into
mainfrom
a90-dtype1-dtypes
Oct 17, 2026
Merged

bashtage merged 9 commits into
mainfrom
a90-dtype1-dtypes

Conversation

@bashtage

Copy link
Copy Markdown
Owner

Branch a90-dtype1-dtypes, commits on a90-fix-main-tests. Merge #953 first.
It merges cleanly with #955 and #956. This settles release decision 65 and the
maintainer's request: "someone might want to use complex data in a covariance
estimator ... Arch models must be float64. Unit root tests are also float64 or
float32 (I think float32 is OK, but you check). Same for cointegration ... Float16
is never OK. Find other places though where complex might make sense." It also
fixes the 7 tests that fail on main since the last commits of #942.

One rule, in one place (_rejected_dtype in arch/utility/_validation.py,
used by as_float64, ensure1d, ensure2d, check_array and to_array_1d):

  • Integer, unsigned integer, Boolean, float32, float64 and longdouble data are
    converted to float64, and every calculation is done in float64.
  • float16 is never accepted: it raises a TypeError that names the argument
    and the accepted types, also for a float16 DataFrame column.
  • Complex raises a TypeError, except in the functions that estimate a
    Hermitian covariance (below), where it becomes complex128.

float32 is accepted as input but promoted. Computing in float32 is not
accurate enough. With every array in float32:

  • Kernel long-run covariance: relative errors of 6e-8 to 1.3e-6 on returns,
    but 8e-3 for long series with a large level. The automatic bandwidth was off by
    4.8%.
  • Unit roots: the Phillips-Perron statistic was off by up to 0.036 at a level
    of 1000, and the ADF-AIC and Zivot-Andrews lag searches raised "singular
    matrix" in every replication for large levels or trends.
  • Cointegration: errors up to 6e-5 (FMOLS).

float32 is also not faster. So float32 data give exactly the float64 result for
the same values, which is tested. The scripts and outputs are in the release
folder (work/dtype1/prec_*).

Complex data, where they make sense:

  • The kernel covariance estimators and cov_nw estimate the Hermitian
    long-run covariance. They use the conjugate transpose (the lag-j autocovariance
    is the sum of x[t] conj(x[t-j])), the automatic bandwidth uses the real part
    of the autocovariances, and the estimates are Hermitian complex128 with a real
    diagonal.
  • Verification: for z = a + ib the result equals
    Omega_aa + Omega_bb + i(Omega_ba - Omega_ab), computed from the stacked real
    data [a, b] with the same kernel and bandwidth (10 kernels x center x
    df_adjust, max error 4e-13). Real float64 results are bit-identical to before.
  • The bootstraps: apply, cov and var accept statistics with complex
    values. cov is Hermitian and var is real; the imaginary part used to be
    dropped with a ComplexWarning. conf_int raises a TypeError for a complex
    statistic, since it has no interval.
  • add_trend accepts complex data and adds real trend columns.
  • PreWhitenedRecolored rejects complex data. A complex VAR is either
    strictly linear or the stacked real VAR, and the selected order depends on that
    choice. Its notes say to stack the real and imaginary parts, or to use a kernel
    estimator.
  • Rejected with a TypeError: unit root and cointegration tests, ARCH
    models, volatility processes, distributions, MCS/SPA/StepM,
    optimal_block_length, auto_bandwidth and to_array_1d. to_array_1d used
    to drop the imaginary part silently.
  • ensure1d/ensure2d gain an allow_complex keyword (default False).

Changes to #942's tests (antonsoo; credit kept in the release notes):

  • test_preserve_floating_kernel_dtype becomes
    test_floating_kernel_dtype_is_float64. float32 input gives float64 for all
    four estimates; on main, BUG: Promote discrete covariance inputs before products #942 left short_run float32 and long_run float64.
  • test_preserve_inexact_newey_west becomes test_inexact_newey_west. Its
    expected value now uses the conjugate, since data.T @ data is the
    pseudo-covariance, not the covariance.

Also fixed: a crash with longdouble data on Windows, and a leftover
z = asarray(y) in cov_nw from a merge.

Tests:

  • New tables: test_validation_dtypes.py (571) and test_dtype_inputs.py (200),
    plus the Hermitian identity, float32 equality and float16 rejection tests.
  • 867 of the new or changed tests fail on the base.
  • Fast suite: 24,992 passed, 0 failed, including the 7 that fail on main.
  • Also on Python 3.10 with numba and NumPy 2.2: 10,994 passed.

Checks: the golden battery is identical, numpydoc lint finds nothing, and
style is clean. The contributing guide has a new "Data types" section, and the
release notes have the rules, the complex support and the float16 change.

🤖 Generated with Claude Code

…of the distributions

The standardized residuals of a degenerate model, such as one fit to a constant series,
can be 0/0, which is NaN on macOS on x86-64. The check of std_resid in starting_values
rejected them, so fit raised a ValueError there instead of the ConvergenceWarning that
test_no_variance expects. NaN and inf are allowed again, and the distributions return
their default starting values for them, as before.
…uals

The kurtosis of residuals that contain inf is NaN, which gives the default starting
value, and SciPy warned about the invalid value.
A merge of main brought back the copy of the examples before the references, so the
section appeared twice and the docstring test failed.
as_float64, ensure1d, ensure2d, check_array and to_array_1d now share one rule for
the data type of numeric data, defined once in as_float64 and _rejected_dtype:

* bool, integer, unsigned integer, float32, float64 and longdouble data are converted
  to float64, and the calculation is done in float64. float32 is promoted and not
  computed in float32 (see below).
* float16 data are never accepted. They raise a TypeError that names the argument, the
  data type received and the data types accepted, for example
  "y must be a 1-d array of finite real numbers (integer, Boolean, float32 or float64
  data); got float16 data with shape (120,) (ndarray)". A DataFrame with one float16
  column is rejected, which np.asarray(frame) would hide by merging the columns.
* complex data raise a TypeError, unless the new keyword allow_complex is True
  (check_array, ensure1d, ensure2d, as_float64). Then complex64 and complex128 data,
  and object data with a complex element, are converted to complex128 and real data
  are still converted to float64. The finite check covers both parts.
* to_array_1d no longer discards the imaginary part of complex data (it used astype)
  and raises the same TypeError.

np.longdouble is equal to np.float64 where it has 64 bits (Windows), so that
`dtype == np.float64` passed such an array through as it was, and LAPACK rejects it
("array type float64 is unsupported in linalg"). The fast path now also checks the
type character, and pandas objects with a longdouble column are rebuilt, since astype
does not convert equal types.

Why float32 is promoted. float32 data computed in float32, emulated with every array
in float32 (OLS, lagmat, add_trend and the data), against the same values in float64:
the kernel long-run covariance has a relative error of 1e-7 to 1e-6 for well scaled
data, 4e-4 at 25,000 observations and 8e-3 at 250,000 for data with a mean of 1,000 and
a standard deviation of 1 (the centering loses digits), and the bandwidth changes by
5%; the ADF, DF-GLS, KPSS and Zivot-Andrews statistics differ by 1e-7 to 1e-6, the
Phillips-Perron statistic by up to 0.04 and the variance ratio by up to 1e-4 for a
series with a large level; the ADF lag search raises a singular matrix error in every
replication for a level of 1,000, and with a trend for a level of 5 with steps of 0.01
and 2,500 observations and for a random walk with 5,000 observations; and the
cointegrating regressions differ by up to 6e-5. Selected lags were the same in the
runs that completed. NumPy promotes in numpy.cov for the same reason. The work is not
faster either: the float32 and float64 times of the kernel estimators were the same.

The tests are tables of data types for each helper, container (ndarray, Series,
DataFrame) and allow_complex.
The kernel covariance estimators, cov_nw and add_trend accept complex data. The
estimators are Hermitian: every product uses the conjugate transpose, so that the lag-j
autocovariance is the sum of x[t] times the conjugate of x[t - j], the long-run
covariance is Gamma_0 + sum of w_j (Gamma_j + Gamma_j^H) and the estimates are
complex128 matrices that are exactly Hermitian, with a real diagonal.

* The automatic bandwidth combines the columns with the (real) weights and uses the real
  part of the autocovariance of the complex series, since the sums over j and -j add a
  value and its conjugate. The product v[t] * v[t - j] of the earlier code is the
  pseudo-covariance, which is not what the bandwidth formula needs.
* cov_nw returns a real float for a 1-d complex series, and a complex Hermitian matrix
  otherwise.
* add_trend adds the real trend columns to complex data and returns complex128. Constant
  columns of complex data are detected.
* PreWhitenedRecolored raises a TypeError for complex data. A VAR for complex data is
  not one model: a VAR with complex coefficients cannot represent real and imaginary
  parts that depend on each other in a way that is not a complex multiplication, the VAR
  of the stacked real and imaginary parts is a different model with more parameters,
  and the order that the information criteria select depends on which is used. The
  Notes say to use the stacked data or a kernel estimator.
* The weights of the bandwidth are real.

The estimates for float32 data are float64, and for complex64 data they are complex128.
The tests of antonsoo's last commit (#942) expected float32 to be preserved, which the
estimators did not do consistently (short_run was float32 and long_run float64) and the
float32 estimates are not accurate enough, and expected the complex estimates to be the
products without the conjugate. test_preserve_floating_kernel_dtype and
test_preserve_inexact_newey_west are replaced by tests of the same values with float64
and complex128 dtypes and the conjugate transpose.

The reference for the tests is independent of the complex arithmetic. For z = a + ib the
Hermitian long-run covariance of the estimators equals S_aa + S_bb + i (S_ba - S_ab),
where S is the long-run covariance of the real data [a, b] with the same kernel,
bandwidth, centering and degrees of freedom, and the automatic bandwidth equals the one
computed from the sums of the autocovariances of the real and imaginary parts. The
tests cover all ten kernels, every estimate (long run, short run, one sided, one sided
strict), centering and degrees of freedom, a rotation of the data (Hermitian estimates
are invariant to it, the pseudo-covariance is not), real data stored as complex, labels
of a DataFrame, and an exact rational reference for float32 data with a large mean. They
pass with and without numba.
The replications of a statistic were stored in a float64 array, so that a function that
returns complex values lost its imaginary part with a ComplexWarning (apply, cov, var)
or gave complex intervals that mean nothing (conf_int). The data of the bootstraps are
not converted and are resampled as given, so complex data worked until the statistic.

* apply, cov and var store the replications as complex128 for a complex statistic.
* cov is the Hermitian covariance (errors.T @ errors.conj()), and var is the real
  variance, the mean of the squared absolute errors.
* conf_int raises a TypeError for a complex statistic, before the replications are run,
  since a region and not an interval is needed. A reused result is checked too.

For z = a + ib the covariance of the bootstrap mean equals S_aa + S_bb + i (S_ba - S_ab)
where S is the covariance of the mean of the real data [a, b] computed with the same
seed, for the iid, stationary, circular and moving block bootstraps. Real statistics
are unchanged.
test_validation_dtypes.py has a row for each entry point, argument and data type, built
on validation_table.py: the unit root tests, the cointegration tests and estimators,
the mean models, the volatility processes, the distributions, the covariance
estimators, cov_nw, the multiple comparison procedures, optimal_block_length and
add_trend. Each argument is replaced by float16 (rejected, with the data type in the
message), complex64 and complex128 (rejected, except where the estimator is Hermitian)
and by bool, integer, unsigned integer, float32, float64 and longdouble data
(accepted). pandas Series and DataFrames with one bad column are rejected too.

test_dtype_inputs.py runs the cases of test_integer_inputs.py, 41 calls of the public
API, with float32 data (the results equal those of float64), float16 data and complex
data (TypeError), and runs each entry point with random float32 data, rounded to
float32 first, and requires the result to equal the float64 result for the same values
to 1e-12. A calculation in float32 differs by 1e-7 or more.
contributing.rst: rule 5 of the input validation section points to a new "Data types"
section, which has the table of data types by the default and by allow_complex, why
float32 is promoted, why float16 is rejected, where complex data are accepted and why
the others are not, and how to add complex support to a function and to test it against
the real and imaginary parts.

9.0.rst: the entry about the conversion of integer, unsigned integer and Boolean data
states the rules for all data types and credits antonsoo for the first float32 and
complex support of the covariance estimators (#942, #943), and a new entry in the new
features describes the Hermitian covariance estimators, add_trend and the bootstraps.
Comment on lines +41 to +46
from arch.utility.array import (
_numbers_rule,
_rejected_dtype,
as_float64,
cutoff_to_index,
)
@codecov

codecov Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.96433% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 99.01%. Comparing base (d921c35) to head (8fb9911).

Files with missing lines Patch % Lines
arch/utility/array.py 88.75% 7 Missing and 2 partials ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main     #957    +/-   ##
========================================
  Coverage   99.00%   99.01%            
========================================
  Files         129      134     +5     
  Lines       28773    29563   +790     
  Branches     2231     2290    +59     
========================================
+ Hits        28488    29271   +783     
- Misses        233      239     +6     
- Partials       52       53     +1     
Flag Coverage Δ
adder 98.95% <98.96%> (+<0.01%) ⬆️
subtractor 98.95% <98.96%> (+<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 a93ff73 into main Oct 17, 2026
24 of 25 checks passed
bashtage added a commit that referenced this pull request Oct 17, 2026
bashtage added a commit that referenced this pull request Oct 17, 2026
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