forked from open-energy-transition/pypsa-eur
-
Notifications
You must be signed in to change notification settings - Fork 19
Expand file tree
/
Copy pathbuild_energy_totals.py
More file actions
1384 lines (1091 loc) · 46.1 KB
/
Copy pathbuild_energy_totals.py
File metadata and controls
1384 lines (1091 loc) · 46.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# SPDX-FileCopyrightText: Contributors to Open-TYNDP <https://github.com/open-energy-transition/open-tyndp>
# SPDX-FileCopyrightText: Contributors to PyPSA-Eur <https://github.com/pypsa/pypsa-eur>
# SPDX-FileCopyrightText: Open Energy Transition gGmbH
#
# SPDX-License-Identifier: MIT
"""
Build total energy demands and carbon emissions per country using JRC IDEES,
eurostat, and EEA data.
- Country-specific data is read in `build_idees` and read in from [build_eurostat_balances][] and `build_swiss_energy_balances`.
- `build_energy_totals` then combines energy data from Eurostat, Swiss, and IDEES data.
- `build_district_heat_share` calculates the share of district heating for each country from IDEES data.
- Historical CO2 emissions are calculated in `build_eea_co2` and `build_eurostat_co2` and combined in `build_co2_totals`.
Outputs
-------
- `resources/<run_name>/energy_totals.csv`: Energy totals per country, sector and year.
- `resources/<run_name>/co2_totals.csv`: CO2 emissions per country, sector and year.
- `resources/<run_name>/transport_data.csv`: Transport data per country and year.
- `resources/<run_name>/district_heat_share.csv`: District heating share per by country and year.
"""
import logging
import multiprocessing as mp
from functools import partial
from pathlib import Path
import country_converter as coco
import geopandas as gpd
import numpy as np
import pandas as pd
from tqdm import tqdm
from scripts._helpers import configure_logging, mute_print, set_scenario_config
pd.set_option("future.no_silent_downcasting", True)
cc = coco.CountryConverter()
logger = logging.getLogger(__name__)
idx = pd.IndexSlice
def cartesian(s1: pd.Series, s2: pd.Series) -> pd.DataFrame:
"""
Compute the Cartesian product of two pandas Series.
Parameters
----------
s1 : pd.Series
The first pandas Series.
s2 : pd.Series
The second pandas Series.
Returns
-------
pd.DataFrame
A DataFrame representing the Cartesian product of s1 and s2.
Examples
--------
>>> s1 = pd.Series([1, 2, 3], index=["a", "b", "c"])
>>> s2 = pd.Series([4, 5, 6], index=["d", "e", "f"])
>>> cartesian(s1, s2)
d e f
a 4 5 6
b 8 10 12
c 12 15 18
"""
return pd.DataFrame(np.outer(s1, s2), index=s1.index, columns=s2.index)
def reverse(dictionary: dict) -> dict:
"""
Reverses the keys and values of a dictionary.
Parameters
----------
dictionary : dict
The dictionary to be reversed.
Returns
-------
dict
A new dictionary with the keys and values reversed.
Examples
--------
>>> d = {"a": 1, "b": 2, "c": 3}
>>> reverse(d)
{1: 'a', 2: 'b', 3: 'c'}
"""
return {v: k for k, v in dictionary.items()}
idees_rename = {"GR": "EL", "GB": "UK"}
eu28 = cc.EU28as("ISO2").ISO2.tolist()
eu27 = cc.EU27as("ISO2").ISO2.tolist()
eu28_eea = eu28.copy()
eu28_eea.remove("GB")
eu28_eea.append("UK")
to_ipcc = {
"electricity": "1.A.1.a - Public Electricity and Heat Production",
"residential non-elec": "1.A.4.b - Residential",
"services non-elec": "1.A.4.a - Commercial/Institutional",
"rail non-elec": "1.A.3.c - Railways",
"road non-elec": "1.A.3.b - Road Transportation",
"domestic navigation": "1.A.3.d - Domestic Navigation",
"international navigation": "1.D.1.b - International Navigation",
"domestic aviation": "1.A.3.a - Domestic Aviation",
"international aviation": "1.D.1.a - International Aviation",
"total energy": "1 - Energy",
"industrial processes": "2 - Industrial Processes and Product Use",
"agriculture": "3 - Agriculture",
"agriculture, forestry and fishing": "1.A.4.c - Agriculture/Forestry/Fishing",
"LULUCF": "4 - Land Use, Land-Use Change and Forestry",
"waste management": "5 - Waste management",
"other": "6 - Other Sector",
"indirect": "ind_CO2 - Indirect CO2",
"total wL": "Total (with LULUCF)",
"total woL": "Total (without LULUCF)",
}
def idees_per_country(ct: str, base_dir: str) -> pd.DataFrame:
"""
Calculate energy totals per country using JRC-IDEES data.
Parameters
----------
ct : str
The country code.
base_dir : str
The base directory where the JRC-IDEES data files are located.
Returns
-------
pd.DataFrame
A DataFrame containing the energy totals per country. Columns are energy uses.
Notes
-----
- Retrieves JRC-IDEES data for the specified country from `base_dir` for residential, tertiary, and transport sectors.
- Calculates energy totals for each sector, stores them in a dictionary and returns them as data frame.
- Assertions ensure indices of JRC-IDEES data are as expected.
"""
ct_idees = idees_rename.get(ct, ct)
root = Path(base_dir, ct_idees)
years = ("2023", "2021")
fn_residential, fn_tertiary, fn_transport = [
next(
p
for y in years
if (p := root / f"JRC-IDEES-{y}_{s}_{ct_idees}.xlsx").exists()
)
for s in ("Residential", "Tertiary", "Transport")
]
ct_totals = {}
# residential
df = pd.read_excel(fn_residential, "RES_hh_fec", index_col=0)
rows = ["Advanced electric heating", "Conventional electric heating"]
ct_totals["electricity residential space"] = df.loc[rows].sum()
ct_totals["total residential space"] = df.loc["Space heating"]
ct_totals["total residential water"] = df.loc["Water heating"]
assert df.index[23] == "Electricity"
ct_totals["electricity residential water"] = df.iloc[23]
ct_totals["total residential cooking"] = df.loc["Cooking"]
assert df.index[30] == "Electricity"
ct_totals["electricity residential cooking"] = df.iloc[30]
df = pd.read_excel(fn_residential, "RES_summary", index_col=0)
row = "Energy consumption by fuel - Eurostat structure (ktoe)"
ct_totals["total residential"] = df.loc[row]
assert df.index[40] == "Electricity"
ct_totals["electricity residential"] = df.iloc[40]
assert df.index[39] == "Distributed heat"
ct_totals["distributed heat residential"] = df.iloc[39]
assert df.index[43] == "Thermal uses"
ct_totals["thermal uses residential"] = df.iloc[43]
df = pd.read_excel(fn_residential, "RES_hh_eff", index_col=0)
ct_totals["total residential space efficiency"] = df.loc["Space heating"]
assert df.index[5] == "Diesel oil"
ct_totals["oil residential space efficiency"] = df.iloc[5]
assert df.index[6] == "Natural gas"
ct_totals["gas residential space efficiency"] = df.iloc[6]
assert df.index[7] == "Biomass"
ct_totals["biomass residential space efficiency"] = df.iloc[7]
ct_totals["total residential water efficiency"] = df.loc["Water heating"]
assert df.index[18] == "Diesel oil"
ct_totals["oil residential water efficiency"] = df.iloc[18]
assert df.index[19] == "Natural gas"
ct_totals["gas residential water efficiency"] = df.iloc[19]
assert df.index[20] == "Biomass"
ct_totals["biomass residential water efficiency"] = df.iloc[20]
# services
df = pd.read_excel(fn_tertiary, "SER_hh_fec", index_col=0)
ct_totals["total services space"] = df.loc["Space heating"]
rows = ["Advanced electric heating", "Conventional electric heating"]
ct_totals["electricity services space"] = df.loc[rows].sum()
ct_totals["total services water"] = df.loc["Hot water"]
assert df.index[24] == "Electricity"
ct_totals["electricity services water"] = df.iloc[24]
ct_totals["total services cooking"] = df.loc["Catering"]
assert df.index[31] == "Electricity"
ct_totals["electricity services cooking"] = df.iloc[31]
df = pd.read_excel(fn_tertiary, "SER_summary", index_col=0)
row = "Energy consumption by fuel - Eurostat structure (ktoe)"
ct_totals["total services"] = df.loc[row]
assert df.index[43] == "Electricity"
ct_totals["electricity services"] = df.iloc[43]
assert df.index[42] == "Distributed heat"
ct_totals["distributed heat services"] = df.iloc[42]
assert df.index[46] == "Thermal uses"
ct_totals["thermal uses services"] = df.iloc[46]
df = pd.read_excel(fn_tertiary, "SER_hh_eff", index_col=0)
ct_totals["total services space efficiency"] = df.loc["Space heating"]
assert df.index[5] == "Diesel oil"
ct_totals["oil services space efficiency"] = df.iloc[5]
assert df.index[7] == "Conventional gas heaters"
ct_totals["gas services space efficiency"] = df.iloc[7]
assert df.index[8] == "Biomass"
ct_totals["biomass services space efficiency"] = df.iloc[8]
ct_totals["total services water efficiency"] = df.loc["Hot water"]
assert df.index[20] == "Diesel oil"
ct_totals["oil services water efficiency"] = df.iloc[20]
assert df.index[21] == "Natural gas"
ct_totals["gas services water efficiency"] = df.iloc[21]
assert df.index[22] == "Biomass"
ct_totals["biomass services water efficiency"] = df.iloc[22]
# agriculture, forestry and fishing
start = "Detailed split of energy consumption (ktoe)"
end = "Market shares of energy uses (%)"
df = pd.read_excel(fn_tertiary, "AGR_fec", index_col=0).loc[start:end]
rows = [
"Lighting",
"Ventilation",
"Specific electricity uses",
"Pumping devices (electricity)",
]
ct_totals["total agriculture electricity"] = df.loc[rows].sum()
rows = ["Specific heat uses", "Low enthalpy heat"]
ct_totals["total agriculture heat"] = df.loc[rows].sum()
rows = [
"Motor drives",
"Farming machine drives (diesel oil and liquid biofuels)",
"Pumping devices (diesel oil and liquid biofuels)",
]
ct_totals["total agriculture machinery"] = df.loc[rows].sum()
row = "Agriculture, forestry and fishing"
ct_totals["total agriculture"] = df.loc[row]
# transport
df = pd.read_excel(fn_transport, "TrRoad_ene", index_col=0)
ct_totals["total road"] = df.loc["by fuel (EUROSTAT DATA)"]
ct_totals["electricity road"] = df.loc["Electricity"]
ct_totals["total two-wheel"] = df.loc["Powered two-wheelers (Gasoline)"]
assert df.index[19] == "Passenger cars"
ct_totals["total passenger cars"] = df.iloc[19]
assert df.index[30] == "Battery electric vehicles"
ct_totals["electricity passenger cars"] = df.iloc[30]
assert df.index[31] == "Motor coaches, buses and trolley buses"
ct_totals["total other road passenger"] = df.iloc[31]
assert df.index[39] == "Battery electric vehicles"
ct_totals["electricity other road passenger"] = df.iloc[39]
assert df.index[41] == "Light commercial vehicles"
ct_totals["total light duty road freight"] = df.iloc[41]
assert df.index[49] == "Battery electric vehicles"
ct_totals["electricity light duty road freight"] = df.iloc[49]
row = next(
r
for r in (
"Heavy goods vehicles (diesel oil incl. biofuels)",
"Heavy goods vehicles (Diesel oil incl. biofuels)",
)
if r in df.index
)
ct_totals["total heavy duty road freight"] = df.loc[row]
assert df.index[61] == "Passenger cars"
ct_totals["passenger car efficiency"] = df.iloc[61]
df = pd.read_excel(fn_transport, "TrRail_ene", index_col=0)
ct_totals["total rail"] = df.loc["by fuel"]
ct_totals["electricity rail"] = df.loc["Electricity"]
assert df.index[9] == "Passenger transport"
ct_totals["total rail passenger"] = df.iloc[9]
assert df.index[10] == "Metro and tram, urban light rail"
assert df.index[13] == "Electric"
assert df.index[14] == "High speed passenger trains"
ct_totals["electricity rail passenger"] = df.iloc[[10, 13, 14]].sum()
assert df.index[15] == "Freight transport"
ct_totals["total rail freight"] = df.iloc[15]
assert df.index[17] == "Electric"
ct_totals["electricity rail freight"] = df.iloc[17]
df = pd.read_excel(fn_transport, "TrAvia_ene", index_col=0)
assert df.index[4] == "Passenger transport"
ct_totals["total aviation passenger"] = df.iloc[4]
assert df.index[8] == "Freight transport"
ct_totals["total aviation freight"] = df.iloc[8]
assert df.index[2] == "Domestic"
ct_totals["total domestic aviation passenger"] = df.iloc[2]
assert df.index[6] in (
"International - Intra-EEAwCHUK",
"International - Intra-EEAwUK",
)
assert df.index[7] in (
"International - Extra-EEAwCHUK",
"International - Extra-EEAwUK",
)
ct_totals["total international aviation passenger"] = df.iloc[[6, 7]].sum()
assert df.index[9] == "Domestic"
assert df.index[10] in (
"International - Intra-EEAwCHUK",
"International - Intra-EEAwUK",
)
ct_totals["total domestic aviation freight"] = df.iloc[[9, 10]].sum()
assert df.index[11] in (
"International - Extra-EEAwCHUK",
"International - Extra-EEAwUK",
)
ct_totals["total international aviation freight"] = df.iloc[11]
ct_totals["total domestic aviation"] = (
ct_totals["total domestic aviation freight"]
+ ct_totals["total domestic aviation passenger"]
)
ct_totals["total international aviation"] = (
ct_totals["total international aviation freight"]
+ ct_totals["total international aviation passenger"]
)
df = pd.read_excel(fn_transport, "TrNavi_ene", index_col=0)
# coastal and inland
ct_totals["total domestic navigation"] = df.loc["Energy consumption (ktoe)"]
df = pd.read_excel(fn_transport, "TrRoad_act", index_col=0)
assert df.index[85] == "Passenger cars"
ct_totals["passenger cars"] = df.iloc[85]
return pd.DataFrame(ct_totals)
def build_idees(countries: list[str]) -> pd.DataFrame:
"""
Build energy totals from IDEES database for the given list of countries
using :func:`idees_per_country`.
Parameters
----------
countries : list[str]
List of country names for which energy totals need to be built.
Returns
-------
pd.DataFrame
Energy totals for the given countries.
Notes
-----
- Retrieves energy totals per country and year using :func:`idees_per_country`.
- Returns a DataFrame with columns: country, year, and energy totals for different categories.
"""
nprocesses = snakemake.threads
disable_progress = snakemake.config["run"].get("disable_progressbar", False)
func = partial(idees_per_country, base_dir=snakemake.input.idees)
tqdm_kwargs = dict(
ascii=False,
unit=" country",
total=len(countries),
desc="Build from IDEES database",
disable=disable_progress,
)
with mute_print():
with mp.Pool(processes=nprocesses) as pool:
totals_list = list(tqdm(pool.imap(func, countries), **tqdm_kwargs))
totals = pd.concat(
totals_list,
keys=countries,
names=["country", "year"],
)
# clean up dataframe
years = np.arange(2000, 2024)
totals = totals[totals.index.get_level_values(1).isin(years)]
# efficiency kgoe/100km -> ktoe/100km so that after conversion TWh/100km
totals.loc[:, "passenger car efficiency"] /= 1e6
# convert ktoe to TWh
patterns = ["passenger cars", ".*space efficiency", ".*water efficiency"]
exclude = totals.columns.str.fullmatch("|".join(patterns))
totals = totals.copy()
totals.loc[:, ~exclude] *= 11.63 / 1e3
return totals
def fill_missing_years(fill_values: pd.Series) -> pd.Series:
"""
Fill missing years for some countries by first using forward fill (ffill)
and then backward fill (bfill).
Parameters
----------
fill_values : pd.Series
A pandas Series with a MultiIndex (levels: country and year) representing
energy values, where some values may be zero and need to be filled.
Returns
-------
pd.Series
A pandas Series with zero values replaced by the forward-filled and
backward-filled values of the corresponding country.
Notes
-----
- The function groups the data by the 'country' level and performs forward fill
and backward fill to fill zero values.
- Zero values in the original Series are replaced by the ffilled and bfilled
value of their respective country group.
"""
# Forward fill and then backward fill within each country group
fill_values = fill_values.groupby(level="country").ffill().bfill()
return fill_values
def build_energy_totals(
countries: list[str],
eurostat: pd.DataFrame,
swiss: pd.DataFrame,
idees: pd.DataFrame,
) -> pd.DataFrame:
"""
Combine energy totals for the specified countries from Eurostat, Swiss, and
IDEES data.
Parameters
----------
countries : list[str]
List of country codes for which energy totals are to be calculated.
eurostat : pd.DataFrame
Eurostat energy balances dataframe.
swiss : pd.DataFrame
Swiss energy data dataframe.
idees : pd.DataFrame
IDEES energy data dataframe.
Returns
-------
pd.DataFrame
Energy totals dataframe for the given countries.
Notes
-----
- Missing values are filled based on Eurostat energy balances and average values in EU28.
- The function also performs specific calculations for Norway and splits road, rail, and aviation traffic for non-IDEES data.
References
----------
- `Norway heating data <http://www.ssb.no/en/energi-og-industri/statistikker/husenergi/hvert-3-aar/2014-07-14>`_
"""
eurostat_countries = eurostat.country.unique()
eurostat_years = eurostat.year.unique()
new_index = pd.MultiIndex.from_product(
[countries, eurostat_years], names=["country", "year"]
)
efficiency_keywords = ["space efficiency", "water efficiency"]
to_drop = idees.columns[idees.columns.str.contains("|".join(efficiency_keywords))]
to_drop = to_drop.append(pd.Index(["passenger cars", "passenger car efficiency"]))
df = idees.reindex(new_index).drop(to_drop, axis=1)
in_eurostat = df.index.levels[0].intersection(eurostat_countries)
# add international navigation
fill_values = (
eurostat.query("nrg_bal == 'INTMARB' and siec == 'TOTAL'")
.groupby(["country", "year"])
.value.sum(min_count=1)
)
# fill missing years for some countries by mean over the other years
fill_values = fill_missing_years(fill_values)
df.loc[in_eurostat, "total international navigation"] = fill_values
# add swiss energy data
df = pd.concat([df.drop("CH", errors="ignore"), swiss]).sort_index()
# get values for missing countries based on Eurostat EnergyBalances
# agriculture
to_fill = df.index[
df["total agriculture"].isna()
& df.index.get_level_values("country").isin(eurostat_countries)
]
c = to_fill.get_level_values("country")
y = to_fill.get_level_values("year")
# take total final energy consumption from Eurostat
fill_values = (
eurostat.query("nrg_bal == 'FC_OTH_AF_E' and siec == 'TOTAL'")
.groupby(["country", "year"])
.value.sum(min_count=1)
)
# fill missing years for some countries by mean over the other years
fill_values = fill_missing_years(fill_values)
df.loc[to_fill, "total agriculture"] = fill_values
# split into end uses by average EU data from IDEES
uses = ["electricity", "heat", "machinery"]
for use in uses:
avg = (
idees["total agriculture electricity"] / idees["total agriculture"]
).mean()
df.loc[to_fill, f"total agriculture {use}"] = (
df.loc[to_fill, "total agriculture"] * avg
)
# divide cooking/space/water according to averages in EU28
uses = ["space", "cooking", "water"]
to_fill = df.index[
df["total residential"].isna()
& df.index.get_level_values("country").isin(eurostat_countries)
]
c = to_fill.get_level_values("country") # noqa: F841
y = to_fill.get_level_values("year") # noqa: F841
for sector, s in [
("residential", "FC_OTH_HH_E"),
("services", "FC_OTH_CP_E"),
("road", "FC_TRA_ROAD_E"),
("rail", "FC_TRA_RAIL_E"),
]:
# fuel use
for fuel, f in [("electricity", "E7000"), ("total", "TOTAL")]:
fill_values = (
eurostat.query(
"nrg_bal == @s and siec == @f and country in @c and year in @y"
)
.groupby(["country", "year"])
.value.sum(min_count=1)
)
# fill missing years for some countries by mean over the other years
fill_values = fill_missing_years(fill_values)
df.loc[to_fill, f"{fuel} {sector}"] = fill_values
for sector in ["residential", "services"]:
# electric use
for use in uses:
fuel_use = df[f"electricity {sector} {use}"]
fuel = (
df[f"electricity {sector}"].replace(0, np.nan).infer_objects(copy=False)
)
avg = fuel_use.div(fuel).mean()
logger.debug(
f"{sector}: average fraction of electricity for {use} is {avg:.3f}"
)
df.loc[to_fill, f"electricity {sector} {use}"] = (
avg * df.loc[to_fill, f"electricity {sector}"]
)
# non-electric use
for use in uses:
nonelectric_use = (
df[f"total {sector} {use}"] - df[f"electricity {sector} {use}"]
)
nonelectric = df[f"total {sector}"] - df[f"electricity {sector}"]
nonelectric = (
nonelectric.copy().replace(0, np.nan).infer_objects(copy=False)
)
avg = nonelectric_use.div(nonelectric).mean()
logger.debug(
f"{sector}: average fraction of non-electric for {use} is {avg:.3f}"
)
electric_use = df.loc[to_fill, f"electricity {sector} {use}"]
nonelectric = (
df.loc[to_fill, f"total {sector}"]
- df.loc[to_fill, f"electricity {sector}"]
)
df.loc[to_fill, f"total {sector} {use}"] = electric_use + avg * nonelectric
# Fix Norway space and water heating fractions
# http://www.ssb.no/en/energi-og-industri/statistikker/husenergi/hvert-3-aar/2014-07-14
# The main heating source for about 73 per cent of the households is based on electricity
# => 26% is non-electric
if "NO" in df.index:
elec_fraction = 0.73
no_norway = df.drop("NO")
for sector in ["residential", "services"]:
# assume non-electric is heating
nonelectric = (
df.loc["NO", f"total {sector}"] - df.loc["NO", f"electricity {sector}"]
)
total_heating = nonelectric / (1 - elec_fraction)
for use in uses:
nonelectric_use = (
no_norway[f"total {sector} {use}"]
- no_norway[f"electricity {sector} {use}"]
)
nonelectric = (
no_norway[f"total {sector}"] - no_norway[f"electricity {sector}"]
)
nonelectric = (
nonelectric.copy().replace(0, np.nan).infer_objects(copy=False)
)
fraction = nonelectric_use.div(nonelectric).mean()
df.loc["NO", f"total {sector} {use}"] = (
total_heating * fraction
).values
df.loc["NO", f"electricity {sector} {use}"] = (
total_heating * fraction * elec_fraction
).values
# Missing aviation
fill_values = (
eurostat.query("nrg_bal == 'FC_TRA_DAVI_E' and siec == 'TOTAL'")
.groupby(["country", "year"])
.value.sum(min_count=1)
)
# fill missing years for some countries by mean over the other years
fill_values = fill_missing_years(fill_values)
df.loc[to_fill, "total domestic aviation"] = fill_values
fill_values = (
eurostat.query("nrg_bal == 'INTAVI' and siec == 'TOTAL'")
.groupby(["country", "year"])
.value.sum(min_count=1)
)
# fill missing years for some countries by mean over the other years
fill_values = fill_missing_years(fill_values)
df.loc[to_fill, "total international aviation"] = fill_values
# missing domestic navigation
fill_values = (
eurostat.query("nrg_bal == 'FC_TRA_DNAVI_E' and siec == 'TOTAL'")
.groupby(["country", "year"])
.value.sum(min_count=1)
)
# fill missing years for some countries by mean over the other years
fill_values = fill_missing_years(fill_values)
df.loc[to_fill, "total domestic navigation"] = fill_values
# split road traffic for non-IDEES
missing = df.index[df["total passenger cars"].isna()]
for fuel in ["total", "electricity"]:
selection = [
f"{fuel} passenger cars",
f"{fuel} other road passenger",
f"{fuel} light duty road freight",
]
if fuel == "total":
selection.extend([f"{fuel} two-wheel", f"{fuel} heavy duty road freight"])
road = df[selection].sum()
road_fraction = road / road.sum()
fill_values = cartesian(df.loc[missing, f"{fuel} road"], road_fraction)
df.loc[missing, road_fraction.index] = fill_values
# split rail traffic for non-IDEES
missing = df.index[df["total rail passenger"].isna()]
for fuel in ["total", "electricity"]:
selection = [f"{fuel} rail passenger", f"{fuel} rail freight"]
rail = df[selection].sum()
rail_fraction = rail / rail.sum()
fill_values = cartesian(df.loc[missing, f"{fuel} rail"], rail_fraction)
df.loc[missing, rail_fraction.index] = fill_values
# split aviation traffic for non-IDEES
missing = df.index[df["total domestic aviation passenger"].isna()]
for destination in ["domestic", "international"]:
selection = [
f"total {destination} aviation passenger",
f"total {destination} aviation freight",
]
aviation = df[selection].sum()
aviation_fraction = aviation / aviation.sum()
fill_values = cartesian(
df.loc[missing, f"total {destination} aviation"], aviation_fraction
)
df.loc[missing, aviation_fraction.index] = fill_values
for purpose in ["passenger", "freight"]:
attrs = [
f"total domestic aviation {purpose}",
f"total international aviation {purpose}",
]
df.loc[missing, f"total aviation {purpose}"] = df.loc[missing, attrs].sum(
axis=1
)
if "BA" in df.index:
# fill missing data for BA (services and road energy data)
# proportional to RS with ratio of total residential demand
mean_BA = df.loc["BA"].loc[2014:2023, "total residential"].mean()
mean_RS = df.loc["RS"].loc[2014:2023, "total residential"].mean()
ratio = mean_BA / mean_RS
df.loc["BA"] = (
df.loc["BA"].replace(0.0, np.nan).infer_objects(copy=False).values
)
df.loc["BA"] = df.loc["BA"].combine_first(ratio * df.loc["RS"]).values
return df
def build_district_heat_share(countries: list[str], idees: pd.DataFrame) -> pd.Series:
"""
Calculate the share of district heating for each country.
Parameters
----------
countries : list[str]
List of country codes for which to calculate district heating share.
idees : pd.DataFrame
IDEES energy data dataframe.
Returns
-------
pd.Series
Series with the district heating share for each country.
Notes
-----
- The function calculates the district heating share as the sum of residential and services distributed heat, divided by the sum of residential and services thermal uses.
- The district heating share is then reindexed to match the provided list of countries.
- Missing district heating shares are filled from `data/district_heat_share.csv`.
- The function makes a conservative assumption and takes the minimum district heating share from both the IDEES data and `data/district_heat_share.csv`.
"""
# district heating share
district_heat = idees[
["distributed heat residential", "distributed heat services"]
].sum(axis=1)
total_heat = (
idees[["thermal uses residential", "thermal uses services"]]
.sum(axis=1)
.replace(0, np.nan)
.infer_objects(copy=False)
)
district_heat_share = district_heat / total_heat
district_heat_share = district_heat_share.reindex(countries, level="country")
# Missing district heating share
dh_share = (
pd.read_csv(snakemake.input.district_heat_share, index_col=0, usecols=[0, 1])
.div(100)
.squeeze()
)
# make conservative assumption and take minimum from both data sets
new_index = pd.MultiIndex.from_product(
[dh_share.index, district_heat_share.index.get_level_values(1).unique()]
)
district_heat_share = pd.concat(
[district_heat_share, dh_share.reindex(new_index, level=0)], axis=1
).min(axis=1)
district_heat_share = district_heat_share.reindex(countries, level=0)
district_heat_share.name = "district heat share"
# restrict to available years
district_heat_share = (
district_heat_share.unstack()
.dropna(how="all", axis=1)
.ffill(axis=1)
.infer_objects(copy=False)
)
return district_heat_share
def build_eea_co2(
input_co2: str, year: int = 1990, emissions_scope: str = "CO2"
) -> pd.DataFrame:
"""
Calculate CO2 emissions for a given year based on EEA data in Mt.
Parameters
----------
input_co2 : str
Path to the input CSV file with CO2 data.
year : int, optional
Year for which to calculate emissions, by default 1990.
emissions_scope : str, optional
Scope of the emissions to consider, by default "CO2".
Returns
-------
pd.DataFrame
DataFrame with CO2 emissions for the given year.
Notes
-----
- The function reads the `input_co2` data and for a specific `year` and `emission scope`
- It calculates "industrial non-elec" and "agriculture" emissions from that data
- It drops unneeded columns and converts the emissions to Mt.
References
----------
- `EEA CO2 data <https://www.eea.europa.eu/data-and-maps/data/national-emissions-reported-to-the-unfccc-and-to-the-eu-greenhouse-gas-monitoring-mechanism-16>`_ (downloaded 201228, modified by EEA last on 201221)
"""
df = pd.read_csv(input_co2, encoding="latin-1", low_memory=False)
df.replace(dict(Year="1985-1987"), 1986, inplace=True)
df.Year = df.Year.astype(int)
index_col = ["Country_code", "Pollutant_name", "Year", "Sector_name"]
df = df.set_index(index_col).sort_index()
cts = ["CH", "EUA", "NO"] + eu28_eea
slicer = idx[cts, emissions_scope, year, to_ipcc.values()]
emissions = (
df.loc[slicer, "emissions"]
.unstack("Sector_name")
.rename(columns=reverse(to_ipcc))
.droplevel([1, 2])
)
emissions.rename(index={"EUA": "EU28", "UK": "GB"}, inplace=True)
to_subtract = [
"electricity",
"services non-elec",
"residential non-elec",
"road non-elec",
"rail non-elec",
"domestic aviation",
"international aviation",
"domestic navigation",
"international navigation",
"agriculture, forestry and fishing",
]
emissions["industrial non-elec"] = emissions["total energy"] - emissions[
to_subtract
].sum(axis=1)
emissions["agriculture"] += emissions["agriculture, forestry and fishing"]
to_drop = [
"total energy",
"total wL",
"total woL",
"agriculture, forestry and fishing",
]
emissions.drop(columns=to_drop, inplace=True)
# convert from Gt to Mt
return emissions / 1e3
def build_eurostat_co2(eurostat: pd.DataFrame, year: int = 1990) -> pd.Series:
"""
Calculate CO2 emissions for a given year based on Eurostat fuel consumption
data and fuel-specific emissions.
Parameters
----------
eurostat : pd.DataFrame
DataFrame with Eurostat data.
year : int, optional
Year for which to calculate emissions, by default 1990.
Returns
-------
pd.Series
Series with CO2 emissions for the given year.
Notes
-----
- The function hard-sets fuel-specific emissions:
- solid fuels: 0.36 tCO2_equi/MW_th (approximates coal)
- oil: 0.285 tCO2_equi/MW_th (average of distillate and residue)
- natural gas: 0.2 tCO2_equi/MW_th
- It then multiplies the Eurostat fuel consumption data for `year` by the specific emissions and sums the result.
References
----------
- Oil values from `EIA <https://www.eia.gov/tools/faqs/faq.cfm?id=74&t=11>`_
- Distillate oil (No. 2) 0.276
- Residual oil (No. 6) 0.298
- `EIA Electricity Annual <https://www.eia.gov/electricity/annual/html/epa_a_03.html>`_
"""
emissions = pd.Series(
{
"C0000X0350-0370": 0.36, # solid fossil fuels
"O4000XBIO": 0.285, # oil and petroleum products
"G3000": 0.2, # natural gas
}
)
return (
eurostat.query("year == @year and siec in @emissions.index")
.assign(value=lambda df: df["value"] * df["siec"].map(emissions))
.groupby(["country", "nrg_bal"])["value"]
.sum(min_count=1)
)