Документация Engee

Стратегия «Разделение-применение-объединение» (split-apply-combine)

Разработка поддержки разделения-применения-объединения

Многие задачи анализа данных состоят из трех этапов:

  1. разделение набора данных на группы;

  2. применение некоторых функций к каждой группе;

  3. объединение результатов.

Обратите внимание, что этапы 1 и 3 этой общей процедуры можно опустить. В этом случае мы просто преобразуем фрейм данных без группировки, а затем объединяем результат.

Стандартизированная платформа для работы с такого рода вычислениями описана в статье The Split-Apply-Combine Strategy for Data Analysis (Стратегия «Разделение-применение-объединение» для анализа данных), автором которой является Хэдли Викхэм (Hadley Wickham).

Пакет DataFrames поддерживает стратегию разделения-применения-объединения с помощью функции groupby, которая создает GroupedDataFrame, а затем выполняются combine, select/select! или transform/transform!.

Все операции, описанные в этом разделе руководства, поддерживаются как для AbstractDataFrame (когда шаги разделения и объединения пропускаются), так и для GroupedDataFrame. Технически AbstractDataFrame считается сгруппированным без столбцов (это означает, что у него есть одна группа или ни одной, если он пуст). Единственное отличие заключается в том, что в этом случае именованные аргументы keepkeys и ungroup (описанные ниже) не поддерживаются, и всегда возвращается фрейм данных, поскольку в этом случае нет шагов разделения и объединения.

Чтобы выполнять операции с группами, сначала нужно создать объект GroupedDataFrame из фрейма данных с помощью функции groupby, которая принимает два аргумента: (1) фрейм данных, который нужно сгруппировать, и (2) набор столбцов, по которым осуществляется группировка.

Затем можно применить операции к каждой группе с помощью одной из следующих функций.

  • combine: не накладывает ограничений на количество возвращаемых строк в каждой группе; возвращаемые значения конкатенируются по вертикали в соответствии с порядком групп в GroupedDataFrame; обычно используется для вычисления сводной статистики по группам; для GroupedDataFrame, если группирующие столбцы сохраняются, они помещаются первыми в результат;

  • select: возвращает фрейм данных с количеством и порядком строк, точно таким же, как в исходном фрейме данных, включая только новые вычисленные столбцы; select! является версией на месте для select;

  • transform: возвращает фрейм данных с количеством и порядком строк, точно таким же, как в исходном фрейме данных, включая только новые вычисленные столбцы; transform! является версией на месте для transform; существующие столбцы в исходном фрейме данных помещаются первыми в результат.

Особый случай: если передается GroupedDataFrame без групп, результат операции определяется путем выполнения одного вызова функции преобразования с переданным ей аргументом с нулем строк. Вывод этой операции используется только для определения количества и типа созданных столбцов, но результат не содержит строк.

Все эти функции принимают спецификацию одной или нескольких функций для применения к каждому подмножеству DataFrame. Эта спецификация может иметь следующий вид:

  1. Стандартные селекторы столбцов (целые числа, символы (Symbol), строки, векторы целых чисел, векторы символов (Symbol), векторы строк, All, Cols, :, Between, Not и регулярные выражения)

  2. Пара cols => function, указывающая, что функцию (function) следует вызывать с позиционными аргументами, содержащими столбцы cols, которые могут быть любым допустимым селектором столбцов. В этом случае имя целевого столбца генерируется автоматически, и предполагается, что функция (function) возвращает единичное значение или вектор. Сгенерированное имя создается путем конкатенации имени исходного столбца и имени функции (function) по умолчанию (см. примеры ниже).

  3. Форма cols => function => target_cols, дополнительно явно указывающая целевой столбец или столбцы, которые должны быть одним именем (как Symbol или строка), вектор имен или AsTable. Кроме того, это может быть функция (Function), которая принимает в качестве аргумента строку или вектор строк, содержащие имена столбцов, выбранные с помощью cols, и возвращает имена целевых столбцов (допускаются все принятые типы, кроме AsTable).

  4. Пара col => target_cols, которая переименовывает столбец col в target_cols, который должен быть одним именем (как Symbol или строка), вектором имен или AsTable.

  5. Независимые от столбцов операции function => target_cols или просто function для конкретных функций (function), где входные столбцы опущены; без target_cols новый столбец имеет то же имя, что и function, в противном случае должен быть одним именем (как Symbol или строка). Поддерживаются следующие функции (function).

    • nrow для эффективного вычисления количества строк в каждой группе.

    • proprow для эффективного вычисления доли строк в каждой группе.

    • eachindex для возврата вектора, содержащего номер каждой строки в каждой группе.

    • groupindices для возврата номера группы.

  6. Векторы или матрицы, содержащие преобразования, заданные синтаксисом Pair, описываются в пунктах 2—​5.

  7. Функция, которая будет вызвана с SubDataFrame, соответствующим каждой группе, если обрабатывается GroupedDataFrame, или с самим фреймом данных, если обрабатывается AbstractDataFrame. Эту форму не рекомендуется использовать из-за ее низкой производительности, за исключением случаев, когда количество групп невелико или обрабатывается очень большое количество столбцов (в этом случае SubDataFrame позволяет избежать чрезмерной компиляции).

Примечание. Если передается выражение вида x => y, то, за исключением специального вспомогательного вида nrow => target_cols, оно всегда интерпретируется как cols => function. В частности, следующее выражение function => target_cols не является допустимой спецификацией преобразования.

Примечание. Если cols или target_cols являются All, Cols, Between или Not, поддерживается трансляция с использованием .=>, и она эквивалентна трансляции результата names(df, cols) или names(df, target_cols). Это работает, как будто трансляция произошла после замены селектора выбранными именами столбцов в области фрейма данных.

Все функции имеют два типа сигнатур. Один из них принимает GroupedDataFrame в качестве первого аргумента и произвольное количество преобразований, описанных выше, в качестве последующих аргументов. Второй тип сигнатуры — это когда в качестве первого аргумента передается Function или Type, а в качестве второго — GroupedDataFrame (аналогично map).

Есть особое правило: если при использовании синтаксисов cols => function и cols => function => target_cols cols заключен в объект AsTable, то NamedTuple, содержащий столбцы, выбранные с помощью cols, передается (функции) function.

