Transformation to DataFrames#

Split-apply-combine

using DataFrames

Grouping a data frame#

groupby

x = DataFrame(id=[1, 2, 3, 4, 1, 2, 3, 4], id2=[1, 2, 1, 2, 1, 2, 1, 2], v=rand(8))
8×3 DataFrame
Rowidid2v
Int64Int64Float64
1110.452854
2220.604998
3310.589787
4420.224989
5110.259274
6220.663381
7310.637711
8420.0547333
groupby(x, :id)

GroupedDataFrame with 4 groups based on key: id

First Group (2 rows): id = 1
Rowidid2v
Int64Int64Float64
1110.452854
2110.259274

Last Group (2 rows): id = 4
Rowidid2v
Int64Int64Float64
1420.224989
2420.0547333
groupby(x, [])

GroupedDataFrame with 1 group based on key:

First Group (8 rows):
Rowidid2v
Int64Int64Float64
1110.452854
2220.604998
3310.589787
4420.224989
5110.259274
6220.663381
7310.637711
8420.0547333
gx2 = groupby(x, [:id, :id2])

GroupedDataFrame with 4 groups based on keys: id, id2

First Group (2 rows): id = 1, id2 = 1
Rowidid2v
Int64Int64Float64
1110.452854
2110.259274

Last Group (2 rows): id = 4, id2 = 2
Rowidid2v
Int64Int64Float64
1420.224989
2420.0547333

get the parent DataFrame

parent(gx2)
8×3 DataFrame
Rowidid2v
Int64Int64Float64
1110.452854
2220.604998
3310.589787
4420.224989
5110.259274
6220.663381
7310.637711
8420.0547333

back to the DataFrame, but in a different order of rows than the original

vcat(gx2...)
8×3 DataFrame
Rowidid2v
Int64Int64Float64
1110.452854
2110.259274
3220.604998
4220.663381
5310.589787
6310.637711
7420.224989
8420.0547333

the same as above

DataFrame(gx2)
8×3 DataFrame
Rowidid2v
Int64Int64Float64
1110.452854
2110.259274
3220.604998
4220.663381
5310.589787
6310.637711
7420.224989
8420.0547333

drop grouping columns when creating a data frame

DataFrame(gx2, keepkeys=false)
8×1 DataFrame
Rowv
Float64
10.452854
20.259274
30.604998
40.663381
50.589787
60.637711
70.224989
80.0547333

vector of names of grouping variables

groupcols(gx2)
2-element Vector{Symbol}:
 :id
 :id2

and non-grouping variables

valuecols(gx2)
1-element Vector{Symbol}:
 :v

group indices in parent(gx2)

groupindices(gx2)
8-element Vector{Union{Missing, Int64}}:
 1
 2
 3
 4
 1
 2
 3
 4
kgx2 = keys(gx2)
4-element DataFrames.GroupKeys{DataFrames.GroupedDataFrame{DataFrames.DataFrame}}:
 GroupKey: (id = 1, id2 = 1)
 GroupKey: (id = 2, id2 = 2)
 GroupKey: (id = 3, id2 = 1)
 GroupKey: (id = 4, id2 = 2)

You can index into a GroupedDataFrame like to a vector or to a dictionary. The second form accepts GroupKey, NamedTuple or a Tuple.

gx2

GroupedDataFrame with 4 groups based on keys: id, id2

First Group (2 rows): id = 1, id2 = 1
Rowidid2v
Int64Int64Float64
1110.452854
2110.259274

Last Group (2 rows): id = 4, id2 = 2
Rowidid2v
Int64Int64Float64
1420.224989
2420.0547333
k = keys(gx2)[1]
GroupKey: (id = 1, id2 = 1)
ntk = NamedTuple(k)
(id = 1, id2 = 1)
tk = Tuple(k)
(1, 1)

the operations below produce the same result and are proformant

gx2[1], gx2[k], gx2[ntk], gx2[tk]
(2×3 SubDataFrame
 Row  id     id2    v        
     │ Int64  Int64  Float64  
─────┼────────────────────────
   1 │     1      1  0.452854
   2 │     1      1  0.259274, 2×3 SubDataFrame
 Row  id     id2    v        
     │ Int64  Int64  Float64  
─────┼────────────────────────
   1 │     1      1  0.452854
   2 │     1      1  0.259274, 2×3 SubDataFrame
 Row  id     id2    v        
     │ Int64  Int64  Float64  
─────┼────────────────────────
   1 │     1      1  0.452854
   2 │     1      1  0.259274, 2×3 SubDataFrame
 Row  id     id2    v        
     │ Int64  Int64  Float64  
─────┼────────────────────────
   1 │     1      1  0.452854
   2 │     1      1  0.259274)

