From 23cecac9ba6fa57e3d66b7140939b59a86d1d061 Mon Sep 17 00:00:00 2001 From: Israel Afangideh <105407225+IsraelAfangideh@users.noreply.github.com> Date: Thu, 25 Jun 2026 20:01:58 -0500 Subject: [PATCH 1/2] fix: handle read-only arrays in DunedinPACE quantile normalization quantile_normalize_using_target mutated its input array in place. When the array originates from DataFrame.values it can be read-only (pandas copy-on-write, default in pandas >= 3.0), raising "assignment destination is read-only" during DunedinPACE.predict. This also explains why the same data worked under older pandas/numpy but broke on newer ones. Operate on a writable float copy instead, which fixes the crash and avoids mutating the caller's array. Adds regression tests covering the read-only input, no-mutation, and target-distribution mapping behavior. Fixes #195 Co-Authored-By: Claude Opus 4.8 --- biolearn/dunedin_pace.py | 5 ++++ biolearn/test/test_dunedin_pace.py | 41 ++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 biolearn/test/test_dunedin_pace.py diff --git a/biolearn/dunedin_pace.py b/biolearn/dunedin_pace.py index e76e7fa..2e0587a 100644 --- a/biolearn/dunedin_pace.py +++ b/biolearn/dunedin_pace.py @@ -50,6 +50,11 @@ def quantile_normalize_using_target(data, target_values): """ Apply quantile normalization on data using target values. """ + # Copy to a writable float array. Inputs coming from ``DataFrame.values`` + # can be read-only (e.g. pandas copy-on-write in pandas >= 3.0), which + # would otherwise raise "assignment destination is read-only" on the + # in-place updates below. + data = np.array(data, dtype=float) sorted_target = np.sort(target_values) for _, column_data in enumerate(data.T): diff --git a/biolearn/test/test_dunedin_pace.py b/biolearn/test/test_dunedin_pace.py new file mode 100644 index 0000000..7d593e0 --- /dev/null +++ b/biolearn/test/test_dunedin_pace.py @@ -0,0 +1,41 @@ +import numpy as np +import pandas as pd + +from biolearn.dunedin_pace import quantile_normalize_using_target + + +def _target(): + return list(np.linspace(0.0, 1.0, 10)) + + +def test_quantile_normalize_accepts_read_only_array(): + # Arrays from DataFrame.values can be read-only (e.g. pandas + # copy-on-write in pandas >= 3.0). Normalization must not fail with + # "assignment destination is read-only". See issue #195. + data = pd.DataFrame(np.random.rand(10, 3)).values + data.flags.writeable = False + + result = quantile_normalize_using_target(data, _target()) + + assert result.shape == (10, 3) + + +def test_quantile_normalize_does_not_mutate_input(): + data = pd.DataFrame(np.random.rand(10, 3)).values + original = data.copy() + + quantile_normalize_using_target(data, _target()) + + np.testing.assert_array_equal(data, original) + + +def test_quantile_normalize_maps_values_onto_target_distribution(): + target = _target() + data = pd.DataFrame(np.random.rand(10, 3)).values + + result = quantile_normalize_using_target(data, target) + + sorted_target = np.sort(target) + midpoints = 0.5 * (sorted_target[:-1] + sorted_target[1:]) + allowed = np.round(np.concatenate([sorted_target, midpoints]), 6) + assert np.all(np.isin(np.round(result, 6), allowed)) From bf51b7607e1307fd934d6f4529bf1648c97eb392 Mon Sep 17 00:00:00 2001 From: Israel Afangideh <105407225+IsraelAfangideh@users.noreply.github.com> Date: Mon, 6 Jul 2026 08:06:43 -0500 Subject: [PATCH 2/2] docs: condense read-only-array comment per review Tighten the explanatory comment on the writable-copy line from four lines to two, keeping the essential "why" (read-only DataFrame.values under pandas>=3.0 copy-on-write breaks the in-place updates below). Comment-only; no behavior change. Co-Authored-By: Claude Opus 4.8 --- biolearn/dunedin_pace.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/biolearn/dunedin_pace.py b/biolearn/dunedin_pace.py index 2e0587a..ff60a2b 100644 --- a/biolearn/dunedin_pace.py +++ b/biolearn/dunedin_pace.py @@ -50,10 +50,8 @@ def quantile_normalize_using_target(data, target_values): """ Apply quantile normalization on data using target values. """ - # Copy to a writable float array. Inputs coming from ``DataFrame.values`` - # can be read-only (e.g. pandas copy-on-write in pandas >= 3.0), which - # would otherwise raise "assignment destination is read-only" on the - # in-place updates below. + # Writable float copy: DataFrame.values can be read-only under pandas>=3.0 + # copy-on-write, which would break the in-place updates below. data = np.array(data, dtype=float) sorted_target = np.sort(target_values)