ds@mcpanalytics.aiEmail your data and your question and get an answer about your own data, with the method shown.Email your data and question. Get an answer.

How to Use Billing vs Shipping Address Analysis in Squarespace: Step-by-Step Tutorial

Updated October 2026

Category: Squarespace Analytics | Reading Time: 10 minutes

Introduction to Billing vs Shipping Address Analysis

Understanding the difference between billing and shipping addresses in your Squarespace orders isn't just about logistics. It is a window into customer behavior. When a customer's billing address differs from their shipping address, the reason can be a gift, a business buyer, a customer with more than one home, or a billing address that is simply out of date. The addresses alone cannot say which, but the share of orders that differ is a useful number to track.

Billing vs shipping address variance analysis measures how frequently customers choose to ship products to addresses different from their payment card's billing address. A higher rate is consistent with more gift or business shipping, and a lower one with more orders shipped to the buyer's own address, but confirming either needs evidence beyond the addresses, such as a gift option or message chosen at checkout, or a customer survey. Watching how the rate moves from month to month can show when it rises, which is a prompt to investigate, not a forecast.

This tutorial calculates the share of orders whose shipping address differs from the billing address, step by step with pandas, and shows how to state how precise that share is and how to check the matching rule.

Prerequisites and Data Requirements

Export Your Squarespace Orders

Export your orders as a CSV by following Squarespace's help page on exporting Commerce orders. Its field list includes billing columns (Billing Name, Billing Address1, Billing Address2, Billing City, Billing Zip, Billing Province, Billing Country, Billing Phone) and the matching shipping columns. The page states that order-wide details, such as the shipping address, appear "only in the first row with that order ID" and that the other rows show line item details only. An order with three items is therefore three rows with the address on one of them, and counting rows instead of orders would overweight large orders.

Step-by-Step Analysis Process

Step 1: Load the Export

The code below builds a small synthetic export in the same layout. It is made up for this tutorial and does not come from any store; every figure in the outputs on this page comes from it. Orders 3001 and 3011 have a second row with blank address fields.

import pandas as pd

# Synthetic orders made up for this tutorial. Column names follow the Squarespace order export;
# the values do not come from any store. Order-wide fields (the addresses) are filled only on the
# first row of each order, as the export does; later rows of the same order carry line item details only.
cols = ["Order ID", "Billing Address1", "Billing City", "Billing Province", "Billing Zip", "Billing Country",
        "Shipping Address1", "Shipping City", "Shipping Province", "Shipping Zip", "Shipping Country"]
orders = [
    # same address, with spelling differences that normalization should absorb
    (3001, "12 Oak Street", "Austin", "TX", "78701", "US", "12 Oak St.", "Austin", "TX", "78701", "US"),
    (3002, "40 Pine Avenue", "Denver", "CO", "80202", "US", "40 Pine Ave", "Denver", "CO", "80202", "US"),
    (3003, "7 Lake Road", "Boston", "MA", "02108", "US", "7 Lake Rd", "Boston", "MA", "02108", "US"),
    (3004, "88 Elm Street", "Seattle", "WA", "98101", "US", "88 Elm Street", "Seattle", "WA", "98101", "US"),
    (3005, "5 Cedar Lane", "Miami", "FL", "33101", "US", "5 Cedar Lane", "Miami", "FL", "33101", "US"),
    (3006, "301 Main Street", "Chicago", "IL", "60601", "US", "301 Main Street", "Chicago", "IL", "60601", "US"),
    (3007, "19 Birch Drive", "Portland", "OR", "97201", "US", "19 Birch Drive", "Portland", "OR", "97201", "US"),
    (3008, "250 Hill Street", "Atlanta", "GA", "30301", "US", "250 Hill Street", "Atlanta", "GA", "30301", "US"),
    # same city, different street
    (3009, "12 Oak Street", "Austin", "TX", "78701", "US", "900 River Road", "Austin", "TX", "78702", "US"),
    (3010, "40 Pine Avenue", "Denver", "CO", "80202", "US", "15 Park Avenue", "Denver", "CO", "80203", "US"),
    # different city, same country
    (3011, "7 Lake Road", "Boston", "MA", "02108", "US", "3 Shore Road", "Providence", "RI", "02903", "US"),
    (3012, "88 Elm Street", "Seattle", "WA", "98101", "US", "61 Sun Street", "Phoenix", "AZ", "85001", "US"),
    (3013, "5 Cedar Lane", "Miami", "FL", "33101", "US", "22 Bay Lane", "Tampa", "FL", "33602", "US"),
    # different country
    (3014, "301 Main Street", "Chicago", "IL", "60601", "US", "10 King Street", "Toronto", "ON", "M5H 2N2", "CA"),
    # no shipping address captured (for example a digital product)
    (3015, "19 Birch Drive", "Portland", "OR", "97201", "US", None, None, None, None, None),
    (3016, "250 Hill Street", "Atlanta", "GA", "30301", "US", None, None, None, None, None),
]
rows = pd.DataFrame(orders, columns=cols)
# Second line items of orders 3001 and 3011: every address field is blank, as in the export.
extra = pd.DataFrame({"Order ID": [3001, 3011]})
export = pd.concat([rows, extra], ignore_index=True).sort_values("Order ID", kind="stable").reset_index(drop=True)
print("Rows in the export:", len(export), "| distinct orders:", export["Order ID"].nunique())
print(export[["Order ID", "Billing City", "Shipping City"]].head(5).to_string(index=False))

