<jupyter_start><jupyter_text>WUM 2020: Projekt II, Milestone II  <NAME>, <NAME> & <NAME> przejdziemy do inżynierii cech, wczytajmy nasze dane i odtwórzmy to, do czego doszliśmy już ostatnio.Pamiętajmy, że nasza cudowna ramka nie ma żadnych braków, co bardzo ułatwia sprawę.Wpierw wczytajmy potrzebne pakiety do obróbki datasetu i jego oglądu wtrakcie i po modyfikacjach.<jupyter_code>import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import random
import warnings
warnings.simplefilter('ignore')
df = pd.read_csv("online_shoppers_intention.csv")
random.seed(123)
df.sample(10)<jupyter_output><empty_output><jupyter_text>Ciągle te same dane - wspaniale!<jupyter_code>df.shape<jupyter_output><empty_output><jupyter_text>Inżynieria cech Przed nami ekscytująca podróż po wnikliwej analizie 18 kolumn. Niech się dzieje!  Encoding Okey, przyjrzyjmy się po kolei naszym kolumnom i poreflektujmy nad każdą z nich.  Month Wpierw zróbmy porządek z miesiącami - tutaj sprawa prosta: zwyczajnie przekonwertujemy ich słowne skróty na numery im odpowiadające (styczeń - 1, luty - 2, itd).<jupyter_code>months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'June', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
for i in range(len(months)):
    df[['Month']] = df[['Month']].replace(months[i], i + 1)
df[['Month']].sample(5)<jupyter_output><empty_output><jupyter_text>Administrative, Informational & ProductRelated Trzy ważne features mówiące o liczbie stron trzech różnych typów, na które wchodzili internauci przy okazji witryny z kupnem produktu. Jak prezentują się ich wartości?<jupyter_code>df.Administrative.value_counts().reset_index()
df.Informational.value_counts().reset_index()
df.ProductRelated.value_counts().reset_index()
df.ProductRelated.nunique()<jupyter_output><empty_output><jupyter_text>Biorąc pod uwagę cel naszego projektu, mimo posiadania skończonej liczby wartości dla tych trzech kolumn odpowiadających za liczbę stron danych typów, na które weszli użytkownicy, zdecydowanie mamy przydatną informację... ale zauważmy, że nie ma dużej różnicy czy klient odwiedził 305 czy 306 stron związanych z produktem. Aby użytkowników o podobnej liczbie "zlać w jedno" i otrzymać potencjalnie podział mówiący bardziej sam za siebie, możemy te trzy kolumny zastąpić trzema nowymi features. *AdministrativeResMan*, *InformationalResMan* i *ProductRelatedResMan* będą zawierać liczby przybliżające informację o tym, jak bardzo klient wnikliwie przeszukiwał inne witryny. Kolejne liczby będą odpowiadać wcześniejszym wartościom kolejno:*AdministrativeResMan*: 0 - (0), 1 - (1-4), 2 - (5-9), 3 - (10-14), 4 - (15-18), 5 - (19-26)*InformationalResMan*: 0 - (0), 1 - (1-2), 2 - (3-4), 3 - (5-7), 4 - (8-10), 5 - (11-16)*ProductRelatedResMan*: 0 - (0), 1 - (1-9), 2 - (10-29), 3 - (30-44), 4 - (45-84), 5 - (85-310) Wskaźnik *ResMan* to nasza autorska miara, która informuje o stopniu bycia tzw. *Research maniac* użytkownika. Jak można się spodziewać, wynik 0 tego wskaźnika oznacza absolutny brak zainteresowania internauty konkretnym rodzajem strony, 1 małe zainteresowanie, 2 umiarkowane, 3 średnie, 4 solidne i 5 maniakalne.Liczby zostały dobrane na podstawie liczb odwiedzeń stron danych kategorii.<jupyter_code>df['AdministrativeResMan']=df['Administrative']
df['InformationalResMan']=df['Informational']
df['ProductRelatedResMan']=df['ProductRelated']
df.loc[(df['Administrative']>=1) & (df['Administrative']<=4), 'AdministrativeResMan'] = 1
df.loc[(df['Administrative']>=5) & (df['Administrative']<=9), 'AdministrativeResMan'] = 2
df.loc[(df['Administrative']>=10) & (df['Administrative']<=14), 'AdministrativeResMan'] = 3
df.loc[(df['Administrative']>=15) & (df['Administrative']<=18), 'AdministrativeResMan'] = 4
df.loc[(df['Administrative']>=19), 'AdministrativeResMan'] = 5
df.loc[(df['Informational']>=1) & (df['Informational']<=9), 'InformationalResMan'] = 1
df.loc[(df['Informational']>=3) & (df['Informational']<=4), 'InformationalResMan'] = 2
df.loc[(df['Informational']>=5) & (df['Informational']<=7), 'InformationalResMan'] = 3
df.loc[(df['Informational']>=8) & (df['Informational']<=10), 'InformationalResMan'] = 4
df.loc[(df['Informational']>=11), 'InformationalResMan'] = 5
df.loc[(df['ProductRelated']>=1) & (df['ProductRelated']<=9), 'ProductRelatedResMan'] = 1
df.loc[(df['ProductRelated']>=10) & (df['ProductRelated']<=29), 'ProductRelatedResMan'] = 2
df.loc[(df['ProductRelated']>=30) & (df['ProductRelated']<=44), 'ProductRelatedResMan'] = 3
df.loc[(df['ProductRelated']>=45) & (df['ProductRelated']<=84), 'ProductRelatedResMan'] = 4
df.loc[(df['ProductRelated']>=85), 'ProductRelatedResMan'] = 5
df.sample(10)<jupyter_output><empty_output><jupyter_text>Mając przygotowane nowe miary, nie będą nam już potrzebne te oryginalne.<jupyter_code>df = df.drop('Administrative', axis = 1)
df = df.drop('Informational', axis = 1)
df = df.drop('ProductRelated', axis = 1)<jupyter_output><empty_output><jupyter_text>VisitorType Co w przypadku *VisitorType*? Na pewno wartości *Returning_Visitor* to coś absolutnie przeciwnego do *New_visitor*. Natomiast w przypadku *Other* sprawa jest już nieco badziej nieoczywista i może być to związane np. z brakiem lub niepewnością danej. Dlatego też w tym wypadku zastosujemy *binary encoding*, korzystając z dwóch dodatkowych feaures, gdzie wartość *returning* przyjmie 0 i 1, *new* - 1 i 0, zaś *other* - 0 i 0.<jupyter_code>df.loc[(df['VisitorType']=='New_Visitor'), 'VisitorType0'] = 1
df.loc[(df['VisitorType']=='New_Visitor'), 'VisitorType1'] = 0
df.loc[(df['VisitorType']=='Returning_Visitor'), 'VisitorType0'] = 0
df.loc[(df['VisitorType']=='Returning_Visitor'), 'VisitorType1'] = 1
df.loc[(df['VisitorType']=='Other'), 'VisitorType0'] = 0
df.loc[(df['VisitorType']=='Other'), 'VisitorType1'] = 0

