{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Python для анализа данных\n", "\n", "*Алла Тамбовцева, НИУ ВШЭ*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Работа с txt-файлами. Предобработка текста.\n", "\n", "Сначала мы посмотрим, как считывать текст из txt-файлов. Это необходимое умение, поскольку не всегда набор текстов сохранен в виде готовой таблицы и выгружен в csv-файл. Часто приходится иметь дело с множеством txt-файлов, которые просто лежат в одной папке. \n", "\n", "Откроем txt-файл, в котором сохранено описание фильма «Господин оформитель» из Википедии." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "f = open('mytext.txt', 'r', encoding = 'UTF-8')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Для открытия файла используется функция `open()`. Так как мы открываем файл только для чтения, мы выставляем флаг (аргумент) `r` (от *read*). Если нужно открыть файл сразу для всего (чтение, изменение, сохранение), то можно выставить флаг `a` (от *all*). При открытии файла лучше сразу указывать его кодировку, особенно если файл не на латинице, здесь это `UTF-8`.\n", "\n", "Чтобы считать строки в файле, понадобится метод `.readlines()`:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "f.readlines()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "У этого метода есть одна особенность, не очень приятная, если к ней не привыкнуть ‒ он выводит строки только для чтения. Чтобы понять, что это значит, давайте попробуем сохранить результат в список `lines` с помощью обычного присваивания:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "lines = f.readlines()" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "# ха-ха\n", "lines" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Список оказался пустым! Более того, если мы снова попробуем вызвать `.readlines()`, мы ничего хорошего не получим:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "f.readlines() # все сломалось" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Чтобы избежать таких неприятных сюрпризов, лучше воспользоваться циклом, и в цикле заполнить новый список строк." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "# дубль два\n", "f = open('mytext.txt', 'r', encoding = 'UTF-8')\n", "\n", "lines = []\n", "for l in f.readlines():\n", " lines.append(l)\n", " \n", "lines" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь все в порядке. Приведем в порядок наши строки. Видно, что в списке строк встречаются «пустые» строки, состоящие из одного символа для перехода на новую строку (`\\n`). Кроме того, этот символ встречается на конце строк. Исправим это, используя списковые включения! " ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "# убираем пустые строки\n", "clean = [l for l in lines if l != '\\n']\n", "clean" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "# убираем \\n на конце строк (и лишние пробелы по краям вообще)\n", "clean = [s.strip() for s in clean]\n", "clean" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Так как при анализе текстов часто используется модель \"мешка слов\" (*bag of words*), грамматическая структура предложений, порядок слов и знаки препинания не играют никакой роли. Давайте для начала избавимся от знаков пунктуации. Импортируем модуль `string`, который позволит получить готовую строку со знаками препинания:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "import string\n", "string.punctuation" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Получили строку, в которой учтены почти все знаки препинания. Почему почти? Так как многие модули ориентированы на англоязычный текст (и вообще текст на латинице), русская пунктуация в рассмотрение не входит. Так, здесь не хватает кавычек-ёлочек, принятых в русскоязычных текстах. Кроме того, здесь не хватает тире. Добавим их. Так как результат `string.punctuation` – это обычная строка, к ней можно добавить свои символы с помощью конкатенации:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "to_remove = string.punctuation + '«»—'\n", "to_remove" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Убирать из текстов символы, которые есть в строке `to_remove`, можно по-разному. Мы воспользуемся такой хитростью: создадим `translator`, который будет заменять знаки препинания из `to_remove` на пустые строки `''`, а затем будем использовать его в качестве функции, которая будет применяться в методе `translate` для строк." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "# создаем translator\n", "translator = str.maketrans('', '', to_remove)" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "# применяем (на примере одной строки)\n", "s = 'После всех попыток добиться истины, оформитель слышит от неё только: «Забудьте об Анне. Её больше нет».'\n", "s.translate(translator)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Задание.** Написать функцию `normalize(x)`, которая удаляет в строке `x` все знаки препинания, приводит весь текст к нижнему регистру и возвращает новую строку. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*Решение:*" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "def normalize(x):\n", " to_remove = string.punctuation + '«»—'\n", " translator = str.maketrans('', '', to_remove)\n", " res = x.translate(translator)\n", " res = res.lower()\n", " return res" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Применим функцию к элементам списка `сlean` и назовем новый список `normalized`." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "normalized = [normalize(c) for c in clean]\n", "normalized" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'действие происходит в петербурге в 19081914 годах знаменитый художникоформитель платон андреевич хочет продлить жизнь человека в скульптуре и на рисунках пытаясь побороть смерть и усовершенствовать окружающий мир с помощью своего таланта многие годы ему не давала покоя мысль о состязании с всевышним ему автору великолепных восковых манекенов хотелось создать нечто совершенное и вечное не поддающееся течению времени в 1908 году художник получает заказ от ювелира на оформление витрины магазина в поисках натурщицы для изготовления манекена для витрины художник находит анну молодую девушку смертельно больную чахоткой и ваяет с неё свой лучший манекен вкладывая в работу всю душу проходит время на дворе 1914 год известный ранее художник впал в забвение дела идут совсем не так хорошо как в прежние времена под влиянием творческого кризиса художник начинает злоупотреблять морфием ему грозит полное разорение однажды находясь в крайней нужде платон андреевич принял предложение некоего богатого дельца грильо оформить интерьер его дома знакомство с женой хозяина марией привело художника в замешательство он был убежден что несколько лет назад с неё носящей тогда имя анны белецкой он вылепил свой лучший восковой манекен но мария говорит ему что никогда раньше не видела художника и ничего не знает ни о какой анне после всех попыток добиться истины оформитель слышит от неё только забудьте об анне её больше нет платон андреевич делает предложение девушке но получает отказ мария говорит ему что он слишком беден благодаря невероятному случаю оформитель выигрывает у мужа марии огромное состояние и делает предложение снова и снова получает отказ тут понятное течение событий меняет свой ход и наступает страшная развязка девушка оказывается ожившей скульптурой его работы волей злого рока сбылась мечта художника его творения обрели жизнь но воплощённая в облике человека вещь созданная в стремлении превзойти творения всевышнего переняла самые низкие и разрушительные свойства мария убивает грильо и завладевает его домом её следующая жертва художник художник гибнет под колесами серого автомобиля на котором едет мария и безымянные представители потусторонних тёмных сил'" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "text = \" \".join(normalized)\n", "text" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Продолжим выполнять предварительную обработку текста. Прежде чем анализировать текст, смотреть какие слова в нем встречаются чаще и прочее, необходимо максимально унифицировать текст. Нужно сделать так, чтобы все грамматические формы слова считались как одно и то же слово. Например, чтобы слова *страны* и *страной* воспринимались как разные формы одного и того же слова *страна*. Для этого есть два пути: стемминг или лемматизация. \n", "\n", "Стемминг – это процесс обработки, в результате которого от слов остаются только их основы. Так, от слова *страны* останется *стран*, от слова *делает* – *дела*. Этот способ достаточно удобен, в библиотеке `nltk` встроены готовые стеммеры для разных языков, но есть две проблемы. Во-первых, стемминг хуже работает в случае морфологически богатых языков, языков, где возможны совершенно разнообразные формы слова (к таким относится русский язык). Во-вторых, если по итогам анализа текста мы захотим построить облака слов, нам придется писать функцию, которая возвращает предобработанным словам их первоначальный вид, так как \"обрезаные\" слова в облаке будут смотреться неэстетично и неинформативно.\n", "\n", "Лемматизация – это приведение слова к его словарной (начальной) форме. В случае существительных это будет форма единственного числа и именительного падежа, в случае прилагательных – форма единственного числа, мужского рода и именительного падежа, и так далее. Например, слово *белую* в результате лемматизации превратится в *белый*, *стоял* – в *стоять*. Как раз лемматизация отлично подойдет для русского языка. \n", "\n", "Для стемминга и лемматизации русскоязычных слов подойдет [библиотека](https://github.com/nlpub/pymystem3) `pymystem3`, которая является питоновской оболочкой для морфологического анализатора Яндекса. \n", "\n", "Импортируем из этой библиотеки класс `Mystem` (пока про классы можно думать как про наборы методов, которые можно написать самостоятельно и применять к объектам определенного типа)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Чтобы установить библиотеку `pymystem3`, нужно:\n", "* на Windows: в меню Пуск выбрать папку Anaconda3 и Anaconda Command Prompt\n", "* на Mac OS, Linux: открыть новое окно терминала (командной строки)\n", "\n", "В Anaconda Command Prompt/терминале ввести\n", "\n", " pip install pymystem3\n", " \n", "и нажать *Enter*. При подключении к Интернету будут скачаны установочные файлы, а затем компоненты библиотеки будут установлены. В процессе установки могут всплывать предупреждения, но на них можно не обращать внимание, если в итоге есть строка с `successfully installed`." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "from pymystem3 import Mystem" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "m = Mystem()\n", "m" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Получим список лемм (слов, приведенных к начальной форме) на основе нашего текста:" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['действие',\n", " ' ',\n", " 'происходить',\n", " ' ',\n", " 'в',\n", " ' ',\n", " 'петербург',\n", " ' ',\n", " 'в',\n", " ' ',\n", " '19081914',\n", " ' ',\n", " 'год',\n", " ' ',\n", " 'знаменитый',\n", " ' ',\n", " 'художникоформитель',\n", " ' ',\n", " 'платон',\n", " ' ',\n", " 'андреевич']" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "lemmas = m.lemmatize(text)\n", "lemmas[0:21] # первые несколько элементов" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь можем склеить из этого списка слов новый текст и сохранить его в файл на всякий случай." ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'действие происходить в петербург в 19081914 год знаменитый художникоформитель платон андреевич хотеть продлять жизнь человек в скульптура и на рисунок пытаться побороть смерть и усовершенствовать окружающий мир с помощь свой талант многий год он не давать покой мысль о состязание с всевышний он автор великолепный восковой манекен хотеться создавать нечто совершать и вечный не поддаваться течение время в 1908 год художник получать заказ от ювелир на оформление витрина магазин в поиск натурщица для изготовление манекен для витрина художник находить анна молодой девушка смертельно больной чахотка и ваять с она свой хороший манекен вкладывать в работа весь душа проходить время на двор 1914 год известный ранее художник впадать в забвение дело идти совсем не так хорошо как в прежний время под влияние творческий кризис художник начинать злоупотреблять морфий он грозить полный разорение однажды находиться в крайний нужда платон андреевич принимать предложение некий богатый делец грильо оформлять интерьер его дом знакомство с жена хозяин мария приводить художник в замешательство он быть убежденный что несколько год назад с она носить тогда имя анна белецкая он вылепливать свой хороший восковой манекен но мария говорить он что никогда рано не видеть художник и ничто не знать ни о какой анна после весь попытка добиваться истина оформитель слышать от она только забывать об анна она больше нет платон андреевич делать предложение девушка но получать отказ мария говорить он что он слишком бедный благодаря невероятный случай оформитель выигрывать у муж мария огромный состояние и делать предложение снова и снова получать отказ тут понятный течение событие менять свой ход и наступать страшный развязка девушка оказываться оживать скульптура его работа воля злой рок сбываться мечта художник его творение обретать жизнь но воплощать в облик человек вещь создавать в стремление превосходить творение всевышний перенимать самый низкий и разрушительный свойство мария убивать грильо и завладевать его дом ее следующий жертва художник художник гибнуть под колесо серый автомобиль на который ехать мария и безымянный представитель потусторонний темный сила\\n'" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "lemmatized = \"\".join(m.lemmatize(text))\n", "lemmatized" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [], "source": [ "lemmatized = lemmatized.rstrip() # уберем \\n в конце" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Запишем новый файл. Воспользуемся той же функцией `open()`, только выставим флаг `w` (от *write*):" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [], "source": [ "new_f = open('my_text_lemmas.txt', 'w')" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [], "source": [ "print(lemmatized, file = new_f) # впечатаем в файл текст\n", "new_f.close() # и закроем файл" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Строка с `.close()` небоходима, поскольку она закрывает файл, сохраняя его таким образом от последующих случайных изменений. Если забыть закрыть файл, то его содержимое может стереться при следующем обращении." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "С помощью класса `Mystem` можно делать и более интересные вещи. Например, узнавать части речи слов и их грамматические признаки:" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[{'analysis': [{'lex': 'действие',\n", " 'wt': 1,\n", " 'gr': 'S,сред,неод=(вин,ед|им,ед)'}],\n", " 'text': 'действие'},\n", " {'text': ' '},\n", " {'analysis': [{'lex': 'происходить',\n", " 'wt': 1,\n", " 'gr': 'V,нп=непрош,ед,изъяв,3-л,несов'}],\n", " 'text': 'происходит'},\n", " {'text': ' '},\n", " {'analysis': [{'lex': 'в', 'wt': 0.9999917878, 'gr': 'PR='}], 'text': 'в'},\n", " {'text': ' '},\n", " {'analysis': [{'lex': 'петербург', 'wt': 1, 'gr': 'S,гео,муж,неод=пр,ед'}],\n", " 'text': 'петербурге'},\n", " {'text': ' '},\n", " {'analysis': [{'lex': 'в', 'wt': 0.9999917878, 'gr': 'PR='}], 'text': 'в'},\n", " {'text': ' '},\n", " {'text': '19081914'}]" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# m.analyze(text)\n", "m.analyze(text)[0:11]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Здесь `S` – это существительное, `V` – глагол, `PR` – предлог. Сокращением `lex` обозначена лексема – словарная форма слова, а `text` – это слово в том виде, в котором оно встречается в тексте. Через `|` обозначены спорные формы. Например, по одному слову непонятно, именительный это падеж, или винительный." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Бонус**\n", "\n", "На самом деле, остался еще один этап предобработки текста – удаление стоп-слов. Стоп-слова – это часто встречающиеся слова, которые не несут важной смысловой информации. К таким словам обычно относят предлоги, частицы, союзы, некоторые наречия, междометия. Ведь, если частотный анализ текстов покажет нам, например, что для одной группы документов характерны слова «для» и «более», а для другой ‒ «не» и «что», это вряд ли позволит нам сделать осмысленные содержательные выводы. Базовые списки стоп-слов, встроенные в различные библиотеки, обычно расширяют, добавляя новые слова, исходя из контекста. Так, если мы работаем с постами пользователей, и в каждом посте обнаруживаем сочетание вида «имя пользователя», слова *имя* и *пользователь* имеет смысл отнести к стоп-словам, потому что они в любом случае будут среди самых распространенных, вне зависимости от тематики поста. \n", "\n", "Установим библиотеку `nltk` (*Natural Language Toolkit*). Установить библиотеку можно точно так же, как и `mystem3` через:\n", "\n", " pip install nltk" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Но для работы с отдельными модулями библиотеки нужно их доставить прямо через Jupyter. Нам понадобятся модули для токенизации (разбиение текста на слова) и для выбора стоп-слов. Если прогнать следующий код, появится отдельное окно (вне Jupyter) и в нем можно будет выбрать все модули (*all*, кликнуть внизу *Download* и подождать):" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "showing info https://raw.githubusercontent.com/nltk/nltk_data/gh-pages/index.xml\n" ] }, { "data": { "text/plain": [ "True" ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import nltk\n", "nltk.download()" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [], "source": [ "from nltk.tokenize import sent_tokenize, word_tokenize\n", "from nltk.corpus import stopwords\n", "\n", "stopWords = set(stopwords.words('russian')) # для русского языка" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [], "source": [ "# stopWords # можно раскомментировать и посмотреть на множество слов (оно довольно объемное)" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['действие', 'происходить', 'петербург', '19081914', 'год', 'знаменитый', 'художникоформитель', 'платон', 'андреевич', 'хотеть', 'продлять', 'жизнь', 'человек', 'скульптура', 'рисунок', 'пытаться', 'побороть', 'смерть', 'усовершенствовать', 'окружающий', 'мир', 'помощь', 'свой', 'талант', 'многий', 'год', 'давать', 'покой', 'мысль', 'состязание', 'всевышний', 'автор', 'великолепный', 'восковой', 'манекен', 'хотеться', 'создавать', 'нечто', 'совершать', 'вечный', 'поддаваться', 'течение', 'время', '1908', 'год', 'художник', 'получать', 'заказ', 'ювелир', 'оформление', 'витрина', 'магазин', 'поиск', 'натурщица', 'изготовление', 'манекен', 'витрина', 'художник', 'находить', 'анна', 'молодой', 'девушка', 'смертельно', 'больной', 'чахотка', 'ваять', 'свой', 'хороший', 'манекен', 'вкладывать', 'работа', 'весь', 'душа', 'проходить', 'время', 'двор', '1914', 'год', 'известный', 'ранее', 'художник', 'впадать', 'забвение', 'дело', 'идти', 'прежний', 'время', 'влияние', 'творческий', 'кризис', 'художник', 'начинать', 'злоупотреблять', 'морфий', 'грозить', 'полный', 'разорение', 'однажды', 'находиться', 'крайний', 'нужда', 'платон', 'андреевич', 'принимать', 'предложение', 'некий', 'богатый', 'делец', 'грильо', 'оформлять', 'интерьер', 'дом', 'знакомство', 'жена', 'хозяин', 'мария', 'приводить', 'художник', 'замешательство', 'убежденный', 'несколько', 'год', 'назад', 'носить', 'имя', 'анна', 'белецкая', 'вылепливать', 'свой', 'хороший', 'восковой', 'манекен', 'мария', 'говорить', 'рано', 'видеть', 'художник', 'ничто', 'знать', 'анна', 'весь', 'попытка', 'добиваться', 'истина', 'оформитель', 'слышать', 'забывать', 'анна', 'платон', 'андреевич', 'делать', 'предложение', 'девушка', 'получать', 'отказ', 'мария', 'говорить', 'слишком', 'бедный', 'благодаря', 'невероятный', 'случай', 'оформитель', 'выигрывать', 'муж', 'мария', 'огромный', 'состояние', 'делать', 'предложение', 'снова', 'снова', 'получать', 'отказ', 'понятный', 'течение', 'событие', 'менять', 'свой', 'ход', 'наступать', 'страшный', 'развязка', 'девушка', 'оказываться', 'оживать', 'скульптура', 'работа', 'воля', 'злой', 'рок', 'сбываться', 'мечта', 'художник', 'творение', 'обретать', 'жизнь', 'воплощать', 'облик', 'человек', 'вещь', 'создавать', 'стремление', 'превосходить', 'творение', 'всевышний', 'перенимать', 'самый', 'низкий', 'разрушительный', 'свойство', 'мария', 'убивать', 'грильо', 'завладевать', 'дом', 'следующий', 'жертва', 'художник', 'художник', 'гибнуть', 'колесо', 'серый', 'автомобиль', 'который', 'ехать', 'мария', 'безымянный', 'представитель', 'потусторонний', 'темный', 'сила']\n" ] } ], "source": [ "words = word_tokenize(lemmatized) # разбиваем текст на слова\n", "wordsFiltered = []\n", "\n", "for w in words:\n", " if w not in stopWords: # фильтруем слова - все, что вне множества StopWords\n", " wordsFiltered.append(w)\n", "\n", "print(wordsFiltered)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "На этом с предварительной обработкой текста можно закончить. Мы привели все к нижнему регистру, удалили пунктуацию, провели лемматизацию, превратили текст в список слов, убрав стоп-слова. По-хорошему, нужно было ещё убрать числа, но мы их пока оставили." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.5" } }, "nbformat": 4, "nbformat_minor": 2 }