Skip to content

Commit a98eae5

Browse files
committed
cont
1 parent 665b62b commit a98eae5

2 files changed

Lines changed: 109 additions & 35 deletions

File tree

‎src/catomatic/BinaryCatalogue.py‎

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class BinaryBuilder(PiezoExporter):
2727
Required columns: ['UNIQUEID', 'MUTATION']
2828
Optional columns: ['FRS']
2929
30-
FRS (float, optional): The Fraction Read Support threshold used to construct the catalogues.
30+
frs (float, optional): The Fraction Read Support threshold used to construct the catalogues.
3131
Lower FRS values allow for greater genotype heterogeneity.
3232
3333
seed (list) optional): A list of predefined GARC neutral mutations with associated phenotypes
@@ -140,10 +140,36 @@ def build(
140140
# If no more susceptible solos, classify all R and U solos in one, final sweep
141141
self.classify(self.samples, self.mutations)
142142

143+
# add in rows for mutations without solos
144+
# self.add_missing()
145+
143146
self.order_catalogue()
144147

145148
return self
146149

150+
def add_missing(self):
151+
152+
# add in remaining mutations
153+
all_muts = set(self.mutations.MUTATION.dropna().unique())
154+
classified_muts = set(self.catalogue.keys())
155+
missing = all_muts - classified_muts
156+
157+
for mut in missing:
158+
full_x = self.build_full_contingency(self.samples, self.mutations, mut)
159+
160+
proportion = self.calc_proportion(full_x)
161+
ci = self.calc_confidence_interval(full_x)
162+
163+
data = {
164+
"proportion": proportion,
165+
"confidence": ci,
166+
"full_contingency": full_x,
167+
"solo_contingency": None, # explicitly absent
168+
"note": "No solo evidence; added post hoc",
169+
}
170+
171+
self.add_mutation(mut, "U", data)
172+
147173
def classify(self, samples: pd.DataFrame, mutations: pd.DataFrame) -> None:
148174
"""
149175
Orchestrate one classification iteration over exposed 'solo' mutations.
@@ -196,22 +222,26 @@ def _process_solos(self, solos: pd.DataFrame, mut: str) -> None:
196222
# Skip mutations with fewer than min_count samples
197223
if solos[solos.MUTATION == mut].shape[0] < self.min_count:
198224
return
199-
# build a contingency table
200-
x, ids = self.build_contingency(solos, mut)
225+
# build a contingency table for solo mutations
226+
x, ids = self.build_solo_contingency(solos, mut)
227+
# build a contingency table for all observations
228+
full_x = self.build_full_contingency(self.samples, self.mutations, mut)
201229
# temporarily store mutation groups:
202230
self.temp_ids = ids
203231

204232
# classify susceptible variants according to specified test mode
205233
if self.test is None:
206-
self.skeleton_build(mut, x)
234+
self.skeleton_build(mut, x, full_x)
207235
elif self.test == "Binomial":
208-
self.binomial_build(mut, x)
236+
self.binomial_build(mut, x, full_x)
209237
elif self.test == "Fisher":
210-
self.fishers_build(mut, x)
238+
self.fishers_build(mut, x, full_x)
211239
else:
212240
raise ValueError(f"Unknown test mode: {self.test}")
213241

214-
def skeleton_build(self, mutation: str, x: Contingency) -> None:
242+
def skeleton_build(
243+
self, mutation: str, x: Contingency, full_x: Contingency
244+
) -> None:
215245
"""
216246
Record descriptive statistics and optionally mark susceptible solos.
217247
Calls homogenous susceptible S.
@@ -227,7 +257,12 @@ def skeleton_build(self, mutation: str, x: Contingency) -> None:
227257
proportion = self.calc_proportion(x)
228258
ci = self.calc_confidence_interval(x)
229259

230-
data = {"proportion": proportion, "confidence": ci, "contingency": x}
260+
data = {
261+
"proportion": proportion,
262+
"confidence": ci,
263+
"solo_contingency": x,
264+
"contingency": full_x,
265+
}
231266

232267
if self.run_iter:
233268
# if iteratively classifing S variants
@@ -238,8 +273,12 @@ def skeleton_build(self, mutation: str, x: Contingency) -> None:
238273
# not phenotyping, just adding to catalogue
239274
self.add_mutation(mutation, "U", data)
240275

241-
def binomial_build(self, mutation: str, x: Contingency) -> None:
242-
assert self.background is not None, "background must be provided for Binomial test"
276+
def binomial_build(
277+
self, mutation: str, x: Contingency, full_x: Contingency
278+
) -> None:
279+
assert (
280+
self.background is not None
281+
), "background must be provided for Binomial test"
243282
bg: float = float(self.background)
244283

245284
# p-value function for binomial
@@ -258,10 +297,11 @@ def susceptible_rule(proportion: float, p_calc: float, x) -> bool:
258297
def resistant_rule(proportion: float, p_calc: float, x) -> bool:
259298
return bool(proportion > bg)
260299

261-
self.hypothesis_test(mutation, x, pvalue_fn, susceptible_rule, resistant_rule)
262-
300+
self.hypothesis_test(
301+
mutation, x, full_x, pvalue_fn, susceptible_rule, resistant_rule
302+
)
263303

264-
def fishers_build(self, mutation: str, x: Contingency) -> None:
304+
def fishers_build(self, mutation: str, x: Contingency, full_x: Contingency) -> None:
265305
"""
266306
Classify mutation using Fisher's exact test and directional inference.
267307
@@ -291,12 +331,15 @@ def resistant_rule(proportion, p_calc, x) -> bool:
291331
odds = self.calc_odds_ratio(x)
292332
return odds > 1
293333

294-
self.hypothesis_test(mutation, x, pvalue_fn, susceptible_rule, resistant_rule)
334+
self.hypothesis_test(
335+
mutation, x, full_x, pvalue_fn, susceptible_rule, resistant_rule
336+
)
295337

296338
def hypothesis_test(
297339
self,
298340
mutation: str,
299341
x: Contingency,
342+
full_x: Contingency,
300343
pvalue_fn: Callable[[Contingency], float],
301344
susceptible_rule: Callable[[float, float, Contingency], bool],
302345
resistant_rule: Callable[[float, float, Contingency], bool],
@@ -324,7 +367,8 @@ def hypothesis_test(
324367
"proportion": proportion,
325368
"confidence": ci,
326369
"p_value": p_calc,
327-
"contingency": x,
370+
"solo_contingency": x, # solo-based (classification)
371+
"contingency": full_x, # (descriptive only)
328372
}
329373

330374
# ITERATIVE MODE (we actively try to find susceptibles)
@@ -414,7 +458,7 @@ def calc_confidence_interval(self, x: Contingency) -> Tuple[float, float]:
414458
return (lower, upper)
415459

416460
@staticmethod
417-
def build_contingency(
461+
def build_solo_contingency(
418462
solos: pd.DataFrame, mutation: str
419463
) -> Tuple[list[list[int]], list[str]]:
420464
"""
@@ -438,6 +482,30 @@ def build_contingency(
438482

439483
return [[R_count, S_count], [R_count_no_mut, S_count_no_mut]], ids
440484

485+
@staticmethod
486+
def build_full_contingency(
487+
samples: pd.DataFrame, mutations: pd.DataFrame, mutation: str
488+
) -> list[list[int]]:
489+
"""
490+
Full contingency using ALL samples (sample-level, not row-level).
491+
"""
492+
493+
# determine which samples have the mutation
494+
mut_present = mutations[mutations.MUTATION == mutation][
495+
"UNIQUEID"
496+
].drop_duplicates()
497+
498+
df = samples.copy()
499+
df["has_mut"] = df["UNIQUEID"].isin(mut_present)
500+
501+
R_with = ((df.PHENOTYPE == "R") & (df.has_mut)).sum()
502+
S_with = ((df.PHENOTYPE == "S") & (df.has_mut)).sum()
503+
504+
R_without = ((df.PHENOTYPE == "R") & (~df.has_mut)).sum()
505+
S_without = ((df.PHENOTYPE == "S") & (~df.has_mut)).sum()
506+
507+
return [[int(R_with), int(S_with)], [int(R_without), int(S_without)]]
508+
441509
@staticmethod
442510
def calc_odds_ratio(x: Contingency) -> float:
443511
"""

‎src/tests/test_BuildBinaryCatalogue.py‎

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -127,15 +127,15 @@ def test_add_mutation(builder):
127127
assert builder.catalogue[mutation]["evid"] == evidence
128128

129129

130-
def test_build_contingency(solo_data):
130+
def test_build_solo_contingency(solo_data):
131131
# for R + S
132-
x_mut1 = BinaryBuilder.build_contingency(solo_data, "gene@A1S")[0]
132+
x_mut1 = BinaryBuilder.build_solo_contingency(solo_data, "gene@A1S")[0]
133133
assert x_mut1 == [[1, 1], [3, 1]]
134134
# for R
135-
x_mut2 = BinaryBuilder.build_contingency(solo_data, "gene@A2S")[0]
135+
x_mut2 = BinaryBuilder.build_solo_contingency(solo_data, "gene@A2S")[0]
136136
assert x_mut2 == [[0, 1], [3, 1]]
137137
# For non-existent mutation - although should be filtered out beforehand
138-
x_mut4 = BinaryBuilder.build_contingency(solo_data, "gene@A4S")[0]
138+
x_mut4 = BinaryBuilder.build_solo_contingency(solo_data, "gene@A4S")[0]
139139
assert x_mut4 == [[0, 0], [3, 1]]
140140

141141

@@ -149,7 +149,7 @@ def test_calc_proportion():
149149
for x, expected in x_tests:
150150
assert (
151151
BinaryBuilder.calc_proportion(x) == expected
152-
), f"Failed for contingency {x}"
152+
), f"Failed for solo_contingency {x}"
153153

154154

155155
def test_calc_odds_ratio():
@@ -162,7 +162,7 @@ def test_calc_odds_ratio():
162162
for x, expected in x_tests:
163163
assert (
164164
BinaryBuilder.calc_odds_ratio(x) == expected
165-
), f"Failed for contingency {x}"
165+
), f"Failed for solo_contingency {x}"
166166

167167

168168
@pytest.mark.parametrize("builder", [{"p": 0.95}], indirect=True)
@@ -178,7 +178,7 @@ def test_calc_confidence_interval(builder):
178178
ci = builder.calc_confidence_interval(x)
179179
ci = [round(ci[0], 4), round(ci[1], 4)]
180180

181-
assert ci == expected, f"Failed for contingency {x}"
181+
assert ci == expected, f"Failed for solo_contingency {x}"
182182

183183

184184
@pytest.mark.parametrize("builder", [{"p": 0.95}], indirect=True)
@@ -188,8 +188,9 @@ def test_skeleton_build(builder):
188188
# now test addition when run_iter = False
189189
mutation = "mut4"
190190
x = [[27, 21], [3, 22]]
191+
full_x = [[0, 0], [0,0]]
191192

192-
builder.skeleton_build(mutation, x)
193+
builder.skeleton_build(mutation, x, full_x)
193194
# check its actually been added
194195
assert mutation in builder.catalogue
195196
# check if the correct phenotype has been added
@@ -198,7 +199,8 @@ def test_skeleton_build(builder):
198199
expected_e = {
199200
"proportion": builder.calc_proportion(x),
200201
"confidence": builder.calc_confidence_interval(x),
201-
"contingency": x,
202+
"solo_contingency": x,
203+
"contingency": full_x,
202204
}
203205

204206
assert builder.catalogue[mutation]["evid"] == expected_e
@@ -218,6 +220,7 @@ def test_binomial_build_RU(builder):
218220
([[27, 21], [3, 22]], "R"),
219221
([[5, 21], [3, 22]], "U"),
220222
]
223+
full_x = [[0, 0], [0, 0]]
221224

222225
for mut in range(len(mutations)):
223226
mutation = mutations[mut]
@@ -232,20 +235,21 @@ def test_binomial_build_RU(builder):
232235
"proportion": builder.calc_proportion(x),
233236
"confidence": builder.calc_confidence_interval(x),
234237
"p_value": p_expected,
235-
"contingency": x,
238+
"solo_contingency": x,
239+
"contingency": full_x
236240
}
237241

238-
builder.binomial_build(mutation, x)
242+
builder.binomial_build(mutation, x, full_x)
239243
# make sure mutaiton was added
240-
assert mutation in builder.catalogue, f"Failed for contingency {x}"
244+
assert mutation in builder.catalogue, f"Failed for solo_contingency {x}"
241245
# make sure phenotype was determined correclty
242246
assert (
243247
builder.catalogue[mutation]["pred"] == phenotype
244-
), f"Failed for contingency {x}"
248+
), f"Failed for solo_contingency {x}"
245249
# make sure evidence was added correclty
246250
assert (
247251
builder.catalogue[mutation]["evid"] == expected_e
248-
), f"Failed for contingency {x}"
252+
), f"Failed for solo_contingency {x}"
249253

250254
# fyi haven't tested entire decision tree for run_iter=True, but they're are almost identical
251255

@@ -264,6 +268,7 @@ def test_fisher_build_RU(builder):
264268
([[27, 21], [3, 22]], "R"),
265269
([[5, 21], [3, 22]], "U"),
266270
]
271+
full_x = [[0, 0], [0, 0]]
267272

268273
for mut in range(len(mutations)):
269274
mutation = mutations[mut]
@@ -276,20 +281,21 @@ def test_fisher_build_RU(builder):
276281
"proportion": builder.calc_proportion(x),
277282
"confidence": builder.calc_confidence_interval(x),
278283
"p_value": p_expected,
279-
"contingency": x,
284+
"solo_contingency": x,
285+
"contingency": full_x
280286
}
281287

282-
builder.fishers_build(mutation, x)
288+
builder.fishers_build(mutation, x, full_x)
283289
# make sure mutaiton was added
284-
assert mutation in builder.catalogue, f"Failed for contingency {x}"
290+
assert mutation in builder.catalogue, f"Failed for solo_contingency {x}"
285291
# make sure phenotype was determined correclty
286292
assert (
287293
builder.catalogue[mutation]["pred"] == phenotype
288-
), f"Failed for contingency {x}"
294+
), f"Failed for solo_contingency {x}"
289295
# make sure evidence was added correclty
290296
assert (
291297
builder.catalogue[mutation]["evid"] == expected_e
292-
), f"Failed for contingency {x}"
298+
), f"Failed for solo_contingency {x}"
293299

294300

295301
# the S tests below are conceptually more complex than the RU tests, as these use the

0 commit comments

Comments
 (0)