H₀ (null): The rate of change of the difference signal (ADC2 − reference) does not change at the shadow event — any observed trend differences are explained by sensor noise and environmental drift alone.
H₁ (alternate): The rate of change differs after shadow onset — ADC2 progressively diverges from the moonlit reference channels (ADC0, ADC1) in a sustained, accumulating way.
Analysis approach: Due to sensor thermal mass acting as a thermal sink, the moonlight effect is expected to manifest as a gradual slope change in the difference signal, not an instantaneous step. The analysis looks for a change in trend rate, with possible lag relative to the recorded shadow event time. Note: higher ADC counts = cooler; lower ADC counts = warmer.
# Distribution comparison: pre-shadow vs post-shadow deltaall_delta = pd.concat([pre.delta, post.delta])bins = np.linspace(all_delta.min(), all_delta.max(), 50)fig = make_subplots(rows=1, cols=2, subplot_titles=['Distribution of Δ counts', 'Box plot by period'])# Histogramfor df_part, color, name in [(pre, MUTED, 'Pre-shadow'), (post, ACCENT, 'Post-shadow')]: fig.add_trace(go.Histogram( x=df_part.delta, name=name, opacity=0.7, xbins=dict(start=bins[0], end=bins[-1], size=bins[1]-bins[0]), marker_color=color, ), row=1, col=1)# Box plotsfor df_part, color, name in [(pre, MUTED, 'Pre-shadow'), (post, ACCENT, 'Post-shadow')]: fig.add_trace(go.Box( y=df_part.delta, name=name, marker_color=color, boxmean='sd', showlegend=False, ), row=1, col=2)fig.update_layout(**rooster_layout( title=f'{experiment_id} — Pre vs post shadow delta distributions', barmode='overlay', height=420, xaxis=dict(title='Δ ADC counts', gridcolor=GRID, linecolor=GRID), yaxis=dict(title='count', gridcolor=GRID, linecolor=GRID), xaxis2=dict(gridcolor=GRID, linecolor=GRID), yaxis2=dict(title='Δ ADC counts', gridcolor=GRID, linecolor=GRID),))fig.show()
Code
# Hypothesis test: slope comparison (rate of change of delta, pre vs post shadow)# H₀: slope of delta is the same in both periods# H₁: slope differs after shadow onset (sustained divergence)## Autocorrelation-corrected SE: HC3 residual std scaled by sqrt(n/n_eff)def n_eff(series):"""Effective sample size correcting for lag-1 autocorrelation.""" rho = series.autocorr(lag=1) n =len(series)returnmax(int(n * (1- rho) / (1+ rho)), 2)def slope_and_corrected_se(df_seg, t0): x = (df_seg.timestamp - t0).dt.total_seconds() /60 y = df_seg.delta.values X = sm.add_constant(x.values) m = sm.OLS(y, X).fit(cov_type='HC3') rho = pd.Series(m.resid).autocorr(lag=1) ne =max(int(len(y) * (1- rho) / (1+ rho)), 2) se_c = m.bse[1] * np.sqrt(len(y) / ne)return m.params[1], se_c, ne, mt0 = df.timestamp.iloc[0]slope_pre, se_pre, neff_pre, m_pre = slope_and_corrected_se(pre.dropna(subset=['delta']), t0)slope_post, se_post, neff_post, m_post = slope_and_corrected_se(post.dropna(subset=['delta']), t0)slope_diff = slope_post - slope_prese_diff = np.sqrt(se_pre**2+ se_post**2)t_stat = slope_diff / se_diffdf_t = neff_pre + neff_post -4p_val =2* stats.t.sf(abs(t_stat), df_t)sig_label =lambda p: '*** p<0.001'if p<0.001else'** p<0.01'if p<0.01else'* p<0.05'if p<0.05else'(ns)'print('── Slope comparison test: H₀ = same rate of change pre vs post shadow ──')print()print(f' Pre-shadow slope: {slope_pre:+.4f} counts/min SE={se_pre:.4f} n={len(pre)} n_eff={neff_pre}')print(f' Post-shadow slope: {slope_post:+.4f} counts/min SE={se_post:.4f} n={len(post)} n_eff={neff_post}')print()print(f' Slope change (post − pre): {slope_diff:+.4f} counts/min')print(f' t = {t_stat:.3f} df = {df_t} p = {p_val:.4f}{sig_label(p_val)}')print()print(f' Noise floor reference: ±{NOISE_FLOOR:.1f} counts total')print()if p_val <0.05: direction ='rising'if slope_diff >0else'falling'print(f' ✓ Reject H₀ — slope change of {slope_diff:+.4f} counts/min is significant.')print(f' ADC2 delta is {direction} relative to reference at {abs(slope_diff):.4f} counts/min post-shadow.')else:print(f' ✗ Fail to reject H₀ — no significant slope change detected (p={p_val:.3f}).')print(f' The rate of divergence is not distinguishable from pre-shadow trend.')
── Slope comparison test: H₀ = same rate of change pre vs post shadow ──
Pre-shadow slope: +0.0526 counts/min SE=0.1092 n=143 n_eff=6
Post-shadow slope: -0.0166 counts/min SE=0.2033 n=3009 n_eff=2
Slope change (post − pre): -0.0692 counts/min
t = -0.300 df = 4 p = 0.7792 (ns)
Noise floor reference: ±3.0 counts total
✗ Fail to reject H₀ — no significant slope change detected (p=0.779).
The rate of divergence is not distinguishable from pre-shadow trend.