06 — Data quality and gap filling
Learning objectives: visualize the 3/4/48/49-hour boundaries, distinguish linear and daily-profile estimates, understand optional missing data, and interpolate wind direction circularly.
Network: none. Expected runtime: under one minute. Synthetic periodic data makes the method visible without claiming to reproduce a real weather event.
[ ]:
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from smhi2epw import processing
from smhi2epw.errors import DataGapError
index = pd.date_range("2023-06-01", periods=24 * 16, freq="h", tz="UTC")
hour = np.arange(len(index))
baseline = 16 + 6 * np.sin(2 * np.pi * (hour - 7) / 24) + 0.03 * hour
base = pd.Series(baseline, index=index, name="dry_bulb")
[ ]:
def fill_example(length, start=120, required=True):
"""Insert one gap and return its frame and processing report."""
frame = pd.DataFrame({"dry_bulb": base.copy()})
frame.iloc[start : start + length, 0] = np.nan
report = processing.impute(
frame,
["dry_bulb"] if required else [],
optional=[] if required else ["dry_bulb"],
)
return frame, report
examples = {length: fill_example(length) for length in (3, 4, 48)}
pd.DataFrame(
{
length: {
"linear_hours": report.linear_filled_hours["dry_bulb"],
"diurnal_hours": report.diurnal_filled_hours["dry_bulb"],
"maximum_gap": report.max_gap_hours["dry_bulb"],
}
for length, (_, report) in examples.items()
}
).T
[ ]:
fig, axes = plt.subplots(3, 1, figsize=(10, 8), sharex=True)
window = slice(95, 195)
for ax, (length, (frame, _)) in zip(axes, examples.items()):
ax.plot(base.iloc[window].index, base.iloc[window], label="known synthetic truth")
ax.plot(
frame.iloc[window].index,
frame.dry_bulb.iloc[window],
linestyle="--",
label=f"filled {length} h",
)
ax.set_ylabel("Temperature (°C)")
ax.legend()
axes[-1].set_xlabel("UTC timestamp")
fig.tight_layout()
The temporal reconstruction ceiling
The low-level processing.impute() helper raises for a required 49-hour gap and leaves the same optional gap as NaN. The 48-hour limit is a bounded engineering policy, not a universal accuracy threshold. Automatic compile_epw() can recover unresolved required gaps with assessed same-year donors or ERA5 without extending temporal filling beyond this ceiling. This lesson isolates temporal filling; it does not demonstrate the entire automatic pipeline.
[ ]:
try:
fill_example(49)
except DataGapError as error:
print("Required field:", error)
optional_frame, optional_report = fill_example(49, required=False)
print("Optional hours still missing:", int(optional_frame.dry_bulb.isna().sum()))
[ ]:
wind = pd.DataFrame({"wind_direction": [359.0, np.nan, 1.0]})
processing.impute(wind, ["wind_direction"])
wind # midpoint is near north (0°), not south (180°)
Key takeaways and exercise
Linear filling is local and smooth; daily profiles preserve a diurnal shape. Both are estimates, so reports quantify their use.
Try it: move a 12-hour gap to the beginning of the series. Which one-sided reference is available, and what happens if you remove that entire reference day?