Skip to content

Commit b35b165

Browse files
committed
Merge branch 'fix/8852' of https://github.com/aevri/pandas into aevri-fix/8852
Conflicts: pandas/tests/test_common.py
2 parents 4b4c1d5 + bd68912 commit b35b165

File tree

5 files changed

+119
-4
lines changed

5 files changed

+119
-4
lines changed

doc/source/whatsnew/v0.15.2.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@ Bug Fixes
158158
and the last offset is not calculated from the start of the range (:issue:`8683`)
159159

160160

161+
162+
- Bug where DataFrame.plot(kind='scatter') fails when checking if an np.array is in the DataFrame (:issue:`8852`)
163+
164+
165+
161166
- Bug in `pd.infer_freq`/`DataFrame.inferred_freq` that prevented proper sub-daily frequency inference
162167
when the index contained DST days (:issue:`8772`).
163168
- Bug where index name was still used when plotting a series with ``use_index=False`` (:issue:`8558`).

pandas/core/common.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2504,6 +2504,38 @@ def is_list_like(arg):
25042504
not isinstance(arg, compat.string_and_binary_types))
25052505

25062506

2507+
def is_hashable(arg):
2508+
"""Return True if hash(arg) will succeed, False otherwise.
2509+
2510+
Some types will pass a test against collections.Hashable but fail when they
2511+
are actually hashed with hash().
2512+
2513+
Distinguish between these and other types by trying the call to hash() and
2514+
seeing if they raise TypeError.
2515+
2516+
Examples
2517+
--------
2518+
>>> a = ([],)
2519+
>>> isinstance(a, collections.Hashable)
2520+
True
2521+
>>> is_hashable(a)
2522+
False
2523+
"""
2524+
# don't consider anything not collections.Hashable, so as not to broaden
2525+
# the definition of hashable beyond that. For example, old-style classes
2526+
# are not collections.Hashable but they won't fail hash().
2527+
if not isinstance(arg, collections.Hashable):
2528+
return False
2529+
2530+
# narrow the definition of hashable if hash(arg) fails in practice
2531+
try:
2532+
hash(arg)
2533+
except TypeError:
2534+
return False
2535+
else:
2536+
return True
2537+
2538+
25072539
def is_sequence(x):
25082540
try:
25092541
iter(x)

pandas/tests/test_common.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# -*- coding: utf-8 -*-
2+
import collections
23
from datetime import datetime
34
import re
5+
import sys
46

57
import nose
68
from nose.tools import assert_equal
@@ -43,7 +45,7 @@ def test_get_callable_name():
4345
from functools import partial
4446
getname = com._get_callable_name
4547

46-
def fn(x):
48+
def fn(x):
4749
return x
4850
lambda_ = lambda x: x
4951
part1 = partial(fn)
@@ -399,6 +401,55 @@ def test_is_list_like():
399401
assert not com.is_list_like(f)
400402

401403

404+
def test_is_hashable():
405+
406+
# all new-style classes are hashable by default
407+
class HashableClass(object):
408+
pass
409+
410+
class UnhashableClass1(object):
411+
__hash__ = None
412+
413+
class UnhashableClass2(object):
414+
def __hash__(self):
415+
raise TypeError("Not hashable")
416+
417+
hashable = (
418+
1, 'a', tuple(), (1,), HashableClass(),
419+
)
420+
not_hashable = (
421+
[], UnhashableClass1(),
422+
)
423+
abc_hashable_not_really_hashable = (
424+
([],), UnhashableClass2(),
425+
)
426+
427+
for i in hashable:
428+
assert isinstance(i, collections.Hashable)
429+
assert com.is_hashable(i)
430+
for i in not_hashable:
431+
assert not isinstance(i, collections.Hashable)
432+
assert not com.is_hashable(i)
433+
for i in abc_hashable_not_really_hashable:
434+
assert isinstance(i, collections.Hashable)
435+
assert not com.is_hashable(i)
436+
437+
# numpy.array is no longer collections.Hashable as of
438+
# https://github.com/numpy/numpy/pull/5326, just test
439+
# pandas.common.is_hashable()
440+
assert not com.is_hashable(np.array([]))
441+
442+
# old-style classes in Python 2 don't appear hashable to
443+
# collections.Hashable but also seem to support hash() by default
444+
if sys.version_info[0] == 2:
445+
class OldStyleClass():
446+
pass
447+
c = OldStyleClass()
448+
assert not isinstance(c, collections.Hashable)
449+
assert not com.is_hashable(c)
450+
hash(c) # this will not raise
451+
452+
402453
def test_ensure_int32():
403454
values = np.arange(10, dtype=np.int32)
404455
result = com._ensure_int32(values)

