{"id":17,"date":"2019-04-01T18:09:00","date_gmt":"2019-04-01T18:09:00","guid":{"rendered":""},"modified":"2023-08-26T13:00:31","modified_gmt":"2023-08-26T13:00:31","slug":"how-to-separate-your-data-from-your-code","status":"publish","type":"post","link":"https:\/\/sandrock.co.za\/carl\/2019\/04\/how-to-separate-your-data-from-your-code\/","title":{"rendered":"How to separate your data from your code"},"content":{"rendered":"<h2>\nThe wrong way<\/h2>\n<p>You&#8217;ve started writing some code which reads from data files, and does some expensive computation which is written to some different data files. Perhaps you&#8217;re doing this in a Jupyter notebook which contains the following cell:<\/p>\n<div>\n<pre><code><span>import<\/span> pandas\n\ninput_data <span>=<\/span> pandas.read_csv('input.csv')\nresults_data <span>=<\/span> expensive_operation(input_data)\nresults_data.to_csv('results.csv')<\/code><\/pre>\n<pre><code>\n<\/code><\/pre>\n<\/div>\n<p>Now, you want to put your code on GitHub, so you add <code>Calculation.ipynb<\/code>, <code>input.csv<\/code> and <code>results.csv<\/code> to the repository.<\/p>\n<p>The advantage of this method is that everything is in one place and so is easy to share with other people. If we weren&#8217;t using version control, this would be a reasonable method. However, there are some problems with this method when using version control to collaborate:<\/p>\n<ul>\n<li>If the <code>expensive_operation<\/code> doesn&#8217;t produce <em>exactly<\/em> the same output every time, <code>results.csv<\/code> will change with every run of your code. This will clog up the change log with lots of meaningless changes.<\/li>\n<li>If the <code>expensive_operation<\/code> produces large output files, your repository will also fill up with a large number of changes (remember tools like Git store all the versions of files)<\/li>\n<li>If you use binary formats for these files, the version control tracking is less useful.<\/li>\n<\/ul>\n<p>For these reasons I believe both input data and output data should be kept separately from the code repository. I propose using a file syncing service like Dropbox, Google Drive, Box, etc to handle the task of managing and storing shared data.<br \/>\n\u201cBut\u201d, I can hear you ask, \u201cwhy not have the code on one of these platforms as well?\u201d. Well, because version control handles source code much better and uses delayed sync so that you can tell the system when your code is ready to be shared, while data files are usually edited with external programs and uploaded all at once.<\/p>\n<h2>\nEvery solution brings a new problem<\/h2>\n<h3>\nDifferent people have different paths<\/h3>\n<p>So let&#8217;s say you&#8217;ve understood this, so you move those files into your Dropbox under a folder called \u201cData files\u201d, which you can share with your colleagues. Now you need to rewrite your program to read from your Dropbox folder. Your first problem is finding the right filenames to put in to the code, which involves finding the path for the files. For Windows users the path to your folder may be <code>C:UsersPeterDropboxData files<\/code>. The <code>C:UsersPeter<\/code> part is called your \u201chome directory\u201d or \u201chome folder\u201d which we&#8217;ll get back to later. For Mac users this is <code>\/Users\/Peter\/Dropbox\/Data files<\/code> and for Linux users it&#8217;s often <code>\/home\/Peter\/Dropbox\/Data files<\/code>. Let&#8217;s assume you&#8217;re using windows, so you write the code as follows:<\/p>\n<div>\n<pre><code><span>import<\/span> pandas\n\ninput_data <span>=<\/span> pandas.read_csv(r'C:<span><\/span>Users<span><\/span>Peter<span><\/span>Dropbox<span><\/span>Data files<span><\/span><span>input<\/span>.csv')\nresults_data <span>=<\/span> expensive_operation(input_data)\nresults_data.to_csv(r'C:<span><\/span>Users<span><\/span>Peter<span><\/span>Dropbox<span><\/span>Data files<span><\/span>results.csv')<\/code><\/pre>\n<\/div>\n<p>\nThe <code>r<\/code> in front of those strings stands for \u201craw\u201d and stops Python from interpreting the backslashes as escapes.<br \/>\nThe problem here is that every user has a different username, so we can no longer just download this code and run it. Furthermore, you will notice that the different operating systems have different conventions for which character separates the parts of paths (Windows uses <code><\/code>, Unix-like OSs use <code>\/<\/code>).<\/p>\n<h3>\nPathlib to the rescue<\/h3>\n<p>Both of these problems (different user directories and different separators) can be solved with the <code>pathlib<\/code> library:<\/p>\n<div>\n<pre><code><span>import<\/span> pathlib\n<span>import<\/span> pandas\n\n<span># find this user's Dropbox folder<\/span>\ndatadir <span>=<\/span> pathlib.Path('<span>~\/<\/span>Dropbox').expanduser()\n\ninput_data <span>=<\/span> pandas.read_csv(datadir <span>\/<\/span> 'input.csv')\nresults_data <span>=<\/span> expensive_operation(input_data)\nresults_data.to_csv(datadir <span>\/<\/span> 'results.csv')<\/code><\/pre>\n<\/div>\n<p>\nPathlib allows us to expand the <code>~<\/code> character into the user directory correctly on every platform. It also allows us to use the division operator to join file parts correctly. The above code will run correctly on every major operating system and work for the default settings of Dropbox. Also notice how we didn&#8217;t repeat the same folder twice in the code.<\/p>\n<h2>\nOne size fits all?<\/h2>\n<p>But of course, not everyone accepts the default settings. Also, what if you don&#8217;t use Dropbox, but some other service which syncs folders? How can we accommodate all the differences between people&#8217;s setups?<br \/>\nThe answer is local configuration. This requires a couple of things to be in place, but is actually quite easy. I&#8217;ll start with the improved code first:<\/p>\n<div>\n<pre><code><span>import<\/span> configparser\n<span>import<\/span> pathlib\n<span>import<\/span> pandas\n\nconfig <span>=<\/span> configparser.ConfigParser()\nconfig.read('config.ini')\n\ndatadir <span>=<\/span> pathlib.Path(config['paths']['datadir']).expanduser()\n\ninput_data <span>=<\/span> pandas.read_csv(datadir <span>\/<\/span> 'input.csv')\nresults_data <span>=<\/span> expensive_operation(input_data)\nresults_data.to_csv(datadir <span>\/<\/span> 'results.csv')<\/code><\/pre>\n<\/div>\n<p>\nFor this code to run, there needs to be a file called <code>config.ini<\/code> in the current directory when it runs. This file looks like follows:<\/p>\n<div>\n<pre><code><span>\n<\/span><\/code><\/pre>\n<pre><code><span>[paths]<\/span>\n<span>datadir<\/span><span>=<\/span><span>~\/Dropbox<\/span><\/code><\/pre>\n<\/div>\n<p>\n\u201cBut\u201d, I hear you say again, \u201caren&#8217;t we back to the same problem as before with local filenames?\u201d. The answer is \u201cNo\u201d. Because this filename is the same for all users of the code. It&#8217;s the <em>contents<\/em> of the file which changes per user. For this system to work, you need to avoid checking in the <code>config.ini<\/code> file into your repository (you can do this by adding it to <code>.gitignore<\/code>). And since you might want to use this configuration file in a lots of different places, I recommend creating a <code>config.py<\/code> module like this:<\/p>\n<div>\n<pre><code><span>import<\/span> configparser\n<span>import<\/span> pathlib\n\nconfig <span>=<\/span> configparser.ConfigParser()\nconfig.read('config.ini')\ndatadir <span>=<\/span> pathlib.Path(config['paths']['datadir']).expanduser()<\/code><\/pre>\n<\/div>\n<p>\nNow you can simply do this in the parts where you need to access your data:<\/p>\n<div>\n<pre><code><span>\n<\/span><\/code><\/pre>\n<pre><code><span>from<\/span> config <span>import<\/span> datadir<\/code><\/pre>\n<pre><code><span>import<\/span> pandas\n<\/code><\/pre>\n<pre><code>\ninput_data <span>=<\/span> pandas.read_csv(datadir <span>\/<\/span> 'input.csv')\nresults_data <span>=<\/span> expensive_operation(input_data)\nresults_data.to_csv(datadir <span>\/<\/span> 'results.csv')<\/code><\/pre>\n<\/div>\n<h2>\nIt&#8217;s not just for data<\/h2>\n<p>The cool thing about having this configuration file is that you can use it for any local configuration you need. Think about all the things that are in your code right now which should be local.<\/p>\n<h2>\nDecide between user-local and directory local<\/h2>\n<p>I&#8217;ve made a particular design choice in this solution which is to have the config file in the directory where the code is. It might suit your program better to have a configuration file in each user&#8217;s User directory or perhaps a global one. What I&#8217;ve presented here is already much better than hard-coding your paths, though.<\/p>\n<p><b>Late edit: <\/b>It turns out that there is a nice package in PyPI which can help you to avoid writing your own config file reader. It&#8217;s called&nbsp;<a href=\"https:\/\/github.com\/henriquebastos\/python-decouple\/\">decouple<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The wrong way You&#8217;ve started writing some code which reads from data files, and does some expensive computation which is written to some different data files. Perhaps you&#8217;re doing this in a Jupyter notebook which contains the following cell: import pandas input_data = pandas.read_csv(&#8216;input.csv&#8217;) results_data = expensive_operation(input_data) results_data.to_csv(&#8216;results.csv&#8217;) Now, you want to put your code [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[1],"tags":[],"class_list":["post-17","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/sandrock.co.za\/carl\/wp-json\/wp\/v2\/posts\/17","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/sandrock.co.za\/carl\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/sandrock.co.za\/carl\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/sandrock.co.za\/carl\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/sandrock.co.za\/carl\/wp-json\/wp\/v2\/comments?post=17"}],"version-history":[{"count":1,"href":"https:\/\/sandrock.co.za\/carl\/wp-json\/wp\/v2\/posts\/17\/revisions"}],"predecessor-version":[{"id":124,"href":"https:\/\/sandrock.co.za\/carl\/wp-json\/wp\/v2\/posts\/17\/revisions\/124"}],"wp:attachment":[{"href":"https:\/\/sandrock.co.za\/carl\/wp-json\/wp\/v2\/media?parent=17"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/sandrock.co.za\/carl\/wp-json\/wp\/v2\/categories?post=17"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/sandrock.co.za\/carl\/wp-json\/wp\/v2\/tags?post=17"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}