pyglam 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyglam-0.1.0/PKG-INFO +17 -0
- pyglam-0.1.0/pyglam/__init__.py +1 -0
- pyglam-0.1.0/pyglam/glam.py +181 -0
- pyglam-0.1.0/pyproject.toml +22 -0
pyglam-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyglam
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generalized Lambda Distribution models
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Prof. Wanderlei Malaquias Pereira Junior
|
|
7
|
+
Author-email: wanderlei_junior@ufcat.edu.br
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Requires-Dist: pandas (==3.0.1)
|
|
17
|
+
Requires-Dist: scipy (==1.17.1)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .glam import *
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import scipy as sc
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
class GlamFKML:
|
|
5
|
+
"""Generalized Lambda Distribution (GLD) - Freimer-Kollia-Mudholkar-Lin parameterization."""
|
|
6
|
+
def __init__(self, lam1: float | None = None, lam2: float | None = None, lam3: float | None = None, lam4: float | None = None) -> None:
|
|
7
|
+
"""Initialize the GLAM-FKML distribution."""
|
|
8
|
+
self.lam1 = lam1
|
|
9
|
+
self.lam2 = lam2
|
|
10
|
+
self.lam3 = lam3
|
|
11
|
+
self.lam4 = lam4
|
|
12
|
+
|
|
13
|
+
def moments(self, data: list | np.ndarray):
|
|
14
|
+
"""Compute the first four moments of a dataset.
|
|
15
|
+
|
|
16
|
+
:param data: dataset
|
|
17
|
+
|
|
18
|
+
:return: [0] = mean, [1] = variance, [2] = skewness, [3] = kurtosis
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
x = self._prepare_data(data)
|
|
22
|
+
mean = np.mean(x)
|
|
23
|
+
var = np.mean((x - mean)**2)
|
|
24
|
+
sigma = np.sqrt(var)
|
|
25
|
+
skew = np.mean(((x - mean) / sigma)**3)
|
|
26
|
+
kurt = np.mean(((x - mean) / sigma)**4)
|
|
27
|
+
|
|
28
|
+
return mean, var, skew, kurt
|
|
29
|
+
|
|
30
|
+
def _prepare_data(self, data: list | np.ndarray) -> np.ndarray:
|
|
31
|
+
"""Prepare data for analysis: convert to numpy array, ensure 1D, and remove NaN/inf values.
|
|
32
|
+
|
|
33
|
+
:param data: dataset
|
|
34
|
+
|
|
35
|
+
:return: cleaned 1D numpy array
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
x = np.asarray(data, dtype=float)
|
|
39
|
+
if x.ndim != 1:
|
|
40
|
+
x = x.ravel()
|
|
41
|
+
x = x[np.isfinite(x)]
|
|
42
|
+
|
|
43
|
+
return x
|
|
44
|
+
|
|
45
|
+
def _v1(self, lambda3: float, lambda4: float) -> float:
|
|
46
|
+
|
|
47
|
+
term1 = 1.0 / (lambda3 * (lambda3 + 1.0))
|
|
48
|
+
term2 = 1.0 / (lambda4 * (lambda4 + 1.0))
|
|
49
|
+
|
|
50
|
+
return term1 - term2
|
|
51
|
+
|
|
52
|
+
def _v2(self, lambda3: float, lambda4: float) -> float:
|
|
53
|
+
|
|
54
|
+
term1 = 1.0 / (lambda3**2 * (2.0 * lambda3 + 1.0))
|
|
55
|
+
term2 = (2.0 / (lambda3 * lambda4)) * (sc.special.gamma(lambda3 + 1.0) * sc.special.gamma(lambda4 + 1.0)) / sc.special.gamma(lambda3 + lambda4 + 2.0)
|
|
56
|
+
term3 = 1.0 / (lambda4**2 * (2.0 * lambda4 + 1.0))
|
|
57
|
+
|
|
58
|
+
return term1 - term2 + term3
|
|
59
|
+
|
|
60
|
+
def _v3(self, lambda3: float, lambda4: float) -> float:
|
|
61
|
+
|
|
62
|
+
l3 = float(lambda3)
|
|
63
|
+
l4 = float(lambda4)
|
|
64
|
+
term1 = 1.0 / (l3**3 * (3.0*l3 + 1.0))
|
|
65
|
+
term2 = (3.0 / (l3**2 * l4)) * (sc.special.gamma(2.0*l3 + 1.0) * sc.special.gamma(l4 + 1.0)) / sc.special.gamma(2.0*l3 + l4 + 2.0)
|
|
66
|
+
term3 = (3.0 / (l3 * l4**2)) * (sc.special.gamma(l3 + 1.0) * sc.special.gamma(2.0*l4 + 1.0)) / sc.special.gamma(l3 + 2.0*l4 + 2.0)
|
|
67
|
+
term4 = 1.0 / (l4**3 * (3.0*l4 + 1.0))
|
|
68
|
+
|
|
69
|
+
return term1 - term2 + term3 - term4
|
|
70
|
+
|
|
71
|
+
def _v4(self, lambda3: float, lambda4: float) -> float:
|
|
72
|
+
|
|
73
|
+
l3 = float(lambda3)
|
|
74
|
+
l4 = float(lambda4)
|
|
75
|
+
term1 = 1.0 / (l3**4 * (4.0*l3 + 1.0))
|
|
76
|
+
term2 = (4.0 / (l3**3 * l4)) * (sc.special.gamma(3.0*l3 + 1.0) * sc.special.gamma(l4 + 1.0)) / sc.special.gamma(3.0*l3 + l4 + 2.0)
|
|
77
|
+
term3 = (6.0 / (l3**2 * l4**2)) * (sc.special.gamma(2.0*l3 + 1.0) * sc.special.gamma(2.0*l4 + 1.0)) / sc.special.gamma(2.0*l3 + 2.0*l4 + 2.0)
|
|
78
|
+
term4 = (4.0 / (l3 * l4**3)) * (sc.special.gamma(l3 + 1.0) * sc.special.gamma(3.0*l4 + 1.0)) / sc.special.gamma(l3 + 3.0*l4 + 2.0)
|
|
79
|
+
term5 = 1.0 / (l4**4 * (4.0*l4 + 1.0))
|
|
80
|
+
|
|
81
|
+
return term1 - term2 + term3 - term4 + term5
|
|
82
|
+
|
|
83
|
+
def _theoretical_moments(self, lambda1: float, lambda2: float, lambda3: float, lambda4: float):
|
|
84
|
+
|
|
85
|
+
v1 = self._v1(lambda3, lambda4)
|
|
86
|
+
v2 = self._v2(lambda3, lambda4)
|
|
87
|
+
v3 = self._v3(lambda3, lambda4)
|
|
88
|
+
v4 = self._v4(lambda3, lambda4)
|
|
89
|
+
|
|
90
|
+
# mean
|
|
91
|
+
mean = lambda1 - (1.0 / lambda2) * (1.0 / (lambda3 + 1.0) - 1.0 / (lambda4 + 1.0))
|
|
92
|
+
|
|
93
|
+
# variance
|
|
94
|
+
a2 = (v2 - v1**2)
|
|
95
|
+
var = a2 / (lambda2**2)
|
|
96
|
+
|
|
97
|
+
if a2 <= 0 or var <= 0:
|
|
98
|
+
return mean, np.nan, np.nan, np.nan
|
|
99
|
+
|
|
100
|
+
# skewness
|
|
101
|
+
skew = (v3 - 3.0*v1*v2 + 2.0*(v1**3)) / (a2 ** 1.5)
|
|
102
|
+
|
|
103
|
+
# kurtosis
|
|
104
|
+
kurt = (v4 - 4.0*v1*v3 + 6.0*(v1**2)*v2 - 3.0*(v1**4)) / (a2 ** 2)
|
|
105
|
+
|
|
106
|
+
return mean, var, skew, kurt
|
|
107
|
+
|
|
108
|
+
def fit_lambdas(self, data, x0=None, method="least_squares"):
|
|
109
|
+
"""Fit the GLAM-FKML distribution parameters to the data using moment matching.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
mean_hat, var_hat, skew_hat, kurt_hat = self.moments(data)
|
|
113
|
+
target = np.array([mean_hat, var_hat, skew_hat, kurt_hat], dtype=float)
|
|
114
|
+
|
|
115
|
+
if x0 is None:
|
|
116
|
+
sd = np.sqrt(var_hat) if var_hat > 0 else 1.0
|
|
117
|
+
x0 = np.array([mean_hat, 1.0/max(sd, 1e-8), 0.5, 0.5], dtype=float)
|
|
118
|
+
else:
|
|
119
|
+
x0 = np.array(x0, dtype=float)
|
|
120
|
+
|
|
121
|
+
def residuals(x):
|
|
122
|
+
lambda1, lambda2, lambda3, lambda4 = x
|
|
123
|
+
|
|
124
|
+
# domínios mínimos para evitar divisões por zero e singularidades triviais
|
|
125
|
+
if lambda2 == 0 or lambda3 in (0.0,) or lambda4 in (0.0,):
|
|
126
|
+
return np.ones(4) * 1e6
|
|
127
|
+
|
|
128
|
+
mean, var, skew, kurt = self._theoretical_moments(lambda1, lambda2, lambda3, lambda4)
|
|
129
|
+
r = np.array([mean, var, skew, kurt], dtype=float) - target
|
|
130
|
+
|
|
131
|
+
# se algum virou nan (região inválida), penaliza
|
|
132
|
+
if not np.all(np.isfinite(r)):
|
|
133
|
+
return np.ones(4) * 1e6
|
|
134
|
+
|
|
135
|
+
# (opcional) escala: evita a variância dominar tudo
|
|
136
|
+
scale = np.array([max(abs(mean_hat), 1.0),
|
|
137
|
+
max(abs(var_hat), 1.0),
|
|
138
|
+
1.0,
|
|
139
|
+
max(abs(kurt_hat), 1.0)], dtype=float)
|
|
140
|
+
return r / scale
|
|
141
|
+
|
|
142
|
+
if method == "root":
|
|
143
|
+
sol = sc.optimize.root(residuals, x0, method="hybr")
|
|
144
|
+
return sol
|
|
145
|
+
|
|
146
|
+
# default: least_squares (mais robusto)
|
|
147
|
+
sol = sc.optimize.least_squares(residuals, x0, method="trf")
|
|
148
|
+
|
|
149
|
+
return sol
|
|
150
|
+
|
|
151
|
+
def _phi(self, u, lam):
|
|
152
|
+
return np.where(np.isclose(lam, 0.0), np.log(u), (u**lam - 1.0)/lam)
|
|
153
|
+
|
|
154
|
+
def _gld_fkml_quantile(self, u, l1, l2, l3, l4):
|
|
155
|
+
u = np.clip(u, 1e-12, 1-1e-12)
|
|
156
|
+
return l1 + ( self._phi(u, l3) - self._phi(1-u, l4) ) / l2
|
|
157
|
+
|
|
158
|
+
def _gld_fkml_qprime(self, u, l2, l3, l4):
|
|
159
|
+
u = np.clip(u, 1e-12, 1-1e-12)
|
|
160
|
+
term1 = np.where(np.isclose(l3, 0.0), 1.0/u, u**(l3 - 1.0))
|
|
161
|
+
term2 = np.where(np.isclose(l4, 0.0), 1.0/(1.0 - u), (1.0 - u)**(l4 - 1.0))
|
|
162
|
+
return (term1 + term2) / l2
|
|
163
|
+
|
|
164
|
+
def rvs(self, size: int = 1000, quantile_trim: float = 1e-3, qp_min: float =1e-8, lam1: float = None, lam2: float = None, lam3: float = None, lam4: float = None) -> np.ndarray:
|
|
165
|
+
"""Generate samples from the GLAM-Freimer-Kollia-Mudholkar-Lin distribution.
|
|
166
|
+
|
|
167
|
+
:param size: number of samples to generate
|
|
168
|
+
:param quantile_trim: trim quantiles to avoid extreme tails
|
|
169
|
+
:param qp_min: minimum quantile derivative to avoid numerical issues
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
a = float(quantile_trim)
|
|
173
|
+
u = np.linspace(a, 1.0 - a, int(size))
|
|
174
|
+
x = self._gld_fkml_quantile(u, self.lam1, self.lam2, self.lam3, self.lam4)
|
|
175
|
+
qp = self._gld_fkml_qprime(u, self.lam2, self.lam3, self.lam4)
|
|
176
|
+
mask = np.isfinite(x) & np.isfinite(qp) & (qp > qp_min)
|
|
177
|
+
x, f = x[mask], (1.0/qp[mask])
|
|
178
|
+
order = np.argsort(x)
|
|
179
|
+
x, f = x[order], f[order]
|
|
180
|
+
|
|
181
|
+
return x
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pyglam"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Generalized Lambda Distribution models"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Prof. Wanderlei Malaquias Pereira Junior", email = "wanderlei_junior@ufcat.edu.br"},
|
|
7
|
+
{name = "Prof. Marcos Luiz Henrique", email = "marcos.henrique@ufpe.br"},
|
|
8
|
+
{name = "Prof. Ketson Roberto Maximiano dos Santos", email = "dossantk@umn.edu"},
|
|
9
|
+
{name = "Victor Hugo Moreira do Couto Souza", email = "victor.souza@discente.ufcat.edu.br"},
|
|
10
|
+
{name = "Renata Maria Pensin", email = "renata.pensin@discente.ufcat.edu.br"}
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
license = {text = "MIT"}
|
|
14
|
+
requires-python = ">=3.10"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"scipy==1.17.1",
|
|
17
|
+
"pandas==3.0.1"
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
22
|
+
build-backend = "poetry.core.masonry.api"
|