In [17]:
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
In [21]:
#df = pd.read_csv('SwissRe_CatLosses.csv')
df = pd.read_csv('SwissRe_CatLosses.csv')
print(df.tail(10))
    Year  Weather         EQ
45  2015    36.83   0.670000
46  2016    50.29  11.680000
47  2017   183.02   2.160000
48  2018   102.15   3.860000
49  2019    65.82   0.225841
50  2020   102.59   0.780000
51  2021   124.73   5.210000
52  2022   134.56   3.720000
53  2023   107.88   7.190000
54  2024   134.63   2.730000

Let's start by trying to recreate the Swiss Re graph. This partly will serve as a check we haven't messed up the data import

In [24]:
weather_losses = df.iloc[:, 1]
eq_losses = df.iloc[:, 2]

years = df.iloc[:, 0]
values = df.iloc[:, 1]

plt.figure(figsize=(12, 6))
plt.bar(years, weather_losses, alpha=0.7, label='Weather Related')
plt.bar(years, eq_losses, alpha=0.7, bottom=weather_losses, label='EQ/Tsunami')

plt.xlabel('Year')
plt.ylabel('Insured Losses')
plt.title('Global Natural Catastrophe Insured Losses')
plt.legend()
plt.tight_layout()
plt.show()
No description has been provided for this image

Which looks pretty good to me.

Now let's calculate the inflation in the weather column and the equivalent inflation in the EQ column for a range of different time windows.

In [27]:
windows = [50, 40, 30, 20]

results = []
for window in windows:
    recent_years = years.iloc[-window:]
    recent_weather = df['Weather'].iloc[-window:]
    recent_eq = df['EQ'].iloc[-window:]
    
    ln_weather = np.log(recent_weather)
    slope_weather = np.polyfit(recent_years, ln_weather, 1)[0]
    inflation_weather = np.exp(slope_weather) - 1
    
    mask = recent_eq > 0
    ln_eq = np.log(recent_eq[mask])
    slope_eq = np.polyfit(recent_years[mask], ln_eq, 1)[0]
    inflation_eq = np.exp(slope_eq) - 1
    
    results.append({
        'Window': f'{window}yr',
        'Weather Inflation': f'{inflation_weather*100:.2f}%',
        'EQ Inflation': f'{inflation_eq*100:.2f}%',
        'Difference': f'{(inflation_weather - inflation_eq)*100:.2f}%'
    })

results_df = pd.DataFrame(results)
print(results_df)
  Window Weather Inflation EQ Inflation Difference
0   50yr             7.45%        4.09%      3.36%
1   40yr             6.43%        3.15%      3.27%
2   30yr             6.53%        6.07%      0.46%
3   20yr             5.67%        6.26%     -0.59%

We can see we get a range of values here. Let's calcualte the sensitivity of these estimates to a small shift in the window. i.e. compare the 50 yr estimate of 1975-2024 to the 50 year estimate of 1974-2023, and so on.

In [30]:
windows = [50, 40, 30, 20]

stability_results = []
for window in windows:
    recent_years = years.iloc[-window:]
    recent_weather = df['Weather'].iloc[-window:]
    recent_eq = df['EQ'].iloc[-window:]
    
    ln_weather = np.log(recent_weather)
    slope_weather = np.polyfit(recent_years, ln_weather, 1)[0]
    inflation_weather = np.exp(slope_weather) - 1
    
    mask = recent_eq > 0
    ln_eq = np.log(recent_eq[mask])
    slope_eq = np.polyfit(recent_years[mask], ln_eq, 1)[0]
    inflation_eq = np.exp(slope_eq) - 1
    
    shifted_years = years.iloc[-(window+1):-1]
    shifted_weather = df['Weather'].iloc[-(window+1):-1]
    shifted_eq = df['EQ'].iloc[-(window+1):-1]
    
    ln_weather_shifted = np.log(shifted_weather)
    slope_weather_shifted = np.polyfit(shifted_years, ln_weather_shifted, 1)[0]
    inflation_weather_shifted = np.exp(slope_weather_shifted) - 1
    
    mask_shifted = shifted_eq > 0
    ln_eq_shifted = np.log(shifted_eq[mask_shifted])
    slope_eq_shifted = np.polyfit(shifted_years[mask_shifted], ln_eq_shifted, 1)[0]
    inflation_eq_shifted = np.exp(slope_eq_shifted) - 1
    
    stability_results.append({
        'Window': f'{window}yr',
        'Weather': f'{inflation_weather*100:.1f}%',
        'Weather Shifted': f'{inflation_weather_shifted*100:.1f}%',
        'Weather Δ': f'{(inflation_weather - inflation_weather_shifted)*100:.1f}%',
        'EQ': f'{inflation_eq*100:.1f}%',
        'EQ Shifted': f'{inflation_eq_shifted*100:.1f}%',
        'EQ Δ': f'{(inflation_eq - inflation_eq_shifted)*100:.1f}%'
    })

