<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Lists
Before coming into the Notebook, spend some time in an interactive session learning about sequences (strings, lists), and doing basic indexing, slicing, append(), in, etc. 
Then you can come in here...
Step2:  Functions
Definition, inputs, side-effects, returning, scope, docstrings
Step3:  Put in a file and import into a new notebook
Numpy
Before continuing, do some basic NumPy array stuff in the interpreter.
Let's make a really big 'log' from random numbers
Step4:  Note that the log has to be fairly big for the benchmarking to work properly, because otherwise the CPU caches the computation and this skews the results.
Now we can re-write our function using arrays instead of lists.
Step5:  60 times faster on my machine!
Aside
Step6:  OK, we'll make a fake example.
Step8:  However, you can't speed up our original list-based function this way.
Step9:  Plotting basics
Step10:  Not we can only plot part of biglog because it contains too many points for matplotlib (and for our screen!). If we really wanted to plot it, we'd have to find a way to upscale it.
Step11:  Objected oriented basics
The point is that we often want to store data along with relevant functions (methods) in one 'thing' — an object. 
Build this up, piece by piece. 
Start with __init__() which is required anyway. Only define self.layers.
Then add rcs(), then plot(), and finally __len__(), once you discover that len(l) doesn't work, because this object doesn't have that property.
Step12:  Web scraping basics
Step13:  Use View Source in your browser to figure out where the age range is on the page, and what it looks like.
Try to find the same string here.
Step14:  Using a regular expression
Step15:  Exercise
Step16:  Functional programming basics
In Python, functions are first class objects — you can pass them around like other objects.
Sometimes it's convenient to think in terms of map() and reduce(). This pattern is fundamental in data analytics and some packages, such as pandas. 
A couple of very common functions, sorted() and filter(), always works with a function as one of their arguments. It can be confusing the first time you see it.
With no functional argument, sorted does what you'd expect
Step17:  What if we want to sort based on the number of characters? Don't ask why, we just do. Then we write a function that returns a key which, when sorted, will give the ordering we want.
Step18:  We could rewrite that tiny function as a lambda, which is basically a little unnamed function
Step19:  When would you make a named function vs a lambda? It all depends on whether you want to use it again or not. 
map and lambda
You can think of map as 'apply this function to all of these items'.
Step20:  We can get around defining that tiny function sq() with a lambda, which you can think of as a temporary, 'throwaway' function
Step21:  In practice, we'd often write this as a list comprehension. Then we can skip the creation of the funciton or lambda entirely
Step22:  One of the advantages of map is that it is 'lazy', so if you map a function to a giant list, you don't get a giant list back, you get an iterator. A list-comp would give you a giant list, possibly jamming the memory on your box. 
reduce
reduce takes a sequence and applies some function to it recursively. You could think of it like a running sum, say, but for any function, not just summing.
Step23:  partial for making curry
'Preapplying' an argument to a function is sometimes useful. For example, we might have a general function for raising a number a to the power b, but then want another function which raises numbers to the 3rd power.
I could do this by simply calling the first function from the second
Step24:  But some people might find it more inuitive to do it this way
<ASSISTANT_TASK:>
Python Code:
layers = [0.23, 0.34, 0.45, 0.25, 0.23, 0.35]
uppers = layers[:-1]
lowers = layers[1:]
rcs = []
for pair in zip(lowers, uppers):
    rc = (pair[1] - pair[0]) / (pair[1] + pair[0])
    rcs.append(rc)
rcs
Explanation: Lists
Before coming into the Notebook, spend some time in an interactive session learning about sequences (strings, lists), and doing basic indexing, slicing, append(), in, etc. 
Then you can come in here...
End of explanation
# Exercise
def compute_rc(layers):
    
    Computes reflection coefficients given
    a list of layer impedances.
    
    uppers = layers[:-1]
    lowers = layers[1:]
    rcs = []
    for pair in zip(lowers, uppers):
        rc = (pair[1] - pair[0]) / (pair[1] + pair[0])
        rcs.append(rc)
    return rcs
