@@ -295,6 +295,38 @@ def spike_filter(series, max_val):
295295 return series .where (series <= max_val )
296296
297297
298+ def peak_wind_qc (df ):
299+ """Apply quality control to peak_wind_kph in-place and return the DataFrame.
300+
301+ Two filters are applied:
302+ 1. Reed switch bounce: peak_wind_kph / avg_wind_kph > 8 AND peak_wind_kph > 25.
303+ A ratio this extreme is physically implausible and indicates sensor artefact.
304+ Rows where avg == 0 and peak > 25 are treated as infinite ratio and flagged.
305+ 2. Ceiling filter: peak_wind_kph > 100, regardless of average speed.
306+
307+ Flagged rows have peak_wind_kph replaced with NaN. avg_wind_kph is unaffected.
308+ A boolean column 'peak_wind_flagged' is added (True = flagged).
309+
310+ Args:
311+ df: DataFrame with columns avg_wind_kph and peak_wind_kph.
312+ Returns:
313+ The same DataFrame with peak_wind_kph cleaned and peak_wind_flagged added.
314+ """
315+ import numpy as np
316+
317+ # Ratio-based bounce filter: peak > 8x average AND peak > 25 km/h.
318+ # Rows with avg == 0 and peak > 25 are treated as ratio-infinite, so flagged directly.
319+ zero_avg = df ["avg_wind_kph" ] == 0
320+ ratio = np .where (zero_avg , np .inf , df ["peak_wind_kph" ] / df ["avg_wind_kph" ].replace (0 , np .nan ))
321+ bounce_mask = (np .array (ratio ) > 8 ) & (df ["peak_wind_kph" ] > 25 )
322+ ceiling_mask = df ["peak_wind_kph" ] > 100
323+ flagged = bounce_mask | ceiling_mask
324+
325+ df ["peak_wind_flagged" ] = flagged
326+ df .loc [flagged , "peak_wind_kph" ] = np .nan
327+ return df
328+
329+
298330def compass_bin (degrees , n_points = 16 ):
299331 """Assign a compass direction label to a bearing in degrees."""
300332 dirs = COMPASS_DIRS_16 if n_points == 16 else COMPASS_DIRS_8
0 commit comments