df = df.drop('VisitorType', axis = 1)
df.sample(10)<jupyter_output><empty_output><jupyter_text>Weekend  & Revunue Tutaj po prostu pozamieniamy wartości False/True na 0/1.<jupyter_code>df.loc[(df['Weekend']==False), 'Weekend'] = 0
df.loc[(df['Weekend']==True), 'Weekend'] = 1
df.loc[(df['Revenue']==False), 'Revenue'] = 0
df.loc[(df['Revenue']==True), 'Revenue'] = 1
df['VisitorType0'] = df['VisitorType0'].astype(object)
df['VisitorType1'] = df['VisitorType1'].astype(object)
df['Revenue'] = df['Revenue'].astype(object)
df['Weekend'] = df['Weekend'].astype(object)
df.sample(10)<jupyter_output><empty_output><jupyter_text>Administrative_Duration, Informational_Duration & ProductRelated_Duration Zróbmy jeszcze operacje z features odpowiadającymi za długości analogiczne do kolumn reprezentujących ilość wizyt na stronach danych kategorii. Przypomnijmy rozkłady *Administrative_Duration*, *Informational_Duration* i *ProductRelated_Duration*.<jupyter_code>duration_columns = ('Administrative_Duration', 'Informational_Duration', 'ProductRelated_Duration')

f, axes = plt.subplots(1, 3, figsize=(13, 8))

for i in range(0, len(duration_columns)):
    sns.distplot(df[duration_columns[i]].dropna(), color="purple", 
                 hist=True, kde=True, kde_kws={'shade': True, 'linewidth': 3, 'bw': 1},
                 ax=axes[i]).set_title('Rozkład zmiennej '+ duration_columns[i])

