Mathew K Analytics

Lesson 59 · Mastering Pandas

Volatility and Risk Analysis Training for Finance and Stock Market

Welcome! In this lesson, we will go beyond basic pandas and use real stock data. Explore, clean, and transform time series. Analyze performance and trends…

⬇ Download notebookOpen in Colab ↗

What you'll learn

Data

No separate download needed — the notebook creates or downloads everything it uses.

📓 Full notebook

Download .ipynb

Intermediate Pandas: Analysing Financial and Stock Market Data#

Welcome! In this lesson, we will go beyond basic pandas and use real stock data.

  • Explore, clean, and transform time series.
  • Analyze performance and trends with grouping and pivoting.
  • Visualize results for actionable insights.

We will use Apple Inc.'s daily historical stock prices.

Let us dive in!

import warnings
warnings.filterwarnings("ignore")

# Import libraries for data and visualization
import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(42)
# Data setup (Stock Market Data via Yahoo Finance)
df = yf.download('AAPL', start='2022-01-01', end='2023-12-31')
print(df.shape)
print(df.head(3))
[*********************100%***********************]  1 of 1 completed
(501, 5)
Price            Close        High         Low        Open     Volume
Ticker            AAPL        AAPL        AAPL        AAPL       AAPL
Date                                                                 
2022-01-03  178.443146  179.296107  174.227425  174.345068  104487900
2022-01-04  176.178406  179.354917  175.609770  179.050994   99310400
2022-01-05  171.492081  176.639196  171.217569  176.090173   94537600

Exploring Stock Data: Columns and Index#

When you work with financial data, it is key to understand your columns and the index.

  • The index here is the date.
  • Columns: Open, High, Low, Close, Adj Close, and Volume.

Let us learn how to inspect their names and types.

print(df.columns)
print(df.index)
print(df.info())
MultiIndex([( 'Close', 'AAPL'),
            (  'High', 'AAPL'),
            (   'Low', 'AAPL'),
            (  'Open', 'AAPL'),
            ('Volume', 'AAPL')],
           names=['Price', 'Ticker'])
DatetimeIndex(['2022-01-03', '2022-01-04', '2022-01-05', '2022-01-06',
               '2022-01-07', '2022-01-10', '2022-01-11', '2022-01-12',
               '2022-01-13', '2022-01-14',
               ...
               '2023-12-15', '2023-12-18', '2023-12-19', '2023-12-20',
               '2023-12-21', '2023-12-22', '2023-12-26', '2023-12-27',
               '2023-12-28', '2023-12-29'],
              dtype='datetime64[ns]', name='Date', length=501, freq=None)
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 501 entries, 2022-01-03 to 2023-12-29
Data columns (total 5 columns):
 #   Column          Non-Null Count  Dtype  
---  ------          --------------  -----  
 0   (Close, AAPL)   501 non-null    float64
 1   (High, AAPL)    501 non-null    float64
 2   (Low, AAPL)     501 non-null    float64
 3   (Open, AAPL)    501 non-null    float64
 4   (Volume, AAPL)  501 non-null    int64  
dtypes: float64(4), int64(1)
memory usage: 23.5 KB
None
# Check for missing values
print(df.isnull().sum())
Price   Ticker
Close   AAPL      0
High    AAPL      0
Low     AAPL      0
Open    AAPL      0
Volume  AAPL      0
dtype: int64
# Fill missing data with forward fill
df_filled = df.fillna(method='ffill')
print(df_filled.isnull().sum())
Price   Ticker
Close   AAPL      0
High    AAPL      0
Low     AAPL      0
Open    AAPL      0
Volume  AAPL      0
dtype: int64

Calculating Daily Returns#

In finance, we often want to know how much a stock changed each day.

This helps us spot periods of big losses or gains.

