Skip to content

Commit 9dbb5cd

Browse files
authored
Pandas datetime and numpy numeric array fixes (#1163)
* Fix error when setting compound or compound array property to `None` * Pandas datetime and numpy numeric array fixes 1) Preserve numeric numpy types as is in validator out, even if that numeric type is not supported as JavaScript TypedArray 2) Update widget serializer to check numeric numpy arrays for whether they are compatible with TypedArrays. If not, serialize as list. 3) Call to_pydatetime() on pandas datetime series/index values when passed to copy_to_readonly_numpy_array. This returns numpy array of datetimes (which we already know how to serialize) Fixes datetime issue in #1160 Fixes FigureWidget issue in #1155
1 parent 5814699 commit 9dbb5cd

File tree

5 files changed

+242
-35
lines changed

5 files changed

+242
-35
lines changed

_plotly_utils/basevalidators.py

+51-30
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,18 @@ def to_scalar_or_list(v):
5252
return v
5353

5454

55-
def copy_to_readonly_numpy_array(v, dtype=None, force_numeric=False):
55+
def copy_to_readonly_numpy_array(v, kind=None, force_numeric=False):
5656
"""
5757
Convert an array-like value into a read-only numpy array
5858
5959
Parameters
6060
----------
6161
v : array like
6262
Array like value (list, tuple, numpy array, pandas series, etc.)
63-
dtype : str
64-
If specified, the numpy dtype that the array should be forced to
65-
have. If not specified then let numpy infer the datatype
63+
kind : str or tuple of str
64+
If specified, the numpy dtype kind (or kinds) that the array should
65+
have, or be converted to if possible.
66+
If not specified then let numpy infer the datatype
6667
force_numeric : bool
6768
If true, raise an exception if the resulting numpy array does not
6869
have a numeric dtype (i.e. dtype.kind not in ['u', 'i', 'f'])
@@ -74,30 +75,53 @@ def copy_to_readonly_numpy_array(v, dtype=None, force_numeric=False):
7475

7576
assert np is not None
7677

77-
# Copy to numpy array and handle dtype param
78-
# ------------------------------------------
79-
# If dtype was not specified then it will be passed to the numpy array
80-
# constructor as None and the data type will be inferred automatically
78+
# ### Process kind ###
79+
if not kind:
80+
kind = ()
81+
elif isinstance(kind, string_types):
82+
kind = (kind,)
83+
84+
first_kind = kind[0] if kind else None
8185

82-
# TODO: support datetime dtype here and in widget serialization
8386
# u: unsigned int, i: signed int, f: float
84-
numeric_kinds = ['u', 'i', 'f']
87+
numeric_kinds = {'u', 'i', 'f'}
88+
kind_default_dtypes = {
89+
'u': 'uint32', 'i': 'int32', 'f': 'float64', 'O': 'object'}
8590

86-
# Unwrap data types that have a `values` property that might be a numpy
87-
# array. If this values property is a numeric numpy array then we
88-
# can take the fast path below
91+
# Handle pandas Series and Index objects
8992
if pd and isinstance(v, (pd.Series, pd.Index)):
90-
v = v.values
93+
if v.dtype.kind in numeric_kinds:
94+
# Get the numeric numpy array so we use fast path below
95+
v = v.values
96+
elif v.dtype.kind == 'M':
97+
# Convert datetime Series/Index to numpy array of datetimes
98+
if isinstance(v, pd.Series):
99+
v = v.dt.to_pydatetime()
100+
else:
101+
# DatetimeIndex
102+
v = v.to_pydatetime()
91103

92104
if not isinstance(v, np.ndarray):
105+
# v is not homogenous array
93106
v_list = [to_scalar_or_list(e) for e in v]
107+
108+
# Lookup dtype for requested kind, if any
109+
dtype = kind_default_dtypes.get(first_kind, None)
110+
111+
# construct new array from list
94112
new_v = np.array(v_list, order='C', dtype=dtype)
95113
elif v.dtype.kind in numeric_kinds:
96-
if dtype:
114+
# v is a homogenous numeric array
115+
if kind and v.dtype.kind not in kind:
116+
# Kind(s) were specified and this array doesn't match
117+
# Convert to the default dtype for the first kind
118+
dtype = kind_default_dtypes.get(first_kind, None)
97119
new_v = np.ascontiguousarray(v.astype(dtype))
98120
else:
121+
# Either no kind was requested or requested kind is satisfied
99122
new_v = np.ascontiguousarray(v.copy())
100123
else:
124+
# v is a non-numeric homogenous array
101125
new_v = v.copy()
102126

103127
# Handle force numeric param
@@ -106,7 +130,7 @@ def copy_to_readonly_numpy_array(v, dtype=None, force_numeric=False):
106130
raise ValueError('Input value is not numeric and'
107131
'force_numeric parameter set to True')
108132

109-
if dtype != 'unicode':
133+
if 'U' not in kind:
110134
# Force non-numeric arrays to have object type
111135
# --------------------------------------------
112136
# Here we make sure that non-numeric arrays have the object
@@ -116,12 +140,6 @@ def copy_to_readonly_numpy_array(v, dtype=None, force_numeric=False):
116140
if new_v.dtype.kind not in ['u', 'i', 'f', 'O']:
117141
new_v = np.array(v, dtype='object')
118142

119-
# Convert int64 arrays to int32
120-
# -----------------------------
121-
# JavaScript doesn't support int64 typed arrays
122-
if new_v.dtype == 'int64':
123-
new_v = new_v.astype('int32')
124-
125143
# Set new array to be read-only
126144
# -----------------------------
127145
new_v.flags['WRITEABLE'] = False
@@ -749,10 +767,13 @@ def validate_coerce(self, v):
749767
# Pass None through
750768
pass
751769
elif self.array_ok and is_homogeneous_array(v):
752-
if v.dtype.kind not in ['i', 'u']:
753-
self.raise_invalid_val(v)
754770

755-
v_array = copy_to_readonly_numpy_array(v, dtype='int32')
771+
v_array = copy_to_readonly_numpy_array(v,
772+
kind=('i', 'u'),
773+
force_numeric=True)
774+
775+
if v_array.dtype.kind not in ['i', 'u']:
776+
self.raise_invalid_val(v)
756777

757778
# Check min/max
758779
if self.has_min_max:
@@ -875,7 +896,7 @@ def validate_coerce(self, v):
875896

876897
if is_homogeneous_array(v):
877898
# If not strict, let numpy cast elements to strings
878-
v = copy_to_readonly_numpy_array(v, dtype='unicode')
899+
v = copy_to_readonly_numpy_array(v, kind='U')
879900

880901
# Check no_blank
881902
if self.no_blank:
@@ -1057,10 +1078,10 @@ def validate_coerce(self, v, should_raise=True):
10571078
# ### Check that elements have valid colors types ###
10581079
elif self.numbers_allowed() or invalid_els:
10591080
v = copy_to_readonly_numpy_array(
1060-
validated_v, dtype='object')
1081+
validated_v, kind='O')
10611082
else:
10621083
v = copy_to_readonly_numpy_array(
1063-
validated_v, dtype='unicode')
1084+
validated_v, kind='U')
10641085
elif self.array_ok and is_simple_array(v):
10651086
validated_v = [
10661087
self.validate_coerce(e, should_raise=False)
@@ -1509,7 +1530,7 @@ def validate_coerce(self, v):
15091530
self.raise_invalid_elements(invalid_els)
15101531

15111532
if is_homogeneous_array(v):
1512-
v = copy_to_readonly_numpy_array(validated_v, dtype='unicode')
1533+
v = copy_to_readonly_numpy_array(validated_v, kind='U')
15131534
else:
15141535
v = to_scalar_or_list(v)
15151536
else:
@@ -1559,7 +1580,7 @@ def validate_coerce(self, v):
15591580
# Pass None through
15601581
pass
15611582
elif self.array_ok and is_homogeneous_array(v):
1562-
v = copy_to_readonly_numpy_array(v, dtype='object')
1583+
v = copy_to_readonly_numpy_array(v, kind='O')
15631584
elif self.array_ok and is_simple_array(v):
15641585
v = to_scalar_or_list(v)
15651586
return v

_plotly_utils/tests/validators/test_integer_validator.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def test_acceptance_aok_list(val, validator_aok):
128128
def test_coercion_aok_list(val, expected, validator_aok):
129129
v = validator_aok.validate_coerce(val)
130130
if isinstance(val, (np.ndarray, pd.Series, pd.Index)):
131-
assert v.dtype == np.int32
131+
assert v.dtype == val.dtype
132132
assert np.array_equal(validator_aok.present(v),
133133
np.array(expected, dtype=np.int32))
134134
else:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import pytest
2+
import numpy as np
3+
import pandas as pd
4+
from datetime import datetime
5+
from _plotly_utils.basevalidators import (NumberValidator,
6+
IntegerValidator,
7+
DataArrayValidator,
8+
ColorValidator)
9+
10+
11+
@pytest.fixture
12+
def data_array_validator(request):
13+
return DataArrayValidator('prop', 'parent')
14+
15+
16+
@pytest.fixture
17+
def integer_validator(request):
18+
return IntegerValidator('prop', 'parent', array_ok=True)
19+
20+
21+
@pytest.fixture
22+
def number_validator(request):
23+
return NumberValidator('prop', 'parent', array_ok=True)
24+
25+
26+
@pytest.fixture
27+
def color_validator(request):
28+
return ColorValidator('prop', 'parent', array_ok=True, colorscale_path='')
29+
30+
31+
@pytest.fixture(
32+
params=['int8', 'int16', 'int32', 'int64',
33+
'uint8', 'uint16', 'uint32', 'uint64',
34+
'float16', 'float32', 'float64'])
35+
def numeric_dtype(request):
36+
return request.param
37+
38+
39+
@pytest.fixture(
40+
params=[pd.Series, pd.Index])
41+
def pandas_type(request):
42+
return request.param
43+
44+
45+
@pytest.fixture
46+
def numeric_pandas(request, pandas_type, numeric_dtype):
47+
return pandas_type(np.arange(10), dtype=numeric_dtype)
48+
49+
50+
@pytest.fixture
51+
def color_object_pandas(request, pandas_type):
52+
return pandas_type(['blue', 'green', 'red']*3, dtype='object')
53+
54+
55+
@pytest.fixture
56+
def color_categorical_pandas(request, pandas_type):
57+
return pandas_type(pd.Categorical(['blue', 'green', 'red']*3))
58+
59+
60+
@pytest.fixture
61+
def dates_array(request):
62+
return np.array([
63+
datetime(year=2013, month=10, day=10),
64+
datetime(year=2013, month=11, day=10),
65+
datetime(year=2013, month=12, day=10),
66+
datetime(year=2014, month=1, day=10),
67+
datetime(year=2014, month=2, day=10)
68+
])
69+
70+
71+
@pytest.fixture
72+
def datetime_pandas(request, pandas_type, dates_array):
73+
return pandas_type(dates_array)
74+
75+
76+
def test_numeric_validator_numeric_pandas(number_validator, numeric_pandas):
77+
res = number_validator.validate_coerce(numeric_pandas)
78+
79+
# Check type
80+
assert isinstance(res, np.ndarray)
81+
82+
# Check dtype
83+
assert res.dtype == numeric_pandas.dtype
84+
85+
# Check values
86+
np.testing.assert_array_equal(res, numeric_pandas)
87+
88+
89+
def test_integer_validator_numeric_pandas(integer_validator, numeric_pandas):
90+
res = integer_validator.validate_coerce(numeric_pandas)
91+
92+
# Check type
93+
assert isinstance(res, np.ndarray)
94+
95+
# Check dtype
96+
if numeric_pandas.dtype.kind in ('u', 'i'):
97+
# Integer and unsigned integer dtype unchanged
98+
assert res.dtype == numeric_pandas.dtype
99+
else:
100+
# Float datatypes converted to default integer type of int32
101+
assert res.dtype == 'int32'
102+
103+
# Check values
104+
np.testing.assert_array_equal(res, numeric_pandas)
105+
106+
107+
def test_data_array_validator(data_array_validator,
108+
numeric_pandas):
109+
res = data_array_validator.validate_coerce(numeric_pandas)
110+
111+
# Check type
112+
assert isinstance(res, np.ndarray)
113+
114+
# Check dtype
115+
assert res.dtype == numeric_pandas.dtype
116+
117+
# Check values
118+
np.testing.assert_array_equal(res, numeric_pandas)
119+
120+
121+
def test_color_validator_numeric(color_validator,
122+
numeric_pandas):
123+
res = color_validator.validate_coerce(numeric_pandas)
124+
125+
# Check type
126+
assert isinstance(res, np.ndarray)
127+
128+
# Check dtype
129+
assert res.dtype == numeric_pandas.dtype
130+
131+
# Check values
132+
np.testing.assert_array_equal(res, numeric_pandas)
133+
134+
135+
def test_color_validator_object(color_validator,
136+
color_object_pandas):
137+
138+
res = color_validator.validate_coerce(color_object_pandas)
139+
140+
# Check type
141+
assert isinstance(res, np.ndarray)
142+
143+
# Check dtype
144+
assert res.dtype == 'object'
145+
146+
# Check values
147+
np.testing.assert_array_equal(res, color_object_pandas)
148+
149+
150+
def test_color_validator_categorical(color_validator,
151+
color_categorical_pandas):
152+
153+
res = color_validator.validate_coerce(color_categorical_pandas)
154+
155+
# Check type
156+
assert color_categorical_pandas.dtype == 'category'
157+
assert isinstance(res, np.ndarray)
158+
159+
# Check dtype
160+
assert res.dtype == 'object'
161+
162+
# Check values
163+
np.testing.assert_array_equal(res, np.array(color_categorical_pandas))
164+
165+
166+
def test_data_array_validator_dates(data_array_validator,
167+
datetime_pandas,
168+
dates_array):
169+
170+
res = data_array_validator.validate_coerce(datetime_pandas)
171+
172+
# Check type
173+
assert isinstance(res, np.ndarray)
174+
175+
# Check dtype
176+
assert res.dtype == 'object'
177+
178+
# Check values
179+
np.testing.assert_array_equal(res, dates_array)

plotly/basedatatypes.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -2981,7 +2981,7 @@ def _set_compound_prop(self, prop, val):
29812981
# ------------------
29822982
if not self._in_batch_mode:
29832983
if not new_dict_val:
2984-
if prop in self._props:
2984+
if self._props and prop in self._props:
29852985
self._props.pop(prop)
29862986
else:
29872987
self._init_props()
@@ -3055,7 +3055,7 @@ def _set_array_prop(self, prop, val):
30553055
# ------------------
30563056
if not self._in_batch_mode:
30573057
if not new_dict_vals:
3058-
if prop in self._props:
3058+
if self._props and prop in self._props:
30593059
self._props.pop(prop)
30603060
else:
30613061
self._init_props()

0 commit comments

Comments
 (0)