forked from beef-broccoli/deebo
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathchem_analyze.py
2006 lines (1733 loc) · 102 KB
/
chem_analyze.py
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 pandas as pd
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
import matplotlib.patches as mpatches
import gif
import itertools
from utils import scaler
from scipy import stats
la_gold = '#FDB927'
la_purple = '#552583'
# TODO: somehow ETC provides a more accurace estimation of average. Need to investigate.
# TODO: try lower number of simulations
# TODO: try to enforce 2 experiments per arm
# TODO: if predicted yield is low, sample again. could be risky
# arm elimination
def plot_arm_counts(d='',
top_n=5,
title='',
bar_errbar=False,
bar_color='#1f77b4',
plot='bar'):
"""
Plot the average sampling counts for each arm over all simulations
Parameters
----------
d: str
directory path for acquisition, should have an arms.pkl file and a history.csv file
top_n: int
plot top n most sampled arms
title: str
title of the plot
bar_errbar: bool
plot a 1 std error for bar plot
bar_color: str
color for bar plot
plot: str
box plot or bar plot
Returns
-------
None
"""
import os
import pickle
# load files
if not os.path.isfile(f'{d}/arms.pkl'):
exit('arms.pkl does not exist in this directory')
if not os.path.isfile(f'{d}/history.csv'):
exit('history.csv does not exist in this directory')
with open(f'{d}/arms.pkl', 'rb') as f:
arms_dict = pickle.load(f)
df = pd.read_csv(f'{d}/log.csv')
# grab some info from log
num_sims = max(df['num_sims']) + 1 # number of simulations done
max_horizon = max(df['horizon']) + 1 # max time horizon
# calculate average number of selection per simulation for top arms
allcounts = df[['num_sims', 'chosen_arm', 'reward']].groupby(['chosen_arm', 'num_sims']).count()
# for bar plot, calculate average and std
sorted_means = allcounts.groupby('chosen_arm').agg({'reward': ['mean', 'std']}).sort_values(by=('reward', 'mean'),
ascending=False)
average_counts = list(sorted_means.values[:top_n, 0].flatten())
average_counts_errs = list(sorted_means.values[:top_n, 1].flatten())
arms_indexes = sorted_means.index.to_numpy()[:top_n] # corresponding arm index of top n arms
arm_names = ['/'.join(arms_dict[ii]) for ii in arms_indexes] # arm names come as tuple, join all elements in tuple
# for box plot, get all results
x = allcounts.groupby('chosen_arm')['reward'].apply(np.array)
x = x.loc[x.index[[int(a) for a in arms_indexes]]] # use the arms_indexes got from sorted array above, make it int
indexes = x.index.to_list() # substrate pairs names
values = x.to_list() # list of arrays, since dimensions might not match
# calculate baseline (time horizon evenly divided by number of arms)
baseline = max_horizon / len(arms_dict)
# start plotting
plt.rcParams['savefig.dpi'] = 300
fig, ax = plt.subplots()
if plot == 'bar':
if bar_errbar:
ax.barh(arm_names, average_counts, height=0.5, xerr=average_counts_errs, capsize=4, color=bar_color)
else:
ax.barh(arm_names, average_counts, height=0.5, color=bar_color)
plt.axvline(x=baseline, ymin=0, ymax=1, linestyle='dashed', color='black', label='baseline', alpha=0.5)
ax.set_xlabel('average number of times sampled')
ax.set_ylabel('arms')
ax.set_xticks(np.arange(max(average_counts) + max(average_counts_errs)))
elif plot == 'box':
plt.axvline(x=baseline, ymin=0, ymax=1, linestyle='dashed', color='black', label='baseline', alpha=0.5)
ax.boxplot(values,
notch=False,
labels=arm_names,
vert=False,
patch_artist=True,
boxprops=dict(facecolor=la_gold, color=la_gold),
capprops=dict(color=la_purple, linewidth=1.5),
whiskerprops=dict(color=la_purple, linewidth=1.5),
medianprops=dict(color=la_purple, linewidth=1.5),
flierprops=dict(markerfacecolor=la_purple, markeredgecolor=la_purple, marker='.'),
showmeans=True,
meanprops=dict(marker='x', markeredgecolor=la_purple, markerfacecolor=la_purple))
ax.set_xlabel('number of times sampled')
ax.set_ylabel('arms')
ax.set_xticks(np.arange(max([max(v) for v in values]) + 1))
else:
pass
ax.set_title(title)
plt.show()
return None
def plot_arm_rewards(ground_truth_loc,
d='',
top_n=5,
title='',
errbar=False):
"""
Parameters
----------
ground_truth_loc: str
location for ground truth file
d: str
directory path for acquisition, should have an arms.pkl file and a history.csv file
top_n: int
plot top n most sampled arms
title: str
title of the plot
errbar: bool
plot error bar or not
Returns
-------
None
"""
import pickle
import os
if not os.path.isfile(f'{d}/arms.pkl'):
exit('arms.pkl does not exist in this directory')
if not os.path.isfile(f'{d}/history.csv'):
exit('history.csv does not exist in this directory')
with open(f'{d}/arms.pkl', 'rb') as f:
arms_dict = pickle.load(f)
df = pd.read_csv(f'{d}/log.csv')
max_horizon = max(df['horizon']) + 1 # max time horizon
n_arms = len(arms_dict)
# for each arm, average yield across each simulation first
# then calculate the average yield
# the simulations where a particular arm is not sampled is ignored here
gb = df[['num_sims', 'chosen_arm', 'reward']].groupby(['chosen_arm', 'num_sims']).mean().groupby('chosen_arm')
sorted_means = gb.agg({'reward': ['mean', 'std']}).sort_values(by=('reward', 'mean'),
ascending=False) # sorted mean values and pick top n
sim_average_vals = list(sorted_means.values[:top_n, 0].flatten())
sim_average_errs = list(sorted_means.values[:top_n, 1].flatten())
arms_indexes = sorted_means.index.to_numpy()[:top_n] # corresponding arm index of top n arms
arms_names = ['/'.join(arms_dict[ii]) for ii in arms_indexes]
true_averages, etc_averages, etc_errs = calculate_true_and_etc_average(arms_dict,
arms_indexes,
ground_truth=pd.read_csv(ground_truth_loc),
n_sim=100,
n_sample=int(max_horizon // n_arms))
# it's a horizontal bar plot, but use vertical bar terminology here
width = 0.3 # actually is height
xs = np.arange(len(arms_names)) # actually is ys
plt.rcParams['savefig.dpi'] = 300
if errbar:
plt.barh(xs - width / 2, sim_average_vals, color=la_gold, height=width, label='experimental average',
xerr=sim_average_errs, capsize=4)
plt.barh(xs + width / 2, etc_averages, color=la_purple, height=width, label='explore-then-commit baseline',
xerr=etc_errs, capsize=4)
else:
plt.barh(xs - width / 2, sim_average_vals, color=la_gold, height=width, label='experimental average')
plt.barh(xs + width / 2, etc_averages, color=la_purple, height=width, label='explore-then-commit baseline')
plt.yticks(xs, arms_names)
plt.xlabel('yield')
for ii in range(len(true_averages)):
plt.vlines(true_averages[ii], ymin=xs[ii] - width - 0.1, ymax=xs[ii] + width + 0.1, linestyles='dotted',
colors='k')
plt.title(title)
plt.legend(ncol=2, bbox_to_anchor=(0, 1), loc='lower left', fontsize='medium')
plt.tight_layout()
plt.show()
return None
def calculate_true_and_etc_average(arms_dict,
arms_indexes,
ground_truth,
n_sim=-1,
n_sample=-1):
"""
Helper function to calculate true average and explore-then-commit average
Parameters
----------
arms_dict: dict
dictionary from arms.pkl, stores arm indexes and corresponding names
arms_indexes: list like
the indexes for arms of interest
ground_truth: pd.DataFrame
data frame with experimental results
n_sim: int
number of simulations for explore then commit, good to match the actual acquisition n_sim
n_sample: int
number of samples drawn per arm. This is calculated (# of available experiments // # of available arms)
Returns
-------
"""
if ground_truth['yield'].max() > 2:
ground_truth['yield'] = ground_truth['yield'].apply(scaler)
arms = [arms_dict[ii] for ii in arms_indexes] # get all relevant arms names as a list of tuples
inverse_arms_dict = {v: k for k, v in arms_dict.items()} # inverse arms_dict {arm_name: arm_index}
# figure out which columns are involved in arms
example = arms[0]
cols = []
for e in example:
l = ground_truth.columns[(ground_truth == e).any()].to_list()
assert (len(l) == 1)
cols.append(l[0])
ground_truth['to_query'] = list(zip(*[ground_truth[c] for c in cols])) # select these cols and make into tuple
ground_truth = ground_truth[['to_query', 'yield']]
filtered = ground_truth[
ground_truth['to_query'].isin(arms)] # filter, only use arms of interest supplied by indexes
# calculate average and generate a dict of results
means = filtered.groupby(['to_query']).mean()['yield'].to_dict()
true_averages = {}
for arm in arms:
true_averages[inverse_arms_dict[arm]] = means[arm]
true_averages = [true_averages[ii] for ii in arms_indexes] # make into a list based on arms_indexes
# do explore-then-commit
means = np.zeros((n_sim, len(arms)))
for n in range(n_sim):
for ii in range(len(arms)):
df = filtered.loc[filtered['to_query'] == arms[ii]]
y = df['yield'].sample(n_sample)
means[n, ii] = y.mean()
etc_averages = np.average(means, axis=0) # arms is already sorted with arms_indexes, can directly use here
etc_errs = np.std(means, axis=0)
return true_averages, etc_averages, etc_errs
def calculate_etc_accuracy(arms_dict,
explore_limit,
arms_indexes,
n_sim,
ground_truth):
if ground_truth['yield'].max() > 2:
ground_truth['yield'] = ground_truth['yield'].apply(scaler)
arms_of_interest = [arms_dict[ii] for ii in arms_indexes] # get all relevant arms names as a list of tuples
arms_all = list(arms_dict.values()) # all arm names
inverse_arms_dict = {v: k for k, v in arms_dict.items()} # inverse arms_dict {arm_name: arm_index}
# figure out which columns are involved in arms
example = arms_of_interest[0]
cols = []
for e in example:
l = ground_truth.columns[(ground_truth == e).any()].to_list()
assert (len(l) == 1)
cols.append(l[0])
ground_truth['to_query'] = list(zip(*[ground_truth[c] for c in cols])) # select these cols and make into tuple
ground_truth = ground_truth[['to_query', 'yield']]
# do explore then commit
means = np.zeros((n_sim, len(arms_all)))
for e in explore_limit:
for n in range(n_sim):
for ii in range(len(arms_all)):
pass
# TODO: write a generic version of this
return None
def plot_probs_choosing_best_arm(best_arm_indexes,
fn_list,
legend_list,
fp='',
hline=0,
vline=0,
etc_baseline=False,
etc_fp='',
title='',
legend_title='',
long_legend=False,
ignore_first_rounds=0):
"""
The probability of choosing the best arm(s) at each time point across all simulations
Parameters
----------
best_arm_indexes: list like
list of indexes for optimal arms
fn_list: Collection
list of data file names
legend_list: Collection
list of labels for legend
hline: int/float
value for plotting horizontal baseline
vline: int/float
value for plotting a vertical baseline
etc_baseline: bool
display explore-then-commit baseline or not
etc_fp: str
file path for calculated etc baseline at each time point, a numpy array object
fp: str
the deepest common directory for where the data files are stored
title: str
title for the plot
legend_title: str
title for the legend
long_legend: bool
if true, legend will be plotted outside the plot; if false mpl finds the best position within plot
ignore_first_rounds: int
when plotting, ignore the first n rounds. Useful for algos that require running one pass of all arms
Returns
-------
matplotlib.pyplot plt object
"""
assert len(fn_list) == len(legend_list)
fps = [fp + fn for fn in fn_list]
plt.rcParams['savefig.dpi'] = 300
fig, ax = plt.subplots()
if hline != 0:
plt.axhline(y=hline, xmin=0, xmax=1, linestyle='dashed', color='black', label='baseline', alpha=0.5)
if vline != 0:
plt.axvline(x=vline, ymin=0, ymax=1, linestyle='dashed', color='black', label='baseline', alpha=0.5)
if etc_baseline:
base = np.load(etc_fp)
plt.plot(np.arange(len(base))[ignore_first_rounds:], base[ignore_first_rounds:], color='black',
label='explore-then-commit', lw=2)
for i in range(len(fps)):
fp = fps[i]
df = pd.read_csv(fp)
df = df[['num_sims', 'horizon', 'chosen_arm']]
n_simulations = int(np.max(df['num_sims'])) + 1
time_horizon = int(np.max(df['horizon'])) + 1
all_arms = np.zeros((n_simulations, time_horizon))
for ii in range(int(n_simulations)):
all_arms[ii, :] = list(df.loc[df['num_sims'] == ii]['chosen_arm'])
counts = np.count_nonzero(np.isin(all_arms, best_arm_indexes),
axis=0) # average across simulations. shape: (1, time_horizon)
probs = counts / n_simulations
ax.plot(np.arange(time_horizon)[ignore_first_rounds:], probs[ignore_first_rounds:], label=str(legend_list[i]))
ax.set_xlabel('time horizon')
ax.set_ylabel(f'probability of finding best arm: {best_arm_indexes}')
ax.set_title(title)
ax.grid(visible=True, which='both', alpha=0.5)
if long_legend:
ax.legend(title=legend_title, bbox_to_anchor=(1.02, 1), loc="upper left")
plt.tight_layout()
else:
ax.legend(title=legend_title)
plt.show()
def plot_accuracy_best_arm(best_arm_indexes,
fn_list,
legend_list,
fp='',
hlines=None,
vlines=None,
etc_baseline=False,
etc_fp='',
title='',
xlabel='Time horizon (number of experiments)',
ylabel=None,
legend_title='',
long_legend=False,
ignore_first_rounds=0,
shade_first_rounds=0,
max_horizon_plot=0,
calc_random_exploration=False):
"""
Accuracy up to each time point
At each time point, consider all past experiments until this point, and pick the arm with the highest number of samples
Accuracy = (# of times best empirical arm is in best_arm_indexes) / (# of simulations)
Parameters
----------
best_arm_indexes: Collection
list of indexes for the best arms
fn_list: Collection
list of data file names
legend_list: Collection
list of labels used in legend
fp: str
file path of the deepest common dir, used with fn_list
hlines: Collection
list of y axis locations to draw horizontal lines
vlines: Collection
list of x axis locations to draw horizontal lines
etc_baseline: bool
draw the explore-then-commit baseline or not
etc_fp: str
filepath of .npy file that stores numpy array with etc baseline
title: str
title of plot
legend_title: str
title of legend
long_legend: bool
if the legend is long, will move the legend outside of the plot
ignore_first_rounds: int
ignore the first n data points in the plot for all traces
shade_first_rounds: int
plot all data points, but vertically shade until x=n. And mark this area as "exploration"
max_horizon_plot: int
plot until this maximum time horizon
Returns
-------
"""
assert len(fn_list) == len(legend_list)
fps = [fp + fn for fn in fn_list]
plt.rcParams['savefig.dpi'] = 300
fig, ax = plt.subplots()
if hlines is not None:
for hline in hlines:
plt.axhline(y=hline, xmin=0, xmax=1, linestyle='dashed', color='black', label='baseline', alpha=0.5)
if vlines is not None:
for vline in vlines:
plt.axvline(x=vline, ymin=0, ymax=1, linestyle='dashed', color='black', alpha=0.5)
max_time_horizon = 0 # etc baseline cuts off at this time horizon
random_exploration_fp_for_calc = None
for i in range(len(fps)):
fp = fps[i]
if calc_random_exploration and 'random' in fp: # save the fp for random exploration, calcualte baseline and plot
random_exploration_fp_for_calc = fp
df = pd.read_csv(fp)
df = df[['num_sims', 'horizon', 'chosen_arm']]
n_simulations = int(np.max(df['num_sims'])) + 1
time_horizon = int(np.max(df['horizon'])) + 1
if time_horizon > max_time_horizon:
max_time_horizon = time_horizon
best_arms = np.zeros((n_simulations, time_horizon)) # each time point will have a best arm up to that point
for n in range(int(n_simulations)):
data = np.array(list(df.loc[df['num_sims'] == n]['chosen_arm']))
for t in range(len(data)):
u, counts = np.unique(data[:t+1], return_counts=True)
best_arms[n, t] = u[np.random.choice(np.flatnonzero(counts == max(counts)))]
isinfunc = lambda x: x in best_arm_indexes
visinfunc = np.vectorize(isinfunc)
boo = visinfunc(best_arms)
probs = boo.sum(axis=0)/n_simulations
#print(list(probs))
#print('')
if max_horizon_plot == 0:
ax.plot(np.arange(time_horizon)[ignore_first_rounds:], probs[ignore_first_rounds:], label=str(legend_list[i]))
else:
ax.plot(np.arange(time_horizon)[ignore_first_rounds:max_horizon_plot], probs[ignore_first_rounds:max_horizon_plot],
label=str(legend_list[i]))
if max_horizon_plot != 0:
max_time_horizon = max_horizon_plot # adjust max_time_horizon again before plotting ETC
if etc_baseline:
base = np.load(etc_fp)
plt.plot(np.arange(len(base))[ignore_first_rounds:max_time_horizon], base[ignore_first_rounds:max_time_horizon],
color='black',
label='Explore-then-commit',
lw=2,
zorder=-100)
if calc_random_exploration:
df = pd.read_csv(random_exploration_fp_for_calc)
df = df[['num_sims', 'horizon', 'chosen_arm', 'reward']]
n_simulations = int(np.max(df['num_sims'])) + 1
time_horizon = int(np.max(df['horizon'])) + 1
best_arms = np.zeros((n_simulations, time_horizon)) # each time point will have a best arm up to that point
for n in range(int(n_simulations)):
data = df.loc[df['num_sims'] == n][['chosen_arm', 'reward']]
for t in range(len(data)):
mean = data[:t + 1]
mean = mean.groupby(by='chosen_arm').mean() # mean metric for every arm
maxes = list(mean.index[mean['reward'] == mean[
'reward'].values.max()]) # find all maxes. idxmax() selects first instance
best_arms[n, t] = np.random.choice(maxes) # randomly break ties
isinfunc = lambda x: x in best_arm_indexes
visinfunc = np.vectorize(isinfunc)
boo = visinfunc(best_arms)
probs = boo.sum(axis=0) / n_simulations
plt.plot(np.arange(len(probs))[ignore_first_rounds:max_time_horizon], probs[ignore_first_rounds:max_time_horizon],
color='black',
label='Pure exploration',
lw=2,
ls=':',
zorder=-101)
if shade_first_rounds != 0:
ax.axvspan(0, shade_first_rounds, facecolor='lightgray', alpha=0.5, zorder=100)
_, ymax = ax.get_ylim()
ax.text(shade_first_rounds/2, ymax*0.75, 'Exploration', verticalalignment='center',
horizontalalignment='center', zorder=101, rotation=90)
# ##custom text area
# ax.text(37, 0.91, '1% of data', c='black', backgroundcolor='whitesmoke', fontstyle='italic', fontweight='semibold', ha='center', va='center')
# ax.text(70, 0.96, '2% of data', c='black', backgroundcolor='whitesmoke', fontstyle='italic', fontweight='semibold', ha='center', va='center')
# ##
ax.set_xlabel(xlabel)
if not ylabel:
ylabel = f'Accuracy of identifying best arm: {best_arm_indexes}'
ax.set_ylabel(ylabel)
ax.set_title(title)
ax.grid(visible=True, which='both', alpha=0.5)
if long_legend:
ax.legend(title=legend_title, bbox_to_anchor=(1.02, 1), loc="upper left")
plt.tight_layout()
else:
ax.legend(title=legend_title)
plt.show()
return None
def get_accuracy_bandit_algos(fp, best_arm_indexes):
"""
Calculates the accuracy with the simulation log files of a bandit algorithm and the indices of the best arms
Parameters
----------
fp: str
file path of the log.csv file generated from simulation
best_arm_indexes: list of int
a list of integer indices for the best conditions.
These can be identified from the arms.pkl file in the same folder
Returns
-------
"""
df = pd.read_csv(fp)
df = df[['num_sims', 'horizon', 'chosen_arm']]
n_simulations = int(np.max(df['num_sims'])) + 1
time_horizon = int(np.max(df['horizon'])) + 1
best_arms = np.zeros((n_simulations, time_horizon)) # each time point will have a best arm up to that point
for n in range(int(n_simulations)):
data = np.array(list(df.loc[df['num_sims'] == n]['chosen_arm']))
for t in range(len(data)):
u, counts = np.unique(data[:t+1], return_counts=True)
best_arms[n, t] = u[np.random.choice(np.flatnonzero(counts == max(counts)))]
isinfunc = lambda x: x in best_arm_indexes
visinfunc = np.vectorize(isinfunc)
boo = visinfunc(best_arms)
probs = boo.sum(axis=0)/n_simulations
return probs
def get_accuracy_random_exploration(fp, best_arm_indexes, title='',):
"""
Accuracy of random exploration. Rather than looking at the number of samples
(which returns the random pick probability), this accuracy looks at the average reactivity of each arm at each time
Returns
-------
"""
#
#
# plt.rcParams['savefig.dpi'] = 300
# fig, ax = plt.subplots()
df = pd.read_csv(fp)
df = df[['num_sims', 'horizon', 'chosen_arm', 'reward']]
n_simulations = int(np.max(df['num_sims'])) + 1
time_horizon = int(np.max(df['horizon'])) + 1
# if time_horizon > max_time_horizon:
# max_time_horizon = time_horizon
best_arms = np.zeros((n_simulations, time_horizon)) # each time point will have a best arm up to that point
for n in range(int(n_simulations)):
data = df.loc[df['num_sims'] == n][['chosen_arm', 'reward']]
for t in range(len(data)):
mean = data[:t+1]
mean = mean.groupby(by='chosen_arm').mean() # mean metric for every arm
maxes = list(mean.index[mean['reward'] == mean['reward'].values.max()]) # find all maxes. idxmax() selects first instance
best_arms[n, t] = np.random.choice(maxes) # randomly break ties
isinfunc = lambda x: x in best_arm_indexes
visinfunc = np.vectorize(isinfunc)
boo = visinfunc(best_arms)
probs = boo.sum(axis=0)/n_simulations
#print(list(probs))
#print('')
# ax.plot(np.arange(time_horizon), probs, label='random exploration')
#
# ax.set_xlabel('N experiments')
# ax.set_ylabel(f'Accuracy of identifying best arm: {best_arm_indexes}')
# ax.set_title(title)
# ax.grid(visible=True, which='both', alpha=0.5)
# plt.show()
return probs
def plot_accuracy_best_arm_scope_expansion(arm_indexes,
legend_list,
fp,
baseline_arm_indexes,
baseline_fps,
baseline_labels,
baseline_kwargs,
hlines=None,
vlines=None,
etc_baseline=False,
etc_fp='',
title='',
legend_title='',
long_legend=False,
ignore_first_rounds=0,
shade_first_rounds=0,
max_horizon_plot=9999999,
preset_colors=None):
"""
Accuracy up to each time point
At each time point, consider all past experiments until this point, and pick the arm with the highest number of samples
Accuracy = (# of times best empirical arm is in best_arm_indexes) / (# of simulations)
Modified for scope expansion results with arylation data, top-1 accuracy for different ligands from the same data
Parameters
----------
arm_indexes: Collection
list of indexes for all the arms to be plotted
legend_list: Collection
list of labels used in legend, list of ligands in this case
baseline_arm_indexes: Collection
list of indexes for arms to be plotted from baseline file
fp: str
file path of the data file
baseline_fps: list of str
list of file path of the data file where baseline is plotted. In this case it's optimization with the full scope
baseline_labels: list of str
list of labels for baseline
baseline_kwargs: list of dict
list of kwargs for plotting
hlines: Collection
list of y axis locations to draw horizontal lines
vlines: Collection
list of x axis locations to draw horizontal lines
etc_baseline: bool
draw the explore-then-commit baseline or not
etc_fp: str
filepath of .npy file that stores numpy array with etc baseline
title: str
title of plot
legend_title: str
title of legend
long_legend: bool
if the legend is long, will move the legend outside of the plot
ignore_first_rounds: int
ignore the first n data points in the plot for all traces
shade_first_rounds: int
plot all data points, but vertically shade until x=n. And mark this area as "exploration"
max_horizon_plot: int
plot until this maximum time horizon
preset_colors: list-like
a list of colors to be used sequentially when plotting
Returns
-------
"""
assert len(arm_indexes) == len(legend_list)
plt.rcParams['savefig.dpi'] = 300
# transparent settings
plt.rcParams['figure.facecolor'] = (1.0, 1.0, 1.0, 0)
plt.rcParams['axes.facecolor'] = (1.0, 1.0, 1.0, 0)
plt.rcParams['savefig.facecolor'] = (1.0, 1.0, 1.0, 0)
fig, ax = plt.subplots()
if hlines is not None:
for hline in hlines:
plt.axhline(y=hline, xmin=0, xmax=1, linestyle='dashed', color='black', label='baseline', alpha=0.5)
if vlines is not None:
for vline in vlines:
plt.axvline(x=vline, ymin=0, ymax=1, linestyle='dashed', color='black', alpha=0.5)
max_time_horizon = 0 # etc baseline cuts off at this time horizon
# this plots accuracies for multiple arms from the one data log with scope expansion
df = pd.read_csv(fp)
df = df[['num_sims', 'horizon', 'chosen_arm']]
n_simulations = int(np.max(df['num_sims'])) + 1
time_horizon = int(np.max(df['horizon'])) + 1
if time_horizon > max_time_horizon:
max_time_horizon = time_horizon
best_arms = np.zeros((n_simulations, time_horizon)) # each time point will have a best arm up to that point
# calculate best arms for all time points
for n in range(int(n_simulations)):
data = np.array(list(df.loc[df['num_sims'] == n]['chosen_arm']))
for t in range(len(data)):
u, counts = np.unique(data[:t + 1], return_counts=True)
best_arms[n, t] = u[np.random.choice(np.flatnonzero(counts == max(counts)))]
# check pre-set colors for color consistency
if preset_colors is not None:
assert len(preset_colors) == len(arm_indexes)
colors = preset_colors
else:
colors = plt.cm.tab10.colors
# start plotting for each arm
for i in range(len(arm_indexes)):
isinfunc = lambda x: x == arm_indexes[i]
visinfunc = np.vectorize(isinfunc)
boo = visinfunc(best_arms)
probs = boo.sum(axis=0) / n_simulations
ax.plot(np.arange(time_horizon)[ignore_first_rounds:max_horizon_plot],
probs[ignore_first_rounds:max_horizon_plot],
lw=2,
label=str(legend_list[i]),
color=colors[i])
# this plots a baseline, where the same algorithm picks the overall top-1 arm with all the data
for baseline_fp, baseline_arm_index, baseline_label, kwargs in zip(baseline_fps, baseline_arm_indexes, baseline_labels, baseline_kwargs):
df = pd.read_csv(baseline_fp)
df = df[['num_sims', 'horizon', 'chosen_arm']]
n_simulations = int(np.max(df['num_sims'])) + 1
time_horizon = int(np.max(df['horizon'])) + 1
if time_horizon > max_time_horizon:
max_time_horizon = time_horizon
best_arms = np.zeros((n_simulations, time_horizon)) # each time point will have a best arm up to that point
# calculate best arms for all time points
for n in range(int(n_simulations)):
data = np.array(list(df.loc[df['num_sims'] == n]['chosen_arm']))
for t in range(len(data)):
u, counts = np.unique(data[:t + 1], return_counts=True)
best_arms[n, t] = u[np.random.choice(np.flatnonzero(counts == max(counts)))]
isinfunc = lambda x: x in baseline_arm_index
visinfunc = np.vectorize(isinfunc)
boo = visinfunc(best_arms)
probs = boo.sum(axis=0) / n_simulations
ax.plot(np.arange(time_horizon)[ignore_first_rounds:max_horizon_plot],
probs[ignore_first_rounds:max_horizon_plot],
label=baseline_label, **kwargs)
if max_horizon_plot != 0:
max_time_horizon = max_horizon_plot # adjust max_time_horizon again before plotting ETC
if etc_baseline:
base = np.load(etc_fp)
plt.plot(np.arange(len(base))[ignore_first_rounds:max_time_horizon], base[ignore_first_rounds:max_time_horizon],
color='black',
label='Explore-then-commit',
lw=2,
ls='dashed',
zorder=-100)
if shade_first_rounds != 0:
ax.axvspan(0, shade_first_rounds, facecolor='lightgray', alpha=0.5, zorder=100)
_, ymax = ax.get_ylim()
ax.text(shade_first_rounds / 2, ymax * 0.75, 'Exploration', verticalalignment='center',
horizontalalignment='center', zorder=101, rotation=90)
# ##custom text area
# ax.text(37, 0.91, '1% of data', c='black', backgroundcolor='whitesmoke', fontstyle='italic', fontweight='semibold', ha='center', va='center')
# ax.text(70, 0.96, '2% of data', c='black', backgroundcolor='whitesmoke', fontstyle='italic', fontweight='semibold', ha='center', va='center')
# ##
ax.set_xlabel('Time horizon (number of experiments)')
ax.set_ylabel(f'Accuracy of identifying best arm')
#ax.set_title(title)
ax.grid(visible=True, which='both', alpha=0.5)
if long_legend:
#ax.legend(title=legend_title, bbox_to_anchor=(1.02, 1), loc="upper left")
handles, labels = plt.gca().get_legend_handles_labels()
# specify order of items in legend
order = [0,6,1,4,2,3,5,7]
# add legend to plot
ax.legend([handles[idx] for idx in order], [labels[idx] for idx in order],
loc='upper center', bbox_to_anchor=(0.5, 1.2), ncol=4)
#ax.legend(loc='upper center', bbox_to_anchor=(0.5, 1.1), ncol=4)
plt.tight_layout()
else:
ax.legend(title=legend_title)
plt.show()
return None
def plot_cumulative_reward(fn_list,
legend_list,
fp='',
title='Cumulative reward',
legend_title='',
ignore_first_rounds=0,
shade_first_rounds=0,
long_legend=False,
etc_baseline=False,
etc_fp='',
show_std=False):
assert len(fn_list) == len(legend_list)
fps = [fp + fn for fn in fn_list]
plt.rcParams['savefig.dpi'] = 300
fig, ax = plt.subplots()
max_time_horizon=0
for i in range(len(fps)):
fp = fps[i]
df = pd.read_csv(fp)
df = df[['num_sims', 'horizon', 'cumulative_reward']]
def get_rewards(df): # for one simulation, calculate reward (average or cumulative) at each time horizon t
rewards = df['cumulative_reward'].to_numpy()
return rewards
n_simulations = int(np.max(df['num_sims']))+1
time_horizon = int(np.max(df['horizon']))+1
all_rewards = np.zeros((n_simulations, time_horizon))
if time_horizon > max_time_horizon:
max_time_horizon = time_horizon
for ii in range(int(n_simulations)):
rewards = df.loc[df['num_sims'] == ii]['cumulative_reward'].to_numpy()
all_rewards[ii, :] = rewards
avg_reward = np.average(all_rewards, axis=0) # average across simulations. shape: (1, time_horizon)
interval = np.std(all_rewards, axis=0) # standard error
lower_bound = avg_reward - interval
upper_bound = avg_reward + interval
ax.plot(np.arange(time_horizon)[ignore_first_rounds:], avg_reward[ignore_first_rounds:], label=str(legend_list[i]))
if show_std: # makes me dizzy; se too small
ax.fill_between(np.arange(time_horizon)[ignore_first_rounds:], lower_bound, upper_bound, alpha=0.3)
if shade_first_rounds != 0:
ax.axvspan(0, shade_first_rounds, facecolor='lightgray', alpha=0.5, zorder=100)
_, ymax = ax.get_ylim()
ax.text(shade_first_rounds/2, ymax*0.75, 'exploration', verticalalignment='center', horizontalalignment='center', zorder=101)
if etc_baseline:
base = np.load(etc_fp)
plt.plot(np.arange(len(base))[ignore_first_rounds:max_time_horizon], base[ignore_first_rounds:max_time_horizon],
color='black',
label='explore-then-commit',
lw=2,
zorder=-100)
ax.set_xlabel('time horizon')
ax.set_ylabel('cumulative reward')
ax.set_title(title)
ax.grid(visible=True, which='both', alpha=0.5)
if long_legend:
ax.legend(title=legend_title, bbox_to_anchor=(1.02, 1), loc="upper left")
plt.tight_layout()
else:
ax.legend(title=legend_title)
plt.show()
@gif.frame
def plot_acquisition_history_heatmap_arylation_scope(history_fp='./test/history.csv', roun=0, sim=0, binary=False,
cutoff=80):
"""
plot heatmap for acquisition history at specific time point. Decorator is used to make gifs
Parameters
----------
history_fp: str
file path of the history.csv file from acquisition
roun: int
avoid built-in func round(); plot the heatmap until this round
sim: int
which simulation
binary
cutoff
Returns
-------
"""
df = pd.read_csv('https://raw.githubusercontent.com/beef-broccoli/ochem-data/main/deebo/aryl-scope-ligand.csv')
df = df[['ligand_name', 'electrophile_id', 'nucleophile_id', 'yield']]
df['electrophile_id'] = df['electrophile_id'].apply(lambda x: x.lstrip('e')).astype(
'int') # change to 'int' for sorting purposes, so 10 is not immediately after 1
df['nucleophile_id'] = df['nucleophile_id'].apply(lambda x: x.lstrip('n'))
ligands = list(df['ligand_name'].unique())
# heatmap is divided into 4x6 grid based on ligands. Each ligand is represented by a 8x8 block, overall 32x48
df = df.sort_values(by=['ligand_name', 'electrophile_id', 'nucleophile_id'])
ligand_names = list(df['ligand_name'].unique())
nuc_names = list(df['nucleophile_id'].unique())
elec_names = list(df['electrophile_id'].unique())
ground_truth = df[['electrophile_id', 'nucleophile_id', 'ligand_name']].to_numpy()
# from acquisition history, fetch the reactions that was run, find them in ground_truth to get the indexes (to get yield later)
history = pd.read_csv(history_fp)
history = history.loc[(history['round'] <= roun) & (history['num_sims'] == sim)][
['electrophile_id', 'nucleophile_id', 'ligand_name']]
history['electrophile_id'] = history['electrophile_id'].apply(lambda x: x.lstrip('e')).astype('int')
history['nucleophile_id'] = history['nucleophile_id'].apply(lambda x: x.lstrip('n'))
history = history.to_numpy()
# get the indexes for the experiments run, keep the yield, and set the rest of the yields to -1 to signal no rxns run
indexes = []
for row in range(history.shape[0]):
indexes.append(np.argwhere(np.isin(ground_truth, history[row, :]).all(axis=1))[0, 0])
df = df.reset_index()
idx_to_set = df.index.difference(indexes)
df.loc[idx_to_set, 'yield'] = -1
# divide yields by ligand and into 8x8 stacks
l = []
for ligand in ligand_names:
tempdf = df.loc[df['ligand_name'] == ligand]
tempdf = tempdf.drop(['ligand_name'], axis=1)
a = np.array(tempdf.groupby(['electrophile_id'], sort=True)['yield'].apply(list).to_list())
# each row is a electrophile, each column is a nucleophile
l.append(a)
a1 = np.hstack(l[0:6])
a2 = np.hstack(l[6:12])
a3 = np.hstack(l[12:18])