csv einlesen und weiter verarbeiten

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
JaSyMa
User
Beiträge: 30
Registriert: Sonntag 3. September 2023, 10:52

Hi zusammen,

ich habe als "Python rookie" folgende Zielsetzung:

- Einlesen einer csv datei mit xyz Spalten
- Multiplikation Spalte x mit Spalte Y
- Ausgabe Summe Spalte x und Summe Ergebnisspalte aus Vorpunkt

Das ganze dann noch gefiltert mit Zeilen in einem bestimmten Datumsbereich und definierten Gruppen. Die csv Datei hat keine Spaltenköpfe.

Ich habe das Ganze zunächst mit panda versucht, aber da bin ich schon am Einlesen gescheitert. Habe nach umfangreicher Googlesuche sehr viele encodings probiert und auch das per Python ausgelesene Codeformat probiert. Aber nichts hat funktioniert, so dass ich mit der csv Bibliothek weitergemacht habe.

Diese liest sauber ein und ich kann die Inhalte ausgeben. Mit folgendem Code lasse ich mir die Inhalte in der 10. Spalte anzeigen und versuche, zu summieren.

import csv

total=0
with open("kontobuchungen.csv") as csvfile:
data=csv.reader(csvfile, delimiter=";")

for row in data:
try:
#print("Zeile " + str(row[11]))
print(f"Einzel: {row[11]}")
total = total + float(row[11])
except ValueError:
total=total

print (f"Gesamt: {total}")

Die 10. Spalte enthält Nachkommazahlen und liefert keine Summe. Hier die Ausgabe im Debuggingfenster:

Einzel: 0,00
Einzel: 749,70
Einzel: 500,00
Einzel: 0,00
Einzel: 500,00
Einzel: 0,00
Gesamt: 0

Mache ich das Ganze in Spalte 8 (enthält nur Ganzzahlen) wird die Summe sauber ausgegeben. Was kann ich tun, um die Spalte mit den Gleitzahlen zu addieren? Wenn ich das habe mache ich mich an die Filterung nach Termin und Gruppe.

Und wie kann ich den Spalten Namen zuweisen, um sie sprechender im Code zu haben?
Benutzeravatar
__blackjack__
User
Beiträge: 13141
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@JaSyMa: Beim öffnen von Textdateien sollte man immer die Kodierung angeben.

``spam = spam + ham`` kann man einfacher als ``spam += ham`` schreiben.

Das ``total = total`` ist nicht wirklich sinnvoll. Wenn man in einem Block eigentlich gar nichts machen will, verwendet man die ``pass``-Anweisung.

Wenn am Ende 0 raus kommt, dann gab es offensichtlich immer einen `ValueError` im ``try``-Block. Das Komma ist halt falsch für `float()`. Das will einen Dezimal*punkt*. Simple Lösung ist das ersetzen des Komma durch einen Punkt:

Code: Alles auswählen

In [403]: float("42,23")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Input In [403], in <cell line: 1>()
----> 1 float("42,23")

ValueError: could not convert string to float: '42,23'

In [404]: float("42.23")
Out[404]: 42.23

In [405]: float("42,23".replace(",", "."))
Out[405]: 42.23
Hier eventuell noch aufpassen ob man auch grosse Zahlen in der Eingabe haben kann die den Punkt als Tausendertrenner verwenden. Die müsste man vorher entfernen.

Bezüglich der Spaltennamen könnte man einen `csv.DictReader()` verwenden dem man beim erstellen die Spaltennamen übergibt. Alternativ könnte man sich Konstanten für die Spaltenindizes erstellen. Das würde ich persönlich davon abhängig machen wie das Verhältnis von Spaltenanzahl zu verwendete Spaltenanzahl ist. Wenn die Datei 50 Spalten hat, von denen ich nur drei brauche, würde ich da eher nicht 50 Spaltennamen angeben wollen. Wobei es natürlich auch einen dokumentierenden Charakter hat.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import csv

IRGENDEIN_BETRAG_INDEX = 11

