Code Review - Datenbank Zugriff

Installation und Anwendung von Datenbankschnittstellen wie SQLite, PostgreSQL, MariaDB/MySQL, der DB-API 2.0 und sonstigen Datenbanksystemen.
Antworten
OWT
User
Beiträge: 29
Registriert: Montag 20. Juni 2022, 10:45

Guten Tag Zusammen

Ich bin eher neu und dilettantisch mit Python am Weg und stecke grad in meinem ersten Datenbankprojekt. Daher dachte ich, ich Teil meinen Zugriffs Code mit euch, vielleicht findet ja der ein oder andere etwas, was man besser machen könnte. Prinzipiell geht es darum, dass ich Daten von Datenloggern hole, jeweils Messpunkt und Zeitpunkt. Die Verbindung wird mit pyodbc aufgebaut.

Code: Alles auswählen

Logger_ID = [23,55,90,100]
alle_messdaten = dict()
for k in Logger_ID:
    messwerte = []
    zeitpunkte = []
    rows = cursor.execute(sql, [k]) #Holt Wert und Zeit für den ersten Logger
    for row in rows:
        messwerte += [row[0]]
        a = datetime.datetime.fromtimestamp(row[1]/1000)
        zeitpunkte += [a]
    header1 = str(k)+" MPs"
    header2 = str(k)+" Zeit"
    alle_messdaten[header1] = [messwerte]
    alle_messdaten[header2] = [zeitpunkte]
(print('fertig'))
Eigentlich hätte ich das ganze gern als Pandas Dataframe mit Zeit als Index und nur 6 Spalten, aber ich glaub nicht das ich das hinbekomme.

Schönes Wochenende
OWT
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Wie oft eigentlich noch: man benutzt append, statt eine Einelemente Liste zu erzeugen. Ich frage mich, was es Dir und mir nützt, hier was zu schreiben, kann man gleich sein lassen. Formatstrings wurden ein Deinem anderen Thread hier auch schon angesprochen.
`messwerte` und `zeitpunkte` sind schon Listen, die in eine Einelementige Liste zu packen ist wenig sinnvoll.
Einen print-Aufruf zu klammern ist auch überflüssig.
Haben denn alle Logger-ID-Werte die selben Timestamps? Oder wie soll daraus ein Zeit-Index werden?
Wie kommst Du auf 6 Spalten?
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@OWT: `Logger_ID` hält sich nicht an die Namenskonventionen und ist inhaltlich falsch, weil es nicht eine Logger-ID ist, sondern mehrere.

Der Kommentar ist inhaltlich falsch, weil der nur für den ersten Schleifendurchlauf zutrifft.

Die Ergebnisdatenstruktur ist sinnlos weil man die nicht wirklich gebrauchen kann in der Form, weil es unsinnig schwierig ist, da die zusammengehörenden Daten wieder raus zu holen.

Sowohl aus Performance-Gründen, als auch wenn man das in einen Pandas-Dataframe packen will, würde es mehr Sinn machen gleich *alle* Daten in der ”sauberen” Langform abzufragen und dann den Dataframe entsprechend umzuformen. Die Rechnung aus den Tausendstelsekunden Sekunden zu machen und in pandas.timestamp Werte umzuwandeln, würde ich auch nicht manuell mit jedem einzelnen Wert machen. Dafür hat man ja Pandas.

Und dann wären wir auch schon wieder bei SQLAlchemy, denn sich eine SQL-Abfrage mit einer IN-Bedingung als Zeichenkette zu basteln finde ich ziemlich nervig.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
OWT
User
Beiträge: 29
Registriert: Montag 20. Juni 2022, 10:45

Hallo Zusammen

Danke fürs Feedback, ich werde die "unsauberen" Formulierungen anpassen. Auch das hin und her rechnen mit den Zeiten wird nochmal angeschaut, vor allem da es im Pandas DataFrame frisch wieder als datetime.datetime(...) abgelegt ist. Wie müsste ich das machen, einfach row[1] als Integer in die Liste Zeitpunkte schreiben oder doch zuerst durch 1000 dividieren und irgendwie als "Zeit" definieren?

