Python-Anfänger braucht Hilfe bei multiplen Datenstrukturen

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Dingels
User
Beiträge: 61
Registriert: Dienstag 23. Dezember 2008, 19:50

Hallo und guten Abend,

ich komme eigentlich aus der Perl-Gemeinde, möchte mir aber Python beibringen. Daher schreibe ich momentan einige meiner Programme in Python neu.

Ich schildere euch mal mein Problem:
Ich habe ein Textkorpus, in dem in jeder Zeile ein Wort steht und dahinter ein Wortarten-Tag (also ein Kürzel, das die Wortart angibt).
Ein Beispiel:
The AT
Fulton NP
County NN
Grand JJ
Jury NN
said VBD
Friday NR
an AT
investigation NN
of IN
Atlanta's NP$
recent JJ
primary NN
election NN
produced VBD
Nun möchte ich in einem Dictionary zählen, wie oft welches Wort mit welchem Wortarten-Tag im Korpus auftaucht. Denn manche Wörter gehören nicht eindeutig immer nur einer Wortart an, sondern können auch zu zwei Wortarten gerechnet werden.

Hierzu benutze ich in Perl einen "Hash of Hashes". Der Code dazu sieht folgendermaßen aus:

Code: Alles auswählen

use strict;

# Dieses Array besteht aus den einzelnen Zeilen des Korpus:
# (die Subroutine namens 'lese_korpus' ist hier uninteressant)
my @trainingskorpus = lese_korpus("brown.train.tags.txt");

# Dieser Hash wird zum Zählen benutzt:
my %worttyp_tag_kombinationen;

# Dann wird in einer For-Schleife gezählt:
# (dabei trenne ich Wort und Tag mit einem regulären Ausdruck)
foreach my $zeile (@trainingskorpus) {
	if ($zeile =~ /(\S+)\s+(\S+)/) {
		$worttyp_tag_kombinationen{$1}->{$2}++;	
	}
}
Der Hash hat dann folgende Struktur:

Code: Alles auswählen

$VAR1 = {
          'measles' => {
                         'NN' => 1
                       },
          'Revolutionaries' => {
                                 'NNS' => 1
                               },
          'unsure' => {
                        'JJ' => 1
                      },
          'Unconscionable' => {
                                'JJ' => 1
                              },
          'proven' => {
                        'VBN' => 2
                      },
          'perverted' => {
                           'VBN' => 1
                         },
          'issue' => {
                       'VB' => 7,
                       'NN' => 55
                     },
          'Village' => {
                         'NN' => 4
                       }
               }
Wie man sehen kann, kommt das Wort issue mit zwei verschiedenen Tags im Korpus vor. Beide Vorkommen werden getrennt gezählt.

--> Genau das möchte ich jetzt in Python nachbilden. Das ist mir bisher aber mehr schlecht als recht gelungen. Mein bisheriger Code sieht so aus:

Code: Alles auswählen

# -*- coding: utf-8 -*-

trainingskorpus = file("brown.test.tags.txt", "r").readlines()

lexikon = {}

for zeile in trainingskorpus:
    zeile = zeile.rstrip().split()
    lexikon[zeile[0]] = {}
Jetzt habe ich zumindest schon mal das jeweilige Wort als Schlüssel und ein leeres Dictionary als Wert. Wie muss ich jetzt weiter vorgehen, um das gleiche wie in Perl zu erreichen? Hat jemand von euch eine Idee?

Vielen herzlichen Dank und frohe Weihnachten! :)

LG,
Dingels
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Statt file sollte man open nutzen, außerdem kannst du über die Datei selbst iterieren.

Code: Alles auswählen

with open('brown.test.tags.txt') as f:
    for line in f:
        word, tag = line.strip().split(' ')
Die Verwendung von with garantiert dass die Datei auch wieder sauber geschlossen wird, sollte dass Programm innerhalb des Blocks abstürzen. Außerdem siehst du hier eine coole Sache bei Python: unpacking, das funktioniert mit allen Sequenzen.

