Anwendungsbeispiel: Small-World

Werbung
Diskrete Modellierung
Wintersemester 2016/17
Martin Mundhenk
Uni Jena, Institut für Informatik
12. Februar 2017
4.5 Das Small-World-Phänomen
Anwendungsbeispiel
Die Untersuchung von Graphen hat sich – verstärkt durch die weltweite
Vernetzung – zu einem eigenen Forschungsgebiet entwickelt. Das
Small-World-Experiment sollte herausfinden, wie kurz der Abstand“
”
zwischen zwei beliebigen Menschen ist, der als Länge der kürzesten Kette
von Freunden, Freunden von Freunden usw. definiert ist, die die beiden
Menschen verbindet (Stanley Milgram, 1967). Mit den
Freundschaftsgraphen“ der sozialen Netzwerke lässt sich das heutzutage
”
berechnen.
Wir werden sehen, wie man das algorithmisch machen kann.
Wir schauen uns nochmal die Implementierung von Graphen mit den in
Python vorhandenen Datenstrukturen dict und set an.
4.5.1
Die API für set
Für Mengen braucht man Operationen zum Einfügen, Entfernen und
Abfragen von Elementen, zum Durchlaufen aller Elemente der Menge und
für grundlegende Mengenoperationen.
Operation
set()
Beschreibung
eine neue (leere) Menge
s.add(item)
füge item zur Menge s hinzu
s.delete(item)
entferne item aus der Menge s
item in s
gibt True zurück, falls item in der Menge s enthalten ist
for item in s:
iteriere über alle Elemente der Menge s
s.intersection(t)
der Durchschnitt der Mengen s und t
s.union(t)
die Vereinigung der Mengen s und t
4.5.2
Ein Graph (aus Knoten und Kanten)
aus: Introduction to Programming in Python (Sedgewick, Wayne, Dondero, 2015)
4.5.3
Wege in Graphen
aus: Introduction to Programming in Python (Sedgewick, Wayne, Dondero, 2015)
4.5.4
Noch ein Graph (aus Knoten und Kanten)
aus: Introduction to Programming in Python (Sedgewick, Wayne, Dondero, 2015)
4.5.5
Anwendungen von Graphen
§
Verkehrssysteme
§
Kommunikationssysteme
§
Humanbiologie
§
Soziale Netzwerke
§
Physikalische Systeme
§
Softwaresysteme
§
Finanzsysteme
Darstellung von Graphen als Textdateien
Pro Zeile: ein Knoten und weitere Knoten, zu denen er eine Kante hat.
movies.txt
21 Grams (2003)/Alban, Carlo/Finnell, Michael W./Finnell, Michael/...
...
Manhattan (1979)/.../Allen, Woody/.../Keaton, Diane/Farrow, Tisa/...
...
fluege.txt
...
CEK
CEK
DME
DME
DME
EGO
LAX
...
KZN
OVB
KZN
NBC
TGK
KGD
PHX
4.5.7
Die Grundidee für den Datentyp für Graphen
aus: Introduction to Programming in Python (Sedgewick, Wayne, Dondero, 2015)
4.5.8
Die (neue) API für Graph
Ein Graph soll aus einer Datei eingelesen werden können. Knoten und
Kanten können hinzugefügt werden. Die Knoten können durchlaufen
werden, ebenso alle Nachbarn eines Knotens.
Operation
Beschreibung
Graph(dateiname,trennzeichen) ein neuer Graph, dessen Beschreibung aus der
Datei dateiname eingelesen wird; trennzeichen ist das
Trennzeichen zwischen den Knoten einer Zeile in der Datei. Wenn dateiname None ist, wird ein leerer Graph
erzeugt.
g.kanteHinzufuegen(von,nach) fügt die Kante zwischen den Knoten von und
nach in beiden Richtungen zum Graph g hinzu
g.knoten()
ein iterable für die Knoten von g
g.nachbarnVon(k)
ein iterable für die Nachbarn von Knoten k im Graph g
4.5.9
Operation
g.hatKnoten(k)
Beschreibung
gibt True zurück, falls Knoten k in Graph g vorkommt
g.hatKante(v,n)
gibt True zurück, falls eine Kante zwischen v und n in
Graph g vorkommt
g.grad(k)
gibt die Anzahl der Nachbarn von Knoten k im Graph g
zurück
g.knotenzahl()
gibt die Anzahl der Knoten von Graph g zurück
g.kantenzahl()
gibt die Anzahl der Kanten von Graph g zurück
4.5.10
Ein Teil der Implementierung von Graph
from instream import InStream
class Graph:
def __init__(self, dateiname=None, trennzeichen=None):
self._anzahlKanten = 0
self._adj = dict()
if dateiname is not None:
eingabe = InStream(dateiname)
while eingabe.hasNextLine():
zeile = eingabe.readLine()
knoten = zeile.split(trennzeichen)
for i in range(1,len(knoten)):
self.kanteHinzufuegen(knoten[0],knoten[i])
def kanteHinzufuegen(self,von,nach):
if not self.hatKnoten(von): self.knotenHinzufuegen(von)
if not self.hatKnoten(nach): self.knotenHinzufuegen(nach)
if not self.hatKante(von,nach):
self._anzahlKanten += 1
self._adj[von].add(nach)
self._adj[nach].add(von)
4.5.11
Ein Client für Graph
Wir wollen ein Programm schreiben, das einen Graph aus einer Datei einliest
und anschließend zu Knoten, die von der Konsole eingegeben werden, die
Nachbarn im Graph ausgibt.
import sys
import stdio
from Graph import Graph
dateiname = sys.argv[1]
trennzeichen = sys.argv[2]
graph = Graph(dateiname,trennzeichen)
while stdio.hasNextLine():
v = stdio.readLine()
if graph.hatKnoten(v):
for w in graph.nachbarnVon(v):
stdio.writeln('
' + w)
4.5.12
Wir benutzen drei Beispiel-Graphen: movies.txt enthält Titel/Schauspieler
zu 4188 Filmen, moviesg.txt ist ein Teil davon mit 1100 Filmen, und
fluege.txt ist eine Datei mit Start-/Zielflughafen von 67000 Linienflügen.
$ python zeigeNachbarn.py fluege.txt " "
ERF
$ python zeigeNachbarn.py movies.txt "/"
PMI
Manhattan (1979)
FUE
Streep, Meryl
LPA
Conroy, Frances
AYT
Murphy, Michael (I)
LGW
Allen, Woody
TFS
...
LEJ
BCN
Allen, Woody
DME
Husbands and Wives (1992)
ACE
Deconstructing Harry (1997)
AGA
Bananas (1971)
IST
Stanley Kubrick: A Life in Pictures (2001) FUE
New York Stories (1989)
FNC
...
MUC
CGN
...
Suche nach (kürzesten) Wegen – PfadFinder
Das hatten wir schonmal – Breitensuche.
Jetzt implementieren wir dafür eine eigene Klasse.
Operation
Beschreibung
PfadFinder(graph,startknoten)
finde kürzeste Pfade von startknoten
zu den Knoten im Graph graph
pf.erreichbar(k)
gibt True zurück, falls Knoten k erreichbar ist
pf.distanzZu(k)
gibt die Entfernung von Knoten k zurück
pf.pfadZu(k)
gibt den Pfad zu Knoten k als Array zurück
pf.erreichbareKnoten()
Iterator über die erreichbaren Knoten
pf.maxEntfernung()
gibt die maximale Entfernung und einen Knoten mit
dieser Entfernung von startknoten zurück
import ...
class PfadFinder:
def __init__(self,graph,startknoten):
self._entfernungZu = dict()
self._kanteZu = dict()
warteschlange = Queue()
warteschlange.enqueue(startknoten)
self._entfernungZu[startknoten] = 0
self._kanteZu[startknoten] = None
while not warteschlange.isEmpty():
k = warteschlange.dequeue()
for n in graph.nachbarnVon(k):
if n not in self._entfernungZu:
warteschlange.enqueue(n)
self._entfernungZu[n] = 1 + self._entfernungZu[k]
self._kanteZu[n] = k
def pfadZu(self, k):
pfad = []
while k is not None:
pfad += [str(k)]
k = self._kanteZu[k]
pfad.reverse()
return pfad
def erreichbar(self,k):
return k in self._entfernungZu
def erreichbareKnoten(self):
return iter(self._entfernungZu)
def distanzZu(self, k):
return self._entfernungZu[k]
def hatPfadZu(self, k):
return k in self._entfernungZu
def maxEntfernung(self):
entf = 0
knoten = None
for k,e in self._entfernungZu.iteritems():
if e > entf:
entf = e
knoten = k
return entf, knoten
4.5.16
Clienten für PfadFinder
Wir wollen feststellen, ob in unseren Graphen jeder Knoten von jedem
anderen aus erreichbar ist.
Jeder Graph zerfällt in Komponenten aus gegenseitig erreichbaren Knoten.
Eine übliche Vorstellung ist, dass ein Graph nur aus einer Komponente
besteht. Das ist aber nicht immer so.
Der Client komponenten.py für PfadFinder liest einen Graph aus einer Datei
(dazu erhält er den Dateinamen und das Trennzeichen von der
Kommandozeile) und zerlegt“ ihn in seine Komponenten. Dazu geht er alle
”
Knoten durch. Falls der betrachtete Knoten k nicht in einer bereits gefundenen
Komponente liegt, bestimmt er mittels PfadFinder alle von k aus
erreichbaren Knoten. Sie bilden die Komponente, die auch k enthält Unter
dem Schlüssel k werden sie in das Dictionary komponenten eingetragen.
Am Ende enthält komponenten einen Eintrag für jede Komponente des
Graphen. Der Client gibt für jede Komponente den Knoten, von dem aus sie
gefunden wurde, und die Anzahl der Knoten in der Komponente aus.
4.5.17
# komponenten.py
import sys, stdio
from Graph import Graph
from PathFinder import PfadFinder
dateiname = sys.argv[1]
trennzeichen = sys.argv[2]
graph = Graph(dateiname,trennzeichen)
gefundeneKnoten = set()
komponenten = dict()
for k in graph.knoten():
if k not in gefundeneKnoten:
komponenten[k] = set()
pf = PfadFinder(graph,k)
for ek in pf.erreichbareKnoten():
komponenten[k].add(ek)
gefundeneKnoten.add(ek)
print "Der Graph hat",len(komponenten), \
"Komponenten."
for k,s in komponenten.iteritems():
print k, len(s)
$ python komponenten.py movies.txt "/"
Der Graph hat 33 Komponenten.
Jeon, Jin-bae 16
Mitchell, Paula 118762
Joffroy, Pierre 14
...
Eggleston, Ralph 2
...
Mystery Science Theater 3000: The Movie (1996) 6
...
Suknovalov, Aleksei 11
McGowan, Kathleen (I) 29
Amado, Chisco 18
$ python komponenten.py moviesg.txt "/"
Der Graph hat 1 Komponenten.
Chambers, Janice 20144
$ python komponenten.py fluege.txt " "
Der Graph hat 8 Komponenten.
AGN 3397
FRD 4
TKJ 2
OND 4
BLD 2
AKB 4
SPB 2
KNQ 10
Das Small-World-Experiment
Nun können wir in einer Komponente das Small-World-Experiment machen –
also die maximale Länge der kürzesten Verbindung zwischen zwei Knoten
berechnen.
Dazu brauchen wir einen Graph, der aus einer Komponente besteht – bei dem
man also von jedem Knoten aus jeden anderen Knoten erreichen kann. Auf der
letzten Folie haben wir gesehen, dass der Graph in moviesg.txt so ist.
Der Graph in fluege.txt besteht aus 8 Komponenten. Es ist leicht, daraus
nur die Flüge in der größten Komponente auszuwählen. Wir speichern sie als
Datei in fluege2.txt.
Der folgende Client findet zwei Knoten, deren kürzeste Verbindung die längste
aller kürzesten Verbindungen zwischen zwei Knoten ist. Dazu bestimmt er für
jeden Knoten den längsten kürzesten Weg zu einem anderen Knoten, und
merkt sich das Maximum aller dieser längsten kürzesten Wege.
4.5.19
# enge.py
import sys
from PathFinder import PfadFinder
from Graph import Graph
def maxMinEntfernung(graph):
maxe = 0
for k in graph.knoten():
pf = PfadFinder(graph,k)
$ python enge.py fluege2.txt " "
(entf,knoten) = pf.maxEntfernung()
Der Graph hat 3397 Knoten.
if entf > maxe:
Die groesste Entfernung ist 13
maxe = entf
von YPO nach SVR.
maxstart = k
['YPO', 'YAT', 'ZKE', 'YFA', 'YMO',
maxziel = knoten
'YTS', 'YYZ', 'CPH', 'SFJ', 'JAV',
maxpfad = pf.pfadZu(knoten)
'JUV', 'NAQ', 'THU', 'SVR']
return (maxe,maxstart,maxziel,maxpfad)
#------------------------------------------------------def test():
dateiname = sys.argv[1]
trennzeichen = sys.argv[2]
g = Graph(dateiname,trennzeichen)
print "Der Graph hat", g.knotenzahl(), "Knoten."
erg = maxMinEntfernung(g)
print "Die groesste Entfernung ist", str(erg[0]), \
"von", str(erg[1]), "nach", str(erg[2])+"."
if __name__=='__main__': test()
Small-World-Graphen haben die Eigenschaften
§
nur Knoten mit wenigen Nachbarn zu haben,
§
kurze durchschnittliche Entfernung zwischen allen Knoten haben,
§
aus vielen Clustern mit sehr vielen Verbindungen untereinander zu
bestehen.
Diese Eigenschaften von Graphen können wir messen.
Als Beispiel betrachten wir die durchschnittliche Entfernung zwischen
allen Knoten.
Die Funktion mittlerePfadlaengeK(g,k) berechnet die
durchschnittliche Entfernung von Knoten k zu allen anderen Knoten im
Graph g. Dazu bestimmt sie mit PfadFinder die Entfernungen von k zu
allen anderen Knoten, summiert sie auf und teilt sie durch die Anzahl der
anderen Knoten.
Entsprechend berechnet mittlerePfadlaenge(g) den Durchschnitt der
durchschnittlichen Entfernung aller Knoten.
Außerdem bestimmen wir noch einen zentralen Knoten, dessen
durchschnittliche Entfernung zu allen anderen Knoten minimal ist.
# graphprops.py
import sys
from PathFinder import PfadFinder
from Graph import Graph
def mittlerePfadlaenge(graph):
summe = 0
for k in graph.knoten():
summe += mittlerePfadlaengeK(graph,k)
return summe / graph.knotenzahl()
def mittlerePfadlaengeK(graph,k):
p = PfadFinder(graph,k)
summe = 0
for n in graph.knoten():
summe += p.distanzZu(n)
return 1.0 * summe / (graph.knotenzahl()-1)
4.5.23
def zentralerKnoten(graph):
tabelle = dict()
for k in graph.knoten():
tabelle[k] = mittlerePfadlaengeK(graph,k)
minimum = None
for key, value in tabelle.iteritems():
if minimum == None or value<minimum:
minkey = key
minimum = value
return (minkey,minimum)
#------------------------------------------------------------------------dateiname = sys.argv[1]
trennzeichen = sys.argv[2]
g = Graph(dateiname,trennzeichen)
print "Der Graph hat", g.knotenzahl(), "Knoten."
k,e = zentralerKnoten(g)
print "Zentraler Knoten ist", str(k), " mit mittlerer Entfernung", e, "."
print "Die mittlere Pfadlaenge ist", mittlerePfadlaenge(g), "."
4.5.24
$ python graphprops.py fluege2.txt " "
Der Graph hat 3397 Knoten.
Zentraler Knoten ist FRA mit mittlerer Entfernung 2.48851590106 .
Die mittlere Pfadlaenge ist 4.1032411679 .
$ python graphprops.py moviesg.txt "/"
Der Graph hat 20144 Knoten.
Zentraler Knoten ist Around the World in Eighty Days (1956)
mit mittlerer Entfernung 4.59246388323 .
Die mittlere Pfadlaenge ist 6.9264036957 .
4.5.25
Herunterladen