Kapitoly kurzu
1. Obsahová mapa kurzu 2. Notebooky: PySpark vs Pure Python 3. Demo: Tři cesty a Mini Medallion 4. CU Costs a Capacity Metrics 5. SQL trojúhelník 6. Real-Time Intelligence: demo arc 7. Semantic modely, DAX a optimalizace 8. Security a administrace Fabricu 9. Zkouška DP-600: tipy
Runbooky — medallion
10. Mini Medallion ČHMÚ 11. Mini Medallion NYC Taxi 12. NYC Taxi třemi cestami 13. Mini Medallion OpenAQ 14. Big Medallion OpenAQ (2 roky)
Runbooky — RTI
15. RTI Library: architektura 16. A: Bicycles 60s 17. B: ČHMÚ HTTP polling 18. C: NDIC dopravní detektory 19. D: Camera ALPR simulátor 20. E: PID GTFS-RT 21. F: Activator alerty 22. KQL cheat sheet
🏠 Rozcestník kurzu
DP-600 · Kapitola 2

Notebooky: PySpark vs Pure Python

Decision matrix, cheat sheet pro oba kernely, časté chyby a workflow s AI asistencí
🗂 Kapitola kurzu

Cíle kapitoly

  • Rozhodnout podle objemu dat a kapacity mezi PySpark a Pure Python notebookem
  • Načíst CSV z URL i z Lakehouse Files a zapsat výsledek jako Delta tabulku v obou variantách
  • Používat mssparkutils, magic commands a správné cesty Files/ vs Tables/
  • Rozpoznat a opravit nejčastější chyby (encoding, whitespace, session timeout, OOM)
  • Použít VS Code s AI asistencí pro přípravu notebooku a vědět, co po Copilotu kontrolovat
2.1

Úvod

Cheat sheet pro práci s daty v Fabric notebooku. Pokrývá obě varianty:

  • PySpark — distribuovaný engine pro velká data, vyšší F-SKU kapacity
  • Pure Python (Pandas / Polars / DuckDB) — jednomomentový pohyb pro malá-střední data, F2/F4 kapacity
💡
Tip:

TL;DR pro netrpělivé:

  • Data <1 GB / <10M řádků → pure Python (Polars nebo DuckDB) je rychlejší end-to-end než Spark
  • Data >10 GB nebo distribuované transformace → PySpark
  • Mezi tím záleží na kapacitě a komfortu týmu
  • Spark má 30–120 s cold start (vytvoření session), pure Python startuje instantně
2.2

Decision matrix — kdy co

Scénář Doporučení Důvod
Ingest 1 CSV <100 MB Pandas / Polars Spark startup je delší než celý task
Ingest 5 CSVs á 200 MB Polars / DuckDB Stále menší než Spark overhead, ale Pandas memory-tight
Ingest 50 GB historických logů PySpark Nelze v paměti, distribuce nutná
Komplexní join 5+ tabulek nad >1 GB PySpark nebo DuckDB DuckDB výborný pro single-node, Spark pro multi-node
Streamované micro-batch zpracování PySpark Structured Streaming Pure Python neumí
Ad-hoc explorace, SELECT WHERE DuckDB nad Parquetem Sub-second response na Delta tables
Tým neumí Spark, dělá ETL Pure Python první Spark se učí týdny, Pandas znají všichni
Capacity F2 / F4 Pure Python preferovat Spark executor pool je malý, frequent cold starts
Capacity F8+ Buď, dle data size Spark dává smysl od F8 výš
ℹ️
Specifikum F2 / Fabric trial: F2 má pouze 2 CU pro Spark s burstable módem (lze krátkodobě přesáhnout na ~6 CU). Spark session se vytvoří, ale je pomalá a jednoduchá agregace nad 100 MB CSV trvá déle než ekvivalent v Polars na single-node. Účastníci to musí vidět — jinak budou doma plánovat F2 + PySpark a divit se.
2.3

Společné pro všechny notebooky v Fabricu

Magic commands (cell-level)

