Erste öffentliche Fassung: Math Model Generator & Solver
Übersetzt natürlichsprachige Optimierungsaufgaben (Deutsch) per LLM in eine geprüfte Modellspezifikation, erzeugt Solver-Code und führt ihn in einer bubblewrap-Sandbox aus. Deterministische Prüfungen ohne LLM, Report als Markdown/HTML, Benchmark mit 19 verifizierten Fällen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
commit
b74445b8d6
1 changed files with 7655 additions and 0 deletions
89
data_profile.py
Normal file
89
data_profile.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
"""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 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)
|
||||
Loading…
Reference in a new issue