Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 105f50c

Browse filesBrowse files
BUG: Respect print options in masked array formatting
Previously, the string representation of masked arrays did not honor global print options such as floatmode, precision, and suppress. This led to inconsistencies between the display of masked and unmasked arrays. This update addresses the issue by: - Utilizing np.array2string to format the underlying data, ensuring consistency with global print settings. - Replacing masked elements with the masked_print_option string ('--' by default) while preserving formatting for visible elements. - Maintaining support for legacy formatting options, including truncation with ellipsis (...) when legacy='1.13' is set. (Fixes #28685)
1 parent 8f11c7b commit 105f50c
Copy full SHA for 105f50c

File tree

Expand file treeCollapse file tree

2 files changed

+256
-4
lines changed
Filter options
Expand file treeCollapse file tree

2 files changed

+256
-4
lines changed

‎numpy/ma/core.py

Copy file name to clipboardExpand all lines: numpy/ma/core.py
+201-2Lines changed: 201 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
import numpy as np
3333
import numpy._core.umath as umath
3434
import numpy._core.numerictypes as ntypes
35-
from numpy._core import multiarray as mu
35+
from numpy._core import multiarray as mu, arrayprint
3636
from numpy import ndarray, amax, amin, iscomplexobj, bool_, _NoValue, angle
3737
from numpy import array as narray, expand_dims, iinfo, finfo
3838
from numpy._core.numeric import normalize_axis_tuple
@@ -4082,8 +4082,207 @@ def _insert_masked_print(self):
40824082
res = self.filled(self.fill_value)
40834083
return res
40844084

4085+
def _flatten(self, x):
4086+
# prevent infinite recursion
4087+
if isinstance(x, np.matrix):
4088+
x = np.asarray(x)
4089+
4090+
# handle structured scalar (np.void)
4091+
if isinstance(x, np.void):
4092+
return [item for name in x.dtype.names for item in self._flatten(x[name])]
4093+
4094+
# handle 0d structured arrays (e.g., array(..., dtype=[(...)]) )
4095+
if isinstance(x, np.ndarray) and x.ndim == 0 and x.dtype.names:
4096+
return self._flatten(x.item())
4097+
4098+
# handle masked scalars and arrays
4099+
if isinstance(x, (MaskedArray, MaskedConstant)):
4100+
if hasattr(x, 'ndim') and x.ndim == 0:
4101+
return [x.item()]
4102+
else:
4103+
return [item for sub in x for item in self._flatten(sub)]
4104+
4105+
# handle lists and tuples
4106+
if isinstance(x, (list, tuple)):
4107+
return [item for sub in x for item in self._flatten(sub)]
4108+
4109+
# handle non-scalar ndarrays
4110+
if isinstance(x, np.ndarray) and x.ndim > 0:
4111+
return [item for sub in x for item in self._flatten(sub)]
4112+
4113+
# base case: scalar value
4114+
return [x]
4115+
4116+
def _replacer(self, match, data_iter, exp_format):
4117+
"""
4118+
Replace matched tokens in a string with corresponding data from an iterator.
4119+
4120+
Parameters
4121+
----------
4122+
match : re.Match
4123+
The regular expression match object containing the token to be replaced.
4124+
data_iter : iterator
4125+
An iterator providing the data to replace the matched tokens.
4126+
Returns
4127+
-------
4128+
str
4129+
The string with the matched token replaced by the appropriate data.
4130+
"""
4131+
token = match.group(0)
4132+
# handle ellipsis in legacy printing
4133+
if token == '...':
4134+
value = next(data_iter) # consume from iterator to keep alignment
4135+
return '...'
4136+
# compute decimal precision from token
4137+
if '.' in token:
4138+
decimal_places = len(token.split(".")[1])
4139+
else:
4140+
decimal_places = 0
4141+
4142+
width = len(token)
4143+
float_format = f"{{:>{width}.{decimal_places}f}}"
4144+
value = next(data_iter)
4145+
# handle masked values
4146+
if (
4147+
value is masked or
4148+
isinstance(value, _MaskedPrintOption) or
4149+
is_masked(value)
4150+
):
4151+
return '--'.center(width)
4152+
4153+
opts = np.get_printoptions()
4154+
suppress = opts.get('suppress')
4155+
precision = opts.get('precision')
4156+
4157+
if not suppress and isinstance(value, float) and exp_format:
4158+
return arrayprint.format_float_scientific(
4159+
value,
4160+
precision=precision,
4161+
min_digits=builtins.min(decimal_places, precision),
4162+
sign=False,
4163+
)
4164+
4165+
# trim trailing .0 in floats if template does so
4166+
if token.endswith('.') and str(value).endswith('.0'):
4167+
return str(value)[:-1].rjust(width)
4168+
4169+
return float_format.format(float(value))
4170+
4171+
def _list_to_string(self, template, data):
4172+
"""
4173+
Convert a data structure into a formatted string based on a template.
4174+
4175+
Parameters
4176+
----------
4177+
template : str
4178+
The string template that dictates the formatting.
4179+
data : array-like
4180+
The data to be formatted into the string.
4181+
4182+
Returns
4183+
-------
4184+
str
4185+
The formatted string representation of the data.
4186+
"""
4187+
# handle scalar object arrays
4188+
if (isinstance(data, np.ndarray)
4189+
and data.dtype == object
4190+
and data.shape == ()):
4191+
return str(data.item())
4192+
4193+
# apply truncation before flattening, if legacy and 1D MaskedArray
4194+
legacy = np.get_printoptions().get('legacy') == '1.13'
4195+
if legacy and data.ndim == 1 and data.size > 10:
4196+
head = data[:3].tolist()
4197+
tail = data[-3:].tolist()
4198+
data = head + ['...'] + tail # insert ellipsis marker in the data
4199+
4200+
flat_data = self._flatten(data)
4201+
data_iter = iter(flat_data)
4202+
# match numbers, masked token, or ellipsis
4203+
pattern = (
4204+
r"-?\d+\.\d*(?:[eE][+-]?\d+)?|"
4205+
r"-?\d+(?:[eE][+-]?\d+)?|--|\.\.\."
4206+
)
4207+
4208+
opts = np.get_printoptions()
4209+
suppress = opts.get('suppress')
4210+
precision = opts.get('precision')
4211+
legacy = opts.get('legacy')
4212+
floatmode = opts.get('floatmode')
4213+
4214+
# flatten the original masked array to extract all scalar elements
4215+
flat_data = self._flatten(self.data)
4216+
4217+
# collect all float elements to analyze formatting needs
4218+
float_values = [x for x in flat_data if isinstance(x, float)]
4219+
4220+
# convert to a NumPy array (empty if no float values)
4221+
if float_values:
4222+
float_array = np.array(float_values, dtype=float)
4223+
else:
4224+
float_array = np.array([], dtype=float)
4225+
4226+
exp_format = False
4227+
if not suppress:
4228+
# normalize legacy printoption to an integer (e.g., "1.13" → 113)
4229+
if isinstance(legacy, str):
4230+
legacy_val = int(legacy.replace('.', ''))
4231+
else:
4232+
legacy_val = legacy
4233+
4234+
# determine whether exponential format should be used
4235+
exp_format = arrayprint.FloatingFormat(
4236+
float_array, precision, floatmode, suppress, legacy=legacy_val
4237+
).exp_format
4238+
4239+
# prepare replacement function for regex substitution
4240+
replacer_fn = lambda match: self._replacer(
4241+
match, data_iter, exp_format
4242+
)
4243+
4244+
# substitute matched tokens with formatted values
4245+
return re.sub(pattern, replacer_fn, template)
4246+
4247+
def _masked_array_to_string(self, masked_data):
4248+
"""
4249+
Process a masked array and return its string representation.
4250+
4251+
Parameters
4252+
----------
4253+
masked_data : numpy.ma.MaskedArray
4254+
The masked array to process.
4255+
4256+
Returns
4257+
-------
4258+
str
4259+
The string representation of the masked array.
4260+
"""
4261+
# get formatted string using array2string
4262+
formatted_str = np.array2string(self.data)
4263+
4264+
# if legacy mode is enabled, normalize whitespace
4265+
legacy = np.get_printoptions().get('legacy') == '1.13'
4266+
if legacy:
4267+
formatted_str = re.sub(r"\s{2,}", " ", formatted_str)
4268+
formatted_str = re.sub(r"(?<=\[)\s+", "", formatted_str)
4269+
4270+
return self._list_to_string(formatted_str, masked_data)
4271+
40854272
def __str__(self):
4086-
return str(self._insert_masked_print())
4273+
# handle 0-dimensional unmasked arrays by returning the scalar value
4274+
if self.shape == () and not self.mask:
4275+
return str(self.item())
4276+
4277+
masked_data = self._insert_masked_print()
4278+
# if the masked data has a custom __str__, use it
4279+
if (type(masked_data) is not np.ndarray
4280+
and hasattr(masked_data, '__str__')
4281+
and type(masked_data).__str__ is not np.ndarray.__str__):
4282+
return masked_data.__str__()
4283+
4284+
# process the array using our method
4285+
return self._masked_array_to_string(masked_data)
40874286

40884287
def __repr__(self):
40894288
"""

‎numpy/ma/tests/test_core.py

Copy file name to clipboardExpand all lines: numpy/ma/tests/test_core.py
+55-2Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -852,7 +852,7 @@ def test_fancy_printoptions(self):
852852
test = array([(1, (2, 3.0)), (4, (5, 6.0))],
853853
mask=[(1, (0, 1)), (0, (1, 0))],
854854
dtype=fancydtype)
855-
control = "[(--, (2, --)) (4, (--, 6.0))]"
855+
control = "[(--, (2, --)) (4, (--, 6.))]" # if trim='k' starts to work, adjust
856856
assert_equal(str(test), control)
857857

858858
# Test 0-d array with multi-dimensional dtype
@@ -863,9 +863,62 @@ def test_fancy_printoptions(self):
863863
[False, False, True]],
864864
False),
865865
dtype = "int, (2,3)float, float")
866-
control = "(0, [[--, 0.0, --], [0.0, 0.0, --]], 0.0)"
866+
control = "(0, [[--, 0., --], [0., 0., --]], 0.)" # if trim='k' starts to work, adjust
867867
assert_equal(str(t_2d0), control)
868868

869+
def test_floatmode_printoption(self):
870+
# Test printing a masked array w/ different float modes and precise print options. Issue #28685
871+
with np.printoptions(precision=2, suppress=True, floatmode='fixed'):
872+
# Create a masked array with a lower triangular mask
873+
mask = np.tri(5, dtype=bool)
874+
masked_array = np.ma.MaskedArray(np.ones((5, 5)) * 0.0001, mask=mask)
875+
control = (
876+
"[[ -- 0.00 0.00 0.00 0.00]\n"
877+
" [ -- -- 0.00 0.00 0.00]\n"
878+
" [ -- -- -- 0.00 0.00]\n"
879+
" [ -- -- -- -- 0.00]\n"
880+
" [ -- -- -- -- -- ]]"
881+
)
882+
assert_equal(str(masked_array), control)
883+
884+
with np.printoptions(precision=2, suppress=True, floatmode='unique'):
885+
control = (
886+
"[[ -- 0.0001 0.0001 0.0001 0.0001]\n"
887+
" [ -- -- 0.0001 0.0001 0.0001]\n"
888+
" [ -- -- -- 0.0001 0.0001]\n"
889+
" [ -- -- -- -- 0.0001]\n"
890+
" [ -- -- -- -- -- ]]"
891+
)
892+
assert_equal(str(masked_array), control)
893+
894+
with np.printoptions(precision=2, suppress=False, floatmode='maxprec'):
895+
mask = np.array([
896+
[True, False, False, False],
897+
[False, True, False, False],
898+
[False, False, True, False]
899+
])
900+
data = np.array([
901+
[0.12345678, 0.00001234, 1.0, 100.0],
902+
[0.5, 0.025, 0.333333, 0.999999],
903+
[0.0, 1.5, 2.25, 3.125]
904+
])
905+
masked_array = np.ma.MaskedArray(data, mask=mask)
906+
control = (
907+
"[[ -- 1.23e-05 1.00e+00 1.00e+02]\n"
908+
" [5.00e-01 -- 3.33e-01 1.00e+00]\n"
909+
" [0.00e+00 1.50e+00 -- 3.12e+00]]"
910+
)
911+
assert_equal(str(masked_array), control)
912+
913+
# if trail='k' starts to work, test needs to be adjusted
914+
with np.printoptions(precision=3, suppress=True, floatmode='maxprec_equal'):
915+
control = (
916+
"[[ -- 0.000 1.000 100.000]\n"
917+
" [ 0.500 -- 0.333 1.000]\n"
918+
" [ 0.000 1.500 -- 3.125]]"
919+
)
920+
assert_equal(str(masked_array), control)
921+
869922
def test_flatten_structured_array(self):
870923
# Test flatten_structured_array on arrays
871924
# On ndarray

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.