<jupyter_start><jupyter_text><img src="http://dask.readthedocs.io/en/latest/_images/dask_horizontal.svg"     align="right"     width="30%"     alt="Dask logo\"> Parallelize code with `dask.delayed`In this section we parallelize simple for-loop style code with Dask and `dask.delayed`.This is a simple way to use `dask` to parallelize existing codebases or build [complex systems](http://matthewrocklin.com/blog/work/2018/02/09/credit-models-with-dask).  This will also help us to develop an understanding for later sections.  BasicsFirst let's make some toy functions, `inc` and `add`, that sleep for a while to simulate work. We'll then time running these functions normally.In the next section we'll parallelize this code.<jupyter_code>from time import sleep

def inc(x):
    sleep(1)
    return x + 1

def add(x, y):
    sleep(1)
    return x + y
%%time
# This takes three seconds to run because we call each
# function sequentially, one after the other

x = inc(1)
y = inc(2)
z = add(x, y)<jupyter_output><empty_output><jupyter_text>Parallelize with the `dask.delayed` decoratorThose two increment calls *could* be called in parallel.We'll wrap the `inc` and `add` functions in the `dask.delayed` decorator. When we call the delayed version by passing the arguments, the original function isn't actually called yet.Instead, a *task graph* is built up, representing the *delayed* function call.<jupyter_code>from dask import delayed
%%time
# This runs immediately, all it does is build a graph

x = delayed(inc)(1)
y = delayed(inc)(2)
z = delayed(add)(x, y)<jupyter_output><empty_output><jupyter_text>This ran immediately, since nothing has really happened yet.To get the result, call `compute`.<jupyter_code>%%time
# This actually runs our computation using a local thread pool

z.compute()<jupyter_output><empty_output><jupyter_text>What just happened?The `z` object is a lazy `Delayed` object.  This object holds everything we need to compute the final result.  We can compute the result with `.compute()` as above or we can visualize the task graph for this value with `.visualize()`.<jupyter_code>z
# Look at the task graph for `z`
z.visualize()<jupyter_output><empty_output><jupyter_text>Some questions to consider:-  Why did we go from 3s to 2s?  Why weren't we able to parallelize down to 1s?-  What would have happened if the inc and add functions didn't include the `sleep(1)`?  Would Dask still be able to speed up this code?-  What if we have multiple outputs or also want to get access to x or y?  Exercise: Parallelize a for loop`for` loops are one of the most common things that we want to parallelize.  Use `dask.delayed` on `inc` and `sum` to parallelize the computation below:<jupyter_code>data = [1, 2, 3, 4, 5, 6, 7, 8]
%%time
# Sequential code

results = []
for x in data:
    y = inc(x)
    results.append(y)
    
total = sum(results)
total
%%time
# Your parallel code here...
%load solutions/01-delayed-loop.py<jupyter_output><empty_output><jupyter_text>Exercise: Parallelizing a for-loop code with control flowOften we want to delay only *some* functions, running a few of them immediately.  This is especially helpful when those functions are fast and help us to determine what other slower functions we should call.  This decision, to delay or not to delay, is usually where we need to be thoughtful when using `dask.delayed`.In the example below we iterate through a list of inputs.  If that input is even then we want to call `inc`.  If the input is odd then we want to call `double`.  This `is_even` decision to call `inc` or `double` has to be made immediately (not lazily) in order for our graph-building Python code to proceed.<jupyter_code>def double(x):
    sleep(1)
    return 2 * x

def is_even(x):
    return not x % 2

data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
%%time
# Sequential code

results = []
for x in data:
    if is_even(x):
        y = double(x)
    else:
        y = inc(x)
    results.append(y)
    
total = sum(results)
print(total)
%%time
# Your parallel code here...
# TODO: parallelize the sequential code above using dask.delayed
# You will need to delay some functions, but not all
%load solutions/01-delayed-control-flow.py
%time total.compute()
total.visualize()<jupyter_output><empty_output><jupyter_text>Some questions to consider:-  What are other examples of control flow where we can't use delayed?-  What would have happened if we had delayed the evaluation of `is_even(x)` in the example above?-  What are your thoughts on delaying `sum`?  This function is both computational but also fast to run.  Exercise: Parallelizing a Pandas Groupby ReductionIn this exercise we read several CSV files and perform a groupby operation in parallel.  We are given sequential code to do this and parallelize it with `dask.delayed`.The computation we will parallelize is to compute the mean departure delay per airport from some historical flight data.  We will do this by using `dask.delayed` together with `pandas`.  In a future section we will do this same exercise with `dask.dataframe`.  Prep dataFirst, run this code to prep some data.This downloads and extracts some historical flight data for flights out of NYC between 1990 and 2000. The data is originally from [here](http://stat-computing.org/dataexpo/2009/the-data.html).<jupyter_code>%run prep_data.py<jupyter_output><empty_output><jupyter_text>Inspect data<jupyter_code>import os
sorted(os.listdir(os.path.join('data', 'nycflights')))<jupyter_output><empty_output><jupyter_text>Read one file with `pandas.read_csv` and compute mean departure delay<jupyter_code>import pandas as pd
df = pd.read_csv(os.path.join('data', 'nycflights', '1990.csv'))
df.head()
# What is the schema?
df.dtypes
# What originating airports are in the data?
df.Origin.unique()
# Mean departure delay per-airport for one year
df.groupby('Origin').DepDelay.mean()<jupyter_output><empty_output><jupyter_text>Sequential code: Mean Departure Delay Per AirportThe above cell computes the mean departure delay per-airport for one year. Here we expand that to all years using a sequential for loop.<jupyter_code>from glob import glob
filenames = sorted(glob(os.path.join('data', 'nycflights', '*.csv')))
%%time

sums = []
counts = []
for fn in filenames:
    # Read in file
    df = pd.read_csv(fn)
    
    # Groupby origin airport
    by_origin = df.groupby('Origin')
    
    # Sum of all departure delays by origin
    total = by_origin.DepDelay.sum()
    
    # Number of flights by origin
    count = by_origin.DepDelay.count()
    
    # Save the intermediates
    sums.append(total)
    counts.append(count)

# Combine intermediates to get total mean-delay-per-origin
total_delays = sum(sums)
n_flights = sum(counts)
mean = total_delays / n_flights
mean<jupyter_output><empty_output><jupyter_text>Parallelize the code aboveUse `dask.delayed` to parallelize the code above.  Some extra things you will need to know.1.  Methods and attribute access on delayed objects work automatically, so if you have a delayed object you can perform normal arithmetic, slicing, and method calls on it and it will produce the correct delayed calls.    ```python    x = delayed(np.arange)(10)    y = (x + 1)[::2].sum()   everything here was delayed    ```2.  Calling the `.compute()` method works well when you have a single output.  When you have multiple outputs you might want to use the `dask.compute` function:    ```python    >>> x = delayed(np.arange)(10)    >>> y = x ** 2    >>> min_, max_ = compute(y.min(), y.max())    >>> min_, max_    (0, 81)    ```        This way Dask can share the intermediate values (like `y = x**2`)    So your goal is to parallelize the code above (which has been copied below) using `dask.delayed`.  You may also want to visualize a bit of the computation to see if you're doing it correctly.<jupyter_code>from dask import compute
%%time

sums = []
counts = []
for fn in filenames:
    # Read in file
    df = pd.read_csv(fn)
    
    # Groupby origin airport
    by_origin = df.groupby('Origin')
    
    # Sum of all departure delays by origin
    total = by_origin.DepDelay.sum()
    
    # Number of flights by origin
    count = by_origin.DepDelay.count()
    
    # Save the intermediates
    sums.append(total)
    counts.append(count)

# Combine intermediates to get total mean-delay-per-origin
total_delays = sum(sums)
n_flights = sum(counts)
mean = total_delays / n_flights
mean
%load solutions/01-delayed-groupby.py<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.0913171884229179, 'fraction_numerical': 0.009214412285883048, 'mean_word_length': 4.242105263157895, 'pattern_counts': {'":': 0, '<': 46, '<?xml version=': 0, '>': 58, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '11', 'dir': 'jupyter-structured-clean-dedup', 'id': '48112', 'license': 'Apache-2.0', 'n_tokens_mistral': 2656, 'n_tokens_neox': 2552, 'n_words': 1054, 'path': 'a03_Dask/Collection/dask-tutorial-scipy-2018/01-dask.delayed.ipynb', 'repo_name': 'mindis/Big_Data_Analysis', 'size': '8465'}