Skip to content

Commit efdcb2d

Browse files
committed
updated algorithm for the data augmentation
1 parent bd1bdc7 commit efdcb2d

1 file changed

Lines changed: 104 additions & 91 deletions

File tree

scripts/databases/generate_noisy_probes.py

Lines changed: 104 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,6 @@
77
from typing import Tuple, List, Dict
88
import argparse
99

10-
def load_formamide_probabilities(file_path: str) -> Tuple[List[float], List[float]]:
11-
"""Load formamide probabilities from TSV file and normalize them."""
12-
# Read the file with explicit float conversion
13-
df = pd.read_csv(file_path, sep=r'\s+', header=None, names=['formamide', 'probability'])
14-
15-
# Convert to float explicitly and handle any potential NaN values
16-
df['probability'] = pd.to_numeric(df['probability'], errors='coerce')
17-
df = df.dropna() # Remove any rows with NaN values
18-
19-
if df.empty:
20-
raise ValueError("No valid probability values found in the formamide file")
21-
22-
# Convert probabilities to numpy array and normalize
23-
probs = df['probability'].values.astype(float)
24-
25-
# Check for any invalid values
26-
if np.any(np.isnan(probs)) or np.any(np.isinf(probs)):
27-
raise ValueError("Invalid probability values found (NaN or Inf)")
28-
29-
# Normalize probabilities
30-
total = np.sum(probs)
31-
if total <= 0:
32-
raise ValueError("Sum of probabilities must be positive")
33-
34-
probs = probs / total
35-
36-
# Verify the probabilities sum to 1 (within numerical precision)
37-
if not np.isclose(np.sum(probs), 1.0, rtol=1e-5):
38-
raise ValueError("Probabilities do not sum to 1 after normalization")
39-
40-
return df['formamide'].tolist(), probs.tolist()
41-
4210
def clean_sequence(sequence: str) -> str:
4311
"""Clean sequence by removing non-ACGT characters and converting to uppercase."""
4412
sequence = sequence.strip().upper()
@@ -52,34 +20,47 @@ def calculate_gc_content(sequence: str) -> int:
5220
gc_count = sequence.count('G') + sequence.count('C')
5321
return round((gc_count / len(sequence)) * 100)
5422

55-
def apply_mutations(sequence: str,
23+
def apply_mutations_sequence(sequence: str,
5624
insertion_rate: float = 0.01,
5725
deletion_rate: float = 0.01,
58-
mutation_rate: float = 0.1) -> str:
26+
mutation_rate: float = 0.1,
27+
values: list=[None]) -> str:
5928
"""Apply random mutations to the sequence."""
6029
nucleotides = ['A', 'C', 'G', 'T']
6130
result = []
6231

63-
for base in sequence:
64-
# Apply mutations based on probabilities
65-
if random.random() < mutation_rate:
66-
# SNP mutation
67-
new_base = random.choice([n for n in nucleotides if n != base])
68-
result.append(new_base)
69-
else:
70-
result.append(base)
71-
72-
# Insertion after current position
73-
if random.random() < insertion_rate:
74-
result.append(random.choice(nucleotides))
75-
76-
# Deletion of current position
77-
if random.random() < deletion_rate:
78-
if result:
79-
result.pop()
32+
while sequence == ''.join(result):
33+
for base in sequence:
34+
# Apply mutations based on probabilities
35+
if random.random() < mutation_rate:
36+
# SNP mutation
37+
new_base = random.choice([n for n in nucleotides if n != base])
38+
result.append(new_base)
39+
else:
40+
result.append(base)
41+
42+
# Insertion after current position
43+
if random.random() < insertion_rate:
44+
result.append(random.choice(nucleotides))
45+
46+
# Deletion of current position
47+
if random.random() < deletion_rate:
48+
if result:
49+
result.pop()
8050

8151
return ''.join(result)
8252

53+
def apply_mutations_experiment(sequence,
54+
values: list=[None],
55+
insertion_rate: float = 0.01,
56+
deletion_rate: float = 0.01,
57+
mutation_rate: float = 0.1):
58+
"""Apply random mutations to the experiment parameters."""
59+
if random.random() < mutation_rate:
60+
return random.choice(values)
61+
else:
62+
return None
63+
8364
def process_probe_data(df: pd.DataFrame) -> List[Dict]:
8465
"""Process the input DataFrame into a list of probe dictionaries."""
8566
probes = []
@@ -103,18 +84,37 @@ def process_probe_data(df: pd.DataFrame) -> List[Dict]:
10384

10485
def generate_noisy_probes(input_file: str,
10586
output_file: str,
106-
formamide_file: str,
87+
mutation_number: int = 1,
88+
obligate_mutations: list = ["Sequence"],
89+
facultative_mutations: list = ["Formamide [%]","Modified version(s)"],
10790
insertion_rate: float = 0.01,
10891
deletion_rate: float = 0.01,
10992
mutation_rate: float = 0.1,
11093
iterations: int = 1) -> None:
111-
"""Generate noisy probe data with mutations and formamide variations."""
112-
113-
# Load formamide probabilities
114-
formamide_values, formamide_probs = load_formamide_probabilities(formamide_file)
115-
94+
"""Generate noisy probe data with sequence mutations and experiment parameter variations.
95+
Parameters:
96+
input_file (str): Path to the input CSV file containing probe data.
97+
output_file (str): Path to the output file where noisy probes will be saved.
98+
insertion_rate (float, optional): Probability of inserting a nucleotide at each position. Default is 0.01.
99+
deletion_rate (float, optional): Probability of deleting a nucleotide at each position. Default is 0.01.
100+
mutation_rate (float, optional): Probability of introducing a SNP at each position. Default is 0.1.
101+
iterations (int, optional): Number of noisy probe sets to generate. Default is 1.
102+
"""
103+
116104
# Read input probe data
117105
df = pd.read_csv(input_file, header=None)
106+
# Prepare
107+
mutation_list = [*obligate_mutations, *facultative_mutations]
108+
mutation_functions = {
109+
"Sequence":apply_mutations_sequence,
110+
"Formamide [%]":apply_mutations_experiment,
111+
"Modified version(s)":apply_mutations_experiment
112+
}
113+
mutation_values = {
114+
"Sequence":None,
115+
"Formamide [%]":df.loc[df[0] == "Formamide [%]", 1].tolist(),
116+
"Modified version(s)":df.loc[df[0] == "Modified version(s)", 1].tolist()
117+
}
118118

