Skip to content

ENH: ExtensionArray.fillna #19909

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Mar 16, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
74614cb
ENH: ExtensionArray.fillna
TomAugspurger Feb 22, 2018
67a19ba
REF: cast to object
TomAugspurger Feb 27, 2018
280ac94
Revert "REF: cast to object"
TomAugspurger Feb 27, 2018
6705712
Merge remote-tracking branch 'upstream/master' into fu1+fillna
TomAugspurger Feb 28, 2018
69a0f9b
Simpler implementation
TomAugspurger Feb 28, 2018
4d6846b
Support limit
TomAugspurger Feb 28, 2018
f3b81dc
Test backfill with limit
TomAugspurger Feb 28, 2018
70efbb8
BUG: ensure array-like for indexer
TomAugspurger Feb 28, 2018
8fc3851
Refactor tolist
TomAugspurger Mar 1, 2018
39096e5
Test Series[ea].tolist
TomAugspurger Mar 1, 2018
9e44f88
Merge remote-tracking branch 'upstream/master' into fu1+fillna
TomAugspurger Mar 1, 2018
e902f18
Fixed Categorical unwrapping
TomAugspurger Mar 1, 2018
17126e6
updated
TomAugspurger Mar 2, 2018
1106ef2
Back to getvalues list
TomAugspurger Mar 2, 2018
744c381
Simplified
TomAugspurger Mar 2, 2018
9a3fa55
As a method
TomAugspurger Mar 2, 2018
2bc43bc
Merge remote-tracking branch 'upstream/master' into fu1+fillna
TomAugspurger Mar 3, 2018
f22fd7b
Removed tolist fully
TomAugspurger Mar 3, 2018
c26d261
Merge remote-tracking branch 'upstream/master' into fu1+fillna
TomAugspurger Mar 5, 2018
b342efe
Linting
TomAugspurger Mar 5, 2018
a609f48
Merge remote-tracking branch 'upstream/master' into fu1+fillna
TomAugspurger Mar 12, 2018
a35f93c
Just apply to objects
TomAugspurger Mar 12, 2018
3f78dec
Merge remote-tracking branch 'upstream/master' into fu1+fillna
TomAugspurger Mar 12, 2018
1160e15
Linting
TomAugspurger Mar 13, 2018
c9c7a48
Merge remote-tracking branch 'upstream/master' into fu1+fillna
TomAugspurger Mar 14, 2018
05fced6
Merge remote-tracking branch 'upstream/master' into fu1+fillna
TomAugspurger Mar 15, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions pandas/core/arrays/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""An interface for extending pandas with custom arrays."""
import itertools

import numpy as np

from pandas.errors import AbstractMethodError
Expand Down Expand Up @@ -216,6 +218,88 @@ def isna(self):
"""
raise AbstractMethodError(self)

def tolist(self):
# type: () -> list
"""Convert the array to a list of scalars."""
return list(self)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed for fillna?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indirectly, via the default fillna.

Series.__iter__ calls Series.tolist, which calls Series._values.tolist. We could modify that to check the type and just call list(self._values) for EAs. I don't have a strong preference vs. adding a default .tolist.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or directly call values.__iter__ ? As iterating through values to make a list and then iterate again through that list sounds like a bit of overhead.

For .tolist() itself, it is of course consistent with Series and numpy arrays, but personally I am not sure what its value is compared to list(values). I don't think you typically can implement a more efficient conversion to lists than what list(values) will do by default? (i.e. iterating through the values)


def fillna(self, value=None, method=None, limit=None):
""" Fill NA/NaN values using the specified method.

Parameters
----------
method : {'backfill', 'bfill', 'pad', 'ffill', None}, default None
Method to use for filling holes in reindexed Series
pad / ffill: propagate last valid observation forward to next valid
backfill / bfill: use NEXT valid observation to fill gap
value : scalar, array-like
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value before method ? (order of signature)

If a scalar value is passed it is used to fill all missing values.
Alternatively, an array-like 'value' can be given. It's expected
that the array-like have the same length as 'self'.
limit : int, default None
(Not implemented yet for ExtensionArray!)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can now maybe be passed to pad_1d / backfill_1d ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I had to change things up slightly to use integers and the datetime fillna methods if you want to take a look.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Categorical.fillna can probably do soemthing similar, haven't looked.

If method is specified, this is the maximum number of consecutive
NaN values to forward/backward fill. In other words, if there is
a gap with more than this number of consecutive NaNs, it will only
be partially filled. If method is not specified, this is the
maximum number of entries along the entire axis where NaNs will be
filled.

Returns
-------
filled : ExtensionArray with NA/NaN filled
"""
from pandas.api.types import is_scalar
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A putmask type method would be extremely useful here.