PYTHON
%%pyspark      # PySpark cell (default v Spark notebooku)
%%spark        # Scala Spark
%%sql          # Spark SQL
%%csharp       # .NET pro Spark (zřídka)
%%html         # Vlastní HTML render výstup
%pip install   # Instalace package pouze pro session
%conda install # Persistent install do environment (pomalu)

Lakehouse attach

V notebooku je pojem default Lakehouse — bez něj spark.read.parquet("Files/...") nefunguje. Attach se dělá přes UI (levé panely → Add Lakehouse → pin existing). Cesta v kódu:

PYTHON
"Files/raw/nehody_2024.csv"
"abfss://<workspace_id>@onelake.dfs.fabric.microsoft.com/<lakehouse_id>/Files/raw/nehody_2024.csv"

"Tables/nehody_silver"
spark.read.table("nehody_silver")  # když je default lakehouse attached
💡
Tip: ukázat oba způsoby, ale v dema používat krátkou cestu (Files/..., Tables/...). Plnou ABFSS uvést jednou jako "kdyby Lakehouse nebyl default".

mssparkutils / notebookutils — bridge mezi Spark a Fabric servisem

ℹ️
Poznámka: mssparkutils je zastaralý alias — nové notebooky používají notebookutils (stejné API: notebookutils.fs, notebookutils.credentials, …). Příklady níže fungují s oběma.
PYTHON
mssparkutils.fs.ls("Files/raw/")
mssparkutils.fs.mkdirs("Files/silver/")
mssparkutils.fs.cp("Files/raw/a.csv", "Files/archive/a.csv")
mssparkutils.fs.rm("Files/tmp/", recurse=True)

mssparkutils.notebook.run("01_Bronze_Ingest", timeoutSeconds=300, arguments={"year": "2024"})

mssparkutils.credentials.getSecret("https://kv.vault.azure.net/", "my-secret")

mssparkutils.notebook.exit("done")

mssparkutils funguje i v Pure Python notebook (od Q4 2024). Pro starší verze platilo, že je jen v Spark — ověřit aktuálně.

Cesty Files vs Tables

Cesta Typ Použití
Files/... Plain filesystem (ADLS Gen2) Raw CSVs, Excel, Parquet, JSON, libovolné soubory
Tables/... Managed Delta Lake Strukturovaná data, queryable přes SQL Endpoint
Shortcuts Reference do jiného OneLake/ADLS/S3 Bez kopie dat — pointer

Pravidlo Bronze/Silver/Gold:

  • Bronze typicky v Files/ (raw, original format, immutable)
  • Silver v Tables/ (Delta, čištěné, schema-on-write)
  • Gold v Tables/ (agregované, denormalizované, ready pro BI)
2.4

PySpark cheat sheet

Příklady postavené nad CHMI air quality datasetem (4 sloupce: idRegistration, startTime, idValueType, value). UTF-8, comma-delimited.

Read CSV — direct URL je tricky

Spark spark.read.csv() neumí HTTPS přímo. Je třeba mezikrok přes Files:

PYTHON
import urllib.request
url = "https://opendata.chmi.cz/air_quality/now/data/airquality_1h_avg_CZ.csv"
raw = urllib.request.urlopen(url).read().decode("utf-8")
mssparkutils.fs.put("Files/raw/chmi_snapshot.csv", raw, overwrite=True)

df = (spark.read
    .option("header", "true")
    .option("delimiter", ",")
    .option("ignoreLeadingWhiteSpace", "true")  # CHMI má ", " za čárkou
    .option("inferSchema", "true")
    .csv("Files/raw/chmi_snapshot.csv"))

df.printSchema()
df.show(5, truncate=False)
df.count()
ℹ️
Pozn.: inferSchema=true projde data 2× (1× pro inferenci, 1× pro načtení). U velkých souborů to dvojnásobí čas. Lepší: explicit schema.

Explicit schema

PYTHON
from pyspark.sql.types import StructType, StructField, IntegerType, DoubleType, StringType

schema = StructType([
    StructField("idRegistration", IntegerType(), True),
    StructField("startTime",      StringType(),  True),  # ISO 8601, parsujeme později
    StructField("idValueType",    IntegerType(), True),
    StructField("value",          DoubleType(),  True),
])

