McKinney Chapter 3 - Practice for Section 04

FINA 6333 for Spring 2024

Author

Richard Herron

1 Announcements

  1. Due Friday, 1/19, at 11:59 PM:
    1. Complete Introduction to Python course on DataCamp (and upload certificate to Canvas)
    2. Acknowledge academic integrity statement on Canvas
  2. I will record and post the next lecture video on Thursday, 1/18, and the associate pre-class quiz is due before class on Tuesday, 1/23
  3. Start joining groups on Canvas (Canvas > People > Team Projects); I removed joining groups as a scored assignment, but please prioritize joining groups

2 10-minute Recap

2.1 List

A list is a collection of items that are ordered and changeable. You can add, remove, or modify elements in a list. In Python, you can create an empty list using either [] or list().

my_list = [1, 2, 3, [1, 2, 3, [1, 2, 3]]]

Python is zero-indexed!

my_list[1]
2

We can chain index operations!

my_list[-1][-1][-1]
3
my_list[-1][-1][-1] = 'Foobar'

my_list
[1, 2, 3, [1, 2, 3, [1, 2, 'Foobar']]]

2.2 Tuple

A tuple is similar to a list, but it is immutable, meaning that you cannot change its contents once it has been created. You can create a tuple using parentheses () or the tuple() function.

my_tuple = (1, 2, 3, (1, 2, 3, (1, 2, 3)))

Tuples are immutable and cannot be changed!

# my_tuple[-1][-1][-1] = 'Foobar'
# ---------------------------------------------------------------------------
# TypeError                                 Traceback (most recent call last)
# Cell In[10], line 1
# ----> 1 my_tuple[-1][-1][-1] = 'Foobar'

# TypeError: 'tuple' object does not support item assignment
my_tuple_2 = (1, 2, 3, (1, 2, 3, [1, 2, 3]))

We cannot change the tuple, but we can change the list inside the tuple.

my_tuple_2[-1][-1][-1] = 'Foobar'

my_tuple_2
(1, 2, 3, (1, 2, 3, [1, 2, 'Foobar']))

Here is a crazy-making example of when we might want to use tuples instead of lists to prevent modifying my_list.

my_list
[1, 2, 3, [1, 2, 3, [1, 2, 'Foobar']]]
def foobar():
    my_list[0] += 7
foobar()
my_list
[8, 2, 3, [1, 2, 3, [1, 2, 'Foobar']]]

2.3 Dictionary

A dictionary is a collection of key-value pairs that are unordered and changeable. You can add, remove, or modify elements in a dictionary. In Python, you can create an empty dictionary using either {} or the dict() function.

stock_prices = [1, 2, 3] # AAPL, MSFT, and TSLA
stock_prices = {'AAPL': 1, 'MSFT': 2, 'TSLA': 3}

stock_prices['AAPL']
1

2.4 List Comprehension

A list comprehension is a concise way of creating a new list by iterating over an existing list or other iterable object. It is more time and space-efficient than traditional for loops and offers a cleaner syntax. The basic syntax of a list comprehension is new_list = [expression for item in iterable if condition] where:

  1. expression is the operation to be performed on each element of the iterable
  2. item is the current element being processed
  3. iterable is the list or other iterable object being iterated over
  4. condition is an optional filter that only accepts items that evaluate to True.

For example, we can use the following list comprehension to create a new list of even numbers from 0 to 8: even_numbers = [x for x in range(9) if x % 2 == 0]

List comprehensions are a powerful tool in Python that can help you write more efficient and readable code (i.e., more Pythonic code).

even_numbers = []
for i in range(9):
    if i%2 == 0:
        even_numbers.append(i)

even_numbers
[0, 2, 4, 6, 8]
%%timeit
[i for i in range(9) if i%2 == 0]
405 ns ± 23.3 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

Here is another way to get even up to 8:

%%timeit
list(range(0, 9, 2))
152 ns ± 7.38 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)

The first is more flexible and allows testing multiple conditions. The second is more streamlined. Above, we use the %%timeit magic to time our code, but, remember, premature optimization is the root of all evil!

3 Practice

3.1 Swap the values assigned to a and b using a third variable c.

a = 1
b = 2
c = a
a = b
b = c
del c
print(f'a is {a}, and b is {b}')
a is 2, and b is 1

