Начало работы с 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), приводятся в дальнейших разделах.