def main():
    with open("kontobuchungen.csv", encoding="ascii") as csv_file:
        total = 0
        for row in csv.reader(csv_file, delimiter=";"):
            try:
                print(f"Einzel: {row[IRGENDEIN_BETRAG_INDEX]}")
                total += float(row[IRGENDEIN_BETRAG_INDEX].replace(",", "."))
            except ValueError:
                pass  # Ingore invalid numbers.

        print(f"Gesamt: {total}")


if __name__ == "__main__":
    main()
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
JaSyMa
User
Beiträge: 30
Registriert: Sonntag 3. September 2023, 10:52

Das mit Punkt und Komma macht eine Menge sinn, danke.

Der Code stoppt in Zeile 27. Die Zahl dort ist 261,4. Sieht also zunächst anders aus als die Zahlen zuvor, die zwei Nachkommastellen haben. Allerdings hat die 0 gar keine und läuft auch sauber durch. Hier der ausgeworfene Fehler:

Einzel: 2451,32
Einzel: 315,16
Einzel: 261,42
Einzel: 315,13
Traceback (most recent call last):
File "c:\Users\Jan\Desktop\Python\csv_einlesen_BlackJack", line 20, in <module>
main()
File "c:\Users\Jan\Desktop\Python\csv_einlesen_BlackJack", line 9, in main
for row in csv.reader(csv_file, delimiter=";"):
File "C:\Python311\Lib\encodings\ascii.py", line 26, in decode
return codecs.ascii_decode(input, self.errors)[0]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 6049: ordinal not in range(128)

Die Zeile sieht auch sonst nicht anders aus als die davor... Sagt Dir der Fehlercode was?
Benutzeravatar
__blackjack__
User
Beiträge: 13141
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Ja der sagt das die Datei nicht ASCII-kodiert ist. *Wie* die kodiert ist, musst *Du* wissen, und entsprechend beim Öffnen angeben.
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
Sirius3
User
Beiträge: 17768
Registriert: Sonntag 21. Oktober 2012, 17:20

Deine Datei ist offensichtlich UTF8-kodiert und nicht ASCII. Wahrscheinlich irgendein Leerraum, den Du nicht siehst.
Benutzeravatar
__blackjack__
User
Beiträge: 13141
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Es könnte auch ISO-8859-1 oder CP1252 sein. Dann wäre es ein „ä“.
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
JaSyMa
User
Beiträge: 30
Registriert: Sonntag 3. September 2023, 10:52

Es ist CP1252. Ist es nicht möglich, das Einlesen flexibel zu gestalten, um verschiedene Inputs verlässlich verarbeiten zu können? Ich hatte mich schon durch stackoverflow durchgearbeitet - die Vielfalt der codings war riesig und dort habe ich keine “variable Waffe” gefunden…
Benutzeravatar
grubenfox
User
Beiträge: 435
Registriert: Freitag 2. Dezember 2022, 15:49

:D viewtopic.php?t=57247 am ende hatte mir dort die Erwähnung von chardet sehr geholfen...

https://pypi.org/project/chardet/
https://github.com/chardet/chardet

Aus der Doku hatte ich dann bei mir diesen Code angepasst und übernommen:
https://chardet.readthedocs.io/en/lates ... iple-files
Benutzeravatar
__blackjack__
User
Beiträge: 13141
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Wenn man nur Zahlenspalten verarbeitet und keine Texte unverändert wieder speichern möchte, könnte man auch ``errors="replace"`` verwenden. Oder falls man die Daten auch wieder irgendwo kodieren muss "surrogateescape" beim lesen *und* schreiben.
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
JaSyMa
User
Beiträge: 30
Registriert: Sonntag 3. September 2023, 10:52

Hi zusammen,

im folgenden mein aktueller Wurf, der bei den beiden Beispielgruppen schon gut durchläuft.

Code: Alles auswählen

#!/usr/bin/env python3
import csv
import locale
from openpyxl import load_workbook
import time

zeit_anf = time.time()
locale.setlocale(locale.LC_ALL, "")

sp_haben = 10  # Spalte K
sp_soll = 11  # Spalte L
sp_steuer = 6 # Spalte G
sp_konto = 1  # Spalte B