plt.tight_layout();<jupyter_output><empty_output><jupyter_text>Absolutna większość użytkowników spędza malutki czas w stosunku do najdłuższego pobytu ogólnie. Przyjrzyjmy się wartościom centylowym tych długości dla kolejnych wielokrotności 5.<jupyter_code>df[['Administrative_Duration', 'Informational_Duration', 'ProductRelated_Duration']].quantile([x*0.05 + 0.05 for x in range(20)])<jupyter_output><empty_output><jupyter_text>I co my tu mamy?* dla *Administrative_Duration* rzeczywiście mamy dosyć niezrównoważony rozkład - możemy więc danym czasom przypisać ich wartości centylowe.<jupyter_code>df[['AdministrativeDurPerc']] = df[['Administrative_Duration']]
from scipy import stats
for i in range(len(df[['Administrative_Duration']])):
    df['AdministrativeDurPerc'][i] = stats.percentileofscore(df[['Administrative_Duration']], df['Administrative_Duration'][i])
df[['AdministrativeDurPerc']].sample(10)
df = df.drop('Administrative_Duration', axis = 1)<jupyter_output><empty_output><jupyter_text>Dalej* dla *Informational_Duration* w większości mamy użytkowników, którzy na stronę tego typu nie wchodzą wcale. Ci zaś, którzy ją odwiedzają, siedzą na niej najczęściej do paru minut - można więc wnioskować, że trafiają tam z przypadku bądź aby szybko coś sprawdzić. Prawdziwa śmietanka to Ci, którzy siedzą tam dłużej niż 3 minuty - takich jest zaledwie kilka procent. Dlatego też, zastosujemy encoding, w którym tym pierwszym przypiszemy wartość 0, drugim 1, a trzecim - 2.<jupyter_code>df.loc[(df['Informational_Duration'] > 0) & (df['Informational_Duration'] <= 180), 'Informational_Duration'] = 1
df.loc[(df['Informational_Duration'] > 180), 'Informational_Duration'] = 2
df[['Informational_Duration']].sample(10)<jupyter_output><empty_output><jupyter_text>* dla *ProductRelated_Duration* sytuacja ma się podobnie jak dla *Administrative_Duration* - również i tutaj wartościom przypiszemy ich percentyle.<jupyter_code>df[['ProductRelatedDurPerc']] = df[['ProductRelated_Duration']]
from scipy import stats
for i in range(len(df[['ProductRelated_Duration']])):
    df['ProductRelatedDurPerc'][i] = stats.percentileofscore(df[['ProductRelated_Duration']], df['ProductRelated_Duration'][i])
df[['ProductRelatedDurPerc']].sample(10)
df = df.drop('ProductRelated_Duration', axis = 1)
df.rename(columns = {'Informational_Duration': 'InformationalDurRate'}, inplace = True)
df.sample(10)<jupyter_output><empty_output><jupyter_text>BounceRates, ExitRates & SpecialDay Dyskusyjne jest to, co można z nimi zrobić - postanowiliśmy jednak zaufać przygotowanym miarom i tym, że dane wyliczone liczby są mądrze przekminione.  PageValues Zobaczmy centyle i rozkład owej kolumny.<jupyter_code>df[['PageValues']].quantile([x*0.05 + 0.05 for x in range(20)])<jupyter_output><empty_output><jupyter_text>Również mamy tutaj dosyć nieregularny rozkład, gdzie absolutna większość wartości jest bliska zeru - choć jest i parę bardzo dużych wartości. W tym wypadku zdecydowaliśmy się na **procedurę standaryzacji**.<jupyter_code>from sklearn import preprocessing
scaler = preprocessing.StandardScaler()
df[['PageValues']] = scaler.fit_transform(df[['PageValues']])
df[['PageValues']].sample(10)<jupyter_output><empty_output><jupyter_text>OperatingSystems, Browser, Region & TrafficType I ostatnie kolumny, którymi się zajmiemy - tutaj dla każdej z nich mamy kilka-kilkanaście wartości, gdzie do końca właściwie nie wiemy, co kolejne numery reprezentują - z wyjątkiem tego, że jedne factory pojawiają się zdecydowanie częściej niż inne.<jupyter_code>df[['OperatingSystems', 'Browser', 'Region', 'TrafficType']].nunique()<jupyter_output><empty_output><jupyter_text>Biorąc pod uwage, że mamy tutaj jednak niepowiązane ze sobą kategorie, a nie jakikolwiek sensowny numeryczny porządek, dla nich z tego względu zdecydowaliśmy się na **OneHotEncoding**.<jupyter_code>oper = np.array(df[["OperatingSystems"]])
brow = np.array(df[["Browser"]])
reg = np.array(df[["Region"]])
traf = np.array(df[["TrafficType"]])

