quanttoolbox.stats¶
stats.distributions¶
Python alternatives
Simple wrappers (normal/t/chi2/F/MVN/beta/skew-normal) are one-liners around scipy.stats — switch/keep as call-site convenience, no reason to avoid calling scipy.stats directly either. Skew-normal in particular fully switches to scipy.stats.skewnorm (verified numerically equivalent — no hand-rolled Newton iteration needed for its quantile function). GQF1/GQF2, skew-t (Azzalini's, not scipy's Jones-Faddy jf_skew_t), the Bates distribution, and the normal-ratio (Hinkley) distribution are genuinely niche — nothing in scipy, statsmodels, or elsewhere implements these. Keep. Poisson-binomial matches scipy.stats.poisson_binom exactly — switch. Order-statistic CDF is a thin scipy.stats.binom.sf-backed convenience; its quantile counterpart (grid search) has no scipy equivalent — hybrid.
quanttoolbox.stats.distributions
¶
Probability distribution functions: CDF/PDF/quantile wrappers and the GQF (generalized quadratic form) distribution family.
Ported from QuantToolBox/stats/{cdfn,cdfni,cdft,cdfti,cdftc,cdfchi2,
cdfchi2c,cdff,cdffc,cdfmvn,pdfmvn,pdfn,rndmvn,gqf1_,gqf2_}.m, extended
with HSF toolbox stats/{cdfSN,cdfSNi,pdfSN,momSN,rndSN,cdfST,cdfSTi,pdfST,
momST,rndST,cdfBates,pdfBates,cdfbeta,pdfbeta,cdfig,pdfig,cdfln,pdfln,
cdfNormalRatio,pdfNormalRatio,pdfPoissonBinomial,cdfchi2i,pdft,
compute_cdf_order_statistics,compute_inv_cdf_order_statistics,
constant_correlation_matrix}.m (HSF toolbox port -- see
docs/migration_map.md).
Translation notes:
- The simple distribution wrappers (normal/Student-t/chi-square/F/MVN) are
thin passthroughs to
scipy.stats-- MATLAB's Statistics Toolbox functions (tcdf,chi2cdf,mvncdf, ...) map directly ontoscipy.stats.{t,chi2,f,multivariate_normal}, so there's no need to hand-roll any of this.student_t_pdf/chi2_ppf(from the HSF toolbox'spdft.m/cdfchi2i.m) fill out this same family. - GQF #1 is the distribution of
sum(a_i * (Z_i + b_i)^2)for independent standard normals Z_i (a "weighted noncentral chi-square mixture"); GQF #2 is the distribution of the general quadratic form(X - mu)' Q (X - mu)(or similar) forX ~ N(mu, Sigma). Both use cumulant-based approximations (a Laguerre-type series for GQF1, a three-cumulant noncentral-chi-square matching for GQF2 -- see Solomon & Stephens (1978) / Buckley & Eagleson (1988) for the underlying theory). These have no scipy equivalent and are ported algorithm-for-algorithm. - Beta/lognormal/inverse-Gaussian (
cdfbeta/pdfbeta,cdfln/pdfln,cdfig/pdfig): thin wrappers with the original's call signature, same "call-site compatibility" reasoning as the simple wrappers above --beta_cdf/beta_pdfgo straight toscipy.stats.beta;lognormal_cdf/lognormal_pdfandinverse_gaussian_cdf/inverse_gaussian_pdfare evaluated directly from the closed-form formulas the originals use (verified algebraically equivalent toscipy.stats.lognorm/invgaussunder those distributions' own shape/scale reparameterization, but ported directly rather than requiring callers to compute that reparameterization themselves). - Bates distribution (
bates_cdf/bates_pdf): no scipy equivalent (scipy has no named Bates distribution) -- ported algorithm-for-algorithm (scipy.special.combreplaces MATLAB'snchoosek). - Poisson-binomial PMF (
poisson_binomial_pmf): matchesscipy.stats.poisson_binomexactly (verified numerically against both the original's FFT and direct-recursion branches) -- switched to the scipy-backed distribution rather than hand-rolling either branch. - Order statistics (
order_statistic_cdf/order_statistic_ppf): the order-statistic CDF formulaF_{i:n}(x) = P(Binom(n, F_x) >= i)is evaluated viascipy.stats.binom.sfrather than hand-computing binomial coefficients in a loop -- same formula, more numerically stable for large n. The quantile version is a grid search (given sample pointsxand their CDF valuesF_x, find the first point where the order-statistic CDF crossesalpha) with no scipy equivalent -- ported directly. - Normal-ratio distribution (
normal_ratio_cdf/normal_ratio_pdf, Hinkley's distribution for the ratio of two independent normals): no scipy equivalent -- ported directly, using this module's ownquanttoolbox.stats.multivariate.bvn_cdfin place of the original'scdfbvncalls, matching the original's(p, a_z, b_z, c, rho_z)multi-output signature. - Skew-normal (
skew_normal_{cdf,ppf,pdf,moments,rvs}): switched entirely toscipy.stats.skewnorm-- verified numerically thatpdfSN.m's formula,momSN.m's moment formulas, andscipy.stats.skewnorm's pdf/cdf/stats(moments="mvsk")all agree exactly under the direct parameter mapping (eta->a,xi->loc,omega->scale). The original's two alternate numerical CDF branches (mtd0 vs 1, both bivariate-normal-orthant identities for the same value) and its hand-rolled Newton-iteration quantile function (cdfSNi.m) are therefore unnecessary --scipycomputes the exact analytic answer directly via.cdf/.ppf. - Skew-t (
skew_t_{cdf,ppf,pdf,moments,rvs}): not the same family asscipy.stats.jf_skew_t(Jones & Faddy's skew-t, a different two-shape- parameter family) -- this is Azzalini's skew-t (one skewness parameteretaplus degrees of freedomnu), which has no scipy equivalent. Ported algorithm-for-algorithm, including the original's Newton-iteration quantile function (cdfSTi.m, viaquanttoolbox.config.NewtonConfig) and its bivariate-Student-t-based CDF (viaquanttoolbox.stats.multivariate.bvt_cdf). max_size.m(a MATLAB shape-broadcasting helper) is not ported as a function -- superseded entirely by numpy's native broadcasting (np.broadcast_arrays), used throughout this module andstats.multivariateinstead.- MATLAB's
missex(cdf, cond)(replace-with-missing where cond is true) is inlined asnp.where(cond, np.nan, cdf). - MATLAB's
eigis replaced withnumpy.linalg.eighingqf2_to_gqf1since the matrix involved is symmetric by construction; the eigenvalue order may differ from MATLAB's, but the resulting (a, b) pairs describe the same distribution regardless of order.
NormalRatioResult(p, a_z, b_z, c, rho_z)
dataclass
¶
CDF or PDF of Z = X / Y (X, Y independent normals), plus the
intermediate quantities (a_z, b_z, c, rho_z) the
original returns alongside the probability.
bates_cdf(x, n)
¶
CDF of the Bates distribution: the mean of n iid Uniform(0, 1)
variables. No scipy equivalent.
Original: stats/cdfBates.m (HSF toolbox)
Source code in src/quanttoolbox/stats/distributions.py
460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 | |
bates_pdf(x, n)
¶
PDF of the Bates distribution: the mean of n iid Uniform(0, 1)
variables. No scipy equivalent.
Original: stats/pdfBates.m (HSF toolbox)
Source code in src/quanttoolbox/stats/distributions.py
477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 | |
beta_cdf(x, a, b)
¶
Original: stats/cdfbeta.m (HSF toolbox)
Source code in src/quanttoolbox/stats/distributions.py
407 408 409 | |
beta_pdf(x, a, b)
¶
Original: stats/pdfbeta.m (HSF toolbox)
Source code in src/quanttoolbox/stats/distributions.py
412 413 414 | |
chi2_cdf(x, nu)
¶
Original: stats/cdfchi2.m
Source code in src/quanttoolbox/stats/distributions.py
145 146 147 | |
chi2_ppf(p, nu)
¶
Original: stats/cdfchi2i.m (HSF toolbox)
Source code in src/quanttoolbox/stats/distributions.py
150 151 152 | |
chi2_sf(x, nu)
¶
Upper-tail (survival) chi-square. Original: stats/cdfchi2c.m
Source code in src/quanttoolbox/stats/distributions.py
155 156 157 | |
constant_correlation_matrix(n, rho)
¶
An n x n correlation matrix with constant off-diagonal correlation
rho and unit diagonal.
Original: stats/constant_correlation_matrix.m (HSF toolbox)
Source code in src/quanttoolbox/stats/distributions.py
648 649 650 651 652 653 654 655 656 | |
f_cdf(x, nu1, nu2)
¶
Original: stats/cdff.m
Source code in src/quanttoolbox/stats/distributions.py
160 161 162 | |
f_sf(x, nu1, nu2)
¶
Upper-tail (survival) F distribution. Original: stats/cdffc.m
Source code in src/quanttoolbox/stats/distributions.py
165 166 167 | |
gqf1_cdf(x, a, b, beta=None, order=200)
¶
CDF of the GQF #1 distribution. Original: stats/gqf1_cdf.m
Source code in src/quanttoolbox/stats/distributions.py
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 | |
gqf1_coeffs(a, b, beta, order)
¶
Laguerre-series mixing coefficients for the GQF #1 CDF/PDF.
Original: stats/gqf1_coeffs.m
Source code in src/quanttoolbox/stats/distributions.py
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | |
gqf1_moments(a, b)
¶
Mean, std dev, skewness, and excess kurtosis of the GQF #1 distribution.
Original: stats/gqf1_moments.m
Source code in src/quanttoolbox/stats/distributions.py
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | |
gqf1_pdf(x, a, b, beta=None, order=100)
¶
PDF of the GQF #1 distribution. Original: stats/gqf1_pdf.m
Source code in src/quanttoolbox/stats/distributions.py
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 | |
gqf1_to_gqf2(a, b)
¶
Convert GQF #1 parameters (a, b) to GQF #2 parameters (mu, Sigma, Q).
Original: stats/gqf1_to_gqf2.m
Source code in src/quanttoolbox/stats/distributions.py
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 | |
gqf2_cdf(x, mu, sigma, q)
¶
CDF of the GQF #2 distribution, via noncentral-chi-square matching.
Original: stats/gqf2_cdf.m
Source code in src/quanttoolbox/stats/distributions.py
346 347 348 349 350 351 352 353 354 355 356 357 358 359 | |
gqf2_moments(mu, sigma, q)
¶
Mean, std dev, skewness, excess kurtosis, and the two Pearson-type shape statistics (s1, s2) used by gqf2_cdf/gqf2_pdf.
Original: stats/gqf2_moments.m
Source code in src/quanttoolbox/stats/distributions.py
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 | |
gqf2_pdf(x, mu, sigma, q)
¶
PDF of the GQF #2 distribution, via noncentral-chi-square matching.
Original: stats/gqf2_pdf.m
Source code in src/quanttoolbox/stats/distributions.py
362 363 364 365 366 367 368 369 370 371 372 373 374 375 | |
gqf2_to_gqf1(mu, sigma, q)
¶
Convert GQF #2 parameters (mu, Sigma, Q) to GQF #1 parameters (a, b).
Original: stats/gqf2_to_gqf1.m
Note: eigenvalue ordering may differ from MATLAB's eig (which does
not sort), but the resulting (a, b) pairs describe the same
distribution regardless of order.
Source code in src/quanttoolbox/stats/distributions.py
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 | |
inverse_gaussian_cdf(x, mu, lam)
¶
CDF of the inverse Gaussian (Wald) distribution, mean mu, shape
lam.
Original: stats/cdfig.m (HSF toolbox; algebraically equivalent to
scipy.stats.invgauss.cdf(x, mu / lam, scale=lam), ported directly
to keep the original's (mu, lam) call signature)
Source code in src/quanttoolbox/stats/distributions.py
436 437 438 439 440 441 442 443 444 445 446 447 | |
inverse_gaussian_pdf(x, mu, lam)
¶
PDF of the inverse Gaussian (Wald) distribution, mean mu, shape
lam.
Original: stats/pdfig.m (HSF toolbox)
Source code in src/quanttoolbox/stats/distributions.py
450 451 452 453 454 455 456 457 | |
lognormal_cdf(x, mu, sigma)
¶
CDF of a lognormal variable, i.e. log(X) ~ N(mu, sigma^2).
Original: stats/cdfln.m (HSF toolbox)
Source code in src/quanttoolbox/stats/distributions.py
417 418 419 420 421 422 423 | |
lognormal_pdf(x, mu, sigma)
¶
PDF of a lognormal variable, i.e. log(X) ~ N(mu, sigma^2).
Original: stats/pdfln.m (HSF toolbox)
Source code in src/quanttoolbox/stats/distributions.py
426 427 428 429 430 431 432 433 | |
mvn_cdf(x, mu, sigma)
¶
CDF of the multivariate normal N(mu, Sigma). Original: stats/cdfmvn.m
Source code in src/quanttoolbox/stats/distributions.py
170 171 172 | |
mvn_pdf(x, mu, sigma)
¶
PDF of the multivariate normal N(mu, Sigma). Original: stats/pdfmvn.m
Source code in src/quanttoolbox/stats/distributions.py
175 176 177 | |
mvn_rvs(mu, sigma, n_samples, random_state=None)
¶
Draw samples from the multivariate normal N(mu, Sigma).
Original: stats/rndmvn.m
Source code in src/quanttoolbox/stats/distributions.py
180 181 182 183 184 185 186 187 | |
normal_cdf(x, mu=0.0, sigma=1.0)
¶
Original: stats/cdfn.m
Source code in src/quanttoolbox/stats/distributions.py
110 111 112 | |
normal_pdf(x, mu=0.0, sigma=1.0)
¶
Original: stats/pdfn.m
Source code in src/quanttoolbox/stats/distributions.py
120 121 122 | |
normal_ppf(p)
¶
Quantile function of N(0,1). Original: stats/cdfni.m
Source code in src/quanttoolbox/stats/distributions.py
115 116 117 | |
normal_ratio_cdf(z, mu_x, sigma_x, mu_y, sigma_y)
¶
CDF of Z = X / Y, for independent X ~ N(mu_x, sigma_x^2) and Y ~ N(mu_y, sigma_y^2) (Hinkley's ratio distribution). No scipy equivalent.
Original: stats/cdfNormalRatio.m (HSF toolbox)
Source code in src/quanttoolbox/stats/distributions.py
692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 | |
normal_ratio_pdf(z, mu_x, sigma_x, mu_y, sigma_y)
¶
PDF of Z = X / Y, for independent X ~ N(mu_x, sigma_x^2) and Y ~ N(mu_y, sigma_y^2) (Hinkley's ratio distribution). No scipy equivalent.
Original: stats/pdfNormalRatio.m (HSF toolbox)
Source code in src/quanttoolbox/stats/distributions.py
711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 | |
order_statistic_cdf(f_x, n, i_select=None)
¶
CDF of the i-th order statistic (i = 1..n) of n iid draws from a
distribution with CDF value(s) f_x: F_{i:n} = P(Binom(n, F_x) >=
i), evaluated via scipy.stats.binom.sf rather than hand-computing
binomial coefficients (same formula, more numerically stable for large
n).
f_x may be an array (one row per evaluation point); returns an array
of shape (len(f_x), len(i_select)) with one column per selected order
statistic i (default: all of 1..n).
Original: stats/compute_cdf_order_statistics.m (HSF toolbox)
Source code in src/quanttoolbox/stats/distributions.py
593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 | |
order_statistic_ppf(alpha, x, f_x, n, i_select=None)
¶
Quantile of the i-th order statistic, found by grid search: given
sample points x with CDF values f_x, returns (for each alpha and
each selected order statistic i) the first x value where
order_statistic_cdf crosses alpha. No scipy equivalent -- ported
directly.
Original: stats/compute_inv_cdf_order_statistics.m (HSF toolbox)
Source code in src/quanttoolbox/stats/distributions.py
616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 | |
poisson_binomial_pmf(p)
¶
PMF of the sum of n independent Bernoulli(p_i) variables, for
support k = 0..n. Matches scipy.stats.poisson_binom exactly
(verified numerically against the original's own FFT and
direct-recursion branches) -- neither branch is hand-rolled here.
Original: stats/pdfPoissonBinomial.m (HSF toolbox)
Source code in src/quanttoolbox/stats/distributions.py
502 503 504 505 506 507 508 509 510 511 512 513 | |
poisson_binomial_pmf_brute_force(p)
¶
PMF of the sum of n independent Bernoulli(p_i) variables, via
exact brute-force enumeration over all 2^n outcomes -- feasible only
for small n; intended purely as an independent cross-check of
poisson_binomial_pmf_dp/poisson_binomial_pmf_dft, not for general
use.
Not ported from the MATLAB HSF toolbox -- no .m file in hfs-archive
implements this. Promoted from HSF-Notebooks chapter 16d as an
independent reference implementation, kept deliberately separate from
poisson_binomial_pmf -- see that notebook's three-way cross-check of
DP/DFT/brute-force methods against the packaged function.
Source code in src/quanttoolbox/stats/distributions.py
563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 | |
poisson_binomial_pmf_dft(p)
¶
PMF of the sum of n independent Bernoulli(p_i) variables, via a
DFT of the characteristic function (Fernandez & Williams (2010) /
the poibin package's "DFT-CF" method).
Not ported from the MATLAB HSF toolbox -- no .m file in hfs-archive
implements this. Promoted from HSF-Notebooks chapter 16d as an
independent reference implementation, kept deliberately separate from
poisson_binomial_pmf -- see that notebook's three-way cross-check of
DP/DFT/brute-force methods against the packaged function.
Source code in src/quanttoolbox/stats/distributions.py
536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 | |
poisson_binomial_pmf_dp(p)
¶
PMF of the sum of n independent Bernoulli(p_i) variables, via the
direct O(n^2) DP/convolution recursion (each Bernoulli in turn either
shifts the running PMF by one and scales by p_i, or leaves it in
place scaled by 1 - p_i).
Not ported from the MATLAB HSF toolbox -- no .m file in hfs-archive
implements this. Promoted from HSF-Notebooks chapter 16d as an
independent reference implementation, kept deliberately separate from
poisson_binomial_pmf -- see that notebook's three-way cross-check of
DP/DFT/brute-force methods against the packaged function.
Source code in src/quanttoolbox/stats/distributions.py
516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 | |
skew_normal_cdf(x, xi, omega, eta)
¶
CDF of Azzalini's skew-normal distribution (location xi, scale
omega, shape eta).
Original: stats/cdfSN.m (HSF toolbox; both of the original's numerical
branches are exact identities for this same value -- scipy computes
it directly, see module docstring)
Source code in src/quanttoolbox/stats/distributions.py
737 738 739 740 741 742 743 744 745 | |
skew_normal_moments(xi, omega, eta)
¶
Mean, std dev, skewness, and excess kurtosis of Azzalini's skew-normal distribution.
Original: stats/momSN.m (HSF toolbox; matches
scipy.stats.skewnorm.stats(eta, loc=xi, scale=omega,
moments="mvsk") exactly, see module docstring)
Source code in src/quanttoolbox/stats/distributions.py
766 767 768 769 770 771 772 773 774 775 | |
skew_normal_pdf(x, xi, omega, eta)
¶
PDF of Azzalini's skew-normal distribution.
Original: stats/pdfSN.m (HSF toolbox)
Source code in src/quanttoolbox/stats/distributions.py
758 759 760 761 762 763 | |
skew_normal_ppf(p, xi, omega, eta)
¶
Quantile function of Azzalini's skew-normal distribution.
Original: stats/cdfSNi.m (HSF toolbox; the original's Newton iteration
is unnecessary -- scipy.stats.skewnorm.ppf is exact, see module
docstring)
Source code in src/quanttoolbox/stats/distributions.py
748 749 750 751 752 753 754 755 | |
skew_normal_rvs(xi, omega, eta, size=1, random_state=None)
¶
Draw samples from Azzalini's skew-normal distribution.
Original: stats/rndSN.m (HSF toolbox)
Source code in src/quanttoolbox/stats/distributions.py
778 779 780 781 782 783 784 785 | |
skew_t_cdf(x, xi, omega, eta, nu, method=0)
¶
CDF of Azzalini's skew-t distribution (location xi, scale omega,
shape eta, degrees of freedom nu).
Original: stats/cdfST.m (HSF toolbox)
Source code in src/quanttoolbox/stats/distributions.py
794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 | |
skew_t_moments(xi, omega, eta, nu)
¶
Mean, std dev, skewness, and excess kurtosis of Azzalini's skew-t distribution (requires nu > 4 for the kurtosis to be finite).
Original: stats/momST.m (HSF toolbox)
Source code in src/quanttoolbox/stats/distributions.py
876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 | |
skew_t_pdf(x, xi, omega, eta, nu)
¶
PDF of Azzalini's skew-t distribution.
Original: stats/pdfST.m (HSF toolbox)
Source code in src/quanttoolbox/stats/distributions.py
814 815 816 817 818 819 820 821 822 | |
skew_t_ppf(p, xi, omega, eta, nu, config=None)
¶
Quantile function of Azzalini's skew-t distribution, via Newton iteration (no closed form / no scipy equivalent).
The default tolerance is looser than NewtonConfig's own default
(1e-4 rather than 1e-10): skew_t_cdf is itself backed by
bvt_cdf, whose underlying scipy.stats.multivariate_t.cdf uses a
randomized quasi-Monte-Carlo integrator with irreducible call-to-call
noise on the order of 1e-4 (verified empirically -- repeated calls
with identical inputs vary by ~7e-5 to ~1e-4). A tighter tolerance
(e.g. the 1e-8 this function used before) can never actually be
satisfied, so the loop always burns its full max_iters budget
without improving on the noise floor -- roughly a 4x slowdown for no
accuracy gain, confirmed by direct timing. 1e-4 lets the loop exit
as soon as it reaches that floor, matching the precision the
underlying CDF can actually deliver.
Original: stats/cdfSTi.m (HSF toolbox)
Source code in src/quanttoolbox/stats/distributions.py
825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 | |
skew_t_rvs(xi, omega, eta, nu, size=1, random_state=None)
¶
Draw samples from Azzalini's skew-t distribution: a skew-normal variate divided by sqrt(chi-square(nu) / nu).
Original: stats/rndST.m (HSF toolbox)
Source code in src/quanttoolbox/stats/distributions.py
904 905 906 907 908 909 910 911 912 913 914 915 | |
student_t_cdf(x, nu)
¶
Original: stats/cdft.m
Source code in src/quanttoolbox/stats/distributions.py
125 126 127 | |
student_t_pdf(x, nu)
¶
Original: stats/pdft.m (HSF toolbox)
Source code in src/quanttoolbox/stats/distributions.py
140 141 142 | |
student_t_ppf(p, nu)
¶
Original: stats/cdfti.m
Source code in src/quanttoolbox/stats/distributions.py
130 131 132 | |
student_t_sf(x, nu)
¶
Upper-tail (survival) Student's t. Original: stats/cdftc.m
Source code in src/quanttoolbox/stats/distributions.py
135 136 137 | |
Examples¶
Pinball-loss quantile estimation vs. sample and true quantiles — ects/quantile1.py
"""Translated from Examples/ects/quantile1.m -- illustrates that
minimizing the pinball (check) loss recovers the same quantile as the
standard sorted-sample quantile estimator, compared against the true
Normal quantile function (numeric core only; the original's comparison
plot is dropped).
The original draws y from MATLAB's unseeded `randn`; a fixed seed
(`np.random.default_rng(0)`) is substituted here. `quantile`/`sort`-based
quantile estimation uses plain `np.quantile` directly (not a ported
quanttoolbox function -- there's nothing toolbox-specific to translate
here, same as other examples that lean on bare NumPy/pandas idioms)."""
import numpy as np
from scipy.optimize import minimize_scalar
from quanttoolbox.stats.distributions import normal_ppf
rng = np.random.default_rng(0)
n_s = 1000
y = rng.standard_normal(n_s)
alpha = np.array([0.01, 0.05, 0.10, 0.20, 0.30, 0.40, 0.50, 0.60, 0.70, 0.80, 0.90, 0.95, 0.99])
# Empirical quantile via sorting
q1 = np.quantile(y, alpha)
def pinball_objective(q, data, a):
u = data - q
e = u > 0
return a * np.sum(np.abs(u[e])) + (1 - a) * np.sum(np.abs(u[~e]))
q3 = np.array([minimize_scalar(pinball_objective, args=(y, a)).x for a in alpha])
# True Normal quantiles
q4_true = normal_ppf(alpha)
print(
"alpha, empirical quantile (sorted), M-estimator quantile (pinball minimization), true Normal quantile:"
)
print(np.round(np.column_stack([alpha, q1, q3, q4_true]), 4))
stats.dose_response¶
Python alternatives
Keep — scipy has no dose-response-curve module. These are small, closed-form sigmoidal curves (log-logistic, log-normal, Weibull, and two "hormetic" variants) used in toxicology/ecotoxicology; no general-purpose equivalent found.
quanttoolbox.stats.dose_response
¶
Dose-response curve models (toxicology/ecotoxicology): log-logistic, log-normal, and Weibull sigmoidal curves, plus two "hormetic" variants that add a low-dose stimulatory term to the log-logistic curve.
Ported from HSF toolbox stats/{drcHormetic1,drcHormetic2,drcLogLogistic,
drcLogNormal,drcWeibull1,drcWeibull2}.m.
Translation notes:
- All six curves are small, closed-form functions with no
scipyequivalent (scipyhas no dose-response-curve module) -- ported algorithm-for-algorithm.drc_log_normalreuses this package's ownquanttoolbox.stats.distributions.normal_cdfin place of the original'scdfn. alphais the curve's inflection-point dose (ED50-style location parameter),betaits slope,y_min/y_maxthe lower/upper response asymptotes;gamma_/delta(hormetic variants only) control the extra low-dose stimulatory term. Parameter names and order match the originals exactly.
drc_hormetic1(x, alpha, beta, y_min, y_max, gamma_)
¶
Hormetic (type I) dose-response curve: adds a linear-in-dose
stimulatory term gamma_ * x to the log-logistic curve's numerator,
producing a low-dose "hump" before the usual sigmoidal decline.
Original: stats/drcHormetic1.m
Source code in src/quanttoolbox/stats/dose_response.py
77 78 79 80 81 82 83 84 85 86 87 88 | |
drc_hormetic2(x, alpha, beta, y_min, y_max, gamma_, delta)
¶
Hormetic (type II) dose-response curve: like drc_hormetic1, but
the stimulatory term decays as exp(-1 / x**delta) instead of
growing linearly in x.
Original: stats/drcHormetic2.m
Source code in src/quanttoolbox/stats/dose_response.py
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | |
drc_log_logistic(x, alpha, beta, y_min, y_max)
¶
Log-logistic dose-response curve.
Original: stats/drcLogLogistic.m
Source code in src/quanttoolbox/stats/dose_response.py
29 30 31 32 33 34 35 36 37 38 | |
drc_log_normal(x, alpha, beta, y_min, y_max)
¶
Log-normal dose-response curve.
Original: stats/drcLogNormal.m
Source code in src/quanttoolbox/stats/dose_response.py
41 42 43 44 45 46 47 48 49 50 | |
drc_weibull1(x, alpha, beta, y_min, y_max)
¶
Weibull (type I) dose-response curve.
Original: stats/drcWeibull1.m
Source code in src/quanttoolbox/stats/dose_response.py
53 54 55 56 57 58 59 60 61 62 | |
drc_weibull2(x, alpha, beta, y_min, y_max)
¶
Weibull (type II) dose-response curve.
Original: stats/drcWeibull2.m
Source code in src/quanttoolbox/stats/dose_response.py
65 66 67 68 69 70 71 72 73 74 | |
stats.multivariate¶
Python alternatives
Switch: bvn_cdf/bvt_cdf are thin scipy.stats.multivariate_normal/multivariate_t-backed wrappers with the original's bivariate (x, y, rho[, nu]) call signature — kept for that call-site convenience, not because scipy lacks the capability. The 8 low-level Genz quasi-Monte-Carlo integrators in genz/ (qsimvn, qsimvt, qsilatmvnv, qsimvnauto, ...) are not hand-ported at all: they solve exactly the arbitrary-dimension MVN/MVT probability problem scipy.stats.multivariate_normal.cdf/multivariate_t.cdf already solve, via the same Genz algorithm family (same author). See the module docstring and Library alternatives for the full reasoning.
quanttoolbox.stats.multivariate
¶
Bivariate normal/Student-t CDF and PDF wrappers, matching the original
toolbox's call signature (explicit x, y, rho, nu arguments,
broadcast element-wise) rather than requiring the caller to assemble a full
covariance matrix.
Ported from HSF toolbox stats/{cdfbvn,pdfbvn,cdfbvt}.m.
Translation notes:
genz/{bvn,bvnu}.mare Alan Genz's Drezner-Wesolowsky quadrature for the bivariate normal CDF;stats/cdfbvn.mis a thin wrapper aroundbvnu.m.scipy.stats.multivariate_normal.cdfalready computes this (for any dimension, not just 2) viascipy.stats._mvn, which is itself built on Alan Genz's own Fortranmvndstroutine -- the same author, the same underlying algorithm.bvn_cdfhere is therefore a thinscipy-backed convenience wrapper with the original's(x, y, rho)call signature, not a hand-rolled reimplementation ofbvnu.m's quadrature.stats/cdfbvt.mwraps MATLAB's Statistics Toolboxmvtcdf; ported the same way, viascipy.stats.multivariate_t.cdf(arbitrary dimension, available since SciPy 1.9).- Both
bvn_cdfandbvt_cdfcliprhoa hair inside (-1, 1) before building the 2x2 correlation matrix:scipy'smultivariate_normal/multivariate_traiseLinAlgErroron an exactly-singular (|rho| == 1) matrix rather than falling back to the (well-defined) degenerate limit, andrhocan round to exactly +/-1 in float64 even when the caller's inputs are only extremely close to that boundary -- e.g.stats.distributions.skew_t_cdfhits this atetanear 0, wheredelta = (1 - eta^2) / (1 + eta^2)underflows to1.0. Clipping is a robustness addition beyond a literal transliteration of the original, not a behavior change: the clipped and unclipped results agree to float64 precision anywherescipywould have succeeded anyway. genz/{qsimvn,qsimvnv,qsimvt,qsimvtv,qsilatmvnv,qsilatmvtv,qsimvnauto, mvnrcnv}.m(8 of the 10 files ingenz/) are randomized quasi-Monte-Carlo integrators solving exactly the same problem asscipy.stats.multivariate_normal.cdf/multivariate_t.cdf-- arbitrary- dimension MVN/MVT orthant probabilities -- via the same Genz (1992) algorithm family (Niederreiter/Cranley-Patterson randomized lattice rules);mvnrcnv.mis a constant-correlation special case of the same problem. None of these are hand-ported: doing so would duplicate already-available, better-testedscipyfunctionality (the same author's own algorithm, reimplemented) with no functional gain. Seestats.distributions.mvn_cdf/mvn_pdffor the general n-dimensional case (already ported);bvn_cdf/bvt_cdfbelow just add the bivariate- specific call signature (x,y,rhorather than a point + covariance matrix) for parity with how the original toolbox's other modules call these.
bvn_cdf(x, y, rho)
¶
Bivariate standard normal CDF P(X <= x, Y <= y), for (X, Y) with unit
variances and correlation rho. Broadcasts element-wise over
x/y/rho.
Original: stats/cdfbvn.m (via genz/bvnu.m)
Source code in src/quanttoolbox/stats/multivariate.py
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | |
bvn_pdf(x1, x2, mu1, mu2, sigma1, sigma2, rho)
¶
Bivariate normal PDF at (x1, x2), for (X1, X2) ~ N((mu1, mu2), Sigma)
with std devs (sigma1, sigma2) and correlation rho. Closed-form, so
(unlike bvn_cdf) evaluated directly rather than via scipy.
Original: stats/pdfbvn.m
Source code in src/quanttoolbox/stats/multivariate.py
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | |
bvt_cdf(x, y, rho, nu)
¶
Bivariate Student-t CDF P(X <= x, Y <= y), for (X, Y) with unit scale,
correlation rho, and nu degrees of freedom. Broadcasts element-wise
over x/y/rho/nu.
Original: stats/cdfbvt.m (via MATLAB's mvtcdf)
Source code in src/quanttoolbox/stats/multivariate.py
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | |
stats.moments¶
Python alternatives
rolling_correlation/rolling_volatility: switch to pandas.DataFrame.rolling().corr()/.std() for the standard case — Cython-backed, meaningfully faster. Keep ours only for the method=2 "returns computed within each window" variant pandas doesn't offer. active_share, herfindahl_index, asynchronous_cov, weekly_cov: keep — no general-purpose equivalent exists.
quanttoolbox.stats.moments
¶
Sample moments, dispersion, covariance/correlation, and portfolio-overlap measures.
Ported from QuantToolBox/stats/{skewness_coefficient,kurtosis_coefficient, herfindahl_index,mean_absolute_difference,cov2cor,cor2cov,corrx, pearson_correlation,active_share,active_share_upper_bound,asynchronous_cov, weekly_cov,rolling_correlation,rolling_volatility}.m
Translation notes:
- Every "column vector output for each column of x" pattern (MATLAB loops
over
cols(x)) is replaced by native NumPy/pandas vectorized operations (axis=0reductions) instead of an explicit Python loop. packr(drop rows containing NaN before computing a statistic) is replaced by pandas' NaN-aware reductions (.mean(),.std(), ... skip NaN by default) or explicit~np.isnan(...)masks where a NumPy-only implementation is used.- Excess-kurtosis convention: like the original,
kurtosishere returns the raw fourth standardized moment (3.0 for a normal distribution), not scipy's default excess-kurtosis convention (which subtracts 3).
active_share(x, b)
¶
Portfolio active share: 0.5 * sum(|weight - benchmark weight|).
Original: stats/active_share.m
Source code in src/quanttoolbox/stats/moments.py
172 173 174 175 176 177 | |
active_share_upper_bound(b, x_minus=0.0, x_plus=1.0)
¶
Compute the maximum achievable active share given per-position weight bounds [x_minus, x_plus] that must still sum to 1.
Greedy algorithm: iteratively push each position to whichever bound (x_minus or x_plus) is farthest from the benchmark weight, subject to the running total staying feasible.
Original: stats/active_share_upper_bound.m
Returns:
| Name | Type | Description |
|---|---|---|
as_max |
the maximum active share achieved.
|
|
x_max |
the weight vector achieving it (None if infeasible).
|
|
retcode |
1 if converged exactly to a total weight of 1, else 0/-1.
|
|
Source code in src/quanttoolbox/stats/moments.py
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 | |
asynchronous_cov(x, method=0)
¶
Newey-West-style covariance correction for asynchronously-timed data (e.g. assets trading in different time zones), using a lag-1 autocovariance adjustment.
method=0: half-window correction (default). method=1: full-window.
Original: stats/asynchronous_cov.m
Source code in src/quanttoolbox/stats/moments.py
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 | |
corr_to_cov(sigma, rho)
¶
Recombine standard deviations and a correlation matrix into a covariance matrix.
Original: stats/cor2cov.m
Source code in src/quanttoolbox/stats/moments.py
109 110 111 112 113 114 115 116 117 118 119 | |
corrx(x)
¶
Sample correlation matrix and covariance matrix of x (columns = variables, rows dropped if they contain NaN).
Original: stats/corrx.m
Source code in src/quanttoolbox/stats/moments.py
122 123 124 125 126 127 128 129 130 131 132 133 134 135 | |
cov_to_corr(cov_matrix)
¶
Decompose a covariance matrix into (std devs, correlation matrix).
Original: stats/cov2cor.m
Source code in src/quanttoolbox/stats/moments.py
98 99 100 101 102 103 104 105 106 | |
herfindahl_index(x, b=None)
¶
Herfindahl index H = sum(x^2), its inverse N = 1/H (effective number of positions), and optionally the same for a benchmark b.
Original: stats/herfindahl_index.m
Source code in src/quanttoolbox/stats/moments.py
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | |
kurtosis(x)
¶
Sample kurtosis (raw, not excess) of each column of x. Original: stats/kurtosis_coefficient.m
Source code in src/quanttoolbox/stats/moments.py
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | |
mean_absolute_difference(x)
¶
Gini-style mean absolute pairwise difference of each column of x.
Original: stats/mean_absolute_difference.m
Source code in src/quanttoolbox/stats/moments.py
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | |
pearson_correlation(x, y)
¶
Column-wise (or cross) Pearson correlation between x and y.
If x and y have the same number of columns, returns the correlation of each matching pair of columns. Otherwise returns the full nX x nY cross-correlation matrix.
Original: stats/pearson_correlation.m
Source code in src/quanttoolbox/stats/moments.py
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | |
rolling_correlation(x, y, n_lags, method=0)
¶
Rolling (trailing n_lags-window) correlation, and each series' rolling volatility, column-by-column.
method=1 treats both x and y as prices and converts to returns up front. method=2 converts to returns within each window (useful for illiquid series where a fixed global return series would drop too many rows).
Original: stats/rolling_correlation.m
Source code in src/quanttoolbox/stats/moments.py
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 | |
rolling_volatility(x, n_lags, method=0)
¶
Rolling (trailing n_lags-window) volatility of each column of x.
method=1 treats x as prices and converts to simple returns first.
Original: stats/rolling_volatility.m
Source code in src/quanttoolbox/stats/moments.py
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 | |
skewness(x)
¶
Sample skewness of each column of x. Original: stats/skewness_coefficient.m
Source code in src/quanttoolbox/stats/moments.py
27 28 29 30 31 32 33 34 35 36 37 38 39 40 | |
weekly_cov(x_weekly, x_daily=None)
¶
Covariance matrix using weekly correlations but daily-data volatilities (useful when daily vol estimates are more reliable than weekly ones but weekly correlations are less affected by asynchronous trading).
Original: stats/weekly_cov.m
Source code in src/quanttoolbox/stats/moments.py
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 | |
Examples¶
Black-Litterman view sensitivity across five scenarios — rpb/test_bl2.py
"""Translated from Examples/rpb/test_bl2.m -- Black-Litterman sensitivity
analysis across 6 scenarios (base case + 5 view/uncertainty/tau variants),
each solved as a fixed-risk-aversion MVO portfolio."""
import numpy as np
from quanttoolbox.linalg.special_matrices import xpnd
from quanttoolbox.portfolio.black_litterman import black_litterman_moments, implied_risk_premia
from quanttoolbox.portfolio.mean_variance import mvo_portfolio
from quanttoolbox.stats.moments import corr_to_cov
sigma = np.array([0.15, 0.20, 0.25, 0.30])
rho = xpnd(np.array([1.00, 0.10, 1.00, 0.40, 0.70, 1.00, 0.50, 0.40, 0.80, 1.00]), method=1)
cov_matrix = corr_to_cov(sigma, rho)
x0 = np.array([0.40, 0.30, 0.20, 0.10])
r = 0.03
irp = implied_risk_premia(x0, cov_matrix, sharpe_ratio=0.25)
mu_tilde = r + irp.pi
gamma0 = irp.gamma
scenarios = [
dict(
P=np.array([[1, 0, 0, 0], [0, 1, -1, 0]], dtype=float),
Q=np.array([0.04, -0.01]),
Omega=np.diag([0.10**2, 0.05**2]),
tau=1,
),
dict(
P=np.array([[1, 0, 0, 0], [0, 1, -1, 0]], dtype=float),
Q=np.array([0.07, -0.01]),
Omega=np.diag([0.10**2, 0.05**2]),
tau=1,
),
dict(
P=np.array([[1, 0, 0, 0], [0, 1, -1, 0]], dtype=float),
Q=np.array([0.04, -0.01]),
Omega=np.diag([0.20**2, 0.20**2]),
tau=1,
),
dict(
P=np.array([[1, 0, 0, 0], [0, 1, -1, 0]], dtype=float),
Q=np.array([0.04, -0.01]),
Omega=np.diag([0.10**2, 0.05**2]),
tau=0.10,
),
dict(
P=np.array([[1, 0, 0, 0], [0, 1, -1, 0]], dtype=float),
Q=np.array([0.04, -0.01]),
Omega=np.diag([0.10**2, 0.05**2]),
tau=0.01,
),
]
results = [
dict(weights=x0, mu=x0 @ mu_tilde, sigma=np.sqrt(x0 @ cov_matrix @ x0), alpha=0.0, te=0.0)
]
for s in scenarios:
bl = black_litterman_moments(mu_tilde, s["tau"] * cov_matrix, s["P"], s["Q"], s["Omega"])
mvo = mvo_portfolio(bl.mu_bar, cov_matrix, gamma=gamma0, lb=0.0, ub=1.0)
alpha = (mvo.weights - x0) @ bl.mu_bar
te = np.sqrt((mvo.weights - x0) @ cov_matrix @ (mvo.weights - x0))
results.append(
dict(weights=mvo.weights, mu=mvo.expected_return, sigma=mvo.volatility, alpha=alpha, te=te)
)
for i, r_ in enumerate(results):
print(
f"scenario {i}: weights={np.round(r_['weights'],4)} mu={round(r_['mu'],5)} te={round(r_['te'],5)}"
)
Equal risk contribution and box-constrained risk budgeting — rpb/test_box1.py
"""Translated from Examples/rpb/test_box1.m -- ERC and box-constrained
("C-ERC") risk budgeting portfolios at progressively wider bounds around
a starting position."""
import numpy as np
from quanttoolbox.linalg.special_matrices import xpnd
from quanttoolbox.portfolio.risk_budgeting import erc_portfolio, solve_box_constrained
from quanttoolbox.stats.moments import corr_to_cov
x0 = np.array([0.29, 0.25, 0.23, 0.18, 0.05])
sigma = np.array([0.20, 0.20, 0.25, 0.15, 0.25])
rho = xpnd(
np.array(
[1.00, 0.40, 1.00, 0.70, 0.75, 1.00, 0.60, 0.55, 0.90, 1.00, 0.70, 0.60, 0.70, 0.65, 1.00]
),
method=1,
)
cov_matrix = corr_to_cov(sigma, rho)
r1 = erc_portfolio(cov_matrix)
print("ERC weights:", np.round(r1.weights, 4))
for delta in [0.02, 0.07, 0.20]:
x_minus, x_plus = x0 - delta, x0 + delta
r = solve_box_constrained(cov_matrix, x_minus=x_minus, x_plus=x_plus, x0=x0)
print(f"box (delta={delta}) weights:", np.round(r.weights, 4), "converged:", r.converged)
Mean-variance frontier: risk-aversion, return, and volatility targets — rpb/test_mvo2.py
"""Translated from Examples/rpb/test_mvo2.m -- Roncalli [2013], "Introduction
to Risk Parity and Budgeting", Example 1 (pages 7-8): the same 4-asset
mean-variance problem evaluated three ways -- the gamma-problem (pick a
risk-aversion, solve directly), the mu-problem (pick a target expected
return, bisect on gamma to hit it), and the sigma-problem (pick a target
volatility, bisect on gamma to hit it). All three route through
`compute_mvo_portfolio.m`'s three branches; here that's
`mvo_frontier`/`mvo_target_portfolio`.
The original passes `lb=0, ub=0` (MATLAB's "use the default -100/100 wide
bounds" sentinel); passed through explicitly here as `lb=-100, ub=100`."""
import numpy as np
from quanttoolbox.linalg.special_matrices import xpnd
from quanttoolbox.portfolio.mean_variance import mvo_frontier, mvo_target_portfolio
from quanttoolbox.stats.moments import corr_to_cov
mu = np.array([0.05, 0.06, 0.08, 0.06])
sigma = np.array([0.15, 0.20, 0.25, 0.30])
rho = xpnd(np.array([1.00, 0.10, 1.00, 0.40, 0.70, 1.00, 0.50, 0.40, 0.80, 1.00]), method=1)
cov_matrix = corr_to_cov(sigma, rho)
print("1. gamma-problem (page 7)")
gamma_values = np.array([0.00, 0.20, 0.50, 1.00, 2.00, 5.00])
results = mvo_frontier(mu, cov_matrix, gamma_values, lb=-100.0, ub=100.0)
for g, r in zip(gamma_values, results, strict=False):
print(
f" gamma={g:5.2f} mu={100 * r.expected_return:6.2f} sigma={100 * r.volatility:6.2f} "
f"w={np.round(100 * r.weights, 2)}"
)
print("\n2. mu-problem (page 8)")
mu_targets = np.array([5.00, 6.00, 7.00, 8.00, 9.00]) / 100
mu_results = mvo_target_portfolio(mu, cov_matrix, mu_targets, problem="mu", lb=-100.0, ub=100.0)
for target, r in zip(mu_targets, mu_results, strict=False):
print(
f" target_mu={100 * target:5.2f} gamma={r.gamma:6.3f} mu={100 * r.expected_return:6.2f} "
f"sigma={100 * r.volatility:6.2f} w={np.round(100 * r.weights, 2)}"
)
print("\n3. sigma-problem (page 8)")
sigma_targets = np.array([15.00, 20.00, 25.00, 30.00, 35.00]) / 100
sigma_results = mvo_target_portfolio(
mu, cov_matrix, sigma_targets, problem="sigma", lb=-100.0, ub=100.0
)
for target, r in zip(sigma_targets, sigma_results, strict=False):
print(
f" target_sigma={100 * target:5.2f} gamma={r.gamma:6.3f} mu={100 * r.expected_return:6.2f} "
f"sigma={100 * r.volatility:6.2f} w={np.round(100 * r.weights, 2)}"
)
Mean-variance optimization plus ridge/lasso-penalized portfolios — rpb/test_lasso1.py
"""Translated from Examples/rpb/test_lasso1.m -- Roncalli [2013],
"Introduction to Risk Parity and Budgeting", Example 1 (page 53):
compares an unconstrained gamma-problem MVO portfolio against the same
problem with a budget (sum-to-1) constraint, and ridge/lasso-penalized
variants (toward 0 and toward equal-weight) of the unconstrained
problem.
`compute_mvo_portfolio`/`quadprog_ridge`/`quadprog_lasso` map to
`mvo_portfolio`/`solve_qp(..., ridge_penalty=...)`/`solve_qp(...,
lasso_penalty=...)` as established in test_lasso3.py/test_lasso5.py; the
original's `0, 0` sentinel arguments for "no equality constraint" /
"no bounds" map to simply omitting those keyword arguments."""
import numpy as np
from quanttoolbox.linalg.special_matrices import xpnd
from quanttoolbox.optim.quadprog import solve_qp
from quanttoolbox.portfolio.mean_variance import mvo_portfolio
from quanttoolbox.stats.moments import corr_to_cov
mu = np.array([0.05, 0.06, 0.08, 0.06])
sigma = np.array([0.15, 0.20, 0.25, 0.30])
rho = xpnd(np.array([1.00, 0.10, 1.00, 0.40, 0.70, 1.00, 0.50, 0.40, 0.80, 1.00]), method=1)
cov_matrix = corr_to_cov(sigma, rho)
n = 4
x0 = np.full(n, 1 / n)
# Case gamma-problem
gamma_x = 0.5
x1 = mvo_portfolio(mu, cov_matrix, gamma=gamma_x).weights
x2 = mvo_portfolio(
mu, cov_matrix, gamma=gamma_x, a_eq=np.ones((1, n)), b_eq=np.array([1.0])
).weights
lambda_ridge = 0.03
s_ridge = lambda_ridge * np.eye(n)
x3 = solve_qp(cov_matrix, gamma_x * mu, ridge_penalty=(s_ridge, np.zeros(n)))
x4 = solve_qp(cov_matrix, gamma_x * mu, ridge_penalty=(s_ridge, x0))
lambda_lasso = 0.03 / 2
s_lasso = lambda_lasso * np.ones(n)
x5 = solve_qp(cov_matrix, gamma_x * mu, lasso_penalty=(s_lasso, np.zeros(n)))
x6 = solve_qp(cov_matrix, gamma_x * mu, lasso_penalty=(s_lasso, x0))
results = 100 * np.column_stack([x1, x2, x3, x4, x5, x6])
print(" x1 x2 x3 x4 x5 x6")
print(np.round(results, 2))
Minimum-variance portfolio under general linear constraints — rpb/test_minvar2.py
"""Translated from Examples/rpb/test_minvar2.m -- minimum-variance
portfolio with general linear equality/inequality constraints and box
bounds."""
import numpy as np
from quanttoolbox.linalg.special_matrices import xpnd
from quanttoolbox.portfolio.mean_variance import minvar_portfolio
from quanttoolbox.stats.moments import corr_to_cov
sigma = np.array([0.15, 0.20, 0.25, 0.30])
rho = xpnd(np.array([1.00, 0.10, 1.00, 0.40, 0.70, 1.00, 0.50, 0.40, 0.80, 1.00]), method=1)
cov_matrix = corr_to_cov(sigma, rho)
a_eq = np.array([[1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 0.0, 0.0]])
b_eq = np.array([1.0, 0.0])
c_ineq = np.array([[0.0, 0.0, 0.0, -1.0]])
d_ineq = np.array([-0.90])
r = minvar_portfolio(
cov_matrix, a_eq=a_eq, b_eq=b_eq, c_ineq=c_ineq, d_ineq=d_ineq, lb=-1.50, ub=2.00
)
print("weights:", np.round(r.weights, 3))
print("volatility:", round(r.volatility, 5))
Mixed ridge+lasso penalties toward two different target vectors — rpb/test_lasso5.py
"""Translated from Examples/rpb/test_lasso5.m -- ridge/lasso/mixed
portfolios with two different ridge/lasso target vectors (equal-weight
and a custom 20/20/30/30 target)."""
import numpy as np
from quanttoolbox.linalg.special_matrices import xpnd
from quanttoolbox.optim.quadprog import solve_qp
from quanttoolbox.portfolio.mean_variance import mvo_portfolio
from quanttoolbox.stats.moments import corr_to_cov
mu = np.array([0.05, 0.06, 0.08, 0.06])
sigma = np.array([0.15, 0.20, 0.25, 0.30])
rho = xpnd(np.array([1.00, 0.10, 1.00, 0.40, 0.70, 1.00, 0.50, 0.40, 0.80, 1.00]), method=1)
cov_matrix = corr_to_cov(sigma, rho)
n = 4
a_eq, b_eq = np.ones((1, n)), np.array([1.0])
gamma_x = 0.5
y1 = np.full(n, 1 / n)
y2 = np.array([0.20, 0.20, 0.30, 0.30])
S_ridge = np.diag(np.diag(cov_matrix))
lambda_lasso = 0.005
x1 = mvo_portfolio(mu, cov_matrix, gamma=gamma_x, a_eq=a_eq, b_eq=b_eq, lb=0.0, ub=1.0).weights
x2 = solve_qp(
cov_matrix, gamma_x * mu, a_eq=a_eq, b_eq=b_eq, lb=0.0, ub=1.0, ridge_penalty=(S_ridge, y1)
)
x4 = solve_qp(
cov_matrix, gamma_x * mu, a_eq=a_eq, b_eq=b_eq, lb=0.0, ub=1.0, lasso_penalty=(lambda_lasso, y1)
)
x6 = solve_qp(
cov_matrix,
gamma_x * mu,
a_eq=a_eq,
b_eq=b_eq,
lb=0.0,
ub=1.0,
ridge_penalty=(S_ridge, y1),
lasso_penalty=(lambda_lasso, y1),
)
# mixed with DIFFERENT targets for ridge (toward y1) vs lasso (toward y2)
x8 = solve_qp(
cov_matrix,
gamma_x * mu,
a_eq=a_eq,
b_eq=b_eq,
lb=0.0,
ub=1.0,
ridge_penalty=(S_ridge, y1),
lasso_penalty=(lambda_lasso, y2),
)
for name, x in [
("MVO", x1),
("Ridge->y1", x2),
("Lasso->y1", x4),
("Mixed(ridge->y1,lasso->y1)", x6),
("Mixed(ridge->y1,lasso->y2)", x8),
]:
print(f"{name}: {np.round(x, 4)}")
Ridge, lasso, and mixed-norm penalized portfolios — rpb/test_lasso3.py
"""Translated from Examples/rpb/test_lasso3.m (byte-identical to
test_lasso4.m) -- MVO/ridge/lasso/mixed-penalty portfolios via the
consolidated solve_qp (replacing the original's separate quadprog_ridge/
quadprog_lasso/quadprog_mixed calls -- see optim/quadprog.py docstring)."""
import numpy as np
from quanttoolbox.linalg.special_matrices import xpnd
from quanttoolbox.optim.quadprog import solve_qp
from quanttoolbox.portfolio.mean_variance import mvo_portfolio
from quanttoolbox.stats.moments import corr_to_cov
mu = np.array([0.05, 0.06, 0.08, 0.06])
sigma = np.array([0.15, 0.20, 0.25, 0.30])
rho = xpnd(np.array([1.00, 0.10, 1.00, 0.40, 0.70, 1.00, 0.50, 0.40, 0.80, 1.00]), method=1)
cov_matrix = corr_to_cov(sigma, rho)
n = 4
x0 = np.full(n, 1 / n)
gamma_x = 0.5
a_eq, b_eq = np.ones((1, n)), np.array([1.0])
# gamma-problem (plain MVO)
r1 = mvo_portfolio(mu, cov_matrix, gamma=gamma_x, a_eq=a_eq, b_eq=b_eq, lb=0.0, ub=1.0)
x1 = r1.weights
# ridge-problem: penalize toward zero, scaled by each asset's own variance
S_ridge = np.diag(np.diag(cov_matrix))
x2 = solve_qp(
cov_matrix, gamma_x * mu, a_eq=a_eq, b_eq=b_eq, lb=0.0, ub=1.0, ridge_penalty=(S_ridge, x0)
)
# lasso-problem: L1 penalty toward equal weight
lambda_lasso = 0.005
x3 = solve_qp(
cov_matrix, gamma_x * mu, a_eq=a_eq, b_eq=b_eq, lb=0.0, ub=1.0, lasso_penalty=(lambda_lasso, x0)
)
# mixed-problem: both ridge and lasso, toward various targets
x4 = solve_qp(
cov_matrix,
gamma_x * mu,
a_eq=a_eq,
b_eq=b_eq,
lb=0.0,
ub=1.0,
ridge_penalty=(S_ridge, np.zeros(n)),
lasso_penalty=(lambda_lasso, np.zeros(n)),
)
x5 = solve_qp(
cov_matrix,
gamma_x * mu,
a_eq=a_eq,
b_eq=b_eq,
lb=0.0,
ub=1.0,
ridge_penalty=(S_ridge, x0),
lasso_penalty=(lambda_lasso, np.zeros(n)),
)
x6 = solve_qp(
cov_matrix,
gamma_x * mu,
a_eq=a_eq,
b_eq=b_eq,
lb=0.0,
ub=1.0,
ridge_penalty=(S_ridge, np.zeros(n)),
lasso_penalty=(lambda_lasso, x0),
)
x7 = solve_qp(
cov_matrix,
gamma_x * mu,
a_eq=a_eq,
b_eq=b_eq,
lb=0.0,
ub=1.0,
ridge_penalty=(S_ridge, x0),
lasso_penalty=(lambda_lasso, x0),
)
for name, x in [
("MVO", x1),
("Ridge", x2),
("Lasso", x3),
("Mixed(0,0)", x4),
("Mixed(EW,0)", x5),
("Mixed(0,EW)", x6),
("Mixed(EW,EW)", x7),
]:
print(f"{name}: {np.round(x, 4)}")
Risk budgeting toward unequal target budgets — rpb/test_erc3.py
"""Translated from Examples/rpb/test_erc3.m -- Example 17 (page 123),
Roncalli (2013). RB portfolio with unequal target budgets."""
import numpy as np
from quanttoolbox.linalg.special_matrices import xpnd
from quanttoolbox.portfolio.risk_budgeting import risk_contribution, solve_unconstrained
from quanttoolbox.stats.moments import corr_to_cov
sigma = np.array([0.15, 0.20, 0.30, 0.10])
rho = xpnd(np.array([1.00, 0.50, 1.00, 0.00, 0.20, 1.00, -0.10, 0.40, 0.70, 1.00]), method=1)
cov_matrix = corr_to_cov(sigma, rho)
x = np.full(4, 0.25)
rc = risk_contribution(x, cov_matrix)
print("equal-weight risk contribution:", rc.risk, np.round(100 * rc.pct_risk_contribution, 2))
b = np.array([0.20, 0.20, 0.30, 0.30])
r = solve_unconstrained(cov_matrix, b=b, method="ccd")
print("RB weights (target budgets 20/20/30/30):", np.round(r.weights, 4))
print("converged:", r.converged, "n_iters:", r.n_iters)
Risk contribution decomposition and equal-budget risk budgeting — rpb/test_erc2.py
"""Translated from Examples/rpb/test_erc2.m -- Example 7 (page 80),
Roncalli (2013). Risk contribution decomposition + risk budgeting at
various target budgets."""
import numpy as np
from quanttoolbox.linalg.special_matrices import xpnd
from quanttoolbox.portfolio.risk_budgeting import risk_contribution, solve_unconstrained
from quanttoolbox.stats.moments import corr_to_cov
sigma = np.array([0.30, 0.20, 0.15])
rho = xpnd(np.array([1.00, 0.80, 1.00, 0.50, 0.30, 1.00]), method=1)
cov_matrix = corr_to_cov(sigma, rho)
x = np.array([0.50, 0.20, 0.30])
rc = risk_contribution(x, cov_matrix)
print("fixed-x risk contribution:", rc.risk, np.round(100 * rc.pct_risk_contribution, 2))
r_equal = solve_unconstrained(cov_matrix, b=np.full(3, 1 / 3), method="ccd")
print("equal-budget RB weights:", np.round(r_equal.weights, 4))
r_custom = solve_unconstrained(cov_matrix, b=x, method="ccd")
print("target-x-as-budget RB weights:", np.round(r_custom.weights, 4))
stats.regression.ols¶
Python alternatives
Hybrid: statsmodels.OLS/WLS is more complete for plain unrestricted regression. Keep this module for the restriction=(RR, r) linear-restriction parameterization, which statsmodels doesn't support as directly.
quanttoolbox.stats.regression.ols
¶
Ordinary least squares, centering/standardization, conditional-normal regression, and principal component analysis.
Ported from QuantToolBox/stats/{regOLS,regCenter,regStandardize,regCND, regPCA}.m
Translation notes:
regOLSusesnumpy.linalg.lstsq(QR-based) internally for numerical stability, rather than MATLAB'sinv(x'*x) * x'*ynormal equations, but still returns the explicitinv(x'x)covariance factor since downstream code needs it for standard errors.regPCAdecomposes a correlation matrix (following the original), usingnumpy.linalg.eigh(the matrix is symmetric by construction) instead of MATLAB'seig; eigenvalues/vectors are then sorted descending to match the original's convention.- The MATLAB
global Print_Resultsdiagnostic-printing branch inregPCAis dropped -- callers should print/inspect the returnedPCAResultfields themselves.
center(x)
¶
Center each column of x by its (NaN-dropped) mean.
Original: stats/regCenter.m
Source code in src/quanttoolbox/stats/regression/ols.py
70 71 72 73 74 75 76 77 | |
conditional_normal_regression(mu_y=None, mu_x=None, sigma_yy=None, sigma_yx=None, sigma_xx=None, *, mu=None, sigma=None)
¶
Linear regression coefficients implied by a joint normal distribution's moments (rather than estimated from a data sample).
Two calling conventions, matching the two MATLAB entry points:
conditional_normal_regression(mu_y, mu_x, sigma_yy, sigma_yx, sigma_xx): regress a single y on x given the joint (y, x) moments.conditional_normal_regression(mu=mu, sigma=sigma): leave-one-out regression of each variable inmu/sigmaon all the others.
Original: stats/regCND.m
Source code in src/quanttoolbox/stats/regression/ols.py
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | |
ols(y, x)
¶
Ordinary least squares regression, dropping rows with missing y or x.
Original: stats/regOLS.m
Source code in src/quanttoolbox/stats/regression/ols.py
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | |
pca(x, num_factors=None, normalize=False)
¶
Principal component analysis on a data matrix (standardized first) or directly on a pre-computed correlation matrix.
Original: stats/regPCA.m
Source code in src/quanttoolbox/stats/regression/ols.py
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | |
standardize(x)
¶
Center and scale each column of x to unit variance (using the NaN-dropped mean/std).
Original: stats/regStandardize.m
Source code in src/quanttoolbox/stats/regression/ols.py
80 81 82 83 84 85 86 87 88 89 90 | |
Examples¶
Elastic-net regression path — stats/elasticnet1.py
"""Translated from Examples/stats/elasticnet1.m and elasticnet2.m --
elastic net regression path at two different alpha (L1/L2 mix) values,
on the same 15-observation dataset used in ridge1.m/lasso1.m."""
import numpy as np
from quanttoolbox.stats.regression.lasso import elastic_net_ccd
from quanttoolbox.stats.regression.ols import standardize
data = np.array(
[
[3.1, 2.8, 4.3, 0.3, 2.2, 3.5],
[24.9, 5.9, 3.6, 3.2, 0.7, 6.4],
[27.3, 6.0, 9.6, 7.6, 9.5, 0.9],
[25.4, 8.4, 5.4, 1.8, 1.0, 7.1],
[46.1, 5.2, 7.6, 8.3, 0.6, 4.5],
[45.7, 6.0, 7.0, 9.6, 0.6, 0.6],
[47.4, 6.1, 1.0, 8.5, 9.6, 8.6],
[-1.8, 1.2, 9.6, 2.7, 4.8, 5.8],
[20.8, 3.2, 5.0, 4.2, 2.7, 3.6],
[6.8, 0.5, 9.2, 6.9, 9.3, 0.7],
[12.9, 7.9, 9.1, 1.0, 5.9, 5.4],
[37.0, 1.8, 1.3, 9.2, 6.1, 8.3],
[14.7, 7.4, 5.6, 0.9, 5.6, 3.9],
[-3.2, 2.3, 6.6, 0.0, 3.6, 6.4],
[44.3, 7.7, 2.2, 6.5, 1.3, 0.7],
]
)
y = standardize(data[:, 0])
x = standardize(data[:, 1:6])
for alpha, lam in [(0.50, 1.0), (0.25, 1.0)]:
beta, path = elastic_net_ccd(y, x, lambda_=lam, alpha=alpha, n_iters=200)
print(f"alpha={alpha}, lambda={lam}: beta={np.round(beta,4)}")
Lasso path with R-squared, degrees of freedom, and complexity — stats/lasso1.py
"""Translated from Examples/stats/lasso1.m -- penalized-form lasso path at
5 lambda values (numeric core only; the original's 501-point tau-sweep and
its multi-line coefficient-path plot are dropped -- the same lambda
values and dataset are already used by lasso2.py, which this extends with
R^2/degrees-of-freedom/complexity reporting to match lasso1.m's own
output table)."""
import numpy as np
from quanttoolbox.stats.regression.lasso import lasso_ccd
from quanttoolbox.stats.regression.ols import standardize
data = np.array(
[
[3.1, 2.8, 4.3, 0.3, 2.2, 3.5],
[24.9, 5.9, 3.6, 3.2, 0.7, 6.4],
[27.3, 6.0, 9.6, 7.6, 9.5, 0.9],
[25.4, 8.4, 5.4, 1.8, 1.0, 7.1],
[46.1, 5.2, 7.6, 8.3, 0.6, 4.5],
[45.7, 6.0, 7.0, 9.6, 0.6, 0.6],
[47.4, 6.1, 1.0, 8.5, 9.6, 8.6],
[-1.8, 1.2, 9.6, 2.7, 4.8, 5.8],
[20.8, 3.2, 5.0, 4.2, 2.7, 3.6],
[6.8, 0.5, 9.2, 6.9, 9.3, 0.7],
[12.9, 7.9, 9.1, 1.0, 5.9, 5.4],
[37.0, 1.8, 1.3, 9.2, 6.1, 8.3],
[14.7, 7.4, 5.6, 0.9, 5.6, 3.9],
[-3.2, 2.3, 6.6, 0.0, 3.6, 6.4],
[44.3, 7.7, 2.2, 6.5, 1.3, 0.7],
]
)
y = standardize(data[:, 0])
x = standardize(data[:, 1:6])
beta_ols = np.linalg.inv(x.T @ x) @ (x.T @ y)
tss = np.mean(y**2)
print("lambda |beta| tau=sum|beta| rss R2 df complexity")
for lam in [0.0, 0.9, 2.5, 5.5, 7.5]:
beta, _ = lasso_ccd(y, x, lambda_=lam, n_iters=200)
u = y - x @ beta
rss = np.mean(u**2)
r2 = 1 - rss / tss
tau = np.sum(np.abs(beta))
df = int(np.sum(np.abs(beta) >= 1e-6))
complexity = 1.0 / df if df > 0 else np.inf
print(
f"{lam:6.2f} {np.round(beta, 4)} tau={tau:.4f} rss={rss:.4f} "
f"R2={r2:.4f} df={df} complexity={complexity:.4f}"
)
Norm-budget (tau-targeted) ridge regression vs. fixed-lambda ridge — stats/ridge2.py
"""Translated from Examples/stats/ridge2.m -- tau-targeted ridge regression
(`ridge_tau_targeted`) cross-checked against fixed-lambda ridge (`ridge`),
in both absolute and OLS-relative tau modes, on the same 15-observation
dataset used throughout stats/."""
import numpy as np
from quanttoolbox.stats.regression.ols import standardize
from quanttoolbox.stats.regression.ridge import ridge, ridge_tau_targeted
data = np.array(
[
[3.1, 2.8, 4.3, 0.3, 2.2, 3.5],
[24.9, 5.9, 3.6, 3.2, 0.7, 6.4],
[27.3, 6.0, 9.6, 7.6, 9.5, 0.9],
[25.4, 8.4, 5.4, 1.8, 1.0, 7.1],
[46.1, 5.2, 7.6, 8.3, 0.6, 4.5],
[45.7, 6.0, 7.0, 9.6, 0.6, 0.6],
[47.4, 6.1, 1.0, 8.5, 9.6, 8.6],
[-1.8, 1.2, 9.6, 2.7, 4.8, 5.8],
[20.8, 3.2, 5.0, 4.2, 2.7, 3.6],
[6.8, 0.5, 9.2, 6.9, 9.3, 0.7],
[12.9, 7.9, 9.1, 1.0, 5.9, 5.4],
[37.0, 1.8, 1.3, 9.2, 6.1, 8.3],
[14.7, 7.4, 5.6, 0.9, 5.6, 3.9],
[-3.2, 2.3, 6.6, 0.0, 3.6, 6.4],
[44.3, 7.7, 2.2, 6.5, 1.3, 0.7],
]
)
y = standardize(data[:, 0])
x = standardize(data[:, 1:6])
beta_ols = np.linalg.inv(x.T @ x) @ (x.T @ y)
tau_ols = np.sum(beta_ols**2)
print("tau(ols) =", round(tau_ols, 4))
# `ridge()` returns (beta, df, complexity) -- it doesn't report the
# resulting L2 budget, so tau_ridge (the quantity `ridge_tau_targeted`
# was aiming for) is recomputed here as sum(beta**2), to cross-check that
# fixed-lambda ridge at `lambda_out` reproduces the same beta/tau
# `ridge_tau_targeted` found by its grid search.
# Absolute tau targets
tau = np.array([0.7, 0.9, 1.1])
beta_ridge2, lambda_out, df_ridge2, complexity2 = ridge_tau_targeted(y, x, tau)
beta_ridge, df_ridge, complexity = ridge(y, x, lambda_out)
tau_ridge = np.sum(beta_ridge**2, axis=1)
print("\nAbsolute-tau analysis: tau target, tau achieved (fixed-lambda ridge), lambda")
print(np.round(np.column_stack([tau, tau_ridge, lambda_out]), 4))
# Relative tau targets (fraction of tau_ols)
tau = np.array([0.25, 0.50, 0.75, 1.00])
beta_ridge2, lambda_out, df_ridge2, complexity2 = ridge_tau_targeted(y, x, tau, relative=True)
beta_ridge, df_ridge, complexity = ridge(y, x, lambda_out)
tau_ridge_rel = np.sum(beta_ridge**2, axis=1) / tau_ols
print("\nRelative-tau analysis: tau target, tau achieved / tau_ols, lambda")
print(np.round(np.column_stack([tau, tau_ridge_rel, lambda_out]), 4))
print("\nbeta (fixed-lambda ridge) at each relative-tau lambda:")
print(np.round(beta_ridge, 4))
print("\nbeta (tau-targeted ridge):")
print(np.round(beta_ridge2, 4))
# Finer lambda search grid
lambda_search = np.arange(0, 15, 0.001)
beta_ridge2, lambda_out, df_ridge2, complexity2 = ridge_tau_targeted(
y, x, tau, lambda_search=lambda_search, relative=True
)
beta_ridge, df_ridge, complexity = ridge(y, x, lambda_out)
tau_ridge_rel = np.sum(beta_ridge**2, axis=1) / tau_ols
print("\nRelative-tau analysis, finer lambda grid: tau target, tau achieved / tau_ols, lambda")
print(np.round(np.column_stack([tau, tau_ridge_rel, lambda_out]), 4))
Penalized-form lasso path via coordinate descent — stats/lasso2.py
"""Translated from Examples/stats/lasso2.m -- penalized-form lasso
(regLasso2) at several lambda values, on the same 15-obs dataset."""
import numpy as np
from quanttoolbox.stats.regression.lasso import lasso_ccd
from quanttoolbox.stats.regression.ols import standardize
data = np.array(
[
[3.1, 2.8, 4.3, 0.3, 2.2, 3.5],
[24.9, 5.9, 3.6, 3.2, 0.7, 6.4],
[27.3, 6.0, 9.6, 7.6, 9.5, 0.9],
[25.4, 8.4, 5.4, 1.8, 1.0, 7.1],
[46.1, 5.2, 7.6, 8.3, 0.6, 4.5],
[45.7, 6.0, 7.0, 9.6, 0.6, 0.6],
[47.4, 6.1, 1.0, 8.5, 9.6, 8.6],
[-1.8, 1.2, 9.6, 2.7, 4.8, 5.8],
[20.8, 3.2, 5.0, 4.2, 2.7, 3.6],
[6.8, 0.5, 9.2, 6.9, 9.3, 0.7],
[12.9, 7.9, 9.1, 1.0, 5.9, 5.4],
[37.0, 1.8, 1.3, 9.2, 6.1, 8.3],
[14.7, 7.4, 5.6, 0.9, 5.6, 3.9],
[-3.2, 2.3, 6.6, 0.0, 3.6, 6.4],
[44.3, 7.7, 2.2, 6.5, 1.3, 0.7],
]
)
y = standardize(data[:, 0])
x = standardize(data[:, 1:6])
beta_ols = np.linalg.inv(x.T @ x) @ (x.T @ y)
for lam in [0.0, 0.9, 2.5, 5.5, 7.5]:
beta, _ = lasso_ccd(y, x, lambda_=lam, n_iters=200)
rss = np.mean((y - x @ beta) ** 2)
print(f"lambda={lam}: beta={np.round(beta,4)} rss={round(rss,4)}")
Principal component analysis of a 3-asset correlation matrix — stats/pca1.py
"""Translated from Examples/stats/pca1.m -- PCA on a 3-asset correlation
matrix."""
import numpy as np
from quanttoolbox.linalg.special_matrices import xpnd
from quanttoolbox.stats.regression.ols import pca
C = xpnd(np.array([1.00, 0.80, 1.00, 0.80, 0.80, 1.00]), method=1)
result = pca(C)
print("eigenvalues:", np.round(result.eigenvalues, 4))
print("quality (variance share):", np.round(result.quality, 4))
print("loadings:\n", np.round(result.loadings, 4))
stats.regression.ridge¶
Python alternatives
Hybrid: sklearn.linear_model.Ridge/RidgeCV is better optimized for large/sparse problems. Keep ridge_tau_targeted — its L2-norm-budget (not penalty) parameterization has no sklearn equivalent.
quanttoolbox.stats.regression.ridge
¶
Ridge regression: fixed-lambda and tau-targeted (L2-budget) variants.
Ported from QuantToolBox/stats/{regRidge,regRidge2}.m
Translation notes:
- Ridge regression's closed-form solution is a simple 3-line NumPy
expression, so no external library (not even scikit-learn) is needed
here --
numpy.linalgis sufficient and keeps this dependency-free. ridge_tau_targeted(the originalregRidge2.m) reproduces the original's grid-search approach (scan a dense lambda grid, pick the closest match for the desired L2-norm budget tau) rather than solving for lambda analytically, to preserve exact behavioral parity.
ridge(y, x, lambda_)
¶
Ridge regression at one or more penalty values lambda.
Original: stats/regRidge.m
Returns:
| Name | Type | Description |
|---|---|---|
beta |
(n_lambda, n_vars) array of coefficients (or (n_vars,) if scalar lambda_).
|
|
df |
effective degrees of freedom at each lambda.
|
|
complexity |
1 / df.
|
|
Source code in src/quanttoolbox/stats/regression/ridge.py
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | |
ridge_tau_targeted(y, x, tau, lambda_search=None, relative=False)
¶
Ridge regression targeting a desired sum-of-squared-coefficients budget tau, found by grid search over a candidate lambda range.
Original: stats/regRidge2.m
Returns:
| Name | Type | Description |
|---|---|---|
beta |
(n_tau, n_vars) array of coefficients (or (n_vars,) if scalar tau).
|
|
lambda_out |
the lambda achieving the closest match to each tau.
|
|
df |
effective degrees of freedom at each match.
|
|
complexity |
1 / df.
|
|
Source code in src/quanttoolbox/stats/regression/ridge.py
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | |
Examples¶
Norm-budget (tau-targeted) ridge regression vs. fixed-lambda ridge — stats/ridge2.py
"""Translated from Examples/stats/ridge2.m -- tau-targeted ridge regression
(`ridge_tau_targeted`) cross-checked against fixed-lambda ridge (`ridge`),
in both absolute and OLS-relative tau modes, on the same 15-observation
dataset used throughout stats/."""
import numpy as np
from quanttoolbox.stats.regression.ols import standardize
from quanttoolbox.stats.regression.ridge import ridge, ridge_tau_targeted
data = np.array(
[
[3.1, 2.8, 4.3, 0.3, 2.2, 3.5],
[24.9, 5.9, 3.6, 3.2, 0.7, 6.4],
[27.3, 6.0, 9.6, 7.6, 9.5, 0.9],
[25.4, 8.4, 5.4, 1.8, 1.0, 7.1],
[46.1, 5.2, 7.6, 8.3, 0.6, 4.5],
[45.7, 6.0, 7.0, 9.6, 0.6, 0.6],
[47.4, 6.1, 1.0, 8.5, 9.6, 8.6],
[-1.8, 1.2, 9.6, 2.7, 4.8, 5.8],
[20.8, 3.2, 5.0, 4.2, 2.7, 3.6],
[6.8, 0.5, 9.2, 6.9, 9.3, 0.7],
[12.9, 7.9, 9.1, 1.0, 5.9, 5.4],
[37.0, 1.8, 1.3, 9.2, 6.1, 8.3],
[14.7, 7.4, 5.6, 0.9, 5.6, 3.9],
[-3.2, 2.3, 6.6, 0.0, 3.6, 6.4],
[44.3, 7.7, 2.2, 6.5, 1.3, 0.7],
]
)
y = standardize(data[:, 0])
x = standardize(data[:, 1:6])
beta_ols = np.linalg.inv(x.T @ x) @ (x.T @ y)
tau_ols = np.sum(beta_ols**2)
print("tau(ols) =", round(tau_ols, 4))
# `ridge()` returns (beta, df, complexity) -- it doesn't report the
# resulting L2 budget, so tau_ridge (the quantity `ridge_tau_targeted`
# was aiming for) is recomputed here as sum(beta**2), to cross-check that
# fixed-lambda ridge at `lambda_out` reproduces the same beta/tau
# `ridge_tau_targeted` found by its grid search.
# Absolute tau targets
tau = np.array([0.7, 0.9, 1.1])
beta_ridge2, lambda_out, df_ridge2, complexity2 = ridge_tau_targeted(y, x, tau)
beta_ridge, df_ridge, complexity = ridge(y, x, lambda_out)
tau_ridge = np.sum(beta_ridge**2, axis=1)
print("\nAbsolute-tau analysis: tau target, tau achieved (fixed-lambda ridge), lambda")
print(np.round(np.column_stack([tau, tau_ridge, lambda_out]), 4))
# Relative tau targets (fraction of tau_ols)
tau = np.array([0.25, 0.50, 0.75, 1.00])
beta_ridge2, lambda_out, df_ridge2, complexity2 = ridge_tau_targeted(y, x, tau, relative=True)
beta_ridge, df_ridge, complexity = ridge(y, x, lambda_out)
tau_ridge_rel = np.sum(beta_ridge**2, axis=1) / tau_ols
print("\nRelative-tau analysis: tau target, tau achieved / tau_ols, lambda")
print(np.round(np.column_stack([tau, tau_ridge_rel, lambda_out]), 4))
print("\nbeta (fixed-lambda ridge) at each relative-tau lambda:")
print(np.round(beta_ridge, 4))
print("\nbeta (tau-targeted ridge):")
print(np.round(beta_ridge2, 4))
# Finer lambda search grid
lambda_search = np.arange(0, 15, 0.001)
beta_ridge2, lambda_out, df_ridge2, complexity2 = ridge_tau_targeted(
y, x, tau, lambda_search=lambda_search, relative=True
)
beta_ridge, df_ridge, complexity = ridge(y, x, lambda_out)
tau_ridge_rel = np.sum(beta_ridge**2, axis=1) / tau_ols
print("\nRelative-tau analysis, finer lambda grid: tau target, tau achieved / tau_ols, lambda")
print(np.round(np.column_stack([tau, tau_ridge_rel, lambda_out]), 4))
stats.regression.lasso¶
Python alternatives
Switch the penalized-form solvers (lasso_ccd, lasso_admm) to sklearn.linear_model.Lasso/ElasticNet — Cython-compiled, extensively battle-tested. Keep lasso_tau_constrained — sklearn has no L1-budget interface.
quanttoolbox.stats.regression.lasso
¶
Lasso and elastic-net regression via coordinate descent / ADMM.
Ported from QuantToolBox/stats/{regLassoCCD,regLassoADMM,regLassoADMM2, regLasso,regElasticNet,selectLasso,regLasso2}.m
Translation notes:
- The original MATLAB toolbox has two Lasso parameterizations that are
easy to conflate:
- Penalized form (
regLassoCCD.m,regLassoADMM.m): minimize ||y - X*beta||^2 + lambda * ||beta||_1. This is what coordinate descent and ADMM solve directly, and whatscikit-learn's ownLasso(also coordinate-descent-based) solves too. - Constrained/budget form (
regLasso.m,selectLasso.m, viaquadprog): minimize ||y - X*beta||^2 subject to ||beta||_1 <= tau. These are Lagrangian duals of each other but need different solvers. This module ports the penalized form directly (lasso_ccd,lasso_admm) using plain NumPy coordinate descent -- the same algorithm scikit-learn uses internally, so no extra dependency is needed.lasso_tau_constrainedrecovers the budget-form behavior ofregLasso.m/selectLasso.mby bisecting on lambda until the resulting L1 norm matches the target tau, rather than porting the original'squadprog-based QP formulation (MATLAB Optimization Toolbox has no free-standing Python equivalent worth adding as a dependency just for this).
- Penalized form (
regElasticNet.m's alpha/lambda parameterization (lambda*(1-alpha)*L2 + lambda*alpha*L1) is preserved inelastic_net_ccdfor call-site compatibility, rather than switching to scikit-learn'sl1_rationaming.- MATLAB's
global ADMM_*settings blocks are replaced by thequanttoolbox.config.ADMMConfigdataclass. regLasso2.mbuilds a regularization path by looping its solver over a vector of lambdas, returning[beta, tau, df, complexity]. It usesquadprogon a doubled positive/negative-part formulation of beta (likeregLasso.m/selectLasso.m); this is ported aslasso_regularization_path, which loopslasso_ccdinstead -- the same quadprog-avoiding substitutiondocs/examples/stats/lasso2.pyalready makes forExamples/stats/lasso2.m's single-lambdaregLasso2calls.
elastic_net_ccd(y, x, lambda_, alpha, beta_init=None, n_iters=100)
¶
Elastic net via cyclical coordinate descent: minimize ||y - Xbeta||^2 + lambda(1-alpha)||beta||_2^2 + lambdaalpha*||beta||_1.
Original: stats/regElasticNet.m (ported as CCD rather than the original's quadprog QP formulation -- see module docstring.)
Source code in src/quanttoolbox/stats/regression/lasso.py
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 | |
lasso_admm(y, x, lambda_, beta_init=None, config=None)
¶
Lasso via ADMM (penalized form) with a fixed step-size (varphi).
Note: the original MATLAB also supported an adaptive-varphi mode
(ADMM_varphi_mtd == 2, residual-balancing per Boyd et al. 2011);
that branch is not ported here -- only the fixed-varphi path, which
is what the default config uses anyway. Add adaptive step-size
updates here if convergence speed on your problem needs it.
Original: stats/regLassoADMM.m
Returns:
| Name | Type | Description |
|---|---|---|
beta |
final coefficient estimate.
|
|
beta_path |
(n_iters_run, n_vars) iterate history.
|
|
converged |
whether the convergence tolerance was reached.
|
|
n_iters_run |
number of iterations actually performed.
|
|
Source code in src/quanttoolbox/stats/regression/lasso.py
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 | |
lasso_ccd(y, x, lambda_, beta_init=None, n_iters=100)
¶
Lasso via cyclical coordinate descent (penalized form).
Original: stats/regLassoCCD.m
Source code in src/quanttoolbox/stats/regression/lasso.py
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | |
lasso_regularization_path(y, x, lambda_grid, n_iters=1000)
¶
Build the full lasso regularization path by looping lasso_ccd
(penalized-form coordinate descent) over a grid of lambda values.
At each lambda, records the fitted coefficients, their L1 norm, the
number of nonzero coefficients (degrees of freedom), and the resulting
model complexity (1 / df).
Original: stats/regLasso2.m -- regLasso2.m solves the same
[beta, tau, df, complexity] = f(y, x, lambda) grid problem via
quadprog on a doubled positive/negative-part formulation of beta;
this is translated using lasso_ccd's coordinate descent instead,
the same substitution docs/examples/stats/lasso2.py already makes
for Examples/stats/lasso2.m's single-lambda regLasso2 calls
(see module docstring). Promoted from HSF-Notebooks chapter 15b
(reg_lasso2), which uses this exact grid-loop as a documented
example of driving quanttoolbox's own lasso_ccd solver across a
lambda grid, rather than an external one (e.g. scikit-learn's
lasso_path).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y
|
response vector, shape (n,).
|
|
required |
x
|
design matrix, shape (n, p).
|
|
required |
lambda_grid
|
penalty values to solve at, shape (n_lambda,).
|
|
required |
n_iters
|
coordinate-descent iterations passed through to
|
|
1000
|
Returns:
| Type | Description |
|---|---|
LassoPathResult with, for each lambda in ``lambda_grid``:
|
|
Source code in src/quanttoolbox/stats/regression/lasso.py
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | |
lasso_tau_constrained(y, x, tau, lambda_search=None, n_iters=200)
¶
Lasso in budget-constrained form: minimize ||y - X*beta||^2 subject to ||beta||_1 <= tau, found via bisection on the penalized-form lambda.
Reproduces the tau-based interface of stats/regLasso.m and stats/selectLasso.m without needing a QP solver.
Returns:
| Name | Type | Description |
|---|---|---|
beta |
coefficients whose L1 norm is closest to tau.
|
|
lambda_matched |
the lambda value that achieved it.
|
|
df |
number of non-zero coefficients.
|
|
Source code in src/quanttoolbox/stats/regression/lasso.py
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 | |
soft_threshold(v, threshold)
¶
Elementwise soft-thresholding operator: sign(v) * max(|v| - threshold, 0).
Original: optim/soft_thresholding.m (used by regLassoADMM.m)
Source code in src/quanttoolbox/stats/regression/lasso.py
53 54 55 56 57 58 | |
Examples¶
Elastic-net regression path — stats/elasticnet1.py
"""Translated from Examples/stats/elasticnet1.m and elasticnet2.m --
elastic net regression path at two different alpha (L1/L2 mix) values,
on the same 15-observation dataset used in ridge1.m/lasso1.m."""
import numpy as np
from quanttoolbox.stats.regression.lasso import elastic_net_ccd
from quanttoolbox.stats.regression.ols import standardize
data = np.array(
[
[3.1, 2.8, 4.3, 0.3, 2.2, 3.5],
[24.9, 5.9, 3.6, 3.2, 0.7, 6.4],
[27.3, 6.0, 9.6, 7.6, 9.5, 0.9],
[25.4, 8.4, 5.4, 1.8, 1.0, 7.1],
[46.1, 5.2, 7.6, 8.3, 0.6, 4.5],
[45.7, 6.0, 7.0, 9.6, 0.6, 0.6],
[47.4, 6.1, 1.0, 8.5, 9.6, 8.6],
[-1.8, 1.2, 9.6, 2.7, 4.8, 5.8],
[20.8, 3.2, 5.0, 4.2, 2.7, 3.6],
[6.8, 0.5, 9.2, 6.9, 9.3, 0.7],
[12.9, 7.9, 9.1, 1.0, 5.9, 5.4],
[37.0, 1.8, 1.3, 9.2, 6.1, 8.3],
[14.7, 7.4, 5.6, 0.9, 5.6, 3.9],
[-3.2, 2.3, 6.6, 0.0, 3.6, 6.4],
[44.3, 7.7, 2.2, 6.5, 1.3, 0.7],
]
)
y = standardize(data[:, 0])
x = standardize(data[:, 1:6])
for alpha, lam in [(0.50, 1.0), (0.25, 1.0)]:
beta, path = elastic_net_ccd(y, x, lambda_=lam, alpha=alpha, n_iters=200)
print(f"alpha={alpha}, lambda={lam}: beta={np.round(beta,4)}")
Lasso path with R-squared, degrees of freedom, and complexity — stats/lasso1.py
"""Translated from Examples/stats/lasso1.m -- penalized-form lasso path at
5 lambda values (numeric core only; the original's 501-point tau-sweep and
its multi-line coefficient-path plot are dropped -- the same lambda
values and dataset are already used by lasso2.py, which this extends with
R^2/degrees-of-freedom/complexity reporting to match lasso1.m's own
output table)."""
import numpy as np
from quanttoolbox.stats.regression.lasso import lasso_ccd
from quanttoolbox.stats.regression.ols import standardize
data = np.array(
[
[3.1, 2.8, 4.3, 0.3, 2.2, 3.5],
[24.9, 5.9, 3.6, 3.2, 0.7, 6.4],
[27.3, 6.0, 9.6, 7.6, 9.5, 0.9],
[25.4, 8.4, 5.4, 1.8, 1.0, 7.1],
[46.1, 5.2, 7.6, 8.3, 0.6, 4.5],
[45.7, 6.0, 7.0, 9.6, 0.6, 0.6],
[47.4, 6.1, 1.0, 8.5, 9.6, 8.6],
[-1.8, 1.2, 9.6, 2.7, 4.8, 5.8],
[20.8, 3.2, 5.0, 4.2, 2.7, 3.6],
[6.8, 0.5, 9.2, 6.9, 9.3, 0.7],
[12.9, 7.9, 9.1, 1.0, 5.9, 5.4],
[37.0, 1.8, 1.3, 9.2, 6.1, 8.3],
[14.7, 7.4, 5.6, 0.9, 5.6, 3.9],
[-3.2, 2.3, 6.6, 0.0, 3.6, 6.4],
[44.3, 7.7, 2.2, 6.5, 1.3, 0.7],
]
)
y = standardize(data[:, 0])
x = standardize(data[:, 1:6])
beta_ols = np.linalg.inv(x.T @ x) @ (x.T @ y)
tss = np.mean(y**2)
print("lambda |beta| tau=sum|beta| rss R2 df complexity")
for lam in [0.0, 0.9, 2.5, 5.5, 7.5]:
beta, _ = lasso_ccd(y, x, lambda_=lam, n_iters=200)
u = y - x @ beta
rss = np.mean(u**2)
r2 = 1 - rss / tss
tau = np.sum(np.abs(beta))
df = int(np.sum(np.abs(beta) >= 1e-6))
complexity = 1.0 / df if df > 0 else np.inf
print(
f"{lam:6.2f} {np.round(beta, 4)} tau={tau:.4f} rss={rss:.4f} "
f"R2={r2:.4f} df={df} complexity={complexity:.4f}"
)
Penalized-form lasso path via coordinate descent — stats/lasso2.py
"""Translated from Examples/stats/lasso2.m -- penalized-form lasso
(regLasso2) at several lambda values, on the same 15-obs dataset."""
import numpy as np
from quanttoolbox.stats.regression.lasso import lasso_ccd
from quanttoolbox.stats.regression.ols import standardize
data = np.array(
[
[3.1, 2.8, 4.3, 0.3, 2.2, 3.5],
[24.9, 5.9, 3.6, 3.2, 0.7, 6.4],
[27.3, 6.0, 9.6, 7.6, 9.5, 0.9],
[25.4, 8.4, 5.4, 1.8, 1.0, 7.1],
[46.1, 5.2, 7.6, 8.3, 0.6, 4.5],
[45.7, 6.0, 7.0, 9.6, 0.6, 0.6],
[47.4, 6.1, 1.0, 8.5, 9.6, 8.6],
[-1.8, 1.2, 9.6, 2.7, 4.8, 5.8],
[20.8, 3.2, 5.0, 4.2, 2.7, 3.6],
[6.8, 0.5, 9.2, 6.9, 9.3, 0.7],
[12.9, 7.9, 9.1, 1.0, 5.9, 5.4],
[37.0, 1.8, 1.3, 9.2, 6.1, 8.3],
[14.7, 7.4, 5.6, 0.9, 5.6, 3.9],
[-3.2, 2.3, 6.6, 0.0, 3.6, 6.4],
[44.3, 7.7, 2.2, 6.5, 1.3, 0.7],
]
)
y = standardize(data[:, 0])
x = standardize(data[:, 1:6])
beta_ols = np.linalg.inv(x.T @ x) @ (x.T @ y)
for lam in [0.0, 0.9, 2.5, 5.5, 7.5]:
beta, _ = lasso_ccd(y, x, lambda_=lam, n_iters=200)
rss = np.mean((y - x @ beta) ** 2)
print(f"lambda={lam}: beta={np.round(beta,4)} rss={round(rss,4)}")
stats.regression.kernel¶
Python alternatives
Hybrid: statsmodels.nonparametric.KernelReg supports automatic bandwidth selection via cross-validation and both local-constant/local-linear estimators. Switch for general use; keep ours where the exact original bandwidth formula must match existing MATLAB-based results.
quanttoolbox.stats.regression.kernel
¶
Nonparametric kernel density estimation and local-polynomial kernel regression.
Ported from QuantToolBox/stats/{regKernelDensity,regKernelMean, regKernelQuantile,regKernelPayoff}.m
Translation notes:
regKernelDensityis a manual Gaussian-kernel density/CDF estimator usingscipy.stats.normas the kernel -- this is equivalent toscipy.stats.gaussian_kdebut the original's Silverman-type bandwidth rule (1.364 * std * n^-0.2) differs slightly from scipy's default bandwidth factor, so it's reproduced explicitly here rather than delegating togaussian_kde(which would silently change bandwidths).regKernelMean/regKernelPayoffare local polynomial regression (Nadaraya-Watson generalized to include local polynomial terms), solved via a small per-evaluation-point weighted least squares -- there's no single scipy/sklearn function that matches this directly, so it's ported as a direct weighted-least-squares loop.regKernelQuantiledepends onquantile_regression(scipy.optimize.linprog-based); seequanttoolbox.stats.regression.quantile.
kernel_density(data, x, h=None)
¶
Gaussian-kernel density (and CDF) estimate of each column of data,
evaluated at points x.
Original: stats/regKernelDensity.m
Returns:
| Name | Type | Description |
|---|---|---|
density |
(n_x, n_cols) estimated density values.
|
|
cdf |
(n_x, n_cols) estimated CDF values.
|
|
bandwidths |
(n_cols,) bandwidth used per column (auto-computed if h is None).
|
|
Source code in src/quanttoolbox/stats/regression/kernel.py
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | |
kernel_mean_regression(y, x, z, order=1, h=None)
¶
Local polynomial kernel regression of y on x, evaluated at points z.
order=1 is standard local-linear (Nadaraya-Watson-generalized) regression; higher orders fit a local polynomial of that degree.
Original: stats/regKernelMean.m
Source code in src/quanttoolbox/stats/regression/kernel.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | |
kernel_payoff_regression(x, y, z=None, n_points=100, order=1, h=None)
¶
Convenience wrapper around kernel_mean_regression: builds an
evaluation grid z automatically (from data range, or from a
(min, max) pair) if one isn't supplied directly.
Original: stats/regKernelPayoff.m
Source code in src/quanttoolbox/stats/regression/kernel.py
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | |
kernel_quantile_regression(y, x, tau, z, order=1, h=1.0)
¶
Local polynomial kernel quantile regression of y on x at quantile tau, evaluated at points z.
Original: stats/regKernelQuantile.m
Source code in src/quanttoolbox/stats/regression/kernel.py
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | |
Examples¶
Gaussian kernel density of two correlated series — stats/kernel1.py
"""Translated from Examples/stats/kernel1.m -- Gaussian-kernel density
estimate of two correlated series (numeric core only; the original's plot
of both density curves is dropped).
The original draws x from MATLAB's unseeded `randn`; a fixed seed
(`np.random.default_rng(0)`) is substituted here."""
import numpy as np
from quanttoolbox.stats.regression.kernel import kernel_density
rng = np.random.default_rng(0)
x1 = rng.standard_normal(100)
x2 = 0.8 * x1 + 1.0
x = np.column_stack([x1, x2])
z = np.linspace(-5, 5, 101)
density, cdf, bandwidths = kernel_density(x, z)
print("Bandwidths (auto-computed, Silverman-type rule):", np.round(bandwidths, 4))
print("\nDensity at z in {-5,-2.5,0,2.5,5} (columns: series 1, series 2):")
idx = [0, 25, 50, 75, 100]
print(np.round(np.column_stack([z[idx], density[idx]]), 4))
Kernel mean and quantile regression vs. true population curves — stats/qreg2.py
"""Translated from Examples/stats/qreg2.m -- local-polynomial kernel mean
and quantile regression (orders 1 and 2) of a nonlinear y=f(x)+noise
relationship, compared against the known population mean/quantile curves
(numeric core only; the original's scatter+curve plot is dropped).
The original explicitly seeds MATLAB's RNG (`rng(123)`); NumPy's generator
is seeded the same way for a comparable (not bit-identical) run."""
import numpy as np
from quanttoolbox.stats.regression.kernel import kernel_mean_regression, kernel_quantile_regression
rng = np.random.default_rng(123)
n = 500
x = rng.random(n)
y = rng.random(n) * (np.cos(2 * np.pi * x - np.pi) + 1)
tau = 0.95
p = 50
z = np.arange(0, 1 + 1e-9, 1 / (p - 1))[:p]
m0 = 0.5 * (np.cos(2 * np.pi * z - np.pi) + 1)
m1 = kernel_mean_regression(y, x, z, order=1)
m2 = kernel_mean_regression(y, x, z, order=2)
q0 = tau * (np.cos(2 * np.pi * z - np.pi) + 1)
q1 = kernel_quantile_regression(y, x, tau, z, order=1)
q2 = kernel_quantile_regression(y, x, tau, z, order=2)
print("z, population mean m0, local-linear m1, local-quadratic m2 (every 10th point):")
print(np.round(np.column_stack([z, m0, m1, m2])[::10], 4))
print("\nz, population q(0.95) q0, local-linear q1, local-quadratic q2 (every 10th point):")
print(np.round(np.column_stack([z, q0, q1, q2])[::10], 4))
stats.regression.quantile¶
Python alternatives
Switch to statsmodels.regression.quantile_regression.QuantReg or sklearn.linear_model.QuantileRegressor — both more battle-tested than this module's scipy.optimize.linprog-based implementation. Keep ours only if the LP slack-variable (u, v) outputs are needed downstream.
quanttoolbox.stats.regression.quantile
¶
Quantile regression (via linear programming) and quantile-regression copulas.
Ported from QuantToolBox/stats/{quantile_regression,qrCopulaNormal, qrCopulaStudent}.m
Translation notes:
- MATLAB's
linprog(Optimization Toolbox, interior-point) maps directly ontoscipy.optimize.linprog(also supports an interior-point method) -- both are standard-library-adjacent linear programming solvers, so no extra dependency is needed. - The original solves each quantile tau independently in a loop; this is preserved here (rather than vectorizing across tau) since each is an independent LP.
qr_copula_normal(u1, rho, alpha)
¶
Conditional quantile u2 = Q(alpha | u1) implied by a bivariate Gaussian copula with correlation rho.
Original: stats/qrCopulaNormal.m
Source code in src/quanttoolbox/stats/regression/quantile.py
71 72 73 74 75 76 77 78 79 | |
qr_copula_student(u1, rho, nu, alpha)
¶
Conditional quantile u2 = Q(alpha | u1) implied by a bivariate Student-t copula with correlation rho and nu degrees of freedom.
Original: stats/qrCopulaStudent.m
Source code in src/quanttoolbox/stats/regression/quantile.py
82 83 84 85 86 87 88 89 90 91 92 | |
quantile_regression(y, x, tau, weights=None)
¶
Linear quantile regression at one or more quantile levels tau, solved via linear programming (Koenker & Bassett's LP formulation).
Original: stats/quantile_regression.m
Returns:
| Name | Type | Description |
|---|---|---|
beta |
(n_vars, n_tau) coefficients (or (n_vars,) if scalar tau).
|
|
u |
(n_obs, n_tau) positive residual parts.
|
|
v |
(n_obs, n_tau) negative residual parts.
|
|
Source code in src/quanttoolbox/stats/regression/quantile.py
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | |
Examples¶
Linear quantile regression at nine quantile levels — stats/qreg1.py
"""Translated from Examples/stats/qreg1.m -- linear quantile regression at
9 quantile levels (tau=0.1..0.9) on a simulated 3-predictor dataset.
The original draws x/b/u from MATLAB's unseeded `rand`/`randn`; a fixed
seed (`np.random.default_rng(0)`) is substituted here."""
import numpy as np
from quanttoolbox.stats.regression.quantile import quantile_regression
rng = np.random.default_rng(0)
n, m = 100, 3
tau = np.arange(0.1, 0.91, 0.1)
x = 10 * rng.random((n, m))
b = 10 * rng.standard_normal(m)
sigma = 5.20
u = sigma * rng.standard_normal(n)
y = x @ b + u
beta, u_pos, v_neg = quantile_regression(y, x, tau)
res = y[:, None] - x @ beta
print("tau:", np.round(tau, 2))
print("beta (rows=predictors, cols=tau):")
print(np.round(beta, 4))
print("\nresidual std by tau:", np.round(np.std(res, axis=0), 4))
Monte Carlo OLS vs. LAD regression under heteroskedastic noise — ects/quantile2.py
"""Translated from Examples/ects/quantile2.m -- Monte Carlo comparison of
OLS vs. LAD (median quantile regression) under heteroskedastic noise:
mean and standard error of each estimator's slope coefficient across many
simulated datasets (numeric core only; the original's kernel-density plot
of the two estimators' sampling distributions is dropped).
The original draws x/sigma/u from MATLAB's unseeded `rand`/`randn`; a
fixed seed (`np.random.default_rng(0)`) is substituted here."""
import numpy as np
from quanttoolbox.stats.regression.quantile import quantile_regression
rng = np.random.default_rng(0)
n_s = 2000
n_t = 200
b0, b1 = 0.1, 0.2
alpha = 0.50
beta_ols = np.zeros(n_s)
beta_lad = np.zeros(n_s)
for it in range(n_s):
x = rng.standard_normal(n_t)
sigma = 0.20 + 0.60 * rng.random(n_t)
sigma = sigma**2.5
u = sigma * rng.standard_normal(n_t)
y = b0 + b1 * x + u
design = np.column_stack([np.ones(n_t), x])
beta = np.linalg.inv(design.T @ design) @ (design.T @ y)
beta_ols[it] = beta[1]
beta_q, _, _ = quantile_regression(y, design, alpha)
beta_lad[it] = beta_q[1]
print("parameters estimate stderr")
print(f"OLS {np.mean(beta_ols):.4f} {np.std(beta_ols, ddof=1):.4f}")
print(f"LAD {np.mean(beta_lad):.4f} {np.std(beta_lad, ddof=1):.4f}")
OLS, LAD, quantile, and SVM regression compared — svm/svm6.py
"""Translated from Examples/svm/svm6.m -- comparison of OLS, LAD, and
SVM-regression (LS-SVM and epsilon-SVM, at two different C values)
estimates on the same 10-obs dataset used in ols1.m/robust1.m."""
import numpy as np
from quanttoolbox.econometrics.estimation import ols_estimation
from quanttoolbox.stats.regression.quantile import quantile_regression
from quanttoolbox.stats.regression.robust import lad_regression
from quanttoolbox.svm.svm import svm_regression_primal
data = np.array(
[
[1.5, 1.0, 2.4, 3.6, 0.3],
[20.4, 1.0, 1.1, 3.8, 5.9],
[17.1, 1.0, 5.1, 6.3, 6.1],
[30.9, 1.0, 2.7, 2.4, 9.5],
[22.2, 1.0, 3.3, 3.0, 7.4],
[9.1, 1.0, 1.0, 5.4, 4.9],
[39.2, 1.0, 9.6, 2.8, 8.1],
[3.1, 1.0, 2.9, 4.4, 1.0],
[7.2, 1.0, 4.2, 5.6, 1.7],
[27.6, 1.0, 8.1, 1.7, 5.4],
]
)
y = data[:, 0]
x_full = data[:, 1:5]
beta_ols = ols_estimation(y, x_full).beta
beta_lad = lad_regression(y, x_full).beta
beta_lad2, _, _ = quantile_regression(y, x_full, tau=0.5)
x = x_full[:, 1:4] # drop the intercept and 4th column for SVM (3 predictors)
r_ls1 = svm_regression_primal(y, x, c=1)
r_eps1 = svm_regression_primal(y, x, c=1, epsilon=1)
r_ls2 = svm_regression_primal(y, x, c=100000)
r_eps2 = svm_regression_primal(y, x, c=100000, epsilon=0)
print("OLS:", np.round(beta_ols, 3))
print("LAD:", np.round(beta_lad, 3))
print("Quantile(0.5):", np.round(beta_lad2, 3))
print("SVM LS (C=1):", round(r_ls1.beta0, 3), np.round(r_ls1.beta, 3))
print("SVM eps (C=1, eps=1):", round(r_eps1.beta0, 3), np.round(r_eps1.beta, 3))
print("SVM LS (C=1e5):", round(r_ls2.beta0, 3), np.round(r_ls2.beta, 3))
print("SVM eps (C=1e5, eps=0):", round(r_eps2.beta0, 3), np.round(r_eps2.beta, 3))
OLS, median, LAD, and Huber regression compared — ects/robust2.py
"""Translated from Examples/ects/robust2.m -- OLS vs. median (quantile,
alpha=0.5) regression, LAD regression, and Huber regression, on a
simulated 3-predictor dataset.
The original draws x/u from MATLAB's unseeded `rand`/`randn`; a fixed
seed (`np.random.default_rng(0)`) is substituted here."""
import numpy as np
from quanttoolbox.econometrics.estimation import ols_estimation
from quanttoolbox.stats.regression.quantile import quantile_regression
from quanttoolbox.stats.regression.robust import huber_regression, lad_regression
rng = np.random.default_rng(0)
n, k = 100, 3
beta_true = np.arange(1, k + 1, dtype=float)
sigma = 0.20
x = rng.random((n, k))
y = x @ beta_true + sigma * rng.standard_normal(n)
r_ols = ols_estimation(y, x)
u_ols = r_ols.residuals
print("OLS beta:", np.round(r_ols.beta, 4))
alpha = 0.50
beta_med, _, _ = quantile_regression(y, x, alpha)
print("\nMedian regression beta:", np.round(beta_med, 4))
r_lad = lad_regression(y, x)
print("\nLAD regression beta:", np.round(r_lad.beta, 4), "converged:", r_lad.converged)
c = np.quantile(np.abs(u_ols), 0.90)
r_huber = huber_regression(y, x, c=c)
print(
f"\nHuber regression (c={c:.4f}) beta:",
np.round(r_huber.beta, 4),
"converged:",
r_huber.converged,
)
OLS/median/LAD plus quantile regression via IRLS and exact LP — ects/robust3.py
"""Translated from Examples/ects/robust3.m -- OLS vs. median regression,
LAD regression, and quantile regression at alpha=0.90, both via the IRLS
M-estimator (`quantile_m_regression`) and the exact LP formulation
(`quantile_regression`), on a simulated 3-predictor dataset.
The original draws x/u from MATLAB's unseeded `rand`/`randn`; a fixed
seed (`np.random.default_rng(0)`) is substituted here."""
import numpy as np
from quanttoolbox.econometrics.estimation import ols_estimation
from quanttoolbox.stats.regression.quantile import quantile_regression
from quanttoolbox.stats.regression.robust import lad_regression, quantile_m_regression
rng = np.random.default_rng(0)
n, k = 100, 3
beta_true = np.arange(1, k + 1, dtype=float)
sigma = 0.20
x = rng.random((n, k))
y = x @ beta_true + sigma * rng.standard_normal(n)
r_ols = ols_estimation(y, x)
u_ols = r_ols.residuals
print("OLS beta:", np.round(r_ols.beta, 4))
alpha = 0.50
beta_med, _, _ = quantile_regression(y, x, alpha)
print("\nMedian regression beta:", np.round(beta_med, 4))
r_lad = lad_regression(y, x)
print("\nLAD regression beta:", np.round(r_lad.beta, 4), "converged:", r_lad.converged)
alpha = 0.90
r_qm = quantile_m_regression(y, x, alpha)
print(f"\nQuantile (alpha={alpha}) regression via IRLS M-estimator, beta:", np.round(r_qm.beta, 4))
beta_q, _, _ = quantile_regression(y, x, alpha)
print(f"Quantile (alpha={alpha}) regression via exact LP, beta: ", np.round(beta_q, 4))
OLS/SVM-LS and quantile/SVM-epsilon regression on synthetic data — svm/svm8.py
"""Translated from Examples/svm/svm8.m -- OLS, LAD, quantile, and SVM
regression (primal, at two very different C values, LS- and
epsilon-insensitive) compared on a larger (n=1000) simulated dataset.
The original draws x/beta/u from MATLAB's unseeded `rand`/`randn`; a fixed
seed (`np.random.default_rng(0)`) is substituted here for reproducibility
-- this is the fixed-seed substitution the tracker notes svm8.m as still
needing."""
import numpy as np
from quanttoolbox.econometrics.estimation import ols_estimation
from quanttoolbox.stats.regression.quantile import quantile_regression
from quanttoolbox.stats.regression.robust import lad_regression
from quanttoolbox.svm.svm import svm_regression_primal
rng = np.random.default_rng(0)
n, k = 1000, 4
x = rng.random((n, k))
beta_true = 5 * rng.random(k)
u = 0.20 * rng.standard_normal(n)
beta0_true = -3.0
y = beta0_true + x @ beta_true + u
x_design = np.column_stack([np.ones(n), x])
beta_ols = ols_estimation(y, x_design).beta
beta_lad = lad_regression(y, x_design).beta
beta_lad2, _, _ = quantile_regression(y, x_design, tau=0.5)
# SVM regression (primal), C=1
r_ls1 = svm_regression_primal(y, x, c=1) # LS-SVM
beta_svm_ls = np.concatenate([[r_ls1.beta0], r_ls1.beta])
r_eps1 = svm_regression_primal(y, x, c=1, epsilon=1) # epsilon-SVM
beta_svm_epsilon = np.concatenate([[r_eps1.beta0], r_eps1.beta])
# SVM regression (primal), C=1000 (effectively unregularized)
r_ls2 = svm_regression_primal(y, x, c=1000) # LS-SVM
beta_svm_ls2 = np.concatenate([[r_ls2.beta0], r_ls2.beta])
r_eps2 = svm_regression_primal(y, x, c=1000, epsilon=0) # epsilon-SVM, epsilon=0
beta_svm_epsilon2 = np.concatenate([[r_eps2.beta0], r_eps2.beta])
results = np.column_stack(
[beta_ols, beta_lad, beta_lad2, beta_svm_ls, beta_svm_epsilon, beta_svm_ls2, beta_svm_epsilon2]
)
print("Comparison of OLS, LAD, quantile(0.5), and SVM estimates")
print(
"columns: OLS, LAD, Quantile(0.5), SVM-LS(C=1), SVM-eps(C=1,eps=1), SVM-LS(C=1000), SVM-eps(C=1000,eps=0)"
)
print(np.round(results, 3))
print("\nOLS vs. SVM-LS at large C (expect near-identical -- unregularized limit):")
print(np.round(np.column_stack([beta_ols, beta_svm_ls2]), 3))
print(
"\nQuantile(0.5) vs. SVM-eps at large C, epsilon=0 (expect near-identical -- both approach LAD):"
)
print(np.round(np.column_stack([beta_lad2, beta_svm_epsilon2]), 3))
stats.regression.robust¶
Python alternatives
Hybrid: statsmodels.robust.robust_linear_model.RLM covers Huber, Tukey biweight, Andrew's wave, Hampel, and trimmed-mean M-estimators via IRLS — a superset of this module's Huber implementation. It does not cover LAD or quantile M-estimation directly (though QuantReg(q=0.5) is exactly LAD). Use RLM for general M-estimation; keep lad_regression/quantile_m_regression/inverse_quantile_m_regression for those specific losses.
quanttoolbox.stats.regression.robust
¶
Robust M-estimator regression via Iteratively Reweighted Least Squares (IRLS).
Ported from QuantToolBox/ects/{robust_regression,robust_huber_regression, robust_lad_regression,robust_quantile_regression, robust_inverse_quantile_regression}.m
Translation notes:
- All four specific estimators (Huber, LAD, quantile, inverse-quantile)
are thin wrappers supplying a (rho, rho_prime) loss-function pair to one
shared IRLS core (
robust_regression), exactly mirroring the original's structure. quantile_m_regression/inverse_quantile_m_regressionhere solve the quantile-loss problem via IRLS (a smooth M-estimation approximation), which is a different algorithm fromquanttoolbox.stats.regression.quantile.quantile_regression(which solves the exact Koenker-Bassett LP formulation). IRLS is faster and matches the original MATLAB toolbox's approach here, but the LP version is the numerically exact one if precision matters more than speed.- MATLAB's
global ROBUST_epsconvergence tolerance andglobal Print_Resultsare replaced by theRobustRegressionConfigdataclass (the original never actually set a value forROBUST_epsin the files reviewed, so a conventional 1e-6 default is used here -- passRobustRegressionConfig(eps=...)to match a specific original run). - The original's
rho_primenumerical-gradient fallback (numerical_gradient(rho, beta)when onlyrhois supplied) is reproduced with a simple central-difference approximation. - MATLAB's
cdftc/cdffc(upper-tail Student-t / F complementary CDFs) map directly toscipy.stats.t.sf/scipy.stats.f.sf.
RobustRegressionConfig(eps=1e-06, max_iters=500)
dataclass
¶
Replaces the ROBUST_eps global. See module docstring for the default's provenance.
huber_regression(y, x, c=1.345, config=None)
¶
Huber M-estimator regression: quadratic loss for |residual| < c, linear loss beyond. c=1.345 is the conventional choice giving ~95% efficiency under normality.
Original: ects/robust_huber_regression.m
Source code in src/quanttoolbox/stats/regression/robust.py
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 | |
inverse_quantile_m_regression(y, x, alpha, config=None)
¶
"Inverse" quantile regression (loss weighted by which side of zero the residual falls on, complementary to quantile_m_regression).
Original: ects/robust_inverse_quantile_regression.m
Source code in src/quanttoolbox/stats/regression/robust.py
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 | |
lad_regression(y, x, config=None)
¶
Least Absolute Deviations (median) regression.
Note: LAD's weight function 1/(u+eps) is not smooth near u=0, so the
IRLS loop typically doesn't settle below a tight eps tolerance --
it will often run to max_iters even after beta has already
stabilized at the optimum. Check that the coefficients look stable
across the last few iterations rather than relying solely on
result.converged for this estimator.
Original: ects/robust_lad_regression.m
Source code in src/quanttoolbox/stats/regression/robust.py
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 | |
quantile_m_regression(y, x, alpha, config=None)
¶
Quantile regression at level alpha via IRLS M-estimation (see module
docstring for how this differs from the exact LP-based
quantile.quantile_regression).
Original: ects/robust_quantile_regression.m
Source code in src/quanttoolbox/stats/regression/robust.py
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | |
robust_regression(y, x, rho, rho_prime=None, config=None)
¶
Generic M-estimation regression via IRLS, for an arbitrary loss function rho (with derivative rho_prime, numerically approximated if not supplied).
Original: ects/robust_regression.m
Source code in src/quanttoolbox/stats/regression/robust.py
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | |
Examples¶
OLS, LAD, quantile, and SVM regression compared — svm/svm6.py
"""Translated from Examples/svm/svm6.m -- comparison of OLS, LAD, and
SVM-regression (LS-SVM and epsilon-SVM, at two different C values)
estimates on the same 10-obs dataset used in ols1.m/robust1.m."""
import numpy as np
from quanttoolbox.econometrics.estimation import ols_estimation
from quanttoolbox.stats.regression.quantile import quantile_regression
from quanttoolbox.stats.regression.robust import lad_regression
from quanttoolbox.svm.svm import svm_regression_primal
data = np.array(
[
[1.5, 1.0, 2.4, 3.6, 0.3],
[20.4, 1.0, 1.1, 3.8, 5.9],
[17.1, 1.0, 5.1, 6.3, 6.1],
[30.9, 1.0, 2.7, 2.4, 9.5],
[22.2, 1.0, 3.3, 3.0, 7.4],
[9.1, 1.0, 1.0, 5.4, 4.9],
[39.2, 1.0, 9.6, 2.8, 8.1],
[3.1, 1.0, 2.9, 4.4, 1.0],
[7.2, 1.0, 4.2, 5.6, 1.7],
[27.6, 1.0, 8.1, 1.7, 5.4],
]
)
y = data[:, 0]
x_full = data[:, 1:5]
beta_ols = ols_estimation(y, x_full).beta
beta_lad = lad_regression(y, x_full).beta
beta_lad2, _, _ = quantile_regression(y, x_full, tau=0.5)
x = x_full[:, 1:4] # drop the intercept and 4th column for SVM (3 predictors)
r_ls1 = svm_regression_primal(y, x, c=1)
r_eps1 = svm_regression_primal(y, x, c=1, epsilon=1)
r_ls2 = svm_regression_primal(y, x, c=100000)
r_eps2 = svm_regression_primal(y, x, c=100000, epsilon=0)
print("OLS:", np.round(beta_ols, 3))
print("LAD:", np.round(beta_lad, 3))
print("Quantile(0.5):", np.round(beta_lad2, 3))
print("SVM LS (C=1):", round(r_ls1.beta0, 3), np.round(r_ls1.beta, 3))
print("SVM eps (C=1, eps=1):", round(r_eps1.beta0, 3), np.round(r_eps1.beta, 3))
print("SVM LS (C=1e5):", round(r_ls2.beta0, 3), np.round(r_ls2.beta, 3))
print("SVM eps (C=1e5, eps=0):", round(r_eps2.beta0, 3), np.round(r_eps2.beta, 3))
OLS, median, LAD, and Huber regression compared — ects/robust2.py
"""Translated from Examples/ects/robust2.m -- OLS vs. median (quantile,
alpha=0.5) regression, LAD regression, and Huber regression, on a
simulated 3-predictor dataset.
The original draws x/u from MATLAB's unseeded `rand`/`randn`; a fixed
seed (`np.random.default_rng(0)`) is substituted here."""
import numpy as np
from quanttoolbox.econometrics.estimation import ols_estimation
from quanttoolbox.stats.regression.quantile import quantile_regression
from quanttoolbox.stats.regression.robust import huber_regression, lad_regression
rng = np.random.default_rng(0)
n, k = 100, 3
beta_true = np.arange(1, k + 1, dtype=float)
sigma = 0.20
x = rng.random((n, k))
y = x @ beta_true + sigma * rng.standard_normal(n)
r_ols = ols_estimation(y, x)
u_ols = r_ols.residuals
print("OLS beta:", np.round(r_ols.beta, 4))
alpha = 0.50
beta_med, _, _ = quantile_regression(y, x, alpha)
print("\nMedian regression beta:", np.round(beta_med, 4))
r_lad = lad_regression(y, x)
print("\nLAD regression beta:", np.round(r_lad.beta, 4), "converged:", r_lad.converged)
c = np.quantile(np.abs(u_ols), 0.90)
r_huber = huber_regression(y, x, c=c)
print(
f"\nHuber regression (c={c:.4f}) beta:",
np.round(r_huber.beta, 4),
"converged:",
r_huber.converged,
)
OLS/median/LAD plus quantile regression via IRLS and exact LP — ects/robust3.py
"""Translated from Examples/ects/robust3.m -- OLS vs. median regression,
LAD regression, and quantile regression at alpha=0.90, both via the IRLS
M-estimator (`quantile_m_regression`) and the exact LP formulation
(`quantile_regression`), on a simulated 3-predictor dataset.
The original draws x/u from MATLAB's unseeded `rand`/`randn`; a fixed
seed (`np.random.default_rng(0)`) is substituted here."""
import numpy as np
from quanttoolbox.econometrics.estimation import ols_estimation
from quanttoolbox.stats.regression.quantile import quantile_regression
from quanttoolbox.stats.regression.robust import lad_regression, quantile_m_regression
rng = np.random.default_rng(0)
n, k = 100, 3
beta_true = np.arange(1, k + 1, dtype=float)
sigma = 0.20
x = rng.random((n, k))
y = x @ beta_true + sigma * rng.standard_normal(n)
r_ols = ols_estimation(y, x)
u_ols = r_ols.residuals
print("OLS beta:", np.round(r_ols.beta, 4))
alpha = 0.50
beta_med, _, _ = quantile_regression(y, x, alpha)
print("\nMedian regression beta:", np.round(beta_med, 4))
r_lad = lad_regression(y, x)
print("\nLAD regression beta:", np.round(r_lad.beta, 4), "converged:", r_lad.converged)
alpha = 0.90
r_qm = quantile_m_regression(y, x, alpha)
print(f"\nQuantile (alpha={alpha}) regression via IRLS M-estimator, beta:", np.round(r_qm.beta, 4))
beta_q, _, _ = quantile_regression(y, x, alpha)
print(f"Quantile (alpha={alpha}) regression via exact LP, beta: ", np.round(beta_q, 4))
OLS/SVM-LS and quantile/SVM-epsilon regression on synthetic data — svm/svm8.py
"""Translated from Examples/svm/svm8.m -- OLS, LAD, quantile, and SVM
regression (primal, at two very different C values, LS- and
epsilon-insensitive) compared on a larger (n=1000) simulated dataset.
The original draws x/beta/u from MATLAB's unseeded `rand`/`randn`; a fixed
seed (`np.random.default_rng(0)`) is substituted here for reproducibility
-- this is the fixed-seed substitution the tracker notes svm8.m as still
needing."""
import numpy as np
from quanttoolbox.econometrics.estimation import ols_estimation
from quanttoolbox.stats.regression.quantile import quantile_regression
from quanttoolbox.stats.regression.robust import lad_regression
from quanttoolbox.svm.svm import svm_regression_primal
rng = np.random.default_rng(0)
n, k = 1000, 4
x = rng.random((n, k))
beta_true = 5 * rng.random(k)
u = 0.20 * rng.standard_normal(n)
beta0_true = -3.0
y = beta0_true + x @ beta_true + u
x_design = np.column_stack([np.ones(n), x])
beta_ols = ols_estimation(y, x_design).beta
beta_lad = lad_regression(y, x_design).beta
beta_lad2, _, _ = quantile_regression(y, x_design, tau=0.5)
# SVM regression (primal), C=1
r_ls1 = svm_regression_primal(y, x, c=1) # LS-SVM
beta_svm_ls = np.concatenate([[r_ls1.beta0], r_ls1.beta])
r_eps1 = svm_regression_primal(y, x, c=1, epsilon=1) # epsilon-SVM
beta_svm_epsilon = np.concatenate([[r_eps1.beta0], r_eps1.beta])
# SVM regression (primal), C=1000 (effectively unregularized)
r_ls2 = svm_regression_primal(y, x, c=1000) # LS-SVM
beta_svm_ls2 = np.concatenate([[r_ls2.beta0], r_ls2.beta])
r_eps2 = svm_regression_primal(y, x, c=1000, epsilon=0) # epsilon-SVM, epsilon=0
beta_svm_epsilon2 = np.concatenate([[r_eps2.beta0], r_eps2.beta])
results = np.column_stack(
[beta_ols, beta_lad, beta_lad2, beta_svm_ls, beta_svm_epsilon, beta_svm_ls2, beta_svm_epsilon2]
)
print("Comparison of OLS, LAD, quantile(0.5), and SVM estimates")
print(
"columns: OLS, LAD, Quantile(0.5), SVM-LS(C=1), SVM-eps(C=1,eps=1), SVM-LS(C=1000), SVM-eps(C=1000,eps=0)"
)
print(np.round(results, 3))
print("\nOLS vs. SVM-LS at large C (expect near-identical -- unregularized limit):")
print(np.round(np.column_stack([beta_ols, beta_svm_ls2]), 3))
print(
"\nQuantile(0.5) vs. SVM-eps at large C, epsilon=0 (expect near-identical -- both approach LAD):"
)
print(np.round(np.column_stack([beta_lad2, beta_svm_epsilon2]), 3))