Zu deinem eigentlich Problem:

Ich würde 2 `collections.defaultdict`s verwenden, eines fürs `lexikon` wessen default_factory in defaultdict ist, wessen default_factory wiederum ein int ist.
(Problem hierbei ist das defaultdicts nur callables nehmen, ein defaultdict selber ist dies nicht. Wie du dass Problem löst überlasse ich an dieser stelle mal dir ;) )

Dann kannst du `word` als Key für das Lexikon verwenden, den Tag wieder als Key für das dict was lexikon[word] zurückgibt und dann musst du nur noch den Wert erhöhen der per default 0 ist.

Klingt vielleicht kompliziert aber der Code dafür ist nicht eine Zeile länger als den, den du sowieso schon hast und ist auch recht einfach.
Dingels
User
Beiträge: 61
Registriert: Dienstag 23. Dezember 2008, 19:50

Hallo DasIch,

vielen Dank für deine schnelle Antwort. Allerdings versteh ich leider nur die Hälfte von dem, was du mir sagen willst. Insbesondere folgendes versteh ich nicht:
Ich würde 2 `collections.defaultdict`s verwenden, eines fürs `lexikon` wessen default_factory in defaultdict ist, wessen default_factory wiederum ein int ist.
(Problem hierbei ist das defaultdicts nur callables nehmen, ein defaultdict selber ist dies nicht.
Was genau meinst du damit? Sorry, aber ich bin noch ziemlicher Anfänger. :P Danke
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Code: Alles auswählen

In [1]: normal_dict = {}

In [2]: normal_dict['foo'] += 1
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)

/home/dasich/projects/vocablr/vocablr/<ipython console> in <module>()

KeyError: 'foo'

In [3]: from collections import defaultdict

In [4]: default_dict = defaultdict(int)

In [5]: default_dict['foo'] += 1

In [6]: default_dict
Out[6]: defaultdict(<type 'int'>, {'foo': 1})
Du verstehst den Vorteil eines defaultdicts? Sollte ein Key nicht existieren(du aber trotzdem versuchen darauf zuzugreifen) wird die default_factory, die du bei der instanzierung übergeben hast, aufgerufen und was auch immer diese zurückgibt wird als Wert für diesen Key verwendet.
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

Also um es zu verdeutlichen: in Python würde man auch ein ``dict-of-dicts`` verwenden. Nur kann man es sich einfacher machen, indem man statt einem normalen ``dict``, das sich sicherlich ähnlich verhält wie ein Hash in Perl, eben ein defaultdict dass nicht vorhandene Werte automatisch auf einem spezifizierten Wert initialisiert.
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
Dingels
User
Beiträge: 61
Registriert: Dienstag 23. Dezember 2008, 19:50

Ahso, jetzt verstehe ich. Das hab ich bisher mit der get()-Methode gelöst. Also folgendermaßen:

Code: Alles auswählen

# -*- coding: utf-8 -*-

trainingskorpus = file("brown.test.tags.txt", "r").readlines()

lexikon = {}

for zeile in trainingskorpus:
    zeile = zeile.rstrip().split()
    lexikon[zeile[0]] = lexikon.get(zeile[0], 0) + 1
Damit kann ich jetzt die Worte zählen. Wenn ich jetzt aber dazu noch die Tags zählen will, hatte ich erst folgendes gemacht, was aber nicht funktioniert:

Code: Alles auswählen

# -*- coding: utf-8 -*-

trainingskorpus = file("brown.test.tags.txt", "r").readlines()

lexikon = {}

for zeile in trainingskorpus:
    zeile = zeile.rstrip().split()
    lexikon[zeile[0]] = {}
    lexikon[zeile[0]][zeile[1]] = lexikon[zeile[0]].get(zeile[1], 0) + 1
