McKinney Chapter 8 - Practice - Sec 04

FINA 6333 for Spring 2025

Author

Richard Herron

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import pandas_datareader as pdr
import yfinance as yf
%precision 4
pd.options.display.float_format = '{:.4f}'.format
# %config InlineBackend.figure_format = 'retina'

Announcements

  1. The deadline for forming project groups is Tuesday, 2/11. That evening, I will create random project groups from the unassigned students.
  2. 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.

  1. 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 Ticker and Date, or columns by Variable and Ticker.
  2. Combining Data: We will use three functions and methods to combine datasets on one or more keys. All three offer inner, outer, left, or right combinations.
    1. The pd.merge() function (or the .merge() method) provides the most flexible way to perform database-style joins on data frames.
    2. The .join() method combines data frames with similar indexes.
    3. The pd.concat() function combines similarly-shaped series and data frames.
  3. 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

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'].columns
Index(['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: Stacl stocks_wide!

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, make them long, then join them!

_ = stocks_wide['Adj Close'].iloc[:-1].pct_change().stack(future_stack=True).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_28432\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'])
Note

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:

  1. The .join() method left joins by default, so the combined output has only dates in stocks_long_2
  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

Note

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!

Download earnings per share for the stocks in stocks_long and combine to a long data frame earnings.

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.

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!