df = (spark.read
    .option("header", "true")
    .option("delimiter", ",")
    .option("ignoreLeadingWhiteSpace", "true")
    .schema(schema)
    .csv("Files/raw/chmi_snapshot.csv"))

Common transformations

PYTHON
from pyspark.sql.functions import col, to_timestamp, year, month, dayofmonth, hour, when, lit, regexp_replace

df_clean = (df
    # ISO 8601 with 'Z' suffix → DATETIME
    .withColumn("ts_utc", to_timestamp(regexp_replace(col("startTime"), "Z$", "")))
    .withColumn("datum", col("ts_utc").cast("date"))
    .withColumn("hodina", hour(col("ts_utc")))
    # Categorize value range
    .withColumn("over_who_threshold",
        when((col("idValueType") == 8) & (col("value") > 200), lit(1))    # NO2 hourly
        .when((col("idValueType") == 5) & (col("value") > 50), lit(1))    # PM10 24h
        .otherwise(lit(0))
    )
    .filter(col("ts_utc").isNotNull())
)

Aggregations

PYTHON
from pyspark.sql.functions import count, avg, max as _max

agg = (df_clean
    .groupBy("datum", "hodina", "idValueType")
    .agg(
        count("idRegistration").alias("pocet_stanic"),
        avg("value").alias("prum_value"),
        _max("value").alias("max_value")
    )
    .orderBy("datum", "hodina", "idValueType"))

agg.show()

Join s lookup tabulkou

PYTHON
dim_station = spark.read.csv("Files/lookups/dim_station.csv", header=True, inferSchema=True)
dim_kraj = spark.read.csv("Files/lookups/dim_kraj.csv", header=True, inferSchema=True)
dim_pollutant = spark.read.csv("Files/lookups/dim_pollutant.csv", header=True, inferSchema=True)

df_enriched = (df_clean
    .join(dim_station, df_clean.idRegistration == dim_station.id_registration, "left")
    .join(dim_kraj, dim_station.kraj_id == dim_kraj.kraj_id, "left")
    .join(dim_pollutant, df_clean.idValueType == dim_pollutant.id_value_type, "left")
    .select("ts_utc", "datum", "hodina", "kraj_nazev", "mesto",
            "nazev", "pollutant_kod", "value", "jednotka")
)

Spark SQL alternativa

PYTHON
df.createOrReplaceTempView("chmi_raw")

result = spark.sql("""
    SELECT
        TO_DATE(REGEXP_REPLACE(startTime, 'Z$', '')) AS datum,
        idValueType AS pollutant_id,
        COUNT(DISTINCT idRegistration) AS pocet_stanic,
        AVG(value) AS prum_value,
        MAX(value) AS max_value
    FROM chmi_raw
    WHERE value IS NOT NULL
    GROUP BY TO_DATE(REGEXP_REPLACE(startTime, 'Z$', '')), idValueType
    ORDER BY datum, pollutant_id
""")
result.show()

Write Delta do Tables

PYTHON
(df_clean.write
    .format("delta")
    .mode("overwrite")           # overwrite | append | error | ignore
    .option("overwriteSchema", "true")  # když měníš schema
    .saveAsTable("ovzdusi_silver"))

(df_clean.write
    .format("delta")
    .mode("append")
    .partitionBy("datum")
    .saveAsTable("ovzdusi_silver_history"))
⚠️
Anti-pattern: partitionBy("hodina") na hodinových datech = 24 malých partition denně. Cíl je 100 MB – 1 GB / partition. Pro CHMI snapshot data partition by datum (ne hodina) je optimum, případně month pokud máš víc let.

Optimize a Vacuum (housekeeping)

PYTHON
spark.sql("OPTIMIZE ovzdusi_silver")

spark.sql("OPTIMIZE ovzdusi_silver VORDER")

spark.sql("VACUUM ovzdusi_silver RETAIN 168 HOURS")  # 7 dní default
2.5

Pure Python cheat sheet

Notebook setup — Pure Python kernel