from sklearn.preprocessing import LabelEncoder

leOper = LabelEncoder()
leBrow = LabelEncoder()
leReg = LabelEncoder()
leTraf = LabelEncoder()
operIntEnc = leOper.fit_transform(oper)
browIntEnc = leBrow.fit_transform(brow)
regIntEnc = leReg.fit_transform(reg)
trafIntEnc = leTraf.fit_transform(traf)
from sklearn.preprocessing import OneHotEncoder

operOnehotEncoder = OneHotEncoder(sparse=False)
browOnehotEncoder = OneHotEncoder(sparse=False)
regOnehotEncoder = OneHotEncoder(sparse=False)
trafOnehotEncoder = OneHotEncoder(sparse=False)

operIntEnc = operIntEnc.reshape(len(operIntEnc), 1)
browIntEnc = browIntEnc.reshape(len(browIntEnc), 1)
regIntEnc = regIntEnc.reshape(len(regIntEnc), 1)
trafIntEnc = trafIntEnc.reshape(len(trafIntEnc), 1)

operOnehotEncoded = operOnehotEncoder.fit_transform(operIntEnc)
browOnehotEncoded = browOnehotEncoder.fit_transform(browIntEnc)
regOnehotEncoded = regOnehotEncoder.fit_transform(regIntEnc)
trafOnehotEncoded = trafOnehotEncoder.fit_transform(trafIntEnc)
print(operOnehotEncoded.shape)

# invert 
#inverted = onehot_encoder.inverse_transform(onehot_encoded)
#print(inverted.transpose())
pd.DataFrame(operOnehotEncoded)
pd.DataFrame(operOnehotEncoded).columns
['OperatingSystems' + str(col) for col in pd.DataFrame(operOnehotEncoded).columns]
dfOperOnehot = pd.DataFrame(operOnehotEncoded)
dfBrowOnehot = pd.DataFrame(browOnehotEncoded)
dfRegOnehot = pd.DataFrame(regOnehotEncoded)
dfTrafOnehot = pd.DataFrame(trafOnehotEncoded)

dfOperOnehot.columns = ['OperatingSystems' + str(col) for col in pd.DataFrame(operOnehotEncoded).columns]
dfBrowOnehot.columns = ['Browser' + str(col) for col in pd.DataFrame(browOnehotEncoded).columns]
dfRegOnehot.columns = ['Region' + str(col) for col in pd.DataFrame(regOnehotEncoded).columns]
dfTrafOnehot.columns = ['TrafficType' + str(col) for col in pd.DataFrame(trafOnehotEncoded).columns]
dfOperOnehot.sample(10)
df = pd.concat([df, dfOperOnehot, dfBrowOnehot, dfRegOnehot, dfTrafOnehot], axis = 1)
df.sample(10)
df.shape
df = df.drop(['OperatingSystems', 'Browser', 'Region', 'TrafficType'], axis = 1)
df.sample(10)<jupyter_output><empty_output><jupyter_text>Normalizacja Mamy już dobrze wyznaczone wartości dla kolejnych kolumn. Biorąc ramkę danych zbiorczo, warto by jednak było, by każda kolumna miała wejściowo takie samo znaczenie - zastosujemy więc standardową normalizację, w celu zachowania wszystkich wartości w przedziale .<jupyter_code>df = (df-df.min())/(df.max()-df.min())
df.sample(10)<jupyter_output><empty_output><jupyter_text>Zanim przejdziemy do wstępnego modelowania, sztucznie zmniejszymy jeszcze ważność tabelek powstałych przez *One-hot encoding*. Zapewne kilkadziesiąt features mówiących o regionie czy przeglądarce będą miały o wiele mniejszą ważność w procesie klasteryzacji niż chociażby miara *SpecialDay* czy czas spędzony na stronach związanych z produktem... więc wszystkie jedynki utworzone dzięki one-hotowi zmienimy na 0.3, tym samym zmniejszając istotność tych nowych utworzonych przez nas wartości.<jupyter_code>for col in range(15, 65):
    df.iloc[:, col][df.iloc[:, col] == 1] = 0.3
    
df.sample(10)
df['TrafficType1'].sample(10)<jupyter_output><empty_output><jupyter_text>Sukces!  Wstępne modelowanie Użyjemy Kmeans, DBSCAN, GMM  Kmeans<jupyter_code>from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
n = []
score = []

