<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Content under Creative Commons Attribution license CC-BY 4.0, code under BSD 3-Clause License © 2017 L.A. Barba, N.C. Clementi
Step1:  Play with NumPy Arrays
Welcome to Lesson 4 of the first course module in "Engineering Computations." You have come a long way! 
Remember, this course assumes no coding experience, so the first three lessons were focused on creating a foundation with Python programming constructs using essentially no mathematics. The previous lessons are
Step2:  Creating arrays
To create a NumPy array from an existing list of (homogeneous) numbers, we call numpy.array(), like this
Step3:  NumPy offers many ways to create arrays in addition to this. We already mentioned some of them above. 
Play with numpy.ones() and numpy.zeros()
Step4:  Another useful one
Step5:  numpy.linspace() is similar to numpy.arange(), but uses number of samples instead of a step size. It returns an array with evenly spaced numbers over the specified interval.  
Syntax
Step6:  Array operations
Let's assign some arrays to variable names and perform some operations with them.
Step7:  Now that we've saved it with a variable name, we can do some computations with the array. E.g., take the square of every element of the array, in one go
Step8:  We can also take the square root of a positive array, using the numpy.sqrt() function
Step9:  Now that we have different arrays x_array, y_array and z_array, we can do more computations, like add or multiply them. For example
Step10:  Array addition is defined element-wise, like when adding two vectors (or matrices). Array multiplication is also element-wise
Step11:  We can also divide arrays, but you have to be careful not to divide by zero. This operation will result in a nan which stands for Not a Number. Python will still perform the division, but will tell us about the problem.  
Let's see how this might look
Step12:  Multidimensional arrays
2D arrays
NumPy can create arrays of N dimensions.  For example, a 2D array is like a matrix, and is created from a nested list as follows
Step13:  2D arrays can be added, subtracted, and multiplied
Step14:  The addition of these two matrices works exactly as you would expect
Step15:  What if we try to multiply arrays using the '*'operator?
Step16:  The multiplication using the '*' operator is element-wise. If we want to do matrix multiplication we use the '@' operator
Step17:  Or equivalently we can use numpy.dot()
Step18:  3D arrays
Let's create a 3D array by reshaping a 1D array. We can use numpy.reshape(), where we pass the array we want to reshape and the shape we want to give it, i.e., the number of elements in each dimension. 
Syntax
numpy.reshape(array, newshape)
For example
Step19:  We can check for the shape of a NumPy array using the function numpy.shape()
Step20:  Visualizing the dimensions of the a_3D array can be tricky, so here is a diagram that will help you to understand how the dimensions are assigned
Step21:  Exercises
Step22:  When we don't specify the start and/or end point in the slicing, the symbol '
Step23:  Exercises
Step24:  If we want to grab the first column of both matrices in our a_3D array, we do
Step25:  The line above is telling NumPy that we want
Step26:  Below, from the first matrix in our a_3D array, we will grab the two middle elements (5,6)
Step27:  Exercises
Step28:  We need to write a for statement, appending the result of the element-wise sum into a new list we call result_lst. 
For timing, we can use the IPython "magic" %%time. Writing at the beginning of the code cell the command %%time will give us the time it takes to execute all the code in that cell.
Step29:  Element-wise sum of NumPy arrays
In this case, we generate arrays with random integers using the NumPy function numpy.random.randint(). The arrays we generate with this function are not going to be like the lists
Step30:  Now we can use the %%time cell magic, again, to see how long it takes NumPy to compute the element-wise sum.
Step31:  Notice that in the case of arrays, the code not only is more readable (just one line of code), but it is also faster than with lists. This time advantage will be larger with bigger arrays/lists. 
(Your timing results may vary to the ones we show in this notebook, because you will be computing in a different machine.)
Exercise
Try the comparison between lists and arrays, using bigger arrays; for example, of size 10,000. 
Repeat the analysis, but now computing the operation that raises each element of an array/list to the power two. Use arrays of 10,000 elements. 
Time to Plot
You will love the Python library Matplotlib! You'll learn here about its module pyplot, which makes line plots. 
We need some data to plot. Let's define a NumPy array, compute derived data using its square, cube and square root (element-wise), and plot these values with the original array in the x-axis.
Step32:  To plot the resulting arrays as a function of the orginal one (xarray) in the x-axis, we need to import the module pyplot from Matplotlib.
Step33:  The command %matplotlib inline is there to get our plots inside the notebook (instead of a pop-up window, which is the default behavior of pyplot). 
We'll use the pyplot.plot() function, specifying the line color ('k' for black) and line style ('-', '--' and '
Step34:  To illustrate other features, we will plot the same data, but varying the colors instead of the line style. We'll also use LaTeX syntax to write formulas in the labels. If you want to know more about LaTeX syntax, there is a quick guide to LaTeX available online.
Adding a semicolon (';') to the last line in the plotting code block prevents that ugly output, like &lt;matplotlib.legend.Legend at 0x7f8c83cc7898&gt;. Try it.
<ASSISTANT_TASK:>
Python Code:
# Execute this cell to load the notebook's style sheet, then ignore it
from IPython.core.display import HTML
css_file = '../style/custom.css'
HTML(open(css_file, "r").read())
Explanation: Content under Creative Commons Attribution license CC-BY 4.0, code under BSD 3-Clause License © 2017 L.A. Barba, N.C. Clementi
End of explanation
import numpy
Explanation: Play with NumPy Arrays
Welcome to Lesson 4 of the first course module in "Engineering Computations." You have come a long way! 
Remember, this course assumes no coding experience, so the first three lessons were focused on creating a foundation with Python programming constructs using essentially no mathematics. The previous lessons are:
Lesson 1: Interacting with Python
Lesson 2: Play with data in Jupyter
Lesson 3: Strings and lists in action
In engineering applications, most computing situations benefit from using arrays: they are sequences of data all of the same type. They behave a lot like lists, except for the constraint in the type of their elements. There is a huge efficiency advantage when you know that all elements of a sequence are of the same type—so equivalent methods for arrays execute a lot faster than those for lists.
The Python language is expanded for special applications, like scientific computing, with libraries. The most important library in science and engineering is NumPy, providing the n-dimensional array data structure (a.k.a, ndarray) and a wealth of functions, operations and algorithms for efficient linear-algebra computations.
In this lesson, you'll start playing with NumPy arrays and discover their power. You'll also meet another widely loved library: Matplotlib, for creating two-dimensional plots of data.
Importing libraries
First, a word on importing libraries to expand your running Python session. Because libraries are large collections of code and are for special purposes, they are not loaded automatically when you launch Python (or IPython, or Jupyter). You have to import a library using the import command. For example, to import NumPy, with all its linear-algebra goodness, we enter:
python
import numpy
Once you execute that command in a code cell, you can call any NumPy function using the dot notation, prepending the library name. For example, some commonly used functions are:
numpy.linspace()
numpy.ones()
numpy.zeros()
numpy.empty()
numpy.copy()
Follow the links to explore the documentation for these very useful NumPy functions!
Warning:
You will find a lot of sample code online that uses a different syntax for importing. They will do:
python
import numpy as np
All this does is create an alias for numpy with the shorter string np, so you then would call a NumPy function like this: np.linspace(). This is just an alternative way of doing it, for lazy people that find it too long to type numpy and want to save 3 characters each time. For the not-lazy, typing numpy is more readable and beautiful. 
We like it better like this:
End of explanation
numpy.array([3, 5, 8, 17])
Explanation: Creating arrays
To create a NumPy array from an existing list of (homogeneous) numbers, we call numpy.array(), like this:
End of explanation
numpy.ones(5)
numpy.zeros(3)
Explanation: NumPy offers many ways to create arrays in addition to this. We already mentioned some of them above. 
Play with numpy.ones() and numpy.zeros(): they create arrays full of ones and zeros, respectively. We pass as an argument the number of array elements we want.
End of explanation
numpy.arange(4)
numpy.arange(2, 6)
numpy.arange(2, 6, 2)
numpy.arange(2, 6, 0.5)
Explanation: Another useful one: numpy.arange() gives an array of evenly spaced values in a defined interval. 
Syntax:
numpy.arange(start, stop, step)
where start by default is zero, stop is not inclusive, and the default
for step is one.  Play with it!
End of explanation
numpy.linspace(2.0, 3.0)
len(numpy.linspace(2.0, 3.0))
numpy.linspace(2.0, 3.0, 6)
numpy.linspace(-1, 1, 9)
Explanation: numpy.linspace() is similar to numpy.arange(), but uses number of samples instead of a step size. It returns an array with evenly spaced numbers over the specified interval.  
Syntax:
numpy.linspace(start, stop, num)
stop is included by default (it can be removed, read the docs), and num by default is 50.
End of explanation
x_array = numpy.linspace(-1, 1, 9)
Explanation: Array operations
Let's assign some arrays to variable names and perform some operations with them.
End of explanation
y_array = x_array**2
print(y_array)
Explanation: Now that we've saved it with a variable name, we can do some computations with the array. E.g., take the square of every element of the array, in one go:
End of explanation
z_array = numpy.sqrt(y_array)
print(z_array)
Explanation: We can also take the square root of a positive array, using the numpy.sqrt() function:
End of explanation
add_array = x_array + y_array 
print(add_array)
Explanation: Now that we have different arrays x_array, y_array and z_array, we can do more computations, like add or multiply them. For example:
End of explanation
mult_array = x_array * z_array
print(mult_array)
Explanation: Array addition is defined element-wise, like when adding two vectors (or matrices). Array multiplication is also element-wise:
End of explanation
x_array / y_array
Explanation: We can also divide arrays, but you have to be careful not to divide by zero. This operation will result in a nan which stands for Not a Number. Python will still perform the division, but will tell us about the problem.  
Let's see how this might look:
End of explanation
array_2d = numpy.array([[1, 2], [3, 4]])
print(array_2d)
Explanation: Multidimensional arrays
2D arrays
NumPy can create arrays of N dimensions.  For example, a 2D array is like a matrix, and is created from a nested list as follows:
End of explanation
X = numpy.array([[1, 2], [3, 4]])
Y = numpy.array([[1, -1], [0, 1]])
Explanation: 2D arrays can be added, subtracted, and multiplied:
End of explanation
X + Y
Explanation: The addition of these two matrices works exactly as you would expect:
End of explanation
X * Y
Explanation: What if we try to multiply arrays using the '*'operator?
End of explanation
X @ Y
Explanation: The multiplication using the '*' operator is element-wise. If we want to do matrix multiplication we use the '@' operator:
End of explanation
numpy.dot(X, Y)
Explanation: Or equivalently we can use numpy.dot():
End of explanation
a = numpy.arange(24)
a_3D = numpy.reshape(a, (2, 3, 4))
print(a_3D)
Explanation: 3D arrays
Let's create a 3D array by reshaping a 1D array. We can use numpy.reshape(), where we pass the array we want to reshape and the shape we want to give it, i.e., the number of elements in each dimension. 
Syntax
numpy.reshape(array, newshape)
For example:
End of explanation
numpy.shape(a_3D)
Explanation: We can check for the shape of a NumPy array using the function numpy.shape():
End of explanation
X
# Grab the element in the 1st row and 1st column 
X[0, 0]
# Grab the element in the 1st row and 2nd column 
X[0, 1]
Explanation: Visualizing the dimensions of the a_3D array can be tricky, so here is a diagram that will help you to understand how the dimensions are assigned: each dimension is shown as  a coordinate axis. For a 3D array, on the "x axis", we have the sub-arrays that themselves are two-dimensional (matrices). We have two of these 2D sub-arrays, in this case; each one has 3 rows and 4 columns. Study this sketch carefully, while comparing with how the array a_3D is printed out above. 
<img src="images/3d_array_sketch.png" style="width: 400px;"/> 
When we have multidimensional arrays, we can access slices of their elements by slicing on each dimension. This is one of the advantages of using arrays: we cannot do this with lists. 
Let's access some elements of our 2D array called X.
End of explanation
# Grab the 1st column
X[:, 0]
Explanation: Exercises:
From the X array:
Grab the 2nd element in the 1st column.
Grab the 2nd element in the 2nd column.
Play with slicing on this array:
End of explanation
# Grab the 1st row
X[0, :]
Explanation: When we don't specify the start and/or end point in the slicing, the symbol ':' means "all". In the example above, we are telling NumPy that we want all the elements from the 0-th index in the second dimension (the first column).
End of explanation
a_3D
Explanation: Exercises:
From the X array:
Grab the 2nd column.
Grab the 2nd row.
Let's practice with a 3D array.
End of explanation
a_3D[:, :, 0]
Explanation: If we want to grab the first column of both matrices in our a_3D array, we do:
End of explanation
a_3D[:, 0:2, 0]
Explanation: The line above is telling NumPy that we want:
first ':' : from the first dimension, grab all the elements (2 matrices).
second ':': from the second dimension, grab all the elements (all the rows).
'0'       : from the third dimension, grab the first element (first column).
If we want the first 2 elements of the first column of both matrices:
End of explanation
a_3D[0, 1, 1:3]
Explanation: Below, from the first matrix in our a_3D array, we will grab the two middle elements (5,6):
End of explanation
#import random library
import random
lst_1 = random.sample(range(100), 100)
lst_2 = random.sample(range(100), 100)
#print first 10 elements
print(lst_1[0:10])
print(lst_2[0:10])
Explanation: Exercises:
From the array named a_3D: 
Grab the two middle elements (17, 18) from the second matrix.
Grab the last row from both matrices.
Grab the elements of the 1st matrix that exclude the first row and the first column. 
Grab the elements of the 2nd matrix that exclude the last row and the last column. 
NumPy == Fast and Clean!
When we are working with numbers, arrays are a better option because the NumPy library has built-in functions that are optimized, and therefore faster than vanilla Python. Especially if we have big arrays. Besides, using NumPy arrays and exploiting their properties makes our code more readable.
For example, if we wanted to add element-wise the elements of 2 lists, we need to do it with a for statement. If we want to add two NumPy arrays, we just use the addtion '+' symbol!
Below, we will add two lists and two arrays (with random elements) and we'll compare the time it takes to compute each addition.
Element-wise sum of a Python list
Using the Python library random, we will generate two lists with 100 pseudo-random elements in the range [0,100), with no numbers repeated.
End of explanation
%%time
res_lst = []
for i in range(100):
    res_lst.append(lst_1[i] + lst_2[i])
