Skip to main content

Cross-Section Factor Models - High-performance Python implementation

Project description

facmodCS: Cross-Sectional Factor Models in Python

Python 3.9+ Code Coverage Tests

A comprehensive Python implementation of cross-sectional fundamental factor models, providing tools for factor model estimation, risk decomposition, and portfolio analysis.

This package is a Python translation of the R facmodCS package, offering full feature parity with enhanced interactive visualizations using Plotly.


Features

Core Capabilities

  • Multiple Estimation Methods: OLS, WLS, Robust (MM-estimation), Weighted Robust
  • Risk Decomposition: Standard Deviation, Value-at-Risk (VaR), Expected Shortfall (ES)
  • Factor Analysis: Exposure analysis, factor returns, correlation matrices
  • Portfolio Analytics: Performance attribution, risk decomposition by factor
  • Interactive Visualizations: 10+ Plotly-based charts for model diagnostics
  • Comprehensive Reporting: Automated reports with statistics and insights

Key Features

Cross-sectional regression at each time period ✅ Categorical variables (sectors, industries) ✅ Exposure standardization (cross-sectional, time-series) ✅ Variance estimation (EWMA, Robust EWMA, GARCH) ✅ Euler risk decomposition with marginal, component, and percentage contributions ✅ Production-ready with 86% test coverage and 223 passing tests


Installation

Prerequisites

  • Python 3.9 or higher
  • UV package manager (recommended) or pip

Install from Source

# Clone the repository
cd facmodcs_py

# Install with UV (recommended)
uv pip install -e ".[test]"

# Or install with pip
pip install -e ".[test]"

Dependencies

Core dependencies:

  • polars - Fast DataFrame operations
  • pandas - Data manipulation
  • numpy - Numerical computing
  • scipy - Scientific computing
  • statsmodels - Statistical models
  • plotly - Interactive visualizations
  • numba - JIT compilation for performance

Quick Start

Basic Usage

import polars as pl
from facmodcs import fit_ffm, fm_sd_decomp, port_sd_decomp
from facmodcs import plot_factor_returns, create_model_dashboard

# Load your panel data (balanced panel: Date, Asset, Return, Exposures)
data = pl.read_csv("panel_data.csv")

# Fit a cross-sectional factor model
model = fit_ffm(
    data=data,
    asset_var="Asset",
    ret_var="Return",
    date_var="Date",
    exposure_vars=["BP", "PM12M1M", "Beta60M"],  # Your factor exposures
    fit_method="LS",                              # LS, WLS, Rob, or W-Rob
    standardize="CrossSection",                   # Standardize exposures
    lag_exposures_flag=True,                      # Lag exposures by 1 period
)

# View model summary
print(model.summary())

# Access fitted components
beta_matrix = model.beta              # (N × K) factor exposures
factor_returns = model.factor_returns # (T × K) factor returns
residuals = model.residuals           # (N × T) residual matrix
r_squared = model.r_squared           # (T,) R-squared by period

# Risk decomposition (asset-level)
sd_decomp = fm_sd_decomp(model)
first_asset = model.asset_names[0]
print(sd_decomp[first_asset].summary())

# Portfolio risk decomposition
import numpy as np
weights = np.ones(model.n_assets) / model.n_assets  # Equal weights
port_sd = port_sd_decomp(model, weights)
print(f"Portfolio SD: {port_sd.total_risk:.4f}")
print(f"Factor contributions: {port_sd.percentage}")

Interactive Visualization

from facmodcs import (
    plot_factor_returns,
    plot_cumulative_factor_returns,
    plot_sd_decomposition,
    create_model_dashboard,
)

# Plot factor returns over time
fig = plot_factor_returns(model)
fig.show()

# Plot cumulative factor returns
fig = plot_cumulative_factor_returns(model)
fig.show()

# Plot SD decomposition for top 10 risky assets
fig = plot_sd_decomposition(model, top_n=10)
fig.show()

# Create comprehensive dashboard
fig = create_model_dashboard(model, weights)
fig.show()

# Save plots to HTML
fig.write_html("model_dashboard.html")

Main Entry Points

1. Model Fitting

fit_ffm() - Main fitting function

from facmodcs import fit_ffm

