Skip to content

Commit 8ff9238

Browse files
committed
Allow float conversion in clamp and replace fill values by NaN functions
In order to simplify the API an 'astype' method was also introduced. Closes #204. Signed-off-by: Alexis Jeandet <alexis.jeandet@member.fsf.org>
1 parent 17c439b commit 8ff9238

File tree

3 files changed

+79
-6
lines changed

3 files changed

+79
-6
lines changed

speasy/core/data_containers.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ def ndim(self):
121121
def dtype(self):
122122
return self.__values.dtype
123123

124+
def astype(self, dtype) -> "DataContainer":
125+
return DataContainer(values=self.__values.astype(dtype), meta=self.__meta, name=self.__name,
126+
is_time_dependent=self.__is_time_dependent)
127+
124128
@property
125129
def unit(self) -> str:
126130
return self.__meta.get('UNITS')

speasy/products/variable.py

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,25 @@ def shape(self):
400400
def dtype(self):
401401
return self.__values_container.dtype
402402

403+
def astype(self, dtype) -> "SpeasyVariable":
404+
"""Returns a SpeasyVariable with values converted to given dtype
405+
406+
Parameters
407+
----------
408+
dtype : str or np.dtype or type
409+
desired dtype
410+
411+
Returns
412+
-------
413+
SpeasyVariable
414+
SpeasyVariable with values converted to given dtype
415+
"""
416+
return SpeasyVariable(
417+
axes=deepcopy(self.__axes),
418+
values=self.__values_container.astype(dtype),
419+
columns=deepcopy(self.__columns),
420+
)
421+
403422
@property
404423
def name(self) -> str:
405424
"""SpeasyVariable name
@@ -700,14 +719,16 @@ def plot(self, *args, **kwargs):
700719
values=self.__values_container, columns_names=self.columns, axes=self.axes
701720
)
702721

703-
def replace_fillval_by_nan(self, inplace=False) -> "SpeasyVariable":
704-
"""Replaces fill values by NaN, non float values are automatically converted to float.
722+
def replace_fillval_by_nan(self, inplace=False, convert_to_float=False) -> "SpeasyVariable":
723+
"""Replaces fill values by NaN, non float values are automatically converted to float if convert_to_float is True.
705724
Fill value is taken from metadata field "FILLVAL"
706725
707726
Parameters
708727
----------
709728
inplace : bool, optional
710729
Modifies source variable when true else modifies and returns a copy, by default False
730+
convert_to_float : bool, optional
731+
Automatically converts variable to float if true and needed, by default False.
711732
712733
Returns
713734
-------
@@ -719,16 +740,25 @@ def replace_fillval_by_nan(self, inplace=False) -> "SpeasyVariable":
719740
clamp_with_nan: replaces values outside valid range by NaN
720741
sanitized: removes fill and invalid values
721742
"""
743+
# @TODO replace by a match case when Python 3.9 is EOL
722744
if inplace:
723745
res = self
746+
if convert_to_float and not np.issubdtype(self.dtype, np.floating):
747+
res.__values_container = res.__values_container.astype(float)
724748
else:
725-
res = deepcopy(self)
749+
if convert_to_float and not np.issubdtype(self.dtype, np.floating):
750+
res = self.astype(float)
751+
else:
752+
res = deepcopy(self)
726753
if (fill_value := self.fill_value) is not None:
754+
if convert_to_float and not np.issubdtype(res.dtype, np.floating):
755+
res.__values_container = res.__values_container.astype(float)
727756
res[res == fill_value] = np.nan
728757
return res
729758

