Wann sollte ich jemals pandas apply () in meinem Code verwenden wollen?


109

Ich habe viele Antworten auf Fragen zum Stapelüberlauf gesehen, die die Verwendung der Pandas-Methode betreffen apply. Ich habe auch Benutzer gesehen, die unter ihnen kommentierten und sagten: "apply langsam ist und vermieden werden sollte".

Ich habe viele Artikel zum Thema Leistung gelesen, die erklären, dass applyes langsam ist. Ich habe auch einen Haftungsausschluss in den Dokumenten darüber gesehen, wie applyeinfach eine praktische Funktion zum Übergeben von UDFs ist (kann das jetzt anscheinend nicht finden). Der allgemeine Konsens ist also, dass dies applynach Möglichkeit vermieden werden sollte. Dies wirft jedoch folgende Fragen auf:

  1. Wenn apply es so schlecht ist, warum ist es dann in der API?
  2. Wie und wann soll ich meinen Code machen apply ?
  3. Gibt es jemals Situationen, in denen applyes gut ist (besser als andere mögliche Lösungen)?

1
returns.add(1).apply(np.log)vs. np.log(returns.add(1)ist ein Fall, in dem die applyGeschwindigkeit im Allgemeinen geringfügig schneller ist. Dies ist das grüne Feld unten rechts im Diagramm von jpp unten.
Alexander

@ Alexander danke. Ich habe nicht erschöpfend auf diese Situationen hingewiesen, aber sie sind nützlich zu wissen!
CS95

Antworten:


107

apply, die Komfortfunktion, die Sie nie gebraucht haben

Wir beginnen damit, die Fragen im OP einzeln zu beantworten.

" Wenn bewerben so schlecht ist, warum ist es dann in der API? "

DataFrame.applyund Series.applysind Komfortfunktionen, die für DataFrame- bzw. Serienobjekte definiert sind. applyAkzeptiert alle benutzerdefinierten Funktionen, die eine Transformation / Aggregation auf einen DataFrame anwenden. applyist praktisch eine Silberkugel, die alles tut, was eine bestehende Pandas-Funktion nicht kann.

Einige der Dinge applykönnen tun:

  • Führen Sie eine benutzerdefinierte Funktion auf einem DataFrame oder einer Serie aus
  • Wenden Sie eine Funktion zeilenweise ( axis=1) oder spaltenweise ( axis=0) auf einen DataFrame an
  • Führen Sie die Indexausrichtung durch, während Sie die Funktion anwenden
  • Führen Sie eine Aggregation mit benutzerdefinierten Funktionen durch (wir bevorzugen jedoch normalerweise aggoder transformin diesen Fällen)
  • Führen Sie elementweise Transformationen durch
  • Übertragen Sie aggregierte Ergebnisse in die ursprünglichen Zeilen (siehe result_typeArgument).
  • Akzeptieren Sie Positions- / Schlüsselwortargumente, um sie an die benutzerdefinierten Funktionen zu übergeben.

...Unter anderen. Weitere Informationen finden Sie in der Dokumentation unter Zeilen- oder spaltenweise Funktionsanwendung .

Warum ist es bei all diesen Funktionen applyschlecht? Es ist, weil applyes langsam ist . Pandas macht keine Annahmen über die Art Ihrer Funktion und wendet Ihre Funktion daher bei Bedarf iterativ auf jede Zeile / Spalte an. Darüber hinaus bedeutet die Behandlung aller oben genannten Situationen, dass applybei jeder Iteration ein erheblicher Aufwand entsteht. Ferner applyverbraucht Speicher viel mehr, was eine Herausforderung für Speicher begrenzt Anwendungen ist.

Es gibt nur sehr wenige Situationen, in denen applydie Verwendung angemessen ist (mehr dazu weiter unten). Wenn Sie nicht sicher sind, ob Sie verwenden sollten apply, sollten Sie wahrscheinlich nicht.


Lassen Sie uns die nächste Frage beantworten.

" Wie und wann soll ich meinen Code kostenlos anwenden lassen ? "

Um es neu zu formulieren, hier sind einige häufige Situationen, in denen Sie alle Anrufe an loswerden möchten apply.

Numerische Daten

Wenn Sie mit numerischen Daten arbeiten, gibt es wahrscheinlich bereits eine vektorisierte Cython-Funktion, die genau das tut, was Sie versuchen (wenn nicht, stellen Sie entweder eine Frage zum Stapelüberlauf oder öffnen Sie eine Funktionsanforderung auf GitHub).

Vergleichen Sie die Leistung applyfür eine einfache Additionsoperation.

df = pd.DataFrame({"A": [9, 4, 2, 1], "B": [12, 7, 5, 4]})
df

   A   B
0  9  12
1  4   7
2  2   5
3  1   4

df.apply(np.sum)

A    16
B    28
dtype: int64

df.sum()

A    16
B    28
dtype: int64

In Bezug auf die Leistung gibt es keinen Vergleich, das cythonisierte Äquivalent ist viel schneller. Es ist kein Diagramm erforderlich, da der Unterschied selbst für Spielzeugdaten offensichtlich ist.

%timeit df.apply(np.sum)
%timeit df.sum()
2.22 ms ± 41.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
471 µs ± 8.16 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Selbst wenn Sie das Übergeben von Raw-Arrays mit dem rawArgument aktivieren , ist es immer noch doppelt so langsam.

%timeit df.apply(np.sum, raw=True)
840 µs ± 691 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Ein anderes Beispiel:

df.apply(lambda x: x.max() - x.min())

A    8
B    8
dtype: int64

df.max() - df.min()

A    8
B    8
dtype: int64

%timeit df.apply(lambda x: x.max() - x.min())
%timeit df.max() - df.min()

2.43 ms ± 450 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
1.23 ms ± 14.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Suchen Sie im Allgemeinen nach vektorisierten Alternativen, wenn dies möglich ist.

String / Regex

Pandas bietet in den meisten Situationen "vektorisierte" Zeichenfolgenfunktionen, aber es gibt seltene Fälle, in denen diese Funktionen sozusagen nicht ... "zutreffen".

Ein häufiges Problem besteht darin, zu überprüfen, ob ein Wert in einer Spalte in einer anderen Spalte derselben Zeile vorhanden ist.

df = pd.DataFrame({
    'Name': ['mickey', 'donald', 'minnie'],
    'Title': ['wonderland', "welcome to donald's castle", 'Minnie mouse clubhouse'],
    'Value': [20, 10, 86]})
df

     Name  Value                       Title
0  mickey     20                  wonderland
1  donald     10  welcome to donald's castle
2  minnie     86      Minnie mouse clubhouse

Dies sollte die zweite und dritte Zeile der Zeile zurückgeben, da "Donald" und "Minnie" in ihren jeweiligen "Titel" -Spalten vorhanden sind.

Mit apply würde dies mit using erfolgen

df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)

