Python has emerged as the primary programming language used in Deep Learning. The syntax is similar to MATLAB, but it has been designed with S/W developers in mind, whereas MATLAB was designed to translate mathematical formulae.

In its simplest form, a Python program is simply a text file containing Python statements After you create the file, you must tell Python to execute that file. This is done through a terminal shell, an integrated development environment (IDE), or HTML notebooks that can be run from a browser.

Python is a scripting language. This means that the command in your file have to be compiled into byte code which is then routed to a virtual machine. This functionality is hidden from the user. Byte code is a lower leel platform independent representation of the source code. This is done to speed the execution of the program. The byte code is saved as a .pyc file, whereas th source is saved as a .py file.

The simplest way to run your program is through a shell environment. So affter installing Python on your computer, you would open a shell (terminal program). Developing large progams in this manner, however, is cumbersome. A much more natural way to write your program and to test its functionality in small chunks is through an html notebook such as the Juypter notebook. These notebook files are saved as *.ipynb. Some editors such as Visual Studio Code support Juypter notebook files.

In fact this Python tutorial was written in a Jupyter notebook. The notebook has two types of blocks; markdown (for comments) and code bocks (for the python code). The nice thing about the code blocks is that you can execute them by themselves, thereby providing a convenient way to incrementally write and check the functionality of your program.

I am going to use this notebook to illustrate the basic data types, methods, and ways of writing Python programs.

Python is a dynamically typed language. This means that you do not need always need to explicitly declare the type of a variable ahead of time. Python if often intelligent enough to make a reasonably good guess about the type.

There are several basic data types. The first data type of interest are numbers and their basic binary operations.

In [2]:
x = 3                           #declare a number variable, x
print(type(x))                  #prints "<class 'int'>"
print(x)                        #prints "3"
print(x+1)                      #Addition; prints "4"
print(x-1)                      #Subtraction: prints "2"
print(x*2)                      #Multiplication: prints "6"
print(x**2)                     #Exponentiation; prints "9"   
x += 1                  
print(x)                        #increment, prints 4
x*=2    
print(x)                        #prints 8
y = 2.5
print(type(y))                  #prints "<class 'float'>"
print(y, y+1, y*2, y**2)        #prints " 2.5 3.5 5.0 6.25"
<class 'int'>
3
4
2
6
9
4
8
<class 'float'>
2.5 3.5 5.0 6.25

Another important data type are Boolean variables.

In [3]:
t = True
f = False
print(type(t))          #prints "<class 'bool'>"
print(t and f)          #Logical AND; prints "False"
print(t or f)           #Logical OR; prints "True"
print(not t)            #Logical NOT; prints "False" 
print(t != f)           #Logical Xor; prints "True"
<class 'bool'>
False
True
False
True

Python also supports string data types

In [4]:
hello = 'hello'                             #string literals cna use single quotes
world = "world"                             # or double quotes
print (hello)                               #prints "hello"
print(len(hello))                           #print string length; prints "5"
hw = hello + ' ' + world                    #string concatenation
print(hw)                                   #prints "hello world"
hw12 = '%s %s %d' % (hello, world, 12)      #sprintf style string formatting
print(hw12) 
hello
5
hello world
hello world 12

Strings are extremely useful data types for programmers. So there are a number of methods that are particularly useful in manipulating such strings.

In [5]:
s = "hello"
print(s.capitalize())           #Capitalize a strong; prints "Hello"
print(s.upper())                #convert a string to upperase; prints HELLO
print(s.rjust(7))               #right justify a strong, padding with spaces; prints "   hello"
print(s.center(7))              #center a string, padding with spaces, prints "  hello  "
print(s.replace('l','(ell)'))   #replade all instances of one substring by another
print('   world  '.strip())     #strip leading  and trailing whitespace
Hello
HELLO
  hello
 hello 
he(ell)(ell)o
world

Python has some important container types (i.e. data structures consisting of several subtypes of data).

These container types are lists, dictionaries, sets, and tuples. Let us first look at the list data type