Результат, который может возвращать функция (function), определяется значением target_cols.

  1. Если cols и target_cols опущены (передается только function), при возврате фрейма данных, матрицы, NamedTuple, Tables.AbstractRow или DataFrameRow будет создано несколько столбцов. При возврате любого другого значения создается один столбец.

  2. Если target_cols является символом (Symbol) или строкой, предполагается, что функция возвратит один столбец. В этом случае при возврате фрейма данных, матрицы, NamedTuple, Tables.AbstractRow или DataFrameRow возникнет ошибка.

  3. Если target_cols является вектором символов (Symbol) или строк или AsTable, предполагается, что функция (function) возвратит несколько столбцов. Если функция (function) возвращает какой-либо элемент из AbstractDataFrame, NamedTuple, DataFrameRow, Tables.AbstractRow, AbstractMatrix, применяются правила, описанные в пункте 1 выше. Если функция (function) возвращает AbstractVector, каждый элемент этого вектора должен поддерживать функцию keys, которая должна возвращать коллекцию Symbol, строк или целых чисел. Возвращаемое значение keys должно быть одинаковым для всех элементов. Затем создается столько столбцов, сколько элементов содержится в возвращаемом значении функции keys. Если target_cols является AsTable, их имена задаются равными именам ключей, за исключением случаев, когда keys возвращает целые числа, тогда они получают префикс x (таким образом, имена столбцов будут выглядеть как x1, x2 и т. д.). Если target_cols является вектором символов (Symbol) или строк, имена столбцов, созданные на основе приведенных выше правил, игнорируются и заменяются target_cols (в этом случае количество столбцов должно совпадать с длиной target_cols). Если fun возвращает значение любого другого типа, предполагается, что это таблица, соответствующая API Tables.jl, и для нее вызывается функция Tables.columntable для получения результирующих столбцов и их имен. Имена сохраняются, когда target_cols является AsTable, и заменяются, если target_cols является вектором символов (Symbol) или строк.

Во всех этих случаях функция (function) может возвращать как одну строку, так и несколько. Как правило, значения, заключенные в Ref или 0-мерный массив AbstractArray, распаковываются и после этого считаются одной строкой.

select/select! и transform/transform! всегда возвращают фрейм данных с количеством и порядком строк, точно таким же, как у исходного (даже если в GroupedDataFrame был изменен порядок групп), за исключением случаев, когда в результирующем фрейме данных нет столбцов (в этом случае результат не содержит строк).

Для combine строки в возвращаемом объекте отображаются в порядке групп в GroupedDataFrame. Функции могут возвращать произвольное количество строк для каждой группы, но тип возвращаемого объекта и количество и имена столбцов должны быть одинаковыми для всех групп, за исключением случаев, когда возвращается DataFrame() или NamedTuple(), тогда заданная группа пропускается.

Допускается смешивать единичные значения и векторы, если запрашивается несколько преобразований. В этом случае такое значение будет повторяться, чтобы соответствовать длине столбцов, указанных в возвращаемых векторах.

По умолчанию (threads=true) для каждого указанного преобразования порождается отдельная задача. Затем каждое преобразование порождает столько задач, сколько потоков доступно в Julia, и разделяет обработку групп между ними (однако в настоящее время преобразования с оптимизированной реализацией, такие как sum, и преобразования, возвращающие несколько строк, используют одну задачу для всех групп). Это позволяет выполнять параллельную работу, если среда Julia была запущена с несколькими потоками. Поэтому передаваемые функции преобразования не должны изменять глобальные переменные (то есть они должны быть чистыми), использовать блокировки для управления параллельным доступом, либо следует передать threads=false, чтобы отключить многопоточность.

Чтобы применить функцию (function) к каждой строке, а не ко всем столбцам, ее можно заключить в структуру ByRow. cols может быть любым синтаксисом индексирования столбцов. Тогда функции (function) будет передан один аргумент для каждого из столбцов, указанных с помощью cols, или NamedTuple, если указанные столбцы заключены в AsTable. Если используется ByRow, cols может выбрать пустой набор столбцов, тогда function вызывается для каждой строки без аргументов, и передается пустой NamedTuple, если в AsTable заключен пустой набор столбцов.

Функции преобразования поддерживают следующие именованные аргументы (не все именованные аргументы поддерживаются во всех случаях; в целом они разрешены в ситуациях, когда имеют смысл; более подробные сведения см. в документации по конкретным функциям).

  • keepkeys: должны ли группирующие столбцы сохраняться в возвращаемом фрейме данных.

  • ungroup: должно ли возвращаемое значение операции быть фреймом данных или GroupedDataFrame.

  • copycols: следует ли копировать столбцы исходного фрейма данных, если к ним не применены преобразования.

  • renamecols: должны ли автоматически генерируемые имена столбцов в форме cols => function включать название функций преобразования или нет.

  • threads: могут ли преобразования осуществляться в отдельных задачах, которые могут выполняться параллельно.

Примеры операций разделения-применения-объединения

Ниже приводится несколько примеров применения этих функций к набору данных iris:

julia> using DataFrames, CSV, Statistics

julia> path = joinpath(pkgdir(DataFrames), "docs", "src", "assets", "iris.csv");

julia> iris = CSV.read(path, DataFrame)
150×5 DataFrame
 Row │ SepalLength  SepalWidth  PetalLength  PetalWidth  Species
     │ Float64      Float64     Float64      Float64     String15
─────┼──────────────────────────────────────────────────────────────────
   1 │         5.1         3.5          1.4         0.2  Iris-setosa
   2 │         4.9         3.0          1.4         0.2  Iris-setosa
   3 │         4.7         3.2          1.3         0.2  Iris-setosa
   4 │         4.6         3.1          1.5         0.2  Iris-setosa
   5 │         5.0         3.6          1.4         0.2  Iris-setosa
   6 │         5.4         3.9          1.7         0.4  Iris-setosa
   7 │         4.6         3.4          1.4         0.3  Iris-setosa
   8 │         5.0         3.4          1.5         0.2  Iris-setosa
  ⋮  │      ⋮           ⋮            ⋮           ⋮             ⋮
 144 │         6.8         3.2          5.9         2.3  Iris-virginica
 145 │         6.7         3.3          5.7         2.5  Iris-virginica
 146 │         6.7         3.0          5.2         2.3  Iris-virginica
 147 │         6.3         2.5          5.0         1.9  Iris-virginica
 148 │         6.5         3.0          5.2         2.0  Iris-virginica
 149 │         6.2         3.4          5.4         2.3  Iris-virginica
 150 │         5.9         3.0          5.1         1.8  Iris-virginica
                                                        135 rows omitted

julia> iris_gdf = groupby(iris, :Species)
GroupedDataFrame with 3 groups based on key: Species
First Group (50 rows): Species = "Iris-setosa"
 Row │ SepalLength  SepalWidth  PetalLength  PetalWidth  Species
     │ Float64      Float64     Float64      Float64     String15
─────┼───────────────────────────────────────────────────────────────
   1 │         5.1         3.5          1.4         0.2  Iris-setosa
   2 │         4.9         3.0          1.4         0.2  Iris-setosa
  ⋮  │      ⋮           ⋮            ⋮           ⋮            ⋮
  49 │         5.3         3.7          1.5         0.2  Iris-setosa
  50 │         5.0         3.3          1.4         0.2  Iris-setosa
                                                      46 rows omitted
⋮
Last Group (50 rows): Species = "Iris-virginica"
 Row │ SepalLength  SepalWidth  PetalLength  PetalWidth  Species
     │ Float64      Float64     Float64      Float64     String15
