| import streamlit as st |
| import pandas as pd |
| import numpy as np |
| import time |
| import os |
| import tempfile |
| from ucimlrepo import fetch_ucirepo |
| from sklearn.preprocessing import LabelEncoder, StandardScaler |
| from sklearn.metrics.pairwise import euclidean_distances |
| from sklearn.neighbors import KDTree |
|
|
| |
| DATA_FILE = os.path.join(tempfile.gettempdir(), 'case_base_data.csv') |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
|
|
| |
| |
| |
| |
|
|
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| @st.cache_data |
| def load_data(): |
| dataset = fetch_ucirepo(id=544) |
| X = dataset.data.features |
| y = dataset.data.targets |
| df = pd.concat([X, y], axis=1) |
| try : |
| df.to_csv(DATA_FILE, index=False) |
| print(f"succesfull write data to '{DATA_FILE}'") |
| print(f"File saved at: {os.path.abspath(DATA_FILE)}") |
| except Exception as e: |
| print(f"An error occurred while writing the CSV file: {e}") |
| return df |
|
|
| |
| @st.cache_data |
| def load_existing_data(): |
| if os.path.exists(DATA_FILE): |
| return pd.read_csv(DATA_FILE) |
| else: |
| return pd.DataFrame() |
|
|
| |
| if 'case_base' not in st.session_state: |
| df_original = load_data() |
| st.write("Original DataFrame after loading:", df_original.head()) |
| st.session_state.case_base = load_existing_data() if not df_original.empty else df_original.copy() |
|
|
| |
| df = st.session_state.case_base |
| df_encoded = df.copy() |
| label_encoders = {} |
|
|
| |
| if df_encoded.empty: |
| st.error("The dataset is empty. Please check the data loading process.") |
| else: |
| for col in df_encoded.select_dtypes(include='object').columns: |
| le = LabelEncoder() |
| df_encoded[col] = le.fit_transform(df_encoded[col]) |
| label_encoders[col] = le |
|
|
| |
| if 'NObeyesdad' in df_encoded.columns: |
| features = df_encoded.drop(columns=['NObeyesdad']) |
| st.write("cek isi featur atas:", features.tail()) |
| target = df_encoded['NObeyesdad'] |
| st.write("cek target:", target.tail()) |
| else: |
| st.error("Column 'NObeyesdad' not found in the dataset.") |
| features = df_encoded |
| target = None |
|
|
| |
| if not features.empty: |
| scaler = StandardScaler() |
| features_scaled = scaler.fit_transform(features) |
| st.write("cek isi featur bawh:", features.tail()) |
| else: |
| st.error("Features DataFrame is empty. Cannot proceed with scaling.") |
|
|
| |
|
|
| |
| def heom_distance(x1, x2, numerical_cols, categorical_cols, ranges): |
| dist = 0 |
| for col in numerical_cols: |
| r = ranges[col] |
| if r > 0: |
| d = ((x1[col] - x2[col]) / r) ** 2 |
| dist += d |
| for col in categorical_cols: |
| dist += 0 if x1[col] == x2[col] else 1 |
| return np.sqrt(dist) |
|
|
| |
| def calculate_adaptability_score(new_case_df, case_base_df): |
| info_score = 0 |
| epsilon = 1e-9 |
| for col in new_case_df.columns: |
| freq = case_base_df[col].value_counts(normalize=True) |
| p = freq.get(new_case_df.iloc[0][col], epsilon) |
| info_score += -np.log2(p) |
| return info_score |
|
|
| |
| @st.cache_data |
| def get_adaptive_threshold(case_base_df, percentile=50): |
| all_scores = [ |
| calculate_adaptability_score(pd.DataFrame([row]), case_base_df) |
| for _, row in case_base_df.iterrows() |
| ] |
| return np.percentile(all_scores, percentile) |
| |
| |
| def retain_case(new_case_dict, case_base_df, distance_threshold=0.5, adaptive_threshold=30, force=False): |
| new_case_df = pd.DataFrame([new_case_dict]) |
| for col in new_case_df.select_dtypes(include='object').columns: |
| if col in label_encoders: |
| new_case_df[col] = label_encoders[col].transform(new_case_df[col]) |
| new_case_encoded = new_case_df.copy() |
| new_case_scaled = scaler.transform(new_case_encoded) |
|
|
| numerical_cols = features.select_dtypes(include=np.number).columns.tolist() |
| categorical_cols = [col for col in features.columns if col not in numerical_cols] |
| feature_ranges = {col: df_encoded[col].max() - df_encoded[col].min() for col in numerical_cols} |
|
|
| raw_input = new_case_encoded.iloc[0] |
| heom_distances = [ |
| heom_distance(raw_input, row, numerical_cols, categorical_cols, feature_ranges) |
| for _, row in features.iterrows() |
| ] |
| min_dist = min(heom_distances) |
|
|
| adaptability_score = calculate_adaptability_score(new_case_encoded, df_encoded.drop(columns=['NObeyesdad'])) |
| retain_flag = (min_dist > distance_threshold and adaptability_score < adaptive_threshold) or force |
|
|
| if retain_flag: |
| try: |
| |
| new_case_with_label = new_case_dict.copy() |
| new_case_with_label['NObeyesdad'] = predicted_label |
| new_case_df = pd.DataFrame([new_case_with_label]) |
| print(f"cek new casebase {new_case_df}, ") |
| new_case_df.to_csv(DATA_FILE, mode='a', header=not os.path.exists(DATA_FILE), index=False) |
| st.session_state.case_base = pd.read_csv(DATA_FILE) |
| st.success(f"β
Kasus berhasil disimpan. Jumlah kasus sekarang: {len(st.session_state.case_base)}") |
|
|
| except PermissionError: |
| st.error("Permission denied: Unable to save the case to the CSV file.") |
| except Exception as e: |
| st.error(f"An error occurred while saving the case: {e}") |
|
|
| return retain_flag, min_dist, adaptability_score, adaptive_threshold |
|
|
| |
|
|
| |
| def case_based_reasoning(new_input_dict): |
| new_input_df = pd.DataFrame([new_input_dict]) |
| for col in new_input_df.select_dtypes(include='object').columns: |
| if col in label_encoders: |
| new_input_df[col] = label_encoders[col].transform(new_input_df[col]) |
| new_input_scaled = scaler.transform(new_input_df) |
|
|
| |
| eucl_distances = euclidean_distances(new_input_scaled, features_scaled) |
| eucl_closest_index = eucl_distances.argmin() |
|
|
| |
| tree = KDTree(features_scaled) |
| kd_dist, kd_idx = tree.query(new_input_scaled, k=1) |
| kd_index = kd_idx[0][0] |
|
|
| |
| raw_input = new_input_df.iloc[0] |
| numerical_cols = features.select_dtypes(include=np.number).columns.tolist() |
| categorical_cols = [col for col in features.columns if col not in numerical_cols] |
| feature_ranges = {col: df_encoded[col].max() - df_encoded[col].min() for col in numerical_cols} |
| heom_distances = [ |
| heom_distance(raw_input, row, numerical_cols, categorical_cols, feature_ranges) |
| for _, row in features.iterrows() |
| ] |
| heom_closest_index = int(np.argmin(heom_distances)) |
|
|
| |
| k_candidates = 50 |
| kd_tree_indices = tree.query(new_input_scaled, k=k_candidates)[1][0] |
| heom_candidate_dists = [] |
| for idx in kd_tree_indices: |
| row = features.iloc[idx] |
| dist = heom_distance(raw_input, row, numerical_cols, categorical_cols, feature_ranges) |
| heom_candidate_dists.append(dist) |
| heom_kdtree_index = int(kd_tree_indices[int(np.argmin(heom_candidate_dists))]) |
|
|
| return { |
| "euclidean": { |
| "index": int(eucl_closest_index), |
| "distance": float(eucl_distances[0][eucl_closest_index]), |
| }, |
| "kdtree": { |
| "index": int(kd_index), |
| "distance": float(kd_dist[0][0]), |
| }, |
| "heom": { |
| "index": heom_closest_index, |
| "case": df.iloc[heom_closest_index].to_dict() |
| }, |
| "heom_kdtree": { |
| "index": heom_kdtree_index, |
| "case": df.iloc[heom_kdtree_index].to_dict() |
| } |
| } |
|
|
|
|
| |
| st.title("π§ CBR Obesitas + Retain Adaptif (Live Session)") |
| st.markdown(f"Jumlah kasus dalam database saat ini: **{len(st.session_state.case_base)} kasus**") |
|
|
| user_input = { |
| 'Gender': st.selectbox("Gender", ['Male', 'Female']), |
| 'Age': st.number_input("Age", 10, 100, 25), |
| 'Height': st.number_input("Height (in meters)", 1.0, 2.5, 1.70), |
| 'Weight': st.number_input("Weight (in kg)", 30.0, 200.0, 70.0), |
| 'family_history_with_overweight': st.selectbox("Family history with overweight", ['yes', 'no']), |
| 'FAVC': st.selectbox("Frequent consumption of high caloric food", ['yes', 'no']), |
| 'FCVC': st.slider("Vegetable consumption (0β3)", 0.0, 3.0, 2.0), |
| 'NCP': st.slider("Number of main meals", 1.0, 5.0, 3.0), |
| 'CAEC': st.selectbox("Food between meals", ['no', 'Sometimes', 'Frequently', 'Always']), |
| 'SMOKE': st.selectbox("Do you smoke?", ['yes', 'no']), |
| 'CH2O': st.slider("Daily water intake", 0.0, 3.0, 2.0), |
| 'SCC': st.selectbox("Calories monitoring", ['yes', 'no']), |
| 'FAF': st.slider("Physical activity (hrs/week)", 0.0, 5.0, 1.0), |
| 'TUE': st.slider("Tech usage (hrs/day)", 0.0, 5.0, 1.0), |
| 'CALC': st.selectbox("Alcohol consumption", ['no', 'Sometimes', 'Frequently', 'Always']), |
| 'MTRANS': st.selectbox("Transport", ['Automobile', 'Motorbike', 'Bike', 'Public_Transportation', 'Walking']) |
| } |
| force_save = st.checkbox("πΎ Simpan paksa jika ditolak?", value=False) |
| |
|
|
| if st.button("π Prediksi dan Evaluasi Retain"): |
| result = case_based_reasoning(user_input) |
|
|
| st.markdown("### π’ Kasus Paling Mirip") |
| st.json(result['heom_kdtree']['case']) |
|
|
| |
| predicted_label = result['heom_kdtree']['case']['NObeyesdad'] |
| user_input_with_label = user_input.copy() |
| user_input_with_label['NObeyesdad'] = predicted_label |
|
|
| |
| st.markdown("### π§Ύ Prediksi Kategori Obesitas") |
| st.write(f"π― **{predicted_label}** (berdasarkan kasus paling mirip)") |
|
|
| |
| adaptive_threshold = get_adaptive_threshold(df_encoded.drop(columns=['NObeyesdad']), percentile=50) |
| retain_flag, min_dist, adaptability_score, threshold_used = retain_case( |
| user_input, df_encoded, distance_threshold=0.5, |
| adaptive_threshold=adaptive_threshold, force=force_save |
| ) |
|
|
| st.markdown("### π₯ Evaluasi Retain") |
| st.write(f"HEOM Distance Terdekat: `{min_dist:.3f}`") |
| st.write(f"Skor Adaptabilitas: `{adaptability_score:.2f}` (Threshold adaptif: `{threshold_used:.2f}`)") |
| |
|
|
| if retain_flag: |
| latest_data = pd.read_csv(DATA_FILE) |
| st.write("cek data baru:", latest_data.tail()) |
| st.success(f"β
Kasus berhasil disimpan. Jumlah kasus sekarang: {len(latest_data)}") |
| else: |
| st.info("β Kasus tidak disimpan otomatis (tidak adaptif & terlalu mirip).") |
| if force_save: |
| st.session_state.case_base = pd.concat([st.session_state.case_base, pd.DataFrame([user_input_with_label])], ignore_index=True) |
| st.warning(f"β οΈ Simpan paksa dilakukan. Jumlah kasus sekarang: {len(st.session_state.case_base)}") |
|
|