In [6]:
xs = [3, 1,  2]                                     #create a list
print(xs, xs[2])                                    #prints "[3, 1, 2] 2"
print(xs[-1])                                       #negative indices cont from end of list; prints "2"
xs[2] = 'foo'                                       #lists can contain elements of different types
print(xs)                                           #prints "[3, 1, 'foo']"
xs.append('bar')                                    #add a new element to the end of the list
print(xs)                                           #prints "[3, 1, 'foo', 'bar']"
x = xs.pop()                                          #remove and return the last element of list
print(x,xs)                                         #prints "bar  [3, 1, 'foo']"
[3, 1, 2] 2
2
[3, 1, 'foo']
[3, 1, 'foo', 'bar']
bar [3, 1, 'foo']

There is special concise syntax used to access sublists. This is sometimes called "slicing" a list. The syntax is similar to MATLAB's slicing syntax with some notable exceptions. Another important thing to note about lists is that the indices start at 0, rather than 1.

In [7]:
nums = list(range(5))       #range is built-in function to create a list of integers
print(nums)                 #Prints "[0, 1, 2, 3, 4]"
print(nums[2:4])            #get a slice from index to 4 (exclusive); prints "[2, 3]"
print(nums[2:])             #get slice from endex 2 to end; prints "[2, 3, 4]"
print(nums[:2])             #get slice from start to index 2 (exclusive); prints "[0, 1]"
print(nums[:])              #get slice of whole list; prints "[0, 1, 2, 3, 4]"
print(nums[:-1])            #slice indices can be negative; prints "[0, 1, 2, 3]"
nums[2:4] = [8, 9]          #assign a new sublist to a slice
print(nums)                 #prints "[0, 1, 8, 9, 4]"
[0, 1, 2, 3, 4]
[2, 3]
[2, 3, 4]
[0, 1]
[0, 1, 2, 3, 4]
[0, 1, 2, 3]
[0, 1, 8, 9, 4]

There is a FOR statement that can be used to loop over the elements of a list. The syntax is somewhat different from MATLAB in that it doesn't have to explicitly declare the range of indices to loop over. The statement "for animal in animals:" knows it is going to loop through all the indices and the actual variable for that index will be called "animal"

One important difference between Python and other languages is that it does not use scoping braces to identify the statements to be executed within a loop. In other words, we don't need to always match and if or while or for statement with a corresponding "end" statement. Instead Python uses the colon : to declare the start of the statements to be looped through. These statements are then typed in using a TAB indent. The end of the looped statements is the last TAB indented statement. This can be troublesome for beginners who are not used to using TABS in this way.

In [8]:
animals = ['cat', 'dog', 'monkey']
for animal in animals:
    print(animal)
#prints "cat", "dot", "monkey", each on its own line
cat
dog
monkey

If you want access to index of each element within the body of aloop use the enumerate function

In [9]:
animals = ['cat', 'dog', 'monkey']
for idx, animal in enumerate(animals):
    print('#%d: %s' % (idx + 1, animal))
# prints "#1: cat"
#        "#2: dog"
#       "#3: monkey"
#1: cat
#2: dog
#3: monkey
In [2]:
animals = ['cat', 'dog', 'monkey']
for indx in range(len(animals)):
    print('#%d: %s' % (indx + 1, animals[indx]))
#1: cat
#2: dog
#3: monkey

List comprehensions: when programming, we often want to transform one type of data into another. As an example, consider the following that computes square numbers

In [10]:
nums = [0, 1, 2, 3, 4]
squares = []
for x in nums:
    squares.append(x**2)
print(squares)        #prints [0, 1,  4,  9,  16]
[0, 1, 4, 9, 16]

You can make this simpler using list comprehension

In [11]:
nums = [0, 1, 2, 3, 4]
squares = [x**2 for x in nums]
print(squares)
#Prints [0, 1, 4, 9, 16]
[0, 1, 4, 9, 16]

List comprehensions can also contain conditions:

In [12]:
nums = [0, 1, 2, 3, 4]
even_squares = [x ** 2 for x in nums if x % 2==0]
print(even_squares)    #prints "[0, 4, 16]"
[0, 4, 16]

Dictionaries: a dictionary stores (key, value) pairs.

