This report provides a comprehensive mathematical and empirical audit of the Theriome Aristotle+ multi-omics testing platform across all 296 biomarkers in the universal organic acid and metabolomics panel.
We configure file paths relative to the R script location (located
one level up from the data/ directory). New test CSV files
can be added directly to the file_paths vector.
# Define CSV data files (relative to R/ directory)
file_paths <- c(
"data/Another-Sample-Aristotle-Report-universal-oat.csv",
"data/Sample-Aristotle-Report-universal-oat.csv",
"data/Still-Another-Sample-Aristotle-Report-universal-oat.csv",
"data/Theriome-Aristotle-Plus-6-2026-universal-oat.csv", # Kelly
"data/AddisonIrvin9989_ARISTOTLE_PLUS_PTO7-universal-oat.csv",
"data/HollyIrvin0523_ARISTOTLE_PLUS_QT6Z-universal-oat.csv"
)
# Function to read and clean a single Theriome CSV export
read_theriome_csv <- function(path) {
# Verify file exists
if (!file.exists(path)) {
warning(paste("File not found:", path))
return(NULL)
}
# Read CSV skipping first 30 metadata rows (row 31 is column header)
# Analyte data spans rows 31 through 327 (header and 296 analytes)
raw_df <- read_csv(path, skip = 30, show_col_types = FALSE)
# Filter to analyte rows (using the category column for robustness)
analyte_df <- raw_df %>%
filter(category == "Analytes") %>%
select(
canonicalAnalyteId,
sourceAnalyteName,
metric,
value,
refLow,
refHigh
) %>%
mutate(
source_file = basename(path),
z_score = metric / 100.0, # Convert metric (Z * 100) to actual Z-score
value = as.numeric(value),
refHigh = as.numeric(refHigh),
refLow = as.numeric(refLow)
)
return(analyte_df)
}
# Ingest and pool all available test files
raw_combined <- map_dfr(file_paths, read_theriome_csv)
# Resolve name inconsistencies using Canonical ID.
# Count frequencies of each name per canonical ID.
name_freqs <- raw_combined %>%
count(canonicalAnalyteId, sourceAnalyteName) %>%
filter(!is.na(canonicalAnalyteId))
# Determine the resolved name for each canonical ID, handling overrides
resolved_names <- name_freqs %>%
group_by(canonicalAnalyteId) %>%
arrange(desc(n), sourceAnalyteName, .by_group = TRUE) %>%
slice(1) %>%
ungroup() %>%
select(canonicalAnalyteId, resolved_name = sourceAnalyteName) %>%
mutate(
resolved_name = case_when(
canonicalAnalyteId == 7165 ~ "Oxidized Glutahione",
canonicalAnalyteId == 7176 ~ "Prenenolone sulfate",
TRUE ~ resolved_name
)
)
# Replace sourceAnalyteName with resolved_name in raw_combined
raw_combined <- raw_combined %>%
left_join(resolved_names, by = "canonicalAnalyteId") %>%
mutate(sourceAnalyteName = ifelse(!is.na(resolved_name), resolved_name, sourceAnalyteName)) %>%
select(-resolved_name)Below is the successfully loaded test reports:
| Source File | Observations | Min Value | Max Value |
|---|---|---|---|
| AddisonIrvin9989_ARISTOTLE_PLUS_PTO7-universal-oat.csv | 296 | 0.00 | 9384347 |
| Another-Sample-Aristotle-Report-universal-oat.csv | 296 | 0.00 | 17366942 |
| HollyIrvin0523_ARISTOTLE_PLUS_QT6Z-universal-oat.csv | 296 | 0.00 | 12932562 |
| Sample-Aristotle-Report-universal-oat.csv | 296 | 0.00 | 43433943 |
| Still-Another-Sample-Aristotle-Report-universal-oat.csv | 296 | 2.44 | 18985247 |
| Theriome-Aristotle-Plus-6-2026-universal-oat.csv | 296 | 0.00 | 9091720 |
A total of 6 test reports were successfully ingested, pooling 1776 total observations across 296 distinct analytes. Parameters
Even though population means are omitted from the export CSVs, we mathematically recover both the population mean (\(\mu\)) and standard deviation (\(\sigma\)) for each test using two complementary methods:
\[\sigma_{\text{derived}} = \frac{X_{\text{high}} - X}{3 - Z} \quad \text{and} \quad \mu_{\text{derived}} = X_{\text{high}} - 3\sigma_{\text{derived}}\]
# Calculate derived parameters per observation
df_processed <- raw_combined %>%
mutate(
# Single-test algebraic recovery of sigma and mu
sigma_derived = ifelse(abs(3 - z_score) > 1e-4, (refHigh - value) / (3 - z_score), NA_real_),
mu_derived = refHigh - (3 * sigma_derived),
cv_derived = ifelse(mu_derived > 0, sigma_derived / mu_derived, NA_real_),
z_floor_derived = ifelse(!is.na(cv_derived) & cv_derived > 0, -1 / cv_derived, NA_real_)
)Below is a preview of the derived standard deviation (\(\sigma_{\text{derived}}\)), mean (\(\mu_{\text{derived}}\)), coefficient of variation (CV), and theoretical lower Z-score floor (\(Z_{\text{min}}\)) for the first 10 observations:
| Analyte Name | Raw Value | Ref High | Z-Score | Derived Std Dev (σ) | Derived Mean (μ) | Derived CV | Z Floor (Z_min) |
|---|---|---|---|---|---|---|---|
| 4-Imidazoleacetic acid | 839 | 82246.2 | -0.65 | 22303.342 | 15336.173 | 1.454 | -0.688 |
| Acetylcholine | 15145 | 97154.9 | -0.10 | 26454.806 | 17790.481 | 1.487 | -0.672 |
| Cystamine | 55 | 1323.0 | -0.39 | 374.041 | 200.876 | 1.862 | -0.537 |
| Dopamine | 128 | 19914.0 | -0.41 | 5802.346 | 2506.962 | 2.314 | -0.432 |
| Epinephrine | 361 | 9274.0 | -0.45 | 2583.478 | 1523.565 | 1.696 | -0.590 |
| Histamine | 402 | 176996.4 | -0.32 | 53191.084 | 17423.147 | 3.053 | -0.328 |
| Imidazole | 2296 | 70427.0 | -0.79 | 17976.517 | 16497.449 | 1.090 | -0.918 |
| Metanephrine | 1641 | 340947.1 | -0.26 | 104081.626 | 28702.223 | 3.626 | -0.276 |
| Methylguanidine | 27070 | 71451.2 | 0.61 | 18569.540 | 15742.581 | 1.180 | -0.848 |
| Methylhistamine | 2973 | 16526.2 | -0.56 | 3807.079 | 5104.964 | 0.746 | -1.341 |
For each observation, the underlying population mean (\(\mu\)) and standard deviation (\(\sigma\)) are algebraically derived using the upper limit of the reference range (\(X_{\text{high}}\)) and the reported Z-score. This allows us to reconstruct the coefficient of variation (CV) and compute the theoretical minimum possible Z-score floor (\(Z_{\text{min}} = -1/\text{CV}\)) for every analyte.
For each analyte with multiple test observations, we evaluate whether \(Z\) scales linearly with raw concentration \(X\) or logarithmic concentration \(\ln(X)\).
# Group by analyte and perform model fitting
analyte_summary <- df_processed %>%
group_by(canonicalAnalyteId, sourceAnalyteName) %>%
summarise(
n_obs = n(),
mean_value = mean(value, na.rm = TRUE),
mean_z = mean(z_score, na.rm = TRUE),
avg_refHigh = mean(refHigh, na.rm = TRUE),
avg_mu_derived = mean(mu_derived, na.rm = TRUE),
avg_sigma_derived = mean(sigma_derived, na.rm = TRUE),
avg_cv = mean(cv_derived, na.rm = TRUE),
avg_z_floor = mean(z_floor_derived, na.rm = TRUE),
# Regression Fits (Raw vs Log) - robustly handled to avoid NA errors
fit_raw_r2 = tryCatch(
{
df_temp <- data.frame(z = z_score, val = value) %>%
filter(!is.na(z), !is.na(val))
if (nrow(df_temp) >= 2 && n_distinct(df_temp$val) >= 2) {
summary(lm(z ~ val, data = df_temp))$r.squared
} else {
NA_real_
}
},
error = function(e) NA_real_
),
fit_log_r2 = tryCatch(
{
df_temp <- data.frame(z = z_score, val = value) %>%
filter(!is.na(z), !is.na(val), val > 0)
if (nrow(df_temp) >= 2 && n_distinct(df_temp$val) >= 2) {
summary(lm(z ~ log(val), data = df_temp))$r.squared
} else {
NA_real_
}
},
error = function(e) NA_real_
),
.groups = "drop"
) %>%
mutate(
raw_fit_dominates = ifelse(!is.na(fit_raw_r2) & !is.na(fit_log_r2), fit_raw_r2 >= fit_log_r2, TRUE)
)We evaluate both raw concentration (\(X\)) and logarithmic concentration (\(\ln(X)\)) models for each analyte. If an
analyte’s Z-score fits a linear model of raw concentration better than
log concentration, raw_fit_dominates is set to
TRUE.
Below is the summary of the model fits:
| Total Evaluated Analytes | Raw Fit Better or Equal | Log Fit Better | Average R² (Raw) | Average R² (Log) |
|---|---|---|---|---|
| 296 | 247 | 49 | 0.8379 | 0.7554 |
The table below lists the top 10 analytes where a logarithmic concentration model fits the reported Z-score significantly better than a raw concentration model:
| Analyte Name | R² Raw Model | R² Log Model | R² Improvement |
|---|---|---|---|
| Betaine | 0.3579 | 0.6319 | 0.2740 |
| Glutaconic acid | 0.7348 | 0.9407 | 0.2059 |
| 3-Aminobutyric acid | 0.5046 | 0.7034 | 0.1988 |
| Biotin | 0.6743 | 0.8497 | 0.1754 |
| p-Coumaric acid | 0.7269 | 0.8786 | 0.1517 |
| Threonine | 0.6908 | 0.8420 | 0.1512 |
| N-Acetyl-D-galactosamine | 0.7293 | 0.8573 | 0.1280 |
| Methyl succinate | 0.2017 | 0.3168 | 0.1152 |
| Pentadecanoic acid | 0.1007 | 0.2104 | 0.1097 |
| 4-Hydroxy-3-methylbenzoic acid | 0.2209 | 0.3152 | 0.0943 |
This interactive visualization tool allows you to examine individual analytes in detail.
This has been disabled because it is extremely resource intensive.
library(plotly)
# Unique analytes sorted alphabetically by name
analytes_list <- unique(df_processed %>% select(canonicalAnalyteId, sourceAnalyteName)) %>%
arrange(sourceAnalyteName)
# Total number of analytes
M <- nrow(analytes_list)
p_explorer <- plot_ly()
# Loop through each analyte and add its traces
for (i in 1:M) {
# Get data for this analyte
id <- analytes_list$canonicalAnalyteId[i]
name <- analytes_list$sourceAnalyteName[i]
obs <- df_processed %>% filter(canonicalAnalyteId == id)
ref_high <- mean(obs$refHigh, na.rm = TRUE)
if (is.na(ref_high) || ref_high <= 0) ref_high <- 100 # safe default
mu_raw <- mean(obs$mu_derived, na.rm = TRUE)
sigma_raw <- mean(obs$sigma_derived, na.rm = TRUE)
# Fallback for raw parameters if they cannot be derived
if (is.na(mu_raw) || is.na(sigma_raw) || sigma_raw <= 0) {
mu_raw <- mean(obs$value, na.rm = TRUE)
sigma_raw <- sd(obs$value, na.rm = TRUE)
if (is.na(sigma_raw) || sigma_raw <= 0) {
sigma_raw <- ref_high / 3
if (is.na(mu_raw)) mu_raw <- ref_high / 2
}
}
# Fit log model for log-normal parameters
df_fit <- obs %>% filter(!is.na(value), !is.na(z_score), value > 0)
fit_log_ok <- FALSE
if (nrow(df_fit) >= 2 && n_distinct(df_fit$value) >= 2) {
fit <- tryCatch(lm(z_score ~ log(value), data = df_fit), error = function(e) NULL)
if (!is.null(fit)) {
coefs <- coef(fit)
beta <- coefs[1]
alpha <- coefs[2]
if (!is.na(alpha) && !is.na(beta) && abs(alpha) > 1e-5) {
mu_log <- -beta / alpha
sigma_log <- 1 / abs(alpha)
fit_log_ok <- TRUE
}
}
}
# Fallback for log parameters
if (!fit_log_ok) {
if (mu_raw > 0) {
mu_log <- log(mu_raw^2 / sqrt(mu_raw^2 + sigma_raw^2))
sigma_log <- sqrt(log(1 + (sigma_raw / mu_raw)^2))
} else {
mu_log <- log(ref_high / 2)
sigma_log <- 1
}
}
# ----------------------------------------------------
# 1. Left Plot (Linear scale)
# ----------------------------------------------------
max_val <- max(ref_high, max(obs$value, na.rm = TRUE), mu_raw + 3 * sigma_raw, na.rm = TRUE)
x_grid_lin <- seq(0, max_val * 1.2, length.out = 100)
y_raw_lin <- dnorm(x_grid_lin, mean = mu_raw, sd = sigma_raw)
y_log_lin <- dlnorm(x_grid_lin, meanlog = mu_log, sdlog = sigma_log)
max_y_lin <- max(y_raw_lin, y_log_lin, na.rm = TRUE)
if (is.na(max_y_lin) || max_y_lin <= 0) max_y_lin <- 1.0
# Trace 1: Raw density (linear)
p_explorer <- p_explorer %>% add_trace(
x = x_grid_lin, y = y_raw_lin, type = "scatter", mode = "lines",
line = list(color = "#3182ce", width = 2), name = "Raw Model Density",
xaxis = "x", yaxis = "y", visible = (i == 1),
showlegend = (i == 1), legendgroup = "raw_dens",
hoverinfo = "text", text = paste("Raw Model Density:", round(y_raw_lin, 5))
)
# Trace 2: Log density (linear)
p_explorer <- p_explorer %>% add_trace(
x = x_grid_lin, y = y_log_lin, type = "scatter", mode = "lines",
line = list(color = "#dd6b20", width = 2), name = "Log Model Density",
xaxis = "x", yaxis = "y", visible = (i == 1),
showlegend = (i == 1), legendgroup = "log_dens",
hoverinfo = "text", text = paste("Log Model Density:", round(y_log_lin, 5))
)
# Trace 3: Test results (linear vertical lines)
x_tests_lin <- rep(obs$value, each = 3) * c(1, 1, NA)
y_tests_lin <- rep(c(0, max_y_lin * 1.05, NA), nrow(obs))
p_explorer <- p_explorer %>% add_trace(
x = x_tests_lin, y = y_tests_lin, type = "scatter", mode = "lines",
line = list(color = "#718096", dash = "dash", width = 1), name = "Test Results",
xaxis = "x", yaxis = "y", visible = (i == 1),
showlegend = (i == 1), legendgroup = "test_res",
hoverinfo = "none"
)
# Trace 4: Inferred mean (linear)
p_explorer <- p_explorer %>% add_trace(
x = c(mu_raw, mu_raw), y = c(0, max_y_lin * 1.05), type = "scatter", mode = "lines",
line = list(color = "#2f855a", width = 2), name = "Inferred Mean (\u03bc)",
xaxis = "x", yaxis = "y", visible = (i == 1),
showlegend = (i == 1), legendgroup = "inf_mean",
hoverinfo = "text", text = paste("Inferred Mean (\u03bc):", round(mu_raw, 2))
)
# Trace 5: Reference limit (linear)
p_explorer <- p_explorer %>% add_trace(
x = c(ref_high, ref_high), y = c(0, max_y_lin * 1.05), type = "scatter", mode = "lines",
line = list(color = "#e53e3e", width = 2), name = "Reference Limit (X_high)",
xaxis = "x", yaxis = "y", visible = (i == 1),
showlegend = (i == 1), legendgroup = "ref_limit",
hoverinfo = "text", text = paste("Reference Limit:", round(ref_high, 2))
)
# ----------------------------------------------------
# 2. Right Plot (Log scale)
# ----------------------------------------------------
max_val_log <- log(max_val + 0.1)
y_grid_log <- seq(-1.5, max_val_log * 1.2, length.out = 100)
# Density values in log space
dens_log_log <- dnorm(y_grid_log, mean = mu_log, sd = sigma_log)
# Raw model transformed to log scale
exp_y <- exp(y_grid_log)
dens_raw_log <- dnorm(exp_y, mean = mu_raw, sd = sigma_raw) * exp_y
max_y_log <- max(dens_log_log, dens_raw_log, na.rm = TRUE)
if (is.na(max_y_log) || max_y_log <= 0) max_y_log <- 1.0
# Trace 6: Raw density (log space)
p_explorer <- p_explorer %>% add_trace(
x = y_grid_log, y = dens_raw_log, type = "scatter", mode = "lines",
line = list(color = "#3182ce", width = 2), name = "Raw Model Density",
xaxis = "x2", yaxis = "y2", visible = (i == 1),
showlegend = FALSE, legendgroup = "raw_dens",
hoverinfo = "text", text = paste("Raw Density (Log-Space):", round(dens_raw_log, 5))
)
# Trace 7: Log density (log space)
p_explorer <- p_explorer %>% add_trace(
x = y_grid_log, y = dens_log_log, type = "scatter", mode = "lines",
line = list(color = "#dd6b20", width = 2), name = "Log Model Density",
xaxis = "x2", yaxis = "y2", visible = (i == 1),
showlegend = FALSE, legendgroup = "log_dens",
hoverinfo = "text", text = paste("Log Density (Log-Space):", round(dens_log_log, 5))
)
# Trace 8: Test results (log space vertical lines)
log_vals <- ifelse(obs$value == 0, -1, log(obs$value))
x_tests_log <- rep(log_vals, each = 3) * c(1, 1, NA)
y_tests_log <- rep(c(0, max_y_log * 1.05, NA), nrow(obs))
p_explorer <- p_explorer %>% add_trace(
x = x_tests_log, y = y_tests_log, type = "scatter", mode = "lines",
line = list(color = "#718096", dash = "dash", width = 1), name = "Test Results",
xaxis = "x2", yaxis = "y2", visible = (i == 1),
showlegend = FALSE, legendgroup = "test_res",
hoverinfo = "none"
)
# Trace 9: Inferred mean (log space)
log_mu_raw <- ifelse(mu_raw <= 0, -1, log(mu_raw))
p_explorer <- p_explorer %>% add_trace(
x = c(log_mu_raw, log_mu_raw), y = c(0, max_y_log * 1.05), type = "scatter", mode = "lines",
line = list(color = "#2f855a", width = 2), name = "Inferred Mean (\u03bc)",
xaxis = "x2", yaxis = "y2", visible = (i == 1),
showlegend = FALSE, legendgroup = "inf_mean",
hoverinfo = "text", text = paste("Inferred Mean (Log-Space):", round(log_mu_raw, 2))
)
# Trace 10: Reference limit (log space)
log_ref_high <- log(ref_high)
p_explorer <- p_explorer %>% add_trace(
x = c(log_ref_high, log_ref_high), y = c(0, max_y_log * 1.05), type = "scatter", mode = "lines",
line = list(color = "#e53e3e", width = 2), name = "Reference Limit (X_high)",
xaxis = "x2", yaxis = "y2", visible = (i == 1),
showlegend = FALSE, legendgroup = "ref_limit",
hoverinfo = "text", text = paste("Reference Limit (Log-Space):", round(log_ref_high, 2))
)
}
# Construct the dropdown buttons
buttons <- list()
for (k in 1:M) {
visible_vector <- rep(FALSE, 10 * M)
visible_vector[(10 * (k - 1) + 1):(10 * k)] <- TRUE
buttons[[k]] <- list(
method = "restyle",
args = list("visible", as.list(visible_vector)),
label = analytes_list$sourceAnalyteName[k]
)
}
# Define tick positions for custom log axis
raw_ticks <- c(0, 0.1, 0.5, 1, 5, 10, 50, 100, 500, 1000, 5000, 10000, 50000, 100000, 500000)
tickvals <- ifelse(raw_ticks == 0, -1, log(raw_ticks))
ticktext <- as.character(raw_ticks)
# Set layouts and configure dropdown
p_explorer <- p_explorer %>% layout(
xaxis = list(domain = c(0, 0.47), title = "Raw Concentration (Linear Scale)"),
yaxis = list(title = "Density"),
xaxis2 = list(
domain = c(0.53, 1),
title = "Log-transformed Concentration (Raw Value Ticks; 0 mapped to -1)",
anchor = "x2",
tickvals = tickvals,
ticktext = ticktext
),
yaxis2 = list(title = "Density", anchor = "x2"),
updatemenus = list(
list(
buttons = buttons,
direction = "down",
showactive = TRUE,
x = 0.05, y = 1.25,
xanchor = "left",
yanchor = "top"
)
),
margin = list(t = 100),
legend = list(orientation = "h", y = -0.2)
)
p_explorerTo understand how well the reported Z-scores fit raw vs. logarithmic models across all analytes, we plot the distribution of their \(R^2\) values.
# Prepare data for R2 histograms
r2_data <- analyte_summary %>%
filter(!is.na(fit_raw_r2) | !is.na(fit_log_r2)) %>%
select(canonicalAnalyteId, sourceAnalyteName, fit_raw_r2, fit_log_r2) %>%
pivot_longer(
cols = c(fit_raw_r2, fit_log_r2),
names_to = "model_type",
values_to = "r2_value"
) %>%
mutate(
model_type = ifelse(model_type == "fit_raw_r2", "Raw Linear Model", "Log-Linear Model")
)
# Plot interactive histograms using ggplot and plotly
p_hist <- ggplot(r2_data, aes(x = r2_value, fill = model_type)) +
geom_histogram(alpha = 0.6, binwidth = 0.05, position = "identity", color = "white") +
scale_fill_manual(values = c("Raw Linear Model" = "#3182ce", "Log-Linear Model" = "#dd6b20")) +
theme_minimal() +
labs(
title = "Distribution of R² Fits Across Analytes",
x = "R² Value",
y = "Count of Analytes",
fill = "Model Type"
)
ggplotly(p_hist)In this platform audit, we notice that while many analytes have very high \(R^2\) values (close to 1.0, indicating a perfect linear or logarithmic mapping), some analytes exhibit low \(R^2\) fits for both models.
Here are the primary analytical explanations for low \(R^2\) values:
total_analytes <- nrow(analyte_summary)
evaluatable <- sum(!is.na(analyte_summary$fit_raw_r2))
raw_omitted_count <- sum(analyte_summary$raw_fit_dominates, na.rm = TRUE)
pct_omitted <- round((raw_omitted_count / total_analytes) * 100, 1)
median_floor <- median(analyte_summary$avg_z_floor, na.rm = TRUE)Below are the key platform audit summary metrics recovered from the ingested data:
| Metric | Value |
|---|---|
| Total Analytes Evaluated | 296 |
| Analytes with Sufficient Data for Fit (N >= 2) | 296 |
| Analytes Fitting Raw Linear Model Better | 247 |
| Percentage of Analytes fitting Raw Model Better | 83.4% |
| Median Theoretical Lower Z-Score Floor | -0.712 |
Key Findings: - Raw Linear Dominance: 83.4 % of the evaluatable analytes have a better fit to a raw linear model than to a logarithmic model. This indicates the platform’s standardization engine is applying a standard linear Z-score mapping on raw concentration metrics. - Diagnostic Floor: The median theoretical lower floor is -0.712. At zero concentration, patients will have a suppressed lower boundary on their reported Z-score, demonstrating why standard raw-concentration standardization fails to provide fine metabolic deficiency resolution.
Hover over any data point to inspect the analyte name, raw value, reported Z-score, derived population mean, upper reference range, and computed minimum Z-score floor.
# Prepare dataframe for plotting
plot_data <- df_processed %>%
left_join(
analyte_summary %>% select(canonicalAnalyteId, fit_raw_r2, fit_log_r2, raw_fit_dominates),
by = "canonicalAnalyteId"
) %>%
mutate(
original_z = z_score,
# Winsorize Z-score to [-4, +4]
z_score = pmin(pmax(original_z, -4), 4),
winsorized = !is.na(original_z) & (original_z < -4 | original_z > 4),
# Log transform x axis. Transform 0 to -1.
x_trans = ifelse(!is.na(value) & value == 0, -1, ifelse(!is.na(value) & value > 0, log(value), NA_real_)),
hover_info = paste0(
"<b>Analyte:</b> ", sourceAnalyteName, "<br>",
"<b>Canonical ID:</b> ", canonicalAnalyteId, "<br>",
"<b>Report File:</b> ", source_file, "<br>",
"<b>Raw Value (X):</b> ", round(value, 3), "<br>",
"<b>Log-transformed X:</b> ", round(x_trans, 3), "<br>",
"<b>Reported Z-Score:</b> ", round(original_z, 2),
ifelse(winsorized, paste0(" (Winsorized to ", round(z_score, 2), ")"), ""), "<br>",
"<b>Ref Range:</b> [0 - ", round(refHigh, 1), "]<br>",
"<b>Derived Pop Mean (μ):</b> ", round(mu_derived, 1), "<br>",
"<b>Derived Pop Std (σ):</b> ", round(sigma_derived, 1), "<br>",
"<b>Theoretical Floor (Z_min):</b> ", round(z_floor_derived, 2)
),
z_score_status = ifelse(!is.na(z_score) & z_score < 0, "Negative", "Positive")
)
p <- ggplot(plot_data, aes(x = x_trans, y = z_score, text = hover_info, color = z_score_status)) +
geom_point(alpha = 0.7, size = 2.2) +
geom_hline(yintercept = 0, linetype = "dashed", color = "#718096") +
geom_hline(yintercept = median_floor, linetype = "dotted", color = "#e53e3e") +
scale_color_manual(
values = c("Negative" = "#e53e3e", "Positive" = "#3182ce"),
labels = c("Negative" = "Negative Z-Score (Suppressed)", "Positive" = "Positive Z-Score")
) +
theme_minimal() +
labs(
title = "Theriome Aristotle+: Log Concentration vs. Winsorized Z-Score",
x = "Log-transformed Concentration Value (ln(X); 0 mapped to -1)",
y = "Winsorized Z-Score (bounded at [-4, +4])",
color = "Z-Score Status"
)
ggplotly(p, tooltip = "text")The interactive table below records all observations that had their
Z-scores truncated because they fell outside the [-4, +4]
range:
Sort and filter across all 296 analytes to inspect derived population metrics and theoretical lower-bound floors.
table_data <- analyte_summary %>%
select(
canonicalAnalyteId,
sourceAnalyteName,
n_obs,
avg_refHigh,
avg_mu_derived,
avg_sigma_derived,
avg_cv,
avg_z_floor,
fit_raw_r2,
fit_log_r2
) %>%
mutate(
# Convert canonical ID to character to render text search filter in DT
canonicalAnalyteId = as.character(canonicalAnalyteId),
avg_refHigh = round(avg_refHigh, 1),
avg_mu_derived = round(avg_mu_derived, 1),
avg_sigma_derived = round(avg_sigma_derived, 1),
avg_cv = round(avg_cv, 3),
avg_z_floor = round(avg_z_floor, 2),
fit_raw_r2 = round(fit_raw_r2, 4),
fit_log_r2 = round(fit_log_r2, 4)
)
datatable(
table_data,
options = list(
pageLength = 15,
searchHighlight = TRUE,
autoWidth = TRUE
),
filter = "top",
colnames = c(
"Canonical ID", "Analyte Name", "N Tests", "Ref High",
"Derived Mean (\u03bc)", "Derived Std (\u03c3)", "Derived CV",
"Z Floor (Z_min)", "R\u00b2 Raw", "R\u00b2 Log"
)
)Standard raw Z-score standardization assumes that concentrations follow a symmetric Gaussian distribution. However, biological concentrations are typically log-normally distributed (skewed right). When a raw linear Z-score is applied, it suppresses diagnostic resolution at the lower end (creating a hard floor) and exaggerates variations at the higher end.
By converting raw concentrations to log-normal space via Method-of-Moments, we compute true log-normal Z-scores: \[Z_{\log} = \frac{\ln(X_{\text{safe}}) - \mu_{\log}}{\sigma_{\log}}\] where \(\sigma_{\log} = \sqrt{\ln(1 + \text{CV}^2)}\), \(\mu_{\log} = \ln(\mu_{\text{raw}}) - 0.5\sigma_{\log}^2\), and \(X_{\text{safe}} = \max(X, 10^{-4} \cdot \mu_{\text{raw}})\) to prevent \(\ln(0)\) singularities.
Below is the comparison of original reported versus log-normal converted Z-scores across all analytes.
# Calculate true log-normal parameters directly from raw parameters
# using standard Method-of-Moments formulas:
# sigma_log = sqrt(log(1 + CV^2))
# mu_log = log(mu) - 0.5 * sigma_log^2
df_processed_with_log_z <- df_processed %>%
left_join(
analyte_summary %>% select(canonicalAnalyteId, avg_mu_derived, avg_sigma_derived, avg_cv),
by = "canonicalAnalyteId"
) %>%
mutate(
# True log-normal parameter conversions
sigma_log_true = ifelse(!is.na(avg_cv) & avg_cv > 0, sqrt(log(1 + avg_cv^2)), NA_real_),
mu_log_true = ifelse(avg_mu_derived > 0 & !is.na(sigma_log_true), log(avg_mu_derived) - 0.5 * sigma_log_true^2, NA_real_),
# Protect against 0 singularities with epsilon floor (1e-4 * mean)
x_safe = ifelse(!is.na(value) & !is.na(avg_mu_derived), pmax(value, 1e-4 * avg_mu_derived), value),
# Calculate true log-normal Z-score
z_log_normal_unwinsorized = (log(x_safe) - mu_log_true) / sigma_log_true,
# Winsorize log-normal Z-scores to [-4, +4]
z_log_normal = pmin(pmax(z_log_normal_unwinsorized, -4), 4),
# Winsorize original reported Z-scores to [-4, +4] as well
z_score = pmin(pmax(z_score, -4), 4)
)
# Group by analyte to show comparison
comparison_table_data <- df_processed_with_log_z %>%
group_by(canonicalAnalyteId, sourceAnalyteName) %>%
summarise(
n_obs = n(),
avg_refHigh = mean(refHigh, na.rm = TRUE),
avg_mu_derived = mean(mu_derived, na.rm = TRUE),
avg_sigma_derived = mean(sigma_derived, na.rm = TRUE),
avg_cv = mean(avg_cv, na.rm = TRUE),
avg_original_z = mean(z_score, na.rm = TRUE),
avg_log_normal_z = mean(z_log_normal, na.rm = TRUE),
.groups = "drop"
) %>%
mutate(
# Convert canonical ID to character to render text search filter in DT
canonicalAnalyteId = as.character(canonicalAnalyteId),
avg_refHigh = round(avg_refHigh, 1),
avg_mu_derived = round(avg_mu_derived, 1),
avg_sigma_derived = round(avg_sigma_derived, 1),
avg_cv = round(avg_cv, 3),
avg_original_z = round(avg_original_z, 3),
avg_log_normal_z = round(avg_log_normal_z, 3)
)
datatable(
comparison_table_data,
options = list(
pageLength = 15,
searchHighlight = TRUE,
autoWidth = TRUE
),
filter = "top",
colnames = c(
"Canonical ID", "Analyte Name", "N Tests", "Ref High",
"Derived Mean (\u03bc)", "Derived Std (\u03c3)", "CV",
"Avg Original Z", "Avg Log-Normal Z"
)
)Below is a scatterplot comparing the Log-Normal Converted Z-scores
against the Original reported Z-scores across all individual test
values, including marginal boxplots along both axes and regression fits
generated by the car package.
library(car)
# Render car package scatterplot directly
car::scatterplot(
z_log_normal ~ z_score,
data = df_processed_with_log_z,
xlab = "Original Reported Z-Score (Winsorized to [-4, +4])",
ylab = "Log-Normal Converted Z-Score (Winsorized to [-4, +4])",
main = "Comparison of Log-Normal vs Original Reported Z-Scores",
id = FALSE, # Suppress identification labels for individual outliers
grid = TRUE,
ellipse = FALSE, # Suppress data ellipses for cleaner presentation
col = c("#3182ce", "#e53e3e", "#dd6b20") # Custom colors for points, linear regression, and loess line
)Below is an interactive overlapped histogram comparing the distribution of the Original Reported Z-Scores and the Log-Normal Converted Z-Scores across all observations.
Two theoretical reference curves are overlaid:
1. Theoretical \(N(0,
1)\): The standard Gaussian density curve representing
an ideal standardized normal distribution.
2. Theoretical Linear Z on Log-Normal: The mathematical
probability density resulting from calculating a linear Z-score (\(Z = \frac{X - \mu}{\sigma}\)) on
log-normally distributed analyte concentrations. Computed across the
panel mixture of analyte CVs:
\[f_Z(z; CV) = \frac{CV}{(1 + CV \cdot z)
\sigma_{\log} \sqrt{2\pi}} \exp\left( -\frac{(\ln(1 + CV \cdot z) +
\sigma_{\log}^2/2)^2}{2\sigma_{\log}^2} \right) \quad \text{for } z >
-1/CV\]
Use the Linear Y-Axis and Log Y-Axis buttons above the plot to toggle between linear and logarithmic vertical scaling.
# Prepare data for Z-score histograms
z_hist_data <- df_processed_with_log_z %>%
select(canonicalAnalyteId, sourceAnalyteName, z_score, z_log_normal) %>%
pivot_longer(
cols = c(z_score, z_log_normal),
names_to = "score_type",
values_to = "z_value"
) %>%
mutate(
score_type = case_when(
score_type == "z_score" ~ "Original Reported Z-Score",
score_type == "z_log_normal" ~ "Log-Normal Converted Z-Score"
)
)
# Grid of Z-values
z_grid <- seq(-4, 4, length.out = 400)
# 1. Theoretical Standard Normal N(0, 1)
d_norm_vals <- dnorm(z_grid, mean = 0, sd = 1)
# 2. Theoretical Linear Z on Log-Normal ground truth
valid_cvs <- analyte_summary$avg_cv[!is.na(analyte_summary$avg_cv) & analyte_summary$avg_cv > 0]
if (length(valid_cvs) == 0) valid_cvs <- 0.6
d_linear_z_lognorm <- function(z, cv) {
sigma_l <- sqrt(log(1 + cv^2))
arg <- 1 + cv * z
ifelse(arg > 0,
(cv / (arg * sigma_l * sqrt(2 * pi))) * exp(-(log(arg) + 0.5 * sigma_l^2)^2 / (2 * sigma_l^2)),
0
)
}
# Compute panel mixture density over all analyte CVs
d_lin_z_mixture <- sapply(z_grid, function(z) {
mean(d_linear_z_lognorm(z, valid_cvs), na.rm = TRUE)
})
theoretical_curves <- bind_rows(
tibble(
z_value = z_grid,
density = d_norm_vals,
curve_type = "Theoretical N(0, 1)"
),
tibble(
z_value = z_grid,
density = d_lin_z_mixture,
curve_type = "Theoretical Linear Z on Log-Normal"
)
)
# Plot interactive histograms using ggplot and plotly
p_z_hist <- ggplot() +
geom_histogram(
data = z_hist_data,
aes(x = z_value, y = after_stat(density), fill = score_type),
alpha = 0.55,
binwidth = 0.2,
position = "identity",
color = "white"
) +
geom_line(
data = theoretical_curves,
aes(x = z_value, y = density, color = curve_type, linetype = curve_type),
linewidth = 1
) +
scale_fill_manual(
name = "Empirical Distribution",
values = c(
"Original Reported Z-Score" = "#3182ce",
"Log-Normal Converted Z-Score" = "#dd6b20"
)
) +
scale_color_manual(
name = "Theoretical Model",
values = c(
"Theoretical N(0, 1)" = "#1a202c",
"Theoretical Linear Z on Log-Normal" = "#e53e3e"
)
) +
scale_linetype_manual(
name = "Theoretical Model",
values = c(
"Theoretical N(0, 1)" = "dashed",
"Theoretical Linear Z on Log-Normal" = "solid"
)
) +
theme_minimal() +
labs(
title = "Empirical Z-Scores vs. Theoretical Reference Models",
x = "Z-Score Value",
y = "Density"
)
# Add interactive linear/log Y-axis switcher buttons to plotly
p_z_plotly <- ggplotly(p_z_hist) %>%
layout(
legend = list(orientation = "h", x = 0.0, y = -0.2),
updatemenus = list(
list(
type = "buttons",
direction = "right",
x = 0.0,
y = 1.15,
showactive = TRUE,
active = 0,
buttons = list(
list(
method = "relayout",
args = list(list(yaxis = list(type = "linear", title = "Density (Linear Scale)"))),
label = "Linear Y-Axis"
),
list(
method = "relayout",
args = list(list(yaxis = list(type = "log", title = "Density (Log Scale)"))),
label = "Log Y-Axis"
)
)
)
)
)
p_z_plotlyTo quantitatively benchmark how closely each empirical scoring pipeline aligns with the ideal Gaussian standard (\(N(0, 1)\)) versus the flawed linear standardized log-normal error model, we compute the goodness-of-fit metrics across the bin density grid (\(z \in [-4, +4]\), \(\Delta z = 0.2\)):
| Empirical Score Pipeline | Theoretical Reference Model | Density R² | RMSE | MAE | Statistical Conclusion |
|---|---|---|---|---|---|
| Original Reported Z-Score | Theoretical N(0, 1) | 0.4784 | 0.1815 | 0.1013 | Poor fit: Truncated floor (Z < -1.67) and severe right-tail skew |
| Original Reported Z-Score | Theoretical Linear Z on Log-Normal | 0.8544 | 0.0959 | 0.0410 | High concordance (R² ~ 0.85): Confirms data is log-normal standardized linearly |
| Log-Normal Converted Z-Score | Theoretical N(0, 1) | 0.3622 | 0.0771 | 0.0529 | Optimal Gaussian fit: Lowest RMSE/MAE, eliminates floor artifact |
| Log-Normal Converted Z-Score | Theoretical Linear Z on Log-Normal | -5.6709 | 0.2494 | 0.1338 | Low concordance: Confirms complete removal of linear skew |
Depending on what level of refinement you want to achieve, here is the exact data requirement breakdown: