Skip to content

DataFrame interop (pandas, polars)

Lazy-imported converters between TSeries / MVTSeries / Workspace and pandas.DataFrame / polars.DataFrame. Neither library is a hard dependency; install with the optional extra:

pip install 'TimeSeriesEconPy[pandas]'   # or [polars], or [all]

The pandas surface accepts index="auto" | "mit" | "date"; the polars surface keeps the time axis as a column (polars has no row index) and requires explicit freq= on from_polars(date_column=...) since polars carries no period semantics.

tsecon.interop.pandas

tsecon.interop.pandas

pandas DataFrame interop (lazy / optional).

pandas is not a hard dependency: this module raises :class:ImportError with an install hint when called without pandas installed.

Public API
  • :func:to_pandasTSeries / MVTSeries / Workspacepd.Series / pd.DataFrame.
  • :func:from_pandaspd.Series / pd.DataFrameTSeries / MVTSeries / Workspace.
Required DataFrame shape (for :func:from_pandas)

The time axis must live on one of:

  • the row index, of one of the following types:

  • :class:pd.PeriodIndex — frequency inferred from .freqstr; no freq= kwarg needed. Supports pandas 2.x (Q-DEC, M, ...) and pandas 3.x (QE-DEC, ME, YE-DEC, ...) aliases.

  • :class:pd.DatetimeIndex — frequency cannot be auto-inferred; the freq= kwarg is required (one of :class:~tsecon.Daily, :class:~tsecon.BDaily, :class:~tsecon.Weekly, etc.).
  • :class:pd.RangeIndex or any integer index — interpreted as :class:~tsecon.frequencies.Unit by default; pass freq=Unit() explicitly to be explicit.
  • Object dtype of :class:~tsecon.MIT instances — the frequency is read off the MITs themselves; all MITs must share a single frequency. This is the round-trip target of :func:to_pandas(..., index="mit").

  • a column named time_col (must be passed explicitly). The same type / dtype rules apply, with a Date / Datetime column matching :class:pd.DatetimeIndex.

The time axis must be strictly ascending and contiguous (no gaps, no duplicates). Sparse or unsorted inputs raise :class:ValueError; callers who need gap-filling should sort_index().asfreq(...) on the pandas side first.

For wide DataFrames (default, wide=True), every non-time column becomes a variable. For long DataFrames (wide=False), time_col, name_col, and value_col are all required; the frame is pivoted into wide form before conversion.

Return type
  • :class:~tsecon.TSeries when the input is a Series.
  • :class:~tsecon.Workspace when to_workspace=True (one named TSeries per non-time column, in DataFrame column order; per-column dtypes preserved).
  • :class:~tsecon.MVTSeries otherwise (one column per variable, single shared dtype, numeric-only).
Output shape (for :func:to_pandas)

The index argument controls how the time axis is materialised:

  • "auto" — :class:pd.PeriodIndex for Yearly / Quarterly / Monthly / Weekly, :class:pd.DatetimeIndex (period-end dates) for Daily / BDaily, :class:pd.RangeIndex for Unit, and an object-dtype Index of MIT for HalfYearly (which pandas has no period analogue for).
  • "mit" — always emit an object-dtype Index of MIT instances. The only fully lossless option (re-from_pandas round-trips exactly).
  • "date" — always emit a :class:pd.DatetimeIndex of period-end dates (or period-begin dates if date_ref="begin"). Lossy for non-Daily frequencies on round-trip unless freq= is passed back to :func:from_pandas.

to_pandas

to_pandas(
    obj: TSeries | MVTSeries | Workspace,
    *,
    index: IndexKind = "auto",
    date_ref: Literal["begin", "end"] = "end",
    name: str | None = None,
) -> pd.Series | pd.DataFrame

Convert a tsecon container to a pandas Series or DataFrame.

Parameters:

Name Type Description Default
obj TSeries | MVTSeries | Workspace

A :class:~tsecon.TSeries (→ pd.Series), a :class:~tsecon.MVTSeries (→ pd.DataFrame with one column per variable), or a :class:~tsecon.Workspace containing only TSeries / MVTSeries members of a common frequency (→ pd.DataFrame).

