Aggregating earned premium and loss+LAE from ISO property data to compute loss ratios and peril compositions across states and ZIP codes.
next step:
occupancy mapping
Water/Sewage treatment plants Petrochemical - Refineries Light Industrial - Biomedical Heavy Industrial - Cement Heavy Industrial - Pulp & Paper Light Industrial - Pharmaceutical Light Industrial - Semiconductor Electric Power - Nuclear Power Plant Electric Power - Nuclear power plant
zip is numeric, it discards zeros on the left. no records with zip=00000iso_prop_18_22: zip is numeric, it discards zeros on the left. 221 records with zip=00000postalcode is numeric, it discards zeros on the leftcinfin_master_adj: postalcode is char. postalcode=00000. 21 states, mostly around 200k per state, small relative to statewise total sum. I propose: discard these records.zip_isoterr: derived from iso_prop_18_22. zipcd numeric. terr char3mapping_isoterr_bg1 : terr char3 derived from excel file. one terr code has different states. thus we should use st-terr pair.terrdatacube_bg1cic: terr char3in iso_prop_18_22, there are some zip code that have multiple terr code. remedy: choose the terr that has maximum number of counts
business_unit = MIDDLE MARKETSMarketSegments: Peril column has the breakdown we need.
Peril | PerilModel | SourcePeril |
|---|---|---|
| CS | SCS | OW |
| CS | SCS | SCS |
| CS | SCS | TH |
| EQ | EQ | EQ |
| EQ | EQ | EQ CovC Only |
| EQ | EQ | EQ Full Coverage |
| FF | FF | FF |
| HU | HU | HU |
| WF | WF | WF |
| WF | WF | Wildfire |
| WT | SCS | Winter |
TIV column sums up everything, so we will use that column
occupancy_type_desc in cf_datamart and occupancy in cinfin_master_adj
Occupancy
Oil Distributing, Oil Terminals and LPG Tank Farms, Excluding Stock I matchnot manufacturers, but does repair and maintenance
general commercial Reasons: general commercial Reasons: Bars and Taverns Bowling Alleys Boys’ and Girls’ Camps Dance Halls, Ballrooms & Discotheques Drive-in Theaters Golf Clubs, Tennis Clubs and Similar Sports Facilities without Cooking Halls and Auditoriums Motion Picture Studios Museums, Libraries, Art Galleries (non-profit) Recreational Facilities Noc Recreational Facilities, NOC Skating Rinks–Roller Rinks Theaters
all records in datamart has minor_class_desc = Electric Generating Stations - Public Ut records in data mart do not have nuclear power plant
minor_class_desc = nursing and convalescnet homes or penal institution.
minor_class_desc = Governmental Offices or Other Public Buildings, Fire Dept., Police, Water/Sewer
EARNED_PREMIUM do not contain TOL and have zero LOSS_LAE_INCURRED.EARNED_PREMIUM contain TOL (categorical loss type) and have nonzero LOSS_LAE_INCURRED.This information is provided by ISO DataCube™: Detail and Statistics of verisk % cite: https://www.verisk.com/4a79d9/siteassets/media/downloads/underwriting/iso-datacube-detail-and-statistics.pdf
Construction Not AvailableFire ResistiveFrameJoisted MasonryMasonry Non-CombustibleModified Fire ResistiveNon-CombustibleNot ApplicableAll OtherFire & LightningNot AvailableWater DamageWindBurglary & TheftMoldVandalismFreezingSprinkler LeakageCollapseExplosionRiot & Civil CommotionHail• Rating: Class rated versus schedule (specific) rated, where applicable • Sprinklered: Sprinklered versus non-sprinklered, where applicable
• Size of loss range
libname iso "/sas/data/project/EG/ActShared/ISO_DataCube/Scrubbed_DataCube_Tables";
/* Count distinct categories */
proc sql;
select count(distinct COV) as num_cov_categories
from iso.iso_prop_20_24;
quit;
/* List all categories */
proc sql;
select distinct Coverage
from iso.iso_prop_20_24
where Coverage is not missing
order by Coverage;
quit;
libname iso "/sas/data/project/EG/ActShared/ISO_DataCube/Scrubbed_DataCube_Tables";
/* Count distinct categories */
proc sql;
select count(distinct Construction_Type) as num_categories
from iso.iso_prop_20_24;
quit;
/* List all categories */
proc sql;
select distinct Construction_Type
from iso.iso_prop_20_24
where Construction_Type is not missing
order by Construction_Type;
quit;
libname iso "/sas/data/project/EG/ActShared/ISO_DataCube/Scrubbed_DataCube_Tables";
/* Count distinct categories */
proc sql;
select count(CAT_code) as num_categories
from iso.iso_prop_20_24;
quit;
/* List all categories */
proc sql;
select distinct CAT_code
from iso.iso_prop_20_24
where CAT_code is not missing
order by CAT_code;
quit;
libname iso "/sas/data/project/EG/ActShared/ISO_DataCube/Scrubbed_DataCube_Tables";
/* Count distinct categories */
proc sql;
select count(CAT_event) as num_categories
from iso.iso_prop_20_24;
quit;
/* List all categories */
proc sql;
select distinct CAT_event
from iso.iso_prop_20_24
where CAT_event is not missing
order by CAT_event;
quit;
Start working to aggregate EARNED_PREMIUM & LOSS_LAE_INCURRED by TOL (type of loss / peril), ZIPCD (zip code) and ST (state). Look through the rest of the fields.
Earend premium represents the portion of the written premium for which coverage has laready been provied as of a certian point in time.
Losses + LAE takes up most of insurance cost and thus the premium.
Peril mix for each region and statewise peril permissible loss ratios
| Region | SCS AAL | HU AAL | EQ AAL | NC AAL | Total AAL |
|---|---|---|---|---|---|
| Region 1 | 450,000 | 30,000 | 15,000 | 600,000 | 1,095,000 |
| Region 2 | 50,000 | 25,000 | 12,500 | 500,000 | 587,500 |
| Region 3 | 300,000 | 20,000 | 160,000 | 400,000 | 880,000 |
| Region 4 | 30,000 | 15,000 | 120,000 | 300,000 | 465,000 |
| Region 5 | 20,000 | 240,000 | 5,000 | 200,000 | 465,000 |
| Total | 850,000 | 330,000 | 312,500 | 2,000,000 | 3,492,500 |
The following values are assumed given:
| SCS PLR | HU PLR | EQ PLR | NC PLR |
|---|---|---|---|
| 54.3% | 37.6% | 40.6% | 62.9% |
Damage Ratio = Average Annual Loss per $1000 Total Insured Value
AAL = Average Annual Loss
TIV = Total Insured Value
in-force : currently in our contract historical data vs in-force modeled data vs industry modeled data we are currently doing the second we want to do the third
From the discussion earlier, please join Doug’s territory information onto the ISO data by zip code.
Based on the feedback from Doug yesterday, we can aggregate the ISO TOL (peril) data into two groups: CAT related & non-CAT. The types of CAT events we will be getting from the simulated data are
Lets produce a table that has the following fields:
| Coverage [Building | Contents | time element] |
| Subline (maybe its labeled as SUB) [BGI | BGII | SCL] |
Don’t forget to add the filter for when exposures are included in the data. I will probably have more feedback after we review this data together.
Using this data, we can get the damage ratios for various segments. We will use these for the non-cat portion of the capital allocation factors.
CAF simplified example file actually represents exposure by subline (1:bg1, 2:bg2, 3:scl).cov variable in iso means building/content/indivisible/time element. Since the RMS data do not have this classification, we do not use cov for aggregation.| peril | subline | terr |
|---|---|---|
| WT | 2 | bg2_terr |
| SCS | 2 | scs_terr |
| HU | 2 | bg2_terr |
| eq | 3 | eq_terr |
| ff | 3 | eq_terr |
| non-cat | 1 | bg1_terr |
Based on this, the exposure table to be created is:
| geographical aggregation level | exposure |
|---|---|
| bg2_terr | sub=2 |
| scs_terr | sub=2 |
| eq_terr | sub=3 |
| bg1_terr | sub=1 |
The first three rows are included solely to validate that catastrophic events are being separated correctly; they will not be used in the primary analysis.
| Peril | Geographic Aggregation Level |
|---|---|
| EQ/FF | No records on ISO |
| HU | bg2_terr |
| SCS | bg2_terr |
| WT | bg2_terr |
| non-cat | bg1_terr |
Exposure is not provided for the Time Element (Business Interruption) coverage. Additionally, certain records have had exposure removed due to data quality issues.
Loss after reinsurance recoveries What the company actually retains Equivalent to
The analysis is based on the dataset: /sas/data/project/EG/ActShared/ISO_DataCube/Scrubbed_DataCube_Tables/iso_prop_20_24.sas7bdat
After creating the new categorical variables (is_CAT, CAT_type), retain the following fields for downstream analysis:
YEARSTZIPCDBGII_TerritoryCOVSUBis_CATCAT_typeEARNED_PREMIUMEARNED_EXPOSURETOLEXPOSURE_VIEWLOSS_LAE_INCURREDJoin the main dataset with the ZIP mapping file: /sas/data/project/EG/jmun/zip_mapping.sas7bdat with join key:
ZIPCD (main dataset) = zip (mapping dataset) to add the variable: eq_risk_colorThe data is noisy on the ZIPCD level. Therefore we will aggregate on a territory level. For the time being, let’s prepare two tables:
pseudocode for non-cat: create agg_iso_prop_18_22_noncat select year, ST, SUB, sum(earned_premium), sum(earned_exposure), sum(earned_premium)/ sum(earned_exposure) as damage ratio group by st, sub from /sas/data/project/EG/jmun/agg_iso_prop_18_22.sas7bdat where exposure_view = ‘with exposure’
aggregate over territory
for the scs hurrivan bg2 territory eq : eq terriorty non-cat: state
exposure separated by coverage:
swap covereage with subline
three tables
comparison in Ohio SCS
| Variable | N | Mean | Std Dev | Minimum | 1st Pctl | 5th Pctl | 25th Pctl | Median | 75th Pctl | 95th Pctl | 99th Pctl | Maximum |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| iso earned_exposure | 1338 | 464834.93 | 795836.04 | 0 | 45.8330000 | 903.0070000 | 11402.88 | 90412.69 | 642997.16 | 2019014.47 | 3170730.60 | 7744521.31 |
| iso loss_incurred | 507 | 90107.59 | 482882.05 | 3.0000000 | 321.0000000 | 1762.00 | 7500.00 | 21770.00 | 55288.00 | 265017.00 | 946214.00 | 9453910.00 |
| iso damage_ratio | 507 | 0.3786888 | 1.7265477 | 2.5143807E-6 | 0.000578392 | 0.0018185 | 0.0141434 | 0.0418223 | 0.1660173 | 1.4827292 | 5.6232747 | 31.0825358 |
| rms tiv | 1138 | 5242524800 | 7221842245 | 1386549.96 | 14425624.14 | 60350954.66 | 625804948 | 1956799737 | 7470847820 | 20396616531 | 30575829261 | 59773184221 |
| rms Location_GU_AAL | 1138 | 1258294.71 | 1734993.82 | 213.8191455 | 3835.15 | 16599.87 | 198519.40 | 523674.18 | 1641872.94 | 4804241.39 | 8157281.50 | 14463048.53 |
| rms damage_ratio | 1138 | 0.000285271 | 0.000101462 | 0.000049453 | 0.000093600 | 0.000130112 | 0.000207177 | 0.000277313 | 0.000356425 | 0.000463171 | 0.000530662 | 0.000649731 |
KA more variance
ground up and gross
iso ground up
##
it
done so far:
will do:
libname iso "/sas/data/project/EG/ActShared/ISO_DataCube/Scrubbed_DataCube_Tables";
proc sql;
select count(distinct TOL) as num_tol_categories
from iso.iso_prop_20_24
where EARNED_PREMIUM = 0 and TOL is not missing;
quit;
The TOL categories are:
/* Assign library to the exact folder */
libname iso "/sas/data/project/EG/ActShared/ISO_DataCube/Scrubbed_DataCube_Tables";
/* Verify dataset exists and is readable */
proc contents data=iso.iso_prop_18_22;
run;
/* Your query using that dataset */
proc sql;
select TOL,
count(*) as N_obs
from iso.iso_prop_18_22
where TOL is not missing
group by TOL
order by N_obs desc;
quit;+
libname iso "/sas/data/project/EG/ActShared/ISO_DataCube/Scrubbed_DataCube_Tables";
/* Step 1: Tag CAT_event into groups */
proc sql;
create table work.cat_event_tagged as
select
CAT_event,
CAT_code,
case
when CAT_event is missing then 'Blank'
when upcase(CAT_event) like '%NON-CATASTROPHE%' then 'Non-catastrophe'
when upcase(CAT_event) like '%HURRICANE%' then 'Hurricane'
when upcase(CAT_event) like '%FIRE'
or upcase(CAT_event) like '%FIRES' then 'Fire'
when upcase(CAT_event) like 'RIOT%'
or upcase(CAT_event) like 'CIVIL%' then 'Riot/Civil'
when upcase(CAT_event) like 'TROPICAL STORM%' then 'Tropical Storm'
when upcase(CAT_event) like 'WIND%'
or upcase(CAT_event) like 'THUNDERSTORM%'
or upcase(CAT_event) like 'TORNADO%' then 'Wind/Thunderstorm/Tornado'
when upcase(CAT_event) like 'WINTER STORM%' then 'Winter Storm'
when upcase(CAT_event) like '%MUDSLIDE' then 'Mudslide'
else 'Other'
end as CAT_group
from iso.iso_prop_20_24;
quit;
proc sql;
select CAT_group,
put(count(*), comma20.) as N_obs_format
from work.cat_event_tagged
group by CAT_group;
quit;
libname iso "/sas/data/project/EG/ActShared/ISO_DataCube/Scrubbed_DataCube_Tables";
/* Step 1: Tag CAT_event into groups */
proc sql;
create table work.cat_event_tagged as
select
CAT_event,
CAT_code,
case
when CAT_event is missing then 'Blank'
when upcase(CAT_event) like '%NON-CATASTROPHE%' then 'Non-catastrophe'
when upcase(CAT_event) like '%HURRICANE%' then 'Hurricane'
when upcase(CAT_event) like '%FIRE'
or upcase(CAT_event) like '%FIRES' then 'Fire'
when upcase(CAT_event) like 'RIOT%'
or upcase(CAT_event) like 'CIVIL%' then 'Riot/Civil'
when upcase(CAT_event) like 'TROPICAL STORM%' then 'Tropical Storm'
when upcase(CAT_event) like 'WIND%'
or upcase(CAT_event) like 'THUNDERSTORM%'
or upcase(CAT_event) like 'TORNADO%' then 'Wind/Thunderstorm/Tornado'
when upcase(CAT_event) like 'WINTER STORM%' then 'Winter Storm'
when upcase(CAT_event) like '%MUDSLIDE' then 'Mudslide'
else 'Other'
end as CAT_group
from iso.iso_prop_18_22;
quit;
proc sql;
select CAT_group,
put(count(*), comma20.) as N_obs_format
from work.cat_event_tagged
group by CAT_group;
quit;
proc sql;
select distinct CAT_event
from iso.iso_prop_18_22
where CAT_code ne '0000'
and CAT_event is not missing
and upcase(CAT_event) like '%EARTHQUAKE%';
quit;
proc sql;
select distinct CAT_event
from iso.iso_prop_20_24
where CAT_code ne '0000'
and CAT_event is not missing
and upcase(CAT_event) like '%EARTHQUAKE%';
quit;
libname iso "/sas/data/project/EG/ActShared/ISO_DataCube/Scrubbed_DataCube_Tables";
/* view top 100. we can see that CAT_event is in the format of Tropical Storm XXX, for example, Tropical Strom Isaias */
proc sql outobs=100;
select *
from iso.iso_prop_20_24
where CAT_event is not missing
and upcase(CAT_event) like '%TROPICAL STORM%';
quit;
proc sql;
select distinct CAT_event
from iso.iso_prop_20_24
where CAT_event is not missing
and upcase(CAT_event) like '%TROPICAL STORM%'
order by CAT_event;
quit;
libname iso “/sas/data/project/EG/ActShared/ISO_DataCube/Scrubbed_DataCube_Tables”;
proc sql; select case when upcase(coalescec(TOL,’’)) like ‘%WIND%’ or upcase(coalescec(TOL,’’)) like ‘%HAIL%’ then 1 else 0 end as tol_flag,
case
when substr(upcase(coalescec(CAT_event,'')),1,4) = 'WIND'
or substr(upcase(coalescec(CAT_event,'')),1,13) = 'THUNDERSTORM'
or substr(upcase(coalescec(CAT_event,'')),1,7) = 'TORNADO'
then 1 else 0
end as cat_flag,
count(*) as N_obs
from iso.iso_prop_18_22
where TOL is not missing
group by calculated tol_flag, calculated cat_flag
order by tol_flag, cat_flag ; quit;
proc sql; select case when upcase(coalescec(TOL,’’)) like ‘%WIND%’ or upcase(coalescec(TOL,’’)) like ‘%HAIL%’ then 1 else 0 end as tol_flag,
case
when substr(upcase(coalescec(CAT_event,'')),1,4) = 'WIND'
or substr(upcase(coalescec(CAT_event,'')),1,13) = 'THUNDERSTORM'
or substr(upcase(coalescec(CAT_event,'')),1,7) = 'TORNADO'
then 1 else 0
end as cat_flag,
count(*) as N_obs
from iso.iso_prop_10_24
where TOL is not missing
group by calculated tol_flag, calculated cat_flag
order by tol_flag, cat_flag ; quit;
libname iso "/sas/data/project/EG/ActShared/ISO_DataCube/Scrubbed_DataCube_Tables";
proc sql outobs=10;
select *
from iso.iso_prop_18_22
where EARNED_EXPOSURE > 0;
quit;
proc sql outobs=10;
select *
from iso.iso_prop_18_22
where EARNED_EXPOSURE > 0
and TOL is not missing;
quit;
proc sql;
create table work.grouping as
select
a.YEAR,
a.ST,
a.ZIPCD,
a.BGII_Territory,
a.COV,
a.SUB,
a.EARNED_PREMIUM,
a.EARNED_EXPOSURE,
a.TOL,
a.EXPOSURE_VIEW,
a.LOSS_LAE_INCURRED,
b.eq_risk_color,
case
when upcase(coalescec(a.CAT_event,'')) like '%MUDSLIDE%' then 'Mudslide'
when upcase(coalescec(a.CAT_event,'')) like 'RIOT%'
or upcase(coalescec(a.CAT_event,'')) like 'CIVIL%' then 'Civil'
when upcase(coalescec(a.CAT_event,'')) like '%HURRICANE%'
or upcase(coalescec(a.CAT_event,'')) like '%TROPICAL STORM%' then 'HU'
when upcase(coalescec(a.CAT_event,'')) like '%WINTER STORM%' then 'WS'
when (upcase(coalescec(a.CAT_event,'')) like '%WIND%'
or upcase(coalescec(a.CAT_event,'')) like '%THUNDERSTORM%'
or upcase(coalescec(a.CAT_event,'')) like '%TORNADO%')
or (upcase(coalescec(a.TOL,'')) like '%WIND%'
or upcase(coalescec(a.TOL,'')) like '%HAIL%')
then 'SCS'
when upcase(coalescec(a.CAT_event,'')) like '%FIRE%' then 'Fire'
else ''
end as CAT_type length=10,
case
when upcase(coalescec(a.CAT_event,'')) like '%MUDSLIDE%' then 0
when upcase(coalescec(a.CAT_event,'')) like 'RIOT%'
or upcase(coalescec(a.CAT_event,'')) like 'CIVIL%' then 0
when a.CAT_code = '0000' then 0
when upcase(coalescec(a.CAT_event,'')) like '%HURRICANE%'
or upcase(coalescec(a.CAT_event,'')) like '%TROPICAL STORM%' then 1
when upcase(coalescec(a.CAT_event,'')) like '%WINTER STORM%' then 1
when (upcase(coalescec(a.CAT_event,'')) like '%WIND%'
or upcase(coalescec(a.CAT_event,'')) like '%THUNDERSTORM%'
or upcase(coalescec(a.CAT_event,'')) like '%TORNADO%')
or (upcase(coalescec(a.TOL,'')) like '%WIND%'
or upcase(coalescec(a.TOL,'')) like '%HAIL%')
then 1
when upcase(coalescec(a.CAT_event,'')) like '%FIRE%' then 1
else 0
end as is_CAT
from iso.iso_prop_20_24 as a
left join mylib.zip_mapping as b
on a.ZIPCD = put(b.zip, z5.)
;
quit;
proc sql;
create table mylib.agg_iso_prop_20_24 as
select
YEAR,
ST,
ZIPCD,
BGII_Territory,
COV,
SUB,
EXPOSURE_VIEW,
TOL,
CAT_type,
is_CAT,
eq_risk_color,
sum(EARNED_PREMIUM) as EARNED_PREMIUM,
sum(EARNED_EXPOSURE) as EARNED_EXPOSURE,
sum(LOSS_LAE_INCURRED) as LOSS_LAE_INCURRED
from work.grouping
group by
YEAR,
ST,
ZIPCD,
BGII_Territory,
COV,
SUB,
EXPOSURE_VIEW,
TOL,
CAT_type,
is_CAT,
eq_risk_color
;
quit;
proc sql;
create table work.grouping as
select
a.YEAR,
a.ST,
a.ZIPCD,
a.BGII_Territory,
a.COV,
a.SUB,
a.EARNED_PREMIUM,
a.EARNED_EXPOSURE,
a.TOL,
a.EXPOSURE_VIEW,
a.LOSS_LAE_INCURRED,
b.eq_risk_color,
case
when upcase(coalescec(a.CAT_event,'')) like '%MUDSLIDE%' then 'Mudslide'
when upcase(coalescec(a.CAT_event,'')) like 'RIOT%'
or upcase(coalescec(a.CAT_event,'')) like 'CIVIL%' then 'Civil'
when upcase(coalescec(a.CAT_event,'')) like '%HURRICANE%'
or upcase(coalescec(a.CAT_event,'')) like '%TROPICAL STORM%' then 'HU'
when upcase(coalescec(a.CAT_event,'')) like '%WINTER STORM%' then 'WS'
when (upcase(coalescec(a.CAT_event,'')) like '%WIND%'
or upcase(coalescec(a.CAT_event,'')) like '%THUNDERSTORM%'
or upcase(coalescec(a.CAT_event,'')) like '%TORNADO%')
or (upcase(coalescec(a.TOL,'')) like '%WIND%'
or upcase(coalescec(a.TOL,'')) like '%HAIL%')
then 'SCS'
when upcase(coalescec(a.CAT_event,'')) like '%FIRE%' then 'Fire'
else ''
end as CAT_type length=10,
case
when upcase(coalescec(a.CAT_event,'')) like '%MUDSLIDE%' then 0
when upcase(coalescec(a.CAT_event,'')) like 'RIOT%'
or upcase(coalescec(a.CAT_event,'')) like 'CIVIL%' then 0
when a.CAT_code = '0000' then 0
when upcase(coalescec(a.CAT_event,'')) like '%HURRICANE%'
or upcase(coalescec(a.CAT_event,'')) like '%TROPICAL STORM%' then 1
when upcase(coalescec(a.CAT_event,'')) like '%WINTER STORM%' then 1
when (upcase(coalescec(a.CAT_event,'')) like '%WIND%'
or upcase(coalescec(a.CAT_event,'')) like '%THUNDERSTORM%'
or upcase(coalescec(a.CAT_event,'')) like '%TORNADO%')
or (upcase(coalescec(a.TOL,'')) like '%WIND%'
or upcase(coalescec(a.TOL,'')) like '%HAIL%')
then 1
when upcase(coalescec(a.CAT_event,'')) like '%FIRE%' then 1
else 0
end as is_CAT
from iso.iso_prop_18_22 as a
left join mylib.zip_mapping as b
on a.ZIPCD = b.zip
;
quit;
proc sql;
create table mylib.agg_iso_prop_18_22 as
select
YEAR,
ST,
ZIPCD,
BGII_Territory,
COV,
SUB,
EXPOSURE_VIEW,
TOL,
CAT_type,
is_CAT,
eq_risk_color,
sum(EARNED_PREMIUM) as EARNED_PREMIUM,
sum(EARNED_EXPOSURE) as EARNED_EXPOSURE,
sum(LOSS_LAE_INCURRED) as LOSS_LAE_INCURRED
from work.grouping
group by
YEAR,
ST,
ZIPCD,
BGII_Territory,
COV,
SUB,
EXPOSURE_VIEW,
TOL,
CAT_type,
is_CAT,
eq_risk_color
;
quit;
import pandas as pd
import os
import re
# ✅ Folder containing your files
folder_path = "RMS 2025 CAT"
results = []
# ✅ Loop through all CSV files
for file in os.listdir(folder_path):
if file.endswith(".csv"):
file_path = os.path.join(folder_path, file)
# ✅ Extract catastrophe tag from filename
# looks for EQ / FF / HU / SCS / WT
match = re.search(r'\b(EQ|FF|HU|SCS|WT)\b', file)
cat_event = match.group(1) if match else "UNKNOWN"
print(f"Processing: {file} → {cat_event}")
# ✅ Load data
df = pd.read_csv(file_path)
df.columns = df.columns.str.strip()
# ✅ Convert numeric columns
cols = ["TIV", "Location_GU_AAL", "Location_GR_AAL"]
df[cols] = df[cols].apply(pd.to_numeric, errors="coerce")
# ✅ Aggregate by state
agg = (
df.groupby("STATECODE", dropna=False)
.agg(
TIV_sum=("TIV", "sum"),
GU_AAL_sum=("Location_GU_AAL", "sum"),
GR_AAL_sum=("Location_GR_AAL", "sum"),
)
.reset_index()
)
# ✅ Compute ratios
agg["GU_damage_ratio"] = agg["GU_AAL_sum"] / agg["TIV_sum"]
agg["GR_damage_ratio"] = agg["GR_AAL_sum"] / agg["TIV_sum"]
# ✅ Add catastrophe event label
agg["cat_event"] = cat_event
# ✅ Store result
results.append(agg)
# ✅ Combine all files
final_df = pd.concat(results, ignore_index=True)
# ✅ Optional: sort
final_df = final_df.sort_values(["cat_event", "GU_damage_ratio"], ascending=[True, False])
# ✅ Save output
output_path = os.path.join(folder_path, "state_damage_ratios_by_cat.csv")
final_df.to_csv(output_path, index=False)
print("Saved to:", output_path)