Соединения в стиле базы данных
Вводные сведения о соединениях
Чтобы получить полное представление об изучаемой теме, часто требуется объединить два или более наборов данных. Например, предположим, что у нас есть два следующих набора данных:
julia> using DataFrames
julia> people = DataFrame(ID=[20, 40], Name=["John Doe", "Jane Doe"])
2×2 DataFrame
Row │ ID Name
│ Int64 String
─────┼─────────────────
1 │ 20 John Doe
2 │ 40 Jane Doe
julia> jobs = DataFrame(ID=[20, 40], Job=["Lawyer", "Doctor"])
2×2 DataFrame
Row │ ID Job
│ Int64 String
─────┼───────────────
1 │ 20 Lawyer
2 │ 40 Doctor
Может понадобиться использовать больший набор данных, содержащий имена и должности для каждого идентификатора. Это можно сделать с помощью функции innerjoin
:
julia> innerjoin(people, jobs, on = :ID)
2×3 DataFrame
Row │ ID Name Job
│ Int64 String String
─────┼─────────────────────────
1 │ 20 John Doe Lawyer
2 │ 40 Jane Doe Doctor
В теории реляционных баз данных эта операция обычно называется соединением. Столбцы, используемые для определения строки, которые должны быть объединены в процессе соединения, называются ключами.
Для выполнения семи видов соединений существуют следующие функции.
-
innerjoin
: вывод содержит строки для значений ключа, которые существуют во всех переданных фреймах данных. -
leftjoin
: вывод содержит строки для значений ключа, которые существуют в первом (левом) аргументе независимо от того, существует ли это значение во втором (правом) аргументе. -
rightjoin
: вывод содержит строки для значений ключа, которые существуют во втором (правом) аргументе независимо от того, существует ли это значение в первом (левом) аргументе. -
outerjoin
: вывод содержит строки для значений ключа, которые существуют в любом переданном фрейме данных. -
semijoin
: аналогично внутреннему соединению, но вывод ограничен столбцами из первого (левого) аргумента. -
antijoin
: вывод содержит строки для значений ключа, которые существуют в первом (левом), но не во втором (правом) аргументе. Как и в случае сsemijoin
, вывод ограничен столбцами из первого (левого) аргумента. -
crossjoin
: вывод представляет собой декартово произведение строк из всех переданных фреймов данных.
Дополнительные сведения см. на странице Википедии о соединениях SQL.
Вот примеры различных видов соединений.
julia> jobs = DataFrame(ID=[20, 60], Job=["Lawyer", "Astronaut"])
2×2 DataFrame
Row │ ID Job
│ Int64 String
─────┼──────────────────
1 │ 20 Lawyer
2 │ 60 Astronaut
julia> innerjoin(people, jobs, on = :ID)
1×3 DataFrame
Row │ ID Name Job
│ Int64 String String
─────┼─────────────────────────
1 │ 20 John Doe Lawyer
julia> leftjoin(people, jobs, on = :ID)
2×3 DataFrame
Row │ ID Name Job
│ Int64 String String?
─────┼──────────────────────────
1 │ 20 John Doe Lawyer
2 │ 40 Jane Doe missing
julia> rightjoin(people, jobs, on = :ID)
2×3 DataFrame
Row │ ID Name Job
│ Int64 String? String
─────┼────────────────────────────
1 │ 20 John Doe Lawyer
2 │ 60 missing Astronaut
julia> outerjoin(people, jobs, on = :ID)
3×3 DataFrame
Row │ ID Name Job
│ Int64 String? String?
─────┼────────────────────────────
1 │ 20 John Doe Lawyer
2 │ 40 Jane Doe missing
3 │ 60 missing Astronaut
julia> semijoin(people, jobs, on = :ID)
1×2 DataFrame
Row │ ID Name
│ Int64 String
─────┼─────────────────
1 │ 20 John Doe
julia> antijoin(people, jobs, on = :ID)
1×2 DataFrame
Row │ ID Name
│ Int64 String
─────┼─────────────────
1 │ 40 Jane Doe
Перекрестные соединения — это единственный вид соединения, в котором не используется ключ on
:
julia> crossjoin(people, jobs, makeunique = true)
4×4 DataFrame
Row │ ID Name ID_1 Job
│ Int64 String Int64 String
─────┼───────────────────────────────────
1 │ 20 John Doe 20 Lawyer
2 │ 20 John Doe 60 Astronaut
3 │ 40 Jane Doe 20 Lawyer
4 │ 40 Jane Doe 60 Astronaut
Сравнение значений ключей и значений с плавающей запятой
Значения ключей из двух или более фреймов данных сравниваются с помощью функции isequal
. Это соответствует типам Set
и Dict
в Julia Base.
Не рекомендуется использовать в качестве ключей числа с плавающей запятой: сравнения со значениями с плавающей запятой могут быть неожиданными и непредсказуемыми. Если вы используете ключи с плавающей запятой, обратите внимание, что по умолчанию возникает ошибка, когда ключи включают значения -0.0
(отрицательный ноль) или NaN
. Вот пример:
julia> innerjoin(DataFrame(id=[-0.0]), DataFrame(id=[0.0]), on=:id)
ERROR: ArgumentError: Currently for numeric values `NaN` and `-0.0` in their real or imaginary components are not allowed. Such value was found in column :id in left data frame. Use CategoricalArrays.jl to wrap these values in a CategoricalVector to perform the requested join.
Это поведение можно переопределить, заключив значения ключей в категориальный вектор.
Соединение по столбцам ключей с разными именами
Чтобы соединить фреймы данных по ключам, которые имеют разные имена в левой и правой таблицах, можно передать пары left => right
в качестве аргумента on
:
julia> a = DataFrame(ID=[20, 40], Name=["John Doe", "Jane Doe"])
2×2 DataFrame
Row │ ID Name
│ Int64 String
─────┼─────────────────
1 │ 20 John Doe
2 │ 40 Jane Doe
julia> b = DataFrame(IDNew=[20, 40], Job=["Lawyer", "Doctor"])
2×2 DataFrame
Row │ IDNew Job
│ Int64 String
─────┼───────────────
1 │ 20 Lawyer
2 │ 40 Doctor
julia> innerjoin(a, b, on = :ID => :IDNew)
2×3 DataFrame
Row │ ID Name Job
│ Int64 String String
─────┼─────────────────────────
1 │ 20 John Doe Lawyer
2 │ 40 Jane Doe Doctor
Вот еще один пример с несколькими столбцами.
julia> a = DataFrame(City=["Amsterdam", "London", "London", "New York", "New York"],
Job=["Lawyer", "Lawyer", "Lawyer", "Doctor", "Doctor"],
Category=[1, 2, 3, 4, 5])
5×3 DataFrame
Row │ City Job Category
│ String String Int64
─────┼─────────────────────────────
1 │ Amsterdam Lawyer 1
2 │ London Lawyer 2
3 │ London Lawyer 3
4 │ New York Doctor 4
5 │ New York Doctor 5
julia> b = DataFrame(Location=["Amsterdam", "London", "London", "New York", "New York"],
Work=["Lawyer", "Lawyer", "Lawyer", "Doctor", "Doctor"],
Name=["a", "b", "c", "d", "e"])
5×3 DataFrame
Row │ Location Work Name
│ String String String
─────┼───────────────────────────
1 │ Amsterdam Lawyer a
2 │ London Lawyer b
3 │ London Lawyer c
4 │ New York Doctor d
5 │ New York Doctor e
julia> innerjoin(a, b, on = [:City => :Location, :Job => :Work])
9×4 DataFrame
Row │ City Job Category Name
│ String String Int64 String
─────┼─────────────────────────────────────
1 │ Amsterdam Lawyer 1 a
2 │ London Lawyer 2 b
3 │ London Lawyer 3 b
4 │ London Lawyer 2 c
5 │ London Lawyer 3 c
6 │ New York Doctor 4 d
7 │ New York Doctor 5 d
8 │ New York Doctor 4 e
9 │ New York Doctor 5 e
Обработка повторяющихся ключей и отслеживание исходного фрейма данных
Кроме того, обратите внимание, что в последнем соединении строки 2 и 3 имели одинаковые значения переменных on
в обоих соединенных DataFrame
. В такой ситуации innerjoin
, outerjoin
, leftjoin
и rightjoin
выдадут все комбинации совпадающих строк. В нашем примере в результате были созданы строки от 2 до 5. Такое же поведение можно наблюдать для строк 4 и 5 в обоих соединенных DataFrame
.
Чтобы проверить, что столбцы, переданные в качестве аргумента on
, определяют уникальные ключи (согласно isequal
) в каждом входном фрейме данных, можно задать именованный аргумент validate
в виде двухэлементного кортежа или пары значений Bool
, каждый элемент которого указывает, следует ли запускать проверку для соответствующего фрейма данных. Вот пример операции соединения, описанной выше.
julia> innerjoin(a, b, on = [(:City => :Location), (:Job => :Work)], validate=(true, true))
ERROR: ArgumentError: Merge key(s) are not unique in both df1 and df2. df1 contains 2 duplicate keys: (City = "London", Job = "Lawyer") and (City = "New York", Job = "Doctor"). df2 contains 2 duplicate keys: (Location = "London", Work = "Lawyer") and (Location = "New York", Work = "Doctor").
Наконец, используя именованный аргумент source
, вы можете добавить в результирующий фрейм данных столбец, указывающий, появилась ли данная строка только в левом, правом или в обоих фреймах данных. Вот пример:
julia> a = DataFrame(ID=[20, 40], Name=["John", "Jane"])
2×2 DataFrame
Row │ ID Name
│ Int64 String
─────┼───────────────
1 │ 20 John
2 │ 40 Jane
julia> b = DataFrame(ID=[20, 60], Job=["Lawyer", "Doctor"])
2×2 DataFrame
Row │ ID Job
│ Int64 String
─────┼───────────────
1 │ 20 Lawyer
2 │ 60 Doctor
julia> outerjoin(a, b, on=:ID, validate=(true, true), source=:source)
3×4 DataFrame
Row │ ID Name Job source
│ Int64 String? String? String
─────┼─────────────────────────────────────
1 │ 20 John Lawyer both
2 │ 40 Jane missing left_only
3 │ 60 missing Doctor right_only
Обратите внимание, что в этот раз мы также использовали именованный аргумент validate
, и это не привело к ошибкам, поскольку ключи, определенные в обоих исходных фреймах данных, были уникальными.
Переименование соединенных столбцов
Часто требуется отслеживать исходный фрейм данных. Эта возможность поддерживается с помощью именованного аргумента renamecols
:
julia> innerjoin(a, b, on=:ID, renamecols = "_left" => "_right")
1×3 DataFrame
Row │ ID Name_left Job_right
│ Int64 String String
─────┼─────────────────────────────
1 │ 20 John Lawyer
В приведенном выше примере мы добавили суффикс "_left"
к столбцам без ключей из левой таблицы и суффикс "_right"
к столбцам без ключей из правой таблицы.
В качестве альтернативы допускается передача функции, преобразующей имена столбцов:
julia> innerjoin(a, b, on=:ID, renamecols = lowercase => uppercase)
1×3 DataFrame
Row │ ID name JOB
│ Int64 String String
─────┼───────────────────────
1 │ 20 John Lawyer
Сопоставление отсутствующих значений в соединениях
По умолчанию при попытке выполнить соединение по ключу, имеющему отсутствующие (missing
) значения, возникает ошибка:
julia> df1 = DataFrame(id=[1, missing, 3], a=1:3)
3×2 DataFrame
Row │ id a
│ Int64? Int64
─────┼────────────────
1 │ 1 1
2 │ missing 2
3 │ 3 3
julia> df2 = DataFrame(id=[1, 2, missing], b=1:3)
3×2 DataFrame
Row │ id b
│ Int64? Int64
─────┼────────────────
1 │ 1 1
2 │ 2 2
3 │ missing 3
julia> innerjoin(df1, df2, on=:id)
ERROR: ArgumentError: Missing values in key columns are not allowed when matchmissing == :error. `missing` found in column :id in left data frame.
Если вы хотите, чтобы значения missing
рассматривались как равные, передайте именованный аргумент matchmissing=:equal
:
julia> innerjoin(df1, df2, on=:id, matchmissing=:equal)
2×3 DataFrame
Row │ id a b
│ Int64? Int64 Int64
─────┼───────────────────────
1 │ 1 1 1
2 │ missing 2 3
Или же можно отбросить все строки со значениями missing
. В этом случае передайте matchmissing=:notequal
:
julia> innerjoin(df1, df2, on=:id, matchmissing=:notequal)
1×3 DataFrame
Row │ id a b
│ Int64? Int64 Int64
─────┼──────────────────────
1 │ 1 1 1
Указание порядка строк в результате соединения
По умолчанию порядок строк, получаемых в результате операции соединения, не определен:
julia> df_left = DataFrame(id=[1, 2, 4, 5], left=1:4)
4×2 DataFrame
Row │ id left
│ Int64 Int64
─────┼──────────────
1 │ 1 1
2 │ 2 2
3 │ 4 3
4 │ 5 4
julia> df_right = DataFrame(id=[2, 1, 3, 6, 7], right=1:5)
5×2 DataFrame
Row │ id right
│ Int64 Int64
─────┼──────────────
1 │ 2 1
2 │ 1 2
3 │ 3 3
4 │ 6 4
5 │ 7 5
julia> outerjoin(df_left, df_right, on=:id)
7×3 DataFrame
Row │ id left right
│ Int64 Int64? Int64?
─────┼─────────────────────────
1 │ 2 2 1
2 │ 1 1 2
3 │ 4 3 missing
4 │ 5 4 missing
5 │ 3 missing 3
6 │ 6 missing 4
7 │ 7 missing 5
Чтобы в результате сохранился порядок строк левой таблицы, передайте именованный аргумент order=:left
:
julia> outerjoin(df_left, df_right, on=:id, order=:left)
7×3 DataFrame
Row │ id left right
│ Int64 Int64? Int64?
─────┼─────────────────────────
1 │ 1 1 2
2 │ 2 2 1
3 │ 4 3 missing
4 │ 5 4 missing
5 │ 3 missing 3
6 │ 6 missing 4
7 │ 7 missing 5
Обратите внимание, что в этом случае ключи, отсутствующие в левой таблице, помещаются после ключей, присутствующих в ней.
Аналогично order=:right
сохраняет порядок правой таблицы (и помещает ключи, отсутствующие в ней, в конец):
julia> outerjoin(df_left, df_right, on=:id, order=:right)
7×3 DataFrame
Row │ id left right
│ Int64 Int64? Int64?
─────┼─────────────────────────
1 │ 2 2 1
2 │ 1 1 2
3 │ 3 missing 3
4 │ 6 missing 4
5 │ 7 missing 5
6 │ 4 3 missing
7 │ 5 4 missing
Левое соединение на месте
Распространенной операцией является добавление данных из эталонной таблицы в основную таблицу. Такое обновление на месте можно выполнить с помощью функции leftjoin!
. В этом случае левая таблица обновляется на месте соответствующими строками из правой таблицы.
julia> main = DataFrame(id=1:4, main=1:4)
4×2 DataFrame
Row │ id main
│ Int64 Int64
─────┼──────────────
1 │ 1 1
2 │ 2 2
3 │ 3 3
4 │ 4 4
julia> leftjoin!(main, DataFrame(id=[2, 4], info=["a", "b"]), on=:id);
julia> main
4×3 DataFrame
Row │ id main info
│ Int64 Int64 String?
─────┼───────────────────────
1 │ 1 1 missing
2 │ 2 2 a
3 │ 3 3 missing
4 │ 4 4 b
Обратите внимание, что при этом порядок и количество строк в левой таблице не изменяется. Поэтому, в частности, не допускается наличие повторяющихся ключей в правой таблице:
julia> leftjoin!(main, DataFrame(id=[2, 2], info_bad=["a", "b"]), on=:id) ERROR: ArgumentError: duplicate rows found in right table