V Fabricu vyber Python notebook (ne Spark). Kernel je vanilla CPython 3.11 s pre-instalovanými balíky:

  • pandas, numpy, pyarrow
  • polars (od 2024)
  • duckdb (od 2024)
  • deltalake (rust-based Delta library)
  • fsspec, adlfs (storage abstraction)

Cold start: instantní (~3 sekundy), žádný Spark cluster.

Read CSV — Pandas

Killer feature pure Pythonu: přímý URL fetch funguje out-of-box, žádný mezikrok přes Files.

PYTHON
import pandas as pd

url = "https://opendata.chmi.cz/air_quality/now/data/airquality_1h_avg_CZ.csv"
df = pd.read_csv(url, skipinitialspace=True)  # CHMI má ", " — POZOR
print(df.shape)        # (~470, 4)
print(df.columns)      # idRegistration, startTime, idValueType, value
df.head()

df_local = pd.read_csv(
    "/lakehouse/default/Files/raw/chmi_snapshot.csv",
    skipinitialspace=True,
    dtype={"idRegistration": "Int32", "idValueType": "Int16"},
)
ℹ️
Pozn. cesta: v Pure Python notebook je Lakehouse mount na /lakehouse/default/Files/... — POSIX cesta, ne Files/... jako Spark.

Read CSV — Polars (rychlejší než Pandas)

PYTHON
import polars as pl

df = pl.read_csv(
    "https://opendata.chmi.cz/air_quality/now/data/airquality_1h_avg_CZ.csv",
    has_header=True,
    skip_rows_after_header=0,
    schema_overrides={"idRegistration": pl.Int32, "idValueType": pl.Int16, "value": pl.Float64},
)

df_multi = pl.read_csv("/lakehouse/default/Files/raw/chmi_*.csv")

Polars je v průměru 2–10× rychlejší než Pandas a používá výrazně méně paměti (Apache Arrow backend, lazy execution).

Read CSV — DuckDB (SQL-first)

PYTHON
import duckdb

con = duckdb.connect()

df = con.execute("""
    INSTALL httpfs;
    LOAD httpfs;
    SELECT *
    FROM read_csv_auto(
        'https://opendata.chmi.cz/air_quality/now/data/airquality_1h_avg_CZ.csv',
        delim=',',
        header=true
    )
""").df()

agg = con.execute("""
    SELECT
        idValueType AS pollutant_id,
        COUNT(DISTINCT idRegistration) AS pocet_stanic,
        AVG(value) AS prum_value,
        MAX(value) AS max_value
    FROM read_csv_auto(
        '/lakehouse/default/Files/raw/chmi_*.csv',
        header=true
    )
    GROUP BY idValueType
    ORDER BY prum_value DESC
""").df()
💡
DuckDB killer feature: zero-copy nad Parquet/Delta. Když máš data v Tables/, můžeš dotazovat bez načtení do paměti.

Common transforms — Pandas

PYTHON
df["ts_utc"] = pd.to_datetime(df["startTime"].str.rstrip("Z"), errors="coerce")
df["datum"] = df["ts_utc"].dt.date
df["hodina"] = df["ts_utc"].dt.hour
df = df.dropna(subset=["ts_utc", "value"])

df["over_no2_hourly"] = (df["idValueType"] == 8) & (df["value"] > 200)
df["over_pm10_24h"] = (df["idValueType"] == 5) & (df["value"] > 50)

Common transforms — Polars (lazy + expressions)

PYTHON
df_clean = (df.lazy()
    .with_columns([
        pl.col("startTime").str.strip_chars_end("Z").str.strptime(pl.Datetime, strict=False).alias("ts_utc"),
    ])
    .with_columns([
        pl.col("ts_utc").dt.date().alias("datum"),
        pl.col("ts_utc").dt.hour().alias("hodina"),
    ])
    .filter(pl.col("ts_utc").is_not_null() & pl.col("value").is_not_null())
    .collect())

Aggregations

PYTHON
agg = (df_clean
    .groupby(["datum", "idValueType"])
    .agg(pocet_stanic=("idRegistration", "nunique"),
         prum_value=("value", "mean"),
         max_value=("value", "max"))
    .reset_index())

