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

Начало работы с 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 позволяет для удобства использовать символы Symbol (например, :id) и строки (например, "id") для всех операций индексирования столбцов. Символы Symbol дают небольшой выигрыш в скорости, но со строками проще работать, когда в именах столбцов есть нестандартные символы или с этими именами нужно производить какие-либо операции.

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