Python exemplarisch
deutsch     english

Affinitätsanalysen

 

Alle Programme können von hier heruntergeladen werden.


 

Problembeschreibung

 

Beim Einkauf in einem Supermarkt aber auch über das Internet können die Kunden mehrere Produkte miteinander einkaufen. Es ist oft interessant, das Kundenverhalten zu studieren und herauszufinden, welche Produkte oft gemeinsam eingekauft werden. Dadurch können etwa Kunden gezielt beworben werden, indem ihnen beim Einkauf des einen Produkts der Kauf eines dazu affinen Produkts angeboten wird.

Statt für Einkäufe in einem Supermarkt werden solche Informationen auch bei Bücherbestellungen im Online-Buchhandel oder beim Mieten von TV-Filmen gesammelt und gezielt ausgewertet.

Wir betrachten exemplarisch 6 Produkte in einem Supermarkt für Lebensmittel: Brot (bread), Käse (cheese), Äpfel (apple), Bananen (banana), Teigwaren (pasta), Tomatensauce (sugo)

An der Kasse werden die gekauften Produkte erfasst. Jeder Einkauf entspricht einem Datensatz, d.h. jede Person hat die Attribute (attributes): Brot (bread), Käse (cheese), Äpfel (apple), Bananen (banana), Teigwaren (pasta), Tomatensauce (sugo) mit je den Werten (values): 1 oder 0, je nachdem, ob sich das Produkt im Warenkorb befindet oder nicht.


 

Erzeugung der Daten

 

Zuerst erstellen wir in der Datei shopping.dat eine zufällige Datensammlung, die aber gewisse Tendenzen (typisch für Schweizer Personen) widerspiegelt. Die Werte der Attribute jedes Datensatzes werden komma-getrennt in eine Datei gespeichert (entsprechend des häufigsten Formats von csv-Dateien).

Programm: [►]

# CreatePurchases.py 

from random import randint

datafile = "shopping.dat"                            
nbSamples = 100
attributes = ["bread", "cheese", "apple", "banana",  "pasta", "sugo"]
nbAttributes = len(attributes)
fOut = open(datafile, "w")

for i in range(nbSamples):
    purchase = [0] * nbAttributes
    r = randint(0, 100)
    if r <= 25: # Swiss germans like bread and cheese
        purchase[0] = 1
        purchase[1] = 1
    elif r > 25 and r <= 40: # Swiss romands like fruits
        purchase[2] = 1
        purchase[3] = 1
    elif r > 40 and r <= 50: # Swiss italians like pasta with sugo
        purchase[4] = 1
        purchase[5] = 1
    else: # anybody else selects each item randomly
        for i in range(nbAttributes): 
            r = randint(0, 100)
            if r < 50:
                purchase[i] = 1
    purchaseStr = [str(e) for e in purchase]                
    sample = ','.join(purchaseStr)
    fOut.write(sample + "\n")

fOut.close()
print nbSamples, "purchases saved."
 
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

Bemerkungen:
Der Datensatz wir zuerst mit Integers in einer Liste purchase gespeichert. Diese wird nachfolgend in die Liste purchaseStr mit Strings umkopiert und mit join() zu einem Datensatz zusammengestellt, der mit in der Datei gespeichert wird. (Sie befindet sich im gleiche Verzeichnis wie das Programm.)


 

Affinität von Produkten

 

Wir untersuchen die Wahrscheinlichkeiten bestimmter Affinitätsregeln: Falls jemand das Produkt A kauft, kauft er/sie auch das Produkt B. Beispiel: Falls jemand Pasta kauft, so kauft er/sie auch Sugo.

Hier einige Begriffe:

Unter der Prämisse (premise) versteht man die Aussage, dass jemand Pasta kauft, unter der Konklusion (conclusion) versteht man die Aussage, dass jemand dann auch Sugo kauft.

Unter dem Support (support) versteht man die totale Zahl der Einkaufskörbe,bei denen die Prämisse erfüllt ist.

Unter der Konfidenz (Confidence) versteht man die relative Anzahl, dass falls die Prämisse erfüllt ist, auch die Konklusion erfüllt ist.

Programm: [►]

# Shopping1.py

dataFile = "shopping.dat"

