Archive for the ‘Programmierung’ Category.

Neue Excel Office365 Funktionen

Excel 365 aus Office365 hat eine Reihe neuer Funktionen, die ich auch noch nicht kannte. Hier eine Liste, die mir ChatGPT ausgespuckt hat.

Name (Englisch) Erläuterung
TEXTKETTE (TEXTJOIN) Verbindet Text aus mehreren Zellen mit einem Trennzeichen. Unterstützt auch Zellbereiche.
TEXTVERKETTEN (CONCAT) Ähnlich wie VERKETTEN, aber flexibler und moderner.
WENNS (IFS) Ersetzt verschachtelte WENN-Funktionen durch eine klarere Syntax.
MAXWENNS (MAXIFS) Gibt den größten Wert zurück, der bestimmte Kriterien erfüllt.
MINWENNS (MINIFS) Gibt den kleinsten Wert zurück, der bestimmte Kriterien erfüllt.
ERSTERWERT (SWITCH) Gibt einen Wert basierend auf dem ersten zutreffenden Vergleich zurück.
FILTER Filtert Daten dynamisch basierend auf Bedingungen.
SORTIEREN (SORT) Sortiert Daten dynamisch.
SORTIERENNACH (SORTBY) Sortiert Daten basierend auf einem anderen Bereich.
EINDEUTIG (UNIQUE) Gibt eindeutige Werte aus einem Bereich zurück.
SEQUENZ (SEQUENCE) Erstellt eine Liste von Zahlen in einer Sequenz.
WAHL.ZEILE (XLOOKUP) Moderner Ersatz für SVERWEIS und WVERWEIS.
WAHL.BEREICH (XMATCH) Gibt die Position eines Werts in einem Bereich zurück.
LET Ermöglicht das Definieren von Variablen innerhalb einer Formel.
LAMBDA Erstellt benutzerdefinierte Funktionen direkt in Excel.
TEXTSPLIT Teilt Text anhand eines Trennzeichens in mehrere Zellen auf.
TEXTBEFORE / TEXTAFTER Gibt Text vor oder nach einem bestimmten Zeichen zurück.
VSTACK / HSTACK Stapelt Zellbereiche vertikal oder horizontal.
WRAPROWS / WRAPCOLS Wandelt eine Liste in ein Array mit mehreren Zeilen oder Spalten um.
TAKE / DROP Gibt die ersten/letzten Elemente eines Bereichs zurück oder entfernt sie.
EXPAND Erweitert einen Bereich auf eine bestimmte Größe.
TOCOL / TOROW Wandelt einen Bereich in eine Spalte oder Zeile um.

Outer Apply und Cross Apply in SQL nutzen – NULL-Werte auffüllen

This entry is part 3 of 3 in the series Outer Apply und Cross Apply

Schauen wir uns das Forward Filling an. Je nach Version des SQL Servers funktioniert eine Version oder leider nicht. 🙂

Mit SQL Server 2022 kann man den folgenden Code nutzen, um einen weiteren CTE zu bauen:

Filled AS (
    SELECT
        ContNo,
        MonthEnd,
        Amount,
        last_value(Amount) IGNORE NULLS
        OVER (
            PARTITION BY ContNo
            ORDER BY MonthEnd
            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
        ) AS ForwardFilledAmount
    FROM Combined
)
SELECT * FROM Filled

Versionen von SQL Server vor 2022 unterstützen leider „ignore nulls“ nicht, hier kann man dann folgenden Code nutzen:

Filled AS (
    SELECT
        c.ContNo,
        c.MonthEnd,
        c.Amount,
        ca.ForwardFilledAmount
    FROM Combined c
    OUTER APPLY (
        SELECT TOP 1 Amount AS ForwardFilledAmount
        FROM Combined c2
        WHERE c2.ContNo = c.ContNo
          AND c2.MonthEnd <= c.MonthEnd
          AND c2.Amount IS NOT NULL
        ORDER BY c2.MonthEnd DESC
    ) ca
)
SELECT * FROM Filled