3.2 Swap the values assigned to a and b without using a third variable c.

a = 1
b = 2
b, a = a, b
print(f'a is {a}, and b is {b}')
a is 2, and b is 1

3.3 What is the output of the following code and why?

_ = 1, 1, 1

type(_)
tuple
1, 1, 1 == (1, 1, 1)
(1, 1, False)
(1, 1, 1) == 1, 1, 1
(False, 1, 1)
(1, 1, 1) == (1, 1, 1)
True

Parentheses around tuples are optional, but eliminate ambiguity! Also, always check your output!

3.4 Create a list l1 of integers from 1 to 100.

l1 = list(range(1, 101))

print(l1) # we can use print to print a list with less white space
[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, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100]

3.5 Slice l1 to create a list of integers from 60 to 50 (inclusive).

Name this list l2.

Below, there are two steps:

  1. The [49:60] slices from 50 to 60
  2. The [::-1] reverse the list
l2 = l1[49:60][::-1]

l2
[60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50]

Most methods on core Python data structures modify the data structure “in place”.

l2_alt = l1[49:60]
l2_alt.reverse()

l2_alt
[60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50]

3.6 Create a list l3 of odd integers from 1 to 21.

l3 = [i for i in range(22) if i%2 != 0]

l3
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21]
list(range(1, 22, 2))
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21]

3.7 Create a list l4 of the squares of integers from 1 to 100.

l4 = [i**2 for i in range(1, 101)]

print(l4)
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 400, 441, 484, 529, 576, 625, 676, 729, 784, 841, 900, 961, 1024, 1089, 1156, 1225, 1296, 1369, 1444, 1521, 1600, 1681, 1764, 1849, 1936, 2025, 2116, 2209, 2304, 2401, 2500, 2601, 2704, 2809, 2916, 3025, 3136, 3249, 3364, 3481, 3600, 3721, 3844, 3969, 4096, 4225, 4356, 4489, 4624, 4761, 4900, 5041, 5184, 5329, 5476, 5625, 5776, 5929, 6084, 6241, 6400, 6561, 6724, 6889, 7056, 7225, 7396, 7569, 7744, 7921, 8100, 8281, 8464, 8649, 8836, 9025, 9216, 9409, 9604, 9801, 10000]

3.8 Create a list l5 that contains the squares of odd integers from 1 to 100.

l5 = [i**2 for i in range(1, 101) if i%2 != 0]

print(l5)
[1, 9, 25, 49, 81, 121, 169, 225, 289, 361, 441, 529, 625, 729, 841, 961, 1089, 1225, 1369, 1521, 1681, 1849, 2025, 2209, 2401, 2601, 2809, 3025, 3249, 3481, 3721, 3969, 4225, 4489, 4761, 5041, 5329, 5625, 5929, 6241, 6561, 6889, 7225, 7569, 7921, 8281, 8649, 9025, 9409, 9801]

3.9 Use a lambda function to sort strings by the last letter.

strings = ['card', 'aaaa', 'foo', 'bar', 'abab']

Most core Python methods modify their object in place. Here we will use sorted(), which is the function equivalent of the .sort() method. Using the function helps us focus on the output.

sorted(strings, key=len)
['foo', 'bar', 'card', 'aaaa', 'abab']

What if, we want sort by length, then by alphabetical order within length?

  1. Sort by alphabetical order
  2. Then, sort by length
sorted(sorted(strings), key=len)
['bar', 'foo', 'aaaa', 'abab', 'card']

Just like we get the last element in a list or tuple with [-1], we get the last character in a string with [-1]!

'Friday!'[-1]
'!'

How can I reverse a string?

'Friday!'[::-1]
'!yadirF'
def get_last_letter(x):
    return x[-1]
get_last_letter('Friday!')
'!'
sorted(strings, key=get_last_letter)
['aaaa', 'abab', 'card', 'foo', 'bar']

For cases when would only use a function once, we can skip all the overhead of writing a function and use an anonymous function (or a lambda function).

sorted(strings, key=lambda x: x[-1])
['aaaa', 'abab', 'card', 'foo', 'bar']
sorted(strings, key=lambda x: x[1])
['card', 'aaaa', 'bar', 'abab', 'foo']

Here lambda is a keyword that indicates what follows in a nameless function that only exists in this specific context.

