Synthetic 15-minute electricity load profiles for German households, labeled by building type, occupancy, and sector-coupled loads (heat pump, EV, electric water heating).
Every profile is one point in a full factorial over five dimensions, so each combination of household constellation, building type, heat pump, electric water heating and electric vehicle exists exactly once and carries the labels that produced it. The four load components stay in separate columns, so consumers can decompose a profile, drive flexibility models from a single component, or recombine them differently.
The underlying series come from synPRO (Fraunhofer ISE). This repository does the assembly: unit conversion, per-household normalisation, superposition and plausibility checking.
These are synthetic profiles, not measurements. See Known limitations before using them for anything that depends on realistic diversity between households.
pip install -e .With the test suite:
pip install -e .[test]
pytestsynload-de buildThe synPRO exports ship inside the package, so this works from any directory and there is nothing to download.
This writes output/profiles.csv (192 rows of metadata) and output/timeseries.parquet (6.7 million rows, 56 MB on disk), then prints a benchmark report.
synload-de build --realizations 3 --target-year 2023 --format csv
synload-de validate
synload-de build --helpOutput formats. --format takes parquet (the default), csv or hdf5; all three work out of the box. HDF5 is written as a queryable PyTables table under the key timeseries, zlib compressed, with profile_id indexed, so a single profile comes out of the file without reading the rest:
one = pd.read_hdf("output/timeseries.h5", "timeseries", where="profile_id == 7")Memory. The time series is assembled in memory, at roughly 32 bytes per row: about 215 MB for the default build and 650 MB with --realizations 3. Writing needs roughly twice the peak. Each build logs its projected size and warns above 750 MB, so check that line before starting a large run on a small machine. --restrict-family-houses cuts the row count by 38 %, and building in several runs with different --seed values is always an option.
Library use:
from synload_de import (
build_combinations,
build_dataset,
load_inputs,
PACKAGED_RAW_DATA,
report,
validate,
)
inputs = load_inputs(PACKAGED_RAW_DATA)
profiles, timeseries = build_dataset(inputs, combinations=build_combinations())
print(report(validate(profiles)))
# Energy per component on a winter day, in kWh
one = timeseries[timeseries["profile_id"] == 0].set_index("datetime")
components = ["base_load_kw", "hp_load_kw", "dhw_load_kw", "ev_load_kw"]
print(one.loc["2021-01-15", components].sum() * 0.25) # 0.25 h per stepprofiles.csv, one row per profile:
| Column | Meaning |
|---|---|
profile_id |
Row id, and the join key to the time series |
realization |
0 for the base case, higher for diversified copies |
constellation, n_persons |
single_person_under30, 2_fulltime_employees, 2_persons_over65, family |
building_type, building_class, single_family |
{old_building, passive}_{single_family, multi_party}_house. old_building means unrenovated 1979 to 2001 stock, not pre-war; passive is post-2002 stock brought to passive house standard |
heat_pump, electric_water_heating, electric_vehicle, electric_vehicle_type |
Consumer labels; the vehicle type is tesla, vw_eup or the explicit string none |
n_apartments, floor_area_m2 |
Apartments the source building was simulated with, and the floor area of one of them |
specific_heat_demand_kwh_m2 |
Heat demand per square metre of apartment |
annual_{base,hp,dhw,ev,total}_kwh |
Annual electricity per component |
hp_thermal_kwh, hp_spf |
Delivered heat and the resulting seasonal performance factor |
peak_load_kw |
Highest 15-minute total load |
timeseries.parquet, long format, 35 040 rows per profile:
| Column | Unit |
|---|---|
datetime |
UTC, 15-minute grid, 2021 unless --target-year is given |
profile_id |
Join key to profiles.csv |
base_load_kw |
Household appliances and lighting |
hp_load_kw |
Heat pump electricity |
dhw_load_kw |
Electric instantaneous water heater |
ev_load_kw |
Home charging |
total_load_kw |
Sum of the four components |
Resulting spread across the 192 profiles: 1 320 to 19 880 kWh per year, peak load 2.8 to 26.1 kW.
Three normalisation steps do the real work, and each corrects a mistake that is easy to make and hard to see in a plot.
Thermal to electric. Q_htg and Q_dhw in the synPRO export are thermal power, not electric. Space heating is divided by a temperature-dependent COP,
COP = 0.45 * (T_flow + 273.15) / (T_flow - T_amb), clipped to [1.5, 5.5]
with T_flow at 55 °C for unrenovated buildings (radiators) and 35 °C for passive houses (underfloor heating). A constant COP would flatten exactly the winter peak that matters for grid studies. Hot water is assumed to come from a resistive instantaneous heater, so its COP is 1.
The resulting seasonal performance factors are 2.82 for old buildings and 4.0 to 4.1 for passive houses, inside the 2.6 to 5.4 band measured across the Fraunhofer ISE heat pump field trials. --cop-mode constant replaces the curve with a flat COP, as an escape hatch rather than a recommendation.
The ambient temperature is the one synPRO used. The packaged heat/ directory contains meta_info_used_weather_data_TRY2010_6_Jahr.dat, the DWD test reference year the heat demand was simulated against — region 6, representative station Bad Marienberg, a synthetic mean year of 7.79 °C assembled from 1988 to 2007 observations. It is the only temperature source, which is the point: heat demand and COP see the same atmosphere, and the two correlate at -0.79. Substituting observed weather of some calendar year would drop that to about -0.60.
The file is found by glob, so re-exporting the profiles for a different climate region works without a code change as long as exactly one such file sits in the heat/ directory. The simulated year is read from the profiles' own date column rather than assumed, so a re-export for a different year works too — except a leap year, which is rejected with an explanation, because a reference year has only 8760 hours.
Whole building to one household. synPRO reports multi-party demand for the entire building. The multi-party files cover four apartments, so heat and hot water are divided by number of apartments read from the file header.
meta_info_used_building_data.txt documents the reference buildings the simulations were configured from, and it names a different apartment count: 6 where the profile header records 4. The header wins, and its own numbers show why. The header also states a specific heating load of 151 kWh/m², which is per apartment, so it fixes the apartment size:
| Apartments assumed | kWh per apartment | implied apartment area |
|---|---|---|
| 4 | 12 099 | 80.1 m² |
| 6 | 8 066 | 53.4 m² |
The reference apartment in that file is 74.5 m², so 4 fits and 6 does not. The hot water file agrees independently: 8 occupants across 4 apartments is 2.0 people per flat, against 1.33 at six. synload-de validate recomputes this chain on every build, so a wrong divisor shows up as a 50 % deviation instead of the 2 % tolerance.
The metadata file is read by nothing in the package; the normalisation works entirely from the profile headers.
Household total to household size. The hot water file is the total for the occupancy it was simulated with, not a per-person series. Scaling to a different household size means multiplying by n_persons / occupants_per_apartment. Multiplying by n_persons alone overstates demand by up to a factor of four.
Only the p_evse_home_w column of the vehicle files enters a household profile; those files also record charging at work and at public chargers.
Superposition is done positionally on numpy arrays with explicit length checks. Adding pandas Series would align on the index and silently produce NaN.
Coincidence factor of 1.0 by default. The raw data holds a single series per constellation, building type and vehicle, so all households of one combination are bit-for-bit identical and their peaks align perfectly. Aggregating them to a feeder or portfolio will badly overstate the peak, because real households never charge or cook in lockstep. --realizations N applies a seeded circular time shift and energy scaling to break the coherence. That is diversification of one simulation, not independent resampling of synPRO, and it does not reproduce the diversity of a real household population.
Temperature is hourly, the profiles are quarter-hourly. The reference year gives one value per hour, interpolated onto the 15-minute grid, so sub-hourly temperature swings do not reach the COP. The residual mismatch between heat demand and temperature (-0.79 rather than closer to -1.0) is mostly physics rather than data: thermal inertia, solar gains, night setback and the summer-zero floor all break the linear relation. Passive houses sit between -0.49 and -0.54 because gains dominate their demand.
Unrenovated buildings heat through the summer. The reference year is an upland climate — Bad Marienberg at roughly 550 m, 7.79 °C annual mean — where a July day averaging 11 °C is ordinary rather than exceptional. The two old_building types therefore draw space heat on 15 of the 31 July days, with about 4.7 % of their annual heat demand falling into June to August; the passive houses draw none at all. The load follows the weather rather than trickling along at a constant rate, correlating at -0.57 with the daily mean temperature. This is what the synPRO series contains, not an artefact of the assembly: no heating limit temperature is imposed, because a cut-off applied after the fact would break the consistency between heat demand and the temperature that drives the COP.
Demand only. The dataset models electricity consumption and nothing else. There is no on-site generation, no storage and no sizing attributes for either, so every load column is gross consumption at the meter. Anyone wanting net load has to bring their own generation series and subtract it.
Not representative of the building stock. The factorial weights every combination equally. It is a coverage grid, not a sample: do not read the mean over all profiles as a German average. By default any constellation may occupy any building type, because one and two person households, especially over 65, are the most common occupants of German single-family houses. --restrict-family-houses reverts to placing only four-person households there.
Relabelling the year does not change the weather. --target-year shifts the axis while preserving weekdays, so a 2023 run starts on 30 December 2022. The load still carries the reference year's weather; combining it with another year's spot prices or generation data mixes weather years.
synload_de/raw_data/ holds unmodified synPRO exports, including the two documentation files Fraunhofer ships with the heat profiles: meta_info_used_weather_data_TRY2010_6_Jahr.dat, the DWD test reference year the build reads, and meta_info_used_building_data.txt, the reference building parameters, which nothing in the package reads. They are distributed with the package so that an installation can build the dataset without a separate download.
Fraunhofer ISE states in the file headers that it accepts no liability for the correctness or completeness of the data, and that liability claims for damage caused by using it are rejected. That notice applies to everything in that directory.
Two licences apply to this repository, and they cover different things.
The code — everything under synload_de/ except raw_data/, plus the tests — is MIT licensed. See LICENSE.
The data in synload_de/raw_data/ comes from Fraunhofer ISE and is accompanied by the Community Data License Agreement – Permissive, Version 2.0, whose full text ships alongside it as CDLA-Permissive-2.0.pdf. CDLA-Permissive-2.0 allows the data to be used, modified and shared, provided the agreement text and the attribution notices travel with it — which is why the licence file is packaged together with the profiles rather than left behind.
Anyone redistributing this package or the data in it should read that agreement, and should confirm the current terms with Fraunhofer ISE rather than relying on this summary.