cytocommunity2 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 (51) hide show
  1. {cytocommunity2-1.0.2/src/cytocommunity2.egg-info → cytocommunity2-1.0.4}/PKG-INFO +3 -1
  2. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/README.md +2 -0
  3. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/pyproject.toml +1 -1
  4. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/__init__.py +1 -1
  5. cytocommunity2-1.0.4/src/cytocommunity2/_io.py +11 -0
  6. cytocommunity2-1.0.4/src/cytocommunity2/colors.py +34 -0
  7. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/config.py +10 -17
  8. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/__init__.py +1 -5
  9. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/coherence.py +5 -9
  10. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/communication/between.py +14 -24
  11. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/communication/cca.py +41 -36
  12. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/communication/within.py +1 -1
  13. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/differential_composition.py +15 -113
  14. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/dominant_celltype.py +7 -19
  15. cytocommunity2-1.0.4/src/cytocommunity2/downstream/enrichment.py +176 -0
  16. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/moran.py +11 -19
  17. cytocommunity2-1.0.4/src/cytocommunity2/downstream/plotting/cn_selection.py +43 -0
  18. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/plotting/coherence.py +6 -15
  19. cytocommunity2-1.0.4/src/cytocommunity2/downstream/plotting/common.py +146 -0
  20. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/plotting/communication.py +48 -61
  21. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/plotting/dominant.py +15 -24
  22. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/plotting/dotplots.py +64 -125
  23. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/plotting/recurrence.py +65 -107
  24. cytocommunity2-1.0.4/src/cytocommunity2/downstream/recurrence.py +112 -0
  25. cytocommunity2-1.0.4/src/cytocommunity2/downstream/wilcoxon.py +51 -0
  26. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/ensemble.py +9 -25
  27. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/learning/api.py +2 -3
  28. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/learning/dataset.py +3 -14
  29. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/learning/model.py +2 -2
  30. cytocommunity2-1.0.4/src/cytocommunity2/learning/selection.py +128 -0
  31. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/learning/training.py +2 -2
  32. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/paths.py +2 -2
  33. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/visualization/spatial.py +3 -17
  34. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4/src/cytocommunity2.egg-info}/PKG-INFO +3 -1
  35. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2.egg-info/SOURCES.txt +4 -2
  36. cytocommunity2-1.0.2/src/cytocommunity2/colors.py +0 -19
  37. cytocommunity2-1.0.2/src/cytocommunity2/downstream/plotting/cn_selection.py +0 -63
  38. cytocommunity2-1.0.2/src/cytocommunity2/downstream/plotting/common.py +0 -79
  39. cytocommunity2-1.0.2/src/cytocommunity2/downstream/plotting/composition.py +0 -80
  40. cytocommunity2-1.0.2/src/cytocommunity2/downstream/runtime.py +0 -13
  41. cytocommunity2-1.0.2/src/cytocommunity2/learning/selection.py +0 -375
  42. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/LICENSE +0 -0
  43. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/setup.cfg +0 -0
  44. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/communication/__init__.py +0 -0
  45. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/communication/utils.py +0 -0
  46. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/plotting/__init__.py +0 -0
  47. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/learning/__init__.py +0 -0
  48. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/visualization/__init__.py +0 -0
  49. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2.egg-info/dependency_links.txt +0 -0
  50. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2.egg-info/requires.txt +0 -0
  51. {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cytocommunity2
3
- Version: 1.0.2
3
+ Version: 1.0.4
4
4
  Summary: Weakly supervised cellular-neighborhood learning for spatial omics
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -30,6 +30,8 @@ Dynamic: license-file
30
30
 
31
31
  ## Overview
32
32
 
33
+ ![CytoCommunity2 overview](support/Fig.1.png)
34
+
33
35
  To enhance CytoCommunity (https://github.com/huBioinfo/CytoCommunity), we
34
36
  present CytoCommunity2, **a unified weakly-supervised framework** for
35
37
  identifying and comparing tissue cellular neighborhoods (TCNs or CNs) across
@@ -10,6 +10,8 @@
10
10
 
11
11
  ## Overview
12
12
 
13
+ ![CytoCommunity2 overview](support/Fig.1.png)
14
+
13
15
  To enhance CytoCommunity (https://github.com/huBioinfo/CytoCommunity), we
14
16
  present CytoCommunity2, **a unified weakly-supervised framework** for
15
17
  identifying and comparing tissue cellular neighborhoods (TCNs or CNs) across
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cytocommunity2"
7
- version = "1.0.2"
7
+ version = "1.0.4"
8
8
  description = "Weakly supervised cellular-neighborhood learning for spatial omics"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -10,7 +10,7 @@ from .learning import (
10
10
  )
11
11
  from .visualization import visualize_results
12
12
 
13
- __version__ = "1.0.2"
13
+ __version__ = "1.0.4"
14
14
 
15
15
  __all__ = [
16
16
  "CytoCommunityConfig",
@@ -0,0 +1,11 @@
1
+ """Shared reader for the workflow's one-value-per-line text files."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ def read_nonempty_lines(path):
7
+ return [
8
+ line.strip()
9
+ for line in Path(path).read_text(encoding="utf-8-sig").splitlines()
10
+ if line.strip()
11
+ ]
@@ -0,0 +1,34 @@
1
+ """Shared categorical colors used across CytoCommunity2 figures."""
2
+
3
+
4
+ CN_PALETTE = "d3_category20"
5
+
6
+
7
+ def cn_color_map(cns):
8
+ """Map CN numbers or CN-prefixed labels to fixed spatial/OC colors."""
9
+ import sci_palettes
10
+
11
+ palette = list(sci_palettes.PALETTES[CN_PALETTE].values())
12
+ colors = {}
13
+ for cn in cns:
14
+ number = int(str(cn).removeprefix("CN"))
15
+ if not 1 <= number <= len(palette):
16
+ raise ValueError(
17
+ f"CN {cn!r} is outside the 1..{len(palette)} range of "
18
+ f"palette {CN_PALETTE!r}."
19
+ )
20
+ colors[cn] = palette[number - 1]
21
+ return colors
22
+
23
+
24
+ def celltype_color_map(celltypes):
25
+ """Return the canonical cell-type colors in the supplied display order."""
26
+ import matplotlib.pyplot as plt
27
+
28
+ palette = []
29
+ for name in ("tab20", "tab20b", "tab20c", "Set1", "Dark2", "Accent", "Paired"):
30
+ palette.extend(plt.get_cmap(name).colors)
31
+ return {
32
+ celltype: palette[index]
33
+ for index, celltype in enumerate(celltypes)
34
+ }
@@ -9,11 +9,10 @@ from typing import Any, Mapping
9
9
 
10
10
  DEFAULT_CONFIG_FILE = "hyperparameters.json"
11
11
 
12
- # Established analysis rules stay stable across runs; only experimental choices
13
- # belong in hyperparameters.json.
14
- FIXED_DOWNSTREAM = {
12
+ # Statistical thresholds and numerical settings for downstream analyses.
13
+ DOWNSTREAM_PARAMETERS = {
15
14
  "dominant_cell_type": {"frequency_threshold": 0.5},
16
- "differential_composed": {"p_value_threshold": 0.05, "minimum_samples": 3},
15
+ "differential_composition": {"p_value_threshold": 0.05, "minimum_samples": 3},
17
16
  "moran": {
18
17
  "minimum_positive_count": 2,
19
18
  "minimum_cn_cells": 10,
@@ -27,9 +26,10 @@ FIXED_DOWNSTREAM = {
27
26
  "cca_p_threshold": 0.05,
28
27
  "cca_minimum_common_samples": 3,
29
28
  "cca_minimum_abs_rho": 0.0,
30
- "cca_sd_epsilon": 1e-8,
29
+ "cca_min_std": 1e-8,
31
30
  "cca_max_iterations": 1000,
32
- "cca_canonical_components": 2,
31
+ "cca_n_components": 2,
32
+ "cca_top_celltypes": 5,
33
33
  },
34
34
  }
35
35
 
@@ -40,7 +40,7 @@ class MainConfig:
40
40
  cn_num_min: int
41
41
  cn_num_max: int
42
42
  final_num_runs: int
43
- num_epoch: int
43
+ num_epochs: int
44
44
  embedding_dimension: int
45
45
  learning_rate: float
46
46
  mini_batch_size: int
@@ -82,17 +82,10 @@ class CytoCommunityConfig:
82
82
  return _load_config(config_file, config_class=cls)
83
83
 
84
84
 
85
- def _require_keys(values, required, section):
86
- missing = [key for key in required if key not in values]
87
- if missing:
88
- raise ValueError(f"Missing configuration keys in {section}: {missing}")
89
-
90
-
91
85
  def _load_config(config_file=None, config_class=CytoCommunityConfig):
92
86
  """Load one configuration file and resolve its configured input folder."""
93
87
  config_file = Path(config_file or DEFAULT_CONFIG_FILE).expanduser().resolve()
94
88
  raw = json.loads(config_file.read_text(encoding="utf-8"))
95
- _require_keys(raw, ["input_dir", "random_seed", "main", "downstream"], "root")
96
89
 
97
90
  project_root = config_file.parent
98
91
  input_dir = Path(raw["input_dir"]).expanduser()
@@ -104,14 +97,14 @@ def _load_config(config_file=None, config_class=CytoCommunityConfig):
104
97
  recurrence = RecurrenceConfig()
105
98
  visualization = VisualizationConfig(**raw.get("visualization", {}))
106
99
  downstream = dict(raw["downstream"])
107
- for section, fixed_values in FIXED_DOWNSTREAM.items():
108
- downstream[section] = {**downstream.get(section, {}), **fixed_values}
100
+ for section, parameters in DOWNSTREAM_PARAMETERS.items():
101
+ downstream[section] = {**downstream.get(section, {}), **parameters}
109
102
 
110
103
  if main.knn_k < 1:
111
104
  raise ValueError("main.knn_k must be positive")
112
105
  if main.cn_num_min < 1 or main.cn_num_min > main.cn_num_max:
113
106
  raise ValueError("Require 1 <= main.cn_num_min <= main.cn_num_max")
114
- if main.final_num_runs < 1 or main.num_epoch < 1:
107
+ if main.final_num_runs < 1 or main.num_epochs < 1:
115
108
  raise ValueError("Run and epoch counts must be positive")
116
109
  if main.embedding_dimension < 1 or main.mini_batch_size < 1:
117
110
  raise ValueError("Embedding dimension and mini-batch size must be positive")
@@ -1,8 +1,4 @@
1
- """Reusable downstream analyses for CytoCommunity2 results.
2
-
3
- The top-level :mod:`cytocommunity2` API intentionally remains small. These
4
- functions live in a dedicated namespace because downstream analyses are
5
- optional and evolve independently from model training.
1
+ """Downstream analyses of learned cellular neighborhoods.
6
2
  """
7
3
 
8
4
  from .coherence import run_coherence_analysis
@@ -20,15 +20,10 @@ class CoherenceMetrics:
20
20
  pas: float
21
21
 
22
22
 
23
- def _coherence_inputs(cluster_labels, coordinates):
24
- labels = np.asarray(cluster_labels)
25
- locations = np.asarray(coordinates, dtype=float)
26
- return labels, locations
27
-
28
-
29
23
  def compute_chaos(cluster_labels, coordinates):
30
24
  """Compute the normalized sum of within-CN nearest-neighbor distances."""
31
- labels, locations = _coherence_inputs(cluster_labels, coordinates)
25
+ labels = np.asarray(cluster_labels)
26
+ locations = np.asarray(coordinates, dtype=float)
32
27
  locations = StandardScaler().fit_transform(locations)
33
28
  distances = []
34
29
  for label in np.unique(labels):
@@ -43,7 +38,8 @@ def compute_chaos(cluster_labels, coordinates):
43
38
 
44
39
  def compute_pas(cluster_labels, coordinates, *, k=10):
45
40
  """Compute the percentage of abnormal spots (PAS) metric."""
46
- labels, locations = _coherence_inputs(cluster_labels, coordinates)
41
+ labels = np.asarray(cluster_labels)
42
+ locations = np.asarray(coordinates, dtype=float)
47
43
  if not 1 <= k < len(labels):
48
44
  raise ValueError("k must satisfy 1 <= k < number of cells")
49
45
  neighbors = NearestNeighbors(n_neighbors=k + 1, algorithm="kd_tree").fit(
@@ -93,5 +89,5 @@ def run_coherence_analysis(config: CytoCommunityConfig, *, output_dir=None, pas_
93
89
  output_dir = Path(output_dir or paths.downstream_data / "Coherence")
94
90
  output_dir.mkdir(parents=True, exist_ok=True)
95
91
  result = analyze_result_tables(paths.result_tables, pas_k=pas_k)
96
- result.to_csv(output_dir / "config.csv", index=False)
92
+ result.to_csv(output_dir / "CoherenceMetrics.csv", index=False)
97
93
  return result
@@ -2,7 +2,6 @@
2
2
 
3
3
  from pathlib import Path
4
4
 
5
- import numpy as np
6
5
  import pandas as pd
7
6
  from scipy.stats import spearmanr
8
7
 
@@ -27,26 +26,22 @@ def select_cca_pairs(cca_pairs, top_n):
27
26
 
28
27
 
29
28
  def analyze_between_cn(
30
- scores_long, cca_pairs, coordinate_tables, *, minimum_samples, top_n_cca_pairs
29
+ scores_long, cca_pairs, loading_tables, *, minimum_samples, top_n_cca_pairs
31
30
  ):
32
- """Calculate between-CN correlations without filesystem access.
33
-
34
- ``coordinate_tables`` maps ``(CN_A, CN_B)`` string tuples to coordinate
35
- DataFrames returned by :func:`cca_coordinate_tables`.
36
- """
31
+ """Calculate between-CN correlations without filesystem access."""
37
32
  scores = normalize_scores_long(scores_long)
38
33
  pairs = select_cca_pairs(cca_pairs, top_n_cca_pairs)
39
34
  rows = []
40
35
  for _, pair in pairs.iterrows():
41
36
  cn_a, cn_b = str(pair["CN_A"]), str(pair["CN_B"])
42
- coordinates = coordinate_tables[(cn_a, cn_b)].copy()
43
- coordinates["CellType"] = coordinates["CellType"].astype(str).str.strip()
44
- coordinates_a = coordinates.dropna(subset=["CN_A_coord_can1"])
45
- coordinates_b = coordinates.dropna(subset=["CN_B_coord_can1"])
46
- top_a_index = coordinates_a["CN_A_coord_can1"].abs().idxmax()
47
- top_b_index = coordinates_b["CN_B_coord_can1"].abs().idxmax()
48
- top_a = coordinates_a.loc[top_a_index, "CellType"]
49
- top_b = coordinates_b.loc[top_b_index, "CellType"]
37
+ loadings = loading_tables[(cn_a, cn_b)].copy()
38
+ loadings["CellType"] = loadings["CellType"].astype(str).str.strip()
39
+ loadings_a = loadings.dropna(subset=["CN_A_loading1"])
40
+ loadings_b = loadings.dropna(subset=["CN_B_loading1"])
41
+ top_a_index = loadings_a["CN_A_loading1"].abs().idxmax()
42
+ top_b_index = loadings_b["CN_B_loading1"].abs().idxmax()
43
+ top_a = loadings_a.loc[top_a_index, "CellType"]
44
+ top_b = loadings_b.loc[top_b_index, "CellType"]
50
45
  scores_a = scores.loc[
51
46
  (scores["CN"] == cn_a) & (scores["CellType"] == top_a),
52
47
  ["Sample", "Score"],
@@ -70,13 +65,8 @@ def analyze_between_cn(
70
65
  "CN_A": cn_a,
71
66
  "CN_B": cn_b,
72
67
  "rho1_CCA": pair["rho1"],
73
- "pval1_CCA": pair["pval1"],
74
- "rho2_CCA": pair["rho2"] if "rho2" in pairs else np.nan,
75
- "pval2_CCA": pair["pval2"] if "pval2" in pairs else np.nan,
76
68
  "Top_CellType_A": top_a,
77
69
  "Top_CellType_B": top_b,
78
- "TopA_coord_can1": coordinates_a.loc[top_a_index, "CN_A_coord_can1"],
79
- "TopB_coord_can1": coordinates_b.loc[top_b_index, "CN_B_coord_can1"],
80
70
  "Spearman_rho": rho,
81
71
  "Spearman_p": p_value,
82
72
  "N_overlap": len(merged),
@@ -86,19 +76,19 @@ def analyze_between_cn(
86
76
  if not result.empty:
87
77
  result = result.sort_values(
88
78
  "rho1_CCA", key=lambda values: values.abs(), ascending=False
89
- )
79
+ ).drop(columns="rho1_CCA")
90
80
  return result
91
81
 
92
82
 
93
83
  def run_between_cn_statistics(config_dir, minimum_samples, top_n_cca_pairs):
94
- """Compatibility workflow operating on the established communication folder."""
84
+ """Calculate and save Spearman statistics for top CCA pairs."""
95
85
  config_dir = Path(config_dir)
96
86
  scores = pd.read_csv(config_dir / "EnrichScoreMatrix_long.csv")
97
- pairs = pd.read_csv(config_dir / "CCA_config.csv")
87
+ pairs = pd.read_csv(config_dir / "CCA_results.csv")
98
88
  selected = select_cca_pairs(pairs, top_n_cca_pairs)
99
89
  tables = {
100
90
  (str(row.CN_A), str(row.CN_B)): pd.read_csv(
101
- config_dir / f"CCA_coordinates_CN{row.CN_A}_vs_CN{row.CN_B}.csv"
91
+ config_dir / f"CCA_loadings_CN{row.CN_A}_vs_CN{row.CN_B}.csv"
102
92
  )
103
93
  for row in selected.itertuples(index=False)
104
94
  }
@@ -20,7 +20,7 @@ def pair_rng(base_seed, condition, cn_a, cn_b):
20
20
  return np.random.default_rng(seed)
21
21
 
22
22
 
23
- def clean_cca_data(x, y, minimum_common_samples, sd_epsilon):
23
+ def clean_cca_data(x, y, minimum_common_samples, min_std):
24
24
  """Align two matrices and remove incomplete or near-constant variables."""
25
25
  if x is None or y is None or x.shape[1] == 0 or y.shape[1] == 0:
26
26
  return None
@@ -29,15 +29,13 @@ def clean_cca_data(x, y, minimum_common_samples, sd_epsilon):
29
29
  return None
30
30
  x = x.loc[common]
31
31
  y = y.loc[common]
32
- x = x.loc[:, x.notna().sum(axis=0) > 0]
33
- y = y.loc[:, y.notna().sum(axis=0) > 0]
34
- x = x.loc[:, x.std(axis=0) > sd_epsilon]
35
- y = y.loc[:, y.std(axis=0) > sd_epsilon]
32
+ x = x.loc[:, x.std(axis=0) > min_std]
33
+ y = y.loc[:, y.std(axis=0) > min_std]
36
34
  if x.shape[1] == 0 or y.shape[1] == 0:
37
35
  return None
38
36
  complete = x.notna().all(axis=1) & y.notna().all(axis=1)
39
37
  x, y = x.loc[complete], y.loc[complete]
40
- if x.shape[0] < minimum_common_samples or x.shape[1] == 0 or y.shape[1] == 0:
38
+ if x.shape[0] < minimum_common_samples:
41
39
  return None
42
40
  return {
43
41
  "X": x.values,
@@ -62,10 +60,23 @@ def build_score_list(frame, cn_list):
62
60
  return matrices
63
61
 
64
62
 
63
+ def select_top_features(matrix, top_n=5):
64
+ """Select cell types by decreasing mean enrichment before variance filtering."""
65
+ if matrix is None or matrix.shape[1] == 0:
66
+ return matrix
67
+ means = matrix.mean(axis=0).fillna(-np.inf)
68
+ selected = means.sort_values(ascending=False, kind="stable").head(top_n).index
69
+ return matrix.loc[:, selected]
70
+
71
+
65
72
  def run_cca_for_scores(
66
73
  score_list, cn_list, communication_params, random_seed, condition=None
67
74
  ):
68
75
  """Run CCA and permutation tests on in-memory enrichment matrices."""
76
+ score_list = {
77
+ cn: select_top_features(matrix, communication_params["cca_top_celltypes"])
78
+ for cn, matrix in score_list.items()
79
+ }
69
80
  result_rows = []
70
81
  cache = {}
71
82
  for cn_a, cn_b in combinations(map(str, cn_list), 2):
@@ -75,7 +86,7 @@ def run_cca_for_scores(
75
86
  score_list[cn_a],
76
87
  score_list[cn_b],
77
88
  communication_params["cca_minimum_common_samples"],
78
- communication_params["cca_sd_epsilon"],
89
+ communication_params["cca_min_std"],
79
90
  )
80
91
  if cleaned is None:
81
92
  continue
@@ -93,7 +104,7 @@ def run_cca_for_scores(
93
104
  model.fit(x_matrix, y_matrix)
94
105
  x_scores, y_scores = model.transform(x_matrix, y_matrix)
95
106
  actual_components = min(
96
- communication_params["cca_canonical_components"], x_scores.shape[1]
107
+ communication_params["cca_n_components"], x_scores.shape[1]
97
108
  )
98
109
  correlations = [
99
110
  np.corrcoef(x_scores[:, index], y_scores[:, index])[0, 1]
@@ -111,7 +122,7 @@ def run_cca_for_scores(
111
122
  shuffled_y = rng.permutation(y_matrix)
112
123
  permutation_model = CCA(
113
124
  n_components=min(
114
- communication_params["cca_canonical_components"],
125
+ communication_params["cca_n_components"],
115
126
  max_components,
116
127
  ),
117
128
  scale=True,
@@ -143,7 +154,7 @@ def run_cca_for_scores(
143
154
  row = {"CN_A": cn_a, "CN_B": cn_b, "n_samples": n_samples}
144
155
  if condition is not None:
145
156
  row["Condition"] = str(condition)
146
- for component in range(communication_params["cca_canonical_components"]):
157
+ for component in range(communication_params["cca_n_components"]):
147
158
  row[f"rho{component + 1}"] = (
148
159
  correlations[component] if component < len(correlations) else None
149
160
  )
@@ -177,8 +188,8 @@ def run_cca_for_scores(
177
188
  return {"results": results, "cache": cache}
178
189
 
179
190
 
180
- def cca_coordinate_tables(
181
- results, cache, celltype_order, canonical_components, condition=None
191
+ def cca_loading_tables(
192
+ results, cache, celltype_order, n_components, condition=None
182
193
  ):
183
194
  """Convert fitted CCA scores into per-pair cell-type loading tables."""
184
195
  tables = {}
@@ -198,47 +209,41 @@ def cca_coordinate_tables(
198
209
  if cell_type in set(entry["X_ct"]) | set(entry["Y_ct"])
199
210
  ]
200
211
  data = {"CellType": all_celltypes}
201
- maximum = min(canonical_components, entry["X_scores"].shape[1])
212
+ maximum = min(n_components, entry["X_scores"].shape[1])
202
213
  for component in range(maximum):
203
214
  x_score = entry["X_scores"][:, component]
204
215
  y_score = entry["Y_scores"][:, component]
205
216
  x_loadings = {
206
217
  cell_type: np.corrcoef(entry["X_mat"][:, index], x_score)[0, 1]
207
- if len(x_score) > 1
208
- else np.nan
209
218
  for index, cell_type in enumerate(entry["X_ct"])
210
219
  }
211
220
  y_loadings = {
212
221
  cell_type: np.corrcoef(entry["Y_mat"][:, index], y_score)[0, 1]
213
- if len(y_score) > 1
214
- else np.nan
215
222
  for index, cell_type in enumerate(entry["Y_ct"])
216
223
  }
217
- data[f"CN_A_coord_can{component + 1}"] = [
224
+ data[f"CN_A_loading{component + 1}"] = [
218
225
  x_loadings.get(cell_type, np.nan) for cell_type in all_celltypes
219
226
  ]
220
- data[f"CN_B_coord_can{component + 1}"] = [
227
+ data[f"CN_B_loading{component + 1}"] = [
221
228
  y_loadings.get(cell_type, np.nan) for cell_type in all_celltypes
222
229
  ]
223
- data[f"rho{component + 1}"] = [row.get(f"rho{component + 1}", np.nan)] * len(all_celltypes)
224
- data[f"pval{component + 1}"] = [row.get(f"pval{component + 1}", np.nan)] * len(all_celltypes)
225
230
  tables[(cn_a, cn_b)] = pd.DataFrame(data)
226
231
  return tables
227
232
 
228
233
 
229
- def write_cca_coordinates(
230
- results, cache, output_dir, celltype_order, canonical_components, condition=None
234
+ def write_cca_loadings(
235
+ results, cache, output_dir, celltype_order, n_components, condition=None
231
236
  ):
232
- """Write coordinate tables using the original filenames."""
237
+ """Write per-pair cell-type CCA loading tables."""
233
238
  output_dir = Path(output_dir)
234
239
  output_dir.mkdir(parents=True, exist_ok=True)
235
- tables = cca_coordinate_tables(
236
- results, cache, celltype_order, canonical_components, condition
240
+ tables = cca_loading_tables(
241
+ results, cache, celltype_order, n_components, condition
237
242
  )
238
243
  prefix = f"{condition}_" if condition is not None else ""
239
244
  for (cn_a, cn_b), table in tables.items():
240
245
  table.to_csv(
241
- output_dir / f"CCA_coordinates_{prefix}CN{cn_a}_vs_CN{cn_b}.csv",
246
+ output_dir / f"CCA_loadings_{prefix}CN{cn_a}_vs_CN{cn_b}.csv",
242
247
  index=False,
243
248
  )
244
249
  return tables
@@ -268,13 +273,13 @@ def run_cca_analysis(
268
273
  pooled = run_cca_for_scores(
269
274
  build_score_list(frame, cn_list), cn_list, params, config.random_seed
270
275
  )
271
- pooled["results"].to_csv(output_dir / "CCA_config.csv", index=False)
272
- write_cca_coordinates(
276
+ pooled["results"].to_csv(output_dir / "CCA_results.csv", index=False)
277
+ write_cca_loadings(
273
278
  pooled["results"],
274
279
  pooled["cache"],
275
280
  output_dir,
276
281
  celltype_order,
277
- params["cca_canonical_components"],
282
+ params["cca_n_components"],
278
283
  )
279
284
  condition_results = []
280
285
  for condition in conditions:
@@ -286,13 +291,12 @@ def run_cca_analysis(
286
291
  config.random_seed,
287
292
  condition,
288
293
  )
289
- result["results"].to_csv(output_dir / f"CCA_config_{condition}.csv", index=False)
290
- write_cca_coordinates(
294
+ write_cca_loadings(
291
295
  result["results"],
292
296
  result["cache"],
293
297
  output_dir,
294
298
  celltype_order,
295
- params["cca_canonical_components"],
299
+ params["cca_n_components"],
296
300
  condition,
297
301
  )
298
302
  condition_results.append(result["results"])
@@ -310,16 +314,17 @@ def run_cca_analysis(
310
314
  key=lambda values: abs(values) if values.name == "rho1" else values,
311
315
  ascending=[True, False],
312
316
  )
313
- by_condition.to_csv(output_dir / "CCA_config_by_condition.csv", index=False)
317
+ by_condition.to_csv(output_dir / "CCA_results_by_condition.csv", index=False)
314
318
  return {"pooled": pooled["results"], "by_condition": by_condition}
315
319
 
316
320
 
317
321
  __all__ = [
318
322
  "build_score_list",
319
- "cca_coordinate_tables",
323
+ "cca_loading_tables",
320
324
  "clean_cca_data",
321
325
  "pair_rng",
322
326
  "run_cca_analysis",
323
327
  "run_cca_for_scores",
324
- "write_cca_coordinates",
328
+ "select_top_features",
329
+ "write_cca_loadings",
325
330
  ]
@@ -234,7 +234,7 @@ def run_within_cn_analysis(
234
234
  }
235
235
  outputs["pooled"].to_csv(output_dir / "SpearmanWithinCNs.csv", index=False)
236
236
  for comparison in build_condition_comparisons(
237
- scores["Condition"], config.downstream["ttest_mode"]
237
+ scores["Condition"], config.downstream["comparison_mode"]
238
238
  ):
239
239
  result = compute_condition_comparison_stats(
240
240
  wide_dict=wide,