Skip to content
Search

Pivot

Back

Pivot operations transform data from a long format to a wide format by rotating column values into column headers. This is commonly used for creating cross-tabular reports and summary tables.

Basic Pivot Operation

Pivot is part of the grouping specification: it is declared between groupBy() and aggregate() and requires exactly one aggregation function.

The pivot columns are declared, not discovered. pivot() takes the values it will turn into columns, so the plan can name them before a row is read. Pass them with pivot_values(...), or let discover_pivot_values() read the column once at build time and turn what it finds into the same declared list.

<?php

use function Flow\ETL\DSL\{data_frame, from_array, col, pivot_values, sum, to_output};

$salesData = data_frame()
    ->read(from_array([
        ['region' => 'North', 'product' => 'Laptop', 'month' => 'Jan', 'sales' => 1000],
        ['region' => 'North', 'product' => 'Laptop', 'month' => 'Feb', 'sales' => 1200],
        ['region' => 'North', 'product' => 'Phone', 'month' => 'Jan', 'sales' => 800],
        ['region' => 'North', 'product' => 'Phone', 'month' => 'Feb', 'sales' => 900],
        ['region' => 'South', 'product' => 'Laptop', 'month' => 'Jan', 'sales' => 1100],
        ['region' => 'South', 'product' => 'Laptop', 'month' => 'Feb', 'sales' => 1300],
        ['region' => 'South', 'product' => 'Phone', 'month' => 'Jan', 'sales' => 700],
        ['region' => 'South', 'product' => 'Phone', 'month' => 'Feb', 'sales' => 850],
    ]))
    ->groupBy(['region', 'product'])
    ->pivot(col('product'), pivot_values('Laptop', 'Phone')) // creates the 'Laptop' and 'Phone' columns
    ->aggregate(sum(col('sales')->as('total_sales')))
    ->write(to_output())
    ->run();

Result Structure:

| region | Laptop | Phone |
|--------|--------|-------|
| North  | 2200   | 1700  |
| South  | 2400   | 1550  |

Monthly Sales Pivot

Create a pivot table showing sales by month:

<?php

use function Flow\ETL\DSL\{data_frame, from_array, col, discover_pivot_values, sum, average, to_output};

$monthlySales = data_frame()
    ->read(from_array([
        ['region' => 'North', 'month' => 'Jan', 'sales' => 5000],
        ['region' => 'North', 'month' => 'Feb', 'sales' => 5500],
        ['region' => 'North', 'month' => 'Mar', 'sales' => 6000],
        ['region' => 'South', 'month' => 'Jan', 'sales' => 4500],
        ['region' => 'South', 'month' => 'Feb', 'sales' => 4800],
        ['region' => 'South', 'month' => 'Mar', 'sales' => 5200],
    ]))
    ->groupBy(['region'])
    ->pivot(col('month'), discover_pivot_values()) // reads the month column once at build time
    ->aggregate(average(col('sales')->as('avg_sales')))
    ->write(to_output())
    ->run();

Result:

| region | Jan  | Feb  | Mar  |
|--------|------|------|------|
| North  | 5000 | 5500 | 6000 |
| South  | 4500 | 4800 | 5200 |

Declaring versus discovering the values

pivot_values('Laptop', 'Phone') is the cheap form: nothing is read to work out the columns, a value the data never carries still produces a column of nulls, and a value the data carries but you did not declare produces no column at all.

discover_pivot_values() scans the pivot column once, before the plan runs, and resolves to the declared form. It scans the frame as it stands at the pivot() call, not the bare source - so the pivot column may be one an earlier withEntry() produced, and an earlier filter() narrows which values exist:

data_frame()
    ->read(from_array($rows))
    ->filter(col('region')->equals(lit('North')))   // only North's months are discovered
    ->groupBy(['region'])
    ->pivot(col('month'), discover_pivot_values())
    ->aggregate(sum(col('sales')));

The scan is DISTINCT, skips nulls, sorts, and is bounded - discover_pivot_values(50) refuses a column with more than 50 distinct values. Because it reads the frame before the real run, it needs a source it can read twice, and refuses one it cannot:

Flow\ETL\Extractor\DataFrameExtractor cannot read its dataset twice, so describing it would consume the
rows before they are extracted. Pass an array, or declare the schema with ->withSchema().

Found a typo or an outdated section? Edit this page on GitHub


Contributors

Built in the open.

Join us on GitHub
scroll back to top