required
index IndexKind

Time-axis representation. See module docstring.

'auto'
date_ref Literal['begin', 'end']

Period-anchor when index="date": "end" (default) uses the last calendar day of each period; "begin" uses the first.

'end'
name str | None

Optional Series.name override when converting a TSeries.

None

Returns:

Type Description
Series or DataFrame

A Series for TSeries; a DataFrame otherwise.

Source code in src/tsecon/interop/pandas.py
def to_pandas(
    obj: TSeries | MVTSeries | Workspace,
    *,
    index: IndexKind = "auto",
    date_ref: Literal["begin", "end"] = "end",
    name: str | None = None,
) -> pd.Series | pd.DataFrame:
    """Convert a tsecon container to a pandas Series or DataFrame.

    Parameters
    ----------
    obj
        A :class:`~tsecon.TSeries` (→ ``pd.Series``), a
        :class:`~tsecon.MVTSeries` (→ ``pd.DataFrame`` with one column per
        variable), or a :class:`~tsecon.Workspace` containing only
        TSeries / MVTSeries members of a common frequency
        (→ ``pd.DataFrame``).
    index
        Time-axis representation. See module docstring.
    date_ref
        Period-anchor when ``index="date"``: ``"end"`` (default) uses the
        last calendar day of each period; ``"begin"`` uses the first.
    name
        Optional ``Series.name`` override when converting a TSeries.

    Returns
    -------
    pd.Series or pd.DataFrame
        A Series for TSeries; a DataFrame otherwise.
    """
    pd = _require_pandas()
    if isinstance(obj, TSeries):
        return _tseries_to_series(pd, obj, index=index, date_ref=date_ref, name=name)
    if isinstance(obj, MVTSeries):
        return _mvtseries_to_frame(pd, obj, index=index, date_ref=date_ref)
    if isinstance(obj, Workspace):
        return _workspace_to_frame(pd, obj, index=index, date_ref=date_ref)
    msg = (  # type: ignore[unreachable]
        f"to_pandas accepts TSeries, MVTSeries, or Workspace; got {type(obj).__name__}."
    )
    raise TypeError(msg)

from_pandas

from_pandas(
    obj: Series | DataFrame,
    *,
    freq: Frequency | None = None,
    wide: bool = True,
    time_col: str | None = None,
    name_col: str | None = None,
    value_col: str | None = None,
    to_workspace: bool = False,
) -> TSeries | MVTSeries | Workspace

Convert a pandas Series / DataFrame to a tsecon container.

Parameters:

Name Type Description Default
obj Series | DataFrame

A pd.Series (→ TSeries) or pd.DataFrame.

required
freq Frequency | None

Required when the time axis is a :class:pd.DatetimeIndex (or a date-typed column) that has no inferable period semantics — pandas DatetimeIndex without .freq cannot be safely auto-classified between Daily / Weekly / Monthly / Quarterly / Yearly. Optional when the index is a PeriodIndex (frequency inferred from .freqstr) or an object index of MIT (frequency inferred from the MITs themselves).

None
wide bool

When True (the default) every non-time column becomes a variable of the resulting MVTSeries. When False the DataFrame is assumed to be in long format with time_col / name_col / value_col, and is pivoted to wide before conversion.

True
time_col str | None

Optional name of the column to use as the time axis. When unset, the DataFrame index is used.

None
name_col str | None

Long-format only. Name of the column holding the per-row variable name. Required when wide=False.

None
value_col str | None

Long-format only. Name of the column holding the per-row value. Required when wide=False.

None
to_workspace bool

When True and obj is a DataFrame, return a :class:~tsecon.Workspace with one named TSeries per non-time column rather than a single MVTSeries. Useful when round-tripping a heterogeneous Workspace through pandas or when downstream code expects a Workspace. The default (False) preserves the established single-container MVTSeries return for DataFrames.

False

Returns:

Type Description
TSeries or MVTSeries or Workspace

A TSeries when the input is a Series; a Workspace when to_workspace=True; an MVTSeries otherwise.

