Combine(groupby())

Is there a way to do this:

c = combine(groupby(df, :A),:B => mean => :B_mean)

but in such a way that the output does not only show :A and :B_mean but also the other variables that df had (well, at least the variables that can also be combined due to repetition)?

DataFrames can’t know your intent beyond what you program. It can’t intuit which variables are invariant withing an β€œA” grouping unless you program that.

You could write:

vars_sub = Symbol.(setdiff(names(df), ["B", "A"]) )
c = combine(groupby(df, :A), :B => mean => :B_mean, (vars_sub .=> first )...) 

You can just add all the variables that are constant within groups as extra grouping variables.

julia> using Statistics, StatsBase

julia> df = DataFrame(A = [1, 1, 2, 2], B = [1, 2, 300, 400], C = [5, 5, 6, 6], D = [10, 30, 60, 80])
4Γ—4 DataFrame
 Row β”‚ A      B      C      D     
     β”‚ Int64  Int64  Int64  Int64 
─────┼────────────────────────────
   1 β”‚     1      1      5     10
   2 β”‚     1      2      5     30
   3 β”‚     2    300      6     60
   4 β”‚     2    400      6     80

julia> function only_constant(x)
           all(==(first(x)), x) || throw(ArgumentError("All elements of the vector must be equal"))
           return first(x)
       end;

julia> @chain df begin
           @by :A begin
               :B = mean(:B)
               $([:C] .=> only_constant)
           end
       end
2Γ—3 DataFrame
 Row β”‚ A      B        C_only_constant 
     β”‚ Int64  Float64  Int64           
─────┼─────────────────────────────────
   1 β”‚     1      1.5                5
   2 β”‚     2    350.0                6

Thanks for the great answers. I wonder whether there could be a shortcut for this. It’s probably not so rare that you have not only multiple factors and levels, but also multiple observations per subject and you want to group by ID and e.g. average some result.

A variant of @croberts’ code:

gdf = groupby(df, :A)
combine(gdf, :B => mean => :B_mean, Not([:A, :B]) .=> first)

@rafael.guerra ’s solution is the easiest to parse for newbies, but still bit hard to remember.

In plain English, it reads: For each column in the grouped dataframe (gdf), excluding the grouping variable (:A) and the aggregated column (:B), take the first value within each group.

It could help to avoid redundancy in tabular data. In this case, you would have two datasets: one for person-level observations, and one for person-time -level observations. If you want to bring along more variables from the person-level table to the summary table, you join by ID. Example:

julia> using DataFrames, Chain, Statistics

julia> person_df = DataFrame(id = [1,2,3], A = [1,0,0])
3Γ—2 DataFrame
 Row β”‚ id     A     
     β”‚ Int64  Int64 
─────┼──────────────
   1 β”‚     1      1
   2 β”‚     2      0
   3 β”‚     3      0

julia> person_time_df = DataFrame(id = [1,1,2,2,3,3], time = [1,2,1,2,1,2], B = [0,3,0,2,1,4])
6Γ—3 DataFrame
 Row β”‚ id     time   B     
     β”‚ Int64  Int64  Int64 
─────┼─────────────────────
   1 β”‚     1      1      0
   2 β”‚     1      2      3
   3 β”‚     2      1      0
   4 β”‚     2      2      2
   5 β”‚     3      1      1
   6 β”‚     3      2      4

julia> mean_B_by_id = @chain person_time_df begin
           groupby(:id)
           combine(:B => mean => :mean_B)
       end
3Γ—2 DataFrame
 Row β”‚ id     mean_B  
     β”‚ Int64  Float64 
─────┼────────────────
   1 β”‚     1      1.5
   2 β”‚     2      1.0
   3 β”‚     3      2.5

julia> result = leftjoin(mean_B_by_id, person_df, on = :id)
3Γ—3 DataFrame
 Row β”‚ id     mean_B   A      
     β”‚ Int64  Float64  Int64? 
─────┼────────────────────────
   1 β”‚     1      1.5       1
   2 β”‚     2      1.0       0
   3 β”‚     3      2.5       0