[{"data":1,"prerenderedAt":1478},["ShallowReactive",2],{"lang-switch-post-\u002Fprojects\u002Fnetwork-intrusion-detection\u002F01-explorando-25-milhoes-de-sessoes":3,"chapter-pt-network-intrusion-detection-01-explorando-25-milhoes-de-sessoes":4},null,{"id":5,"title":6,"body":7,"cover":3,"date":1463,"description":1464,"extension":1465,"meta":1466,"navigation":101,"order":74,"path":1467,"project":1468,"seo":1469,"status":1470,"stem":1471,"tags":1472,"__hash__":1477},"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":9,"toc":1452},"minimark",[10,35,38,43,61,115,132,155,252,301,316,337,347,386,408,417,424,432,436,443,448,497,501,504,538,578,743,765,769,772,889,909,916,920,954,979,1026,1068,1072,1092,1097,1104,1118,1122,1128,1163,1212,1236,1240,1247,1269,1276,1332,1343,1354,1358,1361,1445,1448],[11,12,13,18,19,23,24,27,28,34],"p",{},[14,15,17],"a",{"href":16},"\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 é ",[20,21,22],"strong",{},"tráfego normal"," ou ",[20,25,26],{},"ataque",". Notebook completo, executado, ",[14,29,33],{"href":30,"rel":31},"https:\u002F\u002Fcolab.research.google.com\u002Fdrive\u002F1tDmkJZOjd4yia-iPxcajnsjOSeaJJGGs?usp=sharing",[32],"nofollow","aqui",", os três capítulos saem dele.",[11,36,37],{},"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.",[39,40,42],"h2",{"id":41},"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,44,45,46,51,52,56,57,60],{},"O ",[14,47,50],{"href":48,"rel":49},"https:\u002F\u002Fwww.kaggle.com\u002Fdatasets\u002Fmrwellsdavid\u002Funsw-nb15",[32],"UNSW-NB15"," 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 (",[53,54,55],"code",{},"UNSW-NB15_1.csv"," a ",[53,58,59],{},"_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:",[62,63,68],"pre",{"className":64,"code":65,"language":66,"meta":67,"style":67},"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","",[53,69,70,78,84,90,96,103,109],{"__ignoreMap":67},[71,72,75],"span",{"class":73,"line":74},"line",1,[71,76,77],{},"features = pd.read_csv(\"\u002Fcontent\u002Fdata\u002FNUSW-NB15_features.csv\", encoding=\"latin-1\")\n",[71,79,81],{"class":73,"line":80},2,[71,82,83],{},"features.columns = features.columns.str.strip()\n",[71,85,87],{"class":73,"line":86},3,[71,88,89],{},"pd.set_option(\"display.max_colwidth\", None)\n",[71,91,93],{"class":73,"line":92},4,[71,94,95],{},"pd.set_option(\"display.max_rows\", None)\n",[71,97,99],{"class":73,"line":98},5,[71,100,102],{"emptyLinePlaceholder":101},true,"\n",[71,104,106],{"class":73,"line":105},6,[71,107,108],{},"print(f\"Total de features descritas: {len(features)}\")\n",[71,110,112],{"class":73,"line":111},7,[71,113,114],{},"display(features[[\"No.\", \"Name\", \"Type\", \"Description\"]])\n",[116,117,118],"blockquote",{},[11,119,120,123,124,127,128,131],{},[20,121,122],{},"Saída:"," ",[53,125,126],{},"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, ",[53,129,130],{},"sttl"," aparece descrita como \"Source to destination time to live value\").",[11,133,134,137,138,141,142,145,146,149,150,154],{},[53,135,136],{},"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 ",[53,139,140],{},"read_csv"," quebra. As duas linhas de ",[53,143,144],{},"pd.set_option"," existem só pra essa exibição específica: sem elas, o pandas trunca a coluna ",[53,147,148],{},"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 é ",[151,152,153],"em",{},"ler"," a descrição de cada feature. Esse catálogo é o que eu uso a seguir pra nomear as colunas de verdade:",[62,156,158],{"className":64,"code":157,"language":66,"meta":67,"style":67},"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",[53,159,160,165,169,174,179,184,189,194,199,205,211,216,222,228,234,240,246],{"__ignoreMap":67},[71,161,162],{"class":73,"line":74},[71,163,164],{},"import re\n",[71,166,167],{"class":73,"line":80},[71,168,102],{"emptyLinePlaceholder":101},[71,170,171],{"class":73,"line":86},[71,172,173],{},"# Filtragem apenas dos arquivos de dados -> UNSW-NB15_1.csv até UNSW-NB15_4.csv\n",[71,175,176],{"class":73,"line":92},[71,177,178],{},"partes = sorted([\n",[71,180,181],{"class":73,"line":98},[71,182,183],{},"    f for f in os.listdir(\"\u002Fcontent\u002Fdata\")\n",[71,185,186],{"class":73,"line":105},[71,187,188],{},"    if re.match(r\"UNSW-NB15_\\d+\\.csv\", f)\n",[71,190,191],{"class":73,"line":111},[71,192,193],{},"])\n",[71,195,197],{"class":73,"line":196},8,[71,198,102],{"emptyLinePlaceholder":101},[71,200,202],{"class":73,"line":201},9,[71,203,204],{},"# Extraindo os nomes das colunas do arquivo de features\n",[71,206,208],{"class":73,"line":207},10,[71,209,210],{},"colunas = features[\"Name\"].tolist()\n",[71,212,214],{"class":73,"line":213},11,[71,215,102],{"emptyLinePlaceholder":101},[71,217,219],{"class":73,"line":218},12,[71,220,221],{},"# Carregando e concatenando todas as partes em um único DataFrame\n",[71,223,225],{"class":73,"line":224},13,[71,226,227],{},"df = pd.concat(\n",[71,229,231],{"class":73,"line":230},14,[71,232,233],{},"    [pd.read_csv(f\"\u002Fcontent\u002Fdata\u002F{p}\", header=None, names=colunas, low_memory=False)\n",[71,235,237],{"class":73,"line":236},15,[71,238,239],{},"     for p in partes],\n",[71,241,243],{"class":73,"line":242},16,[71,244,245],{},"    ignore_index=True\n",[71,247,249],{"class":73,"line":248},17,[71,250,251],{},")\n",[11,253,254,255,258,259,261,262,265,266,269,270,56,272,274,275,278,279,282,283,286,287,286,290,286,293,296,297,300],{},"Vale abrir esse bloco, porque tem uma pegadinha escondida: sem o ",[53,256,257],{},"names=colunas"," ali dentro do ",[53,260,140],{},", o pandas numeraria as colunas como ",[53,263,264],{},"0, 1, 2..."," e eu perderia a etiqueta de cada uma. Antes disso, ",[53,267,268],{},"re.match(r\"UNSW-NB15_\\d+\\.csv\", f)"," filtra, de todos os arquivos que vieram no download, só os quatro que interessam aqui (",[53,271,55],{},[53,273,59],{},"), descartando de propósito o próprio catálogo de features e os arquivos da partição oficial (",[53,276,277],{},"UNSW_NB15_training-set.csv"," etc., que uso só a partir do capítulo 2) que também estão na mesma pasta. ",[53,280,281],{},"sorted(...)"," garante que os quatro pedaços entrem na ordem certa (",[53,284,285],{},"_1",", ",[53,288,289],{},"_2",[53,291,292],{},"_3",[53,294,295],{},"_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 ",[53,298,299],{},"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).",[62,302,304],{"className":64,"code":303,"language":66,"meta":67,"style":67},"print(f\"shape do dataset: \\n {df.shape}\\n\\n\")\ndf.head(5)\n",[53,305,306,311],{"__ignoreMap":67},[71,307,308],{"class":73,"line":74},[71,309,310],{},"print(f\"shape do dataset: \\n {df.shape}\\n\\n\")\n",[71,312,313],{"class":73,"line":80},[71,314,315],{},"df.head(5)\n",[116,317,318],{},[11,319,320,123,322,325,326,286,329,332,333,336],{},[20,321,122],{},[53,323,324],{},"(2540047, 49)",", seguido das 5 primeiras linhas reais, uma sessão UDP com ",[53,327,328],{},"sport=1390",[53,330,331],{},"dsport=53"," (porta de DNS) e ",[53,334,335],{},"dur=0.001055"," segundos, entre outras.",[11,338,339,340,342,343,346],{},"Um jeito simples e barato de conferir que a concatenação dos quatro arquivos e o batimento de nomes deram certo: se ",[53,341,257],{}," tivesse desalinhado alguma coluna, o ",[53,344,345],{},"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.",[62,348,350],{"className":64,"code":349,"language":66,"meta":67,"style":67},"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",[53,351,352,357,362,367,372,376,381],{"__ignoreMap":67},[71,353,354],{"class":73,"line":74},[71,355,356],{},"print(f\"Linhas: {df.shape[0]:,}\")\n",[71,358,359],{"class":73,"line":80},[71,360,361],{},"print(f\"Colunas: {df.shape[1]}\")\n",[71,363,364],{"class":73,"line":86},[71,365,366],{},"print(\"Tipagem dos dados:\")\n",[71,368,369],{"class":73,"line":92},[71,370,371],{},"print(df.dtypes.value_counts())\n",[71,373,374],{"class":73,"line":98},[71,375,102],{"emptyLinePlaceholder":101},[71,377,378],{"class":73,"line":105},[71,379,380],{},"pd.options.display.float_format = \"{:,.2f}\".format\n",[71,382,383],{"class":73,"line":111},[71,384,385],{},"display(df.describe().T)\n",[116,387,388],{},[11,389,390,123,392,395,396,399,400,403,404,407],{},[20,391,122],{},[53,393,394],{},"int64"," 28 colunas, ",[53,397,398],{},"float64"," 12, ",[53,401,402],{},"object"," 9. O ",[53,405,406],{},"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,409,410,411,413,414,416],{},"Esse terceiro grupo, as 9 colunas ",[53,412,402],{},", merece desconfiança, porque uma coluna virar ",[53,415,402],{}," 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.",[418,419],"category-bar-chart",{":points":420,"color":421,"x-label":422,"y-label":423},"[{\"label\":\"Normal\",\"value\":2218764},{\"label\":\"Ataque\",\"value\":321283}]","#0033cc","classe","quantidade de sessões",[11,425,426,427,431],{},"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 ",[14,428,430],{"href":429},"\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.",[39,433,435],{"id":434},"que-tipo-de-ataque-tem-aí","Que tipo de ataque tem aí",[11,437,438,439,442],{},"A coluna ",[53,440,441],{},"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:",[418,444],{":points":445,"color":446,"x-label":447,"y-label":423},"[{\"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,449,450,451,454,455,458,459,462,463,286,466,462,469,472,473,462,476,479,480,484,485,488,489,492,493,496],{},"Antes de eu somar essas 9 categorias, o dado bruto tinha 13 rótulos diferentes, porque ",[53,452,453],{},"\" Fuzzers\""," (com espaço na frente) e ",[53,456,457],{},"\"Fuzzers\""," contavam como duas categorias separadas pro pandas, mesma bobagem com ",[53,460,461],{},"\"Reconnaissance\"","\u002F",[53,464,465],{},"\" Reconnaissance\"",[53,467,468],{},"\"Backdoor\"",[53,470,471],{},"\"Backdoors\""," e ",[53,474,475],{},"\"Shellcode\"",[53,477,478],{},"\" Shellcode\"",". O mesmo tipo de sujeira de digitação que ",[14,481,483],{"href":482},"\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 ",[53,486,487],{},".strip()"," resolveria. ",[53,490,491],{},"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 ",[53,494,495],{},"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).",[39,498,500],{"id":499},"redundância-metade-das-colunas-conta-a-mesma-história","Redundância: metade das colunas conta a mesma história",[11,502,503],{},"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):",[62,505,507],{"className":64,"code":506,"language":66,"meta":67,"style":67},"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",[53,508,509,514,519,523,528,533],{"__ignoreMap":67},[71,510,511],{"class":73,"line":74},[71,512,513],{},"sample = df[colunas_numericas].sample(n=50000, random_state=42)\n",[71,515,516],{"class":73,"line":80},[71,517,518],{},"corr_matrix = sample.corr()\n",[71,520,521],{"class":73,"line":86},[71,522,102],{"emptyLinePlaceholder":101},[71,524,525],{"class":73,"line":92},[71,526,527],{},"mask_pairs = np.triu(np.ones_like(corr_matrix, dtype=bool), k=1)\n",[71,529,530],{"class":73,"line":98},[71,531,532],{},"corr_pairs = corr_matrix.where(mask_pairs).stack()\n",[71,534,535],{"class":73,"line":105},[71,536,537],{},"fortes = corr_pairs[corr_pairs.abs() >= 0.8].sort_values(key=abs, ascending=False)\n",[11,539,540,541,544,545,548,549,552,553,556,557,552,559,561,562,565,566,569,570,573,574,577],{},"Esse bloco final vale desmontar, porque ",[53,542,543],{},"corr_matrix"," sozinha é uma armadilha de duplicata: é uma tabela 40×40, cada variável cruzada com todas as outras, mas ela é ",[20,546,547],{},"simétrica",", o cruzamento de ",[53,550,551],{},"sbytes"," com ",[53,554,555],{},"sloss"," e o de ",[53,558,555],{},[53,560,551],{}," são o mesmo número aparecendo duas vezes. Contar isso duas vezes infla a lista à toa. ",[53,563,564],{},"np.triu(..., k=1)"," desenha uma \"grade\" que marca só o triângulo de cima da tabela como verdadeiro, pulando a diagonal (o ",[53,567,568],{},"k=1",", senão eu contaria cada variável correlacionada com ela mesma, sempre 1,0). ",[53,571,572],{},"corr_matrix.where(mask_pairs)"," apaga (vira vazio) tudo que caiu fora dessa grade, sobrando só metade única da tabela. ",[53,575,576],{},".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:",[579,580,581,596],"table",{},[582,583,584],"thead",{},[585,586,587,592],"tr",{},[588,589,591],"th",{"align":590},"left","Par de variáveis",[588,593,595],{"align":594},"right","Correlação",[597,598,599,614,626,639,650,661,673,685,698,709,721,732],"tbody",{},[585,600,601,611],{},[602,603,604,607,608],"td",{"align":590},[53,605,606],{},"Stime"," × ",[53,609,610],{},"Ltime",[602,612,613],{"align":594},"1,00",[585,615,616,624],{},[602,617,618,607,621],{"align":590},[53,619,620],{},"swin",[53,622,623],{},"dwin",[602,625,613],{"align":594},[585,627,628,636],{},[602,629,630,607,633],{"align":590},[53,631,632],{},"dloss",[53,634,635],{},"Dpkts",[602,637,638],{"align":594},"0,99",[585,640,641,648],{},[602,642,643,607,646],{"align":590},[53,644,645],{},"dbytes",[53,647,632],{},[602,649,638],{"align":594},[585,651,652,658],{},[602,653,654,607,656],{"align":590},[53,655,551],{},[53,657,555],{},[602,659,660],{"align":594},"0,96",[585,662,663,671],{},[602,664,665,607,668],{"align":590},[53,666,667],{},"ct_srv_src",[53,669,670],{},"ct_srv_dst",[602,672,660],{"align":594},[585,674,675,683],{},[602,676,677,607,680],{"align":590},[53,678,679],{},"ct_dst_ltm",[53,681,682],{},"ct_src_dport_ltm",[602,684,660],{"align":594},[585,686,687,695],{},[602,688,689,607,692],{"align":590},[53,690,691],{},"tcprtt",[53,693,694],{},"synack",[602,696,697],{"align":594},"0,92",[585,699,700,707],{},[602,701,702,607,704],{"align":590},[53,703,691],{},[53,705,706],{},"ackdat",[602,708,697],{"align":594},[585,710,711,718],{},[602,712,713,607,715],{"align":590},[53,714,130],{},[53,716,717],{},"ct_state_ttl",[602,719,720],{"align":594},"0,91",[585,722,723,730],{},[602,724,725,607,727],{"align":590},[53,726,130],{},[53,728,729],{},"Label",[602,731,720],{"align":594},[585,733,734,740],{},[602,735,736,607,738],{"align":590},[53,737,717],{},[53,739,729],{},[602,741,742],{"align":594},"0,88",[11,744,745,746,472,748,750,751,754,755,757,758,761,762,764],{},"Alguns desses pares são redundância por construção, não coincidência: ",[53,747,606],{},[53,749,610],{}," 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 ",[53,752,753],{},"dur",", outra coluna). Já ",[53,756,130],{}," (TTL, \"time to live\", o contador que cada roteador no caminho decrementa até o pacote expirar) correlacionar ",[20,759,760],{},"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, ",[53,763,130],{},", porque ele vai voltar a aparecer nos próximos dois capítulos.",[39,766,768],{"id":767},"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,770,771],{},"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?",[62,773,775],{"className":64,"code":774,"language":66,"meta":67,"style":67},"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",[53,776,777,782,786,791,795,800,805,810,815,820,825,830,835,840,845,850,855,860,866,871,877,883],{"__ignoreMap":67},[71,778,779],{"class":73,"line":74},[71,780,781],{},"features_box = [\"dur\", \"sbytes\", \"dbytes\", \"Sload\", \"Dload\"]\n",[71,783,784],{"class":73,"line":80},[71,785,102],{"emptyLinePlaceholder":101},[71,787,788],{"class":73,"line":86},[71,789,790],{},"fig, axes = plt.subplots(1, len(features_box), figsize=(18, 5))\n",[71,792,793],{"class":73,"line":92},[71,794,102],{"emptyLinePlaceholder":101},[71,796,797],{"class":73,"line":98},[71,798,799],{},"for i, feat in enumerate(features_box):\n",[71,801,802],{"class":73,"line":105},[71,803,804],{},"    sns.boxplot(\n",[71,806,807],{"class":73,"line":111},[71,808,809],{},"        data=df,\n",[71,811,812],{"class":73,"line":196},[71,813,814],{},"        x=\"Label\",\n",[71,816,817],{"class":73,"line":201},[71,818,819],{},"        y=feat,\n",[71,821,822],{"class":73,"line":207},[71,823,824],{},"        ax=axes[i],\n",[71,826,827],{"class":73,"line":213},[71,828,829],{},"        palette=[\"steelblue\", \"tomato\"],\n",[71,831,832],{"class":73,"line":218},[71,833,834],{},"        showfliers=False,   # tirando outliers extremos para melhor visualização\n",[71,836,837],{"class":73,"line":224},[71,838,839],{},"        hue=\"Label\",\n",[71,841,842],{"class":73,"line":230},[71,843,844],{},"        legend=False\n",[71,846,847],{"class":73,"line":236},[71,848,849],{},"    )\n",[71,851,852],{"class":73,"line":242},[71,853,854],{},"    axes[i].set_title(feat)\n",[71,856,857],{"class":73,"line":248},[71,858,859],{},"    axes[i].set_xticks([0, 1])\n",[71,861,863],{"class":73,"line":862},18,[71,864,865],{},"    axes[i].set_xticklabels([\"Normal\", \"Ataque\"])\n",[71,867,869],{"class":73,"line":868},19,[71,870,102],{"emptyLinePlaceholder":101},[71,872,874],{"class":73,"line":873},20,[71,875,876],{},"plt.suptitle(\"Distribuição de Features por Classe\", y=1.02)\n",[71,878,880],{"class":73,"line":879},21,[71,881,882],{},"plt.tight_layout()\n",[71,884,886],{"class":73,"line":885},22,[71,887,888],{},"plt.show()\n",[11,890,891,892,286,894,286,896,286,898,286,901,904,905,908],{},"Cada subgráfico compara a distribuição de uma feature (",[53,893,753],{},[53,895,551],{},[53,897,645],{},[53,899,900],{},"Sload",[53,902,903],{},"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. ",[53,906,907],{},"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,910,911,912,915],{},"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 ",[20,913,914],{},"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.",[39,917,919],{"id":918},"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,921,922,923,926,927,286,930,286,933,286,936,286,939,286,942,286,945,286,948,472,951,953],{},"Separando as colunas que o pandas leu como texto (",[53,924,925],{},"select_dtypes(include=[\"object\"])","), aparecem nove: ",[53,928,929],{},"srcip",[53,931,932],{},"sport",[53,934,935],{},"dstip",[53,937,938],{},"dsport",[53,940,941],{},"proto",[53,943,944],{},"state",[53,946,947],{},"service",[53,949,950],{},"ct_ftp_cmd",[53,952,441],{},". Contando os valores mais frequentes de cada uma, dois grupos bem diferentes aparecem:",[62,955,957],{"className":64,"code":956,"language":66,"meta":67,"style":67},"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",[53,958,959,964,969,974],{"__ignoreMap":67},[71,960,961],{"class":73,"line":74},[71,962,963],{},"categoricas = df.select_dtypes(include=[\"object\"]).columns.tolist()\n",[71,965,966],{"class":73,"line":80},[71,967,968],{},"for col in categoricas:\n",[71,970,971],{"class":73,"line":86},[71,972,973],{},"    print(f\"\\n{col}:\")\n",[71,975,976],{"class":73,"line":92},[71,977,978],{},"    print(df[col].value_counts().head(10))\n",[11,980,981,983,984,987,988,991,992,994,995,998,999,1002,1003,1006,1007,1009,1010,1013,1014,1017,1018,1021,1022,1025],{},[53,982,941],{}," é dominado por ",[53,985,986],{},"tcp"," (1.495.074 sessões) e ",[53,989,990],{},"udp"," (990.435), ",[53,993,944],{}," por ",[53,996,997],{},"FIN"," (1.478.689), ",[53,1000,1001],{},"CON"," (560.588) e ",[53,1004,1005],{},"INT"," (490.471), e ",[53,1008,947],{}," tem um ",[53,1011,1012],{},"-"," enorme (1.246.397, serviço não identificado pelo sniffer), seguido de ",[53,1015,1016],{},"dns"," (781.668) e ",[53,1019,1020],{},"http"," (206.273). Essas três têm poucas categorias e sem ordem entre si, boas candidatas a ",[20,1023,1024],{},"one-hot encoding"," mais na frente.",[11,1027,1028,1029,286,1031,286,1033,472,1035,1037,1038,1040,1041,56,1044,1047,1048,472,1050,1052,1053,1056,1057,1059,1060,1063,1064,1067],{},"Já ",[53,1030,929],{},[53,1032,932],{},[53,1034,935],{},[53,1036,938],{}," são outra espécie: identificadores de altíssima cardinalidade. ",[53,1039,929],{}," sozinho tem só 10 valores únicos nesse laboratório (",[53,1042,1043],{},"59.166.0.0",[53,1045,1046],{},"59.166.0.9",", as máquinas atacantes\u002Fgeradoras de tráfego), mas ",[53,1049,932],{},[53,1051,938],{}," 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 ",[20,1054,1055],{},"bem demais",", de um jeito que não generaliza. Um modelo treinado com ",[53,1058,929],{}," decoraria que \"",[53,1061,1062],{},"59.166.0.4"," é sempre suspeito\" em vez de aprender o ",[151,1065,1066],{},"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.",[39,1069,1071],{"id":1070},"dados-ausentes-falta-estrutural-não-falha-de-coleta","Dados ausentes: falta estrutural, não falha de coleta",[62,1073,1075],{"className":64,"code":1074,"language":66,"meta":67,"style":67},"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",[53,1076,1077,1082,1087],{"__ignoreMap":67},[71,1078,1079],{"class":73,"line":74},[71,1080,1081],{},"nulos = df.isnull().sum()\n",[71,1083,1084],{"class":73,"line":80},[71,1085,1086],{},"nulos_percentual = (nulos \u002F len(df) * 100)\n",[71,1088,1089],{"class":73,"line":86},[71,1090,1091],{},"resumo_nulos = pd.DataFrame({\"nulos\": nulos, \"percentual\": nulos_percentual}).sort_values(\"percentual\", ascending=False)\n",[418,1093],{":points":1094,"color":446,"x-label":1095,"y-label":1096},"[{\"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,1098,1099,1100,1103],{},"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 ",[151,1101,1102],{},"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,1105,1106,1107,1109,1110,1113,1114,1117],{},"E os três nulos que existem contam uma história estrutural, não um problema de coleta. ",[53,1108,441],{}," (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. ",[53,1111,1112],{},"is_ftp_login"," (56,29%) e ",[53,1115,1116],{},"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.",[39,1119,1121],{"id":1120},"um-bug-sutil-quando-vazio-não-conta-como-nulo","Um bug sutil: quando \"vazio\" não conta como nulo",[11,1123,1124,1125,1127],{},"Falei que só três colunas têm nulo. Mas reparei em algo estranho olhando os valores de ",[53,1126,950],{}," (quantidade de comandos FTP na sessão, que deveria ser um número) na lista de categóricas:",[62,1129,1131],{"className":64,"code":1130,"language":66,"meta":67,"style":67},"ct_ftp_cmd:\n     1429879\n0     686192\n0     380306\n1      29605\n1      10472\n",[53,1132,1133,1138,1143,1148,1153,1158],{"__ignoreMap":67},[71,1134,1135],{"class":73,"line":74},[71,1136,1137],{},"ct_ftp_cmd:\n",[71,1139,1140],{"class":73,"line":80},[71,1141,1142],{},"     1429879\n",[71,1144,1145],{"class":73,"line":86},[71,1146,1147],{},"0     686192\n",[71,1149,1150],{"class":73,"line":92},[71,1151,1152],{},"0     380306\n",[71,1154,1155],{"class":73,"line":98},[71,1156,1157],{},"1      29605\n",[71,1159,1160],{"class":73,"line":105},[71,1161,1162],{},"1      10472\n",[11,1164,1165,1166,1169,1170,1173,1174,1177,1178,1180,1181,1183,1184,1186,1187,1190,1191,1194,1195,1198,1199,1202,1203,1205,1206,1208,1209,1211],{},"Duas pistas aqui. Primeira: a categoria mais comum, com 1.429.879 sessões, não tem rótulo nenhum, é uma ",[20,1167,1168],{},"string vazia"," (",[53,1171,1172],{},"\"\"","), não um ",[53,1175,1176],{},"NaN",". E 1.429.879 é exatamente o número de nulos de ",[53,1179,1112],{}," lá de cima, ou seja, toda sessão sem login FTP também não tem ",[53,1182,950],{}," preenchido, faz sentido, mas o pandas não contou isso como nulo porque tecnicamente não é ",[53,1185,1176],{},", é uma string vazia disfarçada de categoria válida. ",[53,1188,1189],{},"df.isnull().sum()"," simplesmente não via esse problema. Segunda pista: ",[53,1192,1193],{},"\"0\""," aparece ",[20,1196,1197],{},"duas vezes"," na lista, uma com 686.192 sessões e outra com 380.306, porque uma tem espaço em branco colado (",[53,1200,1201],{},"\"0 \"",") e a outra não, o mesmo tipo de sujeira de digitação que já vi em ",[53,1204,441],{},". Essas duas coisas combinadas (string vazia em vez de nulo, mais espaço em branco duplicando categoria) são o motivo de ",[53,1207,950],{}," ter virado ",[53,1210,402],{}," 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,1213,1214,1215,1217,1218,1221,1222,1224,1225,1228,1229,1232,1233,1235],{},"A lição que fica: ",[53,1216,1189],{}," é um bom primeiro filtro, mas não é a palavra final sobre o que tá faltando. Uma string vazia, um espaço sozinho, um ",[53,1219,1220],{},"\"-\""," genérico (como o que já vi em ",[53,1223,947],{},"), tudo isso passa batido por um ",[53,1226,1227],{},".isnull()"," ingênuo. Vale sempre cruzar com ",[53,1230,1231],{},"df.dtypes"," e desconfiar de toda coluna numérica por natureza que virou ",[53,1234,402],{},".",[39,1237,1239],{"id":1238},"selecionando-ou-não-características","Selecionando (ou não) características",[11,1241,1242,1243,1246],{},"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 ",[20,1244,1245],{},"seleção de características",", e existem três famílias clássicas:",[1248,1249,1250,1257,1263],"ul",{},[1251,1252,1253,1256],"li",{},[20,1254,1255],{},"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.",[1251,1258,1259,1262],{},[20,1260,1261],{},"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.",[1251,1264,1265,1268],{},[20,1266,1267],{},"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,1270,1271,1272,1275],{},"Antes de decidir qualquer coisa, rodei um histograma em oito features numéricas escolhidas só pra inspecionar a ",[151,1273,1274],{},"forma"," das distribuições, não pra ranquear importância:",[62,1277,1279],{"className":64,"code":1278,"language":66,"meta":67,"style":67},"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",[53,1280,1281,1286,1290,1295,1300,1305,1310,1315,1319,1324,1328],{"__ignoreMap":67},[71,1282,1283],{"class":73,"line":74},[71,1284,1285],{},"features_ilustrativas = [\"dur\", \"sbytes\", \"dbytes\", \"sttl\", \"dttl\", \"sloss\", \"dloss\", \"Sload\"]\n",[71,1287,1288],{"class":73,"line":80},[71,1289,102],{"emptyLinePlaceholder":101},[71,1291,1292],{"class":73,"line":86},[71,1293,1294],{},"axes = df[features_ilustrativas].hist(figsize=(14, 8), bins=50, color=\"steelblue\",\n",[71,1296,1297],{"class":73,"line":92},[71,1298,1299],{},"                                      edgecolor=\"black\", log=True)\n",[71,1301,1302],{"class":73,"line":98},[71,1303,1304],{},"for ax in axes.ravel():\n",[71,1306,1307],{"class":73,"line":105},[71,1308,1309],{},"    ax.set_xlabel(\"Valor da feature\")\n",[71,1311,1312],{"class":73,"line":111},[71,1313,1314],{},"    ax.set_ylabel(\"Frequencia (escala log)\")\n",[71,1316,1317],{"class":73,"line":196},[71,1318,102],{"emptyLinePlaceholder":101},[71,1320,1321],{"class":73,"line":201},[71,1322,1323],{},"plt.suptitle(\"Distribuicao de Features Numericas (amostra ilustrativa)\", y=1.02)\n",[71,1325,1326],{"class":73,"line":207},[71,1327,882],{},[71,1329,1330],{"class":73,"line":213},[71,1331,888],{},[11,1333,1334,1335,1338,1339,1342],{},"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 ",[20,1336,1337],{},"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 (",[53,1340,1341],{},"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,1344,1345,1346,1349,1350,1353],{},"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: ",[20,1347,1348],{},"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 ",[20,1351,1352],{},"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\".",[39,1355,1357],{"id":1356},"principais-insights-antes-de-modelar","Principais insights, antes de modelar",[11,1359,1360],{},"Fechando esse primeiro capítulo, os pontos que carrego comigo pro resto do projeto:",[579,1362,1363,1373],{},[582,1364,1365],{},[585,1366,1367,1370],{},[588,1368,1369],{"align":590},"O que a EDA mostrou",[588,1371,1372],{"align":590},"Por que importa lá na frente",[597,1374,1375,1383,1397,1409,1421,1429],{},[585,1376,1377,1380],{},[602,1378,1379],{"align":590},"Desbalanceamento de 12,6% de ataque",[602,1381,1382],{"align":590},"Acurácia sozinha engana, F1 e AUC vão ser as métricas que realmente confio",[585,1384,1385,1394],{},[602,1386,1387,286,1389,472,1391,1393],{"align":590},[53,1388,441],{},[53,1390,1112],{},[53,1392,1116],{}," têm nulo estrutural",[602,1395,1396],{"align":590},"Não vou imputar às cegas, a ausência em si já é sinal",[585,1398,1399,1404],{},[602,1400,1401,1403],{"align":590},[53,1402,950],{}," escondia dado ausente como string vazia",[602,1405,1406,1408],{"align":590},[53,1407,1227],{}," sozinho não basta, sempre cruzar com dtype",[585,1410,1411,1418],{},[602,1412,1413,472,1415,1417],{"align":590},[53,1414,441],{},[53,1416,950],{}," tinham categoria duplicada por espaço extra",[602,1419,1420],{"align":590},"Corrigido antes de qualquer contagem valer a pena",[585,1422,1423,1426],{},[602,1424,1425],{"align":590},"Metade das 40 variáveis numéricas é redundante entre si (correlação ≥ 0,8)",[602,1427,1428],{"align":590},"Random Forest tolera isso, mas registrar a redundância importa pra interpretar depois",[585,1430,1431,1442],{},[602,1432,1433,462,1435,462,1437,462,1439,1441],{"align":590},[53,1434,929],{},[53,1436,935],{},[53,1438,932],{},[53,1440,938],{}," separam bem demais, de um jeito que não generaliza",[602,1443,1444],{"align":590},"Descartados no capítulo 2, junto com a explicação completa do porquê",[11,1446,1447],{},"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.",[1449,1450,1451],"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":67,"searchDepth":80,"depth":80,"links":1453},[1454,1455,1456,1457,1458,1459,1460,1461,1462],{"id":41,"depth":80,"text":42},{"id":434,"depth":80,"text":435},{"id":499,"depth":80,"text":500},{"id":767,"depth":80,"text":768},{"id":918,"depth":80,"text":919},{"id":1070,"depth":80,"text":1071},{"id":1120,"depth":80,"text":1121},{"id":1238,"depth":80,"text":1239},{"id":1356,"depth":80,"text":1357},"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.","md",{},"\u002Fpt\u002Fprojects\u002Fnetwork-intrusion-detection\u002F01-explorando-25-milhoes-de-sessoes","network-intrusion-detection",{"title":6,"description":1464},"published","pt\u002Fprojects\u002Fnetwork-intrusion-detection\u002F01-explorando-25-milhoes-de-sessoes",[1473,1474,1475,1476],"seguranca","eda","unsw-nb15","selecao-de-caracteristicas","y9UEazawMIWUhlwBSPzNQHo8bg0P9AYxussV_Y6nF34",1788965101295]