Let us compute daily percentage returns.

 
MultiIndex([( 'Close', 'AAPL'),
            (  'High', 'AAPL'),
            (   'Low', 'AAPL'),
            (  'Open', 'AAPL'),
            ('Volume', 'AAPL')],
           names=['Price', 'Ticker'])
# Add daily returns column
df_filled['Daily Return'] = df_filled['Adj Close'].pct_change() * 100
print(df_filled[['Adj Close', 'Daily Return']].head())
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:3812, in Index.get_loc(self, key)
   3811 try:
-> 3812     return self._engine.get_loc(casted_key)
   3813 except KeyError as err:

File pandas/_libs/index.pyx:167, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/index.pyx:196, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/hashtable_class_helper.pxi:7088, in pandas._libs.hashtable.PyObjectHashTable.get_item()

File pandas/_libs/hashtable_class_helper.pxi:7096, in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: 'Adj Close'

The above exception was the direct cause of the following exception:

KeyError                                  Traceback (most recent call last)
Cell In[6], line 2
      1 # Add daily returns column
----> 2 df_filled['Daily Return'] = df_filled['Adj Close'].pct_change() * 100
      3 print(df_filled[['Adj Close', 'Daily Return']].head())

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:4106, in DataFrame.__getitem__(self, key)
   4104 if is_single_key:
   4105     if self.columns.nlevels > 1:
-> 4106         return self._getitem_multilevel(key)
   4107     indexer = self.columns.get_loc(key)
   4108     if is_integer(indexer):

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:4164, in DataFrame._getitem_multilevel(self, key)
   4162 def _getitem_multilevel(self, key):
   4163     # self.columns is a MultiIndex
-> 4164     loc = self.columns.get_loc(key)
   4165     if isinstance(loc, (slice, np.ndarray)):
   4166         new_columns = self.columns[loc]

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:3059, in MultiIndex.get_loc(self, key)
   3056     return mask
   3058 if not isinstance(key, tuple):
-> 3059     loc = self._get_level_indexer(key, level=0)
   3060     return _maybe_to_slice(loc)
   3062 keylen = len(key)

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:3410, in MultiIndex._get_level_indexer(self, key, level, indexer)
   3407         return slice(i, j, step)
   3409 else:
-> 3410     idx = self._get_loc_single_level_index(level_index, key)
   3412     if level > 0 or self._lexsort_depth == 0:
   3413         # Desired level is not sorted
   3414         if isinstance(idx, slice):
   3415             # test_get_loc_partial_timestamp_multiindex

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:2999, in MultiIndex._get_loc_single_level_index(self, level_index, key)
   2997     return -1
   2998 else:
-> 2999     return level_index.get_loc(key)

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:3819, in Index.get_loc(self, key)
   3814     if isinstance(casted_key, slice) or (
   3815         isinstance(casted_key, abc.Iterable)
   3816         and any(isinstance(x, slice) for x in casted_key)
   3817     ):
   3818         raise InvalidIndexError(key)
-> 3819     raise KeyError(key) from err
   3820 except TypeError:
   3821     # If we have a listlike key, _check_indexing_error will raise
   3822     #  InvalidIndexError. Otherwise we fall through and re-raise
   3823     #  the TypeError.
   3824     self._check_indexing_error(key)

KeyError: 'Adj Close'
# Describe the returns
print(df_filled['Daily Return'].describe())
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:3812, in Index.get_loc(self, key)
   3811 try:
-> 3812     return self._engine.get_loc(casted_key)
   3813 except KeyError as err:

File pandas/_libs/index.pyx:167, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/index.pyx:196, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/hashtable_class_helper.pxi:7088, in pandas._libs.hashtable.PyObjectHashTable.get_item()

File pandas/_libs/hashtable_class_helper.pxi:7096, in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: 'Daily Return'

The above exception was the direct cause of the following exception:

KeyError                                  Traceback (most recent call last)
Cell In[7], line 2
      1 # Describe the returns
