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.
Files changed (45) hide show
  1. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/bimax_biclus.py +11 -0
  2. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/cc_bifunc.py +16 -4
  3. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/cc_main_func.py +15 -9
  4. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/cvx_main_func.py +34 -34
  5. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/fem_bifunc.py +6 -0
  6. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/fem_main_func.py +10 -5
  7. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/lbm_bifunc.py +6 -2
  8. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/lbm_main_func.py +13 -13
  9. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/sas_bifunc.py +6 -3
  10. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/sas_main_func.py +11 -10
  11. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/simulation_data.py +10 -10
  12. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/sparse_bifunc.py +3 -0
  13. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/sparse_main_func.py +5 -5
  14. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/ssvd_biclus.py +2 -2
  15. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/ssvd_main_func.py +12 -12
  16. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib.egg-info/PKG-INFO +1 -1
  17. {bifunclib-1.0.2 → bifunclib-1.0.4}/PKG-INFO +1 -1
  18. {bifunclib-1.0.2 → bifunclib-1.0.4}/setup.py +1 -1
  19. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/AuxFunc.py +0 -0
  20. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/BiclustResult.py +0 -0
  21. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/BsplineFunc.py +0 -0
  22. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/FDPlot.py +0 -0
  23. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/__init__.py +0 -0
  24. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/bcheatmap.py +0 -0
  25. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/cvx_biclus.py +0 -0
  26. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/local_bifunc.py +0 -0
  27. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/local_main_func.py +0 -0
  28. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/pf_bifunc.py +0 -0
  29. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/pf_main_func.py +0 -0
  30. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/simulation_data/bimax_sim_data.csv +0 -0
  31. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/simulation_data/cc_sim_data.csv +0 -0
  32. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/simulation_data/cvx_sim_data.csv +0 -0
  33. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/simulation_data/velib_bonus.csv +0 -0
  34. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/simulation_data/velib_data.csv +0 -0
  35. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/simulation_data/velib_dates.csv +0 -0
  36. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/simulation_data/velib_names.csv +0 -0
  37. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib/simulation_data/velib_position.csv +0 -0
  38. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib.egg-info/SOURCES.txt +0 -0
  39. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib.egg-info/dependency_links.txt +0 -0
  40. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib.egg-info/requires.txt +0 -0
  41. {bifunclib-1.0.2 → bifunclib-1.0.4}/BiFuncLib.egg-info/top_level.txt +0 -0
  42. {bifunclib-1.0.2 → bifunclib-1.0.4}/LICENSE.txt +0 -0
  43. {bifunclib-1.0.2 → bifunclib-1.0.4}/README.md +0 -0
  44. {bifunclib-1.0.2 → bifunclib-1.0.4}/pyproject.toml +0 -0
  45. {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
- # Check inputs
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
- # Return results
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"] = {