import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.neighbors import NearestNeighbors
from scipy.stats import rankdata

class BMFK:
    def __init__(self, n_neighbors=5, m=2, p=2, q=2):
        self.n_neighbors = n_neighbors
        self.m = m  # fuzzy strength parameter
        self.p = p  # Bonferroni p parameter
        self.q = q  # Bonferroni q parameter

    def fit(self, X, y):
        self.X = X
        self.y = y
        self.classes = np.unique(y)
        self.nn = NearestNeighbors(n_neighbors=self.n_neighbors, metric='minkowski', p=2)
        self.nn.fit(X)

    def bonferroni_mean(self, values):
        n = len(values)
        if n <= 1:
            return np.mean(values)  # Return simple mean if there's only one value
        sum_pq = 0
        for i in range(n):
            for j in range(n):
                if i != j:
                    sum_pq += values[i]**self.p * values[j]**self.q
        return (sum_pq / (n * (n-1)))**(1 / (self.p + self.q))

    def predict(self, X):
        predictions = []
        for x in X:
            distances, indices = self.nn.kneighbors([x])  # Ensure x is in 2D
            neighbors = self.X[indices[0]]
            neighbor_labels = self.y[indices[0]]
            
            # Calculate fuzzy memberships
            memberships = 1 / (distances[0] ** (2 / (self.m - 1)) + 1e-8)
            memberships /= np.sum(memberships)
            
            # Calculate class memberships using Bonferroni mean
            class_memberships = {}
            for c in self.classes:
                class_indices = neighbor_labels == c
                if np.any(class_indices):
                    class_memberships[c] = self.bonferroni_mean(memberships[class_indices])
                else:
                    class_memberships[c] = 0
            
            # Predict the class with highest membership
            predictions.append(max(class_memberships, key=class_memberships.get))
        
        return np.array(predictions)

# Load the dataset
file_path = "PCOS_data_without_infertility.xlsx"
df = pd.read_excel(file_path, sheet_name="Full_new")

# Data Preprocessing
df = df.drop(columns=['Sl. No', 'Patient File No.', 'Unnamed: 44'])
df = df.apply(pd.to_numeric, errors='coerce')
df.fillna(df.median(), inplace=True)

# Encode categorical columns
categorical_columns = ['Blood Group', 'Cycle(R/I)', 'Pregnant(Y/N)', 
                       'Weight gain(Y/N)', 'hair growth(Y/N)', 
                       'Skin darkening (Y/N)', 'Hair loss(Y/N)', 
                       'Pimples(Y/N)', 'Fast food (Y/N)', 
                       'Reg.Exercise(Y/N)']
for col in categorical_columns:
    if col in df.columns:
        df[col] = df[col].astype('category').cat.codes

# Split into features and target
X = df.drop(columns=['PCOS (Y/N)'])
y = df['PCOS (Y/N)'].values  # Convert to numpy array

# Standardize features
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# Selected features from Ensemble Filter + BEEO (RL) + BMFK (Proposed)
selected_features_proposed = ['Follicle No. (L)', 'hair growth(Y/N)', 'Follicle No. (R)', 
                              'Cycle(R/I)', 'Fast food (Y/N)', 'Skin darkening (Y/N)', 
                              'Cycle length(days)', 'FSH/LH']

# Selected features from Ensemble Filter only
ensemble_selected_features = ['Follicle No. (L)', 'hair growth(Y/N)', 'Follicle No. (R)', 
                              'Cycle(R/I)', 'Fast food (Y/N)', 'AMH(ng/mL)', 
                              'Skin darkening (Y/N)', 'Weight gain(Y/N)', 
                              'Pimples(Y/N)', 'Cycle length(days)', 
                              'Hip(inch)', 'Weight (Kg)', 'FSH/LH', 'FSH(mIU/mL)']

# Selected Features without ensemble, only RL
rl_selected_features = [' Age (yrs)', 'Height(Cm) ', 'RR (breaths/min)', 'No. of aborptions', 
                        'Hip(inch)', 'Waist:Hip Ratio', 'AMH(ng/mL)', 'PRG(ng/mL)', 
                        'Fast food (Y/N)', 'Follicle No. (R)', 'Avg. F size (L) (mm)', 
                        'Endometrium (mm)'] 

