Argolytics / Code /argo_dashboard.py
Pavan Kumar Jonnakuti
Replace width='stretch' with use_container_width=True for backward compatibility
4c5b11a
Raw
History Blame Contribute Delete
14.4 kB
# 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)
# ------------------------------
@st.cache_data
def load_total():
df = pd.read_csv(TOTAL_CSV, low_memory=False, encoding='latin1')
return df
@st.cache_data
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'])}")