degree_days#

Degree days and climate correction (Witterungsbereinigung) of heating energy — Swiss heating degree days HGT 20/12 (SIA 381/3), accumulated temperature differences ATD (SIA 380:2015 / ISO 15927-6), plain degree days GT and cooling degree days CDD, a reference climatology, the correction factor DD_ref / DD_actual, a fixed-base degree-day regression with ASHRAE Guideline 14 metrics, and the BFE/Prognos GT&S method (degree days and solar radiation).

See Degree Days & Climate Correction for usage examples. The plotting counterparts live in plots; Swiss reference series can be downloaded with open_data_ch and meteo_swiss.

Background#

Method

Definition

Source

HGT 20/12

Σ (20 °C − T_mean) over days with T_mean ≤ 12 °C; heating days HT

SIA 381/3, MeteoSwiss, BFE

ATD

Σ max(0, T_base − T_mean) with one base (heating limit), ideally building-specific

SIA 380:2015, SN EN ISO 15927-6

GT

Σ (20 °C − T_mean) over all days below 20 °C, no heating limit

BFE/Prognos GT&S

CDD

Σ (T_mean − 18.3 °C) over days with T_mean ≥ 18.3 °C

BFE

Correction

E_norm = E · (1 − s) + E · s · DD_ref / DD_actual, with s the weather-dependent share

SIA 380, cantonal statistics

GT&S

Σ_m share_m · (1 + e_GT,m · ΔGT_m + e_S,m · ΔS_m) with functional elasticities e = c + a · Δ

Prognos 2003/2015 for the BFE

API Reference#

Degree days and climate correction (Witterungsbereinigung) of heating energy.

Provides the Swiss degree-day conventions and the classic degree-day based climate correction of measured heating energy:

  • HGT 20/12 heating degree days per SIA 381/3 and MeteoSwiss: sum of 20 °C − T_mean over all days with T_mean <= 12 °C (heating days HT). Used by the BFE (population-weighted Swiss series since 1980) and most cantonal energy statistics.

  • ATD accumulated temperature difference per SIA 380:2015 and SN EN ISO 15927-6: sum of max(0, T_base − T_mean) with a single base (heating-limit) temperature, ideally building-specific.

  • GT plain degree days 20 °C − T_mean without heating limit (basis of the BFE/Prognos GT&S method) and CDD cooling degree days (BFE: base 18.3 °C).

  • A reference climatology (monthly means over a climate-normal period such as 1991–2020), the correction factor DD_ref / DD_actual and the normalization of a consumption whose weather-dependent share is known.

  • A fixed-base degree-day regression (E = a + b · HDD) with ASHRAE Guideline 14 fit metrics, usable with the balance temperature from pyedautils.energy_signature.compute_pes().

  • The generic GT&S correction factor (Prognos 2003/2015 for the BFE: degree days and solar radiation with monthly elasticities and a normal-year load curve).

Plotting counterparts live in pyedautils.plots.degree_days; Swiss open-data sources for reference values in pyedautils.weather.open_data_ch and pyedautils.weather.meteo_swiss.

pyedautils.degree_days.HGT_ROOM_TEMP_CH = 20.0#

Room temperature of the Swiss HGT 20/12 convention (SIA 381/3) [°C].

pyedautils.degree_days.HGT_HEATING_LIMIT_CH = 12.0#

Heating limit of the Swiss HGT 20/12 convention (SIA 381/3) [°C].

pyedautils.degree_days.CDD_BASE_BFE = 18.3#

Base temperature of the BFE cooling degree days [°C].

pyedautils.degree_days.GT_BASE = 20.0#

Base of the plain degree days (“Gradtage”) used by the GT&S method [°C].

pyedautils.degree_days.degree_days(temperature, base: float = 20.0, threshold: float | None = 12.0, mode: str = 'heating', method: str = 'daily_mean', min_coverage: float = 0.75) → DataFrame[source]#

Daily degree days of an outdoor temperature series.

Heating: a day counts as degree-day day when t_mean <= threshold (threshold=None: when t_mean < base); its degree days are base − t_mean. Cooling: a day counts when t_mean >= threshold (None: base); its degree days are t_mean − base.

