This is a gentle introduction to people who have some experience with programming to get started with python. We used this boot camp for EE 16A at Berkeley. Source files: ee16a_python_bootcamp.zip

# Overview

This mini-lab serves as an introduction to IPython and a couple important packages we will be using throughout the semester. The lab aims to teach you proper usage of certain commands and can serve as a reference doc in the future. Even if you are a python wizard already, we recommend that you at least look through the lab to get re-acquainted with the functions we will be using a lot.

This lab is separated into two main parts: Guide and Questions. The Guide portion walks you through frequently used Python and IPython code, functions, and techniques. The Guide is supplemented with numerous blocks of example code to showcase concepts. The Questions portion of the lab is a collection of 10 problems meant to test your understanding of this guide. If you can answer these questions, then you have the knowledge to complete any and all programming tasks in EE 16A.

# Python

Both the labs and the homeworks in this course will require you to write some Python code. If you’re new to programming, have no fear, as the assignments don’t require more than just the fundamentals; this is not round 2 of 61A.

## Control Flow in Python

Programming languages usually contain statements that can be used to direct or “control the flow” of execution. This includes (but is not limited to) conditional statements such as if, else, and elif, and loop-control statements such as while, for, break, and continue.

### Conditional Statements: (if, else, elif)

# Example 1: Simple if/else

x = 16

if x > 20: # Asking the question, "Is x greater than 20?"
print('if condition is True!')
else:
print('if condition is False!')

# Example 2: Introducing elif

x = 16

if x > 20: # Asking the question, "Is x greater than 20?"
print('first if condition is True!')
elif x > 10 and x < 20: # Asking the question, "Is x greater than 10 AND less than 20?"
print('first if condition is False and second if condition is True!')
else:
print('Neither if condition was True!')


### Loop-Control Statements: (while, for)

# Example 3: while

i = 0
while i < 5: # Check if i < 5 every iteration. Stop looping if the condition is false, i.e. if i >= 5.
print('i:',i)
i += 1 # increment i by 1


Unlike while loops, which can theoretically run “forever” given the right condition, for loops serve a different purpose – iterating a fixed number of times. For loops in Python expect an iterable object – something similar to a list – to control the number of iterations. The example below is “equivalent” to the while loop in the previous example.

# Example 4: for (pun intended)

print('i:',i)

# Notice no i += 1 statement!

# Example 5: Iterating through lists

char_list = [1, 6, 'a']
word = ''

for element in char_list:
word += str(element)

print('word:',word)


All of the loop examples so far have terminated with some sort of stopping condition (i < 5, i in range(0,5), element in char_list). But what if we wanted to exit a loop early? Or, what if we wanted to immediately go to the next loop iteration? These two changes can be applied using the break and continue statements, respectively.

# Example 6: The break statement

candies = ['Skittles', 'Snickers', '3 Musketeers', 'Twizzlers', 'Kit-Kat', 'Twix', 'Almond Joy']

print('Loop without break statement.')
for candy in candies:
print(candy)

print('\nLoop with break statement.')
for candy in candies:
print(candy)
if candy == 'Kit-Kat':
break

# Example 7: The continue statement

candies = ['Skittles', 'Snickers', '3 Musketeers', 'Twizzlers', 'Kit-Kat', 'Twix', 'Almond Joy']

print('Same Loop as above but with continue instead of break statement.')
for candy in candies:
print(candy)
if candy == 'Kit-Kat':
continue

print('\nLoop that skips over every-other candy.')
for i in range(len(candies)):
if i % 2 == 1: # if i is odd. The "%" symbol is the modulo operator: https://en.wikipedia.org/wiki/Modulo_operation
continue
print(candies[i])


Notice how the continue statement enabled us to skip every-other candy.

## List Comprehension

There are multiple ways of creating lists in Python. A list is a mutable array of data. They can be created using square brackets [ ]. Elements in the list are separated by commas. Elements can be of any type (int, string, float, etc.).

Important Python List functions:

• '+' joins lists and creates a new list.

• len(x) to get the length of list x

Next, we will explore the idea of a list comprehension, which is a compact way of creating a list from a for-loop in a single line. Please keep in mind that list comprehension is just a style suggestion; any list comprehension can be expanded to a fully fleshed out control-loop block. However, the advantage of list comprehensions is their compact yet expressive syntax.

