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

Dienstag 23. Dezember 2008, 20:24

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: 2437
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Dienstag 23. Dezember 2008, 20:41

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

Dienstag 23. Dezember 2008, 20:48

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: 2437
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Dienstag 23. Dezember 2008, 20:52

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
Administrator
Beiträge: 16024
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

Dienstag 23. Dezember 2008, 20:56

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 Modvoice
Dingels
User
Beiträge: 61
Registriert: Dienstag 23. Dezember 2008, 19:50

Dienstag 23. Dezember 2008, 21:11

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: 2437
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Dienstag 23. Dezember 2008, 21:20

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

Dienstag 23. Dezember 2008, 21:26

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

Dienstag 23. Dezember 2008, 21:40

@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
Moderator
Beiträge: 4432
Registriert: Sonntag 30. März 2008, 04:16
Wohnort: RGFybXN0YWR0

Dienstag 23. Dezember 2008, 21:44

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: 2437
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Dienstag 23. Dezember 2008, 22:00

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

Dienstag 23. Dezember 2008, 22:10

@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

Dienstag 23. Dezember 2008, 22:26

@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
Moderator
Beiträge: 4432
Registriert: Sonntag 30. März 2008, 04:16
Wohnort: RGFybXN0YWR0

Mittwoch 24. Dezember 2008, 01:25

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: 2437
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Mittwoch 24. Dezember 2008, 01:45

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
Antworten