Parameters:
  • temperature – Outdoor temperature [°C] as Series with DatetimeIndex, DataFrame with DatetimeIndex and one column, or positional [timestamp, value] DataFrame. Daily or finer resolution.

  • base – Reference (room) temperature [°C]. Default 20.

  • threshold – Heating limit [°C] deciding whether a day counts. Default 12 (Swiss HGT 20/12). None uses base.

  • mode – "heating" or "cooling".

  • method – "daily_mean" (degree days from the daily mean) or "hourly" (ISO 15927-6 style: mean of the positive hourly differences, i.e. degree-hours / hours; requires sub-daily data). With a threshold the day still has to qualify via its daily mean; without, every hour below base counts.

  • min_coverage – Minimum fraction of expected samples per day for sub-daily data; days below are NaN. Default 0.75.

Returns:

Indexed by day with columns t_mean [°C], dd [Kd] (0 on non-degree-day days, NaN on missing days), is_dd_day (bool) and coverage (0–1).

Return type:

pd.DataFrame

Raises:

ValueError – On invalid mode/method, base <= threshold for heating (base < threshold for cooling), coarser-than-daily input or method="hourly" with daily input.

pyedautils.degree_days.aggregate_degree_days(daily: DataFrame, freq: str | None = 'MS', scale_incomplete: bool = False, season_start_month: int = 7) → DataFrame[source]#

Aggregate daily degree days to months, years or heating seasons.

Parameters:
  • daily – Output of degree_days().

  • freq – "MS" (calendar months), "YS" (calendar years), "season" (heating seasons starting at season_start_month) or None (return daily unchanged).

  • scale_incomplete – Scale dd and dd_days of periods with missing days by n_days / n_valid (IWU rule for incomplete months). Default False.

  • season_start_month – First month of a heating season (default 7, i.e. July–June, labelled e.g. "2022/2023").

Returns:

Indexed by period start with columns dd, dd_days (number of degree-day days), n_days (calendar days of the period), n_valid (days with data), completeness (n_valid / n_days) and t_mean. "season" adds a season label column. Periods without any data have NaN dd.

Return type:

pd.DataFrame

Raises:

ValueError – On an unsupported freq (note that "M"/"Y" are rejected; use "MS"/"YS").

pyedautils.degree_days.heating_degree_days(temperature, room_temp: float = 20.0, heating_limit: float | None = 12.0, freq: str | None = 'MS', method: str = 'daily_mean', min_coverage: float = 0.75, scale_incomplete: bool = False) → DataFrame[source]#

Heating degree days HGT and heating days HT (Swiss convention HGT 20/12).

HGT = Σ (room_temp − t_mean) over all days with t_mean <= heating_limit (SIA 381/3, MeteoSwiss, BFE). heating_limit=None gives the plain degree days GT of the BFE/Prognos GT&S method (all days with t_mean < room_temp).

Parameters:
  • temperature – Outdoor temperature, see degree_days().

  • room_temp – Room temperature [°C]. Default 20.

  • heating_limit – Heating limit [°C]. Default 12. None for GT.

  • freq – Aggregation, see aggregate_degree_days() (None for daily values).

  • method – "daily_mean" or "hourly", see degree_days().

  • min_coverage – Minimum daily sample coverage for sub-daily input.

  • scale_incomplete – Scale incomplete periods, see aggregate_degree_days().

Returns:

As aggregate_degree_days() with dd renamed to hgt [Kd] and dd_days to ht [d].

Return type:

pd.DataFrame

pyedautils.degree_days.accumulated_temperature_difference(temperature, base: float = 12.0, freq: str | None = 'MS', method: str = 'daily_mean', min_coverage: float = 0.75, scale_incomplete: bool = False) → DataFrame[source]#

Accumulated temperature difference ATD per SIA 380:2015 / ISO 15927-6.

ATD = Σ max(0, base − t_mean) with a single base temperature (the heating limit). SIA 380 recommends a building-specific base; the balance temperature tb of pyedautils.energy_signature.compute_pes() can be used directly. The Stadt Zürich open data uses base = 12 °C.

Parameters:
  • temperature – Outdoor temperature, see degree_days().

  • base – Base / heating-limit temperature [°C]. Default 12.

  • freq – Aggregation, see aggregate_degree_days().

  • method – "daily_mean" or "hourly".

  • min_coverage – Minimum daily sample coverage for sub-daily input.

  • scale_incomplete – Scale incomplete periods.

Returns:

As aggregate_degree_days() with dd renamed to atd [Kd] and dd_days to ht [d].

Return type:

pd.DataFrame

