Skip to content

chc.panel

Stability tier: stable.

A validated long-format panel: one place where the shape assumptions are checked and recorded.

The estimators in this library take panel data in two incompatible shapes. The DiD and synthetic control routines want a dense (n_units, n_periods) matrix of one outcome (chc.did, chc.scm); everything else wants flat named columns (chc.frames). Callers currently pivot between the two by hand, and the pivot is where the quiet failures live: a duplicated (unit, time) row silently keeps whichever value NumPy wrote last, a missing one silently becomes a zero, and neither shows up until an effect estimate is already in a slide.

Panel does that pivot once, refuses to do it when the data cannot support it, and says which unit and which period were responsible. It holds a read-only copy of the columns it was given, made once, and in an object column only values that cannot change in place, so that the data cannot change under its hash; passing one around costs what passing a dict does.

A panel holds text, dates and times as well as numbers: a unit's name, a period's date. Which columns are numbers is the reader's question, so a column is checked when it is read as numbers, by Panel.wide and by every routine here that reads a panel's column as float64, and refused there, naming the column, the unit and the time, unless it holds real numbers.

Provenance travels with it. A number is reproducible only together with the bytes it came from and the precision it was computed in, and this library has already been bitten by the second: JAX's x64 flag changes which sample a seed draws, so a seed alone does not name a dataset. The hash is over the column bytes, so two panels that agree numerically but differ in dtype hash differently --- which is the honest answer, because they will not produce the same numbers. An object column's bytes are pointers, which no two runs share, so it is hashed by its values: each value's type and text, each with its length.

A panel reads its periods on a calendar only where the caller declares one, as frequency: calendar months stamped as dates lie 28 to 31 days apart, on no grid a library could infer, and read in the order logged a month no unit logged is not seen. The declared frequency is part of the dataset's identity, so it enters the hash.

PanelError

Bases: ValueError

A frame could not be read as a panel; the message names the column and the entity.

Provenance dataclass

What a number would need beside it to be reproduced: the bytes, the version, the precision.

x64 is not decoration. JAX's double-precision flag changes how many bits a threefry key spends per element, so the same seed draws a different sample at the two settings; a run recorded without it cannot be repeated. See docs/concepts/dtype-policy.md.

to_json()

A plain dict, ready for json.dumps beside the result it describes.

Panel dataclass

Long-format panel data whose (unit, time) index has been checked exactly once.

eq=False because the fields hold arrays: a generated __eq__ would compare them elementwise and return an array where a caller expects a bool, so panel_a == panel_b would raise inside any if. Identity of a panel is provenance and its hash.

names property

Every column name, in the frame's own order.

units property

The distinct unit labels, sorted --- the row order of wide.

periods property

The distinct period labels, sorted --- the column order of wide.

is_balanced property

Whether every unit is observed in every period, which wide requires.

from_frame(data, *, unit, time, frequency=None, cluster=None, seed=None, require_balanced=False) classmethod

Read a pandas/polars frame or a column mapping as a panel, or say exactly why not.

An object column, which is how pandas hands over text, holds values of one type that cannot change in place and whose text names them: str, bytes, bool, int, float, complex, Decimal, Fraction, UUID, date, time, datetime or timedelta, a subclass of one (a pandas Timestamp is a datetime), or NumPy's scalar of one of these kinds. Each value is hashed by its type and its text. A float, a complex or a Decimal, NumPy's among them, is refused when it is nan or infinite, as in a float column. A value that can change in place --- a dict, a list, a set, a bytearray, an array, an object of the caller's own class --- is refused rather than copied, since a copy would still change through panel[name][row]. So are None and pandas' NA, which is how a frame hands over a missing value, and any other type, a tuple or a pandas Period among them: fill or drop a missing value, and convert any other value to text, or a Period to a timestamp. A masked array's masked cell is a missing value too, refused by chc.frames.as_columns.

A column of text, dates, times, durations or complex numbers is held, as a label is, and so is a number past float64's range. Each is refused where it is read as numbers, by wide and by chc.decision.prescribe among others: a column is read as numbers only where its dtype is boolean, integer or floating, or its values are bool, int, float, Decimal or Fraction, NumPy's among them, and each is finite in float64 where it is finite in its own type.

Parameters:

Name Type Description Default
unit, time

the column names holding the entity and the period. Values may be any sortable type; periods are ranked, not assumed to be 0..T-1.

required
frequency str | float | None

the calendar the periods are read on, which says which two periods are consecutive. For dates, "D", "W", "M", "Q" or "Y": a row's period is the day, week, month, quarter or year its stamp falls in, by numpy's calendar units, a week running from a Thursday as 1970-01-01 did, a quarter being three months from January. For numbers, a positive step: a period is a whole number of steps from the first. Either way the rows of one period share one label, the earliest stamp logged in it. "observed" reads the periods in the order logged, as consecutive where no period lies between them. Undeclared, numbers and dates on a uniform grid are read on it, other numbers and labels in the order logged, and dates off a grid, calendar months among them, are refused.

None
cluster str | None

an optional column naming the group cluster-robust inference should use --- declared here rather than at the call site, because it is a property of the sampling design and not of the estimator.

None
seed int | None

the generator seed, when the data is simulated. Recorded in Provenance beside the x64 flag, which is the other half of what a re-draw needs.

None
require_balanced bool

fail at construction if some unit is missing some period, rather than at the first wide call. Off by default: unbalanced panels are ordinary, and the routines that cannot take one say so themselves.

False

Raises:

Type Description
PanelError

for a missing index column, one column named as both the unit and the time, a non-1-D or ragged column, a non-finite number in a float, a complex or an object column, a missing date, a duplicated (unit, time) pair, an object column whose values are not of one type (a missing value among strings, say), whose values' text is their address in memory, or whose values' type a panel does not hold, a frequency the time column cannot be read in, dates off a uniform grid with no frequency declared, a period off the declared step, a unit with two rows in one declared period, or --- under require_balanced --- a hole. Every message names the column and the offending entity.

ValueError

from chc.frames.as_columns, for two columns of one name or a masked cell, naming the column and its row.

codes()

Row-aligned integer codes into units and periods.

What the estimators that group by entity want: chc.regret's fold builders and the cluster-robust variance routines index by position, and deriving the positions here means one ranking convention rather than one per caller.

wide(name)

Column name as the dense (n_units, n_periods) float64 matrix DiD and SCM take.

Float64 regardless of the input dtype and of JAX's x64 flag, matching chc.did and chc.scm, which are NumPy-float64 by contract so that an estimate does not move with a global flag.

Raises:

Type Description
PanelError

if the column does not hold real numbers --- text, dates, times, durations, complex numbers, any other object --- or holds one that is finite in its own type and an infinity in float64, naming the first such value's unit and time; or if the panel is unbalanced, naming the first missing (unit, time).

installed_version()

The version of the installed distribution, which is the only one that can be reproduced.

Read from package metadata rather than from a literal in the source, because a literal is a second copy of pyproject.toml's version and second copies drift: chc.__version__ sat at 0.3.0 through the 0.4.0 and 0.5.0 releases, so every Provenance written by a caller reading it named a version that did not produce the numbers beside it.