Tag: Mondo codice 🖥

  • Che cosa sono i frattali

    Che cosa sono i frattali

    Significato frattale – La parola “frattale” deriva dal latino “fractus”, che significa “rotto” o “fratturato”. L’uso del termine “frattale” in matematica e nella scienza in generale è stato coniato nel XX secolo per descrivere oggetti geometrici o strutture che mostrano auto-somiglianza a diverse scale, cioè quando le parti di un oggetto sono simili all’oggetto stesso quando ingrandite o ridotte.

    La parola “frattale” è stata coniata da B. Mandelbrot nel 1975. Mandelbrot è stato uno dei pionieri nello studio e nella definizione formale dei frattali, e il suo lavoro ha avuto un profondo impatto in diversi campi, tra cui matematica, fisica, biologia e arte. L’uso del termine “frattale” si è diffuso ampiamente per descrivere una vasta gamma di oggetti e fenomeni naturali che mostrano questa auto-somiglianza a diverse scale.

    Par Evercat — Travail personnel, Domaine public, https://commons.wikimedia.org/w/index.php?curid=16069
    Par Evercat — Travail personnel, Domaine public, https://commons.wikimedia.org/w/index.php?curid=16069

    I frattali sono spesso associati a forme geometriche complesse, come l’insieme di Mandelbrot, ma possono anche apparire in contesti molto diversi, come nel caso delle strutture naturali menzionate in risposta alla domanda precedente.

    I frattali sono figure geometriche complesse che mostrano auto-somiglianza a diverse scale. Sono ampiamente presenti in natura e possono essere osservati in molti contesti. Ecco alcuni esempi di frattali presenti in natura:

    1. Foglie: Le foglie delle piante spesso mostrano una struttura frattale, con vene che si ramificano in modo simile a una versione più piccola della foglia stessa.
    2. Rami degli alberi: La struttura dei rami degli alberi è spesso frattale, con rami che si ramificano in modo ricorrente e simile alla struttura dell’albero nel suo complesso.
    3. Cavolfiori e broccolo: Questi ortaggi cruciferi hanno una struttura frattale evidente, con piccole protuberanze che si ripetono in scala più grande, creando una somiglianza a spirali e frattali.
    4. Fiumi e reti fluviali: La struttura dei sistemi fluviali, con il loro intrecciarsi di ruscelli, fiumi e affluenti, può mostrare un’auto-somiglianza frattale quando si osservano i dettagli a diverse scale.
    5. Nuvole: Le nuvole spesso mostrano una struttura frattale, con pattern che si ripetono a diverse scale.
    6. Montagne: L’aspetto generale di molte catene montuose può essere descritto utilizzando frattali, con picchi e valli che si ripetono in modo simile su diverse scale.
    7. Coste frastagliate: Le coste dei continenti spesso presentano un aspetto frattale, con insenature e promontori che si ripetono in modo simile a diverse scale.
    8. Strutture cristalline: Molti cristalli hanno una struttura interna che mostra un’auto-somiglianza frattale, con unità ripetitive che si combinano per formare il cristallo nel suo complesso.
    9. Fulmini: La forma e il percorso dei fulmini possono apparire frattali, con ramificazioni simili a quelle di un albero.
    10. Coloni di batteri: In biologia, i coloni di batteri possono formare strutture frattali complesse mentre crescono e si riproducono.

    Questi sono solo alcuni esempi di frattali che si trovano in natura. I frattali sono una parte affascinante della geometria e della matematica che possono essere osservati in molti fenomeni naturali, contribuendo a comprendere la bellezza e la complessità del mondo che ci circonda.

    Un frattale è formalmente una figura geometrica che si ripete in scala ridotta o ingrandita all’infinito. Si tratta di un oggetto matematico complesso che mostra la stessa struttura a qualsiasi livello di ingrandimento, e che presenta curiose proprietà tra cui quella di poter rappresentare aspetti della natura, della fisica o del mondo che ci circonda in una modalità che non sarebbe, altrimenti, possibile.

    I frattali sono noti per la loro bellezza e complessità, e si trovano spesso in natura, nell’arte e nella matematica. Esempi di frattali tratti dalla natura includono:

    1. Foglie d’albero: Le foglie delle piante spesso seguono un modello frattale, con venature che si ramificano in strutture simili a mini copie della foglia principale.
    2. Fiumi: I sistemi fluviali possono mostrare caratteristiche frattali nella loro struttura, con affluenti che si ramificano in modo simile alla forma del fiume principale.
    3. Nuvole: Le nuvole possono spesso apparire frattali, con forme complesse e ricorrenti.

    Frattale di Koch

    Una semplice formulazione matematica di un frattale è quella del “Frattale di Koch“. Questo frattale inizia con un segmento di lunghezza e successivamente suddivide il segmento in quattro parti uguali, sostituendo la parte centrale con un triangolo equilatero. Questo processo viene quindi ripetuto per ogni nuovo segmento generato, creando una struttura frattale.

    Visivamente avviene qualcosa del genere (scarica il video): mediante il richiamo ad una funzione ricorsiva si disegna la forma geometrica un pezzo per volta, considerando ogni volta micro segmenti di dimensione molto piccola i quali, una volta completato il disegno, assumono una forma compiuta. (qui sotto puoi vedere l’animazione GIF del frattale dell’esempio: se non vedi subito, aspetta un po’ o prova a ricaricare la pagina, alla peggio)

    Come realizzare un frattale di Koch in Python

    Il codice per realizzare frattali in Python è molto semplice, e sfrutta la libreria grafica Turtle.

    import turtle
    
    def koch_snowflake(t, order, size):
    if order == 0:
    t.forward(size)
    else:
    for angle in [60, -120, 60, 0]:
    koch_snowflake(t, order-1, size/3)
    t.left(angle)
    
    # Creare una finestra per il disegno
    wn = turtle.Screen()
    wn.bgcolor("white")
    
    # Creare una tartaruga
    fractal_turtle = turtle.Turtle()
    fractal_turtle.speed(0) # Velocità massima
    
    # Disegnare il Frattale di Koch
    for _ in range(3):
    koch_snowflake(fractal_turtle, 4, 300)
    fractal_turtle.right(120)
    
    # Chiudere la finestra con un clic
    wn.exitonclick()

    Esempi di frattali generati da Midjourney

    Se stavi cercando degli esempi di frattali, eccone alcuni che abbiamo generato con Midjourney.

    Foto di Merlin Lightpainting: https://www.pexels.com/it-it/foto/arte-luci-modello-astratto-14314637/

  • Architettura CISC vs RISC, cosa cambia

    Architettura CISC vs RISC, cosa cambia

    Differenze e Utilizzi dei Computer RISC e CISC

    Un processore RISC (Reduced Instruction Set Computer) è progettato con un’architettura semplificata che permette di eseguire le istruzioni in modo rapido ed efficiente. L’obiettivo principale del RISC è ottimizzare il software, riducendo il numero di istruzioni che il processore deve eseguire. Questo approccio contrasta con quello dei processori CISC (Complex Instruction Set Computer), che utilizzano un set di istruzioni più complesso e si concentrano sull’hardware per gestire un’ampia varietà di compiti.

    In breve. I processori RISC sono spesso utilizzati in applicazioni che richiedono elevate prestazioni di calcolo, come elaborazione dati intensiva, calcoli matematici complessi e grafica avanzata. Al contrario, i processori CISC sono più comuni in ambito aziendale, dove la flessibilità e la capacità di gestire compiti eterogenei sono fondamentali.

    La scelta tra RISC e CISC dipende dalle specifiche esigenze operative. I responsabili IT possono decidere di adottare uno o l’altro tipo di processore in base ai requisiti di performance, efficienza energetica e aggiornamenti tecnologici necessari per supportare al meglio le attività aziendali.

    Esempi Pratici

    RISC

    1. ARM Cortex-A Series: Utilizzato in smartphone, tablet e dispositivi embedded grazie alla sua efficienza energetica e prestazioni elevate.
    2. MIPS: Spesso impiegato in dispositivi di rete, router e console di gioco.
    3. PowerPC: Utilizzato in passato nei computer Apple e ora in alcune console di gioco e server.

    CISC

    1. Intel Core Series: Presente in molti computer desktop e laptop, famoso per la sua potenza e flessibilità.
    2. AMD Ryzen: Utilizzato in PC da gioco, workstation e server per le sue elevate prestazioni e capacità multitasking.
    3. Intel Xeon: Progettato per server e workstation, noto per la sua affidabilità e capacità di gestire carichi di lavoro complessi.

    Ibridi (Architettura che combina elementi di RISC e CISC)

    1. Intel Atom: Utilizzato in dispositivi a basso consumo energetico come netbook, tablet e dispositivi IoT, combina un’architettura semplificata con una certa complessità per gestire compiti più vari.
    2. ARM big.LITTLE: Utilizzato in dispositivi mobili, combina core ad alta efficienza energetica (RISC) con core ad alte prestazioni per migliorare l’efficienza complessiva del sistema.
    3. IBM POWER: Utilizzato in server di fascia alta, combina l’architettura RISC con alcune caratteristiche avanzate tipiche dei CISC per ottimizzare le prestazioni e l’efficienza.

    Approfondimento: CISC vs RISC

    La differenza tra architetture CISC e RISC è stata molto diffusa e ha influenzato lo sviluppo delle CPU nel corso degli anni. Di base non è semplice ricordare la differenza tra i due, anche perchè molte CPU moderne si basano su caratteristiche mutuate da entrambe le parti. La parola chiave Complex associata a CISC aiuta a ricordare che si tratta di un processore che gestisce molte operazioni dotate di set di istruzioni complessi, in cui sono tendenzialmente presenti pochi registri ed è richiesto un carico di lavoro superiore. Nelle architetture RISC la parola chiave è Reduced, nel secondo che si sono poche istruzioni disponibili, il set istruzioni è uniforme, sono presenti più registri del caso precedente e sono richiesti pochi cicli di CPU.

    Le architetture CISC sono state predominanti nei primi computer e sono state sviluppate per fornire istruzioni complesse e versatili in grado di eseguire molte operazioni in un’unica istruzione. Processori come l’Intel x86 sono un esempio di architettura CISC che ha raggiunto una vasta diffusione in computer personali e server.

    D’altra parte, le architetture RISC sono emerse come risposta alla necessità di prestazioni più elevate e costi inferiori. Processori come ARM e MIPS sono esempi di architetture RISC che hanno guadagnato popolarità, specialmente nei dispositivi embedded, negli smartphone e in altri dispositivi che richiedono efficienza energetica e prestazioni ottimizzate.

    Tuttavia, nel corso del tempo, la distinzione tra CISC e RISC si è in parte sfumata. Alcuni processori moderni, come quelli prodotti da Intel e AMD, adottano tecniche di progettazione che combinano caratteristiche delle due architetture. Ad esempio, possono utilizzare tecniche RISC all’interno dei loro core di esecuzione per aumentare l’efficienza senza abbandonare completamente le istruzioni CISC che sono ancora compatibili con le architetture precedenti.

    Quindi, sebbene la distinzione tra CISC e RISC sia stata molto rilevante nello sviluppo delle CPU, negli anni recenti le linee si sono sfumate, e le moderne architetture spesso integrano aspetti di entrambe le filosofie di progettazione per ottenere un equilibrio ottimale tra prestazioni e efficienza.

    In sintesi:

    1. le architetture CISC tendono a offrire istruzioni più complesse e variegate;
    2. le architetture RISC si concentrano su istruzioni più semplici ed efficienti.

    Entrambe le architetture hanno vantaggi e svantaggi, e l’efficacia dipende spesso dall’implementazione specifica e dall’uso previsto della CPU.

    Le architetture CISC (Complex Instruction Set Computer) e RISC (Reduced Instruction Set Computer) differiscono in modo significativo nel modo in cui affrontano l’esecuzione delle istruzioni e l’organizzazione interna delle CPU.

    Architettura CISC

    • Set di istruzioni complesso: Le CPU CISC hanno un set di istruzioni più ampio e complesso, che comprende istruzioni molto specifiche e complesse per eseguire diverse operazioni in una singola istruzione. Queste istruzioni possono coinvolgere molte fasi e richiedere più cicli di clock per essere eseguite.
    • Istruzioni di lunghezza variabile: Le istruzioni CISC possono avere lunghezze variabili, con alcune istruzioni che richiedono più byte di codice per essere rappresentate rispetto ad altre. Questa flessibilità può portare a una maggiore complessità nella decodifica delle istruzioni.
    • Utilizzo di microcodice: Le operazioni complesse vengono spesso implementate attraverso microcodice, sequenze di istruzioni di livello inferiore che gestiscono l’esecuzione di istruzioni complesse.
    • Accesso diretto alla memoria: Le istruzioni CISC spesso supportano modalità di indirizzamento complesse e flessibili che permettono l’accesso diretto alla memoria.

    Architettura RISC

    • Set di istruzioni ridotto: Le CPU RISC hanno un set di istruzioni più ridotto e semplificato, che si concentra su istruzioni elementari ed efficienti. Queste istruzioni sono progettate per essere eseguite in un singolo ciclo di clock, rendendo l’esecuzione più veloce e prevedibile.
    • Istruzioni di lunghezza fissa: Le istruzioni RISC hanno spesso lunghezze fisse, semplificando la fase di decodifica e consentendo una pipeline più efficiente.
    • Pipeline più semplice: Le architetture RISC solitamente presentano pipeline più semplici e più lunghe, che possono essere più efficienti nel processo di esecuzione delle istruzioni.
    • Accesso alla memoria più limitato: Le istruzioni RISC tendono ad avere un accesso più limitato alla memoria, con operazioni di caricamento e memorizzazione più semplici e specifiche.

    Le architetture CISC (Complex Instruction Set Computer) e RISC (Reduced Instruction Set Computer) differiscono principalmente per la filosofia progettuale e l’approccio nell’esecuzione delle istruzioni.

    CISC (Complex Instruction Set Computer):

    • Complessità delle istruzioni: Le architetture CISC hanno un set di istruzioni complesso e variegato. Ogni istruzione può compiere operazioni complesse e può richiedere diversi cicli di clock per essere eseguita.
    • Dimensione variabile delle istruzioni: Le istruzioni CISC possono avere dimensioni variabili. Alcuni comandi possono richiedere un numero significativo di bit per essere codificati.
    • Utilizzo di microcodice: Le istruzioni complesse vengono spesso implementate attraverso microprogrammi, che sono sequenze di istruzioni di livello inferiore che gestiscono l’esecuzione delle istruzioni complesse.
    • Accesso alla memoria più flessibile: Le istruzioni CISC possono accedere direttamente alla memoria.

    RISC (Reduced Instruction Set Computer):

    • Set di istruzioni ridotto e uniforme: Le architetture RISC hanno un set di istruzioni limitato e uniforme, generalmente composte da istruzioni semplici ed elementari.
    • Istruzioni di lunghezza fissa: Le istruzioni RISC sono spesso di lunghezza fissa, rendendo l’esecuzione più veloce e prevedibile.
    • Pipeline più efficienti: Le architetture RISC tendono ad avere pipeline più semplici e più efficienti, consentendo l’esecuzione di istruzioni in modo più rapido.
    • Esecuzione efficiente delle istruzioni: Gli approcci RISC mirano a eseguire le istruzioni più velocemente, favorendo la velocità di clock e l’efficienza.

    In definitiva mentre le architetture CISC mirano a fornire istruzioni complesse e versatili per ridurre il numero di istruzioni necessarie per completare una determinata operazione, le architetture RISC cercano di ottimizzare l’efficienza eseguendo un set più piccolo di istruzioni in modo più rapido e diretto. Entrambe le architetture hanno vantaggi e svantaggi, e l’efficacia dipende spesso dall’implementazione specifica e dall’uso previsto della CPU.

    Assembler CISC vs CISC

    A livello ipotetico, per evidenziare la differenza tra set di istruzioni Complex e Reduced, potremmo fare una somma come segue in termini di macchina RISC:

    # Architettura RISC (esempio MIPS)
    # Somma dei registri $s0 e $s1, risultato in $s2
    add $s2, $s0, $s1

    mentre su CISC avremmo avuto:

    # Architettura CISC (esempio x86)
    # Somma dei registri eax ed ebx, risultato in eax
    
    add eax, ebx
  • Guida totale a igraph per Python

    Guida totale a igraph per Python

    Quando si parla di igraph Python, si entra nel cuore dell’analisi dei network complessi. Se il tuo obiettivo è studiare relazioni, flussi, strutture e dinamiche in sistemi complessi, igraph è una libreria Python di riferimento che ti offre strumenti potenti, efficienti e altamente performanti. Non lasciarti ingannare dal prefisso i: sotto il cofano, igraph è scritto in C, garantendo velocità notevoli, essenziali quando si lavora con grafi di grandi dimensioni.

    Questo articolo esplorerà come iniziare a utilizzare igraph in Python, dalla creazione dei grafi all’analisi delle loro proprietà e alla loro visualizzazione, con esempi pratici e gradualmente più complessi.

    1. Installazione: Il Primo Passo

    Per iniziare a lavorare con igraph, devi prima installarlo. Il modo più semplice è tramite pip:

    pip install python-igraph
    

    Potrebbe essere necessario installare anche matplotlib se intendi visualizzare i grafi direttamente all’interno di Python, dato che igraph si integra bene con esso per il plotting.

    pip install matplotlib
    

    Se usate Python 3, si può scrivere equivalentemente:

    pip3 install python-igraph

    pip3 install matplotlib

    2. Concetti Fondamentali di igraph

    Prima di immergerci nel codice, è utile ripassare alcuni concetti chiave che igraph modella:

    • Grafo (Graph): La struttura dati principale, composta da vertici (nodi) e archi (collegamenti). A sua volta il grafo può essere:
      • Diretto (Directed): I archi hanno una direzione (A -> B). Utile per relazioni di precedenza, dipendenza.
      • Non Diretto (Undirected): I archi non hanno una direzione (A – B). Utile per amicizie, connessioni fisiche.
      • Misto (con archi sia orientati che non orientati)
    • Nodo / Vertice (Vertex): Gli elementi individuali nel grafo. In igraph, sono indicizzati a partire da 0.
    • Collegamento / Arco (Edge): Le connessioni tra i vertici.
    • Attributi (Attributes): Proprietà personalizzate che puoi assegnare a vertici e lati (es. nome di una persona per un vertice, peso per un lato, colore, etc.).

    3. Creazione di Grafi

    igraph offre diversi modi per costruire un grafo, a seconda dei dati che hai a disposizione.

    a) Grafo Vuoto

    Puoi iniziare con un grafo vuoto e aggiungere vertici e lati in seguito.

    b) Aggiunta di Vertici e Lati

    Dopo aver creato un grafo, potrai aggiungere elementi. Questo esempio crea il seguente grafo in formato JPG e PDF.

    import igraph as ig
    import matplotlib.pyplot as plt
    
    # Costruisci un grafo con 5 vertici (persone)
    n_vertices = 5
    # Definiamo i lati (le relazioni tra le persone)
    # Es: (0, 1) significa che la persona 0 è connessa alla persona 1
    edges = [(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4), (3, 4)]
    g = ig.Graph(n_vertices, edges)
    
    # Imposta attributi per il grafo, i nodi (persone) e i lati (relazioni)
    g["titolo"] = "Piccola Rete Sociale Italiana" # Titolo del grafo
    g.vs["nome"] = ["Mario Rossi", "Anna Ferrari", "Luca Bianchi", "Giulia Verdi", "Paolo Russo"] # Nomi delle persone
    g.vs["genere"] = ["M", "F", "M", "F", "M"] # Genere M (Maschio) o F (Femmina)
    g.es["sposati"] = [False, False, False, False, False, False, False, True] # Se la relazione è di matrimonio (tra 3 e 4)
    
    # Imposta attributi individuali per specifiche persone o relazioni
    g.vs[1]["nome"] = "Anna Rossi" # Anna Ferrari ha cambiato cognome, è ora Anna Rossi
    g.es[0]["sposati"] = True # La relazione tra Mario (0) e Anna (1) è un matrimonio
    
    # Plot in matplotlib
    # Nota: gli attributi possono essere impostati globalmente (es. vertex_size),
    # o impostati individualmente usando array (es. vertex_color)
    fig, ax = plt.subplots(figsize=(7,7)) # Aumentiamo leggermente la dimensione per una migliore leggibilità
    
    ig.plot(
    g,
    target=ax,
    layout="circle", # Dispone i nodi in un layout circolare
    vertex_size=30, # Dimensione dei nodi
    # Colora i nodi: azzurro ("steelblue") per maschi, salmone ("salmon") per femmine
    vertex_color=["steelblue" if genere == "M" else "salmon" for genere in g.vs["genere"]],
    vertex_frame_width=4.0, # Larghezza del bordo del nodo
    vertex_frame_color="white", # Colore del bordo del nodo
    vertex_label=g.vs["nome"], # Etichette dei nodi (i nomi delle persone)
    vertex_label_size=8.0, # Dimensione del font delle etichette
    # Larghezza dei lati: più spessa (2) se 'sposati' è True, altrimenti normale (1)
    edge_width=[3 if sposati else 1 for sposati in g.es["sposati"]], # Rendo la linea del matrimonio più evidente
    # Colore dei lati: viola scuro per relazioni di matrimonio, grigio per le altre
    edge_color=["#8A2BE2" if sposati else "#AAA" for sposati in g.es["sposati"]], # Viola per il matrimonio
    edge_curved=0.2 # Rendi i lati leggermente curvi per maggiore chiarezza
    )
    
    plt.title(g["titolo"], fontsize=12) # Aggiungi il titolo del grafo
    plt.show()
    
    # Salva il grafo come file immagine
    fig.savefig('rete_sociale_italiana.png')
    fig.savefig('rete_sociale_italiana.jpg')
    fig.savefig('rete_sociale_italiana.pdf')
    
    print("\nGrafo della rete sociale italiana generato e salvato.")
    
    # Esporta e importa un grafo come file GML (Graph Modeling Language)
    # GML è un formato testuale comune per la descrizione dei grafi
    g.save("rete_sociale_italiana.gml")
    g_caricato = ig.load("rete_sociale_italiana.gml")
    
    print(f"\nGrafo salvato come 'rete_sociale_italiana.gml' e ricaricato. Vertici: {g_caricato.vcount()}, Lati: {g_caricato.ecount()}")

    c) Da Lista di Lati (Graph.TupleList())

    Questo è un modo molto comune per creare grafi quando hai una lista di connessioni. Puoi anche includere attributi per i lati.

    import igraph as ig
    import matplotlib.pyplot as plt
    # Definiamo le relazioni direttamente con i nomi delle persone,
    # e aggiungiamo un attributo ‘tipo_relazione’ per ogni lato.
    # Ogni tupla è (sorgente, destinazione, tipo_relazione)
    relazioni_con_nomi = [
    (“Mario Rossi”, “Anna Rossi”, “coniugi”),
    (“Mario Rossi”, “Luca Bianchi”, “amici”),
    (“Mario Rossi”, “Giulia Verdi”, “colleghi”),
    (“Mario Rossi”, “Paolo Russo”, “vicini”),
    (“Anna Rossi”, “Luca Bianchi”, “amici”),
    (“Anna Rossi”, “Giulia Verdi”, “colleghi”),
    (“Anna Rossi”, “Paolo Russo”, “conoscenti”),
    (“Giulia Verdi”, “Paolo Russo”, “sposati”) # L’ultimo matrimonio è tra Giulia e Paolo
    ]
    # Creiamo il grafo utilizzando Graph.TupleList()
    # “vertex_name_attr=’nome’” indica che i nomi dei vertici saranno memorizzati nell’attributo ‘nome’
    # “edge_attrs=[‘tipo_relazione’]” indica che l’ultimo elemento di ogni tupla sarà l’attributo ‘tipo_relazione’ del lato
    g = ig.Graph.TupleList(relazioni_con_nomi, directed=False,
    vertex_name_attr=’nome’, edge_attrs=[‘tipo_relazione’])
    # Impostiamo il titolo del grafo
    g[“titolo”] = “Piccola Rete Sociale Italiana (da TupleList)”
    # Aggiungiamo attributi ai vertici creati automaticamente
    # igraph ha già assegnato ID numerici e memorizzato i nomi
    # Recuperiamo i nomi per assegnare il genere
    nomi_vertici = g.vs[“nome”]
    generi = []
    for nome in nomi_vertici:
    if”Mario”innomeor”Luca”innomeor”Paolo”innome:
    generi.append(“M”)
    else:
    generi.append(“F”)
    g.vs[“genere”] = generi
    # Aggiorniamo l’attributo ‘sposati’ per i lati
    # Utilizziamo l’attributo ‘tipo_relazione’ appena creato
    # Rimuoviamo il vecchio attributo ‘sposati’ per ricrearlo in base al nuovo criterio
    if “sposati” in g.es.attributes():
    delg.es[“sposati”]
    g.es[“sposati”] = [True if tipo == “sposati” or tipo == “coniugi” else False for tipo in g.es[“tipo_relazione”]]
    # Plot in matplotlib
    fig, ax = plt.subplots(figsize=(7,7))
    ig.plot(
    g,
    target=ax,
    layout=”circle”, # Dispone i nodi in un layout circolare
    vertex_size=30, # Dimensione dei nodi
    # Colora i nodi: azzurro (“steelblue”) per maschi, salmone (“salmon”) per femmine
    vertex_color=[“steelblue”ifgenere==”M”else”salmon”forgenereing.vs[“genere”]],
    vertex_frame_width=4.0, # Larghezza del bordo del nodo
    vertex_frame_color=”white”, # Colore del bordo del nodo
    vertex_label=g.vs[“nome”], # Etichette dei nodi (i nomi delle persone)
    vertex_label_size=8.0, # Dimensione del font delle etichette
    # Larghezza dei lati: più spessa (3) se ‘sposati’ è True, altrimenti normale (1)
    edge_width=[3ifsposatielse1forsposatiing.es[“sposati”]],
    # Colore dei lati: viola scuro per relazioni di matrimonio, grigio per le altre
    edge_color=[“#8A2BE2″ifsposatielse”#AAA”forsposatiing.es[“sposati”]],
    edge_curved=0.2# Rendi i lati leggermente curvi per maggiore chiarezza
    )
    plt.title(g[“titolo”], fontsize=12) # Aggiungi il titolo del grafo
    plt.show()
    # Salva il grafo come file immagine
    fig.savefig(‘rete_sociale_italiana_tuplelist.png’)
    fig.savefig(‘rete_sociale_italiana_tuplelist.jpg’)
    fig.savefig(‘rete_sociale_italiana_tuplelist.pdf’)
    print(“\nGrafo della rete sociale italiana (creato con TupleList) generato e salvato.”)
    # Esporta e importa un grafo come file GML (Graph Modeling Language)
    g.save(“rete_sociale_italiana_tuplelist.gml”)
    g_caricato = ig.load(“rete_sociale_italiana_tuplelist.gml”)
    print(f”\nGrafo salvato come ‘rete_sociale_italiana_tuplelist.gml’ e ricaricato. Vertici: {g_caricato.vcount()}, Lati: {g_caricato.ecount()}”)

    d) Da Matrice di Adiacenza (Graph.Adjacency())

    Certamente! Procediamo con la spiegazione del metodo Graph.Adjacency() di igraph, iniziando con una breve introduzione su cosa sia una matrice di adiacenza.

    Cos’è una Matrice di Adiacenza?

    Una matrice di adiacenza è una rappresentazione matematica di un grafo. È una matrice quadrata (ovvero, ha lo stesso numero di righe e colonne) dove sia le righe che le colonne corrispondono ai vertici (nodi) del grafo.

    • Se il grafo ha vertici, la matrice di adiacenza sarà una matrice .
    • L’elemento (situato all’intersezione della riga e della colonna ) della matrice indica se esiste un lato (o arco) che connette il vertice al vertice .

    Per i grafi non diretti:

    • se esiste un lato tra il vertice e il vertice .
    • se non esiste un lato tra il vertice e il vertice .
    • La matrice è simmetrica (), poiché se c’è un lato , c’è anche un lato .

    Per i grafi diretti:

    • se esiste un lato che va dal vertice al vertice .
    • se non esiste un lato in quella direzione.
    • La matrice non è necessariamente simmetrica.

    Matrici Pesate:

    • Per i grafi pesati, può contenere il peso del lato anziché semplicemente o . Un valore di indica assenza di lato.

    Creazione di un Grafo da Matrice di Adiacenza con Graph.Adjacency()

    Il metodo Graph.Adjacency() di igraph ti permette di costruire un grafo a partire da una matrice di adiacenza fornita come una lista di liste (o un array NumPy).

    Sintassi:

    Python

    Graph.Adjacency(adj_matrix, mode="undirected" | "directed" | "max" | "min" | "plus" | "lower" | "upper", attr=None, mapping=True)
    

    Parametri chiave:

    • adj_matrix: La matrice di adiacenza, tipicamente una lista di liste di numeri interi (0 o 1 per grafi non pesati, pesi per grafi pesati).
    • mode: Questo parametro è fondamentale per specificare come igraph deve interpretare la matrice, specialmente per i grafi non diretti o quando la matrice non è perfettamente simmetrica:
      • "undirected" (default): Crea un grafo non diretto. Se la matrice non è simmetrica, igraph aggiungerà un lato se o è diverso da zero.
      • "directed": Crea un grafo diretto. Un lato esiste se è diverso da zero.
      • "max", "min", "plus": Utilizzati per grafi non diretti pesati. Determinano come combinare i pesi e se la matrice non è simmetrica (es. max prende il massimo tra i due).
      • "lower", "upper": Utilizzati per grafi non diretti. Usano solo la parte inferiore o superiore della matrice (utile se la matrice è già stata pensata come simmetrica e si vuole risparmiare sul tempo di elaborazione).
    • attr (opzionale): Se la matrice contiene pesi, questo parametro specifica il nome dell’attributo del lato in cui verranno memorizzati i pesi (es. attr="weight").
    • mapping: Se True (default), igraph crea vertici con ID da a . Se False, usa direttamente i valori non zero come ID, il che è raro e va usato con cautela.

    Esempio: Creazione di un Grafo Sociale da Matrice di Adiacenza

    Per collegarci al tuo esempio precedente, useremo una matrice di adiacenza per rappresentare le stesse connessioni, ma senza nomi diretti nella matrice stessa (i nomi verranno aggiunti come attributi in seguito).

    Matrice di Adiacenza per il tuo Grafo (5 vertici):

    Consideriamo i tuoi vertici come:

    • 0: Mario Rossi
    • 1: Anna Rossi
    • 2: Luca Bianchi
    • 3: Giulia Verdi
    • 4: Paolo Russo

    Le tue connessioni originali erano: [(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4), (3, 4)]

    Quindi, la matrice di adiacenza (non diretta, simmetrica) sarà:

        0 1 2 3 4
    0: [0,1,1,1,1]  (0 è connesso con 1,2,3,4)
    1: [1,0,1,1,1]  (1 è connesso con 0,2,3,4)
    2: [1,1,0,0,0]  (2 è connesso con 0,1)
    3: [1,1,0,0,1]  (3 è connesso con 0,1,4)
    4: [1,1,0,1,0]  (4 è connesso con 0,1,3)
    

    Nota: ho basato la matrice sugli edges forniti nel tuo primo esempio.

    [(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4), (3, 4)]

    Convertiamo questa logica in una matrice:

    • (0,1): 0-1, 1-0
    • (0,2): 0-2, 2-0
    • (0,3): 0-3, 3-0
    • (0,4): 0-4, 4-0
    • (1,2): 1-2, 2-1
    • (1,3): 1-3, 3-1
    • (1,4): 1-4, 4-1
    • (3,4): 3-4, 4-3

    Matrice corretta per i tuoi edges:

    Indices: 0 1 2 3 4

    —————-

    0: [0, 1, 1, 1, 1]

    1: [1, 0, 1, 1, 1]

    2: [1, 1, 0, 0, 0]

    3: [1, 1, 0, 0, 1]

    4: [1, 1, 0, 1, 0]

    Python

    import igraph as ig
    import matplotlib.pyplot as plt
    
    # Definizione della matrice di adiacenza
    # I valori '1' indicano una connessione, '0' l'assenza
    adj_matrix = [
        [0, 1, 1, 1, 1], # Connessioni di Mario (0)
        [1, 0, 1, 1, 1], # Connessioni di Anna (1)
        [1, 1, 0, 0, 0], # Connessioni di Luca (2)
        [1, 1, 0, 0, 1], # Connessioni di Giulia (3)
        [1, 1, 0, 1, 0]  # Connessioni di Paolo (4)
    ]
    
    # Creiamo il grafo dalla matrice di adiacenza
    # Usiamo mode="undirected" perché le relazioni sociali sono tipicamente bidirezionali
    g = ig.Graph.Adjacency(adj_matrix, mode="undirected")
    
    # Impostiamo gli attributi dei vertici (persone)
    g["titolo"] = "Piccola Rete Sociale Italiana (da Matrice di Adiacenza)"
    g.vs["nome"] = ["Mario Rossi", "Anna Ferrari", "Luca Bianchi", "Giulia Verdi", "Paolo Russo"]
    g.vs["genere"] = ["M", "F", "M", "F", "M"]
    
    # Impostiamo gli attributi dei lati (relazioni)
    # Dobbiamo sapere quali lati sono "sposati" in base alla loro posizione
    # Ricordiamo che igraph assegna ID agli edge nell'ordine in cui vengono "trovati"
    # per prima il lato (0,1), poi (0,2), (0,3), (0,4), (1,2), (1,3), (1,4), (3,4)
    # Nel tuo esempio originale, il matrimonio era (3,4) e (0,1).
    # Qui le dobbiamo gestire manualmente, dato che la matrice non ha questo attributo
    married_edges = []
    for edge in g.es:
        # Controlliamo se la coppia di nomi corrisponde a quelle sposate
        source_name = g.vs[edge.source]["nome"]
        target_name = g.vs[edge.target]["nome"]
    
        # Anna e Mario (0,1)
        if (source_name == "Mario Rossi" and target_name == "Anna Ferrari") or \
           (source_name == "Anna Ferrari" and target_name == "Mario Rossi"):
            married_edges.append(True)
        # Giulia e Paolo (3,4)
        elif (source_name == "Giulia Verdi" and target_name == "Paolo Russo") or \
             (source_name == "Paolo Russo" and target_name == "Giulia Verdi"):
            married_edges.append(True)
        else:
            married_edges.append(False)
    
    g.es["sposati"] = married_edges
    
    
    # Plot in matplotlib
    fig, ax = plt.subplots(figsize=(7,7))
    
    ig.plot(
        g,
        target=ax,
        layout="circle",
        vertex_size=30,
        vertex_color=["steelblue" if genere == "M" else "salmon" for genere in g.vs["genere"]],
        vertex_frame_width=4.0,
        vertex_frame_color="white",
        vertex_label=g.vs["nome"],
        vertex_label_size=8.0,
        edge_width=[3 if sposati else 1 for sposati in g.es["sposati"]],
        edge_color=["#8A2BE2" if sposati else "#AAA" for sposati in g.es["sposati"]],
        edge_curved=0.2
    )
    
    plt.title(g["titolo"], fontsize=12)
    plt.show()
    
    # Salva il grafo come file immagine
    fig.savefig('rete_sociale_italiana_adj_matrix.png')
    print("\nGrafo della rete sociale italiana (da Matrice di Adiacenza) generato e salvato.")
    
    # Esporta e importa un grafo come file GML
    g.save("rete_sociale_italiana_adj_matrix.gml")
    g_caricato = ig.load("rete_sociale_italiana_adj_matrix.gml")
    
    print(f"\nGrafo salvato come 'rete_sociale_italiana_adj_matrix.gml' e ricaricato. Vertici: {g_caricato.vcount()}, Lati: {g_caricato.ecount()}")
    

    Spiegazione delle Modifiche e Considerazioni

    1. Definizione della Matrice di Adiacenza:

      • Abbiamo creato una lista di liste adj_matrix che rappresenta esplicitamente le connessioni tra i 5 vertici. L’indice di riga/colonna corrisponde all’ID del vertice (da 0 a 4).
      • I valori 1 indicano la presenza di un lato, 0 l’assenza. Data la natura non diretta del grafo, la matrice è simmetrica.
    2. g = ig.Graph.Adjacency(adj_matrix, mode="undirected"):

      • Questo è il cuore della creazione. igraph legge la matrice e crea i vertici (ID da 0 a 4) e i lati corrispondenti.
      • Il mode="undirected" è cruciale per indicare che stiamo costruendo un grafo non diretto.
    3. Attribuzione degli Attributi:

      • A differenza di TupleList, la matrice di adiacenza di base non veicola direttamente nomi o attributi dei lati. Quindi, dobbiamo assegnare g.vs["nome"] e g.vs["genere"] separatamente, una volta che il grafo e i suoi vertici numerici sono stati creati.
      • Per l’attributo g.es["sposati"], è necessario iterare sui lati esistenti del grafo (che igraph ha creato dalla matrice) e determinare quali di essi corrispondono alle relazioni di matrimonio, basandosi sui nomi dei vertici coinvolti. Questo è un po’ più laborioso qui perché la matrice di adiacenza “pura” non ha un concetto di “tipo di relazione” o “peso” a meno che non si usino valori diversi da 0 e 1 e l’argomento attr.

    Vantaggi di Graph.Adjacency():

    • Input diretto: Ideale se hai già i tuoi dati in formato matrice di adiacenza (es. da calcoli scientifici o da altri sistemi).
    • Controllo esplicito: Puoi vedere esattamente quali connessioni stai definendo a livello di riga/colonna.

    Svantaggi (rispetto a TupleList per alcuni casi):

    • Meno intuitivo per grafi sparsi: Per grafi con pochi lati rispetto al numero di vertici (grafi sparsi), la maggior parte della matrice sarà piena di zeri, rendendola inefficiente in termini di spazio e meno immediata da leggere per gli esseri umani.
    • Gestione attributi: I nomi dei vertici e gli attributi dei lati (come “sposati” o “tipo_relazione”) non sono inclusi direttamente nella matrice (a meno che non sia una matrice pesata e tu usi l’argomento attr per un singolo attributo numerico). Richiedono un passaggio successivo per l’assegnazione.

    Questo esempio mostra chiaramente come puoi costruire un grafo igraph partendo dalla sua rappresentazione matriciale, e poi arricchirlo con gli attributi necessari per l’analisi e la visualizzazione.

  • Che significa implementare?

    Che significa implementare?

    Implementare è un termine che significa mettere in pratica o realizzare un piano, un progetto o un’idea. Quando si parla di implementare qualcosa, si sta traducendo un concetto teorico o un piano in azioni concrete o in un sistema funzionante. Questo termine è spesso utilizzato in contesti tecnologici, informatici e aziendali, ma può essere applicato anche in altri ambiti.

    Ad esempio, nell’ambito della programmazione informatica, implementare un software significa scrivere il codice e sviluppare il programma in modo che esegua le funzioni desiderate. Nell’ambito aziendale, implementare una strategia significa mettere in atto le azioni necessarie per realizzarla con successo. In generale, implementare implica prendere misure concrete per trasformare un’idea o un piano in realtà.

    Ecco un esempio di come si potrebbe implementare un software per calcolare il perimetro di un rettangolo in un linguaggio di programmazione come Python:

    # Definire una funzione per calcolare il perimetro di un rettangolo
    def calcola_perimetro_rettangolo(base, altezza):
    perimetro = 2 * (base + altezza)
    return perimetro
    
    # Chiedere all'utente di inserire le dimensioni del rettangolo
    base = float(input("Inserisci la lunghezza della base del rettangolo: "))
    altezza = float(input("Inserisci l'altezza del rettangolo: "))
    
    # Chiamare la funzione per calcolare il perimetro
    perimetro = calcola_perimetro_rettangolo(base, altezza)
    
    # Stampare il risultato
    print("Il perimetro del rettangolo è:", perimetro)

    In questo esempio, abbiamo definito una funzione calcola_perimetro_rettangolo che prende due argomenti (la base e l’altezza del rettangolo) e restituisce il perimetro del rettangolo. Successivamente, chiediamo all’utente di inserire le dimensioni del rettangolo, chiamiamo la funzione per calcolare il perimetro e quindi stampiamo il risultato.

    Questo è solo un esempio semplice di implementazione di un software per calcolare il perimetro di un rettangolo. In un’applicazione più complessa, potrebbero essere necessari ulteriori controlli e funzionalità, ma questo dovrebbe darti un’idea di come l’implementazione di un software possa tradurre un’operazione matematica in un’applicazione pratica. Foto di Gerd Altmann da Pixabay

  • Java: costanti, variabili, cicli [guida]

    Java: costanti, variabili, cicli [guida]

    Java è un linguaggio di programmazione oggettotale che consente di creare applicazioni robuste e scalabili per una vasta gamma di piattaforme, dalle applicazioni desktop ai sistemi operativi embedded. Per iniziare a programmare in Java, è fondamentale comprendere i concetti di base del linguaggio, tra cui le costanti, le variabili e i cicli.

    Le costanti, le variabili e i cicli sono elementi fondamentali della programmazione Java, poiché consentono di memorizzare e manipolare dati, eseguire operazioni ripetute e controllare il flusso di esecuzione del programma. In questo articolo, esploreremo in dettaglio questi concetti, fornendo esempi e spiegazioni per aiutare i lettori a comprendere come utilizzarli efficacemente nella loro programmazione Java.

    Le costanti in Java sono valori che non possono essere modificati una volta definiti. Sono utilizzate per rappresentare valori che non cambiano nel tempo, come ad esempio il valore di pi greco o il numero di giorni in una settimana. Le costanti in Java sono definite utilizzando la parola chiave `final` e possono essere di tipo primitivo, come `int` o `double`, o di tipo di riferimento, come `String`.

    Le variabili in Java sono contenitori che possono memorizzare valori di tipo primitivo o di riferimento. Le variabili possono essere dichiarate con un tipo specifico, come `int` o `String`, e possono essere assegnate un valore utilizzando l’operatore di assegnazione (`=`). Le variabili in Java possono essere utilizzate per memorizzare dati temporanei o per scambiare informazioni tra diverse parti del programma.

    I cicli in Java sono costrutti che consentono di eseguire un blocco di codice ripetutamente fino a quando una condizione specificata non è più vera. I cicli più comuni in Java sono il ciclo `for`, il ciclo `while` e il ciclo `do-while`. Il ciclo `for` è utilizzato per eseguire un blocco di codice un numero fisso di volte, mentre il ciclo `while` e il ciclo `do-while` sono utilizzati per eseguire un blocco di codice fino a quando una condizione specificata non è più vera.

    In questo articolo esploreremo pertanto i concetti di costanti, variabili e cicli in Java, fornendo esempi e spiegazioni per aiutare i lettori a comprendere come utilizzarli efficacemente nella loro programmazione Java. Speriamo che questo articolo sia utile per chi sta iniziando a programmare in Java e che possa fornire una base solida per ulteriori studi e applicazioni pratiche. 🙂

    Variabili in Java

    Una variabile è uno spazio in memoria dove salvi un valore. In Java, devi dichiararne il tipo.

    Esempi:

    int numero = 5;             // intero
    double decimale = 3.14;     // numero con virgola
    boolean acceso = true;      // vero o falso
    String nome = "Mario";      // testo
    

    Java è fortemente tipizzato, quindi non puoi cambiare tipo dopo.

    numero = 10;        // OK
    numero = "ciao";    // ❌ Errore
    

    Costanti

    Una costante è come una variabile, ma non cambia mai.

    Sintassi:

    final double PI = 3.14159;
    final int MAX = 100;
    
    • final = rende il valore immutabile
    • per convenzione: nomi delle costanti in MAIUSCOLO

    Cicli (loop)

    I cicli ripetono qualcosa finché serve.

    for – quando sai quante volte ripetere

    for (int i = 0; i < 5; i++) {
        System.out.println("i vale: " + i);
    }
    

    Stampa i da 0 a 4.

    while – quando non sai quante volte, ma hai una condizione

    int i = 0;
    while (i < 5) {
        System.out.println("i: " + i);
        i++;
    }
    

    do...while – come while, ma esegue almeno una volta

    int i = 0;
    do {
        System.out.println("i: " + i);
        i++;
    } while (i < 5);
    

    for-each – per scorrere array o collezioni

    String[] nomi = {"Anna", "Luca", "Marta"};
    for (String nome : nomi) {
        System.out.println(nome);
    }
    

    Provalo

    Cicli.java

    public class Cicli {
        public static void main(String[] args) {
            final int MAX = 3;
    
            for (int i = 1; i <= MAX; i++) {
                System.out.println("Conto: " + i);
            }
    
            String[] colori = {"rosso", "verde", "blu"};
            for (String c : colori) {
                System.out.println("Colore: " + c);
            }
        }
    }
    

    Compila con:

    javac Cicli.java
    java Cicli
    
  • Java: come gestire le liste (ArrayList)

    Java: come gestire le liste (ArrayList)

    Ecco un spiegone introduttivo sulle liste ArrayList in Java, con un esempio pratico semplice per capire subito come funziona. Quando programmi in Java, spesso hai bisogno di una struttura dati che ti permetta di conservare e gestire una raccolta di elementi dinamica: una lista che può crescere o ridursi mentre il programma gira. Qui entra in gioco l’ArrayList.

    Cos’è un ArrayList?

    • È una collezione che memorizza elementi in ordine
    • È basata su un array interno che si ridimensiona automaticamente quando serve
    • Permette di aggiungere, rimuovere, modificare elementi facilmente
    • È parte del package java.util
    • È tipo generico, quindi puoi usarla per qualsiasi tipo di dato (ArrayList<String>, ArrayList<Integer>, o classi personalizzate)

    Perché usare ArrayList invece di un array normale?

    • Gli array in Java hanno dimensione fissa: non puoi aggiungere elementi oltre la capacità iniziale
    • Con ArrayList, la dimensione cresce in automatico
    • Offre molti metodi utili per manipolare la lista:
      • add(), remove(), get(), set(), size(), ecc.
    • È molto comoda e flessibile per gestire liste di dati variabili

    ArrayList — la lista più usata in Java!

    Si usa così:

    import java.util.ArrayList;
    
    public class ListaDemo {
        public static void main(String[] args) {
            ArrayList<String> nomi = new ArrayList<>();
    
            nomi.add("Anna");
            nomi.add("Marco");
            nomi.add("Luca");
    
            System.out.println("Lista: " + nomi);             // [Anna, Marco, Luca]
            System.out.println("Primo nome: " + nomi.get(0)); // Anna
    
            nomi.remove("Marco"); // oppure: nomi.remove(1);
            System.out.println("Dopo rimozione: " + nomi);
        }
    }

    In sintesi il programma qui riportato crea una lista di nomi, la stampa, legge un elemento specifico, ne elimina uno, e poi stampa la lista modificata. È un esempio semplice di come usare un ArrayList per memorizzare, leggere e modificare dati in Java.

    Nel dettaglio:

    1. Importa la classe ArrayList dal package java.util, così possiamo usarla nel programma.
    2. Definisce una classe chiamata ListaDemo con il metodo main, il punto di partenza del programma.
    3. All’interno del main, crea una nuova lista dinamica di stringhe chiamata nomi usando ArrayList<String>.
    4. Aggiunge tre nomi alla lista: "Anna", "Marco" e "Luca" usando il metodo .add().
    5. Stampa la lista completa, quindi mostra tutti i nomi contenuti: [Anna, Marco, Luca].
    6. Stampa il primo elemento della lista usando .get(0), cioè "Anna".
    7. Rimuove l’elemento "Marco" dalla lista (puoi farlo specificando il valore o l’indice).
    8. Stampa la lista aggiornata dopo la rimozione, quindi ora la lista contiene solo [Anna, Luca].

    Ciclo su ArrayList

    Per fare un ciclo variamo leggermente la sintassi e scriviamo così:

    for (String nome : nomi) {
        System.out.println("Ciao " + nome);
    }
    

    Se ad esempio nomi contiene [“pippo”, “pluto”, “fernando”] avremo come output:

    Ciao pippo
    Ciao pluto
    Ciao fernando

    Lista di oggetti personalizzati

    Puoi anche salvare oggetti dentro una lista, esattamente come con gli array.

    Persona.java

    public class Persona {
        String nome;
        int età;
    
        public Persona(String nome, int età) {
            this.nome = nome;
            this.età = età;
        }
    
        public void stampa() {
            System.out.println(nome + " ha " + età + " anni.");
        }
    }
    

    Main.java

    import java.util.ArrayList;
    
    public class Main {
        public static void main(String[] args) {
            ArrayList<Persona> persone = new ArrayList<>();
    
            persone.add(new Persona("Giulia", 24));
            persone.add(new Persona("Davide", 31));
            persone.add(new Persona("Sara", 29));
    
            for (Persona p : persone) {
                p.stampa();
            }
        }
    }
    

    ✨ Operazioni utili sulle liste

    MetodoEffetto
    add(x)aggiunge alla fine
    add(i, x)inserisce in posizione i
    remove(i) / remove(obj)rimuove per indice o oggetto
    get(i)legge elemento in posizione i
    set(i, x)cambia elemento in posizione i
    size()restituisce numero di elementi
    clear()svuota la lista
    contains(x)controlla se contiene x

    Ordinare liste

    import java.util.Collections;
    
    Collections.sort(nomi); // ordina la lista in ordine alfabetico
    

    Per oggetti personalizzati serve in genere un Comparator.

  • Che significa cluster?

    Che significa cluster?

    Il termine “cluster” può avere significati diversi a seconda del contesto in cui viene utilizzato. Ecco alcuni significati comuni in ambiti informatici e medici, insieme a un esempio di “cluster” in ognuno dei due contesti:

    Cluster in informatica

    In informatica, un “cluster” può fare riferimento a un gruppo di elementi o unità di dati che sono raggruppati insieme per scopi specifici. Ad esempio, in un contesto di elaborazione dati, si potrebbe avere un “cluster di server” che rappresenta un gruppo di server fisici o virtuali che lavorano insieme per fornire risorse computazionali condivise o servizi di rete. Questi server possono essere collegati tra loro per migliorare la scalabilità e l’affidabilità di un’applicazione.

    Esempio: “Abbiamo creato un cluster di server per gestire il carico di lavoro del nostro sito web, in modo che possa gestire un alto numero di utenti simultanei.”

    Cluster in medicina

    In medicina, il termine “cluster” può essere utilizzato per descrivere una concentrazione di eventi, sintomi o casi di una determinata malattia o condizione in una specifica area geografica o periodo di tempo. Un “cluster di casi” potrebbe indicare un numero insolitamente elevato di persone che hanno contratto la stessa malattia nello stesso luogo o periodo.

    Esempio: “Nel corso dell’ultimo mese, abbiamo osservato un cluster di casi di influenza in questa comunità, il che suggerisce un’epidemia locale.”

    In entrambi gli ambiti, il concetto di “cluster” si riferisce all’aggregazione o alla concentrazione di elementi simili o correlati in uno specifico contesto.

    Cluster in Python

    Metodo k-means

    Il metodo K-Means è un algoritmo iterativo che cerca di minimizzare la somma delle distanze quadrate tra i punti dati e i centroidi dei loro cluster. Questo significa, in altri termini, che l’algoritmo cerca di creare cluster in cui i punti all’interno di ciascun cluster sono il più simili possibile tra loro. L’algoritmo K-Means è ampiamente utilizzato nella pratica in una varietà di applicazioni in cui è necessario raggruppare dati simili in cluster. Ecco alcune delle sue applicazioni più comuni:

    1. Segmentazione dei Clienti: Le aziende utilizzano il K-Means per raggruppare i clienti in segmenti omogenei in base a comportamenti d’acquisto, preferenze o altri dati demografici. Questa segmentazione aiuta a personalizzare le strategie di marketing e migliorare il servizio clienti.
    2. Ricerca di Mercato: Il K-Means può essere utilizzato per analizzare i dati di ricerca di mercato e identificare gruppi di consumatori con comportamenti simili. Questo aiuta le aziende a comprendere meglio il loro pubblico di destinazione.
    3. Elaborazione delle Immagini: In ambito di visione artificiale, il K-Means può essere utilizzato per la segmentazione delle immagini. Ad esempio, può essere utilizzato per separare automaticamente oggetti o regioni di interesse in un’immagine.
    4. Ricerca di Documenti Simili: In motori di ricerca o sistemi di gestione documentale, il K-Means può essere utilizzato per raggruppare documenti simili in base al loro contenuto, semplificando così la ricerca e l’organizzazione.
    5. Riconoscimento dei Modelli: In analisi dei dati, il K-Means può essere utilizzato per riconoscere pattern o anomalie nei dati. Ad esempio, può essere utilizzato per il rilevamento delle frodi nelle transazioni finanziarie.
    6. Astronomia: Nell’astronomia, il K-Means può essere utilizzato per categorizzare stelle o galassie in base alle loro caratteristiche osservate.
    7. Analisi del Testo: Nel campo dell’analisi del testo, il K-Means può essere utilizzato per raggruppare documenti simili o per creare riepiloghi automatici di grandi raccolte di testo.
    8. Controllo di Qualità: In produzione, il K-Means può essere utilizzato per monitorare la qualità dei prodotti e identificare potenziali difetti.

    Ecco un’illustrazione semplice del funzionamento del metodo K-Means:

    1. Inizializzazione: Si inizia scegliendo casualmente K punti dati come “centroidi iniziali”. Questi centroidi rappresentano il centro di ciascun cluster immaginario.
    2. Assegnazione: Per ciascun punto dati nel dataset, si calcola la distanza tra il punto e tutti i centroidi. Il punto viene quindi assegnato al cluster il cui centroide è più vicino.
    3. Ricalcolo dei centroidi: Una volta assegnati tutti i punti ai cluster, si calcolano nuovi centroidi per ciascun cluster. Questi nuovi centroidi sono calcolati come la media dei punti all’interno di ciascun cluster.
    4. Ripetizione: I passaggi 2 e 3 vengono ripetuti iterativamente fino a quando i centroidi non cambiano significativamente o un numero massimo di iterazioni viene raggiunto.
    5. Risultato finale: Alla fine, si ottengono K cluster in cui i punti dati sono raggruppati in base alla loro somiglianza. Ogni punto appartiene a uno e solo uno di questi cluster.

    Creare un cluster di numeri in Python è un’operazione che richiede l’utilizzo di una libreria di clustering, come scikit-learn. Ecco un esempio di come creare un cluster utilizzando il metodo K-Means, uno dei metodi di clustering più comuni, con un insieme casuale di numeri. In questo esempio, abbiamo generato 100 punti bidimensionali casuali e quindi creato un modello K-Means con 3 cluster.

    Il modello è stato addestrato sui dati, e le etichette dei cluster per ciascun punto dati sono state ottenute utilizzando kmeans.labels_, mentre i centri dei cluster sono stati ottenuti utilizzando kmeans.cluster_centers_

    import numpy as np
    from sklearn.cluster import KMeans
    
    # Generiamo un insieme casuale di numeri come dati di esempio
    np.random.seed(0)
    data = np.random.rand(100, 2)  # Creiamo 100 punti bidimensionali
    
    # Definiamo il numero di cluster desiderato
    numero_cluster = 3
    
    # Creiamo il modello K-Means
    kmeans = KMeans(n_clusters=numero_cluster)
    
    # Addestriamo il modello sui dati
    kmeans.fit(data)
    
    # Otteniamo le etichette dei cluster per ciascun punto dati
    etichette_cluster = kmeans.labels_
    
    # Otteniamo i centri dei cluster
    centri_cluster = kmeans.cluster_centers_
    
    # Stampiamo le etichette dei cluster e i centri dei cluster
    print("Etichette dei cluster:")
    print(etichette_cluster)
    print("\nCentri dei cluster:")
    print(centri_cluster)

    Nella foto, un cluster “immaginato” da Midjourney

  • Il teorema di Bohm-Jacopini spiegato in modo semplice

    Il teorema di Bohm-Jacopini spiegato in modo semplice

    Il Teorema di Bohm-Jacopini viene ideato dai matematici e informatici teorici Corrado Bohm e Giuseppe Jacopini negli anni ’60, rappresenta un risultato cruciale nell’ambito della teoria dei linguaggi di programmazione. Questo teorema dimostra infatti che qualsiasi programma in un linguaggio iterativo (come PHP, Javascript, Python, C++, …) può essere espresso utilizzando esclusivamente tre tipi di strutture di controllo fondamentali: sequenza, selezione/condizione, ciclo.

    Nello specifico, la sequenza indica una sequenza o blocco di istruzioni, da eseguire una di seguito all’altra; la selezione / condizionale indica un bivio in cui si deve scegliere se fare una cosa oppure un’altra; il ciclo che indica un blocco di istruzioni che viene ripetuto. Nel teorema originale si applicava il ragionamento ad una macchina di Turing, modello ideale di qualsiasi computer programmabile. La dimostrazione avviene per induzione e viene affidata ad un formalismo di decomposizione matematica, che si può consultare ad esempio qui.

    A livello informale, possiamo utilizzare l’esempio delle ricette per dimostrare il teorema di Bohm-Jacopini. Immaginiamo di dover scrivere un algoritmo per preparare una ricetta: la ricetta contiene istruzioni sequenziali, decisioni basate su condizioni (selezioni) e iterazioni (cicli). Dimostreremo che qualsiasi algoritmo di questo tipo può essere riscritto utilizzando solo sequenze, selezioni e cicli.

    Supponiamo di avere la seguente ricetta:

    1. Prepara gli ingredienti.
    2. Mescola gli ingredienti.
    3. Se la miscela sembra troppo densa, aggiungi un po' di liquido.
    4. Cuoci sulla griglia finché dorato.
    5. Servi caldo.

    Possiamo adattare questa ricetta in termini di struttura di controllo:

    markdown
    Sequenza
    Prepara gli ingredienti
    Mescola gli ingredienti
    Seleziona
    Condizione: la miscela sembra troppo densa
    Aggiungi un po' di liquido
    Fine
    Cuoci sulla griglia finché dorato
    Servi caldo

    Ora dimostreremo come trasformare ogni parte di questa struttura in selezioni annidate:

    1. Prepara gli ingredienti è un’istruzione semplice, quindi è già nella forma desiderata.
    2. Mescola gli ingredienti è un’altra istruzione semplice.
    3. Condizione: la miscela sembra troppo densa è una selezione.
    4. Aggiungi un po’ di liquido è un’istruzione semplice.
    5. Cuoci sulla griglia finché dorato è un ciclo (iterazione) mentre la condizione “finché dorato” è un’istruzione di selezione.
    6. Servi caldo è un’altra istruzione semplice.

    In questa dimostrazione dell’esempio delle ricette, abbiamo mostrato come una sequenza di istruzioni, decisioni basate su condizioni (selezioni) e iterazioni (cicli) possano essere rappresentate utilizzando solo selezioni annidate. Questo esemplifica il teorema di Bohm-Jacopini, che afferma che qualsiasi algoritmo può essere implementato utilizzando solo sequenze, selezioni e cicli.

    Non è difficile immaginare, a livello pratico, che la sequenza sia la sequenza di istruzioni di un linguaggio, la selezione rappresenti una condizione if dicotomica o binaria (del tipo vero o falso, a cui eventuali condizioni a n possibilità si possono sempre ricondurre), mentre il ciclo sia di fatto il loop che si implementa mediante for oppure while. Questo rende l’idea dell’importanza teorica del teorema, ampiamente sottovalutato e dato un po’ per scontato, ed estremamente utile per le ricerche successive in informatica teorica.

  • L’algoritmo del codice fiscale, spiegato passo-passo

    Il codice fiscale è un codice alfanumerico utilizzato principalmente in Italia per identificare in modo univoco le persone ai fini fiscali e amministrativi. Esso contiene informazioni sulla persona, inclusi il suo nome, cognome, data e luogo di nascita. La data di nascita di una persona può essere identificata dai primi 6 caratteri del codice fiscale, che sono composti dall’anno, mese e giorno di nascita. Per esempio, nel codice fiscale “RSSMRA01A01H501D”:

    • “01A” rappresenta l’anno di nascita. In questo caso, l’anno di nascita è “2001”.
    • “01” rappresenta il mese di nascita. In questo caso, il mese di nascita è “Gennaio”.
    • “H” rappresenta il giorno di nascita e il sesso. In questo caso, il giorno di nascita è il “1°” e il sesso è maschile.

    Quindi, basandoci sui primi 6 caratteri di questo codice fiscale, possiamo determinare che la persona è nata il 1° gennaio 2001.

    Un codice fiscale italiano è composto da 16 caratteri alfanumerici. I primi 15 caratteri sono utilizzati per identificare in modo univoco la persona, mentre l’ultimo carattere è una lettera di controllo. (Immagine tratta da https://pypi.org/project/python-codicefiscale/)

    Struttura del codice fiscale

    Il codice fiscale è suddiviso in 7 parti:

    1. Tre lettere per l’individuazione del cognome
    2. Tre lettere per l’individuazione del nome
    3. Due numeri per l’anno di nascita
    4. Una lettera per il mese di nascita
    5. Due numeri per il giorno di nascita e il sesso
    6. Una lettera e tre numeri per l’individuazione del luogo di nascita
    7. Una lettera finale di controllo

    Questo schema di suddivisione fornisce informazioni dettagliate sulla persona, inclusi il suo nome, cognome, data e luogo di nascita, garantendo al contempo l’unicità e l’integrità del codice fiscale attraverso il carattere di controllo finale.

    Cognome

    Dobbiamo ottenere 3 lettere per il cognome. Iniziamo prendendo la prima, la seconda e la terza consonante del cognome. Tuttavia, potrebbe verificarsi il caso in cui ci siano solo due consonanti oppure una sola; in tal caso, dopo aver preso le consonanti disponibili, iniziamo a prendere anche le vocali. Se ancora mancano altre lettere per completare la nostra stringa di tre caratteri, aggiungiamo la lettera X. È importante considerare i cognomi composti da più parole come se fossero una sola parola, trattando il cognome composto come un’entità unica ai fini dell’estrazione delle lettere.

    Nome

    Il procedimento utilizzato per ricavare le tre lettere del nome è del tutto analogo a quello del cognome, con l’unica differenza che ora dobbiamo prendere la prima, la terza e la quarta consonante del nome. Nel caso in cui non ci siano quattro consonanti, prenderemo le prime tre. Se ci sono meno di tre consonanti, seguiremo lo stesso procedimento utilizzato per il cognome.

    Anno nascita

    Prendi le ultime due cifre dell’anno di nascita.

    Mese nascita

    Assegna una lettera corrispondente al mese di nascita secondo una tabella prestabilita (fonte).

    Lettera Mese Lettera Mese Lettera Mese
    A gennaio E maggio P settembre
    B febbraio H giugno R ottobre
    C marzo L luglio S novembre
    D aprile M agosto T dicembre

    Giorno nascita e sesso

    Prendi i due numeri che rappresentano il giorno di nascita e aggiungi un numero per indicare il sesso (0 per le donne, 1 per gli uomini).

    Lettera e tre numeri per luogo di nascita

    Usa il codice catastale del luogo di nascita, una serie di una lettera e tre numeri.

    Lettera di controllo

    Calcola il carattere di controllo basato sui primi 15 caratteri del codice fiscale. (fonte)

    Verifica del CF

    La verifica della correttezza e validità può avvenire mediante il sito dell’Agenzia delle Entrate.

    Algoritmo del codice fiscale in Python

    Per convertire da nome e cognome a codice fiscale conviene fare uso di python-codicefiscale, che consta di due semplici istruzioni:

    from codicefiscale import codicefiscale
    
    cf = codicefiscale.encode(
        lastname="Caccamo",
        firstname="Fabio",
        gender="M",
        birthdate="03/04/1985",
        birthplace="Torino",
    )
    
    # cf = "CCCFBA85D03L219P"

    Per fare il processo inverso, possiamo scrivere invece:

    from codicefiscale import decode
    
    # Codice fiscale completo
    codice_fiscale_completo = "RSSMRA01A01H501D"
    
    # Decodifica il codice fiscale per ottenere il nome
    nome = decode(codice_fiscale_completo)['name']
    
    print("Il nome associato al codice fiscale è:", nome)
  • Algoritmo di compressione LZW (Lempel-Ziv-Welch): cos’è e come funziona

    Algoritmo di compressione LZW (Lempel-Ziv-Welch): cos’è e come funziona

    L’algoritmo LZW (Lempel-Ziv-Welch) è un algoritmo di compressione senza perdita dei dati. Funziona cercando e sostituendo sequenze di dati ripetitive con codici più corti.

    In pseudo-codice

    Leggi P
    Scrivi P
    C = P
    WHILE ci sono caratteri da leggere DO
        leggi C
        IF NEW_CODE non è nel dizionario THEN
            STRING = dizionario( OLD_CODE )
            STRING = STRING+CHARACTER
        ELSE
            STRING = dizionario( NEW_CODE )
        ENDIF
        scrivi STRING
        C = first character in STRING
        add OLD_CODE + C to the translation table
        OLD_CODE = NEW_CODE
    END of WHILE

    Algoritmo passo passo

    Codice: https://gist.github.com/salvatorecapolupo/558151eba2ec27d24593c3af03f9a4ca

    Ecco una spiegazione semplificata del funzionamento dell’algoritmo:

    1. Inizializzazione del Dizionario: L’algoritmo inizia con un dizionario che contiene tutte le possibili combinazioni di singoli caratteri. Ad esempio, se stiamo lavorando con caratteri ASCII, il dizionario iniziale conterrà tutti i singoli caratteri da 0 a 255.
    2. Analisi della Stringa di Input: L’algoritmo analizza la stringa di input carattere per carattere.
    3. Costruzione di Sequenze: A mano a mano che analizza la stringa, l’algoritmo costruisce sequenze di caratteri. Inizia con singoli caratteri e, man mano che incontra nuove sequenze, le aggiunge al dizionario.
    4. Sostituzione con Codici: Quando una sequenza è già presente nel dizionario, l’algoritmo la sostituisce con un codice più corto, che rappresenta la sequenza completa.
    5. Aggiornamento del Dizionario: Ogni volta che una nuova sequenza viene aggiunta al dizionario, viene assegnato un nuovo codice.
    6. Generazione dell’Output Compresso: L’output compresso consiste in una sequenza di codici che rappresentano le sequenze di input.
    7. Decompressione: Per decomprimere i dati, il processo viene invertito. I codici vengono letti e sostituiti con le sequenze corrispondenti dal dizionario.

    L’efficienza di LZW deriva dalla sua capacità di adattarsi dinamicamente al contenuto dei dati, creando e aggiornando il dizionario durante il processo di compressione. Questo consente di ottenere una buona compressione per dati che contengono pattern ripetitivi o sequenze lunghe.