Mittelwerte aus csv-Dateien, Leerfelder berücksichtigen

mit matplotlib, NumPy, pandas, SciPy, SymPy und weiteren mathematischen Programmbibliotheken.
Antworten
Schlangenbeschwörung
User
Beiträge: 2
Registriert: Samstag 21. Mai 2022, 09:43

Moin,
ich bin neu im Club und auch noch nicht sonderlich versiert in Sachen Python. Soviel zur Vorrede. :-)
Mein aktuelles Projekt möchte folgendes.
Ich habe ca 10 gleich aufgebaute csv-Dateien. Jede Zeile ist einer Person zugeordnet. Die Spalten enthalten 7 Eigenschaften, die zu beobachtende Verhaltensweisen darstellen. Durch die Zahlenwerte 1 bis 4 werden die Verhaltensweisen pro Person eingestuft.
Das Programm soll also pro Person und Verhaltensweise aus allen 10 csv-Dateien den jeweiligen Mittelwert errechnen und als csv ausgeben.

Ich habe das Problem behelfsmäßig bisher so gelöst, dass ich die Dateien durchnummeriert (1.csv, 2.csv, ...) in einen eigenen Ordner gepackt habe, die jeweiligen Felder addieren und dann durch die Anzahl der files im Ordner teilen lasse.

Das funktioniert auf diese einfache Weise auch gut. Stößt aber an seine Grenzen, sobald mal ein Feld keine Daten enthält.

Mir ist klar, dass für die Berücksichtigung der NuN -Situation gänzlich anders vorgegangen werden muss. Nur wie?
Im Grunde müsste dies doch ein alltägliche Problem sein, dass irgendwie gelöst ist. Ich finde aber nix - oder habe nicht die richtige Suchstrategie?

Vielen Dank für's Lesen! Bin für jede Hilfe dankbar!
Anbei ein bisschen Code.

Code: Alles auswählen

### Import
import pandas as pd
import os
## Variablen
home=os.path.abspath(".")
n=1
dat=1
## Festlegen der Spaltenüberschriften

column_names = ["Name","orga","met","konz","selbst","eng","tem","konf"]
files = os.listdir(home+"/data")

## Funktion lesen

def lesen(n):
        
        dat=pd.read_csv(home+"/data/"+str(n)+".csv",
                       header=0,
                       index_col=0,
                       names=column_names)
        return dat

### Daten lesen
while n <= (len(files)):
    if n==1:
        data = lesen(dat)
        n=n+1
        dat = dat +1
        out = data
    else:
        data = lesen(dat)
        dat = dat +1
        out = out + data
        n = n +1

###Berechnung der Mittelwerte
mittel = out/len(files)
mittel.head()

####Ausgabe des Ergebnisses
mittel.to_csv("Ergebnismittelwerte.csv")
nezzcarth
User
Beiträge: 1633
Registriert: Samstag 16. April 2011, 12:47

Zuerst müsstest du dir überlegen, wie du mit NaN Datenpunkt umgehen möchtest. Sollen die auf einen Default gesetzt oder z.B. aus der Rechnung ausgenommen werden? Das kannst nur du entscheiden, da die Wahl der Vorgehensweise vom Kontext bzw. der Interpretation der Daten abhängt.

Aus meiner Sicht wäre es für das Aggregieren zudem einfacher, sich die Daten so vorzustellen, statt dieser "Datensatz-zentrierten" Darstellung, die du zur Zeit hast:

Code: Alles auswählen

Datei	Name	Eigenschaft	Wert
1	A	orga	        1
1	A	met	        4
1	A	konz	        3
…	…	…	        …
1	B	orga	        3
1	B	met	        4
…	…	…	        …
2	A	orga	        2
2	A	konz	        2
…	…	…	        …
2	B	orga	        4
…	…	…	        …
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