handling missing values

x = DataFrame(id=[missing, 5, 1, 3, missing], x=1:5)
5×2 DataFrame
Rowidx
Int64?Int64
1missing1
252
313
434
5missing5

by default groups include missing values and their order is not guaranteed

groupby(x, :id)

GroupedDataFrame with 4 groups based on key: id

First Group (1 row): id = 1
Rowidx
Int64?Int64
113

Last Group (2 rows): id = missing
Rowidx
Int64?Int64
1missing1
2missing5

but we can change it; now they are sorted

groupby(x, :id, sort=true, skipmissing=true)

GroupedDataFrame with 3 groups based on key: id

First Group (1 row): id = 1
Rowidx
Int64?Int64
113

Last Group (1 row): id = 5
Rowidx
Int64?Int64
152

and now they are in the order they appear in the source data frame

groupby(x, :id, sort=false)

GroupedDataFrame with 4 groups based on key: id

First Group (2 rows): id = missing
Rowidx
Int64?Int64
1missing1
2missing5

Last Group (1 row): id = 3
Rowidx
Int64?Int64
134

Performing transformations#

by group using combine, select, select!, transform, and transform!

using Statistics
using Chain

x = DataFrame(id=rand('a':'d', 100), v=rand(100))
100×2 DataFrame
75 rows omitted
Rowidv
CharFloat64
1b0.687815
2a0.907297
3d0.753421
4c0.746023
5d0.184972
6d0.519753
7d0.819507
8c0.171625
9b0.651596
10c0.578907
11b0.457389
12a0.242115
13b0.929259
89c0.0398338
90a0.504607
91a0.0691471
92d0.0866579
93b0.105838
94d0.761518
95d0.418352
96c0.198222
97c0.354353
98c0.605072
99c0.474177
100d0.277162

apply a function to each group of a data frame combine keeps as many rows as are returned from the function

@chain x begin
    groupby(:id)
    combine(:v => mean)
end
4×2 DataFrame
Rowidv_mean
CharFloat64
1b0.502598
2a0.555364
3d0.509115
4c0.52345
x.id2 = axes(x, 1)
Base.OneTo(100)

Select and transform keep as many rows as are in the source data frame and in correct order. Additionally, transform keeps all columns from the source.

@chain x begin
    groupby(:id)
    transform(:v => mean)
end
100×4 DataFrame
75 rows omitted
Rowidvid2v_mean
CharFloat64Int64Float64
1b0.68781510.502598
2a0.90729720.555364
3d0.75342130.509115
4c0.74602340.52345
5d0.18497250.509115
6d0.51975360.509115
7d0.81950770.509115
8c0.17162580.52345
9b0.65159690.502598
10c0.578907100.52345
11b0.457389110.502598
12a0.242115120.555364
13b0.929259130.502598
89c0.0398338890.52345
90a0.504607900.555364
91a0.0691471910.555364
92d0.0866579920.509115
93b0.105838930.502598
94d0.761518940.509115
95d0.418352950.509115
96c0.198222960.52345
97c0.354353970.52345
98c0.605072980.52345
99c0.474177990.52345
100d0.2771621000.509115

note that combine reorders rows by group of GroupedDataFrame

@chain x begin
    groupby(:id)
    combine(:id2, :v => mean)
end
100×3 DataFrame
75 rows omitted
Rowidid2v_mean
CharInt64Float64
1b10.502598
2b90.502598
3b110.502598
4b130.502598
5b140.502598
6b290.502598
7b310.502598
8b370.502598
9b380.502598
10b390.502598
11b470.502598
12b530.502598
13b580.502598
89c650.52345
90c700.52345
91c740.52345
92c760.52345
93c770.52345
94c850.52345
95c880.52345
96c890.52345
97c960.52345
98c970.52345
99c980.52345
100c990.52345

we give a custom name for the result column

@chain x begin
    groupby(:id)
    combine(:v => mean => :res)
end
4×2 DataFrame
Rowidres
CharFloat64
1b0.502598
2a0.555364
3d0.509115
4c0.52345

you can have multiple operations