----> 2 print(df_filled['Daily Return'].describe())

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:4106, in DataFrame.__getitem__(self, key)
   4104 if is_single_key:
   4105     if self.columns.nlevels > 1:
-> 4106         return self._getitem_multilevel(key)
   4107     indexer = self.columns.get_loc(key)
   4108     if is_integer(indexer):

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:4164, in DataFrame._getitem_multilevel(self, key)
   4162 def _getitem_multilevel(self, key):
   4163     # self.columns is a MultiIndex
-> 4164     loc = self.columns.get_loc(key)
   4165     if isinstance(loc, (slice, np.ndarray)):
   4166         new_columns = self.columns[loc]

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:3059, in MultiIndex.get_loc(self, key)
   3056     return mask
   3058 if not isinstance(key, tuple):
-> 3059     loc = self._get_level_indexer(key, level=0)
   3060     return _maybe_to_slice(loc)
   3062 keylen = len(key)

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:3410, in MultiIndex._get_level_indexer(self, key, level, indexer)
   3407         return slice(i, j, step)
   3409 else:
-> 3410     idx = self._get_loc_single_level_index(level_index, key)
   3412     if level > 0 or self._lexsort_depth == 0:
   3413         # Desired level is not sorted
   3414         if isinstance(idx, slice):
   3415             # test_get_loc_partial_timestamp_multiindex

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:2999, in MultiIndex._get_loc_single_level_index(self, level_index, key)
   2997     return -1
   2998 else:
-> 2999     return level_index.get_loc(key)

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:3819, in Index.get_loc(self, key)
   3814     if isinstance(casted_key, slice) or (
   3815         isinstance(casted_key, abc.Iterable)
   3816         and any(isinstance(x, slice) for x in casted_key)
   3817     ):
   3818         raise InvalidIndexError(key)
-> 3819     raise KeyError(key) from err
   3820 except TypeError:
   3821     # If we have a listlike key, _check_indexing_error will raise
   3822     #  InvalidIndexError. Otherwise we fall through and re-raise
   3823     #  the TypeError.
   3824     self._check_indexing_error(key)

KeyError: 'Daily Return'

Visualizing the Time Series#

Charts make it simple to spot trends and volatility in financial markets.

Let us plot the closing price and daily returns.

df_filled['Adj Close'].plot(figsize=(12, 5), title='AAPL Adjusted Close (2022-2023)')
plt.ylabel('Price in USD')
plt.show()
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:3812, in Index.get_loc(self, key)
   3811 try:
-> 3812     return self._engine.get_loc(casted_key)
   3813 except KeyError as err:

File pandas/_libs/index.pyx:167, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/index.pyx:196, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/hashtable_class_helper.pxi:7088, in pandas._libs.hashtable.PyObjectHashTable.get_item()

File pandas/_libs/hashtable_class_helper.pxi:7096, in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: 'Adj Close'

The above exception was the direct cause of the following exception:

KeyError                                  Traceback (most recent call last)
Cell In[8], line 1
----> 1 df_filled['Adj Close'].plot(figsize=(12, 5), title='AAPL Adjusted Close (2022-2023)')
      2 plt.ylabel('Price in USD')
      3 plt.show()

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:4106, in DataFrame.__getitem__(self, key)
   4104 if is_single_key:
   4105     if self.columns.nlevels > 1:
-> 4106         return self._getitem_multilevel(key)
   4107     indexer = self.columns.get_loc(key)
   4108     if is_integer(indexer):

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:4164, in DataFrame._getitem_multilevel(self, key)
   4162 def _getitem_multilevel(self, key):
   4163     # self.columns is a MultiIndex
-> 4164     loc = self.columns.get_loc(key)
   4165     if isinstance(loc, (slice, np.ndarray)):
   4166         new_columns = self.columns[loc]

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:3059, in MultiIndex.get_loc(self, key)
   3056     return mask
   3058 if not isinstance(key, tuple):
