String(s) mit math. Formel evaluieren | Ansätze, Ideen, Vorschläge

mit matplotlib, NumPy, pandas, SciPy, SymPy und weiteren mathematischen Programmbibliotheken.
Antworten
tony_pythony
User
Beiträge: 4
Registriert: Sonntag 9. August 2020, 12:23

Hallo Zusammen,

ich mache (privat) schon seit 2 Monaten an einem Problem rum, und nun muss ich mir Hilfe holen. Ich glaube das Problem ist nicht ganz trivial und mir fehlen da Lösungsansätze - oder mangelt es einfach an der Logik?

Folgendes:
Ich habe mehrere Formeln der Art als Strings:

Code: Alles auswählen

result = sqrt(sma(delay(x, 15), 22) + sin(x) + wma(sma(y, 15), 62)) + sma(z, 2) 

- x, y, z sind Variablen, deren Werte in einer SQLITE Datenbank gespeichert werden. Die ganzen Werte sind als eine Zeitserie zu sehen - fortlaufend kommen da neue dazu und auch entsprechend neue Spalten, wobei Spaltenname = Variablenname. Tabellen-Index ist ein date Objekt. Der Index (value) wird zu einem fortlaufenden Zähler (key) in ein dict gemapt und ist (global) zugänglich. Variablen sind durch den jeweiligen String gegeben, also ohne Vorverarbeitung unbekannt / während Evaluation zur Laufzeit zu ermitteln.

- delay(), sqrt(), sma(), mma(), sin(),... sind Funktionen. Der Großteil der Funktionen kann von math importiert werden, aber diese delay(), sma(), mma() sind custom.
Grundsätzlich ist der Aufbau aller custom-Funktionen so:
func(var, int), mit var = Variablenname, int = positive Ganzzahl (könnte aber auch ein pos. float werden..)

Jede custom-Funktion hat einen Zweck:
- delay(var, 5) -> bezogen auf einen Startpunkt -> gehe 5 rows zurück (also, die 6te Reihe inkl. Startpunkt), Rückgabe: float/int
- sma(var, 5) -> simple moving average, bezogen auf einen Startpunkt -> geom. Durchschnitt der letzen 5 rows inkl dem Startpunkt, Rückgabe: float
- wma(var, 5) -> weighted moving average, bezogen auf einen Startpunkt -> gewichteter Durchschnitt der letzten 5 rows inkl. dem Startpunkt, Rückgabe: float

Startpunkt (start_point) ist als ein Zähler definiert, dann muss nicht die ganze Zeit mit den date-Objects hantiert werden. Sprich, über das dict und dem key kommt das date Objekt zurück, mit dem dann die DB Abfrage erfolgt.

Soweit so gut. Relativ einfaches Umsetzen der custom-Funktionen per

Code: Alles auswählen

 def func(var, int):
    new_start = start_point - int
    get_start_date(new_start)
    get_valueDB(var, start_date)
    ... do magic ...
    return value

Die Evaluierung habe ich dem eval() überlassen. Entsprechend globale und lokale Variablen und Funktionen sind mitgegeben. Das funktioniert soweit.

Stufe schwieriger an die obigen Beispiele angelehnt:

Code: Alles auswählen

res = sma(delay(x, 3), 5)
Das Ergebnis würde sein: "Geh vom Startpunkt 3 Reihen zurück, da angefangen, Mittelwert aus 5 Reihen."
Das habe ich auch hinbekommen. Die Funktion delay() liefert zurück den Wert "value" und einen delay_counter. Die Funktion sma() überprüft, ob ein delay_counter gesetzt ist und verwendet diesen entsprechend als (start_point - delay_counter) und führt dann die Datenabnkabfragen entsprechend durch. Damit werden auch ganz ganz komische Konstruktionen wie delay(delay(delay(var, 1), 2), 3) möglich (-> delay_counter = 6, (1+2+3)).

Noch eine Stufe schwieriger. Da hat alles angefangen:

Code: Alles auswählen

wma(sma(delay(x, 3), 4), 2)
Das Ergebnis soll sein: "Geh vom Startpunkt 3 Reihen zurück, da angefangen (Startpunkt_neu), bilde den Mittelwert aus 4 Reihen (inkl. Startpunkt_neu), nimm den Mittelwert, gewichte ihn, setze den Startpunkt_neu2 = Startpunkt_neu - 1 , bilde den Mittelwert aus 4 Reihen (inkl. Startpunkt_neu2), gewichte ihn, addiere die beiden gewichteten Mittelwerte, Rückgabe: gew. Mittelwert aus 2 Durchläufen der Funktion sma()...