I'll see what I can do to simplify this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would that be different with array[mask] = values ?

from pandas.util._validators import validate_fillna_kwargs

value, method = validate_fillna_kwargs(value, method)

if not is_scalar(value):
if len(value) != len(self):
raise ValueError("Length of 'value' does not match. Got ({}) "
" expected {}".format(len(value), len(self)))
else:
value = itertools.cycle([value])

if limit is not None:
msg = ("Specifying 'limit' for 'fillna' has not been implemented "
"yet for {} typed data".format(self.dtype))
raise NotImplementedError(msg)

mask = self.isna()

if mask.any():
# ffill / bfill
if method is not None:
if method == 'backfill':
data = reversed(self)
mask = reversed(mask)
last_valid = self[len(self) - 1]
else:
last_valid = self[0]
data = self

new_values = []

for is_na, val in zip(mask, data):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

huh? what is all this.

Copy link
Member

@jorisvandenbossche jorisvandenbossche Feb 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you can clearly see from the diff, it is an generic implementation of fillna, and the above part for the method kwarg (forward or backward fill).
If you don't like the implementation (I also don't really like it, feels very inefficient), it can be more helpful to give a more specific comment than that.

@TomAugspurger do you want this for cyberpandas? Otherwise, we could also start for now with only supporting filling with a value ? (that's what we supported for now in geopandas)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the other hand, we could maybe do this differently (that might be more efficient, without looping through all data): create an array of indices (arange), mask them with the missing values, then fill those indices with the existing fill-method machinery, and then use that to index into the original data and to fill the missing values.

Copy link
Contributor Author

@TomAugspurger TomAugspurger Feb 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really care about ffill / bfill, but Categorical supports it so best to be consistent.

Here's one alternative based astyping to object and setting.

+        from pandas.core.missing import pad_1d, backfill_1d
 
         value, method = validate_fillna_kwargs(value, method)
 
+        mask = self.isna()
+
         if not is_scalar(value):
             if len(value) != len(self):
                 raise ValueError("Length of 'value' does not match. Got ({}) "
                                  " expected {}".format(len(value), len(self)))
-        else:
-            value = itertools.cycle([value])
+            value = value[mask]
 
         if limit is not None:
             msg = ("Specifying 'limit' for 'fillna' has not been implemented "
                    "yet for {} typed data".format(self.dtype))
             raise NotImplementedError(msg)
 
-        mask = self.isna()
-
         if mask.any():
             # ffill / bfill
-            if method is not None:
-                if method == 'backfill':
-                    data = reversed(self)
-                    mask = reversed(mask)
-                    last_valid = self[len(self) - 1]
-                else:
-                    last_valid = self[0]
-                    data = self
-
-                new_values = []
-
-                for is_na, val in zip(mask, data):
-                    if is_na:
-                        new_values.append(last_valid)
-                    else:
-                        new_values.append(val)
-                        last_valid = val
-
-                if method in {'bfill', 'backfill'}:
-                    new_values = list(reversed(new_values))
+            if method == 'pad':
+                values = self.astype(object)
+                new_values = pad_1d(values, mask=mask)
+            elif method == 'backfill':
+                values = self.astype(object)
+                new_values = backfill_1d(values, mask=mask)
             else:
                 # fill with value
-                new_values = [
-                    val if is_na else original
-                    for is_na, original, val in zip(mask, self, value)
-                ]
+                new_values = self.copy()
+                new_values[mask] = value
         else:
             new_values = self
         return type(self)(new_values)

Timings (n_rows, value/method, seconds):

astype

1000, value, 0.01
1000, ffill, 0.00
1000, bfill, 0.00
1000000, value, 4.61
1000000, ffill, 2.34
1000000, bfill, 2.36

Loopy

1000, value, 0.00
1000, ffill, 0.00
1000, bfill, 0.00
1000000, value, 1.28
1000000, ffill, 1.37
1000000, bfill, 1.32

So the loopy version (current PR) is ~2-4x faster. Lots of variability there, esp depending on the speed of EA.setitem, but it seems like a good default.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What code did you use for the timings here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from timeit import default_timer as tic
import pandas as pd
import numpy as np
from pandas.tests.extension.decimal.array import DecimalArray
from decimal import Decimal

Ns = [1000, 1_000_000]

for N in Ns:

    arr = np.random.uniform(size=(N,))
    arr[arr > .8] = np.nan
    arr = DecimalArray([Decimal(x) for x in arr])

    start = tic()
    arr.fillna(value=Decimal('1.0'))
    stop = tic()

    print(f'{N}, value, {stop - start:0.2f}')

    start = tic()
    arr.fillna(method='ffill')
    stop = tic()

    print(f'{N}, ffill, {stop - start:0.2f}')

    start = tic()
    arr.fillna(method='bfill')
    stop = tic()

    print(f'{N}, bfill, {stop - start:0.2f}')

I can re-run it / profile things a bit to see why the array[object] version was so much slower.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With a quick dirty check:

In [1]: from geopandas.array import from_shapely
   ...: import shapely.geometry
   ...: a = np.array([shapely.geometry.Point(i, i) for i in np.random.randn(10000)], dtype=object)
   ...: a[np.random.randint(0, 10000, 10)] = None
   ...: ga = from_shapely(a)
   ...: s = pd.Series(ga)

In [2]: s.head()
Out[2]: 
0    POINT (-0.9684702137574125 -0.9684702137574125)
1        POINT (0.419808066032094 0.419808066032094)
2          POINT (0.57207855258413 0.57207855258413)
3      POINT (0.7773915414611537 0.7773915414611537)
4        POINT (2.073261603753559 2.073261603753559)
dtype: geometry

In [3]: %timeit s.fillna(method='pad')
115 ms ± 730 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [4]: %timeit s.fillna(method='pad_new')
217 µs ± 3.27 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

In [5]: s.fillna(method='pad').equals(s.fillna(method='pad_new'))
Out[5]: True

And for this 'new' method I did :

+                if method == 'pad_new':
+                    idx = np.arange(len(self), dtype=float)
+                    idx[mask] = np.nan
+                    idx = pad_1d(idx)
+                    idx = idx.astype('int64')
+                    new_values = self.take(idx)
+                else: ... existing code in PR

Copy link
Member

@jorisvandenbossche jorisvandenbossche Feb 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, didn't see your post before posting mine. So doing the same with the decimal array arr (with 10000 values) doesn't show a speed-up, rather a slowdown:

In [18]: %timeit arr.fillna(method='pad')
17.3 ms ± 772 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [19]: %timeit arr.fillna(method='pad_new')
28.5 ms ± 116 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

I suppose this is because DecimalArray uses an object array under the hood anyhow? So something like take will not be that efficient (while in geopandas it is a take on a int array)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we shouldn't care about performance for the default implementation. It'll be too dependent on which optimizations the subclass can implement.

I'll go with a simple implementation like your pad_new.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And also, if you care about performance, you probably don't have an object array under the hood but a typed array, and then this will be faster.

if is_na:
new_values.append(last_valid)
else:
new_values.append(val)
last_valid = val

if method in {'bfill', 'backfill'}:
new_values = list(reversed(new_values))
else:
# fill with value
new_values = [
val if is_na else original
for is_na, original, val in zip(mask, self, value)
]
else:
new_values = self
return type(self)(new_values)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait on #19906 before merging this, so I can update this line.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can now just be new_values ?


# ------------------------------------------------------------------------
# Indexing methods
# ------------------------------------------------------------------------
Expand Down
38 changes: 17 additions & 21 deletions pandas/core/internals.py
Original file line number Diff line number Diff line change
Expand Up @@ -1963,6 +1963,23 @@ def concat_same_type(self, to_concat, placement=None):
return self.make_block_same_class(values, ndim=self.ndim,
placement=placement)

def fillna(self, value, limit=None, inplace=False, downcast=None,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was moved from Categorical.fillna with changes

1.) Removed check for limit, since that's done in values.fillna
2.) Removed _try_coerce_result
For CategoricalBlock, this was unnecessary since
Categorical.fillna always returns a Categorical
3.) Used make_block_same_class
This limits ExtensionArray.fillna to not change the type
of the array / block, which I think is a good thing.

