From b626176d44e61b46264e6db48db8f21f451f6bcf Mon Sep 17 00:00:00 2001 From: jorenham Date: Sun, 10 Dec 2023 02:04:52 +0100 Subject: [PATCH 01/26] reparemetrized GLD into standard form, like Wakeby --- docs/distributions.md | 52 ++++++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/docs/distributions.md b/docs/distributions.md index 7f69b989..40eba8cc 100644 --- a/docs/distributions.md +++ b/docs/distributions.md @@ -1176,44 +1176,64 @@ implementation. The [Tukey lambda distribution ](https://wikipedia.org/wiki/Tukey_lambda_distribution) can be extended to -the *generalized lambda distribution*, which has two scale parameters -\( \alpha, \gamma \), and two shape parameters \( \beta, \delta \). +the *generalized lambda distribution* (GLD). +Like the Wakeby distribution, Lmo uses an unconventional "standardized" +paremetrization, with shape parameters \( \beta,\ \delta,\ \phi \), where +\( \phi \in [0, 1] \) replaces the more commonly used shape parameters +\( \alpha \) and \( \beta \). Refer to the Wakeby section for details. -Like the Wakeby distribution, the generalized lambda has no closed-form PDF -or CDF. Instead, it is defined through its PPF: +Like the Wakeby distribution, the PDF and CDF of the GPD are not analytically +expressible. Instead, the GPD is defined through its PPF (quantile function, +the inverse of the CDF): \[ -x(F) = \alpha \qlog{1 - \beta}{F} - \gamma \qlog{1 - \delta}{1 - F} +x(F) = \phi \qlog{1 - \beta}{F} - (1 - \phi) \qlog{1 - \delta}{1 - F} \] -Although its central product moments have no closed-form expression, when -\( \beta > -1 \) and \( \delta > -1 \), the general trimmed L-moments can be -compactly expressed as: +The domain is + +\[ +\left. +\begin{array}{l} + \text{if } \beta \le 0: & \displaystyle -\infty \\ + \text{if } 0 < \beta: & \displaystyle -\frac \phi \beta +\end{array} +\right\} \le +x +\le \begin{cases} + \displaystyle \infty \ , & \text{if } \delta \le 0 \\ + \displaystyle \frac{1 - \phi}{\phi} \ , & \text{if } 0 < \delta +\end{cases} +\] + +Unlike GPD's central product-moments, which have no general closed-form +expression, its trimmed L-moments can be expressed quite elegantly. +When \( \beta > -1 \) and \( \delta > -1 \), all L-moments are defined for +\( r \ge 1 \) and \( s, t \ge 0 \) as: \[ \begin{equation} \tlmoment{s,t}{r} - = \alpha + = \phi \frac {\rfact{r + s}{t + 1} \ \ffact{\beta + s}{r + s - 1}} {r \ \rfact{\beta}{r + s + t + 1}} - + (-1)^r \gamma \ + + (-1)^r (1 - \phi) \ \frac {\rfact{r + t}{s + t} \ \ffact{\delta + t}{r + t - 1}} {r \ \rfact{\delta}{r + s + t + 1}} - \underbrace{ \ffact{1}{r} \left( - \frac \alpha \beta - \frac \gamma \delta + \frac \phi \beta - \frac{1 - \phi}{\delta} \right) }_{\text{will be } 0 \text{ if } r>1} \end{equation} \] -When \( \alpha = \gamma \) and \( \beta = \delta \), this is the -(non-generalized) Tukey-lambda distribution, which has been implemented as -[`scipy.stats.tukeylambda`][scipy.stats.tukeylambda]. Currently, this -4-parameter generalization has no [`scipy.stats`][scipy.stats] implementation. - +When \( \phi = 1 - \phi = \frac 1 2 \) and \( \beta = \delta \), GPD is the +(non-generalized) Tukey-lambda distribution, implemented as +[`scipy.stats.tukeylambda`][scipy.stats.tukeylambda]. +At the moment, the GPD itself has no Python implementation *yet*. ## Constants and special functions From 796f5a06dc57cf8019f6eaa2dfcabd784014c891 Mon Sep 17 00:00:00 2001 From: jorenham Date: Sun, 10 Dec 2023 18:46:06 +0100 Subject: [PATCH 02/26] typo fixes --- docs/distributions.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/distributions.md b/docs/distributions.md index 40eba8cc..a2b63635 100644 --- a/docs/distributions.md +++ b/docs/distributions.md @@ -1182,8 +1182,8 @@ paremetrization, with shape parameters \( \beta,\ \delta,\ \phi \), where \( \phi \in [0, 1] \) replaces the more commonly used shape parameters \( \alpha \) and \( \beta \). Refer to the Wakeby section for details. -Like the Wakeby distribution, the PDF and CDF of the GPD are not analytically -expressible. Instead, the GPD is defined through its PPF (quantile function, +Like the Wakeby distribution, the PDF and CDF of the GLD are not analytically +expressible. Instead, the GLD is defined through its PPF (quantile function, the inverse of the CDF): \[ @@ -1196,17 +1196,18 @@ The domain is \left. \begin{array}{l} \text{if } \beta \le 0: & \displaystyle -\infty \\ - \text{if } 0 < \beta: & \displaystyle -\frac \phi \beta + \text{if } \beta > 0: & \displaystyle -\frac \phi \beta \end{array} \right\} \le x -\le \begin{cases} +\le \left\{ \begin{array}{l} \displaystyle \infty \ , & \text{if } \delta \le 0 \\ - \displaystyle \frac{1 - \phi}{\phi} \ , & \text{if } 0 < \delta -\end{cases} + \displaystyle \frac{1 - \phi}{\phi} \ , & \text{if } \delta > 0 +\end{array} +\right. \] -Unlike GPD's central product-moments, which have no general closed-form +Unlike GLD's central product-moments, which have no general closed-form expression, its trimmed L-moments can be expressed quite elegantly. When \( \beta > -1 \) and \( \delta > -1 \), all L-moments are defined for \( r \ge 1 \) and \( s, t \ge 0 \) as: @@ -1230,7 +1231,7 @@ When \( \beta > -1 \) and \( \delta > -1 \), all L-moments are defined for \end{equation} \] -When \( \phi = 1 - \phi = \frac 1 2 \) and \( \beta = \delta \), GPD is the +When \( \phi = 1 - \phi = \frac 1 2 \) and \( \beta = \delta \), GLD is the (non-generalized) Tukey-lambda distribution, implemented as [`scipy.stats.tukeylambda`][scipy.stats.tukeylambda]. At the moment, the GPD itself has no Python implementation *yet*. From bf955e0d6f98b527a496dbc3d93599180032c462 Mon Sep 17 00:00:00 2001 From: jorenham Date: Sun, 10 Dec 2023 19:21:18 +0100 Subject: [PATCH 03/26] switch to `pymdownx.blocks.admonition` --- docs/distributions.md | 116 ++++++++++++++++++++++-------------------- mkdocs.yml | 9 ++-- 2 files changed, 67 insertions(+), 58 deletions(-) diff --git a/docs/distributions.md b/docs/distributions.md index a2b63635..e038a405 100644 --- a/docs/distributions.md +++ b/docs/distributions.md @@ -24,23 +24,25 @@ likely to include some novel solutions. This is also the reason for the lack of references. But this should pose no problems in practise, since Lmo makes it trivial to check if they aren't incorrect. -!!! tip - Numerical calculation of these L-statistics using `scipy.stats` - distributions, refer to - [`rv_continuous.l_stats`][lmo.contrib.scipy_stats.l_rv_generic.l_stats]. +## Simple distributions - For direct calculation of the L-stats from a CDF or PPF (quantile function, - inverse CDF), see [`l_stats_from_cdf`][lmo.theoretical.l_stats_from_cdf] or - [`l_stats_from_ppf`][lmo.theoretical.l_stats_from_ppf], respectively. +/// tip +Numerical calculation of these L-statistics using [`scipy.stats`][scipy.stats] +distributions, refer to +[`rv_continuous.l_stats`][lmo.contrib.scipy_stats.l_rv_generic.l_stats]. +For direct calculation of the L-stats from a CDF or PPF (quantile function, +inverse CDF), see [`l_stats_from_cdf`][lmo.theoretical.l_stats_from_cdf] or +[`l_stats_from_ppf`][lmo.theoretical.l_stats_from_ppf], respectively. +/// -## L-stats - -An overview of the untrimmed L-location, L-scale, L-skewness and L-kurtosis, +An overview of the L-location, L-scale, L-skewness and L-kurtosis, of a bunch of popular univariate probability distributions, for which they exist (in closed form). +### L-stats + @@ -352,10 +354,7 @@ exist (in closed form).
-## TL-stats - -Collection of TL-location, -scale, -skewness, -kurtosis coefficients, with -symmetric trimming of order 1, i.e. `trim=(1, 1)`. +### TL-stats @@ -895,29 +894,33 @@ where \( H_n \) is a [harmonic number](#def-harmonic). See [`scipy.stats.genpareto`][scipy.stats.genpareto] for an Lmo-compatible implementation. -!!! info "Special cases" +/// admonition | Special cases + type: info - There are several notable special cases of the GPD: +There are several notable special cases of the GPD: - [\( q \)-Exponential](https://wikipedia.org/wiki/Q-exponential_distribution) - : When \( \alpha > -1 \), GPD is \( q \)-exponential with shape - \( q = 2 - 1 / (1 + \alpha) \) and rate (inverse scale) - \( \lambda = \alpha + 1 \). +[\( q \)-Exponential](https://wikipedia.org/wiki/Q-exponential_distribution) +: When \( \alpha > -1 \), GPD is \( q \)-exponential with shape + \( q = 2 - 1 / (1 + \alpha) \) and rate (inverse scale) + \( \lambda = \alpha + 1 \). - [Exponential](https://wikipedia.org/wiki/Exponential_distribution) - : When \( \alpha = 0 \), GPD is standard exponential. +[Exponential](https://wikipedia.org/wiki/Exponential_distribution) +: When \( \alpha = 0 \), GPD is standard exponential. - [Uniform](https://wikipedia.org/wiki/Continuous_uniform_distribution) - : When \( \alpha = 1 \) GPD is uniform on \( [0, 1] \). +[Uniform](https://wikipedia.org/wiki/Continuous_uniform_distribution) +: When \( \alpha = 1 \) GPD is uniform on \( [0, 1] \). +/// -!!! info "Generalizations" +/// admonition | Generalizations + type: info - [Wakeby's distribution](#wakeby) - : Implemented as [`lmo.distributions.wakeby`][lmo.distributions.wakeby]. - See below for details, including the general L-moments in closed-form. +[Wakeby's distribution](#wakeby) +: Implemented as [`lmo.distributions.wakeby`][lmo.distributions.wakeby]. + See below for details, including the general L-moments in closed-form. - [Kappa distribution](https://doi.org/10.1147/rd.383.0251) - : Implemented in as [`scipy.stats.kappa4`][scipy.stats.kappa4]. +[Kappa distribution](https://doi.org/10.1147/rd.383.0251) +: Implemented in as [`scipy.stats.kappa4`][scipy.stats.kappa4]. +/// ### Burr III / Dagum @@ -1097,19 +1100,19 @@ x(F) = \] -!!! note "Alternative parametrization" +/// note | Alternative parametrization +This 3-parameter Wakeby distribution is equivalent to the 5-parameter +variant that is generally used, after scaling by \( \sigma \) and shifting +by \( \xi \). The shape parameters \( \beta \) and \( \delta \) are +(intentionally) equivalent, the scale parameters are related by +\( \alpha \equiv \sigma \phi \) and \( gamma \equiv \sigma (1 - \phi) \), +and the location parameter is precisely \( \xi \). - This 3-parameter Wakeby distribution is equivalent to the 5-parameter - variant that is generally used, after scaling by \( \sigma \) and shifting - by \( \xi \). The shape parameters \( \beta \) and \( \delta \) are - (intentionally) equivalent, the scale parameters are related by - \( \alpha \equiv \sigma \phi \) and \( gamma \equiv \sigma (1 - \phi) \), - and the location parameter is precisely \( \xi \). - - Conversely, Lmo's "standard" Wakeby distribution can by obtained from - 5-Wakeby, by shifting and scaling s.t. \( \xi = 0 \) and - \( \alpha + \gamma = 1 \). Finally, \( \phi \equiv \alpha = 1 - \gamma \) - effectively combines the two scale parameters. +Conversely, Lmo's "standard" Wakeby distribution can by obtained from +5-Wakeby, by shifting and scaling s.t. \( \xi = 0 \) and +\( \alpha + \gamma = 1 \). Finally, \( \phi \equiv \alpha = 1 - \gamma \) +effectively combines the two scale parameters. +/// Lmo figured out that when \( \delta < t + 1 \), all of Wakeby's (trimmed) L-moments can be expressed as @@ -1155,22 +1158,25 @@ where \( H_n \) is a [harmonic number](#def-harmonic). See [`lmo.distributions.wakeby`][lmo.distributions.wakeby] for the implementation. -!!! info "Special cases" +/// admonition | Special cases + type: info +There are several notable special cases of the Wakeby distribution: + +[GPD -- Generalized Pareto](#gpd) +: With \( \phi = 0 \), Wakeby is the standard GPD, and + \( \delta \) its shape parameter. - There are several notable special cases of the Wakeby distribution: + Conversely, \( \phi = 1 \) yields a *bounded* GPD variant, with + shape parameter \( -\beta \), and \( 1 / \beta \) the upper bound. - [GPD -- Generalized Pareto](#gpd) - : With \( \phi = 0 \), Wakeby is the standard GPD, and - \( \delta \) its shape parameter. +[Exponential](https://wikipedia.org/wiki/Exponential_distribution) +: With \( \beta = \delta = 0 \) and \( \phi = 1 \), Wakeby is + standard exponential. - Conversely, \( \phi = 1 \) yields a *bounded* GPD variant, with - shape parameter \( -\beta \), and \( 1 / \beta \) the upper bound. - [Exponential](https://wikipedia.org/wiki/Exponential_distribution) - : With \( \beta = \delta = 0 \) and \( \phi = 1 \), Wakeby is - standard exponential. - [Uniform](https://wikipedia.org/wiki/Continuous_uniform_distribution) - : With \( \beta = \phi = 1 \) (and therefore \( \delta = 0 \)) Wakeby - is uniform on \( [0, 1] \). +[Uniform](https://wikipedia.org/wiki/Continuous_uniform_distribution) +: With \( \beta = \phi = 1 \) (and therefore \( \delta = 0 \)) Wakeby + is uniform on \( [0, 1] \). +/// ### Generalized Lambda diff --git a/mkdocs.yml b/mkdocs.yml index fedab8b0..77edcb37 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -72,7 +72,7 @@ plugins: markdown_extensions: # https://python-markdown.github.io/extensions/ - - admonition + # - admonition - extra - md_in_html - sane_lists @@ -81,14 +81,17 @@ markdown_extensions: permalink: true # https://facelessuser.github.io/pymdown-extensions/ + - pymdownx.arithmatex: + generic: true - pymdownx.betterem + - pymdownx.blocks.admonition + - pymdownx.blocks.details - pymdownx.highlight - pymdownx.inlinehilite + - pymdownx.striphtml - pymdownx.superfences - pymdownx.tasklist: custom_checkbox: true - - pymdownx.arithmatex: - generic: true extra_css: - styles/theme.css From ad4eb1104aa4e6ff027f4bacd33cbe35afba32ae Mon Sep 17 00:00:00 2001 From: jorenham Date: Sun, 10 Dec 2023 20:18:23 +0100 Subject: [PATCH 04/26] abbreviation tooltips --- docs/distributions.md | 50 ++++++++++++++++++++++++------------------- mkdocs.yml | 10 +++++++-- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/docs/distributions.md b/docs/distributions.md index e038a405..bbc73dc1 100644 --- a/docs/distributions.md +++ b/docs/distributions.md @@ -748,9 +748,7 @@ The general trimmed L-moments of the Gompertz distribution are: ### GEV -The [*generalized extreme value* (GEV) -](https://wikipedia.org/wiki/Generalized_extreme_value_distribution) -distribution unifies the +The [GEV](https://wikipedia.org/wiki/GEV_distribution) unifies the [Gumbel](https://wikipedia.org/wiki/Gumbel_distribution), [Fréchet](https://wikipedia.org/wiki/Fr%C3%A9chet_distribution), and [Weibull](https://wikipedia.org/wiki/Weibull_distribution) distributions. @@ -758,10 +756,10 @@ It has one shape parameter \( \alpha \in \mathbb{R} \), and the following distribution functions: \[ - \begin{align*} - F(x) &= e^{-\qexp{1 - \alpha}{-x}} \\ - x(F) &= -\qlog{1 - \alpha}{-\ln(F)} - \end{align*} +\begin{align*} + F(x) &= e^{-\qexp{1 - \alpha}{-x}} \\ + x(F) &= -\qlog{1 - \alpha}{-\ln(F)} +\end{align*} \] Here, \( \qexp{q}{y} \) and \( \qlog{q}{y} \) are the @@ -770,7 +768,7 @@ Here, \( \qexp{q}{y} \) and \( \qlog{q}{y} \) are the respectively. An alternative parametrization is sometimes used, e.g. on -[Wikipedia](https://wikipedia.org/wiki/Generalized_extreme_value_distribution), +[Wikipedia](https://wikipedia.org/wiki/GEV_distribution), where \( \xi = -\alpha \). The convention that is used here, is the same as in [`scipy.stats.genextreme`][scipy.stats.genextreme], where `c` corresponds to @@ -779,7 +777,7 @@ The convention that is used here, is the same as in The trimmed L-moments of the GEV are \[ - \begin{equation} +\begin{equation} \tlmoment{s, t}{r} = \frac{(-1)^{r}}{r} \sum_{k = s + 1}^{r + s + t} @@ -795,7 +793,7 @@ The trimmed L-moments of the GEV are \end{cases} \right) \label{eq:lr_gev} - \end{equation} +\end{equation} \] Note that the GEV is effectively a reparametrized @@ -805,8 +803,7 @@ Note that the GEV is effectively a reparametrized ### GLO -The *generalized logistic distribution* (GLO), also known as the [shifted -log-logistic distribution +The GLO, also known as the [shifted log-logistic distribution ](https://wikipedia.org/wiki/Shifted_log-logistic_distribution), with shape parameter \( \alpha \in \mathbb{R} \), is characterized by the following distribution functions: @@ -856,8 +853,7 @@ Note that the GLO is effectively a reparametrized ### GPD -The [*generalized Pareto distribution* -](https://wikipedia.org/wiki/Generalized_Pareto_distribution) (GPD), with +The [GPD](https://wikipedia.org/wiki/Generalized_Pareto_distribution), with shape parameter \( \alpha \in \mathbb{R} \), has for \( x \ge 0 \) the distribution functions: @@ -896,7 +892,6 @@ implementation. /// admonition | Special cases type: info - There are several notable special cases of the GPD: [\( q \)-Exponential](https://wikipedia.org/wiki/Q-exponential_distribution) @@ -1085,7 +1080,7 @@ The domain of the distribution is \end{cases} \] -The quantile function (PPF) is defined to be +The PPF is defined to be \[ x(F) = -\phi \qlog{1 - \beta}{1 - F} - (1 - \phi) \qlog{1 + \delta}{1 - F} @@ -1178,19 +1173,17 @@ There are several notable special cases of the Wakeby distribution: is uniform on \( [0, 1] \). /// -### Generalized Lambda +### GLD -The [Tukey lambda distribution -](https://wikipedia.org/wiki/Tukey_lambda_distribution) can be extended to -the *generalized lambda distribution* (GLD). +The GLD is a flexible generalization of the [Tukey lambda distribution +](https://wikipedia.org/wiki/Tukey_lambda_distribution). Like the Wakeby distribution, Lmo uses an unconventional "standardized" paremetrization, with shape parameters \( \beta,\ \delta,\ \phi \), where \( \phi \in [0, 1] \) replaces the more commonly used shape parameters \( \alpha \) and \( \beta \). Refer to the Wakeby section for details. Like the Wakeby distribution, the PDF and CDF of the GLD are not analytically -expressible. Instead, the GLD is defined through its PPF (quantile function, -the inverse of the CDF): +expressible. Instead, the GLD is defined through its PPF: \[ x(F) = \phi \qlog{1 - \beta}{F} - (1 - \phi) \qlog{1 - \delta}{1 - F} @@ -1501,3 +1494,16 @@ and constants.
[`scipy.special.boxcox`][scipy.special.boxcox]
+ +*[STD]: Standard deviation +*[MAD]: Median absolute deviation +*[RV]: Random variable +*[PMF]: Probability mass function +*[PDF]: Probability density function +*[CDF]: Cumulative distribution function +*[PPF]: Percent point function, inverse of the CDF, a.k.a. quantile function +*[QDF]: Quantile density function, derivative of the PPF +*[GEV]: Generalized (maximum) Extreme Value distribution +*[GLO]: Generalized Logistic distribution +*[GPD]: Generalized Pareto Distribution +*[GLD]: Generalized Tukey–Lambda Distribution diff --git a/mkdocs.yml b/mkdocs.yml index 77edcb37..c00550af 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -19,7 +19,7 @@ theme: logo: img/logo.png favicon: img/favicon.ico features: - # - navigation.sections + - content.tooltips - navigation.path - navigation.tabs - navigation.tabs.sticky @@ -72,7 +72,8 @@ plugins: markdown_extensions: # https://python-markdown.github.io/extensions/ - # - admonition + - abbr + - attr_list - extra - md_in_html - sane_lists @@ -85,9 +86,14 @@ markdown_extensions: generic: true - pymdownx.betterem - pymdownx.blocks.admonition + - pymdownx.blocks.definition - pymdownx.blocks.details + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.highlight - pymdownx.inlinehilite + - pymdownx.snippets: - pymdownx.striphtml - pymdownx.superfences - pymdownx.tasklist: From 433826766e17d99d57c2ca77f2ba418e2061da62 Mon Sep 17 00:00:00 2001 From: jorenham Date: Sun, 10 Dec 2023 20:24:28 +0100 Subject: [PATCH 05/26] minor fixes --- docs/distributions.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/docs/distributions.md b/docs/distributions.md index bbc73dc1..489fec53 100644 --- a/docs/distributions.md +++ b/docs/distributions.md @@ -24,7 +24,6 @@ likely to include some novel solutions. This is also the reason for the lack of references. But this should pose no problems in practise, since Lmo makes it trivial to check if they aren't incorrect. - ## Simple distributions /// tip @@ -32,8 +31,8 @@ Numerical calculation of these L-statistics using [`scipy.stats`][scipy.stats] distributions, refer to [`rv_continuous.l_stats`][lmo.contrib.scipy_stats.l_rv_generic.l_stats]. -For direct calculation of the L-stats from a CDF or PPF (quantile function, -inverse CDF), see [`l_stats_from_cdf`][lmo.theoretical.l_stats_from_cdf] or +For direct calculation of the L-stats from a CDF or PPF, see +[`l_stats_from_cdf`][lmo.theoretical.l_stats_from_cdf] or [`l_stats_from_ppf`][lmo.theoretical.l_stats_from_ppf], respectively. /// @@ -864,7 +863,6 @@ distribution functions: \end{align*} \] - The L-moments of the GPD exist when \( \alpha < 1 + t \), and can be compactly expressed as @@ -972,7 +970,6 @@ The alternative parametrization \( \alpha \mapsto 1 / \gamma \), where \( \gamma > 0 \), is known as the (standard) type IV [*Pareto distribution*](https://wikipedia.org/wiki/Pareto_distribution) - The distribution functions for \( x > 0 \) are defined as: \[ @@ -1003,7 +1000,7 @@ This distribution is implemented in and `d` correspond to \( \alpha \) and \( \beta \), respectively. The Burr XII and Burr III distributions are related as \( Y = 1 / X \), where -\( X \) and \( Y \) are random variables with Burr XII \( (\alpha, \beta) \) +\( X \) and \( Y \) are RV's with Burr XII \( (\alpha, \beta) \) and Burr III \( (1 / \alpha, \beta) \) distributions (or vice-versa), respectively. @@ -1094,7 +1091,6 @@ x(F) = - \frac{1 - \phi}{\delta} (1 - (1 - F)^{-\delta}) \] - /// note | Alternative parametrization This 3-parameter Wakeby distribution is equivalent to the 5-parameter variant that is generally used, after scaling by \( \sigma \) and shifting From e0fbf152fe1b4b96fd27191b240735118f9e19b4 Mon Sep 17 00:00:00 2001 From: jorenham Date: Sun, 10 Dec 2023 21:13:20 +0100 Subject: [PATCH 06/26] added references --- docs/distributions.md | 76 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 15 deletions(-) diff --git a/docs/distributions.md b/docs/distributions.md index 489fec53..a932abe4 100644 --- a/docs/distributions.md +++ b/docs/distributions.md @@ -696,8 +696,8 @@ wolfram alpha / mathematica. ### Bernoulli Surprisingly, the L-moments of the discrete -[Bernoulli distribution](https://wikipedia.org/wiki/Bernoulli_distribution), -can't be expressed as easily as the distribution itself: +[Bernoulli distribution](https://wikipedia.org/wiki/Bernoulli_distribution) +[^BERN], can't be expressed as easily as the distribution itself: \[ \begin{equation} @@ -714,12 +714,15 @@ Here, \( \jacobi{n}{\alpha}{\beta}{x} \) is a [Jacobi polynomial](#def-jacobi) (although it's not orthogonal, since \( \beta > -1 \) does not hold). +[^BERN]: + [J.V. Uspensky (1937)](https://www.worldcat.org/oclc/996937) -- + Introduction to mathematical probability ### Gompertz The [Gompertz distribution](https://wikipedia.org/wiki/Gompertz_distribution) -with shape parameter \( \alpha > 0 \) and \( x \ge 0 \), has the following CDF -and PPF: +[^GOMP] with shape parameter \( \alpha > 0 \) and \( x \ge 0 \), has the +following CDF and PPF: \[ @@ -745,6 +748,11 @@ The general trimmed L-moments of the Gompertz distribution are: \end{equation} \] +[^GOMP]: + [B. Gompertz (1825)](https://doi.org/10.1098/rstl.1825.0026) -- On the + nature of the function expressive of the law of human mortality, and on a + new mode of determining the value of life contingencies. + ### GEV The [GEV](https://wikipedia.org/wiki/GEV_distribution) unifies the @@ -800,9 +808,14 @@ Note that the GEV is effectively a reparametrized [Tsallis distribution](https://wikipedia.org/wiki/Tsallis_distribution), with \( q = 1 - \alpha \). +[^GEV]: + [A.F. Jenkinson (1955)](https://doi.org/10.1002/qj.49708134804) -- + The frequency distribution of the annual maximum (or minimum) values of + meteorological elements + ### GLO -The GLO, also known as the [shifted log-logistic distribution +The GLO [^GLO], also known as the [shifted log-logistic distribution ](https://wikipedia.org/wiki/Shifted_log-logistic_distribution), with shape parameter \( \alpha \in \mathbb{R} \), is characterized by the following distribution functions: @@ -850,10 +863,15 @@ Note that the GLO is effectively a reparametrized [Tsallis distribution](https://wikipedia.org/wiki/Tsallis_distribution), with \( q = 1 - \alpha \). +[^GLO]: + [J.R.M. Hosking (1986) + ](https://dominoweb.draco.res.ibm.com/reports/RC12210.pdf) -- + The theory of probability weighted moments + ### GPD -The [GPD](https://wikipedia.org/wiki/Generalized_Pareto_distribution), with -shape parameter \( \alpha \in \mathbb{R} \), has for \( x \ge 0 \) the +The [GPD](https://wikipedia.org/wiki/Generalized_Pareto_distribution) [^GPD], +with shape parameter \( \alpha \in \mathbb{R} \), has for \( x \ge 0 \) the distribution functions: \[ @@ -915,9 +933,14 @@ There are several notable special cases of the GPD: : Implemented in as [`scipy.stats.kappa4`][scipy.stats.kappa4]. /// +[^GPD]: + [J.R.M. Hosking & J.R. Wallis (1987) + ](https://doi.org/10.1080/00401706.1987.10488243) -- Parameter and + Quantile Estimation for the Generalized Pareto Distribution + ### Burr III / Dagum -The *Burr III* distribution, also known as the +The *Burr III* distribution [^BURR], also known as the [*Dagum distribution*](https://wikipedia.org/wiki/Dagum_distribution), has two shape parameters \( \alpha \) and \( \beta \), both restricted to the positive reals @@ -963,7 +986,7 @@ distribution ### Burr XII / Pareto IV The -[*Burr XII distribution*](https://wikipedia.org/wiki/Burr_distribution) +[*Burr XII distribution*](https://wikipedia.org/wiki/Burr_distribution) [^BURR] has two shape parameters \( \alpha \) and \( \beta \), both restricted to the positive reals. It is also known as the *Singh-Maddala distribution*. The alternative parametrization \( \alpha \mapsto 1 / \gamma \), where @@ -1009,14 +1032,18 @@ In the special case where \( \alpha = 1 \) is known as the has been implemented as [scipy.stats.lomax][scipy.stats.lomax], where the parameter `c` corresponds to \( \beta \). +[^BURR]: + [I.W. Burr (1942)](https://doi.org/10.1214%2Faoms%2F1177731607) -- + Cumulative Frequency Functions + ### Kumaraswamy For [Kumaraswamy's distribution -](https://wikipedia.org/wiki/Kumaraswamy_distribution) with parameters +](https://wikipedia.org/wiki/Kumaraswamy_distribution) [^KUM1] with parameters \( \alpha \in \mathbb{R}_{>0} \) and \( \beta \in \mathbb{R}_{>0} \), -the general solution for the \( r \)th L-moment has been derived by -[Jones (2009)](https://doi.org/10.1016/j.stamet.2008.04.001). This can be -extended for the general trimmed L-moments. +the general solution for the \( r \)th (untrimmed L-moment has been derived by +M.C. Jones in 2009 [^KUM2]. Lmo has extended these results for the general +trimmed L-moments. The distribution functions are for \( 0 \le x \le 1 \) defined as: @@ -1045,10 +1072,19 @@ Its general \( r \)-th trimmed L-moment are: The Kumaraswamy distribution is implemented in [`lmo.distributions.kumaraswamy`][lmo.distributions.kumaraswamy]. +[^KUM1]: + [P. Kumaraswamy](https://doi.org/10.1016/0022-1694(80)90036-0) -- + A generalized probability density function for double-bounded random + processes +[^KUM2]: + [M.C. Jones (2009)](https://doi.org/10.1016/j.stamet.2008.04.001) -- + Kumaraswamy’s distribution: A beta-type distribution with some + tractability advantages + ### Wakeby The [*Wakeby distribution*](https://wikipedia.org/wiki/Wakeby_distribution) -is quantile-based -- the CDF and PDF are not analytically expressible for the +[^WAK] is quantile-based -- the CDF and PDF are not analytically expressible for the general case. Without loss of generality, Lmo uses a 3-parameter "standardized" paremetrization, with shape parameters \( \beta,\ \delta,\ \phi \). @@ -1169,9 +1205,13 @@ There are several notable special cases of the Wakeby distribution: is uniform on \( [0, 1] \). /// +[^WAK]: + [J.C. Houghton (1978)](https://doi.org/10.1029/WR014i006p01105) -- Birth + of a parent: The Wakeby Distribution for modeling flood flows + ### GLD -The GLD is a flexible generalization of the [Tukey lambda distribution +The GLD [^GLD] is a flexible generalization of the [Tukey lambda distribution ](https://wikipedia.org/wiki/Tukey_lambda_distribution). Like the Wakeby distribution, Lmo uses an unconventional "standardized" paremetrization, with shape parameters \( \beta,\ \delta,\ \phi \), where @@ -1231,6 +1271,11 @@ When \( \phi = 1 - \phi = \frac 1 2 \) and \( \beta = \delta \), GLD is the [`scipy.stats.tukeylambda`][scipy.stats.tukeylambda]. At the moment, the GPD itself has no Python implementation *yet*. +[^GLD]: + [J.S. Ramberg & B.W. Schmeiser (1974) + ](https://doi.org/10.1145/360827.360840) -- An approximate method for + generating asymmetric random variables + ## Constants and special functions An overview of the (non-obvious) mathematical notation of special functions @@ -1491,6 +1536,7 @@ and constants. + *[STD]: Standard deviation *[MAD]: Median absolute deviation *[RV]: Random variable From ee977c038ff15b1895fe8423f8818db893a09aaa Mon Sep 17 00:00:00 2001 From: jorenham Date: Sun, 10 Dec 2023 22:05:46 +0100 Subject: [PATCH 07/26] fixed domain of GLD --- docs/distributions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/distributions.md b/docs/distributions.md index a932abe4..335ed13f 100644 --- a/docs/distributions.md +++ b/docs/distributions.md @@ -1237,7 +1237,7 @@ The domain is x \le \left\{ \begin{array}{l} \displaystyle \infty \ , & \text{if } \delta \le 0 \\ - \displaystyle \frac{1 - \phi}{\phi} \ , & \text{if } \delta > 0 + \displaystyle \frac{1 - \phi}{\delta} \ , & \text{if } \delta > 0 \end{array} \right. \] From 1d3a0971dbc17ed1d5279f36709820f97ca8a11e Mon Sep 17 00:00:00 2001 From: jorenham Date: Mon, 11 Dec 2023 00:33:02 +0100 Subject: [PATCH 08/26] changed the GLD phi param for tukey-lambda compat --- docs/distributions.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/distributions.md b/docs/distributions.md index 335ed13f..fd3a5aff 100644 --- a/docs/distributions.md +++ b/docs/distributions.md @@ -1213,16 +1213,16 @@ There are several notable special cases of the Wakeby distribution: The GLD [^GLD] is a flexible generalization of the [Tukey lambda distribution ](https://wikipedia.org/wiki/Tukey_lambda_distribution). -Like the Wakeby distribution, Lmo uses an unconventional "standardized" +Lmo uses an unconventional "standardized" paremetrization, with shape parameters \( \beta,\ \delta,\ \phi \), where -\( \phi \in [0, 1] \) replaces the more commonly used shape parameters -\( \alpha \) and \( \beta \). Refer to the Wakeby section for details. +\( \phi \in [-1, 1] \) replaces the more commonly used shape parameters +\( \alpha \mapsto 1 + \phi \) and \( \gamma \mapsto 1 - \phi \). Like the Wakeby distribution, the PDF and CDF of the GLD are not analytically expressible. Instead, the GLD is defined through its PPF: \[ -x(F) = \phi \qlog{1 - \beta}{F} - (1 - \phi) \qlog{1 - \delta}{1 - F} +x(F) = (\phi + 1) \qlog{1 - \beta}{F} + (\phi - 1) \qlog{1 - \delta}{1 - F} \] The domain is @@ -1231,7 +1231,7 @@ The domain is \left. \begin{array}{l} \text{if } \beta \le 0: & \displaystyle -\infty \\ - \text{if } \beta > 0: & \displaystyle -\frac \phi \beta + \text{if } \beta > 0: & \displaystyle -\frac{1 + \phi}{\beta} \end{array} \right\} \le x @@ -1250,7 +1250,7 @@ When \( \beta > -1 \) and \( \delta > -1 \), all L-moments are defined for \[ \begin{equation} \tlmoment{s,t}{r} - = \phi + = (1 + \phi) \frac {\rfact{r + s}{t + 1} \ \ffact{\beta + s}{r + s - 1}} {r \ \rfact{\beta}{r + s + t + 1}} @@ -1260,16 +1260,17 @@ When \( \beta > -1 \) and \( \delta > -1 \), all L-moments are defined for {r \ \rfact{\delta}{r + s + t + 1}} - \underbrace{ \ffact{1}{r} \left( - \frac \phi \beta - \frac{1 - \phi}{\delta} + \frac{\phi + 1} \beta + \frac{\phi - 1}{\delta} \right) }_{\text{will be } 0 \text{ if } r>1} \end{equation} \] -When \( \phi = 1 - \phi = \frac 1 2 \) and \( \beta = \delta \), GLD is the -(non-generalized) Tukey-lambda distribution, implemented as -[`scipy.stats.tukeylambda`][scipy.stats.tukeylambda]. At the moment, the GPD itself has no Python implementation *yet*. +But, when \( \beta = \delta \) and \( \phi = 0 \), GLD is the +regular Tukey-lambda distribution with shape +\( \lambda \equiv \beta = \delta \), which is implemented as +[`scipy.stats.tukeylambda`][scipy.stats.tukeylambda]. [^GLD]: [J.S. Ramberg & B.W. Schmeiser (1974) From 1934f63425bdf9a3aeb9f875f8c6ab36a6f0d3bd Mon Sep 17 00:00:00 2001 From: jorenham Date: Mon, 11 Dec 2023 04:06:49 +0100 Subject: [PATCH 09/26] Added `theoretical.entropy_from_qdf` --- lmo/theoretical.py | 47 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/lmo/theoretical.py b/lmo/theoretical.py index 32df1800..714a9667 100644 --- a/lmo/theoretical.py +++ b/lmo/theoretical.py @@ -19,6 +19,8 @@ 'l_comoment_from_pdf', 'l_coratio_from_pdf', + + 'entropy_from_qdf', ) import functools @@ -1528,3 +1530,48 @@ def l_coratio_from_pdf( ) return ll_r / np.expand_dims(ll_r0.diagonal(), -1) + + +def entropy_from_qdf( + qdf: Callable[Concatenate[float, Theta], float], + /, + *args: Theta.args, + **kwds: Theta.kwargs, +) -> float: + r""" + Evaluate the (differential / continuous) entropy \( H(X) \) of a + univariate random variable \( X \), from its *quantile density + function* (QDF), \( q(u) = \frac{\mathrm{d} F^{-1}(u)}{\mathrm{d} u} \), + with \( F^{-1} \) the inverse of the CDF, i.e. the PPF / quantile function. + + The derivation follows from the identity \( f(x) = 1 / q(F(x)) \) of PDF + \( f \), specifically: + + \[ + h(X) + = \E[-\ln f(X)] + = \int_\mathbb{R} \ln \frac{1}{f(x)} \mathrm{d} x + = \int_1 \ln q(u) \mathrm{d} u + \] + + Args: + qdf: + The quantile distribution function (QDF), with signature + `(float, ...) -> float`. + *args: + Optional additional positional arguments to pass to `qdf`. + **kwds: + Optional keyword arguments to pass to `qdf`. + + Returns: + The differential entropy \( h(X) \). + + See Also: + - [Differential entropy - Wikipedia + ](https://wikipedia.org/wiki/Differential_entropy) + + """ + def ic(p: float) -> float: + return np.log(qdf(p, *args, **kwds)) + + return cast(float, sci.quad(ic, 0, 1, limit=QUAD_LIMIT)[0]) From c70d4699eed1eaf4201ab7e8c412ea9a45eff58f Mon Sep 17 00:00:00 2001 From: jorenham Date: Mon, 11 Dec 2023 05:53:11 +0100 Subject: [PATCH 10/26] initial GLD implementation as `distributions.genlambda` --- lmo/distributions.py | 231 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 228 insertions(+), 3 deletions(-) diff --git a/lmo/distributions.py b/lmo/distributions.py index bc9f26e8..3decfac6 100644 --- a/lmo/distributions.py +++ b/lmo/distributions.py @@ -3,6 +3,7 @@ 'l_rv_nonparametric', 'kumaraswamy', 'wakeby', + 'genlambda', ) # pyright: reportIncompatibleMethodOverride=false @@ -36,7 +37,7 @@ ) from .diagnostic import l_ratio_bounds from .special import harmonic -from .theoretical import l_moment_from_ppf +from .theoretical import entropy_from_qdf, l_moment_from_ppf from .typing import ( AnyTrim, FloatVector, @@ -661,7 +662,7 @@ def _wakeby_sf0( # noqa: C901 z = .7 eps = 1e-8 - maxit = 20 + maxit = 50 ufl = math.log(math.nextafter(0, 1)) for _ in range(maxit): @@ -765,6 +766,14 @@ def _get_support( return self.a, _wakeby_ub(b, d, f) + def _fitstart( + self, + data: npt.NDArray[np.float64], + args: tuple[float, float, float] | None = None, + ) -> tuple[float, float, float, float, float]: + # Arbitrary, but the default f=1 is a bad start + return super()._fitstart(data, args or (1., 1., .5)) # type: ignore + def _pdf( self, x: npt.NDArray[np.float64], @@ -898,5 +907,221 @@ def _entropy(self, b: float, d: float, f: float) -> float: [`wakeby`][wakeby] takes \( b \), \( d \) and \( f \) as shape parameters. -For details, see [Theoretical L-moments - Wakeby](distributions.md#wakeby). +For a detailed description of the Wakeby distribution, refer to +[Distributions - Wakeby](distributions.md#wakeby). +""" + +def _genlambda_support(b: float, d: float, f: float) -> tuple[float, float]: + xa = -(1 + f) / b if b > 0 else -math.inf + xb = (1 - f) / d if d > 0 else math.inf + return xa, xb + +def _genlambda_ppf0(q: float, b: float, d: float, f: float) -> float: + """PPF of the GLD.""" + if math.isnan(q): + return math.nan + if q <= 0: + return _genlambda_support(b, d, f)[0] + if q >= 1: + return _genlambda_support(b, d, f)[1] + + u = math.log(q) if b == 0 else (q**b - 1) / b + v = math.log(1 - q) if d == 0 else ((1 - q)**d - 1) / d + return (1 + f) * u - (1 - f) * v + +_genlambda_ppf = np.vectorize(_genlambda_ppf0, [float]) + +def _genlambda_qdf(q: V, b: float, d: float, f: float) -> V: + return cast(V, (1 + f) * q**(b - 1) - (1 - f) * (1 - q)**(d - 1)) + +def _genlambda_cdf0( # noqa: C901 + x: float, + b: float, + d: float, + f: float, + *, + ptol: float = 1e-4, + xtol: float = 1e-7, + maxiter: int = 100, +) -> float: + """ + Compute the CDF of the GLD using bracketing search with special checks. + + Uses the same (unnamed?) algorithm as `scipy.special.tklmbda`: + https://github.com/scipy/scipy/blob/v1.11.4/scipy/special/cephes/tukey.c + """ + if math.isnan(x) or math.isnan(b) or math.isnan(d) or math.isnan(f): + return math.nan + + # extrema + xa, xb = _genlambda_support(b, d, f) + if x <= xa: + return 0 + if x >= xb: + return 1 + + # special cases + if abs(f + 1) < ptol: + return 1 - math.exp(-x) if d == 0 else 1 - (1 - d * x)**(1 / d) + if abs(f - 1) < ptol: + return math.exp(x) if b == 0 else (1 + b * x)**(1 / b) + if f < ptol and abs(b) < ptol and abs(d) < ptol: + return (1 + np.tanh(x)) / 2 + if abs(b - 1) < ptol and abs(d - 1) < ptol: + assert 0 <= x + f <= 1, (x, f) + return x + f + + # bracketing search, using a similar algorithm as `scipy.special.tklmbda` + p_min, p_mid, p_max = 0.0, 0.5, 1.0 + p_low, p_high = p_min, p_max + for _ in range(maxiter): + x_eval = _genlambda_ppf0(p_mid, b, d, f) + if abs(x_eval - x) <= xtol: + break + + if x_eval > x: + p_high = p_mid + p_mid = (p_mid + p_low) / 2 + else: + p_low = p_mid + p_mid = (p_mid + p_high) / 2 + + if (p_mid - p_low)**2 <= xtol: + break + + return p_mid + + +_genlambda_cdf = np.vectorize( + _genlambda_cdf0, + [float], + excluded={'ptol', 'xtol', 'maxiter'}, +) + + +class genlambda_gen(_rv_continuous): # noqa: N801 + def _argcheck(self, b: float, d: float, f: float) -> int: + return np.isfinite(b) & np.isfinite(d) & (f >= 0) & (f <= 1) + + def _shape_info(self) -> Sequence[_ShapeInfo]: + ibeta = _ShapeInfo('b', False, (-np.inf, np.inf), (False, False)) + idelta = _ShapeInfo('d', False, (-np.inf, np.inf), (False, False)) + iphi = _ShapeInfo('f', False, (-1, 1), (True, True)) + return [ibeta, idelta, iphi] + + def _get_support( + self, + b: float, + d: float, + f: float, + ) -> tuple[float, float]: + return _genlambda_support(b, d, f) + + def _fitstart( + self, + data: npt.NDArray[np.float64], + args: tuple[float, float, float] | None = None, + ) -> tuple[float, float, float, float, float]: + # Arbitrary, but the default f=1 is a bad start + return super()._fitstart(data, args or (1., 1., 0.)) # type: ignore + + def _pdf( + self, + x: npt.NDArray[np.float64], + b: float, + d: float, + f: float, + ) -> npt.NDArray[np.float64]: + return 1 / _genlambda_qdf(self._cdf(x, b, d, f), b, d, f) + + def _cdf( + self, + x: npt.NDArray[np.float64], + b: float, + d: float, + f: float, + ) -> npt.NDArray[np.float64]: + return _genlambda_cdf(x, b, d, f) + + def _ppf( + self, + x: npt.NDArray[np.float64], + b: float, + d: float, + f: float, + ) -> npt.NDArray[np.float64]: + return _genlambda_ppf(x, b, d, f) + + def _stats(self, b: float, d: float, f: float) -> tuple[ + float | None, + float | None, + float | None, + float | None, + ]: + if b <= -1 or d <= -1: + # hard NaN (not inf); indeterminate integral + return math.nan, math.nan, math.nan, math.nan + + a, c = 1 + f, 1 - f + b1, d1 = 1 + b + 1, 1 + d + + m1 = c / d1 - a / b1 + + if b <= -1 / 2 or d <= -1 / 2: + return m1, math.nan, math.nan, math.nan + + if b == d == 0: + m2 = 4 * f**2 + math.pi**2 * (1 - f**2) / 3 + elif b == 0: + m2 = ( + a**2 + + (c / d1)**2 / (d1 + d) + + 2 * a * c / (d * d1) * (1 - cast(float, harmonic(1 + d))) + ) + elif d == 0: + m2 = ( + c**2 + + (a / b1)**2 / (b1 + b) + + 2 * a * c / (b * b1) * (1 - cast(float, harmonic(1 + b))) + ) + else: + m2 = ( + (a / b1)**2 / (b1 + b) + + (c / d1)**2 / (d1 + d) + + 2 * a * c / (b * d) * ( + 1 / (b1 * d1) + - cast(float, sc.beta(b1, d1)) # type: ignore + ) + ) + + # Feeling adventurous? You're welcome to contribute these missing + # skewness and kurtosis stats here :) + if b <= -1 / 3 or d <= -1 / 3: + return m1, m2, math.nan, math.nan + m3 = None + + if b <= -1 / 4 or d <= -1 / 4: + return m1, m2, m3, math.nan + m4 = None + + return m1, m2, m3, m4 + + def _entropy(self, b: float, d: float, f: float) -> float: + return entropy_from_qdf(_genlambda_qdf, b, d, f) + + +genlambda: RVContinuous[float, float, float] = genlambda_gen( + name='genlambda', +) # type: ignore +r"""A generalized Tukey-Lambda random variable. + +`genlambda` takes `b`, `d` and `f` as shape parameters. +`b` and `d` can be any float, and `f` requires `-1 <= f <= 1`. + +If `f == 0` and `b == d`, `genlambda` is equivalent to +[`scipy.stats.tukeylambda`][scipy.stats.tukeylambda], with `b` (or `d`) as +shape parameter. + +For a detailed description of the GLD, refer to +[Distributions - GLD](distributions.md#gld). """ From e3b815dc2f570e806aa4c990f7466428728d3576 Mon Sep 17 00:00:00 2001 From: jorenham Date: Mon, 11 Dec 2023 05:53:37 +0100 Subject: [PATCH 11/26] refer to `distributions.genlambda` from GLD --- docs/distributions.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/distributions.md b/docs/distributions.md index fd3a5aff..0849a1ba 100644 --- a/docs/distributions.md +++ b/docs/distributions.md @@ -1266,9 +1266,11 @@ When \( \beta > -1 \) and \( \delta > -1 \), all L-moments are defined for \end{equation} \] -At the moment, the GPD itself has no Python implementation *yet*. -But, when \( \beta = \delta \) and \( \phi = 0 \), GLD is the -regular Tukey-lambda distribution with shape +The GLD is implemented as +[`lmo.distributions.genlamda`][lmo.distributions.genlambda]. + +When \( \beta = \delta \) and \( \phi = 0 \), GLD is the +"regular" Tukey-lambda distribution with shape \( \lambda \equiv \beta = \delta \), which is implemented as [`scipy.stats.tukeylambda`][scipy.stats.tukeylambda]. From 96afd6a47d33a2a7a2fc16af67d4de03a00719c3 Mon Sep 17 00:00:00 2001 From: jorenham Date: Mon, 11 Dec 2023 13:44:25 +0100 Subject: [PATCH 12/26] grammar fix --- docs/distributions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/distributions.md b/docs/distributions.md index 0849a1ba..83201761 100644 --- a/docs/distributions.md +++ b/docs/distributions.md @@ -1218,8 +1218,8 @@ paremetrization, with shape parameters \( \beta,\ \delta,\ \phi \), where \( \phi \in [-1, 1] \) replaces the more commonly used shape parameters \( \alpha \mapsto 1 + \phi \) and \( \gamma \mapsto 1 - \phi \). -Like the Wakeby distribution, the PDF and CDF of the GLD are not analytically -expressible. Instead, the GLD is defined through its PPF: +As with the Wakeby distribution, the PDF and CDF of the GLD are not +analytically expressible. Instead, the GLD is defined through its PPF: \[ x(F) = (\phi + 1) \qlog{1 - \beta}{F} + (\phi - 1) \qlog{1 - \delta}{1 - F} From dcce3939dee501b5d24bb70583e6920cddab8b8e Mon Sep 17 00:00:00 2001 From: jorenham Date: Mon, 11 Dec 2023 13:52:12 +0100 Subject: [PATCH 13/26] cleaner types in `entropy_from_qdf` docs --- lmo/theoretical.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lmo/theoretical.py b/lmo/theoretical.py index 714a9667..70ea0fb0 100644 --- a/lmo/theoretical.py +++ b/lmo/theoretical.py @@ -1555,12 +1555,11 @@ def entropy_from_qdf( \] Args: - qdf: - The quantile distribution function (QDF), with signature - `(float, ...) -> float`. - *args: + qdf ( (float, *Ts, **Ts) -> float): + The quantile distribution function (QDF). + *args (*Ts): Optional additional positional arguments to pass to `qdf`. - **kwds: + **kwds (**Ts): Optional keyword arguments to pass to `qdf`. Returns: From 6b8c958c2cd6a17a44dc77dd5d71ab8e7d030b5b Mon Sep 17 00:00:00 2001 From: jorenham Date: Mon, 11 Dec 2023 14:20:31 +0100 Subject: [PATCH 14/26] loosened GLD L-moment contraints --- docs/distributions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/distributions.md b/docs/distributions.md index 83201761..e2496a9d 100644 --- a/docs/distributions.md +++ b/docs/distributions.md @@ -1244,7 +1244,7 @@ x Unlike GLD's central product-moments, which have no general closed-form expression, its trimmed L-moments can be expressed quite elegantly. -When \( \beta > -1 \) and \( \delta > -1 \), all L-moments are defined for +When \( \beta > -s - 1 \) and \( \delta > -t - 1 \), all L-moments are defined for \( r \ge 1 \) and \( s, t \ge 0 \) as: \[ From 3b3c9b4bf141f71b70db2476756b05ed2ebb9b07 Mon Sep 17 00:00:00 2001 From: jorenham Date: Mon, 11 Dec 2023 15:24:54 +0100 Subject: [PATCH 15/26] GLD L-moment equation fix --- docs/distributions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/distributions.md b/docs/distributions.md index e2496a9d..216b4c6c 100644 --- a/docs/distributions.md +++ b/docs/distributions.md @@ -1256,7 +1256,7 @@ When \( \beta > -s - 1 \) and \( \delta > -t - 1 \), all L-moments are defined f {r \ \rfact{\beta}{r + s + t + 1}} + (-1)^r (1 - \phi) \ \frac - {\rfact{r + t}{s + t} \ \ffact{\delta + t}{r + t - 1}} + {\rfact{r + t}{s + 1} \ \ffact{\delta + t}{r + t - 1}} {r \ \rfact{\delta}{r + s + t + 1}} - \underbrace{ \ffact{1}{r} \left( From 4c427c134b0db20aeda9e447bc5779d162ad8f4e Mon Sep 17 00:00:00 2001 From: jorenham Date: Mon, 11 Dec 2023 16:36:49 +0100 Subject: [PATCH 16/26] explicit GLD L-moment limiting cases --- docs/distributions.md | 53 +++++++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/docs/distributions.md b/docs/distributions.md index 216b4c6c..db1c5c8f 100644 --- a/docs/distributions.md +++ b/docs/distributions.md @@ -1243,29 +1243,48 @@ x \] Unlike GLD's central product-moments, which have no general closed-form -expression, its trimmed L-moments can be expressed quite elegantly. -When \( \beta > -s - 1 \) and \( \delta > -t - 1 \), all L-moments are defined for -\( r \ge 1 \) and \( s, t \ge 0 \) as: +expression, its trimmed L-moments can be expressed quite elegantly using +[falling factorials](#def-falling). +When \( \beta > -s - 1 \) and \( \delta > -t - 1 \), all L-moments are defined +for \( r \ge 1 \) and \( s, t \ge 0 \) as \[ -\begin{equation} - \tlmoment{s,t}{r} - = (1 + \phi) +\begin{align} + \tlmoment{s, t}{1} + &= (\phi + 1) \left( \begin{cases} + \displaystyle H_s - H_{s + t + 1} + & \text{if } \beta = 0 \\ + \displaystyle \frac + {\ffact{n}{t + 1} \ \ffact{s + \beta}{s}} + {\ffact{n + \beta}{n + 1}} + - \frac{1}{\beta} + & \text{if } \beta \neq 0 + \end{cases} \right) + + (\phi - 1) \left( \begin{cases} + \displaystyle H_t - H_{s + t + 1} + & \text{if } \delta = 0 \\ + \displaystyle \frac + {\ffact{n}{s + 1} \ \ffact{t + \beta}{t}} + {\ffact{n + \delta}{n + 1}} + - \frac{1}{\delta} + & \text{if } \delta \neq 0 + \end{cases} \right) + \\ + r \tlmoment{s, t}{r} + &= (\phi + 1) \frac - {\rfact{r + s}{t + 1} \ \ffact{\beta + s}{r + s - 1}} - {r \ \rfact{\beta}{r + s + t + 1}} - + (-1)^r (1 - \phi) \ + {\ffact{n}{t + 1} \ \ffact{s + \beta}{n - t - 1}} + {\ffact{n + \beta}{n + 1}} + + (-1)^{r-1} (\phi - 1) \ \frac - {\rfact{r + t}{s + 1} \ \ffact{\delta + t}{r + t - 1}} - {r \ \rfact{\delta}{r + s + t + 1}} - - \underbrace{ - \ffact{1}{r} \left( - \frac{\phi + 1} \beta + \frac{\phi - 1}{\delta} - \right) - }_{\text{will be } 0 \text{ if } r>1} -\end{equation} + {\ffact{n}{s + 1} \ \ffact{t + \delta}{n - s - 1}} + {\ffact{n + \delta}{n + 1}} + \ , +\end{align} \] +with \( n = r + s + t \). + The GLD is implemented as [`lmo.distributions.genlamda`][lmo.distributions.genlambda]. From 8660324cb6d2e20cc19c60f1e2e85b45e6ab6bd6 Mon Sep 17 00:00:00 2001 From: jorenham Date: Mon, 11 Dec 2023 16:40:39 +0100 Subject: [PATCH 17/26] Cleaner harmonic numbers in the GLD L-moments --- docs/distributions.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/distributions.md b/docs/distributions.md index db1c5c8f..eb51080d 100644 --- a/docs/distributions.md +++ b/docs/distributions.md @@ -1252,7 +1252,7 @@ for \( r \ge 1 \) and \( s, t \ge 0 \) as \begin{align} \tlmoment{s, t}{1} &= (\phi + 1) \left( \begin{cases} - \displaystyle H_s - H_{s + t + 1} + \displaystyle H_s - H_n & \text{if } \beta = 0 \\ \displaystyle \frac {\ffact{n}{t + 1} \ \ffact{s + \beta}{s}} @@ -1261,7 +1261,7 @@ for \( r \ge 1 \) and \( s, t \ge 0 \) as & \text{if } \beta \neq 0 \end{cases} \right) + (\phi - 1) \left( \begin{cases} - \displaystyle H_t - H_{s + t + 1} + \displaystyle H_t - H_n & \text{if } \delta = 0 \\ \displaystyle \frac {\ffact{n}{s + 1} \ \ffact{t + \beta}{t}} @@ -1283,7 +1283,7 @@ for \( r \ge 1 \) and \( s, t \ge 0 \) as \end{align} \] -with \( n = r + s + t \). +with \( n = r + s + t \), and \( H_k \) a [harmonic number](#def-harmonic). The GLD is implemented as [`lmo.distributions.genlamda`][lmo.distributions.genlambda]. From 1ce4e22dbc05ddf58ed9e3ef9682ffdfc104ca53 Mon Sep 17 00:00:00 2001 From: jorenham Date: Mon, 11 Dec 2023 23:32:14 +0100 Subject: [PATCH 18/26] rewrote GLD L-moments using beta functions --- docs/distributions.md | 70 ++++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/docs/distributions.md b/docs/distributions.md index eb51080d..f66369e2 100644 --- a/docs/distributions.md +++ b/docs/distributions.md @@ -1243,47 +1243,49 @@ x \] Unlike GLD's central product-moments, which have no general closed-form -expression, its trimmed L-moments can be expressed quite elegantly using -[falling factorials](#def-falling). -When \( \beta > -s - 1 \) and \( \delta > -t - 1 \), all L-moments are defined -for \( r \ge 1 \) and \( s, t \ge 0 \) as +expression, its trimmed L-moments can be compactly expressed. +When \( \beta > -s - 1 \) and \( \delta > -t - 1 \), the L-moments are defined +for \( r = 2, 3, \ldots \) and \( s, t \ge 0 \) as \[ -\begin{align} - \tlmoment{s, t}{1} - &= (\phi + 1) \left( \begin{cases} - \displaystyle H_s - H_n - & \text{if } \beta = 0 \\ - \displaystyle \frac - {\ffact{n}{t + 1} \ \ffact{s + \beta}{s}} - {\ffact{n + \beta}{n + 1}} - - \frac{1}{\beta} - & \text{if } \beta \neq 0 - \end{cases} \right) - + (\phi - 1) \left( \begin{cases} - \displaystyle H_t - H_n - & \text{if } \delta = 0 \\ - \displaystyle \frac - {\ffact{n}{s + 1} \ \ffact{t + \beta}{t}} - {\ffact{n + \delta}{n + 1}} - - \frac{1}{\delta} - & \text{if } \delta \neq 0 - \end{cases} \right) - \\ +\begin{equation} r \tlmoment{s, t}{r} - &= (\phi + 1) + = \frac{(-1)^r \ (1 + \phi)}{r + s + t + \beta} \frac - {\ffact{n}{t + 1} \ \ffact{s + \beta}{n - t - 1}} - {\ffact{n + \beta}{n + 1}} - + (-1)^{r-1} (\phi - 1) \ + {\B(1 + s + \beta ,\ r - 1 - \beta)} + {\B(r + s + t + \beta,\ 1 - \beta)} + + \frac{1 - \phi}{r + s + t + \delta} \frac - {\ffact{n}{s + 1} \ \ffact{t + \delta}{n - s - 1}} - {\ffact{n + \delta}{n + 1}} - \ , -\end{align} + {\B(1 + t + \delta ,\ r - 1 - \delta)} + {\B(r + s + t + \delta,\ 1 - \delta)} \ , +\end{equation} +\] + +and the arbitrarily-trimmed L-location is + +\[ +\begin{equation} + \tlmoment{s, t}{1} + = (\phi + 1) \mathfrak{L}_{1}^{(s)}(\beta) + + (\phi - 1) \mathfrak{L}_{1}^{(t)}(\delta) \, +\end{equation} \] -with \( n = r + s + t \), and \( H_k \) a [harmonic number](#def-harmonic). +where + +\[ +\mathfrak{L}_{1}^{(k)}(\theta) = \begin{cases} + \displaystyle H_k - H_{s + t + 1} + & \text{if } \theta = 0 \\ + \displaystyle \frac{1}{\theta}\left( + \frac + {\B(1 + k + \theta,\ 2 + s + t)} + {\B(1 + k,\ 2 + s + t + \theta)} + - 1 + \right) + & \text{otherwise.} +\end{cases} +\] The GLD is implemented as [`lmo.distributions.genlamda`][lmo.distributions.genlambda]. From 0698fd86ef72fc340ce95a14d5b9d279f3395a58 Mon Sep 17 00:00:00 2001 From: jorenham Date: Tue, 12 Dec 2023 02:52:52 +0100 Subject: [PATCH 19/26] consistend GLD L-moment sign usage --- docs/distributions.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/distributions.md b/docs/distributions.md index f66369e2..a2f16c79 100644 --- a/docs/distributions.md +++ b/docs/distributions.md @@ -1266,8 +1266,8 @@ and the arbitrarily-trimmed L-location is \[ \begin{equation} \tlmoment{s, t}{1} - = (\phi + 1) \mathfrak{L}_{1}^{(s)}(\beta) - + (\phi - 1) \mathfrak{L}_{1}^{(t)}(\delta) \, + = -(1 + \phi) \mathfrak{L}_{1}^{(s)}(\beta) + + (1 - \phi) \mathfrak{L}_{1}^{(t)}(\delta) \, \end{equation} \] @@ -1275,13 +1275,12 @@ where \[ \mathfrak{L}_{1}^{(k)}(\theta) = \begin{cases} - \displaystyle H_k - H_{s + t + 1} + \displaystyle H_{s + t + 1} - H_k & \text{if } \theta = 0 \\ \displaystyle \frac{1}{\theta}\left( - \frac + 1 - \frac {\B(1 + k + \theta,\ 2 + s + t)} {\B(1 + k,\ 2 + s + t + \theta)} - - 1 \right) & \text{otherwise.} \end{cases} From c6d714055f9a2bf836c3642ece828e4b0de5130f Mon Sep 17 00:00:00 2001 From: jorenham Date: Tue, 12 Dec 2023 02:53:16 +0100 Subject: [PATCH 20/26] `genlambda` tests and related fixes --- lmo/distributions.py | 103 ++++++++++++++++++++++------ tests/test_distributions.py | 130 +++++++++++++++++++++++++++++++----- 2 files changed, 195 insertions(+), 38 deletions(-) diff --git a/lmo/distributions.py b/lmo/distributions.py index 3decfac6..a0890292 100644 --- a/lmo/distributions.py +++ b/lmo/distributions.py @@ -931,8 +931,9 @@ def _genlambda_ppf0(q: float, b: float, d: float, f: float) -> float: _genlambda_ppf = np.vectorize(_genlambda_ppf0, [float]) +@np.errstate(divide='ignore') def _genlambda_qdf(q: V, b: float, d: float, f: float) -> V: - return cast(V, (1 + f) * q**(b - 1) - (1 - f) * (1 - q)**(d - 1)) + return cast(V, (1 + f) * q**(b - 1) + (1 - f) * (1 - q)**(d - 1)) def _genlambda_cdf0( # noqa: C901 x: float, @@ -941,8 +942,8 @@ def _genlambda_cdf0( # noqa: C901 f: float, *, ptol: float = 1e-4, - xtol: float = 1e-7, - maxiter: int = 100, + xtol: float = 1e-14, + maxiter: int = 60, ) -> float: """ Compute the CDF of the GLD using bracketing search with special checks. @@ -962,31 +963,31 @@ def _genlambda_cdf0( # noqa: C901 # special cases if abs(f + 1) < ptol: - return 1 - math.exp(-x) if d == 0 else 1 - (1 - d * x)**(1 / d) + return 1 - math.exp(-x / 2) if d == 0 else 1 - (1 - d * x / 2)**(1 / d) if abs(f - 1) < ptol: - return math.exp(x) if b == 0 else (1 + b * x)**(1 / b) - if f < ptol and abs(b) < ptol and abs(d) < ptol: - return (1 + np.tanh(x)) / 2 + return math.exp(x / 2) if b == 0 else (1 + b * x / 2)**(1 / b) + if abs(f) < ptol and abs(b) < ptol and abs(d) < ptol: + # logistic + if x >= 0: + return 1 / (1 + math.exp(-x)) + return math.exp(x) / (1 + math.exp(x)) if abs(b - 1) < ptol and abs(d - 1) < ptol: - assert 0 <= x + f <= 1, (x, f) - return x + f + # uniform on [-1 - f, 1 - f] + return (x + f + 1) / 2 # bracketing search, using a similar algorithm as `scipy.special.tklmbda` - p_min, p_mid, p_max = 0.0, 0.5, 1.0 - p_low, p_high = p_min, p_max + p_low, p_mid, p_high = 0.0, 0.5, 1.0 for _ in range(maxiter): x_eval = _genlambda_ppf0(p_mid, b, d, f) if abs(x_eval - x) <= xtol: break if x_eval > x: - p_high = p_mid - p_mid = (p_mid + p_low) / 2 + p_mid, p_high = (p_mid + p_low) / 2, p_mid else: - p_low = p_mid - p_mid = (p_mid + p_high) / 2 + p_mid, p_low = (p_mid + p_high) / 2, p_mid - if (p_mid - p_low)**2 <= xtol: + if abs(p_mid - p_low) <= xtol: break return p_mid @@ -998,10 +999,42 @@ def _genlambda_cdf0( # noqa: C901 excluded={'ptol', 'xtol', 'maxiter'}, ) +def _genlambda_lmo0( + r: int, + s: float, + t: float, + b: float, + d: float, + f: float, +) -> float: + if r == 0: + return 1 + + if b <= -1 - s and d <= -1 - t: + return math.nan + + def _lmo0_partial(trim: float, theta: float) -> float: + if r == 1 and theta == 0: + return cast(float, harmonic(trim) - harmonic(s + t + 1)) + + return ( + (-1)**r * + sc.poch(r + trim, s + t - trim + 1) # type: ignore + * sc.poch(1 - theta, r - 2) # type: ignore + / sc.poch(1 + theta + trim, r + s + t - trim) # type: ignore + - (1 / theta if r == 1 else 0) + ) / r + + return ( + (1 + f) * _lmo0_partial(s, b) + + (-1)**r * (1 - f) * _lmo0_partial(t, d) + ) + +_genlambda_lmo = np.vectorize(_genlambda_lmo0, [float], excluded={1, 2}) class genlambda_gen(_rv_continuous): # noqa: N801 def _argcheck(self, b: float, d: float, f: float) -> int: - return np.isfinite(b) & np.isfinite(d) & (f >= 0) & (f <= 1) + return np.isfinite(b) & np.isfinite(d) & (f >= -1) & (f <= 1) def _shape_info(self) -> Sequence[_ShapeInfo]: ibeta = _ShapeInfo('b', False, (-np.inf, np.inf), (False, False)) @@ -1053,8 +1086,8 @@ def _ppf( return _genlambda_ppf(x, b, d, f) def _stats(self, b: float, d: float, f: float) -> tuple[ - float | None, - float | None, + float, + float, float | None, float | None, ]: @@ -1063,9 +1096,9 @@ def _stats(self, b: float, d: float, f: float) -> tuple[ return math.nan, math.nan, math.nan, math.nan a, c = 1 + f, 1 - f - b1, d1 = 1 + b + 1, 1 + d + b1, d1 = 1 + b, 1 + d - m1 = c / d1 - a / b1 + m1 = 0 if b == d and f == 0 else _genlambda_lmo0(1, 0, 0, b, d, f) if b <= -1 / 2 or d <= -1 / 2: return m1, math.nan, math.nan, math.nan @@ -1109,6 +1142,34 @@ def _stats(self, b: float, d: float, f: float) -> tuple[ def _entropy(self, b: float, d: float, f: float) -> float: return entropy_from_qdf(_genlambda_qdf, b, d, f) + def _l_moment( + self, + r: npt.NDArray[np.int64], + b: float, + d: float, + f: float, + trim: tuple[int, int] | tuple[float, float], + quad_opts: QuadOptions | None = None, + ) -> _ArrF8: + s, t = trim + + if quad_opts is not None: + # only do numerical integration when quad_opts is passed + lmbda_r = cast( + float | npt.NDArray[np.float64], + l_moment_from_ppf( + functools.partial(self._ppf, b=b, d=d, f=f), # type: ignore + r, + trim=trim, + quad_opts=quad_opts, + ), # type: ignore + ) + return np.asarray(lmbda_r) + + return np.atleast_1d( + cast(_ArrF8, _genlambda_lmo(r, s, t, b, d, f)), + ) + genlambda: RVContinuous[float, float, float] = genlambda_gen( name='genlambda', diff --git a/tests/test_distributions.py b/tests/test_distributions.py index 2e6e2038..ca58c9df 100644 --- a/tests/test_distributions.py +++ b/tests/test_distributions.py @@ -1,9 +1,17 @@ +from typing import cast + import numpy as np +from numpy.testing import assert_allclose import pytest -from lmo.distributions import wakeby +from lmo.distributions import wakeby, genlambda +from lmo.typing import RVContinuous + +from scipy.stats.distributions import tukeylambda # type: ignore +ATOL = 1e-10 +Q = np.linspace(1 / 100, 1, 99, endpoint=False) @pytest.mark.parametrize('scale', [1, .5, 2]) @pytest.mark.parametrize('loc', [0, 1, -1]) @@ -19,7 +27,7 @@ (.8, 1.2, .4), (.3, .3, .7), (3, -2, .69), - (1, -0.99, .5), + (1, -0.9, .5), ]) def test_wakeby(b: float, d: float, f: float, loc: float, scale: float): X = wakeby(b, d, f, loc, scale) @@ -28,24 +36,112 @@ def test_wakeby(b: float, d: float, f: float, loc: float, scale: float): assert X.ppf(0) == X.support()[0] assert X.ppf(1) == X.support()[1] - q = np.linspace(0, 1, 100, endpoint=False) - x = X.ppf(q) + x = X.ppf(Q) + q2 = X.cdf(x) + assert_allclose(q2, Q) + + # quad_opts={} forces numerical evaluation + l_stats_quad = X.l_stats(quad_opts={}) + l_stats_theo = X.l_stats() + assert_allclose(l_stats_theo, l_stats_quad, atol=ATOL, equal_nan=d >= 1) + + ll_stats_quad = X.l_stats(quad_opts={}, trim=(0, 1)) + ll_stats_theo = X.l_stats(trim=(0, 1)) + assert_allclose(ll_stats_theo, ll_stats_quad, atol=ATOL) + + tl_stats_quad = X.l_stats(quad_opts={}, trim=1) + tl_stats_theo = X.l_stats(trim=1) + assert_allclose(tl_stats_theo, tl_stats_quad, atol=ATOL) + + tll_stats_quad = X.l_stats(quad_opts={}, trim=(1, 2)) + tll_stats_theo = X.l_stats(trim=(1, 2)) + assert_allclose(tll_stats_theo, tll_stats_quad, atol=ATOL) + + +@pytest.mark.parametrize('lam', [0, 0.14, 1, -1]) +def test_genlambda_tukeylamba(lam: float): + X0 = cast(RVContinuous[float], tukeylambda(lam)) + X = genlambda(lam, lam, 0,) + + x0 = X0.ppf(Q) + x = X.ppf(Q) + assert x[0] >= X.support()[0] + assert x[-1] <= X.support()[1] + assert_allclose(x, x0) + + _pp = np.linspace(X0.ppf(0.05), X0.ppf(0.95), 100) + u0 = X0.cdf(_pp) + u = X.cdf(_pp) + assert_allclose(u, u0) + + # the `scipy.statstukeylambda` implementation kinda sucks,,, + with np.errstate(divide='ignore'): + du0 = X0.pdf(_pp) + + du = X.pdf(_pp) + assert_allclose(du, du0) + + s0 = X0.var() + s = X.var() + assert_allclose(s, s0, equal_nan=True) + + h0 = X0.entropy() + h = X.entropy() + assert_allclose(h, h0) + + tl_tau0 = X0.l_stats(trim=1) + tl_tau = X.l_stats(trim=1) + assert_allclose(tl_tau, tl_tau0) + +# @pytest.mark.parametrize('scale', [1, .5, 2]) +# @pytest.mark.parametrize('loc', [0, 1, -1]) +# @pytest.mark.parametrize('f', [0, .5, 1, -.5, -1]) +@pytest.mark.parametrize('scale', [1]) +@pytest.mark.parametrize('loc', [0]) +@pytest.mark.parametrize('f', [0, 1, -1]) +@pytest.mark.parametrize('d', [0, .5, 2, -0.9, -1.95]) +@pytest.mark.parametrize('b', [0, .5, 1, -0.9, -1.95]) +def test_genlambda(b: float, d: float, f: float, loc: float, scale: float): + X = genlambda(b, d, f, loc, scale) + + assert X.cdf(X.support()[0]) == 0 + assert X.ppf(0) == X.support()[0] + assert X.ppf(1) == X.support()[1] + + x = X.ppf(Q) q2 = X.cdf(x) - assert np.allclose(q2, q) + assert_allclose(q2, Q) + + # m_x1 = X.expect(lambda x: x) if min(b, d) > -1 else np.nan + # mean = X.mean() + # assert_allclose(mean, m_x1, equal_nan=True, atol=ATOL) + + # m_x2 = X.expect(lambda x: (x - m_x1)**2) if min(b, d) > -.5 else np.nan + # var = X.var() + # assert_allclose(var, m_x2, equal_nan=True) # quad_opts={} forces numerical evaluation - l_stats_numerical = X.l_stats(quad_opts={}) - l_stats_exact = X.l_stats() - assert np.allclose(l_stats_exact, l_stats_numerical, equal_nan=d >= 1) + if b > -1 and d > -1: + l_tau_quad = X.l_stats(quad_opts={}) + assert_allclose(l_tau_quad[0], X.mean(), atol=ATOL) + assert l_tau_quad[1] > 0 or np.isnan(l_tau_quad[1]) + l_tau_theo = X.l_stats() + assert_allclose(l_tau_theo, l_tau_quad, atol=ATOL) + + if b > -1 and d > -2: + ll_tau_quad = X.l_stats(quad_opts={}, trim=(0, 1)) + assert ll_tau_quad[1] > 0 or np.isnan(ll_tau_quad[1]) + ll_tau_theo = X.l_stats(trim=(0, 1)) + assert_allclose(ll_tau_theo, ll_tau_quad, atol=ATOL) - ll_stats_numerical = X.l_stats(quad_opts={}, trim=(0, 1)) - ll_stats_exact = X.l_stats(trim=(0, 1)) - assert np.allclose(ll_stats_exact, ll_stats_numerical) + if b > -2 and d > -1: + lh_tau_quad = X.l_stats(quad_opts={}, trim=(1, 0)) + assert lh_tau_quad[1] > 0 or np.isnan(lh_tau_quad[1]) + lh_tau_theo = X.l_stats(trim=(1, 0)) + assert_allclose(lh_tau_theo, lh_tau_quad, atol=ATOL) - tl_stats_numerical = X.l_stats(quad_opts={}, trim=1) - tl_stats_exact = X.l_stats(trim=1) - assert np.allclose(tl_stats_exact, tl_stats_numerical) + tl_tau_quad = X.l_stats(quad_opts={}, trim=1) + assert tl_tau_quad[1] > 0 or np.isnan(tl_tau_quad[1]) + tl_tau_theo = X.l_stats(trim=1) + assert_allclose(tl_tau_theo, tl_tau_quad, atol=1e-7) - tll_stats_numerical = X.l_stats(quad_opts={}, trim=(1, 2)) - tll_stats_exact = X.l_stats(trim=(1, 2)) - assert np.allclose(tll_stats_exact, tll_stats_numerical) From 4feb1ff5f8bce41ad9961926d269bf93026db3e1 Mon Sep 17 00:00:00 2001 From: jorenham Date: Tue, 12 Dec 2023 20:05:46 +0100 Subject: [PATCH 21/26] GLD special cases --- docs/distributions.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docs/distributions.md b/docs/distributions.md index a2f16c79..02d1da74 100644 --- a/docs/distributions.md +++ b/docs/distributions.md @@ -1294,6 +1294,41 @@ When \( \beta = \delta \) and \( \phi = 0 \), GLD is the \( \lambda \equiv \beta = \delta \), which is implemented as [`scipy.stats.tukeylambda`][scipy.stats.tukeylambda]. +/// admonition | Special cases + type: info +There are several notable special cases of the GLD: + +[GPD](#gpd) +: With \( \phi = -1 \), GLD is GPD with shape \( \alpha \equiv -\delta \) + and scale \( \sigma = 2 \). + +[Lomax](https://wikipedia.org/wiki/Lomax_distribution) +: With \( \phi = -1 \) and \( \delta < 0 \), GLD is the Lomax distribution + with shape \( \alpha = -1 / \delta \) and scale \( \sigma = -2 / \delta \). + +[Exponential](https://wikipedia.org/wiki/Logistic_distribution) +: With \( \beta = \delta = 0 \) and \( \phi = -1 \), GLD is + exponential with rate \( \lambda = \frac 1 2 \), or scale \( \sigma = 2 \). + +[Tukey-Lambda](https://wikipedia.org/wiki/Tukey_lambda_distribution) +: With \( \lambda \equiv \beta = \delta \) and \( \phi = 0 \), GLD is the + standard Tukey-lambda distribution, and \( \lambda \) its shape parameter. + +[Logistic](https://wikipedia.org/wiki/Logistic_distribution) +: With \( \beta = \delta = 0 \) and \( \phi = 0 \), GLD is + standard logistic. + +[Uniform](https://wikipedia.org/wiki/Continuous_uniform_distribution) +: With \( \beta = \delta = 1 \), GLD is uniform on + \( [-1 - \phi,\ 1 - \phi] \). + + With \( \beta = \delta = 2 \) and \( \phi = 0 \) GLD is uniform on + \( \left[-\frac 1 2,\ \frac 1 2\right] \). + + With \( \delta = 1 \) and \( \phi = -1 \), GLD is uniform on + \( [0,\ 2] \) +/// + [^GLD]: [J.S. Ramberg & B.W. Schmeiser (1974) ](https://doi.org/10.1145/360827.360840) -- An approximate method for From 422edf0f626aab114eeaa7172b0161d31fc72cee Mon Sep 17 00:00:00 2001 From: jorenham Date: Wed, 13 Dec 2023 01:01:02 +0100 Subject: [PATCH 22/26] Some PDF plots for the GLD --- docs/distributions.md | 4 + docs/gallery/genlambda.py | 63 ++ docs/gallery/genlambda.svg | 1657 ++++++++++++++++++++++++++++++++++ docs/styles/gallery.mplstyle | 14 + 4 files changed, 1738 insertions(+) create mode 100644 docs/gallery/genlambda.py create mode 100644 docs/gallery/genlambda.svg create mode 100644 docs/styles/gallery.mplstyle diff --git a/docs/distributions.md b/docs/distributions.md index 02d1da74..59cdc6ee 100644 --- a/docs/distributions.md +++ b/docs/distributions.md @@ -1242,6 +1242,10 @@ x \right. \] +Generalized Tukey-Lambda distribution PDF + Unlike GLD's central product-moments, which have no general closed-form expression, its trimmed L-moments can be compactly expressed. When \( \beta > -s - 1 \) and \( \delta > -t - 1 \), the L-moments are defined diff --git a/docs/gallery/genlambda.py b/docs/gallery/genlambda.py new file mode 100644 index 00000000..bf6029f2 --- /dev/null +++ b/docs/gallery/genlambda.py @@ -0,0 +1,63 @@ +# pyright: reportUnknownMemberType=false +from pathlib import Path + +import numpy as np +import matplotlib as mpl +import matplotlib.pyplot as plt + +from lmo.distributions import genlambda + +GALLERY_PATH = Path(__file__).resolve().parent +TEX_LABEL_TEMPLATE = r'$\beta = {},\ \delta = {}$' + +X_MIN, X_MAX = -2, 2 + +if __name__ == '__main__': + plt.style.use([ + 'dark_background', + 'seaborn-v0_8-poster', + GALLERY_PATH.parent / 'styles' / 'gallery.mplstyle', + ]) + + plt.figure(figsize=(16, 9), dpi=240) + palette = mpl.color_sequences['Paired'] + + b_denom = 2 + f = 0 + + y_max = 0 + for b_numer, linestyle in zip([1, -1], ['-', '--']): + for i, d in enumerate(range(-2, 3)): + b = b_numer / b_denom + xa, xb = genlambda.support(b, d, f) + + x = np.linspace(max(X_MIN, xa), min(X_MAX, xb), 1000) + y = genlambda.pdf(x, b, d, f) + y_max = max(y.max(), y_max) + + plt.plot( + x, + y, + linestyle, + label=TEX_LABEL_TEMPLATE.format( + f'{b_numer:+d}/{b_denom}', + f'{d:+d}', + ).replace('+', r'\quad\;'), # (\vphantom requires amsmath) + alpha=0.8, + color=palette[i] + ) + + plt.legend() + plt.ylabel('$f(X)$', rotation=0) + plt.xlabel('$X$') + plt.xlim(X_MIN, X_MAX) + plt.ylim(0, 1.01 * y_max) + plt.title(r'PDF of $X \sim \mathrm{GLD}(\beta, \delta, \phi = 0)$') + plt.tight_layout() + plt.savefig( + GALLERY_PATH / 'genlambda.svg', + transparent=True, + format='svg', + bbox_inches='tight' + ) + diff --git a/docs/gallery/genlambda.svg b/docs/gallery/genlambda.svg new file mode 100644 index 00000000..53636831 --- /dev/null +++ b/docs/gallery/genlambda.svg @@ -0,0 +1,1657 @@ + + + + + + + + 2023-12-13T00:58:58.356534 + image/svg+xml + + + Matplotlib v3.8.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/styles/gallery.mplstyle b/docs/styles/gallery.mplstyle new file mode 100644 index 00000000..6c2fc72e --- /dev/null +++ b/docs/styles/gallery.mplstyle @@ -0,0 +1,14 @@ +axes.labelcolor: "#e4e4e9d1" +axes.spines.top: False +axes.spines.right: False + +lines.linewidth: 3 + +xtick.color: "#e4e4e9d1" +ytick.color: "#e4e4e9d1" + +text.color: "#e4e4e9d1" +font.family: sans-serif +font.sans-serif: Fira Sans + +# text.usetex: True From 5db07511e0aecf0ff0445b7a6069abbd805bd3ce Mon Sep 17 00:00:00 2001 From: jorenham Date: Wed, 13 Dec 2023 02:36:20 +0100 Subject: [PATCH 23/26] css cleanup --- docs/styles/theme.css | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/docs/styles/theme.css b/docs/styles/theme.css index 1a700f17..5f84bff9 100644 --- a/docs/styles/theme.css +++ b/docs/styles/theme.css @@ -22,22 +22,22 @@ div.doc-contents:not(.first) { } a.external::after, a.autorefs-external::after { - /* https://primer.style/octicons/arrow-up-right-24 */ - mask-image: url('data:image/svg+xml,'); - -webkit-mask-image: url('data:image/svg+xml,'); - content: ' '; + /* https://primer.style/octicons/arrow-up-right-24 */ + mask-image: url('data:image/svg+xml,'); + -webkit-mask-image: url('data:image/svg+xml,'); + content: ' '; - display: inline-block; - vertical-align: middle; - position: relative; + display: inline-block; + vertical-align: middle; + position: relative; - height: 1em; - width: 1em; - background-color: var(--md-typeset-a-color); + height: 1em; + width: 1em; + background-color: var(--md-typeset-a-color); } a.external:hover::after, a.autorefs-external:hover::after { - background-color: var(--md-accent-fg-color); + background-color: var(--md-accent-fg-color); } /* Custom styles */ @@ -50,10 +50,7 @@ td, th { white-space: nowrap; } -.md-typeset__scrollwrap > .md-typeset__table > table { - overflow-y: hidden; -} +.md-typeset__scrollwrap > .md-typeset__table > table, .md-typeset div.arithmatex { overflow-y: hidden; } - From 2664222d07843bc2d4858942867b033d0dc6ed93 Mon Sep 17 00:00:00 2001 From: jorenham Date: Wed, 13 Dec 2023 02:36:36 +0100 Subject: [PATCH 24/26] explicit tables mkdocs markdown extension --- mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdocs.yml b/mkdocs.yml index c00550af..4ef50a40 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -78,6 +78,7 @@ markdown_extensions: - md_in_html - sane_lists - smarty + - tables - toc: permalink: true From 3bb0a350d7ad203c822a20d5b2d48bdb8bc81c07 Mon Sep 17 00:00:00 2001 From: jorenham Date: Wed, 13 Dec 2023 02:36:50 +0100 Subject: [PATCH 25/26] list GLD"s first 4 L-moments --- docs/distributions.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/distributions.md b/docs/distributions.md index 59cdc6ee..b9fea014 100644 --- a/docs/distributions.md +++ b/docs/distributions.md @@ -1290,6 +1290,45 @@ where \end{cases} \] +To illustrate; the first four L-moments of the GLD with +\( \beta > -1 \) and \( \delta > -1 \), are: + +\[ +\begin{align*} + \lmoment{1} + &= -(1 + \phi) \frac + {1} + {1 + \beta} + &&+ (1 - \phi) \frac + {1} + {1 + \delta} + \\ + \lmoment{2} + &= \hphantom{-}(1 + \phi) \frac + {1} + {(1 + \beta)(2 + \beta)} + &&+ (1 - \phi) \frac + {1} + {(1 + \delta)(2 + \delta)} + \\ + \lmoment{3} + &= -(1 + \phi) \frac + {1 - \beta} + {(1 + \beta)(2 + \beta)(3 + \beta)} + &&+ (1 - \phi) \frac + {(1 - \delta)} + {(1 + \delta)(2 + \delta)(3 + \delta)} + \\ + \lmoment{4} + &= \hphantom{-}(1 + \phi) \frac + {(1 - \beta)(2 - \beta)} + {(1 + \beta)(2 + \beta)(3 + \beta)(4 + \beta)} + &&+ (1 - \phi) \frac + {(1 - \delta)(2 - \delta)} + {(1 + \delta)(2 + \delta)(3 + \delta)(4 + \delta)} +\end{align*} +\] + The GLD is implemented as [`lmo.distributions.genlamda`][lmo.distributions.genlambda]. From 4b370aa71a8cae96b7dc8cfdb962e1b133390675 Mon Sep 17 00:00:00 2001 From: jorenham Date: Wed, 13 Dec 2023 04:14:40 +0100 Subject: [PATCH 26/26] Listing of first 4 GLD L-/LL-/LH-/TL-moments --- docs/distributions.md | 208 ++++++++++++++++++++++++++++++++---------- mkdocs.yml | 7 ++ 2 files changed, 167 insertions(+), 48 deletions(-) diff --git a/docs/distributions.md b/docs/distributions.md index b9fea014..5e6b81f7 100644 --- a/docs/distributions.md +++ b/docs/distributions.md @@ -1089,8 +1089,8 @@ general case. Without loss of generality, Lmo uses a 3-parameter "standardized" paremetrization, with shape parameters \( \beta,\ \delta,\ \phi \). -Wakeby distribution PDF +See [`lmo.distributions.wakeby`][lmo.distributions.wakeby] for the +implementation. Each of the following restrictions apply: @@ -1127,6 +1127,9 @@ x(F) = - \frac{1 - \phi}{\delta} (1 - (1 - F)^{-\delta}) \] +Wakeby distribution PDF + /// note | Alternative parametrization This 3-parameter Wakeby distribution is equivalent to the 5-parameter variant that is generally used, after scaling by \( \sigma \) and shifting @@ -1182,9 +1185,6 @@ L-moments can be expressed as where \( H_n \) is a [harmonic number](#def-harmonic). -See [`lmo.distributions.wakeby`][lmo.distributions.wakeby] for the -implementation. - /// admonition | Special cases type: info There are several notable special cases of the Wakeby distribution: @@ -1218,6 +1218,9 @@ paremetrization, with shape parameters \( \beta,\ \delta,\ \phi \), where \( \phi \in [-1, 1] \) replaces the more commonly used shape parameters \( \alpha \mapsto 1 + \phi \) and \( \gamma \mapsto 1 - \phi \). +The GLD is implemented as +[`lmo.distributions.genlamda`][lmo.distributions.genlambda]. + As with the Wakeby distribution, the PDF and CDF of the GLD are not analytically expressible. Instead, the GLD is defined through its PPF: @@ -1290,52 +1293,161 @@ where \end{cases} \] -To illustrate; the first four L-moments of the GLD with -\( \beta > -1 \) and \( \delta > -1 \), are: +These equations look scarier that they actually are. To see why, take a look +at the first 4 L-moment, with 4 styles of trimming: -\[ -\begin{align*} - \lmoment{1} - &= -(1 + \phi) \frac - {1} - {1 + \beta} - &&+ (1 - \phi) \frac - {1} - {1 + \delta} - \\ - \lmoment{2} - &= \hphantom{-}(1 + \phi) \frac - {1} - {(1 + \beta)(2 + \beta)} - &&+ (1 - \phi) \frac - {1} - {(1 + \delta)(2 + \delta)} - \\ - \lmoment{3} - &= -(1 + \phi) \frac - {1 - \beta} - {(1 + \beta)(2 + \beta)(3 + \beta)} - &&+ (1 - \phi) \frac - {(1 - \delta)} - {(1 + \delta)(2 + \delta)(3 + \delta)} - \\ - \lmoment{4} - &= \hphantom{-}(1 + \phi) \frac - {(1 - \beta)(2 - \beta)} - {(1 + \beta)(2 + \beta)(3 + \beta)(4 + \beta)} - &&+ (1 - \phi) \frac - {(1 - \delta)(2 - \delta)} - {(1 + \delta)(2 + \delta)(3 + \delta)(4 + \delta)} -\end{align*} -\] +=== "L-moments" + If \( \beta > -1 \) and \( \delta > -1 \): -The GLD is implemented as -[`lmo.distributions.genlamda`][lmo.distributions.genlambda]. + \[ + \begin{align*} + \lmoment{1} + &= -(1 + \phi) \frac + {1} + {1 + \beta} + &&+ (1 - \phi) \frac + {1} + {1 + \delta} + \\ + \lmoment{2} + &= \hphantom{-}(1 + \phi) \frac + {1} + {(1 + \beta)(2 + \beta)} + &&+ (1 - \phi) \frac + {1} + {(1 + \delta)(2 + \delta)} + \\ + \lmoment{3} + &= -(1 + \phi) \frac + {1 - \beta} + {(1 + \beta)(2 + \beta)(3 + \beta)} + &&+ (1 - \phi) \frac + {1 - \delta} + {(1 + \delta)(2 + \delta)(3 + \delta)} + \\ + \lmoment{4} + &= \hphantom{-}(1 + \phi) \frac + {(1 - \beta)(2 - \beta)} + {(1 + \beta)(2 + \beta)(3 + \beta)(4 + \beta)} + &&+ (1 - \phi) \frac + {(1 - \delta)(2 - \delta)} + {(1 + \delta)(2 + \delta)(3 + \delta)(4 + \delta)} + \end{align*} + \] +=== "LL-moments" + If \( \beta > -1 \) and \( \delta > -2 \): + + \[ + \begin{align*} + \tlmoment{0, 1}{1} + &= -(1 + \phi) \frac + {3 + \beta} + {(1 + \beta)(2 + \beta)} + &&+ (1 - \phi) \frac + {1} + {2 + \delta} + \\ + \frac{1}{3}\tlmoment{0, 1}{2} + &= \hphantom{-} (1 + \phi) \frac + {1} + {(1 + \beta)(2 + \beta)(3 + \beta)} + &&+ \frac{1 - \phi}{2} \frac + {1} + {(2 + \delta)(3 + \delta)} + \\ + \frac{1}{4} \tlmoment{0, 1}{3} + &= -(1 + \phi) \frac + {1 - \beta} + {(1 + \beta)(2 + \beta)(3 + \beta)(4 + \beta)} + &&+ \frac{1 - \phi}{3} \frac + {(1 - \delta)} + {(2 + \delta)(3 + \delta)(4 + \delta)} + \\ + \frac{1}{5} \tlmoment{0, 1}{4} + &= \hphantom{-} (1 + \phi) \frac + {(1 - \beta)(2 - \beta)} + {(1 + \beta)(2 + \beta)(3 + \beta)(4 + \beta)(5 + \beta)} + &&+ \frac{1 - \phi}{4} \frac + {(1 - \delta)(2 - \delta)} + {(2 + \delta)(3 + \delta)(4 + \delta)(5 + \delta)} + \end{align*} + \] +=== "LH-moments" + If \( \beta > -2 \) and \( \delta > -1 \): -When \( \beta = \delta \) and \( \phi = 0 \), GLD is the -"regular" Tukey-lambda distribution with shape -\( \lambda \equiv \beta = \delta \), which is implemented as -[`scipy.stats.tukeylambda`][scipy.stats.tukeylambda]. + \[ + \begin{align*} + \tlmoment{1, 0}{1} + &= -(1 + \phi) \frac + {1} + {(2 + \beta)} + &&+ (1 - \phi) \frac + {3 + \beta} + {(1 + \delta)(2 + \delta)} + \\ + \frac{1}{3}\tlmoment{1, 0}{2} + &= \hphantom{-} \frac{1 + \phi}{2} \frac + {1} + {(2 + \beta)(3 + \beta)} + &&+ (1 - \phi) \frac + {1} + {(1 + \delta)(2 + \delta)(3 + \delta)} + \\ + \frac{1}{4} \tlmoment{1, 0}{3} + &= -\frac{1 + \phi}{3} \frac + {1 - \beta} + {(2 + \beta)(3 + \beta)(4 + \beta)} + &&+ (1 - \phi) \frac + {(1 - \delta)} + {(1 + \delta)(2 + \delta)(3 + \delta)(4 + \delta)} + \\ + \frac{1}{5} \tlmoment{1, 0}{4} + &= \hphantom{-} \frac{1 + \phi}{4} \frac + {(1 - \beta)(2 - \beta)} + {(2 + \beta)(3 + \beta)(4 + \beta)(5 + \beta)} + &&+ (1 - \phi) \frac + {(1 - \delta)(2 - \delta)} + {(1 + \delta)(2 + \delta)(3 + \delta)(4 + \delta)(5 + \delta)} + \end{align*} + \] +=== "TL-moments" + If \( \beta > -2 \) and \( \delta > -2 \): + + \[ + \begin{align*} + \tlmoment{1}{1} + &= -(1 + \phi) \frac + {5 + \beta} + {(2 + \beta)(3 + \beta)} + &&+ (1 - \phi) \frac + {5 + \delta} + {(2 + \delta)(3 + \delta)} + \\ + \frac{2}{3 \cdot 4} \tlmoment{1}{2} + &= \hphantom{-} (1 + \phi) \frac + {1} + {(2 + \beta)(3 + \beta)(4 + \beta)} + &&+ (1 - \phi) \frac + {1} + {(2 + \delta)(3 + \delta)(4 + \delta)} + \\ + \frac{3}{4 \cdot 5} \tlmoment{1}{3} + &= -(1 + \phi) \frac + {(1 - \beta)} + {(2 + \beta)(3 + \beta)(4 + \beta)(5 + \beta)} + &&+ (1 - \phi) \frac + {(1 - \delta)} + {(2 + \delta)(3 + \delta)(4 + \delta)(5 + \delta)} + \\ + \frac{4}{5 \cdot 6} \tlmoment{1}{4} + &= \hphantom{-} (1 + \phi) \frac + {(1 - \beta)(2 - \beta)} + {(2 + \beta)(3 + \beta)(4 + \beta)(5 + \beta)(6 + \beta)} + &&+ (1 - \phi) \frac + {(1 - \delta)(2 - \delta)} + {(2 + \delta)(3 + \delta)(4 + \delta)(5 + \delta)(6 + \delta)} + \end{align*} + \] /// admonition | Special cases type: info diff --git a/mkdocs.yml b/mkdocs.yml index 4ef50a40..faae5409 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -19,6 +19,7 @@ theme: logo: img/logo.png favicon: img/favicon.ico features: + - content.tabs.link - content.tooltips - navigation.path - navigation.tabs @@ -97,6 +98,12 @@ markdown_extensions: - pymdownx.snippets: - pymdownx.striphtml - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + combine_header_slug: true + slugify: !!python/object/apply:pymdownx.slugs.slugify + kwds: + case: lower - pymdownx.tasklist: custom_checkbox: true