Skip to content

Commit 640d4c9

Browse files
committed
DOC: update docs in enhancedperf.rst
TST: addtional tests for multiple assignment, targets ENH: add target to Scope, use instead of resolvers
1 parent 44c37b9 commit 640d4c9

File tree

5 files changed

+55
-31
lines changed

5 files changed

+55
-31
lines changed

doc/source/enhancingperf.rst

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,18 +441,27 @@ The ``DataFrame.eval`` method (Experimental)
441441
In addition to the top level :func:`~pandas.eval` function you can also
442442
evaluate an expression in the "context" of a ``DataFrame``.
443443

444-
445444
.. ipython:: python
446445
447446
df = DataFrame(randn(5, 2), columns=['a', 'b'])
448447
df.eval('a + b')
449448
450-
451449
Any expression that is a valid :func:`~pandas.eval` expression is also a valid
452450
``DataFrame.eval`` expression, with the added benefit that *you don't have to
453451
prefix the name of the* ``DataFrame`` *to the column you're interested in
454452
evaluating*.
455453

454+
In addition, you can perform in-line assignment of columns within an expression.
455+
This can allow for *formulaic evaluation*. Only a signle assignement is permitted.
456+
It can be a new column name or an existing column name. It must be a string-like.
457+
458+
.. ipython:: python
459+
460+
df = DataFrame(dict(a = range(5), b = range(5,10)))
461+
df.eval('c=a+b')
462+
df.eval('d=a+b+c')
463+
df.eval('a=1')
464+
df
456465
457466
Local Variables
458467
~~~~~~~~~~~~~~~

pandas/computation/eval.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ def _convert_expression(expr):
113113

114114

115115
def eval(expr, parser='pandas', engine='numexpr', truediv=True,
116-
local_dict=None, global_dict=None, resolvers=None, level=2):
116+
local_dict=None, global_dict=None, resolvers=None, level=2,
117+
target=None):
117118
"""Evaluate a Python expression as a string using various backends.
118119
119120
The following arithmetic operations are supported: ``+``, ``-``, ``*``,
@@ -169,6 +170,8 @@ def eval(expr, parser='pandas', engine='numexpr', truediv=True,
169170
level : int, optional
170171
The number of prior stack frames to traverse and add to the current
171172
scope. Most users will **not** need to change this parameter.
173+
target : a target object for assignment, optional, default is None
174+
essentially this is a passed in resolver
172175
173176
Returns
174177
-------
@@ -194,7 +197,7 @@ def eval(expr, parser='pandas', engine='numexpr', truediv=True,
194197

195198
# get our (possibly passed-in) scope
196199
env = _ensure_scope(global_dict=global_dict, local_dict=local_dict,
197-
resolvers=resolvers, level=level)
200+
resolvers=resolvers, level=level, target=target)
198201

199202
parsed_expr = Expr(expr, engine=engine, parser=parser, env=env,
200203
truediv=truediv)
@@ -205,8 +208,8 @@ def eval(expr, parser='pandas', engine='numexpr', truediv=True,
205208
ret = eng_inst.evaluate()
206209

207210
# assign if needed
208-
if parsed_expr.assignee is not None and parsed_expr.assigner is not None:
209-
parsed_expr.assignee[parsed_expr.assigner] = ret
211+
if env.target is not None and parsed_expr.assigner is not None:
212+
env.target[parsed_expr.assigner] = ret
210213
return None
211214

212215
return ret

pandas/computation/expr.py

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@
2525

2626

2727
def _ensure_scope(level=2, global_dict=None, local_dict=None, resolvers=None,
28-
**kwargs):
28+
target=None, **kwargs):
2929
"""Ensure that we are grabbing the correct scope."""
3030
return Scope(gbls=global_dict, lcls=local_dict, level=level,
31-
resolvers=resolvers)
31+
resolvers=resolvers, target=target)
3232

3333

3434
def _check_disjoint_resolver_names(resolver_keys, local_keys, global_keys):
@@ -89,20 +89,23 @@ class Scope(StringMixin):
8989
resolver_keys : frozenset
9090
"""
9191
__slots__ = ('globals', 'locals', 'resolvers', '_global_resolvers',
92-
'resolver_keys', '_resolver', 'level', 'ntemps')
92+
'resolver_keys', '_resolver', 'level', 'ntemps', 'target')
9393

94-
def __init__(self, gbls=None, lcls=None, level=1, resolvers=None):
94+
def __init__(self, gbls=None, lcls=None, level=1, resolvers=None, target=None):
9595
self.level = level
9696
self.resolvers = tuple(resolvers or [])
9797
self.globals = dict()
9898
self.locals = dict()
99+
self.target = target
99100
self.ntemps = 1 # number of temporary variables in this scope
100101

101102
if isinstance(lcls, Scope):
102103
ld, lcls = lcls, dict()
103104
self.locals.update(ld.locals.copy())
104105
self.globals.update(ld.globals.copy())
105106
self.resolvers += ld.resolvers
107+
if ld.target is not None:
108+
self.target = ld.target
106109
self.update(ld.level)
107110

108111
frame = sys._getframe(level)
@@ -131,9 +134,10 @@ def __init__(self, gbls=None, lcls=None, level=1, resolvers=None):
131134