─────┼──────────────────────────────────────────────────────────────────
   1 │         6.3         3.3          6.0         2.5  Iris-virginica
   2 │         5.8         2.7          5.1         1.9  Iris-virginica
  ⋮  │      ⋮           ⋮            ⋮           ⋮             ⋮
  50 │         5.9         3.0          5.1         1.8  Iris-virginica
                                                         47 rows omitted

julia> combine(iris_gdf, :PetalLength => mean)
3×2 DataFrame
 Row │ Species          PetalLength_mean
     │ String15         Float64
─────┼───────────────────────────────────
   1 │ Iris-setosa                 1.464
   2 │ Iris-versicolor             4.26
   3 │ Iris-virginica              5.552

julia> combine(iris_gdf, nrow, proprow, groupindices)
3×4 DataFrame
 Row │ Species          nrow   proprow   groupindices
     │ String15         Int64  Float64   Int64
─────┼────────────────────────────────────────────────
   1 │ Iris-setosa         50  0.333333             1
   2 │ Iris-versicolor     50  0.333333             2
   3 │ Iris-virginica      50  0.333333             3

julia> combine(iris_gdf, nrow, :PetalLength => mean => :mean)
3×3 DataFrame
 Row │ Species          nrow   mean
     │ String15         Int64  Float64
─────┼─────────────────────────────────
   1 │ Iris-setosa         50    1.464
   2 │ Iris-versicolor     50    4.26
   3 │ Iris-virginica      50    5.552

julia> combine(iris_gdf,
               [:PetalLength, :SepalLength] =>
               ((p, s) -> (a=mean(p)/mean(s), b=sum(p))) =>
               AsTable) # в качестве аргументов передается несколько столбцов
3×3 DataFrame
 Row │ Species          a         b
     │ String15         Float64   Float64
─────┼────────────────────────────────────
   1 │ Iris-setosa      0.292449     73.2
   2 │ Iris-versicolor  0.717655    213.0
   3 │ Iris-virginica   0.842744    277.6

julia> combine(iris_gdf,
               AsTable([:PetalLength, :SepalLength]) =>
               x -> std(x.PetalLength) / std(x.SepalLength)) # передача NamedTuple
3×2 DataFrame
 Row │ Species          PetalLength_SepalLength_function
     │ String15         Float64
─────┼───────────────────────────────────────────────────
   1 │ Iris-setosa                              0.492245
   2 │ Iris-versicolor                          0.910378
   3 │ Iris-virginica                           0.867923

julia> combine(x -> std(x.PetalLength) / std(x.SepalLength), iris_gdf) # передача SubDataFrame
3×2 DataFrame
 Row │ Species          x1
     │ String15         Float64
─────┼───────────────────────────
   1 │ Iris-setosa      0.492245
   2 │ Iris-versicolor  0.910378
   3 │ Iris-virginica   0.867923

julia> combine(iris_gdf, 1:2 => cor, nrow)
3×3 DataFrame
 Row │ Species          SepalLength_SepalWidth_cor  nrow
     │ String15         Float64                     Int64
─────┼────────────────────────────────────────────────────
   1 │ Iris-setosa                        0.74678      50
   2 │ Iris-versicolor                    0.525911     50
   3 │ Iris-virginica                     0.457228     50

julia> combine(iris_gdf, :PetalLength => (x -> [extrema(x)]) => [:min, :max])
3×3 DataFrame
 Row │ Species          min      max
     │ String15         Float64  Float64
─────┼───────────────────────────────────
   1 │ Iris-setosa          1.0      1.9
   2 │ Iris-versicolor      3.0      5.1
   3 │ Iris-virginica       4.5      6.9

Чтобы получить номер строки для каждого наблюдения в каждой группе, используйте функцию eachindex:

julia> combine(iris_gdf, eachindex)
150×2 DataFrame
 Row │ Species         eachindex
     │ String15        Int64
─────┼───────────────────────────
   1 │ Iris-setosa             1
   2 │ Iris-setosa             2
   3 │ Iris-setosa             3
  ⋮  │       ⋮             ⋮
 148 │ Iris-virginica         48
 149 │ Iris-virginica         49
 150 │ Iris-virginica         50
                 144 rows omitted

В отличие от combine, функции select и transform всегда возвращают фрейм данных с тем же количеством и порядком строк, что и в исходном. В приведенном ниже примере возвращаемые значения в столбцах :SepalLength_SepalWidth_cor и :nrow транслируются в соответствии с количеством элементов в каждой группе.

julia> select(iris_gdf, 1:2 => cor)
150×2 DataFrame
 Row │ Species         SepalLength_SepalWidth_cor
     │ String          Float64
─────┼────────────────────────────────────────────
   1 │ Iris-setosa                       0.74678
   2 │ Iris-setosa                       0.74678
   3 │ Iris-setosa                       0.74678
   4 │ Iris-setosa                       0.74678
  ⋮  │       ⋮                     ⋮
 148 │ Iris-virginica                    0.457228
 149 │ Iris-virginica                    0.457228
 150 │ Iris-virginica                    0.457228
                                  143 rows omitted

julia> transform(iris_gdf, :Species => x -> chop.(x, head=5, tail=0))
150×6 DataFrame
 Row │ SepalLength  SepalWidth  PetalLength  PetalWidth  Species         Species_function
     │ Float64      Float64     Float64      Float64     String          SubString…
─────┼────────────────────────────────────────────────────────────────────────────────────
   1 │         5.1         3.5          1.4         0.2  Iris-setosa     setosa
   2 │         4.9         3.0          1.4         0.2  Iris-setosa     setosa
   3 │         4.7         3.2          1.3         0.2  Iris-setosa     setosa
   4 │         4.6         3.1          1.5         0.2  Iris-setosa     setosa
  ⋮  │      ⋮           ⋮            ⋮           ⋮             ⋮                ⋮
 148 │         6.5         3.0          5.2         2.0  Iris-virginica  virginica
 149 │         6.2         3.4          5.4         2.3  Iris-virginica  virginica
 150 │         5.9         3.0          5.1         1.8  Iris-virginica  virginica
                                                                          143 rows omitted

Все функции также поддерживают форму блокировки do. Однако, как отмечалось выше, эта форма работает медленно, поэтому ее следует избегать, когда речь идет о производительности.

julia> combine(iris_gdf) do df
           (m = mean(df.PetalLength), s² = var(df.PetalLength))
       end
3×3 DataFrame
 Row │ Species          m        s²
     │ String15         Float64  Float64
─────┼─────────────────────────────────────
   1 │ Iris-setosa        1.464  0.0301061
   2 │ Iris-versicolor    4.26   0.220816
   3 │ Iris-virginica     5.552  0.304588

Чтобы применить функцию к каждому негруппирующему столбцу GroupedDataFrame, можно написать следующее.

