sofhiaazzhr Claude Opus 5.5 commited on
Commit
f28898b
·
1 Parent(s): 9c70930

[NOTICKET] fix(trend,forecast): a period with no rows stays empty (P0-8)

Browse files

- resample().agg("sum") turned a month with no rows into 0: the trend read it as a
collapse and the forecast fitted it as a real value
- New temporal._resample(): sum uses min_count=1, so an empty period is None; count
keeps 0 (zero events is the true count); other aggs were already None
- analyze_forecast uses the same helper, so the empty month is interpolated for the fit
- Example (April empty): trend R^2 0.06 -> 0.90, forecast no longer dips after "April 0"
- Chart already draws None as a gap; the report table shows it as a dash
- Reported by Rifqi (audit P0-8, decision D4)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

src/tools/analytics/forecast.py CHANGED
@@ -34,6 +34,7 @@ from src.tools.analytics.temporal import (
34
  _clean,
35
  _parse_date_column,
36
  _period_label,
 
37
  )
38
  from src.tools.ml.executor import run_fit
39
  from src.tools.spec_builder import build_description
@@ -221,7 +222,7 @@ async def analyze_forecast(
221
  dates = pd.Series(_parse_date_column(df, date_column).to_numpy(), name="_date")
222
  values = pd.Series(df[value_column].to_numpy(), name="_value")
223
  frame = pd.concat([dates, values], axis=1).dropna(subset=["_date"]).set_index("_date")
224
- resampled = frame["_value"].sort_index().resample(FREQ_MAP[freq]).agg(agg).astype(float)
225
 
226
  observed = resampled.dropna()
227
  if observed.shape[0] < _MIN_POINTS:
 
34
  _clean,
35
  _parse_date_column,
36
  _period_label,
37
+ _resample,
38
  )
39
  from src.tools.ml.executor import run_fit
40
  from src.tools.spec_builder import build_description
 
222
  dates = pd.Series(_parse_date_column(df, date_column).to_numpy(), name="_date")
223
  values = pd.Series(df[value_column].to_numpy(), name="_value")
224
  frame = pd.concat([dates, values], axis=1).dropna(subset=["_date"]).set_index("_date")
225
+ resampled = _resample(frame["_value"], freq, agg).astype(float)
226
 
227
  observed = resampled.dropna()
228
  if observed.shape[0] < _MIN_POINTS:
src/tools/analytics/temporal.py CHANGED
@@ -128,6 +128,19 @@ def _parse_date_column(df: pd.DataFrame, date_column: str) -> pd.Series:
128
  )
129
 
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  def _period_label(ts: pd.Timestamp, freq: str) -> str:
132
  """Human-readable period label keyed off the friendly frequency name."""
133
  if freq == "month":
@@ -268,7 +281,7 @@ def analyze_trend(
268
  dates = pd.Series(_parse_date_column(df, date_column).to_numpy(), name="_date")
269
  values = pd.Series(df[value_column].to_numpy(), name="_value")
270
  s = pd.concat([dates, values], axis=1).dropna(subset=["_date"]).set_index("_date")
271
- resampled = s["_value"].sort_index().resample(FREQ_MAP[freq]).agg(agg)
272
 
273
  points = [
274
  {"period": _period_label(ts, freq), "value": _clean(val)}
 
128
  )
129
 
130
 
131
+ def _resample(series: pd.Series, freq: str, agg: str) -> pd.Series:
132
+ """Aggregate a datetime-indexed series per period; a period with no rows stays empty.
133
+
134
+ Plain `resample().agg("sum")` turns a month with no rows into 0, which reads as a
135
+ collapse in the trend and is fitted as a real value by the forecast. `count` keeps
136
+ its 0 — zero events in a month is the true count.
137
+ """
138
+ resampler = series.sort_index().resample(FREQ_MAP[freq])
139
+ if agg == "sum":
140
+ return resampler.sum(min_count=1)
141
+ return resampler.agg(agg)
142
+
143
+
144
  def _period_label(ts: pd.Timestamp, freq: str) -> str:
145
  """Human-readable period label keyed off the friendly frequency name."""
146
  if freq == "month":
 
281
  dates = pd.Series(_parse_date_column(df, date_column).to_numpy(), name="_date")
282
  values = pd.Series(df[value_column].to_numpy(), name="_value")
283
  s = pd.concat([dates, values], axis=1).dropna(subset=["_date"]).set_index("_date")
284
+ resampled = _resample(s["_value"], freq, agg)
285
 
286
  points = [
287
  {"period": _period_label(ts, freq), "value": _clean(val)}