@actuarial-ts/core 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -4
- package/dist/mack.d.ts.map +1 -1
- package/dist/mack.js +58 -18
- package/dist/mack.js.map +1 -1
- package/dist/odpBootstrap.d.ts +8 -2
- package/dist/odpBootstrap.d.ts.map +1 -1
- package/dist/odpBootstrap.js +11 -2
- package/dist/odpBootstrap.js.map +1 -1
- package/package.json +3 -1
- package/src/benktander.ts +90 -0
- package/src/berquist.ts +349 -0
- package/src/bf.ts +129 -0
- package/src/canonical.ts +122 -0
- package/src/capping.ts +295 -0
- package/src/caseOutstanding.ts +270 -0
- package/src/chainladder.ts +101 -0
- package/src/clark.ts +719 -0
- package/src/diagnostics.ts +435 -0
- package/src/discounting.ts +417 -0
- package/src/elrMethods.ts +257 -0
- package/src/factors.ts +147 -0
- package/src/fisherLange.ts +374 -0
- package/src/freqSev.ts +152 -0
- package/src/ilf.ts +567 -0
- package/src/index.ts +29 -0
- package/src/mack.ts +329 -0
- package/src/merzWuthrich.ts +147 -0
- package/src/munichChainLadder.ts +398 -0
- package/src/odpBootstrap.ts +337 -0
- package/src/onlevel.ts +155 -0
- package/src/salvageSubro.ts +205 -0
- package/src/stochastic.ts +151 -0
- package/src/tail.ts +156 -0
- package/src/trend.ts +150 -0
- package/src/triangle.ts +235 -0
- package/src/triangleAlgebra.ts +111 -0
- package/src/types.ts +357 -0
- package/src/ulae.ts +326 -0
- package/src/util.ts +68 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { mackEstimators, extrapolateSigma2 } from "./mack.js";
|
|
2
|
+
import type { Triangle } from "./types.js";
|
|
3
|
+
import { ReservingError } from "./types.js";
|
|
4
|
+
import { isNum, lastObservedIndex } from "./util.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Munich chain ladder, Quarg & Mack (2004): a paired paid/incurred projection
|
|
8
|
+
* that corrects each triangle's development factors with the momentary
|
|
9
|
+
* (paid/incurred) ratio, so the projected paid and incurred ultimates
|
|
10
|
+
* converge instead of preserving each accident year's (P/I) gap forever
|
|
11
|
+
* (the separate-chain-ladder problem, Sec 1.1.2 of the paper).
|
|
12
|
+
*
|
|
13
|
+
* Estimators (Sec 3.1, all reproduced against the paper's printed rows):
|
|
14
|
+
* - fhat^P/fhat^I: volume-weighted development factors (shared with Mack).
|
|
15
|
+
* - sigmahat: Mack variance estimators around fhat, denominator = pairs - 1.
|
|
16
|
+
* - qhat_s: incurred-weighted average (P/I) per column; its reciprocal
|
|
17
|
+
* estimates the conditional mean of (I/P).
|
|
18
|
+
* - rhohat^P_s / rhohat^I_s: Mack-style variance estimators for the ratio
|
|
19
|
+
* series, volume-weighted by paid resp. incurred, denominator = cells - 1.
|
|
20
|
+
* - lambda^P / lambda^I: slopes of the single regression line THROUGH THE
|
|
21
|
+
* ORIGIN over the pooled residual plots (factor residual vs preceding
|
|
22
|
+
* ratio residual, all development years at once). The lambdas equal the
|
|
23
|
+
* residual correlation coefficients, so weak correlation collapses MCL
|
|
24
|
+
* gracefully toward the separate chain ladder.
|
|
25
|
+
*
|
|
26
|
+
* Projection (Sec 3.1.2): the paid and incurred recursions run
|
|
27
|
+
* SIMULTANEOUSLY, cell by cell left to right, because each paid step needs
|
|
28
|
+
* the current projected (I/P) ratio and each incurred step the projected
|
|
29
|
+
* (P/I). Implemented in the multiplied-out form of Sec 3.2.1
|
|
30
|
+
* (P*f + lambda*(sigma/rho)*(I - P/q)), which needs no division by the
|
|
31
|
+
* projected paid/incurred value and therefore stays sensible when current
|
|
32
|
+
* paid is tiny or zero.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
export interface MunichChainLadderOptions {
|
|
36
|
+
/**
|
|
37
|
+
* Explicit sigma (standard-deviation scale, same units as the printed
|
|
38
|
+
* sigmahat rows) for the FINAL development column, whose sigma is never
|
|
39
|
+
* estimable from a single factor. Quarg-Mack manually set both to 0.100
|
|
40
|
+
* in the paper's example. When omitted, the engine falls back to Mack's
|
|
41
|
+
* sigma^2 extrapolation rule (extrapolateSigma2); a warning is pushed
|
|
42
|
+
* either way, since the paper notes a sounder extrapolation should be
|
|
43
|
+
* used in practice.
|
|
44
|
+
*/
|
|
45
|
+
lastColumnSigma?: { paid?: number; incurred?: number };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The four estimated-residual triangles (Sec 3.1.2), null where not estimable. */
|
|
49
|
+
export interface MunichChainLadderResiduals {
|
|
50
|
+
/** Reshat(P_{i,t}): paid development-factor residuals, [origin][step]. */
|
|
51
|
+
paidFactor: (number | null)[][];
|
|
52
|
+
/** Reshat(I_{i,t}): incurred development-factor residuals. */
|
|
53
|
+
incurredFactor: (number | null)[][];
|
|
54
|
+
/** Reshat(Q^-1_{i,s}): (I/P) ratio residuals, [origin][ageColumn]. */
|
|
55
|
+
paidRatio: (number | null)[][];
|
|
56
|
+
/** Reshat(Q_{i,s}): (P/I) ratio residuals. */
|
|
57
|
+
incurredRatio: (number | null)[][];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface MunichChainLadderRow {
|
|
61
|
+
origin: string;
|
|
62
|
+
paidLatest: number;
|
|
63
|
+
incurredLatest: number;
|
|
64
|
+
paidUltimate: number;
|
|
65
|
+
incurredUltimate: number;
|
|
66
|
+
/** Separate (plain volume-weighted) chain ladder ultimates for comparison. */
|
|
67
|
+
sclPaidUltimate: number;
|
|
68
|
+
sclIncurredUltimate: number;
|
|
69
|
+
/** MCL ultimate paid / ultimate incurred; null when incurred is not positive. */
|
|
70
|
+
finalRatio: number | null;
|
|
71
|
+
/** SCL ultimate paid / ultimate incurred (the gap MCL is built to close). */
|
|
72
|
+
sclFinalRatio: number | null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface MunichChainLadderResult {
|
|
76
|
+
method: "munichChainLadder";
|
|
77
|
+
/** Volume-weighted fhat^P per development step. */
|
|
78
|
+
paidFactors: number[];
|
|
79
|
+
/** Volume-weighted fhat^I per development step. */
|
|
80
|
+
incurredFactors: number[];
|
|
81
|
+
/** sigmahat^P per step, after last-column fallback (see options). */
|
|
82
|
+
sigmaPaid: number[];
|
|
83
|
+
sigmaIncurred: number[];
|
|
84
|
+
/** qhat_s per age column; null where a column has no usable (P, I) pairs. */
|
|
85
|
+
qRatios: (number | null)[];
|
|
86
|
+
/** rhohat^P_s per age column; null where fewer than two pairs exist. */
|
|
87
|
+
rhoPaid: (number | null)[];
|
|
88
|
+
rhoIncurred: (number | null)[];
|
|
89
|
+
/** Through-origin regression slopes over the pooled residuals. */
|
|
90
|
+
lambdaPaid: number;
|
|
91
|
+
lambdaIncurred: number;
|
|
92
|
+
residuals: MunichChainLadderResiduals;
|
|
93
|
+
/** Full projected rectangles (observed cells passed through); null rows = skipped. */
|
|
94
|
+
projectedPaid: ((number | null)[] | null)[];
|
|
95
|
+
projectedIncurred: ((number | null)[] | null)[];
|
|
96
|
+
rows: MunichChainLadderRow[];
|
|
97
|
+
totals: {
|
|
98
|
+
paidUltimate: number;
|
|
99
|
+
incurredUltimate: number;
|
|
100
|
+
sclPaidUltimate: number;
|
|
101
|
+
sclIncurredUltimate: number;
|
|
102
|
+
};
|
|
103
|
+
warnings: string[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Cell usable for ratio work: both triangles observed, both strictly positive. */
|
|
107
|
+
function usablePair(p: number | null, inc: number | null): boolean {
|
|
108
|
+
return isNum(p) && isNum(inc) && p > 0 && inc > 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function runMunichChainLadder(
|
|
112
|
+
paid: Triangle,
|
|
113
|
+
incurred: Triangle,
|
|
114
|
+
options: MunichChainLadderOptions = {},
|
|
115
|
+
): MunichChainLadderResult {
|
|
116
|
+
const n = paid.origins.length;
|
|
117
|
+
const K = paid.ages.length;
|
|
118
|
+
|
|
119
|
+
if (
|
|
120
|
+
incurred.origins.length !== n ||
|
|
121
|
+
incurred.ages.length !== K ||
|
|
122
|
+
paid.origins.some((o, i) => incurred.origins[i] !== o) ||
|
|
123
|
+
paid.ages.some((a, j) => incurred.ages[j] !== a)
|
|
124
|
+
) {
|
|
125
|
+
throw new ReservingError(
|
|
126
|
+
"SHAPE",
|
|
127
|
+
"Munich chain ladder needs paid and incurred triangles with identical origins and ages",
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
for (const tri of [paid, incurred]) {
|
|
131
|
+
if (tri.values.length !== n || tri.values.some((row) => row.length !== K)) {
|
|
132
|
+
throw new ReservingError(
|
|
133
|
+
"SHAPE",
|
|
134
|
+
"Triangle values must be a full origins x ages rectangle (null below the diagonal)",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (K < 2) {
|
|
139
|
+
throw new ReservingError(
|
|
140
|
+
"TOO_SMALL",
|
|
141
|
+
"Munich chain ladder requires at least two development ages",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const warnings: string[] = [];
|
|
146
|
+
|
|
147
|
+
// Chain-ladder estimators, shared with Mack so the two can never drift.
|
|
148
|
+
const paidEst = mackEstimators(paid);
|
|
149
|
+
const incurredEst = mackEstimators(incurred);
|
|
150
|
+
|
|
151
|
+
// sigma completion: the final column (single factor) is never estimable.
|
|
152
|
+
// An explicit lastColumnSigma wins (the paper's manual 0.100); otherwise
|
|
153
|
+
// Mack's extrapolation rule fills every non-estimable column.
|
|
154
|
+
const completeSigma2 = (
|
|
155
|
+
raw: (number | null)[],
|
|
156
|
+
label: "paid" | "incurred",
|
|
157
|
+
override: number | undefined,
|
|
158
|
+
): number[] => {
|
|
159
|
+
const withOverride = [...raw];
|
|
160
|
+
if (withOverride[K - 2] === null) {
|
|
161
|
+
const tag = label === "paid" ? "P" : "I";
|
|
162
|
+
if (isNum(override) && override >= 0) {
|
|
163
|
+
withOverride[K - 2] = override * override;
|
|
164
|
+
warnings.push(
|
|
165
|
+
`sigma^${tag} for the final development column is not estimable; using the caller-supplied ${override} (Quarg-Mack set 0.100 in their example)`,
|
|
166
|
+
);
|
|
167
|
+
} else {
|
|
168
|
+
warnings.push(
|
|
169
|
+
`sigma^${tag} for the final development column is not estimable; filled by Mack's extrapolation rule (the paper suggests a sounder extrapolation in practice)`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return extrapolateSigma2(withOverride, paid.ages, warnings);
|
|
174
|
+
};
|
|
175
|
+
const sigma2Paid = completeSigma2(paidEst.sigma2, "paid", options.lastColumnSigma?.paid);
|
|
176
|
+
const sigma2Incurred = completeSigma2(
|
|
177
|
+
incurredEst.sigma2,
|
|
178
|
+
"incurred",
|
|
179
|
+
options.lastColumnSigma?.incurred,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// qhat_s (incurred-weighted average P/I per column) and the rho variance
|
|
183
|
+
// estimators for the ratio series (Sec 3.1.2; denominator = pairs - 1,
|
|
184
|
+
// exactly the Mack convention, verified against the printed rho rows).
|
|
185
|
+
const qRatios: (number | null)[] = new Array(K).fill(null);
|
|
186
|
+
const rhoPaid: (number | null)[] = new Array(K).fill(null);
|
|
187
|
+
const rhoIncurred: (number | null)[] = new Array(K).fill(null);
|
|
188
|
+
for (let s = 0; s < K; s++) {
|
|
189
|
+
let sumP = 0;
|
|
190
|
+
let sumI = 0;
|
|
191
|
+
let count = 0;
|
|
192
|
+
for (let i = 0; i < n; i++) {
|
|
193
|
+
const p = paid.values[i]![s] ?? null;
|
|
194
|
+
const inc = incurred.values[i]![s] ?? null;
|
|
195
|
+
if (usablePair(p, inc)) {
|
|
196
|
+
sumP += p!;
|
|
197
|
+
sumI += inc!;
|
|
198
|
+
count++;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (count === 0 || sumP <= 0 || sumI <= 0) continue;
|
|
202
|
+
const q = sumP / sumI;
|
|
203
|
+
qRatios[s] = q;
|
|
204
|
+
if (count < 2) continue;
|
|
205
|
+
let devP = 0;
|
|
206
|
+
let devI = 0;
|
|
207
|
+
for (let i = 0; i < n; i++) {
|
|
208
|
+
const p = paid.values[i]![s] ?? null;
|
|
209
|
+
const inc = incurred.values[i]![s] ?? null;
|
|
210
|
+
if (!usablePair(p, inc)) continue;
|
|
211
|
+
devP += p! * (inc! / p! - 1 / q) ** 2;
|
|
212
|
+
devI += inc! * (p! / inc! - q) ** 2;
|
|
213
|
+
}
|
|
214
|
+
rhoPaid[s] = Math.sqrt(devP / (count - 1));
|
|
215
|
+
rhoIncurred[s] = Math.sqrt(devI / (count - 1));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// The four residual triangles. Factor residuals need the DATA-ESTIMATED
|
|
219
|
+
// sigma (not the extrapolated one); ratio residuals need rho > 0.
|
|
220
|
+
const residuals: MunichChainLadderResiduals = {
|
|
221
|
+
paidFactor: Array.from({ length: n }, () => new Array<number | null>(K - 1).fill(null)),
|
|
222
|
+
incurredFactor: Array.from({ length: n }, () => new Array<number | null>(K - 1).fill(null)),
|
|
223
|
+
paidRatio: Array.from({ length: n }, () => new Array<number | null>(K).fill(null)),
|
|
224
|
+
incurredRatio: Array.from({ length: n }, () => new Array<number | null>(K).fill(null)),
|
|
225
|
+
};
|
|
226
|
+
for (let s = 0; s < K; s++) {
|
|
227
|
+
const q = qRatios[s];
|
|
228
|
+
const rP = rhoPaid[s];
|
|
229
|
+
const rI = rhoIncurred[s];
|
|
230
|
+
if (!isNum(q) || !isNum(rP) || !isNum(rI) || rP <= 0 || rI <= 0) continue;
|
|
231
|
+
for (let i = 0; i < n; i++) {
|
|
232
|
+
const p = paid.values[i]![s] ?? null;
|
|
233
|
+
const inc = incurred.values[i]![s] ?? null;
|
|
234
|
+
if (!usablePair(p, inc)) continue;
|
|
235
|
+
residuals.paidRatio[i]![s] = ((inc! / p! - 1 / q) / rP) * Math.sqrt(p!);
|
|
236
|
+
residuals.incurredRatio[i]![s] = ((p! / inc! - q) / rI) * Math.sqrt(inc!);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
for (let s = 0; s < K - 1; s++) {
|
|
240
|
+
const s2P = paidEst.sigma2[s];
|
|
241
|
+
const s2I = incurredEst.sigma2[s];
|
|
242
|
+
for (let i = 0; i < n; i++) {
|
|
243
|
+
const p0 = paid.values[i]![s] ?? null;
|
|
244
|
+
const p1 = paid.values[i]![s + 1] ?? null;
|
|
245
|
+
if (isNum(s2P) && s2P > 0 && isNum(p0) && isNum(p1) && p0 > 0) {
|
|
246
|
+
residuals.paidFactor[i]![s] = ((p1 / p0 - paidEst.f[s]!) / Math.sqrt(s2P)) * Math.sqrt(p0);
|
|
247
|
+
}
|
|
248
|
+
const i0 = incurred.values[i]![s] ?? null;
|
|
249
|
+
const i1 = incurred.values[i]![s + 1] ?? null;
|
|
250
|
+
if (isNum(s2I) && s2I > 0 && isNum(i0) && isNum(i1) && i0 > 0) {
|
|
251
|
+
residuals.incurredFactor[i]![s] =
|
|
252
|
+
((i1 / i0 - incurredEst.f[s]!) / Math.sqrt(s2I)) * Math.sqrt(i0);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Size gate: the pooled regressions need factor residuals, which exist
|
|
258
|
+
// only where a development column has two or more observed factors (a 3x3
|
|
259
|
+
// triangle at minimum). Zero-variance ratio series (rho = 0) are NOT a
|
|
260
|
+
// size problem - they collapse lambda to 0 below, the paper's built-in
|
|
261
|
+
// safety mechanism.
|
|
262
|
+
const anySigma = (raw: (number | null)[]): boolean => raw.some((v) => v !== null);
|
|
263
|
+
if (!anySigma(paidEst.sigma2) && !anySigma(incurredEst.sigma2)) {
|
|
264
|
+
throw new ReservingError(
|
|
265
|
+
"TOO_SMALL",
|
|
266
|
+
"Munich chain ladder needs at least one development column with two observed factors (a 3x3 triangle at minimum) to pool residuals",
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// lambda^P / lambda^I: through-origin regression slopes over the pooled
|
|
271
|
+
// residual pairs (factor residual on the preceding ratio residual).
|
|
272
|
+
let numP = 0;
|
|
273
|
+
let denP = 0;
|
|
274
|
+
let numI = 0;
|
|
275
|
+
let denI = 0;
|
|
276
|
+
for (let s = 0; s < K - 1; s++) {
|
|
277
|
+
for (let i = 0; i < n; i++) {
|
|
278
|
+
const fResP = residuals.paidFactor[i]![s];
|
|
279
|
+
const rResP = residuals.paidRatio[i]![s];
|
|
280
|
+
if (isNum(fResP) && isNum(rResP)) {
|
|
281
|
+
numP += rResP * fResP;
|
|
282
|
+
denP += rResP * rResP;
|
|
283
|
+
}
|
|
284
|
+
const fResI = residuals.incurredFactor[i]![s];
|
|
285
|
+
const rResI = residuals.incurredRatio[i]![s];
|
|
286
|
+
if (isNum(fResI) && isNum(rResI)) {
|
|
287
|
+
numI += rResI * fResI;
|
|
288
|
+
denI += rResI * rResI;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const lambdaFor = (num: number, den: number, label: string): number => {
|
|
293
|
+
if (den > 0) return num / den;
|
|
294
|
+
warnings.push(
|
|
295
|
+
`No usable ${label} residual variation; lambda^${label === "paid" ? "P" : "I"} set to 0 (the projection collapses to the separate chain ladder)`,
|
|
296
|
+
);
|
|
297
|
+
return 0;
|
|
298
|
+
};
|
|
299
|
+
const lambdaPaid = lambdaFor(numP, denP, "paid");
|
|
300
|
+
const lambdaIncurred = lambdaFor(numI, denI, "incurred");
|
|
301
|
+
|
|
302
|
+
// Simultaneous cell-by-cell projection, and the SCL comparison projection.
|
|
303
|
+
const projectedPaid: ((number | null)[] | null)[] = new Array(n).fill(null);
|
|
304
|
+
const projectedIncurred: ((number | null)[] | null)[] = new Array(n).fill(null);
|
|
305
|
+
const rows: MunichChainLadderRow[] = [];
|
|
306
|
+
const totals = { paidUltimate: 0, incurredUltimate: 0, sclPaidUltimate: 0, sclIncurredUltimate: 0 };
|
|
307
|
+
const droppedCorrectionColumns = new Set<number>();
|
|
308
|
+
|
|
309
|
+
for (let i = 0; i < n; i++) {
|
|
310
|
+
const lastP = lastObservedIndex(paid.values[i]!);
|
|
311
|
+
const lastI = lastObservedIndex(incurred.values[i]!);
|
|
312
|
+
if (lastP < 0 || lastI < 0) {
|
|
313
|
+
warnings.push(
|
|
314
|
+
`Origin ${paid.origins[i]} has no observed ${lastP < 0 ? "paid" : "incurred"} values; excluded from results`,
|
|
315
|
+
);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (lastP !== lastI) {
|
|
319
|
+
warnings.push(
|
|
320
|
+
`Origin ${paid.origins[i]}: paid and incurred diagonals end at different ages (${paid.ages[lastP]} vs ${paid.ages[lastI]}); the joint projection starts at the earlier age and observed cells are kept as given`,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
const start = Math.min(lastP, lastI);
|
|
324
|
+
|
|
325
|
+
const mclP: (number | null)[] = paid.values[i]!.map((v) => (isNum(v) ? v : null));
|
|
326
|
+
const mclI: (number | null)[] = incurred.values[i]!.map((v) => (isNum(v) ? v : null));
|
|
327
|
+
for (let s = start; s < K - 1; s++) {
|
|
328
|
+
const p = mclP[s]!;
|
|
329
|
+
const inc = mclI[s]!;
|
|
330
|
+
// Multiplied-out recursion (Sec 3.2.1): no division by p or inc, so
|
|
331
|
+
// zero paid stays projectable, driven by the incurred side.
|
|
332
|
+
let nextP = p * paidEst.f[s]!;
|
|
333
|
+
let nextI = inc * incurredEst.f[s]!;
|
|
334
|
+
const q = qRatios[s];
|
|
335
|
+
const rP = rhoPaid[s];
|
|
336
|
+
const rI = rhoIncurred[s];
|
|
337
|
+
if (isNum(q) && q > 0 && isNum(rP) && rP > 0 && isNum(rI) && rI > 0) {
|
|
338
|
+
nextP += lambdaPaid * (Math.sqrt(sigma2Paid[s]!) / rP) * (inc - p / q);
|
|
339
|
+
nextI += lambdaIncurred * (Math.sqrt(sigma2Incurred[s]!) / rI) * (p - inc * q);
|
|
340
|
+
} else if (!droppedCorrectionColumns.has(s)) {
|
|
341
|
+
droppedCorrectionColumns.add(s);
|
|
342
|
+
warnings.push(
|
|
343
|
+
`No usable (P/I) ratio parameters at age ${paid.ages[s]}; the MCL correction is dropped for that step (falls back to the chain-ladder factor)`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
// Never overwrite an observation (only possible on ragged diagonals).
|
|
347
|
+
const observedP = mclP[s + 1] ?? null;
|
|
348
|
+
const observedI = mclI[s + 1] ?? null;
|
|
349
|
+
mclP[s + 1] = isNum(observedP) ? observedP : nextP;
|
|
350
|
+
mclI[s + 1] = isNum(observedI) ? observedI : nextI;
|
|
351
|
+
}
|
|
352
|
+
projectedPaid[i] = mclP;
|
|
353
|
+
projectedIncurred[i] = mclI;
|
|
354
|
+
|
|
355
|
+
// Separate chain ladder from each triangle's own diagonal.
|
|
356
|
+
let sclP = paid.values[i]![lastP]!;
|
|
357
|
+
for (let s = lastP; s < K - 1; s++) sclP *= paidEst.f[s]!;
|
|
358
|
+
let sclI = incurred.values[i]![lastI]!;
|
|
359
|
+
for (let s = lastI; s < K - 1; s++) sclI *= incurredEst.f[s]!;
|
|
360
|
+
|
|
361
|
+
const paidUltimate = mclP[K - 1]!;
|
|
362
|
+
const incurredUltimate = mclI[K - 1]!;
|
|
363
|
+
rows.push({
|
|
364
|
+
origin: paid.origins[i]!,
|
|
365
|
+
paidLatest: paid.values[i]![lastP]!,
|
|
366
|
+
incurredLatest: incurred.values[i]![lastI]!,
|
|
367
|
+
paidUltimate,
|
|
368
|
+
incurredUltimate,
|
|
369
|
+
sclPaidUltimate: sclP,
|
|
370
|
+
sclIncurredUltimate: sclI,
|
|
371
|
+
finalRatio: incurredUltimate > 0 ? paidUltimate / incurredUltimate : null,
|
|
372
|
+
sclFinalRatio: sclI > 0 ? sclP / sclI : null,
|
|
373
|
+
});
|
|
374
|
+
totals.paidUltimate += paidUltimate;
|
|
375
|
+
totals.incurredUltimate += incurredUltimate;
|
|
376
|
+
totals.sclPaidUltimate += sclP;
|
|
377
|
+
totals.sclIncurredUltimate += sclI;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
method: "munichChainLadder",
|
|
382
|
+
paidFactors: paidEst.f,
|
|
383
|
+
incurredFactors: incurredEst.f,
|
|
384
|
+
sigmaPaid: sigma2Paid.map((v) => Math.sqrt(v)),
|
|
385
|
+
sigmaIncurred: sigma2Incurred.map((v) => Math.sqrt(v)),
|
|
386
|
+
qRatios,
|
|
387
|
+
rhoPaid,
|
|
388
|
+
rhoIncurred,
|
|
389
|
+
lambdaPaid,
|
|
390
|
+
lambdaIncurred,
|
|
391
|
+
residuals,
|
|
392
|
+
projectedPaid,
|
|
393
|
+
projectedIncurred,
|
|
394
|
+
rows,
|
|
395
|
+
totals,
|
|
396
|
+
warnings,
|
|
397
|
+
};
|
|
398
|
+
}
|