@chain x begin
    groupby(:id)
    combine(:v => mean => :res1, :v => sum => :res2, nrow => :n)
end
4×4 DataFrame
Rowidres1res2n
CharFloat64Float64Int64
1b0.5025989.5493619
2a0.55536412.773423
3d0.50911514.255228
4c0.5234515.703530

Additional notes:

  • select! and transform! perform operations in-place

  • The general syntax for transformation is source_columns => function => target_column

  • if you pass multiple columns to a function they are treated as positional arguments

  • ByRow and AsTable work exactly like discussed for operations on data frames in 05_columns.ipynb

  • you can automatically groupby again the result of combine, select etc. by passing ungroup=false keyword argument to them

  • similarly keepkeys keyword argument allows you to drop grouping columns from the resulting data frame

It is also allowed to pass a function to all these functions (also - as a special case, as a first argument). In this case the return value can be a table. In particular it allows for an easy dropping of groups if you return an empty table from the function.

If you pass a function you can use a do block syntax. In case of passing a function it gets a SubDataFrame as its argument.

Here is an example:

combine(groupby(x, :id)) do sdf
    n = nrow(sdf)
    n < 25 ? DataFrame() : DataFrame(n=n) ## drop groups with low number of rows
end
2×2 DataFrame
Rowidn
CharInt64
1d28
2c30

You can also produce multiple columns in a single operation:

df = DataFrame(id=[1, 1, 2, 2], val=[1, 2, 3, 4])
4×2 DataFrame
Rowidval
Int64Int64
111
212
323
424
@chain df begin
    groupby(:id)
    combine(:val => (x -> [x]) => AsTable)
end
2×3 DataFrame
Rowidx1x2
Int64Int64Int64
1112
2234
@chain df begin
    groupby(:id)
    combine(:val => (x -> [x]) => [:c1, :c2])
end
2×3 DataFrame
Rowidc1c2
Int64Int64Int64
1112
2234

It is easy to unnest the column into multiple columns,

df = DataFrame(a=[(p=1, q=2), (p=3, q=4)])
select(df, :a => AsTable)
2×2 DataFrame
Rowpq
Int64Int64
112
234

automatic column names generated

df = DataFrame(a=[[1, 2], [3, 4]])
select(df, :a => AsTable)
2×2 DataFrame
Rowx1x2
Int64Int64
112
234

custom column names generated

select(df, :a => [:C1, :C2])
2×2 DataFrame
RowC1C2
Int64Int64
112
234

Finally, observe that one can conveniently apply multiple transformations using broadcasting:

df = DataFrame(id=repeat(1:10, 10), x1=1:100, x2=101:200)
100×3 DataFrame
75 rows omitted
Rowidx1x2
Int64Int64Int64
111101
222102
333103
444104
555105
666106
777107
888108
999109
101010110
11111111
12212112
13313113
89989189
901090190
91191191
92292192
93393193
94494194
95595195
96696196
97797197
98898198
99999199
10010100200
@chain df begin
    groupby(:id)
    combine([:x1, :x2] .=> minimum)
end
10×3 DataFrame
Rowidx1_minimumx2_minimum
Int64Int64Int64
111101
222102
333103
444104
555105
666106
777107
888108
999109
101010110
@chain df begin
    groupby(:id)
    combine([:x1, :x2] .=> [minimum maximum])
end
10×5 DataFrame
Rowidx1_minimumx2_minimumx1_maximumx2_maximum
Int64Int64Int64Int64Int64
11110191191
22210292192
33310393193
44410494194
55510595195
66610696196
77710797197
88810898198
99910999199
101010110100200

Aggregation of a data frame using mapcols#

x = DataFrame(rand(10, 10), :auto)
10×10 DataFrame
Rowx1x2x3x4x5x6x7x8x9x10
Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64
10.1066170.2911680.16740.7753440.9439250.1667960.4909570.538460.4632520.741993
20.6622030.1303010.9922430.1384320.2810770.4386270.7574780.9799520.5088830.573489
30.1364190.8353840.2518230.3187470.9561280.4389750.4394690.01386960.1913990.216512
40.5754290.744410.7988610.600230.749010.6774740.2597280.4371180.6753920.50832
50.9488110.1666460.2011580.7051860.6816650.896160.8238250.6950290.30910.985866
60.5600980.8377520.03907270.543360.1878870.597320.499070.5159120.5669150.335263
70.1455220.5961910.04466290.7651270.8523650.9966520.3516310.8650930.3775560.121889
80.14560.2455670.4414240.5854070.5724430.903180.5571730.5615930.8184710.384634
90.901920.7189530.04711030.03320920.1521480.6966490.2699120.3793090.3731110.121797
100.7612040.3325110.8456870.7924850.2027570.1314070.7379860.03985690.3112860.502011
mapcols(mean, x)
1×10 DataFrame
Rowx1x2x3x4x5x6x7x8x9x10
Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64
10.4943820.4898880.3829440.5257530.557940.5943240.5187230.5026190.4595370.449177

