Standard Did
Executive Summary

Executive Summary

Did the change move employment? New Jersey (min wage raised) vs Pennsylvania (control), before vs after.

Observations
768
DiD Effect
2.75
95% CI
-0.65 to 6.15
p-value
0.113
Effect vs Counterfactual
15.15%
Parallel Trends
not testable
No statistically reliable effect of the change on employment was found: the estimate is 2.75 (95% CI -0.65 to 6.15, p = 0.113), and the interval includes zero. Relative to the level expected without the change (18.15), that is 15.15%. With a single pre period the parallel-trends assumption could not be tested — the causal reading rests on it untested. This is an observational comparison, not a randomized experiment.
What this means

The wage increase did not produce a statistically significant employment effect. The estimate is 2.75 jobs per store (95% CI −0.65 to 6.15, p = 0.113), with the confidence interval spanning zero. Relative to the counterfactual level of 18.15, this represents 15.15% of expected employment. The parallel-trends assumption—critical for the causal claim—could not be tested with only one pre-period observation per group. This remains an observational comparison without experimental controls.

Overview

Analysis Overview

Difference-in-differences on employment: New Jersey (min wage raised) (treated) vs Pennsylvania (control) (control), before vs after.

N Observations768
Did Estimate2.75
Treated Change0.467
Control Change-2.283
What this means

This analysis isolates the wage change's effect by subtracting Pennsylvania's natural employment movement from New Jersey's. Pennsylvania declined by 2.28 jobs per store while New Jersey rose by 0.47, yielding a difference-in-differences estimate of 2.75. This method removes seasonality, trends, and common shocks that a simple before-and-after would wrongly credit to the wage increase. The causal claim rests entirely on the parallel-trends assumption: that without the wage change, New Jersey would have moved like Pennsylvania.

Data Preparation

Data Preparation

How the groups and periods were identified, and what was dropped.

Initial Rows768
Final Rows768
Rows Removed0
What this means

All 768 rows were retained; no observations were dropped. Pennsylvania was identified as the control group by name, making New Jersey the treated group. Because no treatment start date was provided, the analysis split the period at the midpoint between 1992-02-19 and 1992-11-19—an assumption that should be refined if the exact implementation date is known. Observations were aggregated to one pre and one post value per store (384 units total), protecting the significance test against repeated measurements of the same location.

Visualization

The 2x2: Group Means Before and After

Mean employment per group, before and after the change.

What this means

New Jersey employment moved from 20.43 before the wage increase to 20.90 after, a gain of 0.47 jobs per store. Pennsylvania employment fell from 23.38 to 21.10, a loss of 2.28 jobs per store. The difference between these two movements—0.47 minus (−2.28)—is the 2.75 estimate. The level gap between states (New Jersey lower than Pennsylvania both before and after) is expected and is eliminated by the difference-in-differences method. The story lies in the divergence of the trends, not in the level difference.

Visualization

Treated vs Control Over Time

Mean employment per period for New Jersey (min wage raised) and Pennsylvania (control).

What this means

With only one pre-period and one post-period observation per group, the parallel-trends assumption cannot be tested. The data show New Jersey at 20.43 before and 20.90 after; Pennsylvania at 23.38 before and 21.10 after. To verify that the two groups moved in parallel absent the wage change would require multiple pre-period observations showing similar trajectories. A timeline with finer period granularity would allow direct inspection of whether the groups tracked together before the change and diverged after it.

Data Table

The Difference-in-Differences Estimate

Cell means, per-group changes, and the estimate with its confidence interval.

MeasureEstimateDetail
New Jersey (min wage raised) — pre mean20.43Mean employment for New Jersey (min wage raised) before the change
New Jersey (min wage raised) — post mean20.9Mean employment for New Jersey (min wage raised) after the change
New Jersey (min wage raised) — change0.467How much New Jersey (min wage raised) moved (0.47)
Pennsylvania (control) — pre mean23.38Mean employment for Pennsylvania (control) before the change
Pennsylvania (control) — post mean21.1Mean employment for Pennsylvania (control) after the change
Pennsylvania (control) — change-2.283How much Pennsylvania (control) moved anyway (-2.28)
Difference-in-differences2.7595% CI -0.65 to 6.15; p = 0.113
Effect vs counterfactual15.15Relative to the expected level of 18.15 absent the change (15.15%)
What this means