julia> combine(iris_gdf, valuecols(iris_gdf) .=> mean)
3×5 DataFrame
 Row │ Species          SepalLength_mean  SepalWidth_mean  PetalLength_mean  P ⋯
     │ String15         Float64           Float64          Float64           F ⋯
─────┼──────────────────────────────────────────────────────────────────────────
   1 │ Iris-setosa                 5.006            3.418             1.464    ⋯
   2 │ Iris-versicolor             5.936            2.77              4.26
   3 │ Iris-virginica              6.588            2.974             5.552
                                                                1 column omitted

Обратите внимание, что GroupedDataFrame — это представление, поэтому группирующие столбцы его родительского фрейма данных не должны изменяться, а строки не должны добавляться в него или удаляться из него. Если количество или строки родительского элемента изменяются, то при использовании дочернего элемента GroupedDataFrame возникает ошибка:

julia> df = DataFrame(id=1:2)
2×1 DataFrame
 Row │ id
     │ Int64
─────┼───────
   1 │     1
   2 │     2

julia> gd = groupby(df, :id)
GroupedDataFrame with 2 groups based on key: id
First Group (1 row): id = 1
 Row │ id
     │ Int64
─────┼───────
   1 │     1
⋮
Last Group (1 row): id = 2
 Row │ id
     │ Int64
─────┼───────
   1 │     2

julia> push!(df, [3])
3×1 DataFrame
 Row │ id
     │ Int64
─────┼───────
   1 │     1
   2 │     2
   3 │     3

julia> gd[1]
ERROR: AssertionError: The current number of rows in the parent data frame is 3 and it does not match the number of rows it contained when GroupedDataFrame was created which was 2. The number of rows in the parent data frame has likely been changed unintentionally (e.g. using subset!, filter!, deleteat!, push!, or append! functions).

Иногда бывает полезно добавить строки в исходный фрейм данных GroupedDataFrame, не затрагивая строки, используемые для группировки. В таком случае, чтобы избежать ошибки, можно создать сгруппированный фрейм данных, используя представление (view) родительского фрейма данных:

julia> df = DataFrame(id=1:2)
2×1 DataFrame
 Row │ id
     │ Int64
─────┼───────
   1 │     1
   2 │     2

julia> gd = groupby(view(df, :, :), :id)
GroupedDataFrame with 2 groups based on key: id
First Group (1 row): id = 1
 Row │ id
     │ Int64
─────┼───────
   1 │     1
⋮
Last Group (1 row): id = 2
 Row │ id
     │ Int64
─────┼───────
   1 │     2

julia> push!(df, [3])
3×1 DataFrame
 Row │ id
     │ Int64
─────┼───────
   1 │     1
   2 │     2
   3 │     3

julia> gd[1]
1×1 SubDataFrame
 Row │ id
     │ Int64
─────┼───────
   1 │     1

Использование GroupedDataFrame в качестве итерируемого и индексируемого объекта

Если вы хотите только разделить набор данных на подмножества, используйте функцию groupby. Затем можно выполнить итерацию SubDataFrame, составляющих идентифицированные группы:

julia> for subdf in iris_gdf
           println(size(subdf, 1))
       end
50
50
50

Чтобы при этом получить значения группирующих столбцов для каждой группы, используйте функцию pairs:

julia> for (key, subdf) in pairs(iris_gdf)
           println("Number of data points for $(key.Species): $(nrow(subdf))")
       end
Number of data points for Iris-setosa: 50
Number of data points for Iris-versicolor: 50
Number of data points for Iris-virginica: 50

Значение key в примере выше, где мы итерировали pairs(iris_gdf), представляет собой объект DataFrames.GroupKey, который можно использовать так же, как и NamedTuple.

Группировку фрейма данных с помощью функции groupby можно рассматривать как добавление к нему ключа поиска. Такие поиски можно выполнять, индексируя полученный GroupedDataFrame с помощью DataFrames.GroupKey (как было представлено выше), Tuple, NamedTuple или словаря. Вот ряд дополнительных примеров подобного индексирования.

julia> iris_gdf[(Species="Iris-virginica",)]  # NamedTuple
50×5 SubDataFrame
 Row │ SepalLength  SepalWidth  PetalLength  PetalWidth  Species
     │ Float64      Float64     Float64      Float64     String15
─────┼──────────────────────────────────────────────────────────────────
   1 │         6.3         3.3          6.0         2.5  Iris-virginica
   2 │         5.8         2.7          5.1         1.9  Iris-virginica
   3 │         7.1         3.0          5.9         2.1  Iris-virginica
   4 │         6.3         2.9          5.6         1.8  Iris-virginica
   5 │         6.5         3.0          5.8         2.2  Iris-virginica
   6 │         7.6         3.0          6.6         2.1  Iris-virginica
   7 │         4.9         2.5          4.5         1.7  Iris-virginica
   8 │         7.3         2.9          6.3         1.8  Iris-virginica
  ⋮  │      ⋮           ⋮            ⋮           ⋮             ⋮
  44 │         6.8         3.2          5.9         2.3  Iris-virginica
  45 │         6.7         3.3          5.7         2.5  Iris-virginica
  46 │         6.7         3.0          5.2         2.3  Iris-virginica
  47 │         6.3         2.5          5.0         1.9  Iris-virginica
  48 │         6.5         3.0          5.2         2.0  Iris-virginica
  49 │         6.2         3.4          5.4         2.3  Iris-virginica
  50 │         5.9         3.0          5.1         1.8  Iris-virginica
                                                         35 rows omitted

julia> iris_gdf[[("Iris-virginica",), ("Iris-setosa",)]] # вектор кортежей
GroupedDataFrame with 2 groups based on key: Species
First Group (50 rows): Species = "Iris-virginica"
 Row │ SepalLength  SepalWidth  PetalLength  PetalWidth  Species
     │ Float64      Float64     Float64      Float64     String15
─────┼──────────────────────────────────────────────────────────────────
   1 │         6.3         3.3          6.0         2.5  Iris-virginica
   2 │         5.8         2.7          5.1         1.9  Iris-virginica
  ⋮  │      ⋮           ⋮            ⋮           ⋮             ⋮
  49 │         6.2         3.4          5.4         2.3  Iris-virginica
  50 │         5.9         3.0          5.1         1.8  Iris-virginica
                                                         46 rows omitted
⋮
Last Group (50 rows): Species = "Iris-setosa"
 Row │ SepalLength  SepalWidth  PetalLength  PetalWidth  Species
     │ Float64      Float64     Float64      Float64     String15
─────┼───────────────────────────────────────────────────────────────
   1 │         5.1         3.5          1.4         0.2  Iris-setosa
   2 │         4.9         3.0          1.4         0.2  Iris-setosa
  ⋮  │      ⋮           ⋮            ⋮           ⋮            ⋮
  50 │         5.0         3.3          1.4         0.2  Iris-setosa
                                                      47 rows omitted

julia> key = keys(iris_gdf) |> last # последний ключ в iris_gdf
GroupKey: (Species = String15("Iris-virginica"),)