0    False
1     True
2     True
dtype: bool

df[df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)]

     Name                       Title  Value
1  donald  welcome to donald's castle     10
2  minnie      Minnie mouse clubhouse     86

Es gibt jedoch eine bessere Lösung, wenn Listenverständnisse verwendet werden.

df[[y.lower() in x.lower() for x, y in zip(df['Title'], df['Name'])]]

     Name                       Title  Value
1  donald  welcome to donald's castle     10
2  minnie      Minnie mouse clubhouse     86

%timeit df[df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)]
%timeit df[[y.lower() in x.lower() for x, y in zip(df['Title'], df['Name'])]]

2.85 ms ± 38.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
788 µs ± 16.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Hierbei ist zu beachten, dass iterative Routinen applyaufgrund des geringeren Overheads schneller sind als . Wenn Sie mit NaNs und ungültigen dtypes umgehen müssen, können Sie darauf mit einer benutzerdefinierten Funktion aufbauen, die Sie dann mit Argumenten innerhalb des Listenverständnisses aufrufen können.

Weitere Informationen darüber, wann Listenverständnisse als gute Option angesehen werden sollten, finden Sie in meinem Artikel: Für Schleifen mit Pandas - Wann sollte es mich interessieren? .

Hinweis
Datums- und Datums- / Uhrzeitoperationen haben auch vektorisierte Versionen. So zum Beispiel, sollten Sie es vorziehen pd.to_datetime(df['date']), über, sagen df['date'].apply(pd.to_datetime).

