exam-evaluator / src /data_cleaning.py
KarmanovaLidiia
Initial clean commit for HF Space (models via Git LFS)
bcb314a
# src/data_cleaning.py
import re
import pandas as pd
from bs4 import BeautifulSoup
# ⚠️ новый импорт: извлекаем речь тестируемого
from src.text_roles import extract_tester_reply
# ---------- утилиты очистки ----------
def clean_html(text: str) -> str:
"""Удаляем HTML/разметку из текста вопроса/ответа."""
if pd.isna(text):
return ""
return BeautifulSoup(str(text), "lxml").get_text(separator=" ", strip=True)
# эвристический парсер на случай, если в транскрипте есть роли
# (оставляем куски после "Тестируемый:/Кандидат:/Студент:" до следующего "Экзаменатор:")
_SPEAKER_PAT = re.compile(
r"(?:Тестируемый|Кандидат|Студент)\s*:\s*(.+?)(?=(?:Экзаменатор|Преподаватель|Собеседник)\s*:|$)",
re.IGNORECASE | re.DOTALL,
)
def extract_answer(transcript: str) -> str:
"""Базовое извлечение ответа из общей транскрипции (если есть метки ролей)."""
if not isinstance(transcript, str) or not transcript.strip():
return ""
t = transcript.replace("\r", "\n")
chunks = _SPEAKER_PAT.findall(t)
joined = " ".join(x.strip() for x in chunks) if chunks else t
return re.sub(r"\s+", " ", joined).strip()
# ---------- поиски колонок ----------
_CANDIDATES = {
"question_number": [
"номер вопроса", "порядковый номер", "порядковый номер вопроса",
"№ вопроса", "вопрос №", "номер", "question_number"
],
"question_text": [
"текст вопроса", "вопрос", "формулировка вопроса",
"question_text", "question"
],
"transcript": [
"транскрипция ответа", "транскрибация ответа", "транскрипт",
"диалог", "ответ (текст)", "аудио транскрипт", "текст ответа",
"transcript", "answer_text"
],
"score": [
"оценка", "оценка экзаменатора", "балл", "баллы",
"score", "target"
],
}
def _find_column(df: pd.DataFrame, keys: list[str]) -> str:
"""Ищем колонку по списку рус/англ вариантов (точно или по подстроке)."""
# уже стандартизированный файл? — возвращаем ключ, если он есть
for k in keys:
if k in df.columns:
return k
norm = {str(c).lower().strip(): c for c in df.columns}
for key in keys:
k = key.lower().strip()
if k in norm:
return norm[k]
for nk, orig in norm.items():
if k in nk: # частичное совпадение
return orig
raise KeyError(f"Не удалось найти колонку из набора: {keys} в {list(df.columns)}")
# ---------- основная функция ----------
def prepare_dataframe(df: pd.DataFrame) -> pd.DataFrame:
"""
Приводим датафрейм к стандартному виду:
columns = [question_number, question_text, answer_text, score]
Умеет работать и с «сырым» CSV из задания, и с уже обработанным,
где колонки могли быть: question_number, question_text, answer_text, score.
"""
cols = set(df.columns)
# кейс: файл уже в стандарте — просто мягко нормализуем
if {"question_number", "question_text", "answer_text", "score"}.issubset(cols):
out = df[["question_number", "question_text", "answer_text", "score"]].copy()
# подчистим HTML и лишние пробелы
out["question_text"] = out["question_text"].apply(clean_html).str.replace(r"\s+", " ", regex=True).str.strip()
out["answer_text"] = (
out["answer_text"]
.fillna("").astype(str)
.apply(clean_html)
.apply(extract_tester_reply) # ⚠️ извлекаем реплики тестируемого
.str.replace(r"\s+", " ", regex=True).str.strip()
)
# приведение типа номера вопроса (если возможно)
with pd.option_context("mode.chained_assignment", None):
try:
out["question_number"] = pd.to_numeric(out["question_number"], errors="coerce").astype("Int64")
except Exception:
pass
return out
# кейс: «сырой» файл из задания — ищем русские колонки
qnum_col = _find_column(df, _CANDIDATES["question_number"])
qtxt_col = _find_column(df, _CANDIDATES["question_text"])
tran_col = _find_column(df, _CANDIDATES["transcript"])
score_col = _find_column(df, _CANDIDATES["score"])
out = pd.DataFrame()
out["question_number"] = df[qnum_col]
out["question_text"] = df[qtxt_col].apply(clean_html)
# 1) базовое извлечение по ролям (если есть метки)
# 2) затем более мягкая эвристика extract_tester_reply (из src.text_roles)
out["answer_text"] = (
df[tran_col].apply(extract_answer)
.fillna("").astype(str)
.apply(clean_html)
.apply(extract_tester_reply)
)
out["score"] = df[score_col]
# финальная нормализация пробелов
out["question_text"] = out["question_text"].str.replace(r"\s+", " ", regex=True).str.strip()
out["answer_text"] = out["answer_text"].str.replace(r"\s+", " ", regex=True).str.strip()
# аккуратно приводим номер вопроса к целочисленному типу, если возможно
with pd.option_context("mode.chained_assignment", None):
try:
out["question_number"] = pd.to_numeric(out["question_number"], errors="coerce").astype("Int64")
except Exception:
pass
return out