skfolio.containers.AssetPanel#

class skfolio.containers.AssetPanel(fields, observations, asset_names, active_mask=None, estimation_mask=None, _validate_on_init=True)[source]#

Container for aligned cross-sectional asset data.

AssetPanel stores asset-level fields (e.g. returns, volumes, industry classification, factor exposure), over shared observation and asset axes. Every field uses observations as the first axis and assets as the second axis. These two axes always have shape (n_observations, n_assets).

Three kinds of fields are supported:

  • 2D numeric fields (e.g. returns, volume, market_cap). These are stored as 2D numpy array in a Field2D.

  • 2D categorical fields (e.g. country, industry). These are stored as 2D numpy array of integer codes in a FieldCategorical, together with the category labels (e.g. “bank”, “technology”)

  • 3D numeric fields (e.g. factor_exposures). These are stored in a 3D numpy array in a Field3D with shape (n_observations, n_assets, n_third_axis), together with the third axis labels such as factor names (e.g. “size”, “momentum”) and optional group labels such as factor families (e.g. “style”, “industry”)

The container is scikit-learn compatible: len(panel) returns n_observations and panel[start:stop] returns an AssetPanelView that can be passed to cross-validation and hyper-parameter tuning utilities. View creation is zero-copy when the observation selector is a slice or a contiguous index.

Parameters:
fieldsdict[str, BaseField or ndarray]

Field mapping. Raw arrays must be 2D and are converted to Field2D. Use FieldCategorical for integer-coded categorical fields and Field3D for 3D fields; both carry the metadata needed to interpret their codes or third axes.

observationsndarray of shape (n_observations,)

Unique observation labels for the sample axis. NumPy dtypes are preserved. Object-dtype datetime-like labels are converted with NumPy, object-dtype string labels are converted to strings and mixed object labels are rejected.

asset_namesndarray of shape (n_assets,)

Unique asset labels. Object-dtype labels are converted to strings.

active_maskboolean ndarray of shape (n_observations, n_assets), optional

Boolean mask indicating whether each asset belongs to the universe at each observation. This separates assets that are outside the universe (e.g. before listing, after delisting) from assets that are in the universe but have a missing observation (e.g. holiday, missing quote). If None, all pairs are active.

estimation_maskboolean ndarray of shape (n_observations, n_assets), optional