OUTER APPLY funktioniert dabei so ähnlich wie ein LEFT JOIN. Der Unterschied besteht darin, dass beim OUTER APPLY die rechte Seite von der linken Seite abhängt. In einem weiteren Artikel zum Thema werde ich noch ein paar Beispiele dazu zeigen. CROSS APPLY ist ähnlich, hier ist es aber kein Äquivalent zum LEFT JOIN, sondern zum INNER JOIN. Auch dazu mehr in einem der weiteren Artikel.

Outer Apply und Cross Apply in SQL nutzen – Die Datenmenge bauen

This entry is part 2 of 3 in the series Outer Apply und Cross Apply

Wir kennen jetzt das Problem, nun können wir uns an die Lösung wagen! In diesem Blogbeitrag werden wir alles vorbereiten, was wir für das anschließende „Forward Filling“ der Daten brauchen.

Im ersten Schritt bestimmen wir für die einzelnen Verträge die Monatsendwerte von Start des jeweiligen Vertrags bis zu dessen Ende. Dazu nutze ich den den folgenden rekursiven CTE (Common Table Expression)

WITH MonthSequence AS (
    SELECT
        c.ContNo,
        EOMONTH(c.StartDate) AS MonthEnd,
        c.EndDate
    FROM Contracts c
    UNION ALL
    SELECT
        ms.ContNo,
        EOMONTH(DATEADD(MONTH, 1, ms.MonthEnd)),
        ms.EndDate
    FROM MonthSequence ms
    WHERE EOMONTH(DATEADD(MONTH, 1, ms.MonthEnd)) <= ms.EndDate
)

Das erzeugt uns die folgende Sequenz:

ContNo MonthEnd EndDate
123 2025-01-31 2025-12-31
123 2025-02-28 2025-12-31
123 2025-03-31 2025-12-31
123 2025-04-30 2025-12-31
123 2025-05-31 2025-12-31
123 2025-06-30 2025-12-31
123 2025-07-31 2025-12-31
123 2025-08-31 2025-12-31
123 2025-09-30 2025-12-31
123 2025-10-31 2025-12-31
123 2025-11-30 2025-12-31
123 2025-12-31 2025-12-31
456 2024-01-31 2024-12-31
456 2024-02-29 2024-12-31
456 2024-03-31 2024-12-31
456 2024-04-30 2024-12-31
456 2024-05-31 2024-12-31
456 2024-06-30 2024-12-31
456 2024-07-31 2024-12-31
456 2024-08-31 2024-12-31
456 2024-09-30 2024-12-31
456 2024-10-31 2024-12-31
456 2024-11-30 2024-12-31
456 2024-12-31 2024-12-31

Als nächstes suchen wir uns die End-of-Month der Cashflows raus, die wir im nächsten Schritt mit den End-of-Month aus Schritt 1 kombinieren werden.

WITH CashflowBuckets AS (
    SELECT
        ContNo,
        EOMONTH(CashflowDate) AS MonthEnd,
        Amount
    FROM Cashflows
)
SELECT * FROM CashflowBuckets
ContNo MonthEnd Amount
123 2025-01-31 100.00
123 2025-04-30 110.00
123 2025-07-31 105.00
123 2025-12-31 120.00
456 2024-01-31 100.00
456 2024-06-30 130.00
456 2024-12-31 101.00

Die Kombination ist dann recht einfach und gibt uns die Liste aller End-of-Months aus mit den dazugehörigen Cashflows.

WITH MonthSequence AS (
    SELECT
        c.ContNo,
        EOMONTH(c.StartDate) AS MonthEnd,
        c.EndDate
    FROM Contracts c
    UNION ALL
    SELECT
        ms.ContNo,
        EOMONTH(DATEADD(MONTH, 1, ms.MonthEnd)),
        ms.EndDate
    FROM MonthSequence ms
    WHERE EOMONTH(DATEADD(MONTH, 1, ms.MonthEnd)) <= ms.EndDate
)
,CashflowBuckets AS (
    SELECT
        ContNo,
        EOMONTH(CashflowDate) AS MonthEnd,
        Amount
    FROM Cashflows
)
,Combined AS (
   SELECT
        ms.ContNo,
        ms.MonthEnd,
        cb.Amount
    FROM MonthSequence ms
    LEFT JOIN CashflowBuckets cb
        ON ms.ContNo = cb.ContNo
        AND ms.MonthEnd = cb.MonthEnd
)
 
