{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": "# 03 · Ispezionare una tabella\n\nDescrivi una nuova tabella Spark prima di usarla. Parti da una piccola tabella di righe d’ordine del bar e finisci con una tabella di consegne. Esegui tutte le celle in una nuova sessione Python su Kaggle con PySpark classico 4.x e Java 17 o successivo. Se serve, esegui `%pip install \"pyspark>=4.0,<4.1\"` in una cella di preparazione con Internet abilitato. Tutti i dati sono definiti nel notebook.",
      "id": "cell-01"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": "from pyspark.sql import SparkSession\nfrom pyspark.sql import functions as F\n\nspark = SparkSession.builder.master(\"local[2]\").appName(\"03-inspect-a-table\").getOrCreate()\n\nlines = spark.createDataFrame(\n    [\n        (101, 1, \"tea\", 2, 9.00, \"2026-11-01\", \"Two teas, one with lemon\"),\n        (101, 2, \"cake\", 1, 9.00, \"2026-11-01\", None),\n        (102, 1, \"coffee\", 3, 12.00, \"2026-11-02\", \"Collect from the counter\"),\n        (103, 1, \"tea\", 4, 15.00, \"2026-11-02\", None),\n        (103, 2, \"coffee\", 2, 15.00, \"2026-11-02\", \"One decaf coffee\"),\n        (104, 1, \"tea\", 5, 18.00, \"2026-11-03\", \"Serve in takeaway cups\"),\n    ],\n    \"order_id INT, line_id INT, item STRING, quantity INT, order_total DOUBLE, order_date_text STRING, note STRING\",\n)",
      "execution_count": null,
      "outputs": [],
      "id": "cell-02"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": "## 1. Mostrare le prime n righe\n\n`show(3)` stampa fino a tre righe. L’anteprima aiuta a orientarsi. Senza ordinamento Spark non garantisce l’ordine delle righe mostrate.\n\n## 2. Valore mancante\n\nUn null in Spark indica un valore assente. Una nota null è diversa da una nota che contiene una stringa vuota.\n\n## 3. Contare le righe\n\n`count()` restituisce il numero totale di righe, comprese quelle con valori null.\n\n## 4. Elencare i nomi delle colonne\n\n`columns` restituisce i nomi nell’ordine dello schema.",
      "id": "cell-03"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": "lines.show(3)\nassert lines.count() == 6\nprint(lines.columns)",
      "execution_count": null,
      "outputs": [],
      "id": "cell-04"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": "## 5. Colonna dimensione\n\nUna dimensione descrive un’osservazione o permette di raggrupparla. Qui `item` consente di confrontare tè, caffè e torta.\n\n## 6. Colonna misura\n\nUna misura rappresenta una quantità che puoi riepilogare con un calcolo adatto alla grana. Per esempio, la somma di `quantity` tra le righe dà il numero di unità vendute.\n\n## 7. Identificativo numerico e misura\n\n`order_id` usa interi come etichette. Contando gli ID distinti ottieni il numero di ordini. Sommare gli ID non ha significato per l’attività.\n\n## 8. Controllare la grana\n\nLa grana ipotizzata è una riga per ogni riga d’ordine. Raggruppa per `order_id` e `line_id`, conta le righe di ogni gruppo e filtra i conteggi superiori a uno. Cerca anche identificativi mancanti. Qui usiamo `groupBy()`, `filter()` e `count_distinct()` come strumenti. Li ritroverai nelle lezioni successive.",
      "id": "cell-05"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": "duplicate_lines = lines.groupBy(\"order_id\", \"line_id\").count().filter(F.col(\"count\") > 1)\nmissing_keys = lines.filter(F.col(\"order_id\").isNull() | F.col(\"line_id\").isNull())\nassert duplicate_lines.count() == 0\nassert missing_keys.count() == 0\nlines.agg(F.count_distinct(\"order_id\").alias(\"orders\")).show()",
      "execution_count": null,
      "outputs": [],
      "id": "cell-06"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": "## 9. Doppio conteggio da una grana sbagliata\n\n`order_total` appartiene all’ordine e si ripete su ciascuna delle sue righe. Sommando alla grana della riga ottieni 78. Alla grana dell’ordine ottieni 54. Nei dati di esempio sappiamo che il totale è costante all’interno di ogni ordine, quindi `max()` ne conserva una copia. Con dati nuovi verifica questa ipotesi prima di usare la scorciatoia.",
      "id": "cell-07"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": "orders = lines.groupBy(\"order_id\").agg(F.max(\"order_total\").alias(\"order_total\"))\nassert lines.agg(F.sum(\"order_total\")).first()[0] == 78\nassert orders.agg(F.sum(\"order_total\")).first()[0] == 54\norders.show()",
      "execution_count": null,
      "outputs": [],
      "id": "cell-08"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": "## 10. Descrivere una tabella prima di usarla\n\nIndica grana, identificativi, dimensioni, misure e lacune note. Qui ogni riga d’ordine è identificata dalla coppia `order_id` e `line_id`. Il prodotto è una dimensione, la quantità è una misura della riga, il totale appartiene all’ordine e le note possono mancare.\n\n## 11. Formato lungo\n\nNella tabella lunga delle vendite giornaliere ogni riga contiene un giorno, un prodotto e le unità vendute. Aggiungere un prodotto significa aggiungere righe.\n\n## 12. Formato largo\n\nNella tabella larga delle vendite giornaliere ogni riga contiene un giorno e una colonna per prodotto. Aggiungere un prodotto cambia lo schema.\n\n## 13. Scegliere la forma\n\nUsa il formato lungo per confrontare o raggruppare per prodotto quando l’insieme delle categorie cambia. Il formato largo può servire per un report fisso che affianca poche categorie note. Gli esempi mostrano un solo giorno, con zero unità di caffè vendute.",
      "id": "cell-09"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": "daily_long = spark.createDataFrame(\n    [(\"2026-11-01\", \"tea\", 2), (\"2026-11-01\", \"cake\", 1), (\"2026-11-01\", \"coffee\", 0)],\n    \"day STRING, item STRING, units INT\",\n)\ndaily_wide = spark.createDataFrame(\n    [(\"2026-11-01\", 2, 1, 0)],\n    \"day STRING, tea_units INT, cake_units INT, coffee_units INT\",\n)\ndaily_long.show()\ndaily_wide.show()",
      "execution_count": null,
      "outputs": [],
      "id": "cell-10"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": "## 14. Portare tutte le righe nel driver\n\n`collect()` porta tutte le righe in una lista Python nel driver. Riservalo a dati che sai poter contenere in memoria, come l’esempio giornaliero di tre righe.",
      "id": "cell-11"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": "print(daily_long.collect())",
      "execution_count": null,
      "outputs": [],
      "id": "cell-12"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": "## 15. Stampare lo schema\n\n`printSchema()` stampa i nomi, i tipi e la possibilità di contenere null per ciascun campo. Se un campo ammette null, esamina i dati per vedere se ne contiene.\n\n## 16. Struttura della tabella\n\nDescrivi la struttura logica usando schema e grana. Lo schema può dirti che `line_id` è un intero. La definizione operativa e i controlli delle chiavi indicano che identifica una riga all’interno del suo ordine.\n\n## 17. Data letta come testo\n\n`order_date_text` sembra una data, ma lo schema la dichiara come stringa. Usa `to_date()` con il formato di input noto per creare una colonna di date. Controlla lo schema dopo la conversione.",
      "id": "cell-13"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": "lines.printSchema()\ndated = lines.withColumn(\"order_date\", F.to_date(\"order_date_text\", \"yyyy-MM-dd\"))\ndated.select(\"order_date_text\", \"order_date\").printSchema()",
      "execution_count": null,
      "outputs": [],
      "id": "cell-14"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": "## 18. Limitare il numero di righe\n\n`limit(2)` restituisce un DataFrame con al massimo due righe. È una trasformazione che descrive il risultato da calcolare quando serve.\n\n## 19. Portare n righe nel driver\n\n`take(2)` restituisce al massimo due righe in una lista Python. È un’azione. Qui `orderBy()` rende l’anteprima deterministica ordinando per la chiave della riga già verificata.\n\n## 20. Mostrare il testo completo\n\n`truncate=False` fa sì che `show()` stampi tutto il testo di ogni cella.\n\n## 21. Mostrare un campo per riga\n\n`vertical=True` stampa ogni record come un blocco con un campo per riga di output. È utile quando un record è troppo largo da leggere in orizzontale.\n\n## 22. Controllare se una tabella è vuota\n\n`isEmpty()` verifica se il numero di righe è zero. Una riga con tutti i valori null conta comunque come riga.",
      "id": "cell-15"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": "sorted_lines = lines.orderBy(\"order_id\", \"line_id\")\nsorted_lines.limit(2).show(truncate=False)\nprint(sorted_lines.take(2))\nsorted_lines.show(1, truncate=False, vertical=True)\nassert lines.limit(0).isEmpty()\nassert not lines.isEmpty()\nassert not spark.createDataFrame([(None,)], \"note STRING\").isEmpty()",
      "execution_count": null,
      "outputs": [],
      "id": "cell-16"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": "## 23. Statistiche descrittive\n\n`describe()` restituisce conteggio, media, deviazione standard, minimo e massimo per una colonna numerica. Il conteggio della colonna esclude i null. `count(\"*\")` conta le righe e `count(\"note\")` conta le note presenti. In questa tabella non vuota, il rapporto tra la differenza dei conteggi e il numero di righe, moltiplicato per 100, dà la percentuale di note mancanti.\n\n## 24. Statistiche con quartili\n\nPer impostazione predefinita, `summary()` include anche quartili approssimati al 25%, 50% e 75%. I quartili aiutano a descrivere la distribuzione dei valori.",
      "id": "cell-17"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": "lines.describe(\"quantity\").show()\nlines.select(\"quantity\").summary().show()\ncounts = lines.agg(F.count(\"*\").alias(\"rows\"), F.count(\"note\").alias(\"present_notes\")).first()\nprint(f\"Missing notes: {100 * (counts.rows - counts.present_notes) / counts.rows:.1f}%\")",
      "execution_count": null,
      "outputs": [],
      "id": "cell-18"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": "## 25. Quantili approssimati\n\n`approxQuantile()` accetta probabilità da 0 a 1. L’errore relativo delimita la posizione del valore restituito nell’ordinamento. I null vengono ignorati.\n\n## 26. Correlazione tra due colonne\n\n`corr()` restituisce la correlazione di Pearson. Misura direzione e intensità di una relazione lineare su una scala da -1 a 1 per input non costanti. Qui usiamo una riga per ordine e confrontiamo le unità totali con il totale dell’ordine.\n\n## 27. Covarianza tra due colonne\n\n`cov()` restituisce la covarianza campionaria. Il segno indica la direzione della variazione congiunta. La grandezza dipende dalle unità di misura degli input.\n\n## 28. Valori frequenti\n\n`freqItems()` restituisce candidati in base alla soglia di supporto scelta e può includere falsi positivi. Usa conteggi esatti con `groupBy().count()` se devi riportare quali valori raggiungono la soglia.",
      "id": "cell-19"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": "print(\"Quantity quantiles:\", lines.approxQuantile(\"quantity\", [0.25, 0.5, 0.75], 0.05))\norder_measures = lines.groupBy(\"order_id\").agg(\n    F.sum(\"quantity\").alias(\"units\"), F.max(\"order_total\").alias(\"order_total\")\n)\nprint(\"Correlation:\", order_measures.corr(\"units\", \"order_total\"))\nprint(\"Sample covariance:\", order_measures.cov(\"units\", \"order_total\"))\nlines.freqItems([\"item\"], support=0.4).show(truncate=False)\nlines.groupBy(\"item\").count().orderBy(F.desc(\"count\"), \"item\").show()",
      "execution_count": null,
      "outputs": [],
      "id": "cell-20"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": "## 29. Domande prima del codice\n\nChe cosa rappresenta una riga? Che cosa dovrebbe identificarla? Quali campi sono misure e in quali unità? Che cosa significano i null? Qualche valore potrebbe ripetersi perché appartiene a una grana diversa?\n\n## 30. Ispezionare una nuova tabella\n\nLa tabella ha una riga per consegna. Usa anteprima, schema, controlli delle chiavi e conteggi per confermarlo sui dati di esempio. Prima di calcolare una durata tipica, decidi se una durata mancante debba valere zero.",
      "id": "cell-21"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": "deliveries = spark.createDataFrame(\n    [(\"D1\", 101, 18), (\"D2\", 102, None), (\"D3\", 104, 27)],\n    \"delivery_id STRING, order_id INT, duration_minutes INT\",\n)\ndeliveries.orderBy(\"delivery_id\").show()\ndeliveries.printSchema()\nprofile = deliveries.agg(\n    F.count(\"*\").alias(\"rows\"),\n    F.count_distinct(\"delivery_id\").alias(\"distinct_deliveries\"),\n    F.count(\"duration_minutes\").alias(\"known_durations\"),\n).first()\nassert profile.rows == profile.distinct_deliveries == 3\nassert profile.known_durations == 2",
      "execution_count": null,
      "outputs": [],
      "id": "cell-22"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": "## Verifica la comprensione\n\n1. Quali campi identificano una riga d’ordine del bar?\n2. Perché la somma dei totali d’ordine alla grana della riga è troppo alta?\n3. Come descriveresti la copertura delle durate nei dati di consegna?\n\n### Risposte\n\n1. La coppia `order_id` e `line_id`.\n2. Un ordine con più righe contribuisce con l’intero totale una volta per ogni riga. Sommando un solo totale per ordine ottieni 54.\n3. Due consegne su tre hanno una durata nota. Il null indica una durata sconosciuta, quindi sostituirlo con zero cambierebbe il significato dei dati.",
      "id": "cell-23"
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}
