This report analyzes the sneaker resale market using real completed sales from StockX and GOAT, two of the biggest sneaker marketplaces.
Dataset: SneakerPulse resale data (CC BY 4.0), built from GOAT and StockX completed sales. The main file covers the last 90 days ending November 3, 2026: 49 popular sneaker models and 11,137 individual shoes (colorways), with the median sale price, the 25th/75th percentile prices, and total sales for each. A second file compares each shoe’s price to its own price a year earlier, showing which sneakers hold their value.
Question: which sneakers command the highest resale prices, which brands move the most pairs, and are resale prices going up or down?
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import re
df = pd.read_csv('sneakerpulse_current_prices.csv')
same = pd.read_csv('sneakerpulse_same_shoe_12m.csv')
models = df[df['level'] == 'model'].copy()
shoes = df[df['level'] == 'shoe'].copy()
def brand_of(name):
n = name.lower()
if 'jordan' in n: return 'Air Jordan'
if 'yeezy' in n: return 'Yeezy'
if n.startswith('nike') or n in ('air force 1','blazer','dunk','dunk sb','air max 1','air max 270','air max 90','air max 95','air max 97','air max plus','air vapormax'): return 'Nike'
if 'new balance' in n or n in ('2002r','574','990'): return 'New Balance'
if 'asics' in n or 'onitsuka' in n: return 'ASICS'
if 'converse' in n or 'chuck 70' in n: return 'Converse'
if 'vans' in n: return 'Vans'
if 'puma' in n: return 'Puma'
return 'adidas'
models['brand'] = models['name'].apply(brand_of)
print(models.shape)
(49, 12)
print(shoes.shape)
(11137, 11)
The data covers 49 models and 11,137 individual shoes, with 1,230,873 completed sales in 90 days. The median resale price across models is $134, ranging from $82 (Samba) to $320 (Nike Kobe 6). Nine brands are represented, with Nike (15 models) and Air Jordan (11 models) making up more than half the list.
print("Total completed sales (90 days):", f"{models['sales'].sum():,}")
Total completed sales (90 days): 1,230,873
print("Median resale price across models: $", models['median_price_usd'].median())
Median resale price across models: $ 134.0
print("Price range: $", models['median_price_usd'].min(), "- $", models['median_price_usd'].max())
Price range: $ 82 - $ 320
print()
print(models['median_price_usd'].describe().round(1))
count 49.0
mean 142.7
std 46.0
min 82.0
25% 108.0
50% 134.0
75% 164.0
max 320.0
Name: median_price_usd, dtype: float64
print()
print("Models per brand:")
Models per brand:
print(models['brand'].value_counts())
brand
Nike 15
Air Jordan 11
New Balance 6
adidas 6
ASICS 4
Converse 2
Vans 2
Yeezy 2
Puma 1
Name: count, dtype: int64
A bar chart ranks categories side by side, so the priciest models jump out immediately. Each bar is colored by brand. The Nike Kobe 6 sits far above everything at $320 — a tribute-driven market after Kobe Bryant’s legacy kept demand high. Right behind it is a wall of Air Jordans: the AJ11 ($220), AJ9 ($215), AJ5 and AJ14 ($212). Jordan owns 8 of the top 12 spots, which says the retro basketball market still sets the ceiling on resale prices.
top12 = models.sort_values('median_price_usd', ascending=False).head(12)
brand_colors = {'Air Jordan':'#c9a227','Nike':'#4a4a6a','Yeezy':'#6a6a8a','adidas':'#9aa0a6','New Balance':'#5a7a5a','ASICS':'#4a7a9a','Converse':'#a06a4a','Vans':'#7a4a6a','Puma':'#6a8a4a'}
colors = [brand_colors.get(b, '#888888') for b in top12['brand']]
plt.figure(figsize=(12, 5))
plt.barh(top12['name'][::-1], top12['median_price_usd'][::-1], color=colors[::-1])
plt.xlabel('Median resale price (USD, last 90 days)')
plt.title('Top 12 Sneaker Models by Median Resale Price', fontsize=14, fontweight='bold')
for i, v in enumerate(top12['median_price_usd'][::-1]):
plt.text(v + 3, i, f'${v:.0f}', va='center', fontsize=9)
plt.tight_layout()
plt.savefig('sviz1_top12_bar.png', dpi=120)
plt.show()
A box plot shows the full spread of prices, not just the middle. Here each box is the distribution of colorway prices inside one model — some Jordans sell for $100 in one colorway and $400 in another. The Air Jordan 1 has the widest spread by far (over 1,000 tracked colorways), which makes sense: it’s the most released and most hyped line, so rare collabs pull the top up while general releases sit near retail. The ASICS Gel-1130 is the tightest box — a steady lifestyle shoe where almost every colorway sells in a narrow band around $100.
top8 = ['Air Jordan 1','Air Jordan 4','Air Force 1','Air Jordan 5','Air Jordan 3','Dunk','ASICS Gel-1130','Air Jordan 11']
kw = {
'Air Jordan 1': r'jordan 1(?!\d)',
'Air Jordan 4': r'jordan 4(?!\d)',
'Air Force 1': r'air force 1',
'Air Jordan 5': r'jordan 5(?!\d)',
'Air Jordan 3': r'jordan 3(?!\d)',
'Dunk': r'dunk(?!\s*sb)',
'ASICS Gel-1130': r'gel[- ]?1130',
'Air Jordan 11': r'jordan 11(?!\d)',
}
shoes['lname'] = shoes['name'].str.lower()
def map_model(lname):
for m, pat in kw.items():
if re.search(pat, lname):
return m
return None
shoes['model_fam'] = shoes['lname'].apply(map_model)
boxdata = [shoes[shoes['model_fam'] == m]['median_price_usd'].values for m in top8]
boxlabels = [m.replace('Air Jordan ','AJ').replace('ASICS ','') for m in top8]
plt.figure(figsize=(12, 5))
plt.boxplot(boxdata, patch_artist=True,
boxprops=dict(facecolor='#c9a227', alpha=0.7))
{'whiskers': [<matplotlib.lines.Line2D object at 0x11c34f4d0>, <matplotlib.lines.Line2D object at 0x11c34f610>, <matplotlib.lines.Line2D object at 0x11c34fd90>, <matplotlib.lines.Line2D object at 0x11c34fed0>, <matplotlib.lines.Line2D object at 0x11c380690>, <matplotlib.lines.Line2D object at 0x11c3807d0>, <matplotlib.lines.Line2D object at 0x11c380f50>, <matplotlib.lines.Line2D object at 0x11c381090>, <matplotlib.lines.Line2D object at 0x11c381810>, <matplotlib.lines.Line2D object at 0x11c381950>, <matplotlib.lines.Line2D object at 0x11c382210>, <matplotlib.lines.Line2D object at 0x11c382350>, <matplotlib.lines.Line2D object at 0x11c382990>, <matplotlib.lines.Line2D object at 0x11c382ad0>, <matplotlib.lines.Line2D object at 0x11c383250>, <matplotlib.lines.Line2D object at 0x11c383390>], 'caps': [<matplotlib.lines.Line2D object at 0x11c34f750>, <matplotlib.lines.Line2D object at 0x11c34f890>, <matplotlib.lines.Line2D object at 0x11c380050>, <matplotlib.lines.Line2D object at 0x11c380190>, <matplotlib.lines.Line2D object at 0x11c380910>, <matplotlib.lines.Line2D object at 0x11c380a50>, <matplotlib.lines.Line2D object at 0x11c3811d0>, <matplotlib.lines.Line2D object at 0x11c381310>, <matplotlib.lines.Line2D object at 0x11c381a90>, <matplotlib.lines.Line2D object at 0x11c381bd0>, <matplotlib.lines.Line2D object at 0x11c382490>, <matplotlib.lines.Line2D object at 0x11c3825d0>, <matplotlib.lines.Line2D object at 0x11c382c10>, <matplotlib.lines.Line2D object at 0x11c382d50>, <matplotlib.lines.Line2D object at 0x11c3834d0>, <matplotlib.lines.Line2D object at 0x11c383610>], 'boxes': [<matplotlib.patches.PathPatch object at 0x11c1330e0>, <matplotlib.patches.PathPatch object at 0x11c34fc50>, <matplotlib.patches.PathPatch object at 0x11c380550>, <matplotlib.patches.PathPatch object at 0x11c380e10>, <matplotlib.patches.PathPatch object at 0x11c3816d0>, <matplotlib.patches.PathPatch object at 0x11c3820d0>, <matplotlib.patches.PathPatch object at 0x11c382850>, <matplotlib.patches.PathPatch object at 0x11c383110>], 'medians': [<matplotlib.lines.Line2D object at 0x11c34f9d0>, <matplotlib.lines.Line2D object at 0x11c3802d0>, <matplotlib.lines.Line2D object at 0x11c380b90>, <matplotlib.lines.Line2D object at 0x11c381450>, <matplotlib.lines.Line2D object at 0x11c381e50>, <matplotlib.lines.Line2D object at 0x11c382710>, <matplotlib.lines.Line2D object at 0x11c382e90>, <matplotlib.lines.Line2D object at 0x11c383750>], 'fliers': [<matplotlib.lines.Line2D object at 0x11c34fb10>, <matplotlib.lines.Line2D object at 0x11c380410>, <matplotlib.lines.Line2D object at 0x11c380cd0>, <matplotlib.lines.Line2D object at 0x11c381590>, <matplotlib.lines.Line2D object at 0x11c381f90>, <matplotlib.lines.Line2D object at 0x11c381d10>, <matplotlib.lines.Line2D object at 0x11c382fd0>, <matplotlib.lines.Line2D object at 0x11c383890>], 'means': []}
plt.xticks(range(1, len(top8) + 1), boxlabels, rotation=20, ha='right')
([<matplotlib.axis.XTick object at 0x11c34d1d0>, <matplotlib.axis.XTick object at 0x11c34c910>, <matplotlib.axis.XTick object at 0x11c3dc050>, <matplotlib.axis.XTick object at 0x11c3dc7d0>, <matplotlib.axis.XTick object at 0x11c3dcf50>, <matplotlib.axis.XTick object at 0x11c3dd6d0>, <matplotlib.axis.XTick object at 0x11c3dde50>, <matplotlib.axis.XTick object at 0x11c3de5d0>], [Text(1, 0, 'AJ1'), Text(2, 0, 'AJ4'), Text(3, 0, 'Air Force 1'), Text(4, 0, 'AJ5'), Text(5, 0, 'AJ3'), Text(6, 0, 'Dunk'), Text(7, 0, 'Gel-1130'), Text(8, 0, 'AJ11')])
plt.ylabel('Resale price per colorway (USD)')
plt.title('Price Spread Across Colorways: 8 Biggest Models', fontsize=14, fontweight='bold')
plt.xticks(rotation=20, ha='right')
(array([1, 2, 3, 4, 5, 6, 7, 8]), [Text(1, 0, 'AJ1'), Text(2, 0, 'AJ4'), Text(3, 0, 'Air Force 1'), Text(4, 0, 'AJ5'), Text(5, 0, 'AJ3'), Text(6, 0, 'Dunk'), Text(7, 0, 'Gel-1130'), Text(8, 0, 'AJ11')])
plt.tight_layout()
plt.savefig('sviz2_boxplot.png', dpi=120)
plt.show()
A heatmap turns a table into colors so differences pop out. Each row is a model, each column is a marketplace, and darker cells mean higher median prices. The two marketplaces track each other closely — no model is dramatically cheaper on one side, which tells us the resale market is efficient: arbitrage gaps get closed fast. The small gaps that exist (Jordan 5 runs a bit hotter on GOAT, Jordan 11 on StockX) are the kind of edges resellers watch.
hm = shoes[shoes['model_fam'].notna()].groupby(['model_fam','marketplace'])['median_price_usd'].median().unstack()
hm = hm.reindex(top8)
fig, ax = plt.subplots(figsize=(10, 5))
im = ax.imshow(hm.values, cmap='YlOrRd', aspect='auto')
ax.set_xticks([0, 1]); ax.set_xticklabels(['GOAT','StockX'])
ax.set_yticks(range(len(top8))); ax.set_yticklabels([m.replace('Air Jordan ','AJ') for m in top8])
for i in range(len(top8)):
for j in range(2):
ax.text(j, i, f"${hm.values[i, j]:.0f}", ha='center', va='center', fontsize=10, fontweight='bold')
ax.set_title('Median Resale Price: GOAT vs StockX by Model', fontsize=14, fontweight='bold')
plt.colorbar(im, label='USD')
<matplotlib.colorbar.Colorbar object at 0x11c133a10>
plt.tight_layout()
plt.savefig('sviz3_heatmap.png', dpi=120)
plt.show()
A line chart makes a trend visible at a glance. Here each point is one model’s year-over-year price change (same shoe vs its own price a year ago), sorted from biggest gain to biggest loss. The story is a cooling market: most models sit below zero, with Air Jordan down 6.8% as a brand. But the winners stand out — the Chuck 70 is up 30.5% and the Forum up 23.3%, while the New Balance 1906R fell 12.8%. Hype is rotating from retro basketball toward casual classics.
sm = same[same['level'] == 'model'].sort_values('median_change_pct').reset_index(drop=True)
plt.figure(figsize=(12, 5))
plt.plot(sm['median_change_pct'].values, marker='o', markersize=4, color='#4a4a6a', linewidth=1.5)
plt.axhline(0, color='red', linestyle='--', linewidth=1)
plt.xlabel('Models (sorted worst to best)')
plt.ylabel('Price change vs a year ago (%)')
plt.title('Do They Hold Value? Same-Shoe Price Change, Year Over Year', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.savefig('sviz4_yoy_line.png', dpi=120)
plt.show()
print("Brand-level year-over-year change:")
Brand-level year-over-year change:
print(same[same['level']=='brand'][['name','median_change_pct','shoes_rose','shoes_fell']].sort_values('median_change_pct', ascending=False).to_string(index=False))
name median_change_pct shoes_rose shoes_fell
Converse 15.7 132 66
Vans 8.4 64 41
Puma 3.6 143 118
Nike 0.0 4432 4528
ASICS -2.1 342 394
adidas -2.9 1028 1207
Yeezy -3.1 291 387
New Balance -5.3 603 905
Air Jordan -6.8 1519 2833
A donut chart shows shares of a whole. This one splits the 1.23 million tracked sales by brand. Nike and Air Jordan together own the clear majority of resale volume — the Air Jordan 1 alone moved over 155,000 pairs in 90 days. That volume plus Jordan’s price premium is why the brand dominates both ends of the market: most pairs sold AND highest prices.
brand_sales = models.groupby('brand')['sales'].sum().sort_values(ascending=False)
plt.figure(figsize=(10, 5))
wedges, texts, autotexts = plt.pie(brand_sales.values, labels=brand_sales.index, autopct='%1.1f%%',
colors=['#c9a227','#4a4a6a','#6a6a8a','#9aa0a6','#5a7a5a','#4a7a9a','#a06a4a','#7a4a6a','#6a8a4a'],
wedgeprops=dict(width=0.45), textprops={'fontsize': 10})
plt.title('Share of Resale Volume by Brand (90 days)', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.savefig('sviz5_brand_donut.png', dpi=120)
plt.show()
This one is interactive — hover over any dot to see the model, its median price, and how many pairs sold. It plots all 49 models at once: price on the vertical axis, sales volume on the horizontal (log scale so the giants don’t squash everything else). Two patterns jump out. First, the Air Jordan 1 is in a league of its own on volume — over 155,000 pairs, far right of everyone. Second, price and hype don’t always move together: the Kobe 6 sits at the very top on price with modest volume, while lifestyle models like the Gel-1130 move big volume at low prices. Zoom, pan, and toggle brands with the legend.