Skip to content

Commit 3d4d5af

Browse files
committed
BUG: treat None as NA in DataFrame arithmetic operations, #992
1 parent 5901364 commit 3d4d5af

File tree

3 files changed

+62
-14
lines changed

3 files changed

+62
-14
lines changed

pandas/core/frame.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -168,35 +168,54 @@
168168
#----------------------------------------------------------------------
169169
# Factory helper methods
170170

171-
def _arith_method(func, name, default_axis='columns'):
171+
def _arith_method(op, name, default_axis='columns'):
172+
def na_op(x, y):
173+
try:
174+
result = op(x, y)
175+
except TypeError:
176+
xrav = x.ravel()
177+
result = np.empty(x.size, dtype=x.dtype)
178+
if isinstance(y, np.ndarray):
179+
yrav = y.ravel()
180+
mask = notnull(xrav) & notnull(yrav)
181+
result[mask] = op(xrav[mask], yrav[mask])
182+
else:
183+
mask = notnull(xrav)
184+
result[mask] = op(xrav[mask], y)
185+
186+
np.putmask(result, -mask, np.nan)
187+
result = result.reshape(x.shape)
188+
189+
return result
190+
172191
@Appender(_arith_doc % name)
173192
def f(self, other, axis=default_axis, level=None, fill_value=None):
174193
if isinstance(other, DataFrame): # Another DataFrame
175-
return self._combine_frame(other, func, fill_value, level)
194+
return self._combine_frame(other, na_op, fill_value, level)
176195
elif isinstance(other, Series):
177-
return self._combine_series(other, func, fill_value, axis, level)
196+
return self._combine_series(other, na_op, fill_value, axis, level)
178197
elif isinstance(other, (list, tuple)):
179198
if axis is not None and self._get_axis_name(axis) == 'index':
180199
casted = Series(other, index=self.index)
181200
else:
182201
casted = Series(other, index=self.columns)
183-
return self._combine_series(casted, func, fill_value, axis, level)
202+
return self._combine_series(casted, na_op, fill_value, axis, level)
184203
elif isinstance(other, np.ndarray):
185204
if other.ndim == 1:
186205
if axis is not None and self._get_axis_name(axis) == 'index':
187206
casted = Series(other, index=self.index)
188207
else:
189208
casted = Series(other, index=self.columns)
190-
return self._combine_series(casted, func, fill_value,
209+
return self._combine_series(casted, na_op, fill_value,
191210
axis, level)
192211
elif other.ndim == 2:
193212
casted = DataFrame(other, index=self.index,
194213
columns=self.columns)
195-
return self._combine_frame(casted, func, fill_value, level)
214+
return self._combine_frame(casted, na_op, fill_value, level)
196215
else: # pragma: no cover
197216
raise ValueError("Bad argument shape")
198217
else:
199-
return self._combine_const(other, func)
218+
return self._combine_const(other, na_op)
200219

201220
f.__name__ = name
202221

pandas/core/series.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,14 @@ def na_op(x, y):
4848
try:
4949
result = op(x, y)
5050
except TypeError:
51+
result = np.empty(len(x), dtype=x.dtype)
5152
if isinstance(y, np.ndarray):
5253
mask = notnull(x) & notnull(y)
53-
result = np.empty(len(x), dtype=x.dtype)
5454
result[mask] = op(x[mask], y[mask])
5555
else:
5656
mask = notnull(x)
57-
result = np.empty(len(x), dtype=x.dtype)
5857
result[mask] = op(x[mask], y)
58+
np.putmask(result, -mask, np.nan)
5959

6060
return result
6161

pandas/tests/test_frame.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2136,6 +2136,30 @@ def test_operators(self):
21362136
expected = self.frame2 * 2
21372137
assert_frame_equal(added, expected)
21382138

2139+
def test_operators_none_as_na(self):
2140+
df = DataFrame({"col1": [2,5.0,123,None],
2141+
"col2": [1,2,3,4]})
2142+
2143+
ops = [operator.add, operator.sub, operator.mul, operator.div]
2144+
2145+
for op in ops:
2146+
filled = df.fillna(np.nan)
2147+
result = op(df, 3)
2148+
expected = op(filled, 3)
2149+
expected[com.isnull(expected)] = None
2150+
assert_frame_equal(result, expected)
2151+
2152+
result = op(df, df)
2153+
expected = op(filled, filled)
2154+
expected[com.isnull(expected)] = None
2155+
assert_frame_equal(result, expected)
2156+
2157+
result = op(df, df.fillna(7))
2158+
assert_frame_equal(result, expected)
2159+
2160+
result = op(df.fillna(7), df)
2161+
assert_frame_equal(result, expected)
2162+
21392163
def test_logical_operators(self):
21402164
import operator
21412165

@@ -2154,16 +2178,21 @@ def _check_unary_op(op):
21542178
assert_frame_equal(result, expected)
21552179

21562180
df1 = {'a': {'a': True, 'b': False, 'c': False, 'd': True, 'e': True},
2157-
'b': {'a': False, 'b': True, 'c': False, 'd': False, 'e': False},
2158-
'c': {'a': False, 'b': False, 'c': True, 'd': False, 'e': False},
2181+
'b': {'a': False, 'b': True, 'c': False,
2182+
'd': False, 'e': False},
2183+
'c': {'a': False, 'b': False, 'c': True,
2184+
'd': False, 'e': False},
21592185
'd': {'a': True, 'b': False, 'c': False, 'd': True, 'e': True},
21602186
'e': {'a': True, 'b': False, 'c': False, 'd': True, 'e': True}}
21612187

21622188
df2 = {'a': {'a': True, 'b': False, 'c': True, 'd': False, 'e': False},
2163-
'b': {'a': False, 'b': True, 'c': False, 'd': False, 'e': False},
2189+
'b': {'a': False, 'b': True, 'c': False,
2190+
'd': False, 'e': False},
21642191
'c': {'a': True, 'b': False, 'c': True, 'd': False, 'e': False},
2165-
'd': {'a': False, 'b': False, 'c': False, 'd': True, 'e': False},
2166-
'e': {'a': False, 'b': False, 'c': False, 'd': False, 'e': True}}
2192+
'd': {'a': False, 'b': False, 'c': False,
2193+
'd': True, 'e': False},
2194+
'e': {'a': False, 'b': False, 'c': False,
2195+
'd': False, 'e': True}}
21672196

21682197
df1 = DataFrame(df1)
21692198
df2 = DataFrame(df2)

0 commit comments

Comments
 (0)