For the example below, our goal is to create a list of the squares of each even integer in the range from 0 to 10 (inclusive).

# Example 8a: for-loop list construction

# Expected output: [0*0, 2*2, 4*4, 6*6, 8*8, 10*10], which equals: [0, 4, 16, 36, 64, 100]

lst = []
for i in range(11): # iterate over numbers 0 through 10 (inclusive)
if i % 2 == 0: # see example 7 for explanation of "%" symbol.
lst += [i**2] # '**' is Python syntax for raising the power. Alternatively: lst.append(i**2)

print(lst)

# Example 8b: List Comprehension

lst = [i**2 for i in range(11) if i % 2 == 0] # one-liner magic
print(lst)


The syntax for a list comprehension is as follows:
list = ** **[function(ITEM) for ITEM in ITERABLE_OBJECT if condition(ITEM)]

In example 8b above:
ITEM = i
ITERABLE_OBJECT = range(11)
function() = raise ITEM to the second power
condition() = is ITEM even?

A couple notes: list comprehensions DO NOT require a condition, list comprehensions can have nested for-loops.

# NumPy

From the NumPy website, “NumPy is the fundamental package for scientific computing with Python. It contains among other things: a powerful N-dimensional array object.” For the purposes of this course, we primarily use NumPy for its fast and fancy matrix functions. In general, Python list operations are slow; NumPy functions exploit the NumPy array object to “vectorize” the code, which usually improves the runtime of matrix calculations. As a general rule of thumb, if a task involves vectors or matrices, you should resort to NumPy. In addition to speeding up basic operations, NumPy contains an enormous library of matrix functions, so if you ever need to manipulate a vector or matrix, NumPy most likely already has a function implemented to suit your needs.

Quintessential NumPy Documentation: http://docs.scipy.org/doc/numpy/reference/index.html

Run the cell below to import the packages needed to complete this lab.

import numpy as np # from now on, we can access numpy functions by referencing "np" instead of numpy
from numpy import linalg # import the linalg package, which includes useful matrix operations


## Creating a NumPy array object

NumPy is centered around the numpy.array() class. This array object is extremely useful, however, it is often confused with built-in Python lists, particularly when trying to represent vectors. NumPy arrays and Python Lists are NOT synonymous; you cannot simply apply functions to NumPy arrays as if they were Python Lists.

# Example 9: Going from Python list to NumPy array

py_lst = [1,2,3,4]
np_arr = np.array(py_lst)
print('Python list:',py_lst)
print('NumPy array:',np_arr)

# Example 10: Populating an empty NumPy array

np_arr = np.empty([4,4], dtype=np.int) # An empty 4x4 numpy array

for i in range(4):
for j in range(4):
np_arr[i,j] = i+j

print(np_arr)

# Example 11: Creating a NumPy array of zeros and the Identity matrix

np_zeros = np.zeros([5,5]) # 5x5 NumPy array of all zeros
np_id = np.eye(5) # 5x5 Identity array

print('np_zeros:\n',np_zeros)
print('\nnp_id:\n',np_id)

# Example 12: Creating a NumPy array that spans a certain set/list of numbers

"""numpy.linspace() is useful when you know the number of divisions over a certain range you want,
i.e., you want to divide the range [0-9] into 10 equal divisions.
"""
np_arr1 = np.linspace(0, 9, 10) # args for linspace(): (start, stop, num_divisions)
print('np_arr1:',np_arr1)

"""numpy.arange() is useful when you know how far away each division is from one another, a.k.a. the step size.
You want to start at 0 and get every number that is 1 away from the previous number until you get to 9.
"""
np_arr2 = np.arange(0, 10, 1) # args for arange(): (start, stop, step)
print('np_arr2:',np_arr2)


### NumPy array vs. Python List

Most arithmetic operations apply to NumPy arrays in an element-wise fashion. This is in contrast with arithmetic operations for Python lists, which apply via concatenation.

# Example 13: NumPy array vs. Python list

lst = [1,2,3]
arr = np.eye(3)

lst2 = lst + lst
arr2 = arr + arr

print('lst:',lst)
print('lst + lst =',lst2)
print('\narr:\n',arr)
print('arr + arr =\n',arr2)


