logit_gen.py 24 KB

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