"""raw.csv + labels.csv -> features.csv (one row per measurement). python experiments/make_features.py data/demo_calibration python experiments/make_features.py data/2026-11_E1 --heater-steps 5 The folder must contain raw.csv (written by record.py, can be several days concatenated) and labels.csv (typed by hand, one row per sample_id). """ from __future__ import annotations import argparse import sys from pathlib import Path import pandas as pd sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from enose.features import build_feature_table, odor_columns # noqa: E402 def main() -> None: parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("folder", type=Path) parser.add_argument("--heater-steps", type=int, default=5) arguments = parser.parse_args() raw = pd.read_csv(arguments.folder / "raw.csv") labels = pd.read_csv(arguments.folder / "labels.csv") # A sample measured twice has more than one block of "purge" rows (two readings in the # same phase_second are normal: the device clock and the laptop clock drift a little). starts = raw["phase"].eq("purge") & raw["phase"].ne(raw["phase"].shift()) | raw["sample_id"].ne( raw["sample_id"].shift()) & raw["phase"].eq("purge") purge_blocks = starts.groupby(raw["sample_id"]).sum() merged = sorted(purge_blocks[purge_blocks > 1].index) if merged: print(f"注意:{len(merged)} 個樣本編號被量了不只一次,資料會混在一起,請改編號後重算:{merged[:5]}") short = raw[raw["phase"].isin(["purge", "sample"])].groupby(["sample_id", "phase"])["phase_second"].max() too_short = sorted(short[short < 60].index.get_level_values("sample_id").unique()) if too_short: print(f"注意:{len(too_short)} 個樣本的換氣或量測階段不到 60 秒:{too_short[:5]}") table = build_feature_table(raw, labels, arguments.heater_steps) missing = sorted(set(labels["sample_id"]) - set(table["sample_id"])) if missing: print(f"注意:labels.csv 有 {len(missing)} 個樣本在 raw.csv 找不到:{missing[:5]}…") broken = table[table[odor_columns(table)].isna().any(axis=1)] if len(broken): print(f"注意:{len(broken)} 次量測有缺值(可能某一階段太短):{list(broken['sample_id'][:5])}") table.to_csv(arguments.folder / "features.csv", index=False) print(f"已寫入 {arguments.folder / 'features.csv'}:{len(table)} 次量測 × {len(odor_columns(table))} 個氣味特徵") if __name__ == "__main__": main()