1+ # This contains the functions that the front end interface will have access to.
2+ # Only this module will be imported to main for API calls.
3+
4+ # Imports
5+ import pandas as pd
6+ import numpy as np
7+ from sklearn .decomposition import PCA
8+ from sklearn .preprocessing import StandardScaler , LabelEncoder
9+ from reinforcementLearning import run_rl
10+
11+
12+ backend_data = {"df" : None ,
13+ "uid" : None ,
14+ "features" : None ,
15+ "anomalies" : None
16+ } #the backend "memory"
17+
18+ ##### DATA PREPARATION #####
19+
20+ '''
21+ Summary: Takes in user-uploaded data, cleans it, and sets the global dataframe.
22+ Input:
23+ Output:
24+ '''
25+ def add_data (file ):
26+ if not hasattr (file , "filename" ) or not file .filename .lower ().endswith (".csv" ):
27+ raise ValueError ("Only CSV files are allowed" )
28+
29+ raw_file = pd .read_csv (file .file if hasattr (file , "file" ) else file )
30+ cleaned_df = clean_data (raw_file )
31+ backend_data ["df" ] = cleaned_df
32+ return cleaned_df
33+
34+ def clean_data (df ):
35+ # print("# Starting Columns:", str(df.shape[1]))
36+ original_columns = df .columns .tolist ()
37+ original_shape = (df .shape [0 ], df .shape [1 ])
38+
39+ # replace blanks with NaN
40+ df .replace ("" , np .nan , inplace = True )
41+
42+ # Drop columns where ALL values are NaN
43+ df .dropna (axis = 1 , how = 'all' , inplace = True )
44+
45+ # Drop rows where ANY value is NaN
46+ df .dropna (axis = 0 , how = 'any' , inplace = True )
47+
48+ # Identify which rows/columns were dropped
49+ dropped_columns = list (set (original_columns ) - set (df .columns ))
50+ # print("Dropped columns:", dropped_columns)
51+ # print(f"Original shape: {original_shape}, New shape: {df.shape}")
52+
53+ return df
54+
55+ ###### FEATURE SELECTION & FIND ANOMALIES #####
56+
57+ '''
58+ Summary: Takes the user query, performs feature selection and RL. Updates all global data.
59+ Input:
60+ Output:
61+ '''
62+ def find_anomalies (query , uid , num_feat ):
63+
64+ ## FEATURE SELECTION
65+ main_identifiers = [uid ] # TODO: could later add option for additional headings to ignore, otherwise switch to just UID
66+ backend_data ["uid" ] = uid
67+ df = backend_data ["df" ]
68+ num_entries = df .shape [0 ]
69+ drop = []
70+ for i in df :
71+ if df [i ].dtype == 'O' and i not in main_identifiers : # qualitative
72+ qual_to_quant (df , i )
73+ if df [i ].nunique () > (num_entries / 2 ) or df [i ].nunique () == 1 : # if more than 1/2 of data points have a unique label OR all have same label, drop the column. can change this!
74+ drop .append (i )
75+ # print(drop)
76+ cleaned_df = df .drop (columns = drop )
77+ qual_to_quant (cleaned_df , main_identifiers [0 ])
78+ backend_data ["df" ] = cleaned_df
79+ # print("Final Columns:", str(cleaned_df.shape[1]))
80+ # print(cleaned_df.head())
81+
82+ pca , features = get_features (cleaned_df , num_feat , main_identifiers )
83+ backend_data ["features" ] = features
84+
85+ ## REINFORCEMENT LEARNING
86+ anomalies , cluster_sizes , final_features = run_rl (backend_data ) #TODO: others for output data
87+ backend_data ["anomalies" ] = anomalies
88+ return anomalies
89+
90+ '''Converts the qualitative column col in df to quantitative values'''
91+ def qual_to_quant (df , col ):
92+ le = LabelEncoder ()
93+ df [col ] = le .fit_transform (df [col ])
94+
95+ "Returns an array with the indexes of the top n values in arr"
96+ def get_top_n_idx (n , arr ):
97+ arr = np .abs (arr )
98+ top = np .argpartition (arr , - n )[- n :]
99+ return top
100+
101+ '''
102+ Returns a numpy array of the selected features from data using PCA.
103+
104+ PCA: develops unspecified number of components to represent data
105+ Feature Selection: getting the top_n features that have the highest weighted
106+ importance across all components
107+ '''
108+ def get_features (data , top_n , main_identifiers ):
109+ copy = data .copy (deep = True ) # Make a copy of the original data to avoid modifying it
110+ feat_options = copy .drop (columns = main_identifiers ).copy (deep = True ) # Drop columns like unique IDs that shouldn't be scaled
111+
112+ # Standardize the data
113+ scaler = StandardScaler ()
114+ scaler .fit (feat_options )
115+ scaled_data = scaler .transform (feat_options )
116+
117+ pca = PCA (n_components = 0.95 ) # should represent at least 95% of overall trends in data
118+ pca .fit (scaled_data )
119+
120+ ### USE ABSOLUTE VALUE OF LOADINGS ONLY FOR FEATURE IMPORTANCE
121+ loadings = np .abs (pca .components_ ) # Get importance for each feature in each component
122+ # feature_importance = np.sum(loadings, axis=0) # Sum the absolute loadings for each feature across all components
123+
124+ ### USE WEIGHTED LOADINGS BY EXPLAINED VARIANCE FOR FEATURE IMPORTANCE
125+ weighted_loadings = np .abs (pca .components_ ) * pca .explained_variance_ratio_ .reshape (- 1 , 1 )
126+ feature_importance = np .sum (weighted_loadings , axis = 0 )
127+
128+
129+ # Get the indexes of the top n most important features based on summed importance
130+ top = get_top_n_idx (top_n , feature_importance )
131+
132+ # Get the feature names for the most important features
133+ most_important_names = feat_options .columns [top ]
134+
135+ # Print the selected important features
136+ print ("Most Important Features:" , most_important_names .tolist ())
137+
138+ # Return the unique top features
139+ unique_feats = np .unique (most_important_names )
140+ print ("Unique Features:" , unique_feats .tolist ())
141+
142+ # Return the selected unique features
143+ return pca , unique_feats
144+
145+
146+ ##### GET OUTPUT #####
147+
148+ '''
149+ Summary
150+ Input:
151+ Output:
152+ '''
153+ def get_output ():
154+ return ("Returning results" )
0 commit comments