Ana, Berta, Cilka in ostale znanke Programiranja 1 nakupujejo v neki spletni trgovini. Ana, recimo, je tam kupila stol, mizo, metlo in sesalec. Ema je kupila kroglo in frnikole. Celoten seznam (ehm, ne seznam, temveč nekaj drugega) je takšen:

stranke = {"Ana": {"stol", "miza", "metla", "sesalec"},
           "Berta": {"metla", "sesalec", "vedro"},
           "Cilka": {"flavta", "pesmarica"},
           "Dani": {"top", "krogla", "metla"},
           "Ema": {"krogla", "frnikole"},
           "Fanči": {"metla", "sesalec", "vedro"},
           "Greta": set(),
           "Helga": {"flavta"},
           "Iva": {"trobenta"}
          }

Napisali bomo nekaj funkcij, ki jih trgovina potrebuje za obdelavo svojih strank in priporočanje proizvodov.

Ogrevalna naloga

Podobnost med dvema strankama bi lahko "izmerili" tako, da bi izračunali število proizvodov, ki sta jih kupili obe. Tako bi bila podobnost med, recimo, Ano in Berto enaka 2, saj sta obe kupili metlo in sesalec. Vendar ta definicija ne bi bila dobra, ker so nekatere stranke kupile veliko proizvodov, torej je jasno, da bo tudi veliko skupnih. Po drugi strani pa sta dve stranki morda kupili le po en proizvod - vendar isti, torej sta si popolnoma podobni.

Podobnost zato definiramo kot število skupnih proizvodov deljeno s številom proizvodov, ki jih je kupila ena ali druga ali obe. Tako je podobnost med Ano in Berto enaka 2 / 5: obe sta kupili dva enaka proizvoda, vseh različnih proizvodov, ki jih je kupila ena ali druga ali obe, pa je pet (stol, miza, metla, sesalec in vedro).

Napiši funkcijo podobnost(stranke, stranka1, stranka2), ki kot argument dobi slovar, kakršen je gornji ter imeni dveh strank, ter vrne njuno podobnost.

  • Klic podobnost(stranke, "Ana", "Berta") mora vrniti 0.4,
  • klic podobnost(stranke, "Ana", "Cilka") vrne 0 in
  • klic podobnost(stranke, "Berta", "Fanči") vrne 1.

Poleg tega napiši funkcijo najpodobnejsa(stranke, stranka), ki prejme slovar strank in ime ene stranke, ter vrne ime najpodobnejše stranke.

Klic najpodobnejsa(stranke, "Ana") vrne "Fanči" ali "Berta".

Rešitev

Da brez preverlikih naporov napišemo prvo funkcijo, podobnost, moramo dovolj pozorno prebrati nalogo, da odkrijemo, da zahteva le, da delimo presek dveh množic z njuno unijo. Najlepše bo, da množici spravimo v dve spremenljivki, na primer nakup1 in nakup2, ter izračunamo in vrnemo, kar je treba.

def podobnost(stranke, stranka1, stranka2):
    nakup1, nakup2 = stranke[stranka1], stranke[stranka2]
    return len(nakup1 & nakup2) / len(nakup1 | nakup2)

Funkcija najpodobnejsa pa je klasika, ki jo obračamo že, odkar se poznamo.

def najpodobnejsa(stranke, stranka):
    najpodobnost = najpodobnejsa = None
    for stranka2 in stranke:
        if stranka2 != stranka and (
                najpodobnost == None or
                podobnost(stranke, stranka, stranka2) > najpodobnost):
            najpodobnost, najpodobnejsa = podobnost(stranke, stranka, stranka2), stranka2
    return najpodobnejsa

Ker to tolikokrat počnemo, obstajajo tudi različne bližnjice, o katerih se bomo učili prihodnje leto. Ena je, recimo

def najpodobnejsa(stranke, stranka):
    return max([x for x in stranke if x != stranka], key=lambda x: podobnost(stranke, stranka, x))

Obvezna naloga

Napiši funkcijo vsi_artikli(stranke), ki vrne množico vseh reči, ki jih je kdaj kdo kupil. Klic vsi_artikli(stranke) - pri čemer je stranke gornji slovar - tako vrne {"stol", "miza", "metla", "sesalec", "vedro", "flavta", "pesmarica", "top", "krogla", "frnikole", "trobenta"}.

Namig: slovar ima metodi items in values. Vsaj ena od njiju ti lahko pride prav pri tej in drugih funkcijah.

Napiši funkcijo kupljeno_poleg(stranke, stvar), ki vrne množico stvari, ki so jih kupile stranke, ki so kupile podano stvar. Klic kupljeno_poleg(stranke, "krogla") vrne {"top", "metla", "frnikole"}, saj so stranke, ki so kupile kroglo, kupovale tudi topove, metle in frnikole.

Napiši še funkcijo nasveti, ki vrne slovar, katerega ključi so vse stvari, ki jih je kdaj kdo kupil, vrendosti pa množice stvari, ki so jih kupili poleg njih. Tako nasveti(stranke) vrne

{'krogla': {'metla', 'top', 'frnikole'},
 'metla': {'sesalec', 'vedro', 'miza', 'krogla', 'stol', 'top'},
 'stol': {'metla', 'sesalec', 'miza'},
 'flavta': {'pesmarica'},
 'pesmarica': {'flavta'},
 'frnikole': {'krogla'},
 'sesalec': {'metla', 'vedro', 'stol', 'miza'},
 'vedro': {'metla', 'sesalec'},
 'top': {'krogla', 'metla'},
 'miza': {'metla', 'sesalec', 'stol'}}

Rešitev

Naloga vsi_artikli sprašuje po uniji vseh množic, ki so shranjene kot vrednosti v slovarju.

