[READY] Improved performance of `Period`'s default formatter (`period_format`)
State: closed

`Period`'s default formatter (`period_format`) is now significantly (~twice) faster. 

- This improves performance of `str(Period)`, `repr(Period)`, and `Period.strftime(fmt=None)`, 
- as well as `PeriodArray.strftime(fmt=None)`, `PeriodIndex.strftime(fmt=None)` and `PeriodIndex.format(fmt=None)`. 
- Finally, `to_csv` operations involving `PeriodArray` or `PeriodIndex` with default `date_format` are also significantly accelerated.

ASVs:

```
> asv compare d82f9ddb c1bbfd6c
       before           after         ratio
     [d82f9ddb]       [c1bbfd6c]
-     3.12±0.05μs         998±40ns     0.32  tslibs.period.PeriodUnaryMethods.time_repr('M')
-     3.31±0.05μs       1.58±0.1μs     0.48  tslibs.period.PeriodUnaryMethods.time_repr('min')
-     3.15±0.09μs      1.05±0.07μs     0.33  tslibs.period.PeriodUnaryMethods.time_str('M')
-     3.20±0.05μs       1.75±0.1μs     0.55  tslibs.period.PeriodUnaryMethods.time_str('min')
-      2.90±0.1μs         850±40ns     0.29  tslibs.period.PeriodUnaryMethods.time_strftime_default('M')
-     3.01±0.01μs       1.48±0.1μs     0.49  tslibs.period.PeriodUnaryMethods.time_strftime_default('min')
-     4.16±0.05ms      2.37±0.08ms     0.57  strftime.PeriodStrftime.time_frame_period_formatting_default(1000, 'D')
-     4.35±0.06ms       2.63±0.1ms     0.60  strftime.PeriodStrftime.time_frame_period_formatting_default(1000, 'H')
-      40.3±0.6ms       21.8±0.2ms     0.54  strftime.PeriodStrftime.time_frame_period_formatting_default(10000, 'D')
-      43.5±0.2ms         26.3±1ms     0.60  strftime.PeriodStrftime.time_frame_period_formatting_default(10000, 'H')
      4.58±0.08ms      2.88±0.07ms    ~0.63  strftime.PeriodStrftime.time_frame_period_formatting_index_default(1000, 'D')
       4.73±0.2ms      3.02±0.05ms    ~0.64  strftime.PeriodStrftime.time_frame_period_formatting_index_default(1000, 'H')
-      42.8±0.3ms       24.0±0.5ms     0.56  strftime.PeriodStrftime.time_frame_period_formatting_index_default(10000, 'D')
-      43.2±0.3ms       27.3±0.3ms     0.63  strftime.PeriodStrftime.time_frame_period_formatting_index_default(10000, 'H')
-     4.16±0.05ms       2.41±0.3ms     0.58  strftime.PeriodStrftime.time_frame_period_to_str(1000, 'D')
-     4.28±0.03ms       2.71±0.1ms     0.63  strftime.PeriodStrftime.time_frame_period_to_str(1000, 'H')
-      41.6±0.7ms       22.7±0.2ms     0.55  strftime.PeriodStrftime.time_frame_period_to_str(10000, 'D')
-      42.9±0.6ms       26.0±0.5ms     0.61  strftime.PeriodStrftime.time_frame_period_to_str(10000, 'H')
```
EDIT: additional for `to_csv`
```
-     5.94±0.05ms       3.94±0.3ms     0.66  io.csv.ToCSVPeriod.time_frame_period_formatting(1000, 'D') 
-      6.39±0.1ms      4.37±0.04ms     0.68  io.csv.ToCSVPeriod.time_frame_period_formatting(1000, 'H') 
-      51.8±0.2ms       30.2±0.3ms     0.58  io.csv.ToCSVPeriod.time_frame_period_formatting(10000, 'D')
-       59.8±20ms       35.0±0.2ms     0.59  io.csv.ToCSVPeriod.time_frame_period_formatting(10000, 'H')
-      7.09±0.2ms       3.94±0.2ms     0.56  io.csv.ToCSVPeriod.time_frame_period_no_format(1000, 'D')  
-      6.72±0.1ms      4.30±0.03ms     0.64  io.csv.ToCSVPeriod.time_frame_period_no_format(1000, 'H')  
-        54.0±2ms       30.5±0.3ms     0.56  io.csv.ToCSVPeriod.time_frame_period_no_format(10000, 'D') 
-        54.4±1ms       35.3±0.5ms     0.65  io.csv.ToCSVPeriod.time_frame_period_no_format(10000, 'H')
       9.94±0.3ms       9.70±0.1ms     0.98  io.csv.ToCSVPeriodIndex.time_frame_period_formatting_index(1000, 'D')
      9.69±0.07ms       9.84±0.7ms     1.02  io.csv.ToCSVPeriodIndex.time_frame_period_formatting_index(1000, 'H')
       85.4±0.9ms       84.9±0.9ms     0.99  io.csv.ToCSVPeriodIndex.time_frame_period_formatting_index(10000, 'D')
         89.3±2ms         87.3±3ms     0.98  io.csv.ToCSVPeriodIndex.time_frame_period_formatting_index(10000, 'H')
-      6.32±0.3ms      3.78±0.03ms     0.60  io.csv.ToCSVPeriodIndex.time_frame_period_no_format_index(1000, 'D')
-      6.30±0.2ms       4.60±0.4ms     0.73  io.csv.ToCSVPeriodIndex.time_frame_period_no_format_index(1000, 'H')
         50.9±2ms         33.9±8ms    ~0.66  io.csv.ToCSVPeriodIndex.time_frame_period_no_format_index(10000, 'D')
         52.6±1ms         35.6±6ms    ~0.68  io.csv.ToCSVPeriodIndex.time_frame_period_no_format_index(10000, 'H')
```