pyedautils.degree_days.cooling_degree_days(temperature, base: float = 18.3, threshold: float | None = None, freq: str | None = 'MS', method: str = 'daily_mean', min_coverage: float = 0.75, scale_incomplete: bool = False) → DataFrame[source]#

Cooling degree days CDD and cooling days CD.

CDD = Σ (t_mean − base) over all days with t_mean >= threshold (BFE: base and threshold 18.3 °C).

Parameters:
  • temperature – Outdoor temperature, see degree_days().

  • base – Base temperature [°C]. Default 18.3.

  • threshold – Day-counting threshold [°C]; None uses base.

  • freq – Aggregation, see aggregate_degree_days().

  • method – "daily_mean" or "hourly".

  • min_coverage – Minimum daily sample coverage for sub-daily input.

  • scale_incomplete – Scale incomplete periods.

Returns:

As aggregate_degree_days() with dd renamed to cdd [Kd] and dd_days to cd [d].

Return type:

pd.DataFrame

class pyedautils.degree_days.ReferenceDegreeDays(monthly: pd.Series, monthly_days: pd.Series, annual: float, annual_days: float, n_years: int, period: Tuple[str, str])[source]#

Bases: NamedTuple

Monthly degree-day climatology of a reference period.

monthly#

Mean degree days per calendar month [Kd], index 1–12.

Type:

pandas.Series

monthly_days#

Mean degree-day days per calendar month [d] (NaN if the source had no day counts).

Type:

pandas.Series

annual#

Sum of monthly [Kd].

Type:

float

annual_days#

Sum of monthly_days [d].

Type:

float

n_years#

Number of years contributing data.

Type:

int

period#

(first_year, last_year) as strings.

Type:

Tuple[str, str]

monthly: Series#

Alias for field number 0

monthly_days: Series#

Alias for field number 1

annual: float#

Alias for field number 2

annual_days: float#

Alias for field number 3

n_years: int#

Alias for field number 4

period: Tuple[str, str]#

Alias for field number 5

pyedautils.degree_days.climatology_from_monthly(monthly_dd, period: Tuple[str, str] | None = ('1991', '2020'), min_completeness: float = 0.9) → ReferenceDegreeDays[source]#

Monthly degree-day climatology from a monthly time series.

Parameters:
  • monthly_dd – Monthly degree days with a DatetimeIndex: the output of aggregate_degree_days() / heating_degree_days() (uses its completeness and day-count columns) or a plain Series, e.g. download_degree_days_bfe()["hgt"].

  • period – (first_year, last_year) inclusive, e.g. the climate normal ("1991", "2020"); None uses all data.

  • min_completeness – Months with a lower completeness are ignored.

Returns:

Climatology with 12 monthly means.

Return type:

ReferenceDegreeDays

Raises:

ValueError – If no month of the period has data, or a calendar month has no valid value.

pyedautils.degree_days.reference_degree_days(temperature, period: Tuple[str, str] | None = ('1991', '2020'), base: float = 20.0, threshold: float | None = 12.0, mode: str = 'heating', method: str = 'daily_mean', min_coverage: float = 0.75, min_completeness: float = 0.9) → ReferenceDegreeDays[source]#

Degree-day climatology computed from a long outdoor temperature series.

Convenience wrapper: degree_days() → monthly aggregate_degree_days() → climatology_from_monthly().

Parameters:
  • temperature – Long outdoor temperature series (e.g. MeteoSwiss daily values via pyedautils.weather.meteo_swiss.download_data() with granularity="d").

  • period – Reference years, inclusive. Default climate normal 1991–2020.

  • base – Base temperature [°C], see degree_days().

  • threshold – Day-counting threshold [°C], see degree_days().

  • mode – "heating" or "cooling".

  • method – "daily_mean" or "hourly".

  • min_coverage – Minimum daily sample coverage for sub-daily input.

  • min_completeness – Minimum monthly completeness.

Returns:

Climatology with 12 monthly means.

Return type:

ReferenceDegreeDays

pyedautils.degree_days.climate_correction_factor(dd_actual, dd_reference)[source]#

Degree-day based climate correction factor f = DD_ref / DD_actual.

Multiply the weather-dependent consumption of a period by f to get the consumption under reference (normal-year) weather. f < 1 means the period was colder than the reference.

Parameters:
  • dd_actual – Degree days of the analysed period(s): scalar or Series with a DatetimeIndex (e.g. monthly or yearly hgt).

  • dd_reference – Reference degree days: scalar, Series indexed by calendar month 1–12 (matched via index.month), Series with the same DatetimeIndex, or a ReferenceDegreeDays (monthly for Series input, annual for scalar input).

