In [1]:
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¶

In [2]:
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¶

In [3]:
# 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
Out[3]:
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¶

In [4]:
# download stock data
stock_data = clean_stock_data(stock_data)

stock_data.to_csv('stocks.csv', index=False)
stock_data
Out[4]:
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¶

In [3]:
# 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()
Out[3]:
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

In [4]:
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¶

In [5]:
crowdstrike_ticker = yf.Ticker("CRWD")
fortinet_ticker = yf.Ticker("FTNT")
palo_ticker = yf.Ticker("PANW")
zscaler_ticker = yf.Ticker("ZS")
In [6]:
crowdstrike_ticker.quarterly_financials
Out[6]:
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
In [8]:
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)
Out[8]:
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¶

In [58]:
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()
No description has been provided for this image

Key Takeaways¶

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

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

  3. 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¶

In [12]:
# 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
Out[12]:
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

In [13]:
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()
No description has been provided for this image

Key Takeaways¶

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

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

  3. 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¶

In [49]:
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¶

  1. 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?

  2. 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
  3. 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¶

In [53]:
stock_data[['Date', 'Close_CRWD', 'Close_FTNT', 'Close_PANW', 'Close_ZS', 'Volume_CRWD', 'Volume_FTNT', 'Volume_PANW', 'Volume_ZS']]
Out[53]:
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

In [186]:
# 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¶

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

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

    1. 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
    2. 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
    3. 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
    4. ZS is the notable underperformer — negative total return, low volume, and no particular volatility events to explain a recovery catalyst
    5. 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
    6. 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
  3. 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¶

In [181]:
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()
No description has been provided for this image

Key Takeaways¶

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

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

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

Collab Link¶

https://colab.research.google.com/drive/1Jt4F_8shFA2fIOm3SGViwcmNcs4JMZxA?usp=sharing