| import re |
| import streamlit as st |
| import plotly.express as px |
| import plotly.graph_objects as go |
| from plotly.subplots import make_subplots |
| import pandas as pd |
| import numpy as np |
| from src.utils.logging import log_frontend_error, log_frontend_warning |
|
|
| SAMPLE_SIZE = 10000 |
|
|
| |
| @st.cache_data(show_spinner=False) |
| def compute_df_hash(df): |
| """Optimized dataframe hashing""" |
| return hash((df.shape, pd.util.hash_pandas_object(df.iloc[:min(100, len(df))]).sum())) |
|
|
|
|
| @st.cache_data(show_spinner=False, ttl=3600) |
| def is_potential_date_column(series, sample_size=5): |
| """Check if column might contain dates""" |
| |
| if any(keyword in series.name.lower() for keyword in ['date', 'time', 'year', 'month', 'day']): |
| return True |
| |
| |
| sample = series.dropna().head(sample_size).astype(str) |
| date_patterns = [ |
| r'\d{4}-\d{2}-\d{2}', |
| r'\d{2}/\d{2}/\d{4}', |
| r'\d{2}-\w{3}-\d{2,4}', |
| r'\d{1,2} \w{3,} \d{4}' |
| ] |
| |
| date_count = sum(1 for val in sample if any(re.match(p, val) for p in date_patterns)) |
| return date_count / len(sample) > 0.5 if len(sample) > 0 else False |
|
|
|
|
|
|
|
|
| |
| @st.cache_data(show_spinner=False, ttl=3600) |
| def get_column_types(df): |
| """Detect column types efficiently and cache the results.""" |
| column_types = {} |
| |
| |
| for chunk_start in range(0, len(df.columns), 10): |
| chunk_end = min(chunk_start + 10, len(df.columns)) |
| chunk_columns = df.columns[chunk_start:chunk_end] |
| |
| for column in chunk_columns: |
| |
| if pd.api.types.is_numeric_dtype(df[column]): |
| |
| if df[column].nunique() <= 2: |
| column_types[column] = "BINARY" |
| |
| elif df[column].nunique() < 20: |
| column_types[column] = "NUMERIC_DISCRETE" |
| |
| else: |
| column_types[column] = "NUMERIC_CONTINUOUS" |
| else: |
| |
| if is_potential_date_column(df[column]): |
| try: |
| |
| converted = pd.to_datetime(df[column], errors='coerce') |
| if not converted.isnull().all(): |
| column_types[column] = "TEMPORAL" |
| continue |
| except Exception: |
| pass |
| |
| |
| if (df[column].nunique() > len(df) * 0.9 and |
| any(x in column.lower() for x in ['id', 'code', 'key', 'uuid', 'identifier'])): |
| column_types[column] = "ID" |
| |
| elif df[column].nunique() <= 20: |
| column_types[column] = "CATEGORICAL" |
| |
| else: |
| column_types[column] = "TEXT" |
| |
| return column_types |
|
|
|
|
|
|
|
|
| |
| @st.cache_data(show_spinner=False, ttl=3600) |
| def get_corr_matrix(df): |
| """Compute and cache the correlation matrix for numeric columns.""" |
| |
| numeric_cols = df.select_dtypes(include=[np.number]).columns |
| |
| |
| if len(numeric_cols) > 30: |
| numeric_cols = numeric_cols[:30] |
| |
| |
| return df[numeric_cols].corr() if len(numeric_cols) > 1 else None |
|
|
|
|
|
|
|
|
|
|
| |
| @st.cache_data(show_spinner=False, ttl=3600) |
| def get_subsampled_data(df, column): |
| """Return subsampled data for faster visualization.""" |
| |
| if column not in df.columns: |
| return pd.DataFrame() |
| |
| |
| if df[column].nunique() < 20 and len(df) > SAMPLE_SIZE: |
| try: |
| |
| fractions = min(0.5, SAMPLE_SIZE / len(df)) |
| return df[[column]].groupby(column, group_keys=False).apply( |
| lambda x: x.sample(max(1, int(fractions * len(x))), random_state=42) |
| ) |
| except Exception: |
| |
| pass |
| |
| |
| return df[[column]].sample(min(len(df), SAMPLE_SIZE), random_state=42) |
|
|
|
|
|
|
|
|
| |
| @st.cache_data(show_spinner=False, ttl=1800, hash_funcs={ |
| pd.DataFrame: compute_df_hash, |
| pd.Series: lambda s: hash((s.name, compute_df_hash(s.to_frame()))) |
| }) |
| def create_chart(df, column, column_type): |
| """Generate optimized charts based on column type.""" |
| |
| if column not in df.columns: |
| return None |
| |
| |
| df_sample = get_subsampled_data(df, column) |
| if df_sample.empty: |
| return None |
| |
| try: |
| |
| if "year" in column.lower(): |
| fig = make_subplots(rows=1, cols=2, subplot_titles=("Year Distribution", "Box Plot"), |
| specs=[[{"type": "bar"}, {"type": "box"}]], column_widths=[0.7, 0.3], horizontal_spacing=0.1) |
| year_counts = df_sample[column].value_counts().sort_index() |
| fig.add_trace(go.Bar(x=year_counts.index, y=year_counts.values, marker_color='#7B68EE'), row=1, col=1) |
| fig.add_trace(go.Box(x=df_sample[column], marker_color='#7B68EE'), row=1, col=2) |
| |
| |
| elif column_type == "BINARY": |
| value_counts = df_sample[column].value_counts() |
| fig = make_subplots(rows=1, cols=2, |
| subplot_titles=("Distribution", "Percentage"), |
| specs=[[{"type": "bar"}, {"type": "pie"}]], |
| column_widths=[0.5, 0.5], |
| horizontal_spacing=0.1) |
| |
| fig.add_trace(go.Bar( |
| x=value_counts.index, |
| y=value_counts.values, |
| marker_color=['#FF4B4B', '#4CAF50'], |
| text=value_counts.values, |
| textposition='auto' |
| ), row=1, col=1) |
| |
| fig.add_trace(go.Pie( |
| labels=value_counts.index, |
| values=value_counts.values, |
| marker=dict(colors=['#FF4B4B', '#4CAF50']), |
| textinfo='percent+label' |
| ), row=1, col=2) |
| |
| fig.update_layout(title_text=f"Binary Distribution: {column}") |
| |
| |
| elif column_type == "NUMERIC_CONTINUOUS": |
| fig = make_subplots(rows=2, cols=2, |
| subplot_titles=("Distribution", "Box Plot", "Violin Plot", "Cumulative Distribution"), |
| specs=[[{"type": "histogram"}, {"type": "box"}], |
| [{"type": "violin"}, {"type": "scatter"}]], |
| vertical_spacing=0.15, |
| horizontal_spacing=0.1) |
| |
| |
| fig.add_trace(go.Histogram( |
| x=df_sample[column], |
| nbinsx=30, |
| marker_color='#FF4B4B', |
| opacity=0.7 |
| ), row=1, col=1) |
| |
| |
| fig.add_trace(go.Box( |
| x=df_sample[column], |
| marker_color='#FF4B4B', |
| boxpoints='outliers' |
| ), row=1, col=2) |
| |
| |
| fig.add_trace(go.Violin( |
| x=df_sample[column], |
| marker_color='#FF4B4B', |
| box_visible=True, |
| points='outliers' |
| ), row=2, col=1) |
| |
| |
| sorted_data = np.sort(df_sample[column].dropna()) |
| cumulative = np.arange(1, len(sorted_data) + 1) / len(sorted_data) |
| |
| fig.add_trace(go.Scatter( |
| x=sorted_data, |
| y=cumulative, |
| mode='lines', |
| line=dict(color='#FF4B4B', width=2) |
| ), row=2, col=2) |
| |
| fig.update_layout(height=600, title_text=f"Continuous Variable Analysis: {column}") |
| |
| |
| elif column_type == "NUMERIC_DISCRETE": |
| value_counts = df_sample[column].value_counts().sort_index() |
| fig = make_subplots(rows=1, cols=2, |
| subplot_titles=("Distribution", "Percentage"), |
| specs=[[{"type": "bar"}, {"type": "pie"}]], |
| column_widths=[0.7, 0.3], |
| horizontal_spacing=0.1) |
| |
| fig.add_trace(go.Bar( |
| x=value_counts.index, |
| y=value_counts.values, |
| marker_color='#FF4B4B', |
| text=value_counts.values, |
| textposition='auto' |
| ), row=1, col=1) |
| |
| fig.add_trace(go.Pie( |
| labels=value_counts.index, |
| values=value_counts.values, |
| marker=dict(colors=px.colors.sequential.Reds), |
| textinfo='percent+label' |
| ), row=1, col=2) |
| |
| fig.update_layout(title_text=f"Discrete Numeric Distribution: {column}") |
| |
| |
| elif column_type == "CATEGORICAL": |
| value_counts = df_sample[column].value_counts().head(20) |
| fig = make_subplots(rows=1, cols=2, |
| subplot_titles=("Category Distribution", "Percentage Breakdown"), |
| specs=[[{"type": "bar"}, {"type": "pie"}]], |
| column_widths=[0.6, 0.4], |
| horizontal_spacing=0.1) |
| |
| |
| fig.add_trace(go.Bar( |
| x=value_counts.index, |
| y=value_counts.values, |
| marker_color='#00FFA3', |
| text=value_counts.values, |
| textposition='auto' |
| ), row=1, col=1) |
| |
| |
| fig.add_trace(go.Pie( |
| labels=value_counts.index, |
| values=value_counts.values, |
| marker=dict(colors=px.colors.sequential.Greens), |
| textinfo='percent+label' |
| ), row=1, col=2) |
| |
| fig.update_layout(title_text=f"Categorical Analysis: {column}") |
| |
| |
| elif column_type == "TEMPORAL": |
| |
| dates = pd.to_datetime(df_sample[column], errors='coerce', format='mixed') |
| valid_dates = dates[dates.notna()] |
| |
| fig = make_subplots( |
| rows=2, |
| cols=2, |
| subplot_titles=("Monthly Pattern", "Yearly Pattern", "Cumulative Trend", "Day of Week Distribution"), |
| vertical_spacing=0.15, |
| horizontal_spacing=0.1, |
| specs=[[{"type": "bar"}, {"type": "bar"}], |
| [{"type": "scatter"}, {"type": "bar"}]] |
| ) |
| |
| |
| if not valid_dates.empty: |
| monthly_counts = valid_dates.dt.month.value_counts().sort_index() |
| month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] |
| month_labels = [month_names[i-1] for i in monthly_counts.index] |
| |
| fig.add_trace(go.Bar( |
| x=month_labels, |
| y=monthly_counts.values, |
| marker_color='#7B68EE', |
| text=monthly_counts.values, |
| textposition='auto' |
| ), row=1, col=1) |
| |
| |
| yearly_counts = valid_dates.dt.year.value_counts().sort_index() |
| |
| fig.add_trace(go.Bar( |
| x=yearly_counts.index, |
| y=yearly_counts.values, |
| marker_color='#7B68EE', |
| text=yearly_counts.values, |
| textposition='auto' |
| ), row=1, col=2) |
| |
| |
| sorted_dates = valid_dates.sort_values() |
| cumulative = np.arange(1, len(sorted_dates) + 1) |
| |
| fig.add_trace(go.Scatter( |
| x=sorted_dates, |
| y=cumulative, |
| mode='lines', |
| line=dict(color='#7B68EE', width=2) |
| ), row=2, col=1) |
| |
| |
| dow_counts = valid_dates.dt.dayofweek.value_counts().sort_index() |
| dow_names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] |
| dow_labels = [dow_names[i] for i in dow_counts.index] |
| |
| fig.add_trace(go.Bar( |
| x=dow_labels, |
| y=dow_counts.values, |
| marker_color='#7B68EE', |
| text=dow_counts.values, |
| textposition='auto' |
| ), row=2, col=2) |
| |
| fig.update_layout(height=600, title_text=f"Temporal Analysis: {column}") |
| |
| |
| elif column_type == "ID": |
| |
| id_lengths = df_sample[column].astype(str).str.len() |
| |
| |
| id_prefixes = df_sample[column].astype(str).str[:2].value_counts().head(15) |
| |
| fig = make_subplots( |
| rows=1, |
| cols=2, |
| subplot_titles=("ID Length Distribution", "Common ID Prefixes"), |
| horizontal_spacing=0.1, |
| specs=[[{"type": "histogram"}, {"type": "bar"}]] |
| ) |
| |
| |
| fig.add_trace(go.Histogram( |
| x=id_lengths, |
| nbinsx=20, |
| marker_color='#9C27B0' |
| ), row=1, col=1) |
| |
| |
| fig.add_trace(go.Bar( |
| x=id_prefixes.index, |
| y=id_prefixes.values, |
| marker_color='#9C27B0', |
| text=id_prefixes.values, |
| textposition='auto' |
| ), row=1, col=2) |
| |
| fig.update_layout(title_text=f"ID Analysis: {column}") |
| |
| |
| elif column_type == "TEXT": |
| |
| value_counts = df_sample[column].value_counts().head(15) |
| |
| |
| text_lengths = df_sample[column].astype(str).str.len() |
| |
| fig = make_subplots( |
| rows=2, |
| cols=1, |
| subplot_titles=("Top Values", "Text Length Distribution"), |
| vertical_spacing=0.2, |
| specs=[[{"type": "bar"}], [{"type": "histogram"}]] |
| ) |
| |
| |
| fig.add_trace( |
| go.Bar( |
| x=value_counts.index, |
| y=value_counts.values, |
| marker_color='#00B4D8', |
| text=value_counts.values, |
| textposition='auto' |
| ), |
| row=1, col=1 |
| ) |
| |
| |
| fig.add_trace( |
| go.Histogram( |
| x=text_lengths, |
| nbinsx=20, |
| marker_color='#00B4D8' |
| ), |
| row=2, col=1 |
| ) |
| |
| fig.update_layout( |
| height=600, |
| title_text=f"Text Analysis: {column}" |
| ) |
| |
| |
| else: |
| fig = go.Figure(go.Histogram(x=df_sample[column], marker_color='#888')) |
| fig.update_layout(title_text=f"Generic Analysis: {column}") |
|
|
| |
| fig.update_layout( |
| height=400, |
| showlegend=False, |
| plot_bgcolor='rgba(0,0,0,0)', |
| paper_bgcolor='rgba(0,0,0,0)', |
| font=dict(color='#FFFFFF'), |
| margin=dict(l=40, r=40, t=50, b=40) |
| ) |
| |
| return fig |
| |
| except Exception as e: |
| log_frontend_error("Chart Generation", f"Error creating chart for {column}: {str(e)}") |
| return None |
|
|
|
|
|
|
|
|
| def visualize_data(df): |
| """Automated dashboard with optimized visualizations.""" |
| if df is None or df.empty: |
| st.error("β No data available. Please upload and clean your data first.") |
| return |
|
|
| |
| df_hash = compute_df_hash(df) |
|
|
| |
| if "selected_viz_columns" not in st.session_state: |
| |
| initial_columns = list(df.columns[:min(4, len(df.columns))]) |
| st.session_state.selected_viz_columns = initial_columns |
| |
| |
| valid_columns = [col for col in st.session_state.selected_viz_columns if col in df.columns] |
| |
| |
| def on_column_selection_change(): |
| |
| st.session_state.selected_viz_columns = st.session_state.viz_column_selector |
| |
| st.session_state.current_tab_index = 2 |
| |
| |
| selected_columns = st.multiselect( |
| "Select columns to visualize", |
| options=df.columns, |
| default=valid_columns, |
| key="viz_column_selector", |
| on_change=on_column_selection_change |
| ) |
| |
| |
| |
| |
| |
| |
| recompute_needed = ( |
| "column_types" not in st.session_state or |
| "df_hash" not in st.session_state or |
| st.session_state.get("df_hash") != df_hash |
| ) |
| |
| if recompute_needed: |
| with st.spinner("π Analyzing data structure..."): |
| |
| st.session_state.column_types = get_column_types(df) |
| |
| st.session_state.corr_matrix = get_corr_matrix(df) |
| |
| st.session_state.df_hash = df_hash |
| |
| st.session_state.current_tab_index = 2 |
| |
| |
| if "test_results_calculated" in st.session_state: |
| st.session_state.test_results_calculated = False |
| |
| for key in ['test_metrics', 'test_y_pred', 'test_y_test', 'test_cm', 'sampling_message']: |
| if key in st.session_state: |
| del st.session_state[key] |
| |
| |
| column_types = st.session_state.column_types |
| corr_matrix = st.session_state.corr_matrix |
|
|
| if selected_columns: |
| |
| viz_container = st.container() |
| |
| with viz_container: |
| for idx in range(0, len(selected_columns), 2): |
| col1, col2 = st.columns(2) |
|
|
| for i, col in enumerate([col1, col2]): |
| if idx + i < len(selected_columns): |
| column = selected_columns[idx + i] |
| with col: |
| |
| chart_key = f"plot_{column.replace(' ', '_')}" |
| |
| |
| if column in column_types: |
| fig = create_chart(df, column, column_types[column]) |
| if fig: |
| st.plotly_chart(fig, use_container_width=True, key=chart_key) |
| with st.expander(f"π Summary Statistics - {column}", expanded=False): |
| if "NUMERIC" in column_types[column]: |
| st.dataframe(df[column].describe(), key=f"stats_{column.replace(' ', '_')}") |
| else: |
| st.dataframe(df[column].value_counts(), key=f"counts_{column.replace(' ', '_')}") |
| else: |
| st.warning(f"β οΈ Column '{column}' not found in the dataset or its type couldn't be determined.") |
|
|
| if corr_matrix is not None: |
| st.subheader("π Correlation Analysis") |
| fig = px.imshow(corr_matrix, title="Correlation Matrix", color_continuous_scale="RdBu") |
| st.plotly_chart(fig, use_container_width=True, key="corr_matrix_plot") |
| |
| else: |
| st.info("π Please select columns to visualize") |
|
|