Standard Interrupted Time Series
Executive Summary

Executive Summary

Did the intervention shift the level or the slope of Monthly Revenue?

Time Points
48
Level Change
-12.892
Slope Change
1.549
p (level)
< 0.001
p (slope)
< 0.001
Lag-1 Autocorrelation
0.09
Gap at Series End
22.729
At 2023-01-01, Monthly Revenue shows both an immediate drop of 12.89 (95% CI -15.31 to -10.48, p < 0.001) and a change in trend of 1.549 per period (95% CI 1.374 to 1.723, p < 0.001) — a pattern consistent with an intervention effect. By the end of the series the fitted values sit 22.73 (12.69%) away from where the pre-intervention trend was headed. NOTE: no intervention date was provided, so the break was ASSUMED at the series midpoint (2022-12-17) — provide an intervention_date parameter to place it where the intervention actually happened. This is a single-series observational design: any other event at the same time would be indistinguishable from the intervention.
Suggested Interpretation

The short answer

Both the revenue level and growth trend shifted in month 25. Revenue dropped immediately by 12.89 units, but the monthly growth rate then accelerated sharply, leaving revenue 22.73 units above where the old trend would have taken it by the end of the series. Both effects are statistically robust.

The detail

At the assumed intervention point (2023-01-01), Monthly Revenue shows an immediate level drop of -12.892 (95% CI -15.31 to -10.48, p < 0.001) and a trend break of 1.549 per period (95% CI 1.374 to 1.723, p < 0.001). The lag-1 residual autocorrelation is 0.09, low enough that classical OLS inference is reliable. By the series end, the fitted values sit 22.729 units ahead of the pre-intervention trend projection.

What this can't tell you

The intervention date was assumed at the series midpoint rather than provided — every estimate depends on that placement. As a single-series observational design, any other event at the same time would be indistinguishable from the pricing change itself.

Overview

Analysis Overview

Interrupted time series on Monthly Revenue: segmented regression split at 2023-01-01.

N Points48
Level Change-12.892
Slope Change1.549
Gap At End22.729
Suggested Interpretation

The short answer

The pricing change in month 25 (2023-01-01) had two distinct effects: revenue dropped immediately by 12.89, then began growing much faster afterward. The old trend was adding 0.609 per month; the new one adds 1.549 per month. By the end of the data, the faster growth had more than offset the initial drop.

The detail

Segmented regression splits the intervention's impact into two components measured exactly at the break. The level change is -12.892 (95% CI -15.31 to -10.48, p < 0.001) — an immediate drop. The slope change is 1.549 per period (95% CI 1.374 to 1.723, p < 0.001) — the pre-intervention trend of 0.609 per period steepened to 2.158 per period afterward. By the final observation the fitted series sits 22.73 units (12.69% of the counterfactual level) above where the old trend would have gone.

What this can't tell you

The intervention point was assumed at the series midpoint (2022-12-17) rather than provided; if the actual pricing change occurred on a different date, all estimates shift accordingly. No control series exists, so any concurrent event — seasonal pattern, market shift, data collection change — cannot be ruled out as an alternative explanation.

Data Preparation

Data Preparation

How the series was assembled and where the break was placed.

Initial Rows48
Final Rows48
Rows Removed0
Suggested Interpretation

The short answer

All 48 monthly observations were retained without loss. The intervention point was placed at the midpoint of the date range (2022-12-17) because no explicit date was provided, creating 24 pre-intervention and 24 post-intervention points with the first post-break month at 2023-01-01.

The detail

No rows were dropped: 48 rows loaded, 48 used, forming 48 time points. The preprocessing step flags that no intervention_date parameter was supplied, so the break was assumed at the series' temporal midpoint rather than placed at the actual pricing change date. This assumption is critical: all coefficient estimates, confidence intervals, and p-values depend on this placement. The split produces exactly 24 pre-intervention and 24 post-intervention observations, with the first post-period point dated 2023-01-01.

What this can't tell you

Without the true intervention date, we cannot confirm whether the break is correctly positioned. Providing the actual pricing change date would anchor all estimates to the real event timing rather than a statistical midpoint assumption.

Visualization

Actual, Fitted, and Counterfactual

The series split at 2023-01-01, with the pre-trend projected forward.

