Notebooky: PySpark vs Pure Python
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
Ú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
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ě
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ýš |
Společné pro všechny notebooky v Fabricu
Magic commands (cell-level)
%%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:
"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
Files/..., Tables/...). Plnou ABFSS uvést jednou jako "kdyby Lakehouse nebyl default".mssparkutils / notebookutils — bridge mezi Spark a Fabric servisem
mssparkutils je zastaralý alias — nové notebooky používají notebookutils (stejné API: notebookutils.fs, notebookutils.credentials, …). Příklady níže fungují s oběma.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)
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:
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()
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
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
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
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
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
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
(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"))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)
spark.sql("OPTIMIZE ovzdusi_silver") spark.sql("OPTIMIZE ovzdusi_silver VORDER") spark.sql("VACUUM ovzdusi_silver RETAIN 168 HOURS") # 7 dní default
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,pyarrowpolars(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.
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"}, )
/lakehouse/default/Files/... — POSIX cesta, ne Files/... jako Spark.Read CSV — Polars (rychlejší než Pandas)
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)
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()
Common transforms — Pandas
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)
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
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:
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", )
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
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.
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 |
Spark cold start využiješ jako učební moment — vykládej koncepty Spark architecture (Driver, Executors, Stages) zatímco session startuje.
Č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:
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.
Decision tree — co použít, kdy
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
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
-
VS Code s rozšířeními:
- Jupyter (Microsoft) — spuštění .ipynb lokálně
- GitHub Copilot (paid)
- Python (Microsoft)
- Pylance (Microsoft) — type hints, autocomplete
-
Python environment:
python -m venv .venv && pip install pyspark pandas polars duckdb deltalake -
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
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")vscast(IntegerType())— někdy mixuje.
Pravidlo: Copilot navrhuje, vy validujete. Vždy projděte vygenerovaný kód, než ho commitnete / runnete na production data.
Reference — kam dál
- Spark in Fabric: https://learn.microsoft.com/fabric/data-engineering/spark-compute
- Polars docs: https://docs.pola.rs/
- DuckDB Fabric integration: https://duckdb.org/docs/extensions/azure
- deltalake Python: https://delta-io.github.io/delta-rs/python/
- mssparkutils: https://learn.microsoft.com/fabric/data-engineering/microsoft-spark-utilities
- V-Order: https://learn.microsoft.com/fabric/data-engineering/delta-optimization-and-v-order
- Synapse VS Code Extension: Fabric Data Engineering VS Code extension