@@ -290,7 +290,12 @@ def _plot_circadian_stacked(
290290# ─────────────────────────────────────────────────────────────
291291
292292def 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