-> 3059     loc = self._get_level_indexer(key, level=0)
   3060     return _maybe_to_slice(loc)
   3062 keylen = len(key)

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:3410, in MultiIndex._get_level_indexer(self, key, level, indexer)
   3407         return slice(i, j, step)
   3409 else:
-> 3410     idx = self._get_loc_single_level_index(level_index, key)
   3412     if level > 0 or self._lexsort_depth == 0:
   3413         # Desired level is not sorted
   3414         if isinstance(idx, slice):
   3415             # test_get_loc_partial_timestamp_multiindex

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:2999, in MultiIndex._get_loc_single_level_index(self, level_index, key)
   2997     return -1
   2998 else:
-> 2999     return level_index.get_loc(key)

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:3819, in Index.get_loc(self, key)
   3814     if isinstance(casted_key, slice) or (
   3815         isinstance(casted_key, abc.Iterable)
   3816         and any(isinstance(x, slice) for x in casted_key)
   3817     ):
   3818         raise InvalidIndexError(key)
-> 3819     raise KeyError(key) from err
   3820 except TypeError:
   3821     # If we have a listlike key, _check_indexing_error will raise
   3822     #  InvalidIndexError. Otherwise we fall through and re-raise
   3823     #  the TypeError.
   3824     self._check_indexing_error(key)

KeyError: 'Adj Close'
df_filled['Daily Return'].plot(kind='hist', bins=30, title='Histogram of Daily Returns', figsize=(8,4))
plt.xlabel('Daily Return (%)')
plt.show()
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:3812, in Index.get_loc(self, key)
   3811 try:
-> 3812     return self._engine.get_loc(casted_key)
   3813 except KeyError as err:

File pandas/_libs/index.pyx:167, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/index.pyx:196, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/hashtable_class_helper.pxi:7088, in pandas._libs.hashtable.PyObjectHashTable.get_item()

File pandas/_libs/hashtable_class_helper.pxi:7096, in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: 'Daily Return'

The above exception was the direct cause of the following exception:

KeyError                                  Traceback (most recent call last)
Cell In[9], line 1
----> 1 df_filled['Daily Return'].plot(kind='hist', bins=30, title='Histogram of Daily Returns', figsize=(8,4))
      2 plt.xlabel('Daily Return (%)')
      3 plt.show()

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:4106, in DataFrame.__getitem__(self, key)
   4104 if is_single_key:
   4105     if self.columns.nlevels > 1:
-> 4106         return self._getitem_multilevel(key)
   4107     indexer = self.columns.get_loc(key)
   4108     if is_integer(indexer):

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:4164, in DataFrame._getitem_multilevel(self, key)
   4162 def _getitem_multilevel(self, key):
   4163     # self.columns is a MultiIndex
-> 4164     loc = self.columns.get_loc(key)
   4165     if isinstance(loc, (slice, np.ndarray)):
   4166         new_columns = self.columns[loc]

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:3059, in MultiIndex.get_loc(self, key)
   3056     return mask
   3058 if not isinstance(key, tuple):
-> 3059     loc = self._get_level_indexer(key, level=0)
   3060     return _maybe_to_slice(loc)
   3062 keylen = len(key)

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:3410, in MultiIndex._get_level_indexer(self, key, level, indexer)
   3407         return slice(i, j, step)
   3409 else:
-> 3410     idx = self._get_loc_single_level_index(level_index, key)
   3412     if level > 0 or self._lexsort_depth == 0:
   3413         # Desired level is not sorted
   3414         if isinstance(idx, slice):
   3415             # test_get_loc_partial_timestamp_multiindex

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:2999, in MultiIndex._get_loc_single_level_index(self, level_index, key)
   2997     return -1
   2998 else:
-> 2999     return level_index.get_loc(key)

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:3819, in Index.get_loc(self, key)
   3814     if isinstance(casted_key, slice) or (
   3815         isinstance(casted_key, abc.Iterable)
   3816         and any(isinstance(x, slice) for x in casted_key)
   3817     ):
   3818         raise InvalidIndexError(key)
