Are you on the latest chainladder version?
Describe the bug in words
BUG: cum_to_incr() produces spurious values on triangles produced by .grain().
When a monthly triangle is aggregated to annual via .grain("OYDY"),
cum_to_incr() replaces NaN with 0 before differencing, producing
(0 - prior_value) instead of NaN for unobserved cells.
This does NOT occur on triangles built directly at the target grain,
only on triangles produced via .grain().
Observed in: chainladder 0.9.1
Environment: pandas 2.3.3, numpy 2.4.4, macOS arm64
How can the bug be reproduced?
import chainladder as cl
import numpy as np
import pandas as pd
# Build a 24-month triangle from simple data
rows = []
for year in [2022, 2023]:
for month in range(1, 13):
origin = f"{year}-{month:02d}-01"
origin_dt = pd.Timestamp(origin)
# Each origin develops until 2023-12
end = pd.Timestamp("2023-12-01")
dev_months = (end.year - origin_dt.year) * 12 + (end.month - origin_dt.month) + 1
for d in range(1, dev_months + 1):
dev_dt = origin_dt + pd.DateOffset(months=d - 1)
rows.append({
"origin": origin,
"dev": dev_dt.strftime("%Y-%m-%d"),
"value": 1000 * d,
})
data = pd.DataFrame(rows)
tri = cl.Triangle(
data=data, origin="origin", development="dev",
columns=["value"], cumulative=True,
)
print(f"Monthly triangle: shape={tri.shape}\n")
# --- Test 1: grain to annual, then cum_to_incr ---
tri_y = tri.grain("OYDY")
incr_y = tri_y.cum_to_incr()
print("ANNUAL via .grain('OYDY'):")
print(f" Cumulative:\n{tri_y.to_frame()}\n")
print(f" cum_to_incr():\n{incr_y.to_frame()}\n")
# Expected
cum = tri_y.values[0, 0].copy()
expected = cum.copy()
expected[:, 1:] = cum[:, 1:] - cum[:, :-1]
print(f" Expected (numpy):\n{expected}\n")
# Flag bugs
print(" Problem cells:")
for r in range(cum.shape[0]):
for c in range(cum.shape[1]):
cum_val = cum[r, c]
incr_val = incr_y.values[0, 0, r, c]
if np.isnan(cum_val) and not np.isnan(incr_val):
print(f" row={r} col={c}: cumulative=NaN, cum_to_incr={incr_val:.0f} <-- BUG")
# --- Test 2: grain to quarterly, then cum_to_incr ---
tri_q = tri.grain("OQDQ")
incr_q = tri_q.cum_to_incr()
print("\nQUARTERLY via .grain('OQDQ'):")
print(f" Cumulative:\n{tri_q.to_frame()}\n")
print(f" cum_to_incr():\n{incr_q.to_frame()}\n")
cum_q = tri_q.values[0, 0].copy()
print(" Problem cells:")
found = False
for r in range(cum_q.shape[0]):
for c in range(cum_q.shape[1]):
cum_val = cum_q[r, c]
incr_val = incr_q.values[0, 0, r, c]
if np.isnan(cum_val) and not np.isnan(incr_val):
print(f" row={r} col={c}: cumulative=NaN, cum_to_incr={incr_val:.0f} <-- BUG")
found = True
if not found:
print("None (quarterly is correct)")
What is the expected behavior?
assert np.isnan(incr_vals[1, 1]), (
f"Expected incremental (2023, dev 24) to be NaN, got {incr_vals[1, 1]}. "
f"cum_to_incr() appears to fill NaN with 0 before differencing, "
f"producing 0 - {cum_vals[1, 0]:.0f} = {incr_vals[1, 1]:.0f}"
)
Are you on the latest chainladder version?
Describe the bug in words
BUG: cum_to_incr() produces spurious values on triangles produced by .grain().
When a monthly triangle is aggregated to annual via .grain("OYDY"),
cum_to_incr() replaces NaN with 0 before differencing, producing
(0 - prior_value) instead of NaN for unobserved cells.
This does NOT occur on triangles built directly at the target grain,
only on triangles produced via .grain().
Observed in: chainladder 0.9.1
Environment: pandas 2.3.3, numpy 2.4.4, macOS arm64
How can the bug be reproduced?
What is the expected behavior?