Das funktioniert aber nur insofern, dass nur das Tag, welches in derselben Zeile wie das Wort steht, dem Wort als Wert zugewiesen wird. Jedes innere Dictionary hat also nur ein Schlüssel-Wert-Paar.

So sieht das aus:

Code: Alles auswählen

{'raining': {'VBG': 1}, 'unscientific': {'JJ': 1}, 'writings': {'NNS': 1}, 'yellow': {'JJ': 1}, 'Heights': {'NNS': 1} }
Wie kann ich das Problem lösen?
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Code: Alles auswählen

In [1]: from collections import defaultdict

In [2]: lexikon = defaultdict(lambda: defaultdict(int))
Was du mit dem `lexikon` jetzt wie anstellst überlasse ich mal dir. Probiers einfach mal in der Python Shell aus.
Dingels
User
Beiträge: 61
Registriert: Dienstag 23. Dezember 2008, 19:50

Aber tut meine get()-Methode nicht das gleiche, was deine default_dicts tun?

Das Problem wär ja jetzt gelöst. Nur das andere Problem nicht, dass nicht alle vorkommenden Kombinationen von Wort und Tag gezählt werden.
BlackJack

@Dingels: Dein Problem ist Zeile 10 in der Du was auch immer vorher unter dem Schlüssel erreichbar war, durch ein leeres Dictionary ersetzt. Beispiel:

Text:
foo A
foo B

Nach dem ersten Schleifendurchgang hast Du dann ``lexikon = {'foo': {'A': 1}}``

Im zweiten Durchgang hast Du nach Zeile 10: ``lexicon = {'foo': {}}``.
Benutzeravatar
cofi
Python-Forum Veteran
Beiträge: 4432
Registriert: Sonntag 30. März 2008, 04:16
Wohnort: RGFybXN0YWR0

Ja das tut deine get Methode ... aber mit einem defaultdict ist es weit lesbarer:

Code: Alles auswählen

In [5]: default_dict = defaultdict(int)

In [6]: default_dict["a"]
Out[6]: 0
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Dingels hat geschrieben:Aber tut meine get()-Methode nicht das gleiche, was deine default_dicts tun?
Deine Variante ist komplizierter und schwerer zu lesen aber sonst ja.
Dingels
User
Beiträge: 61
Registriert: Dienstag 23. Dezember 2008, 19:50

@BlackJack

Aber wenn ich die Zeile 10 weglasse, erhalte ich folgenden Fehler:

Code: Alles auswählen

Traceback (most recent call last):
    lexikon[zeile[0]][zeile[1]] = lexikon[zeile[0]].get(zeile[1], 0) + 1
KeyError: 'With'
Habt ihr nicht mal einen Vorschlag für konkreten Code, mit dem sich das Problem lösen ließe? Ich würde übrigens zunächst gerne bei der get()-Methode bleiben.
yipyip
User
Beiträge: 418
Registriert: Samstag 12. Juli 2008, 01:18

@DasIch:
Danke für das
"lambda im defaultdict"
Darauf wäre ich jetzt nicht gekommen.
:)

@Dingels:
Leider hab ich auch nur eine Lösung mit
'defaultdict'.

Ohne Gewähr:

Code: Alles auswählen

from __future__ import with_statement
from collections import defaultdict

word_dict = defaultdict(lambda: defaultdict(int))

with open('text', 'r') as fp:
  for line in fp:
    word, tag = line.strip().split()[:2]
    word_dict[word][tag] += 1


for w in word_dict:
  print '%s\t' % w,
  for t in word_dict[w]:
    print '\t%s:%s ' % (t, word_dict[w][t]),
  print
:wink:
yipyip
Benutzeravatar
cofi
Python-Forum Veteran
Beiträge: 4432
Registriert: Sonntag 30. März 2008, 04:16
Wohnort: RGFybXN0YWR0

