Problem
I have orders in a long format — one row per observation with a
category column and a quantity. I want them wide — one row per
region with a tea column and a coffee column. Or the other way:
take a wide table and melt it into a long one.
Setup
val orders = [
{ region = "north", category = "tea", quantity = 12 },
{ region = "south", category = "coffee", quantity = 30 },
{ region = "north", category = "tea", quantity = 8 },
{ region = "west", category = "coffee", quantity = 50 },
{ region = "east", category = "tea", quantity = 6 },
{ region = "south", category = "coffee", quantity = 20 },
{ region = "west", category = "tea", quantity = 14 },
{ region = "west", category = "tea", quantity = 25 }
];
Example
Long to wide — pivot category into separate tea and coffee
columns, summing quantity within each. The trick is that sum over <expr> takes an expression, and the expression can be conditional.
One aggregate per target column:
from ord in orders
group ord.region
compute { tea = sum over (if ord.category = "tea" then ord.quantity else 0),
coffee = sum over (if ord.category = "coffee" then ord.quantity else 0) };
val it =
[{coffee=0,region="north",tea=20},{coffee=50,region="south",tea=0},
{coffee=50,region="west",tea=39},{coffee=0,region="east",tea=6}]
: {coffee:int, region:string, tea:int} list
What's happening
Wide-pivoting in Morel is "one aggregate per target column, with a
predicate inside the aggregate expression." It works because Morel's
sum over e is just sum applied to a derived column — and that
column can be anything the row permits, including a conditional.
Write one name = ... over (if ... then ... else 0) per output
column and you've got a pivot.
The catch is that the output columns are fixed at compile time. You
can't pivot a set of categories you don't know until you run the
query. That's a real constraint — most cookbook-style reshaping knows
the list up front ("tea and coffee"), but a generic pivot_by(col)
isn't something Morel 0.9 offers. When you need a dynamic shape,
emit the long-format result and do the final reshape in whatever
tool will consume it.
Long-to-wide's inverse — melt, unpivot, wide-to-long — is easier.
Morel 0.9's yieldAll step is the direct way to say it: an ordinary
yield returns one row per input row, while yieldAll returns a
list per input row and flattens the lot. One wide row in, one row per
column out.
Variations
Wide to long with yieldAll. Each input row expands to one row per
category:
val wide = [
{ region = "north", tea = 20, coffee = 0 },
{ region = "south", tea = 0, coffee = 50 },
{ region = "west", tea = 39, coffee = 50 }
];
from row in wide
yieldAll [ { region = row.region, category = "tea", quantity = row.tea },
{ region = row.region, category = "coffee", quantity = row.coffee } ];
val it =
[{category="tea",quantity=20,region="north"},
{category="coffee",quantity=0,region="north"},
{category="tea",quantity=0,region="south"},
{category="coffee",quantity=50,region="south"},
{category="tea",quantity=39,region="west"},
{category="coffee",quantity=50,region="west"}]
: {category:string, quantity:int, region:string} list
Before 0.9 you'd write this as a second scan over a list of labels
with a case to pick the column — which works, but needs a dead
_ => 0 arm that yieldAll doesn't:
from row in wide,
cat in ["tea", "coffee"]
yield { row.region, category = cat,
quantity = case cat of
"tea" => row.tea
| "coffee" => row.coffee
| _ => 0 };
See also
- Recipe 10 — Group and aggregate — the engine behind long-to-wide; pivots are group+compute with a trick.
- Recipe 11 — Join two tables — multi-scan pipelines that look a lot like wide-to-long.
- Recipe 12 — Derive columns with pattern matching — the
caseused inside theyieldfor wide-to-long.