import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
#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
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()
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.
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.
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.
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
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
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.
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()
Next let's import the real GDP values
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.
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.
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.
# 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%