# -*- coding: utf-8 -*- """生成贝朗相关产品模拟不良事件数据(合成数据,仅供分析/培训)。 依据:贝朗数据/生成不良事件模拟数据.md """ from __future__ import annotations import random from datetime import date, timedelta from pathlib import Path from openpyxl import Workbook from openpyxl.utils import get_column_letter ROOT = Path(__file__).resolve().parent HEADERS = [ "报告编码", "CC", "单位名称", "事业线", "产品名称", "注册证编号/曾用注册证编号", "注册人", "型号", "产品批号", "伤害", "伤害表现", "器械故障表现", "审核日期", ] # 产品名:基于贝朗中国常见产品线及行业通用命名归纳;具体注册信息以 NMPA 为准(本表为模拟) # events: (伤害表现K, 器械故障表现L);J 列固定为「是」(见提示词) PRODUCT_PROFILES: list[dict] = [ { "line": "输液治疗", "name": "一次性使用静脉留置针", "weight": 180, "models": ("4251624", "4251625", "Introcan Safety 20G", "Introcan 18G"), "events": [ ("穿刺部位疼痛", "套管与导管座连接处渗漏"), ("血肿", "回血观察窗模糊影响血流判断"), ("出血", "针尖保护装置回弹不畅"), ("红斑", "肝素帽旋紧后微量渗液"), ("瘙痒", "延长管打折致滴速下降"), ("水肿", "留置针固定翼粘贴失效"), ("静脉炎", "导管腔内回血阻力增高"), ("局部感染征象", "三通接头裂纹"), ], }, { "line": "输液治疗", "name": "一次性使用输液器", "weight": 160, "models": ("IS-7.0", "IS-5.0", "4053000"), "events": [ ("输液部位疼痛", "滴斗液面波动异常"), ("空气栓塞相关症状(已处理)", "管路接头松动致滴管内进气"), ("心悸", "流量调节轮锁止不良"), ("瘙痒", "精密过滤器外壳裂纹"), ("渗漏致皮肤红斑", "穿刺器与药瓶胶塞密封不严"), ("恶心", "滴速过快相关不适"), ("寒战", "输液管路可见异物附着"), ], }, { "line": "输液治疗", "name": "一次性使用精密过滤输液器", "weight": 90, "models": ("PF-5.0", "PF-7.0"), "events": [ ("低血压", "过滤膜侧压力升高致滴速骤降"), ("头痛", "排气孔堵塞需二次排气"), ("胸闷", "侧孔进气不畅"), ("发热", "过滤器下游管路温度异常升高"), ], }, { "line": "输液治疗", "name": "一次性使用输注延长管", "weight": 70, "models": ("EX-50cm", "EX-100cm"), "events": [ ("输注中断相关焦虑", "螺旋接口与泵管不匹配"), ("药物输注延迟", "延长管扭转触发流量报警"), ("局部肿胀", "接口渗液"), ], }, { "line": "输液治疗", "name": "一次性使用肠内营养输液器", "weight": 50, "models": ("EN-SET-1.2", "EN-SET-2.0"), "events": [ ("腹胀", "营养袋接口与泵管连接处渗漏"), ("呕吐", "滴速传感器识别不稳定"), ("腹泻", "营养液温度偏低相关不适"), ], }, { "line": "透析产品", "name": "血液透析器", "weight": 120, "models": ("Diacap Pro 1.3", "Diacap 1.4", "HD-180"), "events": [ ("透析中低血压", "跨膜压波动偏大"), ("出血", "动脉端管路接头渗液"), ("头痛", "透析液侧压力传感器报警"), ("肌肉痉挛", "透析器外壳细微裂纹"), ("恶心", "超滤率设置与监测不一致"), ("胸痛", "静脉压升高报警"), ], }, { "line": "透析产品", "name": "血液透析浓缩液", "weight": 60, "models": ("BIC-35", "ACID-8L"), "events": [ ("恶心", "浓缩液桶盖密封条变形渗漏"), ("呕吐", "电导度监测短暂漂移"), ("低血压", "透析液成分配比异常相关不适"), ], }, { "line": "透析产品", "name": "血液透析管路", "weight": 55, "models": ("AV-SET-A", "AV-SET-P"), "events": [ ("失血相关血红蛋白下降", "动脉壶液面持续下降"), ("出血", "泵管段磨损起皱"), ("寒战", "管路预冲残留气泡"), ], }, { "line": "外科产品", "name": "可吸收性外科缝线", "weight": 75, "models": ("Novosyn 3-0", "Monosyn 4-0"), "events": [ ("切口裂开", "缝线结滑脱"), ("疼痛", "缝针弯曲"), ("出血", "线体断裂残留"), ("感染征象", "缝线张力过早丧失"), ], }, { "line": "外科产品", "name": "非吸收性外科缝线", "weight": 50, "models": ("Premilene 3-0", "Dafilon 5-0"), "events": [ ("异物感", "线结切割组织"), ("水肿", "缝针与线体连接处松动"), ("红斑", "缝线表面粗糙刺激"), ], }, { "line": "外科产品", "name": "一次性使用无菌手术膜", "weight": 35, "models": ("OP-FILM-45", "OP-FILM-60"), "events": [ ("皮肤红斑", "粘性不足边缘翘起"), ("疼痛", "去除敷料时皮肤撕脱"), ("瘙痒", "敷料下汗液积聚刺激"), ], }, { "line": "诊断/监测耗材", "name": "一次性使用动脉采血器", "weight": 30, "models": ("ABG-3ml", "SAFE-ABG"), "events": [ ("血肿", "针头保护套脱落困难"), ("出血", "肝素化不足标本凝固需重新采血"), ("疼痛", "采血后桡动脉痉挛"), ], }, { "line": "输液治疗", "name": "一次性使用无菌注射器", "weight": 25, "models": ("10ml Luer", "20ml Luer"), "events": [ ("疼痛", "推杆卡顿致推注阻力骤增"), ("给药剂量偏差相关不适", "刻度印刷模糊"), ("局部肿胀", "针头与针座连接处渗漏"), ], }, ] REGISTRANTS = ( "贝朗医疗(上海)国际贸易有限公司", "贝朗爱敦(上海)医疗管理有限公司", "贝朗医疗(苏州)有限公司", ) HOSPITALS = ( "上海市第一人民医院", "浙江大学医学院附属第二医院", "四川大学华西医院", "广东省人民医院", "华中科技大学同济医学院附属协和医院", "中南大学湘雅医院", "山东大学齐鲁医院", "中国医科大学附属第一医院", "西安交通大学第一附属医院", "南京鼓楼医院", "福建省立医院", "重庆医科大学附属第一医院", "天津市肿瘤医院", "郑州大学第一附属医院", "昆明医科大学第一附属医院", ) CC_POOL = ( "质量反馈", "临床使用", "包装标识", "灭菌外观", "物流储运", "培训咨询", "不良事件", ) def weighted_products() -> list[dict]: pool: list[dict] = [] for p in PRODUCT_PROFILES: pool.extend([p] * p["weight"]) return pool def random_reg_number(rng: random.Random) -> str: kinds = ("国械注进20", "国械注进201", "国械注准20", "国械注准201") return f"{rng.choice(kinds)}{rng.randint(5, 9)}{rng.randint(100000, 999999)}" def random_batch(rng: random.Random) -> str: # 约 1% 概率重复上一批号(模拟同批聚集),其余唯一风格 return f"{rng.choice('ABCDEFGHJK')}{rng.randint(100000, 999999)}{rng.choice('0123456789')}" def random_workday(rng: random.Random, end: date) -> date: start = end - timedelta(days=365 * 2 + 120) d = start + timedelta(days=rng.randint(0, (end - start).days)) while d.weekday() >= 5: d -= timedelta(days=1) return d def build_rows(n: int, rng: random.Random, end_d: date) -> list[list]: pool = weighted_products() rows: list[list] = [] prev_batch: str | None = None for i in range(1, n + 1): prof = rng.choice(pool) harm_k, device_fault = rng.choice(prof["events"]) code = f"SIM-2024-{i:06d}" if prev_batch is not None and rng.random() < 0.01: batch = prev_batch else: batch = random_batch(rng) prev_batch = batch rows.append( [ code, rng.choice(CC_POOL), rng.choice(HOSPITALS), prof["line"], prof["name"], random_reg_number(rng), rng.choice(REGISTRANTS), rng.choice(prof["models"]), batch, "是", harm_k, device_fault, random_workday(rng, end_d), ] ) return rows def main() -> None: end_d = date.today() rng = random.Random(int(end_d.strftime("%Y%m%d"))) out_name = f"不良事件数据-模拟1000条-{end_d:%Y%m%d}.xlsx" out_path = ROOT / out_name wb = Workbook() ws = wb.active assert ws is not None ws.title = "POWER BI 总信息" ws.append(HEADERS) for row in build_rows(1000, rng, end_d): ws.append(row) m_col = HEADERS.index("审核日期") + 1 for r in range(2, 1002): c = ws.cell(row=r, column=m_col) if isinstance(c.value, date): c.number_format = "YYYY-MM-DD" for j, _ in enumerate(HEADERS, start=1): ws.column_dimensions[get_column_letter(j)].width = min(28, 12 + len(HEADERS[j - 1]) // 2) wb.save(out_path) print(f"Written: {out_path}") if __name__ == "__main__": main()