Статьи

Очистка данных с помощью Go: часть 2

Это вторая часть из двух в серии, посвященной очистке данных с помощью Go. В первой части мы рассмотрели основные текстовые возможности Go и работу с CSV-файлами. В этом уроке мы углубимся в фактическую очистку данных.

Мы начнем с понимания проблемы запутанных данных и разработки стратегии, а затем рассмотрим проверку отдельных полей, исправим данные, где это возможно, и решим, что делать с пропущенными значениями.

Стратегия очистки данных должна определять, что делать при обнаружении неверных, грязных, частичных или отсутствующих данных. Следует также определить, какой уровень отчетности необходим для процесса очистки.

Данные, на которых мы фокусируемся, являются табличными данными, где каждая строка независима. Не существует вложенных иерархий или связей между различными строками данных. Многие реальные наборы данных обладают этим хорошим свойством.

Самый простой подход для работы с недействительными данными — удалить их. Если какое-либо поле отсутствует или содержит недопустимые данные, просто избавьтесь от всей строки. Это очень легко, а иногда это правильно. Если проблемное поле является критическим, и у вас нет возможности восстановить его, все, что вы можете сделать, это удалить всю запись.

Лучшее решение — исправить плохое поле. В некоторых случаях легко обнаружить проблему и устранить ее. В наборе данных наблюдений НЛО поле штата может быть одним из 52 штатов США.

Если значение должно быть только в верхнем регистре, а некоторые строки содержат строчные буквы, вы можете просто сделать их заглавными.

Отчеты о недействительных строках, удаленных или исправленных, очень важны. Организация может решить позволить людям попытаться исправить пропущенные данные. Может потребоваться запустить фиксированные данные с помощью QA, чтобы автоматические исправления не вводили недействительные данные.

Сбор статистики по процессу очистки необходим для оценки качества исходных данных, а иногда и для определения того, стоит ли обрабатывать очищенные данные. Статистика может включать количество пропущенных и фиксированных строк, а также количество плохих и пропущенных полей для каждого столбца.

До сих пор я описал подход предварительной обработки для очистки данных. Тем не менее, можно выполнить очистку во время обработки. Каждая строка проверяется непосредственно перед обработкой. Это иногда полезно, если нет смысла в предварительной обработке, поскольку никто не может заранее исправить неверные данные для последующего анализа или если обработка зависит от времени.

В этом сценарии основная цель очистки состоит в том, чтобы убедиться, что неверные строки данных не нарушают весь конвейер обработки и могут быть пропущены или исправлены при необходимости.

Как вы проводите проверку полей? Вы должны точно знать, какой тип данных должен быть там, а иногда и какие значения. Вот несколько примеров.

Числовые поля очень распространены в наборах данных. Помимо типа числа (целое, вещественное, сложное), некоторые поля являются более специализированными. Например, поле цены может потребовать ровно две десятичные точки и быть положительным. Вот функция, которая проверяет, представляет ли строка цену:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func validate_price(s string) bool {
    parts := strings.Split(s,».»)
    if len(parts) != 2 {
        return false
    }
 
    dollars, err := strconv.Atoi(parts[0])
    if err != nil {
        return false
    }
     
    if dollars < 0 {
        return false
    }
     
    cents, err := strconv.Atoi(parts[1])
    if err != nil {
        return false
    }
     
    if cents < 0 ||
        return false
    }
 
    return true
}

Иногда вам нужно идти выше и выше. Если вам нужно убедиться, что URL-адрес действителен, есть два подхода:

  1. Разобрать URL.
  2. Попробуйте и получите его (или, по крайней мере, получите заголовки).

Если вам важно, чтобы URL был правильно сформирован, тогда первый подход работает. Но если вы хотите убедиться, что URL действительно указывает на реальный пункт назначения, вам нужно использовать второй подход. Поскольку второй подход является надмножеством первого подхода, давайте просто воспользуемся им:

1
2
3
4
func validate_url(url string) bool {
    _, err := http.Head(url)
    return err == nil
}

Если значения должны соответствовать пользовательскому формату, вы можете либо сопоставить его, используя простые строковые функции, такие как Split() либо в более сложных случаях использовать регулярные выражения. Например, если ваш набор данных содержит номера социального страхования (я надеюсь, что нет) в формате XXX-XX-XXXX то вы можете разделить на «-» и убедиться, что есть три токена, в которых первый состоит из трех цифр, а второй — из двух цифр. долго, а третье четыре цифры в длину. Но более кратко использовать регулярные выражения, такие как ^\d{3}-?\d{2}-?\d{4}$ .

Исправление неверных значений не является тривиальной вещью. Если ваш метод исправления неверен, вы можете получить поврежденные данные. Вы должны внимательно рассмотреть важность поля, диапазон возможных допустимых значений и насколько вы уверены, что действительно можете исправить любое недопустимое значение автоматически.

Это довольно безопасное решение. Если текстовое поле должно быть написано заглавными буквами, вы можете исправить это, не рискуя сильно, потому что символы, которые изначально были строчными, не являются важной частью информации. Нет необходимости писать специальный код, так как пакет strings имеет функцию ToUpper() . Есть также функции ToLower() и даже ToTitle() и ToTitleSpecific() для правильного использования текста.

Другое распространенное простое решение — удаление начальных и конечных пробелов. Вы будете удивлены, сколько людей добавляют пробелы или новые строки при вводе данных. Пакет strings имеет набор функций TrimXXX() которые могут позаботиться о большинстве ситуаций:

  • Отделка()
  • TrimFunc ()
  • TrimLeft ()
  • TrimLeftFunc ()
  • TrimPrefix ()
  • TrimRight ()
  • TrimRightFunc ()
  • TrimSpace ()
  • TrimSuffix ()

В некоторых случаях можно удалить недопустимые символы. Я рекомендую делать это только для некритических и необязательных полей. Например, у вас может быть поле описания или примечаний, содержащее свободный текст, и вы хотите убедиться, что оно не содержит определенных символов, таких как кавычки или двойные кавычки. Вот как это сделать:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func remove_quotes(s string) string {
    var b bytes.Buffer
    for _, r := range (s) {
        if r != ‘»‘ && r != ‘\» {
            b.WriteRune(r)
        }
    }
 
    return b.String()
}
 
 
func main() {
    original := `’quotes’ and «double quotes».`
    clean := remove_quotes(original)
    fmt.Println(original)
    fmt.Println(clean)
}
 
Output:
 
‘quotes’ and «double quotes».
quotes and double quotes.

Числовые значения часто легко исправить. Если вам требуется точность в два десятичных знака, вы можете усечь или округлить дополнительные цифры. Таким же образом легко конвертировать целые числа в числа с плавающей точкой.

Иногда существует диапазон допустимых значений, и вы можете привести слишком большие или слишком маленькие числа, чтобы соответствовать диапазону. Следующая функция принимает строку и диапазон целых чисел и возвращает строку, представляющую целое число в диапазоне. Слишком большие значения становятся максимальными, а слишком маленькие — минимальными.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func fit_into_range(s string, min int, max int) string {
    n, _ := strconv.Atoi(s)
 
    if n < min {
        n = min
    } else if n > max {
        n = max
    } else {
        return s
    }
 
    return strconv.Itoa(n)
}
 
 
func main() {
    fmt.Println(fit_into_range(«15», 10, 20))
    fmt.Println(fit_into_range(«-15», 10, 20))
    fmt.Println(fit_into_range(«55», 10, 20))
}
 
Output:
 
15
10
20

URL-адреса часто можно безопасно исправить, используя различные схемы («http» или «https») или добавляя или удаляя «www» субдомены. Объединение вариантов с попыткой выбрать кандидатов может дать вам уверенность в том, что исправление было правильным.

Пропущенные значения очень распространены при приеме реальных данных. Если отсутствует пропущенное значение, существует два основных способа его обработки (без полного отклонения строки) — использовать значения по умолчанию или восстановить значение из альтернативного источника.

Значения по умолчанию полезны, потому что код обработки не должен проверять, присутствует ли значение или нет. Код очистки данных гарантирует, что всегда есть значение на месте. Во многих случаях значения по умолчанию настолько распространены, что они также являются помощником для ввода данных, когда вам не нужно вводить одно и то же значение по умолчанию снова и снова.

Этот подход немного сложнее. Идея состоит в том, чтобы обратиться к другому источнику данных, который имеет запрошенную информацию. Например, если у вас есть электронная почта пользователя, но отсутствуют имя и фамилия, вы можете обратиться к базе данных пользователей и извлечь имя пользователя. Это спасает код обработки от доступа к БД или даже от знания этой зависимости.

Давайте очистим небольшой набор данных продуктов. Поля:

Имя столбца Описание столбца
Я бы PRD-XXXX-XXXX (где X — это цифра)
имя до 40 символов
Цена числовое поле с фиксированной точностью (две десятичные точки)
Описание длиной до 500 символов (необязательно)

Вот набор данных в читаемой форме (пробелы будут урезаны во время очистки):

1
2
3
4
5
6
7
8
const data = `
    Id, Name, Price, Description
    PRD-1234-0000, Airzooka, 9.99, Shoots air at people
    PRD-1234-0017, Pink Onesie, 34.55,
    PRD-1234-666, Oh oh, 18.18, Invalid product id
    PRD-1234-7777, Oh oh 2, , Missing price
    prd-1234-8888, PostIt!, 13.13, Fixable: lowercase id
`

Первые два продукта действительны. Третий продукт, «PRD-1234-666», в его идентификаторе отсутствует цифра. Следующий продукт, «PRD-1234-7777», не имеет цены. Последний продукт, «prd-1234-8888», имеет недопустимый идентификатор продукта, но его можно безопасно исправить (сделать его заглавным).

Следующий код очистит данные, исправит то, что может быть исправлено, удалит строки, которые не могут быть исправлены, и создаст чистый набор данных и отчет, который можно использовать для ручной корректировки неверных данных.

Чтобы проверить идентификатор продукта и цену, я буду использовать регулярные выражения. Вот две вспомогательные функции:

1
2
3
4
5
6
7
8
9
func verifyProductId(s string) bool {
    matched, _ := regexp.MatchString(`^PRD-\d{4}-\d{4}$`, s)
    return matched
}
 
func verifyProductPrice(s string) bool {
    matched, _ := regexp.MatchString(`^\d+\.\d\d$`, s)
    return matched
}

После очистки данных и удаления всех недопустимых строк данных следующая функция запишет чистые данные в новый CSV-файл с именем «clean.csv» и распечатает их на экране.

01
02
03
04
05
06
07
08
09
10
11
func writeCleanData(cleanData []string) {
    f, _ := os.Create(«clean.csv»)
    w := bufio.NewWriter(f)
    fmt.Println(«Clean data:»)
    defer w.Flush()
    for _, line := range cleanData {
        fmt.Println(line)
        w.WriteString(line)
        w.WriteString(«\n»)
    }
}

Функция main() выполняет большую часть работы. Он перебирает исходный набор данных, устраняет лишние пробелы, исправляет все, что может, отслеживает пропущенные строки данных, записывает чистые данные в файл и, наконец, сообщает об удаленных строках.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
func main() {
    cleanData := []string{«Id,Name,Price,Description»}
    dropped := []string{}
    // Clean up data
    all_lines := strings.Split(data, «\n»)
    for _, line := range all_lines {
        fields := strings.Split(line, «,»)
        if len(fields) != 4 {
            continue
        }
 
        // Strip all leading and trailing spaces from each field
        for i, f := range fields {
            fields[i] = strings.TrimSpace(f)
        }
 
        // Automatic fix (no need to check)
        id := strings.ToUpper(fields[0])
        if !verifyProductId(id) {
            dropped = append(dropped, line)
            continue
        }
 
        name := fields[1]
        // Product names can’t be empty
        if name == «» {
            dropped = append(dropped, line)
            continue
        }
 
        // Truncate name at 40 characters (runes)
        if len([]rune(name)) > 40 {
            name = string([]rune(name)[:40])
        }
 
        price := fields[2]
        if !verifyProductPrice(price) {
            dropped = append(dropped, line)
            continue
        }
 
        description := fields[3]
        // Truncate description at 500 characters (runes)
        if len([]rune(name)) > 500 {
            name = string([]rune(name)[:500])
        }
 
        cleanLine := strings.Join([]string{id,
                                           name,
                                           price,
                                           description}, «,»)
        cleanData = append(cleanData, cleanLine)
    }
 
    writeCleanData(cleanData)
 
    // Report
    fmt.Println(«Dropped lines:»)
    for _, s := range dropped {
        fmt.Println(s)
    }
}

Go имеет хорошо продуманные пакеты для обработки текста. В отличие от большинства языков, строковый объект на самом деле представляет собой просто кусочек байтов. Вся логика обработки строк находится в отдельных пакетах, таких как «strings» и «strconv».

Во второй части руководства мы использовали множество концепций для решения общей реальной задачи очистки набора данных в формате CSV перед анализом.