Cross-Section Factor Models - High-performance Python implementation
Project description
facmodCS: Cross-Sectional Factor Models in Python
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 operationspandas- Data manipulationnumpy- Numerical computingscipy- Scientific computingstatsmodels- Statistical modelsplotly- Interactive visualizationsnumba- 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:
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 varianceEWMA: Exponentially weighted moving average (default λ=0.9)RobustEWMA: Robust EWMA with outlier rejectionGARCH: 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 & visualizationexamples/end_to_end_analysis.ipynb- Jupyter notebook with full workflowexamples/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.csv⭐ NEW - 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:
- Fork the repository
- Create a feature branch
- Write tests for new functionality
- Ensure all tests pass:
pytest tests/ -v - 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:
- Mido Shammaa (Author, Creator) - midoshammaa@yahoo.com
- Doug Martin (Contributor, Author) - martinrd3d@gmail.com
- Kirk Li (Author, Contributor) - cocokecoli@gmail.com
- Avinash Acharya (Contributor) - Jon.Spinney@vestcor.org
- Lingjie Yi (Contributor) - lingjy.uw@gmail.com
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.mdfor 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6a8253ae2071e3fdbd279eb4c0152f9b972a73a32fa18b22519f57c39e8156a1
|
|
| MD5 |
d9ba77e5b3781bdb2d6373cf7ad0d87f
|
|
| BLAKE2b-256 |
e94b6f81e756bb020b1bba6cbf0fb7ed4ec873f1f2fd1e76f28d5829cc66c216
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
562fe3e85edc10417b8c0ef053cefe2429f48fdce51785da7434e740c9a68263
|
|
| MD5 |
b32be44164ebb688504eb4867ccd9e63
|
|
| BLAKE2b-256 |
d713d98a5fa352d0a3109cefcec8bfc9d56fe7514940165b447efd5f456883b0
|