Skip to content

Guides

Transforms & feature engineering

Raw rows preserve exactly what the station reported. Transforms are opt-in — you call them when you want derived features. The raw fields stay in the row alongside the derived ones, so a downstream consumer can recompute or override.

Raw-as-reported is a hard contract. If the SDK silently rounded temperatures or computed RH at fetch time, a backtest replayed against historical settlements would silently drift. So transforms live in a separate mostlyright.transforms (Python) / @mostlyrightmd/core/transforms (TS) namespace — explicit, traceable.

The observation schema carries both temp_c and temp_f natively; same for wind_speed_kt / wind_speed_ms and pressure_inhg / pressure_hpa. Use whichever your model expects.

For ad-hoc conversions:

from mostlyright.transforms import (
c_to_f, f_to_c,
kt_to_ms, ms_to_kt,
inhg_to_hpa, hpa_to_inhg,
m_to_ft, ft_to_m,
)
print(c_to_f(20.0)) # 68.0
print(kt_to_ms(10)) # 5.144

Magnus-Tetens formula, returns 0–100 (clamped).

from mostlyright.transforms import relative_humidity
rh = relative_humidity(temp_c=20.0, dewpoint_c=10.0)
print(rh) # 52.5
import { relativeHumidity } from "@mostlyrightmd/core/transforms";
const rh = relativeHumidity({ tempC: 20.0, dewpointC: 10.0 });

NWS-canonical formula. Returns the input temperature unchanged below 80 °F (heat index isn’t meaningful in cool conditions).

from mostlyright.transforms import heat_index_f
hi = heat_index_f(temp_f=95, rh_pct=70)
print(hi) # 124.4

NWS-canonical (2001 revision). Returns input unchanged above 50 °F or below 3 mph.

from mostlyright.transforms import wind_chill_f
wc = wind_chill_f(temp_f=20, wind_speed_mph=15)
print(wc) # 6

Auto-picks heat index OR wind chill based on temperature:

from mostlyright.transforms import feels_like_f
print(feels_like_f(temp_f=95, rh_pct=70, wind_speed_mph=0)) # 124.4 (heat index)
print(feels_like_f(temp_f=20, rh_pct=30, wind_speed_mph=15)) # 6 (wind chill)
print(feels_like_f(temp_f=65, rh_pct=50, wind_speed_mph=5)) # 65 (no transform)

Apply transforms across a DataFrame:

import pandas as pd
from mostlyright import research
from mostlyright.transforms import relative_humidity, feels_like_f, c_to_f
df = research("KNYC", "2025-01-06", "2025-01-12")
df["obs_rh"] = relative_humidity(
df["obs_mean_temp_c"], df["obs_mean_dewpoint_c"],
)
df["obs_feels_like_f"] = feels_like_f(
temp_f=df["obs_mean_f"],
rh_pct=df["obs_rh"],
wind_speed_mph=df["obs_max_wind_kt"] * 1.15078, # kt to mph
)

Transforms are NumPy-friendly — they vectorize over Series automatically.

Pure-function transforms work over single values; map across an array yourself:

import { relativeHumidity, feelsLikeF } from "@mostlyrightmd/core/transforms";
const enriched = rows.map((r) => ({
...r,
rh_pct: relativeHumidity({ tempC: r.temp_c, dewpointC: r.dewpoint_c }),
feels_like_f: feelsLikeF({
tempF: r.temp_f,
rhPct: relativeHumidity({ tempC: r.temp_c, dewpointC: r.dewpoint_c }),
windSpeedMph: (r.wind_speed_kt ?? 0) * 1.15078,
}),
}));

mostlyright.preprocessing (Python) / mostlyright.preprocessing (TypeScript, lowercase per Phase 21 21-10) wraps the common transforms behind a single ergonomic surface:

from mostlyright import preprocessing
enriched = preprocessing.attach_derived_features(rows)
# rh, feels_like_f, vapor_pressure, etc. attached per row

research() already does per-day rollups (high, low, mean, count) using LST bucketing. For custom aggregation windows:

from mostlyright.weather import obs
import pandas as pd
rows = obs("KNYC", "2025-01-06", "2025-01-12")
df = pd.DataFrame(rows)
df["observed_at"] = pd.to_datetime(df["observed_at"])
df = df.set_index("observed_at")
# 6-hour buckets
buckets = df.resample("6H").agg({
"temp_f": ["min", "max", "mean"],
"wind_speed_kt": "max",
})