Returns:

Correction factor(s); NaN where dd_actual is 0 (a warning is logged).

Return type:

float or pd.Series

Raises:

ValueError – On negative degree days or incompatible inputs.

pyedautils.degree_days.normalize_consumption(energy, dd_actual, dd_reference, weather_dependent_share: float = 1.0)[source]#

Climate-correct a measured consumption to reference weather.

E_norm = E · (1 − s) + E · s · f with s the weather-dependent (space-heating) share and f from climate_correction_factor(). Domestic hot water and process shares are left unchanged, as in SIA 380 and the cantonal energy statistics.

The share can be derived from the energy signature, e.g. with pes = compute_pes(data) and E_total the measured annual energy: s = 1 − (pes.p_dhw + pes.p_dhwc) · hours / E_total.

Parameters:
  • energy – Measured consumption: scalar or Series aligned with dd_actual (same DatetimeIndex).

  • dd_actual – Degree days of the same period(s).

  • dd_reference – Reference degree days, see climate_correction_factor().

  • weather_dependent_share – Share s in 0–1. Default 1 (everything is corrected).

Returns:

Climate-corrected consumption.

Return type:

float or pd.Series

Raises:

ValueError – If weather_dependent_share is outside 0–1.

class pyedautils.degree_days.DegreeDayLine(base: float, intercept: float, slope: float, r2: float, cvrmse: float, nmbe: float, n: int, freq: str, data: pd.DataFrame)[source]#

Bases: NamedTuple

Result of fit_degree_day_line().

base#

Base temperature [°C] used for the heating degree days.

Type:

float

intercept#

Weather-independent consumption per day [kWh/d].

Type:

float

slope#

Consumption per degree day [kWh/Kd].

Type:

float

r2#

Coefficient of determination.

Type:

float

cvrmse#

Coefficient of variation of the RMSE [%] (ASHRAE G14).

Type:

float

nmbe#

Normalized mean bias error [%] (ASHRAE G14).

Type:

float

n#

Number of periods used.

Type:

int

freq#

"D" or "MS".

Type:

str

data#

Per-period DataFrame with t_mean, hdd (per day), energy (per day), n_days and fitted.

Type:

pandas.DataFrame

base: float#

Alias for field number 0

intercept: float#

Alias for field number 1

slope: float#

Alias for field number 2

r2: float#

Alias for field number 3

cvrmse: float#

Alias for field number 4

nmbe: float#

Alias for field number 5

n: int#

Alias for field number 6

freq: str#

Alias for field number 7

data: DataFrame#

Alias for field number 8

pyedautils.degree_days.fit_degree_day_line(energy, temperature, base: float = 12.0, freq: str = 'D', min_coverage: float = 0.75) → DegreeDayLine[source]#

Fit E = a + b · HDD(base) with a fixed base temperature.

Daily: ordinary least squares of daily energy on daily heating degree days max(0, base − t_mean). Monthly (freq="MS"): weighted least squares of energy per day on degree days per day, weighted by the number of valid days (CalTRACK billing convention). The base temperature is not searched; pass the balance temperature from pyedautils.energy_signature.compute_pes() (pes.tb) or a known heating limit.

Parameters:
  • energy – Consumption [kWh] (sub-daily, daily or monthly) as Series or [timestamp, value] DataFrame; sub-daily values are summed per day.

  • temperature – Outdoor temperature [°C], daily or finer.

  • base – Base temperature [°C]. Default 12.

  • freq – "D" or "MS".

  • min_coverage – Minimum daily sample coverage for sub-daily temperature.

Returns:

Fit parameters, ASHRAE Guideline 14 metrics and the per-period data.

Return type:

DegreeDayLine

Raises:

ValueError – On unsupported freq, timezone mismatch or fewer than three usable periods.

class pyedautils.degree_days.GTSResult(ratio: float, factor: float, monthly: pd.DataFrame)[source]#

Bases: NamedTuple

Result of gts_correction_factor().

ratio#

Weather-weighted annual load relative to the normal year (the Prognos “Korrekturfaktor”): divide the measured consumption by it. > 1 means colder and/or less sunny than the reference.

Type:

float

factor#

1 / ratio; multiply the measured consumption by it (same convention as climate_correction_factor()).

Type:

float

monthly#