wb = load_workbook('referenz.xlsx')
dict = {}  # Konten und zugehörige Gruppen
for row in wb.worksheets[0].iter_rows(min_row=2, max_row=4000, min_col=2, max_col=4):
    dict[row[0].value] = row[1].value
wb.close

def main():
    umsatzerlöse = 0
    fremdleistungen = 0
    steuer_umsatzerlöse = 0
    steuer_fremdleistungen = 0
    umsatz = 0
    Anzahl = 1

    with open("kontobuchungen2022.csv", encoding="CP1252") as csv_file:
        Buchungen_unfiltered = csv.reader(csv_file, delimiter=";")
        daten = [row for row in Buchungen_unfiltered]

    with open("kontobuchungen.csv", encoding="CP1252") as csv_file:
        Buchungen_unfiltered = csv.reader(csv_file, delimiter=";")
        daten.extend([row for row in Buchungen_unfiltered])
        #print(daten)
        
        Buchungen_filtered = filter(lambda row: row[0] == "0" and int(row[1]) < 9000, daten)
        
        for row in Buchungen_filtered:
            #if Anzahl == 20: break
            konto = int(row[sp_konto])
            
            try:
                gruppe = dict.get(konto)
                umsatz = (float(row[sp_soll].replace(",", ".")) - float(row[sp_haben].replace(",", ".")))
                match gruppe:
                    case "Umsatzerlöse":
                        umsatzerlöse += umsatz
                        steuer_umsatzerlöse += float(row[sp_steuer].replace(",", ".")) * umsatz / 100
                    case "Fremdleistungen":
                        fremdleistungen += -umsatz
                        steuer_fremdleistungen += float(row[sp_steuer].replace(",", ".")) * -umsatz / 100

            except ValueError:
                pass  # Ignore invalid numbers
                print (f"ohne Zuordnung: {konto}")
            Anzahl += 1
        zeit_temp = time.time()

        if umsatzerlöse != 0:
            steuersatz_umsatzerlöse = steuer_umsatzerlöse / umsatzerlöse * 100
            print (f"Steuersatz Umsatzerlöse: {format(steuersatz_umsatzerlöse, '0,.1f')}%")
        if fremdleistungen != 0:
            steuersatz_fremdleistugen = steuer_fremdleistungen / fremdleistungen * 100
            print (f"Steuersatz Fremdleistungen: {format(steuersatz_fremdleistugen, '0,.1f')}%")
        print ("Umsatzerlöse: " + locale.format_string("%d", steuer_umsatzerlöse, True) + " - " + locale.format_string("%d", umsatzerlöse, True))
        print ("Fremdleistungen: " + locale.format_string("%d", steuer_fremdleistungen, True) + " - " + locale.format_string("%d", fremdleistungen, True))

    zeit_temp = time.time()
    print (f"Dauer: {format(zeit_temp - zeit_anf, '0,.2f')}s - Anzahl: {Anzahl}")
        
if __name__ == "__main__":
    main()
Was mir nicht gefällt ist, dass ich für jede Gruppe mehrere Variablen (immer jeweils gruppe_umsatz und gruppe_steuer) brauche. Das sind bei knapp 20 Gruppen schon mal 40 Variablen, die den code unübersichtlich machen. Habt Ihr eine Idee, wie ich die gruppen iterieren kann und (vielleicht durch Zwischenspeicherung?) in Summe nur zwei Variablen brauche?

Ich hatte schon überlegt den Durchlauf "for row in buchungen_filtered" für jede Gruppe einmal zu machen (und dann nach jedem Durchlauf die fertige Gruppe abzuspeichern), aber das scheitert daran, dass der reader-Zeiger nur einmal durchlaufen lässt und jedes mal den csv.reader neu zu generieren kostet deutlich perfomance.

Gibt es eine schlaue Technik für effizientes Durchlaufen pro Gruppe mit nur zwei Variablen? Wenn mein code sonst an irgendeiner Stelle "unschön" ist bin ich wie immer für Hinweise dankbar :)
Sirius3
User
Beiträge: 17768
Registriert: Sonntag 21. Oktober 2012, 17:20

