Введение
Сегодня я продолжу рассказ о применении методов анализа данных и машинного обучения на практических примерах. В прошлой статье мы с вами разбирались с задачей кредитного скоринга. Ниже я попытаюсь продемонстрировать решение другой задачи с того же турнира, а именно «Задачи о паспортах» (Задание №2).
При решении будут показаны основы анализа текстовой информации, а также ее кодирование для построения модели с помощью Python и модулей для анализа данных (pandas, scikit-learn, pymorphy).
Постановка задачи
При работе с большим объёмом данных важно поддерживать их чистоту. А при заполнении заявки на банковский продукт необходимо указывать полные паспортные данные, в том числе и поле «кем выдан паспорт», число различных вариантов написаний одного и того же отделения потенциальными клиентами может достигать нескольких сотен. Важно понимать, не ошибся ли клиент, заполняя другие поля: «код подразделения», «серию/номер паспорта». Для этого необходимо сверять «код подразделения» и «кем выдан паспорт».
Задача заключается в том, чтобы проставить коды подразделений для записей из тестовой выборки, основываясь на обучающей выборке.
Предварительная обработка данных
Загрузим данные и посмотрим, что мы имеем:
from pandas import read_csv import pymorphy2 from sklearn.feature_extraction.text import HashingVectorizer from sklearn.cross_validation import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score, roc_auc_score from sklearn.decomposition import PCA train = read_csv('https://static.tcsbank.ru/documents/olymp/passport_training_set.csv',';', index_col='id' ,encoding='cp1251') train.head(5)
passport_div_code | passport_issuer_name | passport_issue_month/year | |
---|---|---|---|
id | |||
1 | 422008 | БЕЛОВСКИМ УВД КЕМЕРОВСКОЙ ОБЛАСТИ | 11M2001 |
2 | 500112 | ТП №2 В ГОР. ОРЕХОВО-ЗУЕВО ОУФМС РОССИИ ПО МО … | 03M2009 |
3 | 642001 | ВОЛЖСКИМ РОВД ГОР.САРАТОВА | 04M2002 |
4 | 162004 | УВД МОСКОВСКОГО РАЙОНА Г.КАЗАНЬ | 12M2002 |
5 | 80001 | ОТДЕЛОМ ОФМС РОССИИ ПО РЕСП КАЛМЫКИЯ В Г ЭЛИСТА | 08M2009 |
Теперь можно посмотреть как пользователи записывают поле «кем выдан паспорт» на примере какого-либо подразделения:
example_code = train.passport_div_code[train.passport_div_code.duplicated()].values[0] for i in train.passport_issuer_name[train.passport_div_code == example_code].drop_duplicates(): print i
ОТДЕЛЕНИЕМ УФМС РОССИИ ПО РЕСПУБЛИКЕ КАРЕЛИЯ В МЕДВЕЖ. Р-Е
ОТДЕЛЕНИЕМ УФМС РОССИИ ПО Р. КАРЕЛИЯ В МЕДВЕЖЬЕГОРСКОМ РАЙОНЕ
ОТДЕЛЕНИЕМ УФМС РОССИИ ПО РЕСП КАРЕЛИЯ В МЕДВЕЖЬЕГОРСКОМ Р-НЕ
ОТДЕЛЕНИЕМ УФМС РОССИИ ПО РЕСПУБЛИКЕ КАРЕЛИЯ В МЕДВЕЖЬЕГОРСКОМ РАЙОНЕ
ОУФМС РОССИИ ПО РЕСПУБЛИКЕ КАРЕЛИЯ В МЕДВЕЖЬЕГОРСКОМ РАЙОНЕ
УФМС РОССИИ ПО РК В МЕДВЕЖЬЕГОРСКОМ РАЙОНЕ
ОТДЕЛЕНИЕМ УФМС РОССИИ ПО РЕСПУБЛИКЕ КАРЕЛИЯ МЕДВЕЖЬЕГОРСКОМ Р-ОНЕ
ОТДЕЛЕНИЕМ УФМС РОССИИ ПО РК В МЕДВЕЖЬЕГОРСКОМ РАЙОНЕ
ОТДЕЛЕНИЕМ УФМС РОССИИ ПО РЕСПУБЛИКЕ КОРЕЛИЯ В МЕДВЕЖИГОРСКОМ РАЙОНЕ
УФМС РОССИИ ПО Р. КАРЕЛИЯ МЕДВЕЖЬЕГОРСКОГО Р-НА
ОТДЕЛОМ УФМС РОССИИ ПО РЕСПУБЛИКЕ КАРЕЛИЯ В МЕДВЕЖЬЕГОРСКОМ
УФМС РЕСПУБЛИКИ КАРЕЛИИ МЕДВЕЖЬЕГОРСКОГО Р-ОН
МЕДВЕЖЬЕГОРСКИМ ОВД
Как можно заметить нужно на поле действительно заполняется криво. Но для нормально кодирования мы должны привести это поле к более-менее нормальному (однозначному) виду.
Для начала я бы предложил привести все записи к одному регистру, например, чтобы все буквы стали строчными. Это легко сделать с помощью атрибута str, столбца DataFrame’a. Этот атрибут позволяет работать со столбцом как с строкой, а также выполнять различного рода поиск и замену по регулярным выражениям:
train.passport_issuer_name = train.passport_issuer_name.str.lower() train[train.passport_div_code == example_code].head(5)
passport_div_code | passport_issuer_name | passport_issue_month/year | |
---|---|---|---|
id | |||
19 | 100010 | отделением уфмс россии по республике карелия в… | 04M2008 |
22 | 100010 | отделением уфмс россии по р. карелия в медвежь… | 10M2009 |
5642 | 100010 | отделением уфмс россии по респ карелия в медве… | 08M2008 |
6668 | 100010 | отделением уфмс россии по республике карелия в… | 08M2011 |
8732 | 100010 | отделением уфмс россии по республике карелия в… | 08M2012 |
C регистром определились. Далее надо по возможности избавиться от популярных сокращений, например район, город и т.д. Сделаем это с помощью регулярных выражений. Pandas предоставляет удобное использование регулярных выражений применительно к каждому столбцу. Это выглядит так:
train.passport_issuer_name = train.passport_issuer_name.str.replace(u'р-(а|й|о|н|е)*',u'район') train.passport_issuer_name = train.passport_issuer_name.str.replace(u' г( |\.|(ор(\.| )))', u' город ') train.passport_issuer_name = train.passport_issuer_name.str.replace(u' р(\.|есп )', u' республика ') train.passport_issuer_name = train.passport_issuer_name.str.replace(u' адм([а-я]*)(\.)?', u' административный ') train.passport_issuer_name = train.passport_issuer_name.str.replace(u' окр(\.| |уга( )?)', u' округ ') train.passport_issuer_name = train.passport_issuer_name.str.replace(u' ао ', u' административный округ ')
Теперь избавимся от всех лишних символов, кроме русских букв, дефисов и пробелов. Это связано с тем, что паспорт о одинаковым подразделением может выдаваться отделами с разными номерами, и это ухудшит дальнейшую кодировку:
train.passport_issuer_name = train.passport_issuer_name.str.replace(u' - ?', u'-') train.passport_issuer_name = train.passport_issuer_name.str.replace(u'[^а-я -]','') train.passport_issuer_name = train.passport_issuer_name.str.replace(u'- ',' ') train.passport_issuer_name = train.passport_issuer_name.str.replace(u' *',' ')
На следующем шаге, надо расшифровать аббревиатуры, типа УВД, УФНС, ЦАО, ВАО и т.д., т.к. этих их в принципе не много, но на качестве дальнейшего кодирования это скажется положительно. Например если у нас будет две записи «УВД» и «управление внутренних дел», то закодированы они будут по разному, т. к. для компьютера это разные значения.
Итак перейдем к расшифровке. И, для начала, заведем словарь сокращений, с помощью которого мы и сделаем расшифровку:
sokr = {u'нао': u'ненецкий автономный округ', u'хмао': u'ханты-мансийский автономный округ', u'чао': u'чукотский автономный округ', u'янао': u'ямало-ненецкий автономный округ', u'вао': u'восточный административный округ', u'цао': u'центральный административный округ', u'зао': u'западный административный округ', u'cао': u'северный административный округ', u'юао': u'южный административный округ', u'юзао': u'юго-западный округ', u'ювао': u'юго-восточный округ', u'свао': u'северо-восточный округ', u'сзао': u'северо-западный округ', u'оуфмс': u'отдел управление федеральной миграционной службы', u'офмс': u'отдел федеральной миграционной службы', u'уфмс': u'управление федеральной миграционной службы', u'увд': u'управление внутренних дел', u'ровд': u'районный отдел внутренних дел', u'говд': u'городской отдел внутренних дел', u'рувд': u'районное управление внутренних дел', u'овд': u'отдел внутренних дел', u'оувд': u'отдел управления внутренних дел', u'мро': u'межрайонный отдел', u'пс': u'паспортный стол', u'тп': u'территориальный пункт'}
Теперь, собственно произведем расшифровку абривеатур и отформатируем полученные записи:
for i in sokr.iterkeys(): train.passport_issuer_name = train.passport_issuer_name.str.replace(u'( %s )|(^%s)|(%s$)' % (i,i,i), u' %s ' % (sokr[i])) #удалим лишние пробелы в конце и начале строки train.passport_issuer_name = train.passport_issuer_name.str.lstrip() train.passport_issuer_name = train.passport_issuer_name.str.rstrip()
Предварительный этап обработки поля «кем выдан паспорт» на этом закончим. И перейдем к полю, в котором находится дата выдачи.
Как можно заметить данные в нем хранятся в виде: месяцMгод.
Соответственно можно просто убрать букву «M» и привести поле к числовому типу. Но если хорошо подумать, то это поле можно удалить, т.к. на один месяц в году может приходиться несколько подразделений выдававших паспорт, и соответственно это может испортить нашу модель. Исходя из этого удалим его из выборки:
train = train.drop(['passport_issue_month/year'], axis=1)
Теперь мы можем перейти к анализу данных.
Анализ данных
Итак, данные для построения модели у нас есть, но они находятся в текстовом виде. Для построения модели хорошо бы было их закодировать в числовом виде.
Авторы пакета scikit-learn заботливо о нас позаботились и добавили несколько способов для извлечения и кодирования текстовых данных. Из них мне больше всего нравятся два:
FeatureHasher преобразовывает строку в числовой массив заданной длинной с помощью хэш-функции (32-разрядная версия Murmurhash3)
CountVectorizer преобразовывает входной текст в матрцицу, значениями которой, являются количества вхождения данного ключа(слова) в текст. В отличие от FeatureHasher имеет больше настраиваемых параметров(например можно задать токенизатор), но работает медленнее.
Для более точного понимания работы CountVectorizer приведем простой пример. Допустим есть таблица с текстовыми значениями:
Значение |
---|
раз два три |
три четыре два два |
раз раз раз четыре |
Для начала CountVectorizer собирает уникальные ключи из всех записей, в нашем примере это будет:
[раз, два, три, четыре]
Длина списка из уникальных ключей и будет длиной нашего закодированного текста (в нашем случае это 4). А номера элементов будут соответствовать, количеству раз встречи данного ключа с данным номером в строке:
раз два три —> [1,1,1,0]
три четыре два два —> [0,2,1,1]
Соответственно после кодировки, применения данного метода мы получим:
Значение |
---|
1,1,1,0 |
0,2,1,1 |
3,0,0,1 |
HashingVectorizer является смесью двух выше описанных методов. В нем можно и регулировать размер закодированной строки (как в FeatureHasher) и настраивать токенизатор (как в CountVectorizer). К тому же его производительность ближе к FeatureHasher.
Итак, вернемся к анализу. Если мы посмотрим по внимательнее на наш набор данных то можно заметить, что есть похожие строки но записанные по разному например: "… республика карелия…" и "… по республике карелия…".
Соответственно, если мы попробуем применить один из методов кодирования сейчас мы получим очень похожие значения. Такие случаем можно минимизировать если все слова в записи мы приведем к нормальной форме.
Для этой задачи хорошо подходит pymorphy или nltk. Я буду использовать первый, т.к. он изначально создавался для работы с русским языком. Итак, функция которая будет отвечать за нормализацию и очиску строки выглядит так:
def f_tokenizer(s): morph = pymorphy2.MorphAnalyzer() if type(s) == unicode: t = s.split(' ') else: t = s f = [] for j in t: m = morph.parse(j.replace('.','')) if len(m) <> 0: wrd = m[0] if wrd.tag.POS not in ('NUMR','PREP','CONJ','PRCL','INTJ'): f.append(wrd.normal_form) return f
Функция делает следующее:
- Сначала она преобразовывает строку в список
- Затем для всех слов производит разбор
- Если слово является числительным, предикативном, предлогом, союзом, частицей или междометием не включаем его в конечный набор
- Если слово не попало в предыдущий список, берем его нормальную форму и добавляем в финальный набор
Теперь, когда есть функция для нормализации можно приступить к кодированию с помощью метода CountVectorizer. Он выбран потому, что ему можно передать нашу функцию, как токенизатор и он составит список ключей по значениям полученным в результате работы нашей функции:
coder = HashingVectorizer(tokenizer=f_tokenizer, n_features=256)
Как можно заметить при создании метода кроме токенизатора мы задаем еще один параметр n_features. Через данный параметр задается длина закодированной строки (в нашем случае строка кодируется при помощи 256 столбцов). Кроме того, у HashingVectorizer есть еще одно преимущество перед CountVectorizer, но сразу может выполнять нормализацию значений, что хорошо для таких алгоритмов, как SVM.
Теперь применим наш кодировщик к обучающему набору:
TrainNotDuble = train.drop_duplicates() trn = coder.fit_transform(TrainNotDuble.passport_issuer_name.tolist()).toarray()
Построение модели
Для начала нам надо задать значения для столбца, в котором будут содержаться метки классов:
target = TrainNotDuble.passport_div_code.values
Задача, которую мы решаем сегодня, принадлежит к классу задач классификации со множеством классов. Для решения данной задачи лучше всего подошел алгоритм RandomForest. Остальные алгоритмы показали очень плохие результаты (менее 50%) поэтому я решил не занимать место в статье. При желании любой интересующийся может проверить данные результаты.
Для оценки качества классификации будем использовать количество документов по которым принято правильное решение, т. е.
, где P — количество документов по которым классификатор принял правильное решение, а N – размер обучающей выборки.
В пакете scikit-learn для этого есть функция: accuracy_score
Перед началом построения собственно модели, давайте сократим размерность с помощью «метода главных компонент», т.к. 256 столбцов для обучения довольно много:
pca = PCA(n_components = 15) trn = pca.fit_transform(trn)
Модель будет выглядеть так:
model = RandomForestClassifier(n_estimators = 100, criterion='entropy') TRNtrain, TRNtest, TARtrain, TARtest = train_test_split(trn, target, test_size=0.4) model.fit(TRNtrain, TARtrain) print 'accuracy_score: ', accuracy_score(TARtest, model.predict(TRNtest))
accuracy_score: 0.6523456
Заключение
В качестве вывода нужно отметить, что полученная точность в 65% близка к угадыванию. Чтобы улучшить нужно при первичной обработке обработать грамматические ошибки и различного рода описки. Данное действие также скажется положительно и на словаре при кодировании поля, т. е. его размер уменьшиться и соответственно уменьшиться длина строки после ее кодировки.
Кроме того этап обучения тестовой выборки опущен специально, т. к. в нем нет ничего особенного, кроме его приведения к нужному виду (это можно легко сделать взяв за основу преобразования обучающей выборки)
В статье я попытался показать минимальный список этапов по обработке текстовой информации для подачи ее алгоритмам машинного обучения. Возможно делающим первые шаги в анализе данных данная информация будет полезной.
ссылка на оригинал статьи http://habrahabr.ru/post/205360/
Добавить комментарий