A raw orders/sales export is not a dashboard input β it is a dashboard's raw material. It typically arrives with 20-60 columns (order id, timestamps in two formats, SKU, category, channel, currency, discount code, tax, shipping, refund flag, customer id, and a dozen more), most of which do not belong on a business owner's screen. This page covers the four things an agent must do between "I have a CSV" and "here is a dashboard someone will actually read": select the metrics, aggregate them correctly, separate trend from noise, and catch the anomaly row that would otherwise poison an average silently.
Pick 3-5 KPIs, not one chart per column
Business β A dashboard with 20 charts gets glanced at once and ignored. A dashboard with 4 numbers gets checked every Monday. A KPI's value is not how much data it summarizes β it is whether it changes what the owner does next. A column that cannot be tied to an action is trivia, not a KPI.
Technical β Apply one filter to every candidate column: if this number moved 20% next week, would the business owner change a decision because of it? Run every column through it before charting anything.
| Candidate column | Decision it could change | Keep as KPI? |
|---|---|---|
revenue (daily/weekly total) | Spend more/less on ads, restock urgency | Yes |
order_count | Staffing, fulfillment capacity | Yes |
average_order_value | Bundle/upsell strategy | Yes |
refund_rate | Product quality or listing-accuracy issue | Yes |
top_sku_share (revenue concentration) | Diversification risk | Often yes |
customer_id (raw) | None β an identifier, not a metric | No |
shipping_zip | None on its own | No |
tax_amount | Rarely β usually a pass-through, not a lever | No |
Most exports yield 15-30 "keepable" numeric columns and only 3-5 that survive the decision test. Discard the rest from the dashboard β keep them in the underlying table for drill-down, but do not chart them by default. If two KPIs move together for every plausible scenario (revenue and units_sold when price is fixed), keep the one closer to the decision (revenue) and drop the other.
Step 1 β Aggregate: build the pivot-table equivalent
Business β The same revenue number can look like a flat line or a jagged saw depending on whether it is grouped by day, week, or month. Picking the wrong granularity does not just make the chart uglier β it changes the conclusion the owner draws. Daily data on a young or low-volume store is mostly weekday/weekend noise, not signal; monthly data on a fast-moving store hides a mid-month slump until it is too late to act on it.
Technical β The aggregation itself is a group-by plus a reducer β exactly what a spreadsheet pivot table does, just done in code so it is repeatable:
- Parse every date column to a single normalized type first. Exports routinely mix
2026-03-01,03/01/2026, andMar 1 2026in the same file β normalize before grouping or the group-by silently creates duplicate buckets. - Choose the grouping key (
datetruncated to day/week/month, plus optionalcategoryorchannel) and the reducer per column:sumfor flow quantities (revenue,units_sold,refunds),meanfor rates (average_order_value,refund_rate),nuniquefororder_count. - Pick granularity by volume, not habit: fewer than ~30 orders/day β weekly buckets minimum; 30-300/day β daily is usable; strong weekday seasonality β also keep a 7-day rolling view so weekday effects do not read as trend.
df["order_date"] = pd.to_datetime(df["order_date"], errors="coerce")
weekly = (
df.groupby(pd.Grouper(key="order_date", freq="W"))
.agg(revenue=("line_total", "sum"),
orders=("order_id", "nunique"),
refunds=("is_refund", "sum"))
)
weekly["aov"] = weekly["revenue"] / weekly["orders"]
weekly["refund_rate"] = weekly["refunds"] / weekly["orders"]
Rows that fail date parsing (errors="coerce" turns them into NaT) must be counted and reported, not silently dropped β a jump in unparseable rows is itself a data-quality signal worth surfacing.
Step 2 β Trend vs noise in a short series
Business β Three weeks of data is not a trend, it is three data points. An owner who sees two down weeks and panics-cuts ad spend, or sees two up weeks and over-orders inventory, is reacting to noise. The dashboard's job is to make "this is drifting" versus "this is normal wobble" visible without a statistics degree.
Technical β Two lightweight, explainable techniques cover almost every practical case β no hypothesis testing required:
- Rolling average. Plot a 3-7 period rolling mean alongside the raw series. If the raw line crosses the rolling line constantly, that is noise. If the rolling line itself is sloping, that is signal.
- Consecutive-direction heuristic. Flag a trend only when the metric moves the same direction (up or down) for 3 consecutive periods, each beyond a small deadband (e.g., >2% change) to ignore rounding-level noise. One up week after two down weeks is not a reversal; three in a row is worth a headline on the dashboard ("Revenue up 3 weeks running").
pct_change = weekly["revenue"].pct_change()
direction = pct_change.apply(lambda x: "up" if x > 0.02 else ("down" if x < -0.02 else "flat"))
is_trend = (direction.rolling(3).apply(lambda w: len(set(w)) == 1 and "flat" not in set(w)).fillna(0).astype(bool))
Label the chart with the verdict, not just the line β "trending up" or "no clear trend" next to the number does more for a business owner than the sparkline itself.
Step 3 β Flag anomaly rows before they corrupt an aggregate
Business β One duplicated order, one refund logged as a $0 sale, one test order from the warehouse β any of these can silently drag an average or spike a total, and a dashboard that reports the corrupted number without a warning quietly trains the owner not to trust it.
Technical β Run two cheap checks on raw rows before aggregating β catching an anomaly post-aggregation is too late to isolate which row caused it:
- N-standard-deviation outlier. For a numeric column like
line_total, flag any row whereabs(value - mean) > 3 * stdwithin its group (e.g., withincategory, since a $2,000 furniture order and a $2,000 phone-case order mean very different things). Tighten to 2 std for smaller, more homogeneous categories. - Sudden zero. Flag a row where a normally nonzero metric drops to exactly 0 β far more often a broken export or sync failure than genuine zero activity. A single 0-revenue day inside an otherwise active week should raise a flag, not silently pull the weekly average down.
grp = df.groupby("category")["line_total"]
z = (df["line_total"] - grp.transform("mean")) / grp.transform("std")
df["is_outlier"] = z.abs() > 3
df["is_suspicious_zero"] = (df["line_total"] == 0) & (df["units_sold"] > 0)
Route flagged rows to a "needs review" list, and exclude them from headline KPIs with a visible footnote count ("2 rows excluded β see review") rather than silently including or deleting them. Both silent choices erode trust faster than a visible asterisk.
Example β from raw export to KPI table
A trimmed sample of the kind of export this applies to (placeholder data, one brand, one week):
order_id,order_date,sku,category,units_sold,line_total,is_refund
1001,2026-02-02,acme-hoodie-001,apparel,2,59.98,0
1002,2026-02-02,acme-mug-014,home,1,12.00,0
1003,2026-02-03,acme-hoodie-001,apparel,1,0.00,0
1004,2026-02-04,acme-hoodie-001,apparel,1,29.99,0
1005,2026-02-05,acme-mug-014,home,3,36.00,0
1006,2026-02-05,acme-hoodie-001,apparel,1,29.99,1
1007,2026-02-06,acme-tote-007,home,1,18.50,0
Row 1003 is a sudden zero (units_sold = 1 but line_total = 0) β flag it, exclude it from revenue and average_order_value, and route it to review before it drags the week's AOV down.
Resulting weekly KPI table after grouping, excluding the flagged row:
| Week | Revenue | Orders | AOV | Refund rate |
|---|---|---|---|---|
| 2026-W05 | $186.46 | 6 | $31.08 | 16.7% |
That single row is the dashboard: four numbers, each tied to a decision (spend more on ads, staff up fulfillment, adjust bundle pricing, investigate the return) β not a wall of charts built from every column the export happened to include.
Common pitfalls
- Charting every numeric column by default. Run the decision-relevance filter first; most columns fail it.
- Grouping at a fixed granularity regardless of volume. Daily buckets on a 5-orders-a-day store is a noise generator, not a trend line.
- Calling two data points a trend. Require a minimum run length (3 periods) plus a deadband before labeling anything "trending" on the dashboard.
- Aggregating before checking for anomalies. Once a bad row is folded into a
sumormean, it cannot be un-mixed β screen rows first. - Silently dropping or silently including flagged rows. Both erode trust. Show the excluded-row count next to the KPI instead.