Executive Summary
Price elasticity of Total Volume with respect to AveragePrice
The short answer
Avocado demand is elastic: a 1% price increase is associated with a 1.18% decrease in volume (95% CI 1.10% to 1.26%). This means revenue moves in the opposite direction from price — price cuts are consistent with higher revenue in the observed data.
The detail
Estimated price elasticity: −1.184 (95% CI −1.265 to −1.103, p<0.001). The confidence interval excludes −1.0, confirming elastic demand at conventional significance levels across 18,249 observations. The model explains 70.2% of variation in log volume. Because prices were observed rather than randomized, this elasticity is associational: seasonality, promotions, and cost-driven repricing can move price and volume together in historical data in ways a controlled experiment might not replicate.
What this can't tell you
The historical association does not guarantee the causal effect of a price change. A pricing experiment would be needed to confirm whether cutting prices would actually increase revenue in practice.
Analysis Overview
Log-log OLS elasticity of Total Volume with respect to AveragePrice on 18,249 observations.
The short answer
Avocado demand is price-sensitive: a 1% increase in price is associated with a 1.184% decrease in quantity sold. This elastic demand means price cuts are associated with higher total revenue in the observed data. The pattern holds across 18,249 observations after accounting for type and region differences.
The detail
The analysis uses log-log ordinary least squares regression on 18,249 observations. The elasticity coefficient is −1.184, meaning demand exceeds unit elastic. The model explains 70.2% of the variation in log Total Volume. Because AveragePrice was observed rather than randomized, the −1.184 slope describes association in historical data: it reflects how quantity moved together with price across different types and regions, holding those factors constant. The elasticity magnitude of 1.184 exceeds 1, classifying demand as elastic.
What this can't tell you
The observed prices were not set experimentally, so the −1.184 elasticity describes historical association, not the guaranteed revenue outcome of a specific price change. Generalization to future or counterfactual prices depends on whether the relationship remains stable. A longer time window or transaction-level data would clarify whether elasticity varies by season or market segment.
Data Quality
Row filtering for the log transform, and control handling.
The short answer
All 18,249 rows in the dataset had positive prices and volumes, so no rows were dropped. Every observation was eligible for the log transform and included in the regression, ensuring the full sample was used.
The detail
Initial rows loaded: 18,249; final rows used: 18,249; rows removed: 0. No observations had zero or negative AveragePrice or Total Volume. Categorical controls (type, region) were lumped to at most 8 levels; numeric controls were median-imputed. No missing values required exclusion.
What this can't tell you
Data quality checks confirm structural completeness for the log-log regression but do not assess whether the price and volume measurements themselves are accurate or whether the data capture the full transaction universe (e.g., direct-to-consumer sales not in the dataset).
Demand Curve
Median Total Volume at each AveragePrice decile.
The short answer
The demand curve slopes sharply downward across the observed price range (0.44 to 3.25), with median volume falling from 858,069.55 at the lowest price decile to 9,529.15 at the highest. The smooth, consistent decline supports the constant-elasticity model: there are no kinks or flat stretches that would suggest different price regimes.
The detail
Each of the 10 price deciles shows a median volume point. At AveragePrice = 0.83, median Total Volume = 858,069.55; at AveragePrice = 2.12, it = 9,529.15. Intermediate deciles show a steady downward progression (e.g., 335,108.11 at 1.00, 117,579.21 at 1.32, 24,059.71 at 1.54). The smooth pattern across all deciles is consistent with a single elasticity coefficient rather than a piecewise or kinked relationship.
What this can't tell you
The decile medians mask within-decile variation and do not reveal whether individual transactions cluster tightly around the median or scatter widely. A transaction-level export would show the full distribution and identify any outlier subgroups.
Elasticity Estimates
The log-log regression coefficients with 95% confidence intervals.
| Term | Estimate | CI Low | CI High | P Value | Interpretation |
|---|---|---|---|---|---|
| log(AveragePrice) | -1.184 | -1.265 | -1.103 | <0.001 | A 1% increase in AveragePrice is associated with a 1.18% decrease in Total Volume. |
| type: organic | -3.23 | -3.277 | -3.184 | <0.001 | Rows at this level show about 96.0% lower Total Volume than the baseline level, holding AveragePrice constant. |
| region: Atlanta | 1.479 | 1.29 | 1.669 | <0.001 | Rows at this level show about 339.1% higher Total Volume than the baseline level, holding AveragePrice constant. |
| region: BaltimoreWashington | 2.233 | 2.045 | 2.422 | <0.001 | Rows at this level show about 833.2% higher Total Volume than the baseline level, holding AveragePrice constant. |
| region: Boise | -0.201 | -0.391 | -0.012 | 0.038 | Rows at this level show about 18.2% lower Total Volume than the baseline level, holding AveragePrice constant. |
| region: Boston | 1.737 | 1.548 | 1.926 | <0.001 | Rows at this level show about 468.3% higher Total Volume than the baseline level, holding AveragePrice constant. |
| region: BuffaloRochester | 0.532 | 0.344 | 0.721 | <0.001 | Rows at this level show about 70.3% higher Total Volume than the baseline level, holding AveragePrice constant. |
| region: California | 4.093 | 3.904 | 4.283 | <0.001 | Rows at this level show about 5,895% higher Total Volume than the baseline level, holding AveragePrice constant. |
| region: Charlotte | 1.011 | 0.822 | 1.2 | <0.001 | Rows at this level show about 174.9% higher Total Volume than the baseline level, holding AveragePrice constant. |
| region: Other | 1.744 | 1.609 | 1.88 | <0.001 | Rows at this level show about 472.1% higher Total Volume than the baseline level, holding AveragePrice constant. |
The short answer
The price elasticity of −1.184 (95% CI −1.265 to −1.103, p<0.001) is the decisive row: it directly answers the question of price sensitivity. Both bounds are below −1, confirming elastic demand. Type and region controls are all statistically significant (all p<0.001 except Boise at p=0.038) and show large volume differences across product lines and geographies, but they do not change the price elasticity.
The detail
log(AveragePrice) coefficient: −1.184, CI [−1.265, −1.103], p<0.001. Type organic: −3.23 (p<0.001), meaning organic rows show about 96.0% lower volume than conventional, holding price constant. Region controls range from −0.201 (Boise, p=0.038) to +4.093 (California, p<0.001), reflecting large regional demand differences. All region and type estimates are significant at the 0.05 level, confirming systematic geographic and product-line effects.
What this can't tell you
The controls explain regional and type differences but do not clarify which factors drive them (population, climate, distribution infrastructure). Finer-grained controls (e.g., store count, local income) would sharpen understanding of regional leverage.
Fit and Classification
Model fit, the elasticity classification, and the revenue direction.
| Metric | Value | Interpretation |
|---|---|---|
| R-squared (log-log fit) | 0.702 | The model explains a strong share of the variation in log Total Volume. |
| Observations used | 18,249 | Rows with positive AveragePrice and Total Volume used in the regression. |
| Rows dropped (nonpositive or missing) | 0 | No rows were dropped — every row had positive price and quantity. |
| Elasticity classification | elastic | The magnitude of the elasticity exceeds 1: demand responds more than proportionally to price. |
| Revenue direction (associational) | price cuts increase revenue | Read from the sign of 1 plus the elasticity; associational because prices were not randomized. |
The short answer
The model fits well: it explains 70.2% of the variation in log Total Volume across all 18,249 rows. Demand is classified as elastic, and the data are consistent with a regime where price cuts increase revenue—though this is an association, not proof of a causal effect.
The detail
R-squared is 0.702, a strong fit for a cross-sectional relationship. All 18,249 observations had positive price and quantity; 0 rows were dropped. The elasticity classification is elastic because the magnitude (1.184) exceeds 1. The sign of 1 plus the elasticity (1 + (−1.184) = −0.184) is negative, which implies that in the historical data, price cuts are associated with revenue increases. This reading is associational because prices were observed, not randomized.
What this can't tell you
Fit quality does not establish causation: the 0.702 R-squared confirms the log-log model captures much of the historical relationship, but does not prove that lowering prices will mechanically raise revenue in a future setting. Seasonal or market-segment variation in elasticity would not appear in a single aggregate coefficient. Consider a segment-level export to test whether elasticity is stable across avocado types or regions.
Price Elasticity of Demand — How Price-Sensitive Are Your Customers?
Estimates the price elasticity of demand from transactional or aggregated rows via log-log OLS: regress log(quantity) on log(price) plus optional controls. The slope on log(price) is the elasticity — the percent change in demand associated with a 1% change in price.
Why This Method?
On the log-log scale a constant-elasticity demand curve is a straight line, so one OLS slope answers the pricing question directly, with a confidence interval and a significance test. Comparing the magnitude of the elasticity to 1 gives the revenue direction: elastic demand means price cuts raise revenue, inelastic demand means price increases do.
What This Analysis Covers
- Elasticity estimate with 95% CI and significance
- Elastic / inelastic classification (CI-aware)
- Associational revenue direction (dRev/dP sign = 1 + elasticity)
- Ten-decile observed demand curve
- Control-variable adjustments (promo, season, region, ...)
Standard Library
Platform standard-library module (LAT-1441): runs on ANY dataset via the semantic mapping {price, quantity, control_1..control_5}. All narrative is derived from the user's own column names and computed values. Prices in user data are observed, not randomized — every conclusion is phrased as association, not causation.
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
compute_shared <- function(df, params, col_map = list()) {
# === SHARED EXPORTS ===
# initial_rows/final_rows/rows_removed $ row accounting
# rows_nonpositive / rows_missing $ why rows were dropped
# h_price / h_quantity $ humanized column names
# used_controls / dropped_controls / ctrl_names $ control accounting
# elasticity / ci_low / ci_high / p_value $ slope on log(price)
# r2 / n_used $ fit quality
# demand_class $ "elastic"|"inelastic"|"unit elastic"
# unit_in_ci / zero_in_ci $ CI-aware qualifiers
# rev_direction $ sign(1+e) revenue clause
# price_min / price_max $ observed price range
# decile_df $ data.frame(price_level, median_quantity)
# results_df $ elasticity_results table
# fitq_df $ fit_quality table
# metrics / json_output
# === /SHARED EXPORTS ===Step 1: Locate and coerce the price and quantity columns
initial_rows <- nrow(df)
h_price <- humanize_semantic("price", col_map)
h_quantity <- humanize_semantic("quantity", col_map)
h_by_key <- c(price = h_price, quantity = h_quantity)
for (key in c("price", "quantity")) {
if (!key %in% names(df)) {
stop(sprintf("The %s column(%s) was not found in the mapped data.",
key, h_by_key[[key]]))
}
v <- df[[key]]
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[[key]] <- conv
} else {
stop(sprintf("The %s column(%s) must be numeric — fewer than 95%% of its values could be read as numbers.",
key, h_by_key[[key]]))
}
}
}Step 2: Drop rows the logarithm cannot use (nonpositive or missing)
p <- df$price
q <- df$quantity
rows_missing <- sum(is.na(p) | is.na(q))
rows_nonpositive <- sum(!is.na(p) & !is.na(q) & (p <= 0 | q <= 0))
keep <- !is.na(p) & !is.na(q) & p > 0 & q > 0
df <- df[keep, , drop = FALSE]
final_rows <- nrow(df)
rows_removed <- initial_rows - final_rows
if (final_rows < 30) {
stop(sprintf("Only %d usable rows with positive %s and %s — price elasticity estimation needs at least 30.",
final_rows, h_price, h_quantity))
}
if (length(unique(df$price)) < 3) {
stop(sprintf("%s takes fewer than 3 distinct positive values — elasticity cannot be estimated without real price variation.",
h_price))
}
price_min <- min(df$price)
price_max <- max(df$price)Step 3: Prepare controls — numeric via the 95% rule + median impute,
categorical lumped to at most 8 levels (+ "Missing"/"Other")
ctrl_cols <- grep("^control_[0-9]+$", names(df), value = TRUE)
ctrl_cols <- ctrl_cols[order(as.integer(sub("^control_", "", ctrl_cols)))]
ctrl_names <- setNames(humanize_semantic(ctrl_cols, col_map), ctrl_cols)
used_controls <- character(0)
dropped_controls <- character(0)
md <- data.frame(log_quantity = log(df$quantity), log_price = log(df$price))
for (cc in ctrl_cols) {
v <- df[[cc]]
conv <- suppressWarnings(as.numeric(as.character(v)))
n_orig <- sum(!is.na(v) & as.character(v) != "")
is_num <- is.numeric(v) || (n_orig > 0 && sum(!is.na(conv)) >= 0.95 * n_orig)
if (is_num) {
nv <- if (is.numeric(v)) v else conv
med <- median(nv, na.rm = TRUE)
if (is.na(med)) { dropped_controls <- c(dropped_controls, cc); next }
nv[is.na(nv)] <- med
if (is.na(var(nv)) || isTRUE(var(nv) == 0)) {
dropped_controls <- c(dropped_controls, cc); next
}
md[[cc]] <- nv
} else {
ch <- as.character(v)
ch[is.na(ch) | trimws(ch) == ""] <- "Missing"
n_lvl <- length(unique(ch))
if (n_lvl < 2 || n_lvl > 0.9 * final_rows) {
# constant or identifier-like — useless as a control
dropped_controls <- c(dropped_controls, cc); next
}
if (n_lvl > 8) {
top <- names(sort(table(ch), decreasing = TRUE))[1:8]
ch[!ch %in% top] <- "Other"
}
md[[cc]] <- factor(ch)
}
used_controls <- c(used_controls, cc)
}Step 4: Fit the log-log OLS and read off the elasticity
fit <- lm(log_quantity ~ ., data = md)
sm <- summary(fit)
co <- sm$coefficients
if (!"log_price" %in% rownames(co) || is.na(co["log_price", 1])) {
stop(sprintf("The price term could not be estimated — check that %s varies independently of the mapped controls.",
h_price))
}
elasticity <- unname(co["log_price", 1])
p_value <- unname(co["log_price", 4])
ci_all <- suppressMessages(confint(fit, level = 0.95))
ci_low <- unname(ci_all["log_price", 1])
ci_high <- unname(ci_all["log_price", 2])
if (is.na(ci_low) || is.na(ci_high)) {
stop(sprintf("The confidence interval for the %s elasticity could not be computed — the model may be collinear.",
h_price))
}
r2 <- unname(sm$r.squared)
n_used <- nrow(md)Step 5: Classify — CI-aware language around unit elasticity
abs_e <- abs(elasticity)
demand_class <- if (abs_e > 1) "elastic" else if (abs_e < 1) "inelastic" else "unit elastic"
unit_in_ci <- if (elasticity < 0) (ci_low <= -1 && ci_high >= -1) else (ci_low <= 1 && ci_high >= 1)
zero_in_ci <- (ci_low <= 0 && ci_high >= 0)
# dRev/dP sign = 1 + elasticity: negative => cutting price raises revenue
rev_direction <- if ((1 + elasticity) < 0) "price cuts increase revenue" else "price increases increase revenue"Step 6: Ten-decile observed demand curve
br <- unique(quantile(df$price, probs = seq(0, 1, by = 0.1), na.rm = TRUE, type = 7))
if (length(br) < 3) br <- unique(range(df$price))
grp <- cut(df$price, breaks = br, include.lowest = TRUE)
agg_p <- tapply(df$price, grp, median)
agg_q <- tapply(df$quantity, grp, median)
ok <- !is.na(agg_p) & !is.na(agg_q)
decile_df <- data.frame(
price_level = round(unname(agg_p[ok]), 2),
median_quantity = round(unname(agg_q[ok]), 2),
stringsAsFactors = FALSE
)
decile_df <- decile_df[order(decile_df$price_level), , drop = FALSE]
rownames(decile_df) <- NULLStep 7: Coefficient table — the log(price) row first, controls after
pretty_term <- function(term) {
if (term == "log_price") return(paste0("log(", h_price, ")"))
for (cc in used_controls) {
if (startsWith(term, cc)) {
lvl <- substring(term, nchar(cc) + 1)
nm <- ctrl_names[[cc]]
return(if (nzchar(lvl)) paste0(nm, ": ", lvl) else nm)
}
}
term
}
interp_term <- function(term, est) {
if (term == "log_price") {
dir_word <- if (est < 0) "decrease" else "increase"
return(sprintf("A 1%% increase in %s is associated with a %s%% %s in %s.",
h_price, fmt_dec(abs(est), 2), dir_word, h_quantity))
}
pct <- 100 * (exp(est) - 1)
dir_word <- if (pct < 0) "lower" else "higher"
pct_txt <- if (abs(pct) >= 1000) fmt_amount(abs(pct), 0) else fmt_dec(abs(pct), 1)
is_level <- {
hit <- FALSE
for (cc in used_controls) if (term != cc && startsWith(term, cc)) hit <- TRUE
hit
}
if (is_level) {
sprintf("Rows at this level show about %s%% %s %s than the baseline level, holding %s constant.",
pct_txt, dir_word, h_quantity, h_price)
} else {
sprintf("Each one-step increase is associated with about %s%% %s %s, holding %s constant.",
pct_txt, dir_word, h_quantity, h_price)
}
}
term_names <- setdiff(rownames(co), "(Intercept)")
term_names <- term_names[!is.na(co[term_names, 1])]
term_names <- c("log_price", setdiff(term_names, "log_price"))
results_df <- do.call(rbind, lapply(term_names, function(tn) {
data.frame(
term = pretty_term(tn),
estimate = round(unname(co[tn, 1]), 3),
ci_low = round(unname(ci_all[tn, 1]), 3),
ci_high = round(unname(ci_all[tn, 2]), 3),
p_value = fmt_p_cell(unname(co[tn, 4])),
interpretation = interp_term(tn, unname(co[tn, 1])),
stringsAsFactors = FALSE
)
}))
rownames(results_df) <- NULLStep 8: Fit-quality table
r2_word <- if (r2 >= 0.6) "strong" else if (r2 >= 0.3) "moderate" else "weak"
class_interp <- if (unit_in_ci) {
sprintf("The point estimate is %s, but the 95%% CI spans an absolute elasticity of 1, so unit elasticity cannot be ruled out.", demand_class)
} else if (abs_e > 1) {
"The magnitude of the elasticity exceeds 1: demand responds more than proportionally to price."
} else {
"The magnitude of the elasticity is below 1: demand responds less than proportionally to price."
}
fitq_df <- data.frame(
metric = c("R-squared(log-log fit)", "Observations used",
"Rows dropped(nonpositive or missing)",
"Elasticity classification", "Revenue direction(associational)"),
value = c(fmt_dec(r2, 3), fmt_count(n_used), fmt_count(rows_removed),
demand_class, rev_direction),
interpretation = c(
sprintf("The model explains a %s share of the variation in log %s.", r2_word, h_quantity),
sprintf("Rows with positive %s and %s used in the regression.", h_price, h_quantity),
if (rows_removed == 0) {
"No rows were dropped — every row had positive price and quantity."
} else {
"Logarithms require strictly positive values, so these rows could not be used."
},
class_interp,
"Read from the sign of 1 plus the elasticity; associational because prices were not randomized."
),
stringsAsFactors = FALSE
)
metrics <- list(
`Price Elasticity` = round(elasticity, 3),
`CI Lower(95%)` = round(ci_low, 3),
`CI Upper(95%)` = round(ci_high, 3),
`R Squared` = round(r2, 3),
`Observations` = n_used,
`Demand Class` = demand_class
)
json_output <- list(
answer = paste0(
"Log-log OLS on ", fmt_count(n_used), " rows: the estimated price elasticity of ",
h_quantity, " with respect to ", h_price, " is ", fmt_dec(elasticity, 2),
" (95% CI ", fmt_dec(ci_low, 2), " to ", fmt_dec(ci_high, 2), ", ", fmt_p(p_value),
"). Demand is ", demand_class,
if (unit_in_ci) ", though the interval spans unit elasticity" else "",
"; the data is consistent with a regime where ", rev_direction,
". Prices were not randomized, so this is an association, not a causal effect."
),
cards = lapply(
c("tldr", "overview", "preprocessing", "demand_curve",
"elasticity_table", "fit_quality"),
function(cid) list(id = cid, metrics = metrics)
)
)
list(
initial_rows = initial_rows, final_rows = final_rows,
rows_removed = rows_removed,
rows_nonpositive = rows_nonpositive, rows_missing = rows_missing,
h_price = h_price, h_quantity = h_quantity,
used_controls = used_controls, dropped_controls = dropped_controls,
ctrl_names = ctrl_names,
elasticity = elasticity, ci_low = ci_low, ci_high = ci_high,
p_value = p_value, r2 = r2, n_used = n_used,
demand_class = demand_class, unit_in_ci = unit_in_ci,
zero_in_ci = zero_in_ci, rev_direction = rev_direction,
price_min = price_min, price_max = price_max,
decile_df = decile_df, results_df = results_df, fitq_df = fitq_df,
metrics = metrics, json_output = json_output
)
}