# Function to evaluate the BMFK classifier
def evaluate_bmfk(X_data, y_data):
    # Split the data into training (90%) and testing (10%) sets
    X_train, X_test, y_train, y_test = train_test_split(X_data, y_data, test_size=0.1, random_state=42)

    # Initialize BMFK classifier
    bmfk_clf = BMFK(n_neighbors=5, m=2, p=2, q=2)
    bmfk_clf.fit(X_train, y_train)

    # Make predictions on the test set
    y_pred = bmfk_clf.predict(X_test)

    # Calculate accuracy
    accuracy = accuracy_score(y_test, y_pred)
    
    return accuracy

# Calculate accuracies for each method using BMFK optimizer
accuracy_proposed = evaluate_bmfk(X[selected_features_proposed], y)  # Proposed method
accuracy_ensemble = evaluate_bmfk(X[ensemble_selected_features], y)  # Ensemble method
accuracy_rl = evaluate_bmfk(X[rl_selected_features], y)  # RL method
accuracy_all = evaluate_bmfk(X, y)  # All features

# Create a dictionary to hold the accuracies
accuracies = {
    "Ensemble Filter + BEEO (RL) + BMFK (Proposed)": accuracy_proposed,
    "With Ensemble Filter & BEO alone (No RL)": accuracy_ensemble,
    "Without Ensemble Filter & with BEO-RL": accuracy_rl,
    "Without Filter & Wrapper (All 44 features to BMFK classifier)": accuracy_all
}

# Convert accuracies into an array for ranking
accuracy_values = np.array(list(accuracies.values()))

# Calculate ranks (lower rank is better, so higher accuracy gets lower rank)
ranks = rankdata(-accuracy_values)  # Negative to rank by highest accuracy

# Create a DataFrame to format the table neatly
df_results = pd.DataFrame({
    "Method": list(accuracies.keys()),
    "Accuracy": accuracy_values,
    "Friedman Mean Rank": ranks,
    "Rank": ranks
})

# Assign final rank based on mean rank
final_ranks = rankdata(ranks)

# Add final rank to the DataFrame
df_results["Final Rank"] = final_ranks

# Display the results
print("\n--- Accuracy, Friedman Mean Ranks, and Final Ranks ---")
print(df_results)

c:\Users\manch\AppData\Local\Programs\Python\Python39\lib\site-packages\sklearn\base.py:493: UserWarning: X does not have valid feature names, but NearestNeighbors was fitted with feature names
  warnings.warn(
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[1], line 122
    119     return accuracy
    121 # Calculate accuracies for each method using BMFK optimizer
--> 122 accuracy_proposed = evaluate_bmfk(X[selected_features_proposed], y)  # Proposed method
    123 accuracy_ensemble = evaluate_bmfk(X[ensemble_selected_features], y)  # Ensemble method
    124 accuracy_rl = evaluate_bmfk(X[rl_selected_features], y)  # RL method

Cell In[1], line 114
    111 bmfk_clf.fit(X_train, y_train)
    113 # Make predictions on the test set
--> 114 y_pred = bmfk_clf.predict(X_test)
    116 # Calculate accuracy
    117 accuracy = accuracy_score(y_test, y_pred)

Cell In[1], line 37
     35 predictions = []
     36 for x in X:
---> 37     distances, indices = self.nn.kneighbors([x])  # Ensure x is in 2D
     38     neighbors = self.X[indices[0]]
     39     neighbor_labels = self.y[indices[0]]

File c:\Users\manch\AppData\Local\Programs\Python\Python39\lib\site-packages\sklearn\neighbors\_base.py:825, in KNeighborsMixin.kneighbors(self, X, n_neighbors, return_distance)
    823         X = _check_precomputed(X)
...
   1056     )

ValueError: Expected 2D array, got 1D array instead:
array=['Follicle No. (L)'].
Reshape your data either using array.reshape(-1, 1) if your data has a single feature or array.reshape(1, -1) if it contains a single sample.
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...