-> 3819     raise KeyError(key) from err
   3820 except TypeError:
   3821     # If we have a listlike key, _check_indexing_error will raise
   3822     #  InvalidIndexError. Otherwise we fall through and re-raise
   3823     #  the TypeError.
   3824     self._check_indexing_error(key)

KeyError: 'Daily Return'
# Calculate rolling 20-day mean and plot
df_filled['20D_MA'] = df_filled['Adj Close'].rolling(window=20).mean()
df_filled['Adj Close'].plot(figsize=(12,5), label='Adj Close')
df_filled['20D_MA'].plot(label='20-Day Moving Avg')
plt.legend()
plt.title('AAPL: Price and 20-Day Moving Average')
plt.show()
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:3812, in Index.get_loc(self, key)
   3811 try:
-> 3812     return self._engine.get_loc(casted_key)
   3813 except KeyError as err:

File pandas/_libs/index.pyx:167, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/index.pyx:196, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/hashtable_class_helper.pxi:7088, in pandas._libs.hashtable.PyObjectHashTable.get_item()

File pandas/_libs/hashtable_class_helper.pxi:7096, in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: 'Adj Close'

The above exception was the direct cause of the following exception:

KeyError                                  Traceback (most recent call last)
Cell In[10], line 2
      1 # Calculate rolling 20-day mean and plot
----> 2 df_filled['20D_MA'] = df_filled['Adj Close'].rolling(window=20).mean()
      3 df_filled['Adj Close'].plot(figsize=(12,5), label='Adj Close')
      4 df_filled['20D_MA'].plot(label='20-Day Moving Avg')

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:4106, in DataFrame.__getitem__(self, key)
   4104 if is_single_key:
   4105     if self.columns.nlevels > 1:
-> 4106         return self._getitem_multilevel(key)
   4107     indexer = self.columns.get_loc(key)
   4108     if is_integer(indexer):

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:4164, in DataFrame._getitem_multilevel(self, key)
   4162 def _getitem_multilevel(self, key):
   4163     # self.columns is a MultiIndex
-> 4164     loc = self.columns.get_loc(key)
   4165     if isinstance(loc, (slice, np.ndarray)):
   4166         new_columns = self.columns[loc]

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:3059, in MultiIndex.get_loc(self, key)
   3056     return mask
   3058 if not isinstance(key, tuple):
-> 3059     loc = self._get_level_indexer(key, level=0)
   3060     return _maybe_to_slice(loc)
   3062 keylen = len(key)

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:3410, in MultiIndex._get_level_indexer(self, key, level, indexer)
   3407         return slice(i, j, step)
   3409 else:
-> 3410     idx = self._get_loc_single_level_index(level_index, key)
   3412     if level > 0 or self._lexsort_depth == 0:
   3413         # Desired level is not sorted
   3414         if isinstance(idx, slice):
   3415             # test_get_loc_partial_timestamp_multiindex

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:2999, in MultiIndex._get_loc_single_level_index(self, level_index, key)
   2997     return -1
   2998 else:
-> 2999     return level_index.get_loc(key)

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:3819, in Index.get_loc(self, key)
   3814     if isinstance(casted_key, slice) or (
   3815         isinstance(casted_key, abc.Iterable)
   3816         and any(isinstance(x, slice) for x in casted_key)
   3817     ):
   3818         raise InvalidIndexError(key)
-> 3819     raise KeyError(key) from err
   3820 except TypeError:
   3821     # If we have a listlike key, _check_indexing_error will raise
   3822     #  InvalidIndexError. Otherwise we fall through and re-raise
   3823     #  the TypeError.
   3824     self._check_indexing_error(key)

KeyError: 'Adj Close'

Group and Resample: Monthly Analysis#

Raw daily data can be overwhelming. Resampling lets us zoom out and see the bigger monthly picture.

Let us check average adjusted close price for each month.

