Стратегия «Разделение-применение-объединение» (split-apply-combine)
Разработка поддержки разделения-применения-объединения
Многие задачи анализа данных состоят из трех этапов:
-
разделение набора данных на группы;
-
применение некоторых функций к каждой группе;
-
объединение результатов.
Обратите внимание, что этапы 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
. Эта спецификация может иметь следующий вид:
-
Стандартные селекторы столбцов (целые числа, символы (
Symbol
), строки, векторы целых чисел, векторы символов (Symbol
), векторы строк,All
,Cols
,:
,Between
,Not
и регулярные выражения) -
Пара
cols => function
, указывающая, что функцию (function
) следует вызывать с позиционными аргументами, содержащими столбцыcols
, которые могут быть любым допустимым селектором столбцов. В этом случае имя целевого столбца генерируется автоматически, и предполагается, что функция (function
) возвращает единичное значение или вектор. Сгенерированное имя создается путем конкатенации имени исходного столбца и имени функции (function
) по умолчанию (см. примеры ниже). -
Форма
cols => function => target_cols
, дополнительно явно указывающая целевой столбец или столбцы, которые должны быть одним именем (какSymbol
или строка), вектор имен илиAsTable
. Кроме того, это может быть функция (Function
), которая принимает в качестве аргумента строку или вектор строк, содержащие имена столбцов, выбранные с помощьюcols
, и возвращает имена целевых столбцов (допускаются все принятые типы, кромеAsTable
). -
Пара
col => target_cols
, которая переименовывает столбецcol
вtarget_cols
, который должен быть одним именем (какSymbol
или строка), вектором имен илиAsTable
. -
Независимые от столбцов операции
function => target_cols
или простоfunction
для конкретных функций (function
), где входные столбцы опущены; безtarget_cols
новый столбец имеет то же имя, что иfunction
, в противном случае должен быть одним именем (какSymbol
или строка). Поддерживаются следующие функции (function
).-
nrow
для эффективного вычисления количества строк в каждой группе. -
proprow
для эффективного вычисления доли строк в каждой группе. -
eachindex
для возврата вектора, содержащего номер каждой строки в каждой группе. -
groupindices
для возврата номера группы.
-
-
Векторы или матрицы, содержащие преобразования, заданные синтаксисом
Pair
, описываются в пунктах 2—5. -
Функция, которая будет вызвана с
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
.
-
Если
cols
иtarget_cols
опущены (передается толькоfunction
), при возврате фрейма данных, матрицы,NamedTuple
,Tables.AbstractRow
илиDataFrameRow
будет создано несколько столбцов. При возврате любого другого значения создается один столбец. -
Если
target_cols
является символом (Symbol
) или строкой, предполагается, что функция возвратит один столбец. В этом случае при возврате фрейма данных, матрицы,NamedTuple
,Tables.AbstractRow
илиDataFrameRow
возникнет ошибка. -
Если
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",)