Bezüglich der Ergebnisstruktur: Es war tatsächlich etwas lästig, die Ergebnisse anschließend wieder aus dem "Dictionary" raus zu holen. Am Schluss musste eine weitere Liste "Headers" her, damit die Einträge im Dict wieder gerufen werden konnten. Mir ist aber neben dem Dict nichts eingefallen, um auf einmal an alle Daten zu komme. Gibt es eine Möglichkeit um bsp. Logger_IDs[0] als Name für eine neue Liste zu definieren? Wären das dann dynamische Listen?

Code: Alles auswählen

 #nicht funktionierender Code, als beispiel was ich mir vorstelle
Logger_IDs =  [23,55,90,100]

Logger_IDs[0] = messwerte_logger_23
Logger_IDs[1] = messwerte_logger_55
usw.
print('23') --> messwerte_logger_23
Blackjack, meinst du mit "saubere Langform", dass alle Messwerte gleichzeitig geholt werden. Dann müsste man zusätzlich die Spalte "logger id" holen, was bedeutet, dass der Datensatz um 50% größer wird (von 2 auf 3 Spalten).Ich weiß nicht, ob das besser wäre, aber aufgrund der Datenmenge hab ich mich dagegen entschieden.

Was ist eine IN-Bedingung? Ich vermute mal die Liste "Logger_IDs". Wäre es bei SQLAlchemy am Schluss nicht auch so, dass ich per Hand eingebe, welche Logger mich interessieren? Vielleicht liegt es daran, dass ich relativ neu mit Python arbeite, aber mir gefällt dieses "man sieht genau was gemacht wird" extrem gut, das ist vermutlich auch ein Grund warum ich "+=" einem append vorziehe :)

Ich wünsche einen schönen Start in die Woche
OWT


ps.: @Sirius, leider tue ich mir schwer mit deinem Feedback, sowohl beim verstehen, wie beim annehmen.
Bis jetzt habe ich drei Argumente für append gehört, von denen keines überzeugend war. 1) das macht man so; 2) append ist weniger typen sensitiv; 3) es wurde schon gesagt, dass man append verwendet. Ich lade dich herzlich ein, ein weiteres Argument für append zu bringen, aber du bist auch herzlich eingeladen zu akzeptieren, dass ich "+=" sehr toll finde :)
Die einelementigen Listen sind notwendig, da sonst der "+=" Operator etwas falsches macht! Alternativ könnte man hier append() verwenden. 'Print' ohne Klammern führt bei mir leider zu folgendem Fehler: SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)? Im Beispiel Code waren 4 Logger angegeben, und anschließend habe ich von 6 Spalten geschrieben, das war verwirrend, sry, es hätte 4 Spalten heißen sollen.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@OWT: Wörterbuch ist schon richtig aber halt nicht so wie Du das gemacht hast da Schlüssel mit zwei verschiedenen Bedeutungen rein zu stecken, die man dann am Ende wieder auseinander puzzeln muss. Der Schlüssel sollte die Logger-ID sein und zu allen Daten führen die zu diesem Logger gehören, und nicht Schlüssel die aus Logger-ID und Datumsart zusammengesetzt sind, und dann nur zu den Daten von dieser Art führen. Das macht nur Sinn wenn man Logger-ID und Datumsart auch wirklich so als Schlüssel braucht, nicht wenn man am Ende nur über die Logger-ID zu allen Daten kommen will.

Ja die „saubere Langform“ — Spalten entsprechen Variablen und Zeilen einer Messung/Beobachtung entspricht im groben einer normalisierten Tabelle in relationalen Datenbanken und da ist die Variable Logger-ID eine Spalte, und nicht eine Spalte pro Wert den Logger-ID annehmen kann.