Und da komm ich nicht weiter.
Mein erster Ansatz war alles in einer globalen Variable zu speichern und die Funktionen liefern nur die Werte, die dann in Funktionen wie sin() weiterverwendet werden. Daraus ist eine If-Hölle entstanden, die alles überprüft. Woher kam der Eingabewert (Funktionsrückgabe? oder einfacher String?), was ist schon mal wie oft durchlaufen, welche Werte sind vorhanden, zu welchem moving average gehören die?? usw.. *hände überm kopp zusammenschlag*

Mein zweiter Ansatz war mit func(var, in, *args) zu arbeiten und am Ende ein einigermaßen schlankes dict zu übergeben, das ständig weitergeschleppt wird. Die If-Hölle habe ich damit nur minimal verkleinert.

Und dann kam es durch einen Tippfehler zufällig zur folgenden Situation und mein Kopf ist vollends explodiert:

Code: Alles auswählen

wma(sma(delay(x, 3), 4), [b]6[/b])
Da ist mir zum ersten mal bewusst geworden, dass sma(delay(x, 3), 4) innerhalb der Funktion wma(..., 6) sechs mal durchlaufen werden muss, die eval() Funktion aber von innen-nach-außen arbeitet. Das heisst, bevor wma() nicht ausgeführt wird, ist nicht bekannt, wieviele Rückgabewerte erwartet werden!

Um noch eine logische Schwierigkeit draufzusetzen:

Code: Alles auswählen

delay(wma(sma(delay(x, 3), 4), 6), 2)
das äußere delay(..., 2) setzt den Startpunkt auf t -2, dann kommt das innere delay(x, 3) und setzt den Startpunkt nochmals auf t (-2) + (-3) = t -5, und dann erst kommen die ganzen Mittelwertbildungen vom Startpunkt -5 anfangend. Mit eval() fängt der aber auch innen an und weiss bis zum Schluss nicht, dass da nochmal ein delay kommt..

Wie geht man hier am besten vor?
Benutzeravatar
sparrow
User
Beiträge: 4195
Registriert: Freitag 17. April 2009, 10:28

Das klingt als würdest du an Dynamik scheitern.

Es fängt schon damit an, dass du Dinge in einer Relationalen Datenbank speicherst und dabei daw Design einer Tabelle veränderst. Dafür sind diese Art von Datenbanken nicht gemacht. Das sind keine Tabellenkalkulationen. Benutze Relationen oder eine passende Datenbank.

Und das Problem der Dynamik zieht sich weiter.
Es gibt nur sehr wenige Situationen in denen eval sinnvoll ist. Ich sehe den auxh hier nicht. Schreib entsprechende Funktionen. Entweder müssen die immer die Daten und die aktuelle "Postion" mit übergeben bekommen (uns die nicht globalmagisch herzaubern) oder du nimmst eine Klasse.
tony_pythony
User
Beiträge: 4
Registriert: Sonntag 9. August 2020, 12:23

Hallo sparrow,

danke dir.
Das mit der sqlite db klappt eigentlich ganz gut. Ich hab mich vielleicht falsch ausgedrückt. Das Befüllen der db und das Herauslesen der Werte zur Evaluierung mit der Formel sind zwei verschiedene Vorgänge, die nicht gleichzeitig stattfinden.
Ich werde mich nochmals befassen, ob vielleicht ein Teil der Berechnung an die DB engine ausgelagert werden kann / Relationen bilde, oder wie meintest du das mit Relationen? Weil, die Daten sind sehr "einfach" gestrickt. Ich habe nix zum joinen, keine views oder Schnittmengen bilden.. etc..

*Tabelle verändern heisst in meinem Anwendungsfall Spalten hinzufügen? Ja, da muss ich dir recht geben, da wird am db design rumgepfuscht. Ist alles neu-land für mich, aber bin genetisch vorprogrammierter Problemsucher ;)