In [13]:
d = {'cat': 'cute', 'dog': 'furry'}             #create a new dictionary with some data
print(d['cat'])                                 #get entry from dictionary for; prints "cute" 
print('cat' in d)                               #check if dict has a given key; prints "True" 
d['fish'] = 'wet'                               #set an entry in the dict
print(d['fish'])                                #prints "wet" 
#print(d[ 'monkey'])                             #returns "KeyError: 'monkey'   since 'monkey' not in dict"
print(d.get('monkey', 'N/A'))                   #get an element with adefualt; prints "N/A" 
print(d.get('fish', 'N/A'))                     # prints "wet" (using default)
del d['fish']                                   # removes element from dictionary
print(d.get('fish', 'N/A'))                     # prints "N/A" since fish no longer a key in dict
cute
True
wet
N/A
wet
N/A

Loops: it is easy to iterate over the keys in a dictionary

In [14]:
d = {'person': 2, "cat": 4, "spider":8 }
for animal in d:
    legs = d[animal]
    print('A %s has %d legs' % (animal,legs))
#prints "A person has 2 legs", "A cat has 4 lengs", "A spider has 8 legs"
A person has 2 legs
A cat has 4 legs
A spider has 8 legs

Cute use of these ideas for a madlib story First declare the story using keys from a list to be created

In [15]:
storyFormat = """                                        
        Once upon a time, deep in an ancient jungle,             
        there lived a {animal}.  This {animal}                   
        liked to eat {food}, but the jungle had                  
        very little {food} to offer.  One day, an               
        explorer found the {animal} and discovered
        it liked {food}.  The explorer took the
        {animal} back to {city}, where it could
        eat as much {food} as it wanted.  However,
        the {animal} became homesick, so the
        explorer brought it back to the jungle,
        leaving a large supply of {food}.
                                                                
        The End                                                 
        """          

print(storyFormat)     
                                        
        Once upon a time, deep in an ancient jungle,             
        there lived a {animal}.  This {animal}                   
        liked to eat {food}, but the jungle had                  
        very little {food} to offer.  One day, an               
        explorer found the {animal} and discovered
        it liked {food}.  The explorer took the
        {animal} back to {city}, where it could
        eat as much {food} as it wanted.  However,
        the {animal} became homesick, so the
        explorer brought it back to the jungle,
        leaving a large supply of {food}.
                                                                
        The End                                                 
        

define functions that will allow you to pick entries for dictionary may function tellStory, tells the story after asking you to pick animal, food, city

In [16]:
def tellStory():
    userPicks = dict()
    addPick('animal', userPicks)
    addPick('food', userPicks)
    addPick('city', userPicks)
    story = storyFormat.format(**userPicks)
    print(story)

def addPick(cue, dictionary):
    '''Prompt for a user response using the cue string,
    and place the cue-response pair in the dictionary.
    '''
    prompt = 'Enter an example for ' + cue + ': '
    response = input(prompt)
    dictionary[cue] = response
    

Now run tellStory to generate a story and print it ou

In [17]:
tellStory()
input("Press Enter to end the program.")
                                        
        Once upon a time, deep in an ancient jungle,             
        there lived a dog.  This dog                   
        liked to eat tofu, but the jungle had                  
        very little tofu to offer.  One day, an               
        explorer found the dog and discovered
        it liked tofu.  The explorer took the
        dog back to detroit, where it could
        eat as much tofu as it wanted.  However,
        the dog became homesick, so the
        explorer brought it back to the jungle,
        leaving a large supply of tofu.
                                                                
        The End                                                 
        
Out[17]:
''

If you want access to keys and their corresponding values use the items method

In [18]:
d = {'person': 2, 'cat': 4, 'spider': 8}
for animal, legs in d.items():
    print('A %s has %d legs' % (animal, legs))
#prints "A person has 2 legs", "A cat has 4 lengs", "A spider has 8 legs"
A person has 2 legs
A cat has 4 legs
A spider has 8 legs

Dictionary comprehensions: these are similar to list comprehensions, and allow you to easily construct dictionaries