- [x] closes one part of #44764 
- [x] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [x] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.


Labels:
name: Performance
description: Memory or execution speed performance
name: Output-Formatting
description: __repr__ of pandas objects, to_string
name: Stale

Comments:
It seems that the failing tests are not related to this PR's contents, do you confirm ?
I added a few asvs for the to_csv operations and updated the what's new. This is now ready for review @MarcoGorelli 

~~Note that I do not understand why `ToCSVPeriod.time_frame_period_formatting` is accelerated too, since it has a custom date_format. Maybe there is some default period formatting operation happening as an intermediate step ? To investigate... in another PR maybe~~

I found the reason and added a docstring on that bench function. The `date_format` argument is not used by `to_csv` when the `PeriodArray` is not the index of the dataframe, so the formatting uses the default format, and therefore it is impacted by the acceleration.
Note: the failing test seeems to be related to a network issue, so completely unrelated. 

> FAILED pandas/tests/io/parser/common/test_file_buffer_url.py::test_file_descriptor_leak[c_high] - AssertionError: ([], [pconn(fd=14, family=<AddressFamily.AF_INET6: 10>, type=<SocketKind.SOCK_STREAM: 1>, laddr=addr(ip='::1', port=35970), raddr=addr(ip='::1', port=3306), status='ESTABLISHED')])

@mroeschke since you have reviewed my similar PRs for strftime in the past, do not hesitate to jump in too.

Note that once this PR will be merged, https://github.com/pandas-dev/pandas/pull/51298/ will be finally small enough to be revewed

Thanks @MarcoGorelli !

> Regarding date_format only taking effect for Period if it's in the index - that looks like a bug to me, well done for noticing! I don't see any issue open about this, do you want to open one? (else I can, no worries)

Done #51621

Thanks @MarcoGorelli . Any reason this is for 2.1 and not for 2.0 ? If so I need to move the what's new lines  :)

