From c68ac4f1fea8fe2690d3c5486515be461f215975 Mon Sep 17 00:00:00 2001 From: Eli Dourado Date: Tue, 5 Jul 2022 17:39:35 -0400 Subject: [PATCH 01/15] Make xticks from _quarterly_finder() line up better When I plot a series of quarterly data that spans more than 11 years, my vertical grid lines get placed a year before where they should, i.e., one year before a year that is divisible by the default annual spacing. Changing one line in the _quarterly_finder() function, from +1 to +1970, fixes this for me. Can anyone please confirm the issue and that this fixes it? --- pandas/plotting/_matplotlib/converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/plotting/_matplotlib/converter.py b/pandas/plotting/_matplotlib/converter.py index 31f014d25d67d..f1fbd02ea8e96 100644 --- a/pandas/plotting/_matplotlib/converter.py +++ b/pandas/plotting/_matplotlib/converter.py @@ -867,7 +867,7 @@ def _quarterly_finder(vmin, vmax, freq): info_fmt[year_start] = "%F" else: - years = dates_[year_start] // 4 + 1 + years = dates_[year_start] // 4 + 1970 nyears = span / periodsperyear (min_anndef, maj_anndef) = _get_default_annual_spacing(nyears) major_idx = year_start[(years % maj_anndef == 0)] From 53e33ebde3e100f6b9d2374280f611d0f20e54fb Mon Sep 17 00:00:00 2001 From: Eli Dourado Date: Wed, 6 Jul 2022 15:36:02 -0400 Subject: [PATCH 02/15] Create test for _quarterly_finder() --- .../tests/plotting/test_quarterly_finder.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 pandas/tests/plotting/test_quarterly_finder.py diff --git a/pandas/tests/plotting/test_quarterly_finder.py b/pandas/tests/plotting/test_quarterly_finder.py new file mode 100644 index 0000000000000..800efa5d19de8 --- /dev/null +++ b/pandas/tests/plotting/test_quarterly_finder.py @@ -0,0 +1,34 @@ +import pytest +import pandas as pd +import numpy as np +from pandas import Period +from pandas.plotting._matplotlib import converter + + +@pytest.mark.parametrize("year_span", [11.25, 30, 80, 150, 300, 584.5]) +# valid ranges are from 11.25 to 584.5 years, limited at the bottom by if statements in the _quarterly_finder() function and at the top end by the Timestamp format +def test_quarterly_finder(year_span): + # earliest start date given pd.Timestamp.min + start_date = pd.to_datetime('1677Q4') + daterange = pd.period_range( + start_date, periods=year_span * 4, freq='Q').asi8 + vmin = daterange[0] + vmax = daterange[-1] + span = vmax - vmin + 1 + if span < 45: # the quarterly finder is only invoked if the span is >= 45 + return + nyears = span / 4 + (min_anndef, maj_anndef) = converter._get_default_annual_spacing(nyears) + result = converter._quarterly_finder(vmin, vmax, 'Q') + quarters = pd.PeriodIndex(pd.arrays.PeriodArray( + np.array([x[0] for x in result]), freq='Q')) + majors = np.array([x[1] for x in result]) + minors = np.array([x[2] for x in result]) + major_quarters = quarters[majors] + minor_quarters = quarters[minors] + check_major_years = major_quarters.year % maj_anndef == 0 + check_minor_years = minor_quarters.year % min_anndef == 0 + check_major_quarters = major_quarters.quarter == 1 + check_minor_quarters = minor_quarters.quarter == 1 + assert (np.all(check_major_years) and np.all(check_minor_years) + and np.all(check_major_quarters) and np.all(check_minor_quarters)) From df240a5afecf5c5cbb18d6aaf1ce24f40771b95b Mon Sep 17 00:00:00 2001 From: Eli Dourado Date: Wed, 6 Jul 2022 15:37:47 -0400 Subject: [PATCH 03/15] Shorten comment --- pandas/tests/plotting/test_quarterly_finder.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pandas/tests/plotting/test_quarterly_finder.py b/pandas/tests/plotting/test_quarterly_finder.py index 800efa5d19de8..bb88848d94be3 100644 --- a/pandas/tests/plotting/test_quarterly_finder.py +++ b/pandas/tests/plotting/test_quarterly_finder.py @@ -6,7 +6,9 @@ @pytest.mark.parametrize("year_span", [11.25, 30, 80, 150, 300, 584.5]) -# valid ranges are from 11.25 to 584.5 years, limited at the bottom by if statements in the _quarterly_finder() function and at the top end by the Timestamp format +# valid ranges are from 11.25 to 584.5 years, limited at the bottom by +# if statements in the _quarterly_finder() function and at the top end +# by the Timestamp format def test_quarterly_finder(year_span): # earliest start date given pd.Timestamp.min start_date = pd.to_datetime('1677Q4') From 566edbba527d558947adc68c74b73e1c14f9efa0 Mon Sep 17 00:00:00 2001 From: Eli Dourado Date: Wed, 6 Jul 2022 16:17:42 -0400 Subject: [PATCH 04/15] Style refinements --- .../tests/plotting/test_quarterly_finder.py | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/pandas/tests/plotting/test_quarterly_finder.py b/pandas/tests/plotting/test_quarterly_finder.py index bb88848d94be3..c190db8927e9e 100644 --- a/pandas/tests/plotting/test_quarterly_finder.py +++ b/pandas/tests/plotting/test_quarterly_finder.py @@ -1,7 +1,6 @@ +import numpy as np import pytest import pandas as pd -import numpy as np -from pandas import Period from pandas.plotting._matplotlib import converter @@ -11,9 +10,9 @@ # by the Timestamp format def test_quarterly_finder(year_span): # earliest start date given pd.Timestamp.min - start_date = pd.to_datetime('1677Q4') + start_date = pd.to_datetime("1677Q4") daterange = pd.period_range( - start_date, periods=year_span * 4, freq='Q').asi8 + start_date, periods=year_span * 4, freq="Q").asi8 vmin = daterange[0] vmax = daterange[-1] span = vmax - vmin + 1 @@ -21,9 +20,10 @@ def test_quarterly_finder(year_span): return nyears = span / 4 (min_anndef, maj_anndef) = converter._get_default_annual_spacing(nyears) - result = converter._quarterly_finder(vmin, vmax, 'Q') - quarters = pd.PeriodIndex(pd.arrays.PeriodArray( - np.array([x[0] for x in result]), freq='Q')) + result = converter._quarterly_finder(vmin, vmax, "Q") + quarters = pd.PeriodIndex( + pd.arrays.PeriodArray(np.array([x[0] for x in result]), freq="Q") + ) majors = np.array([x[1] for x in result]) minors = np.array([x[2] for x in result]) major_quarters = quarters[majors] @@ -32,5 +32,9 @@ def test_quarterly_finder(year_span): check_minor_years = minor_quarters.year % min_anndef == 0 check_major_quarters = major_quarters.quarter == 1 check_minor_quarters = minor_quarters.quarter == 1 - assert (np.all(check_major_years) and np.all(check_minor_years) - and np.all(check_major_quarters) and np.all(check_minor_quarters)) + assert ( + np.all(check_major_years) + and np.all(check_minor_years) + and np.all(check_major_quarters) + and np.all(check_minor_quarters) + ) From db3595781f078825ea385677e71ca021c6ed3f26 Mon Sep 17 00:00:00 2001 From: Eli Dourado Date: Wed, 6 Jul 2022 17:05:06 -0400 Subject: [PATCH 05/15] Style --- pandas/tests/plotting/test_quarterly_finder.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pandas/tests/plotting/test_quarterly_finder.py b/pandas/tests/plotting/test_quarterly_finder.py index c190db8927e9e..6be763bc4666e 100644 --- a/pandas/tests/plotting/test_quarterly_finder.py +++ b/pandas/tests/plotting/test_quarterly_finder.py @@ -11,8 +11,7 @@ def test_quarterly_finder(year_span): # earliest start date given pd.Timestamp.min start_date = pd.to_datetime("1677Q4") - daterange = pd.period_range( - start_date, periods=year_span * 4, freq="Q").asi8 + daterange = pd.period_range(start_date, periods=year_span * 4, freq="Q").asi8 vmin = daterange[0] vmax = daterange[-1] span = vmax - vmin + 1 From 70dbb7178b4bc840fc4963fd209b03bfe74494d2 Mon Sep 17 00:00:00 2001 From: Eli Dourado Date: Wed, 6 Jul 2022 17:57:57 -0400 Subject: [PATCH 06/15] Sort imports --- pandas/tests/plotting/test_quarterly_finder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/plotting/test_quarterly_finder.py b/pandas/tests/plotting/test_quarterly_finder.py index 6be763bc4666e..d49dbd0609d77 100644 --- a/pandas/tests/plotting/test_quarterly_finder.py +++ b/pandas/tests/plotting/test_quarterly_finder.py @@ -1,6 +1,6 @@ import numpy as np -import pytest import pandas as pd +import pytest from pandas.plotting._matplotlib import converter From 9640e3c76dd952e7a0c9ee3d8144502c91761533 Mon Sep 17 00:00:00 2001 From: Eli Dourado Date: Mon, 11 Jul 2022 17:48:58 -0400 Subject: [PATCH 07/15] Simplify test and move to test_converter.py file --- pandas/tests/plotting/test_converter.py | 32 +++++++++++++++ .../tests/plotting/test_quarterly_finder.py | 39 ------------------- 2 files changed, 32 insertions(+), 39 deletions(-) delete mode 100644 pandas/tests/plotting/test_quarterly_finder.py diff --git a/pandas/tests/plotting/test_converter.py b/pandas/tests/plotting/test_converter.py index 656969bfad703..d8dd4c484a8de 100644 --- a/pandas/tests/plotting/test_converter.py +++ b/pandas/tests/plotting/test_converter.py @@ -15,8 +15,10 @@ from pandas import ( Index, Period, + PeriodIndex, Series, Timestamp, + arrays, date_range, ) import pandas._testing as tm @@ -375,3 +377,33 @@ def get_view_interval(self): tdc = converter.TimeSeries_TimedeltaFormatter() monkeypatch.setattr(tdc, "axis", mock_axis()) tdc(0.0, 0) + +@pytest.mark.parametrize("year_span", [11.25, 30, 80, 150, 400, 800, 1500, 2500, 3500]) +# The range is limited to 11.25 at the bottom by if statements in +# the _quarterly_finder() function +def test_quarterly_finder(year_span): + vmin = -1000 + vmax = vmin + year_span * 4 + span = vmax - vmin + 1 + if span < 45: # the quarterly finder is only invoked if the span is >= 45 + return + nyears = span / 4 + (min_anndef, maj_anndef) = converter._get_default_annual_spacing(nyears) + result = converter._quarterly_finder(vmin, vmax, "Q") + quarters = PeriodIndex( + arrays.PeriodArray(np.array([x[0] for x in result]), freq="Q") + ) + majors = np.array([x[1] for x in result]) + minors = np.array([x[2] for x in result]) + major_quarters = quarters[majors] + minor_quarters = quarters[minors] + check_major_years = major_quarters.year % maj_anndef == 0 + check_minor_years = minor_quarters.year % min_anndef == 0 + check_major_quarters = major_quarters.quarter == 1 + check_minor_quarters = minor_quarters.quarter == 1 + assert ( + np.all(check_major_years) + and np.all(check_minor_years) + and np.all(check_major_quarters) + and np.all(check_minor_quarters) + ) \ No newline at end of file diff --git a/pandas/tests/plotting/test_quarterly_finder.py b/pandas/tests/plotting/test_quarterly_finder.py deleted file mode 100644 index d49dbd0609d77..0000000000000 --- a/pandas/tests/plotting/test_quarterly_finder.py +++ /dev/null @@ -1,39 +0,0 @@ -import numpy as np -import pandas as pd -import pytest -from pandas.plotting._matplotlib import converter - - -@pytest.mark.parametrize("year_span", [11.25, 30, 80, 150, 300, 584.5]) -# valid ranges are from 11.25 to 584.5 years, limited at the bottom by -# if statements in the _quarterly_finder() function and at the top end -# by the Timestamp format -def test_quarterly_finder(year_span): - # earliest start date given pd.Timestamp.min - start_date = pd.to_datetime("1677Q4") - daterange = pd.period_range(start_date, periods=year_span * 4, freq="Q").asi8 - vmin = daterange[0] - vmax = daterange[-1] - span = vmax - vmin + 1 - if span < 45: # the quarterly finder is only invoked if the span is >= 45 - return - nyears = span / 4 - (min_anndef, maj_anndef) = converter._get_default_annual_spacing(nyears) - result = converter._quarterly_finder(vmin, vmax, "Q") - quarters = pd.PeriodIndex( - pd.arrays.PeriodArray(np.array([x[0] for x in result]), freq="Q") - ) - majors = np.array([x[1] for x in result]) - minors = np.array([x[2] for x in result]) - major_quarters = quarters[majors] - minor_quarters = quarters[minors] - check_major_years = major_quarters.year % maj_anndef == 0 - check_minor_years = minor_quarters.year % min_anndef == 0 - check_major_quarters = major_quarters.quarter == 1 - check_minor_quarters = minor_quarters.quarter == 1 - assert ( - np.all(check_major_years) - and np.all(check_minor_years) - and np.all(check_major_quarters) - and np.all(check_minor_quarters) - ) From 15565077bffd951194349182eee6df945ee03250 Mon Sep 17 00:00:00 2001 From: Eli Dourado Date: Mon, 11 Jul 2022 17:56:33 -0400 Subject: [PATCH 08/15] Newline at end of file --- pandas/tests/plotting/test_converter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/tests/plotting/test_converter.py b/pandas/tests/plotting/test_converter.py index d8dd4c484a8de..2544454412d09 100644 --- a/pandas/tests/plotting/test_converter.py +++ b/pandas/tests/plotting/test_converter.py @@ -406,4 +406,5 @@ def test_quarterly_finder(year_span): and np.all(check_minor_years) and np.all(check_major_quarters) and np.all(check_minor_quarters) - ) \ No newline at end of file + ) + \ No newline at end of file From fcbc9b30ff268707015b9bbe034fcdf119ec2d36 Mon Sep 17 00:00:00 2001 From: Eli Dourado Date: Thu, 14 Jul 2022 16:51:35 -0400 Subject: [PATCH 09/15] Make 4 individual asserts --- pandas/tests/plotting/test_converter.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pandas/tests/plotting/test_converter.py b/pandas/tests/plotting/test_converter.py index 2544454412d09..818db5fe216a5 100644 --- a/pandas/tests/plotting/test_converter.py +++ b/pandas/tests/plotting/test_converter.py @@ -401,10 +401,8 @@ def test_quarterly_finder(year_span): check_minor_years = minor_quarters.year % min_anndef == 0 check_major_quarters = major_quarters.quarter == 1 check_minor_quarters = minor_quarters.quarter == 1 - assert ( - np.all(check_major_years) - and np.all(check_minor_years) - and np.all(check_major_quarters) - and np.all(check_minor_quarters) - ) + assert np.all(check_major_years) + assert np.all(check_minor_years) + assert np.all(check_major_quarters) + assert np.all(check_minor_quarters) \ No newline at end of file From dc4bec842cad96970c23c0fb5de4d848f5be2b70 Mon Sep 17 00:00:00 2001 From: Eli Dourado Date: Thu, 14 Jul 2022 16:51:52 -0400 Subject: [PATCH 10/15] Add Github discussion link as comment --- pandas/plotting/_matplotlib/converter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/plotting/_matplotlib/converter.py b/pandas/plotting/_matplotlib/converter.py index f1fbd02ea8e96..873084393371c 100644 --- a/pandas/plotting/_matplotlib/converter.py +++ b/pandas/plotting/_matplotlib/converter.py @@ -867,6 +867,7 @@ def _quarterly_finder(vmin, vmax, freq): info_fmt[year_start] = "%F" else: + # https://github.com/pandas-dev/pandas/pull/47602 years = dates_[year_start] // 4 + 1970 nyears = span / periodsperyear (min_anndef, maj_anndef) = _get_default_annual_spacing(nyears) From eeb696c3252f62bf076a9a53603ac0816e45367f Mon Sep 17 00:00:00 2001 From: Eli Dourado Date: Fri, 15 Jul 2022 10:24:46 -0400 Subject: [PATCH 11/15] Remove newline from end of file --- pandas/tests/plotting/test_converter.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pandas/tests/plotting/test_converter.py b/pandas/tests/plotting/test_converter.py index 818db5fe216a5..ed9662a68655f 100644 --- a/pandas/tests/plotting/test_converter.py +++ b/pandas/tests/plotting/test_converter.py @@ -404,5 +404,4 @@ def test_quarterly_finder(year_span): assert np.all(check_major_years) assert np.all(check_minor_years) assert np.all(check_major_quarters) - assert np.all(check_minor_quarters) - \ No newline at end of file + assert np.all(check_minor_quarters) \ No newline at end of file From 3bebe4c1b610de7806c398614e23dc60ff72e246 Mon Sep 17 00:00:00 2001 From: Eli Dourado Date: Fri, 15 Jul 2022 10:57:37 -0400 Subject: [PATCH 12/15] Replace newline at end of file (minus tab) --- pandas/tests/plotting/test_converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/plotting/test_converter.py b/pandas/tests/plotting/test_converter.py index ed9662a68655f..fda582fbfd9b6 100644 --- a/pandas/tests/plotting/test_converter.py +++ b/pandas/tests/plotting/test_converter.py @@ -404,4 +404,4 @@ def test_quarterly_finder(year_span): assert np.all(check_major_years) assert np.all(check_minor_years) assert np.all(check_major_quarters) - assert np.all(check_minor_quarters) \ No newline at end of file + assert np.all(check_minor_quarters) From aa7aa40ee44306625b369ed0092c069e9b7e51fe Mon Sep 17 00:00:00 2001 From: Eli Dourado Date: Fri, 15 Jul 2022 11:27:18 -0400 Subject: [PATCH 13/15] Add one line of white space --- pandas/tests/plotting/test_converter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/tests/plotting/test_converter.py b/pandas/tests/plotting/test_converter.py index fda582fbfd9b6..3ec8f4bd71c2b 100644 --- a/pandas/tests/plotting/test_converter.py +++ b/pandas/tests/plotting/test_converter.py @@ -378,6 +378,7 @@ def get_view_interval(self): monkeypatch.setattr(tdc, "axis", mock_axis()) tdc(0.0, 0) + @pytest.mark.parametrize("year_span", [11.25, 30, 80, 150, 400, 800, 1500, 2500, 3500]) # The range is limited to 11.25 at the bottom by if statements in # the _quarterly_finder() function From 895f69ee23cba12e628f8f76d4f85af908123622 Mon Sep 17 00:00:00 2001 From: Eli Dourado Date: Fri, 15 Jul 2022 13:22:26 -0400 Subject: [PATCH 14/15] Added release note in bug fixes/visualization section --- doc/source/whatsnew/v1.5.0.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst index 0b450fab53137..8f42b6681c39b 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -1034,6 +1034,10 @@ Metadata - Fixed metadata propagation in :meth:`DataFrame.explode` (:issue:`28283`) - +Visualization +^^^^^^^^^^^^^ +- Bug in :meth:`~pandas/plotting/_matplotlib/converter/_quarterly_finder` that led to xticks and vertical grids being improperly placed when plotting a quarterly series (:issue:`47602`) + Other ^^^^^ From d5a5ffb008a9e61e0d7ca6db4b42d59f03a51b8c Mon Sep 17 00:00:00 2001 From: Eli Dourado Date: Fri, 15 Jul 2022 13:28:25 -0400 Subject: [PATCH 15/15] Moved release note to different section and noted public method --- doc/source/whatsnew/v1.5.0.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst index 8f42b6681c39b..746d58fb6b437 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -970,6 +970,7 @@ Plotting - Bug in :meth:`DataFrame.plot.scatter` that prevented specifying ``norm`` (:issue:`45809`) - The function :meth:`DataFrame.plot.scatter` now accepts ``color`` as an alias for ``c`` and ``size`` as an alias for ``s`` for consistency to other plotting functions (:issue:`44670`) - Fix showing "None" as ylabel in :meth:`Series.plot` when not setting ylabel (:issue:`46129`) +- Bug in :meth:`DataFrame.plot` that led to xticks and vertical grids being improperly placed when plotting a quarterly series (:issue:`47602`) Groupby/resample/rolling ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1034,10 +1035,6 @@ Metadata - Fixed metadata propagation in :meth:`DataFrame.explode` (:issue:`28283`) - -Visualization -^^^^^^^^^^^^^ -- Bug in :meth:`~pandas/plotting/_matplotlib/converter/_quarterly_finder` that led to xticks and vertical grids being improperly placed when plotting a quarterly series (:issue:`47602`) - Other ^^^^^