Source code in src/tsecon/interop/pandas.py
def from_pandas(
    obj: pd.Series | pd.DataFrame,
    *,
    freq: Frequency | None = None,
    wide: bool = True,
    time_col: str | None = None,
    name_col: str | None = None,
    value_col: str | None = None,
    to_workspace: bool = False,
) -> TSeries | MVTSeries | Workspace:
    """Convert a pandas Series / DataFrame to a tsecon container.

    Parameters
    ----------
    obj
        A ``pd.Series`` (→ TSeries) or ``pd.DataFrame``.
    freq
        Required when the time axis is a :class:`pd.DatetimeIndex` (or a
        date-typed column) that has no inferable period semantics — pandas
        DatetimeIndex without ``.freq`` cannot be safely auto-classified
        between Daily / Weekly / Monthly / Quarterly / Yearly. Optional
        when the index is a ``PeriodIndex`` (frequency inferred from
        ``.freqstr``) or an object index of MIT (frequency inferred from
        the MITs themselves).
    wide
        When ``True`` (the default) every non-time column becomes a
        variable of the resulting MVTSeries. When ``False`` the DataFrame
        is assumed to be in long format with ``time_col`` / ``name_col`` /
        ``value_col``, and is pivoted to wide before conversion.
    time_col
        Optional name of the column to use as the time axis. When unset,
        the DataFrame index is used.
    name_col
        Long-format only. Name of the column holding the per-row variable
        name. Required when ``wide=False``.
    value_col
        Long-format only. Name of the column holding the per-row value.
        Required when ``wide=False``.
    to_workspace
        When ``True`` and ``obj`` is a DataFrame, return a
        :class:`~tsecon.Workspace` with one named TSeries per non-time
        column rather than a single MVTSeries. Useful when round-tripping
        a heterogeneous Workspace through pandas or when downstream code
        expects a Workspace. The default (``False``) preserves the
        established single-container MVTSeries return for DataFrames.

    Returns
    -------
    TSeries or MVTSeries or Workspace
        A TSeries when the input is a Series; a Workspace when
        ``to_workspace=True``; an MVTSeries otherwise.
    """
    pd = _require_pandas()
    if isinstance(obj, pd.Series):
        if to_workspace:
            msg = "to_workspace=True requires a DataFrame, not a Series."
            raise TypeError(msg)
        return _series_to_tseries(pd, obj, freq=freq)
    if isinstance(obj, pd.DataFrame):
        if not wide:
            obj = _long_to_wide(pd, obj, time_col=time_col, name_col=name_col, value_col=value_col)
            # After pivot, the time axis lives on the index.
            time_col = None
        if to_workspace:
            return _frame_to_workspace(pd, obj, freq=freq, time_col=time_col)
        return _frame_to_mvtseries(pd, obj, freq=freq, time_col=time_col)
    msg = f"from_pandas accepts pd.Series or pd.DataFrame; got {type(obj).__name__}."
    raise TypeError(msg)

tsecon.interop.polars

tsecon.interop.polars

polars DataFrame interop (lazy / optional).

polars is not a hard dependency: this module raises :class:ImportError with an install hint when called without polars installed.

polars has no row-index concept, so the time axis is always a regular column. The default name is "time" (override with time_col=).

Public API
  • :func:to_polarsTSeries / MVTSeries / Workspace → :class:polars.DataFrame.
  • :func:from_polars — :class:polars.DataFrameTSeries / MVTSeries / Workspace.
Required DataFrame shape (for :func:from_polars)

The DataFrame must contain a column named time_col (default "time") of one of the following dtypes:

  • :class:polars.Date or :class:polars.Datetime — the freq= kwarg is required; polars carries no period semantics so the frequency cannot be auto-inferred. Pass a :class:~tsecon.Daily / :class:~tsecon.BDaily / :class:~tsecon.Weekly / :class:~tsecon.Monthly / :class:~tsecon.Quarterly / :class:~tsecon.HalfYearly / :class:~tsecon.Yearly instance.
  • Any integer dtype — interpreted as :class:~tsecon.frequencies.Unit by default; pass freq=Unit() explicitly to be explicit. The column values are taken as the raw MIT.value integers.

The time column must be strictly ascending and contiguous (no gaps, no duplicates). Sparse or unsorted input raises :class:ValueError; callers who need gap-filling should pre-process with polars before calling :func:from_polars.

