-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlibrary.py
More file actions
2259 lines (2187 loc) · 134 KB
/
Copy pathlibrary.py
File metadata and controls
2259 lines (2187 loc) · 134 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
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import numpy as np
import scanpy as sc
from collections import Counter
import random
import matplotlib.pyplot as plt
import os
import shutil
from sklearn.metrics import silhouette_samples, calinski_harabasz_score, pairwise_distances, silhouette_score
import umap
from scipy.spatial.distance import cosine as cosine_distance, euclidean as euclidean_distance
import argparse
from anndata import AnnData
import pickle
import pandas as pd
import warnings
import gseapy
import csv
import traceback
from sklearn.metrics.pairwise import cosine_distances
from sklearn.preprocessing import StandardScaler, normalize as sklearn_normalize
import socket
from sklearn.cluster import HDBSCAN, KMeans, AgglomerativeClustering
from scipy.cluster.hierarchy import dendrogram
import itertools
import time
from scipy.stats import spearmanr, pearsonr
import psutil
from adjustText import adjust_text
import multiprocessing as mp
def get_filtered_perturbations(adata, control_strings, exclude_perturbations):
"""
return list of unique perturbations in adata minus control strings and exclude_perturbations
"""
pert_candidates = set(adata.obs["gene_target"])
##remove control strings and unique perturbations from candidates
for control in control_strings:
pert_candidates.remove(control)
for u_p in exclude_perturbations:
if u_p in pert_candidates:
pert_candidates.remove(u_p)
return list(pert_candidates)
def get_umap(X):
"""
return UMAP reduction of X
"""
reducer = umap.UMAP()
embedding = reducer.fit_transform(X)
return embedding
def rank_perturbations_by_similarity(adata, anchor_state="Untreated_SAFE_TARGET", normalize=False, distance_metric="cosine", embedding_type="method_embedding"):
"""
Given adata with perturbation labels in .obs["gene_target"] and embeddings within adata.X,
ranks label classes by their distance (distance type determined by distance_metric: either cosine or eulicean) from average of label embedding to average anchor state A.
Anchor state is {condition}_{perturbation}, e.g. Untreated_SAFE_TARGET
embedding_type will dictate what embeddings to use for calculating the distances:
"method_embedding" will be the raw embedding of the method
"umap_embedding" will be the UMAP of the raw embedding of the method
returns list of tuples: (perturbation, distance)
"""
if embedding_type == "method_embedding":
matrix = adata.X
elif embedding_type == "umap_embedding":
matrix = adata.obsm["umap"]
else:
raise Exception()
if normalize:
matrix = preprocess_matrix(matrix, standardize=False, normalize=normalize)
labels = list(adata.obs["gene_target"])
##add condition to target so that final labels are {condition}_{gene_target}
labels = [adata.obs["condition"].iloc[i] + "_" + labels[i] for i in range(0, len(labels))]
all_labels = set(labels)
label_to_indices = {label: [i for i in range(0, len(labels)) if labels[i] == label] for label in all_labels}
label_to_centroid = {label: np.mean(matrix[label_to_indices[label]], axis=0) for label in all_labels}
if distance_metric == "cosine":
anchor_to_labels_distance = {label: cosine_distance(label_to_centroid[label], label_to_centroid[anchor_state]) for label in all_labels}
elif distance_metric == "euclidean":
anchor_to_labels_distance = {label: euclidean_distance(label_to_centroid[label], label_to_centroid[anchor_state]) for label in all_labels}
else:
raise Exception()
sorted_ = sorted(anchor_to_labels_distance.items(), key=lambda x: x[1], reverse=False) ##sorted from lowest distance to highest
return sorted_
def is_control(string, control_strings):
"""
given a string and a set of control strings, will return True if string is a superset of any of the control strings else False, case insensitive
"""
for control_string in control_strings:
if control_string.upper() in string.upper():
return True
return False
def get_stats(l):
"""
from a list of values, return basic stats
"""
mean = np.mean(l)
median = np.median(l)
std = np.std(l)
mini = np.min(l)
maxi = np.max(l)
return f"mean: {mean:.2E}, median: {median:.2E}, std: {std:.2E}, min: {mini:.2E}, max: {maxi:.2E}"
def perturbation_vs_control_umap(adata, control_strings, unique_perturbations, plot_perts=None, n=50, method=None, direction=None, dataset=None, save_dir=None):
"""
if plot_perts is NOT set:
plot UMAP of 4 control populations:
(1) treated control
(2) untreated control
(3) treated perturbation
(4) untreated perturbation
will choose n perturbations at random
else if plot_perts is set:
must also specify direction:
direction is either "to_healthy" or "to_disease" and will be used to decide which perturbed treatment type to plot for reversion maps
plot_perts specifies a list of specific perturbations to plot
if direction == "to_healthy" will remove set (4) because we want to visualize just treated perturbed
if direction == "to_disease" will remove set (3) because we want to visualize just untreated perturbed
will also plot each pert in plot_perts individually
"""
if not os.path.isdir(save_dir):
os.makedirs(save_dir)
if plot_perts != None:
assert(direction != None)
selected_perturbations = plot_perts
optional_save_prefix = "_" + direction
else:
pert_candidates = get_filtered_perturbations(adata, control_strings, unique_perturbations)
selected_perturbations = set(random.sample(pert_candidates, n))
optional_save_prefix = ""
control_and_selected_perts = control_strings.union(selected_perturbations)
sub_adata = adata[(adata.obs["gene_target"].isin(control_and_selected_perts))]
coordinates = sub_adata.obsm["umap"]
kds = sub_adata.obs["knockdown_efficiencies"]
adata_treated_control_indices = np.array([i for i in range(0, len(sub_adata)) if sub_adata.obs["condition"].iloc[i] == "Treated" and sub_adata.obs["gene_target"].iloc[i] in control_strings])
adata_untreated_control_indices = np.array([i for i in range(0, len(sub_adata)) if sub_adata.obs["condition"].iloc[i] == "Untreated" and sub_adata.obs["gene_target"].iloc[i] in control_strings])
adata_treated_perturbed_indices = np.array([i for i in range(0, len(sub_adata)) if sub_adata.obs["condition"].iloc[i] == "Treated" and sub_adata.obs["gene_target"].iloc[i] in selected_perturbations])
adata_untreated_perturbed_indices = np.array([i for i in range(0, len(sub_adata)) if sub_adata.obs["condition"].iloc[i] == "Untreated" and sub_adata.obs["gene_target"].iloc[i] in selected_perturbations])
mapp = {"untreated control": adata_untreated_control_indices, "untreated perturbed": adata_untreated_perturbed_indices, "treated control": adata_treated_control_indices, "treated perturbed": adata_treated_perturbed_indices}
color_map = {"untreated control": "lime", "untreated perturbed": "violet", "treated control": "deepskyblue", "treated perturbed": "midnightblue"}
if plot_perts != None:
if direction == "to_healthy":
del mapp["untreated perturbed"] ##for reversion to healthy, want to visualize just treated perturbed
if direction == "to_disease":
del mapp["treated perturbed"] ##for reversion to disease, want to visualize just untreated perturbed
##make scatter plot of categories
fig, ax = plt.subplots()
for key in mapp:
indices = mapp[key]
sub_coordinates = coordinates[indices]
ax.scatter(sub_coordinates[:,0], sub_coordinates[:,1], label=key, s=1.0, color=color_map[key])
ax.set_xlabel("UMAP 1")
ax.set_ylabel("UMAP 2")
box = ax.get_position()
ax.set_position([box.x0, box.y0, box.width, box.height * 0.85])
ax.legend(loc='upper right', prop={"size":7}, bbox_to_anchor=(1, 1.32))
plt.gcf().subplots_adjust(top=.76)
if dataset == "full":
if method in ["umap", "raw"]:
prefix1 = f"UMAP"
else:
prefix1 = f"UMAP of {get_method_title(method)} Embeddings\n"
prefix2 = f"UMAP of {get_method_title(method)} Embeddings\n"
else:
prefix1 = f"{get_method_title(method)} on {get_dataset_title(dataset)}:\nUMAP"
prefix2 = f"{get_method_title(method)} on {get_dataset_title(dataset)}:\nUMAP"
if plot_perts == None:
plt.title(f"{prefix1} for {n} Randomly Chosen Perturbations + Controls")
else:
plt.title(f"{prefix2} of Top {len(plot_perts)} Perturbations Inducing Shift {direction.replace('_',' ').title()}\n{plot_perts}", fontsize=8)
plt.savefig(os.path.join(save_dir, f"{method}{optional_save_prefix}_controls_vs_perturbations_plot_perts={plot_perts}.png"), dpi=300)
##also plot each perturbation individually if plot_perts specifies a list of genes
if plot_perts != None:
for pert in plot_perts:
fig, ax = plt.subplots()
p_indices_treated = np.array([i for i in range(0, len(sub_adata)) if sub_adata.obs["condition"].iloc[i] == "Treated" and sub_adata.obs["gene_target"].iloc[i] == pert])
p_indices_untreated = np.array([i for i in range(0, len(sub_adata)) if sub_adata.obs["condition"].iloc[i] == "Untreated" and sub_adata.obs["gene_target"].iloc[i] == pert])
if direction == "to_healthy":
mapp = {"untreated control": adata_untreated_control_indices, "treated control": adata_treated_control_indices, f"treated {pert}": p_indices_treated}
if direction == "to_disease":
mapp = {"untreated control": adata_untreated_control_indices, "treated control": adata_treated_control_indices, f"untreated {pert}": p_indices_untreated}
color_map = {"untreated control": ("lime", 0), "treated control": ("deepskyblue", 1), f"untreated {pert}": ("violet", 2), f"treated {pert}": ("midnightblue", 3)} ##colors and z-order
for key in mapp:
indices = mapp[key]
if len(indices) == 0:
print(f"WARNING: {method} {pert} {key} has no indices, skipping plot")
continue
sub_coordinates = coordinates[indices]
ax.scatter(sub_coordinates[:,0], sub_coordinates[:,1], label=f"{key}, n={len(indices)}", s=1.0, color=color_map[key][0], zorder=color_map[key][1])
##annotate reversion points with knockdown ratio
if (direction == "to_healthy" and key == f"treated {pert}") or (direction == "to_disease" and key == f"untreated {pert}"):
sub_kds = kds.iloc[indices]
sub_kds = [round(k, 2) for k in sub_kds]
x = sub_coordinates[:,0]
y = sub_coordinates[:,1]
for i, txt in enumerate(sub_kds):
plt.annotate(
txt, # The text to display
(x[i], y[i]), # The coordinates of the point to annotate
textcoords="offset points", # How to position the text relative to the point
xytext=(0, 3), # Distance from text to point (x, y)
ha='center', # Horizontal alignment of the text
# arrowprops=dict(arrowstyle="->", color='gray'), # Optional: add an arrow
fontsize=3)
box = ax.get_position()
ax.set_position([box.x0, box.y0, box.width, box.height * 0.85])
ax.legend(loc='upper right', prop={"size":7}, bbox_to_anchor=(1, 1.32))
plt.gcf().subplots_adjust(top=.76)
plt.title(f"{get_method_title(method)} on {get_dataset_title(dataset)}:\nUMAP of Untreated Control vs {pert}")
plt.savefig(os.path.join(save_dir, f"{method}{optional_save_prefix}_controls_vs_pert_{pert}.png"), dpi=300)
def untreated_with_treated_control_umap(adata, control_strings, method, dataset, normalize, just_inflammatory_genes=False):
"""
plots UMAP of 4 control populations: untreated safe target, untreated no target, treated safe target, treated no target
plots UMAP of treated vs untreated controls
"""
adata = adata[adata.obs["gene_target"].isin(control_strings)]
if just_inflammatory_genes:
if method != "raw": ##this will only work on raw data, scFMs do not preserve raw gene counts for (+) controls
print(f"skipping untreated_with_treated_control_umap() with method={method}")
return
full_matrix = preprocess_matrix(adata.X, standardize=False, normalize=True)
library_sets = get_relevant_pathways(just_this_study=True, include_imru_internal=True, key_type="tuple")
inflammatory_genes = set()
for pathway in library_sets:
inflammatory_genes = inflammatory_genes.union(set(library_sets[pathway]))
print("inflammatory genes: ", sorted(inflammatory_genes))
feature_names = list(adata.var.index)
feature_coordinates = np.array([i for i in range(0, len(feature_names)) if feature_names[i] in inflammatory_genes])
coordinates = full_matrix[:, feature_coordinates] ##select just the columns of the positive controls
coordinates = get_umap(coordinates) ##perform UMAP on just these positive control features as final coordinates to plot
else:
coordinates = adata.obsm["umap"] ##take existing UMAP coordinates pre-computed over all feature space
##scatter plot of 4 categories - treated safe, treated no, untreated safe, untreated no
treated_safe = np.array([i for i in range(0, len(adata)) if adata.obs["condition"].iloc[i] == "Treated" and adata.obs["gene_target"].iloc[i] == "SAFE_TARGET"])
treated_no = np.array([i for i in range(0, len(adata)) if adata.obs["condition"].iloc[i] == "Treated" and adata.obs["gene_target"].iloc[i] == "NO-TARGET"])
untreated_safe = np.array([i for i in range(0, len(adata)) if adata.obs["condition"].iloc[i] == "Untreated" and adata.obs["gene_target"].iloc[i] == "SAFE_TARGET"])
untreated_no = np.array([i for i in range(0, len(adata)) if adata.obs["condition"].iloc[i] == "Untreated" and adata.obs["gene_target"].iloc[i] == "NO-TARGET"])
mapp = {"untreated safe target": untreated_safe, "untreated no target": untreated_no, "treated safe target": treated_safe, "treated no target": treated_no}
fig, ax = plt.subplots()
for key in mapp:
indices = mapp[key]
sub_coordinates = coordinates[indices]
ax.scatter(sub_coordinates[:,0], sub_coordinates[:,1], label=key, s=1.0)
box = ax.get_position()
ax.set_position([box.x0, box.y0, box.width, box.height * 0.85])
ax.legend(loc='upper right', prop={"size":7}, bbox_to_anchor=(1, 1.32))
plt.gcf().subplots_adjust(top=.76)
plt.title(f"UMAP of {get_method_title(method)} Embeddings\nfor Control Cells")
plt.savefig(f"outputs/{dataset}/{method}_controls.png", dpi=300)
##scatter plot of treated vs untreated
treated = np.concatenate((treated_safe, treated_no))
untreated = np.concatenate((untreated_safe, untreated_no))
mapp = {"untreated": (untreated, "lime", 0), "treated": (treated, "deepskyblue", 1)}
fig, ax = plt.subplots()
for key in mapp:
sub_coordinates = coordinates[mapp[key][0]]
ax.scatter(sub_coordinates[:,0], sub_coordinates[:,1], label=key.capitalize(), s=1.0, color=mapp[key][1], zorder=mapp[key][2])
ax.set_xlabel("UMAP 1")
ax.set_ylabel("UMAP 2")
box = ax.get_position()
ax.set_position([box.x0, box.y0, box.width, box.height * 0.85])
ax.legend(loc='upper right', prop={"size":7}, bbox_to_anchor=(1, 1.15))
plt.gcf().subplots_adjust(top=.76)
if method == "umap":
plt.title(f"UMAP of Raw Transcriptome\nfor Control Cells")
else:
plt.title(f"UMAP of {get_method_title(method)} Embeddings\nfor Control Cells")
save_string = f"outputs/{dataset}/{method}_controls_treatment_type_only"
if method == "raw" and just_inflammatory_genes:
plt.title("UMAP of Just Inflammatory Genes\nfor Control Cells")
save_string += "_only_inflammatory_gene_features"
plt.savefig(f"{save_string}.png", dpi=300)
def get_knockdown_efficiencies_Replogle(control_strings):
"""
calculate KD stats by method of Reploge 2020 paper (https://pmc.ncbi.nlm.nih.gov/articles/PMC7416462/):
do a sum normalization to the median UMI and ignore genes with an average of <1 UMI
"""
adata = sc.read_h5ad("data/raw_imru_full_processed.h5ad")
##set up basic data structures
gene_columns = list(adata.var.index)
target_set = list(set(adata.obs["gene_target"]))
target_set = [x for x in target_set if x not in control_strings] ##remove controls from the gene target set
target_to_col_index = {t: gene_columns.index(t) for t in target_set} ##index of each target in the columns of X
target_list = list(adata.obs["gene_target"])
median_UMI = np.median(adata.obs["nUMI"])
print(f"median UMI over all cells: {median_UMI}")
X = adata.X
X = sum_normalize_matrix(X, total=median_UMI, log1p=False) ##sum normalize matrix to median_UMI
##get control average (average after sum normalization)
control_row_indices = np.array([i for i in range(0, len(target_list)) if target_list[i] in control_strings])
control_col_means = np.mean(X[control_row_indices], axis=0)
##calculate KD % for all cells
kds = [] ##list of kds for each cell
for i in range(0, len(target_list)):
print(i, len(target_list))
if target_list[i] in control_strings:
kds.append(-1.0) ##controls will get -1.0 (actual value won't matter when we calculate KD on a per target basis - these values will be ignored)
else:
target_col_index = target_to_col_index[target_list[i]]
kds.append(1.0 - (X[i][target_col_index] / control_col_means[target_col_index]))
kds = np.array(kds)
##calculate avg KD% for all targets
target_to_row_indices = {t: [i for i in range(0, len(target_list)) if target_list[i] == t] for t in target_set}
target_to_avg_kd = {t: np.mean(kds[target_to_row_indices[t]]) for t in target_set}
print("all targets: ", np.mean(list(target_to_avg_kd.values())), np.median(list(target_to_avg_kd.values())))
##calculate avg KD% for just high UMI targets
avg_columns = np.mean(adata.X, axis=0) ##use raw matrix for filtering out gene targets (not normalized) for computing gene UMI averages
gene_indices_with_high_UMI = [i for i in range(0, len(avg_columns)) if avg_columns[i] >= 1.0] ##ignore genes with an average of <1 UMI
targets_with_high_UMI = set([t for t in target_to_col_index if target_to_col_index[t] in gene_indices_with_high_UMI]) ##set of targets with UMI >= 1.0 in matrix
print(f"{len(targets_with_high_UMI)} targets with UMI >= 1.0, keeping for KD calculation")
target_to_avg_kd = {t: np.mean(kds[target_to_row_indices[t]]) for t in targets_with_high_UMI}
print("just targets with high UMI: ", np.mean(list(target_to_avg_kd.values())), np.median(list(target_to_avg_kd.values())))
##save results
pickle.dump((kds, target_list, target_to_row_indices, targets_with_high_UMI), open("pickles/kd_calculation.pkl", "wb"))
def plot_knockdown_efficiencies():
"""
plot results of KD efficiency calculations according to the Replogle paper
will use this for main results figures because more standard
"""
kds, target_list, target_to_row_indices, targets_with_high_UMI = pickle.load(open("pickles/kd_calculation.pkl", "rb"))
target_to_avg_kd = {t: np.mean(kds[target_to_row_indices[t]]) for t in target_to_row_indices}
target_kds = list(target_to_avg_kd.values())
print(f"KD % stats: mean: {np.mean(target_kds)}, median: {np.median(target_kds)}, min: {np.min(target_kds)}, max: {np.max(target_kds)}, std: {np.std(target_kds)}")
##high UMI targets only
high_target_to_avg_kd = {t: np.mean(kds[target_to_row_indices[t]]) for t in targets_with_high_UMI}
high_target_kds = list(high_target_to_avg_kd.values())
print(f"KD % stats (high UMI): mean: {np.mean(high_target_kds)}, median: {np.median(high_target_kds)}, min: {np.min(high_target_kds)}, max: {np.max(high_target_kds)}, std: {np.std(high_target_kds)}")
##make plots off all targets
less_than_eq_0 = sum([1 for x in target_kds if x <= 0])
less_than_eq_neg_10 = sum([1 for x in target_kds if x <= -10])
greater_than_0 = sum([1 for x in target_kds if x > 0])
print(f"frequency of cells > 0: {float(greater_than_0 / (greater_than_0 + less_than_eq_0))}")
print(greater_than_0, less_than_eq_0, less_than_eq_neg_10)
##bar chart
fig, ax = plt.subplots()
x = np.array([0, 1, 2])
y = [greater_than_0, less_than_eq_0, less_than_eq_neg_10]
bars = ax.bar(x, y)
ax.bar_label(bars, fmt="%.2e", fontsize=7)
ax.set_xticks(x)
ax.set_xticklabels(["KD > 0.0", "KD <= 0.0", "KD <= -10.0"])
ax.set_ylabel("Cell Count")
plt.title("Cell Count by Knockdown Percentage")
plt.ticklabel_format(axis='y', style='scientific', scilimits=(0,0))
plt.savefig("outputs/knockdown_percentage_bar.png", dpi=300)
##histogram
fig, ax = plt.subplots()
ax.hist(target_kds, bins=100)
ax.set_xlim((-1.0, 1.0))
ax.set_ylabel("Target Count")
ax.set_xlabel("Knockdown Efficiency")
plt.title("Knockdown Efficiency Histogram")
ax.set_xticklabels(['{:,.0%}'.format(x_) for x_ in ax.get_xticks()])
plt.savefig("outputs/knockdown_percentage_histogram.png", dpi=300)
def data_stats(adata_path):
"""
basic stats about data written to outputs/
"""
adata = sc.read_h5ad(adata_path)
X = adata.X.toarray()
##get basic stats about data
print(f"read count stats: median {np.median(X)}, fraction zero: {1.0 - (np.count_nonzero(X) / (X.shape[0] * X.shape[1]))}")
##counts per cell
axis1_sum = np.sum(X, axis=1).flatten() ##sum for each cell
fig, ax = plt.subplots()
ax.violinplot(axis1_sum, showmedians=True)
ax.set_ylabel("Transcript Count")
plt.title(f"Violin Plot of Transcripts per Cell\n {get_stats(axis1_sum)}", fontsize=9)
plt.savefig(f"outputs/violin_transcripts_per_cell.png", dpi=300)
fig, ax = plt.subplots()
n, bins, _ = plt.hist(axis1_sum, bins=100)
ax.set_xlabel("Transcript Count")
ax.set_ylabel("Cell Count")
plt.ticklabel_format(axis='both', style='sci', scilimits=(0,0))
plt.title(f"Histogram of Transcripts per Cell")
plt.savefig(f"outputs/histogram_transcripts_per_cell.png", dpi=300)
##counts per gene
axis0_sum = np.sum(X, axis=0).flatten() ##sum for each gene
print("genes that have 0 sum: ", len(X[0]) - np.count_nonzero(axis0_sum))
fig, ax = plt.subplots()
ax.violinplot(axis0_sum, showmedians=True)
ax.set_ylabel("# of transcripts")
plt.title(f"Violin Plot of Transcripts per Gene\n{get_stats(axis0_sum)}", fontsize=9)
plt.savefig(f"outputs/violin_transcripts_per_gene.png", dpi=300)
fig, ax = plt.subplots()
n, bins, _ = plt.hist(axis0_sum, bins=100)
ax.set_xlabel("Number of Transcripts")
ax.set_ylabel("# of Genes")
plt.title(f"Histogram of Transcripts per Gene\n {get_stats(axis1_sum)}", fontsize=9)
plt.savefig(f"outputs/histogram_transcripts_per_gene.png", dpi=300)
##histogram of ratio of zeros for each gene
zeros = (len(X) - np.count_nonzero(X, axis=0)) / len(X)
fig, ax = plt.subplots()
n, bins, _ = plt.hist(zeros, bins=100)
ax.set_xlabel(f"% of Cells with Zero Expression")
ax.set_ylabel("# of Genes")
plt.title(f"Histogram of Cells \n with Zero Frequencies per Gene Median={round(np.median(zeros), 3)}")
plt.savefig(f"outputs/histogram_zeros_per_gene.png", dpi=300)
##plot x-axis: sorted genes by zero frequency, y-axis: 0 frequency
fig, ax = plt.subplots()
sorted_zeros = np.sort(zeros)
ax.plot(np.array(range(0, len(sorted_zeros))), sorted_zeros)
plt.ticklabel_format(axis='y', style='sci', scilimits=(0,0))
plt.title(f"% Cells with Zero Read Count for each Gene")
ax.set_xlabel("Gene Indices Sorted by % Cells with Zero Read Count for G")
ax.set_ylabel("% Cells with Zero Read Count for G")
plt.savefig(f"outputs/line_graph_zeros_per_gene.png", dpi=300)
def preprocess_matrix(X, standardize=False, normalize=False):
"""
Given matrix X, will perform preprocessing operations and return a copy
"""
X_new = X.copy()
if standardize: ##standardize features (zero mean, unit variance)
scaler = StandardScaler()
X_new = scaler.fit_transform(X_new)
print("standardized matrix")
if normalize: # normalize each sample to unit norm (L2)
X_new = sklearn_normalize(X_new, norm='l2')
print("(l2) normalized matrix")
return X_new
def subsample_matrix(X, y, sample_pct=0.5, n=10, random_state=None, constraint="keep_ratio"):
"""
if constraint == "keep_ratio"
for each class in y, sample `sample_pct` percent of them to keep and return a subsampled version of X, y.
n must equal None
if constraint == "equal_count"
for each class in y, sample n at random and return a subsampled version of X, y.
sample_pct must equal None
if n is greater than total size of class y_i, will keep all of y_i
Parameters:
- X: np.ndarray or similar, shape (n_samples, n_features)
- y: list of strings
- sample_pct: float, percentage of samples to keep per class (between 0 and 1) when constraint == "keep_ratio"
_ sample_n: n to use when constraint == "equal_count"
- random_state: int or None, for reproducibility
Returns:
- X_sub: subsampled version of X
- y_sub: subsampled version of y
"""
if constraint == "keep_ratio":
assert n == None
if constraint == "equal_count":
assert sample_pct == None
if random_state is not None:
np.random.seed(random_state)
unique_classes = set(y)
indices_to_keep = []
for cls_ in unique_classes:
cls_indices = np.array([i for i in range(0, len(y)) if y[i] == cls_])
if constraint == "keep_ratio":
n_samples = int(len(cls_indices) * sample_pct)
elif constraint == "equal_count":
n_samples = min(n, len(cls_indices))
sampled_indices = np.random.choice(cls_indices, n_samples, replace=False)
indices_to_keep.extend(sampled_indices)
indices_to_keep = np.array(indices_to_keep)
return X[indices_to_keep], [y[i] for i in indices_to_keep]
def subsample_adata_to_min_replicate(adata, n=10):
"""
given adata and a minimum replicate count will return an adata object with just samples that have at least n replicates for gene_target for each treatment condition (needs to have min count for treated and min count for untreated)
"""
treated_adata = adata[adata.obs["condition"] == "Treated"]
treated_counts = Counter(treated_adata.obs["gene_target"])
untreated_adata = adata[adata.obs["condition"] == "Untreated"]
untreated_counts = Counter(untreated_adata.obs["gene_target"])
key_intersection = [key for key in treated_counts if key in untreated_counts] ##if a key is only present in either Treated OR Untreated (but not both), then remove it
keep_pert = set([key for key in key_intersection if treated_counts[key] > 10 and untreated_counts[key] > 10])
indices = [i for i in range(0, len(adata.obs["gene_target"])) if adata.obs["gene_target"].iloc[i] in keep_pert]
return adata[indices]
def get_true_and_random_clustering_scores(matrix, labels, clustering_metric):
"""
given a matrix of embeddings, a list of labels, and a clustering method to use
returns a list of clustering scores for true labels, and a list of clustering scores for random labels (for each trial)
if clustering_metric == "Silhouette"
will subsample matrix for each unique label (such that each label has 10 unique samples), and will do 30 trials of random labels (Silhouette is computationally expensive)
if clusering_metric == "Calinski-Harabasz"
will not do any subsampling (will use entire matrix), and will run 30 trials of randomized labels
"""
true_scores = []
random_scores = []
if clustering_metric == "Calinski-Harabasz":
true_scores.append(calinski_harabasz_score(matrix, labels))
for trial in range(0, 30):
random_labels = random.sample(labels, len(labels))
r_s = calinski_harabasz_score(matrix, random_labels)
random_scores.append(r_s)
elif clustering_metric == "Silhouette": ##silhouette is computationally expensive, parallelize this
for trial in range(0, 30):
init_time = time.time()
##pairwise distance matrix of all points for this dataset is infeasible and will require more memory than we can possibly have (about 2.5 TB at 16 point precision to just store the resulting distance matrix) --> subsample the matrix
sub_matrix, sub_labels = subsample_matrix(matrix, labels, sample_pct=None, n=10, random_state=None, constraint="equal_count")
sub_matrix = sub_matrix.astype(np.float16) ##huge memory requirements to compute pairwise distance, cast as 16 bit
sub_random_labels = random.sample(sub_labels, len(sub_labels)) ##need to get random labels again if subsampled labels
distance_matrix = pairwise_distances(sub_matrix, metric='cosine', n_jobs=-1)
# print(f"completed pairwise calculation of matrix with shape: {sub_matrix.shape} in {round(time.time() - init_time, 2)}")
if np.sum(distance_matrix.diagonal()) != 0:
print(f"WARNING: diagonal of distance_matrix (with shape {distance_matrix.shape}) was NOT all zeros, filling with zeros\ndiagonal of distance_matrix max: {np.max(distance_matrix.diagonal())}, avg: {np.mean(distance_matrix.diagonal())}, sum: {np.sum(distance_matrix.diagonal())}")
np.fill_diagonal(distance_matrix, 0)
true_scores.append(silhouette_score(distance_matrix, sub_labels, metric='precomputed'))
random_scores.append(silhouette_score(distance_matrix, sub_random_labels, metric='precomputed'))
else:
raise Exception("clustering metric must be in [Calinski-Harabasz, Silhouette]")
return true_scores, random_scores
def score_perturbation_clusters(adata, control_strings, method="umap", dataset=None, knockdown_threshold=500.0, normalize=False):
"""
score clustering based on true labels vs random labels
for two setup types:
(1) labels are treatment condition - will do separately for Control and Perturbed cells
(e.g. 4 clustering scores using treatment condition as cluster label: true control, random control, true perturbed, random perturbed)
(2) labels are gene targets (with same treatment condition) - will remove the control cells
and use all perturbations from adata, and do separately for Treated and Untreated
(e.g. 4 clustering scores using gene target as cluster label: true treated, random treated, true untreated, random untreated
will filter adata and select cells where .obs["knockdown_efficiencies"] (in this case ratio of perturbed / control expression) is < knockdown_threshold
writes figure to outputs/
writes dictionary to pickle file with key: label_type, key: treatment, key: random or true, key: clustering_metric, value: clustering score
"""
adata = adata[adata.obs["knockdown_efficiencies"] < knockdown_threshold]
clustering_metrics = ["Calinski-Harabasz"]
label_types = ["Gene Target", "Treatment Condition"] ## label points either by treatment condition or by perturbation (where perturbations all have the same treatment condition)
shuffle_states = ["true", "random"]
embedding_types = ["method_embedding", "umap_embedding"] #embedding_type will dictate what embeddings to use for labeling and scoring: "method_embedding" will be the raw embedding of the method, "umap_embedding" will be the UMAP of the raw embedding of the method
results_map = {embedding_type: {label_type: {condition: {shuffle_state: {clustering_metric: "" for clustering_metric in clustering_metrics} for shuffle_state in shuffle_states} for condition in ["Treated", "Untreated", "Control", "Perturbed"]} for label_type in label_types} for embedding_type in embedding_types}
for embedding_type in embedding_types:
for clustering_metric in clustering_metrics:
for label_type in label_types:
if label_type == "Gene Target":
split_bys = ["Treated", "Untreated"] ##split by treated and untreated
elif label_type == "Treatment Condition":
split_bys = ["Control", "Perturbed"] ##split by control or perturbed
else:
raise Exception()
##for label_type == Gene Target: ensure that all perturbations are either treated or untreated, and we don't mix them together
##for label_type == Treatment Condition: split_bys will be either control cells or perturbed cells
for s_b in split_bys:
##get matrix and labels for clustering based on label_type
if label_type == "Gene Target": ##for given condition, use all genetic perturbations in analysis
print(f"for label_type={label_type} removing controls, old shape: {adata.X.shape}")
sub_adata = adata[~adata.obs["gene_target"].isin(control_strings)]
print(f"new shape: {sub_adata.X.shape}")
sub_adata = sub_adata[sub_adata.obs["condition"] == s_b]
labels = list(sub_adata.obs["gene_target"])
elif label_type == "Treatment Condition":
if s_b == "Control":
print(f"for label_type={label_type}, split={s_b} keeping just controls, old shape: {adata.X.shape}")
sub_adata = adata[adata.obs["gene_target"].isin(control_strings)]
print(f"new shape: {sub_adata.X.shape}")
elif s_b == "Perturbed":
print(f"for label_type={label_type}, split={s_b} keeping just perturbed cells, old shape: {adata.X.shape}")
sub_adata = adata[~adata.obs["gene_target"].isin(control_strings)]
else:
raise Exception()
labels = list(sub_adata.obs["condition"])
else:
raise Exception()
if embedding_type == "method_embedding":
matrix = sub_adata.X
else:
matrix = sub_adata.obsm["umap"]
if normalize:
matrix = preprocess_matrix(matrix, standardize=False, normalize=normalize)
##now that we have the matrix and labels, get clustering score of true and score of random, write to dictionary
true_scores, random_scores = get_true_and_random_clustering_scores(matrix, labels, clustering_metric)
results_map[embedding_type][label_type][s_b]["true"][clustering_metric] = true_scores
results_map[embedding_type][label_type][s_b]["random"][clustering_metric] = random_scores
##print results of true vs random for each s_b
for s_b in split_bys:
print(f"{dataset} {method}")
print(f" {clustering_metric} {label_type} {s_b} delta: true - random: {np.mean(results_map[embedding_type][label_type][s_b]['true'][clustering_metric]) - np.mean(results_map[embedding_type][label_type][s_b]['random'][clustering_metric])}")
if clustering_metric != "Silhouette": ##ratio of silhouette scores can be misleading (if both are negative ratio < 1 indicates true is better than random, but if both are positive a ratio < 1 means true is worse than random
print(f" {clustering_metric} {label_type} {s_b} ratio: {s_b} true / random: {np.mean(results_map[embedding_type][label_type][s_b]['true'][clustering_metric]) / np.mean(results_map[embedding_type][label_type][s_b]['random'][clustering_metric])}")
##bar plot of clustering metric scores
fig, ax = plt.subplots()
x_labels = split_bys
width = 0.20
x = np.array(range(0, len(x_labels)))
ax.set_xticks(x)
ax.set_xticklabels(x_labels)
for shuffle_state in shuffle_states:
y = [np.mean(results_map[embedding_type][label_type][x_label][shuffle_state][clustering_metric]) for x_label in x_labels]
bars = ax.bar(x, y, width=width, label=f"{shuffle_state} target labels")
x = x + width
if max(y) > 100:
plt.ticklabel_format(axis='y', style='scientific', scilimits=(0,0))
fmt = '%.2E'
else:
fmt = '%.2f'
ax.bar_label(bars, fmt=fmt, fontsize=7)
box = ax.get_position()
ax.set_position([box.x0, box.y0, box.width, box.height * 0.85])
ax.legend(loc='upper right', prop={"size":7}, bbox_to_anchor=(1, 1.32))
plt.gcf().subplots_adjust(top=.76)
ax.set_ylabel(f"{clustering_metric} Score")
plt.title(f"{clustering_metric} Scores for\nTrue vs Randomized {label_type} in {get_method_title(method)} Space")
plt.savefig(f"outputs/{dataset}/cluster_{embedding_type}_{method}_{label_type}_{clustering_metric}_vs_random_mean_{knockdown_threshold}.png", dpi=300, bbox_inches="tight")
##save pickle of results
pickle.dump(results_map, open(f"pickles/clustering_scores/{dataset}_{method}_{knockdown_threshold}.pkl", "wb"))
def compare_method_replicate_similarity():
"""
Compare each method's clustering of true vs random using two different clustering metrics:
(1) Calinski-Harabasz
will plot the ratio of true score / random score
(2) Silhouette
will pot the difference (true score - random score)
will look at label = Gene Target or label = Treatment Condition
for Gene Target, will separate Treated vs Untreated
for Treatment Conditon, will just plot control cells for comparison (eliminates confounder of perturbation)
will plot both using the method's embeddings as the representations vs using the UMAP of the method's embeddings as the representations (embedding_type)
saves plot to outputs/
"""
knockdown_threshold = 500.0
width = 0.35
color_map = {"Treated": "midnightblue", "Untreated": "violet", "Perturbed": "gold", "Control": "blue"}
for embedding_type in ["method_embedding"]:
if embedding_type == "method_embedding":
methods = ["random", "raw", "umap", "scimilarity", "scgpt", "state"]
x_labels = [get_method_title(method) for method in methods]
if embedding_type == "umap_embedding":
methods = ["random", "umap", "scimilarity", "scgpt", "state"] ##embedding_type == "umap_embedding" will take the .obsm["umap"] of the adata object, the raw adata .obsm["umap"] and umap data .obsm["umap"] will be different (two independent UMAP reductions of raw space with different random seeds - see preprocessing.py) --> just show one of them
x_labels = [f"UMAP({get_method_title(method)})" if method != "umap" else f"UMAP(Raw)" for method in methods]
x = np.arange(0, len(x_labels))
method_map = {m: pickle.load(open(f"pickles/clustering_scores/full_{m}_{knockdown_threshold}.pkl", "rb")) for m in methods}
for label_type in ["Gene Target", "Treatment Condition"]:
if label_type == "Gene Target":
split_bys = ["Treated", "Untreated"]
if label_type == "Treatment Condition":
split_bys = ["Control"] ##let's just look at the control cells to reduce variability and the added complexity of also introducing a perturbation (split_by="Perturbation")
for clustering_metric in method_map["raw"][embedding_type][label_type][split_bys[0]]["true"]:
fig, ax = plt.subplots()
ax.set_xticks(x)
ax.set_xticklabels(x_labels, rotation=40, ha="right", rotation_mode='anchor')
if clustering_metric == "Calinski-Harabasz": ##ratio between true labels and random
operator = "Ratio"
ax.set_ylabel(f"{clustering_metric} Ratio (True / Random)", fontsize=11)
if clustering_metric == "Silhouette": ##difference between true labels and random
operator = "Difference"
ax.set_ylabel(f"{clustering_metric} Difference (True - Random)", fontsize=11)
ax.set_xlabel("Embedding Type", fontsize=11)
for split_by in split_bys:
true_list = [method_map[method][embedding_type][label_type][split_by]["true"][clustering_metric] for method in methods] ##list of lists ordered by method
random_list = [method_map[method][embedding_type][label_type][split_by]["random"][clustering_metric] for method in methods]
if clustering_metric == "Calinski-Harabasz": ##ratio between true labels and random
y = [np.mean(true_list[i]) / np.mean(random_list[i]) for i in range(0, len(methods))]
##yerr will be the std of a list of each true / random from each trial
yerr = [np.std([true_list[j][i] / random_list[j][i] for i in range(0, len(true_list[j]))]) for j in range(0, len(methods))]
if clustering_metric == "Silhouette": ##difference between true labels and random
y = [np.mean(true_list[i]) - np.mean(random_list[i]) for i in range(0, len(methods))]
yerr = [np.std([true_list[j][i] - random_list[j][i] for i in range(0, len(true_list[j]))]) for j in range(0, len(methods))]
bars = ax.bar(x, y, width=width,yerr=yerr, capsize=5, label=split_by, color=color_map[split_by])
if max(y) > 100 :
fmt = '%.1E'
plt.ticklabel_format(axis='y', style='scientific', scilimits=(0,0))
else:
fmt = '%.1f'
ax.bar_label(bars, fmt=fmt, fontsize=6)
x = x + width
if label_type == "Gene Target":
plt.title(f"{clustering_metric} {operator} Scores\nwith Cluster Labels = {label_type}")
if label_type == "Treatment Condition":
if split_bys == ["Control"]: ##if just looking at control cells and cluster labels = Treatment Condition
plt.title(f"{clustering_metric} {operator} Scores for Control Cells\nwith Cluster Labels = {label_type}")
else:
plt.title(f"{clustering_metric} {operator} Scores\nwith Cluster Labels = {label_type}")
if len(split_bys) > 1: ##if more than 1 split_bys add a legend
box = ax.get_position()
ax.set_position([box.x0, box.y0, box.width, box.height * 0.90])
ax.legend(loc='upper right', prop={"size":7}, bbox_to_anchor=(1, 1.25))
plt.gcf().subplots_adjust(top=.80)
plt.savefig(f"outputs/method_clustering_{knockdown_threshold}_{embedding_type}_{label_type}_{clustering_metric}.png", dpi=300, bbox_inches="tight")
def get_perturbation_counts_per_treatment(adata):
"""
returns dictionary with key treatment, value: Counter map for gene targets
"""
treatment_types = ["Treated", "Untreated"]
return_map = {"Treated": {}, "Untreated": {}}
for treatment_type in treatment_types:
sub_adata = adata[adata.obs["condition"] == treatment_type]
counts = Counter(sub_adata.obs["gene_target"])
return_map[treatment_type] = counts
return return_map
def get_opposite_anchor(anchor_state):
"""
return the opposite anchor string given anchor_state string
"""
if "Treated" in anchor_state:
return anchor_state.replace("Treated", "Untreated")
elif "Untreated" in anchor_state:
return anchor_state.replace("Untreated", "Treated")
else:
raise Exception()
def get_top_perturbations(adata, method="raw", top_n=5, anchor_state_candidate_treatment_types=[], min_sample=0, dataset=None, knockdown_threshold=500.0, normalize=False, distance_metric="cosine", embedding_type="method_embedding"):
"""
anchor_state_candidate_treatment_types is a list of tuples (anchor_state, treatment_type) to each use as an independent 'anchor' to compute cosine distances towards in the embedding space specified by method
treatment_type is the condition of candidates that we are interested in (e.g. just find the treatment_type = "Treated" candidates closest to my anchor_state)
will call rank_perturbations_by_similarity
will also call perturbation_vs_control_umap to visualize the top n of these perturbations
min_sample is the minimum number of cells for a condition+perturbation to be included in the list, will filter out any condition+perturbation that does not meet this minimal size
returns:
(1) dictionary with key: (anchor_state, treatment_type), value: ranked list of tuples(gene target, cosine distance) (ascending by cosine distance)
(2) count map with key: treatment_type, key: perturbation, value: # of cells
"""
##filter cells out that are >= knockdown threshold
subadata = adata[(adata.obs["knockdown_efficiencies"] < knockdown_threshold)] ##will keep control cells because these have a knockdown efficiency of -1.0
print(f"knockdown_threshold of {knockdown_threshold} filtered {len(adata) - len(subadata)} cells out")
anchor_map = {a_t: [] for a_t in anchor_state_candidate_treatment_types} ##key: (anchor_state, treatment_type) tuple, value: ranked list of (gene target, cosine) tuples
count_map = get_perturbation_counts_per_treatment(subadata) ##key: condition, key: perturbation name, value: sample size / number of cells
for (anchor_state, treatment_type) in anchor_state_candidate_treatment_types:
assert "Treated" in anchor_state or "Untreated" in anchor_state
sorted_perts = rank_perturbations_by_similarity(subadata, anchor_state=anchor_state, normalize=normalize, distance_metric=distance_metric, embedding_type=embedding_type) ##list of tuples: (perturbation, cosine)
condition_gene_target_cosines = [x for x in sorted_perts if treatment_type in x[0]] ##from the sorted_perts, get just the treated perts for to_healthy, or untreated perts for to_disease
gene_target_cosines = [(x[0].replace(f"{treatment_type}_", ""), x[1]) for x in condition_gene_target_cosines] ##get rid of treatment condition in candidate strings
gene_target_cosines = [(x[0], x[1]) for x in gene_target_cosines if count_map[treatment_type][x[0]] >= min_sample] ##filter by sample size
anchor_map[(anchor_state, treatment_type)] = gene_target_cosines
plot_perts = [x[0] for x in gene_target_cosines[0:top_n]] ##top n perts - just perturbation target names
print(f"{dataset} {method} knockdown_thresh: {knockdown_threshold}, top {top_n} perts most similar to {anchor_state}: {plot_perts}")
return anchor_map, count_map
def create_state_precursor_h5ads():
"""
state requires a column called gene_name in .var dataframe to use for gene names,
writes a new AnnData object data/state_imru_precursor.h5ad
after method completes, can process written AnnData object further via:
uv run state emb transform \
--model-folder /home/wongd26/mlcs/mlhub/state_SE-600M \
--checkpoint /home/wongd26/mlcs/mlhub/state_SE-600M/se600m_epoch16.ckpt \
--input /hpfs/userws/wongd26/imru-reversion/data/state_imru_precursor.h5ad \
--output /hpfs/userws/wongd26/imru-reversion/data/state_imru.h5ad
"""
##full dataset
adata = sc.read_h5ad("data/imru_full.h5ad")
adata.var["gene_name"] = adata.var.index
adata.write("data/state_imru_precursor_full.h5ad")
##indication specific subsets
dataset_to_gene_names = extract_indication_genes()
for dataset in dataset_to_gene_names:
adata = sc.read_h5ad(f"data/imru_{dataset}.h5ad")
adata.var["gene_name"] = adata.var.index
adata.write(f"data/state_imru_precursor_{dataset}.h5ad")
def sum_normalize_matrix(X, total=1e6, log1p=True):
"""
given 2D matrix X, will have it such that each row of X sums to total,
if log1p is true, will apply log1p (ln(1 + x))
returns new matrix
if a row is all zeros, will keep it as is
"""
row_sums = X.sum(axis=1, keepdims=True)
row_sums[row_sums == 0] = 1.0
normalized_X = (X / row_sums) * total
if log1p:
normalized_X = np.log1p(normalized_X)
return normalized_X
def get_knockdown_efficiency(adata, control_strings):
"""
given adata, will compute the knockdown efficiency for each cell:
count of cell-normalized perturbed gene G / cell-normalized avg expression of G in control population in same treatment condition
return adata with knockdown efficiencies added to .obs["knockdown_efficiencies"]
Note that this is not the traditional KD efficiency as presented in Replogle, but a ratio of raw count of target gene relative to control
See get_knockdown_efficiencies_Replogle(control_strings) for the more standard approach
"""
treated_control_indices = np.array([i for i in range(0, len(adata)) if adata.obs["condition"].iloc[i] == "Treated" and adata.obs["gene_target"].iloc[i] in control_strings])
untreated_control_indices = np.array([i for i in range(0, len(adata)) if adata.obs["condition"].iloc[i] == "Untreated" and adata.obs["gene_target"].iloc[i] in control_strings])
X = sum_normalize_matrix(adata.X.toarray(), log1p=True)
condition_to_avg_control_vector = {"Treated": np.mean(X[treated_control_indices], axis=0), "Untreated": np.mean(X[untreated_control_indices], axis=0)}
columns = list(adata.var.index)
efficiencies = []
for i in range(0, len(adata.obs["gene_target"])): ##i will be cell index (row)
gene_pert = adata.obs["gene_target"].iloc[i]
if gene_pert in ["NO-TARGET", "SAFE_TARGET"]: ##if any of the control strings, use -1.0
efficiencies.append(-1.0)
else:
condition = adata.obs["condition"].iloc[i]
column_index = columns.index(gene_pert)
perturbed_gene_count = X[i][column_index]
avg_expression = condition_to_avg_control_vector[condition][column_index]
efficiencies.append(perturbed_gene_count / avg_expression)
if i % 5000 == 0:
print(i, len(adata.obs["gene_target"]))
adata.obs["knockdown_efficiencies"] = efficiencies
return adata
def get_method_title(string):
"""
Get title string of method and return
"""
if "SCGPT" in string.upper():
return "scGPT"
elif "SCIMILARITY" in string.upper():
return "SCimilarity"
elif "UMAP" in string.upper():
return "UMAP"
elif "STATE" in string.upper():
return "STATE"
elif "RANDOM" in string.upper():
return "Random"
elif "RAW" in string.upper():
return "Raw"
elif "CHATGPT5.0" in string.upper():
return "ChatGPT 5.0"
elif "COMPBIO_DIRECT" in string.upper():
# return "Traditional Comp Bio (Inflammatory)"
return "DE (Inflammatory)"
elif "COMPBIO_MANUAL" in string.upper():
return "Comp Bio Manual"
elif "COMPBIO_VOX" in string.upper():
return "Comp Bio Vox"
elif "COMPBIO_UNTREATED_DIRECT" in string.upper():
# return "Traditional Comp Bio (Basal)"
return "DE (Basal)"
else:
raise Exception(f"method {string} not accounted for")
def get_dataset_title(string):
"""
Get title string of dataset and return
"""
if string.upper() == "FULL":
return "Full Dataset"
elif string.upper() in ["ALSAIGH", "PASS", "TELLIDES"]:
return string.capitalize()
else:
raise Exception(f"no dataset found for string {string}")
def get_sorted_perts(dataset="full", method=None, knockdown_threshold=500.0, min_sample=10):
"""
loads the sorted target map and count map, filters the target rankings by min_sample for cell counts
"""
sorted_perts, count_map = pickle.load(open(f"pickles/{dataset}_{method}_sorted_perts_knockdown_threshold_{knockdown_threshold}_min_sample_0.pkl", "rb")) ##load ranking without any filtering and then apply cell count filter; dictionary with key: (anchor_state, treatment_type), value: ranked list of tuples(gene target, cosine distance) (ascending by cosine distance)
if min_sample != 0: ##if cell count filtering needs to be applied
for (anchor_state, treatment_type) in sorted_perts:
entries = sorted_perts[(anchor_state, treatment_type)] ##ranked list of tuples(gene target, cosine distance)
sorted_perts[(anchor_state, treatment_type)] = [(x[0], x[1]) for x in entries if count_map[treatment_type][x[0]] >= min_sample]
return sorted_perts, count_map
def plot_cosines(top_n=10, plot_type="distance", min_sample=0):
"""
Plot distribution of cosine similarities / distances (depending on plot_type) for the different methods
will also write CSVs with header ['Perturbation', f'Cosine Distance to {anchor_state}', 'Cell Count'] to outputs/cosines/
"""
if not os.path.isdir("outputs/cosines/"):
os.makedirs("outputs/cosines/")
knockdown_threshold = 500.0
methods = ["random", "raw", "umap", "scimilarity", "scgpt", "state"]
for method in methods:
sorted_perts, count_map = get_sorted_perts(method=method, min_sample=min_sample)
for (anchor_state, treatment_type) in sorted_perts:
perturbations = [x[0] for x in sorted_perts[(anchor_state, treatment_type)]]
cosine_dist = [x[1] for x in sorted_perts[(anchor_state, treatment_type)]]
cosine_sims = [1.0 - x[1] for x in sorted_perts[(anchor_state, treatment_type)]]
if plot_type == "distance":
cosine_plot = cosine_dist
plt_label = "Distance"
elif plot_type == "similarity":
cosine_plot = cosine_sims
plt_label = "Similarity"
else:
raise Exception()
counts = [count_map[treatment_type][perturbation] for perturbation in perturbations]
fig, ax = plt.subplots()
x_labels = perturbations[0: top_n]
x = np.array(range(0, len(x_labels)))
y = cosine_plot[0: top_n]
fontsize = 5 if top_n >= 25 else 9
if top_n <= 50:
ax.set_xticks(x)
ax.set_xticklabels(x_labels, rotation=90, fontsize=fontsize)
if plot_type == "similarity":
ax.bar(x, y[::-1]) ##reverse for just the plot so ascending order
else:
ax.bar(x, y)
ax.set_ylabel(f"Cosine {plt_label}")
if top_n <= 100:
plt.title(f"{get_method_title(method)}: Cosine {plt_label} for Top {min(top_n, len(perturbations))} Perturbations")
else:
plt.title(f"Distribution of Cosine {plt_label} for {get_method_title(method)}")
if plot_type == "similarity":
ax.set_ylim((min(cosine_plot) - 0.02, 1.0))
plt.savefig(f"outputs/cosines/full_{method}_{treatment_type}_{anchor_state}_{knockdown_threshold}_{min_sample}_top_n.png", dpi=300, bbox_inches="tight")
##plot histogram of all cosines
fig, ax = plt.subplots()
ax.hist(cosine_plot, bins=50)
ax.set_ylabel(f"Cosine {plt_label}")
plt.title(f"{get_method_title(method)}: Histogram of Cosine Similarities for All Perturbations")
plt.savefig(f"outputs/cosines/full_{method}_{treatment_type}_{anchor_state}_{knockdown_threshold}_{min_sample}_histogram.png", dpi=300, bbox_inches="tight")
##write csv of all cosine *DISTANCES*
output_filename = f'outputs/cosines/full_{method}_{treatment_type}_perturbations_closest_to_{anchor_state}_{knockdown_threshold}_{min_sample}.csv'
with open(output_filename, 'w', newline='') as csvfile:
writer = csv.writer(csvfile)
writer.writerow(['Perturbation', f'Cosine Distance to {anchor_state}', 'Cell Count'])
for item1, item2, item3 in zip(perturbations, cosine_dist, counts):
writer.writerow([item1, item2, item3])
print(f"CSV file '{output_filename}' created successfully.")
def get_library_abbrev(string):
"""
given library string will return an abbreviated string
"""
if string == "imru_internal":
return "IMRU"
elif string == "MSigDB_Hallmark_2020":
return "MSig"
elif "KEGG" in string.upper():
return "KEGG"
elif string == "GO_Biological_Process_2025":
return "GO"
elif "BIOCARTA" in string.upper():
return "BioCarta"
elif "REACTOME" in string.upper():
return "Reactome"
elif string == "curated":
return "Curated"
elif string == "curated filtered":
return "Curated Filtered"
elif string == "fgsea enrichment":
return "FGSEA Enrichment"
else:
print(f"WARNING: no library abbreviation for {string}")
return string
def enrichment_runner(min_sample=0):
"""
runner call for enrichment analyses, saves to outputs/
"""
library_sets = get_relevant_pathways(just_this_study=True, include_imru_internal=True, key_type="tuple")
##dictionary that will be used for plotting the main permutation of interest
##key: method, key: pathway, value: AUC for parameter-combo of interest: dataset=="full", kd=500.0 (no filtering), target="Untreated_SAFE_TARGET"
plot_map = {}
bests = []
knockdown_threshold = 500.0
for library, pathway in library_sets:
abbrev = f"{get_library_abbrev(library)}: {pathway.upper()} (n={len(library_sets[(library, pathway)])})"
print(library, pathway)
meta_AUC, best_method = enrichment_plot(top_n=10, library=library, pathway=pathway, positive_controls=library_sets[(library, pathway)], save_dir=f"outputs/enrichment/{library}/{pathway}/", min_sample=min_sample, verbose=False)
bests.append(best_method)
for method in meta_AUC:
if method not in plot_map:
plot_map[method] = {abbrev: meta_AUC[method][knockdown_threshold]["Untreated_SAFE_TARGET"]}
else:
plot_map[method][abbrev] = meta_AUC[method][knockdown_threshold]["Untreated_SAFE_TARGET"]
print(f"enrichment best methods on standard permutation: {Counter(bests)}")
##plot a summary bar graph of main permutation of interest
fig, ax = plt.subplots()
width = .09
methods = ["random", "raw", "umap", "scimilarity", "scgpt", "state", "chatgpt5.0", "compbio_direct", "compbio_untreated_direct"]
x_labels = list(plot_map["raw"].keys())
print(x_labels)
x_labels = sorted([x_ for x_ in x_labels if "INFLAMMATION REVERSAL GIVEN" not in x_]) ##don't include (+) control set in summary annotated pathway plot
x = np.array(range(0, len(x_labels)))
ax.set_xticks(x)
for method in methods:
y = [plot_map[method][pathway] for pathway in x_labels]
ax.bar(x, y, width=width, label=get_method_title(method))
x = x + width
plt.yticks(fontsize=7)
ax.set_xticklabels(x_labels, rotation=40, fontsize=6, ha="right", rotation_mode='anchor')
ax.set_ylabel("AUC", fontsize=7)
ax.set_ylim((0.0, 1.02))
ax.set_title("Enrichment Comparison\n Across Relevant Pathways", fontsize=9)
ax.set_yticklabels(['{:,.0%}'.format(y_) for y_ in ax.get_yticks()])
box = ax.get_position()
ax.set_position([box.x0, box.y0, box.width, box.height * 0.85])
ax.legend(loc='upper right', prop={"size":7}, bbox_to_anchor=(1, 1.55))
plt.gcf().subplots_adjust(top=.76)
plt.savefig("outputs/enrichment/summary_enrichment_standard_permutation.png", dpi=300, bbox_inches="tight")
def enrichment_plot(top_n, library, pathway, positive_controls, save_dir, min_sample=0, verbose=False):
"""
Given a set of positive_controls, a directory to save to, will save enrichment plots
we can calculate enrichment scores of our ranked priority lists to see how early in the queue the (+) controls come up:
will also print how many (+) controls come up in top_n for each method+dataset
returns map with key: method, key KD threshold, key: anchor_state, value: AUC for this pathway and set of positive_controls, and also which method was the best on the standard permutation
"""
if not os.path.isdir(save_dir):
os.makedirs(save_dir)
methods = ["random", "raw", "umap", "scimilarity", "scgpt", "state", "chatgpt5.0", "compbio_direct", "compbio_untreated_direct"]
knockdown_thresholds = [500.0]
meta_AUC = {m: {kd: {} for kd in knockdown_thresholds} for m in methods} ##key: method, key: anchor_state, key KD threshold, value: AUC for this pathway and set of positive_controls
global_max_AUC, global_max_params = 0, ""
best_method, best_method_AUC = "", -1.0 ##the best method and best AUC
method_to_x_y = {} ##key: anchor_state, key: method, value: [x,y]
for method in methods:
for knockdown_threshold in knockdown_thresholds:
sorted_perts, _ = get_sorted_perts(method=method, min_sample=min_sample)