Начало работы с DataFrames.jl
Настройка среды
Чтобы проверить, все ли работает правильно, можно выполнить тесты, входящие в состав DataFrames.jl, но имейте в виду, что это займет более 30 минут:
julia> using Pkg
julia> Pkg.test("DataFrames") # Внимание! Это займет более 30 минут.
Кроме того, рекомендуется проверить установленную версию DataFrames.jl с помощью команды status.
julia> ]
(@v1.9) pkg> status DataFrames
Status `~\v1.6\Project.toml`
[a93c6f00] DataFrames v1.5.0
В оставшейся части этого руководства мы будем считать, что вы установили пакет DataFrames.jl и уже ввели using DataFrames, чтобы загрузить его:
julia> using DataFrames
Самым базовым типом в пакете DataFrames.jl является DataFrame. Обычно в нем каждая строка интерпретируется как наблюдение, а каждый столбец — как признак.
|
При сборке (предварительной компиляции) пакета DataFrames.jl предпринимаются дополнительные усилия, чтобы обеспечить его быстродействие. Однако в некоторых ситуациях пользователь может отказаться от предварительной компиляции, чтобы сэкономить время на сборке и последующей загрузке пакета. Чтобы отключить предварительную компиляцию DataFrames.jl в текущем проекте, следуйте инструкциям в документации по PrecompileTools.jl. |
Конструкторы и основные вспомогательные функции
Конструкторы
В этом разделе описывается несколько способов создания объекта DataFrame с помощью конструктора. Подробный список поддерживаемых конструкторов и дополнительные примеры можно найти в документации по объекту DataFrame.
Начнем с создания пустого объекта DataFrame:
julia> DataFrame()
0×0 DataFrame
Теперь давайте инициализируем объект DataFrame с несколькими столбцами. Вот самый простой способ.
julia> DataFrame(A=1:3, B=5:7, fixed=1)
3×3 DataFrame
Row │ A B fixed
│ Int64 Int64 Int64
─────┼─────────────────────
1 │ 1 5 1
2 │ 2 6 1
3 │ 3 7 1
Обратите внимание, что при использовании этого конструктора скаляры, например 1 для столбца :fixed, автоматически транслируются для заполнения всех строк создаваемого объекта DataFrame.
Иногда бывает необходимо создать фрейм данных, имена столбцов которого не являются допустимыми именами в Julia. В таком случае может быть полезна следующая форма, где = заменяется =>:
julia> DataFrame("customer age" => [15, 20, 25],
"first name" => ["Rohit", "Rahul", "Akshat"])
3×2 DataFrame
Row │ customer age first name
│ Int64 String
─────┼──────────────────────────
1 │ 15 Rohit
2 │ 20 Rahul
3 │ 25 Akshat
Обратите внимание, на этот раз мы передали имена столбцов в виде строк.
Исходные данные часто хранятся в словаре. Если ключами словаря являются строки или символы Symbol, вы можете легко создать DataFrame на его основе:
julia> dict = Dict("customer age" => [15, 20, 25],
"first name" => ["Rohit", "Rahul", "Akshat"])
Dict{String, Vector} with 2 entries:
"first name" => ["Rohit", "Rahul", "Akshat"]
"customer age" => [15, 20, 25]
julia> DataFrame(dict)
3×2 DataFrame
Row │ customer age first name
│ Int64 String
─────┼──────────────────────────
1 │ 15 Rohit
2 │ 20 Rahul
3 │ 25 Akshat
julia> dict = Dict(:customer_age => [15, 20, 25],
:first_name => ["Rohit", "Rahul", "Akshat"])
Dict{Symbol, Vector} with 2 entries:
:customer_age => [15, 20, 25]
:first_name => ["Rohit", "Rahul", "Akshat"]
julia> DataFrame(dict)
3×2 DataFrame
Row │ customer_age first_name
│ Int64 String
─────┼──────────────────────────
1 │ 15 Rohit
2 │ 20 Rahul
3 │ 25 Akshat
В качестве имен столбцов предпочтительнее использовать символы Symbol, например :customer_age, а не строки, например "customer age", так как быстродействие в этом случае выше. Однако, как видно в примере выше, если имя столбца содержит пробел, передавать его как Symbol не очень удобно (его придется записывать в виде Symbol("customer age"), что слишком громоздко), поэтому лучше использовать строку.
Кроме того, объект DataFrame нередко создается на основе кортежа NamedTuple векторов или вектора кортежей NamedTuple. Вот несколько примеров таких операций.
julia> DataFrame((a=[1, 2], b=[3, 4]))
2×2 DataFrame
Row │ a b
│ Int64 Int64
─────┼──────────────
1 │ 1 3
2 │ 2 4
julia> DataFrame([(a=1, b=0), (a=2, b=0)])
2×2 DataFrame
Row │ a b
│ Int64 Int64
─────┼──────────────
1 │ 1 0
2 │ 2 0
В завершение нашего обзора контейнеров покажем, как создать DataFrame на основе матрицы. В этом случае матрица передается первым аргументом. Если вторым аргументом указано просто :auto, то имена столбцов x1, x2 и т. д. создаются автоматически.
julia> DataFrame([1 0; 2 0], :auto)
2×2 DataFrame
Row │ x1 x2
│ Int64 Int64
─────┼──────────────
1 │ 1 0
2 │ 2 0
Вы также можете передать вектор имен столбцов во втором аргументе конструктора DataFrame:
julia> mat = [1 2 4 5; 15 58 69 41; 23 21 26 69]
3×4 Matrix{Int64}:
1 2 4 5
15 58 69 41
23 21 26 69
julia> nms = ["a", "b", "c", "d"]
4-element Vector{String}:
"a"
"b"
"c"
"d"
julia> DataFrame(mat, nms)
3×4 DataFrame
Row │ a b c d
│ Int64 Int64 Int64 Int64
─────┼────────────────────────────
1 │ 1 2 4 5
2 │ 15 58 69 41
3 │ 23 21 26 69
Теперь вы знаете, как создать DataFrame на основе данных, уже имеющихся в сеансе Julia. В следующем разделе мы покажем, как загрузить данные в DataFrame с диска.
Чтение данных из CSV-файлов
Здесь мы рассмотрим один из наиболее распространенных сценариев, когда данные хранятся на диске в формате CSV.
Во-первых, убедитесь в том, что установлен пакет CSV.jl. Это можно сделать с помощью следующих инструкций.
julia> using Pkg
julia> Pkg.add("CSV")
Для считывания файла мы используем функцию CSV.read.
julia> using CSV
julia> path = joinpath(pkgdir(DataFrames), "docs", "src", "assets", "german.csv");
julia> german_ref = CSV.read(path, DataFrame)
1000×10 DataFrame
Row │ id Age Sex Job Housing Saving accounts Checking accou ⋯
│ Int64 Int64 String7 Int64 String7 String15 String15 ⋯
──────┼─────────────────────────────────────────────────────────────────────────
1 │ 0 67 male 2 own NA little ⋯
2 │ 1 22 female 2 own little moderate
3 │ 2 49 male 1 own little NA
4 │ 3 45 male 2 free little little
5 │ 4 53 male 2 free little little ⋯
6 │ 5 35 male 1 free NA NA
7 │ 6 53 male 2 own quite rich NA
8 │ 7 35 male 3 rent little moderate
⋮ │ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋱
994 │ 993 30 male 3 own little little ⋯
995 │ 994 50 male 2 own NA NA
996 │ 995 31 female 1 own little NA
997 │ 996 40 male 3 own little little
998 │ 997 38 male 2 own little NA ⋯
999 │ 998 23 male 2 free little little
1000 │ 999 27 male 2 own moderate moderate
4 columns and 985 rows omitted
Как видите, фрейм данных не помещается на экране в длину и в ширину, поэтому он был обрезан: правые 4 столбца и средние 985 строк не выводятся. Далее в руководстве мы обсудим, как сделать так, чтобы среда Julia выводила весь фрейм данных.
Также обратите внимание, что DataFrames.jl выводит тип данных столбца под его именем. В данном случае это Int64 или String7 и String15.
Здесь стоит упомянуть различие между стандартным типом String в Julia и такими типами, как, например, String7 или String15. Типы с числовым суффиксом означают строки фиксированной ширины (что аналогично типу CHAR(N), имеющемуся во многих базах данных). Работать с такими строками получается гораздо быстрее (особенно если их много), чем со стандартным типом String, потому что их экземпляры не размещаются в куче. По этой причине CSV.read по умолчанию считывает столбцы узких строк с использованием этих типов фиксированной ширины.
Давайте теперь подробно рассмотрим следующий блок кода:
path = joinpath(pkgdir(DataFrames), "docs", "src", "assets", "german.csv");
german_ref = CSV.read(path, DataFrame)
-
Файл
german.csvхранится в репозитории DataFrames.jl, чтобы пользователю не приходилось скачивать его каждый раз. -
pkgdir(DataFrames)возвращает полный путь к корневому каталогу пакета DataFrames.jl. -
Затем из этого каталога нам необходимо перейти в каталог, где хранится файл
german.csv. Мы используемjoinpath, так как это рекомендуемый способ составления путей к ресурсам на диске независимо от операционной системы (напомним, что в Windows и Unix в качестве разделителей путей используются разные символы —/и\; функцияjoinpathпозволяет избежать связанных с этим проблем). -
Далее мы считываем CSV-файл. Значение второго аргумента функции
CSV.read—DataFrame— указывает на то, что файл необходимо считать вDataFrame(CSV.readподдерживает считывание данных во множество форматов).
Прежде чем продолжить, скопируем эталонный фрейм данных:
julia> german = copy(german_ref); # копируем фрейм данных
Так мы всегда сможем легко восстановить данные, даже если внесем некорректные изменения во фрейм данных german.
Основные операции с фреймами данных
Чтобы извлечь столбцы фрейма данных напрямую (то есть без копирования), можно воспользоваться одной из следующих синтаксических конструкций: german.Sex, german."Sex", german[!, :Sex] или german[!, "Sex"].
Две последние конструкции с индексированием более гибкие, поскольку позволяют передавать переменную, содержащую имя столбца, а не только литеральное имя, как в случае синтаксиса с использованием ..
julia> german.Sex
1000-element PooledArrays.PooledVector{String7, UInt32, Vector{UInt32}}:
"male"
"female"
"male"
"male"
"male"
"male"
"male"
"male"
"male"
"male"
⋮
"male"
"male"
"male"
"male"
"female"
"male"
"male"
"male"
"male"
julia> colname = "Sex"
"Sex"
julia> german[!, colname]
1000-element PooledArrays.PooledVector{String7, UInt32, Vector{UInt32}}:
"male"
"female"
"male"
"male"
"male"
"male"
"male"
"male"
"male"
"male"
⋮
"male"
"male"
"male"
"male"
"female"
"male"
"male"
"male"
"male"
Поскольку german.Sex не создает копию при извлечении столбца из фрейма данных, изменение элементов вектора, возвращаемого этой операцией, повлияет на значения, хранящиеся в исходном фрейме данных german. Для получения копии столбца можно использовать german[:, :Sex] или german[:, "Sex"]. В таком случае изменение вектора, возвращаемого операцией, не влияет на данные, хранящиеся во фрейме данных german.
Функция === позволяет проверить, дают ли оба выражения один и тот же объект, и подтвердить описанное ниже поведение:
julia> german.Sex === german[!, :Sex]
true
julia> german.Sex === german[:, :Sex]
false
Получить вектор имен столбцов фрейма данных в виде строк (String) можно с помощью функции names:
julia> names(german)
10-element Vector{String}:
"id"
"Age"
"Sex"
"Job"
"Housing"
"Saving accounts"
"Checking account"
"Credit amount"
"Duration"
"Purpose"
Иногда нужно получить имена столбцов, отвечающие определенному условию.
Например, чтобы получить имена столбцов с определенным типом элементов, передайте этот тип вторым аргументом функции names:
julia> names(german, AbstractString)
5-element Vector{String}:
"Sex"
"Housing"
"Saving accounts"
"Checking account"
"Purpose"
С другими способами фильтрации имен столбцов можно ознакомиться в документации по функции names.
Если же вы хотите получить имена столбцов фрейма данных в виде символов (Symbol), используйте функцию propertynames:
julia> propertynames(german)
10-element Vector{Symbol}:
:id
:Age
:Sex
:Job
:Housing
Symbol("Saving accounts")
Symbol("Checking account")
Symbol("Credit amount")
:Duration
:Purpose
Как видите, с именами столбцов, содержащими пробелы, не очень удобно работать как с Symbol, так как приходится вводить больше текста, что затрудняет восприятие.
Если вы вместо этого хотите узнать типы элементов столбцов, вы можете воспользоваться функцией eachcol(german), чтобы получить итератор по столбцам фрейма данных. Затем на него можно транслировать функцию eltype, чтобы получить нужный результат:
julia> eltype.(eachcol(german))
10-element Vector{DataType}:
Int64
Int64
String7
Int64
String7
String15
String15
Int64
Int64
String31
|
Помните, что DataFrames.jl позволяет для удобства использовать символы |
Прежде чем мы завершим, давайте обсудим функции empty и empty!, которые удаляют все строки из DataFrame. Понимание различий в поведении этих двух функций поможет разобраться в схеме именования функций в DataFrames.jl в целом.
Начнем с примера использования функций empty и empty!:
julia> empty(german)
0×10 DataFrame
Row │ id Age Sex Job Housing Saving accounts Checking accoun ⋯
│ Int64 Int64 String7 Int64 String7 String15 String15 ⋯
─────┴──────────────────────────────────────────────────────────────────────────
4 columns omitted
julia> german
1000×10 DataFrame
Row │ id Age Sex Job Housing Saving accounts Checking accou ⋯
│ Int64 Int64 String7 Int64 String7 String15 String15 ⋯
──────┼─────────────────────────────────────────────────────────────────────────
1 │ 0 67 male 2 own NA little ⋯
2 │ 1 22 female 2 own little moderate
3 │ 2 49 male 1 own little NA
4 │ 3 45 male 2 free little little
5 │ 4 53 male 2 free little little ⋯
6 │ 5 35 male 1 free NA NA
7 │ 6 53 male 2 own quite rich NA
8 │ 7 35 male 3 rent little moderate
⋮ │ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋱
994 │ 993 30 male 3 own little little ⋯
995 │ 994 50 male 2 own NA NA
996 │ 995 31 female 1 own little NA
997 │ 996 40 male 3 own little little
998 │ 997 38 male 2 own little NA ⋯
999 │ 998 23 male 2 free little little
1000 │ 999 27 male 2 own moderate moderate
4 columns and 985 rows omitted
julia> empty!(german)
0×10 DataFrame
Row │ id Age Sex Job Housing Saving accounts Checking accoun ⋯
│ Int64 Int64 String7 Int64 String7 String15 String15 ⋯
─────┴──────────────────────────────────────────────────────────────────────────
4 columns omitted
julia> german
0×10 DataFrame
Row │ id Age Sex Job Housing Saving accounts Checking accoun ⋯
│ Int64 Int64 String7 Int64 String7 String15 String15 ⋯
─────┴──────────────────────────────────────────────────────────────────────────
4 columns omitted
В приведенном выше примере функция empty создает новый объект DataFrame с теми же именами столбцов и типами их элементов, что и в german, но без строк. В свою очередь, функция empty! удаляет все строки из объекта german на месте и делает пустыми все его столбцы.
Различие в поведении функций empty и empty! обуславливается стилистическим соглашением, принятым в языке Julia. Это соглашение соблюдается во всех функциях, предоставляемых пакетом DataFrames.jl.
Получение базовой информации о фрейме данных
В этом разделе мы узнаем, как получить базовую информацию о нашем объекте DataFrame с именем german:
Функция size возвращает измерения фрейма данных. Сначала мы восстановим фрейм данных german, так как ранее его очистили.
julia> german = copy(german_ref);
julia> size(german)
(1000, 10)
julia> size(german, 1)
1000
julia> size(german, 2)
10
Кроме того, с помощью функций nrow и ncol можно получить количество строк и столбцов во фрейме данных:
julia> nrow(german)
1000
julia> ncol(german)
10
Для получения базовой статистики по данным во фрейме данных используйте функцию describe (сведения о том, как настроить выводимую статистику, см. в справке по функции describe).
julia> describe(german)
10×7 DataFrame
Row │ variable mean min median max nmissing ⋯
│ Symbol Union… Any Union… Any Int64 ⋯
─────┼──────────────────────────────────────────────────────────────────────────
1 │ id 499.5 0 499.5 999 0 ⋯
2 │ Age 35.546 19 33.0 75 0
3 │ Sex female male 0
4 │ Job 1.904 0 2.0 3 0
5 │ Housing free rent 0 ⋯
6 │ Saving accounts NA rich 0
7 │ Checking account NA rich 0
8 │ Credit amount 3271.26 250 2319.5 18424 0
9 │ Duration 20.903 4 18.0 72 0 ⋯
10 │ Purpose business vacation/others 0
1 column omitted
Чтобы ограничить столбцы, обрабатываемые функцией describe, используйте именованный аргумент cols, например:
julia> describe(german, cols=1:3)
3×7 DataFrame
Row │ variable mean min median max nmissing eltype
│ Symbol Union… Any Union… Any Int64 DataType
─────┼────────────────────────────────────────────────────────────
1 │ id 499.5 0 499.5 999 0 Int64
2 │ Age 35.546 19 33.0 75 0 Int64
3 │ Sex female male 0 String7
По умолчанию выводятся такие статистические показатели, как среднее, минимальное, медианное и максимальное значения, количество отсутствующих значений и тип элементов столбца. При вычислении сводной статистики значения missing пропускаются.
Чтобы настроить вывод фрейма данных, можно вызвать функцию show вручную: show(german, allrows=true) выводит все строки, даже если они не помещаются на экране, а show(german, allcols=true) делает то же самое для столбцов, например:
julia> show(german, allcols=true)
1000×10 DataFrame
Row │ id Age Sex Job Housing Saving accounts Checking account Credit amount Duration Purpose
│ Int64 Int64 String7 Int64 String7 String15 String15 Int64 Int64 String31
──────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
1 │ 0 67 male 2 own NA little 1169 6 radio/TV
2 │ 1 22 female 2 own little moderate 5951 48 radio/TV
3 │ 2 49 male 1 own little NA 2096 12 education
4 │ 3 45 male 2 free little little 7882 42 furniture/equipment
5 │ 4 53 male 2 free little little 4870 24 car
6 │ 5 35 male 1 free NA NA 9055 36 education
7 │ 6 53 male 2 own quite rich NA 2835 24 furniture/equipment
8 │ 7 35 male 3 rent little moderate 6948 36 car
⋮ │ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮
994 │ 993 30 male 3 own little little 3959 36 furniture/equipment
995 │ 994 50 male 2 own NA NA 2390 12 car
996 │ 995 31 female 1 own little NA 1736 12 furniture/equipment
997 │ 996 40 male 3 own little little 3857 30 car
998 │ 997 38 male 2 own little NA 804 12 radio/TV
999 │ 998 23 male 2 free little little 1845 45 radio/TV
1000 │ 999 27 male 2 own moderate moderate 4576 45 car
985 rows omitted
Описательную статистику можно также легко вычислять непосредственно по отдельным столбцам с помощью функций, определенных в модуле Statistics:
julia> using Statistics
julia> mean(german.Age)
35.546
Если же мы хотим применить какую-либо функцию ко всем столбцам фрейма данных, можно прибегнуть к функции mapcols. Она возвращает объект DataFrame, в котором каждый столбец исходного фрейма данных преобразован с помощью функции, переданной в первом аргументе. Обратите внимание, что mapcols гарантирует, что столбцы из german не будут повторно использоваться в возвращаемом DataFrame. Если преобразование возвращает аргумент, он копируется перед сохранением.
julia> mapcols(id -> id .^ 2, german)
1000×10 DataFrame
Row │ id Age Sex Job Housing Saving accounts Ch ⋯
│ Int64 Int64 String Int64 String String St ⋯
──────┼─────────────────────────────────────────────────────────────────────────
1 │ 0 4489 malemale 4 ownown NANA li ⋯
2 │ 1 484 femalefemale 4 ownown littlelittle mo
3 │ 4 2401 malemale 1 ownown littlelittle NA
4 │ 9 2025 malemale 4 freefree littlelittle li
5 │ 16 2809 malemale 4 freefree littlelittle li ⋯
6 │ 25 1225 malemale 1 freefree NANA NA
7 │ 36 2809 malemale 4 ownown quite richquite rich NA
8 │ 49 1225 malemale 9 rentrent littlelittle mo
⋮ │ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋱
994 │ 986049 900 malemale 9 ownown littlelittle li ⋯
995 │ 988036 2500 malemale 4 ownown NANA NA
996 │ 990025 961 femalefemale 1 ownown littlelittle NA
997 │ 992016 1600 malemale 9 ownown littlelittle li
998 │ 994009 1444 malemale 4 ownown littlelittle NA ⋯
999 │ 996004 529 malemale 4 freefree littlelittle li
1000 │ 998001 729 malemale 4 ownown moderatemoderate mo
4 columns and 985 rows omitted
Для просмотра первой и последней строк фрейма данных можно воспользоваться функциями first и last соответственно:
julia> first(german, 6)
6×10 DataFrame
Row │ id Age Sex Job Housing Saving accounts Checking accoun ⋯
│ Int64 Int64 String7 Int64 String7 String15 String15 ⋯
─────┼──────────────────────────────────────────────────────────────────────────
1 │ 0 67 male 2 own NA little ⋯
2 │ 1 22 female 2 own little moderate
3 │ 2 49 male 1 own little NA
4 │ 3 45 male 2 free little little
5 │ 4 53 male 2 free little little ⋯
6 │ 5 35 male 1 free NA NA
4 columns omitted
julia> last(german, 5)
5×10 DataFrame
Row │ id Age Sex Job Housing Saving accounts Checking accoun ⋯
│ Int64 Int64 String7 Int64 String7 String15 String15 ⋯
─────┼──────────────────────────────────────────────────────────────────────────
1 │ 995 31 female 1 own little NA ⋯
2 │ 996 40 male 3 own little little
3 │ 997 38 male 2 own little NA
4 │ 998 23 male 2 free little little
5 │ 999 27 male 2 own moderate moderate ⋯
4 columns omitted
Если при использовании first или last количество строк не передается, возвращается первая или последняя строка DataFrameRow во фрейме данных. DataFrameRow — это представление одной строки в AbstractDataFrame. В нем хранятся ссылка на родительский объект DataFrame и информация о выбранных из него строке и столбцах. Объект DataFrameRow можно представить себе как изменяемый кортеж NamedTuple, то есть он позволяет изменять исходный фрейм данных, что часто бывает полезно.
julia> first(german)
DataFrameRow
Row │ id Age Sex Job Housing Saving accounts Checking accoun ⋯
│ Int64 Int64 String7 Int64 String7 String15 String15 ⋯
─────┼──────────────────────────────────────────────────────────────────────────
1 │ 0 67 male 2 own NA little ⋯
4 columns omitted
julia> last(german)
DataFrameRow
Row │ id Age Sex Job Housing Saving accounts Checking accou ⋯
│ Int64 Int64 String7 Int64 String7 String15 String15 ⋯
──────┼─────────────────────────────────────────────────────────────────────────
1000 │ 999 27 male 2 own moderate moderate ⋯
4 columns omitted
Получение и задание данных во фрейме данных
Синтаксис индексирования
Фрейм данных может индексироваться так же, как матрица. В разделе Indexing руководства вы найдете подробную информацию обо всех доступных вариантах. Здесь мы рассмотрим самые основные.
Синтаксис индексирования имеет общий вид data_frame[selected_rows, selected_columns]. Обратите внимание, что, в отличие от матриц в модуле Base Julia, необходимо обязательно передавать как селектор строк, так и селектор столбцов. Двоеточие : указывает на то, что все элементы (строки или столбцы в зависимости от позиции) должны сохраняться: Вот несколько примеров:
julia> german[1:5, [:Sex, :Age]]
5×2 DataFrame
Row │ Sex Age
│ String7 Int64
─────┼────────────────
1 │ male 67
2 │ female 22
3 │ male 49
4 │ male 45
5 │ male 53
julia> german[1:5, :]
5×10 DataFrame
Row │ id Age Sex Job Housing Saving accounts Checking accoun ⋯
│ Int64 Int64 String7 Int64 String7 String15 String15 ⋯
─────┼──────────────────────────────────────────────────────────────────────────
1 │ 0 67 male 2 own NA little ⋯
2 │ 1 22 female 2 own little moderate
3 │ 2 49 male 1 own little NA
4 │ 3 45 male 2 free little little
5 │ 4 53 male 2 free little little ⋯
4 columns omitted
julia> german[[1, 6, 15], :]
3×10 DataFrame
Row │ id Age Sex Job Housing Saving accounts Checking accoun ⋯
│ Int64 Int64 String7 Int64 String7 String15 String15 ⋯
─────┼──────────────────────────────────────────────────────────────────────────
1 │ 0 67 male 2 own NA little ⋯
2 │ 5 35 male 1 free NA NA
3 │ 14 28 female 2 rent little little
4 columns omitted
julia> german[:, [:Age, :Sex]]
1000×2 DataFrame
Row │ Age Sex
│ Int64 String7
──────┼────────────────
1 │ 67 male
2 │ 22 female
3 │ 49 male
4 │ 45 male
5 │ 53 male
6 │ 35 male
7 │ 53 male
8 │ 35 male
⋮ │ ⋮ ⋮
994 │ 30 male
995 │ 50 male
996 │ 31 female
997 │ 40 male
998 │ 38 male
999 │ 23 male
1000 │ 27 male
985 rows omitted
Обратите внимание, что german[!, [:Sex]] и german[:, [:Sex]] возвращают фрейм данных, а german[!, :Sex] и german[:, :Sex] — вектор. В первом случае [:Sex] является вектором, указывающим, что результирующий объект должен быть фреймом данных. С другой стороны, :Sex — это одиночный символ Symbol, указывающий на то, что должен быть извлечен вектор с одним столбцом. Обратите внимание, что в первом случае требуется передать вектор (а не просто итерируемый объект), поэтому, например, german[:, (:Age, :Sex)] не разрешается, а german[:, [:Age, :Sex]] допускается. Ниже показаны обе операции, чтобы продемонстрировать это различие.
julia> german[!, [:Sex]]
1000×1 DataFrame
Row │ Sex
│ String7
──────┼─────────
1 │ male
2 │ female
3 │ male
4 │ male
5 │ male
6 │ male
7 │ male
8 │ male
⋮ │ ⋮
994 │ male
995 │ male
996 │ female
997 │ male
998 │ male
999 │ male
1000 │ male
985 rows omitted
julia> german[!, :Sex]
1000-element PooledArrays.PooledVector{String7, UInt32, Vector{UInt32}}:
"male"
"female"
"male"
"male"
"male"
"male"
"male"
"male"
"male"
"male"
⋮
"male"
"male"
"male"
"male"
"female"
"male"
"male"
"male"
"male"
Как объяснялось ранее в этом руководстве, разница между использованием ! и : при передаче индекса строки заключается в том, что ! не выполняет копирование столбцов при считывании данных из фрейма данных, а : выполняет. Поэтому в german[!, [:Sex]] хранится тот же вектор, что и в исходном фрейме данных german, а в german[:, [:Sex]] — его копия.
Селектора !, как правило, следует избегать, так как его использование может приводить к трудновыявляемым ошибкам. Однако при работе с очень большими фреймами данных он может быть полезен, так как позволяет экономить память и повышать производительность операций.
Подводя итог, чтобы получить столбец :Age из фрейма данных german, можно сделать следующее:
-
скопировать вектор:
german[:, :Age],german[:, "Age"]илиgerman[:, 2]; -
получить вектор без копирования:
german.Age,german."Age",german[!, :Age],german[!, "Age"]илиgerman[!, 2].
Для получения первых двух столбцов в виде DataFrame можно обратиться к ним по индексам следующим образом:
-
получить скопированные столбцы:
german[:, 1:2],german[:, [:id, :Age]]илиgerman[:, ["id", "Age"]]; -
использовать столбцы повторно без копирования:
german[!, 1:2],german[!, [:id, :Age]]илиgerman[!, ["id", "Age"]].
Чтобы получить одну ячейку фрейма данных, используйте тот же синтаксис, что и для получения ячейки матрицы:
julia> german[4, 4]
2
Представления
Мы можем также создать представление (view) фрейма данных. Такой подход зачастую полезен, так как при нем расходуется меньше памяти, чем при создании материализованной выборки. Создать представление можно с помощью функции view:
julia> view(german, :, 2:5)
1000×4 SubDataFrame
Row │ Age Sex Job Housing
│ Int64 String7 Int64 String7
──────┼────────────────────────────────
1 │ 67 male 2 own
2 │ 22 female 2 own
3 │ 49 male 1 own
4 │ 45 male 2 free
5 │ 53 male 2 free
6 │ 35 male 1 free
7 │ 53 male 2 own
8 │ 35 male 3 rent
⋮ │ ⋮ ⋮ ⋮ ⋮
994 │ 30 male 3 own
995 │ 50 male 2 own
996 │ 31 female 1 own
997 │ 40 male 3 own
998 │ 38 male 2 own
999 │ 23 male 2 free
1000 │ 27 male 2 own
985 rows omitted
или с помощью макроса @view:
julia> @view german[end:-1:1, [1, 4]]
1000×2 SubDataFrame
Row │ id Job
│ Int64 Int64
──────┼──────────────
1 │ 999 2
2 │ 998 2
3 │ 997 2
4 │ 996 3
5 │ 995 1
6 │ 994 2
7 │ 993 3
8 │ 992 1
⋮ │ ⋮ ⋮
994 │ 6 2
995 │ 5 1
996 │ 4 2
997 │ 3 2
998 │ 2 1
999 │ 1 2
1000 │ 0 2
985 rows omitted
Аналогичным образом, можно получить представление одного столбца фрейма данных:
julia> @view german[1:5, 1]
5-element view(::Vector{Int64}, 1:5) with eltype Int64:
0
1
2
3
4
одной его ячейки:
julia> @view german[2, 2]
0-dimensional view(::Vector{Int64}, 2) with eltype Int64:
22
или одной строки:
julia> @view german[3, 2:5]
DataFrameRow
Row │ Age Sex Job Housing
│ Int64 String7 Int64 String7
─────┼────────────────────────────────
3 │ 49 male 1 own
Как видите, синтаксис индексирования строк и столбцов точно такой же. Единственное отличие в том, что создается не новый объект, а представление существующего.
Чтобы сравнить производительность при индексировании и создании представления, выполним следующий тест производительности с помощью пакета BenchmarkTools.jl (чтобы выполнить сравнение самостоятельно, установите его):
julia> using BenchmarkTools
julia> @btime $german[1:end-1, 1:end-1];
9.900 μs (44 allocations: 57.56 KiB)
julia> @btime @view $german[1:end-1, 1:end-1];
67.332 ns (2 allocations: 32 bytes)
Как видите, при создании представления:
-
скорость на порядок выше;
-
выделяется гораздо меньше памяти.
Однако у представлений есть и недостатки:
-
Представление указывает на ту же область памяти, что и родительский объект (поэтому при изменении представления меняется и родительский объект, что иногда нежелательно).
-
Некоторые операции могут выполняться немного медленнее (так как пакету DataFrames.jl приходится сопоставлять индексы представления с индексами родительского объекта).
Изменение данных, хранящихся во фрейме данных
Чтобы продемонстрировать выполнение операций с изменением применительно к фрейму данных, мы сначала создадим подмножество фрейма данных german:
julia> df1 = german[1:6, 2:4]
6×3 DataFrame
Row │ Age Sex Job
│ Int64 String7 Int64
─────┼───────────────────────
1 │ 67 male 2
2 │ 22 female 2
3 │ 49 male 1
4 │ 45 male 2
5 │ 53 male 2
6 │ 35 male 1
В следующем примере мы заменяем столбец :Age во фрейме данных df1 новым вектором:
julia> val = [80, 85, 98, 95, 78, 89]
6-element Vector{Int64}:
80
85
98
95
78
89
julia> df1.Age = val
6-element Vector{Int64}:
80
85
98
95
78
89
julia> df1
6×3 DataFrame
Row │ Age Sex Job
│ Int64 String7 Int64
─────┼───────────────────────
1 │ 80 male 2
2 │ 85 female 2
3 │ 98 male 1
4 │ 95 male 2
5 │ 78 male 2
6 │ 89 male 1
Это операция без копирования. Ее можно выполнить, только если длина val равна количеству строк в df1, или в особом случае, если в df1 нет столбцов.
julia> df1.Age === val # копирование не выполняется
true
Если при индексировании выбирается подмножество строк из фрейма данных, изменение выполняется на месте, то есть запись производится в существующий вектор. В этом примере элементам в столбце :Job и строках 1:3 присваиваются значения [2, 4, 6]:
julia> df1[1:3, :Job] = [2, 3, 2]
3-element Vector{Int64}:
2
3
2
julia> df1
6×3 DataFrame
Row │ Age Sex Job
│ Int64 String7 Int64
─────┼───────────────────────
1 │ 80 male 2
2 │ 85 female 3
3 │ 98 male 2
4 │ 95 male 2
5 │ 78 male 2
6 │ 89 male 1
Есть еще одно особое правило: при использовании ! в качестве селектора строк столбец заменяется без копирования (как в примере df1.Age = val выше). Например, здесь мы заменяем столбец :Sex:
julia> df1[!, :Sex] = ["male", "female", "female", "transgender", "female", "male"]
6-element Vector{String}:
"male"
"female"
"female"
"transgender"
"female"
"male"
julia> df1
6×3 DataFrame
Row │ Age Sex Job
│ Int64 String Int64
─────┼───────────────────────────
1 │ 80 male 2
2 │ 85 female 3
3 │ 98 female 2
4 │ 95 transgender 2
5 │ 78 female 2
6 │ 89 male 1
Присваивать значения можно не только выбранным строкам в одном столбце, но и выбранным столбцам в одной строке фрейма данных:
julia> df1[3, 1:3] = [78, "male", 4]
3-element Vector{Any}:
78
"male"
4
julia> df1
6×3 DataFrame
Row │ Age Sex Job
│ Int64 String Int64
─────┼───────────────────────────
1 │ 80 male 2
2 │ 85 female 3
3 │ 78 male 4
4 │ 95 transgender 2
5 │ 78 female 2
6 │ 89 male 1
Как уже упоминалось, DataFrameRow можно использовать для изменения родительского фрейма данных. Вот несколько примеров:
julia> dfr = df1[2, :] # DataFrameRow со второй строкой и всеми столбцами df1
DataFrameRow
Row │ Age Sex Job
│ Int64 String Int64
─────┼──────────────────────
2 │ 85 female 3
julia> dfr.Age = 98 # присваиваем значение `98` элементу в столбце `:Age` в строке `2` на месте
98
julia> dfr
DataFrameRow
Row │ Age Sex Job
│ Int64 String Int64
─────┼──────────────────────
2 │ 98 female 3
julia> dfr[2:3] = ["male", 2] # присваиваем значения элементам в столбцах `:Sex` и `:Job`
2-element Vector{Any}:
"male"
2
julia> dfr
DataFrameRow
Row │ Age Sex Job
│ Int64 String Int64
─────┼──────────────────────
2 │ 98 male 2
Эти операции изменили данные, хранящиеся во фрейме данных df1.
Аналогичным образом, представления можно использовать для изменения данных, хранящихся в родительском фрейме данных. Вот ряд примеров.
julia> sdf = view(df1, :, 2:3)
6×2 SubDataFrame
Row │ Sex Job
│ String Int64
─────┼────────────────────
1 │ male 2
2 │ male 2
3 │ male 4
4 │ transgender 2
5 │ female 2
6 │ male 1
julia> sdf[2, :Sex] = "female" # присваиваем значение `female` элементу в столбце `:Sex` во второй строке на месте
"female"
julia> sdf
6×2 SubDataFrame
Row │ Sex Job
│ String Int64
─────┼────────────────────
1 │ male 2
2 │ female 2
3 │ male 4
4 │ transgender 2
5 │ female 2
6 │ male 1
julia> sdf[6, 1:2] = ["female", 3]
2-element Vector{Any}:
"female"
3
julia> sdf
6×2 SubDataFrame
Row │ Sex Job
│ String Int64
─────┼────────────────────
1 │ male 2
2 │ female 2
3 │ male 4
4 │ transgender 2
5 │ female 2
6 │ female 3
Во всех этих случаях также изменился родительский объект представления sdf.
Присваивание с трансляцией
Помимо обычного присваивания, можно выполнять присваивание с трансляцией с помощью операции .=.
Прежде чем продолжить, давайте остановимся на том, как работает трансляция в Julia. Стандартный синтаксис для выполнения трансляции — точка (.). Например, в отличие от языка R, следующая операция завершится сбоем:
julia> s = [25, 26, 35, 56]
4-element Vector{Int64}:
25
26
35
56
julia> s[2:3] = 0
ERROR: ArgumentError: indexed assignment with a single value to possibly many locations is not supported; perhaps use broadcasting `.=` instead?
Вместо этого следует написать такой код:
julia> s[2:3] .= 0
2-element view(::Vector{Int64}, 2:3) with eltype Int64:
0
0
julia> s
4-element Vector{Int64}:
25
0
0
56
Аналогичный синтаксис полностью поддерживается в DataFrames.jl. Здесь столбец :Age заменяется новым размещенным в памяти вектором из-за присваивания с трансляцией:
julia> df1[!, :Age] .= [85, 89, 78, 58, 96, 68] # столбец `:Age` заменяется новым размещенным в памяти вектором
6-element Vector{Int64}:
85
89
78
58
96
68
julia> df1
6×3 DataFrame
Row │ Age Sex Job
│ Int64 String Int64
─────┼───────────────────────────
1 │ 85 male 2
2 │ 89 female 2
3 │ 78 male 4
4 │ 58 transgender 2
5 │ 96 female 2
6 │ 68 female 3
Если бы в примере выше вместо ! использовался символ :, произошло бы присваивание существующему столбцу на месте с трансляцией. Основное различие между выполняемой на месте операцией и операцией замены заключается в том, что замена столбцов требуется в том случае, если у новых значений тип отличается от старых.
В примерах ниже выполняются операции со столбцами :Customers и :City, отсутствующими в df1. В данном случае символы ! и : равносильны, и в памяти размещается новый столбец:
julia> df1[!, :Customers] .= ["Rohit", "Akshat", "Rahul", "Aayush", "Prateek", "Anam"]
6-element Vector{String}:
"Rohit"
"Akshat"
"Rahul"
"Aayush"
"Prateek"
"Anam"
julia> df1[:, :City] .= ["Kanpur", "Lucknow", "Bhuvneshwar", "Jaipur", "Ranchi", "Dehradoon"]
6-element Vector{String}:
"Kanpur"
"Lucknow"
"Bhuvneshwar"
"Jaipur"
"Ranchi"
"Dehradoon"
julia> df1
6×5 DataFrame
Row │ Age Sex Job Customers City
│ Int64 String Int64 String String
─────┼───────────────────────────────────────────────────
1 │ 85 male 2 Rohit Kanpur
2 │ 89 female 2 Akshat Lucknow
3 │ 78 male 4 Rahul Bhuvneshwar
4 │ 58 transgender 2 Aayush Jaipur
5 │ 96 female 2 Prateek Ranchi
6 │ 68 female 3 Anam Dehradoon
Чаще всего операция присваивания с трансляцией предполагает использование скаляра в правой части, например:
julia> df1[:, 3] .= 4 # замена значений, хранящихся в столбце номер 3, на 4 на месте
6-element view(::Vector{Int64}, :) with eltype Int64:
4
4
4
4
4
4
julia> df1
6×5 DataFrame
Row │ Age Sex Job Customers City
│ Int64 String Int64 String String
─────┼───────────────────────────────────────────────────
1 │ 85 male 4 Rohit Kanpur
2 │ 89 female 4 Akshat Lucknow
3 │ 78 male 4 Rahul Bhuvneshwar
4 │ 58 transgender 4 Aayush Jaipur
5 │ 96 female 4 Prateek Ranchi
6 │ 68 female 4 Anam Dehradoon
Для селектора строк : операция присваивания с трансляцией выполняется на месте, поэтому следующая операция выдает ошибку:
julia> df1[:, :Age] .= "Economics"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Int64
Вместо этого следует использовать !, так как при этом старый вектор заменяется новым размещенным в памяти:
julia> df1[!, :Age] .= "Economics"
6-element Vector{String}:
"Economics"
"Economics"
"Economics"
"Economics"
"Economics"
"Economics"
julia> df1
6×5 DataFrame
Row │ Age Sex Job Customers City
│ String String Int64 String String
─────┼───────────────────────────────────────────────────────
1 │ Economics male 4 Rohit Kanpur
2 │ Economics female 4 Akshat Lucknow
3 │ Economics male 4 Rahul Bhuvneshwar
4 │ Economics transgender 4 Aayush Jaipur
5 │ Economics female 4 Prateek Ranchi
6 │ Economics female 4 Anam Dehradoon
В DataFrames.jl есть ряд сценариев, когда поведение наподобие трансляции было бы естественным, но использование операции . не допускается. В таких случаях для удобства пользователя выполняется так называемая псевдотрансляция. Мы уже видели ее в примерах использования конструктора DataFrame. Ниже псевдотрансляция демонстрируется на примере функции insertcols!, которая вставляет столбец во фрейм данных в произвольной позиции.
В приведенном ниже примере с помощью функции insertcols! создается столбец :Country. Так как мы передаем скалярное значение "India" столбца, оно транслируется во все строки выходного фрейма данных:
julia> insertcols!(df1, 1, :Country => "India")
6×6 DataFrame
Row │ Country Age Sex Job Customers City
│ String String String Int64 String String
─────┼────────────────────────────────────────────────────────────────
1 │ India Economics male 4 Rohit Kanpur
2 │ India Economics female 4 Akshat Lucknow
3 │ India Economics male 4 Rahul Bhuvneshwar
4 │ India Economics transgender 4 Aayush Jaipur
5 │ India Economics female 4 Prateek Ranchi
6 │ India Economics female 4 Anam Dehradoon
Во втором аргументе функции insertcols! можно передать позицию, в которую нужно поместить вставляемый столбец:
julia> insertcols!(df1, 4, :b => exp(4))
6×7 DataFrame
Row │ Country Age Sex b Job Customers City ⋯
│ String String String Float64 Int64 String String ⋯
─────┼──────────────────────────────────────────────────────────────────────────
1 │ India Economics male 54.5982 4 Rohit Kanpur ⋯
2 │ India Economics female 54.5982 4 Akshat Lucknow
3 │ India Economics male 54.5982 4 Rahul Bhuvneshwar
4 │ India Economics transgender 54.5982 4 Aayush Jaipur
5 │ India Economics female 54.5982 4 Prateek Ranchi ⋯
6 │ India Economics female 54.5982 4 Anam Dehradoon
Селекторы столбцов Not, Between, Cols и All
Вы можете использовать селекторы Not, Between, Cols и All в более сложных сценариях выбора столбцов:
-
Селектор
Not(из пакета InvertedIndices.jl) позволяет указать столбцы, которые следует исключить из итогового фрейма данных. ВNotможно поместить любой другой допустимый селектор столбцов. -
Селектор
Betweenпозволяет указать диапазон столбцов (передать начальный и конечный столбцы можно с использованием любого селектора одного столбца). -
Селектор
Cols(...)выбирает объединение других селекторов, переданных в качестве его аргументов. -
All()позволяет выбрать все столбцы объектаDataFrame; это равносильно передаче:. -
Регулярное выражение для выбора столбцов с соответствующими условиям именами.
Рассмотрим ряд примеров этих селекторов.
Удаление столбца :Age:
julia> german[:, Not(:Age)]
1000×9 DataFrame
Row │ id Sex Job Housing Saving accounts Checking account Cre ⋯
│ Int64 String7 Int64 String7 String15 String15 Int ⋯
──────┼─────────────────────────────────────────────────────────────────────────
1 │ 0 male 2 own NA little ⋯
2 │ 1 female 2 own little moderate
3 │ 2 male 1 own little NA
4 │ 3 male 2 free little little
5 │ 4 male 2 free little little ⋯
6 │ 5 male 1 free NA NA
7 │ 6 male 2 own quite rich NA
8 │ 7 male 3 rent little moderate
⋮ │ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋱
994 │ 993 male 3 own little little ⋯
995 │ 994 male 2 own NA NA
996 │ 995 female 1 own little NA
997 │ 996 male 3 own little little
998 │ 997 male 2 own little NA ⋯
999 │ 998 male 2 free little little
1000 │ 999 male 2 own moderate moderate
3 columns and 985 rows omitted
Выбор столбцов, начиная с :Sex и заканчивая :Housing:
julia> german[:, Between(:Sex, :Housing)]
1000×3 DataFrame
Row │ Sex Job Housing
│ String Int64 String
──────┼────────────────────────
1 │ male 2 own
2 │ female 2 own
3 │ male 1 own
4 │ male 2 free
5 │ male 2 free
6 │ male 1 free
7 │ male 2 own
8 │ male 3 rent
⋮ │ ⋮ ⋮ ⋮
994 │ male 3 own
995 │ male 2 own
996 │ female 1 own
997 │ male 3 own
998 │ male 2 own
999 │ male 2 free
1000 │ male 2 own
985 rows omitted
В следующем примере селектор Cols выбирает объединение селекторов "Age" и Between("Sex", "Job"), переданных в качестве его аргументов:
julia> german[:, Cols("Age", Between("Sex", "Job"))]
1000×3 DataFrame
Row │ Age Sex Job
│ Int64 String7 Int64
──────┼───────────────────────
1 │ 67 male 2
2 │ 22 female 2
3 │ 49 male 1
4 │ 45 male 2
5 │ 53 male 2
6 │ 35 male 1
7 │ 53 male 2
8 │ 35 male 3
⋮ │ ⋮ ⋮ ⋮
994 │ 30 male 3
995 │ 50 male 2
996 │ 31 female 1
997 │ 40 male 3
998 │ 38 male 2
999 │ 23 male 2
1000 │ 27 male 2
985 rows omitted
Для выбора столбцов можно также использовать Regex (регулярное выражение). В следующем примере выбираются столбцы, в именах которых есть символ "S", а с помощью Not удаляется строка номер 5:
julia> german[Not(5), r"S"]
999×2 DataFrame
Row │ Sex Saving accounts
│ String7 String15
─────┼──────────────────────────
1 │ male NA
2 │ female little
3 │ male little
4 │ male little
5 │ male NA
6 │ male quite rich
7 │ male little
8 │ male rich
⋮ │ ⋮ ⋮
993 │ male little
994 │ male NA
995 │ female little
996 │ male little
997 │ male little
998 │ male little
999 │ male moderate
984 rows omitted
Базовое использование функций преобразования
В пакете DataFrames.jl есть пять функций, с помощью которых можно преобразовывать столбцы фрейма данных.
-
combine: создает фрейм данных, заполненный столбцами, которые являются результатом преобразования столбцов исходного фрейма данных, при необходимости объединяя строки. -
select: создает фрейм данных с тем же количеством строк, что и в исходном фрейме данных, заполняя его столбцами, которые являются результатом преобразования столбцов исходного фрейма данных. -
select!: действует аналогичноselect, но изменяет переданный фрейм данных на месте. -
transform: действует аналогичноselect, но сохраняет столбцы, уже имевшиеся во фрейме данных (обратите внимание, однако, что эти столбцы могут быть изменены преобразованием, переданным вtransform). -
transform!: действует аналогичноtransform, но изменяет переданный фрейм данных на месте.
Вот основные способы указания преобразования:
-
source_column => transformation => target_column_name. В этом случаеsource_columnпередается в качестве аргумента функцииtransformationи сохраняется в столбцеtarget_column_name. -
source_column => transformation. В этом случае функция преобразования применяется кsource_column, а имена целевых столбцов генерируются автоматически. -
source_column => target_column_nameпереименовываетsource_columnвtarget_column_name. -
source_column. В этом случае исходный столбец сохраняется в результате без какого-либо преобразования.
Эти правила обычно называются мини-языком преобразований.
Перейдем к примерам применения этих правил.
julia> using Statistics
julia> combine(german, :Age => mean => :mean_age)
1×1 DataFrame
Row │ mean_age
│ Float64
─────┼──────────
1 │ 35.546
julia> select(german, :Age => mean => :mean_age)
1000×1 DataFrame
Row │ mean_age
│ Float64
──────┼──────────
1 │ 35.546
2 │ 35.546
3 │ 35.546
4 │ 35.546
5 │ 35.546
6 │ 35.546
7 │ 35.546
8 │ 35.546
⋮ │ ⋮
994 │ 35.546
995 │ 35.546
996 │ 35.546
997 │ 35.546
998 │ 35.546
999 │ 35.546
1000 │ 35.546
985 rows omitted
Как видите, в обоих случаях функция mean была применена к столбцу :Age, а результат был сохранен в столбце :mean_age. Различие между функциями combine и select в том, что combine агрегирует данные и создает столько строк, сколько было возвращено функцией преобразования. В свою очередь, функция select всегда оставляет количество строк во фрейме данных таким же, как в исходном фрейме. Поэтому в данном случае результат функции mean транслируется.
Так как функция combine допускает создание любого количества строк в результате преобразования, при наличии комбинации преобразований, некоторые из которых создают вектор, а другие — скаляры, скаляры транслируются точно так же, как в select. Вот пример:
julia> combine(german, :Age => mean => :mean_age, :Housing => unique => :housing)
3×2 DataFrame
Row │ mean_age housing
│ Float64 String7
─────┼───────────────────
1 │ 35.546 own
2 │ 35.546 free
3 │ 35.546 rent
Обратите внимание, однако, что разные преобразования не могут возвращать векторы разной длины:
julia> combine(german, :Age, :Housing => unique => :Housing)
ERROR: ArgumentError: New columns must have the same length as old columns
Рассмотрим еще несколько примеров использования функции select. Зачастую бывает нужно применить какую-либо функцию не ко всему столбцу фрейма данных, а к его отдельным элементам. Обычно этого можно достичь посредством трансляции:
julia> select(german, :Sex => (x -> uppercase.(x)) => :Sex)
1000×1 DataFrame
Row │ Sex
│ String
──────┼────────
1 │ MALE
2 │ FEMALE
3 │ MALE
4 │ MALE
5 │ MALE
6 │ MALE
7 │ MALE
8 │ MALE
⋮ │ ⋮
994 │ MALE
995 │ MALE
996 │ FEMALE
997 │ MALE
998 │ MALE
999 │ MALE
1000 │ MALE
985 rows omitted
Такой шаблон часто встречается на практике, поэтому для функции существует удобная оболочка ByRow, создающая ее транслируемый вариант. В этих примерах ByRow — это особый тип, используемый в операциях выборки для указания на то, что заключенная функция должна применяться к каждому элементу (строке) выборки. Здесь мы передаем оболочку ByRow в целевое имя столбца :Sex с помощью функции uppercase:
julia> select(german, :Sex => ByRow(uppercase) => :SEX)
1000×1 DataFrame
Row │ SEX
│ String
──────┼────────
1 │ MALE
2 │ FEMALE
3 │ MALE
4 │ MALE
5 │ MALE
6 │ MALE
7 │ MALE
8 │ MALE
⋮ │ ⋮
994 │ MALE
995 │ MALE
996 │ FEMALE
997 │ MALE
998 │ MALE
999 │ MALE
1000 │ MALE
985 rows omitted
В этом случае мы преобразовываем исходный столбец :Age с помощью оболочки ByRow и автоматически генерируем имя целевого столбца:
julia> select(german, :Age, :Age => ByRow(sqrt))
1000×2 DataFrame
Row │ Age Age_sqrt
│ Int64 Float64
──────┼─────────────────
1 │ 67 8.18535
2 │ 22 4.69042
3 │ 49 7.0
4 │ 45 6.7082
5 │ 53 7.28011
6 │ 35 5.91608
7 │ 53 7.28011
8 │ 35 5.91608
⋮ │ ⋮ ⋮
994 │ 30 5.47723
995 │ 50 7.07107
996 │ 31 5.56776
997 │ 40 6.32456
998 │ 38 6.16441
999 │ 23 4.79583
1000 │ 27 5.19615
985 rows omitted
Если передается только столбец (без части =>), можно использовать любой селектор столбцов, допускаемый при индексировании.
Здесь мы исключаем столбец :Age из итогового фрейма данных:
julia> select(german, Not(:Age))
1000×9 DataFrame
Row │ id Sex Job Housing Saving accounts Checking account Cre ⋯
│ Int64 String7 Int64 String7 String15 String15 Int ⋯
──────┼─────────────────────────────────────────────────────────────────────────
1 │ 0 male 2 own NA little ⋯
2 │ 1 female 2 own little moderate
3 │ 2 male 1 own little NA
4 │ 3 male 2 free little little
5 │ 4 male 2 free little little ⋯
6 │ 5 male 1 free NA NA
7 │ 6 male 2 own quite rich NA
8 │ 7 male 3 rent little moderate
⋮ │ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋱
994 │ 993 male 3 own little little ⋯
995 │ 994 male 2 own NA NA
996 │ 995 female 1 own little NA
997 │ 996 male 3 own little little
998 │ 997 male 2 own little NA ⋯
999 │ 998 male 2 free little little
1000 │ 999 male 2 own moderate moderate
3 columns and 985 rows omitted
В следующем примере мы удаляем столбцы "Age", "Saving accounts", "Checking account", "Credit amount" и "Purpose". Обратите внимание, что на этот раз применяются строковые селекторы столбцов, потому что в именах некоторых столбцов есть пробелы:
julia> select(german, Not(["Age", "Saving accounts", "Checking account",
"Credit amount", "Purpose"]))
1000×5 DataFrame
Row │ id Sex Job Housing Duration
│ Int64 String7 Int64 String7 Int64
──────┼──────────────────────────────────────────
1 │ 0 male 2 own 6
2 │ 1 female 2 own 48
3 │ 2 male 1 own 12
4 │ 3 male 2 free 42
5 │ 4 male 2 free 24
6 │ 5 male 1 free 36
7 │ 6 male 2 own 24
8 │ 7 male 3 rent 36
⋮ │ ⋮ ⋮ ⋮ ⋮ ⋮
994 │ 993 male 3 own 36
995 │ 994 male 2 own 12
996 │ 995 female 1 own 12
997 │ 996 male 3 own 30
998 │ 997 male 2 own 12
999 │ 998 male 2 free 45
1000 │ 999 male 2 own 45
985 rows omitted
В качестве еще одного примера покажем использование приводившегося ранее регулярного выражения r"S" с функцией select:
julia> select(german, r"S")
1000×2 DataFrame
Row │ Sex Saving accounts
│ String7 String15
──────┼──────────────────────────
1 │ male NA
2 │ female little
3 │ male little
4 │ male little
5 │ male little
6 │ male NA
7 │ male quite rich
8 │ male little
⋮ │ ⋮ ⋮
994 │ male little
995 │ male NA
996 │ female little
997 │ male little
998 │ male little
999 │ male little
1000 │ male moderate
985 rows omitted
Преимущество функции select или combine по сравнению с индексированием заключается в том, что так проще получить объединение нескольких селекторов столбцов, например:
julia> select(german, r"S", "Job", 1)
1000×4 DataFrame
Row │ Sex Saving accounts Job id
│ String7 String15 Int64 Int64
──────┼────────────────────────────────────────
1 │ male NA 2 0
2 │ female little 2 1
3 │ male little 1 2
4 │ male little 2 3
5 │ male little 2 4
6 │ male NA 1 5
7 │ male quite rich 2 6
8 │ male little 3 7
⋮ │ ⋮ ⋮ ⋮ ⋮
994 │ male little 3 993
995 │ male NA 2 994
996 │ female little 1 995
997 │ male little 3 996
998 │ male little 2 997
999 │ male little 2 998
1000 │ male moderate 2 999
985 rows omitted
Такая гибкость реализуется в следующем идиоматическом шаблоне для перемещения одного из столбцов в начало фрейма данных:
julia> select(german, "Sex", :)
1000×10 DataFrame
Row │ Sex id Age Job Housing Saving accounts Checking accou ⋯
│ String7 Int64 Int64 Int64 String7 String15 String15 ⋯
──────┼─────────────────────────────────────────────────────────────────────────
1 │ male 0 67 2 own NA little ⋯
2 │ female 1 22 2 own little moderate
3 │ male 2 49 1 own little NA
4 │ male 3 45 2 free little little
5 │ male 4 53 2 free little little ⋯
6 │ male 5 35 1 free NA NA
7 │ male 6 53 2 own quite rich NA
8 │ male 7 35 3 rent little moderate
⋮ │ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋱
994 │ male 993 30 3 own little little ⋯
995 │ male 994 50 2 own NA NA
996 │ female 995 31 1 own little NA
997 │ male 996 40 3 own little little
998 │ male 997 38 2 own little NA ⋯
999 │ male 998 23 2 free little little
1000 │ male 999 27 2 own moderate moderate
4 columns and 985 rows omitted
Ниже мы просто передаем исходный столбец и целевое имя столбца для переименования (не указывая преобразование):
julia> select(german, :Sex => :x1, :Age => :x2)
1000×2 DataFrame
Row │ x1 x2
│ String7 Int64
──────┼────────────────
1 │ male 67
2 │ female 22
3 │ male 49
4 │ male 45
5 │ male 53
6 │ male 35
7 │ male 53
8 │ male 35
⋮ │ ⋮ ⋮
994 │ male 30
995 │ male 50
996 │ female 31
997 │ male 40
998 │ male 38
999 │ male 23
1000 │ male 27
985 rows omitted
Важно отметить, что select всегда возвращает фрейм данных, даже если выбран один столбец, в отличие от синтаксиса индексирования. Сравним следующие варианты:
julia> select(german, :Age)
1000×1 DataFrame
Row │ Age
│ Int64
──────┼───────
1 │ 67
2 │ 22
3 │ 49
4 │ 45
5 │ 53
6 │ 35
7 │ 53
8 │ 35
⋮ │ ⋮
994 │ 30
995 │ 50
996 │ 31
997 │ 40
998 │ 38
999 │ 23
1000 │ 27
985 rows omitted
julia> german[:, :Age]
1000-element Vector{Int64}:
67
22
49
45
53
35
53
35
61
28
⋮
34
23
30
50
31
40
38
23
27
По умолчанию select копирует столбцы переданного исходного фрейма данных. Чтобы копирование не выполнялось, передайте именованный аргумент copycols=false:
julia> df = select(german, :Sex)
1000×1 DataFrame
Row │ Sex
│ String7
──────┼─────────
1 │ male
2 │ female
3 │ male
4 │ male
5 │ male
6 │ male
7 │ male
8 │ male
⋮ │ ⋮
994 │ male
995 │ male
996 │ female
997 │ male
998 │ male
999 │ male
1000 │ male
985 rows omitted
julia> df.Sex === german.Sex # копирование выполняется
false
julia> df = select(german, :Sex, copycols=false)
1000×1 DataFrame
Row │ Sex
│ String7
──────┼─────────
1 │ male
2 │ female
3 │ male
4 │ male
5 │ male
6 │ male
7 │ male
8 │ male
⋮ │ ⋮
994 │ male
995 │ male
996 │ female
997 │ male
998 │ male
999 │ male
1000 │ male
985 rows omitted
julia> df.Sex === german.Sex # копирование не выполняется
true
Чтобы выполнить операцию выбора на месте, используйте select!:
julia> select!(german, Not(:Age));
julia> german
1000×9 DataFrame
Row │ id Sex Job Housing Saving accounts Checking account Cre ⋯
│ Int64 String7 Int64 String7 String15 String15 Int ⋯
──────┼─────────────────────────────────────────────────────────────────────────
1 │ 0 male 2 own NA little ⋯
2 │ 1 female 2 own little moderate
3 │ 2 male 1 own little NA
4 │ 3 male 2 free little little
5 │ 4 male 2 free little little ⋯
6 │ 5 male 1 free NA NA
7 │ 6 male 2 own quite rich NA
8 │ 7 male 3 rent little moderate
⋮ │ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋱
994 │ 993 male 3 own little little ⋯
995 │ 994 male 2 own NA NA
996 │ 995 female 1 own little NA
997 │ 996 male 3 own little little
998 │ 997 male 2 own little NA ⋯
999 │ 998 male 2 free little little
1000 │ 999 male 2 own moderate moderate
3 columns and 985 rows omitted
Как видите, столбец :Age был удален из фрейма данных german.
Функции transform и transform! работают так же, как select и select!, с той лишь разницей, что они сохраняют все столбцы, которые присутствуют в исходном фрейме данных. Вот ряд примеров.
julia> german = copy(german_ref);
julia> df = german_ref[1:8, 1:5]
8×5 DataFrame
Row │ id Age Sex Job Housing
│ Int64 Int64 String7 Int64 String7
─────┼───────────────────────────────────────
1 │ 0 67 male 2 own
2 │ 1 22 female 2 own
3 │ 2 49 male 1 own
4 │ 3 45 male 2 free
5 │ 4 53 male 2 free
6 │ 5 35 male 1 free
7 │ 6 53 male 2 own
8 │ 7 35 male 3 rent
julia> transform(df, :Age => maximum)
8×6 DataFrame
Row │ id Age Sex Job Housing Age_maximum
│ Int64 Int64 String7 Int64 String7 Int64
─────┼────────────────────────────────────────────────────
1 │ 0 67 male 2 own 67
2 │ 1 22 female 2 own 67
3 │ 2 49 male 1 own 67
4 │ 3 45 male 2 free 67
5 │ 4 53 male 2 free 67
6 │ 5 35 male 1 free 67
7 │ 6 53 male 2 own 67
8 │ 7 35 male 3 rent 67
В приведенном ниже примере мы меняем местами значения, хранящиеся в столбцах :Sex и :Age:
julia> transform(german, :Age => :Sex, :Sex => :Age)
1000×10 DataFrame
Row │ id Age Sex Job Housing Saving accounts Checking accou ⋯
│ Int64 String7 Int64 Int64 String7 String15 String15 ⋯
──────┼─────────────────────────────────────────────────────────────────────────
1 │ 0 male 67 2 own NA little ⋯
2 │ 1 female 22 2 own little moderate
3 │ 2 male 49 1 own little NA
4 │ 3 male 45 2 free little little
5 │ 4 male 53 2 free little little ⋯
6 │ 5 male 35 1 free NA NA
7 │ 6 male 53 2 own quite rich NA
8 │ 7 male 35 3 rent little moderate
⋮ │ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋱
994 │ 993 male 30 3 own little little ⋯
995 │ 994 male 50 2 own NA NA
996 │ 995 female 31 1 own little NA
997 │ 996 male 40 3 own little little
998 │ 997 male 38 2 own little NA ⋯
999 │ 998 male 23 2 free little little
1000 │ 999 male 27 2 own moderate moderate
4 columns and 985 rows omitted
Если для преобразования необходимо предоставить несколько исходных столбцов, они передаются как последовательность позиционных аргументов. Например, следующее преобразование [:Age, :Job] => (+) => :res вычисляет +(df1.Age, df1.Job) (то есть складывает два столбца) и сохраняет результат в столбце :res:
julia> select(german, :Age, :Job, [:Age, :Job] => (+) => :res)
1000×3 DataFrame
Row │ Age Job res
│ Int64 Int64 Int64
──────┼─────────────────────
1 │ 67 2 69
2 │ 22 2 24
3 │ 49 1 50
4 │ 45 2 47
5 │ 53 2 55
6 │ 35 1 36
7 │ 53 2 55
8 │ 35 3 38
⋮ │ ⋮ ⋮ ⋮
994 │ 30 3 33
995 │ 50 2 52
996 │ 31 1 32
997 │ 40 3 43
998 │ 38 2 40
999 │ 23 2 25
1000 │ 27 2 29
985 rows omitted
В примерах в этом вводном руководстве были охвачены не все возможности мини-языка преобразований. Более сложные примеры, включая передачу или получение нескольких столбцов с помощью операции AsTable (с которой вы, возможно, уже встречались в некоторых демонстрациях DataFrames.jl), приводятся в дальнейших разделах.