Multi-tier spanners
The following example shows how you can create layouts with several tiers of column spanners.
We will use the PalmerPenguins data.
using DataFrames, Chain, StyledTables
using Statistics: mean
df = DataFrame(StyledTables.penguins())
describe(df)| Row | variable | mean | min | median | max | nmissing | eltype |
|---|---|---|---|---|---|---|---|
| Symbol | Union… | Any | Union… | Any | Int64 | DataType | |
| 1 | species | Adelie | Gentoo | 0 | String | ||
| 2 | island | Biscoe | Torgersen | 0 | String | ||
| 3 | bill_length_mm | 43.9928 | 32.1 | 44.5 | 59.6 | 0 | Float64 |
| 4 | bill_depth_mm | 17.1649 | 13.1 | 17.3 | 21.5 | 0 | Float64 |
| 5 | flipper_length_mm | 200.967 | 172 | 197.0 | 231 | 0 | Int64 |
| 6 | body_mass_g | 4207.06 | 2700 | 4050.0 | 6300 | 0 | Int64 |
| 7 | sex | female | male | 0 | String |
Our goal is to summarise the data by island, species, and sex.
bill_cols = [:bill_length_mm, :bill_depth_mm]
number_cols = [string.(bill_cols)..., "flipper_length_mm"] # , "body_mass_g"
male_ordered = ["male_" * colname for colname in number_cols]
female_ordered = ["female_" * colname for colname in number_cols]
summary = @chain df begin
select(_, :island, :species, :sex, number_cols...)
dropmissing(_)
groupby(_, [:island, :species, :sex])
combine(_, Cols(r"_mm$|_g$") .=> mean => identity)
stack(_, number_cols)
transform(_, [:sex, :variable] => ByRow((s, v) -> join([s, v], "_")) => :sex_variable)
select(_, Not(:sex, :variable))
unstack(_, :sex_variable, :value)
transform(_, :island => ByRow(x -> "$x Is.") => identity)
select(_, :island, :species, male_ordered..., female_ordered...)
sort(_, :island)
end| Row | island | species | male_bill_length_mm | male_bill_depth_mm | male_flipper_length_mm | female_bill_length_mm | female_bill_depth_mm | female_flipper_length_mm |
|---|---|---|---|---|---|---|---|---|
| String | String | Float64? | Float64? | Float64? | Float64? | Float64? | Float64? | |
| 1 | Biscoe Is. | Adelie | 40.5909 | 19.0364 | 190.409 | 37.3591 | 17.7045 | 187.182 |
| 2 | Biscoe Is. | Gentoo | 49.4738 | 15.718 | 221.541 | 45.5638 | 14.2379 | 212.707 |
| 3 | Dream Is. | Adelie | 40.0714 | 18.8393 | 191.929 | 36.9111 | 17.6185 | 187.852 |
| 4 | Dream Is. | Chinstrap | 51.0941 | 19.2529 | 199.912 | 46.5735 | 17.5882 | 191.735 |
| 5 | Torgersen Is. | Adelie | 40.587 | 19.3913 | 194.913 | 37.5542 | 17.55 | 188.292 |
The table we want to create will feature island as the row group, and the species present on each island are listed in each group. As some of the length measurements we summarised describe the bill, we will group these as a column spanner. Finally, a higher-order column spanner will indicate which measurements are from female and which from male penguins.
Step 1: Row groups
We begin by setting row groups and hiding the grouping column:
tbl = StyledTable(summary)
rowgroup!(tbl, :island)
hide!(tbl, :island)
render(tbl)| species | male_bill_length_mm | male_bill_depth_mm | male_flipper_length_mm | female_bill_length_mm | female_bill_depth_mm | female_flipper_length_mm |
| Biscoe Is. | ||||||
| Adelie | 40.6 | 19 | 190 | 37.4 | 17.7 | 187 |
| Gentoo | 49.5 | 15.7 | 222 | 45.6 | 14.2 | 213 |
| Dream Is. | ||||||
| Adelie | 40.1 | 18.8 | 192 | 36.9 | 17.6 | 188 |
| Chinstrap | 51.1 | 19.3 | 200 | 46.6 | 17.6 | 192 |
| Torgersen Is. | ||||||
| Adelie | 40.6 | 19.4 | 195 | 37.6 | 17.6 | 188 |
Step 2: Level one spanner
Next, we add the level one spanner, grouping all columns with bill measures together:
spanner!(tbl, "male_" .* string.(bill_cols) => "Bill measures")
spanner!(tbl, "female_" .* string.(bill_cols) => "Bill measures")
render(tbl)| Bill measures | Bill measures | |||||
| species | male_bill_length_mm | male_bill_depth_mm | male_flipper_length_mm | female_bill_length_mm | female_bill_depth_mm | female_flipper_length_mm |
| Biscoe Is. | ||||||
| Adelie | 40.6 | 19 | 190 | 37.4 | 17.7 | 187 |
| Gentoo | 49.5 | 15.7 | 222 | 45.6 | 14.2 | 213 |
| Dream Is. | ||||||
| Adelie | 40.1 | 18.8 | 192 | 36.9 | 17.6 | 188 |
| Chinstrap | 51.1 | 19.3 | 200 | 46.6 | 17.6 | 192 |
| Torgersen Is. | ||||||
| Adelie | 40.6 | 19.4 | 195 | 37.6 | 17.6 | 188 |
Step 3: Level two spanner
On top of the level one spanner, we'll add a spanner grouping columns of males and females together:
spanner!(tbl, male_ordered => "Male"; level = 2)
spanner!(tbl, female_ordered => "Female"; level = 2)
render(tbl)| Male | Female | |||||
| Bill measures | Bill measures | |||||
| species | male_bill_length_mm | male_bill_depth_mm | male_flipper_length_mm | female_bill_length_mm | female_bill_depth_mm | female_flipper_length_mm |
| Biscoe Is. | ||||||
| Adelie | 40.6 | 19 | 190 | 37.4 | 17.7 | 187 |
| Gentoo | 49.5 | 15.7 | 222 | 45.6 | 14.2 | 213 |
| Dream Is. | ||||||
| Adelie | 40.1 | 18.8 | 192 | 36.9 | 17.6 | 188 |
| Chinstrap | 51.1 | 19.3 | 200 | 46.6 | 17.6 | 192 |
| Torgersen Is. | ||||||
| Adelie | 40.6 | 19.4 | 195 | 37.6 | 17.6 | 188 |
Step 4: Add column labels
Now we can simplify the names of the columns because the information about bill measures and sex is contained in our column spanners:
label_dict = Dict(
:species => "Species",
:male_bill_length_mm => "Length",
:male_bill_depth_mm => "Depth",
:male_flipper_length_mm => "Flipper length",
:female_bill_length_mm => "Length",
:female_bill_depth_mm => "Depth",
:female_flipper_length_mm => "Flipper length",
)
relabel!(tbl, label_dict)
format!(tbl, IntegerFormatter([male_ordered..., female_ordered...]))
render(tbl)| Male | Female | |||||
| Bill measures | Bill measures | |||||
| Species | Length | Depth | Flipper length | Length | Depth | Flipper length |
| Biscoe Is. | ||||||
| Adelie | 41 | 19 | 190 | 37 | 18 | 187 |
| Gentoo | 49 | 16 | 222 | 46 | 14 | 213 |
| Dream Is. | ||||||
| Adelie | 40 | 19 | 192 | 37 | 18 | 188 |
| Chinstrap | 51 | 19 | 200 | 47 | 18 | 192 |
| Torgersen Is. | ||||||
| Adelie | 41 | 19 | 195 | 38 | 18 | 188 |