Aller Code sollte in Funktionen stehen, warum steht der Code zum lesen von referenz.xlsx außerhalb?
wb.close nur zu referenzieren macht wenig Sinn, die Methode sollte man auch aufrufen, am besten implizit mit contextlib.closing.
Konstanten werden komplett GROSS geschrieben. Variablennamen werden dagegen komplett klein geschrieben.
dict ist ein schlechter Variablenname, weil erstens zu generisch und zweitens überdeckt das den Typ dict.
Die Main-Funktion ist zu lang und sollte in mehrere Funktionen aufgeteilt werden.
Wenn Du schon locale benutzt, warum dann nicht auch zum Konvertieren in floats? Und warum werden die Prozente nicht mit lokale konvertiert?
Wenn Du eigentlich gar keine Funktionalität von Listcomprehensions benutzt, warum schreibst Du dann welche?

Da Du eh erst alle Daten einliest und sie dann verarbeitest, verstehe ich das Argument nicht, dass Du mehrmals die csv-Dateien parsen müßtest.
Du benutzt einmal die Konstante sp_konto und einmal 1 direkt, und 0 hat gar keine entsprechung als Konstante??
Zusammengehörige Daten sollte man immer zusammen behalten, also Umsatz und Steuer gehören zusammen, sollten also in der selben Datenstruktur stehen.
Generell versucht man doppelten Code zu vermeiden, indem man passende Datenstrukturen benutzt, wie hier z.B. Wörterbücher.

Code: Alles auswählen

import csv
import contextlib
import locale
from locale import atof, format_string
from collections import defaultdict
from openpyxl import load_workbook

HABEN = 10  # Spalte K
SOLL = 11  # Spalte L
STEUER = 6 # Spalte G
KONTO = 1  # Spalte B


def read_references():
    with contextlib.closing(load_workbook('referenz.xlsx')) as workbook:
        sheet = workbook.worksheets[0]
        return {
            row[0].value: row[1].value
            for row in sheet.iter_rows(min_row=2, max_row=4000, min_col=2, max_col=4)
        }

def read_files(filenames);
    for filename in filenames:
        with open(filename, encoding="CP1252") as csv_file:
            yield from csv.reader(csv_file, delimiter=";")

def main():
    locale.setlocale(locale.LC_ALL, "")
    konto_zu_gruppe = read_references()
    buchungen = read_files(["kontobuchungen2022.csv", "kontobuchungen.csv"])
    buchungen_filtered = filter(lambda row: row[0] == "0" and int(row[KONTO]) < 9000, buchungen)
        
    summen_und_steuer = defaultdict(lambda: (0, 0))
    for row in buchungen_filtered:
        konto = int(row[KONTO])
        gruppe = konto_zu_gruppe(konto)
        try:
            umsatz = atof(row[SOLL]) - atof(row[HABEN])
            steuer = atof(row[STEUER])
        except ValueError:
            print(f"ohne Zuordnung: {konto}")
        else:
            summe, summe_steuer = summen_und_steuer[gruppe]
            summe += umsatz
            summe_steuer += umsatz * steuer / 100
            summen_und_steuer[gruppe] = summe, summe_steuer

    for gruppe, (umsatz, steuer) in summen_und_steuer.items():
        if umsatz != 0:
            steuersatz = umsatz / steuer * 100
            print(f"Steuersatz {gruppe}: {format_string('%.1f', steuersatz)}%")
            print(f"{gruppe}: {format_string('%d', steuer, True)} - {format_string('%d', umsatz, True)}")
        
if __name__ == "__main__":
    main()
Benutzeravatar
__blackjack__
User
Beiträge: 13141
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@JaSyMa: ``match``/``case`` würde ich hier nicht verwenden. Dieses Syntaxkonstrukt ist kein SELECT/CASE wie in Basic oder ``switch``/``case`` in C und ähnlichen Sprachen. Das ist strukturelles „pattern matching“ um gleichzeitig Strukturen erkennen und Werte an Namen binden zu können. Wenn im ``case``-Ausdruck keine Namen definiert werden, dann ist das IMHO ein Missbrauch von dem Konstrukt. Da tut es auch ein einfaches ``if``.

