|
|
| Line 1: |
Line 1: |
| | '''This article is still being developed - released earlier to align with Department seminar.''' |
|
| |
|
| This wiki presents a motivational example of a lot of properties of python packages and how to use them. Please see [[packages motivation]] for motivation about why you should utilize the package framework while doing research. This material captures the basics presented by [https://py-pkgs.org/ py-pkgs], (recently) developed by UBC professors. As such you’ll find that python packages online may have different structures. Still, <code>py-pkgs</code> provides a lot of helpful tools to automate python package development. The <code>py-pkgs</code> project was actually started to mirror the helpful [[Developing_R_packages|package development tools in <code>R</code>]]. Note that this wiki page does provide additional commentary but the process proposed is very similar to <code>py-pkgs</code>’s approach.
| | In python, parallelization is a bit more of a “wild west” in terms of the number of packages that can help you parallelize your processes (for a long . This wiki will attempt to present minor examples of some along with an outline of things to think about in selecting the library you end up using. In general, the process in python involves |
|
| |
|
| == Foundations (what tools will be used) ==
| | # defining a function that captures what you want to have parallelized |
| | # call that function repeatedly in the the parallelization package way (which often looks like a list-recursive call). |
|
| |
|
| In the package development pipeline that is proposed by <code>py-pkgs</code> we have a few very power packages to help you develop an <code>python</code> package as painlessly as possible.
| | There are a few different general frameworks to accomplish this. |
|
| |
|
| * <code>poetry</code> help build a python package
| | # '''general wrappers''', define the cores you’ll be using and then wrap that around the function call in a list-recursive way |
| * <code>cookiecutter</code> helps utilize pre-made templates (for our package’s files)
| | # '''just in time compilers''', in addition to providing parallelization tools these packages provide decorators (e.g. <code>@jit</code>) for functions that allow for just in time compilation of the python function into lower level machine code (like <code>C++</code>) |
| * <code>pytest</code> provides clean and standardized tools for testing
| |
| * (numpy style code) provides rubrics for documentation
| |
|
| |
|
| == Building an python package ==
| | We will discuss a python package that is build specifically to parallelize jobs inside '''jupyter notebooks'''. |
|
| |
|
| This process is slightly different than the one we demonstrated in the [[Developing_R_packages]] wiki (we set up the structure of the package and then link to github).
| | = Non-parallelized: single core example = |
|
| |
|
| === Step 1: Creating a minimal source package ===
| | We naturally want to compare a parallelization approach with the base single core analysis, which can be done with the code as follows: |
|
| |
|
| A python (''source'') package just files in a directory, formatted in a specific way.
| | <syntaxhighlight lang="python">#base for-loop |
| | | import numpy as np |
| To create the bare bones python package, we will be using the packages <code>poetry</code> and <code>cookiecutter</code>, so if you haven’t already done so, install those two packages (we’ll be using <code>conda</code>) by typing:
| | from sklearn.linear_model import LinearRegression |
| | |
| <syntaxhighlight lang="bash">conda install -c conda-forge poetry cookiecutter</syntaxhighlight> | |
| ''Note that this line is taken directly from [https://py-pkgs.org/02-setup py-pkgs/02-setup], and it is possible that some day these packages can be installed wit ha simple <code>conda install poetry cookiecutter</code>.''
| |
| | |
| '''Now''', to actually start setting up your python package we will first use <code>cookiecutter</code> to copy a package structure provided by <code>py-pkgs</code> by running the following in our terminal:
| |
| | |
| <syntaxhighlight lang="bash">cookiecutter https://github.com/UBC-MDS/cookiecutter-ubc-mds.git</syntaxhighlight>
| |
| This will prompt you to answer a set of questions including
| |
| <ol>
| |
| <li> the author name (can include caps and spaces) </li>
| |
| <li> github username </li>
| |
| <li> package name (can include caps and spaces) </li>
| |
| <li> package slug name (can included underscores but no spaces) </li>
| |
| <li> a short package description (can include caps and spaces) </li>
| |
| <li> version number (we recommend either 0.0.1 or 0.1.0) </li>
| |
| <li> python version 8. open source license (they list the options) </li>
| |
| <li> if the package should include github actions (we suggest no for now, and plan to have wikis help with this later) </li>
| |
| </ol>
| |
| | |
| ''Note: our package for this example will be named <code>tartanpy</code>'' (an a copy of the "final" product can be found on [https://github.com/benjaminleroy/tartanpy|github].
| |
| | |
| ''Note: the cookiecutter template from <code>py-pkg</code>s (aka the one above) is still under construction - so it might look slightly different in the future.''
| |
| | |
| This will create a <code>tartanpy</code> folder with the following items:
| |
| | |
| # Directory <code>src/</code> (where all the function source code goes)
| |
| # Directory <code>docs/</code> (where all the pretty looking documentation goes '''- auto generated''')
| |
| # Directory <code>tests/</code> (where you’ll store your test, with a placeholder test file)
| |
| # <code>pyproject.toml</code> file (a description file about your package, which includes the things you entered when you ran the <code>cookiecutter</code> call)
| |
| # <code>LICENSE</code> (booler plate License - don’t really need to deal with this) | |
| # A set of <code>.md</code> files for document things like general info for viewers (<code>README.md</code>), info for contributors and their conduct (<code>CONTRIBUTING.md</code> and <code>CONDUCT.md</code>) and a file to include information on changes you’ve made <code>CHANGELOG.md</code>).
| |
| | |
| [[File:Tartanpy_folder.png|frameless|600px|alt=initial directory items|initial directory items]]
| |
| | |
| ==== pyproject.toml file meta-data ====
| |
| | |
| There are few ways for packages to store their meta-data, and <code>py-pkgs</code>’s path is through the <code>pyproject.toml</code>. Other options include things like <code>setup.py</code> files. If you made a mistake when answering questions via your cookiecutter prompts you can change them by opening up the <code>pyproject.toml</code> file. Our example package’s <code>pyproject.toml</code> file looks like the image below
| |
| | |
| [[File:Tartanpy_pyproject_description.png|frameless|600px|alt=initialized description file|initialized description file]]
| |
| | |
| In a <code>pyproject.toml</code> file you’ll notice that each section is headed by a <code>[info]</code> heading. Notice that three of the sections in the image above have <code>tool.poetry</code> headers. This means that the package <code>poetry</code> can update these sections. As we showcase the power of this package you may find that using poetry to update a lot of these fields is more desirable than manually doing it yourself.
| |
| | |
| === Step 2: Setting up a Github repo ===
| |
| | |
| Version control (and discussed in Stat Computing) is a very useful tool to help streamline file management and provide piece of mind. Oftentimes, you will want to have a copy of the repository not just on your personal computer but online (to help with sharing, just in case your computer dies, it’s cool, etc.). As such these steps will walk you through how to set up a github repo for your new package and how to connect that repo to a new python package. This step will mirror thoughts in [https://py-pkgs.org/03-how-to-package-a-python#set-up-remote-version-control py-pkgs/03-how-to-package-a-python].
| |
| | |
| <ol style="list-style-type: decimal;">
| |
| <li><p>'''In <code>Github</code>''', do the following</p>
| |
| <ul>
| |
| <li><p>go to the top-level directory (i.e., <code>github.com/<your user name></code>)</p></li>
| |
| <li><p>click on “+” at top right, and select “New repository”</p></li>
| |
| <li><p>name the repository (e.g., “tartan_python”). Note that if you’d like to share your package with others via pypi you’ll need to make your package name unique!</p></li>
| |
| <li><p>provide a short description of the repository (don’t leave completely blank!)</p></li>
| |
| <li><p>keep the repository public (as students you have access to free private repos https://education.github.com/pack, but for purposes of this example we will keep the repo public)</p></li>
| |
| <li><p>'''do not''' initalize with a README, .gitignore, or license (It even says “Skip this step if you’re importing an exisiting repo”!!!)</p></li>
| |
| <li><p>click on “Create Repository”</p></li></ul>
| |
| </li>
| |
| <li><p>'''…then in your terminal, do the following'''</p>
| |
| <ul>
| |
| <li>First, initialize your repository with
| |
| | |
| <syntaxhighlight lang="bash">git init</syntaxhighlight>
| |
| <p>then add all <code>cookiecutter</code> files and create your initial commit with <code>bash git add . git commit -m "initial package setup"</code></p>
| |
| </li>
| |
| <li>setup connection to github by adding a a remote, your main branch and pushing your first commit with
| |
| | |
| <syntaxhighlight lang="bash">git remote add origin https://github.com/benjaminleroy/tartanpy.git
| |
| git branch -M main
| |
| git push -u origin main</syntaxhighlight>
| |
| <p>''Note that github provides you with walk throughs that mirror what was recommended above and showcases 2 approaches if you want to use the command line (<code>https</code> and <code>ssh</code>) as well as how you might do it with github desktop. So helpful - thanks github!''</p></li></ul>
| |
| | |
| </li></ol>
| |
| | |
| === Step 3: Writing a function ===
| |
| | |
| All of your python code needs to go in <code>.py</code> files in the <code>src/tartanpy/</code> directory. Suppose our first set of functions deal with communicating to the user - then we might make a <code>communicate.py</code> file in <code>src/tartanpy/</code> folder. <br> Inside this file, let’s image we add the following function
| |
| | |
| <syntaxhighlight lang="python">def call_scotty_demo(your_name):
| |
| | |
| name_length = len(your_name)
| |
| | |
| # say Hiiiiii
| |
| hi_string = "\n" +\
| |
| "-" * (name_length + 8) + "\n" +\
| |
| "| Hi " + your_name + "! | \n" +\
| |
| "| " + "-" * (name_length + 4) + "\n" +\
| |
| "| /\n"
| |
| | |
| scotty_string = ""+\
| |
| " |/ |\\_/| \n" +\
| |
| " | |q p| /} \n" +\
| |
| " | ( 0 )\"\"\"\\ \n" +\
| |
| " \\ |\"-\"` | \n" +\
| |
| " || /=\\\\ | \n" +\
| |
| " \"'\" '\"\"\"'"
| |
| | |
| print(hi_string + scotty_string)
| |
| </syntaxhighlight>
| |
| To get access to this function we can use <code>poetry</code> to locally install our package with the bash command below (that needs to be run in the top folder of our package)
| |
| | |
| <pre>poetry install</pre>
| |
| ''Note that without poetry this could be done with a <code>pip install -e .</code> in the same directory and this would actually not require running this line of code every time we update the package. On the other hand - we’ll show the versatility of <code>poetry</code> in the rest of the article.''
| |
| | |
| To utilize our function we can then open up a interactive python instance (e.g. <code>ipython</code> in your terminal) and then run
| |
| | |
| <syntaxhighlight lang="python">In [1]: from tartanpy import communicate
| |
| | |
| In [2]: communicate.call_scotty_demo("Andrew Carnegie")</syntaxhighlight>
| |
| which will return:
| |
| | |
| <syntaxhighlight lang="bash">-----------------------
| |
| | Hi Andrew Carnegie! |
| |
| | -------------------
| |
| | /
| |
| |/ |\_/|
| |
| | |q p| /}
| |
| | ( 0 )"""\
| |
| \ |"-"` |
| |
| || /=\\ |
| |
| "'" '"""'</syntaxhighlight>
| |
| Note that <code>poetry install</code> can be called over and over if you keep updating your function files and will re-load the newest verion of all your functions.
| |
| | |
| ==== Additional notes: ====
| |
| | |
| ===== Understanding <code>__init__.py</code> =====
| |
| | |
| For <code>python</code> packages you’ll always have an <code>__init__.py</code> file in this directory. We currently have to import the <code>communicate</code> file from <code>tartanpy</code> in order to get access to our amazing function. To make this function come directly with the package, in the <code>__init__.py</code> file we can write
| |
| | |
| <syntaxhighlight lang="python">from .communicate import call_scotty_demo</syntaxhighlight>
| |
| ''Note the dot in <code>.communicate</code> says “in this same directory as this current file”, and we don’t have to include the <code>.py</code> file link at the end.'' Additionally This approach '''only''' imports call_scotty_demo to the global package, we could use <code>... import *</code> to import all functions (but this approach is smart if we had helper functions we didn’t really need to share with the end user).*
| |
| | |
| With the above change, we can then do (the <code>poetry</code> part still needs to be at the top directory):
| |
| | |
| <syntaxhighlight lang="bash">poetry install
| |
| ipython</syntaxhighlight>
| |
| and then in the ipython shell:
| |
| | |
| <syntaxhighlight lang="python">In [1]: import tartanpy
| |
| | |
| In [2]: tartanpy.call_scotty_demo("Andrew Mellon")</syntaxhighlight>
| |
| which will return:
| |
| | |
| <syntaxhighlight lang="bash">---------------------
| |
| | Hi Andrew Mellon! |
| |
| | -----------------
| |
| | /
| |
| |/ |\_/|
| |
| | |q p| /}
| |
| | ( 0 )"""\
| |
| \ |"-"` |
| |
| || /=\\ |
| |
| "'" '"""'</syntaxhighlight>
| |
| ===== Using external packages =====
| |
| | |
| [You'll want to read the next section as well for a full understanding of dealing with external packages.]
| |
| | |
| Suppose I wished to truncate really long names I’m greeting but also give the user a warning that I’m going to do so. I’d need to use the <code>warnings</code> package. To use this in our package I’d need to change our <code>communicate.py</code> file to
| |
| | |
| <syntaxhighlight lang="python">import warnings
| |
| | |
| def call_scotty_demo(your_name):
| |
| if len(your_name) > 30:
| |
| warnings.warn("your name is beyond 30 characters, "+\
| |
| "has been truncated")
| |
| your_name = your_name[:30]
| |
| | |
| name_length = len(your_name)
| |
| | |
| # say Hiiiiii
| |
| hi_string = "\n" +\
| |
| "-" * (name_length + 8) + "\n" +\
| |
| "| Hi " + your_name + "! | \n" +\
| |
| "| " + "-" * (name_length + 4) + "\n" +\
| |
| "| /\n"
| |
| | |
| scotty_string = ""+\
| |
| " |/ |\\_/| \n" +\
| |
| " | |q p| /} \n" +\
| |
| " | ( 0 )\"\"\"\\ \n" +\
| |
| " \\ |\"-\"` | \n" +\
| |
| " || /=\\\\ | \n" +\
| |
| " \"'\" '\"\"\"'"
| |
| | |
| print(hi_string + scotty_string)</syntaxhighlight>
| |
| Notice that we have to impot the <code>warnings</code> package at the top of our function file.
| |
| | |
| Now we have the additional funcitonality (after reloading with <code>poetry</code>)
| |
| | |
| <syntaxhighlight lang="python">In [1]: import tartanpy
| |
|
| |
|
| In [2]: tartanpy.call_scotty_demo("Oski Bear, your a wonderful, but slightly creepy Berkeley Bear")</syntaxhighlight>
| | my_k = list(np.arange(14)+1)*20 |
| which returns
| |
|
| |
|
| <syntaxhighlight lang="python">.../tartanpy/src/tartanpy/communicate.py:6: UserWarning: your name is beyond 30 characters, has been truncated
| | def my_simulation(k): |
| warnings.warn("your name is beyond 30 characters, "+\
| |
| | |
| --------------------------------------
| |
| | Hi Oski Bear, your a wonderful, b! |
| |
| | ----------------------------------
| |
| | /
| |
| |/ |\_/|
| |
| | |q p| /}
| |
| | ( 0 )"""\
| |
| \ |"-"` |
| |
| || /=\\ |
| |
| "'" '"""'</syntaxhighlight>
| |
| ==== Using external packages: declaring dependences & Using local functions in other files ====
| |
| | |
| <ol style="list-style-type: decimal;">
| |
| <li><p>'''Declaring dependencies''': The <code>warnings</code> module is actually a built in python package, but suppose we also needed to use <code>numpy</code> in our package. For example, suppose we created another <code>.py</code> file called <code>utils.py</code>, in it we had</p>
| |
| <syntaxhighlight lang="python">import numpy as np
| |
| | |
| def _top_and_bottom(name_length):
| |
| length_vec = np.array([8,4]) + name_length
| |
| return(length_vec)</syntaxhighlight>
| |
| <p>The first thing we’d need to do (so the code works for others) is to tell the package we require the installation of <code>numpy</code> for anyone using our package. We do so by running</p>
| |
| <syntaxhighlight lang="bash">poetry add numpy</syntaxhighlight>
| |
| <p>This will update our <code>pyproject.toml</code> and add asssociated dependences (there are none for <code>numpy</code>), and will put that note under <code>[tool.poetry.dependencies]</code>.</p>
| |
| <p>''Note: as of 8/20/21 this also required restricting my python version (in the <code>pyproject.toml</code> to <code>python = "?=3.8,<3.11"</code>)</p></li>
| |
| <li><p>'''Internal Functions''': To use our interal function <code>_top_and_bottom</code> we might change the <code>communicate.py</code> file to:</p>
| |
| <syntaxhighlight lang="python">import warnings
| |
| from .utils import _top_and_bottom
| |
| | |
| def call_scotty_demo(your_name):
| |
| # import warnings
| |
| if len(your_name) > 30:
| |
| warnings.warn("your name is beyond 30 characters, "+\
| |
| "has been truncated")
| |
| your_name = your_name[:30]
| |
| | |
| name_length = len(your_name)
| |
| | |
| tb_info = _top_and_bottom(name_length)
| |
| | |
| # say Hiiiiii
| |
| hi_string = "\n" +\
| |
| "-" * tb_info[0] + "\n" +\
| |
| "| Hi " + your_name + "! | \n" +\
| |
| "| " + "-" * tb_info[1] + "\n" +\
| |
| "| /\n"
| |
| | |
| ...</syntaxhighlight>
| |
| <p>Notice that to give <code>call_scotty_demo</code> and all functions in the <code>communicate.py</code> file access to <code>_top_and_bottom</code> we had to import it using <code>from .utils import _top_and_bottom</code>. Additionally, it’s best practice to use add a <code>_</code> at the beginning of a function name if you plan to keep it internal.</p></li></ol>
| |
| | |
| === Step 4: Documenting your functions ===
| |
| | |
| Python documentation is simplier than R documentation as when you look at the documentation of a python function you just get show a docstring at the head of function. In this section we’ll discuss a bit about the extra support poetry can do so that you can generate a localized website for your documentation (that you could upload to [https:://readthedocs.org readthedocs] later).
| |
| | |
| For documentation to be easily publishable at a later date we recommend that your docstrings follow the <code>numpydoc</code> format (more details can be found at [https://numpydoc.readthedocs.io/en/latest/format.html#docstring-standard numpydoc.readthedocs]). Using this guideline we can document <code>call_scotty_demo</code> as follows:
| |
| | |
| <syntaxhighlight lang="python">def call_scotty_demo(your_name):
| |
| """ | | """ |
| Greet Scotty, the Scottie Dog | | estamine the polynomial fit of degree k's MSE for a linear |
| | | relationship |
| Scotty, the Scottie Dog greets your by name! | |
|
| |
|
| Parameters | | Parameters |
| ---------- | | ---------- |
| your_name: str | | k : int |
| string of your name (max length 30 characters)
| | degree of polynomial fit |
|
| |
|
| Returns | | Returns |
| ------- | | ------- |
| None | | mse of fit based on 30 points applied to a test set of 10000 points |
| Though scotty appears on your screen and says Hi to you.
| |
| | |
| Examples
| |
| --------
| |
| >>> import tartanpy
| |
| >>> tartanpy.call_scotty_demo("Andrew Carnegie")
| |
| | |
| -----------------------
| |
| | Hi Andrew Carnegie! |
| |
| | -------------------
| |
| | /
| |
| |/ |\_/|
| |
| | |q p| /}
| |
| | ( 0 )\"\"\"\\
| |
| \\ |\"-\"` |
| |
| || /=\\\\ |
| |
| \"'\" '\"\"\"'
| |
| """ | | """ |
| ...</syntaxhighlight> | | x = np.random.uniform(size=30,low=-3,high=3) |
| The basic structure of the documention is decently straight forward with the title / description of your function first, followed by (1) Parameters header with the parameter name, the type, and the description in the format above (2) Returns header followed by similar structure as Parameters. Adding (3) examples with standard python lines can be helpful as well.
| | y = 2*x + 2* np.random.normal(size=30) |
| | y = y.reshape(-1,1) |
| | X_poly = np.concatenate([x.reshape(-1,1)**kk |
| | for kk in np.arange(1,k+1,dtype = int)], |
| | axis = 1) |
|
| |
|
| After reloading the package you could see that
| |
|
| |
|
| <syntaxhighlight lang="python">In [1]: import tartanpy
| | x0 = np.random.uniform(size=10000,low=-3,high=3) |
| | y0 = 2*x0 + 2*np.random.normal(size=10000) |
| | y0 = y0.reshape(-1,1) |
| | X0_poly = np.concatenate([x0.reshape(-1,1)**kk |
| | for kk in np.arange(1,k+1,dtype = int)], |
| | axis = 1) |
|
| |
|
| In [2]: ?tartanpy.call_scotty_demo
| | my_lm = LinearRegression().fit(X_poly,y) |
| Signature: tartanpy.call_scotty_demo(your_name)
| | y0_hat = my_lm.predict(X0_poly) |
| Docstring:
| |
| Greet Scotty, the Scottie Dog
| |
|
| |
|
| Scotty, the Scottie Dog greets your by name!
| | return(np.mean((y0-y0_hat)**2)) |
| | | </syntaxhighlight> |
| Parameters
| | <syntaxhighlight lang="python">%%timeit -n3 |
| ----------
| |
| your_name: str
| |
| string of your name (max length 30 characters) | |
| | |
| Returns
| |
| -------
| |
| None
| |
| Though scotty appears on your screen and says Hi to you.
| |
|
| |
|
| Examples
| | [my_simulation(k) for k in my_k]</syntaxhighlight> |
| --------
| | <syntaxhighlight lang="python">676 ms ± 37.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)</syntaxhighlight> |
| >>> import tartanpy
| | = General python parallelization packages = |
| >>> tartanpy.call_scotty_demo("Andrew Carnegie")
| |
| -----------------------
| |
| | Hi Andrew Carnegie! |
| |
| | -------------------
| |
| | /
| |
| |/ |\_/|
| |
| | |q p| /}
| |
| | ( 0 )"""\
| |
| \ |"-"` |
| |
| || /=\\ |
| |
| "'" '"""'
| |
| File: ..../tartanpy/src/tartanpy/communicate.py
| |
| Type: function</syntaxhighlight>
| |
| ==== Towards "readthedocs"-style html documentation ==== | |
|
| |
|
| py-pkgs and poetry also provide a quick way to transform your documentation into a local html website so you can quickly look through your documentation or load it to readthedocs later, with information on how to here: [https://py-pkgs.org/06-documentation py-pkgs/06-documentation].
| | As mentioned there are a lot of package to do general parallelization. Two commonly used ones are <code>joblib</code> and <code>multiprocessing</code>. <code>joblib</code> looks a bit more like list-recursion and <code>multiprocessing</code> uses functions like <code>.map()</code>. <code>multiprocessing</code> can have problems with <code>ipython</code> and <code>jupyter notebook</code>s (though <code>joblib</code> wraps it and <code>multiprocess</code> is just a different fork of <code>multiprocessing</code>). |
|
| |
|
| Generally speaking you have to
| | == <code>multiprocessing</code>/<code>multiprocess</code> == |
|
| |
|
| <ol> | | The <code>multiprocessing</code> package is actually built into python, and works in a pretty straightforward manner. It also allows for synchronous vs asynchronous parallelization, where synchronous means the order of the jobs must be followed and asynchronous where the order of jobs need not be followed. Many statistical uses of parallelization does not need synchronous parallelization. |
| <li> install some additional packages to the development side of your package with | |
|
| |
|
| <syntaxhighlight lang="bash">poetry add --dev sphinx nbsphinx ipykernel</syntaxhighlight> </li> | | There since both approaches have the same syntax we’ll be using <code>multiprocess</code> (as it works with <code>ipython</code> and <code>jupyter notebooks</code>). |
|
| |
|
| <li>and then go to your docs folder and make the website locally (the <code>--dev</code> parameter makes these packages not tracked for those just wanted to use your package, but required as you continue to develop the package locally).
| | === parallelization === |
|
| |
|
| <syntaxhighlight lang="bash">cd docs
| | First, you need to identify the number of cores you have: |
| poetry run make html</syntaxhighlight>
| |
| and this will create a local webpage (just open <code>docs/index.html</code>) that contains your documentation. If you find the docs website useful maybe bookmark the local file?
| |
|
| |
|
| </li></ol> | | <syntaxhighlight lang="python">import multiprocess as mp |
| | #import multiprocessing as mp |
|
| |
|
| === Step 5: Testing === | | num_cores = mp.cpu_count()</syntaxhighlight> |
| | ==== synchronous ==== |
|
| |
|
| Testing is critical to making sure your functions do what you expect. There are few ways to write unit tests in python (e.g. <code>unittest</code>, <code>pytest</code> and <code>nose</code>). We recommend using <code>pytest</code> as it requires the least amount of overhead and provides the most details write off the bat.
| | <syntaxhighlight lang="python">%%timeit |
|
| |
|
| To use <code>pytest</code> we first to tell <code>poetry</code> that we’ll use <code>pytest</code> in our development with
| | with mp.Pool(processes=num_cores-2) as p: |
| | results = p.map(my_simulation, my_k)</syntaxhighlight> |
| | <syntaxhighlight lang="python">209 ms ± 24.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)</syntaxhighlight> |
| | ==== asynchronous ==== |
|
| |
|
| <syntaxhighlight lang="bash">poetry add --dev pytest</syntaxhighlight> | | <syntaxhighlight lang="python">%%timeit |
| ==== What’s the philosophy behind <code>pytest</code> ====
| |
|
| |
|
| <ol> | | with mp.Pool(processes=num_cores-2) as p: |
| <li>'''Assertions''': <code>pytest</code>’s foundation is defined around checking basic assertion statements like | | results = p.map_async(my_simulation, my_k).get() # not clear why I needed to use .get() to get something returnable for async</syntaxhighlight> |
| | <syntaxhighlight lang="python">48.9 ms ± 3.08 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)</syntaxhighlight> |
| | == <code>joblib</code> == |
|
| |
|
| <syntaxhighlight lang="python">In [1]: assert 1 == 1, "a true statement shouldn't have errored..." | | The <code>joblib</code> package is pretty straightforward and requires you to structure things in terms of list-recursion with a few wrappers to help the process work as desired. |
| ...: assert .3 - .2 == .1, "but sometimes floating points are bad..."</syntaxhighlight>
| |
| that return:
| |
|
| |
|
| <syntaxhighlight lang="python">---------------------------------------------------------------------------
| | === setup === |
| AssertionError Traceback (most recent call last)
| |
| <ipython-input-3-0daf5f7e9b9c> in <module>
| |
| 1 assert 1 == 1, "a true statement shouldn't have errored..."
| |
| ----> 2 assert .3 - .2 == .1, "but sometimes floating points are bad..."
| |
|
| |
|
| AssertionError: but sometimes floating points are bad...</syntaxhighlight>
| | <syntaxhighlight lang="python">import joblib |
| as the second line actual fails (due to some floating point silliness). This simple structure can be combined with standard logical <code>and</code> and <code>or</code> as well as other tools like <code>numpy</code> logic (.e.g <code>np.all()</code>). Notice that following a logical statement with a string nicely associated a statement to be returned if the assertion is not true.
| |
| </li>
| |
| <li>'''Grouping Assertions''': We can imagine wishing to combine a set of assertions, and we can do this easily with a function wrapper. For example
| |
|
| |
|
| <syntaxhighlight lang="python">import tartanpy | | num_cores = joblib.cpu_count()</syntaxhighlight> |
| import pytest # only for warning
| | === implimentation === |
|
| |
|
| def test_call_scotty_demo():
| | To use <code>joblib</code> functionality we can import the functions <code>Parallel</code> and <code>delayed</code>. |
| """
| |
| test the basics of call_scotty_demo
| |
| """
| |
|
| |
|
| assert tartanpy.call_scotty_demo("Oski Bear") is None, \
| | <syntaxhighlight lang="python">from joblib import Parallel, delayed</syntaxhighlight> |
| "the output of call_scotty_demo should be None"
| | We then can move our list-recursive example into our <code>joblib</code> structure. We wrap <code>Parallel(...)()</code> around the old of the inside of the list-recursion and <code>delayed()</code> around the functions that will be repeated multiple times. |
|
| |
|
| # test warning
| | <syntaxhighlight lang="python">%% timeit |
| with pytest.warns(UserWarning) as record:
| |
| tartanpy.call_scotty_demo("Oski Bear, your a wonderful, but slightly creepy Berkeley Bear")
| |
|
| |
|
| assert len(record) > 0, \
| | r = Parallel(n_jobs=num_cores-2)(delayed(my_simulation)(k) for k in my_k)</syntaxhighlight> |
| "expect a warning for name_string longer than 30 for call_scotty_demo"
| | <syntaxhighlight lang="python">172 ms ± 23.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)</syntaxhighlight> |
| | === Add-ons === |
|
| |
|
| assert .3 - .2 == .1, "but sometimes floating points are bad..."</syntaxhighlight>
| | There are multiple add-ons that can be desirable |
| </li>
| |
| <li>To actually leverage <code>pytest</code> we can put '''A set of grouped tests''' in single file. When building a package your functions will likelyl live in different <code>.py</code> files in the <code>src/<packagename></code> folder. To mirror this structure we put of our tests associated with a single <code>.py</code> file into 1 test file. These files sit in the <code>tests/</code> folder and must start with <code>test</code>.
| |
|
| |
|
| In these larger files (relative to group functions) need to make sure our functions are brought in, for example at the head we need to put <code>import tartanpy</code> as see in the code block above.
| | <ol style="list-style-type: decimal;"> |
| | | <li><p>'''verbosity''' can be added with a single parameter use (as seen below) - the large the value, the more messages:</p> |
| To run these tests you can either do
| | <syntaxhighlight lang="python">r = Parallel(n_jobs=num_cores-2, verbose=10)(delayed(my_simulation)(k) for k in my_k)</syntaxhighlight></li> |
| | <li><p>'''multiple parameters''' can be passed with redefinitions of functions to take groupings. To replace multiple for-loops often the function <code>itertools.product()</code> is used for the parameters.</p></li></ol> |
|
| |
|
| <syntaxhighlight lang="bash">pytest</syntaxhighlight>
| | = Just in Time compilers + parallelization = |
| or (making sure you have all the correct packages)
| |
|
| |
|
| <syntaxhighlight lang="bash">poetry run pytest</syntaxhighlight> | | TODO: <code>numba</code> and <code>cython</code>. The idea of these is to compile the function you will be using the first time you use it and then never compile it again (speeding up the actual use of the function). That is then combined with standard parallelization |
| ''Note: we actually have some printouts in <code>call_scotty_demo</code>, which are not desirable, so we actually follow a suggestion from [https://stackoverflow.com/questions/8391411/how-to-block-calls-to-print stackoverflow] to suppress the printing (doesn’t mess with the testing), and is seen in the code block below:''
| |
|
| |
|
| <syntaxhighlight lang="python">import tartanpy | | = Jupyter notebook: <code>ipyparallel</code> = |
| import pytest # only for warning
| |
|
| |
|
| def test_call_scotty_demo():
| | For those that heavily use jupyter notebooks and nothing else (which this author personally doesn’t encourage), the package <code>ipyparallel</code> nicely works inside jupyter notebooks (but so does many of the other packages above like <code>joblib</code>, <code>multiprocess</code>, etc.). One nice this about this package is the verbosity, etc. works intellegently in the notebook. This package also works with <code>numba.jit</code> and <code>cython</code>. |
| """
| |
| test the basics of call_scotty_demo
| |
| """
| |
| with open(os.devnull, "w") as f, contextlib.redirect_stdout(f):
| |
| assert tartanpy.call_scotty_demo("Oski Bear") is None, \
| |
| "the output of call_scotty_demo should be None"
| |
|
| |
|
| # test warning
| | Before opening your <code>jupyter notebook</code> you’ll want to make sure the extension is enabled - which can be down from the command line with the following command or inside the <code>jupyter notebook</code> extensions tab. |
| with pytest.warns(UserWarning) as record:
| |
| tartanpy.call_scotty_demo("Oski Bear, your a wonderful, but slightly creepy Berkeley Bear")
| |
|
| |
|
| assert len(record) > 0, \
| | <syntaxhighlight lang="bash">ipcluster nbextension enable</syntaxhighlight> |
| "expect a warning for name_string longer than 30 for call_scotty_demo"
| | And then you’ll need to go to the “IPython Clusters” tab in <code>jupyter noteook</code> and start some parallel clusters. |
|
| |
|
| | == <code>ipyparallel</code> parallelization == |
|
| |
|
| | <syntaxhighlight lang="python">import ipyparallel as ipp</syntaxhighlight> |
| | For direct execution we define <code>dv</code> and provide it with what it needs… |
|
| |
|
| assert .3 - .2 == .1, "but sometimes floating points are bad..."</syntaxhighlight>
| | <syntaxhighlight lang="python">rc = ipp.Client() |
| If we remove the silly floating point error assession we get the following:
| | dv = rc[:] # direct view</syntaxhighlight> |
| | === passing things to each cluster === |
|
| |
|
| [[File:Pytest_example.png|frameless|600px|alt=output of pytest|output of pytest]]
| | <code>ipyparallel</code> provides ways for the user to pass only the necessary items to the the clusters (as to not overload the clusters with too many things). |
|
| |
|
| And if there are errors <code>pytest</code> tries to be very clear about where in your tests you had an error.
| | <ol style="list-style-type: decimal;"> |
| </li></ol>
| | <li><p>passing packages all together</p> |
| | | <syntaxhighlight lang="python">with dv.sync_imports(): |
| ==== More comments on testing ==== | | from sklearn.linear_model import LinearRegression |
| | | # getting dependencies on the clusters</syntaxhighlight></li> |
| <ol>
| | <li><p>passing variable over:</p> |
| <li>'''Hidden and/or not exported functions''' | | <p>Below is just an example - we don’t need to pass <code>my_k</code> since <code>dv</code> will directly get it when we wnat to use it.</p> |
| | | <syntaxhighlight lang="python">dv["my_k"] = my_k</syntaxhighlight></li> |
| In our example we have a hidden function <code>_top_and_bottom</code>. Different thoughts exist on whether or not one should test hidden functions, but to do so we have to be a bit tricker since the function isn’t loaded by the package. We can still test it by using the full path to the function, that is:
| | <li><p>we can also use the <code>%px</code> magick to run lines of code on all servers. Given I think <code>numpy</code> is cooler as <code>np</code> I actually have to do it this way, not the <code>dv.sync_imports</code> way.</p> |
| | | <syntaxhighlight lang="python">%px import numpy as np</syntaxhighlight></li></ol> |
| <syntaxhighlight lang="python">import tartanpy | |
| import numpy as np
| |
| | |
| def test__top_and_bottom():
| |
| """ | |
| test for hidden function _top_and_bottom
| |
| """
| |
| option4 = tartanpy.utils._top_and_bottom(len("test"))
| |
| option17 = tartanpy.utils._top_and_bottom(len("test_a_longer_one")) | |
| | |
| assert np.all(option17 - option4 == 17-4), \
| |
| "expect _top_and_bottom values to only differ by length of input"
| |
| | |
| assert np.all(option17 - 17 == np.array([8,4])), \
| |
| "expect that _top_and_bottom adds 8 and 4 to length of input"
| |
| | |
| </syntaxhighlight> | |
| Additionally, since we started the name off with a <code>_</code>, we could also find that if we did
| |
| | |
| <syntaxhighlight lang="python">from tartanpy.utils import *</syntaxhighlight> | |
| which should import everything from the <code>util.py</code> file we won’t actually have access to it, whereas
| |
| | |
| <syntaxhighlight lang="python">from tartanpy.communicate import *</syntaxhighlight> | |
| would get access to <code>call_scotty_demo</code> even if it wasn’t in the <code>__init__.py</code> file.
| |
| | |
| </li> | |
| <li>'''Numerical instablity''' | |
| | |
| We included an example where floating point differnces made <code>.3-.2</code> not equal to <code>.1</code>. To avoid such errors we recommend using tools like
| |
| | |
| * <code>np.allclose</code> (which allows user to provide relative and absolute tolerance between vectors)
| |
| * or do something like <code>np.abs(a-b)<1e-08</code> or <code>np.abs(a-b)/np.abs(b) < 1e-08</code>
| |
| </li></ol>
| |
| | |
| === Step 6. Sharing / using on other computers ===
| |
| | |
| You could publish your package to pypi (documented here: [https://py-pkgs.org/07-releasing-versioning py-pkg/07-releasing-versioning]) but if you’re still actively working on a developer version another easy way is to make sure you have a clean copy on github, have the user clone the repo and then run either (when in the top directory of the cloned repo)
| |
| | |
| <syntaxhighlight lang="bash">pip install .</syntaxhighlight> | |
| or
| |
| | |
| <syntaxhighlight lang="bash">pip install -e .</syntaxhighlight> | |
| where the first installs the package 1 time, and the second actually installs the package and updates the package if that user was to do more pulls / updates the the package.
| |
|
| |
|
| | === actual functionality === |
|
| |
|
| | The actual functionality is very similar to <code>multiprocess</code>/<code>multiprocessing</code> with <code>maps</code> and <code>apply</code>. |
|
| |
|
| === Step 7. And beyond! === | | ==== synchronous ==== |
|
| |
|
| In the above example, we still didn’t cover a lot of things, including:
| | <syntaxhighlight lang="python">%%timeit |
| | results = dv.map_sync(my_simulation, my_k)</syntaxhighlight> |
| | <syntaxhighlight lang="python">201 ms ± 34.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)</syntaxhighlight> |
| | ==== asynchronous ==== |
|
| |
|
| *publishing the package
| | <syntaxhighlight lang="python">%%timeit |
| *publishing the docs
| | results = dv.map_async(my_simulation, my_k).get() # not clear why I needed to use .get() to get something returnable for async</syntaxhighlight> |
| *including data
| | <syntaxhighlight lang="python">209 ms ± 28.1 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)</syntaxhighlight> |
| *CI/CD (which will be provided in another wiki)
| | = additional topics and resources = |
|
| |
|
| See [https://py-pkgs.org py-pkgs] for some good starting blocks for most of these.
| | # [https://ray.io/ <code>Ray</code> python package] allows paralleilization across multiple computers |
| | # [https://people.duke.edu/~ccc14/sta-663-2018/notebooks/S14A_Parallel_Programming_Introduction.html Duke’s statistical computing course] provides a set of examples for multiple python parallelization packages | |
This article is still being developed - released earlier to align with Department seminar.
In python, parallelization is a bit more of a “wild west” in terms of the number of packages that can help you parallelize your processes (for a long . This wiki will attempt to present minor examples of some along with an outline of things to think about in selecting the library you end up using. In general, the process in python involves
- defining a function that captures what you want to have parallelized
- call that function repeatedly in the the parallelization package way (which often looks like a list-recursive call).
There are a few different general frameworks to accomplish this.
- general wrappers, define the cores you’ll be using and then wrap that around the function call in a list-recursive way
- just in time compilers, in addition to providing parallelization tools these packages provide decorators (e.g.
@jit) for functions that allow for just in time compilation of the python function into lower level machine code (like C++)
We will discuss a python package that is build specifically to parallelize jobs inside jupyter notebooks.
Non-parallelized: single core example
We naturally want to compare a parallelization approach with the base single core analysis, which can be done with the code as follows:
#base for-loop
import numpy as np
from sklearn.linear_model import LinearRegression
my_k = list(np.arange(14)+1)*20
def my_simulation(k):
"""
estamine the polynomial fit of degree k's MSE for a linear
relationship
Parameters
----------
k : int
degree of polynomial fit
Returns
-------
mse of fit based on 30 points applied to a test set of 10000 points
"""
x = np.random.uniform(size=30,low=-3,high=3)
y = 2*x + 2* np.random.normal(size=30)
y = y.reshape(-1,1)
X_poly = np.concatenate([x.reshape(-1,1)**kk
for kk in np.arange(1,k+1,dtype = int)],
axis = 1)
x0 = np.random.uniform(size=10000,low=-3,high=3)
y0 = 2*x0 + 2*np.random.normal(size=10000)
y0 = y0.reshape(-1,1)
X0_poly = np.concatenate([x0.reshape(-1,1)**kk
for kk in np.arange(1,k+1,dtype = int)],
axis = 1)
my_lm = LinearRegression().fit(X_poly,y)
y0_hat = my_lm.predict(X0_poly)
return(np.mean((y0-y0_hat)**2))
%%timeit -n3
[my_simulation(k) for k in my_k]
676 ms ± 37.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
General python parallelization packages
As mentioned there are a lot of package to do general parallelization. Two commonly used ones are joblib and multiprocessing. joblib looks a bit more like list-recursion and multiprocessing uses functions like .map(). multiprocessing can have problems with ipython and jupyter notebooks (though joblib wraps it and multiprocess is just a different fork of multiprocessing).
multiprocessing/multiprocess
The multiprocessing package is actually built into python, and works in a pretty straightforward manner. It also allows for synchronous vs asynchronous parallelization, where synchronous means the order of the jobs must be followed and asynchronous where the order of jobs need not be followed. Many statistical uses of parallelization does not need synchronous parallelization.
There since both approaches have the same syntax we’ll be using multiprocess (as it works with ipython and jupyter notebooks).
parallelization
First, you need to identify the number of cores you have:
import multiprocess as mp
#import multiprocessing as mp
num_cores = mp.cpu_count()
synchronous
%%timeit
with mp.Pool(processes=num_cores-2) as p:
results = p.map(my_simulation, my_k)
209 ms ± 24.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
asynchronous
%%timeit
with mp.Pool(processes=num_cores-2) as p:
results = p.map_async(my_simulation, my_k).get() # not clear why I needed to use .get() to get something returnable for async
48.9 ms ± 3.08 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
joblib
The joblib package is pretty straightforward and requires you to structure things in terms of list-recursion with a few wrappers to help the process work as desired.
setup
import joblib
num_cores = joblib.cpu_count()
implimentation
To use joblib functionality we can import the functions Parallel and delayed.
from joblib import Parallel, delayed
We then can move our list-recursive example into our joblib structure. We wrap Parallel(...)() around the old of the inside of the list-recursion and delayed() around the functions that will be repeated multiple times.
%% timeit
r = Parallel(n_jobs=num_cores-2)(delayed(my_simulation)(k) for k in my_k)
172 ms ± 23.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Add-ons
There are multiple add-ons that can be desirable
verbosity can be added with a single parameter use (as seen below) - the large the value, the more messages:
r = Parallel(n_jobs=num_cores-2, verbose=10)(delayed(my_simulation)(k) for k in my_k)
multiple parameters can be passed with redefinitions of functions to take groupings. To replace multiple for-loops often the function itertools.product() is used for the parameters.
Just in Time compilers + parallelization
TODO: numba and cython. The idea of these is to compile the function you will be using the first time you use it and then never compile it again (speeding up the actual use of the function). That is then combined with standard parallelization
Jupyter notebook: ipyparallel
For those that heavily use jupyter notebooks and nothing else (which this author personally doesn’t encourage), the package ipyparallel nicely works inside jupyter notebooks (but so does many of the other packages above like joblib, multiprocess, etc.). One nice this about this package is the verbosity, etc. works intellegently in the notebook. This package also works with numba.jit and cython.
Before opening your jupyter notebook you’ll want to make sure the extension is enabled - which can be down from the command line with the following command or inside the jupyter notebook extensions tab.
ipcluster nbextension enable
And then you’ll need to go to the “IPython Clusters” tab in jupyter noteook and start some parallel clusters.
ipyparallel parallelization
import ipyparallel as ipp
For direct execution we define dv and provide it with what it needs…
rc = ipp.Client()
dv = rc[:] # direct view
passing things to each cluster
ipyparallel provides ways for the user to pass only the necessary items to the the clusters (as to not overload the clusters with too many things).
passing packages all together
with dv.sync_imports():
from sklearn.linear_model import LinearRegression
# getting dependencies on the clusters
passing variable over:
Below is just an example - we don’t need to pass my_k since dv will directly get it when we wnat to use it.
we can also use the %px magick to run lines of code on all servers. Given I think numpy is cooler as np I actually have to do it this way, not the dv.sync_imports way.
actual functionality
The actual functionality is very similar to multiprocess/multiprocessing with maps and apply.
synchronous
%%timeit
results = dv.map_sync(my_simulation, my_k)
201 ms ± 34.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
asynchronous
%%timeit
results = dv.map_async(my_simulation, my_k).get() # not clear why I needed to use .get() to get something returnable for async
209 ms ± 28.1 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
additional topics and resources
Ray python package allows paralleilization across multiple computers
- Duke’s statistical computing course provides a set of examples for multiple python parallelization packages |