compute_rc(layers)
Explanation: Functions
Definition, inputs, side-effects, returning, scope, docstrings
End of explanation
import numpy as np  # Just like importing file
biglog = np.random.random(10000000)
%timeit compute_rc(biglog)
Explanation: Put in a file and import into a new notebook
Numpy
Before continuing, do some basic NumPy array stuff in the interpreter.
Let's make a really big 'log' from random numbers:
End of explanation
# Exercise
def compute_rc_vector(layers):
    uppers = layers[:-1]
    lowers = layers[1:]
    return (lowers - uppers) / (uppers + lowers)
%timeit compute_rc_vector(biglog)
Explanation: Note that the log has to be fairly big for the benchmarking to work properly, because otherwise the CPU caches the computation and this skews the results.
Now we can re-write our function using arrays instead of lists.
End of explanation
from numba import jit
@jit
def compute_rc_numba(layers):
    uppers = layers[:-1]
    lowers = layers[1:]
    return (lowers - uppers) / (uppers + lowers)
%timeit compute_rc_numba(biglog)
Explanation: 60 times faster on my machine!
Aside: more performance with numba
End of explanation
def compute_rc_slow(layers):
    uppers = layers[:-1]
    lowers = layers[1:]
    rcs = np.zeros_like(uppers)
    for i in range(rcs.size):
        rcs[i] = (lowers[i] - uppers[i]) / (uppers[i] + lowers[i])
    return rcs
%timeit compute_rc_slow(biglog)
@jit
def compute_rc_faster(layers):
    uppers = layers[:-1]
    lowers = layers[1:]
    rcs = np.zeros_like(uppers)
    for i in range(rcs.size):
        rcs[i] = (lowers[i] - uppers[i]) / (uppers[i] + lowers[i])
    return rcs
%timeit compute_rc_faster(biglog)
Explanation: OK, we'll make a fake example.
End of explanation
@jit
def compute_rc_hopeful(layers):
    
    Computes reflection coefficients given
    a list of layer impedances.
    
    uppers = layers[:-1]
    lowers = layers[1:]
    rcs = []
    for pair in zip(lowers, uppers):
        rc = (pair[1] - pair[0]) / (pair[1] + pair[0])
        rcs.append(rc)
    return rcs
%timeit compute_rc_hopeful(biglog)
Explanation: However, you can't speed up our original list-based function this way.
End of explanation
import matplotlib.pyplot as plt
%matplotlib inline
Explanation: Plotting basics
End of explanation
plt.plot(biglog[:500])
fig = plt.figure(figsize=(15,2))
ax = fig.add_subplot(111)
ax.plot(biglog[:500])
ax.set_title("big log")
plt.show()
Explanation: Not we can only plot part of biglog because it contains too many points for matplotlib (and for our screen!). If we really wanted to plot it, we'd have to find a way to upscale it.
End of explanation
class Layers(object):
    
    def __init__(self, layers, label=None):
        # Just make sure we end up with an array
        self.layers = np.array(layers)
        self.label = label or "My log"
        self.length = self.layers.size  # But storing len in an attribute is unexpected...
        
    def __len__(self):  # ...better to do this.
        return len(self.layers)
        
    def rcs(self):
        uppers = self.layers[:-1]
        lowers = self.layers[1:]
        return (lowers-uppers) / (uppers+lowers)
    
    def plot(self, lw=0.5, color='#6699ff'):
        fig = plt.figure(figsize=(2,6))
        ax = fig.add_subplot(111)
        ax.barh(range(len(self.layers)), self.layers, color=color, lw=lw, align='edge', height=1.0, alpha=1.0, zorder=10)
        ax.grid(zorder=2)
        ax.set_ylabel('Layers')
        ax.set_title(self.label)
        ax.set_xlim([-0.5,1.0])
        ax.set_xlabel('Measurement (units)')
        ax.invert_yaxis()  
        #ax.set_xticks(ax.get_xticks()[::2])    # take out every second tick
        ax.spines['right'].set_visible(False)  # hide the spine on the right
        ax.yaxis.set_ticks_position('left')    # Only show ticks on the left and bottom spines
        
        plt.show()