def loadIntData(fileName):    
    '''
    Reads data table from Ascii file with comma separated integers.
    Returns table as matrix in format [[row0], [row1], ...].
    E.g. 
    1,2,3,4
    -1,1,-1,1
    20,50,100,600   
    returns
    [[1, 2, 3, 4], [-1, 1, -1, 1], [20, 50, 100, 600]]
    '''
    try:    
        fData = open(fileName, 'r')
    except:
        return []
    out = []
    for line in fData:
        li = [int(i) for i in line.split(',')]
        out.append(li)
    fData.close()
    return out

# Read dataset with attributes: 
# bread, cheese, apple, banana,  pasta, sugo
X = loadIntData(dataFile)
print "Rule: If a person buys pasta, they also buy sugo"

# How many of the cases that a person bought pasta involved 
# the person purchasing sugo too?
nbPastaPurchases = 0
ruleValid = 0
for sample in X:
    if sample[4] == 1:  # This person bought pasta
        nbPastaPurchases += 1
        if sample[5] == 1: # This person bought sugo too
            ruleValid += 1
print nbPastaPurchases, "cases of the pasta purchases were discovered."
print ruleValid, "cases of the rule being valid were discovered"

# The support is the (absolute) number of times  the rule is discovered
support = ruleValid  

# The confidence is the relative frequency the rule is discovered.
confidence = ruleValid / nbPastaPurchases 

print "The support is", support, "and the confidence is", \
       round(100 * confidence, 2), "percent"
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

Typisches Resultat:
Rule: If a person buys pasta, they also buy sugo
32 cases of the pasta purchases were discovered.
20 cases of the rule being valid were discovered
The support is 20 and the confidence is 62.5 percent


 

Affinität zwischen allen Produkten

 

Wir untersuchen die Wahrscheinlichkeiten bestimmter Affinitätsregeln: Falls jemand das Produkt A kauft, kauft er/sie auch das Produkt B. Beispiel: Falls jemand Pasta kauft, so kauft er/sie auch Sugo.

Um einen Überblick über das Einkaufsverhalten zu erhalten, untersuchen wir alle möglichen Regeln. Es gibt offenbar 6 Prämissen, die man je mit 5 Konklusionen kombinieren kann, als 30 verschiedene Regeln.

Die Attribute sollen durch einen Index 0...5 beschrieben sein. In einer 6x6-Tabelle (Matrix) fassen wir alle Support-Zahlen für die 30 Regeln zusammen.

   Brot  Käse  Äpfel  Bananen  Pasta  Sugo
 Brot    falls Brot,
 dann auch
 Käse
 falls Brot,
 dann auch
 Äpfel
 falls Brot,
 dann auch
 Bananen
 falls Brot,
 dann auch
 Pasta
 alls Brot,
 dann auch
 Sugo
 Käse  ...    ...  ...  ...  ...
 Äpfel            
 Bananen            
 Pasta            
 Sugo            

Programm: [►]

# Shopping2.py

from pprint import pprint

datafile = "shopping.dat"

def printRule():
    premiseName = attributes[premise]
    conclusionName = attributes[conclusion]
    print "Rule: If a person buys", premiseName, \
          "they will also buy", conclusionName
    print " - Purchases with premise:", support[premise][premise]
    print " - Support:", support[premise][conclusion]
    print " - Confidence:", round(confidence[premise][conclusion], 2)
    print ""

def loadIntData(fileName):    
    fData = open(fileName, 'r')
    out = []
    for line in fData:
        li = [int(i) for i in line.split(',')]
        out.append(li)
    fData.close()
    return out

X = loadIntData(datafile)
# Names of attributes
attributes = ["bread", "cheese", "apple", "banana", "pasta", "sugo"]
nbAttr = len(attributes) # Number of attributes
support = [[0 for i in range(nbAttr)] for k in range(nbAttr)]
confidence = [[0 for i in range(nbAttr)] for k in range(nbAttr)]

'''
premises
|   -------------------------------------
v   |  0: |  1: |  2: |  3: |  4: |  5: |    <- conclusions
    -------------------------------------
0:  |     |     |     |     |     |     |
    -------------------------------------
1:  |     |     |     |     |     |     |
    -------------------------------------
2:  |     |     |     |     |     |     |
    ------------------------------------
3:  |     |     |     |     |     |     |
    -------------------------------------
4:  |     |     |     |     |     |     |
    ------------------------------------
5:  |     |     |     |     |     |     |
    -------------------------------------
'''

