From e731cc0f733b8631a4abe6d4e29be9143dc7d066 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Sun, 16 Apr 2017 21:30:42 -0500 Subject: [PATCH 1/3] BUG: Handle iterable of arrays in convert DatetimeConverter.convert can take an array or iterable of arrays. Fixed the converter to detect which case we're in and then re-use the existing logic. --- doc/source/whatsnew/v0.20.0.txt | 1 + pandas/core/dtypes/inference.py | 46 ++++++++++++++++++++++ pandas/plotting/_converter.py | 25 +++++++++++- pandas/tests/dtypes/test_inference.py | 22 +++++++++++ pandas/tests/plotting/test_converter.py | 13 ++++++ pandas/tests/plotting/test_datetimelike.py | 8 ++++ 6 files changed, 113 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v0.20.0.txt b/doc/source/whatsnew/v0.20.0.txt index de33b7d4e3371..5129b70ad3358 100644 --- a/doc/source/whatsnew/v0.20.0.txt +++ b/doc/source/whatsnew/v0.20.0.txt @@ -1565,6 +1565,7 @@ Plotting - Bug in ``DataFrame.hist`` where ``plt.tight_layout`` caused an ``AttributeError`` (use ``matplotlib >= 2.0.1``) (:issue:`9351`) - Bug in ``DataFrame.boxplot`` where ``fontsize`` was not applied to the tick labels on both axes (:issue:`15108`) +- Bug in the date and time converters pandas registers with matplotlib not handling multiple dimensions (:issue:`16026`) - Bug in ``pd.scatter_matrix()`` could accept either ``color`` or ``c``, but not both (:issue:`14855`) Groupby/Resample/Rolling diff --git a/pandas/core/dtypes/inference.py b/pandas/core/dtypes/inference.py index b0a93d24228af..b8f7c9f2463ba 100644 --- a/pandas/core/dtypes/inference.py +++ b/pandas/core/dtypes/inference.py @@ -273,6 +273,52 @@ def is_list_like(obj): not isinstance(obj, string_and_binary_types)) +def is_nested_list_like(obj): + """ + Check if the object is list-like, and that all of its elements + are also list-like. + + .. versionadded:: 0.20.0 + + Parameters + ---------- + obj : The object to check. + + Returns + ------- + is_list_like : bool + Whether `obj` has list-like properties. + + Examples + -------- + >>> is_list_like([1, 2, 3]) + True + >>> is_list_like({1, 2, 3}) + True + >>> is_list_like(datetime(2017, 1, 1)) + False + >>> is_list_like("foo") + False + >>> is_list_like(1) + False + >>> is_list_like([]) + False + + Notes + ----- + This won't reliably detect whether a consumable iterator (e. g. + a generator) is a nested-list-like without consuming the iterator. + To avoid consuming it, we always return False if the outer container + doesn't define `__len__`. + + See Also + -------- + is_list_like + """ + return (is_list_like(obj) and hasattr(obj, '__len__') and + len(obj) and all(is_list_like(item) for item in obj)) + + def is_dict_like(obj): """ Check if the object is dict-like. diff --git a/pandas/plotting/_converter.py b/pandas/plotting/_converter.py index 0e51e95057be2..71abd5088c12c 100644 --- a/pandas/plotting/_converter.py +++ b/pandas/plotting/_converter.py @@ -10,13 +10,15 @@ from matplotlib.ticker import Formatter, AutoLocator, Locator from matplotlib.transforms import nonsingular - from pandas.core.dtypes.common import ( is_float, is_integer, is_integer_dtype, is_float_dtype, is_datetime64_ns_dtype, - is_period_arraylike) + is_period_arraylike, + is_list_like, + is_nested_list_like +) from pandas.compat import lrange import pandas.compat as compat @@ -127,6 +129,15 @@ class PeriodConverter(dates.DateConverter): @staticmethod def convert(values, units, axis): + if is_nested_list_like(values): + values = [PeriodConverter._convert_1d(v, units, axis) + for v in values] + else: + values = PeriodConverter._convert_1d(values, units, axis) + return values + + @staticmethod + def _convert_1d(values, units, axis): if not hasattr(axis, 'freq'): raise TypeError('Axis must have `freq` set to convert to Periods') valid_types = (compat.string_types, datetime, @@ -178,6 +189,16 @@ class DatetimeConverter(dates.DateConverter): @staticmethod def convert(values, unit, axis): + # values might be a 1-d array, or a list-like of arrays. + if is_nested_list_like(values): + values = [DatetimeConverter._convert_1d(v, unit, axis) + for v in values] + else: + values = DatetimeConverter._convert_1d(values, unit, axis) + return values + + @staticmethod + def _convert_1d(values, unit, axis): def try_parse(values): try: return _dt_to_float_ordinal(tools.to_datetime(values)) diff --git a/pandas/tests/dtypes/test_inference.py b/pandas/tests/dtypes/test_inference.py index 94d1d21d59d88..dd8f65a8e48ff 100644 --- a/pandas/tests/dtypes/test_inference.py +++ b/pandas/tests/dtypes/test_inference.py @@ -11,6 +11,7 @@ from datetime import datetime, date, timedelta, time import numpy as np import pytz +import pytest import pandas as pd from pandas._libs import tslib, lib @@ -66,6 +67,27 @@ def test_is_list_like(): assert not inference.is_list_like(f) +@pytest.mark.parametrize('inner', [ + [], [1], (1, ), (1, 2), {'a': 1}, set([1, 'a']), Series([1]), + Series([]), Series(['a']).str, (x for x in range(5)) +]) +@pytest.mark.parametrize('outer', [ + list, Series, np.array, tuple +]) +def test_is_nested_list_like_passes(inner, outer): + result = outer([inner for _ in range(5)]) + assert inference.is_list_like(result) + + +@pytest.mark.parametrize('obj', [ + 'abc', [], [1], (1,), ['a'], 'a', {'a'}, + [1, 2, 3], Series([1]), DataFrame({"A": [1]}), + ([1, 2] for _ in range(5)), +]) +def test_is_nested_list_like_fails(obj): + assert not inference.is_nested_list_like(obj) + + def test_is_dict_like(): passes = [{}, {'A': 1}, Series([1])] fails = ['1', 1, [1, 2], (1, 2), range(2), Index([1])] diff --git a/pandas/tests/plotting/test_converter.py b/pandas/tests/plotting/test_converter.py index 683f4ee89687f..44cdfe33782be 100644 --- a/pandas/tests/plotting/test_converter.py +++ b/pandas/tests/plotting/test_converter.py @@ -138,6 +138,13 @@ def _assert_less(ts1, ts2): _assert_less(ts, ts + Milli()) _assert_less(ts, ts + Micro(50)) + def test_convert_nested(self): + inner = [Timestamp('2017-01-01', Timestamp('2017-01-02'))] + data = [inner, inner] + result = self.dtc.convert(data, None, None) + expected = [self.dtc.convert(x, None, None) for x in data] + assert result == expected + class TestPeriodConverter(tm.TestCase): @@ -196,3 +203,9 @@ def test_integer_passthrough(self): rs = self.pc.convert([0, 1], None, self.axis) xp = [0, 1] self.assertEqual(rs, xp) + + def test_convert_nested(self): + data = ['2012-1-1', '2012-1-2'] + r1 = self.pc.convert(data, None, self.axis) + r2 = [self.pc.convert(x, None, self.axis) for x in data] + assert r1 == r2 diff --git a/pandas/tests/plotting/test_datetimelike.py b/pandas/tests/plotting/test_datetimelike.py index 547770ebcf6e5..4beb804acacc5 100644 --- a/pandas/tests/plotting/test_datetimelike.py +++ b/pandas/tests/plotting/test_datetimelike.py @@ -1334,6 +1334,14 @@ def test_timedelta_plot(self): s = Series(np.random.randn(len(index)), index) _check_plot_works(s.plot) + def test_hist(self): + # https://github.com/matplotlib/matplotlib/issues/8459 + rng = date_range('1/1/2011', periods=10, freq='H') + x = rng + w1 = np.arange(0, 1, .1) + w2 = np.arange(0, 1, .1)[::-1] + self.plt.hist([x, x], weights=[w1, w2]) + def _check_plot_works(f, freq=None, series=None, *args, **kwargs): import matplotlib.pyplot as plt From d0b045358a0390da6ddea0fa98b8d5082edfc636 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Mon, 17 Apr 2017 06:55:41 -0500 Subject: [PATCH 2/3] PEP8: remove unused is_list_like --- pandas/plotting/_converter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pandas/plotting/_converter.py b/pandas/plotting/_converter.py index 71abd5088c12c..9621ee3d0cad4 100644 --- a/pandas/plotting/_converter.py +++ b/pandas/plotting/_converter.py @@ -16,7 +16,6 @@ is_float_dtype, is_datetime64_ns_dtype, is_period_arraylike, - is_list_like, is_nested_list_like ) From a786627060b13cbfccbae0b305c669cb396b312d Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Mon, 17 Apr 2017 07:30:41 -0500 Subject: [PATCH 3/3] Fix Joris comments --- pandas/core/dtypes/inference.py | 14 ++++++-------- pandas/tests/plotting/test_converter.py | 4 ++-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/pandas/core/dtypes/inference.py b/pandas/core/dtypes/inference.py index b8f7c9f2463ba..66f4d87aa8e33 100644 --- a/pandas/core/dtypes/inference.py +++ b/pandas/core/dtypes/inference.py @@ -291,17 +291,15 @@ def is_nested_list_like(obj): Examples -------- - >>> is_list_like([1, 2, 3]) + >>> is_nested_list_like([[1, 2, 3]]) True - >>> is_list_like({1, 2, 3}) + >>> is_nested_list_like([{1, 2, 3}, {1, 2, 3}]) True - >>> is_list_like(datetime(2017, 1, 1)) + >>> is_nested_list_like(["foo"]) False - >>> is_list_like("foo") - False - >>> is_list_like(1) + >>> is_nested_list_like([]) False - >>> is_list_like([]) + >>> is_nested_list_like([[1, 2, 3], 1]) False Notes @@ -316,7 +314,7 @@ def is_nested_list_like(obj): is_list_like """ return (is_list_like(obj) and hasattr(obj, '__len__') and - len(obj) and all(is_list_like(item) for item in obj)) + len(obj) > 0 and all(is_list_like(item) for item in obj)) def is_dict_like(obj): diff --git a/pandas/tests/plotting/test_converter.py b/pandas/tests/plotting/test_converter.py index 44cdfe33782be..30eb3ef24fe30 100644 --- a/pandas/tests/plotting/test_converter.py +++ b/pandas/tests/plotting/test_converter.py @@ -206,6 +206,6 @@ def test_integer_passthrough(self): def test_convert_nested(self): data = ['2012-1-1', '2012-1-2'] - r1 = self.pc.convert(data, None, self.axis) - r2 = [self.pc.convert(x, None, self.axis) for x in data] + r1 = self.pc.convert([data, data], None, self.axis) + r2 = [self.pc.convert(data, None, self.axis) for _ in range(2)] assert r1 == r2