Gremo torej čez stranke.values() in vse vrednosti "priunijamo" v množico, ki jo na koncu vrnemo.

def vsi_artikli(stranke):
    vse = set()
    for nakup in stranke.values():
        vse |= nakup
    return vse

Funkcija kupljeno_poleg je na las podobna, le da dodaja le tiste množice, ki vsebujejo podano stvar. Poleg tega moramo na koncu iz množice odstraniti to reč. Za to bi lahko uporabili remove, vendar bi ta deloval le, če množica dejansko vsebuje to stvar. Če je ne (in je množica torej prazna), bi javil napako. Zato od množice raje odštejemo množico, ki vsebuje podano stvar. Odštevanje bo vedno delovalo - četudi množica te stvari morda sploh ne vsebuje.

def kupljeno_poleg(stranke, stvar):
    poleg = set()
    for nakup in stranke.values():
        if stvar in nakup:
            poleg |= nakup
    return poleg - {stvar}

Pazite: poleg - {stvar} in ne poleg - stvar. Od množice odštevamo množice, ne elementov.

Funkcija nasveti za vsako stvar pokliče funkcijo kupljeno_poleg in v novi slovar dodaja stvar kot ključ in rezultat funkcije kupljeno_poleg kot vrednost.

def nasveti(stranke):
    n = {}
    for stvar in vsi_artikli(stranke):
        poleg = kupljeno_poleg(stranke, stvar)
        if poleg:
            n[stvar] = poleg
    return n

Dodatna naloga

Napiši funkcijo najpodobnejse(stranke, stranka, k), ki dela podobno kot funkcija najpodobnejsa, le da vrne množico k najpodobnejših strank.

Napiši tudi funkcijo vse_najpodobnejse(stranka, stranka), ki dela podobno kot funkcija najpodobnejsa, le da vrača množico, v kateri so vse stranke, ki si delijo prvo mesto po podobnosti. Tako klic najpodobnejsa(stranke, "Ana") vrne {"Berta", "Fanči"}, saj sta obe najpodobnejši (to je, enako podobni) Ani.

Rešitev

Najpodobnejših k bomo poiskali tako, da bomo uredili stranke po padajoči podobnosti in vrnili prvih k. Po padajoči podobnosti jih lahko uredimo, recimo, tako da sestavimo seznam parov (-podobnost, stranka). Ko jih uredimo, bodo urejene po naraščajoči negativni podrobnosti (finta!). Potem iz tega slovarja poberemo le stranke.

Opis je malo zapleten, program pa bo tem, ki vas zanimajo dodatne naloge, najbrž jasen:

def najpodobnejse(stranke, stranka, k):
    s = [(-podobnost(stranke, stranka, stranka2), stranka2)
         for stranka2 in stranke
         if stranka2 != stranka]
    return {x[1] for x in sorted(s)[:k]}

Funkcijo vse_najpodobnejse bi lahko rešili tako, da bi poiskali največjo podobnost, nato pa šli ponovno čez stranke in nabrali te, ki imajo takšno podobnost.

Vendar je taka rešitev za nas prelahka. :) Napodobnejše bomo spravljali v množico najpodobnejsa. Kadar bomo naleteli na stranko, ki je podobnejša od najpodobnejše, bomo sestavili novo množico, najpodobnejsa = {stranka2}. Kadar pa bomo naleteli na stranko, ki si deli največjo podobnost, jo bomo dodali v množico, najpodobnejsa.add(stranka2).

def vse_najpodobnejse(stranke, stranka):
    najpodobnost = najpodobnejsa = None
    for stranka2 in stranke:
        if stranka2 == stranka:
            continue
        pod = podobnost(stranke, stranka, stranka2)
        if najpodobnost == None or pod > najpodobnost:
            najpodobnost = pod
            najpodobnejsa = {stranka2}
        elif pod == najpodobnost:
            najpodobnost = pod
            najpodobnejsa.add(stranka2)
    return najpodobnejsa

Krajše rešitve

Ker smo se kmalu po tej domači nalogi naučili uporabljati izpeljane sezname, množice in slovarje, rešimo naloge še na ta način, v enovrstičnih funkcijah.

def podobnost(stranke, stranka1, stranka2):
    return len(stranke[stranka1] & stranke[stranka2]) / len(stranke[stranka1] | stranke[stranka2])

def najpodobnejsa(stranke, stranka):
    return max([x for x in stranke if x != stranka], key=lambda x: len(stranke[stranka] & stranke[x]) / len(stranke[stranka] | stranke[x]))

def vsi_artikli(stranke):
    return reduce(set.union, stranke.values(), set())

def kupljeno_poleg(stranke, stvar):
    return reduce(set.union, (x for x in stranke.values() if stvar in x), set()) - {stvar}

def nasveti(stranke):
    return {stvar: kupljeno_poleg(stranke, stvar) for stvar in vsi_artikli(stranke) if kupljeno_poleg(stranke, stvar)}

Funkcijo najpodobnejse smo v bistvu že rešili v eni vrstici (zgoraj), le prvo vrstico je potrebno stlačiti v drugo.

Funkcije vse_najpodobnejse pa ne bomo tlačili v eno vrstico, saj bi bila predolga (brez kakega trika, ki pa se ga v tem trenutku ne spomnim).

Že funkcija nasveti ni prav lepa, ker za vsako stvar dvakrat pokliče kupljeno_poleg. Rešimo se lahko z generatorjem znotraj generatorja.

def nasveti(stranke):
    return {stvar: poleg for stvar, poleg in ((stvar, kupljeno_poleg(stranke, stvar)) for stvar in vsi_artikli(stranke)) if poleg}

Ampak to je grdo.

Zadnja sprememba: ponedeljek, 2. marec 2026, 21.28