heatstress
Vectorised heat-stress indices for public-health, climate-adaptation and occupational-safety analysis: NWS Heat Index, Humidex, Apparent Temperature, wet-bulb temperature, WBGT (full Liljegren et al. 2008 model, shade/indoor, ISO 7243, BoM approximation), UTCI, Wind Chill, mean radiant temperature, solar position, risk categories and heatwave detection, including the Excess Heat Factor.
- Pure numpy core; every function takes scalars, numpy arrays, pandas Series or xarray DataArrays and returns the same kind of object.
- Units are explicit: degC by default with a
units="C" | "F" | "K"switch on temperature functions, wind in m/s, pressure in hPa, radiation in W/m2, humidity in %. Converters are inheatstress.units. - Inputs outside a formula's validity range raise a
ValidityRangeWarning; NaN propagates. - Validated against published reference values and independent implementations (see Validation).
- A command-line tool for single values and CSV time series.
Disclaimer. heatstress computes screening indices from weather data. It is not medical advice and is not a substitute for on-site measurement, an occupational health programme, clinical judgement or the official warnings of your national meteorological or public-health service.
Installation
pip install heatstress # numpy only
pip install "heatstress[pandas]" # + pandas helpers (DataFrames, events_to_frame)
pip install "heatstress[xarray]" # + xarray/pandas
Python 3.9 or newer.
Quick start
import heatstress as hs
hs.heat_index(35, 60) # 45.1 degC (NWS algorithm)
hs.heat_index(95, 50, units="F") # 105.2 degF
hs.heat_index_category(45.1) # 'danger'
hs.humidex(30, dew_point=15) # 34.0
hs.apparent_temperature(30, 50, wind=2) # 31.6 degC
hs.wet_bulb_psychrometric(25, 50) # 17.9 degC
hs.utci(30, tmrt=50, wind=2, rh=50) # 34.6 degC
hs.utci_category(34.6) # 'strong heat stress'
# Outdoor WBGT from standard weather data (Liljegren et al. 2008)
z = hs.solar_zenith("2026-07-15T10:00Z", lat=38.42, lon=27.14)
w = hs.wbgt_liljegren_components(t=33, rh=55, wind=1.5, solar=850, zenith=z, pressure=1008)
w.wbgt, w.globe_temperature, w.natural_wet_bulb # 32.7, 49.7, 27.7 degC
hs.work_rest_allocation(w.wbgt, "moderate") # ACGIH TLV work-rest guidance
# Heatwaves on a daily series
import pandas as pd
tmax = pd.Series(..., index=pd.date_range("1991-01-01", periods=..., freq="D"))
thr = hs.calendar_day_percentile(tmax, tmax.index, q=90, window=15, baseline=(1991, 2020))
events = hs.heatwave_events(tmax, thr, min_duration=3)
hs.events_to_frame(events)
ehf = hs.excess_heat_factor(tmean_daily) # Nairn & Fawcett (2015)
hs.ehf_severity(ehf) # none / low-intensity / severe / extreme
Indices
| Index | Function | Inputs | Source | Validity / notes |
|---|---|---|---|---|
| Heat Index | heat_index |
T, RH | NWS/WPC algorithm: Steadman (1979) simple formula, Rothfusz (1990) regression and the two NWS adjustments | Regression error +/-1.3 degF; warning above 120 degF air temperature or 150 degF HI |
| Rothfusz regression only | rothfusz_regression |
T, RH | Rothfusz (1990) NWS SR 90-23 | For comparison only |
| Humidex | humidex |
T, RH or dew point | Masterton & Richardson (1979), Environment Canada | MSC vapour-pressure form |
| Apparent Temperature | apparent_temperature |
T, RH, wind (10 m), optional net radiation | Steadman (1994), Australian Bureau of Meteorology | Shade and radiation forms |
| Wet bulb (Stull) | wet_bulb_stull |
T, RH | Stull (2011) J. Appl. Meteor. Climatol. 50:2267 | RH 5-99 %, T -20 to 50 degC, 1013 hPa; error -1 to +0.65 K (larger in cold, dry air) |
| Wet bulb (psychrometric) | wet_bulb_psychrometric |
T, RH, pressure | WMO No. 8 (2018) Annex 4.B psychrometer equation, Newton solver | Ventilated wet bulb, wick over water |
| WBGT outdoor | wbgt_liljegren, wbgt_liljegren_components, globe_temperature_liljegren, natural_wet_bulb_liljegren |
T, RH, wind, global solar, zenith, pressure, optional direct fraction | Liljegren et al. (2008) J. Occup. Environ. Hyg. 5:645 | 50.8 mm globe; 2 m wind (other heights scaled with a power law); solar capped at 0.85 x top of atmosphere as in the reference code |
| WBGT shade / indoor | wbgt_shade |
T, RH, pressure (wind for method="liljegren") |
Simplified 0.7 Tw + 0.3 Ta, or Liljegren with no sun and 0.7 Tnwb + 0.3 Tg |
No solar load |
| WBGT from measurements | wbgt_from_measurements |
Tnwb, Tg, (Ta) | ISO 7243:2017 | Indoor and outdoor formulas |
| WBGT approximation | wbgt_bom |
T, RH | Australian Bureau of Meteorology | Approximation: assumes moderately high radiation and light wind; can be off by several degrees |
| UTCI | utci |
T, Tmrt, wind (10 m), RH | Broede et al. (2012) Int. J. Biometeorol. 56:481, 6th-order polynomial, 210 coefficients from the reference program UTCI_a002.f90 |
Ta -50 to 50 degC, Tmrt-Ta -30 to 70 K, wind 0.5-17 m/s, vapour pressure <= 5 kPa; out-of-range values NaN by default; wind below 0.5 m/s raised to 0.5 |
| Wind Chill | wind_chill |
T, wind (10 m) | NWS/MSC (2001), Osczevski & Bluestein (2005) | T <= 10 degC; MSC light-wind formula below 4.8 km/h |
| Feels like | feels_like |
T, RH, wind | NWS convention | Wind chill / heat index / air temperature |
| Mean radiant temperature | mean_radiant_temperature |
Tg, T, air speed | ISO 7726:1998 Annex B | Forced or natural convection; 150 mm globe, emissivity 0.95 by default |
| Solar position | solar_position, solar_zenith |
time, lat, lon | NOAA solar calculator (Meeus 1991) | Geometric zenith, no refraction |
| Humidity helpers | saturation_vapour_pressure, vapour_pressure, dew_point, relative_humidity_from_dew_point, relative_humidity_from_vapour_pressure |
T, RH / Td / e | Magnus (Alduchov & Eskridge 1996), WMO, Buck (1981), BoM, Hardy (1998) ITS-90 | Over liquid water |
Risk categories
| Function | Scale |
|---|---|
heat_index_category |
NWS: caution 80-89 degF, extreme caution 90-102, danger 103-124, extreme danger >= 125 |
humidex_category |
Environment Canada: < 30, 30-39, 40-45, >= 46 |
utci_category |
10 UTCI stress classes from extreme cold (< -40) to extreme heat stress (> 46) |
work_rest_allocation |
ACGIH TLV (acclimatized) / Action Limit (unacclimatized) WBGT work-rest table as printed in the OSHA Technical Manual Sec. III Ch. 4, for light / moderate / heavy / very heavy work, with a clothing adjustment value |
iso7243_reference_wbgt, iso7243_assessment |
ISO 7243:2017 Annex A reference WBGT 56.7 - 11.5 log10(M) (acclimatized) and 59.9 - 14.1 log10(M) (unacclimatized) |
ehf_severity |
Nairn & Fawcett (2015): low-intensity, severe (EHF >= EHF85), extreme (EHF >= 3 EHF85) |
Heatwave detection
percentile_thresholdandcalendar_day_percentile(centred moving window of calendar days, optional baseline years, 29 February merged with 28 February).heatwave_mask,consecutive_runsandheatwave_eventsfind spells of at leastmin_durationconsecutive days above a scalar or day-varying threshold and report start, end, duration, peak and exceedance statistics;events_to_frameconverts them to a DataFrame.excess_heat_factor:EHI_sig = T3 - T95,EHI_accl = T3 - T30prev,EHF = EHI_sig x max(1, EHI_accl), attributed to the first day of the three-day window;T95from a reference period (t95=orreference=).
Command line
$ heatstress hi 35 --rh 60
heat_index: 45.1 degC
category: danger
$ heatstress wbgt 33 --rh 55 --wind 1.5 --solar 850 --time 2026-07-15T10:00Z --lat 38.42 --lon 27.14
$ heatstress utci 30 --tmrt 55 --wind 2 --rh 50
$ heatstress windchill 0 --wind 15 --wind-units mph --units F
$ heatstress series weather.csv -o indices.csv --lat 38.42 --lon 27.14
Every value command accepts --units C|F|K and --json; wind accepts
--wind-units ms|kmh|mph|kn. heatstress series reads a CSV, recognises common
column names (time, t/temperature, rh, td, wind, solar/ghi,
pressure, tmrt, tg, zenith, lat, lon), and appends every index that
the columns allow (UTCI needs tmrt or a globe temperature; the Liljegren WBGT
needs wind, solar and either a zenith column or time plus coordinates). The same
logic is available in Python as hs.compute_indices(df).
Validation
All comparisons are in the test suite (tests/test_reference_values.py) unless noted.
| Check | Reference | Result |
|---|---|---|
| UTCI polynomial | 1000 official test conditions, Broede et al. UTCI-Test-Data (doi:10.5281/zenodo.5503968, CC BY 4.0), column UTCI_polynomial |
max deviation 0.050 degC (= rounding of the published values) |
| UTCI vs UTCI-Fiala model | same data set, 921 cases with wind <= 17 m/s | RMS 1.17 K (Broede et al. report about 1.1 K) |
| UTCI vs independent code | thermofeel 2.3.0 calculate_utci_polynomial |
max 1e-12 degC (scratch cross-check) |
| Heat index vs NWS chart | 135 cells of the NWS heat index chart | bare Rothfusz regression within 0.92 degF; full NWS algorithm within 0.92 degF where no adjustment applies and within 2.29 degF in the high-humidity cells where NWS adds its adjustment |
| Heat index vs independent code | thermofeel calculate_heat_index_adjusted, 5000 random cases T <= 43 degC |
max 0.08 degC (scratch cross-check) |
| Liljegren WBGT, globe, natural wet bulb | thermofeel 2.3.0 thermofeel.liljegren (KNMI port of the reference C code), 3000 random cases |
max 4e-13 degC; 8 frozen vectors in the tests |
| Wet bulb | Stull (2011) worked example 20 degC / 50 % -> 13.7 degC; psychrometric table 25 degC / 50 % -> 17.9 degC | exact to the published precision; Stull vs psychrometric within +/-1 K for 0-45 degC, RH 20-95 % |
| Wind chill | NWS wind chill chart (6 cells); thermofeel | within 0.6 degF; identical to thermofeel |
| Humidex | Environment Canada example 30 degC / Td 15 degC -> 34 | exact; thermofeel max 0.04 |
| Apparent temperature | thermofeel calculate_apparent_temperature |
identical (scratch cross-check) |
| Mean radiant temperature | ISO 7726 formula; thermofeel | max 0.06 degC (thermofeel uses 273.15 K in place of ISO's 273) |
| Solar position | pvlib 0.16.1 NREL SPA, 4000 random times 1990-2060 and latitudes +/-80 deg | zenith max 0.017 deg, Earth-Sun distance max 7e-5 AU; 6 frozen cases in the tests |
| Excess Heat Factor | thermofeel thermofeel.excess_heat with pandas rolling windows |
max 4e-11 |
Units and conventions
- Temperatures: degC unless
units="F"or"K"(applies to all temperature inputs and outputs of that call). - Relative humidity in %, wind speed in m/s (10 m for Apparent Temperature,
Wind Chill and UTCI; 2 m for Liljegren unless
wind_heightis given), pressure in hPa (station pressure), solar radiation in W/m2 (global horizontal). - Results come back as Python floats for scalar input, numpy arrays for array input, and pandas Series / xarray DataArrays (same index or coordinates) when the first array argument is one.
Citation
See CITATION.cff. Please also cite the original authors of each index you use.
License
MIT (c) 2026 Yusuf Eminoğlu. The UTCI test data shipped in tests/data are by
Broede et al., CC BY 4.0.
Metadata
Release files for heatstress 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| heatstress-0.1.1.tar.gz | 77.8 kB | With provenance | Details for heatstress-0.1.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| heatstress-0.1.1-py3-none-any.whl | Python 3 | none | any | Details for heatstress-0.1.1-py3-none-any.whl |
Total release size: 127.9 kB
Release files / heatstress-0.1.1.tar.gz
| Download URL | heatstress-0.1.1.tar.gz |
|---|---|
| Size | 77.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
571c98bde3bd45361aae476b4c0795a3da7deb312b0715977e322f79d9018340
|
|
BLAKE2b-256 checksum How to use checksums |
d5ea79ff13baf83429e0b2929266d1307f6c148f4c622d833d3dbbba34a7de64
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.
Transparency logRelease files / heatstress-0.1.1-py3-none-any.whl
| Download URL | heatstress-0.1.1-py3-none-any.whl |
|---|---|
| Size | 50.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1f83305361c4b5d05af7f8af27d115538b2d3d85e767fe384cc9f3cc0e263e34
|
|
BLAKE2b-256 checksum How to use checksums |
08819cbd6547e0a317291344035ff3cd4c9aa38c6067b0302b9e902e1da8b020
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.
Transparency logSecurity for heatstress 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for heatstress-0.1.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.