In [19]:
num = [0, 1, 2, 3, 4]
even_num_to_square = {x: x**2 for x in nums if x%2==0}
print(even_num_to_square)       #prints "{0: 0, 2: 4, 4: 16}" 
{0: 0, 2: 4, 4: 16}

Sets: a set is an unordered collection of distinct elements

In [20]:
animals = {'cat', 'dog'}
print('cat' in animals)     #check if an element is in a set; prints "True"
print('fish' in animals)    #prints "False" 
animals.add('fish')         #add an element to a set
print('fish' in animals)    #no prints TRUE
animals.add('cat')          #adding an element already in set does nothing
print(len(animals))         #prints "3" 
animals.remove('cat')       #remove an element from a set
print(len(animals))         #prints "2" 
True
False
True
3
2

Loops: iterating over a set has the same syntax as iterating over a list However since sets are unordered, you cannot assumptions about the order in which you visit the set elements

In [21]:
animals = {'cat', 'dog', 'fish'}
for idx, animal in enumerate(animals):
    print('#%d: %s' % (idx+1, animal))
#prints "?"
#1: dog
#2: cat
#3: fish

Set comprehensions: like lists and dictionaries, we can easily construct sets using set comprehensions

In [22]:
from math import sqrt           #import the sqrt function from math library
nums = {int(sqrt(x)) for x in range(30)}
print(nums)         #prints "{0, 1, 2, 3, 4, 5}"
{0, 1, 2, 3, 4, 5}

TUPLES: a tuple is an (immutable) ordered list of values. A tuple is imilar to a list with the difference being that tuples can be used as keys in dictionaries and as elemetns of sets, while lists cannot. Here is an example

In [3]:
d = {(x, x+1): x for x in range(10)}        #create dict with tuple keys
t = (5, 6)                                  #create a tuple
print(type(t))                              #check type; prints "<class 'tuple'>" 
print(d[t])                                 #fetches contents for t; prints "5" 
print(d[(1,2)])  
<class 'tuple'>
5
1

Functions: we saw above in matlib example python functions are dfined using the def keyword

In [23]:
def sign(x):
    if x>0:
        return "positive"
    elif x< 0:
        return "negative" 
    else:
        return 'zero'

for x in [-1,0,1]:
    print(sign(x))
#prints "negative", "zero", "positive" 
negative
zero
positive

We often define functions to take optional keyword arguments

In [24]:
def hello(name, loud = False):
    if loud:
        print('HELLO, %s!' % name.upper())
    else:
        print('Hello, %s' %name)

hello('Bob')            #prints "Hello, Bob"
hello('Fred', loud=True)    #prints "HELLO, FRED!"
Hello, Bob
HELLO, FRED!

CLASSES: Python syntax support the easy creation of class objects

In [25]:
class Greeter(object):
    #Constructor
    def __init__(self, name):
        self.name = name        #create an instance variable

    #Instance method
    def greet(self, loud=False):
        if loud:
            print('HELLO, %s!' % self.name.upper())
        else:
            print('Hello, %s' % self.name)
g = Greeter('Fred')     #consruct an instance of the Greeter class
g.greet()               #call an instance method; prints "Hello, Fred" 
g.greet(loud=True)      #call instance method; prints "HELLO FRED!" 
Hello, Fred
HELLO, FRED!

NUMPY: Numpy is the core library for scientific computing in Python. It provides a high performance multidimensional array object, and tools for working with these arrays.

Arrays: A numpy array is a grid of values, all of the same type, and is indexed by a tuple of nonnegative integers. The number of dimensions is the rank of the array, the shape of an array is a tuple of integers giving the size of the array along each dimension

We initialize numpy arrays from nested Python lists, and access elements using square brackets.

In [26]:
import numpy as np

a = np.array([1, 2, 3])             #create a rank 1 array
print(type(a))                      #prints "<class 'numpy.ndarray'>" 
print(a.shape)                      #prints "(3,)"
print(a[0], a[1], a[2])             #prints "1 2 3"
a[0] = 5                            #change array element
print(a)                            #prints "[5, 2, 3]"