for sample in X:
    for premise in range(nbAttr):
        if sample[premise] == 0:  # no premise
            continue
        support[premise][premise] += 1   # total of premise item
        for conclusion in range(nbAttr):
            if premise == conclusion:  
                continue
            if sample[conclusion] == 1:
                # This person also bought the conclusion item
                support[premise][conclusion] += 1
print "Support:"
pprint(support)

# Calculate confidence
for premise in range(nbAttr):
    for conclusion in range(nbAttr):
        if premise == conclusion:
            continue
        v = support[premise][conclusion]
        t = support[premise][premise]
        if t != 0:
            confidence[premise][conclusion] = round(v / t, 2)
print "Confidence"
pprint(confidence)

# print result                              
for premise in range(nbAttr):
    for conclusion in range(nbAttr):
        if premise == conclusion:
            continue
        printRule()
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

Typisches Resultat:

Support:
[[42, 35, 11, 14, 11, 14],
[35, 50, 18, 17, 12, 16],
[11, 18, 48, 33, 13, 14],
[14, 17, 33, 47, 14, 15],
[11, 12, 13, 14, 32, 20],
[14, 16, 14, 15, 20, 36]]

Bemerkungen:
Um die Tabelle schön auszuschreiben, wird das Modul pprint verwendet. In der Diagonalen der Tabelle speichern wir die Totalzahl der Einkäufe, die die Prämisse erfüllten.


 

Visualisierung der Resultate

 

Einmal mehr veranschaulichen wir die Resultate mit einer gefälligen Visualisierung. Es wird wie üblich das Modul gpanel (für TigerJython), bzw. pygpanel (für Python 2.7) eingesetzt.

Programm: [►]

# Shopping3.py

from gpanel import *
datafile = "shopping.dat"

def loadIntData(fileName):    
    fData = open(fileName, 'r')
    out = []
    for line in fData:
        li = [int(i) for i in line.split(',')]
        out.append(li)
    fData.close()
    return out

def showGrid():
    image("index.png", 0, 470)
    for i in range(nbAttr + 2):
        line(80 * i, 0, 80 * i, 560)
    for i in range(nbAttr + 2):
        line(0, 80 * i, 560, 80 * i)

X = loadIntData(datafile)
attributes = ["bread", "cheese", "apple", "banana", "pasta", "sugo"]
nbAttr = len(attributes) # Number of attributes
support = [[0 for i in range(nbAttr)] for k in range(nbAttr)]
confidence = [[0 for i in range(nbAttr)] for k in range(nbAttr)]

for sample in X:
    for premise in range(nbAttr):
        if sample[premise] == 0:  # no premise
            continue
        support[premise][premise] += 1   # total of premise item
        for conclusion in range(nbAttr):
            if premise == conclusion:  
                continue
            if sample[conclusion] == 1:
                # This person also bought the conclusion item
                support[premise][conclusion] += 1

# Calculate confidence
for premise in range(nbAttr):
    for conclusion in range(nbAttr):
        if premise == conclusion:
            continue
        v = support[premise][conclusion]
        t = support[premise][premise]
        if t != 0:
            confidence[premise][conclusion] = round(v / t, 2)

# show result
makeGPanel(Size(560, 560))
window(0, 560, 0, 560)
title("Number Of Purchases: " + str(len(X))) 
# show images
for i in range(nbAttr):
   image(attributes[i] + ".png", 0, 400 - 80  * i)
for k in range(nbAttr):
   image(attributes[k] + ".png", 80 * k + 80, 480)
showGrid()

for i in range(nbAttr):
    for k in range(nbAttr):
        if i != k:
            text(80 * (k + 1) + 30, 400 - 80  * i + 45, 
                str(support[i][k]))
            text(80 * (k + 1) + 30, 400 - 80  * i + 25, 
                str(confidence[i][k]))
        else:
            text(80 * (i + 1) + 30, 400 - 80  * i + 35, 
                "(" + str(support[i][i]) + ")")
keep()
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

Typisches Resultat:

shopping1


 

Sortierte Darstellung

 

Um mit einem Blick darzustellen, welche Affinitäten besonders hoch sind, können die Konklusionen in der Reihenfolge ihrer Konfidenzen angezeigt werden.

Programm: [►]

# Shopping4.py

from gpanel import *
datafile = "shopping.dat"

def loadIntData(fileName):    
    fData = open(fileName, 'r')
    out = []
    for line in fData:
        li = [int(i) for i in line.split(',')]
        out.append(li)
    fData.close()
    return out