thanks, I'd missed that actually - the 2.0 release candidate is already out, and this isn't a regression, so would be more suited to 2.1
Ok, I moved the whatsnew.
@MarcoGorelli I just re-updated the branch with latest HEAD from main. Could we either merge now or define an explicit additional reviewer so that I can get back to the (more important) PR https://github.com/pandas-dev/pandas/pull/51298 ?
sure - there's been no objections in several days, so I'll merge on green

thanks for your work here!
Thanks @MarcoGorelli ! Unfortunately there is one red, completely unrelated (numpy https://github.com/pandas-dev/pandas/actions/runs/4324702806/jobs/7549886360#step:8:287 )

`pandas/tests/apply/test_frame_apply_relabeling.py::test_agg_relabel_partial_functions - ValueError: cannot reindex on an axis with duplicate labels`

details:

```
=================================== FAILURES ===================================
______________________ test_agg_relabel_partial_functions ______________________
[gw1] linux -- Python 3.10.9 /home/runner/micromamba/envs/test/bin/python3.10

    def test_agg_relabel_partial_functions():
        # GH 26513, test on partial, functools or more complex cases
        df = pd.DataFrame({"A": [1, 2, 1, 2], "B": [1, 2, 3, 4], "C": [3, 4, 5, 6]})
        result = df.agg(foo=("A", np.mean), bar=("A", "mean"), cat=("A", min))
        expected = pd.DataFrame(
            {"A": [1.5, 1.5, 1.0]}, index=pd.Index(["foo", "bar", "cat"])
        )
        tm.assert_frame_equal(result, expected)
    
>       result = df.agg(
            foo=("A", min),
            bar=("A", np.min),
            cat=("B", max),
            dat=("C", "min"),
            f=("B", np.sum),
            kk=("B", lambda x: min(x)),
        )

pandas/tests/apply/test_frame_apply_relabeling.py:54: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
pandas/core/frame.py:9189: in aggregate
    result = op.agg()
pandas/core/apply.py:690: in agg
    result = super().agg()
pandas/core/apply.py:163: in agg
    return self.agg_dict_like()
pandas/core/apply.py:443: in agg_dict_like
    result = concat(
pandas/core/reshape/concat.py:383: in concat
    return op.get_result()
pandas/core/reshape/concat.py:590: in get_result
    df = cons(data, index=index, copy=self.copy)
pandas/core/frame.py:704: in __init__
    mgr = dict_to_mgr(data, index, columns, dtype=dtype, copy=copy, typ=manager)
pandas/core/internals/construction.py:461: in dict_to_mgr
    return arrays_to_mgr(arrays, columns, index, dtype=dtype, typ=typ, consolidate=copy)
pandas/core/internals/construction.py:119: in arrays_to_mgr
    arrays, refs = _homogenize(arrays, index, dtype)
pandas/core/internals/construction.py:569: in _homogenize
    val = val.reindex(index, copy=False)
pandas/core/series.py:4848: in reindex
    return super().reindex(
pandas/core/generic.py:5346: in reindex
    return self._reindex_axes(
pandas/core/generic.py:5361: in _reindex_axes
    new_index, indexer = ax.reindex(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = Index(['min', 'min'], dtype='object')
target = Index(['min', 'max', 'sum', '<lambda>'], dtype='object'), method = None
level = None, limit = None, tolerance = None

    def reindex(
        self, target, method=None, level=None, limit=None, tolerance=None
    ) -> tuple[Index, npt.NDArray[np.intp] | None]:
        """
        Create index with target's values.
    
        Parameters
        ----------
        target : an iterable
        method : {None, 'pad'/'ffill', 'backfill'/'bfill', 'nearest'}, optional
            * default: exact matches only.
            * pad / ffill: find the PREVIOUS index value if no exact match.
            * backfill / bfill: use NEXT index value if no exact match
            * nearest: use the NEAREST index value if no exact match. Tied
              distances are broken by preferring the larger index value.
        level : int, optional
            Level of multiindex.
        limit : int, optional
            Maximum number of consecutive labels in ``target`` to match for
            inexact matches.
        tolerance : int or float, optional
            Maximum distance between original and new labels for inexact
            matches. The values of the index at the matching locations must
            satisfy the equation ``abs(index[indexer] - target) <= tolerance``.
    
            Tolerance may be a scalar value, which applies the same tolerance
            to all values, or list-like, which applies variable tolerance per
            element. List-like includes list, tuple, array, Series, and must be
            the same size as the index and its dtype must exactly match the
            index's type.
    
        Returns
        -------
        new_index : pd.Index
            Resulting index.
        indexer : np.ndarray[np.intp] or None
            Indices of output values in original index.
    
        Raises
        ------
        TypeError
            If ``method`` passed along with ``level``.
        ValueError
            If non-unique multi-index
        ValueError
            If non-unique index and ``method`` or ``limit`` passed.
    
        See Also
        --------
        Series.reindex : Conform Series to new index with optional filling logic.
        DataFrame.reindex : Conform DataFrame to new index with optional filling logic.
    
        Examples
        --------
        >>> idx = pd.Index(['car', 'bike', 'train', 'tractor'])
        >>> idx
        Index(['car', 'bike', 'train', 'tractor'], dtype='object')
        >>> idx.reindex(['car', 'bike'])
        (Index(['car', 'bike'], dtype='object'), array([0, 1]))
        """
        # GH6552: preserve names when reindexing to non-named target
        # (i.e. neither Index nor Series).
        preserve_names = not hasattr(target, "name")
    
        # GH7774: preserve dtype/tz if target is empty and not an Index.
        target = ensure_has_len(target)  # target may be an iterator
    
        if not isinstance(target, Index) and len(target) == 0:
            if level is not None and self._is_multi:
                # "Index" has no attribute "levels"; maybe "nlevels"?
                idx = self.levels[level]  # type: ignore[attr-defined]
            else:
                idx = self
            target = idx[:0]
        else:
            target = ensure_index(target)
    
        if level is not None and (
            isinstance(self, ABCMultiIndex) or isinstance(target, ABCMultiIndex)
        ):
            if method is not None:
                raise TypeError("Fill method not supported if level passed")
    
            # TODO: tests where passing `keep_order=not self._is_multi`
            #  makes a difference for non-MultiIndex case
            target, indexer, _ = self._join_level(
                target, level, how="right", keep_order=not self._is_multi
            )
    
        else:
            if self.equals(target):
                indexer = None
            else:
                if self._index_as_unique:
                    indexer = self.get_indexer(
                        target, method=method, limit=limit, tolerance=tolerance
                    )
                elif self._is_multi:
                    raise ValueError("cannot handle a non-unique multi-index!")
                elif not self.is_unique:
                    # GH#42568
>                   raise ValueError("cannot reindex on an axis with duplicate labels")
E                   ValueError: cannot reindex on an axis with duplicate labels

pandas/core/indexes/base.py:4251: ValueError
```

As you can see, the dataframe in the test does not even use datetime/period..

So you can either proceed "as is" or wait until someone fixes the issue on the main branch
Confirmed: the "red" on CI is described in #51782, totally unrelated
I fixed everything but I realized that the explicit default format could be actually caught, so I reworked a bit the period_format method.

I therefore need to re-run the ASVs to be sure this is ok
All done. I took into account all your comments @jbrockmendel . Actually one comment let me to refactor the code so that the "explicit default" (user passes a custom fmt that is identical to the default fmt for this freq) now works as fast as the default.

Ready for a last round of review !
@jbrockmendel  @MarcoGorelli 

```
       before           after         ratio
     [a07cb654]       [55d180e5]
     <feature/44764_perf_issue_new_period~5^2>       <feature/44764_perf_issue_new_period>
	 
       7.82±0.5ms       5.23±0.4ms    ~0.67  io.csv.ToCSVPeriod.time_frame_period_formatting(1000, 'D')
       7.37±0.2ms         6.89±2ms     0.94  io.csv.ToCSVPeriod.time_frame_period_formatting(1000, 'H')
-        59.6±1ms       36.1±0.4ms     0.61  io.csv.ToCSVPeriod.time_frame_period_formatting(10000, 'D')
-        60.3±2ms         41.1±1ms     0.68  io.csv.ToCSVPeriod.time_frame_period_formatting(10000, 'H')

-      7.33±0.4ms       4.58±0.4ms     0.63  io.csv.ToCSVPeriod.time_frame_period_formatting_default(1000, 'D')
       7.42±0.5ms       5.08±0.1ms    ~0.69  io.csv.ToCSVPeriod.time_frame_period_formatting_default(1000, 'H')
-      58.4±0.6ms         40.3±2ms     0.69  io.csv.ToCSVPeriod.time_frame_period_formatting_default(10000, 'D')
         62.4±2ms         44.5±3ms    ~0.71  io.csv.ToCSVPeriod.time_frame_period_formatting_default(10000, 'H')

-      7.06±0.4ms       4.78±0.2ms     0.68  io.csv.ToCSVPeriod.time_frame_period_formatting_default_explicit(1000, 'D')
       7.46±0.2ms       5.37±0.4ms    ~0.72  io.csv.ToCSVPeriod.time_frame_period_formatting_default_explicit(1000, 'H')
-        59.0±1ms         37.2±1ms     0.63  io.csv.ToCSVPeriod.time_frame_period_formatting_default_explicit(10000, 'D')
         64.0±2ms         43.0±6ms    ~0.67  io.csv.ToCSVPeriod.time_frame_period_formatting_default_explicit(10000, 'H')

       11.1±0.3ms       11.6±0.4ms     1.05  io.csv.ToCSVPeriodIndex.time_frame_period_formatting_index(1000, 'D')
         11.9±1ms       11.8±0.5ms     0.99  io.csv.ToCSVPeriodIndex.time_frame_period_formatting_index(1000, 'H')
         97.7±4ms         99.2±2ms     1.02  io.csv.ToCSVPeriodIndex.time_frame_period_formatting_index(10000, 'D')
         95.0±1ms         98.7±2ms     1.04  io.csv.ToCSVPeriodIndex.time_frame_period_formatting_index(10000, 'H')

-      4.90±0.2ms       2.41±0.3ms     0.49  io.csv.ToCSVPeriodIndex.time_frame_period_formatting_index_default(1000, 'D')
-      5.21±0.1ms       3.03±0.1ms     0.58  io.csv.ToCSVPeriodIndex.time_frame_period_formatting_index_default(1000, 'H')
-      34.9±0.5ms         14.6±1ms     0.42  io.csv.ToCSVPeriodIndex.time_frame_period_formatting_index_default(10000, 'D')
-      38.6±0.5ms       19.0±0.7ms     0.49  io.csv.ToCSVPeriodIndex.time_frame_period_formatting_index_default(10000, 'H')

-      9.27±0.4ms       4.94±0.3ms     0.53  io.csv.ToCSVPeriodIndex.time_frame_period_formatting_index_default_explicit(1000, 'D')
         11.3±1ms         8.08±2ms    ~0.72  io.csv.ToCSVPeriodIndex.time_frame_period_formatting_index_default_explicit(1000, 'H')
-        85.6±5ms       37.5±0.7ms     0.44  io.csv.ToCSVPeriodIndex.time_frame_period_formatting_index_default_explicit(10000, 'D')
-        97.6±8ms         41.3±2ms     0.42  io.csv.ToCSVPeriodIndex.time_frame_period_formatting_index_default_explicit(10000, 'H')

       6.47±0.2ms       6.19±0.3ms     0.96  strftime.PeriodStrftime.time_frame_period_formatting_custom(1000, 'D')
      6.01±0.06ms       6.34±0.1ms     1.06  strftime.PeriodStrftime.time_frame_period_formatting_custom(1000, 'H')
       58.8±0.6ms         60.2±2ms     1.02  strftime.PeriodStrftime.time_frame_period_formatting_custom(10000, 'D')
       60.9±0.9ms         62.8±1ms     1.03  strftime.PeriodStrftime.time_frame_period_formatting_custom(10000, 'H')

-      3.00±0.1ms         802±60μs     0.27  strftime.PeriodStrftime.time_frame_period_formatting_default(1000, 'D')
-      2.93±0.1ms      1.11±0.03ms     0.38  strftime.PeriodStrftime.time_frame_period_formatting_default(1000, 'H')
-      28.7±0.4ms       7.11±0.1ms     0.25  strftime.PeriodStrftime.time_frame_period_formatting_default(10000, 'D')
-      28.7±0.7ms       9.70±0.1ms     0.34  strftime.PeriodStrftime.time_frame_period_formatting_default(10000, 'H')

-      4.21±0.2ms         784±40μs     0.19  strftime.PeriodStrftime.time_frame_period_formatting_default_explicit(1000, 'D')
-     5.10±0.09ms      1.11±0.05ms     0.22  strftime.PeriodStrftime.time_frame_period_formatting_default_explicit(1000, 'H')
-      41.3±0.4ms       7.19±0.4ms     0.17  strftime.PeriodStrftime.time_frame_period_formatting_default_explicit(10000, 'D')
-        50.7±1ms       10.3±0.3ms     0.20  strftime.PeriodStrftime.time_frame_period_formatting_default_explicit(10000, 'H')

-      2.85±0.2ms        742±100μs     0.26  strftime.PeriodStrftime.time_frame_period_formatting_index_default(1000, 'D')
-      3.07±0.1ms      1.02±0.04ms     0.33  strftime.PeriodStrftime.time_frame_period_formatting_index_default(1000, 'H')
-      27.9±0.8ms       7.22±0.2ms     0.26  strftime.PeriodStrftime.time_frame_period_formatting_index_default(10000, 'D')
-        30.7±1ms       10.0±0.7ms     0.33  strftime.PeriodStrftime.time_frame_period_formatting_index_default(10000, 'H')

-      2.90±0.8ms         687±10μs     0.24  strftime.PeriodStrftime.time_frame_period_formatting_index_default_explicit(1000, 'D')
-      3.02±0.1ms         997±20μs     0.33  strftime.PeriodStrftime.time_frame_period_formatting_index_default_explicit(1000, 'H')
-      27.5±0.4ms       7.32±0.3ms     0.27  strftime.PeriodStrftime.time_frame_period_formatting_index_default_explicit(10000, 'D')
-        30.1±1ms       10.2±0.3ms     0.34  strftime.PeriodStrftime.time_frame_period_formatting_index_default_explicit(10000, 'H')

       5.96±0.1ms       6.52±0.3ms     1.09  strftime.PeriodStrftime.time_frame_period_formatting_iso8601_strftime_Z(1000, 'D')
       5.66±0.2ms      5.81±0.05ms     1.03  strftime.PeriodStrftime.time_frame_period_formatting_iso8601_strftime_Z(1000, 'H')
        72.2±10ms       55.2±0.5ms    ~0.76  strftime.PeriodStrftime.time_frame_period_formatting_iso8601_strftime_Z(10000, 'D')
         56.0±1ms         56.6±2ms     1.01  strftime.PeriodStrftime.time_frame_period_formatting_iso8601_strftime_Z(10000, 'H')

       6.21±0.2ms       6.14±0.4ms     0.99  strftime.PeriodStrftime.time_frame_period_formatting_iso8601_strftime_offset(1000, 'D')
       6.45±0.2ms         7.06±1ms     1.09  strftime.PeriodStrftime.time_frame_period_formatting_iso8601_strftime_offset(1000, 'H')
       58.5±0.6ms         62.2±2ms     1.06  strftime.PeriodStrftime.time_frame_period_formatting_iso8601_strftime_offset(10000, 'D')
       58.6±0.6ms       60.6±0.9ms     1.03  strftime.PeriodStrftime.time_frame_period_formatting_iso8601_strftime_offset(10000, 'H')
	
-      3.23±0.2ms         844±70μs     0.26  strftime.PeriodStrftime.time_frame_period_to_str(1000, 'D')
-      3.07±0.1ms      1.07±0.08ms     0.35  strftime.PeriodStrftime.time_frame_period_to_str(1000, 'H')
-      27.7±0.6ms       7.60±0.4ms     0.27  strftime.PeriodStrftime.time_frame_period_to_str(10000, 'D')
-      29.6±0.7ms       11.2±0.7ms     0.38  strftime.PeriodStrftime.time_frame_period_to_str(10000, 'H')

       9.42±0.1μs       9.81±0.2μs     1.04  tslibs.period.PeriodUnaryMethods.time_asfreq('M')
       9.95±0.7μs       10.2±0.5μs     1.02  tslibs.period.PeriodUnaryMethods.time_asfreq('min')
	   
         10.5±3μs       9.38±0.3μs    ~0.89  tslibs.period.PeriodUnaryMethods.time_now('M')
		 
-      3.41±0.1μs         980±50ns     0.29  tslibs.period.PeriodUnaryMethods.time_str('M')
-      3.60±0.1μs      1.65±0.02μs     0.46  tslibs.period.PeriodUnaryMethods.time_str('min')

       6.36±0.1μs       6.44±0.3μs     1.01  tslibs.period.PeriodUnaryMethods.time_strftime_custom('M')
       6.28±0.2μs         8.80±4μs    ~1.40  tslibs.period.PeriodUnaryMethods.time_strftime_custom('min')
	   
-      3.33±0.1μs         877±30ns     0.26  tslibs.period.PeriodUnaryMethods.time_strftime_default('M')
-      3.34±0.1μs      1.56±0.05μs     0.47  tslibs.period.PeriodUnaryMethods.time_strftime_default('min')

-      4.18±0.2μs      1.01±0.03μs     0.24  tslibs.period.PeriodUnaryMethods.time_strftime_default_explicit('M')
-     5.44±0.06μs      1.71±0.03μs     0.31  tslibs.period.PeriodUnaryMethods.time_strftime_default_explicit('min')

         68.7±2μs         67.5±1μs     0.98  tslibs.period.PeriodUnaryMethods.time_to_timestamp('M')
         66.6±2μs         71.0±4μs     1.07  tslibs.period.PeriodUnaryMethods.time_to_timestamp('min')
```

@jbrockmendel @MarcoGorelli would you have a few mins this week to check this PR so that I can move to the next one ? 
or maybe @mroeschke  ?
yup taking a look now (was off last week)
ok should be good - I'll give it a final look and merge this week
> Reckon we should add such unit tests for each period group (perhaps in a separate PR, which we could get in very quickly)? That would give me much more confidence about merging this

I did not review the available tests extensively so maybe this is indeed needed. I'll have a look one of these days 
This pull request is stale because it has been open for thirty days with no activity. Please [update](https://pandas.pydata.org/pandas-docs/stable/development/contributing.html#updating-your-pull-request) and respond to this comment if you're still interested in working on this.
Yes this is still active and even ready for merge, but it would require that another PR pushes a few more tests first so that @MarcoGorelli is more confident about the non-regressive impact :)
Now that #53003 is merged this should be ok - all new tests seem to pass.
let's get this in, thanks @smarie 
Great news @MarcoGorelli thanks ! 
I'll now finally move back to the main PR of this thread: #51298
Stay tuned ;)

Thanks again !