model = fit_ffm(
    data: pl.DataFrame,           # Input panel data
    asset_var: str,               # Asset identifier column
    ret_var: str,                 # Return column
    date_var: str,                # Date column
    exposure_vars: list[str],     # List of exposure variables
    fit_method: str = "LS",       # LS, WLS, Rob, W-Rob
    standardize: str = "CrossSection",  # CrossSection, TimeSeries, or none
    lag_exposures_flag: bool = True,    # Lag exposures by 1 period
    var_model: str = "EWMA",      # For WLS: StdDev, EWMA, RobustEWMA, GARCH
    **kwargs
) -> FactorModel

Returns: FactorModel object with:

  • beta: Factor exposures (N × K)
  • factor_returns: Factor returns (T × K)
  • residuals: Residuals (N × T)
  • r_squared: R² values (T,)
  • factor_cov: Factor covariance matrix (K × K)
  • resid_var: Residual variances (N,)

2. Risk Decomposition

Asset-Level Decomposition

from facmodcs import fm_sd_decomp, fm_var_decomp, fm_es_decomp

# Standard Deviation decomposition
sd_decomp = fm_sd_decomp(model)
# Returns: dict[str, DecompositionResult]
# Each asset has: total_risk, component, marginal, percentage

# Value-at-Risk decomposition
var_decomp = fm_var_decomp(model, p=0.05, type="np")
# type: "np" (non-parametric) or "normal" (parametric)

# Expected Shortfall decomposition
es_decomp = fm_es_decomp(model, p=0.05, type="np")

Portfolio-Level Decomposition

from facmodcs import port_sd_decomp, port_var_decomp, port_es_decomp

weights = np.ones(model.n_assets) / model.n_assets  # Portfolio weights

# Portfolio SD decomposition
port_sd = port_sd_decomp(model, weights)
print(f"Total Portfolio SD: {port_sd.total_risk:.4f}")
print(f"Factor contributions (%): {port_sd.percentage}")

# Portfolio VaR decomposition
port_var = port_var_decomp(model, weights, p=0.05, type="np")

# Portfolio ES decomposition
port_es = port_es_decomp(model, weights, p=0.05, type="np")

3. Post-Fitting Analysis

from facmodcs import fm_cov, fm_rsq, fm_tstats, vif, summary_statistics

# Covariance matrix
cov_matrix = fm_cov(model)  # (N × N) asset return covariance

# R-squared statistics
rsq_stats = fm_rsq(model, adj_rsq=True)
print(f"Mean R²: {rsq_stats['mean_r_squared']:.4f}")
print(f"Mean Adj. R²: {rsq_stats['mean_adj_r_squared']:.4f}")

# T-statistics for factor significance
tstats = fm_tstats(model, z_alpha=1.96)  # 95% confidence
print(f"Significant factors: {tstats['total_significant']}")

# Variance Inflation Factors (multicollinearity check)
vif_results = vif(model)
for factor, mean_vif in vif_results['mean_vif'].items():
    print(f"{factor}: VIF = {mean_vif:.2f}")

# Comprehensive summary
summary_df = summary_statistics(model)
print(summary_df)

4. Reporting Functions

from facmodcs import (
    rep_exposures,
    rep_return,
    rep_risk,
    model_summary_report,
    factor_correlation_report,
    performance_attribution_summary,
)

# Exposure summary (mean, std, min, max, VIF)
exposure_report = rep_exposures(model)
print(exposure_report)

# Return attribution (factor contributions to returns)
asset = model.asset_names[0]
return_attr = rep_return(model, asset=asset)
print(return_attr.head(10))

# Risk decomposition report (top N risky assets)
risk_reports = rep_risk(model, risk_measures=["SD", "VaR", "ES"], p=0.05, top_n=10)
print(risk_reports['SD'])  # Top 10 by SD
print(risk_reports['VaR']) # Top 10 by VaR

# Model summary (comprehensive text report)
summary_text = model_summary_report(model)
print(summary_text)

# Factor correlation matrix
corr_matrix = factor_correlation_report(model)
print(corr_matrix)

# Performance attribution (portfolio level)
perf_attr = performance_attribution_summary(model, weights)
print(perf_attr)

5. Plotting Functions

All plotting functions return Plotly Figure objects with interactive features (zoom, pan, hover).

from facmodcs import (
    plot_factor_returns,            # Time series of factor returns
    plot_cumulative_factor_returns, # Cumulative returns
    plot_r_squared,                 # R² over time
    plot_factor_distribution,       # Histogram with normal curve
    plot_sd_decomposition,          # Stacked bar chart (top N assets)
    plot_portfolio_risk_decomposition,  # Waterfall chart
    plot_risk_comparison,           # Grouped bar chart (SD/VaR/ES)
    plot_exposure_heatmap,          # Factor exposure heatmap
    plot_residual_diagnostics,      # 4-panel diagnostic plots
    create_model_dashboard,         # Comprehensive 6-panel dashboard
)

