File size: 4,931 Bytes
ec0a9aa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
138
139
140
141
142
143
144
"""
Join task_labels.csv back to meta/episodes.jsonl, filter to the first
NUM_CHUNKS chunks, and sample SAMPLE_PER_LABEL episodes per label.

Usage:
    python scripts/join_labels_to_episodes.py

    # Optional flags:
    python scripts/join_labels_to_episodes.py \
        --episodes-jsonl   datasets/droid_1.0.1_20chunks/meta/episodes.jsonl \
        --task-labels-csv  scripts/task_labels.csv \
        --output-csv       scripts/episode_markovian_split.csv \
        --num-chunks       20 \
        --sample-per-label 130 \
        --seed             42
"""

import argparse
import json
from pathlib import Path

import pandas as pd


CHUNKS_SIZE = 1000  # episodes per chunk (from info.json)


def load_episodes(episodes_jsonl: Path, max_episode: int) -> pd.DataFrame:
    rows = []
    with open(episodes_jsonl) as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            obj = json.loads(line)
            ep_idx = obj.get("episode_index", -1)
            if ep_idx < 0 or ep_idx >= max_episode:
                continue
            length = obj.get("length")
            tasks = obj.get("tasks", [])
            task_text = next((t for t in tasks if t and t.strip()), "")
            rows.append(
                {
                    "episode_index": ep_idx,
                    "length": length,
                    "task": task_text,
                }
            )
    return pd.DataFrame(rows)


def main() -> None:
    parser = argparse.ArgumentParser(description="Join task labels to episodes and sample")
    parser.add_argument(
        "--episodes-jsonl",
        type=Path,
        default=Path("datasets/droid_1.0.1_20chunks/meta/episodes.jsonl"),
    )
    parser.add_argument(
        "--task-labels-csv",
        type=Path,
        default=Path("scripts/task_labels.csv"),
    )
    parser.add_argument(
        "--output-csv",
        type=Path,
        default=Path("scripts/episode_markovian_split.csv"),
    )
    parser.add_argument(
        "--num-chunks",
        type=int,
        default=20,
        help="Only include episodes from the first N chunks",
    )
    parser.add_argument(
        "--sample-per-label",
        type=int,
        default=130,
        help="Number of episodes to sample per label (0 = keep all)",
    )
    parser.add_argument("--seed", type=int, default=42)
    args = parser.parse_args()

    workspace = Path(__file__).parent.parent

    def resolve(p: Path) -> Path:
        return workspace / p if not p.is_absolute() else p

    episodes_jsonl = resolve(args.episodes_jsonl)
    task_labels_csv = resolve(args.task_labels_csv)
    output_csv = resolve(args.output_csv)
    output_csv.parent.mkdir(parents=True, exist_ok=True)

    max_episode = args.num_chunks * CHUNKS_SIZE
    print(f"[load] episodes (chunks 0–{args.num_chunks - 1}, episode_index < {max_episode})")
    episodes_df = load_episodes(episodes_jsonl, max_episode)
    print(f"       {len(episodes_df)} episodes loaded")

    print(f"[load] task labels: {task_labels_csv}")
    labels_df = pd.read_csv(task_labels_csv)
    print(f"       {len(labels_df)} task labels loaded")
    print(f"       label counts:\n{labels_df['label'].value_counts().to_string()}")

    # Join on task text (episodes.jsonl has no task_index field)
    merged = episodes_df.merge(
        labels_df[["task_index", "task", "label", "reason"]],
        on="task",
        how="left",
    )

    unmatched = merged["label"].isna().sum()
    if unmatched:
        print(f"[warn] {unmatched} episodes have no matching label (empty/unknown task)")
    merged["label"] = merged["label"].fillna("unknown")
    merged["reason"] = merged["reason"].fillna("")

    # Save full joined table before sampling
    full_csv = output_csv.with_name(output_csv.stem + "_full.csv")
    merged.sort_values("episode_index").to_csv(full_csv, index=False)
    print(f"\n[saved] full join → {full_csv}  ({len(merged)} rows)")
    print(merged["label"].value_counts().to_string())

    # Sample per label
    if args.sample_per_label > 0:
        sampled_parts = []
        for label, group in merged.groupby("label"):
            if label in ("unknown", "parse_error", "api_error"):
                continue
            n = min(args.sample_per_label, len(group))
            sampled_parts.append(group.sample(n=n, random_state=args.seed))
            print(f"[sample] {label}: {n}/{len(group)} episodes selected")

        sampled = pd.concat(sampled_parts).sort_values("episode_index").reset_index(drop=True)
        sampled.to_csv(output_csv, index=False)
        print(f"\n[saved] sampled → {output_csv}  ({len(sampled)} rows)")
        print(sampled["label"].value_counts().to_string())
    else:
        merged.sort_values("episode_index").to_csv(output_csv, index=False)
        print(f"\n[saved] {output_csv}  ({len(merged)} rows)")


if __name__ == "__main__":
    main()