22  Підготовка та очистка текстової інформації


author: Юрій Клебан


Зчитаємо інформацію про стать з попереднього прикладу:

library(janitor)
data <- read.csv("data/badtitled.csv")
data <- clean_names(data)
data <- as.data.frame(data$person_gender)
colnames(data) <- c("gender")
unlist(data)
gender1
'Male'
gender2
' M'
gender3
'Female'
gender4
'Man'
gender5
'female'
gender6
'F '
gender7
'male.'
gender8
'm'
gender9
'Man'
gender10
'female'
gender11
'F '
gender12
'male.'
gender13
'm'

Схоже, що ці дані насправді мають всього 2 записи, проте у базу даних їх вносили різні люди або вони були зібрані з різних джерел інформації. Це досить поширена проблема у роботі з даними. Особливо коли відбуваєть заміна людей на рочих місцях або перехід на інше програмне забезпечення.

Якщо це буде розглядатися як факторна змінна без будь-якої попередньої обробки, очевидно, що 8, а не 2 класи будуть збережені. Тому завдання полягає в тому, щоб автоматично розпізнавати наведені вище дані про те, чи відноситься кожен елемент до чоловічої чи жіночої статі. У статистичних контекстах класифікацію таких “безладні” текстові рядки в ряд фіксованих категорій часто називають кодуванням.

Опишемо два взаємодоповнюючих підходи до кодування рядків: нормалізація (string normalization) рядків і аналіз схожості тексту (approximate text matching).

Розглянемо наступні підходи до очистки текстових даних:

- Видалення пробілів на початку або в кінці
- Обрізання/збільшення рядків до певної ширини
– Перетворення у верхній/нижній регістр.
– Пошук рядків, що містять прості шаблони (підрядки).
– Апроксимація рядків на основі "відстаней".

Робота з текстом у R здійснюється за допомогою пакету stringr.

Видалення пробілів на початку або в кінці здійснюється за допомогою функції str_trim().

library(stringr)
str_trim(" ostroh academy  ")
str_trim(" ostroh academy ", side = "left")
str_trim(" ostroh academy ", side = "right")
'ostroh academy'
'ostroh academy '
' ostroh academy'

Обрізання/збільшення рядків до певної ширини здійснюється за допомогою функції str_pad().

str_pad(57, width = 6, side = "left", pad = 0)
'000057'
str_pad("ostroh", width = 10, side = "right", pad = "_")
'ostroh____'

Перетворення у верхній/нижній регістр

text <- "Ostroh Academy!"
toupper(text)
tolower(text)
'OSTROH ACADEMY!'
'ostroh academy!'

Пошук рядків, що містять прості шаблони (підрядки)

Скористаємося функцієя grep() та grepl() для пошуку підрядків у інформації про стать:

grepl("m", data$gender) # Повертає TRUE/FALSE, якщо знахоить входження рядка
grep("m", data$gender) # Повертає номери рядків, по яких є входження
Your code contains a unicode char which cannot be displayed in your
current locale and R will silently convert it to an escaped form when the
R kernel executes this code. This can lead to subtle errors if you use
such chars to do comparisons. For more information, please see
https://github.com/IRkernel/repr/wiki/Problems-with-unicode-on-windows
  1. FALSE
  2. FALSE
  3. TRUE
  4. FALSE
  5. TRUE
  6. FALSE
  7. TRUE
  8. TRUE
  9. FALSE
  10. TRUE
  11. FALSE
  12. TRUE
  13. TRUE
  1. 3
  2. 5
  3. 7
  4. 8
  5. 10
  6. 12
  7. 13
grepl("m", data$gender, ignore.case = TRUE) # не враховує регістр букв
grepl("m", tolower(data$gender))
Your code contains a unicode char which cannot be displayed in your
current locale and R will silently convert it to an escaped form when the
R kernel executes this code. This can lead to subtle errors if you use
such chars to do comparisons. For more information, please see
https://github.com/IRkernel/repr/wiki/Problems-with-unicode-on-windows
  1. TRUE
  2. TRUE
  3. TRUE
  4. TRUE
  5. TRUE
  6. FALSE
  7. TRUE
  8. TRUE
  9. TRUE
  10. TRUE
  11. FALSE
  12. TRUE
  13. TRUE
  1. TRUE
  2. TRUE
  3. TRUE
  4. TRUE
  5. TRUE
  6. FALSE
  7. TRUE
  8. TRUE
  9. TRUE
  10. TRUE
  11. FALSE
  12. TRUE
  13. TRUE