SELECT * FROM Combined

In gelb sind die Einträge markiert, die wir im finalen Schritt mit den jeweils letzten gültigen Cashflow-Werten befüllen müssen.

ContNo MonthEnd Amount
123 2025-01-31 100.00
456 2024-01-31 100.00
456 2024-02-29 NULL
456 2024-03-31 NULL
456 2024-04-30 NULL
456 2024-05-31 NULL
456 2024-06-30 130.00
456 2024-07-31 NULL
456 2024-08-31 NULL
456 2024-09-30 NULL
456 2024-10-31 NULL
456 2024-11-30 NULL
456 2024-12-31 101.00
123 2025-02-28 NULL
123 2025-03-31 NULL
123 2025-04-30 110.00
123 2025-05-31 NULL
123 2025-06-30 NULL
123 2025-07-31 105.00
123 2025-08-31 NULL
123 2025-09-30 NULL
123 2025-10-31 NULL
123 2025-11-30 NULL
123 2025-12-31 120.00

Outer Apply und Cross Apply in SQL nutzen – Einleitung

This entry is part 1 of 3 in the series Outer Apply und Cross Apply

Ich halte mich schon recht erfahren im Umgang SQL, kürzlich bin ich aber an einer Ecke von SQL vorbeigekommen, die ich auch noch nicht kannte. Dabei handelt es sind um die „OUTER APPLY“ bzw. „CROSS APPLY“ Operatoren.

Um die Lösung herzuleiten betrachten wir zuerst das Problem:

Gegeben seien Kreditverträge mit verschiedenen Zahlplänen. Ein Vertrag zahlt vielleicht monatlich, einer zahlt quartalsweise. Ein Vertrag hat dabei ein Startdatum und ein Enddatum sowie verschiedene Cashflows.

Hier ein paar Testdaten mit den entsprechenden Tabellen, die dazugehörigen SQL-Statements folgen später.

Cashflows

ContNo StartDate EndDate
123 2025-01-01 2025-12-31
456 2024-01-01 2024-12-31

Cashflows

ContNo CashflowDate Amount
123 2025-01-05 100.0
123 2025-04-07 110.0
123 2025-07-06 105.0
123 2025-12-16 120.0
456 2024-01-05 100.0
456 2024-06-12 130.0
456 2025-12-22 101.0

Das Problem ist jetzt, wie kann man eine monatliche Übersicht pro Vertrag bekommen, bei der in den Monaten, wo es kein Cashflow gab, einfach der letzte Cashflow angezeigt wird? Das Endergebnis soll wie folgt aussehen:

ContNo CashflowDate Amount
123 2025-01-31 100,00
123 2025-02-28 100,00
123 2025-03-31 100,00
123 2025-04-30 110,00
123 2025-05-31 110,00
123 2025-06-30 110,00
123 2025-07-31 105,00
123 2025-08-31 105,00
123 2025-09-30 105,00
123 2025-10-31 105,00
123 2025-11-30 105,00
123 2025-12-31 120,00
456 2024-01-31 100,00
456 2024-02-29 100,00
456 2024-03-31 100,00
456 2024-04-30 100,00
456 2024-05-31 100,00
456 2024-06-30 130,00
456 2024-07-31 130,00
456 2024-08-31 130,00
456 2024-09-30 130,00
456 2024-10-31 130,00
456 2024-11-30 130,00
456 2024-12-31 101,00

Im nächsten Teil fangen wir dann mit den SQL Statements an.

Zahlungspläne mit Python basteln

Für ein kleines Projekt brauchte ich die Möglichkeit, flexible Zahlungspläne zu erzeugen. Der folgende Python-Code tut genau das und erzeugt auch gleich passende INSERT Statements für Postgres.

Das CREATE TABLE ist wie folgt:

CREATE TABLE paymentplan (
    contract INTEGER NOT NULL,
    paymentdate INTEGER NOT NULL,
    paymenttype INTEGER NOT NULL,
    amount NUMERIC(12, 2) NOT NULL
);
import pandas as pd
from datetime import datetime
from dateutil.relativedelta import relativedelta
 