agg = (df_clean
    .group_by(["datum", "idValueType"])
    .agg([
        pl.col("idRegistration").n_unique().alias("pocet_stanic"),
        pl.col("value").mean().alias("prum_value"),
        pl.col("value").max().alias("max_value"),
    ])
    .sort(["datum", "idValueType"]))

Write Delta — deltalake knihovna

Pure Python notebook nemá saveAsTable jako Spark. Použít deltalake:

PYTHON
from deltalake import write_deltalake

write_deltalake(
    "/lakehouse/default/Tables/ovzdusi_silver",
    df_clean.to_pandas() if isinstance(df_clean, pl.DataFrame) else df_clean,
    mode="overwrite",
    schema_mode="overwrite",
    engine="rust",  # default a doporučeno
)

write_deltalake(
    "/lakehouse/default/Tables/ovzdusi_silver",
    new_hourly_data,
    mode="append",
    engine="rust",
)
⚠️
Důležité: po zápisu přes deltalake musíš v Lakehouse Explorer refreshnout (3-tečky → Refresh) aby tabulka byla viditelná v SQL Endpoint a v Spark catalogu.

Write Delta — DuckDB s Delta export

PYTHON
con.execute("""
    COPY (
        SELECT * FROM df_clean
    )
    TO '/lakehouse/default/Files/silver/ovzdusi_silver.parquet'
    (FORMAT PARQUET, COMPRESSION ZSTD)
""")
from deltalake.writer import write_deltalake
import pyarrow.parquet as pq
table = pq.read_table('/lakehouse/default/Files/silver/ovzdusi_silver.parquet')
write_deltalake('/lakehouse/default/Tables/ovzdusi_silver', table, mode='overwrite')

V praxi: pro Delta výstup z DuckDB → čti přes deltalake rovnou. Nebo si DuckDB drž jen na čtení a write nech na Polars/Pandas.

2.6

Performance — co očekávat na různých kapacitách

Empirické řády velikosti pro CHMI snapshot (~470 řádků, 1 soubor ~ 18 KB) a 30-day rolling history (~340 K řádků):

Operace F2 PySpark F2 Pure Python F64 PySpark
Cold start 60–120 s 2–3 s 30–60 s
Read 1 snapshot 5–10 s <1 s (Pandas) 2–5 s
Read 30 days history (~340K řádků) 30–60 s 5–15 s (Polars) 8–20 s
GroupBy per kraj/polutant 10–20 s 0,5–2 s 2–5 s
Write Delta 15–30 s 5–10 s 5–10 s
Total end-to-end (snapshot) 2–3 min 8–15 s 45–60 s
Total end-to-end (30 days) 3–5 min 30–60 s 1–2 min
💡
Důsledek pro výuku: pro CHMI snapshot demo (470 řádků) je PySpark výrazný overkill. Pure Python je 10× rychlejší end-to-end. Použij PySpark jen pro pedagogickou demonstraci ("tady je proč Spark, na malých datech je to overhead, na velkých datech je to záchrana"). Pro skutečný demo workflow doporučuji Polars/Pandas.

Spark cold start využiješ jako učební moment — vykládej koncepty Spark architecture (Driver, Executors, Stages) zatímco session startuje.

2.7

Časté chyby a fixy

"ConnectionRefused" / "Lakehouse not attached"

Notebook nemá default Lakehouse. Klikni v levém panelu na ikonu Lakehouse → Add → Existing → vyber.

"UnicodeDecodeError 0x9a in position..." při čtení CSV

Encoding není UTF-8. Pro CZ vládní data zkus cp1250. Pro německá latin1. Pro mezinárodní utf-8-sig (BOM). Pozn.: CHMI je UTF-8, ale jiné CZ zdroje (např. Policie ČR / ČNB legacy) jedou na cp1250.

"KeyError" při přístupu ke sloupcům CSV

Sloupce mají leading whitespace (časté u CZ datasetů). Použij pd.read_csv(..., skipinitialspace=True) nebo df.columns = df.columns.str.strip().

