Skip to content

Commit d87579a

Browse files
bgeurtenclaude
andcommitted
feat(viz_temporal): plot barn-temperature beside THI by hour-of-day
Replace the THI-only daily exceedance figure with a 2-row figure: barn temperature on top, THI on bottom, sharing the hour-of-day x-axis. Months coloured consistently across panels (Wong palette: June teal, July orange, August vermillion, September pink); year encoded as line style on the all-years variant. Herd median breakpoints drawn on each panel. This makes it possible to read off the simultaneous temperature × humidity load that drives the THI integration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4c5d773 commit d87579a

1 file changed

Lines changed: 75 additions & 60 deletions

File tree

src/digimuh/viz_temporal.py

Lines changed: 75 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,12 @@ def _plot_circadian_stacked(
290290
# ─────────────────────────────────────────────────────────────
291291

292292
def plot_thi_daily_profile(out_dir: Path) -> None:
293-
"""Plot barn THI across 24h by month, with herd breakpoint line."""
293+
"""Plot barn THI and barn temperature across 24h by month.
294+
295+
Two stacked panels per year (top: barn temperature, bottom:
296+
THI), so the reader can compare the two heat-load axes
297+
side by side. Months coloured consistently across panels.
298+
"""
294299
import matplotlib.pyplot as plt
295300
setup_figure()
296301

@@ -303,82 +308,92 @@ def plot_thi_daily_profile(out_dir: Path) -> None:
303308
if df.empty:
304309
return
305310

306-
log.info(" Plotting THI daily profile …")
311+
log.info(" Plotting THI + barn-temp daily profile …")
307312

308-
herd_bp = df["herd_median_bp"].iloc[0] if "herd_median_bp" in df.columns else np.nan
309-
310-
# One plot per year
311-
years = sorted(df["year"].unique().astype(int))
313+
herd_bp_thi = (df["herd_median_bp"].iloc[0]
314+
if "herd_median_bp" in df.columns else np.nan)
315+
herd_bp_temp = (df["herd_median_temp_bp"].iloc[0]
316+
if "herd_median_temp_bp" in df.columns else np.nan)
312317

313-
for year in years:
314-
ydf = df[df["year"] == year]
315-
if ydf.empty:
316-
continue
318+
month_colours = {6: "#009E73", 7: "#E69F00", 8: "#D55E00", 9: "#CC79A7"}
319+
month_names = {6: "June", 7: "July", 8: "August", 9: "September"}
317320

318-
fig, ax = plt.subplots(figsize=(10, 6))
321+
metric_specs = [
322+
("temp_mean", "temp_q25", "temp_q75", "Barn temperature (°C)",
323+
herd_bp_temp, "Herd median barn-temp breakpoint"),
324+
("thi_mean", "thi_q25", "thi_q75", "Barn THI",
325+
herd_bp_thi, "Herd median THI breakpoint"),
326+
]
319327

320-
month_colours = {6: "#009E73", 7: "#E69F00", 8: "#D55E00", 9: "#CC79A7"}
321-
month_names = {6: "June", 7: "July", 8: "August", 9: "September"}
328+
years = sorted(df["year"].unique().astype(int))
322329

323-
for month in sorted(ydf["month"].unique().astype(int)):
324-
msub = ydf[ydf["month"] == month]
330+
def _draw_panel(ax, sub, mean_col, q25_col, q75_col, ylabel,
331+
herd_bp, bp_label, *, show_legend=True):
332+
for month in sorted(sub["month"].unique().astype(int)):
333+
msub = sub[sub["month"] == month]
325334
if msub.empty:
326335
continue
327-
328336
colour = month_colours.get(month, "#888")
329-
ax.fill_between(msub["hour"], msub["thi_q25"], msub["thi_q75"],
330-
alpha=0.1, color=colour)
331-
ax.plot(msub["hour"], msub["thi_mean"], color=colour, linewidth=2,
337+
ax.fill_between(msub["hour"], msub[q25_col], msub[q75_col],
338+
alpha=0.10, color=colour)
339+
ax.plot(msub["hour"], msub[mean_col], color=colour, linewidth=2,
332340
marker="o", markersize=3,
333-
label=f"{month_names.get(month, str(month))}")
334-
335-
# Herd median breakpoint
341+
label=month_names.get(month, str(month)))
336342
if not np.isnan(herd_bp):
337343
ax.axhline(herd_bp, color="#333", linewidth=1.5, linestyle="--",
338-
label=f"Herd median THI breakpoint ({herd_bp:.1f})")
339-
340-
# Mark milking windows
344+
label=f"{bp_label} ({herd_bp:.1f})")
341345
for start, end in [(4, 7), (16, 19)]:
342346
ax.axvspan(start, end, alpha=0.08, color="#999", zorder=0)
343-
344-
ax.set_xlabel("Hour of day")
345-
ax.set_ylabel("Barn THI")
346-
ax.set_title(f"Barn THI daily profile ({year})\n"
347-
f"(lines = mean, shading = IQR)")
347+
ax.set_ylabel(ylabel)
348348
ax.set_xticks(range(0, 24, 2))
349349
ax.set_xlim(-0.5, 23.5)
350-
ax.legend(fontsize=9)
351-
fig.tight_layout()
350+
if show_legend:
351+
ax.legend(fontsize=7, loc="best")
352+
353+
# ── Per-year: 2-row figure (barn temp on top, THI below) ──
354+
for year in years:
355+
ydf = df[df["year"] == year]
356+
if ydf.empty:
357+
continue
358+
fig, axes = plt.subplots(2, 1, figsize=(7.87, 6.5),
359+
sharex=True)
360+
for ax, (mean_c, q25_c, q75_c, ylabel,
361+
herd_bp, bp_label) in zip(axes, metric_specs):
362+
_draw_panel(ax, ydf, mean_c, q25_c, q75_c, ylabel,
363+
herd_bp, bp_label, show_legend=(ax is axes[0]))
364+
axes[-1].set_xlabel("Hour of day")
365+
fig.suptitle(f"Barn climate daily profile ({year}) "
366+
f"— lines = mean, shading = IQR",
367+
fontsize=10, y=0.995)
368+
fig.tight_layout(rect=(0, 0, 1, 0.97))
352369
save_figure(fig, f"thi_daily_profile_{year}", out_dir)
353370

