Skip to content

Commit 3698adc

Browse files
committed
fix rabit comment, add support for percentages
1 parent 3890a5f commit 3698adc

File tree

5 files changed

+139
-19
lines changed

5 files changed

+139
-19
lines changed

dascore/proc/taper.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,6 @@ def taper_range(
277277
>>> # Apply two non-overlapping tapers
278278
>>> taper_range = ((25,50,100,125), (150,175,200,225))
279279
>>> patch_tapered_5 = patch.taper_range(distance=taper_range)
280-
281280
"""
282281
dim, ax, values = get_dim_axis_value(patch, kwargs=kwargs)[0]
283282
coord = patch.get_coord(dim, require_sorted=True)

dascore/units.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import dascore as dc
1515
from dascore.compat import is_array
1616
from dascore.exceptions import UnitError
17-
from dascore.utils.misc import unbyte
17+
from dascore.utils.misc import iterate, unbyte
1818
from dascore.utils.time import dtype_time_like, is_datetime64, is_timedelta64, to_float
1919

2020
str_or_none = TypeVar("str_or_none", None, str)
@@ -357,6 +357,60 @@ def quant_sequence_to_quant_array(sequence: Sequence[Quantity]) -> Quantity:
357357
return array * next(iter(units))
358358

359359

360+
def maybe_convert_percent_to_fraction(obj):
361+
"""
362+
Iterate an object and convert any percentages to fractions.
363+
364+
Parameters
365+
----------
366+
obj
367+
The input object. Can be a single value or an iterable.
368+
369+
Returns
370+
-------
371+
list
372+
A list where any percentage quantities are converted to their
373+
fractional equivalents (e.g., 50% becomes 0.5).
374+
375+
Examples
376+
--------
377+
>>> from dascore.units import maybe_convert_percent_to_fraction, get_quantity
378+
>>>
379+
>>> # Convert a single percentage to fraction
380+
>>> result = maybe_convert_percent_to_fraction(get_quantity("50%"))
381+
>>> assert result == [0.5]
382+
>>>
383+
>>> # Convert a list with percentages
384+
>>> result = maybe_convert_percent_to_fraction(
385+
... [get_quantity("25%"), get_quantity("75%")]
386+
... )
387+
>>> assert result == [0.25, 0.75]
388+
>>>
389+
>>> # Non-percentage values are unchanged
390+
>>> result = maybe_convert_percent_to_fraction([get_quantity("10 m"), 5])
391+
>>> assert result[0] == get_quantity("10 m")
392+
>>> assert result[1] == 5
393+
>>>
394+
>>> # Mixed values
395+
>>> result = maybe_convert_percent_to_fraction(
396+
... [get_quantity("100%"), 0.5, get_quantity("2 Hz")]
397+
... )
398+
>>> assert result[0] == 1.0
399+
>>> assert result[1] == 0.5
400+
>>> assert result[2] == get_quantity("2 Hz")
401+
"""
402+
percent = get_unit("percent")
403+
out = []
404+
obj = [obj] if isinstance(obj, Quantity | Unit) and obj.ndim == 0 else obj
405+
for val in iterate(obj):
406+
if hasattr(val, "units"):
407+
mag, unit = val.magnitude, val.units
408+
if unit == percent:
409+
val = mag / 100
410+
out.append(val)
411+
return out
412+
413+
360414
def __getattr__(name):
361415
"""
362416
Allows arbitrary units (quantities) to be imported from this module.

dascore/viz/waterfall.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from dascore.constants import PatchType
1212
from dascore.exceptions import ParameterError
13-
from dascore.units import get_quantity_str
13+
from dascore.units import get_quantity_str, maybe_convert_percent_to_fraction
1414
from dascore.utils.patch import patch_function
1515
from dascore.utils.plotting import (
1616
_format_time_axis,
@@ -51,23 +51,25 @@ def _get_scale(scale, scale_type, data):
5151
Calculate the color bar scale limits based on scale and scale_type.
5252
"""
5353
_validate_scale_type(scale_type)
54-
54+
# This ensures we have a list of the previous scale parameters.
55+
scale = maybe_convert_percent_to_fraction(scale)
5556
match (scale, scale_type):
5657
# Case 1: Single value with relative scaling
5758
# Scale is symmetric around the mean, using fraction of dynamic range
58-
case (scale, "relative") if isinstance(scale, float | int):
59+
case (scale, "relative") if len(scale) == 1:
60+
scale = scale[0]
5961
mod = 0.5 * (np.nanmax(data) - np.nanmin(data))
6062
mean = np.nanmean(data)
6163
scale = np.asarray([mean - scale * mod, mean + scale * mod])
6264
# Case 2: No scale specified with relative scaling
6365
# Use Tukey's fence (C*IQR, C is normally 1.5) to exclude outliers.
6466
# This prevents a few extreme values from obscuring the majority of the
6567
# data at the cost of a slight performance penalty.
66-
case (None, "relative"):
67-
q2, q3 = np.nanpercentile(data, [25, 75])
68+
case ([], "relative"):
69+
q1, q3 = np.nanpercentile(data, [25, 75])
6870
dmin, dmax = np.nanmin(data), np.nanmax(data)
69-
diff = q3 - q2 # Interquartile range (IQR)
70-
q_lower = np.nanmax([q2 - diff * IQR_FENCE_MULTIPLIER, dmin])
71+
diff = q3 - q1 # Interquartile range (IQR)
72+
q_lower = np.nanmax([q1 - diff * IQR_FENCE_MULTIPLIER, dmin])
7173
q_upper = np.nanmin([q3 + diff * IQR_FENCE_MULTIPLIER, dmax])
7274
scale = np.asarray([q_lower, q_upper])
7375
return scale
@@ -88,8 +90,8 @@ def _get_scale(scale, scale_type, data):
8890
# Map [0, 1] to [data_min, data_max]
8991
scale = dmin + scale * data_range
9092
# Case 4: Absolute scaling
91-
case (scale, "absolute") if isinstance(scale, int | float):
92-
scale = np.array([-abs(scale), abs(scale)])
93+
case (scale, "absolute") if len(scale) == 1:
94+
scale = np.array([-abs(scale[0]), abs(scale[0])])
9395
# Case 5: Absolute scaling with sequence: no match needed.
9496

9597
# Scale values are used directly as colorbar limits
@@ -168,13 +170,16 @@ def waterfall(
168170
--------
169171
>>> # Plot with default scaling (uses 1.5*IQR fence to exclude outliers)
170172
>>> import dascore as dc
173+
>>> from dascore.units import percent
171174
>>> patch = dc.get_example_patch("example_event_1").normalize("time")
172175
>>> _ = patch.viz.waterfall()
173176
>>>
174-
>>> # Use relative scaling with a float to saturate at 10% of dynamic range
175-
>>> # This centers the colorbar around the mean and extends ±10% of the
176-
>>> # data's dynamic range in each direction
177+
>>> # Use relative scaling with a tuple to show a specific fraction
178+
>>> # of data range. Scale values of (0.1, 0.9) map to 10% and 90%
179+
>>> # of the [data_min, data_max] range data's dynamic range
177180
>>> _ = patch.viz.waterfall(scale=0.1, scale_type="relative")
181+
>>> # Likewise, percent units can be used for additional clarity
182+
>>> _ = patch.viz.waterfall(scale=10*percent, scale_type="absolute")
178183
>>>
179184
>>> # Use relative scaling with a tuple to show the middle 80% of data range
180185
>>> # Scale values of (0.1, 0.9) map to 10th and 90th percentile of data
@@ -218,15 +223,13 @@ def waterfall(
218223
"""
219224
# Validate inputs
220225
patch = _validate_patch_dims(patch)
221-
222226
# Setup axes and data
223227
ax = _get_ax(ax)
224228
cmap = _get_cmap(cmap)
225229
data = np.log10(np.absolute(patch.data)) if log else patch.data
226230
dims = patch.dims
227231
dims_r = tuple(reversed(dims))
228232
coords = {dim: patch.coords.get_array(dim) for dim in dims}
229-
230233
# Plot using imshow and set colorbar limits
231234
extents = _get_extents(dims_r, coords)
232235
scale = _get_scale(scale, scale_type, data)
@@ -244,14 +247,11 @@ def waterfall(
244247
interpolation_stage="data",
245248
)
246249
im.set_clim(scale)
247-
248250
# Format axis labels and handle time-like dimensions
249251
_format_axis_labels(ax, patch, dims_r)
250-
251252
# Add colorbar if requested
252253
if cmap is not None:
253254
_add_colorbar(ax, im, patch, log)
254-
255255
if show:
256256
plt.show()
257257
return ax

tests/test_units.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
get_quantity_str,
1818
get_unit,
1919
invert_quantity,
20+
maybe_convert_percent_to_fraction,
2021
quant_sequence_to_quant_array,
2122
)
2223

@@ -353,3 +354,66 @@ def test_numpy_array_input(self):
353354
sequence = np.array([1, 2, 3])
354355
out = quant_sequence_to_quant_array(sequence)
355356
assert isinstance(out, Quantity)
357+
358+
359+
class TestMaybeConvertPercentToFraction:
360+
"""Tests for converting percentages to fractions."""
361+
362+
def test_single_percentage(self):
363+
"""Test converting a single percentage value."""
364+
result = maybe_convert_percent_to_fraction(get_quantity("50%"))
365+
assert len(result) == 1
366+
assert result[0] == 0.5
367+
368+
def test_list_of_percentages(self):
369+
"""Test converting a list of percentages."""
370+
result = maybe_convert_percent_to_fraction(
371+
[get_quantity("25%"), get_quantity("75%"), get_quantity("100%")]
372+
)
373+
assert len(result) == 3
374+
assert result[0] == 0.25
375+
assert result[1] == 0.75
376+
assert result[2] == 1.0
377+
378+
def test_non_percentage_quantity_unchanged(self):
379+
"""Test that non-percentage quantities remain unchanged."""
380+
meter = get_quantity("10 m")
381+
hz = get_quantity("5 Hz")
382+
result = maybe_convert_percent_to_fraction([meter, hz])
383+
assert len(result) == 2
384+
assert result[0] == meter
385+
assert result[1] == hz
386+
387+
def test_plain_numeric_values(self):
388+
"""Test that plain numeric values without units are unchanged."""
389+
result = maybe_convert_percent_to_fraction([1, 2.5, 0])
390+
assert result == [1, 2.5, 0]
391+
392+
def test_mixed_values(self):
393+
"""Test a mix of percentages, quantities, and plain values."""
394+
percent_val = get_quantity("50%")
395+
meter_val = get_quantity("10 m")
396+
plain_val = 0.5
397+
result = maybe_convert_percent_to_fraction([percent_val, plain_val, meter_val])
398+
assert len(result) == 3
399+
assert result[0] == 0.5 # 50% converted to fraction
400+
assert result[1] == 0.5 # plain value unchanged
401+
assert result[2] == meter_val # quantity unchanged
402+
403+
def test_zero_percent(self):
404+
"""Test that 0% converts correctly."""
405+
result = maybe_convert_percent_to_fraction(get_quantity("0%"))
406+
assert len(result) == 1
407+
assert result[0] == 0.0
408+
409+
def test_large_percentage(self):
410+
"""Test that percentages over 100% convert correctly."""
411+
result = maybe_convert_percent_to_fraction(get_quantity("250%"))
412+
assert len(result) == 1
413+
assert result[0] == 2.5
414+
415+
def test_fractional_percentage(self):
416+
"""Test that fractional percentages convert correctly."""
417+
result = maybe_convert_percent_to_fraction(get_quantity("12.5%"))
418+
assert len(result) == 1
419+
assert np.isclose(result[0], 0.125)

tests/test_viz/test_waterfall.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,3 +233,6 @@ def test_constant_patch(self, random_patch):
233233
patch = random_patch.update(data=data)
234234
ax = patch.viz.waterfall()
235235
assert isinstance(ax, plt.Axes)
236+
237+
def test_precent_scale(self, random_patch):
238+
"""Ensure the percent unit works with scale."""

0 commit comments

Comments
 (0)