stability_df = pd.DataFrame(stability_results)
print(stability_df)
  Window Weather Weather Shifted Weather Δ    EQ EQ Shifted  EQ Δ
0   50yr    7.4%            7.4%      0.1%  4.1%       4.1%  0.0%
1   40yr    6.4%            6.6%     -0.1%  3.2%       3.1%  0.1%
2   30yr    6.5%            6.7%     -0.2%  6.1%       3.2%  2.9%
3   20yr    5.7%            4.5%      1.1%  6.3%       4.1%  2.1%

We can see that for weather 50, 40, and 30 years are all stable. For EQ only the 50 and 40 years are stable. Because we are going to be comparing the two estimates (Weather against non-weather), we need both to be stable, so we're going to throw out the 30,20 estimates due to the instability in the EQ value. The final table will be our result for method 1.

In [33]:
windows = [50, 40]

results = []
for window in windows:
    recent_years = years.iloc[-window:]
    recent_weather = df['Weather'].iloc[-window:]
    recent_eq = df['EQ'].iloc[-window:]
    
    ln_weather = np.log(recent_weather)
    slope_weather = np.polyfit(recent_years, ln_weather, 1)[0]
    inflation_weather = np.exp(slope_weather) - 1
    
    mask = recent_eq > 0
    ln_eq = np.log(recent_eq[mask])
    slope_eq = np.polyfit(recent_years[mask], ln_eq, 1)[0]
    inflation_eq = np.exp(slope_eq) - 1
    
    results.append({
        'Window': f'{window}yr',
        'Weather Inflation': f'{inflation_weather*100:.1f}%',
        'EQ Inflation': f'{inflation_eq*100:.1f}%',
        'Difference': f'{(inflation_weather - inflation_eq)*100:.1f}%'
    })

results_df = pd.DataFrame(results)
print(results_df)
  Window Weather Inflation EQ Inflation Difference
0   50yr              7.4%         4.1%       3.4%
1   40yr              6.4%         3.2%       3.3%

Now let's start on method 2. First we need to import CPI data

In [36]:
cpi_df = pd.read_csv('US_CPI.csv')
print(cpi_df.head())
   Year  (rate of inflation)  Inc Inflation  Cml inflation
0  1970                0.057          1.057       8.067480
1  1971                0.044          1.044       7.727471
2  1972                0.032          1.032       7.487860
3  1973                0.062          1.062       7.050716
4  1974                0.110          1.110       6.351996

Next we need to detrend the cpi adjustment from the Swiss Re data to get unadjusted estimates

In [39]:
cpi_years = cpi_df.iloc[:, 0]
cpi_values = cpi_df.iloc[:, 3]

cpi_dict = dict(zip(cpi_years, cpi_values))

df['Weather_RealTerms'] = df.iloc[:, 1] / df.iloc[:, 0].map(cpi_dict)
df['EQ_RealTerms'] = df.iloc[:, 2] / df.iloc[:, 0].map(cpi_dict)

print(df.head())
   Year   Weather        EQ  Weather_RealTerms  EQ_RealTerms
0  1970  3.989024  0.117324           0.494457      0.014543
1  1971  1.055918  0.234648           0.136645      0.030365
2  1972  2.815782  1.055918           0.376046      0.141017
3  1973  4.810294  0.000000           0.682242      0.000000
4  1974  6.570158  0.000000           1.034345      0.000000

Now let's graph the untrended data.

In [42]:
plt.figure(figsize=(12, 6))
plt.bar(years, df['Weather_RealTerms'], alpha=0.7, label='Weather Related')
plt.bar(years, df['EQ_RealTerms'], alpha=0.7, bottom=df['Weather_RealTerms'], label='EQ/Tsunami')

