11from __future__ import annotations
22from collections .abc import Callable
3- from dataclasses import dataclass
4- import math
5- from typing import Any , cast
3+ from typing import Any , cast , TYPE_CHECKING
64from scipy .integrate import nquad , quad
75import numpy as np
86
97from pysatl_core .distributions .fitters import _ppf_brentq_from_cdf
10- from pysatl_core .families .parametric_family import (
11- ParametricFamily ,
12- )
8+ from pysatl_core .families .parametric_family import ParametricFamily
139from pysatl_core .families .parametrizations import Parametrization , parametrization
1410from pysatl_core .types import (
1511 DistributionType ,
1915 SamplingStrategy ,
2016)
2117
18+ if TYPE_CHECKING :
19+ from pysatl_core .distributions .support import Support
20+
21+ type ParametrizedFunction = Callable [[Parametrization , Any ], Any ]
22+ type SupportArg = Callable [[Parametrization ], Support | None ] | None
23+
24+
2225PDF = "pdf"
2326CDF = "cdf"
2427PPF = "ppf"
2932KURT = "kurtosis"
3033
3134
32- class ExponentialClassParametrization (Parametrization ):
35+ class ExponentialFamilyParametrization (Parametrization ):
3336 """
3437 Standard parametrization of Exponential Family.
3538 """
@@ -46,45 +49,56 @@ def __str__(self):
4649 return f"alpha={ self .alpha } , beta={ self .beta } "
4750
4851
49- def accepts (x , support ):
52+ def doesAccept (x , support ):
5053 if not hasattr (x , "__len__" ):
5154 x = [x ]
5255
5356 def accept_1D (x , borders ):
5457 left , right = borders
58+ if abs (x ) == 0 and (abs (left ) == 0 or abs (right ) == 0 ):
59+ return False
5560 return left <= x <= right
5661
5762 return all (accept_1D (x_i , border ) for x_i , border in zip (x , support ))
5863
5964
60- class ExponentialFamily (ParametricFamily ):
65+ class SpacePredicate :
66+ def __init__ (self , predicate : Callable [[Any ], bool ]):
67+ self ._predicate = predicate
68+
69+ def accepts (self , x : Any ) -> bool :
70+ return self ._predicate (x )
71+
72+
73+ class SpacePredicateArray (SpacePredicate ):
74+ def __init__ (self , space : list [tuple [float , float ]]):
75+ SpacePredicate .__init__ (self , lambda x : doesAccept (x , space ))
76+ self ._space = space
77+
78+
79+ class NaturalExponentialFamily (ParametricFamily ):
6180 def __init__ (
6281 self ,
6382 * ,
64- A : Callable [[ExponentialClassParametrization ], float ],
65- T : Callable [[Any ], Any ],
66- h : Callable [[Any ], float ],
67- eta : Callable [[Any ], Any ],
68- support : list [tuple [float , float ]],
69- param_space : list [tuple [float , float ]],
70- natural_param_space : list [tuple [float , float ]],
71- name : str = "ExponentialFamily" ,
72- theta_from_eta : Callable [[Any ], Any ] = None ,
83+ log_partition : Callable [[ExponentialFamilyParametrization ], float ],
84+ sufficient_statistics : Callable [[Any ], Any ],
85+ normalization_constant : Callable [[Any ], Any ],
86+ support : SpacePredicate ,
87+ parameter_space : SpacePredicate ,
88+ sufficient_statistics_values : SpacePredicate ,
89+ name : str = "NaturalExponentialFamily" ,
7390 distr_type : DistributionType | Callable [[Parametrization ], DistributionType ],
7491 distr_parametrizations : list [ParametrizationName ],
7592 sampling_strategy : SamplingStrategy ,
7693 support_by_parametrization : SupportArg = None ,
7794 ):
95+ self ._sufficient = sufficient_statistics
96+ self ._log_partition = log_partition
97+ self ._normalization = normalization_constant
7898
79- self ._A = A
80- self ._T = T
81- self ._h = h
82-
83- self ._eta = eta if eta is not None else (lambda th : th )
84- self ._theta_from_eta = theta_from_eta
85- self ._natural_param_space = natural_param_space
86- self ._param_space = param_space
8799 self ._support = support
100+ self ._parameter_space = parameter_space
101+ self ._sufficient_statistics_values = sufficient_statistics_values
88102
89103 distr_characteristics = {
90104 PDF : self .density ,
@@ -101,23 +115,25 @@ def __init__(
101115 sampling_strategy = sampling_strategy ,
102116 support_by_parametrization = support_by_parametrization ,
103117 )
104- parametrization (family = self , name = "theta" )((ExponentialClassParametrization ))
118+ parametrization (family = self , name = "theta" )((ExponentialFamilyParametrization ))
105119
106120 @property
107121 def log_density (self ) -> ParametrizedFunction :
108122 def log_density_func (
109- parametrization : ExponentialClassParametrization , x : Any
123+ parametrization : ExponentialFamilyParametrization , x : Any
110124 ) -> Any :
111- if not accepts ( x , self ._support ):
125+ if not self ._support . accepts ( x ):
112126 return float ("-inf" )
113127
114- params = cast (ExponentialClassParametrization , parametrization )
128+ params = cast (ExponentialFamilyParametrization , parametrization )
115129 theta = params .parameters .get ("theta" )
116- eta = self ._eta (theta )
117- sufficient = self ._T (x )
118- dot = np .dot (eta , sufficient )
119-
120- result = float (np .log (self ._h (x )) + dot + self ._A (parametrization ))
130+ sufficient = self ._sufficient (x )
131+ dot = np .dot (theta , sufficient )
132+ result = float (
133+ np .log (self ._normalization (x ))
134+ + dot
135+ + self ._log_partition (parametrization )
136+ )
121137 return result
122138
123139 return log_density_func
@@ -128,41 +144,59 @@ def density(self) -> ParametrizedFunction:
128144
129145 @property
130146 def conjugate_prior_family (self ):
131- def conjugate_sufficient (eta : Any ):
132- theta = [self ._theta_from_eta (eta )]
133- if not accepts (theta , self ._param_space ):
147+ def conjugate_sufficient (theta : Any ):
148+ if not self ._parameter_space .accepts (theta ):
134149 return [float ("-inf" ), float ("-inf" )]
135150
136- return [eta , self ._A (ExponentialClassParametrization (theta = theta ))]
151+ return [
152+ theta ,
153+ self ._log_partition (ExponentialFamilyParametrization (theta = [theta ])),
154+ ]
137155
138- def conjugate_log_partition (parametrization : ExponentialClassParametrization ):
156+ def conjugate_log_partition (parametrization : ExponentialFamilyParametrization ):
139157 alpha = parametrization .theta [0 ]
140158 beta = parametrization .theta [1 ]
141159
142- def pdf (eta : Any ):
143- theta = self ._theta_from_eta (eta )
160+ def pdf (theta : Any ):
144161 if not hasattr (theta , "__len__" ):
145162 theta = [theta ]
146- parametrization = ExponentialClassParametrization (
163+ parametrization = ExponentialFamilyParametrization (
147164 theta = theta ,
148165 )
149- return np .exp (np .dot (eta , alpha ) + beta * self ._A (parametrization ))
166+ return np .exp (
167+ np .dot (theta , alpha ) + beta * self ._log_partition (parametrization )
168+ )[0 ]
150169
151- all_value = nquad (pdf , self ._natural_param_space )[0 ]
170+ all_value = nquad (
171+ lambda x : pdf (x ) if self ._parameter_space .accepts (x ) else 0 ,
172+ [(float ("-inf" ), float ("+inf" ))],
173+ )[0 ]
152174 return - np .log (all_value )
153175
154- if self ._theta_from_eta is None :
155- raise RuntimeError ("Theta from eta wasn't specified" )
156-
157- return ExponentialFamily (
158- A = conjugate_log_partition ,
159- T = conjugate_sufficient ,
160- h = lambda _ : 1 ,
161- eta = lambda x : x ,
162- theta_from_eta = lambda eta : eta ,
163- support = self ._natural_param_space ,
164- natural_param_space = [(float ("-inf" ), float ("inf" ))] * 2 ,
165- param_space = [(float ("-inf" ), float ("inf" ))] * 2 ,
176+ # TODO: remove hardcoding - Done, all hardcoding is only on user's hands
177+ # 1. pr with prototype/draft - in progress
178+ # 2. write instruction about to add distributions as member of exponential family - not started
179+ # 3. parametrization's spaces (передавать в конструктор) - maybe impossible, discuss this with desiment on meeting
180+
181+ def conjugate_sufficient_accepts (
182+ parametrization : ExponentialFamilyParametrization ,
183+ ):
184+ parametrization = cast (parametrization , ExponentialFamilyParametrization )
185+ theta = parametrization .parameters .get ("theta" )
186+ xi = theta [:- 1 ]
187+ nu = theta [- 1 ]
188+
189+ return self ._sufficient_statistics_values (xi ) and SpacePredicateArray (
190+ [(0 , float ("+inf" ))]
191+ ).accepts (nu )
192+
193+ return NaturalExponentialFamily (
194+ log_partition = conjugate_log_partition ,
195+ sufficient_statistics = conjugate_sufficient ,
196+ normalization_constant = lambda _ : 1 ,
197+ support = self ._parameter_space ,
198+ sufficient_statistics_values = self ._parameter_space , # TODO: write convex hull for this
199+ parameter_space = SpacePredicate (conjugate_sufficient_accepts ),
166200 sampling_strategy = self .sampling_strategy ,
167201 distr_type = self ._distr_type ,
168202 distr_parametrizations = self .parametrization_names ,
@@ -172,13 +206,15 @@ def pdf(eta: Any):
172206 @property
173207 def _mean (self ) -> ParametrizedFunction :
174208 def mean_func (parametrization : Parametrization , x : Any ) -> Any :
209+ dimension_size = 1
175210 if hasattr (x , "__len__" ):
176211 dimension_size = len (x )
177- else :
178- dimension_size = 1
179- print (dimension_size )
180212 return nquad (
181- lambda x : np .dot (x , self .density (parametrization , x )),
213+ lambda x : (
214+ np .dot (x , self .density (parametrization , x ))
215+ if self ._support .accepts (x )
216+ else 0
217+ ),
182218 [(float ("-inf" ), float ("inf" ))] * dimension_size ,
183219 )[0 ]
184220
@@ -187,12 +223,15 @@ def mean_func(parametrization: Parametrization, x: Any) -> Any:
187223 @property
188224 def _second_moment (self ) -> ParametrizedFunction :
189225 def func (parametrization : Parametrization , x : Any ) -> Any :
226+ dimension_size = 1
190227 if hasattr (x , "__len__" ):
191228 dimension_size = len (x )
192- else :
193- dimension_size = 1
194229 return nquad (
195- lambda x : x ** 2 * self .density (parametrization , x ),
230+ lambda x : (
231+ x ** 2 * self .density (parametrization , x )
232+ if self ._support .accepts (x )
233+ else 0
234+ ),
196235 [(float ("-inf" ), float ("inf" ))] * dimension_size ,
197236 )[0 ]
198237
@@ -217,12 +256,62 @@ def posterior_hyperparameters(
217256 alpha_post = None
218257 beta_post = None
219258 if hasattr (sample , "__iter__" ) and not isinstance (sample , str ):
220- alpha_post = np .sum ([self ._T (x ) for x in sample ], axis = 0 )
259+ alpha_post = np .sum ([self ._sufficient (x ) for x in sample ], axis = 0 )
221260 beta_post = len (sample )
222261 else :
223- alpha_post = self .T (sample )
262+ alpha_post = self ._sufficient (sample )
224263 beta_post = 1
225264
226265 return ExponentialConjugateHyperparameters (
227266 alpha = alpha + alpha_post , beta = beta + beta_post
228267 )
268+
269+
270+ class ExponentialFamily (NaturalExponentialFamily ):
271+ def __init__ (
272+ self ,
273+ * ,
274+ log_partition : Callable [[ExponentialFamilyParametrization ], float ],
275+ sufficient_statistics : Callable [[Any ], Any ],
276+ normalization_constant : Callable [[Any ], Any ],
277+ parameter_from_natural_parameter : Callable [[Any ], Any ],
278+ support : SpacePredicate ,
279+ parameter_space : SpacePredicate ,
280+ sufficient_statistics_values : SpacePredicate ,
281+ distr_type : DistributionType | Callable [[Parametrization ], DistributionType ],
282+ distr_parametrizations : list [ParametrizationName ],
283+ sampling_strategy : SamplingStrategy ,
284+ name : str = "ExponentialFamily" ,
285+ support_by_parametrization : SupportArg = None ,
286+ ):
287+ def natural_log_partition (eta_parametrizaion : ExponentialFamilyParametrization ):
288+ eta_parametrizaion = cast (
289+ ExponentialFamilyParametrization , eta_parametrizaion
290+ )
291+ eta = eta_parametrizaion .parameters .get ("theta" )
292+ theta = parameter_from_natural_parameter (eta )
293+ return log_partition (ExponentialFamilyParametrization (theta = [theta ]))
294+
295+ natural_sufficient_statistics_values = SpacePredicate (
296+ lambda eta : sufficient_statistics_values .accepts (
297+ parameter_from_natural_parameter (eta )
298+ )
299+ )
300+ natural_parameter_space = SpacePredicate (
301+ lambda eta : parameter_space .accepts (parameter_from_natural_parameter (eta )),
302+ )
303+
304+ NaturalExponentialFamily .__init__ (
305+ self ,
306+ log_partition = natural_log_partition ,
307+ sufficient_statistics = sufficient_statistics ,
308+ normalization_constant = normalization_constant ,
309+ support = support ,
310+ parameter_space = natural_parameter_space ,
311+ sufficient_statistics_values = natural_sufficient_statistics_values ,
312+ name = name ,
313+ distr_parametrizations = distr_parametrizations ,
314+ distr_type = distr_type ,
315+ sampling_strategy = sampling_strategy ,
316+ support_by_parametrization = support_by_parametrization ,
317+ )
0 commit comments