354-
# All years combined
355-
fig, ax = plt.subplots(figsize=(12, 6))
356-
labels_seen = set()
357-
for ml in sorted(df["month_label"].unique()):
358-
msub = df[df["month_label"] == ml]
359-
month = int(msub["month"].iloc[0])
360-
year = int(msub["year"].iloc[0])
361-
colour = month_colours.get(month, "#888")
362-
# Vary line style by year
363-
ls = ["-", "--", ":", "-."][years.index(year) % 4]
364-
ax.plot(msub["hour"], msub["thi_mean"], color=colour, linewidth=1.5,
365-
linestyle=ls, alpha=0.7, label=ml)
366-
367-
if not np.isnan(herd_bp):
368-
ax.axhline(herd_bp, color="#333", linewidth=1.5, linestyle="--",
369-
label=f"Herd breakpoint ({herd_bp:.1f})")
370-
371-
for start, end in [(4, 7), (16, 19)]:
372-
ax.axvspan(start, end, alpha=0.08, color="#999", zorder=0)
373-
374-
ax.set_xlabel("Hour of day")
375-
ax.set_ylabel("Barn THI")
376-
ax.set_title("Barn THI daily profile — all years\n"
377-
"(when does heat stress occur?)")
378-
ax.set_xticks(range(0, 24, 2))
379-
ax.set_xlim(-0.5, 23.5)
380-
ax.legend(fontsize=7, ncol=2)
381-
fig.tight_layout()
371+
# ── All years combined: 2-row figure, line style encodes year ──
372+
fig, axes = plt.subplots(2, 1, figsize=(7.87, 6.5), sharex=True)
373+
for ax, (mean_c, q25_c, q75_c, ylabel,
374+
herd_bp, bp_label) in zip(axes, metric_specs):
375+
for ml in sorted(df["month_label"].unique()):
376+
msub = df[df["month_label"] == ml]
377+
month = int(msub["month"].iloc[0])
378+
year = int(msub["year"].iloc[0])
379+
colour = month_colours.get(month, "#888")
380+
ls = ["-", "--", ":", "-."][years.index(year) % 4]
381+
ax.plot(msub["hour"], msub[mean_c], color=colour, linewidth=1.5,
382+
linestyle=ls, alpha=0.7, label=ml)
383+
if not np.isnan(herd_bp):
384+
ax.axhline(herd_bp, color="#333", linewidth=1.5, linestyle="--",
385+
label=f"{bp_label} ({herd_bp:.1f})")
386+
for start, end in [(4, 7), (16, 19)]:
387+
ax.axvspan(start, end, alpha=0.08, color="#999", zorder=0)
388+
ax.set_ylabel(ylabel)
389+
ax.set_xticks(range(0, 24, 2))
390+
ax.set_xlim(-0.5, 23.5)
391+
axes[0].legend(fontsize=6, ncol=4, loc="best")
392+
axes[-1].set_xlabel("Hour of day")
393+
fig.suptitle("Barn climate daily profile — all years "
394+
"(when does heat stress occur?)",
395+
fontsize=10, y=0.995)
396+
fig.tight_layout(rect=(0, 0, 1, 0.97))
382397
save_figure(fig, "thi_daily_profile_all", out_dir)
383398

384399

0 commit comments

Comments
 (0)