Es steht Code ausserhalb von der `main()`-Funktion die keine Konstanten definiert. Und die Konstanten sind nicht KOMPLETT_GROSS benannt.

Kryptische Abkürzungen sollte man vermeiden. Wenn man `SPALTE` meint, sollte man nicht nur `SP` schreiben. Und sofern man nicht Yoda ist, sollte man die Reihenfolge von Worten richtig wählen.

`openpyxel` hat eine Funktion um Spaltennamen in eine Spaltennummer umzurechnen. Dann muss man das nicht im Kommentar hinschreiben *und* man kann sich sogar recht sicher sein, dass die Werte stimmen.

Zeitmessungen sollte man mit `time.monotonic()` machen. `time.time()` ist nicht garantiert nur strikt aufsteigend.

Das Workbook wird gar nicht wieder geschlossen. Man muss `close()` auch *aufrufen*. Das passiert nicht automatisch wenn man die Methode von dem Objekt abfragt.

`dict` ist der Name eines eingebauten Datentyps, den sollte man nicht an etwas anderes binden. Der Name ist auch etwas sehr generisch. Man möchte ja nicht wissen, dass es ein Wörterbuch ist, sondern was Schlüssel und Werte bedeuten, im Kontext des Programms. Das sollte nicht an der einen Stelle in einem Kommentar stehen, sondern im Namen, damit man das überall wo es benutzt wird, sehen kann.

`iter_rows()` kennt ein `values_only`-Argument, damit liefert das nur die Werte statt `Cell`-Objekte. Und damit kann man beim Einlesen von `dict` eigentlich `dict` selbst sehr gut gebrauchen, denn das kann man mit einem iterierbaren Objekt aufrufen das Schlüssel/Wert-Paare liefert und man bekommt ein gefülltes `dict` als Ergebnis.

``[row for row in Buchungen_unfiltered]`` wäre einfacher ``list(Buchungen_unfiltered)``. Da mit zwei Dateien im Grunde das gleiche gemacht wird, würde ich da aber eher eine Schleife schreiben die mit beiden Dateien eine anfangs leere Liste erweitert. Und da braucht man nicht einmal eine „list comprehension“, denn `extend()` kann man iterierbare Objekte übergeben, also den CSV-Reader selbst, ohne da vorher erst einmal eine Liste draus machen zu müssen die dann noch mal umkopiert wird.

Der Code nach dem zweiten ``with`` ist zu weit eingerückt. Es macht ja keinen Sinn diese Datei nach dem Einlesen weiterhin offen zu halten.

Ich würde da eher nicht anfangen auch noch innerhalb von *einem* Namen Deutsch und Englisch zu mischen.

Letztlich ist die Frage ob man die Daten überhaupt in eine Liste einlesen muss, oder ob man die nicht einfach während des Einlesens verarbeiten kann. Und da kann man dann auch gleich schauen ob/wie man die in ”beiden Dimensionen” schon verkleinern kann. Also nur die Zeilen und Spalten liefern, die tatsächlich verarbeitet werden.

Vor dem Einlesen der CSV-Dateien werden schon einige Namen definiert, die dafür noch gar nicht gebraucht werden. Die Definition für `umsatz` dort wird überhaupt nicht gebraucht. Mittendrin wird `zeit_temp` definiert aber der Wert wird nicht verwendet.

`anzahl` sollte vielleicht nicht mit 1 initialisiert werden bevor überhaut ein Datensatz verarbeitet wurde. Und da `anzahl` bei *jedem* Schleifendurchlauf hochgezählt wird, sollte man das mit `enumerate()` lösen.

Das Parsen einer Gleitkommazahl kann man in eine Funktion herausziehen.

`format()` innerhalb fon f-Zeichenkettenliteralen macht eher keinen Sinn. Die Formatangaben kann man doch schon beim Platzhalter dort angeben.

Ungetesteter Zwischenstand:

Code: Alles auswählen

#!/usr/bin/env python3
import csv
import locale
import time
from contextlib import closing
from itertools import chain

from attr import attrib, attrs
from openpyxl import load_workbook
from openpyxl.utils import column_index_from_string