plt.xlabel('Year')
plt.ylabel('Real Insured Losses')
plt.title('Global Natural Catastrophe Insured Losses (Real Terms)')
plt.legend()
plt.tight_layout()
plt.show()
No description has been provided for this image

Next let's import the real GDP values

In [45]:
gdp_df = pd.read_csv('RealGDPGrowth.csv')
print(gdp_df.head())
     Year  Real GDP growth
0  1970.0          -0.0017
1  1971.0           0.0437
2  1972.0           0.0689
3  1973.0           0.0402
4  1974.0          -0.0195

As our final step, we know the 50,40, and 30 year estimates for weather are stable, lets work with these. Next let's calculate the CPI inflation and real GDP growth trends for these periods. And as a final step, subtract these values from the overall weather trend to calculate a residual trend.

In [48]:
windows = [50, 40, 30]

combined_results = []
for window in windows:
    recent_years = years.iloc[-window:]
    recent_weather = df['Weather_RealTerms'].iloc[-window:]
    
    ln_weather = np.log(recent_weather)
    slope_weather = np.polyfit(recent_years, ln_weather, 1)[0]
    inflation_weather = np.exp(slope_weather) - 1
    
    recent_cpi = cpi_df.iloc[-window:, 2]
    avg_cpi = np.prod(recent_cpi) ** (1/window) - 1
    
    recent_gdp = gdp_df.iloc[-window:, 1]
    avg_gdp_growth = np.prod(1 + recent_gdp) ** (1/window) - 1
    
    residual = inflation_weather - avg_cpi - avg_gdp_growth
    
    combined_results.append({
        'Window': f'{window}yr',
        'Weather Inflation': f'{inflation_weather*100:.1f}%',
        'CPI Inflation': f'{avg_cpi*100:.1f}%',
        'Real GDP Growth': f'{avg_gdp_growth*100:.1f}%',
        'Residual Inflation': f'{residual*100:.1f}%'
    })

combined_df = pd.DataFrame(combined_results)
print(combined_df)
  Window Weather Inflation CPI Inflation Real GDP Growth Residual Inflation
0   50yr             10.8%          3.8%            2.8%               4.3%
1   40yr              9.1%          2.8%            2.6%               3.7%
2   30yr              9.0%          2.5%            2.5%               4.0%

As a final step, let's test the stability of the weather trends to including/excluding Katrina, 2011 Non-weather (Japanese EQ), and 2017 (HIM) losses.

In [80]:
windows = [50, 40, 30]
outlier_results = []
for window in windows:
    recent_years = years.iloc[-window:]
    recent_weather = df['Weather_RealTerms'].iloc[-window:]
    recent_eq = df['EQ_RealTerms'].iloc[-window:]
    
    ln_weather = np.log(recent_weather)
    slope_weather = np.polyfit(recent_years, ln_weather, 1)[0]
    inflation_weather = np.exp(slope_weather) - 1
    
    mask_eq = recent_eq > 0
    ln_eq = np.log(recent_eq[mask_eq])
    slope_eq = np.polyfit(recent_years[mask_eq], ln_eq, 1)[0]
    inflation_eq = np.exp(slope_eq) - 1
    
    mask_no_2011 = recent_years != 2011
    ln_eq_no_2011 = np.log(recent_eq[mask_eq & mask_no_2011])
    slope_eq_no_2011 = np.polyfit(recent_years[mask_eq & mask_no_2011], ln_eq_no_2011, 1)[0]
    inflation_eq_no_2011 = np.exp(slope_eq_no_2011) - 1
    
    mask_no_2005 = recent_years != 2005
    ln_weather_no_2005 = np.log(recent_weather[mask_no_2005])
    slope_weather_no_2005 = np.polyfit(recent_years[mask_no_2005], ln_weather_no_2005, 1)[0]
    inflation_weather_no_2005 = np.exp(slope_weather_no_2005) - 1
    
    mask_no_2017 = recent_years != 2017
    ln_weather_no_2017 = np.log(recent_weather[mask_no_2017])
    slope_weather_no_2017 = np.polyfit(recent_years[mask_no_2017], ln_weather_no_2017, 1)[0]
    inflation_weather_no_2017 = np.exp(slope_weather_no_2017) - 1
    
    # Get CPI and GDP values from combined_df (assuming same row index)
    recent_cpi = cpi_df.iloc[-window:, 2]
    avg_cpi = np.prod(recent_cpi) ** (1/window) - 1
    
    recent_gdp = gdp_df.iloc[-window:, 1]
    avg_gdp_growth = np.prod(1 + recent_gdp) ** (1/window) - 1
    
    # Calculate residuals
    residual = inflation_weather - avg_cpi - avg_gdp_growth
    residual_no_2005 = inflation_weather_no_2005 - avg_cpi - avg_gdp_growth
    residual_no_2011 = inflation_weather - avg_cpi - avg_gdp_growth  # Weather unchanged, EQ excluded
    residual_no_2017 = inflation_weather_no_2017 - avg_cpi - avg_gdp_growth
    
    outlier_results.append({
        'Window': f'{window}yr',
        'Weather': f'{inflation_weather*100:.1f}%',
        'Weather (no 2005)': f'{inflation_weather_no_2005*100:.1f}%',
        'Weather (no 2017)': f'{inflation_weather_no_2017*100:.1f}%',
        'EQ': f'{inflation_eq*100:.1f}%',
        'EQ (no 2011)': f'{inflation_eq_no_2011*100:.1f}%',
        'Residual': f'{residual*100:.1f}%',
        'Residual (no 2005)': f'{residual_no_2005*100:.1f}%',
        'Residual (no 2011)': f'{residual_no_2011*100:.1f}%',
        'Residual (no 2017)': f'{residual_no_2017*100:.1f}%'
    })
