"""観察シートの記録(LINE・メールで届いた文)を取り込み、試算の前提の表に入れる値の案を出す(2026-10-31) 使い方: python3 field_import.py <届いた文.txt> [<届いた文.txt> ...] # 取り込んで、案を作り直す python3 field_import.py # 案だけ作り直す field のフォルダは iCloud の サービスロボット導入シミュレーション/field/(店のデータは GitHub に入れない)。 - raw/ 届いた文の原文(取り込んだときに写す。直さない) - observations.csv 1行1回の記録(作業の時間・ロボットの往復)。同じ店・同じ日・同じ時刻・同じ作業の行は二重に入れない - store_conditions.csv 1通の記録ごとの、その店の条件(スタッフ・卓の数・時間帯・ようす・来店のペースなど)。今は記録だけ - proposal.md 前提の表に入れる値の案と、今の値との差(毎回作り直す) 決まり(2026-10-31 エイイチと決めた): - 1つの値は、測った回数の中央値で入れる - 店ごとに15回に届くまでは「参考」 - 標準の値(assumptions.json)を置き換える候補にするのは、2店以上・合わせて15回以上そろったとき。 自動では書き換えない。案と差を見て、エイイチが承認してから表を直す(basis を「現場」、n に回数、source に店の数と期間。店の名前は書かない) - 店ごとの値で試算を回すのは、今はしない(記録だけ) - 席へ案内は 2026-10-31 にシートの区切りを「席に着いて説明を始めたら〜テーブルを離れたら」に変えた(歩く時間を含まない=guide_s と同じ定義)。 それより前(2026-10-31 16:08 より前)の案内の記録は、歩く時間を含むので案に使わない """ import csv import json import os import re import shutil import statistics import sys from datetime import date, datetime HERE = os.path.dirname(os.path.abspath(__file__)) GOAL = 15 # 店ごとに「参考」を外す回数(シートの目安と同じ) MIN_STORES = 2 # 標準の値の候補にする店の数 GUIDE_FROM = datetime(2026, 10, 31, 16, 8) # これより前の案内の記録は、前の区切り(歩く時間を含む)。シートの直しが公開された時刻 # シートの作業の名前 → 前提の表の行(どれも中央値の秒をそのまま入れる) TASK_TO_KEY = { "会計": "pay_s", "片付け": "clear_s", "テーブルを拭く": "wipe_s", "席へ案内": "guide_s", "呼び出し": "call_s", "料理を置く": "staff_place_s", } ROBOT_TASK = "ロボットの往復" # ロボットの往復 → 前提の表の行(往復のうち、1回以上止まった・人が動かした往復の割合。試算も「1往復で起きる確率」) ROBOT_TO_KEY = {"stops": "pause_rate", "helps": "stuck_rate"} OBS_COLS = ["store", "date", "time", "kind", "sec", "stops", "helps", "raw_file"] COND_COLS = ["store", "date", "purpose", "time_of_day", "tags", "staff", "robots", "tables", "arrived", "left", "arrive_per_h", "left_per_h", "turnover_per_h", "stay_min", "occ0", "note", "raw_file"] def parse(text): """届いた文を読む。読めない行は飛ばす""" lines = [l.rstrip() for l in text.replace("\r\n", "\n").split("\n")] if not lines or "観察シートの記録" not in lines[0]: raise ValueError("観察シートの記録の文ではない(1行目が「観察シートの記録」でない)") info = {"store": "", "date": "", "purpose": "", "time_of_day": "", "tags": "", "staff": "", "robots": "", "tables": "", "note": "", "arrived": "", "left": "", "arrive_per_h": "", "left_per_h": "", "turnover_per_h": "", "stay_min": "", "occ0": ""} recs, sec = [], None for l in lines[1:]: if l.startswith("---"): sec = l continue if sec and "1回ずつの記録" in sec: m = re.match(r"(\d\d:\d\d:\d\d)\s+(\S+)\s+([\d.]+)秒(?:(止まった(\d+)回・人が動かした(\d+)回))?", l) if m: recs.append({"time": m[1], "kind": m[2], "sec": float(m[3]), "stops": m[4] or "", "helps": m[5] or ""}) continue if sec: continue if l.startswith("日付:"): y, mo, d = re.findall(r"\d+", l)[:3] info["date"] = f"{int(y):04d}-{int(mo):02d}-{int(d):02d}" elif l.startswith("目的:"): info["purpose"] = l[3:] elif l.startswith("お店:"): m = re.match(r"お店:(.*?) スタッフ(\d+)人(?: ロボット(\d+)台)?", l) if m: info["store"] = "" if m[1] == "(未入力)" else m[1].strip() info["staff"], info["robots"] = m[2], m[3] or "" elif l.startswith("卓の数:"): info["tables"] = (re.findall(r"\d+", l) or [""])[0] elif l.startswith("時間帯:"): a = l[4:].split(" ", 1) info["time_of_day"], info["tags"] = a[0], (a[1] if len(a) > 1 else "") elif l.startswith("メモ:"): info["note"] = l[3:] elif l.startswith("出入り:"): n = re.findall(r"(\d+)組", l) if len(n) >= 2: info["arrived"], info["left"] = n[0], n[1] m = re.search(r"数えはじめに(\d+)卓", l) info["occ0"] = m[1] if m else "" elif l.startswith("1時間あたり:"): n = re.findall(r"約(\d+)組", l) if len(n) >= 2: info["arrive_per_h"], info["left_per_h"] = n[0], n[1] elif l.startswith("回転率:"): info["turnover_per_h"] = (re.findall(r"[\d.]+", l) or [""])[0] elif l.startswith("滞在時間の目安:"): info["stay_min"] = (re.findall(r"\d+", l) or [""])[0] if not info["date"]: raise ValueError("日付の行がない") if not info["store"]: info["store"] = "名前なし_" + info["date"] return info, recs def read_csv(path, cols): if not os.path.exists(path): return [] with open(path, encoding="utf-8-sig", newline="") as f: return list(csv.DictReader(f)) def write_csv(path, cols, rows): with open(path + ".tmp", "w", encoding="utf-8-sig", newline="") as f: w = csv.DictWriter(f, fieldnames=cols) w.writeheader() w.writerows(rows) os.replace(path + ".tmp", path) def take(field, src): text = open(src, encoding="utf-8").read() info, recs = parse(text) os.makedirs(os.path.join(field, "raw"), exist_ok=True) base = f"{info['date']}_{re.sub(r'[^0-9A-Za-zぁ-んァ-ヶ一-龠ー_]', '', info['store'])}" raw_name, i = base + ".txt", 1 while os.path.exists(os.path.join(field, "raw", raw_name)): if open(os.path.join(field, "raw", raw_name), encoding="utf-8").read() == text: break # 同じ文はもう取り込んである i += 1 raw_name = f"{base}_{i}.txt" else: shutil.copyfile(src, os.path.join(field, "raw", raw_name)) obs_path = os.path.join(field, "observations.csv") obs = read_csv(obs_path, OBS_COLS) seen = {(r["store"], r["date"], r["time"], r["kind"]) for r in obs} added = 0 for r in recs: k = (info["store"], info["date"], r["time"], r["kind"]) if k in seen: continue seen.add(k) obs.append({"store": info["store"], "date": info["date"], "time": r["time"], "kind": r["kind"], "sec": r["sec"], "stops": r["stops"], "helps": r["helps"], "raw_file": raw_name}) added += 1 write_csv(obs_path, OBS_COLS, obs) cond_path = os.path.join(field, "store_conditions.csv") conds = [c for c in read_csv(cond_path, COND_COLS) if c["raw_file"] != raw_name] conds.append({**{c: info.get(c, "") for c in COND_COLS}, "raw_file": raw_name}) write_csv(cond_path, COND_COLS, conds) print(f"{src}: {info['store']} {info['date']} → 記録 {len(recs)} 回(新しく {added} 回)・原文 raw/{raw_name}") def fmt(v): return "—" if v is None else (f"{v:.3f}".rstrip("0").rstrip(".") if isinstance(v, float) else str(v)) def proposal(field): A = json.load(open(os.path.join(HERE, "assumptions.json"), encoding="utf-8")) obs = read_csv(os.path.join(field, "observations.csv"), OBS_COLS) L = ["# 前提の表に入れる値の案(観察シートの記録から)", "", f"作った日:{date.today().isoformat()} 記録 {len(obs)} 回", "", f"- 店ごとに {GOAL} 回に届くまでは「参考」。標準の値の候補は、{MIN_STORES}店以上・合わせて {GOAL} 回以上", "- 表はまだ書き換えていない。候補を表に入れるかはエイイチが決める(basis を「現場」、n に回数、source に店の数と期間。店の名前は書かない)", "- 中央値で比べる。席へ案内は 2026-10-31 16:08 より前の記録を使わない(歩く時間を含む前の区切り)", ""] def rows_for(kind): out = [r for r in obs if r["kind"] == kind] if kind == "席へ案内": out = [r for r in out if datetime.fromisoformat(f"{r['date']}T{r['time']}") >= GUIDE_FROM] return out L += ["## 作業の時間(秒)", "", "| 行 | シートの作業 | 今の値(根拠) | 全体の中央値 | 店の数 | 回数 | 判定 | 店ごと(中央値・回数) |", "|---|---|---|---|---|---|---|---|"] for task, key in TASK_TO_KEY.items(): rs = rows_for(task) cur = A["common"][key] by = {} for r in rs: by.setdefault(r["store"], []).append(float(r["sec"])) allv = [float(r["sec"]) for r in rs] med = statistics.median(allv) if allv else None judge = "候補" if len(by) >= MIN_STORES and len(allv) >= GOAL else ("参考" if allv else "記録なし") per = "、".join(f"{s} {fmt(statistics.median(v))}秒・{len(v)}回{'' if len(v) >= GOAL else '(参考)'}" for s, v in by.items()) L.append(f"| `{key}` | {task} | {cur['value']}({cur['basis']}) | {fmt(med)} | {len(by)} | {len(allv)} | {judge} | {per or '—'} |") L += ["", "## ロボット(往復のうち、起きた往復の割合)", "", "| 行 | 数え方 | 今の値(根拠) | 全体 | 店の数 | 往復の回数 | 判定 | 店ごと |", "|---|---|---|---|---|---|---|---|"] rs = [r for r in obs if r["kind"] == ROBOT_TASK] for f, key in ROBOT_TO_KEY.items(): cur = A["common"][key] by = {} for r in rs: by.setdefault(r["store"], []).append(1 if int(r[f] or 0) > 0 else 0) allv = [x for v in by.values() for x in v] rate = sum(allv) / len(allv) if allv else None judge = "候補" if len(by) >= MIN_STORES and len(allv) >= GOAL else ("参考" if allv else "記録なし") name = "1回以上止まった往復" if f == "stops" else "人が動かした往復" per = "、".join(f"{s} {fmt(sum(v) / len(v))}・{len(v)}往復" for s, v in by.items()) L.append(f"| `{key}` | {name} | {cur['value']}({cur['basis']}) | {fmt(rate)} | {len(by)} | {len(allv)} | {judge} | {per or '—'} |") secs = [float(r["sec"]) for r in rs] L += ["", f"ロボットの往復の時間(答え合わせ用・表には入れない):中央値 {fmt(statistics.median(secs)) if secs else '—'} 秒・{len(secs)} 往復"] conds = read_csv(os.path.join(field, "store_conditions.csv"), COND_COLS) L += ["", "## 店の条件(今は記録だけ。店ごとの値で試算は回さない)", "", "| 店 | 日付 | 時間帯・ようす | スタッフ | ロボット | 卓 | 来店/時 | 回転率/時 | 滞在の目安(分) |", "|---|---|---|---|---|---|---|---|---|"] for c in conds: L.append(f"| {c['store']} | {c['date']} | {c['time_of_day']} {c['tags']} | {c['staff']} | {c['robots'] or '—'} | {c['tables'] or '—'} " f"| {c['arrive_per_h'] or '—'} | {c['turnover_per_h'] or '—'} | {c['stay_min'] or '—'} |") open(os.path.join(field, "proposal.md"), "w", encoding="utf-8").write("\n".join(L) + "\n") print("proposal.md を作り直した") if __name__ == "__main__": field = sys.argv[1] os.makedirs(field, exist_ok=True) for src in sys.argv[2:]: take(field, src) proposal(field)