Spaces:
Sleeping
Sleeping
# modules/profiling.py | |
# -*- coding: utf-8 -*- | |
# | |
# PROJECT: CognitiveEDA v5.7 - The QuantumLeap Intelligence Platform | |
# | |
# DESCRIPTION: A dedicated module for profiling and characterizing customer | |
# segments identified through clustering. | |
import pandas as pd | |
import plotly.express as px | |
import plotly.graph_objects as go | |
import logging | |
def profile_clusters(df: pd.DataFrame, cluster_labels: pd.Series, numeric_cols: list, cat_cols: list) -> tuple: | |
""" | |
Analyzes and profiles clusters to create meaningful business personas. | |
This function groups the data by cluster and calculates key statistics | |
for numeric and categorical features to describe each segment. It then | |
visualizes these differences. | |
Args: | |
df (pd.DataFrame): The feature-engineered DataFrame. | |
cluster_labels (pd.Series): The series of cluster labels from the K-Means model. | |
numeric_cols (list): List of numeric columns to profile (e.g., ['Total_Revenue']). | |
cat_cols (list): List of categorical columns to profile (e.g., ['City', 'Product']). | |
Returns: | |
A tuple containing: | |
- A markdown string with the detailed profile of each cluster. | |
- A Plotly Figure visualizing the differences between clusters. | |
""" | |
# Ensure the dataframe used for profiling has the same index as the labels | |
profile_df = df.loc[cluster_labels.index].copy() | |
profile_df['Cluster'] = cluster_labels | |
if profile_df.empty: | |
return "No data available to profile clusters.", go.Figure() | |
logging.info(f"Profiling {profile_df['Cluster'].nunique()} clusters...") | |
# --- Generate Markdown Report --- | |
report_md = "### Cluster Persona Analysis\n\n" | |
# Analyze numeric features by cluster | |
numeric_profile = profile_df.groupby('Cluster')[numeric_cols].mean().round(2) | |
# Analyze categorical features by cluster (get the most frequent value - mode) | |
cat_profile_list = [] | |
for col in cat_cols: | |
# This lambda is more robust for cases where a mode might not exist | |
mode_series = profile_df.groupby('Cluster')[col].apply(lambda x: x.mode()[0] if not x.mode().empty else "N/A") | |
mode_df = mode_series.to_frame() | |
cat_profile_list.append(mode_df) | |
full_profile = pd.concat([numeric_profile] + cat_profile_list, axis=1) | |
for cluster_id in sorted(profile_df['Cluster'].unique()): | |
# Try to name the persona by the dominant city, fall back to a generic name | |
try: | |
persona_name = full_profile.loc[cluster_id, 'City'] | |
except KeyError: | |
persona_name = f"Segment {cluster_id}" | |
report_md += f"#### Cluster {cluster_id}: The '{persona_name}' Persona\n" | |
# Numeric Summary | |
for col in numeric_cols: | |
val = full_profile.loc[cluster_id, col] | |
report_md += f"- **Avg. {col.replace('_', ' ')}:** `{val:,.2f}`\n" | |
# Categorical Summary | |
for col in cat_cols: | |
val = full_profile.loc[cluster_id, col] | |
report_md += f"- **Dominant {col.replace('_', ' ')}:** `{val}`\n" | |
report_md += "\n" | |
# --- Generate Visualization --- | |
# We'll visualize the average 'Total_Revenue' by 'City' for each cluster | |
# This directly tests our hypothesis that 'City' is the dominant feature. | |
try: | |
vis_df = profile_df.groupby(['Cluster', 'City'])['Total_Revenue'].mean().reset_index() | |
fig = px.bar( | |
vis_df, | |
x='Cluster', | |
y='Total_Revenue', | |
color='City', | |
barmode='group', | |
title='<b>Cluster Profile: Avg. Total Revenue by Dominant City</b>', | |
labels={'Total_Revenue': 'Average Total Revenue ($)', 'Cluster': 'Customer Segment'} | |
) | |
except Exception as e: | |
logging.error(f"Could not generate profile visualization: {e}") | |
fig = go.Figure().update_layout(title="Could not generate profile visualization.") | |
return report_md, fig |