# Name: Scott Cagnard
# Homework 5: Election prediction

import csv
import os
import time

def read_csv(path):
    """Reads the CSV file at path, and returns a list of rows from the file.

    Parameters:
        path: path to a CSV file. 

    Returns:
        list of dictionaries: Each dictionary maps the columns of the CSV file
        to the values found in one row of the CSV file. Although this function 
        will work for any csv file, for our purposes, depending on the contents
        of the CSV file, this will typically be a list of *ElectionDataRow*s or
        a list of *PollDataRow*s (or a list of electoral college data rows).
    """
    output = []
    for row in csv.DictReader(open(path)):
        output.append(row)
    return output


################################################################################
# Problem 1: State edges
################################################################################

def row_to_edge(row):
    """Given an *ElectionDataRow* or *PollDataRow*, returns the 
    Democratic *Edge* in that *State*.

    Parameters:
        row: an *ElectionDataRow* or *PollDataRow* for a particular *State*

    Returns:
        Democratic *Edge* in that *State*: a float
    """
    return float(row["Dem"]) - float(row["Rep"])

 
def state_edges(election_result_rows):
    """Given a list of *ElectionDataRow*s, returns *StateEdge*s.

    Parameters:
        election_result_rows: list of *ElectionDataRow*s 
            This list has no duplicate *States*; that is, each *State* is 
            represented at most once in the input list.

    Returns:
        *StateEdges*: 
            dictionary from *State* (string) to *Edge* (float)
    """

    state_edges = {}                     #Create empty dictionary
    for row in election_result_rows:    #Iterate through each row
        state = row['State']      
        edge = row_to_edge(row)
        state_edges[state] = edge   #Map each state to the democratic edge of the state

    return state_edges     #Return the new dictionary 
    
    

################################################################################
# Problem 2: Find the most recent poll row
################################################################################

def earlier_date(date1, date2):
    """Given two dates as strings, returns True if date1 is before date2.

    Parameters:
        date1: a string representing a date (formatted like "Oct 06 2012")
        date2: a string representing a date (formatted like "Oct 06 2012")

    Returns:
        bool: True if date1 is before date2.
    """
    return (time.strptime(date1, "%b %d %Y") < time.strptime(date2, "%b %d %Y"))


def most_recent_poll_row(poll_rows, pollster, state):
    """ Given a list of *PollDataRow*s, returns the most recent row with the
    specified *Pollster* and *State*. If no such row exists, returns None.

    Parameters:
        poll_rows: a list of *PollDataRow*s
        pollster: a string representing a *Pollster*
        state: a string representing a *State*

    Returns:
        A *PollDataRow*: a dictionary from string to string  OR 
        None, if no such row exists
    """

    mostrecent = None     #Set a variable equal to None
    date2 = 'Feb 10 1900' #Create an old formatted date
    for row in poll_rows:      #Iterate through each row
        date = row['Date']     #Extract the date of each row
        if row['State'] == state and row['Pollster'] == pollster and earlier_date(date, date2) == False:
            mostrecent = row    
            date2 = row['Date'] 
    return mostrecent

#If there is a state and pollster and it is the most recent poll, then
#we can return it as an output for our most recent poll.
#If not, it will produce the output None

################################################################################
# Problem 3: Pollster predictions
################################################################################

def unique_column_values(rows, column_name):
    """Given a list of rows and the name of a column (a string), 
    returns a set containing all values in that column.

    Parmeters:
        rows: a list of rows (could be a *PollDataRow* or another type of row)
        column_name: a string
    
    Returns:
        A set: containing all unique values in column `column_name`
    """

    values = []      #Create an empty list
    for row in rows:       #Iterate through each row
        values.append(row[column_name])    
    values = set(values)
    return values

#Append each unique value and transform it into a set and
#return the set as an output


def pollster_predictions(poll_rows):
    """Given a list of *PollDataRow*s, returns *PollsterPredictions*.
    For a given pollster, uses only the most recent poll for a state.

    Parameters:
        poll_rows: a list of *PollDataRow*s

    Returns:
        A *PollsterPredictions*: a dictionary from *Pollster* to *StateEdges*
    """



    dict = {}      #Create an empty dictionary
    dict = dict.fromkeys(unique_column_values(poll_rows, "Pollster"))

    #Extract all the unique Pollster values and assign them as specific keys
    
    for keys in dict:   #Iterate through each key of the dictionary
        list = []            #Create an empty list
        for poll_row in poll_rows:     #Iterate through each row
            if (most_recent_poll_row(poll_rows, keys, poll_row["State"]) != None):
                list.append(most_recent_poll_row(poll_rows, keys, poll_row["State"]))
                dict[keys] = state_edges(list)
    return dict

    #Using only the most recent poll for a state, the pollsters become the key
    #of the dictionary, and are assigned to the value of the state and its edge

################################################################################
# Problem 4: Pollster errors
################################################################################

