leo-serafim/evolucaoalvorada
0
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",