def genPaymentPlan(contract, principal, paymentsperyear, interestrate, startdate, years):
    interest = interestrate/100*principal/paymentsperyear
    date = datetime.strptime(startdate, '%Y%m%d').date()
    output = pd.DataFrame(columns=['Contract', 'Paymentdate', 'Paymenttype', 'Amount'])
 
    # Add a row to the DataFrame
    start = {'Contract': contract, 'Paymentdate': startdate, 'Paymenttype': 1, 'Amount' : principal}
    output = pd.concat([output, pd.DataFrame([start])], ignore_index=True)  
 
    for payment in range(paymentsperyear*years):
        new_date = (date + relativedelta(months=12/paymentsperyear))
        date = new_date
        interest_line = {'Contract': contract, 'Paymentdate': new_date.strftime('%Y%m%d'),'Paymenttype': 2, 'Amount' : -1*interest}
        output = pd.concat([output, pd.DataFrame([interest_line])], ignore_index=True)  
 
    end = {'Contract': contract, 'Paymentdate': date.strftime('%Y%m%d'), 'Paymenttype': 3, 'Amount' : -principal}
    output = pd.concat([output, pd.DataFrame([end])], ignore_index=True)          
    return output
 
 
test = genPaymentPlan(123458, 2000, 2, 12, '20260101', 4)
#print(test)
 
 
 
# Generate INSERT statements
print("Generated SQL INSERT statements:\n")
 
for index, row in test.iterrows():
    contract = int(row['Contract'])
    date = row['Paymentdate']  # already string in format YYYY-MM-DD
    type =  row['Paymenttype'] 
    amount = float(row['Amount'])
 
 
    insert = f"INSERT INTO paymentplan (contract, paymentdate,paymenttype, amount) VALUES ({contract}, '{date}', {type} , {amount});"
    print(insert)

Mit Python pandas CSV nach Excel konvertieren

Hier ein einfacher Code-Schnipsel, um aus CSV-Dateien Excel-Dateien zu machen.

Es empfiehlt sich auch, die Engine explizit zu setzen, mit der die Excel-Datei geschrieben wird. Standardmäßig nutzt pandas openpyxl, xlsxwriter scheint nach meinen Tests aber um ca. 30% schneller zu sein.

import pandas as pd
from openpyxl import Workbook
from openpyxl.utils.dataframe import dataframe_to_rows
 
 
# output control
files = ['']
 
wb = Workbook()
wb.remove(wb['Sheet'])
 
for index, file in enumerate(files,0):
    temp = pd.read_csv(file + '.csv',sep='\t')
    ws1 = wb.create_sheet()
    ws1.title = file
    rows = dataframe_to_rows(temp, index=False, header=True)
 
    for r_idx, row in enumerate(rows, 1):
        for c_idx, value in enumerate(row, 1):
            ws1.cell(row=r_idx, column=c_idx, value=value)
 
wb.save('files45646.xlsx')
wb.close()

Werte in pandas Dataframes ersetzen mit replace()

Aus aktuellem Anlass hier ein einfaches Beispiel, wie man Werte in pandas Dataframes ersetzen kann:

import pandas as pd
 
# initialize data of lists.
data = {'Hersteller': ['VW', 'BMW', 'VW', 'Porsche'],
        'Modell': ['Golf', '1er', 'Polo', '911']}
 
 
df = pd.DataFrame(data)
 
 
print(df,'\n')
 
 
df['Hersteller'] = df['Hersteller'].replace(
    {"VW": "Volkswagen", "Horch": "Audi"})
 
 
print(df)

DuckDB Beispiel-Code für Python

Hier ein Code-Beispiel für die wichtigsten Funktionen von DuckDB.

import duckdb as ddb
import pandas as pd
 
con = ddb.connect(':memory:')
 
con_p = ddb.connect('my_database.db')
 
con_p.execute('CREATE OR REPLACE TABLE telefonnummern(fullname VARCHAR,phone VARCHAR);')
 
 
con_p.execute("INSERT INTO telefonnummern VALUES ('Max Mustermann', '0123-4567890')")
 
print(con_p.sql('SHOW ALL TABLES'))
 
