Kmeans mit Daten aus CSV

mit matplotlib, NumPy, pandas, SciPy, SymPy und weiteren mathematischen Programmbibliotheken.
Antworten
Seppl2202
User
Beiträge: 16
Registriert: Dienstag 10. Juli 2018, 06:24

Hallo zusammen,
ich bin neu hier und auch neu in Python (aber Softwareentwickler in Java, es geht also nur um die Syntax ;) ) und habe ein Problem mit einem privaten Projekt.
Ich möchte einen Kmeans-Algorithmus benutzen, um aus einer CSV-Datei mit Verkaufsstatistiken Muster zu erkennen.
Hier mal ein Beispieldatensatz, wie die CSV aufgebaut ist und wie sie aussehen könnte (ich kenne die Daten im späteren Use Case nicht vorher):

Code: Alles auswählen

ArtikelNummer, Menge, Datum
10001,          250,    2018-07-10
10001,          265,    2018-07-09
10002           380,    2018-07-10
Für jede Artikelnummer gibt es ca 40-55 Einträge. Diese möchte ich nun clustern, um Muster zu erkennen (verkauft sich gut im Sommer/Winter, gleichbleibender Verlauf etc.)
Folgendes habe ich bereits gemacht: CSV in ein Pandas-Dataframe eingelesen, L1 bzw. L2-Normalisierung mit dem sklearn-Package durchgeführt, die Center der Labels geplottet und ein paar Artikel aus der CSV predictet. Allerdings ist der Code auf manuell ausgelegt und den würde ich gerne automatisieren und dafür bräuchte ich Hilfe : )
Hier ist was ich bis jetzt gemacht habe:

Code: Alles auswählen

import pandas as pd
from sklearn.cluster import KMeans
import numpy as np
from matplotlib import pyplot as plp
from sklearn import preprocessing
df = pd.read_csv('statistic.csv', parse_dates=["DATE"], nrows=850)
df_tr = df
clmns = ['QUANTITY', 'ARTICLENO', 'DATE']
df_tr['DATE'] = df_tr['DATE'].astype(int)
# modelling article numbers
df_art1 = df_tr[df_tr['ARTICLENO'] == 430070868]
df_art2 = df_tr[df_tr['ARTICLENO'] == 430070875]
df_art3 = df_tr[df_tr['ARTICLENO'] == 430070899]
df_art4 = df_tr[df_tr['ARTICLENO'] == 430070943]
df_art5 = df_tr[df_tr['ARTICLENO'] == 430070950]
df_art6 = df_tr[df_tr['ARTICLENO'] == 430070967]
df_art7 = df_tr[df_tr['ARTICLENO'] == 430071360]
df_art8 = df_tr[df_tr['ARTICLENO'] == 430073371]
df_art9 = df_tr[df_tr['ARTICLENO'] == 430074590]
df_art10 = df_tr[df_tr['ARTICLENO'] == 430075016]
# predicting articles
df_art11 = df_tr[df_tr['ARTICLENO'] == 430079229]
df_art12 = df_tr[df_tr['ARTICLENO'] == 430079854]
df_art13 = df_tr[df_tr['ARTICLENO'] == 430086845]
df_art14 = df_tr[df_tr['ARTICLENO'] == 430086234]
df_art15 = df_tr[df_tr['ARTICLENO'] == 430086326]

def fillNullValues(art1, art2):
if len(art1) == len(art2):
return
if len(art1) > len(art2):
while len(art1) > len(art2):
art2.append(0)
if len(art1) < len(art2):
while len(art1) < len(art2):
art1.append(0)
return

X = pd.DataFrame(
[df_art1['QUANTITY'].values, df_art2['QUANTITY'].values, df_art3['QUANTITY'].values, df_art4['QUANTITY'].values,
df_art5['QUANTITY'].values, df_art6['QUANTITY'].values, df_art7['QUANTITY'].values, df_art8['QUANTITY'].values,
df_art9['QUANTITY'].values, df_art10['QUANTITY'].values]).fillna(0)

X_L1 = preprocessing.normalize(X, norm='l1')
X_L2 = preprocessing.normalize(X, norm='l2')

df_normalized = (X.pct_change(fill_method='ffill'))
df_normalized = df_normalized.replace([np.inf, -np.inf], np.nan)
df_normalized = df_normalized.fillna(0)
kmeans = KMeans(n_clusters=4, random_state=0).fit(X_L1)
print(kmeans.labels_)

for i in range(2):
df_art11 = df_art11.append([0])
df_art12 = df_art12.append([0])
for i in range(3):
df_art13 = df_art13.append([0])
df_art11 = df_art11.fillna(0)