julia> iris_gdf[key]
50×5 SubDataFrame
 Row │ SepalLength  SepalWidth  PetalLength  PetalWidth  Species
     │ Float64      Float64     Float64      Float64     String15
─────┼──────────────────────────────────────────────────────────────────
   1 │         6.3         3.3          6.0         2.5  Iris-virginica
   2 │         5.8         2.7          5.1         1.9  Iris-virginica
   3 │         7.1         3.0          5.9         2.1  Iris-virginica
   4 │         6.3         2.9          5.6         1.8  Iris-virginica
   5 │         6.5         3.0          5.8         2.2  Iris-virginica
   6 │         7.6         3.0          6.6         2.1  Iris-virginica
   7 │         4.9         2.5          4.5         1.7  Iris-virginica
   8 │         7.3         2.9          6.3         1.8  Iris-virginica
  ⋮  │      ⋮           ⋮            ⋮           ⋮             ⋮
  44 │         6.8         3.2          5.9         2.3  Iris-virginica
  45 │         6.7         3.3          5.7         2.5  Iris-virginica
  46 │         6.7         3.0          5.2         2.3  Iris-virginica
  47 │         6.3         2.5          5.0         1.9  Iris-virginica
  48 │         6.5         3.0          5.2         2.0  Iris-virginica
  49 │         6.2         3.4          5.4         2.3  Iris-virginica
  50 │         5.9         3.0          5.1         1.8  Iris-virginica
                                                         35 rows omitted

julia> iris_gdf[Dict("Species" => "Iris-setosa")] # словарь
50×5 SubDataFrame
 Row │ SepalLength  SepalWidth  PetalLength  PetalWidth  Species
     │ Float64      Float64     Float64      Float64     String15
─────┼───────────────────────────────────────────────────────────────
   1 │         5.1         3.5          1.4         0.2  Iris-setosa
   2 │         4.9         3.0          1.4         0.2  Iris-setosa
   3 │         4.7         3.2          1.3         0.2  Iris-setosa
   4 │         4.6         3.1          1.5         0.2  Iris-setosa
   5 │         5.0         3.6          1.4         0.2  Iris-setosa
   6 │         5.4         3.9          1.7         0.4  Iris-setosa
   7 │         4.6         3.4          1.4         0.3  Iris-setosa
   8 │         5.0         3.4          1.5         0.2  Iris-setosa
  ⋮  │      ⋮           ⋮            ⋮           ⋮            ⋮
  44 │         5.0         3.5          1.6         0.6  Iris-setosa
  45 │         5.1         3.8          1.9         0.4  Iris-setosa
  46 │         4.8         3.0          1.4         0.3  Iris-setosa
  47 │         5.1         3.8          1.6         0.2  Iris-setosa
  48 │         4.6         3.2          1.4         0.2  Iris-setosa
  49 │         5.3         3.7          1.5         0.2  Iris-setosa
  50 │         5.0         3.3          1.4         0.2  Iris-setosa
                                                      35 rows omitted

Обратите внимание, что хотя GroupedDataFrame является итерируемым и индексируемым, он не является AbstractVector. По этой причине в настоящее время было решено не поддерживать ни map, ни трансляцию (чтобы в будущем можно было принять решение о том, какой тип результата они должны выдавать). Чтобы применить функцию ко всем группам фрейма данных и получить вектор результатов, используйте сначала либо включение, либо преобразование collect GroupedDataFrame в вектор. Вот примеры обоих подходов.

julia> sdf_vec = collect(iris_gdf)
3-element Vector{Any}:
 50×5 SubDataFrame
 Row │ SepalLength  SepalWidth  PetalLength  PetalWidth  Species
     │ Float64      Float64     Float64      Float64     String15
─────┼───────────────────────────────────────────────────────────────
   1 │         5.1         3.5          1.4         0.2  Iris-setosa
   2 │         4.9         3.0          1.4         0.2  Iris-setosa
   3 │         4.7         3.2          1.3         0.2  Iris-setosa
   4 │         4.6         3.1          1.5         0.2  Iris-setosa
   5 │         5.0         3.6          1.4         0.2  Iris-setosa
   6 │         5.4         3.9          1.7         0.4  Iris-setosa
   7 │         4.6         3.4          1.4         0.3  Iris-setosa
   8 │         5.0         3.4          1.5         0.2  Iris-setosa
  ⋮  │      ⋮           ⋮            ⋮           ⋮            ⋮
  44 │         5.0         3.5          1.6         0.6  Iris-setosa
  45 │         5.1         3.8          1.9         0.4  Iris-setosa
  46 │         4.8         3.0          1.4         0.3  Iris-setosa
  47 │         5.1         3.8          1.6         0.2  Iris-setosa
  48 │         4.6         3.2          1.4         0.2  Iris-setosa
  49 │         5.3         3.7          1.5         0.2  Iris-setosa
  50 │         5.0         3.3          1.4         0.2  Iris-setosa
                                                      35 rows omitted
 50×5 SubDataFrame
 Row │ SepalLength  SepalWidth  PetalLength  PetalWidth  Species
     │ Float64      Float64     Float64      Float64     String15
─────┼───────────────────────────────────────────────────────────────────
   1 │         7.0         3.2          4.7         1.4  Iris-versicolor
   2 │         6.4         3.2          4.5         1.5  Iris-versicolor
   3 │         6.9         3.1          4.9         1.5  Iris-versicolor
   4 │         5.5         2.3          4.0         1.3  Iris-versicolor
   5 │         6.5         2.8          4.6         1.5  Iris-versicolor
   6 │         5.7         2.8          4.5         1.3  Iris-versicolor
   7 │         6.3         3.3          4.7         1.6  Iris-versicolor
   8 │         4.9         2.4          3.3         1.0  Iris-versicolor
  ⋮  │      ⋮           ⋮            ⋮           ⋮              ⋮
  44 │         5.0         2.3          3.3         1.0  Iris-versicolor
  45 │         5.6         2.7          4.2         1.3  Iris-versicolor
  46 │         5.7         3.0          4.2         1.2  Iris-versicolor
  47 │         5.7         2.9          4.2         1.3  Iris-versicolor
  48 │         6.2         2.9          4.3         1.3  Iris-versicolor
  49 │         5.1         2.5          3.0         1.1  Iris-versicolor
  50 │         5.7         2.8          4.1         1.3  Iris-versicolor
                                                          35 rows omitted
 50×5 SubDataFrame
 Row │ SepalLength  SepalWidth  PetalLength  PetalWidth  Species
     │ Float64      Float64     Float64      Float64     String15