# Factor returns time series
fig = plot_factor_returns(model, factors=["BP", "PM12M1M"])
fig.show()

# Cumulative returns (compound returns)
fig = plot_cumulative_factor_returns(model)
fig.write_html("cumulative_returns.html")

# R-squared over time
fig = plot_r_squared(model)
fig.show()

# Factor distribution with normal overlay
fig = plot_factor_distribution(model, factor="BP")
fig.show()

# SD decomposition (top 10 assets)
fig = plot_sd_decomposition(model, top_n=10)
fig.show()

# Portfolio risk decomposition (waterfall chart)
fig = plot_portfolio_risk_decomposition(model, weights, risk_measure="SD")
fig.show()

# Risk comparison (SD vs VaR vs ES)
fig = plot_risk_comparison(model, weights, p=0.05)
fig.show()

# Exposure heatmap
fig = plot_exposure_heatmap(model, n_assets=20)
fig.show()

# Residual diagnostics (time series, histogram, Q-Q, ACF)
fig = plot_residual_diagnostics(model, asset=model.asset_names[0])
fig.show()

# Comprehensive dashboard (6 panels)
fig = create_model_dashboard(model, weights)
fig.write_html("dashboard.html")

Example Visualizations:

Factor Returns SD Decomposition Model Dashboard


Data Requirements

Input data must be a balanced panel with the following structure:

Date Asset Return BP PM12M1M Beta60M Sector
2020-01-31 AAPL 0.0523 1.23 0.45 1.05 Tech
2020-01-31 MSFT 0.0412 1.45 0.38 0.98 Tech
... ... ... ... ... ... ...
2020-02-29 AAPL 0.0234 1.25 0.48 1.06 Tech

Requirements:

  • Balanced panel: All assets present in all time periods
  • Date column: Convertible to date type
  • Asset column: Unique identifiers
  • Return column: Numeric returns
  • Exposure columns: Numeric (continuous) or categorical (sectors)

Data Formats Supported:

  • Polars DataFrame (preferred for performance)
  • Pandas DataFrame (automatically converted)
  • CSV files (via pl.read_csv())

Estimation Methods

1. Ordinary Least Squares (LS)

model = fit_ffm(data, ..., fit_method="LS")

Standard OLS cross-sectional regression at each time period.

Pros: Fast, unbiased estimates Cons: Sensitive to outliers


2. Weighted Least Squares (WLS)

model = fit_ffm(data, ..., fit_method="WLS", var_model="EWMA")

Two-pass estimation with inverse variance weights.

Variance Models:

  • StdDev: Sample variance
  • EWMA: Exponentially weighted moving average (default λ=0.9)
  • RobustEWMA: Robust EWMA with outlier rejection
  • GARCH: GARCH(1,1) conditional variance

Pros: Accounts for heteroskedasticity Cons: Requires first-pass estimates


3. Robust Regression (Rob)

model = fit_ffm(data, ..., fit_method="Rob")

MM-estimation with Huber loss function.

Pros: Robust to outliers Cons: Slower convergence


4. Weighted Robust (W-Rob)

model = fit_ffm(data, ..., fit_method="W-Rob", var_model="RobustEWMA")

Combines WLS weighting with robust estimation.

Pros: Best of both worlds Cons: Most computationally intensive


Examples

See the examples/ directory for complete demonstrations:

  • examples/phase5_demo.py - Comprehensive demo of reporting & visualization
  • examples/end_to_end_analysis.ipynb - Jupyter notebook with full workflow
  • examples/plots/ - 10 interactive HTML plots

Run the Demo

python examples/phase5_demo.py

This generates:

  • 6 console reports (model summary, exposures, correlations, etc.)
  • 10 interactive HTML plots in examples/plots/

Testing

Run All Tests

pytest tests/ -v --cov=facmodcs --cov-report=html

Current Status:

  • ✅ 223 tests passing
  • ✅ 86% code coverage
  • ✅ 0 failures

Test Categories

  • Unit tests: Individual function testing
  • Integration tests: End-to-end workflows
  • Regression tests: All method combinations
  • Edge cases: Missing data, outliers, numerical stability
  • Performance tests: Benchmarks for speed and memory
  • R comparison (Real Data): Validation against R package using stocks_factors.csvNEW
  • R comparison (Synthetic Data): Validation against R package using generated data

