Python library alternatives: what to keep, what to switch¶
For every ported module, this document names the closest existing Python library alternative (if one exists), makes an honest call on whether to keep the ported code, switch to the library, or use both for different purposes, and — for anything worth keeping — asks a further question: could the gap it fills be contributed back upstream, so the alternative library covers it too, instead of it staying locked inside this port? The guiding question throughout the first three columns is: does the ported code do something the alternative genuinely can't, or is it mostly duplicating well-trodden ground? The Upstream potential column then asks, for whatever survives that question: is this a clean enough, generalizable enough gap that a maintainer would plausibly accept it, or is it too tied to this port's specific conventions to be worth proposing?
Three verdicts are used in the Verdict column:
- KEEP — the ported code does something the alternative doesn't (a specific parameterization, a quant-specific measure, a missing feature), so there's a real reason to maintain it.
- SWITCH — a mature, well-tested library already does this better; the ported code is mostly worth keeping as a reference/fallback, not as the primary path.
- HYBRID — use the library for the common case, keep the ported code for a specific capability it has that the library lacks.
A SWITCH verdict almost always means N/A in the Upstream potential column: if the alternative library already does the job better, there's nothing this port has to offer it back. Upstream potential mostly applies to KEEP and HYBRID cases, where this port has a capability the alternative lacks.
Dates (dates/)¶
| Module | Alternative | Verdict | Why | Upstream potential |
|---|---|---|---|---|
convert.py |
pandas (already the base) |
KEEP | Pandas has no built-in Excel-serial-date conversion; this is a thin, genuinely useful wrapper. | Thin enough (one conversion function) to read as a personal utility rather than a generalizable pandas capability — not a strong candidate on its own. |
rebalancing.py |
pandas_market_calendars |
HYBRID | Our rebalancing dates are weekday-only (no exchange holiday calendar) — that's a real gap versus the original MATLAB behavior too. pandas_market_calendars gives real NYSE/LSE/etc. holiday calendars; worth wiring in as an optional calendar source for production backtests, while keeping our nearest-available-date snapping logic (which the library doesn't replicate exactly). |
The nearest-available-date snapping logic has no direct equivalent in pandas_market_calendars either — worth raising as a feature request there once the HYBRID switch is done, rather than upstreaming preemptively. |
Stats (stats/)¶
| Module | Alternative | Verdict | Why | Upstream potential |
|---|---|---|---|---|
distributions.py — simple wrappers (normal/t/chi2/F/MVN) |
scipy.stats (already the backend) |
KEEP | These are one-line wrappers around scipy; no separate module needed downstream, but no reason to remove them either — call-site compatibility with the original toolbox. | Not worth upstreaming — kept only for call-site compatibility, not a capability gap in scipy. |
distributions.py — GQF1/GQF2 |
(none) | KEEP | Genuinely niche (generalized quadratic-form distributions for Delta-Gamma VaR). Nothing in scipy, statsmodels, or elsewhere in the ecosystem does this. | No clean existing home found. scipy.stats is conservative about adding new distribution families, and no general econometrics/risk package is a natural fit either — more realistic as its own small standalone release than a PR into an existing library. |
distributions.py — beta/lognormal/inverse-Gaussian wrappers |
scipy.stats.{beta,lognorm,invgauss} (already the backend, or algebraically equivalent under reparameterization) |
KEEP (as call-site convenience) | Same reasoning as the simple normal/t/chi2/F wrappers — verified numerically equivalent to the scipy distributions under each one's own shape/scale convention; kept so callers don't have to compute that reparameterization themselves. | Not worth upstreaming — no capability gap. |
distributions.py — bates_cdf/bates_pdf |
(none) | KEEP | scipy.stats has no named Bates distribution (mean of n iid Uniform(0,1) variables). |
Small, self-contained (a finite sum with scipy.special.comb) — a plausible scipy.stats PR, but a genuinely new distribution addition is a scoping conversation with scipy maintainers, not a drive-by PR. |
distributions.py — poisson_binomial_pmf |
scipy.stats.poisson_binom (available since SciPy ~1.17) |
SWITCH | Verified numerically identical to the original's own FFT and direct-recursion branches — no reason to hand-roll either. | N/A — scipy already covers this. |
distributions.py — order_statistic_cdf/order_statistic_ppf |
scipy.stats.binom.sf (for the CDF formula) |
HYBRID | The order-statistic CDF is exactly scipy.stats.binom.sf(i-1, n, F_x) — evaluated via that rather than hand-computed binomial coefficients (more numerically stable for large n). The quantile version (grid search over sample points) has no scipy equivalent — kept as a hand-ported utility. |
The CDF formula itself isn't a gap (it's one line of scipy.stats.binom); the grid-search quantile utility is too narrow/implementation-specific to propose upstream. |
distributions.py — normal_ratio_cdf/normal_ratio_pdf (Hinkley's ratio distribution) |
(none) | KEEP | The distribution of the ratio of two independent normals has no scipy equivalent (scipy has no "ratio distribution" family). | Niche enough (a two-normal-ratio distribution, used here for risk-model diagnostics) that no obvious library is a natural home. |
distributions.py — skew-normal (skew_normal_{cdf,ppf,pdf,moments,rvs}) |
scipy.stats.skewnorm (already the backend) |
SWITCH | Verified numerically exact match (pdf, cdf, and moments="mvsk" all agree under the direct eta/xi/omega -> a/loc/scale mapping) — the original's dual numerical CDF branches and hand-rolled Newton-iteration quantile function are unnecessary once routed through scipy. |
N/A — scipy already covers this exactly. |
distributions.py — skew-t (skew_t_{cdf,ppf,pdf,moments,rvs}) |
scipy.stats.jf_skew_t (Jones & Faddy's skew-t — checked and confirmed not the same family) |
KEEP | This is Azzalini's skew-t (one skewness parameter + degrees of freedom); scipy's only skew-t is a different two-shape-parameter family (Jones & Faddy) with an incompatible parameterization. No scipy equivalent for Azzalini's version. skew_t_cdf is itself backed by bvt_cdf (see the genz/ row below) -- a downstream consequence surfaced during a later review pass: skew_t_ppf's Newton loop originally targeted tol=1e-8, which bvt_cdf's ~1e-4-noisy QMC integrator can never satisfy, so every call silently burned its full iteration budget for no accuracy gain (~4x slower than necessary). Fixed by loosening the default to tol=1e-4, matching the CDF's actual achievable precision -- see the function's docstring. |
Genuinely missing from scipy.stats — a plausible feature request, but skew-t already exists there under a different family, so framing (and possibly naming) would need care; a scoping conversation, not a drive-by PR. |
multivariate.py — bvn_cdf/bvn_pdf/bvt_cdf |
scipy.stats.multivariate_normal/multivariate_t (already the backend) |
KEEP (as a call-site convenience) | Thin wrappers matching the original's bivariate (x, y, rho[, nu]) signature; genuinely one-line passthroughs, same reasoning as the simple distributions.py wrappers above. |
Not worth upstreaming — no capability gap, just a narrower call signature. |
multivariate.py — the 8 low-level genz/ quasi-Monte-Carlo integrators (qsimvn, qsimvt, qsilatmvnv, qsimvnauto, mvnrcnv, ...) |
scipy.stats.multivariate_normal.cdf/multivariate_t.cdf |
SWITCH | These hand-roll exactly the arbitrary-dimension MVN/MVT orthant-probability problem scipy already solves, via the same Genz (1992) algorithm family — scipy's multivariate_normal.cdf is itself built on Alan Genz's own Fortran mvndst routine. Confirmed empirically too: multivariate_t.cdf shows the same call-to-call randomized-QMC noise (~1e-4) the original's [p, e] error-estimate return signature implies. Reimplementing these would duplicate already-available, better-tested code with zero functional gain — not hand-ported at all (see multivariate.py's module docstring). |
N/A — scipy already covers this via the same author's own algorithm. |
moments.py — rolling_correlation/rolling_volatility |
pandas.DataFrame.rolling().corr()/.std() |
SWITCH | Pandas' rolling ops are implemented in Cython and are meaningfully faster than our Python-loop version for anything beyond toy sizes. The only reason to keep ours is the specific method=2 "compute returns within each window" variant for illiquid series, which pandas doesn't offer directly. |
N/A for the switched core. The method=2 variant is real but narrow enough (illiquid-series-specific) that it's not obviously worth a pandas PR — a documented recipe is more realistic than a new .rolling() option. |
moments.py — active_share, herfindahl_index, asynchronous_cov, weekly_cov |
(none) | KEEP | Portfolio-construction-specific measures with no general-purpose library equivalent. | Narrow enough that no single library is a natural home; if pursued, active_share specifically fits performance-analytics packages like empyrical/pyfolio-reloaded better than a general stats library. |
regression/ols.py |
statsmodels.OLS/WLS |
HYBRID | For plain (unrestricted) OLS, statsmodels is more complete (more diagnostics, better-tested edge cases). Ours is worth keeping specifically for the restriction=(RR, r) linear-restriction parameterization, which statsmodels doesn't support as directly. |
statsmodels' API design opinions run strong; worth raising as an issue before assuming a PR would land. |
regression/ridge.py |
sklearn.linear_model.Ridge/RidgeCV |
HYBRID | sklearn's solver is more optimized for large/sparse problems. Keep ours for the ridge_tau_targeted (L2-norm-budget, not penalty) parameterization — sklearn has no equivalent for that. |
ridge_tau_targeted is an L2-budget parameterization; sklearn core is conservative about scope creep, so a scikit-learn-contrib package is the more realistic landing spot than sklearn itself. |
regression/lasso.py |
sklearn.linear_model.Lasso/ElasticNet/LassoCV |
SWITCH (for the penalized-form solvers) | sklearn's coordinate descent is Cython-compiled and extensively battle-tested; there's no good reason to keep hand-rolled lasso_ccd/lasso_admm for the standard penalized case — recommend routing those through sklearn directly. KEEP lasso_tau_constrained specifically, since sklearn has no L1-budget (as opposed to L1-penalty) interface. |
N/A for the switched solvers. lasso_tau_constrained (L1-budget) is the one piece worth offering — again more realistic via scikit-learn-contrib than sklearn core. |
regression/kernel.py |
statsmodels.nonparametric.KernelReg |
HYBRID | statsmodels' version supports automatic bandwidth selection via cross-validation and both local-constant/local-linear estimators — more complete than our fixed-bandwidth port. Worth switching to for general use; keep ours only where the exact original bandwidth formula needs to be reproduced for parity with existing MATLAB-based results. | Our fixed-bandwidth formula is a strict subset of what KernelReg already does — nothing to contribute back. |
regression/quantile.py |
statsmodels.regression.quantile_regression.QuantReg, or sklearn.linear_model.QuantileRegressor (newer sklearn) |
SWITCH | Both are more battle-tested than our scipy.optimize.linprog-based implementation, handle edge cases (rank-deficient design matrices, ties) more robustly, and QuantReg in particular has been used in production statsmodels code for years. Keep ours only if the exact slack-variable (u, v) outputs are needed downstream. |
N/A — both alternatives already cover this more robustly; the slack-variable output convention is too implementation-specific to propose upstream. |
regression/robust.py |
statsmodels.robust.robust_linear_model.RLM |
HYBRID | RLM covers Huber, Tukey biweight, Andrew's wave, Hampel, and trimmed-mean M-estimators via IRLS — a superset of our Huber implementation, and more robustly tested. It does not cover LAD or quantile M-estimation directly (though QuantReg(q=0.5) is exactly LAD). Recommend: RLM for Huber/general M-estimation, keep our lad_regression/quantile_m_regression/inverse_quantile_m_regression for those specific losses. |
LAD/quantile M-estimation aren't in RLM today — worth raising as a statsmodels issue, though its estimator set is deliberately curated around IRLS M-estimators, so acceptance isn't a given. |
dose_response.py (log-logistic/log-normal/Weibull/hormetic curves) |
(none) | KEEP | scipy has no dose-response-curve module (this is toxicology/ecotoxicology territory, closer to R's drc package than anything in the scipy/statsmodels ecosystem). |
R's drc package is the closest prior art but there's no obvious Python home; niche enough that a standalone small package (mirroring drc's scope) is more realistic than a PR into an existing general-purpose library. |
Econometrics (econometrics/)¶
| Module | Alternative | Verdict | Why | Upstream potential |
|---|---|---|---|---|
estimation.py (GMM/ML) |
statsmodels.sandbox.regression.gmm.GMM, statsmodels.base.model.GenericLikelihoodModel, or the linearmodels package (more modern GMM/IV support) |
HYBRID | These libraries are more mature for standard GMM/MLE use cases. Keep ours specifically for the explicit theta = RR @ gamma + r linear-restriction interface, which none of the alternatives expose as directly — that parameterization is the main reason this module exists as custom code at all. |
Same restriction interface as regression/ols.py above; worth a combined issue/PR rather than two separate proposals, but statsmodels' opinionated API design means this is a discussion, not a quick patch. |
var.py |
statsmodels.tsa.api.VAR |
SWITCH for the unrestricted case | statsmodels' VAR is comprehensive: automatic lag-order selection, impulse response functions, forecast error variance decomposition, forecasting — well beyond what we ported, and it's the standard tool the econometrics community actually uses. KEEP varx_estimate's linear-restriction support (a_eq/b_eq on the stacked coefficient vector) — statsmodels' VAR doesn't support arbitrary parameter restrictions. |
A genuinely useful gap for applied macro/finance users — VAR has no arbitrary-restriction support today — but VAR is mature and stable, so this is a bigger API-design conversation than a quick patch, best opened as an issue first. |
kalman.py |
statsmodels.tsa.statespace.MLEModel/kalman_filter |
SWITCH for anything beyond simple filtering | statsmodels' state-space framework is dramatically more capable: smoothing (not just filtering), MLE parameter fitting built in, diffuse initialization, a compiled Cython backend for real performance on long series. Our kalman_filter is fine for simple, transparent filtering tasks or minimal-dependency use, but statsmodels is the better choice for anything production-scale. |
N/A — statsmodels' framework is already strictly more capable; there's nothing here to contribute back. |
whittle.py |
(none found) | KEEP | Whittle (frequency-domain) estimation isn't implemented in statsmodels, arch, or other common packages. Genuinely fills a gap. |
Strongest candidate in this file. Either a new estimator class following statsmodels' Model/Results convention, or a standalone function in arch. Would need generalizing from this port's specific local-level/local-linear-trend/custom-sdf_fn interface to whatever calling convention the host library expects — substantial enough to warrant a scoping conversation with a maintainer before an unsolicited PR. |
tests.py (ADF) |
statsmodels.tsa.stattools.adfuller (already used) |
SWITCH (already done) | Also worth knowing: the arch package's arch.unitroot module has a wider family of unit-root tests (ADF, Phillips-Perron, DFGLS, KPSS, Zivot-Andrews) if more than ADF is ever needed — not currently ported, but worth knowing about. |
N/A — already routes through statsmodels; nothing custom left to offer back. |
Optimization (optim/)¶
| Module | Alternative | Verdict | Why | Upstream potential |
|---|---|---|---|---|
proximal.py, projection.py |
pyproximal covers some overlapping norm/constraint proximal operators |
KEEP | Low-level building blocks with no strong off-the-shelf equivalent covering our exact operator set (turnover constraints, combined Dykstra-projected linear+box systems for portfolio construction specifically). pyproximal is oriented toward signal-processing/inverse-problems use cases, not portfolio constraints. |
pyproximal's API is built around signal-processing/inverse-problems operators, so our portfolio-specific set (turnover, Dykstra-projected linear+box systems) doesn't map cleanly onto it — a targeted PR isn't obviously easy. More realistic as a standalone niche package if this is ever generalized beyond this port. |
quadprog.py (solve_qp) |
qpsolvers.solve_qp directly (already a declared dependency, currently used underneath cvxpy) |
HYBRID | For hot loops that call solve_qp many times (e.g. risk-budgeting's inner ADMM loop), calling qpsolvers.solve_qp directly — bypassing cvxpy's DSL-parsing overhead — would likely be measurably faster. Worth profiling if performance in tight loops becomes a concern; keep the cvxpy-based version for its more expressive constraint-building (ridge/lasso penalty terms, arbitrary constraint composition), which raw qpsolvers doesn't offer. |
N/A — this module is built on top of qpsolvers/cvxpy rather than replacing anything either one does; there's no independent capability to contribute back. |
bisection.py |
scipy.optimize.brentq/bisect |
HYBRID | scipy's brentq converges faster (superlinear, not just bisection) for scalar root-finding, and is compiled. KEEP our vectorized (array-broadcast, many-roots-at-once) version — scipy's root finders are scalar-only, and vectorizing over many independent brackets is exactly what our version adds. |
The easiest "quick win" of this whole document: small, self-contained, dependency-free. Array-broadcast bisection (many independent brackets solved at once) has come up as a recurring, low-priority SciPy feature request, and this module already does it cleanly. |
Portfolio (portfolio/)¶
| Module | Alternative | Verdict | Why | Upstream potential |
|---|---|---|---|---|
mean_variance.py, black_litterman.py, tracking_error.py |
PyPortfolioOpt |
HYBRID | PyPortfolioOpt is a mature, actively maintained library covering mean-variance optimization, Black-Litterman, CVaR, discrete allocation, and more — genuinely worth using directly for standard portfolio construction. Keep ours for tighter integration with the rest of this codebase (shared solve_qp, direct ridge/lasso penalty composability) and where the exact original parameterization needs to match existing analysis. |
mvo_target_portfolio/te_target_portfolio (bisection-based mu-problem/sigma-problem target-matching, added this cycle) are the candidate. PyPortfolioOpt already has efficient_risk/efficient_return covering similar ground for plain MVO — compare directly before assuming this is additive. The tracking-error-relative version (te_target_portfolio) is the more likely genuine gap, since PyPortfolioOpt's target-matching isn't benchmark-relative in most places. |
risk_budgeting.py |
riskparityportfolio (PyPI, narrower scope) |
KEEP | riskparityportfolio covers basic risk parity but not the box-constrained/general-linear-constrained/VaR-ES/target-matching breadth this module has. This is the single largest and most-tested piece of custom work in the whole port (75+ original files consolidated) — genuinely the strongest "keep" case in this document. |
Propose the constrained solvers as additional solver options on riskparityportfolio rather than pitching a from-scratch new library — same problem domain, narrower existing scope. This is the single largest, most-tested "keep" case in the whole port, substantial enough that it deserves a scoping conversation with the maintainer before any PR, not a quick drive-by. |
Mixtures (mixtures/)¶
| Module | Alternative | Verdict | Why | Upstream potential |
|---|---|---|---|---|
gaussian_mixture.py — estimate_em_mixture |
sklearn.mixture.GaussianMixture |
HYBRID | sklearn's EM implementation is more numerically robust (covariance regularization, multiple initializations, convergence diagnostics) and well-tested for the general n-component case. Worth switching to for the pure model-fitting step. KEEP everything downstream of fitting — VaR/ES, risk contribution, risk budgeting, PDF/skewness under the mixture — since sklearn's GaussianMixture has none of that; it only fits parameters. |
N/A for the fitting step (switching to sklearn). The downstream risk layer (VaR/ES, risk contribution, PDF/skewness under a mixture) is mixture-specific enough that no active general-purpose project is an obvious target — see jump_diffusion.py below for the same conclusion in a related module. |
jump_diffusion.py |
(none) | KEEP | Jump-diffusion-specific risk measures; no general-purpose equivalent exists. | Niche enough that no actively-maintained project in this exact space is an obvious target — probably not worth pursuing as a standalone contribution right now. |
SVM (svm/)¶
| Module | Alternative | Verdict | Why | Upstream potential |
|---|---|---|---|---|
svm.py |
sklearn.svm.SVC/SVR |
SWITCH for standalone use | sklearn's SVM is backed by libsvm/liblinear (compiled C, extremely well optimized and battle-tested) — already verified to match our implementation to 3+ decimal places. For any standalone classification/regression task, use sklearn directly; it will be faster and more robust to edge cases (kernel tricks, class imbalance handling, probability calibration — none of which we ported). KEEP ours only if the SVM needs to be composed with other constraints inside the same solve_qp/cvxpy optimization (e.g. embedding an SVM-like margin constraint inside a larger portfolio problem) — that composability is the one thing sklearn's opaque solver can't offer. |
N/A — sklearn's SVM is already the better standalone tool. The one thing ours offers (composing an SVM-like margin constraint inside a larger cvxpy/solve_qp problem) doesn't fit sklearn's opaque-solver API at all, so there's no narrow feature to propose there. |
Spline (spline/)¶
| Module | Alternative | Verdict | Why | Upstream potential |
|---|---|---|---|---|
spline.py |
scipy.interpolate.CubicSpline (pure interpolation), scipy.interpolate.UnivariateSpline/make_smoothing_spline (smoothing) |
SWITCH for general use | Already verified our p=1 case matches CubicSpline to machine precision — scipy's tools are more mature, better-tested, and actively maintained. The one real friction point: our p parameter (a [0,1] interpolation/smoothing blend) and scipy's s parameter (a target sum-of-squared-residuals) are different parameterizations of smoothness, not directly interchangeable. KEEP ours only where exact p-parameterized behavior needs to match existing MATLAB-based configs or analysis. |
N/A — scipy already covers general use well; the p-parameterization is a different (not clearly superior) smoothness convention, not an obvious missing option in scipy's API. |
Maths (maths/)¶
| Module | Alternative | Verdict | Why | Upstream potential |
|---|---|---|---|---|
numerical_diff.py |
numdifftools |
HYBRID | numdifftools uses adaptive step sizing and Richardson extrapolation — meaningfully more accurate than our fixed-step-size approach, especially for ill-conditioned functions. Worth using where precision matters (e.g. as an alternative Hessian source for MLE standard errors). Keep ours for the specific magnitude-scaled step convention already wired into econometrics.estimation/whittle, to avoid adding a dependency for something already working correctly. |
N/A — numdifftools' adaptive approach is already the more general, more accurate one; our fixed convention is specific to this port's estimation code, not a missing capability worth proposing there. |
simulation.py — compute_ewma |
pandas.DataFrame.ewm() |
HYBRID | Pandas' .ewm() is a highly optimized, feature-complete exponentially-weighted accessor. The friction: pandas parameterizes by alpha/span/halflife (smoothing factor), while our compute_ewma uses lambda_ as a mean-reversion rate with explicit dt scaling — a different but related convention requiring a small translation layer (alpha = lambda_ * dt, roughly) to switch cleanly. Worth doing if performance on very long series matters; keep ours as-is for now given the interface is already wired into volatility_target/momentum_ewma. |
N/A — .ewm() is already more complete; the lambda_/dt convention is just a different parameterization, not a missing pandas capability. |
simulation.py — GBM simulators, Riccati/Lyapunov |
scipy.linalg (already the backend for Riccati/Lyapunov) |
KEEP | Riccati/Lyapunov already route through scipy.linalg.solve_continuous_are/solve_lyapunov — this is the "switch" already done. GBM simulation has no strong standard-library equivalent at this level of simplicity (QuantLib-Python exists but is a much heavier dependency for far more sophisticated PDE/Monte Carlo needs than this module targets). |
Too simple (a few lines of Cholesky decomposition + cumsum) to need a dependency; most users would rather inline it than take one. QuantLib-Python already covers this need at a much higher sophistication level for anyone who actually needs that — probably not worth pursuing. |
Backtest (backtest/)¶
| Module | Alternative | Verdict | Why | Upstream potential |
|---|---|---|---|---|
returns.py, stats.py, reporting.py |
vectorbt, bt, backtrader, zipline |
KEEP | These are full backtesting frameworks — heavier dependencies, different paradigms (event-driven for backtrader/zipline vs. vectorized), and a much bigger surface area than this module's lightweight, transparent, vectorized style suits. vectorbt specifically is worth knowing about if backtest performance on very large universes/long histories becomes a bottleneck (it's Numba-accelerated), but it's a genuinely different tool, not a drop-in replacement for this module's scope. |
vectorbt/backtrader/zipline are deliberately different tools (event-driven or heavier-dependency, vs. this module's lightweight vectorized style); contributing into one of them would fight its design philosophy rather than fill a gap in it — probably not worth pursuing. |
Linear algebra (linalg/)¶
| Module | Alternative | Verdict | Why | Upstream potential |
|---|---|---|---|---|
special_matrices.py |
(none found as a standalone public utility) | KEEP | vec/vech/xpnd/commutation/duplication/elimination matrices show up as private internals scattered inside packages like linearmodels, but there's no clean, importable public utility module for these. Genuinely fills a gap other libraries solve ad hoc internally rather than exposing. |
The cleanest PR candidate in this whole document: pure, dependency-free NumPy functions, no state. linearmodels is the more directly relevant target (already has near-equivalents to consolidate against); scipy.linalg is the higher-visibility one. Small enough for a maintainer to review in one sitting. |
Bond & sustainable finance (bond/, sustainable_finance/)¶
| Module | Alternative | Verdict | Why | Upstream potential |
|---|---|---|---|---|
bond/pricing.py — bond_price, bond_ytm, coupon_yield |
QuantLib-Python |
HYBRID | QuantLib has far more complete bond conventions (day-count fractions, calendars, callable/amortizing structures) than this flat-rate, cash-flow-list version. Its bond machinery is also a much heavier dependency for the simple flat-discounting case this module targets. Keep this module for quick, dependency-free pricing/YTM against an explicit cash-flow schedule; reach for QuantLib once real-world conventions (day counts, holiday calendars, embedded options) matter. | N/A — QuantLib already covers the general case more completely; this module's simplicity is the point, not a gap to contribute. |
bond/pricing.py — bond_portfolio_quadratic_form(_vs_benchmark), sustainable_finance/risk.py |
(none found) | KEEP | Sector-level modified-duration/DTS-targeting quadratic risk forms (with an optional active-share term against a benchmark) are specific to fixed-income portfolio construction and don't appear as a public utility in any general optimization or fixed-income library surveyed. | Narrow and tied to this port's (Q, R, c) quadratic-form convention (shared with portfolio/risk_budgeting.py) — more realistic as a documented pattern within this port than a standalone proposal. |
sustainable_finance/carbon.py — cumulative-emissions ("carbon budget") integrals |
(none found) | KEEP | Closed-form integrals of assumed emissions trajectories (linear, compound-decline, piecewise-linear, GDP-adjusted) are specific to climate/decarbonization-target analysis; no general-purpose or climate-finance Python library surveyed exposes these as a public utility. | Small, dependency-free (pure numpy), self-contained formulas — a plausible narrow PR, but tied closely to this book's specific decarbonization-pathway conventions. |
sustainable_finance/esg.py — esg_beta_star, esg_minimum_variance, pedersen_portfolio |
(none found) | KEEP | Roncalli's implied-ESG-beta tilt and the Pedersen-Fitzgibbons-Pomorski (2021) ESG-efficient-frontier portfolio are specific portfolio-construction techniques from the sustainable-finance literature; no general portfolio-optimization library (PyPortfolioOpt, cvxpy-based tools, etc.) implements either. |
Niche enough (tied to two specific published models) that a standalone PyPortfolioOpt-style package is more plausible than folding into an existing general library. |
sustainable_finance/climate.py — DICE carbon-cycle/temperature model |
pydice and similar community DICE ports |
HYBRID | Several community Python ports of Nordhaus's DICE model exist, generally more complete (full economic-optimization loop, not just the carbon-cycle/temperature submodule). This module's narrower scope (just the state-transition matrices and a forward simulation given exogenous GDP/mitigation paths) is useful when only the physical climate response is needed, without pulling in a full DICE optimization stack. | A full DICE port is out of scope for this toolbox; the physical submodule alone is a reasonable standalone utility, but not an obvious upstream contribution target given existing DICE ports already cover more ground. |
sustainable_finance/ecology.py — species-area/abundance, Hurlbert's rarefaction |
scikit-bio, vegan (R, no direct Python port) |
HYBRID | scikit-bio covers alpha/beta diversity metrics broadly (including some rarefaction) but not the specific species-area/endemics-area relationship formulas here. Keep for the species-area/endemics-area functions; scikit-bio is worth checking first for general diversity-index needs. |
Narrow overlap with scikit-bio's existing rarefaction tools — a documented pattern here is more realistic than a PR, given the differing scope. |
sustainable_finance/entropy.py — shannon_entropy, shannon_entropy_markov_chain |
scipy.stats.entropy |
HYBRID | scipy.stats.entropy computes plain (marginal) Shannon/KL entropy of a single distribution — a one-line building block this module's marginal-entropy calculation could call instead of its own xlogy-based sum. Keep the module for the joint/mutual-information decomposition (I_X, I_Y, I_XY, I_X_Y) and the Markov-chain time-horizon sweep, which scipy.stats doesn't provide. |
scipy.stats.entropy already covers the marginal case cleanly — no gap to contribute there; the joint/Markov-chain extensions are specific enough to this book's presentation to keep local. |
sustainable_finance/entropy.py — estimate_markov_generator |
(none found) | KEEP | The Israel-Rosenthal-Wei (2001) generator-repair technique (used here for rating-migration-generator estimation) doesn't appear as a public utility in any credit-risk or Markov-chain library surveyed. | Small, self-contained, dependency-free (pure numpy) — a plausible narrow PR to a credit-risk-modeling library, if one with an active generator-estimation module exists; none identified during this survey. |
Credit models (credit/)¶
| Module | Alternative | Verdict | Why | Upstream potential |
|---|---|---|---|---|
credit/structural.py — black_scholes |
py_vollib, mibian |
HYBRID | Both are more complete for implied-volatility solving and Greeks. Neither exposes a single generalized cost-of-carry b parameter the way this function does (they split by asset class -- equity, futures, FX -- into separate functions instead); keep this one when a single function covering all of those via b is convenient, e.g. inside merton_jump_model's per-jump-count loop. |
py_vollib/mibian already cover the general case with a more complete feature set (Greeks, implied vol) — no real gap to contribute into. |
credit/structural.py — pd_merton_model, b0_extended_merton_model/e0_extended_merton_model/pd_extended_merton_model, pd_black_cox_model, merton_jump_model, merton_jump_climate_model, reinders_credit_model |
(none found) | KEEP | Each is a specific published structural credit model (Merton 1974/1976, Black-Cox 1976, Blasberg 2024's extended Merton, Reinders et al.'s transition-loss model) — no general credit-risk or derivatives-pricing Python library surveyed (QuantLib-Python included) implements this particular family as public utilities; QuantLib's credit machinery targets CDS/bond pricing given a hazard curve, not firm-value calibration from equity data. |
Niche enough, and tied to specific named academic models, that a standalone package (or PR into a structural-credit-model-focused project, none identified) is more plausible than folding into a general derivatives library. |
credit/reduced_form.py — survival_markov_generator, density_markov_generator, hazard_markov_generator |
(none found) | KEEP | Survival/density/hazard functions implied by a continuous-time Markov generator matrix (e.g. a credit-rating transition-intensity matrix) are a scipy.linalg.expm-backed niche calculation specific to Markov-chain-based credit/reliability modeling; no general survival-analysis library exposes this generator-to-hazard mapping directly. |
Small, scipy-backed, dependency-free beyond that — a plausible narrow PR to a credit-risk-modeling library with an active Markov-generator module, none identified during this survey. |
credit/reduced_form.py — survival_exponential, cdf_exponential, pdf_exponential, inv_exponential, rnd_exponential |
lifelines, scikit-survival |
HYBRID | Both are full survival-analysis libraries: they estimate hazard/survival functions from observed (possibly censored) event-time data (Kaplan-Meier, Cox PH, etc.). This module instead simulates from and inverts an already-specified (piecewise-constant) hazard curve — a different, complementary use case neither library targets directly. Keep for simulation/quantile-inversion given an assumed hazard; reach for lifelines/scikit-survival when the hazard itself needs to be estimated from data. |
Different use case from what these libraries solve — not a natural upstream target. |
Copula (copula/)¶
| Module | Alternative | Verdict | Why | Upstream potential |
|---|---|---|---|---|
copula/families.py — clayton_cdf/pdf, frank_cdf/pdf, gumbel_cdf/pdf |
statsmodels.distributions.copula.api (ClaytonCopula, FrankCopula, GumbelCopula; already a dependency) |
HYBRID | Verified numerically identical to statsmodels to full float64 precision (CDF, PDF, and — via dependence.py — Kendall's tau). Not wrapped directly: statsmodels' classes fix one theta per instance at construction, while this port (like the original MATLAB) vectorizes theta per observation — a shape of API statsmodels doesn't offer. Kept as plain vectorized numpy, cross-checked against statsmodels in tests rather than depending on it at runtime. |
statsmodels' copula classes already cover the non-vectorized case well; a vectorized-theta constructor mode is a plausible but narrow feature request, not an obvious gap. |
copula/families.py — gaussian_copula_cdf/pdf |
statsmodels.distributions.copula.api.GaussianCopula |
HYBRID, internally reuses stats/multivariate.py's bvn_cdf/bvn_pdf (bivariate) and stats/distributions.py's mvn_cdf (n-dim) |
Verified numerically identical to statsmodels (bivariate and 3-dim). Rather than a third from-scratch bivariate/multivariate-normal-CDF implementation, gaussian_copula_cdf/pdf delegate to this package's own already-tested wrappers around scipy.stats.multivariate_normal — the actual redundancy this module avoids is internal, not statsmodels. |
N/A — the reuse is internal to this package, not a library gap. |
copula/families.py — student_copula_cdf/pdf |
statsmodels.distributions.copula.api.StudentTCopula |
KEEP (statsmodels gap), reuses stats/multivariate.py's bvt_cdf internally |
statsmodels' StudentTCopula.cdf() raises NotImplementedError("CDF not available in closed form.") — no closed-form Student-t copula CDF is implemented there at all, so this had to be self-implemented via scipy.stats.multivariate_t/bvt_cdf regardless of the vectorization question above. pdf was cross-checked against statsmodels and matches. |
A statsmodels PR implementing StudentTCopula.cdf() (numerically, via scipy.stats.multivariate_t.cdf, the same approach used here) is a plausible, well-scoped gap to report upstream. |
copula/families.py — Fréchet-Hoeffding bounds, independence copula |
(none needed — trivial closed forms) | KEEP | min/max/product formulas; not worth a dependency either way, but merges each MATLAB n-dim/bivariate file pair (cdfCopulaUpper.m+cdfCopulaUpper2.m, etc.) into one function per bound, see module docstring. |
N/A — trivial formulas, no capability gap to speak of. |
copula/families.py — AMH, Gumbel-Barnett, Galambos, Husler-Reiss, Plackett, FGM, Cubic, logistic-Gumbel, Marshall-Olkin, Sloane, nested Gumbel |
(none found) | KEEP | 13 named bivariate copula families with no equivalent in statsmodels, copulas (focused on synthetic tabular data generation, ships Gaussian/Clayton/Frank/Gumbel only), or pyvinecopulib (vine-copula construction, not this set of standalone bivariate families). |
Individually small, closed-form numpy functions — plausible as a batch PR to a copula-focused library, but scattered enough across different published sources that a scoping conversation (which families a maintainer actually wants) would come first. |
copula/dependence.py — spearman_rho_numeric, clayton_rho/gumbel_rho, Debye/dilog special functions |
(none found) | KEEP | The original SpearmanCopula.m is itself a generic double-integral Spearman's-rho estimator for any copula CDF, generalized here beyond the two families (Clayton, Gumbel) the original wired it up to. No general copula library exposes a "give me any CDF, get Spearman's rho" utility this directly. |
Genuinely generic and dependency-light (scipy.integrate.dblquad) — a plausible small PR to a copula-focused library, though the closed-form-first-numeric-fallback pattern would need to fit that library's existing API shape. |
copula/simulate.py — simulate_from_conditional_cdf |
(none found — general copula libraries expose named-family sampling, not this generic API) | KEEP, built on this package's own optim/bisection.py |
The original rndCopula2.m is itself a generic bivariate copula simulator (draw u1, v2, invert conditional_cdf(u1, u2) = v2 by bisection) the original only ever plugged Gumbel into. Generalized here to accept any family's conditional CDF. statsmodels/copulas/pyvinecopulib all expose per-family .rvs() methods, not a generic conditional-inversion primitive a caller can point at an arbitrary family. |
The generic engine is a plausible utility for a copula library's internals, but tied closely to this port's BisectionConfig/vectorized-bisection convention — a scoping conversation, not a drive-by PR. |
copula/simulate.py — simulate_gaussian_copula/simulate_student_copula |
statsmodels.distributions.copula.api.{GaussianCopula,StudentTCopula}.rvs() |
HYBRID | statsmodels already provides Cholesky-based sampling for these two families through its class .rvs() methods; kept here mainly so all of copula/simulate.py's functions share one dependency-light, numpy-only calling convention (corr, n_samples, random_state) rather than mixing a statsmodels-class code path in with the from-scratch simulators the other families need regardless. |
N/A — statsmodels already covers this case; kept for calling-convention consistency, not a capability gap. |
Summary: where the strongest "switch" cases are¶
If prioritizing effort, these five would give the most practical benefit for the least risk, roughly in order of impact:
stats/regression/lasso.py→ route the penalized-form solvers throughsklearn.linear_model.Lasso/ElasticNet(keeplasso_tau_constrainedas-is).econometrics/var.py→ usestatsmodels.tsa.api.VARfor anything unrestricted (keepvarx_estimatefor the restricted case).econometrics/kalman.py→ usestatsmodels.tsa.statespace.MLEModelfor anything beyond simple filtering.svm/svm.py→ recommendsklearn.svm.SVC/SVRdirectly to users for standalone SVM tasks; keep the module for QP-composability use cases only.spline/spline.py→ recommendscipy.interpolate.CubicSplinefor pure interpolation; keep the smoothing-spline path only where thep-parameterization specifically matters.
The strongest "keep, no serious alternative" cases are stats/
distributions.py's GQF family, portfolio/risk_budgeting.py,
linalg/special_matrices.py, and econometrics/whittle.py — these are
the places where the port is providing something genuinely absent
elsewhere in the Python ecosystem, not just re-deriving what a
well-known library already does.
Summary: where the strongest upstreaming candidates are¶
These are the same "keep, no serious alternative" cases, now looked at from the other direction: which of them are worth actually proposing to an upstream project, rather than staying locked inside this port?
linalg/special_matrices.py→linearmodelsorscipy.linalg. Pure, dependency-free NumPy functions with no state — the cleanest PR of this whole document.optim/bisection.py(the vectorized version) →scipy.optimize. Small, self-contained, and array-broadcast bisection has come up as a recurring (if low-priority) SciPy feature request already.econometrics/whittle.py→statsmodels.tsaorarch. Frequency-domain estimation isn't implemented in either — genuinely fills a gap, but needs generalizing beyond this port's specific interface, so this is a scoping conversation, not a drive-by PR.portfolio/risk_budgeting.py→riskparityportfolio. The single largest, most-tested piece of custom work in the whole port (75+ original MATLAB files consolidated) — substantial enough that it deserves its own scoping conversation with the maintainer.stats/distributions.py's GQF1/GQF2 family — no clean existing home found anywhere in the ecosystem; more realistic as a small standalone release than a PR into an existing library.
A few narrower gaps are real but smaller, and land better as an
scikit-learn-contrib package or a statsmodels issue than a from-scratch
library: the restriction=(RR, r) linear-restriction interface shared
by stats/regression/ols.py and econometrics/estimation.py; the
a_eq/b_eq restriction support in econometrics/var.py; the
tau-budget parameterizations in stats/regression/ridge.py/lasso.py;
and portfolio/mean_variance.py/tracking_error.py's new
mvo_target_portfolio/te_target_portfolio functions, worth comparing
directly against PyPortfolioOpt's efficient_risk/efficient_return
before assuming they're additive (the tracking-error-relative version is
the more likely genuine gap). See each module's row above for the
specific target and caveat.
Probably not worth pursuing right now: backtest/ (fights the
design philosophy of the event-driven/heavier frameworks it would land
in), maths/simulation.py's GBM simulators (too simple to need a
dependency), and mixtures/jump_diffusion.py (niche enough that no
actively-maintained project in this exact space is an obvious target).
None of this is a promise any of these would be accepted — each library
has its own contribution norms (issue-first discussion, its own
test/type conventions, maintainer bandwidth), and "no existing
alternative" doesn't automatically mean "wanted upstream." Start with
the clean, dependency-free entries (special_matrices.py,
bisection.py) — small enough for a maintainer to review in one
sitting — before attempting the larger modules (risk_budgeting.py,
whittle.py), which are substantial enough to warrant a scoping
conversation with maintainers first, rather than an unsolicited PR.