diff --git a/pyproject.toml b/pyproject.toml index d36887230..2eabce8c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,7 @@ dependencies = [ "pulp<2.8.0", "psycopg2", "pyaml", + "pyarrow", "pypsa==0.20.1", "pydantic<2.0", "rasterio", diff --git a/src/egon/data/cli.py b/src/egon/data/cli.py index b38597097..24a7f9956 100644 --- a/src/egon/data/cli.py +++ b/src/egon/data/cli.py @@ -101,12 +101,13 @@ ) @click.option( "--household-electrical-demand-source", - type=click.Choice(["bottom-up-profiles", "slp"]), + type=click.Choice(["bottom-up-profiles", "slp", "lpg"]), default="slp", help=( "Choose the source to calculate and allocate household electrical" - "demands. There are currently two options:" - "'bottom-up-profiles' and 'slp' (Standard Load Profiles)" + "demands. There are currently three options:" + "'bottom-up-profiles' (IEE profiles), 'slp' (Standard Load Profiles)," + " and 'lpg' (Load Profile Generator profiles)" ), show_default=True, ) diff --git a/src/egon/data/datasets.yml b/src/egon/data/datasets.yml index 13794e10b..437606ba0 100644 --- a/src/egon/data/datasets.yml +++ b/src/egon/data/datasets.yml @@ -511,6 +511,10 @@ hh_demand_profiles: path_testmode: "hh_el_load_profiles_2511.hdf" path: "hh_el_load_profiles_100k.hdf" + lpg_household_electricity_demand_profiles: + path_testmode: "hh_el_load_profiles_lpg_testmode.parquet" + path: "hh_el_load_profiles_lpg_10k.parquet" + zensus_household_types: path: "Zensus2011_Personen.csv" diff --git a/src/egon/data/datasets/electricity_demand/__init__.py b/src/egon/data/datasets/electricity_demand/__init__.py index 8c9a72fd3..c86838785 100644 --- a/src/egon/data/datasets/electricity_demand/__init__.py +++ b/src/egon/data/datasets/electricity_demand/__init__.py @@ -14,6 +14,9 @@ HouseholdElectricityProfilesOfBuildings, get_iee_hh_demand_profiles_raw, ) +from egon.data.datasets.electricity_demand_timeseries.lpg_hh_profiles import ( + get_lpg_hh_demand_profiles_raw, +) from egon.data.datasets.electricity_demand_timeseries.hh_profiles import ( HouseholdElectricityProfilesInCensusCells, ) @@ -191,7 +194,13 @@ def get_annual_household_el_demand_cells(): ) # Read demand profiles from egon-data-bundle - df_profiles = get_iee_hh_demand_profiles_raw() + source = egon.data.config.settings()["egon-data"][ + "--household-electrical-demand-source" + ] + if source == "lpg": + df_profiles = get_lpg_hh_demand_profiles_raw() + else: + df_profiles = get_iee_hh_demand_profiles_raw() def ve(s): raise (ValueError(s)) diff --git a/src/egon/data/datasets/electricity_demand_timeseries/hh_buildings.py b/src/egon/data/datasets/electricity_demand_timeseries/hh_buildings.py index 8dc5103a8..438cba36f 100755 --- a/src/egon/data/datasets/electricity_demand_timeseries/hh_buildings.py +++ b/src/egon/data/datasets/electricity_demand_timeseries/hh_buildings.py @@ -730,7 +730,16 @@ def get_building_peak_loads(): df_buildings_and_profiles = df_buildings_and_profiles.fillna(np.nan) # Read demand profiles from egon-data-bundle - df_profiles = get_iee_hh_demand_profiles_raw() + source = egon.data.config.settings()["egon-data"][ + "--household-electrical-demand-source" + ] + if source == "lpg": + from egon.data.datasets.electricity_demand_timeseries.lpg_hh_profiles import ( + get_lpg_hh_demand_profiles_raw, + ) + df_profiles = get_lpg_hh_demand_profiles_raw() + else: + df_profiles = get_iee_hh_demand_profiles_raw() def ve(s): raise (ValueError(s)) diff --git a/src/egon/data/datasets/electricity_demand_timeseries/hh_profiles.py b/src/egon/data/datasets/electricity_demand_timeseries/hh_profiles.py index 32afb83ef..216d17932 100644 --- a/src/egon/data/datasets/electricity_demand_timeseries/hh_profiles.py +++ b/src/egon/data/datasets/electricity_demand_timeseries/hh_profiles.py @@ -11,6 +11,7 @@ from itertools import cycle, product from pathlib import Path +import logging import os import random @@ -26,6 +27,11 @@ from egon.data.datasets.scenario_parameters import get_scenario_year from egon.data.datasets.zensus_mv_grid_districts import MapZensusGridDistricts import egon.data.config +from egon.data.datasets.electricity_demand_timeseries.lpg_hh_profiles import ( + get_lpg_hh_demand_profiles_raw, +) + +logger = logging.getLogger(__name__) Base = declarative_base() engine = db.engine() @@ -255,6 +261,10 @@ class HouseholdDemands(Dataset): "path_testmode": "hh_el_load_profiles_2511.hdf", "path": "hh_el_load_profiles_100k.hdf", }, + "lpg_household_electricity_demand_profiles": { + "path_testmode": "hh_el_load_profiles_lpg_testmode.parquet", + "path": "hh_el_load_profiles_lpg_10k.parquet", + }, "zensus_household_types": {"path": "Zensus2011_Personen.csv"}, }, ) @@ -1359,7 +1369,7 @@ def get_cell_demand_profile_ids(df_cell, pool_size): # can be parallelized with grouping df_zensus_cells by grid_id/nuts3/nuts1 -def assign_hh_demand_profiles_to_cells(df_zensus_cells, df_iee_profiles): +def assign_hh_demand_profiles_to_cells(df_zensus_cells, df_hh_demand_profiles): """ Assign household demand profiles to each census cell. @@ -1373,7 +1383,7 @@ def assign_hh_demand_profiles_to_cells(df_zensus_cells, df_iee_profiles): df_zensus_cells: pd.DataFrame Household type parameters. Each row representing one household. Hence, multiple rows per zensus cell. - df_iee_profiles: pd.DataFrame + df_hh_demand_profiles: pd.DataFrame Household load profile data * Index: Times steps as serial integers @@ -1405,7 +1415,7 @@ def assign_hh_demand_profiles_to_cells(df_zensus_cells, df_iee_profiles): df_hh_profiles_in_census_cells.rename_axis("grid_id") ) - pool_size = df_iee_profiles.groupby(level=0, axis=1).size() + pool_size = df_hh_demand_profiles.groupby(level=0, axis=1).size() # only use non zero entries df_zensus_cells = df_zensus_cells.loc[df_zensus_cells["hh_10types"] != 0] @@ -1435,7 +1445,7 @@ def assign_hh_demand_profiles_to_cells(df_zensus_cells, df_iee_profiles): # can be parallelized with grouping df_zensus_cells by grid_id/nuts3/nuts1 def adjust_to_demand_regio_nuts3_annual( - df_hh_profiles_in_census_cells, df_iee_profiles, df_demand_regio + df_hh_profiles_in_census_cells, df_hh_demand_profiles, df_demand_regio ): """ Computes the profile scaling factor for alignment to demand regio data @@ -1448,7 +1458,7 @@ def adjust_to_demand_regio_nuts3_annual( ---------- df_hh_profiles_in_census_cells: pd.DataFrame Result of :func:`assign_hh_demand_profiles_to_cells`. - df_iee_profiles: pd.DataFrame + df_hh_demand_profiles: pd.DataFrame Household load profile data * Index: Times steps as serial integers @@ -1473,7 +1483,7 @@ def adjust_to_demand_regio_nuts3_annual( # take all profiles of one nuts3, aggregate and sum # profiles in Wh nuts3_profiles_sum_annual = ( - df_iee_profiles.loc[:, nuts3_profile_ids].sum().sum() + df_hh_demand_profiles.loc[:, nuts3_profile_ids].sum().sum() ) # Scaling Factor @@ -1495,7 +1505,7 @@ def adjust_to_demand_regio_nuts3_annual( def get_load_timeseries( - df_iee_profiles, + df_hh_demand_profiles, df_hh_profiles_in_census_cells, cell_ids, year, @@ -1510,7 +1520,7 @@ def get_load_timeseries( Parameters ---------- - df_iee_profiles: pd.DataFrame + df_hh_demand_profiles: pd.DataFrame Household load profile data in Wh * Index: Times steps as serial integers @@ -1538,7 +1548,7 @@ def get_load_timeseries( Aggregated time series for given `cell_ids` or peak load of this time series in MWh. """ - timesteps = len(df_iee_profiles) + timesteps = len(df_hh_demand_profiles) if aggregate: full_load = pd.Series( data=np.zeros(timesteps), dtype=np.float64, index=range(timesteps) @@ -1555,7 +1565,7 @@ def get_load_timeseries( ): if aggregate: part_load = ( - df_iee_profiles.loc[:, df["cell_profile_ids"].sum()].sum( + df_hh_demand_profiles.loc[:, df["cell_profile_ids"].sum()].sum( axis=1 ) * factor @@ -1564,7 +1574,7 @@ def get_load_timeseries( full_load = full_load.add(part_load) elif not aggregate: part_load = ( - df_iee_profiles.loc[:, df["cell_profile_ids"].sum()] + df_hh_demand_profiles.loc[:, df["cell_profile_ids"].sum()] * factor / 1e6 ) # from Wh to MWh @@ -1619,16 +1629,27 @@ def gen_profile_names(n): np.random.seed(RANDOM_SEED) # Read demand profiles from egon-data-bundle - df_iee_profiles = get_iee_hh_demand_profiles_raw() + source = egon.data.config.settings()["egon-data"][ + "--household-electrical-demand-source" + ] + logger.info("Household electrical demand source: %s", source) + if source == "lpg": + df_hh_demand_profiles = get_lpg_hh_demand_profiles_raw() + else: + df_hh_demand_profiles = get_iee_hh_demand_profiles_raw() + logger.debug("Profiles loaded: %d columns", len(df_hh_demand_profiles.columns)) # Write raw profiles into db - write_hh_profiles_to_db(df_iee_profiles) + logger.info("Writing raw household profiles to database") + write_hh_profiles_to_db(df_hh_demand_profiles) # Process profiles for further use - df_iee_profiles = set_multiindex_to_profiles(df_iee_profiles) + df_hh_demand_profiles = set_multiindex_to_profiles(df_hh_demand_profiles) # Download zensus household NUTS-1 data with family type and age categories + logger.info("Fetching census NUTS-1 household data") df_census_households_nuts1_raw = get_census_households_nuts1_raw() + logger.debug("NUTS-1 raw rows: %d", len(df_census_households_nuts1_raw)) # Reduce age intervals and remove kids df_census_households_nuts1 = process_nuts1_census_data( @@ -1648,7 +1669,9 @@ def gen_profile_names(n): ) # Query census household grid data with family type + logger.info("Fetching census household grid data") df_census_households_grid = get_census_households_grid() + logger.debug("Census grid cells: %d", len(df_census_households_grid)) # fill cells with missing household distribution values but population # by hh distribution value of random cell with same population value @@ -1657,18 +1680,25 @@ def gen_profile_names(n): ) # Refine census household grid data with additional NUTS-1 level attributes + logger.info("Refining census data at cell level") df_census_households_grid_refined = refine_census_data_at_cell_level( df_census_households_grid, df_census_households_nuts1 ) + logger.debug("Refined census rows: %d", len(df_census_households_grid_refined)) write_refinded_households_to_db(df_census_households_grid_refined) # Allocate profile ids to each cell by census data + logger.info("Assigning household demand profiles to census cells") df_hh_profiles_in_census_cells = assign_hh_demand_profiles_to_cells( - df_census_households_grid_refined, df_iee_profiles + df_census_households_grid_refined, df_hh_demand_profiles + ) + logger.debug( + "Cells with profile assignments: %d", len(df_hh_profiles_in_census_cells) ) # Annual household electricity demand on NUTS-3 level (demand regio) + logger.info("Fetching demand regio annual demand on NUTS-3 level") df_demand_regio = db.select_dataframe( sql=f""" SELECT year, nuts3, SUM (demand) as demand_mWha @@ -1679,8 +1709,9 @@ def gen_profile_names(n): ) # Scale profiles to meet demand regio annual demand projections + logger.info("Scaling profiles to demand regio NUTS-3 annual demand") df_hh_profiles_in_census_cells = adjust_to_demand_regio_nuts3_annual( - df_hh_profiles_in_census_cells, df_iee_profiles, df_demand_regio + df_hh_profiles_in_census_cells, df_hh_demand_profiles, df_demand_regio ) df_hh_profiles_in_census_cells = ( @@ -1698,6 +1729,10 @@ def gen_profile_names(n): ) # Write allocation table into database + logger.info( + "Writing %d cell profile assignments to database", + len(df_hh_profiles_in_census_cells), + ) HouseholdElectricityProfilesInCensusCells.__table__.drop( bind=engine, checkfirst=True ) @@ -1710,6 +1745,7 @@ def gen_profile_names(n): HouseholdElectricityProfilesInCensusCells, df_hh_profiles_in_census_cells.to_dict(orient="records"), ) + logger.info("houseprofiles_in_census_cells completed") def get_houseprofiles_in_census_cells(): @@ -1869,7 +1905,7 @@ def get_demand_regio_hh_profiles_from_db(year): """ query = f""" - Select * + Select * FROM {HouseholdDemands.sources.tables["demandregio_household_load_profiles"]} Where year = year""" @@ -1927,6 +1963,14 @@ def tuple_format(x): "--household-electrical-demand-source" ] + logger.info( + "mv_grid_district_HH_electricity_load: scenario=%s year=%d method=%s cells=%d", + scenario_name, + scenario_year, + method, + len(cells), + ) + if method == "slp": # Import demand regio timeseries demand per nuts3 area dr_series = pd.read_sql_query( @@ -1938,6 +1982,14 @@ def tuple_format(x): dr_series = dr_series[dr_series["year"] == scenario_year] dr_series.drop(columns=["year"], inplace=True) dr_series.set_index("nuts3", inplace=True) + if dr_series.index.duplicated().any(): + logger.warning( + "Duplicate nuts3 entries found in demandregio_household_load_profiles " + "for year=%d — keeping first occurrence. " + "The table may have been written multiple times.", + scenario_year, + ) + dr_series = dr_series[~dr_series.index.duplicated(keep="first")] dr_series = dr_series.squeeze() # Population data per cell_id is used to scale the demand per nuts3 @@ -1975,23 +2027,37 @@ def tuple_format(x): mvgd_profiles.reset_index(inplace=True) - elif method == "bottom-up-profiles": + elif method in ("bottom-up-profiles", "lpg"): # convert profile ids to tuple (type, id) format cells["cell_profile_ids"] = cells["cell_profile_ids"].apply( lambda x: list(map(tuple_format, x)) ) # Read demand profiles from egon-data-bundle - df_iee_profiles = get_iee_hh_demand_profiles_raw() + if method == "lpg": + df_hh_demand_profiles = get_lpg_hh_demand_profiles_raw() + else: + df_hh_demand_profiles = get_iee_hh_demand_profiles_raw() # Process profiles for further use - df_iee_profiles = set_multiindex_to_profiles(df_iee_profiles) + df_hh_demand_profiles = set_multiindex_to_profiles(df_hh_demand_profiles) # Create aggregated load profile for each MV grid district + grid_districts = cells["bus_id"].unique() + logger.info( + "Aggregating load profiles for %d MV grid districts", len(grid_districts) + ) mvgd_profiles_dict = {} - for grid_district, data in cells.groupby("bus_id"): + for i, (grid_district, data) in enumerate(cells.groupby("bus_id")): + if i % 100 == 0: + logger.debug( + "Processing grid district %d / %d (bus_id=%s)", + i + 1, + len(grid_districts), + grid_district, + ) mvgd_profile = get_load_timeseries( - df_iee_profiles=df_iee_profiles, + df_hh_demand_profiles=df_hh_demand_profiles, df_hh_profiles_in_census_cells=data, cell_ids=data.index, year=scenario_year, @@ -2009,6 +2075,11 @@ def tuple_format(x): mvgd_profiles.columns = ["bus_id", "p_set"] # Add remaining columns + logger.info( + "Writing %d MV grid district profiles to database (scenario=%s)", + len(mvgd_profiles), + scenario_name, + ) mvgd_profiles["scn_name"] = scenario_name # Insert data into respective database table @@ -2068,10 +2139,10 @@ def get_scaled_profiles_from_db( ) profile_ids = cell_demand_metadata.cell_profile_ids.sum() - df_iee_profiles = get_hh_profiles_from_db(profile_ids) + df_hh_demand_profiles = get_hh_profiles_from_db(profile_ids) scaled_profiles = get_load_timeseries( - df_iee_profiles=df_iee_profiles, + df_hh_demand_profiles=df_hh_demand_profiles, df_hh_profiles_in_census_cells=cell_demand_metadata, cell_ids=cell_demand_metadata.index.to_list(), year=year, diff --git a/src/egon/data/datasets/electricity_demand_timeseries/lpg_hh_profiles.py b/src/egon/data/datasets/electricity_demand_timeseries/lpg_hh_profiles.py new file mode 100644 index 000000000..12fd7bc48 --- /dev/null +++ b/src/egon/data/datasets/electricity_demand_timeseries/lpg_hh_profiles.py @@ -0,0 +1,192 @@ +""" +LPG household electricity demand profiles. + +Provides the CHR/CHS → IEE bucket mapping and a loader function that reads +the LPG Parquet bundle and returns a DataFrame with IEE-compatible column +names, so all downstream code in hh_profiles.py remains unchanged. +""" + +from pathlib import Path +import logging +import os + +import pandas as pd + +import egon.data.config + +logger = logging.getLogger(__name__) + +# Mapping from LPG household types (CHR/CHS series) to IEE bucket types +# (SR, SO, SK, PR, PO, P1, P2, P3, OR, OO). +CH_TO_HH_TYPE = { + # SR — Single Retiree + "CHR23": "SR", # Single man over 65 years + "CHR24": "SR", # Single woman over 65 years + "CHR30": "SR", # Single, Retired Man + "CHR31": "SR", # Single, Retired Woman + # SO — Single Adult + "CHR06": "SO", # Jak Jobless + "CHR07": "SO", # Single with work + "CHR09": "SO", # Single woman, 30-64 years, with work + "CHR10": "SO", # Single man, 30-64, shift worker + "CHR11": "SO", # Student, Female, Philosophy + "CHR12": "SO", # Student 2, Male, Philosophy + "CHR13": "SO", # Student with Work + "CHR25": "SO", # Single woman under 30 years with work + "CHR26": "SO", # Single woman under 30 years without work + "CHR28": "SO", # Single man under 30 years without work + "CHR29": "SO", # Single man under 30 years with work + "CHR35": "SO", # Single woman, 30-64 years, with work + "CHR36": "SO", # Single woman, 30-64 years, without work + "CHR37": "SO", # Single man, 30-64 years, with work + "CHR38": "SO", # Single man, 30-64 years, without work + # SK — Single Parent Adult + "CHR08": "SK", # Single woman, 2 children, with work + "CHR22": "SK", # Single woman, 1 child, with work + "CHR42": "SK", # Single man with 2 children, with work + "CHR43": "SK", # Single man with 1 child, with work + "CHR46": "SK", # Single woman, 1 child, without work + "CHR47": "SK", # Single woman, 2 children, without work + "CHR50": "SK", # Single woman with 3 children, without work + # PO — Couple Adults + "CHR01": "PO", # Couple both at Work + "CHR02": "PO", # Couple, 30-64 age, with work + "CHR04": "PO", # Couple, 30-64 years, 1 at work, 1 at home + "CHR17": "PO", # Shiftworker Couple + "CHR19": "PO", # Couple, 30-64 years, both at work, with homehelp + "CHR21": "PO", # Couple, 30-64 years, shift worker + "CHR32": "PO", # Couple under 30 years without work + "CHR33": "PO", # Couple under 30 years with work + "CHR34": "PO", # Couple under 30 years, one at work, one at home + "CHR39": "PO", # Couple, 30-64 years, with work + "CHR40": "PO", # Couple, 30-64 years, without work + "CHR55": "PO", # Couple with work around 40 + "CHS12": "PO", # Shiftworker Couple + # PR — Couple Seniors + "CHR16": "PR", # Couple over 65 years + "CHR51": "PR", # Couple over 65 years II + "CHR54": "PR", # Retired Couple, no work + "CHR58": "PR", # Retired Couple, no work, no cooking + "CHS04": "PR", # Retired Couple, no work + # P1 — Couple + 1 child + "CHR03": "P1", # Family, 1 child, both at work + "CHR45": "P1", # Family with 1 child, 1 at work, 1 at home + "CHR49": "P1", # Family with 1 child, without work + "CHR60": "P1", # Family, 1 toddler, one at work, one at home + "CHR61": "P1", # Family, 1 child, both at work, early living pattern + # P2 — Couple + 2 children + "CHR18": "P2", # Family, 2 children, parents without work + "CHR27": "P2", # Family both at work, 2 children + "CHR44": "P2", # Family with 2 children, 1 at work, 1 at home + "CHR48": "P2", # Family with 2 children, without work + "CHR53": "P2", # 2 Parents, 1 Working, 2 Children + "CHR56": "P2", # Couple with 2 children, husband at work + "CHR57": "P2", # Family with 2 Children, Man at work + "CHS01": "P2", # Couple with 2 Children, Dad Employed + # P3 — Couple + 3+ children + "CHR05": "P3", # Family, 3 children, both with work + "CHR20": "P3", # one at work, one work home, 3 children + "CHR41": "P3", # Family with 3 children, both at work + "CHR59": "P3", # Family, 3 children, parents without work + # OR — Multi-person household with seniors + "CHR14": "OR", # 3 adults: Couple, 30-64 + Senior at home + "CHR15": "OR", # Multigenerational Home + # OO — Multi-person household adults + "CHR52": "OO", # Student Flatsharing +} + + +def get_lpg_hh_demand_profiles_raw(): + """Gets and returns household electricity demand profiles from the + Load Profile Generator (LPG), stored in the egon-data-bundle. + + Profiles are generated by LPG for multi-family houses (mfh) and cover + household archetypes of two series: CHR (CHR01-CHR61) and CHS (CHS01, + CHS04, CHS12). All types are mapped to the same 10 Eurostat/IEE household + type buckets used by the IEE profiles (SR, SO, SK, PR, PO, P1, P2, P3, + OR, OO) so all downstream processing remains unchanged. + + Notes + ----- + The LPG profiles are originally at 15-minute resolution for year 2011 + (one CSV per household-type × seed combination). The Parquet bundle file + is prepared externally by: + 1. Reading each CSV (``Electricity_HH1`` column, 35,040 rows) + 2. Resampling 15-min → 1-hour (sum 4 intervals per hour) + 3. Converting kWh → Wh (multiply by 1000) + 4. Storing as a single DataFrame ``(8760, N_profiles)`` via + ``df.to_parquet()`` with a ``DatetimeIndex`` for 2011 + + Column format in Parquet: ``{HH_TYPE}a{id:05d}``, + e.g. ``CHR01a00001``, ``CHS04a00003`` + + The profiles cover year 2011, so no date-alignment step is needed + (unlike IEE profiles which are 2016-based and require dropping day 1). + + Returns + ------- + pd.DataFrame + Table with profiles in columns and time as index, with IEE-compatible + column names (e.g. ``POa00001``, ``SRa00003``). + """ + + def ve(s): + raise ValueError(s) + + dataset = egon.data.config.settings()["egon-data"]["--dataset-boundary"] + + file_section = ( + "path" + if dataset == "Everything" + else ( + "path_testmode" + if dataset == "Schleswig-Holstein" + else ve(f"'{dataset}' is not a valid dataset boundary.") + ) + ) + + from egon.data.datasets.electricity_demand_timeseries.hh_profiles import ( + HouseholdDemands, + ) + + file_path = HouseholdDemands.sources.files[ + "lpg_household_electricity_demand_profiles" + ][file_section] + + download_directory = os.path.join( + "data_bundle_egon_data", "household_electricity_demand_profiles" + ) + + hh_profiles_file = Path(".") / Path(download_directory) / Path(file_path) + + logger.info("Reading LPG household profiles from: %s", hh_profiles_file) + df_lpg_profiles = pd.read_parquet(hh_profiles_file) + logger.debug( + "Loaded LPG profiles: %d profiles, %d timesteps", + df_lpg_profiles.shape[1], + df_lpg_profiles.shape[0], + ) + + # Rename columns from LPG-type format to IEE bucket format. + # Multiple household types map to the same bucket; IDs are renumbered + # sequentially within each bucket so there are no collisions. + bucket_counters = {} + new_columns = [] + for col in df_lpg_profiles.columns: + hh_type = col[:5] # e.g. "CHR01" or "CHS04" + if hh_type not in CH_TO_HH_TYPE: + raise ValueError( + f"Unknown household type '{hh_type}' in LPG profiles file. " + "Update CH_TO_HH_TYPE mapping." + ) + bucket = CH_TO_HH_TYPE[hh_type] + idx = bucket_counters.get(bucket, -1) + 1 + bucket_counters[bucket] = idx + new_columns.append(f"{bucket}a{idx:05d}") + df_lpg_profiles.columns = new_columns + + logger.debug( + "Remapped LPG columns to IEE buckets: %s", + sorted(set(c[:2] for c in new_columns)), + ) + return df_lpg_profiles