Testing with Real Data

The test suite includes comprehensive R-Python comparison tests using a common real-world dataset (stocks_factors.csv). This ensures both implementations produce identical results on the same data:

Dataset Details:

  • 294 US stocks from 2006-2010 (monthly)
  • Real-world patterns: Same data used in R package examples
  • No seed differences: Eliminates random number generation issues
  • Comprehensive: Validates fitting, decomposition, and portfolio analysis

Why Real Data?

  • ✅ Both implementations handle real-world patterns identically
  • ✅ No synthetic data generation differences (R vs Python seeds differ)
  • ✅ Reproducible results across languages
  • ✅ Validation against published R results

Run R Comparison Tests on Real Data:

# Requires: R installed, facmodCS R package, rpy2
pytest tests/test_r_comparison_real_data.py --run-r-comparison -v

# Install R package (if needed)
R -e "install.packages('facmodCS', repos='https://cloud.r-project.org')"

See: tests/README_TESTING.md for comprehensive testing guide

Run Specific Tests

# Real data R comparison (RECOMMENDED for validation)
pytest tests/test_r_comparison_real_data.py --run-r-comparison -v

# Synthetic data R comparison
pytest tests/test_r_comparison.py --run-r-comparison -v

# Seed behavior and reproducibility
pytest tests/test_seed_equivalence.py -v

# Plotting tests
pytest tests/test_plotting.py -v

# Reporting tests
pytest tests/test_reporting.py -v

# Performance benchmarks
pytest tests/test_performance.py -v -s

Random Seed Handling

IMPORTANT: R's set.seed() and Python's np.random.seed() produce different values even with the same seed number due to different RNG algorithms.

Best Practice:

  • Use real data (stocks_factors.csv) for R-Python comparison
  • Use synthetic data for Python-only edge case tests
  • Don't compare R and Python synthetic data directly

See: docs/SEED_HANDLING.md for detailed explanation


Performance

Benchmarks (on medium dataset: 100 assets, 60 periods):

Operation Time Target
LS Fitting 1.2s < 5s
SD Decomposition 0.3s < 1s
Portfolio SD 0.02s < 0.1s
Full Workflow 3.5s < 10s

Memory Usage: < 500 MB for large models (500 assets, 120 periods)

Optimizations:

  • Polars for fast data operations
  • Numba JIT compilation for hot loops
  • Efficient matrix operations with NumPy
  • Lazy evaluation where possible

Project Structure

facmodcs_py/
├── facmodcs/              # Main package
│   ├── __init__.py        # Public API
│   ├── core.py            # Fitting functions (~380 lines)
│   ├── covariance.py      # Post-fitting analysis (~100 lines)
│   ├── decomposition.py   # Risk decomposition (~250 lines)
│   ├── plotting.py        # Visualizations (~180 lines)
│   ├── reporting.py       # Reports (~170 lines)
│   ├── models.py          # Data classes (~115 lines)
│   └── utils.py           # Utilities (~75 lines)
├── tests/                 # Test suite (~2,000 lines)
│   ├── conftest.py        # Test configuration
│   ├── test_core.py       # Core fitting tests
│   ├── test_decomposition.py
│   ├── test_plotting.py
│   ├── test_reporting.py
│   ├── test_regression_suite.py
│   ├── test_edge_cases.py
│   ├── test_performance.py
│   ├── test_r_comparison.py
│   └── fixtures/
│       └── synthetic_data_generator.py
├── examples/              # Examples and demos
│   ├── phase5_demo.py
│   ├── end_to_end_analysis.ipynb
│   └── plots/             # Generated plots
├── pyproject.toml         # Package configuration
├── requirements.txt       # Dependencies
├── README.md             # This file
└── IMPLEMENTATION_STATUS.md  # Detailed implementation notes

Advanced Usage

Custom Preprocessing

from facmodcs import lag_exposures, standardize_exposures

# Lag exposures manually
lagged_data = lag_exposures(data, asset_var="Asset", date_var="Date",
                             exposures=["BP", "PM12M1M"])

# Standardize with custom method
standardized_data = standardize_exposures(
    lagged_data,
    date_var="Date",
    exposures_num=["BP", "PM12M1M"],
    method="TimeSeries",  # or CrossSection
    lambda_ewma=0.9,      # EWMA decay
)

