mirror of
https://github.com/microsoft/SkillOpt.git
synced 2026-07-08 00:49:57 +08:00
- Skill optimization framework with training loop analogy - 11 benchmarks, 4 model backends (Azure OpenAI, Claude, Codex, Qwen) - WebUI for browser-based training control - Pluggable architecture for extending benchmarks and backends
159 lines
5.4 KiB
Python
159 lines
5.4 KiB
Python
"""Cell-value evaluator faithful to the official SpreadsheetBench
|
|
`evaluation/evaluation.py` (https://github.com/RUCKBReasoning/SpreadsheetBench).
|
|
|
|
Key rules (copied from the official `transform_value` / `compare_cell_value`):
|
|
* numeric values (int/float and numeric strings) are compared after
|
|
``round(float(v), 2)`` — a fixed 2-decimal quantization (NOT a tolerance);
|
|
* ``datetime.time`` is stringified and the trailing microseconds stripped;
|
|
* ``datetime.datetime`` is converted to an Excel serial day and rounded
|
|
to an integer day;
|
|
* an empty string ``""`` and ``None`` are considered equal, but otherwise
|
|
``type(v1) != type(v2)`` fails the comparison.
|
|
|
|
Format/style comparison is deliberately NOT performed — the official
|
|
reference evaluator also skips it (the relevant lines are commented out
|
|
in `cell_level_compare`). soft vs hard is defined at the run_bench level
|
|
across a task's multiple test cases, not here.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import datetime
|
|
import os
|
|
import re
|
|
|
|
import openpyxl
|
|
|
|
|
|
# ---------- value transform / compare (official port) ----------
|
|
|
|
def _datetime_to_float(dt: datetime.datetime) -> float:
|
|
excel_start_date = datetime.datetime(1899, 12, 30)
|
|
delta = dt - excel_start_date
|
|
return delta.days + delta.seconds / 86400.0
|
|
|
|
|
|
def _transform_value(v):
|
|
if isinstance(v, bool):
|
|
# openpyxl can return Python bool; official code doesn't special-case
|
|
# bools, but round(float(True), 2) == 1.0 which breaks 1 vs True. Keep
|
|
# parity with the official transform by promoting bool -> float.
|
|
return round(float(v), 2)
|
|
if isinstance(v, (int, float)):
|
|
return round(float(v), 2)
|
|
if isinstance(v, datetime.time):
|
|
return str(v)[:-3]
|
|
if isinstance(v, datetime.datetime):
|
|
return round(_datetime_to_float(v), 0)
|
|
if isinstance(v, str):
|
|
try:
|
|
return round(float(v), 2)
|
|
except ValueError:
|
|
return v
|
|
return v
|
|
|
|
|
|
def _compare_cell_value(v1, v2) -> bool:
|
|
v1 = _transform_value(v1)
|
|
v2 = _transform_value(v2)
|
|
if (v1 == "" and v2 is None) or (v1 is None and v2 == ""):
|
|
return True
|
|
if (v1 == "" and v2 == "") or (v1 is None and v2 is None):
|
|
return True
|
|
if type(v1) is not type(v2):
|
|
return False
|
|
return v1 == v2
|
|
|
|
|
|
# ---------- range parsing (official port) ----------
|
|
|
|
def _col_num2name(n: int) -> str:
|
|
name = ""
|
|
while n > 0:
|
|
n, r = divmod(n - 1, 26)
|
|
name = chr(65 + r) + name
|
|
return name
|
|
|
|
|
|
def _col_name2num(name: str) -> int:
|
|
num = 0
|
|
for c in name:
|
|
num = num * 26 + (ord(c) - ord("A") + 1)
|
|
return num
|
|
|
|
|
|
def _parse_range(range_str: str):
|
|
start_cell, end_cell = range_str.split(":")
|
|
sc = "".join(ch for ch in start_cell if ch.isalpha())
|
|
sr = "".join(ch for ch in start_cell if ch.isdigit())
|
|
ec = "".join(ch for ch in end_cell if ch.isalpha())
|
|
er = "".join(ch for ch in end_cell if ch.isdigit())
|
|
return (_col_name2num(sc), int(sr)), (_col_name2num(ec), int(er))
|
|
|
|
|
|
def _generate_cell_names(range_str: str):
|
|
if ":" not in range_str:
|
|
return [range_str]
|
|
(sc, sr), (ec, er) = _parse_range(range_str)
|
|
cols = [_col_num2name(i) for i in range(sc, ec + 1)]
|
|
return [f"{c}{r}" for c in cols for r in range(sr, er + 1)]
|
|
|
|
|
|
def _cell_level_compare(wb_gt, wb_proc, sheet_name: str, cell_range: str):
|
|
if sheet_name not in wb_proc.sheetnames:
|
|
return False, f"worksheet not found: {sheet_name}"
|
|
ws_gt = wb_gt[sheet_name]
|
|
ws_proc = wb_proc[sheet_name]
|
|
for cn in _generate_cell_names(cell_range):
|
|
cg = ws_gt[cn]
|
|
cp = ws_proc[cn]
|
|
if not _compare_cell_value(cg.value, cp.value):
|
|
return False, f"value@{sheet_name}!{cn}: gt={cg.value!r} pred={cp.value!r}"
|
|
return True, ""
|
|
|
|
|
|
# ---------- public API ----------
|
|
|
|
def compare_workbooks(gt_file: str, proc_file: str, answer_position: str) -> tuple[bool, str]:
|
|
"""Return (ok, msg). Single test-case comparison, official semantics."""
|
|
if not os.path.exists(proc_file):
|
|
return False, "file not exist"
|
|
try:
|
|
wb_gt = openpyxl.load_workbook(filename=gt_file, data_only=True)
|
|
wb_proc = openpyxl.load_workbook(filename=proc_file, data_only=True)
|
|
except Exception as e: # noqa: BLE001
|
|
return False, f"load error: {e}"
|
|
try:
|
|
ok_all = True
|
|
msg_first = ""
|
|
for scr in (answer_position or "").split(","):
|
|
scr = scr.strip()
|
|
if not scr:
|
|
continue
|
|
if "!" in scr:
|
|
sheet_name, cell_range = scr.split("!", 1)
|
|
sheet_name = sheet_name.strip().strip("'\"")
|
|
else:
|
|
sheet_name = wb_gt.sheetnames[0]
|
|
cell_range = scr
|
|
cell_range = cell_range.strip().strip("'\"")
|
|
ok, msg = _cell_level_compare(wb_gt, wb_proc, sheet_name, cell_range)
|
|
if not ok:
|
|
ok_all = False
|
|
if not msg_first:
|
|
msg_first = msg
|
|
return ok_all, msg_first
|
|
finally:
|
|
wb_gt.close()
|
|
wb_proc.close()
|
|
|
|
|
|
def evaluate(pred_path: str, gold_path: str,
|
|
instruction_type: str, answer_position: str) -> dict:
|
|
"""Single test-case evaluate. soft/hard aggregation happens in run_bench."""
|
|
ok, msg = compare_workbooks(gold_path, pred_path, answer_position)
|
|
return {
|
|
"ok": ok,
|
|
"reason": msg,
|
|
"instruction_type": instruction_type,
|
|
}
|