""" Interactive Montreal Cognitive Assessment (MoCA) v8.1 Streamlit App for Hugging Face Spaces Deployment To deploy: 1. Create a Hugging Face Space (https://huggingface.co/spaces) 2. Choose "Streamlit" as SDK 3. Upload this file as app.py 4. Create a requirements.txt with: streamlit, pandas, matplotlib, numpy """ import streamlit as st import pandas as pd import matplotlib.pyplot as plt import numpy as np from datetime import datetime import json # Page config st.set_page_config( page_title="MoCA Assessment", layout="wide", initial_sidebar_state="expanded" ) # Custom CSS st.markdown(""" """, unsafe_allow_html=True) st.title("🧠 Interactive Montreal Cognitive Assessment (MoCA) v8.1") st.markdown(""" --- **📋 About this tool:** - Digital implementation of the Montreal Cognitive Assessment - Assessment takes approximately 10-15 minutes - For research and educational purposes **⚠️ DISCLAIMER:** Clinical interpretation must be done by a qualified healthcare professional. Do NOT use this for diagnosis without professional oversight. **Normal Score:** ≥26/30 (add 1 point if education ≤12 years) --- """) # Initialize session state if 'test_data' not in st.session_state: st.session_state.test_data = { 'demographics': {}, 'responses': {}, 'scores': {}, 'timestamp': datetime.now().isoformat() } test_data = st.session_state.test_data # Sidebar navigation with st.sidebar: st.header("📍 Assessment Steps") step = st.radio( "Select Step:", [ "1️⃣ Demographics", "2️⃣ Visuospatial", "3️⃣ Naming", "4️⃣ Memory (Learning)", "5️⃣ Attention", "6️⃣ Language", "7️⃣ Delayed Recall", "8️⃣ Orientation", "📊 Results" ] ) st.divider() # Progress indicator completed_sections = len([s for s in test_data['scores'].keys() if s != 'education_bonus' and s != 'mis']) st.metric("Progress", f"{completed_sections}/8 sections") # ===== STEP 1: DEMOGRAPHICS ===== if "1️⃣" in step: st.header("👤 Participant Information") col1, col2 = st.columns(2) with col1: name = st.text_input( "Name (optional):", value=test_data['demographics'].get('name', ''), help="For record keeping only" ) age = st.slider( "Age:", min_value=18, max_value=100, value=test_data['demographics'].get('age', 65), step=1 ) with col2: education = st.slider( "Years of Education:", min_value=0, max_value=20, value=test_data['demographics'].get('education', 12), step=1, help="Total formal education completed" ) sex = st.selectbox( "Sex:", ["Not Specified", "Male", "Female", "Other"], index=["Not Specified", "Male", "Female", "Other"].index( test_data['demographics'].get('sex', "Not Specified") ) ) language = st.selectbox( "Native Language:", ["English", "Other"], index=0 if test_data['demographics'].get('language') == "English" else 1 ) if st.button("✅ Save Demographics & Continue", key="demo_btn", use_container_width=True): test_data['demographics'] = { 'name': name, 'age': age, 'education': education, 'sex': sex, 'language': language } st.session_state.test_data = test_data st.success("✓ Demographics saved! Proceed to next step.") # ===== STEP 2: VISUOSPATIAL ===== elif "2️⃣" in step: st.header("🎨 Visuospatial & Executive Function") col1, col2 = st.columns(2) with col1: st.subheader("1️⃣ Trail Making Test") st.info(""" **Instructions:** Draw a line from number to letter in ascending order: 1 → A → 2 → B → 3 → C → 4 → D → 5 → E **Important:** Lines should not cross. Do not connect E back to 1. """) trail = st.radio( "Result:", ["✓ Completed correctly (no crossing lines)", "✗ Errors or lines crossed"], key="trail_radio" ) with col2: st.subheader("2️⃣ Cube Drawing") st.info(""" **Instructions:** Copy this cube drawing as accurately as you can. **Scoring:** 3-dimensional, all lines drawn, lines meet with little space, no extra lines, lines relatively parallel. """) cube = st.radio( "Result:", ["✓ Correctly executed", "✗ Errors in drawing"], key="cube_radio" ) st.subheader("3️⃣ Clock Drawing") st.info("**Instructions:** Draw a clock. Put in all numbers and set time to 10 past 11.") col1, col2, col3 = st.columns(3) with col1: clock_contour = st.checkbox("✓ Contour correct (circle/square)") with col2: clock_numbers = st.checkbox("✓ All numbers present & correct order") with col3: clock_hands = st.checkbox("✓ Hands correct (short hour, long minute, centered)") if st.button("✅ Save Visuospatial & Continue", key="vis_btn", use_container_width=True): trail_pts = 1 if "✓" in trail else 0 cube_pts = 1 if "✓" in cube else 0 clock_pts = sum([clock_contour, clock_numbers, clock_hands]) test_data['responses']['visuospatial'] = { 'trail': trail, 'cube': cube, 'clock_contour': clock_contour, 'clock_numbers': clock_numbers, 'clock_hands': clock_hands } test_data['scores']['visuospatial'] = trail_pts + cube_pts + clock_pts st.session_state.test_data = test_data st.success(f"✓ Visuospatial saved! Points: {trail_pts + cube_pts + clock_pts}/5") # ===== STEP 3: NAMING ===== elif "3️⃣" in step: st.header("🦁 Naming Test") st.info(""" **Instructions:** The examiner points to three animals. What name did the participant give for each? """) st.markdown("### Animals:") col1, col2, col3 = st.columns(3) with col1: st.markdown("**Animal 1: Lion**") animal1 = st.text_input("Participant's answer:", key="animal1", placeholder="e.g., lion") with col2: st.markdown("**Animal 2: Rhinoceros**") animal2 = st.text_input("Participant's answer:", key="animal2", placeholder="e.g., rhino") with col3: st.markdown("**Animal 3: Camel**") animal3 = st.text_input("Participant's answer:", key="animal3", placeholder="e.g., camel") if st.button("✅ Save Naming & Continue", key="naming_btn", use_container_width=True): score = 0 if 'lion' in animal1.lower(): score += 1 if any(x in animal2.lower() for x in ['rhino', 'rhinoceros']): score += 1 if any(x in animal3.lower() for x in ['camel', 'dromedary']): score += 1 test_data['responses']['naming'] = { 'animal1': animal1, 'animal2': animal2, 'animal3': animal3 } test_data['scores']['naming'] = score st.session_state.test_data = test_data st.success(f"✓ Naming saved! Points: {score}/3") # ===== STEP 4: MEMORY LEARNING ===== elif "4️⃣" in step: st.header("🧠 Memory Test - Learning Phase") st.warning(""" ⚠️ **IMPORTANT:** The participant must REMEMBER these words. They will be tested on them later in the assessment. """) st.markdown(""" ### 📝 WORDS TO READ (at rate of 1 word per second): # **FACE • VELVET • CHURCH • DAISY • RED** """) st.info(""" **Examiner Instructions:** "This is a memory test. I am going to read a list of words that you will have to remember now and later on. Listen carefully. When I am through, tell me as many words as you can remember." """) if st.button("✅ Participant Heard Words - Continue to Attention", key="memory_btn", use_container_width=True): test_data['responses']['memory_words'] = ['FACE', 'VELVET', 'CHURCH', 'DAISY', 'RED'] st.session_state.test_data = test_data st.success("✓ Memory encoding recorded. Proceed to Attention section.") # ===== STEP 5: ATTENTION ===== elif "5️⃣" in step: st.header("👁️ Attention Tests") with st.container(border=True): st.subheader("1️⃣ Forward Digit Span") st.markdown("**Read at 1 digit/second:** 2 1 8 5 4") digit_forward = st.text_input( "Participant repeated:", placeholder="e.g., 2 1 8 5 4 or 2-1-8-5-4", key="digit_forward" ) with st.container(border=True): st.subheader("2️⃣ Backward Digit Span") st.markdown("**Read at 1 digit/second:** 7 4 2") st.markdown("*Correct backward answer: 2 4 7*") digit_backward = st.text_input( "Participant repeated:", placeholder="e.g., 2 4 7", key="digit_backward" ) with st.container(border=True): st.subheader("3️⃣ Vigilance (Tap on Letter A)") st.markdown("**Read letters at 1/second. Participant taps on every 'A':**") st.markdown("F B A C M N A A J K L B A F A K D E A A A J A M O F A A B") vig_errors = st.slider("Errors (tapped wrong or missed A):", 0, 10, value=0, key="vig_errors") with st.container(border=True): st.subheader("4️⃣ Serial 7 Subtraction") st.markdown(""" **Instruction:** "Count backward by 7s starting from 100." Expected sequence: 93 → 86 → 79 → 72 → 65 """) serial7_correct = st.slider( "Number of correct subtractions:", 0, 5, value=0, key="serial7_correct" ) if st.button("✅ Save Attention & Continue", key="attention_btn", use_container_width=True): score = 0 score += 1 if '2 1 8 5 4' in digit_forward.replace('-', ' ').replace(',', ' ') else 0 score += 1 if '2 4 7' in digit_backward.replace('-', ' ').replace(',', ' ') else 0 score += 1 if vig_errors <= 1 else 0 if serial7_correct >= 4: score += 3 elif serial7_correct >= 2: score += 2 elif serial7_correct >= 1: score += 1 test_data['responses']['attention'] = { 'digit_forward': digit_forward, 'digit_backward': digit_backward, 'vigilance_errors': vig_errors, 'serial7_correct': serial7_correct } test_data['scores']['attention'] = score st.session_state.test_data = test_data st.success(f"✓ Attention saved! Points: {score}/8") # ===== STEP 6: LANGUAGE ===== elif "6️⃣" in step: st.header("💬 Language Tests") with st.container(border=True): st.subheader("1️⃣ Sentence Repetition") st.markdown(""" **Instruction:** "I am going to read you a sentence. Repeat it exactly as I say it." """) st.markdown("**Sentence 1:**") st.markdown('> "I only know that John is the one to help today."') sent1 = st.checkbox( "✓ Repeated exactly (watch for omissions, substitutions, grammar errors)", key="sent1" ) st.markdown("**Sentence 2:**") st.markdown('> "The cat always hid under the couch when dogs were in the room."') sent2 = st.checkbox( "✓ Repeated exactly", key="sent2" ) with st.container(border=True): st.subheader("2️⃣ Verbal Fluency (Letter F)") st.markdown(""" **Instruction:** "Tell me as many words as you can think of that begin with the letter F. I will tell you to stop after 60 seconds." *Rules: No proper nouns, numbers, or different verb forms* """) fluency = st.slider( "Number of valid words starting with F:", 0, 30, value=0, key="fluency" ) with st.container(border=True): st.subheader("3️⃣ Abstraction") st.markdown("**Pair 1: Train and Bicycle**") st.markdown("*Acceptable answers: means of transportation, means of travel, you take trips*") st.markdown("*NOT acceptable: they have wheels*") abstract1 = st.radio( "Response:", ["✓ Correct (transportation/travel)", "✗ Incorrect or concrete"], key="abstract1" ) st.markdown("**Pair 2: Ruler and Watch**") st.markdown("*Acceptable answers: measuring instruments, used to measure*") st.markdown("*NOT acceptable: they have numbers*") abstract2 = st.radio( "Response:", ["✓ Correct (measuring instruments)", "✗ Incorrect or concrete"], key="abstract2" ) if st.button("✅ Save Language & Continue", key="language_btn", use_container_width=True): score = 0 score += int(sent1) score += int(sent2) score += 1 if fluency >= 11 else 0 score += 1 if "✓" in abstract1 else 0 score += 1 if "✓" in abstract2 else 0 test_data['responses']['language'] = { 'sent1': sent1, 'sent2': sent2, 'fluency': fluency, 'abstract1': abstract1, 'abstract2': abstract2 } test_data['scores']['language'] = score st.session_state.test_data = test_data st.success(f"✓ Language saved! Points: {score}/5") # ===== STEP 7: DELAYED RECALL ===== elif "7️⃣" in step: st.header("🧠 Delayed Recall & Memory Index Score") st.warning("⚠️ **Recall the 5 words from earlier:**") st.markdown("# **FACE • VELVET • CHURCH • DAISY • RED**") with st.container(border=True): st.subheader("Spontaneous Recall (WITHOUT CUES)") st.markdown("Which words did participant recall without any help?") col1, col2, col3 = st.columns(3) with col1: recall_face = st.checkbox("FACE", key="recall_face") recall_velvet = st.checkbox("VELVET", key="recall_velvet") with col2: recall_church = st.checkbox("CHURCH", key="recall_church") recall_daisy = st.checkbox("DAISY", key="recall_daisy") with col3: recall_red = st.checkbox("RED", key="recall_red") with st.container(border=True): st.subheader("With Category Cues (if needed)") st.markdown("For words not recalled, give category hints:") col1, col2, col3 = st.columns(3) with col1: cue_face = st.checkbox("FACE (body part)", key="cue_face") cue_velvet = st.checkbox("VELVET (fabric)", key="cue_velvet") with col2: cue_church = st.checkbox("CHURCH (building)", key="cue_church") cue_daisy = st.checkbox("DAISY (flower)", key="cue_daisy") with col3: cue_red = st.checkbox("RED (color)", key="cue_red") with st.container(border=True): st.subheader("With Multiple-Choice Only") st.markdown("Words only recalled when given multiple choice options:") col1, col2, col3 = st.columns(3) with col1: mult_face = st.checkbox("FACE (MC only)", key="mult_face") mult_velvet = st.checkbox("VELVET (MC only)", key="mult_velvet") with col2: mult_church = st.checkbox("CHURCH (MC only)", key="mult_church") mult_daisy = st.checkbox("DAISY (MC only)", key="mult_daisy") with col3: mult_red = st.checkbox("RED (MC only)", key="mult_red") if st.button("✅ Save Delayed Recall & Continue", key="memory_recall_btn", use_container_width=True): spontaneous = sum([ recall_face, recall_velvet, recall_church, recall_daisy, recall_red ]) category = sum([ cue_face and not recall_face, cue_velvet and not recall_velvet, cue_church and not recall_church, cue_daisy and not recall_daisy, cue_red and not recall_red ]) multiple = sum([ mult_face and not recall_face and not cue_face, mult_velvet and not recall_velvet and not cue_velvet, mult_church and not recall_church and not cue_church, mult_daisy and not recall_daisy and not cue_daisy, mult_red and not recall_red and not cue_red ]) mis = (spontaneous * 3) + (category * 2) + (multiple * 1) test_data['responses']['delayed_recall'] = { 'spontaneous': spontaneous, 'category': category, 'multiple': multiple } test_data['scores']['delayed_recall'] = spontaneous test_data['scores']['mis'] = mis st.session_state.test_data = test_data st.success(f""" ✓ Delayed Recall Saved! - Spontaneous: {spontaneous}/5 points - Memory Index Score (MIS): {mis}/15 points """) # ===== STEP 8: ORIENTATION ===== elif "8️⃣" in step: st.header("📍 Orientation") today = datetime.now() st.info(f"Today's date should be: **{today.strftime('%A, %B %d, %Y')}**") st.markdown("### Check which orientation items are correct:") col1, col2, col3 = st.columns(3) with col1: orient_date = st.checkbox("✓ Date correct (exact, no ±1 day error)") orient_month = st.checkbox("✓ Month correct") orient_year = st.checkbox("✓ Year correct") with col2: orient_day = st.checkbox("✓ Day of week correct") orient_place = st.checkbox("✓ Place/Location correct") with col3: orient_city = st.checkbox("✓ City correct") if st.button("✅ Complete Assessment", key="orient_btn", use_container_width=True): score = sum([ orient_date, orient_month, orient_year, orient_day, orient_place, orient_city ]) test_data['scores']['orientation'] = score st.session_state.test_data = test_data st.success(f"✓ Orientation saved! Points: {score}/6") st.balloons() # ===== RESULTS PAGE ===== elif "📊" in step: st.header("📊 Assessment Results & Scoring") def calc_total(): total = sum([ test_data['scores'].get('visuospatial', 0), test_data['scores'].get('naming', 0), test_data['scores'].get('attention', 0), test_data['scores'].get('language', 0), test_data['scores'].get('delayed_recall', 0), test_data['scores'].get('orientation', 0) ]) if test_data['demographics'].get('education', 0) <= 12: total += 1 return min(total, 30) total_score = calc_total() is_normal = total_score >= 26 # Summary table summary_data = { 'Domain': ['Visuospatial/Executive', 'Naming', 'Attention', 'Language', 'Delayed Recall', 'Orientation'], 'Points': [ test_data['scores'].get('visuospatial', 0), test_data['scores'].get('naming', 0), test_data['scores'].get('attention', 0), test_data['scores'].get('language', 0), test_data['scores'].get('delayed_recall', 0), test_data['scores'].get('orientation', 0) ], 'Max': [5, 3, 8, 5, 5, 6] } df = pd.DataFrame(summary_data) st.subheader("Score Breakdown by Domain") st.dataframe(df, use_container_width=True, hide_index=True) # Key metrics col1, col2, col3, col4 = st.columns(4) with col1: st.metric("📊 Total Score", f"{total_score}/30", delta="Normal" if is_normal else "Possible Impairment") with col2: st.metric("📈 Status", "✓ Normal" if is_normal else "⚠ Below 26") with col3: st.metric("🧠 MIS Score", f"{test_data['scores'].get('mis', 0)}/15", help="Memory Index Score - shows type of memory deficit") with col4: st.metric("👤 Participant", test_data['demographics'].get('name', 'Not provided'), test_data['demographics'].get('age', 'N/A')) # Visualizations st.subheader("Score Visualization") col1, col2 = st.columns(2) with col1: fig1, ax1 = plt.subplots(figsize=(10, 5)) scores = df['Points'].tolist() max_scores = df['Max'].tolist() x = np.arange(len(df['Domain'])) ax1.bar(x - 0.2, scores, 0.4, label='Participant Score', color='steelblue', alpha=0.8) ax1.bar(x + 0.2, max_scores, 0.4, label='Maximum', color='lightgray', alpha=0.5) ax1.set_ylabel('Points', fontsize=11) ax1.set_title('MoCA Score by Cognitive Domain', fontsize=12, fontweight='bold') ax1.set_xticks(x) ax1.set_xticklabels(df['Domain'], rotation=45, ha='right', fontsize=9) ax1.legend() ax1.grid(axis='y', alpha=0.3, linestyle='--') ax1.set_ylim(0, 10) plt.tight_layout() st.pyplot(fig1, use_container_width=True) with col2: fig2, ax2 = plt.subplots(figsize=(8, 6)) colors = ['#2ecc71' if is_normal else '#e74c3c', '#ecf0f1'] explode = (0.1, 0) wedges, texts, autotexts = ax2.pie( [total_score, 30 - total_score], labels=[f'Scored: {total_score}', f'Remaining: {30 - total_score}'], autopct='%1.1f%%', colors=colors, explode=explode, startangle=90, textprops={'fontsize': 11} ) status_text = "✓ NORMAL COGNITION" if is_normal else "⚠ POSSIBLE IMPAIRMENT" ax2.set_title(f'Overall Score\n{status_text}', fontsize=13, fontweight='bold') st.pyplot(fig2, use_container_width=True) # Interpretation st.divider() st.subheader("📋 Clinical Interpretation") if is_normal: st.success(f""" **✓ NORMAL COGNITION** Total Score: **{total_score}/30** The participant's performance falls within the normal range for cognitive function. A score of 26 or above is considered normal on the MoCA. """) else: st.warning(f""" **⚠ POSSIBLE COGNITIVE IMPAIRMENT** Total Score: **{total_score}/30** The participant's score is below 26, suggesting possible mild cognitive impairment. **Areas of concern:** """) for idx, row in df.iterrows(): if row['Points'] < (row['Max'] * 0.75): # <75% correct st.markdown(f"- **{row['Domain']}:** {row['Points']}/{row['Max']} points") # Memory Index interpretation st.info(f""" **Memory Index Score (MIS): {test_data['scores'].get('mis', 0)}/15** The MIS reveals the nature of memory problems: - **High spontaneous recall:** Memory retrieval is intact - **Improved with cues:** Retrieval problem (generally better prognosis) - **No improvement with cues:** Encoding problem (suggests more severe impairment) """) # Export section st.divider() st.subheader("💾 Export Results") # Prepare export data export_df = df.copy() export_df.loc[len(export_df)] = ['TOTAL', total_score, 30] export_df.loc[len(export_df)] = ['MIS Score', test_data['scores'].get('mis', 0), 15] csv = export_df.to_csv(index=False) json_str = json.dumps(test_data, indent=2, default=str) col1, col2 = st.columns(2) with col1: st.download_button( label="📥 Download as CSV", data=csv, file_name=f"moca_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv", mime="text/csv" ) with col2: st.download_button( label="📥 Download as JSON", data=json_str, file_name=f"moca_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json", mime="application/json" ) # Critical disclaimer st.divider() st.error(""" ### ⚠️ CRITICAL DISCLAIMER **This digital assessment is for RESEARCH AND EDUCATIONAL PURPOSES ONLY.** Clinical interpretation must ONLY be performed by: - A qualified healthcare professional (MD, DO, NP, PA) - With training in cognitive assessment - Who understands the participant's complete medical history - Who knows about current medications and their cognitive effects - Who can assess the overall clinical context and comorbidities **DO NOT USE THIS FOR DIAGNOSIS WITHOUT PROFESSIONAL OVERSIGHT.** Results should be reviewed with a healthcare provider before any clinical decisions are made. """) # Footer st.divider() st.markdown("""
Interactive MoCA v8.1 Assessment Tool | Based on the Montreal Cognitive Assessment
Original MoCA © Z. Nasreddine MD | www.mocatest.org
This implementation is for research and educational purposes only.