Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
# app.py β Streamlit radar charts from MongoDB (scores 0β1)
|
| 2 |
import os
|
| 3 |
-
import json
|
| 4 |
from datetime import date
|
| 5 |
from typing import Dict, List
|
| 6 |
|
|
@@ -52,24 +51,13 @@ def safe_mean(vals):
|
|
| 52 |
vals = [v for v in vals if v is not None]
|
| 53 |
return float(np.mean(vals)) if vals else 0.0
|
| 54 |
|
| 55 |
-
|
| 56 |
def to_frame(records: List[dict]) -> pd.DataFrame:
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
df = pd.DataFrame(records)
|
| 60 |
-
# Expand skills into columns in SKILLS order
|
| 61 |
-
skill_df = pd.json_normalize(df.get("skills", {})).reindex(columns=SKILLS)
|
| 62 |
-
for k in SKILLS:
|
| 63 |
-
if k not in skill_df:
|
| 64 |
-
skill_df[k] = 0.0
|
| 65 |
-
df = pd.concat([df.drop(columns=["skills"], errors="ignore"), skill_df], axis=1)
|
| 66 |
-
return df
|
| 67 |
-
|
| 68 |
|
| 69 |
def aggregate_groups_row(row: pd.Series) -> Dict[str, float]:
|
| 70 |
return {g: safe_mean([float(row.get(s, 0.0)) for s in members]) for g, members in SKILL_GROUPS.items()}
|
| 71 |
|
| 72 |
-
|
| 73 |
def summarize(records: List[dict], level: str = "student") -> pd.DataFrame:
|
| 74 |
"""Average per label over SKILLS; level in {student, student+source}."""
|
| 75 |
df = to_frame(records)
|
|
@@ -79,8 +67,11 @@ def summarize(records: List[dict], level: str = "student") -> pd.DataFrame:
|
|
| 79 |
df["label"] = df["student"].astype(str) + " β " + df["source"].astype(str)
|
| 80 |
else:
|
| 81 |
df["label"] = df["student"].astype(str)
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
def plot_radar(df: pd.DataFrame, grouped: bool, title: str):
|
| 86 |
if df.empty:
|
|
@@ -123,10 +114,8 @@ def plot_radar(df: pd.DataFrame, grouped: bool, title: str):
|
|
| 123 |
)
|
| 124 |
return fig
|
| 125 |
|
| 126 |
-
|
| 127 |
# ------------------- Mongo Access (secrets-only) -------------------
|
| 128 |
def _get_secret(name: str) -> str | None:
|
| 129 |
-
# Prefer Streamlit secrets, fallback to env vars
|
| 130 |
try:
|
| 131 |
val = st.secrets.get(name)
|
| 132 |
if val is not None:
|
|
@@ -135,7 +124,6 @@ def _get_secret(name: str) -> str | None:
|
|
| 135 |
pass
|
| 136 |
return os.getenv(name)
|
| 137 |
|
| 138 |
-
|
| 139 |
def _build_uri(db_name: str | None) -> str | None:
|
| 140 |
user = _get_secret("MONGO_USER")
|
| 141 |
pw = _get_secret("MONGO_PASS")
|
|
@@ -145,19 +133,15 @@ def _build_uri(db_name: str | None) -> str | None:
|
|
| 145 |
user_q = quote_plus(user)
|
| 146 |
pw_q = quote_plus(pw)
|
| 147 |
db_path = f"/{db_name}" if db_name else ""
|
| 148 |
-
# TLS flags help on some hosts; SRV requires dnspython (present on Streamlit Cloud)
|
| 149 |
return (
|
| 150 |
f"mongodb+srv://{user_q}:{pw_q}@{cluster}{db_path}"
|
| 151 |
f"?retryWrites=true&w=majority&tls=true&tlsAllowInvalidCertificates=true"
|
| 152 |
)
|
| 153 |
|
| 154 |
-
|
| 155 |
@st.cache_resource(show_spinner=False)
|
| 156 |
def _client(uri: str):
|
| 157 |
-
# Use a cached client resource (not cache_data) for connections
|
| 158 |
return MongoClient(uri, serverSelectionTimeoutMS=10000)
|
| 159 |
|
| 160 |
-
|
| 161 |
@st.cache_data(show_spinner=False)
|
| 162 |
def mongo_distinct(uri: str, db: str, coll: str, field: str) -> List[str]:
|
| 163 |
if not uri:
|
|
@@ -169,7 +153,6 @@ def mongo_distinct(uri: str, db: str, coll: str, field: str) -> List[str]:
|
|
| 169 |
except Exception:
|
| 170 |
return []
|
| 171 |
|
| 172 |
-
|
| 173 |
@st.cache_data(show_spinner=False)
|
| 174 |
def mongo_records(
|
| 175 |
uri: str,
|
|
@@ -180,6 +163,7 @@ def mongo_records(
|
|
| 180 |
start: str | None,
|
| 181 |
end: str | None,
|
| 182 |
) -> List[dict]:
|
|
|
|
| 183 |
if not uri:
|
| 184 |
return []
|
| 185 |
q = {}
|
|
@@ -196,25 +180,33 @@ def mongo_records(
|
|
| 196 |
try:
|
| 197 |
c = _client(uri)
|
| 198 |
proj = {"_id": 0, "student": 1, "source": 1, "date": 1, "skills": 1}
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
for
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
except Exception:
|
| 206 |
return []
|
| 207 |
|
| 208 |
-
|
| 209 |
# ------------------- UI -------------------
|
| 210 |
-
st.title("π Student Skill Radar")
|
| 211 |
|
| 212 |
with st.sidebar:
|
| 213 |
st.subheader("MongoDB Settings")
|
| 214 |
db_name = st.text_input("Database name", value="student_skills")
|
| 215 |
coll_name = st.text_input("Collection name", value="responses_IFE_2025")
|
| 216 |
|
| 217 |
-
# Build from MONGO_USER / MONGO_PASS / MONGO_CLUSTER only
|
| 218 |
mongo_uri = _build_uri(db_name)
|
| 219 |
|
| 220 |
if not mongo_uri:
|
|
@@ -261,9 +253,11 @@ with right:
|
|
| 261 |
if df.empty:
|
| 262 |
st.info("No data. Adjust filters or check Mongo connection.")
|
| 263 |
else:
|
| 264 |
-
|
| 265 |
csv = df.to_csv(index=False).encode("utf-8")
|
| 266 |
st.download_button("Download CSV", data=csv, file_name="skill_scores.csv", mime="text/csv")
|
|
|
|
|
|
|
| 267 |
|
| 268 |
|
| 269 |
# # app.py
|
|
|
|
| 1 |
# app.py β Streamlit radar charts from MongoDB (scores 0β1)
|
| 2 |
import os
|
|
|
|
| 3 |
from datetime import date
|
| 4 |
from typing import Dict, List
|
| 5 |
|
|
|
|
| 51 |
vals = [v for v in vals if v is not None]
|
| 52 |
return float(np.mean(vals)) if vals else 0.0
|
| 53 |
|
|
|
|
| 54 |
def to_frame(records: List[dict]) -> pd.DataFrame:
|
| 55 |
+
# Records are returned already flattened with skill columns
|
| 56 |
+
return pd.DataFrame(records) if records else pd.DataFrame()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
def aggregate_groups_row(row: pd.Series) -> Dict[str, float]:
|
| 59 |
return {g: safe_mean([float(row.get(s, 0.0)) for s in members]) for g, members in SKILL_GROUPS.items()}
|
| 60 |
|
|
|
|
| 61 |
def summarize(records: List[dict], level: str = "student") -> pd.DataFrame:
|
| 62 |
"""Average per label over SKILLS; level in {student, student+source}."""
|
| 63 |
df = to_frame(records)
|
|
|
|
| 67 |
df["label"] = df["student"].astype(str) + " β " + df["source"].astype(str)
|
| 68 |
else:
|
| 69 |
df["label"] = df["student"].astype(str)
|
| 70 |
+
# ensure missing skill columns exist (safety)
|
| 71 |
+
for k in SKILLS:
|
| 72 |
+
if k not in df:
|
| 73 |
+
df[k] = 0.0
|
| 74 |
+
return df.groupby("label", dropna=False)[SKILLS].mean(numeric_only=True).reset_index()
|
| 75 |
|
| 76 |
def plot_radar(df: pd.DataFrame, grouped: bool, title: str):
|
| 77 |
if df.empty:
|
|
|
|
| 114 |
)
|
| 115 |
return fig
|
| 116 |
|
|
|
|
| 117 |
# ------------------- Mongo Access (secrets-only) -------------------
|
| 118 |
def _get_secret(name: str) -> str | None:
|
|
|
|
| 119 |
try:
|
| 120 |
val = st.secrets.get(name)
|
| 121 |
if val is not None:
|
|
|
|
| 124 |
pass
|
| 125 |
return os.getenv(name)
|
| 126 |
|
|
|
|
| 127 |
def _build_uri(db_name: str | None) -> str | None:
|
| 128 |
user = _get_secret("MONGO_USER")
|
| 129 |
pw = _get_secret("MONGO_PASS")
|
|
|
|
| 133 |
user_q = quote_plus(user)
|
| 134 |
pw_q = quote_plus(pw)
|
| 135 |
db_path = f"/{db_name}" if db_name else ""
|
|
|
|
| 136 |
return (
|
| 137 |
f"mongodb+srv://{user_q}:{pw_q}@{cluster}{db_path}"
|
| 138 |
f"?retryWrites=true&w=majority&tls=true&tlsAllowInvalidCertificates=true"
|
| 139 |
)
|
| 140 |
|
|
|
|
| 141 |
@st.cache_resource(show_spinner=False)
|
| 142 |
def _client(uri: str):
|
|
|
|
| 143 |
return MongoClient(uri, serverSelectionTimeoutMS=10000)
|
| 144 |
|
|
|
|
| 145 |
@st.cache_data(show_spinner=False)
|
| 146 |
def mongo_distinct(uri: str, db: str, coll: str, field: str) -> List[str]:
|
| 147 |
if not uri:
|
|
|
|
| 153 |
except Exception:
|
| 154 |
return []
|
| 155 |
|
|
|
|
| 156 |
@st.cache_data(show_spinner=False)
|
| 157 |
def mongo_records(
|
| 158 |
uri: str,
|
|
|
|
| 163 |
start: str | None,
|
| 164 |
end: str | None,
|
| 165 |
) -> List[dict]:
|
| 166 |
+
"""Return FLAT rows: student, source, date, and one column per skill (0β1 floats)."""
|
| 167 |
if not uri:
|
| 168 |
return []
|
| 169 |
q = {}
|
|
|
|
| 180 |
try:
|
| 181 |
c = _client(uri)
|
| 182 |
proj = {"_id": 0, "student": 1, "source": 1, "date": 1, "skills": 1}
|
| 183 |
+
docs = list(c[db][coll].find(q, proj))
|
| 184 |
+
rows = []
|
| 185 |
+
for d in docs:
|
| 186 |
+
base = {
|
| 187 |
+
"student": str(d.get("student", "")),
|
| 188 |
+
"source": str(d.get("source", "")),
|
| 189 |
+
"date": str(d.get("date", "")),
|
| 190 |
+
}
|
| 191 |
+
sd = d.get("skills") or {}
|
| 192 |
+
for k in SKILLS:
|
| 193 |
+
try:
|
| 194 |
+
base[k] = float(sd.get(k, 0.0))
|
| 195 |
+
except Exception:
|
| 196 |
+
base[k] = 0.0
|
| 197 |
+
rows.append(base)
|
| 198 |
+
return rows
|
| 199 |
except Exception:
|
| 200 |
return []
|
| 201 |
|
|
|
|
| 202 |
# ------------------- UI -------------------
|
| 203 |
+
st.title("π Student Skill Radar β MongoDB (secrets-based)")
|
| 204 |
|
| 205 |
with st.sidebar:
|
| 206 |
st.subheader("MongoDB Settings")
|
| 207 |
db_name = st.text_input("Database name", value="student_skills")
|
| 208 |
coll_name = st.text_input("Collection name", value="responses_IFE_2025")
|
| 209 |
|
|
|
|
| 210 |
mongo_uri = _build_uri(db_name)
|
| 211 |
|
| 212 |
if not mongo_uri:
|
|
|
|
| 253 |
if df.empty:
|
| 254 |
st.info("No data. Adjust filters or check Mongo connection.")
|
| 255 |
else:
|
| 256 |
+
# Removed the table per your request
|
| 257 |
csv = df.to_csv(index=False).encode("utf-8")
|
| 258 |
st.download_button("Download CSV", data=csv, file_name="skill_scores.csv", mime="text/csv")
|
| 259 |
+
st.caption(f"{len(df)} line(s) aggregated.")
|
| 260 |
+
|
| 261 |
|
| 262 |
|
| 263 |
# # app.py
|