Kommentare sollen dem Leser einen Mehrwert bieten, # Importe ist überflüssig, wenn direkt darunter `import` steht. Und wenn jemand keine Variable erkennt, hilft der Kommentar auch nicht weiter.
Eingerückt wird immer mit 4 Leerzeichen pro Ebene, nicht mal 4 und mal 8.
Benutze keine Abkürzungen. orga, met, konz? Selbst `selbst` sagt mir in diesem Kontext nichts.
`dat` sagt als Variable gar nichts, verwirrt nur, weil es wie `n` benutzt wird, was immerhin geläufig ist für eine Zählvariable, wenn auch nicht gut. Eine Variable definiert man nicht 16 Zeilen, bevor man sie braucht und steckt auch noch Funktionsdefinitionen dazwischen. Auf oberster Ebene sollte eh kein Ausführbarer Code stehen, sondern alles in eine main-Funktion gepackt werden.
Pfade setzt man nicht mit + zusammen. Für Pfade nimmt man das pathlib-Modul.
Du ermittelst alle Dateinamen, nur um dann die Anzahl zu nehmen und die Dateinamen wieder selbst zusammenzubauen? Warum?

Daten setzt man per `concat` zusammen und Mittelwerte bildet man mit `mean`:

Code: Alles auswählen

import pandas as pd
from pathlib import Path

COLUMN_NAMES = ["Name","orga","met","konz","selbst","eng","tem","konf"]

def lesen(filename):
    return pd.read_csv(filename,
        header=0, index_col=0, names=COLUMN_NAMES)

def main():
    data = pd.concat([
        lesen(filename)
        for filename in Path.glob('data/*.csv')
    ])
    mittelwerte = data.groupby('Name').mean()
    mittelwerte.to_csv("Ergebnismittelwerte.csv")

if __name__ == "__main__":
    main()
Benutzeravatar
__blackjack__
User
Beiträge: 13068
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Schlangenbeschwörung: Anmerkungen zum Quelltext: Kommentare sollen dem Leser einen Mehrwert über den Code geben. Faustregel: Kommentare beschreiben nicht *was* der Code macht, denn das steht da bereits als Code, sondern warum er das macht. Sofern das nicht offensichtlich ist. Offensichtlich ist in aller Regel auch was in der Dokumentation von Python und den verwendeten Bibliotheken steht.

Kommentare werden mit *einem* "#"-Zeichen eingeleitet.

Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Namen werden in Python klein_mit_unterstrichen geschrieben. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (PascalCase).

`home` ist so sinnfrei weil man das aktuelle Arbeitsverzeichnis nicht vor einen relativen Pfad setzen muss. Das kann man einfach weglassen. Und `home` ist auch ein bisschen irreführend, denn die meisten würde darunter das Heimatverzeichnis des Benutzers verstehen. Das kann mit dem aktuellen Arbeitsverzeichnis übereinstimmen, muss es aber nicht.

Das "data"-Verzeichnis dagegen sollte man als Konstante definieren, denn das kommt sonst zweimal literal im Quelltext vor.

Pfade sind keine einfachen Zeichenketten, weil für Pfade regeln gelten damit sie gültig sind/bleiben, und diese Regeln hängen teilweise auch von der Plattform ab. Deshalb setzt man Pfade nicht einfach mit Zeichenkettenoperationen zusammen. Früher hat man dafür die Funktionen im `os.path`-Modul verwendet. In neuem Code sollte man besser `pathlib.Path` verwenden.

Du hast in `files` die Dateinamen, nutzt die aber überhaupt nicht, sondern baust die dann noch mal per Code aus einer laufenden Nummer zusammen.

`n` und `dat` werden viel zu weit von der Stelle entfernt definiert wo sie dann letztendlich tatsächlich verwendet werden. Das macht den Code schwerer zu lesen und zu refaktorisieren und man vergisst bei solchen Abständen bei Programmänderungen gerne mal Definitionen die nicht mehr gebraucht werden.

Die beiden Variablen enthalten auch in jedem Schleifendurchlauf den gleichen Wert, warum sind das also überhaupt zwei Variablen?

Wenn man am Anfang oder am Ende von allen ``if``/``elif``/``else``-Zweigen das gleiche macht, dann schreibt man das einmal vor bzw. nach dem Konstrukt und nicht nicht in jeden Zweig. Das gilt auch wenn die Sachen nicht am Anfang oder Ende stehen, aber dort stehen könnten, ohne die Semantik zu verändern. Das würde man auch leichter sehen wenn man in den Zweigen die Schritte immer in der gleichen Reihenfolge machen würde.

