Executive Summary
Readiness of 12 columns for parametric analysis
The short answer
All 12 columns are non-normal and unsuitable for parametric tests without transformation or nonparametric alternatives. The export contains 2019 extreme outliers and 13,294 mild outliers, with babies as the single most non-normal column (Shapiro-Wilk p < 0.001).
The detail
Of the 12 screened columns, 0 are approximately normal and 12 are skewed or heavy-tailed. Extreme outliers (beyond 3 times the interquartile range) total 2019; mild outliers total 13,294. The Shapiro-Wilk test rejects normality in all 12 columns at p < 0.001. Babies shows the most severe departure from normality, making it the highest-risk column for parametric inference. All 12 columns should be analyzed using nonparametric methods or after explicit transformation.
What this can't tell you
This screen does not reveal whether outliers represent genuine booking anomalies (e.g., group reservations, ultra-premium rates) or data quality errors. Inspection of individual extreme cases—such as the 55 adults in a single booking or the 5,400 ADR value—is needed to decide whether to trim, transform, or retain them.
Analysis Overview
Normality and outlier screening across 12 columns and 119,390 observations.
The short answer
Before running t-tests, ANOVAs, or regressions on this hotel bookings export, the data must pass normality and outlier checks—these parametric methods assume roughly symmetric, tail-light distributions and fail silently on skewed data. This screen examines all 12 numeric columns across 119,390 observations to flag shape problems and extreme values that could distort results.
The detail
The analysis measures skewness (0 = symmetric; ±1 = marked lean) and excess kurtosis (0 = normal bell; positive = heavy tails) for each column, runs Shapiro-Wilk on a 5,000-row random sample per column (standard practice at n > 5,000), and counts outliers using the 1.5× and 3× interquartile-range fences plus a Grubbs test on the single most extreme value. Screening identifies problems but does not fix them; transformation or trimming remain deliberate choices downstream.
What this can't tell you
This screen detects shape and extremes in isolation. It does not assess whether outliers are data-entry errors versus legitimate rare events, nor does it evaluate whether a transformation would be effective for the intended analysis. Consider reviewing the source system for entry-validation rules and domain knowledge about plausible booking patterns (e.g., whether 55 adults in a single booking is operationally possible).
Data Quality
Column typing, constant-column handling, and any Shapiro-Wilk sampling.
The short answer
All 12 numeric columns were successfully screened; no columns were dropped or imputed, preserving the data as recorded. Shapiro-Wilk was run on a random sample of 5,000 rows per column because the full 119,390 observations would render the test overly sensitive to negligible departures from normality.
The detail
119,390 rows loaded and retained. 12 columns held more than 5,000 values each, so Shapiro-Wilk was computed on a random sample of 5,000 (seed 42) rather than the full set—a standard approach when sample size is so large that even tiny deviations become statistically significant. Four missing values per column were set aside rather than imputed, so each shape metric reflects only the values actually present and observed.
What this can't tell you
Sample-based Shapiro-Wilk testing is more conservative than full-sample testing and may miss genuine but subtle non-normality; however, the skewness and kurtosis figures reported here are computed on all available rows, so they reflect the true shape. If precise p-values at the full sample size matter for a specific downstream test, consider running that test's own diagnostic on the full data.
Normality by Column
Shape, Shapiro-Wilk normality test, and a verdict for each column.
| Column | N | Skew | Kurtosis | Shapiro P | Verdict |
|---|---|---|---|---|---|
| lead time | 119390 | 1.35 | 1.7 | <0.001 | right-skewed |
| stays in weekend nights | 119390 | 1.38 | 7.17 | <0.001 | right-skewed |
| stays in week nights | 119390 | 2.86 | 24.28 | <0.001 | right-skewed |
| adults | 119390 | 18.32 | 1352 | <0.001 | right-skewed |
| children | 119386 | 4.11 | 18.67 | <0.001 | right-skewed |
| babies | 119390 | 24.65 | 1634 | <0.001 | right-skewed |
| previous cancellations | 119390 | 24.46 | 674 | <0.001 | right-skewed |
| booking changes | 119390 | 6 | 79.39 | <0.001 | right-skewed |
| days in waiting list | 119390 | 11.94 | 186.8 | <0.001 | right-skewed |
| adr | 119390 | 10.53 | 1013 | <0.001 | right-skewed |
| required car parking spaces | 119390 | 4.16 | 30 | <0.001 | right-skewed |
| total of special requests | 119390 | 1.35 | 1.49 | <0.001 | right-skewed |
The short answer
Every column fails the normality test (Shapiro-Wilk p < 0.001 for all 12), and all 12 lean right. The most extreme departures are babies (skewness 24.65, kurtosis 1633.85), previous cancellations (skewness 24.46, kurtosis 674.03), and adults (skewness 18.32, kurtosis 1352.04)—these are far beyond the ±1 skewness threshold for approximate symmetry.
The detail
All 12 columns reject normality at p < 0.001. Skewness ranges from 1.35 (lead time, total of special requests) to 24.65 (babies); all are positive, indicating right lean. Excess kurtosis ranges from 1.49 (total of special requests) to 1633.85 (babies), showing heavy right tails. Columns with skewness > 1 are considered markedly skewed: 10 of 12 columns exceed this threshold. The verdict for all 12 is "right-skewed."
What this can't tell you
Shapiro-Wilk p-values do not quantify practical significance—they only confirm that the data depart from normality. The magnitude of skewness and kurtosis is more informative for deciding whether a transformation (e.g., log, square root) might help or whether a nonparametric method is safer. Consider piloting a transformation on a subset to assess whether it brings skewness below ±1.
Outliers by Column
IQR-fence outlier counts and a Grubbs test on the most extreme value.
| Column | Mild Outliers | Extreme Outliers | Most Extreme Value | Grubbs P |
|---|---|---|---|---|
| lead time | 3005 | 181 | 737 | <0.001 |
| stays in weekend nights | 265 | 33 | 19 | <0.001 |
| stays in week nights | 3354 | 1438 | 50 | <0.001 |
| adults | 0 | 0 | 55 | <0.001 |
| children | 0 | 0 | 10 | <0.001 |
| babies | 0 | 0 | 10 | <0.001 |
| previous cancellations | 0 | 0 | 26 | <0.001 |
| booking changes | 0 | 0 | 21 | <0.001 |
| days in waiting list | 0 | 0 | 391 | <0.001 |
| adr | 3793 | 327 | 5,400 | <0.001 |
| required car parking spaces | 0 | 0 | 8 | <0.001 |
| total of special requests | 2877 | 40 | 5 | 0.003 |
The short answer
Stays in week nights carries the most extreme outliers (1438 beyond the 3× IQR fence), and adr carries the most mild outliers (3793). Every column's most extreme value is confirmed as a genuine statistical outlier by Grubbs test (p < 0.001 or p = 0.003), ruling out data entry errors as the sole cause.
The detail
Mild outliers (1.5× IQR fence) are concentrated in four columns: adr (3793), lead time (3005), stays in week nights (3354), and total of special requests (2877). Extreme outliers (3× IQR fence) are dominated by stays in week nights (1438) and lead time (181). All 12 columns show at least one value flagged by Grubbs test: 11 columns at p < 0.001 and total of special requests at p = 0.003. Specific extreme values include a 737-day lead time, a 50-night stay in week nights, a 5,400 ADR, and 55 adults in a single booking.
What this can't tell you
High outlier counts do not necessarily indicate data quality problems—they may reflect genuine business variation (group bookings, premium rates, last-minute cancellations). The distribution of extreme values across multiple columns suggests either real heterogeneity in booking behavior or systematic data collection issues; domain inspection of a sample of these flagged rows would clarify which.
Skewness by Column
Absolute skewness per column against the plus-one readiness line.
The short answer
All 12 columns exceed the skewness guideline of ±1, meaning their distributions lean far enough that parametric methods (t-test, ANOVA, linear regression) risk misleading inference. Use nonparametric alternatives (Mann-Whitney U, Kruskal-Wallis, Spearman correlation) or apply a transformation (log, square root, Box-Cox) before parametric testing.
The detail
Absolute skewness by column (highest to lowest): babies 24.65, previous cancellations 24.46, adults 18.32, days in waiting list 11.94, adr 10.53, booking changes 6.00, required car parking spaces 4.16, children 4.11, stays in week nights 2.86, stays in weekend nights 1.38, total of special requests 1.35, lead time 1.35. The guide line at |skewness| = 1 marks approximate symmetry; all 12 columns cross it, so all are better served by nonparametric methods or transformation.
What this can't tell you
This chart does not show which transformation (if any) would be effective for your specific analysis. If you plan to use parametric methods, pilot a log or square-root transformation on the most skewed columns (babies, previous cancellations, adults) and re-screen to see whether skewness drops below ±1. The choice between nonparametric methods and transformation depends on your sample size, the strength of the effect you expect to detect, and whether interpretability of transformed coefficients matters.
Normality & Outlier Screening — Is Your Data Ready?
Screens each numeric column the user maps before a parametric test: per column it reports n, mean, median, standard deviation, skewness and excess kurtosis, runs the Shapiro-Wilk normality test, counts outliers with the 1.5 and 3 times IQR fences plus a Grubbs test on the single most extreme value, flags robust-z outliers (median/MAD), and returns a clear parametric-versus-nonparametric recommendation.
Why This Method?
The t-test, ANOVA, correlation and linear regression all lean on roughly normal, outlier-free columns. A five-minute screen up front — normality test, skew and kurtosis, and an outlier count — tells you whether those assumptions hold or whether a nonparametric method or a transformation is the safer path, before a single misleading p-value is reported.
What This Analysis Covers
- Per-column shape: mean, median, spread, skewness, excess kurtosis
- Shapiro-Wilk normality test (sampled to 5,000 when a column is larger)
- Outliers by the 1.5 and 3 times IQR fences, plus a Grubbs test
- A parametric-versus-nonparametric recommendation for each column
Standard Library
Platform standard-library module (LAT-1441): runs on ANY dataset via the semantic mapping {column_1..column_N}. All narrative is derived from the user's own column names and computed values.
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))Core Analysis Pipeline
p-value: invert G -> t, then Bonferroni over n observations (two-sided)
u <- (n * G^2) / ((n - 1)^2)
if (u >= 1) {
p <- 0
} else {
t2 <- u * (n - 2) / (1 - u)
tval <- sqrt(t2)
p <- n * 2 * pt(-tval, df = n - 2)
}
p <- min(1, max(0, p))critical G at alpha via the two-sided t quantile
tcrit <- qt(alpha / (2 * n), df = n - 2, lower.tail = FALSE)
crit <- ((n - 1) / sqrt(n)) * sqrt(tcrit^2 / (n - 2 + tcrit^2))
list(G = G, crit = as.numeric(crit), p = p,
value = as.numeric(x[i]), flagged = isTRUE(G > crit))
}
compute_shared <- function(df, params, col_map = list()) {
# === SHARED EXPORTS ===
# initial_rows/final_rows/rows_removed $ row accounting
# col_names $ named character — semantic -> humanized names
# used_cols $ character — semantic names screened (incl. constant)
# dropped_cols $ character — excluded (non-numeric / empty)
# stats $ per-column list of computed stats (aligned to used_cols)
# normality_df $ column, n, skew, kurtosis, shapiro_p, verdict
# outliers_df $ column, mild_outliers, extreme_outliers,
# most_extreme_value, grubbs_p
# skew_by_column_df $ column, abs_skew (bar; constant/NA -> 0)
# n_used/n_constant $ counts
# n_normal/n_nonnormal/n_nonparam $ verdict + recommendation counts
# total_mild/total_extreme $ outlier totals across columns
# n_sampled $ how many columns were Shapiro-sampled at 5,000
# most_name/most_p $ single most non-normal column + its Shapiro p
# metrics / json_output
# === /SHARED EXPORTS ===Step 1: Discover mapped columns (column_1..column_N)
initial_rows <- nrow(df)
col_cols <- grep("^column_[0-9]+$", names(df), value = TRUE)
col_cols <- col_cols[order(as.integer(sub("^column_", "", col_cols)))]
if (length(col_cols) < 1) {
stop("column_mapping must map at least one numeric column(column_1).")
}
col_names <- setNames(humanize_semantic(col_cols, col_map), col_cols)Step 2: Coerce numeric (95% rule); drop non-numeric and report them.
Unlike relationship tools, screening keeps the actual values — missing entries are set aside per column (na.omit), never imputed, so the shape and outlier counts describe the data as it stands.
dropped_cols <- character(0)
for (cc in col_cols) {
v <- df[[cc]]
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[[cc]] <- conv
} else {
dropped_cols <- c(dropped_cols, cc); next
}
}
v <- suppressWarnings(as.numeric(df[[cc]]))
if (all(is.na(v))) { dropped_cols <- c(dropped_cols, cc); next }
df[[cc]] <- v
}
used_cols <- setdiff(col_cols, dropped_cols)
if (length(used_cols) < 1) {
stop(paste0("Screening needs at least one usable numeric column; none of ",
"the mapped columns(",
paste(unname(col_names[col_cols]), collapse = ", "),
") were numeric."))
}
final_rows <- initial_rows
rows_removed <- 0L
if (final_rows < 10) stop(sprintf("Only %d rows — screening needs at least 10.", final_rows))Step 3: Per-column screen
n, moments, Shapiro-Wilk (sampled >5,000, seed 42), IQR + Grubbs + robust-z outliers, verdict, and a parametric-vs-nonparametric call.
stats <- list()
for (cc in used_cols) {
hn <- col_names[[cc]]
x_all <- suppressWarnings(as.numeric(df[[cc]]))
x <- x_all[is.finite(x_all)]
n <- length(x)
n_missing <- sum(!is.finite(x_all))
mean_v <- if (n > 0) mean(x) else NA_real_
median_v <- if (n > 0) median(x) else NA_real_
sd_v <- if (n > 1) sd(x) else NA_real_
constant <- (n < 2) || !is.finite(sd_v) || sd_v == 0
sk <- if (constant) NA_real_ else skewness(x)
ku <- if (constant) NA_real_ else excess_kurtosis(x)Shapiro-Wilk: valid for 3 <= n <= 5000; sample larger columns.
shapiro_p <- NA_real_
sampled <- FALSE
shap_n <- n
if (!constant && n >= 3) {
xs <- x
if (n > 5000) {
set.seed(42)
xs <- x[sample(n, 5000)]
sampled <- TRUE
shap_n <- 5000L
}
sp <- tryCatch(shapiro.test(xs)$p.value, error = function(e) NA_real_)
shapiro_p <- as.numeric(sp)
}IQR fences: mild = beyond 1.5 x IQR, extreme = beyond 3 x IQR.
mild <- 0L; extreme <- 0L
if (n > 0 && !constant) {
q <- quantile(x, c(0.25, 0.75), names = FALSE, type = 7)
iqr <- q[2] - q[1]
if (is.finite(iqr) && iqr > 0) {
mild <- as.integer(sum(x < q[1] - 1.5 * iqr | x > q[2] + 1.5 * iqr))
extreme <- as.integer(sum(x < q[1] - 3 * iqr | x > q[2] + 3 * iqr))
}
}Grubbs on the single most extreme value.
gr <- grubbs_one(x)Robust-z outliers: |(x - median) / (1.4826 * MAD)| > 3.5.
rz_flagged <- 0L
if (n > 0 && !constant) {
med <- median(x)
mad_s <- 1.4826 * median(abs(x - med))
if (!is.finite(mad_s) || mad_s == 0) mad_s <- sd_v
if (is.finite(mad_s) && mad_s > 0) {
rz_flagged <- as.integer(sum(abs((x - med) / mad_s) > 3.5))
}
}
verdict <- if (constant) {
"constant(no variation)"
} else if (!is.na(sk) && sk > 1) {
"right-skewed"
} else if (!is.na(sk) && sk < -1) {
"left-skewed"
} else if (!is.na(ku) && ku > 2) {
"heavy-tailed"
} else if (!is.na(shapiro_p) && shapiro_p >= 0.05) {
"approximately normal"
} else if (!is.na(shapiro_p) && shapiro_p < 0.05) {
"mildly non-normal"
} else {
"approximately normal"
}
nonparam <- (!is.na(shapiro_p) && shapiro_p < 0.05) ||
(!is.na(sk) && abs(sk) > 1)
recommendation <- if (constant) {
"constant — excluded from testing"
} else if (nonparam) {
"consider a nonparametric method or transformation"
} else {
"parametric methods are appropriate"
}
stats[[cc]] <- list(
name = hn, n = n, n_missing = n_missing,
mean = mean_v, median = median_v, sd = sd_v, constant = constant,
skew = sk, kurtosis = ku, abs_skew = if (is.na(sk)) 0 else abs(sk),
shapiro_p = shapiro_p, sampled = sampled, shap_n = shap_n,
mild = mild, extreme = extreme,
grubbs_G = gr$G, grubbs_crit = gr$crit, grubbs_p = gr$p,
grubbs_value = gr$value, grubbs_flagged = gr$flagged,
most_extreme_value = gr$value, robust_z_flagged = rz_flagged,
verdict = verdict, recommendation = recommendation, nonparam = nonparam
)
}Step 5: Roll-up counts + single most non-normal column (NA-guarded)
constant_vec <- vapply(used_cols, function(cc) isTRUE(stats[[cc]]$constant), logical(1))
verdict_vec <- vapply(used_cols, function(cc) stats[[cc]]$verdict, character(1))
shapiro_vec <- vapply(used_cols, function(cc) stats[[cc]]$shapiro_p, numeric(1))
absskew_vec <- vapply(used_cols, function(cc) stats[[cc]]$abs_skew, numeric(1))
nonparam_vec <- vapply(used_cols, function(cc) isTRUE(stats[[cc]]$nonparam), logical(1))
n_used <- length(used_cols)
n_constant <- sum(constant_vec)
n_testable <- n_used - n_constant
n_normal <- sum(verdict_vec == "approximately normal")
n_nonnormal <- max(0L, n_testable - n_normal)
n_nonparam <- sum(nonparam_vec)
total_mild <- sum(vapply(used_cols, function(cc) stats[[cc]]$mild, integer(1)))
total_extreme <- sum(vapply(used_cols, function(cc) stats[[cc]]$extreme, integer(1)))
n_sampled <- sum(vapply(used_cols, function(cc) isTRUE(stats[[cc]]$sampled), logical(1)))Most non-normal = lowest Shapiro p among testable columns; NA-filter BEFORE which.min (LAT-1445 crash class). Fall back to largest |skew|.
testable_idx <- which(!constant_vec)
most_name <- "none"; most_p <- NA_real_
if (length(testable_idx) > 0) {
sp_ok <- testable_idx[is.finite(shapiro_vec[testable_idx])]
if (length(sp_ok) > 0) {
mi <- sp_ok[which.min(shapiro_vec[sp_ok])]
} else {
as_ok <- testable_idx[is.finite(absskew_vec[testable_idx])]
mi <- if (length(as_ok) > 0) as_ok[which.max(absskew_vec[as_ok])] else testable_idx[1]
}
most_name <- hn_vec[mi]
most_p <- shapiro_vec[mi]
}
metrics <- list(
`Columns Screened` = as.integer(n_used),
`Approximately Normal` = as.integer(n_normal),
`Non-Normal or Skewed` = as.integer(n_nonnormal),
`Extreme Outliers Flagged` = as.integer(total_extreme),
`Nonparametric Recommended`= as.integer(n_nonparam),
`Most Non-Normal Column` = most_name
)
json_output <- list(
answer = paste0(
"Screened ", n_used, " numeric column", .s(n_used), " across ",
format(final_rows, big.mark = ","), " row", .s(final_rows), ": ",
n_normal, " look", if (n_normal == 1) "s" else "",
" approximately normal and ", n_nonnormal, " ",
if (n_nonnormal == 1) "is" else "are", " skewed or non-normal. ",
total_extreme, " extreme outlier", .s(total_extreme),
" were found across the columns. ",
if (most_name != "none") paste0(
"The single most non-normal column is ", most_name,
" (Shapiro-Wilk ", fmt_pp(most_p), "). ") else "",
n_nonparam, " column", .s(n_nonparam), " ",
if (n_nonparam == 1) "is" else "are",
" better served by a nonparametric method or a transformation."
),
cards = lapply(
c("tldr", "overview", "preprocessing", "normality_table",
"outlier_summary", "recommendation"),
function(cid) list(id = cid, metrics = metrics)
)
)
list(
initial_rows = initial_rows, final_rows = final_rows,
rows_removed = rows_removed,
col_names = col_names, used_cols = used_cols, dropped_cols = dropped_cols,
stats = stats,
normality_df = normality_df, outliers_df = outliers_df,
skew_by_column_df = skew_by_column_df,
n_used = n_used, n_constant = n_constant, n_testable = n_testable,
n_normal = n_normal, n_nonnormal = n_nonnormal, n_nonparam = n_nonparam,
total_mild = total_mild, total_extreme = total_extreme,
n_sampled = n_sampled, most_name = most_name, most_p = most_p,
metrics = metrics, json_output = json_output
)
}