#normalize predicting data too
df_art11 = preprocessing.normalize([df_art11['QUANTITY']], norm='l2')

df_art12 = df_art12.fillna(0)
df_art12 = preprocessing.normalize([df_art12['QUANTITY']], norm='l2')
df_art13 = df_art13.fillna(0)
df_art13 = preprocessing.normalize([df_art13['QUANTITY']], norm='l2')

# print(kmeans.cluster_centers_)
print("Predicting article 430079229")
print(kmeans.predict(([df_art11[0]])))
print("Predicting article 430079854")
print(kmeans.predict(([df_art12[0]])))
print("Predicting article 430074590")
print(kmeans.predict(([df_art9['QUANTITY'].values])))
plp.plot(kmeans.cluster_centers_[0], label='Center 0')
plp.plot(kmeans.cluster_centers_[1], label='Center 1')
plp.plot(kmeans.cluster_centers_[2], label='Center 2')
plp.plot(kmeans.cluster_centers_[3], label='Center 3')
# plp.plot(kmeans.cluster_centers_[4], label='Center 4')
plp.legend(loc='best')
plp.show()

Wie man sieht, ist der Code sehr bescheiden, weswegen ich ihn gerne automatisieren und optimieren würde.
Für den Anfang möchte ich einen Parameter übergeben, wieviele Reihen eingelesen werden sollen. Dann soll das Dataframe, in dem die ganze CSV liegt, in ein Array von Dataframes zerlegt werden, sodass alle Einträge zu einer Artikelnummer im gleichen Datframe sind. Dieses Array soll dann später zum Clustern verwendet werden. Dann müssen die Dataframes alle auf die gleiche Länge gebracht werden, auch die, die vorhergesagt werden sollen.
Kann mir da jemand helfen?

Vielen Dank und viele Grüße ;)
Benutzeravatar
__blackjack__
User
Beiträge: 13077
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Seppl2202: Die Code-Tags hast Du ja gefunden, trotzdem ist irgendwie die Einrückung verloren gegangen. :-(

Da ist einiges drin was nicht benutzt wird. `fillNullValues()` (was `fill_null_values()` geschrieben werden sollte), `clmns` (ich rate mal es sollte `columns` heissen), `df_art14`, `df_art14`, ``df_normalized, und `X_L2`. Und `df` im Grunde auch, denn das wird ja sofort an den Namen `df_tr` gebunden. Da hätte man auch gleich den Namen `df_tr` verwenden können.

Namen bis auf Konstanten (KOMPLETT_IN_GROSSBUCHSTABEN) und Klassen (MixedCase wie in Java) schreibt man in Python klein_mit_unterstrichen. Weiteres im Style Guide for Python Code.

Die Namen sind auch durch die Bank nichtssagende Kürzel, teils durchnummeriert. Das ist okay wenn man live in einer Python-Shell oder in einem IPython-Notebook experimentiert, aber in einem Programm sollte man Namen wählen an denen jemand der das liest deren Bedeutung ablesen kann. Das schliesst den Programmierer selbst auch ein, wenn der nach ein paar Wochen oder Monaten den Code wieder mal anfassen muss, und nicht mehr frisch im Kopf hat, was er da gemacht hat.

Es sind auch zu viele Namen beziehungsweise zu viel Code für eine Funktion.

Die Anzahl der Namen kann man drastisch kürzen wenn man mit den nummerierten Einzelnamen aufhört und die Werte in eine Liste steckt.

Mir ist nicht so ganz klar warum ``df_art11.fillna(0)`` in einer Schleife steht in der keine neuen NaN-Werte entstehen können. Das macht bis auf den ersten Schleifendurchlauf keinen Sinn. Zudem würde es vom Muster her besser in den Abschnitt nach der Schleife passen wo das ja auch mit `df_art12` und `df_art13` gemacht wird.

Beim erstellen der DataFrame-Objekte für die ”modeling”-Artikelnummern werden unterschiedliche Wege zum Erstellen der ”predicting”-Artikelnummern gemacht, die ich nicht wirklich verstehe. Wenn man das analog lösen würde, könnte man da eine Funktion heraus ziehen.

Das ganze mal etwas aufgeräumter, ohne Gewähr, dass es in Pandas nicht Funktionen gibt, mit denen man es besser lösen kann, und komplett ungetestet:

Code: Alles auswählen

from __future__ import absolute_import, division, print_function
import pandas as pd
from matplotlib import pyplot as plp
from sklearn import preprocessing
from sklearn.cluster import KMeans


def extract_articles(data, article_numbers):
    return pd.DataFrame(
        [
            data[data['ARTICLENO'] == article_no]['QUANTITY'].values
            for article_no in article_numbers
        ],
        columns=article_numbers,
    ).fillna(0)


def main():
    data = pd.read_csv('statistic.csv', parse_dates=['DATE'], nrows=850)
    data['DATE'] = data['DATE'].astype(int)
    
    modeling_article_numbers = [
        430070868, 430070875, 430070899, 430070943, 430070950, 430070967,
        430071360, 430073371, 430074590, 430075016,
    ]
    modeling_data = extract_articles(data, modeling_article_numbers)
    normalized_modeling_data = preprocessing.normalize(modeling_data, norm='l1')

    predicting_article_numbers = [430079229, 430079854, 430086845]
    predicting_article_data = extract_articles(data, predicting_article_numbers)
    normalized_predicting_article_data = preprocessing(
        predicting_article_data, norm='l2'
    )

    kmeans = KMeans(n_clusters=4, random_state=0).fit(normalized_modeling_data)
    print(kmeans.labels_)
    for data, article_no in [
        (normalized_predicting_article_data, 430079229),
        (normalized_predicting_article_data, 430079854),
        (modeling_data, 430074590),
    ]:
        print('Predicting article {0}'.format(article_no))
        print(kmeans.predict(data[article_no]))
    
    for i, cluster_center in enumerate(kmeans.cluster_centers_):
        plp.plot(cluster_center, label='Center {0}'.format(i))
    plp.legend(loc='best')
    plp.show()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Seppl2202
User
Beiträge: 16
Registriert: Dienstag 10. Juli 2018, 06:24

Hallo,
vielen Dank für die schnelle Antwort. Ja, die Einrückungen sind leider verloren gegangen. Ich habe dieses Codeschnippsel einfach angefangen und rumgespielt, bis es das getan hat was es soll, verzeih mir bitte : )
Dein Code wirft in Zeile 14 eine Fehlermeldung:
AssertionError: 10 columns passed, passed data had 54 columns