New Jersey's pre mean was 20.431 and post mean was 20.897, a change of 0.467. Pennsylvania's pre mean was 23.38 and post mean was 21.097, a change of −2.283. The difference between these changes—the difference-in-differences estimate—is 2.75 (95% CI −0.65 to 6.15, p = 0.113). The confidence interval includes zero, indicating the effect is not statistically significant at conventional levels. Relative to the counterfactual expected level of 18.15 absent the change, the estimate represents 15.15%.

Data Table

Method & Assumptions

The design, the identification choices made, and the assumptions the causal read rests on.

ItemDetail
DesignObservational difference-in-differences: the treated group's before-to-after change minus the control group's. Not a randomized experiment.
Treated group'Pennsylvania (control)' was identified as the control group from its name; 'New Jersey (min wage raised)' is therefore treated.
Period splitNo treatment start date was provided, so periods were split at the midpoint of the observed date range (1992-02-19 to 1992-11-19) — an assumption; provide a treatment_start parameter for a precise split.
ModelOLS regression of employment on group, period, and their interaction — the interaction term is the difference-in-differences estimate.
InferenceObservations were first averaged to one pre and one post value per store id (384 units), which guards the significance test against repeated measurements of the same unit.
Parallel trendsWith only one pre period per group in 'period', the parallel-trends assumption cannot be tested here — the causal reading rests on it untested.
Key assumptionsNo other shock hit only one group at the same time as the change; group composition is stable; absent the change, the treated group would have moved like the control group.
What this means

Difference-in-differences is observational: it claims causality only through untested assumptions. The estimate is the interaction term from ordinary least squares regression of employment on group, period, and their interaction. Pennsylvania was identified as control by name; New Jersey is treated. The period split at the midpoint of 1992-02-19 to 1992-11-19 because no treatment date was provided—a refinable assumption. Observations were averaged to one pre and one post value per store (384 units) to protect inference against repeated measurements. Parallel trends cannot be tested with a single pre-period per group, so the causal reading rests on an untestable assumption. The analysis assumes no other shock hit only one group when the wage change occurred and that group composition remained stable.

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

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 outcome/group/period
  #   outcome_h/group_h/period_h/unit_h  $ humanized user names for prose
  #   trt_lvl/ctl_lvl/trt_rule  $ which group value is treated + how decided
  #   period_rule          $ how periods were split into pre and post
  #   has_time/date_iso    $ whether a time axis exists + ISO strings (or NULL)
  #   n_pre_periods/n_post_periods  $ distinct periods each side of the split
  #   m_t_pre/m_t_post/m_c_pre/m_c_post  $ the 2x2 cell means (analysis frame)
  #   chg_t/chg_c          $ per-group pre-to-post change
  #   did/did_se/did_p/ci_low/ci_high  $ the DiD estimate (OLS interaction)
  #   cf/pct/pct_txt       $ counterfactual level + effect as % of it
  #   collapse_note/n_units  $ unit-collapse behavior (if unit mapped)
  #   pt_testable/pt_slope/pt_p/pt_short/pt_verdict  $ parallel-trends check
  #   cell_means_df        $ period_label, mean_outcome, group_label (4 rows)
  #   trends_df            $ period_date (ISO), mean_outcome, group_label
  #   did_table_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)
  outcome_h <- humanize_semantic("outcome", col_map)
  group_h   <- humanize_semantic("group", col_map)
  period_h  <- humanize_semantic("period", col_map)
  unit_h    <- humanize_semantic("unit", col_map)
  for (req in c("outcome", "group", "period")) {
    if (!(req %in% names(df))) {
      stop(sprintf("Difference-in-differences needs &#x27;%s' (%s) mapped.",
                   humanize_semantic(req, col_map),
                   c(outcome = "the numeric outcome",
                     group = "the treated/control indicator",
                     period = "the pre/post indicator or date")[[req]]))
    }
  }
  has_unit <- "unit" %in% names(df)

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

