Skip to content

Commit cc1025a

Browse files
jennolsen84jreback
authored andcommitted
COMPAT: do not upcast results to float64 when float32 scalar *+/- float64 array
closes pandas-dev#12388 Author: Jenn Olsen <[email protected]> Closes pandas-dev#12559 from jennolsen84/noevalupcast and squashes the following commits: 3f61252 [Jenn Olsen] do not upcast to float64 everytime
1 parent ed4cd3a commit cc1025a

File tree

4 files changed

+57
-2
lines changed

4 files changed

+57
-2
lines changed

doc/source/whatsnew/v0.18.2.txt

+1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ Other enhancements
8989

9090
- ``pd.read_html()`` has gained support for the ``decimal`` option (:issue:`12907`)
9191

92+
- ``eval``'s upcasting rules for ``float32`` types have been updated to be more consistent with NumPy's rules. New behavior will not upcast to ``float64`` if you multiply a pandas ``float32`` object by a scalar float64. (:issue:`12388`)
9293

9394

9495
.. _whatsnew_0182.api:

pandas/computation/expr.py

+15
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import tokenize
66

77
from functools import partial
8+
import numpy as np
89

910
import pandas as pd
1011
from pandas import compat
@@ -356,6 +357,19 @@ def _possibly_transform_eq_ne(self, node, left=None, right=None):
356357
right)
357358
return op, op_class, left, right
358359

360+
def _possibly_downcast_constants(self, left, right):
361+
f32 = np.dtype(np.float32)
362+
if left.isscalar and not right.isscalar and right.return_type == f32:
363+
# right is a float32 array, left is a scalar
364+
name = self.env.add_tmp(np.float32(left.value))
365+
left = self.term_type(name, self.env)
366+
if right.isscalar and not left.isscalar and left.return_type == f32:
367+
# left is a float32 array, right is a scalar
368+
name = self.env.add_tmp(np.float32(right.value))
369+
right = self.term_type(name, self.env)
370+
371+
return left, right
372+
359373
def _possibly_eval(self, binop, eval_in_python):
360374
# eval `in` and `not in` (for now) in "partial" python space
361375
# things that can be evaluated in "eval" space will be turned into
@@ -399,6 +413,7 @@ def _possibly_evaluate_binop(self, op, op_class, lhs, rhs,
399413

400414
def visit_BinOp(self, node, **kwargs):
401415
op, op_class, left, right = self._possibly_transform_eq_ne(node)
416+
left, right = self._possibly_downcast_constants(left, right)
402417
return self._possibly_evaluate_binop(op, op_class, left, right)
403418

404419
def visit_Div(self, node, **kwargs):

pandas/computation/ops.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -276,18 +276,26 @@ def _not_in(x, y):
276276
_binary_ops_dict.update(d)
277277

278278

279-
def _cast_inplace(terms, dtype):
279+
def _cast_inplace(terms, acceptable_dtypes, dtype):
280280
"""Cast an expression inplace.
281281
282282
Parameters
283283
----------
284284
terms : Op
285285
The expression that should cast.
286+
acceptable_dtypes : list of acceptable numpy.dtype
287+
Will not cast if term's dtype in this list.
288+
289+
.. versionadded:: 0.18.2
290+
286291
dtype : str or numpy.dtype
287292
The dtype to cast to.
288293
"""
289294
dt = np.dtype(dtype)
290295
for term in terms:
296+
if term.type in acceptable_dtypes:
297+
continue
298+
291299
try:
292300
new_value = term.value.astype(dt)
293301
except AttributeError:
@@ -452,7 +460,9 @@ def __init__(self, lhs, rhs, truediv, *args, **kwargs):
452460
rhs.return_type))
453461

454462
if truediv or PY3:
455-
_cast_inplace(com.flatten(self), np.float_)
463+
# do not upcast float32s to float64 un-necessarily
464+
acceptable_dtypes = [np.float32, np.float_]
465+
_cast_inplace(com.flatten(self), acceptable_dtypes, np.float_)
456466

457467

458468
_unary_ops_syms = '+', '-', '~', 'not'

pandas/computation/tests/test_eval.py

+29
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,35 @@ def check_chained_cmp_op(self, lhs, cmp1, mid, cmp2, rhs):
749749

750750
ENGINES_PARSERS = list(product(_engines, expr._parsers))
751751

752+
#-------------------------------------
753+
# typecasting rules consistency with python
754+
# issue #12388
755+
756+
class TestTypeCasting(tm.TestCase):
757+
758+
def check_binop_typecasting(self, engine, parser, op, dt):
759+
tm.skip_if_no_ne(engine)
760+
df = mkdf(5, 3, data_gen_f=f, dtype=dt)
761+
s = 'df {} 3'.format(op)
762+
res = pd.eval(s, engine=engine, parser=parser)
763+
self.assertTrue(df.values.dtype == dt)
764+
self.assertTrue(res.values.dtype == dt)
765+
assert_frame_equal(res, eval(s))
766+
767+
s = '3 {} df'.format(op)
768+
res = pd.eval(s, engine=engine, parser=parser)
769+
self.assertTrue(df.values.dtype == dt)
770+
self.assertTrue(res.values.dtype == dt)
771+
assert_frame_equal(res, eval(s))
772+
773+
def test_binop_typecasting(self):
774+
for engine, parser in ENGINES_PARSERS:
775+
for op in ['+', '-', '*', '**', '/']:
776+
# maybe someday... numexpr has too many upcasting rules now
777+
#for dt in chain(*(np.sctypes[x] for x in ['uint', 'int', 'float'])):
778+
for dt in [np.float32, np.float64]:
779+
yield self.check_binop_typecasting, engine, parser, op, dt
780+
752781

753782
#-------------------------------------
754783
# basic and complex alignment

0 commit comments

Comments
 (0)