DataFrame indexed by month 1–12 with share (normal-year load share), d_gt and d_rad (relative deviations of degree days and radiation from the reference), e_gt and e_rad (resulting elasticities), effect (relative change of the monthly demand) and contribution (weather-weighted monthly share; ratio is their sum).

Type:

pandas.DataFrame

ratio: float#

Alias for field number 0

factor: float#

Alias for field number 1

monthly: DataFrame#

Alias for field number 2

pyedautils.degree_days.gts_correction_factor(gt_actual, gt_norm, rad_actual, rad_norm, load_share_norm, elasticity_gt: Mapping[int, Tuple[float, float]] | Tuple[Sequence[float], Sequence[float]], elasticity_rad: Mapping[int, Tuple[float, float]] | Tuple[Sequence[float], Sequence[float]]) → GTSResult[source]#

Annual climate correction factor of the BFE/Prognos GT&S method.

Degree-day and radiation method (“Gradtag- und Strahlungsverfahren”, Prognos 2003/2015 for the BFE): for every month the relative deviations of degree days ΔGT and solar radiation ΔS from the reference period are weighted with functional elasticities e = c + a · Δ and applied to the normal-year load share:

ratio = Σ_m share_m · (1 + e_GT,m · ΔGT_m + e_S,m · ΔS_m)

The elasticities and load shares depend on the building category and year (derived from SIA 380/1 model buildings) and are not published in full; GTS_MFH_2010_EXAMPLE holds the documented example for multi-family houses in 2010.

Parameters:
  • gt_actual – Monthly degree days GT (base 20 °C, no heating limit) of the analysed year: 12 values, Series indexed by month or by month-start timestamps, or mapping {month: value}.

  • gt_norm – Monthly reference degree days (same shape).

  • rad_actual – Monthly global radiation of the analysed year [MJ/m² or any consistent unit].

  • rad_norm – Monthly reference global radiation.

  • load_share_norm – Normal-year monthly shares of the annual space heating demand (sum 1).

  • elasticity_gt – Degree-day elasticities per month as {month: (c, a)} or (c_values, a_values).

  • elasticity_rad – Radiation elasticities per month, same format.

Returns:

ratio, factor and the monthly breakdown.

Return type:

GTSResult

Raises:

ValueError – If any input does not cover 12 months or the load shares do not sum to 1 (±1 %).

pyedautils.degree_days.GTS_MFH_2010_EXAMPLE: Dict[str, object] = {'elasticity_gt': ([1.22, 1.3, 1.43, 1.58, 3.0, 4.89, 4.89, 4.89, 2.86, 1.56, 1.25, 1.2], [0.007, 0.054, 0.136, -0.09, 1.373, 7.711, 7.711, 7.711, 1.034, -0.043, 0.047, 0.025]), 'elasticity_rad': ([-0.11, -0.19, -0.31, -0.42, -1.44, -2.8, -2.8, -2.8, -1.26, -0.34, -0.12, -0.08], [0.002, 0.008, 0.034, -0.025, 0.66, 4.044, 4.044, 4.044, 0.471, 0.01, 0.003, 0.001]), 'gt_norm': [605, 515, 452, 348, 205, 126, 65, 69, 177, 305, 469, 562], 'load_share_norm': [0.196, 0.153, 0.118, 0.08, 0.023, 0.002, 0.001, 0.001, 0.022, 0.074, 0.146, 0.185], 'rad_norm': [112, 176, 323, 423, 545, 579, 609, 526, 360, 224, 117, 86]}#

Documented GT&S parameters for the category “Wohnen Mehrfamilienhäuser”, year 2010 (Prognos 2015, “Witterungsbereinigung auf Basis von Gradtagen und Solarstrahlung”, Tabelle 2-9). Reference period 1984–2002, Swiss mean of 53 MeteoSwiss stations; radiation in MJ/m². Elasticities are (c, a) pairs of e = c + a · Δ with Δ as a fraction.

pyedautils.degree_days.GTS_CH_2010_WEATHER: Dict[str, Sequence[float]] = {'gt': [668, 535, 476, 298, 249, 101, 39, 90, 197, 340, 442, 642], 'rad': [110, 185, 350, 534, 464, 587, 688, 495, 422, 238, 131, 95]}#

Swiss mean weather of the year 2010 from the same source (Tabelle 2-9): monthly degree days GT and global radiation [MJ/m²]. Together with GTS_MFH_2010_EXAMPLE this reproduces the documented ratio 1.063.