Skip to content

Commit ea0043d

Browse files
committed
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 717b191 commit ea0043d

File tree

4 files changed

+243
-25
lines changed

4 files changed

+243
-25
lines changed

Diff for: _plotly_utils/basevalidators.py

+54-22
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'])
@@ -81,23 +82,57 @@ def copy_to_readonly_numpy_array(v, dtype=None, force_numeric=False):
8182

8283
# TODO: support datetime dtype here and in widget serialization
8384
# u: unsigned int, i: signed int, f: float
84-
numeric_kinds = ['u', 'i', 'f']
85+
86+
# ### Process kind ###
87+
if not kind:
88+
kind = ()
89+
elif isinstance(kind, string_types):
90+
kind = (kind,)
91+
92+
first_kind = kind[0] if kind else None
93+
94+
numeric_kinds = {'u', 'i', 'f'}
95+
kind_default_dtypes = {
96+
'u': 'uint32', 'i': 'int32', 'f': 'float64', 'O': 'object'}
8597

8698
# Unwrap data types that have a `values` property that might be a numpy
8799
# array. If this values property is a numeric numpy array then we
88100
# can take the fast path below
101+
#
102+
# Use date_series.to_pydatetime()
103+
#
89104
if pd and isinstance(v, (pd.Series, pd.Index)):
90-
v = v.values
105+
if v.dtype.kind in numeric_kinds:
106+
# Get the numeric numpy array so we use fast path below
107+
v = v.values
108+
elif v.dtype.kind == 'M':
109+
# Convert datetime Series/Index to numpy array of datetime's
110+
if isinstance(v, pd.Series):
111+
v = v.dt.to_pydatetime()
112+
else:
113+
v = v.to_pydatetime()
91114

92115
if not isinstance(v, np.ndarray):
116+
# v is not homogenous array
93117
v_list = [to_scalar_or_list(e) for e in v]
118+
119+
# Lookup dtype for requested kind, if any
120+
dtype = kind_default_dtypes.get(first_kind, None)
121+
122+
# construct new array from list
94123
new_v = np.array(v_list, order='C', dtype=dtype)
95124
elif v.dtype.kind in numeric_kinds:
96-
if dtype:
125+
# v is a homogenous numeric array
126+
if kind and v.dtype.kind not in kind:
127+
# Kind(s) were specified and this array doens't match
128+
# Convert to the default dtype for the first kind
129+
dtype = kind_default_dtypes.get(first_kind, None)
97130
new_v = np.ascontiguousarray(v.astype(dtype))
98131
else:
132+
# Either no kind was requested or requested kind is satisfied
99133
new_v = np.ascontiguousarray(v.copy())
100134
else:
135+
# v is a non-numeric homogenous array
101136
new_v = v.copy()
102137

103138
# Handle force numeric param
@@ -106,7 +141,7 @@ def copy_to_readonly_numpy_array(v, dtype=None, force_numeric=False):
106141
raise ValueError('Input value is not numeric and'
107142
'force_numeric parameter set to True')
108143

109-
if dtype != 'unicode':
144+
if 'U' not in kind:
110145
# Force non-numeric arrays to have object type
111146
# --------------------------------------------
112147
# Here we make sure that non-numeric arrays have the object
@@ -116,12 +151,6 @@ def copy_to_readonly_numpy_array(v, dtype=None, force_numeric=False):
116151
if new_v.dtype.kind not in ['u', 'i', 'f', 'O']:
117152
new_v = np.array(v, dtype='object')
118153

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-
125154
# Set new array to be read-only
126155
# -----------------------------
127156
new_v.flags['WRITEABLE'] = False
@@ -749,10 +778,13 @@ def validate_coerce(self, v):
749778
# Pass None through
750779
pass
751780
elif self.array_ok and is_homogeneous_array(v):
752-
if v.dtype.kind not in ['i', 'u']:
753-
self.raise_invalid_val(v)
754781

755-
v_array = copy_to_readonly_numpy_array(v, dtype='int32')
782+
v_array = copy_to_readonly_numpy_array(v,
783+
kind=('i', 'u'),
784+
force_numeric=True)
785+
786+
if v_array.dtype.kind not in ['i', 'u']:
787+
self.raise_invalid_val(v)
756788

757789
# Check min/max
758790
if self.has_min_max:
@@ -875,7 +907,7 @@ def validate_coerce(self, v):
875907

876908
if is_homogeneous_array(v):
877909
# If not strict, let numpy cast elements to strings
878-
v = copy_to_readonly_numpy_array(v, dtype='unicode')
910+
v = copy_to_readonly_numpy_array(v, kind='U')
879911

880912
# Check no_blank
881913
if self.no_blank:
@@ -1057,10 +1089,10 @@ def validate_coerce(self, v, should_raise=True):
10571089
# ### Check that elements have valid colors types ###
10581090
elif self.numbers_allowed() or invalid_els:
10591091
v = copy_to_readonly_numpy_array(
1060-
validated_v, dtype='object')
1092+
validated_v, kind='O')
10611093
else:
10621094
v = copy_to_readonly_numpy_array(
1063-
validated_v, dtype='unicode')
1095+
validated_v, kind='U')
10641096
elif self.array_ok and is_simple_array(v):
10651097
validated_v = [
10661098
self.validate_coerce(e, should_raise=False)
@@ -1509,7 +1541,7 @@ def validate_coerce(self, v):
15091541
self.raise_invalid_elements(invalid_els)
15101542

15111543
if is_homogeneous_array(v):
1512-
v = copy_to_readonly_numpy_array(validated_v, dtype='unicode')
1544+
v = copy_to_readonly_numpy_array(validated_v, kind='U')
15131545
else:
15141546
v = to_scalar_or_list(v)
15151547
else:
@@ -1559,7 +1591,7 @@ def validate_coerce(self, v):
15591591
# Pass None through
15601592
pass
15611593
elif self.array_ok and is_homogeneous_array(v):
1562-
v = copy_to_readonly_numpy_array(v, dtype='object')
1594+
v = copy_to_readonly_numpy_array(v, kind='O')
15631595
elif self.array_ok and is_simple_array(v):
15641596
v = to_scalar_or_list(v)
15651597
return v

Diff for: _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:
+179
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)

Diff for: plotly/serializers.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from .optional_imports import get_module
33
np = get_module('numpy')
44

5+
56
def _py_to_js(v, widget_manager):
67
"""
78
Python -> Javascript ipywidget serializer
@@ -38,12 +39,18 @@ def _py_to_js(v, widget_manager):
3839
elif np is not None and isinstance(v, np.ndarray):
3940
# Convert 1D numpy arrays with numeric types to memoryviews with
4041
# datatype and shape metadata.
41-
if v.ndim == 1 and v.dtype.kind in ['u', 'i', 'f']:
42+
if (v.ndim == 1 and
43+
v.dtype.kind in ['u', 'i', 'f'] and
44+
v.dtype != 'int64' and
45+
v.dtype != 'uint64'):
46+
47+
# We have a numpy array the we can directly map to a JavaScript
48+
# Typed array
4249
return {'buffer': memoryview(v),
4350
'dtype': str(v.dtype),
4451
'shape': v.shape}
4552
else:
46-
# Convert all other numpy to lists
53+
# Convert all other numpy arrays to lists
4754
return v.tolist()
4855

4956
# Handle Undefined

0 commit comments

Comments
 (0)