Spaces:
Running
Running
Pavan Kumar Jonnakuti
Replace width='stretch' with use_container_width=True for backward compatibility
4c5b11a | # argo_dashboard.py | |
| # Full dashboard (Static map + Bar + Donut + Interactive map + DAC table) | |
| # - Static & Interactive map use: C:\Users\medagam INDU\Desktop\total dataset.csv | |
| # - Bar, Donut, Table use: C:\Users\medagam INDU\Desktop\bio_dataset.csv | |
| import streamlit as st | |
| import pandas as pd | |
| import plotly.express as px | |
| import matplotlib.pyplot as plt | |
| import cartopy.crs as ccrs | |
| import cartopy.feature as cfeature | |
| from datetime import datetime | |
| # ------------------------------ | |
| # UPDATE THESE PATHS if needed | |
| # ------------------------------ | |
| TOTAL_CSV = r"./total dataset.csv" | |
| BIO_CSV = r"./bio_dataset.csv" | |
| # ------------------------------ | |
| # Page setup | |
| # ------------------------------ | |
| st.set_page_config(page_title="Argo Float Dashboard", layout="wide") | |
| # Simple headings only (no extra notes) | |
| st.title("Argo Float Dashboard") | |
| # ------------------------------ | |
| # Helper: detect lat/lon column names | |
| # ------------------------------ | |
| def detect_latlon(df): | |
| lat = None | |
| lon = None | |
| for c in df.columns: | |
| cl = c.strip().lower() | |
| if cl in ("lat", "latitude") or cl.startswith("lat"): | |
| lat = c | |
| if cl in ("lon", "long", "longitude") or cl.startswith("lon") or cl.startswith("long"): | |
| lon = c | |
| return lat, lon | |
| # ------------------------------ | |
| # Load datasets (with caching) | |
| # ------------------------------ | |
| def load_total(): | |
| df = pd.read_csv(TOTAL_CSV, low_memory=False, encoding='latin1') | |
| return df | |
| def load_bio(): | |
| df = pd.read_csv(BIO_CSV, low_memory=False, encoding='latin1') | |
| return df | |
| total_df = load_total() | |
| bio_df = load_bio() | |
| # ------------------------------ | |
| # Prepare total dataset for static map: try find core/bio type or split | |
| # ------------------------------ | |
| def prepare_total_map(df, N_split=10): | |
| d = df.copy() | |
| # detect lat/lon | |
| lat_col, lon_col = detect_latlon(d) | |
| # find a type column if exists | |
| type_col = None | |
| for cand in ("Type", "FLOAT_TYPE", "Float_Type", "float_type", "TYPE"): | |
| if cand in d.columns: | |
| type_col = cand | |
| break | |
| if type_col is not None: | |
| core = d[d[type_col].astype(str).str.lower().str.contains("core", na=False)].copy() | |
| bio = d[d[type_col].astype(str).str.lower().str.contains("bio|bgc", na=False)].copy() | |
| if core.empty and bio.empty: | |
| core = d.copy() | |
| bio = d.iloc[0:0].copy() | |
| else: | |
| # fallback to Excel-like split (first N columns core, rest bio) | |
| try: | |
| core = d.iloc[:, :N_split].copy() | |
| bio = d.iloc[:, N_split:].copy() | |
| except Exception: | |
| core = d.copy() | |
| bio = d.iloc[0:0].copy() | |
| return core, bio, lat_col, lon_col | |
| core_map_df, bio_map_df, total_lat_col, total_lon_col = prepare_total_map(total_df, N_split=10) | |
| # Ensure we have lat/lon for total dataset (fallback common names) | |
| if total_lat_col is None or total_lon_col is None: | |
| for a,b in [("LATITUDE","LONGITUDE"), ("LAT","LONG"), ("Latitude","Longitude")]: | |
| if a in total_df.columns and b in total_df.columns: | |
| total_lat_col, total_lon_col = a,b | |
| break | |
| # ------------------------------ | |
| # Prepare bio dataset (dates, per-float aggregated) | |
| # ------------------------------ | |
| def prepare_bio(df): | |
| b = df.copy() | |
| # find birth and death columns | |
| birth_col = None | |
| death_col = None | |
| for c in ("DATE_UPDATE","DATE"): | |
| if c in b.columns: | |
| birth_col = c | |
| break | |
| for c in ("DATE_UPDATE.1","DATE.1","DATE_UPDATE1","DATE_UPDATE_1"): | |
| if c in b.columns: | |
| death_col = c | |
| break | |
| # fallback search any 'date' columns | |
| if birth_col is None: | |
| for c in b.columns: | |
| if 'date' in c.lower(): | |
| birth_col = c | |
| break | |
| # parse dates | |
| if birth_col is not None: | |
| b[birth_col] = pd.to_datetime(b[birth_col], errors='coerce', dayfirst=False) | |
| if death_col is not None: | |
| b[death_col] = pd.to_datetime(b[death_col], errors='coerce', dayfirst=False) | |
| else: | |
| b['DATE_UPDATE.1'] = pd.NaT | |
| death_col = 'DATE_UPDATE.1' | |
| # Year for bar chart | |
| if birth_col is not None: | |
| b['Year'] = b[birth_col].dt.year | |
| else: | |
| b['Year'] = pd.NA | |
| # id column | |
| id_col = 'WMOID' if 'WMOID' in b.columns else b.columns[0] | |
| # per-float aggregation: birth=min(birth_col), death=max(death_col) | |
| grouped = b.groupby(id_col).agg({ | |
| birth_col: 'min' if birth_col in b.columns else (lambda x: pd.NaT), | |
| death_col: 'max' if death_col in b.columns else (lambda x: pd.NaT), | |
| 'DAC': 'first' if 'DAC' in b.columns else (lambda x: None) | |
| }).reset_index().rename(columns={birth_col: 'birth', death_col: 'death'}) | |
| today = pd.Timestamp.today() | |
| grouped['end_date'] = grouped['death'].fillna(today) | |
| grouped['age_days'] = (grouped['end_date'] - grouped['birth']).dt.days | |
| # classify by 90 days | |
| grouped['status_90'] = grouped['age_days'].apply(lambda x: 'Live' if pd.notnull(x) and x >= 90 else 'Dead') | |
| return b, id_col, birth_col, death_col, grouped | |
| bio_prepared, bio_id_col, bio_birth_col, bio_death_col, bio_floats_grouped = prepare_bio(bio_df) | |
| # ------------------------------ | |
| # Sidebar: filters and search | |
| # ------------------------------ | |
| st.sidebar.header("Controls") | |
| # Year filter (bio) | |
| years_available = sorted([int(y) for y in bio_prepared['Year'].dropna().unique() if pd.notnull(y)]) | |
| selected_years = st.sidebar.multiselect("Year(s) for bar chart", years_available, default=years_available) | |
| # DAC filter | |
| dac_options = sorted(bio_prepared['DAC'].dropna().unique().astype(str)) if 'DAC' in bio_prepared.columns else [] | |
| selected_dacs = st.sidebar.multiselect("DAC(s)", dac_options, default=dac_options) | |
| # Search box | |
| search_text = st.sidebar.text_input("Search WMOID or DAC") | |
| # Apply filters to bio_prepared for bar/donut | |
| bio_filtered = bio_prepared.copy() | |
| if selected_years: | |
| bio_filtered = bio_filtered[bio_filtered['Year'].isin(selected_years)] | |
| if selected_dacs: | |
| bio_filtered = bio_filtered[bio_filtered['DAC'].astype(str).isin(selected_dacs)] | |
| if search_text and search_text.strip(): | |
| bio_filtered = bio_filtered[ | |
| bio_filtered['WMOID'].astype(str).str.contains(search_text, case=False, na=False) | | |
| bio_filtered['DAC'].astype(str).str.contains(search_text, case=False, na=False) | |
| ] | |
| # Also filter grouped_for_table (per-float) by DAC/search | |
| grouped_for_table = bio_floats_grouped.copy() | |
| if selected_dacs: | |
| grouped_for_table = grouped_for_table[grouped_for_table['DAC'].astype(str).isin(selected_dacs)] | |
| if search_text and search_text.strip(): | |
| grouped_for_table = grouped_for_table[ | |
| grouped_for_table[bio_id_col].astype(str).str.contains(search_text, case=False, na=False) | | |
| grouped_for_table['DAC'].astype(str).str.contains(search_text, case=False, na=False) | |
| ] | |
| # ------------------------------ | |
| # Static Map (top) — core pink, bio blue (use total dataset) | |
| # ------------------------------ | |
| st.subheader("Static Map") | |
| plt.figure(figsize=(14,7)) | |
| ax = plt.axes(projection=ccrs.PlateCarree()) | |
| ax.add_feature(cfeature.LAND, facecolor='lightgray') | |
| ax.add_feature(cfeature.COASTLINE) | |
| ax.add_feature(cfeature.BORDERS, linestyle=':') | |
| # detect lat/lon in core_map_df and bio_map_df returned earlier from total split | |
| core_lat, core_lon = detect_latlon(core_map_df) | |
| bio_lat, bio_lon = detect_latlon(bio_map_df) | |
| # fallback common names | |
| if core_lat is None or core_lon is None: | |
| for a,b in (("LATITUDE","LONGITUDE"), ("LAT","LONG"), ("Latitude","Longitude")): | |
| if a in core_map_df.columns and b in core_map_df.columns: | |
| core_lat, core_lon = a,b | |
| break | |
| if bio_lat is None or bio_lon is None: | |
| for a,b in (("LATITUDE","LONGITUDE"), ("LAT","LONG"), ("Latitude","Longitude")): | |
| if a in bio_map_df.columns and b in bio_map_df.columns: | |
| bio_lat, bio_lon = a,b | |
| break | |
| # plot if available | |
| if core_lat and core_lon and (core_lat in core_map_df.columns) and (core_lon in core_map_df.columns): | |
| ax.scatter(core_map_df[core_lon], core_map_df[core_lat], color='pink', s=15, alpha=0.7, label='Core') | |
| if bio_lat and bio_lon and (bio_lat in bio_map_df.columns) and (bio_lon in bio_map_df.columns): | |
| ax.scatter(bio_map_df[bio_lon], bio_map_df[bio_lat], color='blue', s=15, alpha=0.7, label='Bio') | |
| plt.title("Core (pink) vs Bio (blue) — Map") | |
| plt.legend(loc='upper right') | |
| st.pyplot(plt.gcf()) | |
| # ------------------------------ | |
| # Middle row: Bar chart (left) and Donut chart (right) — both from bio dataset | |
| # ------------------------------ | |
| col1, col2 = st.columns(2) | |
| # Bar chart: number of unique floats per Year and DAC | |
| with col1: | |
| st.subheader("Bar Chart") | |
| id_col = bio_id_col | |
| df_grouped = bio_filtered.groupby(['Year','DAC'])[id_col].nunique().reset_index(name='Float_Count') | |
| totals = df_grouped.groupby('Year')['Float_Count'].sum().reset_index(name='Total_Floats') | |
| fig_bar = px.bar( | |
| df_grouped, | |
| x='Year', | |
| y='Float_Count', | |
| color='DAC', | |
| text='Float_Count', | |
| title='Number of Floats per DAC' | |
| ) | |
| for _, r in totals.iterrows(): | |
| fig_bar.add_annotation(x=r['Year'], y=r['Total_Floats'], text=str(int(r['Total_Floats'])), showarrow=False, yshift=10) | |
| fig_bar.update_layout(barmode='stack', xaxis=dict(dtick=1), height=520) | |
| st.plotly_chart(fig_bar, use_container_width=True) | |
| # Donut chart: age distribution of alive floats (bio dataset) | |
| with col2: | |
| st.subheader("Donut Chart") | |
| g = bio_floats_grouped.copy() | |
| # alive = death is NaT | |
| alive = g[g['death'].isna()].copy() | |
| if not alive.empty: | |
| alive['age_years'] = ((pd.Timestamp.today() - alive['birth']).dt.days // 365).astype('Int64') | |
| alive = alive[alive['age_years'].notna() & (alive['age_years'] >= 0)] | |
| age_counts = alive['age_years'].value_counts().sort_index().reset_index() | |
| age_counts.columns = ['Age_Years','Count'] | |
| if not age_counts.empty: | |
| fig_donut = px.pie(age_counts, names='Age_Years', values='Count', hole=0.55, title='Age (years) distribution of alive floats') | |
| fig_donut.update_traces(textinfo='percent+label') | |
| st.plotly_chart(fig_donut, use_container_width=True) | |
| else: | |
| st.write("No alive float age groups available for selected filters.") | |
| else: | |
| st.write("No alive floats found for selected filters.") | |
| # ------------------------------ | |
| # Interactive Plotly Map (uses total dataset) | |
| # ------------------------------ | |
| # ------------------------------ | |
| # Interactive Plotly Map (ALL DACs always visible) | |
| # ------------------------------ | |
| st.subheader("Interactive Plotly Map (All DACs)") | |
| # copy full total dataset (no DAC filtering) | |
| map_df = total_df.copy() | |
| # detect lat/lon | |
| lat_t, lon_t = detect_latlon(map_df) | |
| if lat_t is None and "LATITUDE" in map_df.columns: | |
| lat_t = "LATITUDE" | |
| if lon_t is None and "LONGITUDE" in map_df.columns: | |
| lon_t = "LONGITUDE" | |
| if lat_t is None and "LAT" in map_df.columns: | |
| lat_t = "LAT" | |
| if lon_t is None and "LONG" in map_df.columns: | |
| lon_t = "LONG" | |
| if lat_t is None or lon_t is None: | |
| st.warning("Latitude/Longitude not found in total dataset for interactive map.") | |
| else: | |
| # Convert dates | |
| if 'DATE_UPDATE' in map_df.columns: | |
| map_df['DATE_UPDATE'] = pd.to_datetime(map_df['DATE_UPDATE'], errors='coerce') | |
| if 'DATE_UPDATE.1' in map_df.columns: | |
| map_df['DATE_UPDATE.1'] = pd.to_datetime(map_df['DATE_UPDATE.1'], errors='coerce') | |
| # Compute status (90-day rule) | |
| if 'DATE_UPDATE' in map_df.columns: | |
| map_df['end_date'] = map_df['DATE_UPDATE.1'].fillna(pd.Timestamp.today()) \ | |
| if 'DATE_UPDATE.1' in map_df.columns else pd.Timestamp.today() | |
| map_df['age_days'] = (map_df['end_date'] - map_df['DATE_UPDATE']).dt.days | |
| map_df['Status'] = map_df['age_days'].apply( | |
| lambda x: 'Live' if pd.notnull(x) and x >= 90 else 'Dead' | |
| ) | |
| else: | |
| map_df['Status'] = 'Unknown' | |
| # Hover information | |
| hover_data = { | |
| lat_t: True, | |
| lon_t: True, | |
| "Status": True | |
| } | |
| if "WMOID" in map_df.columns: | |
| hover_data["WMOID"] = True | |
| if "DAC" in map_df.columns: | |
| hover_data["DAC"] = True | |
| if "DATE_UPDATE" in map_df.columns: | |
| hover_data["DATE_UPDATE"] = True | |
| if "DATE_UPDATE.1" in map_df.columns: | |
| hover_data["DATE_UPDATE.1"] = True | |
| # Plot ALL DACs (no filters) | |
| fig_map = px.scatter_geo( | |
| map_df, | |
| lat=lat_t, | |
| lon=lon_t, | |
| color="DAC" if "DAC" in map_df.columns else None, | |
| hover_name="WMOID" if "WMOID" in map_df.columns else None, | |
| hover_data=hover_data, | |
| title="Interactive Map — All DACs", | |
| projection="natural earth" | |
| ) | |
| fig_map.update_layout(height=650) | |
| st.plotly_chart(fig_map, use_container_width=True) | |
| # ------------------------------ | |
| # DAC summary table (bio dataset) - bottom | |
| # Live = age_days >= 90, Dead = age_days < 90, Total = unique floats | |
| # ------------------------------ | |
| st.subheader("DAC Table") | |
| g = grouped_for_table.copy() | |
| # ensure age_days already present (end_date computed earlier) | |
| g['age_days'] = (g['end_date'] - g['birth']).dt.days | |
| g['Live90'] = g['age_days'].apply(lambda x: 1 if pd.notnull(x) and x >= 90 else 0) | |
| g['Dead90'] = g['age_days'].apply(lambda x: 1 if pd.notnull(x) and x < 90 else 0) | |
| summary_table = g.groupby('DAC').agg( | |
| Live=('Live90','sum'), | |
| Dead=('Dead90','sum'), | |
| Total=(bio_id_col, 'nunique') | |
| ).reset_index() | |
| # present in requested order: DAC | Live | Dead | Total | |
| summary_table = summary_table[['DAC','Live','Dead','Total']] | |
| st.dataframe(summary_table, use_container_width=True) | |
| # Compact text lines | |
| st.markdown("**Compact summary (DAC — Live / Dead / Total)**") | |
| for _, row in summary_table.iterrows(): | |
| st.write(f"{row['DAC']} — {int(row['Live'])} / {int(row['Dead'])} / {int(row['Total'])}") | |