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

# 
# Kody źródłowe do książki: Python. Uczenie maszynowe w przykładach
# 
# Rozdział 2.: Tworzenie systemu rekomendacji filmów na bazie naiwnego klasyfikatora Bayesa
# 
# Autor: Yuxi (Hayden) Liu (yuxi.liu.ece@gmail.com)
# 

# # Implementacja naiwnego klasyfikatora Bayesa

# ## Implementacja naiwnego klasyfikatora Bayesa od podstaw

import numpy as np


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

Y_train = ['T', 'N', 'T', 'T']

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


def get_label_indices(labels):
    """
    Grupowanie próbek na podstawie ich etykiet i zwrócenie indeksów
    @param labels: lista etykiet
    @return: słownik, {klasa1: [indeksy], klasa2: [indeksy]}   
    """
    from collections import defaultdict
    label_indices = defaultdict(list)
    for index, label in enumerate(labels):
        label_indices[label].append(index)
    return label_indices


label_indices = get_label_indices(Y_train)
print('label_indices:\n', label_indices)


def get_prior(label_indices):
    """
    Wyliczenie prawdopodobieństwa a priori na podstawie próbek treningowych
    @param label_indices: indeksy próbek pogrupowane według klas
    @return: słownik, w którym kluczem jest etykieta klasy, a wartością odpowiednie prawdopodobieństwo a priori
    """
    prior = {label: len(indices) for label, indices in label_indices.items()}
    total_count = sum(prior.values())
    for label in prior:
        prior[label] /= total_count
    return prior


prior = get_prior(label_indices)
print('A priori:', prior)


def get_likelihood(features, label_indices, smoothing=0):
    """
    Wyliczenie szansy na podstawie próbek treningowych
    @param features: macierz cech
    @param label_indices: indeksy próbek pogrupowane wg klas
    @param smoothing: liczba całkowita, dodawany współczynnik wygładzający
    @return: słownik, w którym kluczem jest klasa, a wartością
             odpowiedni wektor prawdopodobieństwa warunkowego P(cecha | klasa)
    """
    likelihood = {}
    for label, indices in label_indices.items():
        likelihood[label] = features[indices, :].sum(axis=0) + smoothing
        total_count = len(indices)
        likelihood[label] = likelihood[label] / (total_count + 2 * smoothing)
    return likelihood


smoothing = 1
likelihood = get_likelihood(X_train, label_indices, smoothing)
print('Szansa:\n', likelihood)


def get_posterior(X, prior, likelihood):
    """
    Wyliczenie prawdopodobieństwa a posteriori na podstawie a priori i szansy
    @param X: próbki treningowe
    @param prior: słownik, w którym kluczem jest etykieta klasy, a wartością
                  odpowiednie prawdopodobieństwo a priori
    @param likelihood: słownik, z etkietami klas jako kluczami, i wektorami prawdopodobieństwa warunkowego jako wartościami
    @return: słownik, w którym kluczem jest etykieta klasy, a wartością
             odpowiednie prawdopodobieństwo a posteriori
    """
    posteriors = []
    for x in X:
        # A posteriori jest proporcjonalne do a priori * szansa
        posterior = prior.copy()
        for label, likelihood_label in likelihood.items():
            for index, bool_value in enumerate(x):
                posterior[label] *= likelihood_label[index] if bool_value else (1 - likelihood_label[index])
        # Normalizacja, aby wszystko sumowało się do 1
        sum_posterior = sum(posterior.values())
        for label in posterior:
            if posterior[label] == float('inf'):
                posterior[label] = 1.0
            else:
                posterior[label] /= sum_posterior
        posteriors.append(posterior.copy())
    return posteriors


posterior = get_posterior(X_test, prior, likelihood)
print('A posteriori:\n', posterior)


# ## Implementacja z wykorzystaniem pakietu scikit-learn 

from sklearn.naive_bayes import BernoulliNB


clf = BernoulliNB(alpha=1.0, fit_prior=True)
clf.fit(X_train, Y_train)


pred_prob = clf.predict_proba(X_test)
print('[scikit-learn] Wartości prawdopodobieństwa:\n', pred_prob)


pred = clf.predict(X_test)
print('[scikit-learn] Prognoza:', pred)


# ---

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

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

