logit_utils_gen.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825
  1. """
  2. The file provides a class for logistic regression utilities with polynomial
  3. logit (log odds) function:
  4. p(x|pars) = 1/(1 + exp(-F(x|beta(pars))))
  5. with log odds F of polynomial form:
  6. F(x|beta) = sum_{j=0}^degree beta_j x^j
  7. with decision function (aka logit) F and parameters
  8. beta(pars) = [beta(pars)_j]_{j=0}^degree
  9. where pars are regression parameters, len(pars) = degree + 1.
  10. Coefficients beta(pars) can be constrained to be monotonic
  11. function of x by using monotonic cubic transformation:
  12. beta(pars) = mc.forward_map(pars)
  13. where mc is module mono_cubic2.
  14. NOTES: The model defines the conditional probability
  15. Prob(Y = y|x, pars) = 1/(1 + exp(-s(y) F(x| beta(pars))))
  16. where
  17. s(y) = 2*y - 1
  18. with x in R and y in {0,1}. For degree = 1 this is standard logistic regression
  19. beta(pars) = (pars[0], pars[1]).
  20. For degree = 3 and mono = True the coefficients beta(pars) are constrained to be
  21. monotonic by using monotonic cubic transformation.
  22. We have data
  23. {(x_i, y_i) : x_i in R, y_i in {0,1}, i = 0, ..., n-1 }
  24. and the model is fit to data by minimizing negative log-likelihood function:
  25. nlff = -sum_i log(Prob(Y = y_i| x_i, pars)) : neg. log likelihood
  26. with respect to parameters pars. We can have regularization term in cost function
  27. and this case we minimize cost function:
  28. cost(pars) = nllf(pars) + lambda ||pars||^2
  29. Author: Martin Horvat, January 2026
  30. """
  31. import numpy as np
  32. import scipy
  33. import scipy.optimize
  34. import mono_cubic2 as mc
  35. def resize_with_const(v, n, val=0):
  36. """
  37. Resize a 1D vector to a specified length `n`.
  38. If the input vector `v` is longer than `n`, it is truncated.
  39. If it is shorter, it is padded with the constant value `val`.
  40. If it is already of length `n`, it is returned unchanged.
  41. Parameters:
  42. v (array-like): Input 1D vector (list or NumPy array).
  43. n (int): Target length of the output vector.
  44. val (scalar, optional): Value used to pad if `v` is shorter than `n`. Default is 0.
  45. Returns:
  46. np.ndarray: Resized 1D NumPy array of length `n`.
  47. """
  48. v = np.asarray(v)
  49. if len(v) == n: return v
  50. if len(v) > n: return v[:n]
  51. return np.concatenate([v, np.full(n - len(v), val)])
  52. def safe_exp(x, max_exp = 700):
  53. """
  54. A numerically robust version of np.exp that avoids overflow by clipping the input.
  55. Parameters:
  56. x : array_like
  57. Input value or array.
  58. max_exp : float
  59. Maximum allowed exponent value. np.exp(709) ≈ 8.2e307 (close to float64 max).
  60. Returns:
  61. array_like
  62. The exponential of the input with overflow protection.
  63. """
  64. return np.exp(np.clip(x, -max_exp, max_exp))
  65. def safe_expit(x, max_exp = 700): return 1/(1 + safe_exp(-x, max_exp))
  66. """
  67. Fitting data
  68. {(x_i, y_i) : x_i in R, y_i in {0,1}, i = 0, ..., n-1 }
  69. to model function
  70. f(x|pars) = 1/(1 + exp(-F(x|beta(pars))))
  71. with log odds of polynomial form:
  72. log(f(x|pars)/(1 - f(x|pars))) = F(x| beta(pars))
  73. where F is decision function (aka logit)
  74. F(x|beta) = sum_{i=0}^degree beta_i x^i
  75. and coefficients
  76. beta(pars) = [beta_i(pars)]_{i=0}^degrees
  77. Conditional probability
  78. Prob(Y = y_i|x, pars) = 1/(1 + exp(-s(y) F(x|beta(pars)))
  79. with
  80. s(y) = 2*y -1
  81. """
  82. class LogisticPolyRegression:
  83. """
  84. Class constructor.
  85. Input:
  86. degree: int, degree of polynomial
  87. mono: boolean, default False
  88. lambda: None or tuple float, L2 regularization
  89. """
  90. def __init__(self, degree = 1, mono = False, lam = None):
  91. self.degree = degree
  92. self.mono = mono
  93. self.big = 200
  94. self.small = 1e-8
  95. self.lam = lam
  96. if mono:
  97. assert self.degree in [1, 3], f"Degree {self.degree} not supported in mono!"
  98. self.mono3 = self.mono and (self.degree == 3)
  99. """
  100. Mapping regression parameters pars to coefficients beta
  101. beta = beta(pars)
  102. used in decision function:
  103. F(x|beta) = sum_{i=0}^degree beta_i x^i
  104. Input:
  105. pars
  106. Return:
  107. beta
  108. """
  109. def get_beta(self, pars):
  110. beta = mc.forward_map(pars) if self.mono3 else pars
  111. return np.array(beta)
  112. """
  113. Mapping beta to regression parameters used in decision function.
  114. Input:
  115. beta: coefficient beta
  116. Return:
  117. pars
  118. """
  119. def get_pars(self, beta):
  120. pars = mc.backward_map(beta) if self.mono3 else beta
  121. return np.array(pars)
  122. """
  123. Calculate jacobian between beta and regression parameters
  124. J = d(beta)/d(pars)
  125. = [d(beta_i)/d(pars_a)]_{i,a}
  126. and
  127. H = [d^2 beta_i/(d(pars_a) d(pars_b))]_{i,a,b}
  128. Input:
  129. pars
  130. hess: boolean, False
  131. Return:
  132. J if hess = True
  133. (J, H) if hess = False
  134. """
  135. def get_jac_beta(self, pars, hess = False):
  136. n = len(pars)
  137. J = mc.forward_map_jacobian(pars) if self.mono3 else np.eye(n)
  138. if not hess: return J
  139. H = mc.forward_map_hessian(pars) if self.mono3 else np.zeros(shape = (n, n, n))
  140. return (J, H)
  141. """
  142. Model function
  143. f(x|pars) = 1/(1 + exp(-F(x|beta))) beta = beta(pars)
  144. Input:
  145. x: scalar value or a array of values
  146. pars: array of r = degree + 1 floats, model parameters array of floats
  147. Return:
  148. model function values
  149. """
  150. def model(self, x, pars):
  151. beta = self.get_beta(pars) # beta
  152. X = np.column_stack([x**i for i in range(len(beta))])
  153. F = X @ beta # decision function, X beta
  154. return safe_expit(F)
  155. """
  156. Calculate negative log-likelihood function
  157. nllf = -sum_i log(Prob(Y = y_i| x_i, pars)) : neg. log likelihood
  158. grad = [d(nllf)/d(pars_a)]_a : jacobian
  159. where
  160. Prob(Y = y_i| x_i, pars) = 1/(1 + exp(-s_i F(x_i| beta))) beta=beta(pars)
  161. s_i = 2*y_i - 1
  162. Input:
  163. x: array of n floats
  164. y: array of n int in {0,1}
  165. pars: array of r = degree + 1 floats, model parameters array of floats
  166. jac: boolean, default False, if jacobian is needed
  167. Return:
  168. nllf : if jac is false
  169. (nllf, grad) : if jac is true
  170. """
  171. def nllf(self, x, y, pars, jac = False):
  172. beta = self.get_beta(pars)
  173. X = np.column_stack([x**i for i in range(len(beta))])
  174. # signs
  175. s = 2.0*y - 1
  176. # decision function for conditional probability Prob(Y = y| x)
  177. F = s*(X @ beta)
  178. nllf = np.sum(np.log(1 + safe_exp(-F)))
  179. if not jac: return nllf
  180. J = self.get_jac_beta(pars)
  181. grad = -(s*safe_expit(-F)) @ (X @ J)
  182. return (nllf, grad)
  183. """
  184. Penalty function
  185. Input:
  186. pars: array of r = degree + 1 floats, model parameters array of floats
  187. jac: boolean, default False, if jacobian is needed
  188. Return:
  189. val : if jac is false
  190. (val, grad) : if jac is true
  191. """
  192. def penalty(self, pars, jac = False):
  193. val = self.lam[0]*np.sum(np.abs(pars)) + self.lam[1]*np.sum(pars**2)
  194. if jac:
  195. grad = self.lam[0]*np.sign(pars) + 2*self.lam[1]*pars
  196. return (val, grad)
  197. return val
  198. """
  199. Cost function
  200. """
  201. def cost(self, x, y, pars, jac = False):
  202. val = self.nllf(x, y, pars, jac)
  203. if self.lam is not None:
  204. pen = self.penalty(pars, jac)
  205. return (val[0] + pen[0], val[1] + pen[1]) if jac else val + pen
  206. return val
  207. """
  208. Estimate parameters.
  209. Input:
  210. x: array of n floats
  211. y: array of n int in {0,1}
  212. Return:
  213. pars0
  214. """
  215. def get_est_pars(self, x, y):
  216. L = np.log(2*len(x) + 1)
  217. if self.mono3:
  218. z = L*np.polyfit(x, 2.0*y - 1, 1)[::-1]
  219. beta = resize_with_const(z, self.degree + 1, 1e-8)
  220. else:
  221. beta = L*np.polyfit(x, 2.0*y - 1, self.degree)[::-1]
  222. return self.get_pars(beta)
  223. """
  224. Performing logistic regression with log odds of polynomial form:
  225. log(f(x|pars)/(1 - f(x|pars))) = F(x|beta) beta = beta(pars)
  226. and this gives
  227. f(x|pars) = 1/(1 + exp(-F(x|beta)))
  228. where coefficient beta = [beta_i(pars)]_{i=0}^degree, with decision
  229. function (aka logit)
  230. F(x|beta) = sum_{i=0}^degree beta_i x^i
  231. Input:
  232. x: array of n floats
  233. y: array of n int in {0, 1}
  234. Return:
  235. pars
  236. """
  237. def fit(self, x, y, pars0 = None, method = "local"):
  238. bnds = [(-self.big, self.big)]*(self.degree + 1)
  239. if method == "local":
  240. pars0 = self.get_est_pars(x, y)
  241. cf = lambda pars: self.cost(x, y, pars, jac = True)
  242. res = scipy.optimize.minimize(cf, x0 = pars0, method = 'L-BFGS-B',
  243. jac = True, bounds = bnds, tol=1e-12)
  244. elif method == "diff_evol":
  245. cf = lambda pars: self.cost(x, y, pars, jac = False)
  246. res = scipy.optimize.differential_evolution(cf, bounds = bnds,
  247. tol = 1e-8, polish = False)
  248. cf = lambda pars: self.cost(x, y, pars, jac = True)
  249. res = scipy.optimize.minimize(cf, x0 = res.x, method = 'L-BFGS-B',
  250. jac = True, bounds = bnds, tol=1e-12)
  251. elif method == "anneal":
  252. cf = lambda pars: self.cost(x, y, pars, jac = False)
  253. res = scipy.optimize.dual_annealing(cf, bounds = bnds)
  254. else:
  255. assert False, "This method is not supported."
  256. return {"pars": res.x, "cost": res.fun, "success": res.success}
  257. """
  258. Producing goodness of fit measures:
  259. LLF = log_likelihood function
  260. AIC = Akaike information criterion
  261. BIC = Bayesian information criterion
  262. Input:
  263. x: array of n floats
  264. y: array of n int in {0,1}
  265. pars: array of r = degree+1 floats, model parameters
  266. thresh: float, default 0.5, threshold value for classification
  267. Return:
  268. {"n": n,
  269. "k": r,
  270. "dof": n - r,
  271. "LLF": log_likelihood,
  272. "AIC": AIC,
  273. "BIC": BIC,
  274. "A": classification accuracy (threshold values = 0.5 prob)}
  275. Ref:
  276. https://en.wikipedia.org/wiki/Logistic_regression
  277. https://en.wikipedia.org/wiki/Akaike_information_criterion
  278. https://www.medicine.mcgill.ca/epidemiology/joseph/courses/epib-621/logfit.pdf
  279. """
  280. def goodness_of_fit(self, x, y, pars, thresh = 0.5):
  281. # model probabilities
  282. p = self.model(x, pars)
  283. # log likelihood
  284. llf = -self.nllf(x, y, pars)
  285. # information criteria
  286. k, n = len(pars), len(x)
  287. AIC = 2*k - 2*llf
  288. BIC = k*np.log(n) - 2*llf
  289. # chi2
  290. dof = n - k
  291. r = (y - p)/np.sqrt(p*(1-p) + self.small)
  292. chi2 = np.sum(r**2)
  293. p_val = scipy.stats.chi2.sf(chi2, dof)
  294. # using model as classifier
  295. matches = y == np.heaviside(p - thresh, 1)
  296. # accuracy A
  297. A = np.count_nonzero(matches)/n
  298. return {"LLF": llf,
  299. "AIC": AIC,
  300. "BIC": BIC,
  301. "A" : A,
  302. "chi2": chi2,
  303. "p-value(chi2)": p_val, # not very useful
  304. "n": n, "k": k, "dof": dof}
  305. """
  306. Calculation of asymptotic variance-covariance matrix of regression
  307. parameters pars
  308. cov_{asymp}[pars] = H^{-1}
  309. where H is hessian of nllf
  310. H = [d^2(nllf)/(d(pars)_a d(pars)_b ]_{a,b}
  311. for the logistic regression of the polynomial model:
  312. log(f(x)/(1 - f(x))) ~ sum_{i=0}^degree b_i(pars) x^i
  313. Input:
  314. x: array of n floats
  315. pars: array of r = degree+1 floats, model parameters
  316. Return:
  317. array of rxr floats; r = degree + 1
  318. Ref:
  319. * https://stats.stackexchange.com/questions/89484/how-to-compute-the-standard-errors-of-a-logistic-regressions-coefficients
  320. * https://goodboychan.github.io/machine_learning/2020/09/14/02-Regularized-likelihood-methods.html
  321. """
  322. def cov(self, x, y, pars):
  323. # coefficients
  324. beta = self.get_beta(pars)
  325. # design matrix -- add column of 1's at the beginning of your X_train matrix
  326. X = np.column_stack([x**i for i in range(len(beta))])
  327. # Jacobian J = [dbeta_i/dpars_j]_{ij}
  328. J, H = self.get_jac_beta(pars, hess = True)
  329. # signs
  330. s = 2.0*y - 1
  331. # decision function for conditional probability Prob(Y = y| x)
  332. F = s*(X @ beta)
  333. # probabilities p_i = P(Y=y_i | x_i)
  334. p = safe_expit(F)
  335. q = 1 - p
  336. # calculate hessian
  337. L = X @ J
  338. H = (L.T*(q*p))@L - np.tensordot((s*q)@X, H, axes = ([0], [0]))
  339. if self.lam is not None:
  340. Hp = H + 2*self.lam[1]*np.eye(len(pars)) # H' = H + lambda id
  341. iHp = np.linalg.inv(Hp) # inv(H')
  342. return iHp@H@iHp
  343. # covariance matrix C_params = H^-1
  344. return np.linalg.inv(H)
  345. """
  346. Calculating quantiles of the model parameters at given probabilities p
  347. for normal distribution of parameters:
  348. pars ~ N(mean_pars, cov_pars)
  349. Input:
  350. probs: array of m floats, probabilities
  351. mean_pars: array of r = degree+1 floats, mean model parameters
  352. cov_pars: array of rxr floats, variance-covariance matrix of parameters
  353. Return:
  354. array of mxr floats
  355. """
  356. def get_pars_quantiles_normal(self, probs, mean_pars, cov_pars):
  357. # mean and standard variance parameters
  358. locs = mean_pars
  359. scales = np.sqrt(np.diag(cov_pars))
  360. # computing quantiles of parameters
  361. return locs + np.outer(scipy.stats.norm.ppf(probs), scales)
  362. """
  363. Calculating quantiles of the model values
  364. f(x|pars) = 1/(1 + exp(-F(x|beta))) beta = beta(pars)
  365. with
  366. F(x|beta) = sum_{i=0}^degree x^i beta_i
  367. at given probabilities p and values x assuming
  368. normal distribution of parameters:
  369. pars ~ N(mean_pars, cov_pars)
  370. This distribution is asymptotic MLE distribution of parameters.
  371. Input:
  372. x: array of n float
  373. probs: array of m floats, probabilities
  374. mean_pars: array of r = degree+1 floats, mean model parameters
  375. cov_pars: array of rxr floats, variance-covariance matrix of parameters
  376. Return:
  377. array of mxn floats
  378. """
  379. def get_model_quantiles_normal(self, x, probs, mean_pars, cov_pars,
  380. exact = True, seed = 1977, m = 10**5):
  381. mean_beta = self.get_beta(mean_pars)
  382. X = np.column_stack([x**i for i in range(len(mean_beta))])
  383. if exact and self.mono3:
  384. # init random generator
  385. rng = np.random.default_rng(seed)
  386. pars = rng.multivariate_normal(mean_pars, cov_pars, size = m)
  387. # get betas
  388. beta = np.apply_along_axis(self.get_beta, 1, pars)
  389. # quantiles of decision function
  390. Q = np.quantile(X@beta.T, probs, axis = 1)
  391. return np.apply_along_axis(safe_expit, 1, Q)
  392. # J = d(beta)/d(pars)
  393. J = self.get_jac_beta(mean_pars)
  394. # transform data
  395. S = X@J
  396. # mean and standard variance of logit (aka log of odds)
  397. locs = X@mean_beta
  398. scales = np.sqrt(np.diag(S@cov_pars@S.T))
  399. # computing quantiles of logit
  400. Q = locs + np.outer(scipy.stats.norm.ppf(probs), scales)
  401. # convert logit to expit
  402. return safe_expit(Q)
  403. """
  404. Calculating quantiles using delta method of the model values
  405. f(x|pars) = 1/(1 + exp(-F(x|beta))) beta = beta(pars)
  406. with
  407. F(x|beta) = sum_{i=0}^degree x^i beta_i
  408. at given probabilities p and values x assuming
  409. normal distribution of parameters :
  410. pars ~ N(mean_pars, cov_pars)
  411. This distribution is asymptotic MLE distribution of parameters.
  412. We approximate exact model with linear expansion
  413. f(x|pars) = p(x|pars_mean) + dp/db(x|pars_mean) (pars - pars_mean)
  414. and the last term is normally distributed.
  415. Input:
  416. x: array of n float
  417. probs: array of m floats, probabilities
  418. mean_pars: array of r = degree+1 floats, mean model parameters
  419. cov_pars: array of rxr floats, variance-covariance matrix of parameters
  420. Return:
  421. array of mxn floats
  422. """
  423. def get_model_quantiles_delta(self, x, probs, mean_pars, cov_pars):
  424. mean_beta = self.get_beta(mean_pars)
  425. X = np.column_stack([x**i for i in range(len(mean_beta))])
  426. F = X@mean_beta
  427. # J = d(beta)/d(pars)
  428. J = self.get_jac_beta(mean_pars)
  429. # S = d(F)/d(pars)
  430. S = X@J
  431. # attributes of normal distribution of model values
  432. locs = safe_expit(F)
  433. scales = np.sqrt(np.diag(S@cov_pars@S.T))/(4*np.cosh(F/2)**2)
  434. # computing quantiles of logit
  435. Q = locs + np.outer(scipy.stats.norm.ppf(probs), scales)
  436. return np.clip(Q, a_min = 0, a_max = 1)
  437. """
  438. Generate parameters assuming normal distribution.
  439. Input:
  440. x : array of n floats
  441. y : array of n int in {0,1}
  442. m: integer, number of samples
  443. seed : int, seed for the random generator
  444. Return:
  445. array of mx(degree + 1)
  446. """
  447. def get_normal_pars(self, x, y, m, seed = 1977):
  448. res = self.fit(x, y, method = "diff_evol")
  449. assert res["success"], "Fit did not succeed."
  450. # optimal parameters
  451. pars = res["pars"]
  452. # covariance matrix of parameters
  453. cov = self.cov(x, y, pars)
  454. # init random generator
  455. rng = np.random.default_rng(seed)
  456. return rng.multivariate_normal(pars, cov, size = m)
  457. """
  458. Generate m parameters via non-parametric bootstrapping with a minimal constraint
  459. that both groups should be present in the sampled data:
  460. boostrapped sample = (xb, yb) by sampling with replacement pairs (x_i, y_i)
  461. with condition that yb can not be just 0 or just 1
  462. Input:
  463. x : array of n floats
  464. y : array of n int in {0,1}
  465. m: integer, number of samples
  466. seed : int, seed for the random generator
  467. Return:
  468. array of mx(degree + 1)
  469. """
  470. def get_nonparam_boots_pars(self, x, y, m, seed = 1977):
  471. # fitting original data
  472. res_fit = self.fit(x, y, method = "diff_evol")
  473. assert res_fit["success"]
  474. pars0 = res_fit["pars"]
  475. n = len(x)
  476. rng = np.random.default_rng(seed)
  477. # generate parameters
  478. lst = [pars0]
  479. while True:
  480. # create set indices for sampling with replacement + constraint
  481. idx = rng.choice(n, n)
  482. if np.sum(y[idx]) in [0, n]: continue
  483. # do fitting
  484. res_fit = self.fit(x[idx], y[idx], pars0, method="local")
  485. if not res_fit["success"]: continue
  486. # store pars
  487. lst.append(res_fit["pars"])
  488. if len(lst) == m: break
  489. return np.array(lst)
  490. """
  491. Generate m parameters via non-parametric stratified bootstrapping:
  492. boostrapped sample = (xb, yb)
  493. xb = (sampled with replacement from x0, sampled with replacement from x1)
  494. yb = (0...0, 1...1)
  495. Input:
  496. x : array of n floats
  497. y : array of n int in {0,1}
  498. m: integer, number of samples
  499. seed : int, seed for the random generator
  500. Return:
  501. array of mx(degree + 1)
  502. """
  503. def get_nonparam_stratified_boots_pars(self, x, y, m, seed = 1977):
  504. # pars of original data
  505. res_fit = self.fit(x, y, method = "diff_evol")
  506. assert res_fit["success"]
  507. pars0 = res_fit["pars"]
  508. # statistics about groups
  509. xs = [x[y == i] for i in range(2)]
  510. ns = [len(e) for e in xs]
  511. # common vector states
  512. yb = np.concatenate([np.full(ns[i], i) for i in range(2)])
  513. rng = np.random.default_rng(seed)
  514. # generate parameters
  515. lst = [pars0]
  516. for _ in range(m):
  517. # stratified sampling with replacement
  518. xb = np.concatenate([rng.choice(xs[i], ns[i]) for i in range(2)])
  519. # do fitting
  520. res_fit = self.fit(xb, yb, pars0, method="local")
  521. if not res_fit["success"]: continue
  522. # store pars
  523. lst.append(res_fit["pars"])
  524. if len(lst) == m: break
  525. return np.array(lst)
  526. """
  527. Generate m parameters via parametric bootstrapping:
  528. boostrapped sample = (x, yb) yb ~ B(model(x, fitted pars))
  529. where B is Bernoulli distribution
  530. Input:
  531. x : array of n floats
  532. y : array of n int in {0,1}
  533. m: integer, number of samples
  534. seed : int, seed for the random generator
  535. Return:
  536. array of mx(degree + 1)
  537. Ref:
  538. * https://www.scirp.org/journal/paperinformation?paperid=70962
  539. * https://en.wikipedia.org/wiki/Bernoulli_distribution
  540. """
  541. def get_parametric_boots_pars(self, x, y, m, seed = 1977):
  542. # first discuss original dataset
  543. res_fit = self.fit(x, y, method = "diff_evol")
  544. assert res_fit["success"]
  545. pars0 = res_fit["pars"]
  546. p = self.model(x, pars0)
  547. n = len(x)
  548. rng = np.random.default_rng(seed)
  549. # generate parameters
  550. lst = [pars0]
  551. while True:
  552. # Generate new binary outcomes from Bernoulli(p_i)
  553. y_sim = rng.binomial(n = 1, p = p)
  554. if np.sum(y_sim) in [0, n]: continue
  555. # do fitting
  556. res_fit = self.fit(x, y_sim, pars0, method="local")
  557. if not res_fit["success"]: continue
  558. # store pars
  559. lst.append(res_fit["pars"])
  560. if len(lst) == m: break
  561. return np.array(lst)