Die übergebene Liste hat die Länge 10 und zu einer derArtikelnummern darin gibt es 54 Einträge, aber was hat das mit den Spalten zu tun?
Das verstehe ich nicht ganz, denn du versuchst ja die Spalten des Dataframes auf die jeweilige Artikelnummer zu setzen, sodass man später über die Artikelnummer auf die einzelnen Werte zugreifen kann richtig?

Viele Grüße
Benutzeravatar
__blackjack__
User
Beiträge: 13077
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Ah, da muss wohl das ``.values`` weg und man muss schauen wo man dann später `values` vom gesamten DataFrame nehmen muss falls eine Funktion nicht mit einem `DataFrame` als Argument klar kommt.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Seppl2202
User
Beiträge: 16
Registriert: Dienstag 10. Juli 2018, 06:24

Hallo,
wenn ich das .values weglasse,
sind die Mengen im kompletten modelling_dataframe 0.0.
Es sieht dann so aus:

Code: Alles auswählen

           430070868 430070875  ...
QUANTITY   0.0000    0.0000  ...
QUANTITY   0.0000    0.0000 ...
...
Insgesamt gibt es diese Zeilen zehnmal (es gibt zehn Einträge für Quantity, also sozusagen falscherhum, die Quantity muss nach oben und die Artikelnummer an die Seite)
Benutzeravatar
__blackjack__
User
Beiträge: 13077
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Seppl2202: Stimmt, `DataFrame()` erwartet da ja Zeilen. Also `pd.concat()` (wieder ungetestet):

Code: Alles auswählen

def extract_articles(data, article_numbers):
    result = pd.concat(
        [
            data[data['ARTICLENO'] == article_no]['QUANTITY']
            for article_no in article_numbers
        ],
        axis=1,
    ).fillna(0)
    result.columns = article_numbers
    return result
Ich mache das ja komplett im Blindflug ohne Daten zu haben. Besser wäre es wenn Du Dich damit, mit den Daten, interaktiv in einer Python-Shell beschäftigst. Am besten in IPython oder einem Jupyter-Notebook, damit das ein wenig Komfort hat.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Seppl2202
User
Beiträge: 16
Registriert: Dienstag 10. Juli 2018, 06:24