mgr=None):
values = self.values if inplace else self.values.copy()
values = values.fillna(value=value, limit=limit)
return [self.make_block_same_class(values=values,
placement=self.mgr_locs,
ndim=self.ndim)]

def interpolate(self, method='pad', axis=0, inplace=False, limit=None,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was just a move from CategoricalBlock to ExtensionBlock, no changes.

fill_value=None, **kwargs):

values = self.values if inplace else self.values.copy()
return self.make_block_same_class(
values=values.fillna(value=fill_value, method=method,
limit=limit),
placement=self.mgr_locs)


class NumericBlock(Block):
__slots__ = ()
Expand Down Expand Up @@ -2522,27 +2539,6 @@ def _try_coerce_result(self, result):

return result

def fillna(self, value, limit=None, inplace=False, downcast=None,
mgr=None):
# we may need to upcast our fill to match our dtype
if limit is not None:
raise NotImplementedError("specifying a limit for 'fillna' has "
"not been implemented yet")

values = self.values if inplace else self.values.copy()
values = self._try_coerce_result(values.fillna(value=value,
limit=limit))
return [self.make_block(values=values)]

def interpolate(self, method='pad', axis=0, inplace=False, limit=None,
fill_value=None, **kwargs):

values = self.values if inplace else self.values.copy()
return self.make_block_same_class(
values=values.fillna(fill_value=fill_value, method=method,
limit=limit),
placement=self.mgr_locs)