Lesen Sie mehr in den Dokumenten .

Eine häufige Gefahr: Explodierende Spalten von Listen

s = pd.Series([[1, 2]] * 3)
s

0    [1, 2]
1    [1, 2]
2    [1, 2]
dtype: object

Menschen sind versucht zu benutzen apply(pd.Series). Das ist schrecklich in Bezug auf die Leistung.

s.apply(pd.Series)

   0  1
0  1  2
1  1  2
2  1  2

Eine bessere Option besteht darin, die Spalte aufzulisten und an pd.DataFrame zu übergeben.

pd.DataFrame(s.tolist())

   0  1
0  1  2
1  1  2
2  1  2

%timeit s.apply(pd.Series)
%timeit pd.DataFrame(s.tolist())

2.65 ms ± 294 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
816 µs ± 40.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Zuletzt,

" Gibt es Situationen, in denen apply es gut ist? "

Anwenden ist eine praktische Funktion, daher gibt es Situationen, in denen der Overhead vernachlässigbar genug ist, um zu vergeben. Es hängt wirklich davon ab, wie oft die Funktion aufgerufen wird.

Funktionen, die für Serien vektorisiert sind, jedoch keine DataFrames
Was ist, wenn Sie eine Zeichenfolgenoperation auf mehrere Spalten anwenden möchten? Was ist, wenn Sie mehrere Spalten in datetime konvertieren möchten? Diese Funktionen sind nur für Serien vektorisiert, daher müssen sie auf jede Spalte angewendet werden, die Sie konvertieren / bearbeiten möchten.

df = pd.DataFrame(
         pd.date_range('2018-12-31','2019-01-31', freq='2D').date.astype(str).reshape(-1, 2), 
         columns=['date1', 'date2'])
df

       date1      date2
0 2018-12-31 2019-01-02
1 2019-01-04 2019-01-06
2 2019-01-08 2019-01-10
3 2019-01-12 2019-01-14
4 2019-01-16 2019-01-18
5 2019-01-20 2019-01-22
6 2019-01-24 2019-01-26
7 2019-01-28 2019-01-30

df.dtypes

date1    object
date2    object
dtype: object

Dies ist ein zulässiger Fall für apply:

df.apply(pd.to_datetime, errors='coerce').dtypes

date1    datetime64[ns]
date2    datetime64[ns]
dtype: object

Beachten Sie, dass es auch sinnvoll wäre, stackeine explizite Schleife zu verwenden oder einfach nur zu verwenden. Alle diese Optionen sind etwas schneller als die Verwendung apply, aber der Unterschied ist klein genug, um zu vergeben.

%timeit df.apply(pd.to_datetime, errors='coerce')
%timeit pd.to_datetime(df.stack(), errors='coerce').unstack()
%timeit pd.concat([pd.to_datetime(df[c], errors='coerce') for c in df], axis=1)
%timeit for c in df.columns: df[c] = pd.to_datetime(df[c], errors='coerce')

5.49 ms ± 247 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.94 ms ± 48.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.16 ms ± 216 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
2.41 ms ± 1.71 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Sie können einen ähnlichen Fall für andere Operationen wie Zeichenfolgenoperationen oder die Konvertierung in eine Kategorie festlegen.

u = df.apply(lambda x: x.str.contains(...))
v = df.apply(lambda x: x.astype(category))

v / s

u = pd.concat([df[c].str.contains(...) for c in df], axis=1)
v = df.copy()
for c in df:
    v[c] = df[c].astype(category)

Und so weiter...

Konvertieren von Serien in str: astypeversusapply

Dies scheint eine Eigenart der API zu sein. Die Verwendung applyzum Konvertieren von Ganzzahlen in einer Serie in eine Zeichenfolge ist vergleichbar (und manchmal schneller) als die Verwendung astype.

