How to Use Digital vs Physical Product Analysis in Etsy: Step-by-Step Tutorial
Updated October 2026
Introduction: Why Digital vs Physical Product Analysis Matters
As an Etsy seller, one of the most critical strategic decisions you'll face is determining the optimal mix between digital and physical products. Should you invest more time creating digital downloads that carry no per-unit materials or shipping cost, or focus on handcrafted physical items that command premium prices? How significantly do shipping costs erode your physical product margins?
These questions don't have universal answers. They depend entirely on your unique business data. That's where systematic digital versus physical product analysis becomes invaluable. By examining your actual sales performance across both product categories, you can make evidence-based decisions that maximize profitability and align with your business goals.
This tutorial compares:
- Revenue contribution: Which product type drives more sales volume?
- Profit margin reality: After accounting for all costs (especially shipping), which products are truly more profitable?
- Growth opportunities: Where should you focus your expansion efforts?
Whether you're running a shop that offers both product types or considering adding one category to complement the other, this analysis will provide the clarity you need to optimize your Etsy business strategy.
Step 1: Prerequisites and Data Requirements
This tutorial compares your digital and physical products on revenue, cost, profit and orders. All code uses pandas, and every figure on the page comes from a synthetic data set of 896 orders over one year that was generated for this tutorial. It is not from a real shop, so your numbers will differ.
What You Need
- Orders with a product type: for each order, an order ID, date, quantity, item price, and whether it is digital or physical. If your export does not say, add a
product_typecolumn yourself from the listing title or SKU. - Your shipping cost per order: what you paid the carrier, from your shipping labels or records. It is a cost to you, separate from any shipping the buyer paid.
- Your cost of goods: materials and labor for physical items, software and design time for digital ones, as a share of revenue or per item. These come from your own records.
- Enough orders in each group: with only a few dozen orders of one type, its margin is an unstable estimate. How many are enough depends on how much your order values vary; the more variable, the more you need.
Digital and Physical Products
Digital products are items delivered as a file: printable art, planners, SVG files, patterns, templates, fonts, worksheets. Physical products are items you make or source and ship. Items that are both (an art print sold as a file or as a shipped print) need to be classified by what the buyer ordered.
Exporting Your Etsy Orders
Etsy lets sellers download sold orders as a CSV from Shop Manager. We could not check Etsy's current help pages when updating this tutorial, so treat the menu path, the file name and the column names as assumptions to check against your own account and file. Confirm that the file has an order ID, a date, a quantity and a price per item, and check whether the price includes or excludes shipping. An order export is unlikely to contain your own costs, buyer identifiers, reviews, returns or delivery times; the analysis below uses only data you can supply.
Classifying by title: if you label products from titles with keywords, review the result by hand, because a title containing "download" can still be a physical item.
Step 2: Build the Order Table
The block below builds the synthetic orders. It prints the number of orders in each group and the total, which should equal the number of rows in your file. Replace the generator with your own data when you run it.
import numpy as np
import pandas as pd
# Synthetic orders generated for this tutorial. Replace this block with your own order export,
# renamed to these columns. item_price is the price per item; shipping_cost is what YOU paid
# the carrier for that order (from your own records or shipping labels).
rng = np.random.default_rng(11)
n_dig, n_phy = 312, 584
orders = pd.DataFrame({
'order_id': np.arange(1, n_dig + n_phy + 1),
'product_type': ['Digital'] * n_dig + ['Physical'] * n_phy,
})
orders['order_date'] = pd.Timestamp('2024-01-01') + pd.to_timedelta(rng.integers(0, 366, len(orders)), unit='D')
orders['quantity'] = np.where(orders['product_type'] == 'Digital',
rng.choice([1, 2, 3], len(orders), p=[0.4, 0.4, 0.2]),
rng.choice([1, 2], len(orders), p=[0.75, 0.25]))
orders['item_price'] = np.where(orders['product_type'] == 'Digital',
rng.gamma(6, 20 / 6, len(orders)), rng.gamma(6, 30 / 6, len(orders))).round(2)
orders['shipping_cost'] = np.where(orders['product_type'] == 'Digital', 0.0,
rng.gamma(8, 5 / 8, len(orders))).round(2)
orders['revenue'] = (orders['item_price'] * orders['quantity']).round(2)
print(orders.groupby('product_type').size().rename('orders').to_frame().T.to_string())
print('total order rows:', len(orders))
Expected output:
product_type Digital Physical
orders 312 584
total order rows: 896
Everything that follows uses these 896 orders: 312 digital and 584 physical.
Step 3: Interpreting Your Results
Profit by Product Type
The next block applies cost assumptions to every order and totals revenue, each cost line and profit by type. The cost lines are: cost of goods (as a share of revenue), shipping you paid, packaging, Etsy fees and payment fees. The percentages and the packaging figure are assumptions for the illustration (10% and 45% cost of goods, 6.5% Etsy fee, 3% + 0.25 per order payment fee, 0.80 packaging per physical order). Replace them with your own; take fee rates from Etsy's fee pages and your payment account statements.
# Cost assumptions: replace with your own. The fee rates are placeholders; take yours from Etsy's fee pages
# and your payment account statements.
COGS_PCT = {'Digital': 0.10, 'Physical': 0.45} # share of revenue
ETSY_FEE_PCT = 0.065
PAYMENT_PCT, PAYMENT_FIXED = 0.03, 0.25 # per order
PACKAGING_PER_ORDER = {'Digital': 0.0, 'Physical': 0.80}
orders['cogs'] = orders['revenue'] * orders['product_type'].map(COGS_PCT)
orders['etsy_fee'] = orders['revenue'] * ETSY_FEE_PCT
orders['payment_fee'] = orders['revenue'] * PAYMENT_PCT + PAYMENT_FIXED
orders['packaging'] = orders['product_type'].map(PACKAGING_PER_ORDER)
cost_cols = ['cogs', 'shipping_cost', 'packaging', 'etsy_fee', 'payment_fee']
orders['profit'] = orders['revenue'] - orders[cost_cols].sum(axis=1)
summary = orders.groupby('product_type')[['revenue'] + cost_cols + ['profit']].sum().T.round(2)
summary.loc['margin_pct'] = (100 * summary.loc['profit'] / summary.loc['revenue']).round(1)
print(summary.to_string())
print('revenue share:', (100 * summary.loc['revenue'] / summary.loc['revenue'].sum()).round(1).to_dict())
Expected output:
product_type Digital Physical
revenue 10996.34 22117.92
cogs 1099.63 9953.06
shipping_cost 0.00 2942.08
packaging 0.00 467.20
etsy_fee 714.76 1437.66
payment_fee 407.89 809.54
profit 8774.05 6508.37
margin_pct 79.80 29.40
revenue share: {'Digital': 33.2, 'Physical': 66.8}
Physical products bring in 66.8% of revenue (22,117.92 of 33,114.26), about twice the digital figure of 10,996.34. After the assumed costs, digital profit is 8,774.05 (a margin of 79.8%) and physical profit is 6,508.37 (29.4%), so in this example digital products earn more profit on half the revenue. Part of that gap comes from the cost assumptions themselves: the cost of goods line differs by 8,853 (9,953.06 against 1,099.63); 7,741 of that comes from the 45% against 10% rates and the remaining 1,112 from physical revenue being about twice as large, so change those inputs and the answer changes. The table includes the costs listed above and no others: it has no allowance for your time on listings, advertising or overhead.
Per-Order Metrics
per = orders.groupby('product_type').agg(orders=('order_id', 'count'), items=('quantity', 'sum'),
revenue=('revenue', 'sum'), profit=('profit', 'sum'))
per['avg_order_value'] = (per['revenue'] / per['orders']).round(2)
per['items_per_order'] = (per['items'] / per['orders']).round(2)
per['profit_per_order'] = (per['profit'] / per['orders']).round(2)
print(per[['orders', 'avg_order_value', 'items_per_order', 'profit_per_order']].T.to_string())
Expected output:
product_type Digital Physical
orders 312.00 584.00
avg_order_value 35.24 37.87
items_per_order 1.75 1.23
profit_per_order 28.12 11.14
Average order values are close (35.24 digital, 37.87 physical), but that does not say what buyers would pay for the two kinds of product; it says what these baskets came to. Digital orders have more items per order (1.75 against 1.23). Profit per order is 28.12 against 11.14, which is the same finding as the margin table expressed per order.
Customer Behavior
Repeat purchase rate, review rate, return rate and fulfillment time are useful for this comparison, but they need data that an order export does not normally hold: a buyer identifier to count repeat buyers, review and return records, and delivery dates. If you have them, compute each for the two groups on the same basis, for example the share of buyers with two or more orders in the period, and compare with the order counts in mind. This tutorial does not give example figures for them, because the synthetic data set has none.
Monthly Revenue
If you have a full year, group revenue by month and type. The last line checks that the months add up to the total revenue.
monthly = (orders.assign(month=orders['order_date'].dt.to_period('M'))
.pivot_table(index='month', columns='product_type', values='revenue', aggfunc='sum', fill_value=0)
.round(2))
print(monthly.to_string())
print('sum of months equals total revenue:', bool(np.isclose(monthly.to_numpy().sum(), orders['revenue'].sum(), atol=0.05)))
Expected output:
product_type Digital Physical
month
2024-01 1192.60 1991.23
2024-02 1378.55 1800.51
2024-03 973.36 1773.46
2024-04 797.74 1759.07
2024-05 714.21 1887.66
2024-06 537.87 1584.27
2024-07 932.65 1793.17
2024-08 937.94 1901.04
2024-09 767.76 1932.08
2024-10 762.22 2069.56
2024-11 1082.43 2148.44
2024-12 919.01 1477.43
sum of months equals total revenue: True
The synthetic order dates are random, so this table has no seasonal pattern by construction. In your own data, look for months where one type moves differently from the other, and compare the same month across years if you have them before you plan around a season.
Step 4: Making Data-Driven Decisions
The tables describe what happened in the period you chose. They do not say what would happen if you changed your mix, so treat each use below as a hypothesis to test.
Scenario 1: Should I Offer More Digital Products?
Questions the data can inform:
- Is the digital margin well above the physical margin on your own cost figures, and are those cost figures complete?
- How much of your time does each new digital product take, compared with the profit it has brought in?
- How concentrated is digital revenue: one best-seller or many listings?
Questions it cannot answer: whether more digital listings will sell as well as the ones you have, and whether your niche is crowded. A small test, such as launching two or three digital products and comparing the first months with your existing ones, is better evidence than a threshold on a margin.
Scenario 2: How Do Shipping Costs Affect Physical Margins?
Shipping you paid is a column in the table, so you can see how much of physical revenue it takes and what a change would do. The block below asks what the margin would be if shipping cost per order fell by 25%, everything else unchanged.
# What if shipping cost per physical order fell by 25%? Everything else unchanged.
phy = summary['Physical']
new_profit = phy['profit'] + 0.25 * phy['shipping_cost']
print('shipping as % of physical revenue:', round(100 * phy['shipping_cost'] / phy['revenue'], 1))
print('physical margin now:', phy['margin_pct'], '-> with 25% lower shipping:', round(100 * new_profit / phy['revenue'], 1))
Expected output:
shipping as % of physical revenue: 13.3
physical margin now: 29.4 -> with 25% lower shipping: 32.8
Shipping is 13.3% of physical revenue in the example, and a 25% reduction would lift the physical margin from 29.4% to 32.8%. The 25% is a what-if, not a promise that any carrier or package change delivers it. Ways to look for a saving include comparing carriers and labels, lighter packaging, and encouraging multi-item orders; build any saving into the numbers only once you have a quote.
Scenario 3: Balancing the Mix
Digital and physical products can support each other: a physical buyer may also want the digital version, and the reverse. If you have buyer identifiers, count how many buyers purchased both kinds. If you do not, you cannot measure it from an order export. For seasonal planning, use the monthly table from your own data for at least a year before changing what you launch when.
To test pricing changes or product descriptions systematically, see our guide to A/B testing and statistical significance.
Run It on Your Own Etsy Export
Export your orders from Etsy as a CSV and upload it at MCP Analytics. The analysis compares revenue and orders for digital and physical products on your file and shows the result with the method used. You choose the columns; nothing connects to your Etsy account. Add a product type column and your cost columns to the file first if you want profit and margin.
Step 5: Next Steps with Etsy Analytics
- Product-level performance: the same table by listing shows which individual products carry each type's margin.
- Customer lifetime value: if you have buyer identifiers, compare repeat buying for buyers who started with each type.
- Pricing tests: use the margin as a baseline and change one price at a time, comparing orders before and after.
- Re-run quarterly: keep the revenue share, margin per type and shipping share for each period so you can see whether they move.
Step 6: Common Issues and Solutions
Issue 1: Missing Product Type
Problem: Your export does not say whether an item is digital or physical.
Solution: Classify from the listing title or SKU with keywords, then review by hand. For example:
items = pd.DataFrame({'title': ['Printable Wall Art (digital download)', 'Hand-Poured Candle', 'Planner Template']})
items['product_type'] = items['title'].str.contains('digital|printable|download|template', case=False, regex=True).map({True: 'Digital', False: 'Physical'})
print(items.to_string(index=False))
Expected output:
title product_type
Printable Wall Art (digital download) Digital
Hand-Poured Candle Physical
Planner Template Digital
Keyword rules misclassify some items, so read a sample of each group.
Issue 2: Shipping on Mixed Orders
Problem: An order contains both a digital and a physical item, and shipping is recorded for the whole order.
Solution: Digital items need no shipping, so assign the order's shipping cost to the physical items. If there are several physical items, split it by their value. State which rule you used next to the result.
Issue 3: No Exact Cost of Goods
Problem: You do not track cost per product.
Solution: Use rough figures from your receipts and time records, label them as estimates, and run the analysis at two or three different cost percentages to see how much the margin depends on them. Keep a simple log of costs per product from now on.
Issue 4: Too Few Orders
Problem: One of the groups has only a few dozen orders.
Solution: Use a longer date range, or compare at a coarser level, and read the margins as rough. Re-run when you have more orders.
Issue 5: Seasonal Business
Problem: Your sales are concentrated in a season, so a short window is not typical.
Solution: Use at least a full year, or compare the same months across years, and run peak and off-peak separately.
Issue 6: A Few Large Orders Skew the Averages
Problem: A few very large or small orders move the average order value.
Solution: Report the median next to the mean, and look at the largest orders by hand before deciding whether any of them is an error or a one-off:
print(orders.groupby('product_type')['revenue'].agg(['mean', 'median']).round(2).to_string())
print(orders.nlargest(5, 'revenue')[['order_id', 'product_type', 'revenue']].to_string(index=False))
Conclusion: Data-Driven Product Strategy
Revenue and profit can rank two product types in opposite orders. In the synthetic example physical products earn twice the revenue and digital products earn more profit, but only under the cost assumptions stated above, and only for costs that were counted. The method is what carries over to your shop: classify each order, count the costs you actually have, reconcile the totals, and say which inputs are assumptions. Then test any change you make instead of reading it off the table.
Explore more: Etsy Shop Analytics: all tools, tutorials, and guides →