def shift(self, periods, axis=0, mgr=None):
return self.make_block_same_class(values=self.values.shift(periods),
placement=self.mgr_locs)
Expand Down
80 changes: 80 additions & 0 deletions pandas/tests/extension/base/missing.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import numpy as np
import pytest

import pandas as pd
import pandas.util.testing as tm
Expand Down Expand Up @@ -45,3 +46,82 @@ def test_dropna_frame(self, data_missing):
result = df.dropna()
expected = df.iloc[:0]
self.assert_frame_equal(result, expected)

def test_fillna_limit_raises(self, data_missing):
ser = pd.Series(data_missing)
fill_value = data_missing[1]
xpr = "Specifying 'limit' for 'fillna'.*{}".format(data_missing.dtype)

with tm.assert_raises_regex(NotImplementedError, xpr):
ser.fillna(fill_value, limit=2)

def test_fillna_series(self, data_missing):
fill_value = data_missing[1]
ser = pd.Series(data_missing)

result = ser.fillna(fill_value)
expected = pd.Series(type(data_missing)([fill_value, fill_value]))
self.assert_series_equal(result, expected)

# Fill with a series
result = ser.fillna(expected)
self.assert_series_equal(result, expected)

# Fill with a series not affecting the missing values
result = ser.fillna(ser)
self.assert_series_equal(result, ser)

@pytest.mark.xfail(reason="Too magical?")
def test_fillna_series_with_dict(self, data_missing):
fill_value = data_missing[1]
ser = pd.Series(data_missing)
expected = pd.Series(type(data_missing)([fill_value, fill_value]))

# Fill with a dict
result = ser.fillna({0: fill_value})
self.assert_series_equal(result, expected)

# Fill with a dict not affecting the missing values
result = ser.fillna({1: fill_value})
ser = pd.Series(data_missing)
self.assert_series_equal(result, ser)

@pytest.mark.parametrize('method', ['ffill', 'bfill'])
def test_fillna_series_method(self, data_missing, method):
fill_value = data_missing[1]

if method == 'ffill':
data_missing = type(data_missing)(data_missing[::-1])

result = pd.Series(data_missing).fillna(method=method)
expected = pd.Series(type(data_missing)([fill_value, fill_value]))

self.assert_series_equal(result, expected)

def test_fillna_frame(self, data_missing):
fill_value = data_missing[1]

result = pd.DataFrame({
"A": data_missing,
"B": [1, 2]
}).fillna(fill_value)

expected = pd.DataFrame({
"A": type(data_missing)([fill_value, fill_value]),
"B": [1, 2],
})

self.assert_frame_equal(result, expected)

def test_fillna_fill_other(self, data):
result = pd.DataFrame({
"A": data,
"B": [np.nan] * len(data)
}).fillna({"B": 0.0})

expected = pd.DataFrame({
"A": data,
"B": [0.0] * len(result),
})

self.assert_frame_equal(result, expected)
5 changes: 4 additions & 1 deletion pandas/tests/extension/category/test_categorical.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ def test_getitem_scalar(self):


class TestMissing(base.BaseMissingTests):
pass

@pytest.mark.skip(reason="Backwards compatability")
def test_fillna_limit_raises(self):
"""Has a different error message."""
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can just up the error message for Categorical.fillna probably.



class TestMethods(base.BaseMethodsTests):
Expand Down
75 changes: 33 additions & 42 deletions pandas/tests/extension/decimal/test_decimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,68 +35,59 @@ def na_value():
return decimal.Decimal("NaN")


