|
|
import os |
|
|
import sys |
|
|
|
|
|
current_dir = os.path.dirname(os.path.abspath(__file__)) |
|
|
parent_dir = os.path.dirname(current_dir) |
|
|
sys.path.append(parent_dir) |
|
|
|
|
|
import streamlit as st |
|
|
import numpy as np |
|
|
import pandas as pd |
|
|
import tensorflow as tf |
|
|
import pickle |
|
|
from utils.data_helper import * |
|
|
from environment import raw_world as Env |
|
|
from utils import irl_helper |
|
|
from utils.trajectory_comparison import analyze_chapter_engagement |
|
|
from typing import List, Dict, Union, Tuple |
|
|
import plotly.graph_objects as go |
|
|
st.set_page_config(layout="wide") |
|
|
from utils.evaluation import * |
|
|
from utils.rnn_models import * |
|
|
from PIL import Image |
|
|
import re |
|
|
|
|
|
def performance_prediction(model, x_test): |
|
|
y_pred = model.predict(x_test) |
|
|
y_pred = np.array([1 if y[0] >= 0.5 else 0 for y in y_pred]) |
|
|
num_fail = sum(y_pred) |
|
|
num_pass = len(y_pred) - num_fail |
|
|
return num_fail, num_pass, y_pred |
|
|
|
|
|
|
|
|
def extract_signal_name_regex(input_string): |
|
|
match = re.match(r"^\d+\.\d\s*-\s*(.*?)\s*\[\d{1,2}:\d{2}\]$", input_string) |
|
|
if match: |
|
|
return match.group(1).strip() |
|
|
return input_string |
|
|
|
|
|
def plot_performance(df=None, show_image=False, show_performance=False, col_image=None, col_performance=None): |
|
|
if show_image and col_image is not None: |
|
|
with col_image: |
|
|
fig = Image.open('streamlit-assets/whatif_results_week_6.png') |
|
|
st.image(fig, caption="Predicted effectiveness of interventions in week 6. \ |
|
|
For example, adding content from topic 7 might improve students' performance, \ |
|
|
while adding content from week 2 might harm", use_container_width=True) |
|
|
elif show_image and col_image is None: |
|
|
fig = Image.open('streamlit-assets/whatif_results_week_6.png') |
|
|
st.image(fig, caption="Predicted effectiveness of interventions in week 6. \ |
|
|
For example, adding content from topic 7 might improve students' performance, \ |
|
|
while adding content from week 2 might harm", use_container_width=False) |
|
|
|
|
|
if show_performance and df is not None and col_performance is not None: |
|
|
with col_performance: |
|
|
fig = go.Figure() |
|
|
bar_height_px = 250 |
|
|
total_height = bar_height_px * len(df) |
|
|
fig.add_trace(go.Bar( |
|
|
y=df['class'], |
|
|
x=df['fail_percent'], |
|
|
name='Fail', |
|
|
orientation='h', |
|
|
text=[f'{x:.1f}%' for x in df['fail_percent']], |
|
|
textfont=dict(size=25), |
|
|
textposition='auto', |
|
|
customdata=df[['fail_num']], |
|
|
hovertemplate='%{y}<br> No. Students: %{customdata[0]}<extra></extra>', |
|
|
marker=dict(color='#c0392b') |
|
|
)) |
|
|
|
|
|
fig.add_trace(go.Bar( |
|
|
y=df['class'], |
|
|
x=df['pass_percent'], |
|
|
name='Pass', |
|
|
orientation='h', |
|
|
text=[f'{x:.1f}%' for x in df['pass_percent']], |
|
|
textfont=dict(size=25), |
|
|
textposition='auto', |
|
|
customdata=df[['pass_num']], |
|
|
hovertemplate='%{y}<br> No. Students: %{customdata[0]}<extra></extra>', |
|
|
marker=dict(color='#27ae60') |
|
|
)) |
|
|
|
|
|
fig.update_layout( |
|
|
barmode='stack', |
|
|
xaxis=dict(title='Percentage', range=[0, 100]), |
|
|
legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1), |
|
|
height=total_height, |
|
|
margin=dict(l=200, r=40, t=120, b=60), |
|
|
) |
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
|
|
|
def compare_chapter_engagement(syn_trajectories: Union[List, np.ndarray, pd.Series], |
|
|
real_trajectories: Union[List, np.ndarray, pd.Series], |
|
|
world, |
|
|
save_dir=None) -> Tuple[Dict, Dict]: |
|
|
""" |
|
|
Compare engagement metrics between real and synthetic students per chapter |
|
|
|
|
|
Parameters: |
|
|
----------- |
|
|
real_trajectories : Union[List, np.ndarray, pd.Series] |
|
|
Real student trajectories |
|
|
syn_trajectories : Union[List, np.ndarray, pd.Series] |
|
|
Synthetic student trajectories |
|
|
world : ClickstreamWorld |
|
|
World object containing state and action information |
|
|
save_dir : str, optional |
|
|
Directory to save visualizations |
|
|
|
|
|
Returns: |
|
|
-------- |
|
|
Tuple[Dict, Dict]: Real and synthetic engagement metrics |
|
|
""" |
|
|
st.header("Engagement Analysis: Before vs. After Intervention") |
|
|
|
|
|
real_metrics = analyze_chapter_engagement(real_trajectories, world, "Real") |
|
|
|
|
|
syn_metrics = analyze_chapter_engagement(syn_trajectories, world, "Synthetic") |
|
|
|
|
|
chapter_id_to_name = st.session_state.metadata.set_index('Topic')['Skills'].to_dict() |
|
|
metrics_to_plot = [ |
|
|
('visit_count', 'Total Visits'), |
|
|
('completion_rate', 'Completion Rate (%)'), |
|
|
('problem_attempts', 'Quiz Attempts'), |
|
|
('video_views', 'Video Views') |
|
|
] |
|
|
increase_color = '#27ae60' |
|
|
decrease_color = '#c0392b' |
|
|
neutral_color = '#bdc3c7' |
|
|
plotly_figures = [] |
|
|
|
|
|
for idx, (metric, ylabel) in enumerate(metrics_to_plot): |
|
|
chapters = sorted(set(real_metrics.keys()) | set(syn_metrics.keys())) |
|
|
chapters_name = [chapter_id_to_name.get(ch, f'Chapter {ch}') for ch in chapters] |
|
|
real_values = [real_metrics.get(ch, {}).get(metric, 0) for ch in chapters] |
|
|
syn_values = [syn_metrics.get(ch, {}).get(metric, 0) for ch in chapters] |
|
|
pct_diffs = [] |
|
|
colors = [] |
|
|
|
|
|
for real_val, syn_val in zip(real_values, syn_values): |
|
|
|
|
|
if metric == 'completion_rate': |
|
|
|
|
|
pct_diff = syn_val - real_val |
|
|
colors.append(increase_color if pct_diff > 0 else decrease_color) |
|
|
else: |
|
|
if real_val > 0: |
|
|
pct_diff = ((syn_val - real_val) / real_val) * 100 |
|
|
colors.append(increase_color if pct_diff > 0 else decrease_color) |
|
|
else: |
|
|
|
|
|
|
|
|
pct_diff = 0 |
|
|
colors.append(increase_color if syn_val > 0 else neutral_color) |
|
|
pct_diffs.append(pct_diff) |
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
fig.add_trace( |
|
|
go.Bar( |
|
|
x=chapters, |
|
|
y=pct_diffs, |
|
|
marker=dict(color=colors, line=dict(color='#2c3e50', width=0.5)), |
|
|
name=ylabel, |
|
|
showlegend=False, |
|
|
text=[f'{val:+.1f}%' for val in pct_diffs], |
|
|
textposition='auto', |
|
|
hovertemplate='%{x}<br>%{y:.1f}%<extra></extra>', |
|
|
width=0.6 |
|
|
) |
|
|
) |
|
|
|
|
|
fig.update_layout( |
|
|
title=dict( |
|
|
text=ylabel, |
|
|
x=0.5, |
|
|
xanchor='center', |
|
|
font=dict(size=20) |
|
|
), |
|
|
font=dict(size=14), |
|
|
margin=dict(l=50, r=40, t=60, b=50), |
|
|
hovermode="x unified", |
|
|
bargap=0.25, |
|
|
template='plotly' |
|
|
) |
|
|
|
|
|
fig.update_xaxes( |
|
|
title_text='Topic', |
|
|
title_font=dict(size=12), |
|
|
tickvals=chapters, |
|
|
ticktext=[f'{ch}' for ch in chapters_name], |
|
|
tickfont=dict(size=13), |
|
|
showgrid=False, |
|
|
zeroline=False |
|
|
) |
|
|
|
|
|
fig.update_yaxes( |
|
|
title_text='Change (%)', |
|
|
title_font=dict(size=16), |
|
|
tickfont=dict(size=13), |
|
|
zeroline=True, |
|
|
zerolinecolor='white', |
|
|
zerolinewidth=1.5, |
|
|
showgrid=True, |
|
|
) |
|
|
|
|
|
|
|
|
plotly_figures.append(fig) |
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
with col1: |
|
|
st.caption("Total Visits: Number of interactions made by students for every course material group by Topic") |
|
|
st.plotly_chart(plotly_figures[0], use_container_width=True) |
|
|
with col2: |
|
|
st.caption("""Completion Rate: The percentage of course materials that students interacted with within each topic. A value of 100% means the student engaged with every material in that topic at least once.\\ |
|
|
For example: if there are 5 course material in a topic, the completion rate would be 80% if they interacted with 4 unique materials.""") |
|
|
st.plotly_chart(plotly_figures[1], use_container_width=True) |
|
|
|
|
|
col3, col4 = st.columns(2) |
|
|
with col3: |
|
|
st.caption("Quiz Attempts: Number of quiz attempts made by students for every quiz group by Topic") |
|
|
st.plotly_chart(plotly_figures[2], use_container_width=True) |
|
|
with col4: |
|
|
st.caption("Video Views: Number of video views made by students for every video group by Topic") |
|
|
st.plotly_chart(plotly_figures[3], use_container_width=True) |
|
|
|
|
|
def add_new_state(world, trajectories, event_data, week_list=range(7,11), test_ids=None, |
|
|
OUTPUT_DIR='streamlit-assets/dsp-004', trajectories_each_week=None, trajectories_each_week_pass=None, trajectories_each_week_fail=None, |
|
|
history_whatif_pass=None, history_whatif_fail=None, fail_only=False): |
|
|
|
|
|
print("Adding new state with parameters:", event_data) |
|
|
|
|
|
results = { |
|
|
'week': [], |
|
|
'real_trajectories': [], |
|
|
'syn_trajectories': [], |
|
|
'world': [] |
|
|
} |
|
|
|
|
|
for week in week_list: |
|
|
print(f"\nProcessing week {week}") |
|
|
world._reset_transition_prob_table() |
|
|
world._update_transition_prob_table(trajectories) |
|
|
|
|
|
|
|
|
chapter = event_data['chapter'] |
|
|
is_problem = True if event_data['event_type'] in 'quiz' else False |
|
|
value = float(event_data['difficulty']) if is_problem else float(event_data['duration']) |
|
|
|
|
|
print(f'Adding event - Topic: {chapter}, Type: {"Quiz" if is_problem else "Video"}, Value: {value}') |
|
|
add_feature_matrix = world.add_new_state(chapter=chapter, |
|
|
value=value, |
|
|
is_problem=is_problem, |
|
|
predict=False) |
|
|
|
|
|
pred = pd.read_csv(f'{OUTPUT_DIR}/predictions_weeks_{week}.csv') |
|
|
pred = pred['y_pred'] |
|
|
if test_ids is None: |
|
|
real_student = trajectories_each_week[week-1] |
|
|
else: |
|
|
real_student = trajectories_each_week[week-1][test_ids] |
|
|
if fail_only: |
|
|
real_student = [real_student.iloc[i] for i in range(len(real_student)) if pred[i] == 1] |
|
|
else: |
|
|
real_student = [real_student.iloc[i] for i in range(len(real_student))] |
|
|
|
|
|
syn_students = irl_helper.make_syn_student_personalized( |
|
|
trajectories_pass=[trajectories_each_week_pass[week-1]], |
|
|
trajectories_fail=[trajectories_each_week_fail[week-1]], |
|
|
students=[trajectories_each_week[week-1]], |
|
|
history_whatif_pass=[history_whatif_pass[week-1]], |
|
|
history_whatif_fail=[history_whatif_fail[week-1]], |
|
|
feature_matrix=add_feature_matrix, |
|
|
world=world, |
|
|
num_week=1, |
|
|
is_start_state_arr=False, |
|
|
test_labels=pred, |
|
|
new_state=True, |
|
|
test_ids=test_ids, |
|
|
fail_only=fail_only |
|
|
) |
|
|
|
|
|
syn_students = [syn_students[i][0] for i in range(len(syn_students))] |
|
|
if len(week_list) > 1: |
|
|
results['week'].append(week) |
|
|
results['syn_trajectories'].append(syn_students) |
|
|
results['real_trajectories'].append(real_student) |
|
|
results['world'].append(world) |
|
|
else: |
|
|
return syn_students, real_student, world |
|
|
return results |
|
|
|
|
|
def get_top_rewards_per_week(df, top_n=3): |
|
|
|
|
|
if 'Week' not in df.columns: |
|
|
st.error(f"DataFrame is missing 'Week' column: {df.columns}") |
|
|
return pd.DataFrame() |
|
|
df_sorted = df.sort_values(by=['Week', 'Reward'], ascending=[True, False]) |
|
|
top_n_df = df_sorted.groupby('Week').head(top_n).reset_index(drop=True) |
|
|
return top_n_df |
|
|
|
|
|
def show_highest_rewards(course_id='dsp-002'): |
|
|
st.header("🏆 Popular Picks by Passing Students by Week") |
|
|
st.markdown("This section displays the top 3 videos and quizzes each week that passing students in previous year are most likely to interact with.") |
|
|
df_video = pd.read_csv(f'streamlit-assets/{course_id}/sorted_reward_video.csv') |
|
|
df_problem = pd.read_csv(f'streamlit-assets/{course_id}/sorted_reward_problem.csv') |
|
|
|
|
|
top_3_video = get_top_rewards_per_week(df_video, top_n=3) |
|
|
top_3_problem = get_top_rewards_per_week(df_problem, top_n=3) |
|
|
|
|
|
tab_video, tab_problem = st.tabs(["Videos", "Quizzes"]) |
|
|
|
|
|
|
|
|
top_3_video = top_3_video.merge(st.session_state.metadata[['Topic', 'Skills']], left_on='chapter', right_on='Topic', how='left') |
|
|
top_3_problem = top_3_problem.merge(st.session_state.metadata[['Topic', 'Skills']], left_on='chapter', right_on='Topic', how='left') |
|
|
|
|
|
with tab_video: |
|
|
|
|
|
if not top_3_video.empty: |
|
|
weeks = sorted(top_3_video['Week'].unique()) |
|
|
week_tabs = st.tabs([f"Week {w+1}" for w in weeks]) |
|
|
|
|
|
for week, week_tab in zip(weeks, week_tabs): |
|
|
with week_tab: |
|
|
st.markdown(f"**Top Videos - Week {week+1}**") |
|
|
week_data = top_3_video[top_3_video['Week'] == week] |
|
|
week_data['duration'] = week_data['duration'].apply(lambda x: f"{int(x)//60} min {int(x)%60} sec") |
|
|
week_data['title'] = week_data['title'].apply(extract_signal_name_regex) |
|
|
display_cols = ['title', 'Skills', 'subchapter', 'duration'] |
|
|
rename_cols = {'title': 'Name', 'Skills': 'Topic', 'subchapter': 'Sub-Topic','duration': 'Duration'} |
|
|
display_data = week_data[display_cols].dropna(axis=1, how='all').rename(columns=rename_cols) |
|
|
st.dataframe(display_data, use_container_width=False, hide_index=True) |
|
|
|
|
|
else: |
|
|
st.info("No video event data available to display top rewards.") |
|
|
|
|
|
with tab_problem: |
|
|
|
|
|
if not top_3_problem.empty: |
|
|
weeks = sorted(top_3_problem['Week'].unique()) |
|
|
week_tabs = st.tabs([f"Week {w+1}" for w in weeks]) |
|
|
|
|
|
for week, week_tab in zip(weeks, week_tabs): |
|
|
with week_tab: |
|
|
st.markdown(f"**Top Quizzes - Week {week+1}**") |
|
|
week_data = top_3_problem[top_3_problem['Week'] == week] |
|
|
week_data['Difficulty'] = week_data['Difficulty'].apply(lambda x: f"{x:.2f}") |
|
|
display_cols = ['title', 'chapter', 'subchapter', 'grade_max', 'Difficulty'] |
|
|
rename_cols = {'title': 'Name', 'chapter': 'Topic', 'subchapter': 'Sub-Topic', |
|
|
'grade_max': 'Max Grade', 'Difficulty': 'Difficulty Level (0-1)'} |
|
|
display_data = week_data[display_cols].dropna(axis=1, how='all').rename(columns=rename_cols) |
|
|
st.dataframe(display_data, use_container_width=False, hide_index=True) |
|
|
|
|
|
else: |
|
|
st.info("No problem event data available to display top rewards.") |
|
|
|
|
|
class StateManager: |
|
|
def __init__(self, **kwargs): |
|
|
self._kwargs = kwargs |
|
|
week = kwargs.get('week_list', [6])[0] |
|
|
course_id = 'dsp-004' |
|
|
DATA_DIR = f'streamlit-assets/{course_id}/' |
|
|
model_path = { |
|
|
5: 'lstm-bi-32-64-5-1722490972.1859/model.keras_final_e.keras', |
|
|
6: 'lstm-bi-32-64-6-1722494926.4949/model.keras_final_e.keras', |
|
|
7: 'lstm-bi-32-64-7-1722499225.71723/model.keras_final_e.keras', |
|
|
8: 'lstm-bi-32-64-8-1722504182.3553/model.keras_final_e.keras', |
|
|
9: 'lstm-bi-32-64-9-1722511435.7777/model.keras_final_e.keras', |
|
|
10: 'lstm-bi-32-64-10-1722519098.62673/model.keras_final_e.keras', |
|
|
} |
|
|
self.reconstructed_model = tf.keras.models.load_model(f'checkpoints/{model_path[week]}') |
|
|
|
|
|
self.test_ids = None |
|
|
self.x_test = np.load(f'{DATA_DIR}/real-data-early-prediction_{course_id}_1to10_ver2.npy_features.npy') |
|
|
print(f"Loaded x_test shape: {self.x_test.shape}") |
|
|
|
|
|
def show_metadata(): |
|
|
st.subheader("Course Information") |
|
|
st.markdown("""On the left is the skill set for each topic; on the right is a visual representation of the prerequisite skill structure. |
|
|
The taught skills are divided into three categories: |
|
|
|
|
|
1. Core skills: Fundamental skills that are essential for understanding the course material. |
|
|
2. Applied skills: Skills that apply the core skills in practical scenarios. |
|
|
3. Theory-based extension: Advanced skills that build upon the core skills. |
|
|
|
|
|
""") |
|
|
|
|
|
col1, col2 = st.columns([1, 2]) |
|
|
df = pd.read_csv('streamlit-assets/dsp_prerequisite_skills_2.csv') |
|
|
st.session_state.metadata = df |
|
|
with col1: |
|
|
st.subheader("📋 Topic Table") |
|
|
st.dataframe(df[['Topic', 'Skills', 'No. Videos', 'No. Quizzes']], use_container_width=False, hide_index=True) |
|
|
|
|
|
with col2: |
|
|
st.subheader("Prerequisite skill structure") |
|
|
fig = Image.open('streamlit-assets/prerequisite_skills_structure.png') |
|
|
st.image(fig, caption='**Blue**: Core skills, **Pink**: Applied skills, **Purple**: Theory-based extension', use_container_width=False) |
|
|
|
|
|
|
|
|
if 'current_page' not in st.session_state: |
|
|
st.session_state.current_page = 1 |
|
|
|
|
|
def show_navigation(): |
|
|
"""Display navigation buttons""" |
|
|
col1, col2, col3 = st.columns(3) |
|
|
|
|
|
with col1: |
|
|
if st.button("Page 1: Course Description", key="nav1"): |
|
|
st.session_state.current_page = 1 |
|
|
|
|
|
with col2: |
|
|
if st.button("Page 2: Course Analysis", key="nav2"): |
|
|
st.session_state.current_page = 2 |
|
|
|
|
|
with col3: |
|
|
if st.button("Page 3: What-If Simulation", key="nav3"): |
|
|
st.session_state.current_page = 3 |
|
|
|
|
|
def get_reward(history_whatif, whatif_world, feature_matrix): |
|
|
transition_probability = whatif_world.p_transition |
|
|
for i in range(len(history_whatif)): |
|
|
alpha = history_whatif[i]['alpha'] |
|
|
|
|
|
reward = feature_matrix @ alpha |
|
|
|
|
|
not_valid_state_action = transition_probability.sum(axis=-1) <= 0 |
|
|
reward[not_valid_state_action] = np.nan |
|
|
reward = (reward - np.nanmean(reward.flatten())) / np.nanstd(reward.flatten()) |
|
|
reward[not_valid_state_action] = -np.inf |
|
|
|
|
|
history_whatif[i]['reward'] = reward |
|
|
return history_whatif |
|
|
|
|
|
def load_data(): |
|
|
"""Load all required data (only once)""" |
|
|
if 'data_loaded' not in st.session_state: |
|
|
|
|
|
course_id = 'dsp-004' |
|
|
|
|
|
with open(f'streamlit-assets/{course_id}/trajectories_each_week.pkl', 'rb') as f: |
|
|
st.session_state.trajectories_each_week = pickle.load(f) |
|
|
with open(f'streamlit-assets/{course_id}/trajectories_each_week_pass.pkl', 'rb') as f: |
|
|
st.session_state.trajectories_each_week_pass = pickle.load(f) |
|
|
with open(f'streamlit-assets/{course_id}/trajectories_each_week_fail.pkl', 'rb') as f: |
|
|
st.session_state.trajectories_each_week_fail = pickle.load(f) |
|
|
with open(f'streamlit-assets/{course_id}/trajectories.pkl', 'rb') as f: |
|
|
st.session_state.trajectories = pickle.load(f) |
|
|
with open('history_whatif_fail_300.pkl', 'rb') as f: |
|
|
history_whatif_fail = pickle.load(f) |
|
|
with open('history_whatif_pass_300.pkl', 'rb') as f: |
|
|
history_whatif_pass = pickle.load(f) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with open(f'streamlit-assets/{course_id}/dict_event.pkl', 'rb') as f: |
|
|
st.session_state.dict_event = pickle.load(f) |
|
|
with open(f'streamlit-assets/{course_id}/dict_action.pkl', 'rb') as f: |
|
|
st.session_state.dict_action = pickle.load(f) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with open(f'streamlit-assets/{course_id}/problem_event.pkl', 'rb') as f: |
|
|
st.session_state.problem_event = pickle.load(f) |
|
|
with open(f'streamlit-assets/{course_id}/video_event.pkl', 'rb') as f: |
|
|
st.session_state.video_event = pickle.load(f) |
|
|
pred = pd.read_csv(f'streamlit-assets/{course_id}/predictions_weeks_6.csv') |
|
|
st.session_state.pred = pred['y_pred'] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.session_state.values_df = pd.read_csv(f'streamlit-assets/value_df_duration_{course_id}.csv') |
|
|
st.session_state.duration = np.load(f'streamlit-assets/duration_{course_id}.npy') |
|
|
|
|
|
|
|
|
|
|
|
st.session_state.ids = None |
|
|
st.session_state.world = Env.ClickstreamWorld(trajectories=st.session_state.trajectories, |
|
|
dict_action=st.session_state.dict_action, |
|
|
dict_event=st.session_state.dict_event, |
|
|
video_arr=st.session_state.video_event, |
|
|
problem_arr=st.session_state.problem_event, |
|
|
values=st.session_state.values_df, |
|
|
add_state=True) |
|
|
|
|
|
feature_matrix = st.session_state.world.designed_features(st.session_state.world.values) |
|
|
st.session_state.history_whatif_fail = get_reward(history_whatif_fail, st.session_state.world, feature_matrix=feature_matrix) |
|
|
st.session_state.history_whatif_pass = get_reward(history_whatif_pass, st.session_state.world, feature_matrix=feature_matrix) |
|
|
st.session_state.state_manager = StateManager(world=st.session_state.world, |
|
|
trajectories=st.session_state.trajectories, |
|
|
test_ids=st.session_state.ids, |
|
|
trajectories_each_week=st.session_state.trajectories_each_week, |
|
|
trajectories_each_week_pass=st.session_state.trajectories_each_week_pass, |
|
|
trajectories_each_week_fail=st.session_state.trajectories_each_week_fail, |
|
|
history_whatif_pass=st.session_state.history_whatif_pass, |
|
|
history_whatif_fail=st.session_state.history_whatif_fail, |
|
|
week_list=[6]) |
|
|
|
|
|
st.session_state.data_loaded = True |
|
|
|
|
|
|
|
|
def page_1_description(): |
|
|
st.title("What-if Classroom for Course Design: Digital Signal Processing (DSP)") |
|
|
|
|
|
st.markdown( |
|
|
""" |
|
|
<h4>Description:</h4> |
|
|
You are teaching a 10-week Digital Signal Processing (DSP) course on a MOOC platform and have just completed the first 5 weeks. |
|
|
You're now considering whether to add new content in week 6. |
|
|
|
|
|
The platform provides access to detailed clickstream data that tracks how students in previous years interacted with videos and quizzes. |
|
|
You want to identify which content is most effective in supporting student success. |
|
|
|
|
|
This demo walks you through how to use our simulation model to preview the impact of adding new materials before implementing them in reality. |
|
|
""", |
|
|
unsafe_allow_html=True |
|
|
) |
|
|
|
|
|
st.markdown("---") |
|
|
|
|
|
st.header("Course Overview") |
|
|
st.markdown(""" |
|
|
**Course:** Digital Signal Processing (DSP) |
|
|
**Platform:** MOOC (Massive Open Online Course) |
|
|
**Duration:** 10 weeks |
|
|
**Current Status:** Completed first 5 weeks |
|
|
""") |
|
|
|
|
|
try: |
|
|
show_metadata() |
|
|
except: |
|
|
st.info("Metadata display function not available in this demo.") |
|
|
st.markdown("---") |
|
|
col1, col2, col3 = st.columns([1, 1, 1]) |
|
|
with col3: |
|
|
if st.button("Next: Course Analysis →", key="next_page_1", type="primary"): |
|
|
st.session_state.current_page = 2 |
|
|
st.rerun() |
|
|
|
|
|
def page_2_analysis(): |
|
|
st.title("Course Material Analysis") |
|
|
|
|
|
if st.session_state.get('data_loaded', False): |
|
|
|
|
|
load_data() |
|
|
if not st.session_state.get('data_loaded', False): |
|
|
st.error("Failed to load data. Please check data files.") |
|
|
|
|
|
st.header("Top Performing Course Materials") |
|
|
|
|
|
try: |
|
|
show_highest_rewards() |
|
|
except: |
|
|
st.info("Highest rewards analysis not available in this demo.") |
|
|
|
|
|
st.header("Current Student Performance Prediction") |
|
|
plot_performance(show_image=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.header("Week 6 Intervention Opportunity") |
|
|
st.markdown(""" |
|
|
Based on the analysis of the first 5 weeks, we can now simulate the impact of introducing |
|
|
new content in week 6. The next page allows you to experiment with different interventions |
|
|
and see their predicted effects on student performance. |
|
|
""") |
|
|
st.markdown("---") |
|
|
col1, col2, col3 = st.columns([1, 1, 1]) |
|
|
with col1: |
|
|
if st.button("← Previous: Course Description", key="prev_page_2"): |
|
|
st.session_state.current_page = 1 |
|
|
st.rerun() |
|
|
with col3: |
|
|
if st.button("Next: What-If Simulation →", key="next_page_2", type="primary"): |
|
|
st.session_state.current_page = 3 |
|
|
st.rerun() |
|
|
|
|
|
def page_3_simulation(): |
|
|
show_navigation() |
|
|
st.title("What-If Simulation: Week 6 Interventions") |
|
|
st.header("Simulation Capabilities") |
|
|
st.markdown(""" |
|
|
Our simulation model allows you to: |
|
|
- Preview the impact of new content before implementation |
|
|
- Test different content types (videos, quizzes) and difficulties |
|
|
- Target interventions for specific student populations |
|
|
- Analyze engagement patterns and predict performance outcomes |
|
|
""") |
|
|
if not st.session_state.get('data_loaded', False): |
|
|
st.warning("Loading course data...") |
|
|
load_data() |
|
|
if not st.session_state.get('data_loaded', False): |
|
|
st.error("Failed to load data. Please check data files.") |
|
|
return |
|
|
|
|
|
|
|
|
st.header("Introduce New Course Content") |
|
|
st.markdown("You are teaching a 10-week Digital Signal Processing (DSP) course on a MOOC platform and have just completed the first 5 weeks. You're now considering whether to add new content in week 6.\ |
|
|
You want to identify which content is most effective in supporting student success by trying different interventions.") |
|
|
|
|
|
|
|
|
st.write("**Step 1: Please select the topic the new content covers**") |
|
|
|
|
|
selected_chapter_name = st.selectbox( |
|
|
'Topic:', |
|
|
options=st.session_state.metadata['Skills'], |
|
|
index=0 |
|
|
) |
|
|
chapter = st.session_state.metadata[st.session_state.metadata['Skills'] == selected_chapter_name]['Topic'].values[0] |
|
|
|
|
|
st.write("**Step 2: Please select the type of course material you want to add, either a quiz or a video**") |
|
|
event_type = st.selectbox('Course Material Type:', ['quiz', 'video']) |
|
|
max_duration = st.session_state.duration[0]//60 if 'duration' in st.session_state else 0 |
|
|
min_duration = st.session_state.duration[1]//60 if 'duration' in st.session_state else 60 |
|
|
st.write("**Step 3: Please select the difficulty of the quiz or the duration of the video.**\ |
|
|
\nDifficulty: value between 0 and 1; easiest 0, hardest: 1.\ |
|
|
\nDuration: in minutes; shortest: 2-minute, longest: 25-minute.") |
|
|
if event_type == 'quiz': |
|
|
value_label = 'Difficulty:' |
|
|
value = st.slider(value_label, 0.0, 1.0, 0.5, 0.01) |
|
|
else: |
|
|
value_label = 'Duration (minutes):' |
|
|
value = st.slider(value_label, min_duration, max_duration, min_duration, 0.5) |
|
|
|
|
|
st.write("**Step 4: Please select whether you want to focus on low-performed students only.**") |
|
|
fail_only = st.selectbox('Simulate low-performed students only:', |
|
|
[False, True], |
|
|
format_func=lambda x: 'Yes' if x else 'No') |
|
|
|
|
|
|
|
|
st.subheader("Intervention Settings: ready to run simulation") |
|
|
col1, col2 = st.columns([1, 1]) |
|
|
with col1: |
|
|
st.write(f"**Topic:** {chapter}") |
|
|
st.write(f"**Content Type:** {event_type.title()}") |
|
|
with col2: |
|
|
st.write(f"**{value_label}** {value:.2f}") |
|
|
st.write(f"**Target Low-Performers Only:** {'Yes' if fail_only else 'No'}") |
|
|
|
|
|
if st.button('Run Simulation', key='add_state_button'): |
|
|
st.session_state.world = Env.ClickstreamWorld(trajectories=st.session_state.trajectories, |
|
|
dict_action=st.session_state.dict_action, |
|
|
dict_event=st.session_state.dict_event, |
|
|
video_arr=st.session_state.video_event, |
|
|
problem_arr=st.session_state.problem_event, |
|
|
values=st.session_state.values_df, |
|
|
add_state=True) |
|
|
event_data = { |
|
|
'chapter': chapter, |
|
|
'event_type': event_type, |
|
|
'difficulty': value, |
|
|
'duration':(value - min_duration)/(max_duration - min_duration), |
|
|
'fail_only': fail_only |
|
|
} |
|
|
|
|
|
with st.spinner("Analyzing trajectories and simulating intervention..."): |
|
|
try: |
|
|
results = add_new_state(world=st.session_state.world, |
|
|
event_data=event_data, |
|
|
week_list=[6], |
|
|
test_ids=st.session_state.state_manager.test_ids, |
|
|
trajectories=st.session_state.trajectories, |
|
|
trajectories_each_week=st.session_state.trajectories_each_week, |
|
|
trajectories_each_week_pass=st.session_state.trajectories_each_week_pass, |
|
|
trajectories_each_week_fail=st.session_state.trajectories_each_week_fail, |
|
|
history_whatif_pass=st.session_state.history_whatif_pass, |
|
|
history_whatif_fail=st.session_state.history_whatif_fail, |
|
|
fail_only=fail_only) |
|
|
|
|
|
st.session_state.results = results |
|
|
st.success("Simulation completed! ") |
|
|
except Exception as e: |
|
|
st.error(f"Simulation failed: {str(e)}") |
|
|
|
|
|
|
|
|
if 'results' in st.session_state: |
|
|
st.header("Simulation Results") |
|
|
|
|
|
try: |
|
|
|
|
|
import utils.data_helper as data_helper |
|
|
if 'real_pred_df' not in st.session_state: |
|
|
fail_num, pass_num, _ = performance_prediction( |
|
|
st.session_state.state_manager.reconstructed_model, |
|
|
st.session_state.state_manager.x_test |
|
|
) |
|
|
|
|
|
st.session_state.real_pred_df = pd.DataFrame({ |
|
|
'class': ['Predicted Performance Before Intervention'], |
|
|
'fail_num': [fail_num], |
|
|
'pass_num': [pass_num], |
|
|
'fail_percent': [fail_num / (fail_num + pass_num) * 100], |
|
|
'pass_percent': [pass_num / (fail_num + pass_num) * 100] |
|
|
}) |
|
|
|
|
|
if isinstance(st.session_state.results, dict): |
|
|
syn = st.session_state.results['syn_trajectories'] |
|
|
real = st.session_state.results['real_trajectories'] |
|
|
world = st.session_state.results['world'] |
|
|
else: |
|
|
syn, real, world = st.session_state.results |
|
|
|
|
|
course_features = data_helper.trajectories_to_features( |
|
|
syn, world, num_week=1, syn=True, save_to_disk=False |
|
|
) |
|
|
if not fail_only: |
|
|
x_test = np.concatenate((st.session_state.state_manager.x_test[:, :(st.session_state.state_manager.x_test.shape[0]-100), :], |
|
|
course_features), axis=1) |
|
|
else: |
|
|
x_test = np.concatenate((st.session_state.state_manager.x_test[st.session_state.pred == 1, :(st.session_state.state_manager.x_test.shape[0]-100), :], |
|
|
course_features), axis=1) |
|
|
|
|
|
fail_num, pass_num, _ = performance_prediction( |
|
|
st.session_state.state_manager.reconstructed_model, x_test |
|
|
) |
|
|
|
|
|
temp_df = pd.DataFrame({ |
|
|
'class': ['Predicted Performance After Intervention'], |
|
|
'fail_num': [fail_num], |
|
|
'pass_num': [pass_num], |
|
|
'fail_percent': [fail_num / (fail_num + pass_num) * 100], |
|
|
'pass_percent': [pass_num / (fail_num + pass_num) * 100] |
|
|
}) |
|
|
if not fail_only: |
|
|
merged_df = pd.concat([temp_df, st.session_state.real_pred_df], ignore_index=True) |
|
|
st.session_state.performance_df = merged_df |
|
|
st.session_state.num_students = temp_df.iloc[0]['fail_num'] + temp_df.iloc[0]['pass_num'] |
|
|
else: |
|
|
st.session_state.fail_df = pd.DataFrame({ |
|
|
'class': ['Predicted Performance After Intervention'], |
|
|
'fail_num': st.session_state.real_pred_df.fail_num, |
|
|
'pass_num': [0], |
|
|
'fail_percent': 100, |
|
|
'pass_percent': 0 |
|
|
}) |
|
|
merged_df = pd.concat([temp_df, st.session_state.fail_df], ignore_index=True) |
|
|
st.session_state.performance_df = merged_df |
|
|
|
|
|
|
|
|
st.subheader("Performance Prediction: Before and After Intervention") |
|
|
col1, col2 = st.columns(2) |
|
|
|
|
|
with col1: |
|
|
|
|
|
st.metric("Before Intervention - Pass Rate", |
|
|
f"{st.session_state.performance_df.iloc[-1]['pass_percent']:.1f}%") |
|
|
st.metric("Before Intervention - Students Passing", |
|
|
f"{int(st.session_state.performance_df.iloc[-1]['pass_num'])} / {st.session_state.num_students} Students") |
|
|
|
|
|
with col2: |
|
|
new_pass_rate = temp_df.iloc[0]['pass_percent'] |
|
|
old_pass_rate = st.session_state.performance_df.iloc[-1]['pass_percent'] |
|
|
improvement = new_pass_rate - old_pass_rate |
|
|
|
|
|
st.metric("After Intervention - Pass Rate", |
|
|
f"{new_pass_rate:.1f}%", |
|
|
f"{improvement:+.1f}%") |
|
|
st.metric("After Intervention - Students Passing", |
|
|
f"{int(temp_df.iloc[0]['pass_num'])} / {st.session_state.num_students} Students", |
|
|
int(temp_df.iloc[0]['pass_num'] - st.session_state.performance_df.iloc[-1]['pass_num'])) |
|
|
|
|
|
|
|
|
try: |
|
|
plot_performance(st.session_state.performance_df, show_performance=True) |
|
|
except: |
|
|
st.info("Performance visualization not available.") |
|
|
|
|
|
try: |
|
|
compare_chapter_engagement(syn, real, world) |
|
|
except Exception as e: |
|
|
raise e |
|
|
st.info(f"Engagement analysis not available: {str(e)}") |
|
|
|
|
|
except Exception as e: |
|
|
raise e |
|
|
st.error(f"Error processing simulation results: {str(e)}") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
else: |
|
|
st.info("Run a simulation using the controls to see results.") |
|
|
st.markdown("---") |
|
|
col1, col2, col3 = st.columns([1, 1, 1]) |
|
|
with col1: |
|
|
if st.button("← Previous: Course Analysis", key="prev_page_3"): |
|
|
st.session_state.current_page = 2 |
|
|
st.rerun() |
|
|
|
|
|
def main(): |
|
|
|
|
|
load_data() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if st.session_state.current_page == 1: |
|
|
page_1_description() |
|
|
elif st.session_state.current_page == 2: |
|
|
page_2_analysis() |
|
|
elif st.session_state.current_page == 3: |
|
|
page_3_simulation() |
|
|
|
|
|
|
|
|
st.markdown("---") |
|
|
st.markdown(f"**Current Page: {st.session_state.current_page} of 3**") |
|
|
|
|
|
if __name__ == "__main__": |
|
|
main() |
|
|
|