Da würde ich Speicher erst einmal aussen vor lassen, solange das konstant oder linear mehr Speicher ist und kein tatsächlich messbares Problem, und erst einmal Wert auf verständlichen, nicht unnötig komplexen Code legen. Insbesondere bei Numpy und Pandas kann ”optimieren” auch gegenteilige Effekte haben, weil die ja für grössere Datenmengen ausgelegt sind und eine interne Operation von Numpy oder Pandas ist in der Regel schneller als wenn man sich Teile davon ”aussen” in Python nachbastelt. Darum verwendet man ja Numpy & Co. Selbst wenn man am Ende die „Breite“ Form statt der Langform haben möchte, würde ich diese Umformung mit Pandas machen, denn sonst muss man ja auch von Hand noch das auffüllen von Zellen machen bei denen Werte fehlern, und dann wahrscheinlich für jeden Logger den Datensatz erneut mit dem schon vorhandenen zusammenführen, was am Ende mehr Speicher und Rechnenleistung brauchen kann, als wenn man die ensprechenden Pandas-Operationen dafür verwendet.

IN ist SQL und der Unterschied zwischen per Hand SQL als Zeichenkette basteln, und das SQLAlchemy machen zu lassen, ist dass man bei SQLAlchemy einfach die Liste mit den Logger-IDs angibt und das war's. Bei SQL als Zeichenkette muss man eine der länge der Liste entsprechende Anzahl von Platzhaltern erstellen und die Liste an passenden Stelle in die Liste der Werte beim `execute()` ”flach” einbauen. Also nicht die Liste als Elemente, sondern die Elemente der Liste einzeln mit den anderen Werten. Das macht mehr Arbeit und ist ein bisschen fehleranfälliger, weil man erst die Liste der Platzhalter in der Zeichenkette mit dem SQL hat, und dann eine Liste der Werte, und beides muss in Anzahl und Reihenfolge zusammen passen.

``+=`` macht ja gerade nicht das was man da eigentlich machen will, sondern ist umständlicher und hat am Ende nur den gleichen Effekt. Was auch noch undurchsichtig und überraschend ist, denn eigentlich erwartet man ja, dass ``a += b`` äquivalent zu ``a = a + b`` ist, was in diesem Fall auch nicht stimmt, denn es ist bei Listen äquivalent zu ``a.extend(b)``. Was auch der Grund ist warum ich selbst wenn ich diesen Effekt haben will, nicht ``+=`` sondern die Methode verwende, die tut was ich dann will und verwirrt auch nicht, weil man bei ``+=`` eigentlich eine andere Semantik erwartet hätte.
Das ``+=`` nicht das macht was Du willst, sieht man ja am nächsten Argument, dass die einelementigen Listen nicht unnötig seien, weil ``+=`` sonst nicht macht was Du willst. Sie sind eben doch überflüssig weil ``+=`` hier die falsche Operation ist. `append()` ist das was gewollt ist, und es sagt das auch ganz klar und deutlich was es macht. Im Gegensatz zu ``+=``, was auf Listen verwirrend implementiert ist.

Das macht man so, und das machen alle so, ist übrigens ein valides Argument. Konventionen und idiomatischer Code tragen zur Kommunikation und zum leichteren Verständnis von Code bei. Das ist ein handfester Mehrwert und nicht reine Kosmetik oder Dogma.

Hinterfragen ist okay und wichtig, dabei lernt man was, aber wenn man Leuten die helfen wollen, immer erst mal ein „okay die Leute die das schon lange machen, haben gesagt das macht man anders, aber ich mache trotzdem so weiter bis mich da jemand an die Hand genommen und 100% überzeugt hat“, insbesondere bei so einfachen Sachen wie `append()` statt ``+=``, also wo eine Änderung nichts kostet, führt das irgendwann dazu das keiner mehr Lust hat zu helfen.

Übrigens waren gar nicht die überflüssigen einelementigen Listen beim ``+=`` gemeint, sondern das hier:

