File size: 6,394 Bytes
bcb314a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# 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