#!/usr/bin/env python
# coding: utf-8

# 
# Kody źródłowe do książki: Python. Uczenie maszynowe w przykładach
#   
# Rozdział 4.: Prognozowanie kliknięć reklam internetowych przy użyciu regresji logistycznej
#   
# Autor: Yuxi (Hayden) Liu (yuxi.liu.ece@gmail.com)
# 

# # Przekształcanie cech kategorialnych w liczbowe: kodowanie porządkowe i „1 z n”

from sklearn.feature_extraction import DictVectorizer


X_dict = [{'zainteresowania': 'technika', 'zawód': 'specjalista'},
          {'zainteresowania': 'moda', 'zawód': 'student'},
          {'zainteresowania': 'moda', 'zawód': 'specjalista'},
          {'zainteresowania': 'sport', 'zawód': 'student'},
          {'zainteresowania': 'technika', 'zawód': 'student'},
          {'zainteresowania': 'technika', 'zawód': 'emeryt'},
          {'zainteresowania': 'sport', 'zawód': 'specjalista'}]


dict_one_hot_encoder = DictVectorizer(sparse=False)
X_encoded = dict_one_hot_encoder.fit_transform(X_dict)
print(X_encoded)


print(dict_one_hot_encoder.vocabulary_)


new_dict = [{'zainteresowania': 'sport', 'zawód': 'emeryt'}]
new_encoded = dict_one_hot_encoder.transform(new_dict)
print(new_encoded)


print(dict_one_hot_encoder.inverse_transform(new_encoded))


# nowa kategoria, nieznana wcześniej 
new_dict = [{'zainteresowania': 'nieznane_zaint', 'zawód': 'emeryt'},
           {'zainteresowania': 'technika', 'zawód': 'nieznany_zawód'}]
new_encoded = dict_one_hot_encoder.transform(new_dict)
print(new_encoded)


import pandas as pd
df = pd.DataFrame({'ocena': ['niska',
                             'wysoka',
                             'średnia',
                             'średnia',
                             'niska']})
print(df)

mapping = {'niska':1, 'średnia':2, 'wysoka':3}
df['ocena'] = df['ocena'].replace(mapping)

print(df)


# # Klasyfikowanie danych z wykorzystaniem regresji logistycznej

# ## Wprowadzenie do funkcji logistycznej

import numpy as np
import matplotlib.pyplot as plt


def sigmoid(input):
    return 1.0 / (1 + np.exp(-input))


z = np.linspace(-8, 8, 1000)
y = sigmoid(z)
plt.plot(z, y)
plt.axhline(y=0, ls='dotted', color='k')
plt.axhline(y=0.5, ls='dotted', color='k')
plt.axhline(y=1, ls='dotted', color='k')
plt.yticks([0.0, 0.25, 0.5, 0.75, 1.0])
plt.xlabel('z')
plt.ylabel('y(z)')
plt.show()


# ## Przejście od funkcji logistycznej do regresji logistycznej

# Wykres przykładowego kosztu i y_hat (prognozy), dla y (truth) = 1
y_hat = np.linspace(0.001, 0.999, 1000)
cost = -np.log(y_hat)
plt.plot(y_hat, cost)
plt.xlabel('Prognoza')
plt.ylabel('Koszt')
plt.xlim(0, 1)
plt.ylim(0, 7)
plt.show()


# Wykres przykładowego kosztu i y_hat (prognozy), dla y (truth) = 0
y_hat = np.linspace(0.001, 0.999, 1000)
cost = -np.log(1 - y_hat)
plt.plot(y_hat, cost)
plt.xlabel('Prognoza')
plt.ylabel('Koszt')
plt.xlim(0, 1)
plt.ylim(0, 7)
plt.show()


# # Trening modelu opartego na regresji logistycznej

# ## Trening modelu opartego na regresji logistycznej z gradientem prostym

# Trenowanie modelu od podstaw
def compute_prediction(X, weights):
    """
    Funkcja wyliczająca prognozę y_hat z wykorzystaniem bieżących wag
    """
    z = np.dot(X, weights)
    return sigmoid(z)


