Compare commits
2 Commits
0046b8d695
...
faf6beb536
| Author | SHA1 | Date | |
|---|---|---|---|
| faf6beb536 | |||
| 73163de39c |
@@ -0,0 +1,93 @@
|
||||
# Data Cleanup Report — pgz_sport.klubovi
|
||||
**Datum:** 2026-05-05
|
||||
**Skripta:** `/opt/pgz-sport/scripts/cleanup_garbage_clubs.py`
|
||||
**Backup:** `pgz_sport.klubovi_backup_20260505` (2244 redaka snimljeno)
|
||||
|
||||
## Sažetak
|
||||
|
||||
| Kategorija | Brojka |
|
||||
|---|---|
|
||||
| Detektirani problemi | 23 |
|
||||
| Automatski ispravljeni | 18 |
|
||||
| Označeni za manualnu reviziju | 5 |
|
||||
| Failed (bez sugestije) | 0 |
|
||||
|
||||
## Kategorije problema
|
||||
|
||||
### 1. Adresa u polju `naziv` (14 odbojkaških klubova)
|
||||
Za svaki problemski klub adresa je premještena u polje `adresa`, naziv je dohvaćen iz `civic.entities` po preklapanju adrese. Gdje je više kandidata postojalo, ručno je odabran primarni klub na toj adresi (HOS roster cross-reference). Gdje nije bilo `civic.entities` zapisa, postavljen je heuristički naziv s prefiksom `[VERIFY]` i `metadata.manual_review=true`.
|
||||
|
||||
| ID | Stara vrijednost (sad u `adresa`) | Novi naziv | OIB postavljen | Izvor |
|
||||
|---|---|---|---|---|
|
||||
| 2613 | Trg Viktora Bubnja 1, 51000 Rijeka | Hrvatski Akademski Odbojkaški Klub "Rijeka" | 47139832980 | civic.entities#100700 (curated_pick) |
|
||||
| 2616 | Antona Raspora 8, Opatija 51410 | ODBOJKAŠKI KLUB OPATIJA VOLLEY | 83261523211 | civic.entities#398471 (single_match) |
|
||||
| 2618 | Zdravka Kučića 1, 51000 Rijeka | Muški Odbojkaški Klub "Gornja Vežica" | 35549440954 | civic.entities#82677 (curated_pick) |
|
||||
| **2619** | Vrh Čavje 31, Čavle, 51219 Čavle | `[VERIFY] Odbojkaški Klub Čavle` | — | heuristic — manual review |
|
||||
| 2622 | Čavja 47, 51219 Čavle | Odbojkaški Klub "Grobničan" | 55649998593 | civic.entities#63386 (single_match) |
|
||||
| 2624 | Stražnica 2, 51215 Kastav | Odbojkaški Klub "Kastav 1998" | 83495265520 | civic.entities#79270 (single_match) |
|
||||
| 2626 | Žuknica 1b, 51221 Kostrena | Odbojkaški Klub "Kostrena" Kostrena | 81511316706 | civic.entities#108081 (single_match) |
|
||||
| **2630** | 1. Istarske čete 3, 51410 Opatija | `[VERIFY] Odbojkaški Klub Opatija` | — | heuristic — manual review |
|
||||
| 2632 | Palit 365, 51280 Rab | Odbojkaški Klub "Rab" | 67434497493 | civic.entities#63647 (single_match) |
|
||||
| 2634 | Kosi 11/11, Marčelji, 51216 Viškovo | Odbojkaški Klub "Sveti Matej 06" - Viškovo | 19353575292 | civic.entities#119396 (single_match) |
|
||||
| **2636** | Sv. Križ 24, 51000 Rijeka | `[VERIFY] Odbojkaški Klub Rijeka` | — | heuristic — manual review |
|
||||
| 2638 | Mihačeva draga 13, 51000 Rijeka | Ženski Akademski Odbojkaški Klub Škurinje Rijeka | 43219260850 | civic.entities#93517 (single_match) |
|
||||
| **2641** | Kotorska 15a 8p.p.83), 51260 Crikvenica | `[VERIFY] Odbojkaški Klub Crikvenica` | — | heuristic — manual review |
|
||||
| 2643 | Cvetkov trg 1, 51000 Rijeka, 51216 Viškovo | Ženski Odbojkaški Klub "Drenova" Rijeka | 45039738493 | civic.entities#79268 (single_match) |
|
||||
|
||||
### 2. `naziv` jednak `grad` (8 boćarskih klubova) — automatski ispravljeno
|
||||
Heuristički prepended `Boćarski klub ` ispred imena grada. Sport je svakom bio `boćanje` i source_url je hrvatski-bocarski-savez.hr što potvrđuje obrazac.
|
||||
|
||||
| ID | Stari naziv | Novi naziv |
|
||||
|---|---|---|
|
||||
| 2578 | Kastav | Boćarski klub Kastav |
|
||||
| 2579 | Kostrena | Boćarski klub Kostrena |
|
||||
| 2582 | Krk | Boćarski klub Krk |
|
||||
| 2583 | Lovran | Boćarski klub Lovran |
|
||||
| 2584 | Opatija | Boćarski klub Opatija |
|
||||
| 2589 | Rijeka | Boćarski klub Rijeka |
|
||||
| 2590 | Hreljin | Boćarski klub Hreljin |
|
||||
| 2593 | Brod Moravice | Boćarski klub Brod Moravice |
|
||||
|
||||
### 3. Prazan `naziv` (1 klub)
|
||||
| ID | Akcija |
|
||||
|---|---|
|
||||
| 4426 | naziv prazan **i** grad prazan → označeno `[UNRESOLVED]` + `metadata.manual_review=true` |
|
||||
|
||||
### 4. Sportaši s e-mailom/telefonom u ime/prezime
|
||||
**0 zapisa pronađeno.** Schema je čista.
|
||||
|
||||
### 5. Drugi klubovi s adresom u nazivu (širi scan)
|
||||
Pronađena su 2 false positive (`HNK Orijent 1919 (Sušak)`, `Košarkaški Klub Matulji 2000 Matulji`) — godina osnutka u nazivu, **ne** popravljati.
|
||||
|
||||
## Manual review queue (5 zapisa)
|
||||
|
||||
Otvori panel klikom na link u app-u, provjeri na:
|
||||
- https://www.hos-cvf.hr/klubovi/
|
||||
- https://sport-pgz.hr/odbojkaski-savez-pgz
|
||||
- https://sudreg.pravosudje.hr/registar/oc/index.html (po adresi)
|
||||
|
||||
| ID | Trenutni naziv | Adresa | Razlog |
|
||||
|---|---|---|---|
|
||||
| 2619 | `[VERIFY] Odbojkaški Klub Čavle` | Vrh Čavje 31, Čavle | bez civic.entities pogotka |
|
||||
| 2630 | `[VERIFY] Odbojkaški Klub Opatija` | 1. Istarske čete 3, Opatija | bez civic.entities pogotka |
|
||||
| 2636 | `[VERIFY] Odbojkaški Klub Rijeka` | Sv. Križ 24, Rijeka | bez civic.entities pogotka |
|
||||
| 2641 | `[VERIFY] Odbojkaški Klub Crikvenica` | Kotorska 15a, Crikvenica | bez civic.entities pogotka |
|
||||
| 4426 | `[UNRESOLVED] empty naziv & grad — id 4426` | — | naziv i grad oba prazni |
|
||||
|
||||
## Opoziv (rollback)
|
||||
Ako bilo koji ispravak treba opozvati:
|
||||
```sql
|
||||
UPDATE pgz_sport.klubovi k
|
||||
SET naziv = b.naziv, adresa = b.adresa, oib = b.oib, metadata = b.metadata
|
||||
FROM pgz_sport.klubovi_backup_20260505 b
|
||||
WHERE k.id = b.id AND k.id IN (2613, 2616, 2618, ...);
|
||||
```
|
||||
|
||||
## Audit trail
|
||||
Sve izmjene su zapisane u `pgz_sport.klubovi.metadata` s ključevima `cleanup_at`, `cleanup_reason`, `cleanup_source`. Filtrirati može:
|
||||
```sql
|
||||
SELECT id, naziv, metadata->>'cleanup_reason'
|
||||
FROM pgz_sport.klubovi
|
||||
WHERE metadata ? 'cleanup_at'
|
||||
ORDER BY metadata->>'cleanup_at' DESC;
|
||||
```
|
||||
@@ -0,0 +1,149 @@
|
||||
{
|
||||
"started_at": "2026-05-04T23:27:09.643760+00:00",
|
||||
"problem_ids": [
|
||||
2613,
|
||||
2616,
|
||||
2618,
|
||||
2619,
|
||||
2622,
|
||||
2624,
|
||||
2626,
|
||||
2630,
|
||||
2632,
|
||||
2634,
|
||||
2636,
|
||||
2638,
|
||||
2641,
|
||||
2643
|
||||
],
|
||||
"fixed": [
|
||||
{
|
||||
"klub_id": 2613,
|
||||
"old_naziv": "Trg Viktora Bubnja 1, 51000 Rijeka",
|
||||
"new_naziv": "Hrvatski Akademski Odbojkaški Klub \"Rijeka\"",
|
||||
"oib_set": "47139832980",
|
||||
"civic_entity_id": 100700,
|
||||
"confidence": 0.9,
|
||||
"path": "curated_pick"
|
||||
},
|
||||
{
|
||||
"klub_id": 2616,
|
||||
"old_naziv": "Antona Raspora 8, Opatija 51410",
|
||||
"new_naziv": "ODBOJKAŠKI KLUB OPATIJA VOLLEY",
|
||||
"oib_set": "83261523211",
|
||||
"civic_entity_id": 398471,
|
||||
"confidence": 0.95,
|
||||
"path": "single_match"
|
||||
},
|
||||
{
|
||||
"klub_id": 2618,
|
||||
"old_naziv": "Zdravka Kučića 1, 51000 Rijeka",
|
||||
"new_naziv": "Muški Odbojkaški Klub \"Gornja Vežica\"",
|
||||
"oib_set": "35549440954",
|
||||
"civic_entity_id": 82677,
|
||||
"confidence": 0.9,
|
||||
"path": "curated_pick"
|
||||
},
|
||||
{
|
||||
"klub_id": 2622,
|
||||
"old_naziv": "Čavja 47, 51219 Čavle",
|
||||
"new_naziv": "Odbojkaški Klub \"Grobničan\"",
|
||||
"oib_set": "55649998593",
|
||||
"civic_entity_id": 63386,
|
||||
"confidence": 0.95,
|
||||
"path": "single_match"
|
||||
},
|
||||
{
|
||||
"klub_id": 2624,
|
||||
"old_naziv": "Stražnica 2, 51215 Kastav",
|
||||
"new_naziv": "Odbojkaški Klub \"Kastav 1998\"",
|
||||
"oib_set": "83495265520",
|
||||
"civic_entity_id": 79270,
|
||||
"confidence": 0.95,
|
||||
"path": "single_match"
|
||||
},
|
||||
{
|
||||
"klub_id": 2626,
|
||||
"old_naziv": "Žuknica 1b, 51221 Kostrena",
|
||||
"new_naziv": "Odbojkaški Klub \"Kostrena\" Kostrena",
|
||||
"oib_set": "81511316706",
|
||||
"civic_entity_id": 108081,
|
||||
"confidence": 0.95,
|
||||
"path": "single_match"
|
||||
},
|
||||
{
|
||||
"klub_id": 2632,
|
||||
"old_naziv": "Palit 365, 51280 Rab",
|
||||
"new_naziv": "Odbojkaški Klub \"Rab\"",
|
||||
"oib_set": "67434497493",
|
||||
"civic_entity_id": 63647,
|
||||
"confidence": 0.95,
|
||||
"path": "single_match"
|
||||
},
|
||||
{
|
||||
"klub_id": 2634,
|
||||
"old_naziv": "Kosi 11/11, Marčelji, 51216 Viškovo",
|
||||
"new_naziv": "Odbojkaški Klub \"Sveti Matej 06\" - Viškovo",
|
||||
"oib_set": "19353575292",
|
||||
"civic_entity_id": 119396,
|
||||
"confidence": 0.95,
|
||||
"path": "single_match"
|
||||
},
|
||||
{
|
||||
"klub_id": 2638,
|
||||
"old_naziv": "Mihačeva draga 13, 51000 Rijeka",
|
||||
"new_naziv": "Ženski Akademski Odbojkaški Klub Škurinje Rijeka",
|
||||
"oib_set": "43219260850",
|
||||
"civic_entity_id": 93517,
|
||||
"confidence": 0.95,
|
||||
"path": "single_match"
|
||||
},
|
||||
{
|
||||
"klub_id": 2643,
|
||||
"old_naziv": "Cvetkov trg 1, 51000 Rijeka, 51216 Viškovo",
|
||||
"new_naziv": "Ženski Odbojkaški Klub \"Drenova\" Rijeka",
|
||||
"oib_set": "45039738493",
|
||||
"civic_entity_id": 79268,
|
||||
"confidence": 0.95,
|
||||
"path": "single_match"
|
||||
}
|
||||
],
|
||||
"manual_review": [
|
||||
{
|
||||
"klub_id": 2619,
|
||||
"address": "Vrh Čavje 31, Čavle, 51219 Čavle",
|
||||
"suggested_name": "Odbojkaški Klub Čavle",
|
||||
"note": "Vrh Čavje 31, Čavle",
|
||||
"reason": "no civic.entities match — heuristic suggestion needs verification"
|
||||
},
|
||||
{
|
||||
"klub_id": 2630,
|
||||
"address": "1. Istarske čete 3, 51410 Opatija",
|
||||
"suggested_name": "Odbojkaški Klub Opatija",
|
||||
"note": "1. Istarske čete 3, Opatija",
|
||||
"reason": "no civic.entities match — heuristic suggestion needs verification"
|
||||
},
|
||||
{
|
||||
"klub_id": 2636,
|
||||
"address": "Sv. Križ 24, 51000 Rijeka",
|
||||
"suggested_name": "Odbojkaški Klub Rijeka",
|
||||
"note": "Sv. Križ 24, Rijeka — possibly OK Rijeka senior",
|
||||
"reason": "no civic.entities match — heuristic suggestion needs verification"
|
||||
},
|
||||
{
|
||||
"klub_id": 2641,
|
||||
"address": "Kotorska 15a 8p.p.83), 51260 Crikvenica",
|
||||
"suggested_name": "Odbojkaški Klub Crikvenica",
|
||||
"note": "Kotorska 15a, Crikvenica",
|
||||
"reason": "no civic.entities match — heuristic suggestion needs verification"
|
||||
}
|
||||
],
|
||||
"failed": [],
|
||||
"completed_at": "2026-05-04T23:27:09.776609+00:00",
|
||||
"summary": {
|
||||
"total": 14,
|
||||
"fixed": 10,
|
||||
"manual_review": 4,
|
||||
"failed": 0
|
||||
}
|
||||
}
|
||||
@@ -95,30 +95,46 @@ _SPORTAS_KEYS = ('sport','profile_url','slika_url','hns_igrac_id','biografija',
|
||||
'datum_rodenja','mjesto_rodenja','broj_dresa')
|
||||
|
||||
|
||||
def _coverage_expr(table_keys: tuple[str, ...]) -> str:
|
||||
"""Postgres expression that returns 0..100 coverage % for the row."""
|
||||
def _coverage_expr(table_keys: tuple[str, ...], prefix: str = '') -> str:
|
||||
"""Postgres expression that returns 0..100 coverage % for the row.
|
||||
|
||||
`prefix` is e.g. 'c.' when the SQL uses a table alias.
|
||||
"""
|
||||
parts = []
|
||||
for k in table_keys:
|
||||
parts.append(f"(CASE WHEN {k} IS NOT NULL AND ({k}::text) <> '' THEN 1 ELSE 0 END)")
|
||||
col = f"{prefix}{k}"
|
||||
parts.append(f"(CASE WHEN {col} IS NOT NULL AND ({col}::text) <> '' THEN 1 ELSE 0 END)")
|
||||
total = len(table_keys)
|
||||
return f"((({' + '.join(parts)})::numeric * 100) / {total})"
|
||||
|
||||
|
||||
def _pick_sportas(limit: int = 50) -> list[int]:
|
||||
"""Athletes with coverage<COVERAGE_MAX, randomly ordered."""
|
||||
cov = _coverage_expr(_SPORTAS_KEYS)
|
||||
"""Athletes with coverage<COVERAGE_MAX, randomly ordered.
|
||||
|
||||
Selection is sport-agnostic now: the router decides which federation to
|
||||
query based on c.sport (or klubovi.sport via the JOIN). We require either
|
||||
sport to be set on the row OR a known external linkage so we don't burn
|
||||
cycles on rows the router can't enrich.
|
||||
"""
|
||||
cov = _coverage_expr(_SPORTAS_KEYS, prefix='c.')
|
||||
sql = f"""
|
||||
SELECT id FROM pgz_sport.clanovi
|
||||
WHERE aktivan = TRUE
|
||||
SELECT c.id
|
||||
FROM pgz_sport.clanovi c
|
||||
LEFT JOIN pgz_sport.klubovi k ON k.id = c.klub_id
|
||||
WHERE c.aktivan = TRUE
|
||||
AND {cov} < %s
|
||||
AND (
|
||||
source IN ('hns_semafor','hns_family','manual','godisnjak')
|
||||
OR jsonb_exists(vanjski_id, 'hns_comet')
|
||||
OR (source_url ILIKE '%%semafor.hns.family%%')
|
||||
OR profile_url ILIKE '%%semafor.hns.family%%'
|
||||
c.sport IS NOT NULL
|
||||
OR k.sport IS NOT NULL
|
||||
OR c.source IN ('hns_semafor','hns_family','manual','godisnjak','hbs_savez','hks_savez')
|
||||
OR jsonb_exists(c.vanjski_id, 'hns_comet')
|
||||
OR (c.source_url ILIKE '%%semafor.hns.family%%')
|
||||
OR (c.profile_url ILIKE '%%semafor.hns.family%%')
|
||||
OR (c.source_url ILIKE '%%hrvatski-bocarski-savez.hr%%')
|
||||
OR (c.profile_url ILIKE '%%hrvatski-bocarski-savez.hr%%')
|
||||
)
|
||||
AND ((metadata->>'enriched_at') IS NULL
|
||||
OR (metadata->>'enriched_at')::timestamptz < now() - interval '7 days')
|
||||
AND ((c.metadata->>'enriched_at') IS NULL
|
||||
OR (c.metadata->>'enriched_at')::timestamptz < now() - interval '7 days')
|
||||
ORDER BY random()
|
||||
LIMIT %s
|
||||
"""
|
||||
@@ -179,9 +195,24 @@ def _http_post(path: str, body: dict | None = None) -> dict | None:
|
||||
# zajednica generic info, so we down-weight them so a plain DeepSeek synthesis
|
||||
# off a single sport-pgz.hr source falls below the gate.
|
||||
_SOURCE_WEIGHTS = {
|
||||
'semafor.hns.family': 0.95,
|
||||
'wikipedia.hr': 0.80,
|
||||
'sport-pgz.hr': 0.55,
|
||||
'semafor.hns.family': 0.95,
|
||||
'hrvatski-bocarski-savez.hr': 0.92,
|
||||
'hns-cff.hr': 0.90,
|
||||
'hks-cbf.hr': 0.90,
|
||||
'hrs.hr': 0.90,
|
||||
'hos-cvf.hr': 0.90,
|
||||
'hvs.hr': 0.90,
|
||||
'hps.hr': 0.90,
|
||||
'atletika.hr': 0.90,
|
||||
'htsavez.hr': 0.90,
|
||||
'judo-savez.hr': 0.88,
|
||||
'karate.hr': 0.88,
|
||||
'veslacki-savez.hr': 0.88,
|
||||
'gimnastika.hr': 0.88,
|
||||
'stolni-tenis.hr': 0.88,
|
||||
'kuglanje.hr': 0.88,
|
||||
'wikipedia.hr': 0.80,
|
||||
'sport-pgz.hr': 0.55,
|
||||
}
|
||||
# Fields that are safe to auto-write even from low-confidence sources because
|
||||
# they come from the entity's own structured page (URLs, IDs).
|
||||
|
||||
Reference in New Issue
Block a user