For wide DataFrames (default, wide=True), every non-time column becomes a variable. For long DataFrames (wide=False), time_col, name_col, and value_col are all required; the frame is pivoted into wide form before conversion.

Return type
  • :class:~tsecon.Workspace when to_workspace=True (one named TSeries per non-time column, in DataFrame column order; per-column dtypes preserved).
  • :class:~tsecon.TSeries when the wide form has a single non-time column (and to_workspace=False).
  • :class:~tsecon.MVTSeries otherwise.
Output shape (for :func:to_polars)

The time column dtype is :class:polars.Date for Daily / BDaily / Weekly and YP frequencies (using period-end dates by default; date_ref="begin" swaps to period-begin), and :class:polars.Int64 for Unit (raw MIT values).

to_polars

to_polars(
    obj: TSeries | MVTSeries | Workspace,
    *,
    time_col: str = "time",
    date_ref: Literal["begin", "end"] = "end",
    value_col: str = "value",
) -> Any

Convert a tsecon container to a polars DataFrame.

Parameters:

Name Type Description Default
obj TSeries | MVTSeries | Workspace

A :class:~tsecon.TSeries (→ DataFrame with time_col plus a single value column named value_col), an :class:~tsecon.MVTSeries (→ DataFrame with time_col plus one column per variable), or a :class:~tsecon.Workspace of TSeries / MVTSeries members of a common frequency.

required
time_col str

Name of the emitted time column. Default "time".

'time'
date_ref Literal['begin', 'end']

Period-anchor for non-Unit frequencies. "end" (default) uses the last calendar day of each period; "begin" uses the first.

'end'
value_col str

Name of the value column when converting a TSeries (ignored for MVTSeries / Workspace). Default "value".

'value'

Returns:

Type Description
DataFrame

Annotated as Any because polars's type stubs are not used in tsecon's mypy config; the runtime return is always pl.DataFrame.

Source code in src/tsecon/interop/polars.py
def to_polars(
    obj: TSeries | MVTSeries | Workspace,
    *,
    time_col: str = "time",
    date_ref: Literal["begin", "end"] = "end",
    value_col: str = "value",
) -> Any:
    """Convert a tsecon container to a polars DataFrame.

    Parameters
    ----------
    obj
        A :class:`~tsecon.TSeries` (→ DataFrame with ``time_col`` plus a
        single value column named ``value_col``), an
        :class:`~tsecon.MVTSeries` (→ DataFrame with ``time_col`` plus one
        column per variable), or a :class:`~tsecon.Workspace` of TSeries /
        MVTSeries members of a common frequency.
    time_col
        Name of the emitted time column. Default ``"time"``.
    date_ref
        Period-anchor for non-Unit frequencies. ``"end"`` (default) uses
        the last calendar day of each period; ``"begin"`` uses the first.
    value_col
        Name of the value column when converting a TSeries (ignored for
        MVTSeries / Workspace). Default ``"value"``.

    Returns
    -------
    polars.DataFrame
        Annotated as ``Any`` because polars's type stubs are not used in
        tsecon's mypy config; the runtime return is always ``pl.DataFrame``.
    """
    pl = _require_polars()
    if isinstance(obj, TSeries):
        return _tseries_to_frame(pl, obj, time_col=time_col, value_col=value_col, date_ref=date_ref)
    if isinstance(obj, MVTSeries):
        return _mvtseries_to_frame(pl, obj, time_col=time_col, date_ref=date_ref)
    if isinstance(obj, Workspace):
        return _workspace_to_frame(pl, obj, time_col=time_col, date_ref=date_ref)
    msg = (  # type: ignore[unreachable]
        f"to_polars accepts TSeries, MVTSeries, or Workspace; got {type(obj).__name__}."
    )
    raise TypeError(msg)

from_polars

from_polars(
    obj: DataFrame,
    *,
    freq: Frequency | None = None,
    wide: bool = True,
    time_col: str = "time",
    name_col: str | None = None,
    value_col: str | None = None,
    to_workspace: bool = False,
) -> TSeries | MVTSeries | Workspace