def update_weights_gd(X_train, y_train, weights, learning_rate):
    """
    Funkcja modyfikująca wagi w bieżącym kroku
    """
    predictions = compute_prediction(X_train, weights)
    weights_delta = np.dot(X_train.T, y_train - predictions)
    m = y_train.shape[0]
    weights += learning_rate / float(m) * weights_delta
    return weights


def compute_cost(X, y, weights):
    """
    Funkcja wyliczająca wagi J(w)
    """
    predictions = compute_prediction(X, weights)
    cost = np.mean(-y * np.log(predictions) - (1 - y) * np.log(1 - predictions))
    return cost


def train_logistic_regression(X_train, y_train, max_iter, learning_rate, fit_intercept=False):
    """ Funkcja trenująca model regresji logistycznej
    Argumenty:
        X_train, y_train (numpy.ndarray, treningowy zbiór danych)
        max_iter (int, liczba iteracji)
        learning_rate (float)
        fit_intercept (bool, flaga: z przechwyceniem w0 czy bez niego)
    Wynik:
        numpy.ndarray, wyliczone wagi
    """
    if fit_intercept:
        intercept = np.ones((X_train.shape[0], 1))
        X_train = np.hstack((intercept, X_train))
    weights = np.zeros(X_train.shape[1])
    for iteration in range(max_iter):
        weights = update_weights_gd(X_train, y_train, weights, learning_rate)
        # Wyświetlenie kosztu co 100 iteracji
        if iteration % 100 == 0:
            print(compute_cost(X_train, y_train, weights))
    return weights


def predict(X, weights):
    if X.shape[1] == weights.shape[0] - 1:
        intercept = np.ones((X.shape[0], 1))
        X = np.hstack((intercept, X))
    return compute_prediction(X, weights)


# Przykład
X_train = np.array([[6, 7],
                    [2, 4],
                    [3, 6],
                    [4, 7],
                    [1, 6],
                    [5, 2],
                    [2, 0],
                    [6, 3],
                    [4, 1],
                    [7, 2]])

y_train = np.array([0,
                    0,
                    0,
                    0,
                    0,
                    1,
                    1,
                    1,
                    1,
                    1])


weights = train_logistic_regression(X_train, y_train, max_iter=1000, learning_rate=0.1, fit_intercept=True)



X_test = np.array([[6, 1],
                   [1, 3],
                   [3, 1],
                   [4, 5]])

predictions = predict(X_test, weights)
print(predictions)


plt.scatter(X_train[:5,0], X_train[:5,1], c='b', marker='x')
plt.scatter(X_train[5:,0], X_train[5:,1], c='k', marker='.')
for i, prediction in enumerate(predictions):
    marker = 'X' if prediction < 0.5 else 'o'
    c = 'b' if prediction < 0.5 else 'k'
    plt.scatter(X_test[i,0], X_test[i,1], c=c, marker=marker)
plt.show()


# ## Prognozowanie kliknięć reklam z wykorzystaniem regresji logistycznej z gradientem prostym

import pandas as pd
n_rows = 300000
df = pd.read_csv("train.csv", nrows=n_rows)

X = df.drop(['click', 'id', 'hour', 'device_id', 'device_ip'], axis=1).values
Y = df['click'].values

n_train = 10000
X_train = X[:n_train]
Y_train = Y[:n_train]
X_test = X[n_train:]
Y_test = Y[n_train:]

from sklearn.preprocessing import OneHotEncoder
enc = OneHotEncoder(handle_unknown='ignore')
X_train_enc = enc.fit_transform(X_train)

X_test_enc = enc.transform(X_test)


import timeit
start_time = timeit.default_timer()
weights = train_logistic_regression(X_train_enc.toarray(), Y_train, max_iter=10000, learning_rate=0.01,
                                    fit_intercept=True)
print(f"--- {(timeit.default_timer() - start_time):.3f} s ---")


pred = predict(X_test_enc.toarray(), weights)
from sklearn.metrics import roc_auc_score
print(f'Liczba próbek treningowych: {n_train}, pole pod krzywą ROC dla zbioru treningowego: {roc_auc_score(Y_test, pred):.3f}')

# ## Trening modelu opartego na regresji logistycznej ze stochastycznym gradientem prostym (SGD)

