Repository: pandas-dev/pandas
Pull request: COMPAT: matplotlib 3.4.0
- [ ] closes #40714
- [ ] tests added / passed
- [ ] Ensure all linting tests pass, see [here](https://pandas.pydata.org/pandas-docs/dev/development/contributing.html#code-standards) for how to run them
- [ ] whatsnew entry


Problem statement:
BUG: MatplotlibDeprecationWarning:  The is_first_col function was deprecated in Matplotlib 3.4
```python
import pandas as pd
from matplotlib import pyplot as plt

series = [pd.Series([1,2,3,4]) for _ in range(4)]

nrow, ncol = 2, 2
fig, axes = plt.subplots(nrow, ncol)
axes = axes.reshape(-1)
for i, s in enumerate(series):
    s.plot(ax=axes[i])

plt.tight_layout()
```

#### Problem description

After executing the previous code the following message from Matplotlib is shown:

```my_personal_folder/lib/python3.8/site-packages/pandas/plotting/_matplotlib/tools.py:400: MatplotlibDeprecationWarning: 
The is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.
  if ax.is_first_col():
```

Hints from issue discussion:
Hi @arnaujc91,
Thanks for the bug report. I can confirm that this happens with the latest matplotlib, and I've spotted some failing tests in our CI related to this. 
e.g. https://github.com/pandas-dev/pandas/pull/40713/checks?check_run_id=2240746140
```
______________ TestDataFramePlotsSubplots.test_subplots_warnings _______________
[gw1] linux -- Python 3.8.8 /home/runner/miniconda3/envs/pandas-dev/bin/python

self = <pandas.tests.plotting.frame.test_frame_subplots.TestDataFramePlotsSubplots object at 0x7f8d83a6edf0>

    def test_subplots_warnings(self):
        # GH 9464
        with tm.assert_produces_warning(None):
            df = DataFrame(np.random.randn(100, 4))
            df.plot(subplots=True, layout=(3, 2))
    
            df = DataFrame(
                np.random.randn(100, 4), index=date_range("1/1/2000", periods=100)
            )
>           df.plot(subplots=True, layout=(3, 2))

pandas/tests/plotting/frame/test_frame_subplots.py:264: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../../../miniconda3/envs/pandas-dev/lib/python3.8/contextlib.py:120: in __exit__
    next(self.gen)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

    def _assert_caught_no_extra_warnings(
        *,
        caught_warnings: Sequence[warnings.WarningMessage],
Warning: xpected_warning: Optional[Union[Type[Warning], bool]],
    ) -> None:
        """Assert that no extra warnings apart from the expected ones are caught."""
        extra_warnings = []
    
        for actual_warning in caught_warnings:
            if _is_unexpected_warning(actual_warning, expected_warning):
                extra_warnings.append(
                    (
                        actual_warning.category.__name__,
                        actual_warning.message,
                        actual_warning.filename,
                        actual_warning.lineno,
                    )
                )
    
        if extra_warnings:
>           raise AssertionError(f"Caused unexpected warning(s): {repr(extra_warnings)}")
E           AssertionError: Caused unexpected warning(s): [('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426)]

pandas/_testing/_warnings.py:155: AssertionError
______________________ TestDataFramePlots.test_pie_df_nan ______________________
[gw0] linux -- Python 3.8.8 /home/runner/miniconda3/envs/pandas-dev/bin/python

self = <pandas.tests.plotting.frame.test_frame.TestDataFramePlots object at 0x7f65e95a4fd0>

    def test_pie_df_nan(self):
        import matplotlib as mpl
    
        df = DataFrame(np.random.rand(4, 4))
        for i in range(4):
            df.iloc[i, i] = np.nan
        fig, axes = self.plt.subplots(ncols=4)
    
        # GH 37668
        kwargs = {}
        if mpl.__version__ >= "3.3":
            kwargs = {"normalize": True}
    
        with tm.assert_produces_warning(None):
>           df.plot.pie(subplots=True, ax=axes, legend=True, **kwargs)

pandas/tests/plotting/frame/test_frame.py:1523: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../../../miniconda3/envs/pandas-dev/lib/python3.8/contextlib.py:120: in __exit__
    next(self.gen)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

    def _assert_caught_no_extra_warnings(
        *,
        caught_warnings: Sequence[warnings.WarningMessage],
Warning: xpected_warning: Optional[Union[Type[Warning], bool]],
    ) -> None:
        """Assert that no extra warnings apart from the expected ones are caught."""
        extra_warnings = []
    
        for actual_warning in caught_warnings:
            if _is_unexpected_warning(actual_warning, expected_warning):
                extra_warnings.append(
                    (
                        actual_warning.category.__name__,
                        actual_warning.message,
                        actual_warning.filename,
                        actual_warning.lineno,
                    )
                )
    
        if extra_warnings:
>           raise AssertionError(f"Caused unexpected warning(s): {repr(extra_warnings)}")
E           AssertionError: Caused unexpected warning(s): [('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426), ('MatplotlibDeprecationWarning', MatplotlibDeprecationWarning('\nThe is_first_col function was deprecated in Matplotlib 3.4 and will be removed two minor releases later. Use ax.get_subplotspec().is_first_col() instead.'), '/home/runner/work/pandas/pandas/pandas/plotting/_matplotlib/tools.py', 426)]

pandas/_testing/_warnings.py:155: AssertionError
```
The culprit seems to be here.
https://github.com/pandas-dev/pandas/blob/d01561fb7b9ad337611fa38a3cfb7e8c2faec608/pandas/plotting/_matplotlib/tools.py#L426
Indeed, I just changed this line of code in my local version of Pandas and everything seems to work well. I will try to create a PR tomorrow when I have more time.

Thank you!

Commit message: lazify import

Patch to review:
diff --git a/pandas/plotting/_matplotlib/compat.py b/pandas/plotting/_matplotlib/compat.py
index 964596d9b6319..729d2bf1f019a 100644
--- a/pandas/plotting/_matplotlib/compat.py
+++ b/pandas/plotting/_matplotlib/compat.py
@@ -22,3 +22,4 @@ def inner():
 mpl_ge_3_1_0 = _mpl_version("3.1.0", operator.ge)
 mpl_ge_3_2_0 = _mpl_version("3.2.0", operator.ge)
 mpl_ge_3_3_0 = _mpl_version("3.3.0", operator.ge)
+mpl_ge_3_4_0 = _mpl_version("3.4.0", operator.ge)
diff --git a/pandas/plotting/_matplotlib/tools.py b/pandas/plotting/_matplotlib/tools.py
index 500d570835493..9e1f54e5e12bc 100644
--- a/pandas/plotting/_matplotlib/tools.py
+++ b/pandas/plotting/_matplotlib/tools.py
@@ -392,6 +392,11 @@ def handle_shared_axes(
             row_num = lambda x: x.rowNum
             col_num = lambda x: x.colNum
 
+        if compat.mpl_ge_3_4_0:
+            is_first_col = lambda x: x.get_subplotspec().is_first_col()
+        else:
+            is_first_col = lambda x: x.is_first_col()
+
         if nrows > 1:
             try:
                 # first find out the ax layout,
@@ -423,7 +428,7 @@ def handle_shared_axes(
                 # only the first column should get y labels -> set all other to
                 # off as we only have labels in the first column and we always
                 # have a subplot there, we can skip the layout test
-                if ax.is_first_col():
+                if is_first_col(ax):
                     continue
                 if sharey or _has_externally_shared_axis(ax, "y"):
                     _remove_labels_from_axis(ax.yaxis)
diff --git a/pandas/tests/plotting/frame/test_frame.py b/pandas/tests/plotting/frame/test_frame.py
index bed60be169e57..53f859cc4a3eb 100644
--- a/pandas/tests/plotting/frame/test_frame.py
+++ b/pandas/tests/plotting/frame/test_frame.py
@@ -746,7 +746,9 @@ def test_plot_scatter_with_categorical_data(self, x, y):
 
         _check_plot_works(df.plot.scatter, x=x, y=y)
 
-    def test_plot_scatter_with_c(self):
+    def test_plot_scatter_with_c(self, request):
+        from pandas.plotting._matplotlib.compat import mpl_ge_3_4_0
+
         df = DataFrame(
             np.random.randn(6, 4),
             index=list(string.ascii_letters[:6]),
@@ -760,6 +762,10 @@ def test_plot_scatter_with_c(self):
 
             # n.b. there appears to be no public method
             # to get the colorbar label
+            if mpl_ge_3_4_0:
+                mark = pytest.mark.xfail(reason="_label attribute removed")
+                request.node.add_marker(mark)
+
             assert ax.collections[0].colorbar._label == "z"
 
         cm = "cubehelix"


Code review comments:
diff_hunk: @@ -760,6 +762,10 @@ def test_plot_scatter_with_c(self):
 
             # n.b. there appears to be no public method
             # to get the colorbar label
+            if mpl_ge_3_4_0:
+                mark = pytest.mark.xfail(reason="_label attribute removed")
path: pandas/tests/plotting/frame/test_frame.py
text: @user1:
For recent versions, you can use `ax.collections[0].colorbar.ax.get_ylabel()` instead of the private attribute