import sciris as sc
od = sc.odict(a=['some', 'strings'], b=[1,2,3])
print(od)#0: 'a': ['some', 'strings']
#1: 'b': [1, 2, 3]
Needing a better way of ordering dictionaries was one of the original inspirations for Sciris back in 2014. In those dark days of Python <=3.6, dictionaries were unordered, which meant that dict.keys() could give you anything. (And you still can’t do dict.keys()[0], much less dict[0]). This tutorial describes Sciris’ ordered dict, the odict, its close cousin the objdict, and its pandas-powered pseudorelative, the dataframe.
odictIn basically every situation except one, an odict can be used like a dict. (Since this is a tutorial, see if you can intuit what that one situation is!) For example, creating an odictworks just like creating a regular dict:
#0: 'a': ['some', 'strings']
#1: 'b': [1, 2, 3]
Okay, it doesn’t exactly look like a dict, but it is one:
Keys: ['a', 'b']
Values: [['some', 'strings'], [1, 2, 3]]
Items: [('a', ['some', 'strings']), ('b', [1, 2, 3])]
Looks pretty much the same as a regular dict, except that od.keys() returns a regular list (so, yes, you can do od.keys()[0]). But, you can do things you can’t do with a regular dict, such as:
Item 0 is called a and has value ['some', 'strings']
Item 1 is called b and has value [1, 2, 3]
We can, as you probably guessed, also retrieve items by index as well:
Remember the question about the situation where you wouldn’t use an odict? The answer is if your dict has integer keys, then although you still could use an odict, it’s probably best to use a regular dict. But even float keys are fine to use (if somewhat strange).
You might’ve noticed that the odict has more verbose output than a regular dict. This is because its primary purpose is as a high-level container for storing large(ish) objects.
For example, let’s say we want to store a number of named simulation results. Look at how we’re able to leverage the odict in the loop that creates the plots
import numpy as np
import matplotlib.pyplot as plt
class Sim:
def __init__(self, n=20, n_factors=6):
self.results = sc.odict()
self.n = n
self.n_factors = n_factors
def run(self):
for i in range(self.n_factors):
label = f'y = N^{i+1}'
result = np.random.randn(self.n)**(i+1)
self.results[label] = result
def plot(self):
with sc.options.context(jupyter=True): # Jupyter-optimized plotting
plt.figure()
rows,cols = sc.getrowscols(len(self.results))
for i,label,result in self.results.enumitems(): # odict magic!
plt.subplot(rows, cols, i+1)
plt.scatter(np.arange(self.n), result, c=result, cmap='parula')
plt.title(label)
sc.figlayout() # Trim whitespace from the figure
sim = Sim()
sim.run()
sim.plot()
We can quickly access these results for exploratory data analysis without having to remember and type the labels explicitly:
Sim results are
#0: 'y = N^1':
array([-1.03307712, -1.60457207, -0.58389521, -0.00353775, -0.67917868,
-1.21801116, -1.31547044, -0.89120027, 0.84096007, 0.35661919,
-0.56793564, -0.00421156, 0.27938184, 1.07673398, 0.53637045,
-0.96364602, -0.88382217, 1.57779423, 0.29095129, -1.23966912])
#1: 'y = N^2':
array([0.05921431, 0.02134717, 0.01040825, 0.02895642, 0.89097971,
0.01619604, 1.40665577, 0.30008869, 0.38568194, 3.22952084,
0.11051021, 0.24268951, 0.1198886 , 2.19255992, 0.3990796 ,
3.22968607, 0.29061348, 0.91600263, 0.17463414, 2.88317047])
#2: 'y = N^3':
array([-2.48870132e+00, -8.61418372e-01, 8.57758346e-03, -4.66234045e-01,
4.08510358e-01, 7.31458299e-03, -5.10526373e+00, 3.10936471e-02,
2.23894490e+00, 3.26617220e+00, -5.03021510e-01, -2.36373506e-01,
6.25989097e-06, 1.06843253e-04, 1.25011171e+00, 5.46831657e-01,
-1.41358741e-03, -3.56868817e-03, -7.26856928e-03, -1.22763076e+00])
#3: 'y = N^4':
array([1.05560576e-02, 3.80224868e-02, 4.77495124e+00, 8.90366452e-03,
1.27579749e-03, 7.87221434e+01, 3.26079855e-03, 6.36389423e+01,
4.70736455e-02, 1.09715516e+00, 2.83749906e+00, 6.60586787e-02,
1.46178513e-02, 1.62954646e-04, 2.70129593e-03, 7.03938338e-01,
3.98595449e-02, 1.65267700e-07, 4.33421951e-02, 6.51232562e+00])
#4: 'y = N^5':
array([-1.53681630e+01, -2.26679315e-02, -2.48745859e-02, -3.84841996e+00,
-6.76217712e-02, -1.51698246e-02, -8.38623264e-05, -3.66316295e-06,
-2.08597675e-03, -1.47524175e-02, -3.44191416e-05, 7.25884924e-02,
-2.75333469e+01, 8.54175117e-01, -1.75825666e+01, -8.38904417e+00,
-7.90165880e+00, 8.59043929e-02, -1.16691773e+00, -1.97511313e-08])
#5: 'y = N^6':
array([2.11969895e+00, 1.36725300e+00, 3.02610895e-06, 2.24078040e-04,
7.42248075e-01, 9.75834535e-02, 9.44270891e-01, 3.44524925e-03,
1.53560971e+02, 8.90920448e-01, 2.61542892e+00, 1.98028812e-02,
1.29582852e+02, 1.60674332e-02, 3.68825336e-01, 1.59171254e-04,
1.43538923e-01, 1.88678071e-04, 1.73717615e+00, 4.73522726e+01])
The first set of results is
[-1.03307712 -1.60457207 -0.58389521 -0.00353775 -0.67917868 -1.21801116
-1.31547044 -0.89120027 0.84096007 0.35661919 -0.56793564 -0.00421156
0.27938184 1.07673398 0.53637045 -0.96364602 -0.88382217 1.57779423
0.29095129 -1.23966912]
The first set of results has median
-0.576 (95% CI: -1.467, 1.340)
This is a have-your-cake-and-eat-it-too situation: the first set of results is correctly labeled (sim.results['y = N^1']), but you can easily access it without having to type all that (sim.results[0]).
objdictWhen you’re just writing throwaway analysis code, it can be a pain to type mydict['key1']['key2'] over and over. (Right-pinky overuse is a real medical issue.) Wouldn’t it be nice if you could just type mydict.key1.key2, but otherwise have everything work exactly like a dict? This is where the objdict comes in: it’s identical to an odict (and hence like a regular dict), except you can use “object syntax” (a.b) instead of “dict syntax” (a['b']). This is especially handy for using f-strings, since you don’t have to worry about nested quotes:
Checking ob[0] = ['some', 'strings']
Checking ob.key1 = ['some', 'strings']
Checking ob["key1"] = ['some', 'strings']
In most cases, you probably want to use objdicts rather than odicts just to have the extra flexibility. Since they’re so common, sc.obj() is a shorter name for sc.objdict().
All this flexibility has a cost, although a small one. If you only need attribute access and not the other odict features (such as indexing by position), sc.dictobj() is a lighter-weight alternative that is nearly as fast as a plain dict. Approximate times per operation, in nanoseconds:
obj['a'] |
obj.a |
obj['a'] = 1 |
obj.a = 1 |
Create | |
|---|---|---|---|---|---|
dict |
10 | n/a | 15 | n/a | 50 |
sc.dictobj() |
20 | 15 | 15 | 20 | 450 |
sc.objdict() |
40 | 50 | 190 | 330 | 600 |
These differences won’t matter unless you’re doing millions of operations. But if you’re reading this, chances are high that you do sometimes need to do millions of dict operations, in which case use sc.dictobj() or a plain dict inside the loop. The other difference is that an sc.objdict() protects its methods (obj.keys = 3 raises an error, and obj['keys'] = 3 leaves obj.keys() working), whereas in an sc.dictobj() a key replaces a method of the same name.
The Sciris sc.dataframe() works exactly like pandas pd.DataFrame(), with a couple extra features, mostly to do with creation, indexing, and manipulation.
Any valid pandas dataframe initialization works exactly the same in Sciris. However, Sciris is a bit more flexible about how you can create the dataframe, again optimized for letting you make them quickly with minimal code. For example:
It’s not a huge difference, but the Sciris one is shorter. Sciris also makes it easier to define types on dataframe creation:
x y z
0 a 1.0 True
1 b 2.0 False
2 c 3.0 True
You can also define data types along with the columns:
x y z
0 a 1.0 True
1 b 2.0 False
2 c 3.0 True
The df.disp() command will do its best to show the full dataframe. By default, Sciris dataframes (just like pandas) are shown in abbreviated form:
0 1 2 3 4 5 6 \
0 0.546975 0.599934 0.124945 0.787096 0.803484 0.775250 0.248447
1 0.930906 0.474520 0.970431 0.775781 0.800958 0.739598 0.975797
2 0.550348 0.393949 0.638982 0.055300 0.236322 0.494199 0.375237
3 0.894867 0.927297 0.016640 0.388333 0.614110 0.468960 0.118777
4 0.967417 0.467436 0.177525 0.000581 0.677436 0.064750 0.029525
.. ... ... ... ... ... ... ...
65 0.647408 0.487705 0.741640 0.370809 0.556047 0.551134 0.643668
66 0.097821 0.867045 0.966138 0.900150 0.003465 0.931670 0.677371
67 0.201979 0.930724 0.337816 0.227465 0.735999 0.613575 0.316369
68 0.201117 0.808333 0.800347 0.834381 0.815295 0.977109 0.368942
69 0.668335 0.534060 0.472425 0.083378 0.980644 0.064007 0.112864
7 8 9
0 0.755329 0.241168 0.120138
1 0.570234 0.985122 0.364300
2 0.020963 0.349574 0.277760
3 0.039472 0.046648 0.108963
4 0.889953 0.967551 0.307972
.. ... ... ...
65 0.519540 0.065582 0.739346
66 0.631445 0.060307 0.340040
67 0.919253 0.770444 0.801552
68 0.604633 0.840894 0.026615
69 0.560426 0.445604 0.186399
[70 rows x 10 columns]
But sometimes you just want to see the whole thing. The official way to do it in pandas is with pd.options_context, but this is a lot of effort if you’re just poking around in a script or terminal (which, if you’re printing a dataframe, you probably are). By default, df.disp() shows the whole damn thing:
0 1 2 3 4 5 6 7 8 9
0 0.5470 0.5999 0.1249 0.7871 0.8035 0.7753 0.2484 0.7553 0.2412 0.1201
1 0.9309 0.4745 0.9704 0.7758 0.8010 0.7396 0.9758 0.5702 0.9851 0.3643
2 0.5503 0.3939 0.6390 0.0553 0.2363 0.4942 0.3752 0.0210 0.3496 0.2778
3 0.8949 0.9273 0.0166 0.3883 0.6141 0.4690 0.1188 0.0395 0.0466 0.1090
4 0.9674 0.4674 0.1775 0.0006 0.6774 0.0648 0.0295 0.8900 0.9676 0.3080
5 0.9414 0.9394 0.6817 0.0663 0.1286 0.0140 0.4993 0.9329 0.3002 0.0466
6 0.8856 0.0138 0.9832 0.0026 0.5587 0.8380 0.0084 0.0003 0.8934 0.2605
7 0.4853 0.7665 0.7845 0.3104 0.9608 0.5595 0.2147 0.5363 0.5242 0.4671
8 0.5127 0.0987 0.7721 0.9686 0.2647 0.5564 0.1425 0.9267 0.1556 0.8831
9 0.5727 0.6045 0.2852 0.1999 0.8289 0.2807 0.2374 0.1674 0.3549 0.3400
10 0.5055 0.2641 0.3044 0.3615 0.0423 0.6195 0.2706 0.4479 0.4350 0.6590
11 0.6642 0.4487 0.4471 0.0105 0.4615 0.3436 0.7379 0.0223 0.6932 0.8919
12 0.9717 0.2594 0.7010 0.7791 0.8958 0.2626 0.6511 0.5750 0.0649 0.3428
13 0.8217 0.5963 0.4929 0.7584 0.1693 0.5686 0.6354 0.5598 0.3436 0.9632
14 0.8010 0.2681 0.3363 0.4396 0.8636 0.9076 0.7600 0.1134 0.2467 0.9031
15 0.5038 0.1032 0.9332 0.3467 0.0944 0.7861 0.5318 0.9916 0.2236 0.6870
16 0.6516 0.3437 0.7978 0.6913 0.0254 0.0988 0.5066 0.7062 0.6452 0.6732
17 0.5247 0.3239 0.9081 0.6861 0.6985 0.6643 0.0442 0.7809 0.8518 0.0377
18 0.2232 0.2115 0.4159 0.4412 0.8562 0.2038 0.4049 0.4004 0.7300 0.0986
19 0.7444 0.4884 0.0234 0.5067 0.7438 0.4777 0.1747 0.5962 0.6062 0.4549
20 0.5285 0.8796 0.7195 0.3744 0.8896 0.8043 0.0256 0.1563 0.9292 0.2046
21 0.5643 0.0939 0.9540 0.6480 0.9305 0.8541 0.1436 0.4246 0.4418 0.6631
22 0.7271 0.0177 0.1436 0.9602 0.9341 0.4077 0.4528 0.2612 0.7087 0.5054
23 0.6310 0.9646 0.2370 0.9513 0.7533 0.4517 0.6048 0.3577 0.7501 0.3153
24 0.9684 0.8956 0.5295 0.0410 0.1286 0.9970 0.9813 0.8382 0.8668 0.7225
25 0.2926 0.3193 0.1149 0.2756 0.0911 0.6218 0.5507 0.1591 0.5136 0.9613
26 0.2475 0.0384 0.6511 0.4773 0.9968 0.5019 0.1804 0.6035 0.6087 0.5128
27 0.8349 0.2980 0.7567 0.3052 0.8001 0.4942 0.7960 0.1665 0.0589 0.8393
28 0.0078 0.8942 0.1284 0.2914 0.6419 0.1821 0.1053 0.7913 0.0184 0.3883
29 0.9401 0.5452 0.8259 0.7470 0.1457 0.2134 0.4680 0.0963 0.9348 0.4694
30 0.8117 0.5840 0.8608 0.6794 0.9059 0.7143 0.6711 0.1336 0.1432 0.8674
31 0.2252 0.9160 0.1623 0.7630 0.4081 0.4100 0.8819 0.4933 0.6777 0.6967
32 0.2519 0.2878 0.4524 0.8787 0.9887 0.4226 0.9040 0.4773 0.4323 0.2832
33 0.7221 0.6477 0.1158 0.1102 0.4551 0.1308 0.8532 0.9440 0.9829 0.2421
34 0.6374 0.1089 0.7256 0.1008 0.3167 0.0290 0.2907 0.2665 0.1566 0.1152
35 0.9517 0.1922 0.8987 0.4300 0.3447 0.0493 0.1581 0.2274 0.1936 0.9663
36 0.8855 0.5372 0.1995 0.6616 0.1329 0.2864 0.5118 0.3227 0.8946 0.4418
37 0.0740 0.0434 0.6890 0.1247 0.0740 0.1365 0.2586 0.9172 0.5320 0.8089
38 0.9205 0.8231 0.7294 0.1231 0.9631 0.2539 0.1799 0.3868 0.1611 0.3505
39 0.3285 0.1637 0.0840 0.3700 0.1190 0.2646 0.5569 0.9606 0.0018 0.5460
40 0.9284 0.8922 0.2054 0.8922 0.4326 0.6983 0.1179 0.5942 0.6147 0.6341
41 0.2232 0.7589 0.4517 0.6980 0.0840 0.2385 0.7783 0.7114 0.9610 0.5860
42 0.7725 0.3835 0.0299 0.2310 0.3844 0.8691 0.3045 0.8949 0.4213 0.5862
43 0.9364 0.2256 0.3139 0.8954 0.7730 0.0107 0.8743 0.3778 0.3026 0.1879
44 0.6362 0.1776 0.0340 0.8647 0.5443 0.1153 0.0712 0.0025 0.4772 0.7650
45 0.4491 0.4491 0.2301 0.8249 0.3014 0.2487 0.2642 0.7292 0.8947 0.6228
46 0.5144 0.2977 0.3896 0.4436 0.7729 0.3597 0.4411 0.6381 0.3165 0.7723
47 0.7065 0.6502 0.2917 0.1207 0.7990 0.2573 0.3797 0.3608 0.7955 0.5225
48 0.2227 0.2055 0.7706 0.3132 0.0760 0.6554 0.7738 0.0721 0.5918 0.9171
49 0.0807 0.6349 0.2823 0.6356 0.5772 0.2767 0.7551 0.5766 0.6922 0.8835
50 0.6731 0.0548 0.4252 0.1591 0.1619 0.8665 0.9925 0.0861 0.1296 0.9883
51 0.4010 0.6597 0.3948 0.2253 0.5895 0.2682 0.2923 0.9923 0.9214 0.3569
52 0.0899 0.6078 0.1787 0.8264 0.4445 0.5982 0.3398 0.8279 0.8262 0.7779
53 0.5510 0.5909 0.4970 0.6404 0.0278 0.6906 0.5544 0.6859 0.7401 0.1744
54 0.0541 0.8350 0.9904 0.0649 0.1475 0.3299 0.5338 0.1419 0.4823 0.2530
55 0.8730 0.7038 0.6982 0.9964 0.7803 0.3327 0.6638 0.9843 0.8097 0.1493
56 0.3679 0.8530 0.4840 0.1250 0.2582 0.9996 0.8126 0.6619 0.9508 0.7906
57 0.3739 0.5733 0.1579 0.6775 0.0279 0.9461 0.2943 0.5211 0.4222 0.7700
58 0.7059 0.5759 0.4322 0.0039 0.2548 0.1038 0.7773 0.7261 0.1462 0.2579
59 0.1786 0.0072 0.3998 0.8342 0.8039 0.6367 0.7423 0.2242 0.9959 0.4339
60 0.6027 0.1328 0.4104 0.0822 0.3911 0.1874 0.3936 0.1887 0.7866 0.8274
61 0.3632 0.2421 0.9134 0.1650 0.6138 0.1678 0.9131 0.4081 0.5154 0.9519
62 0.3851 0.4863 0.8925 0.8070 0.2043 0.8483 0.6680 0.2834 0.1315 0.5122
63 0.0084 0.2937 0.6384 0.0558 0.6927 0.4917 0.7573 0.4260 0.3463 0.2902
64 0.5348 0.2220 0.0465 0.0398 0.8580 0.2496 0.4905 0.1489 0.3111 0.9496
65 0.6474 0.4877 0.7416 0.3708 0.5560 0.5511 0.6437 0.5195 0.0656 0.7393
66 0.0978 0.8670 0.9661 0.9001 0.0035 0.9317 0.6774 0.6314 0.0603 0.3400
67 0.2020 0.9307 0.3378 0.2275 0.7360 0.6136 0.3164 0.9193 0.7704 0.8016
68 0.2011 0.8083 0.8003 0.8344 0.8153 0.9771 0.3689 0.6046 0.8409 0.0266
69 0.6683 0.5341 0.4724 0.0834 0.9806 0.0640 0.1129 0.5604 0.4456 0.1864
You can also pass other options if you want to customize it further:
0 1 ... 8 9
0 5.5e-01 0.6 ... 2.4e-01 1.2e-01
1 9.3e-01 0.5 ... 9.9e-01 3.6e-01
2 5.5e-01 0.4 ... 3.5e-01 2.8e-01
3 8.9e-01 0.9 ... 4.7e-02 1.1e-01
4 9.7e-01 0.5 ... 9.7e-01 3.1e-01
.. ... ... ... ... ...
65 6.5e-01 0.5 ... 6.6e-02 7.4e-01
66 9.8e-02 0.9 ... 6.0e-02 3.4e-01
67 2.0e-01 0.9 ... 7.7e-01 8.0e-01
68 2.0e-01 0.8 ... 8.4e-01 2.7e-02
69 6.7e-01 0.5 ... 4.5e-01 1.9e-01
[70 rows x 10 columns]
All the regular pandas methods (df['mycol'], df.mycol, df.loc, df.iloc, etc.) work exactly the same. But Sciris gives additional options for indexing. Specifically, getitem commands (what happens under the hood when you call df[thing]) will first try the standard pandas getitem, but then fall back to iloc if that fails. For example:
——————————————————————— Regular pandas indexing ——————————————————————— 23 ————————————————————————— Pandas-like iloc indexing ————————————————————————— x 2 values 23 valid 0 Name: 1, dtype: int64 ——————————————————————— Automatic iloc indexing ——————————————————————— x 2 values 23 valid 0 Name: 1, dtype: int64
One quirk of pandas dataframes is that almost every operation creates a copy rather than modifies the original dataframe in-place (leading to the infamous SettingWithCopyWarning.) This is extremely helpful, and yet, sometimes you do want to modify a dataframe in place. For example, to append a row:
x y z
0 a 1 1
1 b 2 0
2 c 3 1
3 d 4 0
That was easy! For reference, here’s the pandas equivalent (since append was deprecated):
That’s rather a pain to type, and if you mess up (e.g. type newrow instead of [newrow]), in some cases it won’t even fail, just give you the wrong result! Crikey.
Just like how sc.cat() will take anything vaguely arrayish and turn it into an actual array, sc.dataframe.cat() will do the same thing: