Data Mining: Первичная обработка данных при помощи СУБД

advertisement
Data Mining: Первичная обработка данных при
помощи СУБД. Часть 1
tutorial
SQL*, Data Mining*, Big Data*
О чем статья
В задачах исследования больших объемов данных есть множество тонкостей и
подводных камней. Особенно для тех, кто только начинает исследовать скрытые
зависимости и внутренние связи внутри массивов информации. Если человек
делает это самостоятельно, то дополнительной трудностью становится выбор
примеров, на которых можно учиться и поиск сообщества для обмена мнениями и
оценки своих успехов. Пример не должен быть слишком сложным, но в тоже время
должен покрывать основные проблемы, возникающие при решении задач
приближенных к реальности, так чтобы задача не воспринималась примерно вот
так:
С этой точки зрения, очень интересным будет ресурс Kaggle[1], который
превращает исследование данных в спорт. Там проводят соревнования по анализу
данных. Некоторые соревнования — с обучающими материалами и предназначены
для начинающих. Вот именно обучению анализу данных, на примере решения
одной из обучающих задач, и будет посвящён цикл статей. Первая статья будет о
подготовке данных и использованию СУБД для этой цели. Собственно, о том, как и
с чего начать. Предполагается что читатель понимает SQL.
Задача: «Титаник: Машинное обучение на катастрофах.»
Одно из двух соревнований для начинающих — «Титаник»[2]. Перевод задания:
«Гибель Титаника это одно из наиболее бесславных кораблекрушений в истории.
15 апреля 1912 года, во время своего первого рейса, Титаник затонул после
столкновения с айсбергом, погубив 1502 из 2224 человек пассажиров и команды.
Эта сенсционная трагедия шокировала международное сообщество и привела к
улучшению требований правил техники безопасности на судах. Одной из причин
того, что кораблекрушение повлекло такие жертвы, стала нехватка спасательных
шлюпок для пассажиров и команды. Также был элемент счастливой случайности,
влияющий на спасение от утопления. Также, некоторые группы людей имели
больше шансов выжить чем другие, такие как женщины, дети и высший класс. В
этом соревновании мы предложим вам завершить анализ того, какие типы людей
скорее всего спасутся. В частности, мы попросим вас применить средства
машинного обучения для определения того, какие пассажиры спасутся в этой
трагедии.»
Данные
Для соревнования дано два файла: train.csv и test.csv. Данные в текстовом виде
разделенные запятыми. Одна строка — одна запись о пассажире. Некоторые даные
для записи могут быть неизвестны и пропущены.
ОПИСАНИЕ ПЕРЕМЕННЫХ:
Имя переменной
Что обозначает
survival
Спасение (0 = Нет; 1 = Да)
pclass
Класс пассажира(1 = 1st; 2 = 2nd; 3 = 3rd)
name
Имя
sex
Пол
age
Возраст
sibsp
Количество Братьев(Сестер)/Супругов на борту
parch
Количество Родителей/Детей на борту
ticket
Номер билета
fare
Пассажирский тариф
cabin
Каюта
embarked
Порт посадки(C = Cherbourg; Q = Queenstown; S = Southampton)
Примечание:
Pclass является показателем социально-экономического статуса (SES)
1st ~ Upper; 2nd ~ Middle; 3rd ~ Lower
Возраст в годах; Дробный, если возраст меньше единицы(1)
Если возраст оценочный — то он в форме xx.5
Следующие определения используются для sibsp и parch.
Братья(Сестры): Брат, Сестра, Сводный брат, или Сводная сестра среди
пассажиров Титаника
Супруги: Муж или Жена среди пассажиров Титаника (Любовницы и Женихи
игнорируются)
Родители: Мать или Отец среди пассажиров Титаника.
Дети: Сын, Дочь, Пасынок или Падчерица среди пассажиров Титаника.
Другие члены семьи исключены, включая кузенов, кузин, дядь, тетушек, невесток,
зятей
Дети, путешествующие с нянями имели parch=0.
Точно также путешествующие с близкими друзьями, соседями, не учитываются в
родственных отношениях parch.
Эта задача, как видим, является хорошо структурированной и поля практически
определены. Собственно, первый этап заключается в том, чтобы подать данные для
машинного обучения. Вопросы выбора и переопределения полей, разбиения,
слияния, классификации — зависят от подачи данных. По большому счету вопрос
упирается в кодирование и нормализацию. Кодирование качественных
признаков(существует несколько подходов) и предварительную обработку данных
перед применением методов машинного обучения.Данная задача не sсодержит
действительно большого объема данных. Но большинство задач(например
Herritage Health Pr., Yandex интернет-математика ) не такие структурированные и
приходиться оперировать миллионами записей. Делать это удобней с
использованием СУБД. Я выбрал СУБД PostgreSQL. Весь SQL код писался под эту
СУБД, но с небольшими изменениями подойдет и для MySQL, Oracle и MS SQL
сервер.
Загружаем данные
Скачиваем два файла — train.csv и test.csv.
Создаем таблицы для хранения данных:
--Удаляем таблицу если она существует
DROP TABLE IF EXISTS titanik_train;
--Создаем таблицу
CREATE TABLE titanik_train
(
survived int,
pclass int,
name varchar(255),
sex varchar(255),
age float,
sibsp int,
parch int,
ticket varchar(255),
fare float,
cabin varchar(255),
embarked varchar(255)
);
--Загружаем таблицу из CSV файла '/home/andrew/titanik/train.csv'.
-- HEADER значит что в CSV файле есть заголовок c именами полей.
COPY titanik_train FROM '/home/andrew/titanik/train.csv' CSV HEADER;
DROP TABLE IF EXISTS titanik_test;
CREATE TABLE titanik_test
(
pclass int,
name varchar(255),
sex varchar(255),
age float,
sibsp int,
parch int,
ticket varchar(255),
fare float,
cabin varchar(255),
embarked varchar(255)
);
COPY titanik_test FROM '/home/andrew/titanik/test.csv' CSV HEADER;
В результате получаем две таблицы: с тренировочными и тестовыми данными.
В этом случае не было ошибок загрузки данных. Т.е. все данные соответствовали
тем типам, которые мы определили. Если же ошибки появляются — тогда есть два
пути: сначала исправить и регулярными выражениями и редактором вроде
sed(либо командой на основе perl -e) или сначала все загрузить в виде текстовых
данных, и исправлять регулярными выражениямии запросами средствами СУБД.
Добавим первичный ключ:
--создаем последовательность
CREATE SEQUENCE titanik_train_seq;
--создаем таблицу с первичным ключем
select nextval('titanik_train_seq') as id, a.*
into titanik_train_pk
from titanik_train a;
Исследование данных
Таблицы СУБД это всего-навсего средство. Средство помогающее удобнее изучать
и преобразовывать данные. С самого начала удобно разделить поля записи про
каждого пассажира на числовые и текстовые — по типу данных. Для применения
машинных алгоритмов обучения, все равно прийдется все преобразовать к
числовому представлению. И от того, насколько адекватно мы это сделаем, очень
сильно зависит качество работы методов, которые мы выберем.
Числовые данные:
survived, pclass, age, sibsp, parch, fare
Текстовые данные:
name, sex, ticket, cabin, embarked
Начнем с данных, которые представлены в текстовом виде. Фактически, это
проблема кодирования информации. И тут не обязательно самый простой подход
будет самым правильным. Например, если отвлечься от этого примера, и
представить, что у нас есть поле, которое определяет должность:

