Source code for kaxanuk.attribution_analysis.modules.synthetic_exposures_creator
import datetime
import logging
from collections.abc import Collection
from pathlib import Path
import numpy as np
import pandas as pd
logger = logging.getLogger(__name__)
# --- synthetic exposure generator ---
[docs]
def generate_synthetic_exposure_dict(
portfolio_weights: pd.DataFrame,
universe_returns: pd.DataFrame,
date_list: list[str] | None = None,
factor_index: Collection[str] = (
"ci_intercept",
"nc_beta",
"nc_log_market_cap",
"nc_momentum",
"nc_resid_vol",
"nc_value",
),
seed: int | None = 42,
dtype: str = "float32",
na_percentage: float = 0.0,
) -> dict[datetime.date, pd.DataFrame]:
"""
Create an in-memory synthetic exposure dictionary that mirrors the real data shape,
with an optional percentage of random NaN values injected.
Parameters
----------
portfolio_weights : pd.DataFrame
DataFrame with dates as index and assets as columns. Columns are used for synthetic exposure columns.
universe_returns : pd.DataFrame
DataFrame containing universe assets (used for column reference).
date_list : list[str] | None
Dates to generate (format '%Y-%m-%d'). If None, use all dates from portfolio_weights.index.
factor_index : iterable[str]
Row index (factors) for each synthetic exposure matrix.
seed : int | None
RNG seed for reproducibility. Use None for non-deterministic draws.
dtype : str
Numpy/pandas dtype for the generated values.
na_percentage : float
Fraction (0-1) of entries to replace with NaN randomly. Default = 0 (no missing data).
Returns
-------
dict[datetime.date, pd.DataFrame]
Keys are datetime.date objects, values are DataFrames with index=factor_index and columns=portfolio_weights.columns.
"""
rng = np.random.default_rng(seed)
# Choose dates
if date_list is None:
date_strs = list(pd.to_datetime(portfolio_weights.index).strftime("%Y-%m-%d"))
else:
date_strs = list(date_list)
assets = list(universe_returns.columns)
n_factors = len(factor_index)
n_assets = len(assets)
exposure_dict = {}
for ds in date_strs:
# Draw standard normals (z-score-like)
data = rng.standard_normal(size=(n_factors, n_assets)).astype(dtype, copy=False)
# Optionally inject NaNs
if na_percentage > 0:
n_total = data.size
n_missing = int(n_total * na_percentage)
if n_missing > 0:
missing_idx = rng.choice(n_total, size=n_missing, replace=False)
data.flat[missing_idx] = np.nan
# Build DataFrame
df = pd.DataFrame(data, index=list(factor_index), columns=assets)
exposure_dict[ds] = df
return exposure_dict
[docs]
def stock_factor_returns_loader(
f_rets_by_stock_path: str,
files_to_load: list[str],
first_day_weights: pd.Timestamp,
) -> dict[str, pd.DataFrame]:
"""
Load per-stock factor-returns CSV files into a dict of DataFrames.
Parameters
----------
f_rets_by_stock_path : str
Directory containing one CSV file per stock.
files_to_load : list[str]
File names (with extension) to load from `f_rets_by_stock_path`.
The stock name is derived from each file name (text before the
first ``.``).
first_day_weights : pd.Timestamp
Earliest date to keep; each file is sliced from this date
onward.
Returns
-------
dict[str, pd.DataFrame]
Mapping of stock name to its factor-returns DataFrame, indexed
by date. Files listed in `files_to_load` that are not found are
logged as a warning and omitted from the result.
"""
by_stock_factor_returns = {}
missing_files = []
for file in files_to_load:
try:
full_path = Path(f_rets_by_stock_path) / file
df = pd.read_csv(
full_path,
index_col=0,
parse_dates=[0],
date_format='%Y-%m-%d'
)
stock = file.split('.')[0]
by_stock_factor_returns[stock] = df.loc[first_day_weights:]
except FileNotFoundError:
missing_files.append(file)
if missing_files:
logger.warning("%d files not found: %s", len(missing_files), missing_files)
# break
return by_stock_factor_returns