Hallo,
danke nochmal : ) deine neue Variante macht es dann so, dass in der ersten Spalte alle Einträge der ersten Artikelnummer stehen, die restlichen Werte haben in diesem Bereich den Wert 0. In der zweiten Spalte beginnen die Werte der zweiten Artikelnummer dann da, wo die erste endete + 1. Kann man Pandas sagen, dass er alles 'nebeneinander' legen soll?
Benutzeravatar
__blackjack__
User
Beiträge: 13077
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Seppl2202: Kann man machen, aber eigentlich willst Du ja wohl als Index das Datum haben und gegebenenfalls auf die Zeiträume aggregieren die Dich interessieren. Also beispielsweise Monatsweise zusammenfassen und dann Monat oder Jahr und Monat als Index verwenden.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Seppl2202
User
Beiträge: 16
Registriert: Dienstag 10. Juli 2018, 06:24

Hallo,
ja, aber ich hätte ungern "Nullspalten" im Dataframe, weil das im Clustering blöd wird.
Eigentlich hätte ich ganz gerne, dass alle bei Null anfangen und dann der Index das Datum angibt (Da ich für jeden Tag einen Eintrag habe, sollte das gehen).
So könnte ich später auch neue Produkte vorhersagen (z.B. anhand der Entwicklung der ersten 100 Datensätze in einem Custer)
Benutzeravatar
__blackjack__
User
Beiträge: 13077
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Wie gesagt: Nimm das Datum als Index und nicht von 0 aufsteigende Zahlen die keine Bedeutung haben. Um das mal an den Beispieldaten im ersten Beitrag zu demonstrieren:

Code: Alles auswählen