HABEN_SPALTE = column_index_from_string("K")
SOLL_SPALTE = column_index_from_string("L")
STEUER_SPALTE = column_index_from_string("G")
KONTO_SPALTE = column_index_from_string("B")

GRUPPE_AUF_FAKTOR = {"Fremdleistungen": -1, "Umsatzerlöse": 1}


def get_float(row, index):
    return float(row[index].replace(",", "."))


def lade_buchungen(dateiname):
    with open(dateiname, encoding="CP1252", newline="") as csv_file:
        for row in csv.reader(csv_file, delimiter=";"):
            if row[0] == "0" and int(row[KONTO_SPALTE]) < 9000:
                yield row


@attrs
class Summen:
    gesamt = attrib(default=0)
    steuer = attrib(default=0)

    @property
    def steuersatz(self):
        return self.steuer / self.gesamt * 100


def main():
    start_zeit = time.monotonic()
    locale.setlocale(locale.LC_ALL, "")

    with closing(load_workbook("referenz.xlsx")) as workbook:
        konto_auf_gruppe = dict(
            workbook.worksheets[0].iter_rows(2, 4000, 2, 4, values_only=True)
        )

    gruppe_auf_summen = {gruppe: Summen() for gruppe in GRUPPE_AUF_FAKTOR}
    anzahl = 0
    for anzahl, row in enumerate(
        chain.from_iterable(
            map(
                lade_buchungen,
                ["kontobuchungen2022.csv", "kontobuchungen.csv"],
            )
        ),
        1,
    ):
        konto = int(row[KONTO_SPALTE])
        try:
            gruppe = konto_auf_gruppe.get(konto)
            faktor = GRUPPE_AUF_FAKTOR.get(gruppe)
            if faktor is not None:
                summen = gruppe_auf_summen[gruppe]
                umsatz = faktor * (
                    get_float(row, SOLL_SPALTE) - get_float(row, HABEN_SPALTE)
                )
                steuer = get_float(row, STEUER_SPALTE) * umsatz / 100
                summen.gesamt += umsatz
                summen.steuer += steuer

        except ValueError:
            print(f"ohne Zuordnung: {konto}")

    for name, summen in gruppe_auf_summen.items():
        if summen.gesamt != 0:
            print(f"Steuersatz {name}: {summen.steuersatz:0,.1f}%")

    for name, summen in gruppe_auf_summen.items():
        print(
            f"{name}:",
            locale.format_string("%d", summen.steuer, True),
            "-",
            locale.format_string("%d", summen.gesamt, True),
        )

    print(
        f"Dauer: {format(time.monotonic() - start_zeit, '0,.2f')}s"
        f" - Anzahl: {anzahl}"
    )


if __name__ == "__main__":
    main()
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
JaSyMa
User
Beiträge: 30
Registriert: Sonntag 3. September 2023, 10:52

Hi sirius3,

vielen Dank für Deine Weiterentwicklung. Bin die einzelnen Codeschritte durchgegangen.

Das "contextlib - Wörterbuch ist mir noch nicht ganz klar. Ich brauche noch weitere Parameter pro Konto (im ersten Schritt den Parameter "Seite - Aufwand vs Ertrag bzw. Haben und Soll). In meinem alten code hatte ich dazu ein weiteres dictionary angelegt, dass dann das Konto mit dem Seitenwert definiert. Kann ich mit contextlib zu einem Konto auch zwei Werte zuordnen (Konto mit (i) der gruppe und (ii) der Seite)?

Bei den Gruppen nimmst Du aktuell alle möglichen, die es in den Buchungen gibt, sozusagen eine Unikatsliste. Ich brauche aber nicht alle, sondern nur vordefinierte, die bspw. in einer Exceltabelle Zellen A1:A20 stehen oder alternativ hier im code fest definiert sind. Die Summen sollen dann nur für die definierten Gruppen gebildet werden, i.e. Summe + Steuer aller Konten, die auf die definierte Gruppe Umsatzerlöse und zusätzlich Summe + Steuer aller Konten der definierten Gruppe Fremdleistungen etc. Ich habe dies am Beispiel der Variable "Umsatzerlöse" drin, brauche es aber eben für x weitere Gruppen. Wie kann ich dies variabel gestalten?

