До этого мы работали с одной таблицей. Но это бывает очень редко, так как данные хранятся в разных таблицах. Поэтому важно уметь комбинировать их. Все эти таблицы связаны друг с другом и называются реляционными данными. Реляционные данные - совокупность взаимосвязанных таблиц. Такие данные хранятся в СУБД(система управления базами данных). Для их управления используется SQL. То, что мы будем проходить на этом занятии очень похоже на SQL, но немного проще. Все нужные нам функции находятся в пакете dplyr
.
Мы рассмотрим как работать с реляционными данными на примере знакомых нам данных из пакета nycflights13
.
Главной таблицей в этом пакете является flights
, которую мы использовали на предыдущем занятии. Также имеется 4 таблице, связанных с ней:
airlines
- позволяет найти полное название перевозчика по его сокращенному коду.airports
- предоставляет информацию о каждом аэропорте, идентифицируемом с помощью кода faa
.planes
- информация о каждом самолете, идентифицируемом по его бортовому номеру.weather
- предоставляет почасовые сведения о погодных условиях в каждом из аэропортов Нью-Йорка.## # A tibble: 6 x 2
## carrier name
## <chr> <chr>
## 1 9E Endeavor Air Inc.
## 2 AA American Airlines Inc.
## 3 AS Alaska Airlines Inc.
## 4 B6 JetBlue Airways
## 5 DL Delta Air Lines Inc.
## 6 EV ExpressJet Airlines Inc.
## # A tibble: 6 x 8
## faa name lat lon alt tz dst tzone
## <chr> <chr> <dbl> <dbl> <dbl> <dbl> <chr> <chr>
## 1 04G Lansdowne Airport 41.1 -80.6 1044 -5 A America/New…
## 2 06A Moton Field Municipal A… 32.5 -85.7 264 -6 A America/Chi…
## 3 06C Schaumburg Regional 42.0 -88.1 801 -6 A America/Chi…
## 4 06N Randall Airport 41.4 -74.4 523 -5 A America/New…
## 5 09J Jekyll Island Airport 31.1 -81.4 11 -5 A America/New…
## 6 0A9 Elizabethton Municipal … 36.4 -82.2 1593 -5 A America/New…
## # A tibble: 6 x 9
## tailnum year type manufacturer model engines seats speed engine
## <chr> <int> <chr> <chr> <chr> <int> <int> <int> <chr>
## 1 N10156 2004 Fixed win… EMBRAER EMB-1… 2 55 NA Turbo…
## 2 N102UW 1998 Fixed win… AIRBUS INDUST… A320-… 2 182 NA Turbo…
## 3 N103US 1999 Fixed win… AIRBUS INDUST… A320-… 2 182 NA Turbo…
## 4 N104UW 1999 Fixed win… AIRBUS INDUST… A320-… 2 182 NA Turbo…
## 5 N10575 2002 Fixed win… EMBRAER EMB-1… 2 55 NA Turbo…
## 6 N105UW 1999 Fixed win… AIRBUS INDUST… A320-… 2 182 NA Turbo…
## # A tibble: 6 x 15
## origin year month day hour temp dewp humid wind_dir wind_speed
## <chr> <int> <int> <int> <int> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 EWR 2013 1 1 1 39.0 26.1 59.4 270 10.4
## 2 EWR 2013 1 1 2 39.0 27.0 61.6 250 8.06
## 3 EWR 2013 1 1 3 39.0 28.0 64.4 240 11.5
## 4 EWR 2013 1 1 4 39.9 28.0 62.2 250 12.7
## 5 EWR 2013 1 1 5 39.0 28.0 64.4 260 12.7
## 6 EWR 2013 1 1 6 37.9 28.0 67.2 240 11.5
## # … with 5 more variables: wind_gust <dbl>, precip <dbl>, pressure <dbl>,
## # visib <dbl>, time_hour <dttm>
Взаимосвязь этих таблиц можно с помощью картинки.
Мы видим, что для пакета nycflights13
:
flights
соединяется с planes
посредством переменной tailnum
flights
соединяется с airlines
посредством переменной carrier
flights
соединяется с airports
посредством двух переменных origin
и dest
flights
соединяется с weather
посредством переменных origin
, year
, month
, day
и hour
Переменные, используемые для установления связи между каждой парой таблиц, называются ключами. Ключ - это переменная(или набор переменных), которая однозначно идентифицирует наблюдение. Например, каждый самолет можно однозначно идентифицировать по его бортовому номеру(tailnum
). Но для идентификации наблюдения в таблице погодных условий(weather
), требуются пять переменных: origin
, year
, month
, day
и hour
.
Ключи бывают двух типов:
planes$tailnum
- первичный ключ, поскольку он однозначно идентифицирует каждый самолет в таблице planes
)flights$tailnum
- внешний ключ, так как он устанавливает для каждого авиарейса однозначно определяемый самолет в таблице flights
)Переменная может служить одновременно первичным и внешним ключом(например переменная origin
является частью первичного ключа для таблицы weather
и одновременно внешним ключом для таблицы airports
).
Как только вы определите первичные ключи в своих таблицах, целесообразно проверить, что они действительно однозначно определяют каждое значение.
Можно это сделать с помощью следующей команды:
## # A tibble: 0 x 2
## # … with 2 variables: tailnum <chr>, n <int>
Видим, что фрейм пустой, это означает, что нет таких значений первичного ключа, которые появлялись дважды. Следовательно, они уникальны.
## # A tibble: 1,092 x 5
## year month day origin n
## <int> <int> <int> <chr> <int>
## 1 2013 1 1 EWR 22
## 2 2013 1 1 JFK 22
## 3 2013 1 1 LGA 23
## 4 2013 1 2 EWR 24
## 5 2013 1 2 JFK 24
## 6 2013 1 2 LGA 24
## 7 2013 1 3 EWR 24
## 8 2013 1 3 JFK 24
## 9 2013 1 3 LGA 24
## 10 2013 1 4 EWR 24
## # … with 1,082 more rows
В данном случае видим, что для каждого набора origin
, year
, month
и day
имеется несколько значений. Поэтому этот набор не является первичным ключом. Нужно добавить переменную hour
, чтобы определить правильно первичный ключ.
## # A tibble: 3 x 6
## year month day hour origin n
## <int> <int> <int> <int> <chr> <int>
## 1 2013 11 3 1 EWR 2
## 2 2013 11 3 1 JFK 2
## 3 2013 11 3 1 LGA 2
Иногда таблица не имееет явного первичного ключа: каждая строка является наблюдением, но ни одна комбинация переменных не обеспечивает ее надежную информацию. Например, что является первичным ключом в таблице flights
? Можно подумать, что мы могли бы использовать комбинацию даты и номера авиарейса или бортового номера. Но это не дает результатов.
## # A tibble: 29,768 x 5
## year month day flight n
## <int> <int> <int> <int> <int>
## 1 2013 1 1 1 2
## 2 2013 1 1 3 2
## 3 2013 1 1 4 2
## 4 2013 1 1 11 3
## 5 2013 1 1 15 2
## 6 2013 1 1 21 2
## 7 2013 1 1 27 4
## 8 2013 1 1 31 2
## 9 2013 1 1 32 2
## 10 2013 1 1 35 2
## # … with 29,758 more rows
## # A tibble: 64,928 x 5
## year month day tailnum n
## <int> <int> <int> <chr> <int>
## 1 2013 1 1 N0EGMQ 2
## 2 2013 1 1 N11189 2
## 3 2013 1 1 N11536 2
## 4 2013 1 1 N11544 3
## 5 2013 1 1 N11551 2
## 6 2013 1 1 N12540 2
## 7 2013 1 1 N12567 2
## 8 2013 1 1 N13123 2
## 9 2013 1 1 N13538 3
## 10 2013 1 1 N13566 3
## # … with 64,918 more rows
То есть каждый номер авиарейса используется несколько раз за день, как и некоторые самолеты совершают несколько авиарейсов в день. Можно дать ключ таким данным просто пронумеровав их с помощью функции row_number
.
flights %>%
mutate(key = row_number()) %>%
select(key,year, month, day, flight) %>%
arrange(year, month, day, flight)
## # A tibble: 336,776 x 5
## key year month day flight
## <int> <int> <int> <int> <int>
## 1 160 2013 1 1 1
## 2 292 2013 1 1 1
## 3 113 2013 1 1 3
## 4 296 2013 1 1 3
## 5 202 2013 1 1 4
## 6 488 2013 1 1 4
## 7 709 2013 1 1 6
## 8 645 2013 1 1 7
## 9 475 2013 1 1 8
## 10 625 2013 1 1 9
## # … with 336,766 more rows
Первичный ключ и соотвествующий внешний ключ в другой таблице образуют отношение. Как правило, такие отношения относятся к типу “один ко многим”(например, каждому авиарейсу соотвествует один самолёт, но каждому самолёту соотвествует множество авиарейсов). Встречаются отношения “один к одному” и “многие ко многим”(между таблицами airlines
и airports
: каждая авиакомпания совершает полеты во многие аэропорты, и каждый аэропорт принимает самолеты многих авиакомпаний).
Такие соединения позволяют объединять переменные из разных двух таблиц. Сначала оно находит соответствующие наблюдения по их ключам, а затем копирует переменные из одной таблицы в другую. Чтобы было проще работать с данными, возьмем только некоторые столбцы.
Предположим, что мы хотим добавить полное название авиакомпании для каждого перелета в таблице flights2
. Фреймы данных airlines
и flighths2
можно соединить с помощью функции left_join
.
## # A tibble: 336,776 x 7
## year month day hour tailnum carrier name
## <int> <int> <int> <dbl> <chr> <chr> <chr>
## 1 2013 1 1 5 N14228 UA United Air Lines Inc.
## 2 2013 1 1 5 N24211 UA United Air Lines Inc.
## 3 2013 1 1 5 N619AA AA American Airlines Inc.
## 4 2013 1 1 5 N804JB B6 JetBlue Airways
## 5 2013 1 1 6 N668DN DL Delta Air Lines Inc.
## 6 2013 1 1 5 N39463 UA United Air Lines Inc.
## 7 2013 1 1 6 N516JB B6 JetBlue Airways
## 8 2013 1 1 6 N829AS EV ExpressJet Airlines Inc.
## 9 2013 1 1 6 N593JB B6 JetBlue Airways
## 10 2013 1 1 6 N3ALAA AA American Airlines Inc.
## # … with 336,766 more rows
Появилась новая переменная name
, в которой теперь хранится полное название компании.
Давайте разберем этот и другие виды соединений на более простых примерах. Возьмем вот такие 2 таблицы
x <- tribble(
~key, ~val_x,
1, "x1",
2, "x2",
3, "x3"
)
y <- tribble(
~key, ~val_y,
1, "y1",
2, "y2",
4, "y3"
)
Также будем использовать визуальное представление таких соединений, для пониманию. Цветные столбцы представляют переменную key: они используются для установления соответствия между строками обеих таблиц. Серый столбец - это столбец значений.
Соединение это способ связывания каждой строки x
с нулем, одной или несколькими строками y
.
В фактическом соединении совпадения будут обозначаться точками. Количество точек = количеству совпадений = количеству строк в результате.
Такой тип соединения называется внутренним(inner_join
). Оно устаналивает соответствие между парами наблюдений, если их ключи совпадают.
Результатом внутреннего соединения является новый дата фрейм, который содержит ключ, значения x
и значения y
. Для того, чтобы сообщить какая из переменных является ключом, мы используем параметр by
.
## # A tibble: 2 x 3
## key val_x val_y
## <dbl> <chr> <chr>
## 1 1 x1 y1
## 2 2 x2 y2
Несовподающие строки не включаются в результат. Это означает, что, как праивло, при таком соединении теряются наблюдения. С этим нужно быть аккуратным.
Еще одним видом являются внешние соединения. Внутреннее соединение сохраняет наблюдения, которые встречаются в обеих таблицах. Внешнее соединение сохраняет наблюдения, встречающиеся хотя бы в одной из таблиц. Различают три типа внешних соединений:
x
y
x
и y
Эти соединения работают, добавляя дополнительные “виртуальное” наблюдение в каждую таблицу.ю Данное наблюдение имеет ключ, который совпадает с любым ключом(если никакой другой ключ не совпадает), и значение NA
.
Представим это графически.
Или можно это представить в виде множеств.
Чаще всего выбирается левое соединение, так как ,обычно, у вас имеется одна таблица(например,для регрессии), которая “наполняется” с помощью других таблиц. Старайтесь использовать именно его.
Рассмотрим ситуации, когда у нас неуникальные ключи;
x <- tribble(
~key, ~val_x,
1, "x1",
2, "x2",
2, "x3",
1, "x4"
)
y <- tribble(
~key, ~val_y,
1, "y1",
2, "y2"
)
left_join(x, y, by = "key")
## # A tibble: 4 x 3
## key val_x val_y
## <dbl> <chr> <chr>
## 1 1 x1 y1
## 2 2 x2 y2
## 3 2 x3 y2
## 4 1 x4 y1
x <- tribble(
~key, ~val_x,
1, "x1",
2, "x2",
2, "x3",
3, "x4"
)
y <- tribble(
~key, ~val_y,
1, "y1",
2, "y2",
2, "y3",
3, "y4"
)
left_join(x, y, by = "key")
## # A tibble: 6 x 3
## key val_x val_y
## <dbl> <chr> <chr>
## 1 1 x1 y1
## 2 2 x2 y2
## 3 2 x2 y3
## 4 2 x3 y2
## 5 2 x3 y3
## 6 3 x4 y4
Если названия переменных в ваших таблицах не совпадает, то можно передать массив соответствия.
## # A tibble: 336,776 x 15
## year month day hour origin dest tailnum carrier name lat lon
## <int> <int> <int> <dbl> <chr> <chr> <chr> <chr> <chr> <dbl> <dbl>
## 1 2013 1 1 5 EWR IAH N14228 UA Geor… 30.0 -95.3
## 2 2013 1 1 5 LGA IAH N24211 UA Geor… 30.0 -95.3
## 3 2013 1 1 5 JFK MIA N619AA AA Miam… 25.8 -80.3
## 4 2013 1 1 5 JFK BQN N804JB B6 <NA> NA NA
## 5 2013 1 1 6 LGA ATL N668DN DL Hart… 33.6 -84.4
## 6 2013 1 1 5 EWR ORD N39463 UA Chic… 42.0 -87.9
## 7 2013 1 1 6 EWR FLL N516JB B6 Fort… 26.1 -80.2
## 8 2013 1 1 6 LGA IAD N829AS EV Wash… 38.9 -77.5
## 9 2013 1 1 6 JFK MCO N593JB B6 Orla… 28.4 -81.3
## 10 2013 1 1 6 LGA ORD N3ALAA AA Chic… 42.0 -87.9
## # … with 336,766 more rows, and 4 more variables: alt <dbl>, tz <dbl>,
## # dst <chr>, tzone <chr>
## # A tibble: 336,776 x 15
## year month day hour origin dest tailnum carrier name lat lon
## <int> <int> <int> <dbl> <chr> <chr> <chr> <chr> <chr> <dbl> <dbl>
## 1 2013 1 1 5 EWR IAH N14228 UA Newa… 40.7 -74.2
## 2 2013 1 1 5 LGA IAH N24211 UA La G… 40.8 -73.9
## 3 2013 1 1 5 JFK MIA N619AA AA John… 40.6 -73.8
## 4 2013 1 1 5 JFK BQN N804JB B6 John… 40.6 -73.8
## 5 2013 1 1 6 LGA ATL N668DN DL La G… 40.8 -73.9
## 6 2013 1 1 5 EWR ORD N39463 UA Newa… 40.7 -74.2
## 7 2013 1 1 6 EWR FLL N516JB B6 Newa… 40.7 -74.2
## 8 2013 1 1 6 LGA IAD N829AS EV La G… 40.8 -73.9
## 9 2013 1 1 6 JFK MCO N593JB B6 John… 40.6 -73.8
## 10 2013 1 1 6 LGA ORD N3ALAA AA La G… 40.8 -73.9
## # … with 336,766 more rows, and 4 more variables: alt <dbl>, tz <dbl>,
## # dst <chr>, tzone <chr>
Теперь рассмотрим фильтрующие соединения. Они сопоставляют наблюдения точно так же, как и мутирующие, но влияют на наблюдения, а не на переменные. Они бывают двух типов:
semi_join(x, y)
- сохраняет все наблюдения в x
, для которых имеется сопоставление в y
.anti_join(x, y)
- отбрасывает все наблюдения в x
, для которых имеется совпадение в y
.Предположим, например, мы нашлим топ-10 популярных пунктов назначения.
После чего вы хотели бы увидеть, только те перелеты, где пункт назначения был в топ-10. Это можно сделать следующим образом.
## # A tibble: 141,145 x 8
## year month day hour origin dest tailnum carrier
## <int> <int> <int> <dbl> <chr> <chr> <chr> <chr>
## 1 2013 1 1 5 JFK MIA N619AA AA
## 2 2013 1 1 6 LGA ATL N668DN DL
## 3 2013 1 1 5 EWR ORD N39463 UA
## 4 2013 1 1 6 EWR FLL N516JB B6
## 5 2013 1 1 6 JFK MCO N593JB B6
## 6 2013 1 1 6 LGA ORD N3ALAA AA
## 7 2013 1 1 6 JFK LAX N29129 UA
## 8 2013 1 1 6 EWR SFO N53441 UA
## 9 2013 1 1 5 JFK BOS N708JB B6
## 10 2013 1 1 6 LGA FLL N595JB B6
## # … with 141,135 more rows
Ту же операцию может сделать и semi_join
.
## # A tibble: 141,145 x 8
## year month day hour origin dest tailnum carrier
## <int> <int> <int> <dbl> <chr> <chr> <chr> <chr>
## 1 2013 1 1 5 JFK MIA N619AA AA
## 2 2013 1 1 6 LGA ATL N668DN DL
## 3 2013 1 1 5 EWR ORD N39463 UA
## 4 2013 1 1 6 EWR FLL N516JB B6
## 5 2013 1 1 6 JFK MCO N593JB B6
## 6 2013 1 1 6 LGA ORD N3ALAA AA
## 7 2013 1 1 6 JFK LAX N29129 UA
## 8 2013 1 1 6 EWR SFO N53441 UA
## 9 2013 1 1 5 JFK BOS N708JB B6
## 10 2013 1 1 6 LGA FLL N595JB B6
## # … with 141,135 more rows
Возникает вопрос, а зачем вообще нужен semi_join
, если это можно сделать через filter
? Представьте, что вы используете не один фильтр а несколько. Например давайте найдем 10 дней с наибольшей задержкой отправления.
top_delay <- flights %>%
group_by(year,month,day) %>%
summarise(delay = max(arr_delay, na.rm = TRUE)) %>%
arrange(desc(delay)) %>%
head(10)
А теперь найдем все авиарейсы, которые были в эти дни.
## # A tibble: 9,164 x 8
## year month day hour origin dest tailnum carrier
## <int> <int> <int> <dbl> <chr> <chr> <chr> <chr>
## 1 2013 1 9 23 JFK PSE N603JB B6
## 2 2013 1 9 23 JFK BQN N563JB B6
## 3 2013 1 9 5 EWR CLT N566UW US
## 4 2013 1 9 5 LGA IAH N17627 UA
## 5 2013 1 9 5 EWR IAH N825UA UA
## 6 2013 1 9 5 JFK MIA N5CSAA AA
## 7 2013 1 9 5 JFK BQN N571JB B6
## 8 2013 1 9 6 LGA ATL N542MQ MQ
## 9 2013 1 9 6 LGA BOS N747UW US
## 10 2013 1 9 6 LGA ATL N955AT FL
## # … with 9,154 more rows
А как бы вы написали такое же через filter
?
Визуализация для semi_join
представлена ниже.
Важно отметить, что semi_join
не дублирует строки.
Функция anti_join
делает все наоборот, выводит все наблюдения, которые не соответствуют нашему фильтру.
Например:
## # A tibble: 195,631 x 8
## year month day hour origin dest tailnum carrier
## <int> <int> <int> <dbl> <chr> <chr> <chr> <chr>
## 1 2013 1 1 5 EWR IAH N14228 UA
## 2 2013 1 1 5 LGA IAH N24211 UA
## 3 2013 1 1 5 JFK BQN N804JB B6
## 4 2013 1 1 6 LGA IAD N829AS EV
## 5 2013 1 1 6 JFK PBI N793JB B6
## 6 2013 1 1 6 JFK TPA N657JB B6
## 7 2013 1 1 6 LGA DFW N3DUAA AA
## 8 2013 1 1 6 EWR LAS N76515 UA
## 9 2013 1 1 6 EWR PBI N644JB B6
## 10 2013 1 1 6 LGA MSP N971DL DL
## # … with 195,621 more rows
Последним видом операций явлюятся операции над множествами. Они достаточно редкие, но бывают очень полезны. Приведенные ниже функции принимают на вход две таблицы x
и y
, которые имеют одни и те же переменные.
intersect(x, y)
- возвращает только наблюдения, содержащиеся одновременно в x
и y
(\(x \cap y\))union(x, y)
- возращает уникальные наблюдения, которые содержатся в x
и y
setdiff(x,y)
- возвращает наблюдения, которые есть в x
, но нет в y
(\(x / y\))Представим, что у нас имеются следующие таблицы.
## # A tibble: 2 x 2
## x y
## <dbl> <chr>
## 1 1 one
## 2 2 one
## # A tibble: 2 x 2
## x y
## <dbl> <chr>
## 1 1 one
## 2 1 two
Применим эти функции на них.
## # A tibble: 1 x 2
## x y
## <dbl> <chr>
## 1 1 one
## # A tibble: 3 x 2
## x y
## <dbl> <chr>
## 1 1 one
## 2 2 one
## 3 1 two
## # A tibble: 1 x 2
## x y
## <dbl> <chr>
## 1 2 one
## # A tibble: 1 x 2
## x y
## <dbl> <chr>
## 1 1 two
Функция union
очень удобна в следующей ситуации. Представьте, что вы скачали данные по доходностям облигаций из Bloomberg и из Cbonds. Каждое агентсво имеет свои данные, у кого-то есть больше информации по одним облигациям, у кого-то по другим, но есть и пересекающиеся облигации. Чтобы выбрать все доступные вам наблюдения, можно использовать функцию union
.
Если данные поступают к вам частями, то скорее всего вам хотелось бы их соединить. Для этого существует 2 функции:
bind_rows(x, y)
- склеивание по строкамbind_cols(x, y)
- склеивание по столбцам## # A tibble: 2 x 2
## x y
## <dbl> <chr>
## 1 1 one
## 2 2 one
## # A tibble: 2 x 2
## x y
## <dbl> <chr>
## 1 1 one
## 2 1 two
## # A tibble: 4 x 2
## x y
## <dbl> <chr>
## 1 1 one
## 2 2 one
## 3 1 one
## 4 1 two
## # A tibble: 2 x 4
## x y x1 y1
## <dbl> <chr> <dbl> <chr>
## 1 1 one 1 one
## 2 2 one 1 two
Эти функции удобно использовать, если у вас много таких табличек и все они находятся в листе.
## # A tibble: 4 x 2
## x y
## <dbl> <chr>
## 1 1 one
## 2 2 one
## 3 1 one
## 4 1 two
## # A tibble: 2 x 4
## x y x1 y1
## <dbl> <chr> <dbl> <chr>
## 1 1 one 1 one
## 2 2 one 1 two
Иногда бывает, что вам нужно соединить данные не с одинаковым ключом. К примеру даны котировки акции какой-то компании.
## # A tibble: 4 x 2
## Date Yield
## <chr> <dbl>
## 1 01012019 101
## 2 02012019 104.
## 3 03012019 107.
## 4 04012019 109.
Предположим, что вы хотите понять как котировка зависит от курса евро. Для построения такой модели вам нужна табличка с переменными Date
, Yield
и Euro
. У вас имеется табличка с курсом евро.
## # A tibble: 3 x 2
## Date Euro
## <chr> <dbl>
## 1 02012019 75
## 2 03012019 74.2
## 3 04012019 73.1
Все, что нужно это использовать left_join
.
## # A tibble: 4 x 3
## Date Yield Euro
## <chr> <dbl> <dbl>
## 1 01012019 101 NA
## 2 02012019 104. 75
## 3 03012019 107. 74.2
## 4 04012019 109. 73.1
Можем видеть, что значение курса для 1 января отсутствует. Таких отсутствующих значений может быть очень много. Это означает, что мы теряем большое количество наблюдений. Есть смысл в том, чтобы для курса 1 января взять ближайшее доступное нам значение. К примеру, за 2 января. Эта цифра будет близка к реальному значению. И в итоге получить следующую таблицу.
## # A tibble: 4 x 3
## Date Yield Euro
## <chr> <dbl> <dbl>
## 1 01012019 101 75
## 2 02012019 104. 75
## 3 03012019 107. 74.2
## 4 04012019 109. 73.1
Различные варианты таких условий можно задавать самому или с помощью пакета fuzzyjoin
.