Skip to content

BUG: partial-string slicing on a PeriodIndex with an anchored freq resolves bounds one period early #66571

Description

@jbrockmendel

Pandas version checks

  • I have checked that this issue has not already been reported.
  • I have confirmed this bug exists on the latest version of pandas.
  • I have confirmed this bug exists on the main branch of pandas.

Reproducible Example

import pandas as pd

pi = pd.period_range("2000Q1", periods=12, freq="Q-FEB")
ser = pd.Series(range(12), index=pi)

# Slicing a single label by string returns two periods
ser.loc["2001Q1":"2001Q1"]
# 2000Q4    3
# 2001Q1    4
# Freq: Q-FEB, dtype: int64

# ...even though get_loc resolves the same label correctly
pi.get_loc("2001Q1")   # 4  (correct)

# Period bounds give the right answer; only the string path is wrong
ser.loc[pd.Period("2001Q1", freq="Q-FEB"):pd.Period("2001Q1", freq="Q-FEB")]
# 2001Q1    4

Issue Description

Partial-string slicing on a PeriodIndex whose frequency is anchored to a month that
does not line up with calendar quarters resolves the string bounds one period too early,
so the slice is shifted and includes an extra label at the start.

It is not specific to quarterly frequencies — anchored annual frequencies are affected
too:

for anchor in ["Q-DEC", "Q-JAN", "Q-FEB", "Q-MAR", "Q-APR", "Q-NOV"]:
    ser = pd.Series(range(12), index=pd.period_range("2000Q1", periods=12, freq=anchor))
    print(anchor, list(ser.loc["2001Q1":"2001Q1"].index))

# Q-DEC ['2001Q1']                # OK
# Q-JAN ['2000Q4', '2001Q1']      # wrong
# Q-FEB ['2000Q4', '2001Q1']      # wrong
# Q-MAR ['2001Q1']                # OK
# Q-APR ['2000Q4', '2001Q1']      # wrong
# Q-NOV ['2000Q4', '2001Q1']      # wrong

for anchor in ["Y-DEC", "Y-JUN", "Y-MAR"]:
    ser = pd.Series(range(8), index=pd.period_range("2000", periods=8, freq=anchor))
    print(anchor, list(ser.loc["2002":"2002"].index))

# Y-DEC ['2002']                  # OK
# Y-JUN ['2002', '2003']          # wrong
# Y-MAR ['2002', '2003']          # wrong

Q-DEC and Q-MAR are correct because their quarters coincide with calendar quarters.

The cause is in PeriodIndex._parsed_string_to_bounds
(pandas/core/indexes/period.py). The string is first parsed with the index frequency,
which correctly applies the anchor, but the result is then re-bucketed into a calendar
period before being converted back, which discards the anchor:

def _parsed_string_to_bounds(self, reso: Resolution, parsed: datetime):
    freq = OFFSET_TO_PERIOD_FREQSTR.get(reso.attr_abbrev, reso.attr_abbrev)
    iv = Period(parsed, freq=freq)   # <- freq is "Q"/"Y", anchor lost here
    return (iv.asfreq(self.freq, how="start"), iv.asfreq(self.freq, how="end"))

Traced for "2001Q1" on a Q-FEB index:

parse_datetime_string_with_reso("2001Q1", "Q-FEB")  ->  2000-03-01, reso="quarter"
Period(2000-03-01, freq="Q")                        ->  2000Q1      # calendar, anchor lost
.asfreq("Q-FEB", how="start"/"end")                 ->  2000Q4 .. 2001Q1

so the bounds span two labels instead of resolving to Period("2001Q1", freq="Q-FEB").

This also reaches DataFrame.truncate/Series.truncate, which delegate to .loc.

Expected Behavior

The string bound should resolve to the same period the equivalent Period object does, so
that ser.loc["2001Q1":"2001Q1"] returns exactly the 2001Q1 row for every anchor:

ser.loc["2001Q1":"2001Q1"]
# 2001Q1    4
# Freq: Q-FEB, dtype: int64

i.e. the bounds should be derived using the index's own frequency
(Period("2001Q1", freq=self.freq)) rather than by re-bucketing the parsed datetime into a
calendar period.

Installed Versions

Details
pandas  : 3.1.0.dev0+1434.gcbae8aea4a3
python  : 3.13.11
OS      : Darwin 23.3.0
numpy   : 2.4.4

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions