text_to_solver_program/data/data_profile.py
dschlueter 7459690bf5 refactor: Pipeline-Stufen in Ordner mit ihren Prompts
Im Wurzelverzeichnis liegt nur noch TextToModel.py (Ablauf und CLI).
Jede Stufe liegt mit Code und Prompt in einem eigenen Ordner: core,
understanding, modeling, checks, solving, reporting, data, benchmark;
Tests unter tests/, Aufgaben unter tasks/. understand_task,
text_to_model/revise_model und review_assumptions/needs_work/select_best
sind aus TextToModel.py zu ihren Prompts gewandert, inhaltlich
unveraendert. Prompt-Kennung des Benchmarks bleibt 93744636095a.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 12:11:51 +02:00

89 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Profil beigelegter Datendateien – Zeilen, Spalten, Typen, Spannweiten, Lücken.
Bewusst **ohne LLM**: Das Fremdprojekt MM-Agent lässt eine Datenbeschreibung vom Modell schreiben
(`DataDescription.summary`). Gerechnet ist billiger, reproduzierbar und nicht halluzinierbar – und
das Profil ist damit selbst eine geprüfte Tatsache, auf die sich die späteren Stufen berufen dürfen.
"""
from pathlib import Path
import pandas as pd
from core.model_schema import ColumnProfile, DataFileProfile
SAMPLE_ROWS = 3 # so viele Beispielwerte je Spalte in den Prompt
MAX_COLUMNS = 40 # breitere Tabellen werden abgeschnitten, damit der Prompt nicht platzt
def _read(path: Path) -> pd.DataFrame:
"""CSV (mit Trennzeichen-Erkennung) oder Excel einlesen."""
if path.suffix.lower() in (".xlsx", ".xls"):
return pd.read_excel(path)
# sep=None + engine="python" erkennt Komma, Semikolon und Tabulator selbst
return pd.read_csv(path, sep=None, engine="python")
def _as_numbers(spalte: pd.Series) -> pd.Series:
"""Zahlen einer Spalte, auch bei deutscher Schreibweise ('1.234,56' als Text eingelesen)."""
zahlen = pd.to_numeric(spalte, errors="coerce")
vorhanden = spalte.notna().sum()
if vorhanden and zahlen.notna().sum() < vorhanden and pd.api.types.is_string_dtype(spalte):
deutsch = pd.to_numeric(
spalte.astype(str).str.replace(".", "", regex=False).str.replace(",", ".", regex=False),
errors="coerce")
if deutsch.notna().sum() > zahlen.notna().sum():
return deutsch
return zahlen
def _column(name: str, spalte: pd.Series) -> ColumnProfile:
zahlen = _as_numbers(spalte)
# Eine Spalte gilt als Zahlenspalte, wenn die große Mehrheit der belegten Werte Zahlen sind
vorhanden = max(int(spalte.notna().sum()), 1)
numerisch = zahlen.notna().sum() >= 0.8 * vorhanden and not pd.api.types.is_bool_dtype(spalte)
beispiele = [str(v) for v in spalte.dropna().unique()[:SAMPLE_ROWS]]
return ColumnProfile(
name=str(name),
dtype="Zahl" if numerisch else "Text",
missing=int(spalte.isna().sum()),
minimum=float(zahlen.min()) if numerisch and zahlen.notna().any() else None,
maximum=float(zahlen.max()) if numerisch and zahlen.notna().any() else None,
mean=float(zahlen.mean()) if numerisch and zahlen.notna().any() else None,
samples=beispiele,
)
def profile_file(path) -> DataFileProfile:
"""Eine Datei einlesen und ihr Profil bestimmen; unlesbare Dateien melden den Fehler."""
path = Path(path)
try:
tabelle = _read(path)
except Exception as exc: # kaputte Datei darf den Lauf nicht abbrechen
return DataFileProfile(name=path.name, rows=0, columns=[], error=f"{type(exc).__name__}: {exc}")
spalten = [_column(name, tabelle[name]) for name in list(tabelle.columns)[:MAX_COLUMNS]]
return DataFileProfile(name=path.name, rows=int(len(tabelle)), columns=spalten,
truncated_columns=max(0, len(tabelle.columns) - MAX_COLUMNS))
def profile_files(paths) -> list:
return [profile_file(p) for p in paths or []]
def to_prompt(profiles) -> str:
"""Das Profil als knapper Text für die Prompts – Spaltennamen sind das Wichtigste."""
if not profiles:
return ""
teile = ["BEIGELEGTE DATEN (deterministisch ermittelt, nicht geschätzt):"]
for p in profiles:
if p.error:
teile.append(f"- {p.name}: NICHT LESBAR ({p.error})")
continue
teile.append(f"- {p.name}: {p.rows} Zeilen, {len(p.columns)} Spalten"
+ (f" (weitere {p.truncated_columns} Spalten nicht gezeigt)" if p.truncated_columns else ""))
for s in p.columns:
spanne = (f", von {s.minimum:g} bis {s.maximum:g}, Mittel {s.mean:g}"
if s.minimum is not None else "")
luecken = f", {s.missing} fehlend" if s.missing else ""
teile.append(f" {s.name} ({s.dtype}{spanne}{luecken}): {', '.join(s.samples)}")
teile.append("Lies diese Dateien im Programm über ihren Dateinamen (sie liegen im Arbeitsverzeichnis). "
"Jede abgeleitete Kennzahl wird aus den Daten berechnet, nie geschätzt.")
return "\n".join(teile)