ebm 0.99.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. ebm/__init__.py +0 -0
  2. ebm/__main__.py +152 -0
  3. ebm/__version__.py +1 -0
  4. ebm/cmd/__init__.py +0 -0
  5. ebm/cmd/calibrate.py +83 -0
  6. ebm/cmd/calibrate_excel_com_io.py +128 -0
  7. ebm/cmd/heating_systems_by_year.py +18 -0
  8. ebm/cmd/helpers.py +134 -0
  9. ebm/cmd/initialize.py +167 -0
  10. ebm/cmd/migrate.py +92 -0
  11. ebm/cmd/pipeline.py +227 -0
  12. ebm/cmd/prepare_main.py +174 -0
  13. ebm/cmd/result_handler.py +272 -0
  14. ebm/cmd/run_calculation.py +221 -0
  15. ebm/data/area.csv +92 -0
  16. ebm/data/area_new_residential_buildings.csv +3 -0
  17. ebm/data/area_per_person.csv +12 -0
  18. ebm/data/building_code_parameters.csv +9 -0
  19. ebm/data/energy_need_behaviour_factor.csv +6 -0
  20. ebm/data/energy_need_improvements.csv +7 -0
  21. ebm/data/energy_need_original_condition.csv +534 -0
  22. ebm/data/heating_system_efficiencies.csv +13 -0
  23. ebm/data/heating_system_forecast.csv +9 -0
  24. ebm/data/heating_system_initial_shares.csv +1113 -0
  25. ebm/data/holiday_home_energy_consumption.csv +24 -0
  26. ebm/data/holiday_home_stock.csv +25 -0
  27. ebm/data/improvement_building_upgrade.csv +9 -0
  28. ebm/data/new_buildings_residential.csv +32 -0
  29. ebm/data/population_forecast.csv +51 -0
  30. ebm/data/s_curve.csv +40 -0
  31. ebm/energy_consumption.py +307 -0
  32. ebm/extractors.py +115 -0
  33. ebm/heating_system_forecast.py +472 -0
  34. ebm/holiday_home_energy.py +341 -0
  35. ebm/migrations.py +224 -0
  36. ebm/model/__init__.py +0 -0
  37. ebm/model/area.py +403 -0
  38. ebm/model/bema.py +149 -0
  39. ebm/model/building_category.py +150 -0
  40. ebm/model/building_condition.py +78 -0
  41. ebm/model/calibrate_energy_requirements.py +84 -0
  42. ebm/model/calibrate_heating_systems.py +180 -0
  43. ebm/model/column_operations.py +157 -0
  44. ebm/model/construction.py +827 -0
  45. ebm/model/data_classes.py +223 -0
  46. ebm/model/database_manager.py +410 -0
  47. ebm/model/dataframemodels.py +115 -0
  48. ebm/model/defaults.py +30 -0
  49. ebm/model/energy_need.py +6 -0
  50. ebm/model/energy_need_filter.py +182 -0
  51. ebm/model/energy_purpose.py +115 -0
  52. ebm/model/energy_requirement.py +353 -0
  53. ebm/model/energy_use.py +202 -0
  54. ebm/model/enums.py +8 -0
  55. ebm/model/exceptions.py +4 -0
  56. ebm/model/file_handler.py +388 -0
  57. ebm/model/filter_scurve_params.py +83 -0
  58. ebm/model/filter_tek.py +152 -0
  59. ebm/model/heat_pump.py +53 -0
  60. ebm/model/heating_systems.py +20 -0
  61. ebm/model/heating_systems_parameter.py +17 -0
  62. ebm/model/heating_systems_projection.py +3 -0
  63. ebm/model/heating_systems_share.py +28 -0
  64. ebm/model/scurve.py +224 -0
  65. ebm/model/tek.py +1 -0
  66. ebm/s_curve.py +515 -0
  67. ebm/services/__init__.py +0 -0
  68. ebm/services/calibration_writer.py +262 -0
  69. ebm/services/console.py +106 -0
  70. ebm/services/excel_loader.py +66 -0
  71. ebm/services/files.py +38 -0
  72. ebm/services/spreadsheet.py +289 -0
  73. ebm/temp_calc.py +99 -0
  74. ebm/validators.py +565 -0
  75. ebm-0.99.5.dist-info/METADATA +212 -0
  76. ebm-0.99.5.dist-info/RECORD +80 -0
  77. ebm-0.99.5.dist-info/WHEEL +5 -0
  78. ebm-0.99.5.dist-info/entry_points.txt +3 -0
  79. ebm-0.99.5.dist-info/licenses/LICENSE +21 -0
  80. ebm-0.99.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,150 @@
