How to Use Order Value Analysis in Amazon: Step-by-Step Tutorial
By MCP Analytics Team | Published , updated
Introduction to Order Value Analysis
Understanding your Amazon order values is fundamental to growing your e-commerce business profitably. Average Order Value (AOV) represents the typical amount customers spend per transaction. Unlike total sales or conversion rate alone, AOV reveals how much value you extract from each customer interaction.
Whether you ship through FBA (Fulfillment by Amazon) or FBM (fulfillment by the merchant), a critical decision discussed in our Amazon FBA vs FBM performance guide, understanding order values helps you allocate resources strategically.
In this tutorial, you'll learn how to perform comprehensive order value analysis using your Amazon sales data. We'll walk through calculating average order value, understanding items per order, identifying high-value products, and interpreting results with their limits in mind. The examples use a small synthetic data set generated for this tutorial (6 made-up products, 1,400 orders over 120 days, written by the seeded generator at the start of Step 1), not a real store, so your numbers will differ.
Prerequisites and Data Requirements
Before beginning this analysis, ensure you have the following:
Required Access
- An Amazon Seller Central account with access to order reports
- Historical order data covering at least 30 days; 90 days or more gives a steadier average
- Python with pandas for the code below, or a SQL database if you prefer the SQL versions
Data Fields You'll Need
The code below reads one row per order line (one product on one order), with these columns. Amazon's reports use their own column names, so rename them when you load the file:
order_id- identifier for the order; repeats on every line of the same orderorder_date- date of purchaseorder_status- for examplecompletedorcanceledproduct_sku- the product identifier (SKU, or ASIN if you work by Amazon's product number)product_name- item descriptionquantity- units on that lineitem_price- price per unit
The order total and the number of items per order are not columns here; the code computes them from the lines, so an order that spans several rows is counted once. The totals use merchandise prices only. If your file also has shipping, tax or fees, decide whether you want them in AOV and apply the same rule to every order.
How Much Data Is Enough
An average is steadier when it rests on more orders, and the average for one product rests only on the orders that include it. There is no magic order count. Step 3 prints a rough 95% margin next to each product's average so you can see how much it could move; wider margins mean treat the ranking as a lead to check, not a result.
Step 1: Calculate Your Average Order Value
Average Order Value (AOV) is the cornerstone metric of order value analysis. It tells you how much revenue you generate per transaction, independent of traffic or conversion rate.
The Formula
The basic AOV calculation is straightforward:
Average Order Value = Total Revenue / Number of Orders
Implementation
Example Data (Made Up)
The rest of the page reads amazon_order_lines.csv. To follow along without your own export, run this first: it makes up that file from a seeded random generator (seed 2026). Every product, price and order in it is invented.
# MADE-UP DATA. This block invents amazon_order_lines.csv from a seeded generator:
# not a real store and not an Amazon report. Skip it if you have your own file.
import numpy as np
import pandas as pd
rng = np.random.default_rng(2026)
products = [ # sku, name, unit price, share of picks (all made up)
("PRO-001", "Premium Coffee Maker Set", 89.99, 0.10),
("ACC-234", "Barista Accessories Bundle", 34.99, 0.14),
("FIL-789", "Water Filters (6-Pack)", 16.99, 0.20),
("CON-456", "Gourmet Coffee Bean Sample", 12.49, 0.30),
("MUG-100", "Travel Mug", 17.99, 0.15),
("SPN-050", "Measuring Spoon Set", 8.99, 0.11),
]
weights = [p[3] for p in products]
days = pd.date_range("2026-06-01", periods=120, freq="D")
rows = []
for k in range(1400):
order_id = f"114-{k + 1:07d}"
order_date = days[rng.integers(len(days))].date()
status = "canceled" if rng.random() < 0.06 else "completed"
n_products = rng.choice([1, 2, 3], p=[0.6, 0.3, 0.1])
for i in rng.choice(len(products), size=n_products, replace=False, p=weights):
sku, name, price, _ = products[i]
qty = int(rng.choice([1, 2, 3], p=[0.75, 0.18, 0.07]))
# now and then one line of a completed order is canceled on its own
line_status = "canceled" if status == "completed" and rng.random() < 0.03 else status
rows.append([order_id, order_date, line_status, sku, name, qty, price])
lines = pd.DataFrame(rows, columns=["order_id", "order_date", "order_status", "product_sku",
"product_name", "quantity", "item_price"])
lines.to_csv("amazon_order_lines.csv", index=False)
print(f"Wrote {len(lines):,} made-up order lines for {lines['order_id'].nunique():,} orders, "
f"{lines['order_date'].min()} to {lines['order_date'].max()}")
Wrote 2,116 made-up order lines for 1,400 orders, 2026-06-01 to 2026-09-28
With SQL, load the same file into a table called amazon_order_lines (in psql, \copy amazon_order_lines FROM 'amazon_order_lines.csv' CSV HEADER after creating the table), first turn the order lines into one row per order, then average over orders:
SQL Approach
CREATE VIEW amazon_orders AS
SELECT
order_id,
order_status,
MIN(order_date) AS order_date,
SUM(quantity * item_price) AS order_total,
SUM(quantity) AS item_count
FROM amazon_order_lines
GROUP BY order_id, order_status;
SELECT
COUNT(*) AS total_orders,
SUM(order_total) AS total_revenue,
ROUND(AVG(order_total), 2) AS average_order_value
FROM amazon_orders
WHERE order_date > (SELECT MAX(order_date) FROM amazon_order_lines) - 90
AND order_status = 'completed';
The view has one row per order and status, so if one line of an order is canceled, the completed lines still form a completed order and the canceled line is left out of its total. That is the same rule as the pandas code, which keeps completed lines before it adds them up. (Taking MAX(order_status) per order instead would label a mixed order completed and count the canceled line in its total.) The window is the 90 days up to the latest order in the file, as in the pandas code; use CURRENT_DATE instead for a live table. The Step 1 to Step 3 SQL was run on PostgreSQL 14 against the same made-up file and returns the same counts and averages as the pandas code.
Python/Pandas Approach
import pandas as pd
# One row per order line: order_id, order_date, order_status, product_sku,
# product_name, quantity, item_price (price per unit)
df = pd.read_csv('amazon_order_lines.csv', parse_dates=['order_date'])
df['line_total'] = df['quantity'] * df['item_price']
# Completed orders in the 90 days up to the latest order in the file
window_end = df['order_date'].max()
recent = df[
(df['order_date'] > window_end - pd.Timedelta(days=90)) &
(df['order_status'] == 'completed')
]
# Collapse order lines to one row per order
orders = recent.groupby('order_id').agg(
order_total=('line_total', 'sum'),
item_count=('quantity', 'sum'),
)
total_orders = len(orders)
total_revenue = orders['order_total'].sum()
aov = total_revenue / total_orders
print(f"Total Orders: {total_orders:,}")
print(f"Total Revenue: ${total_revenue:,.2f}")
print(f"Average Order Value: ${aov:.2f}")
Expected Output
On the made-up file from the generator above, the code prints the following. 981 of the 1,400 made-up orders have completed lines dated in the last 90 days of the file:
Total Orders: 981
Total Revenue: $48,275.03
Average Order Value: $49.21
Interpreting Your AOV
A single AOV number has no meaning without something to compare it with. Useful comparisons are your own earlier periods, your median order value (see Troubleshooting if the mean and median differ a lot), and the same measure for one product line against another. A general "good AOV" range would depend on category, price level and fulfillment method, so this tutorial does not give one.
Step 2: Analyze Items Per Order
Understanding how many items customers purchase per order reveals critical insights about buying behavior, product bundling effectiveness, and cross-sell opportunities.
Why Items Per Order Matters
Two stores can have identical AOV but radically different business models:
- Store A: $50 AOV with 1 item per order (selling $50 products)
- Store B: $50 AOV with 3 items per order (selling products around $16-17 each)
Store B may have more room to cross-sell, while Store A depends on each product's own price. Which is better depends on margins and returns, which AOV does not show.
Calculation Method
Using the one-row-per-order view from Step 1:
SELECT
AVG(item_count) AS avg_items_per_order,
MIN(item_count) AS min_items,
MAX(item_count) AS max_items,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY item_count) AS median_items,
COUNT(CASE WHEN item_count = 1 THEN 1 END) * 100.0 / COUNT(*) AS single_item_percentage
FROM amazon_orders
WHERE order_date > (SELECT MAX(order_date) FROM amazon_order_lines) - 90
AND order_status = 'completed';
Python Implementation
# Items per order statistics (orders has one row per order)
items_stats = orders['item_count']
avg_items = items_stats.mean()
median_items = items_stats.median()
single_item_pct = (items_stats == 1).mean() * 100
print(f"Average Items Per Order: {avg_items:.2f}")
print(f"Median Items Per Order: {median_items:.0f}")
print(f"Single-Item Orders: {single_item_pct:.1f}%")
# Distribution analysis, with orders of 5 or more units grouped together
size = items_stats.clip(upper=5)
dist = size.value_counts().sort_index()
print("\nOrder Size Distribution:")
for n, count in dist.items():
label = '5+' if n == 5 else str(n)
print(f"{label} {count} orders ({count / len(items_stats) * 100:.1f}%)")
Expected Output
Average Items Per Order: 2.04
Median Items Per Order: 2
Single-Item Orders: 44.3%
Order Size Distribution:
1 435 orders (44.3%)
2 257 orders (26.2%)
3 173 orders (17.6%)
4 78 orders (8.0%)
5+ 38 orders (3.9%)
The percentages in the distribution add up to 100%, and the counts add up to the total orders in Step 1.
Strategic Insights
Based on your items-per-order metrics:
- High single-item percentage: Many customers buy one thing. It is a place to test bundles, "Frequently Bought Together" placement, or volume discounts; the data cannot tell you in advance which will work
- Low average items (close to 1): Cross-sell recommendations and complementary products are worth testing
- High average items: Customers already buy several things per order. Whether that comes from bundling, low unit prices or product mix is a question to check by comparing orders with and without a bundle; the average alone cannot say
Step 3: Identify Products with Highest Order Values
Not all products contribute equally to your AOV. This step shows which items appear in high-value transactions, which is a place to start testing, not proof that they cause the higher value.
Product-Level AOV Analysis
For each product, look at the orders that contain it: how large those orders are, how much of each order is the product itself, and how much is other items. Because one order can contain several units of the product, the product's share is its summed line revenue in that order, not one unit's price.
WITH product_orders AS (
SELECT
order_id,
product_sku,
product_name,
SUM(quantity * item_price) AS sku_revenue
FROM amazon_order_lines
WHERE order_status = 'completed'
GROUP BY order_id, product_sku, product_name
)
SELECT
po.product_sku,
po.product_name,
COUNT(*) AS order_count,
ROUND(AVG(o.order_total), 2) AS avg_order_value,
ROUND(AVG(po.sku_revenue), 2) AS avg_sku_revenue,
ROUND(AVG(o.order_total - po.sku_revenue), 2) AS avg_other_items_value
FROM product_orders po
JOIN amazon_orders o ON o.order_id = po.order_id
WHERE o.order_date > (SELECT MAX(order_date) FROM amazon_order_lines) - 90
AND o.order_status = 'completed'
GROUP BY po.product_sku, po.product_name
HAVING COUNT(*) >= 20 -- skip SKUs with very few orders
ORDER BY avg_order_value DESC
LIMIT 20;
Python Implementation with Pandas
This continues from the recent and orders tables built in Step 1:
import numpy as np
# One row per (order, product): how much of that order the product accounts for
focal = (
recent.groupby(['order_id', 'product_sku', 'product_name'], as_index=False)['line_total']
.sum()
.rename(columns={'line_total': 'sku_revenue'})
.merge(orders[['order_total']], left_on='order_id', right_index=True)
)
focal['other_items_value'] = focal['order_total'] - focal['sku_revenue']
product_aov = focal.groupby(['product_sku', 'product_name']).agg(
order_count=('order_id', 'nunique'),
avg_order_value=('order_total', 'mean'),
order_value_sd=('order_total', 'std'),
avg_sku_revenue=('sku_revenue', 'mean'),
avg_other_items_value=('other_items_value', 'mean'),
).reset_index()
# Rough 95% margin for the average (normal approximation: 1.96 x standard error)
product_aov['margin_95'] = 1.96 * product_aov['order_value_sd'] / np.sqrt(product_aov['order_count'])
# Exploratory filter: skip SKUs with very few orders. This is not a significance test.
product_aov = product_aov[product_aov['order_count'] >= 20]
top_products = product_aov.sort_values('avg_order_value', ascending=False).head(20)
print(top_products.drop(columns='order_value_sd').round(2).to_string(index=False))
Expected Output
product_sku product_name order_count avg_order_value avg_sku_revenue avg_other_items_value margin_95
PRO-001 Premium Coffee Maker Set 147 137.70 115.09 22.61 9.25
ACC-234 Barista Accessories Bundle 212 73.37 45.39 27.98 6.55
MUG-100 Travel Mug 218 55.39 24.18 31.21 6.20
FIL-789 Water Filters (6-Pack) 285 52.62 23.43 29.19 5.68
CON-456 Gourmet Coffee Bean Sample 453 46.22 17.04 29.18 4.18
SPN-050 Measuring Spoon Set 160 38.04 12.92 25.12 6.37
Key Metrics Explained
- avg_order_value: Average total of the orders that contain this product (includes every item in those orders)
- avg_sku_revenue: Average revenue the product itself accounts for in those orders, units included
- avg_other_items_value: Average value of the other items in the same orders. It describes what was bought together; it does not show that the product caused those other purchases
- margin_95: Rough 95% margin around
avg_order_value(1.96 standard errors, normal approximation). Order values are skewed, so treat it as a guide to how noisy the average is, not an exact interval
The order_count >= 20 filter only drops products with very few orders. It is a convenience filter, not a significance test; the margin column is the better guide to precision. Products with many small orders and a few very large ones will have wide margins.
Strategic Applications
Use this analysis to:
- Find candidate anchor products: Items whose orders have a high
avg_other_items_valuesit in larger baskets, which is worth testing as a bundle or placement idea - Optimize advertising spend: Consider shifting PPC (pay-per-click advertising) budget toward products whose orders are high-value, then compare the results against the ad cost
- Improve product placement: Feature high-AOV products prominently in your storefront
- Bundle strategically: Create bundles around products that are often bought with other items
- Inventory prioritization: Ensure high-AOV products never go out of stock
Interpreting Your Order Value Analysis Results
Raw numbers only become valuable when you transform them into actionable insights. Here's how to interpret your analysis comprehensively.
Establishing Your Baseline Performance
Create a performance snapshot combining all three metrics, plus the what-if arithmetic behind the improvement ideas later in the tutorial:
# Baseline snapshot from the numbers computed above
print("Overall Performance Summary:")
print(f"- Average Order Value: ${aov:.2f}")
print(f"- Average Items Per Order: {avg_items:.2f}")
print(f"- Average Price Per Item: ${total_revenue / orders['item_count'].sum():.2f}")
print(f"- Single-Item Order Rate: {single_item_pct:.1f}%")
best = product_aov.sort_values('avg_order_value', ascending=False).iloc[0]
print(f"- Highest Product AOV: ${best['avg_order_value']:.2f} ({best['avg_order_value'] / aov:.1f}x overall average)")
# What-if arithmetic over the same 90-day window (nothing here is a forecast)
print()
print("What-if arithmetic, same orders, same 90 days:")
print(f"- AOV +10%: ${aov * 1.1:.2f} per order, +${total_revenue * 0.10:,.2f} over the window")
print(f"- Items per order +0.5: {avg_items:.2f} to {avg_items + 0.5:.2f}, +{0.5 / avg_items * 100:.0f}% units if order count is unchanged")
print(f"- Single-item orders to 35.0%: {total_orders * (single_item_pct - 35.0) / 100:.0f} more multi-item orders over the window")
Expected Output
Overall Performance Summary:
- Average Order Value: $49.21
- Average Items Per Order: 2.04
- Average Price Per Item: $24.17
- Single-Item Order Rate: 44.3%
- Highest Product AOV: $137.70 (2.8x overall average)
What-if arithmetic, same orders, same 90 days:
- AOV +10%: $54.13 per order, +$4,827.50 over the window
- Items per order +0.5: 2.04 to 2.54, +25% units if order count is unchanged
- Single-item orders to 35.0%: 92 more multi-item orders over the window
These are arithmetic on the sample above, not forecasts. They assume the same orders and customers, which is the part real changes rarely deliver, so use them to size an idea before testing it.
Trend Analysis Over Time
Calculate AOV by week or month to identify patterns:
SELECT
DATE_TRUNC('week', order_date) as week,
COUNT(DISTINCT order_id) as orders,
ROUND(AVG(order_total), 2) as avg_order_value,
ROUND(AVG(item_count), 2) as avg_items
FROM amazon_orders
WHERE order_status = 'completed'
AND order_date >= CURRENT_DATE - INTERVAL '180 days'
GROUP BY DATE_TRUNC('week', order_date)
ORDER BY week;
Look for:
- Seasonal patterns: Holiday spikes, summer slumps, or industry-specific cycles
- Growth trends: Is AOV increasing over time? Check whether prices, mix or promotions changed before reading it as improvement
- Anomalies: Sudden drops might indicate pricing errors or competitive pressure
Segmentation Insights
Break down AOV by meaningful dimensions:
- Customer type: New vs. returning customers
- Traffic source: Organic search vs. PPC vs. external traffic
- Device type: Mobile vs. desktop purchase behavior
- Fulfillment method: FBA vs. FBM differences (as discussed in our FBA vs FBM analysis)
Run It on Your Own Amazon Export
Export your orders from Amazon as a CSV and upload it at MCP Analytics, then ask the question in plain language; the report names the method. Nothing connects to your Amazon account.
Next Steps: From Analysis to Action
Now that you understand your order value metrics, implement these optimization strategies:
1. Optimize Product Bundling
Look at products whose orders carry a high avg_other_items_value. If customers buying Product A also tend to buy a lot of other items (the avg_other_items_value column shows how much), an "A + Accessories" bundle at a slight discount is a reasonable thing to test.
2. Enhance Cross-Sell Recommendations
Use your product-level AOV analysis to improve "Frequently Bought Together" and "Customers Also Bought" placements. Feature products that naturally appear in high-value orders.
3. Implement Tiered Pricing
Encourage larger orders with volume discounts: "Buy 2 get 10% off, Buy 3 get 20% off." Calculate breakpoints based on your current items-per-order distribution.
4. Free Shipping Thresholds
A free-shipping threshold set a little above your current AOV can encourage customers to add one more item. Pick a level, run it for a few weeks and compare AOV and order count against the weeks before; a threshold set too high may lose orders instead.
5. Expand High-AOV Product Lines
If certain products consistently appear in high-value orders, expand your catalog with complementary variations or accessories for those items.
6. A/B Test Pricing Strategies
Test price adjustments on high-volume products to find the optimal balance between price point and order value. Compare test and control groups over the same weeks, and judge differences against their uncertainty (see statistical significance).
A Monthly Review Routine
Order value work is not a one-time project. A monthly review cycle can help:
- Week 1: Review overall AOV trends and identify anomalies
- Week 2: Analyze top/bottom performers and adjust inventory accordingly
- Week 3: Test new bundling or pricing strategies
- Week 4: Measure impact of changes and plan next iteration
Troubleshooting Common Issues
Problem: AOV Calculation Seems Incorrect
Symptoms: Your calculated AOV doesn't match Amazon's reports or seems unreasonably high/low.
Common Causes:
- Including cancelled or refunded orders in the calculation
- Mixing pre-tax and post-tax order values
- Counting the same order once per line instead of once per order (the code above groups by
order_idfirst for this reason) - Including Amazon fees in the order total instead of the customer-paid amount
Solution:
-- Ensure you're filtering correctly
SELECT
COUNT(DISTINCT order_id) as clean_order_count,
ROUND(AVG(order_total), 2) as aov
FROM amazon_orders
WHERE order_status IN ('completed', 'shipped') -- exclude canceled/pending; use your own status labels
AND order_total > 0 -- exclude zero-value orders
AND order_date >= CURRENT_DATE - INTERVAL '90 days';
Problem: Insufficient Data for Product-Level Analysis
Symptoms: Most products have fewer than 20 orders, so the averages are noisy.
Solution:
- Extend your analysis time period (e.g., from 30 to 90 or 180 days)
- Group products by category instead of individual SKUs
- Lower the minimum order count for exploratory analysis, and read the
margin_95column before trusting a ranking
-- Category-level analysis for smaller catalogs
-- (needs a category column on amazon_order_lines, added by you)
-- One row per order and category first, so each order counts once per category
WITH order_categories AS (
SELECT DISTINCT order_id, category
FROM amazon_order_lines
WHERE order_status = 'completed'
)
SELECT
oc.category,
COUNT(*) AS order_count,
ROUND(AVG(o.order_total), 2) AS avg_order_value
FROM order_categories oc
JOIN amazon_orders o ON o.order_id = oc.order_id AND o.order_status = 'completed'
GROUP BY oc.category
HAVING COUNT(*) >= 50
ORDER BY avg_order_value DESC;
Joining the lines straight to the orders would repeat an order's total once per line, so orders with many lines would weigh more in the average. An order with products from two categories still counts in both, so the category counts can add up to more than the total number of orders.
Problem: Wide Variance in Order Values
Symptoms: Your AOV is $50 but individual orders range from $5 to $500, making the average misleading.
Solution: Use median and percentiles alongside mean:
SELECT
ROUND(AVG(order_total), 2) as mean_aov,
ROUND((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY order_total))::numeric, 2) as median_aov,
ROUND((PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY order_total))::numeric, 2) as p25_aov,
ROUND((PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY order_total))::numeric, 2) as p75_aov,
ROUND(STDDEV(order_total), 2) as standard_deviation
FROM amazon_orders
WHERE order_status = 'completed';
If the median is far from the mean, consider segmenting your analysis by order size brackets (e.g., $0-25, $25-50, $50-100, $100+).
Problem: Seasonal Variations Skewing Results
Symptoms: AOV is much higher/lower during certain months, making year-round comparisons difficult.
Solution: Perform year-over-year comparisons for the same period:
SELECT
EXTRACT(MONTH FROM order_date) as month,
EXTRACT(YEAR FROM order_date) as year,
ROUND(AVG(order_total), 2) as aov
FROM amazon_orders
WHERE order_status = 'completed'
AND order_date >= CURRENT_DATE - INTERVAL '24 months'
GROUP BY EXTRACT(YEAR FROM order_date), EXTRACT(MONTH FROM order_date)
ORDER BY year, month;
Problem: Data Export Limitations
Symptoms: One export does not cover the whole period you want to analyze.
Solution:
- Download multiple smaller date ranges and concatenate them
- Keep each month's export in your own folder so you build a history yourself
Your turn
Bring your own data and the question you actually need answered.
MCP AnalyticsData science by email Send us your data and your question, and we will reply. ds@mcpanalytics.ai
Explore more: Amazon Seller Analytics: all tools, tutorials, and guides →
Not sure which plan? Compare plans →