Python package for downloading and processing data from the Kansas Mesonet, an environmental monitoring network operated by Kansas State University.
pip install ksmesopy
pip install "ksmesopy[app]" # Use this to also install a GUIDependencies: numpy, pandas, matplotlib. The desktop app additionally requires guile.
import ksmesopy as ms
# Download daily temperature and precipitation for one station
df = ms.request_data(
station="Manhattan",
start="2024-01-01",
end="2024-12-31",
interval="day",
variables=["TEMP2MAVG", "TEMP2MMIN", "TEMP2MMAX", "PRECIP"],
verbose=True, # print progress while downloading (default False)
)
# Optional: rename columns to snake_case
df = ms.rename_columns(df)
# TIMESTAMP → timestamp, TEMP2MAVG → tair_2m_avg, PRECIP → precip, …
# Snapshot: every station at a single time step
snap = ms.request_snapshot(
timestamp="2025-01-01", # for 5min/hour, include a time e.g. "2025-01-01 14:30"
interval="day",
variables=["TEMP2MAVG", "PRECIP"],
)
# TIMESTAMP, STATION, TEMP2MAVG, PRECIP — one row per stationRun this in your terminal
ksmesopy-appA GUI for selecting stations, date ranges, variables, and intervals, with a tabular view and time-series chart. Exports data to CSV and charts as PNG.
| Function | Returns | Description |
|---|---|---|
get_stations() |
DataFrame |
Full station metadata table |
get_stations(names_only=True) |
list[str] |
Sorted list of station names only |
get_stations_active() |
DataFrame |
Availability table: STATION, OBS_INTERVAL (s), START, END |
| Function | Returns | Description |
|---|---|---|
request_data(station, start, end, interval, variables, *, verbose, sleep) |
DataFrame |
Download data for one station. interval is "day", "hour", or "5min". Daily timestamps are corrected from the Mesonet's next-day convention. Pass verbose=True to print progress (see Progress reporting). |
request_data_multi(stations, start, end, interval, variables, *, verbose, sleep) |
dict[str, DataFrame] |
Same as above for a list of stations; returns one DataFrame per station. verbose=True adds a [n/total] station header before each station. |
request_snapshot(timestamp, interval, variables, *, verbose) |
DataFrame |
One timestamp across all stations at once (the Mesonet stn=all endpoint). interval is "day", "hour", or "5min". Returns one row per station with a STATION column. For "day", the time component is ignored and the timestamp is corrected from the next-day convention, so results line up with request_data() for the same date. |
list_variables(interval=None) |
list[dict] |
Variable catalogue filtered by interval, or all variables if None. Each entry has keys api_name, snake_name, description, intervals. |
rename_columns(df, preset="snake") |
DataFrame |
Rename API column names to snake_case (e.g. TEMP2MAVG → tair_2m_avg). Pass a dict for a custom mapping. |
| Function | Returns | Description |
|---|---|---|
calibrate_vwc(df, vwc_cols=None) |
DataFrame |
Replace firmware VWC values with the KSU site-specific calibration. Requires SOILKA*CM and SOILEC*CM columns. Works on any subset of depths. |
compute_soil_water_storage(df) |
DataFrame |
Trapezoidal soil water storage in the top 50 cm (mm). Requires all four VWC depths; adds a STORAGE_MM column. Call calibrate_vwc() first for calibrated storage. |
| Function | Returns | Description |
|---|---|---|
growing_degree_days(tmin, tmax, base=10.0, ceiling=30.0) |
ndarray |
Daily GDD. Both tmin/tmax clipped to [base, ceiling] before averaging. |
heat_index(temp, rh) |
ndarray |
NOAA/NWS apparent temperature (°C). |
wind_chill(temp, wspd) |
ndarray |
NWS wind chill (°C); valid for temp ≤ 10 °C, wind ≥ 1.3 m s⁻¹. |
temperature_humidity_index(temp, rh) |
ndarray |
THI for livestock heat stress. Thresholds (dairy): <68 none · 68–72 mild · 72–80 moderate · 80–90 severe · >90 dangerous. |
| Function | Returns | Description |
|---|---|---|
reference_et_penman_monteith(doy, lat, elev, tmin, tmax, srad, wspd, rhmin=None, rhmax=None, *, vpd=None, wind_height=2.0) |
ndarray |
FAO-56 Penman-Monteith. Supply humidity as either rhmin=+rhmax= or vpd= (at least one is required); when only vpd= is given, ea for net longwave is derived as es − vpd. srad in W m⁻², converted internally. wind_height (m) adjusts wind to 2 m via the FAO-56 log profile — pass 10.0 for the 10 m sensor. |
reference_et_hargreaves(doy, lat, tmin, tmax, *, tmean) |
ndarray |
Hargreaves–Samani. Temperature only — no humidity, radiation, or wind needed. |
Both return ETo (mm day⁻¹) as an ndarray. Use extraterrestrial_radiation(doy, lat) if you also need Ra.
| Function | Output |
|---|---|
saturation_vapor_pressure(temp) |
es (kPa) |
actual_vapor_pressure(temp, rh) |
ea (kPa) |
vapor_pressure_deficit(temp, rh) |
VPD (kPa, ≥ 0) |
slope_saturation_vapor_pressure(temp) |
Δ (kPa °C⁻¹) |
atmospheric_pressure(elev) |
P (kPa) |
psychrometric_constant(elev) |
γ (kPa °C⁻¹) |
extraterrestrial_radiation(doy, lat) |
Ra (MJ m⁻² day⁻¹) |
net_radiation(srad_mj, tmin, tmax, ea, elev, doy, lat) |
Rn (MJ m⁻² day⁻¹) |
srad_to_mj(srad, period) |
energy (MJ m⁻²) |
All functions accept scalars or NumPy arrays.
Each function draws onto a Matplotlib Axes supplied by the caller, so panels compose freely inside any figure layout. All functions accept API column names (TEMP2MAVG) or snake_case names (tair_2m_avg) interchangeably, and return the axes they drew on so they can be further customised.
import matplotlib.pyplot as plt
import ksmesopy as ms
fig, axes = plt.subplots(5, 1, sharex=True, figsize=(12, 14))
ms.plot_temperature(axes[0], df, ["TEMP2MAVG", "TEMP2MMIN", "TEMP2MMAX"])
ms.plot_precip(axes[1], df, "PRECIP")
ms.plot_humidity(axes[2], df, "RELHUM2MAVG")
ms.plot_solar_radiation(axes[3], df, ["SRAVG", "Ra"]) # Ra drawn dashed
ms.plot_vwc(axes[4], df) # auto-detects VWC columns
plt.tight_layout()
plt.savefig("meteogram.png", dpi=150)All functions accept API column names (TEMP2MAVG) or snake_case names (tair_2m_avg) interchangeably, and return the axes they drew on so they can be further customised.
| Function | Key behaviour |
|---|---|
plot_temperature(ax, df, variables, *, band, ylabel, legend) |
Shaded band when min/avg/max triplet detected (band=True); plain lines otherwise. Works for air and soil temperature. |
plot_precip(ax, df, variable, *, ylabel, color) |
Bar chart, bar width inferred from timestamp spacing. |
plot_humidity(ax, df, variables, *, ylabel, legend) |
Lines, y-axis fixed 0–100 %. |
plot_vpd(ax, df, variables, *, ylabel, legend) |
Filled area + line, y-axis starts at 0. |
plot_solar_radiation(ax, df, variables, *, ylabel, legend) |
Filled area for observed; dashed line for Ra columns (detected by name). |
plot_wind(ax, df, speed, direction, *, ylabel, legend) |
Speed as line; direction overlaid as scatter on a twin y-axis with N/E/S/W ticks. |
plot_vwc(ax, df, variables, *, ylabel, legend) |
Sequential colormap shallow→deep; auto-detects VWC columns if variables=None. |
plot_et(ax, df, variables, *, bar, ylabel, legend) |
Line by default; bar=True for daily totals. |
Intervals: D = daily only · H = hourly and daily · A = 5-min, hourly, and daily
| API name | snake_case | Description | Unit | Intervals |
|---|---|---|---|---|
TEMP2MAVG |
tair_2m_avg |
Air temperature 2 m avg | °C | A |
TEMP2MMIN |
tair_2m_min |
Air temperature 2 m min | °C | D |
TEMP2MMAX |
tair_2m_max |
Air temperature 2 m max | °C | D |
TEMP10MAVG |
tair_10m_avg |
Air temperature 10 m avg | °C | A |
TEMP10MMIN |
tair_10m_min |
Air temperature 10 m min | °C | D |
TEMP10MMAX |
tair_10m_max |
Air temperature 10 m max | °C | D |
RELHUM2MAVG |
rh_2m_avg |
Relative humidity 2 m avg | % | A |
RELHUM2MMIN |
rh_2m_min |
Relative humidity 2 m min | % | D |
RELHUM2MMAX |
rh_2m_max |
Relative humidity 2 m max | % | D |
VPDEFAVG |
vpd_avg |
Vapor pressure deficit avg | kPa | A |
PRESSUREAVG |
pressure_avg |
Atmospheric pressure avg | kPa | A |
PRECIP |
precip |
Precipitation gauge 1 | mm | A |
PRECIP2 |
precip2 |
Precipitation gauge 2 | mm | A |
SRAVG |
srad |
Solar radiation avg | W m⁻² | A |
WSPD2MAVG |
wspd_2m_avg |
Wind speed 2 m avg | m s⁻¹ | A |
WSPD2MMAX |
wspd_2m_max |
Wind speed 2 m max | m s⁻¹ | H¹ |
WDIR2M |
wdir_2m |
Wind direction 2 m | ° | A |
WDIR2MSTD |
wdir_2m_std |
Wind direction 2 m std dev | ° | A |
WSPD10MAVG |
wspd_10m_avg |
Wind speed 10 m avg | m s⁻¹ | A |
WSPD10MMAX |
wspd_10m_max |
Wind speed 10 m max | m s⁻¹ | H¹ |
WDIR10M |
wdir_10m |
Wind direction 10 m | ° | A |
WDIR10MSTD |
wdir_10m_std |
Wind direction 10 m std dev | ° | A |
¹ Available at 5-min and daily only (not hourly).
| API name | snake_case | Description | Unit | Intervals |
|---|---|---|---|---|
SOILTMP5AVG |
tsoil_5cm |
Soil temperature 5 cm avg | °C | A |
SOILTMP5MIN |
tsoil_5cm_min |
Soil temperature 5 cm min | °C | D |
SOILTMP5MAX |
tsoil_5cm_max |
Soil temperature 5 cm max | °C | D |
SOILTMP10AVG |
tsoil_10cm |
Soil temperature 10 cm avg | °C | A |
SOILTMP10MIN |
tsoil_10cm_min |
Soil temperature 10 cm min | °C | D |
SOILTMP10MAX |
tsoil_10cm_max |
Soil temperature 10 cm max | °C | D |
| API name | snake_case | Description | Unit | Intervals |
|---|---|---|---|---|
SOILTMP5AVG655 |
tsoil_5cm_655 |
Soil temperature 5 cm | °C | A |
SOILTMP10AVG655 |
tsoil_10cm_655 |
Soil temperature 10 cm | °C | A |
SOILTMP20AVG655 |
tsoil_20cm_655 |
Soil temperature 20 cm | °C | A |
SOILTMP50AVG655 |
tsoil_50cm_655 |
Soil temperature 50 cm | °C | A |
SOILKA5CM |
ka_5cm |
Dielectric constant 5 cm | — | A |
SOILKA10CM |
ka_10cm |
Dielectric constant 10 cm | — | A |
SOILKA20CM |
ka_20cm |
Dielectric constant 20 cm | — | A |
SOILKA50CM |
ka_50cm |
Dielectric constant 50 cm | — | A |
SOILEC5CM |
ec_5cm |
Electrical conductivity 5 cm | dS m⁻¹ | A |
SOILEC10CM |
ec_10cm |
Electrical conductivity 10 cm | dS m⁻¹ | A |
SOILEC20CM |
ec_20cm |
Electrical conductivity 20 cm | dS m⁻¹ | A |
SOILEC50CM |
ec_50cm |
Electrical conductivity 50 cm | dS m⁻¹ | A |
VWC5CM |
vwc_5cm |
Volumetric water content 5 cm | m³ m⁻³ | A |
VWC10CM |
vwc_10cm |
Volumetric water content 10 cm | m³ m⁻³ | A |
VWC20CM |
vwc_20cm |
Volumetric water content 20 cm | m³ m⁻³ | A |
VWC50CM |
vwc_50cm |
Volumetric water content 50 cm | m³ m⁻³ | A |
VWC note: the Mesonet API returns VWC computed by the CS655 firmware equation. Call
calibrate_vwc()to replace those values with the KSU site-specific calibration. RequiresSOILKA*CMandSOILEC*CMto be fetched alongsideVWC*CM. Works for any subset of depths independently.
Precipitation. The Mesonet operates dual tipping-bucket rain gauges at most stations. request_data() returns both (PRECIP, PRECIP2); the desktop app merges them to the row-wise maximum automatically. For scripted use, merge manually:
df["PRECIP"] = df[["PRECIP", "PRECIP2"]].max(axis=1)
df.drop(columns="PRECIP2", inplace=True)Daily timestamps. The Mesonet API stores each day's aggregated values at 00:00 of the following calendar day. request_data() corrects for this automatically; the returned TIMESTAMP always reflects the observation date.
Network snapshots. request_snapshot() fetches one time step for every station in a single call — handy for mapping a variable across Kansas on a given day or 5-minute mark. It's restricted to a single instant by design; for a time series at one station use request_data(). Daily snapshots use the same next-day correction as request_data(), so the two agree for any given date.
Progress reporting. Long requests are split into chunks of 3 000 records, so a multi-year download is several round trips to the API. Pass verbose=True to request_data(), request_data_multi(), or request_snapshot() to print one line per request as it is issued:
df = ms.request_data("Manhattan", "2013-01-01", "2024-12-31", "day",
["TEMP2MAVG", "PRECIP"], verbose=True)Manhattan: 2013-01-01 - 2021-03-20
Manhattan: 2021-03-21 - 2024-12-31
Manhattan: done — 4383 rows × 2 variables
Dates carry a time component for "hour" and "5min" intervals. Failed attempts are reported too, so a slow download that is quietly retrying is visible rather than looking hung. verbose defaults to False, keeping scripted use silent.
Progress messages are also sent to the ksmesopy logger at DEBUG level regardless of verbose, so an application can capture them instead of printing:
import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("ksmesopy").setLevel(logging.DEBUG)Missing values. The API encodes missing observations as "M". These are converted to NaN on read. Periods before a station or sensor was installed are pre-filled with NaN rather than omitted.
MIT