1
+ import typing
2
+ from enum import unique, StrEnum, EnumType
3
+
4
+ import pandas as pd
5
+ from loguru import logger
6
+
7
+ RESIDENTIAL = 'residential'
8
+ NON_RESIDENTIAL = 'non_residential'
9
+
10
+
11
+ class MyEnumType(EnumType):
12
+ def __contains__(cls, value):
13
+ return value in cls._value2member_map_
14
+
15
+
16
+ @unique
17
+ class BuildingCategory(StrEnum, metaclass=MyEnumType):
18
+ HOUSE = 'house'
19
+ APARTMENT_BLOCK = 'apartment_block'
20
+ KINDERGARTEN = 'kindergarten'
21
+ SCHOOL = 'school'
22
+ UNIVERSITY = 'university'
23
+ OFFICE = 'office'
24
+ RETAIL = 'retail'
25
+ HOTEL = 'hotel'
26
+ HOSPITAL = 'hospital'
27
+ NURSING_HOME = 'nursing_home'
28
+ CULTURE = 'culture'
29
+ SPORTS = 'sports'
30
+ STORAGE = 'storage_repairs'
31
+
32
+ def __repr__(self):
33
+ return f'{self.__class__.__name__}.{self.name}'
34
+
35
+ def yearly_construction_floor_area(self):
36
+ logger.warning('Using static yearly_construction_floor_area')
37
+ raise NotImplementedError(f'yearly_construction_floor_area does not support category {self.name} (yet)')
38
+
39
+ def is_residential(self) -> bool:
40
+ return self == BuildingCategory.HOUSE or self == BuildingCategory.APARTMENT_BLOCK
41
+
42
+ def is_non_residential(self) -> bool:
43
+ return not self.is_residential()
44
+
45
+ @staticmethod
46
+ def from_string(category_name: str) -> 'BuildingCategory':
47
+ """Create an enum object from category name
48
+ Args:
49
+ category_name (str)
50
+
51
+ Returns:
52
+ building_category (BuildingCategory (Enum))
53
+
54
+ Raises:
55
+ ValueError: category_name not found in BuildingCategory
56
+ """
57
+ search = category_name.lower().replace(' ', '').replace('_', '')
58
+ for building_category in iter(BuildingCategory):
59
+ if search == building_category.value.lower().replace('_', ''):
60
+ return building_category
61
+ raise ValueError(f'No such building category {category_name}')
62
+
63
+ def from_norsk(norsk: str) -> BuildingCategory:
64
+ if norsk.lower() == 'småhus':
65
+ return BuildingCategory.HOUSE
66
+ if norsk.lower() in ('leilighet', 'boligblokk'):
67
+ return BuildingCategory.APARTMENT_BLOCK
68
+ if norsk.lower() == 'barnehage':
69
+ return BuildingCategory.KINDERGARTEN
70
+ if norsk.lower() == 'kontor':
71
+ return BuildingCategory.OFFICE
72
+ if norsk.lower() == 'skole':
73
+ return BuildingCategory.SCHOOL
74
+ if norsk.lower() == 'universitet':
75
+ return BuildingCategory.UNIVERSITY
76
+ if norsk.lower() == 'sykehjem':
77
+ return BuildingCategory.NURSING_HOME
78
+ if norsk.lower() == 'sykehus':
79
+ return BuildingCategory.HOSPITAL
80
+ if norsk.lower() == 'hotell':
81
+ return BuildingCategory.HOTEL
82
+ if norsk.lower() == 'idrettsbygg':
83
+ return BuildingCategory.SPORTS
84
+ if norsk.lower() == 'forretningsbygg':
85
+ return BuildingCategory.RETAIL
86
+ if norsk.lower() == 'kulturbygg':
87
+ return BuildingCategory.CULTURE
88
+ return BuildingCategory.from_string(norsk)
89
+
90
+
91
+ def expand_building_category(row: pd.Series) -> pd.DataFrame:
92
+ """
93
+ Expand a row of data based on the building category into multiple rows,
94
+ each representing a specific sub-category of either residential or non-residential buildings.
95
+
96
+ Parameters
97
+ ----------
98
+ row : pd.Series
99
+ A pandas Series containing the data for a single row, including a 'building_category' field.
100
+
101
+ Returns
102
+ -------
103
+ pd.DataFrame
104
+ A DataFrame with expanded rows for each sub-category of the building category.
105
+ """
106
+ if row['building_category'] in BuildingCategory:
107
+ return pd.DataFrame([row.to_dict()])
108
+ if row['building_category'] == NON_RESIDENTIAL:
109
+ categories = [b for b in BuildingCategory if b.is_non_residential()]
110
+ elif row['building_category'] == RESIDENTIAL:
111
+ categories = [b for b in BuildingCategory if b.is_residential()]
112
+
113
+ values = {k: [v] * len(categories) for k, v in row.to_dict().items() if k != 'building_category'}
114
+
115
+ return pd.DataFrame({
116
+ 'building_category': categories,
117
+ **values
118
+ })
119
+
120
+
121
+ # Apply the function to each row and concatenate the results
122
+ def expand_building_categories(df: pd.DataFrame, unique_columns: typing.List[str] = None):
123
+ """
124
+ Transform input dataframe so that building_category within groups (residential/non-residential) are unpacked
125
+ into all containing categories. Duplicates categories are removed. Specific categories with values area
126
+ preferred over category groups when there is a conflict.
127
+
128
+ Parameters
129
+ ----------
130
+ df : pandas.core.frame.DataFrame
131
+ unique_columns : str
132
+ list of column names that should be treated as joint unique. default: ['building_category']
133
+
134
+
135
+ Returns
136
+ -------
137
+ pandas.core.frame.DataFrame
138
+ """
139
+ if unique_columns:
140
+ df = df.drop_duplicates(subset=unique_columns, ignore_index=True, keep='last')
141
+ groups = df[df.building_category.isin([RESIDENTIAL, NON_RESIDENTIAL])]
142
+ specific = df[~df.building_category.isin(groups.building_category)]
143
+
144
+ expanded_groups = [expand_building_category(row) for _, row in groups.iterrows()]
145
+
146
+ filtered = [d[~d.building_category.isin(specific.building_category)] for d in expanded_groups]
147
+
148
+ return pd.concat(filtered + [specific]).reindex()
149
+
150
+
@@ -0,0 +1,78 @@
1
+ import typing
2
+
3
+ from enum import StrEnum, unique, auto
4
+
5
+
6
+ @unique
7
+ class BuildingCondition(StrEnum):
8
+ ORIGINAL_CONDITION = auto()
9
+ SMALL_MEASURE = auto()
10
+ RENOVATION = auto()
11
+ RENOVATION_AND_SMALL_MEASURE = auto()
12
+ DEMOLITION = auto()
13
+
14
+ @classmethod
15
+ def _missing_(cls, value: str):
16
+ """
17
+ Attempts to create an enum member from a given value by normalizing the string.
18
+
19
+ This method is called when a value is not found in the enumeration. It converts the input value
20
+ to lowercase, replaces spaces and hyphens with underscores, and then checks if this transformed
21
+ value matches the value of any existing enum member.
22
+
23
+ Parameters:
24
+ - value (str): The input value to convert and check against existing enum members.
25
+
26
+ Returns:
27
+ - Enum member: The corresponding enum member if a match is found.
28
+
29
+ Raises:
30
+ - ValueError: If no matching enum member is found.
31
+ """
32
+ value = value.lower().replace(' ', '_').replace('-', '_')
33
+ for member in cls:
34
+ if member.value == value:
35
+ return member
36
+ return ValueError(f'No such building condition: {value}')
37
+
38
+ def __repr__(self):
39
+ return f'{self.__class__.__name__}.{self.name}'
40
+
41
+ @staticmethod
42
+ def get_scruve_condition_list() -> typing.List[str]:
43
+ """
44
+ Retrieves a list with the building conditions used in S-curve calculations (in lower case).
45
+
46
+ Returns:
47
+ - condition_list (list[str]): list of building conditions
48
+ """
49
+ condition_list = [BuildingCondition.SMALL_MEASURE.value, BuildingCondition.RENOVATION.value, BuildingCondition.DEMOLITION.value]
50
+ return condition_list
51
+
52
+ @staticmethod
53
+ def get_full_condition_list() -> typing.List[str]:
54
+ """
55
+ Retrieves a list with all building conditions (in lower case).
56
+
57
+ Returns:
58
+ - condition_list (list[str]): list of building conditions
59
+ """
60
+ condition_list = [condition.value for condition in iter(BuildingCondition)]
61
+ return condition_list
62
+
63
+ @staticmethod
64
+ def existing_conditions() -> typing.Iterable['BuildingCondition']:
65
+ """
66
+ Returns all BuildingCondition except demolition
67
+
68
+ Returns
69
+ -------
70
+ Iterable of all BuildingCondition except demolition
71
+ """
72
+ yield from (b for b in BuildingCondition if b != BuildingCondition.DEMOLITION)
73
+
74
+
75
+ if __name__ == '__main__':
76
+ for building_condition in BuildingCondition:
77
+ print(building_condition)
78
+ print(repr(building_condition))
@@ -0,0 +1,84 @@
1
+ import pathlib
2
+ import typing
3
+
4
+ from loguru import logger
5
+ import pandas as pd
6
+
7
+ from ebm.model.file_handler import FileHandler
8
+
9
+
10
+ class EnergyRequirementCalibrationWriter:
11
+
12
+ def __init__(self):
13
+ pass
14
+
15
+ def load(self, df: pd.DataFrame, to_file: typing.Union[str, pathlib.Path] = None):
16
+ logger.debug(f'Save {to_file}')
17
+ if to_file is None:
18
+ to_file = pathlib.Path('input') / FileHandler.CALIBRATE_ENERGY_REQUIREMENT
19
+ file_path: pathlib.Path = to_file if isinstance(to_file, pathlib.Path) else pathlib.Path(to_file)
20
+ df = df[df['group'].isin(['energy_requirements', 'energy_requirement'])]
21
+ df = df.rename(columns={'variable': 'purpose'})
22
+ df = df[['building_category', 'purpose', 'heating_rv_factor']].reset_index(drop=True)
23
+ if file_path.suffix == '.csv':
24
+ df.to_csv(file_path, index=False)
25
+ elif file_path.suffix == '.xlsx':
26
+ df.to_excel(file_path, index=False)
27
+ logger.info(f'Wrote {to_file}')
28
+
29
+
30
+ class EnergyConsumptionCalibrationWriter:
31
+ df: pd.DataFrame
32
+
33
+ def __init__(self):
34
+ pass
35
+
36
+ def transform(self, df):
37
+ df = df[df['group'] == 'energy_consumption']
38
+ df = df[['building_category', 'variable', 'extra', 'heating_rv_factor']].reset_index(drop=True)
39
+ df = df.rename(columns={'variable': 'to',
40
+ 'extra': 'from',
41
+ 'heating_rv_factor': 'factor'}, errors='ignore')
42
+
43
+ self.df = df
44
+ return df
45
+
46
+ def load(self, df: pd.DataFrame, to_file: typing.Union[str, pathlib.Path] = None):
47
+ logger.debug(f'Save {to_file}')
48
+ if to_file is None:
49
+ to_file = pathlib.Path('input/calibrate_energy_consumption.xlsx')
50
+ file_path: pathlib.Path = to_file if isinstance(to_file, pathlib.Path) else pathlib.Path(to_file)
51
+
52
+ if file_path.suffix == '.csv':
53
+ df.to_csv(file_path, index=False)
54
+ elif file_path.suffix == '.xlsx':
55
+ df.to_excel(file_path, index=False)
56
+ logger.info(f'Wrote {to_file}')
57
+
58
+
59
+ def transform(heating_rv: pd.Series, heating_rv_factor=None) -> pd.Series:
60
+ if heating_rv_factor is None:
61
+ return heating_rv
62
+ calibrated = heating_rv * heating_rv_factor
63
+ calibrated.name = heating_rv.name
64
+ return calibrated
65
+
66
+
67
+ class EbmCalibration:
68
+ energy_requirement_original_condition: pd.Series
69
+ pass
70
+
71
+
72
+ class CalibrationReader:
73
+ def extract(self) -> pd.Series:
74
+ pass
75
+
76
+ def transform(self) -> pd.Series:
77
+ pass
78
+
79
+ def load(self) -> None:
80
+ pass
81
+
82
+
83
+ class CalibrationWriter:
84
+ pass
@@ -0,0 +1,180 @@
1
+ from loguru import logger
2
+ import pandas as pd
3
+
4
+ from ebm.cmd.run_calculation import calculate_building_category_area_forecast
5
+ from ebm.cmd.run_calculation import calculate_building_category_energy_requirements, calculate_heating_systems
6
+ from ebm.model.data_classes import YearRange
7
+ from ebm.model.building_category import BuildingCategory
8
+
9
+ from ebm.energy_consumption import (HEATING_RV_BASE_TOTAL, HEATING_RV_PEAK_TOTAL, BASE_LOAD_ENERGY_PRODUCT, PEAK_LOAD_ENERGY_PRODUCT,
10
+ TERTIARY_LOAD_ENERGY_PRODUCT, HEATING_RV_TERTIARY_TOTAL, COOLING_TOTAL, OTHER_TOTAL, DHW_TOTAL,
11
+ DOMESTIC_HOT_WATER_ENERGY_PRODUCT, HEAT_PUMP, HP_ENERGY_SOURCE)
12
+
13
+ ELECTRICITY = 'Elektrisitet'
14
+ DISTRICT_HEATING = 'Fjernvarme'
15
+ BIO = 'Bio'
16
+ FOSSIL = 'Fossil'
17
+
18
+ DOMESTIC_HOT_WATER = 'Tappevann'
19
+
20
+ HEATPUMP_AIR_SOURCE = 'Luft/luft'
21
+ HEATPUMP_WATER_SOUCE = 'Vannbåren varme'
22
+
23
+ CALIBRATION_YEAR = 2023
24
+
25
+ model_period = YearRange(2020, 2050)
26
+ start_year = model_period.start
27
+ end_year = model_period.end
28
+
29
+
30
+ def extract_area_forecast(database_manager) -> pd.DataFrame:
31
+ area_forecasts = []
32
+ for building_category in BuildingCategory:
33
+ area_forecast_result = calculate_building_category_area_forecast(
34
+ building_category=building_category,
35
+ database_manager=database_manager,
36
+ start_year=start_year,
37
+ end_year=end_year)
38
+ area_forecasts.append(area_forecast_result)
39
+
40
+ area_forecast = pd.concat(area_forecasts)
41
+ return area_forecast
42
+
43
+
44
+ def extract_energy_requirements(area_forecast: pd.DataFrame, database_manager) -> pd.DataFrame:
45
+ en_req = calculate_building_category_energy_requirements(
46
+ building_category=list(BuildingCategory),
47
+ area_forecast=area_forecast,
48
+ database_manager=database_manager,
49
+ start_year=start_year,
50
+ end_year=end_year)
51
+
52
+ return en_req
53
+
54
+
55
+ def extract_heating_systems(energy_requirements, database_manager) -> pd.DataFrame:
56
+ heating_systems = calculate_heating_systems(energy_requirements=energy_requirements,
57
+ database_manager=database_manager)
58
+
59
+ # heating_systems[heating_systems['purpose']=='heating_rv']
60
+ return heating_systems
61
+
62
+
63
+ def transform_by_energy_source(df, energy_class_column, energy_source_column):
64
+ rv_gl = df.loc[:, [energy_class_column, energy_source_column, 'building_group']]
65
+ rv_gl = rv_gl[rv_gl[energy_class_column] > 0]
66
+ rv_gl['typ'] = energy_class_column
67
+ rv_gl = rv_gl.rename(columns={energy_source_column: 'energy_source',
68
+ energy_class_column: 'energy_use'})
69
+ rv_gl = rv_gl.reset_index().set_index(['building_category',
70
+ 'building_condition',
71
+ 'purpose',
72
+ 'building_code',
73
+ 'year',
74
+ 'heating_systems',
75
+ 'typ'])
76
+ return rv_gl
77
+
78
+
79
+ def group_heating_systems_by_energy_carrier(df: pd.DataFrame) -> pd.DataFrame:
80
+ df = df.reindex()
81
+ df = df.sort_index()
82
+ df['building_group'] = 'yrkesbygg'
83
+ try:
84
+ df.loc[('house', slice(None),slice(None),slice(None),slice(None), slice(None),), 'building_group'] = 'bolig'
85
+ except KeyError as key_error:
86
+ logger.error('Missing key when setting group bolig for house')
87
+ logger.error(key_error)
88
+ try:
89
+ df.loc[('apartment_block', slice(None),slice(None),slice(None), slice(None), slice(None),), 'building_group'] = 'bolig'
90
+ except KeyError as key_error:
91
+ logger.error('Missing key when setting group bolig for apartment_block')
92
+ logger.error(key_error)
93
+
94
+ # df.loc['apartment_block', 'building_group'] = 'bolig'
95
+
96
+ df['ALWAYS_ELECTRICITY'] = 'Electricity'
97
+ rv_gl = transform_by_energy_source(df, HEATING_RV_BASE_TOTAL, BASE_LOAD_ENERGY_PRODUCT)
98
+ rv_sl = transform_by_energy_source(df, HEATING_RV_PEAK_TOTAL, PEAK_LOAD_ENERGY_PRODUCT)
99
+ rv_el = transform_by_energy_source(df, HEATING_RV_TERTIARY_TOTAL, TERTIARY_LOAD_ENERGY_PRODUCT)
100
+ cooling = transform_by_energy_source(df, COOLING_TOTAL, 'ALWAYS_ELECTRICITY')
101
+ spesifikt_elforbruk = transform_by_energy_source(df, OTHER_TOTAL, 'ALWAYS_ELECTRICITY')
102
+ tappevann = transform_by_energy_source(df, DHW_TOTAL, DOMESTIC_HOT_WATER_ENERGY_PRODUCT)
103
+ rv_hp = transform_by_energy_source(df, HEAT_PUMP, HP_ENERGY_SOURCE)
104
+
105
+ energy_use = pd.concat([rv_gl, rv_sl, rv_el, cooling, spesifikt_elforbruk, tappevann, rv_hp])
106
+
107
+ sums = energy_use.groupby(by=['building_group', 'energy_source', 'year']).sum() / (10**6)
108
+ df = sums.reset_index()
109
+ df = df.rename(columns={'building_group': 'building_category'})
110
+ try:
111
+ df.loc[df.energy_source == 'DH', 'energy_source'] = 'Fjernvarme'
112
+ except KeyError as ex:
113
+ logger.exception(ex)
114
+ try:
115
+ df.loc[df.energy_source == 'Electricity', 'energy_source'] = 'Elektrisitet'
116
+ except KeyError as ex:
117
+ logger.exception(ex)
118
+ try:
119
+ df.loc[df.building_category == 'bolig', 'building_category'] = 'Bolig'
120
+ except KeyError as ex:
121
+ logger.exception(ex)
122
+ try:
123
+ df.loc[df.building_category == 'yrkesbygg', 'building_category'] = 'Yrkesbygg'
124
+ except KeyError as ex:
125
+ logger.exception(ex)
126
+ return df.set_index(['building_category', 'energy_source', 'year'])
127
+
128
+
129
+ def transform_pumps(df: pd.DataFrame, calibration_year) -> pd.DataFrame:
130
+ df['building_group'] = 'Yrkesbygg'
131
+ df.loc['house', 'building_group'] = 'Bolig'
132
+ df.loc['apartment_block', 'building_group'] = 'Bolig'
133
+
134
+ return df
135
+
136
+
137
+ def _calculate_energy_source(df, heating_type, primary_source, secondary_source=None):
138
+ if secondary_source and primary_source == secondary_source:
139
+ df.loc[(heating_type, slice(None)), primary_source] = df.loc[(heating_type, slice(None)), HEATING_RV_BASE_TOTAL] + \
140
+ df.loc[(heating_type, slice(None)), HEATING_RV_PEAK_TOTAL]
141
+
142
+ return df
143
+ df.loc[(heating_type, slice(None)), primary_source] = df.loc[(heating_type, slice(None)), HEATING_RV_BASE_TOTAL]
144
+ if secondary_source:
145
+ df.loc[(heating_type, slice(None)), secondary_source] = df.loc[
146
+ (heating_type, slice(None)), HEATING_RV_PEAK_TOTAL]
147
+
148
+ return df
149
+
150
+
151
+ def sort_heating_systems_by_energy_source(transformed):
152
+ custom_order = [ELECTRICITY, BIO, FOSSIL, DISTRICT_HEATING]
153
+
154
+ unsorted = transformed.reset_index()
155
+ unsorted['energy_source'] = pd.Categorical(unsorted['energy_source'], categories=custom_order, ordered=True)
156
+ df_sorted = unsorted.sort_values(by=['energy_source'])
157
+ df_sorted = df_sorted.set_index([('energy_source', '')])
158
+
159
+ return df_sorted
160
+
161
+
162
+ class DistributionOfHeatingSystems:
163
+ @staticmethod
164
+ def extract(database_manager):
165
+ return database_manager.get_heating_systems_shares_start_year()
166
+
167
+ @staticmethod
168
+ def transform(df):
169
+ df = df.reset_index()
170
+ df['building_group'] = 'Yrkesbygg'
171
+
172
+ df = df[df['building_category'] != 'storage_repairs']
173
+ df.loc[df['building_category'].isin(['apartment_block']), 'building_group'] = 'Boligblokk'
174
+ df.loc[df['building_category'].isin(['house']), 'building_group'] = 'Småhus'
175
+
176
+ distribution_of_heating_systems_by_building_group = df.groupby(by=['building_group', 'heating_systems'])[
177
+ ['heating_system_share']].mean()
178
+ return distribution_of_heating_systems_by_building_group
179
+
180
+
@@ -0,0 +1,157 @@
1
+ import pathlib
2
+ from typing import List, Optional
3
+
4
+ import pandas as pd
5
+ from pandera.typing.common import DataFrameBase
6
+
7
+ from ebm.model.building_category import BuildingCategory
8
+
9
+
10
+ def explode_building_category_column(df: pd.DataFrame, unique_columns: List[str]) -> pd.DataFrame:
11
+ """
12
+ Explodes the 'building_category' column in the DataFrame into multiple columns based on residential and non-residential categories.
13
+
14
+ Parameters
15
+ ----------
16
+ df : pd.DataFrame
17
+ The input DataFrame containing the 'building_category' column.
18
+ unique_columns : List[str]
19
+ List of columns to use for de-duplication.
20
+
21
+ Returns
22
+ -------
23
+ pd.DataFrame
24
+ The DataFrame with exploded 'building_category' columns.
25
+ """
26
+ df = explode_column_alias(df=df, column='building_category',
27
+ values=[bc for bc in BuildingCategory if bc.is_residential()],
28
+ alias='residential',
29
+ de_dup_by=unique_columns)
30
+ df = explode_column_alias(df=df, column='building_category',
31
+ values=[bc for bc in BuildingCategory if not bc.is_residential()],
32
+ alias='non_residential',
33
+ de_dup_by=unique_columns)
34
+ df = explode_column_alias(df=df, column='building_category',
35
+ values=[bc for bc in BuildingCategory],
36
+ alias='default',
37
+ de_dup_by=unique_columns)
38
+ return df
39
+
40
+
41
+ def explode_building_code_column(df: pd.DataFrame, unique_columns: List[str],
42
+ default_building_code: None | pd.DataFrame = None) -> pd.DataFrame:
43
+ """
44
+ Explodes the 'building_code' column in the DataFrame into multiple columns based on the provided building_codelist.
45
+
46
+ Parameters
47
+ ----------
48
+ df : pd.DataFrame
49
+ The input DataFrame containing the 'building_code' column.
50
+ unique_columns : List[str]
51
+ List of columns to use for de-duplication.
52
+ default_building_code : Optional[pd.DataFrame], optional
53
+ DataFrame containing default building_codevalues. If not provided, building_codevalues are read from 'input/building_codes.csv'.
54
+
55
+ Returns
56
+ -------
57
+ pd.DataFrame
58
+ The DataFrame with exploded 'building_code' columns.
59
+ """
60
+ # Hvor skal building_code_list hentes fra?
61
+ building_code_list = pd.read_csv(pathlib.Path(__file__).parent.parent / 'data' / 'building_code_parameters.csv')['building_code'].unique() if default_building_code is None else default_building_code
62
+ df = explode_column_alias(df=df,
63
+ column='building_code',
64
+ values=building_code_list,
65
+ de_dup_by=unique_columns)
66
+ return df
67
+
68
+
69
+ def explode_unique_columns(df: pd.DataFrame| DataFrameBase,
70
+ unique_columns: List[str],
71
+ default_building_code: List[str]|None = None) -> pd.DataFrame:
72
+ """
73
+ Explodes 'building_code' and 'building_category' columns in df.
74
+
75
+
76
+ Parameters
77
+ ----------
78
+ df : pd.DataFrame
79
+ The input DataFrame containing the columns to be exploded.
80
+ unique_columns : List[str]
81
+ List of columns to use for de-duplication.
82
+ default_building_code : List[str], optional
83
+ List of TEKs to replace default
84
+
85
+ Returns
86
+ -------
87
+ pd.DataFrame
88
+ The DataFrame with exploded columns.
89
+ """
90
+
91
+ df = explode_building_code_column(df, unique_columns, default_building_code=default_building_code)
92
+ df = explode_building_category_column(df, unique_columns)
93
+ return df
94
+
95
+
96
+ def explode_column_alias(df, column, values: list|dict=None, alias='default', de_dup_by: list[str]=None):
97
+ """
98
+ Explodes a specified column in the DataFrame into multiple rows based on provided values and alias.
99
+
100
+ Parameters
101
+ ----------
102
+ df : pd.DataFrame
103
+ The input DataFrame containing the column to be exploded.
104
+ column : str
105
+ The name of the column to be exploded.
106
+ values : Optional[List[str], dict[str, list[str]], optional
107
+ List or dict of values to explode the column by. If not provided, unique values from the column excluding the
108
+ alias are used.
109
+ alias : str, optional
110
+ The alias to be used for default values. Default is 'default'.
111
+ When values is a dict the parameter alias is ignored
112
+ de_dup_by : Optional[List[str]], optional
113
+ List of columns to use for de-duplication. If not provided, no de-duplication is performed.
114
+
115
+ Returns
116
+ -------
117
+ pd.DataFrame
118
+ The DataFrame with the exploded column.
119
+
120
+ Examples
121
+ --------
122
+ >>> d_f = pd.DataFrame({'category': ['A', 'B', 'default']})
123
+ >>> explode_column_alias(d_f, column='category', values=['A', 'B'], alias='default')
124
+ category
125
+ 0 A
126
+ 1 B
127
+ 2 A
128
+ 2 B
129
+ """
130
+ if column not in df.columns:
131
+ raise ValueError(f"The DataFrame (df) must contain the column: {column}")
132
+
133
+ df = replace_column_alias(df, column=column, values=values, alias=alias, de_dup_by=None)
134
+
135
+ df = df.assign(**{column: df[column].str.split('+')}).explode(column)
136
+ if de_dup_by:
137
+ df = df.sort_values(by='_explode_column_alias_default', ascending=True)
138
+ df = df.drop_duplicates(de_dup_by)
139
+ return df.drop(columns=['_explode_column_alias_default'], errors='ignore')
140
+
141
+
142
+ def replace_column_alias(df: pd.DataFrame, column: str, values: Optional[list|dict]=None, alias: Optional[str]='default',
143
+ de_dup_by=None) -> pd.DataFrame:
144
+ values = values if values is not None else [c for c in df[column].unique().tolist() if c != alias]
145
+ aliases = {alias: values} if not isinstance(values, dict) else values
146
+ df = df.copy()
147
+ for k, v in aliases.items():
148
+ df['_explode_column_alias_default'] = df[column] == k
149
+ df.loc[df[df[column] == k].index, column] = '+'.join(v)
150
+ if not de_dup_by:
151
+ return df
152
+ return df.drop(columns=['_explode_column_alias_default'], errors='ignore')
153
+
154
+
155
+ def explode_column(df: pd.DataFrame, column: str) -> pd.DataFrame:
156
+ df = df.assign(**{column: df[column].str.split('+')}).explode(column)
157
+ return df