─────┼──────────────────────────────────────────────────────────────────
   1 │         6.3         3.3          6.0         2.5  Iris-virginica
   2 │         5.8         2.7          5.1         1.9  Iris-virginica
   3 │         7.1         3.0          5.9         2.1  Iris-virginica
   4 │         6.3         2.9          5.6         1.8  Iris-virginica
   5 │         6.5         3.0          5.8         2.2  Iris-virginica
   6 │         7.6         3.0          6.6         2.1  Iris-virginica
   7 │         4.9         2.5          4.5         1.7  Iris-virginica
   8 │         7.3         2.9          6.3         1.8  Iris-virginica
  ⋮  │      ⋮           ⋮            ⋮           ⋮             ⋮
  44 │         6.8         3.2          5.9         2.3  Iris-virginica
  45 │         6.7         3.3          5.7         2.5  Iris-virginica
  46 │         6.7         3.0          5.2         2.3  Iris-virginica
  47 │         6.3         2.5          5.0         1.9  Iris-virginica
  48 │         6.5         3.0          5.2         2.0  Iris-virginica
  49 │         6.2         3.4          5.4         2.3  Iris-virginica
  50 │         5.9         3.0          5.1         1.8  Iris-virginica
                                                         35 rows omitted

julia> map(nrow, sdf_vec)
3-element Vector{Int64}:
 50
 50
 50

julia> nrow.(sdf_vec)
3-element Vector{Int64}:
 50
 50
 50

Поскольку GroupedDataFrame является итерируемым, тот же результат можно получить с помощью включения:

julia> [nrow(sdf) for sdf in iris_gdf]
3-element Vector{Int64}:
 50
 50
 50

Обратите внимание, что использование стратегии разделения-применения-объединения с синтаксисом спецификации операций в combine, select или transform обычно быстрее для больших объектов GroupedDataFrame, чем их итерация, с той разницей, что они создают фрейм данных. Операция, соответствующая приведенному выше примеру, выглядит следующим образом.

julia> combine(iris_gdf, nrow)
3×2 DataFrame
 Row │ Species          nrow
     │ String15         Int64
─────┼────────────────────────
   1 │ Iris-setosa         50
   2 │ Iris-versicolor     50
   3 │ Iris-virginica      50

Моделирование предложения where SQL

Вы можете эффективно работать с подмножествами фрейма данных, используя SubDataFrame. Операции, выполняемые с такими объектами, могут либо создавать новый фрейм данных, либо осуществляться на месте. Вот ряд примеров.

julia> df = DataFrame(a=1:5)
5×1 DataFrame
 Row │ a
     │ Int64
─────┼───────
   1 │     1
   2 │     2
   3 │     3
   4 │     4
   5 │     5

julia> sdf = @view df[2:3, :]
2×1 SubDataFrame
 Row │ a
     │ Int64
─────┼───────
   1 │     2
   2 │     3

julia> transform(sdf, :a => ByRow(string)) # создание нового фрейма данных
2×2 DataFrame
 Row │ a      a_string
     │ Int64  String
─────┼─────────────────
   1 │     2  2
   2 │     3  3

julia> transform!(sdf, :a => ByRow(string)) # обновление исходного фрейма данных на месте
2×2 SubDataFrame
 Row │ a      a_string
     │ Int64  String?
─────┼─────────────────
   1 │     2  2
   2 │     3  3

julia> df # был создан новый столбец, заполненный отсутствующими значениями в отфильтрованных строках
5×2 DataFrame
 Row │ a      a_string
     │ Int64  String?
─────┼─────────────────
   1 │     1  missing
   2 │     2  2
   3 │     3  3
   4 │     4  missing
   5 │     5  missing

julia> select!(sdf, :a => -, renamecols=false) # обновление исходного фрейма данных на месте
2×1 SubDataFrame
 Row │ a
     │ Int64
─────┼───────
   1 │    -2
   2 │    -3

julia> df # столбец заменил существующий столбец; ранее сохраненные значения повторно используются в отфильтрованных строках
5×1 DataFrame
 Row │ a
     │ Int64
─────┼───────
   1 │     1
   2 │    -2
   3 │    -3
   4 │     4
   5 │     5

Аналогичные действия можно выполнить и с GroupedDataFrame:

julia> df = DataFrame(a=[1, 1, 1, 2, 2, 3], b=1:6)
6×2 DataFrame
 Row │ a      b
     │ Int64  Int64
─────┼──────────────
   1 │     1      1
   2 │     1      2
   3 │     1      3
   4 │     2      4
   5 │     2      5
   6 │     3      6

julia> sdf = @view df[2:4, :]
3×2 SubDataFrame
 Row │ a      b
     │ Int64  Int64
─────┼──────────────
   1 │     1      2
   2 │     1      3
   3 │     2      4

julia> gsdf = groupby(sdf, :a)
GroupedDataFrame with 2 groups based on key: a
First Group (2 rows): a = 1
 Row │ a      b
     │ Int64  Int64
─────┼──────────────
   1 │     1      2
   2 │     1      3
⋮
Last Group (1 row): a = 2
 Row │ a      b
     │ Int64  Int64
─────┼──────────────
   1 │     2      4

julia> transform(gsdf, nrow) # создание нового фрейма данных
3×3 DataFrame
 Row │ a      b      nrow
     │ Int64  Int64  Int64
─────┼─────────────────────
   1 │     1      2      2
   2 │     1      3      2
   3 │     2      4      1

julia> transform!(gsdf, nrow, :b => :b_copy)
3×4 SubDataFrame
 Row │ a      b      nrow    b_copy
     │ Int64  Int64  Int64?  Int64?
─────┼──────────────────────────────
   1 │     1      2       2       2
   2 │     1      3       2       3
   3 │     2      4       1       4

julia> df
6×4 DataFrame
 Row │ a      b      nrow     b_copy
     │ Int64  Int64  Int64?   Int64?
─────┼────────────────────────────────
   1 │     1      1  missing  missing
   2 │     1      2        2        2
   3 │     1      3        2        3
   4 │     2      4        1        4
   5 │     2      5  missing  missing
   6 │     3      6  missing  missing

julia> select!(gsdf, :b_copy, :b => sum, renamecols=false)
3×3 SubDataFrame
 Row │ a      b_copy  b
     │ Int64  Int64?  Int64
─────┼──────────────────────
   1 │     1       2      5
   2 │     1       3      5
   3 │     2       4      4

julia> df
6×3 DataFrame
 Row │ a      b_copy   b
     │ Int64  Int64?   Int64
─────┼───────────────────────
   1 │     1  missing      1
   2 │     1        2      5
   3 │     1        3      5
   4 │     2        4      4
   5 │     2  missing      5
   6 │     3  missing      6

Независимые от столбцов операции

Язык спецификации операций, используемый с combine, select и transform, поддерживает следующие независимые от столбцов операции:

  • получение количества строк в группе (nrow);

  • получение доли строк в группе (proprow);

  • получение номера группы (groupindices);

  • получение вектора индексов в группах (eachindex).

Эти операции не зависят от столбцов, поскольку не требуют указания имени входного столбца в синтаксисе спецификации операции.