Was jetzt gestern noch gemacht wurde:
Ich hatte bei jedem Funktionsaufruf und auch teilweise innerhalb von Schleifen db Zugriffe gestartet, um die Werte zu holen. Das hat das ganze echt langsam gemacht. Nun ziehe ich einfach die Werte der benötigten Spalte vorab in eine liste und arbeite mit der dann weiter.
Zu diesem Zweck hatte ich auch pandas dataframe angeschaut, aber momentan bin ich informationstechnisch überflutet und seh den Wald vor lauter Bäumen nicht :(

Den Sinn vom eval() habe ich mir so hergeleitet:
Ein string kann nicht einfach un-string-ed werden, sodass aus einem String der Form "func(func(func))" die Funktionsaufrufe einfach gestartet werden. Alternativen waren ein vorheriges:
- compile() und exec() -> compile() gibt ein AST, das eventuell weiterverwendet werden kann
- python's parse() und exec() -> parse() gibt ein parser-tree, das ebenfalls eventuell weiterverwendet werden kann
Oder fertige parser:
- pyparsing -> parsen und evaluieren, die infix Schreibweise kann beibehalten werden und böse chars (^ = **) und operator precedens ist alles gegeben.
- cexprtk als parser und evaluator
- nltk, pandas, sympy und andere modul-pakete zum parsen und evaluieren

Vom pyparsing hab ich mir abgeschaut, dass ein String als "value" in einem dictionary gespeichert wird, und bei Aufruf des dict['key'] eben die Auswertung der Funktionen startet. In dem Fall muss der String erst "sanitized" werden. Bin mir echt nicht sicher, ob das so geht, aber, wenn, dann kann das eval() tatsächlich wieder raus und ich benötige auch keinen parser.

Aber, das Problem besteht eigentlich trotzdem immer noch. Keines der Ansätze oben bietet "von Haus aus" eine links-nach-rechts Auswertung. Sympy hat zwar was eingebaut (preorder_traversal()), aber ich verstehe das sympy framework und was es von mir möchte nicht.

Momentan gebe ich mir die Infos zum AST und NodeVisitor/NodeTransformer - mit dem könnte man irgendwie einen pre-order-traversed AST bauen und eventuell schon alle Verweise, Positionsänderungen, etc, direkt in die Funktionen "infusen" - denk ich mir.

Dein Vorschlag mit Klassen. Da stehe ich immer noch vorm Problem der richtigen Funktionsaufruf-Reihenfolge - ich sehe echt nicht, wie Klassen die Situation entschärfen "ein "Positions"- Argument an die bereits ausgeführte(n) Funktion(en) zu übergeben und diese dann mit der neuen Position nochmals alles berechnen lassen". Kannst du vielleicht ein kleines Beispiel stichwortartig/pseudocode geben? Klassen-konzept verstehe ich schon seit meiner Jugend nicht und habe leider auch nicht den informationstechnischen Weg begangen ;(

Und dann.. gibts da noch sowas :)
https://pyvideo.org/pycon-us-2012/stop- ... asses.html (video)
https://fixes.co.za/python/stop-writing-classes/ (Text)

Alternativ - nur eine Idee - könnte man von "innen" heraus arbeiten und mit dem return der Funktionen auch den ursprünglichen Funktionsaufruf mit zurückgeben, sodass die nächste Funktion (bei Bedarf) nochmal die aktuelle Funktion aufruft..zum Beispiel:

Code: Alles auswählen

Formel: sma(wma(delay(var, 2), 3), 5) 
1. delay(var, 2) 
        pos = start - 2
        value = ...
    |---> (value, "delay(var, 2)", pos)
2. wma((value, "delay(var, 2), pos"), 3):
        pos =  pos  
        for i in range(0, 3):
            value = delay(var, pos - i) 
            ...value_i weighting..
            ...
    |----> (value, "wma(delay(var, 2), 3), pos")
3. sma((value, "wma(delay(var, 2), 3), pos"), 5):
        pos = pos
        for i in range(0, 5):
            wma(delay(var, pos -i), 3) 
            ...
    |----> (value, "sma((value, "wma(delay(var, 2), 3)"), 5)")

Aber, da rechnet der sich doch zu Tode und durchläuft immer wieder die gleichen Funktionen. Memoization würde eventuell helfen (???), aber da jeder Funktions-re-aufruf zwangsweise Positionsänderungen benötigt, ist es ja keine "gleiche" Berechnung??? Hmmmmmm... habe ich hierbei ein Denkfehler???? Muss zugeben, Memoization ist schnell erklärt, aber meinerseits sehr langsam verstanden ;(
Antworten