Procházet zdrojové kódy

Adding the work on bayesian model

Martin Horvat před 1 rokem
rodič
revize
2bb1415e77

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 153 - 0
python/bayesian/bayesian.ipynb


+ 443 - 0
python/bayesian/bayesian.py

@@ -0,0 +1,443 @@
+import numpy as np
+import scipy
+
+"""
+    Bayesian model describing conditional probability 
+
+        Prob(X|Y = 1) 
+            = Prob(Y=1)p(X|Y=1)/(Prob(Y=0) p(X|Y=0) + Prob(Y=1) p(X|Y=1))
+            = 1 /(1 + O p(X|Y=0)/p(X|Y=1)) 
+            = 1/(1 + exp( -F))
+
+    where F is the decision function
+
+        F =  log(p(X|Y=1)) - log(p(X|Y=0)) - log(O)
+    
+    and O are the odds
+
+        O = Prob(Y=0)/Prob(Y=1)
+"""
+
+class BayesianModelRegression:
+    
+    """
+        Constructor
+
+        Input:
+            odds: float, ratio Prob(Y=0)/Prob(Y=1)
+            log_pdfs: function (x, pars, choice = 2) 
+                        match choice:
+                            case 0: return log_pdf0
+                            case 1: return log_pdf1
+                            case _: return (log_pdf0, log_pdf1)
+            bounds: tuple of bounds, (bounds0, bound1)
+
+            distr_sample: function (rng, x, pars, n, choice):
+                          generate n sampled os points using pdfs(choice, pars) 
+    """   
+    def __init__(self, odds, log_pdfs, bounds, distr_sample = None):
+        
+        self.odds = odds
+        self.log_odds = np.log(odds)
+        self.log_pdfs = log_pdfs
+        self.bounds = bounds
+        self.distr_sample = distr_sample
+
+    """
+        Calculate decision function:
+
+             decision = log(p(X|Y=1)) - log(p(X|Y=0)) - log(odds)
+
+        Input:
+            x: float or array of floats
+            pars: parameters for log_pdfs
+        Return:
+            float or array of float
+    """
+    def decision(self, x, pars):
+
+        # log of pdf for each group
+        lf0, lf1 = self.log_pdfs(x, pars)
+        
+        return lf1 - lf0 - self.log_odds
+    
+    """
+        Calculate model of the conditional probability Prob(X|Y = 1)
+        Input:
+            x: float or array of floats
+            pars: parameters for log_pdfs
+    """    
+    def model(self, x, pars):
+    
+        # decision function
+        F = self.decision(x, pars)
+
+        # calculating model
+        return 1/(1 + np.exp(-F))
+    
+    """
+        Negative Log Likelihood function:
+
+            neg. log likelihood = -sum_i log(Prob(X = x_i, Y = y_i))
+        
+        where
+
+            Prob(X, Y = 1) = 1/(1 + exp(-F))
+            Prob(X, Y = 0) = 1 -  Prob(X, Y = 1) = 1/(1 + exp(+F))
+        
+        with
+
+            log Prob(X, Y = y) = -log(1 + exp(-S(y) F))
+            S(y) =  [ +1 : y = 1
+                    [ -1 : y = 0
+
+        Input:
+            x: array of floats
+            y: array of ints in {0,1}
+            pars: array of floats, model parameters
+        
+        Return:
+            float: negative log likelihood
+
+    """
+    def nllf(self, x, y, pars):
+        
+        # signs for the groups:
+        # group 1 has + sign and group 0 has - sign
+        S = 2.0*y - 1
+
+        # decision function
+        F = self.decision(x, pars)
+        
+        return np.sum(np.log(1 + np.exp(-S*F)))
+    
+
+    """
+        MLE fitting of a distribution,  given by log_pdf, to data x associated 
+        to the group 0 or 1 by maximizing 
+
+            loglikehood_{single group} = sum_i log_pdf(x | pars)
+
+        Input:
+            x: array of floats
+            choice: int in {0,1}, selecting the group
+            method: string in ["local", "diff_evol", "anneal"]
+            seed: int, seed of the random generator
+        
+        Return:
+            {"pars": pars_MLE, "cost": NLLF at pars_MLE}
+    """
+    
+    def fit_distr(self, x, choice, method = "local", seed = 1977):
+        fname = "fit_distr"
+
+        cost = lambda pars: -np.sum(self.log_pdfs(x, pars, choice))
+        bnds = self.bounds[choice]
+
+        match method:
+            case "local":
+                # random parameters from boundaries
+                pars0 = np.random.default_rng(seed).uniform(*zip(*bnds))
+                # use local optimizer
+                res = scipy.optimize.minimize(cost, pars0, bounds = bnds, method = "L-BFGS-B")
+            case "diff_evol":
+                res = scipy.optimize.differential_evolution(cost, bounds = bnds)
+            case "annel":
+                res = scipy.optimize.dual_annealing(cost, bounds = bnds)
+            case _:
+                assert False, f"{fname}::this method does not exist"
+    
+        return {"pars": res.x, "success": res.success, "cost": res.fun} 
+
+
+    """
+        MLE fitting of the Bayesian model:
+
+            pars_MLE = argmin_pars NLLF(pars| x, y)
+
+        Input:
+            x: array of floats
+            y: array of ints in {0,1}
+            method: string in ["local", "diff_evol", "anneal"], optimizer
+        
+        Return:
+            {"pars": pars_MLE, "cost": NLLF at pars_MLE}
+    """
+    def fit(self, x, y, pars0 = None, method = "local"):
+        fname = "fit"
+
+        # defined nllf as function of parameters, data is already included
+        cost = lambda pars: self.nllf(x, y, pars)
+
+        # joint bounds of two groups
+        bnds = np.concatenate(self.bounds)
+        
+        match method:
+            case "local":
+                # estimate initial guess of parameters (for local method)
+                if pars0 is None:
+                    get_pars = lambda choice: self.fit_distr(x[y == choice], choice)["pars"]
+                    pars0 = np.r_[get_pars(0), get_pars(1)]
+          
+                # optimize using local optimizer
+                res = scipy.optimize.minimize(cost, pars0, bounds = bnds, method = "L-BFGS-B")
+            case "diff_evol":
+                res = scipy.optimize.differential_evolution(cost, bounds = bnds)
+            case "anneal":
+                res = scipy.optimize.dual_annealing(cost, bounds = bnds)
+            case _:
+                assert False, f"{fname}::this method does not exist"
+
+        return {"pars": res.x, "success": res.success, "cost": res.fun} 
+
+    """
+        Producing goodness of fit measures:
+    
+            LLF = log_likelihood function
+            AIC = Akaike information criterion
+            BIC = Bayesian information criterion
+            
+        Input:
+            x: array of floats
+            y: array of ints in {0,1}
+            pars: array of floats,  model parameters
+            thresh: float, default 0.5, threshold value for classification
+
+        Return:
+            {"n": n, "k":k, "dof":n-k, 
+            "LLF": log_likelihood, 
+            "AIC": AIC, 
+            "BIC": BIC, 
+            "A": classification accuracy (threshold values = 0.5 prob)}    
+    """
+    def goodness_of_fit(self, x, y, pars, thresh = 0.5):
+
+        # model probabilities
+        p = self.model(x, pars)
+
+        # log likelihood
+        llf = -self.nllf(x, y, pars)
+        
+        # information criteria
+        k, n = len(pars), len(x)
+        AIC = 2*k - 2*llf
+        BIC = k*np.log(n) - 2*llf
+
+        # chi2
+        dof = n - k
+        r = (y - p)/np.sqrt(p*(1-p))
+        chi2 = np.sum(r**2)
+        p_val = scipy.stats.chi2.sf(chi2, dof)
+
+        # using model as classifier
+        matches = y == np.heaviside(p - thresh, 1)
+
+        return {"LLF": llf, "AIC": AIC, "BIC": BIC, 
+                "A" : np.count_nonzero (matches)/n,
+                "chi2": chi2, "p-value(chi2)": p_val,  # not very useful
+                "n": n, "k": k, "dof": dof}
+
+    """
+        Generate m parameters via non-parametric bootstrapping with minimal constraint
+
+            bootstrapped sampled = (xb, yb) sampled with replacement from (x,y)
+
+        Input:
+            x : array of n floats
+            y : array of n int in {0,1}
+            m: integer, number of samples
+            seed : int, seed for the random generator
+            method: string in ["local", "diff_evol", "anneal"], optimizer
+        
+        Return:
+            array of m x len(pars) floats
+    """
+    def get_nonparam_boots_pars(self, x, y, m, seed = 1, method = "local"):
+        fname = "get_nonparam_boots_pars"
+
+        rng = np.random.default_rng(seed)
+
+        # discussing original data 
+        res = self.fit(x, y, method = method)
+        assert res["success"], f"{fname}::fitting original data failed."
+
+        # generate bootstrapped parameters
+        pars = res["pars"] 
+        lst = [pars]
+        n = len(x)
+        
+        while True:
+            
+            # sampling with replacement with restrictions
+            idx = rng.choice(n, n)
+            if np.sum(y[idx]) in [0, n]: continue
+            
+            res = self.fit(x[idx], y[idx], pars0 = pars, method = method)
+            if not res["success"]: continue
+
+            lst.append(res["pars"])
+            if len(lst) == m: break
+
+        return np.array(lst)
+
+
+    """
+        Generate m parameters via non-parametric stratified bootstrapping:
+
+            bootstrapped sampled = (xb, yb) sampled with replacement from (x,y) for each groups separately
+
+        meaning         
+          
+            xb = (sampled with replacement from x0, sampled with replacement from x1)
+            yb = (0 ... 0, 1 ... 1)
+                    n0        n1
+        
+        Note samples from each group in yb is constant and same as in y.
+
+        Input:
+            x : array of n floats
+            y : array of n int in {0,1}
+            m: integer, number of samples
+            seed : int, seed for the random generator
+            method: string in ["local", "diff_evol", "anneal"], optimizer
+        
+        Return:
+            array of m x len(pars) floats
+    """
+    def get_nonparam_strat_boots_pars(self, x, y, m, seed = 1, method = "local"):
+        fname = "get_nonparam_strat_boots_pars"
+
+        rng = np.random.default_rng(seed)
+
+        # discussing original data 
+        res = self.fit(x, y, method = method)
+        assert res["success"], f"{fname}::fitting original data failed."
+
+        # separate data of both groups
+        xs = [x[y == i] for i in range(2)]
+        ns = [len(e) for e in xs]
+        
+        # common vector states
+        yb = np.concatenate([np.full(ns[i], i) for i in range(2)])
+        
+        # generate bootstrapped parameters
+        pars = res["pars"]   
+        lst = [pars]
+        while True:
+            # stratified sampling with replacement
+            xb = np.concatenate([rng.choice(xs[i], ns[i]) for i in range(2)])
+        
+            # do fitting
+            res = self.fit(xb, yb, pars0 = pars, method = method)
+            if not res["success"]: continue
+
+            lst.append(res["pars"])
+            if len(lst) == m: break
+
+        return np.array(lst)
+    
+    """
+        Generate m parameters via parametric bootstrapping:
+            
+            bootstrapped sample = (x, yb)  yb ~ B(ymodel)
+        
+        Input:
+            x : array of n floats
+            y : array of n int in {0,1}
+            m: integer, number of samples
+            seed : int, seed for the random generator
+            method: 
+        
+        Return:
+            array of m x len(pars) floats
+    """
+    def get_param_boots_pars(self, x, y, m, seed = 1, method = "local"):
+        fname = "get_param_boots_pars"
+
+        rng = np.random.default_rng(seed)
+        
+        # discussing original data 
+        res = self.fit(x, y, method = method)
+        assert res["success"], f"{fname}::fitting original data failed."
+
+        # calculate predicted conditional probabilities 
+        pars = res["pars"]
+        p = self.model(x, pars)
+
+        # generate bootstrapped parameters
+        lst = [pars]
+        n = len(x)
+
+        while True:
+
+            # Generate new binary outcomes from Bernoulli(p_i)
+            y_sim = rng.binomial(n = 1, p = p)
+
+            if np.sum(y_sim) in [0, n]: continue
+            
+            # fit and get new parameter
+            res = self.fit(x, y_sim, pars0 = pars, method=method)
+            if not res["success"]: continue
+
+            lst.append(res["pars"])
+            if len(lst) == m: break
+
+        return np.array(lst)
+    
+    """
+        Generate m parameters via parametric stratified bootstrapping by 
+        sampling x from parametrized distributions associated to individual groups:
+
+            bootstrapped sample = (xb, yb)
+                xb = (sampled from distr for x0, sampled from distr for x1)
+                yb = (0 ... 0, 1 ... 1)
+                        n0        n1
+
+        Input:
+            x : array of n floats
+            y : array of n int in {0,1}
+            m: integer, number of samples
+            seed : int, seed for the random generator
+            method: 
+        
+        Return:
+            array of m x len(pars) floats
+    """
+    def get_param_strat_boots_pars(self, x, y, m, seed = 1, method = "local"):
+        fname = "get_param_strat_boots_pars"
+
+        assert self.distr_sample is not None, f"{fname}::distr_sample is not defined"
+
+        rng = np.random.default_rng(seed)
+        
+        # separate data of both groups
+        xs = [x[y == i] for i in range(2)]
+        ns = [len(e) for e in xs]
+        
+        # separate data of both groups
+        pars_g = np.concatenate([self.fit_distr(e, i)["pars"] for i, e in enumerate(xs)])
+        
+        # discussing original data 
+        res = self.fit(x, y, method = method)
+        assert res["success"], f"{fname}::fitting original data failed."
+
+        # common vector states
+        yb = np.concatenate([np.full(ns[i], i) for i in range(2)])
+
+        # generate bootstrapped parameters
+        pars = res["pars"]
+        lst = [pars]
+        
+        while True:
+
+            # Generate new sample of points for each group
+            xb = self.distr_sample(rng, pars_g, ns)
+            
+            # fit and get new parameter
+            res = self.fit(xb, yb, pars0 = pars, method = method)
+            if not res["success"]: continue
+
+            lst.append(res["pars"])
+            if len(lst) == m: break
+
+        return np.array(lst)

binární
python/bayesian/results/bayesian_CI_cmp.pdf


binární
python/bayesian/results/bayesian_fit.pdf


Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů