Degree Days & Climate Correction#

This example shows how to

  1. compute Swiss heating degree days HGT 20/12 and accumulated temperature differences ATD from an outdoor temperature series,

  2. climate-correct a measured heating consumption with a reference climatology,

  3. fit a fixed-base degree-day regression (energy vs. outside temperature) and

  4. apply the GT&S method of the BFE (degree days and solar radiation).

All plots are interactive — hover to see exact values.

# Configure Plotly to render as HTML (required for Jupyter Book)
import plotly.io as pio
pio.renderers.default = "notebook_connected"

Heating degree days from hourly data#

The bundled sample contains hourly outdoor temperatures of Zürich-Kloten for 2023. heating_degree_days resamples to daily means, counts a day as heating day when the daily mean is at most 12 °C and sums 20 °C − T_mean over those days (SIA 381/3 convention).

import pandas as pd
from importlib import resources

from pyedautils.degree_days import heating_degree_days

data_path = resources.files("pyedautils") / "data" / "kloten_weather_sample.csv"
weather = pd.read_csv(data_path, parse_dates=["timestamp"])
temp = weather[["timestamp", "temperature"]]

hgt_monthly = heating_degree_days(temp, freq="MS")
hgt_monthly.round(1)
hgt ht n_days n_valid completeness t_mean
period
2023-01-01 00:00:00+00:00 548.5 31.0 31 31 1.0 2.3
2023-02-01 00:00:00+00:00 495.0 28.0 28 28 1.0 2.3
2023-03-01 00:00:00+00:00 427.1 31.0 31 31 1.0 6.2
2023-04-01 00:00:00+00:00 339.0 26.0 30 30 1.0 7.8
2023-05-01 00:00:00+00:00 118.2 13.0 31 31 1.0 13.5
2023-06-01 00:00:00+00:00 0.0 0.0 30 30 1.0 19.9
2023-07-01 00:00:00+00:00 0.0 0.0 31 31 1.0 19.8
2023-08-01 00:00:00+00:00 0.0 0.0 31 31 1.0 19.6
2023-09-01 00:00:00+00:00 16.4 2.0 30 30 1.0 17.7
2023-10-01 00:00:00+00:00 150.7 15.0 31 31 1.0 12.4
2023-11-01 00:00:00+00:00 428.3 29.0 30 30 1.0 5.5
2023-12-01 00:00:00+00:00 528.0 31.0 31 31 1.0 3.0

n_days are the calendar days of each month, n_valid the days with data, completeness their ratio. Use freq="YS" for calendar years or freq="season" for heating seasons (July–June).

heating_degree_days(temp, freq="YS").round(1)
hgt ht n_days n_valid completeness t_mean
period
2023-01-01 00:00:00+00:00 3051.2 206.0 365 365 1.0 10.9

Plot against a reference#

A reference climatology is usually built from a long series (see reference_degree_days and the open-data downloaders). Here we use illustrative monthly reference values for the Swiss Plateau.

from pyedautils.plots import plot_degree_days

# Illustrative HGT 20/12 climatology [Kd] per calendar month (index 1..12)
ref_monthly = pd.Series([560, 470, 390, 240, 90, 15, 2, 3, 70, 230, 420, 530],
                        index=range(1, 13))

fig = plot_degree_days(hgt_monthly, reference=ref_monthly,
                       title="Heating Degree Days 20/12 — Kloten 2023")
fig.show()

Accumulated temperature difference (SIA 380)#

SIA 380:2015 recommends the ATD with a single base temperature, the heating limit of the building. The default is 12 °C; a building-specific value can come from the energy signature (compute_pes(...).tb).

from pyedautils.degree_days import accumulated_temperature_difference

atd_monthly = accumulated_temperature_difference(temp, base=12.0)
atd_monthly[["atd", "ht"]].round(1).T
period 2023-01-01 00:00:00+00:00 2023-02-01 00:00:00+00:00 2023-03-01 00:00:00+00:00 2023-04-01 00:00:00+00:00 2023-05-01 00:00:00+00:00 2023-06-01 00:00:00+00:00 2023-07-01 00:00:00+00:00 2023-08-01 00:00:00+00:00 2023-09-01 00:00:00+00:00 2023-10-01 00:00:00+00:00 2023-11-01 00:00:00+00:00 2023-12-01 00:00:00+00:00
atd 300.5 271.0 179.1 131.0 14.2 0.0 0.0 0.0 0.4 30.7 196.3 280.0
ht 31.0 28.0 31.0 26.0 13.0 0.0 0.0 0.0 2.0 15.0 29.0 31.0

Climate correction of a measured consumption#

The classic correction scales only the weather-dependent share of the consumption with DD_ref / DD_actual; domestic hot water stays untouched.

from pyedautils.degree_days import climate_correction_factor, normalize_consumption

