SPB Git

spb/metrika Public

Stata-class statistics, GPU-accelerated by Apple Silicon. Native Swift — no Electron, no Python runtime, no compromises.

Swift 92.4% HTML 3.3% R 3% Shell 1.3%
14.9 KB · 417 lines r
Raw Blame History
1#!/usr/bin/env Rscript2#3#  generate.R — Metrika4#5#  Author:  Simon-Pierre Boucher6#  Contact: contact@spboucher.ai7#  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.8#9#  Generates the golden numerical fixtures for MetrikaKit estimator tests10#  (CLAUDE.md §9). Writes a reference dataset (CSV) plus expected values11#  (TSV, key<TAB>value at 17 significant digits) into the Swift test12#  resources at MetrikaKit/Tests/MetrikaKitTests/Fixtures/.13#14#  All expected values are computed from the CSV **after** a write/read15#  round-trip so Swift and R consume bit-identical inputs.16#1718out_dir <- file.path(19  dirname(dirname(normalizePath(sub("--file=", "", grep("--file=", commandArgs(FALSE), value = TRUE))))),20  "..", "MetrikaKit", "Tests", "MetrikaKitTests", "Fixtures"21)22out_dir <- normalizePath(out_dir, mustWork = FALSE)23dir.create(out_dir, recursive = TRUE, showWarnings = FALSE)2425# ---------------------------------------------------------------- dataset26set.seed(42)27n <- 6028region  <- rep(1:3, length.out = n)29firm_id <- rep(1:12, each = 5)30price   <- round(runif(n, 5, 20), 4)31noise   <- round(rnorm(n, 0, 0.15), 6)32log_rev <- 4 - 0.08 * price + 0.10 * (region == 2) + 0.20 * (region == 3) + noise33revenue <- round(exp(log_rev), 6)3435# GLM outcomes — drawn AFTER all draws above so earlier fixture values36# stay bit-identical when regenerating.37purchase <- rbinom(n, 1, plogis(2 - 0.20 * price))38orders   <- rpois(n, exp(1.6 - 0.09 * price))3940# Instruments for the 2SLS fixture (correlated with price by41# construction), drawn after everything above.42z1 <- round(price + rnorm(n, 0, 2), 4)43z2 <- round(0.5 * price + rnorm(n, 0, 3), 4)4445# Inject missing values to exercise listwise deletion.46price_missing <- price47price_missing[c(7, 23, 41)] <- NA4849data <- data.frame(50  revenue = revenue,51  price = price_missing,52  region = region,53  firm_id = firm_id,54  purchase = purchase,55  orders = orders,56  z1 = z1,57  z2 = z258)59csv_path <- file.path(out_dir, "regression.csv")60write.csv(data, csv_path, row.names = FALSE, quote = FALSE, na = "")6162# Round-trip: recompute everything from what was actually written.63data <- read.csv(csv_path)6465sink_path <- file.path(out_dir, "expected.tsv")66lines <- character(0)67emit <- function(key, value) {68  lines <<- c(lines, sprintf("%s\t%.17g", key, value))69}7071# ------------------------------------------------------------- summarize72x <- data$revenue73emit("sum_revenue_n",    length(x))74emit("sum_revenue_mean", mean(x))75emit("sum_revenue_var",  var(x))76emit("sum_revenue_sd",   sd(x))77emit("sum_revenue_min",  min(x))78emit("sum_revenue_max",  max(x))79m2 <- mean((x - mean(x))^2)80m3 <- mean((x - mean(x))^3)81m4 <- mean((x - mean(x))^4)82emit("sum_revenue_skewness", m3 / m2^1.5)   # Stata definition83emit("sum_revenue_kurtosis", m4 / m2^2)8485# ------------------------------------------------------------ regression86# Estimation sample: listwise deletion on price.87complete <- !is.na(data$price)88d <- data[complete, ]89d$log_rev <- log(d$revenue)90n_est <- nrow(d)9192X <- cbind(price = d$price, `_cons` = 1)93y <- d$log_rev94k <- ncol(X)9596fit <- lm(log_rev ~ price, data = d)97beta <- coef(fit)                      # (Intercept), price98resid <- residuals(fit)99XtXinv <- solve(t(X) %*% X)100df_r <- n_est - k101sigma2 <- sum(resid^2) / df_r102103emit("ols_n", n_est)104emit("ols_dropped", sum(!complete))105emit("ols_b_price", beta["price"])106emit("ols_b_cons",  beta["(Intercept)"])107emit("ols_r2",   summary(fit)$r.squared)108emit("ols_r2a",  summary(fit)$adj.r.squared)109emit("ols_rmse", sqrt(sigma2))110emit("ols_F",    summary(fit)$fstatistic[["value"]])111112V_classical <- sigma2 * XtXinv113emit("ols_se_classical_price", sqrt(V_classical["price", "price"]))114emit("ols_se_classical_cons",  sqrt(V_classical["_cons", "_cons"]))115t_price <- beta["price"] / sqrt(V_classical["price", "price"])116emit("ols_t_price", t_price)117emit("ols_p_price", 2 * pt(-abs(t_price), df_r))118crit <- qt(0.975, df_r)119emit("ols_ci_lower_price", beta["price"] - crit * sqrt(V_classical["price", "price"]))120emit("ols_ci_upper_price", beta["price"] + crit * sqrt(V_classical["price", "price"]))121122# HC0–HC3 sandwich estimators (manual, no packages).123h <- hatvalues(fit)124sandwich_se <- function(w) {125  meat <- t(X * w) %*% X126  V <- XtXinv %*% meat %*% XtXinv127  sqrt(diag(V))128}129se_hc0 <- sandwich_se(resid^2)130se_hc1 <- sandwich_se(resid^2 * n_est / df_r)131se_hc2 <- sandwich_se(resid^2 / (1 - h))132se_hc3 <- sandwich_se(resid^2 / (1 - h)^2)133emit("ols_se_hc0_price", se_hc0["price"])134emit("ols_se_hc0_cons",  se_hc0["_cons"])135emit("ols_se_hc1_price", se_hc1["price"])136emit("ols_se_hc1_cons",  se_hc1["_cons"])137emit("ols_se_hc2_price", se_hc2["price"])138emit("ols_se_hc2_cons",  se_hc2["_cons"])139emit("ols_se_hc3_price", se_hc3["price"])140emit("ols_se_hc3_cons",  se_hc3["_cons"])141142# Cluster-robust with Stata regress small-sample factor.143cl <- d$firm_id144u <- rowsum(X * resid, cl)145G <- length(unique(cl))146meat_cl <- (G / (G - 1)) * ((n_est - 1) / df_r) * (t(u) %*% u)147V_cl <- XtXinv %*% meat_cl %*% XtXinv148emit("ols_G", G)149emit("ols_se_cluster_price", sqrt(V_cl["price", "price"]))150emit("ols_se_cluster_cons",  sqrt(V_cl["_cons", "_cons"]))151t_cl <- beta["price"] / sqrt(V_cl["price", "price"])152emit("ols_p_cluster_price", 2 * pt(-abs(t_cl), G - 1))153154# Factor-variable regression: log_rev ~ price + i.region (base = 1).155fit2 <- lm(log_rev ~ price + factor(region), data = d)156b2 <- coef(fit2)157emit("ols2_b_price",   b2[["price"]])158emit("ols2_b_region2", b2[["factor(region)2"]])159emit("ols2_b_region3", b2[["factor(region)3"]])160emit("ols2_b_cons",    b2[["(Intercept)"]])161emit("ols2_r2", summary(fit2)$r.squared)162163# ------------------------------------------------------------------ GLMs164# Fit tightly (epsilon 1e-12) so R and Swift land on the same optimum to165# well past the 1e-10 test tolerance. Estimation sample = complete cases.166ctrl <- glm.control(epsilon = 1e-12, maxit = 100)167168# Expected information (X'WX)^-1 evaluated AT the converged coefficients.169# R's vcov(fit) instead reuses the weights of the last IRLS step (at the170# second-to-last iterate), so it is only ~1e-6-accurate by its own171# stopping rule — not good enough for 1e-10 fixtures.172glm_bread <- function(fit) {173  Xg <- model.matrix(fit)174  eta <- as.vector(Xg %*% coef(fit))175  fam <- fit$family176  mu <- fam$linkinv(eta)177  W <- fam$mu.eta(eta)^2 / fam$variance(mu)178  solve(t(Xg * W) %*% Xg)179}180181glm_bread_sandwich <- function(fit, score_scale, cl = NULL) {182  Xg <- model.matrix(fit)183  u <- Xg * score_scale184  bread <- glm_bread(fit)185  if (is.null(cl)) {186    meat <- t(u) %*% u187  } else {188    ug <- rowsum(u, cl)189    G <- nrow(ug)190    meat <- (G / (G - 1)) * (t(ug) %*% ug)191  }192  sqrt(diag(bread %*% meat %*% bread))193}194195lg <- glm(purchase ~ price, data = d, family = binomial(), control = ctrl)196V_lg <- glm_bread(lg)197emit("logit_b_price", coef(lg)[["price"]])198emit("logit_b_cons",  coef(lg)[["(Intercept)"]])199emit("logit_se_price", sqrt(V_lg["price", "price"]))200emit("logit_se_cons",  sqrt(V_lg["(Intercept)", "(Intercept)"]))201emit("logit_ll",  as.numeric(logLik(lg)))202lg0 <- glm(purchase ~ 1, data = d, family = binomial(), control = ctrl)203emit("logit_ll0", as.numeric(logLik(lg0)))204emit("logit_chi2", 2 * (as.numeric(logLik(lg)) - as.numeric(logLik(lg0))))205emit("logit_chi2p", pchisq(206  2 * (as.numeric(logLik(lg)) - as.numeric(logLik(lg0))), 1, lower.tail = FALSE207))208emit("logit_se_hc0_price", glm_bread_sandwich(lg, d$purchase - fitted(lg))["price"])209emit("logit_se_cluster_price",210     glm_bread_sandwich(lg, d$purchase - fitted(lg), cl = d$firm_id)["price"])211212pr <- glm(purchase ~ price, data = d,213          family = binomial(link = "probit"), control = ctrl)214emit("probit_b_price", coef(pr)[["price"]])215emit("probit_b_cons",  coef(pr)[["(Intercept)"]])216emit("probit_se_price", sqrt(glm_bread(pr)["price", "price"]))217emit("probit_ll", as.numeric(logLik(pr)))218219ps <- glm(orders ~ price, data = d, family = poisson(), control = ctrl)220emit("pois_b_price", coef(ps)[["price"]])221emit("pois_b_cons",  coef(ps)[["(Intercept)"]])222emit("pois_se_price", sqrt(glm_bread(ps)["price", "price"]))223emit("pois_ll", as.numeric(logLik(ps)))224emit("pois_se_hc0_price", glm_bread_sandwich(ps, d$orders - fitted(ps))["price"])225226# ----------------------------------------------------- fixed effects (FE)227# Within estimator, Stata conventions: demean within firm, add back grand228# means (so _cons is reported), df = N − K − G. Cluster VCE on the panel229# with the G/(G−1) factor and t on G−1 df.230fe_within <- function(v, g) {231  gm <- ave(v, g)             # group means232  v - gm + mean(v)233}234g <- d$firm_id235Nw <- nrow(d)236yt <- fe_within(d$log_rev, g)237xt_p <- fe_within(d$price, g)238Xw <- cbind(price = xt_p, `_cons` = 1)239G <- length(unique(g))240Kw <- 1241df_fe <- Nw - Kw - G242243fe_fit <- lm.fit(Xw, yt)244b_fe <- fe_fit$coefficients245res_fe <- fe_fit$residuals246XtXinv_w <- solve(t(Xw) %*% Xw)247sigma2_fe <- sum(res_fe^2) / df_fe248emit("fe_N", Nw)249emit("fe_G", G)250emit("fe_b_price", b_fe[["price"]])251emit("fe_b_cons",  b_fe[["_cons"]])252emit("fe_se_price", sqrt(sigma2_fe * XtXinv_w["price", "price"]))253yd <- d$log_rev - ave(d$log_rev, g)254emit("fe_r2_within", 1 - sum(res_fe^2) / sum(yd^2))255emit("fe_rmse", sqrt(sigma2_fe))256257# Cluster on the panel: scores use DEMEANED x (constant column as-is).258xd <- d$price - ave(d$price, g)259u_fe <- rowsum(cbind(xd * res_fe, res_fe), g)260meat_fe <- (G / (G - 1)) * (t(u_fe) %*% u_fe)261V_fe_cl <- XtXinv_w %*% meat_fe %*% XtXinv_w262emit("fe_se_cluster_price", sqrt(V_fe_cl[1, 1]))263t_fe_cl <- b_fe[["price"]] / sqrt(V_fe_cl[1, 1])264emit("fe_p_cluster_price", 2 * pt(-abs(t_fe_cl), G - 1))265266# ------------------------------------------------------------- 2SLS (IV)267# ivregress 2sls log_rev (price = z1 z2), Stata `small` convention:268# residuals from the ORIGINAL regressors, sigma2 = u'u/(N-K), t stats.269Xiv <- cbind(price = d$price, `_cons` = 1)270Ziv <- cbind(z1 = d$z1, z2 = d$z2, `_cons` = 1)271PZ <- Ziv %*% solve(t(Ziv) %*% Ziv) %*% t(Ziv)272Xhat <- PZ %*% Xiv273XhXhinv <- solve(t(Xhat) %*% Xhat)274b_iv <- XhXhinv %*% t(Xhat) %*% d$log_rev275u_iv <- d$log_rev - Xiv %*% b_iv276df_iv <- Nw - 2277sigma2_iv <- sum(u_iv^2) / df_iv278V_iv <- sigma2_iv * XhXhinv279emit("iv_N", Nw)280emit("iv_b_price", b_iv[1, 1])281emit("iv_b_cons",  b_iv[2, 1])282emit("iv_se_price", sqrt(V_iv[1, 1]))283emit("iv_se_cons",  sqrt(V_iv[2, 2]))284t_iv <- b_iv[1, 1] / sqrt(V_iv[1, 1])285emit("iv_p_price", 2 * pt(-abs(t_iv), df_iv))286emit("iv_r2", 1 - sum(u_iv^2) / sum((d$log_rev - mean(d$log_rev))^2))287emit("iv_rmse", sqrt(sigma2_iv))288289# Robust (HC1 with the small factor N/(N-K)) built on projected X.290meat_iv <- t(Xhat * as.vector(u_iv^2)) %*% Xhat * (Nw / df_iv)291V_iv_r <- XhXhinv %*% meat_iv %*% XhXhinv292emit("iv_se_hc1_price", sqrt(V_iv_r[1, 1]))293294# ---------------------------------------------------- margins (AME, delta)295# Average marginal effects with delta-method SEs at the converged296# coefficients, mirroring the engine's formulas exactly.297ame_glm <- function(fit, var, fprime, fsecond) {298  Xg <- model.matrix(fit)299  eta <- as.vector(Xg %*% coef(fit))300  fp <- fprime(eta)301  fs <- fsecond(eta)302  b <- coef(fit)[[var]]303  ame <- b * mean(fp)304  grad <- colMeans(Xg * fs) * b305  grad[[var]] <- grad[[var]] + mean(fp)306  V <- glm_bread(fit)307  se <- sqrt(as.numeric(t(grad) %*% V %*% grad))308  c(ame = ame, se = se)309}310311m_lg <- ame_glm(312  lg, "price",313  function(e) plogis(e) * (1 - plogis(e)),314  function(e) plogis(e) * (1 - plogis(e)) * (1 - 2 * plogis(e))315)316emit("margins_logit_price", m_lg[["ame"]])317emit("margins_logit_se",    m_lg[["se"]])318319m_ps <- ame_glm(ps, "price", exp, exp)320emit("margins_pois_price", m_ps[["ame"]])321emit("margins_pois_se",    m_ps[["se"]])322323# ------------------------------------------------------------ elastic net324# glmnet at fixed lambda, tight threshold. price/z1/z2 are correlated by325# construction, so the l1 penalty must actually choose among them.326if (requireNamespace("glmnet", quietly = TRUE)) {327  Xen <- as.matrix(d[, c("price", "z1", "z2", "orders")])328  yen <- d$log_rev329330  fit_lasso <- glmnet::glmnet(331    Xen, yen, alpha = 1, lambda = 0.05,332    thresh = 1e-15, maxit = 1e7, standardize = TRUE333  )334  cl <- as.vector(coef(fit_lasso))335  emit("lasso_b_cons",   cl[1])336  emit("lasso_b_price",  cl[2])337  emit("lasso_b_z1",     cl[3])338  emit("lasso_b_z2",     cl[4])339  emit("lasso_b_orders", cl[5])340341  fit_enet <- glmnet::glmnet(342    Xen, yen, alpha = 0.4, lambda = 0.02,343    thresh = 1e-15, maxit = 1e7, standardize = TRUE344  )345  ce <- as.vector(coef(fit_enet))346  emit("enet_b_cons",   ce[1])347  emit("enet_b_price",  ce[2])348  emit("enet_b_z1",     ce[3])349  emit("enet_b_z2",     ce[4])350  emit("enet_b_orders", ce[5])351} else {352  cat("glmnet not installed — skipping elastic-net fixtures\n")353}354355# ------------------------------------------------------- gradient boosting356# xgboost exact-greedy at fixed settings, no subsampling — deterministic.357# Complete-case features only (missing-value default-direction search358# differs between implementations).359if (requireNamespace("xgboost", quietly = TRUE)) {360  # Full sample: these variables have no missing values, so the engine's361  # listwise deletion keeps all 60 rows.362  Xgb <- as.matrix(data[, c("z1", "z2", "orders")])363  ygb <- log(data$revenue)364  base <- mean(ygb)365  booster <- xgboost::xgboost(366    x = Xgb, y = ygb,367    nrounds = 20, max_depth = 3, learning_rate = 0.3,368    reg_lambda = 1, reg_alpha = 0, min_child_weight = 1,369    subsample = 1, colsample_bytree = 1,370    tree_method = "exact", base_score = base,371    nthread = 1, verbosity = 0372  )373  pred <- predict(booster, Xgb)374  emit("gbm_rmse", sqrt(mean((ygb - pred)^2)))375  emit("gbm_pred_1",  pred[1])376  emit("gbm_pred_17", pred[17])377  emit("gbm_pred_42", pred[42])378  emit("gbm_pred_57", pred[57])379} else {380  cat("xgboost not installed — skipping boosting fixtures\n")381}382383# -------------------------------------------------------------- correlate384emit("corr_rev_price", cor(d$revenue, d$price))385emit("corr_rev_logrev", cor(d$revenue, d$log_rev))386387# --------------------------------------------------------- distributions388emit("dist_pchisq_3p8_1", pchisq(3.8, 1))389emit("dist_pchisq_25_4",  pchisq(25, 4))390emit("dist_qnorm_0p975",  qnorm(0.975))391emit("dist_pt_2p5_df10",   pt(2.5, 10))392emit("dist_pt_m1p3_df3",   pt(-1.3, 3))393emit("dist_pt_0p05_df57",  pt(0.05, 57))394emit("dist_pf_3p7_2_30",   pf(3.7, 2, 30))395emit("dist_pf_0p5_5_100",  pf(0.5, 5, 100))396emit("dist_qt_0p975_df12", qt(0.975, 12))397emit("dist_qt_0p995_df4",  qt(0.995, 4))398emit("dist_pnorm_1p64",    pnorm(1.64))399400# ------------------------------------------------------------- .dta files401# haven-written fixtures for the native DTA reader: same dataset plus a402# string column, in formats 118 (version 14) and 117 (version 13).403if (requireNamespace("haven", quietly = TRUE)) {404  dta <- data405  dta$firm_name <- paste0("firm_", data$firm_id)406  dta$firm_name[5] <- ""   # string missing407  haven::write_dta(dta, file.path(out_dir, "regression_v118.dta"), version = 14)408  haven::write_dta(dta, file.path(out_dir, "regression_v117.dta"), version = 13)409  cat("wrote .dta fixtures (117, 118)\n")410} else {411  cat("haven not installed — skipping .dta fixtures\n")412}413414writeLines(lines, sink_path)415cat("wrote", csv_path, "\n")416cat("wrote", sink_path, "with", length(lines), "expected values\n")417