Files
seppedl 1639f05e6e Voeg Pandas bestandsformaat-converter demo toe
Programma dat CSV, Excel, JSON, vaste-breedte tekst, pickle en
Python-snippets onderling converteert via een Pandas DataFrame.
Demonstreert dtype-afleiding, datetime-parsing met tijdzonebeheer
(UTC-normalisatie) en het omzetten naar een lijst-van-dicts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 10:47:23 +02:00

409 lines
12 KiB
Markdown

# 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 <invoerbestand> <uitvoerbestand>
```
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"])) # <class 'int'>
print(type(records[0]["created_at"])) # <class 'datetime.datetime'>
```
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.