132135
def __unicode__(self):
133136
return com.pprint_thing("locals: {0}\nglobals: {0}\nresolvers: "
134-
"{0}".format(list(self.locals.keys()),
135-
list(self.globals.keys()),
136-
list(self.resolver_keys)))
137+
"{0}\ntarget: {0}".format(list(self.locals.keys()),
138+
list(self.globals.keys()),
139+
list(self.resolver_keys),
140+
self.target))
137141

138142
def __getitem__(self, key):
139143
return self.resolve(key, globally=False)
@@ -418,7 +422,6 @@ def __init__(self, env, engine, parser, preparser=_preparse):
418422
self.engine = engine
419423
self.parser = parser
420424
self.preparser = preparser
421-
self.assignee = None
422425
self.assigner = None
423426

424427
def visit(self, node, **kwargs):
@@ -583,7 +586,7 @@ def visit_Assign(self, node, **kwargs):
583586
584587
c = a + b
585588
586-
set the assignee at the top level, must be a Name node which
589+
set the assigner at the top level, must be a Name node which
587590
might or might not exist in the resolvers
588591
589592
"""
@@ -592,10 +595,8 @@ def visit_Assign(self, node, **kwargs):
592595
raise SyntaxError('can only assign a single expression')
593596
if not isinstance(node.targets[0], ast.Name):
594597
raise SyntaxError('left hand side of an assignment must be a single name')
595-
596-
# we have no one to assign to
597-
if not len(self.env.resolvers):
598-
raise NotImplementedError
598+
if self.env.target is None:
599+
raise ValueError('cannot assign without a target object')
599600

600601
try:
601602
assigner = self.visit(node.targets[0], **kwargs)
@@ -605,10 +606,6 @@ def visit_Assign(self, node, **kwargs):
605606
self.assigner = getattr(assigner,'name',assigner)
606607
if self.assigner is None:
607608
raise SyntaxError('left hand side of an assignment must be a single resolvable name')
608-
try:
609-
self.assignee = self.env.resolvers[0]
610-
except:
611-
raise ValueError('cannot create an assignee for this expression')
612609

613610
return self.visit(node.value, **kwargs)
614611

@@ -749,10 +746,6 @@ def __init__(self, expr, engine='numexpr', parser='pandas', env=None,
749746
def assigner(self):
750747
return getattr(self._visitor,'assigner',None)
751748

752-
@property
753-
def assignee(self):
754-
return getattr(self._visitor,'assignee',None)
755-
756749
def __call__(self):
757750
self.env.locals['truediv'] = self.truediv
758751
return self.terms(self.env)

pandas/computation/tests/test_eval.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1152,10 +1152,11 @@ def test_assignment_fails(self):
11521152
df = DataFrame(np.random.randn(5, 3), columns=list('abc'))
11531153
df2 = DataFrame(np.random.randn(5, 3))
11541154
expr1 = 'df = df2'
1155-
self.assertRaises(NotImplementedError, self.eval, expr1,
1155+
self.assertRaises(ValueError, self.eval, expr1,
11561156
local_dict={'df': df, 'df2': df2})
11571157

11581158
def test_assignment_column(self):
1159+
skip_if_no_ne('numexpr')
11591160
df = DataFrame(np.random.randn(5, 2), columns=list('ab'))
11601161
orig_df = df.copy()
11611162

@@ -1181,9 +1182,13 @@ def test_assignment_column(self):
11811182
assert_frame_equal(df,expected)
11821183

11831184
# with a local name overlap
1184-
a = 1
1185-
df = orig_df.copy()
1186-
df.eval('a = 1 + b')
1185+
def f():
1186+
df = orig_df.copy()
1187+
a = 1
1188+
df.eval('a = 1 + b')
1189+
return df
1190+
1191+
df = f()
11871192
expected = orig_df.copy()
11881193
expected['a'] = 1 + expected['b']
11891194
assert_frame_equal(df,expected)
@@ -1194,6 +1199,18 @@ def f():
11941199
df.eval('a=a+b')
11951200
self.assertRaises(NameResolutionError, f)
11961201

1202+
# multiple assignment
1203+
df = orig_df.copy()
1204+
df.eval('c = a + b')
1205+
self.assertRaises(SyntaxError, df.eval, 'c = a = b')
1206+
1207+
# explicit targets
1208+
df = orig_df.copy()
1209+
self.eval('c = df.a + df.b', local_dict={'df' : df}, target=df)
1210+
expected = orig_df.copy()
1211+
expected['c'] = expected['a'] + expected['b']
1212+
assert_frame_equal(df,expected)
1213+
11971214
def test_basic_period_index_boolean_expression(self):
11981215
df = mkdf(2, 2, data_gen_f=f, c_idx_type='p', r_idx_type='i')
11991216

pandas/core/frame.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1797,12 +1797,14 @@ def eval(self, expr, **kwargs):
17971797
>>> from pandas import DataFrame
17981798
>>> df = DataFrame(randn(10, 2), columns=list('ab'))
17991799
>>> df.eval('a + b')
1800+
>>> df.eval('c=a + b')
18001801
"""
18011802
resolvers = kwargs.pop('resolvers', None)
18021803
if resolvers is None:
18031804
index_resolvers = self._get_resolvers()
18041805
resolvers = [self, index_resolvers]
18051806
kwargs['local_dict'] = _ensure_scope(resolvers=resolvers, **kwargs)
1807+
kwargs['target'] = self
18061808
return _eval(expr, **kwargs)
18071809

18081810
def _slice(self, slobj, axis=0, raise_on_error=False, typ=None):

0 commit comments

Comments
 (0)