pandas/tests/test_graphics.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1645,6 +1645,31 @@ def test_plot_scatter_with_c(self):
16451645
self.assertIs(ax.collections[0].colorbar, None)
16461646
self._check_colors(ax.collections, facecolors=['r'])
16471647

1648+
# Ensure that we can pass an np.array straight through to matplotlib,
1649+
# this functionality was accidentally removed previously.
1650+
# See https://github.com/pydata/pandas/issues/8852 for bug report
1651+
#
1652+
# Exercise colormap path and non-colormap path as they are independent
1653+
#
1654+
df = DataFrame({'A': [1, 2], 'B': [3, 4]})
1655+
red_rgba = [1.0, 0.0, 0.0, 1.0]
1656+
green_rgba = [0.0, 1.0, 0.0, 1.0]
1657+
rgba_array = np.array([red_rgba, green_rgba])
1658+
ax = df.plot(kind='scatter', x='A', y='B', c=rgba_array)
1659+
# expect the face colors of the points in the non-colormap path to be
1660+
# identical to the values we supplied, normally we'd be on shaky ground
1661+
# comparing floats for equality but here we expect them to be
1662+
# identical.
1663+
self.assertTrue(
1664+
np.array_equal(
1665+
ax.collections[0].get_facecolor(),
1666+
rgba_array))
1667+
# we don't test the colors of the faces in this next plot because they
1668+
# are dependent on the spring colormap, which may change its colors
1669+
# later.
1670+
float_array = np.array([0.0, 1.0])
1671+
df.plot(kind='scatter', x='A', y='B', c=float_array, cmap='spring')
1672+
16481673
@slow
16491674
def test_plot_bar(self):
16501675
df = DataFrame(randn(6, 4),

pandas/tools/plotting.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1403,16 +1403,18 @@ def _make_plot(self):
14031403
x, y, c, data = self.x, self.y, self.c, self.data
14041404
ax = self.axes[0]
14051405

1406+
c_is_column = com.is_hashable(c) and c in self.data.columns
1407+
14061408
# plot a colorbar only if a colormap is provided or necessary
1407-
cb = self.kwds.pop('colorbar', self.colormap or c in self.data.columns)
1409+
cb = self.kwds.pop('colorbar', self.colormap or c_is_column)
14081410

14091411
# pandas uses colormap, matplotlib uses cmap.
14101412
cmap = self.colormap or 'Greys'
14111413
cmap = plt.cm.get_cmap(cmap)
14121414

14131415
if c is None:
14141416
c_values = self.plt.rcParams['patch.facecolor']
1415-
elif c in self.data.columns:
1417+
elif c_is_column:
14161418
c_values = self.data[c].values
14171419
else:
14181420
c_values = c
@@ -1427,7 +1429,7 @@ def _make_plot(self):
14271429
img = ax.collections[0]
14281430
kws = dict(ax=ax)
14291431
if mpl_ge_1_3_1:
1430-
kws['label'] = c if c in self.data.columns else ''
1432+
kws['label'] = c if c_is_column else ''
14311433
self.fig.colorbar(img, **kws)
14321434

14331435
self._add_legend_handle(scatter, label)

0 commit comments

Comments
 (0)