## NumPy array slicing

Array slicing is a technique in Python (and other languages) that programmers use to extract specific index-based information from an array. Array slicing answers queries such as, “What are the first/last n elements in this array?”, “What are the elements in the first r rows and first c columns of this matrix?”, “What is every nth element in this array?”

# Example 14: Basic vector/list slicing

simple_arr = np.arange(0,100,1)

print('\nFirst ten elements of simple_arr:',simple_arr[:10])
print('\nLast ten elements of simple_arr:',simple_arr[-10:]) # you should be aware that in Python,
# requesting a negative index (-n) from list a is the same as requesting is equivalent to requesting a[len(a)-n].
print('\nElements 16-26 of simple_arr:',simple_arr[16:26]) # Notice slicing includes the first index and excludes that last index.


Slicing includes the start index and excludes the end index, i.e. simple_arr[16:26] means to extract the values in simple_arr at indexes in the range [16,26) which is the same as [16,25] since indexes can only be integers.

# Example 15: Some fancy vector/list slicing

simple_arr = np.arange(0,20,1)

print('\nEvery-other element of simple_arr, starting from 0:',simple_arr[::2])
print('\nEvery-third element of simple_arr, starting from 0:',simple_arr[::3])
print('\nEvery-other element of simple_arr, starting from 10-16:',simple_arr[10:16:2])

# Example 16: Slicing NumPy arrays

i = np.array(range(25), dtype=np.int).reshape([5,5]) # numpy.reshape() will be introduced in the next cell
print('i:\n',i)

print('\nFirst row of i:',i[0])
print('\nFirst column of i:',i[:,0])
print('\nRows 1-3 of i:\n',i[1:4])
print('\nColumns 1-3 of i:\n',i[:,1:4])
print('\nTop left 3x3 of i:\n',i[:3,:3])
print('\nEvery-other column of i:\n',i[:,::2])


### NumPy array reshaping

Reshaping is useful when you want to do something such as turn a vector into a matrix or vice-versa. We want to be able to do this because it is often easier to construct the desired array as a vector then reshape the vector into a matrix.

# Example 17: Determining the shape of a NumPy array

test_arr = np.zeros([15,189])

print('Shape of test_arr:',test_arr.shape) # Notice .shape is a NumPy array property NOT a function, i.e. no parenthesis.
print('Number of elements in test_arr:',test_arr.size)

# Example 18: Using reshape()

test_arr = np.array(range(16), dtype=np.int)
print('\ntest_arr:',test_arr)
print('Shape of test_arr:',test_arr.shape)

test_arr_4x4 = test_arr.reshape([4,4]) # Notice reshape() is called on the array object, i.e. array.reshape(dimensions) NOT np.reshape(arr, dimensions)!
print('\nReshaped test_arr:\n',test_arr_4x4)
print('Shape of test_arr_4x4:',test_arr_4x4.shape)

test_arr_vec = test_arr_4x4.reshape(test_arr_4x4.size) # Use array.flatten() instead. This is just to show array.reshape works in both directions.
print('\ntest_arr back as a vector:',test_arr_vec)
print('Shape of test_arr_vec:',test_arr_vec.shape)


## Useful NumPy functions: (transpose(), linalg.inv(), dot(), concatenate(), vstack(), hstack(), max(), argmax())

Quintessential NumPy Documentation: http://docs.scipy.org/doc/numpy/reference/index.html

# Example 19: numpy.transpose()

norm = np.array(range(16), dtype=np.int).reshape([4,4])
print('\nnorm:\n',norm)

norm_transpose = np.transpose(norm)
print('\nnorm_transpose:\n',norm_transpose)

print('\nnorm easy transpose:\n',norm.T) # numpy.transpose(arr) == arr.T

# Example 20: numpy.linalg.inv (finds the inverse of a matrix)

i = np.eye(4)
print('\ni:\n',i)

i_inv = np.linalg.inv(i) # Notice .inv() is a function in the linalg library of NumPy.
print('\ni_inv:\n',i_inv)
print('\nAs expected, i == inv(i).')

# Example 21a: numpy.dot() (how to do matrix multiplication in NumPy!)

a = np.array([[2,3],[4,5]])
print('\na:\n',a)
b = np.array([[1,2],[0,2]])
print('\nb:\n',b)