monthly_mean = df_filled['Adj Close'].resample('M').mean()
print(monthly_mean.head())
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:3812, in Index.get_loc(self, key)
   3811 try:
-> 3812     return self._engine.get_loc(casted_key)
   3813 except KeyError as err:

File pandas/_libs/index.pyx:167, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/index.pyx:196, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/hashtable_class_helper.pxi:7088, in pandas._libs.hashtable.PyObjectHashTable.get_item()

File pandas/_libs/hashtable_class_helper.pxi:7096, in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: 'Adj Close'

The above exception was the direct cause of the following exception:

KeyError                                  Traceback (most recent call last)
Cell In[11], line 1
----> 1 monthly_mean = df_filled['Adj Close'].resample('M').mean()
      2 print(monthly_mean.head())

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:4106, in DataFrame.__getitem__(self, key)
   4104 if is_single_key:
   4105     if self.columns.nlevels > 1:
-> 4106         return self._getitem_multilevel(key)
   4107     indexer = self.columns.get_loc(key)
   4108     if is_integer(indexer):

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:4164, in DataFrame._getitem_multilevel(self, key)
   4162 def _getitem_multilevel(self, key):
   4163     # self.columns is a MultiIndex
-> 4164     loc = self.columns.get_loc(key)
   4165     if isinstance(loc, (slice, np.ndarray)):
   4166         new_columns = self.columns[loc]

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:3059, in MultiIndex.get_loc(self, key)
   3056     return mask
   3058 if not isinstance(key, tuple):
-> 3059     loc = self._get_level_indexer(key, level=0)
   3060     return _maybe_to_slice(loc)
   3062 keylen = len(key)

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:3410, in MultiIndex._get_level_indexer(self, key, level, indexer)
   3407         return slice(i, j, step)
   3409 else:
-> 3410     idx = self._get_loc_single_level_index(level_index, key)
   3412     if level > 0 or self._lexsort_depth == 0:
   3413         # Desired level is not sorted
   3414         if isinstance(idx, slice):
   3415             # test_get_loc_partial_timestamp_multiindex

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:2999, in MultiIndex._get_loc_single_level_index(self, level_index, key)
   2997     return -1
   2998 else:
-> 2999     return level_index.get_loc(key)

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:3819, in Index.get_loc(self, key)
   3814     if isinstance(casted_key, slice) or (
   3815         isinstance(casted_key, abc.Iterable)
   3816         and any(isinstance(x, slice) for x in casted_key)
   3817     ):
   3818         raise InvalidIndexError(key)
-> 3819     raise KeyError(key) from err
   3820 except TypeError:
   3821     # If we have a listlike key, _check_indexing_error will raise
   3822     #  InvalidIndexError. Otherwise we fall through and re-raise
   3823     #  the TypeError.
   3824     self._check_indexing_error(key)

KeyError: 'Adj Close'
# Visualize monthly average price
monthly_mean.plot(marker='o', figsize=(10,4), title='AAPL Monthly Avg Close')
plt.ylabel('Price in USD')
plt.show()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[12], line 2
      1 # Visualize monthly average price
----> 2 monthly_mean.plot(marker='o', figsize=(10,4), title='AAPL Monthly Avg Close')
      3 plt.ylabel('Price in USD')
      4 plt.show()

NameError: name 'monthly_mean' is not defined
# Find the month with highest and lowest average price
best_month = monthly_mean.idxmax()
worst_month = monthly_mean.idxmin()
print(f'Highest average price: {monthly_mean.max():.2f} in {best_month.strftime("%B %Y")}' )
print(f'Lowest average price: {monthly_mean.min():.2f} in {worst_month.strftime("%B %Y")}' )
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[13], line 2
      1 # Find the month with highest and lowest average price
----> 2 best_month = monthly_mean.idxmax()
      3 worst_month = monthly_mean.idxmin()
      4 print(f'Highest average price: {monthly_mean.max():.2f} in {best_month.strftime("%B %Y")}' )

