[{"data":1,"prerenderedAt":738},["ShallowReactive",2],{"lang-switch-post-\u002Fplaylists\u002Fpattern-recognition\u002Fcredit-card-fraud":3,"post-pt-pattern-recognition-credit-card-fraud":4},"\u002Fen\u002Fplaylists\u002Fpattern-recognition\u002Fcredit-card-fraud",{"id":5,"title":6,"body":7,"cover":722,"date":723,"description":724,"extension":725,"meta":726,"navigation":132,"order":727,"path":728,"playlist":729,"seo":730,"status":731,"stem":732,"tags":733,"__hash__":737},"posts\u002Fpt\u002Fplaylists\u002Fpattern-recognition\u002Fcredit-card-fraud.md","Detecção de Fraude: Quando 99.8% de Acurácia Não Significa Nada",{"type":8,"value":9,"toc":712},"minimark",[10,22,27,51,61,84,88,152,167,175,179,193,204,207,218,222,225,347,354,361,365,371,405,412,431,435,442,491,498,528,549,553,601,605,612,627,694,701,708],[11,12,13,14,21],"p",{},"Aula 12, e o dataset (o ",[15,16,20],"a",{"href":17,"rel":18},"https:\u002F\u002Fwww.kaggle.com\u002Fdatasets\u002Fmlg-ulb\u002Fcreditcardfraud",[19],"nofollow","Credit Card Fraud Detection do Kaggle",", transações reais e anonimizadas de cartão de crédito europeu) é grande demais e protegido demais pra eu conseguir baixar e reproduzir sozinho nesse ambiente, sem conta no Kaggle. Esse post fica em cima dos números que o próprio notebook já rodou (executado de verdade, saídas reais), e a seção de Aplicação Prática reconstrói o achado mais importante da aula num dataset sintético que eu consigo gerar e conferir na hora.",[23,24,26],"h2",{"id":25},"o-dataset-492-fraudes-em-quase-285-mil-transações","O dataset: 492 fraudes em quase 285 mil transações",[28,29,34],"pre",{"className":30,"code":31,"language":32,"meta":33,"style":33},"language-python shiki shiki-themes github-light github-dark","df = pd.read_csv('creditcard.csv')\nprint(df['Class'].value_counts())\n","python","",[35,36,37,45],"code",{"__ignoreMap":33},[38,39,42],"span",{"class":40,"line":41},"line",1,[38,43,44],{},"df = pd.read_csv('creditcard.csv')\n",[38,46,48],{"class":40,"line":47},2,[38,49,50],{},"print(df['Class'].value_counts())\n",[52,53,54],"blockquote",{},[11,55,56,60],{},[57,58,59],"strong",{},"Saída:"," classe 0 (transação normal): 284315. Classe 1 (fraude): 492.",[11,62,63,64,67,68,71,72,75,76,79,80,83],{},"Isso é ",[57,65,66],{},"0.17%"," de fraude. As variáveis de entrada já vêm transformadas por PCA (",[35,69,70],{},"V1"," a ",[35,73,74],{},"V28",", sem nome original, por privacidade do banco), mais ",[35,77,78],{},"Time"," e ",[35,81,82],{},"Amount"," sem transformar. Esse desbalanceamento extremo é o assunto do post inteiro.",[23,85,87],{"id":86},"o-baseline-que-expõe-a-mentira-da-acurácia","O baseline que expõe a mentira da acurácia",[28,89,91],{"className":30,"code":90,"language":32,"meta":33,"style":33},"class ZeroR(BaseEstimator, TransformerMixin):\n    def fit(self, X, y):\n        self.most_frequent_class_ = y.value_counts().idxmax()\n        return self\n    def transform(self, X):\n        return [self.most_frequent_class_] * len(X)\n\nmodel = ZeroR()\nmodel.fit(X_train, y_train)\nprint(accuracy_score(y_test, model.transform(X_test)))\n",[35,92,93,98,103,109,115,121,127,134,140,146],{"__ignoreMap":33},[38,94,95],{"class":40,"line":41},[38,96,97],{},"class ZeroR(BaseEstimator, TransformerMixin):\n",[38,99,100],{"class":40,"line":47},[38,101,102],{},"    def fit(self, X, y):\n",[38,104,106],{"class":40,"line":105},3,[38,107,108],{},"        self.most_frequent_class_ = y.value_counts().idxmax()\n",[38,110,112],{"class":40,"line":111},4,[38,113,114],{},"        return self\n",[38,116,118],{"class":40,"line":117},5,[38,119,120],{},"    def transform(self, X):\n",[38,122,124],{"class":40,"line":123},6,[38,125,126],{},"        return [self.most_frequent_class_] * len(X)\n",[38,128,130],{"class":40,"line":129},7,[38,131,133],{"emptyLinePlaceholder":132},true,"\n",[38,135,137],{"class":40,"line":136},8,[38,138,139],{},"model = ZeroR()\n",[38,141,143],{"class":40,"line":142},9,[38,144,145],{},"model.fit(X_train, y_train)\n",[38,147,149],{"class":40,"line":148},10,[38,150,151],{},"print(accuracy_score(y_test, model.transform(X_test)))\n",[52,153,154],{},[11,155,156,158,159,162,163,166],{},[57,157,59],{}," ",[35,160,161],{},"0.9983",". Chutando \"não é fraude\" sempre, sem olhar pra nenhuma variável, o modelo mais idiota possível acerta ",[57,164,165],{},"99.83%",".",[11,168,169,170,174],{},"Esse é o baseline mais extremo que já apareceu nessa playlist (",[15,171,173],{"href":172},"\u002Fplaylists\u002Fpattern-recognition\u002Fdecision-trees","o Car Evaluation, no post de árvores, tinha 70%",", aqui são quase 100%). Qualquer notícia de \"modelo de fraude com 99% de acurácia\" precisa dessa régua do lado, porque sem ela o número não diz nada.",[23,176,178],{"id":177},"um-modelo-de-verdade-e-duas-curvas-melhores-que-acurácia","Um modelo de verdade, e duas curvas melhores que acurácia",[28,180,182],{"className":30,"code":181,"language":32,"meta":33,"style":33},"model = LogisticRegression(tol=0.005)\nmodel.fit(X_train, y_train)\n",[35,183,184,189],{"__ignoreMap":33},[38,185,186],{"class":40,"line":41},[38,187,188],{},"model = LogisticRegression(tol=0.005)\n",[38,190,191],{"class":40,"line":47},[38,192,145],{},[52,194,195],{},[11,196,197,199,200,203],{},[57,198,59],{}," matriz de confusão ",[35,201,202],{},"[[56832, 32], [26, 72]]",". Precisão 0.69, revocação 0.73, F1 0.71 (pra classe fraude). ROC AUC 0.867. AUC da curva precisão-revocação: 0.613.",[11,205,206],{},"Repara que eu nem cito acurácia aqui, ela ia esconder tudo (98 fraudes entre quase 57 mil transações, então qualquer modelo razoável já bate 99.8%+ de acurácia). Precisão e revocação, sim, contam a história certa: das transações que o modelo marcou como fraude, 69% eram fraude de verdade. Das fraudes reais, o modelo pegou 73%.",[11,208,209,210,213,214,217],{},"E entre as duas curvas, a curva precisão-revocação (AUC 0.613) é mais honesta que a curva ROC (AUC 0.867) pra esse tipo de problema. A curva ROC usa a taxa de falso positivo no eixo, que é ",[35,211,212],{},"falsos positivos \u002F total de negativos",", e com quase 57 mil negativos, até um punhado de falsos positivos vira uma fração minúscula, a curva parece ótima quase à toa. A curva precisão-revocação, em compensação, usa precisão no eixo, que é ",[35,215,216],{},"verdadeiros positivos \u002F (verdadeiros positivos + falsos positivos)",", direto sensível a quantos falsos positivos existem comparado às poucas fraudes reais, sem se diluir no mar de negativos.",[23,219,221],{"id":220},"resampling-o-remédio-que-piora-as-coisas","Resampling: o remédio que piora as coisas",[11,223,224],{},"A ideia mais comum pra lidar com desbalanceamento é reamostrar o treino, de um jeito ou de outro, pra equilibrar as classes:",[226,227,228,252],"table",{},[229,230,231],"thead",{},[232,233,234,239,242,246,249],"tr",{},[235,236,238],"th",{"align":237},"left","Técnica",[235,240,241],{"align":237},"Como funciona",[235,243,245],{"align":244},"right","Precisão",[235,247,248],{"align":244},"Revocação",[235,250,251],{"align":244},"F1",[253,254,255,273,292,310,329],"tbody",{},[232,256,257,261,264,267,270],{},[258,259,260],"td",{"align":237},"Nenhuma (baseline)",[258,262,263],{"align":237},"-",[258,265,266],{"align":244},"0.69",[258,268,269],{"align":244},"0.73",[258,271,272],{"align":244},"0.71",[232,274,275,280,283,286,289],{},[258,276,277],{"align":237},[35,278,279],{},"RandomOverSampler",[258,281,282],{"align":237},"duplica exemplos da classe minoritária",[258,284,285],{"align":244},"0.04",[258,287,288],{"align":244},"0.92",[258,290,291],{"align":244},"0.08",[232,293,294,299,302,305,307],{},[258,295,296],{"align":237},[35,297,298],{},"RandomUnderSampler",[258,300,301],{"align":237},"descarta exemplos da classe majoritária",[258,303,304],{"align":244},"0.03",[258,306,288],{"align":244},[258,308,309],{"align":244},"0.06",[232,311,312,317,320,323,326],{},[258,313,314],{"align":237},[35,315,316],{},"SMOTE",[258,318,319],{"align":237},"cria exemplos sintéticos interpolando vizinhos da minoria",[258,321,322],{"align":244},"0.07",[258,324,325],{"align":244},"0.91",[258,327,328],{"align":244},"0.12",[232,330,331,336,339,341,344],{},[258,332,333],{"align":237},[35,334,335],{},"NearMiss",[258,337,338],{"align":237},"descarta exemplos majoritários perto da fronteira",[258,340,304],{"align":244},[258,342,343],{"align":244},"0.90",[258,345,346],{"align":244},"0.05",[11,348,349,350,353],{},"Nas quatro técnicas, a revocação sobe (de 0.73 pra ~0.90), o modelo passa a pegar mais fraudes de verdade. Mas a precisão ",[57,351,352],{},"despenca"," (de 0.69 pra 0.03-0.07), o modelo passa a gritar \"fraude!\" pra um monte de transação normal também. O F1 (que equilibra as duas) piora bastante em todos os quatro casos. Reamostrar não é bala de prata, é uma troca, e nesse dataset específico a troca sai perdendo.",[11,355,356,357,360],{},"O motivo da precisão desabar tão feio é uma mistura simples de duas coisas. Primeiro, o reamostramento só mexe no ",[57,358,359],{},"treino",": o modelo aprende num mundo artificialmente equilibrado (perto de 50\u002F50), mas continua sendo testado no mundo real, onde fraude é 0.17% das transações. Segundo, com tanta transação normal no teste (quase 57 mil), até uma taxinha pequena de erro nelas vira um monte de caso em número absoluto: se o modelo, calibrado pra um mundo onde fraude é comum, passa a \"desconfiar\" de qualquer coisa que se pareça um pouco com fraude, mesmo uma taxa de falso positivo de 1-2% em cima de 57 mil transações normais já gera centenas de alarmes falsos, muito mais do que as poucas dezenas de fraudes reais que existem pra acertar. É essa desproporção entre \"quantos normais existem\" e \"quantos falsos positivos o modelo agora comete\" que faz a precisão desabar.",[23,362,364],{"id":363},"o-jeito-errado-de-reamostrar-e-por-que-ele-engana","O jeito errado de reamostrar (e por que ele engana)",[11,366,367,368,166],{},"O notebook tem uma seção com o título mais direto da matéria inteira: ",[57,369,370],{},"\"Abordagem ERRADA - Não fazer assim!\"",[28,372,374],{"className":30,"code":373,"language":32,"meta":33,"style":33},"smote = SMOTE(random_state=42)\nX_resampled, y_resampled = smote.fit_resample(X, y)  # reamostra ANTES de separar treino\u002Fteste\n\nX_train_resampled, X_test_resampled, y_train_resampled, y_test_resampled = train_test_split(\n    X_resampled, y_resampled, test_size=0.2, random_state=42)\nmodel.fit(X_train_resampled, y_train_resampled)\n",[35,375,376,381,386,390,395,400],{"__ignoreMap":33},[38,377,378],{"class":40,"line":41},[38,379,380],{},"smote = SMOTE(random_state=42)\n",[38,382,383],{"class":40,"line":47},[38,384,385],{},"X_resampled, y_resampled = smote.fit_resample(X, y)  # reamostra ANTES de separar treino\u002Fteste\n",[38,387,388],{"class":40,"line":105},[38,389,133],{"emptyLinePlaceholder":132},[38,391,392],{"class":40,"line":111},[38,393,394],{},"X_train_resampled, X_test_resampled, y_train_resampled, y_test_resampled = train_test_split(\n",[38,396,397],{"class":40,"line":117},[38,398,399],{},"    X_resampled, y_resampled, test_size=0.2, random_state=42)\n",[38,401,402],{"class":40,"line":123},[38,403,404],{},"model.fit(X_train_resampled, y_train_resampled)\n",[52,406,407],{},[11,408,409,411],{},[57,410,59],{}," precisão 0.98, revocação 0.97, F1 0.97. Um resultado impressionante.",[11,413,414,415,418,419,421,422,426,427,430],{},"Impressionante e ",[57,416,417],{},"inválido",". O ",[35,420,316],{}," foi chamado em cima do dataset inteiro, antes de separar treino e teste. Como SMOTE cria exemplo sintético interpolando entre vizinhos reais da classe minoritária, alguns dos exemplos sintéticos que caem no \"treino\" depois da divisão são quase idênticos a exemplos reais que caíram no \"teste\". O modelo não está generalizando pra dado nunca visto, está reconhecendo cópias quase idênticas do que ele já treinou, ",[15,423,425],{"href":424},"\u002Fplaylists\u002Fpattern-recognition\u002Fpipeline-cross-validation","o mesmo tipo de vazamento que eu já vi antes",", só que dessa vez escondido dentro de uma técnica de reamostragem em vez de um ",[35,428,429],{},"fit_transform"," no lugar errado.",[23,432,434],{"id":433},"o-jeito-certo-reamostragem-dentro-do-pipeline","O jeito certo: reamostragem dentro do pipeline",[11,436,437,438,441],{},"A correção é reamostrar ",[57,439,440],{},"depois"," de cada divisão de validação cruzada, nunca antes, exatamente como qualquer normalização ou seleção de feature deveria ser feita:",[28,443,445],{"className":30,"code":444,"language":32,"meta":33,"style":33},"from imblearn.pipeline import Pipeline\nfrom sklearn.model_selection import cross_validate, StratifiedKFold\n\npipe = Pipeline([\n    ('sampling', SMOTE(random_state=42)),\n    ('model', LogisticRegression(tol=0.005))\n])\ncv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)\nscores = cross_validate(pipe, X, y, cv=cv, scoring=['precision', 'recall', 'f1'])\n",[35,446,447,452,457,461,466,471,476,481,486],{"__ignoreMap":33},[38,448,449],{"class":40,"line":41},[38,450,451],{},"from imblearn.pipeline import Pipeline\n",[38,453,454],{"class":40,"line":47},[38,455,456],{},"from sklearn.model_selection import cross_validate, StratifiedKFold\n",[38,458,459],{"class":40,"line":105},[38,460,133],{"emptyLinePlaceholder":132},[38,462,463],{"class":40,"line":111},[38,464,465],{},"pipe = Pipeline([\n",[38,467,468],{"class":40,"line":117},[38,469,470],{},"    ('sampling', SMOTE(random_state=42)),\n",[38,472,473],{"class":40,"line":123},[38,474,475],{},"    ('model', LogisticRegression(tol=0.005))\n",[38,477,478],{"class":40,"line":129},[38,479,480],{},"])\n",[38,482,483],{"class":40,"line":136},[38,484,485],{},"cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)\n",[38,487,488],{"class":40,"line":142},[38,489,490],{},"scores = cross_validate(pipe, X, y, cv=cv, scoring=['precision', 'recall', 'f1'])\n",[52,492,493],{},[11,494,495,497],{},[57,496,59],{}," precisão média ≈ 0.07, revocação média ≈ 0.89, F1 médio ≈ 0.14, nas 5 dobras.",[11,499,500,501,504,505,508,509,511,512,515,516,518,519,158,521,158,524,527],{},"Bem mais parecido com o resultado honesto do SMOTE isolado (F1 0.12) do que com o 0.97 inflado da abordagem errada. Repara no detalhe técnico: aqui é o ",[35,502,503],{},"Pipeline"," do pacote ",[35,506,507],{},"imblearn",", não o do scikit-learn. O ",[35,510,503],{}," comum só aceita etapas que transformam ",[35,513,514],{},"X",", mas ",[35,517,316],{}," precisa mexer em ",[35,520,514],{},[57,522,523],{},"e",[35,525,526],{},"y"," juntos (cria linhas novas nos dois), então precisa de uma versão de pipeline que saiba propagar essa mudança de tamanho adiante.",[11,529,530,531,534,535,537,538,541,542,158,544,548],{},"Um bônus notado de passagem: adicionar um ",[35,532,533],{},"StandardScaler"," antes do ",[35,536,316],{}," no pipeline não muda muito o resultado, mas deixa o ajuste ",[57,539,540],{},"bem mais rápido"," (de uns 15-19 segundos por dobra pra 2-3 segundos). Faz sentido: o ",[35,543,316],{},[15,545,547],{"href":546},"\u002Fplaylists\u002Fpattern-recognition\u002Fknn-classifier","precisa achar vizinhos mais próximos pra interpolar"," pra criar cada exemplo sintético, e busca de vizinho em dado não normalizado, com variáveis em escalas bem diferentes, é mais cara de calcular.",[23,550,552],{"id":551},"fechando","Fechando",[226,554,555,565],{},[229,556,557],{},[232,558,559,562],{},[235,560,561],{"align":237},"O que eu já sabia",[235,563,564],{"align":237},"O que essa aula assentou",[253,566,567,575,583],{},[232,568,569,572],{},[258,570,571],{"align":237},"Um baseline idiota ajuda a interpretar acurácia",[258,573,574],{"align":237},"Com desbalanceamento extremo (0.17% de fraude), a acurácia praticamente perde o sentido, e precisão\u002Frevocação\u002Fcurva PR precisam assumir o posto",[232,576,577,580],{},[258,578,579],{"align":237},"Vazamento acontece quando o mesmo dado influencia treino e avaliação",[258,581,582],{"align":237},"Reamostrar antes de separar treino\u002Fteste é um vazamento tão sério quanto normalizar errado, só que mais fácil de não perceber",[232,584,585,590],{},[258,586,587,589],{"align":237},[35,588,503],{}," evita vazamento entre etapas",[258,591,592,595,596,598,599],{"align":237},[35,593,594],{},"imblearn.Pipeline"," estende a mesma ideia pra técnicas que também mudam o ",[35,597,526],{},", não só o ",[35,600,514],{},[23,602,604],{"id":603},"aplicação-prática","Aplicação Prática",[11,606,607,608,611],{},"Não consigo baixar o dataset real de fraude aqui (precisa de login no Kaggle), então reconstruí o achado mais importante da aula, o vazamento do SMOTE-antes-do-split, num dataset sintético que eu controlo e posso conferir: 20 mil exemplos, 2% de classe positiva (",[35,609,610],{},"make_classification"," do scikit-learn, com uma fração de rótulo invertido de propósito pra não ficar bom demais).",[28,613,615],{"className":30,"code":614,"language":32,"meta":33,"style":33},"from sklearn.datasets import make_classification\nX, y = make_classification(n_samples=20000, weights=[0.98, 0.02], flip_y=0.01, random_state=42)\n",[35,616,617,622],{"__ignoreMap":33},[38,618,619],{"class":40,"line":41},[38,620,621],{},"from sklearn.datasets import make_classification\n",[38,623,624],{"class":40,"line":47},[38,625,626],{},"X, y = make_classification(n_samples=20000, weights=[0.98, 0.02], flip_y=0.01, random_state=42)\n",[226,628,629,642],{},[229,630,631],{},[232,632,633,636,638,640],{},[235,634,635],{"align":237},"Abordagem",[235,637,245],{"align":244},[235,639,248],{"align":244},[235,641,251],{"align":244},[253,643,644,658,680],{},[232,645,646,649,652,655],{},[258,647,648],{"align":237},"Regressão logística, sem reamostrar",[258,650,651],{"align":244},"1.000",[258,653,654],{"align":244},"0.190",[258,656,657],{"align":244},"0.319",[232,659,660,665,670,675],{},[258,661,662],{"align":237},[57,663,664],{},"SMOTE antes do split (errado)",[258,666,667],{"align":244},[57,668,669],{},"0.801",[258,671,672],{"align":244},[57,673,674],{},"0.791",[258,676,677],{"align":244},[57,678,679],{},"0.796",[232,681,682,685,688,691],{},[258,683,684],{"align":237},"SMOTE dentro do pipeline + validação cruzada (certo)",[258,686,687],{"align":244},"0.086",[258,689,690],{"align":244},"0.731",[258,692,693],{"align":244},"0.154",[11,695,696,697,700],{},"(O ",[35,698,699],{},"ZeroR",", sempre chutando \"não é fraude\", bate 0.975 de acurácia sem detectar nenhuma fraude, o mesmo baseline idiota de sempre, agora numa escala menor.)",[11,702,703,704,707],{},"A diferença entre \"errado\" e \"certo\" aqui é ainda mais dramática que no dataset real de fraude: F1 de 0.796 (parece excelente) contra 0.154 (o número honesto), só trocando ",[57,705,706],{},"quando"," o SMOTE roda. Mesma conclusão da aula, confirmada num dataset que eu montei do zero: o vazamento de reamostrar antes de separar treino e teste não é um detalhe teórico, ele infla o resultado o suficiente pra transformar um modelo mediano num modelo que parece pronto pra produção, sem ser.",[709,710,711],"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":33,"searchDepth":47,"depth":47,"links":713},[714,715,716,717,718,719,720,721],{"id":25,"depth":47,"text":26},{"id":86,"depth":47,"text":87},{"id":177,"depth":47,"text":178},{"id":220,"depth":47,"text":221},{"id":363,"depth":47,"text":364},{"id":433,"depth":47,"text":434},{"id":551,"depth":47,"text":552},{"id":603,"depth":47,"text":604},null,"2026-08-20","Aula 12: o professor usa o dataset real de fraude de cartão do Kaggle (492 fraudes em quase 285 mil transações) pra mostrar por que acurácia mente em dado desbalanceado, e um jeito de vazar dado tão sutil que o resultado errado parece ótimo.","md",{},13,"\u002Fpt\u002Fplaylists\u002Fpattern-recognition\u002Fcredit-card-fraud","pattern-recognition",{"title":6,"description":724},"published","pt\u002Fplaylists\u002Fpattern-recognition\u002Fcredit-card-fraud",[734,735,736],"dados-desbalanceados","deteccao-de-fraude","vazamento-de-dados","4nM_cL1UfP9bisw2YNJW2W3TwuUQcYaA3-rUSb9ve3U",1787338982975]