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,472 @@
1
+ # noinspection SpellCheckingInspection
2
+ import pandas as pd
3
+ from loguru import logger
4
+
5
+ from ebm.model.building_category import NON_RESIDENTIAL, RESIDENTIAL, BuildingCategory
6
+ from ebm.model.data_classes import YearRange
7
+ from ebm.model.database_manager import DatabaseManager
8
+ from ebm.model.heating_systems import HeatingSystems
9
+
10
+ BUILDING_CATEGORY = 'building_category'
11
+ BUILDING_CODE = 'building_code'
12
+ HEATING_SYSTEMS = 'heating_systems'
13
+ NEW_HEATING_SYSTEMS = 'new_heating_systems'
14
+ YEAR = 'year'
15
+ TEK_SHARES = 'heating_system_share'
16
+
17
+
18
+ class HeatingSystemsForecast: # noqa: D101
19
+
20
+ def __init__(self, shares_start_year: pd.DataFrame, efficiencies: pd.DataFrame, forecast: pd.DataFrame, building_code_list: list[str], period: YearRange):
21
+ """Init HeatingSystemsForecast."""
22
+ self.shares_start_year = shares_start_year
23
+ self.efficiencies = efficiencies
24
+ self.forecast = forecast
25
+ self.building_code_list = building_code_list
26
+ self.period = period
27
+
28
+ self._validate_years()
29
+ check_sum_of_shares(shares_start_year)
30
+
31
+ def _validate_years(self) -> None:
32
+ """
33
+ Ensure that the years in the dataframes provided during initialization align with the specified period.
34
+
35
+ This method performs the following validations:
36
+ 1. Confirms that `shares_start_year` has exactly one unique start year.
37
+ 2. Checks that the minimum year in `projection` for each combination of `BUILDING_CATEGORY` and `TEK` matches the expected start year + 1.
38
+ 3. Verifies that all years in the given `period` are present in the `projection` dataframe for unique combinations of `BUILDING_CATEGORY` and `TEK`.
39
+
40
+ Raises
41
+ ------
42
+ ValueError
43
+ If any of the above validations fail.
44
+
45
+ """
46
+ start_year = self.shares_start_year[YEAR].unique()
47
+ if len(start_year) != 1:
48
+ raise ValueError("More than one start year in dataframe.")
49
+ start_year = start_year[0]
50
+
51
+ if start_year != self.period.start:
52
+ raise ValueError("Start year in dataframe doesn't match start year for given period.")
53
+
54
+ projection = self.forecast.melt(id_vars = [BUILDING_CATEGORY, BUILDING_CODE, HEATING_SYSTEMS, NEW_HEATING_SYSTEMS],
55
+ var_name = YEAR, value_name = "Andel_utskiftning")
56
+ projection[YEAR] = projection[YEAR].astype(int)
57
+ min_df = projection.groupby([BUILDING_CATEGORY, BUILDING_CODE]).agg(min_year=(YEAR, 'min')).reset_index()
58
+ min_mismatch = min_df[min_df['min_year'] != (start_year + 1)]
59
+
60
+ if not min_mismatch.empty:
61
+ raise ValueError("Years don't match between dataframes.")
62
+
63
+ projection_period = self.period.subset(1).range()
64
+
65
+ def check_years(group: pd.Series): # noqa: ANN202
66
+ return set(projection_period).issubset(group[YEAR])
67
+
68
+ period_match = projection.groupby(by=[BUILDING_CATEGORY, BUILDING_CODE]).apply(check_years).reset_index()
69
+ if not period_match[period_match[0] == False].empty: # noqa: E712
70
+ raise ValueError("Years in dataframe not present in given period.")
71
+
72
+ def calculate_forecast(self) -> pd.DataFrame:
73
+ """
74
+ Project heating system shares across model years.
75
+
76
+ Returns
77
+ -------
78
+ pd.Dataframe
79
+ TEK shares for heating systems per year, along with different load shares and efficiencies.
80
+
81
+ Raises
82
+ ------
83
+ ValueError
84
+ If sum of shares for a building_code is not equal to 1.
85
+
86
+ """
87
+ shares_all_heating_systems = add_missing_heating_systems(self.shares_start_year,
88
+ HeatingSystems,
89
+ self.period.start)
90
+ projected_shares = expand_building_category_building_code(self.forecast, self.building_code_list)
91
+ new_shares = project_heating_systems(shares_all_heating_systems, projected_shares, self.period)
92
+ heating_systems_projection = add_existing_heating_system_shares_to_projection(new_shares,
93
+ self.shares_start_year,
94
+ self.period)
95
+ check_sum_of_shares(heating_systems_projection)
96
+ heating_systems_projection = add_load_shares_and_efficiencies(heating_systems_projection, self.efficiencies)
97
+ return heating_systems_projection
98
+
99
+ @staticmethod
100
+ def new_instance(period: YearRange,
101
+ database_manager: DatabaseManager = None) -> 'HeatingSystemsForecast':
102
+ """
103
+ Create a new instance of the HeatingSystemsProjection class, using the specified YearRange Period and an optional database manager.
104
+
105
+ If a database manager is not provided, a new DatabaseManager instance will be created.
106
+
107
+ Parameters
108
+ ----------
109
+ period: YearRange
110
+ period of forecast
111
+ database_manager: DatabaseManager
112
+ a database manager for heating system forecast
113
+
114
+ Returns
115
+ -------
116
+ HeatingSystemsForecast
117
+ A new instance of HeatingSystemsProjection initialized with data from the specified database manager.
118
+
119
+ """
120
+ dm = database_manager if isinstance(database_manager, DatabaseManager) else DatabaseManager()
121
+ shares_start_year = dm.get_heating_systems_shares_start_year()
122
+ efficiencies = dm.get_heating_system_efficiencies()
123
+ projection = dm.get_heating_system_forecast()
124
+ building_code_list = dm.get_building_code_list()
125
+ return HeatingSystemsForecast(shares_start_year=shares_start_year,
126
+ efficiencies=efficiencies,
127
+ forecast=projection,
128
+ building_code_list=building_code_list,
129
+ period=period)
130
+
131
+ @staticmethod
132
+ def pad_projection(hf: pd.DataFrame, years_to_pad: YearRange) -> pd.DataFrame:
133
+ """
134
+ Left pad dataframe hf with years in years_to_pad. The padding will be equal to existing first year of hf.
135
+
136
+ Parameters
137
+ ----------
138
+ hf : pd.DataFrame
139
+ heating systems to pad
140
+ years_to_pad : YearRange
141
+ range of years to pad unto hf
142
+
143
+ Returns
144
+ -------
145
+ pd.DataFrame
146
+ hf with left padding
147
+
148
+ """
149
+ padding_value = hf[hf.year == years_to_pad.end + 1].copy()
150
+ left_padding = []
151
+ for year in years_to_pad:
152
+ year_values = padding_value.copy()
153
+ year_values['year'] = year
154
+ left_padding.append(year_values)
155
+
156
+ return pd.concat(left_padding + [hf]) # noqa: RUF005
157
+
158
+
159
+ def add_missing_heating_systems(heating_systems_shares: pd.DataFrame,
160
+ heating_systems: HeatingSystems = None,
161
+ start_year: int|None = None) -> pd.DataFrame:
162
+ """Add missing HeatingSystems per BuildingCategory and building_codewith a default TEK_share of 0."""
163
+ df_aggregert_0 = heating_systems_shares.copy()
164
+ input_start_year = df_aggregert_0[YEAR].unique()
165
+ if len(input_start_year) != 1:
166
+ raise ValueError("More than one start year in dataframe")
167
+
168
+ # TODO: drop start year as input param and only use year in dataframe?
169
+ if not start_year:
170
+ start_year = input_start_year[0]
171
+ elif start_year != input_start_year:
172
+ raise ValueError("Given start_year doesn't match year in dataframe.")
173
+
174
+ if not heating_systems:
175
+ heating_systems = HeatingSystems
176
+ oppvarmingstyper = pd.DataFrame(
177
+ {HEATING_SYSTEMS: [hs for hs in heating_systems]},
178
+ )
179
+
180
+ df_aggregert_0_kombinasjoner = df_aggregert_0[[BUILDING_CATEGORY, BUILDING_CODE]].drop_duplicates()
181
+ df_aggregert_0_alle_oppvarmingstyper = df_aggregert_0_kombinasjoner.merge(oppvarmingstyper, how = 'cross')
182
+
183
+ df_aggregert_merged = df_aggregert_0_alle_oppvarmingstyper.merge(df_aggregert_0,
184
+ on = [BUILDING_CATEGORY, BUILDING_CODE, HEATING_SYSTEMS],
185
+ how = 'left')
186
+ #TODO: Kan droppe kopi av df og heller ta fillna() for de to kolonnene
187
+ manglende_rader = df_aggregert_merged[df_aggregert_merged[TEK_SHARES].isna()].copy()
188
+ manglende_rader[YEAR] = start_year
189
+ manglende_rader[TEK_SHARES] = 0
190
+ manglende_rader = manglende_rader[[BUILDING_CATEGORY, BUILDING_CODE, HEATING_SYSTEMS, YEAR, TEK_SHARES]]
191
+
192
+ df_aggregert_alle_kombinasjoner = pd.concat([df_aggregert_0, manglende_rader])
193
+
194
+ return df_aggregert_alle_kombinasjoner
195
+
196
+
197
+ def add_load_shares_and_efficiencies(df: pd.DataFrame,
198
+ heating_systems_efficiencies: pd.DataFrame) -> pd.DataFrame:
199
+ """
200
+ Add load share and efficiency data to heating system share records by merging with efficiency reference data.
201
+
202
+ Parameters
203
+ ----------
204
+ df : pandas.DataFrame
205
+ DataFrame containing heating system shares, including columns for year and heating system type.
206
+
207
+ heating_systems_efficiencies : pandas.DataFrame
208
+ DataFrame containing efficiency and load share data for each heating system type.
209
+
210
+ Returns
211
+ -------
212
+ pandas.DataFrame
213
+ Merged DataFrame with heating system shares enriched with efficiency and load share information.
214
+
215
+ Notes
216
+ -----
217
+ - The merge is performed on the HEATING_SYSTEMS column using a left join.
218
+ - The YEAR column is cast to integer to ensure consistent data types.
219
+
220
+ """
221
+ df_hoved_spiss_og_ekstralast = heating_systems_efficiencies.copy()
222
+ df_oppvarmingsteknologier_andeler = df.merge(df_hoved_spiss_og_ekstralast, on = [HEATING_SYSTEMS],
223
+ how ='left')
224
+ df_oppvarmingsteknologier_andeler[YEAR] = df_oppvarmingsteknologier_andeler[YEAR].astype(int)
225
+ return df_oppvarmingsteknologier_andeler
226
+
227
+
228
+ def aggregere_lik_oppvarming_fjern_0(df:pd.DataFrame) -> pd.DataFrame:
229
+ """
230
+ Aggregate heating system shares by summing TEK_share values, excluding entries with zero share.
231
+
232
+ Parameters
233
+ ----------
234
+ df : pandas.DataFrame
235
+ Input DataFrame containing heating system share data, including TEK_share values.
236
+
237
+ Returns
238
+ -------
239
+ pandas.DataFrame
240
+ Aggregated DataFrame grouped by building category, building code, heating system, and year,
241
+ with summed TEK_share values excluding zero-share entries.
242
+
243
+ Notes
244
+ -----
245
+ - Rows where TEK_share is zero are removed before aggregation.
246
+ - The resulting DataFrame is grouped by [BUILDING_CATEGORY, BUILDING_CODE, HEATING_SYSTEMS, YEAR].
247
+
248
+ """
249
+ df_fjern_null = df.query(f"{TEK_SHARES} != 0").copy()
250
+ df_aggregert = df_fjern_null.groupby([BUILDING_CATEGORY, BUILDING_CODE, HEATING_SYSTEMS, YEAR],
251
+ as_index = False)[TEK_SHARES].sum()
252
+ return df_aggregert
253
+
254
+
255
+ def expand_building_category_building_code(projection: pd.DataFrame,
256
+ building_code_list: list[str]) -> pd.DataFrame:
257
+ """Add necessary building categories and building_code to the heating_systems_forecast dataframe."""
258
+ score = '_score'
259
+ original_building_category = '_original_bc'
260
+ original_building_code = '_original_building_code'
261
+ projection[original_building_category] = projection['building_category']
262
+ projection[original_building_code] = projection['building_code']
263
+
264
+ alle_bygningskategorier = '+'.join(BuildingCategory)
265
+ alle_building_code = '+'.join(tek for tek in building_code_list)
266
+ husholdning = '+'.join(bc for bc in BuildingCategory if bc.is_residential())
267
+ yrkesbygg = '+'.join(bc for bc in BuildingCategory if bc.is_non_residential())
268
+
269
+ df = projection.copy()
270
+ df.loc[df[BUILDING_CODE] == "default", BUILDING_CODE] = alle_building_code
271
+ df.loc[df[BUILDING_CATEGORY] == "default", BUILDING_CATEGORY] = alle_bygningskategorier
272
+ df.loc[df[BUILDING_CATEGORY] == RESIDENTIAL, BUILDING_CATEGORY] = husholdning
273
+ df.loc[df[BUILDING_CATEGORY] == NON_RESIDENTIAL, BUILDING_CATEGORY] = yrkesbygg
274
+
275
+ df = df.assign(**{BUILDING_CATEGORY: df[BUILDING_CATEGORY].str.split('+')}).explode(BUILDING_CATEGORY)
276
+ df2 = df.assign(**{BUILDING_CODE: df[BUILDING_CODE].str.split('+')}).explode(BUILDING_CODE)
277
+ df2 = df2.reset_index(drop=True)
278
+ df2[score] = (df2[original_building_category] != 'default') * 1 + \
279
+ (~df2[original_building_category].isin(['default', NON_RESIDENTIAL, RESIDENTIAL])) * 1 + \
280
+ (df2[original_building_code] != 'default') * 1
281
+
282
+ df2 = df2.sort_values(by=[score])
283
+ de_duped = df2.drop_duplicates(subset=[BUILDING_CATEGORY, BUILDING_CODE, HEATING_SYSTEMS, NEW_HEATING_SYSTEMS], keep='last')
284
+
285
+ return de_duped.drop(columns=[score, original_building_category, original_building_code])
286
+
287
+
288
+ def project_heating_systems(shares_start_year_all_systems: pd.DataFrame,
289
+ projected_shares: pd.DataFrame,
290
+ period: YearRange) -> pd.DataFrame:
291
+ """
292
+ Forecast heating system shares over a given period based on initial shares and projected replacement rates.
293
+
294
+ Parameters
295
+ ----------
296
+ shares_start_year_all_systems : pandas.DataFrame
297
+ DataFrame containing TEK_share values for all heating systems at the start year.
298
+
299
+ projected_shares : pandas.DataFrame
300
+ DataFrame containing projected replacement shares for heating systems across years.
301
+
302
+ period : YearRange
303
+ The projection period, defined by a start and end year.
304
+
305
+ Returns
306
+ -------
307
+ pandas.DataFrame
308
+ A DataFrame with projected TEK_share values for both existing and new heating systems
309
+ across the specified period.
310
+
311
+ Notes
312
+ -----
313
+ - The function melts the projected shares into long format and filters them to match the projection period.
314
+ - It calculates new shares based on replacement rates and adjusts existing shares accordingly.
315
+ - New and existing heating system shares are merged and aggregated, removing duplicates and zero-share entries.
316
+ - The final output contains TEK_share values per year, building category, building code, and heating system.
317
+ - Internal helper functions like `aggregere_lik_oppvarming_fjern_0` are used to clean and aggregate data.
318
+ - The YEAR column is explicitly cast to integer at the end to ensure consistency.
319
+
320
+ """
321
+ df = shares_start_year_all_systems.copy()
322
+ inputfil_oppvarming = projected_shares.copy()
323
+
324
+ df_framskrive_oppvarming_long = inputfil_oppvarming.melt(id_vars=[BUILDING_CATEGORY, BUILDING_CODE, HEATING_SYSTEMS,
325
+ NEW_HEATING_SYSTEMS],
326
+ var_name=YEAR, value_name="Andel_utskiftning")
327
+
328
+ df_framskrive_oppvarming_long[YEAR] = df_framskrive_oppvarming_long[YEAR].astype(int)
329
+ df_framskrive_oppvarming_long = df_framskrive_oppvarming_long[df_framskrive_oppvarming_long[YEAR].isin(period.subset(1).range())]
330
+
331
+ liste_eksisterende_oppvarming = list(df_framskrive_oppvarming_long[HEATING_SYSTEMS].unique())
332
+ liste_ny_oppvarming = list(df_framskrive_oppvarming_long[NEW_HEATING_SYSTEMS].unique())
333
+
334
+ columns_to_keep = [BUILDING_CATEGORY, BUILDING_CODE, HEATING_SYSTEMS, YEAR, TEK_SHARES]
335
+ oppvarming_og_tek = df.query(f"{HEATING_SYSTEMS} == {liste_eksisterende_oppvarming}")[columns_to_keep].copy()
336
+
337
+ columns_to_keep = [BUILDING_CATEGORY, BUILDING_CODE, HEATING_SYSTEMS, TEK_SHARES]
338
+ oppvarming_og_tek_foer_endring = df.query(f"{HEATING_SYSTEMS} == {liste_ny_oppvarming}")[columns_to_keep].copy()
339
+
340
+ df_merge = oppvarming_og_tek.merge(df_framskrive_oppvarming_long,
341
+ on=[BUILDING_CATEGORY, BUILDING_CODE, HEATING_SYSTEMS], how='inner')
342
+ df_merge['Ny_andel'] = (df_merge[TEK_SHARES] * df_merge['Andel_utskiftning'])
343
+
344
+ df_ny_andel_sum = df_merge.groupby([BUILDING_CATEGORY, BUILDING_CODE, HEATING_SYSTEMS, f'{YEAR}_y'], as_index = False)[['Ny_andel']].sum()
345
+ df_ny_andel_sum = df_ny_andel_sum.rename(columns={"Ny_andel": "Sum_ny_andel"})
346
+
347
+ df_merge_sum_ny_andel = df_merge.merge(df_ny_andel_sum, on = [BUILDING_CATEGORY,BUILDING_CODE,HEATING_SYSTEMS, f'{YEAR}_y'])
348
+
349
+ df_merge_sum_ny_andel['Eksisterende_andel'] = (df_merge_sum_ny_andel[TEK_SHARES] -
350
+ df_merge_sum_ny_andel['Sum_ny_andel'])
351
+
352
+ kolonner_eksisterende = [f'{YEAR}_y', BUILDING_CATEGORY, BUILDING_CODE, 'Eksisterende_andel', HEATING_SYSTEMS]
353
+ navn_eksisterende_kolonner = {"Eksisterende_andel": TEK_SHARES,
354
+ NEW_HEATING_SYSTEMS : HEATING_SYSTEMS,
355
+ f'{YEAR}_y': YEAR}
356
+
357
+ kolonner_nye = [f'{YEAR}_y', BUILDING_CATEGORY, BUILDING_CODE, 'Ny_andel', NEW_HEATING_SYSTEMS]
358
+ navn_nye_kolonner = {"Ny_andel": TEK_SHARES,
359
+ NEW_HEATING_SYSTEMS: HEATING_SYSTEMS,
360
+ f'{YEAR}_y': YEAR}
361
+
362
+ rekkefolge_kolonner = [YEAR, BUILDING_CATEGORY, BUILDING_CODE, HEATING_SYSTEMS, TEK_SHARES]
363
+
364
+ nye_andeler_eksisterende = df_merge_sum_ny_andel[kolonner_eksisterende].rename(columns=navn_eksisterende_kolonner)
365
+
366
+ nye_andeler_nye = df_merge_sum_ny_andel[kolonner_nye].rename(columns=navn_nye_kolonner)
367
+ nye_andeler_nye = aggregere_lik_oppvarming_fjern_0(nye_andeler_nye)
368
+
369
+ nye_andeler_pluss_eksisterende = nye_andeler_nye.merge(oppvarming_og_tek_foer_endring, on=[BUILDING_CATEGORY,BUILDING_CODE,HEATING_SYSTEMS], how='inner')
370
+ nye_andeler_pluss_eksisterende[TEK_SHARES] = nye_andeler_pluss_eksisterende[f'{TEK_SHARES}_x'] + nye_andeler_pluss_eksisterende[f'{TEK_SHARES}_y']
371
+ nye_andeler_pluss_eksisterende = nye_andeler_pluss_eksisterende.drop(columns=[f'{TEK_SHARES}_x', f'{TEK_SHARES}_y'])
372
+
373
+ nye_andeler_samlet = pd.concat([nye_andeler_eksisterende, nye_andeler_pluss_eksisterende])
374
+ nye_andeler_drop_dupe = nye_andeler_samlet.drop_duplicates(
375
+ subset=[YEAR, BUILDING_CATEGORY, BUILDING_CODE, HEATING_SYSTEMS, TEK_SHARES], keep='first')
376
+
377
+ nye_andeler_samlet_uten_0 = aggregere_lik_oppvarming_fjern_0(nye_andeler_drop_dupe)
378
+ nye_andeler_samlet_uten_0 = nye_andeler_samlet_uten_0[rekkefolge_kolonner]
379
+
380
+ # TODO: check dtype changes in function
381
+ nye_andeler_samlet_uten_0[YEAR] = nye_andeler_samlet_uten_0[YEAR].astype(int)
382
+
383
+ return nye_andeler_samlet_uten_0
384
+
385
+
386
+ def check_sum_of_shares(projected_shares: pd.DataFrame, precision: int = 10) -> None:
387
+ """
388
+ Make sure that the sum of heating_system_share equals 1 per TEK, building category and year.
389
+
390
+ Parameters
391
+ ----------
392
+ projected_shares: pd.Dataframe
393
+ Dataframe must contain columns: 'building_category', 'building_code', 'year' and 'heating_system_share'
394
+ precision: int
395
+ Precision used for value check (with round)
396
+
397
+ Raises
398
+ ------
399
+ ValueError
400
+ If sum of shares for a building_codeis not equal to 1.
401
+
402
+ """
403
+ df = projected_shares.copy()
404
+ df = df.groupby(by=[BUILDING_CATEGORY, BUILDING_CODE, YEAR])[[TEK_SHARES]].sum()
405
+ df['check'] = round(df[TEK_SHARES] * 100, precision) == 100.0 # noqa: PLR2004
406
+ invalid_shares = df[df['check'] == False].copy() # noqa: E712
407
+ invalid_shares = invalid_shares.drop(columns=['check'])
408
+ if len(invalid_shares) > 0:
409
+ logger.error('Sum of TEK shares not equal to 1 for:')
410
+ for idx, row in invalid_shares.iterrows():
411
+ logger.error('{idx}: {}', idx=idx, row_dict=row.to_dict())
412
+ logger.warning('Skipping ValueError on sum!=1.0')
413
+
414
+
415
+ def add_existing_heating_system_shares_to_projection(new_shares: pd.DataFrame,
416
+ existing_shares: pd.DataFrame,
417
+ period: YearRange) -> pd.DataFrame:
418
+ """
419
+ Extend heating system shares in the projection period by preserving TEK_share values for systems with unprojected existing building code shares.
420
+
421
+ Parameters
422
+ ----------
423
+ new_shares : pandas.DataFrame
424
+ DataFrame containing projected heating system shares for buildings.
425
+
426
+ existing_shares : pandas.DataFrame
427
+ DataFrame containing existing heating system shares for buildings.
428
+
429
+ period : YearRange
430
+ The projection period, defined by a start and end year.
431
+
432
+ Returns
433
+ -------
434
+ pandas.DataFrame
435
+ Combined DataFrame with projected shares and extended existing shares
436
+ for heating systems not present in the new projections.
437
+
438
+ Notes
439
+ -----
440
+ - Heating systems with existing shares but missing from the new projections
441
+ will retain their TEK_share values across the projection period.
442
+ - The function filters out combinations already present in the new projections
443
+ and extends the remaining ones across the projection years.
444
+ - The `Sortering` column is used internally for identifying unique combinations
445
+ of building category, code, and heating system.
446
+
447
+ """
448
+
449
+ def sortering_oppvarmingstyper(df: pd.DataFrame) -> [pd.DataFrame, list[str]]:
450
+ df_kombinasjoner = df.copy()
451
+ df_kombinasjoner['Sortering'] = df_kombinasjoner[BUILDING_CATEGORY] + df_kombinasjoner[BUILDING_CODE] + \
452
+ df_kombinasjoner[HEATING_SYSTEMS]
453
+ kombinasjonsliste = list(df_kombinasjoner['Sortering'].unique())
454
+ return df_kombinasjoner, kombinasjonsliste
455
+
456
+ df_nye_andeler_kopi = new_shares.copy()
457
+
458
+ new_shares, alle_nye_kombinasjonsliste = sortering_oppvarmingstyper(new_shares)
459
+ existing_shares, _ = sortering_oppvarmingstyper(existing_shares)
460
+ df_eksisterende_filtrert = existing_shares.query(f"Sortering != {alle_nye_kombinasjonsliste}")
461
+ df_eksisterende_filtrert = df_eksisterende_filtrert.drop(columns = ['Sortering'])
462
+
463
+ # TODO: set lower limit to period equal to last year (max) present in forecast data?
464
+ projection_period = YearRange(period.start + 1, period.end).year_range
465
+ utvidede_aar_uendret = pd.concat([
466
+ df_eksisterende_filtrert.assign(**{YEAR: year}) for year in projection_period
467
+ ])
468
+
469
+ samlede_nye_andeler = pd.concat([utvidede_aar_uendret, df_nye_andeler_kopi,
470
+ existing_shares], ignore_index=True)
471
+ samlede_nye_andeler = samlede_nye_andeler.drop(columns=['Sortering'])
472
+ return samlede_nye_andeler