Geben Sie hier die Bildbeschreibung ein Das Diagramm wurde unter Verwendung der perfplotBibliothek aufgezeichnet .

import perfplot

perfplot.show(
    setup=lambda n: pd.Series(np.random.randint(0, n, n)),
    kernels=[
        lambda s: s.astype(str),
        lambda s: s.apply(str)
    ],
    labels=['astype', 'apply'],
    n_range=[2**k for k in range(1, 20)],
    xlabel='N',
    logx=True,
    logy=True,
    equality_check=lambda x, y: (x == y).all())

Bei Schwimmern sehe ich, dass das astypedurchweg so schnell oder etwas schneller ist als apply. Dies hat also damit zu tun, dass die Daten im Test vom Typ Integer sind.

GroupBy Operationen mit verketteten Transformationen

GroupBy.applywurde bisher noch nicht besprochen, ist aber GroupBy.applyauch eine iterative Komfortfunktion, um alles zu handhaben, was die vorhandenen GroupByFunktionen nicht tun.

Eine häufige Anforderung besteht darin, einen GroupBy und dann zwei Hauptoperationen durchzuführen, z. B. einen "verzögerten Cumsum":

df = pd.DataFrame({"A": list('aabcccddee'), "B": [12, 7, 5, 4, 5, 4, 3, 2, 1, 10]})
df

   A   B
0  a  12
1  a   7
2  b   5
3  c   4
4  c   5
5  c   4
6  d   3
7  d   2
8  e   1
9  e  10

Sie benötigen hier zwei aufeinanderfolgende Gruppenanrufe:

df.groupby('A').B.cumsum().groupby(df.A).shift()

0     NaN
1    12.0
2     NaN
3     NaN
4     4.0
5     9.0
6     NaN
7     3.0
8     NaN
9     1.0
Name: B, dtype: float64

Mit applykönnen Sie dies auf einen einzelnen Anruf verkürzen.

df.groupby('A').B.apply(lambda x: x.cumsum().shift())

0     NaN
1    12.0
2     NaN
3     NaN
4     4.0
5     9.0
6     NaN
7     3.0
8     NaN
9     1.0
Name: B, dtype: float64

Es ist sehr schwierig, die Leistung zu quantifizieren, da dies von den Daten abhängt. Aber im Allgemeinen applyist eine akzeptable Lösung, wenn das Ziel darin besteht, einen groupbyAnruf zu reduzieren (weil groupbyes auch ziemlich teuer ist).


Andere Vorsichtsmaßnahmen

Abgesehen von den oben genannten Einschränkungen ist es auch erwähnenswert, dass applydie erste Zeile (oder Spalte) zweimal ausgeführt wird. Dies wird durchgeführt, um festzustellen, ob die Funktion irgendwelche Nebenwirkungen hat. Wenn nicht, applykann möglicherweise ein schneller Pfad zur Auswertung des Ergebnisses verwendet werden, andernfalls wird auf eine langsame Implementierung zurückgegriffen.

df = pd.DataFrame({
    'A': [1, 2],
    'B': ['x', 'y']
})

def func(x):
    print(x['A'])
    return x

df.apply(func, axis=1)

# 1
# 1
# 2
   A  B
0  1  x
1  2  y

Dieses Verhalten tritt auch bei GroupBy.applyPandas-Versionen <0,25 auf (es wurde für 0,25 behoben, siehe hier für weitere Informationen .)


Ich denke, wir müssen vorsichtig sein. Mit %timeit for c in df.columns: df[c] = pd.to_datetime(df[c], errors='coerce')Sicherheit wird es nach der ersten Iteration viel schneller gehen, da Sie datetimezu ... konvertieren datetime.
Jpp

@jpp Ich hatte die gleiche Sorge. In beiden Fällen müssen Sie dennoch einen linearen Scan durchführen. Der Aufruf von to_datetime für Zeichenfolgen ist genauso schnell wie der Aufruf von datetime-Objekten, wenn nicht sogar schneller. Die Ballpark-Zeiten sind die gleichen. Die Alternative wäre, einen Vorkopierschritt für jede zeitgesteuerte Lösung zu implementieren, die vom Hauptpunkt abweicht. Aber es ist ein berechtigtes Anliegen.
CS95

