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 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"