b = np.array([[1,2,3],[4,5,6]])     #create a rank 2 array
print(b.shape)                      #prints (2,3)
print(b[0, 0], b[0, 1], b[1, 0])    #prints "1 2 4" 
<class 'numpy.ndarray'>
(3,)
1 2 3
[5 2 3]
(2, 3)
1 2 4

Numpy provides functions to create arrays

In [27]:
import numpy as np

a = np.zeros((2,2))                 #create 2 by 2 array of all zeros
print(a)                            #prints "[[0. 0.]
                                    #         [0. 0.]]" 
b = np.ones((1,2))                  #create 1 by 2 array of all ones
print(b)                            #prints  "[[1. 1.]]"

c = np.full((2,2), 7)               #create a constsant array
print(c)                            #prints  "[[7. 7.]
                                    #          [7. 7.]]"
d = np.eye(2)                       # create 2 by 2 indentiy matrix
print(d)                            #prints "[[ 1. 0.]
                                    #         [ 0. 1.]]"

e = np.random.random((2,2))         #create an array filled with random values
print(e)                            #?
[[0. 0.]
 [0. 0.]]
[[1. 1.]]
[[7 7]
 [7 7]]
[[1. 0.]
 [0. 1.]]
[[0.11568196 0.8248139 ]
 [0.38851037 0.70583698]]

Array indexing: numpy offers several ways to index into arrays

Slicing: similar to python lists, numpy arrays can be sliced. Since arrays may be multidimensional, you must specify a slice for each dimension of the array

In [28]:
import numpy as np

#create the following rank 2 array with shape (3,4)
#[[1 2 3 4]
#  [ 5 6 7 8]
#  [9 10 11 12]]

a = np.array([[1,2,3,4],[5,6,7,8],[9,10,11,12]])

#use slicing to pull out the subarray consisting of the first 2 rows
#and columns 1 and 2; b is the following array of shape (2,2):
#  [[2 3]
#    [6,7]]
b = a[:2, 1:3]
print(b)

#a slice of an array is a view into the same data, so modifying it will modify the orignal array
#  so b is not a copy of a, it is a pointer to elements within a

print(a[0,1])           #prints "2"
b[0,0] = 77             #b[0,0] is the same as a[0,1]
print(a[0,1])           #prints "77"
[[2 3]
 [6 7]]
2
77

You can also mix integer indexing with slice indexing. However doing so will yield an array of lowe rank than the original array. NOTE: this is quite different from the way MATLAB handles array slicing

In [29]:
import numpy as np

#create the following rank 2 array with shape (3,4)
#[[ 1 2 3 4]
# [5 6 7 8]
#  [9 10 11 12]]
a = np.array([[1,2,3,4],[5,6,7,8],[9,10,11,12]])

#two ways of accessing the data in the middle row of the array.
#mixing integer indexing with slices yields an array of lowe rrank,
#while using only slices yields an array of the same rank as the original array:

row_r1 = a[1, :]         #Rank 1 view of the second row of a
row_r2 = a[1:2, :]       #Rank 2 view of the second row of a

print(row_r1, row_r1.shape)     #prints "[5 6 7 8] (4,)" 
print(row_r2, row_r2.shape)     #prints "[[5 6 7 8]] (1,4)"

#we can make the same distinction when accessing columns of an array
col_r1 = a[:,1]
col_r2 = a[:,1:2]
print(col_r1, col_r1.shape)     #prints "[2 6 10] (3,)"
print(col_r2, col_r2.shape)     #prints "[[ 2]
                                #         [ 6]
                                #         [10]] (3,1)"
[5 6 7 8] (4,)
[[5 6 7 8]] (1, 4)
[ 2  6 10] (3,)
[[ 2]
 [ 6]
 [10]] (3, 1)

Integer array indexing: When you index into numpy arrays using slicing, the resulting array view will always be a subarray of the original matrx. In contrast, integer array indexing allows you to construct arbirary arrays using the data rrom another array

In [30]:
import numpy as np

a = np.array([[1,2], [3,4], [5,6]])

#an example of integer array indexing.
#the return array will hae shape (3,) and
print(a[[0,1,2],[0,1,0]])
#prints "[1 4 5]"

#the above example is equivalent to this
print(np.array([a[0,0], a[1,1], a[2,0]]))

