FSC — Okano and Kurisu (2026)#

Okano, R. and Kurisu, D. (2026). Functional Synthetic Control Methods for Metric Space-Valued Outcomes. arXiv:2601.07539. Replication package: RyoOkano21/FSC.

Path A, on the authors’ own data, for all three of the paper’s empirical applications. Two artefacts back it, and they answer different questions.

benchmarks/cases/fsc_okano.py pins a faithful port of the authors’ R code and reproduces every number they publish, exactly. It answers: is the method as implemented by its authors correctly understood here?

benchmarks/cases/fsc_estimator.py runs mlsynth.FSC itself. It answers a different question: does the estimator mlsynth ships land on the paper’s results? It does on one application exactly, and diverges on the other two for reasons that are measured and stated below.

What the reference port reproduces#

Every published figure, at the four decimals the paper prints.

Application

FSC

paper

augmented

paper

6.1 fertility, ASFR curves in \(L^2\)

0.1259

0.1259

0.0687

0.0687

6.2 mortality, age-at-death distributions

0.2092

0.2092

0.0634

0.0634

6.3 service trade, covariance matrices

39.3429

39.3429

20.0639

20.0639

All twenty augmented donor weights of Table 1, all seventeen of Table 2, and the three nonzero FSC weights of Table 3 match to their printed three decimals, with a maximum deviation of 0.000. The cross-validated penalties of Remark 5 reproduce too: mortality lands on 5.889182 to eight digits and service on 0.001864 against a printed 0.00186.

Four details of the reference code decide the answer, and none is visible from the paper alone.

The rounding. FSCM returns round(weight_scm, 4), and the rounding is carried into every downstream number. On the service application it is the difference between the published 39.3429 and the exact quadratic program’s 39.3423.

The norm is not consistent across the reported figures. The pre-treatment fit is computed as sum(diffs[-1]) — dropping the first grid point — for the plain estimator in all three applications, but the augmented leg is coded sum(diffs). The published augmented figures come out under drop-first for fertility and mortality and under all-points for service, so the three reported pairs are not on one common norm.

The rearrangement is not a sort. Rearrangement::rearrangement rescales the grid to the unit interval, interpolates onto 1001 equispaced points, and returns their type-7 empirical quantiles at the rescaled grid. Substituting a plain sort lands the mortality figure at 0.0640 where the published value is 0.0634.

The covariance outcome is a plain half-vectorisation, with no \(\sqrt 2\) on the off-diagonals. The Frobenius metric of the paper’s Example 3 counts each off-diagonal entry twice, so that map is not an isometry and 39.3429 is a vech norm, not a Frobenius one. The same weights scored under Example 3’s own metric give 51.9613.

Table 1 also contains an arithmetic slip: the Switzerland entry of 0.089 makes the FSC column sum to 1.089, which the simplex constraint forbids. Evaluating the objective settles it — the reported fit of 0.1259 is attained at Austria 0.396, Bulgaria 0.416, Czechia 0.188 and nothing else, while the Table 1 vector gives 0.2324 and its renormalisation 0.1482.

What the shipped estimator reproduces#

mlsynth.FSC matches the fertility application exactly — 0.1259 before augmentation and 0.0687 after, from the standard configuration with the penalty cross-validated, not supplied. That is the paper’s flagship application and the one its Example 1 is built around.

The other two diverge.

Mortality reproduces on the plain estimator, 0.2092, which does not involve the basis. The augmented figure comes out at 0.0630 against a published 0.0634, a gap of 0.6 percent. The cause is the basis inner product: mlsynth integrates over the whole argument grid, the reference code drops the first point. For fertility that choice is invisible — the fertility rate at age 12 is essentially zero for every country, so the dropped coordinate carries no information and the weights agree to 7e-7. For mortality it is not, because the quantile function at \(p = 0.01\) is a real number and dropping it discards real information; holding everything else fixed, the augmented weights move by up to 0.109. Using every point the data provides is the defensible reading of an \(L^2\) inner product, so that is what mlsynth does.

Service trade is not a divergence in the estimate so much as in the yardstick. The estimator applies the \(\sqrt 2\) off-diagonal scaling that makes the half-vectorisation a genuine Frobenius isometry, which the reference code does not, so its fit is a Frobenius norm and the published 39.3429 is a vech norm. The two are not comparable and should not be compared. The like-for-like comparator is 51.9613 — the authors’ own weights scored under Frobenius — and mlsynth attains 51.7665, which is what re-optimising under the correct metric should do.

Both are corrections, not discrepancies, and both are pinned in the estimator benchmark so a future change to either surfaces as a failure instead of drifting.

The penalty needs one more note, because getting it wrong caused a real bug here. The cross-validation objective of Remark 5 is nearly flat near its minimum: on the fertility data it varies by 0.03 percent across \(\lambda \in [5, 7]\) while the pre-treatment fit moves in its fourth decimal. The penalty is weakly identified, so its selected value is not a quantity to interpret. Worse, its natural scale is set by the Gram matrix of the basis coefficients, which moves with the square of the outcome — so a fixed absolute search interval means something different on every dataset. The authors’ scripts search \((0, 10)\), which suits their fertility panel and is four orders of magnitude too coarse for their mortality panel once the coefficients are a genuine \(L^2\) inner product; searching it there puts the optimum outside the interval and lands the augmented fit at 0.1688 instead of 0.0630. mlsynth therefore searches the penalty as a multiple of the design’s own Gram scale and reports both the relative and the absolute value.

Data#

basedata/okano_fsc_fertility.csv, basedata/okano_fsc_mortality.csv and basedata/okano_fsc_service.csv, converted from the authors’ asfr.RData, aad.RData and service.RData with the rdata package — nested R lists, which pyreadr cannot read, so no R installation is needed to rebuild them. benchmarks/reference/fsc_okano_data.py regenerates them. Underlying sources are the Human Fertility Database, the Human Mortality Database, and UN Trade and Development.