3.10 Given an integer array nums and an integer k, return the \(k^{th}\) largest element in the array.

Note that it is the \(k^{th}\) largest element in the sorted order, not the \(k^{th}\) distinct element.

Example 1:

Input: nums = [3,2,1,5,6,4], k = 2
Output: 5

Example 2:

Input: nums = [3,2,3,1,2,4,5,5,6], k = 4
Output: 4

I saw this question on LeetCode.

def get_largest(x, k):
    return sorted(x)[-k]
get_largest(x=[3,2,1,5,6,4], k=2)
5
get_largest(x=[3,2,3,1,2,4,5,5,6], k=4)
4

3.11 Given an integer array nums and an integer k, return the k most frequent elements.

You may return the answer in any order.

Example 1:

Input: nums = [1,1,1,2,2,3], k = 2
Output: [1,2]

Example 2:

Input: nums = [1], k = 1
Output: [1]

I saw this question on LeetCode.

def get_frequent(nums, k):
    counts = {}
    for n in nums:
        if n in counts:
            counts[n] += 1
        else:
            counts[n] = 1
    return [x[0] for x in sorted(counts.items(), key=lambda x: x[1], reverse=True)[:k]]
get_frequent(nums=[1,1,1,2,2,3], k=2)
[1, 2]

3.12 Test whether the given strings are palindromes.

Input: ["aba", "no"]
Output: [True, False]

def is_palindrome(xs):
    return [x == x[::-1] for x in xs]
is_palindrome(["aba", "no"])
[True, False]

3.13 Write a function returns() that accepts lists of prices and dividends and returns a list of returns.

prices = [100, 150, 100, 50, 100, 150, 100, 150]
dividends = [1, 1, 1, 1, 2, 2, 2, 2]
def returns(p, d):
    rs = []
    for i in range(1, len(p)):
        r = (p[i] - p[i-1] + d[i]) / p[i-1]
        rs.append(r)
    return rs

We can use the magic %precision 4 to display floats to only 4 decimal places.

%precision 4
'%.4f'

We have 2+ arguments, it is a good practice to specify argument names (or keywords).

returns(p=prices, d=dividends)
[0.5100, -0.3267, -0.4900, 1.0400, 0.5200, -0.3200, 0.5200]

Here is a solution that avoids a loop counter and combines a list comprehension zip() to loop over prices, dividends, and lagged prices. This solution may be a little more Pythonic, but I find it harder to follow that our loop-counter solution above.

[(p + d - p_lag) / p_lag for p, d, p_lag in zip(prices[1:], dividends[1:], prices[:-1])]
[0.5100, -0.3267, -0.4900, 1.0400, 0.5200, -0.3200, 0.5200]

3.14 Rewrite the function returns() so it returns lists of returns, capital gains yields, and dividend yields.

def returns_2(p, d):
    rs, dps, cgs = [], [], []
    for i in range(1, len(p)):
        r = (p[i] - p[i-1] + d[i]) / p[i-1]
        dp = d[i] / p[i-1]
        cg = r - dp
        rs.append(r)
        dps.append(dp)
        cgs.append(cg)
    return {'r':rs, 'dp':dps, 'cg':cgs}
returns_2(p=prices, d=dividends)
{'r': [0.5100, -0.3267, -0.4900, 1.0400, 0.5200, -0.3200, 0.5200],
 'dp': [0.0100, 0.0067, 0.0100, 0.0400, 0.0200, 0.0133, 0.0200],
 'cg': [0.5000, -0.3333, -0.5000, 1.0000, 0.5000, -0.3333, 0.5000]}
returns_2(p=prices, d=dividends)['r']
[0.5100, -0.3267, -0.4900, 1.0400, 0.5200, -0.3200, 0.5200]

3.15 Rescale and shift numbers so that they cover the range [0, 1].

Input: [18.5, 17.0, 18.0, 19.0, 18.0]
Output: [0.75, 0.0, 0.5, 1.0, 0.5]

x = [18.5, 17.0, 18.0, 19.0, 18.0]
def rescale(x):
    x_min = min(x)
    x_max = max(x)
    return [(i - x_min) / (x_max - x_min) for i in x]
x_rescaled = rescale(x)

x_rescaled
[0.7500, 0.0000, 0.5000, 1.0000, 0.5000]