y_raw <- df$outcome
  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 outcome column &#x27;%s' does not look numeric — fewer than 95%% of its values parse as numbers. Pick a numeric column.",
                   outcome_h))
    }
    y_raw <- conv
  }

Step 3: Keep rows complete on outcome, group, and period (reported)

g_raw <- trimws(as.character(df$group))
  p_raw <- trimws(as.character(df$period))
  keep <- !is.na(y_raw) & !is.na(g_raw) & g_raw != "" & !is.na(p_raw) & p_raw != ""
  n_dropped <- sum(!keep)
  y <- y_raw[keep]; g <- g_raw[keep]; p <- p_raw[keep]
  u <- if (has_unit) trimws(as.character(df$unit))[keep] else NULL
  final_rows <- length(y)
  if (final_rows < 12) {
    stop(sprintf("Only %d usable rows remained after dropping rows missing %s, %s, or %s — at least 12 are needed for a difference-in-differences analysis.",
                 final_rows, outcome_h, group_h, period_h))
  }

Step 4: Identify the treated and control groups

g_lvls <- sort(unique(g))
  if (length(g_lvls) < 2) {
    stop(sprintf("The group column &#x27;%s' has only one distinct value ('%s') — difference-in-differences needs both a treated and a control group.",
                 group_h, g_lvls[1]))
  }
  if (length(g_lvls) > 2) {
    stop(sprintf("The group column &#x27;%s' has %d distinct values (%s) — difference-in-differences needs exactly two: a treated group and a control group.",
                 group_h, length(g_lvls),
                 paste(head(g_lvls, 5), collapse = ", ")))
  }
  low_g <- tolower(g_lvls)
  trt_subs <- c("treat", "test", "interven", "pilot", "exposed", "variant", "trial")
  ctl_subs <- c("control", "comparison", "holdout", "placebo", "untreated", "baseline", "standard")
  hits_trt <- sapply(low_g, function(s)
    any(sapply(trt_subs, function(k) grepl(k, s, fixed = TRUE))) || s %in% c("1", "yes", "true"))
  hits_ctl <- sapply(low_g, function(s)
    any(sapply(ctl_subs, function(k) grepl(k, s, fixed = TRUE))) || s %in% c("0", "no", "false"))
  if (sum(hits_trt & !hits_ctl) == 1) {
    trt_lvl <- g_lvls[hits_trt & !hits_ctl]
    trt_rule <- sprintf("&#x27;%s' was identified as the treated group from its name; '%s' is the control.",
                        trt_lvl, setdiff(g_lvls, trt_lvl))
  } else if (sum(hits_ctl & !hits_trt) == 1) {
    ctl_named <- g_lvls[hits_ctl & !hits_trt]
    trt_lvl <- setdiff(g_lvls, ctl_named)
    trt_rule <- sprintf("&#x27;%s' was identified as the control group from its name; '%s' is therefore treated.",
                        ctl_named, trt_lvl)
  } else {
    n_by_lvl <- table(g)
    trt_lvl <- names(n_by_lvl)[which.min(n_by_lvl)]
    trt_rule <- sprintf("Neither value of &#x27;%s' names a treated or control group, so the smaller group ('%s', %d rows) was ASSUMED to be treated — if that is wrong, the estimate's sign flips.",
                        group_h, trt_lvl, min(n_by_lvl))
  }
  ctl_lvl <- setdiff(g_lvls, trt_lvl)

Step 5: Split periods into pre and post (labels, dates, or numbers)