class TestDtype(base.BaseDtypeTests):
pass
class BaseDecimal(object):
@staticmethod
def assert_series_equal(left, right, *args, **kwargs):
# tm.assert_series_equal doesn't handle Decimal('NaN').
# We will ensure that the NA values match, and then
# drop those values before moving on.

left_na = left.isna()
right_na = right.isna()

class TestInterface(base.BaseInterfaceTests):
pass
tm.assert_series_equal(left_na, right_na)
tm.assert_series_equal(left[~left_na], right[~right_na],
*args, **kwargs)

@staticmethod
def assert_frame_equal(left, right, *args, **kwargs):
# TODO(EA): select_dtypes
decimals = (left.dtypes == 'decimal').index

class TestConstructors(base.BaseConstructorsTests):
pass
for col in decimals:
BaseDecimal.assert_series_equal(left[col], right[col],
*args, **kwargs)

left = left.drop(columns=decimals)
right = right.drop(columns=decimals)
tm.assert_frame_equal(left, right, *args, **kwargs)

class TestReshaping(base.BaseReshapingTests):

def test_align(self, data, na_value):
# Have to override since assert_series_equal doesn't
# compare Decimal(NaN) properly.
a = data[:3]
b = data[2:5]
r1, r2 = pd.Series(a).align(pd.Series(b, index=[1, 2, 3]))
class TestDtype(BaseDecimal, base.BaseDtypeTests):
pass

# NaN handling
e1 = pd.Series(type(data)(list(a) + [na_value]))
e2 = pd.Series(type(data)([na_value] + list(b)))
tm.assert_series_equal(r1.iloc[:3], e1.iloc[:3])
assert r1[3].is_nan()
assert e1[3].is_nan()

tm.assert_series_equal(r2.iloc[1:], e2.iloc[1:])
assert r2[0].is_nan()
assert e2[0].is_nan()
class TestInterface(BaseDecimal, base.BaseInterfaceTests):
pass

def test_align_frame(self, data, na_value):
# Override for Decimal(NaN) comparison
a = data[:3]
b = data[2:5]
r1, r2 = pd.DataFrame({'A': a}).align(
pd.DataFrame({'A': b}, index=[1, 2, 3])
)

# Assumes that the ctor can take a list of scalars of the type
e1 = pd.DataFrame({'A': type(data)(list(a) + [na_value])})
e2 = pd.DataFrame({'A': type(data)([na_value] + list(b))})
class TestConstructors(BaseDecimal, base.BaseConstructorsTests):
pass

tm.assert_frame_equal(r1.iloc[:3], e1.iloc[:3])
assert r1.loc[3, 'A'].is_nan()
assert e1.loc[3, 'A'].is_nan()

tm.assert_frame_equal(r2.iloc[1:], e2.iloc[1:])
assert r2.loc[0, 'A'].is_nan()
assert e2.loc[0, 'A'].is_nan()
class TestReshaping(BaseDecimal, base.BaseReshapingTests):
pass


class TestGetitem(base.BaseGetitemTests):
class TestGetitem(BaseDecimal, base.BaseGetitemTests):
pass


class TestMissing(base.BaseMissingTests):
class TestMissing(BaseDecimal, base.BaseMissingTests):
pass


class TestMethods(base.BaseMethodsTests):
class TestMethods(BaseDecimal, base.BaseMethodsTests):
@pytest.mark.parametrize('dropna', [True, False])
@pytest.mark.xfail(reason="value_counts not implemented yet.")
def test_value_counts(self, all_data, dropna):
Expand All @@ -112,7 +103,7 @@ def test_value_counts(self, all_data, dropna):
tm.assert_series_equal(result, expected)


class TestCasting(base.BaseCastingTests):
class TestCasting(BaseDecimal, base.BaseCastingTests):
pass


Expand Down
8 changes: 7 additions & 1 deletion pandas/tests/extension/json/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,13 @@ class TestGetitem(base.BaseGetitemTests):


class TestMissing(base.BaseMissingTests):
pass
@pytest.mark.xfail(reason="Setting a dict as a scalar")
def test_fillna_series(self):
"""We treat dictionaries as a mapping in fillna, not a scalar."""

@pytest.mark.xfail(reason="Setting a dict as a scalar")
def test_fillna_frame(self):
"""We treat dictionaries as a mapping in fillna, not a scalar."""


class TestMethods(base.BaseMethodsTests):
Expand Down