"Das Aufrufen to_datetimevon Zeichenfolgen ist so schnell wie das Aufrufen von ... datetimeObjekten" .. wirklich? Ich enthalten Dataframe - Erstellung (Fixkosten) in applyvs forSchleife Timings und der Unterschied ist viel kleiner.
jpp

@jpp Nun, das habe ich von meinen (zugegebenermaßen begrenzten) Tests bekommen. Ich bin sicher, dass es von den Daten abhängt, aber die allgemeine Idee ist, dass zum Zwecke der Veranschaulichung der Unterschied "ernsthaft, mach dir keine Sorgen" ist.
CS95

1
@ CS95, Frohes neues Jahr!
jpp

47

Nicht alle applysind gleich

Die folgende Tabelle zeigt, wann apply1 zu berücksichtigen ist . Grün bedeutet möglicherweise effizient; rot vermeiden.

Geben Sie hier die Bildbeschreibung ein

Einiges davon ist intuitiv: Es pd.Series.applyhandelt sich um eine zeilenweise Schleife auf Python-Ebene, ebenso pd.DataFrame.applyzeilenweise ( axis=1). Die Missbräuche sind vielfältig und weitreichend. Der andere Beitrag befasst sich eingehender mit ihnen. Beliebte Lösungen sind die Verwendung vektorisierter Methoden, Listenverständnisse (setzt saubere Daten voraus) oder effiziente Tools wie der pd.DataFrameKonstruktor (z apply(pd.Series). B. zur Vermeidung ).

Wenn Sie pd.DataFrame.applyzeilenweise verwenden, ist die Angabe raw=True(soweit möglich) häufig von Vorteil. In diesem Stadium numbaist in der Regel eine bessere Wahl.

GroupBy.apply: allgemein bevorzugt

Wiederholte groupbyVorgänge zur Vermeidung applybeeinträchtigen die Leistung. GroupBy.applyist hier normalerweise in Ordnung, vorausgesetzt, die Methoden, die Sie in Ihrer benutzerdefinierten Funktion verwenden, sind selbst vektorisiert. Manchmal gibt es keine native Pandas-Methode für eine gruppenweise Aggregation, die Sie anwenden möchten. In diesem Fall bietet eine kleine Anzahl von Gruppen applymit einer benutzerdefinierten Funktion möglicherweise immer noch eine angemessene Leistung.

pd.DataFrame.apply säulenweise: ein gemischter Beutel

pd.DataFrame.apply Spaltenweise (axis=0 ) ist ein interessanter Fall. Für eine kleine Anzahl von Zeilen gegenüber einer großen Anzahl von Spalten ist es fast immer teuer. Bei einer großen Anzahl von Zeilen im Verhältnis zu Spalten, dem häufigeren Fall, können manchmal signifikante Leistungsverbesserungen auftreten , wenn apply:

# Python 3.7, Pandas 0.23.4
np.random.seed(0)
df = pd.DataFrame(np.random.random((10**7, 3)))     # Scenario_1, many rows
df = pd.DataFrame(np.random.random((10**4, 10**3))) # Scenario_2, many columns

                                               # Scenario_1  | Scenario_2
%timeit df.sum()                               # 800 ms      | 109 ms
%timeit df.apply(pd.Series.sum)                # 568 ms      | 325 ms

%timeit df.max() - df.min()                    # 1.63 s      | 314 ms
%timeit df.apply(lambda x: x.max() - x.min())  # 838 ms      | 473 ms

%timeit df.mean()                              # 108 ms      | 94.4 ms
%timeit df.apply(pd.Series.mean)               # 276 ms      | 233 ms

1 Es gibt Ausnahmen, aber diese sind normalerweise marginal oder ungewöhnlich. Einige Beispiele:

  1. df['col'].apply(str)kann leicht übertreffen df['col'].astype(str).
  2. df.apply(pd.to_datetime)Das Arbeiten an Zeichenfolgen lässt sich mit Zeilen im Vergleich zu einer regulären forSchleife nicht gut skalieren .

