Skip to content

Commit cfc7595

Browse files
committed
creation of backend directory & reorg of repo
1 parent 689a6c1 commit cfc7595

4 files changed

Lines changed: 748 additions & 0 deletions

File tree

4.97 KB
Binary file not shown.

backend/backendInterface.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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")

backend/main.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from fastapi import FastAPI, File, UploadFile, HTTPException
2+
from backendInterface import add_data, find_anomalies, get_output
3+
4+
app = FastAPI()
5+
6+
@app.post("/add_data/")
7+
async def api_add_data(file: UploadFile = File(...)):
8+
# should there be a size limit? idk how much the backend can actually store/process :/
9+
if not file.filename.lower().endswith(".csv"):
10+
raise HTTPException(status_code=400, detail="Only CSV files are allowed")
11+
try:
12+
cleaned_df = add_data(file)
13+
return {
14+
"status": "success",
15+
"rows": cleaned_df.shape[0],
16+
"columns": cleaned_df.shape[1]
17+
}
18+
except Exception as e:
19+
raise HTTPException(status_code=400, detail="Error uploading file. Please try again")
20+
21+
@app.get("/find_anomalies/")
22+
async def api_find_anomalies():
23+
# TODO: update to take in the query results from LLM
24+
try:
25+
anomalies = find_anomalies(query) #uid & num_feat from query
26+
return anomalies.to_dict(orient="records") #anomaly results as JSON placeholder
27+
except Exception as e:
28+
raise HTTPException(status_code=400, detail=str(e))
29+
30+
@app.get("/get_output/")
31+
async def api_get_output():
32+
# TODO: update to take in type of output from LLM results
33+
try:
34+
output = get_output(query)
35+
return output.to_dict(orient="records")
36+
except Exception as e:
37+
raise HTTPException(status_code=400, detail=str(e))

0 commit comments

Comments
 (0)