BiFuncLib 1.0.2__tar.gz → 1.0.4__tar.gz
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.
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/bimax_biclus.py +11 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/cc_bifunc.py +16 -4
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/cc_main_func.py +15 -9
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/cvx_main_func.py +34 -34
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/fem_bifunc.py +6 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/fem_main_func.py +10 -5
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/lbm_bifunc.py +6 -2
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/lbm_main_func.py +13 -13
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/sas_bifunc.py +6 -3
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/sas_main_func.py +11 -10
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/simulation_data.py +10 -10
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/sparse_bifunc.py +3 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/sparse_main_func.py +5 -5
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/ssvd_biclus.py +2 -2
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/ssvd_main_func.py +12 -12
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib.egg-info/PKG-INFO +1 -1
- {bifunclib-1.0.2 → bifunclib-1.0.4}/PKG-INFO +1 -1
- {bifunclib-1.0.2 → bifunclib-1.0.4}/setup.py +1 -1
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/AuxFunc.py +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/BiclustResult.py +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/BsplineFunc.py +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/FDPlot.py +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/__init__.py +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/bcheatmap.py +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/cvx_biclus.py +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/local_bifunc.py +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/local_main_func.py +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/pf_bifunc.py +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/pf_main_func.py +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/simulation_data/bimax_sim_data.csv +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/simulation_data/cc_sim_data.csv +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/simulation_data/cvx_sim_data.csv +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/simulation_data/velib_bonus.csv +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/simulation_data/velib_data.csv +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/simulation_data/velib_dates.csv +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/simulation_data/velib_names.csv +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/simulation_data/velib_position.csv +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib.egg-info/SOURCES.txt +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib.egg-info/dependency_links.txt +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib.egg-info/requires.txt +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib.egg-info/top_level.txt +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/LICENSE.txt +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/README.md +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/pyproject.toml +0 -0
- {bifunclib-1.0.2 → bifunclib-1.0.4}/setup.cfg +0 -0
|
@@ -4,8 +4,10 @@ from BiFuncLib.BiclustResult import BiclustResult
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
def apriori_bimax(matrix, minr=2, minc=2, number=100):
|
|
7
|
+
# Get matrix dimensions
|
|
7
8
|
rows = len(matrix)
|
|
8
9
|
cols = len(matrix[0]) if rows else 0
|
|
10
|
+
# Convert each row to a bitmask for efficient column intersection
|
|
9
11
|
row_masks = []
|
|
10
12
|
for row in matrix:
|
|
11
13
|
m = 0
|
|
@@ -13,6 +15,7 @@ def apriori_bimax(matrix, minr=2, minc=2, number=100):
|
|
|
13
15
|
if v:
|
|
14
16
|
m |= 1 << j
|
|
15
17
|
row_masks.append(m)
|
|
18
|
+
# Generate frequent 1-itemsets (single columns meeting min row support)
|
|
16
19
|
L1 = []
|
|
17
20
|
for j in range(cols):
|
|
18
21
|
bit = 1 << j
|
|
@@ -22,11 +25,13 @@ def apriori_bimax(matrix, minr=2, minc=2, number=100):
|
|
|
22
25
|
freq_sets = []
|
|
23
26
|
if minc <= 1:
|
|
24
27
|
freq_sets.extend(L1)
|
|
28
|
+
# Apriori algorithm: iteratively build larger itemsets from smaller ones
|
|
25
29
|
k = 1
|
|
26
30
|
prev_L = L1
|
|
27
31
|
while prev_L and k < cols:
|
|
28
32
|
Ck = {}
|
|
29
33
|
n = len(prev_L)
|
|
34
|
+
# Join step: combine itemsets that share (k-1) prefix
|
|
30
35
|
for a in range(n):
|
|
31
36
|
items_a, mask_a, _ = prev_L[a]
|
|
32
37
|
for b in range(a + 1, n):
|
|
@@ -49,6 +54,7 @@ def apriori_bimax(matrix, minr=2, minc=2, number=100):
|
|
|
49
54
|
prev_L = list(Ck.values())
|
|
50
55
|
if k >= minc:
|
|
51
56
|
freq_sets.extend(prev_L)
|
|
57
|
+
# Sort by itemset size (descending) then support (descending)
|
|
52
58
|
freq_sets.sort(key=lambda x: (len(x[0]), x[2]), reverse=True)
|
|
53
59
|
maximal = []
|
|
54
60
|
for items, mask, sup in freq_sets:
|
|
@@ -57,6 +63,7 @@ def apriori_bimax(matrix, minr=2, minc=2, number=100):
|
|
|
57
63
|
maximal.append((items, mask, sup))
|
|
58
64
|
if len(maximal) >= number:
|
|
59
65
|
break
|
|
66
|
+
# Convert to bicluster format with row and column indices
|
|
60
67
|
biclusters = []
|
|
61
68
|
for items, mask, sup in maximal:
|
|
62
69
|
rows_res = [i for i, rm in enumerate(row_masks) if (rm & mask) == mask]
|
|
@@ -66,17 +73,21 @@ def apriori_bimax(matrix, minr=2, minc=2, number=100):
|
|
|
66
73
|
|
|
67
74
|
|
|
68
75
|
def bimax_biclus(matrix, minr=2, minc=2, number=100):
|
|
76
|
+
# Run Apriori-Bimax to get raw biclusters
|
|
69
77
|
raw = apriori_bimax(matrix, minr, minc, number)
|
|
70
78
|
bic_n = len(raw)
|
|
71
79
|
R = len(matrix)
|
|
72
80
|
C = len(matrix[0]) if R else 0
|
|
81
|
+
# Build RowxNumber matrix: rows x biclusters membership
|
|
73
82
|
RowxNumber = [[False] * bic_n for _ in range(R)]
|
|
83
|
+
# Build NumberxCol matrix: biclusters x columns membership
|
|
74
84
|
NumberxCol = [[False] * C for _ in range(bic_n)]
|
|
75
85
|
for idx, bc in enumerate(raw):
|
|
76
86
|
for r in bc["rows"]:
|
|
77
87
|
RowxNumber[r][idx] = True
|
|
78
88
|
for c in bc["cols"]:
|
|
79
89
|
NumberxCol[idx][c] = True
|
|
90
|
+
# Return structured bicluster result object
|
|
80
91
|
return BiclustResult(
|
|
81
92
|
{
|
|
82
93
|
"Algorithm": "Apriori-Bimax",
|
|
@@ -22,7 +22,7 @@ def cc_bifunc(
|
|
|
22
22
|
max_iter_align=100,
|
|
23
23
|
):
|
|
24
24
|
|
|
25
|
-
#
|
|
25
|
+
# Validate input parameters
|
|
26
26
|
if len(data.shape) != 3:
|
|
27
27
|
raise ValueError("Error: data should be an array of three dimensions")
|
|
28
28
|
if template_type == "medoid" and (alpha != 0 or beta != 0):
|
|
@@ -50,7 +50,7 @@ def cc_bifunc(
|
|
|
50
50
|
if delta < 0:
|
|
51
51
|
raise ValueError("Error: delta must be a number greater than 0")
|
|
52
52
|
|
|
53
|
-
# Initialize parameters
|
|
53
|
+
# Initialize algorithm parameters and matrices
|
|
54
54
|
n, m, p = data.shape
|
|
55
55
|
parameter_input = {
|
|
56
56
|
"delta": [delta],
|
|
@@ -64,6 +64,7 @@ def cc_bifunc(
|
|
|
64
64
|
"shift_max": [shift_max],
|
|
65
65
|
"max_iter_align": [max_iter_align],
|
|
66
66
|
}
|
|
67
|
+
# Determine constraint mode based on alpha/beta settings
|
|
67
68
|
if alpha == 0 and beta == 0:
|
|
68
69
|
only_one = "True"
|
|
69
70
|
elif alpha == 0 and beta != 0:
|
|
@@ -72,6 +73,7 @@ def cc_bifunc(
|
|
|
72
73
|
only_one = "True_alpha"
|
|
73
74
|
elif alpha != 0 and beta != 0:
|
|
74
75
|
only_one = "False"
|
|
76
|
+
# Initialize membership matrices
|
|
75
77
|
x = np.full((n, number), False, dtype=bool)
|
|
76
78
|
y = np.full((number, m), False, dtype=bool)
|
|
77
79
|
xy = np.zeros((n, m), dtype=int)
|
|
@@ -89,6 +91,7 @@ def cc_bifunc(
|
|
|
89
91
|
submat = data[logr, :, 0][:, logc]
|
|
90
92
|
rows_with_all_nan = np.all(np.isnan(submat), axis=1)
|
|
91
93
|
logr[logr] = ~rows_with_all_nan
|
|
94
|
+
# Check stopping conditions based on constraint mode
|
|
92
95
|
if (
|
|
93
96
|
(
|
|
94
97
|
only_one == "False"
|
|
@@ -128,6 +131,7 @@ def cc_bifunc(
|
|
|
128
131
|
submat = data[logr, :, 0][:, logc]
|
|
129
132
|
cols_with_all_nan = np.all(np.isnan(submat), axis=0)
|
|
130
133
|
logc[logc] = ~cols_with_all_nan
|
|
134
|
+
# Check stopping conditions
|
|
131
135
|
if (
|
|
132
136
|
(
|
|
133
137
|
only_one == "False"
|
|
@@ -163,6 +167,7 @@ def cc_bifunc(
|
|
|
163
167
|
)
|
|
164
168
|
):
|
|
165
169
|
break
|
|
170
|
+
# Apply Cheng-Church algorithm to current submatrix
|
|
166
171
|
fun_mat_temp = data[logr][:, logc, :]
|
|
167
172
|
erg = bigcc_fun(
|
|
168
173
|
fun_mat_temp,
|
|
@@ -190,12 +195,14 @@ def cc_bifunc(
|
|
|
190
195
|
logc = np.full(xy.shape[1], False, dtype=bool)
|
|
191
196
|
logc[np.array(clus_col)[cl - 1, :]] = True
|
|
192
197
|
else:
|
|
198
|
+
# Store found bicluster
|
|
193
199
|
k += 1
|
|
194
200
|
true_rows = np.where(logr)[0]
|
|
195
201
|
x[true_rows, k - 1] = erg[0]
|
|
196
202
|
true_cols = np.where(logc)[0]
|
|
197
203
|
y[k - 1, true_cols] = erg[1]
|
|
198
204
|
xy = xy + np.outer(x[:, k - 1].astype(int), y[k - 1, :].astype(int))
|
|
205
|
+
# Find overlapping biclusters using Bimax
|
|
199
206
|
if only_one == "False":
|
|
200
207
|
res = bimax_biclus(1 - xy, minr=2, minc=2, number=100)
|
|
201
208
|
elif only_one == "True":
|
|
@@ -210,6 +217,7 @@ def cc_bifunc(
|
|
|
210
217
|
break
|
|
211
218
|
clus_row = res.RowxNumber
|
|
212
219
|
clus_col = res.NumberxCol
|
|
220
|
+
# Filter out single-element biclusters
|
|
213
221
|
d_rows = np.sum(clus_row, axis=0)
|
|
214
222
|
d_cols = np.sum(clus_col, axis=1)
|
|
215
223
|
dimensioni = d_rows * d_cols
|
|
@@ -222,6 +230,7 @@ def cc_bifunc(
|
|
|
222
230
|
clus_col = np.array(clus_col)[if_oneone, :]
|
|
223
231
|
if n_clust == 0:
|
|
224
232
|
break
|
|
233
|
+
# Sort biclusters by size (descending)
|
|
225
234
|
if n_clust > 1:
|
|
226
235
|
d = np.sum(clus_row, axis=0) * np.sum(clus_col, axis=1)
|
|
227
236
|
sorted_idx = np.argsort(-d)
|
|
@@ -232,7 +241,7 @@ def cc_bifunc(
|
|
|
232
241
|
logc = np.full(xy.shape[1], False, dtype=bool)
|
|
233
242
|
logc[np.array(clus_col)[0, :]] = True
|
|
234
243
|
|
|
235
|
-
#
|
|
244
|
+
# Sort final biclusters by size
|
|
236
245
|
clus_row_final = x[:, :k]
|
|
237
246
|
clus_col_final = y[:k, :]
|
|
238
247
|
if k > 1:
|
|
@@ -248,7 +257,7 @@ def cc_bifunc(
|
|
|
248
257
|
}
|
|
249
258
|
return result
|
|
250
259
|
|
|
251
|
-
|
|
260
|
+
# Cross-validation for delta parameter selection
|
|
252
261
|
def cc_bifunc_cv(
|
|
253
262
|
data,
|
|
254
263
|
delta_list,
|
|
@@ -288,11 +297,13 @@ def cc_bifunc_cv(
|
|
|
288
297
|
shift_max=shift_max,
|
|
289
298
|
max_iter_align=max_iter_align,
|
|
290
299
|
)
|
|
300
|
+
# Handle no clusters found
|
|
291
301
|
if res_fun["Number"] == 0:
|
|
292
302
|
Htot_all_mean_list.append(np.nan)
|
|
293
303
|
Htot_sum_list.append(np.nan)
|
|
294
304
|
num_clust_list.append(0)
|
|
295
305
|
not_assigned_list.append(data.shape[0] * data.shape[1])
|
|
306
|
+
# Evaluate single bicluster
|
|
296
307
|
elif res_fun["Number"] == 1:
|
|
297
308
|
row_mask = res_fun["RowxNumber"]
|
|
298
309
|
col_mask = res_fun["NumberxCol"]
|
|
@@ -316,6 +327,7 @@ def cc_bifunc_cv(
|
|
|
316
327
|
Htot_all_mean_list.append(Htot_d)
|
|
317
328
|
Htot_sum_list.append(np.sum(H_cl))
|
|
318
329
|
num_clust_list.append(1)
|
|
330
|
+
# Evaluate multiple biclusters
|
|
319
331
|
elif res_fun["Number"] > 1:
|
|
320
332
|
num_clust_list.append(res_fun["Number"])
|
|
321
333
|
H_cl = []
|
|
@@ -3,7 +3,7 @@ from scipy.optimize import minimize_scalar
|
|
|
3
3
|
from scipy.interpolate import interp1d
|
|
4
4
|
from scipy.spatial.distance import pdist, squareform
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
# Compute medoid-based template using pairwise distances
|
|
7
7
|
def medoid_evaluation(fun_mat, a, b, const_a, const_b):
|
|
8
8
|
n, m, p = fun_mat.shape
|
|
9
9
|
fun_per_medoid = fun_mat.reshape(n * m, p)
|
|
@@ -16,7 +16,7 @@ def medoid_evaluation(fun_mat, a, b, const_a, const_b):
|
|
|
16
16
|
new_fun = np.tile(medoid_fun, (n, m, 1))
|
|
17
17
|
return new_fun
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
# Compute medoid template for selected rows/columns only
|
|
20
20
|
def medoid_evaluation_add(fun_mat, logr, logc, a, b, const_a, const_b):
|
|
21
21
|
n, m, p = fun_mat.shape
|
|
22
22
|
filtered_fun_mat = fun_mat[logr, :, :][:, logc, :]
|
|
@@ -31,7 +31,7 @@ def medoid_evaluation_add(fun_mat, logr, logc, a, b, const_a, const_b):
|
|
|
31
31
|
new_fun = np.tile(medoid_fun, (n, m, 1))
|
|
32
32
|
return new_fun
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
# Compute mean-based template with row and column effects
|
|
35
35
|
def template_evaluation(fun_mat, a, b, const_a, const_b):
|
|
36
36
|
n, m, p = fun_mat.shape
|
|
37
37
|
count_null = np.sum(np.isnan(fun_mat), axis=2)
|
|
@@ -65,7 +65,7 @@ def template_evaluation(fun_mat, a, b, const_a, const_b):
|
|
|
65
65
|
new_fun = fun_mean_mat + b * beta_fun_mat + a * alpha_fun_mat
|
|
66
66
|
return new_fun
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
# Compute mean template for selected rows/columns with additive effects
|
|
69
69
|
def template_evaluation_add(fun_mat, logr, logc, a, b, const_a, const_b):
|
|
70
70
|
n, m, p = fun_mat.shape
|
|
71
71
|
count_null = np.sum(np.isnan(fun_mat), axis=2)
|
|
@@ -112,6 +112,7 @@ def warping_function(
|
|
|
112
112
|
shift_max,
|
|
113
113
|
max_iter,
|
|
114
114
|
):
|
|
115
|
+
# Compute distance between warped data and template
|
|
115
116
|
def warping_shift(coeff):
|
|
116
117
|
st = x_reg + coeff
|
|
117
118
|
template_t = new_fun[i, j, :]
|
|
@@ -183,6 +184,7 @@ def warping_function_add(
|
|
|
183
184
|
shift_max,
|
|
184
185
|
max_iter,
|
|
185
186
|
):
|
|
187
|
+
# Compute distance for warped data against template
|
|
186
188
|
def warping_shift(coeff):
|
|
187
189
|
st = x_reg + coeff
|
|
188
190
|
template_t = new_fun[i, j, :]
|
|
@@ -322,28 +324,28 @@ def evaluate_mat_dist_add(
|
|
|
322
324
|
mat_dist = np.sum((fun_mat - new_fun) ** 2, axis=2) / p
|
|
323
325
|
return mat_dist
|
|
324
326
|
|
|
325
|
-
|
|
327
|
+
# Compute overall Cheng-Church score as mean distance
|
|
326
328
|
def ccscore_fun(mat_dist):
|
|
327
329
|
score_fun = np.nanmean(mat_dist)
|
|
328
330
|
return score_fun
|
|
329
331
|
|
|
330
|
-
|
|
332
|
+
# Compute row-wise mean distances
|
|
331
333
|
def rowscore_fun(mat_dist):
|
|
332
334
|
score_fun = np.nanmean(mat_dist, axis=1)
|
|
333
335
|
return score_fun
|
|
334
336
|
|
|
335
|
-
|
|
337
|
+
# Compute column-wise mean distances
|
|
336
338
|
def colscore_fun(mat_dist):
|
|
337
339
|
score_fun = np.nanmean(mat_dist, axis=0)
|
|
338
340
|
return score_fun
|
|
339
341
|
|
|
340
|
-
|
|
342
|
+
# Compute row scores for selected columns
|
|
341
343
|
def addrowscore_fun(mat_dist, logc):
|
|
342
344
|
selected_cols = mat_dist[:, logc]
|
|
343
345
|
score_fun = np.nanmean(selected_cols, axis=1)
|
|
344
346
|
return score_fun
|
|
345
347
|
|
|
346
|
-
|
|
348
|
+
# Compute column scores for selected rows
|
|
347
349
|
def addcolscore_fun(mat_dist, logr):
|
|
348
350
|
selected_rows = mat_dist[logr, :]
|
|
349
351
|
score_fun = np.nanmean(selected_rows, axis=0)
|
|
@@ -365,6 +367,7 @@ def cc1_fun(
|
|
|
365
367
|
max_iter,
|
|
366
368
|
only_one,
|
|
367
369
|
):
|
|
370
|
+
# Phase 1: Remove worst rows/columns until score <= delta
|
|
368
371
|
logr = logr.copy()
|
|
369
372
|
logc = logc.copy()
|
|
370
373
|
sub_mat = fun_mat[np.ix_(logr, logc, [True] * fun_mat.shape[2])]
|
|
@@ -478,6 +481,7 @@ def cc2_fun(
|
|
|
478
481
|
shift_max,
|
|
479
482
|
max_iter,
|
|
480
483
|
):
|
|
484
|
+
# Phase 2: Multiple row/column removal using theta threshold
|
|
481
485
|
logr = logr.copy()
|
|
482
486
|
logc = logc.copy()
|
|
483
487
|
mdi = 1
|
|
@@ -565,6 +569,7 @@ def cc3_fun(
|
|
|
565
569
|
shift_max,
|
|
566
570
|
max_iter,
|
|
567
571
|
):
|
|
572
|
+
# Phase 3: Add rows/columns back to expand bicluster
|
|
568
573
|
logr = logr.copy()
|
|
569
574
|
logc = logc.copy()
|
|
570
575
|
br = 1
|
|
@@ -647,6 +652,7 @@ def bigcc_fun(
|
|
|
647
652
|
max_iter,
|
|
648
653
|
only_one,
|
|
649
654
|
):
|
|
655
|
+
# Main Cheng-Church algorithm: 3-phase iterative refinement
|
|
650
656
|
n, m, p = fun_mat.shape
|
|
651
657
|
logr = np.ones(n, dtype=bool)
|
|
652
658
|
logc = np.ones(m, dtype=bool)
|
|
@@ -5,7 +5,7 @@ import networkx as nx
|
|
|
5
5
|
import random
|
|
6
6
|
import math
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
# Compute Gaussian kernel weights for all pairs of columns
|
|
9
9
|
def kernel_weights(X, phi):
|
|
10
10
|
p, n = X.shape
|
|
11
11
|
num_weights = n * (n - 1) // 2
|
|
@@ -19,7 +19,7 @@ def kernel_weights(X, phi):
|
|
|
19
19
|
k += 1
|
|
20
20
|
return w
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
# Sparse matrix-matrix multiplication: M * X
|
|
23
23
|
def spmm(M, X):
|
|
24
24
|
m = M["Nrow"]
|
|
25
25
|
n = M["Ncol"]
|
|
@@ -32,7 +32,7 @@ def spmm(M, X):
|
|
|
32
32
|
Y[row_ind, i] += M["values"][k] * X[j, i]
|
|
33
33
|
return Y
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
# Sparse matrix transpose multiplication: M^T * X
|
|
36
36
|
def spmtm(M, X):
|
|
37
37
|
n = M["Ncol"]
|
|
38
38
|
p = X.shape[1]
|
|
@@ -44,7 +44,7 @@ def spmtm(M, X):
|
|
|
44
44
|
Y[j, i] += M["values"][k] * X[row_ind, i]
|
|
45
45
|
return Y
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
# Sparse matrix multiply with transpose: M * X^T
|
|
48
48
|
def spmmt(M, X):
|
|
49
49
|
m = M["Nrow"]
|
|
50
50
|
n = M["Ncol"]
|
|
@@ -57,19 +57,19 @@ def spmmt(M, X):
|
|
|
57
57
|
Y[row_ind, i] += M["values"][k] * X[i, j]
|
|
58
58
|
return Y
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
# Compute dual objective for convex clustering
|
|
61
61
|
def convex_cluster_dual(XT, UT):
|
|
62
62
|
dual = 0.5 * (np.sum(XT**2) - np.sum(UT**2))
|
|
63
63
|
return dual
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
# Compute primal objective with fusion penalty
|
|
66
66
|
def convex_cluster_primal(XT, UT, VT, Phi, w):
|
|
67
67
|
primal = 0.5 * np.sum((XT - UT) ** 2)
|
|
68
68
|
VT[:] = spmm(Phi, UT)
|
|
69
69
|
penalty = np.sum(w * np.sqrt(np.sum(VT**2, axis=1)))
|
|
70
70
|
return primal + penalty
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
# Compute biclustering primal with row and column fusion penalties
|
|
73
73
|
def convex_bicluster_primal(
|
|
74
74
|
XT, UT, VT_row, VT_col, Phi_row, Phi_col, w_row, w_col
|
|
75
75
|
):
|
|
@@ -80,7 +80,7 @@ def convex_bicluster_primal(
|
|
|
80
80
|
penalty_col = np.sum(w_col * np.sqrt(np.sum(VT_col**2, axis=1)))
|
|
81
81
|
return primal + penalty_row + penalty_col
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
# Proximal operator for L2 norm (group lasso)
|
|
84
84
|
def prox_L2(X, tau):
|
|
85
85
|
m, n = X.shape
|
|
86
86
|
Y = np.empty_like(X)
|
|
@@ -93,7 +93,7 @@ def prox_L2(X, tau):
|
|
|
93
93
|
Y[i, :] = factor * X[i, :]
|
|
94
94
|
return Y
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
# Projection onto L2 ball
|
|
97
97
|
def proj_L2(X, tau):
|
|
98
98
|
m, n = X.shape
|
|
99
99
|
Y = np.empty_like(X)
|
|
@@ -106,49 +106,49 @@ def proj_L2(X, tau):
|
|
|
106
106
|
Y[i, :] = factor * X[i, :]
|
|
107
107
|
return Y
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
# Update U from dual variables
|
|
110
110
|
def update_UT(XT, LambdaT, Phi):
|
|
111
111
|
UT = spmtm(Phi, LambdaT)
|
|
112
112
|
UT = XT - UT
|
|
113
113
|
return UT
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
# Compute gradient of dual variables
|
|
116
116
|
def grad_LambdaT(UT, Phi):
|
|
117
117
|
gLambdaT = spmm(Phi, UT)
|
|
118
118
|
return -gLambdaT
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
# Update dual variables with gradient step
|
|
121
121
|
def update_LambdaT2(LambdaT, gLambdaT, nu, w):
|
|
122
122
|
LambdaT_temp = LambdaT - nu * gLambdaT
|
|
123
123
|
new_LambdaT = proj_L2(LambdaT_temp, w)
|
|
124
124
|
return new_LambdaT
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
# Alternative update for dual variables
|
|
127
127
|
def update_LambdaT(LambdaT, UT, Phi, nu, w):
|
|
128
128
|
LambdaT_temp = spmm(Phi, UT)
|
|
129
129
|
LambdaT_temp = LambdaT + nu * LambdaT_temp
|
|
130
130
|
new_LambdaT = proj_L2(LambdaT_temp, w)
|
|
131
131
|
return new_LambdaT
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
# Update row fusion variables
|
|
134
134
|
def update_VT_row(U, LambdaT, Phi, w, nu):
|
|
135
135
|
tau = w / nu
|
|
136
136
|
VT_temp = spmm(Phi, U) - (1 / nu) * LambdaT
|
|
137
137
|
VT_row = prox_L2(VT_temp, tau)
|
|
138
138
|
return VT_row
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
# Update column fusion variables
|
|
141
141
|
def update_VT_col(UT, LambdaT, Phi, w, nu):
|
|
142
142
|
tau = w / nu
|
|
143
143
|
VT_temp = spmm(Phi, UT) - (1 / nu) * LambdaT
|
|
144
144
|
VT_col = prox_L2(VT_temp, tau)
|
|
145
145
|
return VT_col
|
|
146
146
|
|
|
147
|
-
|
|
147
|
+
# Convert triangular indices to linear index
|
|
148
148
|
def tri2vec(i, j, n):
|
|
149
149
|
return n * (i - 1) - (i * (i - 1)) // 2 + j - i
|
|
150
150
|
|
|
151
|
-
|
|
151
|
+
# Convert linear index to triangular indices
|
|
152
152
|
def vec2tri(k, n):
|
|
153
153
|
k = np.asarray(k)
|
|
154
154
|
i = np.ceil(0.5 * (2 * n - 1 - np.sqrt((2 * n - 1) ** 2 - 8 * k))).astype(
|
|
@@ -157,7 +157,7 @@ def vec2tri(k, n):
|
|
|
157
157
|
j = k - n * (i - 1) + (i * (i - 1)) // 2 + i
|
|
158
158
|
return np.column_stack((i, j))
|
|
159
159
|
|
|
160
|
-
|
|
160
|
+
# Compute graph weights using k-nearest neighbors
|
|
161
161
|
def gkn_weights(X, phi=0.5, k_row=5, k_col=5):
|
|
162
162
|
p, n = X.shape
|
|
163
163
|
w_row = kernel_weights(X.T, phi / n)
|
|
@@ -183,7 +183,7 @@ def gkn_weights(X, phi=0.5, k_row=5, k_col=5):
|
|
|
183
183
|
"nColComp": nColComp,
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
|
|
186
|
+
# Keep only k-nearest neighbor weights
|
|
187
187
|
def knn_weights(w, k, n):
|
|
188
188
|
keep = set()
|
|
189
189
|
i = 1
|
|
@@ -210,7 +210,7 @@ def knn_weights(w, k, n):
|
|
|
210
210
|
new_w[mask] = 0
|
|
211
211
|
return csc_matrix(new_w.reshape(-1, 1))
|
|
212
212
|
|
|
213
|
-
|
|
213
|
+
# Create edge incidence matrix from weights
|
|
214
214
|
def create_edge_incidence(w, n):
|
|
215
215
|
P = vec2tri(w.indices + 1, n)
|
|
216
216
|
nEdges = P.shape[0]
|
|
@@ -220,7 +220,7 @@ def create_edge_incidence(w, n):
|
|
|
220
220
|
E = coo_matrix((data, (row, col)), shape=(nEdges, n)).tocsc()
|
|
221
221
|
return E
|
|
222
222
|
|
|
223
|
-
|
|
223
|
+
# Create adjacency matrix from fusion variables
|
|
224
224
|
def create_adjacency(V, Phi):
|
|
225
225
|
differences = np.linalg.norm(V, axis=0)
|
|
226
226
|
connected_ix = np.where(differences == 0)[0]
|
|
@@ -244,7 +244,7 @@ def create_adjacency(V, Phi):
|
|
|
244
244
|
A = csc_matrix((n, n))
|
|
245
245
|
return A
|
|
246
246
|
|
|
247
|
-
|
|
247
|
+
# Find connected components in graph
|
|
248
248
|
def find_clusters(A):
|
|
249
249
|
G = nx.from_scipy_sparse_array(A, create_using=nx.Graph)
|
|
250
250
|
clusters = list(nx.connected_components(G))
|
|
@@ -256,7 +256,7 @@ def find_clusters(A):
|
|
|
256
256
|
sizes = np.array([len(comp) for comp in clusters], dtype=int)
|
|
257
257
|
return {"cluster": cluster, "size": sizes}
|
|
258
258
|
|
|
259
|
-
|
|
259
|
+
# Create adjacency matrix from weight indices
|
|
260
260
|
def weights_graph(w, n):
|
|
261
261
|
P = vec2tri(w.indices + 1, n)
|
|
262
262
|
nEdges = P.shape[0]
|
|
@@ -266,7 +266,7 @@ def weights_graph(w, n):
|
|
|
266
266
|
A = coo_matrix((data, (row, col)), shape=(n, n)).tocsc()
|
|
267
267
|
return A
|
|
268
268
|
|
|
269
|
-
|
|
269
|
+
# Update missing values via majorization
|
|
270
270
|
def update_majorization(MT, UT, Theta, nMissing):
|
|
271
271
|
MT = np.asfortranarray(MT)
|
|
272
272
|
UT = np.asfortranarray(UT)
|
|
@@ -274,7 +274,7 @@ def update_majorization(MT, UT, Theta, nMissing):
|
|
|
274
274
|
MT.ravel(order="F")[idx] = UT.ravel(order="F")[idx]
|
|
275
275
|
return MT
|
|
276
276
|
|
|
277
|
-
|
|
277
|
+
# Compute mean values for each bicluster block
|
|
278
278
|
def get_subgroup_means_full(X, clusters_row, clusters_col):
|
|
279
279
|
if not isinstance(X, np.ndarray):
|
|
280
280
|
X = np.array(X)
|
|
@@ -295,13 +295,13 @@ def get_subgroup_means_full(X, clusters_row, clusters_col):
|
|
|
295
295
|
M[i, j] = np.nanmean(submat)
|
|
296
296
|
return M
|
|
297
297
|
|
|
298
|
-
|
|
298
|
+
# Get subgroup means with missing values excluded
|
|
299
299
|
def get_subgroup_means(X, Theta, clusters_row, clusters_col):
|
|
300
300
|
Y = X.copy()
|
|
301
301
|
Y.to_numpy().ravel(order="F")[Theta] = np.nan
|
|
302
302
|
return get_subgroup_means_full(Y, clusters_row, clusters_col)
|
|
303
303
|
|
|
304
|
-
|
|
304
|
+
# Generate random validation set indices
|
|
305
305
|
def get_validation(p, n, fraction=0.1, seed=123):
|
|
306
306
|
random.seed(seed)
|
|
307
307
|
total = n * p
|
|
@@ -314,7 +314,7 @@ def get_validation(p, n, fraction=0.1, seed=123):
|
|
|
314
314
|
ThetaV = ix1
|
|
315
315
|
return {"ThetaM": ThetaM, "ThetaV": ThetaV}
|
|
316
316
|
|
|
317
|
-
|
|
317
|
+
# FASTA solver for convex clustering with Nesterov acceleration
|
|
318
318
|
def convex_cluster_fasta(
|
|
319
319
|
XT,
|
|
320
320
|
UT,
|
|
@@ -389,7 +389,7 @@ def convex_cluster_fasta(
|
|
|
389
389
|
break
|
|
390
390
|
return nu, its, primal_hist, dual_hist
|
|
391
391
|
|
|
392
|
-
|
|
392
|
+
# ADMM-style solver for convex biclustering
|
|
393
393
|
def convex_bicluster_dlpa(
|
|
394
394
|
XT,
|
|
395
395
|
LambdaT_row,
|
|
@@ -505,7 +505,7 @@ def convex_bicluster_dlpa(
|
|
|
505
505
|
"iter": its,
|
|
506
506
|
}
|
|
507
507
|
|
|
508
|
-
|
|
508
|
+
# Majorization-minimization for biclustering with missing values
|
|
509
509
|
def convex_bicluster_impute(
|
|
510
510
|
MT,
|
|
511
511
|
UT,
|
|
@@ -603,7 +603,7 @@ def convex_bicluster_impute(
|
|
|
603
603
|
res["nu_col"],
|
|
604
604
|
)
|
|
605
605
|
|
|
606
|
-
|
|
606
|
+
# Wrapper for convex biclustering with imputation
|
|
607
607
|
def test_convex_bicluster_impute(
|
|
608
608
|
mt,
|
|
609
609
|
ut,
|
|
@@ -681,7 +681,7 @@ def test_convex_bicluster_impute(
|
|
|
681
681
|
tol_inner,
|
|
682
682
|
)
|
|
683
683
|
|
|
684
|
-
|
|
684
|
+
# Main COBRA solver (Convex Biclustering)
|
|
685
685
|
def cobra_pod(
|
|
686
686
|
X,
|
|
687
687
|
Lambda_row,
|
|
@@ -773,7 +773,7 @@ def cobra_pod(
|
|
|
773
773
|
"iter": iter_count,
|
|
774
774
|
}
|
|
775
775
|
|
|
776
|
-
|
|
776
|
+
# Cross-validation for gamma parameter selection
|
|
777
777
|
def cobra_validate(
|
|
778
778
|
X,
|
|
779
779
|
E_row,
|
|
@@ -852,7 +852,7 @@ def cobra_validate(
|
|
|
852
852
|
"validation_error": validation_error,
|
|
853
853
|
}
|
|
854
854
|
|
|
855
|
-
|
|
855
|
+
# Smooth data matrix using bicluster means
|
|
856
856
|
def biclust_smooth(X, clusters_row, clusters_col):
|
|
857
857
|
p, n = X.shape
|
|
858
858
|
Y = np.full((p, n), np.nan)
|
|
@@ -20,6 +20,7 @@ def fem_bifunc(
|
|
|
20
20
|
lambda_=0,
|
|
21
21
|
graph=False,
|
|
22
22
|
):
|
|
23
|
+
# Initialize storage for all model-K combinations
|
|
23
24
|
resultat = [None] * (len(K) * len(model))
|
|
24
25
|
bic = [None] * (len(K) * len(model))
|
|
25
26
|
aic = [None] * (len(K) * len(model))
|
|
@@ -27,6 +28,7 @@ def fem_bifunc(
|
|
|
27
28
|
nbprm = [None] * (len(K) * len(model))
|
|
28
29
|
ll = [None] * (len(K) * len(model))
|
|
29
30
|
it = 0
|
|
31
|
+
# Grid search over K values and model types
|
|
30
32
|
for k_idx in range(len(K)):
|
|
31
33
|
current_K = K[k_idx]
|
|
32
34
|
if disp:
|
|
@@ -44,6 +46,7 @@ def fem_bifunc(
|
|
|
44
46
|
lambda_=lambda_,
|
|
45
47
|
graph=graph,
|
|
46
48
|
)
|
|
49
|
+
# Store information criteria if optimization succeeded
|
|
47
50
|
if resultat[it] is not None:
|
|
48
51
|
bic[it] = resultat[it]["bic"]
|
|
49
52
|
aic[it] = resultat[it]["aic"]
|
|
@@ -64,10 +67,12 @@ def fem_bifunc(
|
|
|
64
67
|
f"{current_model}\t:\t icl = {resultat[it]['icl']}"
|
|
65
68
|
)
|
|
66
69
|
it += 1
|
|
70
|
+
# Validate that at least one model succeeded
|
|
67
71
|
if all(v is None for v in bic):
|
|
68
72
|
raise ValueError(
|
|
69
73
|
"No reliable results to return (empty clusters in all partitions)!"
|
|
70
74
|
)
|
|
75
|
+
# Select best model based on chosen criterion
|
|
71
76
|
if crit == "bic":
|
|
72
77
|
id_max = bic.index(max([v for v in bic if v is not None]))
|
|
73
78
|
crit_max = resultat[id_max]["bic"]
|
|
@@ -82,6 +87,7 @@ def fem_bifunc(
|
|
|
82
87
|
print(
|
|
83
88
|
f"The best model is {res['model']} with K = {res['K']} ({crit} = {crit_max})"
|
|
84
89
|
)
|
|
90
|
+
# Add metadata and all criterion values to result
|
|
85
91
|
res["crit"] = crit
|
|
86
92
|
nm = len(model)
|
|
87
93
|
res["allCriterions"] = {
|