Changelog¶
All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased¶
0.6.3 - 2026-08-30¶
Cut so that the corrected replication package reaches an archived record. 0.6.2 archived a 31-file package; #409 and #411 changed it materially, so the DOI the paper cited no longer described the package in the repository.
Fixed¶
pyproject.tomlclaimed the released version while main carried commits past the tag, so a wheel built from main would have announced itself as an already published release from a different tree. After a release main now moves to the next.devNwhileCITATION.cffgoes on naming the last release.build_online_resource.pyprinted a note and exited 0 when the DOI or the corresponding e-mail was still a placeholder, after writing a complete-looking journal supplement. It refuses before writing anything now, with--allow-placeholdersfor drafts.
Changed¶
- The replication notes state two asymmetries rather than leaving them to be discovered: the frozen stress artifact carries no software provenance, and the stress layer ships no replicate-level export, so its Monte Carlo standard errors can be read but not independently recomputed.
ROADMAP.mdandREADME.mddescribe the package that exists. Both listed as future work things that ship, and the README's module table named five modules of thirteen.
0.6.2 - 2026-08-30¶
Added¶
-
The replication package for the sparse-contamination paper is now tracked at
research/sparse_contamination/replication_package/: simulation drivers, analysis-only scripts, the frozen summary and per-replicate losses, the configuration and provenance records, the manuscript with its generated fragments, and a SHA-256 manifest over all 31 files. The paper promised a deposited archive; keeping it here means the release tarball Zenodo archives contains it, so it is citable without a separate deposit. It is excluded from the PyPI distributions, which ship only the package, tests and two metadata files. -
make verify-release VERSION=x.y.z, backed byscripts/verify_release.py, asks whether a released version is actually public: tag, GitHub release, PyPI and the Zenodo archive, plus the local metadata that names it. The preflight cannot answer this, because half of a release does not exist when it runs -- Zenodo mints the version DOI only on archiving. Run against 0.6.0 it reports the incident exactly: tagged, released on GitHub, absent from PyPI. -
make release-checkandmake release-preflight, backed byscripts/check_release.py, verify that the five files carrying the release identity agree before a release is tagged. The version lives inpyproject.toml,CITATION.cff,CHANGELOG.md,docs/about/citation.mdandREADME.md; the date lives inCITATION.cff,.zenodo.jsonandCHANGELOG.md. Most of those couplings already had tests, but they ran in CI, and the publish job depends on the test job -- so a release commit that missed a file still tagged cleanly and simply never reached PyPI. The changelog was checked by nothing at all.--pre-tagadditionally refuses a version whose tag is already spent, since PyPI will not accept a re-upload. docs/development/releasing.mdnow lists all five files. It previously named onlypyproject.tomlandCITATION.cff, which is why 0.6.0 was released with a stale changelog and stale citation guidance.
Fixed¶
- The APA, IEEE, MLA and Chicago citations in
docs/about/citation.md, and the suggested citation inREADME.md, each said "Version 0.6.1" beside the concept DOI, which resolves to whatever is newest. Five citations named a version they did not pin.check_release.pynow reads citations by block -- a fenced snippet or a run of blockquote lines, since the version and the DOI are rarely on the same line -- and requires any block naming a version to cite that version's DOI. - Two false statements in
docs/development/releasing.md.coveragedoes not run "on releases and onmain"; it runs on a release or a manual dispatch and on nothing else. And the After Release checklist still saidpublishdepends ontestalone, which the Before Tagging section had already been corrected away from in the same file.
Changed¶
-
Releasing now dispatches
coveragebefore tagging. It is the one gate no pull request exercises,publishwaits on it, and on an ordinary release it first runs against the shipped code after the tag is public -- so a failure there costs a version number for the same reason 0.6.0 did. -
The "Citing release X exactly" block in
docs/about/citation.mdquoted the concept DOI, which by that same page's table "always resolves to the most recent release". The block that exists to pin a version pinned nothing. It now carries the version DOI, andcheck_release.pyfails when the DOI named inCITATION.cffis not the one the citation guidance quotes. The gap survived because a version DOI does not exist when a release is cut -- Zenodo mints it on archiving, and updating the docs afterwards was a step nothing enforced.
Changed¶
CITATION.cffnow names the version DOI for the release it describes, and lists all seven minted DOIs. The three that were pending have been minted: 0.6.1 is 10.5281/zenodo.22166257, 0.6.0 is 10.5281/zenodo.22161110 and 0.5.0 is 10.5281/zenodo.22070996.docs/development/releasing.mdrecords thatpublishwaits on five jobs, not one, and thatcoverageruns only on releases andmain-- so no pull request exercises that gate before a release depends on it.
0.6.1 - 2026-08-29¶
Fixed¶
.zenodo.jsoncarriedpublication_date: 2026-08-23whileCITATION.cffhad been moved to 2026-08-29.scripts/validate_zenodo_metadata.pyrequires the two to agree, so the CI test job failed on every Python version and the release job, which depends on it, never published 0.6.0 to PyPI. The dates now agree. There is no code change: 0.6.1 is 0.6.0 with metadata that passes its own validator.
0.6.0 - 2026-08-29¶
Fixed¶
- The discrete distribution functions were not zero below their support.
Zipf.cdfclamped its argument up to 1 before summing, socdf(0)-- andcdf(-5)-- returned P(X = 1), which forZipf(s=1.5)is 0.387 rather than 0.DiscretePareto.cdfdid the same atk_min: withk_min = 3,cdf(2)returned 0.165. Clamping the upper end is right, since the support is truncated there; clamping the lower end is not, because below the support the answer is zero and not the first atom.
Changed¶
Zipf,YuleSimonandDiscreteParetotake a number or an array and mirror what they were given, as the continuous families have since 0.5.0.ppfreturns an integer, or an integer array.Zipf.ppfandDiscretePareto.ppffind the quantile by searching a cumulative table instead of walking the support one k at a time. The scan was fine near the median and ruinous in the tail, which is where this library is used: atZipf(s=1.1, kmax=100_000), 200 calls toppf(0.99)took 2.31s and now take 2.0ms; 20,000 quantiles took 9.24s and now take 2.0ms. Values are unchanged -- the same k for every probability tested, and seeded samples identical.
Added¶
heavytails.registry: one place mapping a name to a distribution (#383). Names resolved in at least four ways before — a table incli.py,getattr(heavytails, name)inperformance.pyandroadmap.py, and if/elif chains invalidation.pyandutilities.py, the chain repeated five times invalidation.pyalone. They disagreed: the CLI tookstudent-tandgpd, the chains tookstudenttand had no entry for the generalized Pareto,getattrtook only the class name. Every name any of them accepted is a registry alias, so nothing that worked stops working, and the CLI's table is now a view of the registry rather than a second list.
Changed¶
heavytails.performance,extensions,utilitiesandvalidationare type-checked (#384). Theirignore_errorssuppressions are gone. Most of the 47 errors were the lookup chains rebinding one variable to five distribution types; the registry removed those, and the rest were missing annotations.PerformanceBenchmarks.benchmark_samplingdeclaresdict[int, dict[str, Any]]. It returns one entry per sample size, keyed by that size, and previously claimeddict[str, Any].
Removed¶
The 59 shipped functions that raised NotImplementedError (#312). A module
of empty signatures is indistinguishable from a module of working code until
you call it, and these set an expectation the package does not meet.
heavytails.extensions:RegimeSwitchingModel,VineCopula,SpatialHeavyTailProcess,ScientificComputingIntegration,BayesianHeavyTailRegression,DistributionClassifier,DomainSpecificExtensions,TailGeometryAnalysis, andHeavyTailCopula.sample. The working copulas there are untouched.heavytails.utilities:ConfigurationManager,DataQualityAssessment,DistributionMetadata,FinancialDataScraper,CitationManager,InteractiveTutorials,PluginManager,UnitConverter.DataIO,AutoFit,ParameterValidatorandStatisticalSummaryare untouched.heavytails.roadmap:MultivariateStudentT,HeavyTailGARCH,HeavyTailSurvival,HeavyTailsAPI,gpu_sampling,adaptive_threshold_selection,bayesian_parameter_estimation,memory_efficient_sampling,diagnostic_plots. The stubMultivariateStudentTshadowed the working one inheavytails.multivariate, which is the clearest case of the harm.heavytails.performance:cython_special_functions,jit_accelerated_functions,optimized_rejection_sampling,distribution_specific_optimizations,MemoryProfiler,OnlineEstimation.heavytails.validation:fuzz_testing,special_function_accuracy_analysis,python_version_compatibility,ParameterEstimationValidation,RegressionTesting,MathematicalPropertyVerification.
The ideas are recorded in ROADMAP.md under "Considered, Not Committed", where
they promise nothing. The distribution registry is kept as work rather than
dropped.
Eighteen tests went with them, each of which asserted only that a stub raised
NotImplementedError.
Fixed¶
heavytails.viz.plot_tailoverlaid a sample drawn from the fitted model rather than the model's survival curve. That put Monte Carlo noise into the reference the reader compares against, worst in the far tail where its last points rested on a handful of draws: against Pareto(alpha=2) at n=1000 the reference wandered up to 1.238 in log survival from the curve it claimed to be -- a factor of three, seed-dependent, and entirely in the region the plot exists to show. It reads as misfit and is not. The overlay is nowfitted.sfevaluated on a grid, which is exact, monotone and independent of any seed. Points where a bounded model has zero survival are not drawn, sincelog(0)is not a point on the plot.
0.5.0 - 2026-08-23¶
NumPy is now required, and every distribution method takes a number or an array. The library was pure Python by choice, and that choice stopped paying: evaluating a density over a hundred thousand points cost a hundred thousand interpreter round trips. Over a million points it now takes about 20 milliseconds.
That is the reason this is 0.5.0 and not 0.4.1. Installing into an environment that cannot have NumPy will no longer work.
Breaking¶
- NumPy is a runtime dependency (
numpy>=1.24,<3.0). There were none through 0.4.0. - A scalar call costs more than it did.
Pareto.pdf(0.5)is about 2.6us against roughly 0.2us of plain arithmetic before, because every call goes through the array dispatch. The same ten thousand points in one call take 0.3ms, about eighty times less than the loop. Scalars are carried asnumpy.float64rather than 0-d arrays and guarded with an ordinary conditional rather thannp.where, which is what keeps it to 2.6us and not 5.3. - A few seeded variates move by one unit in the last place. NumPy's
vectorised
logandpowround differently from its scalar ones. For Weibull(k=0.5) that is 14 values in 20,000; sample means are unchanged to twelve digits and distributions are identical. Frechet, Cauchy, Pareto, GEV_Frechet and the generalized Pareto are bit-identical. vectorized.acceleratedanswers per method, not per family. It reportsTrueforLogNormal.pdf, where it used to reportFalse: the density is elementary and vectorises, and only the probabilities need the error function. It also reportsTruefor a distribution defined outside this library, because there is no longer a table of known families to consult.
Changed¶
The distributions. pdf, cdf, sf and ppf on all twelve continuous
families accept a number, a sequence or an array and return the same kind of
thing they were given. Each formula is written once, against NumPy, so the
scalar and array results come from the same expression; they can still differ
in the last bits, and the tests hold them to 1e-13 relative rather than to
equality. rvs inverts its uniforms in a single call, which makes sampling 21x
to 31x faster, and still returns a list -- returning an array would silently
turn sample_a + sample_b from concatenation into elementwise addition. ppf
checks its whole input before computing any of it and names an offending value.
The tail index estimators. hill_estimator is about 5x faster and
hill_plot about 10x on a sample of 200,000, the plot now deriving every
threshold from one cumulative sum instead of re-summing the log-excesses for
each k. The two agree exactly at every k, which they did not before.
trimmed_hill_plot uses suffix sums rather than subtracting from a running
total. _normalised_log_spacings returns an array, and
adaptive_trim_selection returns a Python float for gamma.
The multivariate families. mahalanobis, logpdf and pdf take one point
or many. Over 100,000 points the quadratic form takes 5.7ms against about 2.0s
a point at a time. fit_multivariate_t is about 45x faster -- 0.558s to 0.012s
on 5,000 observations in three dimensions, and 4.371s to 0.103s when nu is
chosen over ten candidates -- for the same answer, to ten decimals of
log-likelihood. _solve_lower is gone.
The actuarial machinery. panjer_recursion evaluates its inner sum as a
dot product: on a 2,000 point grid, 0.176s to 0.014s, with the probabilities
unchanged to the bit on the cases measured. limited_expected_value runs its
512-node midpoint rule in one call, _severity_second_moment its 4,096-node
fallback, and discretise_severity evaluates each cell edge once instead of
twice. PolicyTerms.payment and LayeredSeverity.cdf, sf, ppf take one
value or an array.
heavytails.vectorized is a thin shim, 447 lines to 198. It held a
hand-written NumPy kernel for each of 32 (family, method) pairs, every one a
transcription of the method it shadowed; the methods take arrays themselves
now. The public functions behave as before, and dist.cdf(values) is the same
call for new code.
Four families keep a Python loop for their probabilities. LogNormal,
StudentT, InverseGamma and BetaPrime need the error function or an
incomplete beta or gamma, which NumPy does not provide, so their cdf, sf
and ppf cost what they always did. Their densities are elementary and are
vectorised like everything else.
The package no longer describes itself as pure Python. CITATION.cff,
.zenodo.json, AUTHORS.md, mkdocs.yml and the citation pages carry the
title heavytails: A Python Library for Heavy-Tailed Probability
Distributions; the archived 0.1.0-0.4.0 records keep the old one, which is
correct, because those releases were pure Python. SECURITY.md no longer
justifies the security posture with "dependency-free (pure Python)", and
docs/about/license.md names NumPy's licence rather than claiming there are
none to name.
Continuous integration. Coverage runs at release only. It cost about 6.4x the runtime of the suite -- 3m12s without it, 20m37s with -- and caught none of the defects found while writing this release, every one of which was in covered code. A push to main no longer re-runs the full interpreter matrix that the pull request just ran, and superseded runs are cancelled. Test jobs went from 17-22 minutes to about 3, and a merge from 19.5 minutes to 4.7.
Added¶
- Tests tying the citation metadata together:
CITATION.cffmust name the version inpyproject.toml, and the citation guidance must cite that same version. Neither was checked, which is how the documentation sat a release behind through all of 0.4.0.
Fixed¶
GeneralizedPareto.sfreturned exactly zero in the far tail. It computed1 - cdf; atxi=0.5,sf(1e12)gave0.0where the true value is4.0e-24. This is the tail the distribution exists to describe.streaming.pykept its own transcription of the Hill and moment formulas, described as matching the batch version "operation for operation" -- which held only until the batch version changed its summation order. It calls those functions now, so the agreement is structural.test_pickands_estimator_basicdrew an unseeded sample and required a single Pickands estimate to land in(0.1, 2.0). Atk=20on 1000 points that estimator has a standard deviation of about 0.44 around a true 0.5, so the bound failed for 18% of seeds. It now averages 200 replications and asserts the estimator is centred on the true index.scripts/tail_index_study.pystamped its results with the installed package version, which an editable install reports from whenever its metadata was last built -- 0.2.0 against a working tree at 0.4.0. A saved study carrying the wrong version is worse than one carrying none.- The citation guidance cited 0.3.0 through the whole 0.4.0 release, and
CITATION.cffcarried a placeholder for a DOI Zenodo had already minted. heavytails.performanceevaluated densities throughnp.vectorize, a Python loop wearing an array interface, andheavytails.extensionsguarded NumPy in the sametryas SciPy, so a missing SciPy setnptoNonewhile NumPy sat installed and working.tests/test_zenodo_metadata.pyheld a third copy of the citation title rather than comparing the two files it was named for.
0.4.0 - 2026-08-22¶
Twelve new modules and estimators, and a dozen numerical corrections. The corrections are the reason to upgrade rather than the additions: several functions returned values that were wrong rather than merely imprecise, and one returned negative probabilities.
Some values change, because the old ones were wrong.
GeneralizedPareto.cdf,sfandpdfreturn different values belowmu, where they previously reported probabilities outside [0, 1].Weibull.pdf(0.0)returnsinffor shape below one, where it previously raisedZeroDivisionError.InverseGamma.cdf,BetaPrime.sf,Cauchy.cdfandLogNormal.cdfreturn different values in the tails, where they previously returned exactly zero or kept only a handful of digits.cdfandsfno longer sum to exactly one for several families. Each is now computed from the branch where its own value is the small quantity, which is what lets both tails be accurate; insisting on exact complementarity would force back the subtraction that made the lower tail return zero.
Added¶
heavytails.timeseries, separating heavy tails that come from volatility clustering from heavy tails that come from the shocks (#307).GARCH11with normal or Student-t innovations,fit_garch11by maximum likelihood, standardised residual extraction,extremal_indexby the Ferro-Segers intervals estimator, anddeclusterby runs.
The point the module exists to make, measured on 60,000 simulated returns: Gaussian innovations -- which have no power-law tail at all -- produce a series whose estimated tail index is 4.5. Student-t innovations with 5 degrees of freedom produce 2.85. Estimating a tail index on raw returns measures the volatility process as much as the shocks, and the docstring says which of the two a given question needs.
heavytails.copula, dependence separated from the margins (#306).GaussianCopula,StudentTCopula,GumbelCopulaandGalambosCopula, with densities, exact sampling and closed-form tail dependence, plusempirical_tail_dependencefor estimating it from data.
The Gaussian is included as the cautionary case: its tail dependence is zero at every correlation short of one, so fitting it to data whose extremes do arrive together reproduces the correlation faithfully and understates the joint risk with no sign that it has.
empirical_tail_dependence documents its own bias with measurements rather
than a warning: on 400,000 draws from a Gaussian copula, whose true
coefficient is exactly zero, it reports 0.47 at the 0.90 level and still
0.145 at 0.9995. It cannot distinguish asymptotic independence from moderate
tail dependence, and says so.
Vine copulas are deliberately not included; they need pair-copula construction and structure selection and are a subsystem rather than a class.
heavytails.multivariate, the elliptical family and joint tail dependence (#305).MultivariateStudentTandMultivariateNormalare normal scale mixtures sharing one construction, with density, sampling, Mahalanobis distance, marginals and EM fitting.tail_dependence_coefficientgives the bivariate coefficient in closed form, validated against simulation.
There is deliberately no distribution function: the multivariate t has none
in closed form above one dimension, so cdf_monte_carlo estimates it and
reports a standard error rather than presenting a simulation as exact. The
linear algebra is pure Python, which is right at the dimensions tail
dependence is asked about and would be wrong at hundreds.
-
Runnable examples in the five core modules that had none (#337): 86 across
heavy_tails,extra_distributions,discrete,plottingand_special. Every value was computed rather than transcribed, andtests/test_doctests.pynow guards all nine modules rather than the four that previously had any. -
heavytails.vectorized, evaluatingpdf,cdf,sfandppfover many points at once using NumPy when it is installed (#308). Measured on the public call at 100,000 points, the 32 accelerated calls run 2.1x to 6.4x faster, median 3.9x. Eight families have kernels; LogNormal, StudentT, InverseGamma and BetaPrime cannot, because NumPy has neither the error function nor the incomplete beta and gamma, andaccelerated()reports which is which rather than leaving a caller to guess. Without NumPy everything falls back to the loop.scripts/vectorization_benchmark.pyproduces the table. -
heavytails.streaming, tail index estimation over a stream without holding the sample (#310).TopKmaintains the largest values inO(k)memory with a min-heap;StreamingTailIndexbuilds the Hill and moment estimators on it and returns bit-for-bit the batch result, since both depend on the sample only through its topk + 1values.WindowedTailIndexdoes the same over the most recent observations, inO(window)memory, which is inherent rather than a gap: when the largest value in a window expires the new largest can be any of the survivors. -
tests/reference_values.jsonandscripts/generate_reference_values.py, a database of 209 values computed by mpmath at 50 decimal digits from the mathematical definitions, covering all twelve continuous families (#311). Every point carries its condition number, so the tolerance follows from how well the quantity can be determined at that input rather than from a flat guess. Reading the table needs no mpmath and runs in about a second. -
tests/test_distribution_properties.py, applying the generic properties -- quantile inversion, survival complementarity, monotonicity, non-negative density, support, reproducibility -- to every family from one registry rather than to four families by hand. Adds checks of the documented family relationships, of the tail index each family is supposed to have, and of moments existing exactly when the theory says. -
_gammainc_upper_reg, the regularized upper incomplete gamma computed as itself rather than as1 - P(#309). The subtraction cannot express a result below about 1e-16, so ata=2, x=50it returned exactly zero where the true value is 9.8e-21. -
_gammaincinv_reg, the inverse of the regularized incomplete gamma in both the lower and upper senses, so a caller can go through whichever of the two is its small quantity. -
adaptive_trimmed_hill_estimatorandadaptive_trim_selection, choosing the trimming parameter for the trimmed Hill estimator from the data (#321). Each normalised log-spacing is tested against the mean of the deeper ones, with an exactly computable null distribution, and the scan trims past the deepest anomaly. The median trimming chosen equals the number of planted outliers at 0, 1, 2, 3, 5 and 8 of them, and on clean data the standard deviation is 0.0295 against 0.0292 for the plain Hill estimator. Completes the eleven estimator benchmark suite;scripts/tail_index_study.pynow runs twelve. -
tests/test_doctests.py, checking that the docstring examples in the numerical modules actually reproduce. The main suite does not collect doctests, so four had stopped working unnoticed. -
heavytails.actuarial, building the aggregate loss distribution from a frequency model and a severity, and pricing the reinsurance written on it (#304). Frequency modelsPoisson,NegativeBinomialandBinomial-- the(a,b,0)class the Panjer recursion is defined on.PolicyTermsandLayeredSeverityapply deductibles, limits and coinsurance on either a per-loss or a per-payment basis.panjer_recursiongives the whole aggregate distribution;simulate_aggregate_lossandEmpiricalAggregategive the same interface from a sample, for the cases the recursion cannot reach.compound_momentsreports the exact mean and variance, includinginfwhen the severity is heavy enough that they do not exist.limited_expected_value,excess_of_loss_premiumandAggregateLoss.stop_loss_premiumprice layers. Completes the actuarial item of roadmap Phase 4. -
heavytails.viz, rendering the diagnostics with matplotlib behind a newplotextra (#302):plot_tail,plot_qq,plot_hill,plot_trimmed_hill,plot_mean_residual_lifeandplot_parameter_stability. Each takes an optionalaxand returns it, so a panel of diagnostics composes normally, andplot_tailcan overlay a fitted distribution against the empirical curve.heavytails.plottingkeeps returning coordinates and stays free of third-party imports, so the library itself still never requires matplotlib. Install withpip install "heavytails[plot]". -
heavytails.threshold, withmean_residual_life,parameter_stability,select_thresholdandreturn_level(#300). Choosing the threshold dominates a peaks-over-threshold analysis and no rule settles it, so the two diagnostics come first and the automatic rule is documented as a starting point.select_thresholduses a goodness-of-fit test whose p-values are conservative, which biases it towards thresholds that are too low.return_levelreports a bootstrap interval whose measured coverage is about 0.88 against a nominal 0.95, falling to 0.76 on smaller samples, because it captures sampling variability but not the error in choosing the threshold. -
heavytails.risk, withvalue_at_risk,expected_shortfall,tail_conditional_expectation,monte_carlo_tail_riskandmean_exists(#303). Expected shortfall has closed forms for the Pareto, log-normal, generalized Pareto and Weibull families and falls back to quadrature on the quantile function otherwise; the two paths share no code and agree to four decimal places. Expected shortfall returnsinfwhenever the distribution has no finite mean, rather than a large number that would look like a result, and the Monte Carlo estimator always reports standard errors.
Changed¶
-
InverseGamma.ppfandBetaPrime.ppfinvert the incomplete gamma and beta directly instead of bracketing and solving against their own distribution functions. Round-trip accuracy across the quantile range improves from 2.2e-05 to 9.7e-15 and from 5.6e-04 to 3.5e-15 respectively. -
_betaincinv_regstarts from the small-yasymptote rather than bisecting the whole exponent range, cuttingStudentT.ppffrom 127 to 35 microseconds with identical accuracy.
Fixed¶
-
GeneralizedParetoreturned negative probabilities belowmufor every sign ofxi: its validity check tested only1 + xi z > 0, which is the upper endpoint of a bounded distribution and is satisfied far below the support.cdf(mu - 1)returned -2.586 atxi=0.4, mu=1. Found by the new vectorisation tests, and now covered by a generic property over every family. -
Weibull.pdf(0.0)raisedZeroDivisionErrorfor shape below one, where the density diverges. It returnsinf. -
Cauchy.ppfusedtan(pi(u - 1/2)), whose argument sits next to+-pi/2where the tangent is arbitrarily steep. Atu = 1e-9it returned -318309868.8 for a true -318309886.2, wrong in the eighth digit. Now uses the cotangent form, accurate to 1.9e-16. -
_phi_inverserefined its rational approximation using0.5*(1 + erf(x/sqrt(2))), which cancels oncexis a few units negative, so the correction was computed from noise. The relative error atu = 1e-12was 4.4e-07 -- worse than the unrefined approximation. Now 4.2e-16 across the range, which also improves every normal-quantile consumer, includingLogNormal.ppfand the confidence intervals intail_indexandthreshold. -
GeneralizedPareto.ppf,BurrXII.ppfandWeibull.ppfformed1 - ubefore taking a power or a logarithm, losing the lower tail to about seven digits. Now uselog1p/expm1. -
GeneralizedPareto.cdf,BurrXII.cdf,Weibull.cdf,Cauchy.cdfandLogNormal.cdfcomputed a small probability as1 - g(x)withgapproaching one. Each now takes the branch where its own value is the small quantity. -
The safeguarded Newton iteration in
_betaincinv_regnarrowed its bracket after computing the midpoint to fall back to, so on the first iteration -- where the starting point is the midpoint by construction -- the fallback returned that same point and the no-progress check declared convergence.I_y(50, 0.3) = 1e-3came back with a relative error of 0.18. The fixed bisection to 1e-13 that preceded it had hidden this. -
InverseGamma.cdfcomputed1 - Pand returned exactly zero throughout the lower tail;cdf(0.02)atalpha=2, beta=1is 9.8e-21, not 0. -
BetaPrime.sfcomputed1 - cdf, which is exactly zero above aboutx=1e17becausex/(x+s)rounds to 1 there. It now uses the mirrored incomplete beta, whose arguments/(x+s)is computed rather than subtracted. -
Four docstring examples reported values their code never produced. Two claimed a tail index estimate of 0.5 on samples too small for the estimator's own sampling variability, so the figure shown had been transcribed from what the estimator should give rather than measured; both produced 0.4.
0.3.0 - 2026-08-21¶
Added¶
-
bias_reduced_hill_estimator, withsecond_order_rho,second_order_betaandrecommended_rho_k(#329). The Hill estimator's bias at largekis systematic rather than random, so it can be estimated and subtracted; withrhosupplied the measured bias falls by a factor of four to eighty.second_order_rhois documented as unstable, because it is: sweepingkon a sample whose truerhois -1 gives estimates from -0.07 to -20.5, the latter at a pole. Supplyingrhois strongly preferred, and the correction still helps when it is estimated. -
gpd_mle_estimatorandfit_generalized_pareto(#327), the parametric peaks-over-threshold counterpart to the semiparametric estimators. Fitting uses the reduction of Grimshaw (1993), which turns the two-parameter likelihood into a one-dimensional search, so no third-party optimiser is needed; the fit matchesscipy.stats.genpareto.fitto four decimal places for positive, near-zero and negative shape. Being a general-EVI estimator it handles a bounded tail, where the whole Hill family cannot. -
harmonic_moment_estimatorandt_hill_estimator(#325). Hill's contributions grow without limit, so one extreme observation moves the estimate arbitrarily far; these use the bounded reciprocal ratiosu / X_(i)instead. Sending a single observation of ten thousand from1e2to1e30moves the Hill estimate from 0.502 to 0.631 and moves these not at all.betatrades robustness against efficiency, and the estimator tends to the Hill estimator asbetatends to zero. -
scripts/tail_index_study.pyrecords theheavytailsversion, the git commit, the Python version and the run configuration in its JSON output, so a results file can be traced back to the code that produced it. The commit is recorded as well as the version becauseimportlib.metadatareports what is installed, which lags a working tree after a version bump. -
trimmed_hill_estimatorandtrimmed_hill_plot, following Bhattacharya, Kallitsis and Stoev (2019) (#323). Replacing three observations out of ten thousand with outliers moves the Hill estimate from 0.50 to 0.60; trimming five recovers 0.50. On clean data trimming ten raises the standard deviation only from 0.0296 to 0.0302. -
estimator_kwargsontail_index_confidence_interval, so tuning parameters such as the trimming levelrand the smoothing parameterureach the estimator. Without ittrimmed_hillsilently ran atr = 0, which is the ordinary Hill estimator and gives no robustness at all. -
Contaminated scenarios in
scripts/tail_index_study.py, since robustness is invisible on clean data.
Changed¶
-
The
tail_indexmodule docstring now states that every estimator returns the extreme-value indexgamma = 1/alpha, not the tail indexalpha(#322). The module name refers to the quantity estimated, not the parameterisation returned, and the two conventions are reciprocals. -
smoothed_hill_estimator, the smoothed Hill estimator of Resnick and Stărică (1997), andsmoothed_hill_variance_ratio, which reports the asymptotic variance reduction it achieves (#319). Averaging the Hill estimate overjin(k, u*k]reduces its asymptotic variance by 39% atu = 2and 55% atu = 3, and in the simulation study it has the lowest RMSE of any estimator here on a heavy tail. It inherits Hill's restriction to positivegamma. -
generalized_hill_estimator, the UH estimator of Beirlant, Vynckier and Teugels (1996) (#299). Unlike the Hill estimator it is consistent for every extreme-value index, not only positive ones. On a Uniform(0,1) sample, whose index is -1, Hill can only ever return a positive number and reports about +0.026; the generalized Hill estimator recovers -0.99. -
hill_plot, which sweeps k on a logarithmic grid and returns the(k, gamma)series. The documentation already told readers to find a plateau; they now have something that produces one. -
tail_index_confidence_interval, with an asymptotic interval for the Hill estimator and a percentile bootstrap for all four. Requesting the asymptotic interval for an estimator that has no established closed form raises rather than reporting a number with no basis. -
scripts/tail_index_study.py, a simulation study reporting bias, standard deviation and RMSE for every estimator across known indices and sample sizes. Its results are summarised in the tail estimation guide. -
_phi_inversemoved fromheavy_tailsto_special, alongside the other numeric helpers, sotail_indexcan use it without importing the distribution module. It was never public.
0.2.0 - 2026-08-21¶
Added¶
-
scripts/special_function_accuracy.py, which sweeps both special functions againstmpmathat 50 decimal digits and reports the worst relative error. Both are accurate to about 12 significant digits over the ranges the distributions use, with no degradation at the series/continued-fraction switch.tests/test_special_accuracy.pyasserts those bounds. -
Kolmogorov-Smirnov and Anderson-Darling goodness-of-fit tests in
heavytails.validation.GoodnessOfFitTests, which previously raisedNotImplementedError(#301). Both are reported byAutoFit.compare_distributionsand by theheavytails comparecommand, so a comparison now says whether the winning family fits, not only how it ranks. -
ConvergenceError, raised when a solver cannot reach its tolerance. It previously returned its best guess, which the caller could not distinguish from a converged answer. -
LogNormal.ppfraisedOverflowErrorwhen the quantile exceeded the float range instead of returninginf. The median ofLogNormal(mu=1000)isexp(1000), which is genuinely not representable, soinfis the correct answer; raising broke parameter sweeps and made the failure look like a caller error. (#296) -
LogNormal.sfis computed withmath.erfcrather than as1 - cdf(x), which reached exactly zero byx = 1e5and carried no information beyond it. It now matches SciPy to about 1e-14 relative out tox = 1e12. -
Python 3.13 support, covered by the CI test matrix and declared in the package classifiers.
-
py.typedmarker, so the type annotations that ship with the package are visible to downstream type checkers. TheTyping :: Typedclassifier was previously advertised without one. -
heavytails.__version__, resolved from the installed distribution metadata. -
--version/-Vflag on theheavytailscommand-line interface. -
cliinstallation extra (pip install "heavytails[cli]"). The console script depends ontyperandrich, which were previously development-only dependencies, so the entry point was broken for anyone installing from PyPI. -
CodeQL analysis and dependency-review workflows.
-
.github/CODEOWNERS,.gitattributesand.zenodo.json. -
StudentT.cdf,StudentT.sfandStudentT.ppf. The class previously offered onlypdfandrvs, with a docstring stating that the CDF and PPF "require special functions not in stdlib" — but the regularized incomplete beta needed to write them was already implemented inextra_distributions. All three agree with SciPy to around 1e-14. -
Cauchy.sf,Frechet.sfandGEV_Frechet.sf, so every continuous family now provides the full interface the documentation advertises. -
YuleSimon.sfandYuleSimon.ppf. The survival function uses the closed formP(X > k) = k * B(k, rho + 1), and the quantile function brackets and bisects rather than scanning linearly. -
heavytails._special, holding the shared numeric special functions so that both distribution modules can use them without an import cycle. The previous names remain importable fromheavytails.extra_distributions. -
_betaincinv_reg, an inverse for the regularized incomplete beta. It solves in log-space with a symmetry reduction, which is what keeps extreme quantiles accurate. -
Documentation pages that existing pages already linked to but which had never been written: CLI reference, diagnostics guide, extreme value theory, tail index estimation theory, validation studies, architecture, benchmarking, code review, and an executable
basic_usagenotebook.
Changed¶
-
Migrated project metadata to the PEP 621
[project]table. -
Grouped Dependabot updates so routine bumps arrive as a few reviewable pull requests rather than one per package.
-
Continuous integration now also runs on pull requests targeting
develop, builds and metadata-checks the distributions before publishing, verifies thatpoetry.lockmatchespyproject.toml, and builds the documentation with--strict. -
Security scanning fails the build on findings instead of uploading a report that no one reads.
safetywas replaced bypip-audit, which needs no account to run. -
Refreshed the locked dependency set, clearing 116 known vulnerabilities reported against the previously locked development and documentation toolchain.
-
Pre-commit hooks are pinned to the same tool versions as the development dependencies, so local hooks and CI now agree.
-
Replaced the
Makefiletargets, which measured coverage ofscripts/rather than of the package. -
mkdocstringsis configured for Google-style docstrings, which is what the package actually uses. Under the previousnumpysetting noArgs:orReturns:section was parsed anywhere in the API reference. -
The documentation navigation lists every page. Five API reference entries all pointed at the same directory, and the
gen-filesscript duplicated the hand-written reference pages whileliterate-navlooked for aSUMMARY.mdthat was never generated.
Fixed¶
-
The continued-fraction branch of the regularized lower incomplete gamma was wrong, not merely imprecise: its Lentz recurrence was missing its leading term and its
bwas shifted by one, soP(20, 21)returned0.0against a true0.6157(#297). That branch is reached wheneverx >= a + 1, andInverseGamma.cdfevaluatesP(alpha, beta/x), so its reported probabilities were wrong by factors of 2 to 17 in the lower tail. Found by comparing againstmpmath; the property-based checks could not have caught it, because a consistently wrong value is still monotone and still lies in [0, 1]. -
Quantile functions now behave the same way across every family (#298). Previously the answer at the edges depended on which family you asked:
Pareto,Weibull,Frechet,GEV_Frechet,BurrXIIandLogLogisticraisedOverflowErrorwhere the quantile simply exceeded the float range,InverseGammaraised aValueErrorabout failing to bracket the root,LogNormalreturnedinf, andBetaPrimereturned a finite value. All of them now returninf. -
DiscretePareto.ppfaccepted any float, including0.0,1.5and negatives, and silently returned the support bounds. It now raisesValueErrorlike every other family. -
_ppf_monotonenarrowed its bracket only on bisection fallbacks, never on accepted Newton steps, so a run of Newton steps could exhaust the iteration budget with the bracket as wide as it started. It now narrows on every iteration, which also fixed a non-monotonicity inInverseGamma.ppf. -
heavytails benchmarkraisedZeroDivisionErroron platforms with a low-resolution wall clock, because a sub-millisecond timing measured exactly zero seconds. Timings now usetime.perf_counter(). -
The same defect in the performance tests, which failed intermittently on Windows.
-
Removed an invalid PyPI classifier (
Topic :: Scientific/Engineering :: Statistics) that would have been rejected on upload. -
scripts/pyproject_updater.pydepends ontomlkitandpackaging, which were never declared and only happened to be installed transitively. -
Pinned Poetry 2.2.1 in every workflow. The previous 1.8.3 pin cannot read the version 2.1 lock file this repository uses.
-
Removed a
preferred-citationentry fromCITATION.cffthat pointed at an unpublished paper with a placeholder DOI, which citation tooling would have emitted as a real reference. -
YuleSimon.pmfraisedOverflowErrorfor k of about 170 and above, because it multiplied gamma functions that overflow individually even though their ratio is small. Since sampling called it in a loop, drawing from the tail crashed. It is now evaluated withlgamma. -
Nineteen documentation links pointed at pages that did not exist. The documentation job now builds with
--strict, so a broken link fails CI. -
convergence_validationtook a_max_iterparameter that was never used and documented it under a different name. -
Removed a stale
xfailmarker on the Student-t PPF convergence test. It was recording precision loss that the new incomplete beta inverse eliminates.
Removed¶
-
roadmap.improved_incomplete_beta, a placeholder that delegated to_betainc_regunchanged. The accuracy work it stood in for is now done and measured. -
roadmap.safe_lognormal_ppf, a workaround that caught the overflow above. It is redundant now thatLogNormal.ppfhandles the case itself. -
tox.ini, which ran the test suite againstscripts/instead of the package and duplicated the CI matrix. -
IMPROVEMENT_PLAN.mdfrom the repository root. It described gaps that have since been closed; forward-looking plans live in ROADMAP.md. -
The
isortdevelopment dependency and its configuration, superseded by Ruff'sIrules.
0.1.0 - 2025-10-25¶
Added¶
- Continuous heavy-tailed distributions implemented from first principles: Pareto, Cauchy, Student-t, Log-Normal, Weibull, Fréchet and GEV (ξ > 0).
- Additional continuous families: Generalized Pareto, Burr XII, Log-Logistic (Fisk), Inverse-Gamma and Beta-Prime.
- Discrete heavy-tailed distributions: Zipf, Yule–Simon and Discrete Pareto.
- Tail index estimators: Hill, Pickands and moment.
- Diagnostic plotting helpers for log–log tail and QQ plots.
- Deterministic RNG wrapper for reproducible sampling.
- Custom incomplete-gamma and incomplete-beta implementations, and a safeguarded-Newton numeric PPF solver for families without a closed form.
heavytailscommand-line interface.- Documentation site built with MkDocs Material.