hgt_2023 = hgt_monthly["hgt"].sum()
hgt_ref = ref_monthly.sum()
factor = climate_correction_factor(hgt_2023, hgt_ref)
print(f"HGT 2023: {hgt_2023:.0f} Kd, reference: {hgt_ref:.0f} Kd, factor: {factor:.3f}")

# Measured 2023 consumption of 120 MWh with 75 % space heating share
e_norm = normalize_consumption(120_000, hgt_2023, hgt_ref, weather_dependent_share=0.75)
print(f"Climate-corrected consumption: {e_norm:,.0f} kWh")
HGT 2023: 3051 Kd, reference: 3020 Kd, factor: 0.990
Climate-corrected consumption: 119,081 kWh

Monthly factors follow from a monthly series and the climatology (matched by calendar month):

climate_correction_factor(hgt_monthly["hgt"], ref_monthly).round(2)
3 period(s) with zero degree days; factor set to NaN
period
2023-01-01 00:00:00+00:00    1.02
2023-02-01 00:00:00+00:00    0.95
2023-03-01 00:00:00+00:00    0.91
2023-04-01 00:00:00+00:00    0.71
2023-05-01 00:00:00+00:00    0.76
2023-06-01 00:00:00+00:00     NaN
2023-07-01 00:00:00+00:00     NaN
2023-08-01 00:00:00+00:00     NaN
2023-09-01 00:00:00+00:00    4.26
2023-10-01 00:00:00+00:00    1.53
2023-11-01 00:00:00+00:00    0.98
2023-12-01 00:00:00+00:00    1.00
Name: factor, dtype: float64

Degree-day regression with a fixed base temperature#

fit_degree_day_line fits E = a + b · HDD(base) and reports the ASHRAE Guideline 14 metrics CV(RMSE) and NMBE. The base temperature is not searched; use the balance temperature of the energy signature or a known heating limit.

from pyedautils.degree_days import fit_degree_day_line
from pyedautils.plots import plot_degree_day_regression

sig_path = resources.files("pyedautils") / "data" / "bldg_engy_sig_simple.csv"
sig = pd.read_csv(sig_path, sep=";", parse_dates=["timestamp"])
energy = sig[["timestamp", "value"]]
temperature = sig[["timestamp", "temperature"]]

line = fit_degree_day_line(energy, temperature, base=15.0)
print(f"intercept = {line.intercept:.1f} kWh/d, slope = {line.slope:.2f} kWh/Kd, "
      f"R² = {line.r2:.3f}, CV(RMSE) = {line.cvrmse:.1f} %, NMBE = {line.nmbe:.2f} %")

fig = plot_degree_day_regression(energy, temperature, base=15.0)
fig.show()
intercept = 6.4 kWh/d, slope = 10.43 kWh/Kd, R² = 0.785, CV(RMSE) = 38.5 %, NMBE = 0.00 %

GT&S method (BFE / Prognos)#

The degree-day and radiation method weights monthly deviations of degree days (base 20 °C, no heating limit) and global radiation with functional elasticities and the normal-year load curve. The documented parameters for multi-family houses in 2010 reproduce the published annual factor of 1.063 for the Swiss weather of 2010.

from pyedautils.degree_days import gts_correction_factor, GTS_MFH_2010_EXAMPLE, GTS_CH_2010_WEATHER

ex = GTS_MFH_2010_EXAMPLE
res = gts_correction_factor(
    GTS_CH_2010_WEATHER["gt"], ex["gt_norm"],
    GTS_CH_2010_WEATHER["rad"], ex["rad_norm"],
    ex["load_share_norm"], ex["elasticity_gt"], ex["elasticity_rad"],
)
print(f"ratio = {res.ratio:.3f} (divide the consumption), factor = {res.factor:.3f} (multiply)")
res.monthly.round(3)
ratio = 1.065 (divide the consumption), factor = 0.939 (multiply)
share d_gt d_rad e_gt e_rad effect contribution
month
1 0.196 0.104 -0.018 1.221 -0.110 0.129 0.221
2 0.153 0.039 0.051 1.302 -0.190 0.041 0.159
3 0.118 0.053 0.084 1.437 -0.307 0.051 0.124
4 0.080 -0.144 0.262 1.593 -0.427 -0.341 0.053
5 0.023 0.215 -0.149 3.295 -1.538 0.936 0.045
6 0.002 -0.198 0.014 3.360 -2.744 -0.705 0.001
7 0.001 -0.400 0.130 1.806 -2.275 -1.017 -0.000
8 0.001 0.304 -0.059 7.237 -3.038 2.382 0.003
9 0.022 0.113 0.172 2.977 -1.179 0.133 0.025
10 0.074 0.115 0.062 1.555 -0.339 0.157 0.086
11 0.146 -0.058 0.120 1.247 -0.120 -0.086 0.133
12 0.185 0.142 0.105 1.204 -0.080 0.163 0.215