import csv
import math
import random

# This is an implementation of the Naive Bayes Algorithm.
# The dataset we are using is comprised of 768 observations of medical
# details for Pima Native American patients.
"""
Pima Native American dataset: https://archive.ics.uci.edu/ml/datasets/pima+indians+diabetes

Row schema:
    1. Number of times pregnant.
    2. Plasma glucose concentration after 2 hours in an oral glucose tolerance test.
    3. Diastolic blood pressure (mm Hg).
    4. Triceps skin fold thickness (mm).
    5. 2-Hour serum insulin (mu U/ml).
    6. Body mass index (weight in kg / (height in m) ^ 2).
    7. Diabetes pedigree function.
    8. Age (years).
    9. Class variable (0 or 1) where 1 == "tested positive for diabetes".
"""

FILENAME = 'pima-data.csv'
SPLIT_RATIO = 0.67

""" Step 1: Handle data -- split into training and test datasets.
"""
# Load data.
def load_csv(filename):
    lines = csv.reader(open(filename, "rb"))
    dataset = list(lines)

    # Convert attributes to floats.
    for i in range(len(dataset)):
        dataset[i] = [float(attr) for attr in dataset[i]] 

    return dataset

# Split dataset.
def split_dataset(dataset, split_ratio):
    """Preprocessing fn to split dataset into training and testing subsets.

    Args:
        dataset (list): List of data entries.
        split_ratio (float): Percentage of dataset to use for training set.

    Returns a tuple of the form (training_set, testing_set).

    """
    training_set_size = int(len(dataset) * split_ratio)
    training_set = []
    testing_set = list(dataset)
    for i in range(training_set_size):
        index = random.randrange(len(testing_set)) # Generate random index from dataset.
        training_set.append(testing_set.pop(index)) # Move entry from testing to training set. 
    return (training_set, testing_set)


""" Step 2: Summarize Data
Naive Bayes model is comprised of a summary of the data in the training
dataset; we will need this summary when we go to make predictions.

The summary of the training data involves the mean and standar devation for
each attribute, by class value. For instance, if there are two class values
and 7 numerical attributes, then we need a mean and standard deviation for
each attribute (7) and class value (2), that is 14 attribute summaries.

We will then use this summarized data to calculate the probability of a
specific attribute values belonging to each class values.
"""
# Separate data by class.
def separate_by_class(dataset):
    """Separates attributes by class. For this particular case, assumes
    class is the last value in each row.

    Args:
        dataset (list): List of data entries.
    
    Returns a dictionary mapping attribute rows to each class.

    """
    
    class_map = {}
    for row in dataset:
        if row[-1] not in class_map:
            class_map[row[-1]] = []
        class_map[row[-1]].append(row)
    return class_map

# Calculate mean.
def mean(numbers):
    return sum(numbers) / float(len(numbers))

# Calculate standard deviation.
def standard_deviation(numbers):
    """Calculated as the square root of the variance where the variance
    is the average of the squared differences for each datapoint from the mean.

    Note: We are using the N - 1 method (Bessel's correction) which subtracts
        1 from the number of datapoints when calculating the variance.
    
    Args:
        numbers (list): Dataset to calculate standard_deviation for.

    Returns a float representing the standard deviation of the numbers arg.
    
    """
    avg = mean(numbers)
    squared_differences = [pow(x - avg, 2) for x in numbers]
    variance = sum(squared_differences) / float(len(numbers) - 1)
    return math.sqrt(variance)

# Summarize dataset.
def summarize(dataset):
    """Summarizes dataset; that is, for a given list of class values,
    calculates the mean and standard deviation for each attribute.

    Args:
        dataset (list): List of data entries.
    
    Returns:
        A list of tuples of the form [
                                        mean(attribute_0), std_dev(attribute_0)
                                        ...
                                     ]

    """
    summaries = [(mean(attribute), standard_deviation(attribute))
                 for attribute in zip(*dataset)]
    del summaries[-1] # last summary is a summary of the classes.
    return summaries

# Summarize attributes by class.
def summarize_by_class(dataset):
    class_map = separate_by_class(dataset)
    summaries = dict()
    for class_key in class_map:
        class_attributes = class_map[class_key]
        summaries[class_key] = summarize(class_attributes)
    return summaries

"""Step 3: Make prediction.
To make a prediction, we will calculate the probabiliy that a given data
instnace belongs to each class, then select the class with the largest
probability as the prediction.
"""

# Calculate Guassian Probability Density Function.
# We use the Gaussian function to estimate the probability of a given
# attribute value, given the known mean and standard deviation for the
# attribute estimated from the training data.
# The result is the conditional probability of a given attribute value
# given a class value.
def calculate_gaussian_probability(val, mean, standard_deviation):
    """Calculates the probability that the passed in attribute value belongs
    to a class given the mean and standard_deviation of that attribute for
    the class.
    """

    exponent = math.exp(
        - (math.pow(val - mean, 2) /
          (2 * math.pow(standard_deviation, 2)))
    )
    return (1 / (math.sqrt(2 * math.pi) * standard_deviation)) * exponent

# Calculate Class Probabilities.
def calculate_class_probabilities(summaries, input_vector):
    """Combines the probabilities of all the attribute values for a data
    instance and comes up with a probability of the entire data instance
    belonging to the class. We combine probabilities of individual attributes
    by multiplying them together.
    
    Returns a dictionary mapping class values to probabilities.

    """
    probabilities = dict()
    for class_key in summaries:
        probabilities[class_key] = 1 # Initilize probabilities.
        class_summaries = summaries[class_key]
        for i in range(len(class_summaries)):
            mean, std_dev = class_summaries[i]
            x = input_vector[i]

            # Multiply current probability by probability of current attribute
            # belonging to the current class.
            probabilities[class_key] *= calculate_gaussian_probability(x, 
                                                                       mean, 
                                                                       std_dev)
    return probabilities

# Make a prediction.
def predict(summaries, input_vector):
    """Calculates class by comparing probabilities that the input_vector
    belongs to each class and returning the class with the maximum probability.
    """
    probabilities = calculate_class_probabilities(summaries, input_vector)
    best_label = None
    best_probability = None
    for class_key in probabilities:
        probability = probabilities[class_key]
        if best_label is None or probability > best_probability:
            best_probability = probability
            best_label = class_key
    return best_label

def get_predictions(summaries, test_set):
    """Makes predictions for each data instance in our test dataset. Used
    to measure the accuracy of our model.
    """
    predictions = []
    for entry in test_set:
        predictions.append(predict(summaries, entry))
    return predictions

# Estimate Accuracy.
def get_accuracy(test_set, predictions):
    """Measures the accuracy of the model.
    """
    correct = 0
    for row in range(len(test_set)):
        if test_set[row][-1] == predictions[row]:
            correct += 1
    return (correct / float(len(testSet))) * 100.0

if __name__ == "__main__":
    dataset = load_csv(FILENAME)
    print len(dataset)

    (training, testing) = split_dataset(dataset, SPLIT_RATIO)
    print(len(training), len(testing))

    dataset = [[1,20,1], [2,21,0], [3,22,1]]
    separated = separate_by_class(dataset)
    print('Separated instances: {0}').format(separated)

    data = [1, 2, 3, 4, 5]
    avg = mean(data)
    print(avg)
    std_dev = standard_deviation(data)
    print(std_dev)

    dataset = [[1,20,0], [2,21,1], [3,22,0]]
    summary = summarize(dataset)
    print('Attribute summaries: {0}').format(summary)

    dataset = [[1,20,1], [2,21,0], [3,22,1], [4,22,0]]