Files and versioning#

Unless you’re a string theorist, at some point you’re probably going to want to save and load some data. This tutorial covers some of Sciris’ tools for doing that more easily.

Warning! The tools here are powerful, which also makes them dangerous. Unless it’s in a simple text format like JSON or CSV, loading a data file can run arbitrary code on your computer, just like running a Python script can. If you wouldn’t run a Python file from a particular source, don’t open a data file from that source either.

Click here to open an interactive version of this notebook.

Files#

Saving and loading literally anything#

Let’s assume you’re mostly just saving and loading files you’ve created yourself or from trusted colleagues, not opening email attachments from the branch of the local mafia. Then everything here is absolutely fine.

Let’s revisit our sim from the first tutorial:

[1]:
import sciris as sc
import numpy as np
import matplotlib.pyplot as plt
sc.options(jupyter=True) # To make plots nicer

class Sim:

    def __init__(self, days, trials):
        self.days = days
        self.trials = trials

    def run(self):
        self.x = np.arange(self.days)
        self.y = np.cumsum(np.random.randn(self.days, self.trials)**3, axis=0)

    def plot(self):
        with plt.style.context('sciris.fancy'):
            plt.plot(self.x, self.y, alpha=0.6)

Now let’s run it, save it, reload it, and keep working with the reloaded version:

[2]:
# Run and save
sim = Sim(days=30, trials=5)
sim.run()
sc.save('my-sim.obj', sim) # Save any Python object to disk

# Load and plot
new_sim = sc.load('my-sim.obj') # Load any Python object
new_sim.plot()
../_images/tutorials_tut_files_6_0.png

We can create any object, save it, then reload it from disk and it works just like new – even calling methods works! What’s happening here? Under the hood, sc.save() saves the object as a gzipped (compressed) pickle (byte stream). Pickles are how Python sends objects internally, so can handle almost anything. (For the few corner cases that pickle can’t handle, sc.save() falls back on dill, which really can handle everything.)

