<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Table of Contents
<p><div class="lev1 toc-item"><a href="#Introduction-to-NumPy" data-toc-modified-id="Introduction-to-NumPy-1"><span class="toc-item-num">1&nbsp;&nbsp;</span>Introduction to NumPy</a></div><div class="lev1 toc-item"><a href="#Creating-Arrays" data-toc-modified-id="Creating-Arrays-2"><span class="toc-item-num">2&nbsp;&nbsp;</span>Creating Arrays</a></div><div class="lev2 toc-item"><a href="#Built-in-methods-to-generate-an-array" data-toc-modified-id="Built-in-methods-to-generate-an-array-21"><span class="toc-item-num">2.1&nbsp;&nbsp;</span>Built-in methods to generate an array</a></div><div class="lev3 toc-item"><a href="#Randint" data-toc-modified-id="Randint-211"><span class="toc-item-num">2.1.1&nbsp;&nbsp;</span>Randint</a></div><div class="lev3 toc-item"><a href="#Random-Array-with-a-Uniform-Distribution" data-toc-modified-id="Random-Array-with-a-Uniform-Distribution-212"><span class="toc-item-num">2.1.2&nbsp;&nbsp;</span>Random Array with a Uniform Distribution</a></div><div class="lev3 toc-item"><a href="#Random-Array-with-Standard-Normal-Distribution" data-toc-modified-id="Random-Array-with-Standard-Normal-Distribution-213"><span class="toc-item-num">2.1.3&nbsp;&nbsp;</span>Random Array with Standard Normal Distribution</a></div><div class="lev3 toc-item"><a href="#Array-of-Zeroes" data-toc-modified-id="Array-of-Zeroes-214"><span class="toc-item-num">2.1.4&nbsp;&nbsp;</span>Array of Zeroes</a></div><div class="lev3 toc-item"><a href="#Array-of-Ones" data-toc-modified-id="Array-of-Ones-215"><span class="toc-item-num">2.1.5&nbsp;&nbsp;</span>Array of Ones</a></div><div class="lev3 toc-item"><a href="#Identity-Matrix" data-toc-modified-id="Identity-Matrix-216"><span class="toc-item-num">2.1.6&nbsp;&nbsp;</span>Identity Matrix</a></div><div class="lev3 toc-item"><a href="#Linspace" data-toc-modified-id="Linspace-217"><span class="toc-item-num">2.1.7&nbsp;&nbsp;</span>Linspace</a></div><div class="lev1 toc-item"><a href="#Quick-Operations-on-Numpy-Arrays" data-toc-modified-id="Quick-Operations-on-Numpy-Arrays-3"><span class="toc-item-num">3&nbsp;&nbsp;</span>Quick Operations on Numpy Arrays</a></div><div class="lev2 toc-item"><a href="#Selecting-Values" data-toc-modified-id="Selecting-Values-31"><span class="toc-item-num">3.1&nbsp;&nbsp;</span>Selecting Values</a></div><div class="lev1 toc-item"><a href="#Indexing-2D-Matrices" data-toc-modified-id="Indexing-2D-Matrices-4"><span class="toc-item-num">4&nbsp;&nbsp;</span>Indexing 2D Matrices</a></div><div class="lev2 toc-item"><a href="#Exercise" data-toc-modified-id="Exercise-41"><span class="toc-item-num">4.1&nbsp;&nbsp;</span>Exercise</a></div><div class="lev2 toc-item"><a href="#Exercise" data-toc-modified-id="Exercise-42"><span class="toc-item-num">4.2&nbsp;&nbsp;</span>Exercise</a></div><div class="lev2 toc-item"><a href="#Exercise" data-toc-modified-id="Exercise-43"><span class="toc-item-num">4.3&nbsp;&nbsp;</span>Exercise</a></div><div class="lev2 toc-item"><a href="#Exercise" data-toc-modified-id="Exercise-44"><span class="toc-item-num">4.4&nbsp;&nbsp;</span>Exercise</a></div><div class="lev2 toc-item"><a href="#Exercise" data-toc-modified-id="Exercise-45"><span class="toc-item-num">4.5&nbsp;&nbsp;</span>Exercise</a></div><div class="lev1 toc-item"><a href="#Fancy-Indexing" data-toc-modified-id="Fancy-Indexing-5"><span class="toc-item-num">5&nbsp;&nbsp;</span>Fancy Indexing</a></div><div class="lev2 toc-item"><a href="#Method-1
Step1:  One easy to tell the number of dimensions - look at the number of square brackets at the beginning. [[ = 2 dimensions. [[[ = 3 dimensions. <br>
Remember, dimensions are nothing more than lists inside lists inside lists...
Why use Numpy Arrays, and not just list? One reason right here.
Step2:  Numpy allows for vectorisation, i.e. operations are applied to whole arrays instead of individual elements. To get the results of a_list * b_list using traditional python, you would have had to write a for loop. When dealing with millions or billions of lines of data, that can be inefficient. We will spend some more time on operations of this nature when we get to Broadcasting.
Built-in methods to generate an array
Numpy provides us many methods to generate numbers for our array.
Step3:  We can even reshape these arrays into our desired shape. But remember, when we say desired shape, we are not speaking of circles or pentagons. Think square, reactangles, cubes and the like.
Step4:  The arange feature generates sequential series though. What if we want random numbers?<br><br>
Randint
Step5:  Translating from Python to English, "call the randint module from the random module of numpy, then select 20 numbers between 0 and 999 at random, and assign that to an array named rand_arr i.e. 0 is included, 1000 is excluded.
<img src="images/simples.gif"> <br><br>
Step6:  Remember, the first number always represents the number of rows.
Random Array with a Uniform Distribution
From the official documentation
Step7:  Random Array with Standard Normal Distribution
From the official documentation
Step8:  Array of Zeroes
Step9:  Array of Ones
Step10:  Identity Matrix
An identity matrix is a square matrix, with all the values on the diagonal equal to 1, and the remaining values equal to 0.
Step11:  Linspace
From Numpy's official documentation
Step12:  Quick Operations on Numpy Arrays
Step13:  Now imagine this is just a small snippet of a large array with millions, or even billions of numbers. Does that sound crazy? Well, Data Scientist regularly work with large arrays of numbers. The Netflix Data Scientists for example, deal with a high dimensional sparse matrix. <br><br>
For smaller datasets, let's say, number of people who boarded a particular flight every day for the past hundred days, we have a few useful tools to find the highest or lowest values, and their corresponding locations.
Step14:  Keep in mind that if we have duplicate entries, or multiple entries, only the first entry will be returned.
Step15:  Selecting Values
Secret! You already know how to select values from a numpy array.
Step16:  Remember our old friend, lists?
<img src="images/hello_friend.jpg">
And there you have it - you're already an expert in Numpy Indexing! And very soon, you will learn to be an expert at indexing 2D Matrices too.
Indexing 2D Matrices
Step17:  Remember, rows before columns. Always!<br>
How do we get entire rows, or snippets of values from rows?<br>
Exactly the same as before. Nothing to worry about here!
Step18:  Exercise
Select 26 to 30
Step19:  Exercise
Select
Step20:  Exercise
Select
Step21:  Exercise
Select
Step22:  Exercise
Create atleast 4 challenges for yourself, so you can practice indexing.
Step23:  Fancy Indexing
Method 1
Step24:  Method 2
Step25:  Take
This is similar to Fancy Indexing, but is arguably easier to use, atleast for me. I am sure you might develop a preference for this technique too. Additionally, Wes McKinney - the creator of Pandas, reports that "take" is faster than "fancy indexing".
Step26:  Works with Multi-Dimensional
Step27:  Broadcasting
Broadcasting is a way for Numpy to work with arrays of different shapes.
The easiest example to explain broadcasting would be to use a scalar value. What's a scalar? A quantity having only magnitutde, but not direction. Speed is a scalar, velocity is a vector. For our practical Numpy purposes, scalars are real numbers - 1,2,3,4.....
Broadcasting is fast and efficient because all the underlying looping occurs in C, and happens on the fly without making copies of the data.
Step28:  Here we have broadcast 10 to all other elements in the array. Remember Vectorisation? Same principles!
Step29:  Broadcasting Rule
What about arrays of different dimensions and/or sizes? Well, for that, we have the broadcasting rule.
In order to broadcast, the size of the trailing axes for both arrays in an operation must either be the same size or one of them must be one.
Umm....
<img src="images/english.jpg"> 
Ok, let's not offend our friend Samuel Jackson here, so here's what it means in plain English.
Step30:  A quick digression, in case you are wondering, the .astype('float') was just a quick operation to convert integers to floats as you are already familiar with. If you want to find out what the data type of an element in a numpy array is, simply use the suffix .dtype
Step31:  Back to our array, arr1
Step32:  Do you see what happened here? Our row with 3 elements, was sequentially added to each 3-element row in arr1.
The 1d array is represented as (3,), but think of it as simple a (3). The trailing axes have to match. So (4,3) and (3) match. What happens with it's (4,3) and (4)? It won't work! Let's prove it here.
Step33:  A final example now, with a (5,1) and (3) array. Read the rule once again - and it will be clear that the new array will be a 5X3 array.
Step34:  Other Array Operations
So let's begin with some good news here. You have already performed some advanced algebraic operations! That's the power of numpy.
Let's look at a few more operations now that come in quite handy.
Copying Arrays
Step35:  So what happened to our original array? Let's find out.
Step36:  Why did that happen?! We never touched a1, and even went on to create a whole new array!
This is because Numpy is very efficient in the way it uses memory. If you want a copy, be explicit, else Numpy will make changes to the original array too. Here's how you make a copy.
Step37:  Squaring Arrays
Step38:  Square Roots
<ASSISTANT_TASK:>
Python Code:
import numpy as np
# Create an array with the statement np.array
a = np.array([1,2,3,4])
print('a is of type:', type(a))
print('dimension of a:', a.ndim) # To find the dimension of 'a'
arr1 = np.array([1,2,3,4])
arr1.ndim
arr2 = np.array([[1,2],[2,3],[3,4],[4,5]])
arr2.ndim
# Doesn't make a difference to a computer how you represent it,
# but if humans are going to read your code, this might be useful
arr3 = np.array([[[1,2],[2,3]],
                 [[2,3],[3,4]],
                 [[4,5],[5,6]],
                 [[6,7],[7,8]]
                ])
arr3.ndim
arr4 = np.array([[ 0,  1,  2,  3,  4], 
                 [ 5,  6,  7,  8,  9], 
                 [10, 11, 12, 13, 14]])
arr4.ndim
Explanation: Table of Contents
<p><div class="lev1 toc-item"><a href="#Introduction-to-NumPy" data-toc-modified-id="Introduction-to-NumPy-1"><span class="toc-item-num">1&nbsp;&nbsp;</span>Introduction to NumPy</a></div><div class="lev1 toc-item"><a href="#Creating-Arrays" data-toc-modified-id="Creating-Arrays-2"><span class="toc-item-num">2&nbsp;&nbsp;</span>Creating Arrays</a></div><div class="lev2 toc-item"><a href="#Built-in-methods-to-generate-an-array" data-toc-modified-id="Built-in-methods-to-generate-an-array-21"><span class="toc-item-num">2.1&nbsp;&nbsp;</span>Built-in methods to generate an array</a></div><div class="lev3 toc-item"><a href="#Randint" data-toc-modified-id="Randint-211"><span class="toc-item-num">2.1.1&nbsp;&nbsp;</span>Randint</a></div><div class="lev3 toc-item"><a href="#Random-Array-with-a-Uniform-Distribution" data-toc-modified-id="Random-Array-with-a-Uniform-Distribution-212"><span class="toc-item-num">2.1.2&nbsp;&nbsp;</span>Random Array with a Uniform Distribution</a></div><div class="lev3 toc-item"><a href="#Random-Array-with-Standard-Normal-Distribution" data-toc-modified-id="Random-Array-with-Standard-Normal-Distribution-213"><span class="toc-item-num">2.1.3&nbsp;&nbsp;</span>Random Array with Standard Normal Distribution</a></div><div class="lev3 toc-item"><a href="#Array-of-Zeroes" data-toc-modified-id="Array-of-Zeroes-214"><span class="toc-item-num">2.1.4&nbsp;&nbsp;</span>Array of Zeroes</a></div><div class="lev3 toc-item"><a href="#Array-of-Ones" data-toc-modified-id="Array-of-Ones-215"><span class="toc-item-num">2.1.5&nbsp;&nbsp;</span>Array of Ones</a></div><div class="lev3 toc-item"><a href="#Identity-Matrix" data-toc-modified-id="Identity-Matrix-216"><span class="toc-item-num">2.1.6&nbsp;&nbsp;</span>Identity Matrix</a></div><div class="lev3 toc-item"><a href="#Linspace" data-toc-modified-id="Linspace-217"><span class="toc-item-num">2.1.7&nbsp;&nbsp;</span>Linspace</a></div><div class="lev1 toc-item"><a href="#Quick-Operations-on-Numpy-Arrays" data-toc-modified-id="Quick-Operations-on-Numpy-Arrays-3"><span class="toc-item-num">3&nbsp;&nbsp;</span>Quick Operations on Numpy Arrays</a></div><div class="lev2 toc-item"><a href="#Selecting-Values" data-toc-modified-id="Selecting-Values-31"><span class="toc-item-num">3.1&nbsp;&nbsp;</span>Selecting Values</a></div><div class="lev1 toc-item"><a href="#Indexing-2D-Matrices" data-toc-modified-id="Indexing-2D-Matrices-4"><span class="toc-item-num">4&nbsp;&nbsp;</span>Indexing 2D Matrices</a></div><div class="lev2 toc-item"><a href="#Exercise" data-toc-modified-id="Exercise-41"><span class="toc-item-num">4.1&nbsp;&nbsp;</span>Exercise</a></div><div class="lev2 toc-item"><a href="#Exercise" data-toc-modified-id="Exercise-42"><span class="toc-item-num">4.2&nbsp;&nbsp;</span>Exercise</a></div><div class="lev2 toc-item"><a href="#Exercise" data-toc-modified-id="Exercise-43"><span class="toc-item-num">4.3&nbsp;&nbsp;</span>Exercise</a></div><div class="lev2 toc-item"><a href="#Exercise" data-toc-modified-id="Exercise-44"><span class="toc-item-num">4.4&nbsp;&nbsp;</span>Exercise</a></div><div class="lev2 toc-item"><a href="#Exercise" data-toc-modified-id="Exercise-45"><span class="toc-item-num">4.5&nbsp;&nbsp;</span>Exercise</a></div><div class="lev1 toc-item"><a href="#Fancy-Indexing" data-toc-modified-id="Fancy-Indexing-5"><span class="toc-item-num">5&nbsp;&nbsp;</span>Fancy Indexing</a></div><div class="lev2 toc-item"><a href="#Method-1:-Boolean-Masks" data-toc-modified-id="Method-1:-Boolean-Masks-51"><span class="toc-item-num">5.1&nbsp;&nbsp;</span>Method 1: Boolean Masks</a></div><div class="lev2 toc-item"><a href="#Method-2:-Indexing-with-Array-of-Integers" data-toc-modified-id="Method-2:-Indexing-with-Array-of-Integers-52"><span class="toc-item-num">5.2&nbsp;&nbsp;</span>Method 2: Indexing with Array of Integers</a></div><div class="lev2 toc-item"><a href="#Take" data-toc-modified-id="Take-53"><span class="toc-item-num">5.3&nbsp;&nbsp;</span>Take</a></div><div class="lev2 toc-item"><a href="#Broadcasting" data-toc-modified-id="Broadcasting-54"><span class="toc-item-num">5.4&nbsp;&nbsp;</span>Broadcasting</a></div><div class="lev3 toc-item"><a href="#Broadcasting-Rule" data-toc-modified-id="Broadcasting-Rule-541"><span class="toc-item-num">5.4.1&nbsp;&nbsp;</span>Broadcasting Rule</a></div><div class="lev1 toc-item"><a href="#Other-Array-Operations" data-toc-modified-id="Other-Array-Operations-6"><span class="toc-item-num">6&nbsp;&nbsp;</span>Other Array Operations</a></div><div class="lev3 toc-item"><a href="#Copying-Arrays" data-toc-modified-id="Copying-Arrays-601"><span class="toc-item-num">6.0.1&nbsp;&nbsp;</span>Copying Arrays</a></div><div class="lev3 toc-item"><a href="#Squaring-Arrays" data-toc-modified-id="Squaring-Arrays-602"><span class="toc-item-num">6.0.2&nbsp;&nbsp;</span>Squaring Arrays</a></div><div class="lev3 toc-item"><a href="#Square-Roots" data-toc-modified-id="Square-Roots-603"><span class="toc-item-num">6.0.3&nbsp;&nbsp;</span>Square Roots</a></div>
# Introduction to NumPy
From NumPy's [website](http://numpy.org) we have the following description:<br>
>NumPy is the fundamental package for scientific computing with Python. It contains among other things:
>* a powerful N-dimensional array object
* sophisticated (broadcasting) functions
* tools for integrating C/C++ and Fortran code
* useful linear algebra, Fourier transform, and random number capabilities <br>
>Besides its obvious scientific uses, NumPy can also be used as an efficient multi-dimensional container of generic data. Arbitrary data-types can be defined. This allows NumPy to seamlessly and speedily integrate with a wide variety of databases.
You can think of Numpy as standard Python lists on steroids! 
<img src="images/roids.jpg"> <br><br>
There are a few reasons why **Numpy is so much faster than lists**:
* Numpy's underlying code is written in C
* The contents of Numpy's arrays are homogenous, i.e., all of of the same `type`
* Numpy's arrays have a smaller memory footprint
And since a Data Scientist is always learning, here's an excellent resource on Arrays - [scipy array tip sheet](http://pages.physics.cornell.edu/~myers/teaching/ComputationalMethods/python/arrays.html)
# Creating Arrays
Arrays contain uniform data type, with an arbitrary number of dimensions. What's a dimension? It's just a big word to denote how many levels deep the array goes. <br>
Dimensions are nothing more than lists inside lists inside lists...<br>
<img src="images/inception.jpg">
As we saw earlier with Matplotlib, there are some conventions for importing Numpy too.
End of explanation
a_list = [1,2,3,4,5]
b_list = [5,10,15,20,25]
# Multiplying these will give an error
print(a_list * b_list)
a_list = np.array([1,2,3,4,5])
b_list = np.array([5,10,15,20,25])
print(a_list * b_list)
Explanation: One easy to tell the number of dimensions - look at the number of square brackets at the beginning. [[ = 2 dimensions. [[[ = 3 dimensions. <br>
Remember, dimensions are nothing more than lists inside lists inside lists...
Why use Numpy Arrays, and not just list? One reason right here.
End of explanation
arr1 = np.arange(16)
print(arr1)
Explanation: Numpy allows for vectorisation, i.e. operations are applied to whole arrays instead of individual elements. To get the results of a_list * b_list using traditional python, you would have had to write a for loop. When dealing with millions or billions of lines of data, that can be inefficient. We will spend some more time on operations of this nature when we get to Broadcasting.
Built-in methods to generate an array
Numpy provides us many methods to generate numbers for our array.
End of explanation
arr1.reshape(4,4)
arr1.reshape(2,8)
arr1.reshape(8,2)
arr1.reshape(16,1)
Explanation: We can even reshape these arrays into our desired shape. But remember, when we say desired shape, we are not speaking of circles or pentagons. Think square, reactangles, cubes and the like.
End of explanation
np.random.seed(42) 
rand_arr = np.random.randint(0,1000,20)
print(rand_arr)
Explanation: The arange feature generates sequential series though. What if we want random numbers?<br><br>
Randint
End of explanation
rand_arr.reshape(5,4)
rand_arr.reshape(4,5)
rand_arr.reshape(2,10)
Explanation: Translating from Python to English, "call the randint module from the random module of numpy, then select 20 numbers between 0 and 999 at random, and assign that to an array named rand_arr i.e. 0 is included, 1000 is excluded.
<img src="images/simples.gif"> <br><br>
End of explanation
np.random.seed(42) 
np.random.rand(5)
np.random.seed(42) 
np.random.rand(3,2)
Explanation: Remember, the first number always represents the number of rows.
Random Array with a Uniform Distribution
From the official documentation:
Create an array of the given shape and populate it with random samples from a uniform distribution over [0, 1).
A uniform distribution, is a distribution that has constant probability.
End of explanation
np.random.seed(42) 
np.random.randn(5)
Explanation: Random Array with Standard Normal Distribution
From the official documentation:
Return a sample (or samples) from the “standard normal” distribution.
For random samples from $$N(\mu, \sigma^2)$$ use:
sigma * np.random.randn(...) + mu
Don't get scared by the formula - it's actually very simple, and we will cover this in brief later on in the mathematics section.
End of explanation
np.zeros(16)
np.zeros((4,4))
Explanation: Array of Zeroes
End of explanation
np.ones(5)
np.ones((4,4))
Explanation: Array of Ones
End of explanation
np.eye(10)
Explanation: Identity Matrix
An identity matrix is a square matrix, with all the values on the diagonal equal to 1, and the remaining values equal to 0.
End of explanation
# 5 evenly spaced numbers between -5 and 5
np.linspace(-5,5,5)
Explanation: Linspace
From Numpy's official documentation:
Return evenly spaced numbers over a specified interval.
Returns num evenly spaced samples, calculated over the interval [start, stop].
The endpoint of the interval can optionally be excluded.
Here's an interesting discussion on SO about when to use Linspace v range - https://stackoverflow.com/questions/5779270/linspace-vs-range
End of explanation
import numpy as np
np.random.seed(42) 
arr1 = np.random.randint(1,1000,100)
arr1 = arr1.reshape(10,10)
arr1.shape
arr1
Explanation: Quick Operations on Numpy Arrays
End of explanation
# Find the highest value in arr1
arr1.max()
# Find the lowest value in arr1
arr1.min()
# Find the location of the highest value in arr1
arr1.argmax()
Explanation: Now imagine this is just a small snippet of a large array with millions, or even billions of numbers. Does that sound crazy? Well, Data Scientist regularly work with large arrays of numbers. The Netflix Data Scientists for example, deal with a high dimensional sparse matrix. <br><br>
For smaller datasets, let's say, number of people who boarded a particular flight every day for the past hundred days, we have a few useful tools to find the highest or lowest values, and their corresponding locations.
End of explanation
arr1.argmin()
# From earlier
rand_arr = np.random.randint(0,1000,20)
rand_arr
rand_arr = rand_arr.reshape(4,5)
rand_arr.shape
rand_arr
Explanation: Keep in mind that if we have duplicate entries, or multiple entries, only the first entry will be returned.
End of explanation
import numpy as np
np.random.seed(42)
arr1 = np.arange(1,6)
arr1
arr1[0]
arr1[0:3]
arr1[-1]
Explanation: Selecting Values
Secret! You already know how to select values from a numpy array.
End of explanation
import numpy as np
np.random.seed(42)
rand_arr = np.random.randint(0,1000,20)
print(rand_arr)
rand_arr = rand_arr.reshape(5,4)
rand_arr
rand_arr[0]
rand_arr[1]
rand_arr[0][-1]
# Another way to write the same thing
rand_arr[0,-1]
Explanation: Remember our old friend, lists?
<img src="images/hello_friend.jpg">
And there you have it - you're already an expert in Numpy Indexing! And very soon, you will learn to be an expert at indexing 2D Matrices too.
Indexing 2D Matrices
End of explanation
import numpy as np
np.random.seed(42)
arr1 = np.arange(1,101)
arr1
arr1 = arr1.reshape(10,10)
arr1
Explanation: Remember, rows before columns. Always!<br>
How do we get entire rows, or snippets of values from rows?<br>
Exactly the same as before. Nothing to worry about here!
End of explanation
# Step 1 - Narrow down the row
arr1[2] # 3rd row
# 26 is at index 5, we need all the numbers from thr 6th column onwards
arr1[2,5:]
Explanation: Exercise
Select 26 to 30
End of explanation
# Step 1: Identify the Row
arr1[7:]
# Now we need the first three columns
arr1[7:,:3]
Explanation: Exercise
Select:<br>
[71,  72,  73]<br>
[81,  82,  83]<br>
[91,  92,  93]
End of explanation
# Your code here
Explanation: Exercise
Select: <br>
[56,  57,  58,  59,  60]<br>
[66,  67,  68,  69,  70]<br>
[76,  77,  78,  79,  80]
End of explanation
# Your code here
Explanation: Exercise
Select:<br>
[ 44,  45]<br>
[ 54,  55]
End of explanation
# Your code here
Explanation: Exercise
Create atleast 4 challenges for yourself, so you can practice indexing.
End of explanation
import numpy as np
np.random.seed(42)
arr1 = np.random.randint(0,1000,100)
arr1
# We check what values are greater than 150
arr1>150
# Assign this operation to a variable x
mask = arr1>150
# Create a new array which subsets arr1 based on a boolean operation
arr2 = arr1[mask]
arr2
# Check the shape
arr2.shape
Explanation: Fancy Indexing
Method 1: Boolean Masks
While there are many ways to index, one of the more common methods that Data Scientists use will is Boolean Indexing. You can read more about indexing methods here.
End of explanation
list1 = [1,3,5,7]
list2 = [2,4,6,8]
arr1 = np.arange(1,101)
arr1
arr_even = arr1[list1]
arr_even
# Alternatively
arr_even = arr1[[1,3,5,7]]
arr_even
arr_odd = arr1[list2]
arr_odd
Explanation: Method 2: Indexing with Array of Integers
Don't get intimidated by the big words - it just means indexing by using a Python list.
End of explanation
arr1 = np.arange(1,101)
arr1
indices = [0,2,4,10,20,80,91,97,99]
np.take(arr1, indices)
Explanation: Take
This is similar to Fancy Indexing, but is arguably easier to use, atleast for me. I am sure you might develop a preference for this technique too. Additionally, Wes McKinney - the creator of Pandas, reports that "take" is faster than "fancy indexing".
End of explanation
np.take(arr1, [[0, 1], [11, 18]])
Explanation: Works with Multi-Dimensional
End of explanation
arr_1 = np.arange(1,11)
print(arr_1)
print(arr_1 * 10)
Explanation: Broadcasting
Broadcasting is a way for Numpy to work with arrays of different shapes.
The easiest example to explain broadcasting would be to use a scalar value. What's a scalar? A quantity having only magnitutde, but not direction. Speed is a scalar, velocity is a vector. For our practical Numpy purposes, scalars are real numbers - 1,2,3,4.....
Broadcasting is fast and efficient because all the underlying looping occurs in C, and happens on the fly without making copies of the data.
End of explanation
arr_1 = np.array([[1,2],[3,4]])
a = 2
arr_1 + a
Explanation: Here we have broadcast 10 to all other elements in the array. Remember Vectorisation? Same principles!
End of explanation
arr1 = np.arange(1,13)
arr1
arr1.shape
arr1 = arr1.reshape(4,3).astype('float')
arr1
Explanation: Broadcasting Rule
What about arrays of different dimensions and/or sizes? Well, for that, we have the broadcasting rule.
In order to broadcast, the size of the trailing axes for both arrays in an operation must either be the same size or one of them must be one.
Umm....
<img src="images/english.jpg"> 
Ok, let's not offend our friend Samuel Jackson here, so here's what it means in plain English.
End of explanation
arr1.dtype
arr_example = np.array([1,2,3,4])
print(arr_example)
print('arr_example is an',arr_example.dtype)
arr_example = arr_example.astype('float')
print('arr_example is now a',arr_example.dtype)
Explanation: A quick digression, in case you are wondering, the .astype('float') was just a quick operation to convert integers to floats as you are already familiar with. If you want to find out what the data type of an element in a numpy array is, simply use the suffix .dtype
End of explanation
arr1
arr1.shape
arr2 = np.array([0.0,1.0,2.0])
print(arr2)
print(arr2.shape)
arr1 + arr2
Explanation: Back to our array, arr1
End of explanation
arr3 = np.arange(0,4)
arr3 = arr3.astype('float')
print(arr3)
print(arr3.shape)
# Let's generate our error
arr1 + arr3
Explanation: Do you see what happened here? Our row with 3 elements, was sequentially added to each 3-element row in arr1.
The 1d array is represented as (3,), but think of it as simple a (3). The trailing axes have to match. So (4,3) and (3) match. What happens with it's (4,3) and (4)? It won't work! Let's prove it here.
End of explanation
arr4 = np.arange(1,6)
arr4
arr4 = arr4.reshape(5,1).astype('float')
arr4.shape
arr2
arr4 * arr2
Explanation: A final example now, with a (5,1) and (3) array. Read the rule once again - and it will be clear that the new array will be a 5X3 array.
End of explanation
a1 = np.arange(1,21)
a1 = a1.reshape(4,5)
a1
# Let's get the first column
a1[:,0]
# Assign to new array
new_a1 = a1[:,0]
new_a1
# Recall that this is how you select all values
new_a1[:] = 42
new_a1
Explanation: Other Array Operations
So let's begin with some good news here. You have already performed some advanced algebraic operations! That's the power of numpy.
Let's look at a few more operations now that come in quite handy.
Copying Arrays
End of explanation
a1
Explanation: So what happened to our original array? Let's find out.
End of explanation
a1_copy = a1.copy()
a1_copy
a1_copy = np.arange(1,21)
a1_copy = a1_copy.reshape(4,5)
a1_copy
a1
Explanation: Why did that happen?! We never touched a1, and even went on to create a whole new array!
This is because Numpy is very efficient in the way it uses memory. If you want a copy, be explicit, else Numpy will make changes to the original array too. Here's how you make a copy.
End of explanation
np.square(a1)
Explanation: Squaring Arrays
End of explanation
np.sqrt(a1)
Explanation: Square Roots
End of explanation
<END_TASK>