[{"data":1,"prerenderedAt":3394},["ShallowReactive",2],{"lang-switch-post-\u002Fprojects\u002Fnetwork-intrusion-detection":3,"project-pt-network-intrusion-detection":4,"project-chapters-pt-network-intrusion-detection":72},null,{"id":5,"title":6,"body":7,"cover":3,"description":58,"extension":59,"meta":60,"navigation":61,"order":56,"path":62,"seo":63,"status":64,"stem":65,"tags":66,"__hash__":71},"projects\u002Fpt\u002Fprojects\u002Fnetwork-intrusion-detection\u002Findex.md","Detecção de Intrusão: Caçando Ataque em 2,5 Milhões de Sessões de Rede",{"type":8,"value":9,"toc":54},"minimark",[10,24,43],[11,12,13,14,18,19,23],"p",{},"Esse projeto é diferente até da Olist. A Olist eu construí capítulo por capítulo, com o professor Boldt do lado só na forma da matéria que ele deu. Esse aqui é literalmente meu ",[15,16,17],"strong",{},"trabalho final"," da matéria de Reconhecimento de Padrões, a prova de bloco de tudo que os outros posts dessa playlist ensinaram: EDA de dado sujo e desbalanceado, pré-processamento com ",[20,21,22],"code",{},"ColumnTransformer",", comparação honesta de modelo, avaliação além de acurácia, GridSearch. Só que dessa vez sem professor mostrando o caminho, fui eu sozinho com o dataset e o Bishop do lado.",[11,25,26,27,34,35,38,39,42],{},"O dataset é o ",[28,29,33],"a",{"href":30,"rel":31},"https:\u002F\u002Fwww.kaggle.com\u002Fdatasets\u002Fmrwellsdavid\u002Funsw-nb15",[32],"nofollow","UNSW-NB15",", criado pelo Australian Centre for Cyber Security (ACCS) simulando tráfego de rede real misturado com nove famílias de ataque diferentes (de força bruta a worm), a partir de captura de pacote de verdade num ambiente de laboratório. A tarefa é a mais clássica de segurança de rede que existe: olhando pras características de uma sessão de rede (duração, bytes trocados, protocolo, TTL, e por aí vai), dizer se aquilo é tráfego ",[15,36,37],{},"normal"," ou ",[15,40,41],{},"ataque",".",[11,44,45,46,50,51,42],{},"Diferente da Olist, aqui não são ângulos independentes sobre o mesmo dataframe mestre, é um fluxo de ponta a ponta em três capítulos, cada um assumindo o que o anterior já mostrou: ",[28,47,49],{"href":48},"\u002Fprojects\u002Fnetwork-intrusion-detection\u002F01-explorando-25-milhoes-de-sessoes","capítulo 1"," é a análise exploratória completa nos arquivos brutos, capítulo 2 troca de dataset e escolhe o modelo, capítulo 3 fecha com avaliação final, curva ROC e ajuste de hiperparâmetro. Começa pelo ",[28,52,53],{"href":48},"capítulo 1 aqui",{"title":55,"searchDepth":56,"depth":56,"links":57},"",2,[],"Meu trabalho final da matéria de Reconhecimento de Padrões: um classificador de tráfego de rede treinado no UNSW-NB15, do CSV bruto (2,5 milhões de sessões) até um Random Forest com 98,3% de AUC no teste oficial.","md",{},true,"\u002Fpt\u002Fprojects\u002Fnetwork-intrusion-detection",{"title":6,"description":58},"published","pt\u002Fprojects\u002Fnetwork-intrusion-detection\u002Findex",[67,68,69,70],"scikit-learn","random-forest","seguranca","deteccao-de-anomalia","yD9whCfp3R4Ppc09odenf_MgI4ZHL21wD5EnoFB9cS4",[73,1530,2110],{"id":74,"title":75,"body":76,"cover":3,"date":1518,"description":1519,"extension":59,"meta":1520,"navigation":61,"order":132,"path":1521,"project":1522,"seo":1523,"status":64,"stem":1524,"tags":1525,"__hash__":1529},"projectChapters\u002Fpt\u002Fprojects\u002Fnetwork-intrusion-detection\u002F01-explorando-25-milhoes-de-sessoes.md","Explorando 2,5 Milhões de Sessões de Rede",{"type":8,"value":77,"toc":1507},[78,97,100,105,120,171,188,211,308,357,372,393,403,442,464,473,480,488,492,499,504,553,557,560,594,634,799,821,825,828,945,965,972,976,1010,1035,1082,1124,1128,1148,1153,1160,1174,1178,1184,1219,1268,1291,1295,1302,1324,1331,1387,1398,1409,1413,1416,1500,1503],[11,79,80,84,85,38,88,90,91,96],{},[28,81,83],{"href":82},"\u002Fprojects\u002Fnetwork-intrusion-detection","Como contei na abertura do projeto",", esse é meu trabalho final da matéria de Reconhecimento de Padrões, sem professor mostrando o caminho dessa vez. Pensa numa sessão de rede como uma ligação telefônica entre dois computadores: tem um início e um fim, uma duração, uma quantidade de informação trocada de cada lado (os bytes), e um \"idioma\" combinado entre os dois (o protocolo, tipo TCP ou UDP). A missão do projeto inteiro: dado esse pacotinho de informação sobre a ligação, decidir se aquilo é ",[15,86,87],{},"tráfego normal",[15,89,41],{},". Notebook completo, executado, ",[28,92,95],{"href":93,"rel":94},"https:\u002F\u002Fcolab.research.google.com\u002Fdrive\u002F1tDmkJZOjd4yia-iPxcajnsjOSeaJJGGs?usp=sharing",[32],"aqui",", os três capítulos saem dele.",[11,98,99],{},"Esse primeiro capítulo é só a análise exploratória (EDA). Antes de treinar qualquer modelo eu preciso entender o que tenho na mão: quão desbalanceado é o dataset, onde faltam dados e por quê, se alguma coluna tá suja, quais variáveis contam a mesma história (redundância) e como decidir, com critério, quais entram no modelo. O capítulo 2 muda de dataset (explico por quê) e treina os candidatos, o capítulo 3 fecha com avaliação final, curva ROC e ajuste de hiperparâmetro.",[101,102,104],"h2",{"id":103},"o-dataset-25-milhões-de-sessões-quase-9-em-cada-10-são-tráfego-normal","O dataset: 2,5 milhões de sessões, quase 9 em cada 10 são tráfego normal",[11,106,107,108,111,112,115,116,119],{},"O ",[28,109,33],{"href":30,"rel":110},[32]," nasceu de um laboratório de verdade: o Australian Centre for Cyber Security gerou tráfego de rede misturando navegação normal com nove famílias de ataque diferentes rodando de propósito, e capturou tudo em pacote bruto. O primeiro problema prático: os quatro arquivos CSV brutos (",[20,113,114],{},"UNSW-NB15_1.csv"," a ",[20,117,118],{},"_4.csv",") não trazem nome de coluna nenhum, só número puro. Os nomes moram separados, num quinto arquivo, o \"catálogo\" de features:",[121,122,126],"pre",{"className":123,"code":124,"language":125,"meta":55,"style":55},"language-python shiki shiki-themes github-light github-dark","features = pd.read_csv(\"\u002Fcontent\u002Fdata\u002FNUSW-NB15_features.csv\", encoding=\"latin-1\")\nfeatures.columns = features.columns.str.strip()\npd.set_option(\"display.max_colwidth\", None)\npd.set_option(\"display.max_rows\", None)\n\nprint(f\"Total de features descritas: {len(features)}\")\ndisplay(features[[\"No.\", \"Name\", \"Type\", \"Description\"]])\n","python",[20,127,128,136,141,147,153,159,165],{"__ignoreMap":55},[129,130,133],"span",{"class":131,"line":132},"line",1,[129,134,135],{},"features = pd.read_csv(\"\u002Fcontent\u002Fdata\u002FNUSW-NB15_features.csv\", encoding=\"latin-1\")\n",[129,137,138],{"class":131,"line":56},[129,139,140],{},"features.columns = features.columns.str.strip()\n",[129,142,144],{"class":131,"line":143},3,[129,145,146],{},"pd.set_option(\"display.max_colwidth\", None)\n",[129,148,150],{"class":131,"line":149},4,[129,151,152],{},"pd.set_option(\"display.max_rows\", None)\n",[129,154,156],{"class":131,"line":155},5,[129,157,158],{"emptyLinePlaceholder":61},"\n",[129,160,162],{"class":131,"line":161},6,[129,163,164],{},"print(f\"Total de features descritas: {len(features)}\")\n",[129,166,168],{"class":131,"line":167},7,[129,169,170],{},"display(features[[\"No.\", \"Name\", \"Type\", \"Description\"]])\n",[172,173,174],"blockquote",{},[11,175,176,179,180,183,184,187],{},[15,177,178],{},"Saída:"," ",[20,181,182],{},"Total de features descritas: 49",", seguido de uma tabela com número, nome, tipo declarado e descrição de cada uma das 49 colunas (por exemplo, ",[20,185,186],{},"sttl"," aparece descrita como \"Source to destination time to live value\").",[11,189,190,193,194,197,198,201,202,205,206,210],{},[20,191,192],{},"encoding=\"latin-1\""," não é capricho: o CSV desse catálogo tem caracteres fora do padrão UTF-8 (acentos e símbolos vindos de quem documentou o dataset), e sem especificar o encoding certo o ",[20,195,196],{},"read_csv"," quebra. As duas linhas de ",[20,199,200],{},"pd.set_option"," existem só pra essa exibição específica: sem elas, o pandas trunca a coluna ",[20,203,204],{},"Description"," (que tem frases longas) e corta a tabela em \"...\" no meio das 49 linhas, exatamente o oposto do que eu quero quando o objetivo é ",[207,208,209],"em",{},"ler"," a descrição de cada feature. Esse catálogo é o que eu uso a seguir pra nomear as colunas de verdade:",[121,212,214],{"className":123,"code":213,"language":125,"meta":55,"style":55},"import re\n\n# Filtragem apenas dos arquivos de dados -> UNSW-NB15_1.csv até UNSW-NB15_4.csv\npartes = sorted([\n    f for f in os.listdir(\"\u002Fcontent\u002Fdata\")\n    if re.match(r\"UNSW-NB15_\\d+\\.csv\", f)\n])\n\n# Extraindo os nomes das colunas do arquivo de features\ncolunas = features[\"Name\"].tolist()\n\n# Carregando e concatenando todas as partes em um único DataFrame\ndf = pd.concat(\n    [pd.read_csv(f\"\u002Fcontent\u002Fdata\u002F{p}\", header=None, names=colunas, low_memory=False)\n     for p in partes],\n    ignore_index=True\n)\n",[20,215,216,221,225,230,235,240,245,250,255,261,267,272,278,284,290,296,302],{"__ignoreMap":55},[129,217,218],{"class":131,"line":132},[129,219,220],{},"import re\n",[129,222,223],{"class":131,"line":56},[129,224,158],{"emptyLinePlaceholder":61},[129,226,227],{"class":131,"line":143},[129,228,229],{},"# Filtragem apenas dos arquivos de dados -> UNSW-NB15_1.csv até UNSW-NB15_4.csv\n",[129,231,232],{"class":131,"line":149},[129,233,234],{},"partes = sorted([\n",[129,236,237],{"class":131,"line":155},[129,238,239],{},"    f for f in os.listdir(\"\u002Fcontent\u002Fdata\")\n",[129,241,242],{"class":131,"line":161},[129,243,244],{},"    if re.match(r\"UNSW-NB15_\\d+\\.csv\", f)\n",[129,246,247],{"class":131,"line":167},[129,248,249],{},"])\n",[129,251,253],{"class":131,"line":252},8,[129,254,158],{"emptyLinePlaceholder":61},[129,256,258],{"class":131,"line":257},9,[129,259,260],{},"# Extraindo os nomes das colunas do arquivo de features\n",[129,262,264],{"class":131,"line":263},10,[129,265,266],{},"colunas = features[\"Name\"].tolist()\n",[129,268,270],{"class":131,"line":269},11,[129,271,158],{"emptyLinePlaceholder":61},[129,273,275],{"class":131,"line":274},12,[129,276,277],{},"# Carregando e concatenando todas as partes em um único DataFrame\n",[129,279,281],{"class":131,"line":280},13,[129,282,283],{},"df = pd.concat(\n",[129,285,287],{"class":131,"line":286},14,[129,288,289],{},"    [pd.read_csv(f\"\u002Fcontent\u002Fdata\u002F{p}\", header=None, names=colunas, low_memory=False)\n",[129,291,293],{"class":131,"line":292},15,[129,294,295],{},"     for p in partes],\n",[129,297,299],{"class":131,"line":298},16,[129,300,301],{},"    ignore_index=True\n",[129,303,305],{"class":131,"line":304},17,[129,306,307],{},")\n",[11,309,310,311,314,315,317,318,321,322,325,326,115,328,330,331,334,335,338,339,342,343,342,346,342,349,352,353,356],{},"Vale abrir esse bloco, porque tem uma pegadinha escondida: sem o ",[20,312,313],{},"names=colunas"," ali dentro do ",[20,316,196],{},", o pandas numeraria as colunas como ",[20,319,320],{},"0, 1, 2..."," e eu perderia a etiqueta de cada uma. Antes disso, ",[20,323,324],{},"re.match(r\"UNSW-NB15_\\d+\\.csv\", f)"," filtra, de todos os arquivos que vieram no download, só os quatro que interessam aqui (",[20,327,114],{},[20,329,118],{},"), descartando de propósito o próprio catálogo de features e os arquivos da partição oficial (",[20,332,333],{},"UNSW_NB15_training-set.csv"," etc., que uso só a partir do capítulo 2) que também estão na mesma pasta. ",[20,336,337],{},"sorted(...)"," garante que os quatro pedaços entrem na ordem certa (",[20,340,341],{},"_1",", ",[20,344,345],{},"_2",[20,347,348],{},"_3",[20,350,351],{},"_4","), não na ordem que o sistema de arquivos decide listar. A lista de compreensão entre colchetes lê os quatro arquivos, um de cada vez, já colando esses nomes certos na hora de ler, e ",[20,354,355],{},"pd.concat(..., ignore_index=True)"," empilha os quatro pedaços um embaixo do outro, resetando a contagem de linha do zero (sem isso, cada pedaço reiniciaria em 0, e eu teria índice repetido quatro vezes).",[121,358,360],{"className":123,"code":359,"language":125,"meta":55,"style":55},"print(f\"shape do dataset: \\n {df.shape}\\n\\n\")\ndf.head(5)\n",[20,361,362,367],{"__ignoreMap":55},[129,363,364],{"class":131,"line":132},[129,365,366],{},"print(f\"shape do dataset: \\n {df.shape}\\n\\n\")\n",[129,368,369],{"class":131,"line":56},[129,370,371],{},"df.head(5)\n",[172,373,374],{},[11,375,376,179,378,381,382,342,385,388,389,392],{},[15,377,178],{},[20,379,380],{},"(2540047, 49)",", seguido das 5 primeiras linhas reais, uma sessão UDP com ",[20,383,384],{},"sport=1390",[20,386,387],{},"dsport=53"," (porta de DNS) e ",[20,390,391],{},"dur=0.001055"," segundos, entre outras.",[11,394,395,396,398,399,402],{},"Um jeito simples e barato de conferir que a concatenação dos quatro arquivos e o batimento de nomes deram certo: se ",[20,397,313],{}," tivesse desalinhado alguma coluna, o ",[20,400,401],{},"df.head()"," mostraria valor de texto onde eu esperava número, ou vice-versa, e eu pegaria isso na cara antes de qualquer análise mais séria.",[121,404,406],{"className":123,"code":405,"language":125,"meta":55,"style":55},"print(f\"Linhas: {df.shape[0]:,}\")\nprint(f\"Colunas: {df.shape[1]}\")\nprint(\"Tipagem dos dados:\")\nprint(df.dtypes.value_counts())\n\npd.options.display.float_format = \"{:,.2f}\".format\ndisplay(df.describe().T)\n",[20,407,408,413,418,423,428,432,437],{"__ignoreMap":55},[129,409,410],{"class":131,"line":132},[129,411,412],{},"print(f\"Linhas: {df.shape[0]:,}\")\n",[129,414,415],{"class":131,"line":56},[129,416,417],{},"print(f\"Colunas: {df.shape[1]}\")\n",[129,419,420],{"class":131,"line":143},[129,421,422],{},"print(\"Tipagem dos dados:\")\n",[129,424,425],{"class":131,"line":149},[129,426,427],{},"print(df.dtypes.value_counts())\n",[129,429,430],{"class":131,"line":155},[129,431,158],{"emptyLinePlaceholder":61},[129,433,434],{"class":131,"line":161},[129,435,436],{},"pd.options.display.float_format = \"{:,.2f}\".format\n",[129,438,439],{"class":131,"line":167},[129,440,441],{},"display(df.describe().T)\n",[172,443,444],{},[11,445,446,179,448,451,452,455,456,459,460,463],{},[15,447,178],{},[20,449,450],{},"int64"," 28 colunas, ",[20,453,454],{},"float64"," 12, ",[20,457,458],{},"object"," 9. O ",[20,461,462],{},"df.describe().T"," (transposto, uma linha por coluna numérica) devolve contagem, média, desvio, mínimo, quartis e máximo de cada uma das 40 colunas numéricas de uma vez.",[11,465,466,467,469,470,472],{},"Esse terceiro grupo, as 9 colunas ",[20,468,458],{},", merece desconfiança, porque uma coluna virar ",[20,471,458],{}," não significa necessariamente \"essa coluna é texto de verdade\". Às vezes é o pandas dizendo \"achei algo aí dentro que não bate com número puro\", e mais pra frente nesse capítulo isso vai ser exatamente a pista de um bug.",[474,475],"category-bar-chart",{":points":476,"color":477,"x-label":478,"y-label":479},"[{\"label\":\"Normal\",\"value\":2218764},{\"label\":\"Ataque\",\"value\":321283}]","#0033cc","classe","quantidade de sessões",[11,481,482,483,487],{},"2.218.764 sessões normais contra 321.283 de ataque, uma razão de quase 7 pra 1. Já vi esse tipo de desbalanceamento antes ",[28,484,486],{"href":485},"\u002Fplaylists\u002Fpattern-recognition\u002Fcredit-card-fraud","na detecção de fraude de cartão de crédito",", lá era 0,17% de fraude, aqui são 12,6% de ataque, bem menos extremo, mas ainda desbalanceado o suficiente pra acurácia sozinha mentir: só chutando \"normal\" sempre, um modelo preguiçoso já acerta 87,4% das vezes sem aprender nada.",[101,489,491],{"id":490},"que-tipo-de-ataque-tem-aí","Que tipo de ataque tem aí",[11,493,494,495,498],{},"A coluna ",[20,496,497],{},"attack_cat"," (só preenchida quando a sessão é ataque, por isso 87,35% dela é vazio, exatamente a fração de tráfego normal) diz qual família de ataque é cada sessão maliciosa:",[474,500],{":points":501,"color":502,"x-label":503,"y-label":479},"[{\"label\":\"Generic\",\"value\":215481},{\"label\":\"Exploits\",\"value\":44525},{\"label\":\"Fuzzers\",\"value\":24246},{\"label\":\"DoS\",\"value\":16353},{\"label\":\"Reconnaissance\",\"value\":13987},{\"label\":\"Analysis\",\"value\":2677},{\"label\":\"Backdoor\",\"value\":2329},{\"label\":\"Shellcode\",\"value\":1511},{\"label\":\"Worms\",\"value\":174}]","#cc3300","categoria de ataque",[11,505,506,507,510,511,514,515,518,519,342,522,518,525,528,529,518,532,535,536,540,541,544,545,548,549,552],{},"Antes de eu somar essas 9 categorias, o dado bruto tinha 13 rótulos diferentes, porque ",[20,508,509],{},"\" Fuzzers\""," (com espaço na frente) e ",[20,512,513],{},"\"Fuzzers\""," contavam como duas categorias separadas pro pandas, mesma bobagem com ",[20,516,517],{},"\"Reconnaissance\"","\u002F",[20,520,521],{},"\" Reconnaissance\"",[20,523,524],{},"\"Backdoor\"",[20,526,527],{},"\"Backdoors\""," e ",[20,530,531],{},"\"Shellcode\"",[20,533,534],{},"\" Shellcode\"",". O mesmo tipo de sujeira de digitação que ",[28,537,539],{"href":538},"\u002Fplaylists\u002Fpattern-recognition\u002Ftitanic","já vi no dataset do Titanic",", só que aqui em vez de coluna faltando é categoria duplicada por espaço a mais, um ",[20,542,543],{},".strip()"," resolveria. ",[20,546,547],{},"Generic"," domina disparado (215 mil sessões, dois terços de todo ataque), e são técnicas que forjam uma colisão de hash pra confundir a criptografia, bem mais fáceis de automatizar em massa do que, por exemplo, um ",[20,550,551],{},"Worms"," (só 174 sessões, ataque que precisa se replicar sozinho de máquina em máquina, mais raro de acontecer num laboratório controlado).",[101,554,556],{"id":555},"redundância-metade-das-colunas-conta-a-mesma-história","Redundância: metade das colunas conta a mesma história",[11,558,559],{},"Com 40 variáveis numéricas, uma pergunta natural é: quantas delas carregam informação genuinamente diferente? Rodei a correlação de Pearson entre pares de variáveis numéricas numa amostra de 50 mil sessões (calcular isso nas 2,5 milhões inteiras seria caro à toa):",[121,561,563],{"className":123,"code":562,"language":125,"meta":55,"style":55},"sample = df[colunas_numericas].sample(n=50000, random_state=42)\ncorr_matrix = sample.corr()\n\nmask_pairs = np.triu(np.ones_like(corr_matrix, dtype=bool), k=1)\ncorr_pairs = corr_matrix.where(mask_pairs).stack()\nfortes = corr_pairs[corr_pairs.abs() >= 0.8].sort_values(key=abs, ascending=False)\n",[20,564,565,570,575,579,584,589],{"__ignoreMap":55},[129,566,567],{"class":131,"line":132},[129,568,569],{},"sample = df[colunas_numericas].sample(n=50000, random_state=42)\n",[129,571,572],{"class":131,"line":56},[129,573,574],{},"corr_matrix = sample.corr()\n",[129,576,577],{"class":131,"line":143},[129,578,158],{"emptyLinePlaceholder":61},[129,580,581],{"class":131,"line":149},[129,582,583],{},"mask_pairs = np.triu(np.ones_like(corr_matrix, dtype=bool), k=1)\n",[129,585,586],{"class":131,"line":155},[129,587,588],{},"corr_pairs = corr_matrix.where(mask_pairs).stack()\n",[129,590,591],{"class":131,"line":161},[129,592,593],{},"fortes = corr_pairs[corr_pairs.abs() >= 0.8].sort_values(key=abs, ascending=False)\n",[11,595,596,597,600,601,604,605,608,609,612,613,608,615,617,618,621,622,625,626,629,630,633],{},"Esse bloco final vale desmontar, porque ",[20,598,599],{},"corr_matrix"," sozinha é uma armadilha de duplicata: é uma tabela 40×40, cada variável cruzada com todas as outras, mas ela é ",[15,602,603],{},"simétrica",", o cruzamento de ",[20,606,607],{},"sbytes"," com ",[20,610,611],{},"sloss"," e o de ",[20,614,611],{},[20,616,607],{}," são o mesmo número aparecendo duas vezes. Contar isso duas vezes infla a lista à toa. ",[20,619,620],{},"np.triu(..., k=1)"," desenha uma \"grade\" que marca só o triângulo de cima da tabela como verdadeiro, pulando a diagonal (o ",[20,623,624],{},"k=1",", senão eu contaria cada variável correlacionada com ela mesma, sempre 1,0). ",[20,627,628],{},"corr_matrix.where(mask_pairs)"," apaga (vira vazio) tudo que caiu fora dessa grade, sobrando só metade única da tabela. ",[20,631,632],{},".stack()"," acha essa metade e a \"desdobra\" numa lista comprida de pares, descartando os vazios sozinho. Só falta filtrar quem tem correlação forte (|r| ≥ 0,8) e ordenar do mais forte pro mais fraco, exatamente o que a última linha faz:",[635,636,637,652],"table",{},[638,639,640],"thead",{},[641,642,643,648],"tr",{},[644,645,647],"th",{"align":646},"left","Par de variáveis",[644,649,651],{"align":650},"right","Correlação",[653,654,655,670,682,695,706,717,729,741,754,765,777,788],"tbody",{},[641,656,657,667],{},[658,659,660,663,664],"td",{"align":646},[20,661,662],{},"Stime"," × ",[20,665,666],{},"Ltime",[658,668,669],{"align":650},"1,00",[641,671,672,680],{},[658,673,674,663,677],{"align":646},[20,675,676],{},"swin",[20,678,679],{},"dwin",[658,681,669],{"align":650},[641,683,684,692],{},[658,685,686,663,689],{"align":646},[20,687,688],{},"dloss",[20,690,691],{},"Dpkts",[658,693,694],{"align":650},"0,99",[641,696,697,704],{},[658,698,699,663,702],{"align":646},[20,700,701],{},"dbytes",[20,703,688],{},[658,705,694],{"align":650},[641,707,708,714],{},[658,709,710,663,712],{"align":646},[20,711,607],{},[20,713,611],{},[658,715,716],{"align":650},"0,96",[641,718,719,727],{},[658,720,721,663,724],{"align":646},[20,722,723],{},"ct_srv_src",[20,725,726],{},"ct_srv_dst",[658,728,716],{"align":650},[641,730,731,739],{},[658,732,733,663,736],{"align":646},[20,734,735],{},"ct_dst_ltm",[20,737,738],{},"ct_src_dport_ltm",[658,740,716],{"align":650},[641,742,743,751],{},[658,744,745,663,748],{"align":646},[20,746,747],{},"tcprtt",[20,749,750],{},"synack",[658,752,753],{"align":650},"0,92",[641,755,756,763],{},[658,757,758,663,760],{"align":646},[20,759,747],{},[20,761,762],{},"ackdat",[658,764,753],{"align":650},[641,766,767,774],{},[658,768,769,663,771],{"align":646},[20,770,186],{},[20,772,773],{},"ct_state_ttl",[658,775,776],{"align":650},"0,91",[641,778,779,786],{},[658,780,781,663,783],{"align":646},[20,782,186],{},[20,784,785],{},"Label",[658,787,776],{"align":650},[641,789,790,796],{},[658,791,792,663,794],{"align":646},[20,793,773],{},[20,795,785],{},[658,797,798],{"align":650},"0,88",[11,800,801,802,528,804,806,807,810,811,813,814,817,818,820],{},"Alguns desses pares são redundância por construção, não coincidência: ",[20,803,662],{},[20,805,666],{}," são o instante de início e o instante de fim da mesma sessão, então correlacionar 1,00 é quase automático (a diferença entre os dois é literalmente a ",[20,808,809],{},"dur",", outra coluna). Já ",[20,812,186],{}," (TTL, \"time to live\", o contador que cada roteador no caminho decrementa até o pacote expirar) correlacionar ",[15,815,816],{},"0,91 direto com o rótulo de ataque"," é o achado mais interessante da EDA inteira: ferramenta de ataque automatizada costuma gerar pacote com TTL bem diferente do tráfego normal de navegador, uma \"sotaque\" técnico que sozinho já entrega bastante informação sobre quem tá do outro lado da conexão. Guardo esse nome, ",[20,819,186],{},", porque ele vai voltar a aparecer nos próximos dois capítulos.",[101,822,824],{"id":823},"boxplots-por-classe-inspeção-visual-antes-da-importância-do-modelo","Boxplots por classe: inspeção visual antes da importância do modelo",[11,826,827],{},"Antes de qualquer modelo entrar em cena, uma pergunta mais simples já ajuda a antecipar quais variáveis separam bem tráfego normal de ataque: será que a distribuição de uma feature muda visivelmente entre as duas classes?",[121,829,831],{"className":123,"code":830,"language":125,"meta":55,"style":55},"features_box = [\"dur\", \"sbytes\", \"dbytes\", \"Sload\", \"Dload\"]\n\nfig, axes = plt.subplots(1, len(features_box), figsize=(18, 5))\n\nfor i, feat in enumerate(features_box):\n    sns.boxplot(\n        data=df,\n        x=\"Label\",\n        y=feat,\n        ax=axes[i],\n        palette=[\"steelblue\", \"tomato\"],\n        showfliers=False,   # tirando outliers extremos para melhor visualização\n        hue=\"Label\",\n        legend=False\n    )\n    axes[i].set_title(feat)\n    axes[i].set_xticks([0, 1])\n    axes[i].set_xticklabels([\"Normal\", \"Ataque\"])\n\nplt.suptitle(\"Distribuição de Features por Classe\", y=1.02)\nplt.tight_layout()\nplt.show()\n",[20,832,833,838,842,847,851,856,861,866,871,876,881,886,891,896,901,906,911,916,922,927,933,939],{"__ignoreMap":55},[129,834,835],{"class":131,"line":132},[129,836,837],{},"features_box = [\"dur\", \"sbytes\", \"dbytes\", \"Sload\", \"Dload\"]\n",[129,839,840],{"class":131,"line":56},[129,841,158],{"emptyLinePlaceholder":61},[129,843,844],{"class":131,"line":143},[129,845,846],{},"fig, axes = plt.subplots(1, len(features_box), figsize=(18, 5))\n",[129,848,849],{"class":131,"line":149},[129,850,158],{"emptyLinePlaceholder":61},[129,852,853],{"class":131,"line":155},[129,854,855],{},"for i, feat in enumerate(features_box):\n",[129,857,858],{"class":131,"line":161},[129,859,860],{},"    sns.boxplot(\n",[129,862,863],{"class":131,"line":167},[129,864,865],{},"        data=df,\n",[129,867,868],{"class":131,"line":252},[129,869,870],{},"        x=\"Label\",\n",[129,872,873],{"class":131,"line":257},[129,874,875],{},"        y=feat,\n",[129,877,878],{"class":131,"line":263},[129,879,880],{},"        ax=axes[i],\n",[129,882,883],{"class":131,"line":269},[129,884,885],{},"        palette=[\"steelblue\", \"tomato\"],\n",[129,887,888],{"class":131,"line":274},[129,889,890],{},"        showfliers=False,   # tirando outliers extremos para melhor visualização\n",[129,892,893],{"class":131,"line":280},[129,894,895],{},"        hue=\"Label\",\n",[129,897,898],{"class":131,"line":286},[129,899,900],{},"        legend=False\n",[129,902,903],{"class":131,"line":292},[129,904,905],{},"    )\n",[129,907,908],{"class":131,"line":298},[129,909,910],{},"    axes[i].set_title(feat)\n",[129,912,913],{"class":131,"line":304},[129,914,915],{},"    axes[i].set_xticks([0, 1])\n",[129,917,919],{"class":131,"line":918},18,[129,920,921],{},"    axes[i].set_xticklabels([\"Normal\", \"Ataque\"])\n",[129,923,925],{"class":131,"line":924},19,[129,926,158],{"emptyLinePlaceholder":61},[129,928,930],{"class":131,"line":929},20,[129,931,932],{},"plt.suptitle(\"Distribuição de Features por Classe\", y=1.02)\n",[129,934,936],{"class":131,"line":935},21,[129,937,938],{},"plt.tight_layout()\n",[129,940,942],{"class":131,"line":941},22,[129,943,944],{},"plt.show()\n",[11,946,947,948,342,950,342,952,342,954,342,957,960,961,964],{},"Cada subgráfico compara a distribuição de uma feature (",[20,949,809],{},[20,951,607],{},[20,953,701],{},[20,955,956],{},"Sload",[20,958,959],{},"Dload",") entre sessão normal e sessão de ataque, lado a lado. A linha central da caixa é a mediana, a caixa cobre do 1º ao 3º quartil (os 50% centrais dos dados), e os \"bigodes\" mostram a dispersão típica. ",[20,962,963],{},"showfliers=False"," só tira os outliers mais extremos da imagem, pra ela não ficar achatada, eles continuam existindo nos dados, só não aparecem nesse desenho específico.",[11,966,967,968,971],{},"A leitura que interessa: quando a caixa de \"Normal\" e a caixa de \"Ataque\" ficam em faixas de valores bem diferentes, com pouca sobreposição, aquela variável sozinha já ajuda a separar as classes. Quando as duas caixas se sobrepõem quase por completo, a variável sozinha discrimina pouco. É uma inspeção visual rápida que ",[15,969,970],{},"antecipa"," o que a importância do Random Forest confirma de forma quantitativa lá no capítulo 3, mas sem esperar o modelo terminar de treinar pra ter uma primeira pista.",[101,973,975],{"id":974},"quem-são-as-colunas-categóricas-e-por-que-isso-já-é-um-alerta","Quem são as colunas categóricas, e por que isso já é um alerta",[11,977,978,979,982,983,342,986,342,989,342,992,342,995,342,998,342,1001,342,1004,528,1007,1009],{},"Separando as colunas que o pandas leu como texto (",[20,980,981],{},"select_dtypes(include=[\"object\"])","), aparecem nove: ",[20,984,985],{},"srcip",[20,987,988],{},"sport",[20,990,991],{},"dstip",[20,993,994],{},"dsport",[20,996,997],{},"proto",[20,999,1000],{},"state",[20,1002,1003],{},"service",[20,1005,1006],{},"ct_ftp_cmd",[20,1008,497],{},". Contando os valores mais frequentes de cada uma, dois grupos bem diferentes aparecem:",[121,1011,1013],{"className":123,"code":1012,"language":125,"meta":55,"style":55},"categoricas = df.select_dtypes(include=[\"object\"]).columns.tolist()\nfor col in categoricas:\n    print(f\"\\n{col}:\")\n    print(df[col].value_counts().head(10))\n",[20,1014,1015,1020,1025,1030],{"__ignoreMap":55},[129,1016,1017],{"class":131,"line":132},[129,1018,1019],{},"categoricas = df.select_dtypes(include=[\"object\"]).columns.tolist()\n",[129,1021,1022],{"class":131,"line":56},[129,1023,1024],{},"for col in categoricas:\n",[129,1026,1027],{"class":131,"line":143},[129,1028,1029],{},"    print(f\"\\n{col}:\")\n",[129,1031,1032],{"class":131,"line":149},[129,1033,1034],{},"    print(df[col].value_counts().head(10))\n",[11,1036,1037,1039,1040,1043,1044,1047,1048,1050,1051,1054,1055,1058,1059,1062,1063,1065,1066,1069,1070,1073,1074,1077,1078,1081],{},[20,1038,997],{}," é dominado por ",[20,1041,1042],{},"tcp"," (1.495.074 sessões) e ",[20,1045,1046],{},"udp"," (990.435), ",[20,1049,1000],{}," por ",[20,1052,1053],{},"FIN"," (1.478.689), ",[20,1056,1057],{},"CON"," (560.588) e ",[20,1060,1061],{},"INT"," (490.471), e ",[20,1064,1003],{}," tem um ",[20,1067,1068],{},"-"," enorme (1.246.397, serviço não identificado pelo sniffer), seguido de ",[20,1071,1072],{},"dns"," (781.668) e ",[20,1075,1076],{},"http"," (206.273). Essas três têm poucas categorias e sem ordem entre si, boas candidatas a ",[15,1079,1080],{},"one-hot encoding"," mais na frente.",[11,1083,1084,1085,342,1087,342,1089,528,1091,1093,1094,1096,1097,115,1100,1103,1104,528,1106,1108,1109,1112,1113,1115,1116,1119,1120,1123],{},"Já ",[20,1086,985],{},[20,1088,988],{},[20,1090,991],{},[20,1092,994],{}," são outra espécie: identificadores de altíssima cardinalidade. ",[20,1095,985],{}," sozinho tem só 10 valores únicos nesse laboratório (",[20,1098,1099],{},"59.166.0.0",[20,1101,1102],{},"59.166.0.9",", as máquinas atacantes\u002Fgeradoras de tráfego), mas ",[20,1105,988],{},[20,1107,994],{}," têm milhares. O motivo de eu não usar nenhuma dessas quatro no modelo não é falta de sinal, IP de origem provavelmente separa muito bem quem é máquina de ataque nesse laboratório específico, é o oposto: separa ",[15,1110,1111],{},"bem demais",", de um jeito que não generaliza. Um modelo treinado com ",[20,1114,985],{}," decoraria que \"",[20,1117,1118],{},"59.166.0.4"," é sempre suspeito\" em vez de aprender o ",[207,1121,1122],{},"comportamento"," de um ataque, e fora desse laboratório, com IPs completamente diferentes, essa regra decorada não vale nada. Volto nesse ponto com mais calma no capítulo 2, quando explico a troca de dataset.",[101,1125,1127],{"id":1126},"dados-ausentes-falta-estrutural-não-falha-de-coleta","Dados ausentes: falta estrutural, não falha de coleta",[121,1129,1131],{"className":123,"code":1130,"language":125,"meta":55,"style":55},"nulos = df.isnull().sum()\nnulos_percentual = (nulos \u002F len(df) * 100)\nresumo_nulos = pd.DataFrame({\"nulos\": nulos, \"percentual\": nulos_percentual}).sort_values(\"percentual\", ascending=False)\n",[20,1132,1133,1138,1143],{"__ignoreMap":55},[129,1134,1135],{"class":131,"line":132},[129,1136,1137],{},"nulos = df.isnull().sum()\n",[129,1139,1140],{"class":131,"line":56},[129,1141,1142],{},"nulos_percentual = (nulos \u002F len(df) * 100)\n",[129,1144,1145],{"class":131,"line":143},[129,1146,1147],{},"resumo_nulos = pd.DataFrame({\"nulos\": nulos, \"percentual\": nulos_percentual}).sort_values(\"percentual\", ascending=False)\n",[474,1149],{":points":1150,"color":502,"x-label":1151,"y-label":1152},"[{\"label\":\"attack_cat\",\"value\":87.35},{\"label\":\"is_ftp_login\",\"value\":56.29},{\"label\":\"ct_flw_http_mthd\",\"value\":53.08}]","coluna","% de valores ausentes",[11,1154,1155,1156,1159],{},"Só três colunas, de 49, têm nulo de verdade (as outras 46 ficam com 0,00%). Minha primeira versão desse gráfico era um ",[207,1157,1158],{},"heatmap"," de 5.000 linhas amostradas, um jeito clássico de visualizar padrão de nulo, mas com só 3 colunas afetadas de 49 ele virava uma tira estreita sem informação nenhuma, tempo perdido olhando pra uma imagem quase em branco. Troquei pra um gráfico de barra direto do percentual por coluna, que responde exatamente a pergunta que eu queria fazer: \"quanto falta em cada coluna?\" Foi minha primeira lição prática desse capítulo: o gráfico \"certo\" na teoria não é sempre o gráfico certo pro seu dado específico.",[11,1161,1162,1163,1165,1166,1169,1170,1173],{},"E os três nulos que existem contam uma história estrutural, não um problema de coleta. ",[20,1164,497],{}," (87,35%) só é preenchida quando a sessão é ataque, os 87,35% vazios são exatamente as sessões normais, a mesma coisa que já expliquei lá em cima. ",[20,1167,1168],{},"is_ftp_login"," (56,29%) e ",[20,1171,1172],{},"ct_flw_http_mthd"," (53,08%) só fazem sentido pra sessões FTP e HTTP respectivamente, sessão de outro protocolo não tem o que preencher ali. Não tem imputação cega a fazer aqui, porque a ausência em si já é informação (essa sessão não é FTP, por exemplo), apagar ou preencher essas colunas com qualquer valor destruiria esse sinal.",[101,1175,1177],{"id":1176},"um-bug-sutil-quando-vazio-não-conta-como-nulo","Um bug sutil: quando \"vazio\" não conta como nulo",[11,1179,1180,1181,1183],{},"Falei que só três colunas têm nulo. Mas reparei em algo estranho olhando os valores de ",[20,1182,1006],{}," (quantidade de comandos FTP na sessão, que deveria ser um número) na lista de categóricas:",[121,1185,1187],{"className":123,"code":1186,"language":125,"meta":55,"style":55},"ct_ftp_cmd:\n     1429879\n0     686192\n0     380306\n1      29605\n1      10472\n",[20,1188,1189,1194,1199,1204,1209,1214],{"__ignoreMap":55},[129,1190,1191],{"class":131,"line":132},[129,1192,1193],{},"ct_ftp_cmd:\n",[129,1195,1196],{"class":131,"line":56},[129,1197,1198],{},"     1429879\n",[129,1200,1201],{"class":131,"line":143},[129,1202,1203],{},"0     686192\n",[129,1205,1206],{"class":131,"line":149},[129,1207,1208],{},"0     380306\n",[129,1210,1211],{"class":131,"line":155},[129,1212,1213],{},"1      29605\n",[129,1215,1216],{"class":131,"line":161},[129,1217,1218],{},"1      10472\n",[11,1220,1221,1222,1225,1226,1229,1230,1233,1234,1236,1237,1239,1240,1242,1243,1246,1247,1250,1251,1254,1255,1258,1259,1261,1262,1264,1265,1267],{},"Duas pistas aqui. Primeira: a categoria mais comum, com 1.429.879 sessões, não tem rótulo nenhum, é uma ",[15,1223,1224],{},"string vazia"," (",[20,1227,1228],{},"\"\"","), não um ",[20,1231,1232],{},"NaN",". E 1.429.879 é exatamente o número de nulos de ",[20,1235,1168],{}," lá de cima, ou seja, toda sessão sem login FTP também não tem ",[20,1238,1006],{}," preenchido, faz sentido, mas o pandas não contou isso como nulo porque tecnicamente não é ",[20,1241,1232],{},", é uma string vazia disfarçada de categoria válida. ",[20,1244,1245],{},"df.isnull().sum()"," simplesmente não via esse problema. Segunda pista: ",[20,1248,1249],{},"\"0\""," aparece ",[15,1252,1253],{},"duas vezes"," na lista, uma com 686.192 sessões e outra com 380.306, porque uma tem espaço em branco colado (",[20,1256,1257],{},"\"0 \"",") e a outra não, o mesmo tipo de sujeira de digitação que já vi em ",[20,1260,497],{},". Essas duas coisas combinadas (string vazia em vez de nulo, mais espaço em branco duplicando categoria) são o motivo de ",[20,1263,1006],{}," ter virado ",[20,1266,458],{}," lá na tabela de dtypes: uma coluna que deveria ser puramente numérica ficou \"contaminada\" o suficiente pro pandas desistir de ler como número.",[11,1269,1270,1271,1273,1274,1277,1278,1280,1281,1284,1285,1288,1289,42],{},"A lição que fica: ",[20,1272,1245],{}," é um bom primeiro filtro, mas não é a palavra final sobre o que tá faltando. Uma string vazia, um espaço sozinho, um ",[20,1275,1276],{},"\"-\""," genérico (como o que já vi em ",[20,1279,1003],{},"), tudo isso passa batido por um ",[20,1282,1283],{},".isnull()"," ingênuo. Vale sempre cruzar com ",[20,1286,1287],{},"df.dtypes"," e desconfiar de toda coluna numérica por natureza que virou ",[20,1290,458],{},[101,1292,1294],{"id":1293},"selecionando-ou-não-características","Selecionando (ou não) características",[11,1296,1297,1298,1301],{},"Com a EDA numérica e categórica na mão, chega a pergunta central de qualquer projeto com muitas variáveis: quais delas eu realmente uso? A disciplina trata essa escolha como ",[15,1299,1300],{},"seleção de características",", e existem três famílias clássicas:",[1303,1304,1305,1312,1318],"ul",{},[1306,1307,1308,1311],"li",{},[15,1309,1310],{},"Filtro (filter):"," critério estatístico, independente de qualquer modelo. A análise de redundância por correlação lá em cima é exatamente isso: duas variáveis com r ≈ 1,00 carregam quase a mesma informação, uma seria candidata a sair.",[1306,1313,1314,1317],{},[15,1315,1316],{},"Wrapper:"," testar subconjuntos de variáveis rodando o próprio modelo de verdade (por exemplo, eliminação recursiva de características). Poderoso, mas caro, porque cada subconjunto testado é um treino completo.",[1306,1319,1320,1323],{},[15,1321,1322],{},"Embutida (embedded):"," o próprio modelo pontua a importância de cada variável enquanto treina. É o que a importância da Random Forest faz, e eu chego lá no capítulo 3.",[11,1325,1326,1327,1330],{},"Antes de decidir qualquer coisa, rodei um histograma em oito features numéricas escolhidas só pra inspecionar a ",[207,1328,1329],{},"forma"," das distribuições, não pra ranquear importância:",[121,1332,1334],{"className":123,"code":1333,"language":125,"meta":55,"style":55},"features_ilustrativas = [\"dur\", \"sbytes\", \"dbytes\", \"sttl\", \"dttl\", \"sloss\", \"dloss\", \"Sload\"]\n\naxes = df[features_ilustrativas].hist(figsize=(14, 8), bins=50, color=\"steelblue\",\n                                      edgecolor=\"black\", log=True)\nfor ax in axes.ravel():\n    ax.set_xlabel(\"Valor da feature\")\n    ax.set_ylabel(\"Frequencia (escala log)\")\n\nplt.suptitle(\"Distribuicao de Features Numericas (amostra ilustrativa)\", y=1.02)\nplt.tight_layout()\nplt.show()\n",[20,1335,1336,1341,1345,1350,1355,1360,1365,1370,1374,1379,1383],{"__ignoreMap":55},[129,1337,1338],{"class":131,"line":132},[129,1339,1340],{},"features_ilustrativas = [\"dur\", \"sbytes\", \"dbytes\", \"sttl\", \"dttl\", \"sloss\", \"dloss\", \"Sload\"]\n",[129,1342,1343],{"class":131,"line":56},[129,1344,158],{"emptyLinePlaceholder":61},[129,1346,1347],{"class":131,"line":143},[129,1348,1349],{},"axes = df[features_ilustrativas].hist(figsize=(14, 8), bins=50, color=\"steelblue\",\n",[129,1351,1352],{"class":131,"line":149},[129,1353,1354],{},"                                      edgecolor=\"black\", log=True)\n",[129,1356,1357],{"class":131,"line":155},[129,1358,1359],{},"for ax in axes.ravel():\n",[129,1361,1362],{"class":131,"line":161},[129,1363,1364],{},"    ax.set_xlabel(\"Valor da feature\")\n",[129,1366,1367],{"class":131,"line":167},[129,1368,1369],{},"    ax.set_ylabel(\"Frequencia (escala log)\")\n",[129,1371,1372],{"class":131,"line":252},[129,1373,158],{"emptyLinePlaceholder":61},[129,1375,1376],{"class":131,"line":257},[129,1377,1378],{},"plt.suptitle(\"Distribuicao de Features Numericas (amostra ilustrativa)\", y=1.02)\n",[129,1380,1381],{"class":131,"line":263},[129,1382,938],{},[129,1384,1385],{"class":131,"line":269},[129,1386,944],{},[11,1388,1389,1390,1393,1394,1397],{},"O comentário que deixei junto dessa célula no notebook original vale reproduzir aqui, porque evita uma leitura errada: essas oito features são ",[15,1391,1392],{},"ilustrativas",", uma amostra pra inspecionar a forma da distribuição, não o resultado de nenhuma seleção por importância (a importância de verdade vem da correlação, já vista, e da importância do Random Forest, lá no capítulo 3). O eixo Y usa escala logarítmica (",[20,1395,1396],{},"log=True",") de propósito: bytes trafegados e duração de sessão têm distribuição extremamente assimétrica, a maioria das sessões é curta e leve, uma minoria é muito mais pesada, e numa escala linear essa minoria esmagaria o gráfico até a maioria virar uma linha reta no zero.",[11,1399,1400,1401,1404,1405,1408],{},"Com essa inspeção visual, mais os boxplots por classe e a correlação já vistos lá em cima, já vale registrar a decisão que tomei: ",[15,1402,1403],{},"não vou podar nenhuma feature no pipeline",". A Random Forest que uso no capítulo 3 é razoavelmente robusta a variáveis redundantes e a diferença de escala entre elas, então trato essa seleção como ",[15,1406,1407],{},"análise interpretativa"," (entender o que pesa na decisão) em vez de uma etapa que efetivamente altera o que entra no modelo. É uma escolha consciente, não preguiça: uma Regressão Logística ou um KNN provavelmente se beneficiariam de cortar a redundância que achei aqui, mas pro modelo que escolho no próximo capítulo o ganho não compensaria a complexidade extra de decidir \"qual das duas colunas quase-idênticas eu corto\".",[101,1410,1412],{"id":1411},"principais-insights-antes-de-modelar","Principais insights, antes de modelar",[11,1414,1415],{},"Fechando esse primeiro capítulo, os pontos que carrego comigo pro resto do projeto:",[635,1417,1418,1428],{},[638,1419,1420],{},[641,1421,1422,1425],{},[644,1423,1424],{"align":646},"O que a EDA mostrou",[644,1426,1427],{"align":646},"Por que importa lá na frente",[653,1429,1430,1438,1452,1464,1476,1484],{},[641,1431,1432,1435],{},[658,1433,1434],{"align":646},"Desbalanceamento de 12,6% de ataque",[658,1436,1437],{"align":646},"Acurácia sozinha engana, F1 e AUC vão ser as métricas que realmente confio",[641,1439,1440,1449],{},[658,1441,1442,342,1444,528,1446,1448],{"align":646},[20,1443,497],{},[20,1445,1168],{},[20,1447,1172],{}," têm nulo estrutural",[658,1450,1451],{"align":646},"Não vou imputar às cegas, a ausência em si já é sinal",[641,1453,1454,1459],{},[658,1455,1456,1458],{"align":646},[20,1457,1006],{}," escondia dado ausente como string vazia",[658,1460,1461,1463],{"align":646},[20,1462,1283],{}," sozinho não basta, sempre cruzar com dtype",[641,1465,1466,1473],{},[658,1467,1468,528,1470,1472],{"align":646},[20,1469,497],{},[20,1471,1006],{}," tinham categoria duplicada por espaço extra",[658,1474,1475],{"align":646},"Corrigido antes de qualquer contagem valer a pena",[641,1477,1478,1481],{},[658,1479,1480],{"align":646},"Metade das 40 variáveis numéricas é redundante entre si (correlação ≥ 0,8)",[658,1482,1483],{"align":646},"Random Forest tolera isso, mas registrar a redundância importa pra interpretar depois",[641,1485,1486,1497],{},[658,1487,1488,518,1490,518,1492,518,1494,1496],{"align":646},[20,1489,985],{},[20,1491,991],{},[20,1493,988],{},[20,1495,994],{}," separam bem demais, de um jeito que não generaliza",[658,1498,1499],{"align":646},"Descartados no capítulo 2, junto com a explicação completa do porquê",[11,1501,1502],{},"No capítulo 2 eu troco o dataset bruto que usei esse capítulo inteiro pela partição oficial que os próprios autores do UNSW-NB15 curaram e publicaram, explico com detalhe o porquê dessa troca (é sobre vazamento de dado, não capricho), monto o pipeline de pré-processamento e comparo quatro famílias de modelo numa validação cruzada justa antes de escolher qual treinar de verdade.",[1504,1505,1506],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":55,"searchDepth":56,"depth":56,"links":1508},[1509,1510,1511,1512,1513,1514,1515,1516,1517],{"id":103,"depth":56,"text":104},{"id":490,"depth":56,"text":491},{"id":555,"depth":56,"text":556},{"id":823,"depth":56,"text":824},{"id":974,"depth":56,"text":975},{"id":1126,"depth":56,"text":1127},{"id":1176,"depth":56,"text":1177},{"id":1293,"depth":56,"text":1294},{"id":1411,"depth":56,"text":1412},"2026-08-26","Capítulo 1 do meu trabalho final de Reconhecimento de Padrões: a análise exploratória completa do UNSW-NB15 nos arquivos brutos, desbalanceamento, dados ausentes, um bug de qualidade que só achei porque desconfiei do dtype errado, redundância entre variáveis e a decisão consciente de não podar nenhuma feature.",{},"\u002Fpt\u002Fprojects\u002Fnetwork-intrusion-detection\u002F01-explorando-25-milhoes-de-sessoes","network-intrusion-detection",{"title":75,"description":1519},"pt\u002Fprojects\u002Fnetwork-intrusion-detection\u002F01-explorando-25-milhoes-de-sessoes",[69,1526,1527,1528],"eda","unsw-nb15","selecao-de-caracteristicas","y9UEazawMIWUhlwBSPzNQHo8bg0P9AYxussV_Y6nF34",{"id":1531,"title":1532,"body":1533,"cover":3,"date":1518,"description":2102,"extension":59,"meta":2103,"navigation":61,"order":56,"path":2104,"project":1522,"seo":2105,"status":64,"stem":2106,"tags":2107,"__hash__":2109},"projectChapters\u002Fpt\u002Fprojects\u002Fnetwork-intrusion-detection\u002F02-metodologia-selecao-de-modelo.md","Trocando de Dataset e Escolhendo um Candidato",{"type":8,"value":1534,"toc":2095},[1535,1550,1554,1580,1601,1608,1611,1626,1633,1644,1648,1651,1737,1750,1784,1795,1801,1805,1849,1884,1913,1917,1940,1943,1976,2025,2069,2075,2086,2090,2093],[11,1536,1537,1540,1541,342,1543,342,1545,528,1547,1549],{},[28,1538,1539],{"href":48},"No capítulo 1"," eu explorei os 2,5 milhões de sessões brutas do UNSW-NB15: desbalanceamento, dados ausentes, um bug de qualidade escondido, redundância entre variáveis, e a decisão de não podar nenhuma feature. Terminei apontando um problema que ainda não resolvi: ",[20,1542,985],{},[20,1544,991],{},[20,1546,988],{},[20,1548,994],{}," separam bem demais, de um jeito que não generaliza. Esse capítulo começa resolvendo exatamente isso, trocando de dataset, e termina escolhendo, com critério, qual família de modelo treinar.",[101,1551,1553],{"id":1552},"por-que-trocar-de-dataset-vazamento-de-dado","Por que trocar de dataset: vazamento de dado",[11,1555,1556,1557,342,1559,342,1561,342,1563,1565,1566,518,1568,1570,1571,1225,1574,528,1576,1579],{},"O UNSW-NB15 vem em dois formatos. Um é o que usei o capítulo inteiro passado: os quatro CSVs brutos, com todos os 49 campos, incluindo ",[20,1558,985],{},[20,1560,991],{},[20,1562,988],{},[20,1564,994],{}," e os timestamps ",[20,1567,662],{},[20,1569,666],{},". O outro é a ",[15,1572,1573],{},"partição oficial",[20,1575,333],{},[20,1577,1578],{},"UNSW_NB15_testing-set.csv","), um subconjunto que os próprios autores do dataset (Moustafa & Slay, 2015) já separaram em treino e teste, sem esses identificadores.",[11,1581,1582,1583,1225,1586,1589,1590,1592,1593,1595,1596,518,1598,1600],{},"Escolho a partição oficial pra modelar, e a razão central é ",[15,1584,1585],{},"vazamento de dado",[207,1587,1588],{},"data leakage","): um IP ou uma porta identifica uma máquina específica desse laboratório, não um padrão de comportamento de ataque. Se eu deixasse ",[20,1591,985],{}," entrar no modelo, ele aprenderia algo como \"",[20,1594,1118],{}," é sempre suspeito\" em vez de aprender o que faz um ataque parecer um ataque (a duração da sessão, o volume de bytes, o TTL, e por aí vai). Isso infla o desempenho artificialmente dentro desse laboratório específico e não generaliza pra nenhuma rede fora dele. Os timestamps ",[20,1597,662],{},[20,1599,666],{}," têm o mesmo problema: fluxos próximos no tempo tendem a ser correlacionados entre si (mesma sessão de ataque gerando várias linhas seguidas), e usar o timestamp bruto deixaria essa correlação vazar pro modelo.",[11,1602,1603,1604,1607],{},"Tem um segundo motivo, menos sobre vazamento e mais sobre método científico: a partição oficial é o ",[15,1605,1606],{},"benchmark padrão da literatura"," sobre esse dataset. Treinar e avaliar nela deixa meu resultado comparável com outros trabalhos publicados usando o mesmo dataset, o que eu perderia se inventasse meu próprio split aleatório em cima dos arquivos brutos, e correria o risco adicional de vazamento entre treino e teste por causa de fluxos correlacionados (mesmo host, mesma sessão) caindo dos dois lados do split.",[11,1609,1610],{},"A troca tem um custo real: abro mão de 2,5 milhões de linhas por um conjunto bem menor.",[121,1612,1614],{"className":123,"code":1613,"language":125,"meta":55,"style":55},"df_train = pd.read_csv(\"\u002Fcontent\u002Fdata\u002FUNSW_NB15_training-set.csv\")\ndf_test  = pd.read_csv(\"\u002Fcontent\u002Fdata\u002FUNSW_NB15_testing-set.csv\")\n",[20,1615,1616,1621],{"__ignoreMap":55},[129,1617,1618],{"class":131,"line":132},[129,1619,1620],{},"df_train = pd.read_csv(\"\u002Fcontent\u002Fdata\u002FUNSW_NB15_training-set.csv\")\n",[129,1622,1623],{"class":131,"line":56},[129,1624,1625],{},"df_test  = pd.read_csv(\"\u002Fcontent\u002Fdata\u002FUNSW_NB15_testing-set.csv\")\n",[172,1627,1628],{},[11,1629,1630,1632],{},[15,1631,178],{}," treino com 82.332 linhas, teste com 175.341, cada uma com 45 colunas.",[11,1634,1635,1636,1639,1640,1643],{},"Repara que o teste é ",[15,1637,1638],{},"maior"," que o treino aqui, o oposto do que eu esperaria de um split 80\u002F20 comum. Os autores do UNSW-NB15 desenharam de propósito um conjunto de teste generoso, justamente pra dar uma medida de generalização mais confiável. E tem outra inversão escondida: na partição oficial, a classe ataque vira ",[15,1641,1642],{},"majoritária"," no teste (mais adiante, no capítulo 3, isso aparece nos números: 119.341 sessões de ataque contra 56.000 normais), o oposto da proporção que vi na EDA dos arquivos brutos. É um lembrete de que EDA e modelagem, aqui, não rodam sobre o mesmo dataframe, e as proporções de um não valem pro outro.",[101,1645,1647],{"id":1646},"as-colunas-da-partição-oficial","As colunas da partição oficial",[11,1649,1650],{},"As 45 colunas da partição não são as mesmas 49 dos arquivos brutos. Os autores já removeram os identificadores e os timestamps, e organizaram o que sobrou em grupos que fazem sentido de ler juntos:",[1303,1652,1653,1667,1694,1716,1726],{},[1306,1654,1655,179,1658,1660,1661,342,1663,342,1665,42],{},[15,1656,1657],{},"Básicas do fluxo:",[20,1659,809],{}," (duração), ",[20,1662,997],{},[20,1664,1003],{},[20,1666,1000],{},[1306,1668,1669,179,1672,518,1675,1678,1679,518,1681,1683,1684,342,1687,518,1690,1693],{},[15,1670,1671],{},"Volume e taxa:",[20,1673,1674],{},"spkts",[20,1676,1677],{},"dpkts"," (pacotes), ",[20,1680,607],{},[20,1682,701],{}," (bytes), ",[20,1685,1686],{},"rate",[20,1688,1689],{},"sload",[20,1691,1692],{},"dload"," (carga).",[1306,1695,1696,179,1699,518,1701,1704,1705,518,1707,1709,1710,342,1712,342,1714,42],{},[15,1697,1698],{},"TCP e tempo:",[20,1700,186],{},[20,1702,1703],{},"dttl"," (o TTL que já apareceu no capítulo 1), ",[20,1706,676],{},[20,1708,679],{}," (janela TCP), ",[20,1711,747],{},[20,1713,750],{},[20,1715,762],{},[1306,1717,1718,1725],{},[15,1719,1720,1721,1724],{},"Contadores de conexão (",[20,1722,1723],{},"ct_...","):"," quantas conexões recentes compartilham serviço, host ou porta, a família de variável desenhada especificamente pra capturar varredura e repetição.",[1306,1727,1728,179,1731,1734,1735,42],{},[15,1729,1730],{},"Alvos:",[20,1732,1733],{},"label"," (0\u002F1) e ",[20,1736,497],{},[11,1738,1739,1740,1743,1744,342,1746,342,1748,1724],{},"Das 42 colunas de entrada (descartando ",[20,1741,1742],{},"id"," e os dois alvos), 39 são numéricas e 3 são categóricas (",[20,1745,997],{},[20,1747,1003],{},[20,1749,1000],{},[121,1751,1753],{"className":123,"code":1752,"language":125,"meta":55,"style":55},"DESCARTAR = [\"id\", \"attack_cat\", \"label\"]\nX_train = df_train.drop(columns=[c for c in DESCARTAR if c in df_train.columns])\ny_train = df_train[\"label\"]\n\ncat_cols = X_train.select_dtypes(include=[\"object\"]).columns.tolist()\nnum_cols = X_train.select_dtypes(include=[\"number\"]).columns.tolist()\n",[20,1754,1755,1760,1765,1770,1774,1779],{"__ignoreMap":55},[129,1756,1757],{"class":131,"line":132},[129,1758,1759],{},"DESCARTAR = [\"id\", \"attack_cat\", \"label\"]\n",[129,1761,1762],{"class":131,"line":56},[129,1763,1764],{},"X_train = df_train.drop(columns=[c for c in DESCARTAR if c in df_train.columns])\n",[129,1766,1767],{"class":131,"line":143},[129,1768,1769],{},"y_train = df_train[\"label\"]\n",[129,1771,1772],{"class":131,"line":149},[129,1773,158],{"emptyLinePlaceholder":61},[129,1775,1776],{"class":131,"line":155},[129,1777,1778],{},"cat_cols = X_train.select_dtypes(include=[\"object\"]).columns.tolist()\n",[129,1780,1781],{"class":131,"line":161},[129,1782,1783],{},"num_cols = X_train.select_dtypes(include=[\"number\"]).columns.tolist()\n",[172,1785,1786],{},[11,1787,1788,1790,1791,1794],{},[15,1789,178],{}," categóricas ",[20,1792,1793],{},"['proto', 'service', 'state']",", 39 numéricas.",[11,1796,1797,1800],{},[20,1798,1799],{},"select_dtypes"," separa sozinho as 3 categóricas e as 39 numéricas, sem eu precisar digitar nome de coluna na mão.",[101,1802,1804],{"id":1803},"montando-o-pipeline-de-pré-processamento","Montando o pipeline de pré-processamento",[121,1806,1808],{"className":123,"code":1807,"language":125,"meta":55,"style":55},"from sklearn.compose import ColumnTransformer\nfrom sklearn.preprocessing import StandardScaler, OneHotEncoder\n\ntransformadores = [\n    (\"num\", StandardScaler(), num_cols),\n    (\"cat\", OneHotEncoder(handle_unknown=\"ignore\", sparse_output=False), cat_cols),\n]\npreprocessador = ColumnTransformer(transformers=transformadores)\n",[20,1809,1810,1815,1820,1824,1829,1834,1839,1844],{"__ignoreMap":55},[129,1811,1812],{"class":131,"line":132},[129,1813,1814],{},"from sklearn.compose import ColumnTransformer\n",[129,1816,1817],{"class":131,"line":56},[129,1818,1819],{},"from sklearn.preprocessing import StandardScaler, OneHotEncoder\n",[129,1821,1822],{"class":131,"line":143},[129,1823,158],{"emptyLinePlaceholder":61},[129,1825,1826],{"class":131,"line":149},[129,1827,1828],{},"transformadores = [\n",[129,1830,1831],{"class":131,"line":155},[129,1832,1833],{},"    (\"num\", StandardScaler(), num_cols),\n",[129,1835,1836],{"class":131,"line":161},[129,1837,1838],{},"    (\"cat\", OneHotEncoder(handle_unknown=\"ignore\", sparse_output=False), cat_cols),\n",[129,1840,1841],{"class":131,"line":167},[129,1842,1843],{},"]\n",[129,1845,1846],{"class":131,"line":252},[129,1847,1848],{},"preprocessador = ColumnTransformer(transformers=transformadores)\n",[11,1850,1851,1853,1854,1857,1858,1861,1862,1865,1866,1869,1870,1872,1873,1875,1876,1883],{},[20,1852,22],{}," aplica, em paralelo, um transformador diferente pra cada grupo de coluna: ",[20,1855,1856],{},"StandardScaler"," nas 39 numéricas (centra em média 0 e desvio 1, essencial pra Regressão Logística, inócuo pra árvore) e ",[20,1859,1860],{},"OneHotEncoder"," nas 3 categóricas, ",[28,1863,1864],{"href":538},"o mesmo par que já apareceu no Titanic",", agora resolvendo protocolo de rede em vez de porto de embarque. Um detalhe que vale registrar: ",[20,1867,1868],{},"handle_unknown=\"ignore\"",". Se o teste trouxer um valor de ",[20,1871,1003],{}," que o treino nunca viu, em vez de estourar erro, o ",[20,1874,1860],{}," simplesmente zera aquela linha inteira pra aquela variável, ",[28,1877,1879,1880],{"href":1878},"\u002Fplaylists\u002Fpattern-recognition\u002Fnlp-intro","o mesmo espírito do token ",[20,1881,1882],{},"[UNK]"," que já vi resolvendo palavra desconhecida.",[11,1885,1886,1887,1890,1891,1894,1895,1898,1899,1902,1903,1905,1906,1909,1910,1912],{},"Essa célula não devolve gráfico nem número, só ",[15,1888,1889],{},"monta"," o transformador. E é exatamente aí que mora o ponto mais importante dela: eu não chamo ",[20,1892,1893],{},".fit()"," nesse ",[20,1896,1897],{},"preprocessador"," sozinho, ele entra inteiro dentro de um ",[20,1900,1901],{},"Pipeline",", junto com o classificador. Isso importa porque, na validação cruzada que vem a seguir, cada fold treina seu próprio ",[20,1904,1856],{}," só com o pedaço de treino daquele fold, a média e o desvio nunca veem o pedaço de validação. Se eu tivesse rodado ",[20,1907,1908],{},"preprocessador.fit_transform(X_train)"," antes de dividir os folds, a média e o desvio calculados em cima do treino inteiro vazariam informação estatística do fold de validação pro treino, um vazamento sutil (bem menor que o dos IPs, mas real) que o ",[20,1911,1901],{}," evita de graça.",[101,1914,1916],{"id":1915},"comparando-quatro-candidatos-numa-amostra-rápida","Comparando quatro candidatos, numa amostra rápida",[11,1918,1919,1920,1923,1924,1927,1928,1931,1932,1935,1936,1939],{},"Antes de escolher um modelo, defini o critério ",[15,1921,1922],{},"antes de ver qualquer número",": prioridade pro ",[15,1925,1926],{},"F1-score"," (equilíbrio entre não deixar ataque passar e não gerar alarme falso), com ",[15,1929,1930],{},"AUC"," como desempate. Como as classes são desbalanceadas, uso ",[20,1933,1934],{},"class_weight=\"balanced\""," onde o modelo aceita, e validação cruzada ",[15,1937,1938],{},"estratificada"," (mantém a proporção de classes em cada fold).",[11,1941,1942],{},"Testei quatro famílias bem diferentes entre si, cada uma carregando uma hipótese diferente sobre o dado:",[1303,1944,1945,1951,1961,1970],{},[1306,1946,1947,1950],{},[15,1948,1949],{},"Regressão Logística:"," assume fronteira de decisão linear, referência simples e interpretável.",[1306,1952,1953,1956,1957,1960],{},[15,1954,1955],{},"Árvore de Decisão:"," captura relação não linear, mas tende a ",[207,1958,1959],{},"overfitting"," sozinha.",[1306,1962,1963,1966,1967,42],{},[15,1964,1965],{},"Random Forest:"," agrega muitas árvores, reduzindo variância via ",[207,1968,1969],{},"bagging",[1306,1971,1972,1975],{},[15,1973,1974],{},"Naive Bayes Gaussiano:"," assume independência entre variáveis dado a classe, rápido, uma linha de base.",[121,1977,1979],{"className":123,"code":1978,"language":125,"meta":55,"style":55},"modelos = {\n    \"Regressao Logistica\": LogisticRegression(max_iter=1000, class_weight=\"balanced\"),\n    \"Arvore de Decisao\": DecisionTreeClassifier(random_state=42, class_weight=\"balanced\"),\n    \"Random Forest\": RandomForestClassifier(n_estimators=100, random_state=42, class_weight=\"balanced\", n_jobs=-1),\n    \"Naive Bayes\": GaussianNB(),\n}\n\nscore = lambda clf, m: cross_val_score(Pipeline([(\"prep\", preprocessador), (\"clf\", clf)]), Xs, ys, cv=cv, scoring=m, n_jobs=-1).mean()\nlinhas = [{\"Modelo\": n, \"F1\": score(c, \"f1\"), \"AUC\": score(c, \"roc_auc\")} for n, c in modelos.items()]\n",[20,1980,1981,1986,1991,1996,2001,2006,2011,2015,2020],{"__ignoreMap":55},[129,1982,1983],{"class":131,"line":132},[129,1984,1985],{},"modelos = {\n",[129,1987,1988],{"class":131,"line":56},[129,1989,1990],{},"    \"Regressao Logistica\": LogisticRegression(max_iter=1000, class_weight=\"balanced\"),\n",[129,1992,1993],{"class":131,"line":143},[129,1994,1995],{},"    \"Arvore de Decisao\": DecisionTreeClassifier(random_state=42, class_weight=\"balanced\"),\n",[129,1997,1998],{"class":131,"line":149},[129,1999,2000],{},"    \"Random Forest\": RandomForestClassifier(n_estimators=100, random_state=42, class_weight=\"balanced\", n_jobs=-1),\n",[129,2002,2003],{"class":131,"line":155},[129,2004,2005],{},"    \"Naive Bayes\": GaussianNB(),\n",[129,2007,2008],{"class":131,"line":161},[129,2009,2010],{},"}\n",[129,2012,2013],{"class":131,"line":167},[129,2014,158],{"emptyLinePlaceholder":61},[129,2016,2017],{"class":131,"line":252},[129,2018,2019],{},"score = lambda clf, m: cross_val_score(Pipeline([(\"prep\", preprocessador), (\"clf\", clf)]), Xs, ys, cv=cv, scoring=m, n_jobs=-1).mean()\n",[129,2021,2022],{"class":131,"line":257},[129,2023,2024],{},"linhas = [{\"Modelo\": n, \"F1\": score(c, \"f1\"), \"AUC\": score(c, \"roc_auc\")} for n, c in modelos.items()]\n",[11,2026,2027,2028,2031,2032,2035,2036,2039,2040,2043,2044,2047,2048,2050,2051,2053,2054,2057,2058,2060,2061,2064,2065,2068],{},"Vale abrir esse bloco em câmera lenta, porque ele empacota bastante coisa em duas linhas. ",[20,2029,2030],{},"modelos"," é só um dicionário nome → instância de classificador, os quatro candidatos que eu quero comparar. ",[20,2033,2034],{},"score"," é uma função de uma linha só (",[20,2037,2038],{},"lambda","): recebe um classificador ",[20,2041,2042],{},"clf"," e o nome de uma métrica ",[20,2045,2046],{},"m",", monta um ",[20,2049,1901],{}," novo grudando o mesmo ",[20,2052,1897],{}," na frente ",[15,2055,2056],{},"daquele"," classificador específico (cada candidato precisa do próprio pipeline, não dá pra reaproveitar um pipeline já treinado por outro modelo), roda validação cruzada de 3 dobras nesse pipeline (numa amostra de 20 mil sessões de treino, rodar isso nas 82 mil inteiras a cada candidato custaria tempo à toa nessa fase de triagem), e tira a média das 3 notas. A linha de baixo chama essa função ",[20,2059,2034],{}," duas vezes pra cada um dos 4 modelos, uma vez pedindo ",[20,2062,2063],{},"\"f1\"",", outra pedindo ",[20,2066,2067],{},"\"roc_auc\"",". 4 modelos × 2 métricas × 3 dobras: 24 treinos completos, escondidos atrás de duas linhas de código.",[2070,2071],"grouped-bar-chart",{":categories":2072,":series":2073,"y-label":2074},"[\"Regressão Logística\",\"Árvore de Decisão\",\"Random Forest\",\"Naive Bayes\"]","[{\"name\":\"F1 (classe ataque)\",\"values\":[0.92,0.95,0.97,0.54],\"color\":\"#0033cc\"},{\"name\":\"AUC\",\"values\":[0.98,0.95,1.00,0.72],\"color\":\"#cc3300\"}]","métrica",[11,2076,2077,2078,2082,2083,2085],{},"Random Forest ganha nas duas métricas (F1 0,97, AUC 1,00 nessa amostra de triagem), a Árvore de Decisão sozinha já vem logo atrás, e o Naive Bayes Gaussiano fica bem pra trás dos outros três (F1 0,54). Faz sentido pelo mesmo motivo ",[28,2079,2081],{"href":2080},"\u002Fplaylists\u002Fpattern-recognition\u002Fpca","que já vi na PCA do câncer de mama",": Naive Bayes assume que as variáveis são independentes entre si dado a classe, e o capítulo 1 já mostrou que boa parte das 39 variáveis numéricas são tudo menos independentes, várias contam a mesma história com palavras diferentes. A expectativa teórica também bate: Random Forest combina o baixo viés das árvores com a baixa variância do ",[207,2084,1969],{},", exatamente o tipo de robustez que eu esperaria lidar bem com as 39 variáveis, boa parte redundante entre si, que o capítulo 1 mapeou.",[101,2087,2089],{"id":2088},"fechando-este-capítulo","Fechando este capítulo",[11,2091,2092],{},"Com o critério definido antes de ver os números (F1 primeiro, AUC como desempate) e o resultado batendo com a expectativa teórica, a Random Forest é a escolhida pro treino final. No capítulo 3 eu treino ela em todo o conjunto de treino oficial, avalio no teste oficial (nunca visto), abro a curva ROC, o corte de decisão, quais variáveis mais pesaram na decisão e se dá pra melhorar afinando hiperparâmetro.",[1504,2094,1506],{},{"title":55,"searchDepth":56,"depth":56,"links":2096},[2097,2098,2099,2100,2101],{"id":1552,"depth":56,"text":1553},{"id":1646,"depth":56,"text":1647},{"id":1803,"depth":56,"text":1804},{"id":1915,"depth":56,"text":1916},{"id":2088,"depth":56,"text":2089},"Capítulo 2 do meu trabalho final de Reconhecimento de Padrões: por que abro mão dos 2,5 milhões de sessões brutas em troca de uma partição menor e curada, o pipeline de pré-processamento que evita vazamento na validação cruzada, e a comparação justa entre quatro famílias de modelo antes de escolher qual treinar de verdade.",{},"\u002Fpt\u002Fprojects\u002Fnetwork-intrusion-detection\u002F02-metodologia-selecao-de-modelo",{"title":1532,"description":2102},"pt\u002Fprojects\u002Fnetwork-intrusion-detection\u002F02-metodologia-selecao-de-modelo",[69,67,1527,2108],"pipeline","O5dYS-ACt037Tn8-9EQNr8-yXbIFs6ZsQH7PvfRpXeI",{"id":2111,"title":2112,"body":2113,"cover":3,"date":1518,"description":3387,"extension":59,"meta":3388,"navigation":61,"order":143,"path":3389,"project":1522,"seo":3390,"status":64,"stem":3391,"tags":3392,"__hash__":3393},"projectChapters\u002Fpt\u002Fprojects\u002Fnetwork-intrusion-detection\u002F03-avaliacao-final-e-ajuste.md","Avaliação Final, Curva ROC e Ajuste de Hiperparâmetro",{"type":8,"value":2114,"toc":3378},[2115,2125,2129,2139,2182,2204,2211,2268,2282,2286,2302,2317,2434,2449,2504,2529,2535,2538,2544,2548,2555,2571,2585,2603,2610,2715,2721,2782,2814,2821,2828,2832,2840,2874,2930,2936,2956,2960,2971,3048,3086,3094,3108,3112,3119,3162,3203,3218,3221,3275,3295,3305,3313,3317,3368,3376],[11,2116,2117,2121,2122,42],{},[28,2118,2120],{"href":2119},"\u002Fprojects\u002Fnetwork-intrusion-detection\u002F02-metodologia-selecao-de-modelo","No capítulo 2"," defini o critério antes de ver qualquer número (F1 primeiro, AUC como desempate) e a Random Forest venceu a comparação entre quatro famílias de modelo. Esse capítulo final treina ela de verdade, em todo o conjunto de treino oficial, mede no teste oficial (as 175.341 sessões que o modelo nunca viu), e não para na acurácia: abro o corte de decisão, a curva ROC, quais variáveis mais pesaram, onde exatamente o modelo erra, e se dá pra melhorar afinando hiperparâmetro. O notebook completo, executado, com as três partes do projeto, está linkado ",[28,2123,2124],{"href":48},"na abertura do capítulo 1",[101,2126,2128],{"id":2127},"avaliação-final-random-forest-no-teste-oficial-de-verdade","Avaliação final: Random Forest, no teste oficial de verdade",[11,2130,2131,2132,2134,2135,2138],{},"Treinei o Random Forest (200 árvores, com ",[20,2133,1934],{},", um jeito de avisar o modelo que errar um ataque, a classe rara, custa mais caro do que errar uma sessão normal, sem precisar duplicar ou descartar nenhuma linha ",[28,2136,2137],{"href":485},"do jeito que o reamostramento tentava fazer lá na fraude de cartão",") em todo o conjunto de treino oficial e medi no conjunto de teste oficial, os 175.341 registros que o modelo nunca viu:",[121,2140,2142],{"className":123,"code":2141,"language":125,"meta":55,"style":55},"modelo_final = Pipeline([\n    (\"prep\", preprocessador),\n    (\"clf\", RandomForestClassifier(n_estimators=200, random_state=42, class_weight=\"balanced\", n_jobs=-1)),\n])\nmodelo_final.fit(X_train, y_train)\ny_pred = modelo_final.predict(X_test)\n\nprint(classification_report(y_test, y_pred, digits=4))\n",[20,2143,2144,2149,2154,2159,2163,2168,2173,2177],{"__ignoreMap":55},[129,2145,2146],{"class":131,"line":132},[129,2147,2148],{},"modelo_final = Pipeline([\n",[129,2150,2151],{"class":131,"line":56},[129,2152,2153],{},"    (\"prep\", preprocessador),\n",[129,2155,2156],{"class":131,"line":143},[129,2157,2158],{},"    (\"clf\", RandomForestClassifier(n_estimators=200, random_state=42, class_weight=\"balanced\", n_jobs=-1)),\n",[129,2160,2161],{"class":131,"line":149},[129,2162,249],{},[129,2164,2165],{"class":131,"line":155},[129,2166,2167],{},"modelo_final.fit(X_train, y_train)\n",[129,2169,2170],{"class":131,"line":161},[129,2171,2172],{},"y_pred = modelo_final.predict(X_test)\n",[129,2174,2175],{"class":131,"line":167},[129,2176,158],{"emptyLinePlaceholder":61},[129,2178,2179],{"class":131,"line":252},[129,2180,2181],{},"print(classification_report(y_test, y_pred, digits=4))\n",[11,2183,2184,2185,2187,2188,2190,2191,2193,2194,2196,2197,2199,2200,2203],{},"O mesmo ",[20,2186,1901],{}," de sempre, agora com o ",[20,2189,1897],{}," inteiro (os 39 ",[20,2192,1856],{}," + 3 ",[20,2195,1860],{},") grudado na frente do Random Forest final, um único ",[20,2198,1893],{}," treinando as duas etapas juntas. ",[20,2201,2202],{},"classification_report"," é quem calcula precisão, revocação e F1 por classe de uma vez, a mesma tabela que aparece logo abaixo.",[2205,2206],"heatmap-chart",{":cells":2207,":x-categories":2208,":y-categories":2208,"x-label":2209,"y-label":2210},"[{\"x\":\"Normal\",\"y\":\"Normal\",\"value\":54651},{\"x\":\"Ataque\",\"y\":\"Normal\",\"value\":1349},{\"x\":\"Normal\",\"y\":\"Ataque\",\"value\":15429},{\"x\":\"Ataque\",\"y\":\"Ataque\",\"value\":103912}]","[\"Normal\",\"Ataque\"]","previsto","real",[635,2212,2213,2232],{},[638,2214,2215],{},[641,2216,2217,2220,2223,2226,2229],{},[644,2218,2219],{"align":646},"Classe",[644,2221,2222],{"align":650},"Precisão",[644,2224,2225],{"align":650},"Revocação",[644,2227,2228],{"align":650},"F1",[644,2230,2231],{"align":650},"Sessões no teste",[653,2233,2234,2251],{},[641,2235,2236,2239,2242,2245,2248],{},[658,2237,2238],{"align":646},"Normal",[658,2240,2241],{"align":650},"0,7798",[658,2243,2244],{"align":650},"0,9759",[658,2246,2247],{"align":650},"0,8669",[658,2249,2250],{"align":650},"56.000",[641,2252,2253,2256,2259,2262,2265],{},[658,2254,2255],{"align":646},"Ataque",[658,2257,2258],{"align":650},"0,9872",[658,2260,2261],{"align":650},"0,8707",[658,2263,2264],{"align":650},"0,9253",[658,2266,2267],{"align":650},"119.341",[11,2269,2270,2271,2274,2275,2278,2279,42],{},"Precisão aqui é: das sessões que o modelo marcou como ataque, quantas eram ataque de verdade. Revocação é o oposto: dos ataques reais, quantos o modelo pegou. F1 resume as duas numa nota só. Acurácia geral 90,43%, mas o número que eu realmente confio é a revocação da classe ataque: ",[15,2272,2273],{},"87,07%",", ou seja, o modelo pega 103.912 dos 119.341 ataques reais do teste, e deixa passar 15.429. Em segurança, esse tipo de falso negativo (ataque classificado como normal) é o erro caro, é literalmente o intruso que passa despercebido. Do lado oposto, 1.349 sessões normais viraram alarme falso, incômodo pra quem investiga, mas bem menos grave que deixar passar um ataque de verdade. A ",[15,2276,2277],{},"AUC no teste oficial fica em 0,9830",", quase o teto de 1,0, uma medida de que o modelo sabe rankear risco de sessão bem mesmo antes de qualquer limiar de decisão entrar em jogo, ",[28,2280,2281],{"href":485},"a mesma métrica de ranking que já vi na fraude de cartão",[101,2283,2285],{"id":2284},"ajustando-o-corte-precisão-contra-revocação","Ajustando o corte: precisão contra revocação",[11,2287,2288,2289,2292,2293,2296,2297,2301],{},"Toda essa avaliação usou o corte padrão do scikit-learn: o ",[20,2290,2291],{},".predict()"," decide \"ataque\" sempre que o score de risco (a fração de árvores que votou ataque, o mesmo número que dá pra pegar com ",[20,2294,2295],{},".predict_proba()",") passa de 0,5. Mas esse 0,5 é só um ponto arbitrário numa reta inteira de opções, a mesma ideia de limiar ",[28,2298,2300],{"href":2299},"\u002Fplaylists\u002Fpattern-recognition\u002Fclassification-threshold","que já vi lá na aula de classificação com regressão",", só que agora dá pra girar de verdade.",[11,2303,2304,2305,2308,2309,2312,2313,2316],{},"Pra isso, escrevi um segundo notebook (",[20,2306,2307],{},"datasets\u002Funsw-nb15\u002F02_dados_extras_scatter_e_threshold.ipynb","), só porque o notebook original nunca salvou o ",[20,2310,2311],{},"y_score"," contínuo, apenas o ",[20,2314,2315],{},"y_pred"," já limiarizado em 0,5. As primeiras células desse notebook reconstroem, célula por célula, exatamente o mesmo pipeline dos capítulos 2 e 3:",[121,2318,2320],{"className":123,"code":2319,"language":125,"meta":55,"style":55},"df_train = pd.read_csv(\"\u002Fcontent\u002Fdata\u002FUNSW_NB15_training-set.csv\")\ndf_test = pd.read_csv(\"\u002Fcontent\u002Fdata\u002FUNSW_NB15_testing-set.csv\")\n\nDESCARTAR = [\"id\", \"attack_cat\", \"label\"]\nX_train = df_train.drop(columns=[c for c in DESCARTAR if c in df_train.columns])\ny_train = df_train[\"label\"]\nX_test = df_test.drop(columns=[c for c in DESCARTAR if c in df_test.columns])\ny_test = df_test[\"label\"]\n\ncat_cols = X_train.select_dtypes(include=[\"object\"]).columns.tolist()\nnum_cols = X_train.select_dtypes(include=[\"number\"]).columns.tolist()\n\ntransformadores = [\n    (\"num\", StandardScaler(), num_cols),\n    (\"cat\", OneHotEncoder(handle_unknown=\"ignore\", sparse_output=False), cat_cols),\n]\npreprocessador = ColumnTransformer(transformers=transformadores)\n\nmodelo_final = Pipeline([\n    (\"prep\", preprocessador),\n    (\"clf\", RandomForestClassifier(n_estimators=200, random_state=42, class_weight=\"balanced\", n_jobs=-1)),\n])\nmodelo_final.fit(X_train, y_train)\n\ny_pred = modelo_final.predict(X_test)\ny_score = modelo_final.predict_proba(X_test)[:, 1]\n",[20,2321,2322,2326,2331,2335,2339,2343,2347,2352,2357,2361,2365,2369,2373,2377,2381,2385,2389,2393,2397,2401,2405,2409,2413,2418,2423,2428],{"__ignoreMap":55},[129,2323,2324],{"class":131,"line":132},[129,2325,1620],{},[129,2327,2328],{"class":131,"line":56},[129,2329,2330],{},"df_test = pd.read_csv(\"\u002Fcontent\u002Fdata\u002FUNSW_NB15_testing-set.csv\")\n",[129,2332,2333],{"class":131,"line":143},[129,2334,158],{"emptyLinePlaceholder":61},[129,2336,2337],{"class":131,"line":149},[129,2338,1759],{},[129,2340,2341],{"class":131,"line":155},[129,2342,1764],{},[129,2344,2345],{"class":131,"line":161},[129,2346,1769],{},[129,2348,2349],{"class":131,"line":167},[129,2350,2351],{},"X_test = df_test.drop(columns=[c for c in DESCARTAR if c in df_test.columns])\n",[129,2353,2354],{"class":131,"line":252},[129,2355,2356],{},"y_test = df_test[\"label\"]\n",[129,2358,2359],{"class":131,"line":257},[129,2360,158],{"emptyLinePlaceholder":61},[129,2362,2363],{"class":131,"line":263},[129,2364,1778],{},[129,2366,2367],{"class":131,"line":269},[129,2368,1783],{},[129,2370,2371],{"class":131,"line":274},[129,2372,158],{"emptyLinePlaceholder":61},[129,2374,2375],{"class":131,"line":280},[129,2376,1828],{},[129,2378,2379],{"class":131,"line":286},[129,2380,1833],{},[129,2382,2383],{"class":131,"line":292},[129,2384,1838],{},[129,2386,2387],{"class":131,"line":298},[129,2388,1843],{},[129,2390,2391],{"class":131,"line":304},[129,2392,1848],{},[129,2394,2395],{"class":131,"line":918},[129,2396,158],{"emptyLinePlaceholder":61},[129,2398,2399],{"class":131,"line":924},[129,2400,2148],{},[129,2402,2403],{"class":131,"line":929},[129,2404,2153],{},[129,2406,2407],{"class":131,"line":935},[129,2408,2158],{},[129,2410,2411],{"class":131,"line":941},[129,2412,249],{},[129,2414,2416],{"class":131,"line":2415},23,[129,2417,2167],{},[129,2419,2421],{"class":131,"line":2420},24,[129,2422,158],{"emptyLinePlaceholder":61},[129,2424,2426],{"class":131,"line":2425},25,[129,2427,2172],{},[129,2429,2431],{"class":131,"line":2430},26,[129,2432,2433],{},"y_score = modelo_final.predict_proba(X_test)[:, 1]\n",[11,2435,2436,2437,2439,2440,2442,2443,2445,2446,2448],{},"Célula por célula, é o mesmo código do capítulo 2 (a partição oficial, o ",[20,2438,22],{},") e do começo deste capítulo (o ",[20,2441,1901],{}," com o Random Forest de 200 árvores), sem nenhuma linha nova, só reconstruído aqui porque esse notebook roda isolado, num runtime do Colab que não carrega o estado dos outros dois. A única diferença é a última linha: além de ",[20,2444,2315],{},", guardo também ",[20,2447,2311],{},", a probabilidade contínua, pra poder testar cortes diferentes de 0,5 no bloco seguinte. Com o pipeline retreinado, o bloco novo varre 19 cortes diferentes:",[121,2450,2452],{"className":123,"code":2451,"language":125,"meta":55,"style":55},"thresholds = np.round(np.arange(0.05, 1.0, 0.05), 2)\nthreshold_sweep = []\nfor t in thresholds:\n    y_pred_t = (y_score >= t).astype(int)\n    threshold_sweep.append({\n        \"threshold\": float(t),\n        \"precision\": round(float(precision_score(y_test, y_pred_t, zero_division=0)), 4),\n        \"recall\": round(float(recall_score(y_test, y_pred_t, zero_division=0)), 4),\n        \"f1\": round(float(f1_score(y_test, y_pred_t, zero_division=0)), 4),\n    })\n",[20,2453,2454,2459,2464,2469,2474,2479,2484,2489,2494,2499],{"__ignoreMap":55},[129,2455,2456],{"class":131,"line":132},[129,2457,2458],{},"thresholds = np.round(np.arange(0.05, 1.0, 0.05), 2)\n",[129,2460,2461],{"class":131,"line":56},[129,2462,2463],{},"threshold_sweep = []\n",[129,2465,2466],{"class":131,"line":143},[129,2467,2468],{},"for t in thresholds:\n",[129,2470,2471],{"class":131,"line":149},[129,2472,2473],{},"    y_pred_t = (y_score >= t).astype(int)\n",[129,2475,2476],{"class":131,"line":155},[129,2477,2478],{},"    threshold_sweep.append({\n",[129,2480,2481],{"class":131,"line":161},[129,2482,2483],{},"        \"threshold\": float(t),\n",[129,2485,2486],{"class":131,"line":167},[129,2487,2488],{},"        \"precision\": round(float(precision_score(y_test, y_pred_t, zero_division=0)), 4),\n",[129,2490,2491],{"class":131,"line":252},[129,2492,2493],{},"        \"recall\": round(float(recall_score(y_test, y_pred_t, zero_division=0)), 4),\n",[129,2495,2496],{"class":131,"line":257},[129,2497,2498],{},"        \"f1\": round(float(f1_score(y_test, y_pred_t, zero_division=0)), 4),\n",[129,2500,2501],{"class":131,"line":263},[129,2502,2503],{},"    })\n",[11,2505,2506,2509,2510,2513,2514,2517,2518,2520,2521,2524,2525,2528],{},[20,2507,2508],{},"np.arange(0.05, 1.0, 0.05)"," gera os 19 cortes (0,05, 0,10, ..., 0,95), e o ",[20,2511,2512],{},"for"," testa cada um: ",[20,2515,2516],{},"y_score >= t"," transforma o score contínuo em rótulo binário nesse corte específico (a mesma comparação que ",[20,2519,2291],{}," faz internamente, só que fixando ",[20,2522,2523],{},"t"," em vez de usar sempre 0,5), e as três métricas são recalculadas do zero a cada volta do laço. ",[20,2526,2527],{},"zero_division=0"," evita erro caso algum corte extremo zere uma das classes inteiramente. O resultado, 19 linhas de precisão\u002Frevocação\u002FF1, é o que alimenta o gráfico interativo abaixo:",[2530,2531],"intrusion-threshold-explorer",{"precision-label":2532,"recall-label":2533,"threshold-label":2534},"precisão","revocação","corte do modelo",[11,2536,2537],{},"Arrasta o slider e repara na troca. Baixar o corte pra 0,05 (marcar \"ataque\" com qualquer suspeita, por menor que seja) empurra a revocação pra 0,9781, quase pegando todo mundo, mas derruba a precisão pra 0,9234, mais alarme falso pro time de segurança investigar à toa. Subir o corte pra 0,95 (só marcar \"ataque\" quando o modelo tem certeza quase absoluta) inverte o jogo: precisão de 0,999, praticamente nenhum alarme falso, mas a revocação despenca pra 0,6951, deixando passar quase um terço dos ataques reais. Não existe um corte certo, isolado da pergunta de negócio. Se o custo de investigar um alarme falso é baixo, vale puxar o corte pra baixo e aceitar mais alarme falso em troca de pegar mais ataque de verdade. Se cada investigação é cara, o corte sobe.",[11,2539,2540,2541,2543],{},"No corte que usei o post inteiro (0,5), a combinação de ",[20,2542,1934],{}," com esse corte padrão já entrega revocação de 0,8715 sem eu precisar mexer em nada na mão (os números batem, com uma variação de milésimo, com a tabela lá em cima, porque reconstruí o pipeline do zero nesse notebook separado). É por isso que não fui atrás de ajustar esse corte na avaliação final: o desbalanceamento de classe já tava sendo compensado no próprio treino, não só na hora de decidir.",[101,2545,2547],{"id":2546},"a-curva-roc-o-mesmo-corte-visto-de-outro-ângulo","A curva ROC: o mesmo corte, visto de outro ângulo",[11,2549,2550,2551,2554],{},"A matriz de confusão e a tabela de precisão\u002Frevocação lá em cima avaliam o modelo num ",[15,2552,2553],{},"único"," corte de decisão (o padrão, 0,5). Mas o slider que acabei de mostrar já entrega o resto do quadro: a Random Forest não devolve só um rótulo, ela estima uma probabilidade P(ataque | x) pra cada sessão, e o corte é uma decisão operacional de segurança, não uma verdade matemática fixa.",[11,2556,2557,2558,1225,2561,2564,2565,1225,2567,2570],{},"A ",[15,2559,2560],{},"curva ROC",[207,2562,2563],{},"Receiver Operating Characteristic",") transforma essa troca em desenho: varrendo todos os cortes possíveis, ela traça a taxa de verdadeiro positivo (a mesma revocação de sempre) contra a taxa de falso positivo (a fração de sessões normais que viram alarme falso). A ",[15,2566,1930],{},[207,2568,2569],{},"Area Under the Curve",", a área debaixo dessa curva) condensa o gráfico inteiro num único número, com uma leitura probabilística direta: é a chance de o modelo dar um score mais alto a um ataque sorteado ao acaso do que a uma sessão normal sorteada ao acaso. AUC = 1,0 é o classificador perfeito, AUC = 0,5 é o mesmo que jogar moeda (a diagonal pontilhada do gráfico). No notebook original, esse cálculo é só duas linhas:",[121,2572,2574],{"className":123,"code":2573,"language":125,"meta":55,"style":55},"y_score = modelo_final.predict_proba(X_test)[:, 1]\nauc = roc_auc_score(y_test, y_score)\n",[20,2575,2576,2580],{"__ignoreMap":55},[129,2577,2578],{"class":131,"line":132},[129,2579,2433],{},[129,2581,2582],{"class":131,"line":56},[129,2583,2584],{},"auc = roc_auc_score(y_test, y_score)\n",[11,2586,2587,2590,2591,2594,2595,2598,2599,2602],{},[20,2588,2589],{},"predict_proba"," devolve duas colunas, a probabilidade de cada classe, ",[20,2592,2593],{},"[:, 1]"," pega só a coluna da classe 1 (ataque). ",[20,2596,2597],{},"roc_auc_score"," recebe o rótulo real e esse score contínuo, sem nenhum corte fixo envolvido, e calcula a AUC direto. O notebook original parava aí e só desenhava o gráfico com ",[20,2600,2601],{},"RocCurveDisplay.from_predictions(...)"," do próprio scikit-learn, uma imagem estática, sem nunca salvar os pontos da curva em lugar nenhum.",[11,2604,2605,2606,2609],{},"Pra ter um gráfico real e interativo aqui no blog, escrevi um terceiro notebook (",[20,2607,2608],{},"datasets\u002Funsw-nb15\u002F03_curva_roc.ipynb","), com a mesma reconstrução de sempre no começo (a essa altura, terceira vez que esse bloco aparece nesse projeto, então só o repito, sem re-explicar):",[121,2611,2613],{"className":123,"code":2612,"language":125,"meta":55,"style":55},"df_train = pd.read_csv(\"\u002Fcontent\u002Fdata\u002FUNSW_NB15_training-set.csv\")\ndf_test = pd.read_csv(\"\u002Fcontent\u002Fdata\u002FUNSW_NB15_testing-set.csv\")\n\nDESCARTAR = [\"id\", \"attack_cat\", \"label\"]\nX_train = df_train.drop(columns=[c for c in DESCARTAR if c in df_train.columns])\ny_train = df_train[\"label\"]\nX_test = df_test.drop(columns=[c for c in DESCARTAR if c in df_test.columns])\ny_test = df_test[\"label\"]\n\ncat_cols = X_train.select_dtypes(include=[\"object\"]).columns.tolist()\nnum_cols = X_train.select_dtypes(include=[\"number\"]).columns.tolist()\n\ntransformadores = [\n    (\"num\", StandardScaler(), num_cols),\n    (\"cat\", OneHotEncoder(handle_unknown=\"ignore\", sparse_output=False), cat_cols),\n]\npreprocessador = ColumnTransformer(transformers=transformadores)\n\nmodelo_final = Pipeline([\n    (\"prep\", preprocessador),\n    (\"clf\", RandomForestClassifier(n_estimators=200, random_state=42, class_weight=\"balanced\", n_jobs=-1)),\n])\nmodelo_final.fit(X_train, y_train)\n\ny_score = modelo_final.predict_proba(X_test)[:, 1]\n",[20,2614,2615,2619,2623,2627,2631,2635,2639,2643,2647,2651,2655,2659,2663,2667,2671,2675,2679,2683,2687,2691,2695,2699,2703,2707,2711],{"__ignoreMap":55},[129,2616,2617],{"class":131,"line":132},[129,2618,1620],{},[129,2620,2621],{"class":131,"line":56},[129,2622,2330],{},[129,2624,2625],{"class":131,"line":143},[129,2626,158],{"emptyLinePlaceholder":61},[129,2628,2629],{"class":131,"line":149},[129,2630,1759],{},[129,2632,2633],{"class":131,"line":155},[129,2634,1764],{},[129,2636,2637],{"class":131,"line":161},[129,2638,1769],{},[129,2640,2641],{"class":131,"line":167},[129,2642,2351],{},[129,2644,2645],{"class":131,"line":252},[129,2646,2356],{},[129,2648,2649],{"class":131,"line":257},[129,2650,158],{"emptyLinePlaceholder":61},[129,2652,2653],{"class":131,"line":263},[129,2654,1778],{},[129,2656,2657],{"class":131,"line":269},[129,2658,1783],{},[129,2660,2661],{"class":131,"line":274},[129,2662,158],{"emptyLinePlaceholder":61},[129,2664,2665],{"class":131,"line":280},[129,2666,1828],{},[129,2668,2669],{"class":131,"line":286},[129,2670,1833],{},[129,2672,2673],{"class":131,"line":292},[129,2674,1838],{},[129,2676,2677],{"class":131,"line":298},[129,2678,1843],{},[129,2680,2681],{"class":131,"line":304},[129,2682,1848],{},[129,2684,2685],{"class":131,"line":918},[129,2686,158],{"emptyLinePlaceholder":61},[129,2688,2689],{"class":131,"line":924},[129,2690,2148],{},[129,2692,2693],{"class":131,"line":929},[129,2694,2153],{},[129,2696,2697],{"class":131,"line":935},[129,2698,2158],{},[129,2700,2701],{"class":131,"line":941},[129,2702,249],{},[129,2704,2705],{"class":131,"line":2415},[129,2706,2167],{},[129,2708,2709],{"class":131,"line":2420},[129,2710,158],{"emptyLinePlaceholder":61},[129,2712,2713],{"class":131,"line":2425},[129,2714,2433],{},[11,2716,2717,2718,2720],{},"E, em cima desse ",[20,2719,2311],{},", extrai a curva ponto a ponto:",[121,2722,2724],{"className":123,"code":2723,"language":125,"meta":55,"style":55},"fpr, tpr, thresholds = roc_curve(y_test, y_score)\nauc = roc_auc_score(y_test, y_score)\nprint(f\"AUC (conferencia, deve bater com 0.9830 ja publicado): {auc:.4f}\")\nprint(f\"Curva bruta tem {len(fpr)} pontos, vamos reduzir pra 50 pra plotar no blog\")\n\nfpr_grid = np.linspace(0, 1, 50)\ntpr_grid = np.interp(fpr_grid, fpr, tpr)\n\nroc_curve_points = [\n    {\"fpr\": round(float(f), 4), \"tpr\": round(float(t), 4)}\n    for f, t in zip(fpr_grid, tpr_grid)\n]\n",[20,2725,2726,2731,2735,2740,2745,2749,2754,2759,2763,2768,2773,2778],{"__ignoreMap":55},[129,2727,2728],{"class":131,"line":132},[129,2729,2730],{},"fpr, tpr, thresholds = roc_curve(y_test, y_score)\n",[129,2732,2733],{"class":131,"line":56},[129,2734,2584],{},[129,2736,2737],{"class":131,"line":143},[129,2738,2739],{},"print(f\"AUC (conferencia, deve bater com 0.9830 ja publicado): {auc:.4f}\")\n",[129,2741,2742],{"class":131,"line":149},[129,2743,2744],{},"print(f\"Curva bruta tem {len(fpr)} pontos, vamos reduzir pra 50 pra plotar no blog\")\n",[129,2746,2747],{"class":131,"line":155},[129,2748,158],{"emptyLinePlaceholder":61},[129,2750,2751],{"class":131,"line":161},[129,2752,2753],{},"fpr_grid = np.linspace(0, 1, 50)\n",[129,2755,2756],{"class":131,"line":167},[129,2757,2758],{},"tpr_grid = np.interp(fpr_grid, fpr, tpr)\n",[129,2760,2761],{"class":131,"line":252},[129,2762,158],{"emptyLinePlaceholder":61},[129,2764,2765],{"class":131,"line":257},[129,2766,2767],{},"roc_curve_points = [\n",[129,2769,2770],{"class":131,"line":263},[129,2771,2772],{},"    {\"fpr\": round(float(f), 4), \"tpr\": round(float(t), 4)}\n",[129,2774,2775],{"class":131,"line":269},[129,2776,2777],{},"    for f, t in zip(fpr_grid, tpr_grid)\n",[129,2779,2780],{"class":131,"line":274},[129,2781,1843],{},[11,2783,2784,2787,2788,2790,2791,342,2794,2797,2798,2801,2802,2805,2806,2809,2810,2813],{},[20,2785,2786],{},"roc_curve(y_test, y_score)"," varre internamente cada score único presente em ",[20,2789,2311],{}," como um corte candidato, e devolve três arrays do mesmo tamanho: ",[20,2792,2793],{},"fpr",[20,2795,2796],{},"tpr"," e os ",[20,2799,2800],{},"thresholds"," que geraram cada par. Nesse dataset a curva bruta saiu com 282 pontos, granularidade demais pra um gráfico de blog e um JSON maior que o necessário. ",[20,2803,2804],{},"np.linspace(0, 1, 50)"," cria 50 posições igualmente espaçadas no eixo de falso positivo, e ",[20,2807,2808],{},"np.interp(fpr_grid, fpr, tpr)"," estima a taxa de verdadeiro positivo real em cada uma dessas 50 posições, interpolando entre os pontos reais mais próximos, sem inventar nenhum valor: é a mesma curva, só amostrada num grid mais leve de desenhar. O ",[20,2811,2812],{},"print"," de conferência confirma AUC = 0,9830, batendo exatamente com o número calculado no notebook original.",[2815,2816],"intrusion-roc-chart",{"model-label":2817,"random-label":2818,"x-label":2819,"y-label":2820},"Random Forest (AUC = 0,983)","aleatório (AUC = 0,5)","taxa de falso positivo","taxa de verdadeiro positivo",[11,2822,2823,2824,2827],{},"A curva gruda no canto superior esquerdo, bem longe da diagonal pontilhada do acaso: com um corte de falso positivo de só 2%, a revocação já salta pra 86%, e a curva segue quase colada no teto dali em diante. Isso mostra que o modelo separa bem as duas classes ",[15,2825,2826],{},"independente de qualquer corte específico",", é essa folga que sobra pra eu poder puxar o corte pra baixo, como fiz na seção anterior, sem que a precisão desabe junto.",[101,2829,2831],{"id":2830},"quem-decide-as-15-variáveis-que-mais-pesam","Quem decide: as 15 variáveis que mais pesam",[11,2833,2834,2835,2839],{},"Random Forest permite espiar quais variáveis mais reduziram a impureza das árvores ao longo do treino, ",[28,2836,2838],{"href":2837},"\u002Fplaylists\u002Fpattern-recognition\u002Fdecision-trees","a mesma régua de Gini que já vi lá nas árvores de decisão",", só que agora somada em 200 árvores em vez de uma só:",[121,2841,2843],{"className":123,"code":2842,"language":125,"meta":55,"style":55},"ohe = modelo_final.named_steps[\"prep\"].named_transformers_[\"cat\"]\nnomes_cat = ohe.get_feature_names_out(cat_cols)\nnomes_features = np.concatenate([num_cols, nomes_cat])\n\nimportancias = modelo_final.named_steps[\"clf\"].feature_importances_\nimp = pd.Series(importancias, index=nomes_features).sort_values(ascending=False)\n",[20,2844,2845,2850,2855,2860,2864,2869],{"__ignoreMap":55},[129,2846,2847],{"class":131,"line":132},[129,2848,2849],{},"ohe = modelo_final.named_steps[\"prep\"].named_transformers_[\"cat\"]\n",[129,2851,2852],{"class":131,"line":56},[129,2853,2854],{},"nomes_cat = ohe.get_feature_names_out(cat_cols)\n",[129,2856,2857],{"class":131,"line":143},[129,2858,2859],{},"nomes_features = np.concatenate([num_cols, nomes_cat])\n",[129,2861,2862],{"class":131,"line":149},[129,2863,158],{"emptyLinePlaceholder":61},[129,2865,2866],{"class":131,"line":155},[129,2867,2868],{},"importancias = modelo_final.named_steps[\"clf\"].feature_importances_\n",[129,2870,2871],{"class":131,"line":161},[129,2872,2873],{},"imp = pd.Series(importancias, index=nomes_features).sort_values(ascending=False)\n",[11,2875,2876,2877,2880,2881,2883,2884,342,2886,342,2888,2890,2891,342,2894,342,2897,2900,2901,2904,2905,2908,2909,2911,2912,2915,2916,2919,2920,2922,2923,2925,2926,2929],{},"Esse bloco existe pra resolver um problema chato: ",[20,2878,2879],{},"feature_importances_"," devolve só um array cru de números, sem nome nenhum grudado, e depois do ",[20,2882,22],{}," as três colunas categóricas originais (",[20,2885,997],{},[20,2887,1003],{},[20,2889,1000],{},") nem existem mais, viraram dezenas de colunas binárias novas, tipo ",[20,2892,2893],{},"proto_tcp",[20,2895,2896],{},"proto_udp",[20,2898,2899],{},"service_dns",". Pra reconstruir \"qual número da lista corresponde a qual variável\", preciso ir buscar esses nomes de volta de dentro do próprio pipeline já treinado: ",[20,2902,2903],{},"modelo_final.named_steps[\"prep\"]"," pega a etapa do pré-processador, ",[20,2906,2907],{},".named_transformers_[\"cat\"]"," pega especificamente o ",[20,2910,1860],{}," que rodou nas categóricas, e ",[20,2913,2914],{},".get_feature_names_out(cat_cols)"," pergunta pra ele \"quais foram os nomes das colunas novas que você criou?\". ",[20,2917,2918],{},"np.concatenate([num_cols, nomes_cat])"," gruda essa lista de volta com as 39 numéricas na frente (que mantiveram o nome original, ",[20,2921,1856],{}," não cria coluna nova nenhuma), respeitando a mesma ordem que o ",[20,2924,22],{}," usa por dentro. Só depois disso ",[20,2927,2928],{},"pd.Series(importancias, index=nomes_features)"," consegue emparelhar cada número de importância com o nome certo.",[474,2931],{":points":2932,"color":2933,"x-label":2934,"y-label":2935},"[{\"label\":\"ct_dst_src_ltm\",\"value\":0.074},{\"label\":\"sttl\",\"value\":0.059},{\"label\":\"sbytes\",\"value\":0.054},{\"label\":\"sload\",\"value\":0.052},{\"label\":\"ct_dst_sport_ltm\",\"value\":0.051},{\"label\":\"smean\",\"value\":0.047},{\"label\":\"ct_srv_dst\",\"value\":0.044},{\"label\":\"ct_state_ttl\",\"value\":0.042},{\"label\":\"rate\",\"value\":0.033},{\"label\":\"dbytes\",\"value\":0.033},{\"label\":\"dload\",\"value\":0.031},{\"label\":\"dur\",\"value\":0.029},{\"label\":\"synack\",\"value\":0.028},{\"label\":\"ct_srv_src\",\"value\":0.027},{\"label\":\"dmean\",\"value\":0.026}]","#33aa55","variável","importância (redução de impureza)",[11,2937,2938,2939,2942,2943,2945,2946,342,2949,342,2951,342,2953,2955],{},"A campeã, ",[20,2940,2941],{},"ct_dst_src_ltm",", conta quantas das últimas 100 conexões compartilham a mesma dupla origem-destino, um jeito direto de flagrar comportamento de scanner ou de flood: tráfego normal raramente bate repetidamente no mesmo par origem-destino centenas de vezes seguidas, ataque automatizado sim. Repara que ",[20,2944,186],{}," (o TTL que já apareceu lá na correlação) aparece de novo, na segunda posição, confirmando que aquele 0,91 de correlação com o rótulo não era coincidência estatística vazia, é sinal real. E boa parte do resto da lista (",[20,2947,2948],{},"ct_dst_sport_ltm",[20,2950,726],{},[20,2952,773],{},[20,2954,723],{},") é da mesma família \"quantas conexões recentes se parecem com essa\", o tipo de variável desenhada especificamente pra capturar rajada de conexão repetida, a assinatura mais comum de ataque automatizado contra tráfego humano.",[101,2957,2959],{"id":2958},"onde-o-modelo-erra-400-sessões-reais-nas-3-variáveis-mais-importantes","Onde o modelo erra: 400 sessões reais nas 3 variáveis mais importantes",[11,2961,2962,2963,528,2965,2967,2968,2970],{},"As duas primeiras posições do ranking de importância, ",[20,2964,2941],{},[20,2966,186],{},", mais a terceira, ",[20,2969,607],{},", dão pra visualizar juntas num espaço só. É o mesmo notebook 02 que gerou o sweep de threshold, com o mesmo pipeline retreinado, que também extrai essa amostra:",[121,2972,2974],{"className":123,"code":2973,"language":125,"meta":55,"style":55},"FEATURES_3D = [\"sttl\", \"ct_dst_src_ltm\", \"sbytes\"]\nN_SAMPLE = 400\n\namostra_idx = X_test.sample(n=N_SAMPLE, random_state=42).index\ny_pred_series = pd.Series(y_pred, index=X_test.index)\n\nsample_points = []\nfor i in amostra_idx:\n    sample_points.append({\n        \"x\": float(X_test.loc[i, FEATURES_3D[0]]),\n        \"y\": float(X_test.loc[i, FEATURES_3D[1]]),\n        \"z\": float(X_test.loc[i, FEATURES_3D[2]]),\n        \"label_real\": int(y_test.loc[i]),\n        \"label_previsto\": int(y_pred_series.loc[i]),\n    })\n",[20,2975,2976,2981,2986,2990,2995,3000,3004,3009,3014,3019,3024,3029,3034,3039,3044],{"__ignoreMap":55},[129,2977,2978],{"class":131,"line":132},[129,2979,2980],{},"FEATURES_3D = [\"sttl\", \"ct_dst_src_ltm\", \"sbytes\"]\n",[129,2982,2983],{"class":131,"line":56},[129,2984,2985],{},"N_SAMPLE = 400\n",[129,2987,2988],{"class":131,"line":143},[129,2989,158],{"emptyLinePlaceholder":61},[129,2991,2992],{"class":131,"line":149},[129,2993,2994],{},"amostra_idx = X_test.sample(n=N_SAMPLE, random_state=42).index\n",[129,2996,2997],{"class":131,"line":155},[129,2998,2999],{},"y_pred_series = pd.Series(y_pred, index=X_test.index)\n",[129,3001,3002],{"class":131,"line":161},[129,3003,158],{"emptyLinePlaceholder":61},[129,3005,3006],{"class":131,"line":167},[129,3007,3008],{},"sample_points = []\n",[129,3010,3011],{"class":131,"line":252},[129,3012,3013],{},"for i in amostra_idx:\n",[129,3015,3016],{"class":131,"line":257},[129,3017,3018],{},"    sample_points.append({\n",[129,3020,3021],{"class":131,"line":263},[129,3022,3023],{},"        \"x\": float(X_test.loc[i, FEATURES_3D[0]]),\n",[129,3025,3026],{"class":131,"line":269},[129,3027,3028],{},"        \"y\": float(X_test.loc[i, FEATURES_3D[1]]),\n",[129,3030,3031],{"class":131,"line":274},[129,3032,3033],{},"        \"z\": float(X_test.loc[i, FEATURES_3D[2]]),\n",[129,3035,3036],{"class":131,"line":280},[129,3037,3038],{},"        \"label_real\": int(y_test.loc[i]),\n",[129,3040,3041],{"class":131,"line":286},[129,3042,3043],{},"        \"label_previsto\": int(y_pred_series.loc[i]),\n",[129,3045,3046],{"class":131,"line":292},[129,3047,2503],{},[11,3049,3050,3053,3054,3057,3058,3060,3061,3064,3065,3068,3069,3072,3073,342,3075,342,3077,3079,3080,518,3082,3085],{},[20,3051,3052],{},"X_test.sample(n=400, random_state=42)"," sorteia 400 índices do conjunto de teste oficial, a semente fixa é só pra eu conseguir reproduzir a mesma amostra depois. ",[20,3055,3056],{},"y_pred_series"," reempacota o array cru ",[20,3059,2315],{}," (que só tem posição, sem índice) numa ",[20,3062,3063],{},"Series"," com o mesmo índice de ",[20,3066,3067],{},"X_test",", pra poder buscar a previsão de uma sessão específica por ",[20,3070,3071],{},".loc[i]"," dentro do laço. Pra cada uma das 400 sessões sorteadas, o laço guarda as três features de interesse (",[20,3074,186],{},[20,3076,2941],{},[20,3078,607],{},") mais o rótulo real e o rótulo previsto, nada inventado, é exatamente o ",[20,3081,3067],{},[20,3083,3084],{},"y_test"," oficiais, só que reduzido a uma amostra visualizável. Plotei nesses 3 eixos:",[3087,3088],"intrusion-sample-scatter",{"false-negative-label":3089,"false-positive-label":3090,"true-negative-label":3091,"true-positive-label":3092,"x-label":3093,"y-label":2941,"z-label":607},"falso negativo (ataque não detectado)","falso positivo (alarme falso)","acerto: normal","acerto: ataque","sttl (TTL de origem)",[11,3095,3096,3097,3099,3100,3103,3104,3107],{},"Nessa amostra de 400: 247 acertos em sessão de ataque, 112 acertos em sessão normal, 36 falsos negativos e só 5 falsos positivos, uma proporção parecida com a do teste inteiro. Um detalhe que salta aos olhos ao girar a nuvem: ",[20,3098,186],{}," não é contínuo de verdade, ele se agrupa nuns poucos valores redondos (31, 62, 254...), porque TTL é literalmente um contador de saltos de roteador que cada sistema operacional inicializa num valor padrão (64, 128 ou 255) e vai decrementando um a um a cada salto. Nas 283 sessões de ataque dessa amostra, ",[20,3101,3102],{},"sttl=254"," aparece em 238 delas (84%), mas também aparece em 37 das 117 sessões normais (32%), o valor mais comum dos dois lados, longe de ser um separador limpo sozinho. Já ",[20,3105,3106],{},"sttl=31"," é quase exclusivo de tráfego normal: 74 das 117 sessões normais, zero sessão de ataque. É exatamente por isso que uma variável sozinha não decide nada aqui. Dá pra ver os 36 pontos vermelhos (falso negativo) espalhados pelo meio da nuvem, sem formar uma ilha isolada, e é aí que entram as outras 39 variáveis que o gráfico não mostra, trabalhando junto pra fechar os 87% de revocação que o modelo final entrega.",[101,3109,3111],{"id":3110},"tentando-melhorar-com-gridsearch-e-batendo-no-próprio-teto","Tentando melhorar com GridSearch, e batendo no próprio teto",[11,3113,3114,3115,3118],{},"Testei ",[20,3116,3117],{},"GridSearchCV"," variando número de árvores (200 ou 400), profundidade máxima (sem limite ou até 30) e tamanho mínimo de folha (1 ou 2), 8 combinações, validação cruzada de 3 dobras, numa amostra de 30 mil sessões pra manter o custo tratável:",[121,3120,3122],{"className":123,"code":3121,"language":125,"meta":55,"style":55},"param_grid = {\n    \"clf__n_estimators\": [200, 400],\n    \"clf__max_depth\": [None, 30],\n    \"clf__min_samples_leaf\": [1, 2],\n}\n\ngrid = GridSearchCV(pipe_rf, param_grid, scoring=\"f1\", cv=cv, n_jobs=-1, verbose=1)\ngrid.fit(Xg, yg)\n",[20,3123,3124,3129,3134,3139,3144,3148,3152,3157],{"__ignoreMap":55},[129,3125,3126],{"class":131,"line":132},[129,3127,3128],{},"param_grid = {\n",[129,3130,3131],{"class":131,"line":56},[129,3132,3133],{},"    \"clf__n_estimators\": [200, 400],\n",[129,3135,3136],{"class":131,"line":143},[129,3137,3138],{},"    \"clf__max_depth\": [None, 30],\n",[129,3140,3141],{"class":131,"line":149},[129,3142,3143],{},"    \"clf__min_samples_leaf\": [1, 2],\n",[129,3145,3146],{"class":131,"line":155},[129,3147,2010],{},[129,3149,3150],{"class":131,"line":161},[129,3151,158],{"emptyLinePlaceholder":61},[129,3153,3154],{"class":131,"line":167},[129,3155,3156],{},"grid = GridSearchCV(pipe_rf, param_grid, scoring=\"f1\", cv=cv, n_jobs=-1, verbose=1)\n",[129,3158,3159],{"class":131,"line":252},[129,3160,3161],{},"grid.fit(Xg, yg)\n",[11,3163,3164,3165,3168,3169,3172,3173,3175,3176,3179,3180,3183,3184,3187,3188,3190,3191,3199,3200,3202],{},"O prefixo ",[20,3166,3167],{},"clf__"," na frente de cada chave não é enfeite: ",[20,3170,3171],{},"pipe_rf"," é um ",[20,3174,1901],{}," com uma etapa chamada ",[20,3177,3178],{},"\"clf\""," (o Random Forest), e o ",[20,3181,3182],{},"__"," é o jeito do scikit-learn dizer \"o parâmetro ",[20,3185,3186],{},"n_estimators"," pertence à etapa ",[20,3189,2042],{},", não a qualquer outra do pipeline\", ",[28,3192,3194,3195,3198],{"href":3193},"\u002Fplaylists\u002Fpattern-recognition\u002Fpipeline-cross-validation","a mesma sintaxe ",[20,3196,3197],{},"model__"," que já apareceu no post de pipeline e GridSearch",". Sem esse prefixo, ",[20,3201,3117],{}," não saberia em qual das etapas mexer.",[172,3204,3205],{},[11,3206,3207,3209,3210,3213,3214,3217],{},[15,3208,178],{}," melhor combinação encontrada, ",[20,3211,3212],{},"n_estimators=200, max_depth=None, min_samples_leaf=1",", com F1 de validação 0,9716. Exatamente os parâmetros ",[15,3215,3216],{},"padrão"," do Random Forest que eu já tinha treinado antes.",[11,3219,3220],{},"Pra não confiar cegamente nesse F1 de validação (calculado numa amostra de 30 mil, lembra), re-treinei a configuração vencedora no conjunto de treino oficial inteiro e avaliei no teste oficial, do mesmo jeito rigoroso da avaliação final:",[121,3222,3224],{"className":123,"code":3223,"language":125,"meta":55,"style":55},"melhores = {k.replace(\"clf__\", \"\"): v for k, v in grid.best_params_.items()}\nmodelo_tunado = Pipeline([(\"prep\", preprocessador),\n                          (\"clf\", RandomForestClassifier(random_state=42, class_weight=\"balanced\",\n                                                         n_jobs=-1, **melhores))])\nmodelo_tunado.fit(X_train, y_train)\ny_pred_t = modelo_tunado.predict(X_test)\ny_score_t = modelo_tunado.predict_proba(X_test)[:, 1]\n\nprint(classification_report(y_test, y_pred_t, digits=4))\nprint(f\"AUC-ROC (ajustada): {roc_auc_score(y_test, y_score_t):.4f}\")\n",[20,3225,3226,3231,3236,3241,3246,3251,3256,3261,3265,3270],{"__ignoreMap":55},[129,3227,3228],{"class":131,"line":132},[129,3229,3230],{},"melhores = {k.replace(\"clf__\", \"\"): v for k, v in grid.best_params_.items()}\n",[129,3232,3233],{"class":131,"line":56},[129,3234,3235],{},"modelo_tunado = Pipeline([(\"prep\", preprocessador),\n",[129,3237,3238],{"class":131,"line":143},[129,3239,3240],{},"                          (\"clf\", RandomForestClassifier(random_state=42, class_weight=\"balanced\",\n",[129,3242,3243],{"class":131,"line":149},[129,3244,3245],{},"                                                         n_jobs=-1, **melhores))])\n",[129,3247,3248],{"class":131,"line":155},[129,3249,3250],{},"modelo_tunado.fit(X_train, y_train)\n",[129,3252,3253],{"class":131,"line":161},[129,3254,3255],{},"y_pred_t = modelo_tunado.predict(X_test)\n",[129,3257,3258],{"class":131,"line":167},[129,3259,3260],{},"y_score_t = modelo_tunado.predict_proba(X_test)[:, 1]\n",[129,3262,3263],{"class":131,"line":252},[129,3264,158],{"emptyLinePlaceholder":61},[129,3266,3267],{"class":131,"line":257},[129,3268,3269],{},"print(classification_report(y_test, y_pred_t, digits=4))\n",[129,3271,3272],{"class":131,"line":263},[129,3273,3274],{},"print(f\"AUC-ROC (ajustada): {roc_auc_score(y_test, y_score_t):.4f}\")\n",[11,3276,3277,3280,3281,3283,3284,3287,3288,3291,3292,3294],{},[20,3278,3279],{},"grid.best_params_"," guarda as chaves com o prefixo ",[20,3282,3167],{}," (o mesmo prefixo explicado acima), então a primeira linha tira esse prefixo pra poder passar os valores direto pro construtor do ",[20,3285,3286],{},"RandomForestClassifier"," via ",[20,3289,3290],{},"**melhores",". O resto é o mesmo ",[20,3293,1901],{}," de sempre, só que agora com os hiperparâmetros vencedores no lugar dos valores padrão, treinado no conjunto de treino oficial inteiro (não mais na amostra de 30 mil da busca) e avaliado no teste oficial.",[172,3296,3297],{},[11,3298,3299,3301,3302,42],{},[15,3300,178],{}," precisão, revocação e F1 por classe idênticos à Random Forest original, casa decimal por casa decimal, e ",[20,3303,3304],{},"AUC-ROC (ajustada): 0.9830",[11,3306,3307,3308,3312],{},"O resultado bate, casa decimal por casa decimal, com a Random Forest original: mesma precisão, revocação e F1 por classe, mesma AUC de 0,9830. Não é falha de busca, é a busca confirmando algo que vale registrar sem meias palavras: pra esse dataset, com esse conjunto de candidatos testados, o palpite padrão do scikit-learn (100 ou 200 árvores, sem limitar profundidade) já estava no teto que essa família de hiperparâmetro alcança. Nem toda otimização encontra algo melhor pra otimizar, ",[28,3309,3311],{"href":3310},"\u002Fplaylists\u002Fpattern-recognition\u002Fensembles","o mesmo tipo de resultado \"chato\" que já vi no post de ensembles",", quando a regressão logística crua bateu ensemble nenhum se afinar mudasse o resultado.",[101,3314,3316],{"id":3315},"fechando-o-projeto-e-a-playlist-inteira","Fechando o projeto (e a playlist inteira)",[635,3318,3319,3329],{},[638,3320,3321],{},[641,3322,3323,3326],{},[644,3324,3325],{"align":646},"O que eu já sabia, de algum post anterior",[644,3327,3328],{"align":646},"O que esse trabalho final juntou",[653,3330,3331,3339,3352,3360],{},[641,3332,3333,3336],{},[658,3334,3335],{"align":646},"Dado desbalanceado engana acurácia",[658,3337,3338],{"align":646},"Aqui, com 12,6% de ataque, revocação da classe minoritária importa mais que o número geral",[641,3340,3341,3344],{},[658,3342,3343],{"align":646},"Correlação alta sinaliza redundância",[658,3345,3346,3347,663,3349,3351],{"align":646},"Correlação entre feature e rótulo (",[20,3348,186],{},[20,3350,785],{},", 0,91) pode sinalizar sinal real, não só redundância entre duas variáveis",[641,3353,3354,3357],{},[658,3355,3356],{"align":646},"Random Forest lida bem com padrão não-linear",[658,3358,3359],{"align":646},"Aqui ele bateu Regressão Logística, Árvore sozinha e Naive Bayes nos dois eixos (F1 e AUC)",[641,3361,3362,3365],{},[658,3363,3364],{"align":646},"GridSearch encontra o melhor ponto do espaço de busca",[658,3366,3367],{"align":646},"Às vezes o melhor ponto já é o padrão, e vale a pena descobrir isso medindo, não assumindo",[11,3369,3370,3371,3375],{},"Esse projeto fecha um ciclo pra mim: comecei essa playlist ",[28,3372,3374],{"href":3373},"\u002Fplaylists\u002Fpattern-recognition\u002Flinear-regression-estimator","ajustando uma reta com equação normal",", passei por árvore, cluster, redução de dimensão, texto virando vetor, e terminei aplicando tudo isso sozinho, sem aula pra seguir, num problema de segurança de rede de verdade, com resultado real (AUC 0,98 no teste oficial) que eu consigo defender linha por linha, do CSV bruto até a árvore de decisão final. Valeu, professor Boldt, o resto eu tirei de letra.",[1504,3377,1506],{},{"title":55,"searchDepth":56,"depth":56,"links":3379},[3380,3381,3382,3383,3384,3385,3386],{"id":2127,"depth":56,"text":2128},{"id":2284,"depth":56,"text":2285},{"id":2546,"depth":56,"text":2547},{"id":2830,"depth":56,"text":2831},{"id":2958,"depth":56,"text":2959},{"id":3110,"depth":56,"text":3111},{"id":3315,"depth":56,"text":3316},"Capítulo 3, o fechamento do meu trabalho final de Reconhecimento de Padrões: o Random Forest escolhido no capítulo 2 avaliado no teste oficial (98,3% de AUC), o corte de decisão explorado de ponta a ponta, curva ROC, as variáveis que mais pesaram, onde o modelo erra, e a descoberta de que afinar hiperparâmetro nem sempre bate o palpite padrão.",{},"\u002Fpt\u002Fprojects\u002Fnetwork-intrusion-detection\u002F03-avaliacao-final-e-ajuste",{"title":2112,"description":3387},"pt\u002Fprojects\u002Fnetwork-intrusion-detection\u002F03-avaliacao-final-e-ajuste",[69,68,1527,70],"GK406bU6TKSWKBhvHnAsz3IkRJbagprv6pQIiU2OfOI",1788965096942]