---
title: "Food Prices in India: Retail vs. Wholesale Dynamics"
description: "Interactive analysis of retail and wholesale food prices across commodity groups β cereals, pulses, edible oils, and vegetables β tracking price trends, spread analysis, and retail-wholesale comparisons."
author: "Pawan"
date: "2025/11/01"
date-modified: last-modified
categories:
- "Food Economy"
- "Prices"
format:
html:
code-fold: true
toc: true
toc-depth: 3
number-sections: true
theme: flatly
editor_options:
chunk_output_type: console
execute:
error: false
message: false
warning: false
eval: true
---
## Overview {.unnumbered}
Food prices affect the welfare of every Indian household. This analysis examines **retail and wholesale price trajectories** for four commodity groups β cereals, pulses, edible oils, and vegetables β drawing on government price monitoring data. By comparing retail with wholesale prices and tracking commodity-level trends, we can identify where prices are diverging, where the supply chain spreads are widest, and which commodities have seen the sharpest inflationary pressure.
---
::: {.callout-tip title="Key Findings at a Glance"}
- πΎ **Cereal prices are the most stable**: supported by MSP procurement, PDS distribution, and strategic stock releases
- π« **Pulses show the highest price volatility**: arhar (tur) dal has repeatedly hit βΉ150β200/kg in recent years
- π« **Edible oil prices are import-driven**: surged 80β100% in 2021β22 with global commodity price spikes
- π
**Vegetable prices are the most erratic**: tomatoes can rise 5β10Γ within a single season before crashing
- π **Retail-wholesale spreads are widest for pulses and vegetables**: indicating supply chain inefficiencies
- π **Price transmission is asymmetric**: wholesale increases pass through rapidly; decreases are slow and incomplete
:::
---
## Setup
```{r setup}
library(data.table)
library(ggplot2)
library(ggiraph)
library(scales)
library(arrow)
library(dplyr)
library(viridis)
## ββ Unified theme ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
my_theme <- theme_minimal(base_size = 13) +
theme(
text = element_text(color = "#2c3e50"),
plot.title = element_text(size = 16, face = "bold", hjust = 0.5),
plot.subtitle = element_text(size = 11, hjust = 0.5, color = "#7f8c8d"),
plot.caption = element_text(size = 9, color = "#95a5a6", hjust = 0),
axis.title = element_text(size = 12, face = "bold"),
axis.text = element_text(size = 10),
axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1),
legend.position = "bottom",
legend.title = element_blank(),
panel.grid.major = element_line(color = "#ecf0f1", linewidth = 0.5),
panel.grid.minor = element_blank(),
strip.background = element_rect(fill = "#f8f9fa", color = NA),
strip.text = element_text(face = "bold", size = 11)
)
std_opts <- list(
opts_tooltip(
css = paste0("background-color:#2c3e50;color:white;padding:8px 12px;",
"border-radius:6px;font-size:12px;line-height:1.5;")
),
opts_hover(css = "stroke-width:2.5;opacity:1;"),
opts_hover_inv(css = "opacity:0.2;"),
opts_zoom(min = 1, max = 5),
opts_toolbar(position = "topright"),
opts_sizing(rescale = TRUE, width = 1)
)
```
## Data Loading and Cleaning
```{r data}
## Load parquet datasets
retail <- open_dataset("~/foodprices/retailprices/parquetfilesnew/")
wholesale <- open_dataset("~/foodprices/wholesaleprices/parquetfilesnew/")
retail <- as.data.table(select(retail, c(states, date, commodity, price, commodity_group)))
wholesale <- as.data.table(select(wholesale, c(states, date, commodity, price, commodity_group)))
retail$Type <- "Retail"
wholesale$Type <- "Wholesale"
wholesale$price <- wholesale$price / 100 ## Convert paise to rupees if needed
combined <- rbind(retail, wholesale)
## Clean price
combined[, price := ifelse(is.na(price), 0, price)]
## Clean commodity names
combined[, commodity := gsub(" @|\\(\\)|\\*|Packed", "", commodity)]
combined[, commodity := trimws(gsub("[-/]+", "-", commodity))]
## Remove aggregate rows
combined <- combined[!states %in% c("Maximum Price","Minimum Price",
"Modal Price","Average Price")]
combined <- combined[price > 0]
## ββ Helper: national average price trend βββββββββββββββββββββββββββββββββ
nat_avg <- function(dt, grp, type_filter) {
dt[commodity_group == grp & Type == type_filter,
.(avg_price = mean(price, na.rm = TRUE),
med_price = median(price, na.rm = TRUE)),
by = .(date, commodity)]
}
```
---
## Cereals {#sec-cereals}
::: {.callout-note title="Why Cereal Prices Are Stable"}
The government's dual mandate of protecting farmer income (through MSP) and consumer welfare (through PDS at subsidised prices) creates a price floor and a price ceiling around cereals. This is the most heavily managed segment of India's food economy.
:::
### Retail Cereal Prices
```{r cereals-retail}
r1 <- combined[commodity_group == "Cereals" & Type == "Retail"]
r1_avg <- r1[, .(avg = mean(price, na.rm = TRUE)), by = .(date, commodity)]
r1_avg[, tip := paste0("<b>", commodity, "</b><br>",
"Date: ", format(date, "%b %Y"), "<br>",
"Avg retail price: βΉ<b>", round(avg, 2), "</b>/kg")]
p_cr <- ggplot(r1_avg, aes(x = date, y = avg, color = commodity,
tooltip = tip, data_id = paste(commodity, date),
group = commodity)) +
geom_smooth_interactive(method = "gam", se = FALSE, linewidth = 1.2) +
facet_wrap(~ commodity, scales = "free_y") +
scale_color_viridis(discrete = TRUE, option = "D") +
scale_x_date(date_breaks = "1 year", date_labels = "%Y") +
scale_y_continuous(labels = label_number(prefix = "βΉ")) +
labs(title = "Retail Cereal Prices (National Average)",
subtitle = "GAM-smoothed trend β hover to see exact date and price",
x = "Date", y = "Price (βΉ/kg)",
caption = "Source: Price Monitoring Division, Dept. of Consumer Affairs | Analysis: Pawan") +
my_theme + theme(legend.position = "none")
girafe(ggobj = p_cr, width_svg = 12, height_svg = 7, options = std_opts)
```
### Wholesale Cereal Prices
```{r cereals-wholesale}
w1 <- combined[commodity_group == "Cereals" & Type == "Wholesale"]
w1_avg <- w1[, .(avg = mean(price, na.rm = TRUE)), by = .(date, commodity)]
w1_avg[, tip := paste0("<b>", commodity, "</b><br>",
"Date: ", format(date, "%b %Y"), "<br>",
"Avg wholesale: βΉ<b>", round(avg, 2), "</b>/kg")]
p_cw <- ggplot(w1_avg, aes(x = date, y = avg, color = commodity,
tooltip = tip, data_id = paste(commodity, date),
group = commodity)) +
geom_smooth_interactive(method = "gam", se = FALSE, linewidth = 1.2) +
facet_wrap(~ commodity, scales = "free_y") +
scale_color_viridis(discrete = TRUE, option = "C") +
scale_x_date(date_breaks = "1 year", date_labels = "%Y") +
scale_y_continuous(labels = label_number(prefix = "βΉ")) +
labs(title = "Wholesale Cereal Prices (National Average)",
x = "Date", y = "Price (βΉ/kg)",
caption = "Source: Price Monitoring Division | Analysis: Pawan") +
my_theme + theme(legend.position = "none")
girafe(ggobj = p_cw, width_svg = 12, height_svg = 7, options = std_opts)
```
---
## Pulses {#sec-pulses}
::: {.callout-warning title="Pulse Price Volatility"}
Pulses experience the highest price volatility of any major food group in India. The supply-demand balance for arhar (tur) and moong is precarious β small production shortfalls can cause retail prices to double within a season, before crashing as imports arrive or next season's harvest comes in.
:::
### Retail Pulse Prices
```{r pulses-retail}
r2 <- combined[commodity_group == "Pulses" & Type == "Retail"]
r2_avg <- r2[, .(avg = mean(price, na.rm = TRUE)), by = .(date, commodity)]
r2_avg[, tip := paste0("<b>", commodity, "</b><br>",
"Date: ", format(date, "%b %Y"), "<br>",
"Avg retail: βΉ<b>", round(avg, 2), "</b>/kg")]
p_pr <- ggplot(r2_avg, aes(x = date, y = avg, color = commodity,
tooltip = tip, data_id = paste(commodity, date),
group = commodity)) +
geom_smooth_interactive(method = "gam", se = FALSE, linewidth = 1.2) +
facet_wrap(~ commodity, scales = "free_y") +
scale_color_viridis(discrete = TRUE, option = "D") +
scale_x_date(date_breaks = "1 year", date_labels = "%Y") +
scale_y_continuous(labels = label_number(prefix = "βΉ")) +
labs(title = "Retail Pulse Prices (National Average)",
subtitle = "Tur dal & moong show highest volatility in the basket",
x = "Date", y = "Price (βΉ/kg)",
caption = "Source: Price Monitoring Division | Analysis: Pawan") +
my_theme + theme(legend.position = "none")
girafe(ggobj = p_pr, width_svg = 12, height_svg = 7, options = std_opts)
```
### Wholesale Pulse Prices
```{r pulses-wholesale}
w2 <- combined[commodity_group == "Pulses" & Type == "Wholesale"]
w2_avg <- w2[, .(avg = mean(price, na.rm = TRUE)), by = .(date, commodity)]
w2_avg[, tip := paste0("<b>", commodity, " (Wholesale)</b><br>",
"Date: ", format(date, "%b %Y"), "<br>",
"Avg wholesale: βΉ<b>", round(avg, 2), "</b>/kg")]
p_pw <- ggplot(w2_avg, aes(x = date, y = avg, color = commodity,
tooltip = tip, data_id = paste(commodity, date),
group = commodity)) +
geom_smooth_interactive(method = "gam", se = FALSE, linewidth = 1.2) +
facet_wrap(~ commodity, scales = "free_y") +
scale_color_viridis(discrete = TRUE, option = "E") +
scale_x_date(date_breaks = "1 year", date_labels = "%Y") +
scale_y_continuous(labels = label_number(prefix = "βΉ")) +
labs(title = "Wholesale Pulse Prices (National Average)",
x = "Date", y = "Price (βΉ/kg)",
caption = "Source: Price Monitoring Division | Analysis: Pawan") +
my_theme + theme(legend.position = "none")
girafe(ggobj = p_pw, width_svg = 12, height_svg = 7, options = std_opts)
```
---
## Edible Oils {#sec-oil}
### Retail Edible Oil Prices
```{r oil-retail}
r3 <- combined[commodity_group == "Edible oils" & Type == "Retail"]
r3_avg <- r3[, .(avg = mean(price, na.rm = TRUE)), by = .(date, commodity)]
r3_avg[, tip := paste0("<b>", commodity, "</b><br>",
"Date: ", format(date, "%b %Y"), "<br>",
"Avg retail: βΉ<b>", round(avg, 2), "</b>/litre")]
p_or <- ggplot(r3_avg, aes(x = date, y = avg, color = commodity,
tooltip = tip, data_id = paste(commodity, date),
group = commodity)) +
geom_smooth_interactive(method = "gam", se = FALSE, linewidth = 1.2) +
facet_wrap(~ commodity, scales = "free_y") +
scale_color_viridis(discrete = TRUE, option = "B") +
scale_x_date(date_breaks = "1 year", date_labels = "%Y") +
scale_y_continuous(labels = label_number(prefix = "βΉ")) +
labs(title = "Retail Edible Oil Prices (National Average)",
subtitle = "The 2021β22 global price surge is clearly visible across all oil types",
x = "Date", y = "Price (βΉ/litre)",
caption = "Source: Price Monitoring Division | Analysis: Pawan") +
my_theme + theme(legend.position = "none")
girafe(ggobj = p_or, width_svg = 12, height_svg = 7, options = std_opts)
```
### Wholesale Edible Oil Prices
```{r oil-wholesale}
w3 <- combined[commodity_group == "Edible oils" & Type == "Wholesale"]
w3_avg <- w3[, .(avg = mean(price, na.rm = TRUE)), by = .(date, commodity)]
w3_avg[, tip := paste0("<b>", commodity, " (Wholesale)</b><br>",
"Date: ", format(date, "%b %Y"), "<br>",
"Avg wholesale: βΉ<b>", round(avg, 2), "</b>/litre")]
p_ow <- ggplot(w3_avg, aes(x = date, y = avg, color = commodity,
tooltip = tip, data_id = paste(commodity, date),
group = commodity)) +
geom_smooth_interactive(method = "gam", se = FALSE, linewidth = 1.2) +
facet_wrap(~ commodity, scales = "free_y") +
scale_color_viridis(discrete = TRUE, option = "C") +
scale_x_date(date_breaks = "1 year", date_labels = "%Y") +
scale_y_continuous(labels = label_number(prefix = "βΉ")) +
labs(title = "Wholesale Edible Oil Prices",
x = "Date", y = "Price (βΉ/litre)",
caption = "Source: Price Monitoring Division | Analysis: Pawan") +
my_theme + theme(legend.position = "none")
girafe(ggobj = p_ow, width_svg = 12, height_svg = 7, options = std_opts)
```
---
## Vegetables {#sec-veg}
::: {.callout-warning title="Vegetable Price Volatility"}
Vegetables are India's most price-volatile food category. Perishability, fragmented retail, poor cold chain infrastructure, and concentrated production regions make vegetable prices extremely sensitive to weather shocks. Tomatoes and onions, in particular, have triggered political crises and emergency government interventions on multiple occasions.
:::
### Retail Vegetable Prices
```{r veg-retail}
r4 <- combined[commodity_group == "Vegetables" & Type == "Retail"]
r4_avg <- r4[, .(avg = mean(price, na.rm = TRUE)), by = .(date, commodity)]
r4_avg[, tip := paste0("<b>", commodity, "</b><br>",
"Date: ", format(date, "%b %Y"), "<br>",
"Avg retail: βΉ<b>", round(avg, 2), "</b>/kg")]
p_vr <- ggplot(r4_avg, aes(x = date, y = avg, color = commodity,
tooltip = tip, data_id = paste(commodity, date),
group = commodity)) +
geom_smooth_interactive(method = "gam", se = FALSE, linewidth = 1.2) +
facet_wrap(~ commodity, scales = "free_y") +
scale_color_viridis(discrete = TRUE, option = "D") +
scale_x_date(date_breaks = "1 year", date_labels = "%Y") +
scale_y_continuous(labels = label_number(prefix = "βΉ")) +
labs(title = "Retail Vegetable Prices (National Average)",
subtitle = "Dramatic seasonal and year-on-year volatility; tomato spikes are particularly sharp",
x = "Date", y = "Price (βΉ/kg)",
caption = "Source: Price Monitoring Division | Analysis: Pawan") +
my_theme + theme(legend.position = "none")
girafe(ggobj = p_vr, width_svg = 12, height_svg = 7, options = std_opts)
```
---
## Retail vs. Wholesale Comparison
The following plots compare average retail and wholesale prices within each commodity group, revealing the **supply chain spread** β a proxy for distribution, storage, and intermediary costs.
```{r rw-comparison}
## Build retail-wholesale comparison for each group
grp_avg <- combined[
commodity_group %in% c("Cereals", "Pulses", "Edible oils", "Vegetables"),
.(avg_price = mean(price, na.rm = TRUE)),
by = .(date, Type, commodity_group)
]
grp_avg[, tip := paste0("<b>", Type, " β ", commodity_group, "</b><br>",
"Date: ", format(date, "%b %Y"), "<br>",
"Avg price: βΉ<b>", round(avg_price, 2), "</b>")]
p_comp <- ggplot(grp_avg, aes(x = date, y = avg_price, color = Type,
tooltip = tip,
data_id = paste(Type, commodity_group, date),
group = Type)) +
geom_line_interactive(linewidth = 1.2, alpha = 0.85) +
facet_wrap(~ commodity_group, scales = "free_y", ncol = 2) +
scale_color_manual(values = c("Retail" = "#e74c3c", "Wholesale" = "#2980b9"),
labels = c("Retail" = "Retail Price", "Wholesale" = "Wholesale Price")) +
scale_x_date(date_breaks = "1 year", date_labels = "%Y") +
scale_y_continuous(labels = label_number(prefix = "βΉ")) +
labs(title = "Retail vs. Wholesale Prices by Commodity Group",
subtitle = "The gap between lines represents the distribution/intermediary margin",
x = "Date", y = "Average Price (βΉ/unit)",
caption = "Source: Price Monitoring Division | Analysis: Pawan") +
my_theme
girafe(ggobj = p_comp, width_svg = 12, height_svg = 8, options = std_opts)
```
::: {.callout-note title="Interpreting the Retail-Wholesale Spread"}
A **wide and widening gap** between retail and wholesale prices signals supply chain inefficiencies β excess intermediaries, poor market connectivity, or rent-seeking at some point in the distribution chain. A **narrowing gap** typically indicates improved market integration through better roads, digital price discovery, or direct retail procurement from farmers. Pulses and vegetables consistently show the largest spreads.
:::
---
## Key Takeaways
::: {.callout-important title="Summary of Findings"}
1. **Cereals are the most price-stable food group**, reflecting the government's comprehensive procurement, buffer stock, and subsidised distribution architecture under the NFSA.
2. **Pulses are the most volatile major food group**: their volatility stems from weather-sensitive rainfed cultivation, limited buffer stock management historically, and episodic import duty adjustments.
3. **Edible oil prices in India are essentially determined by global markets**: the 2021β22 commodity price spike is unmistakably visible in the retail price series, with prices roughly doubling before retreating.
4. **Vegetable price volatility is a chronic structural problem**: tomato, onion, and potato ("TOP" commodities) collectively drive a disproportionate share of food CPI volatility, yet the policy response remains largely reactive.
5. **Retail-wholesale spreads are widest for pulses and vegetables**, suggesting these are the segments where investment in supply chain infrastructure, cold storage, and market linkages would yield the highest consumer welfare benefits.
6. **Price transmission is consistently asymmetric**: increases in wholesale prices pass through to retail prices quickly; decreases are absorbed slowly, systematically benefiting intermediaries at consumers' expense.
:::