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