data$gender
grepl("^m", data$gender, ignore.case = TRUE) # Показує усі збіги, що починаються з вказаної літери
Your code contains a unicode char which cannot be displayed in your
current locale and R will silently convert it to an escaped form when the
R kernel executes this code. This can lead to subtle errors if you use
such chars to do comparisons. For more information, please see
https://github.com/IRkernel/repr/wiki/Problems-with-unicode-on-windows
  1. 'Male'
  2. ' M'
  3. 'Female'
  4. 'Man'
  5. 'female'
  6. 'F '
  7. 'male.'
  8. 'm'
  9. 'Man'
  10. 'female'
  11. 'F '
  12. 'male.'
  13. 'm'
  1. TRUE
  2. FALSE
  3. FALSE
  4. TRUE
  5. FALSE
  6. FALSE
  7. TRUE
  8. TRUE
  9. TRUE
  10. FALSE
  11. FALSE
  12. TRUE
  13. TRUE

Пошук “відстані” між ряжками - це аналіз рядків на схожіть з визначенням рівня співпадінь.

adist("ao", "ao")
adist("ao", "oa")
adist("ao", "45fb")
A matrix: 1 × 1 of type dbl
0
A matrix: 1 × 1 of type dbl
2
A matrix: 1 × 1 of type dbl
4

Давайте проаналізуємо інформацію про стать з точки зору схожості текстів:

m <- c("male", "female")
adj_m <- adist(data$gender, m)
#adj_m <- adist(tolower(data$gender), m)
#adj_m <- adist(str_trim(tolower(data$gender), side="both"), m)
colnames(adj_m) <- m 
rownames(adj_m) <- data$gender
adj_m
A matrix: 13 × 2 of type dbl
male female
Male 0 2
M 3 5
Female 2 0
Man 2 4
female 2 0
F 4 5
male. 1 3
m 3 5
Man 2 4
female 2 0
F 4 5
male. 1 3
m 3 5
# Видалимо повтори
adj_m |> as.data.frame() |> dplyr::distinct()
Your code contains a unicode char which cannot be displayed in your
current locale and R will silently convert it to an escaped form when the
R kernel executes this code. This can lead to subtle errors if you use
such chars to do comparisons. For more information, please see
https://github.com/IRkernel/repr/wiki/Problems-with-unicode-on-windows
A data.frame: 6 × 2
male female
<dbl> <dbl>
Male 0 2
X...M 3 5
Female 2 0
Man 2 4
F.... 4 5
male. 1 3

Примінимо інформацію про відстані до “нечистих” даних про стать:

nums <- apply(adj_m, 1, which.min) # Знайдемо найближчі значення
nums
Your code contains a unicode char which cannot be displayed in your
current locale and R will silently convert it to an escaped form when the
R kernel executes this code. This can lead to subtle errors if you use
such chars to do comparisons. For more information, please see
https://github.com/IRkernel/repr/wiki/Problems-with-unicode-on-windows
Male
1
M
1
Female
2
Man
1
female
2
F
1
male.
1
m
1
Man
1
female
2
F
1
male.
1
m
1
data.frame(initial = data$gender, coded = m[nums]) # FFFFFFFFFFFFFF - проблема!
Your code contains a unicode char which cannot be displayed in your
current locale and R will silently convert it to an escaped form when the
R kernel executes this code. This can lead to subtle errors if you use
such chars to do comparisons. For more information, please see
https://github.com/IRkernel/repr/wiki/Problems-with-unicode-on-windows
A data.frame: 13 × 2
initial coded
<chr> <chr>
Male male
M male
Female female
Man male
female female
F male
male. male
m male
Man male
female female
F male
male. male
m male

Як альтернативу для знаходження відстаней між рядками можна використовувати функції з бібліотеки stringdist.

#install.packages("stringdist")
library(stringdist)
adist("ao", "oa")
stringdist("oa", "ao") # 1, а було 2
Your code contains a unicode char which cannot be displayed in your
current locale and R will silently convert it to an escaped form when the
R kernel executes this code. This can lead to subtle errors if you use
such chars to do comparisons. For more information, please see
https://github.com/IRkernel/repr/wiki/Problems-with-unicode-on-windows
A matrix: 1 × 1 of type dbl
2
1

Спробуємо “очистити” дані, які ми отримали з допомогою функції amatch():

nums <- amatch(data$gender,  c("male", "female"), maxDist = 4) # Знайдемо найближчі значення
nums
Your code contains a unicode char which cannot be displayed in your
current locale and R will silently convert it to an escaped form when the
R kernel executes this code. This can lead to subtle errors if you use
such chars to do comparisons. For more information, please see
https://github.com/IRkernel/repr/wiki/Problems-with-unicode-on-windows
  1. 1
  2. 1
  3. 2
  4. 1
  5. 2
  6. <NA>
  7. 1
  8. 1
  9. 1
  10. 2
  11. <NA>
  12. 1
  13. 1
data.frame(initial = data$gender, coded = m[nums]) # FFFFFFFFFFFFFF - проблема!
Your code contains a unicode char which cannot be displayed in your
current locale and R will silently convert it to an escaped form when the
R kernel executes this code. This can lead to subtle errors if you use
such chars to do comparisons. For more information, please see
https://github.com/IRkernel/repr/wiki/Problems-with-unicode-on-windows
A data.frame: 13 × 2
initial coded
<chr> <chr>
Male male
M male
Female female
Man male
female female
F NA
male. male
m male
Man male
female female
F NA
male. male
m male
library(dplyr)
data <- data |> mutate(gender = ifelse(gender == "F", "female", gender)) # ????? # Space
data
data <- data |> mutate(gender = ifelse(str_trim(gender) == "F", "female", gender))
data
nums <- amatch(data$gender,  c("male", "female"), maxDist = 4)
data.frame(initial = data$gender, coded = m[nums]) 
A data.frame: 13 × 1
gender
<chr>
Male
M
Female
Man
female
female
male.
m
Man
female
female
male.
m
A data.frame: 13 × 1
gender
<chr>
Male
M
Female
Man
female
female
male.
m
Man
female
female
male.
m
A data.frame: 13 × 2
initial coded
<chr> <chr>
Male male
M male
Female female
Man male
female female
female female
male. male
m male
Man male
female female
female female
male. male
m male

Місія виконана! Замінимо та збережемо інформацію у файл для майбутніх експериментів по цій темі:

data <- read.csv("data/badtitled.csv")
data <- clean_names(data)
head(data, 2)
A data.frame: 2 × 5
person_age person_height person_weight person_gender empty
<int> <chr> <dbl> <chr> <lgl>
1 23 185 NA Male NA
2 41 175 68.3 M NA
data <- data |> mutate(person_gender = ifelse(str_trim(person_gender) == "F", "female", person_gender))
m <- c("male", "female")
nums <- amatch(data$person_gender, m, maxDist = 4)
data <- data |> mutate(person_gender = m[nums])
data
A data.frame: 13 × 5
person_age person_height person_weight person_gender empty
<int> <chr> <dbl> <chr> <lgl>
23 185 NA male NA
41 175 68.3 male NA
11 142* 55.4 female NA
12 NA 48.2 male NA
54 191 NA female NA
32 168 78.0 female NA
22 NA 54.0 male NA
21 165 NA male NA
14 NA 90.2 male NA
51 250 NA female NA
41 20 81.0 female NA
66 NA 59.0 male NA
71 171 NA male NA

Замінимо також висоту на числове значення, а не текст:

data <- data |> 
    mutate(person_height = str_remove(data$person_height, pattern = "[*]"))
data
A data.frame: 13 × 5
person_age person_height person_weight person_gender empty
<int> <chr> <dbl> <chr> <lgl>
23 185 NA male NA
41 175 68.3 male NA
11 142 55.4 female NA
12 NA 48.2 male NA
54 191 NA female NA
32 168 78.0 female NA
22 NA 54.0 male NA
21 165 NA male NA
14 NA 90.2 male NA
51 250 NA female NA
41 20 81.0 female NA
66 NA 59.0 male NA
71 171 NA male NA
data <- data |> mutate(person_height = as.numeric(person_height))
data
A data.frame: 13 × 5
person_age person_height person_weight person_gender empty
<int> <dbl> <dbl> <chr> <lgl>
23 185 NA male NA
41 175 68.3 male NA
11 142 55.4 female NA
12 NA 48.2 male NA
54 191 NA female NA
32 168 78.0 female NA
22 NA 54.0 male NA
21 165 NA male NA
14 NA 90.2 male NA
51 250 NA female NA
41 20 81.0 female NA
66 NA 59.0 male NA
71 171 NA male NA
write.csv(data, file = "data/cleaned_titled.csv", row.names = F)

22.1 Набори даних

  1. https://github.com/kleban/r-book-published/tree/main/datasets/untitled.csv
  2. https://github.com/kleban/r-book-published/tree/main/datasets/badtitled.csv
  3. https://github.com/kleban/r-book-published/tree/main/datasets/cleaned_titled.csv
  4. https://github.com/kleban/r-book-published/tree/main/datasets/cleaned_titled2.csv
  5. https://github.com/kleban/r-book-published/tree/main/datasets/river_eco.csv

22.2 Використані та додаткові джерела

  1. KPMG Virtual Internship
  2. An introduction to data cleaning with R / Edwin de Jonge, Mark van der Loo, 2013
  3. Anomaly Detection in R
  4. K-nearest Neighbor: The maths behind it, how it works and an example
  5. Quantile. Wikipedia