119119
# Process probes
120120
probes = process_probe_data(df)
@@ -129,34 +129,43 @@ def generate_noisy_probes(input_file: str,
129129

130130
# Clean sequence
131131
clean_seq = clean_sequence(probe['Sequence'])
132+
probe['Sequence'] = clean_seq
132133

133134
# Apply mutations
134-
mutated_seq = apply_mutations(clean_seq,
135-
insertion_rate,
136-
deletion_rate,
137-
mutation_rate)
135+
for OM in obligate_mutations:
136+
tmp = mutation_functions[OM]
137+
probe[OM] = tmp(sequence = probe[OM],
138+
insertion_rate = insertion_rate,
139+
deletion_rate = deletion_rate,
140+
mutation_rate = mutation_rate,
141+
values = mutation_values[OM])
142+
143+
if mutation_number < len(mutation_list):
144+
OMs = random.choices(mutation_list, k=mutation_number)
145+
else:
146+
add = ["Sequence"]*(mutation_number - len(mutation_list))
147+
OMs = [*mutation_list, *add]
138148

149+
for OM in OMs:
150+
tmp = mutation_functions[OM]
151+
probe[OM] = tmp(sequence = probe[OM],
152+
insertion_rate = insertion_rate,
153+
deletion_rate = deletion_rate,
154+
mutation_rate = mutation_rate,
155+
values = mutation_values[OM])
156+
139157
# Calculate new properties
140-
new_length = len(mutated_seq)
141-
new_gc_content = calculate_gc_content(mutated_seq)
142-
143-
# Randomly select new formamide value based on probabilities
144-
new_formamide = random.choices(formamide_values,
145-
weights=formamide_probs,
146-
k=1)[0]
147-
148-
# Create new probe with updated values
149-
new_probe = probe.copy()
150-
new_probe['Sequence'] = mutated_seq
151-
new_probe['Length [nt]'] = str(new_length)
152-
new_probe['G+C content [%]'] = str(new_gc_content)
153-
new_probe['Formamide [%]'] = str(new_formamide)
154-
158+
new_length = len(probe['Sequence'])
159+
new_gc_content = calculate_gc_content(probe['Sequence'])
160+
probe['Length [nt]'] = str(new_length)
161+
probe['G+C content [%]'] = str(new_gc_content)
162+
155163
# Generate unique ID for this iteration
156-
if 'Accession no.' in new_probe:
157-
new_probe['Accession no.'] = f"{new_probe['Accession no.']}_{iteration + 1}"
158-
159-
noisy_probes.append(new_probe)
164+
if 'Accession no.' in probe:
165+
probe['Accession no.'] = f"{probe['Accession no.']}_{iteration + 1}"
166+
167+
probe['Mutation number'] = mutation_number
168+
noisy_probes.append(probe)
160169

161170
all_noisy_probes.extend(noisy_probes)
162171

@@ -174,23 +183,27 @@ def main():
174183
parser = argparse.ArgumentParser(description='Generate noisy probe data')
175184
parser.add_argument('--input', required=True, help='Input probeBase CSV file')
176185
parser.add_argument('--output', type=str, default='data/databases/open/probeBase_false.csv', help='Output noisy probeBase CSV file')
177-
parser.add_argument('--formamide', type=str, default='data/databases/open/probeBase_formamide.tsv', help='Formamide probabilities TSV file')
186+
parser.add_argument('--mutation-number', type=int, default=5, help='Maximum number of mutations to apply to each probe')
178187
parser.add_argument('--insertion-rate', type=float, default=0.01, help='Insertion mutation rate')
179188
parser.add_argument('--deletion-rate', type=float, default=0.01, help='Deletion mutation rate')
180189
parser.add_argument('--mutation-rate', type=float, default=0.1, help='SNP mutation rate')
181190
parser.add_argument('--iterations', type=int, default=10, help='Number of iterations to generate noisy data')
182191

183192
args = parser.parse_args()
184193

185-
generate_noisy_probes(
186-
args.input,
187-
args.output,
188-
args.formamide,
189-
args.insertion_rate,
190-
args.deletion_rate,
191-
args.mutation_rate,
192-
args.iterations
193-
)
194+
for i in range(args.mutation_number):
195+
generate_noisy_probes(
196+
args.input,
197+
args.output,
198+
i,
199+
insertion_rate=args.insertion_rate,
200+
deletion_rate=args.deletion_rate,
201+
mutation_rate=args.mutation_rate,
202+
iterations=args.iterations
203+
)
194204

195205
if __name__ == '__main__':
196-
main()
206+
main()
207+
208+
# Usage:
209+
# python scripts/databases/generate_noisy_probes.py --input data/databases/open/probeBase.csv --output data/databases/open/probeBase_mutated.csv

0 commit comments

Comments
 (0)