def showGrid():
    for i in range(nbAttr + 1):
        line(80 * i, 0, 80 * i, 480)
    for i in range(nbAttr + 1):
        line(0, 80 * i, 480, 80 * i)
    lineWidth(4)
    line(80, 0, 80, 480)

X = loadIntData(datafile)
attributes = ["bread", "cheese", "apple", "banana", "pasta", "sugo"]
nbAttr = len(attributes) # Number of attributes
support = [[0 for i in range(nbAttr)] for k in range(nbAttr)]
confidence = [[0 for i in range(nbAttr)] for k in range(nbAttr)]

for sample in X:
    for premise in range(nbAttr):
        if sample[premise] == 0:  # no premise
            continue
        support[premise][premise] += 1   # total of premise item
        for conclusion in range(nbAttr):
            if premise == conclusion:  
                continue
            if sample[conclusion] == 1:
                # This person also bought the conclusion item
                support[premise][conclusion] += 1

# Calculate confidence
for premise in range(nbAttr):
    for conclusion in range(nbAttr):
        if premise == conclusion:
            continue
        v = support[premise][conclusion]
        t = support[premise][premise]
        if t != 0:
            confidence[premise][conclusion] = round(v / t, 2)

# show result
makeGPanel(Size(480, 480))
window(0, 480, 0, 480)
title("Affinity Order")
for i in range(nbAttr):
   image(attributes[i] + ".png", 0, 400 - 80  * i)

for i in range(nbAttr):
    di = {}  # dictionary: index:value
    for k in range(nbAttr):
        if k != i:
            di[k] = confidence[i][k] # insert element
    sortedConclusions = sorted(di, key = di.get) # sort by value
    sortedConclusions.reverse() # largest first
    for k in range(nbAttr - 1):        
        image(attributes[sortedConclusions[k]] + 
            ".png", 80 * k + 80, 400 - 80 * i)
        text(80 * (k + 1) + 5, 402 - 80  * i, \
            str(confidence[i][sortedConclusions[k]]))
showGrid()
keep() # for pygpanel
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

Typisches Resultat:

shopping2

Bemerkungen:
Beispielsweise ist sehr schön zu erkennen, dass

  • wer Brot kauft mit hoher Konfidenz auch Käse kauft, hingegen kaum Gemüse
  • auch Pasta und Sugo eine höhe Affinität haben
  • zwar gleich viele Personen Brot und Käse miteinander einkaufen, die Konfidenz aber, dass jemand der Brot auch Käse kauft, höher ist als die Konfidenz, dass jemand, der Käse kauft auch Brot kauft (da mehr Leute Käse als Brot kaufen)

 

Warenkorb interaktiv erstellen

 

Um Einkäufe nach eigenem Geschmack zu tätigen und einer Datesammlung zu speichern, kann wiederum eine anspruchsvolle Grafik angeboten werden, wo man die Produkte mit einem Mausklick in den Einkaufskorb legt und nach maximal 6 verschiedenen Produkten an der Kasse auscheckt.

Programm: [►]

# Purchase.py

#from gpanel import *
from pygpanel import *

dataFile = "samples.dat"

def onMousePressed(x, y):
    global nbItems, purchase
    item = (int)(x // 80)
    if item < n:
        if purchase[item] == 0:
            purchase[item] = 1
            title("Item " + attributes[item] + " added to basket. Total: " + 
                   str(purchase.count(1)))
        else:
            title("Item " + attributes[item] + " already in basket. Total: " + 
                  str(purchase.count(1)))
    else:
        nbItems = purchase.count(1)
        title ("Checked out with " + str(nbItems) + " items")
        if nbItems > 0:
            fOut = open(dataFile, "a")
            sample = ','.join(str(e) for e in purchase)
            fOut.write(sample + "\n")
            fOut.close()
            purchase = [0] * n

attributes = ["bread", "cheese", "apple", "banana", "pasta", "sugo"]
n = 6
purchase = [0] * n

makeGPanel(Size(560, 80), mousePressed = onMousePressed)
title("Click maximal 6 items and check out")
window(0, 560, 0, 560)
for i in range(n):
    image(attributes[i] + ".png", 80 * i, 0)
image("checkout.png", 480, 0)
keep()

Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

Tätige deine Einkäufe!

shopping3