#when using integer array indexing, you can reuse the same element from the source array
print(a[[0,0], [1,1]])
#which is equivalent to this
print(np.array([a[0,1], a[0,1]]))   #both print "[2,2]" 
[1 4 5]
[1 4 5]
[2 2]
[2 2]

A useful trick with integer array indexing is selecting or mutating one element from each row of a matrix

In [31]:
import numpy as np

#create a new array from which we select elements
a = np.array([[1,2,3],[4,5,6],[7,8,9],[10,11,12]])

print(a)

#create an array of indices
b = np.array([0, 2, 0, 1])

#select one elmement from each row of a using the indices in b
print(a[np.arange(4), b])           #prints "[1 6 7 11]"

#mutuate one element from each row of a rusing indices in b
a [np.arange(4), b]  += 10

print(a)    #prints "array([[ 11 2 3],
            #               [4, 5, 16],
            #               [17, 8, 9],
            #               [10, 21, 12]])"
[[ 1  2  3]
 [ 4  5  6]
 [ 7  8  9]
 [10 11 12]]
[ 1  6  7 11]
[[11  2  3]
 [ 4  5 16]
 [17  8  9]
 [10 21 12]]

Boolean array indexing lets you pick out aribtary elements of an array. This is used to select elements of an array that satsify some condition

In [32]:
import numpy as np
a = np.array([[1,2], [3,4], [5,6]]) 
bool_idx = (a>2)            #find elements of a that are bigger than 2
                            #this returns a numpy array of Booleans of the same
                            #shape as a, where each slot of bool_idx tells
                            #whether that element of a is > 2
print(bool_idx)             #prints  "[[False False]
                            #            [True True]
                            #            [True True]]"
#We use boolean array indexing to construct a rank 1 array
#consisting of the elements of a corresponding to the true values of bool_idx

print(a[bool_idx])              #prints "[3 4 5 6]"

#23 can do all of the above in a single statement
print(a[a>2])                   #prints "[3 4 5 6]"
[[False False]
 [ True  True]
 [ True  True]]
[3 4 5 6]
[3 4 5 6]

Data types Every numpy array is a grid of elements of the same type. Numpy provides many numeric data types that you can use to construt arrays. Numpty tries to gess a data type when you create an array, but functions that construct arrays often include an arugmeent to explicitly change the datatype

In [33]:
import numpy as np

x = np.array([1, 2])            #let numpy choose datatype
print(x.dtype)                  #prints "int64"

x = np.array([1.0, 2.0])        #let numpy choose datatype
print(x.dtype)                  #prints "float64"

x = np.array([1.0, 2.0], dtype = np.int64)      #force a particular datatype
print(x.dtype)
int64
float64
int64

Array math Basic mathematical functions operate ELEMENTWISE an arrays, and are available both as operator overloads and functions

In [34]:
import numpy as np

x = np.array([[1,2],[3,4]], dtype=np.float64)
y = np.array([[5,6],[7,8]], dtype=np.float64)

#elementwise sum; both produce the array
#[[ 6.0 8.0]
#  [ 10.0 12.0]]

print(x+y)
print(np.add(x,y))

#elementwise diffference, both produce array
#[[-4.0 -4.0]
# [-4.0 -4.0]]
print(x-y)
print(np.subtract(x,y))

#elementwise product; both produce the array
#[[ 5.0 12.0]
# [21.0 32.0]]
print(x*y)
print(np.multiply(x,y))

#elementwise division; both produce the array
# [[ 0.2   0.33333]
# [0.4285713 0.5]]
print(x/y)
print(np.divide(x,y))

#elementwise sequare root
#[[ 1.   1.41421356]
# [1.73205081 2]]
print(np.sqrt(x))
[[ 6.  8.]
 [10. 12.]]
[[ 6.  8.]
 [10. 12.]]
[[-4. -4.]
 [-4. -4.]]
[[-4. -4.]
 [-4. -4.]]
[[ 5. 12.]
 [21. 32.]]
[[ 5. 12.]
 [21. 32.]]
[[0.2        0.33333333]
 [0.42857143 0.5       ]]