Suggested Interpretation

The short answer

The fitted line shows a sharp downward jump at the 2023-01-01 break, then diverges upward from the counterfactual (old trend) with increasing velocity. The vertical gap at the break is the level change (-12.89); the widening gap afterward is the slope change (1.549 per period) in action.

The detail

Three series overlay the chart: actual Monthly Revenue, the segmented model's fit, and the counterfactual (pre-intervention trend extended forward). At the break, the fitted and counterfactual lines separate vertically by -12.89 (the level change). Thereafter they diverge linearly: the gap grows by 1.549 units each period. By the final observation (2024-12-01) the gap reaches 22.73 units. The counterfactual would place revenue at 179.097; the model places it at 201.826. This separation pattern is consistent with an intervention effect, though any concurrent event would paint the identical picture.

What this can't tell you

Visual separation at the break does not prove causation. The design observes a single series with no control group; confounding events cannot be ruled out. The projection grows more speculative the further it extends past the break.

Data Table

Level and Slope Changes

Pre and post slopes, the jump at the break, and the trend break — each with its confidence interval.

MeasureEstimateDetail
Pre-intervention slope0.609Change in Monthly Revenue per period before 2023-01-01 (95% CI 0.486 to 0.732)
Post-intervention slope2.158Change in Monthly Revenue per period from 2023-01-01 on (95% CI 2.035 to 2.281)
Level change at intervention-12.89Immediate jump at 2023-01-01; 95% CI -15.31 to -10.48; p < 0.001
Slope change at intervention1.549Trend break at 2023-01-01; 95% CI 1.374 to 1.723; p < 0.001
Suggested Interpretation

The short answer

The pricing change fractured the growth trajectory. Pre-intervention, revenue was climbing 0.609 per month. Post-intervention, it climbed 2.158 per month—a 1.549-unit steeper slope. Both the immediate drop (-12.89) and the trend acceleration (1.549) are statistically indistinguishable from zero with less than 0.1% probability under the model.

The detail

Pre-intervention slope: 0.609 per period (95% CI 0.486 to 0.732). Post-intervention slope: 2.158 per period (95% CI 2.035 to 2.281). Level change at intervention: -12.892 (95% CI -15.31 to -10.48, p < 0.001). Slope change at intervention: 1.549 (95% CI 1.374 to 1.723, p < 0.001). Both confidence intervals exclude zero, indicating changes larger than residual noise would produce by chance.

What this can't tell you

The confidence intervals assume classical OLS conditions hold and no residual autocorrelation biases them. Lag-1 autocorrelation is 0.09 (low), so this approximation is reasonable here. The intervals do not account for the midpoint assumption on intervention placement.

Data Table

The Counterfactual Gap

Actual versus the pre-trend projection at the end of the series, in units and percent.

MeasureEstimateDetail
Counterfactual Monthly Revenue at series end179.1Where the pre-intervention trend would have put Monthly Revenue by 2024-12-01 had nothing changed
Model-fitted Monthly Revenue at series end201.8Where the segmented model places Monthly Revenue at 2024-12-01 given the level and slope changes
Gap at series end (units)22.73Fitted minus counterfactual at 2024-12-01 (22.73)
Gap at series end (% of counterfactual)12.69Relative to the counterfactual level of 179.10 (12.69%)
Average post-period gap (units)4.918Mean fitted-minus-counterfactual gap across the 24 post-intervention points
Suggested Interpretation

The short answer

The pricing change left revenue 22.73 units above where the old trend would have taken it by series end—a 12.69% gain relative to the counterfactual level. The gap averaged 4.92 units across all 24 post-intervention months, growing as the steeper slope compounded.

The detail

Counterfactual Monthly Revenue at series end (2024-12-01): 179.097. Model-fitted Monthly Revenue at series end: 201.826. Gap at end (units): 22.729. Gap at end (% of counterfactual): 12.69%. Average post-period gap: 4.918 units. The gap widens monotonically because the slope change is positive (1.549 per period): each month the faster post-intervention trend pulls further ahead of the old trend's projection.

What this can't tell you

This projection assumes the pre-intervention trend would have continued unchanged. The further past the break the projection extends, the more speculative it becomes. Any structural shift in the underlying business (market saturation, competitive entry, demand shock) would violate the constant-trend assumption and render the counterfactual unreliable.