NameError: name 'monthly_mean' is not defined

Pivot Tables: Group by Weekday#

Do certain days of the week have higher returns than others?

Let us use a pivot table to compare daily returns by weekday.

df_filled['Weekday'] = df_filled.index.day_name()
weekday_returns = df_filled.pivot_table(values='Daily Return', index='Weekday', aggfunc='mean')
print(weekday_returns)
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[14], line 2
      1 df_filled['Weekday'] = df_filled.index.day_name()
----> 2 weekday_returns = df_filled.pivot_table(values='Daily Return', index='Weekday', aggfunc='mean')
      3 print(weekday_returns)

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:9516, in DataFrame.pivot_table(self, values, index, columns, aggfunc, fill_value, margins, dropna, margins_name, observed, sort)
   9499 @Substitution("")
   9500 @Appender(_shared_docs["pivot_table"])
   9501 def pivot_table(
   (...)
   9512     sort: bool = True,
   9513 ) -> DataFrame:
   9514     from pandas.core.reshape.pivot import pivot_table
-> 9516     return pivot_table(
   9517         self,
   9518         values=values,
   9519         index=index,
   9520         columns=columns,
   9521         aggfunc=aggfunc,
   9522         fill_value=fill_value,
   9523         margins=margins,
   9524         dropna=dropna,
   9525         margins_name=margins_name,
   9526         observed=observed,
   9527         sort=sort,
   9528     )

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\reshape\pivot.py:102, in pivot_table(data, values, index, columns, aggfunc, fill_value, margins, dropna, margins_name, observed, sort)
     99     table = concat(pieces, keys=keys, axis=1)
    100     return table.__finalize__(data, method="pivot_table")
--> 102 table = __internal_pivot_table(
    103     data,
    104     values,
    105     index,
    106     columns,
    107     aggfunc,
    108     fill_value,
    109     margins,
    110     dropna,
    111     margins_name,
    112     observed,
    113     sort,
    114 )
    115 return table.__finalize__(data, method="pivot_table")

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\reshape\pivot.py:148, in __internal_pivot_table(data, values, index, columns, aggfunc, fill_value, margins, dropna, margins_name, observed, sort)
    146 for i in values:
    147     if i not in data:
--> 148         raise KeyError(i)
    150 to_filter = []
    151 for x in keys + values:

KeyError: 'Daily Return'

Mini-Project: Volatility and Risk#

Volatility means how much a stock price changes day to day. It is a key risk measure in finance.

Let us calculate and plot the rolling 20-day standard deviation of daily returns.

This is a simple way to see when the stock was safest or riskiest.

df_filled['20D_Volatility'] = df_filled['Daily Return'].rolling(window=20).std()
df_filled['20D_Volatility'].plot(figsize=(12,5), title='AAPL 20-Day Rolling Volatility')
plt.ylabel('Volatility (%)')
plt.show()
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:3812, in Index.get_loc(self, key)
   3811 try:
-> 3812     return self._engine.get_loc(casted_key)
   3813 except KeyError as err:

File pandas/_libs/index.pyx:167, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/index.pyx:196, in pandas._libs.index.IndexEngine.get_loc()

File pandas/_libs/hashtable_class_helper.pxi:7088, in pandas._libs.hashtable.PyObjectHashTable.get_item()

File pandas/_libs/hashtable_class_helper.pxi:7096, in pandas._libs.hashtable.PyObjectHashTable.get_item()

KeyError: 'Daily Return'

The above exception was the direct cause of the following exception:

KeyError                                  Traceback (most recent call last)
Cell In[15], line 1
----> 1 df_filled['20D_Volatility'] = df_filled['Daily Return'].rolling(window=20).std()
      2 df_filled['20D_Volatility'].plot(figsize=(12,5), title='AAPL 20-Day Rolling Volatility')
      3 plt.ylabel('Volatility (%)')

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:4106, in DataFrame.__getitem__(self, key)
   4104 if is_single_key:
   4105     if self.columns.nlevels > 1:
