stanceeval2026 / code /src /scorer.py
zaher-m's picture
Add files using upload-large-folder tool
7e9cfd1 verified
Raw
History Blame Contribute Delete
3.73 kB
"""Local reimplementation of the shared-task metric, so our offline scores
line up with the leaderboard.
Favg2 = (F1_Favor + F1_Against) / 2 -- the actual metric, no None
Favg3 = (F1_Favor + F1_Against + F1_None) / 3
"""
import argparse
import sys
import pandas as pd
from sklearn.metrics import accuracy_score, f1_score
VALID_LABELS = ["Against", "Favor", "None"]
LABEL2ID = {"Against": 0, "Favor": 1, "None": 2}
def compute_metrics(gold_labels, pred_labels):
y_true = [LABEL2ID[x] for x in gold_labels]
y_pred = [LABEL2ID[x] for x in pred_labels]
f_against = f1_score(y_true, y_pred, labels=[0], average="macro")
f_favor = f1_score(y_true, y_pred, labels=[1], average="macro")
f_none = f1_score(y_true, y_pred, labels=[2], average="macro")
return {
"Favg2": (f_favor + f_against) / 2.0,
"Favg3": (f_favor + f_against + f_none) / 3.0,
"F_Favor": f_favor,
"F_Against": f_against,
"F_None": f_none,
"Accuracy": accuracy_score(y_true, y_pred),
}
def score(gold_df, preds, verbose=True):
if len(preds) != len(gold_df):
raise ValueError(
f"Length mismatch: gold={len(gold_df)}, preds={len(preds)}"
)
invalid = sorted(set(preds) - set(VALID_LABELS))
if invalid:
raise ValueError(f"Invalid prediction labels: {invalid}")
gold_df = gold_df.copy()
gold_df["stance"] = gold_df["stance"].astype(str).str.strip()
gold_df["target"] = gold_df["target"].astype(str).str.strip()
gold_df["pred"] = preds
per_target = {}
for target in sorted(gold_df["target"].unique()):
sub = gold_df[gold_df["target"] == target]
per_target[target] = compute_metrics(
sub["stance"].tolist(), sub["pred"].tolist()
)
overall = compute_metrics(
gold_df["stance"].tolist(), gold_df["pred"].tolist()
)
if verbose:
for t, m in per_target.items():
print(
f" [{t}] Favg2={m['Favg2']:.4f} Favg3={m['Favg3']:.4f} "
f"(Fav={m['F_Favor']:.4f} Agn={m['F_Against']:.4f} "
f"None={m['F_None']:.4f}) Acc={m['Accuracy']:.4f}"
)
print(
f" OVERALL Favg2={overall['Favg2']:.4f} "
f"Favg3={overall['Favg3']:.4f} Acc={overall['Accuracy']:.4f}"
)
return {"overall": overall, "per_target": per_target}
def load_gold(csv_path):
df = pd.read_csv(csv_path, keep_default_na=False, encoding="utf-8-sig")
df.columns = df.columns.astype(str).str.strip()
for col in ("target", "stance"):
if col not in df.columns:
raise ValueError(f"Gold file must contain a '{col}' column.")
return df
def read_preds_txt(path):
with open(path, "r", encoding="utf-8") as f:
preds = [line.strip() for line in f if line.strip() != ""]
if preds and preds[0].lower() == "stance":
preds = preds[1:]
return preds
def validate_submission(preds, n_expected):
if len(preds) != n_expected:
return False, f"count {len(preds)} != expected {n_expected}"
invalid = sorted(set(preds) - set(VALID_LABELS))
if invalid:
return False, f"invalid labels: {invalid}"
return True, f"OK: {len(preds)} rows, labels in {VALID_LABELS}"
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--gold", required=True)
ap.add_argument("--pred", required=True)
args = ap.parse_args()
gold_df = load_gold(args.gold)
preds = read_preds_txt(args.pred)
ok, msg = validate_submission(preds, len(gold_df))
print(f"[validate] {msg}", file=sys.stderr)
if not ok:
sys.exit(1)
score(gold_df, preds)
if __name__ == "__main__":
main()