-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
788 lines (630 loc) · 24.8 KB
/
Copy pathapp.py
File metadata and controls
788 lines (630 loc) · 24.8 KB
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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
"""
Streamlit version of the initial notebook here:
https://github.com/patimus-prime/ML_notebooks/blob/master/high-throughput-analysis.ipynb
"""
import streamlit as st
import pandas as pd
import matplotlib as plt
import seaborn as sns
import statsmodels.api as sm
import statsmodels.formula.api as smf
from datasets import load_dataset
import plotly.express as px
# https://docs.streamlit.io/library/components/components-api
import streamlit.components.v1 as components
# And some extras to have fun with this go:
# I got weird errors, 28 June 2023
# pip install streamlit-extras
# pip install streamlit-elements
#
st.image(
"memes/sneakpeek.png",
)
colA, colB = st.columns(2)
with colA:
st.write(
"""
# Welcome!
Objective: In an experiment with multiple machines running
separate but similar instruments, prone to inefficiency, or with
unstable samples, how can you identify statistically significant
samples? In this case study, we are searching for samples, strains,
that are producing the most amount of target molecule.
We'll use some fun statistics to identify prime candidates, and
Streamlit to make everything beautiful and relatively interactive.
"""
)
with colB:
components.iframe(
"https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/playlists/1644215929&color=%23ff5500\
&auto_play=true\
&hide_related=false\
&show_comments=false\
&show_user=false\
&show_reposts=false\
&show_teaser=false\
&visual=false\
&buying=false",
width=int(166 * 2),
height=int(166 * 1.5),
)
st.write(
"""
Load in our data from Huggingface, available here: https://huggingface.co/patimus-prime
---
"""
)
# datasets from huggingface assume train data
# https://huggingface.co/docs/datasets/load_hub
with st.echo():
dataset = load_dataset("patimus-prime/strain_selection", split="train")
df = pd.DataFrame(dataset)
# if you don't specify split="train" above, must do the coupe following lines
# view dataset, from wher which we can se we must grab one of hte indeces for data
# dataset["train"]
# df = pd.DataFrame(dataset['train'])
# Modify stacker_postion to something less nebulous
df["time_mins"] = df["stacker_position"].apply(lambda x: 3 * x - 3)
# st.dataframe(df)
st.write(
"Let's take a look at the first few rows...",
df.head(),
"...and the 'describe' method, which has FORESHADOWING",
df.describe(),
"And, plotting the data of course:",
)
fig = px.scatter(
df,
x="time_mins",
y="value",
# size="pop",
color="well_type",
hover_name="strain",
hover_data="value",
symbol="robot",
# log_x=True,
# size_max=60,
)
st.plotly_chart(fig, use_container_width=True)
st.write(
"""
A couple things become apparent:
1. We're going to need to resolve the data being from multiple machines.
2. Values are trending downward with time/stacker position. Therefore, options:
time-series analysis, analyze per t-point, or REMOVE THE INFLUENCE OF TIME 👹
3. Plotly + Streamlit = Awesome
Our process control is also decreasing with time; this, as a control, should
remain constant. Suggests that overtime, either the control and
samples are degrading in step or the instrument is affected.
So: detrend the data, prove robots comparable with ANOVA, profit.
# Detrend the data
Assumption: The robots are comparable.
Let's detrend first to be able to pool all day per time point,
then we can have more statistical OOMPH to do ANOVA and
demonstrate the robots may be pooled also and back up our assumption.
Among the options to detrend, e.g. subtracting a rolling average/statistic,
I choose subtracting a line of best fit. The math is more transparent. And
we can fit a line of best fit per well type; in case, perhaps,
the different strain types do have some ongoing, unique phenomenon decaying their value.
"""
)
# note streamlit will use width to also adjust height
st.image("memes/zapp.jpg", width=300)
st.write(
"""
Big bonus for Streamlit:
Big improvement over using matplotlib/seaborn:
plotly express allows one to use OLS fits and also report the
stats. Based on what's coming out I'd guess they use statsmodels.
https://plotly.com/python/linear-fits/
Oh, maybe you'd like to see how LUXURIOUS the code is for this
"""
)
with st.echo():
fig2 = px.scatter(
df,
x="time_mins",
y="value",
color="well_type",
hover_name="strain",
hover_data="value",
symbol="robot",
trendline="ols",
)
st.plotly_chart(fig2)
stats = px.get_trendline_results(fig2)
st.write(stats.px_fit_results.iloc[0].summary())
st.write(
"""
---
Hit the Home button in the graph if you lose the trendlines.
Also you can hit labels in legend to toggle on/off on graph.
Pretty cool. However, the trendline summary
produced below the graph is not really helpful in this case,
the trendlines produced are at least visually similar,
but what would be helpful for our math
is if the trendlines individually per well type
are similar enough to subtract and not miss out on some
phenomenon occurring per well.
"Just because you're paranoid, doesn't mean they aren't after you."
-- Joseph Heller
"""
)
# This next section in the code is to get the OLS trendlines
# for each well. Maybe with some trickery you could do it with the plotly stuff.
pc_data = df[df.well_type == "Process Control"]
pc_ols = smf.ols("value ~ time_mins", data=pc_data).fit()
pcs = pc_ols.summary()
ps_data = df[df.well_type == "Parent Strain"]
ps_ols = smf.ols("value ~ time_mins", data=ps_data).fit()
pss = ps_ols.summary()
x_data = df[df.well_type == "Standard Well"]
x_ols = smf.ols("value ~ time_mins", data=x_data).fit()
xs = x_ols.summary()
# A chance to see these summares side by side
# using streamlit columns, make everything pretty pretty prettcol1,
st.write(
"Feel free to skip past the tables.\
What we're looking for is the second row for coef, the slope of the lines."
)
if "button" not in st.session_state:
st.session_state.button = False
def click_button():
st.session_state.button = not st.session_state.button
st.button("Show delicious statistics", on_click=click_button)
if st.session_state.button:
st.header("Process Control Summary")
pcs
st.header("Parent Strain Summary")
pss
st.header("Candidate Strains Summary")
xs
else:
st.write("")
st.write(
"""
So: PC, -26, PS, -33, Candidates, -33.
Not perfect, and not much we can do but assume we can go on
in this analysis, while NOTING!! that this process control and our
strains, control and candidates, are being treated differently.
It may well be there's some actual degradation occurring over the course
of the experiment, maybe target molecule is being measured while being enzymatically
degraded, or... yeah, just something to investigate in future.
# ANYWAY
Now we'll begin detrending; that is, subtracting the predicted, trendline value,
from the actual measured value. Result: our means go to 0 and all detrended values
become a measure of distance from the mean.
Let's also see the exact code going on:
"""
)
# DETRENDING!! ---------
# PC (and note lambda x is per entry, agnostic, not to be confused with x_data below)
pc_data["predicted_value"] = pc_ols.predict(pc_data.time_mins)
pc_data["detrended_value"] = pc_data.apply(
lambda x: x["value"] - x["predicted_value"], axis=1
)
# same thing for the other dataframes. hope the math is clear atm!
# PS:
ps_data["predicted_value"] = ps_ols.predict(ps_data.time_mins)
ps_data["detrended_value"] = ps_data.apply(
lambda x: x["value"] - x["predicted_value"], axis=1
)
with st.echo():
# X:
x_data["predicted_value"] = x_ols.predict(x_data.time_mins)
x_data["detrended_value"] = x_data.apply(
lambda x: x["value"] - x["predicted_value"], axis=1
)
st.write(
"And, so, behold! Scrolling over to detrended value, mean is 0:",
x_data.describe(),
"# SO! Now pooling our data and re-plotting...",
)
# get the band back together
# d for detrended
# Aloso pandas has updated:
# https://pandas.pydata.org/pandas-docs/stable/user_guide/merging.html
framesToMerge = [pc_data, ps_data, x_data]
d_df = pd.concat(framesToMerge)
fig3 = px.scatter(
d_df,
x="time_mins",
y="detrended_value",
color="well_type",
hover_name="strain",
hover_data="detrended_value",
symbol="robot",
# trendline="ols",
opacity=0.3,
)
st.plotly_chart(fig3)
# For only superior seaborn plot
# maybe come back and try
# s = sns.lineplot(
# data=d_df,
# # literally x and y axes
# x="time_mins",
# y="detrended_value",
# # grouping variables:
# hue="well_type",
# style="robot",
# # legend = 'auto', # auto by default
# )
# st.pyplot(s)
st.write(
"""
This is one case where there's a superior plot in seaborn, lineplot,
that doesn't let the Standard Wells overwhelm everything. I've
tried to make it transparent, but one can still
check, just toggle off the Standard Wells in the legend.
Anyway, the mean is 0, artificially. Yay! We have eliminated time. Yet we still
are slicing by robot. We will now resolve this and validate our assumption
that we could compare these things.
Once that's done, we'll be able to consider just 3 populations,
not sliced by time or machine:
Process Control, Parent Strain and the Candidate Strains.
"""
)
st.image("memes/farnsworthquantum.jpg", width=300)
st.write(
"""
# ANOVA on Process Controls
Here we'll do ANOVA on the process controls, now liberated of time.
By comparing them across machines, and proving there is no statistical difference
of the populations, we can treat them as one, our method is valid, and assume the rest of the
sample types may also be pooled.
A problem, though (which has a solution): ANOVA assumes homoscedasticity,
It demands the stdev of all groups are the same. The mean is artifically 0 for
all populations, but stdev is not likely to be similar and we'll look at it in a second.
An alternative would be repeated measure ANOVA, and do ANOVA per well type
per time point; but this will have much less statistical power splitting up the data in 20 parts.
Therefore: note the stdevs and go with the lesser evil of regular ANOVA.
"""
)
with st.echo():
d_pc_df = d_df[d_df.well_type == "Process Control"]
overall_std = d_pc_df.detrended_value.std()
bender_std = d_pc_df[d_pc_df.robot == "Bender"].detrended_value.std()
c3p0_std = d_pc_df[d_pc_df.robot == "C3P0"].detrended_value.std()
term_std = d_pc_df[d_pc_df.robot == "Terminator"].detrended_value.std()
st.write(
"stdevs: \n",
"overall",
overall_std,
"bender:",
bender_std,
"c3p0:",
c3p0_std,
"terminator:",
term_std,
)
st.write(
"""
Numbers are fairly similar, although the terminator robot isn't as close as the others.
We'll assume homoscedasticity.
The overall stdev of 212 for the process control can be used to define
the outlier threshold: 3*212 = 636, so anything outside +/- 630 can be considered outliers.
Let's peek and see if any exist in the process control population:
""",
d_pc_df[d_pc_df.well_type == "Process Control"].describe(),
"""
Min and max are -556 and 601; seemingly no outliers, nothing beyond the cutoff.
So, we can consider this as som analagous measure of the instrument error for all data:
therefore all other outliers' threshold, for the parent and candidate strains, should be >= 630.
Therefore, let's now confirm the null hypothesis, that all populations are equivalent, for the process
controls via ANOVA. """,
)
# THere appears to be an alternative method to do this via:
# https://www.statsmodels.org/stable/anova.html
# BUt for now I'll do the scipy option to be lazy
# SCIPY method
# ANOVA!!
from scipy.stats import f_oneway
# the scipy implementation takes in a 1D vector of values... soooo... let's see how to get that out
# d_df[d_df.robot == 'Terminator'].detrended_value.values # this should work i
st.write(
f_oneway(
d_pc_df[d_pc_df.robot == "Terminator"].detrended_value.values,
d_pc_df[d_pc_df.robot == "Bender"].detrended_value.values,
d_pc_df[d_pc_df.robot == "C3P0"].detrended_value.values,
),
"""
Pvalue is above 0.05; without even examining the F-statistic we can say
the null hyothesis cannot be rejected, process controls are not statsitically
significant, not meaningfully differente from each other.
It is however surprising that the p-value isn't much higher given how similar
we've seen the process controls look. LIVIN' ON A PRAYER
So, moving forward with assumption that process controls are equivalent, and
thus we may also compare the other sample types, parent strain and candidate strain.
... Also, the parent strain may serve as almost a secondary control, so we'll also be looking
at that in a second.
""",
)
# https://plotly.com/python/histograms/
# https://plotly.com/python-api-reference/generated/plotly.express.histogram.html?highlight=histogram
fig4a = px.histogram(
d_df[d_df.well_type == "Process Control"],
x="detrended_value",
color="robot",
hover_name="robot",
hover_data="detrended_value",
marginal="violin", # in margin, visual of dist/data
opacity=0.5,
barmode="overlay",
histfunc="count",
histnorm="density",
nbins=50,
)
fig4b = sns.displot(
data=d_df[d_df.well_type == "Process Control"],
x="detrended_value",
hue="robot",
kind="kde",
)
st.write(
"""
Nonideal in plotly express. Cannot see density plots that
make it a bit easier, informative to compare; so density plot in second
tab. (Warning: seaborn density plot has no dark mode.)
"""
)
tab1, tab2 = st.tabs(["Plotly Histogram", "SNS Density Plot"])
with tab1:
st.plotly_chart(fig4a)
with tab2:
st.pyplot(fig4b)
st.write(
"""
We can see that the Terminator robot is the most consistent, least stdev/
most populous around the mean. I hope the new movie is good.
Similarly, let's compare the Parent Strain and Candidate strain sample populations
per robot. Same tabs etc.
"""
)
fig5a = px.histogram(
d_df[d_df.well_type == "Parent Strain"],
x="detrended_value",
color="robot",
hover_name="robot",
hover_data="detrended_value",
marginal="violin", # in margin, visual of dist/data
opacity=0.5,
barmode="overlay",
histfunc="count",
histnorm="density",
nbins=50,
)
fig5b = sns.displot(
data=d_df[d_df.well_type == "Parent Strain"],
x="detrended_value",
hue="robot",
kind="kde",
)
fig5a = px.histogram(
d_df[d_df.well_type == "Standard Well"],
x="detrended_value",
color="robot",
hover_name="robot",
hover_data="detrended_value",
marginal="violin", # in margin, visual of dist/data
opacity=0.5,
barmode="overlay",
histfunc="count",
histnorm="density",
nbins=50,
)
fig5b = sns.displot(
data=d_df[d_df.well_type == "Standard Well"],
x="detrended_value",
hue="robot",
kind="kde",
)
# PARENT STRAIN TABS ------
st.write("Parent Strain (~ second control):")
tab1, tab2 = st.tabs(["Plotly Histogram", "SNS Density Plot"])
with tab1:
st.plotly_chart(fig4a)
with tab2:
st.pyplot(fig4b)
# CANDIDATE STAIN TABS ----
st.write("Standard Well/Candidate Strains:")
tab1, tab2 = st.tabs(["Plotly Histogram", "SNS Density Plot"])
with tab1:
st.plotly_chart(fig5a)
with tab2:
st.pyplot(fig5b)
st.write(
"""
# ALRIGHTY THEN, CONCLUDING POOLING:
Alrighty, from these two above we see, *looking at the axes*,
that the Standard Wells have far more data spread and variabiltiy. The
process control and parent strain samples fall mostly within +/- 500.
Versus the Standard Wells going out to like +/- 1500.
And I'll say now these are ~ absorbance units ~ molecule expression in the strain.
So, I think it's reasonable to proceed saying our method is fairly effective, with an
accuracy +/- 500 units. Our next objective will then be to remove outliers, or note them,
and FINALLY identify the prime candidate strains that are statistically significant
improvements over the parent strain; these will move on to the next stage of
selection.
---
Let's now, for the sake of closure, finality, look at all these populations now
grouped together, split by well type rather than robot.
"""
)
fig6a = px.histogram(
d_df,
x="detrended_value",
color="well_type",
hover_name="well_type",
hover_data="detrended_value",
marginal="violin", # in margin, visual of dist/data
opacity=0.5,
barmode="overlay",
histfunc="count",
histnorm="density",
nbins=50,
)
fig6b = sns.displot(
data=d_df,
x="detrended_value",
hue="well_type",
kind="kde",
)
tab1, tab2 = st.tabs(["Plotly Histogram", "SNS Density Plot"])
with tab1:
st.plotly_chart(fig6a)
with tab2:
st.pyplot(fig6b)
st.write(
"""
Yeah so, that's awesome. Can see 'em all in one place,
the product of all our labor and attention. The violin
plots above the histogram are fairly helpful to see the
distributions/spreads of data for the different well types/strains.
So, let's finally get to the exciting, productive part!!
One last step: remove, or note outliers. Maybe they could be wicked productive
strains, don't wanna just discard 'em.
# The Penultimate Step
---
Removing outliers from parent and candidate strains. Basically,
remove the values +/- 3 STDEVs outside each population. BUT, note
the +3 STDEV candidate strains just in case those ones are legit awesome strains.
"""
)
with st.echo():
from scipy import stats
import numpy as np
# get just the parent strain data
ps_detrended = d_df[d_df.well_type == "Parent Strain"]
# find outliers in the data; outside zscore of 3, 3 stdev; note gets absolute value so anything +/-
ps_outliers = ps_detrended[(np.abs(stats.zscore(ps_detrended.detrended_value)) > 3)]
# redefine data to NOT have those outliers
ps_detrended = ps_detrended[
(np.abs(stats.zscore(ps_detrended.detrended_value)) < 3)
]
# show outliers found and removed, just 3 of em lol
ps_outliers
st.write(
"Now do for the candidate strains. Code is slightly different \
to keep track of the positive outliers."
)
with st.echo():
# get just the CANDIDATE strain data
X_detrended = d_df[d_df.well_type == "Standard Well"]
# find outliers in the data;
# THIS LINE and next get the negative and positive ID'd
# outside zscore of 3, 3 stdev; note gets absolute value so anything +/-
X_outliers_negative = X_detrended[(stats.zscore(X_detrended.detrended_value)) < -3]
X_outliers_positive = X_detrended[(stats.zscore(X_detrended.detrended_value)) >= 3]
# and finally, get the cleaned up X data, stuff within 3 stdev/z-score of 3
X_detrended = X_detrended[(np.abs(stats.zscore(X_detrended.detrended_value)) < 3)]
st.write(
"Amount of negative outliers: ",
len(X_outliers_negative),
"Amount of positive outliers: ",
len(X_outliers_positive),
"Remaining non-outlier samples: ",
len(X_detrended),
"""
# FINALLY. We can now identify the best candidate(s) (such as myself).
---
One could do a t-test, sure; or more simply we could just set a
threshold at > 99%% of the parent strains' expression.
""",
)
with st.echo():
# 99% cutoff:
final_cutoff = ps_detrended.detrended_value.quantile(0.99)
# get the candidates that have a value above this cutoff
super_candidates = X_detrended[(X_detrended.detrended_value > final_cutoff)]
st.write(
"Number of excellent candidates: ",
len(super_candidates),
"YAAAAAAAAAAAASSSSSSSSSSSSSSSSSSSSSSS!!!!!!!!!!!!!!!!",
)
st.image("memes/tuco2.jpg", width=300)
st.write(
"Ok, let's plot these, plus the positive outliers, and see what it looks like."
)
super_candidates["sus"] = False
X_outliers_positive["sus"] = True
# all_candidates = super_candidates.append(X_outliers_positive)
all_candidates = pd.concat([super_candidates, X_outliers_positive])
# framesToMerge = [pc_data, ps_data, x_data]
# d_df = pd.concat(framesToMerge)
fig7 = px.histogram(
super_candidates,
x="detrended_value",
color="sus",
hover_name="sus",
hover_data="detrended_value",
marginal="violin", # in margin, visual of dist/data
opacity=0.5,
barmode="overlay",
histfunc="count",
histnorm="density",
nbins=50,
)
st.plotly_chart(fig7)
col1, col2 = st.columns(2)
with col1:
st.write(
"Excluding the outliers",
super_candidates.detrended_value.describe(),
)
with col2:
st.write(
"Including the outliers",
all_candidates.detrended_value.describe(),
)
st.write(
"""
Alrighty then! So we see that the strains of interest start above around 600 greater
than the mean of the parent strain*, and basically become less numerous.
The max is about 1600 above that mean, but may be an outlier.
(*Mathematically, greater than the mean of the candidate strains, but the detrending renders this at 0 for both the candidate strains and parent strain, for better or worse given the line of best fit predicted for either dataset.)
There's 133 candidates we have here. I'll leave it as is, one could slice this different ways depending on
inclusion of the potential outliers or not. But I'll output a 1D vector of these 3 dataframes below.
So, basically, for 100/5280 candidates, we see ~ 800 improvement in absorbance ~ molecule expression.
Evolution in action.
Let's see the exact math so we can have some readout for our overall selection/evolution process:
""",
)
f = df[df.well_type == "Parent Strain"]
st.write(
"Original mean of parent strain, before detrending: ",
f.value.mean(),
"\n",
"Mean of our results including outliers, representing a delta over the parent strain: ",
all_candidates.detrended_value.mean(),
"Percent improvement in these samples, including outliers: ",
all_candidates.detrended_value.mean() / f.value.mean(),
"Percent improvement without outliers: ",
super_candidates.detrended_value.mean() / f.value.mean(),
"So, these numbers suggest somewhere around 7-9%% improvement in these strains; in 99 or 133/5280 strains. Neat!",
"Ok, and to get the data out, one would just come down and save one of these dfs to csv etc.",
)
with st.echo():
all_candidate_strains_counter = all_candidates.counter.values
# super candidates, doesn't include outliers
super_candidate_strains_counter = super_candidates.counter.values
# potential outlier candidates
outlier_counters = X_outliers_positive.counter.values
# Sample output
st.write(outlier_counters)
st.write(
"""
And... THAT'S FINALLY IT! We've identified ~133 candidates based on a cutoff
defined as being superior to 99%% of parent strain samples. Our methodology has included:
- Detrending by ordinary least squares, line of best fit, in lieu of time-series analysis or repeated-measure ANOVA, in order
to obtain a greater statsitical power and compare as many samples as possible
- Confirmation of null hypothesis to pool samples from different machines
- Thresholding, effectively p value <= 0.01 samples to select for next run
- Did this all in a pretty, interactive way using several libraries and memes.
- Takes about a day to get this all together in Streamlit/Plotly, far superior and with
very readable syntax etc. using this than my experience with Shiny, Dash... PowerBI may
be close in production speed but is not as extensible of course.
- Deployment and embedding into my main website is then trivial with Github/Streamlit/Huggingface.
- 10/10 recommend
"""
)