Spark session timeout (po ~20 min nečinnosti)

Defaultně Spark session žije 20 min. Pro dlouhé pipeliny zvedni v Workspace Settings → Data Engineering → Pool → Session timeout. Nebo restartuj přes Stop Session → spustí se nová.

"FileNotFoundError" v Pure Python pro Files/...

Pure Python notebook používá POSIX cesty: /lakehouse/default/Files/.... Nepleť si se Spark cestami.

"Delta table not visible after write"

Po write_deltalake() v Pure Python: Refresh Lakehouse Explorer (3-tečky → Refresh). Spark write tohle dělá automaticky.

"Out of memory" na F2 Pure Python

F2 má pouze ~3 GB Python RAM. Pro >500 MB CSV použij streaming read:

PYTHON
for chunk in pd.read_csv("...", chunksize=100_000):
    process(chunk)

df = pl.scan_csv("...").filter(...).group_by(...).agg(...).collect()

"Spark startup takes forever on F2"

Není chyba, je to design. F2 má 2 CU pro Spark. Demo strategy: spusť ingest jako první akci, pak vykládej. Nebo přepni na Pure Python.

2.8

Decision tree — co použít, kdy

SCHÉMA
Začínám notebook v Fabricu, mám:
│
├─ Data <100 MB
│  └─ Pure Python notebook + Polars (rychlost) nebo Pandas (komfort)
│
├─ Data 100 MB – 5 GB
│  ├─ F2 / F4 kapacita
│  │  └─ Pure Python + DuckDB nebo Polars
│  └─ F8+ kapacita
│     └─ Buď, dle týmové znalosti
│
├─ Data 5–100 GB
│  └─ PySpark + V-Order optimalizace
│
└─ Data >100 GB nebo distribuovaný stream
   └─ PySpark s explicit partitioning
2.9

Vibecoding workflow — VS Code + Copilot pro Fabric notebooky

Pokud nepíšete PySpark denně (=většina lidí), je rozumné napsat notebook v IDE s AI asistencí a pak ho přenést do Fabricu. Setup:

Local setup

  1. VS Code s rozšířeními:

    • Jupyter (Microsoft) — spuštění .ipynb lokálně
    • GitHub Copilot (paid)
    • Python (Microsoft)
    • Pylance (Microsoft) — type hints, autocomplete
  2. Python environment: python -m venv .venv && pip install pyspark pandas polars duckdb deltalake

  3. Synapse Spark VS Code Extension (Microsoft) — umí připojit notebook na Fabric workspace přímo (od 2024 betaa). Není povinné — lze stáhnout z Fabric UI a editovat lokálně.

Workflow při přípravě demo

SCHÉMA
1. V VS Code: vytvoř .ipynb s názvem např. Demo_01_Bronze_PySpark.ipynb
2. Napiš první buňku: kontext (komentář co notebook dělá, jaké vstupy/výstupy)
3. Použij Copilot Chat (Ctrl+I): "Write PySpark code to read CSV with cp1250 encoding from Files/raw/, semicolon delimiter"
4. Copilot vygeneruje boilerplate. Validuj manuálně — Copilot často zapomíná na encoding.
5. Iteruj: "Add aggregation per kraj/year/severity"
6. Když je kód funkční lokálně (nad sample CSV), upload do Fabric workspace přes Workspace → New → Import notebook
7. Re-run v Fabricu, ověř že běží na Spark session

Pitfalls vibecodingu — co Copilot dělá špatně

  • Encoding: zapomíná na cp1250 pro CZ data. Vždy doplnit ručně.
  • Halucinace názvů metod: např. spark.read.csv(encoding=...) vs .option("encoding", ...) — někdy plete API.
  • Outdated Spark API: generuje Spark 2.x style (např. sqlContext) místo Spark 3.x.
  • Naivní agregace: používá .collect() zbytečně — to stahuje data na Driver, OOM risk.
  • Type casting: cast("int") vs cast(IntegerType()) — někdy mixuje.

Pravidlo: Copilot navrhuje, vy validujete. Vždy projděte vygenerovaný kód, než ho commitnete / runnete na production data.