Code: Alles auswählen

import csv
import contextlib
import locale
from locale import atof, format_string
from collections import defaultdict
from openpyxl import load_workbook

HABEN = 10  # Spalte K
SOLL = 11  # Spalte L
STEUER = 6 # Spalte G
KONTO = 1  # Spalte B

def read_references():
    with contextlib.closing(load_workbook('referenz.xlsx')) as workbook:
        sheet = workbook.worksheets[0]
        return {
            row[0].value: row[1].value
            for row in sheet.iter_rows(min_row=2, max_row=4000, min_col=2, max_col=4)
        }

def read_files(filenames):
    for filename in filenames:
        with open(filename, encoding="CP1252") as csv_file:
            yield from csv.reader(csv_file, delimiter=";")

def main():
    locale.setlocale(locale.LC_ALL, "")
    konto_zu_gruppe = read_references()
    buchungen = read_files(["kontobuchungen2022.csv", "kontobuchungen.csv"])
    buchungen_filtered = filter(lambda row: row[0] == "0" and int(row[KONTO]) < 9000, buchungen)
    
    summen_und_steuer = defaultdict(lambda: (0, 0))
    gruppe1 = "Umsatzerlöse"  #hier braucht es variabel 15-20 Gruppen für die berechnet wird

    for row in buchungen_filtered:
        konto = int(row[KONTO])
        gruppe = konto_zu_gruppe[konto]
        try:
            umsatz = atof(row[SOLL]) - atof(row[HABEN])  #todo: variabel setzen nach Hinzufügen des Wertes "Seite" zu Schlüssel Konto
            steuer = atof(row[STEUER])
        except ValueError:
            print(f"ohne Zuordnung: {konto}")
        else:
            if gruppe == gruppe1:
                summe, summe_steuer = summen_und_steuer[gruppe]
                summe += umsatz
                summe_steuer += umsatz * steuer / 100
                summen_und_steuer[gruppe] = summe, summe_steuer

    for gruppe1, (umsatz, steuer) in summen_und_steuer.items():
        if umsatz != 0:
            steuersatz = steuer / umsatz * 100
            print(f"Steuersatz {gruppe1}: {format_string('%.1f', steuersatz)}%")
            print(f"{gruppe1}: {format_string('%d', steuer, True)} - {format_string('%d', umsatz, True)}")
        
if __name__ == "__main__":
    main()
Ich bin Dir dankbar, wenn Du die Beispielumsetzung gleich im code machst. So kann ich das immer am Besten nachvollziehen.
Benutzeravatar
grubenfox
User
Beiträge: 435
Registriert: Freitag 2. Dezember 2022, 15:49

Einfach eine Funktion schreiben die die variable Gruppenliste erzeugt und dann durch die Liste der Gruppennamen laufen....

unvollständiges Beispiel mit einer Gruppenliste die nur einen Gruppennamen enthält:

Code: Alles auswählen


def liefer_variable_gruppenliste():
    liste = ["Umsatzerlöse"]
    return liste
    

def main():
    locale.setlocale(locale.LC_ALL, "")
    konto_zu_gruppe = read_references()
    buchungen = read_files(["kontobuchungen2022.csv", "kontobuchungen.csv"])
    buchungen_filtered = filter(lambda row: row[0] == "0" and int(row[KONTO]) < 9000, buchungen)
    
    summen_und_steuer = defaultdict(lambda: (0, 0))
    for gruppe1 in liefer_variable_gruppenliste()  #hier wird theoretisch eine variable Liste von 15-20 Gruppen genutzt

        for row in buchungen_filtered:
            konto = int(row[KONTO])
            gruppe = konto_zu_gruppe[konto]
Also neben der neuen Funktion ist nur die Zeile mit 'for gruppe1...' verändert, jetzt eben eine For-Schleife. Der vom Code Rest ist eigentlich unverändert, nur alles in der Schleife eben eine Ebene weiter eingerückt.
Sirius3
User
Beiträge: 17768
Registriert: Sonntag 21. Oktober 2012, 17:20

