mocaTest / app.py
garrec2's picture
Upload 2 files
81e8bef verified
Raw History Blame Contribute Delete
25.9 kB
"""
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("""
<style>
.main {
padding: 1rem;
}
.stAlert {
margin: 1rem 0;
}
</style>
""", 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("""
<div style='text-align: center; color: gray; font-size: 0.85em;'>
<p>Interactive MoCA v8.1 Assessment Tool | Based on the Montreal Cognitive Assessment</p>
<p>Original MoCA Β© Z. Nasreddine MD | www.mocatest.org</p>
<p>This implementation is for research and educational purposes only.</p>
</div>
""", unsafe_allow_html=True)