tests/test.sh exit code (0 → resolved); the classification below is post-hoc and cannot change it.cd /app && ls -la && echo "---OUTPUTS---" && ls -la outputs/ 2>/dev/null && echo "---DATA---" && ls -la data/
total 24 drwxrws--- 4 h2tagent h2tagent 4096 Jun 20 17:46 . drwxr-xr-x 28 root root 4096 Jun 20 18:18 .. -rw-rw---- 1 h2tagent h2tagent 8171 Jun 20 17:44 analysis.R drwxrws--- 2 h2tagent h2tagent 4096 Jun 20 17:46 data drwxrws--- 2 h2tagent h2tagent 4096 Jun 20 17:46 outputs ---OUTPUTS--- total 8 drwxrws--- 2 h2tagent h2tagent 4096 Jun 20 17:46 . drwxrws--- 4 h2tagent h2tagent 4096 Jun 20 17:46 .. ---DATA--- total 598 drwxrws--- 2 h2tagent h2tagent 4096 Jun 20 17:46 . drwxrws--- 4 h2tagent h2tagent 4096 Jun 20 17:46 .. -rw-rw---- 1 h2tagent h2tagent 603612 Jun 20 17:44 coffee_ratings.csv
cd /app && cat analysis.R
#!/usr/bin/env Rscript
# Coffee outlier audit -- first draft
# ===================================
# Quick first pass before the deadline. Loaded the CSV, threw mean / sd / IQR
# at each column, used standardised residuals to flag influential lots, and
# took simple country means. Most of this should be good enough but the
# numbers feel off in a few places.
suppressPackageStartupMessages({
library(dplyr)
library(tidyr)
library(readr)
library(stringr)
library(jsonlite)
library(purrr)
})
DATA_PATH <- "/app/data/coffee_ratings.csv"
OUTPUT_DIR <- "/app/outputs"
dir.create(OUTPUT_DIR, showWarnings = FALSE, recursive = TRUE)
raw <- read_csv(DATA_PATH, show_col_types = FALSE)
n_input <- nrow(raw)
dat <- raw %>% filter(total_cup_points > 0)
n_after <- nrow(dat)
dat <- dat %>% mutate(row_id = as.integer(seq_len(n()) - 1L))
# ---- Altitude
alt <- dat$altitude_mean_meters
alt_pos <- alt[!is.na(alt) & alt > 0]
q <- quantile(alt_pos, c(0.25, 0.75))
iqr_alt <- q[[2]] - q[[1]]
lo_m <- q[[1]] - 3 * iqr_alt
hi_m <- q[[2]] + 3 * iqr_alt
flag_alt <- !is.na(alt) & (alt < lo_m | alt > hi_m)
n_flag_alt <- sum(flag_alt)
alt_corrected <- rep(NA_real_, nrow(dat))
unit_corrected <- rep(FALSE, nrow(dat))
n_unit_corrected <- 0L
# ---- Defect counts
defect_summary <- function(x) {
q <- quantile(x, c(0.25, 0.75), na.rm = TRUE)
upper <- q[[2]] + 3 * (q[[2]] - q[[1]])
flagged <- !is.na(x) & x > upper
list(upper_threshold = upper, n = sum(flagged), flag_vec = flagged)
}
d_cat1 <- defect_summary(dat$category_one_defects)
d_cat2 <- defect_summary(dat$category_two_defects)
d_quak <- defect_summary(dat$quakers)
flag_defect <- d_cat1$flag_vec | d_cat2$flag_vec | d_quak$flag_vec
# ---- Multivariate grades (every grade column)
g_all <- c("aroma", "flavor", "aftertaste", "acidity",
"body", "balance", "uniformity", "clean_cup",
"sweetness", "cupper_points")
G <- as.matrix(dat[, g_all])
G_complete <- complete.cases(G)
mu <- colMeans(G[G_complete, ])
S <- cov(G[G_complete, ])
md2 <- rep(NA_real_, nrow(G))
md2[G_complete] <- mahalanobis(G[G_complete, ], mu, S, tol = 1e-30)
md2_thresh <- qchisq(0.999, df = length(g_all))
flag_mv <- !is.na(md2) & md2 > md2_thresh
n_flag_mv <- sum(flag_mv)
# ---- Influence: standardised residuals
fit_df <- dat[G_complete, ]
fit <- lm(total_cup_points ~ aroma + flavor + aftertaste + acidity +
body + balance + cupper_points, data = fit_df)
rstd <- rstandard(fit)
infl_local <- abs(rstd) > 2
n_fit <- length(rstd)
p_fit <- length(coef(fit))
infl_full <- rep(FALSE, nrow(dat))
infl_full[G_complete] <- infl_local
n_high_lev <- sum(infl_full)
n_high_cook <- sum(infl_full)
n_both <- sum(infl_full)
top_idx <- order(-abs(rstd))[1:10]
top10_row_ids <- as.integer(dat$row_id[which(G_complete)[top_idx]])
# ---- Country ranking (raw means)
country_tbl <- dat %>%
filter(!is.na(country_of_origin)) %>%
group_by(country = country_of_origin) %>%
summarise(n = n(),
raw_mean = mean(total_cup_points),
.groups = "drop") %>%
filter(n >= 10) %>%
arrange(desc(raw_mean)) %>%
mutate(raw_rank = rank(-raw_mean, ties.method = "min"),
trimmed_mean = raw_mean,
trimmed_rank = raw_rank,
abs_rank_change = 0L)
n_countries <- nrow(country_tbl)
n_changes_ge_2 <- 0L
max_change <- 0L
# ---- Composite + sensitivity
composite <- flag_alt | flag_mv | infl_full
keep_for_sens <- !composite & G_complete
fit_drop <- lm(total_cup_points ~ aroma + flavor + aftertaste + acidity +
body + balance + cupper_points, data = dat[keep_for_sens, ])
coef_full <- unname(coef(fit)["flavor"])
coef_dropped <- unname(coef(fit_drop)["flavor"])
delta_pct <- 100 * (coef_dropped - coef_full) / coef_full
# ---- Flag interactions
A <- flag_alt; M <- flag_mv; D <- flag_defect; I <- infl_full
total_flags <- as.integer(A) + as.integer(M) + as.integer(D) + as.integer(I)
flag_interactions <- list(
n_with_zero_flags = unbox(as.integer(sum(total_flags == 0L))),
…[truncated]cd /app && head -3 data/coffee_ratings.csv && echo "---COLS---" && head -1 data/coffee_ratings.csv | tr ',' '\n' | nl
total_cup_points,species,owner,country_of_origin,farm_name,lot_number,mill,ico_number,company,altitude,region,producer,number_of_bags,bag_weight,in_country_partner,harvest_year,grading_date,owner_1,variety,processing_method,aroma,flavor,aftertaste,acidity,body,balance,uniformity,clean_cup,sweetness,cupper_points,moisture,category_one_defects,quakers,color,category_two_defects,expiration,certification_body,certification_address,certification_contact,unit_of_measurement,altitude_low_meters,altitude_high_meters,altitude_mean_meters
90.58,Arabica,metad plc,Ethiopia,metad plc,NA,metad plc,2014/2015,metad agricultural developmet plc,1950-2200,guji-hambela,METAD PLC,300,60 kg,METAD Agricultural Development plc,2014,"April 4th, 2015",metad plc,NA,Washed / Wet,8.67,8.83,8.67,8.75,8.5,8.42,10,10,10,8.75,0.12,0,0,Green,0,"April 3rd, 2016",METAD Agricultural Development plc,309fcf77415a3661ae83e027f7e5f05dad786e44,19fef5a731de2db57d16da10287413f5f99bc2dd,m,1950,2200,2075
89.92,Arabica,metad plc,Ethiopia,metad plc,NA,metad plc,2014/2015,metad agricultural developmet plc,1950-2200,guji-hambela,METAD PLC,300,60 kg,METAD Agricultural Development plc,2014,"April 4th, 2015",metad plc,Other,Washed / Wet,8.75,8.67,8.5,8.58,8.42,8.42,10,10,10,8.58,0.12,0,0,Green,1,"April 3rd, 2016",METAD Agricultural Development plc,309fcf77415a3661ae83e027f7e5f05dad786e44,19fef5a731de2db57d16da10287413f5f99bc2dd,m,1950,2200,2075
---COLS---
1 total_cup_points
2 species
3 owner
4 country_of_origin
5 farm_name
6 lot_number
7 mill
8 ico_number
9 company
10 altitude
11 region
12 producer
13 number_of_bags
14 bag_weight
15 in_country_partner
16 harvest_year
17 grading_date
18 owner_1
19 variety
20 processing_method
21 aroma
22 flavor
23 aftertaste
24 acidity
25 body
26 balance
27 uniformity
28 clean_cup
29 sweetness
30 cupper_points
31 moisture
32 category_one_defects
33 quakers
34 color
35 category_two_defects
36 expiration
37 certification_body
38 certification_address
39 certification_contact
40 unit_of_measurement
41 altitude_low_meters
42 altitude_high_meters
43 altitude_mean_meterscd /app && R --version | head -1 && echo "---PKGS---" && Rscript -e 'for (p in c("dplyr","tidyr","readr","stringr","jsonlite","purrr","MASS")) cat(p, as.character(packageVersion(p)), "\n")' 2>&1R version 4.4.2 (2024-10-31) -- "Pile of Leaves" ---PKGS--- dplyr 1.1.4 tidyr 1.3.1 readr 2.1.5 stringr 1.5.1 jsonlite 1.9.0 purrr 1.0.4 MASS 7.3.64
cd /app && Rscript -e '
suppressPackageStartupMessages({library(readr); library(dplyr)})
raw <- read_csv("data/coffee_ratings.csv", show_col_types = FALSE)
cat("n_input =", nrow(raw), "\n\n")
grades <- c("aroma","flavor","aftertaste","acidity","body","balance","uniformity","clean_cup","sweetness","cupper_points")
# Find zero-score rows
allzero <- rowSums(raw[,grades] == 0, na.rm=TRUE) == length(grades)
cat("rows with ALL 10 grades == 0:", sum(allzero, na.rm=TRUE), "\n")
cat("rows with total_cup_points == 0:", sum(raw$total_cup_points == 0, na.rm=TRUE), "\n")
cat("rows with total_cup_points <= 0:", sum(raw$total_cup_points <= 0, na.rm=TRUE), "\n\n")
cat("Summary of grade columns:\n")
print(sapply(raw[,grades], function(x) c(min=min(x,na.rm=T), med=median(x,na.rm=T), max=max(x,na.rm=T), sd=round(sd(x,na.rm=T),3))))
'n_input = 1339
rows with ALL 10 grades == 0: 1
rows with total_cup_points == 0: 1
rows with total_cup_points <= 0: 1
Summary of grade columns:
aroma flavor aftertaste acidity body balance uniformity clean_cup sweetness
min 0.000 0.000 0.000 0.00 0.00 0.000 0.000 0.000 0.000
med 7.580 7.580 7.420 7.58 7.50 7.500 10.000 10.000 10.000
max 8.750 8.830 8.670 8.75 8.58 8.750 10.000 10.000 10.000
sd 0.378 0.398 0.404 0.38 0.37 0.409 0.555 0.764 0.616
cupper_points
min 0.000
med 7.500
max 10.000
sd 0.473cd /app && Rscript -e '
suppressPackageStartupMessages({library(readr); library(dplyr)})
raw <- read_csv("data/coffee_ratings.csv", show_col_types = FALSE)
dat <- raw %>% filter(total_cup_points > 0)
alt <- dat$altitude_mean_meters
cat("altitude_mean_meters: NA count =", sum(is.na(alt)), " of", nrow(dat), "\n")
cat("n positive =", sum(!is.na(alt) & alt>0), " n zero =", sum(!is.na(alt) & alt==0), "\n")
cat("quantiles (raw m):\n"); print(quantile(alt, c(0,.01,.25,.5,.75,.9,.99,1), na.rm=TRUE))
la <- log10(alt[!is.na(alt) & alt>0])
q <- quantile(la, c(.25,.75)); iqr <- q[2]-q[1]
lo <- q[1]-3*iqr; hi <- q[2]+3*iqr
cat("\nlog10 Q1,Q3 =", round(q,4), " IQR =", round(iqr,4), "\n")
cat("log10 fence = [", round(lo,4), ",", round(hi,4), "]\n")
cat("meters fence = [", round(10^lo,2), ",", round(10^hi,2), "]\n")
flg <- !is.na(alt) & (alt < 10^lo | alt > 10^hi)
cat("n flagged outside log fence =", sum(flg), "\n")
cat("\nFlagged altitude values (mean_m) and raw altitude string:\n")
fd <- dat[flg, c("altitude","altitude_mean_meters","unit_of_measurement")]
print(as.data.frame(fd), row.names=FALSE)
'altitude_mean_meters: NA count = 230 of 1338
n positive = 1108 n zero = 0
quantiles (raw m):
0% 1% 25% 50% 75% 90% 99% 100%
1.00 1.77 1100.00 1310.64 1600.00 1799.30 3280.00 190164.00
log10 Q1,Q3 = 3.0414 3.2041 IQR = 0.1627
log10 fence = [ 2.5532 , 3.6923 ]
meters fence = [ 357.45 , 4923.82 ]
n flagged outside log fence = 51
Flagged altitude values (mean_m) and raw altitude string:
altitude altitude_mean_meters unit_of_measurement
1.2 12.0000 m
1.2 12.0000 m
-1 1.0000 m
350 350.0000 m
170 170.0000 m
approx. 1'500 metres a.s.l. 250.5000 m
1 1.0000 m
175 175.0000 m
16.8 168.0000 m
approx.1'500 metres a.s.l. 250.5000 m
1 1.0000 m
350 350.0000 m
11000 metros 11000.0000 m
518 157.8864 ft
1.65 165.0000 m
1 1.0000 m
200 m 200.0000 m
518 157.8864 ft
1 1.0000 m
200 m 200.0000 m
1 1.0000 m
1 1.0000 m
1 1.0000 m
1.3 13.0000 m
518 157.8864 ft
1 1.0000 m
1 1.0000 m
1 1.0000 m
150 m 150.0000 m
190164 190164.0000 m
1.3 13.0000 m
160-200m 180.0000 m
1.25 125.0000 m
110公尺 110.0000 m
1 1.0000 m
200 200.0000 m
300 m 300.0000 m
100 100.0000 m
1100.00 mosl 110000.0000 m
1.85 185.0000 m
50 50.0000 m
280 280.0000 m
200 m 200.0000 m
150 m 150.0000 m
250 m 250.0000 m
250 m 250.0000 m
1901.64 190164.0000 m
200 200.0000 m
12oo 12.0000 m
~350m
…[truncated]cd /app && Rscript -e '
suppressPackageStartupMessages({library(readr); library(dplyr); library(stringr)})
raw <- read_csv("data/coffee_ratings.csv", show_col_types = FALSE)
dat <- raw %>% filter(total_cup_points > 0)
alt <- dat$altitude_mean_meters
la <- log10(alt[!is.na(alt) & alt>0])
q <- quantile(la, c(.25,.75)); iqr <- q[[2]]-q[[1]]
lo_m <- 10^(q[[1]]-3*iqr); hi_m <- 10^(q[[2]]+3*iqr)
cat("fence meters:", round(lo_m,4), round(hi_m,4), "\n")
flg <- !is.na(alt) & (alt < lo_m | alt > hi_m)
idx <- which(flg)
first_tok <- function(s){ m <- str_extract(s, "[0-9]+\\.?[0-9]*"); as.numeric(m) }
corrected <- rep(NA_real_, nrow(dat)); ncorr <- 0L
for (i in idx){
tok <- first_tok(dat$altitude[i])
if (is.na(tok)) next
cands <- c(tok/10, tok/100, tok)
hit <- cands[cands >= lo_m & cands <= hi_m]
if (length(hit) > 0){ corrected[i] <- hit[1]; ncorr <- ncorr + 1L }
}
cat("n_flagged =", sum(flg), " n_unit_corrected =", ncorr, "\n\n")
show <- data.frame(altitude=dat$altitude[idx], mean_m=alt[idx], corrected=corrected[idx])
print(show, row.names=FALSE)
'fence meters: 357.4463 4923.817
n_flagged = 51 n_unit_corrected = 7
altitude mean_m corrected
1.2 12.0000 NA
1.2 12.0000 NA
-1 1.0000 NA
350 350.0000 NA
170 170.0000 NA
approx. 1'500 metres a.s.l. 250.5000 NA
1 1.0000 NA
175 175.0000 NA
16.8 168.0000 NA
approx.1'500 metres a.s.l. 250.5000 NA
1 1.0000 NA
350 350.0000 NA
11000 metros 11000.0000 1100.00
518 157.8864 518.00
1.65 165.0000 NA
1 1.0000 NA
200 m 200.0000 NA
518 157.8864 518.00
1 1.0000 NA
200 m 200.0000 NA
1 1.0000 NA
1 1.0000 NA
1 1.0000 NA
1.3 13.0000 NA
518 157.8864 518.00
1 1.0000 NA
1 1.0000 NA
1 1.0000 NA
150 m 150.0000 NA
190164 190164.0000 1901.64
1.3 13.0000 NA
160-200m 180.0000 NA
1.25 125.0000 NA
110公尺 110.0000 NA
1 1.0000 NA
200 200.0000 NA
300 m 300.0000 NA
100 100.0000 NA
1100.00 mosl 110000.0000 1100.00
1.85 185.0000 NA
50 50.0000 NA
280 280.0000 NA
200 m 200.0000 NA
150 m 150.0000 NA
250 m 250.0000 NA
250 m 250.0000 NA
1901.64 190164.0000 1901.64
200 200.0000 NA
12oo 12.0000 NA
~350m 350.0000 NA
40 40.0000 NAcd /app && Rscript -e '
suppressPackageStartupMessages({library(readr); library(dplyr)})
raw <- read_csv("data/coffee_ratings.csv", show_col_types = FALSE)
dat <- raw %>% filter(total_cup_points > 0)
for (col in c("category_one_defects","category_two_defects","quakers")){
x <- dat[[col]]
cat("==", col, "== NA:", sum(is.na(x)), "\n")
cat(" pct zero:", round(mean(x==0, na.rm=TRUE)*100,1), "%\n")
print(table(x, useNA="ifany"))
cat(" q50,75,90,95,98,99:", round(quantile(x, c(.5,.75,.9,.95,.98,.99), na.rm=TRUE),2), "\n")
# Tukey raw
q <- quantile(x, c(.25,.75), na.rm=TRUE); up <- q[[2]]+3*(q[[2]]-q[[1]])
cat(" raw Tukey IQR3 upper:", up, " n>up:", sum(x>up, na.rm=TRUE), "\n\n")
}
'== category_one_defects == NA: 0 pct zero: 84.9 % x 0 1 2 3 4 5 6 7 8 9 10 11 12 15 20 23 1136 101 38 18 16 9 3 3 1 1 4 1 1 1 1 1 31 63 2 1 q50,75,90,95,98,99: 0 0 1 2 5 7.63 raw Tukey IQR3 upper: 0 n>up: 202 == category_two_defects == NA: 0 pct zero: 27.9 % x 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 373 207 181 134 118 73 44 41 29 23 21 8 13 9 9 6 6 7 2 3 20 21 22 23 24 26 27 28 29 30 31 32 34 38 40 45 47 55 8 2 1 2 1 3 1 1 2 2 1 1 1 1 1 1 1 1 q50,75,90,95,98,99: 2 4 9 13 20 26.63 raw Tukey IQR3 upper: 16 n>up: 43 == quakers == NA: 1 pct zero: 93 % x 0 1 2 3 4 5 6 7 8 9 11 <NA> 1243 39 30 5 5 5 4 3 1 1 1 1 q50,75,90,95,98,99: 0 0 0 1 2 5 raw Tukey IQR3 upper: 0 n>up: 94
cd /app && Rscript -e '
suppressPackageStartupMessages({library(readr); library(dplyr)})
raw <- read_csv("data/coffee_ratings.csv", show_col_types = FALSE)
dat <- raw %>% filter(total_cup_points > 0)
n <- nrow(dat)
for (col in c("category_one_defects","category_two_defects","quakers")){
x <- dat[[col]]; xn <- x[!is.na(x)]
pos <- xn[xn>0]
# Method A: Tukey k=3 on positive-only
qa <- quantile(pos, c(.25,.75)); upA <- qa[[2]]+3*(qa[[2]]-qa[[1]])
# Method B: Tukey k=1.5 on positive-only
upB <- qa[[2]]+1.5*(qa[[2]]-qa[[1]])
# Method C: 98th pctile of all
upC <- quantile(xn, .98)
cat(sprintf("%-22s n=%d posN=%d | posTukey3 up=%.1f nFlag=%d (%.1f%%) | posTukey1.5 up=%.1f nFlag=%d (%.1f%%) | p98=%.1f nFlag=%d (%.1f%%)\n",
col, length(xn), length(pos),
upA, sum(xn>upA), 100*sum(xn>upA)/n,
upB, sum(xn>upB), 100*sum(xn>upB)/n,
upC, sum(xn>upC), 100*sum(xn>upC)/n))
}
'category_one_defects n=1338 posN=202 | posTukey3 up=9.0 nFlag=12 (0.9%) | posTukey1.5 up=6.0 nFlag=17 (1.3%) | p98=5.0 nFlag=20 (1.5%) category_two_defects n=1338 posN=965 | posTukey3 up=18.0 nFlag=34 (2.5%) | posTukey1.5 up=12.0 nFlag=73 (5.5%) | p98=20.0 nFlag=23 (1.7%) quakers n=1337 posN=94 | posTukey3 up=9.0 nFlag=1 (0.1%) | posTukey1.5 up=6.0 nFlag=6 (0.4%) | p98=2.0 nFlag=25 (1.9%)
cd /app && Rscript -e '
suppressPackageStartupMessages({library(readr); library(dplyr)})
raw <- read_csv("data/coffee_ratings.csv", show_col_types = FALSE)
dat <- raw %>% filter(total_cup_points > 0)
g7 <- c("aroma","flavor","aftertaste","acidity","body","balance","cupper_points")
G <- as.matrix(dat[,g7]); cc <- complete.cases(G)
cat("complete cases on 7 grades:", sum(cc), "of", nrow(dat), "\n")
mu <- colMeans(G[cc,]); S <- cov(G[cc,])
md2 <- mahalanobis(G[cc,], mu, S)
thr <- qchisq(0.999, df=7)
cat("threshold chisq(0.999, df=7) =", round(thr,4), "\n")
cat("n flagged (7-grade classical MD) =", sum(md2>thr), "\n")
# compare to all-10 (draft)
g10 <- c("aroma","flavor","aftertaste","acidity","body","balance","uniformity","clean_cup","sweetness","cupper_points")
G10 <- as.matrix(dat[,g10]); cc10 <- complete.cases(G10)
S10 <- cov(G10[cc10,])
cat("\ncondition number cov(7) =", round(kappa(S),1), " | cov(10) =", round(kappa(S10),1), "\n")
cat("sd of cleanliness cols:", round(sapply(dat[c("uniformity","clean_cup","sweetness")], sd, na.rm=TRUE),3), "\n")
'complete cases on 7 grades: 1338 of 1338 threshold chisq(0.999, df=7) = 24.3219 n flagged (7-grade classical MD) = 40 condition number cov(7) = 22.1 | cov(10) = 67.1 sd of cleanliness cols: 0.485 0.715 0.554
cd /app && Rscript -e '
suppressPackageStartupMessages({library(readr); library(dplyr); library(stringr)})
raw <- read_csv("data/coffee_ratings.csv", show_col_types = FALSE)
dat <- raw %>% filter(total_cup_points > 0)
g7 <- c("aroma","flavor","aftertaste","acidity","body","balance","cupper_points")
cc <- complete.cases(as.matrix(dat[,g7]))
cat("rows complete on 7 grades:", sum(cc), "\n")
fit_df <- dat[cc,]
# altitude: corrected where available else mean; compute log10
alt <- fit_df$altitude_mean_meters
# approximate corrected: reuse fence correction quickly (use mean only for AIC test here)
logalt <- log10(alt); logalt[!is.finite(logalt)] <- NA
logalt[is.na(logalt)] <- mean(logalt, na.rm=TRUE)
fit_df$log_alt <- logalt
for (col in c("category_one_defects","category_two_defects","quakers")){
x <- fit_df[[col]]; x[is.na(x)] <- mean(x, na.rm=TRUE); fit_df[[paste0("l1p_",col)]] <- log1p(x)
}
m1 <- lm(total_cup_points ~ aroma+flavor+aftertaste+acidity+body+balance+cupper_points, data=fit_df)
m2 <- update(m1, . ~ . + log_alt)
m3 <- lm(total_cup_points ~ aroma+flavor+aftertaste+acidity+body+balance+cupper_points+l1p_category_one_defects+l1p_category_two_defects+l1p_quakers, data=fit_df)
cat(sprintf("AIC grades_only=%.4f grades_altitude=%.4f grades_defects=%.4f\n", AIC(m1),AIC(m2),AIC(m3)))
cat("resid df / n used:", nobs(m1), nobs(m2), nobs(m3), "\n")
'rows complete on 7 grades: 1338 AIC grades_only=4459.3615 grades_altitude=4461.2746 grades_defects=4418.6801 resid df / n used: 1338 1338 1338
cd /app && Rscript -e '
suppressPackageStartupMessages({library(readr); library(dplyr)})
raw <- read_csv("data/coffee_ratings.csv", show_col_types = FALSE)
dat <- raw %>% filter(total_cup_points > 0)
for (tr in c(0.1, 0.2)){
ct <- dat %>% filter(!is.na(country_of_origin)) %>%
group_by(country=country_of_origin) %>%
summarise(n=n(), raw_mean=mean(total_cup_points), trimmed_mean=mean(total_cup_points, trim=tr), .groups="drop") %>%
filter(n>=10) %>%
mutate(raw_rank=rank(-raw_mean, ties.method="min"),
trimmed_rank=rank(-trimmed_mean, ties.method="min"),
chg=abs(raw_rank-trimmed_rank))
cat(sprintf("trim=%.2f: n_countries=%d n_changes_ge2=%d max_chg=%d\n", tr, nrow(ct), sum(ct$chg>=2), max(ct$chg)))
}
cat("\nCountries with >=10 lots:", nrow(dat %>% filter(!is.na(country_of_origin)) %>% count(country_of_origin) %>% filter(n>=10)), "\n")
'trim=0.10: n_countries=21 n_changes_ge2=2 max_chg=2 trim=0.20: n_countries=21 n_changes_ge2=2 max_chg=2 Countries with >=10 lots: 21
command: create
path: /app/analysis.R
file_text: #!/usr/bin/env Rscript
# Coffee Quality Outlier Audit -- shape-aware rewrite
# ===================================================
# Each column is audited with a method that matches its distribution:
# * altitude -> Tukey fence on log10 scale (right-skewed, heavy tail)
# * defects -> 98th-percentile upper-tail cut (mass-at-zero counts)
# * grades -> classical Mahalanobis on the 7 primary SCA attributes only
# * influence -> AIC-selected OLS, leverage>2p/n AND Cook's D>4/n
# * country -> raw mean vs 20% trimmed mean rank comparison
# The script is self-contained and reproduces every artifact from a clean
# /app/outputs/ directory.
suppressPackageStartupMessages({
library(dplyr)
library(tidyr)
library(readr)
library(stringr)
library(jsonlite)
library(purrr)
})
DATA_PATH <- "/app/data/coffee_ratings.csv"
OUTPUT_DIR <- "/app/outputs"
dir.create(OUTPUT_DIR, showWarnings = FALSE, recursive = TRUE)
GRADES7 <- c("aroma", "flavor", "aftertaste", "acidity",
"body", "balance", "cupper_points")
GRADES10 <- c(GRADES7, "uniformity", "clean_cup", "sweetness")
DEFECTS <- c("category_one_defects", "category_two_defects", "quakers")
# ----------------------------------------------------------------------------
# 1. Load + sentinel drop
# ----------------------------------------------------------------------------
raw <- read_csv(DATA_PATH, show_col_types = FALSE)
n_input <- nrow(raw)
# The withdrawn submission has every one of the ten grades recorded as zero.
all_grades_zero <- rowSums(raw[, GRADES10] == 0, na.rm = TRUE) == length(GRADES10)
dat <- raw[!all_grades_zero, , drop = FALSE]
n_after <- nrow(dat)
dat <- dat %>% mutate(row_id = as.integer(seq_len(n()) - 1L))
# ----------------------------------------------------------------------------
# 2. Altitude: Tukey fence on the log10 scale (k = 3), back-transformed
# ----------------------------------------------------------------------------
alt <- dat$altitude_mean_meters
log_alt_pos <- log10(alt[!is.na(alt) & alt > 0])
qa <- quantile(log_alt_pos, c(0.25, 0.75), names = FALSE)
iqr_a <- qa[2] - qa[1]
lo_log <- qa[1] - 3 * iqr_a
hi_log <- qa[2] + 3 * iqr_a
lo_m <- 10^lo_log
hi_m <- 10^hi_log
flag_alt <- !is.na(alt) & (alt < lo_m | alt > hi_m)
n_flag_alt <- sum(flag_alt)
# Unit-slip recovery: on each flagged row, test power-of-ten corrections of the
# first numeric token in the raw `altitude` string (/10, then /100, then as-is)
# and keep the first candidate that lands inside the fence.
first_numeric_token <- function(s) suppressWarnings(as.numeric(str_extract(s, "[0-9]+\\.?[0-9]*")))
alt_corrected <- rep(NA_real_, nrow(dat))
for (i in which(flag_alt)) {
tok <- first_numeric_token(dat$altitude[i])
if (is.na(tok)) next
candidates <- c(tok / 10, tok / 100, tok)
in_fence <- candidates[candidates >= lo_m & candidates <= hi_m]
if (length(in_fence) > 0) alt_corrected[i] <- in_fence[1]
}
n_unit_corrected <- sum(!is.na(alt_corrected))
# ----------------------------------------------------------------------------
# 3. Defects: 98th-percentile upper-tail cut per column (mass-at-zero counts)
# ----------------------------------------------------------------------------
defect_tail <- function(x) {
thr <- quantile(x, 0.98, na.rm = TRUE, names = FALSE)
flagged <- !is.na(x) & x > thr
list(upper_threshold = thr, n = sum(flagged), flag_vec = flagged)
}
d_list <- lapply(DEFECTS, function(col) defect_tail(dat[[col]]))
names(d_list) <- DEFECTS
flag_defect <- Reduce(`|`, lapply(d_list, `[[`, "flag_vec"))
# ----------------------------------------------------------------------------
# 4. Multivariate grades: classical Mahalanobis on the 7 primary attributes
# ----------------------------------------------------------------------------
G <- as.matrix(dat[, GRADES7])
G_complete <- complete.cases(G)
mu <- colMeans(G[G_complete, ])
S <- cov(G[G_complete, ])
md2 <- rep(NA_real_, nrow(dat))
md2[G_complete
…[truncated]Existing file was successfully overwritten at /app/analysis.R
cd /app && rm -rf outputs && mkdir -p outputs && Rscript analysis.R 2>&1
Done: n_input=1339 n_after=1338 | selected=grades_defects | alt flag=51 corr=7 | mv=40 | defect=63 influence: lev=110 cook=66 both=35 | boot reps=500 delta_pct=-20.748 [-44.328, 6.247]
cd /app && cat outputs/outlier_report.json
{
"n_input": 1339,
"n_after_zero_score_drop": 1338,
"methods": {
"altitude": "Tukey/IQR fence (k=3) on log10(altitude_mean_meters) over positive values, back-transformed to meters via 10^(); flagged rows unit-corrected by testing first-token /10,/100,as-is and keeping the first candidate inside the fence.",
"defects": "Per-column 98th-percentile upper-tail cut on the mass-at-zero counts; a lot is flagged if any of the three columns exceeds its threshold.",
"multivariate_grades": "Classical Mahalanobis distance (sample mean/covariance) on the 7 primary SCA attributes only; chi-squared 99.9% upper-tail threshold with df=7.",
"influence": "Lowest finite-AIC OLS among grades_only / grades_altitude / grades_defects; influence = high leverage (>2p/n) AND high Cook's D (>4/n) on the selected fit.",
"country_ranking": "Countries with >=10 lots: raw mean vs 20% trimmed mean of total_cup_points, comparing descending ranks (ties.method='min')."
},
"altitude": {
"lower_bound_m": 357.446289062499,
"upper_bound_m": 4923.8166791886,
"n_flagged": 51,
"n_unit_corrected": 7
},
"defects": {
"category_one_defects": {
"upper_threshold": 5,
"n_flagged": 20
},
"category_two_defects": {
"upper_threshold": 20,
"n_flagged": 23
},
"quakers": {
"upper_threshold": 2,
"n_flagged": 25
}
},
"multivariate_grades": {
"columns_used": ["aroma", "flavor", "aftertaste", "acidity", "body", "balance", "cupper_points"],
"threshold": 24.3218863478569,
"n_flagged": 40
},
"influence": {
"selected_model": "grades_defects",
"aic_grades_only": 4459.36151596946,
"aic_grades_altitude": 4461.29232958899,
"aic_grades_defects": 4418.68007429542,
"leverage_threshold": 0.0164424514200299,
"cook_threshold": 0.00298953662182362,
"n_high_leverage": 110,
"n_high_cook": 66,
"n_both": 35,
"top10_cook_row_ids": [1309, 1308, 1333, 1295, 1306, 1260, 1307, 1302, 1291, 1259]
},
"country_ranking": {
"n_countries_evaluated": 21,
"n_rank_changes_ge_2": 2,
"max_abs_rank_change": 2,
"per_country": [
{
"country": "Ethiopia",
"n": 44,
"raw_mean": 85.4840909090909,
"trimmed_mean": 85.3978571428571,
"raw_rank": 1,
"trimmed_rank": 1
},
{
"country": "United States",
"n": 10,
"raw_mean": 84.433,
"trimmed_mean": 85.3316666666667,
"raw_rank": 2,
"trimmed_rank": 2
},
{
"country": "Kenya",
"n": 25,
"raw_mean": 84.3096,
"trimmed_mean": 84.544,
"raw_rank": 3,
"trimmed_rank": 3
},
{
"country": "Uganda",
"n": 36,
"raw_mean": 83.4519444444445,
"trimmed_mean": 83.4477272727273,
"raw_rank": 4,
"trimmed_rank": 4
},
{
"country": "Colombia",
"n": 183,
"raw_mean": 83.1065573770492,
"trimmed_mean": 83.2674774774775,
"raw_rank": 5,
"trimmed_rank": 5
},
{
"country": "El Salvador",
"n": 21,
"raw_mean": 83.0528571428571,
"trimmed_mean": 83.0592307692308,
"raw_rank": 6,
"trimmed_rank": 7
},
{
"country": "China",
"n": 16,
"raw_mean": 82.9275,
"trimmed_mean": 83.267,
"raw_rank": 7,
"trimmed_rank": 6
},
{
"country": "Costa Rica",
"n": 51,
"raw_mean": 82.7890196078431,
"trimmed_mean": 83.058064516129,
"raw_rank": 8,
"trimmed_rank": 8
},
{
"country": "Thailand",
"n": 32,
"raw_mean": 82.57375,
"trimmed_mean": 82.713,
"raw_rank": 9,
"trimmed_rank": 10
},
{
"country": "Indonesia",
"n": 20,
"raw_mean": 82.5655,
"trimmed_mean": 82.76,
"raw_rank": 10,
"trimmed_rank
…[truncated]cd /app && Rscript -e '
suppressPackageStartupMessages({library(readr); library(jsonlite)})
fit <- readRDS("outputs/coffee_ols_selected.rds")
csv <- read_csv("outputs/outlier_flags.csv", show_col_types = FALSE)
js <- fromJSON("outputs/outlier_report.json")
n <- nobs(fit); p <- length(coef(fit))
cat("RELOAD fit: class=", class(fit), " n=", n, " p=", p, " terms:", paste(names(coef(fit)), collapse=","), "\n")
lev <- hatvalues(fit); ck <- cooks.distance(fit)
lev_flag <- lev > 2*p/n; cook_flag <- ck > 4/n
# fit rows align 1:1 with csv rows (all complete)
cat("CSV rows =", nrow(csv), " fit rows =", length(lev), "\n")
cat("leverage reconcile:", all(lev_flag == csv$high_leverage_flag), " sum:", sum(lev_flag), "vs", sum(csv$high_leverage_flag), "\n")
cat("cook reconcile:", all(cook_flag == csv$high_cook_flag), " sum:", sum(cook_flag), "vs", sum(csv$high_cook_flag), "\n")
# composite reconcile
comp <- csv$altitude_outlier_flag | csv$multivariate_grade_outlier_flag | (csv$high_leverage_flag & csv$high_cook_flag)
cat("composite reconcile:", all(comp == csv$composite_drop_flag), "\n")
# top10 cook
ord <- order(ck, decreasing=TRUE)[1:10]
cat("top10 cook row_ids (recomputed):", paste(csv$row_id[ord], collapse=","), "\n")
cat("top10 in json: ", paste(js$influence$top10_cook_row_ids, collapse=","), "\n")
cat("top10 match:", all(csv$row_id[ord] == js$influence$top10_cook_row_ids), "\n")
# thresholds match json
cat("lev_thresh json=", js$influence$leverage_threshold, " recomputed=", 2*p/n, "\n")
cat("cook_thresh json=", js$influence$cook_threshold, " recomputed=", 4/n, "\n")
'RELOAD fit: class= lm n= 1338 p= 11 terms: (Intercept),aroma,flavor,aftertaste,acidity,body,balance,cupper_points,l1p_category_one_defects,l1p_category_two_defects,l1p_quakers CSV rows = 1338 fit rows = 1338 leverage reconcile: TRUE sum: 110 vs 110 cook reconcile: TRUE sum: 66 vs 66 composite reconcile: TRUE top10 cook row_ids (recomputed): 1309,1308,1333,1295,1306,1260,1307,1302,1291,1259 top10 in json: 1309,1308,1333,1295,1306,1260,1307,1302,1291,1259 top10 match: TRUE lev_thresh json= 0.01644245 recomputed= 0.01644245 cook_thresh json= 0.002989537 recomputed= 0.002989537
cd /app && Rscript -e '
suppressPackageStartupMessages({library(readr); library(jsonlite); library(dplyr)})
csv <- read_csv("outputs/outlier_flags.csv", show_col_types = FALSE)
js <- fromJSON("outputs/outlier_report.json")
cat("CSV columns:\n"); print(names(csv))
cat("\nrow_id range:", min(csv$row_id), "-", max(csv$row_id), " unique:", length(unique(csv$row_id)), "\n")
cat("altitude_corrected_m non-NA count:", sum(!is.na(csv$altitude_corrected_m)), " (all within corrected-only?)\n")
cat(" corrected rows all flagged as altitude_outlier:", all(csv$altitude_outlier_flag[!is.na(csv$altitude_corrected_m)]), "\n")
cat(" corrected values:", paste(round(na.omit(csv$altitude_corrected_m),2), collapse=", "), "\n")
cat(" all corrected within fence [", round(js$altitude$lower_bound_m,2),",",round(js$altitude$upper_bound_m,2),"]:",
all(na.omit(csv$altitude_corrected_m) >= js$altitude$lower_bound_m & na.omit(csv$altitude_corrected_m) <= js$altitude$upper_bound_m), "\n\n")
# defect reconcile using raw data thresholds
raw <- read_csv("/app/data/coffee_ratings.csv", show_col_types=FALSE)
dat <- raw %>% filter(total_cup_points > 0)
for (col in c("category_one_defects","category_two_defects","quakers")){
thr <- js$defects[[col]]$upper_threshold
nf <- sum(dat[[col]] > thr, na.rm=TRUE)
cat(sprintf("%-22s thr=%-4g json_n=%d recompute(x>thr)=%d match=%s\n", col, thr, js$defects[[col]]$n_flagged, nf, js$defects[[col]]$n_flagged==nf))
}
# union defect
du <- (dat$category_one_defects > js$defects$category_one_defects$upper_threshold) |
(dat$category_two_defects > js$defects$category_two_defects$upper_threshold) |
(ifelse(is.na(dat$quakers),FALSE,dat$quakers) > js$defects$quakers$upper_threshold)
du[is.na(du)] <- FALSE
cat("union defect flag sum:", sum(du), " vs CSV:", sum(csv$defect_outlier_flag), "\n")
'CSV columns: [1] "row_id" "country_of_origin" [3] "total_cup_points" "altitude_outlier_flag" [5] "altitude_corrected_m" "defect_outlier_flag" [7] "multivariate_grade_outlier_flag" "high_leverage_flag" [9] "high_cook_flag" "composite_drop_flag" row_id range: 0 - 1337 unique: 1338 altitude_corrected_m non-NA count: 7 (all within corrected-only?) corrected rows all flagged as altitude_outlier: TRUE corrected values: 1100, 518, 518, 518, 1901.64, 1100, 1901.64 all corrected within fence [ 357.45 , 4923.82 ]: TRUE category_one_defects thr=5 json_n=20 recompute(x>thr)=20 match=TRUE category_two_defects thr=20 json_n=23 recompute(x>thr)=23 match=TRUE quakers thr=2 json_n=25 recompute(x>thr)=25 match=TRUE union defect flag sum: 63 vs CSV: 63
cd /app && Rscript -e '
suppressPackageStartupMessages({library(readr); library(jsonlite)})
csv <- read_csv("outputs/outlier_flags.csv", show_col_types = FALSE)
js <- fromJSON("outputs/outlier_report.json")
A <- csv$altitude_outlier_flag; M <- csv$multivariate_grade_outlier_flag
D <- csv$defect_outlier_flag; I <- csv$high_leverage_flag & csv$high_cook_flag
tot <- A+M+D+I
fi <- js$flag_interactions
chk <- function(lab, a, b) cat(sprintf(" %-20s json=%-5d recompute=%-5d %s\n", lab, a, b, ifelse(a==b,"OK","MISMATCH")))
cat("Flag interactions vs CSV:\n")
chk("zero", fi$n_with_zero_flags, sum(tot==0)); chk("one", fi$n_with_one_flag, sum(tot==1))
chk("two", fi$n_with_two_flags, sum(tot==2)); chk("three", fi$n_with_three_flags, sum(tot==3))
chk("four", fi$n_with_four_flags, sum(tot==4))
chk("alt_only", fi$n_alt_only, sum(A&!M&!I&!D)); chk("mv_only", fi$n_mv_only, sum(M&!A&!I&!D))
chk("influence_only", fi$n_influence_only, sum(I&!A&!M&!D)); chk("defect_only", fi$n_defect_only, sum(D&!A&!M&!I))
chk("alt_and_mv", fi$n_alt_and_mv, sum(A&M)); chk("alt_and_influence", fi$n_alt_and_influence, sum(A&I))
chk("mv_and_influence", fi$n_mv_and_influence, sum(M&I)); chk("alt_mv_influence", fi$n_alt_mv_influence, sum(A&M&I))
cat("composite = A|M|(lev&cook):", all(csv$composite_drop_flag == (A|M|I)), " n_composite=", sum(csv$composite_drop_flag), "\n")
cat("influence n_both matches I:", js$influence$n_both == sum(I), "\n")
'Flag interactions vs CSV: zero json=1186 recompute=1186 OK one json=122 recompute=122 OK two json=23 recompute=23 OK three json=7 recompute=7 OK four json=0 recompute=0 OK alt_only json=42 recompute=42 OK mv_only json=23 recompute=23 OK influence_only json=10 recompute=10 OK defect_only json=47 recompute=47 OK alt_and_mv json=4 recompute=4 OK alt_and_influence json=5 recompute=5 OK mv_and_influence json=13 recompute=13 OK alt_mv_influence json=1 recompute=1 OK composite = A|M|(lev&cook): TRUE n_composite= 105 influence n_both matches I: TRUE
cd /app && cp outputs/outlier_report.json /tmp/run1.json && rm -rf outputs && mkdir -p outputs && Rscript analysis.R >/dev/null 2>&1 && diff <(cat /tmp/run1.json) <(cat outputs/outlier_report.json) && echo "REPRODUCIBLE: JSON identical across runs" || echo "DIFF FOUND"
echo "---sentinel check---"
Rscript -e '
suppressPackageStartupMessages(library(readr))
raw <- read_csv("/app/data/coffee_ratings.csv", show_col_types=FALSE)
g10 <- c("aroma","flavor","aftertaste","acidity","body","balance","uniformity","clean_cup","sweetness","cupper_points")
az <- rowSums(raw[,g10]==0, na.rm=TRUE)==10
cat("all-grades-zero rows:", sum(az), " | their total_cup_points:", raw$total_cup_points[az], "\n")
cat("total_cup_points==0 rows:", sum(raw$total_cup_points==0), " | same row:", which(az)==which(raw$total_cup_points==0), "\n")
'REPRODUCIBLE: JSON identical across runs ---sentinel check--- all-grades-zero rows: 1 | their total_cup_points: 0 total_cup_points==0 rows: 1 | same row: TRUE
cd /app && Rscript -e '
fit <- readRDS("outputs/coffee_ols_selected.rds")
rn <- rownames(model.frame(fit))
cat("n model rows:", length(rn), "\n")
cat("first 5 rownames:", paste(head(rn,5), collapse=","), "\n")
cat("last 5 rownames:", paste(tail(rn,5), collapse=","), "\n")
cat("rownames == 1:1338 ?", all(rn == as.character(1:1338)), "\n")
cat("positional map: model row i -> row_id (i-1). Consistent since all 1338 complete & in order.\n")
# confirm response present for recompute
cat("has response total_cup_points in frame:", "total_cup_points" %in% names(model.frame(fit)), "\n")
cat("model stored (can recompute cooks.distance):", !is.null(fit$model), "\n")
'n model rows: 1338 first 5 rownames: 1,2,3,4,5 last 5 rownames: 1334,1335,1336,1337,1338 rownames == 1:1338 ? TRUE positional map: model row i -> row_id (i-1). Consistent since all 1338 complete & in order. has response total_cup_points in frame: TRUE model stored (can recompute cooks.distance): TRUE
cd /app && Rscript -e '
suppressPackageStartupMessages({library(readr); library(dplyr)})
raw <- read_csv("data/coffee_ratings.csv", show_col_types=FALSE)
dat <- raw %>% filter(total_cup_points>0)
alt <- dat$altitude_mean_meters; ap <- alt[!is.na(alt)&alt>0]
q <- quantile(ap,c(.25,.75)); iqr<-q[[2]]-q[[1]]
cat("DRAFT raw-meter fence: [", round(q[[1]]-3*iqr,1), ",", round(q[[2]]+3*iqr,1), "] -> n flagged:", sum(!is.na(alt)&(alt<q[[1]]-3*iqr|alt>q[[2]]+3*iqr)),"\n")
cat(" (lower bound is negative/meaningless; high-altitude farms >3100m wrongly flagged, decimal slips like 1.0 caught but unfixable)\n")
# draft MD on 10 cols vs 7
g10<-c("aroma","flavor","aftertaste","acidity","body","balance","uniformity","clean_cup","sweetness","cupper_points")
g7<-c("aroma","flavor","aftertaste","acidity","body","balance","cupper_points")
G10<-as.matrix(dat[,g10]); cc<-complete.cases(G10)
md10<-mahalanobis(G10[cc,],colMeans(G10[cc,]),cov(G10[cc,]),tol=1e-30)
cat("DRAFT MD(10 cols) flagged at chisq.999 df10:", sum(md10>qchisq(.999,10)), " vs 7-col:40\n")
# rstandard>2 count (draft influence)
fit<-lm(total_cup_points~aroma+flavor+aftertaste+acidity+body+balance+cupper_points,data=dat)
cat("DRAFT abs(rstandard)>2 count:", sum(abs(rstandard(fit))>2), " (~",round(100*sum(abs(rstandard(fit))>2)/nobs(fit),1),"% by construction, not influence)\n")
'DRAFT raw-meter fence: [ -400 , 3100 ] -> n flagged: 16 (lower bound is negative/meaningless; high-altitude farms >3100m wrongly flagged, decimal slips like 1.0 caught but unfixable) DRAFT MD(10 cols) flagged at chisq.999 df10: 76 vs 7-col:40 DRAFT abs(rstandard)>2 count: 57 (~ 4.3 % by construction, not influence)
command: create path: /app/outputs/audit_memo.md file_text: # Coffee Quality Outlier Audit , Method Memo The draft applied one off-the-shelf recipe to every column. Each block below says why that recipe breaks for the column's actual shape and what replaced it. All figures come from the re-executed `analysis.R` (1,339 input lots). ## Data prep One withdrawn submission has **all ten grades recorded as 0** (and `total_cup_points = 0`). Left in, it is a leverage bomb for every model and a fake extreme for every distance. I drop exactly that lot (identified by all-grades-zero, equivalent here to the single `total_cup_points == 0` row): **n_input = 1339 → n_after_zero_score_drop = 1338**. Survivors are numbered `row_id` 0–1337 in input order so the flag table aligns 1:1 with the fits. ## Altitude `altitude_mean_meters` is strongly right-skewed (median ≈ 1,311 m, max 190,164 m). The draft's Tukey fence on the **raw meter scale** gives `[-400, 3100]`: the lower bound is physically impossible, genuine high-grown lots above 3,100 m are flagged, and the decimal-slip errors are neither isolated nor repaired. I build the k = 3 fence on `log10` of the positive altitudes, then back-transform: **[357.4 m, 4923.8 m]**, flagging **51** lots. For each flagged row I take the first numeric token of the raw `altitude` string and test `÷10`, `÷100`, then as-is, keeping the first candidate inside the fence. That recovers **7** unit slips (e.g. `190164 → 1901.64`, `11000 metros → 1100`, `1100.00 mosl → 1100`); the rest (true lowland lots near 1 m, range text like `1'500`) stay `NA`. The corrected meters feed the altitude regression; the outlier flag is retained. ## Defects `category_one_defects` (84.9% zero), `category_two_defects` (27.9% zero) and `quakers` (93.0% zero) are mass-at-zero counts with a thin upper tail. The draft's raw Tukey fence collapses: with Q1 = Q3 = 0 for the two sparse columns the upper fence is **0**, so *every* nonzero count is "extreme" , **202** and **94** lots (~15% and ~7%), which is meaningless. I instead cut each column at its **98th percentile** and flag counts strictly above it: thresholds 5 / 20 / 2 flag **20 / 23 / 25** lots (1.5% / 1.7% / 1.9%), matching the "top 1–2%" tail. A lot is a defect outlier if any column trips (**63** lots). ## Multivariate grades The draft ran Mahalanobis on **all ten** grade columns. `uniformity`, `clean_cup` and `sweetness` are near-constant at 10 (huge mass at the cap), so they contribute almost no genuine covariance but inflate the covariance condition number (≈ 67 vs ≈ 22 for the seven primaries) and manufacture distance from cap deviations , 76 flags, many driven by clean-cup quirks. I use a **classical** Mahalanobis distance (sample mean and covariance, *not* MCD, as specified) on the **seven primary SCA attributes** only (`aroma, flavor, aftertaste, acidity, body, balance, cupper_points`) with a chi-squared 99.9% cutoff at df = 7 (**24.32**), flagging **40** lots. ## Influence `abs(rstandard) > 2` measures large *residuals* in `y`, not influence on the coefficients, and flags ~4% of rows (57) by construction regardless of leverage. I first choose the model by AIC among three specs on the 1,338 rows complete in the seven grades: grades_only **4459.4**, grades_altitude **4461.3**, grades_defects **4418.7** → **grades_defects** wins (log1p defect terms carry real signal; `total_cup_points` is the grade sum so the fit is a near-identity plus defect adjustment). With n = 1338, p = 11 I flag **high leverage** `h > 2p/n = 0.0164` (**110**) and **high Cook's D** `> 4/n = 0.00299` (**66**) separately; the influence flag is the **conjunction** (**35**), isolating points that are both unusual in X and actually move the fit. The selected `lm` is saved so the verifier can recompute both diagnostics. ## Country ranking The draft reported only raw means and never ran the robustness check it promised (`trimmed_mean = raw_mean`), so every rank change was forced to 0. For the **21** c …[truncated]
New file created successfully at /app/outputs/audit_memo.md
cd /app && echo "Word count: $(wc -w < outputs/audit_memo.md)" && echo "---headings---" && grep '^## ' outputs/audit_memo.md && echo "---files in outputs---" && ls -la outputs/
Word count: 878 ---headings--- ## Data prep ## Altitude ## Defects ## Multivariate grades ## Influence ## Country ranking ## Sensitivity ## Composite flags ---files in outputs--- total 232 drwxr-sr-x 2 h2tagent h2tagent 4096 Jun 20 18:23 . drwxrws--- 4 h2tagent h2tagent 4096 Jun 20 18:22 .. -rw-r--r-- 1 h2tagent h2tagent 5696 Jun 20 18:23 audit_memo.md -rw-r--r-- 1 h2tagent h2tagent 137274 Jun 20 18:22 coffee_ols_selected.rds -rw-r--r-- 1 h2tagent h2tagent 78269 Jun 20 18:22 outlier_flags.csv -rw-r--r-- 1 h2tagent h2tagent 6789 Jun 20 18:22 outlier_report.json
cd /app && Rscript -e '
suppressPackageStartupMessages({library(jsonlite)})
js <- fromJSON("outputs/outlier_report.json")
pc <- js$country_ranking$per_country
pc$chg <- abs(pc$raw_rank - pc$trimmed_rank)
cat("Countries moving >=2 ranks:\n")
print(pc[pc$chg>=2, c("country","raw_rank","trimmed_rank","chg")], row.names=FALSE)
'Countries moving >=2 ranks:
country raw_rank trimmed_rank chg
Taiwan 14 16 2
Guatemala 15 13 2command: str_replace
path: /app/analysis.R
old_str: write_json(results, file.path(OUTPUT_DIR, "outlier_report.json"),
auto_unbox = FALSE, pretty = TRUE, na = "null", digits = NA)
new_str: write_json(results, file.path(OUTPUT_DIR, "outlier_report.json"),
auto_unbox = FALSE, pretty = TRUE, na = "null", digits = NA)
# ----------------------------------------------------------------------------
# 11. Audit memo (data-driven; regenerated on every run)
# ----------------------------------------------------------------------------
qd <- quantile(alt[!is.na(alt) & alt > 0], c(0.25, 0.75), names = FALSE)
draft_alt_lo <- qd[1] - 3 * (qd[2] - qd[1]); draft_alt_hi <- qd[2] + 3 * (qd[2] - qd[1])
draft_alt_n <- sum(!is.na(alt) & (alt < draft_alt_lo | alt > draft_alt_hi))
draft_d1_n <- sum(dat$category_one_defects > 0, na.rm = TRUE)
draft_q_n <- sum(dat$quakers > 0, na.rm = TRUE)
G10 <- as.matrix(dat[, GRADES10]); cc10 <- complete.cases(G10)
md10 <- mahalanobis(G10[cc10, ], colMeans(G10[cc10, ]), cov(G10[cc10, ]), tol = 1e-30)
draft_md10_n <- sum(md10 > qchisq(0.999, df = length(GRADES10)))
draft_rstd_n <- sum(abs(rstandard(fit_grades_only)) > 2)
moving <- country_tbl %>% filter(abs_rank_change >= 2)
moving_txt <- paste(sprintf("%s (%d->%d)", moving$country, moving$raw_rank, moving$trimmed_rank),
collapse = ", ")
d1t <- d_list$category_one_defects$upper_threshold
d2t <- d_list$category_two_defects$upper_threshold
dqt <- d_list$quakers$upper_threshold
n_composite <- sum(composite)
memo <- c(
"# Coffee Quality Outlier Audit -- Method Memo",
"",
"The draft applied one off-the-shelf recipe to every column. Each block says",
"why that recipe breaks for the column's actual shape and what replaced it.",
"All figures are emitted by the re-executed `analysis.R`.",
"",
"## Data prep",
"",
sprintf("One withdrawn submission has **all ten grades recorded as 0** (and `total_cup_points = 0`). Left in, it is a leverage bomb for every model and a fake extreme for every distance. I drop exactly that lot (all-grades-zero, here the single `total_cup_points == 0` row): **n_input = %d -> n_after_zero_score_drop = %d**. Survivors are numbered `row_id` 0-%d in input order so the flag table aligns 1:1 with the fits.", n_input, n_after, n_after - 1L),
"",
"## Altitude",
"",
sprintf("`altitude_mean_meters` is strongly right-skewed (max 190,164 m). The draft's Tukey fence on the **raw meter scale** gives `[%.0f, %.0f]`: the lower bound is physically impossible, genuine high-grown lots are flagged, and decimal slips are never repaired (%d lots). I build the k = 3 fence on `log10` of the positive altitudes, then back-transform: **[%.1f m, %.1f m]**, flagging **%d** lots. For each flagged row I take the first numeric token of the raw `altitude` string and test `/10`, `/100`, then as-is, keeping the first candidate inside the fence. That recovers **%d** unit slips (e.g. 190164 -> 1901.64, '11000 metros' -> 1100, '1100.00 mosl' -> 1100); true lowland lots and range text stay `NA`. Corrected meters feed the altitude regression; the outlier flag is retained.", draft_alt_lo, draft_alt_hi, draft_alt_n, lo_m, hi_m, n_flag_alt, n_unit_corrected),
"",
"## Defects",
"",
sprintf("`category_one_defects` (84.9%% zero), `category_two_defects` (27.9%% zero) and `quakers` (93.0%% zero) are mass-at-zero counts with a thin upper tail. The draft's raw Tukey fence collapses: with Q1 = Q3 = 0 the upper fence is **0**, so *every* nonzero count is 'extreme' -- **%d** and **%d** lots, which is meaningless. I instead cut each column at its **98th percentile** and flag counts strictly above it: thresholds %g / %g / %g flag **%d / %d / %d** lots (~1.5-1.9%%), matching the top 1-2%% tail. A lot is a defect outlier if any column trips (**%d** lots).", draft_d1_n, draft_q_n, d1t, d2t, dqt, d_list$category_one_defects$n, d_list$category_two_defects$n, d_list$quakers$n, sum(flag_defect)),
"",
"## Multivariate grades",
"",
sprintf("The draft ran Mahalanob
…[truncated]The file /app/analysis.R has been edited successfully.
cd /app && rm -rf outputs && mkdir -p outputs && Rscript analysis.R 2>&1 && echo "---FILES---" && ls outputs/ && echo "---MEMO WORDS---" && wc -w < outputs/audit_memo.md && echo "---HEADINGS---" && grep -c '^## ' outputs/audit_memo.md
Done: n_input=1339 n_after=1338 | selected=grades_defects | alt flag=51 corr=7 | mv=40 | defect=63 influence: lev=110 cook=66 both=35 | boot reps=500 delta_pct=-20.748 [-44.328, 6.247] ---FILES--- audit_memo.md coffee_ols_selected.rds outlier_flags.csv outlier_report.json ---MEMO WORDS--- 782 ---HEADINGS--- 8
cd /app && cat outputs/audit_memo.md
# Coffee Quality Outlier Audit -- Method Memo The draft applied one off-the-shelf recipe to every column. Each block says why that recipe breaks for the column's actual shape and what replaced it. All figures are emitted by the re-executed `analysis.R`. ## Data prep One withdrawn submission has **all ten grades recorded as 0** (and `total_cup_points = 0`). Left in, it is a leverage bomb for every model and a fake extreme for every distance. I drop exactly that lot (all-grades-zero, here the single `total_cup_points == 0` row): **n_input = 1339 -> n_after_zero_score_drop = 1338**. Survivors are numbered `row_id` 0-1337 in input order so the flag table aligns 1:1 with the fits. ## Altitude `altitude_mean_meters` is strongly right-skewed (max 190,164 m). The draft's Tukey fence on the **raw meter scale** gives `[-400, 3100]`: the lower bound is physically impossible, genuine high-grown lots are flagged, and decimal slips are never repaired (16 lots). I build the k = 3 fence on `log10` of the positive altitudes, then back-transform: **[357.4 m, 4923.8 m]**, flagging **51** lots. For each flagged row I take the first numeric token of the raw `altitude` string and test `/10`, `/100`, then as-is, keeping the first candidate inside the fence. That recovers **7** unit slips (e.g. 190164 -> 1901.64, '11000 metros' -> 1100, '1100.00 mosl' -> 1100); true lowland lots and range text stay `NA`. Corrected meters feed the altitude regression; the outlier flag is retained. ## Defects `category_one_defects` (84.9% zero), `category_two_defects` (27.9% zero) and `quakers` (93.0% zero) are mass-at-zero counts with a thin upper tail. The draft's raw Tukey fence collapses: with Q1 = Q3 = 0 the upper fence is **0**, so *every* nonzero count is 'extreme' -- **202** and **94** lots, which is meaningless. I instead cut each column at its **98th percentile** and flag counts strictly above it: thresholds 5 / 20 / 2 flag **20 / 23 / 25** lots (~1.5-1.9%), matching the top 1-2% tail. A lot is a defect outlier if any column trips (**63** lots). ## Multivariate grades The draft ran Mahalanobis on **all ten** grade columns. `uniformity`, `clean_cup` and `sweetness` are near-constant at the 10 cap, adding little genuine covariance while inflating the covariance condition number (~67 vs ~22) and manufacturing distance from cap deviations -- **76** flags. I use a **classical** Mahalanobis distance (sample mean and covariance, not MCD) on the **seven primary SCA attributes** only (aroma, flavor, aftertaste, acidity, body, balance, cupper_points) with a chi-squared 99.9% cutoff at df = 7 (**24.32**), flagging **40** lots. ## Influence `abs(rstandard) > 2` measures large *residuals* in y, not influence on the coefficients, and flags ~57 rows by construction regardless of leverage. I first pick the model by AIC on the 1338 rows complete in the seven grades: grades_only **4459.4**, grades_altitude **4461.3**, grades_defects **4418.7** -> **grades_defects** wins. With n = 1338, p = 11 I flag **high leverage** h > 2p/n = 0.0164 (**110**) and **high Cook's D** > 4/n = 0.00299 (**66**) separately; the influence flag is the **conjunction** (**35**), isolating points both unusual in X and actually moving the fit. The selected `lm` is saved for recomputation. ## Country ranking The draft reported only raw means and never ran its promised robustness check (`trimmed_mean = raw_mean`), forcing every rank change to 0. For the **21** countries with >= 10 lots I compare the raw mean against a **20% trimmed mean** of `total_cup_points` and re-rank (descending, ties 'min'). **2** countries move >= 2 positions (max shift **2**): Taiwan (14->16), Guatemala (15->13) -- lots with low-scoring tails shift once trimming removes them. The full table is in the JSON. ## Sensitivity The composite drop set is `altitude OR multivariate OR (leverage AND Cook)` (defects deliberately excluded) -- **105** lots. Refitting **grades_only** before/after dropping them, the `flavor` coeffic …[truncated]
cd /app && cp outputs/outlier_report.json /tmp/r1.json && cp outputs/audit_memo.md /tmp/m1.md
rm -rf outputs && mkdir -p outputs && Rscript analysis.R >/dev/null 2>&1
echo "JSON reproducible: $(diff -q /tmp/r1.json outputs/outlier_report.json >/dev/null && echo YES || echo NO)"
echo "Memo reproducible: $(diff -q /tmp/m1.md outputs/audit_memo.md >/dev/null && echo YES || echo NO)"
echo "---final full reconciliation---"
Rscript -e '
suppressPackageStartupMessages({library(readr);library(jsonlite);library(dplyr)})
js<-fromJSON("outputs/outlier_report.json"); csv<-read_csv("outputs/outlier_flags.csv",show_col_types=FALSE)
fit<-readRDS("outputs/coffee_ols_selected.rds")
stopifnot(js$n_input==1339, js$n_after_zero_score_drop==1338, nrow(csv)==1338)
# schema key presence
need<-c("n_input","n_after_zero_score_drop","methods","altitude","defects","multivariate_grades","influence","country_ranking","sensitivity","flag_interactions")
cat("all top-level keys present:", all(need %in% names(js)),"\n")
cat("methods keys:", all(c("altitude","defects","multivariate_grades","influence","country_ranking") %in% names(js$methods)),"\n")
# model reconcile
n<-nobs(fit);p<-length(coef(fit))
lev<-hatvalues(fit)>2*p/n; ck<-cooks.distance(fit)>4/n
cat("lev/cook/composite reconcile:", all(lev==csv$high_leverage_flag)&&all(ck==csv$high_cook_flag)&&
all(csv$composite_drop_flag==(csv$altitude_outlier_flag|csv$multivariate_grade_outlier_flag|(csv$high_leverage_flag&csv$high_cook_flag))),"\n")
# partition sum
fi<-js$flag_interactions
cat("flag partition sums to 1338:", fi$n_with_zero_flags+fi$n_with_one_flag+fi$n_with_two_flags+fi$n_with_three_flags+fi$n_with_four_flags==1338,"\n")
# per-country internal consistency: ranks match means
pc<-js$country_ranking$per_country
cat("raw_rank consistent with raw_mean order:", all(pc$raw_rank==rank(-pc$raw_mean,ties.method="min")),"\n")
cat("trimmed_rank consistent with trimmed_mean order:", all(pc$trimmed_rank==rank(-pc$trimmed_mean,ties.method="min")),"\n")
cat("n_countries==nrow(per_country):", js$country_ranking$n_countries_evaluated==nrow(pc),"\n")
# CI ordering and delta inside? (not required but sanity)
cat("CI lower<=upper:", js$sensitivity$delta_pct_ci_lower<=js$sensitivity$delta_pct_ci_upper,"\n")
cat("delta_pct recompute:", all.equal(js$sensitivity$delta_pct, 100*(js$sensitivity$coef_flavor_dropped-js$sensitivity$coef_flavor_full)/js$sensitivity$coef_flavor_full),"\n")
cat("selected_model valid:", js$influence$selected_model %in% c("grades_only","grades_altitude","grades_defects"),"\n")
cat("top10 length 10 & integer:", length(js$influence$top10_cook_row_ids)==10 && all(js$influence$top10_cook_row_ids==as.integer(js$influence$top10_cook_row_ids)),"\n")
cat("NA country rows in csv:", sum(is.na(csv$country_of_origin)),"\n")
'JSON reproducible: YES Memo reproducible: YES ---final full reconciliation--- all top-level keys present: TRUE methods keys: TRUE lev/cook/composite reconcile: TRUE flag partition sums to 1338: TRUE raw_rank consistent with raw_mean order: TRUE trimmed_rank consistent with trimmed_mean order: TRUE n_countries==nrow(per_country): TRUE CI lower<=upper: TRUE delta_pct recompute: TRUE selected_model valid: TRUE top10 length 10 & integer: TRUE NA country rows in csv: 1
cd /app && Rscript -e '
suppressPackageStartupMessages({library(readr);library(jsonlite);library(dplyr)})
js<-fromJSON("outputs/outlier_report.json")
# Re-derive bootstrap independently to confirm determinism & n=1338, reps=500
raw<-read_csv("/app/data/coffee_ratings.csv",show_col_types=FALSE)
g10<-c("aroma","flavor","aftertaste","acidity","body","balance","uniformity","clean_cup","sweetness","cupper_points")
dat<-raw[!(rowSums(raw[,g10]==0,na.rm=TRUE)==10),]
csv<-read_csv("outputs/outlier_flags.csv",show_col_types=FALSE)
comp<-csv$composite_drop_flag
f<-total_cup_points~aroma+flavor+aftertaste+acidity+body+balance+cupper_points
set.seed(20260512); B<-500; n<-nrow(dat); dl<-numeric(0)
for(b in 1:B){idx<-sample.int(n,n,replace=TRUE); s<-dat[idx,]; cb<-comp[idx]; k<-s[!cb,]
if(nrow(k)<10) next
ff<-tryCatch(lm(f,s),error=function(e)NULL); fd<-tryCatch(lm(f,k),error=function(e)NULL)
if(is.null(ff)||is.null(fd)) next
cf<-coef(ff)["flavor"]; cd<-coef(fd)["flavor"]; if(is.na(cf)||is.na(cd)||cf==0) next
dl<-c(dl,100*(cd-cf)/cf)}
ci<-quantile(dl,c(.025,.975),names=FALSE)
cat("independent bootstrap: n=",n," reps=",length(dl)," CI=[",round(ci[1],4),",",round(ci[2],4),"]\n")
cat("matches JSON reps=500:",length(dl)==js$sensitivity$n_bootstrap_replicates,
" CI match:",isTRUE(all.equal(ci[1],js$sensitivity$delta_pct_ci_lower))&&isTRUE(all.equal(ci[2],js$sensitivity$delta_pct_ci_upper)),"\n")
# defect semantics: confirm > (strict) matches reported counts and >= would differ
for(col in c("category_one_defects","category_two_defects","quakers")){
t<-js$defects[[col]]$upper_threshold
cat(sprintf("%-22s thr=%g sum(x>thr)=%d (reported=%d) sum(x>=thr)=%d\n",col,t,
sum(dat[[col]]>t,na.rm=TRUE),js$defects[[col]]$n_flagged,sum(dat[[col]]>=t,na.rm=TRUE)))
}
'independent bootstrap: n= 1338 reps= 500 CI=[ -44.3281 , 6.247 ] matches JSON reps=500: TRUE CI match: TRUE category_one_defects thr=5 sum(x>thr)=20 (reported=20) sum(x>=thr)=29 category_two_defects thr=20 sum(x>thr)=23 (reported=23) sum(x>=thr)=31 quakers thr=2 sum(x>thr)=25 (reported=25) sum(x>=thr)=55
cd /app && Rscript -e '
suppressPackageStartupMessages(library(jsonlite))
fit<-readRDS("outputs/coffee_ols_selected.rds"); js<-fromJSON("outputs/outlier_report.json")
ck<-cooks.distance(fit)
ids<-js$influence$top10_cook_row_ids
vals<-ck[ids+1] # row_id = position-1
cat("top10 row_ids: ", paste(ids,collapse=", "),"\n")
cat("their Cook D: ", paste(round(vals,4),collapse=", "),"\n")
cat("strictly decreasing:", all(diff(vals)<0),"\n")
cat("these are the 10 largest overall:", setequal(ids, (order(ck,decreasing=TRUE)[1:10])-1),"\n")
'top10 row_ids: 1309, 1308, 1333, 1295, 1306, 1260, 1307, 1302, 1291, 1259 their Cook D: 0.3795, 0.15, 0.0979, 0.0936, 0.0765, 0.0531, 0.0472, 0.0457, 0.0361, 0.0313 strictly decreasing: TRUE these are the 10 largest overall: TRUE
cd /app && rm -rf outputs && mkdir -p outputs && Rscript -e 'options(warn=1); source("analysis.R")' 2>&1 | grep -iE "warn|error|cannot|fail" || echo "No warnings or errors."
echo "=== Deliverables in /app/outputs ==="
ls -la outputs/
echo "=== JSON validity ==="
Rscript -e '[redacted source](jsonlite::fromJSON("outputs/outlier_report.json")); cat("valid JSON\n")'No warnings or errors. === Deliverables in /app/outputs === total 231 drwxr-sr-x 2 h2tagent h2tagent 4096 Jun 20 18:26 . drwxrws--- 4 h2tagent h2tagent 4096 Jun 20 18:26 .. -rw-r--r-- 1 h2tagent h2tagent 5109 Jun 20 18:26 audit_memo.md -rw-r--r-- 1 h2tagent h2tagent 137274 Jun 20 18:26 coffee_ols_selected.rds -rw-r--r-- 1 h2tagent h2tagent 78269 Jun 20 18:26 outlier_flags.csv -rw-r--r-- 1 h2tagent h2tagent 6789 Jun 20 18:26 outlier_report.json === JSON validity === valid JSON
cd /app && echo "=== header + first 3 rows ===" && head -4 outputs/outlier_flags.csv
echo "=== a unit-corrected row ===" && awk -F, 'NR==1 || ($5!="" && $5!="NA")' outputs/outlier_flags.csv | head -3
echo "=== count of each flag (excl header) ==="
Rscript -e '
suppressPackageStartupMessages(library(readr)); d<-read_csv("outputs/outlier_flags.csv",show_col_types=FALSE)
cat("rows:",nrow(d),"\n")
for(c in c("altitude_outlier_flag","defect_outlier_flag","multivariate_grade_outlier_flag","high_leverage_flag","high_cook_flag","composite_drop_flag")) cat(sprintf(" %-34s TRUE=%d\n",c,sum(d[[c]])))
cat(" altitude_corrected_m non-NA:",sum(!is.na(d$altitude_corrected_m)),"\n")
cat(" col types:",paste(sapply(d,function(x)class(x)[1]),collapse=", "),"\n")'=== header + first 3 rows === row_id,country_of_origin,total_cup_points,altitude_outlier_flag,altitude_corrected_m,defect_outlier_flag,multivariate_grade_outlier_flag,high_leverage_flag,high_cook_flag,composite_drop_flag 0,Ethiopia,90.58,FALSE,NA,FALSE,FALSE,FALSE,FALSE,FALSE 1,Ethiopia,89.92,FALSE,NA,FALSE,FALSE,FALSE,FALSE,FALSE 2,Guatemala,89.75,FALSE,NA,FALSE,FALSE,FALSE,FALSE,FALSE === a unit-corrected row === row_id,country_of_origin,total_cup_points,altitude_outlier_flag,altitude_corrected_m,defect_outlier_flag,multivariate_grade_outlier_flag,high_leverage_flag,high_cook_flag,composite_drop_flag 31,"Tanzania, United Republic Of",86.5,FALSE,NA,FALSE,FALSE,FALSE,FALSE,FALSE 109,"Tanzania, United Republic Of",84.92,FALSE,NA,FALSE,FALSE,FALSE,FALSE,FALSE === count of each flag (excl header) === rows: 1338 altitude_outlier_flag TRUE=51 defect_outlier_flag TRUE=63 multivariate_grade_outlier_flag TRUE=40 high_leverage_flag TRUE=110 high_cook_flag TRUE=66 composite_drop_flag TRUE=105 altitude_corrected_m non-NA: 7 col types: numeric, character, numeric, logical, numeric, logical, logical, logical, logical, logical
cd /app && Rscript -e '
suppressPackageStartupMessages({library(readr);library(dplyr)})
d<-read_csv("outputs/outlier_flags.csv",show_col_types=FALSE)
raw<-read_csv("/app/data/coffee_ratings.csv",show_col_types=FALSE)
g10<-c("aroma","flavor","aftertaste","acidity","body","balance","uniformity","clean_cup","sweetness","cupper_points")
dat<-raw[!(rowSums(raw[,g10]==0,na.rm=TRUE)==10),]
corr<-d %>% filter(!is.na(altitude_corrected_m)) %>% select(row_id,altitude_corrected_m)
corr$raw_altitude <- dat$altitude[corr$row_id+1]
corr$orig_mean_m <- dat$altitude_mean_meters[corr$row_id+1]
cat("Unit-corrected rows (all also altitude_outlier_flag=TRUE):\n")
print(as.data.frame(corr), row.names=FALSE)
cat("\nAll corrected rows flagged as altitude outliers:", all(d$altitude_outlier_flag[d$row_id %in% corr$row_id]),"\n")'Unit-corrected rows (all also altitude_outlier_flag=TRUE):
row_id altitude_corrected_m raw_altitude orig_mean_m
543 1100.00 11000 metros 11000.0000
584 518.00 518 157.8864
677 518.00 518 157.8864
833 518.00 518 157.8864
896 1901.64 190164 190164.0000
1040 1100.00 1100.00 mosl 110000.0000
1144 1901.64 1901.64 190164.0000
All corrected rows flagged as altitude outliers: TRUEcd /app && Rscript -e '[redacted source](parse("analysis.R")); cat("analysis.R parses cleanly\n")'
echo "=== final end-to-end summary ==="
Rscript -e '
suppressPackageStartupMessages(library(jsonlite)); js<-fromJSON("outputs/outlier_report.json")
cat(sprintf("n_input=%d n_after=%d\n",js$n_input,js$n_after_zero_score_drop))
cat(sprintf("altitude fence=[%.1f, %.1f]m flagged=%d corrected=%d\n",js$altitude$lower_bound_m,js$altitude$upper_bound_m,js$altitude$n_flagged,js$altitude$n_unit_corrected))
cat(sprintf("defects thresh/flag: c1=%g/%d c2=%g/%d q=%g/%d\n",js$defects$category_one_defects$upper_threshold,js$defects$category_one_defects$n_flagged,js$defects$category_two_defects$upper_threshold,js$defects$category_two_defects$n_flagged,js$defects$quakers$upper_threshold,js$defects$quakers$n_flagged))
cat(sprintf("MV: %d cols, thresh=%.2f, flagged=%d\n",length(js$multivariate_grades$columns_used),js$multivariate_grades$threshold,js$multivariate_grades$n_flagged))
cat(sprintf("influence: %s (AIC %.1f/%.1f/%.1f) lev=%d cook=%d both=%d\n",js$influence$selected_model,js$influence$aic_grades_only,js$influence$aic_grades_altitude,js$influence$aic_grades_defects,js$influence$n_high_leverage,js$influence$n_high_cook,js$influence$n_both))
cat(sprintf("country: %d eval, %d move>=2, max=%d\n",js$country_ranking$n_countries_evaluated,js$country_ranking$n_rank_changes_ge_2,js$country_ranking$max_abs_rank_change))
cat(sprintf("sensitivity: flavor %.3f->%.3f (%.1f%%) CI[%.1f,%.1f] reps=%d\n",js$sensitivity$coef_flavor_full,js$sensitivity$coef_flavor_dropped,js$sensitivity$delta_pct,js$sensitivity$delta_pct_ci_lower,js$sensitivity$delta_pct_ci_upper,js$sensitivity$n_bootstrap_replicates))
'analysis.R parses cleanly === final end-to-end summary === n_input=1339 n_after=1338 altitude fence=[357.4, 4923.8]m flagged=51 corrected=7 defects thresh/flag: c1=5/20 c2=20/23 q=2/25 MV: 7 cols, thresh=24.32, flagged=40 influence: grades_defects (AIC 4459.4/4461.3/4418.7) lev=110 cook=66 both=35 country: 21 eval, 2 move>=2, max=2 sensitivity: flavor 2.085->1.653 (-20.7%) CI[-44.3,6.2] reps=500
No file changes, the agent left the workspace untouched.
# SyncVals normalized verifier summary # task: coffee-ratings-outliers # attempt: 9 status: completed raw_score: 0.0 # reward rule: binary: score 0.0 != 1.0 # => reward = 0.0 (FAIL) # # Note: these tasks score via an external hidden grader; no pytest/verifier # block is embedded in the agent transcript. The block below is the last # sizable tool-output captured in the run (supporting evidence). ---------------------------------------------------------------------- analysis.R parses cleanly === final end-to-end summary === n_input=1339 n_after=1338 altitude fence=[357.4, 4923.8]m flagged=51 corrected=7 defects thresh/flag: c1=5/20 c2=20/23 q=2/25 MV: 7 cols, thresh=24.32, flagged=40 influence: grades_defects (AIC 4459.4/4461.3/4418.7) lev=110 cook=66 both=35 country: 21 eval, 2 move>=2, max=2 sensitivity: flavor 2.085->1.653 (-20.7%) CI[-44.3,6.2] reps=500
Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_55e625eea0314773. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_55e625eea0314773 · verifier authoritative; classifier explanatory.