-> 4106         return self._getitem_multilevel(key)
   4107     indexer = self.columns.get_loc(key)
   4108     if is_integer(indexer):

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\frame.py:4164, in DataFrame._getitem_multilevel(self, key)
   4162 def _getitem_multilevel(self, key):
   4163     # self.columns is a MultiIndex
-> 4164     loc = self.columns.get_loc(key)
   4165     if isinstance(loc, (slice, np.ndarray)):
   4166         new_columns = self.columns[loc]

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:3059, in MultiIndex.get_loc(self, key)
   3056     return mask
   3058 if not isinstance(key, tuple):
-> 3059     loc = self._get_level_indexer(key, level=0)
   3060     return _maybe_to_slice(loc)
   3062 keylen = len(key)

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:3410, in MultiIndex._get_level_indexer(self, key, level, indexer)
   3407         return slice(i, j, step)
   3409 else:
-> 3410     idx = self._get_loc_single_level_index(level_index, key)
   3412     if level > 0 or self._lexsort_depth == 0:
   3413         # Desired level is not sorted
   3414         if isinstance(idx, slice):
   3415             # test_get_loc_partial_timestamp_multiindex

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\multi.py:2999, in MultiIndex._get_loc_single_level_index(self, level_index, key)
   2997     return -1
   2998 else:
-> 2999     return level_index.get_loc(key)

File c:\Users\makmw\AppData\Local\Programs\Python\Python312\Lib\site-packages\pandas\core\indexes\base.py:3819, in Index.get_loc(self, key)
   3814     if isinstance(casted_key, slice) or (
   3815         isinstance(casted_key, abc.Iterable)
   3816         and any(isinstance(x, slice) for x in casted_key)
   3817     ):
   3818         raise InvalidIndexError(key)
-> 3819     raise KeyError(key) from err
   3820 except TypeError:
   3821     # If we have a listlike key, _check_indexing_error will raise
   3822     #  InvalidIndexError. Otherwise we fall through and re-raise
   3823     #  the TypeError.
   3824     self._check_indexing_error(key)

KeyError: 'Daily Return'
# Challenge: What if you want to compare with Nasdaq or Microsoft?
tick = input('Enter another stock ticker, like MSFT or ^IXIC: ')
df2 = yf.download(tick, start='2022-01-01', end='2023-12-31')
print(df2.head(3))
[*********************100%***********************]  1 of 1 completed
Price            Close        High         Low        Open    Volume
Ticker            MSFT        MSFT        MSFT        MSFT      MSFT
Date                                                                
2022-01-03  324.504517  327.655046  319.686629  325.086159  28865100
2022-01-04  318.940308  324.940858  316.138745  324.582158  32674300
2022-01-05  306.696838  316.090267  306.309087  315.886673  40054300

Best Practices & Troubleshooting#

  • Always check for missing values and data types up front.
  • Visualize early and often.
  • Document your workflow step by step.
  • If columns do not line up after merging, check for index mismatches.

Tip: If you hit import errors, try installing with: !pip install yfinance matplotlib

# CHALLENGE: Try these!
# 1. Calculate annual return for 2023 vs 2022.
# 2. Plot closing prices for Apple and your other stock together.

Recap: What Did We Learn?#

  • How to load and inspect real stock market data
  • How to fill missing values and calculate returns
  • How to group, resample, and visualize time series
  • How to analyze risk with rolling volatility
  • How to compare multiple stocks

Keep practicing with new tickers and time windows.

Thanks for Learning! Next Steps#

  • Try bringing in other companies or sectors for deeper comparisons.
  • Adapt your workflow for any time series: weather, sales, or even social media metrics.

Like, Subscribe, and Share if this helped you, and check out our channel for more Pandas and finance tutorials!

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.