Model Customization

# Fit with specific options
model = fit_ffm(
    data=data,
    asset_var="Asset",
    ret_var="Return",
    date_var="Date",
    exposure_vars=["Sector", "BP", "PM12M1M"],  # Mix categorical + numeric
    fit_method="W-Rob",
    var_model="RobustEWMA",
    lambda_ewma=0.94,           # Custom EWMA decay
    robust_scale_threshold=2.5, # Outlier threshold
    max_iter=50,                # Robust regression iterations
    add_intercept=True,         # Include intercept
    standardize="CrossSection",
    lag_exposures_flag=True,
)

Batch Processing

# Fit multiple models
models = {}
for method in ["LS", "WLS", "Rob"]:
    models[method] = fit_ffm(data, ..., fit_method=method)

# Compare model performance
for method, model in models.items():
    rsq_stats = fm_rsq(model, adj_rsq=True)
    print(f"{method}: Mean R² = {rsq_stats['mean_r_squared']:.4f}")

Contributing

Contributions are welcome! Please follow these guidelines:

  1. Fork the repository
  2. Create a feature branch
  3. Write tests for new functionality
  4. Ensure all tests pass: pytest tests/ -v
  5. Submit a pull request

Development Setup:

# Install in development mode with test dependencies
uv pip install -e ".[test]"

# Run tests with coverage
pytest tests/ -v --cov=facmodcs --cov-report=html

# View coverage report
open htmlcov/index.html

Citation

If you use this package in academic research please cite the original authors. The python implementation is below:

@software{facmodcs_python,
  title = {facmodCS: Cross-Sectional Factor Models in Python},
  author = {Hayes, Drew},
  year = {2025},
  url = {https://github.com/Druhayes/facmodcs}
}

Include the original authors of the R package as well

Original R package:

@manual{facmodCS_R,
  title = {facmodCS: Cross-Sectional Factor Models},
  author = {PCRA Team},
  year = {2023},
  note = {R package}
}

Acknowledgments

Original Authors

This codebase was written by Claude Sonnet 4.5 with Claude Code.

The original R facmodCS package (version 1.0.3) was authored by:

Special thanks to:

  • Chicago Research on Security Prices, LLC for the cross-section of about 300 CRSP stocks data
  • S&P GLOBAL MARKET INTELLIGENCE for contributing 14 factor scores (alpha factors and factor exposures) fundamental data

Technology Stack

  • Built with Polars, NumPy, SciPy, Statsmodels, and Plotly
  • Testing framework using pytest and rpy2

Support

For questions, issues, or feature requests:

  • Issues: GitHub Issues
  • Documentation: See IMPLEMENTATION_STATUS.md for detailed implementation notes
  • Examples: See examples/ directory

Roadmap

Future enhancements:

  • Rolling window estimation
  • Out-of-sample prediction
  • Additional risk measures (CVaR, drawdown)
  • Style analysis tools
  • Performance dashboards
  • API for real-time data feeds

Version: 0.1.0 Status: Production Ready Python: 3.9+ Last Updated: January 2025

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

facmodcs-0.1.1.tar.gz (2.5 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

facmodcs-0.1.1-py3-none-any.whl (40.7 kB view details)

Uploaded Python 3

File details

Details for the file facmodcs-0.1.1.tar.gz.

File metadata

  • Download URL: facmodcs-0.1.1.tar.gz
  • Upload date:
  • Size: 2.5 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.27 {"installer":{"name":"uv","version":"0.9.27","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for facmodcs-0.1.1.tar.gz
Algorithm Hash digest
SHA256 6a8253ae2071e3fdbd279eb4c0152f9b972a73a32fa18b22519f57c39e8156a1
MD5 d9ba77e5b3781bdb2d6373cf7ad0d87f
BLAKE2b-256 e94b6f81e756bb020b1bba6cbf0fb7ed4ec873f1f2fd1e76f28d5829cc66c216

See more details on using hashes here.

File details

Details for the file facmodcs-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: facmodcs-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 40.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.27 {"installer":{"name":"uv","version":"0.9.27","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for facmodcs-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 562fe3e85edc10417b8c0ef053cefe2429f48fdce51785da7434e740c9a68263
MD5 b32be44164ebb688504eb4867ccd9e63
BLAKE2b-256 d713d98a5fa352d0a3109cefcec8bfc9d56fe7514940165b447efd5f456883b0

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page