File size: 11,599 Bytes
932f7e0 cbddf96 932f7e0 cbddf96 932f7e0 cbddf96 932f7e0 cbddf96 932f7e0 cbddf96 932f7e0 cbddf96 932f7e0 cbddf96 932f7e0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 | import argparse
import ast
import libsbml
import os
import re
import numpy as np
import pandas as pd
import sys
from utils.utils import *
def extract_genes_from_gpa(gpa_str):
"""Extract all gene IDs from a gpaAssociation string (e.g. 'G_A or (G_B and G_C)')."""
if pd.isna(gpa_str) or str(gpa_str).strip() == "":
return []
tokens = re.findall(r'[A-Za-z_]\w*', str(gpa_str))
keywords = {'or', 'and', 'not'}
return [t for t in tokens if t.lower() not in keywords]
def parse_genes(gene_value):
"""Parse either a stringified gene list or a boolean gene expression."""
if pd.isna(gene_value) or str(gene_value).strip() == "":
return []
if isinstance(gene_value, (list, tuple, set)):
return list(gene_value)
try:
parsed = ast.literal_eval(str(gene_value))
except (ValueError, SyntaxError):
return extract_genes_from_gpa(gene_value)
if isinstance(parsed, (list, tuple, set)):
return list(parsed)
return extract_genes_from_gpa(parsed)
def read_combined_geneExpr(combined_geneExpr_filename):
if not os.path.isfile(combined_geneExpr_filename):
raise FileNotFoundError(f"Missing combined normalized count file, expected at {combined_geneExpr_filename}")
combined_geneExpr_df = pd.read_csv(combined_geneExpr_filename, index_col=0)
return combined_geneExpr_df
def is_real_gene_with_mapping(gene):
return not gene.startswith(("unmapped", "unknown", "Spontaneous", "Exchange", "Sink", "Diffusion"))
def read_system_gene(model_pre_filename):
rxn_df = read_model_excel(model_pre_filename, "Reactions")
# Support both stringified lists and boolean expressions in 'genes'.
if "genes" in rxn_df.columns:
gene_col = "genes"
elif "gpaAssociation" in rxn_df.columns:
gene_col = "gpaAssociation"
else:
raise ValueError(
"Reactions sheet must have a 'genes' or 'gpaAssociation' column — "
f"columns found: {list(rxn_df.columns)}"
)
system_genes_dict = {}
missing_gene_system_dict = {}
all_genes_set = set()
for system, gene_list_raw in zip(rxn_df["system"], rxn_df[gene_col]):
gene_list = parse_genes(gene_list_raw)
if system != "" and system not in system_genes_dict:
system_genes_dict[system] = set()
for gene in gene_list:
if system != "" and is_real_gene_with_mapping(gene):
system_genes_dict[system].add(gene)
if gene.startswith(("unmapped", "unknown")):
missing_gene_system_dict[gene] = system
if is_real_gene_with_mapping(gene):
all_genes_set.add(gene)
missing_gene_system_dict = dict(sorted(missing_gene_system_dict.items()))
return system_genes_dict, missing_gene_system_dict, all_genes_set
def map_genes(mapping_dict, system_genes_dict, missing_gene_system_dict, all_genes_set):
"""Map model gene tags to expression IDs and retain unmapped tags for bootstrapping."""
mapped_system_genes = {}
missing_genes = dict(missing_gene_system_dict)
for system, gene_set in system_genes_dict.items():
mapped_genes = set()
for gene in gene_set:
if gene in mapping_dict:
mapped_genes.add(mapping_dict[gene])
else:
missing_genes.setdefault(gene, system)
if mapped_genes:
mapped_system_genes[system] = mapped_genes
mapped_all_genes = {mapping_dict[gene] for gene in all_genes_set if gene in mapping_dict}
return mapped_system_genes, dict(sorted(missing_genes.items())), mapped_all_genes
def filter_geneExpr_df(combined_geneExpr_df, all_genes_set):
geneExpr_df = combined_geneExpr_df.loc[combined_geneExpr_df.index.isin(all_genes_set)]
return geneExpr_df
def get_system_gene_counts(system_genes_dict, gene_count_dict):
system_gene_counts_dict = {}
missing_genes = set()
for system, gene_set in system_genes_dict.items():
gene_counts = []
for gene in sorted(gene_set):
if gene in gene_count_dict:
gene_counts.append(gene_count_dict[gene])
else:
missing_genes.add(gene)
system_gene_counts_dict[system] = gene_counts
if missing_genes:
print("Warning : These genes do not have normalized count values - {}".format(", ".join(missing_genes)))
return system_gene_counts_dict
def initialize_df(geneExpr_sample, columns):
new_geneExpr_df_sample = pd.concat([geneExpr_sample.rename(col) for col in columns], axis=1)
return new_geneExpr_df_sample
def bootstrap_missing_genes(missing_gene_system_dict, system_gene_counts_dict, columns):
rng = np.random.default_rng(seed=0)
sample_pool_all_genes = [gc for gene_counts in system_gene_counts_dict.values() for gc in gene_counts]
new_geneExpr_df_sample_missing = pd.DataFrame(columns=columns)
for missing_gene, system in missing_gene_system_dict.items():
if system in system_gene_counts_dict:
sample_pool = system_gene_counts_dict[system]
new_geneExpr_df_sample_missing.loc[missing_gene] = rng.choice(sample_pool, len(columns))
else:
new_geneExpr_df_sample_missing.loc[missing_gene] = rng.choice(sample_pool_all_genes, len(columns))
return new_geneExpr_df_sample_missing
def filter_by_batch(new_geneExpr_df, start, end, columns):
target_columns = [f"{col}_{i}" for col in columns for i in range(start, end)]
new_geneExpr_df_batch_df = new_geneExpr_df[target_columns]
return new_geneExpr_df_batch_df
def bootstrap_genes(model_pre_filenames, mapping_filenames, species_prefixes, combined_geneExpr_filename, geneExpr_folder, batch_count=1000):
print("Read models from", ", ".join(model_pre_filenames))
print("Read mapping tables from", ", ".join(mapping_filenames))
print("Species prefixes:", ", ".join(species_prefixes))
print("Read gene normalized counts from", combined_geneExpr_filename)
os.makedirs(geneExpr_folder, exist_ok=True)
total_to_sample = batch_count
# column: sample_name
# row: real genes for all species
combined_geneExpr_df = read_combined_geneExpr(combined_geneExpr_filename)
# column: A_1, A_2, ..., B_1, B_2, ...
# row: (real genes with mapping + unmapped real genes + unknown genes) + Exchange + Sink for all species
new_geneExpr_df = pd.DataFrame()
for model_pre_filename, mapping_filename, species in zip(model_pre_filenames, mapping_filenames, species_prefixes): # loop by species
print(f"Bootstrapping genes for {species}...")
# mapping_dict: {model_tag in the model file: gene_id in the geneExpr file}
mapping_dict = read_mapping(mapping_filename)
# system_genes_dict: {system: set of real genes with mapping with that system}
# missing_gene_system_dict: {unmapped real gene & unknown gene: system of the gene}
# all_genes_set: set of all the real genes with mapping used in the model of that species
system_genes_dict, missing_gene_system_dict, all_genes_set = read_system_gene(model_pre_filename)
# Map model tags to the annotation IDs used in the expression file.
system_genes_dict, missing_gene_system_dict, all_genes_set = map_genes(
mapping_dict, system_genes_dict, missing_gene_system_dict, all_genes_set
)
# filter to get only geneExprs for that species
geneExpr_df = filter_geneExpr_df(combined_geneExpr_df, all_genes_set)
# column: A_1, A_2, ..., B_1, B_2, ...
# row: (real genes with mapping + unmapped real genes + unknown genes) for one species
new_geneExpr_df_species = pd.DataFrame()
for sample in geneExpr_df.columns:
# gene_count_dict: {gene: gene count}
gene_count_dict = geneExpr_df[sample].to_dict()
# system_gene_counts_dict: {system: list of gene counts of the genes with that system}
system_gene_counts_dict = get_system_gene_counts(system_genes_dict, gene_count_dict)
# column: (one sample) A_1, A_2, ..
columns = [f"{sample}_{i+1}" for i in range(total_to_sample)]
# row: real genes with mapping (same as geneExpr file)
new_geneExpr_df_sample = initialize_df(geneExpr_df[sample], columns)
# row: unmapped real genes + unknown genes
# (randomly bootstrap from the gene counts of the same species and the same sample and the same system)
# (if the system of the missing gene do not have gene counts, randomly bootstrap from the gene counts of the same species and the same sample)
new_geneExpr_df_sample_missing = bootstrap_missing_genes(missing_gene_system_dict, system_gene_counts_dict, columns)
# concat by rows: real genes with mapping + unmapped real genes + unknown genes
new_geneExpr_df_sample = pd.concat([new_geneExpr_df_sample, new_geneExpr_df_sample_missing], axis=0)
# concat by columns: A_1, A_2, ..., B_1, B_2, ...
new_geneExpr_df_species = pd.concat([new_geneExpr_df_species, new_geneExpr_df_sample], axis=1)
# concat by rows: species_1 + species_2, ...
new_geneExpr_df = pd.concat([new_geneExpr_df, new_geneExpr_df_species], axis=0)
new_geneExpr_df.loc["Spontaneous"] = 0
new_geneExpr_df.loc["Exchange"] = 0
new_geneExpr_df.loc["Sink"] = 0
new_geneExpr_df.loc["Growth"] = 0
new_geneExpr_df.loc["Diffusion"] = 0
# separate into batch to different files
print("Writing to files...")
new_geneExpr_df_batch_filename = os.path.join(geneExpr_folder, "geneExpr")
for i in range(batch_count):
# first file: A_1, B_1, ..
# second file: A_2, B_2,
new_geneExpr_df_batch_df = filter_by_batch(new_geneExpr_df, i+1, (i+1)+1, combined_geneExpr_df.columns)
new_geneExpr_df_batch_df.to_csv(new_geneExpr_df_batch_filename + f"_{i+1}.csv")
print(f"Write to {new_geneExpr_df_batch_filename}_<1_{batch_count}>.csv")
if __name__ == "__main__":
# define arguments
parser = argparse.ArgumentParser()
parser.add_argument("--model_pre_filenames", nargs="+", required=True, help="List of parsed model files from construct_modularized_community_model tool")
parser.add_argument("--mapping_filenames", nargs="+", required=True, help="List of mapping files to map gene in model to gene in annotation, from identifiers_mapping tool")
parser.add_argument("--species_prefixes", nargs="+", required=True, help="List of species prefixes for the models, in the same order as input files")
parser.add_argument("--combined_geneExpr_filename", required=True, help="The gene expression values for all species and for all samples")
parser.add_argument("--geneExpr_folder", required=True, help="Folder containing gene expression files (geneExpr_<1-batch_count>.csv) with bootstrapped values added for unmapped and unknown genes")
parser.add_argument("--batch_count", type=int, default=1000, help="Number of batch to bootstrap, write each batch to a file")
args = parser.parse_args()
# read arguments
model_pre_filenames = args.model_pre_filenames
mapping_filenames = args.mapping_filenames
species_prefixes = args.species_prefixes
combined_geneExpr_filename = args.combined_geneExpr_filename
geneExpr_folder = args.geneExpr_folder
batch_count = args.batch_count
bootstrap_genes(model_pre_filenames, mapping_filenames, species_prefixes, combined_geneExpr_filename, geneExpr_folder, batch_count)
|