ebm 0.99.3__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.
- ebm/__init__.py +0 -0
- ebm/__main__.py +152 -0
- ebm/__version__.py +1 -0
- ebm/cmd/__init__.py +0 -0
- ebm/cmd/calibrate.py +83 -0
- ebm/cmd/calibrate_excel_com_io.py +128 -0
- ebm/cmd/heating_systems_by_year.py +18 -0
- ebm/cmd/helpers.py +134 -0
- ebm/cmd/initialize.py +167 -0
- ebm/cmd/migrate.py +92 -0
- ebm/cmd/pipeline.py +227 -0
- ebm/cmd/prepare_main.py +174 -0
- ebm/cmd/result_handler.py +272 -0
- ebm/cmd/run_calculation.py +221 -0
- ebm/data/area.csv +92 -0
- ebm/data/area_new_residential_buildings.csv +3 -0
- ebm/data/area_per_person.csv +12 -0
- ebm/data/building_code_parameters.csv +9 -0
- ebm/data/energy_need_behaviour_factor.csv +6 -0
- ebm/data/energy_need_improvements.csv +7 -0
- ebm/data/energy_need_original_condition.csv +534 -0
- ebm/data/heating_system_efficiencies.csv +13 -0
- ebm/data/heating_system_forecast.csv +9 -0
- ebm/data/heating_system_initial_shares.csv +1113 -0
- ebm/data/holiday_home_energy_consumption.csv +24 -0
- ebm/data/holiday_home_stock.csv +25 -0
- ebm/data/improvement_building_upgrade.csv +9 -0
- ebm/data/new_buildings_residential.csv +32 -0
- ebm/data/population_forecast.csv +51 -0
- ebm/data/s_curve.csv +40 -0
- ebm/energy_consumption.py +307 -0
- ebm/extractors.py +115 -0
- ebm/heating_system_forecast.py +472 -0
- ebm/holiday_home_energy.py +341 -0
- ebm/migrations.py +224 -0
- ebm/model/__init__.py +0 -0
- ebm/model/area.py +403 -0
- ebm/model/bema.py +149 -0
- ebm/model/building_category.py +150 -0
- ebm/model/building_condition.py +78 -0
- ebm/model/calibrate_energy_requirements.py +84 -0
- ebm/model/calibrate_heating_systems.py +180 -0
- ebm/model/column_operations.py +157 -0
- ebm/model/construction.py +827 -0
- ebm/model/data_classes.py +223 -0
- ebm/model/database_manager.py +410 -0
- ebm/model/dataframemodels.py +115 -0
- ebm/model/defaults.py +30 -0
- ebm/model/energy_need.py +6 -0
- ebm/model/energy_need_filter.py +182 -0
- ebm/model/energy_purpose.py +115 -0
- ebm/model/energy_requirement.py +353 -0
- ebm/model/energy_use.py +202 -0
- ebm/model/enums.py +8 -0
- ebm/model/exceptions.py +4 -0
- ebm/model/file_handler.py +388 -0
- ebm/model/filter_scurve_params.py +83 -0
- ebm/model/filter_tek.py +152 -0
- ebm/model/heat_pump.py +53 -0
- ebm/model/heating_systems.py +20 -0
- ebm/model/heating_systems_parameter.py +17 -0
- ebm/model/heating_systems_projection.py +3 -0
- ebm/model/heating_systems_share.py +28 -0
- ebm/model/scurve.py +224 -0
- ebm/model/tek.py +1 -0
- ebm/s_curve.py +515 -0
- ebm/services/__init__.py +0 -0
- ebm/services/calibration_writer.py +262 -0
- ebm/services/console.py +106 -0
- ebm/services/excel_loader.py +66 -0
- ebm/services/files.py +38 -0
- ebm/services/spreadsheet.py +289 -0
- ebm/temp_calc.py +99 -0
- ebm/validators.py +565 -0
- ebm-0.99.3.dist-info/METADATA +217 -0
- ebm-0.99.3.dist-info/RECORD +80 -0
- ebm-0.99.3.dist-info/WHEEL +5 -0
- ebm-0.99.3.dist-info/entry_points.txt +3 -0
- ebm-0.99.3.dist-info/licenses/LICENSE +21 -0
- ebm-0.99.3.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
|