In [203]: df = pd.read_csv('test2.csv', skipinitialspace=True, parse_dates=['Dat
     ...: um'], index_col=['Datum'])

In [204]: df
Out[204]: 
            ArtikelNummer  Menge
Datum                           
2018-07-10          10001    250
2018-07-09          10001    265
2018-07-10          10002    380

In [205]: ns = [10001, 10002]

In [206]: df2 = pd.concat([df[df['ArtikelNummer'] == n]['Menge'] for n in ns], a
     ...: xis=1)

In [207]: df2.columns = ns

In [208]: df2
Out[208]: 
            10001  10002
Datum                   
2018-07-09    265    NaN
2018-07-10    250  380.0
Wenn es für jeden Artikel zu jedem Tag eine Mengenangabe gibt, dann hast Du auch keine NaN-Werte. Wenn es nur wenige Tage gibt an denen mindestens eine Mengenangabe für einen der Artikel fehlt, könntest Du auch einen „inner join“ machen, dann fallen diese Tage raus. Falls das nicht stört.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Seppl2202
User
Beiträge: 16
Registriert: Dienstag 10. Juli 2018, 06:24

Hallo,
es gibt für jeden Tag einen Eintrag, allerdings haben die "Listen" nicht die gleiche Länge (sprich gleiches Enddatum), da die Artikel teilweise nicht am selben Tag "gestartet" wurden.
Wie kann ich dann eine Artikelnummer normalisieren, die vorhergesagt werden soll?
Die braucht ja dann die gleiche Länge wie die Listen im Datframe, mit dem geclustert wurde?
Benutzeravatar
__blackjack__
User
Beiträge: 13077
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Seppl2202: Die sind ja alle gleich lang. Da wo Werte fehlen steht NaN. Kann man wenn nötig durch 0 oder einen anderen Wert ersetzen. Oder Du filterst alle Zeilen aus in denen ein NaN vorkommt, wenn Du auf die Datenpunkte der Artikel verzichten kannst die an dem Tag einen Wert haben.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Seppl2202
User
Beiträge: 16
Registriert: Dienstag 10. Juli 2018, 06:24

Ja, die Einträge in der Liste der vorhersagenden sind unter sich gleich lang, aber sie sind nicht immer gleich lang wie die Einträge im Dataframe, mit denen das Cluster erstellt wurde.
Ich muss da dann mit null auffüllen. Der kmeans-Algorithmus verlangt (aufgrund der euklidschen Distanz) gleich lange Listen
Seppl2202
User
Beiträge: 16
Registriert: Dienstag 10. Juli 2018, 06:24

Also ich habe jetzt mal deinen Ansatz von oben weiter verfolgt:

Code: Alles auswählen

import pandas as pd
from sklearn import preprocessing
from sklearn.cluster import KMeans
from matplotlib import  pyplot as plp

df = pd.read_csv('download.csv', skipinitialspace=True, parse_dates=['DATE'], index_col=['DATE'], nrows=500)
modelling_numbers = [430070868, 430070875]

df2 = pd.concat([df[df['ARTICLENO'] == n]['QUANTITY'] for n in modelling_numbers], axis=1).fillna(0)
df2.columns = modelling_numbers

print(df2)

normalized_modeling_data = preprocessing.normalize(df2, norm='l1')
print(normalized_modeling_data)

kmeans = KMeans(n_clusters=1, random_state=0).fit(normalized_modeling_data)

for i, cluster_center in enumerate(kmeans.cluster_centers_):
  plp.plot(cluster_center, label='Center {0}'.format(i))
plp.legend(loc='best')
plp.show()
Wenn ich nun den Plot betrachte, erhalte ich nur eine Gerade im 45-Grad-Winkel steigend.
Benutzeravatar
__blackjack__
User
Beiträge: 13077
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Seppl2202: Und Du hast etwas anderes erwartet? Wenn ja was? Und warum? Was plottest Du denn da und wie? Was ist auf der X-Achse? Wie ergeben sich in X-Richtung die Abstände der Punkte? Was ist auf der Y-Achse? Macht das überhaupt Sinn? Was bedeutet es diese beiden Datenreihen *so* darzustellen?
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Seppl2202
User
Beiträge: 16
Registriert: Dienstag 10. Juli 2018, 06:24

Das sind die geplotteten Centers des kmeans. Auf der Y-Achse sind die normalisierten Verkaufsmengen, auf der X-Achse noch der Index im Dataframe. Sinn machen tut es nicht, da bei Verkaufsmengen keine Gerade herauskommen sollte (und anhand der CSV weiß ich, dass es auch nicht sein kann).
Aktuell habe ich folgenden Code am laufen, der funktioniert und plausible Ergebnisse liefert:

Code: Alles auswählen

from __future__ import absolute_import, division, print_function
import pandas as pd
import numpy as np
from matplotlib import pyplot as plp
from sklearn import preprocessing
from sklearn.cluster import KMeans
import sys
    def extract_articles(data, article_numbers):
    return pd.DataFrame(
        [
            data[data['ARTICLENO'] == article_no]['QUANTITY'].values
            for article_no in article_numbers
        ]
    ).fillna(0)

    def read_csv_file(file_name, number_of_lines):
        return pd.read_csv(file_name, parse_dates=['DATE'], nrows=number_of_lines)
    def get_unique_article_numbers(data):
        return data['ARTICLENO'].unique()
    def main():
    data = read_csv_file('statistics.csv', 20000)
    data['DATE'] = data['DATE'].astype(int)
    modeling_article_numbers = get_unique_article_numbers(data)
    modeling_data = extract_articles(data, modeling_article_numbers)
    modeling_data = modeling_data.iloc[:, :50]
    normalized_modeling_data = preprocessing.normalize(modeling_data, norm='l2')
    predicting_article_numbers = [430079229, 430079854, 430086845]
    predicting_article_data = extract_articles(data, predicting_article_numbers)
    normalized_predicting_article_data = preprocessing.normalize(
        predicting_article_data, norm='l1'
    )
    # print("Predicting data:", normalized_predicting_article_data)
    print("Size of whole data dataframe:", sys.getsizeof(data), "Bytes")
    # print(normalized_predicting_article_data)

    kmeans = KMeans(n_clusters=4, random_state=0).fit(normalized_modeling_data)
    print(kmeans.labels_)
    # for data, article_no in [
        # (normalized_predicting_article_data, 430079229),
        # (normalized_predicting_article_data, 430079854)
    # ]:
            # print('Predicting article {0}'.format(article_no))
           # print(kmeans.predict(data[0]))
    for i, cluster_center in enumerate(kmeans.cluster_centers_):
        plp.plot(cluster_center, label='Center {0}'.format(i))
        plp.legend(loc='best')
        plp.show()

main()

Durch die Methode get_unique_article_numbers bekomme ich ein Array von allen Artikelnummern im CSV, sodass der Algorithmus seine Modelldaten selbst extrahieren kann. Ich schneide zusätzlich das Dataframe an der 50. Stelle rechts ab, da es nur wenige Artikel mit mehr Datenpunkten gibt und diese dann ewig mit Nullen aufgefüllt werden (man hätte dann für 98% der Einträge 30 oder mehr Nullen am Ende, was das Modell zerstört)
Ich muss noch die Liste der Vorhersagedaten and die Modelldaten anpassen.
Es muss gelten:
foreach model in normalized_modeling_data:
foreach predicting in normalized_predicting_data:
len(model) == len(predicting)
Verstehst du was ich meine? Alle Unterlisten müssen global die gleiche Länge haben.
Antworten