[[0.2        0.33333333]
 [0.42857143 0.5       ]]
[[1.         1.41421356]
 [1.73205081 2.        ]]

Note that unlike MATLAB, *, is elementwise not matrix multiplication. Instead we use dot function to compute inner products of vectors, to multiply a vector by a matrix and to multplity matrices.

In [35]:
import numpy as np

A = np.array([[1,2],[3,4]])
B = np.array([[5,6],[7,8]])

x = np.array([9,10])
y = np.array([11, 12])

#iner product of vectors; both produce 219
print(x.dot(y))
print(np.dot(x,y))

#matrix/vector pfoduct produces rank 1 array [29 67]
print(A.dot(x))
print(np.dot(A,x))

#matrix/matrix product produces rank 2 array [[19 22],[43 50]]
print(A.dot(B))
print(np.dot(A,B))
219
219
[29 67]
[29 67]
[[19 22]
 [43 50]]
[[19 22]
 [43 50]]

Numpy provides functions perofrming computations on arrays. one othe most useful is sum

In [36]:
import numpy as np

x = np.array([[1,2],[3,4]])

print(np.sum(x))            #compute sum of all elements; prints "10"
print(np.sum(x,axis=0))     #compute sum of each columns; prints "[4 6]"
print(np.sum(x, axis=1))    #compute sum of each row; prints "[3 7]"
10
[4 6]
[3 7]

We often need to reshape or manipluate rray data. Examples include transpotion of the atrix

In [37]:
import numpy as np

x = np.array([[1,2],[3,4]])
print(x)            #prints "[[1 2]
                          #  [3 4]]"
print(x.T)          #prints "[[1 3]
                           # [2 4]]"

#note that taking transpose of rank 1 array does nothing:
v = np.array([1,2,3])
print(v)
print(v.T)
[[1 2]
 [3 4]]
[[1 3]
 [2 4]]
[1 2 3]
[1 2 3]

Broadcasting is a mechanism that allows numpy to work with arrays of different shapes when performing arithmetic opeartions. We may have a smaller and larger array and we want to use the smaller array mulitple times to perform some operation on the larger array

In [38]:
import numpy as np

#we will add the vector v to each row of matrix A
A = np.array([[1,2,3],[4,5,6],[7,8,9,],[10,11,12]])
v = np.array([1,0,1])
Y = np.empty_like(A)                    #create an empty matrix with same shape as x

#add the vector to each row of the matrix A with explicit loop
for i in range(4):
    Y[i,:] = A[i,:]+v

#now $Y$ is as follows
#[[ 2 2 4]
# [5 5 7]
# [8 8 10]
# [1 1 13]]
print(Y)
[[ 2  2  4]
 [ 5  5  7]
 [ 8  8 10]
 [11 11 13]]

This is slow when the matrix A is very large. Note that adding the vector v to each row of A is the same as forming a matrix VV by stacking multiple copies of v vertically, and then performing elementwise summation of A and VV

In [39]:
import numpy as np

#We wil add the vector v to each row of the matrix A
#storing the result in Y
x = np.array([[1,2,3],[4,5,6],[7,8,9],[10,11,12]])
v = np.array([1,0,1])
VV = np.tile(v,(4,1))           #stack 4 copies of v on top of each other
print(VV)                       #prints [[1 0 1]
                                #       [1 0 1]
                                #       [1 0 1]
                                #       [1 0 1]]
Y = A+VV                        #add A and VV elementwise
print(Y)                        #prints [[2 2 4]
                                #          [5 5 7]
                                #          [8 8 10]
                                #          [11 11 13]]
[[1 0 1]
 [1 0 1]
 [1 0 1]
 [1 0 1]]
[[ 2  2  4]
 [ 5  5  7]
 [ 8  8 10]
 [11 11 13]]

Numpy broadcasting allows us to perform this computation without creating multiple copies of v.

In [40]:
import numpy as np

#add the vector v to each row of matrix A
#storing the result in matrix Y
A = np.array([[1,2,3,],[4,5,6],[7,8,9],[11,11,12]])
v = np.array([1,0,1])
Y = A+v #add v to each row of A using broadcasting
print(Y)
[[ 2  2  4]
 [ 5  5  7]
 [ 8  8 10]
 [12 11 13]]