Mapping rows and columns using eachcol and eachrow#

map a function over each column and return a vector

map(mean, eachcol(x))
10-element Vector{Float64}:
 0.4943824664189296
 0.48988820236298974
 0.38294400298210396
 0.5257527517021974
 0.5579404589540388
 0.594324121350188
 0.5187228558595499
 0.5026192006128546
 0.45953654364177865
 0.4491773322836904

an iteration returns a Pair with column name and values

foreach(c -> println(c[1], ": ", mean(c[2])), pairs(eachcol(x)))
x1: 0.4943824664189296
x2: 0.48988820236298974
x3: 0.38294400298210396
x4: 0.5257527517021974
x5: 0.5579404589540388
x6: 0.594324121350188
x7: 0.5187228558595499
x8: 0.5026192006128546
x9: 0.45953654364177865
x10: 0.4491773322836904

now the returned value is DataFrameRow which works as a NamedTuple but is a view to a parent DataFrame

map(r -> r.x1 / r.x2, eachrow(x))
10-element Vector{Float64}:
 0.36617154763225235
 5.082108318350985
 0.16330072246858027
 0.7730008652075995
 5.693568306175609
 0.6685728829468034
 0.2440863067533868
 0.5929160989190951
 1.2544915085157406
 2.2892576622142773

it prints like a data frame, only the caption is different so that you know the type of the object

er = eachrow(x)
er.x1 ## you can access columns of a parent data frame directly
10-element Vector{Float64}:
 0.10661731577051836
 0.6622031498542199
 0.1364187656650997
 0.5754294556458643
 0.9488110585459724
 0.5600981459786365
 0.14552211973238283
 0.14560044116986715
 0.9019200455048797
 0.7612041663218551

it prints like a data frame, only the caption is different so that you know the type of the object

ec = eachcol(x)
10×10 DataFrameColumns
Rowx1x2x3x4x5x6x7x8x9x10
Float64Float64Float64Float64Float64Float64Float64Float64Float64Float64
10.1066170.2911680.16740.7753440.9439250.1667960.4909570.538460.4632520.741993
20.6622030.1303010.9922430.1384320.2810770.4386270.7574780.9799520.5088830.573489
30.1364190.8353840.2518230.3187470.9561280.4389750.4394690.01386960.1913990.216512
40.5754290.744410.7988610.600230.749010.6774740.2597280.4371180.6753920.50832
50.9488110.1666460.2011580.7051860.6816650.896160.8238250.6950290.30910.985866
60.5600980.8377520.03907270.543360.1878870.597320.499070.5159120.5669150.335263
70.1455220.5961910.04466290.7651270.8523650.9966520.3516310.8650930.3775560.121889
80.14560.2455670.4414240.5854070.5724430.903180.5571730.5615930.8184710.384634
90.901920.7189530.04711030.03320920.1521480.6966490.2699120.3793090.3731110.121797
100.7612040.3325110.8456870.7924850.2027570.1314070.7379860.03985690.3112860.502011

you can access columns of a parent data frame directly

ec.x1
10-element Vector{Float64}:
 0.10661731577051836
 0.6622031498542199
 0.1364187656650997
 0.5754294556458643
 0.9488110585459724
 0.5600981459786365
 0.14552211973238283
 0.14560044116986715
 0.9019200455048797
 0.7612041663218551

Transposing#

you can transpose a data frame using permutedims:

df = DataFrame(reshape(1:12, 3, 4), :auto)
3×4 DataFrame
Rowx1x2x3x4
Int64Int64Int64Int64
114710
225811
336912
df.names = ["a", "b", "c"]
3-element Vector{String}:
 "a"
 "b"
 "c"
permutedims(df, :names)
4×4 DataFrame
Rownamesabc
StringInt64Int64Int64
1x1123
2x2456
3x3789
4x4101112

This notebook was generated using Literate.jl.