# -*- coding: utf-8 -*-
"""P6-1-ID3 Algorithm .ipynb

Automatically generated by Colab.

Original file is located at
    https://colab.research.google.com/drive/145Q19eQyjP4BwvvSJCQ3_q12QGmQJ-il
"""

import warnings
warnings.filterwarnings('ignore')

import pandas as pd
from pandas import DataFrame

df_tennis = pd.DataFrame(data=pd.read_csv('/content/PlayTennis.csv'))
df_tennis.head()

#Entropy formula procedure
def entropy(probs):
    import math
    return sum([-prob*math.log(prob,2)for prob in probs])

#Entropy calculation for the attributes
def entropy_of_list(a_list): #Input list of class labels
    from collections import Counter
    cnt=Counter(x for x in a_list) #Counter calculates the proportion of class
    num_instances=len(a_list)*1.0  #=14
    probs=[x/num_instances for x in cnt.values()] #x means number of YES/NO 9/14 and 5/14
    return entropy(probs) #Call Entropy

#Calculating the Entropy of the target variable
total_entropy=entropy_of_list(df_tennis['PlayTennis'])
print('\n The total Entropy is:\n',total_entropy)

#Calculating Information Gain
def information_gain(df,split_attribute_name,target_attribute_name,trace=0):
    df_split=df.groupby(split_attribute_name)#Split datasets into groups or subsets by literals
    nobs=len(df.index)*1.0 #count total no of observations
    #Compute Entropy and Proportion for each group
    df_agg_ent=df_split.agg({target_attribute_name:[entropy_of_list,lambda x:len(x)/nobs]})[target_attribute_name]
    df_agg_ent.columns=['Entropy','PropObservations'] #Renaming columns
    new_entropy=sum(df_agg_ent['Entropy']*df_agg_ent['PropObservations'])
    old_entropy=entropy_of_list(df[target_attribute_name])
    return old_entropy-new_entropy

"""Explanation:df_split.agg()
For each group, aggregate (agg) the target_attribute_name (PlayTennis) using two functions:

entropy_of_list → entropy of that group’s labels.

lambda x: len(x)/nobs → proportion of total rows that belong to this group.

Example for "Outlook":

Group "Sunny" (5 rows): entropy ≈ 0.971, proportion = 5/14.

Group "Overcast" (4 rows): entropy = 0.0, proportion = 4/14.

Group "Rain" (5 rows): entropy ≈ 0.971, proportion = 5/14.

The [target_attribute_name] selects only the part of the DataFrame we care about.
"""

def id3(df,target_attribute_name,attribute_names,default_class=None):
    from collections import Counter
    cnt=Counter(x for x in df[target_attribute_name])# Count class labels YES/NO
    if len(cnt)==1:#Pure subset indicating only one class left
	    return next(iter(cnt))#next input data set, or raises StopIteration when EOF is hit
    elif df.empty or (not attribute_names):
	    return default_class #Return None for empty data set or no attributes left
    else:
	    default_class=max(cnt.keys())#Assign the default class as the max between Yes/No
        #Compute Information Gain for each attribute
	    gainz=[information_gain(df,attr,target_attribute_name)for attr in attribute_names]
        #Select best attribute with maximum information gain
	    index_of_max=gainz.index(max(gainz))#Index of Best_Attribute
	    best_attr=attribute_names[index_of_max]
	    tree={best_attr:{}}#Initiate the tree with the best attribute as a node
        #After choosing the best attribute, it’s removed from the list
	    remaining_attribute_names=[i for i in attribute_names if i!=best_attr]
        #Recursive splitting and building subtrees
	    for attr_val,data_subset in df.groupby(best_attr):
		    subtree=id3(data_subset,target_attribute_name,remaining_attribute_names,default_class)
		    tree[best_attr][attr_val]=subtree # Attach the resulting subtree to the main tree
	    return tree

attribute_names=list(df_tennis.columns)
attribute_names.remove('PlayTennis')#Remove the class attirbute

#Run Algorithm:
from pprint import pprint
tree=id3(df_tennis,'PlayTennis',attribute_names)
print("\n The resulatnt DECISION TREE is:\n")
pprint(tree)

#Result for new instance
def classify(instance,tree,default=None):
	attribute=next(iter(tree)) #Get first attribute from the tree
	if instance[attribute] in tree[attribute].keys(): #Check if instance value exists in the tree
		result=tree[attribute][instance[attribute]] #Get the subtree
		if isinstance(result,dict):#Recursive case: if result is still a tree
			return classify(instance,result)
		else:
			return result

"""Get subtree:
If instance["Outlook"] = "Sunny", then
result = {'Humidity': {'High': 'No', 'Normal': 'Yes'}}
"""

#Creation of Training set
training_data=df_tennis.iloc[0:-4]
print(training_data)

#Test set
test_data=df_tennis.iloc[-4:]
print(test_data)

train_tree=id3(training_data,'PlayTennis',attribute_names)

#Prediction on new Data
test_data['predicted']=test_data.apply(classify,axis=1,args=(train_tree,'yes'))
print(test_data['predicted'])

import pandas as pd
import numpy as np
import math
from collections import Counter
from pprint import pprint
import warnings
warnings.filterwarnings("ignore")

# Load dataset
df = pd.read_csv(r"/content/PlayTennis.csv")
print("Dataset:\n", df.head())

# ---------- Step 1: Entropy Calculation ----------
def entropy(values):
    probs = [v / len(values) for v in Counter(values).values()]
    return -sum(p * math.log2(p) for p in probs)

# ---------- Step 2: Information Gain ----------
def information_gain(df, attr, target):
    total_entropy = entropy(df[target])
    vals, counts = np.unique(df[attr], return_counts=True)
    weighted_entropy = sum(
        (counts[i] / len(df)) * entropy(df[df[attr] == vals[i]][target])
        for i in range(len(vals))
    )
    return total_entropy - weighted_entropy

# ---------- Step 3: ID3 Algorithm ----------
def id3(df, target, attributes):
    # Base cases
    if len(df[target].unique()) == 1:
        return df[target].iloc[0]
    if not attributes:
        return df[target].mode()[0]

    # Choose best attribute
    gains = [information_gain(df, attr, target) for attr in attributes]
    best_attr = attributes[np.argmax(gains)]

    # Build tree recursively
    tree = {best_attr: {}}
    for val in df[best_attr].unique():
        subset = df[df[best_attr] == val]
        subtree = id3(subset, target, [a for a in attributes if a != best_attr])
        tree[best_attr][val] = subtree
    return tree

# ---------- Step 4: Train the Model ----------
attributes = list(df.columns)
attributes.remove('PlayTennis')
tree = id3(df, 'PlayTennis', attributes)

print("\nDecision Tree:")
pprint(tree)

# ---------- Step 5: Classification ----------
def classify(instance, tree):
    attr = next(iter(tree))
    val = instance[attr]
    subtree = tree[attr].get(val, None)
    if isinstance(subtree, dict):
        return classify(instance, subtree)
    return subtree

# ---------- Step 6: Train-Test Split ----------
train = df.iloc[:-4]
test = df.iloc[-4:]

tree_train = id3(train, 'PlayTennis', attributes)

# Predict for test set
test['Predicted'] = test.apply(classify, axis=1, args=(tree_train,))
print("\nPredictions:\n", test[['PlayTennis', 'Predicted']])

