A store owner opens Analytics, sees the category report, and asks why we keep pushing a small category when the big one shows more repeat orders. Usually the report is counting raw orders, and the big category is big. I ran into the same wall reading Mahnoor Javed’s census income walkthrough on Towards Data Science, a beginner pandas and seaborn tutorial. For store work, the useful part is the exact spot where counting quietly fails, because WooCommerce order data fails there too.
What the census walkthrough measures
The dataset is the Adult Census Income file from the UCI repository, a 1994 extract where every person is labeled as earning over or under 50K. Javed loads it with pd.read_csv, replaces the question-mark placeholders with NA, drops incomplete rows, and the file shrinks from 32,561 rows to 30,162. Then seaborn plots income against education, hours worked, gender, workclass, occupation and age. Education and occupation track income, age tracks it, hours only weakly, and the writeup is honest that no single factor decides it.
The weak spots are the charts of raw counts. The file is imbalanced, with far more people under 50K than over, and the groups being compared differ hugely in size. The education bar chart shows HS-grad dominating the low bracket, which is true but mostly reflects that HS-grad is the biggest group in the file. Doctorates look impressive for the opposite reason: the group is small and the rate is high. Neither bar is a rate, and on the gender chart the author flags it herself:
we cannot clearly see gender discrimination in this plot
The one chart that behaves is the hours-per-week boxplot, because a boxplot summarizes the distribution inside each group, so group size stops leaking into the picture. That is the standard I hold store charts to.
The same trap in WooCommerce order data
Store data is imbalanced in the same shape. One-time buyers outnumber repeat buyers, a couple of categories dominate volume, and any chart built on raw counts inherits all of it. Where WooCommerce admin reports get read is usually a screenshot in a monthly email, and that is where a misleading count does the most damage.
Getting the data out is the easy half. WC-CLI sits on the REST API and still works now that HPOS moved orders off wp_posts. Run wp wc shop_order list --fields=id,status,date_created,total,customer_id,payment_method --format=csv > orders.csv. Note the command pages at 100 results per call, so pass --per_page and loop, or use the Analytics CSV export once the store has real volume.
Load it with read_csv and let one crosstab do the work the dashboard leaves out.
import pandas as pd
orders = pd.read_csv("orders.csv")
# repeat buyer: this customer_id shows up on more than one order
orders["repeat"] = orders.groupby("customer_id")["customer_id"].transform("size").gt(1)
pd.crosstab(orders["payment_method"], orders["repeat"], normalize="index").round(3)normalize="index" turns each row into proportions that sum to 1, so every payment method answers the same question: of every 100 orders paid this way, how many came from someone who ordered again. The raw count can only tell you that most repeat orders used cards, because most orders use cards. The rate tells you which methods retain, and that is a decision. Swap payment_method for any column you join in, like shipping zone, category, or coupon used, and the line does not change. I am not sure how much of this holds on subscription stores, where every renewal is an order and repeat is the default.
Before running it, decide what counts as an order worth measuring (refunds and failed checkouts included or not), the same way defining revenue has to come before any analytics. And if you want this pass over your own store (the WP-CLI pull plus a short notebook with the normalized tables), that is work I take on. Tell me which decision it needs to support and I will shape the tables around it.
Where the method stops holding
Dropping rows first. The tutorial drops about seven percent of the file, which is fine for learning pandas but wrong for a store. Missing values in real order exports cluster where checkout failed or a plugin did not save a field, so dropna() deletes the interesting rows along with the broken ones. Count what you are dropping before you drop it.
Then weights. The census file carries an fnlwgt column because the survey over- and under-sampled groups, and ignoring it, which every tutorial does, means the proportions describe the file rather than the country. A store order file has its own version: it over-represents whoever your ads reached last month. Small groups bite too. A category with 40 orders can post a lovely repeat rate and still be noise.
The question I still do not have a clean answer to is how big a segment needs to be before I trust its rate enough to change a decision. Everything above depends on that threshold, and on a store with a few hundred orders a month, it decides whether the table changes anything.