Convert a polars DataFrame to a tsecon container.

Parameters:

Name Type Description Default
obj DataFrame

A :class:polars.DataFrame. Must contain time_col.

required
freq Frequency | None

Required when the time column dtype is :class:polars.Date / :class:polars.Datetime — polars carries no period semantics, so the frequency cannot be auto-inferred. For an integer time column the default is :class:~tsecon.frequencies.Unit.

None
wide bool

When True (the default) every non-time column becomes a variable. When False the DataFrame is in long format with time_col / name_col / value_col and is pivoted to wide.

True
time_col str

Name of the time column. Default "time".

'time'
name_col str | None

Long-format only.

None
value_col str | None

Long-format only. Also used to decide the resulting type: a single-value-column wide DataFrame returns a TSeries; otherwise an MVTSeries is returned.

None
to_workspace bool

When True, return a :class:~tsecon.Workspace with one named TSeries per non-time column. Preserves per-column dtypes (unlike the MVTSeries return, which forces a single shared dtype). Wins over the single-value-column TSeries shortcut.

False

Returns:

Type Description
TSeries or MVTSeries or Workspace

A Workspace when to_workspace=True; a TSeries when the wide form has a single non-time column; otherwise an MVTSeries.

Source code in src/tsecon/interop/polars.py
def from_polars(
    obj: pl.DataFrame,
    *,
    freq: Frequency | None = None,
    wide: bool = True,
    time_col: str = "time",
    name_col: str | None = None,
    value_col: str | None = None,
    to_workspace: bool = False,
) -> TSeries | MVTSeries | Workspace:
    """Convert a polars DataFrame to a tsecon container.

    Parameters
    ----------
    obj
        A :class:`polars.DataFrame`. Must contain ``time_col``.
    freq
        Required when the time column dtype is :class:`polars.Date` /
        :class:`polars.Datetime` — polars carries no period semantics, so
        the frequency cannot be auto-inferred. For an integer time column
        the default is :class:`~tsecon.frequencies.Unit`.
    wide
        When ``True`` (the default) every non-time column becomes a
        variable. When ``False`` the DataFrame is in long format with
        ``time_col`` / ``name_col`` / ``value_col`` and is pivoted to wide.
    time_col
        Name of the time column. Default ``"time"``.
    name_col
        Long-format only.
    value_col
        Long-format only. Also used to decide the resulting type: a
        single-value-column wide DataFrame returns a TSeries; otherwise
        an MVTSeries is returned.
    to_workspace
        When ``True``, return a :class:`~tsecon.Workspace` with one named
        TSeries per non-time column. Preserves per-column dtypes (unlike
        the MVTSeries return, which forces a single shared dtype). Wins
        over the single-value-column TSeries shortcut.

    Returns
    -------
    TSeries or MVTSeries or Workspace
        A Workspace when ``to_workspace=True``; a TSeries when the wide
        form has a single non-time column; otherwise an MVTSeries.
    """
    pl = _require_polars()
    if not isinstance(obj, pl.DataFrame):
        msg = f"from_polars accepts pl.DataFrame; got {type(obj).__name__}."
        raise TypeError(msg)
    if not wide:
        obj = _long_to_wide(pl, obj, time_col=time_col, name_col=name_col, value_col=value_col)
    if time_col not in obj.columns:
        msg = f"time_col={time_col!r} not found in DataFrame columns ({list(obj.columns)})."
        raise KeyError(msg)
    mits = _column_to_mits(pl, obj[time_col], freq=freq)
    data_df = obj.drop(time_col)
    if not mits:
        msg = "Cannot build a tsecon container from an empty DataFrame."
        raise ValueError(msg)
    _validate_contiguous(mits)
    rng = MITRange(mits[0], mits[-1])
    if to_workspace:
        out = Workspace()
        for col_name in data_df.columns:
            out[str(col_name)] = TSeries(rng, data_df[col_name].to_numpy())
        return out
    if data_df.width == 1:
        col_name = data_df.columns[0]
        return TSeries(rng, data_df[col_name].to_numpy())
    arr = data_df.to_numpy()
    return MVTSeries(rng, list(data_df.columns), arr)