2
Vielen Dank für
Ihre Teilnahme

1
@coldspeed, danke, an deinem Beitrag ist nicht viel auszusetzen (abgesehen von einigen widersprüchlichen Benchmarks gegenüber meinen, könnte aber auf Eingabe oder Einrichtung basieren). Ich hatte nur das Gefühl, dass es eine andere Sichtweise auf das Problem gibt.
Jpp

@jpp Ich habe immer Ihr exzellentes Flussdiagramm als Anleitung verwendet, bis ich heute sah, dass eine Zeileapply deutlich schneller ist als meine Lösung mit any. Irgendwelche Gedanken dazu?
Stef

1
@jpp: Sie haben Recht: Für 1 Million Zeilen x 100 Spalten anyist etwa 100-mal schneller als apply. Es machte meine ersten Tests mit 2000 Zeilen x 1000 Spalten und hier applywar doppelt so schnell wieany
Stef

1
@jpp Ich möchte Ihr Bild in einer Präsentation / einem Artikel verwenden. Bist du damit einverstanden? Ich werde natürlich die Quelle erwähnen. Danke
Erfan

3

Für axis=1(dh zeilenweise Funktionen) können Sie anstelle von einfach die folgende Funktion verwenden apply. Ich frage mich, warum das nicht das pandasVerhalten ist. (Ungetestet mit zusammengesetzten Indizes, aber es scheint viel schneller zu sein als apply)

def faster_df_apply(df, func):
    cols = list(df.columns)
    data, index = [], []
    for row in df.itertuples(index=True):
        row_dict = {f:v for f,v in zip(cols, row[1:])}
        data.append(func(row_dict))
        index.append(row[0])
    return pd.Series(data, index=index)

Ich war sehr überrascht, dass dies in einigen Fällen zu einer besseren Leistung führte. Es war besonders nützlich, wenn ich mehrere Dinge mit jeweils einer anderen Teilmenge von Spaltenwerten ausführen musste. Die Antwort "Alle Bewerbungen sind nicht gleich" hilft möglicherweise dabei, herauszufinden, wann dies wahrscheinlich hilfreich ist, aber es ist nicht sehr schwierig, eine Stichprobe Ihrer Daten zu testen.
Derson

Ein paar Hinweise: Für die Leistung würde ein Listenverständnis die for-Schleife übertreffen; zip(df, row[1:])ist hier ausreichend; Überlegen Sie in diesem Stadium wirklich, numbaob func eine numerische Berechnung ist. In dieser Antwort finden Sie eine Erklärung.
Jpp

@jpp - wenn Sie eine bessere Funktion haben, teilen Sie bitte. Ich denke, das ist aus meiner Analyse ziemlich nahe am Optimum. Ja numbaist schneller, faster_df_applyist für Leute gedacht, die nur etwas wollen, das dem entspricht, aber schneller als das DataFrame.apply(was seltsam langsam ist).
Pete Cacioppi

2

Gibt es jemals Situationen, in denen applyes gut ist? Ja manchmal.

Aufgabe: Unicode-Strings dekodieren.

import numpy as np
import pandas as pd
import unidecode

s = pd.Series(['mañana','Ceñía'])
s.head()
0    mañana
1     Ceñía


s.apply(unidecode.unidecode)
0    manana
1     Cenia

Update
Ich habe mich keineswegs für die Verwendung von ausgesprochen apply, nur gedacht, da das NumPymit der oben genannten Situation nicht umgehen kann, hätte es ein guter Kandidat sein können pandas apply. Aber ich habe dank der Erinnerung von @jpp das einfache Verständnis der alten Liste vergessen.


Nun, nein. Wie ist das besser als [unidecode.unidecode(x) for x in s]oder list(map(unidecode.unidecode, s))?
Jpp

1
Da es bereits eine Pandas Reihe war, habe ich versucht war , zur Nutzung, sind Ja , du Recht, als es ist besser zu Einsatz list-comp gelten, aber downvote war etwas hart, ich nicht befürwortete apply, dachte nur , das eine gute gewesen sein könnte Anwendungsfall.
Astro123
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.