730-
def clamp_with_nan(self, inplace=False, valid_min=None, valid_max=None) -> "SpeasyVariable":
731-
"""Replaces values outside valid range by NaN, valid range is taken from metadata fields "VALIDMIN" and "VALIDMAX"
759+
def clamp_with_nan(self, inplace=False, valid_min=None, valid_max=None, convert_to_float=False) -> "SpeasyVariable":
760+
"""Replaces values outside valid range by NaN, valid range is taken from metadata fields "VALIDMIN" and "VALIDMAX".
761+
Automatically converts variable to float if convert_to_float is True and needed.
732762
733763
Parameters
734764
----------
@@ -738,6 +768,8 @@ def clamp_with_nan(self, inplace=False, valid_min=None, valid_max=None) -> "Spea
738768
Optional minimum valid value, takes metadata field "VALIDMIN" if not provided, by default None
739769
valid_max : Float, optional
740770
Optional maximum valid value, takes metadata field "VALIDMAX" if not provided, by default None
771+
convert_to_float : bool, optional
772+
Automatically converts variable to float if true and needed, by default False.
741773
742774
Returns
743775
-------
@@ -749,10 +781,16 @@ def clamp_with_nan(self, inplace=False, valid_min=None, valid_max=None) -> "Spea
749781
replace_fillval_by_nan: replaces fill values by NaN
750782
sanitized: removes fill and invalid values
751783
"""
784+
# @TODO replace by a match case when Python 3.9 is EOL
752785
if inplace:
753786
res = self
787+
if convert_to_float and not np.issubdtype(self.dtype, np.floating):
788+
res.__values_container = res.__values_container.astype(float)
754789
else:
755-
res = deepcopy(self)
790+
if convert_to_float and not np.issubdtype(self.dtype, np.floating):
791+
res = self.astype(float)
792+
else:
793+
res = deepcopy(self)
756794
valid_min = valid_min or self.valid_range[0]
757795
valid_max = valid_max or self.valid_range[1]
758796
res[np.logical_or(res > valid_max, res < valid_min)] = np.nan

tests/test_speasy_variable.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,13 @@ def test_overrides_plot_arguments(self):
341341
except ImportError:
342342
self.skipTest("Can't import matplotlib")
343343

344+
def test_astype(self):
345+
var = make_simple_var(1., 10., 1., 10.)
346+
self.assertEqual(var.values.dtype, np.float64)
347+
var2 = (var * 100).astype(np.int32)
348+
self.assertEqual(var2.values.dtype, np.int32)
349+
self.assertTrue(np.all(var2.values == (var.values * 100).astype(np.int32)))
350+
344351
def test_replaces_fill_value(self):
345352
var = make_simple_var(1., 10., 1., 10., meta={"FILLVAL": 50.})
346353
self.assertEqual(var.fill_value, 50.)
@@ -350,6 +357,17 @@ def test_replaces_fill_value(self):
350357
var.replace_fillval_by_nan(inplace=True)
351358
self.assertTrue(np.isnan(var.values[4, 0]))
352359

360+
def test_replaces_fill_value_non_float(self):
361+
var = make_simple_var(1., 10., 1., 10., meta={"FILLVAL": 50.}).astype(np.int32)
362+
self.assertEqual(var.fill_value, 50.)
363+
with self.assertRaises(ValueError):
364+
var.replace_fillval_by_nan(inplace=False)
365+
cleaned_copy = var.replace_fillval_by_nan(inplace=False, convert_to_float=True)
366+
self.assertTrue(np.isnan(cleaned_copy.values[4, 0]))
367+
self.assertFalse(np.isnan(var.values[4, 0]))
368+
var.replace_fillval_by_nan(inplace=True, convert_to_float=True)
369+
self.assertTrue(np.isnan(var.values[4, 0]))
370+
353371
def test_clamps(self):
354372
var = make_simple_var(1., 10., 1., 10., meta={"VALIDMIN": 20., "VALIDMAX": 80.})
355373
clamped_copy = var.clamp_with_nan()
@@ -361,6 +379,19 @@ def test_clamps(self):
361379
self.assertTrue(np.all(np.isnan(var.values[8:10, 0])))
362380
self.assertFalse(np.any(np.isnan(var.values[1:8, 0])))
363381

382+
def test_clamps_non_float(self):
383+
var = make_simple_var(1., 10., 1., 10., meta={"VALIDMIN": 20., "VALIDMAX": 80.}).astype(np.int32)
384+
with self.assertRaises(ValueError):
385+
var.clamp_with_nan(inplace=False)
386+
clamped_copy = var.clamp_with_nan(convert_to_float=True)
387+
self.assertTrue(np.all(np.isnan(clamped_copy.values[0:1, 0])))
388+
self.assertTrue(np.all(np.isnan(clamped_copy.values[8:10, 0])))
389+
self.assertFalse(np.any(np.isnan(clamped_copy.values[1:8, 0])))
390+
var.clamp_with_nan(inplace=True, convert_to_float=True)
391+
self.assertTrue(np.all(np.isnan(var.values[0:1, 0])))
392+
self.assertTrue(np.all(np.isnan(var.values[8:10, 0])))
393+
self.assertFalse(np.any(np.isnan(var.values[1:8, 0])))
394+
364395
def test_cleans(self):
365396
var = make_simple_var(1., 10., 1., 10., meta={"FILLVAL": 50., "VALIDMIN": 20., "VALIDMAX": 80.})
366397
cleaned_copy = var.sanitized()

0 commit comments

Comments
 (0)