outlier_df = pd.DataFrame(outlier_results)
print(outlier_df)
  Window Weather Weather (no 2005) Weather (no 2017)    EQ EQ (no 2011)  \
0   50yr   10.8%             10.7%             10.7%  7.0%         6.5%   
1   40yr    9.1%              9.1%              8.9%  5.7%         5.3%   
2   30yr    9.0%              9.3%              8.7%  8.5%         8.3%   

  Residual Residual (no 2005) Residual (no 2011) Residual (no 2017)  
0     4.3%               4.2%               4.3%               4.2%  
1     3.7%               3.7%               3.7%               3.5%  
2     4.0%               4.3%               4.0%               3.7%  

We can see that including/excluding these all produce a <0.5% difference. So I'm going to call these acceptable, and leave in these events.

In [82]:
# Summary table showing only climate change inflation
summary_data = []

# Method 1
for i, window in enumerate([50, 40]):
    summary_data.append({
        'Method': 'Method 1',
        'Window': f'{window} yr',
        'Climate change inflation': results_df.loc[i, 'Difference']
    })

# Method 2
for i, window in enumerate([50, 40, 30]):
    # Standard
    summary_data.append({
        'Method': 'Method 2',
        'Window': f'{window} yr',
        'Climate change inflation': outlier_df.loc[i, 'Residual']
    })
    
    # Excluding 2005
    summary_data.append({
        'Method': 'Method 2 (excluding 2005)',
        'Window': f'{window} yr',
        'Climate change inflation': outlier_df.loc[i, 'Residual (no 2005)']
    })
    
    # Excluding 2011
    summary_data.append({
        'Method': 'Method 2 (excluding 2011)',
        'Window': f'{window} yr',
        'Climate change inflation': outlier_df.loc[i, 'Residual (no 2011)']
    })
    
    # Excluding 2017
    summary_data.append({
        'Method': 'Method 2 (excluding 2017)',
        'Window': f'{window} yr',
        'Climate change inflation': outlier_df.loc[i, 'Residual (no 2017)']
    })

summary_df = pd.DataFrame(summary_data)
print(summary_df.to_string(index=False))
                   Method Window Climate change inflation
                 Method 1  50 yr                     3.4%
                 Method 1  40 yr                     3.3%
                 Method 2  50 yr                     4.3%
Method 2 (excluding 2005)  50 yr                     4.2%
Method 2 (excluding 2011)  50 yr                     4.3%
Method 2 (excluding 2017)  50 yr                     4.2%
                 Method 2  40 yr                     3.7%
Method 2 (excluding 2005)  40 yr                     3.7%
Method 2 (excluding 2011)  40 yr                     3.7%
Method 2 (excluding 2017)  40 yr                     3.5%
                 Method 2  30 yr                     4.0%
Method 2 (excluding 2005)  30 yr                     4.3%
Method 2 (excluding 2011)  30 yr                     4.0%
Method 2 (excluding 2017)  30 yr                     3.7%