1639f05e6e
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>
409 lines
12 KiB
Markdown
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.
|