Import sanitized HIS processing tools
This commit is contained in:
175
患者首页处理/数据处理工作区/04_质量体检/04_字段核验与数据库体检.py
Executable file
175
患者首页处理/数据处理工作区/04_质量体检/04_字段核验与数据库体检.py
Executable file
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
对 Patient_FrontPages 做字段级体检。
|
||||
|
||||
输出:
|
||||
- 数据处理结果区/05_质量体检/患者首页_数据库体检报告.txt
|
||||
- 数据处理结果区/05_质量体检/患者首页_字段空值统计.csv
|
||||
- 数据处理结果区/05_质量体检/患者首页_疑似异常记录.csv
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
DEFAULT_OUTPUT_DIR = PROJECT_ROOT / "数据处理结果区" / "05_质量体检"
|
||||
DEFAULT_TABLE = "Patient_FrontPages"
|
||||
|
||||
|
||||
def quote_pg_identifier(identifier: str) -> str:
|
||||
if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", identifier):
|
||||
raise ValueError(f"非法 PostgreSQL 表名:{identifier}")
|
||||
return '"' + identifier.replace('"', '""') + '"'
|
||||
|
||||
|
||||
def connection_env(args: argparse.Namespace) -> dict[str, str]:
|
||||
env = os.environ.copy()
|
||||
env.update(
|
||||
{
|
||||
"PGHOST": args.pg_host or os.environ.get("PGHOST", ""),
|
||||
"PGPORT": str(args.pg_port or os.environ.get("PGPORT", "")),
|
||||
"PGDATABASE": args.pg_database or os.environ.get("PGDATABASE", ""),
|
||||
"PGUSER": args.pg_user or os.environ.get("PGUSER", ""),
|
||||
"PGPASSWORD": args.pg_password or os.environ.get("PGPASSWORD", ""),
|
||||
}
|
||||
)
|
||||
missing = [name for name in ["PGHOST", "PGPORT", "PGDATABASE", "PGUSER", "PGPASSWORD"] if not env.get(name)]
|
||||
if missing:
|
||||
raise RuntimeError("缺少 PostgreSQL 连接配置:" + "、".join(missing))
|
||||
return env
|
||||
|
||||
|
||||
def run_psql(sql: str, args: argparse.Namespace) -> str:
|
||||
if shutil.which("psql") is None:
|
||||
raise RuntimeError("未找到 psql。请先安装 PostgreSQL 客户端。")
|
||||
with tempfile.TemporaryDirectory(prefix="patient_front_audit_") as tmpdir:
|
||||
sql_path = Path(tmpdir) / "audit.sql"
|
||||
sql_path.write_text(sql, encoding="utf-8")
|
||||
completed = subprocess.run(
|
||||
["psql", "--no-password", "--file", str(sql_path)],
|
||||
check=False,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
env=connection_env(args),
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
raise RuntimeError(completed.stderr.strip() or completed.stdout.strip() or "psql 执行失败")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def build_empty_stat_sql(table: str) -> str:
|
||||
monitored_columns = [
|
||||
"inpatient_no",
|
||||
"medical_record_no",
|
||||
"patient_name",
|
||||
"gender",
|
||||
"birth_date",
|
||||
"id_card_no",
|
||||
"contact_address",
|
||||
"admission_time",
|
||||
"discharge_time",
|
||||
"major_department",
|
||||
"primary_diagnosis",
|
||||
"primary_diagnosis_code",
|
||||
"discharge_diagnoses",
|
||||
"operations",
|
||||
"quality_status",
|
||||
"review_status",
|
||||
]
|
||||
selects = []
|
||||
for column in monitored_columns:
|
||||
identifier = quote_pg_identifier(column)
|
||||
if column in {"discharge_diagnoses", "operations"}:
|
||||
empty_expr = f"{identifier} IS NULL OR jsonb_array_length({identifier}) = 0"
|
||||
else:
|
||||
empty_expr = f"{identifier} IS NULL OR {identifier}::text = ''"
|
||||
selects.append(f"SELECT '{column}' AS column_name, count(*) FILTER (WHERE {empty_expr}) AS empty_count, count(*) AS total_count FROM {table}")
|
||||
return " UNION ALL ".join(selects)
|
||||
|
||||
|
||||
def audit(args: argparse.Namespace) -> None:
|
||||
output_dir = args.output_dir.resolve()
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
table = quote_pg_identifier(args.pg_table)
|
||||
empty_csv = output_dir / "患者首页_字段空值统计.csv"
|
||||
suspicious_csv = output_dir / "患者首页_疑似异常记录.csv"
|
||||
report_path = output_dir / "患者首页_数据库体检报告.txt"
|
||||
empty_stat_sql = build_empty_stat_sql(table)
|
||||
suspicious_sql = (
|
||||
"SELECT source_file, inpatient_no, medical_record_no, patient_name, review_status, "
|
||||
"review_notes::text AS review_notes, contact_address, primary_diagnosis, primary_diagnosis_code "
|
||||
f"FROM {table} "
|
||||
"WHERE review_status <> 'auto_pass' "
|
||||
"OR inpatient_no !~ '^ZY\\d{12}$' "
|
||||
"OR medical_record_no !~ '^\\d{10}$' "
|
||||
"OR contact_address ~ '急诊|门诊|其他医疗机构转入|医嘱离院' "
|
||||
"OR primary_diagnosis_code IS NULL "
|
||||
"OR primary_diagnosis_code = '' "
|
||||
"ORDER BY source_file"
|
||||
)
|
||||
|
||||
sql = f"""
|
||||
\\set ON_ERROR_STOP on
|
||||
\\pset pager off
|
||||
\\o {str(report_path).replace("'", "''")}
|
||||
SELECT '总记录数' AS item, count(*)::text AS value FROM {table}
|
||||
UNION ALL
|
||||
SELECT '需复核记录数', count(*)::text FROM {table} WHERE review_status <> 'auto_pass'
|
||||
UNION ALL
|
||||
SELECT '缺字段注释数', count(*)::text
|
||||
FROM pg_class c
|
||||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
JOIN pg_attribute a ON a.attrelid = c.oid
|
||||
WHERE n.nspname = 'public'
|
||||
AND c.relname = {args.pg_table!r}
|
||||
AND a.attnum > 0
|
||||
AND NOT a.attisdropped
|
||||
AND col_description(c.oid, a.attnum) IS NULL;
|
||||
\\o
|
||||
\\copy ({empty_stat_sql}) TO '{str(empty_csv).replace("'", "''")}' WITH (FORMAT csv, HEADER true, ENCODING 'UTF8')
|
||||
\\copy ({suspicious_sql}) TO '{str(suspicious_csv).replace("'", "''")}' WITH (FORMAT csv, HEADER true, ENCODING 'UTF8')
|
||||
""".strip()
|
||||
stdout = run_psql(sql, args)
|
||||
if stdout.strip():
|
||||
print(stdout.strip())
|
||||
print(f"体检报告:{report_path}")
|
||||
print(f"字段空值统计:{empty_csv}")
|
||||
print(f"疑似异常记录:{suspicious_csv}")
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description="核验 Patient_FrontPages 字段注释、空值和疑似错位记录。")
|
||||
parser.add_argument("--pg-host", default=os.environ.get("PGHOST", ""), help="PostgreSQL 主机;也可用 PGHOST")
|
||||
parser.add_argument("--pg-port", default=os.environ.get("PGPORT", "5432"), help="PostgreSQL 端口;也可用 PGPORT")
|
||||
parser.add_argument("--pg-database", default=os.environ.get("PGDATABASE", ""), help="PostgreSQL 数据库;也可用 PGDATABASE")
|
||||
parser.add_argument("--pg-user", default=os.environ.get("PGUSER", ""), help="PostgreSQL 用户名;也可用 PGUSER")
|
||||
parser.add_argument("--pg-password", default=os.environ.get("PGPASSWORD", ""), help="PostgreSQL 密码;也可用 PGPASSWORD")
|
||||
parser.add_argument("--pg-table", default=os.environ.get("PG_PATIENT_TABLE", DEFAULT_TABLE), help="PostgreSQL 表名")
|
||||
parser.add_argument("-o", "--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR, help="体检结果输出目录")
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
audit(build_parser().parse_args())
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(f"错误:{exc}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
Reference in New Issue
Block a user