# Joppe — Pandas Bestandsformaat-Converter Een klein demo-programma dat een databestand inleest in een Pandas DataFrame en het terug wegschrijft in een ander formaat. Gemaakt om een Python-ontwikkelaar te tonen hoe Pandas formaatconversie omvormt tot een tweetraps-pijplijn: ```text bestand op schijf ──[lezer]──▶ DataFrame ──[schrijver]──▶ bestand op schijf ``` Ondersteunde formaten (automatisch gekozen op basis van de bestandsextensie): | Extensie | Formaat | Pandas-lezer | Pandas-schrijver | | -------- | ------------------------------- | --------------- | ---------------- | | `.txt` | Tekst met vaste breedte | `pd.read_fwf` | zelf gemaakt | | `.csv` | Kommagescheiden waarden | `pd.read_csv` | `df.to_csv` | | `.xlsx` | Excel-werkmap | `pd.read_excel` | `df.to_excel` | | `.json` | JSON (records) | `pd.read_json` | `df.to_json` | | `.pkl` | Python pickle | `pickle.loads` | `pickle.dumps` | | `.py` | Python-snippet (alleen uitvoer) | — | zelf gemaakt | ## uv — Pakketbeheer voor Python [uv](https://docs.astral.sh/uv/) is een moderne, razendsnel pakketbeheerder voor Python. Het vervangt de combinatie van `pip`, `venv` en `pip-tools` met één enkel gereedschap. ### uv installeren **Linux / macOS:** ```bash curl -LsSf https://astral.sh/uv/install.sh | sh ``` **Windows (PowerShell):** ```powershell powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" ``` **Via pip (als je al een werkende Python hebt):** ```bash pip install uv ``` Controleer de installatie: ```bash uv --version ``` ### Nieuw project aanmaken ```bash uv init mijn-project cd mijn-project ``` `uv init` maakt automatisch aan: | Bestand | Inhoud | | ------------------ | ---------------------------------------------------- | | `pyproject.toml` | Projectnaam, Python-versie, dependencies | | `.python-version` | Gewenste Python-versie (bv. `3.14`) | | `main.py` | Leeg startbestand | | `README.md` | Leeg documentatiebestand | ### Libraries toevoegen ```bash uv add pandas openpyxl ``` `uv add` doet drie dingen tegelijk: 1. Voegt de library toe aan `dependencies` in `pyproject.toml`. 2. Bepaalt compatibele versies en schrijft ze vast in `uv.lock`. 3. Installeert alles in de virtuele omgeving van het project (`.venv/`). Een library verwijderen gaat met: ```bash uv remove openpyxl ``` Een specifieke versie pinnen: ```bash uv add "pandas>=2.0,<4" ``` ### pyproject.toml van dit project ```toml [project] name = "joppe" version = "0.1.0" requires-python = ">=3.14" dependencies = [ "openpyxl>=3.1.5", "pandas>=3.0.2", ] ``` `requires-python` garandeert dat uv een compatibele Python-versie kiest. `dependencies` bevat de minimale versievereisten; de exacte vergrendelde versies staan in `uv.lock`. ### Dit project installeren Clone de repository en voer daarna uit: ```bash uv sync ``` `uv sync` leest `uv.lock`, installeert de exacte versies en maakt `.venv/` aan als die nog niet bestaat. Alle collega's krijgen zo exact dezelfde omgeving. ### Scripts uitvoeren ```bash uv run python main.py sample.txt sample.csv ``` `uv run` zorgt automatisch dat de virtuele omgeving van het project actief is. Je hoeft `.venv/bin/activate` nooit handmatig te roepen. Wil je een gewone Python-REPL in de projectomgeving? ```bash uv run python ``` ## Gebruik ```bash uv run python main.py ``` Het programma kijkt naar de **invoer**-extensie om de lezer te kiezen, en naar de **uitvoer**-extensie om de schrijver te kiezen. Elke combinatie werkt. ## Voorbeelden Een voorbeeldbestand met vaste breedte, `sample.txt`, is meegeleverd om alle formaten te testen. Het bevat 5 rijen en 6 kolommen die een mix van datatypes dekken: integer (`id`, `quantity`), string (`name`), float (`price`), ISO-datetime met tijdzone (`created_at`) en datum in dd/mm/yyyy-formaat (`delivery_date`). ### 1. Vaste breedte → CSV ```bash uv run python main.py sample.txt sample.csv ``` ```csv id,name,price,quantity,created_at,delivery_date 1,Widget,9.99,10,2026-05-08T12:30:00+0000,2026-05-15T00:00:00+0000 2,Gadget,19.95,5,2026-04-12T07:00:00+0000,2026-05-20T00:00:00+0000 3,Sprocket,0.5,250,2026-03-22T07:15:00+0000,2026-06-10T00:00:00+0000 ``` Merk op dat `created_at` van `+02:00` (lokale tijd Brussel) naar `+0000` (UTC) is omgezet: de tijd schuift van 14:30 naar 12:30, maar het is hetzelfde absolute moment. Dat is de gouden regel — _opslaan in UTC_. ### 2. CSV → JSON ```bash uv run python main.py sample.csv sample.json ``` ```json [ { "id": 1, "name": "Widget", "price": 9.99, "quantity": 10, "created_at": "2026-05-08T12:30:00.000Z", "delivery_date": "2026-05-15T00:00:00.000Z" } ] ``` `date_format="iso"` is meegegeven aan `to_json` zodat datums leesbaar blijven in plaats van als Unix-milliseconden te verschijnen. ### 3. JSON → Excel ```bash uv run python main.py sample.json sample.xlsx ``` Excel-cellen kunnen geen tijdzone bevatten, dus het programma laat de tz-info vallen vlak voor het wegschrijven (de UTC-tijd zelf blijft behouden in de Excel-cel). ### 4. Excel → vaste breedte ```bash uv run python main.py sample.xlsx roundtrip.txt ``` In de uitgevoerde `.txt` staat `created_at` weer in ISO 8601 met offset en `delivery_date` weer in `dd/mm/yyyy` — elk in zijn eigen kolomstijl. ### 5. Naar pickle (.pkl) ```bash uv run python main.py sample.txt sample.pkl # of vanuit CSV, JSON, Excel — werkt allemaal uv run python main.py sample.csv sample.pkl ``` Het pickle-bestand bevat een lijst van gewone Python-dicts met standaard types (`int`, `float`, `datetime`). Daarna uitlezen: ```python import pickle with open("sample.pkl", "rb") as f: records = pickle.load(f) print(records[0]) # {'id': 1, 'name': 'Widget', 'price': 9.99, 'quantity': 10, # 'created_at': datetime(2026, 5, 8, 12, 30, 0, tzinfo=timezone.utc), ...} print(type(records[0]["id"])) # print(type(records[0]["created_at"])) # ``` Een pickle opnieuw inlezen als DataFrame: ```bash uv run python main.py sample.pkl sample.csv ``` ### 6. Naar Python-snippet (.py) ```bash uv run python main.py sample.txt sample.py # of vanuit CSV, Excel, JSON, pickle — allemaal ondersteund uv run python main.py sample.csv sample.py ``` Dit genereert een kant-en-klaar Python-bestand: ```python # Auto-gegenereerd door main.py from datetime import datetime, timezone DATA = [ { 'id': 1, 'name': 'Widget', 'price': 9.99, 'quantity': 10, 'created_at': datetime(2026, 5, 8, 12, 30, 0, tzinfo=timezone.utc), 'delivery_date': datetime(2026, 5, 15, 0, 0, 0, tzinfo=timezone.utc), }, ... ] ``` Je kunt dit bestand kopiëren in een project of direct importeren: ```python from sample import DATA for row in DATA: print(row["name"], row["created_at"]) ``` Datetimes zijn echte `datetime`-objecten (niet strings), zodat je ze meteen kunt gebruiken voor berekeningen of vergelijkingen. ### 7. Volledige rondreis Je kan de conversies aaneenschakelen om te controleren dat er niets verloren gaat: ```bash uv run python main.py sample.txt stap1.csv uv run python main.py stap1.csv stap2.json uv run python main.py stap2.json stap3.xlsx uv run python main.py stap3.xlsx stap4.pkl uv run python main.py stap4.pkl eind.py ``` ## Van DataFrame naar lijst-van-dicts De spil van de `.pkl`- en `.py`-uitvoer is `df.to_dict(orient="records")`. Dit zet elke rij van het DataFrame om naar één dict: ```python records = df.to_dict(orient="records") # [{"id": 1, "name": "Widget", ...}, {"id": 2, ...}, ...] ``` En terug de andere kant op: ```python df = pd.DataFrame(records) ``` ### De `orient`-parameter | orient | Resultaat | Gebruik | | ----------- | ------------------------------------------ | ------------------------------ | | `"records"` | `[{"col": val, ...}, ...]` | REST-API, fixtures | | `"dict"` | `{"col": {0: val, 1: val, ...}}` | kolom-opzoektabellen | | `"list"` | `{"col": [val, val, ...]}` | kolom-georiënteerde verwerking | | `"index"` | `{0: {"col": val}, ...}` | rij-index als sleutel | | `"split"` | `{"columns": [...], "data": [[...], ...]}` | compacte overdracht | Voor de meeste situaties is `"records"` de juiste keuze. ### Opgelet: NumPy/Pandas-types in dicts `to_dict()` levert soms `numpy.int64` of `pd.Timestamp` terug — die zijn **niet** JSON-serialiseerbaar en gedragen zich subtiel anders dan ingebouwde Python-types. Converteer ze expliciet: ```python import numpy as np def to_plain_python(row: dict) -> dict: result = {} for k, v in row.items(): if isinstance(v, pd.Timestamp): result[k] = v.to_pydatetime() # pd.Timestamp -> datetime elif isinstance(v, np.integer): result[k] = int(v) # numpy.int64 -> int elif isinstance(v, np.floating): result[k] = float(v) # numpy.float64 -> float else: result[k] = v return result records = [to_plain_python(r) for r in df.to_dict(orient="records")] ``` Dit programma doet dit automatisch in `df_to_records()` — zodat zowel pickle als het Python-snippet correcte standaard-types bevatten. ## Datums en tijdzones Het demo-bestand mengt twee veelvoorkomende datumformaten in dezelfde rijen: - `created_at` in ISO 8601 met tijdzone-offset, bv. `2026-05-08T14:30:00+02:00` (lokale Brusselse tijd). - `delivery_date` in dag-eerst notatie, bv. `15/05/2026` (geen tijd, geen tijdzone). Bij het inlezen normaliseert `parse_datetimes()` beide naar UTC met `pd.to_datetime(..., format="mixed", utc=True)`. De truc: - `format="mixed"` laat Pandas per rij raden welk formaat van toepassing is — handig wanneer dezelfde kolom meerdere stijlen bevat. - `utc=True` zorgt dat alles tijdzone-bewust is in UTC, zodat naïeve en bewuste timestamps niet door elkaar lopen. Voor de dd/mm/yyyy-kolom wordt extra `dayfirst=True` aangezet wanneer de waarden schuine strepen bevatten — anders zou Pandas `"01/02/2026"` als `2 januari` lezen in plaats van `1 februari`. ### De drie nuttige tijdzone-operaties ```python # 1. Parse + normaliseer naar UTC in één stap (altijd veilig). df["created_at"] = pd.to_datetime(df["created_at"], utc=True) # 2. Plak een tijdzone op een naïeve timestamp (je weet welke zone bedoeld is). df["created_at"] = df["created_at"].dt.tz_localize("Europe/Brussels") # 3. Reken om naar een andere tijdzone (zelfde moment, andere weergave). df["created_at"] = df["created_at"].dt.tz_convert("Europe/Brussels") ``` De gouden regel: **opslaan in UTC, weergeven in lokale tijd**. Roep operatie 3 alleen aan vlak voor het tonen of wegschrijven. ## Wat te bestuderen in `main.py` - **`read_any` / `write_any`** — de keuze op basis van de bestandsextensie. Dat is het volledige idee van "formaatconversie" in 10 regels. - **Argumenten van `pd.read_fwf`** — `colspecs`, `names`, en vooral `dtype`. Vaste-breedte bestanden bevatten geen type-informatie, dus je moet Pandas vertellen wat elke kolom is. - **`parse_datetimes`** — laat zien hoe je gemengde datumformaten (ISO én dd/mm/yyyy) in één pass omzet naar UTC-Timestamps. - **`df_to_records` + `_to_plain_python`** — het omzetten van DataFrame naar lijst-van-dicts met schone Python-types. - **`write_fwf`** — een uitgewerkt voorbeeld van uitvoer bouwen vanuit een DataFrame door over kolommen en rijen te itereren. Handig wanneer er geen ingebouwde schrijver bestaat voor je doelformaat. Bovenaan `main.py` staat een lange docstring die elke gebruikte lezer en schrijver doorloopt; lees die naast de code.