MisConceptTutor / app.py
Jintonic92's picture
Create app.py
d9a31b5 verified
raw
history blame
23.6 kB
import streamlit as st
import pandas as pd
import os
from src.SecondModule.module2 import SimilarQuestionGenerator
from src.ThirdModule.module3 import AnswerVerifier
import logging
from typing import Optional, Tuple
#from latex_formatter import LatexFormatter # LaTeX ํฌ๋งทํ„ฐ import
from pylatexenc.latex2text import LatexNodes2Text
logging.basicConfig(level=logging.DEBUG)
# Streamlit ํŽ˜์ด์ง€ ๊ธฐ๋ณธ ์„ค์ •
st.set_page_config(
page_title="MisconcepTutor",
layout="wide",
initial_sidebar_state="expanded"
)
@st.cache_resource
def load_answer_verifier():
"""๋‹ต์•ˆ ๊ฒ€์ฆ ๋ชจ๋ธ ๋กœ๋“œ"""
from src.ThirdModule.module3 import AnswerVerifier
return AnswerVerifier()
# ๊ฒฝ๋กœ ์„ค์ •
base_path = os.path.dirname(os.path.abspath(__file__))
data_path = os.path.join(base_path, 'Data')
misconception_csv_path = os.path.join(data_path, 'misconception_mapping.csv')
# ๋กœ๊น… ์„ค์ •
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# ์„ธ์…˜ ์ƒํƒœ ์ดˆ๊ธฐํ™” - ๊ฐ€์žฅ ๋จผ์ € ์‹คํ–‰๋˜๋„๋ก ์ตœ์ƒ๋‹จ์— ๋ฐฐ์น˜
if 'initialized' not in st.session_state:
st.session_state.initialized = True
st.session_state.wrong_questions = []
st.session_state.misconceptions = []
st.session_state.current_question_index = 0
st.session_state.generated_questions = []
st.session_state.current_step = 'initial'
st.session_state.selected_wrong_answer = None
st.session_state.questions = []
logger.info("Session state initialized")
# ๋ฌธ์ œ ์ƒ์„ฑ๊ธฐ ์ดˆ๊ธฐํ™”
@st.cache_resource
def load_question_generator():
"""๋ฌธ์ œ ์ƒ์„ฑ ๋ชจ๋ธ ๋กœ๋“œ"""
if not os.path.exists(misconception_csv_path):
st.error(f"CSV ํŒŒ์ผ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค: {misconception_csv_path}")
raise FileNotFoundError(f"CSV ํŒŒ์ผ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค: {misconception_csv_path}")
return SimilarQuestionGenerator(misconception_csv_path=misconception_csv_path)
# CSV ๋ฐ์ดํ„ฐ ๋กœ๋“œ ํ•จ์ˆ˜
@st.cache_data
def load_data(data_file = '/train.csv'):
try:
file_path = os.path.join(data_path, data_file.lstrip('/'))
df = pd.read_csv(file_path)
logger.info(f"Data loaded successfully from {file_path}")
return df
except FileNotFoundError:
st.error(f"ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {data_file}")
logger.error(f"File not found: {data_file}")
return None
def start_quiz():
"""ํ€ด์ฆˆ ์‹œ์ž‘ ๋ฐ ์ดˆ๊ธฐํ™”"""
df = load_data()
if df is None or df.empty:
st.error("๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ์…‹์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.")
return
st.session_state.questions = df.sample(n=10, random_state=42)
st.session_state.current_step = 'quiz'
st.session_state.current_question_index = 0
st.session_state.wrong_questions = []
st.session_state.misconceptions = []
st.session_state.generated_questions = []
logger.info("Quiz started")
def generate_similar_question(wrong_q, misconception_id, generator):
"""์œ ์‚ฌ ๋ฌธ์ œ ์ƒ์„ฑ"""
logger.info(f"Generating similar question for misconception_id: {misconception_id}")
# ์ž…๋ ฅ ๋ฐ์ดํ„ฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
if not isinstance(wrong_q, dict):
logger.error(f"Invalid wrong_q type: {type(wrong_q)}")
st.error("์œ ์‚ฌ ๋ฌธ์ œ ์ƒ์„ฑ์— ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ ํ˜•์‹์ด ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
return None
try:
# misconception_id๊ฐ€ ์—†๊ฑฐ๋‚˜ NaN์ธ ๊ฒฝ์šฐ ๋‹ค๋ฅธ misconception ์‚ฌ์šฉ
if pd.isna(misconception_id):
logger.info("Original misconception_id is NaN, trying to find alternative")
# ํ˜„์žฌ๊นŒ์ง€ ๋‚˜์˜จ misconception๋“ค ์ค‘์—์„œ ์„ ํƒ
available_misconceptions = [m for m in st.session_state.misconceptions if not pd.isna(m)]
if available_misconceptions:
# ๊ฐ€์žฅ ์ตœ๊ทผ์— ๋‚˜์˜จ misconception ์„ ํƒ
misconception_id = available_misconceptions[-1]
logger.info(f"Using alternative misconception_id: {misconception_id}")
else:
# ๊ธฐ๋ณธ misconception ID ์‚ฌ์šฉ (์˜ˆ: ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ misconception)
misconception_id = 2001 # ์ ์ ˆํ•œ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์ˆ˜์ • ํ•„์š”
logger.info(f"Using default misconception_id: {misconception_id}")
# ๋ฐ์ดํ„ฐ ์ค€๋น„ (ํŠœํ”Œ ๋ณ€ํ™˜ ๋ฐฉ์ง€)
input_data = {
'construct_name': str(wrong_q.get('ConstructName', '')),
'subject_name': str(wrong_q.get('SubjectName', '')),
'question_text': str(wrong_q.get('QuestionText', '')),
'correct_answer_text': str(wrong_q.get(f'Answer{wrong_q["CorrectAnswer"]}Text', '')),
'wrong_answer_text': str(wrong_q.get(f'Answer{st.session_state.selected_wrong_answer}Text', '')),
'misconception_id': int(misconception_id)
}
logger.info(f"Prepared input data: {input_data}")
with st.spinner("๐Ÿ“ ์œ ์‚ฌ ๋ฌธ์ œ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค..."):
# ์œ ์‚ฌ ๋ฌธ์ œ ์ƒ์„ฑ ํ˜ธ์ถœ
generated_q, _ = generator.generate_similar_question_with_text(
construct_name=input_data['construct_name'],
subject_name=input_data['subject_name'],
question_text=input_data['question_text'],
correct_answer_text=input_data['correct_answer_text'],
wrong_answer_text=input_data['wrong_answer_text'],
misconception_id=input_data['misconception_id']
)
if generated_q:
verifier = load_answer_verifier()
with st.status("๐Ÿค” AI๊ฐ€ ๋ฌธ์ œ๋ฅผ ๊ฒ€ํ† ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค..."):
st.write("๋‹ต์•ˆ์˜ ์ •ํ™•์„ฑ์„ ๊ฒ€์ฆํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค...")
verified_answer = verifier.verify_answer(
question=generated_q.question,
choices=generated_q.choices
)
if verified_answer:
logger.info(f"Answer verified: {verified_answer}")
st.write("โœ… ๊ฒ€์ฆ ์™„๋ฃŒ!")
result = {
'question': generated_q.question,
'choices': generated_q.choices,
'correct': verified_answer,
'explanation': generated_q.explanation
}
st.session_state['current_similar_question_answer'] = verified_answer
return result
else:
logger.warning("Answer verification failed, using original answer")
st.write("โš ๏ธ ๊ฒ€์ฆ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ์›๋ณธ ๋‹ต์•ˆ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.")
result = {
'question': generated_q.question,
'choices': generated_q.choices,
'correct': generated_q.correct_answer,
'explanation': generated_q.explanation
}
st.session_state['current_similar_question_answer'] = generated_q.correct_answer
return result
except Exception as e:
logger.error(f"Error in generate_similar_question: {str(e)}")
st.error(f"๋ฌธ์ œ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}")
return None
return None
def handle_answer(answer, current_q):
"""๋‹ต๋ณ€ ์ฒ˜๋ฆฌ"""
if answer != current_q['CorrectAnswer']:
wrong_q_dict = current_q.to_dict()
st.session_state.wrong_questions.append(wrong_q_dict)
st.session_state.selected_wrong_answer = answer
misconception_key = f'Misconception{answer}Id'
misconception_id = current_q.get(misconception_key)
st.session_state.misconceptions.append(misconception_id)
st.session_state.current_question_index += 1
if st.session_state.current_question_index >= 10:
st.session_state.current_step = 'review'
# ์ „์—ญ LaTeX ํฌ๋งทํ„ฐ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
latex_formatter = LatexFormatter()
def display_math_content(content: str):
"""์ˆ˜ํ•™ ๋‚ด์šฉ์„ ํ™”๋ฉด์— ํ‘œ์‹œ"""
formatted_content = latex_formatter.format_expression(content)
st.latex(formatted_content)
st.markdown(formatted_content, unsafe_allow_html=True)
def format_answer_choice(choice: str) -> str:
"""์„ ํƒ์ง€ LaTeX ํฌ๋งทํŒ…"""
return latex_formatter.format_expression(choice)
def display_question_and_answers(question_text: str, answers: dict):
"""
Display the question and its answer choices in a formatted layout.
Args:
question_text (str): LaTeX-formatted question text.
answers (dict): A dictionary of answer choices (keys: 'A', 'B', 'C', 'D').
"""
# Display the question
st.write("### ๋ฌธ์ œ")
st.latex(question_text)
# Arrange the answers in two columns
col1, col2 = st.columns(2)
with col1:
if st.button(f"A) {answers['A']}", key="A"):
return 'A'
if st.button(f"C) {answers['C']}", key="C"):
return 'C'
with col2:
if st.button(f"B) {answers['B']}", key="B"):
return 'B'
if st.button(f"D) {answers['D']}", key="D"):
return 'D'
return None
def main():
"""๋ฉ”์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋กœ์ง"""
st.title("MisconcepTutor")
# Generator ์ดˆ๊ธฐํ™”
generator = load_question_generator()
# ์ดˆ๊ธฐ ํ™”๋ฉด
if st.session_state.current_step == 'initial':
st.write("#### ํ•™์Šต์„ ์‹œ์ž‘ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. 10๊ฐœ์˜ ๋ฌธ์ œ๋ฅผ ํ’€์–ด๋ณผ๊นŒ์š”?")
if st.button("ํ•™์Šต ์‹œ์ž‘", key="start_quiz"):
start_quiz()
st.rerun()
# ํ€ด์ฆˆ ํ™”๋ฉด
# elif st.session_state.current_step == 'quiz':
# current_q = st.session_state.questions.iloc[st.session_state.current_question_index]
# # ์ง„ํ–‰ ์ƒํ™ฉ ํ‘œ์‹œ
# progress = st.session_state.current_question_index / 10
# st.progress(progress)
# st.write(f"### ๋ฌธ์ œ {st.session_state.current_question_index + 1}/10")
# # ๋ฌธ์ œ ํ‘œ์‹œ
# st.markdown("---")
# #display_math_question(current_q['QuestionText'])
# display_math_content(current_q['QuestionText']) # display_math_question ๋Œ€์‹  display_math_content ์‚ฌ์šฉ
# # ๋ณด๊ธฐ ํ‘œ์‹œ
# col1, col2 = st.columns(2)
# with col1:
# if st.button(f"A) {latex_formatter.format_expression(current_q['AnswerAText'])}", key="A"):
# handle_answer('A', current_q)
# st.rerun()
# if st.button(f"C) {latex_formatter.format_expression(current_q['AnswerCText'])}", key="C"):
# handle_answer('C', current_q)
# st.rerun()
# with col2:
# if st.button(f"B) {latex_formatter.format_expression(current_q['AnswerBText'])}", key="B"):
# handle_answer('B', current_q)
# st.rerun()
# if st.button(f"D) {latex_formatter.format_expression(current_q['AnswerDText'])}", key="D"):
# handle_answer('D', current_q)
# st.rerun()
# ํ€ด์ฆˆ ํ™”๋ฉด
elif st.session_state.current_step == 'quiz':
current_q = st.session_state.questions.iloc[st.session_state.current_question_index]
# ์ง„ํ–‰ ์ƒํ™ฉ ํ‘œ์‹œ
progress = st.session_state.current_question_index / 10
st.progress(progress)
st.write(f"### ๋ฌธ์ œ {st.session_state.current_question_index + 1}/10")
# Extract question and answers
question_text = current_q['QuestionText'] # LaTeX content for the question
answers = {
'A': current_q['AnswerAText'],
'B': current_q['AnswerBText'],
'C': current_q['AnswerCText'],
'D': current_q['AnswerDText']
}
# Display the question and answers
selected_answer = display_question_and_answers(question_text, answers)
# Handle answer selection
if selected_answer:
handle_answer(selected_answer, current_q)
st.rerun()
# ๋ณต์Šต ํ™”๋ฉด
elif st.session_state.current_step == 'review':
st.write("### ํ•™์Šต ๊ฒฐ๊ณผ")
# ๊ฒฐ๊ณผ ํ†ต๊ณ„
col1, col2, col3 = st.columns(3)
col1.metric("์ด ๋ฌธ์ œ ์ˆ˜", 10)
col2.metric("๋งž์€ ๋ฌธ์ œ", 10 - len(st.session_state.wrong_questions))
col3.metric("ํ‹€๋ฆฐ ๋ฌธ์ œ", len(st.session_state.wrong_questions))
# ๊ฒฐ๊ณผ์— ๋”ฐ๋ฅธ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ
if len(st.session_state.wrong_questions) == 0:
st.balloons() # ์ถ•ํ•˜ ํšจ๊ณผ
st.success("๐ŸŽ‰ ์ถ•ํ•˜ํ•ฉ๋‹ˆ๋‹ค! ๋ชจ๋“  ๋ฌธ์ œ๋ฅผ ๋งž์ถ”์…จ์–ด์š”!")
st.markdown("""
### ๐Ÿ† ์ˆ˜ํ•™์™•์ด์‹ญ๋‹ˆ๋‹ค!
์™„๋ฒฝํ•œ ์ ์ˆ˜๋ฅผ ๋ฐ›์œผ์…จ๋„ค์š”! ์ˆ˜ํ•™์  ๊ฐœ๋…์„ ์ •ํ™•ํ•˜๊ฒŒ ์ดํ•ดํ•˜๊ณ  ๊ณ„์‹  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.
""")
elif len(st.session_state.wrong_questions) <= 3:
st.success("์ž˜ ํ•˜์…จ์–ด์š”! ์กฐ๊ธˆ๋งŒ ๋” ์—ฐ์Šตํ•˜๋ฉด ์™„๋ฒฝํ•  ๊ฑฐ์˜ˆ์š”!")
else:
st.info("์ฒœ์ฒœํžˆ ๊ฐœ๋…์„ ๋ณต์Šตํ•ด๋ณด์•„์š”. ์—ฐ์Šตํ•˜๋‹ค ๋ณด๋ฉด ๋Š˜์–ด๋‚  ๊ฑฐ์˜ˆ์š”!")
# ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฒ„ํŠผ
col1, col2 = st.columns(2)
with col1:
if st.button("๐Ÿ”„ ์ƒˆ๋กœ์šด ๋ฌธ์ œ ์„ธํŠธ ์‹œ์ž‘ํ•˜๊ธฐ", use_container_width=True):
start_quiz()
st.rerun()
with col2:
if st.button("๐Ÿ  ์ฒ˜์Œ์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ", use_container_width=True):
st.session_state.clear()
st.rerun()
# ํ‹€๋ฆฐ ๋ฌธ์ œ ๋ถ„์„ ๋ถ€๋ถ„
if st.session_state.wrong_questions:
st.write("### โœ๏ธ ํ‹€๋ฆฐ ๋ฌธ์ œ ๋ถ„์„")
tabs = st.tabs([f"๐Ÿ“ ํ‹€๋ฆฐ ๋ฌธ์ œ #{i + 1}" for i in range(len(st.session_state.wrong_questions))])
for i, (tab, (wrong_q, misconception_id)) in enumerate(zip(
tabs,
zip(st.session_state.wrong_questions, st.session_state.misconceptions)
)):
with tab:
st.write("**๐Ÿ“‹ ๋ฌธ์ œ:**")
st.write(wrong_q['QuestionText'])
st.write("**โœ… ์ •๋‹ต:**", wrong_q['CorrectAnswer'])
st.write("---")
st.write("**๐Ÿ” ๊ด€๋ จ๋œ Misconception:**")
if misconception_id and not pd.isna(misconception_id):
misconception_text = generator.get_misconception_text(misconception_id)
st.info(f"Misconception ID: {int(misconception_id)}\n\n{misconception_text}")
else:
st.info("Misconception ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
if st.button(f"๐Ÿ“š ์œ ์‚ฌ ๋ฌธ์ œ ํ’€๊ธฐ", key=f"retry_{i}"):
st.session_state[f"show_similar_question_{i}"] = True
st.session_state[f"similar_question_answered_{i}"] = False
st.rerun()
if st.session_state.get(f"show_similar_question_{i}", False):
st.divider()
new_question = generate_similar_question(wrong_q, misconception_id, generator)
if new_question:
st.write("### ๐ŸŽฏ ์œ ์‚ฌ ๋ฌธ์ œ")
#st.write(new_question['question'])
display_math_question(new_question['question'])
# ๋‹ต๋ณ€ ์ƒํƒœ ํ™•์ธ
answered = st.session_state.get(f"similar_question_answered_{i}", False)
# ๋ณด๊ธฐ ํ‘œ์‹œ
st.write("**๋ณด๊ธฐ:**")
col1, col2 = st.columns(2)
# # ๋‹ต๋ณ€ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ์—๋งŒ ๋ฒ„ํŠผ ํ™œ์„ฑํ™”
# if not answered:
# with col1:
# for option in ['A', 'C']:
# if st.button(
# f"{option}) {new_question['choices'][option]}",
# key=f"similar_{option}_{i}"
# ):
# st.session_state[f"similar_question_answered_{i}"] = True
# st.session_state[f"selected_answer_{i}"] = option
# correct_answer = st.session_state.get('current_similar_question_answer')
# if option == correct_answer:
# st.session_state[f"is_correct_{i}"] = True
# else:
# st.session_state[f"is_correct_{i}"] = False
# st.rerun()
# with col2:
# for option in ['B', 'D']:
# if st.button(
# f"{option}) {new_question['choices'][option]}",
# key=f"similar_{option}_{i}"
# ):
# st.session_state[f"similar_question_answered_{i}"] = True
# st.session_state[f"selected_answer_{i}"] = option
# correct_answer = st.session_state.get('current_similar_question_answer')
# if option == correct_answer:
# st.session_state[f"is_correct_{i}"] = True
# else:
# st.session_state[f"is_correct_{i}"] = False
# st.rerun()
# ๋‹ต๋ณ€ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ์—๋งŒ ๋ฒ„ํŠผ ํ™œ์„ฑํ™”
if not answered:
with col1:
for option in ['A', 'C']:
if st.button(
f"{option}) {latex_formatter.format_expression(new_question['choices'][option])}",
key=f"similar_{option}_{i}"
):
st.session_state[f"similar_question_answered_{i}"] = True
st.session_state[f"selected_answer_{i}"] = option
correct_answer = st.session_state.get('current_similar_question_answer')
if option == correct_answer:
st.session_state[f"is_correct_{i}"] = True
else:
st.session_state[f"is_correct_{i}"] = False
st.rerun()
with col2:
for option in ['B', 'D']:
if st.button(
f"{option}) {format_math_expression(new_question['choices'][option])}",
key=f"similar_{option}_{i}"
):
st.session_state[f"similar_question_answered_{i}"] = True
st.session_state[f"selected_answer_{i}"] = option
correct_answer = st.session_state.get('current_similar_question_answer')
if option == correct_answer:
st.session_state[f"is_correct_{i}"] = True
else:
st.session_state[f"is_correct_{i}"] = False
st.rerun()
# ๋‹ต๋ณ€ํ•œ ๊ฒฝ์šฐ ๊ฒฐ๊ณผ ํ‘œ์‹œ
if answered:
is_correct = st.session_state.get(f"is_correct_{i}", False)
correct_answer = st.session_state.get('current_similar_question_answer')
if is_correct:
st.success("โœ… ์ •๋‹ต์ž…๋‹ˆ๋‹ค!")
else:
st.error(f"โŒ ํ‹€๋ ธ์Šต๋‹ˆ๋‹ค. ์ •๋‹ต์€ {correct_answer}์ž…๋‹ˆ๋‹ค.")
# ํ•ด์„ค ํ‘œ์‹œ
st.write("---")
st.write("**๐Ÿ“ ํ•ด์„ค:**", new_question['explanation'])
# ๋‹ค์‹œ ํ’€๊ธฐ ๋ฒ„ํŠผ
if st.button("๐Ÿ”„ ๋‹ค์‹œ ํ’€๊ธฐ", key=f"reset_{i}"):
st.session_state[f"similar_question_answered_{i}"] = False
st.session_state[f"selected_answer_{i}"] = None
st.session_state[f"is_correct_{i}"] = None
st.rerun()
# ๋ฌธ์ œ ๋‹ซ๊ธฐ ๋ฒ„ํŠผ
if st.button("โŒ ๋ฌธ์ œ ๋‹ซ๊ธฐ", key=f"close_{i}"):
st.session_state[f"show_similar_question_{i}"] = False
st.session_state[f"similar_question_answered_{i}"] = False
st.session_state[f"selected_answer_{i}"] = None
st.session_state[f"is_correct_{i}"] = None
st.rerun()
# ํ™”๋ฉด ์•„๋ž˜ ์—ฌ๋ฐฑ ์ถ”๊ฐ€
st.markdown("<br>" * 5, unsafe_allow_html=True) # 5์ค„์˜ ๋นˆ ์ค„ ์ถ”๊ฐ€
st.markdown("""
<div style="height: 100px;">
</div>
""", unsafe_allow_html=True) # ์ถ”๊ฐ€ ์—ฌ๋ฐฑ
else:
st.error("์œ ์‚ฌ ๋ฌธ์ œ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
if st.button("โŒ ๋‹ซ๊ธฐ", key=f"close_error_{i}"):
st.session_state[f"show_similar_question_{i}"] = False
st.rerun()
# ํ™”๋ฉด ์•„๋ž˜ ์—ฌ๋ฐฑ ์ถ”๊ฐ€
st.markdown("<br>" * 5, unsafe_allow_html=True) # 5์ค„์˜ ๋นˆ ์ค„ ์ถ”๊ฐ€
st.markdown("""
<div style="height: 100px;">
</div>
""", unsafe_allow_html=True) # ์ถ”๊ฐ€ ์—ฌ๋ฐฑ
if __name__ == "__main__":
main()
# random_state 42์—์„œ ์ •๋‹ต
# D C A A C
# A B B B B