print(con_p.sql('SELECT * FROM telefonnummern;'))
 
 
ddb_object = con_p.sql('SELECT * FROM telefonnummern;')
 
df = ddb_object.to_df()
 
ddb_tuple = ddb_object.fetchall()
 
print(df)
 
print(ddb_tuple)

Python: print() durch ic() ersetzen

Hier ein paar Beispiele, wie man mit icecream print() Ausgaben ersetzen kann.

"""
icecream examples
"""
 
 
from icecream import ic
 
# define some function
def addiere(x, y):
    return x + y
 
# call ice
ic(addiere(1, 2))
 
# Output:
# ic| addiere(1, 2): 3
 
 
d = {'i': 2, 'j': 3, 'k': 4711}
ic(d['k'])
 
 
struct = {
    "hersteller": "VW",
    "modell": "Golf",
    "Farben": ["gelb", "rot"]
}
 
ic(struct)
 
ic.disable()
ic(struct) # no output
ic.enable()
 
 
def logstuff(text):
    # log to output file
    print(text)
 
 
ic.configureOutput(prefix="Hallo| ", outputFunction=logstuff)
 
ic(addiere(7, 7))
 
 
ic.configureOutput(prefix="Welt| ", outputFunction=logstuff)
 
ic(addiere(7, 7))

Mit Python camt.053 aus MT940 erzeugen

Für Dante e.V. bestand die Notwendigkeit, aus MT940 Dateien moderne CAMT.053 zu erzeugen, dank Python wurde das eine lösbare Aufgabe.

Schritt 1

Die MT940 Datei parsen und die Transaktionen in einen pandas DataFrame überführen.

import mt940
import pprint
import pandas as pd

df = pd.DataFrame()

transactions = mt940.parse('Umsaetze_2310007_22.07.2024.mta')

print('Transactions:')
pprint.pprint(transactions.data)

for transaction in transactions:
    print('Transaction: ', transaction)
    pprint.pprint(transaction.data)
    t = transaction.data
    tt = pd.DataFrame(t, index=[0])

    df = pd.concat([df,tt],ignore_index=True)
    
df.to_excel('AllBookings.xlsx',index=False)

Schritt 2

Aus dem DataFrame das XML befüllen, die für den Kopf der XML-Datei notwendigen Kontostandsinformationen holen ich dazu aus der MT940 Datei.

import pandas as pd # data wrangling
import jinja2 # template engine
import os # for file-related stuff
import mt940
from datetime import datetime


today = datetime.today()
now = today.strftime("%Y-%m-%d")



transactions = mt940.parse('Umsaetze_2310007_22.07.2024.mta')

opening = transactions.data['final_opening_balance']
openingamount = str(opening.amount)[:-4]
openingdate = opening.date

closing = transactions.data['final_closing_balance']
closingamount = str(closing.amount)[:-4]
closingdate = closing.date

 
# create jinja env that can load template from filesystem
jinja_env = jinja2.Environment(loader = jinja2.FileSystemLoader(os.path.abspath('.')))
 
df = pd.read_excel('AllBookings.xlsx', dtype={'date': str,'amount':str})

df['CreditDebit'] = ''
df['CreditDebit'] = df['CreditDebit'].where(df['amount'].str.get(0).isin(['-']), 'CRDT')
df['CreditDebit'] = df['CreditDebit'].where(~df['amount'].str.get(0).isin(['-']), 'DBIT')

df['date'] = df['date'].str[:-9]


# Währung weg
df['amount'] = df['amount'].str[:-4]
# Vorzeichen weg
df['amount'] = df['amount'].str.replace('-','')

#df['amount'].replace('-','',inplace=True)
#df["amount"] = df["amount"].apply(lambda x: x.str.replace("-", ""))

template = jinja_env.get_template('Ntry.xml')
 
with open('FertigesXML.xml','w') as output:
    output.write(template.render(data=df,
                                 openingamount=openingamount,
                                 openingdate=openingdate,
                                 closingamount=closingamount,
                                 closingdate=closingdate
                                 ))

Jinja2 XML-Template

Das XML-Template für Jinja2 findet ihr hier:

Ntry_blog