Data Table

Method & Assumptions

The design, the model, the autocorrelation check, and the assumptions the causal read rests on.

ItemDetail
DesignObservational interrupted time series (segmented regression) on a single series — no control group. Not a randomized experiment.
Intervention pointNo intervention_date parameter was provided, so the intervention point was ASSUMED at the midpoint of the observed date range (2022-12-17) — points after it count as post-intervention. This midpoint is an assumption, not data: provide an intervention_date parameter to place the break where the intervention actually happened.
ModelOLS regression of Monthly Revenue on time, a post-intervention indicator, and time-since-intervention: the indicator's coefficient is the level change and the time-since coefficient is the slope change.
InferenceClassical OLS standard errors and 95% confidence intervals. No autocorrelation-robust (e.g. Newey-West) correction is applied — when residual autocorrelation is high, the intervals here are too narrow.
Autocorrelation checkThe lag-1 residual autocorrelation is 0.09, low enough that classical OLS standard errors are a reasonable approximation for this series.
CounterfactualThe pre-intervention trend extended forward unchanged; the gap between it and the fitted series at the end of the data is the cumulative divergence.
Key assumptionsThe pre-intervention trend would have continued unchanged absent the intervention; no other event shifted the series at the same time — any concurrent event is indistinguishable from the intervention itself; the series' measurement is consistent throughout.
Suggested Interpretation

The short answer

Segmented OLS regression on a single revenue series, interrupted at an assumed midpoint. The level and slope changes are the regression coefficients at the break. No randomization, no control group, no autocorrelation correction—only the assumption that nothing else moved the series at the same time.

The detail

Design: observational interrupted time series (segmented regression) on Monthly Revenue alone—no control group or randomization. Intervention point: assumed at the series midpoint (2022-12-17) because no explicit date was provided; this placement is an assumption, not data. Model: OLS regression of revenue on time, a post-intervention indicator, and time-since-intervention; the indicator coefficient is the level change (-12.892), the time-since coefficient is the slope change (1.549). Inference: classical OLS standard errors and 95% confidence intervals; no autocorrelation-robust correction applied. Lag-1 residual autocorrelation: 0.09 (low, so classical intervals are reasonable). Counterfactual: pre-intervention trend extended forward unchanged.

What this can't tell you

The causal reading rests entirely on the assumption that no other event shifted the series at the intervention date. A concurrent campaign, seasonal pattern, data collection change, or market shock would be indistinguishable from the pricing intervention itself. The midpoint assumption anchors all results; the true pricing date may differ.

Methodology

Methodology

Statistical methodology and diagnostics for Interrupted Time Series

Statistical Method

Interrupted Time Series

Standard-library analysis: did the intervention shift the level or the slope? Map a date column and a numeric series, tell it when the intervention happened (or let it assume the series midpoint, clearly flagged), and get a segmented regression: the immediate jump in the series at the break (level change) and the bend in the trend (slope change), each with a 95% confidence interval and p-value, plus the pre-intervention trend projected forward as a counterfactual and the gap between actual and counterfactual at the end of the series, in units and percent. A lag-1 residual autocorrelation check warns when the classical p-values are anti-conservative.

Data
N = 48 observations
Assumptions
  • The pre-intervention trend would have continued unchanged absent the intervention
  • No other event shifted the series at the same time as the intervention
  • The trend on each side of the break is approximately linear
  • The series is measured consistently before and after the break
Limitations
  • This is a single-series observational design with no control group — the result is consistent with an intervention effect, never proof of one
  • Any event coinciding with the intervention date is indistinguishable from the intervention itself
  • Standard errors are classical OLS — when residual autocorrelation exceeds 0.3 the report warns that intervals and p-values are anti-conservative (no Newey-West correction is applied)
  • If no intervention date is provided, the break is assumed at the series midpoint and flagged — the estimates hang on that placement
Software & Citation
MCP Analytics · mcpanalytics.ai
Code Appendix

Analysis Code

Complete R source code for this analysis

Core Analysis Pipeline