print('\nMatrix multiplication.')
c = np.dot(a,b)
print('a*b:\n',c)

print('\nOrder matters in numpy.dot()!')
d = np.dot(b,a)
print('b*a:\n',d)
print('Notice a*b != b*a.')

e = np.array([2,2])
print('\ne:',e)

print('\nnumpy.dot() can be used to multiply an array and vector too.')
f = np.dot(a,e)
print('a*e:',f)


Instead of using numpy.dot() to perform matrix multiplication, NumPy provides an alternative using the * operator. Up until now, we’ve been exclusively dealing with NumPy arrays; but there is another NumPy class called matrix. A NumPy matrix is just a 2-dimensional NumPy array, except it has a few additional features. In particular, we can use the * operator to perform multiplication of two NumPy matrices (we CANNOT use * when multiplying NumPy arrays).

# Example 21b: NumPy matrix multiplication
a = np.matrix([[2,3],[4,5]])
print('\na:\n',a)
b = np.matrix([[1,2],[0,2]])
print('\nb:\n',b)

print('\nMatrix multiplication using * operator.')
c = a*b
print('a*b:\n',c)

# Example 22: numpy.concatenate() (how to append/attach multiple arrays.)

a = np.array([[2,3],[4,5]])
print('\na:\n',a)
b = np.array([[1,2],[0,2]])
print('\nb:\n',b)

c = np.concatenate([a,b], axis=0) # axis controls how to concatenate the arrays. axis=0 attach vertically, axis=1 attach horizontally.
print('\nAppend b to the "bottom" of a:\n',c)

d = np.concatenate([a,b], axis=1)
print('\nAppend b to the "right" of a:\n',d)

# Example 23: numpy.vstack() and numpy.hstack()

a = np.array([[2,3],[4,5]])
print('\na:\n',a)
b = np.array([[1,2],[0,2]])
print('\nb:\n',b)

c = np.vstack([a,b])
print('\nvstack a and b:\n',c)
print('Notice this is equivalent to concatenate with axis=0.')

d = np.hstack([a,b])
print('\nhstack a and b:\n',d)
print('Notice this is equivalent to concatenate with axis=1.')


# Miscellaneous Functions

# Example 24: np.floor(), np.ceil()

a = 16.5
print('a:',a)
print('floor of a:',np.floor(a))
print('ceiling of a:',np.ceil(a))

# Example 25: np.max(), np.min(), np.argmax(), np.argmin()

a = np.array([0,1,2,3,16,3,2,1,0])
print('a:',a)
print('max of a =',np.max(a))
print('min of a =',np.min(a))
print('index of max value of a =',np.argmax(a))
print('index of min value of a =',np.argmin(a))


# Questions

These questions are in no particular order (except for question 0, do that one first). The questions range in difficulty; some are one-liners, others require a lot more thinking. Don’t be discouraged if you hit a roadblock. Talk to your neighbors and ask for help from the lab staff.

### Question 0

In order to test your code, please run the cell below to load the autograder. There is a cell after each question that you can run in order to check your answer. The autograder is purposefully not very verbose.

%run autograder.py


### Question 1

Search the NumPy documentation and/or the web for a NumPy function that can solve a system of linear equations of the form Ax=b. Once you’ve found the package and function, insert those names into the package and function placeholders in the cell below.

# find the missing package and function
func = np.your.function

# Do not modify the code below
def q1(A,b):
return func(A,b)

test_q1(q1)


### Question 2

Given NumPy array A, return an array that consists of every entry of A that is in an even row and in an odd column.

def q2(A):
"""
Input:
A - MxN NumPy array

Output:
Returns a NumPy array that consists of every entry of A that has an even row index and has an odd column index.

Example:
A = np.array([[ 1  2  3  4  5]
[ 6  7  8  9 10]
[11 12 13 14 15]
[16 17 18 19 20]
[21 22 23 24 25]])

Output = np.array([[ 2  4]
[12 14]
[22 24]])
"""


test_q2(q2)


### Question 3

Given an MxN NumPy array, first find the indices of the maximum value in each row of the array, then return the largest of the indices.