генеральный директор;

начальник отдела;

старший смены;

уборщик;

стажер;
Вполне логично кодировать их таким образом:
генеральный директор
5
1
начальник отдела
4
0.75
старший смены
3
0.5
уборщик
2
0.25
стажер
1
0
Т.е. мы таким образом попытались вписать такой вот элемент реальности как
должности, в математическое представление. Это один(простейший) вариант
подачи такой информации. Это при условии, что «расстояние» между
должностями(что не соответствует реальности) считаем одинаковым. Т.е. тут
возникает вопрос, а что такое «расстояние между должностями»? Важность? Объем
полномочий? Объем ответствености? Распространенность? Место в иерархии? Или
количество работников в такой должности на предприятии? Масса вопросов, на
которые для задачи оторванной от реальности и не будет ответа. А в реальности
ответы есть. И есть методы, которые помогают приблизить математическое
представление к реальности. Но это тема для отдельной и сложной статьи, а пока,
примем, что мы будем стараться кодировать данные в диапазоне от 0 до 1(Код 2).
Итак, для данных которые можно сравнить, есть хоть какое-то объяснение почему
мы именно так, с определением меньшего и большего кодировали даные.
Ведь числа несут количественную информацию! Что-то больше, а что-то меньше!
А если у нас в качестве описательной информации «имя». Имеем ли мы право
кодировать Данные например вот так:
Имя
Код2
Николай
0
Петр
0.5
Павел
1
Вроде бы и можем мы так кодировать, но получается, что мы добавляем к данным
составляющую, которой нет — сравнение имен(числа: больше-меньше) по
неизвестному признаку. Логичней было бы подать данные несколько иначе(Ещё
это называют bag-of-words):
№ записи
Николай
Петр
Пав
15602
0
1
0
15603
0
0
1
15604
1
0
0
И вот сколько будет имен — столько будет и полей. И значения: 0 или 1.
Недостаток налицо — а если у нас будет миллион разных имен? Что делать? В
нашей простой задаче такой беды нет, но отвечу — это довольно динамично
растущая отрасль компьютерных наук и математики — сжатие входных данных.
Одно из ключевых слов — «sparse matrix». Ещё — «autoencoder», «Vowpal
Wabbit»[3], «feature hashing». Опять таки — это тема для статьи. Кроме того, на
хабре мелькали материалы по этой теме. Но для нашей задачи, мы пока отвлечемся
от этой проблемы. И вернемся к вопросу:
А имеет ли первое представление право на жизнь?
Ответ сильно зависит от того, а можем ли мы пренебречь добавленной нами
составляющей о ранжировании «неизвестного признака по которому мы
сравнивали имена». Очень часто приходиться пренебрегать — когда другой
возможности нет.
А если поставить вместо имен например число-буквенные номера билетов(что уже
ближе к нашей задаче). С билетами проще — есть серия и есть номер. Номер
можем оставить так как есть. Возможно он определяет место посадки, и
соответственно дальность расположения от входа и т.п. А вот кодирование серии —
уже сложнее. Прежде всего нужно ответить на вопрос: А может ли одно
значение в реальности быть представлено в виде разных строк в
даных? Ответ однозначный — может. Т.е. нужно это проверить. Самый простой
вариант — опечатка. Лишняя точка, запятая, пробел или тире.
Наличие таких данных уже влечет искажения, и модель кторая построена на
«испорченных» сведениях не покажет хорошего результата (только если это не
делалось контролируемо для улучшения свойства обобщения модели).
Для начала отделим номер билета от серии(типа, марки и т.п.).
Для этого просмотрим данные, предварительно отсортировав по названию.
Выделим группы данных:

билет без серии