There are also other compression options than gzip (zstandard or no compression), but you probably don’t need to worry about these. (If you really care about performance, then sc.zsave(), which uses zstandard by default, is slightly faster than sc.save() – but regardless of how a file was saved you can load it with sc.load().

Saving and loading JSON#

While sc.save() and sc.load() are great for many things, they aren’t great for just sharing data. First, they’re not compatible with anything other than Sciris, so if you try to share one of those files with, say, an R user, they won’t be able to open them.

If you just have data and don’t need to save custom objects, you should save just the data. If you want to save to CSV or Excel (i.e., data that looks like a spreadsheet), you should convert it to a dataframe (df = sc.dataframe(data)), then save it from there (df.to_excel() and df.to_csv(), respectively).

But if you want to save data that’s a little more complex, you should consider JSON: it’s fast, it’s easy for humans to read, and absolutely everything loads it. While typically a JSON maps onto a Python dict, Sciris will take pretty much any object and save out the JSONifiable parts of it:

[3]:
# Try saving our sim as a JSON
sc.savejson('my-sim.json', sim)

# Load it as a JSON
sim_json = sc.loadjson('my-sim.json')
print(sim_json)
{'python_class': "<class '__main__.Sim'>", 'days': 30, 'trials': 5, 'x': [0, 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], 'y': [[0.007499835564661518, -0.13614438865221293, -0.04524981178384604, -1.4744243334849403, 0.25751476910490734], [-0.050186178212552605, -1.6897344043159603, 0.7167961158020387, -1.6566842533202035, 0.35855749306603196], [-0.050081111368470696, -2.2880108655111164, 0.740180720024924, 11.728794212894886, 2.1553938707083127], [0.5179704354986674, -2.4474791632515855, 0.7385524261444278, 16.053096263833826, 2.6835743744221254], [3.205929785195052, -2.458682947873807, 0.8824753452217425, 16.05275416929423, 2.683633806117072], [2.6578233882823863, -2.514911297654467, -1.3334052979555002, 16.746179508217942, 2.7448614017853328], [-0.1700534286374915, 4.653702913232798, -1.3138973854191693, 16.091035048226665, 2.744845062194394], [1.3248409278560547, 4.6540798639398, -0.9527368699642579, 16.08282885278255, 14.162903898440812], [1.468669274730385, 4.84550416368494, -0.9353101034195627, 17.083987106800464, 14.537160721916639], [1.4481767192214319, 4.868089594271198, -3.8811942463274756, 15.439519853510891, 14.3064611430694], [3.663430822575863, 4.705174180459313, -3.8632840567838196, 12.248910854101284, 14.287809690798884], [6.770640549898255, 4.758530021202871, -4.224614651807238, 19.36314587242045, 15.710599769052884], [5.476783493087751, -17.11668608705589, -3.222191741336662, 19.537574999433854, 14.366748281832834], [4.370043192788334, -17.11678510174729, -3.236872569187782, 20.498340774416324, 14.237552920304914], [4.371108452032073, -15.604642164961716, 1.9523686801436275, 55.11052989744039, 14.189517053195203], [4.0369375126409555, -0.6725795648253694, 1.9526854075963482, 53.21441168075755, 12.733424745625005], [11.478509990693048, 2.028420956970978, 3.8708614709739315, 56.78096515798997, 12.74052381044759], [19.54175507227643, 2.0177992998267795, 4.267223194258698, 57.321171719433785, 12.34288549902283], [19.838047520111395, 2.1006934716272583, 4.9477480120709485, 56.37772701172317, 12.669910060462641], [19.826660423791115, 3.722393360638941, 5.102139365880833, 56.36128615665871, 13.03144907710027], [19.498459076179664, 2.4449647054646535, 5.007297892411287, 56.319014975962425, 12.938715301662228], [19.389834070362536, -0.20808281261635342, 5.179213458186106, 48.79116671375369, 14.052138543233676], [19.37947280270852, -0.20808295394385834, 6.287647586066182, 46.44190328588742, 6.896743320531282], [19.381066678615714, -0.7703193393576971, 8.219583359001547, 46.05116435869026, 6.896486470066844], [13.096698625808678, 1.144059286761507, 8.219577590526574, 46.06803283143468, 6.305999510971211], [10.92989414823364, 1.3752751202325741, 10.089731092899346, 45.904864991875606, 6.299323885281925], [10.828305469020876, 1.3810544168703096, 8.383144532692327, 45.90195785376271, 12.071510231691152], [10.828290873908085, 1.385064961406759, 5.693902386138083, 45.622844600585395, 12.917560291140095], [11.310502982877454, 1.4182438640182962, 5.489641633049723, 40.79295592206035, 13.066540021410237], [11.30358899554259, 1.035358486109907, 5.838548342345549, 41.14661840886418, 13.065295616254701]]}

It’s not exactly beautiful, and it’s not as powerful as sc.save() (for example, sim_json.plot() doesn’t exist), but it has all the data, exactly as it was laid out in the original object:

[4]:
print(f"{sim_json['x'] = }")
print(f"{sim_json['y'][0] = }")
sim_json['x'] = [0, 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]
sim_json['y'][0] = [0.007499835564661518, -0.13614438865221293, -0.04524981178384604, -1.4744243334849403, 0.25751476910490734]

(Note that when exported to JSON and loaded back again, everything is in default Python types – so the data is now a list of lists rather than a 2D NumPy array.)

Saving and loading YAML#

If you’re not super familiar with YAML, you might think of it as that quirky format for configuration files with lots of colons and indents. It is that, but it’s also a powerful extension to JSON – every JSON file is also a valid YAML file, but the reverse is not true (i.e., JSON is a subset of YAML). Of most interest to you, dear scientist, is that you can add comments to YAML files. Consider this (relatively) common situation:

[5]:
raw_json = '''
{"variables": {
    "timepoints": [0,1,2,3,4,5],
    "really_important_variable": 12.566370614359172
  }
}
'''
data = sc.readjson(raw_json)
print(data)
{'variables': {'timepoints': [0, 1, 2, 3, 4, 5], 'really_important_variable': 12.566370614359172}}

Now you’re tearing your hair out. Where did 12.566370614359172 come from? It looks vaguely familiar, or at least it did when you wrote it 6 months ago. But with YAML, you can have your data and comment it too:

[6]:
raw_yaml = '''
{"variables": {
    "timepoints": [0,1,2,3,4,5],
    "really_important_variable": 12.566370614359172 # This is just 4π lol
  }
}
'''
data = sc.readyaml(raw_yaml)
print(data)
{'variables': {'timepoints': [0, 1, 2, 3, 4, 5], 'really_important_variable': 12.566370614359172}}

Mystery solved.

Other file functions#

Sciris includes a number of other file utilities. For example, to get a list of files, you can use sc.getfilelist():

[7]:
sc.getfilelist('*.ipynb')
[7]:
['tut_advanced.ipynb',
 'tut_arrays.ipynb',
 'tut_dates.ipynb',
 'tut_dicts.ipynb',
 'tut_files.ipynb',
 'tut_intro.ipynb',
 'tut_parallel.ipynb',
 'tut_plotting.ipynb',
 'tut_printing.ipynb',
 'tut_utils.ipynb']

Sometimes it’s useful to get the folder for the current file, since sometimes you’re calling it from a different place, and want the relative paths to remain the same (for example, to load something from a subfolder):

[8]:
sc.thispath()
[8]:
PosixPath('/home/docs/checkouts/readthedocs.org/user_builds/sciris/checkouts/latest/docs/tutorials')

(This looks wonky here because this notebook is run on some random cloud server, but it should look more normal if you do it at home!)

Most Sciris file functions can return either strings or Paths. If you’ve never used pathlib, it’s a really powerful way of handling paths. It’s also quite intuitive. For example, to create a data subfolder that’s always relative to this notebook regardless of where it’s run from, you can do

[9]:
datafolder = sc.thispath() / 'data'
print(datafolder)
/home/docs/checkouts/readthedocs.org/user_builds/sciris/checkouts/latest/docs/tutorials/data

Sciris also makes it easy to ensure that a path exists:

[10]:
datafile = sc.makefilepath(datafolder / 'my-data.csv', makedirs=True)
print(datafile)
/home/docs/checkouts/readthedocs.org/user_builds/sciris/checkouts/latest/docs/tutorials/data/my-data.csv

Sciris usually handles all this internally, but this can be useful for using with non-Sciris functions, e.g.

[11]:
np.savetxt('data/my-data.csv', np.random.rand(2,2)) # Would give an error without sc.makefilepath() above

Lastly, you can clean up with yourself with sc.rmpath(), which will automatically figure out whether to use os.remove() (which works for files but not folders) or shutil.rmtree() (which, frustratingly, works for folders but not files):

[12]:
sc.rmpath('data/my-data.csv')
Removed "data/my-data.csv"

Versioning#

Getting version information#

You’ve probably heard people talk about reproducibility. Quite likely you yourself have talked about reproducibility. Central to computational reproducibility is knowing what version everything is. Sciris provides several tools for this. To collect all the metadata available – including the current Python environment, system version, and so on – use sc.metadata():

[13]:
md = sc.metadata(pipfreeze=False)
print(md)
#0. 'version':      None
#1. 'timestamp':    '2024-Sep-24 22:21:56'
#2. 'user':         'docs'
#3. 'system':
    #0. 'platform':   'Linux-5.19.0-1028-aws-x86_64-with-glibc2.31'
    #1. 'executable': '/home/docs/checkouts/readthedocs.org/user_builds/sciris/e
    nvs/latest/bin/python'
    #2. 'version':    '3.11.9 (main, Jun 18 2024, 10:17:12) [GCC 9.4.0]'
#4. 'versions':
    #0. 'python':     '3.11.9'
    #1. 'sciris':     '3.2.0'
    #2. 'numpy':      '2.1.1'
    #3. 'pandas':     '2.2.3'
    #4. 'matplotlib': '3.9.2'
#5. 'calling_info':
    #0. 'filename': '/home/docs/checkouts/readthedocs.org/user_builds/sciris/env
    s/latest/lib/python3.11/site-packages/IPython/core/interactiveshell.py'
    #1. 'lineno':   3577
#6. 'git_info':
    #0. 'branch': 'Branch N/A'
    #1. 'hash':   'Hash N/A'
    #2. 'date':   'Date N/A'
#7. 'pipfreeze':    None
#8. 'require':      None
#9. 'comments':     None

(We turned off pipfreeze above because this stores the entire output of pip freeze, i.e. every version of every Python library installed. This is a lot to display in a notebook, but typically you’d leave it enabled.)

If you want specific versions of things, there are two functions for that: sc.compareversions(). This does explicit version checks:

[14]:
if sc.compareversions(np, '>1.0'):
    print('You do not have an ancient version of NumPy')
else:
    print('When you last updated NumPy, dinosaurs roamed the earth')
You do not have an ancient version of NumPy

In contrast, sc.require() will raise a warning (or exception) if the requirement isn’t met. For example:

[15]:
sc.require('numpy>99.9.9', die=False) # We don't want to die, we're in the middle of a tutorial!
/tmp/ipykernel_1378/3631962975.py:1: UserWarning:
The following requirement(s) were not met:
• numpy>99.9.9 (error: No package metadata was found for numpy>99.9.9 (available: 2.1.1))
Try "pip install numpy>99.9.9".
  sc.require('numpy>99.9.9', die=False) # We don't want to die, we're in the middle of a tutorial!
[15]:
False

You can see it raises a warning (there is no NumPy v99.9.9), and attempts to give a helpful suggestion (which in this case is not very helpful).

Saving and loading version information#

Metadata-enhanced figures#

Sciris includes a copy of plt.savefig() named sc.savefig(). Aside from saving with publication-quality resolution by default, the other difference is that it automatically saves metadata along with the figure (including optional comments, if we want). For example:

[16]:
plt.pcolor(sc.smooth(np.random.rand(10,10)), cmap='turbo')
sc.savefig('my-fig.png', comments='This is a pretty plot')
[16]:
'my-fig.png'
../_images/tutorials_tut_files_39_1.png

We can load metadata from the saved file using sc.loadmetadata():

[17]:
md = sc.loadmetadata('my-fig.png')
sc.printjson(md) # Can just use print(), but sc.printjson() is prettier
{
  "version": null,
  "timestamp": "2024-Sep-24 22:21:57",
  "user": "docs",
  "system": {
    "platform": "Linux-5.19.0-1028-aws-x86_64-with-glibc2.31",
    "executable": "/home/docs/checkouts/readthedocs.org/user_builds/sciris/envs/latest/bin/python",
    "version": "3.11.9 (main, Jun 18 2024, 10:17:12) [GCC 9.4.0]"
  },
  "versions": {
    "python": "3.11.9",
    "sciris": "3.2.0",
    "numpy": "2.1.1",
    "pandas": "2.2.3",
    "matplotlib": "3.9.2"
  },
  "calling_info": {
    "filename": "/home/docs/checkouts/readthedocs.org/user_builds/sciris/envs/latest/lib/python3.11/site-packages/IPython/core/interactiveshell.py",
    "lineno": 3577
  },
  "git_info": {
    "branch": "Branch N/A",
    "hash": "Hash N/A",
    "date": "Date N/A"
  },
  "pipfreeze": null,
  "require": null,
  "comments": "This is a pretty plot"
}

Metadata-enhanced files#

Remember sc.save() and sc.load() from the previous tutorial? The metadata-enhanced versions of these are sc.savearchive() and sc.loadarchive(). These will save an arbitrary object to a zip file, but also include a file called sciris_metadata.json along with it. You can even include other files or even whole folders in with it too – for example, if you want to save a big set of sim results and figure you might as well throw in the whole source code along with it. For example, re-using our sim from before, let’s save it along with this notebook:

[18]:
sim_archive = sc.savearchive('my-sim.zip', sim, files='tut_files.ipynb', comments='Sim plus notebook')
Zip file saved to "/home/docs/checkouts/readthedocs.org/user_builds/sciris/checkouts/latest/docs/tutorials/my-sim.zip"

This is just an ordinary zip file, so we can open it with any application. But we can also load the metadata automatically with sc.loadmetadata():

[19]:
md = sc.loadmetadata(sim_archive)
print(md['comments'])
Sim plus notebook

And, of course, we can load the whole thing as a brand new, fully-functional object:

[20]:
sim = sc.loadarchive(sim_archive)
sim.plot()
../_images/tutorials_tut_files_47_0.png