def average_error(state_edges_predicted, state_edges_actual):
    """Given predicted *StateEdges* and actual *StateEdges*, returns
    the average error of the prediction.

    For each state present in state_edges_predicted, its error is
    calculated.  The average of all of these errors is returned.

    Parameters:
        state_edges_predicted: *StateEdges*, a dictionary from *State* to *Edge*
        state_edges_actual: *StateEdges*, a dictionary from *State* to *Edge*

    Returns:
        float: the average error
    """

    d1 = state_edges_predicted   #Assign the predicted state edges to a variable
    length = list(state_edges_predicted.values())   #Create a list of the predicted state edge values
    z = len(length)   #Find the number of elements in the list "length"
    d2 = state_edges_actual  #Assign the actual state edge values to a variable
    d3 = {}        #Create an empty dictionary

    for k, v in d1.items():   
        d3[k] = v - d2.get(k, 0)    #Find the difference between the predicted values and the actual values

    list1 = list(d3.values())   #Assign the differences to a list
    x = list(map(abs, list1))    #Take the absolute value of the differences
    a = sum(x)      #Take the sum of the new values

    return a/z       #Calculate the average of all the errors


def pollster_errors(pollster_predictions, state_edges_actual):
    """Given *PollsterPredictions* and actual *StateEdges*, 
    retuns *PollsterErrors*.

    Parameters:
        pollster_predictions: *PollsterPredictionss*, dictionary from *Pollster*
            to *StateEdges*
        state_edges_actual: *StateEdges*, dictionary from *State* to *Edge*

    Returns:
        *PollsterErrors*: a dictionary from *Pollster* to float (The float
        represents the *Pollster*'s average error).
    """

    totalAverage = {}      #Create an empty dictionary

    for k in pollster_predictions:
        states = pollster_predictions[k]
        for j in states:
            if j in state_edges_actual:    
                average = average_error(pollster_predictions[k], state_edges_actual)
                totalAverage[k] = average 
    #Map each pollster to its calculated average error of each state

    return totalAverage
    


################################################################################
# Problem 5: Pivot a nested dictionary
################################################################################

def pivot_nested_dict(nested_dict):
    """Pivots a nested dictionary, producing a different nested dictionary
    containing the same values.

    The input is a dictionary d1 that maps from keys k1 to dictionaries d2,
    where d2 maps from keys k2 to values v.
    The output is a dictionary d3 that maps from keys k2 to dictionaries d4,
    where d4 maps from keys k1 to values v.
    For example:
      input = { "a" : { "x": 1, "y": 2 },
                "b" : { "x": 3, "z": 4 } }
      output = {'y': {'a': 2},
                'x': {'a': 1, 'b': 3},
                'z': {'b': 4} }
    """

    reverse_nest_dict = {}      #Create an empty dictionary
    for k, v in nested_dict.items():   #Iterate through each pair of elements
        for k2, v2 in v.items():   #Iterate through pair of values
            try:
                reverse_nest_dict[k2][k] = v2
            except KeyError:
                reverse_nest_dict[k2] = { k : v2 }
    return reverse_nest_dict
    
     #Create a dictionary that produces a different nested dictionary which
     #contains the same values


################################################################################
# Problem 6: Average the edges in a single state
################################################################################

def average_error_to_weight(error):
    """Given the average error of a *Pollster*, returns that pollster's weight.

    Parameters:
        error: a float representing a *Pollster*'s average error, 
            The error must be a positive number.

    Returns:
        float: weight, calculated as 1/(error)^2
    """
    return error ** (-2)

# The default average error of a pollster who did no polling in the
# previous election.
DEFAULT_AVERAGE_ERROR = 5.0

def pollster_to_weight(pollster, pollster_errors):
    """"Given a *Pollster* and a *PollsterErrors*, return 
    the given pollster's weight.

    Parameters:
        pollster: *Pollster*, a string
        pollster_errors: *PollsterErrors*, a dictionary from *Pollster* to float

    Returns:
        float: weight
    """
    if pollster not in pollster_errors:
        weight = average_error_to_weight(DEFAULT_AVERAGE_ERROR)
    else:
        weight = average_error_to_weight(pollster_errors[pollster])
    return weight


def weighted_average(items, weights):
    """Returns the weighted average of a list of items.

    Parameters:
        items: a list of numbers.
        weights: a list of numbers, whose sum is nonzero.
            Each weight in weights corresponds to the item in items at 
            the same index. items and weights must be the same length.

    Returns:
        float: the weighted average, the sum of (product of each item and 
        its weight) divided by (the sum of the weights)
    """
    
    assert len(items) > 0
    assert len(items) == len(weights)

    a = items       #Assign the items to a variable
    b = weights     #Assign the weights to a variable
    x = list(items)  #Transform the items to a list
    y = list(weights)   #Transform the weights to a list
    sum1 = sum(weights)    #Sum up all of the weights
    z = [a*b for a,b in zip(x, y)]  #Multiply both lists by matching up the elements from both lists
    sum2 = sum(list(z))   #Take the sum of all of the products
    
    return float(sum2/sum1)   #Divide the sum of the products by the sum of the weights to get the weighted average