Boolean mask indicating which active (observation, asset) pairs should be used for estimator-specific statistics by skfolio estimators that support it (e.g. CSStandardScaler, RegimeAdjustedEWCovariance If None, all active pairs are eligible for estimation. Values are always enforced as a subset of active_mask.

Attributes:
n_observationsint

Number of observations.

n_assetsint

Number of assets.

n_fieldsint

Number of fields.

Methods

add_2d_field(name, values, *[, inactive_policy])

Add or replace a numeric 2D field.

add_3d_field(name, values, *, ...[, ...])

Add or replace a numeric 3D field.

add_categorical_field(name, values, *, levels)

Add or replace a 2D categorical field.

align_active_mask_to(fields)

Align active periods to valid field values.

bfill(fields, *[, limit, inplace])

Backward fill NaN values along the observation axis.

copy(*[, deep])

Return a copy of the panel.

decode_categorical_field(name, *[, ...])

Decode a categorical field to labels.

describe(*[, by])

Return a structured missingness summary.

drop(*[, observations, assets])

Return a panel with selected labels removed.

edit_masks(*[, _validate])

Temporarily make masks editable.

ffill(fields, *[, limit, inplace])

Forward fill NaN values along the observation axis.

get_field(name)

Return a field object.

info()

Multi-line report with panel dimensions, mask coverage, field missingness and categorical field level coverage.

isel(*[, observations, assets])

Select observations and assets by integer position.

keys()

Return field names.

load(path, *[, mmap_mode, fields])

Load a panel saved with save.

rename([fields, overwrite])

Rename fields in place.

save(path, *[, overwrite])

Save the panel to a directory of .npy files.

sel(*[, observations, assets, fields])

Select observations, assets and fields by label.

sel_3d(name, *[, labels, groups])

Select entries from the third axis of a 3D field by label.

to_dataframe(*[, fields, assets, ...])

Convert 2D fields to a pandas DataFrame.

Notes

AssetPanel is an optimized middle ground between raw NumPy arrays and general-purpose labeled containers (e.g. pandas, polars, xarray). It is optimized for portfolio, factor and alpha workflows:

  • observations are the sample axis, so scikit-learn cross-validation can slice over time without grouping rows

  • assets are fixed on axis 1, while active_mask represents listings, delistings and other universe changes

  • payload arrays remain numeric, with categorical fields stored as integer codes

  • categorical levels, third-axis labels and third-axis groups are stored with their fields

  • shape, mask and universe invariants are validated by the container

  • estimators can rely on validated axes, masks and float-field universe invariants without repeating full container validation

Compared with DataFrames, this avoids repeated groupby, pivot and index-alignment work while keeping the arrays ready for vectorized cross-sectional and time-series operations. Compared with xarray, it keeps a smaller API optimized for quant workflows.

Performance benefits come from the same layout:

  • observation slices return AssetPanelView objects, so walk-forward folds can reuse field arrays

  • native Field3D fields avoid restacking large lists of 2D arrays

  • integer-coded categoricals and dense boolean masks keep memory and conversion overhead low

  • with Parallel(..., prefer="threads"), workers can read the same big panel in memory instead of receiving separate process copies. This is useful for NumPy-heavy computations where the numeric kernels release the GIL.

  • saved panels use .npy files and support memory-mapped loading with AssetPanel.load(..., mmap_mode=...).

Indexing.

  • panel["name"] returns the underlying field array.

  • panel.fields["name"] returns the field object with its metadata.

  • panel[start:stop] returns an AssetPanelView with shared field arrays.

  • panel.isel(...) and panel.sel(...) select observations and assets by position or label.

Examples

>>> import numpy as np
>>> from skfolio.containers import AssetPanel, concat
>>>
>>> n_observations = 252
>>> observations = np.arange(n_observations)
>>> assets = ["AAPL", "MSFT", "GOOG", "AMZN"]
>>> n_assets = len(assets)
>>>
>>> panel = AssetPanel(
...     fields={
...         "returns": np.random.randn(n_observations, n_assets),
...         "volume": np.random.lognormal(size=(n_observations, n_assets)),
...         "market_cap": np.random.lognormal(size=(n_observations, n_assets)),
...     },
...     observations=observations,
...     asset_names=assets,
... )
>>> panel.add_categorical_field(
...     name="industry",
...     values=np.random.randint(0, 3, size=(n_observations, n_assets)),
...     levels=["energy", "bank", "technology"],
... )
AssetPanel(n_observations=252, n_assets=4, n_fields=4)
>>> factor_labels = ["size", "momentum", "value"]
>>> panel.add_3d_field(
...     name="factor_exposure",
...     values=np.random.randn(n_observations, n_assets, len(factor_labels)),
...     third_axis_name="factor",
...     third_axis_labels=factor_labels,
...     third_axis_groups=["style", "style", "style"],
... )
AssetPanel(n_observations=252, n_assets=4, n_fields=5)
>>> panel.n_observations, panel.n_assets, panel.n_fields
(252, 4, 5)

Access raw NumPy arrays:

>>> returns = panel["returns"]
>>> industry_codes = panel["industry"]
>>> factor_exposure = panel["factor_exposure"]

Use field objects or decoding helpers when labels or metadata are needed:

>>> industry_labels = panel.decode_categorical_field("industry")
>>> exposure_field = panel.fields["factor_exposure"]
>>> exposure_field.third_axis_labels
array(['size', 'momentum', 'value'], dtype='<U8')

Slice observations:

>>> view = panel[100:200]
>>> view.n_observations
100

Select observations and assets by position or label:

>>> panel.isel(observations=slice(0, 60), assets=[0, 1])
AssetPanel(n_observations=60, n_assets=2, n_fields=5)
>>> panel.sel(observations=slice(0, 59), assets=["AAPL", "MSFT"])
AssetPanel(n_observations=60, n_assets=2, n_fields=5)

Select entries from a 3D field by third-axis label or group:

>>> panel.sel_3d("factor_exposure", labels="momentum").shape
(252, 4)
>>> panel.sel_3d("factor_exposure", groups="style").shape
(252, 4, 3)

Convert to pandas:

>>> df = panel.to_dataframe(fields=["returns", "industry"], output_format="wide")

Get summary and inspect missingness:

>>> summary = panel.describe(by="industry")
>>> report = panel.info()

Clean selected fields:

>>> panel.ffill("returns", inplace=False)
AssetPanel(n_observations=252, n_assets=4, n_fields=5)
>>> panel.bfill("returns", inplace=False)
AssetPanel(n_observations=252, n_assets=4, n_fields=5)
>>> panel.align_active_mask_to("returns")
0

Rename field and drop assets:

>>> panel.rename({"market_cap": "capitalization"})
AssetPanel(n_observations=252, n_assets=4, n_fields=5)
>>> panel.drop(assets=["AMZN"])
AssetPanel(n_observations=252, n_assets=3, n_fields=5)

Concatenate, copy, save and load panels:

>>> concat([panel[:126], panel[126:]])
AssetPanel(n_observations=252, n_assets=4, n_fields=5)
>>> panel.copy(deep=True)
AssetPanel(n_observations=252, n_assets=4, n_fields=5)
>>> panel.save("asset_panel")
>>> loaded = AssetPanel.load("asset_panel", mmap_mode="r")
add_2d_field(name, values, *, inactive_policy=MISSING)#

Add or replace a numeric 2D field.

Parameters:
namestr

Field name.

valuesarray-like of shape (n_observations, n_assets)

Numeric 2D values.

inactive_policyInactivePolicy, default=InactivePolicy.MISSING

Validation policy for values outside active_mask.

Returns:
selfBaseAssetPanel

The modified container.

add_3d_field(name, values, *, third_axis_name, third_axis_labels, third_axis_groups=None, inactive_policy=MISSING)#

Add or replace a numeric 3D field.

This is a convenience wrapper around assigning a Field3D. The first two axes of values must be observations and assets with shape (n_observations, n_assets). The third axis stores a homogeneous block such as factors.

Parameters:
namestr

Field name.

valuesarray-like of shape (n_observations, n_assets, n_third_axis)

Numeric 3D values.

third_axis_namestr

Name describing what the third axis represents (e.g. factor).

third_axis_labelsarray-like of shape (n_third_axis,)

Labels for entries along the third axis such as factor names (e.g. size, momentum).

third_axis_groupsarray-like of shape (n_third_axis,), optional

Optional group label for each third-axis entry such as factor families (e.g. style, industry).

inactive_policyInactivePolicy, default=InactivePolicy.MISSING

Validation policy for values outside active_mask.

Returns:
selfBaseAssetPanel

The modified container.

add_categorical_field(name, values, *, levels, inactive_policy=MISSING)#

Add or replace a 2D categorical field.

This is a convenience wrapper around assigning a FieldCategorical. The field values must be integer codes with shape (n_observations, n_assets). Code -1 is reserved for missing values. Code 0 selects levels[0], code 1 selects levels[1] and so on.

Parameters:
namestr

Field name.

valuesarray-like of integers, shape (n_observations, n_assets)

Integer category codes.

levelsarray-like of shape (n_levels,)

Category labels selected by codes 0, 1 and so on.

inactive_policyInactivePolicy, default=InactivePolicy.MISSING

Validation policy for codes outside active_mask.

Returns:
selfBaseAssetPanel

The modified container.

align_active_mask_to(fields)[source]#

Align active periods to valid field values.

For each asset, remove leading active_mask entries until the selected fields have valid values in the remaining active history. If an asset has no valid active value for a selected field, all active entries for that asset are removed. Only leading active entries are removed. Missing values after the first valid active value are left unchanged.

Parameters:
fieldsstr or iterable of str

Field names used to determine when each asset can become active. Floating values must be finite, categorical values must not be missing, and 3D floating values must be finite across the third axis.

Returns:
n_removedint

Number of (observation, asset) entries removed from active_mask.

bfill(fields, *, limit=None, inplace=True)[source]#

Backward fill NaN values along the observation axis.

Parameters:
fieldsstr or iterable of str

Numeric Field2D names to fill.

limitint or None, optional

Maximum number of consecutive NaN values to fill. If None, all consecutive NaN values are eligible.

inplacebool, default=True

If True, modify this panel. If False, return a shallow copy with filled fields.

Returns:
panelAssetPanel

Modified panel or copied panel.

copy(*, deep=False)[source]#

Return a copy of the panel.

Parameters:
deepbool, default=False

If True, copy field arrays and label arrays. If False, field arrays and labels are shared. Masks are always copied so the copy owns independent lockable mask arrays.

Returns:
panelAssetPanel

Copied panel.

decode_categorical_field(name, *, missing_label='MISSING')#

Decode a categorical field to labels.

Parameters:
namestr

Name of a FieldCategorical field.

missing_labelstr, default=”MISSING”

Label assigned to missing or out-of-bound codes.

Returns:
decodedndarray

Decoded labels with shape (n_observations, n_assets).

describe(*, by=None)[source]#

Return a structured missingness summary.

Parameters:
bystr or None, optional

Categorical field used to group missingness statistics. If None, missingness is summarized by field.

Returns:
summarypandas.DataFrame

Missingness summary indexed by field, or by (field, category) when by is provided.

drop(*, observations=None, assets=None)[source]#

Return a panel with selected labels removed.

Parameters:
observationsscalar, iterable, or None, optional

Observation labels to remove.

assetsscalar, iterable, or None, optional

Asset labels to remove.

Returns:
panelAssetPanel

New panel with the selected observations or assets removed.

edit_masks(*, _validate=True)[source]#

Temporarily make masks editable.

On exit, estimation_mask is re-enforced as a subset of active_mask, field inactive policies are applied, and both masks are locked again.

Parameters:
_validatebool, default=True

Internal flag controlling the per-observation non-empty mask check after the context exits.

Yields:
None

The panel with editable mask arrays.

ffill(fields, *, limit=None, inplace=True)[source]#

Forward fill NaN values along the observation axis.

Parameters:
fieldsstr or iterable of str

Numeric Field2D names to fill.

limitint or None, optional

Maximum number of consecutive NaN values to fill. If None, all consecutive NaN values are eligible.

inplacebool, default=True

If True, modify this panel. If False, return a shallow copy with filled fields.

Returns:
panelAssetPanel

Modified panel or copied panel.

get_field(name)[source]#

Return a field object.

Parameters:
namestr

Field name.

Returns:
fieldBaseField

Field object that owns the field values and metadata.

info()[source]#

Multi-line report with panel dimensions, mask coverage, field missingness and categorical field level coverage.

Returns:
reportstr

Multi-line report.

isel(*, observations=None, assets=None)[source]#

Select observations and assets by integer position.

Parameters:
observationsint, slice, array-like, or None, optional

Positional selector for the observation axis. If None, all observations are selected.

assetsint, slice, array-like, or None, optional

Positional selector for the asset axis. If None, assets are not sliced and an AssetPanelView is returned.

Returns:
panel or viewAssetPanel or AssetPanelView

Observation-only selections return a view. Selections that slice assets return a new panel.

keys()[source]#

Return field names.

Returns:
namesKeysView of str

Names of all fields in the panel.

classmethod load(path, *, mmap_mode=None, fields=None)[source]#

Load a panel saved with save.

Parameters:
pathstr or pathlib.Path

Directory containing a saved panel.

mmap_modestr or None, optional

Memory-mapping mode passed to numpy.load for field and mask arrays. Use r for read-only memory maps.

fieldslist of str or None, optional

Field names to load. If None, all fields are loaded.

Returns:
panelAssetPanel

Loaded panel.

property n_assets#

Number of assets.

property n_fields#

Number of fields.

property n_observations#

Number of observations.

property ndim#

Number of dimensions used by scikit-learn sample indexing.

rename(fields=None, *, overwrite=False)[source]#

Rename fields in place.

Parameters:
fieldsmapping of str to str or None, optional

Mapping from existing field names to replacement names.

overwritebool, default=False

If True, an existing target field can be replaced by a renamed field. If False, name conflicts raise KeyError.

Returns:
selfAssetPanel

The modified panel.

save(path, *, overwrite=False)[source]#

Save the panel to a directory of .npy files.

The directory contains one .npy file per field, small metadata files for categorical and third-axis labels and a _metadata.json manifest. Object-dtype axis labels and field metadata labels are converted to strings so the panel is loaded with allow_pickle=False.

Parameters:
pathstr or pathlib.Path

Destination directory.

overwritebool, default=False

If True, replace an existing saved panel at path. Existing directories that do not contain _metadata.json are never overwritten.

sel(*, observations=None, assets=None, fields=None)[source]#

Select observations, assets and fields by label.

Parameters:
observationsscalar, slice, iterable, or None, optional

Observation labels to select. If None, all observations are selected.

assetsscalar, slice, iterable, or None, optional
Asset labels to select. If None, assets are not sliced and an

AssetPanelView is returned.

fieldsstr, iterable of str, or None, optional

Field names to select. If None, all fields are selected.

Returns:
panel or viewAssetPanel or AssetPanelView

Observation-only selections without field filtering return a view. Selections that slice assets or fields return a new panel.

sel_3d(name, *, labels=None, groups=None)#

Select entries from the third axis of a 3D field by label.

Exactly one of labels or groups must be provided. Selecting a single label returns a 2D array with shape (n_observations, n_assets). Selecting multiple labels or any group returns a 3D array whose first two axes are unchanged.

Parameters:
namestr

Name of a Field3D.

labelsscalar, iterable, or None, optional

Third-axis labels to select.

groupsscalar, iterable, or None, optional

Third-axis group labels to select. The field must define third_axis_groups.

Returns:
valuesndarray

Selected values. A scalar labels selection returns 2D values. All other selections return 3D values.

property shape#

Shape tuple used by scikit-learn sample indexing.

to_dataframe(*, fields=None, assets=None, output_format='long', decode_categoricals=True)[source]#

Convert 2D fields to a pandas DataFrame.

Field3D entries are skipped with a warning when multiple fields are converted. Selecting a single Field3D raises ValueError.

Parameters:
fieldsstr, iterable of str, or None, optional

Field names to include. If a single string is passed, the result is a simple field DataFrame with observations as index and assets as columns. If None, all 2D fields are included.

assetsstr, iterable of str, or None, optional

Asset labels to include. If None, all assets are included.

output_format{“long”, “wide”}, default=”long”

Output format used when fields is not a single string. In long format, rows are indexed by (observation, asset) and filtered by active_mask. In wide format, columns are indexed by (field, asset).

decode_categoricalsbool, default=True

If True, categorical codes are decoded to labels.

Returns:
dfpandas.DataFrame

DataFrame representation of the selected 2D fields.