mirror of
https://github.com/migatu/astrololo.git
synced 2026-07-14 21:38:37 +00:00
16d35c16dc
Trzy niezależne usługi FastAPI komunikujące się przez HTTP/JSON, każda zna tylko adres warstwy bezpośrednio pod nią: - presentation (:8000) — strona WWW + formularz - logic (:8001) — reguły biznesowe, pośrednik - data (:8002) — wyszukiwanie danych za interfejsem DataProvider Warstwa danych: czytanie setek plików .xlsx z wykrywaniem nagłówka i mapowaniem układu kolumn na schemat kanoniczny, z 4-poziomowym cache (schemat L1, Parquet L2, wyniki zapytań L3, odwrócony indeks L4) i unieważnianiem po odcisku pliku. Gotowa ścieżka migracji do SQL (ingest/to_sql.py + SqlDataProvider, przełączane przez DATA_PROVIDER). Zawiera docker-compose, Makefile, generator danych przykładowych. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
53 lines
1.8 KiB
Python
53 lines
1.8 KiB
Python
"""Wykrywanie wiersza nagłówka w arkuszu.
|
|
|
|
Pliki Excela w tej domenie nie mają nagłówka zawsze w pierwszym wierszu — bywają
|
|
puste wiersze, tytuły, metadane. Ta heurystyka skanuje pierwsze N wierszy i
|
|
wybiera ten, który "wygląda jak nagłówek": dużo niepustych, tekstowych,
|
|
unikalnych komórek, po którym następują wiersze danych o podobnym wypełnieniu.
|
|
|
|
Wynik (indeks wiersza nagłówka) jest CACHE'OWANY per plik (schema_cache), więc
|
|
ten kosztowny skan robimy raz na wersję pliku.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pandas as pd
|
|
|
|
|
|
def _row_score(raw: pd.DataFrame, i: int) -> float:
|
|
row = raw.iloc[i]
|
|
non_null = row.notna()
|
|
filled = float(non_null.mean()) if len(row) else 0.0
|
|
if filled == 0:
|
|
return -1.0
|
|
|
|
values = [str(v).strip() for v in row[non_null].tolist()]
|
|
text_like = sum(1 for v in values if v and not _looks_numeric(v))
|
|
text_ratio = text_like / max(len(values), 1)
|
|
uniqueness = len(set(values)) / max(len(values), 1)
|
|
|
|
# Wiersze danych pod spodem powinny mieć podobną liczbę wypełnionych kolumn.
|
|
follow_bonus = 0.0
|
|
if i + 1 < len(raw):
|
|
below = raw.iloc[i + 1].notna().mean()
|
|
follow_bonus = 1.0 - abs(filled - float(below))
|
|
|
|
return filled * 0.4 + text_ratio * 0.3 + uniqueness * 0.2 + follow_bonus * 0.1
|
|
|
|
|
|
def _looks_numeric(v: str) -> bool:
|
|
try:
|
|
float(v.replace(",", "."))
|
|
return True
|
|
except ValueError:
|
|
return False
|
|
|
|
|
|
def detect_header_row(raw: pd.DataFrame, max_scan: int = 15) -> int:
|
|
"""Zwraca indeks (0-based) wiersza, który najprawdopodobniej jest nagłówkiem."""
|
|
best_idx, best_score = 0, float("-inf")
|
|
for i in range(min(max_scan, len(raw))):
|
|
score = _row_score(raw, i)
|
|
if score > best_score:
|
|
best_idx, best_score = i, score
|
|
return best_idx
|