def average_edge(pollster_edges, pollster_errors):
    """Given *PollsterEdges* and *PollsterErrors*, returns the average 
    of these *Edge*s weighted by their respective *PollsterErrors*.
    
    Parameters:
        pollster_edges: *PollsterEdges*, a dictionary from *Pollster* to *Edge*
        pollster_errors: *PollsterErrors*, a dictionary from *Pollster* to float

    Returns:
        float: the weighted average of the *Edge*s, weighted by the errors
    """

    a = list(pollster_edges.values())   #List the values of the pollster edges
    alen = len(a)      #Obtain the length of the list of values above
    b = list(pollster_edges.keys())   #List the keys of the pollster edges
    pollster_errors = pollster_errors 
    c = []      #Create an empty list
    for i in b:   #Iterate through each key
        c.append(pollster_to_weight(i, pollster_errors))  #Calcualte the weight of each pollster error
    c1 = c[0:alen]  #Ignore the pollster errors that dont have a pollster edge
    final = weighted_average(a, c1)
    #Use the weighted average function to get the weighted average function
    #to get the weighted avergae of the Edge's, which are weighted by the errors
    

    return  final


################################################################################
# Problem 7: Predict the 2012 election
################################################################################

def predict_state_edges(pollster_predictions, pollster_errors):
    """Given *PollsterPredictions* from a current election and *PollsterErrors*
    from a past election, returns predicted *StateEdges* of the current election.

    Parameters:
        pollster_predictions: *PollsterPredictions*, a dictionary from 
            *Pollster* to *StateEdges*
        pollster_errors: *PollsterErrors*, a dictionary from *Pollster* to float

    Returns:
        *StateEdges*: predicted *StateEdges* of the current election
    """

    dict = {}     #Create an empty dictionary
    pollster_state_prediction = pivot_nested_dict(pollster_predictions)
    for state in pollster_state_prediction:
        dict[state] = average_edge(pollster_state_prediction[state], pollster_errors)
    return dict
    
    #Iterate through each state and calculate the predicted edge for each state


################################################################################
# Electoral College, Main Function, etc.
################################################################################

def electoral_college_outcome(ec_rows, state_edges):
    """Given electoral college rows and *StateEdges*, returns the outcome of
    the Electoral College.

    Parameters:
        ec_rows: a list of electoral college rows, where an electoral college
            row is a dictionary from string to string (similar to an
            *ElectionDataRow* or a *PollDataRow* but with different keys)
        state_edges: *StateEdges*, a dictionary from *State* to *Edge*
        
    Returns:
        dictionary containing only the keys "Dem" and "Rep", mapped to a 
        number (as a float) of electoral votes won by that party.  
        If a state has an edge of exactly 0.0, its votes are evenly divided 
        between both parties.
    """
    ec_votes = {}               # maps from state to number of electoral votes
    for row in ec_rows:
        ec_votes[row["State"]] = float(row["Electors"])

    outcome = {"Dem": 0, "Rep": 0}
    for state in state_edges:
        votes = ec_votes[state]
        if state_edges[state] > 0:
            outcome["Dem"] += votes
        elif state_edges[state] < 0:
            outcome["Rep"] += votes
        else:
            outcome["Dem"] += votes/2.0
            outcome["Rep"] += votes/2.0
    return outcome


def print_dict(dictionary):
    """Given a dictionary, prints its contents in sorted order by key.
    Rounds float values to 8 decimal places.

    Returns:
        None
    """
    for key in sorted(dictionary.keys()):
        value = dictionary[key]
        if type(value) == float:
            value = round(value, 8)
        print (key, value)


def main():
    """Main function, executed when election.py is run as a Python script.
    """
    # Read state edges from the 2008 election
    edges_2008 = state_edges(read_csv("data/2008-results.csv"))

    # Read pollster predictions from the 2008 and 2012 election
    polls_2008 = pollster_predictions(read_csv("data/2008-polls.csv"))
    polls_2012 = pollster_predictions(read_csv("data/2012-polls.csv"))

    # Compute pollster errors for the 2008 election
    error_2008 = pollster_errors(polls_2008, edges_2008)

    # Predict the 2012 state edges
    prediction_2012 = predict_state_edges(polls_2012, error_2008)

    # Predict the 2012 Electoral College outcome
    ec_2012 = electoral_college_outcome(read_csv("data/2012-electoral-college.csv"), prediction_2012)

    print("Predicted 2012 election results:")
    print_dict(prediction_2012)
    print()

    print("Predicted 2012 Electoral College outcome:")
    print_dict(ec_2012)
    print()


# If this file, election.py, is run as a Python script (such as by typing
# "python election.py" at the command shell), then run the main() function.
#if __name__ == "__main__":
#    main()


###
### Collaboration
###

# ... Write your answer here, as a comment (on lines starting with "#").