def q3(A):
"""
Input:
A - MxN NumPy array

Output:
Return the maximum index of maximum row values of A.

Example:
A = np.array([[0 1 0 0]
[1 0 0 0]
[0 0 0 0]
[0 0 1 0]])

Output = 2
"""

return

test_q3(q3)


### Question 4

Given two MxN NumPy arrays, copy every-other column of array A to the right side of array B.

def q4(A, B):
"""
Inputs:
A - MxN NumPy array
B - MxP NumPy array

Output:
Returns an Mx(P+(N/2)) NumPy array where every-other column of A is added to the right side of B in order,
starting from index 0.

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

Output = np.array([[1,2,3,1,0]
[4,5,6,0,0]
[7,8,9,0,1]])
"""

return

test_q4(q4)


### Question 5

Given vectors u = [1,2,3,…,N] and v = [2017,2018,2019,…,2017+N-1], create a vector that contains the following sequence: [1*2017, 2*2018, 3*2019,…,N*(2017+N-1)].

def q5(N):
"""
Input:
N - the number of elements in u and v.

Output:
Returns the sequence: np.array([1*2017,2*2018,...,N*(2017+N-1)])

Example:
N = 5

Output = np.array([ 2017  4036  6057  8080 10105])
"""
u = # YOUR CODE HERE
v = # YOUR CODE HERE

return

test_q5(q5)


### Question 6

Given a NumPy vector v, shift all of the elements in v by n steps to the right; values that “fall off” the right end of v get inserted at the beginning of v, thus the length of v is preserved. You can either attempt to implement this on your own, or, (hint hint) try searching for a related NumPy function that does some/all of the work for you…

def q6(v, N=10):
"""
Input:
v = NumPy vector
N = number of steps to shift v to the right

Output:
Returns v shifted to the right by N steps.

Example:
v = np.array([0,1,2,3,4,5])
N = 3

Output = np.array([3,4,5,0,1,2])
"""

return

test_q6(q6)


### Question 7

Given an MxM identity matrix, convert this to an (M-N)x(M-N) identity matrix WITHOUT using numpy.eye().

def q7(I=np.eye(10), N=4):
"""
Input:
I - MxM NumPy array representing the identity matrix
N - number of rows and columns to cut from I

Output:
Returns an (M-N)x(M-N) NumPy identity array.

Example:
I = np.eye(10)
N = 8

Output = np.array([[1,0]
[0,1]])
"""

# YOUR CODE HERE; REMEMBER, YOU CANNOT USE np.eye()!

return

test_q7(q7)


### Question 8

Given a square NxN NumPy array A, return a Python list of the values along the diagonal of A, sorted in descending order.

def q8(A):
"""
Input:
A - NxN NumPy array

Output:
Returns a Python list containing the diagonal of A sorted in descending order.

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

Output = [9,5,1]
"""

return

test_q8(q8)


### Question 9

Given two differently sized matrices, “pad” the matrices with the smaller dimensions with rows/columns of zeros until they are the same size as one another. Add the padding to the bottom (if adding rows) and to the right (if adding columns). Hint: there might be a NumPy function that does something similar/exactly to this, but it’s good practice to try this yourself.

def q9(A,B):
"""
Input:
A - MxN NumPy array
B - YxZ NumPy array

Output:
Returns the zero-padded versions of each array such that they are of equivalent dimensions.

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

Output = np.array([[1,2,3]
[4,5,6]
[0,0,0]]),
np.array([[1,1,0]
[1,1,0]
[1,1,0]])
"""

return A, B

test_q9(q9)


### Question 10

Given an MxN matrix, A, and an NxM matrix, B, concatenate (side-by-side) the first p rows of A with the transpose of the last p columns of B.

def q10(A, B, p):
"""
Input:
A - MxN NumPy array
B - NxM NumPy array
p - the number of rows from A to concatenate with the number of columns from B

Output:
Returns the side-by-side concatenation of the first p rows of A with the transpose of the last p columns of B.

Example:
A = np.array([[1,1,1]
[1,1,1]
[1,1,1]
[1,1,1]])
B = np.array([[1,2,3,4]
[5,6,7,8]
[9,10,11,12]])
p = 2

Output = np.array([[1,1,1,3,7,11]
[1,1,1,4,8,12]])
"""


test_q10(q10)

test_all(q1,q2,q3,q4,q5,q6,q7,q8,q9,q10)