Report pages: Executive Overview · Delay Analysis · Seller & Freight
A star schema centred on fact_deliveries, at one row per order, carrying five role-playing date keys — purchase, approved, carrier handoff, delivered and estimated delivery — along with aggregated freight value and pre-computed timing measures. It reuses dim_date, dim_customer, dim_seller and dim_product from the sales model without modifying any of those tables.
An order can legitimately involve more than one seller, or span more than one product category. Forcing a single seller or category onto each order row would misrepresent roughly 1.3% of orders and undercount category-level performance. Two lightweight bridge tables — bridge_order_seller and bridge_order_product — let both relationships resolve as two clean many-to-one hops rather than one uncontrolled many-to-many.
dim_order from the sales model already existed at one-row-per-order grain, so adding delivery columns there was tempting. fact_deliveries was built as a fully separate table instead, so nothing here could alter a table the published sales dashboard depends on. The same principle applied upstream: rather than updating stg_orders in place, it was duplicated to stg_orders_logistics before cleaning, leaving the earlier pipeline untouched.
1900-01-01 rather than leaving them null. Rewrote the script to use pd.to_datetime(errors='coerce'), producing true nulls, and separately patched the already-loaded staging table with UPDATE ... SET ... = NULL.order_delivered_customer_date and were never confirmed delivered. Confirmed as expected: largely explained by a visible drop-off between carrier handoff and delivery — 1,182 of the 2,965 — rather than by cancellations.dim_date, built for Part 1, covered only the purchase-date range and did not extend far enough to cover order_estimated_delivery_date, which projects up to 26 days past the last purchase date. Extended dim_date with the missing range only, touching no existing rows.fact_deliveries. Changed both to bidirectional cross-filtering.is_late blank, meaning the order was never confirmed delivered — rather than relying on DAX's default blank handling. An early version of the on-time rate measure that did not do this returned 95% instead of the correct 91.9%: a three-point error that would have overstated performance on a published dashboard.fact_deliveries relates to dim_date five separate times. Only the purchase-date relationship is active by default, matching the sales model's convention; the other four are invoked explicitly with USERELATIONSHIP() wherever a measure needs to pivot onto a different date stage.