CoolFace
Apppublic

leo-serafim/evolucaoalvorada

sourceHugging Faceupdated 8mo agoView on Hugging Face
0likes
relatorio_uti.ipynb29519 linesDownload Raw Back to root
1{2  "cells": [3    {4      "cell_type": "code",5      "source": [6        "# =========================================\n",7        "# EVOLUÇÃO MÉDICA UTI ALVORADA\n",8        "# =========================================\n",9        "import re, math, json, unicodedata\n",10        "import numpy as np, pandas as pd\n",11        "import warnings\n",12        "warnings.filterwarnings(\"ignore\", message=\"Downcasting behavior in `replace`\")\n",13        "\n",14        "import ipywidgets as widgets\n",15        "from IPython.display import display, HTML, Javascript\n",16        "from threading import Timer  # ### PATCH 11: debounce\n",17        "\n",18        "# ----------------- Estilo -----------------\n",19        "display(HTML(\"\"\"\n",20        "<style>\n",21        "  .wrap { width: 98%; }\n",22        "  .card { border:1px solid #ddd; border-radius:12px; padding:14px 18px; margin:10px 0; }\n",23        "  .output_area pre { font-family:'Courier New',monospace; font-size:15px; white-space:pre-wrap; line-height:1.45em; }\n",24        "  .h { margin:0 0 6px 0; color:#222; font-weight:600 }\n",25        "  .hint { color:#666; font-size:12px; }\n",26        "</style>\n",27        "\"\"\"))\n",28        "\n",29        "# -------------------------------------------\n",30        "# Cabeçalho (Paciente, Acompanhante, Peso, Gênero do Texto)\n",31        "# -------------------------------------------\n",32        "LABEL_W = '130px'\n",33        "INPUT_W_FULL = '100%'\n",34        "INPUT_W_MED  = '70%'\n",35        "\n",36        "titulo_top = widgets.HTML(\"<h3 class='h'>Cabeçalho</h3>\")\n",37        "\n",38        "nome_paciente = widgets.Text(\n",39        "    description=\"Paciente:\",\n",40        "    placeholder=\"Nome do paciente\",\n",41        "    style={'description_width': LABEL_W},\n",42        "    layout=widgets.Layout(width=INPUT_W_FULL)\n",43        ")\n",44        "nome_acomp = widgets.Text(\n",45        "    description=\"Acompanhante:\",\n",46        "    placeholder=\"(deixe em branco se não houver)\",\n",47        "    style={'description_width': LABEL_W},\n",48        "    layout=widgets.Layout(width=INPUT_W_MED)\n",49        ")\n",50        "peso_global = widgets.BoundedFloatText(\n",51        "    description=\"Peso (kg):\",\n",52        "    value=70.0, min=1, max=300, step=0.5,\n",53        "    style={'description_width': LABEL_W},\n",54        "    layout=widgets.Layout(width=\"260px\")\n",55        ")\n",56        "\n",57        "# Seleção de gênero do texto\n",58        "sexo_label = widgets.HTML(\n",59        "    value=f\"<div style='width:{LABEL_W};text-align:right;padding-right:8px;'>Texto:</div>\"\n",60        ")\n",61        "cb_masc = widgets.Checkbox(value=False, description=\"Masculino\", indent=False)\n",62        "cb_fem  = widgets.Checkbox(value=False, description=\"Feminino\",  indent=False)\n",63        "def _sync_sexo_left(ch):\n",64        "    if ch[\"new\"]:\n",65        "        cb_fem.value = False\n",66        "def _sync_sexo_right(ch):\n",67        "    if ch[\"new\"]:\n",68        "        cb_masc.value = False\n",69        "cb_masc.observe(_sync_sexo_left, names=\"value\")\n",70        "cb_fem.observe(_sync_sexo_right, names=\"value\")\n",71        "sexo_box = widgets.HBox([cb_masc, cb_fem], layout=widgets.Layout(gap=\"18px\"))\n",72        "sexo_row = widgets.HBox([sexo_label, sexo_box])\n",73        "\n",74        "def get_sexo():\n",75        "    if cb_masc.value and not cb_fem.value: return \"M\"\n",76        "    if cb_fem.value and not cb_masc.value: return \"F\"\n",77        "    return None\n",78        "\n",79        "header_box = widgets.VBox(\n",80        "    [titulo_top,\n",81        "     widgets.VBox([nome_paciente, nome_acomp, peso_global, sexo_row],\n",82        "                  layout=widgets.Layout(gap=\"6px\"))],\n",83        "    layout=widgets.Layout()\n",84        ")\n",85        "\n",86        "# ======================================================\n",87        "# CONTROLES / BALANÇO\n",88        "# ======================================================\n",89        "HOURS = [\"07:00\",\"08:00\",\"09:00\",\"10:00\",\"11:00\",\"12:00\",\n",90        "         \"13:00\",\"14:00\",\"15:00\",\"16:00\",\"17:00\",\"18:00\",\n",91        "         \"19:00\",\"20:00\",\"21:00\",\"22:00\",\"23:00\",\"00:00\",\n",92        "         \"01:00\",\"02:00\",\"03:00\",\"04:00\",\"05:00\",\"06:00\"]\n",93        "\n",94        "def split_blocks(text: str):\n",95        "    lines = [ln.strip() for ln in text.strip().splitlines() if ln.strip()]\n",96        "    blocks = {\"Sinais Vitais\": [], \"Ganhos\": [], \"Perdas\": []}\n",97        "    current, buffer = None, []\n",98        "    header_pattern = re.compile(r\"^(Sinais Vitais|Ganhos|Perdas)\\s+07:00\\b\", re.IGNORECASE)\n",99        "    stop_pattern = re.compile(r\"^(Ganhos\\s*:|Perdas\\s*:|Balanço|[0-9]+º\\s*Período|Resp\\.|Coren|Total Acum\\. Int\\.:|_{3,}|\"\n",100        "                              r\"Competência de:|Matrícula|Especialidade:|Nascimento:|Idade:|Sexo:|Leito:|Aferição de:|\"\n",101        "                              r\"Diagnóstico|Procedimento|Prescrição|Observa|Anota|Evolução|Resumo|Page\\s+\\d+)\", re.IGNORECASE)\n",102        "    for ln in lines:\n",103        "        if header_pattern.match(ln):\n",104        "            if current and buffer:\n",105        "                blocks[current].append(buffer[:]); buffer=[]\n",106        "            current = header_pattern.match(ln).group(1).title(); buffer.append(ln)\n",107        "        elif current:\n",108        "            if stop_pattern.match(ln):\n",109        "                if buffer: blocks[current].append(buffer[:]); buffer=[]; current=None\n",110        "            else:\n",111        "                buffer.append(ln)\n",112        "    if current and buffer: blocks[current].append(buffer[:])\n",113        "    return {k: (v if k!=\"Sinais Vitais\" else (v[0] if v else [])) for k,v in blocks.items()}\n",114        "\n",115        "# -------- Normalização e detectores de evacuação (robustos) --------\n",116        "META_ROW_RE = re.compile(r\"^(COMPETÊNCIA DE:|MATRÍCULA|ESPECIALIDADE|NASCIMENTO|IDADE|SEXO|LEITO|AFERIÇÃO DE:)\", re.IGNORECASE)\n",117        "def is_meta_row(idx:str)->bool: return bool(META_ROW_RE.match(idx))\n",118        "\n",119        "def _norm(s: str) -> str:\n",120        "    s = unicodedata.normalize(\"NFD\", s or \"\")\n",121        "    s = \"\".join(ch for ch in s if unicodedata.category(ch) != \"Mn\")\n",122        "    return s.upper().strip()\n",123        "\n",124        "def is_evac_like(idx: str) -> bool:\n",125        "    return _norm(idx).startswith(\"EVACUA\")\n",126        "\n",127        "def is_evac_qtde(idx: str) -> bool:\n",128        "    u = _norm(idx); u_ns = u.replace(\" \", \"\")\n",129        "    return is_evac_like(idx) and (\"QTDE\" in u or \"(0OU1)\" in u_ns or \"QUANTIDADE\" in u)\n",130        "\n",131        "def is_evac_volume(idx: str) -> bool:\n",132        "    u = _norm(idx); u_ns = u.replace(\" \", \"\")\n",133        "    return is_evac_like(idx) and not (\"QTDE\" in u or \"(0OU1)\" in u_ns or \"QUANTIDADE\" in u)\n",134        "\n",135        "# ### PATCH 3: parsing numérico “locale-friendly”\n",136        "_NUM_GROUPED_INT_RX = re.compile(r\"^\\d{1,3}(?:\\.\\d{3})+$\")\n",137        "def _num_or_nan(tok):\n",138        "    s = (tok or \"\").strip()\n",139        "    if s in (\"-\", \"\"): return np.nan\n",140        "    s = re.sub(r\"[^\\d,.\\-+]\", \"\", s)\n",141        "    if not s: return np.nan\n",142        "    # 1.200,5 → 1200.5 ; 1.200 → 1200\n",143        "    if \",\" in s:\n",144        "        s = s.replace(\".\", \"\").replace(\",\", \".\")\n",145        "    elif _NUM_GROUPED_INT_RX.fullmatch(s):\n",146        "        s = s.replace(\".\", \"\")\n",147        "    try:\n",148        "        return float(s)\n",149        "    except:\n",150        "        return np.nan\n",151        "\n",152        "_NUM_TOKEN_RX = re.compile(r\"^[-+]?\\d+(?:[.,]\\d+)?$|^-$\")  # só número tipo 36.4 / 37,2 / 120 / -  (não pega FIO2)\n",153        "\n",154        "def parse_table_block(block_lines):\n",155        "    if not block_lines: return pd.DataFrame()\n",156        "    cols = [\"item\"] + HOURS + [\"Subtotal\"]; rows=[]\n",157        "    for ln in block_lines[1:]:\n",158        "        parts = ln.split()\n",159        "        if not parts:\n",160        "            continue\n",161        "\n",162        "        item_tokens, num_tokens = [], []\n",163        "        for p in parts:\n",164        "            if _NUM_TOKEN_RX.match(p):\n",165        "                num_tokens.append(p)\n",166        "            else:\n",167        "                item_tokens.append(p)\n",168        "\n",169        "        item = \" \".join(item_tokens).strip()\n",170        "        nums = [_num_or_nan(s) for s in num_tokens]\n",171        "\n",172        "        hours_vals, subtotal_val = [], np.nan\n",173        "        if len(nums)==len(HOURS)+1:\n",174        "            hours_vals, subtotal_val = nums[:len(HOURS)], nums[len(HOURS)]\n",175        "        elif len(nums)<=len(HOURS):\n",176        "            hours_vals = nums\n",177        "        else:\n",178        "            hours_vals, subtotal_val = nums[:len(HOURS)], nums[-1]\n",179        "\n",180        "        if len(hours_vals)<len(HOURS):\n",181        "            hours_vals += [np.nan]*(len(HOURS)-len(hours_vals))\n",182        "\n",183        "        rows.append([item]+hours_vals+[subtotal_val])\n",184        "\n",185        "    df = pd.DataFrame(rows, columns=cols)\n",186        "\n",187        "    df[\"item\"]=(df[\"item\"]\n",188        "                .str.replace(r\"\\s*-\\s*SV$\",\"\",regex=True,case=False)\n",189        "                .str.replace(r\"\\s*-\\s*GANHOS$\",\"\",regex=True,case=False)\n",190        "                .str.replace(r\"\\s*-\\s*PERDAS$\",\"\",regex=True,case=False)\n",191        "                .str.replace(r\"\\s*SV$\",\"\",regex=True,case=False)\n",192        "                .str.replace(r\"\\s*GANHOS$\",\"\",regex=True,case=False)\n",193        "                .str.replace(r\"\\s*PERDAS$\",\"\",regex=True,case=False)\n",194        "                .str.replace(r\"\\s+\",\" \",regex=True).str.strip())\n",195        "\n",196        "    for c in HOURS+[\"Subtotal\"]:\n",197        "        df[c]=pd.to_numeric(df[c], errors=\"coerce\")\n",198        "    df.set_index(\"item\", inplace=True)\n",199        "    return df\n",200        "\n",201        "def combine_perdas_blocks(perdas_blocks):\n",202        "    dfs=[parse_table_block(blk) for blk in perdas_blocks if blk]\n",203        "    if not dfs: return pd.DataFrame()\n",204        "    big=pd.concat(dfs, sort=False).fillna(np.nan)\n",205        "\n",206        "    drop_idx=[]\n",207        "    for idx in big.index.unique():\n",208        "        u = _norm(idx); u_ns = u.replace(\" \", \"\")\n",209        "        if is_meta_row(idx) or \"ASPECTO\" in u or \"QUANTIDADE\" in u or \"(0OU1)\" in u_ns:\n",210        "            drop_idx.append(idx)\n",211        "    if drop_idx:\n",212        "        big = big.drop(index=list(set(drop_idx)), errors=\"ignore\")\n",213        "\n",214        "    horas_cols=HOURS; out={}\n",215        "    if \"DIURESE (ML)\" in big.index:\n",216        "        dius=big.loc[\"DIURESE (ML)\", horas_cols]\n",217        "        out[\"DIURESE (ML)\"]= dius.max(axis=0, skipna=True) if isinstance(dius,pd.DataFrame) else dius\n",218        "\n",219        "    for idx in big.index.unique():\n",220        "        if idx==\"DIURESE (ML)\" or is_evac_like(idx): continue\n",221        "        s=big.loc[idx, horas_cols]\n",222        "        out[idx]= s.sum(axis=0, skipna=True) if isinstance(s,pd.DataFrame) else s\n",223        "\n",224        "    df_perdas=pd.DataFrame(out).T if out else pd.DataFrame(index=[], columns=horas_cols)\n",225        "    for c in horas_cols:\n",226        "        if c not in df_perdas.columns: df_perdas[c]=np.nan\n",227        "    df_perdas=df_perdas[horas_cols]\n",228        "    return df_perdas\n",229        "\n",230        "# ### PATCH 7: SpO2 heurística segura (filtra <30 ou >100)\n",231        "def find_spo2_series(df_sv):\n",232        "    for cand in [\"SPO2\",\"SP O2\",\"SAT O2\",\"SATURAÇÃO\",\"SP\",\"SP 02\"]:\n",233        "        if cand in df_sv.index:\n",234        "            s = df_sv.loc[cand, HOURS].copy()\n",235        "            s = s.mask((s<30) | (s>100))\n",236        "            if s.dropna().empty:\n",237        "                continue\n",238        "            return s\n",239        "    for idx in df_sv.index:\n",240        "        up = idx.upper()\n",241        "        if \"SP\" in up or \"SAT\" in up:\n",242        "            s = df_sv.loc[idx, HOURS].copy()\n",243        "            s = s.mask((s<30) | (s>100))\n",244        "            if s.dropna().empty:\n",245        "                continue\n",246        "            return s\n",247        "    return None\n",248        "\n",249        "def extract_bh_24h_segundo_periodo(text):\n",250        "    label_iter=list(re.finditer(r\"Balanço Total de 24hs\\s*:\", text))\n",251        "    if not label_iter: return float(\"nan\")\n",252        "    tail=text[label_iter[-1].end():]; stop=re.search(r\"[0-9]+º\\s*Período\", tail)\n",253        "    segment=tail[:stop.start()] if stop else tail[:200]\n",254        "    nums=re.findall(r\"([\\-+]?\\d+(?:\\.\\d+)?)\", segment)\n",255        "    if nums: return float(nums[-1])\n",256        "    vals=re.findall(r\"Balanço Total de 24hs\\s*:\\s*([\\-+]?\\d+(?:\\.\\d+)?)\", text)\n",257        "    return float(vals[-1]) if vals else float(\"nan\")\n",258        "\n",259        "def extract_diurese_total_from_raw(text):\n",260        "    totals=[]\n",261        "    for raw_line in text.splitlines():\n",262        "        line=raw_line.strip()\n",263        "        if not re.match(r\"^DIURESE\", line, flags=re.IGNORECASE): continue\n",264        "        nums=re.findall(r\"[-+]?\\d+(?:\\.\\d+)?\", line)\n",265        "        if not nums: continue\n",266        "        try:\n",267        "            subtotal=float(nums[-1])\n",268        "            if len(nums)>=2: totals.append(subtotal)\n",269        "        except:\n",270        "            try: totals.append(sum(float(x) for x in nums))\n",271        "            except: pass\n",272        "    return float(max(totals)) if totals else float(\"nan\")\n",273        "\n",274        "# -------- EVACUAÇÃO: eventos + volume (corrigido para subtotal e QTDE) --------\n",275        "def extract_evac_events_volume_from_raw(text):\n",276        "    lines=[ln.strip() for ln in text.splitlines() if ln.strip()]\n",277        "    found_any=False\n",278        "    vol_lists=[]\n",279        "    qtde_lists=[]\n",280        "\n",281        "    for ln in lines:\n",282        "        head=_norm(ln); head_ns=head.replace(\" \", \"\")\n",283        "        if not head.startswith(\"EVACUA\"):\n",284        "            continue\n",285        "        found_any=True\n",286        "\n",287        "        clean=re.sub(r\"\\([^)]*\\)\",\"\",ln)\n",288        "        nums=re.findall(r\"[-+]?\\d+(?:\\.\\d+)?\", clean)\n",289        "        if not nums:\n",290        "            continue\n",291        "\n",292        "        is_qtde = (\"QTDE\" in head or \"(0OU1)\" in head_ns or \"QUANTIDADE\" in head)\n",293        "        if is_qtde:\n",294        "            qtde_lists.append([int(float(x)) for x in nums])\n",295        "        else:\n",296        "            vol_lists.append([float(x) for x in nums])\n",297        "\n",298        "    # Volume e eventos a partir das linhas com volume\n",299        "    volume_sum = 0.0\n",300        "    events_from_volume = 0\n",301        "    for vals in vol_lists:\n",302        "        if not vals:\n",303        "            continue\n",304        "        if len(vals) >= 2 and abs(sum(vals[:-1]) - vals[-1]) <= 1e-6:\n",305        "            # Último é subtotal ⇒ soma só o subtotal e conta eventos pelos anteriores\n",306        "            volume_sum += vals[-1]\n",307        "            events_from_volume += sum(1 for v in vals[:-1] if v > 0)\n",308        "        else:\n",309        "            volume_sum += sum(v for v in vals if v > 0)\n",310        "            events_from_volume += sum(1 for v in vals if v > 0)\n",311        "\n",312        "    # Eventos a partir das linhas QTDE (0/1), ignorando subtotal final se houver\n",313        "    eventos_qtde = 0\n",314        "    if qtde_lists:\n",315        "        trimmed=[]\n",316        "        for lst in qtde_lists:\n",317        "            if len(lst)>=2 and sum(lst[:-1]) == lst[-1]:\n",318        "                lst = lst[:-1]\n",319        "            trimmed.append(lst)\n",320        "        if trimmed:\n",321        "            max_len = max(len(lst) for lst in trimmed)\n",322        "            arr = np.zeros((len(trimmed), max_len), dtype=int)\n",323        "            for i,lst in enumerate(trimmed):\n",324        "                arr[i,:len(lst)] = lst\n",325        "            eventos_qtde = int(arr.max(axis=0).sum())\n",326        "\n",327        "    if not (vol_lists or qtde_lists):\n",328        "        return 0, None, bool(found_any)\n",329        "\n",330        "    eventos = eventos_qtde if eventos_qtde > 0 else events_from_volume\n",331        "    volume = volume_sum if volume_sum > 0 else None\n",332        "    return int(eventos), (float(volume) if volume is not None else None), True\n",333        "\n",334        "# -------- OUTRAS PERDAS (DRENO, HEMODIÁLISE, etc.) direto do texto bruto --------\n",335        "def extract_other_losses_from_raw(text):\n",336        "    \"\"\"\n",337        "    Lê o texto bruto do balanço e extrai TODAS as perdas que não sejam:\n",338        "    - DIURESE\n",339        "    - EVACUAÇÃO / EVACUAÇÕES\n",340        "\n",341        "    Ignora:\n",342        "    - linhas-resumo \"Perdas : 1202\"\n",343        "    - descrições genéricas tipo \"Perdas Subtotal\"\n",344        "    - linhas de ganhos\n",345        "\n",346        "    Retorna: dict {nome_perda: volume_total_ml}\n",347        "    (sempre usando o subtotal quando ele existe para evitar dobrar valores)\n",348        "    \"\"\"\n",349        "    lines = [ln.strip() for ln in text.splitlines() if ln.strip()]\n",350        "    losses = {}\n",351        "\n",352        "    for ln in lines:\n",353        "        up = ln.upper()\n",354        "\n",355        "        # Tem que ter indicação de PERD / DRENO\n",356        "        if (\"PERD\" not in up) and (\"DRENO\" not in up) and (\"DRENOS\" not in up):\n",357        "            continue\n",358        "\n",359        "        # Ignorar DIURESE\n",360        "        if up.startswith(\"DIURESE\"):\n",361        "            continue\n",362        "\n",363        "        # Ignorar EVACUAÇÕES / EVACUAÇÃO (tratadas em outra função)\n",364        "        if up.startswith(\"EVACUACAO\") or up.startswith(\"EVACUAÇÃO\") or up.startswith(\"EVACUACOES\"):\n",365        "            continue\n",366        "        if \"ASPECTO DA EVACUACAO\" in up or \"EVACUACAO - QTDE\" in up:\n",367        "            continue\n",368        "\n",369        "        # Ignorar linhas-resumo genéricas de Perdas\n",370        "        if up.startswith(\"PERDAS :\") or up.startswith(\"PERDAS:\"):\n",371        "            continue\n",372        "\n",373        "        parts = ln.split()\n",374        "        item_tokens, num_tokens = [], []\n",375        "        for p in parts:\n",376        "            if re.search(r\"[\\d\\-+]\", p):\n",377        "                num_tokens.append(p)\n",378        "            else:\n",379        "                item_tokens.append(p)\n",380        "\n",381        "        if not num_tokens:\n",382        "            continue\n",383        "\n",384        "        nums = [_num_or_nan(p) for p in num_tokens]\n",385        "        nums_clean = [float(n) for n in nums if not math.isnan(n)]\n",386        "        if not nums_clean:\n",387        "            continue\n",388        "\n",389        "        # Se o último é igual à soma dos anteriores, assumimos que é SUBTOTAL\n",390        "        if len(nums_clean) >= 2 and abs(sum(nums_clean[:-1]) - nums_clean[-1]) <= 1e-6:\n",391        "            vol = nums_clean[-1]\n",392        "        else:\n",393        "            vol = sum(nums_clean)\n",394        "\n",395        "        if vol <= 0:\n",396        "            continue\n",397        "\n",398        "        name = \" \".join(item_tokens)\n",399        "        name = re.sub(r\"\\s*-\\s*PERDAS?$\", \"\", name, flags=re.IGNORECASE)\n",400        "        name = re.sub(r\"\\s*-\\s*PERD$\", \"\", name, flags=re.IGNORECASE)\n",401        "        name = re.sub(r\"\\s+PERDAS?$\", \"\", name, flags=re.IGNORECASE)\n",402        "        name = re.sub(r\"\\s+PERD$\", \"\", name, flags=re.IGNORECASE)\n",403        "        name = re.sub(r\"\\s+\", \" \", name).strip()\n",404        "        if not name:\n",405        "            continue\n",406        "\n",407        "        # Filtrar nomes muito genéricos que não queremos no relatório\n",408        "        nu = name.upper().replace(\" \", \"\")\n",409        "        if nu in (\"PERDAS\", \"PERDA\", \"PERDASSUBTOTAL\", \"PERDASSOMA\") or \"SUBTOTAL\" in nu:\n",410        "            continue\n",411        "        if nu.startswith(\"GANHOS\"):\n",412        "            continue\n",413        "\n",414        "        # Se aparecer mais de uma linha com o mesmo nome, usamos o MAIOR valor\n",415        "        prev = losses.get(name)\n",416        "        if prev is None:\n",417        "            losses[name] = vol\n",418        "        else:\n",419        "            losses[name] = max(prev, vol)\n",420        "\n",421        "    return losses\n",422        "\n",423        "def count_grau_hours_from_raw(text):\n",424        "    for raw_line in text.splitlines():\n",425        "        line=raw_line.strip()\n",426        "        if re.match(r\"^GRAU DE CABECEIRA\", line, flags=re.IGNORECASE):\n",427        "            nums=re.findall(r\"[-+]?\\d+(?:\\.\\d+)?\", line)\n",428        "            return len(nums)\n",429        "    return 0\n",430        "\n",431        "# ### PATCH 4: Ranges mais bonitos\n",432        "def _fmt_range(vmin, vmax, is_temp=False):\n",433        "    def _f(x, t):\n",434        "        if t:  # temperatura com 1 casa\n",435        "            return f\"{x:.1f}\"\n",436        "        return f\"{int(x)}\" if abs(x-round(x))<1e-9 else f\"{x:.0f}\"\n",437        "    return f\"{_f(vmin,is_temp)}–{_f(vmax,is_temp)}\"\n",438        "\n",439        "def vminmax(series, is_temp=False):\n",440        "    s=series.dropna()\n",441        "    if s.empty: return \"NA\"\n",442        "    return _fmt_range(s.min(), s.max(), is_temp)\n",443        "\n",444        "def build_report(df_sv, df_g, df_p_allblocks, raw):\n",445        "    horas=int(df_sv.loc[\"GRAU DE CABECEIRA\", HOURS].count()) if \"GRAU DE CABECEIRA\" in df_sv.index else 0\n",446        "    horas=max(horas, count_grau_hours_from_raw(raw))\n",447        "\n",448        "    glic_series = df_sv.loc[\"GLICEMIA\",HOURS].dropna().tolist() if \"GLICEMIA\" in df_sv.index else []\n",449        "    glic_str=\", \".join([(str(int(x)) if float(x).is_integer() else f\"{x:.1f}\") for x in glic_series]) or \"NA\"\n",450        "    temp_str=vminmax(df_sv.loc[\"TEMPERATURA\",HOURS], True) if \"TEMPERATURA\" in df_sv.index else \"NA\"\n",451        "    fc_str=vminmax(df_sv.loc[\"FC\",HOURS]) if \"FC\" in df_sv.index else \"NA\"\n",452        "    pam_str=vminmax(df_sv.loc[\"PAM\",HOURS]) if \"PAM\" in df_sv.index else \"NA\"\n",453        "    fr_str=vminmax(df_sv.loc[\"FR\",HOURS]) if \"FR\" in df_sv.index else \"NA\"\n",454        "    spo2_s=find_spo2_series(df_sv); spo2_str=vminmax(spo2_s) if spo2_s is not None else \"NA\"\n",455        "    df_p=df_p_allblocks\n",456        "\n",457        "    # ### PATCH 8: diurese — soma confiável mesmo sem Subtotal\n",458        "    diur=extract_diurese_total_from_raw(raw)\n",459        "    if np.isnan(diur) and isinstance(df_p,pd.DataFrame) and not df_p.empty and \"DIURESE (ML)\" in df_p.index:\n",460        "        diu_series = pd.to_numeric(df_p.loc[\"DIURESE (ML)\",HOURS], errors=\"coerce\")\n",461        "        diur=float(diu_series.sum(skipna=True))\n",462        "    diu_str=\"NA\" if np.isnan(diur) else f\"{int(round(diur))}\"\n",463        "\n",464        "    eventos, volume_evac, had_evac_info = extract_evac_events_volume_from_raw(raw)\n",465        "\n",466        "    # Outras perdas (drenos, hemodiálise, etc) direto do texto bruto\n",467        "    outras_perdas_dict = extract_other_losses_from_raw(raw)\n",468        "    outras_perdas = [\n",469        "        (name.title(), int(round(vol)))\n",470        "        for name, vol in outras_perdas_dict.items()\n",471        "    ]\n",472        "\n",473        "    bh=extract_bh_24h_segundo_periodo(raw)\n",474        "    bh_str=\"NA\" if np.isnan(bh) else ((\"+\" if (round(bh,1)>0) else \"\") + f\"{str(round(bh,1)).rstrip('0').rstrip('.')} ml\")\n",475        "    lines=[f\"CONTROLES {horas or 24} HORAS:\",\n",476        "           f\"- Glic: {glic_str} mg/dl\",\n",477        "           f\"- T: {temp_str} °C\",\n",478        "           f\"- FC: {fc_str} bpm\",\n",479        "           f\"- PAM: {pam_str} mmHg\",\n",480        "           f\"- FR: {fr_str} ipm\",\n",481        "           f\"- sO2: {spo2_str} %\",\n",482        "           f\"- DIU: {diu_str} ml\"]\n",483        "    if had_evac_info:\n",484        "        if eventos==0: lines.append(\"- Evac: 0 eventos\")\n",485        "        elif volume_evac is None: lines.append(f\"- Evac: {eventos} evento(s) (sem volume anotado no balanço hídrico)\")\n",486        "        else: lines.append(f\"- Evac: {eventos} evento(s) ({int(round(volume_evac))} ml)\")\n",487        "    for nome,tot in outras_perdas:\n",488        "        lines.append(f\"- {nome}: {tot} ml\")\n",489        "    lines.append(f\"- BH: {bh_str}\")\n",490        "    return \"\\n\".join(lines), diur, (eventos, had_evac_info), df_p\n",491        "\n",492        "# ---- UI CONTROLES ----\n",493        "titulo_controles = widgets.HTML(\"<h3 class='h'>CONTROLES</h3>\")\n",494        "ta_controles = widgets.Textarea(placeholder=\"Cole aqui o texto bruto do balanço...\",\n",495        "                                layout=widgets.Layout(width=\"100%\", height=\"220px\"))\n",496        "box_controles = widgets.VBox([titulo_controles, ta_controles], layout=widgets.Layout())\n",497        "\n",498        "# =========================================\n",499        "# Checkboxes exclusivos (pares/grupos)\n",500        "# =========================================\n",501        "def exclusive_pair(label_left, label_right, value_left=True):\n",502        "    cb_left = widgets.Checkbox(value=value_left, description=label_left, indent=False)\n",503        "    cb_right= widgets.Checkbox(value=not value_left, description=label_right, indent=False)\n",504        "    def _sync_left(ch):\n",505        "        if ch[\"new\"]:\n",506        "            cb_right.value = False\n",507        "        elif not cb_right.value:\n",508        "            cb_left.value = True\n",509        "    def _sync_right(ch):\n",510        "        if ch[\"new\"]:\n",511        "            cb_left.value = False\n",512        "        elif not cb_left.value:\n",513        "            cb_right.value = True\n",514        "    cb_left.observe(_sync_left, names=\"value\")\n",515        "    cb_right.observe(_sync_right, names=\"value\")\n",516        "    box = widgets.HBox([cb_left, cb_right], layout=widgets.Layout(gap=\"18px\"))\n",517        "    return box, cb_left, cb_right\n",518        "\n",519        "def make_exclusive_check_group(options, default=None):\n",520        "    cbs = [widgets.Checkbox(value=(opt==default), description=opt, indent=False) for opt in options]\n",521        "    def _sync(change, i):\n",522        "        if change[\"new\"]:\n",523        "            for j,cb in enumerate(cbs):\n",524        "                if j!=i: cb.value=False\n",525        "        elif not any(cb.value for cb in cbs):\n",526        "            cbs[i].value=True\n",527        "    for i,cb in enumerate(cbs):\n",528        "        cb.observe(lambda ch, i=i: _sync(ch,i), names=\"value\")\n",529        "    box = widgets.VBox(cbs, layout=widgets.Layout(margin=\"0 0 0 20px\"))\n",530        "    return box, cbs\n",531        "\n",532        "def current_choice(cb_left, cb_right, left_value, right_value):\n",533        "    return left_value if cb_left.value else right_value\n",534        "\n",535        "# =========================================\n",536        "# NÍVEL DE CONSCIÊNCIA\n",537        "# =========================================\n",538        "titulo_sedo = widgets.HTML(\"<h3 class='h'>NÍVEL DE CONSCIÊNCIA</h3>\")\n",539        "pair_sedo, cb_cons, cb_sedo = exclusive_pair(\"sem sedoanalgesia\", \"Em uso de sedoanalgesia\", value_left=True)\n",540        "\n",541        "# ---- MODO SEM SEDOANALGESIA: 3 colunas ----\n",542        "col1_box, col1_cbs = make_exclusive_check_group(\n",543        "    [\"Consciente\", \"Pouco contactuante mas desperta a estímulos\", \"Inconsciente\"],\n",544        "    default=\"Consciente\"\n",545        ")\n",546        "col1_title = widgets.HTML(\"<b>Estado de consciência</b>\")\n",547        "col1 = widgets.VBox([col1_title, col1_box])\n",548        "\n",549        "def _get_col1_text():\n",550        "    for cb in col1_cbs:\n",551        "        if cb.value:\n",552        "            return cb.description\n",553        "    return \"Consciente\"\n",554        "\n",555        "col2_box, col2_cbs = make_exclusive_check_group(\n",556        "    [\"orientado(a)\", \"algo desorientado(a)\", \"em delirium (CAM-ICU positivo)\"],\n",557        "    default=\"orientado(a)\"\n",558        ")\n",559        "col2_title = widgets.HTML(\"<b>Orientação</b>\")\n",560        "col2 = widgets.VBox([col2_title, col2_box])\n",561        "\n",562        "def _get_col2_text():\n",563        "    for cb in col2_cbs:\n",564        "        if cb.value:\n",565        "            return cb.description\n",566        "    return \"orientado(a)\"\n",567        "\n",568        "col3_opts_box, col3_cbs = make_exclusive_check_group(\n",569        "    [\"sem queixas no momento\", \"se queixando de:\"],\n",570        "    default=\"sem queixas no momento\"\n",571        ")\n",572        "col3_title = widgets.HTML(\"<b>Queixas</b>\")\n",573        "col3_text = widgets.Text(\n",574        "    value=\"\",\n",575        "    placeholder=\"Descreva as queixas (ex.: cefaleia, dor torácica...)\",\n",576        "    layout=widgets.Layout(width=\"100%\")\n",577        ")\n",578        "col3_text.layout.display = \"none\"\n",579        "col3 = widgets.VBox([col3_title, col3_opts_box, col3_text])\n",580        "\n",581        "def _col3_toggle_textarea(*_):\n",582        "    show = any(cb.value and cb.description.startswith(\"se queixando\") for cb in col3_cbs)\n",583        "    col3_text.layout.display = \"block\" if show else \"none\"\n",584        "\n",585        "for cb in col3_cbs:\n",586        "    cb.observe(lambda ch: _col3_toggle_textarea(), names=\"value\")\n",587        "_col3_toggle_textarea()\n",588        "\n",589        "def _get_col3_text():\n",590        "    if any(cb.value and cb.description.startswith(\"se queixando\") for cb in col3_cbs):\n",591        "        queixa = (col3_text.value or \"\").strip()\n",592        "        if queixa:\n",593        "            return f\"com queixa de {queixa}\"\n",594        "        return \"com queixa referida\"\n",595        "    return \"sem queixas no momento\"\n",596        "\n",597        "def _update_columns_enabled(*_):\n",598        "    is_inconsciente = (_get_col1_text() == \"Inconsciente\")\n",599        "    for cb in col2_cbs + col3_cbs:\n",600        "        cb.disabled = is_inconsciente\n",601        "    col3_text.disabled = is_inconsciente\n",602        "    if is_inconsciente:\n",603        "        for cb in col2_cbs:\n",604        "            cb.value = (cb.description == \"orientado(a)\")\n",605        "        for cb in col3_cbs:\n",606        "            cb.value = (cb.description == \"sem queixas no momento\")\n",607        "        col3_text.value = \"\"\n",608        "    _col3_toggle_textarea()\n",609        "\n",610        "for cb in col1_cbs:\n",611        "    cb.observe(lambda ch: _update_columns_enabled(), names=\"value\")\n",612        "_update_columns_enabled()\n",613        "\n",614        "cons_extra = widgets.VBox([\n",615        "    widgets.HTML(\"<b>Preencha o estado atual:</b>\"),\n",616        "    widgets.HBox([col1, col2, col3], layout=widgets.Layout(gap=\"24px\"))\n",617        "])\n",618        "cons_extra.layout.display = \"flex\" if cb_cons.value else \"none\"\n",619        "\n",620        "# ---- MODO COM SEDO ----\n",621        "rass_box = widgets.BoundedIntText(value=0, min=-5, max=4, description='RASS:', layout=widgets.Layout(width=\"160px\"))\n",622        "bps_box  = widgets.BoundedIntText(value=0, min=0,  max=12, description='BPS:',  layout=widgets.Layout(width=\"160px\"))\n",623        "\n",624        "DRUGS_SEDO = {\n",625        "    \"Fentanil\":         {\"unidade\": \"mcg/kg/h\",   \"ampola_mg\": 0.05, \"ampola_ml\": 10},\n",626        "    \"Midazolam\":        {\"unidade\": \"mg/kg/h\",    \"ampola_mg\": 5,    \"ampola_ml\": 10},\n",627        "    \"Propofol\":         {\"unidade\": \"mcg/kg/min\", \"no_dil\": True, \"conc_fixed_mg_ml\": 10.0},\n",628        "    \"Dextrocetamina\":   {\"unidade\": \"mg/kg/h\",    \"ampola_mg\": 50,   \"ampola_ml\": 2},\n",629        "    \"Dexmedetomidina\":  {\"unidade\": \"mcg/kg/min\", \"ampola_mg\": 0.1,  \"ampola_ml\": 2},\n",630        "    \"Atracúrio\":        {\"unidade\": \"mcg/kg/min\", \"ampola_mg\": 10,   \"ampola_ml\": 2.5},\n",631        "    \"Rocurônio\":        {\"unidade\": \"mcg/kg/min\", \"ampola_mg\": 10,   \"ampola_ml\": 5},\n",632        "    \"Cisatracúrio\":     {\"unidade\": \"mcg/kg/min\", \"ampola_mg\": 2,    \"ampola_ml\": 10},\n",633        "}\n",634        "def make_drug_row(name, conf):\n",635        "    check = widgets.Checkbox(value=False, description=f\"{name}\", indent=False,\n",636        "                             layout=widgets.Layout(width='170px'))\n",637        "    amps  = widgets.BoundedFloatText(value=0, min=0, step=1,\n",638        "                                     layout=widgets.Layout(width='90px'))\n",639        "    dil   = widgets.BoundedFloatText(value=0, min=0, step=1,\n",640        "                                     layout=widgets.Layout(width='110px'))\n",641        "    inf   = widgets.BoundedFloatText(value=0, min=0, step=0.1,\n",642        "                                     layout=widgets.Layout(width='110px'))\n",643        "\n",644        "    lbl_amp = widgets.HTML(\"Ampolas:\", layout=widgets.Layout(width='90px'))\n",645        "    lbl_dil = widgets.HTML(\"Diluente (mL):\", layout=widgets.Layout(width='120px'))\n",646        "    lbl_inf = widgets.HTML(\"Infusão (mL/h):\", layout=widgets.Layout(width='120px'))\n",647        "\n",648        "    if conf.get(\"no_dil\", False):\n",649        "        hint_txt = \"Assumindo Propofol 1% (10 mg/mL) sem diluição\"\n",650        "    else:\n",651        "        hint_txt = f\"Assumindo {conf['ampola_mg']} mg/mL · ampola {conf['ampola_ml']} mL\"\n",652        "    hint = widgets.HTML(f\"<span class='hint'>{hint_txt}</span>\")\n",653        "\n",654        "    row = widgets.GridBox(\n",655        "        children=[check, lbl_amp, amps, lbl_dil, dil, lbl_inf, inf, hint],\n",656        "        layout=widgets.Layout(\n",657        "            grid_template_columns=\"170px 90px 90px 120px 110px 120px 110px auto\",\n",658        "            align_items=\"center\",\n",659        "            grid_gap=\"6px\"\n",660        "        )\n",661        "    )\n",662        "\n",663        "    if conf.get(\"no_dil\", False):\n",664        "        for w in (lbl_amp, amps, lbl_dil, dil):\n",665        "            w.layout.visibility = \"hidden\"\n",666        "            if hasattr(w, \"disabled\"):\n",667        "                w.disabled = True\n",668        "        inf.disabled = True\n",669        "        def _toggle(change, c=inf):\n",670        "            enabled = change[\"new\"]\n",671        "            c.disabled = not enabled\n",672        "            if not enabled:\n",673        "                c.value = 0\n",674        "        check.observe(_toggle, names=\"value\")\n",675        "    else:\n",676        "        for w in (amps, dil, inf):\n",677        "            w.disabled = True\n",678        "        def _toggle(change, a=amps, b=dil, c=inf):\n",679        "            enabled = change[\"new\"]\n",680        "            for w in (a, b, c): w.disabled = not enabled\n",681        "            if not enabled: a.value = b.value = c.value = 0\n",682        "        check.observe(_toggle, names=\"value\")\n",683        "\n",684        "    return {\"name\": name, \"check\": check, \"amps\": amps, \"dil\": dil, \"inf\": inf,\n",685        "            \"conf\": conf, \"row\": row}\n",686        "\n",687        "drug_rows = [make_drug_row(k, v) for k, v in DRUGS_SEDO.items()]\n",688        "drugs_box = widgets.VBox([r[\"row\"] for r in drug_rows])\n",689        "\n",690        "sedo_extra = widgets.VBox([\n",691        "    widgets.HTML(\"<b>Escalas:</b>\"),\n",692        "    widgets.HBox([rass_box, bps_box], layout=widgets.Layout(gap='15px')),\n",693        "    widgets.HTML(\"<b>Drogas (marque e preencha):</b>\"),\n",694        "    drugs_box\n",695        "])\n",696        "sedo_extra.layout.display = \"none\"\n",697        "\n",698        "def _toggle_cons_sedo_vis(*_):\n",699        "    cons_extra.layout.display = \"flex\" if cb_cons.value else \"none\"\n",700        "    sedo_extra.layout.display = \"flex\" if cb_sedo.value else \"none\"\n",701        "cb_cons.observe(lambda ch:_toggle_cons_sedo_vis(), names=\"value\")\n",702        "cb_sedo.observe(lambda ch:_toggle_cons_sedo_vis(), names=\"value\")\n",703        "_toggle_cons_sedo_vis()\n",704        "\n",705        "box_sedo = widgets.VBox([titulo_sedo, pair_sedo, cons_extra, sedo_extra], layout=widgets.Layout())\n",706        "\n",707        "# =========================================\n",708        "# HEMODINÂMICA\n",709        "# =========================================\n",710        "titulo_hemo = widgets.HTML(\"<h3 class='h'>HEMODINÂMICA</h3>\")\n",711        "pair_hemo, cb_sem, cb_com = exclusive_pair(\n",712        "    \"Hemodinamicamente sem uso de drogas\",\n",713        "    \"Em uso de drogas vasoativa/inotrópica\",\n",714        "    value_left=True\n",715        ")\n",716        "\n",717        "DRUGS_HEMO = [\n",718        "    {\"key\":\"noradrenalina\", \"label\":\"noradrenalina\", \"conc\":1.0,   \"amp_ml\":4.0,  \"unit_mode\":\"mcg_kg_min\"},\n",719        "    {\"key\":\"adrenalina\",    \"label\":\"adrenalina\",    \"conc\":1.0,   \"amp_ml\":1.0,  \"unit_mode\":\"mcg_kg_min\"},\n",720        "    {\"key\":\"dopamina\",      \"label\":\"dopamina\",      \"conc\":50.0,  \"amp_ml\":5.0,  \"unit_mode\":\"mcg_kg_min\"},\n",721        "    {\"key\":\"dobutamina\",    \"label\":\"dobutamina\",    \"conc\":12.5,  \"amp_ml\":20.0, \"unit_mode\":\"mcg_kg_min\"},\n",722        "    {\"key\":\"milrinona\",     \"label\":\"milrinona\",     \"conc\":1.0,   \"amp_ml\":10.0, \"unit_mode\":\"mcg_kg_min\"},\n",723        "    {\"key\":\"vasopressina\",  \"label\":\"vasopressina\",  \"conc\":20.0,  \"amp_ml\":1.0,  \"unit_mode\":\"ui_min\"},\n",724        "    {\"key\":\"nitroglicerina\",\"label\":\"nitroglicerina\",\"conc\":5.0,   \"amp_ml\":10.0, \"unit_mode\":\"mcg_min\"},\n",725        "    {\"key\":\"nitroprussiato\",\"label\":\"nitroprussiato\",\"conc\":25.0,  \"amp_ml\":2.0,  \"unit_mode\":\"mcg_kg_min\"},\n",726        "]\n",727        "_HEMO_GRID = \"170px 90px 90px 120px 110px 120px 110px auto\"\n",728        "\n",729        "def _FT(desc, width):\n",730        "    return widgets.FloatText(\n",731        "        value=0.0, step=0.1, description=desc,\n",732        "        style={'description_width':'96px'},\n",733        "        layout=widgets.Layout(width=width)\n",734        "    )\n",735        "\n",736        "def make_hemo_row(cfg):\n",737        "    cb   = widgets.Checkbox(value=False, description=cfg[\"label\"], indent=False,\n",738        "                            layout=widgets.Layout(width=\"170px\"))\n",739        "    lblA = widgets.HTML(\"Ampolas:\",       layout=widgets.Layout(width=\"90px\"))\n",740        "    amp  = _FT(\"\", \"90px\"); amp.layout.margin=\"0\"\n",741        "    lblD = widgets.HTML(\"Diluente (mL):\", layout=widgets.Layout(width=\"120px\"))\n",742        "    dil  = _FT(\"\", \"110px\"); dil.layout.margin=\"0\"\n",743        "    lblI = widgets.HTML(\"Infusão (mL/h):\",layout=widgets.Layout(width=\"120px\"))\n",744        "    mlh  = _FT(\"\", \"110px\"); mlh.layout.margin=\"0\"\n",745        "\n",746        "    unit_txt = \"UI\" if cfg[\"unit_mode\"]==\"ui_min\" else \"mg\"\n",747        "    hint = widgets.HTML(\n",748        "        f\"<span class='hint'>Assumindo {cfg['conc']} {unit_txt}/mL · ampola {cfg['amp_ml']} mL</span>\"\n",749        "    )\n",750        "\n",751        "    for w in (amp, dil, mlh): w.disabled = True\n",752        "    def _toggle(change, a=amp, b=dil, c=mlh):\n",753        "        enabled = change[\"new\"]\n",754        "        for w in (a,b,c): w.disabled = not enabled\n",755        "        if not enabled: a.value = b.value = c.value = 0.0\n",756        "    cb.observe(_toggle, names=\"value\")\n",757        "\n",758        "    row = widgets.GridBox(\n",759        "        children=[cb, lblA, amp, lblD, dil, lblI, mlh, hint],\n",760        "        layout=widgets.Layout(\n",761        "            grid_template_columns=_HEMO_GRID,\n",762        "            align_items=\"center\",\n",763        "            grid_gap=\"6px\",\n",764        "            padding=\"2px 0\"\n",765        "        )\n",766        "    )\n",767        "    return row, {\"check\":cb, \"ampolas\":amp, \"diluente\":dil, \"mlh\":mlh, \"cfg\":cfg}\n",768        "\n",769        "drug_widgets_hemo = {}\n",770        "rows_hemo = []\n",771        "for d in DRUGS_HEMO:\n",772        "    row, wd = make_hemo_row(d)\n",773        "    rows_hemo.append(row)\n",774        "    drug_widgets_hemo[d[\"key\"]] = wd\n",775        "\n",776        "label_drogas = widgets.HTML(\"<b>Drogas vasoativas e inotrópicas:</b>\")\n",777        "drogas_box_hemo = widgets.VBox([label_drogas] + rows_hemo)\n",778        "drogas_box_hemo.layout.display = \"none\"\n",779        "\n",780        "def on_hemo_toggle(*_):\n",781        "    drogas_box_hemo.layout.display = \"\" if cb_com.value else \"none\"\n",782        "cb_sem.observe(lambda ch:on_hemo_toggle(), names=\"value\")\n",783        "cb_com.observe(lambda ch:on_hemo_toggle(), names=\"value\")\n",784        "on_hemo_toggle()\n",785        "\n",786        "box_hemo = widgets.VBox([titulo_hemo, pair_hemo, drogas_box_hemo],\n",787        "                        layout=widgets.Layout())\n",788        "\n",789        "# =========================================\n",790        "# RESPIRAÇÃO\n",791        "# =========================================\n",792        "titulo_vm = widgets.HTML(\"<h3 class='h'>RESPIRAÇÃO</h3>\")\n",793        "pair_vm, cb_esp, cb_vm = exclusive_pair(\"Respirando espontaneamente\", \"Ventilação mecânica\", value_left=True)\n",794        "\n",795        "opt_esp_box, cbs_esp = make_exclusive_check_group(\n",796        "    [\n",797        "        \"ar ambiente\",\n",798        "        \"cateter nasal\",\n",799        "        \"máscara de Venturi\",\n",800        "        \"máscara não reinalante\",\n",801        "        \"macronebulização por traqueostomia\",\n",802        "        \"cateter nasal de alto fluxo\",\n",803        "    ],\n",804        "    default=\"ar ambiente\"\n",805        ")\n",806        "def current_opt_esp():\n",807        "    for cb in cbs_esp:\n",808        "        if cb.value: return cb.description\n",809        "    return \"ar ambiente\"\n",810        "\n",811        "vni_cb   = widgets.Checkbox(value=False, description=\"VNI (sessões)\", indent=False)\n",812        "vni_hint = widgets.HTML(\"<span class='hint'>Adiciona a frase: associadas a sessões de ventilação não invasiva</span>\")\n",813        "vni_row  = widgets.HBox([vni_cb, vni_hint], layout=widgets.Layout(gap=\"10px\"))\n",814        "\n",815        "suporte_container = widgets.VBox(\n",816        "    [widgets.HTML(\"<b>Selecione o suporte:</b>\"), opt_esp_box, vni_row]\n",817        ")\n",818        "def suporte_visibility():\n",819        "    if cb_esp.value:\n",820        "        suporte_container.layout.display = \"flex\"\n",821        "    else:\n",822        "        suporte_container.layout.display = \"none\"\n",823        "        vni_cb.value = False\n",824        "cb_esp.observe(lambda ch: suporte_visibility(), names=\"value\")\n",825        "cb_vm.observe(lambda ch: suporte_visibility(), names=\"value\")\n",826        "suporte_visibility()\n",827        "\n",828        "def sentence_resp_esp():\n",829        "    base = current_opt_esp()\n",830        "    return f\"Respirando espontaneamente em {base}\" + \\\n",831        "           (\"; associadas a sessões de ventilação não invasiva.\" if vni_cb.value else \".\")\n",832        "\n",833        "modo_vm = widgets.RadioButtons(options=[(\"PCV\",\"PCV\"),(\"VCV\",\"VCV\"),(\"PSV\",\"PSV\")], value=\"PCV\")\n",834        "fio2 = widgets.BoundedFloatText(value=40.0, min=21, max=100, step=1.0, description=\"fiO2 (%)\",\n",835        "                                layout=widgets.Layout(width=\"160px\"))\n",836        "fr   = widgets.BoundedIntText(value=14, min=0, max=60, step=1, description=\"FR (ipm)\",\n",837        "                              layout=widgets.Layout(width=\"160px\"))\n",838        "peep = widgets.BoundedFloatText(value=8.0,  min=0, max=30, step=0.5, description=\"PEEP\",\n",839        "                                layout=widgets.Layout(width=\"160px\"))\n",840        "pins = widgets.BoundedFloatText(value=16.0, min=0, max=60, step=0.5, description=\"Pins\",\n",841        "                                layout=widgets.Layout(width=\"160px\"))\n",842        "vc   = widgets.BoundedIntText(value=400, min=0, max=1500, step=10, description=\"VC\",\n",843        "                              layout=widgets.Layout(width=\"160px\"))\n",844        "psup = widgets.BoundedFloatText(value=10.0, min=0, max=40, step=0.5, description=\"Psup\",\n",845        "                                layout=widgets.Layout(width=\"160px\"))\n",846        "\n",847        "pcv_box = widgets.VBox([widgets.HTML(\"<b>PCV – preencha:</b>\"),\n",848        "                        widgets.HBox([fio2, fr]),\n",849        "                        widgets.HBox([pins, peep])])\n",850        "\n",851        "vcv_box = widgets.VBox([widgets.HTML(\"<b>VCV – preencha:</b>\"),\n",852        "                        widgets.HBox([fio2, fr]),\n",853        "                        widgets.HBox([vc, peep])],\n",854        "                       layout=widgets.Layout(display=\"none\"))\n",855        "\n",856        "psv_box = widgets.VBox([widgets.HTML(\"<b>PSV – preencha:</b>\"),\n",857        "                        widgets.HBox([fio2]),\n",858        "                        widgets.HBox([psup, peep])],\n",859        "                       layout=widgets.Layout(display=\"none\"))\n",860        "\n",861        "vm_box  = widgets.VBox([widgets.HTML(\"<b>Selecione o modo:</b>\"), modo_vm, pcv_box, vcv_box, psv_box],\n",862        "                       layout=widgets.Layout(display=\"none\"))\n",863        "\n",864        "def show_mode(mode):\n",865        "    pcv_box.layout.display = \"flex\" if mode==\"PCV\" else \"none\"\n",866        "    vcv_box.layout.display = \"flex\" if mode==\"VCV\" else \"none\"\n",867        "    psv_box.layout.display = \"flex\" if mode==\"PSV\" else \"none\"\n",868        "modo_vm.observe(lambda ch: show_mode(ch[\"new\"]), names=\"value\"); show_mode(modo_vm.value)\n",869        "\n",870        "def vm_visibility():\n",871        "    vm_box.layout.display = \"flex\" if cb_vm.value else \"none\"\n",872        "cb_esp.observe(lambda ch: vm_visibility(), names=\"value\")\n",873        "cb_vm.observe(lambda ch: vm_visibility(), names=\"value\")\n",874        "vm_visibility()\n",875        "\n",876        "box_vm = widgets.VBox([titulo_vm, pair_vm, widgets.VBox([suporte_container, vm_box])],\n",877        "                      layout=widgets.Layout())\n",878        "\n",879        "# =========================================\n",880        "# DIETA\n",881        "# =========================================\n",882        "titulo_dieta = widgets.HTML(\"<h3 class='h'>DIETA</h3>\")\n",883        "pair_dieta, cb_zero, cb_comd = exclusive_pair(\"Em dieta zero\", \"Com dieta\", value_left=False)\n",884        "\n",885        "OPCOES_DIETA = [\n",886        "    \"Dieta oral\",\n",887        "    \"Dieta enteral\",\n",888        "    \"Dieta parenteral\"\n",889        "]\n",890        "checks = [widgets.Checkbox(value=(i==0), description=txt, indent=False) for i,txt in enumerate(OPCOES_DIETA)]\n",891        "checks_box = widgets.VBox(checks); checks_box.layout.display = \"flex\" if cb_comd.value else \"none\"\n",892        "\n",893        "def _toggle_dieta_vis(*_):\n",894        "    checks_box.layout.display = \"flex\" if cb_comd.value else \"none\"\n",895        "    if cb_comd.value and not any(c.value for c in checks):\n",896        "        checks[0].value = True\n",897        "cb_zero.observe(lambda ch:_toggle_dieta_vis(), names=\"value\")\n",898        "cb_comd.observe(lambda ch:_toggle_dieta_vis(), names=\"value\")\n",899        "\n",900        "def _enforce_one_checked(changed=None):\n",901        "    if not cb_comd.value: return\n",902        "    if not any(c.value for c in checks):\n",903        "        checks[0].value = True\n",904        "for c in checks:\n",905        "    c.observe(lambda ch: _enforce_one_checked(ch), names=\"value\")\n",906        "\n",907        "box_dieta = widgets.VBox([titulo_dieta, pair_dieta, widgets.HTML(\"<b>Selecione as opções aplicáveis:</b>\"), checks_box],\n",908        "                         layout=widgets.Layout())\n",909        "\n",910        "def lcfirst(s): return s[:1].lower() + s[1:] if s else s\n",911        "\n",912        "# =========================================\n",913        "# EXAMES LABORATORIAIS\n",914        "# =========================================\n",915        "titulo_labs = widgets.HTML(\"<h3 class='h'>EXAMES LABORATORIAIS</h3>\")\n",916        "ta_labs = widgets.Textarea(placeholder=\"Cole aqui o laudo bruto do laboratório (incluindo 'LAUDO COMPARATIVO' quando houver)...\",\n",917        "                           layout=widgets.Layout(width=\"100%\", height=\"260px\"))\n",918        "box_labs = widgets.VBox([titulo_labs, ta_labs], layout=widgets.Layout())\n",919        "\n",920        "# =========================================\n",921        "# OUTROS EXAMES\n",922        "# =========================================\n",923        "titulo_outros = widgets.HTML(\"<h3 class='h'>OUTROS EXAMES E AVALIAÇÕES COMPLEMENTARES</h3>\")\n",924        "ta_outros = widgets.Textarea(\n",925        "    placeholder=\"Exames de imagens, culturas, avaliações de especialidade, etc\",\n",926        "    layout=widgets.Layout(width=\"100%\", height=\"160px\")\n",927        ")\n",928        "box_outros = widgets.VBox([titulo_outros, ta_outros], layout=widgets.Layout())\n",929        "\n",930        "# =========================================\n",931        "# INTERCORRÊNCIAS\n",932        "# =========================================\n",933        "titulo_interc = widgets.HTML(\"<h3 class='h'>INTERCORRÊNCIAS NAS ÚLTIMAS 24 HORAS</h3>\")\n",934        "ta_interc = widgets.Textarea(\n",935        "    placeholder=\"Descreva intercorrências clínicas relevantes que mudaram o quadro clínico do paciente desde a última avaliação\",\n",936        "    layout=widgets.Layout(width=\"100%\", height=\"140px\")\n",937        ")\n",938        "box_interc = widgets.VBox([titulo_interc, ta_interc], layout=widgets.Layout())\n",939        "\n",940        "# =========================================\n",941        "# Helpers diurese\n",942        "# =========================================\n",943        "def _series_diurese_mlh_from_df(df_p):\n",944        "    if isinstance(df_p,pd.DataFrame) and not df_p.empty and \"DIURESE (ML)\" in df_p.index:\n",945        "        s=df_p.loc[\"DIURESE (ML)\",HOURS]\n",946        "        vals=[]\n",947        "        for h in HOURS:\n",948        "            v=s[h]\n",949        "            try: vals.append(float(v) if not pd.isna(v) else 0.0)\n",950        "            except: vals.append(0.0)\n",951        "        return vals\n",952        "    return []\n",953        "\n",954        "def _has_sequence_at_or_below(rate_series, thr, needed_hours):\n",955        "    cnt=0\n",956        "    for v in rate_series:\n",957        "        if v <= thr + 1e-9:\n",958        "            cnt+=1\n",959        "            if cnt>=needed_hours: return True\n",960        "        else: cnt=0\n",961        "    return False\n",962        "\n",963        "def classificar_diurese(diur_total_24h_ml, df_p, peso_kg):\n",964        "    if not math.isnan(diur_total_24h_ml):\n",965        "        total = float(diur_total_24h_ml)\n",966        "        if total < 50:\n",967        "            return \"- Anúrico(a) no período\"\n",968        "        if total < 400:\n",969        "            return \"- Oligúrico(a) no período\"\n",970        "        if total > 3000:\n",971        "            return \"- Poliúrico(a) no período\"\n",972        "        return \"- Diurese fisiológica no período\"\n",973        "\n",974        "    horas_ml = _series_diurese_mlh_from_df(df_p)\n",975        "    if peso_kg and peso_kg > 0 and horas_ml:\n",976        "        rates = [(v / peso_kg) for v in horas_ml]\n",977        "        if _has_sequence_at_or_below(rates, 0.0, 12):\n",978        "            return \"- Anúrico(a) no período\"\n",979        "        if _has_sequence_at_or_below(rates, 0.5 - 1e-9, 6):\n",980        "            return \"- Oligúrico(a) no período\"\n",981        "\n",982        "        total_parcial = sum(horas_ml)\n",983        "        if total_parcial > 0:\n",984        "            return \"- Diurese fisiológica no período\"\n",985        "\n",986        "    return \"- Sem quantificação de diurese no período\"\n",987        "\n",988        "# =========================================\n",989        "# Gênero no texto final\n",990        "# =========================================\n",991        "def apply_gender(text: str, sexo: str) -> str:\n",992        "    if not sexo: return text\n",993        "    end_vowel = \"o\" if sexo == \"M\" else \"a\"\n",994        "    replacements = {\n",995        "        \"orientado(a)\": f\"orientad{end_vowel}\",\n",996        "        \"normocorado(a)\": f\"normocorad{end_vowel}\",\n",997        "        \"aciantótico(a)\": f\"aciantótic{end_vowel}\",\n",998        "        \"anictérico(a)\": f\"anictéric{end_vowel}\",\n",999        "        \"hidratado(a)\": f\"hidratad{end_vowel}\",\n",1000        "        \"poliúrico(a)\": f\"poliúric{end_vowel}\",\n",1001        "        \"anúrico(a)\": f\"anúric{end_vowel}\",\n",1002        "        \"oligúrico(a)\": f\"oligúric{end_vowel}\",\n",1003        "        \"normotenso(a)\": f\"normotens{end_vowel}\",\n",1004        "    }\n",1005        "    out = text\n",1006        "    for k, v in replacements.items(): out = out.replace(k, v)\n",1007        "    out = out.replace(\"o(a)\", end_vowel).replace(\"(a)\", end_vowel).replace(\"(o/a)\", end_vowel)\n",1008        "    out = out.replace(\"o(a) paciente\", f\"{'o' if sexo=='M' else 'a'} paciente\")\n",1009        "    return out\n",1010        "\n",1011        "# =========================================\n",1012        "# ===== LABS — parser + texto natural =====\n",1013        "# =========================================\n",1014        "REFS = {\n",1015        "    \"hb_f\": (12.0, 15.8),\n",1016        "    \"leuc\": (3600, 11000),\n",1017        "    \"pcr\": (0, 5.0),\n",1018        "    \"ureia\": (16.6, 48.5),\n",1019        "    \"creat_f\": (0.50, 0.90),\n",1020        "    \"na\": (136,145),\n",1021        "    \"k\": (3.5,5.1),\n",1022        "    \"cl\": (98,107),\n",1023        "    \"mg\": (1.6,2.6),\n",1024        "    \"p\": (2.5,4.5),\n",1025        "    \"cai\": (1.12,1.32),\n",1026        "    \"plt\": (130,450),\n",1027        "    \"tgo\": (0, 40),\n",1028        "    \"tgp\": (0, 41),\n",1029        "    \"ggt\": (0, 40),\n",1030        "    \"falc\": (40, 129),\n",1031        "    \"lactato\": (0.5, 2.2),\n",1032        "    \"proBNP\": (0, 125),\n",1033        "    \"troponina\": (0, 0.014),\n",1034        "}\n",1035        "\n",1036        "KEYS = {\n",1037        "    \"SÓDIO\":\"na\",\"SODIO\":\"na\",\"NA\":\"na\",\n",1038        "    \"POTÁSSIO\":\"k\",\"POTASSIO\":\"k\",\"K\":\"k\",\n",1039        "    \"CLORO\":\"cl\",\"CLO\":\"cl\",\n",1040        "    \"MAGNÉSIO\":\"mg\",\"MAGNESIO\":\"mg\",\"MAG\":\"mg\",\"MG\":\"mg\",\n",1041        "    \"FÓSFORO\":\"p\",\"FOSFORO\":\"p\",\n",1042        "    \"CÁLCIO IÔNICO\":\"cai\",\"CALCIO IONICO\":\"cai\",\"CAI\":\"cai\",\n",1043        "    \"UREIA\":\"ureia\",\"URÉIA\":\"ureia\",\"URE\":\"ureia\",\n",1044        "    \"CREATININA\":\"creat\",\"CRE\":\"creat\",\n",1045        "    \"HEMOGLOBINA\":\"hb\",\n",1046        "    \"LEUCÓCITOS\":\"leuc\",\"LEUCOCITOS\":\"leuc\",\"LEUCOCIT\":\"leuc\",\n",1047        "    \"PLAQUETAS\":\"plt\",\n",1048        "    \"PCR\":\"pcr\",\n",1049        "    \"PROCALC\":\"procalc\",\"PROCALCITONINA\":\"procalc\",\n",1050        "    \"TGO\":\"tgo\",\"TGP\":\"tgp\",\"GGT\":\"ggt\",\"FALC\":\"falc\",\n",1051        "    \"LACTATOH\":\"lactato\",\"LACTATO\":\"lactato\",\n",1052        "    \"TROPONINA T\":\"troponina\",\"TROPONINA\":\"troponina\",\n",1053        "    \"PROBNP\":\"proBNP\",\"NT PROBNP\":\"proBNP\",\n",1054        "    \"PT - PROTEINAS\":\"pt\",\"ALB - ALBUMINA\":\"albumina\",\n",1055        "    \"BT - BILIRRUBINA TOTAIS\":\"bt\",\"BILIRRUBINA TOTAIS\":\"bt\",\n",1056        "    \"BD - BILIRRUINA DIRETA\":\"bd\",\"BILIRRUBINA DIRETA\":\"bd\",\n",1057        "}\n",1058        "\n",1059        "_RESULT_LABELS = [\n",1060        "    (re.compile(r\"HEMOGLOBINA|HEMOGLOBIN[AÂ]\", re.I), \"hb\"),\n",1061        "    (re.compile(r\"LEUC[ÓO]CITOS?\", re.I), \"leuc\"),\n",1062        "    (re.compile(r\"PCR\\b|PROTE[ÍI]NA C REATIVA\", re.I), \"pcr\"),\n",1063        "    (re.compile(r\"PROCALC\", re.I), \"procalc\"),\n",1064        "    (re.compile(r\"UR[EÉ]IA|UREIA\", re.I), \"ureia\"),\n",1065        "    (re.compile(r\"CREATININA\", re.I), \"creat\"),\n",1066        "    (re.compile(r\"S[ÓO]DIO|NA\\b\", re.I), \"na\"),\n",1067        "    (re.compile(r\"POT[ÁA]SSIO|K\\b\", re.I), \"k\"),\n",1068        "    (re.compile(r\"CLORO|CLO\\b\", re.I), \"cl\"),\n",1069        "    (re.compile(r\"MAGN[ÉE]SIO|MAG\\b\", re.I), \"mg\"),\n",1070        "    (re.compile(r\"F[ÓO]SFORO\\b\", re.I), \"p\"),\n",1071        "    (re.compile(r\"C[ÁA]LCIO I[ÔO]NICO|CAI\\b\", re.I), \"cai\"),\n",1072        "    (re.compile(r\"PLAQUETAS\", re.I), \"plt\"),\n",1073        "    (re.compile(r\"TGO\", re.I), \"tgo\"),\n",1074        "    (re.compile(r\"TGP\", re.I), \"tgp\"),\n",1075        "    (re.compile(r\"GGT\", re.I), \"ggt\"),\n",1076        "    (re.compile(r\"FOSFATASE AL[CK]\", re.I), \"falc\"),\n",1077        "    (re.compile(r\"LACTATOH?\\b\", re.I), \"lactato\"),\n",1078        "    (re.compile(r\"TROPONINA\", re.I), \"troponina\"),\n",1079        "    (re.compile(r\"PROBNP|NT\\s*PROBNP\", re.I), \"proBNP\"),\n",1080        "    (re.compile(r\"ALBUMINA\", re.I), \"albumina\"),\n",1081        "    (re.compile(r\"BILIRRUBINA\\s+TOTAIS\", re.I), \"bt\"),\n",1082        "    (re.compile(r\"BILIRRUBINA\\s+DIRET[AA]\", re.I), \"bd\"),\n",1083        "]\n",1084        "_RES_RESULTADO_RX = re.compile(r\"RESULTADO\\s*[:\\-]?\\s*([^\\n\\r]+)\", re.I)\n",1085        "_NUM_BR_RX = re.compile(r\"[-+]?\\d{1,3}(?:\\.\\d{3})*(?:,\\d+)?|[-+]?\\d+(?:,\\d+)?\")\n",1086        "\n",1087        "def _br_to_float(s: str):\n",1088        "    if s is None:\n",1089        "        return None\n",1090        "    s = s.strip()\n",1091        "    if not s or s == \"--\":\n",1092        "        return None\n",1093        "    s = re.sub(r\"[^\\d\\.,\\-+]\", \"\", s)\n",1094        "    if not s:\n",1095        "        return None\n",1096        "    if \",\" in s:\n",1097        "        s = s.replace(\".\", \"\").replace(\",\", \".\")\n",1098        "        try:\n",1099        "            return float(s)\n",1100        "        except:\n",1101        "            return None\n",1102        "    if s.count(\".\") == 1 and len(s.split(\".\")[-1]) == 3:\n",1103        "        try:\n",1104        "            return float(s.replace(\".\", \"\"))\n",1105        "        except:\n",1106        "            return None\n",1107        "    try:\n",1108        "        return float(s)\n",1109        "    except:\n",1110        "        return None\n",1111        "\n",1112        "def _fmt_int_br(v):\n",1113        "    try:\n",1114        "        n = int(round(float(v)))\n",1115        "    except:\n",1116        "        return \"\"\n",1117        "    return f\"{n:,}\".replace(\",\", \".\")\n",1118        "\n",1119        "def _fmt_br(v, decimals=1, milhar=False):\n",1120        "    if v is None:\n",1121        "        return \"\"\n",1122        "    if milhar and (abs(v - round(v)) < 1e-9 or decimals == 0):\n",1123        "        return _fmt_int_br(v)\n",1124        "    if abs(v - round(v)) < 1e-9 and decimals == 0:\n",1125        "        return str(int(round(v))).replace(\".\", \",\")\n",1126        "    return f\"{float(v):.{decimals}f}\".replace(\".\", \",\")\n",1127        "\n",1128        "def _var_percent(a, b):\n",1129        "    if a is None or b is None or b == 0:\n",1130        "        return None\n",1131        "    return abs(a - b) / abs(b)\n",1132        "\n",1133        "def _pick_variation_text(curr, prev, show_threshold=0.10, decimals=2, milhar=False):\n",1134        "    if curr is None:\n",1135        "        return \"\"\n",1136        "    if prev is None:\n",1137        "        return f\"({_fmt_br(curr, decimals, milhar)})\"\n",1138        "    vp = _var_percent(curr, prev)\n",1139        "    if vp is not None and vp >= show_threshold:\n",1140        "        return f\"({_fmt_br(prev, decimals, milhar)} a {_fmt_br(curr, decimals, milhar)})\"\n",1141        "    return f\"({_fmt_br(curr, decimals, milhar)})\"\n",1142        "\n",1143        "def _status(v, lo, hi, low_word=\"baixo\", high_word=\"elevado\", normal_word=\"normal\"):\n",1144        "    if v is None:\n",1145        "        return None\n",1146        "    if lo is not None and v < lo - 1e-9:\n",1147        "        return low_word\n",1148        "    if hi is not None and v > hi + 1e-9:\n",1149        "        return high_word\n",1150        "    return normal_word\n",1151        "\n",1152        "def _scan_resultado_blocks(text):\n",1153        "    got = {}\n",1154        "    for rx, key in _RESULT_LABELS:\n",1155        "        for m in rx.finditer(text):\n",1156        "            tail = text[m.end():m.end()+260]\n",1157        "            mres = _RES_RESULTADO_RX.search(tail)\n",1158        "            if not mres:\n",1159        "                continue\n",1160        "            raw = mres.group(1)\n",1161        "            num = _NUM_BR_RX.search(raw)\n",1162        "            if num:\n",1163        "                v = _br_to_float(num.group(0))\n",1164        "                if v is not None:\n",1165        "                    got[key] = (v, got.get(key, (None, None))[1])\n",1166        "    return got\n",1167        "\n",1168        "def _merge_sources(primary: dict, secondary: dict):\n",1169        "    out = dict(primary)\n",1170        "    for k, (s_curr, s_prev) in secondary.items():\n",1171        "        if k not in out:\n",1172        "            out[k] = (s_curr, s_prev)\n",1173        "            continue\n",1174        "        c_curr, c_prev = out[k]\n",1175        "        if c_curr is None and s_curr is not None:\n",1176        "            out[k] = (s_curr, c_prev)\n",1177        "    return out\n",1178        "\n",1179        "_CMP_TOKEN_RX = re.compile(r\"--|[-+]?\\d{1,3}(?:\\.\\d{3})*(?:,\\d+)?|[-+]?\\d+(?:,\\d+)?\", re.I)\n",1180        "\n",1181        "def _scan_comparativo_lines(text):\n",1182        "    got = {}\n",1183        "    key_items = sorted(KEYS.items(), key=lambda kv: len(kv[0]), reverse=True)\n",1184        "    in_cmp = False\n",1185        "    for raw in text.splitlines():\n",1186        "        line = raw.strip()\n",1187        "        if not in_cmp and re.search(r\"LAUDO\\s+COMPARATIVO\", line, flags=re.I):\n",1188        "            in_cmp = True\n",1189        "            continue\n",1190        "        if not in_cmp or not line:\n",1191        "            continue\n",1192        "        up_no_sp = re.sub(r\"\\s+\", \"\", line.upper())\n",1193        "        key_norm = None\n",1194        "        matched_key_literal = None\n",1195        "        for k, norm in key_items:\n",1196        "            kcmp = k.upper().replace(\" \", \"\")\n",1197        "            if up_no_sp.startswith(kcmp):\n",1198        "                key_norm = norm\n",1199        "                matched_key_literal = k\n",1200        "                break\n",

Showing the first 1,200 of 29519 lines. Download the file for the rest.