@grubenfox: filter liefert einen Iterator, so funktioniert das also nicht, und wenn ich es richtig verstanden habe, muß man das auch gar nicht so kompliziert machen, da man alle "Gruppen" gleichzeitig in einem Wörterbuch verarbeiten kann.

@JaSyMa: es ist doch egal, ob man jetzt für alle Gruppen die Berechnung macht und dann bei der Ausgabe filtert, oder gleich bei der Berechnung filtert, dann aber ein `if gruppe in relevante_gruppen`.
Benutzeravatar
grubenfox
User
Beiträge: 435
Registriert: Freitag 2. Dezember 2022, 15:49

`filter`? Mir ging es doch nur darum ob `gruppe1` nur einen Gruppennamen enthält oder in einer Schleife mit den Namen aus einer Liste (z.B. `relevante_gruppen`) gefüllt wird.
Aber mir ist da nicht so klar ob man die Ergebnisse einfach zu zusammen werfen kann/darf oder nicht. Für jede "Gruppe" die jeweiligen Ergebnisse in einem Wörterbuch zu sammeln erscheint mir da auch sinnvoller.
JaSyMa
User
Beiträge: 30
Registriert: Sonntag 3. September 2023, 10:52

Sirius3 hat geschrieben: Mittwoch 13. September 2023, 15:35 @JaSyMa: es ist doch egal, ob man jetzt für alle Gruppen die Berechnung macht und dann bei der Ausgabe filtert, oder gleich bei der Berechnung filtert, dann aber ein `if gruppe in relevante_gruppen`.
Das klappt super, danke. Komisch nur, dass "print" die definierte Liste umgekehrt ausgibt, d.h. die Liste ist beispielhaft

Code: Alles auswählen

liste = ["Umsatzerlöse", "Fremdleistungen", "Materialaufwand", "sonstige Kosten"]
aber im print beginnt es mit Material, dann Fremdleistungen, dann Umsatzerlöse. Da ich den output in ein array schreiben möchte, um es dann am Stück in eine Datei zu kippen brauche ich die andere Reihenfolge. Wie kann ich das beeinflussen?

Habt Ihr noch einen Tipp zur Erweiterung des Schlüssels Konto auf mehr als einen value in der Funktion read_references (siehe beispiel "Seite" im vorherigen post)...
Sirius3
User
Beiträge: 17768
Registriert: Sonntag 21. Oktober 2012, 17:20

Die Reihenfolge im Wörterbuch entspricht der Reihenfolge, wie die Einträge angelegt werden. Wenn Du es wie __blackjack__ machst, und das Gruppen-Wörterbuch vordefinierst, dann hast Du automatisch die gewünschte Reihenfolge.
Benutzeravatar
__blackjack__
User
Beiträge: 13141
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Dann muss man allerdings auch wieder daran denken die Einträge raus zu filtern die nicht in den Eingabedaten vorgekommen sind.
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
JaSyMa
User
Beiträge: 30
Registriert: Sonntag 3. September 2023, 10:52

... wie gut, dass sirius3 auf BlackJack verwiesen hat ... der post ist mir tatsächlich voll durchgerutscht. Deshalb erst jetzt feedback zu BlackJacks Vorschlag:

Beim Aufbau des dictionaries konto_auf_gruppe passierte der Fehler, den ich noch als offene Frage hatte - ich möchte dort die Spalte 2 bis 4 (Spalte 2 als schlüssel + 3 und 4 als values dazu) haben. Aktuell wirft er einen Fehler, weil 3 Spalten vorhanden sind/abgefragt werden, er aber offensichtlich nur 2 berücksichtigen kann. Habe das jetzt auf 3 statt 4 geändert. Damit läuft es durch, aber berücksichtigt natürlich die dritte Spalte nicht.

Da Du mit GRUPPE_AUF_FAKTOR ein weiteres dictionary erstellt hast nehme ich an, dass dies die effizientere Variante ist als einem Schlüssel mehrere Werte zuzuordnen? D.h. (angenommen ich habe zu jedem Konto 10 Werte), dass es in dem Fall besser ist 10 dictionaries zu erstellen?
Antworten