McKinney Chapter 3 - Practice - Blank

FINA 6333 for Spring 2025

Author

Richard Herron

The %precision magic makes it easy to round all float on print to 4 decimal places.

%precision 4
'%.4f'

Announcements

  1. Keep forming groups on Canvas under People in the left sidebar
  2. You must ask me for groups larger than four students

Five-Minute Review

List

A list is an ordered collection of objects that is changeable (mutable). You can create an empty list using either [] or list().

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

Python is zero-indexed!

my_list[0]
1
my_list[:3] # to get first 3 objects, :3
[1, 2, 3]
my_list[1:4] # to get next 3 objects from 1, go from 1 to 1+3 or 1:3
[2, 3, [1, 2, 3, [1, 2, 3]]]

Tuple

A tuple is similar to a list but un-changeable (immutable). You can create a tuple using parentheses () or the tuple() function.

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

Python is zero-indexed!

my_tuple[0]
1

Tuples are immutable, so they cannot be changed!

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

# # TypeError: 'tuple' object does not support item assignment

Dictionary

A dictionary is an ordered collection of key-value pairs that are changeable (mutable). You can create an empty dictionary using either {} or the dict() function.

my_dict = {'wb': 'Warren Buffett', 'sk': 'Seth Klarman'}
my_dict
{'wb': 'Warren Buffett', 'sk': 'Seth Klarman'}
  • The key can be anything hashable (string, integer, tuple), but I (almost) always make the key a string
  • The value can be any python object
my_dict['wb']
'Warren Buffett'
my_dict['pl'] = 'Peter Lynch'
my_dict
{'wb': 'Warren Buffett', 'sk': 'Seth Klarman', 'pl': 'Peter Lynch'}
my_dict[(0, 1, 2)] = {'ad': 'Another dictionary!'}
my_dict
{'wb': 'Warren Buffett',
 'sk': 'Seth Klarman',
 'pl': 'Peter Lynch',
 (0, 1, 2): {'ad': 'Another dictionary!'}}

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).

What if we wanted multiples of 3 or 5 from 1 to 25?

threes_fives = [i for i in range(1, 26) if (i%3==0) | (i%5==0)]
threes_fives
[3, 5, 6, 9, 10, 12, 15, 18, 20, 21, 24, 25]
threes_fives_2 = [print(i) for i in range(1, 26) if (i%3==0) | (i%5==0)]
threes_fives_2
3
5
6
9
10
12
15
18
20
21
24
25
[None, None, None, None, None, None, None, None, None, None, None, None]

Practice

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

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

More on f-strings!

F-strings offer a concise way to embed expressions inside string literals, using curly braces {}. Prefixed with f or F, these strings allow for easy formatting of variables, numbers, and expressions. For example:

name = "Alice"
print(f"Hello, {name}!")

This outputs “Hello, Alice!”. F-strings simplify complex formatting, making code more readable. For a deeper understanding and more examples: https://realpython.com/python-f-strings/


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

What is the output of the following code and why?

1, 1, 1 == (1, 1, 1)
(1, 1, False)

Without parentheses (), Python reads the final element in the tuple as 1 == (1, 1, 1), which is False. We can use parentheses () to force Python to do what we want!

(1, 1, 1) == (1, 1, 1)
True

For this example, we must use parentheses () to be unambiguous!

Create a list l1 of integers from 1 to 100.

l1 = list(range(1, 101))
l1[:5]
[1, 2, 3, 4, 5]

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

l1.index(60)
59
l2 = l1[59:48:-1]
l2
[60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50]
l2_alt_1 = l1[49:60]
l2_alt_1.reverse()
l2_alt_1
[60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50]
l2_alt_2 = l1[49:60]
l2_alt_2.sort(reverse=True)
l2_alt_2
[60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50]
l1[49:60][::-1]
[60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50]

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

l3 = list(range(1, 22, 2))
l3
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21]
l3_alt_1 = []
for i in range(1, 22):
    if i%2 != 0:
        l3_alt_1.append(i)        

l3_alt_1
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21]
l3_alt_2 = [i for i in range(1, 22) if i%2!=0]
l3_alt_2
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21]

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

l4 = [i**2 for i in range(1, 101)]
l4[:5]
[1, 4, 9, 16, 25]

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]
l5[:5]
[1, 9, 25, 49, 81]
l5_alt = [i**2 for i in range(1, 101, 2)]
l5_alt[:5]
[1, 9, 25, 49, 81]
l5 == l5_alt
True

Which one is faster?!

%timeit [i**2 for i in range(1, 101) if i%2!=0]
13.1 μs ± 4.81 μs per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
%timeit [i**2 for i in range(1, 101, 2)]
6.51 μs ± 771 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)

