import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import pandas_datareader as pdr
import yfinance as yfMcKinney Chapter 8 - Practice - Sec 03
FINA 6333 for Spring 2025
%precision 4
pd.options.display.float_format = '{:.4f}'.format
# %config InlineBackend.figure_format = 'retina'Announcements
- The deadline for forming project groups is Tuesday, 2/11. That evening, I will create random project groups from the unassigned students.
- The deadline for proposing (and voting on) students’ choice topics is Tuesday, 2/25. That evening, I will finalize our schedule for the second half of the semester.
Five-Minute Review
Chapter 8 of McKinney covers 3 important topics.
- Hierarchical Indexing: Hierarchical indexes (or multi-indexes) organize data at multiple levels instead of just a flat, two-dimensional structure. They help us work with high-dimensional data in a low-dimensional form. For example, we can index rows by multiple levels like
TickerandDate, or columns byVariableandTicker. - Combining Data: We will use three functions and methods to combine datasets on one or more keys. All three offer
inner,outer,left, orrightcombinations.- The
pd.merge()function (or the.merge()method) provides the most flexible way to perform database-style joins on data frames. - The
.join()method combines data frames with similar indexes. - The
pd.concat()function combines similarly-shaped series and data frames.
- The
- Reshaping Data: We can reshape data to change its structure, such as pivoting from wide to long format or vice versa. We will most often use the
.stack()and.unstack()methods, which pivot columns to rows and rows to columns, respectively. Laster in the course we will learn about the.pivot()method for aggregating data and the.melt()method for more advanced reshaping.
Practice
Download data from Yahoo! Finance for BAC, C, GS, JPM, MS, and PNC and assign to data frame stocks_wide.
stocks_wide = yf.download(tickers='BAC, C, GS, JPM, MS, PNC', auto_adjust=False, progress=False)stocks_wide.tail()| Price | Adj Close | Close | ... | Open | Volume | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Ticker | BAC | C | GS | JPM | MS | PNC | BAC | C | GS | JPM | ... | GS | JPM | MS | PNC | BAC | C | GS | JPM | MS | PNC |
| Date | |||||||||||||||||||||
| 2025-02-24 | 44.4600 | 78.5400 | 623.0505 | 261.3400 | 129.9700 | 186.9600 | 44.4600 | 78.5400 | 626.1400 | 261.3400 | ... | 633.5100 | 265.4900 | 132.7600 | 188.8200 | 35512200 | 12830000.0000 | 3274100.0000 | 10372800.0000 | 7784500.0000 | 2738800.0000 |
| 2025-02-25 | 43.9400 | 78.1400 | 611.8759 | 257.4000 | 129.6000 | 186.4700 | 43.9400 | 78.1400 | 614.9100 | 257.4000 | ... | 628.5300 | 262.2300 | 131.0300 | 188.3200 | 38119000 | 14501400.0000 | 2885900.0000 | 9608400.0000 | 7458500.0000 | 1720600.0000 |
| 2025-02-26 | 43.9400 | 79.0700 | 614.7218 | 258.7900 | 131.0500 | 187.0200 | 43.9400 | 79.0700 | 617.7700 | 258.7900 | ... | 616.6700 | 257.1600 | 130.4100 | 187.1100 | 32251700 | 13115000.0000 | 2001200.0000 | 5943600.0000 | 5493600.0000 | 1324900.0000 |
| 2025-02-27 | 44.1200 | 78.8700 | 605.0000 | 259.0500 | 129.2400 | 188.5600 | 44.1200 | 78.8700 | 608.0000 | 259.0500 | ... | 617.5400 | 260.1800 | 131.8000 | 187.7800 | 28477700 | 8485900.0000 | 2396300.0000 | 8204400.0000 | 5538700.0000 | 1337900.0000 |
| 2025-02-28 | 46.1000 | 79.9500 | 622.2900 | 264.6500 | 133.1100 | 191.9200 | 46.1000 | 79.9500 | 622.2900 | 264.6500 | ... | 607.7900 | 260.7300 | 129.9000 | 190.1800 | 62607700 | 21229300.0000 | 3336100.0000 | 10464700.0000 | 7421500.0000 | 2358000.0000 |
5 rows × 36 columns
Reshape stocks_wide from wide to long with dates and tickers as row indexes and assign to data frame stocks_long.
We use the .stack() method to go from wider to longer, and the .unstack() method to go from long longer to wider. Note that we set future_stack=True to accep the future default arguments for .stack() and suppress the FutureWarning. A FutureWarning is not an error, just a warning about some expected change that could cause and error in the future.
stocks_long = stocks_wide.stack(future_stack=True)stocks_long.tail()| Price | Adj Close | Close | High | Low | Open | Volume | |
|---|---|---|---|---|---|---|---|
| Date | Ticker | ||||||
| 2025-02-28 | C | 79.9500 | 79.9500 | 79.9700 | 77.6100 | 79.2200 | 21229300.0000 |
| GS | 622.2900 | 622.2900 | 623.6500 | 604.0100 | 607.7900 | 3336100.0000 | |
| JPM | 264.6500 | 264.6500 | 264.8100 | 257.8900 | 260.7300 | 10464700.0000 | |
| MS | 133.1100 | 133.1100 | 133.4100 | 128.9900 | 129.9000 | 7421500.0000 | |
| PNC | 191.9200 | 191.9200 | 192.2300 | 188.6800 | 190.1800 | 2358000.0000 |
The .melt() methods can reshape data frames from wide to long. However, our data frame has a column multi-index, which makes .melt() difficult to use and .stack() a better option.
Add daily returns to both stocks_wide and stocks_long under the name Returns.
Hint: Use pd.MultiIndex() to create a multi index for the wide data frame stocks_wide.
stocks_wide['Adj Close'].columnsIndex(['BAC', 'C', 'GS', 'JPM', 'MS', 'PNC'], dtype='object', name='Ticker')
_ = pd.MultiIndex.from_product([['Returns'], stocks_wide['Adj Close'].columns])
stocks_wide[_] = (
stocks_wide
['Adj Close']
.iloc[:-1] # do not use mid-day Adj Close for returns calculation
.pct_change()
)stocks_wide.tail()| Price | Adj Close | Close | ... | Volume | Returns | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Ticker | BAC | C | GS | JPM | MS | PNC | BAC | C | GS | JPM | ... | GS | JPM | MS | PNC | BAC | C | GS | JPM | MS | PNC |
| Date | |||||||||||||||||||||
| 2025-02-24 | 44.4600 | 78.5400 | 623.0505 | 261.3400 | 129.9700 | 186.9600 | 44.4600 | 78.5400 | 626.1400 | 261.3400 | ... | 3274100.0000 | 10372800.0000 | 7784500.0000 | 2738800.0000 | -0.0078 | -0.0139 | 0.0009 | -0.0110 | -0.0131 | -0.0058 |
| 2025-02-25 | 43.9400 | 78.1400 | 611.8759 | 257.4000 | 129.6000 | 186.4700 | 43.9400 | 78.1400 | 614.9100 | 257.4000 | ... | 2885900.0000 | 9608400.0000 | 7458500.0000 | 1720600.0000 | -0.0117 | -0.0051 | -0.0179 | -0.0151 | -0.0028 | -0.0026 |
| 2025-02-26 | 43.9400 | 79.0700 | 614.7218 | 258.7900 | 131.0500 | 187.0200 | 43.9400 | 79.0700 | 617.7700 | 258.7900 | ... | 2001200.0000 | 5943600.0000 | 5493600.0000 | 1324900.0000 | 0.0000 | 0.0119 | 0.0047 | 0.0054 | 0.0112 | 0.0029 |
| 2025-02-27 | 44.1200 | 78.8700 | 605.0000 | 259.0500 | 129.2400 | 188.5600 | 44.1200 | 78.8700 | 608.0000 | 259.0500 | ... | 2396300.0000 | 8204400.0000 | 5538700.0000 | 1337900.0000 | 0.0041 | -0.0025 | -0.0158 | 0.0010 | -0.0138 | 0.0082 |
| 2025-02-28 | 46.1000 | 79.9500 | 622.2900 | 264.6500 | 133.1100 | 191.9200 | 46.1000 | 79.9500 | 622.2900 | 264.6500 | ... | 3336100.0000 | 10464700.0000 | 7421500.0000 | 2358000.0000 | NaN | NaN | NaN | NaN | NaN | NaN |
5 rows × 42 columns
To add returns to stocks_long we have two options. I prefer the first option, but I will present the second option to show an application of the .join() method. I will assign the results of these two options to stocks_long_1 and stocks_long_2 so we can keep the original stocks_long as-is.
Option 1: Make stocks_wide long!
stocks_long_1 = stocks_wide.stack(future_stack=True)Recall, we omitted returns for the most recent trading day, which could include a partial data return.
stocks_long_1.tail(12)| Price | Adj Close | Close | High | Low | Open | Volume | Returns | |
|---|---|---|---|---|---|---|---|---|
| Date | Ticker | |||||||
| 2025-02-27 | BAC | 44.1200 | 44.1200 | 44.7800 | 43.9400 | 44.0900 | 28477700.0000 | 0.0041 |
| C | 78.8700 | 78.8700 | 80.3700 | 78.6600 | 79.5600 | 8485900.0000 | -0.0025 | |
| GS | 605.0000 | 608.0000 | 625.2300 | 607.3100 | 617.5400 | 2396300.0000 | -0.0158 | |
| JPM | 259.0500 | 259.0500 | 263.6400 | 257.8600 | 260.1800 | 8204400.0000 | 0.0010 | |
| MS | 129.2400 | 129.2400 | 132.8700 | 128.8100 | 131.8000 | 5538700.0000 | -0.0138 | |
| PNC | 188.5600 | 188.5600 | 190.8400 | 187.4900 | 187.7800 | 1337900.0000 | 0.0082 | |
| 2025-02-28 | BAC | 46.1000 | 46.1000 | 46.2000 | 44.2000 | 44.3200 | 62607700.0000 | NaN |
| C | 79.9500 | 79.9500 | 79.9700 | 77.6100 | 79.2200 | 21229300.0000 | NaN | |
| GS | 622.2900 | 622.2900 | 623.6500 | 604.0100 | 607.7900 | 3336100.0000 | NaN | |
| JPM | 264.6500 | 264.6500 | 264.8100 | 257.8900 | 260.7300 | 10464700.0000 | NaN | |
| MS | 133.1100 | 133.1100 | 133.4100 | 128.9900 | 129.9000 | 7421500.0000 | NaN | |
| PNC | 191.9200 | 191.9200 | 192.2300 | 188.6800 | 190.1800 | 2358000.0000 | NaN |
Option 2: Calculate returns from stocks_wide, make them long, then .join() them to stocks_long!
_ = stocks_wide['Adj Close'].iloc[:-1].pct_change().stack().to_frame('Returns')
stocks_long_2 = stocks_long.join(_)Recall, we omitted returns for the most recent trading day, which could include a partial data return.
stocks_long_2.tail(12)| Adj Close | Close | High | Low | Open | Volume | Returns | ||
|---|---|---|---|---|---|---|---|---|
| Date | Ticker | |||||||
| 2025-02-27 | BAC | 44.1200 | 44.1200 | 44.7800 | 43.9400 | 44.0900 | 28477700.0000 | 0.0041 |
| C | 78.8700 | 78.8700 | 80.3700 | 78.6600 | 79.5600 | 8485900.0000 | -0.0025 | |
| GS | 605.0000 | 608.0000 | 625.2300 | 607.3100 | 617.5400 | 2396300.0000 | -0.0158 | |
| JPM | 259.0500 | 259.0500 | 263.6400 | 257.8600 | 260.1800 | 8204400.0000 | 0.0010 | |
| MS | 129.2400 | 129.2400 | 132.8700 | 128.8100 | 131.8000 | 5538700.0000 | -0.0138 | |
| PNC | 188.5600 | 188.5600 | 190.8400 | 187.4900 | 187.7800 | 1337900.0000 | 0.0082 | |
| 2025-02-28 | BAC | 46.1000 | 46.1000 | 46.2000 | 44.2000 | 44.3200 | 62607700.0000 | NaN |
| C | 79.9500 | 79.9500 | 79.9700 | 77.6100 | 79.2200 | 21229300.0000 | NaN | |
| GS | 622.2900 | 622.2900 | 623.6500 | 604.0100 | 607.7900 | 3336100.0000 | NaN | |
| JPM | 264.6500 | 264.6500 | 264.8100 | 257.8900 | 260.7300 | 10464700.0000 | NaN | |
| MS | 133.1100 | 133.1100 | 133.4100 | 128.9900 | 129.9000 | 7421500.0000 | NaN | |
| PNC | 191.9200 | 191.9200 | 192.2300 | 188.6800 | 190.1800 | 2358000.0000 | NaN |
We can test the equality of stocks_long_1 and stocks_long_2 most easily with the .equals() method.
stocks_long_1.equals(stocks_long_2)True
Download the daily benchmark return factors from Ken French’s data library.
Hint: Use the DataReader() function in the pandas-datareader package. We imported this package above with the pdr. prefix.
I often cannot remember the exact name for the daily factors. We can use the pdr.famafrench.get_available_datasets() to list all the data in Kenneth French’s data library.
pdr.famafrench.get_available_datasets()[:5]['F-F_Research_Data_Factors',
'F-F_Research_Data_Factors_weekly',
'F-F_Research_Data_Factors_daily',
'F-F_Research_Data_5_Factors_2x3',
'F-F_Research_Data_5_Factors_2x3_daily']
ff = pdr.DataReader(
name='F-F_Research_Data_Factors_daily',
data_source='famafrench',
start='1900'
)C:\Users\r.herron\AppData\Local\Temp\ipykernel_15448\875599436.py:1: FutureWarning: The argument 'date_parser' is deprecated and will be removed in a future version. Please use 'date_format' instead, or read your data in as 'object' dtype and then call 'to_datetime'.
ff = pdr.DataReader(
type(ff)dict
The daily factors only have one data frame (in the 0 key) and the data set description (in the DESCR key).
ff.keys()dict_keys([0, 'DESCR'])
Data from the Kenneth French data library are percent returns instead of decimal returns!
ff[0]| Mkt-RF | SMB | HML | RF | |
|---|---|---|---|---|
| Date | ||||
| 1926-07-01 | 0.1000 | -0.2500 | -0.2700 | 0.0090 |
| 1926-07-02 | 0.4500 | -0.3300 | -0.0600 | 0.0090 |
| 1926-07-06 | 0.1700 | 0.3000 | -0.3900 | 0.0090 |
| 1926-07-07 | 0.0900 | -0.5800 | 0.0200 | 0.0090 |
| 1926-07-08 | 0.2100 | -0.3800 | 0.1900 | 0.0090 |
| ... | ... | ... | ... | ... |
| 2024-12-24 | 1.1100 | -0.0900 | -0.0500 | 0.0170 |
| 2024-12-26 | 0.0200 | 1.0400 | -0.1900 | 0.0170 |
| 2024-12-27 | -1.1700 | -0.6600 | 0.5600 | 0.0170 |
| 2024-12-30 | -1.0900 | 0.1200 | 0.7400 | 0.0170 |
| 2024-12-31 | -0.4600 | 0.0000 | 0.7100 | 0.0170 |
25901 rows × 4 columns
print(ff['DESCR'])F-F Research Data Factors daily
-------------------------------
This file was created by CMPT_ME_BEME_RETS_DAILY using the 202412 CRSP database. The Tbill return is the simple daily rate that, over the number of trading days compounds to 1-month TBill rate. The 1-month TBill rate data until 202405 are from Ibbotson Associates. Starting from 202406, the 1-month TBill rate is from ICE BofA US 1-Month Treasury Bill Index. Copyright 2024 Eugene F. Fama and Kenneth R. French
0 : (25901 rows x 4 cols)
Add the daily benchmark return factors to stocks_wide and stocks_long.
Since both ff[0] and stocks_long_2 have date indexes, we can easily combine them with the .join() method.
ff[0].tail()| Mkt-RF | SMB | HML | RF | |
|---|---|---|---|---|
| Date | ||||
| 2024-12-24 | 1.1100 | -0.0900 | -0.0500 | 0.0170 |
| 2024-12-26 | 0.0200 | 1.0400 | -0.1900 | 0.0170 |
| 2024-12-27 | -1.1700 | -0.6600 | 0.5600 | 0.0170 |
| 2024-12-30 | -1.0900 | 0.1200 | 0.7400 | 0.0170 |
| 2024-12-31 | -0.4600 | 0.0000 | 0.7100 | 0.0170 |
stocks_long_2.tail(12)| Adj Close | Close | High | Low | Open | Volume | Returns | ||
|---|---|---|---|---|---|---|---|---|
| Date | Ticker | |||||||
| 2025-02-27 | BAC | 44.1200 | 44.1200 | 44.7800 | 43.9400 | 44.0900 | 28477700.0000 | 0.0041 |
| C | 78.8700 | 78.8700 | 80.3700 | 78.6600 | 79.5600 | 8485900.0000 | -0.0025 | |
| GS | 605.0000 | 608.0000 | 625.2300 | 607.3100 | 617.5400 | 2396300.0000 | -0.0158 | |
| JPM | 259.0500 | 259.0500 | 263.6400 | 257.8600 | 260.1800 | 8204400.0000 | 0.0010 | |
| MS | 129.2400 | 129.2400 | 132.8700 | 128.8100 | 131.8000 | 5538700.0000 | -0.0138 | |
| PNC | 188.5600 | 188.5600 | 190.8400 | 187.4900 | 187.7800 | 1337900.0000 | 0.0082 | |
| 2025-02-28 | BAC | 46.1000 | 46.1000 | 46.2000 | 44.2000 | 44.3200 | 62607700.0000 | NaN |
| C | 79.9500 | 79.9500 | 79.9700 | 77.6100 | 79.2200 | 21229300.0000 | NaN | |
| GS | 622.2900 | 622.2900 | 623.6500 | 604.0100 | 607.7900 | 3336100.0000 | NaN | |
| JPM | 264.6500 | 264.6500 | 264.8100 | 257.8900 | 260.7300 | 10464700.0000 | NaN | |
| MS | 133.1100 | 133.1100 | 133.4100 | 128.9900 | 129.9000 | 7421500.0000 | NaN | |
| PNC | 191.9200 | 191.9200 | 192.2300 | 188.6800 | 190.1800 | 2358000.0000 | NaN |
We can quickly combine stocks_long_2 and ff[0] because both have indexes with daily dates named Date. Two notes:
- The
.join()method left joins by default, so the combined output has only dates instocks_long_2 - Kenneth French provides percent returns, so we divide them by 100 to convert them to decimal returns to match our Yahoo! Finance data
stocks_long_2.join(ff[0].div(100))| Adj Close | Close | High | Low | Open | Volume | Returns | Mkt-RF | SMB | HML | RF | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Date | Ticker | |||||||||||
| 1973-02-21 | BAC | 1.5426 | 4.6250 | 4.6250 | 4.6250 | 4.6250 | 99200.0000 | NaN | -0.0074 | -0.0039 | 0.0054 | 0.0002 |
| C | NaN | NaN | NaN | NaN | NaN | NaN | NaN | -0.0074 | -0.0039 | 0.0054 | 0.0002 | |
| GS | NaN | NaN | NaN | NaN | NaN | NaN | NaN | -0.0074 | -0.0039 | 0.0054 | 0.0002 | |
| JPM | NaN | NaN | NaN | NaN | NaN | NaN | NaN | -0.0074 | -0.0039 | 0.0054 | 0.0002 | |
| MS | NaN | NaN | NaN | NaN | NaN | NaN | NaN | -0.0074 | -0.0039 | 0.0054 | 0.0002 | |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2025-02-28 | C | 79.9500 | 79.9500 | 79.9700 | 77.6100 | 79.2200 | 21229300.0000 | NaN | NaN | NaN | NaN | NaN |
| GS | 622.2900 | 622.2900 | 623.6500 | 604.0100 | 607.7900 | 3336100.0000 | NaN | NaN | NaN | NaN | NaN | |
| JPM | 264.6500 | 264.6500 | 264.8100 | 257.8900 | 260.7300 | 10464700.0000 | NaN | NaN | NaN | NaN | NaN | |
| MS | 133.1100 | 133.1100 | 133.4100 | 128.9900 | 129.9000 | 7421500.0000 | NaN | NaN | NaN | NaN | NaN | |
| PNC | 191.9200 | 191.9200 | 192.2300 | 188.6800 | 190.1800 | 2358000.0000 | NaN | NaN | NaN | NaN | NaN |
78708 rows × 11 columns
We could instead convert the Yahoo! Finance decimal returns to percent returns. I do not have a strong preference on all decimal returns or all percent returns, but all returns should have the same form.
(
stocks_long_2
.assign(Returns=lambda x: 100 * x['Returns'])
.join(ff[0])
)| Adj Close | Close | High | Low | Open | Volume | Returns | Mkt-RF | SMB | HML | RF | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Date | Ticker | |||||||||||
| 1973-02-21 | BAC | 1.5426 | 4.6250 | 4.6250 | 4.6250 | 4.6250 | 99200.0000 | NaN | -0.7400 | -0.3900 | 0.5400 | 0.0220 |
| C | NaN | NaN | NaN | NaN | NaN | NaN | NaN | -0.7400 | -0.3900 | 0.5400 | 0.0220 | |
| GS | NaN | NaN | NaN | NaN | NaN | NaN | NaN | -0.7400 | -0.3900 | 0.5400 | 0.0220 | |
| JPM | NaN | NaN | NaN | NaN | NaN | NaN | NaN | -0.7400 | -0.3900 | 0.5400 | 0.0220 | |
| MS | NaN | NaN | NaN | NaN | NaN | NaN | NaN | -0.7400 | -0.3900 | 0.5400 | 0.0220 | |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2025-02-28 | C | 79.9500 | 79.9500 | 79.9700 | 77.6100 | 79.2200 | 21229300.0000 | NaN | NaN | NaN | NaN | NaN |
| GS | 622.2900 | 622.2900 | 623.6500 | 604.0100 | 607.7900 | 3336100.0000 | NaN | NaN | NaN | NaN | NaN | |
| JPM | 264.6500 | 264.6500 | 264.8100 | 257.8900 | 260.7300 | 10464700.0000 | NaN | NaN | NaN | NaN | NaN | |
| MS | 133.1100 | 133.1100 | 133.4100 | 128.9900 | 129.9000 | 7421500.0000 | NaN | NaN | NaN | NaN | NaN | |
| PNC | 191.9200 | 191.9200 | 192.2300 | 188.6800 | 190.1800 | 2358000.0000 | NaN | NaN | NaN | NaN | NaN |
78708 rows × 11 columns
With stocks_wide, we have to do a little more work becuase of its column multi-index! We will use the pd.MultiIndex.from_product() trick from above.
_ = pd.MultiIndex.from_product([['Factors'], ff[0].columns])
stocks_wide[_] = ff[0].div(100)stocks_wide.loc[:'2024'].tail()| Price | Adj Close | Close | ... | Returns | Factors | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Ticker | BAC | C | GS | JPM | MS | PNC | BAC | C | GS | JPM | ... | BAC | C | GS | JPM | MS | PNC | Mkt-RF | SMB | HML | RF |
| Date | |||||||||||||||||||||
| 2024-12-24 | 44.3800 | 70.5117 | 579.9144 | 241.0650 | 126.2201 | 192.4933 | 44.3800 | 71.0000 | 582.7900 | 242.3100 | ... | 0.0112 | 0.0176 | 0.0210 | 0.0164 | 0.0210 | 0.0050 | 0.0111 | -0.0009 | -0.0005 | 0.0002 |
| 2024-12-26 | 44.5500 | 70.8593 | 578.3621 | 241.8907 | 127.1837 | 193.1777 | 44.5500 | 71.3500 | 581.2300 | 243.1400 | ... | 0.0038 | 0.0049 | -0.0027 | 0.0034 | 0.0076 | 0.0036 | 0.0002 | 0.0104 | -0.0019 | 0.0002 |
| 2024-12-27 | 44.3400 | 70.5117 | 573.3370 | 239.9308 | 125.9221 | 191.6999 | 44.3400 | 71.0000 | 576.1800 | 241.1700 | ... | -0.0047 | -0.0049 | -0.0087 | -0.0081 | -0.0099 | -0.0077 | -0.0117 | -0.0066 | 0.0056 | 0.0002 |
| 2024-12-30 | 43.9100 | 69.9059 | 570.7200 | 238.0904 | 124.9188 | 190.9560 | 43.9100 | 70.3900 | 573.5500 | 239.3200 | ... | -0.0097 | -0.0086 | -0.0046 | -0.0077 | -0.0080 | -0.0039 | -0.0109 | 0.0012 | 0.0074 | 0.0002 |
| 2024-12-31 | 43.9500 | 69.9059 | 569.7946 | 238.4783 | 124.8890 | 191.2734 | 43.9500 | 70.3900 | 572.6200 | 239.7100 | ... | 0.0009 | 0.0000 | -0.0016 | 0.0016 | -0.0002 | 0.0017 | -0.0046 | 0.0000 | 0.0071 | 0.0002 |
5 rows × 46 columns
Write a function download() that accepts tickers and returns a wide data frame of returns with the daily benchmark return factors.
We can even add a shape argument to return a wide or long data frame!
We can even add a shape argument to return a wide or long data frame!
import warnings
def download(tickers, shape='wide'):
"""
Download stock price data and Fama-French factors, returning in either 'wide' or 'long' format.
Parameters:
- tickers (str or list of str): Stock ticker(s) to download.
- shape (str): Output format, either 'wide' (default) or 'long'.
Returns:
- pd.DataFrame: A DataFrame containing stock prices, returns, and Fama-French factors.
"""
# shape must be wide or long
if shape not in ['wide', 'long']:
raise ValueError('Invalid shape: must be "wide" or "long".')
# Download stock data
stocks = yf.download(tickers=tickers, auto_adjust=False, progress=False)
# Download Fama-French factors
# (suppressing FutureWarning for 'date_parser')
with warnings.catch_warnings():
warnings.simplefilter('ignore', category=FutureWarning)
factors = pdr.DataReader(
name='F-F_Research_Data_Factors_daily',
data_source='famafrench',
start='1900'
)[0].div(100) # Convert percentages to decimals
# Multi-index case
if isinstance(stocks.columns, pd.MultiIndex):
# Compute daily returns
_ = pd.MultiIndex.from_product([['Returns'], stocks['Adj Close'].columns])
stocks[_] = stocks['Adj Close'].pct_change()
if shape == 'wide':
# Add factors with multi-index
_ = pd.MultiIndex.from_product([['Factors'], factors.columns])
stocks[_] = factors
return stocks
# Convert to long format then add factors
else:
return stocks.stack(future_stack=True).join(factors)
# Single index case
# (redundant with recent versions of yfinance that always return a multi-index)
stocks['Returns'] = stocks['Adj Close'].pct_change()
return stocks.join(factors)download(tickers='AAPL TSLA')| Price | Adj Close | Close | High | Low | Open | Volume | Returns | Factors | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Ticker | AAPL | TSLA | AAPL | TSLA | AAPL | TSLA | AAPL | TSLA | AAPL | TSLA | AAPL | TSLA | AAPL | TSLA | Mkt-RF | SMB | HML | RF |
| Date | ||||||||||||||||||
| 1980-12-12 | 0.0987 | NaN | 0.1283 | NaN | 0.1289 | NaN | 0.1283 | NaN | 0.1283 | NaN | 469033600 | NaN | NaN | NaN | 0.0138 | -0.0001 | -0.0105 | 0.0006 |
| 1980-12-15 | 0.0936 | NaN | 0.1217 | NaN | 0.1222 | NaN | 0.1217 | NaN | 0.1222 | NaN | 175884800 | NaN | -0.0522 | NaN | 0.0011 | 0.0025 | -0.0046 | 0.0006 |
| 1980-12-16 | 0.0867 | NaN | 0.1127 | NaN | 0.1133 | NaN | 0.1127 | NaN | 0.1133 | NaN | 105728000 | NaN | -0.0734 | NaN | 0.0071 | -0.0075 | -0.0047 | 0.0006 |
| 1980-12-17 | 0.0889 | NaN | 0.1155 | NaN | 0.1161 | NaN | 0.1155 | NaN | 0.1155 | NaN | 86441600 | NaN | 0.0248 | NaN | 0.0152 | -0.0086 | -0.0034 | 0.0006 |
| 1980-12-18 | 0.0914 | NaN | 0.1189 | NaN | 0.1194 | NaN | 0.1189 | NaN | 0.1189 | NaN | 73449600 | NaN | 0.0290 | NaN | 0.0041 | 0.0022 | 0.0126 | 0.0006 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2025-02-24 | 247.1000 | 330.5300 | 247.1000 | 330.5300 | 248.8600 | 342.4000 | 244.4200 | 324.7000 | 244.9300 | 338.1400 | 51326400 | 76052300.0000 | 0.0063 | -0.0215 | NaN | NaN | NaN | NaN |
| 2025-02-25 | 247.0400 | 302.8000 | 247.0400 | 302.8000 | 250.0000 | 328.8900 | 244.9100 | 297.2500 | 248.0000 | 327.0200 | 48013300 | 134228800.0000 | -0.0002 | -0.0839 | NaN | NaN | NaN | NaN |
| 2025-02-26 | 240.3600 | 290.8000 | 240.3600 | 290.8000 | 244.9800 | 309.0000 | 239.1300 | 288.0400 | 244.3300 | 303.7100 | 44433600 | 100118300.0000 | -0.0270 | -0.0396 | NaN | NaN | NaN | NaN |
| 2025-02-27 | 237.3000 | 281.9500 | 237.3000 | 281.9500 | 242.4600 | 297.2300 | 237.0600 | 280.8800 | 239.4100 | 291.1600 | 41153600 | 101748200.0000 | -0.0127 | -0.0304 | NaN | NaN | NaN | NaN |
| 2025-02-28 | 241.8400 | 292.9800 | 241.8400 | 292.9800 | 242.0900 | 293.8800 | 230.2000 | 273.6000 | 236.9500 | 279.5000 | 56796200 | 115397200.0000 | 0.0191 | 0.0391 | NaN | NaN | NaN | NaN |
11144 rows × 18 columns
The yfinance package is a powerful tool for downloading market data, financial statements, and analyst estimates from Yahoo! Finance.
However, because yfinance relies on Yahoo! Finance’s API, changes to the API can disrupt its functionality.
Recently, Yahoo! Finance changed API access to earnings forecasts and announcement dates, so we cannot complete the earnings announcement exercise I had planned.
Instead, I will prepare an alternative set of exercises for us to work on in class on Friday.
Thank you for your flexibility!
Combine earnings with the returns from stocks_long.
Use the .earnings_dates method described here. Use pd.concat() to combine the result of each the .earnings_date data frames and assign them to a new data frame earnings. Name the row indexes Ticker and Date and swap to match the order of the row index in stocks_long.
Plot the relation between daily returns and earnings surprises
Repeat the earnings exercise with the S&P 100 stocks
With more data, we can more clearly see the positive relation between earnings surprises and returns!