lschrand/RM
0
1import pandas as pd2import numpy as np3import plotly.express as px4import plotly.graph_objects as go5import gradio as gr6from sklearn.cluster import KMeans7from sklearn.preprocessing import StandardScaler8from sklearn.metrics import silhouette_score9import os10 11# --- CONSTANTS ---12FEATURES = ['Fresh', 'Milk', 'Grocery', 'Frozen', 'Detergents_Paper', 'Delicassen']13CATEGORICAL = ['Channel', 'Region']14FILE_PATH = "R_Dataset.csv"15 16# --- CORE FUNCTIONS ---17 18def load_data():19 if not os.path.exists(FILE_PATH):20 return None, f"Error: {FILE_PATH} not found. Please ensure the dataset is in the root directory."21 try:22 df = pd.read_csv(FILE_PATH)23 return df, None24 except Exception as e:25 return None, f"Error loading data: {str(e)}"26 27def validate_data(df):28 required = FEATURES + CATEGORICAL29 missing = [col for col in required if col not in df.columns]30 if missing:31 return False, f"Missing columns: {', '.join(missing)}"32 return True, None33 34def preprocess_data(df):35 df_clean = df.copy().dropna(subset=FEATURES)36 scaler = StandardScaler()37 X_scaled = scaler.fit_transform(df_clean[FEATURES])38 return df_clean, X_scaled39 40def run_kmeans(df_clean, X_scaled, n_clusters):41 kmeans = KMeans(n_clusters=n_clusters, init='k-means++', n_init=10, random_state=42)42 df_model = df_clean.copy()43 df_model['Cluster'] = kmeans.fit_predict(X_scaled)44 # Ensure Cluster is treated as a discrete category for plotting45 df_model['Cluster'] = df_model['Cluster'].astype(str)46 return df_model47 48def create_diagnostics():49 df, err = load_data()50 if err: return gr.update(visible=False), err51 52 valid, v_err = validate_data(df)53 if not valid: return gr.update(visible=False), v_err54 55 df_clean, X_scaled = preprocess_data(df)56 57 wcss = []58 sil_scores = []59 k_range = range(2, 11)60 61 for k in k_range:62 km = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=42)63 km.fit(X_scaled)64 wcss.append(km.inertia_)65 sil_scores.append(silhouette_score(X_scaled, km.labels_))66 67 fig_elbow = px.line(x=list(k_range), y=wcss, title="Elbow Method (WCSS)",68 labels={'x': 'Number of Clusters', 'y': 'WCSS'}, markers=True)69 70 fig_sil = px.line(x=list(k_range), y=sil_scores, title="Silhouette Score Analysis",71 labels={'x': 'Number of Clusters', 'y': 'Score'}, markers=True)72 73 return fig_elbow, fig_sil, None74 75def create_3d_plot(n_clusters):76 df, err = load_data()77 if err: return None78 df_clean, X_scaled = preprocess_data(df)79 df_model = run_kmeans(df_clean, X_scaled, n_clusters)80 81 fig = px.scatter_3d(82 df_model,83 x='Grocery',84 y='Detergents_Paper',85 z='Milk',86 color='Cluster',87 symbol='Channel',88 title=f"3D Cluster Segmentation (K={n_clusters})",89 opacity=0.8,90 height=700,91 category_orders={"Cluster": [str(i) for i in range(n_clusters)]}92 )93 fig.update_layout(legend_title_text='Cluster ID')94 return fig95 96def create_cluster_profiles(n_clusters):97 df, err = load_data()98 if err: return None, None, None, None99 df_clean, X_scaled = preprocess_data(df)100 df_model = run_kmeans(df_clean, X_scaled, n_clusters)101 102 # 1. Cluster Sizes103 sizes = df_model.groupby('Cluster').size().reset_index(name='Count')104 fig_sizes = px.bar(sizes, x='Cluster', y='Count', title="Customer Count per Cluster", color='Cluster')105 106 # 2. Radar Chart / Means107 means = df_model.groupby('Cluster')[FEATURES].mean().reset_index()108 fig_radar = go.Figure()109 for i in range(n_clusters):110 cluster_id = str(i)111 row = means[means['Cluster'] == cluster_id]112 if not row.empty:113 fig_radar.add_trace(go.Scatterpolar(114 r=row[FEATURES].values[0],115 theta=FEATURES,116 fill='toself',117 name=f'Cluster {cluster_id}'118 ))119 fig_radar.update_layout(polar=dict(radialaxis=dict(visible=True)), title="Feature Means by Cluster")120 121 # 3. Channel Distribution122 channel_dist = df_model.groupby(['Cluster', 'Channel']).size().reset_index(name='Count')123 fig_channel = px.bar(channel_dist, x='Cluster', y='Count', color='Channel', barmode='group', title="Channel distribution per Cluster")124 125 # 4. Region Distribution126 region_dist = df_model.groupby(['Cluster', 'Region']).size().reset_index(name='Count')127 fig_region = px.bar(region_dist, x='Cluster', y='Count', color='Region', barmode='group', title="Region distribution per Cluster")128 129 return fig_sizes, fig_radar, fig_channel, fig_region130 131# --- GRADIO INTERFACE ---132 133with gr.Blocks(theme=gr.themes.Soft(), title="Recheio Customer Segmentation") as demo:134 gr.Markdown("# ๐ Recheio Customer Segmentation Dashboard")135 gr.Markdown("Production-grade K-Means clustering for wholesale customer behavior analysis.")136 137 with gr.Row():138 cluster_slider = gr.Slider(minimum=2, maximum=10, value=4, step=1, label="Select number of segments")139 140 with gr.Tabs():141 # TAB 1: DIAGNOSTICS142 with gr.TabItem("Clustering Diagnostics"):143 with gr.Row():144 diag_elbow = gr.Plot()145 diag_sil = gr.Plot()146 diag_err = gr.Markdown(visible=False)147 148 # TAB 2: 3D VISUALIZATION149 with gr.TabItem("3D Cluster Visualization"):150 plot_3d = gr.Plot()151 152 # TAB 3: PROFILING153 with gr.TabItem("Cluster Profiling"):154 with gr.Row():155 profile_sizes = gr.Plot()156 profile_radar = gr.Plot()157 with gr.Row():158 profile_channel = gr.Plot()159 profile_region = gr.Plot()160 161 # TAB 4: DATA DICTIONARY162 with gr.TabItem("Data Dictionary"):163 dict_data = {164 "Feature": FEATURES + CATEGORICAL,165 "Description": [166 "Annual spending on fresh products",167 "Annual spending on milk products",168 "Annual spending on grocery products",169 "Annual spending on frozen products",170 "Annual spending on detergents and paper products",171 "Annual spending on delicatessen products",172 "Customer Channel (1: Horeca, 2: Retail)",173 "Customer Region (1: Lisbon, 2: Oporto, 3: Other)"174 ]175 }176 gr.Table(value=pd.DataFrame(dict_data))177 178 # --- LOGIC BINDING ---179 180 # Static Load for Diagnostics181 demo.load(create_diagnostics, outputs=[diag_elbow, diag_sil, diag_err])182 183 # Dynamic Updates184 def update_dynamic_tabs(n):185 fig3d = create_3d_plot(n)186 p1, p2, p3, p4 = create_cluster_profiles(n)187 return fig3d, p1, p2, p3, p4188 189 cluster_slider.change(190 fn=update_dynamic_tabs,191 inputs=cluster_slider,192 outputs=[plot_3d, profile_sizes, profile_radar, profile_channel, profile_region]193 )194 195 # Initial load for dynamic components196 demo.load(197 fn=update_dynamic_tabs,198 inputs=cluster_slider,199 outputs=[plot_3d, profile_sizes, profile_radar, profile_channel, profile_region]200 )201 202if __name__ == "__main__":203 demo.launch()