# Notebook Title: AA Prepflow - Outlier Lab UI
# Author: Advanced Analytics Team
# Date: 2025-11-13
# Purpose: UI Gradio untuk eksperimen dan evaluasi penanganan outlier

from __future__ import annotations

# import sys
from typing import List, Optional

# sys.path.append('../aa-prepflow/')  # Add the src directory to sys.path

import gradio as gr
import pandas as pd
import numpy as np

from ..ui.data_evaluation import call_data_evaluation
from ..services.visualization import AA_Rapid_Eval
from ..services.evaluation_function import AALibrary


class OutlierLab:
    """Kelas UI untuk lab eksperimen outlier di Gradio."""
    
    def __init__(self) -> None:
        """Inisialisasi OutlierLab tanpa state tambahan."""
        pass

    def round_float_columns(self, df: pd.DataFrame, decimals: int = 2) -> pd.DataFrame:
        """Membulatkan kolom numerik (atau numeric-string) ke sejumlah desimal."""
        df_rounded = df.copy()
        for col in df_rounded.columns:
            # Check if the column is already numeric (float, int)
            if pd.api.types.is_numeric_dtype(df_rounded[col]):
                df_rounded[col] = df_rounded[col].round(decimals)
            # Check if the column contains objects (strings)
            elif pd.api.types.is_object_dtype(df_rounded[col]):
                try:
                    # Attempt to convert the series to a numeric type
                    numeric_series = pd.to_numeric(df_rounded[col], errors='raise')
                    # If successful, round the numeric series and convert it back to a string
                    df_rounded[col] = numeric_series.round(decimals).astype(str)
                except (ValueError, TypeError):
                    # If conversion fails, the column likely contains mixed
                    # or non-numeric strings, so we leave it as is.
                    pass
        return df_rounded

    # --- Fungsi Interaksi Gradio ---
    def load_and_update_options(self, data: dict, file_input, upload: bool):
        """Memuat data source dan meng-update pilihan dropdown/komponen UI."""
        if not data['data'] and file_input is None:
            # Jika tidak ada file, kosongkan semuanya
            empty_df = pd.DataFrame()
            return (
                empty_df,
                gr.update(choices=[], value=None), # Kosongkan eval_col_dd
                gr.update(choices=[], value=[]),   # Kosongkan col_filter
                gr.update(choices=[], value=None), # Kosongkan col_time
                gr.update(choices=[], value=None)  # Kosongkan category_filter
            )
     
        try:
            if upload:
                df = pd.read_csv(file_input)
            else:
                df = pd.DataFrame(data['data'], columns=data['headers'])
            df = self.round_float_columns(df)
            column_names = df.columns.tolist()
            num_cols = df.select_dtypes(include=np.number).columns.tolist()
     
            # Nilai default yang aman (None jika daftar kosong)
            default_num_val = num_cols[0] if num_cols else None
            default_col_val = column_names[0] if column_names else None
     
            return (
                df,
                # Reset eval_col_dd dengan choices dan value yang baru & valid
                gr.update(choices=num_cols, value=None),
                # Reset col_filter (multiselect, value=[] sudah benar untuk mengosongkan)
                gr.update(choices=column_names, value=[]),
                # Reset col_time
                gr.update(choices=column_names, value=default_col_val),
                # Reset category_filter
                gr.update(choices=column_names, value=default_col_val)
            )
        except Exception as e:
            # Penanganan error jika file CSV tidak valid
            # print(f"Error reading CSV file: {e}")
            empty_df = pd.DataFrame() # Menggunakan pd.DataFrame kosong
            gr.Warning("Gagal membaca file CSV. Pastikan formatnya benar.")
            return (
                empty_df,
                gr.update(choices=[], value=None),
                gr.update(choices=[], value=[]),
                gr.update(choices=[], value=None),
                gr.update(choices=[], value=None)
            )

    def part_outlier(
        self,
        visibility: bool = True,
        category_columns: Optional[List[str]] = None,
    ):
        """
        Bagian UI Gradio untuk konfigurasi deteksi & penanganan outlier.
        (LOGIKA ASLI DIPERTAHANKAN)
        """

        # === Dropdown metode deteksi ===
        detection_filter = gr.Dropdown(
            choices=['Interquartile Range', 'Z Score'], # LOGIKA ASLI
            label="Pilih Outlier Detection",
            info="Detection Method",
            visible=visibility
        )

        # === Default parameter deteksi ===
        data_detection_hyperparameter = [
            ["Factor", 1.5, "Pelebaran Ambang Batas. Semakin besar nilainya, maka semakin besar jarak antara ambang batas bawah dan atas."]
        ]
        df_detection_hyperparameter = gr.DataFrame(
            headers=["Detection Hyperparameter", "Value", "Description"],
            value=data_detection_hyperparameter,
            label="Custom Hyperparameter",
            interactive=True,
            visible=visibility
        )

        # === Dropdown metode handling (Capping / Imputation / LOCF) ===
        handle_filter = gr.Dropdown(
            choices=['Capping Strategy', 'Imputation Strategy'], # LOGIKA ASLI
            label="Pilih Outlier Handling",
            info="Treatment Method",
            visible=visibility
        )

        # === Default parameter handling ===
        data_handle_hyperparameter = [
            ["Strategi", "mean", "Nilai hanya antara 'mean' atau 'median'. Mengisi nilai yang di luar ambang batas dengan strategi yang dipilih."]
        ]
        df_handle_hyperparameter = gr.DataFrame(
            headers=["Handling Hyperparameter", "Value", "Description"],
            value=data_handle_hyperparameter,
            label="Custom Hyperparameter",
            interactive=True,
            visible=False
        )

        # === Event binding ===
        # (Event binding dipindahkan ke call_data_outlier agar konsisten)
        
        return handle_filter, detection_filter, df_detection_hyperparameter, df_handle_hyperparameter
    
    
    def change_in_data_type(self, input_type_data: str):
        """
        Mengatur visibilitas dan pilihan dropdown berdasarkan tipe data.
        (LOGIKA ASLI 5-OUTPUT DIPERTAHANKAN)
        """
        if input_type_data == "Cross Sectional Data":
            return (
                gr.update(visible=False), # col_time
                gr.update(choices=['Capping Strategy', 'Imputation Strategy'], value='Capping Strategy'), # handle_filter
                gr.update(visible=False), # category_filter
                gr.update(choices=['Interquartile Range', 'Z Score'], value="Interquartile Range"), # detection_filter
                gr.update(visible=False) # df_handle_hyperparameter
            )
        elif input_type_data == "Timeseries":
            return (
                gr.update(visible=True), # col_time
                gr.update(choices=['Capping Strategy', 'Imputation Strategy', 'LOCF Strategy'], value='Capping Strategy'), # handle_filter
                gr.update(visible=False), # category_filter
                gr.update(choices=['Rolling IQR'], value="Rolling IQR"), # detection_filter
                gr.update(visible=False) # df_handle_hyperparameter
            )
        elif input_type_data == "Panel":
            return (
                gr.update(visible=True), # col_time
                gr.update(choices=['Capping Strategy', 'Imputation Strategy', 'LOCF Strategy'], value='Capping Strategy'), # handle_filter
                gr.update(visible=True), # category_filter
                gr.update(choices=['Rolling IQR'], value="Rolling IQR"), # detection_filter
                gr.update(visible=False) # df_handle_hyperparameter
            )
        
        # Fallback
        return (gr.update(), gr.update(), gr.update(), gr.update(), gr.update(visible=False))

    
    def change_in_detection_type(self, detection_filter: str):
        """Mengubah tabel hyperparameter deteksi berdasarkan tipe deteksi."""
        if detection_filter == "Rolling IQR":
            return (gr.update(value=[["Factor", 1.5, "Pelebaran Ambang Batas. Semakin besar nilainya, maka semakin besar jarak antara ambang batas bawah dan atas."],
                                     ["Window", 5, "Window Description"]]))
        else:
            return (gr.update(value=[["Factor", 1.5, "Pelebaran Ambang Batas. Semakin besar nilainya, maka semakin besar jarak antara ambang batas bawah dan atas."]], visible=True))
    
    def change_in_handle_filter(
        self,
        handle_filter: str,
        category_columns: Optional[List[str]] = None,
    ):
        """Update isi tabel hyperparameter handling berdasar metode yang dipilih."""

        # === Capping Strategy ===
        if handle_filter == "Capping Strategy":
            return gr.update(
                value=[],
                visible=False
            )

        # === Imputation Strategy ===
        elif handle_filter == "Imputation Strategy":
            return gr.update(
                value=[
                    ["Strategi", "mean", "Nilai hanya antara 'mean' atau 'median'. Mengisi nilai yang di luar ambang batas dengan strategi yang dipilih."]
                ],
                visible=True
            )

        # === LOCF Strategy ===
        elif handle_filter == "LOCF Strategy":
            return gr.update(
                value=[],
                visible=False
            )

        # === Default / fallback ===
        else:
            return gr.update(
                value=[],
                visible=False
            )

    def change_in_treatement_amount(
        self,
        treatement_amount: str,
        df_handle_hyperparameter2,
        df_handle_hyperparameter3,
    ):
        """Mengatur visibilitas blok Treatment 2 & 3 berdasarkan jumlah treatment."""
        if treatement_amount == "1":
            return gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
        elif treatement_amount == "2":
            return gr.update(visible=True), gr.update(visible=True), gr.update(visible=True), gr.update(visible=True), df_handle_hyperparameter2, gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
        elif treatement_amount == "3":
            return gr.update(visible=True), gr.update(visible=True), gr.update(visible=True), gr.update(visible=True), df_handle_hyperparameter2, gr.update(visible=True), gr.update(visible=True), gr.update(visible=True), gr.update(visible=True), df_handle_hyperparameter3
        
        # Fallback
        return (
            gr.update(), gr.update(), gr.update(), gr.update(), gr.update(),
            gr.update(), gr.update(), gr.update(), gr.update(), gr.update(),
        )

    def call_data_outlier(
        self,
        data,
        file_input,
        col_time,
        category_filter,
        col_filter,
        input_type_data,
        upload,
        demo,
    ):
        """Membangun seluruh layout Outlier Lab dan wiring callback Gradio."""
        data_state_component = data
        jenis_check = gr.Textbox(value="outlier", visible=False)
        with gr.Row():
            with gr.Tabs(elem_classes="level-2-tabs"):
                # ====================== TAB: TREATMENT METHOD ======================
                with gr.TabItem("Treatment Method"):
                    iqr_summary = """
                    **IQRStrategy** adalah metode deteksi *outlier* **non-parametrik** yang **robust**.
                    * **Cara Kerja:** Mengidentifikasi data di luar rentang [Q1 - 1.5 x IQR , Q3 + 1.5 x IQR].
                    * **Kelebihan:** Robust terhadap nilai ekstrem.
                    * **Kekurangan:** Tidak cocok untuk data *time-series* (tidak mempertimbangkan urutan data).
                    * **Kapan Digunakan:** Ketika data **cross-sectional** yang **tidak terdistribusi normal** (non-parametrik).
                    """

                    zscore_summary = """
                    **ZScoreStrategy** adalah metode sederhana yang mengukur jarak data dari mean dalam satuan standar deviasi.
                    * **Cara Kerja:** Menghitung Z-score (Z = (x - μ) / σ) untuk mengukur jarak data dari rata-rata dalam satuan standar deviasi.
                    * **Kelebihan:** Mudah dihitung dan diinterpretasi.
                    * **Kekurangan:** **Tidak robust**; sangat sensitif terhadap *outlier* yang bisa menyebabkan *masking* *outlier* lainnya.
                    * **Kapan Digunakan:** Ketika data **cross-sectional** yang diasumsikan **terdistribusi normal (Gaussian)**.
                    """

                    rolling_iqr_summary = """
                    **RollingIQRStrategy** adalah versi **adaptif** dari IQR untuk data berurutan.
                    * **Cara Kerja:** Menerapkan perhitungan batas IQR pada **moving window** (jendela geser) dari data.
                    * **Kelebihan:** **Adaptif** terhadap *drift* atau perubahan musiman, dan **robust** (mewarisi sifat IQR).
                    * **Kekurangan:** Memerlukan penentuan ukuran jendela yang **optimal**; kurang sensitif terhadap perubahan mendadak jika jendela terlalu besar.
                    * **Kapan Digunakan:** Ketika data **Time-Series atau Panel** berubah seiring waktu (**non-stasioner**).
                    """
                    gr.Markdown("## 📋 Informasi Metode Deteksi Outlier")
    
                    # Accordion untuk IQRStrategy
                    with gr.Accordion("1. IQRStrategy (Interquartile Range)", open=True):
                        gr.Markdown(iqr_summary)
                        
                    # Accordion untuk ZScoreStrategy
                    with gr.Accordion("2. ZScoreStrategy (Standard Score)", open=False):
                        gr.Markdown(zscore_summary)
                        
                    # Accordion untuk RollingIQRStrategy
                    with gr.Accordion("3. RollingIQRStrategy (Time-Series Adaptive)", open=False):
                        gr.Markdown(rolling_iqr_summary)

                    capping_summary = """
                    **CappingStrategy** (dikenal juga sebagai Winsorization) adalah metode untuk **mengurangi pengaruh nilai ekstrem tanpa menghapusnya**.
                    * **Cara Kerja:** Menarik nilai ekstrem ke batas tertentu (misal, persentil ke-95 atau ke-5) dan menggantinya dengan nilai batas tersebut.
                    * **Kelebihan:** Mempertahankan ukuran data (tidak ada penghapusan); Mengurangi varians dan kemiringan (skewness) data.
                    * **Kekurangan:** Memperkenalkan **bias** karena data di bagian ujung (tails) menjadi terkompresi secara artifisial; Penentuan batas (*cap*) sering kali **subjektif**.
                    * **Kapan Digunakan:** Data Cross-Sectional, ketika ingin mengurangi pengaruh nilai ekstrem tetapi masih berasumsi nilai tersebut mungkin valid.
                    """

                    imputation_summary = """
                    **ImputationStrategy** adalah metode umum untuk **mengganti nilai *outlier*** dengan nilai statistik yang dihitung dari sisa data.
                    * **Cara Kerja:** Mengganti *outlier* (yang dianggap sebagai kesalahan input) dengan nilai **tipikal** seperti Mean, Median, atau Modus dari fitur tersebut.
                    * **Kelebihan:** Menjaga ukuran data; Metode dengan menggunakan **median lebih robust** karena tidak terpengaruh oleh *outlier* lain.
                    * **Kekurangan:** Mengurangi varians (jika *outlier* tersebar); Dapat menyebabkan **bias** jika menggunakan *mean*, karena *mean* itu sendiri sensitif terhadap *outlier*.
                    * **Kapan Digunakan:** Data Cross-Sectional, ketika *outlier* dianggap sebagai kesalahan input data.
                    """

                    locf_summary = """
                    **LOCFStrategy** (Singkatan dari **Last Observation Carried Forward**) adalah metode yang hanya digunakan untuk data **Time-Series atau Panel** yang memiliki urutan (temporal).
                    * **Cara Kerja:** Mengganti nilai *outlier* dengan **nilai valid terakhir** yang diamati sebelum *outlier* tersebut dalam urutan data.
                    * **Kelebihan:** Sederhana, mudah dipahami, dan **menjaga urutan temporal** (tidak menggunakan informasi "dari masa depan").
                    * **Kekurangan:** Menciptakan **artefak** atau "garis datar" pada data (step function), terutama jika digunakan untuk mengisi banyak *outlier* secara berurutan.
                    * **Kapan Digunakan:** Data Time-Series atau Panel, dengan asumsi nilai data cenderung tetap sama dari satu waktu ke waktu berikutnya.
                    """
                    gr.Markdown("## Informasi Metode Penanganan Outlier")
                    # Accordion untuk CappingStrategy
                    with gr.Accordion("1. CappingStrategy (Winsorization)", open=True):
                        gr.Markdown(capping_summary)
                        
                    # Accordion untuk ImputationStrategy
                    with gr.Accordion("2. ImputationStrategy (Penggantian Nilai)", open=False):
                        gr.Markdown(imputation_summary)
                        
                    # Accordion untuk LOCFStrategy
                    with gr.Accordion("3. LOCFStrategy (Last Observation Carried Forward)", open=False):
                        gr.Markdown(locf_summary)


                    gr.Markdown("## Outlier Lab")
                    treatement_amount = gr.Dropdown(
                        choices=['1','2','3'], 
                        label="Amount of Treatment",
                        info="Method comparison across [X] treatments."
                    )
                    treatement_markdown1 = gr.Markdown("### Treatment 1")
                    handle_filter, detection_filter, df_detection_hyperparameter, df_handle_hyperparameter = self.part_outlier() 
                    treatement_markdown2 = gr.Markdown("### Treatment 2", visible=False)
                    handle_filter2, detection_filter2, df_detection_hyperparameter2, df_handle_hyperparameter2 = self.part_outlier(visibility=False)
                    treatement_markdown3 = gr.Markdown("### Treatment 3", visible=False)
                    handle_filter3, detection_filter3, df_detection_hyperparameter3, df_handle_hyperparameter3 = self.part_outlier(visibility=False)
                    apply_btn = gr.Button("✨ Terapkan & Evaluasi", variant="primary", elem_classes="apply-button")
                
                # ====================== TAB: RESULT EVALUATION ======================
                with gr.TabItem("Result Evaluation"):
                    with gr.Row():
                        with gr.Column():
                            gr.Markdown("### Hasil & Evaluasi")
                            with gr.Tabs(elem_classes="level-3-tabs"):
                                with gr.TabItem("Pratinjau Data"):
                                    with gr.Row():
                                        df_raw_output = gr.DataFrame(scale=1, label="Perbandingan Data", elem_classes="result-dataframe")
                                        df_raw_ori = gr.DataFrame(scale=1, label="Perbandingan Data", visible=False, elem_classes="result-dataframe")
                                        df_clean_output = gr.DataFrame(scale=1, label="Data Bersih 1", visible=False, elem_classes="result-dataframe")
                                        df_clean_output2 = gr.DataFrame(scale=1, label="Data Bersih 2", visible=False, elem_classes="result-dataframe")
                                        df_clean_output3 = gr.DataFrame(scale=1, label="Data Bersih 3", visible=False, elem_classes="result-dataframe")
                                
                                (
                                    eval_col_dd,
                                    eval_cat_dd,
                                    plot_output,
                                    stats_output,
                                    interpretation_output,
                                    strategy_plot_output,
                                    summary_stats_before,
                                    df_signifikansi_test,
                                    plot_tab,
                                    uji_statistik_tab
                                ) = call_data_evaluation()

            # ====================== WIRING EVENT HANDLERS ======================
            
            if upload.value:
                file_input.upload(fn=self.load_and_update_options, inputs=[gr.State(data_state_component.value), file_input, upload], outputs=[df_raw_output, eval_col_dd, col_filter, col_time, category_filter])
            else:
                demo.load(fn=self.load_and_update_options, inputs=[gr.State(data_state_component.value), file_input, upload], outputs=[df_raw_output, eval_col_dd, col_filter, col_time, category_filter])
            
            treatement_amount.change(fn=self.change_in_treatement_amount, inputs=[treatement_amount, df_handle_hyperparameter2, df_handle_hyperparameter3], outputs=[treatement_markdown2, handle_filter2, detection_filter2, df_detection_hyperparameter2, df_handle_hyperparameter2, treatement_markdown3, handle_filter3, detection_filter3, df_detection_hyperparameter3, df_handle_hyperparameter3])
             
            detection_filter.change(fn=self.change_in_detection_type, inputs=[detection_filter], outputs=[df_detection_hyperparameter])
            detection_filter2.change(fn=self.change_in_detection_type, inputs=[detection_filter2], outputs=[df_detection_hyperparameter2])
            detection_filter3.change(fn=self.change_in_detection_type, inputs=[detection_filter3], outputs=[df_detection_hyperparameter3])
            
            # WIRING ASLI (5-OUTPUT) DIPERTAHANKAN
            input_type_data.change(fn=self.change_in_data_type, inputs=[input_type_data],outputs=[col_time, handle_filter, category_filter, detection_filter, df_handle_hyperparameter])
            input_type_data.change(fn=self.change_in_data_type, inputs=[input_type_data],outputs=[col_time, handle_filter2, category_filter, detection_filter2, df_handle_hyperparameter2])
            input_type_data.change(fn=self.change_in_data_type, inputs=[input_type_data],outputs=[col_time, handle_filter3, category_filter, detection_filter3, df_handle_hyperparameter3])
    
            handle_filter.change(fn=self.change_in_handle_filter, inputs=[handle_filter], outputs=[df_handle_hyperparameter])
            handle_filter2.change(fn=self.change_in_handle_filter, inputs=[handle_filter2], outputs=[df_handle_hyperparameter2])
            handle_filter3.change(fn=self.change_in_handle_filter, inputs=[handle_filter3], outputs=[df_handle_hyperparameter3])
    

            aa_library = AALibrary()
            apply_btn.click(fn=aa_library.run_data_formatting_process, 
                            inputs=[gr.State(data_state_component.value), file_input, upload, eval_col_dd, col_filter, jenis_check, input_type_data, category_filter, col_time, treatement_amount, detection_filter,
                                    df_detection_hyperparameter,detection_filter2,df_detection_hyperparameter2,detection_filter3,df_detection_hyperparameter3,handle_filter,df_handle_hyperparameter,handle_filter2,df_handle_hyperparameter2,handle_filter3,df_handle_hyperparameter3], 
                            outputs=[df_raw_output, plot_output, stats_output, interpretation_output, eval_col_dd, eval_cat_dd, strategy_plot_output, summary_stats_before, df_signifikansi_test, df_clean_output, df_clean_output2, df_clean_output3, df_raw_ori])
    
            aa_rapid_eval = AA_Rapid_Eval()

            eval_col_dd.change(fn=aa_rapid_eval.change_in_plot_strategy, inputs=[df_raw_ori, df_clean_output, df_clean_output2, df_clean_output3, treatement_amount, eval_col_dd, category_filter, eval_cat_dd, col_time, input_type_data], outputs=[strategy_plot_output, plot_tab])
            eval_col_dd.change(fn=aa_rapid_eval.change_in_distribution_strategies, inputs=[df_raw_ori, df_clean_output, df_clean_output2, df_clean_output3, eval_col_dd, category_filter, eval_cat_dd, col_time, treatement_amount, input_type_data], outputs=[plot_output])
            eval_col_dd.change(fn=aa_rapid_eval.change_in_descriptive_and_signifikansi, inputs=[jenis_check, col_filter, input_type_data, category_filter, eval_cat_dd, treatement_amount, df_raw_ori, df_clean_output, df_clean_output2, df_clean_output3, eval_col_dd, col_time], outputs=[summary_stats_before, df_signifikansi_test])
            eval_col_dd.change(fn=aa_rapid_eval.create_comparison_viz_and_stats, inputs=[df_raw_ori, df_clean_output, eval_col_dd, input_type_data, col_time, category_filter, df_clean_output2, df_clean_output3, treatement_amount, eval_cat_dd], outputs=[stats_output, interpretation_output, uji_statistik_tab])

            eval_cat_dd.change(fn=aa_rapid_eval.change_in_plot_strategy, inputs=[df_raw_ori, df_clean_output, df_clean_output2, df_clean_output3, treatement_amount, eval_col_dd, category_filter, eval_cat_dd, col_time, input_type_data], outputs=[strategy_plot_output, plot_tab])
            eval_cat_dd.change(fn=aa_rapid_eval.change_in_distribution_strategies, inputs=[df_raw_ori, df_clean_output, df_clean_output2, df_clean_output3, eval_col_dd, category_filter, eval_cat_dd, col_time, treatement_amount, input_type_data], outputs=[plot_output])
            eval_cat_dd.change(fn=aa_rapid_eval.change_in_descriptive_and_signifikansi, inputs=[jenis_check, col_filter, input_type_data, category_filter, eval_cat_dd, treatement_amount, df_raw_ori, df_clean_output, df_clean_output2, df_clean_output3, eval_col_dd, col_time], outputs=[summary_stats_before, df_signifikansi_test])
            eval_col_dd.change(fn=aa_rapid_eval.create_comparison_viz_and_stats, inputs=[df_raw_ori, df_clean_output, eval_col_dd, input_type_data, col_time, category_filter, df_clean_output2, df_clean_output3, treatement_amount, eval_cat_dd], outputs=[stats_output, interpretation_output, uji_statistik_tab])

# ===================================================================
# Ringkasan Update (Outlier Lab UI)
# ===================================================================
# - Menambahkan metadata header (Notebook Title, Author, Date, Purpose).
# - Menambahkan docstring singkat di semua method utama kelas.
# - Membersihkan import (hapus subprocess, duplikasi sys).
# - Memperbaiki `load_and_update_options` agar mengembalikan pd.DataFrame kosong saat error.
# - Logika asli dari `part_outlier` (2 opsi deteksi, 2 opsi handle) DIPERTAHANKAN.
# - Logika asli dari `change_in_data_type` (5 return output) DIPERTAHANKAN.
# - Wiring Gradio asli (5 output untuk `input_type_data.change`) DIPERTAHANKAN.
# - Struktur UI dan nama komponen tetap kompatibel dengan versi sebelumnya.