Standard Data Screening
Executive Summary

Executive Summary

Readiness of 12 columns for parametric analysis

Columns Screened
12
Approximately Normal
0
Non-Normal or Skewed
12
Extreme Outliers Flagged
2019
Nonparametric Recommended
12
Most Non-Normal Column
babies
Of 12 screened columns, 0 look approximately normal and 12 are skewed or heavy-tailed. The single most non-normal column is babies (Shapiro-Wilk p < 0.001). Across the columns, 2019 extreme and 13294 mild outliers were flagged. All 12 testable columns would be better served by a nonparametric method or a transformation.
What this means

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.

Overview

Analysis Overview

Normality and outlier screening across 12 columns and 119,390 observations.

N Observations119390
N Columns12
N Normal0
N Nonparametric12
What this means

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 Preparation

Data Quality

Column typing, constant-column handling, and any Shapiro-Wilk sampling.

Initial Rows119390
Final Rows119390
Columns Screened12
Columns Dropped0
What this means

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.

Data Table

Normality by Column

Shape, Shapiro-Wilk normality test, and a verdict for each column.

ColumnNSkewKurtosisShapiro PVerdict
lead time1193901.351.7<0.001right-skewed
stays in weekend nights1193901.387.17<0.001right-skewed
stays in week nights1193902.8624.28<0.001right-skewed
adults11939018.321352<0.001right-skewed
children1193864.1118.67<0.001right-skewed
babies11939024.651634<0.001right-skewed
previous cancellations11939024.46674<0.001right-skewed
booking changes119390679.39<0.001right-skewed
days in waiting list11939011.94186.8<0.001right-skewed
adr11939010.531013<0.001right-skewed
required car parking spaces1193904.1630<0.001right-skewed
total of special requests1193901.351.49<0.001right-skewed
What this means

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.

Data Table

Outliers by Column

IQR-fence outlier counts and a Grubbs test on the most extreme value.

ColumnMild OutliersExtreme OutliersMost Extreme ValueGrubbs P
lead time3005181737<0.001
stays in weekend nights2653319<0.001
stays in week nights3354143850<0.001
adults0055<0.001
children0010<0.001
babies0010<0.001
previous cancellations0026<0.001
booking changes0021<0.001
days in waiting list00391<0.001
adr37933275,400<0.001
required car parking spaces008<0.001
total of special requests28774050.003
What this means

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.

Visualization

Skewness by Column

Absolute skewness per column against the plus-one readiness line.

What this means

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.

Rate this report Was this the answer you needed?
The exact source that produced this report — yours to keep, read, and re-run.
Download PDF
How this was computed method · R source · citation
The code that did it

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
  )
}
Your data has more stories to tell.Run any analysis on your own data — validated R modules, interactive reports, AI insights, and PDF export. 500 free credits on signup.
Try Free — No SignupSign Up Free

Cite this analysis

Report an Issue

Tell us what's wrong. You'll get a free re-run of this analysis so you can try again with different parameters. If the re-run still doesn't meet your expectations, we'll refund your credits.

Want to run this analysis on your own data? Upload CSV — Free Analysis See Pricing