Output:

Rows in the export: 18 | distinct orders: 16
 Order ID Billing City Shipping City
     3001       Austin        Austin
     3001          NaN           NaN
     3002       Denver        Denver
     3003       Boston        Boston
     3004      Seattle       Seattle

Step 2: One Row Per Order, Then Compare

Group by Order ID so that each order has one billing and one shipping address. Then standardize spelling (case, punctuation, "Street" against "St") and classify each order from the largest difference to the smallest. Orders with no shipping address, such as a digital product, are set aside rather than counted as "different". Squarespace's province and country values can also come in different spellings in your own file (for example "California" and "CA"); add them to the standardizing step if your data has both.

import re

# One row per order: the address fields live on the first row of each Order ID.
# groupby(...).first() takes the first non-blank value per column, so it also skips the blank rows.
orders = export.groupby("Order ID", as_index=False).first()

ABBREV = {"street": "st", "avenue": "ave", "road": "rd", "drive": "dr", "lane": "ln", "apartment": "apt"}

def norm(value):
    if pd.isna(value):
        return None
    words = re.sub(r"[^\w\s]", " ", str(value).lower()).split()
    return " ".join(ABBREV.get(w, w) for w in words)

for part in ["Address1", "City", "Province", "Zip", "Country"]:
    orders["b_" + part] = orders["Billing " + part].map(norm)
    orders["s_" + part] = orders["Shipping " + part].map(norm)

def classify(r):
    if r["s_Address1"] is None or r["b_Address1"] is None:
        return "excluded: address missing"
    if r["s_Country"] != r["b_Country"]:
        return "different country"
    if (r["s_City"], r["s_Province"]) != (r["b_City"], r["b_Province"]):
        return "different city"
    if (r["s_Address1"], r["s_Zip"]) != (r["b_Address1"], r["b_Zip"]):
        return "same city, different street"
    return "same address"

orders["status"] = orders.apply(classify, axis=1)
print(orders["status"].value_counts().to_string())

Output:

status
same address                   8
different city                 3
same city, different street    2
excluded: address missing      2
different country              1

The five groups add up to the 16 orders from Step 1. In this tutorial a shipping address "differs" when any compared part differs after standardizing, which includes a different street in the same city. If you want a stricter definition, for example different city only, change the final condition and say which one you used.

Step 3: Calculate the Rate and How Precise It Is

The rate is the orders with a different shipping address divided by the orders that have both addresses. Report the excluded orders next to it, because an exclusion that is not random changes the answer. Compare the excluded orders with the rest (for example by product type) before assuming they do not matter. The code also adds a 95% Wilson interval for the proportion, which shows how far the rate could move with this many orders.

import math

used = orders[~orders["status"].str.startswith("excluded")]
n = len(used)
diff = int((used["status"] != "same address").sum())
rate = diff / n
# Wilson 95% interval for a proportion: how far the rate could move with this many orders.
z = 1.96
centre = (rate + z**2 / (2 * n)) / (1 + z**2 / n)
half = z * math.sqrt(rate * (1 - rate) / n + z**2 / (4 * n**2)) / (1 + z**2 / n)
print("Orders:", len(orders), "| excluded for missing address:", len(orders) - n, "| used:", n)
print("Orders with a different shipping address:", diff, "of", n, f"= {100 * rate:.1f}%")
print(f"95% interval for the rate: {100 * (centre - half):.1f}% to {100 * (centre + half):.1f}%")

Output:

Orders: 16 | excluded for missing address: 2 | used: 14
Orders with a different shipping address: 6 of 14 = 42.9%
95% interval for the rate: 21.4% to 67.4%

With 14 orders the rate is 42.9%, but the interval runs from 21.4% to 67.4%, so these data cannot separate a quarter of orders from two thirds. The interval narrows as the order count rises.

Step 4: Read the Result Carefully

Step 5: Check the Matching Rule

The rule can fail in two directions: it can call two spellings of one address "different", or call two different addresses "the same". Checking only the orders it flagged as different measures just the first error. Draw a random sample from each group, the same-address group and the different-address group, open those orders and judge them by eye. Count the misclassified orders in each group separately: a count such as "1 of 20 flagged orders was really the same address, 0 of 20 matched orders were really different" is more useful than one accuracy figure, and a sample of 20 still leaves a wide margin. Fix the standardizing step where you find a pattern (units, PO boxes, abbreviations) and rerun.

Run It on Your Own Squarespace Export

Export your orders from Squarespace as a CSV and upload it at MCP Analytics. The analysis runs a comparison of billing and shipping addresses on your file and shows the result with the method used. You choose the columns; nothing connects to your Squarespace account.

Try it on your data

Common Issues and Solutions

Issue 1: The Rate Looks Too High or Too Low

Issue 2: Many Orders Are Excluded for a Missing Address

Report the count next to the rate. Then compare the excluded and the included orders (product type, order value, month). If they differ, the rate describes only the included orders, and the page or report should say so. An exclusion share alone does not tell you whether the remaining orders represent the store.

Issue 3: Mixed Spellings of States or Countries

Add the variants you see in your file to the standardizing step (for example a table that maps full names to codes), then rerun and recheck a sample.

Next Steps with Squarespace Analytics

Explore more: Squarespace Analytics: all tools, tutorials, and guides →

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