compute_shared <- function(df, params, col_map = list()) {
  # === SHARED EXPORTS ===
  #   initial_rows/final_rows/rows_removed  $ row accounting
  #   n_dropped            $ rows dropped for missing date/value
  #   date_h/value_h       $ humanized user names for prose
  #   n_points             $ distinct time points used
  #   agg_note             $ note when several rows per date were averaged
  #   split_rule           $ how the intervention point was chosen
  #   midpoint_used        $ TRUE when the midpoint fallback was assumed
  #   int_date_str         $ ISO date used as the break (or midpoint date)
  #   t0/n_pre/n_post      $ break index + points each side
  #   b0/b1 (pre trend)    $ intercept + pre slope
  #   level/level_se/level_p/level_lo/level_hi  $ LEVEL change (jump)
  #   slope/slope_se/slope_p/slope_lo/slope_hi  $ SLOPE change (trend break)
  #   post_slope/post_slope_lo/post_slope_hi    $ pre slope + slope change
  #   r1/ac_flag/ac_verdict $ lag-1 residual autocorrelation check
  #   cf_end/fit_end/gap/gap_pct/gap_pct_txt    $ counterfactual gap at end
  #   avg_gap              $ mean post-period gap (actual fit - counterfactual)
  #   its_trends_df        $ period_date (ISO), series_value, series_label
  #   segments_df          $ measure, estimate, detail
  #   gap_df               $ measure, estimate, detail
  #   methods_df           $ item, detail
  #   metrics / json_output
  # === /SHARED EXPORTS ===

Step 1: Resolve mapped columns (humanized for all prose)

initial_rows <- nrow(df)
  date_h  <- humanize_semantic("date", col_map)
  value_h <- humanize_semantic("value", col_map)
  for (req in c("date", "value")) {
    if (!(req %in% names(df))) {
      stop(sprintf("Interrupted time series needs &#x27;%s' (%s) mapped.",
                   humanize_semantic(req, col_map),
                   c(date = "the date/time column",
                     value = "the numeric series to analyse")[[req]]))
    }
  }

Step 2: Coerce the value to numeric (95% rule)

y_raw <- df$value
  if (!is.numeric(y_raw)) {
    ch <- as.character(y_raw)
    non_blank <- !is.na(ch) & trimws(ch) != ""
    conv <- suppressWarnings(as.numeric(ch))
    if (sum(non_blank) == 0 ||
        sum(!is.na(conv[non_blank])) < 0.95 * sum(non_blank)) {
      stop(sprintf("The value column &#x27;%s' does not look numeric — fewer than 95%% of its values parse as numbers. Pick a numeric column.",
                   value_h))
    }
    y_raw <- conv
  }

Step 3: Parse the date column (ISO, mdy, dmy, or integer years)

parse_dates_vec <- function(ch) {
    ok <- function(dd) sum(!is.na(dd)) >= 0.95 * sum(!is.na(ch) & trimws(ch) != "")
    d <- suppressWarnings(as.Date(ch, format = "%Y-%m-%d"))
    if (!ok(d)) d <- suppressWarnings(as.Date(lubridate::ymd(ch, quiet = TRUE)))
    if (!ok(d)) d <- suppressWarnings(as.Date(lubridate::mdy(ch, quiet = TRUE)))
    if (!ok(d)) d <- suppressWarnings(as.Date(lubridate::dmy(ch, quiet = TRUE)))
    if (!ok(d)) {
      nv <- suppressWarnings(as.numeric(ch))
      nv_ok <- stats::na.omit(nv)
      if (length(nv_ok) >= 0.95 * sum(!is.na(ch) & trimws(ch) != "") &&
          length(nv_ok) > 0 && all(nv_ok == round(nv_ok)) &&
          all(nv_ok >= 1900 & nv_ok <= 2100)) {
        d <- as.Date(ifelse(is.na(nv), NA, sprintf("%04d-01-01", nv)),
                     format = "%Y-%m-%d")
      }
    }
    if (ok(d)) d else NULL
  }
  parse_date_one <- function(s) {
    s <- trimws(as.character(s))
    d <- suppressWarnings(as.Date(s, format = "%Y-%m-%d"))
    if (is.na(d)) d <- suppressWarnings(as.Date(lubridate::ymd(s, quiet = TRUE)))
    if (is.na(d)) d <- suppressWarnings(as.Date(lubridate::mdy(s, quiet = TRUE)))
    if (is.na(d)) d <- suppressWarnings(as.Date(lubridate::dmy(s, quiet = TRUE)))
    if (is.na(d)) {
      ny <- suppressWarnings(as.numeric(s))
      if (!is.na(ny) && ny == round(ny) && ny >= 1900 && ny <= 2100)
        d <- as.Date(sprintf("%04d-01-01", ny))
    }
    d
  }

  d_ch <- trimws(as.character(df$date))
  dts <- parse_dates_vec(d_ch)
  if (is.null(dts)) {
    stop(sprintf("The date column &#x27;%s' could not be read as dates — fewer than 95%% of its values parse as calendar dates (or integer years). Map a date column so the series can be ordered in time.",
                 date_h))
  }

Step 4: Keep rows complete on date and value; average duplicates

keep <- !is.na(y_raw) & !is.na(dts)
  n_dropped <- sum(!keep)
  y_k <- y_raw[keep]; d_k <- dts[keep]
  if (length(y_k) < 4) {
    stop(sprintf("Only %d usable rows remained after dropping rows missing %s or %s — far too few for an interrupted time series.",
                 length(y_k), date_h, value_h))
  }
  agg <- stats::aggregate(list(y = y_k), by = list(date_iso = format(d_k, "%Y-%m-%d")), FUN = mean)
  agg <- agg[order(agg$date_iso), , drop = FALSE]
  n_points <- nrow(agg)
  final_rows <- length(y_k)
  rows_removed <- initial_rows - final_rows
  agg_note <- if (n_points < final_rows) {
    sprintf("Several rows share the same %s value, so the %s values were first averaged to one point per date(%s rows became %d points).",
            date_h, value_h, format(final_rows, big.mark = ","), n_points)
  } else ""
  if (n_points < 16) {
    stop(sprintf("The series in &#x27;%s' has only %d distinct time points — interrupted time series needs at least 16 (8 on each side of the intervention).",
                 date_h, n_points))
  }

  dd <- as.Date(agg$date_iso)
  dnum <- as.numeric(dd)
  yv <- agg$y
  idx <- seq_len(n_points)

Step 5: Place the intervention point (parameter or midpoint ASSUMPTION)

int_param <- params$intervention_date %||% params$treatment_date %||% NULL
  midpoint_used <- FALSE
  if (!is.null(int_param)) {
    tsd <- parse_date_one(int_param)
    if (is.na(tsd)) {
      stop(sprintf("The intervention_date parameter(&#x27;%s') could not be read as a date.",
                   as.character(int_param)))
    }
    is_post <- dnum >= as.numeric(tsd)
    int_date_str <- format(tsd, "%Y-%m-%d")
    split_rule <- sprintf("The intervention date was provided as %s: points on or after it count as post-intervention.",
                          int_date_str)
  } else {
    midpoint_used <- TRUE
    cut_v <- min(dnum) + (max(dnum) - min(dnum)) / 2
    is_post <- dnum > cut_v
    int_date_str <- format(as.Date(cut_v, origin = "1970-01-01"), "%Y-%m-%d")
    split_rule <- sprintf("No intervention_date parameter was provided, so the intervention point was ASSUMED at the midpoint of the observed date range(%s) — points after it count as post-intervention. This midpoint is an assumption, not data: provide an intervention_date parameter to place the break where the intervention actually happened.",
                          int_date_str)
  }

  n_pre <- sum(!is_post)
  n_post <- sum(is_post)
  if (n_pre < 8) {
    stop(sprintf("Only %d point(s) in &#x27;%s' fall before the intervention date (%s) — interrupted time series needs at least 8 points on each side of the break to separate a level shift from the pre-existing trend.",
                 n_pre, date_h, int_date_str))
  }
  if (n_post < 8) {
    stop(sprintf("Only %d point(s) in &#x27;%s' fall on or after the intervention date (%s) — interrupted time series needs at least 8 points on each side of the break to estimate the post-intervention trend.",
                 n_post, date_h, int_date_str))
  }
  t0 <- min(idx[is_post])
  break_date_str <- agg$date_iso[t0]

Step 6: Segmented OLS — value ~ time + post + time_since_intervention

post01 <- as.integer(is_post)
  tsi <- pmax(0L, idx - t0)
  af <- data.frame(y = yv, t = idx, post = post01, tsi = tsi)
  fit <- stats::lm(y ~ t + post + tsi, data = af)
  co <- summary(fit)$coefficients
  if (!all(c("t", "post", "tsi") %in% rownames(co)) ||
      any(is.na(coef(fit)[c("t", "post", "tsi")]))) {
    stop("The segmented regression could not be estimated — the series may be too short or degenerate around the intervention point.")
  }
  b0 <- unname(coef(fit)["(Intercept)"])
  b1 <- unname(coef(fit)["t"])
  level    <- unname(coef(fit)["post"])
  slope    <- unname(coef(fit)["tsi"])
  level_se <- co["post", "Std. Error"]; level_p <- co["post", 4]
  slope_se <- co["tsi", "Std. Error"];  slope_p <- co["tsi", 4]
  ci <- stats::confint(fit)
  level_lo <- ci["post", 1]; level_hi <- ci["post", 2]
  slope_lo <- ci["tsi", 1];  slope_hi <- ci["tsi", 2]
  b1_lo <- ci["t", 1]; b1_hi <- ci["t", 2]

Post slope = pre slope + slope change, with a combined-variance CI

V <- stats::vcov(fit)
  post_slope <- b1 + slope
  ps_se <- sqrt(V["t", "t"] + V["tsi", "tsi"] + 2 * V["t", "tsi"])
  tcrit <- stats::qt(0.975, df = fit$df.residual)
  post_slope_lo <- post_slope - tcrit * ps_se
  post_slope_hi <- post_slope + tcrit * ps_se

Step 7: Lag-1 residual autocorrelation check

res <- stats::residuals(fit)[order(idx)]
  r1 <- if (length(res) >= 10) {
    suppressWarnings(stats::cor(res[-1], res[-length(res)]))
  } else NA_real_
  ac_flag <- !is.na(r1) && abs(r1) > 0.3
  ac_verdict <- if (is.na(r1)) {
    "The lag-1 residual autocorrelation could not be computed."
  } else if (ac_flag) {
    sprintf("CAUTION: the lag-1 residual autocorrelation is %s — beyond the 0.3 threshold. This analysis uses classical OLS standard errors, so the confidence intervals and p-values reported here are anti-conservative(too narrow): treat any significance claim with caution, as the effective sample is smaller than the point count suggests.",
            r2(r1))
  } else {
    sprintf("The lag-1 residual autocorrelation is %s, low enough that classical OLS standard errors are a reasonable approximation for this series.",
            r2(r1))
  }

Step 8: Counterfactual projection and the gap at series end

t_end <- n_points
  cf_end  <- b0 + b1 * t_end
  fit_end <- b0 + b1 * t_end + level + slope * (t_end - t0)
  gap <- fit_end - cf_end
  gap_pct <- if (abs(cf_end) > 1e-8) 100 * gap / abs(cf_end) else NA_real_
  gap_pct_txt <- if (is.na(gap_pct)) "not defined(counterfactual level near zero)" else paste0(r2(gap_pct), "%")
  cf_v  <- b0 + b1 * idx
  fit_v <- as.numeric(stats::fitted(fit))
  avg_gap <- mean(fit_v[is_post] - cf_v[is_post])

Step 9: Chart dataset — actual, fitted, counterfactual (ISO dates)

keep_i <- idx
  if (n_points > 500) {
    keep_i <- sort(unique(c(1, n_points, t0 - 1, t0,
                            round(seq(1, n_points, length.out = 500)))))
  }
  cf_i <- keep_i[keep_i >= (t0 - 1)]   # counterfactual from the last pre point on
  its_trends_df <- rbind(
    data.frame(period_date = agg$date_iso[keep_i],
               series_value = round(yv[keep_i], 3),
               series_label = "Actual", stringsAsFactors = FALSE),
    data.frame(period_date = agg$date_iso[keep_i],
               series_value = round(fit_v[keep_i], 3),
               series_label = "Fitted", stringsAsFactors = FALSE),
    data.frame(period_date = agg$date_iso[cf_i],
               series_value = round(cf_v[cf_i], 3),
               series_label = "Counterfactual", stringsAsFactors = FALSE)
  )
  rownames(its_trends_df) <- NULL
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 Signup Sign Up Free

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