билет с серией
Далее поступим таким образом: создадим таблицу, где серия выделена в отдельное
поле. Там где нет серии — пока поставим пустое значение. Для этого
воспользуемся запросом:
select id,survived,pclass,"name",sex,age,sibsp,parch,ticket,fare,cabin,embarked,
m[1] as ticket_type, m[2] as ticket_number
into titanik_train_1
from
(select id,
survived,pclass,"name",sex,age,sibsp,parch,ticket,fare,cabin,embarked,
regexp_matches(ticket, '^\s*?(.*?)\s*?(\d*?)$') as m
from titanik_train_pk
) as a;
где '^\s*?(.*?)\s*?(\d*?)$' — регулярное выражение, позволяющее выделить части
в поле билет — серию и номер в массив.
Номера оставим так как они есть, можно проверить регулярным выражением на
наличие нецифровых символов. При помощи подобного регулярного выражения.
Проблема двойников
Если один и тот же объект реальности именуется по разному — получаем проблему
двойников. Остановимся отлько лишь на одном аспекте обнаружения двойников:
Опечатки в наименовании. Учитывая, что объем наших данных небольшой, и
подлежит простому просматриванию, поступим таким образом — выведем все
уникальные значения поля из таблицы.
Запрос:
select ticket_type, count(ticket_type) from titanik_train_1 group by 1 order by 2 desc
;
А вот результат, где я отметил предположительно одинаковые значения, которые
обозначены разными строками. 45 записей.
Довольно много совпадений. Если вникнуть в задачу глубже и узнать по какому
принципу маркировались билеты — можно еще сократить количество
наименований. Но, одним из условий задачи было не использовать дополнительных
сведений по этой широкоизвестной трагедии. Потому остановимся только на этом.
Запросы для изменения данных выглядят так:
update
titanik_train_1 set ticket_type='A.5.' where ticket_type = 'A./5.';
update
titanik_train_1 set ticket_type='A.5.' where ticket_type = 'A/5';
update
titanik_train_1 set ticket_type='A.5.' where ticket_type = 'A/5.';
update
titanik_train_1 set ticket_type='A.5.' where ticket_type = 'A/S';
update
titanik_train_1 set ticket_type='A/4' where ticket_type = 'A/4.';
update
titanik_train_1 set ticket_type='A/4' where ticket_type = 'A4.';
update
titanik_train_1 set ticket_type='C.A.' where ticket_type = 'CA';
update
titanik_train_1 set ticket_type='C.A.' where ticket_type = 'CA.';
update
titanik_train_1 set ticket_type='SW/PP' where ticket_type = 'S.W./PP';
update
titanik_train_1 set ticket_type='SC/PARIS' where ticket_type = 'SC/Paris';
update
titanik_train_1 set ticket_type='SOTON/O.Q.' where ticket_type = 'SOTON/OQ';
update
titanik_train_1 set ticket_type='SOTON/O2' where ticket_type = 'STON/O 2.';
update
titanik_train_1 set ticket_type='SOTON/O2' where ticket_type = 'STON/O2.';
update
titanik_train_1 set ticket_type='W/C' where ticket_type = 'W./C.';
update
titanik_train_1 set ticket_type='W.E.P.' where ticket_type = 'WE/P';
Теперь записей 30.
В более сложных задачах, обработка существено отличается. Особенно, если
просмотреть все не представляется возможным. Тогда можно воспользоваться
функцией Левенштейна — это позволит найти близкие по написанию слова. Можна
ее немного подправить и сделать слова которые отличаются только знаками
препинания еще ближе. Опять-же возвращаемся к понятию меры и метрики — а
что понимать под растоянием между словами? Символы которые более похожи по
внешнему виду B и 8? Или те которые звучат похоже?
В принципе, таким нехитрым способом нужно пройтись по всем символьным полям
в тренировочной и тестовой таблицах. Ещё, как улучшение можно отметить
объединение данных по столбцам из этих таблиц перед поиском двойников.
О распределенности. На этом этапе работа по обработке данных не требует
специальных знаний и может быть легко распаралеллена между некоторым числом
исполнителей. Вот, например, на этом этапе, команда очень сильно обойдет
соперника-одиночку.
Пост получился довольно объемным, потому продолжение — в следующей части.
Data Mining: Первичная обработка данных при
помощи СУБД. Часть 2
tutorial
SQL*, Data Mining*, Big Data*
Каждые полчаса появляется новая статья с кричащим лозунгом Большие данные
— «новая нефть»!. Просто находка для маркетинговых текстов. Большие Данные
= Большая Нефть = Профит. Откуда взялось данное утверждение? Давайте выйдем
за рамки штампа и копнем чуть глубже:
Одним из первых его употребил Майкл Палмер[1] еще в 2006 году:
Данные это просто сырая нефть. Она ценна, но без переработки она не может быть
по-настоящему использована. Она должна быть превращена в газ, пластик,
химикаты, и т.д., чтобы создать ценность, влекущую прибыльность; так и данные
нужно проанализировать и «раскусить», чтобы они стали ценными.
Такое понимание трендового «Большие данные — новая нефть!» ближе к
реальности чем к маркетингу. И совсем не отменяет высказывания Дизраели:
«Существуют три вида лжи: Есть ложь, наглая ложь и статистика».
Данная статья является продолжением топика Data Mining: Первичная обработка
данных при помощи СУБД. Часть 1
Продолжим добычу!
Продолжение удаления двойников
В прошлой статье был дан небольшой пример кода, который позволяет избавляться
от «двойников». Продолжим двигаться в том же направлении. Для успешной
работы необходимо преобразовать также и таблицу с тестовыми данными:
--последовательность уже создана ранее
--создаем таблицу с первичным ключом
select nextval('titanik_train_seq') as id, a.*
into titanik_test_pk
from titanik_test a;
--разделяем поле с именем билета аналогично способу с тренировочными данными
select id,pclass,"name",sex,age,sibsp,parch,ticket,fare,cabin,embarked,
m[1] as ticket_type, m[2] as ticket_number
into titanik_test_1
from
(select id,pclass,"name",sex,age,sibsp,parch,ticket,fare,cabin,embarked,
regexp_matches(ticket, '^\s*?(.*?)\s*?(\d*?)$') as m
from titanik_test_pk
) as a;
В этом примере кода я решил использовать последовательность из предыдущей
таблицы, для того, чтобы легче было объединять данные в случае необходимости
потом. Разделение наименования билета на текст и серию проводится точно также.
Применяем аналогичные операторы обновления к тестовой таблице(плюс добавим
еще два, в конце, для замены элементов, которых не было в тренировочной
таблице):
update
titanik_test_1 set ticket_type='A.5.' where ticket_type = 'A./5.';
update
titanik_test_1 set ticket_type='A.5.' where ticket_type = 'A/5';
update
titanik_test_1 set ticket_type='A.5.' where ticket_type = 'A/5.';
update
titanik_test_1 set ticket_type='A.5.' where ticket_type = 'A/S';
update
titanik_test_1 set ticket_type='A/4' where ticket_type = 'A/4.';
update
titanik_test_1 set ticket_type='A/4' where ticket_type = 'A4.';
update
titanik_test_1 set ticket_type='C.A.' where ticket_type = 'CA';
update
titanik_test_1 set ticket_type='C.A.' where ticket_type = 'CA.';
update
titanik_test_1 set ticket_type='SW/PP' where ticket_type = 'S.W./PP';
update
titanik_test_1 set ticket_type='SC/PARIS' where ticket_type = 'SC/Paris';
update
titanik_test_1 set ticket_type='SOTON/O.Q.' where ticket_type = 'SOTON/OQ';
update
titanik_test_1 set ticket_type='SOTON/O2' where ticket_type = 'STON/O 2.';
update
titanik_test_1 set ticket_type='SOTON/O2' where ticket_type = 'STON/O2.';
update
titanik_test_1 set ticket_type='W/C' where ticket_type = 'W./C.';
update
titanik_test_1 set ticket_type='W.E.P.' where ticket_type = 'WE/P';
update
titanik_test_1 set ticket_type='SOTON/O.Q.' where ticket_type = 'STON/OQ.';
update
titanik_test_1 set ticket_type='SC/PARIS' where ticket_type = 'S.C./PARIS';
Данные о билетах — обработали. Теперь необходимо по такому же принципу
обработать оставшиеся текстовые данные:
Пол(sex) — двойников не обнаружено, в разделении не нуждается:
select sex, count(sex) from titanik_train_1 group by 1 order by 1 asc;
sex
count
female
314
male
577
select sex, count(sex) from titanik_test_1 group by 1 order by 1 asc;
sex
female
count
152
male
266
Каюты(cabin) — здесь интереснее:
--опасный запрос!
-- select cabin, count(cabin) from titanik_train_1 group by 1 order by 1 asc;
select cabin, count(id) from titanik_train_1 group by 1 order by 1 asc;
Если выполнить первый запрос(закомментированный), то получим довольно
странное значение — 0 записей у которых не указана каюта. Это связано с
особенностями работы агрегирующих функций. Не умеет правильно складывать
пустые значения. А потому, пишем count(id). И получаем результат: 687
пассажиров с неуказанной каютой. Можно сделать предположение, что это
«общий» отсек. И скорее всего для этих записей не указан класс билета.
Проверим наше предположение:
select id, cabin, ticket_type from titanik_train_1 where cabin ISNULL;
select id, cabin, ticket_type from titanik_train_1 where cabin NOTNULL;
Не подтвердилось. Вывело много строк, для которых указан тип билета. А
наоборот(запрос номер 2)? Тоже не подтвердилось. Делаем вывод, что либо серия
билета утеряна для определенного количества людей, либо показывает что-то
другое, а не расположение человека в каюте или нет. То есть, несет
дополнительную информацию. Возвращаемся к предыдущему запросу.
В выводе запросов по кабинам и количеству записей с группировкой, есть
интересные строки:
cabin
count
C23 C25 C27
4
C30
1
F G73
2
T
1
Во первых — информация о типе каюты(первая буква перед цифрами).
Во вторых, на один билет — несколько кают. И очень часто по нескольку человек в
одной каюте с билетами, в которых указано несколько кают(читай мест). Это
получается довольно интересные данные, которые нельзя игнорировать.
Фактически это данные дублирующие родственников, но учитывающие, например
друзей или знакомых, или коллег по работе — т.е. знакомых людей, готовых
помогать друг другу. Также, получаем информацию, сколько человек было в каюте.
Вывод — добавляем поле тип кабины. И добавляем количество кают в билете.
Также добавим поле количество человек в каютах.
Т.е. семья из 4х человек занимает 2 каюты. Или например, два разных человека
занимают одну каюту. Количество данных растет!
Запросы которые это реализуют довольно сложные и требуют понимания работы
регулярных выражений PREG в PostgreSQL.
Можно все вместить в один огромный запрос, но я решил разделить на две части.
Часть один определяет тип каюты и количество кают на билет, а второй запрос
определяет количество человек с такой же каютой(набором кают) в билете.
select id,survived,pclass,"name",sex,age,sibsp,parch,ticket,fare,cabin,embarked, cnt a
s cabin_cnt, cabin_type, ticket_type, ticket_number
into titanik_train_2
from (
select id, survived, pclass,"name",sex,age,sibsp,parch,ticket,fare,cabin,embarked, tic
ket_type, ticket_number,
regexp_matches(cabin, '^\s*?([A-Z]\d*)\s*?([A-Z]\d*)\s*?([A-Z]\d*)\s*?([A-Z]\d*)\s*?.*
?$') as m, 4 as cnt,
substring(cabin, 1, 1) as cabin_type
from titanik_train_1
UNION
select id, survived, pclass,"name",sex,age,sibsp,parch,ticket,fare,cabin,embarked, tic
ket_type, ticket_number,
regexp_matches(cabin, '^\s*?([A-Z]\d*)\s*?([A-Z]\d*)\s*?([A-Z]\d*)\s*?$') as m, 3 as c
nt,
substring(cabin, 1, 1) as cabin_type
from titanik_train_1
UNION
select id, survived, pclass,"name",sex,age,sibsp,parch,ticket,fare,cabin,embarked, tic
ket_type, ticket_number,
regexp_matches(cabin, '^\s*?([A-Z]\d*)\s*?([A-Z]\d*)\s*?$') as m, 2 as cnt,
substring(cabin, 1, 1) as cabin_type
from titanik_train_1
UNION
select id, survived, pclass,"name",sex,age,sibsp,parch,ticket,fare,cabin,embarked, tic
ket_type, ticket_number,
regexp_matches(cabin, '^\s*?([A-Z]\d*)\s*?$') as m, 1 as cnt,
substring(cabin, 1, 1) as cabin_type
from titanik_train_1
UNION
select id, survived, pclass,"name",sex,age,sibsp,parch,ticket,fare,cabin,embarked, tic
ket_type, ticket_number,
NULL as m, 0 as cnt, NULL as cabin_type
from titanik_train_1 where cabin ISNULL)
as a;
В принципе, единственно сложный момент здесь — регулярное выражение. Как я
его строил:
F С82 С45 — пример наименования которые нужно выхватить. Этот запрос строится
из основного блока:
([A-Z]\d*) — [A-Z] означает что должен быть хоть один, буквенный большой
символ, \d* — любое количество 0… цифр.
И второй запрос, который считает количество людей в каютах.
select a.*, b.cnt as cabin_people_cnt
into
titanik_train_3
from
titanik_train_2 a, (
select cabin as cabid, count(id) as cnt from titanik_train_1 group by 1) as b
where
a.cabin = b.cabid OR (a.cabin ISNULL AND b.cabid ISNULL);
--Обновляем. Если кабин не указана - то ставим ноль вместо 687.
update titanik_train_3 set cabin_people_cnt=0 where cabin ISNULL;
В результате получаем три дополнительных поля: тип каюты, количество кают на
билет и количество людей в каюте.
Аналогично делаем для тестовых данных:
select id,pclass,"name",sex,age,sibsp,parch,ticket,fare,cabin,embarked, cnt as cabin_c
nt, cabin_type, ticket_type, ticket_number
into titanik_test_2
from (
select id,
pclass,"name",sex,age,sibsp,parch,ticket,fare,cabin,embarked, ticket_type,
ticket_number,
regexp_matches(cabin, '^\s*?([A-Z]\d*)\s*?([A-Z]\d*)\s*?([A-Z]\d*)\s*?([A-Z]\d*)\s*?.*
?$') as m, 4 as cnt,
substring(cabin, 1, 1) as cabin_type
from titanik_test_1
UNION
select id,
pclass,"name",sex,age,sibsp,parch,ticket,fare,cabin,embarked, ticket_type,
ticket_number,
regexp_matches(cabin, '^\s*?([A-Z]\d*)\s*?([A-Z]\d*)\s*?([A-Z]\d*)\s*?$') as m, 3 as c
nt,
substring(cabin, 1, 1) as cabin_type
from titanik_test_1
UNION
select id,
pclass,"name",sex,age,sibsp,parch,ticket,fare,cabin,embarked, ticket_type,
ticket_number,
regexp_matches(cabin, '^\s*?([A-Z]\d*)\s*?([A-Z]\d*)\s*?$') as m, 2 as cnt,
substring(cabin, 1, 1) as cabin_type
from titanik_test_1
UNION
select id,
pclass,"name",sex,age,sibsp,parch,ticket,fare,cabin,embarked, ticket_type,
ticket_number,
regexp_matches(cabin, '^\s*?([A-Z]\d*)\s*?$') as m, 1 as cnt,
substring(cabin, 1, 1) as cabin_type
from titanik_test_1
UNION
select id,
pclass,"name",sex,age,sibsp,parch,ticket,fare,cabin,embarked, ticket_type,
ticket_number,
NULL as m, 0 as cnt, NULL as cabin_type
from titanik_test_1 where cabin ISNULL)
as a;
и вторая часть:
select a.*, b.cnt as cabin_people_cnt
into
titanik_test_3
from
titanik_test_2 a, (
select cabin as cabid, count(id) as cnt from titanik_test_1 group by 1) as b
where
a.cabin = b.cabid OR (a.cabin ISNULL AND b.cabid ISNULL);
Осталось одно поле: порт посадки (embarked):
select embarked, count(id) from titanik_train_3 group by 1 order by 1 asc;
select embarked, count(id) from titanik_test_3 group by 1 order by 1 asc;
Результат такой — двойников не обнаружено, в разделении не нуждается:
embarked
count
C
168
Q
77
S
644
2
Что делать с двумя записями где нет данных? Можно заменить случайными
значениями, можно отбросить, можно поставить среднее. На выбор.
Выводы
В этой части мы предварительно подготовили текстовые данные в тренировочной и
тестовой выборке. По времени, данная работа заняла порядка трех часов. От
скачивания данных — до текущего момента.
Эта часть получилась довольно внушительной по объему, потому продолжение в
следующем посте. В следующем посте мы попытаемся уже сформировать таблицу с
числовыми значениями вместо строковых. Если вдруг кто решит делать
одновременно со мной, используя запросы и обрабатывая данные по этому
туториалу — в коментах отвечу на вопросы. Жду критики.
Data Mining: Первичная обработка данных при
помощи СУБД. Часть 3 (Сводные таблицы)
SQL*, Data Mining*, Big Data*
Данная серия посвящена анализу данных для поиска закономерностей. В качестве
примера используется одна из обучающих задач сообщества спортивного анализа
данных Kaggle. Хотя размеры данных для задачи не большие, методы обработки,
которые будут рассматриваться вполне применимы для больших объемов данных.
После выполнения Часть 1 и Части 2 сформировались две таблицы, содержащие
преобразованные данные.
titanik_test_3 и titanik_train_3.
Структура полей у них различается на одно поле — survived, значение которого
нам предстоит определить для тестового набора данных. Вот код описывающий
структуру таблицы titanik_train_3
CREATE TABLE titanik_train_3
(
id bigint,
survived integer,
pclass integer,
name character varying(255),
sex character varying(255),
age double precision,
sibsp integer,
parch integer,
ticket character varying(255),
fare double precision,
cabin character varying(255),
embarked character varying(255),
cabin_cnt integer,
cabin_type text,
ticket_type text,
ticket_number text,
cabin_people_cnt integer
)
Фактически, стоит задача превратить таблицу с символьно-числовыми данными — в
таблицу с только числовым представлением. Поможет нам в этом создание
словарей данных и сводные таблицы. Для этого, числовые данные перенесем в том
же виде в котором они и были, а символьные — закодируем.
Важнейшим условием использования словарей, является полное покрытие
значений. Потому, оптимально на этом этапе(хотя в принципе можно и ранее)
слить таблицы в одну. А в недостающее поле поставить NULL.
Учитывая что для создания первичного ключа использоваласть одна и таже
последовательность, проблем быть не должно. Это делается при помощи оператора
UNION.
select a.* into titanik_full_1 from (
select * from
titanik_train_3
union
select
id,
NULL::integer as survived,
pclass, "name",
sex ,
age ,
sibsp ,
cabin_type,ticket_type,ticket_number,
cabin_people_cnt
from
) as a;
titanik_test_3
parch, ticket,fare,cabin,embarked,cabin_cnt,
Теперь получаем одну таблицу которая содержит и тестовый и тренировочный
набор данных.
Уберем все поля, кроме числовых:
select a.* into titanik_full_2 from (
select id, survived, pclass::float, age::float, sibsp::float, parch::float, fare::flo
at, cabin_cnt::float, CAST(ticket_number as float) as ticket_number, cabin_people_cnt:
:float
from titanik_full_1 where ticket_number != ''
union
select id, survived, pclass, age, sibsp, parch, fare, cabin_cnt, 0 as ticket_number,
cabin_people_cnt
from titanik_full_1 where ticket_number = '' ) as a;
Получаем таблицу titanik_full_2, которая выглядит таким образом:
CREATE TABLE titanik_full_2
(
id bigint,
survived integer,
pclass integer,
age double precision,
sibsp integer,
parch integer,
fare double precision,
cabin_cnt integer,
ticket_number integer,
cabin_people_cnt bigint
)
Теперь в эту таблицу мы будем добавлять по полю, которое будет означать, есть
то, либо иное значение у свойства для этой строки. Такие таблицы называются
сводными(pivot tables), только немного не такими как обычно, поля-values будут
принимать либо 0 либо 1. Схематически это тоборажено на рисунке:
Т.е. таблица теперь стала больше, количество полей будет равно количеству
уникальных значений. В принципе, все эти значения можно сделать вручную по
запросам. Но лучше написать небольшую функцию на PL/PGSQL,
котораяавтоматически будет разворачивать поля.
CREATE OR REPLACE FUNCTION sparse_matrix_generator(
tablename_source character varying,
tablename_dest character varying,
field_name character varying)
RETURNS integer AS
$$
DECLARE
pgst_object
REFCURSOR;
unival character varying;
BEGIN
OPEN pgst_object FOR EXECUTE 'select distinct '||field_name ||' from '||tablename_so
urce ||' where ' || field_name ||' NOTNULL';
LOOP
FETCH pgst_object INTO unival;
EXIT WHEN NOT FOUND;
EXECUTE ' ALTER TABLE '|| tablename_dest ||' ADD COLUMN "'|| field_name||unival
||'" smallint NOT NULL DEFAULT 0';
EXECUTE 'UPDATE '||tablename_dest||' SET "'||field_name||unival|| '"= 1 FROM ' |
|tablename_source||
' WHERE '||tablename_dest||'.id = '||tablename_source||'.id and '||field_name||'
= '''||unival||'''';
END LOOP;
RETURN 0;
END;
$$
LANGUAGE 'plpgsql';
Применяется эта функция так:
select sparse_matrix_generator('titanik_full_1', 'titanik_full_2', 'cabin_type');
select sparse_matrix_generator('titanik_full_1', 'titanik_full_2', 'ticket_type');
select sparse_matrix_generator('titanik_full_1', 'titanik_full_2', 'embarked');
select sparse_matrix_generator('titanik_full_1', 'titanik_full_2', 'sex');
Таким образом у нас теперь есть разреженная матрица размерностью в 58
столбцов. Необходимо пронормировать ее и отделить тестовую и тренировочную
выборки по полю survived.
Есть разные способы нормирования. Для разных методов анализа данных есть
разные требования к выборке. Воспользуемся одним из наиболее простых,
минимаксным нормированием. Суть вкратце такова: минимум будет 0, максимум: 1,
а все остальное расоложено пропорционально между ними. Для этого напишем
функцию:
CREATE OR REPLACE FUNCTION minmax_normalizer(tablename_source character varying, field
_name character varying)
RETURNS integer AS
$BODY$
DECLARE
pgst_object
REFCURSOR;
maxval float;
minval float;
C RECORD;
BEGIN
EXECUTE 'select min("'||field_name ||'") as minval, max("'||field_name ||'") as maxv
al from '|| tablename_source INTO C;
maxval := C.maxval;
minval := C.minval;
EXECUTE ' UPDATE '||tablename_source||' SET "'||field_name||'"=("'||field_name||'"-$
1)/($2-$1)' USING minval, maxval;
RETURN 0;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
И применим ее к полям таблицы, которые нужно нормализовать:
select minmax_normalizer('titanik_full_2', 'pclass');
select minmax_normalizer('titanik_full_2', 'age');
select minmax_normalizer('titanik_full_2', 'sibsp');
select minmax_normalizer('titanik_full_2', 'parch');
select minmax_normalizer('titanik_full_2', 'fare');
select minmax_normalizer('titanik_full_2', 'cabin_cnt');
select minmax_normalizer('titanik_full_2', 'ticket_number');
select minmax_normalizer('titanik_full_2', 'cabin_people_cnt');
В результате получим таблицу с только числовыми значениями в диапазоне от нуля
до единицы.
Выберем тестовую и тренировочную выборку:
select * into titanik_test_final from
titanik_full_2 where survived isnull;
alter table titanik_test_final drop column survived;
для тестовой выборки и соответственно:
select * into titanik_train_final from
titanik_full_2 where survived notnull;
для тренировочной.
В данной таблице есть пустые значения. Их можно заменить, например средним
значением. Для этого также воспользуемся функцией:
CREATE OR REPLACE FUNCTION null_normalizer(tablename_source character varying)
RETURNS integer AS
$BODY$
DECLARE
pgst_object
REFCURSOR;
fieldval character varying;
count_null integer;
field_avg float;
BEGIN
OPEN pgst_object FOR EXECUTE 'select column_name from information_schema.columns whe
re'||
' table_name='''||tablename_source||'''';
LOOP
FETCH pgst_object INTO fieldval;
EXIT WHEN NOT FOUND;
count_null := 0;
EXECUTE ' select count(id) from '||tablename_source||' where "'||fieldval||'" is
null' into count_null;
IF count_null > 0 THEN
raise notice 'field: %', fieldval;
EXECUTE 'select avg('||fieldval||') from '||tablename_source INTO field_avg;
EXECUTE 'UPDATE '||tablename_source||' set '||fieldval||'= $1 where '||fieldval|
|' isnull' using field_avg;
END IF;
END LOOP;
RETURN 0;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
Функция работает таким образом: Выбираем все имена полей для таблицы, считаем
количество ненулевых элементов в поле, и если количество больше нуля —
запускаем поиск среднего значение и обновление пустых значений на среднее.
Вызывается функция таким образом:
select null_normalizer('titanik_test_final');
select null_normalizer('titanik_train_final');
Результирующая таблица получилась достаточно большая и разреженная:
CREATE TABLE titanik_test_final
(
id bigint,
pclass double precision,
age double precision,
sibsp double precision,
parch double precision,
fare double precision,
cabin_cnt double precision,
ticket_number double precision,
cabin_people_cnt double precision,
"cabin_typeF" smallint,
"cabin_typeB" smallint,
"cabin_typeG" smallint,
"cabin_typeC" smallint,
"cabin_typeT" smallint,
"cabin_typeD" smallint,
"cabin_typeE" smallint,
"cabin_typeA" smallint,
"ticket_typeSW/PP" smallint,
"ticket_typeC" smallint,
"ticket_typePC" smallint,
"ticket_typeAQ/3." smallint,
"ticket_typeSC/A.3" smallint,
"ticket_typeS.O.C." smallint,
"ticket_typeS.O./P.P." smallint,
"ticket_typeSC/AH" smallint,
"ticket_typeSOTON/O2" smallint,
"ticket_typeC.A." smallint,
"ticket_typeW/C" smallint,
"ticket_typeS.C./A.4." smallint,
"ticket_typeFa" smallint,
"ticket_typeLP" smallint,
"ticket_typeSCO/W" smallint,
"ticket_typeF.C." smallint,
"ticket_typeA.5." smallint,
"ticket_typeSC/AH Basle" smallint,
"ticket_typeSC/A4" smallint,
"ticket_typeS.C./PARIS" smallint,
"ticket_typeS.O.P." smallint,
"ticket_typeLINE" smallint,
"ticket_typeSO/C" smallint,
"ticket_typeP/PP" smallint,
"ticket_typeAQ/4" smallint,
"ticket_typeSC" smallint,
"ticket_typeW.E.P." smallint,
"ticket_typeSOTON/O.Q." smallint,
"ticket_typeA/4" smallint,
"ticket_typeSC/PARIS" smallint,
"ticket_typeA. 2." smallint,
"ticket_typeF.C.C." smallint,
"ticket_typeS.P." smallint,
"ticket_typePP" smallint,
"ticket_typeC.A./SOTON" smallint,
"embarkedC" smallint,
"embarkedQ" smallint,
"embarkedS" smallint,
sexfemale smallint,
sexmale smallint
)
Для выведение в текстовые файлы используем скрипт:
COPY titanik_train_final to '/tmp/titanik_train_final.csv';
COPY titanik_test_final to '/tmp/titanik_test_final.csv';
Собственно, данные готовы. Теперь можем попытаться найти закономерности.
Для уменьшения размерности, теперь разреженой таблицы, можно воспользоваться
автоэнкодером, либо линейным PCA. Продолжение в следующей части.
Планируется применить автоэнкодер и решающий лес и посмотреть на результат,
который получиться в турнирной таблице.
Data mining: Инструментарий — Theano
tutorial
Python*, Data Mining*, Big Data*
В предыдущих материалах этого цикла мы рассматривали методы предварительной
обработки данных при помощи СУБД. Это может быть полезно при очень больших
объемах обрабатываемой информации. В этой статье я продолжу описывать
инструменты для интеллектуальной обработки больших объёмов данных,
остановившись на использовании Python и Theano.
Рассмотрим задачи, которые мы будем решать, и какими инструментами для этого
воспользуемся. Читать предыдущие части [1,2,3] желательно, но не обязательно.
Исходные данные, которые были получены в результате выполнения предыдущих
частей цикла статей можно взять отсюда[Исходные данные].
Задачи
Если среди данных есть скрытые взаимосвязи, корреляция между признаками,
структурные связи, то в таком случае возникает задача уменьшения размерности
входных данных. Эта проблема особенно ярко себя проявляет в ситуации, когда
обрабатывается большое число разреженных данных.
Для сравнения методов уменьшения размерности данных мы собираемся
рассмотреть метод нейросетевого сжатия (Autoencoder) и метод главных компонент
(PCA).
Классический путь решения этой задачи — метод анализа главных компонент
(PCA). В википедии неплохо описано [4], там же есть ссылка [5] на сайт с
примерами в Excel, и подробным и довольно понятным описанием. Вот еще ссылки
на статьи на Хабре[6,7].
Как правило, результаты уменьшения размерности входных данных, в первую
очередь сравнивают именно с результатами применения этого метода.
В статье [8] описано что такое автоенкодер и как он работает, потому основной
материал будет посвящен именно реализации и применению этого подхода.
Бангио и Йошуа написали хороший и внятный обзор применения нейросетевого
сжатия, если вдруг кому-нибудь нужна библиография и ссылки на работы в этом
направлении, чтобы сравнивать свои результаты с существующими, в работе [9],
раздел 4.6, стр. 48 — 50.
В качестве инструментария, я решил применять Python и Theano[10]. Этот пакет я
обнаружил после прослушивания курсов Neural Networks for Machine Learning,
изучая ссылки предоставленные преподавателем J. Hinton. При помощи этого
пакета реализованы нейронные сети «глубокого обучения» (deep learning neural
networks). Такого рода сети, как было описано в лекциях и статье [11,12] легли в
основу системы для распознавания голоса, используемой Google в Android.
Этот подход к построению нейронных сетей по мнению некоторых ученых является
достаточно перспективным, и уже показывает неплохие результаты. Потому мне
стало интересно применить его для решения задач на Kaggle.
Также, на сайте deeplearning.net достаточно много примеров построения систем
машинного обучения с использованием именно этого пакета.
Инструменты
В документации разработчиков, дано такое определение:
Theano — это библиотека Python и оптимизирующий компилятор, которые
позволяют определять, оптимизировать и вычислять математические выражения
эффективно используя многомерные массивы
Возможности библиотеки:

тесная интеграция с NumPy;

прозрачное использование GPU;

эффективное дифференцирование переменных;

быстрая и стабильная оптимизация;

динамическая генерация кода на C;

расширенные возможности юнит-тестирования и самопроверок;
Theano используется в высокоинтенсивных вычислительных научных исследований
с 2007 года.
По сути, программирование под Theano не является программированием в полном
смысле этого слова, так как пишется программа на Python, которая создает
выражение для Theano.
С другой стороны, это является программированием, так как мы объявляем
переменные, создаем выражение которое говорит что делать с этими
переменнымии компилируем эти выражения в функции, которые используются при
вычислении.
Если кратко, то вот список того, что именно может делать Theano в отличии от
NumPy.

оптимизация скорости выполнения: Theano может использовать g++ или nvcc
для того чтобы откомпилировать части части вашего выражения в инструкции
GPU или CPU, которые выполняются намного быстрее чистого Python;

диференциицирование переменных: Theano может автоматически строить
выражения для вычисления градиента;

стабильность оптимизации: Theano может распознать некоторые численно
неточно вычисляемые выражения и рассчитать их используя более надежные
алгоритмы
Наиболее близкий к Theano пакет это sympy. Пакет sympy можно сравнить с
Mathematica, в то время как NumPy более похож на пакет MATLAB. Theano является
гибридом, разработчики которого попытались взять лучшие стороны обоих этих
пакетов.
Скорее всего, в продолжение цикла, будет написана отдельная статью о том как
настроить theano, подключить использование CUDA GPU к тренировке нейронных
сетей и решать проблемы, возникающие при установке.
А пока, на моей операционной системе Linux Slackware 13.37 установлен python2.6, setuptools-0.6c9-py2.6, g++, python-dev, NumPy, SciPy, BLAS. Подробный
мануал по установке для распространеных ОС на английском языке: [12].
Прежде чем мы начнем снижать размерность данных, попытаемся реализовать
простой пример использования Theano для написания функции, которая позволит
нам решить нашу задачу о разбиении группы на две части.
В качестве метода для разбиения данных на два класса воспользуемся
логистической регрессией[13].
Код примера основывается на [14] с некоторыми изменениями:
import numpy
import theano
import theano.tensor as T
import csv as csv
#Открываем и читаем csv файл
csv_file_object = csv.reader(open( './titanik_train_final.csv' , 'rb' ), delimiter= '\t'
)
data=[]
for row in csv_file_object:
data.append(row)
data = numpy.array(data)
data = data.astype( 'float' )
#Создаем переменную-массив
# Каждую строку
# заносим в
массив data
# Превращаем обычный массив в тип numpy.array
# Конвертируем в тип float
Y = data[:, 1 ]
# Все строки первого столбца заносим в Y
X = data[:, 2 :]
# Все строки со второго столбца и до последн
его заносим в X
#Аналогично для тестовой выборки, только без Y
csv_file_object = csv.reader(open( './titanik_test_final.csv' , 'rb' ), delimiter= '\t' )
data_test=[]
for row in csv_file_object:
data_test.append(row)
Tx = numpy.array(data_test)
Tx = Tx.astype( 'float' )
Tx = Tx[:, 1 :]
#Создаем объект для получения случайных чисел
rng = numpy.random
#Количество строк
N = 891
#Количество столбцов
feats = 56
#Количество тренировочных шагов
training_steps = 10000
# Декларируем символьные переменные Theano
x = T.matrix( "x" )
y = T.vector( "y" )
w = theano.shared(rng.randn(feats), name= "w" )
b = theano.shared( 0. , name= "b" )
# Создаем «выражение» Theano
p_1 = 1 / ( 1 + T.exp(-T.dot(x, w) - b))
равен
# Вероятность того, что результат
1
prediction = p_1 > 0.5
# Порог для прогнозирования
xent = -y * T.log(p_1) - ( 1 -y) * T.log( 1 -p_1) # Функция ошибки для перекрестной энтр
опии
cost = xent.mean() + 0.01 * (w ** 2 ).sum()
gw,gb = T.grad(cost, [w, b])
# Стоимость минимизации
# Рассчитываем градиент стоимос
ти
# Компилируем «выражение» Theano
train = theano.function(
inputs=[x,y],
outputs=[prediction, xent],
updates=((w, w - 0.1 * gw), (b, b - 0.1 * gb)))
predict = theano.function(inputs=[x], outputs=prediction)
# Тренировка модели
for i in range(training_steps):
pred, err = train(X, Y)
#Прогнозирование
P = predict(Tx)
#Сохраняем в файл
numpy.savetxt( './autoencoder.csv' ,P, '%i' )
Наиболее интересными в этом коде являются строки:
# Создаем «выражение» Theano
p_1 = 1 / ( 1 + T.exp(-T.dot(x, w) - b))
равен
# Вероятность того, что результат
1
prediction = p_1 > 0.5
# Порог для прогнозирования
xent = -y * T.log(p_1) - ( 1 -y) * T.log( 1 -p_1) # Функция ошибки для перекрестной энтр
опии
cost = xent.mean() + 0.01 * (w ** 2 ).sum()
gw,gb = T.grad(cost, [w, b])
# Стоимость минимизации
# Рассчитываем градиент стоимос
ти
# Компилируем «выражение» Theano
train = theano.function(
inputs=[x,y],
outputs=[prediction, xent],
updates=((w, w - 0.1 * gw), (b, b - 0.1 * gb)))
predict = theano.function(inputs=[x], outputs=prediction)
Здесь происходит следующее(разбираем с конца !): У нас есть два
«выражения» train и predict. Они «компилируются»(это не только компиляция, но
в начале изучения лучше будет упростить) при помощи theano.function в вид,
который может быть потом вызван и исполнен.
Входными параметрами у нас для прогнозирования является x, а выходным —
выражение prediction, которое равно p_1 > 0,5 — т. е. порог, который
возвращает значение да/нет, т. е. значения 0 или 1. В свою очередь,
выражение p_1 содержит сведения о том, что именно нужно делать с
переменной х, а именно:
p_1 = 1 / ( 1 + T.exp(-T.dot(x, w) - b))
где x, w, b — переменные, при этом w и b мы определяем при помощи
выражения train.
Для train входными данными будут x, y, а выходными prediction и xent. При этом мы
будем обновлять(искать оптимальные значения) для w и b, обновляя их по
формулам
w- 0.1 *gw, b- 0.1 *gb
где gw и gb — градиенты, связанные с ошибкой xent через выражение cost.
А ошибка, вычисляемая по формуле, получается из вот такого выражения:
xent = -y * T.log(p_1) - ( 1 -y) * T.log( 1 -p_1)
И когда мы «компилируем» выражения predict и train, Theano берет все
необходимые выражения, создает для них код на С для CPU/GPU и исполняет их
соответствующим образом. Это дает заметный прирост в производительности, при
этом не лишая нас удобств использования среды Python.
Результатом исолнения указанного кода будет качество прогноза, равное 0.765555
про правилам оценивания Kaggle для соревнования Titanik.
В следующих статьях цикла мы попытаемся уменьшить размерность задачи
используя разные алгоритмы, а результаты сравним.
Download