for n_clusters in range(2,17):
    clusterer = KMeans(n_clusters, random_state = 0)
    preds = clusterer.fit_predict(df)
    n.append(n_clusters)
    score.append(silhouette_score(df, preds))
    
plt.plot(n,score,'^-', color="purple")
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score

kmeans = KMeans(n_clusters = 2, random_state = 0)
kmeans.fit(df)
silhouette_score(df, kmeans.fit_predict(df))<jupyter_output><empty_output><jupyter_text>Tutaj jak widać najlepsze wyniki silhouette score daje dla dwóch klastrów. Nie jest to jednak co prawda imponujący wynik bo wychodzi w okolicach ~0.27.<jupyter_code>x = kmeans.predict(df)
rev = df['Revenue']
rev = np.asarray(rev)
rev = rev.astype(int)
x[x == 0] = 2
x[x == 1] = 0
x[x == 2] = 1
from sklearn import metrics
metrics.accuracy_score(rev, x)
metrics.precision_score(rev, x)<jupyter_output><empty_output><jupyter_text>Całkiem ciekawe jest że przypisania przy dwóch klastrach mają 77% accuracy rate dla predykcji 'revenue'.  Jednak jeśli weźmiemy pod uwagę niezbalansowanie klas i miarę precision to wyniki nie są już takie zadowalające.  MiniBatch Kmeans Zobaczymy jak radzi sobie zmodyfikowany algorytm Kmeans<jupyter_code>from sklearn.cluster import MiniBatchKMeans
mbKmeans = MiniBatchKMeans(n_clusters=2)
mbKmeans.fit(df)
pred = mbKmeans.predict(df)
pred
silhouette_score(df, pred)<jupyter_output><empty_output><jupyter_text>DBSCAN<jupyter_code>from sklearn.cluster import DBSCAN
e = [0.0001, 0.001, 0.01, 0.05, 0.1, 0.3, 0.5]
score = []

for eps in e:
    clusterer = DBSCAN(eps)
    preds = clusterer.fit_predict(df)
    score.append(silhouette_score(df, preds))
y_pos = np.arange(len(e))
plt.bar(y_pos, score, color='purple')
plt.xticks(y_pos, e)<jupyter_output><empty_output><jupyter_text>Wygląda na to że DBSCAN średnio radzi sobie z tym datasetem.<jupyter_code>dbscan = DBSCAN(eps=0.001)
dbscan.fit(df)
x1 = dbscan.labels_.astype(int)
np.unique(x1)
preds = dbscan.fit_predict(df)
silhouette_score(df, preds)<jupyter_output><empty_output><jupyter_text>Nawet w najlepszym przypadku wynik silhouette score jest bardzo niski.  OPTICS Zmodyfikowana wersja algorytmu DBSCAN.<jupyter_code>from sklearn.cluster import OPTICS
opt = OPTICS(eps=0.001, min_samples=10)
opt_pred = opt.fit_predict(df)
np.unique(opt_pred)<jupyter_output><empty_output><jupyter_text>Jak optics również dzieli nasz zbiór danych na dużą liczbę klastrów, co w praktyce jest nieinterpretowalne.  GMM<jupyter_code>from sklearn import mixture

n = []
score = []

for n_clusters in range(2,17):
    clusterer = mixture.GaussianMixture(n_clusters, random_state = 0)
    preds = clusterer.fit_predict(df)
    n.append(n_clusters)
    score.append(silhouette_score(df, preds))
    
plt.plot(n,score,'^-', color="purple")<jupyter_output><empty_output><jupyter_text>Jak widać dla GMM najlepszą ilością klastrów jest 5.<jupyter_code>gmm = mixture.GaussianMixture(n_components=5, random_state = 0)
gmm.fit(df)
silhouette_score(df, gmm.fit_predict(df))<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.12536310303582618, 'fraction_numerical': 0.01856809249871846, 'mean_word_length': 7.417066155321189, 'pattern_counts': {'":': 0, '<': 125, '<?xml version=': 0, '>': 129, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '27', 'dir': 'jupyter-structured-clean-dedup', 'id': '199337', 'license': 'Apache-2.0', 'n_tokens_mistral': 7018, 'n_tokens_neox': 6846, 'n_words': 1436, 'path': 'Projekty/Projekt2/Grupa2/KosternaMrozPodsiad/Projekt2_KM2.ipynb', 'repo_name': 'welkierk/2020L-WUM', 'size': '17589'}