parse_dates_vec <- function(ch) {
    ok <- function(dd) sum(!is.na(dd)) >= 0.95 * length(ch)
    d <- suppressWarnings(as.Date(ch, format = "%Y-%m-%d"))
    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)) d <- suppressWarnings(as.Date(lubridate::ymd(ch, quiet = TRUE)))
    if (ok(d) && length(unique(stats::na.omit(d))) >= 2) 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::mdy(s, quiet = TRUE)))
    if (is.na(d)) d <- suppressWarnings(as.Date(lubridate::dmy(s, quiet = TRUE)))
    if (is.na(d)) d <- suppressWarnings(as.Date(lubridate::ymd(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
  }

  tv <- NULL          # numeric time value per row (NULL when labels)
  date_iso <- NULL    # ISO date string per row (NULL when no real dates)
  period_kind <- "label"
  dts <- parse_dates_vec(p)
  if (!is.null(dts)) {
    period_kind <- "date"
    tv <- as.numeric(dts)
    date_iso <- format(dts, "%Y-%m-%d")
  } else {
    pn <- suppressWarnings(as.numeric(p))
    if (sum(!is.na(pn)) >= 0.95 * length(p) &&
        length(unique(stats::na.omit(pn))) >= 2) {
      pn_ok <- stats::na.omit(pn)
      if (all(pn_ok == round(pn_ok)) && all(pn_ok >= 1900 & pn_ok <= 2100) &&
          length(unique(pn_ok)) > 2) {
        # Integer years -> treat as annual dates
        dts <- as.Date(ifelse(is.na(pn), NA, sprintf("%04d-01-01", pn)),
                       format = "%Y-%m-%d")
        period_kind <- "date"
        tv <- as.numeric(dts)
        date_iso <- format(dts, "%Y-%m-%d")
      } else if (length(unique(pn_ok)) == 2) {
        period_kind <- "binary_num"
        tv <- pn
      } else {
        period_kind <- "numeric"
        tv <- pn
      }
    }
  }
  if (period_kind != "label" && any(is.na(tv))) {
    keep2 <- !is.na(tv)
    n_dropped <- n_dropped + sum(!keep2)
    y <- y[keep2]; g <- g[keep2]; p <- p[keep2]; tv <- tv[keep2]
    if (!is.null(date_iso)) date_iso <- date_iso[keep2]
    if (!is.null(u)) u <- u[keep2]
    final_rows <- length(y)
  }
  rows_removed <- initial_rows - final_rows

  ts_param <- params$treatment_start %||% params$treatment_date %||% NULL
  if (period_kind == "date") {
    if (!is.null(ts_param)) {
      tsd <- parse_date_one(ts_param)
      if (is.na(tsd)) {
        stop(sprintf("The treatment_start parameter(&#x27;%s') could not be read as a date.",
                     as.character(ts_param)))
      }
      is_post <- tv >= as.numeric(tsd)
      period_rule <- sprintf("Periods were split at the provided treatment start(%s): dates on or after it count as post.",
                             format(tsd, "%Y-%m-%d"))
    } else {
      cut_v <- min(tv) + (max(tv) - min(tv)) / 2
      is_post <- tv > cut_v
      period_rule <- sprintf("No treatment start date was provided, so periods were split at the midpoint of the observed date range(%s to %s) — an assumption; provide a treatment_start parameter for a precise split.",
                             format(as.Date(min(tv), origin = "1970-01-01"), "%Y-%m-%d"),
                             format(as.Date(max(tv), origin = "1970-01-01"), "%Y-%m-%d"))
    }
  } else if (period_kind == "binary_num") {
    v_hi <- max(tv)
    is_post <- tv == v_hi
    period_rule <- sprintf("&#x27;%s' has two numeric values: %s was read as the pre period and %s as the post period (smaller value first).",
                           period_h, r2(min(tv)), r2(v_hi))
  } else if (period_kind == "numeric") {
    if (!is.null(ts_param) && !is.na(suppressWarnings(as.numeric(ts_param)))) {
      cut_v <- as.numeric(ts_param)
      is_post <- tv >= cut_v
      period_rule <- sprintf("Numeric periods were split at the provided treatment start(%s): values on or after it count as post.", r2(cut_v))
    } else {
      cut_v <- min(tv) + (max(tv) - min(tv)) / 2
      is_post <- tv > cut_v
      period_rule <- sprintf("No treatment start was provided, so the numeric periods were split at the midpoint of their range(%s) — an assumption; provide a treatment_start parameter for a precise split.", r2(cut_v))
    }
  } else {
    p_lvls <- unique(p)
    if (length(p_lvls) == 1) {
      stop(sprintf("The period column &#x27;%s' has only one distinct value ('%s') — difference-in-differences needs data from both before and after the change.",
                   period_h, p_lvls[1]))
    }
    if (length(p_lvls) > 2) {
      stop(sprintf("The period column &#x27;%s' has %d distinct non-date values — use a two-level before/after indicator or a date column.",
                   period_h, length(p_lvls)))
    }
    low_p <- tolower(p_lvls)
    pre_subs  <- c("pre", "before", "baseline", "prior", "begin", "start", "old", "early")
    post_subs <- c("post", "after", "follow", "end", "late", "new")
    is_pre_kw <- sapply(low_p, function(s)
      any(sapply(pre_subs, function(k) grepl(k, s, fixed = TRUE))) || s %in% c("0", "no", "false"))
    is_post_kw <- sapply(low_p, function(s)
      any(sapply(post_subs, function(k) grepl(k, s, fixed = TRUE))) || s %in% c("1", "yes", "true"))
    pre_lvl <- NULL
    if (sum(is_pre_kw & !is_post_kw) == 1) {
      pre_lvl <- p_lvls[is_pre_kw & !is_post_kw]
    } else if (sum(is_post_kw & !is_pre_kw) == 1) {
      pre_lvl <- setdiff(p_lvls, p_lvls[is_post_kw & !is_pre_kw])
    }
    if (is.null(pre_lvl) || length(pre_lvl) != 1) {
      stop(sprintf("Could not tell which value of &#x27;%s' is the before period ('%s' vs '%s') — use labels like pre/post or before/after, or a date column.",
                   period_h, p_lvls[1], p_lvls[2]))
    }
    is_post <- p != pre_lvl
    period_rule <- sprintf("&#x27;%s' was read as the pre period and '%s' as the post period from their labels.",
                           pre_lvl, setdiff(p_lvls, pre_lvl))
  }

  if (!any(is_post)) {
    stop(sprintf("All %d usable rows of &#x27;%s' fall before the change — there is no post-period data to compare.",
                 final_rows, period_h))
  }
  if (all(is_post)) {
    stop(sprintf("All %d usable rows of &#x27;%s' fall after the change — difference-in-differences needs pre-change (baseline) data for both groups.",
                 final_rows, period_h))
  }

  trt01 <- as.integer(g == trt_lvl)
  post01 <- as.integer(is_post)
  has_time <- !is.null(tv)
  n_pre_periods <- if (has_time) length(unique(tv[post01 == 0])) else 1L
  n_post_periods <- if (has_time) length(unique(tv[post01 == 1])) else 1L

Step 6: Analysis frame — optionally collapse to unit x period means

af <- data.frame(y = y, trt = trt01, post = post01)
  collapse_note <- ""
  n_units <- NA_integer_
  if (has_unit) {
    agg <- stats::aggregate(list(y = af$y),
                            by = list(unit = u, trt = af$trt, post = af$post),
                            FUN = mean)
    af <- data.frame(y = agg$y, trt = agg$trt, post = agg$post)
    n_units <- length(unique(u))
    collapse_note <- sprintf("Observations were first averaged to one pre and one post value per %s(%d units), which guards the significance test against repeated measurements of the same unit.",
                             unit_h, n_units)
  }
  cell_n <- table(factor(af$trt, levels = 0:1), factor(af$post, levels = 0:1))
  for (ti in c("1", "0")) {
    for (pi in c("0", "1")) {
      if (cell_n[ti, pi] < 2) {
        lvl_name <- if (ti == "1") trt_lvl else ctl_lvl
        stop(sprintf("Group &#x27;%s' has only %d observation(s) in the %s period — at least 2 observations are needed in each of the four %s-by-%s cells.",
                     lvl_name, cell_n[ti, pi],
                     if (pi == "1") "post" else "pre",
                     group_h, period_h))
      }
    }
  }
  if (nrow(af) < 8) {
    stop(sprintf("Only %d observations remained in the analysis frame — at least 8 are needed to estimate the difference-in-differences model.",
                 nrow(af)))
  }

Step 7: The 2x2 cell means and the OLS interaction estimate

cm <- tapply(af$y, list(factor(af$trt, levels = 0:1),
                          factor(af$post, levels = 0:1)), mean)
  m_c_pre  <- cm["0", "0"]; m_c_post <- cm["0", "1"]
  m_t_pre  <- cm["1", "0"]; m_t_post <- cm["1", "1"]
  chg_t <- m_t_post - m_t_pre
  chg_c <- m_c_post - m_c_pre

  fit <- stats::lm(y ~ trt * post, data = af)
  sm <- summary(fit)$coefficients
  if (!("trt:post" %in% rownames(sm)) || is.na(coef(fit)["trt:post"])) {
    stop("The difference-in-differences model could not be estimated — the group and period columns may be confounded(e.g. one group observed only pre, the other only post).")
  }
  did    <- unname(coef(fit)["trt:post"])
  did_se <- sm["trt:post", "Std. Error"]
  did_p  <- sm["trt:post", 4]
  ci <- stats::confint(fit)["trt:post", ]
  ci_low <- unname(ci[1]); ci_high <- unname(ci[2])

Step 8: Effect size in outcome units and as % of the counterfactual

cf <- m_t_pre + chg_c
  pct <- if (abs(cf) > 1e-8) 100 * did / abs(cf) else NA_real_
  pct_txt <- if (is.na(pct)) "not defined(counterfactual level near zero)" else paste0(r2(pct), "%")

Step 9: Parallel-trends check (needs a time axis + >= 2 pre periods)

pt_testable <- FALSE
  pt_slope <- NA_real_; pt_p <- NA_real_
  if (has_time) {
    pre_i <- post01 == 0
    if (length(unique(tv[pre_i])) >= 2 && length(unique(g[pre_i])) == 2) {
      t_idx <- match(tv, sort(unique(tv)))
      dpre <- data.frame(y = y[pre_i], t = t_idx[pre_i], trt = trt01[pre_i])
      pfit <- tryCatch(stats::lm(y ~ t * trt, data = dpre), error = function(e) NULL)
      if (!is.null(pfit)) {
        pco <- summary(pfit)$coefficients
        if ("t:trt" %in% rownames(pco) && !is.na(pco["t:trt", 1])) {
          pt_slope <- pco["t:trt", 1]
          pt_p <- pco["t:trt", 4]
          pt_testable <- TRUE
        }
      }
    }
  }
  if (!pt_testable) {
    pt_short <- "not testable"
    pt_verdict <- sprintf("With only one pre period per group in &#x27;%s', the parallel-trends assumption cannot be tested here — the causal reading rests on it untested.",
                          period_h)
  } else if (pt_p >= 0.05) {
    pt_short <- "consistent"
    pt_verdict <- sprintf("Across the %d pre-change periods the two groups&#x27; trends do not detectably diverge (slope difference %s per period, %s) — consistent with, though not proof of, parallel trends.",
                          n_pre_periods, r3(pt_slope), fmt_pp(pt_p))
  } else {
    pt_short <- "divergent"
    pt_verdict <- sprintf("The two groups were already trending apart before the change(slope difference %s per period, %s) — the difference-in-differences estimate is likely biased, and any causal conclusion should be treated with strong caution.",
                          r3(pt_slope), fmt_pp(pt_p))
  }
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