import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.dates as mdate # custom tick marks for time series
import seaborn as sns
import yfinance as yf
import plotly.graph_objects as go
import plotly.express as px
Defining the function to clean the stock data¶
def clean_stock_data(df):
df.reset_index(inplace=True)
df.columns = df.columns.to_flat_index()
df.columns = ["_".join(col) for col in df.columns]
df.rename(columns={"Date_": "Date"}, inplace=True)
df["Date"] = pd.to_datetime(df["Date"])
return df
Pull stock data¶
# pull stock data for Crowdstrike, Fortinet, Palo alto network, zscaler and from S&P500 and Nasdaq indices
stock_data = yf.download("CRWD FTNT PANW ZS ^GSPC ^IXIC", start="2022-01-01", end="2026-02-20")
stock_data.head()
[*********************100%***********************] 6 of 6 completed
| Price | Close | High | ... | Open | Volume | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Ticker | CRWD | FTNT | PANW | ZS | ^GSPC | ^IXIC | CRWD | FTNT | PANW | ZS | ... | PANW | ZS | ^GSPC | ^IXIC | CRWD | FTNT | PANW | ZS | ^GSPC | ^IXIC |
| Date | |||||||||||||||||||||
| 2022-01-03 | 198.330002 | 66.624001 | 90.701668 | 301.829987 | 4796.560059 | 15832.799805 | 205.574997 | 72.424004 | 93.400002 | 323.549988 | ... | 93.073334 | 322.600006 | 4778.140137 | 15732.500000 | 5311600 | 9515000 | 7802400 | 2576000 | 3831020000 | 4429960000 |
| 2022-01-04 | 189.190002 | 64.428001 | 87.333336 | 281.579987 | 4793.540039 | 15622.719727 | 197.445007 | 66.683998 | 90.721664 | 298.190002 | ... | 89.574997 | 297.964996 | 4804.509766 | 15852.139648 | 6503400 | 9829000 | 9586200 | 3827700 | 4683170000 | 5131110000 |
| 2022-01-05 | 179.289993 | 60.698002 | 84.143333 | 262.329987 | 4700.580078 | 15100.169922 | 191.360001 | 63.939999 | 86.748337 | 280.369995 | ... | 85.541664 | 275.010010 | 4787.990234 | 15547.160156 | 5003400 | 12033000 | 7494000 | 3437700 | 4887960000 | 5031850000 |
| 2022-01-06 | 187.490005 | 62.799999 | 88.818336 | 260.209991 | 4696.049805 | 15080.860352 | 192.169998 | 63.933998 | 90.166664 | 266.320007 | ... | 83.500000 | 261.000000 | 4693.390137 | 15024.150391 | 6368600 | 12004000 | 16553400 | 2439600 | 4295280000 | 4790820000 |
| 2022-01-07 | 188.990005 | 62.931999 | 87.581665 | 261.529999 | 4677.029785 | 14935.900391 | 193.270004 | 63.966000 | 91.366669 | 268.359985 | ... | 87.809998 | 258.380005 | 4697.660156 | 15095.719727 | 3531900 | 8805000 | 13586400 | 1696000 | 4181510000 | 4238070000 |
5 rows × 30 columns
Clean and Download data¶
# download stock data
stock_data = clean_stock_data(stock_data)
stock_data.to_csv('stocks.csv', index=False)
stock_data
| Date | Close_CRWD | Close_FTNT | Close_PANW | Close_ZS | Close_^GSPC | Close_^IXIC | High_CRWD | High_FTNT | High_PANW | ... | Open_PANW | Open_ZS | Open_^GSPC | Open_^IXIC | Volume_CRWD | Volume_FTNT | Volume_PANW | Volume_ZS | Volume_^GSPC | Volume_^IXIC | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2022-01-03 | 198.330002 | 66.624001 | 90.701668 | 301.829987 | 4796.560059 | 15832.799805 | 205.574997 | 72.424004 | 93.400002 | ... | 93.073334 | 322.600006 | 4778.140137 | 15732.500000 | 5311600 | 9515000 | 7802400 | 2576000 | 3831020000 | 4429960000 |
| 1 | 2022-01-04 | 189.190002 | 64.428001 | 87.333336 | 281.579987 | 4793.540039 | 15622.719727 | 197.445007 | 66.683998 | 90.721664 | ... | 89.574997 | 297.964996 | 4804.509766 | 15852.139648 | 6503400 | 9829000 | 9586200 | 3827700 | 4683170000 | 5131110000 |
| 2 | 2022-01-05 | 179.289993 | 60.698002 | 84.143333 | 262.329987 | 4700.580078 | 15100.169922 | 191.360001 | 63.939999 | 86.748337 | ... | 85.541664 | 275.010010 | 4787.990234 | 15547.160156 | 5003400 | 12033000 | 7494000 | 3437700 | 4887960000 | 5031850000 |
| 3 | 2022-01-06 | 187.490005 | 62.799999 | 88.818336 | 260.209991 | 4696.049805 | 15080.860352 | 192.169998 | 63.933998 | 90.166664 | ... | 83.500000 | 261.000000 | 4693.390137 | 15024.150391 | 6368600 | 12004000 | 16553400 | 2439600 | 4295280000 | 4790820000 |
| 4 | 2022-01-07 | 188.990005 | 62.931999 | 87.581665 | 261.529999 | 4677.029785 | 14935.900391 | 193.270004 | 63.966000 | 91.366669 | ... | 87.809998 | 258.380005 | 4697.660156 | 15095.719727 | 3531900 | 8805000 | 13586400 | 1696000 | 4181510000 | 4238070000 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 1031 | 2026-02-12 | 411.540009 | 84.260002 | 162.809998 | 170.899994 | 6832.759766 | 22597.150391 | 420.670013 | 88.599998 | 167.949997 | ... | 166.289993 | 171.990005 | 6957.540039 | 23142.869141 | 3502500 | 7805800 | 19646500 | 2499300 | 7118700000 | 8855290000 |
| 1032 | 2026-02-13 | 429.640015 | 85.559998 | 166.949997 | 177.720001 | 6836.169922 | 22546.669922 | 432.850006 | 86.529999 | 170.490005 | ... | 165.029999 | 174.339996 | 6834.270020 | 22561.460938 | 3536700 | 8091300 | 12577000 | 2434100 | 5718360000 | 7964830000 |
| 1033 | 2026-02-17 | 414.285004 | 81.690002 | 163.500000 | 172.589996 | 6843.220215 | 22578.380859 | 425.881989 | 85.419998 | 166.009995 | ... | 165.679993 | 175.824997 | 6819.859863 | 22394.759766 | 4428100 | 6061100 | 16586500 | 2038100 | 5418480000 | 7654300000 |
| 1034 | 2026-02-18 | 415.760010 | 80.690002 | 152.350006 | 172.130005 | 6881.310059 | 22753.630859 | 421.760010 | 81.260002 | 155.279999 | ... | 149.550003 | 167.345001 | 6855.479980 | 22629.849609 | 2752700 | 5853300 | 26756800 | 2542100 | 5098160000 | 7705590000 |
| 1035 | 2026-02-19 | 422.140015 | 81.669998 | 150.990005 | 168.990005 | 6861.890137 | 22682.730469 | 424.000000 | 81.769997 | 152.679993 | ... | 152.479996 | 169.639999 | 6861.339844 | 22639.880859 | 2000400 | 4584100 | 13545800 | 3337100 | 5151690000 | 6984780000 |
1036 rows × 31 columns
Read Stock Data from CSV¶
# Reads stock csv and parses Date column type from string to datetime64 since excel removes type
stock_data = pd.read_csv('./stocks.csv', parse_dates=['Date'])
stock_data.head()
| Date | Close_CRWD | Close_FTNT | Close_PANW | Close_ZS | Close_^GSPC | Close_^IXIC | High_CRWD | High_FTNT | High_PANW | ... | Open_PANW | Open_ZS | Open_^GSPC | Open_^IXIC | Volume_CRWD | Volume_FTNT | Volume_PANW | Volume_ZS | Volume_^GSPC | Volume_^IXIC | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2022-01-03 | 198.330002 | 66.624001 | 90.701668 | 301.829987 | 4796.560059 | 15832.799805 | 205.574997 | 72.424004 | 93.400002 | ... | 93.073334 | 322.600006 | 4778.140137 | 15732.500000 | 5311600 | 9515000 | 7802400 | 2576000 | 3831020000 | 4429960000 |
| 1 | 2022-01-04 | 189.190002 | 64.428001 | 87.333336 | 281.579987 | 4793.540039 | 15622.719727 | 197.445007 | 66.683998 | 90.721664 | ... | 89.574997 | 297.964996 | 4804.509766 | 15852.139648 | 6503400 | 9829000 | 9586200 | 3827700 | 4683170000 | 5131110000 |
| 2 | 2022-01-05 | 179.289993 | 60.698002 | 84.143333 | 262.329987 | 4700.580078 | 15100.169922 | 191.360001 | 63.939999 | 86.748337 | ... | 85.541664 | 275.010010 | 4787.990234 | 15547.160156 | 5003400 | 12033000 | 7494000 | 3437700 | 4887960000 | 5031850000 |
| 3 | 2022-01-06 | 187.490005 | 62.799999 | 88.818336 | 260.209991 | 4696.049805 | 15080.860352 | 192.169998 | 63.933998 | 90.166664 | ... | 83.500000 | 261.000000 | 4693.390137 | 15024.150391 | 6368600 | 12004000 | 16553400 | 2439600 | 4295280000 | 4790820000 |
| 4 | 2022-01-07 | 188.990005 | 62.931999 | 87.581665 | 261.529999 | 4677.029785 | 14935.900391 | 193.270004 | 63.966000 | 91.366669 | ... | 87.809998 | 258.380005 | 4697.660156 | 15095.719727 | 3531900 | 8805000 | 13586400 | 1696000 | 4181510000 | 4238070000 |
5 rows × 31 columns
stock_data.info()
<class 'pandas.DataFrame'> RangeIndex: 1036 entries, 0 to 1035 Data columns (total 31 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Date 1036 non-null datetime64[us] 1 Close_CRWD 1036 non-null float64 2 Close_FTNT 1036 non-null float64 3 Close_PANW 1036 non-null float64 4 Close_ZS 1036 non-null float64 5 Close_^GSPC 1036 non-null float64 6 Close_^IXIC 1036 non-null float64 7 High_CRWD 1036 non-null float64 8 High_FTNT 1036 non-null float64 9 High_PANW 1036 non-null float64 10 High_ZS 1036 non-null float64 11 High_^GSPC 1036 non-null float64 12 High_^IXIC 1036 non-null float64 13 Low_CRWD 1036 non-null float64 14 Low_FTNT 1036 non-null float64 15 Low_PANW 1036 non-null float64 16 Low_ZS 1036 non-null float64 17 Low_^GSPC 1036 non-null float64 18 Low_^IXIC 1036 non-null float64 19 Open_CRWD 1036 non-null float64 20 Open_FTNT 1036 non-null float64 21 Open_PANW 1036 non-null float64 22 Open_ZS 1036 non-null float64 23 Open_^GSPC 1036 non-null float64 24 Open_^IXIC 1036 non-null float64 25 Volume_CRWD 1036 non-null int64 26 Volume_FTNT 1036 non-null int64 27 Volume_PANW 1036 non-null int64 28 Volume_ZS 1036 non-null int64 29 Volume_^GSPC 1036 non-null int64 30 Volume_^IXIC 1036 non-null int64 dtypes: datetime64[us](1), float64(24), int64(6) memory usage: 251.0 KB
Download Ticker data for Crowdstrike, Fortinet, Palo Alto networks, and zscaler¶
crowdstrike_ticker = yf.Ticker("CRWD")
fortinet_ticker = yf.Ticker("FTNT")
palo_ticker = yf.Ticker("PANW")
zscaler_ticker = yf.Ticker("ZS")
crowdstrike_ticker.quarterly_financials
| 2025-10-31 | 2025-07-31 | 2025-04-30 | 2025-01-31 | 2024-10-31 | |
|---|---|---|---|---|---|
| Tax Effect Of Unusual Items | 0.000000e+00 | 0.000000e+00 | 0.000000e+00 | 0.000000e+00 | 0.000000e+00 |
| Tax Rate For Calcs | 2.100000e-01 | 2.100000e-01 | 2.100000e-01 | 2.100000e-01 | 2.100000e-01 |
| Normalized EBITDA | 5.551200e+07 | 3.187000e+06 | -1.911500e+07 | 1.764100e+07 | 5.129700e+07 |
| Net Income From Continuing Operation Net Minority Interest | -3.399700e+07 | -7.767500e+07 | -1.102070e+08 | -9.228200e+07 | -1.682200e+07 |
| Reconciled Depreciation | 7.184900e+07 | 6.803800e+07 | 6.405700e+07 | 5.744000e+07 | 5.524800e+07 |
| Reconciled Cost Of Revenue | 3.078050e+08 | 3.102830e+08 | 2.891430e+08 | 2.739900e+08 | 2.550870e+08 |
| EBITDA | 5.551200e+07 | 3.187000e+06 | -1.911500e+07 | 1.764100e+07 | 5.129700e+07 |
| EBIT | -1.633700e+07 | -6.485100e+07 | -8.317200e+07 | -3.979900e+07 | -3.951000e+06 |
| Net Interest Income | 4.395200e+07 | 4.402700e+07 | 3.866500e+07 | 3.993300e+07 | 4.561400e+07 |
| Interest Expense | 6.931000e+06 | 6.823000e+06 | 6.715000e+06 | 6.664000e+06 | 6.587000e+06 |
| Interest Income | 5.088300e+07 | 5.085000e+07 | 4.538000e+07 | 4.659700e+07 | 5.220100e+07 |
| Normalized Income | -3.399700e+07 | -7.767500e+07 | -1.102070e+08 | -9.228200e+07 | -1.682200e+07 |
| Net Income From Continuing And Discontinued Operation | -3.399700e+07 | -7.767500e+07 | -1.102070e+08 | -9.228200e+07 | -1.682200e+07 |
| Total Expenses | 1.303687e+09 | 1.281931e+09 | 1.228090e+09 | 1.143839e+09 | 1.065901e+09 |
| Total Operating Income As Reported | -6.944300e+07 | -1.129790e+08 | -1.246560e+08 | -8.530100e+07 | -5.572300e+07 |
| Diluted Average Shares | 2.513260e+08 | 2.499090e+08 | 2.484320e+08 | 2.469330e+08 | 2.455360e+08 |
| Basic Average Shares | 2.513260e+08 | 2.499090e+08 | 2.484320e+08 | 2.469330e+08 | 2.455360e+08 |
| Diluted EPS | -1.400000e-01 | -3.100000e-01 | -4.400000e-01 | -3.700000e-01 | -7.000000e-02 |
| Basic EPS | -1.400000e-01 | -3.100000e-01 | -4.400000e-01 | -3.700000e-01 | -7.000000e-02 |
| Diluted NI Availto Com Stockholders | -3.399700e+07 | -7.767500e+07 | -1.102070e+08 | -9.228200e+07 | -1.682200e+07 |
| Net Income Common Stockholders | -3.399700e+07 | -7.767500e+07 | -1.102070e+08 | -9.228200e+07 | -1.682200e+07 |
| Net Income | -3.399700e+07 | -7.767500e+07 | -1.102070e+08 | -9.228200e+07 | -1.682200e+07 |
| Minority Interests | -9.000000e+03 | -3.000000e+04 | 7.860000e+05 | 4.490000e+05 | -3.000000e+03 |
| Net Income Including Noncontrolling Interests | -3.398800e+07 | -7.764500e+07 | -1.109930e+08 | -9.273100e+07 | -1.681900e+07 |
| Net Income Continuous Operations | -3.398800e+07 | -7.764500e+07 | -1.109930e+08 | -9.273100e+07 | -1.681900e+07 |
| Tax Provision | 1.072000e+07 | 5.971000e+06 | 2.110600e+07 | 4.626800e+07 | 6.281000e+06 |
| Pretax Income | -2.326800e+07 | -7.167400e+07 | -8.988700e+07 | -4.646300e+07 | -1.053800e+07 |
| Other Income Expense | 2.223000e+06 | -2.722000e+06 | -3.896000e+06 | -1.095000e+06 | -4.290000e+05 |
| Other Non Operating Income Expenses | 2.223000e+06 | -2.722000e+06 | -3.896000e+06 | -1.095000e+06 | -4.290000e+05 |
| Net Non Operating Interest Income Expense | 4.395200e+07 | 4.402700e+07 | 3.866500e+07 | 3.993300e+07 | 4.561400e+07 |
| Interest Expense Non Operating | 6.931000e+06 | 6.823000e+06 | 6.715000e+06 | 6.664000e+06 | 6.587000e+06 |
| Interest Income Non Operating | 5.088300e+07 | 5.085000e+07 | 4.538000e+07 | 4.659700e+07 | 5.220100e+07 |
| Operating Income | -6.944300e+07 | -1.129790e+08 | -1.246560e+08 | -8.530100e+07 | -5.572300e+07 |
| Operating Expense | 9.958820e+08 | 9.716480e+08 | 9.389470e+08 | 8.698490e+08 | 8.108140e+08 |
| Research And Development | 3.475640e+08 | 3.466680e+08 | 3.341290e+08 | 3.151420e+08 | 2.756020e+08 |
| Selling General And Administration | 6.483180e+08 | 6.249800e+08 | 6.048180e+08 | 5.547070e+08 | 5.352120e+08 |
| Selling And Marketing Expense | 4.810320e+08 | 4.470240e+08 | 4.396170e+08 | 4.095040e+08 | 4.082670e+08 |
| General And Administrative Expense | 1.672860e+08 | 1.779560e+08 | 1.652010e+08 | 1.452030e+08 | 1.269450e+08 |
| Other Gand A | 1.672860e+08 | 1.779560e+08 | 1.652010e+08 | 1.452030e+08 | 1.269450e+08 |
| Gross Profit | 9.264390e+08 | 8.586690e+08 | 8.142910e+08 | 7.845480e+08 | 7.550910e+08 |
| Cost Of Revenue | 3.078050e+08 | 3.102830e+08 | 2.891430e+08 | 2.739900e+08 | 2.550870e+08 |
| Total Revenue | 1.234244e+09 | 1.168952e+09 | 1.103434e+09 | 1.058538e+09 | 1.010178e+09 |
| Operating Revenue | 1.234244e+09 | 1.168952e+09 | 1.103434e+09 | 1.058538e+09 | 1.010178e+09 |
closing_prices = stock_data[['Date', 'Close_CRWD', 'Close_FTNT', 'Close_PANW', 'Close_ZS', 'Close_^GSPC', 'Close_^IXIC']]
closing_prices.columns = ['Date', 'CRWD', 'FTNT', 'PANW', 'ZS', 'S&P500', 'NASDAQ']
closing_prices.tail(20)
| Date | CRWD | FTNT | PANW | ZS | S&P500 | NASDAQ | |
|---|---|---|---|---|---|---|---|
| 1016 | 2026-01-22 | 453.769989 | 77.620003 | 182.270004 | 208.309998 | 6913.350098 | 23436.019531 |
| 1017 | 2026-01-23 | 452.489990 | 81.639999 | 180.179993 | 209.619995 | 6915.609863 | 23501.240234 |
| 1018 | 2026-01-26 | 468.329987 | 83.070000 | 184.220001 | 214.550003 | 6950.229980 | 23601.359375 |
| 1019 | 2026-01-27 | 476.660004 | 81.709999 | 183.500000 | 219.669998 | 6978.600098 | 23817.099609 |
| 1020 | 2026-01-28 | 469.190002 | 82.080002 | 183.740005 | 213.949997 | 6978.029785 | 23857.449219 |
| 1021 | 2026-01-29 | 444.619995 | 81.510002 | 176.199997 | 200.630005 | 6969.009766 | 23685.119141 |
| 1022 | 2026-01-30 | 441.404999 | 81.260002 | 176.970001 | 200.009995 | 6939.029785 | 23461.820312 |
| 1023 | 2026-02-02 | 438.850006 | 81.290001 | 175.419998 | 200.610001 | 6976.439941 | 23592.109375 |
| 1024 | 2026-02-03 | 421.730011 | 79.300003 | 166.240005 | 188.050003 | 6917.810059 | 23255.189453 |
| 1025 | 2026-02-04 | 415.359985 | 81.120003 | 166.720001 | 185.789993 | 6882.720215 | 22904.580078 |
| 1026 | 2026-02-05 | 377.160004 | 78.930000 | 154.770004 | 169.389999 | 6798.399902 | 22540.589844 |
| 1027 | 2026-02-06 | 395.500000 | 82.760002 | 159.320007 | 167.330002 | 6932.299805 | 23031.210938 |
| 1028 | 2026-02-09 | 408.040009 | 85.930000 | 166.000000 | 170.179993 | 6964.819824 | 23238.669922 |
| 1029 | 2026-02-10 | 413.390015 | 85.529999 | 165.509995 | 174.339996 | 6941.810059 | 23102.470703 |
| 1030 | 2026-02-11 | 415.809998 | 87.820000 | 165.300003 | 171.000000 | 6941.470215 | 23066.470703 |
| 1031 | 2026-02-12 | 411.540009 | 84.260002 | 162.809998 | 170.899994 | 6832.759766 | 22597.150391 |
| 1032 | 2026-02-13 | 429.640015 | 85.559998 | 166.949997 | 177.720001 | 6836.169922 | 22546.669922 |
| 1033 | 2026-02-17 | 414.285004 | 81.690002 | 163.500000 | 172.589996 | 6843.220215 | 22578.380859 |
| 1034 | 2026-02-18 | 415.760010 | 80.690002 | 152.350006 | 172.130005 | 6881.310059 | 22753.630859 |
| 1035 | 2026-02-19 | 422.140015 | 81.669998 | 150.990005 | 168.990005 | 6861.890137 | 22682.730469 |
1. Time Series Chart¶
sns.set_theme(style='ticks') # set theme to allow minor ticks to show
plt.figure(figsize=(15, 8)) # increases width and height of plot to create more space and reduce crowding of lines
# Normalize
normalized = closing_prices.copy()
# For each col (except Date), normalize closing price to 100% of 1st closing price. Each closing price starts at 100
for col in ['CRWD', 'FTNT', 'PANW', 'S&P500', 'NASDAQ']:
# Divide each closing price by 1st closing price in each col and then multiplying by 100. Result is % of starting value (100 = 100% of start)
normalized[col] = (closing_prices[col] / closing_prices[col].iloc[0]) * 100
# Melt
normalized_melt = normalized.melt(
id_vars='Date',
value_vars=['CRWD', 'FTNT', 'PANW', 'NASDAQ', 'S&P500'],
var_name='Stock',
value_name='Normalized_Price'
)
# Draw lineplot of closing price vs date with 3rd variable (stock) expressed through color
g = sns.lineplot(data=normalized_melt,
x='Date',
y='Normalized_Price',
hue='Stock',
hue_order=['CRWD', 'FTNT', 'PANW', ],
palette=["#ff0000", "#f1730b", "#00D5FF"],
alpha=0.85,
linewidth=2,
)
dense_dot_style = (0, (1, 1))
# plots benchmarks (NASDAQ and S&P500)
sns.lineplot(data=normalized_melt,
x='Date',
y='Normalized_Price',
hue='Stock',
hue_order=['NASDAQ', 'S&P500'],
palette=["#562d59", "#005800"],
#linestyle=dense_dot_style,
linewidth=1.75,
alpha=0.65
)
# Draw line of incidence at Crowdstrike outage in 2024
g.axvline(x = pd.to_datetime('2024-07-19'), color='black', alpha=0.5, label='Crowdstrike Outage', linestyle='--')
#
g.axhline(y=100, color='brown', alpha=0.5, label='Start Value (Jan 2022)', linestyle='--')
# Set labels and title
g.set(xlabel='Date', ylabel='Normalized Price (% of Jan 2022 start value)', title='Stock Performances vs Market Benchmarks (Normalized) Over Time')
# Customize tickers to be per year/month
g.xaxis.set_major_locator(mdate.YearLocator())
g.xaxis.set_minor_locator(mdate.MonthLocator(bymonth=[4,7,10]))
# add grid lines back for major and minor axis (had to use style w/o to get minor tick marks)
g.grid(which='both', linestyle='--', linewidth=0.75, alpha=0.7)
plt.legend()
plt.show()
Key Takeaways¶
Prompt: USING THE PROVIDED CODE that creates the plotted time series data given as well as tail() output of closing_prices prior to normalizing. PERFORM VISUAL ANALYSIS ON THE PLOT GIVEN AND PROVIDE KEY TAKEAWAYS FROM THE PLOT. My goal is to explore and to analyze CRWD and its performance compared to its competitors and benchmarks like NASDAQ and S&P500. RESPOND IN TEXT OR MARKDOWN FORMAT FOR EASY INSERTION AND EDITING IN JUPYTER NOTEBOOK
LLM Response:
Broad market benchmarks (NASDAQ & S&P500) moved in lockstep throughout the period, bottoming near 65–70 in 2022 before recovering steadily past 100 by mid-2023 and finishing near 140–150 by early 2026 — serving as the baseline against which all four cybersecurity names are measured.
CRWD tells the most dramatic story. After a deep drawdown in 2022–2023 (bottoming near 35–45), it staged the strongest recovery of any stock in the chart, peaking near 275 — the highest normalized value of the group. The July 2024 outage is clearly visible as a sharp discrete drop, and while CRWD partially recovered into 2025, it never sustainably reclaimed its highs and sits near 210–215 by early 2026 on a choppy, declining trend. It delivered the highest peak return but also the most significant event-driven risk.
PANW was the most consistent outperformer, recovering earlier and more smoothly than peers and holding up well even around the outage period. However, a sharp deterioration in late 2025 into early 2026 dragged it from ~250 down toward 160–175, making it the most notable recent underperformer despite strong cumulative gains.
FTNT spent much of 2022–2023 near or below the 100 baseline before recovering to ~165 by early 2025. It has since pulled back and now broadly tracks just above the market benchmarks, making it the weakest performer among the cybersecurity names across the full period.
Improvement
The LLM was correct for the most part but missed two key observations. It also missed key insight into why Crowdstrike stock is currently on the decline.
Palo Alto Network's (PANW) rise around the outage date was mentioned but was not emphasized enough. PANW visibly accelerated exactly around the July 2024 dashed line, suggesting that customers migrated from Crowdstrike (CRWD) to PANW as a direct result of the Crowdstrike outage.
The LLM described Fortinet (FTNT) as the "weakest performer" without providing the right context. FTNT actually kept pace with or slightly outperformed the broad market benchmarks (NASDAQ and S&P500) across the full period. So FTNT is not a poor performer in relation to the benchmark indices, instead FTNT significantly lags behing its cybersecurity peers, CRWD and PANW. This suggests that investors do not view FTNT as a high growth company like they do its peers.
Recently Anthropic, the creators of Claude, release "Claude Code Security" a new AI-powered vulnerability scanning feature in a limited research preview for Enterprise and Team customers. It scans codebases for high-severity vulnerabilities that traditional static analysis tools often miss and requires a human security researcher to address. This tool presents a major threat to cybersecurity companies like Crowdstrike and its competitors and because of its announcement, all the stocks started to fall.
2. Facet chart¶
# transposes dataframes so dates are rows and columns are financial stats
crowdstrike_ticker = crowdstrike_ticker.financials.T
palo_ticker = palo_ticker.financials.T
fortinet_ticker = fortinet_ticker.financials.T
zscaler_ticker = zscaler_ticker.financials.T
crowdstrike_ticker
| Tax Effect Of Unusual Items | Tax Rate For Calcs | Normalized EBITDA | Net Income From Continuing Operation Net Minority Interest | Reconciled Depreciation | Reconciled Cost Of Revenue | EBITDA | EBIT | Net Interest Income | Interest Expense | ... | Operating Expense | Research And Development | Selling General And Administration | Selling And Marketing Expense | General And Administrative Expense | Other Gand A | Gross Profit | Cost Of Revenue | Total Revenue | Operating Revenue | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2025-01-31 | 0.0 | 0.210000 | 294801000.0 | -19271000.0 | 213956000.0 | 991481000.0 | 294801000.0 | 80845000.0 | 169863000.0 | 26311000.0 | ... | 3.082573e+09 | 1.076901e+09 | 2.005672e+09 | 1.523356e+09 | 482316000.0 | 482316000.0 | 2.962143e+09 | 991481000.0 | 3.953624e+09 | 3.953624e+09 |
| 2024-01-31 | 0.0 | 0.262439 | 293827000.0 | 89327000.0 | 145254000.0 | 755723000.0 | 293827000.0 | 148573000.0 | 123174000.0 | 25756000.0 | ... | 2.301827e+09 | 7.684970e+08 | 1.533330e+09 | 1.140566e+09 | 392764000.0 | 392764000.0 | 2.299832e+09 | 755723000.0 | 3.055555e+09 | 3.055555e+09 |
| 2023-01-31 | 0.0 | 0.210000 | -40754000.0 | -183245000.0 | 93810000.0 | 601231000.0 | -40754000.0 | -134564000.0 | 27176000.0 | 25319000.0 | ... | 1.830117e+09 | 6.083640e+08 | 1.221753e+09 | 9.044090e+08 | 317344000.0 | 317344000.0 | 1.640005e+09 | 601231000.0 | 2.241236e+09 | 2.241236e+09 |
| 2022-01-31 | 0.0 | 0.210000 | -65982000.0 | -234802000.0 | 68810000.0 | 383221000.0 | -65982000.0 | -134792000.0 | -21443000.0 | 25231000.0 | ... | 1.210921e+09 | 3.712830e+08 | 8.396380e+08 | 6.165460e+08 | 223092000.0 | 223092000.0 | 1.068373e+09 | 383221000.0 | 1.451594e+09 | 1.451594e+09 |
4 rows × 43 columns
ticker_df = {
'Date': [],
'Stock': [],
'Total Revenue': [],
}
tickers = {'CRWD': crowdstrike_ticker, 'PANW': palo_ticker, 'FTNT': fortinet_ticker, 'ZS': zscaler_ticker}
# Populate dictionary used to build dataframe. Includes dates stock ticker ,and total revenues of CRWD ticker and competitors
for ticker_name, ticker in tickers.items():
annual_rev_df = ticker['Total Revenue']
annual_rev_df = annual_rev_df.reset_index()
annual_rev_df = annual_rev_df.rename(columns={'index':'Date'})
ticker_df['Date'].extend(annual_rev_df['Date'])
ticker_df['Stock'].extend([ticker_name] * len(annual_rev_df))
ticker_df['Total Revenue'].extend(annual_rev_df['Total Revenue'])
# Create Dataframe from dictionary and drop rows containing NA Total Revenue
ticker_df = pd.DataFrame(ticker_df)
ticker_df = ticker_df.dropna(subset='Total Revenue')
# adds columns for year periods
ticker_df['year'] = ticker_df['Date'].dt.to_period(freq='Y').astype(str) # year periods
year_order = sorted(ticker_df['year'].unique())
# converts revenue from scientific notation to billions of dollars
ticker_df['Total Revenue'] = ticker_df['Total Revenue'] / 1e9
# Create facet grid, faceted on Stock ticker. Each plot is a barplot of Total Revenue (Billions) vs. Year
g = sns.FacetGrid(ticker_df, col='Stock', col_wrap=2, col_order=['CRWD', 'PANW', 'FTNT', 'ZS'])
g.map_dataframe(sns.barplot, x='year', y='Total Revenue', formatter=lambda s: f"{s[0:4]}", order=year_order)
# Set axis labels and titles for each subplot
g.set_axis_labels(x_var='Year', y_var='Total Revenue (Billions)')
g.set_titles(col_template='{col_name}')
# set limit for yaxis on each subplot
g.set(ylim=(0,10))
# Adds label for each subplot noting the Total Revenue (Billions) each year available
for ax in g.axes.flat:
ax.bar_label(ax.containers[0], fmt='%.2f',fontsize=10)
plt.show()
Key Takeaways¶
Prompt: USING THE PROVIDED CODE that creates the facet chart on stock type showing annual revenue vs year data given as well as the transposed output before plotting. PERFORM VISUAL ANALYSIS ON THE PLOT GIVEN AND PROVIDE KEY TAKEAWAYS FROM THE PLOT BASED OFF PROJECT GUIDELINES. My goal is to explore and to analyze CRWD and its performance compared to its competitors. RESPOND IN TEXT OR MARKDOWN FORMAT FOR EASY INSERTION AND EDITING IN JUPYTER NOTEBOOK
LLM Response:
CRWD has shown consistent and impressive revenue growth, rising from $1.45B in 2022 to $3.95B in 2025 — nearly a 3x increase over four years. However, when compared to its competitors, CRWD is notably the smallest by total revenue. PANW dwarfs the group at $9.22B in 2025, and even FTNT, a slower-growth mature player, sits at $5.96B — more than 50% higher than CRWD's most recent figure.
This creates an interesting contrast with the time series closing price chart. Despite having the lowest or near-lowest revenue among its peers, CRWD achieved the highest normalized stock price peak (~275) of any stock in the group. PANW, which generates more than double CRWD's revenue, only peaked around 230–250 and has since declined sharply. This disconnect between revenue size and stock price performance suggests the market has been pricing CRWD on growth rate and future potential rather than current revenue scale — investors appear to be rewarding CRWD's rapid YoY acceleration (each year adding roughly $0.75–$0.90B incrementally) more than the absolute revenue figures that more mature players like FTNT and PANW command.
In other words, CRWD's stock premium over its higher-revenue competitors reflects a growth story, not a current earnings story — which also explains why the July 2024 outage hit CRWD's stock so hard. The July 2024 outage scared investors that customers would leave which would kill the growth rate and inturn kill the only reason the stock was priced so high in the first place.
Improvement
The LLM did overall very well but made a few errors. It was mentioned that compared to its competitors, CRWD has the smallest by total revenue over time. This is not true, as it can be seen that zscaler (ZS) has a lower total revenue for each year plotted.
The response correctly identifies the core revenue hierarchy across all four tickers, read the chart values accurately with minor misinterpretation, and draws a meaningful cross-chart connection between revenue size and stock price performance. The insight about the market pricing CRWD on growth rate rather than current scale is well-argued and directly relevant to the goal of evaluating CRWD vs. competitors. The 2024 outage callback effectively ties the quantitative observation to a real-world narrative.
3. Choose your own chart¶
fig = go.Figure(data=[go.Candlestick(x=stock_data['Date'],
open=stock_data['Open_CRWD'],
high=stock_data['High_CRWD'],
low=stock_data['Low_CRWD'],
close=stock_data['Close_CRWD'],
name='CRWD',
)]
)
# Vertical line of incidence at CrowdStrike outage date
fig.add_vline(
x='2024-07-19',
line_width=2,
line_dash='dash',
line_color='teal',
opacity=0.5 # transparency (0=invisible, 1=fully opaque)
)
fig.update_layout(
height=600,
title="Crowdstrike Stock since 2022",
xaxis_title='Date',
title_x=0.5,
yaxis_title='Price (USD)',
plot_bgcolor='#0d0d0d',
xaxis_gridcolor='#333333',
yaxis_gridcolor='#333333',
annotations=[dict(
x='2024-07-19', y=0.08, xref='x', yref='paper',
showarrow=False, xanchor='left', text='Crowdstrike Outage', font=dict(color='white'))]
)
fig.update_xaxes(
rangeselector=dict(
buttons=[
dict(count=7, label='1 Week', step='day', stepmode='backward'),
dict(count=1, label='1 Month', step='month', stepmode='backward'),
# dict(count=3, label='3 Months', step='month', stepmode='backward'),
dict(count=1, label='YTD', step='year', stepmode='todate'),
dict(count=1, label='1 Year', step='year', stepmode='backward'),
dict(count=2, label='2 Years', step='year', stepmode='backward'),
dict(step='all', label='All')
]
),
rangeslider=dict(visible=True),
hoverformat='%b. %d %Y',
dtick='M3', # tick every quarter (3 months)
tickformat='%b. %Y' # format as '{Month name} {Full year}'
)
fig.show(renderer='notebook')
Key Takeaways¶
Prompt: I have a candlestick chart of CrowdStrike (CRWD) stock from 2022 to February 2026, pulled from yfinance. A vertical dashed line marks July 19, 2024 — the date of the CrowdStrike global IT outage. Looking at the 2 Year and All time views, analyze the stock's performance before, during, and after the outage. What are the key takeaways about how the market reacted and how the stock recovered?
LLM Response:
Pre-Outage Performance (2022–mid 2024)
- CRWD experienced a significant decline from a high of ~$220 in early 2022 down to a low of ~$100 in early 2023, consistent with the broader tech selloff during rising interest rate environment
- The stock then staged a strong recovery through 2023 and into 2024, reaching ~$400 by mid-2024 — nearly a 4x recovery from its 2023 lows
- This uptrend reflected growing investor confidence in cybersecurity demand and CRWD's market position
The Outage Event (July 19, 2024)
- The outage caused an immediate and severe single-day drop — visible as a large red candle directly at the dashed line
- The stock fell from ~$390 to ~$200, approximately a 50% drawdown in a matter of days
- This represents one of the sharpest short-term drops visible across the entire chart, indicating the market treated this as a serious fundamental threat, not just noise
Post-Outage Recovery (late 2024–2026)
- Despite the severity of the drop, CRWD recovered relatively quickly — within ~6 months the stock had climbed back above pre-outage levels
- By mid-2025 the stock reached new all time highs above $500, suggesting the market ultimately concluded the outage was a reputational but not existential threat
- The recovery trajectory was steady with no major secondary selloffs, indicating institutional confidence returned
Key Takeaways
- The market initially punished CRWD severely, pricing in potential customer churn, lawsuits, and reputational damage
- The speed of recovery suggests cybersecurity demand is structural — customers had few alternatives and largely stayed with CRWD
- The all time high post-outage is arguably the most important data point — it signals the outage was a buying opportunity in hindsight
Improvement
The analysis by the LLM was very well constructed and the key takeaways are well reasoned. The narrative structure of breaking analysis into pre-outage, outage, and post-outage sections was logical and easy to follow. One thing that needed improvement was the direct effect of the Crowdstrike outage. The LLM said the outage caused an immediate and severe single-day drop. However, the direct drop in price was for up to a week. Most importantly, the response completely ignores the sharp early 2026 sell-off visible on the right edge of the chart. This decline from $500+ back to ~$350–$370 is tied to the Claude Code Security announcement.
Animated plot¶
stock_data[['Date', 'Close_CRWD', 'Close_FTNT', 'Close_PANW', 'Close_ZS', 'Volume_CRWD', 'Volume_FTNT', 'Volume_PANW', 'Volume_ZS']]
| Date | Close_CRWD | Close_FTNT | Close_PANW | Close_ZS | Volume_CRWD | Volume_FTNT | Volume_PANW | Volume_ZS | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 2022-01-03 | 198.330002 | 66.624001 | 90.701668 | 301.829987 | 5311600 | 9515000 | 7802400 | 2576000 |
| 1 | 2022-01-04 | 189.190002 | 64.428001 | 87.333336 | 281.579987 | 6503400 | 9829000 | 9586200 | 3827700 |
| 2 | 2022-01-05 | 179.289993 | 60.698002 | 84.143333 | 262.329987 | 5003400 | 12033000 | 7494000 | 3437700 |
| 3 | 2022-01-06 | 187.490005 | 62.799999 | 88.818336 | 260.209991 | 6368600 | 12004000 | 16553400 | 2439600 |
| 4 | 2022-01-07 | 188.990005 | 62.931999 | 87.581665 | 261.529999 | 3531900 | 8805000 | 13586400 | 1696000 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 1031 | 2026-02-12 | 411.540009 | 84.260002 | 162.809998 | 170.899994 | 3502500 | 7805800 | 19646500 | 2499300 |
| 1032 | 2026-02-13 | 429.640015 | 85.559998 | 166.949997 | 177.720001 | 3536700 | 8091300 | 12577000 | 2434100 |
| 1033 | 2026-02-17 | 414.285004 | 81.690002 | 163.500000 | 172.589996 | 4428100 | 6061100 | 16586500 | 2038100 |
| 1034 | 2026-02-18 | 415.760010 | 80.690002 | 152.350006 | 172.130005 | 2752700 | 5853300 | 26756800 | 2542100 |
| 1035 | 2026-02-19 | 422.140015 | 81.669998 | 150.990005 | 168.990005 | 2000400 | 4584100 | 13545800 | 3337100 |
1036 rows × 9 columns
# subset of stock data
close_df = stock_data[['Date', 'Close_CRWD', 'Close_FTNT', 'Close_PANW', 'Close_ZS']]
vol_df = stock_data[['Date', 'Volume_CRWD', 'Volume_FTNT', 'Volume_PANW', 'Volume_ZS']]
close_df.columns = ['Date', 'CRWD', 'FTNT', 'PANW', 'ZS']
vol_df.columns = ['Date', 'CRWD', 'FTNT', 'PANW', 'ZS']
close_df = close_df.melt(id_vars='Date', value_vars=['CRWD', 'FTNT', 'PANW', 'ZS'], var_name='Ticker', value_name='Closing Price')
vol_df = vol_df.melt(id_vars='Date', value_vars=['CRWD', 'FTNT', 'PANW', 'ZS'], var_name='Ticker', value_name='Volume')
anim_df = close_df.merge(vol_df, on=['Date','Ticker'])
# transform Date column to a monthly period and make this a new column as string type
anim_df['month'] = anim_df['Date'].dt.to_period('M').astype(str)
# inserts the popped month series as first column
anim_df.insert(0, 'Month', anim_df.pop('month'))
anim_df = anim_df.drop(columns='Date')
# compute daily return before grouping for volatility
anim_df = anim_df.sort_values(['Ticker', 'Month'])
anim_df['Daily Return'] = anim_df.groupby('Ticker')['Closing Price'].pct_change()
monthly_df = anim_df.groupby(['Month', 'Ticker']).agg(
Close=('Closing Price', 'last'),
Volume=('Volume', 'mean'),
Volatility=('Daily Return', lambda x: x.std() * (21 ** 0.5))
).reset_index()
# normalize each ticker's price to 100 at start
base = monthly_df.groupby('Ticker')['Close'].transform('first')
monthly_df['Indexed Price'] = (monthly_df['Close'] / base) * 100
fig = px.scatter(
monthly_df,
x='Indexed Price',
y='Volume',
animation_frame='Month',
animation_group='Ticker',
size='Volatility',
color='Ticker',
hover_name='Ticker',
size_max=60,
range_x=[0, monthly_df['Indexed Price'].max() * 1.1],
range_y=[-monthly_df['Volume'].max() * 0.15, monthly_df['Volume'].max() * 1.5],
title='CRWD vs Competitors — Indexed Price vs Volume',
labels={
'Indexed Price': 'Growth Since Jan 2022 (Base = 100)',
'Volume': 'Avg Daily Volume',
'Volatility': 'Monthly Volatility'
}
)
fig.layout.updatemenus[0].buttons[0].args[1]['frame']['duration'] = 700 # ms per frame
# for nbconvert HTML export
from IPython.display import display, HTML
animation_html = fig.to_html(full_html=False, include_plotlyjs='cdn', auto_play=False)
display(HTML(animation_html))
Key Takeaways¶
Prompt: USING THE PROVIDED CODE that creates an animated scatter plot in plotly express with the following date on the axes and encoded into color and size.
X — Growth Since Jan 2022 (Base = 100)
- Every stock started at 100 in Jan 2022. A value of 200 means the stock doubled. 50 means it lost half its value. It's a fair comparison because it removes the distortion of different starting prices.
Y — Avg Daily Volume
- How many shares were traded on average each day that month. High Y = lots of market participants actively buying and selling. It measures conviction and interest — is anyone actually putting money behind this stock?
Size — Volatility
- How wildly the stock swung within that month. A large bubble = high risk, unpredictable price moves. Small bubble = stable, predictable.
PERFORM VISUAL ANALYSIS ON THE PLOT GIVEN WHICH SHOWS THE PLOT AT THE START AND END POINT OF THE DATA. AN IMAGE IN July of 2024 is also provided to illustrate the effect crowdstrike outage had on the stock. PROVIDE KEY TAKEAWAYS FROM THE PLOT. My goal is to explore and to analyze CRWD and its performance compared to its competitors. RESPOND IN TEXT OR MARKDOWN FORMAT FOR EASY INSERTION AND EDITING IN JUPYTER NOTEBOOK
LLM Response:
Overview of Encoding
- Each bubble represents one stock in a given month. Its horizontal position shows cumulative price growth since Jan 2022 (100 = flat), its vertical position shows how actively traded it was, and its size reflects how volatile (risky) the stock was that month.
Jan 2022 — Baseline (Image 1)
- All four tickers cluster tightly around x=100, which is expected — this is the base period by construction. What's notable is that PANW already shows the largest bubble at this starting point, suggesting it entered the period with relatively higher volatility than its peers. The vertical spread is minimal, meaning trading volume was roughly comparable across all four names at the start.
July 2024 — The CrowdStrike Outage (Image 3)
This frame captures the aftermath of the July 19, 2024 global IT outage caused by a faulty CrowdStrike Falcon sensor update. Several things stand out:
CRWD (blue) is the largest bubble on the chart — its volatility spiked dramatically, reflecting the massive intraday and week-over-week price swings that followed the incident
Despite the chaos, CRWD's indexed price (~100) shows it had actually given back all gains from its 2022 baseline by this point — a notable underperformance relative to where it had been in prior years
PANW (teal) is sitting around x=190, suggesting it had nearly doubled since Jan 2022, and its relatively small bubble means it did so with less volatility — a much cleaner ride
FTNT (orange) and ZS (purple) are both below x=100, meaning they lost value since the baseline and attracted relatively modest volume — neither was a standout performer
The outage frame illustrates a key dynamic: event-driven volatility without corresponding price appreciation. CRWD was moving violently, but not upward.
Feb 2026 — End of Data (Image 2)
By the most recent frame, the picture has shifted considerably:
PANW is now the clear volume leader (highest Y position, ~20M avg daily volume) and has grown to roughly x=145 — a solid but not spectacular gain since 2022. High volume + moderate growth suggests sustained institutional interest
CRWD has recovered strongly, sitting around x=230 — the furthest right of any ticker, meaning it has more than doubled from its Jan 2022 base and outpaced all competitors on a pure price-return basis. Its bubble is moderate in size, suggesting volatility has normalized post-outage
FTNT has recovered to around x=135, with moderate volume — a quiet, steady compounder that never generated the excitement (or drama) of CRWD
ZS remains the laggard at roughly x=70, still below the 2022 baseline — meaning it has lost value in absolute terms over the full period and attracted relatively little volume
Key Takeaways
- CRWD had the highest total return of the group over the full period (Jan 2022 → Feb 2026), ending north of 2x despite a catastrophic mid-period event
- The July 2024 outage is visible as a volatility spike — the largest bubble CRWD produces in the entire animation — but it did not permanently derail the stock
- PANW commands the most market attention (volume) by the end of the period, suggesting it is the most institutionally traded name in the group regardless of who has the highest return
- ZS is the notable underperformer — negative total return, low volume, and no particular volatility events to explain a recovery catalyst
- Volatility and volume are not the same signal: CRWD had high volatility (large bubbles) at key moments but was not always the highest-volume name — PANW consistently attracted more traders even with a smoother price path
- The animation encodes a narrative that static charts miss: CRWD's journey was the most dramatic — starting flat, surging, crashing on the outage, then recovering to lead the group — while PANW's was the most consistently rewarded by the market in terms of participation
Improvement
The LLM's response was well formed but could be made clear and more concrete by adding a few exact numbers for the key frames (Jan 2022, July 2024 outage, Feb 2026) — e.g., indexed values and the volatility/volume figures — and replace vague words like “largest bubble” with a one-line sentence that says how bubble size maps to volatility. Emphasize the CRWD outage and the recovery in plain terms (what happened to price and to trading activity) and note any important caveats (time window used, how volume is measured) so readers aren’t left guessing how the visuals were made.
The response can also be improved by making the conclusion more actionable and easier to reuse by including a tiny stats summary line for each ticker (total return, a simple volatility number, and date of max drawdown) and finish with a short recommendation.
Heatmap¶
import calendar
# pull just what we need
df = stock_data[['Date', 'Volume_CRWD']].copy()
df['Year'] = df['Date'].dt.year.astype(int)
df['Month'] = df['Date'].dt.month.astype(int)
# pivot to Year x Month grid with mean daily volume
pivot = df.pivot_table(index='Year', columns='Month', values='Volume_CRWD', aggfunc='mean', fill_value=0)
# OR USE: pivot = df.groupby(['Year', 'Month'])['Volume_CRWD'].mean().unstack()
# rename columns from 1-12 to Jan-Dec
pivot.columns = [calendar.month_abbr[m] for m in pivot.columns]
# convert to millions for readable annotations
pivot_millions = pivot / 1_000_000
# plot
fig, ax = plt.subplots(figsize=(14, 6))
sns.heatmap(
pivot_millions,
ax=ax,
cmap='YlOrRd',
fmt='.1f',
linewidths=0.5,
linecolor='white',
cbar_kws={'label': 'Avg Daily Volume (Millions)'},
annot=True,
annot_kws={'size': 9}
)
ax.set_title("CrowdStrike (CRWD) — Seasonal Trading Volume Heatmap", fontsize=14, pad=15)
ax.set_xlabel("Month", fontsize=12, labelpad=15)
ax.set_ylabel("Year", fontsize=12, labelpad=15)
plt.show()
Key Takeaways¶
Prompt: USING THE PROVIDED CODE that constructs a seaborn heatmap from CRWD trading data. The code aggregates average daily trading volume by Year and Month, converts values to millions of shares, and encodes them into a color scale with annotations. Respond in text or markdown format for easy insertion and editing in a Jupyter Notebook.
LLM Response
Each cell in the heatmap represents CrowdStrike’s average daily trading volume for a specific month and year.
The horizontal axis shows the calendar month from Jan to Dec. The vertical axis shows the year. Color intensity encodes trading activity, where lighter yellow represents lower trading volume and darker red represents higher trading volume.
Each cell also contains a numeric annotation showing the exact average daily trading volume in millions of shares. This allows the viewer to quickly compare months visually while also seeing the precise values.
2022–2023 Typical Trading Pattern
Across 2022 and 2023, CRWD trading activity appears relatively stable. Most months fall in the range of roughly 3 to 6 million shares traded per day, producing a fairly uniform color pattern across the heatmap.
There are small fluctuations, with some early-year months slightly higher and some late-year months slightly lower. Overall, this period reflects a normal trading environment without extreme spikes in investor activity.
Mid-2024 Major Trading Activity Spike
The most noticeable feature in the heatmap occurs during June through August of 2024. The color intensity shifts strongly toward red, indicating a sharp increase in trading activity.
July 2024 shows the highest value in the entire chart at about 11.4 million average daily shares traded. June and August are also elevated relative to surrounding months.
This cluster suggests a short but intense surge in market participation, likely tied to a significant event or catalyst that caused investors to actively reposition.
2025 Return to Normal Liquidity
After the 2024 spike, trading activity in 2025 returns to the more typical range of about 2 to 5 million shares per day. The colors shift back toward lighter orange shades similar to those seen in earlier years.
This pattern suggests that the mid-2024 surge was temporary rather than a lasting structural increase in trading volume.
2026 Partial Data
- Only January and February 2026 contain actual values, while the remaining months appear as zeros. This likely reflects missing or incomplete data rather than real trading activity and should not be interpreted as meaningful observations.
Key Takeaways
CRWD trading volume is generally stable, clustering around roughly 3 to 5 million shares per day during normal periods.
A pronounced spike in trading activity appears in mid-2024, particularly in July, which stands out as the highest volume month in the dataset.
Trading activity returned to typical levels in 2025, suggesting the mid-2024 spike was event driven rather than structural.
Seasonal effects appear modest, though some late-year months show slightly lower trading activity.
The apparent zeros in 2026 represent incomplete data rather than actual trading behavior.
Improvements:
The LLM's response could be improved by being more concise and focusing more on the insights from the chart rather than explaining how heatmaps work. Some of the discussion about typical volume levels and stability repeats similar ideas and could be shortened. The explanation of the encoding could also be simplified so it focuses more on how to read the chart and what patterns stand out. It would also help to clearly state that the zero values in 2026 are caused by missing data in the pivot table rather than actual trading volume.
The heatmap output could also be improved. Missing months should be shown as blank instead of zeros so the chart does not imply that trading volume was zero. The figure could be slightly larger to make the annotations easier to read, and the color scale could be adjusted so the large spike in mid-2024 stands out more clearly. Adding a row or column average could also help reveal seasonal patterns in trading volume across months.
Should you invest in CrowdStrike?¶
Yes, but do so cautiously. CRWD has the strongest growth trajectory of the group, nearly tripling revenue and hitting new all-time highs even after the catastrophic July 2024 outage. This proves the market views it as a structural winner in cybersecurity. However, it trades at a steep growth premium with no margin for error. The outage showed PANW will absorb customers the moment CRWD stumbles, and the early 2026 selloff shows even competitive announcements (like Claude Code Security) can tank the stock. It is a high risk, high reward stock that is good for patient investors, not for the risk-averse.