Эти четыре исключения из стандартного синтаксиса спецификации операций были введены для удобства пользователей, так как эти операции часто требуются на практике.

Ниже каждое из них поясняется на примере.

Сначала создадим фрейм данных, с которым мы будем работать:

julia> df = DataFrame(customer_id=["a", "b", "b", "b", "c", "c"],
                      transaction_id=[12, 15, 19, 17, 13, 11],
                      volume=[2, 3, 1, 4, 5, 9])
6×3 DataFrame
 Row │ customer_id  transaction_id  volume
     │ String       Int64           Int64
─────┼─────────────────────────────────────
   1 │ a                        12       2
   2 │ b                        15       3
   3 │ b                        19       1
   4 │ b                        17       4
   5 │ c                        13       5
   6 │ c                        11       9

julia> gdf = groupby(df, :customer_id, sort=true);

julia> show(gdf, allgroups=true)
GroupedDataFrame with 3 groups based on key: customer_id
Group 1 (1 row): customer_id = "a"
 Row │ customer_id  transaction_id  volume
     │ String       Int64           Int64
─────┼─────────────────────────────────────
   1 │ a                        12       2
Group 2 (3 rows): customer_id = "b"
 Row │ customer_id  transaction_id  volume
     │ String       Int64           Int64
─────┼─────────────────────────────────────
   1 │ b                        15       3
   2 │ b                        19       1
   3 │ b                        17       4
Group 3 (2 rows): customer_id = "c"
 Row │ customer_id  transaction_id  volume
     │ String       Int64           Int64
─────┼─────────────────────────────────────
   1 │ c                        13       5
   2 │ c                        11       9

Получение количества строк в группе

Вы можете получить количество строк в группе GroupedDataFrame, просто написав nrow. В этом случае сгенерированное имя столбца с количеством строк будет иметь вид :nrow:

julia> combine(gdf, nrow)
3×2 DataFrame
 Row │ customer_id  nrow
     │ String       Int64
─────┼────────────────────
   1 │ a                1
   2 │ b                3
   3 │ c                2

Кроме того, можно передать имя целевого столбца:

julia> combine(gdf, nrow => "transaction_count")
3×2 DataFrame
 Row │ customer_id  transaction_count
     │ String       Int64
─────┼────────────────────────────────
   1 │ a                            1
   2 │ b                            3
   3 │ c                            2

Обратите внимание, что в обоих случаях мы не передавали имя исходного столбца, поскольку оно не требуется для определения количества строк в группе. Именно по этой причине независимые от столбцов операции являются исключениями из стандартного синтаксиса спецификации операций.

Выражение nrow также работает в синтаксисе спецификации операций, применяемом к фрейму данных. Вот пример:

julia> combine(df, nrow => "transaction_count")
1×1 DataFrame
 Row │ transaction_count
     │ Int64
─────┼───────────────────
   1 │                 6

Наконец, вспомним, что nrow является регулярной функцией, которая возвращает количество строк во фрейме данных:

julia> nrow(df)
6

Такое двойное использование nrow не приводит к неоднозначности и призвано облегчить запоминание этого исключения.

Получение доли строк в группе

Чтобы получить долю строк в группе GroupedDataFrame, можно использовать независимые от столбцов операции proprow и proprow => [target column name]. Вот ряд примеров.

julia> combine(gdf, proprow)
3×2 DataFrame
 Row │ customer_id  proprow
     │ String       Float64
─────┼───────────────────────
   1 │ a            0.166667
   2 │ b            0.5
   3 │ c            0.333333

julia> combine(gdf, proprow => "transaction_fraction")
3×2 DataFrame
 Row │ customer_id  transaction_fraction
     │ String       Float64
─────┼───────────────────────────────────
   1 │ a                        0.166667
   2 │ b                        0.5
   3 │ c                        0.333333

В отличие от nrow, proprow невозможно использовать вне синтаксиса спецификации операций, и оно допускается только при обработке GroupedDataFrame.

Получение номера группы

Еще одна распространенная операция — это получение номера группы. Чтобы получить его, используйте независимые от столбцов операции groupindices и groupindices => [target column name]:

julia> combine(gdf, groupindices)
3×2 DataFrame
 Row │ customer_id  groupindices
     │ String       Int64
─────┼───────────────────────────
   1 │ a                       1
   2 │ b                       2
   3 │ c                       3

julia> transform(gdf, groupindices)
6×4 DataFrame
 Row │ customer_id  transaction_id  volume  groupindices
     │ String       Int64           Int64   Int64
─────┼───────────────────────────────────────────────────
   1 │ a                        12       2             1
   2 │ b                        15       3             2
   3 │ b                        19       1             2
   4 │ b                        17       4             2
   5 │ c                        13       5             3
   6 │ c                        11       9             3

julia> combine(gdf, groupindices => "group_number")
3×2 DataFrame
 Row │ customer_id  group_number
     │ String       Int64
─────┼───────────────────────────
   1 │ a                       1
   2 │ b                       2
   3 │ c                       3

Вне синтаксиса спецификации операций groupindices также является регулярной функцией, которая возвращает индексы групп для каждой строки в родительском фрейме данных переданного GroupedDataFrame:

julia> groupindices(gdf)
6-element Vector{Union{Missing, Int64}}:
 1
 2
 2
 2
 3
 3

Получение вектора индексов в группах

Последняя независимая от столбцов операция, поддерживаемая синтаксисом спецификации операций, — это получение индекса каждой строки в каждой группе:

julia> combine(gdf, eachindex)
6×2 DataFrame
 Row │ customer_id  eachindex
     │ String       Int64
─────┼────────────────────────
   1 │ a                    1
   2 │ b                    1
   3 │ b                    2
   4 │ b                    3
   5 │ c                    1
   6 │ c                    2

julia> select(gdf, eachindex, groupindices)
6×3 DataFrame
 Row │ customer_id  eachindex  groupindices
     │ String       Int64      Int64
─────┼──────────────────────────────────────
   1 │ a                    1             1
   2 │ b                    1             2
   3 │ b                    2             2
   4 │ b                    3             2
   5 │ c                    1             3
   6 │ c                    2             3

julia> combine(gdf, eachindex => "transaction_number")
6×2 DataFrame
 Row │ customer_id  transaction_number
     │ String       Int64
─────┼─────────────────────────────────
   1 │ a                             1
   2 │ b                             1
   3 │ b                             2
   4 │ b                             3
   5 │ c                             1
   6 │ c                             2

Обратите внимание, что эта операция также имеет смысл в контексте фрейма данных, где все строки считаются входящими в одну группу:

julia> transform(df, eachindex)
6×4 DataFrame
 Row │ customer_id  transaction_id  volume  eachindex
     │ String       Int64           Int64   Int64
─────┼────────────────────────────────────────────────
   1 │ a                        12       2          1
   2 │ b                        15       3          2
   3 │ b                        19       1          3
   4 │ b                        17       4          4
   5 │ c                        13       5          5
   6 │ c                        11       9          6