def update_weights_sgd(X_train, y_train, weights, learning_rate):
    """ Pojedyncza iteracja modyfikująca wagi:
        jeden krok na bazie pojedynczej próbki
    Argumenty:
        X_train, y_train (numpy.ndarray, zbiór treningowy)
        weights (numpy.ndarray)
        learning_rate (float)
    Wynik:
        numpy.ndarray, zmodyfikowane wagi
    """
    for X_each, y_each in zip(X_train, y_train):
        prediction = compute_prediction(X_each, weights)
        weights_delta = X_each.T * (y_each - prediction)
        weights += learning_rate * weights_delta
    return weights


def train_logistic_regression_sgd(X_train, y_train, max_iter, learning_rate, fit_intercept=False):
    """ Trening modelu wykorzystującego stochastyczny gradient prosty
    Argumenty:
        X_train, y_train (numpy.ndarray, zbiór treningowy)
        max_iter (int, liczba iteracji)
        learning_rate (float)
        fit_intercept (bool, flaga: z przechwyceniem w0, czy bez niego)
    Wynik:
        numpy.ndarray, wyliczone wagi
    """

    if fit_intercept:
        intercept = np.ones((X_train.shape[0], 1))
        X_train = np.hstack((intercept, X_train))
    weights = np.zeros(X_train.shape[1])
    for iteration in range(max_iter):
        weights = update_weights_sgd(X_train, y_train, weights, learning_rate)
        # Wyświetlenie kosztu co 2 iteracje
        if iteration % 2 == 0:
            print(compute_cost(X_train, y_train, weights))
    return weights


# Wyświetlenie kosztu co 2 iteracje
n_train = 100000
X_train = X[:n_train]
Y_train = Y[:n_train]
X_test = X[n_train:]
Y_test = Y[n_train:]

from sklearn.preprocessing import OneHotEncoder
enc = OneHotEncoder(handle_unknown='ignore')
X_train_enc = enc.fit_transform(X_train)

X_test_enc = enc.transform(X_test)

start_time = timeit.default_timer()
weights = train_logistic_regression_sgd(X_train_enc.toarray(), Y_train, max_iter=10, learning_rate=0.01,
                                        fit_intercept=True)
print(f"--- {(timeit.default_timer() - start_time):.3f} s ---")
pred = predict(X_test_enc.toarray(), weights)
print(f'Liczba próbek treningowych: {n_train}, pole pod krzywą ROC dla zbioru treningowego: {roc_auc_score(Y_test, pred):.3f}')

# # Zastosowanie pakietu scikit-learn 
from sklearn.linear_model import SGDClassifier
sgd_lr = SGDClassifier(loss='log_loss', penalty=None, fit_intercept=True, max_iter=20, learning_rate='constant', eta0=0.01)


sgd_lr.fit(X_train_enc.toarray(), Y_train)

pred = sgd_lr.predict_proba(X_test_enc.toarray())[:, 1]
print(f'Liczba próbek treningowych: {n_train}, pole pod krzywą ROC dla zbioru treningowego: {roc_auc_score(Y_test, pred):.3f}')


# ## Trening modelu opartego na regresji logistycznej z regularyzacją

sgd_lr_l1 = SGDClassifier(loss='log_loss', 
                          penalty='l1', 
                          alpha=0.0001, 
                          fit_intercept=True, 
                          max_iter=10, 
                          learning_rate='constant', 
                          eta0=0.01,
                          random_state=42)
sgd_lr_l1.fit(X_train_enc.toarray(), Y_train)


coef_abs = np.abs(sgd_lr_l1.coef_)
print(coef_abs)


# 10 najniższych wag i odpowiadających im 10 najmniej istotnych cech
print(np.sort(coef_abs)[0][:10])


feature_names = enc.get_feature_names_out()
bottom_10 = np.argsort(coef_abs)[0][:10]
print('10 najmniej istotnych cech:\n', feature_names[bottom_10])

# 10 największych wag i odpowiadających im 10 najbardziej istotnych cech
print(np.sort(coef_abs)[0][-10:])
top_10 = np.argsort(coef_abs)[0][-10:]
print('10 najbardziej istotnych cech:\n', feature_names[top_10])


# ---

# Czytelnicy mogą pominąć następną komórkę.

get_ipython().system('jupyter nbconvert --to python ch4_part1.ipynb --TemplateExporter.exclude_input_prompt=True')