yipyip hat geschrieben:@DasIch:
Danke für das
"lambda im defaultdict"
Darauf wäre ich jetzt nicht gekommen.
:)
Ich muss zugeben ich auch nicht, aber wohl nur weils nicht nötig ist ;)

@dingels Den KeyError kann ich mir nicht erklären. Der tritt auf, da dein "With" nicht im Dict ist, aber get sollte dann eben deinen Defaultwert 0 zurückgeben.
Gibts einen vernünftigen Grund, dass du kein defaultdict verwenden willst? Ich sehe absolut keinen ;)
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

cofi hat geschrieben:Ich muss zugeben ich auch nicht, aber wohl nur weils nicht nötig ist ;)
Da sagt ein TypeError aber was anderes.

Code: Alles auswählen

In [1]: from collections import defaultdict

In [2]: defaultdict(defaultdict(int))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)

/home/dasich/projects/Wok/<ipython console> in <module>()

TypeError: first argument must be callable
Benutzeravatar
cofi
Python-Forum Veteran
Beiträge: 4432
Registriert: Sonntag 30. März 2008, 04:16
Wohnort: RGFybXN0YWR0

Hm natürlich ... es ging ja um genestete Dicts. :( Entschuldige hab ich übersehen ;)
Benutzeravatar
HerrHagen
User
Beiträge: 430
Registriert: Freitag 6. Juni 2008, 19:07

Hallo!

Hier mal eine einfache Lösung. Die Verwendung der default_dicts find ich ja doch eher kryptisch...
Du müsstest noch das Lesen des strings duch das Lesen der Datei ersetzen. Aber da weißt du ja wie das geht.

Code: Alles auswählen

dat = """The AT
Fulton NP
County NN
an AT
an AT
an AT
an NN
an IN
investigation NN
investigation XXX
investigation NN
of IN
Atlanta's NP$
recent JJ
primary NN
election NN
produced VBD """

lines = dat.splitlines()  # ersetzen durch Lesen aus Datei
lines = map(str.split, lines)

lexicon = {}
for word, tag in lines:
    if not lexicon.has_key(word):
        lexicon[word] = {tag:1}
    else:
        try:
            lexicon[word][tag] += 1
        except:
            lexicon[word][tag] = 1
lexicon hat am Ende folgenden Inhalt:

Code: Alles auswählen

>>> lexicon
{'produced': {'VBD': 1}, 'of': {'IN': 1}, "Atlanta's": {'NP$': 1}, 'primary': {'NN': 1}, 'an': {'AT': 3, 'NN': 1, 'IN': 1}, 'County': {'NN': 1}, 'investigation': {'XXX': 1, 'NN': 2}, 'election': {'NN': 1}, 'The': {'AT': 1}, 'Fulton': {'NP': 1}, 'recent': {'JJ': 1}}

>>> for a, b in zip(lexicon.keys(), lexicon.values()):
        print a, b

        
produced {'VBD': 1}
of {'IN': 1}
Atlanta's {'NP$': 1}
primary {'NN': 1}
an {'AT': 3, 'NN': 1, 'IN': 1}
County {'NN': 1}
investigation {'XXX': 1, 'NN': 2}
election {'NN': 1}
The {'AT': 1}
Fulton {'NP': 1}
recent {'JJ': 1}
Ich hoffe das ist das was du dir vorgestellt hast.

Frohe Weihnachten
HerrHagen
BlackJack

@HerrHagen: Also ich finde `defaultdict`\s weit weniger kryptisch als Deine Lösung, wo man sich erst einmal klar machen muss welcher Zweig wann zum Einsatz kommt, oder so eine lange Zeile mit zwei `get()`-Aufrufen.

Alternative zu ``lambda`` wäre `functools.partial()`:

Code: Alles auswählen

from __future__ import division, with_statement
from collections import defaultdict
from functools import partial
from pprint import pprint