Wenn man eine Zählvariable hat deren Anfang und Ende man kennt, dann ist das eine ``for``-Schleife statt einer ``while``-Schleife.

Die Schleife schrumpft damit am Ende auf das hier zusammen:

Code: Alles auswählen

    for i in range(1, len(files) + 1):
        if i == 1:
            out = lesen(i)
        else:
            out += lesen(i)
Was da noch unschön ist, ist der Test der für jeden Durchlauf gemacht wird, obwohl man genau weiss, dass der nur für den ersten Durchlauf gebraucht wird. So etwas behandelt man deshalb normalerweise *vor* der Schleife:

Code: Alles auswählen

    out = lesen(1)
    for i in range(2, len(files) + 1):
        out += lesen(i)
Oder man regelt das mit `map()` und `sum()`:

Code: Alles auswählen

    data_frames = map(lesen, range(1, len(files) + 1))
    out = sum(data_frames, next(data_frames))
Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
from pathlib import Path

import pandas as pd

DATA_PATH = Path("data")
COLUMN_NAMES = ["Name", "orga", "met", "konz", "selbst", "eng", "tem", "konf"]


def lesen(file_path):
    return pd.read_csv(file_path, header=0, index_col=0, names=COLUMN_NAMES)


def main():
    cvs_paths = list(DATA_PATH.glob("*.csv"))
    data_frames = map(lesen, cvs_paths)
    sums = sum(data_frames, next(data_frames))
    (sums / len(cvs_paths)).to_csv("Ergebnismittelwerte.csv")


if __name__ == "__main__":
    main()
Deine Frage kann man nicht so wirklich beantworten, denn Du müsstest schon selber wissen wie ein nicht vorhandener Wert in die Rechnung eingehen soll und das entsprechend programmieren. Also beispielsweise nicht vorhandene Werte durch 0en ersetzen falls die als 0 in das Ergebnis eingehen sollen. Oder nicht pauschal durch die Anzahl der Dateien teilen sondern pro Person ermitteln wie viele Werte tatsächlich vorhanden waren, und durch diese Anzahlen teilen. Je nach dem welches Vorgehen in dem vorliegenden Fall Sinn macht. Vielleicht hängt das sogar von den Bedeutungen der Spalten ab, bei welchem Wert welches vorgehen Sinn macht.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Schlangenbeschwörung
User
Beiträge: 2
Registriert: Samstag 21. Mai 2022, 09:43

Wow, vielen Dankf für die detailierten Hinweise und Korrekturen!!!
Ich brauche wohl nicht auf alle gestellten Fragen, warum ich was wie gemacht habe einzugehen. Die Antwort ist fast immer "geringe Erfahrung", bzw im Falle der Tabellen: ich habe sie halt so benutzt, wie ich sie erhalten habe. Samt kryptischen Abkürzungen.

Die Frage allerdings, wie mit den leeren Feldern umgegangen werden soll ist schon wesentlich. Die sollen schlicht bei der Ermittlung des Mittelwertes nicht mitgezählt werden. Beispiel: bei 5 Tabellen, von denen in zweien an der gleichen Stelle ein Feld leer ist, wird schließlich aus den verbleibenden 3 Werten der Mittelwert gebildet. Der Teiler ist also 3 und nicht 5. Eine Null oder einen anderen Wert in die leeren Felder einzutragen verbietet sich also.

Der Code von @__blackjack__ lässt das Feld bisher leer. Was wäre hier zu tun, damit die verbleibenden Werte berücksichtigt werden?

Der code von @Sirius3 gibt einen Fehler aus, den ich noch nicht interpretieren kann.
for filename in Path.glob('data/*.csv')
TypeError: glob() missing 1 required positional argument: 'pattern'
edit: habe die Path-Variable im Code von @Sirus3 angepasst, jetzt läufts so, wie ich es brauche!
Antworten