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.
AssetPanelstores asset-level fields (e.g. returns, volumes, industry classification, factor exposure), over shared observation and asset axes. Every field usesobservationsas the first axis andassetsas 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 aField2D.2D categorical fields (e.g.
country,industry). These are stored as 2D numpy array of integer codes in aFieldCategorical, 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 aField3Dwith 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)returnsn_observationsandpanel[start:stop]returns anAssetPanelViewthat 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. UseFieldCategoricalfor integer-coded categorical fields andField3Dfor 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 byskfolioestimators that support it (e.g.CSStandardScaler,RegimeAdjustedEWCovarianceIfNone, all active pairs are eligible for estimation. Values are always enforced as a subset ofactive_mask.
- Attributes:
n_observationsintNumber of observations.
n_assetsintNumber of assets.
n_fieldsintNumber 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
.npyfiles.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
AssetPanelis 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_maskrepresents listings, delistings and other universe changespayload 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,pivotand 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
AssetPanelViewobjects, so walk-forward folds can reuse field arraysnative
Field3Dfields avoid restacking large lists of 2D arraysinteger-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
.npyfiles and support memory-mapped loading withAssetPanel.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 anAssetPanelViewwith shared field arrays.panel.isel(...)andpanel.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 ofvaluesmust 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 selectslevels[0], code 1 selectslevels[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_maskentries 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 fromactive_mask.
- bfill(fields, *, limit=None, inplace=True)[source]#
Backward fill NaN values along the observation axis.
- Parameters:
- fieldsstr or iterable of str
Numeric
Field2Dnames 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. IfFalse, 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. IfFalse, 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
FieldCategoricalfield.- 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)whenbyis 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_maskis re-enforced as a subset ofactive_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
Field2Dnames 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. IfFalse, 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 anAssetPanelViewis returned.
- Returns:
- panel or viewAssetPanel or AssetPanelView
Observation-only selections return a view. Selections that slice assets return a new 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.loadfor field and mask arrays. Userfor 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. IfFalse, name conflicts raiseKeyError.
- Returns:
- selfAssetPanel
The modified panel.
- save(path, *, overwrite=False)[source]#
Save the panel to a directory of
.npyfiles.The directory contains one
.npyfile per field, small metadata files for categorical and third-axis labels and a_metadata.jsonmanifest. Object-dtype axis labels and field metadata labels are converted to strings so the panel is loaded withallow_pickle=False.- Parameters:
- pathstr or pathlib.Path
Destination directory.
- overwritebool, default=False
If
True, replace an existing saved panel atpath. Existing directories that do not contain_metadata.jsonare 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 AssetPanelViewis returned.
- Asset labels to select. If
- 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
labelsorgroupsmust 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
labelsselection 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.
Field3Dentries are skipped with a warning when multiple fields are converted. Selecting a singleField3DraisesValueError.- 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
fieldsis not a single string. In long format, rows are indexed by(observation, asset)and filtered byactive_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.