# Notebook Title: Visualization Utilities for AA Prepflow
# Author: Advanced Analytics Team
# Date: 2025-11-13
# Purpose: Fungsi visualisasi & evaluasi distribusi untuk proses prep data (MV/Outlier)

from __future__ import annotations

# Standard Libraries
# import sys
from typing import List

# Data
import pandas as pd
from pandas.api.types import is_numeric_dtype

# Visualization
import plotly.graph_objects as go
import plotly.figure_factory as ff

# Stats
from scipy.stats import shapiro, kurtosis

# UI
import gradio as gr

# Project Modules
# sys.path.append("../aa-prepflow/")  # Add the src directory to sys.path
from ..models.outlier_value import (
    BoxplotStrategy,
    StatisticalSummaryStrategy,
    OutlierEvaluator,
    CrossSectionalTestStrategy,
    KolmogorovSmirnovTest,
    LeveneTest,
    TimeSeriesTestStrategy,
    PanelTestStrategy,
    TTestIndStrategy,
    MannWhitneyUTestStrategy,
    WilcoxonSignedRankTestStrategy,
    TTestPairedStrategy,
    create_summary_table,
)


class AA_Rapid_Eval:
    """Koleksi fungsi utilitas untuk AA Prepflow yang berubah berkali kali."""

    def normalize_by_parts(self, series: pd.Series) -> pd.Series:
        """Normalisasi kolom tanggal menjadi datetime YYYY-MM-DD yang konsisten."""
        if series.dtype == "object":
            temp_dt_series = pd.to_datetime(series, errors="raise")
        else:
            temp_dt_series = series

        year_str = temp_dt_series.dt.year.astype(str)
        month_str = temp_dt_series.dt.month.astype(str).str.zfill(2)
        day_str = temp_dt_series.dt.day.astype(str).str.zfill(2)

        # Heuristik sederhana bila day/month terbalik
        if int(day_str.astype(float).astype(int).sum()) > int(
            month_str.astype(float).astype(int).sum()
        ):
            temp = day_str
            day_str = month_str
            month_str = temp

        new_date_strings = year_str + "-" + month_str + "-" + day_str
        return pd.to_datetime(new_date_strings)


    def combine_into_a_dataframe(
        self, 
        df_raw: pd.DataFrame,
        version_dfs: List[pd.DataFrame],
        keep_raw_strings: bool = True,
    ) -> pd.DataFrame:
        """Gabungkan df_raw dan beberapa versi numerik ke satu DataFrame lebar."""

        def is_numeric_or_numeric_str(series: pd.Series) -> bool:
            """Cek apakah kolom numerik atau string numeric-like sederhana."""
            if pd.api.types.is_numeric_dtype(series):
                return True
            if pd.api.types.is_string_dtype(series):
                return series.map(
                    lambda x: isinstance(x, str) and x.replace(".", "", 1).isdigit(),
                ).all()
            return False

        def filter_version_df(df: pd.DataFrame) -> pd.DataFrame:
            """Ambil hanya kolom numerik atau numeric-like string dari df."""
            return df.loc[
                :,
                [col for col in df.columns if is_numeric_or_numeric_str(df[col])],
            ]

        def base(col: str) -> str:
            """Ambil base name kolom tanpa suffix _raw/_1/_2."""
            for suffix in ["_raw"] + [f"_{i}" for i in range(1, len(version_dfs) + 1)]:
                if col.endswith(suffix):
                    return col.replace(suffix, "")
            return col

        def combine_dataframes(
            df_raw_local: pd.DataFrame,
            version_dfs_local: List[pd.DataFrame],
            keep_raw_strings_local: bool = True,
        ) -> pd.DataFrame:
            """Gabungkan df_raw dan versi untuk kolom common dengan suffix terurut."""
            filtered_versions = [filter_version_df(df) for df in version_dfs_local]

            common_cols = set(filtered_versions[0].columns)
            for df in filtered_versions[1:]:
                common_cols &= set(df.columns)
            sorted_cols = sorted(common_cols)

            renamed_versions = []
            for i, df in enumerate(filtered_versions, start=1):
                df_selected = df[sorted_cols].copy()
                df_renamed = df_selected.rename(
                    columns={col: f"{col}_{i}" for col in df_selected.columns},
                )
                renamed_versions.append(df_renamed)

            df_raw_copy = df_raw_local.copy()
            raw_renamed_cols = {
                col: f"{col}_raw" if col in sorted_cols else col
                for col in df_raw_copy.columns
            }
            df_raw_renamed = df_raw_copy.rename(columns=raw_renamed_cols)

            combined_df = pd.concat([df_raw_renamed] + renamed_versions, axis=1)

            string_cols = [
                col
                for col in df_raw_local.columns
                if pd.api.types.is_string_dtype(df_raw_local[col])
                and col not in sorted_cols
            ]

            other_cols = [col for col in combined_df.columns if col not in string_cols]
            sorted_other_cols = sorted(other_cols, key=lambda x: (base(x), x))

            final_col_order = string_cols + sorted_other_cols
            combined_df = combined_df[final_col_order]

            return combined_df

        combined_df = combine_dataframes(
            df_raw,
            version_dfs,
            keep_raw_strings_local=keep_raw_strings,
        )
        return combined_df


    def round_float_columns(self, df: pd.DataFrame, decimals: int = 2) -> pd.DataFrame:
        """Membulatkan kolom numerik / numeric-string ke sejumlah desimal."""
        df_rounded = df.copy()
        for col in df_rounded.columns:
            if pd.api.types.is_numeric_dtype(df_rounded[col]):
                df_rounded[col] = df_rounded[col].round(decimals)
            elif pd.api.types.is_object_dtype(df_rounded[col]):
                try:
                    numeric_series = pd.to_numeric(df_rounded[col], errors="raise")
                    df_rounded[col] = numeric_series.round(decimals).astype(str)
                except (ValueError, TypeError):
                    pass
        return df_rounded


    def change_in_plot_strategy(
        self, 
        df_raw: pd.DataFrame,
        df_clean: pd.DataFrame,
        df_clean2: pd.DataFrame,
        df_clean3: pd.DataFrame,
        treatement_amount,
        col_to_eval,
        category_filter,
        cat_to_eval,
        col_time,
        input_type_data,
    ):
        """Visual boxplot/TS untuk membandingkan raw vs treatment per variabel."""
        if (
            df_raw is None
            or df_raw.empty
            or col_to_eval is None
            or col_to_eval not in list(df_raw.columns)
        ):
            return go.Figure(), gr.update(visible=False)

        if input_type_data == "Panel" and category_filter in df_raw.columns:
            df_raw = df_raw.loc[
                df_raw[category_filter] == cat_to_eval,
                [category_filter, col_time, col_to_eval],
            ]
            if df_clean is not None and category_filter in df_clean.columns:
                df_clean = df_clean.loc[
                    df_clean[category_filter] == cat_to_eval,
                    [category_filter, col_time, col_to_eval],
                ]
            if df_clean2 is not None and category_filter in df_clean2.columns:
                df_clean2 = df_clean2.loc[
                    df_clean2[category_filter] == cat_to_eval,
                    [category_filter, col_time, col_to_eval],
                ]
            if df_clean3 is not None and category_filter in df_clean3.columns:
                df_clean3 = df_clean3.loc[
                    df_clean3[category_filter] == cat_to_eval,
                    [category_filter, col_time, col_to_eval],
                ]

        if input_type_data == "Cross Sectional Data":
            box_plot_strategy = BoxplotStrategy()
            strategy_plot_output = box_plot_strategy.evaluate(
                df_raw[col_to_eval].to_frame(),
                df_clean[col_to_eval].to_frame(),
                df_clean2[col_to_eval].to_frame(),
                df_clean3[col_to_eval].to_frame(),
                treatement_amount,
            )
        elif input_type_data == "Timeseries":
            strategy_plot_output = go.Figure()
            return strategy_plot_output, gr.update(visible=False)
        elif input_type_data == "Panel":
            strategy_plot_output = go.Figure()
            return strategy_plot_output, gr.update(visible=False)

        return strategy_plot_output, gr.update(visible=True)


    def change_in_distribution_strategies(
        self, 
        df_raw: pd.DataFrame,
        df_clean: pd.DataFrame,
        df_clean2: pd.DataFrame,
        df_clean3: pd.DataFrame,
        col_to_eval,
        category_filter,
        cat_to_eval,
        col_time,
        treatement_amount,
        input_type_data,
    ):
        """Buat plot distribusi (density/TS) untuk membandingkan versi data."""
        if (
            df_raw is None
            or df_raw.empty
            or col_to_eval is None
            or col_to_eval not in list(df_raw.columns)
        ):
            return go.Figure()

        df_raw_filtered = df_raw.copy()
        df_clean_filtered = df_clean.copy() if df_clean is not None else None
        df_clean2_filtered = df_clean2.copy() if df_clean2 is not None else None
        df_clean3_filtered = df_clean3.copy() if df_clean3 is not None else None

        if (
            input_type_data == "Panel"
            and category_filter in df_raw.columns
            and cat_to_eval
        ):
            df_raw_filtered = df_raw.loc[
                df_raw[category_filter] == cat_to_eval,
                [col_time, col_to_eval],
            ].copy()
            if df_clean is not None and category_filter in df_clean.columns:
                df_clean_filtered = df_clean.loc[
                    df_clean[category_filter] == cat_to_eval,
                    [col_time, col_to_eval],
                ].copy()
            if df_clean2 is not None and category_filter in df_clean2.columns:
                df_clean2_filtered = df_clean2.loc[
                    df_clean2[category_filter] == cat_to_eval,
                    [col_time, col_to_eval],
                ].copy()
            if df_clean3 is not None and category_filter in df_clean3.columns:
                df_clean3_filtered = df_clean3.loc[
                    df_clean3[category_filter] == cat_to_eval,
                    [col_time, col_to_eval],
                ].copy()

        custom_settings = [
            {
                "df": df_raw_filtered,
                "name": "Sebelum",
                "color": "#636EFA",
                "marker": "circle",
                "dash": "dash",
            },
            {
                "df": df_clean_filtered,
                "name": "Treatment 1",
                "color": "#fcba03",
                "marker": "x",
                "dash": "solid",
            },
        ]

        if int(treatement_amount) >= 2 and df_clean2 is not None:
            custom_settings.append(
                {
                    "df": df_clean2_filtered,
                    "name": "Treatment 2",
                    "color": "#00CC96",
                    "marker": "square",
                    "dash": "solid",
                },
            )

        if int(treatement_amount) == 3 and df_clean3 is not None:
            custom_settings.append(
                {
                    "df": df_clean3_filtered,
                    "name": "Treatment 3",
                    "color": "#EF553B",
                    "marker": "triangle-up",
                    "dash": "solid",
                },
            )

        fig = go.Figure()

        # Cross-sectional → density plot (KDE)
        if input_type_data == "Cross Sectional Data":
            data_to_plot = []
            group_labels = []
            colors = []

            cross_sectional_settings = [
                {"df": df_raw, "name": "Sebelum", "color": "#636EFA"},
                {"df": df_clean, "name": "Treatment 1", "color": "#fcba03"},
            ]
            if int(treatement_amount) >= 2 and df_clean2 is not None:
                cross_sectional_settings.append(
                    {"df": df_clean2, "name": "Treatment 2", "color": "#00CC96"},
                )
            if int(treatement_amount) == 3 and df_clean3 is not None:
                cross_sectional_settings.append(
                    {"df": df_clean3, "name": "Treatment 3", "color": "#EF553B"},
                )

            for setting in cross_sectional_settings:
                df = setting["df"]
                name = setting["name"]
                color = setting["color"]

                if (
                    df is not None
                    and col_to_eval in df.columns
                    and is_numeric_dtype(df[col_to_eval])
                ):
                    series = df[col_to_eval].dropna()
                    if not series.empty:
                        data_to_plot.append(series.tolist())
                        group_labels.append(name)
                        colors.append(color)

            if data_to_plot:
                fig = ff.create_distplot(
                    data_to_plot,
                    group_labels,
                    colors=colors,
                    bin_size=[],
                    show_hist=False,
                    show_rug=False,
                )

                fig.update_layout(
                    title_text=(
                        f'Perbandingan Distribusi Kolom "{col_to_eval}" (Density Plot)'
                    ),
                    xaxis_title_text=col_to_eval,
                    yaxis_title_text="Density (KDE)",
                    legend_title_text="Versi Data",
                    template="plotly_white",
                    hovermode="x unified",
                    margin=dict(l=40, r=40, t=80, b=40),
                )

                for trace in fig.data:
                    if trace.name in group_labels:
                        idx = group_labels.index(trace.name)
                        trace.line.dash = "dash" if idx == 0 else "solid"

                return fig

        # Time series / Panel → line chart
        else:
            for setting in custom_settings:
                df = setting["df"]
                name = setting["name"]
                color = setting["color"]
                marker = setting["marker"]
                dash = setting["dash"]

                if df is None:
                    continue

                df = df.reset_index(drop=True)

                if col_to_eval in df.columns and is_numeric_dtype(df[col_to_eval]):
                    if col_time in df.columns:
                        df[col_time] = self.normalize_by_parts(df[col_time])
                        df = df.sort_values(by=col_time).dropna(
                            subset=[col_time, col_to_eval],
                        )

                        if not df.empty:
                            fig.add_trace(
                                go.Scatter(
                                    x=df[col_time],
                                    y=df[col_to_eval],
                                    name=name,
                                    mode="lines+markers",
                                    line=dict(color=color, width=2, dash=dash),
                                    marker=dict(symbol=marker, size=6, color=color),
                                    opacity=0.8,
                                ),
                            )

            if fig.data:
                fig.update_layout(
                    legend_title_text="Versi Data",
                    template="plotly_white",
                    hovermode="x unified",
                    margin=dict(l=40, r=40, t=80, b=40),
                    title_text=(
                        "Perbandingan Time Series Sebelum & Sesudah Penanganan"
                        f"<br>Variabel: {col_to_eval}"
                    ),
                )
                fig.update_xaxes(
                    title_text="Date",
                    tickangle=-45,
                    tickformat="%d/%m/%Y",
                    showgrid=True,
                    gridwidth=1,
                    gridcolor="lightgray",
                    zeroline=True,
                    zerolinewidth=1,
                    zerolinecolor="lightgray",
                )
                fig.update_yaxes(
                    title_text="Nilai",
                    showgrid=True,
                    gridwidth=1,
                    gridcolor="lightgray",
                    zeroline=True,
                    zerolinewidth=1,
                    zerolinecolor="lightgray",
                )

        return fig


    def change_in_descriptive_and_signifikansi(
        self, 
        jenis_check,
        col_filter,
        input_type_data,
        category_filter,
        eval_cat_dd,
        treatement_amount,
        df_raw,
        df_clean,
        df_clean2,
        df_clean3,
        eval_col_dd,
        col_time,
    ):
        """Hitung statistik deskriptif & uji signifikansi antar treatment."""
        if input_type_data == "Panel":
            if eval_cat_dd is None or eval_col_dd is None:
                return pd.DataFrame(), pd.DataFrame()
        else:
            if eval_col_dd is None:
                return pd.DataFrame(), pd.DataFrame()

        df_raw = df_raw.drop(columns=col_filter)

        if input_type_data == "Panel" and category_filter in df_raw.columns:
            df_raw = df_raw.loc[
                df_raw[category_filter] == eval_cat_dd,
                [category_filter, col_time, eval_col_dd],
            ]
            if df_clean is not None and category_filter in df_clean.columns:
                df_clean = df_clean.loc[
                    df_clean[category_filter] == eval_cat_dd,
                    [category_filter, col_time, eval_col_dd],
                ]
            if df_clean2 is not None and category_filter in df_clean2.columns:
                df_clean2 = df_clean2.loc[
                    df_clean2[category_filter] == eval_cat_dd,
                    [category_filter, col_time, eval_col_dd],
                ]
            if df_clean3 is not None and category_filter in df_clean3.columns:
                df_clean3 = df_clean3.loc[
                    df_clean3[category_filter] == eval_cat_dd,
                    [category_filter, col_time, eval_col_dd],
                ]

        summary_class = StatisticalSummaryStrategy()
        summary_stats_before, summary_stats_after = summary_class.evaluate(
            df_raw,
            df_clean,
        )
        summary_stats_after = self.round_float_columns(summary_stats_after)

        if int(treatement_amount) >= 2:
            _, summary_stats_after2 = summary_class.evaluate(df_raw, df_clean2)
            summary_stats_after2 = self.round_float_columns(summary_stats_after2)
        if int(treatement_amount) == 3:
            _, summary_stats_after3 = summary_class.evaluate(df_raw, df_clean3)
            summary_stats_after3 = self.round_float_columns(summary_stats_after3)

        summary_stats_before = self.round_float_columns(summary_stats_before)

        # Gabung deskriptif: cross-section vs panel
        if int(treatement_amount) == 1:
            if input_type_data == "Panel":
                del summary_stats_before["Variable"]
                del summary_stats_after["Variable"]
                dfs_panel = [summary_stats_before.T, summary_stats_after.T]
                panel_summary_stats = pd.concat(dfs_panel, axis=1)
                panel_summary_stats.columns = ["Raw", "Treatment 1"]
                summary_stats_before = panel_summary_stats.reset_index()
                summary_stats_before.rename(columns={"index": "Categories"}, inplace=True)
            else:
                summary_stats_before = self.combine_into_a_dataframe(
                    summary_stats_before,
                    [summary_stats_after],
                )
            evaluator_p = OutlierEvaluator(df_raw, df_clean)

        elif int(treatement_amount) == 2:
            if input_type_data == "Panel":
                del summary_stats_before["Variable"]
                del summary_stats_after["Variable"]
                del summary_stats_after2["Variable"]
                dfs_panel = [
                    summary_stats_before.T,
                    summary_stats_after.T,
                    summary_stats_after2.T,
                ]
                panel_summary_stats = pd.concat(dfs_panel, axis=1)
                panel_summary_stats.columns = ["Raw", "Treatment 1", "Treatment 2"]
                summary_stats_before = panel_summary_stats.reset_index()
                summary_stats_before.rename(columns={"index": "Categories"}, inplace=True)
            else:
                summary_stats_before = self.combine_into_a_dataframe(
                    summary_stats_before,
                    [summary_stats_after, summary_stats_after2],
                )
            evaluator_p = OutlierEvaluator(df_raw, df_clean)
            evaluator_p2 = OutlierEvaluator(df_raw, df_clean2)

        elif int(treatement_amount) == 3:
            if input_type_data == "Panel":
                del summary_stats_before["Variable"]
                del summary_stats_after["Variable"]
                del summary_stats_after2["Variable"]
                del summary_stats_after3["Variable"]
                dfs_panel = [
                    summary_stats_before.T,
                    summary_stats_after.T,
                    summary_stats_after2.T,
                    summary_stats_after3.T,
                ]
                panel_summary_stats = pd.concat(dfs_panel, axis=1)
                panel_summary_stats.columns = [
                    "Raw",
                    "Treatment 1",
                    "Treatment 2",
                    "Treatment 3",
                ]
                summary_stats_before = panel_summary_stats.reset_index()
                summary_stats_before.rename(columns={"index": "Categories"}, inplace=True)
            else:
                summary_stats_before = self.combine_into_a_dataframe(
                    summary_stats_before,
                    [summary_stats_after, summary_stats_after2, summary_stats_after3],
                )
            evaluator_p = OutlierEvaluator(df_raw, df_clean)
            evaluator_p2 = OutlierEvaluator(df_raw, df_clean2)
            evaluator_p3 = OutlierEvaluator(df_raw, df_clean3)

        # ================================
        # Significance test per tipe data
        # ================================
        if input_type_data == "Cross Sectional Data":
            cs_strategies = [
                CrossSectionalTestStrategy(test=TTestIndStrategy()),
                CrossSectionalTestStrategy(test=MannWhitneyUTestStrategy()),
                CrossSectionalTestStrategy(test=KolmogorovSmirnovTest()),
                CrossSectionalTestStrategy(test=LeveneTest()),
            ]
            cs_test_map = {
                "T-Test_Ind_p_value_mean": {
                    "name": "Uji T-Test (Rata-rata)",
                    "sig": "Perbedaan Signifikan",
                    "insig": "Tidak Signifikan",
                },
                "MannWhitneyU_p_value_median": {
                    "name": "Uji Mann-Whitney (Median)",
                    "sig": "Perbedaan Signifikan",
                    "insig": "Tidak Signifikan",
                },
                "Levene_p_value_variance": {
                    "name": "Uji Levene (Varians)",
                    "sig": "Perbedaan Signifikan",
                    "insig": "Tidak Signifikan",
                },
                "KS_p_value": {
                    "name": "Uji KS (Distribusi)",
                    "sig": "Perbedaan Signifikan",
                    "insig": "Tidak Signifikan",
                },
            }

            if int(treatement_amount) == 1:
                cs_results = evaluator_p.run(cs_strategies)
                cs_summary = create_summary_table(cs_results, cs_test_map)
                cs_summary_final = cs_summary

            elif int(treatement_amount) == 2:
                cs_results = evaluator_p.run(cs_strategies)
                cs_summary = create_summary_table(cs_results, cs_test_map)
                cs_results2 = evaluator_p2.run(cs_strategies)
                cs_summary2 = create_summary_table(cs_results2, cs_test_map)
                cs_summary_final = cs_summary.join(
                    cs_summary2,
                    how="left",
                    lsuffix="_1",
                    rsuffix="_2",
                )

            else:  # 3 treatments
                cs_results = evaluator_p.run(cs_strategies)
                cs_summary = create_summary_table(cs_results, cs_test_map)
                cs_results2 = evaluator_p2.run(cs_strategies)
                cs_summary2 = create_summary_table(cs_results2, cs_test_map)
                cs_results3 = evaluator_p3.run(cs_strategies)
                cs_summary3 = create_summary_table(cs_results3, cs_test_map)

                cs_summary_final = cs_summary.add_suffix("_1").join(
                    cs_summary2.add_suffix("_2"),
                    how="left",
                )
                cs_summary_final = cs_summary_final.join(
                    cs_summary3.add_suffix("_3"),
                    how="left",
                )

            df_signifikansi_test = cs_summary_final.sort_index(axis=1)

        elif input_type_data == "Timeseries":
            if jenis_check == "missing_value":
                ts_strategies = [
                    TimeSeriesTestStrategy(test=LeveneTest()),
                    TimeSeriesTestStrategy(test=KolmogorovSmirnovTest()),
                ]
                ts_test_map = {
                    "Levene_p_value_variance": {
                        "name": "Uji Levene (Varians)",
                        "sig": "Perbedaan Signifikan",
                        "insig": "Tidak Signifikan",
                    },
                    "KS_p_value": {
                        "name": "Uji KS (Distribusi)",
                        "sig": "Perbedaan Signifikan",
                        "insig": "Tidak Signifikan",
                    },
                }
                column_array_test = [
                    "Uji Levene (Varians)",
                    "Uji KS (Distribusi)",
                ]
            else:
                ts_strategies = [
                    TimeSeriesTestStrategy(test=TTestPairedStrategy()),
                    TimeSeriesTestStrategy(test=WilcoxonSignedRankTestStrategy()),
                    TimeSeriesTestStrategy(test=LeveneTest()),
                    TimeSeriesTestStrategy(test=KolmogorovSmirnovTest()),
                ]
                ts_test_map = {
                    "T-Test_Paired_p_value_mean": {
                        "name": "Uji T-Test Berpasangan (Rata-rata)",
                        "sig": "Perbedaan Signifikan",
                        "insig": "Tidak Signifikan",
                    },
                    "Wilcoxon_p_value_median": {
                        "name": "Uji Wilcoxon (Median)",
                        "sig": "Perbedaan Signifikan",
                        "insig": "Tidak Signifikan",
                    },
                    "Levene_p_value_variance": {
                        "name": "Uji Levene (Varians)",
                        "sig": "Perbedaan Signifikan",
                        "insig": "Tidak Signifikan",
                    },
                    "KS_p_value": {
                        "name": "Uji KS (Distribusi)",
                        "sig": "Perbedaan Signifikan",
                        "insig": "Tidak Signifikan",
                    },
                }
                column_array_test = [
                    "Uji T-Test Berpasangan (Rata-rata)",
                    "Uji Wilcoxon (Median)",
                    "Uji Levene (Varians)",
                    "Uji KS (Distribusi)",
                ]

            if int(treatement_amount) == 1:
                ts_results = evaluator_p.run(ts_strategies)
                ts_summary = create_summary_table(ts_results, ts_test_map)
                ts_summary_final = ts_summary

            elif int(treatement_amount) == 2:
                ts_results = evaluator_p.run(ts_strategies)
                ts_summary = create_summary_table(ts_results, ts_test_map)
                ts_results2 = evaluator_p2.run(ts_strategies)
                ts_summary2 = create_summary_table(ts_results2, ts_test_map)
                ts_summary_final = ts_summary.join(
                    ts_summary2,
                    how="left",
                    lsuffix="_1",
                    rsuffix="_2",
                )

            else:
                ts_results = evaluator_p.run(ts_strategies)
                ts_summary = create_summary_table(ts_results, ts_test_map)
                ts_results2 = evaluator_p2.run(ts_strategies)
                ts_summary2 = create_summary_table(ts_results2, ts_test_map)
                ts_results3 = evaluator_p3.run(ts_strategies)
                ts_summary3 = create_summary_table(ts_results3, ts_test_map)

                ts_summary_final = ts_summary.add_suffix("_1").join(
                    ts_summary2.add_suffix("_2"),
                    how="left",
                )
                ts_summary_final = ts_summary_final.join(
                    ts_summary3.add_suffix("_3"),
                    how="left",
                )

            df_signifikansi_test = ts_summary_final.sort_index(axis=1)

        elif input_type_data == "Panel":
            if jenis_check == "missing_value":
                panel_strategies = [
                    PanelTestStrategy(group_col=category_filter, test=LeveneTest()),
                    PanelTestStrategy(
                        group_col=category_filter,
                        test=KolmogorovSmirnovTest(),
                    ),
                ]
                panel_test_map = {
                    "Levene_p_value_variance": {
                        "name": "Uji Levene (Varians)",
                        "sig": "Perbedaan Signifikan",
                        "insig": "Tidak Signifikan",
                    },
                    "KS_p_value": {
                        "name": "Uji KS (Distribusi)",
                        "sig": "Perbedaan Signifikan",
                        "insig": "Tidak Signifikan",
                    },
                }
                column_array_test = [
                    "Uji Levene (Varians)",
                    "Uji KS (Distribusi)",
                ]
            else:
                panel_strategies = [
                    PanelTestStrategy(
                        group_col=category_filter,
                        test=TTestPairedStrategy(),
                    ),
                    PanelTestStrategy(
                        group_col=category_filter,
                        test=WilcoxonSignedRankTestStrategy(),
                    ),
                    PanelTestStrategy(group_col=category_filter, test=LeveneTest()),
                    PanelTestStrategy(
                        group_col=category_filter,
                        test=KolmogorovSmirnovTest(),
                    ),
                ]
                panel_test_map = {
                    "T-Test_Paired_p_value_mean": {
                        "name": "Uji T-Test Berpasangan (Rata-rata)",
                        "sig": "Perbedaan Signifikan",
                        "insig": "Tidak Signifikan",
                    },
                    "Wilcoxon_p_value_median": {
                        "name": "Uji Wilcoxon (Median)",
                        "sig": "Perbedaan Signifikan",
                        "insig": "Tidak Signifikan",
                    },
                    "Levene_p_value_variance": {
                        "name": "Uji Levene (Varians)",
                        "sig": "Perbedaan Signifikan",
                        "insig": "Tidak Signifikan",
                    },
                    "KS_p_value": {
                        "name": "Uji KS (Distribusi)",
                        "sig": "Perbedaan Signifikan",
                        "insig": "Tidak Signifikan",
                    },
                }
                column_array_test = [
                    "Uji T-Test Berpasangan (Rata-rata)",
                    "Uji Wilcoxon (Median)",
                    "Uji Levene (Varians)",
                    "Uji KS (Distribusi)",
                ]

            if int(treatement_amount) == 1:
                panel_results = evaluator_p.run(panel_strategies)
                panel_summary = create_summary_table(panel_results, panel_test_map)
                panel_summary_final = panel_summary
                
            elif int(treatement_amount) == 2:
                panel_results = evaluator_p.run(panel_strategies)
                panel_summary = create_summary_table(panel_results, panel_test_map)
                panel_results2 = evaluator_p2.run(panel_strategies)
                panel_summary2 = create_summary_table(panel_results2, panel_test_map)
                
                # Menggunakan .join dengan suffix
                panel_summary_final = panel_summary.join(
                    panel_summary2,
                    how="left",
                    lsuffix="_1",
                    rsuffix="_2",
                )
                
            else:
                panel_results = evaluator_p.run(panel_strategies)
                panel_summary = create_summary_table(panel_results, panel_test_map)
                panel_results2 = evaluator_p2.run(panel_strategies)
                panel_summary2 = create_summary_table(panel_results2, panel_test_map)
                panel_results3 = evaluator_p3.run(panel_strategies)
                panel_summary3 = create_summary_table(panel_results3, panel_test_map)
                
                # Menggunakan .join dengan suffix
                panel_summary_final = panel_summary.add_suffix("_1").join(
                    panel_summary2.add_suffix("_2"),
                    how="left",
                )
                panel_summary_final = panel_summary_final.join(
                    panel_summary3.add_suffix("_3"),
                    how="left",
                )
                
            df_signifikansi_test = panel_summary_final.sort_index(axis=1)

        df_signifikansi_test = self.round_float_columns(df_signifikansi_test.reset_index())
        if input_type_data == "Panel":
            df_signifikansi_test.rename(columns={"index": "Categories"}, inplace=True)
        else:
            df_signifikansi_test.rename(columns={"index": "Column Name"}, inplace=True)

        return summary_stats_before, df_signifikansi_test


    def create_comparison_viz_and_stats(
        self, 
        df_raw: pd.DataFrame,
        df_clean: pd.DataFrame,
        col_to_eval,
        input_type_data,
        col_time,
        category_filter,
        df_clean2,
        df_clean3,
        treatement_amount,
        eval_cat_dd,
    ):
        """Hitung variance/skewness/kurtosis per versi dan teks interpretasi."""
        if col_to_eval is None or df_raw is None or df_clean is None:
            # Kembalikan tipe data yang benar untuk setiap komponen:
            # 1. stats_output (DataFrame) -> pd.DataFrame()
            # 2. interpretation_output (Markdown) -> "" (string kosong)
            # 3. uji_statistik_tab (Tab) -> gr.update()
            return pd.DataFrame(), "", gr.update(visible=False)

        if input_type_data == "Cross Sectional Data":
            stats_data = []

            if (
                col_to_eval in list(df_raw.columns)
                and pd.api.types.is_numeric_dtype(df_raw[col_to_eval])
            ):
                series_raw = df_raw[col_to_eval].dropna()
                if len(series_raw) >= 3:
                    stats_data.append(
                        {
                            "Versi Data": "Mentah",
                            "Variance": f"{series_raw.var():.2f}",
                            "Skewness": f"{series_raw.skew():.2f}",
                            "Shapiro-Wilk (p-val)": f"{shapiro(series_raw).pvalue:.4f}",
                            "Kurtosis": f"{kurtosis(series_raw):.2f}",
                        },
                    )

            if (
                col_to_eval in list(df_clean.columns)
                and pd.api.types.is_numeric_dtype(df_clean[col_to_eval])
            ):
                series_clean = df_clean[col_to_eval].dropna()
                if len(series_clean) >= 3:
                    stats_data.append(
                        {
                            "Versi Data": "Bersih 1",
                            "Variance": f"{series_clean.var():.2f}",
                            "Skewness": f"{series_clean.skew():.2f}",
                            "Shapiro-Wilk (p-val)": f"{shapiro(series_clean).pvalue:.4f}",
                            "Kurtosis": f"{kurtosis(series_clean):.2f}",
                        },
                    )

            if int(treatement_amount) >= 2:
                if (
                    col_to_eval in list(df_clean2.columns)
                    and pd.api.types.is_numeric_dtype(df_clean2[col_to_eval])
                ):
                    series_clean = df_clean2[col_to_eval].dropna()
                    if len(series_clean) >= 3:
                        stats_data.append(
                            {
                                "Versi Data": "Bersih 2",
                                "Variance": f"{series_clean.var():.2f}",
                                "Skewness": f"{series_clean.skew():.2f}",
                                "Shapiro-Wilk (p-val)": f"{shapiro(series_clean).pvalue:.4f}",
                                "Kurtosis": f"{kurtosis(series_clean):.2f}",
                            },
                        )

            if int(treatement_amount) == 3:
                if (
                    col_to_eval in list(df_clean3.columns)
                    and pd.api.types.is_numeric_dtype(df_clean3[col_to_eval])
                ):
                    series_clean = df_clean3[col_to_eval].dropna()
                    if len(series_clean) >= 3:
                        stats_data.append(
                            {
                                "Versi Data": "Bersih 3",
                                "Variance": f"{series_clean.var():.2f}",
                                "Skewness": f"{series_clean.skew():.2f}",
                                "Shapiro-Wilk (p-val)": f"{shapiro(series_clean).pvalue:.4f}",
                                "Kurtosis": f"{kurtosis(series_clean):.2f}",
                            },
                        )

            stats_df = self.round_float_columns(pd.DataFrame(stats_data))
        else:
            stats_df = pd.DataFrame()

        interpretation_text = """
    ### 💡 Interpretasi Statistik
    * **Variance (Varians)**: Mengukur seberapa tersebar data dari rata-ratanya.
    * **Skewness (Kemiringan)**: > 0 miring ke kanan, < 0 miring ke kiri, ~0 simetris.
    * **Shapiro-Wilk (p-value)**: p > 0.05 → cenderung normal; p < 0.05 → tidak normal.
    * **Kurtosis**: > 0 lebih banyak outlier ekstrem; < 0 lebih sedikit; ≈0 mirip normal.
    """

        if input_type_data == "Cross Sectional Data":
            return stats_df, interpretation_text, gr.update(visible=True)
        return stats_df, interpretation_text, gr.update(visible=False)


# ===================================================================
# Ringkasan Hasil / Summary Update (visualization)
# ===================================================================
# - Menambahkan metadata notebook-style di bagian atas modul.
# - Menstandarkan import & naming (snake_case, PascalCase).
# - Memberi docstring singkat (≤3 baris) untuk semua fungsi publik.
# - Memperbaiki:
#   * Filter panel di change_in_plot_strategy (slice kolom yang benar).
#   * Penanganan treatement_amount==1 pada panel significance (tidak gunakan dfs undefined).