├── input ├── .DS_Store ├── cop │ └── cop_parameters.csv ├── heating_thresholds │ └── heating_thresholds.csv ├── bgw_bdew │ ├── daily_demand.csv │ ├── hourly_factors_MFH.csv │ ├── hourly_factors_SFH.csv │ └── hourly_factors_COM.csv └── notation.csv ├── checksums.txt ├── environment.yml ├── README.md ├── LICENSE.md ├── scripts ├── misc.py ├── write.py ├── download.py ├── read.py ├── preprocess.py ├── cop.py ├── demand.py └── metadata.py ├── preprocessing └── preprocessing_JRC_IDEES.ipynb └── main.ipynb /input/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oruhnau/when2heat/HEAD/input/.DS_Store -------------------------------------------------------------------------------- /input/cop/cop_parameters.csv: -------------------------------------------------------------------------------- 1 | Coefficient;air;ground;water 2 | 0;6,08;10,29;9,99 3 | 1;-0,0941;-0,2084;-0,2049 4 | 2;0,000464;0,001322;0,001249 5 | -------------------------------------------------------------------------------- /checksums.txt: -------------------------------------------------------------------------------- 1 | when2heat.csv,28feacd386db46959527eae534e79338 2 | when2heat.sqlite,8dbd2b2e699fb9dfadbe610663c58a7a 3 | when2heat.xlsx,befe64ab9006704827b4b23e81b49569 4 | when2heat_multiindex.csv,3516c32f0b029f9ef684a7a689606c19 5 | when2heat_stacked.csv,e30b38420e6a7e8d8e01952bdf611987 6 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: when2heat 2 | channels: 3 | - defaults 4 | - conda-forge 5 | dependencies: 6 | - geopandas=0.9 7 | - jupyter=1.0 8 | - netcdf4=1.5 9 | - pandas=1.3 10 | - pip 11 | - python=3.7 12 | - pyyaml 13 | - pip: 14 | - openpyxl 15 | - cdsapi==0.5.1 16 | 17 | -------------------------------------------------------------------------------- /input/heating_thresholds/heating_thresholds.csv: -------------------------------------------------------------------------------- 1 | ;Heating threshold 2 | AT;14,59 3 | BE;15,2 4 | BG;16,02 5 | CZ;14,8 6 | CH;12 7 | DE;13,98 8 | DK;15,2 9 | EE;11,12 10 | ES;18,47 11 | FI;13,16 12 | FR;15,61 13 | GB;14,18 14 | GR;16,84 15 | HR;18,67 16 | HU;16,84 17 | IE;12,76 18 | IT;15,61 19 | LT;15,2 20 | LU;13,98 21 | LV;12,96 22 | NL;13,98 23 | NO;11,53 24 | PL;15,2 25 | PT;18,47 26 | RO;15,41 27 | SE;13,16 28 | SI;15,41 29 | SK;14,18 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # When2Heat data package 2 | 3 | This repository contains scripts that compile heat demand and COP time series for European countries. 4 | 5 | See the [main Jupter notebook](main.ipynb) for further details. 6 | 7 | ## Preparation 8 | 9 | To work on the Notebooks locally see the installation instructions in the 10 | [wiki](https://github.com/Open-Power-System-Data/common/wiki/Tutorial-to-run-OPSD-scripts). 11 | Note that the requirements can be found in the environment.yml file. 12 | 13 | ## License 14 | 15 | This repository is published under the [MIT License](LICENSE.md). 16 | -------------------------------------------------------------------------------- /input/bgw_bdew/daily_demand.csv: -------------------------------------------------------------------------------- 1 | building_type;SFH;MFH;COM;SFH;MFH;COM;SFH_water 2 | windiness;normal;normal;normal;windy;windy;windy;any 3 | A;1,6209544;1,2328655;1,3010623;1,3819663;1,0443538;1,25696; 4 | B;-37,1833141;-34,7213605;-35,6816144;-37,4124155;-35,0333754;-36,6078453; 5 | C;5,6727847;5,8164304;6,6857976;6,1723179;6,2240634;7,321187; 6 | D;0,0716431;0,0873352;0,1409267;0,0396284;0,0502917;0,077696; 7 | m_s;-0,04957;-0,0409284;-0,0473428;-0,0672159;-0,053583;-0,0696826; 8 | b_s;0,8401015;0,767292;0,8141691;1,1167138;0,9995901;1,1379702; 9 | m_w;-0,002209;-0,002232;-0,0010601;-0,0019982;-0,0021758;-0,0008522;-0,002209 10 | b_w;0,1074468;0,1199207;0,1325092;0,135507;0,1633299;0,1921068;0,1790899 11 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Oliver Ruhnau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /input/bgw_bdew/hourly_factors_MFH.csv: -------------------------------------------------------------------------------- 1 | ;-15;-10;-5;0;5;10;15;20;25;30 2 | 00:00;0,0299;0,0298;0,0291;0,0258;0,0235;0,0225;0,0221;0,0222;0,0246;0,0246 3 | 01:00;0,0296;0,0294;0,0288;0,0266;0,025;0,0247;0,0215;0,022;0,0255;0,0255 4 | 02:00;0,0294;0,0292;0,0285;0,027;0,0254;0,0235;0,0208;0,0207;0,0247;0,0247 5 | 03:00;0,0304;0,0302;0,0296;0,0291;0,0274;0,0258;0,0231;0,0223;0,0237;0,0237 6 | 04:00;0,0355;0,0355;0,0352;0,0351;0,0339;0,0346;0,0343;0,037;0,038;0,038 7 | 05:00;0,0543;0,0543;0,055;0,051;0,0506;0,0513;0,0526;0,0548;0,0519;0,0519 8 | 06:00;0,0549;0,0525;0,0527;0,0505;0,0504;0,0524;0,0605;0,0602;0,044;0,044 9 | 07:00;0,0459;0,0462;0,0465;0,0478;0,049;0,0507;0,055;0,057;0,0555;0,0555 10 | 08:00;0,0459;0,0461;0,0463;0,0487;0,0485;0,0494;0,0521;0,0517;0,0496;0,0496 11 | 09:00;0,0447;0,0449;0,0451;0,0479;0,0475;0,0467;0,0487;0,0507;0,0504;0,0504 12 | 10:00;0,0423;0,0425;0,0426;0,0464;0,0462;0,045;0,0463;0,0474;0,0478;0,0478 13 | 11:00;0,0436;0,0438;0,0439;0,0452;0,0446;0,0434;0,0439;0,0452;0,0464;0,0464 14 | 12:00;0,0427;0,0429;0,043;0,0448;0,0442;0,0426;0,0429;0,0449;0,0466;0,0466 15 | 13:00;0,0423;0,0424;0,0424;0,0438;0,0441;0,042;0,0416;0,0409;0,0428;0,0428 16 | 14:00;0,0419;0,0421;0,0421;0,0437;0,0443;0,042;0,0401;0,0404;0,0439;0,0439 17 | 15:00;0,043;0,0432;0,0432;0,0441;0,0447;0,0432;0,0404;0,0385;0,0398;0,0398 18 | 16:00;0,0436;0,0438;0,0439;0,0459;0,0466;0,0455;0,0415;0,0391;0,0392;0,0392 19 | 17:00;0,0458;0,046;0,0462;0,0473;0,0482;0,0478;0,0436;0,041;0,042;0,042 20 | 18:00;0,0453;0,0457;0,0458;0,0479;0,0492;0,0499;0,047;0,0444;0,0442;0,0442 21 | 19:00;0,0463;0,0466;0,0469;0,0476;0,0491;0,0513;0,0505;0,0474;0,0455;0,0455 22 | 20:00;0,0455;0,0458;0,0461;0,0461;0,048;0,0509;0,0518;0,05;0,0496;0,0496 23 | 21:00;0,0432;0,0434;0,0435;0,0423;0,0439;0,0465;0,0496;0,0494;0,0481;0,0481 24 | 22:00;0,0417;0,0418;0,0419;0,0377;0,0379;0,0393;0,0409;0,0419;0,0437;0,0437 25 | 23:00;0,0321;0,0319;0,0315;0,0277;0,0278;0,0289;0,0292;0,0307;0,0326;0,0326 26 | -------------------------------------------------------------------------------- /input/bgw_bdew/hourly_factors_SFH.csv: -------------------------------------------------------------------------------- 1 | ;-15;-10;-5;0;5;10;15;20;25;30 2 | 00:00;0,0296;0,0292;0,0281;0,0262;0,023;0,0196;0,0142;0,0096;0,0045;0,0045 3 | 01:00;0,0294;0,0289;0,0279;0,0266;0,0237;0,0208;0,0155;0,0105;0,0054;0,0054 4 | 02:00;0,03;0,0296;0,0286;0,0274;0,0243;0,0217;0,0167;0,0112;0,0044;0,0044 5 | 03:00;0,0307;0,0303;0,0294;0,0289;0,0262;0,0249;0,02;0,0162;0,0081;0,0081 6 | 04:00;0,0321;0,0318;0,031;0,0333;0,0312;0,0326;0,0298;0,0289;0,0191;0,0191 7 | 05:00;0,0381;0,038;0,0377;0,043;0,0448;0,0483;0,0541;0,0662;0,0601;0,0601 8 | 06:00;0,0577;0,0573;0,0602;0,0551;0,0577;0,0619;0,0685;0,0821;0,0939;0,0939 9 | 07:00;0,0525;0,053;0,0537;0,051;0,0537;0,0578;0,0679;0,0757;0,0809;0,0809 10 | 08:00;0,0498;0,0501;0,0507;0,0497;0,051;0,0527;0,0578;0,0609;0,0614;0,0614 11 | 09:00;0,0476;0,0479;0,0483;0,0482;0,0488;0,0486;0,0527;0,0601;0,0587;0,0587 12 | 10:00;0,0466;0,0469;0,0472;0,0467;0,0465;0,0452;0,0478;0,0513;0,0533;0,0533 13 | 11:00;0,0437;0,0438;0,0438;0,0446;0,0448;0,043;0,0435;0,0483;0,0541;0,0541 14 | 12:00;0,0423;0,0424;0,0424;0,0439;0,0438;0,0424;0,0415;0,0442;0,0521;0,0521 15 | 13:00;0,0422;0,0422;0,0422;0,0435;0,0437;0,0422;0,0402;0,0397;0,042;0,042 16 | 14:00;0,0418;0,0418;0,0418;0,0448;0,0446;0,0433;0,0399;0,0378;0,0403;0,0403 17 | 15:00;0,0438;0,0439;0,0439;0,0456;0,0458;0,0452;0,0414;0,0359;0,036;0,036 18 | 16:00;0,0472;0,0475;0,0478;0,0481;0,0481;0,0478;0,0441;0,04;0,0423;0,0423 19 | 17:00;0,0482;0,0485;0,0489;0,0489;0,0501;0,0509;0,0477;0,0428;0,0498;0,0498 20 | 18:00;0,0472;0,0475;0,0478;0,0491;0,0506;0,0523;0,0515;0,0456;0,0482;0,0482 21 | 19:00;0,0469;0,0471;0,0475;0,0484;0,0505;0,0527;0,0552;0,0513;0,0508;0,0508 22 | 20:00;0,0461;0,0463;0,0465;0,0466;0,0485;0,0504;0,0535;0,0519;0,0526;0,0526 23 | 21:00;0,0423;0,0424;0,0424;0,0414;0,043;0,044;0,0474;0,0442;0,045;0,045 24 | 22:00;0,0345;0,0343;0,0337;0,0318;0,0309;0,0301;0,0315;0,0309;0,0262;0,0262 25 | 23:00;0,0298;0,0294;0,0284;0,0272;0,0246;0,0218;0,0177;0,0147;0,0108;0,0108 26 | -------------------------------------------------------------------------------- /scripts/misc.py: -------------------------------------------------------------------------------- 1 | 2 | import pytz 3 | import pandas as pd 4 | 5 | 6 | def localize(df, country, ambiguous=None): 7 | 8 | # The exceptions below correct for daylight saving time 9 | try: 10 | df.index = df.index.tz_localize(pytz.country_timezones[country][0], ambiguous=ambiguous) 11 | return df 12 | 13 | # Delete values that do not exist because of daylight saving time 14 | except pytz.NonExistentTimeError as err: 15 | return localize(df.loc[df.index != err.args[0], ], country) 16 | 17 | # Duplicate values that exist twice because of daylight saving time 18 | except pytz.AmbiguousTimeError as err: 19 | idx = pd.Timestamp(err.args[0].split("from ")[1].split(",")[0]) 20 | unambiguous_df = localize(df.loc[df.index != idx, ], country) 21 | ambiguous_df = localize(df.loc[[idx, idx], ], country, ambiguous=[True, False]) 22 | return unambiguous_df.append(ambiguous_df).sort_index() 23 | 24 | 25 | def upsample_df(df, resolution): 26 | 27 | # The low-resolution values are applied to all high-resolution values up to the next low-resolution value 28 | # In particular, the last low-resolution value is extended up to where the next low-resolution value would be 29 | 30 | df = df.copy() 31 | 32 | # Determine the original frequency 33 | freq = df.index[-1] - df.index[-2] 34 | 35 | # Temporally append the DataFrame by one low-resolution value 36 | df.loc[df.index[-1] + freq, :] = df.iloc[-1, :] 37 | 38 | # Up-sample 39 | df = df.resample(resolution).pad() 40 | 41 | # Drop the temporal low-resolution value 42 | df.drop(df.index[-1], inplace=True) 43 | 44 | return df 45 | 46 | 47 | def group_df_by_multiple_column_levels(df, column_levels): 48 | 49 | df = df.groupby(df.columns.droplevel(list(set(df.columns.names) - set(column_levels))), axis=1).sum() 50 | df.columns = pd.MultiIndex.from_tuples(df.columns, names=column_levels) 51 | 52 | return df 53 | -------------------------------------------------------------------------------- /input/notation.csv: -------------------------------------------------------------------------------- 1 | country,variable,attribute,description 2 | ISO-2 digit country code,heat_demand,total,Heat demand for space and water heating 3 | ISO-2 digit country code,heat_demand,space,Heat demand for space heating 4 | ISO-2 digit country code,heat_demand,water,Heat demand for water heating 5 | ISO-2 digit country code,heat_demand,space_SFH,Heat demand for space heating in single-family houses 6 | ISO-2 digit country code,heat_demand,space_MFH,Heat demand for space heating in multi-family houses 7 | ISO-2 digit country code,heat_demand,space_COM,Heat demand for space heating in commercial buildings 8 | ISO-2 digit country code,heat_demand,water_SFH,Heat demand for water heating in single-family houses 9 | ISO-2 digit country code,heat_demand,water_MFH,Heat demand for water heating in multi-family houses 10 | ISO-2 digit country code,heat_demand,water_COM,Heat demand for water heating in commercial buildings 11 | ISO-2 digit country code,heat_profile,space_SFH,Normalized heat demand for space heating in single-family houses 12 | ISO-2 digit country code,heat_profile,space_MFH,Normalized heat demand for space heating in multi-family houses 13 | ISO-2 digit country code,heat_profile,space_COM,Normalized heat demand for space heating in commercial buildings 14 | ISO-2 digit country code,heat_profile,water_SFH,Normalized heat demand for water heating in single-family houses 15 | ISO-2 digit country code,heat_profile,water_MFH,Normalized heat demand for water heating in multi-family houses 16 | ISO-2 digit country code,heat_profile,water_COM,Normalized heat demand for water heating in commercial buildings 17 | ISO-2 digit country code,COP,ASHP_floor,COP of air-source heat pumps with floor heating 18 | ISO-2 digit country code,COP,ASHP_radiator,COP of air-source heat pumps with radiator heating 19 | ISO-2 digit country code,COP,ASHP_water,COP of air-source heat pumps with water heating 20 | ISO-2 digit country code,COP,GSHP_floor,COP of ground-source heat pumps with floor heating 21 | ISO-2 digit country code,COP,GSHP_radiator,COP of ground-source heat pumps with radiator heating 22 | ISO-2 digit country code,COP,GSHP_water,COP of ground-source heat pumps with water heating 23 | ISO-2 digit country code,COP,WSHP_floor,COP of groundwater-source heat pumps with floor heating 24 | ISO-2 digit country code,COP,WSHP_radiator,COP of groundwater-source heat pumps with radiator heating 25 | ISO-2 digit country code,COP,WSHP_water,COP of groundwater-source heat pumps with water heating -------------------------------------------------------------------------------- /scripts/write.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import sqlite3 4 | import pandas as pd 5 | 6 | 7 | def shaping(demand, cop): 8 | print("index:") 9 | 10 | # Merge demand and cop 11 | df = pd.concat([demand, cop], axis=1) 12 | 13 | df = df.sort_index(level=0, axis=1) 14 | 15 | # Timestamp 16 | index = pd.DatetimeIndex(df.index) 17 | df.index = pd.MultiIndex.from_tuples(zip( 18 | index.strftime('%Y-%m-%dT%H:%M:%SZ'), 19 | index.tz_convert('Europe/Brussels').strftime('%Y-%m-%dT%H:%M:%S%z') 20 | )) 21 | df.index.names = ['utc_timestamp', 'cet_cest_timestamp'] 22 | 23 | # SingleIndex 24 | single = df.copy() 25 | single.columns = ['_'.join([level for level in col_name[0:3]]) for col_name in df.columns.values] 26 | single.insert(0, 'cet_cest_timestamp', single.index.get_level_values(1)) 27 | single.index = single.index.droplevel(['cet_cest_timestamp']) 28 | 29 | # Stacked 30 | stacked = df.copy() 31 | stacked.index = stacked.index.droplevel(['cet_cest_timestamp']) 32 | stacked.columns = stacked.columns.droplevel(['unit']) 33 | stacked = stacked.transpose().stack(dropna=True).to_frame(name='data') 34 | 35 | # Excel 36 | df_excel = df.copy() 37 | df_excel.index = pd.MultiIndex.from_tuples(zip( 38 | index.strftime('%Y-%m-%dT%H:%M:%SZ'), 39 | index.tz_convert('Europe/Brussels').strftime('%Y-%m-%dT%H:%M:%S') 40 | )) 41 | 42 | return { 43 | 'multiindex': df, 44 | 'singleindex': single, 45 | 'stacked': stacked, 46 | 'excel': df_excel 47 | } 48 | 49 | 50 | def to_sql(shaped_dfs, output_path, home_path): 51 | 52 | os.chdir(output_path) 53 | table = 'when2heat' 54 | shaped_dfs['singleindex'].to_sql(table, sqlite3.connect('when2heat.sqlite'), 55 | if_exists='replace', index_label='utc_timestamp') 56 | os.chdir(home_path) 57 | 58 | 59 | def to_csv(shaped_dfs, output_path): 60 | 61 | for shape, df in shaped_dfs.items(): 62 | 63 | if shape == 'excel': 64 | file = os.path.join(output_path, 'when2heat.xlsx.csv') 65 | df.to_csv(file, sep=';', decimal=',', float_format='%g') 66 | 67 | elif shape == 'singleindex': 68 | file = os.path.join(output_path, 'when2heat.csv') 69 | df.to_csv(file, sep=';', decimal=',', float_format='%g') 70 | 71 | else: 72 | file = os.path.join(output_path, 'when2heat_{}.csv'.format(shape)) 73 | df.to_csv(file, float_format='%g') 74 | -------------------------------------------------------------------------------- /scripts/download.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import ssl 4 | import datetime 5 | import urllib 6 | import zipfile 7 | import cdsapi 8 | from IPython.display import clear_output 9 | 10 | 11 | 12 | def wind(input_path): 13 | filename = 'ERA_wind.nc' 14 | weather_path = os.path.join(input_path, 'weather') 15 | os.makedirs(weather_path, exist_ok=True) 16 | file = os.path.join(weather_path, filename) 17 | 18 | if not os.path.isfile(file): 19 | 20 | # Select all months from 1979 to 2021 by the date of the first day of the month 21 | data_package = 'reanalysis-era5-single-levels-monthly-means' 22 | variable = "10m_wind_speed" 23 | product_type = 'monthly_averaged_reanalysis' 24 | dates = { 25 | 'year': [str(year) for year in range(1979, 2022)], 26 | 'month': ["%.2d" % month for month in range(1, 13)], 27 | 'time': [datetime.time(i).strftime('%H:%M') for i in range(1)] 28 | } 29 | 30 | # Call the general weather download function with wind specific parameters 31 | weather(data_package, variable, dates, product_type, file) 32 | 33 | else: 34 | print('{} already exists. Download is skipped.'.format(file)) 35 | 36 | clear_output(wait=False) 37 | print("Download successful") 38 | 39 | 40 | def temperatures(input_path, year_start, year_end): 41 | 42 | for year in ["%.2d" % y for y in range(year_start, year_end+1)]: 43 | for variable in ['2m_temperature', 'soil_temperature_level_1']: 44 | 45 | filename = 'ERA_temperature_{}_{}.nc'.format(variable, year) 46 | weather_path = os.path.join(input_path, 'weather') 47 | os.makedirs(weather_path, exist_ok=True) 48 | file = os.path.join(weather_path, filename) 49 | 50 | if not os.path.isfile(file): 51 | #Select period 52 | data_package = 'reanalysis-era5-single-levels' 53 | variable = variable 54 | product_type = 'reanalysis' 55 | dates = { 56 | 'year': year, 57 | 'month': ["%.2d" % month for month in range(1, 13)], 58 | 'day': ["%.2d" % day for day in range(1, 32)], 59 | 'time': [datetime.time(i).strftime('%H:%M') for i in range(24)] 60 | } 61 | 62 | # Call the general weather download function with temperature specific parameters 63 | weather(data_package, variable, dates, product_type, file) 64 | 65 | else: 66 | print('{} already exists. Download is skipped.'.format(file)) 67 | 68 | clear_output(wait=False) 69 | print("Download successful") 70 | 71 | def weather(data_package, variable, dates, product_type, file): 72 | 73 | # if not os.environ.get('PYTHONHTTPSVERIFY', '') and getattr(ssl, '_create_unverified_context', None): 74 | # ssl._create_default_https_context = ssl._create_unverified_context 75 | 76 | c = cdsapi.Client() 77 | 78 | params = { 79 | 'format': 'netcdf', 80 | 'variable': variable, 81 | "year": dates["year"], 82 | "month": dates["month"], 83 | "time": dates["time"], 84 | 'product_type': product_type, 85 | 'area': [72, -10.5, 36.75, 25.5] 86 | } 87 | 88 | if (variable == '2m_temperature') | (variable == 'soil_temperature_level_1'): 89 | params["day"] = ["%.2d" % day for day in range(1, 32)] 90 | c.retrieve(data_package, params, file) 91 | 92 | 93 | def population(input_path): 94 | 95 | # Set URL and directories 96 | url = 'https://ec.europa.eu/eurostat/cache/GISCO/geodatafiles/GEOSTAT-grid-POP-1K-2011-V2-0-1.zip' 97 | population_path = os.path.join(input_path, 'population') 98 | os.makedirs(population_path, exist_ok=True) 99 | destination = os.path.join(population_path, 'GEOSTAT-grid-POP-1K-2011-V2-0-1.zip') 100 | unzip_dir = os.path.join(population_path, 'Version 2_0_1') 101 | 102 | # Download file 103 | if not os.path.isfile(destination): 104 | urllib.request.urlretrieve(url, destination) 105 | else: 106 | print('{} already exists. Download is skipped.'.format(destination)) 107 | # Unzip file 108 | if not os.path.isdir(unzip_dir): 109 | with zipfile.ZipFile(destination, 'r') as f: 110 | f.extractall(population_path) 111 | else: 112 | print('{} already exists. Unzipping is skipped.'.format(unzip_dir)) 113 | 114 | clear_output(wait=False) 115 | print("Download successful") 116 | -------------------------------------------------------------------------------- /scripts/read.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import pandas as pd 4 | import geopandas as gpd 5 | import datetime as dt 6 | from netCDF4 import Dataset, num2date 7 | from shapely.geometry import Point 8 | 9 | 10 | def temperature(input_path, year_start, year_end, parameter): 11 | 12 | 13 | df_list = [] 14 | for year in range(year_start, year_end + 1): 15 | if parameter == "t2m": 16 | df_int = pd.concat( 17 | [weather(input_path, 'ERA_temperature_{}_{}.nc'.format('2m_temperature', year), parameter)], 18 | axis=0 19 | ) 20 | 21 | if parameter == "stl1": 22 | df_int = pd.concat( 23 | [weather(input_path, 'ERA_temperature_{}_{}.nc'.format('soil_temperature_level_1', year), parameter)], 24 | axis=0 25 | ) 26 | df_list.append(df_int) 27 | df = pd.concat(df_list, axis = 0) 28 | 29 | return df 30 | 31 | 32 | def wind(input_path): 33 | 34 | return weather(input_path, 'ERA_wind.nc', 'si10') 35 | 36 | 37 | def weather(input_path, filename, variable_name): 38 | 39 | file = os.path.join(input_path, 'weather', filename) 40 | # Read the netCDF file 41 | nc = Dataset(file) 42 | time = nc.variables['time'][:] 43 | time_units = nc.variables['time'].units 44 | latitude = nc.variables['latitude'][:] 45 | longitude = nc.variables['longitude'][:] 46 | variable = nc.variables[variable_name][:] 47 | 48 | # Transform to pd.DataFrame 49 | index = pd.Index(num2date(time, time_units, only_use_python_datetimes=True), name='time') 50 | 51 | index = index.map(lambda x: dt.datetime(x.year, x.month, x.day, x.hour, x.minute, x.second)) 52 | 53 | df = pd.DataFrame(data=variable.reshape(len(time), len(latitude) * len(longitude)), 54 | index=index, 55 | columns=pd.MultiIndex.from_product([latitude, longitude], names=('latitude', 'longitude'))) 56 | 57 | return df 58 | 59 | def population(input_path): 60 | 61 | directory = 'population/Version 2_0_1/' 62 | filename = 'GEOSTAT_grid_POP_1K_2011_V2_0_1.csv' 63 | 64 | # Read population data 65 | df = pd.read_csv(os.path.join(input_path, directory, filename), 66 | usecols=['GRD_ID', 'TOT_P', 'CNTR_CODE'], 67 | index_col='GRD_ID') 68 | 69 | # Make GeoDataFrame from the the coordinates in the index 70 | gdf = gpd.GeoDataFrame(df) 71 | gdf['geometry'] = df.index.map(lambda i: Point( 72 | [1000 * float(x) + 500 for x in reversed(i.split('N')[1].split('E'))] 73 | )) 74 | 75 | # Transform coordinate reference system to 'latitude/longitude' 76 | gdf.crs = {'init': 'epsg:3035'} 77 | 78 | return gdf 79 | 80 | 81 | def daily_parameters(input_path): 82 | 83 | file = os.path.join(input_path, 'bgw_bdew', 'daily_demand.csv') 84 | return pd.read_csv(file, sep=';', decimal=',', header=[0, 1], index_col=0) 85 | 86 | 87 | def heating_thresholds(input_path): 88 | 89 | file = os.path.join(input_path, 'heating_thresholds', 'heating_thresholds.csv') 90 | return pd.read_csv(file, sep=';', decimal=',', index_col=0)['Heating threshold'] 91 | 92 | 93 | def hourly_parameters(input_path): 94 | 95 | def read(): 96 | file = os.path.join(input_path, 'bgw_bdew', filename) 97 | return pd.read_csv(file, sep=';', decimal=',', index_col=index_col).apply(pd.to_numeric, downcast='float') 98 | 99 | parameters = {} 100 | for building_type in ['SFH', 'MFH', 'COM']: 101 | 102 | filename = 'hourly_factors_{}.csv'.format(building_type) 103 | 104 | # MultiIndex for commercial heat because of weekday dependency 105 | index_col = [0, 1] if building_type == 'COM' else 0 106 | 107 | parameters[building_type] = read() 108 | 109 | return parameters 110 | 111 | 112 | def building_database(input_path): 113 | 114 | return { 115 | heat_type: { 116 | building_type: pd.read_csv( 117 | os.path.join(input_path, 118 | 'JRC_IDEES', 119 | '{}_{}.csv'.format(building_type, heat_type)), 120 | decimal=',', index_col=0 121 | ).apply(pd.to_numeric, downcast='float') 122 | for building_type in ['Residential', 'Tertiary'] 123 | } 124 | for heat_type in ['space', 'water'] 125 | } 126 | 127 | def cop_parameters(input_path): 128 | 129 | file = os.path.join(input_path, 'cop', 'cop_parameters.csv') 130 | return pd.read_csv(file, sep=';', decimal=',', header=0, index_col=0).apply(pd.to_numeric, downcast='float') 131 | -------------------------------------------------------------------------------- /scripts/preprocess.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import pandas as pd 4 | import geopandas as gpd 5 | from shapely.geometry import Point 6 | 7 | import scripts.read as read 8 | from scripts.misc import upsample_df 9 | 10 | 11 | def map_population(input_path, countries, interim_path, plot=True): 12 | 13 | population = None 14 | weather_grid = None 15 | mapped_population = {} 16 | 17 | for country in countries: 18 | 19 | file = os.path.join(interim_path, 'population_{}'.format(country)) 20 | 21 | if not os.path.isfile(file): 22 | 23 | if population is None: 24 | 25 | population = read.population(input_path) 26 | weather_data = read.wind(input_path) # For the weather grid 27 | 28 | # Make GeoDataFrame from the weather data coordinates 29 | weather_grid = gpd.GeoDataFrame(index=weather_data.columns) 30 | weather_grid['geometry'] = weather_grid.index.map(lambda i: Point(reversed(i))) 31 | 32 | # Set coordinate reference system to 'latitude/longitude' 33 | weather_grid.crs = {'init': 'epsg:4326'} 34 | 35 | # Make polygons around the weather points 36 | weather_grid['geometry'] = weather_grid.geometry.apply(lambda point: point.buffer(.75 / 2, cap_style=3)) 37 | 38 | # Make list from MultiIndex (this is necessary for the spatial join) 39 | weather_grid.index = weather_grid.index.tolist() 40 | 41 | 42 | # For Luxembourg, a single weather grid point is manually added for lack of population geodata 43 | if country == 'LU': 44 | s = pd.Series({(49.5, 6): 1}) 45 | 46 | else: 47 | 48 | # Filter population data by country to cut processing time 49 | if country == 'GB': 50 | gdf = population[population['CNTR_CODE'] == 'UK'].copy() 51 | elif country == 'GR': 52 | gdf = population[population['CNTR_CODE'] == 'EL'].copy() 53 | else: 54 | gdf = population[population['CNTR_CODE'] == country].copy() 55 | 56 | # Align coordinate reference systems 57 | gdf = gdf.to_crs({'init': 'epsg:4326'}) 58 | 59 | # Spatial join 60 | gdf = gpd.sjoin(gdf, weather_grid, how="left", op='within') 61 | 62 | # Sum up population 63 | s = gdf.groupby('index_right')['TOT_P'].sum() 64 | 65 | # Write results to interim path 66 | s.to_pickle(file) 67 | 68 | else: 69 | 70 | s = pd.read_pickle(file) 71 | print('{} already exists and is read from disk.'.format(file)) 72 | 73 | mapped_population[country] = s 74 | 75 | if plot: 76 | print('Plot of the re-mapped population data of {} (first selected country) ' 77 | 'for visual inspection:'.format(countries[0])) 78 | gdf = gpd.GeoDataFrame(mapped_population[countries[0]], columns=['TOT_P']) 79 | gdf['geometry'] = gdf.index.map(lambda i: Point(reversed(i))) 80 | gdf.plot(column='TOT_P') 81 | 82 | return mapped_population 83 | 84 | 85 | def wind(input_path, mapped_population, plot=True): 86 | 87 | df = read.wind(input_path) 88 | 89 | # Temporal average 90 | s = df.mean(0) 91 | if plot: 92 | print('Plot of the wind averages for visual inspection:') 93 | gdf = gpd.GeoDataFrame(s, columns=['wind']) 94 | gdf['geometry'] = gdf.index.map(lambda i: Point(reversed(i))) 95 | gdf.plot(column='wind') 96 | 97 | # Wind data is filtered by country 98 | return pd.concat( 99 | [s[population.index] for population in mapped_population.values()], 100 | keys=mapped_population.keys(), names=['country', 'latitude', 'longitude'], axis=0 101 | ).apply(pd.to_numeric, downcast='float') 102 | 103 | 104 | def temperature(input_path, year_start, year_end, mapped_population): 105 | 106 | parameters = { 107 | 'air': 't2m', 108 | 'soil': 'stl1' 109 | } 110 | 111 | ts_parameters = [] 112 | for parameter in parameters.values(): 113 | 114 | ts_years = [] 115 | for year in list(range(year_start, year_end + 1)): 116 | ts = read.temperature(input_path, year, year, parameter) 117 | ts_years.append(ts) 118 | 119 | df_years = pd.concat(ts_years, axis=0) 120 | 121 | # Temperature data is filtered by country 122 | df_countries = pd.concat( 123 | [df_years[population.index] for population in mapped_population.values()], 124 | keys=mapped_population.keys(), axis=1, names=['country', 'latitude', 'longitude'] 125 | ).apply(pd.to_numeric, downcast='float') 126 | 127 | ts_parameters.append(df_countries) 128 | 129 | return pd.concat( 130 | ts_parameters, keys=parameters.keys(), names=['parameter', 'country', 'latitude', 'longitude'], axis=1 131 | ) -------------------------------------------------------------------------------- /preprocessing/preprocessing_JRC_IDEES.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "636d200f", 6 | "metadata": {}, 7 | "source": [ 8 | "## Manual data download" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "id": "d3033e30", 14 | "metadata": {}, 15 | "source": [ 16 | "Download files from [JRC-IDEES](https://data.jrc.ec.europa.eu/dataset/jrc-10110-10001) and follow the Excel url. Than, download the country-specific annual demands \"JRC-IDEES-2015_All_xlsx_COUNTRY.zip\" and pose the sector specific file in your repository or adjust your filename with your download path. " 17 | ] 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "id": "ed6d60d6", 22 | "metadata": {}, 23 | "source": [ 24 | "## Import Python libraries" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 1, 30 | "id": "c6404e3a", 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "import pandas as pd\n", 35 | "from openpyxl import load_workbook\n", 36 | "import os" 37 | ] 38 | }, 39 | { 40 | "cell_type": "markdown", 41 | "id": "1b5bcba0", 42 | "metadata": {}, 43 | "source": [ 44 | "## Select geographical, sectoral, and temporal scope" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": 2, 50 | "id": "1b192e12", 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [ 54 | "all_countries = ['AT', 'BE', 'BG', 'CZ', 'DE', 'DK', \n", 55 | " 'EE', 'ES', 'FI', 'FR', 'GB', 'HR', \n", 56 | " 'HU', 'IE', 'IT', 'LT', 'LU', 'LV', 'GR',\n", 57 | " 'NL', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK']\n", 58 | "selected_countries = all_countries\n", 59 | "\n", 60 | "# GB is named UK in JCR\n", 61 | "# GR is named EL in JCR\n", 62 | "# missing in JCR: CH, NO\n", 63 | "name_clearification = {\"GB\" : \"UK\",\n", 64 | " \"GR\": \"EL\"}" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": 3, 70 | "id": "852ecc55", 71 | "metadata": {}, 72 | "outputs": [], 73 | "source": [ 74 | "sectors = [\"Residential\", \"Tertiary\"]\n", 75 | "applications = [\"water\", \"space\"]\n", 76 | "\n", 77 | "start_year = \"2008\"\n", 78 | "end_year = \"2015\"" 79 | ] 80 | }, 81 | { 82 | "cell_type": "markdown", 83 | "id": "10173279", 84 | "metadata": {}, 85 | "source": [ 86 | "## Preprocessing of JRC-IDEES data" 87 | ] 88 | }, 89 | { 90 | "cell_type": "markdown", 91 | "id": "2ae9398a", 92 | "metadata": {}, 93 | "source": [ 94 | "Read the excel country- and sector-specific excel and make a sector-specific dataframe which is placed in the interim folder" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": 4, 100 | "id": "fe220566", 101 | "metadata": {}, 102 | "outputs": [], 103 | "source": [ 104 | "def read (sector, application, country_code):\n", 105 | " \n", 106 | " if country_code == \"GB\":\n", 107 | " filename = f\"JRC-IDEES-2015_{sector}_{name_clearification[country_code]}.xlsx\"\n", 108 | " elif country_code ==\"GR\":\n", 109 | " filename = f\"JRC-IDEES-2015_{sector}_{name_clearification[country_code]}.xlsx\"\n", 110 | " else:\n", 111 | " filename = f\"JRC-IDEES-2015_{sector}_{country_code}.xlsx\"\n", 112 | " sheet_name = \"RES_hh_tes\" if sector == \"Residential\" else \"SER_hh_tes\"\n", 113 | " \n", 114 | " raw = pd.read_excel(filename, header = 0, sheet_name = sheet_name, index_col = 0)\n", 115 | " \n", 116 | " if application == \"water\":\n", 117 | " row_selection = 'Water heating' if sector == \"Residential\" else \"Hot water\"\n", 118 | " else: \n", 119 | " row_selection = \"Space heating\"\n", 120 | " \n", 121 | " df = raw.loc[row_selection, start_year:end_year].to_frame().rename(columns = {row_selection: country_code})\n", 122 | " df = df.transpose() * 1.163e-2\n", 123 | " return df" 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": 7, 129 | "id": "c20c8af6", 130 | "metadata": {}, 131 | "outputs": [], 132 | "source": [ 133 | "input_path = os.path.realpath('../input')" 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": 8, 139 | "id": "2d27793a", 140 | "metadata": {}, 141 | "outputs": [], 142 | "source": [ 143 | "for sector in sectors:\n", 144 | " for application in applications:\n", 145 | " pd.concat([\n", 146 | " read(sector, application, country) for country in selected_countries\n", 147 | " ], axis = 0).to_csv(f\"{input_path}/JRC_IDEES/{sector}_{application}.csv\", decimal = \",\")" 148 | ] 149 | }, 150 | { 151 | "cell_type": "code", 152 | "execution_count": null, 153 | "id": "2cdf39dc", 154 | "metadata": {}, 155 | "outputs": [], 156 | "source": [] 157 | } 158 | ], 159 | "metadata": { 160 | "kernelspec": { 161 | "display_name": "Python 3", 162 | "language": "python", 163 | "name": "python3" 164 | }, 165 | "language_info": { 166 | "codemirror_mode": { 167 | "name": "ipython", 168 | "version": 3 169 | }, 170 | "file_extension": ".py", 171 | "mimetype": "text/x-python", 172 | "name": "python", 173 | "nbconvert_exporter": "python", 174 | "pygments_lexer": "ipython3", 175 | "version": "3.7.10" 176 | } 177 | }, 178 | "nbformat": 4, 179 | "nbformat_minor": 5 180 | } 181 | -------------------------------------------------------------------------------- /scripts/cop.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import pandas as pd 4 | 5 | from scripts.misc import localize 6 | from scripts.misc import group_df_by_multiple_column_levels 7 | 8 | 9 | def source_temperature(temperature): 10 | 11 | celsius = temperature - 273.15 12 | 13 | return pd.concat( 14 | [celsius['air'], celsius['soil'] - 5, 0 * celsius['air'] + 10 - 5], 15 | keys=['air', 'ground', 'water'], 16 | names=['source', 'country', 'latitude', 'longitude'], 17 | axis=1 18 | ) 19 | 20 | 21 | def sink_temperature(temperature): 22 | 23 | celsius = temperature['air'] - 273.15 24 | 25 | return pd.concat( 26 | [-1 * celsius + 40, -.5 * celsius + 30, 0 * celsius + 50], 27 | keys=['radiator', 'floor', 'water'], 28 | names=['sink', 'country', 'latitude', 'longitude'], 29 | axis=1 30 | ) 31 | 32 | 33 | def spatial_cop(source, sink, cop_parameters): 34 | 35 | def cop_curve(delta_t, source_type): 36 | delta_t = delta_t.clip(lower=15) 37 | return sum(cop_parameters.loc[i, source_type] * delta_t ** i for i in range(3)) 38 | 39 | source_types = source.columns.get_level_values('source').unique() 40 | sink_types = sink.columns.get_level_values('sink').unique() 41 | 42 | return pd.concat( 43 | [pd.concat( 44 | [cop_curve(sink[sink_type] - source[source_type], source_type) 45 | for sink_type in sink_types], 46 | keys=sink_types, 47 | axis=1 48 | ) for source_type in source_types], 49 | keys=source_types, 50 | axis=1, 51 | names=['source', 'sink', 'country', 'latitude', 'longitude'] 52 | ).round(4).swaplevel(0, 2, axis=1) 53 | 54 | 55 | def finishing(cop, demand_space, demand_water, correction=.85): 56 | 57 | # Localize Timestamps (including daylight saving time correction) and convert to UTC 58 | countries = cop.columns.get_level_values('country').unique() 59 | sinks = cop.columns.get_level_values('sink').unique() 60 | cop = pd.concat( 61 | [pd.concat( 62 | [pd.concat( 63 | [localize(cop[country][sink], country).tz_convert('utc') for sink in sinks], 64 | keys=sinks, axis=1 65 | )], keys=[country], axis=1 66 | ).swaplevel(0, 2, axis=1) for country in countries], 67 | axis=1, names=['source', 'sink', 'country', 'latitude', 'longitude'] 68 | ).sort_index(axis=1) 69 | 70 | 71 | 72 | # Prepare demand values 73 | demand_space = demand_space.loc[:, demand_space.columns.get_level_values('unit') == 'MW/TWh'] 74 | demand_space = group_df_by_multiple_column_levels(demand_space, ['country', 'latitude', 'longitude']) 75 | 76 | demand_water = demand_water.loc[:, demand_water.columns.get_level_values('unit') == 'MW/TWh'] 77 | demand_water = group_df_by_multiple_column_levels(demand_water, ['country', 'latitude', 'longitude']) 78 | 79 | # Spatial aggregation 80 | sources = cop.columns.get_level_values('source').unique() 81 | sinks = cop.columns.get_level_values('sink').unique() 82 | power = pd.concat( 83 | [pd.concat( 84 | [(demand_water / cop[source][sink]).groupby(level=0, axis=1).sum() 85 | if sink == 'water' else 86 | (demand_space / cop[source][sink]).groupby(level=0, axis=1).sum() 87 | for sink in sinks], 88 | keys=sinks, axis=1 89 | ) for source in sources], 90 | keys=sources, axis=1, names=['source', 'sink', 'country'] 91 | ) 92 | heat = pd.concat( 93 | [pd.concat( 94 | [demand_water.groupby(level=0, axis=1).sum() 95 | if sink == 'water' else 96 | demand_space.groupby(level=0, axis=1).sum() 97 | for sink in sinks], ## 98 | keys=sinks, axis=1 99 | ) for source in sources], 100 | keys=sources, axis=1, names=['source', 'sink', 'country'] 101 | ) 102 | cop = heat / power 103 | 104 | # Correction and round 105 | cop = (cop * correction).round(2) 106 | 107 | # Fill NA at the end and the beginning of the dataset arising from different local times 108 | cop = cop.fillna(method='bfill').fillna(method='ffill') 109 | 110 | # Rename columns 111 | cop.columns = cop.columns.set_levels(['ASHP', 'GSHP', 'WSHP'], level=0) 112 | cop.columns = cop.columns.set_levels(['floor', 'radiator', 'water'], level=1) 113 | cop.columns = pd.MultiIndex.from_tuples([('_'.join([level for level in col_name[0:2]]), col_name[2]) for col_name in cop.columns.values]) 114 | cop = pd.concat([cop], keys=['COP'], axis=1) 115 | cop = pd.concat([cop], keys=['coefficient'], axis=1) 116 | cop = cop.swaplevel(i=0, j=3, axis=1) 117 | cop = cop.sort_index(level=0, axis=1) 118 | cop.columns.names = ['country', 'variable', 'attribute', 'unit'] 119 | 120 | return cop 121 | 122 | 123 | def validation(cop, heat, output_path, corrected): 124 | 125 | def averages(df): 126 | return pd.concat( 127 | [df.loc[(df.index >= pd.Timestamp(year=2011, month=7, day=1, tz='utc')) 128 | & (df.index < pd.Timestamp(year=2012, month=7, day=1, tz='utc')), ].sum(), 129 | df.loc[(df.index >= pd.Timestamp(year=2012, month=7, day=1, tz='utc')) 130 | & (df.index < pd.Timestamp(year=2013, month=7, day=1, tz='utc')), ].sum()], 131 | keys=['2011/2012', '2012/2013'], axis=1 132 | ) 133 | 134 | # Data preparation 135 | cop = cop['DE']['COP'] 136 | cop.columns = cop.columns.droplevel(1) 137 | 138 | heat = heat['DE']['heat_profile'] 139 | heat.columns = heat.columns.droplevel(1) 140 | 141 | # Power calculation 142 | power = pd.DataFrame() 143 | for heat_pump in ['ASHP', 'GSHP', 'WSHP']: 144 | power[heat_pump] = ( 145 | .8 * .85 * heat['space_SFH'] / cop['{}_{}'.format(heat_pump, 'floor')] + 146 | .8 * .15 * heat['space_SFH'] / cop['{}_{}'.format(heat_pump, 'radiator')] + 147 | .2 * heat['water_SFH'] / cop['{}_water'.format(heat_pump)] 148 | ) 149 | heat = pd.concat([.8 * heat['space_SFH'] + .2 * heat['water_SFH']]*3, axis=1, keys=power.columns) 150 | 151 | # Monthly aggregation 152 | heat = averages(heat) 153 | power = averages(power) 154 | 155 | cop = heat/power 156 | 157 | cop.round(2).to_csv(os.path.join(output_path, 'cop_{}.csv'.format(corrected)), sep=';', decimal=',') 158 | -------------------------------------------------------------------------------- /scripts/demand.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | import pandas as pd 4 | 5 | from scripts.misc import localize, upsample_df, group_df_by_multiple_column_levels 6 | 7 | 8 | def reference_temperature(temperature): 9 | 10 | # Daily average 11 | daily_average = temperature.groupby(pd.Grouper(freq='D')).mean().copy() 12 | 13 | # Weighted mean 14 | return sum([.5 ** i * daily_average.shift(i).fillna(method='bfill') for i in range(4)]) / \ 15 | sum([.5 ** i for i in range(4)]) 16 | 17 | 18 | def adjust_temperature(temperature, heating_thresholds): 19 | 20 | # Difference as compared to Germany 21 | diff = heating_thresholds - heating_thresholds['DE'] 22 | 23 | # Shift reference temperature by this difference 24 | adjusted = temperature.copy() 25 | 26 | for country in temperature.columns.get_level_values(0).unique(): 27 | adjusted[country] = temperature[country] - diff[country] 28 | 29 | return adjusted 30 | 31 | 32 | def daily_heat(temperature, wind, all_parameters): 33 | 34 | # BDEW et al. 2015 describes the function for the daily heat demand 35 | # This is implemented in the following and passed to the general daily function 36 | 37 | def heat_function(t, parameters): 38 | 39 | celsius = t - 273.15 # The temperature input is in Kelvin 40 | 41 | sigmoid = parameters['A'] / ( 42 | 1 + (parameters['B'] / (celsius - 40)) ** parameters['C'] 43 | ) + parameters['D'] 44 | 45 | linear = pd.DataFrame( 46 | [parameters['m_{}'.format(i)] * celsius + parameters['b_{}'.format(i)] for i in ['s', 'w']] 47 | ).max() 48 | 49 | return sigmoid + linear 50 | 51 | return daily(temperature, wind, all_parameters, heat_function) 52 | 53 | 54 | def daily_water(temperature, wind, all_parameters): 55 | 56 | # A function for the daily water heating demand is derived from BDEW et al. 2015 57 | # This is implemented in the following and passed to the general daily function 58 | 59 | def water_function(t, parameters): 60 | 61 | celsius = t - 273.15 # The temperature input is in Kelvin 62 | 63 | # Below 15 °C, the water heating demand is not defined and assumed to stay constant 64 | celsius.clip(15, inplace=True) 65 | 66 | return parameters['m_w'] * celsius + parameters['b_w'] + parameters['D'] 67 | 68 | return daily(temperature, wind, all_parameters, water_function) 69 | 70 | 71 | def daily(temperature, wind, all_parameters, func): 72 | 73 | # All locations are separated by the average wind speed with the threshold 4.4 m/s 74 | windy_locations = { 75 | 'normal': wind[wind <= 4.4].index, 76 | 'windy': wind[wind > 4.4].index 77 | } 78 | 79 | buildings = ['SFH', 'MFH', 'COM'] 80 | 81 | return pd.concat( 82 | [pd.concat( 83 | [temperature[locations].apply(func, parameters=all_parameters[(building, windiness)]) 84 | for windiness, locations in windy_locations.items()], 85 | axis=1 86 | ) for building in buildings], 87 | keys=buildings, names=['building', 'country', 'latitude', 'longitude'], axis=1 88 | ) 89 | 90 | 91 | def hourly_heat(daily_df, temperature, parameters): 92 | 93 | # According to BGW 2006, temperature classes are derived from the temperature data 94 | # This is re-sampled to a 60-min-resolution and passed to the general hourly function 95 | 96 | classes = upsample_df( 97 | (np.ceil(((temperature - 273.15) / 5).astype('float64')) * 5).clip(lower=-15, upper=30), 98 | '60min' 99 | ).astype(int).astype(str) 100 | 101 | return hourly(daily_df, classes, parameters) 102 | 103 | 104 | def hourly_water(daily_df, temperature, parameters): 105 | 106 | # For water heating, the highest temperature classes '30' is chosen 107 | # This is re-sampled to a 60-min-resolution and passed to the general hourly function 108 | 109 | classes = upsample_df( 110 | pd.DataFrame(30, index=temperature.index, columns=temperature.columns), 111 | '60min' 112 | ).astype(int).astype(str) 113 | 114 | return hourly(daily_df, classes, parameters) 115 | 116 | 117 | def hourly(daily_df, classes, parameters): 118 | 119 | def hourly_factors(building, country_classes): 120 | 121 | # This function selects hourly factors from BGW 2006 by time and temperature class 122 | slp = pd.DataFrame(index=country_classes.index, columns=country_classes.columns) 123 | 124 | # Time includes the hour of the day 125 | times = country_classes.index.map(lambda x: x.strftime('%H:%M')) 126 | 127 | # For commercial buildings, time additionally includes the weekday 128 | if building == 'COM': 129 | weekdays = country_classes.index.map(lambda x: int(x.strftime('%w'))) 130 | times = list(zip(weekdays, times)) 131 | 132 | for column in country_classes.columns: 133 | slp[column] = parameters[building].lookup(times, country_classes.loc[:, column]) 134 | 135 | return slp 136 | 137 | country_results = {} 138 | # Upsample daily_df to 60 minutes 139 | upsampled = upsample_df(daily_df, '60min') 140 | 141 | countries = daily_df.columns.get_level_values('country').unique() 142 | buildings = daily_df.columns.get_level_values('building').unique() 143 | 144 | 145 | for country in countries: 146 | print(country) 147 | country_results[country] = pd.concat( 148 | [upsampled[building][country] * hourly_factors(building, classes[country]) 149 | for building in buildings], 150 | keys=buildings, names=['building', 'latitude', 'longitude'], axis=1 151 | ) 152 | 153 | results = pd.concat( 154 | country_results.values(), keys=countries, names=['country', 'building', 'latitude', 'longitude'], axis=1 155 | ) 156 | 157 | return results 158 | 159 | 160 | def finishing(df, mapped_population, building_database): 161 | 162 | # Single- and multi-family houses are aggregated assuming a ratio of 70:30 163 | # Transforming to heat demand assuming an average conversion efficiency of 0.9 164 | building_database = { 165 | 'SFH': .7 * building_database['Residential'], 166 | 'MFH': .3 * building_database['Residential'], 167 | 'COM': building_database['Tertiary'] 168 | } 169 | 170 | results = [] 171 | for country, population in mapped_population.items(): 172 | 173 | # Localize Timestamps (including daylight saving time correction) 174 | df_country = localize(df[country], country) 175 | 176 | normalized = [] 177 | absolute = [] 178 | for building_type, building_data in building_database.items(): 179 | 180 | # Weighting 181 | df_cb = df_country[building_type] * population 182 | 183 | # Scaling to 1 TWh/a 184 | years = df_cb.index.year.unique() 185 | factor = 1000000 / df_cb.sum().sum() * len(years) 186 | normalized.append(df_cb.multiply(factor)) 187 | 188 | # Scaling to building database 189 | if country not in ['CH', 'NO']: 190 | database_years = building_data.columns 191 | factors = pd.Series([ 192 | building_data.loc[country, str(year)] * 1000000 / df_cb.loc[df_cb.index.year == year, ].sum().sum() 193 | if str(year) in database_years else float('nan') 194 | for year in years 195 | ], index=years) 196 | absolute.append(df_cb.multiply( 197 | pd.Series(factors.loc[df_cb.index.year].values, index=df_cb.index), axis=0, fill_value=None 198 | )) 199 | 200 | 201 | if country not in ['CH', 'NO']: 202 | country_results = pd.concat( 203 | [pd.concat(x, axis=1, keys=building_database.keys()) for x in [normalized, absolute]], 204 | axis=1, keys=['MW/TWh', 'MW'] 205 | ).apply(pd.to_numeric, downcast='float') 206 | else: 207 | country_results = pd.concat( 208 | [pd.concat(x, axis=1, keys=building_database.keys()) for x in [normalized]], 209 | axis=1, keys=['MW/TWh'] 210 | ).apply(pd.to_numeric, downcast='float') 211 | 212 | # Change index to UCT 213 | results.append(country_results.tz_convert('utc')) 214 | 215 | return pd.concat(results, keys=mapped_population.keys(), axis=1, 216 | names=['country', 'unit', 'building_type', 'latitude', 'longitude']) 217 | 218 | 219 | def combine(space, water): 220 | 221 | # Spatial aggregation 222 | space = group_df_by_multiple_column_levels(space, ['country', 'unit', 'building_type']) 223 | water = group_df_by_multiple_column_levels(water, ['country', 'unit', 'building_type']) 224 | 225 | # Merge space and water 226 | df = pd.concat([space, water], axis=1, keys=['space', 'water'], 227 | names=['attribute', 'country', 'unit', 'building_type']) 228 | 229 | # Aggregation of building types for absolute values 230 | dfx = df.loc[:, df.columns.get_level_values('unit') == 'MW'] 231 | dfx = dfx.groupby(dfx.columns.droplevel('building_type'), axis=1).sum() 232 | dfx.columns = pd.MultiIndex.from_tuples(dfx.columns) 233 | dfx = pd.concat([dfx['space'], dfx['water'], dfx['space'] + dfx['water']], axis=1, 234 | keys=['space', 'water', 'total'], names=['attribute', 'country', 'unit']) 235 | 236 | # Rename columns 237 | df.columns = pd.MultiIndex.from_tuples( 238 | [('_'.join([level for level in [col_name[0], col_name[3]]]), col_name[1], col_name[2]) 239 | for col_name in df.columns.values] 240 | ) 241 | 242 | # Combine building-specific and aggregated time series, round, restore nan 243 | df = pd.concat([dfx, df], axis=1).round() 244 | df.replace(0, float('nan'), inplace=True) 245 | 246 | # Fill NA at the end and the beginning of the dataset arising from different local times 247 | df_short = df.loc[:, df.columns.get_level_values('unit') == 'MW'].copy().dropna(how='all') 248 | df = df.fillna(method='bfill').fillna(method='ffill') 249 | df[df_short.columns] = df_short.fillna(method='bfill').fillna(method='ffill') 250 | 251 | # Swap MultiIndex 252 | df = pd.concat([ 253 | df.loc[:, df.columns.get_level_values('unit') == 'MW'], 254 | df.loc[:, df.columns.get_level_values('unit') == 'MW/TWh'] 255 | ], axis=1, keys=['heat_demand', 'heat_profile']) 256 | df = df.swaplevel(i=0, j=2, axis=1) 257 | df = df.swaplevel(i=1, j=2, axis=1) 258 | df = df.sort_index(level=0, axis=1) 259 | df.columns.names = ['country', 'variable', 'attribute', 'unit'] 260 | 261 | return df 262 | -------------------------------------------------------------------------------- /scripts/metadata.py: -------------------------------------------------------------------------------- 1 | 2 | import json 3 | import yaml 4 | import os 5 | import hashlib 6 | import shutil 7 | 8 | 9 | # First YAML are defined, which are then parsed and stitched together in the function below 10 | 11 | 12 | metadata_head = head = ''' 13 | hide: true 14 | name: when2heat 15 | external: true 16 | id: https://doi.org/10.25832/when2heat/{version} 17 | profile: tabular-data-package 18 | licenses: 19 | - name: cc-by-4.0 20 | title: Creative Commons Attribution 4.0 21 | path: https://creativecommons.org/licenses/by/4.0/ 22 | attribution: 23 | "Attribution should be given as follows:
| \n", 138 | " | \n", 139 | " | \n", 140 | " | \n", 141 | " |
|---|---|---|---|
| country | \n", 144 | "variable | \n", 145 | "attribute | \n", 146 | "description | \n", 147 | "
| ISO-2 digit country code | \n", 152 | "heat_demand | \n", 153 | "total | \n", 154 | "Heat demand for space and water heating | \n", 155 | "
| space | \n", 158 | "Heat demand for space heating | \n", 159 | "||
| water | \n", 162 | "Heat demand for water heating | \n", 163 | "||
| space_SFH | \n", 166 | "Heat demand for space heating in single-family houses | \n", 167 | "||
| space_MFH | \n", 170 | "Heat demand for space heating in multi-family houses | \n", 171 | "||
| space_COM | \n", 174 | "Heat demand for space heating in commercial buildings | \n", 175 | "||
| water_SFH | \n", 178 | "Heat demand for water heating in single-family houses | \n", 179 | "||
| water_MFH | \n", 182 | "Heat demand for water heating in multi-family houses | \n", 183 | "||
| water_COM | \n", 186 | "Heat demand for water heating in commercial buildings | \n", 187 | "||
| heat_profile | \n", 190 | "space_SFH | \n", 191 | "Normalized heat demand for space heating in single-family houses | \n", 192 | "|
| space_MFH | \n", 195 | "Normalized heat demand for space heating in multi-family houses | \n", 196 | "||
| space_COM | \n", 199 | "Normalized heat demand for space heating in commercial buildings | \n", 200 | "||
| water_SFH | \n", 203 | "Normalized heat demand for water heating in single-family houses | \n", 204 | "||
| water_MFH | \n", 207 | "Normalized heat demand for water heating in multi-family houses | \n", 208 | "||
| water_COM | \n", 211 | "Normalized heat demand for water heating in commercial buildings | \n", 212 | "||
| COP | \n", 215 | "ASHP_floor | \n", 216 | "COP of air-source heat pumps with floor heating | \n", 217 | "|
| ASHP_radiator | \n", 220 | "COP of air-source heat pumps with radiator heating | \n", 221 | "||
| ASHP_water | \n", 224 | "COP of air-source heat pumps with water heating | \n", 225 | "||
| GSHP_floor | \n", 228 | "COP of ground-source heat pumps with floor heating | \n", 229 | "||
| GSHP_radiator | \n", 232 | "COP of ground-source heat pumps with radiator heating | \n", 233 | "||
| GSHP_water | \n", 236 | "COP of ground-source heat pumps with water heating | \n", 237 | "||
| WSHP_floor | \n", 240 | "COP of groundwater-source heat pumps with floor heating | \n", 241 | "||
| WSHP_radiator | \n", 244 | "COP of groundwater-source heat pumps with radiator heating | \n", 245 | "||
| WSHP_water | \n", 248 | "COP of groundwater-source heat pumps with water heating | \n", 249 | "