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.
causarray/gcate_glm.py ADDED
@@ -0,0 +1,256 @@
1
+ from causarray.utils import *
2
+ from causarray.utils import _filter_params
3
+ import numpy as np
4
+ import statsmodels as stats
5
+ import statsmodels.api as sm
6
+ from sklearn.linear_model import LinearRegression
7
+ from joblib import Parallel, delayed
8
+ from tqdm import tqdm
9
+
10
+ warnings.filterwarnings('ignore')
11
+
12
+
13
+ def init_inv_link(Y, family, disp):
14
+ if family=='gaussian':
15
+ val = Y/disp
16
+ elif family=='poisson':
17
+ val = np.log1p(Y)
18
+ elif family=='nb':
19
+ val = np.log1p(Y)
20
+ elif family=='binomial':
21
+ eps = (np.mean(Y, axis=0) + np.mean(Y, axis=1)) / 2
22
+ val = np.log((Y + eps)/(disp - Y + eps))
23
+ else:
24
+ raise ValueError('Family not recognized')
25
+ return val
26
+
27
+
28
+
29
+ def fit_glm(Y, X, A=None, family='gaussian', disp_family='poisson',
30
+ disp_glm=None, impute=False, offset=None, shrinkage=False,
31
+ alpha=1e-4, maxiter=1000, thres_disp=100., n_jobs=-3, random_state=0, verbose=False, **kwargs):
32
+ '''
33
+ Fit GLM to each column of Y, with covariate X and treatment A.
34
+
35
+ Parameters
36
+ ----------
37
+ Y : array
38
+ n x p matrix of outcomes
39
+ X : array
40
+ n x d matrix of covariates
41
+ A : array
42
+ n x 1 vector of treatments or None
43
+ family : str
44
+ family of GLM to fit, can be one of: 'gaussian', 'poisson', 'nb'
45
+ disp_glm : array or None
46
+ dispersion parameter for negative binomial GLM
47
+ return_df : bool
48
+ whether to return results as DataFrame
49
+ impute : bool
50
+ whether to impute potential outcomes and get predicted values
51
+ offset : bool
52
+ whether to use log of sum of Y as offset
53
+ '''
54
+ np.random.seed(random_state)
55
+
56
+ if family not in ['gaussian', 'poisson', 'nb']:
57
+ raise ValueError('Family not recognized')
58
+
59
+ d = X.shape[1]
60
+
61
+ if A is None:
62
+ a = 1 # dummy treatment
63
+ assert impute is False
64
+ else:
65
+ if A.ndim==1:
66
+ A = A[:,None]
67
+ if impute is not False and isinstance(impute, np.ndarray):
68
+ X_test = impute
69
+ else:
70
+ X_test = X
71
+ X_test = np.c_[X,np.zeros_like(A)]
72
+ X = np.c_[X,A]
73
+ # X_test = X.copy()
74
+
75
+ a = A.shape[1]
76
+
77
+ if offset is not None and offset is not False:
78
+ if type(offset)==bool and offset is True:
79
+ offsets = np.log(comp_size_factor(Y, **_filter_params(comp_size_factor, kwargs)))
80
+ else:
81
+ offsets = offset
82
+ else:
83
+ offsets = None
84
+
85
+ # estimate dispersion parameter for negative binomial GLM if not provided
86
+ if family=='nb' and disp_glm is None:
87
+ disp_glm = estimate_disp(Y, X, offset=offsets, disp_family=disp_family, maxiter=1000, verbose=verbose, **kwargs)
88
+
89
+ alpha = np.full(X.shape[1], alpha)
90
+ pprint.pprint('Fitting {} GLM{}...'.format(family, '' if offsets is None else ' with offset'))
91
+ is_constant = np.all(X == X[0, :], axis=0)
92
+ alpha[is_constant] = 0
93
+ # alpha[:-a] = 0
94
+
95
+ families = {
96
+ 'gaussian': lambda disp: sm.families.Gaussian(),
97
+ 'poisson': lambda disp: sm.families.Poisson(),
98
+ 'nb': lambda disp: sm.families.NegativeBinomial(alpha=1/disp)
99
+ }
100
+
101
+ def fit_model(j, Y, X, offsets, family, disp, impute, alpha):
102
+ if family=='nb' and disp[j]>thres_disp:
103
+ family = 'poisson'
104
+ try:
105
+ with warnings.catch_warnings():
106
+ warnings.filterwarnings("ignore")
107
+ glm_family = families.get(family, lambda: ValueError('family must be one of: "gaussian", "poisson", "nb"'))(disp_glm[j] if family == 'nb' else None)
108
+
109
+ try:
110
+ if shrinkage:
111
+ raise ValueError('fit regularized GLM')
112
+ mod = sm.GLM(Y[:,j], X, family=glm_family, offset=offsets).fit(maxiter=maxiter)
113
+ if not np.all(np.isfinite(mod.params)) or np.any(np.abs(mod.params[:d])>50) or np.any(np.abs(mod.params[d:])>10):
114
+ raise ValueError('GLM did not converge')
115
+ resid_deviance = mod.resid_deviance
116
+ except:
117
+ mod = sm.GLM(Y[:,j], X, family=glm_family, offset=offsets).fit_regularized(alpha=alpha, cnvrg_tol=1e-5)
118
+ resid_deviance = np.full(Y.shape[0], 0.)
119
+
120
+ B = mod.params
121
+
122
+ Yhat_0 = np.zeros((Y.shape[0], a))
123
+ Yhat_1 = np.zeros((Y.shape[0], a))
124
+ if impute is not False:
125
+ for j in range(a):
126
+ X_test_copy = X_test.copy()
127
+ Yhat_0[:,j] = mod.predict(X_test_copy, offset=offsets)
128
+ X_test_copy[:, d+j] = 1 # Update the j-th column with all ones
129
+ Yhat_1[:,j] = mod.predict(X_test_copy, offset=offsets)
130
+ else:
131
+ Yhat_0[:,:] = Yhat_1[:,:] = mod.predict(X, offset=offsets).reshape(-1, a)
132
+
133
+ except:
134
+ pprint.pprint('Fitting GLM for column {} does not converge.'.format(j))
135
+ B = np.full(X.shape[1], 0.)
136
+
137
+ Yhat_0 = np.full((Y.shape[0], a), 0)
138
+ Yhat_1 = np.full((Y.shape[0], a), 0)
139
+ if impute is not False:
140
+ for k in range(a):
141
+ Yhat_0[:, k] = np.mean(Y[A[:, k] == 1, j])
142
+ Yhat_1[:, k] = np.mean(Y[A[:, k] == 0, j])
143
+ resid_deviance = np.full(Y.shape[0], 0.)
144
+ return B, Yhat_0, Yhat_1, resid_deviance
145
+
146
+
147
+ results = Parallel(n_jobs=n_jobs)(delayed(fit_model)(
148
+ j, Y, X, offsets, family, disp_glm, impute, alpha) for j in tqdm(range(Y.shape[1])))
149
+
150
+ B, Yhat_0, Yhat_1, resid_deviance = zip(*results)
151
+ B = np.array(B)
152
+ Yhat_0 = np.array(Yhat_0).transpose(1, 0, 2)
153
+ Yhat_1 = np.array(Yhat_1).transpose(1, 0, 2)
154
+ resid_deviance = np.array(resid_deviance).T
155
+
156
+ if impute is not False:
157
+ Yhat = (Yhat_0, Yhat_1)
158
+ else:
159
+ Yhat = np.array(Yhat_0)[:,:,0]
160
+
161
+ return B, Yhat, disp_glm, offsets, resid_deviance
162
+
163
+
164
+ def estimate_disp(Y, X=None, A=None, Y_hat=None, disp_family='gaussian', offset=None, verbose=False, **kwargs):
165
+ if offset is not None:
166
+ if type(offset)==bool and offset is True:
167
+ offsets = np.log(comp_size_factor(Y, **_filter_params(comp_size_factor, kwargs)))
168
+ else:
169
+ offsets = offset
170
+ sf = np.exp(offsets)[:,None]
171
+ else:
172
+ offsets = None
173
+ sf = 1.
174
+
175
+
176
+ if Y_hat is None:
177
+ if verbose:
178
+ pprint.pprint('Estimating dispersion parameter...')
179
+
180
+ if A is not None:
181
+ X = np.c_[X,A]
182
+
183
+ if disp_family=='gaussian':
184
+ Y_norm = Y/sf
185
+ reg = LinearRegression(fit_intercept=False).fit(X, Y_norm)
186
+ Y_hat = reg.predict(X)
187
+ # Y_hat = np.clip(Y_hat, 0, 1) * sf
188
+ elif disp_family=='poisson':
189
+ Y_hat = fit_glm(Y, X, None, offset=offsets, family='poisson', impute=False, **kwargs)[1]
190
+ Y_hat /= sf
191
+
192
+ # Clip Y_hat based on the range of Y per column
193
+ Y_hat = np.clip(Y_hat, 0., np.max(Y/sf, axis=0))
194
+
195
+ disp_glm = np.mean((Y/sf - Y_hat)**2 - Y_hat, axis=0) / np.mean(Y_hat**2, axis=0)
196
+ disp_glm = 1./np.clip(disp_glm, 0.01, 100.)
197
+ disp_glm[np.isnan(disp_glm)] = 1.
198
+
199
+ return disp_glm
200
+
201
+
202
+
203
+
204
+ def loess_fit(Y, X, n_jobs=-3, **kwargs):
205
+
206
+ def _loess_fit(y, x, **kwargs):
207
+ try:
208
+ from skmisc.loess import loess
209
+ l = loess(x, y, **kwargs)
210
+ l.fit()
211
+ pred = l.predict(x, stderror=True)
212
+ conf = pred.confidence()
213
+ pred, lower, upper = pred.values, conf.lower, conf.upper
214
+ except:
215
+ pred, lower, upper = np.full(y.shape[0], np.nan), np.full(y.shape[0], np.nan), np.full(y.shape[0], np.nan)
216
+
217
+ return pred, lower, upper
218
+
219
+ results = Parallel(n_jobs=n_jobs)(delayed(_loess_fit)(Y[:,j], X, **kwargs) for j in range(Y.shape[1]))
220
+
221
+ CATE, CATE_lower, CATE_upper = zip(*results)
222
+ CATE = np.array(CATE).T
223
+ CATE_lower = np.array(CATE_lower).T
224
+ CATE_upper = np.array(CATE_upper).T
225
+ return CATE, CATE_lower, CATE_upper
226
+
227
+
228
+
229
+ def ls_fit(Y, X, n_jobs=-3, **kwargs):
230
+
231
+ def _ls_fit(y, x, **kwargs):
232
+ # try:
233
+ model = sm.OLS(y, x)
234
+ result = model.fit(disp=False)
235
+
236
+ # Get the predicted values
237
+ pred = result.predict(x)
238
+
239
+ # Get the confidence intervals
240
+ conf = result.conf_int()
241
+ pred, lower, upper = pred, np.full(y.shape[0], conf[0][0]), np.full(y.shape[0], conf[0][1])
242
+ # except:
243
+ # pred, lower, upper = np.full(y.shape[0], np.nan), np.full(y.shape[0], np.nan), np.full(y.shape[0], np.nan)
244
+
245
+ return pred, lower, upper
246
+
247
+ results = Parallel(n_jobs=n_jobs)(delayed(_ls_fit)(Y[:,j], X, **kwargs) for j in range(Y.shape[1]))
248
+
249
+ CATE, CATE_lower, CATE_upper = zip(*results)
250
+ CATE = np.array(CATE).T
251
+ CATE_lower = np.array(CATE_lower).T
252
+ CATE_upper = np.array(CATE_upper).T
253
+ return CATE, CATE_lower, CATE_upper
254
+
255
+
256
+
@@ -0,0 +1,143 @@
1
+ import numpy as np
2
+ # from scipy.special import expit, xlogy, xlog1py, logsumexp, factorial
3
+ # from scipy.stats import binom, poisson, norm, nbinom
4
+
5
+ import numba as nb
6
+ from numba import njit, prange
7
+
8
+ type_f = np.float64
9
+
10
+
11
+ from scipy.special import xlogy, gammaln
12
+ from numba.extending import get_cython_function_address
13
+ import ctypes
14
+
15
+ _PTR = ctypes.POINTER
16
+ _dble = ctypes.c_double
17
+ _ptr_dble = _PTR(_dble)
18
+
19
+ addr = get_cython_function_address("scipy.special.cython_special", "gammaln")
20
+ functype = ctypes.CFUNCTYPE(_dble, _dble)
21
+ gammaln_float64 = functype(addr)
22
+
23
+ @nb.vectorize
24
+ def gammaln_nb(x):
25
+ return gammaln_float64(x)
26
+
27
+
28
+ def log_h(y, family, nuisance):
29
+ if family=='nb':
30
+ return gammaln(y + nuisance) - gammaln(nuisance) - gammaln(y+1)
31
+ elif family=='poisson':
32
+ return - gammaln(y+1)
33
+
34
+
35
+ @nb.vectorize
36
+ def log1mexp(a):
37
+ '''
38
+ A numeral stable function to compute log(1-exp(a)) for a in [-inf,0].
39
+ '''
40
+ if(a >= -np.log(type_f(2.))):
41
+ return np.log(-np.expm1(a))
42
+ else:
43
+ return np.log1p(-np.exp(a))
44
+
45
+
46
+ @njit
47
+ def nll(Y, A, B, family, nuisance=np.ones((1,1)), Tys=np.zeros((1,1)), thres_disp=10.
48
+ # size_factor=np.ones((1,1))
49
+ ):
50
+ """
51
+ Compute the negative log likelihood for generalized linear models with optional nuisance parameters.
52
+
53
+ Parameters:
54
+ Y : array-like of shape (n_samples, n_features)
55
+ The response variable.
56
+ A : array-like of shape (n_samples, n_factors)
57
+ The input data matrix.
58
+ B : array-like of shape (n_features, n_factors)
59
+ The input data matrix.
60
+ family : str, optional (default='gaussian')
61
+ The family of the generalized linear model. Options include 'poisson', and 'nb'.
62
+ nuisance : float or array-like of shape (n_samples,), optional (default=1)
63
+ The nuisance parameter for the family. For the Gaussian family, this is the variance; for the Poisson
64
+ family, this is the scaling factor; and for the negative binomial family, this is the overdispersion
65
+ parameter.
66
+ size_factor : float or array-like of shape (n_samples,), optional (default=1)
67
+ The size factor for the response variable.
68
+
69
+ Returns:
70
+ nll : float
71
+ The negative log likelihood.
72
+ """
73
+
74
+ Theta = A @ B.T
75
+ Ty = Y.copy()
76
+ n = Y.shape[0]
77
+
78
+ if family == 'poisson':
79
+ Theta = np.clip(Theta, -np.inf, type_f(1e2))
80
+ b = np.exp(Theta)
81
+ elif family == 'nb':
82
+ Xi = np.clip(Theta, -np.inf, type_f(1e2))
83
+ exp_Xi = np.exp(Xi)
84
+ tmp = np.clip(1 / (type_f(1.) + exp_Xi / nuisance), 1e-6, 1-1e-6)
85
+
86
+ Theta = np.where(nuisance > thres_disp, Xi, np.log1p(-tmp))
87
+ b = np.where(nuisance > thres_disp, exp_Xi, - nuisance * np.log(tmp) #+ gammaln_nb(nuisance+Y) - gammaln_nb(nuisance)
88
+ ) # ignoring a common factor - gammaln_nb(Y)
89
+ else:
90
+ raise ValueError('Family not recognized')
91
+
92
+ nll = - np.sum(Ty * Theta - b + Tys) / type_f(n) # * size_factor to get back the likelihood
93
+
94
+ return nll
95
+
96
+
97
+
98
+ @njit
99
+ def grad(Y, A, B, family, nuisance=np.ones((1,1)), thres_disp=10.
100
+ #size_factor=np.ones((1,1)),
101
+ ):
102
+ """
103
+ Compute the gradient of log likelihood with respect to B
104
+ for generalized linear models with optional nuisance parameters.
105
+
106
+ The natural parameter of Y is Theta = A @ B^T.
107
+
108
+ Parameters:
109
+ Y : array-like of shape (n_samples, n_features)
110
+ The response variable.
111
+ A : array-like of shape (n_samples, n_factors)
112
+ The input data matrix.
113
+ B : array-like of shape (n_features, n_factors)
114
+ The input data matrix.
115
+ family : str, optional (default='gaussian')
116
+ The family of the generalized linear model. Options include 'poisson', and 'nb'.
117
+ nuisance : float or array-like of shape (n_samples,), optional (default=1)
118
+ The nuisance parameter for the family. For the Gaussian family, this is the variance; for the Poisson
119
+ family, this is the scaling factor; and for the negative binomial family, this is the overdispersion
120
+ parameter.
121
+
122
+ Returns:
123
+ grad : array-like of shape (n_features, n_factors)
124
+ The gradient of log likelihood.
125
+ """
126
+ Theta = A @ B.T
127
+ Ty = Y.copy()
128
+ n = Y.shape[0]
129
+
130
+ if family == 'nb':
131
+ Xi = np.clip(Theta, -np.inf, type_f(1e2))
132
+ b_p = np.exp(Xi)
133
+ tmp = np.clip(1 / (type_f(1.) + b_p / nuisance), 1e-6, 1-1e-6)
134
+ grad = - (Ty - b_p) * np.where(nuisance > thres_disp, 1., tmp) # * size_factor to get back the likelihood
135
+ elif family == 'poisson':
136
+ Theta = np.clip(Theta, -np.inf, type_f(1e2))
137
+ b_p = np.exp(Theta)
138
+ grad = - (Ty - b_p) # * size_factor to get back the likelihood
139
+ else:
140
+ raise ValueError('Family not recognized')
141
+ grad = grad.T @ A / type_f(n)
142
+ return grad
143
+
causarray/gcate_opt.py ADDED
@@ -0,0 +1,298 @@
1
+ from causarray.gcate_likelihood import *
2
+ from causarray.utils import *
3
+ from causarray.gcate_glm import *
4
+ import scipy as sp
5
+ from scipy.sparse.linalg import svds
6
+ from joblib import Parallel, delayed
7
+ from collections import defaultdict
8
+
9
+
10
+ @njit
11
+ def project_norm_ball(X, radius):
12
+ """
13
+ Projects a vector X to the norm ball of a given radius.
14
+
15
+ Parameters:
16
+ X (ndarray): The vector to be projected.
17
+ radius (float): The radius of the norm ball.
18
+
19
+ Returns:
20
+ ndarray: The projected matrix.
21
+ """
22
+ norms = np.linalg.norm(X, ord=np.inf)
23
+ if norms > radius:
24
+ X *= radius / norms
25
+
26
+ return X
27
+
28
+
29
+
30
+ @njit
31
+ def line_search(Y, A, x0, g, d, family, nuisance, Ys, thres_disp, start,
32
+ lam, alpha=1., beta=0.5, max_iters=100, tol=1e-3,
33
+ ):
34
+ """
35
+ Performs line search to find the step size that minimizes a given function.
36
+
37
+ Parameters:
38
+ f (callable): A scalar-valued function of a vector argument.
39
+ x0 (ndarray): The starting point for the line search.
40
+ g (ndarray): The search direction vector.
41
+ alpha (float, optional): The initial step size. Default is 10.
42
+ beta (float, optional): The shrinkage factor used to reduce the step size. Default is 0.5.
43
+ max_iters (int, optional): The maximum number of iterations. Default is 100.
44
+ tol (float, optional): The tolerance for the step size. Default is 1e-3.
45
+
46
+ Returns:
47
+ float: The step size that minimizes the function along the search direction.
48
+ """
49
+
50
+ # Evaluate the function at the starting point.
51
+ f0 = nll(Y, A, x0, family, nuisance, Ys, thres_disp) + np.mean(lam * np.abs(x0[start:d]))
52
+
53
+ # Initialize the step size.
54
+ alpha = type_f(alpha)
55
+ beta = type_f(beta)
56
+ t = alpha
57
+ norm_g = np.linalg.norm(g)
58
+
59
+ # Iterate until the maximum number of iterations is reached or the step size is small enough.
60
+ for i in range(max_iters):
61
+
62
+ # Compute the new point.
63
+ x1 = x0 - t*g
64
+ x1[start:d] = np.sign(x1[start:d]) * np.maximum(np.abs(x1[start:d]) - lam, type_f(0.))
65
+
66
+ # Evaluate the function at the new point.
67
+ f1 = nll(Y, A, x1, family, nuisance, Ys, thres_disp) + np.mean(lam * np.abs(x1[start:d]))
68
+ if i==0:
69
+ f01 = f1
70
+
71
+ # Check if the function has decreased sufficiently.
72
+ if f1 < f0 - tol*t*norm_g:
73
+ return x1
74
+ if t<1e-4:
75
+ break
76
+ t *= beta
77
+
78
+ # Return the maximum step size.
79
+ if f01<1.1*f1:
80
+ x1 = x0 - alpha*g
81
+ x1[start:d] = np.sign(x1[start:d]) * np.maximum(np.abs(x1[start:d]) - lam, type_f(0.))
82
+ return x1
83
+
84
+
85
+
86
+
87
+
88
+
89
+ @njit(parallel=True)
90
+ def update(Y, A, B, d, lam, P1, P2,
91
+ family, nuisance, Ys, thres_disp, a, C,
92
+ alpha, beta, max_iters, tol):
93
+ n, p = Y.shape
94
+
95
+ g = grad(Y.T, B, A, family, nuisance.T, thres_disp)
96
+ g[:, :d] = 0.
97
+ for i in prange(n):
98
+ g[i, d:] = project_norm_ball(g[i, d:], 2*C)
99
+
100
+ A[i, :] = line_search(Y[i, :], B, A[i, :], g[i, :], d,
101
+ family, nuisance[0], Ys[i, :], thres_disp, 0,
102
+ type_f(0.), alpha, beta, max_iters, tol)
103
+ if P1 is not None:
104
+ A[:, d:] = P1 @ A[:, d:]
105
+
106
+ g = grad(Y, A, B, family, nuisance, thres_disp)
107
+ if P1 is not None:
108
+ g[:, :d] = 0.
109
+ elif P2 is not None:
110
+ g[:, d-a:d] = P2 @ g[:, d-a:d]
111
+ g[:, d:] = 0.
112
+ g[:, :d-a] = 0.
113
+
114
+ for j in prange(p):
115
+ if P2 is None:
116
+ g[j, d:] = project_norm_ball(g[j, d:], 2*C)
117
+ else:
118
+ g[j, :d] = project_norm_ball(g[j, :d], 2*C)
119
+
120
+ B[j, :] = line_search(Y[:, j], A, B[j, :], g[j, :], d,
121
+ family, nuisance[:,j], Ys[:, j], thres_disp, d-a,
122
+ lam[j,:], alpha, beta, max_iters, tol
123
+ )
124
+
125
+ if P2 is None:
126
+ B[:, d:] = np.clip(B[:, d:], -10., 10.)
127
+ func_val = nll(Y, A, B, family, nuisance, Ys, thres_disp)
128
+
129
+ return func_val, A, B
130
+
131
+
132
+ def alter_min(
133
+ Y, r, X=None, P1=None, P2=None, A=None, B=None,
134
+ kwargs_glm={}, kwargs_ls={}, kwargs_es={},
135
+ lam=0., a=None, verbose=False, thres_disp=100.):
136
+ '''
137
+ Alternative minimization of latent factorization for generalized linear models.
138
+
139
+ Parameters
140
+ ----------
141
+ Y : array-like, shape (n, p)
142
+ Response matrix.
143
+ r : int
144
+ The number of latent factors.
145
+ X : array-like, shape (n, d)
146
+ Observed covariate matrix.
147
+ P1 : array-like, shape (n, n)
148
+ The projection matrix for the rows.
149
+ P2 : array-like, shape (p, p)
150
+ The projection matrix onto the orthogonal column space of Gamma.
151
+ A : array-like, shape (n, d+r)
152
+ The initial matrix for the covariate and latent factors. If None, initialize internally.
153
+ B : array-like, shape (p, d+r)
154
+ The initial matrix for the covariate and latent coefficients. If None, initialize internally.
155
+ kwargs_glm : dict
156
+ Keyword arguments for the generalized linear model.
157
+ kwargs_ls : dict
158
+ Keyword arguments for the line search.
159
+ kwargs_es : dict
160
+ Keyword arguments for the early stopping.
161
+ lam : float
162
+ The regularization parameter for the l1 norm of the coefficients.
163
+ a : int
164
+ 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-offset' by default.
165
+ verbose : bool
166
+ The indicator of whether to print the progress.
167
+ thres_disp : float
168
+ The threshold for the dispersion parameter.
169
+
170
+ Returns
171
+ -------
172
+ res : dict
173
+ A dictionary containing the information of the optimization, including
174
+ 'A': the matrix (n, d+r) for the covariate and updated latent factors,
175
+ 'B': the matrix (p, d+r) for the updated covariate and latent coefficients,
176
+ 'U': the matrix (n, r) for the estimated confouders.
177
+ '''
178
+
179
+ n, p = Y.shape
180
+ d = X.shape[1]
181
+
182
+ assert d>0
183
+ if verbose:
184
+ pprint.pprint({'n':n,'p':p,'d':d,'r':r})
185
+
186
+ kwargs_glm = {**{'family':'poisson', 'disp_glm':np.ones(p), 'size_factor':np.ones(n)
187
+ }, **kwargs_glm}
188
+ kwargs_ls = {**{'alpha':0.1, 'beta':0.5, 'max_iters':20, 'tol':1e-4, 'C':None}, **kwargs_ls}
189
+ kwargs_es = {**{'max_iters':500, 'warmup':0, 'patience':5, 'tolerance':1e-3}, **kwargs_es}
190
+
191
+ if kwargs_es['max_iters'] == 0:
192
+ return A, B, {'n_iter':0, 'func_val':0., 'resid':0., 'hist':[0.], 'kwargs_glm':kwargs_glm, 'kwargs_ls':kwargs_ls, 'kwargs_es':kwargs_es}
193
+
194
+ family, nuisance, size_factor = kwargs_glm['family'], kwargs_glm['disp_glm'].astype(type_f), kwargs_glm['size_factor'].astype(type_f)
195
+ nuisance = nuisance.reshape(1,-1)
196
+ size_factor = size_factor.reshape(-1,1)
197
+
198
+ if kwargs_ls['C'] is None:
199
+ kwargs_ls['C'] = 1e3 if family=='nb' else 1e5
200
+ C = kwargs_ls['C']
201
+
202
+ if P1 is True:
203
+ Q, _ = sp.linalg.qr(X, mode='economic')
204
+ P1 = np.identity(n) - Q @ Q.T
205
+ P1 = P1.astype(type_f)
206
+
207
+ if a is None:
208
+ a = d
209
+
210
+ # initialization for Theta = A @ B^T
211
+ if A is None or B is None:
212
+ if verbose:
213
+ pprint.pprint('Estimating initial latent variables with GLMs...')
214
+ res_glm = fit_glm(Y, X, offset=np.log(size_factor[:,0]), family=family, disp_glm=nuisance[0], maxiter=100)
215
+ u, s, vt = svds(res_glm[-1], k=r)
216
+
217
+ if u.shape[1]<r:
218
+ raise ValueError(f'The number of latent factors is larger than the rank of deviance residuals ({u.shape[1]}). Try to decrease the value of r.')
219
+
220
+ if verbose:
221
+ pprint.pprint('Estimating initial coefficients with GLMs...')
222
+ A = np.c_[X, P1 @ u]
223
+ B = fit_glm(Y, A, offset=np.log(size_factor[:,0]), family=family, disp_glm=nuisance[0], maxiter=100)[0]
224
+
225
+ E = A[:, -r:] @ B[:, -r:].T
226
+ u, s, vh = sp.sparse.linalg.svds(E, k=r)
227
+ A[:, d:] = u * s[None,:]**(1/2)
228
+ B[:, d:] = vh.T * s[None,:]**(1/2)
229
+ del E, u, s, vh
230
+
231
+ # if offset==1:
232
+ # scale = np.sqrt(np.median(np.abs(X[:,0])))
233
+ # B[:, :offset] = scale
234
+ # A[:, :offset] /= scale
235
+
236
+ if P2 is not None:
237
+ P2 = P2.astype(type_f)
238
+ # E = A[:,d-a:] @ B[:,d-a:].T @ (np.identity(p) - P2)
239
+ # u, s, vh = sp.sparse.linalg.svds(E, k=r)
240
+ B[:, d-a:d] = P2 @ B[:, d-a:d]
241
+ # A[:, d:] = u * s[None,:]**(1/2)
242
+ # B[:, d:] = vh.T * s[None,:]**(1/2)
243
+ # del E, u, s, vh
244
+
245
+
246
+ Y = Y.astype(type_f)
247
+ Y /= size_factor
248
+ Ys = - gammaln_nb(Y+1)
249
+ if family=='nb':
250
+ Ys += np.where(nuisance > thres_disp, 0., gammaln_nb(nuisance+Y) - gammaln_nb(nuisance))
251
+
252
+ A = A.astype(type_f)
253
+ B = B.astype(type_f)
254
+
255
+ lam = type_f(lam)
256
+ weights = lam / (np.abs(B[:, d-a:d]) + 1e-6)
257
+
258
+ assert ~np.any(np.isnan(A))
259
+ assert ~np.any(np.isnan(B))
260
+
261
+ t = 0
262
+ func_val_pre = (nll(Y, A, B, family, nuisance, Ys, thres_disp) + np.sum(np.abs(B[:,d-a:d]) * weights)) / p
263
+ func_val = func_val_pre
264
+
265
+ kwargs_ls['alpha'] = kwargs_ls['alpha']
266
+ if verbose:
267
+ pprint.pprint({'kwargs_glm':kwargs_glm,'kwargs_ls':kwargs_ls,'kwargs_es':kwargs_es}, compact=True)
268
+
269
+ hist = [func_val_pre]
270
+ es = Early_Stopping(**kwargs_es)
271
+ with tqdm(np.arange(kwargs_es['max_iters'])) as pbar:
272
+ for t in pbar:
273
+ func_val, A, B = update(
274
+ Y, A, B, d, weights, P1, P2,
275
+ family, nuisance, Ys, thres_disp, a, C,
276
+ kwargs_ls['alpha'], kwargs_ls['beta'], kwargs_ls['max_iters'], kwargs_ls['tol'],
277
+ )
278
+ func_val = (func_val + np.sum(np.abs(B[:,d-a:d]) * weights)) / p
279
+ hist.append(func_val)
280
+ if not np.isfinite(func_val) or func_val>np.maximum(1e3*np.abs(func_val_pre),1e3):
281
+ pprint.pprint('Encountered large or infinity values. Try to decrease the value of C for the norm constraints.')
282
+ break
283
+ elif es(func_val):
284
+ pbar.set_postfix_str('Early stopped.' + es.info)
285
+ pbar.close()
286
+ break
287
+ else:
288
+ func_val_pre = func_val
289
+ pbar.set_postfix(nll='{:.02f}'.format(func_val))
290
+
291
+ kwargs_glm['disp_glm'] = nuisance[0]
292
+ res = {'n_iter':t, 'func_val':func_val, 'resid':func_val_pre - func_val,
293
+ 'hist':hist, 'kwargs_glm':kwargs_glm, 'kwargs_ls':kwargs_ls, 'kwargs_es':kwargs_es}
294
+ res['X_U'] = A; res['B_Gamma'] = B; res['U'] = A[:,d:]
295
+ return res
296
+
297
+
298
+