print(res_lst[0:10])
Explanation: We need to write a for statement, appending the result of the element-wise sum into a new list we call result_lst. 
For timing, we can use the IPython "magic" %%time. Writing at the beginning of the code cell the command %%time will give us the time it takes to execute all the code in that cell.
End of explanation
arr_1 = numpy.random.randint(0, 100, size=100)
arr_2 = numpy.random.randint(0, 100, size=100)
#print first 10 elements
print(arr_1[0:10])
print(arr_2[0:10])
Explanation: Element-wise sum of NumPy arrays
In this case, we generate arrays with random integers using the NumPy function numpy.random.randint(). The arrays we generate with this function are not going to be like the lists: in this case we'll have 100 elements in the range [0, 100) but they can repeat. Our goal is to compare the time it takes to compute addition of a list or an array of numbers, so all that matters is that the arrays and the lists are of the same length and type (integers).
End of explanation
%%time
arr_res = arr_1 + arr_2
Explanation: Now we can use the %%time cell magic, again, to see how long it takes NumPy to compute the element-wise sum.
End of explanation
xarray = numpy.linspace(0, 2, 41)
print(xarray)
pow2 = xarray**2
pow3 = xarray**3
pow_half = numpy.sqrt(xarray)
Explanation: Notice that in the case of arrays, the code not only is more readable (just one line of code), but it is also faster than with lists. This time advantage will be larger with bigger arrays/lists. 
(Your timing results may vary to the ones we show in this notebook, because you will be computing in a different machine.)
Exercise
Try the comparison between lists and arrays, using bigger arrays; for example, of size 10,000. 
Repeat the analysis, but now computing the operation that raises each element of an array/list to the power two. Use arrays of 10,000 elements. 
Time to Plot
You will love the Python library Matplotlib! You'll learn here about its module pyplot, which makes line plots. 
We need some data to plot. Let's define a NumPy array, compute derived data using its square, cube and square root (element-wise), and plot these values with the original array in the x-axis.
End of explanation
from matplotlib import pyplot
%matplotlib inline
Explanation: To plot the resulting arrays as a function of the orginal one (xarray) in the x-axis, we need to import the module pyplot from Matplotlib.
End of explanation
#Plot x^2
pyplot.plot(xarray, pow2, color='k', linestyle='-', label='square')
#Plot x^3
pyplot.plot(xarray, pow3, color='k', linestyle='--', label='cube')
#Plot sqrt(x)
pyplot.plot(xarray, pow_half, color='k', linestyle=':', label='square root')
#Plot the legends in the best location
pyplot.legend(loc='best')
Explanation: The command %matplotlib inline is there to get our plots inside the notebook (instead of a pop-up window, which is the default behavior of pyplot). 
We'll use the pyplot.plot() function, specifying the line color ('k' for black) and line style ('-', '--' and ':' for continuous, dashed and dotted line), and giving each line a label. Note that the values for color, linestyle and label are given in quotes.
End of explanation
#Plot x^2
pyplot.plot(xarray, pow2, color='red', linestyle='-', label='$x^2$')
#Plot x^3
pyplot.plot(xarray, pow3, color='green', linestyle='-', label='$x^3$')
#Plot sqrt(x)
pyplot.plot(xarray, pow_half, color='blue', linestyle='-', label='$\sqrt{x}$')
#Plot the legends in the best location
pyplot.legend(loc='best'); 
Explanation: To illustrate other features, we will plot the same data, but varying the colors instead of the line style. We'll also use LaTeX syntax to write formulas in the labels. If you want to know more about LaTeX syntax, there is a quick guide to LaTeX available online.
Adding a semicolon (';') to the last line in the plotting code block prevents that ugly output, like &lt;matplotlib.legend.Legend at 0x7f8c83cc7898&gt;. Try it.
End of explanation
<END_TASK>