144 lines
5.1 KiB
Python
144 lines
5.1 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""
|
||
|
|
Medical Records ETL for Wuhan Respiratory Disease Risk Prediction Platform
|
||
|
|
Processes outpatient and inpatient records to daily district-level counts.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import pandas as pd
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
BASE_DIR = Path("/home/akiba/CA")
|
||
|
|
OUTPATIENT_SRC = BASE_DIR / "Datas/view_门诊.xlsx"
|
||
|
|
INPATIENT_SRC = BASE_DIR / "Datas/view_住院.xlsx"
|
||
|
|
OUT_DIR = BASE_DIR / "processed/medical"
|
||
|
|
|
||
|
|
RESPIRATORY_KEYWORDS = [
|
||
|
|
"呼吸", "咳", "喘", "肺炎", "支气管", "咽痛", "感冒", "上呼吸道", "流感", "新冠"
|
||
|
|
]
|
||
|
|
|
||
|
|
RESPIRATORY_ICD_CODES = [f"J{i:02d}" for i in range(100)]
|
||
|
|
|
||
|
|
|
||
|
|
def is_respiratory_outpatient(chief_complaint: str) -> bool:
|
||
|
|
if pd.isna(chief_complaint):
|
||
|
|
return False
|
||
|
|
return any(kw in str(chief_complaint) for kw in RESPIRATORY_KEYWORDS)
|
||
|
|
|
||
|
|
|
||
|
|
def is_respiratory_icd(code: str) -> bool:
|
||
|
|
if pd.isna(code):
|
||
|
|
return False
|
||
|
|
code_str = str(code).strip().upper()
|
||
|
|
if not code_str:
|
||
|
|
return False
|
||
|
|
base_code = code_str.split(".")[0]
|
||
|
|
return base_code in RESPIRATORY_ICD_CODES
|
||
|
|
|
||
|
|
|
||
|
|
def extract_district(address: str) -> str:
|
||
|
|
"""Extract district name from address string."""
|
||
|
|
if pd.isna(address):
|
||
|
|
return ""
|
||
|
|
address = str(address)
|
||
|
|
wuhan_districts = [
|
||
|
|
"江岸区", "江汉区", "硚口区", "汉阳区", "武昌区", "青山区",
|
||
|
|
"洪山区", "东西湖区", "汉南区", "蔡甸区", "江夏区",
|
||
|
|
"黄陂区", "新洲区", "东湖高新区", "武汉经开区"
|
||
|
|
]
|
||
|
|
for district in wuhan_districts:
|
||
|
|
if district in address:
|
||
|
|
return district
|
||
|
|
for district in ["江岸", "江汉", "硚口", "汉阳", "武昌", "青山", "洪山",
|
||
|
|
"东西湖", "汉南", "蔡甸", "江夏", "黄陂", "新洲"]:
|
||
|
|
if district in address:
|
||
|
|
return district
|
||
|
|
return ""
|
||
|
|
|
||
|
|
|
||
|
|
def process_outpatient():
|
||
|
|
print("Loading outpatient data...")
|
||
|
|
df = pd.read_excel(OUTPATIENT_SRC)
|
||
|
|
print(f" Total outpatient records: {len(df):,}")
|
||
|
|
|
||
|
|
date_col = "门诊日期_re"
|
||
|
|
district_col = "现住址区"
|
||
|
|
complaint_col = "主诉"
|
||
|
|
|
||
|
|
print(" Filtering respiratory cases...")
|
||
|
|
df["is_respiratory"] = df[complaint_col].apply(is_respiratory_outpatient)
|
||
|
|
df_resp = df[df["is_respiratory"]].copy()
|
||
|
|
print(f" Respiratory outpatient records: {len(df_resp):,}")
|
||
|
|
|
||
|
|
df_resp["district"] = df_resp[district_col].apply(extract_district)
|
||
|
|
df_filtered = df_resp[df_resp["district"] != ""].copy()
|
||
|
|
print(f" Records with valid Wuhan district: {len(df_filtered):,}")
|
||
|
|
|
||
|
|
result = df_filtered.groupby([date_col, "district"]).size().reset_index(name="outpatient_count")
|
||
|
|
result.columns = ["date", "district", "outpatient_count"]
|
||
|
|
print(f" Aggregated to {len(result):,} date-district combinations")
|
||
|
|
|
||
|
|
return result
|
||
|
|
|
||
|
|
|
||
|
|
def process_inpatient():
|
||
|
|
print("Loading inpatient data...")
|
||
|
|
df = pd.read_excel(INPATIENT_SRC)
|
||
|
|
print(f" Total inpatient records: {len(df):,}")
|
||
|
|
|
||
|
|
date_col = "入院日期_re"
|
||
|
|
district_col = "现住址_脱敏"
|
||
|
|
icd_col = "诊断编码"
|
||
|
|
|
||
|
|
print(" Filtering respiratory cases (J00-J99)...")
|
||
|
|
df["is_respiratory"] = df[icd_col].apply(is_respiratory_icd)
|
||
|
|
df_resp = df[df["is_respiratory"]].copy()
|
||
|
|
print(f" Respiratory inpatient records: {len(df_resp):,}")
|
||
|
|
|
||
|
|
df_resp["district"] = df_resp[district_col].apply(extract_district)
|
||
|
|
df_filtered = df_resp[df_resp["district"] != ""].copy()
|
||
|
|
print(f" Records with valid Wuhan district: {len(df_filtered):,}")
|
||
|
|
|
||
|
|
result = df_filtered.groupby([date_col, "district"]).size().reset_index(name="inpatient_count")
|
||
|
|
result.columns = ["date", "district", "inpatient_count"]
|
||
|
|
print(f" Aggregated to {len(result):,} date-district combinations")
|
||
|
|
|
||
|
|
return result
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
print("=" * 60)
|
||
|
|
print("Medical Records ETL - Wuhan Respiratory Disease Platform")
|
||
|
|
print("=" * 60)
|
||
|
|
|
||
|
|
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||
|
|
|
||
|
|
print("\n[1/2] Processing outpatient records...")
|
||
|
|
outpatient_df = process_outpatient()
|
||
|
|
outpatient_path = OUT_DIR / "outpatient_daily.parquet"
|
||
|
|
outpatient_df.to_parquet(outpatient_path, index=False)
|
||
|
|
print(f" Saved: {outpatient_path}")
|
||
|
|
print(f" Records: {len(outpatient_df):,}, Cases: {outpatient_df['outpatient_count'].sum():,}")
|
||
|
|
|
||
|
|
print("\n[2/2] Processing inpatient records...")
|
||
|
|
inpatient_df = process_inpatient()
|
||
|
|
inpatient_path = OUT_DIR / "inpatient_daily.parquet"
|
||
|
|
inpatient_df.to_parquet(inpatient_path, index=False)
|
||
|
|
print(f" Saved: {inpatient_path}")
|
||
|
|
print(f" Records: {len(inpatient_df):,}, Cases: {inpatient_df['inpatient_count'].sum():,}")
|
||
|
|
|
||
|
|
combined = outpatient_df.merge(inpatient_df, on=["date", "district"], how="outer").fillna(0)
|
||
|
|
combined["outpatient_count"] = combined["outpatient_count"].astype(int)
|
||
|
|
combined["inpatient_count"] = combined["inpatient_count"].astype(int)
|
||
|
|
combined_path = OUT_DIR / "medical_daily.parquet"
|
||
|
|
combined.to_parquet(combined_path, index=False)
|
||
|
|
print(f"\n Combined saved: {combined_path}")
|
||
|
|
print(f" Total date-district combinations: {len(combined):,}")
|
||
|
|
|
||
|
|
print("\n" + "=" * 60)
|
||
|
|
print("ETL Complete!")
|
||
|
|
print("=" * 60)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|