[{"data":1,"prerenderedAt":1873},["ShallowReactive",2],{"lang-switch-post-\u002Fprojects\u002Folist-ecommerce":3,"project-pt-olist-ecommerce":4,"project-chapters-pt-olist-ecommerce":97},null,{"id":5,"title":6,"body":7,"cover":3,"description":82,"extension":83,"meta":84,"navigation":85,"order":86,"path":87,"seo":88,"status":89,"stem":90,"tags":91,"__hash__":96},"projects\u002Fpt\u002Fprojects\u002Folist-ecommerce\u002Findex.md","Olist: um Case de E-commerce Brasileiro de Ponta a Ponta",{"type":8,"value":9,"toc":77},"minimark",[10,19,30,35,38,66,69],[11,12,13,14,18],"p",{},"Esse projeto é diferente de qualquer playlist daqui. Playlist é aula, um conceito de cada vez, post independente do anterior. Isso aqui é um ",[15,16,17],"strong",{},"case de ciência de dados de ponta a ponta",": eu pego um dataset real, sujo, com nove tabelas relacionadas entre si, e vou até o fim, EDA, visualização, engenharia de feature, treino de modelo, interpretabilidade e conclusão de negócio. Os capítulos se constroem uns em cima dos outros, o capítulo 3 assume que você já leu o 1 e o 2.",[11,20,21,22,29],{},"O dataset é o ",[23,24,28],"a",{"href":25,"rel":26},"https:\u002F\u002Fwww.kaggle.com\u002Fdatasets\u002Folistbr\u002Fbrazilian-ecommerce",[27],"nofollow","Brazilian E-Commerce Public Dataset, da Olist",", disponível no Kaggle. São dados reais (anonimizados) de mais de 100 mil pedidos feitos entre 2016 e 2018 num marketplace brasileiro de verdade, cliente, vendedor, produto, pagamento, avaliação, tudo interligado por chave. Todo o processamento pesado (pandas, scikit-learn, XGBoost, SHAP) roda num notebook que eu executo no Google Colab, com GPU T4 quando o capítulo precisar. Os resultados reais desses notebooks é que viram gráfico interativo aqui no blog, nunca um número inventado.",[31,32,34],"h2",{"id":33},"os-quatro-cenários","Os quatro cenários",[11,36,37],{},"Definidos já no primeiro capítulo, porque eles guiam o projeto inteiro:",[39,40,41,48,54,60],"ol",{},[42,43,44,47],"li",{},[15,45,46],{},"Previsão de atraso na entrega"," (classificação binária): o pedido chega depois da data estimada ou não?",[42,49,50,53],{},[15,51,52],{},"Previsão da nota de avaliação"," (classificação\u002Fregressão): quantas estrelas o cliente vai dar?",[42,55,56,59],{},[15,57,58],{},"Previsão do valor do frete ou do pedido"," (regressão): quanto vai custar?",[42,61,62,65],{},[15,63,64],{},"Segmentação de clientes"," (clustering, RFM): que tipos de cliente existem nessa base?",[11,67,68],{},"Cada um puxa um recorte diferente do mesmo dataframe, e o objetivo é te mostrar como a mesma base de dado serve pra perguntas de negócio bem diferentes.",[11,70,71,72,76],{},"Começa por ",[23,73,75],{"href":74},"\u002Fprojects\u002Folist-ecommerce\u002F01-intro-relational-model-eda","Capítulo 1: modelo relacional e primeira exploração",".",{"title":78,"searchDepth":79,"depth":79,"links":80},"",2,[81],{"id":33,"depth":79,"text":34},"Um case real de ciência de dados sobre o marketplace brasileiro Olist. Do CSV bruto até modelo treinado, atravessando EDA, engenharia de feature, treino de quatro modelos diferentes e interpretabilidade.","md",{},true,1,"\u002Fpt\u002Fprojects\u002Folist-ecommerce",{"title":6,"description":82},"published","pt\u002Fprojects\u002Folist-ecommerce\u002Findex",[92,93,94,95],"pandas","scikit-learn","xgboost","shap","ZaT33HNzlvJPuyvHZMJ8inWDeP5-5X8APJrwC_JiiRo",[98,553,1037,1432],{"id":99,"title":100,"body":101,"cover":3,"date":543,"description":544,"extension":83,"meta":545,"navigation":85,"order":86,"path":546,"project":547,"seo":548,"status":89,"stem":549,"tags":550,"__hash__":552},"projectChapters\u002Fpt\u002Fprojects\u002Folist-ecommerce\u002F01-intro-relational-model-eda.md","Nove Tabelas, Um Só Negócio: o Modelo Relacional da Olist",{"type":8,"value":102,"toc":535},[103,116,120,182,185,224,228,231,351,356,360,376,438,457,461,464,507,511,514,517,524,528,531],[11,104,105,106,110,111,76],{},"Antes de treinar qualquer modelo, eu preciso entender o formato do dado. E o dataset da Olist não é uma tabela só, é um mini banco relacional de verdade: nove CSVs, cada um representando uma entidade do negócio (pedido, item, pagamento, avaliação, cliente, vendedor, produto), ligados entre si por chave. Rodei tudo isso no notebook ",[107,108,109],"code",{},"01_intro_eda.ipynb",", dentro do próprio diretório do dataset, e deixei ele público no Colab: ",[23,112,115],{"href":113,"rel":114},"https:\u002F\u002Fcolab.research.google.com\u002Fdrive\u002F1kQvo5YjlLPiODqGhlAGZNk84ikg-PZg7?usp=sharing",[27],"confere o notebook completo aqui",[31,117,119],{"id":118},"o-modelo-relacional","O modelo relacional",[121,122,126],"pre",{"className":123,"code":124,"language":125,"meta":78,"style":78},"language-python shiki shiki-themes github-light github-dark","orders = pd.read_csv('olist_orders_dataset.csv')\norder_items = pd.read_csv('olist_order_items_dataset.csv')\npayments = pd.read_csv('olist_order_payments_dataset.csv')\nreviews = pd.read_csv('olist_order_reviews_dataset.csv')\ncustomers = pd.read_csv('olist_customers_dataset.csv')\nsellers = pd.read_csv('olist_sellers_dataset.csv')\nproducts = pd.read_csv('olist_products_dataset.csv')\ngeolocation = pd.read_csv('olist_geolocation_dataset.csv')\ncategory_translation = pd.read_csv('product_category_name_translation.csv')\n","python",[107,127,128,135,140,146,152,158,164,170,176],{"__ignoreMap":78},[129,130,132],"span",{"class":131,"line":86},"line",[129,133,134],{},"orders = pd.read_csv('olist_orders_dataset.csv')\n",[129,136,137],{"class":131,"line":79},[129,138,139],{},"order_items = pd.read_csv('olist_order_items_dataset.csv')\n",[129,141,143],{"class":131,"line":142},3,[129,144,145],{},"payments = pd.read_csv('olist_order_payments_dataset.csv')\n",[129,147,149],{"class":131,"line":148},4,[129,150,151],{},"reviews = pd.read_csv('olist_order_reviews_dataset.csv')\n",[129,153,155],{"class":131,"line":154},5,[129,156,157],{},"customers = pd.read_csv('olist_customers_dataset.csv')\n",[129,159,161],{"class":131,"line":160},6,[129,162,163],{},"sellers = pd.read_csv('olist_sellers_dataset.csv')\n",[129,165,167],{"class":131,"line":166},7,[129,168,169],{},"products = pd.read_csv('olist_products_dataset.csv')\n",[129,171,173],{"class":131,"line":172},8,[129,174,175],{},"geolocation = pd.read_csv('olist_geolocation_dataset.csv')\n",[129,177,179],{"class":131,"line":178},9,[129,180,181],{},"category_translation = pd.read_csv('product_category_name_translation.csv')\n",[183,184],"olist-relational-model",{},[11,186,187,190,191,194,195,194,198,201,202,205,206,194,209,194,212,215,216,219,220,223],{},[107,188,189],{},"order_id"," é a chave que costura quase tudo: aparece em ",[107,192,193],{},"orders",", ",[107,196,197],{},"order_items",[107,199,200],{},"payments"," e ",[107,203,204],{},"reviews",". ",[107,207,208],{},"customer_id",[107,210,211],{},"product_id",[107,213,214],{},"seller_id"," e o prefixo de CEP fecham o resto das ligações. Um detalhe que já vale marcar: ",[107,217,218],{},"olist_order_reviews_dataset.csv"," tem 104.719 linhas de texto bruto no arquivo, mas o pandas só reconhece ",[15,221,222],{},"99.224 registros de verdade"," ao ler o CSV. A diferença é porque um monte de comentário de review tem quebra de linha literal dentro do campo de texto (o cliente escreveu em vários parágrafos), e o parser do pandas conta isso corretamente como um registro só, enquanto contar linha bruta do arquivo superestima. Boa lembrança de que \"número de linha do arquivo\" e \"número de registro\" nem sempre são a mesma coisa num CSV com campo de texto livre.",[31,225,227],{"id":226},"qualidade-do-dado-nulo-tem-história","Qualidade do dado: nulo tem história",[11,229,230],{},"Nem todo nulo é problema, às vezes ele é informação. Nas tabelas onde nulo aparece:",[232,233,234,255],"table",{},[235,236,237],"thead",{},[238,239,240,245,248,252],"tr",{},[241,242,244],"th",{"align":243},"left","Tabela",[241,246,247],{"align":243},"Coluna",[241,249,251],{"align":250},"right","Nulos",[241,253,254],{"align":250},"%",[256,257,258,272,285,298,311,324,338],"tbody",{},[238,259,260,263,266,269],{},[261,262,193],"td",{"align":243},[261,264,265],{"align":243},"order_approved_at",[261,267,268],{"align":250},"160",[261,270,271],{"align":250},"0,2%",[238,273,274,276,279,282],{},[261,275,193],{"align":243},[261,277,278],{"align":243},"order_delivered_carrier_date",[261,280,281],{"align":250},"1.783",[261,283,284],{"align":250},"1,8%",[238,286,287,289,292,295],{},[261,288,193],{"align":243},[261,290,291],{"align":243},"order_delivered_customer_date",[261,293,294],{"align":250},"2.965",[261,296,297],{"align":250},"3,0%",[238,299,300,302,305,308],{},[261,301,204],{"align":243},[261,303,304],{"align":243},"review_comment_title",[261,306,307],{"align":250},"87.656",[261,309,310],{"align":250},"88,3%",[238,312,313,315,318,321],{},[261,314,204],{"align":243},[261,316,317],{"align":243},"review_comment_message",[261,319,320],{"align":250},"58.247",[261,322,323],{"align":250},"58,7%",[238,325,326,329,332,335],{},[261,327,328],{"align":243},"products",[261,330,331],{"align":243},"product_category_name (+ 3 outras colunas de produto)",[261,333,334],{"align":250},"610",[261,336,337],{"align":250},"1,9%",[238,339,340,342,345,348],{},[261,341,328],{"align":243},[261,343,344],{"align":243},"peso\u002Fdimensões (4 colunas)",[261,346,347],{"align":250},"2",[261,349,350],{"align":250},"0,0%",[11,352,353,355],{},[107,354,291],{}," nulo em 3% dos pedidos não é erro de captura, é pedido que nunca chegou (cancelado, extraviado, ainda em trânsito quando o dataset foi congelado). Isso vira feature importante lá na frente, quando eu for montar o cenário de atraso: um pedido sem data de entrega não tem como calcular atraso, então esses 2.965 pedidos precisam de tratamento explícito (excluir da análise de atraso, ou tratar como categoria própria), não posso simplesmente preencher com zero ou com a média. Review sem título ou comentário (88% e 59% dos casos) também não é problema, a maioria dos clientes só dá a nota e não escreve nada, comportamento normal de review de e-commerce.",[31,357,359],{"id":358},"montando-o-dataframe-mestre","Montando o dataframe mestre",[11,361,362,363,366,367,369,370,372,373,375],{},"A granularidade natural do dataset é ",[15,364,365],{},"item de pedido",", não pedido inteiro: um ",[107,368,189],{}," pode ter vários itens, de vendedores diferentes, cada um com seu próprio preço e frete. Por isso o merge parte de ",[107,371,197],{},", não de ",[107,374,193],{},":",[121,377,379],{"className":123,"code":378,"language":125,"meta":78,"style":78},"produtos_com_categoria_en = products.merge(category_translation, on='product_category_name', how='left')\n\nmestre = (\n    order_items\n    .merge(orders, on='order_id', how='left')\n    .merge(customers, on='customer_id', how='left')\n    .merge(produtos_com_categoria_en, on='product_id', how='left')\n    .merge(sellers, on='seller_id', how='left')\n    .merge(payments, on='order_id', how='left')\n    .merge(reviews, on='order_id', how='left')\n)\n",[107,380,381,386,391,396,401,406,411,416,421,426,432],{"__ignoreMap":78},[129,382,383],{"class":131,"line":86},[129,384,385],{},"produtos_com_categoria_en = products.merge(category_translation, on='product_category_name', how='left')\n",[129,387,388],{"class":131,"line":79},[129,389,390],{"emptyLinePlaceholder":85},"\n",[129,392,393],{"class":131,"line":142},[129,394,395],{},"mestre = (\n",[129,397,398],{"class":131,"line":148},[129,399,400],{},"    order_items\n",[129,402,403],{"class":131,"line":154},[129,404,405],{},"    .merge(orders, on='order_id', how='left')\n",[129,407,408],{"class":131,"line":160},[129,409,410],{},"    .merge(customers, on='customer_id', how='left')\n",[129,412,413],{"class":131,"line":166},[129,414,415],{},"    .merge(produtos_com_categoria_en, on='product_id', how='left')\n",[129,417,418],{"class":131,"line":172},[129,419,420],{},"    .merge(sellers, on='seller_id', how='left')\n",[129,422,423],{"class":131,"line":178},[129,424,425],{},"    .merge(payments, on='order_id', how='left')\n",[129,427,429],{"class":131,"line":428},10,[129,430,431],{},"    .merge(reviews, on='order_id', how='left')\n",[129,433,435],{"class":131,"line":434},11,[129,436,437],{},")\n",[11,439,440,442,443,446,447,449,450,452,453,456],{},[107,441,197],{}," sozinho tem 112.650 linhas. Depois de todos os merges, o dataframe mestre tem ",[15,444,445],{},"118.310 linhas",", mais do que o ponto de partida. Isso não é bug: quando um pedido é pago em várias parcelas registradas como linhas separadas em ",[107,448,200],{},", ou recebe mais de uma avaliação em ",[107,451,204],{},", o merge multiplica aquela linha de item de pedido pra cada combinação. É um comportamento esperado de merge relacional, mas é exatamente o tipo de coisa que, se eu não checar o ",[107,454,455],{},"shape"," antes e depois, passa despercebido e infla contagem em qualquer agregação futura.",[31,458,460],{"id":459},"os-quatro-cenários-de-ml","Os quatro cenários de ML",[11,462,463],{},"Com o dataframe mestre em mãos, definido nos capítulos seguintes:",[39,465,466,478,488,502],{},[42,467,468,471,472,474,475,76],{},[15,469,470],{},"Atraso na entrega"," (classificação binária): comparar ",[107,473,291],{}," com ",[107,476,477],{},"order_estimated_delivery_date",[42,479,480,483,484,487],{},[15,481,482],{},"Nota da avaliação"," (classificação multiclasse ou regressão): ",[107,485,486],{},"review_score",", de 1 a 5.",[42,489,490,493,494,497,498,501],{},[15,491,492],{},"Valor do frete ou do pedido"," (regressão): ",[107,495,496],{},"freight_value",", ou a soma de ",[107,499,500],{},"price"," por pedido.",[42,503,504,506],{},[15,505,64],{}," (clustering, sem target): Recência, Frequência e Valor monetário por cliente, técnica RFM.",[31,508,510],{"id":509},"pedidos-por-mês-o-pico-de-black-friday","Pedidos por mês: o pico de Black Friday",[11,512,513],{},"Uma primeira olhada temporal, contando pedido único por mês de compra:",[515,516],"olist-orders-per-month-chart",{},[11,518,519,520,523],{},"Setembro de 2016 começa com só 4 pedidos (a Olist mal tinha começado), o volume cresce mês a mês ao longo de 2017, e novembro de 2017 dá um salto brusco pra ",[15,521,522],{},"7.544 pedidos",", contra 4.631 em outubro e 5.673 em dezembro do mesmo ano. Isso é a Black Friday, um pico isolado que quebra a tendência suave de crescimento, e vai ser um detalhe importante quando eu for pensar em sazonalidade nos capítulos de feature engineering. Setembro e outubro de 2018 aparecem com só 16 e 4 pedidos, sinal de que o dataset foi congelado no meio do mês, não que as vendas despencaram.",[31,525,527],{"id":526},"fechando-o-capítulo","Fechando o capítulo",[11,529,530],{},"Modelo relacional mapeado, dataframe mestre montado (118.310 linhas), qualidade de dado checada (os nulos de entrega e de review têm explicação, não são erro), e os quatro cenários definidos. No próximo capítulo eu entro na visualização de verdade: mapa geográfico de pedido e atraso por estado, distribuição de categoria, forma de pagamento, e a relação entre atraso e nota de avaliação, que já dá um \"aha moment\" antes mesmo de treinar o primeiro modelo.",[532,533,534],"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":78,"searchDepth":79,"depth":79,"links":536},[537,538,539,540,541,542],{"id":118,"depth":79,"text":119},{"id":226,"depth":79,"text":227},{"id":358,"depth":79,"text":359},{"id":459,"depth":79,"text":460},{"id":509,"depth":79,"text":510},{"id":526,"depth":79,"text":527},"2026-08-24","Primeiro capítulo do case Olist: carrego os nove CSVs, monto o dataframe mestre, checo a qualidade real do dado, e defino os quatro cenários de ML que o resto do projeto vai cobrir.",{},"\u002Fpt\u002Fprojects\u002Folist-ecommerce\u002F01-intro-relational-model-eda","olist-ecommerce",{"title":100,"description":544},"pt\u002Fprojects\u002Folist-ecommerce\u002F01-intro-relational-model-eda",[92,551],"eda","058fQ3ZIuDZk4VYmbtciafzHMkPtF5IyFJqEWIwsaXs",{"id":554,"title":555,"body":556,"cover":3,"date":543,"description":1029,"extension":83,"meta":1030,"navigation":85,"order":79,"path":1031,"project":547,"seo":1032,"status":89,"stem":1033,"tags":1034,"__hash__":1036},"projectChapters\u002Fpt\u002Fprojects\u002Folist-ecommerce\u002F02-visualizacoes-exploratorias.md","O Brasil Compra de Dia Útil: Storytelling Exploratório da Olist",{"type":8,"value":557,"toc":1020},[558,566,570,628,631,634,638,713,716,723,727,775,778,792,796,838,841,848,852,890,893,896,899,946,949,955,959,997,1000,1013,1015,1018],[11,559,560,561,76],{},"O capítulo 1 montou o modelo relacional e o dataframe mestre. Agora eu vou deixar o dado falar visualmente, seis ângulos diferentes do mesmo negócio, antes de treinar qualquer modelo. Notebook completo, executado, ",[23,562,565],{"href":563,"rel":564},"https:\u002F\u002Fcolab.research.google.com\u002Fdrive\u002F1PrCuoWoAnb5paNi5dxzbSlxAGrcyA_mk?usp=sharing",[27],"público no Colab",[31,567,569],{"id":568},"temporal-o-brasil-compra-de-dia-útil","Temporal: o Brasil compra de dia útil",[121,571,573],{"className":123,"code":572,"language":125,"meta":78,"style":78},"orders_com_hora = orders.dropna(subset=['order_purchase_timestamp']).copy()\norders_com_hora['weekday'] = orders_com_hora['order_purchase_timestamp'].dt.weekday\norders_com_hora['hour'] = orders_com_hora['order_purchase_timestamp'].dt.hour\n\nweekday_hour = (\n    orders_com_hora\n    .groupby(['weekday', 'hour'])['order_id']\n    .nunique()\n    .reset_index()\n    .rename(columns={'order_id': 'orders'})\n)\n",[107,574,575,580,585,590,594,599,604,609,614,619,624],{"__ignoreMap":78},[129,576,577],{"class":131,"line":86},[129,578,579],{},"orders_com_hora = orders.dropna(subset=['order_purchase_timestamp']).copy()\n",[129,581,582],{"class":131,"line":79},[129,583,584],{},"orders_com_hora['weekday'] = orders_com_hora['order_purchase_timestamp'].dt.weekday\n",[129,586,587],{"class":131,"line":142},[129,588,589],{},"orders_com_hora['hour'] = orders_com_hora['order_purchase_timestamp'].dt.hour\n",[129,591,592],{"class":131,"line":148},[129,593,390],{"emptyLinePlaceholder":85},[129,595,596],{"class":131,"line":154},[129,597,598],{},"weekday_hour = (\n",[129,600,601],{"class":131,"line":160},[129,602,603],{},"    orders_com_hora\n",[129,605,606],{"class":131,"line":166},[129,607,608],{},"    .groupby(['weekday', 'hour'])['order_id']\n",[129,610,611],{"class":131,"line":172},[129,612,613],{},"    .nunique()\n",[129,615,616],{"class":131,"line":178},[129,617,618],{},"    .reset_index()\n",[129,620,621],{"class":131,"line":428},[129,622,623],{},"    .rename(columns={'order_id': 'orders'})\n",[129,625,626],{"class":131,"line":434},[129,627,437],{},[629,630],"olist-weekday-hour-heatmap",{},[11,632,633],{},"168 células (7 dias × 24 horas), soma batendo exatamente com o total de pedidos, sempre bom conferir isso antes de confiar no gráfico. O pico é terça-feira às 14h, com 1.124 pedidos numa hora só. Olhando o total por dia da semana, segunda a sexta somam entre 14 e 16 mil pedidos cada, enquanto sábado cai pra 10.887 e domingo pra 11.960, uma queda de quase 30%. Por hora, o horário de almoço e começo de tarde (11h, 13h-16h) concentra o grosso do movimento, e a madrugada (3h-5h) praticamente para, menos de 300 pedidos em cada uma dessas horas no dataset inteiro. Isso é o padrão clássico de \"compra de trabalho remoto ou pausa do escritório\", não de compra de lazer noturno.",[31,635,637],{"id":636},"geográfico-o-norte-espera-mais","Geográfico: o Norte espera mais",[121,639,641],{"className":123,"code":640,"language":125,"meta":78,"style":78},"orders_com_cliente = orders.merge(customers, on='customer_id', how='left')\n\nfrete_por_pedido = order_items.groupby('order_id')['freight_value'].sum().reset_index()\norders_com_cliente = orders_com_cliente.merge(frete_por_pedido, on='order_id', how='left')\n\norders_com_cliente['delivery_days'] = (\n    orders_com_cliente['order_delivered_customer_date'] - orders_com_cliente['order_purchase_timestamp']\n).dt.days\n\npor_estado = orders_com_cliente.groupby('customer_state').agg(\n    orders=('order_id', 'nunique'),\n    avg_freight=('freight_value', 'mean'),\n    avg_delivery_days=('delivery_days', 'mean'),\n).reset_index()\n",[107,642,643,648,652,657,662,666,671,676,681,685,690,695,701,707],{"__ignoreMap":78},[129,644,645],{"class":131,"line":86},[129,646,647],{},"orders_com_cliente = orders.merge(customers, on='customer_id', how='left')\n",[129,649,650],{"class":131,"line":79},[129,651,390],{"emptyLinePlaceholder":85},[129,653,654],{"class":131,"line":142},[129,655,656],{},"frete_por_pedido = order_items.groupby('order_id')['freight_value'].sum().reset_index()\n",[129,658,659],{"class":131,"line":148},[129,660,661],{},"orders_com_cliente = orders_com_cliente.merge(frete_por_pedido, on='order_id', how='left')\n",[129,663,664],{"class":131,"line":154},[129,665,390],{"emptyLinePlaceholder":85},[129,667,668],{"class":131,"line":160},[129,669,670],{},"orders_com_cliente['delivery_days'] = (\n",[129,672,673],{"class":131,"line":166},[129,674,675],{},"    orders_com_cliente['order_delivered_customer_date'] - orders_com_cliente['order_purchase_timestamp']\n",[129,677,678],{"class":131,"line":172},[129,679,680],{},").dt.days\n",[129,682,683],{"class":131,"line":178},[129,684,390],{"emptyLinePlaceholder":85},[129,686,687],{"class":131,"line":428},[129,688,689],{},"por_estado = orders_com_cliente.groupby('customer_state').agg(\n",[129,691,692],{"class":131,"line":434},[129,693,694],{},"    orders=('order_id', 'nunique'),\n",[129,696,698],{"class":131,"line":697},12,[129,699,700],{},"    avg_freight=('freight_value', 'mean'),\n",[129,702,704],{"class":131,"line":703},13,[129,705,706],{},"    avg_delivery_days=('delivery_days', 'mean'),\n",[129,708,710],{"class":131,"line":709},14,[129,711,712],{},").reset_index()\n",[714,715],"olist-state-choropleth",{},[11,717,718,719,722],{},"O mapa mostra o tempo médio real de entrega (compra até entrega) por estado do cliente. ",[15,720,721],{},"Roraima lidera com quase 29 dias de espera média"," (28,98), seguido por Amapá (26,73), Amazonas (25,99) e Alagoas (24,04). Não é coincidência esses estados aparecerem no topo: Roraima, Amapá e Amazonas são os três estados mais distantes do eixo industrial e logístico do Sudeste, onde a maior parte dos vendedores da Olist está concentrada. São Paulo, pra comparação, fica bem mais perto da ponta rápida dessa distribuição. Essa relação distância-tempo de entrega é candidata natural a feature forte quando eu for montar o cenário de previsão de atraso.",[31,724,726],{"id":725},"categoria-beleza-e-saúde-lidera-o-faturamento","Categoria: beleza e saúde lidera o faturamento",[121,728,730],{"className":123,"code":729,"language":125,"meta":78,"style":78},"top_categorias = (\n    mestre\n    .dropna(subset=['product_category_name_english'])\n    .groupby('product_category_name_english')['price']\n    .sum()\n    .sort_values(ascending=False)\n    .head(15)\n    .reset_index()\n)\n",[107,731,732,737,742,747,752,757,762,767,771],{"__ignoreMap":78},[129,733,734],{"class":131,"line":86},[129,735,736],{},"top_categorias = (\n",[129,738,739],{"class":131,"line":79},[129,740,741],{},"    mestre\n",[129,743,744],{"class":131,"line":142},[129,745,746],{},"    .dropna(subset=['product_category_name_english'])\n",[129,748,749],{"class":131,"line":148},[129,750,751],{},"    .groupby('product_category_name_english')['price']\n",[129,753,754],{"class":131,"line":154},[129,755,756],{},"    .sum()\n",[129,758,759],{"class":131,"line":160},[129,760,761],{},"    .sort_values(ascending=False)\n",[129,763,764],{"class":131,"line":166},[129,765,766],{},"    .head(15)\n",[129,768,769],{"class":131,"line":172},[129,770,618],{},[129,772,773],{"class":131,"line":178},[129,774,437],{},[776,777],"olist-top-categories-chart",{},[11,779,780,783,784,787,788,791],{},[107,781,782],{},"health_beauty"," (beleza e saúde) lidera com 1.301.947,97 dólares em receita somada, seguida de perto por ",[107,785,786],{},"watches_gifts"," (relógios e presentes, 1.254.322,95) e ",[107,789,790],{},"bed_bath_table"," (cama, mesa e banho, 1.107.249,09). É receita, não é frete nem quantidade de item, então uma categoria pode liderar por vender caro (relógio) ou por vender muito (cama\u002Fmesa\u002Fbanho é item de reposição frequente).",[31,793,795],{"id":794},"pagamento-cartão-de-crédito-domina","Pagamento: cartão de crédito domina",[121,797,799],{"className":123,"code":798,"language":125,"meta":78,"style":78},"tipos_pagamento = (\n    payments[payments['payment_type'] != 'not_defined']\n    .groupby('payment_type')['order_id']\n    .nunique()\n    .reset_index()\n    .rename(columns={'order_id': 'count'})\n    .sort_values('count', ascending=False)\n)\n",[107,800,801,806,811,816,820,824,829,834],{"__ignoreMap":78},[129,802,803],{"class":131,"line":86},[129,804,805],{},"tipos_pagamento = (\n",[129,807,808],{"class":131,"line":79},[129,809,810],{},"    payments[payments['payment_type'] != 'not_defined']\n",[129,812,813],{"class":131,"line":142},[129,814,815],{},"    .groupby('payment_type')['order_id']\n",[129,817,818],{"class":131,"line":148},[129,819,613],{},[129,821,822],{"class":131,"line":154},[129,823,618],{},[129,825,826],{"class":131,"line":160},[129,827,828],{},"    .rename(columns={'order_id': 'count'})\n",[129,830,831],{"class":131,"line":166},[129,832,833],{},"    .sort_values('count', ascending=False)\n",[129,835,836],{"class":131,"line":172},[129,837,437],{},[839,840],"olist-payment-types-chart",{},[11,842,843,844,847],{},"Cartão de crédito domina disparado: 76.505 pedidos, mais de 3 vezes o segundo colocado (boleto, 19.784). Voucher vem com 3.866 e cartão de débito com só 1.528. Descartei a categoria ",[107,845,846],{},"not_defined"," do gráfico, tinha só 3 linhas em mais de 100 mil pagamentos, ruído puro, não merece fatia de pizza.",[31,849,851],{"id":850},"satisfação-a-maioria-ama-mas-quem-odeia-é-vocal","Satisfação: a maioria ama, mas quem odeia é vocal",[121,853,855],{"className":123,"code":854,"language":125,"meta":78,"style":78},"distribuicao_notas = (\n    reviews\n    .groupby('review_score')['review_id']\n    .count()\n    .reset_index()\n    .rename(columns={'review_id': 'count'})\n)\n",[107,856,857,862,867,872,877,881,886],{"__ignoreMap":78},[129,858,859],{"class":131,"line":86},[129,860,861],{},"distribuicao_notas = (\n",[129,863,864],{"class":131,"line":79},[129,865,866],{},"    reviews\n",[129,868,869],{"class":131,"line":142},[129,870,871],{},"    .groupby('review_score')['review_id']\n",[129,873,874],{"class":131,"line":148},[129,875,876],{},"    .count()\n",[129,878,879],{"class":131,"line":154},[129,880,618],{},[129,882,883],{"class":131,"line":160},[129,884,885],{},"    .rename(columns={'review_id': 'count'})\n",[129,887,888],{"class":131,"line":166},[129,889,437],{},[891,892],"olist-review-score-chart",{},[11,894,895],{},"57.328 avaliações de nota 5, mais da metade do total, e nota 4 vem em segundo com 19.142. Mas repara no terceiro lugar: nota 1, com 11.424 avaliações, sozinha soma mais que nota 2 (3.151) e nota 3 (8.179) juntas. Isso é o padrão clássico de review de e-commerce: cliente satisfeito às vezes nem avalia, cliente neutro quase nunca avalia, e cliente muito insatisfeito sempre avalia pra desabafar. Uma distribuição assim bimodal (pico grande em 5, segundo pico menor em 1) já é um aviso pra quando eu for montar o cenário de previsão de nota: não é uma escala contínua bem comportada, é quase uma decisão \"adorei ou detestei\" com um meio-termo raro.",[11,897,898],{},"Junto com isso, uma amostra real (400 pedidos) cruzando atraso da entrega com a nota dada:",[121,900,902],{"className":123,"code":901,"language":125,"meta":78,"style":78},"nota_atraso = (\n    orders[['order_id', 'order_delivered_customer_date', 'order_estimated_delivery_date']]\n    .merge(reviews[['order_id', 'review_score']], on='order_id', how='inner')\n    .dropna(subset=['order_delivered_customer_date', 'order_estimated_delivery_date', 'review_score'])\n)\n\nnota_atraso['delay_days'] = (\n    nota_atraso['order_delivered_customer_date'] - nota_atraso['order_estimated_delivery_date']\n).dt.days\n",[107,903,904,909,914,919,924,928,932,937,942],{"__ignoreMap":78},[129,905,906],{"class":131,"line":86},[129,907,908],{},"nota_atraso = (\n",[129,910,911],{"class":131,"line":79},[129,912,913],{},"    orders[['order_id', 'order_delivered_customer_date', 'order_estimated_delivery_date']]\n",[129,915,916],{"class":131,"line":142},[129,917,918],{},"    .merge(reviews[['order_id', 'review_score']], on='order_id', how='inner')\n",[129,920,921],{"class":131,"line":148},[129,922,923],{},"    .dropna(subset=['order_delivered_customer_date', 'order_estimated_delivery_date', 'review_score'])\n",[129,925,926],{"class":131,"line":154},[129,927,437],{},[129,929,930],{"class":131,"line":160},[129,931,390],{"emptyLinePlaceholder":85},[129,933,934],{"class":131,"line":166},[129,935,936],{},"nota_atraso['delay_days'] = (\n",[129,938,939],{"class":131,"line":172},[129,940,941],{},"    nota_atraso['order_delivered_customer_date'] - nota_atraso['order_estimated_delivery_date']\n",[129,943,944],{"class":131,"line":178},[129,945,680],{},[947,948],"olist-review-delay-scatter",{},[11,950,951,954],{},[107,952,953],{},"delay_days"," negativo é entrega adiantada (chegou antes do prometido), positivo é atraso de verdade. Repara que os pontos de nota 1 (a linha de baixo) aparecem espalhados por todo o eixo, inclusive nos negativos, mas com uma concentração visualmente maior no lado direito (atraso) do que os pontos de nota 5.",[31,956,958],{"id":957},"correlação-o-punchline-antes-do-modelo","Correlação: o punchline antes do modelo",[121,960,962],{"className":123,"code":961,"language":125,"meta":78,"style":78},"features_numericas = mestre[['price', 'freight_value', 'product_weight_g', 'payment_value', 'review_score']].copy()\nfeatures_numericas['delivery_days'] = (\n    mestre['order_delivered_customer_date'] - mestre['order_purchase_timestamp']\n).dt.days\nfeatures_numericas = features_numericas.dropna()\n\nmatriz_corr = features_numericas.corr()\n",[107,963,964,969,974,979,983,988,992],{"__ignoreMap":78},[129,965,966],{"class":131,"line":86},[129,967,968],{},"features_numericas = mestre[['price', 'freight_value', 'product_weight_g', 'payment_value', 'review_score']].copy()\n",[129,970,971],{"class":131,"line":79},[129,972,973],{},"features_numericas['delivery_days'] = (\n",[129,975,976],{"class":131,"line":142},[129,977,978],{},"    mestre['order_delivered_customer_date'] - mestre['order_purchase_timestamp']\n",[129,980,981],{"class":131,"line":148},[129,982,680],{},[129,984,985],{"class":131,"line":154},[129,986,987],{},"features_numericas = features_numericas.dropna()\n",[129,989,990],{"class":131,"line":160},[129,991,390],{"emptyLinePlaceholder":85},[129,993,994],{"class":131,"line":166},[129,995,996],{},"matriz_corr = features_numericas.corr()\n",[998,999],"olist-correlation-heatmap",{},[11,1001,1002,1003,1005,1006,474,1009,1012],{},"Aqui está o número que eu prometi lá no fechamento do capítulo 1: ",[107,1004,486],{}," correlaciona ",[15,1007,1008],{},"-0,30",[107,1010,1011],{},"delivery_days",", calculado em cima de 114.838 linhas do dataframe mestre. É a correlação mais forte que qualquer feature numérica tem com a nota de avaliação (preço, frete e peso do produto ficam todos abaixo de 0,08 em módulo). Não é uma correlação gigante (longe de -1), mas é claramente a mais relevante do grupo, e bate exatamente com o que o mapa geográfico já insinuava: entrega demorada é o fator que mais aparenta puxar a satisfação do cliente pra baixo, mais do que quanto o produto custou ou pesou.",[31,1014,527],{"id":526},[11,1016,1017],{},"Seis ângulos, um padrão emergindo: tempo de entrega aparece três vezes (mapa, scatter, correlação) como o fio condutor mais forte pra explicar insatisfação. Cartão de crédito domina pagamento, beleza\u002Fsaúde domina receita, terça-feira à tarde é o pico de compra. No próximo capítulo eu entro em engenharia e seleção de feature pros quatro cenários definidos no capítulo 1, e a discussão de vazamento de dado no cenário de atraso vai ficar bem mais concreta com esses números geográficos e de correlação já na mão.",[532,1019,534],{},{"title":78,"searchDepth":79,"depth":79,"links":1021},[1022,1023,1024,1025,1026,1027,1028],{"id":568,"depth":79,"text":569},{"id":636,"depth":79,"text":637},{"id":725,"depth":79,"text":726},{"id":794,"depth":79,"text":795},{"id":850,"depth":79,"text":851},{"id":957,"depth":79,"text":958},{"id":526,"depth":79,"text":527},"Segundo capítulo do case Olist: mapa de calor de horário de compra, mapa coroplético de atraso por estado, categorias que mais faturam, forma de pagamento, e a correlação que já entrega o punchline antes de qualquer modelo: atraso mata nota.",{},"\u002Fpt\u002Fprojects\u002Folist-ecommerce\u002F02-visualizacoes-exploratorias",{"title":555,"description":1029},"pt\u002Fprojects\u002Folist-ecommerce\u002F02-visualizacoes-exploratorias",[92,551,1035],"visualizacao","H_Prd-7xLiMf38oyKD33VdFKH7EWe_HXV2SeqCY2MRM",{"id":1038,"title":1039,"body":1040,"cover":3,"date":543,"description":1423,"extension":83,"meta":1424,"navigation":85,"order":142,"path":1425,"project":547,"seo":1426,"status":89,"stem":1427,"tags":1428,"__hash__":1431},"projectChapters\u002Fpt\u002Fprojects\u002Folist-ecommerce\u002F03-features-e-selecao.md","Vazamento de Dado: o Jeito Mais Fácil de Enganar a Si Mesmo",{"type":8,"value":1041,"toc":1415},[1042,1049,1053,1060,1109,1124,1128,1159,1174,1177,1180,1195,1198,1201,1213,1217,1225,1234,1294,1301,1305,1308,1362,1365,1369,1372,1401,1408,1410,1413],[11,1043,1044,1045,76],{},"Os capítulos 1 e 2 montaram o dataframe mestre e mostraram seis ângulos exploratórios. Agora eu preciso escolher, pra cada um dos quatro cenários definidos lá no capítulo 1, quais features fazem sentido usar. Notebook completo, executado, ",[23,1046,565],{"href":1047,"rel":1048},"https:\u002F\u002Fcolab.research.google.com\u002Fdrive\u002F1nt9wVC9_2H646HEKG-XTo6P4cvx-zPPr?usp=sharing",[27],[31,1050,1052],{"id":1051},"uma-feature-nova-primeiro-distância-cliente-vendedor","Uma feature nova primeiro: distância cliente-vendedor",[11,1054,1055,1056,1059],{},"O capítulo 2 mostrou que estado do cliente correlaciona com tempo de entrega. Antes de entrar nos cenários, calculei uma distância de verdade, não só \"mesmo estado ou não\": latitude\u002Flongitude média por prefixo de CEP (",[107,1057,1058],{},"olist_geolocation_dataset",", mais de 1 milhão de linhas, por isso a média primeiro), e a fórmula de Haversine entre cliente e vendedor.",[121,1061,1063],{"className":123,"code":1062,"language":125,"meta":78,"style":78},"geo_media = geolocation.groupby('geolocation_zip_code_prefix')[['geolocation_lat', 'geolocation_lng']].mean().reset_index()\n\ndef haversine(lat1, lon1, lat2, lon2):\n    R = 6371\n    phi1, phi2 = np.radians(lat1), np.radians(lat2)\n    dphi = np.radians(lat2 - lat1)\n    dlambda = np.radians(lon2 - lon1)\n    a = np.sin(dphi \u002F 2) ** 2 + np.cos(phi1) * np.cos(phi2) * np.sin(dlambda \u002F 2) ** 2\n    return 2 * R * np.arcsin(np.sqrt(a))\n",[107,1064,1065,1070,1074,1079,1084,1089,1094,1099,1104],{"__ignoreMap":78},[129,1066,1067],{"class":131,"line":86},[129,1068,1069],{},"geo_media = geolocation.groupby('geolocation_zip_code_prefix')[['geolocation_lat', 'geolocation_lng']].mean().reset_index()\n",[129,1071,1072],{"class":131,"line":79},[129,1073,390],{"emptyLinePlaceholder":85},[129,1075,1076],{"class":131,"line":142},[129,1077,1078],{},"def haversine(lat1, lon1, lat2, lon2):\n",[129,1080,1081],{"class":131,"line":148},[129,1082,1083],{},"    R = 6371\n",[129,1085,1086],{"class":131,"line":154},[129,1087,1088],{},"    phi1, phi2 = np.radians(lat1), np.radians(lat2)\n",[129,1090,1091],{"class":131,"line":160},[129,1092,1093],{},"    dphi = np.radians(lat2 - lat1)\n",[129,1095,1096],{"class":131,"line":166},[129,1097,1098],{},"    dlambda = np.radians(lon2 - lon1)\n",[129,1100,1101],{"class":131,"line":172},[129,1102,1103],{},"    a = np.sin(dphi \u002F 2) ** 2 + np.cos(phi1) * np.cos(phi2) * np.sin(dlambda \u002F 2) ** 2\n",[129,1105,1106],{"class":131,"line":178},[129,1107,1108],{},"    return 2 * R * np.arcsin(np.sqrt(a))\n",[11,1110,1111,1112,1115,1116,1119,1120,1123],{},"Achei um bug de verdade nessa etapa antes de publicar: na primeira rodada, o merge entre ",[107,1113,1114],{},"customer_zip_code_prefix","\u002F",[107,1117,1118],{},"seller_zip_code_prefix"," e a tabela de geolocalização perdeu 70% dos dados (só 30% de cobertura), porque o tipo da coluna de CEP não veio garantidamente igual dos dois lados em todo ambiente. Forcei ",[107,1121,1122],{},"int64"," explícito nos três antes do merge, e a cobertura voltou pro esperado: só 581 pedidos de 118.310 ficaram sem distância (99,5% de cobertura). A distância média cliente-vendedor no Brasil inteiro é 597 km, com máximo de 8.678 km, uma prova em número de como o país é gigante.",[31,1125,1127],{"id":1126},"cenário-1-atraso-na-entrega-e-a-demonstração-de-vazamento-de-dado","Cenário 1: Atraso na entrega, e a demonstração de vazamento de dado",[11,1129,1130,1131,194,1134,194,1137,194,1139,1141,1142,1145,1146,1149,1150,474,1152,1154,1155,1158],{},"Candidatas: ",[107,1132,1133],{},"distance_km",[107,1135,1136],{},"product_weight_g",[107,1138,500],{},[107,1140,496],{},", tempo de aprovação (",[107,1143,1144],{},"approval_hours","), mês da compra. O alvo ",[107,1147,1148],{},"atrasado"," é definido comparando ",[107,1151,291],{},[107,1153,477],{},". E aqui mora a armadilha clássica de todo curso de ML: se eu incluir ",[107,1156,1157],{},"atraso_dias"," (a própria diferença entre essas duas datas) como feature, o modelo não aprende a prever atraso, ele lê a resposta direto da prova.",[121,1160,1162],{"className":123,"code":1161,"language":125,"meta":78,"style":78},"features_honestas = ['distance_km', 'product_weight_g', 'price', 'freight_value', 'approval_hours', 'month']\nfeatures_vazadas = features_honestas + ['atraso_dias']\n",[107,1163,1164,1169],{"__ignoreMap":78},[129,1165,1166],{"class":131,"line":86},[129,1167,1168],{},"features_honestas = ['distance_km', 'product_weight_g', 'price', 'freight_value', 'approval_hours', 'month']\n",[129,1170,1171],{"class":131,"line":79},[129,1172,1173],{},"features_vazadas = features_honestas + ['atraso_dias']\n",[11,1175,1176],{},"Treinei os dois, RandomForest simples, mesma seed, mesma divisão treino\u002Fteste:",[1178,1179],"olist-leakage-comparison-chart",{},[11,1181,1182,1183,1186,1187,1190,1191,1194],{},"O modelo honesto chega em ",[15,1184,1185],{},"AUC 0,7388"," (95.968 pedidos usados, taxa real de atraso de 6,76%). O modelo vazado chega em ",[15,1188,1189],{},"AUC 1,0",", perfeito, porque ",[107,1192,1193],{},"atraso_dias > 0"," praticamente já É a definição do alvo. Um detalhe que vale destacar: a acurácia do modelo honesto sozinha (93,24%) parece ótima à primeira vista, mas como só 6,76% dos pedidos atrasam, um modelo bobo que sempre chuta \"no prazo\" já acerta 93,24% sem aprender nada. Acurácia sozinha engana quando as classes são desbalanceadas desse jeito, AUC é a métrica que conta a história real aqui.",[11,1196,1197],{},"Repara também a importância de feature do modelo honesto:",[1199,1200],"olist-feature-importance-chart",{},[11,1202,1203,1209,1210,1212],{},[15,1204,1205,1208],{},[107,1206,1207],{},"month"," lidera disparado, com 0,36 de importância",", praticamente o dobro da segunda colocada (",[107,1211,1133],{},", 0,18). Isso conecta direto com o achado do capítulo 2: novembro (Black Friday) sobrecarrega a logística de um jeito que nem distância física consegue explicar sozinha. Sazonalidade importa mais pra prever atraso do que o quão longe o vendedor está do cliente.",[31,1214,1216],{"id":1215},"cenário-2-nota-da-avaliação","Cenário 2: Nota da avaliação",[11,1218,1130,1219,1221,1222,1224],{},[107,1220,1157],{},", tempo de entrega, preço, frete, número de parcelas. Como ",[107,1223,486],{}," é uma escala ordinal bimodal (o capítulo 2 já mostrou isso), usei informação mútua em vez de correlação de Pearson simples:",[121,1226,1228],{"className":123,"code":1227,"language":125,"meta":78,"style":78},"mi = mutual_info_classif(X2, y2, random_state=42)\n",[107,1229,1230],{"__ignoreMap":78},[129,1231,1232],{"class":131,"line":86},[129,1233,1227],{},[232,1235,1236,1246],{},[235,1237,1238],{},[238,1239,1240,1243],{},[241,1241,1242],{"align":243},"Feature",[241,1244,1245],{"align":250},"Informação mútua",[256,1247,1248,1257,1266,1275,1284],{},[238,1249,1250,1254],{},[261,1251,1252],{"align":243},[107,1253,1157],{},[261,1255,1256],{"align":250},"0,0684",[238,1258,1259,1263],{},[261,1260,1261],{"align":243},[107,1262,1011],{},[261,1264,1265],{"align":250},"0,0571",[238,1267,1268,1272],{},[261,1269,1270],{"align":243},[107,1271,496],{},[261,1273,1274],{"align":250},"0,0077",[238,1276,1277,1281],{},[261,1278,1279],{"align":243},[107,1280,500],{},[261,1282,1283],{"align":250},"0,0074",[238,1285,1286,1291],{},[261,1287,1288],{"align":243},[107,1289,1290],{},"payment_installments",[261,1292,1293],{"align":250},"0,0026",[11,1295,1296,201,1298,1300],{},[107,1297,1157],{},[107,1299,1011],{}," dominam, quase 10 vezes mais informativas que preço ou frete (95.829 pedidos usados). Confirma com uma técnica diferente exatamente o que a correlação do capítulo 2 já tinha apontado.",[31,1302,1304],{"id":1303},"cenário-3-valor-do-frete","Cenário 3: Valor do frete",[11,1306,1307],{},"Candidatas: peso e as três dimensões do produto. Correlação de Pearson simples já resolve aqui (98.650 pedidos usados):",[232,1309,1310,1321],{},[235,1311,1312],{},[238,1313,1314,1316],{},[241,1315,1242],{"align":243},[241,1317,1318,1319],{"align":250},"Correlação com ",[107,1320,496],{},[256,1322,1323,1332,1342,1352],{},[238,1324,1325,1329],{},[261,1326,1327],{"align":243},[107,1328,1136],{},[261,1330,1331],{"align":250},"0,615",[238,1333,1334,1339],{},[261,1335,1336],{"align":243},[107,1337,1338],{},"product_height_cm",[261,1340,1341],{"align":250},"0,393",[238,1343,1344,1349],{},[261,1345,1346],{"align":243},[107,1347,1348],{},"product_width_cm",[261,1350,1351],{"align":250},"0,331",[238,1353,1354,1359],{},[261,1355,1356],{"align":243},[107,1357,1358],{},"product_length_cm",[261,1360,1361],{"align":250},"0,317",[11,1363,1364],{},"Peso domina moderadamente sobre qualquer dimensão isolada, o que faz sentido físico: transportadora cobra por peso volumétrico, mas peso real puxa mais a conta que qualquer lado da caixa sozinho.",[31,1366,1368],{"id":1367},"cenário-4-segmentação-de-clientes-rfm","Cenário 4: Segmentação de clientes (RFM)",[11,1370,1371],{},"Aqui não é seleção de feature no sentido supervisionado, é a engenharia das três variáveis que vão alimentar o clustering do capítulo 5: Recência, Frequência e Valor monetário por cliente.",[121,1373,1375],{"className":123,"code":1374,"language":125,"meta":78,"style":78},"rfm = oc.groupby('customer_unique_id').agg(\n    recencia_dias=('order_purchase_timestamp', lambda x: (data_max - x.max()).days),\n    frequencia=('order_id', 'nunique'),\n    valor_monetario=('payment_value', 'sum'),\n).reset_index()\n",[107,1376,1377,1382,1387,1392,1397],{"__ignoreMap":78},[129,1378,1379],{"class":131,"line":86},[129,1380,1381],{},"rfm = oc.groupby('customer_unique_id').agg(\n",[129,1383,1384],{"class":131,"line":79},[129,1385,1386],{},"    recencia_dias=('order_purchase_timestamp', lambda x: (data_max - x.max()).days),\n",[129,1388,1389],{"class":131,"line":142},[129,1390,1391],{},"    frequencia=('order_id', 'nunique'),\n",[129,1393,1394],{"class":131,"line":148},[129,1395,1396],{},"    valor_monetario=('payment_value', 'sum'),\n",[129,1398,1399],{"class":131,"line":154},[129,1400,712],{},[11,1402,1403,1404,1407],{},"95.560 clientes únicos. E o número que mais chamou atenção: ",[15,1405,1406],{},"só 2.924 clientes (3,06% do total) fizeram mais de um pedido",". A frequência média é 1,034, ou seja, a marketplace inteira é dominada por compra única. Isso muda completamente como pensar em segmentação no capítulo 5: não vai ter \"cliente fiel\" como categoria grande, o RFM aqui provavelmente separa mais por valor gasto e recência do que por frequência de compra.",[31,1409,527],{"id":526},[11,1411,1412],{},"Distância cliente-vendedor calculada (e um bug de merge real corrigido no processo), vazamento de dado demonstrado na prática (a diferença entre AUC 0,74 honesto e AUC 1,0 vazado é a lição mais concreta que dá pra dar sobre o assunto), e as features de cada cenário definidas com técnica apropriada pro tipo de problema. Sazonalidade se provou mais importante que distância pro atraso, atraso domina a nota de avaliação, peso domina o frete, e a base de clientes é majoritariamente de compra única. No próximo capítulo eu treino os modelos de verdade pra cada cenário, com tracking de experimento e comparação de métrica.",[532,1414,534],{},{"title":78,"searchDepth":79,"depth":79,"links":1416},[1417,1418,1419,1420,1421,1422],{"id":1051,"depth":79,"text":1052},{"id":1126,"depth":79,"text":1127},{"id":1215,"depth":79,"text":1216},{"id":1303,"depth":79,"text":1304},{"id":1367,"depth":79,"text":1368},{"id":526,"depth":79,"text":527},"Terceiro capítulo do case Olist: engenharia de feature e seleção pros quatro cenários, com uma demonstração ao vivo de vazamento de dado (um modelo com 100% de AUC que não serve pra nada) e uma descoberta real sobre sazonalidade.",{},"\u002Fpt\u002Fprojects\u002Folist-ecommerce\u002F03-features-e-selecao",{"title":1039,"description":1423},"pt\u002Fprojects\u002Folist-ecommerce\u002F03-features-e-selecao",[93,1429,1430],"feature-engineering","data-leakage","OtCRzHZzopXyFfeHmBNYS52rdEXk2SkORG7YuL1nHpM",{"id":1433,"title":1434,"body":1435,"cover":3,"date":543,"description":1864,"extension":83,"meta":1865,"navigation":85,"order":148,"path":1866,"project":547,"seo":1867,"status":89,"stem":1868,"tags":1869,"__hash__":1872},"projectChapters\u002Fpt\u002Fprojects\u002Folist-ecommerce\u002F04-treinamento-e-tracking.md","AUC Boa, F1 Zero: a Armadilha do Threshold Padrão",{"type":8,"value":1436,"toc":1857},[1437,1454,1458,1461,1547,1553,1573,1581,1584,1588,1591,1648,1652,1664,1668,1674,1727,1731,1734,1738,1750,1843,1849,1852,1854],[11,1438,1439,1440,1445,1446,1449,1450,76],{},"O capítulo 3 definiu as features de cada um dos quatro cenários. Agora é a hora de treinar os modelos de verdade, comparar contra baseline, e registrar cada tentativa com ",[23,1441,1444],{"href":1442,"rel":1443},"https:\u002F\u002Fmlflow.org\u002F",[27],"MLflow"," (parâmetro e métrica de cada run, só local, ",[107,1447,1448],{},"mlruns\u002F",", sem subir pra lugar nenhum, é boa prática de rastreabilidade, não vira gráfico aqui no blog). Notebook completo, executado, ",[23,1451,565],{"href":1452,"rel":1453},"https:\u002F\u002Fcolab.research.google.com\u002Fdrive\u002F1dJzFOogCIDK69Avoo4LcW1QUtYbVpsa-?usp=sharing",[27],[31,1455,1457],{"id":1456},"cenário-1-atraso-na-entrega-e-a-armadilha-do-threshold-padrão","Cenário 1: atraso na entrega, e a armadilha do threshold padrão",[11,1459,1460],{},"Baseline (sempre chuta \"no prazo\") → Regressão Logística → Random Forest → XGBoost, nas features honestas do capítulo 3 (95.968 pedidos, taxa real de atraso de 6,76%):",[232,1462,1463,1482],{},[235,1464,1465],{},[238,1466,1467,1470,1473,1476,1479],{},[241,1468,1469],{"align":243},"Modelo",[241,1471,1472],{"align":250},"AUC",[241,1474,1475],{"align":250},"F1",[241,1477,1478],{"align":250},"Precisão",[241,1480,1481],{"align":250},"Revocação",[256,1483,1484,1499,1516,1530],{},[238,1485,1486,1489,1492,1495,1497],{},[261,1487,1488],{"align":243},"Baseline",[261,1490,1491],{"align":250},"0,5000",[261,1493,1494],{"align":250},"0,0000",[261,1496,1494],{"align":250},[261,1498,1494],{"align":250},[238,1500,1501,1504,1507,1510,1513],{},[261,1502,1503],{"align":243},"Regressão Logística",[261,1505,1506],{"align":250},"0,6064",[261,1508,1509],{"align":250},"0,0031",[261,1511,1512],{"align":250},"1,0000",[261,1514,1515],{"align":250},"0,0015",[238,1517,1518,1521,1524,1526,1528],{},[261,1519,1520],{"align":243},"Random Forest",[261,1522,1523],{"align":250},"0,7388",[261,1525,1494],{"align":250},[261,1527,1494],{"align":250},[261,1529,1494],{"align":250},[238,1531,1532,1535,1538,1541,1544],{},[261,1533,1534],{"align":243},"XGBoost",[261,1536,1537],{"align":250},"0,7261",[261,1539,1540],{"align":250},"0,0268",[261,1542,1543],{"align":250},"0,4000",[261,1545,1546],{"align":250},"0,0139",[1548,1549],"olist-model-comparison-chart",{"color":1550,"scenario":1551,"x-label":1552,"y-label":1472},"#0033cc","delay","modelo",[11,1554,1555,1556,1559,1560,1563,1564,1568,1569,1572],{},"Repara numa coisa estranha nessa tabela: o Random Forest tem a ",[15,1557,1558],{},"melhor AUC de todo mundo (0,7388)",", mas F1, precisão e revocação ",[15,1561,1562],{},"zerados",". Não é bug, é a mecânica da coisa. AUC mede se o modelo consegue ",[1565,1566,1567],"em",{},"ranquear"," melhor os casos arriscados que os seguros, comparando pares de exemplo sem nunca decidir nada, e nisso o Random Forest manda bem. F1, precisão e revocação, em compensação, dependem de uma decisão binária: acima de 0,5 de probabilidade o modelo grita \"vai atrasar!\", abaixo ele fica calado. Com só 6,76% dos pedidos atrasando de verdade, o Random Forest aprendeu tão bem a reconhecer a classe majoritária que ",[15,1570,1571],{},"nunca"," cruza 0,5 de probabilidade pra nenhum pedido, nem pros que realmente atrasam. Ele sabe ranquear risco, só nunca \"aposta\" no positivo.",[11,1574,1575,1576,1580],{},"Precisão aqui é: dos pedidos que o modelo marcou como \"vai atrasar\", quantos atrasaram de verdade. Revocação é o oposto: dos pedidos que atrasaram de verdade, quantos o modelo pegou. F1 é a média harmônica dos dois, um jeito de resumir \"o modelo é bom nas duas pontas\" num só número. É a mesma tríade que eu já tinha usado lá na playlist Pattern Recognition, na ",[23,1577,1579],{"href":1578},"\u002Fplaylists\u002Fpattern-recognition\u002Fcredit-card-fraud","detecção de fraude em cartão de crédito",", outro problema de classe super desbalanceada, e o sintoma é parecido: métrica de ranking (AUC) parece ótima, métrica de decisão (F1) desmascara que o modelo, do jeito que está, não serve pra decidir nada sozinho.",[11,1582,1583],{},"O XGBoost, apesar da AUC um pouco menor (0,7261), é o único que realmente \"aposta\": F1 0,0268, precisão 0,40 (quando ele marca \"vai atrasar\", acerta em 4 de cada 10 vezes), revocação 0,0139 (mas só pega 1,4% dos atrasos reais). Nenhum dos quatro está pronto pra produção assim, do jeito que está publicado aqui. O próximo passo de verdade seria mexer no threshold de decisão em vez de usar 0,5 cego, ou balancear a classe no treino, mas isso é assunto pra outro dia, esse capítulo é sobre comparar modelo, não sobre ajustar limiar de decisão.",[31,1585,1587],{"id":1586},"cenário-2-nota-da-avaliação-regressão","Cenário 2: nota da avaliação (regressão)",[11,1589,1590],{},"Baseline (sempre chuta a média) → Regressão Linear → Random Forest → XGBoost, nas features do capítulo 3 (95.829 pedidos):",[232,1592,1593,1605],{},[235,1594,1595],{},[238,1596,1597,1599,1602],{},[241,1598,1469],{"align":243},[241,1600,1601],{"align":250},"R²",[241,1603,1604],{"align":250},"MAE",[256,1606,1607,1617,1628,1638],{},[238,1608,1609,1611,1614],{},[261,1610,1488],{"align":243},[261,1612,1613],{"align":250},"-0,0001",[261,1615,1616],{"align":250},"0,9950",[238,1618,1619,1622,1625],{},[261,1620,1621],{"align":243},"Regressão Linear",[261,1623,1624],{"align":250},"0,1232",[261,1626,1627],{"align":250},"0,9228",[238,1629,1630,1632,1635],{},[261,1631,1520],{"align":243},[261,1633,1634],{"align":250},"0,2028",[261,1636,1637],{"align":250},"0,8765",[238,1639,1640,1642,1645],{},[261,1641,1534],{"align":243},[261,1643,1644],{"align":250},"0,1861",[261,1646,1647],{"align":250},"0,8794",[1548,1649],{"color":1650,"scenario":1651,"x-label":1552,"y-label":1601},"#cc3300","review",[11,1653,1654,1655,1658,1659,201,1661,1663],{},"R² mede quanto da variação da nota o modelo explica (1,0 seria previsão perfeita, 0 seria \"chutar sempre a média\"). MAE é o erro médio em estrelas, quanto o modelo erra pra cima ou pra baixo, na média, comparado com a nota real. O Random Forest ganha aqui, de raspão, do XGBoost (0,2028 contra 0,1861), o oposto do que normalmente se espera desses dois. Mas o dado mais honesto é o teto baixo dos dois: ",[15,1656,1657],{},"nem o melhor modelo passa de 20% de variância explicada",". Isso bate com o que o capítulo 3 já tinha achado usando informação mútua, ",[107,1660,1157],{},[107,1662,1011],{}," dominam disparado a nota, e sobra pouca informação nas outras features (preço, frete, parcelas) pra um modelo qualquer explorar. A nota do cliente depende de um monte de coisa que não está nesse dataframe (qualidade real do produto, embalagem, atendimento), não é falta de modelo melhor, é falta de feature.",[31,1665,1667],{"id":1666},"cenário-3-valor-do-frete-regressão","Cenário 3: valor do frete (regressão)",[11,1669,1670,1671,1673],{},"Baseline → Regressão Linear → Random Forest → XGBoost, nas features físicas do produto mais a ",[107,1672,1133],{}," calculada no capítulo 3 (98.650 pedidos):",[232,1675,1676,1686],{},[235,1677,1678],{},[238,1679,1680,1682,1684],{},[241,1681,1469],{"align":243},[241,1683,1601],{"align":250},[241,1685,1604],{"align":250},[256,1687,1688,1697,1707,1717],{},[238,1689,1690,1692,1694],{},[261,1691,1488],{"align":243},[261,1693,1613],{"align":250},[261,1695,1696],{"align":250},"8,6057",[238,1698,1699,1701,1704],{},[261,1700,1621],{"align":243},[261,1702,1703],{"align":250},"0,5322",[261,1705,1706],{"align":250},"5,3800",[238,1708,1709,1711,1714],{},[261,1710,1520],{"align":243},[261,1712,1713],{"align":250},"0,6178",[261,1715,1716],{"align":250},"4,5947",[238,1718,1719,1721,1724],{},[261,1720,1534],{"align":243},[261,1722,1723],{"align":250},"0,6385",[261,1725,1726],{"align":250},"4,3112",[1548,1728],{"color":1729,"scenario":1730,"x-label":1552,"y-label":1601},"#33aa55","freight",[11,1732,1733],{},"Aqui sim o XGBoost ganha claro dos outros três, R² 0,6385 e erro médio de R$ 4,31. Faz sentido físico: transportadora calcula frete numa fórmula que mistura peso, dimensão e distância de um jeito não-linear (peso volumétrico, faixas de preço por distância), e árvore de decisão (Random Forest, XGBoost) capta esse tipo de não-linearidade muito melhor que uma reta de Regressão Linear, que já fica em segundo lugar mesmo assim (R² 0,53). Comparado ao teto baixo do cenário 2, esse aqui é o cenário onde \"mais dado numérico relevante\" realmente compra performance de modelo.",[31,1735,1737],{"id":1736},"cenário-4-segmentação-de-clientes-primeira-rodada-do-rfm","Cenário 4: segmentação de clientes, primeira rodada do RFM",[11,1739,1740,1741,1745,1746,1749],{},"Diferente dos três anteriores, aqui não tem \"modelo certo\" pra comparar, é ",[23,1742,1744],{"href":1743},"\u002Fplaylists\u002Fpattern-recognition\u002Fkmeans","K-means"," rodando pra vários valores de K (de 2 a 8) nas três variáveis RFM escalonadas (recência, frequência, valor monetário) do capítulo 3. Pra escolher o K, usei uma métrica diferente do método do cotovelo que o professor usou lá na playlist Pattern Recognition: o ",[15,1747,1748],{},"coeficiente de silhueta",". Ele mede, pra cada cliente, o quão parecido ele é do próprio grupo comparado ao grupo vizinho mais próximo, e a média de todo mundo vira o score do K inteiro. Fica entre -1 (cliente no grupo errado) e 1 (grupos compactos e bem separados).",[232,1751,1752,1765],{},[235,1753,1754],{},[238,1755,1756,1759,1762],{},[241,1757,1758],{"align":243},"K",[241,1760,1761],{"align":250},"Silhueta",[241,1763,1764],{"align":250},"Inércia",[256,1766,1767,1777,1788,1799,1810,1821,1832],{},[238,1768,1769,1771,1774],{},[261,1770,347],{"align":243},[261,1772,1773],{"align":250},"0,7430",[261,1775,1776],{"align":250},"207.275,6",[238,1778,1779,1782,1785],{},[261,1780,1781],{"align":243},"3",[261,1783,1784],{"align":250},"0,4569",[261,1786,1787],{"align":250},"141.952,7",[238,1789,1790,1793,1796],{},[261,1791,1792],{"align":243},"4",[261,1794,1795],{"align":250},"0,4900",[261,1797,1798],{"align":250},"94.963,3",[238,1800,1801,1804,1807],{},[261,1802,1803],{"align":243},"5",[261,1805,1806],{"align":250},"0,4195",[261,1808,1809],{"align":250},"79.940,5",[238,1811,1812,1815,1818],{},[261,1813,1814],{"align":243},"6",[261,1816,1817],{"align":250},"0,4387",[261,1819,1820],{"align":250},"65.668,3",[238,1822,1823,1826,1829],{},[261,1824,1825],{"align":243},"7",[261,1827,1828],{"align":250},"0,4418",[261,1830,1831],{"align":250},"56.032,0",[238,1833,1834,1837,1840],{},[261,1835,1836],{"align":243},"8",[261,1838,1839],{"align":250},"0,4495",[261,1841,1842],{"align":250},"50.353,9",[1548,1844],{"color":1845,"scenario":1846,"x-label":1847,"y-label":1848},"#8855aa","rfm","k","silhueta",[11,1850,1851],{},"K=2 disparado na frente (0,743, quase o dobro de qualquer outro K), e a inércia cai suave e continuamente sem cotovelo óbvio nenhum, dois sinais concordando: o corte mais \"limpo\" estatisticamente é dividir a base em dois grupos só. Só que eu já sei, do capítulo 3, o que esse corte provavelmente É: os 3,06% de clientes que compraram mais de uma vez contra os 96,94% de compra única. Estatisticamente é o K mais \"puro\", mas de negócio é quase a mesma informação que eu já tinha sem rodar clustering nenhum. Do K=3 pra frente a silhueta oscila meio sem padrão claro (entre 0,42 e 0,49), sem um segundo K que se destaque com folga. Vou levar essa tensão (K estatisticamente ótimo vs. segmentação rica o suficiente pra virar ação de negócio) pro capítulo 5, que é onde eu de fato escolho um K final, visualizo os grupos e interpreto o que cada um significa.",[31,1853,527],{"id":526},[11,1855,1856],{},"Os quatro modelos treinados e comparados contra baseline, com MLflow registrando cada tentativa. O achado mais importante não foi \"qual modelo ganhou\", foi a demonstração de que AUC boa não garante decisão boa: o Random Forest do cenário 1 tem a melhor métrica de ranking e zero utilidade prática no threshold padrão, uma armadilha real de quem só olha uma métrica e já sai comemorando. Frete é o cenário onde o modelo mais \"compra\" performance real (XGBoost, R² 0,64), nota de avaliação esbarra num teto baixo de informação disponível, e RFM levanta uma pergunta em aberto sobre o que significa \"melhor\" clusterização, que fica pro capítulo 5, junto com SHAP pra abrir a caixa-preta desses modelos e as conclusões finais do projeto.",{"title":78,"searchDepth":79,"depth":79,"links":1858},[1859,1860,1861,1862,1863],{"id":1456,"depth":79,"text":1457},{"id":1586,"depth":79,"text":1587},{"id":1666,"depth":79,"text":1667},{"id":1736,"depth":79,"text":1737},{"id":526,"depth":79,"text":527},"Quarto capítulo do case Olist: treino os quatro modelos de verdade (atraso, nota, frete, RFM) com tracking via MLflow, e pego um Random Forest com AUC 0,74 que não acerta um único caso positivo no threshold padrão.",{},"\u002Fpt\u002Fprojects\u002Folist-ecommerce\u002F04-treinamento-e-tracking",{"title":1434,"description":1864},"pt\u002Fprojects\u002Folist-ecommerce\u002F04-treinamento-e-tracking",[93,94,1870,1871],"mlflow","model-evaluation","cqM7b_ifQiak2YXdul8YY8l8i8w-6gtcfo-0m7aEmBo",1787605213591]