Repository: pandas-dev/pandas

Issue:
Need support for floor and ceil methods during pandas.eval which are provided by numexpr



Discussion:
a PR would be ok here

Resolving patch:
diff --git a/pandas/core/computation/check.py b/pandas/core/computation/check.py
--- a/pandas/core/computation/check.py
+++ b/pandas/core/computation/check.py
@@ -3,11 +3,13 @@
 
 _NUMEXPR_INSTALLED = False
 _MIN_NUMEXPR_VERSION = "2.6.1"
+_NUMEXPR_VERSION = None
 
 try:
     import numexpr as ne
     ver = LooseVersion(ne.__version__)
     _NUMEXPR_INSTALLED = ver >= LooseVersion(_MIN_NUMEXPR_VERSION)
+    _NUMEXPR_VERSION = ver
 
     if not _NUMEXPR_INSTALLED:
         warnings.warn(
@@ -19,4 +21,4 @@
 except ImportError:  # pragma: no cover
     pass
 
-__all__ = ['_NUMEXPR_INSTALLED']
+__all__ = ['_NUMEXPR_INSTALLED', '_NUMEXPR_VERSION']
diff --git a/pandas/core/computation/ops.py b/pandas/core/computation/ops.py
--- a/pandas/core/computation/ops.py
+++ b/pandas/core/computation/ops.py
@@ -2,6 +2,7 @@
 """
 
 from datetime import datetime
+from distutils.version import LooseVersion
 from functools import partial
 import operator as op
 
@@ -23,8 +24,11 @@
 
 _unary_math_ops = ('sin', 'cos', 'exp', 'log', 'expm1', 'log1p',
                    'sqrt', 'sinh', 'cosh', 'tanh', 'arcsin', 'arccos',
-                   'arctan', 'arccosh', 'arcsinh', 'arctanh', 'abs', 'log10')
+                   'arctan', 'arccosh', 'arcsinh', 'arctanh', 'abs', 'log10',
+                   'floor', 'ceil'
+                   )
 _binary_math_ops = ('arctan2',)
+
 _mathops = _unary_math_ops + _binary_math_ops
 
 
@@ -539,11 +543,17 @@ def __unicode__(self):
 
 
 class FuncNode(object):
-
     def __init__(self, name):
-        if name not in _mathops:
+        from pandas.core.computation.check import (_NUMEXPR_INSTALLED,
+                                                   _NUMEXPR_VERSION)
+        if name not in _mathops or (
+                _NUMEXPR_INSTALLED and
+                _NUMEXPR_VERSION < LooseVersion('2.6.9') and
+                name in ('floor', 'ceil')
+        ):
             raise ValueError(
                 "\"{0}\" is not a supported function".format(name))
+
         self.name = name
         self.func = getattr(np, name)
 


Test patch:
diff --git a/pandas/tests/computation/test_eval.py b/pandas/tests/computation/test_eval.py
--- a/pandas/tests/computation/test_eval.py
+++ b/pandas/tests/computation/test_eval.py
@@ -1,5 +1,6 @@
 import warnings
 import operator
+from distutils.version import LooseVersion
 from itertools import product
 
 import pytest
@@ -14,6 +15,7 @@
 from pandas.util.testing import makeCustomDataframe as mkdf
 
 from pandas.core.computation import pytables
+from pandas.core.computation.check import _NUMEXPR_VERSION
 from pandas.core.computation.engines import _engines, NumExprClobberingError
 from pandas.core.computation.expr import PythonExprVisitor, PandasExprVisitor
 from pandas.core.computation.expressions import (
@@ -32,6 +34,7 @@
                                  assert_produces_warning)
 from pandas.compat import PY3, reduce
 
+
 _series_frame_incompatible = _bool_ops_syms
 _scalar_skip = 'in', 'not in'
 
@@ -54,6 +57,25 @@ def parser(request):
     return request.param
 
 
+@pytest.fixture
+def ne_lt_2_6_9():
+    if _NUMEXPR_INSTALLED and _NUMEXPR_VERSION >= LooseVersion('2.6.9'):
+        pytest.skip("numexpr is >= 2.6.9")
+    return 'numexpr'
+
+
+@pytest.fixture
+def unary_fns_for_ne():
+    if _NUMEXPR_INSTALLED:
+        if _NUMEXPR_VERSION >= LooseVersion('2.6.9'):
+            return _unary_math_ops
+        else:
+            return tuple(x for x in _unary_math_ops
+                         if x not in ("floor", "ceil"))
+    else:
+        pytest.skip("numexpr is not present")
+
+
 def engine_has_neg_frac(engine):
     return _engines[engine].has_neg_frac
 
@@ -1622,16 +1644,26 @@ def eval(self, *args, **kwargs):
         kwargs['level'] = kwargs.pop('level', 0) + 1
         return pd.eval(*args, **kwargs)
 
-    def test_unary_functions(self):
+    def test_unary_functions(self, unary_fns_for_ne):
         df = DataFrame({'a': np.random.randn(10)})
         a = df.a
-        for fn in self.unary_fns:
+
+        for fn in unary_fns_for_ne:
             expr = "{0}(a)".format(fn)
             got = self.eval(expr)
             with np.errstate(all='ignore'):
                 expect = getattr(np, fn)(a)
             tm.assert_series_equal(got, expect, check_names=False)
 
+    def test_floor_and_ceil_functions_raise_error(self,
+                                                  ne_lt_2_6_9,
+                                                  unary_fns_for_ne):
+        for fn in ('floor', 'ceil'):
+            msg = "\"{0}\" is not a supported function".format(fn)
+            with pytest.raises(ValueError, match=msg):
+                expr = "{0}(100)".format(fn)
+                self.eval(expr)
+
     def test_binary_functions(self):
         df = DataFrame({'a': np.random.randn(10),
                         'b': np.random.randn(10)})