Code: Alles auswählen

    alle_messdaten[header1] = [messwerte]
    alle_messdaten[header2] = [zeitpunkte]
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Benutzeravatar
Dennis89
User
Beiträge: 1124
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,
OWT hat geschrieben: Montag 27. Juni 2022, 10:39 'Print' ohne Klammern führt bei mir leider zu folgendem Fehler: SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
schau dir deinen Code nochmal an, da ist nicht nur ein Klammernpaar, das du weglassen könntest.
Du schreibst:

Code: Alles auswählen

(print('fertig'))
Dann könntest du ja mal die äußeren Klammern weg lassen:

Code: Alles auswählen

print('fertig')
Diese haben keinen Effekt, da die nicht zu einem Funktionsaufruf gehören, keine Zusammenhänge darstellen und auch sons nichts machen.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
OWT
User
Beiträge: 29
Registriert: Montag 20. Juni 2022, 10:45

Hallo Zusammen

und ein großes ubbs, sowohl die Klammern ums Print, als auch am Ende um Messergebnisse sind natürlich überflüssig. Sorry das hatte ich vor lauter Wald gar nicht gesehen, danke!

Das ein Dict gemacht wird ist gut? Das Messwert und Messzeitpunkt getrennt werden, ist schlecht? Bzw. ist es schlecht, dass es zu diesem Zeitpunkt passiert und nicht später? Oder wäre Pandas / Numpy eigentlich in der Lage direkt mit dem Paar [Messwert, Messzeitpunkt] zu arbeiten?

Wenn ich Blackjacks Vorschlag richtig verstehe, dann wäre es besser die Daten als "ganzes" zu holen, also als Liste von "Listen-Triplets", welche aus [Wert, Zeit, Logger_ID] besteht. Vermutlich sollte man sich trotzdem auf die gewünschten Logger beschränken und nicht alle holen? Das Auftrennen nach Logger_ID kann während dem Holen passieren und die Logger_ID wird als Schlüssel des Dictionarys verwendet. Die Einträge im Dict währen dann jeweils Werte-Paare bestehend aus [Wert, Zeit], 6 Logger --> 6 Dict Spalten. Diese Werte-Paare würde ich dann mit der gleichen For-Schleife trennen, aber erst 'offline' und nicht während dem holen. Hab ich dich richtig, Black?

Es ist irgendwie Schade, dass '+=' etwas anderes macht als man erwartet. Irgendwie macht es genau was ich erwarte :)
Auch finde ich das 'a += b" genau das gleiche macht wie 'a = a + b', auch hier wurde ich noch nicht überrascht. Das liegt vermutlich daran, dass ich mir "nur" den Effekt an schaue und nicht wie es exakt gerechnet wird. Ich hab derweil noch das Argument gefunden, dass append (streckenweise) deutlich schneller arbeiten kann, da es eine bestehende Liste erweitert und nicht eine neue Liste erstellt, in welche es zuerst alle vorangegangenen Einträge kopieren muss. Im Konkreten Beispiel bei 1000 Messpunkten, wird also mit '+=' der erste Messwert gut 1000 mal in eine Liste kopiert, während er mit append nur einmal kopiert wird.
https://stackoverflow.com/questions/725 ... append-and

Es stimmt schon das "Es macht jeder so" ein guter Anhaltspunkt ist um verständliches zu schreiben. Trotzdem bin ich wohl eher der Typ der lieber drei mal nachfragt und es dann noch vergisst :)

Vielen Dank nochmal fürs Durchschauen vom Code. Ich bin nun relativ zuversichtlich, dass alles funktioniert :)
Woob Woob
OWT
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@OWT: Mein Vorschlag wäre, wenn das am Ende in Pandas verarbeitet werden soll gar nicht erst irgend etwas vorher selber zu machen, sondern die Daten gleich in einen DataFrame zu laden und nötige Berechnungen und Umformungen dann dort vorzunehmen. Schon auf die Logger mit den IDs beschränkt die man haben möchte und nicht einfach alles. Das lässt sich mit SQLAlchemy ja leicht formulieren. 🙂

