pandas-dev/pandas · error · ValueError

Quarter must be 1 <= q <= 4

Error message

Quarter must be 1 <= q <= 4

What it means

Raised by _range_from_fields after _field_to_int64 conversion when any element of the quarter array is outside [1, 4]. Quarters are a closed 1..4 enumeration; anything else indicates bad input.

Solutions

  1. Compute quarter as (month - 1) // 3 + 1.
  2. Clip or filter invalid quarters before passing: q = np.where((q>=1)&(q<=4), q, np.nan) then drop NaN.
  3. Validate explicitly with assert df['quarter'].between(1,4).all().

Example fix

# before
quarter = (month - 1) // 3  # 0..3
pd.PeriodIndex(year=year, quarter=quarter, freq='Q')
# after
quarter = (month - 1) // 3 + 1  # 1..4
pd.PeriodIndex(year=year, quarter=quarter, freq='Q')
Defensive patterns

Strategy: validation

Validate before calling

import numpy as np

def quarters_in_range(quarter) -> bool:
    q = np.asarray(quarter)
    return bool(((q >= 1) & (q <= 4)).all())

Type guard

def is_valid_quarter(q) -> bool:
    try:
        return 1 <= int(q) <= 4
    except (TypeError, ValueError):
        return False

Try / catch

try:
    pi = pd.PeriodIndex(year=y, quarter=q, freq='Q')
except ValueError as e:
    if 'Quarter must be' in str(e):
        q = np.clip(np.asarray(q).astype(int), 1, 4)
        pi = pd.PeriodIndex(year=y, quarter=q, freq='Q')
    else:
        raise

Prevention

When it happens

Trigger: pd.PeriodIndex(year=..., quarter=[0,1,2], freq='Q'); quarter=[1,2,5]; quarter derived from a 0-based index without adding 1; NaN-free float values like 4.5.

Common situations: Off-by-one when computing quarter from month ((month-1)//3 instead of (month-1)//3 + 1); 0-indexed user input; data-entry typo producing quarter=5.

Related errors


AI-assisted analysis of pandas-dev/pandas@3b7651241d (2026-08-11). Data as JSON: /api/errors/85b4fb6e62c8211a. Report an issue: GitHub.

Appendix: source

Thrown at pandas/core/arrays/period.py:1601

        day = 1

    if quarter is not None:
        if freq is None:
            freq = to_offset("Q", is_period=True)
            base = FreqGroup.FR_QTR.value
        else:
            freq = to_offset(freq, is_period=True)
            base = libperiod.freq_to_dtype_code(freq)
            if FreqGroup.from_period_dtype_code(base) != FreqGroup.FR_QTR:
                raise ValueError("freq must be a quarterly frequency")

        freqstr = freq.freqstr
        year, quarter = _make_field_arrays(year, quarter)
        year = _field_to_int64(year)
        quarter = _field_to_int64(quarter)

        if (quarter < 1).any() or (quarter > 4).any():
            raise ValueError("Quarter must be 1 <= q <= 4")

        # Vectorized quarter_to_myear
        mnum = MONTH_NUMBERS[parsing.get_rule_month(freqstr)] + 1
        months = (mnum + (quarter - 1) * 3) % 12 + 1
        years = np.where(months > mnum, year - 1, year)

        length = len(years)
        ones = np.ones(length, dtype=np.int64)
        zeros = np.zeros(length, dtype=np.int64)
        ordinals = libperiod.period_ordinals_from_fields(
            years, months, ones, zeros, zeros, zeros, base
        )
    else:
        freq = to_offset(freq, is_period=True)
        base = libperiod.freq_to_dtype_code(freq)
        arrays = _make_field_arrays(year, month, day, hour, minute, second)
        ordinals = libperiod.period_ordinals_from_fields(
            _field_to_int64(arrays[0]),

View on GitHub (pinned to 3b7651241d)