causarray 0.0.1__py3-none-any.whl

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.
@@ -0,0 +1,296 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ from causarray.DR_estimation import AIPW_mean, cross_fitting
4
+ from causarray.gcate_glm import loess_fit, ls_fit
5
+ from causarray.DR_inference import fdx_control, bh_correction
6
+ from causarray.utils import reset_random_seeds, pprint, tqdm, comp_size_factor, _filter_params
7
+
8
+
9
+
10
+
11
+ def compute_causal_estimand(
12
+ estimand,
13
+ Y, W, A, W_A=None, family='nb', offset=False,
14
+ Y_hat=None, pi_hat=None, eps_var=1e-3,
15
+ fdx=False, fdx_B=1000, fdx_alpha=0.05, fdx_c=0.1,
16
+ verbose=False, random_state=0, **kwargs):
17
+ '''
18
+ Estimate the log-fold chanegs of treatment effects (LFCs) using AIPW.
19
+
20
+ Parameters
21
+ ----------
22
+ estimand : function
23
+ The causal estimand to estimate, it takes the estimated influence function values (eta_0, eta_1)
24
+ of ATE as input and returns the estimated treatment effect and the estimated influence function (tau, eta).
25
+ Y : array
26
+ n x p matrix of outcomes.
27
+ W : array
28
+ n x d matrix of covariates.
29
+ A : array
30
+ n x 1 vector of treatments.
31
+ W_A : array, optional
32
+ n x d_A matrix of covariates for treatment. If None, W is used.
33
+ family : str
34
+ The distribution of the outcome. The default is 'poisson'.
35
+ offset : array-like, optional
36
+ Offset for the model.
37
+
38
+ Y_hat : array, optional
39
+ Predicted outcomes under treatment of shape (n, p, a, 2).
40
+ pi_hat : array, optional
41
+ Predicted propensity scores of shape (n, a).
42
+
43
+ fdx : bool
44
+ Whether to use FDX control, P(FDP > c) < alpha.
45
+ fdx_B : int
46
+ Number of bootstrap samples for FDX control.
47
+ fdx_alpha : float
48
+ The significance level for FDX control.
49
+ fdx_c : float
50
+ The augmentation parameter for FDX control.
51
+
52
+ verbose : bool
53
+ Whether to print the model information.
54
+ **kwargs : dict
55
+ Additional arguments to pass to fit_glm.
56
+
57
+ Returns
58
+ -------
59
+ df_res : DataFrame
60
+ Dataframe of test results.
61
+ '''
62
+ reset_random_seeds(random_state)
63
+
64
+ kwargs = {k:v for k,v in kwargs.items() if k not in
65
+ ['kwargs_ls_1', 'kwargs_ls_2', 'kwargs_es_1', 'kwargs_es_2', 'c1', 'num_d']
66
+ }
67
+
68
+ if isinstance(Y, pd.DataFrame):
69
+ gene_names = Y.columns
70
+ Y = Y.values
71
+ else:
72
+ gene_names = range(Y.shape[1])
73
+ n, p = Y.shape
74
+
75
+ if len(A.shape) == 1:
76
+ A = A.reshape(-1,1)
77
+ if isinstance(A, pd.DataFrame):
78
+ trt_names = A.columns
79
+ A = A.values
80
+ else:
81
+ trt_names = range(A.shape[1])
82
+
83
+ if isinstance(W, pd.DataFrame):
84
+ cov_names = W.columns
85
+ W = W.values
86
+ if W_A is None:
87
+ W_A = W
88
+ elif isinstance(W_A, pd.DataFrame):
89
+ W_A = W_A.values
90
+
91
+
92
+ if verbose:
93
+ d_A = W_A.shape[1]
94
+ pprint.pprint('Estimating LFC...')
95
+ pprint.pprint({'estimands':'LFC','n':n,'p':p,'d':W.shape[1], 'd_A':d_A, 'a':A.shape[1]}, compact=True)
96
+
97
+ if offset is not None and offset is not False:
98
+ if type(offset)==bool and offset is True:
99
+ size_factors = comp_size_factor(Y, **_filter_params(comp_size_factor, kwargs))
100
+ offset = np.log(size_factors)
101
+ else:
102
+ size_factors = np.exp(offset)
103
+ else:
104
+ offset = None
105
+ size_factors = np.ones(n)
106
+
107
+ Y = Y.astype('float')
108
+ Y_hat, pi_hat = cross_fitting(Y, A, W, W_A, family=family, offset=offset,
109
+ Y_hat=Y_hat, pi_hat=pi_hat, random_state=random_state, verbose=verbose, **kwargs)
110
+ pi_hat = pi_hat.reshape(*A.shape)
111
+
112
+ # point estimation of the treatment effect
113
+ _, etas = AIPW_mean(Y, np.stack([1-A, A], axis=-1),
114
+ Y_hat, np.stack([1-pi_hat, pi_hat], axis=-1), positive=True)
115
+
116
+ # normalize the influence function values
117
+ etas /= size_factors[:,None,None,None]
118
+
119
+
120
+ i_ctrl = (np.sum(A, axis=1) == 0.)
121
+
122
+ res = []
123
+ iters = range(A.shape[1]) if A.shape[1]==1 else tqdm(range(A.shape[1]))
124
+ for j in iters:
125
+ i_case = (A[:,j] == 1.)
126
+ i_cells = i_ctrl | i_case
127
+ n_cells = np.sum(i_cells)
128
+ eta_est, tau_est, var_est = estimand(etas[i_cells,:,j], **kwargs)
129
+
130
+ std_est = np.sqrt((var_est + eps_var)/ n_cells)
131
+ tvalues_init = tau_est / std_est
132
+
133
+ # Multiple testing procedure
134
+ V = fdx_control(tau_est, var_est, tvalues_init, eta_est, fdx, fdx_B, fdx_alpha, fdx_c)
135
+
136
+ # BH correction
137
+ tvalues_init[np.isinf(var_est)] = np.nan
138
+ pvals, qvals, pvals_adj, qvals_adj = bh_correction(tvalues_init)
139
+
140
+ df_res = pd.DataFrame({
141
+ 'gene_names': gene_names,
142
+ 'tau': tau_est,
143
+ 'std': std_est,
144
+ 'stat': tvalues_init,
145
+ 'rej': V,
146
+ 'pvalue': pvals,
147
+ 'padj': qvals,
148
+ 'pvalue_emp_null_adj': pvals_adj,
149
+ 'padj_emp_null_adj': qvals_adj,
150
+ })
151
+ if A.shape[1]>1:
152
+ df_res['trt'] = trt_names[j]
153
+ res.append(df_res)
154
+ df_res = pd.concat(res, axis=0).reset_index(drop=True)
155
+ estimation = {**{'pi_hat':pi_hat, 'Y_hat':Y_hat, 'offset':offset, 'size_factors':size_factors}, **kwargs}
156
+ return df_res, estimation
157
+
158
+
159
+ def LFC(
160
+ Y, W, A, W_A=None, family='nb', offset=False,
161
+ Y_hat=None, pi_hat=None, cross_est=False,
162
+ thres_min=1e-4, thres_diff=1e-6, eps_var=1e-3,
163
+ fdx=False, fdx_alpha=0.05, fdx_c=0.1,
164
+ verbose=False, **kwargs):
165
+ '''
166
+ Estimate the log-fold chanegs of treatment effects (LFCs) using AIPW.
167
+
168
+ Parameters
169
+ ----------
170
+ Y : array
171
+ n x p matrix of outcomes.
172
+ W : array
173
+ n x d matrix of covariates.
174
+ A : array
175
+ n x 1 vector of treatments.
176
+ W_A : array, optional
177
+ n x d_A matrix of covariates for treatment. If None, W is used.
178
+ family : str
179
+ The distribution of the outcome. The default is 'poisson'.
180
+ offset : array-like, optional
181
+ Offset for the model.
182
+
183
+ Y_hat : array, optional
184
+ Predicted outcomes under treatment of shape (n, p, a, 2).
185
+ pi_hat : array, optional
186
+ Predicted propensity scores of shape (n, a).
187
+ cross_est : bool
188
+ Whether to use cross-estimation.
189
+
190
+ thres_min : float
191
+ The minimum threshold for the treatment effect.
192
+ thres_diff : float
193
+ The minimum threshold for the difference in treatment effect.
194
+
195
+ fdx : bool
196
+ Whether to use FDX control, P(FDP > c) < alpha.
197
+ fdx_alpha : float
198
+ The significance level for FDX control.
199
+ fdx_c : float
200
+ The augmentation parameter for FDX control.
201
+
202
+ verbose : bool
203
+ Whether to print the model information.
204
+ kwargs : dict
205
+ Additional arguments to pass to fit_glm.
206
+
207
+ Returns
208
+ -------
209
+ df_res : DataFrame
210
+ Dataframe of test results.
211
+ '''
212
+
213
+ def estimand(etas, **kwargs):
214
+ eta_0, eta_1 = etas[..., 0], etas[..., 1]
215
+ tau_0, tau_1 = np.mean(eta_0, axis=0), np.mean(eta_1, axis=0)
216
+
217
+ tau_1 = np.clip(tau_1, thres_diff, None)
218
+ tau_0 = np.clip(tau_0, thres_diff, None)
219
+ tau_est = np.log(tau_1/tau_0)
220
+ eta_est = eta_1 / tau_1[None,:] - eta_0 / tau_0[None,:]
221
+ var_est = np.var(eta_est, axis=0, ddof=1)
222
+
223
+ # filter out low-expressed genes
224
+ idx = (np.maximum(tau_0,tau_1)<thres_min) & ((tau_1-tau_0)<thres_diff)
225
+ tau_est[idx] = 0.; eta_est[:,idx] = 0.; var_est[idx] = np.inf
226
+
227
+ return eta_est, tau_est, var_est
228
+
229
+ return compute_causal_estimand(
230
+ estimand, Y, W, A, W_A, family, offset,
231
+ Y_hat=Y_hat, pi_hat=pi_hat,
232
+ fdx=fdx, fdx_alpha=fdx_alpha, fdx_c=fdx_c, verbose=verbose, **kwargs)
233
+
234
+
235
+
236
+
237
+
238
+ def VIM(eta_est, X, id_covs, **kwargs):
239
+ '''
240
+ Estimate the variable importance measure (VIM) using AIPW.
241
+
242
+ Parameters
243
+ ----------
244
+ eta_est : array
245
+ n x p matrix of influence function values.
246
+ '''
247
+ if len(X.shape)==1:
248
+ X = X[:,None]
249
+
250
+ n, p = eta_est.shape
251
+ d = X.shape[1]
252
+ if id_covs is None:
253
+ id_covs = range(d)
254
+ if np.isscalar(id_covs):
255
+ id_covs = range(id_covs)
256
+
257
+ n_covs = len(id_covs)
258
+
259
+ emp_VTE = (eta_est - np.mean(eta_est, axis=0, keepdims=True))**2
260
+ VTE = np.mean(emp_VTE, axis=0)
261
+ VIM_mean = np.zeros((n_covs, p))
262
+ VIM_sd = np.zeros((n_covs, p))
263
+ emp_CVTE = np.zeros((n_covs, n, p))
264
+ CVTE = np.zeros((n_covs, p))
265
+ CATE = np.zeros((n_covs, n, p))
266
+ CATE_lower = np.zeros((n_covs, n, p))
267
+ CATE_upper = np.zeros((n_covs, n, p))
268
+
269
+ for j,i in enumerate(id_covs):
270
+ print(j,i)
271
+ # regression eta_est on X to get predicted values
272
+ if np.all(np.modf(X[:,i:i+1])[0] == 0):
273
+ CATE[j], CATE_lower[j], CATE_upper[i] = ls_fit(eta_est, X[:,i], **kwargs)
274
+ else:
275
+ CATE[j], CATE_lower[j], CATE_upper[j] = loess_fit(eta_est, X[:,i], **kwargs)
276
+ # compute the variance of treatment effect
277
+ _emp_CVTE = (eta_est - CATE[j])**2
278
+ _CVTE = np.nanmean(_emp_CVTE, axis=0)
279
+ emp_CVTE[j] = _emp_CVTE
280
+ CVTE[j] = _CVTE
281
+
282
+ VIM_mean[j] = _CVTE / VTE - 1
283
+ VIM_sd[j] = np.nanstd((emp_VTE - _emp_CVTE), axis=0, ddof=1)/VTE
284
+
285
+ estimation = {
286
+ 'CATE': CATE,
287
+ 'CATE_lower': CATE_lower,
288
+ 'CATE_upper': CATE_upper,
289
+ 'emp_VTE': emp_VTE,
290
+ 'VTE': VTE,
291
+ 'emp_CVTE' : emp_CVTE,
292
+ 'CVTE' :CVTE,
293
+ 'VIM_mean' : VIM_mean,
294
+ 'VIM_sd' : VIM_sd
295
+ }
296
+ return estimation
causarray/__about__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.0.1"
causarray/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ __all__ = [
2
+ 'LFC', #'ATE', 'SATE', 'FC',
3
+ 'fit_glm', 'reset_random_seeds', 'fit_gcate'
4
+ ]
5
+
6
+
7
+ from causarray.DR_learner import LFC # ATE, SATE, FC
8
+ from causarray.gcate_glm import fit_glm
9
+ from causarray.utils import prep_causarray_data, reset_random_seeds, comp_size_factor
10
+
11
+ from causarray.gcate import *
12
+ from causarray.__about__ import __version__
13
+
14
+ __license__ = "MIT"
15
+
16
+ __author__ = "Jin-Hong Du, Maya Shen, Hansruedi Mathys, and Kathryn Roeder"
17
+ __maintainer__ = "Jin-Hong Du"
18
+ __maintainer_email__ = "jinhongd@andrew.cmu.edu"
19
+ __description__ = ("Causarray: A Python package for simultaneous causal inference"
20
+ " with an array of outcomes."
21
+ )
causarray/gcate.py ADDED
@@ -0,0 +1,237 @@
1
+ from causarray.gcate_opt import *
2
+ import pandas as pd
3
+ from causarray.utils import comp_size_factor, _filter_params
4
+
5
+
6
+ def _check_input(Y, X, family, disp_glm, disp_family, offset, c1, **kwargs):
7
+ if not (X.ndim == 2 and Y.ndim == 2):
8
+ raise ValueError("Input must have ndim of 2. Y.ndim: {}, X.ndim: {}.".format(Y.ndim, X.ndim))
9
+
10
+ if not np.allclose(Y, Y.astype(int)):
11
+ warnings.warn("Y is not integer-valued. It will be rounded to the nearest integer.")
12
+ Y = np.round(Y)
13
+
14
+ if np.sum(np.any(Y!=0., axis=0))<Y.shape[1]:
15
+ raise ValueError("Y contains non-expressed features.")
16
+
17
+ if np.linalg.svd(X, compute_uv=False)[-1] < 1e-3:
18
+ raise ValueError("The covariate matrix is near singular.")
19
+
20
+ Y = np.asarray(Y).astype(type_f)
21
+ n, p = Y.shape
22
+
23
+ kwargs_glm = {}
24
+ kwargs_glm['family'] = family
25
+
26
+ if offset is not None:
27
+ if type(offset)==bool and offset is True:
28
+ size_factor = comp_size_factor(Y, **_filter_params(comp_size_factor, kwargs))
29
+ kwargs_glm['size_factor'] = size_factor
30
+ offset = np.log(size_factor)
31
+ else:
32
+ offset = np.asarray(offset)
33
+ else:
34
+ offset = None
35
+ if kwargs_glm['family']=='nb':
36
+ if disp_family is None:
37
+ disp_family = 'poisson'
38
+ disp_glm = estimate_disp(Y, X, offset=offset, disp_family=disp_family, maxiter=1000, **kwargs)
39
+ if disp_glm is not None:
40
+ kwargs_glm['disp_glm'] = disp_glm
41
+
42
+ kwargs_glm = {**{'family':'gaussian', 'disp_glm':np.ones((1,p)), 'size_factor':np.ones((n,1))
43
+ }, **kwargs_glm}
44
+
45
+ c1 = 0.05 if c1 is None else c1
46
+ lam1 = c1 #* a #* np.sqrt(np.log(p)/n)
47
+
48
+ return Y, kwargs_glm, lam1
49
+
50
+
51
+ def fit_gcate(Y, X, A, r, family='nb', disp_glm=None, disp_family=None, offset=True,
52
+ kwargs_ls_1={}, kwargs_ls_2={}, kwargs_es_1={}, kwargs_es_2={},
53
+ c1=None, **kwargs
54
+ ):
55
+ '''
56
+ Parameters
57
+ ----------
58
+ Y : array-like, shape (n, p)
59
+ The response variable.
60
+ X : array-like, shape (n, d)
61
+ The covariate matrix.
62
+ A : array-like, shape (n, a)
63
+ The treatment matrix.
64
+ r : int
65
+ The number of unmeasured confounders.
66
+ family : str
67
+ The family of the GLM. Default is 'poisson'.
68
+ disp_glm : array-like, shape (p, ) or None
69
+ The dispersion parameter for the negative binomial distribution.
70
+ offset : array-like, shape (p, ) or None
71
+ The offset parameter.
72
+ kwargs_ls_1 : dict
73
+ Keyword arguments for the line search solver in the first phrase.
74
+ kwargs_ls_2 : dict
75
+ Keyword arguments for the line search solver in the second phrase.
76
+ kwargs_es_1 : dict
77
+ Keyword arguments for the early stopper in the first phrase.
78
+ kwargs_es_2 : dict
79
+ Keyword arguments for the early stopper in the second phrase.
80
+ c1 : float
81
+ The regularization constant in the first phrase. Default is 0.1.
82
+ kwargs : dict
83
+ Additional keyword arguments.
84
+ '''
85
+
86
+ X = np.hstack((X, A))
87
+ a = A.shape[1]
88
+ Y, kwargs_glm, lam1 = _check_input(Y, X, family, disp_glm, disp_family, offset, c1, **kwargs)
89
+
90
+ r = int(r)
91
+
92
+ res_1, res_2 = estimate(Y, X, r, a,
93
+ lam1, kwargs_glm, kwargs_ls_1, kwargs_es_1, kwargs_ls_2, kwargs_es_2, **kwargs)
94
+
95
+ return res_1, res_2
96
+
97
+
98
+ def estimate(Y, X, r, a, lam1,
99
+ kwargs_glm, kwargs_ls_1, kwargs_es_1, kwargs_ls_2, kwargs_es_2, **kwargs):
100
+ '''
101
+ Two-stage estimation of the GCATE model.
102
+
103
+ Parameters
104
+ ----------
105
+ Y : array-like, shape (n, p)
106
+ Response matrix.
107
+ X : array-like, shape (n, d+a)
108
+ Observed covariate matrix.
109
+ r : int
110
+ Number of latent variables.
111
+ a : int
112
+ The number of columns to be regularized. Assume the last 'a' columns of the covariates are the regularized coefficients. If 'a' is None, it is set to be 'd' by default.
113
+ lam1 : float
114
+ Regularization parameter for the first optimization problem.
115
+ kwargs_glm : dict
116
+ Keyword arguments for the GLM.
117
+ kwargs_ls_1 : dict
118
+ Keyword arguments of the line search algorithm for the first optimization problem.
119
+ kwargs_ls_2 : dict
120
+ Keyword arguments of the line search algorithm for the second optimization problem.
121
+ kwargs_es_1 : dict
122
+ Keyword arguments of the early stopping monitor for the first optimization problem.
123
+ kwargs_es_2 : dict
124
+ Keyword arguments of the early stopping monitor for the second optimization problem.
125
+ kwargs : dict
126
+ Additional keyword arguments.
127
+
128
+ Returns
129
+ -------
130
+ res_1 : dict
131
+ The results of the first optimization problem.
132
+ res_2 : dict
133
+ The results of the second optimization problem, including
134
+ 'X_U': the matrix (n, d+a+r) for the covariate and updated latent factors.
135
+ 'B_Gamma': the matrix (p, d+a+r) for the updated covariate and latent coefficients.
136
+ '''
137
+
138
+ p = Y.shape[1]
139
+
140
+ valid_params = _filter_params(alter_min, kwargs)
141
+
142
+ res_1 = alter_min(
143
+ Y, r, X=X, P1=True,
144
+ kwargs_glm=kwargs_glm, kwargs_ls=kwargs_ls_1, kwargs_es=kwargs_es_1, **valid_params)
145
+ Q, _ = sp.linalg.qr(res_1['B_Gamma'][:,-r:], mode='economic')
146
+ P_Gamma = np.identity(p) - Q @ Q.T
147
+
148
+ if lam1 == 0.:
149
+ res_2 = {'X_U': res_1['X_U'], 'B_Gamma': res_1['B_Gamma']}
150
+ else:
151
+ res_2 = alter_min(
152
+ Y, r, X=X, P2=P_Gamma, A=res_1['X_U'].copy(), B=res_1['B_Gamma'].copy(), lam=lam1, a=a,
153
+ kwargs_glm=res_1['kwargs_glm'], kwargs_ls=kwargs_ls_2, kwargs_es=kwargs_es_2, **valid_params)
154
+
155
+ return res_1, res_2
156
+
157
+
158
+ def estimate_r(Y, X, A, r_max, c=1.,
159
+ family='nb', disp_glm=None, disp_family='poisson', offset=True,
160
+ kwargs_ls_1={}, kwargs_ls_2={}, kwargs_es_1={}, kwargs_es_2={},
161
+ **kwargs
162
+ ):
163
+ '''
164
+ Estimate the number of latent factors for the GCATE model.
165
+
166
+ Parameters
167
+ ----------
168
+ Y : array-like, shape (n, p)
169
+ Response matrix.
170
+ X : array-like, shape (n, d)
171
+ Observed covariate matrix.
172
+ A : array-like, shape (n, a)
173
+ Treatment matrix.
174
+ r_max : int
175
+ Number of latent variables.
176
+ c : float
177
+ The constant factor for the complexity term.
178
+ family : str
179
+ The family of the GLM. Default is 'poisson'.
180
+ disp_glm : array-like, shape (1, p) or None
181
+ The dispersion parameter for the negative binomial distribution.
182
+ kwargs_glm : dict
183
+ Keyword arguments for the GLM.
184
+ kwargs_ls_1 : dict
185
+ Keyword arguments of the line search algorithm for the first optimization problem.
186
+ kwargs_ls_2 : dict
187
+ Keyword arguments of the line search algorithm for the second optimization problem.
188
+ kwargs_es_1 : dict
189
+ Keyword arguments of the early stopping monitor for the first optimization problem.
190
+ kwargs_es_2 : dict
191
+ Keyword arguments of the early stopping monitor for the second optimization problem.
192
+
193
+ Returns
194
+ -------
195
+ df_r : DataFrame
196
+ Results of the number of latent factors.
197
+ '''
198
+ X = np.hstack((X, A))
199
+ a, d = A.shape[1], X.shape[1]
200
+ n, p = Y.shape
201
+
202
+ Y, kwargs_glm, _ = _check_input(Y, X, family, disp_glm, disp_family, offset, None, **kwargs)
203
+
204
+ family, nuisance, size_factor = kwargs_glm['family'], kwargs_glm['disp_glm'], kwargs_glm['size_factor']
205
+ nuisance = nuisance.reshape(1,-1)
206
+ size_factor = size_factor.reshape(-1,1)
207
+
208
+ res = []
209
+ if np.isscalar(r_max):
210
+ r_list = np.arange(1, int(r_max)+1)
211
+ else:
212
+ r_list = np.array(r_max, dtype=int)
213
+
214
+ for r in r_list:
215
+ res_1, res_2 = estimate(Y, X, r, a,
216
+ 0, kwargs_glm, kwargs_ls_1, kwargs_es_1, kwargs_ls_2, kwargs_es_2, **kwargs)
217
+ A01, A02, A1, A2 = res_1['X_U'], res_1['B_Gamma'], res_2['X_U'], res_2['B_Gamma']
218
+
219
+ logh = log_h(Y, family, nuisance)
220
+
221
+ if r==1:
222
+ ll = 2 * (
223
+ nll(Y, A01, A02, family, nuisance, size_factor) / p
224
+ - np.sum(logh) / (n*p) )
225
+ nu = (d+a) * np.maximum(n,p) * np.log(n * p / np.maximum(n,p)) / (n*p)
226
+ jic = ll + c * nu
227
+ res.append([0, ll, nu, jic])
228
+
229
+ ll = 2 * (
230
+ nll(Y, A1, A2, family, nuisance, size_factor) / p
231
+ - np.sum(logh) / (n*p) )
232
+ nu = (d + a + r) * np.maximum(n,p) * np.log(n * p / np.maximum(n,p)) / (n*p)
233
+ jic = ll + c * nu
234
+ res.append([r, ll, nu, jic])
235
+
236
+ df_r = pd.DataFrame(res, columns=['r', 'deviance', 'nu', 'JIC'])
237
+ return df_r