Hier sieht man den Unterschied zwischen ``a = a + b`` und ``a += b`` bei Listen:

Code: Alles auswählen

In [1]: A = [1]                                                                 

In [2]: B = A                                                                   

In [3]: B = B + [2]                                                             

In [4]: A                                                                       
Out[4]: [1]

In [5]: B                                                                       
Out[5]: [1, 2]

In [6]: B = A                                                                   

In [7]: B += [2]                                                                

In [8]: A                                                                       
Out[8]: [1, 2]

In [9]: B                                                                       
Out[9]: [1, 2]
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
OWT
User
Beiträge: 29
Registriert: Montag 20. Juni 2022, 10:45

Danke fürs Beispiel, das ist nicht das gleiche :/

Irgendwie hab ich das Initial mit Pandas nicht hin bekomme, da ich den Fehler hatte, dass die Länge nicht passt. Das kann ich nun aber auch nicht reproduzieren. Nachfolgend hab ich versucht den Weg für mich zu skizzieren, hierzu wäre es notwendig, jede row in ein DataFrame zu verwandeln. Also wäre der Datenbankzugriff auch über eine For-Schleife, wie bis her. Aber ein Holen der Daten als Triplets und anschließendes Trennen ist möglich.

Die 'start_liste' sollte der Form entsprechen, die aus der Datenbank raus kommt. Nun ist [0] = Messwert , [1] = Zeitpunkt der Messung und [2] sind die Logger_IDs. Für ein klassische X-Y Betrachtung, brauche ich (?) zwei spalten. Aber wie bekomme ich die mit Pandas?

Append gefällt den Leuten , aber append sei für pandas leider veraltet (The frame.append method is deprecated and will be removed from pandas in a future version. Use pandas.concat instead.)

Code: Alles auswählen

import pandas as pd
start_liste = [[1,2,'a'],[3,4,'b'],[5,6,'c']] #Beispielliste, in Real entspricht die Liste der cursor.execute(sql ...

#Verarbeitung der 'fiktiven' Datenbank Verbindung. 
pd_liste = pd.DataFrame()
for k in start_liste:
    pd_liste = pd.concat([pd_liste, pd.DataFrame(k)])
print('Index nach concat ist:',pd_liste.index)
#out: Index nach concat ist: [0, 1, 2, 0, 1, 2, 0, 1, 2]
print('pd_liste nach concat ist',pd_liste)
#out: pd_liste nach concat ist: [1, 2, a, 3, 4, b, 5, 6, c]
Das ist eine Langform, jeder Index 0 ist eine Messwert, jeder Index 1 eine Zeitpunkt und jeder Index 2 eine Logger_ID. Mit dieser Form wäre der Datenbank zugriff fertig. Jetzt noch 'offline' alles richtig auseinander klauben.

Code: Alles auswählen

werte = pd_liste.loc[0]
zeiten = pd_liste.loc[1]
logger = pd_liste.loc[2]
werte = werte.set_index(zeiten[0])
logger = logger.set_index(zeiten[0])
werte = pd.concat([werte,logger],axis=1)
werte.columns = ['Mp','Logger']
Hättest du das so ähnlich gemacht? Es braucht schon etwas Gottvertrauen um zu glauben, dass die Datentriplets wieder richtig zusammen gesteckt sind :)


edit: der letzte Codeblock löst folgenden Warnung aus:
FutureWarning: In a future version, the Index constructor will not infer numeric dtypes when passed object-dtype sequences (matching Series behavior)
return Index(sequences[0], name=names)
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@OWT: `append()` auf DataFrames ist ja auch was anderes als `append()` auf Listen. DataFrames kann man in der Anzahl der Elemente nicht verändern, da werden bei jedem `append()` die alten Daten plus die neuen Daten im Speicher kopiert. Bei Deiner Verwendung von `concat()` auch. Es sollte keine `start_liste` geben, die Datenbankabfrage sollte gleich in einem DataFrame landen.

