Executive Summary
Dimensionality and dominant structure across 768 observations
The short answer
The 8 clinical measurements compress into 5 dominant components that explain 80% of total variation. The first component, driven by BMI, SkinThickness, and Insulin, alone accounts for 26.2% of variation.
The detail
768 observations, 8 features analyzed. PC1 variance: 26.2%; components for 80% cumulative variance: 5; Kaiser components (eigenvalue > 1): 3. PC1 is the strongest axis of variation and is defined by BMI (loading −0.452), SkinThickness (−0.44), and Insulin (−0.435), indicating these measurements co-vary strongly and define the primary structure in the dataset.
What this can't tell you
Effective dimensionality depends on the downstream task. The 80% threshold is conventional but arbitrary; whether 5 components are sufficient for your clinical model depends on the signal-to-noise ratio required for prediction or classification.
Analysis Overview
Principal component analysis of 8 standardized features across 768 observations.
The short answer
The analysis standardizes 8 clinical measurements and decomposes them into independent components to reveal the dominant patterns in the data. The first component captures 26.2% of variation; 5 components together explain 80%.
The detail
All 768 observations and 8 features (Pregnancies, Glucose, BloodPressure, SkinThickness, Insulin, BMI, DiabetesPedigreeFunction, Age) were standardized to mean 0 and variance 1 before decomposition. Loadings show which original features drive each component—features loading together vary together in the dataset. The Kaiser criterion (eigenvalue > 1) identifies 3 components as carrying signal above the noise floor of a single original feature.
What this can't tell you
This method assumes linear relationships among features. Nonlinear patterns or domain-specific interactions not captured by correlation will remain hidden in the residual components.
Data Quality
Feature typing, imputation, and exclusions before PCA.
The short answer
All 768 rows were retained with no exclusions. Missing values were imputed with each column's median, and all features were standardized so that measurement scale differences do not distort the components.
The detail
Initial load: 768 rows; final rows used: 768 (0 removed). All 8 mapped feature columns were numeric and usable. Standardization ensures that a feature measured in large units (e.g., Glucose in mg/dL) does not dominate the decomposition over one measured in small units (e.g., Pregnancies as a count). Observations are colored by Outcome on the component map to reveal whether the leading components separate outcome groups.
What this can't tell you
Median imputation assumes data are missing at random and does not recover information lost to missingness. If missingness is associated with outcome or with unmeasured factors, the imputed values may bias downstream inference. Consider reviewing the original missingness pattern if available.
Variance Explained by Component
How much of the total variation each principal component captures (scree chart).
The short answer
The first component carries 26.2% of variation; the second adds 21.6%. The drop-off steepens after PC3 (12.9%), indicating that the dataset has moderate compressibility—no single dominant pattern, but clear structure in the top few axes.
The detail
PC1: 26.2%; PC2: 21.6%; PC3: 12.9%; PC4: 10.9%; PC5: 9.5%. Cumulative: 5 components reach 81.2%, crossing the 80% threshold. PC6 (8.5%) and PC7 (5.2%) add incrementally; PC8 (5.1%) closes the remaining variance. The visible drop-off between PC3 and PC4 suggests that meaningful signal concentrates in the first three components, with PC4 and PC5 capturing secondary structure and components beyond PC5 encoding mostly noise.
What this can't tell you
Variance explained does not measure whether the components are interpretable or clinically meaningful. High variance does not guarantee that a component relates to the outcome of interest.
Component Summary
Eigenvalues and variance shares for the leading components.
| Component | Eigenvalue | Variance PCT | Cumulative PCT |
|---|---|---|---|
| PC1 | 2.094 | 26.2 | 26.2 |
| PC2 | 1.731 | 21.6 | 47.8 |
| PC3 | 1.03 | 12.9 | 60.7 |
| PC4 | 0.876 | 10.9 | 71.6 |
| PC5 | 0.762 | 9.5 | 81.2 |
| PC6 | 0.683 | 8.5 | 89.7 |
| PC7 | 0.42 | 5.2 | 94.9 |
| PC8 | 0.404 | 5.1 | 100 |
The short answer
Three components pass the Kaiser criterion (eigenvalue > 1), but you need 5 components to capture 80% of the total variation. The first component is substantially stronger than the rest, explaining 26.2% alone, while the second and third contribute 21.6% and 12.9% respectively.
The detail
PC1 has eigenvalue 2.094 and variance 26.2%; PC2 has eigenvalue 1.731 and variance 21.6%; PC3 has eigenvalue 1.03 and variance 12.9%. All three exceed eigenvalue 1. PC4 (eigenvalue 0.876, variance 10.9%) and PC5 (eigenvalue 0.762, variance 9.5%) both fall below the Kaiser threshold but are needed to reach 81.2% cumulative variance. PC6 and PC7 together account for 13.7% more variance (cumulative 94.9%), while PC8 (eigenvalue 0.404, variance 5.1%) completes the set. The 80% threshold lands exactly at PC5 cumulative variance of 81.2%.
What this can't tell you
The Kaiser criterion and 80% threshold are statistical heuristics; your choice of how many components to retain should also consider the interpretability and practical utility of the remaining dimensions in your downstream analysis.
Feature Contributions
Which features define the first two components, scaled 0-100.
The short answer
SkinThickness and Age dominate the main structure (contributions 100 and 99.5), followed by Insulin (89.9) and Pregnancies (86.7). The remaining four features contribute less to the dominant pattern, with DiabetesPedigreeFunction the weakest at 52.
The detail
Feature contributions to PC1 and PC2 combined are ranked: SkinThickness 100, Age 99.5, Insulin 89.9, Pregnancies 86.7, Glucose 75.2, BMI 75, BloodPressure 71.7, and DiabetesPedigreeFunction 52. The top two features (SkinThickness and Age) are tightly clustered and substantially ahead of the rest. The contribution metric weights each feature's loadings on the first two components by the variance those components explain, so it captures which columns define the dataset's main axes of variation. DiabetesPedigreeFunction varies mostly independently of the dominant pattern.
What this can't tell you
High contribution to PC1+PC2 does not mean a feature is clinically important—only that it aligns with the main statistical structure. Features with low contributions may still carry clinical signal on other dimensions (PC3–PC8).
Observation Map (PC1 vs PC2)
Every observation positioned on the two dominant components.
The short answer
Observations spread across a loose cloud on the two dominant components (together 47.8% of variance), with no tight clustering or clear separation between outcome groups. The range spans from about −3.8 to 2.4 on PC1 and −2.2 to 3.3 on PC2, indicating substantial heterogeneity.
The detail
PC1 (26.2% variance) is driven by BMI and SkinThickness; PC2 (21.6% variance) is driven by Age. Observation scores range from −3.785 to 2.415 on PC1 and −2.183 to 3.296 on PC2, showing wide dispersion. The two outcome groups (labeled 0 and 1) are heavily overlapped across the plot, indicating that the dominant two components do not capture the main differences between groups. Observations are distributed throughout the space rather than forming distinct clusters, consistent with the moderate variance explained by PC1+PC2 (47.8%).
What this can't tell you
Heavy overlap between outcome groups on PC1–PC2 does not mean they are identical—only that their main differences lie on the remaining components (PC3–PC8). A more detailed analysis of group separation would require examining all 5 or 7 components needed for 80–90% variance.
Component Loadings
How strongly each feature defines each leading component.
| Feature | Pc1 | Pc2 | Pc3 |
|---|---|---|---|
| Pregnancies | -0.128 | 0.594 | -0.013 |
| Glucose | -0.393 | 0.174 | 0.468 |
| BloodPressure | -0.36 | 0.184 | -0.535 |
| SkinThickness | -0.44 | -0.332 | -0.238 |
| Insulin | -0.435 | -0.251 | 0.337 |
| BMI | -0.452 | -0.101 | -0.362 |
| DiabetesPedigreeFunction | -0.271 | -0.122 | 0.433 |
| Age | -0.198 | 0.621 | 0.075 |
The short answer
PC1 is defined by three features loading near −0.44 to −0.45: BMI (−0.452), SkinThickness (−0.44), and Insulin (−0.435). PC2 is defined by Age (0.621) and Pregnancies (0.594), both positive, indicating these age-related measures move together on the second axis.
The detail
PC1 loadings: BMI −0.452, SkinThickness −0.44, Insulin −0.435, BloodPressure −0.36, Glucose −0.393. These five features load similarly (all negative), suggesting they co-vary as a syndrome. PC2 loadings: Age 0.621, Pregnancies 0.594 (positive); SkinThickness −0.332, Insulin −0.251 (negative). Features with near-zero loadings on PC1 and PC2 (e.g., DiabetesPedigreeFunction on PC1: −0.271) carry information the dominant structure does not capture. PC3 is driven by BloodPressure (−0.535) and Glucose (0.468).
What this can't tell you
Loadings describe linear relationships. If clinical features interact nonlinearly or in domain-specific combinations, those patterns will not appear in the loadings. Naming components in clinical language requires domain expertise beyond the statistical loadings.
Principal Component Analysis — Structure Finder
Runs PCA on the numeric feature columns the user selects, revealing how many independent dimensions the data really has, which features move together, and how observations spread across the leading components.
Why This Method?
PCA is the standard first tool for understanding high-dimensional data: it compresses correlated features into a few independent components, quantifies how much signal each carries, and exposes the groups of features that vary together — before any modelling is attempted.
What This Analysis Covers
- Variance explained per component (scree) + how many components you need
- Component summary table (eigenvalues, cumulative variance)
- Which features drive the leading components (loadings + contributions)
- Observation map on the first two components (optional group coloring)
Standard Library
Platform standard-library module (LAT-1441): runs on ANY dataset via the semantic mapping {feature_1..feature_N, optional label}. All narrative is derived from the user's own column names and computed values.
suppressPackageStartupMessages(library(htmltools))
suppressPackageStartupMessages(library(jsonlite))
suppressPackageStartupMessages(library(plotly))
suppressPackageStartupMessages(library(DT))
suppressPackageStartupMessages(library(htmlwidgets))
suppressPackageStartupMessages(library(arrow))
suppressPackageStartupMessages(library(knitr))
suppressPackageStartupMessages(library(rmarkdown))
suppressPackageStartupMessages(library(dplyr))
suppressPackageStartupMessages(library(tidyr))
suppressPackageStartupMessages(library(ggplot2))
suppressPackageStartupMessages(library(stringr))
suppressPackageStartupMessages(library(lubridate))
suppressPackageStartupMessages(library(broom))
suppressPackageStartupMessages(library(Matrix))
suppressPackageStartupMessages(library(cluster))
suppressPackageStartupMessages(library(data.table))Step 1: Discover mapped feature columns
initial_rows <- nrow(df)
feat_cols <- grep("^feature_[0-9]+$", names(df), value = TRUE)
feat_cols <- feat_cols[order(as.integer(sub("^feature_", "", feat_cols)))]
if (length(feat_cols) < 2) {
stop("column_mapping must map at least two feature columns(feature_1, feature_2)")
}
feature_names <- setNames(humanize_semantic(feat_cols, col_map), feat_cols)
has_label <- "label" %in% names(df)
label_name <- if (has_label) humanize_semantic("label", col_map) else ""Step 2: Coerce features numeric (95% rule); impute median; drop unusable
dropped_features <- character(0)
for (fc in feat_cols) {
v <- df[[fc]]
if (!is.numeric(v)) {
conv <- suppressWarnings(as.numeric(as.character(v)))
n_orig <- sum(!is.na(v) & as.character(v) != "")
if (n_orig > 0 && sum(!is.na(conv)) >= 0.95 * n_orig) {
df[[fc]] <- conv
} else {
dropped_features <- c(dropped_features, fc)
next
}
}
v <- df[[fc]]
med <- median(v, na.rm = TRUE)
if (is.na(med)) { dropped_features <- c(dropped_features, fc); next }
v[is.na(v)] <- med
df[[fc]] <- v
if (isTRUE(var(v) == 0) || is.na(var(v))) {
dropped_features <- c(dropped_features, fc)
}
}
used_features <- setdiff(feat_cols, dropped_features)
if (length(used_features) < 2) {
stop(paste0(
"PCA needs at least two usable numeric feature columns; only ",
length(used_features), " remained after cleaning. Excluded: ",
paste(feature_names[dropped_features], collapse = ", ")
))
}
X <- df[, used_features, drop = FALSE]
final_rows <- nrow(X)
rows_removed <- initial_rows - final_rows
if (final_rows < 10) {
stop(sprintf("Only %d usable rows — PCA needs at least 10.", final_rows))
}Step 3: PCA on standardized features
pca <- prcomp(X, center = TRUE, scale. = TRUE)
eig <- pca$sdev^2
var_pct <- 100 * eig / sum(eig)
cum_pct <- cumsum(var_pct)
n_comp_all <- length(eig)
n_show <- min(n_comp_all, 10)
variance_df <- data.frame(
component = paste0("PC", seq_len(n_show)),
variance_pct = round(var_pct[seq_len(n_show)], 1),
cumulative_pct = round(cum_pct[seq_len(n_show)], 1),
stringsAsFactors = FALSE
)
components_df <- data.frame(
component = paste0("PC", seq_len(n_show)),
eigenvalue = round(eig[seq_len(n_show)], 3),
variance_pct = round(var_pct[seq_len(n_show)], 1),
cumulative_pct = round(cum_pct[seq_len(n_show)], 1),
stringsAsFactors = FALSE
)
n_for_80 <- which(cum_pct >= 80)[1]
n_for_90 <- which(cum_pct >= 90)[1]
n_kaiser <- sum(eig > 1)Step 4: Loadings (signed, first 3 PCs) with humanized feature names
n_pcs_load <- min(3, n_comp_all)
L <- pca$rotation[, seq_len(n_pcs_load), drop = FALSE]
loadings_df <- data.frame(
feature = unname(feature_names[used_features]),
stringsAsFactors = FALSE
)
for (j in seq_len(n_pcs_load)) {
loadings_df[[paste0("pc", j)]] <- round(L[, j], 3)
}Step 5: Feature contributions — |loading| weighted by PC1+PC2 variance, 0-100
w <- var_pct[seq_len(min(2, n_comp_all))]
contrib_raw <- as.vector(abs(L[, 1]) * w[1] +
(if (n_comp_all >= 2) abs(L[, 2]) * w[2] else 0))
contributions_df <- data.frame(
feature = unname(feature_names[used_features]),
contribution = round(100 * contrib_raw / max(contrib_raw), 1),
stringsAsFactors = FALSE
)
contributions_df <- contributions_df[order(-contributions_df$contribution), , drop = FALSE]
rownames(contributions_df) <- NULLStep 6: Scores map (PC1 vs PC2), <=1000 sample; label always present
set.seed(42)
sidx <- if (final_rows > 1000) sample(final_rows, 1000) else seq_len(final_rows)
lab_vals <- if (has_label) {
lv <- as.character(df$label)
lv[is.na(lv) | trimws(lv) == ""] <- "Missing"
tab <- sort(table(lv), decreasing = TRUE)
if (length(tab) > 10) {
keep <- names(tab)[1:10]
lv[!(lv %in% keep)] <- "Other"
}
lv
} else {
rep("All rows", final_rows)
}
scores_df <- data.frame(
pc1 = round(pca$x[sidx, 1], 3),
pc2 = if (n_comp_all >= 2) round(pca$x[sidx, 2], 3) else 0,
label = lab_vals[sidx],
stringsAsFactors = FALSE
)
rownames(scores_df) <- NULLStep 7: Narrative anchors
pc1_order <- order(-abs(L[, 1]))
top_pc1_features <- unname(feature_names[used_features])[pc1_order][
seq_len(min(3, length(used_features)))]
pc2_order <- if (n_comp_all >= 2) order(-abs(L[, 2])) else pc1_order
top_pc2_feature <- unname(feature_names[used_features])[pc2_order][1]
metrics <- list(
`Observations` = final_rows,
`Features Analysed` = length(used_features),
`PC1 Variance %` = round(var_pct[1], 1),
`Components for 80%` = as.integer(n_for_80),
`Kaiser Components` = as.integer(n_kaiser),
`Top PC1 Feature` = top_pc1_features[1]
)
json_output <- list(
answer = paste0(
"PCA on ", length(used_features), " features across ",
format(final_rows, big.mark = ","), " rows: PC1 explains ",
round(var_pct[1], 1), "% of variance(",
n_for_80, " component(s) reach 80%, Kaiser criterion keeps ",
n_kaiser, "). PC1 is driven by ",
paste(top_pc1_features, collapse = ", "), "."
),
cards = lapply(
c("tldr", "overview", "preprocessing", "variance_explained",
"components_table", "feature_contributions", "scores_scatter",
"loadings_table"),
function(cid) list(id = cid, metrics = metrics)
)
)
list(
initial_rows = initial_rows,
final_rows = final_rows,
rows_removed = rows_removed,
feature_names = feature_names,
used_features = used_features,
dropped_features = dropped_features,
has_label = has_label,
label_name = label_name,
pca = pca,
variance_df = variance_df,
components_df = components_df,
loadings_df = loadings_df,
contributions_df = contributions_df,
scores_df = scores_df,
n_for_80 = n_for_80,
n_for_90 = n_for_90,
n_kaiser = n_kaiser,
top_pc1_features = top_pc1_features,
top_pc2_feature = top_pc2_feature,
var_pct = var_pct,
metrics = metrics,
json_output = json_output
)
}Compute shared resources
shared <- compute_shared(df, params, col_map)