l = Layers(layers, label='Well # 1')
l.rcs()
len(l)
l.plot()
rel_interval = np.cumsum(l.rcs(), dtype=float) 
len(rel_interval)
relative_layers = np.insert(rel_interval, 0, 0)
relative_layers
relative = Layers(relative_layers, "relative")
relative.layers
relative.plot()
Explanation: Objected oriented basics
The point is that we often want to store data along with relevant functions (methods) in one 'thing' — an object. 
Build this up, piece by piece. 
Start with __init__() which is required anyway. Only define self.layers.
Then add rcs(), then plot(), and finally __len__(), once you discover that len(l) doesn't work, because this object doesn't have that property.
End of explanation
url = "http://en.wikipedia.org/wiki/Jurassic"
Explanation: Web scraping basics
End of explanation
import requests
r = requests.get(url)
r.text[:500]
Explanation: Use View Source in your browser to figure out where the age range is on the page, and what it looks like.
Try to find the same string here.
End of explanation
import re
s = re.search(r'<i>(.+?million years ago)</i>', r.text)
text = s.group(1)
Explanation: Using a regular expression:
End of explanation
def get_age(period):
    url = "http://en.wikipedia.org/wiki/" + period
    r = requests.get(url)
    start, end = re.search(r'<i>([\.0-9]+)–([\.0-9]+)&#160;million years ago</i>', r.text).groups()
    return float(start), float(end)
period = "Jurassic"
get_age(period)
def duration(period):
    t0, t1 = get_age(period)
    duration = t0 - t1
    response = "According to Wikipedia, the {0} period was {1:.2f} Ma long.".format(period, duration)
    return response
duration('Cretaceous')
Explanation: Exercise: Make a function to get the start and end ages of any geologic period, taking the name of the period as an argument.
End of explanation
l = [0.001, 1, 3, 51, 41 , 601]
sorted(l)
Explanation: Functional programming basics
In Python, functions are first class objects — you can pass them around like other objects.
Sometimes it's convenient to think in terms of map() and reduce(). This pattern is fundamental in data analytics and some packages, such as pandas. 
A couple of very common functions, sorted() and filter(), always works with a function as one of their arguments. It can be confusing the first time you see it.
With no functional argument, sorted does what you'd expect:
End of explanation
def strlen(n):
    return len(str(n))
sorted(l, key=strlen)
Explanation: What if we want to sort based on the number of characters? Don't ask why, we just do. Then we write a function that returns a key which, when sorted, will give the ordering we want.
End of explanation
sorted(l, key=lambda n: len(str(n)))
Explanation: We could rewrite that tiny function as a lambda, which is basically a little unnamed function:
End of explanation
def sq(n):
    return n**2
# In Python 3, map produces an iterator, not a list.
# So we must cast to list to inspect its contents.
list(map(sq, l))
Explanation: When would you make a named function vs a lambda? It all depends on whether you want to use it again or not. 
map and lambda
You can think of map as 'apply this function to all of these items'.
End of explanation
list(map(lambda n: n**2, l))
Explanation: We can get around defining that tiny function sq() with a lambda, which you can think of as a temporary, 'throwaway' function:
End of explanation
[n**2 for n in l]
Explanation: In practice, we'd often write this as a list comprehension. Then we can skip the creation of the funciton or lambda entirely:
End of explanation
def runsum(a, b):
    return a + b
# For some reason reduce is not in the main namespace like map
from functools import reduce
reduce(runsum, l)
def runmult(a, b):
    return a * b
reduce(runmult, l)
Explanation: One of the advantages of map is that it is 'lazy', so if you map a function to a giant list, you don't get a giant list back, you get an iterator. A list-comp would give you a giant list, possibly jamming the memory on your box. 
reduce
reduce takes a sequence and applies some function to it recursively. You could think of it like a running sum, say, but for any function, not just summing.
End of explanation
def power(a, b):
    return a**b
def cuber(a):
    return power(a, 3)
cuber(2)
Explanation: partial for making curry
'Preapplying' an argument to a function is sometimes useful. For example, we might have a general function for raising a number a to the power b, but then want another function which raises numbers to the 3rd power.
I could do this by simply calling the first function from the second:
End of explanation
from functools import partial
cuber = partial(power, b=3)
cuber(2)
Explanation: But some people might find it more inuitive to do it this way:
End of explanation
<END_TASK>