Grunddatentypen gehören nicht in Namen. Man sieht hier ganz gut warum: Wenn sich Code weiterentwickelt ändern sich nicht selten die Typen und dann hat man falsche, irreführende Namen. `pd_liste` ist keine Liste. Und warum da jetzt der abgekürzte Modulname von `pandas` im Namen steckt, erschliesst sich mir auch nicht so ganz.

Wo kommen denn jetzt plötzlich Buchstaben her? Ich dachte die Logger-IDs sind Zahlen‽

Die beiden `print()`-Ausgaben sehen bei mir anders aus.

`pd_liste` hat doch ein ganz beklopptes Format. Das sollte so aussehen wie es aus der Datenbank kommt, mit drei Spalten, eine für die Zeitpunkte, eine für die Messwerte, und eine für die Logger.

Code: Alles auswählen

In [40]: pd.DataFrame(start_liste, columns=["value", "time", "logger"])         
Out[40]: 
   value  time logger
0      1     2      a
1      3     4      b
2      5     6      c
Und von da geht's dann weiter, was man halt braucht. Zum Beispiel gruppieren nach Logger. Oder wenn man das ”breite” Format haben will, beispielsweise für eine Ausgabe als Tabelle, dann die entsprechende(n) Methoden zum umformen verwenden. Immer wenn man denkt man müsse was mit einer Schleife in Python machen, schauen ob man das wirklich machen muss, denn Pandas & Co verwendet man ja gerade, damit man keine Schleifen in Python schreiben muss.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
OWT
User
Beiträge: 29
Registriert: Montag 20. Juni 2022, 10:45

huhu

die "start_liste" entspricht: rows = cursor.execute(sql, [logger_IDs])

Und dass kann ich als ganzes in ein DataFrame packen und es löst die Werte Triplets von selber auf? Wie praktisch. Das würde meinen initialen Code deutlich vereinfachen. Danke!
Ich melde mich wieder, wenn ich dazu gekommen bin, es aus zu probieren.

Bezüglich der Buchstaben:
Ist es nicht egal, ob die IDs Zahlen oder Buchstaben sind. Ich habe versucht ein Beispiel zu wählen, dass übersichtlich ist und noch mehr Zahle hätten das nicht gemacht und dadurch dass ich Strings eingebaut hab, konnte ich (für mich) die Versatilität des Codes überprüfen.
einfachTobi
User
Beiträge: 491
Registriert: Mittwoch 13. November 2019, 08:38

Pandas hat eine Funktion dafür: read_sql.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@OWT: Ob das Buchstaben sind ist egal. Solange bis das eben nicht mehr egal ist. Und wenn da eine Warnung kommt, das man etwas macht, das in Zukunft nicht mehr so funktioniert, dann scheint es wohl nicht egal zu sein. 😉
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
OWT
User
Beiträge: 29
Registriert: Montag 20. Juni 2022, 10:45

huhu

Danke nochmal fürs Feedback!

Für mich zeichnet es sich so ab, dass ich mit pyodbc den vorgestellten Weg übers Dict nehmen muss, weil ich ohne Fehlermeldungen arbeiten will. Ich hab gesehen wie gut pandas.sql_read funktioniert. Danke für den Hinweis Tobi.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@OWT: Ich sehe nicht warum Du das jetzt schon wieder komplizierter machen willst. Welche Fehlermeldung? Die Warnung? Da steht das in Zukunft nicht mehr automatisch geraten wird ob `obj` alles Zeichenketten sind. Man muss das dann halt explizit angeben.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
OWT
User
Beiträge: 29
Registriert: Montag 20. Juni 2022, 10:45

Hallo Black

pandas.sql_read ist nur mit SQLAlchemy Verbindung kompatibel, ich hab diese Fehlermeldung gemeint.
Antworten