def main():
    lexicon = defaultdict(partial(defaultdict, int))
    with open('test.txt') as lines:
        for line in lines:
            word, tag = line.rstrip().split()
            lexicon[word][tag] += 1
    
    for word, tags in sorted(lexicon.iteritems()):
        print '%s: %s' % (word, sorted(tags.iteritems()))


if __name__ == "__main__":
    main()
Ansonsten würde ich ``in`` statt `has_key()` verwenden und auf keinen Fall ein ``except`` ohne eine konkrete Ausnahme verwenden. Zeile 30 soll ja zum Beispiel nicht ausgeführt werden, wenn in Zeile 28 ein Fipptehler bei einem Namen existiert, oder wenn jemand bei der Programmausführung an einer ungünstigen Stelle Strg+C drückt.
Benutzeravatar
HerrHagen
User
Beiträge: 430
Registriert: Freitag 6. Juni 2008, 19:07

Ansonsten würde ich ``in`` statt `has_key()` verwenden und auf keinen Fall ein ``except`` ohne eine konkrete Ausnahme verwenden. Zeile 30 soll ja zum Beispiel nicht ausgeführt werden, wenn in Zeile 28 ein Fipptehler bei einem Namen existiert, oder wenn jemand bei der Programmausführung an einer ungünstigen Stelle Strg+C drückt.
Da hast du natürlich recht, war einfach schon etwas spät in der Nacht...
@HerrHagen: Also ich finde `defaultdict`\s weit weniger kryptisch als Deine Lösung, wo man sich erst einmal klar machen muss welcher Zweig wann zum Einsatz kommt, oder so eine lange Zeile mit zwei `get()`-Aufrufen.
Da bin ich anderer Meinung. Deine Variante ist wirklich nett. Hat aber meiner Meinung nach ein Problem:

Code: Alles auswählen

from collections import defaultdict
from functools import partial
Man benötigt die Kenntnis der Funktionsweise von gleich zwei Standard-Modulen. Das ist ja nichts schlimmes, ist aber als Einsteigerlösung (nach der hier gefragt wurde) eher weniger zu gebrauchen.
Außerdem find ich die Verzweigung nicht so schlimm, da sich das ganze wie Pseudocode ließt. Wenn lexicon nicht word enthält, dann lege einen neuen Eintrag...
Ich finde, dass man bei der defaultdict Variante mehr nachdenken muss warum das ganze überhaupt funktioniert.

Warum importierst du pprint, division wenn du sie dann nicht benutzt?

MFG HerrHagen
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

HerrHagen hat geschrieben:Da bin ich anderer Meinung. Deine Variante ist wirklich nett. Hat aber meiner Meinung nach ein Problem:

Code: Alles auswählen

from collections import defaultdict
from functools import partial
Man benötigt die Kenntnis der Funktionsweise von gleich zwei Standard-Modulen. Das ist ja nichts schlimmes, ist aber als Einsteigerlösung (nach der hier gefragt wurde) eher weniger zu gebrauchen.
Wieso? Eine Einsteigerlösung bedeutet ja nicht dass man alles auf umständliche Weise machen muss. Der Einsatz von ``partial`` ist da etwas Grenzwertig, da für Anfänger Funktionen höherer Ordnung etwas tricky sein können (außer sie haben Higher Order Perl gelesen, was es jetzt kostenlos online gibt), aber das ``defaultdict`` macht den Code ausreichend simpler dass es sich lohnt einfach mal in die Doku zu schauen was es tut. Einsteigercode ist ja auch für den Einstieg gedacht aber das heißt ja nicht dass man an dem Code nicht wachsen kann.
HerrHagen hat geschrieben:Warum importierst du pprint, division wenn du sie dann nicht benutzt?
Einige Leute haben halt Templates für neue Python-Dateien und da kann es dann vorkommen, dass da ``pprint`` oder ``division`` automatisch importiert werden. Für den Quelltext ist es natürlich unnötig.
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
Antworten