The line Y=A+v works even through A and v have different shapes. Note this works as v actually had the shape (4,3) where each row is a copy of v. Broadcasting two arrays together follows these rules

  1. If the arrays do not have the same rank, prepend the shape of the lower rank array with 1s until both shapes have the same length.

  2. The two arrays are said to be compatible in a dimension if they have the same size in the dimension, or if one of the arrays has size 1 in that dimension.

  3. The arrays can be broadcast together if they are compatible in all dimensions.

  4. After broadcasting, each array behaves as if it had shape equal to the elementwise maximum of shapes of the two input arrays.

  5. In any dimension where one array had size 1 and the other array had size greater than 1, the first array behaves as if it were copied along that dimension

Here are some applications of broadcasting

In [41]:
import numpy as np

#compute outer product of vectors
v = np.array([1,2,3])           #v has shape (3,)
w = np.array([4,5])             #w has shape (2,)

#compute outer product, we reshape v to be a column vector of shape (3,1); we can then broadcast it against w
#to yield an output of shape (3,2), whih is the outer product
print(np.reshape(v, (3,1))*w)

#add a ector to each row of a matrix
A = np.array([[ 1,2,3], [4,5,6]])
print(A+v)

#add a vector to each column or a matrix
#A has shape (2,3) and w has shape (2,)
#if we transpose A then its shape is (3,2) and can be broadcast
#against w to yield a result of shape (3,2)
#transposing this results yields the final result with shape (2,3), which is the matrix A with
#vector w added to each column

print((A.T+w).T)

#another solution would reshape w to be a column ector of shape (2,1)
#and then broadcast it against A to produce same otuput

print(A + np.reshape(w, (2,1)))

#multiply a matrix by a constant:
#A has shape (2,3).  nmpy treats scalars as arrays o shape ()
#these cna be broadcast together to shape (2,3), 
print(x*2)
[[ 4  5]
 [ 8 10]
 [12 15]]
[[2 4 6]
 [5 7 9]]
[[ 5  6  7]
 [ 9 10 11]]
[[ 5  6  7]
 [ 9 10 11]]
[[ 2  4  6]
 [ 8 10 12]
 [14 16 18]
 [20 22 24]]

Matplotlib is a plotting library. most important function is plot

In [42]:
import numpy as np
import matplotlib.pyplot as plt

#compute the x and y coordinates for a sine function
x = np.arange(0, 3*np.pi, 0.1)
y = np.sin(x)

#plot points using matplotlib
plt.plot(x,y)
#plt.show()         #must call plt.show() to make graphics appear
Out[42]:
[<matplotlib.lines.Line2D at 0x7fd2e1891c50>]
No description has been provided for this image

plotting multiple lines, adding title legend and axis labels

In [43]:
import numpy as np
import matplotlib.pyplot as plt

#compute x and y coordinates of sine and cosime curves
x = np.arange(0,3*np.pi, 0.1)
y_sin = np.sin(x)
y_cos = np.cos(x)

#plot points
plt.plot(x, y_sin)
plt.plot(x, y_cos)
plt.xlabel('x axis label')
plt.ylabel('y axis label')
plt.title('Sine and Cosine')
plt.legend(['Sine', 'Cosine'])
#plt.show()
Out[43]:
<matplotlib.legend.Legend at 0x7fd2f2c6d450>
No description has been provided for this image

you can plot different things in same figure using subplot function

In [44]:
import numpy as np
import matplotlib.pyplot as plt

x = np.arange(0, 3*np.pi, 0.1)
y_sin = np.sin(x)
y_cos = np.cos(x)

#set up subplot grid of height 2 and width 1,  set first subplot as active
plt.subplot(2,1,1)

#make first plot
plt.plot(x, y_sin)
plt.title('Sine')

#set second subplot as active and make second plot
plt.subplot(2,1,2)
plt.plot(x, y_cos)
plt.title('Cosine')
Out[44]:
Text(0.5, 1.0, 'Cosine')
No description has been provided for this image