Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Connexion PostgreSQL utilisée par l'ETL (etl/database.py).
# Ces valeurs correspondent à l'instance locale définie dans docker-compose.yml.
# Copier ce fichier en `.env` : `cp .env.example .env`
PG_USER=postgres
PG_PWD=postgres
PG_DB=ipolitics
PG_HOST=localhost
PG_PORT=5432

# Mettre à True pour logguer toutes les requêtes SQL émises par SQLAlchemy.
PG_ECHO=False
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Project ignore
data/*
analysis/output/


# From https://github.com/github/gitignore/blob/main/Python.gitignore
Expand Down
205 changes: 201 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ Voici les différents endpoint:
* députés => `https://parlement.tricoteuses.fr/acteurs`
* votes => `https://parlement.tricoteuses.fr/scrutins`

L'API propose de nombreux endpoints.


[Information sur le chemin d'une loi](https://www.assemblee-nationale.fr/dyn/actualites-accueil-hub/le-parcours-de-la-loi)
Expand All @@ -35,10 +34,23 @@ L'API propose de nombreux endpoints.
- [Installation d'UV](https://docs.astral.sh/uv/)

### Setup

1. Installer les dépendances :
```bash
uv sync
```

2. Créer le fichier `.env` à partir de l'exemple, puis l'adapter si besoin :
```bash
cp .env.example .env
```

3. Démarrer PostgreSQL (instance locale définie dans `docker-compose.yml`, sur le port `5432`) :
```bash
docker compose up -d db
```
Les valeurs par défaut de `.env.example` correspondent à ce conteneur (`postgres`/`postgres`, base `ipolitics`).

### Usages

#### Exécuter des commandes
Expand Down Expand Up @@ -112,16 +124,201 @@ Voici une partie du fichier `./data/dossiers.json`

Je veux rajouter le champ `chambre` dans la DB et faire en sorte que l'ETL l'ajoute de lui-même.
1. Rajouter le champ dans le modèle
```
class User(Base):
```python
class Dossier(Base):
__tablename__ = "dossiers"

uid: Mapped[str] = mapped_column(primary_key=True)
titre: Mapped[str] = mapped_column(String(500))
titre: Mapped[str] = mapped_column(String(1000))
dataset: Mapped[int]
chambre: Mapped[str] = mapped_column(String(5)) # <-------- nouvelle colonne qui porte le même nom que le champ du fichier json
```

Le champ doit porter le même nom sinon l'ETL ne sera pas capable de le trouver.

2. Exécuter `just all`

## Objets parlementaires chargés


| Table | Endpoint | Contenu | Volume (législature 17) |
|---|---|---|---|
| `acteurs` | `/acteurs` | Députés / sénateurs (référentiel trans-législature) | ~3 100 |
| `organes` | `/organes` | Groupes politiques, commissions, assemblées… | ~5 400 |
| `mandats` | `/mandats` | Jointure acteur ↔ organe (appartenance + dates) | ~25 600 |
| `scrutins` | `/scrutins` | Scrutins publics et résultat agrégé (pour/contre/abstentions) | ~8 200 |
| `groupesVotants` | `/groupesVotants` | Résultat d'un scrutin ventilé par groupe politique | ~121 500 * |
| `documents` | `/documents` | Textes (projets/propositions de loi, rapports…) | ~4 500 |
| `auteursDocument` | `/auteursDocument` | Auteur(s) d'un document | ~20 000 |
| `coSignatairesDocument` | `/coSignatairesDocument` | Co-signataires d'un document | ~116 000 |

\* `groupesVotants` n'expose pas de filtre `legislature` : la table couvre toutes les législatures
(~98 300 lignes se rattachent à un scrutin de la L17, soit 12 groupes pour chacun des 8 192
scrutins concernés). Se scoper par jointure sur `scrutins`.

Les jointures se font par les colonnes `…RefUid` (références molles, nullable, indexées). Schéma
entité-relation ci-dessous — les boîtes ne montrent que les colonnes clés (PK + FK + quelques
champs parlants) ; la liste complète est dans les modèles `models/`.

```mermaid
erDiagram
dossiers {
string uid PK
string titre
string libelleProcedure
string statut
}
documents {
string uid PK
string dossierRefUid FK
string auteurPrincipalUid FK
text titrePrincipal
string classeLibelle
bool texteLoi
string dateDepot
}
amendements {
string uid PK
string acteurRefUid FK
string groupePolitiqueRefUid FK
string dossierRefUid FK
string documentRefUid FK
string scrutinRefUid FK
string numeroLong
string divisionArticleDesignation
text exposeSommaire
string sortAmendement
string dateDepot
}
acteurs {
string uid PK
string groupeParlementaireUid FK
string nom
string prenom
string chambre
bool actif
}
organes {
string uid PK
string codeType
string libelleAbrev
string positionPolitique
}
mandats {
string uid PK
string acteurRefUid FK
string organeRefUid FK
string typeOrgane
string libQualite
string dateDebut
string dateFin
}
scrutins {
string uid PK
string dossierRefUid FK
string documentRefUid FK
string amendementRefUid FK
string dateScrutin
text objet
string code
int pour
int contre
int abstentions
}
groupesVotants {
string uid PK
string scrutinRefUid FK
string organeRefUid FK
string positionMajoritaire
int pour
int contre
int abstentions
}
auteursDocument {
string uid PK
string documentRefUid FK
string acteurRefUid FK
string qualite
}
coSignatairesDocument {
string uid PK
string documentRefUid FK
string acteurRefUid FK
string dateCosignature
}

dossiers ||--o{ documents : "dossierRefUid"
dossiers ||--o{ amendements : "dossierRefUid"
dossiers ||--o{ scrutins : "dossierRefUid"
documents ||--o{ amendements : "documentRefUid"
documents ||--o{ scrutins : "documentRefUid"
documents ||--o{ auteursDocument : "documentRefUid"
documents ||--o{ coSignatairesDocument : "documentRefUid"
acteurs ||--o{ documents : "auteurPrincipalUid"
acteurs ||--o{ amendements : "acteurRefUid"
acteurs ||--o{ mandats : "acteurRefUid"
acteurs ||--o{ auteursDocument : "acteurRefUid"
acteurs ||--o{ coSignatairesDocument : "acteurRefUid"
organes ||--o{ acteurs : "groupeParlementaireUid"
organes ||--o{ amendements : "groupePolitiqueRefUid"
organes ||--o{ mandats : "organeRefUid"
organes ||--o{ groupesVotants : "organeRefUid"
amendements ||--o{ scrutins : "amendementRefUid"
scrutins ||--o{ amendements : "scrutinRefUid"
scrutins ||--o{ groupesVotants : "scrutinRefUid"
```

Le lien **amendement ↔ scrutin** est natif, et dans les deux sens : `scrutins.amendementRefUid`
pointe vers l'amendement tranché par le scrutin, et `amendements.scrutinRefUid` vers le scrutin
qui a tranché l'amendement. Le second est le plus large (~11 600 amendements contre ~6 800),
un même scrutin pouvant trancher plusieurs amendements identiques. La jointure reste clairsemée :
la plupart des amendements sont tranchés à main levée, sans scrutin public.

# Analyse : détection des mentions de collaboration externe

Objectif : repérer les amendements dont l'exposé sommaire déclare une collaboration ou une
inspiration avec une **entité externe** (lobby, syndicat, association, entreprise, fédération
professionnelle, ONG…) — formulations du type « travaillé avec… », « en concertation avec… »,
« inspiré de… ».

Le détecteur (`analysis/detect_mentions_regex.py`) fonctionne par expressions régulières :
déterministe, instantané et sans coût, il matche des familles de formulations calibrées sur
le corpus réel, avec des exclusions contextuelles (acteurs publics ou parlementaires, référents
textuels type « proposé par le texte ») pour limiter les faux positifs.

## Lancer une analyse

La base doit être alimentée au préalable (table `amendements`, voir les sections ETL ci-dessus).

```bash
# Tout le corpus, résultats en JSONL uniquement
just detect-mentions-regex

# + écriture des mentions dans la table amendement_mentions
just detect-mentions-regex --persist

# Sur un sous-ensemble
just detect-mentions-regex --limit 100

# Équivalent sans just :
uv run python -m analysis.detect_mentions_regex --persist
```

## Sorties

- **JSONL brut** : `analysis/output/mentions_regex.jsonl` (une ligne par amendement, dossier
gitignoré), plus un récap console des formulations rencontrées et de leur fréquence.
- **Base** (avec `--persist`) : table `amendement_mentions`, une ligne par mention détectée
(`amendementUid`, `citation`, `formulation`, `modele='regex:v1'`, `createdAt`). L'écriture est
idempotente par amendement et scopée au tag `modele='regex:v1'` : les lignes produites par
d'autres détecteurs (ex. un LLM) ne sont jamais touchées.
- Le repérage regex ne remplit ni `entite`, ni `typeEntite`, ni `externe` : identifier et
qualifier l'entité demande une analyse sémantique (prévue dans une itération ultérieure).

## Tables d'analyse et rebuild

`amendement_mentions` est une **table d'analyse** : elle n'est pas listée dans `ETL_TABLES`
(`etl/database.py`) et **survit donc à un `db-rebuild`**, contrairement à `amendements`/`dossiers`
qui sont détruites puis rechargées. Sa colonne `amendementUid` est une référence *molle* vers
`amendements.uid` (pas de `ForeignKey`), afin qu'aucune contrainte ne bloque le drop de la table
ETL. Pour ajouter une nouvelle analyse, créer un modèle sur ce principe (hors `ETL_TABLES`).
Empty file added analysis/__init__.py
Empty file.
Loading
Loading