{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "

\n", " LDA Spike 1 - Cleaning\n", "

\n", "\n", "This notebook \"cleans\" the text files containing answers with the help of the Natural Language Processing Library [spaCy](https://spacy.io/). By default the text files are expected to be found in the folder `Corpus` and the cleaned files are written into the folder `Cleaned`. We want to keep only useful information in the files and remove any \"noise\". Our strategy is to do the following:\n", "\n", " * Replace all words by their lemmata ('sang', 'singe', 'singt' --> 'singen').\n", " * Keep the capitalization for nouns and proper nouns but otherwise change to lower case.\n", " * Keep only verbs, nouns, proper nouns and adjectives.\n", "\n", "Even before this more sophisticated processing, we manually cut of greeting phrases at the beginning and the end of the answer, as they do not contribute to the topic.\n", "\n", "The randomly picked example below will (probably) demonstrate the impact of these transformations. Nevertheless, there is still much room for improvement. You may try other NLP libraries as well or on the contrary skip this step altogether.\n", "\n", "

\n", "\n", "__This notebooks writes to and reads from your file system.__ Per default all used directory are within `~/TextData/Abgeordnetenwatch`, where `~` stands for whatever your operating system considers your home directory. To change this configuration either change the default values in the second next cell or edit [LDA Spike - Configuration.ipynb](./LDA%20Spike%20-%20Configuration.ipynb) and run it before you run this notebook.\n", "\n", "

\n", "\n", "This notebooks operates on text files. In our case we retrieved these texts from www.abgeordnetenwatch.de guided by data that was made available under the [Open Database License (ODbL) v1.0](https://opendatacommons.org/licenses/odbl/1.0/) at that site.\n", "\n", "

" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import time\n", "import random as rnd\n", "\n", "from pathlib import Path\n", "\n", "import spacy" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "%store -r own_configuration_was_read\n", "if not('own_configuration_was_read' in globals()): raise Exception(\n", " '\\nReminder: You might want to run your configuration notebook before you run this notebook.' + \n", " '\\nIf you want to manage your configuration from each notebook, just remove this check.')\n", "\n", "%store -r project_name\n", "if not('project_name' in globals()): project_name = 'AbgeordnetenWatch'\n", "\n", "%store -r text_data_dir\n", "if not('text_data_dir' in globals()): text_data_dir = Path.home() / 'TextData'" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "corpus_dir = text_data_dir / project_name / 'Corpus'\n", "cleaned_dir = text_data_dir / project_name / 'Cleaned'\n", "\n", "assert corpus_dir.exists(), 'Directory should exist.'\n", "assert corpus_dir.is_dir(), 'Directory should be a directory.'\n", "assert next(corpus_dir.iterdir(), None) != None, 'Directory should not be empty.'\n", "\n", "cleaned_dir.mkdir(parents=True, exist_ok=True) # Creates a local directory!" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "update_only_missing_texts = True" ] }, { "cell_type": "markdown", "metadata": { "scrolled": false }, "source": [ "## Manual removal of greeting phrases" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "opening_greeting = ['sehr geehrter ', 'sehr geehrte ', 'liebe ', 'lieber ', 'hallo ']\n", "\n", "closing_greeting = ['mit freundlichen grüßen', 'mit freundlichem gruß', 'mfg', 'freundliche grüße'\n", " 'viele grüße', 'beste grüße', 'mit besten grüßen', \n", " 'liebe grüße', 'herzliche grüße', 'vielen dank und', 'vg,', 'vg ']\n", "\n", "max_closing_lines = 4\n", "\n", "\n", "def without_opening_greeting(lines):\n", " for l, line in enumerate(lines):\n", " lower_line = line.strip().lower()\n", " for greeting in opening_greeting:\n", " if lower_line.startswith(greeting):\n", " line = ','.join(line.split(',')[1:])\n", " lower_line = line.strip().lower()\n", " lines[l] = line\n", " return lines\n", "\n", "\n", "def post_scriptum(lines):\n", " for l, line in enumerate(lines):\n", " if line.startswith('P.S.') or line.startswith('PS'):\n", " return lines[l:]\n", " return []\n", "\n", "\n", "def without_closing_greeting(lines):\n", " for l, line in enumerate(lines):\n", " lower_line = line.strip().lower()\n", " if any(lower_line.startswith(greeting) for greeting in closing_greeting):\n", " lines = lines[:l] + post_scriptum(lines[l:])\n", " break\n", " return lines\n", "\n", "\n", "def without_greetings(text):\n", " \n", " lines = text.strip().splitlines()\n", " \n", " if len(lines) < 1: return ''\n", " lines = without_opening_greeting(lines[:1]) + lines[1:]\n", " \n", " closing_start = min(len(lines), max_closing_lines)\n", " lines = lines[:-closing_start] + without_closing_greeting(lines[-closing_start:])\n", "\n", " return '\\n'.join(lines).strip()" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "scrolled": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "wir freuen uns\n", "über Ihre Nachricht, die wir gerne demnächst beantworten.\n", "P.S.: Unsere Partei schätzt den Bürgerdialog\n" ] } ], "source": [ "text = '''\n", "Sehr geehrter Herr N.N., liebe Frau Sonnenschein, wir freuen uns\n", "über Ihre Nachricht, die wir gerne demnächst beantworten.\n", "Vielen Dank und herzliche Grüße\n", "von Ihrem Abgeordneten\n", "P.S.: Unsere Partei schätzt den Bürgerdialog\n", "'''\n", "print(without_greetings(text))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## NLP-based Cleaning" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "notaword_pos = ['SPACE', 'PUNCT']\n", "keepcase_pos = ['NOUN', 'PROPN']\n", "keepword_pos = ['ADJ', 'NOUN', 'PROPN', 'VERB']" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "german = spacy.load('de')" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "def cleaned_text(text):\n", " text_model = german(text)\n", " lemmata = [token.lemma_ if token.pos_ in keepcase_pos else token.lemma_.lower() \n", " for token in text_model if token.pos_ in keepword_pos]\n", " return ' '.join(lemmata)" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Die Kuh rannte bis sie fiel, in die Vertiefung. --> Kuh rennen fallen Vertiefung\n" ] } ], "source": [ "text = 'Die Kuh rannte bis sie fiel, in die Vertiefung.'\n", "print(text, '-->', cleaned_text(text))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Load all files and remove the greetings" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "scrolled": false }, "outputs": [], "source": [ "answer_filenames = []\n", "answer_texts = []\n", "min_text_len = 50\n", "\n", "files = list(corpus_dir.glob('*A*.txt'))\n", "list.sort(files)\n", "\n", "for file in files:\n", " text = without_greetings(file.read_text())\n", " if len(text) >= min_text_len:\n", " answer_filenames.append(file.name)\n", " answer_texts.append(text)\n", "\n", "files = None" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Random Example Text" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "scrolled": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "haben Sie vielen Dank für Ihre Nachricht. Die Kosten für das eigene Wohnen werden in Deutschland steuerlich anders behandelt als andere Bereiche. Zum Beispiel sind auf Mietzahlungen keine Mehrwertsteuer fällig. Ebenso wenig kann man die Miete, die man zahlt, steuerlich absetzen, weil sie zum privaten Lebensumfeld gehört.\n", "Dasselbe gibt es im Bereich der Ernährung: Ebenso wenig wie Sie die Mieteinnahmen und die Mietausgaben verrechnen können, kann eine Gastwirtin die Einnahmen durch Verkauf von Essen verrechnen mit den Kosten, die sie hat, wenn sie selbst einmal essen geht. Man könnte die Grenze zwischen dem privaten Lebensbereich, der steuerlich nicht erfasst wird, und dem Bereich der Einkommenserzielung auch anders ziehen, aber so wurde sie in Deutschland festgelegt.\n" ] } ], "source": [ "min_len = 400\n", "max_len = 800\n", "example_text = ''\n", "\n", "while (len(example_text) < min_len or len(example_text) > max_len):\n", " example = rnd.randint(0, len(answer_filenames))\n", " example_text = answer_texts[example]\n", "\n", "print(example_text)" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "# Create a model of the text. We use POS-Tagging to filter the words:\n", "# https://spacy.io/api/annotation#pos-tagging\n", "\n", "text_model = german(example_text)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Lemmatized words with part of speech tags" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "haben AUX ich PRON viel DET Dank NOUN für ADP mein DET Nachricht NOUN .der DET Kosten NOUN für ADP der DET eigene ADJ Wohnen NOUN werden AUX in ADP Deutschland PROPN steuerlich ADJ anders ADV behandeln VERB als CONJ ander ADJ Bereich NOUN .Zum ADP Beispiel NOUN sein AUX auf ADP Mietzahlung NOUN kein DET Mehrwertsteuer NOUN fällig ADJ .Ebenso ADV wenig PRON können VERB man PRON der DET mieten NOUN ,der PRON man PRON zahlen VERB ,steuerlich ADJ absetzen VERB ,weil SCONJ ich PRON zum ADP privat ADJ Lebensumfeld NOUN hören VERB .\n", "derselbe PRON geben VERB ich PRON im ADP Bereich NOUN der DET Ernährung NOUN :Ebenso ADV wenig PRON wie CONJ ich PRON der DET Mieteinnahmen NOUN und CONJ der DET Mietausgaben NOUN verrechnen VERB können VERB ,können VERB einen DET Gastwirtin NOUN der DET einnehmen NOUN durch ADP verkaufen NOUN von ADP Essen NOUN verrechnen VERB mit ADP der DET Kosten NOUN ,der PRON ich PRON haben AUX ,wenn SCONJ ich PRON selbst ADV einmal ADV essen VERB gehen VERB .Man PRON können VERB der DET grenzen NOUN zwischen ADP der DET privat ADJ Lebensbereich NOUN ,der PRON steuerlich ADJ nicht PART erfasst VERB werden AUX ,und CONJ der DET Bereich NOUN der DET Einkommenserzielung NOUN auch ADV anders ADV ziehen VERB ,aber CONJ so ADV werden AUX ich PRON in ADP Deutschland PROPN festlegen VERB ." ] } ], "source": [ "for token in text_model:\n", " if token.pos_ in notaword_pos: \n", " print(token, end='') \n", " else: \n", " print(token.lemma_, token.pos_, end=' ')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Words by part of speech" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ADJ : andere, eigene, fällig, privaten, steuerlich\n", "ADP : auf, durch, für, im, in, mit, von, zum, zwischen\n", "ADV : anders, auch, ebenso, einmal, selbst, so\n", "AUX : haben, hat, sind, werden, wird, wurde\n", "CONJ : aber, als, und, wie\n", "DET : das, dem, den, der, die, eine, ihre, keine, vielen\n", "NOUN : Beispiel, Bereich, Bereiche, Dank, Einkommenserzielung, Einnahmen, Ernährung, Essen, Gastwirtin, Grenze, Kosten, Lebensbereich, Lebensumfeld, Mehrwertsteuer, Mietausgaben, Miete, Mieteinnahmen, Mietzahlungen, Nachricht, Verkauf, Wohnen\n", "PART : nicht\n", "PRON : dasselbe, der, die, es, man, sie, wenig\n", "PROPN: Deutschland\n", "SCONJ: weil, wenn\n", "VERB : absetzen, behandelt, erfasst, essen, festgelegt, geht, gehört, gibt, kann, können, könnte, verrechnen, zahlt, ziehen\n" ] } ], "source": [ "parts_of_speech = {}\n", "\n", "for token in text_model:\n", " pos = token.pos_\n", " if pos in ['SPACE', 'PUNCT']: continue\n", " words = parts_of_speech.setdefault(pos, set())\n", " if pos in keepcase_pos: words.add(token.text)\n", " else: words.add(token.text.lower())\n", "\n", "for key in sorted(parts_of_speech.keys()):\n", " words = list(parts_of_speech[key])\n", " list.sort(words)\n", " print('{:5}: {}'.format(key, ', '.join(words)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Lemmatizations" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Bereiche -> Bereich, Dasselbe -> derselbe, Die -> der, Einnahmen -> einnehmen, Grenze -> grenzen, Ihre -> mein, Miete -> mieten, Mietzahlungen -> Mietzahlung, Sie -> ich, Verkauf -> verkaufen, andere -> ander, behandelt -> behandeln, das -> der, dem -> der, den -> der, die -> der, eine -> einen, es -> ich, festgelegt -> festlegen, geht -> gehen, gehört -> hören, gibt -> geben, hat -> haben, kann -> können, keine -> kein, könnte -> können, privaten -> privat, sie -> ich, sind -> sein, vielen -> viel, wird -> werden, wurde -> werden, zahlt -> zahlen\n" ] } ], "source": [ "lemmatizations = list(set(\n", " token.text + ' -> ' + token.lemma_ \n", " for token in text_model if token.text != token.lemma_\n", "))\n", "list.sort(lemmatizations)\n", "print(', '.join(lemmatizations))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Filtered by part of speech" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Dank Nachricht Kosten eigene Wohnen Deutschland steuerlich behandeln ander Bereich Beispiel Mietzahlung Mehrwertsteuer fällig können mieten zahlen steuerlich absetzen privat Lebensumfeld hören geben Bereich Ernährung Mieteinnahmen Mietausgaben verrechnen können können Gastwirtin einnehmen verkaufen Essen verrechnen Kosten essen gehen können grenzen privat Lebensbereich steuerlich erfasst Bereich Einkommenserzielung ziehen Deutschland festlegen " ] } ], "source": [ "for token in text_model:\n", " if token.pos_ in keepword_pos: \n", " print(token.lemma_, end=' ')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Cleaned Example Text" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "------------------------------ Original text: ------------------------------\n", "haben Sie vielen Dank für Ihre Nachricht. Die Kosten für das eigene Wohnen werden in Deutschland steuerlich anders behandelt als andere Bereiche. Zum Beispiel sind auf Mietzahlungen keine Mehrwertsteuer fällig. Ebenso wenig kann man die Miete, die man zahlt, steuerlich absetzen, weil sie zum privaten Lebensumfeld gehört.\n", "Dasselbe gibt es im Bereich der Ernährung: Ebenso wenig wie Sie die Mieteinnahmen und die Mietausgaben verrechnen können, kann eine Gastwirtin die Einnahmen durch Verkauf von Essen verrechnen mit den Kosten, die sie hat, wenn sie selbst einmal essen geht. Man könnte die Grenze zwischen dem privaten Lebensbereich, der steuerlich nicht erfasst wird, und dem Bereich der Einkommenserzielung auch anders ziehen, aber so wurde sie in Deutschland festgelegt.\n", "------------------------------ Cleaned text: ------------------------------\n", "Dank Nachricht Kosten eigene Wohnen Deutschland steuerlich behandeln ander Bereich Beispiel Mietzahlung Mehrwertsteuer fällig können mieten zahlen steuerlich absetzen privat Lebensumfeld hören geben Bereich Ernährung Mieteinnahmen Mietausgaben verrechnen können können Gastwirtin einnehmen verkaufen Essen verrechnen Kosten essen gehen können grenzen privat Lebensbereich steuerlich erfasst Bereich Einkommenserzielung ziehen Deutschland festlegen\n" ] } ], "source": [ "print(30 * '-' + ' Original text: ' + 30 * '-')\n", "print(example_text)\n", "print(30 * '-' + ' Cleaned text: ' + 30 * '-')\n", "print(cleaned_text(example_text))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Write all cleaned files" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "48/7696 files succesfully processed. 0 files failed.\n", "Parsing the text as natural language and cleaning took 4.64s\n" ] } ], "source": [ "nlp_start_time = time.perf_counter()\n", "\n", "num_files = len(answer_texts)\n", "success = []\n", "failure = []\n", " \n", "for filename, answer_text in zip(answer_filenames, answer_texts):\n", "\n", " target_file = cleaned_dir / filename\n", " if update_only_missing_texts and target_file.exists(): continue\n", " \n", " try:\n", " target_file.write_text(cleaned_text(answer_text))\n", " success.append(filename)\n", "\n", " except Exception as exception:\n", " failure.append((filename, exception))\n", "\n", " finally:\n", " print('\\r{}/{} files succesfully processed. {} files failed.'.format(len(success), num_files, len(failure)), end='')\n", "\n", "nlp_end_time = time.perf_counter()\n", "print('\\nParsing the text as natural language and cleaning took {:.2f}s'.format(nlp_end_time - nlp_start_time)) " ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "No exception during preprocessing :-)\n" ] } ], "source": [ "for filename, exception in failure:\n", " print('Exception while processing \"{}\" was:'.format(filename))\n", " print(exception)\n", "else:\n", " print('No exception during preprocessing :-)')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", " \n", " \n", " \n", " \n", "
\n", " \n", " \"Creative\n", " \n", " © T. Dong, D. Speicher
\n", " Licensed under a \n", " \n", " CC BY-NC 4.0\n", " .\n", "
\n", " Acknowledgments:\n", " This material was prepared within the project\n", " \n", " P3ML\n", " \n", " which is funded by the Ministry of Education and Research of Germany (BMBF)\n", " under grant number 01/S17064. The authors gratefully acknowledge this support.\n", "
" ] } ], "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.8" } }, "nbformat": 4, "nbformat_minor": 2 }