multineuronchat 2025.11.10.dev0__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.
- multineuronchat/InteractionDB/InteractionDB.py +67 -0
- multineuronchat/InteractionDB/InteractionDBRow.py +50 -0
- multineuronchat/InteractionDB/__init__.py +4 -0
- multineuronchat/MultiNeuronChat.py +447 -0
- multineuronchat/MultiNeuronChatObject.py +959 -0
- multineuronchat/__init__.py +28 -0
- multineuronchat/db/__init__.py +0 -0
- multineuronchat/loompy_utils.py +66 -0
- multineuronchat/masks.py +358 -0
- multineuronchat/normalize.py +177 -0
- multineuronchat/utils.py +159 -0
- multineuronchat/visualize.py +1029 -0
- multineuronchat-2025.11.10.dev0.dist-info/METADATA +117 -0
- multineuronchat-2025.11.10.dev0.dist-info/RECORD +17 -0
- multineuronchat-2025.11.10.dev0.dist-info/WHEEL +5 -0
- multineuronchat-2025.11.10.dev0.dist-info/licenses/LICENSE +674 -0
- multineuronchat-2025.11.10.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import os.path
|
|
2
|
+
|
|
3
|
+
from importlib.resources import files
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from .InteractionDBRow import InteractionDBRow
|
|
8
|
+
|
|
9
|
+
from typing import Optional, List, Set, Union
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InteractionDB:
|
|
13
|
+
def __init__(self,
|
|
14
|
+
db: Optional[str] = 'human_extended'):
|
|
15
|
+
if (db not in ['human', 'mouse', 'human_extended']) and (not os.path.exists(db)):
|
|
16
|
+
raise ValueError('DB must be "human", "human_extended", "mouse", or a valid path to a dataset')
|
|
17
|
+
|
|
18
|
+
if db in ['human', 'mouse', 'human_extended']:
|
|
19
|
+
with files('multineuronchat.db').joinpath(f'../db/interactionDB_{db}.pkl').open('rb') as file:
|
|
20
|
+
#with files('MultiNeuronChat.db').joinpath(f'../db/interactionDB_{db}.pkl').open('rb') as file:
|
|
21
|
+
self.interaction_df: pd.DataFrame = pd.read_pickle(file)
|
|
22
|
+
else:
|
|
23
|
+
self.interaction_df: pd.DataFrame = pd.read_pickle(db)
|
|
24
|
+
|
|
25
|
+
self.__set_of_genes: Set[str] = self.__extract_set_of_genes()
|
|
26
|
+
|
|
27
|
+
def __extract_set_of_genes(self) -> Set[str]:
|
|
28
|
+
set_of_genes: Set[str] = set()
|
|
29
|
+
|
|
30
|
+
for lig_contributor in self.interaction_df['lig_contributor']:
|
|
31
|
+
genes: List[str] = lig_contributor.split('-')
|
|
32
|
+
set_of_genes.update(genes)
|
|
33
|
+
|
|
34
|
+
for target_contributor in self.interaction_df['target_subunit']:
|
|
35
|
+
genes: List[str] = target_contributor.split('-')
|
|
36
|
+
set_of_genes.update(genes)
|
|
37
|
+
|
|
38
|
+
return set_of_genes
|
|
39
|
+
|
|
40
|
+
def get_set_of_genes(self) -> Set[str]:
|
|
41
|
+
return self.__set_of_genes
|
|
42
|
+
|
|
43
|
+
def get_interaction_names(self) -> List[str]:
|
|
44
|
+
return self.interaction_df['interaction_name'].tolist()
|
|
45
|
+
|
|
46
|
+
def __getitem__(self, item: Union[str, int]) -> InteractionDBRow:
|
|
47
|
+
if type(item) is str:
|
|
48
|
+
return InteractionDBRow(self.interaction_df[self.interaction_df['interaction_name'] == item].iloc[0])
|
|
49
|
+
elif type(item) is int:
|
|
50
|
+
return InteractionDBRow(self.interaction_df.iloc[item])
|
|
51
|
+
else:
|
|
52
|
+
raise ValueError('Access is only defined for the type str or int')
|
|
53
|
+
|
|
54
|
+
def __len__(self) -> int:
|
|
55
|
+
return len(self.interaction_df)
|
|
56
|
+
|
|
57
|
+
def __iter__(self):
|
|
58
|
+
self.__iter_idx = 0
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
def __next__(self) -> InteractionDBRow:
|
|
62
|
+
if self.__iter_idx < len(self.interaction_df):
|
|
63
|
+
interaction = InteractionDBRow(self.interaction_df.iloc[self.__iter_idx])
|
|
64
|
+
self.__iter_idx += 1
|
|
65
|
+
return interaction
|
|
66
|
+
else:
|
|
67
|
+
raise StopIteration
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
from typing import List, Dict, Set
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class InteractionDBRow:
|
|
7
|
+
def __init__(self, row_series: pd.Series):
|
|
8
|
+
self.interaction_name: str = row_series['interaction_name']
|
|
9
|
+
self.ligand_type: str = row_series['ligand_type']
|
|
10
|
+
self.interaction_type: str = row_series['interaction_type']
|
|
11
|
+
|
|
12
|
+
self.ligand_contributor: List[str] = row_series['lig_contributor'].split('-')
|
|
13
|
+
self.ligand_contributor_group: List[int] = list(
|
|
14
|
+
map(lambda x: int(x), row_series['lig_contributor_group'].split('-'))
|
|
15
|
+
)
|
|
16
|
+
self.ligand_contributor_coeff: List[int] = list(
|
|
17
|
+
map(lambda x: int(x), row_series['lig_contributor_coeff'].split('-'))
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
self.ligand_group_to_gene_dict: Dict[int, List[str]] = {}
|
|
21
|
+
self.ligand_group_to_coeff_dict: Dict[int, int] = {}
|
|
22
|
+
for ligand, group in zip(self.ligand_contributor,
|
|
23
|
+
self.ligand_contributor_group):
|
|
24
|
+
if group not in self.ligand_group_to_gene_dict.keys():
|
|
25
|
+
self.ligand_group_to_gene_dict[group] = []
|
|
26
|
+
self.ligand_group_to_coeff_dict[group] = self.ligand_contributor_coeff[group-1]
|
|
27
|
+
|
|
28
|
+
self.ligand_group_to_gene_dict[group].append(ligand)
|
|
29
|
+
|
|
30
|
+
self.target_subunit: List[str] = row_series['target_subunit'].split('-')
|
|
31
|
+
self.target_subunit_group: List[int] = list(
|
|
32
|
+
map(lambda x: int(x), row_series['target_subunit_group'].split('-'))
|
|
33
|
+
)
|
|
34
|
+
self.target_subunit_coeff: List[int] = list(
|
|
35
|
+
map(lambda x: int(x), row_series['target_subunit_coeff'].split('-'))
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
self.target_group_to_gene_dict: Dict[int, List[str]] = {}
|
|
39
|
+
self.target_group_to_coeff_dict: Dict[int, int] = {}
|
|
40
|
+
for target, group in zip(self.target_subunit,
|
|
41
|
+
self.target_subunit_group):
|
|
42
|
+
if group not in self.target_group_to_gene_dict.keys():
|
|
43
|
+
self.target_group_to_gene_dict[group] = []
|
|
44
|
+
self.target_group_to_coeff_dict[group] = self.target_subunit_coeff[group-1]
|
|
45
|
+
|
|
46
|
+
self.target_group_to_gene_dict[group].append(target)
|
|
47
|
+
|
|
48
|
+
# Create sets of groups so that one can simply iterate over them
|
|
49
|
+
self.ligand_groups: Set[int] = set(self.ligand_contributor_group)
|
|
50
|
+
self.target_groups: Set[int] = set(self.target_subunit_group)
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
from multiprocessing import Pool
|
|
6
|
+
|
|
7
|
+
import loompy
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import scipy.stats as stats
|
|
11
|
+
import xarray as xr
|
|
12
|
+
|
|
13
|
+
from .InteractionDB import *
|
|
14
|
+
|
|
15
|
+
from typing import List, Tuple, Set, Dict, Optional
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def compute_subject_specific_avg_expression(
|
|
19
|
+
path_to_loom: str,
|
|
20
|
+
subject_id: str,
|
|
21
|
+
subject_label_column: str,
|
|
22
|
+
cell_type_label_column: str,
|
|
23
|
+
gene_label_row: str = 'Gene',
|
|
24
|
+
min_n_cells_threshold: int = -1,
|
|
25
|
+
mean_type: str = 'tri_mean',
|
|
26
|
+
trim_mean_fraction: Optional[float] = None,
|
|
27
|
+
) -> xr.DataArray:
|
|
28
|
+
"""
|
|
29
|
+
Compute the average expression of all genes for a single individual and all cell-types that are present in the loom
|
|
30
|
+
file. The average expression is computed using the Tukey's tri_mean.
|
|
31
|
+
|
|
32
|
+
If the number of cells for a specific cell-type is less than min_n_cells_threshold, the average expression for this
|
|
33
|
+
cell-type is set to NaN.
|
|
34
|
+
|
|
35
|
+
:param path_to_loom: Path to the loom file from which the average expression should be computed. Important: The loom file must not be open in write mode by any other thread!
|
|
36
|
+
:param subject_id: The subject id from the subject_label_column for which the average expression should be computed.
|
|
37
|
+
:param subject_label_column: The column in the loom file that contains the subject id.
|
|
38
|
+
:param cell_type_label_column: The column in the loom file that contains the cell-type labels.
|
|
39
|
+
:param gene_label_row: Name of the row attribute in the loom file that contains the gene labels (default: 'Gene')
|
|
40
|
+
:param min_n_cells_threshold: The minimum number of cells that are required for a cell-type to be included in the average expression computation. If the number of cells is less than min_n_cells_threshold, the average expression for this cell-type is set to NaN.
|
|
41
|
+
:param mean_type: The type of mean that should be computed. The mean can be either 'tri_mean', 'mean', or 'trim_mean'.
|
|
42
|
+
:param trim_mean_fraction: The fraction of the data that should be trimmed when the mean type is set to 'trim_mean'.
|
|
43
|
+
:return: A matrix of the average expression of all genes for the individual and all cell-types.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
if not os.path.isfile(path_to_loom):
|
|
47
|
+
raise FileNotFoundError(f"File {path_to_loom} not found")
|
|
48
|
+
|
|
49
|
+
if not path_to_loom.endswith('.loom'):
|
|
50
|
+
raise ValueError('The file must be a loom file')
|
|
51
|
+
|
|
52
|
+
if subject_id == '' or subject_id is None:
|
|
53
|
+
raise ValueError('The subject id must be provided')
|
|
54
|
+
|
|
55
|
+
if subject_label_column == '' or subject_label_column is None:
|
|
56
|
+
raise ValueError('The subject label column must be provided')
|
|
57
|
+
|
|
58
|
+
if cell_type_label_column == '' or cell_type_label_column is None:
|
|
59
|
+
raise ValueError('The cell type label column must be provided')
|
|
60
|
+
|
|
61
|
+
if mean_type not in ['tri_mean', 'mean', 'trim_mean']:
|
|
62
|
+
raise ValueError('The mean type must be either "tri_mean", "mean", or "trim_mean"')
|
|
63
|
+
if mean_type == 'trim_mean' and trim_mean_fraction is None:
|
|
64
|
+
raise ValueError('The trim_mean fraction must be provided when the mean type is set to "trim_mean"')
|
|
65
|
+
|
|
66
|
+
with loompy.connect(path_to_loom, mode='r') as data_loom:
|
|
67
|
+
# Get all genes and cell-types included in the loom file
|
|
68
|
+
genes: List[str] = data_loom.ra[gene_label_row].tolist()
|
|
69
|
+
cell_types: List[str] = list(set(data_loom.ca[cell_type_label_column]))
|
|
70
|
+
|
|
71
|
+
# Sort the cell-types
|
|
72
|
+
cell_types.sort()
|
|
73
|
+
|
|
74
|
+
# Initialize the average expression matrix
|
|
75
|
+
avg_expression: np.array = np.zeros(shape=(len(genes), len(cell_types)))
|
|
76
|
+
|
|
77
|
+
for i, cell_type in enumerate(cell_types):
|
|
78
|
+
# Get all cells with the specific cell-type and subject id
|
|
79
|
+
subject_and_cell_type_mask: np.array = (data_loom.ca[cell_type_label_column] == cell_type) & \
|
|
80
|
+
(data_loom.ca[subject_label_column] == subject_id)
|
|
81
|
+
|
|
82
|
+
# If there are no cells with the specific cell-type and subject id, set the average expression to NaN
|
|
83
|
+
if not np.any(subject_and_cell_type_mask):
|
|
84
|
+
warnings.warn(f'There are no cell-types with label "{cell_type}" for subject "{subject_id}"')
|
|
85
|
+
avg_expression[:, i] = np.nan
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
# If the number of cells with the specific cell-type and subject id is less than min_n_cells_threshold,
|
|
89
|
+
# set the average expression to NaN
|
|
90
|
+
if min_n_cells_threshold > 0 and np.sum(subject_and_cell_type_mask) < min_n_cells_threshold:
|
|
91
|
+
warnings.warn(f'There are less than {min_n_cells_threshold} cells with label "{cell_type}" for subject "{subject_id}"')
|
|
92
|
+
avg_expression[:, i] = np.nan
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
cell_type_matrix: np.array = data_loom[:, subject_and_cell_type_mask]
|
|
96
|
+
|
|
97
|
+
if mean_type == 'mean':
|
|
98
|
+
avg_expression[:, i] = np.mean(cell_type_matrix, axis=1)
|
|
99
|
+
elif mean_type == 'trim_mean':
|
|
100
|
+
avg_expression[:, i] = stats.trim_mean(cell_type_matrix, trim_mean_fraction, axis=1)
|
|
101
|
+
else:
|
|
102
|
+
# compute turkey's tri_mean
|
|
103
|
+
q1: np.array = np.percentile(cell_type_matrix, 25, axis=1)
|
|
104
|
+
q2: np.array = np.percentile(cell_type_matrix, 50, axis=1)
|
|
105
|
+
q3: np.array = np.percentile(cell_type_matrix, 75, axis=1)
|
|
106
|
+
|
|
107
|
+
tri_mean: np.array = (q1 + 2 * q2 + q3) / 4
|
|
108
|
+
|
|
109
|
+
avg_expression[:, i] = tri_mean
|
|
110
|
+
|
|
111
|
+
avg_expression_xr: xr.DataArray = xr.DataArray(data=avg_expression,
|
|
112
|
+
coords={'genes': genes, 'cell_types': cell_types},
|
|
113
|
+
dims=['genes', 'cell_types'])
|
|
114
|
+
|
|
115
|
+
return avg_expression_xr
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def compute_avg_expression(
|
|
119
|
+
path_to_loom: str,
|
|
120
|
+
|
|
121
|
+
condition_label_column: str,
|
|
122
|
+
condition_label_a: str,
|
|
123
|
+
condition_label_b: str,
|
|
124
|
+
|
|
125
|
+
subject_label_column: str,
|
|
126
|
+
cell_type_label_column: str,
|
|
127
|
+
|
|
128
|
+
min_n_cells_threshold: int = -1,
|
|
129
|
+
|
|
130
|
+
mean_type: str = 'trimean',
|
|
131
|
+
trim_mean_fraction: Optional[float] = None,
|
|
132
|
+
|
|
133
|
+
gene_label_row: str = 'Gene',
|
|
134
|
+
|
|
135
|
+
n_processes: Optional[int] = 1
|
|
136
|
+
) -> Dict[str, Dict[str, xr.DataArray]]:
|
|
137
|
+
"""
|
|
138
|
+
Compute the average expression of all genes for all individuals and all cell-types.
|
|
139
|
+
|
|
140
|
+
:param path_to_loom: Path to the loom file from which the average expression should be computed. Important: The loom file must not be open in write mode by any other thread!
|
|
141
|
+
:param condition_label_column: The column in the loom file that contains the condition label.
|
|
142
|
+
:param condition_label_a: The label of the first condition.
|
|
143
|
+
:param condition_label_b: The label of the second condition.
|
|
144
|
+
:param subject_label_column: The column in the loom file that contains the subject id.
|
|
145
|
+
:param cell_type_label_column: The column in the loom file that contains the cell-type labels.
|
|
146
|
+
:param min_n_cells_threshold: The minimum number of cells that are required for a cell-type to be included in the average expression computation. If the number of cells is less than min_n_cells_threshold, the average expression for this cell-type is set to NaN.
|
|
147
|
+
:param mean_type: The type of mean that should be computed. The mean can be either 'tri_mean', 'mean', or 'trim_mean'.
|
|
148
|
+
:param trim_mean_fraction: The fraction of the data that should be trimmed when the mean type is set to 'trim_mean'.
|
|
149
|
+
:param gene_label_row: Name of the row attribute in the loom file that contains the gene labels (default: 'Gene')
|
|
150
|
+
:param n_processes: The number of processes that should be used for the computation. If n_processes is set to 1, the computation is done in a single process.
|
|
151
|
+
:return: A dictionary with keys 'condition_label_a' and 'condition_label_b'. Each key contains a dictionary with the subject id as key and a matrix of the average expression as value.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
if not os.path.isfile(path_to_loom):
|
|
155
|
+
raise FileNotFoundError(f"File {path_to_loom} not found")
|
|
156
|
+
|
|
157
|
+
if not path_to_loom.endswith('.loom'):
|
|
158
|
+
raise ValueError('The file must be a loom file')
|
|
159
|
+
|
|
160
|
+
if condition_label_a == condition_label_b:
|
|
161
|
+
raise ValueError('The condition labels must be different')
|
|
162
|
+
|
|
163
|
+
if condition_label_a == '' or condition_label_a is None:
|
|
164
|
+
raise ValueError('The condition label a must be provided')
|
|
165
|
+
|
|
166
|
+
if condition_label_b == '' or condition_label_b is None:
|
|
167
|
+
raise ValueError('The condition label b must be provided')
|
|
168
|
+
|
|
169
|
+
if subject_label_column == '' or subject_label_column is None:
|
|
170
|
+
raise ValueError('The subject label column must be provided')
|
|
171
|
+
|
|
172
|
+
if cell_type_label_column == '' or cell_type_label_column is None:
|
|
173
|
+
raise ValueError('The cell type label column must be provided')
|
|
174
|
+
|
|
175
|
+
if n_processes <= 0:
|
|
176
|
+
raise ValueError('The number of processes must be greater than zero')
|
|
177
|
+
|
|
178
|
+
with loompy.connect(path_to_loom, mode='r') as data_loom:
|
|
179
|
+
# containing all subjects regardless of the conditon label that were provided
|
|
180
|
+
all_subjects: List[str] = list(set(data_loom.ca[subject_label_column]))
|
|
181
|
+
|
|
182
|
+
# Create a dictionary of all subjects to their condition
|
|
183
|
+
# Important: we only keep those subjects that have either condition_label_a or condition_label_b
|
|
184
|
+
subjects_to_condition_dict: Dict[str, str] = {
|
|
185
|
+
subject: data_loom.ca[data_loom.ca[subject_label_column] == subject][condition_label_column][0]
|
|
186
|
+
for subject in all_subjects
|
|
187
|
+
}
|
|
188
|
+
subjects_to_condition_dict = {
|
|
189
|
+
key: value
|
|
190
|
+
for key, value in subjects_to_condition_dict.items()
|
|
191
|
+
if value in [condition_label_a, condition_label_b]
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
subjects: List[str] = list(subjects_to_condition_dict.keys())
|
|
195
|
+
|
|
196
|
+
# Multiprocessing of avg_expression computation
|
|
197
|
+
multiprocessing_pool_input: List[Tuple[str, str, str, str, str, int, str, float]] = [
|
|
198
|
+
(
|
|
199
|
+
path_to_loom,
|
|
200
|
+
subject,
|
|
201
|
+
subject_label_column,
|
|
202
|
+
cell_type_label_column,
|
|
203
|
+
gene_label_row,
|
|
204
|
+
min_n_cells_threshold,
|
|
205
|
+
mean_type,
|
|
206
|
+
trim_mean_fraction
|
|
207
|
+
)
|
|
208
|
+
for subject in subjects
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
if n_processes == 1:
|
|
212
|
+
avg_expressions_list: List[xr.DataArray] = [
|
|
213
|
+
compute_subject_specific_avg_expression(*method_input)
|
|
214
|
+
for method_input in multiprocessing_pool_input
|
|
215
|
+
|
|
216
|
+
]
|
|
217
|
+
else:
|
|
218
|
+
with Pool(processes=n_processes) as pool:
|
|
219
|
+
avg_expressions_list: List[xr.DataArray] = pool.starmap(
|
|
220
|
+
compute_subject_specific_avg_expression,
|
|
221
|
+
multiprocessing_pool_input
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Create a dictionary of dictionaries containing the average expression matrix for all subjects separated by
|
|
225
|
+
# the condition.
|
|
226
|
+
avg_expressions: Dict[str, Dict[str, xr.DataArray]] = {
|
|
227
|
+
condition_label_a: {
|
|
228
|
+
subject: avg_expressions_list[i] for i, subject in enumerate(subjects)
|
|
229
|
+
if subjects_to_condition_dict[subject] == condition_label_a
|
|
230
|
+
},
|
|
231
|
+
condition_label_b: {
|
|
232
|
+
subject: avg_expressions_list[i] for i, subject in enumerate(subjects)
|
|
233
|
+
if subjects_to_condition_dict[subject] == condition_label_b
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return avg_expressions
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def compute_subject_specific_communication_score_matrix(
|
|
241
|
+
avg_expression: xr.DataArray,
|
|
242
|
+
interaction_db: InteractionDB
|
|
243
|
+
) -> Tuple[xr.DataArray, Tuple[xr.DataArray, xr.DataArray]]:
|
|
244
|
+
"""
|
|
245
|
+
Compute the communication score matrix for a single individual based on the average expression of all genes and the
|
|
246
|
+
interaction database.
|
|
247
|
+
|
|
248
|
+
The communication score matrix is computed as follows:
|
|
249
|
+
Step 1: Compute the abundance of the ligand and target subunit for each interaction. The abundance of a single
|
|
250
|
+
production step is the defined as the arithmetic mean of the isoenzyme gene expression. Each production
|
|
251
|
+
step is then multiplied by the coefficient of the production step. Then, the geometric mean over all
|
|
252
|
+
production steps is computed.
|
|
253
|
+
Step 2: Compute the communication score for each interaction pair. The communication score is computed
|
|
254
|
+
as the product of the abundance of the ligand and target subunit.
|
|
255
|
+
|
|
256
|
+
:param avg_expression: A matrix describing the average expression of all synthesizing genes for each cell-type.
|
|
257
|
+
:param interaction_db: The interaction database that contains all interactions.
|
|
258
|
+
:return: A three-dimensional matrix of the communication score for each cell-type pair and
|
|
259
|
+
ligand-target pair interaction. Secondary, a tuple of two matrices containing the abundance of the ligand
|
|
260
|
+
and target subunit for each interaction.
|
|
261
|
+
"""
|
|
262
|
+
genes: Set[str] = set(avg_expression['genes'].values.tolist())
|
|
263
|
+
cell_types: List[str] = avg_expression['cell_types'].values.tolist()
|
|
264
|
+
interaction_names: List[str] = interaction_db.get_interaction_names()
|
|
265
|
+
|
|
266
|
+
n_cell_types: int = len(cell_types)
|
|
267
|
+
n_interactions: int = len(interaction_names)
|
|
268
|
+
|
|
269
|
+
ligand_abundance: np.array = np.zeros(shape=(n_interactions, n_cell_types))
|
|
270
|
+
target_abundance: np.array = np.zeros(shape=(n_interactions, n_cell_types))
|
|
271
|
+
|
|
272
|
+
# Iterate through all interaction pairs
|
|
273
|
+
for i, interaction_name in enumerate(interaction_names):
|
|
274
|
+
interaction_info: InteractionDBRow = interaction_db[interaction_name]
|
|
275
|
+
|
|
276
|
+
# If either there are no genes present for the ligand production or target subunit production:
|
|
277
|
+
# skip this interaction
|
|
278
|
+
if (len(set(interaction_info.ligand_contributor).intersection(genes)) == 0) or (len(set(interaction_info.target_subunit).intersection(genes)) == 0):
|
|
279
|
+
# Set the communication score to NaN as the interaction cannot be computed
|
|
280
|
+
ligand_abundance[i, :] = np.nan
|
|
281
|
+
target_abundance[i, :] = np.nan
|
|
282
|
+
continue
|
|
283
|
+
|
|
284
|
+
group_coeff_sum: int = 0
|
|
285
|
+
for ligand_group in interaction_info.ligand_groups:
|
|
286
|
+
group_genes: Set[str] = set(interaction_info.ligand_group_to_gene_dict[ligand_group])
|
|
287
|
+
group_coeff: int = interaction_info.ligand_group_to_coeff_dict[ligand_group]
|
|
288
|
+
|
|
289
|
+
group_coeff_sum += group_coeff
|
|
290
|
+
|
|
291
|
+
available_genes: List[str] = list(set(group_genes).intersection(genes))
|
|
292
|
+
|
|
293
|
+
if len(available_genes) == 0:
|
|
294
|
+
ligand_abundance[i, :] += 0
|
|
295
|
+
else:
|
|
296
|
+
# TODO potentially rewrite this to handle zeros in the log more elegantly?
|
|
297
|
+
with warnings.catch_warnings():
|
|
298
|
+
# Ignore warnings that are raised when the mean of an empty slice is calculated
|
|
299
|
+
warnings.filterwarnings(action='ignore', message='Mean of empty slice')
|
|
300
|
+
# Ignore warnings that are raised when the log of zero is calculated
|
|
301
|
+
warnings.filterwarnings(action='ignore', message='divide by zero encountered in log')
|
|
302
|
+
|
|
303
|
+
group_score: np.array = np.mean(avg_expression.loc[available_genes, cell_types], axis=0)
|
|
304
|
+
ligand_abundance[i, :] += group_coeff * np.log(group_score)
|
|
305
|
+
|
|
306
|
+
ligand_abundance[i, :] = np.exp(ligand_abundance[i, :] / group_coeff_sum)
|
|
307
|
+
|
|
308
|
+
group_coeff_sum: int = 0
|
|
309
|
+
for target_group in interaction_info.target_groups:
|
|
310
|
+
group_genes: List[str] = interaction_info.target_group_to_gene_dict[target_group]
|
|
311
|
+
group_coeff: int = interaction_info.target_group_to_coeff_dict[target_group]
|
|
312
|
+
|
|
313
|
+
group_coeff_sum += group_coeff
|
|
314
|
+
|
|
315
|
+
available_genes: List[str] = list(set(group_genes).intersection(genes))
|
|
316
|
+
|
|
317
|
+
if len(available_genes) == 0:
|
|
318
|
+
target_abundance[i, :] += 0
|
|
319
|
+
else:
|
|
320
|
+
# TODO potentially rewrite this to handle zeros in the log more elegantly?
|
|
321
|
+
with warnings.catch_warnings():
|
|
322
|
+
# Ignore warnings that are raised when the mean of an empty slice is calculated
|
|
323
|
+
warnings.filterwarnings(action='ignore', message='Mean of empty slice')
|
|
324
|
+
# Ignore warnings that are raised when the log of zero is calculated
|
|
325
|
+
warnings.filterwarnings(action='ignore', message='divide by zero encountered in log')
|
|
326
|
+
|
|
327
|
+
group_score: float = np.mean(avg_expression.loc[available_genes, cell_types], axis=0)
|
|
328
|
+
target_abundance[i, :] += group_coeff * np.log(group_score)
|
|
329
|
+
|
|
330
|
+
target_abundance[i, :] = np.exp(target_abundance[i, :] / group_coeff_sum)
|
|
331
|
+
|
|
332
|
+
communication_score_matrix: xr.DataArray = xr.DataArray(
|
|
333
|
+
data=np.zeros(shape=(n_cell_types, n_cell_types, n_interactions), dtype=float),
|
|
334
|
+
dims=['source', 'receiver', 'ligand_target_interactions'],
|
|
335
|
+
coords=[cell_types, cell_types, interaction_names]
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
ligand_abundance_matrix: xr.DataArray = xr.DataArray(
|
|
339
|
+
data=ligand_abundance.T,
|
|
340
|
+
dims=['cell_types', 'ligand_target_interactions'],
|
|
341
|
+
coords=[cell_types, interaction_names]
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
target_abundance_matrix: xr.DataArray = xr.DataArray(
|
|
345
|
+
data=target_abundance.T,
|
|
346
|
+
dims=['cell_types', 'ligand_target_interactions'],
|
|
347
|
+
coords=[cell_types, interaction_names]
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
for s, source in enumerate(cell_types):
|
|
351
|
+
for r, receiver in enumerate(cell_types):
|
|
352
|
+
communication_score_matrix.loc[source, receiver, :] = ligand_abundance[:, s] * target_abundance[:, r]
|
|
353
|
+
|
|
354
|
+
return communication_score_matrix, (ligand_abundance_matrix, target_abundance_matrix)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def compute_communication_score_matrix(
|
|
358
|
+
avg_expression_dict: Dict[str, Dict[str, xr.DataArray]],
|
|
359
|
+
interaction_db: InteractionDB,
|
|
360
|
+
n_processes: Optional[int] = 1
|
|
361
|
+
) -> Tuple[Dict[str, Dict[str, xr.DataArray]], Dict[str, Dict[str, xr.DataArray]], Dict[str, Dict[str, xr.DataArray]]]:
|
|
362
|
+
"""
|
|
363
|
+
Compute the communication score matrices for each condition, subject, and cell-type pair.
|
|
364
|
+
|
|
365
|
+
:param avg_expression_dict: A dictionary containing the average expression off all subjects for each condition. The first key is the condition, the second key is the subject id, and the value is a matrix of the average expression for each cell-type pair and ligand-target pair.
|
|
366
|
+
:param interaction_db: The interaction database that contains all interactions.
|
|
367
|
+
:param n_processes: The number of processes that should be used for the computation. If n_processes is set to 1, the computation is done in a single process.
|
|
368
|
+
:return: A triplet of dictionaries containing the communication score matrix, the ligand abundance matrix, and the target abundance matrix. The first key is the condition, the second key is the subject id, and the value is a matrix of the communication score, ligand abundance, or target abundance, split by condition and subject id.
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
if n_processes <= 0:
|
|
372
|
+
raise ValueError('The number of processes must be greater than zero')
|
|
373
|
+
|
|
374
|
+
# Create a dictionary to look up the condition for a specific subject
|
|
375
|
+
subject_to_condition_dict: Dict[str, str] = {
|
|
376
|
+
subject: condition
|
|
377
|
+
for condition in avg_expression_dict.keys()
|
|
378
|
+
for subject in avg_expression_dict[condition].keys()
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
conditions: List[str] = list(avg_expression_dict.keys())
|
|
382
|
+
subjects: List[str] = list(subject_to_condition_dict.keys())
|
|
383
|
+
|
|
384
|
+
multiprocessing_pool_input: List[Tuple[xr.DataArray, InteractionDB]] = [
|
|
385
|
+
(avg_expression_dict[subject_to_condition_dict[subject]][subject], interaction_db)
|
|
386
|
+
for subject in subjects
|
|
387
|
+
]
|
|
388
|
+
|
|
389
|
+
# Multiprocessing of communication score computation
|
|
390
|
+
if n_processes == 1:
|
|
391
|
+
communication_score_matrices: List[Tuple[xr.DataArray, Tuple[xr.DataArray, xr.DataArray]]] = [
|
|
392
|
+
compute_subject_specific_communication_score_matrix(*method_input)
|
|
393
|
+
for method_input in multiprocessing_pool_input
|
|
394
|
+
]
|
|
395
|
+
else:
|
|
396
|
+
with Pool(processes=n_processes) as pool:
|
|
397
|
+
communication_score_matrices: List[Tuple[xr.DataArray, Tuple[xr.DataArray, xr.DataArray]]] = pool.starmap(
|
|
398
|
+
compute_subject_specific_communication_score_matrix,
|
|
399
|
+
multiprocessing_pool_input
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# Create a dictionary of dictionaries containing the communication score matrix for all subjects separated by
|
|
403
|
+
# the condition.
|
|
404
|
+
communication_scores_dict: Dict[str, Dict[str, xr.DataArray]] = {
|
|
405
|
+
conditions[0]: {
|
|
406
|
+
subject: communication_score_matrices[i][0]
|
|
407
|
+
for i, subject in enumerate(subjects)
|
|
408
|
+
if subject_to_condition_dict[subject] == conditions[0]
|
|
409
|
+
},
|
|
410
|
+
conditions[1]: {
|
|
411
|
+
subject: communication_score_matrices[i][0]
|
|
412
|
+
for i, subject in enumerate(subjects)
|
|
413
|
+
if subject_to_condition_dict[subject] == conditions[1]
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
# Create a dictionary of dictionaries containing the ligand abundance matrix for all subjects separated by
|
|
418
|
+
# the condition.
|
|
419
|
+
ligand_abundance_dict: Dict[str, Dict[str, xr.DataArray]] = {
|
|
420
|
+
conditions[0]: {
|
|
421
|
+
subject: communication_score_matrices[i][1][0]
|
|
422
|
+
for i, subject in enumerate(subjects)
|
|
423
|
+
if subject_to_condition_dict[subject] == conditions[0]
|
|
424
|
+
},
|
|
425
|
+
conditions[1]: {
|
|
426
|
+
subject: communication_score_matrices[i][1][0]
|
|
427
|
+
for i, subject in enumerate(subjects)
|
|
428
|
+
if subject_to_condition_dict[subject] == conditions[1]
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
# Create a dictionary of dictionaries containing the target abundance matrix for all subjects separated by
|
|
433
|
+
# the condition.
|
|
434
|
+
target_abundance_dict: Dict[str, Dict[str, xr.DataArray]] = {
|
|
435
|
+
conditions[0]: {
|
|
436
|
+
subject: communication_score_matrices[i][1][1]
|
|
437
|
+
for i, subject in enumerate(subjects)
|
|
438
|
+
if subject_to_condition_dict[subject] == conditions[0]
|
|
439
|
+
},
|
|
440
|
+
conditions[1]: {
|
|
441
|
+
subject: communication_score_matrices[i][1][1]
|
|
442
|
+
for i, subject in enumerate(subjects)
|
|
443
|
+
if subject_to_condition_dict[subject] == conditions[1]
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return communication_scores_dict, ligand_abundance_dict, target_abundance_dict
|