Наконец, вспомним, что eachindex является стандартной функцией для получения всех индексов в массиве. Именно эта схожесть функциональности и послужила причиной выбора имени функции:

julia> collect(eachindex(df.customer_id))
6-element Vector{Int64}:
 1
 2
 3
 4
 5
 6

Это, например, означает, что в следующем примере два созданных столбца имеют одинаковое содержимое:

julia> combine(gdf, eachindex, :customer_id => eachindex)
6×3 DataFrame
 Row │ customer_id  eachindex  customer_id_eachindex
     │ String       Int64      Int64
─────┼───────────────────────────────────────────────
   1 │ a                    1                      1
   2 │ b                    1                      1
   3 │ b                    2                      2
   4 │ b                    3                      3
   5 │ c                    1                      1
   6 │ c                    2                      2

Независимые от столбцов операции и функции

При обсуждении независимых от столбцов операций важно помнить, что синтаксис спецификации операций позволяет передавать функцию (без имен исходного и целевого столбцов), и в этом случае такой функции передается SubDataFrame, который представляет группу в GroupedDataFrame. Вот пример сравнения независимой от столбца операции и функции.

julia> combine(gdf, eachindex, sdf -> axes(sdf, 1))
6×3 DataFrame
 Row │ customer_id  eachindex  x1
     │ String       Int64      Int64
─────┼───────────────────────────────
   1 │ a                    1      1
   2 │ b                    1      1
   3 │ b                    2      2
   4 │ b                    3      3
   5 │ c                    1      1
   6 │ c                    2      2

Обратите внимание, что независимая от столбцов операция eachindex дает тот же результат, что и использование анонимной функции sdf -> axes(sdf, 1), которая принимает SubDataFrame в качестве первого аргумента и возвращает индексы по первым осям. Важно отметить, что если бы функция eachindex не была определена как независимая от столбцов операция, то при передаче она бы завершилась сбоем, как показано здесь:

julia> combine(gdf, sdf -> eachindex(sdf))
ERROR: MethodError: no method matching keys(::SubDataFrame{DataFrame, DataFrames.Index, Vector{Int64}})

Причина этой ошибки в том, что функция eachindex не позволяет передавать SubDataFrame в качестве аргумента.

То же самое относится к proprow и groupindices: они не будут работать с SubDataFrame как автономные функции.

Независимая от столбца операция nrow — это другой случай, поскольку функция nrow принимает SubDataFrame в качестве аргумента:

julia> combine(gdf, nrow, sdf -> nrow(sdf))
3×3 DataFrame
 Row │ customer_id  nrow   x1
     │ String       Int64  Int64
─────┼───────────────────────────
   1 │ a                1      1
   2 │ b                3      3
   3 │ c                2      2

Обратите внимание, что столбцы :nrow и :x1 имеют одинаковое содержимое, но разница в том, что у них разные имена. nrow является независимой от столбцов операцией, генерирующей имя столбца :nrow по умолчанию с указанием количества строк в группе. С другой стороны, анонимная функция sdf -> nrow(sdf) получает SubDataFrame в качестве аргумента и возвращает количество строк. Имя столбца :x1 — это имя столбца, автоматически генерируемое по умолчанию при обработке анонимных функций.

Передача функции, принимающей SubDataFrame, представляет собой гибкую функциональность, позволяющую выполнять сложные операции с данными. Однако следует помнить о двух аспектах.

  • Использование полного синтаксиса спецификации операций (где передаются имена исходного и целевого столбцов ) или независимых от столбцов операций приведет к более быстрому выполнению кода (так как компилятор Julia способен лучше оптимизировать выполнение таких операций) по сравнению с передачей функции , принимающей SubDataFrame.

  • Хотя написание nrow, proprow, groupindices и eachindex выглядит как просто передача функции, внутренне они не принимают SubDataFrame в качестве аргумента. Как уже объяснялось в этом разделе, proprow, groupindices и eachindex не будут работать с SubDataFrame в качестве аргумента, а nrow будет работать, но выведет другое имя столбца. Эти четыре операции являются специальными независимыми от столбца операциями, которые представляют собой исключения из правил синтаксиса стандартной спецификации операций. Они были добавлены для удобства пользователей.

Указание порядка групп в groupby

По умолчанию порядок групп, создаваемых groupby, не определен. Если нужно, чтобы порядок групп соответствовал порядку первого появления в исходном фрейме данных группирующего ключа, передайте именованный аргумент sort=false функции groupby:

julia> push!(df, ["a", 100, 100]) # отправка строки с большими целыми значениями, чтобы отключить сортировку по умолчанию
7×3 DataFrame
 Row │ customer_id  transaction_id  volume
     │ String       Int64           Int64
─────┼─────────────────────────────────────
   1 │ a                        12       2
   2 │ b                        15       3
   3 │ b                        19       1
   4 │ b                        17       4
   5 │ c                        13       5
   6 │ c                        11       9
   7 │ a                       100     100

julia> keys(groupby(df, :volume))
7-element DataFrames.GroupKeys{GroupedDataFrame{DataFrame}}:
 GroupKey: (volume = 2,)
 GroupKey: (volume = 3,)
 GroupKey: (volume = 1,)
 GroupKey: (volume = 4,)
 GroupKey: (volume = 5,)
 GroupKey: (volume = 9,)
 GroupKey: (volume = 100,)

Если вы хотите, чтобы они были отсортированы в порядке возрастания, передайте sort=true:

julia> keys(groupby(df, :volume, sort=true))
7-element DataFrames.GroupKeys{GroupedDataFrame{DataFrame}}:
 GroupKey: (volume = 1,)
 GroupKey: (volume = 2,)
 GroupKey: (volume = 3,)
 GroupKey: (volume = 4,)
 GroupKey: (volume = 5,)
 GroupKey: (volume = 9,)
 GroupKey: (volume = 100,)

Вы также можете использовать оболочку order при передаче имени столбца группе или передать именованный кортеж в качестве именованного аргумента sort, содержащего одно или несколько полей alg, lt, by, rev и order, которые будут обрабатываться так же, как в sortperm:

julia> keys(groupby(df, [:customer_id, order(:volume, rev=true)]))
6-element DataFrames.GroupKeys{GroupedDataFrame{DataFrame}}:
 GroupKey: (customer_id = "a", volume = 2)
 GroupKey: (customer_id = "b", volume = 4)
 GroupKey: (customer_id = "b", volume = 3)
 GroupKey: (customer_id = "b", volume = 1)
 GroupKey: (customer_id = "c", volume = 9)
 GroupKey: (customer_id = "c", volume = 5)

julia> keys(groupby(df, :customer_id, sort=(rev=true,)))
3-element DataFrames.GroupKeys{GroupedDataFrame{DataFrame}}:
 GroupKey: (customer_id = "c",)
 GroupKey: (customer_id = "b",)
 GroupKey: (customer_id = "a",)