Premature optimization is the root of all evil

                            - Donald Knuth

Use a lambda function to sort strings by the last letter in each string.

strings = ['Pillsbury', 'Shubrick', 'Clemson', 'Hinson']
strings.sort()
strings
['Clemson', 'Hinson', 'Pillsbury', 'Shubrick']
len('Pillsbury')
9
strings.sort(key=len)
strings
['Hinson', 'Clemson', 'Shubrick', 'Pillsbury']
'Pillsbury'[-1]
'y'
strings.sort(key=lambda x: x[-1])
strings
['Shubrick', 'Hinson', 'Clemson', 'Pillsbury']

Given an integer array nums and an integer k, write a function to 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_klarge(nums, k):
    return sorted(nums)[-k]
get_klarge(nums=[3,2,1,5,6,4], k=2)
5
get_klarge(nums=[3,2,3,1,2,4,5,5,6], k=4)
4

The following code follows a bad practice and uses variables in the global scope instead of passing them as arguments or parameters of the functions.

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

Here is an extreme example how this practice can lead to non-deterministic and confusing results that depend on how many times a function has been run.

x = []
def confusing():
    x.append(len(x))
confusing()
confusing()
confusing()
x
[0, 1, 2]

Given an integer array nums and an integer k, write a function to 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_kfreq(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_kfreq(nums=[1,1,1,2,2,3], k=2)
[1, 2]

Test whether the given strings are palindromes.

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

def is_palindrome(x):
    return [_x == _x[::-1] for _x in x]
is_palindrome(["aba", "no"])
[True, False]
tickers = ["AAPL", "GOOG", "XOX", "XOM"]
is_palindrome(tickers)
[False, True, True, False]

Write a function calc_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]

Although loop counters are un-Pythonic, this calculation is the rare case where I found loop counters more clear.

def calc_returns(p, d):
    r =[]
    for i in range(1, len(p)):
        # the following print statement print r, p, and d values each iteration
        # print(f'r is {r}, p[i] is {p[i]}, p[i-1] is {p[i-1]}, d[i] is {d[i]},')
        r.append((p[i] + d[i] - p[i-1]) / p[i-1])
    return r
calc_returns(p=prices, d=dividends)
[0.5100, -0.3267, -0.4900, 1.0400, 0.5200, -0.3200, 0.5200]

We can do the same calculation without indexing! Instead, we can use zip() to simultaneously loop over prices, lagged prices, and dividends.

def calc_returns_zip(p, d):
    r = []
    for _p, _plag, _d in zip(p[1:], p[:-1], d[1:]):
        # the following print statement print r, p, and d values each iteration
        # print(f'r is {r}, _pis {_p}, _plag is {_plag}, _d is {_d},')
        r.append((_p + _d - _plag) / _plag)
    return r
calc_returns_zip(p=prices, d=dividends)
[0.5100, -0.3267, -0.4900, 1.0400, 0.5200, -0.3200, 0.5200]
calc_returns_zip(p=prices, d=dividends) == calc_returns_zip(p=prices, d=dividends)
True

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

def calc_returns_2(p, d):
    r =[]
    cg =[]
    dp =[]
    # r, cg, dp = [], [], [] # tuple unpacking is very Pythonic!
    for i in range(1, len(p)):
        r.append((p[i] + d[i] - p[i-1]) / p[i-1])
        cg.append((p[i] - p[i-1]) / p[i-1])
        dp.append(d[i] / p[i-1])

    return {'r':r, 'cg':cg, 'dp':dp}
calc_returns_2(p=prices, d=dividends)
{'r': [0.5100, -0.3267, -0.4900, 1.0400, 0.5200, -0.3200, 0.5200],
 'cg': [0.5000, -0.3333, -0.5000, 1.0000, 0.5000, -0.3333, 0.5000],
 'dp': [0.0100, 0.0067, 0.0100, 0.0400, 0.0200, 0.0133, 0.0200]}
calc_returns(p=prices, d=dividends) == calc_returns_2(p=prices, d=dividends)['r']
True

Write a function rescale() to 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]

nums = [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]
rescale(nums)
[0.7500, 0.0000, 0.5000, 1.0000, 0.5000]

Write a function calc_portval() that accepts a dictionary of prices and share holdings and returns the portfolio value

data = {
    "AAPL": (150.25, 10),  # (price, shares)
    "GOOGL": (2750.00, 2),
    "MSFT": (300.75, 5)
}
def calc_portval(data):
    portval = 0
    for p, n in data.values():
        portval += p * n
    return portval
calc_portval(data=data)
8506.2500