[{"data":1,"prerenderedAt":476},["ShallowReactive",2],{"lang-switch-post-\u002Fprojects\u002Folist-ecommerce\u002F01-intro-relational-model-eda":3,"chapter-pt-olist-ecommerce-01-intro-relational-model-eda":4},null,{"id":5,"title":6,"body":7,"cover":3,"date":463,"description":464,"extension":465,"meta":466,"navigation":306,"order":45,"path":467,"project":468,"seo":469,"status":470,"stem":471,"tags":472,"__hash__":475},"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":9,"toc":455},"minimark",[10,27,32,97,100,140,144,147,267,272,276,292,355,374,378,381,427,431,434,437,444,448,451],[11,12,13,14,18,19,26],"p",{},"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 ",[15,16,17],"code",{},"01_intro_eda.ipynb",", dentro do próprio diretório do dataset, e deixei ele público no Colab: ",[20,21,25],"a",{"href":22,"rel":23},"https:\u002F\u002Fcolab.research.google.com\u002Fdrive\u002F1kQvo5YjlLPiODqGhlAGZNk84ikg-PZg7?usp=sharing",[24],"nofollow","confere o notebook completo aqui",".",[28,29,31],"h2",{"id":30},"o-modelo-relacional","O modelo relacional",[33,34,39],"pre",{"className":35,"code":36,"language":37,"meta":38,"style":38},"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","",[15,40,41,49,55,61,67,73,79,85,91],{"__ignoreMap":38},[42,43,46],"span",{"class":44,"line":45},"line",1,[42,47,48],{},"orders = pd.read_csv('olist_orders_dataset.csv')\n",[42,50,52],{"class":44,"line":51},2,[42,53,54],{},"order_items = pd.read_csv('olist_order_items_dataset.csv')\n",[42,56,58],{"class":44,"line":57},3,[42,59,60],{},"payments = pd.read_csv('olist_order_payments_dataset.csv')\n",[42,62,64],{"class":44,"line":63},4,[42,65,66],{},"reviews = pd.read_csv('olist_order_reviews_dataset.csv')\n",[42,68,70],{"class":44,"line":69},5,[42,71,72],{},"customers = pd.read_csv('olist_customers_dataset.csv')\n",[42,74,76],{"class":44,"line":75},6,[42,77,78],{},"sellers = pd.read_csv('olist_sellers_dataset.csv')\n",[42,80,82],{"class":44,"line":81},7,[42,83,84],{},"products = pd.read_csv('olist_products_dataset.csv')\n",[42,86,88],{"class":44,"line":87},8,[42,89,90],{},"geolocation = pd.read_csv('olist_geolocation_dataset.csv')\n",[42,92,94],{"class":44,"line":93},9,[42,95,96],{},"category_translation = pd.read_csv('product_category_name_translation.csv')\n",[98,99],"olist-relational-model",{},[11,101,102,105,106,109,110,109,113,116,117,120,121,109,124,109,127,130,131,134,135,139],{},[15,103,104],{},"order_id"," é a chave que costura quase tudo: aparece em ",[15,107,108],{},"orders",", ",[15,111,112],{},"order_items",[15,114,115],{},"payments"," e ",[15,118,119],{},"reviews",". ",[15,122,123],{},"customer_id",[15,125,126],{},"product_id",[15,128,129],{},"seller_id"," e o prefixo de CEP fecham o resto das ligações. Um detalhe que já vale marcar: ",[15,132,133],{},"olist_order_reviews_dataset.csv"," tem 104.719 linhas de texto bruto no arquivo, mas o pandas só reconhece ",[136,137,138],"strong",{},"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.",[28,141,143],{"id":142},"qualidade-do-dado-nulo-tem-história","Qualidade do dado: nulo tem história",[11,145,146],{},"Nem todo nulo é problema, às vezes ele é informação. Nas tabelas onde nulo aparece:",[148,149,150,171],"table",{},[151,152,153],"thead",{},[154,155,156,161,164,168],"tr",{},[157,158,160],"th",{"align":159},"left","Tabela",[157,162,163],{"align":159},"Coluna",[157,165,167],{"align":166},"right","Nulos",[157,169,170],{"align":166},"%",[172,173,174,188,201,214,227,240,254],"tbody",{},[154,175,176,179,182,185],{},[177,178,108],"td",{"align":159},[177,180,181],{"align":159},"order_approved_at",[177,183,184],{"align":166},"160",[177,186,187],{"align":166},"0,2%",[154,189,190,192,195,198],{},[177,191,108],{"align":159},[177,193,194],{"align":159},"order_delivered_carrier_date",[177,196,197],{"align":166},"1.783",[177,199,200],{"align":166},"1,8%",[154,202,203,205,208,211],{},[177,204,108],{"align":159},[177,206,207],{"align":159},"order_delivered_customer_date",[177,209,210],{"align":166},"2.965",[177,212,213],{"align":166},"3,0%",[154,215,216,218,221,224],{},[177,217,119],{"align":159},[177,219,220],{"align":159},"review_comment_title",[177,222,223],{"align":166},"87.656",[177,225,226],{"align":166},"88,3%",[154,228,229,231,234,237],{},[177,230,119],{"align":159},[177,232,233],{"align":159},"review_comment_message",[177,235,236],{"align":166},"58.247",[177,238,239],{"align":166},"58,7%",[154,241,242,245,248,251],{},[177,243,244],{"align":159},"products",[177,246,247],{"align":159},"product_category_name (+ 3 outras colunas de produto)",[177,249,250],{"align":166},"610",[177,252,253],{"align":166},"1,9%",[154,255,256,258,261,264],{},[177,257,244],{"align":159},[177,259,260],{"align":159},"peso\u002Fdimensões (4 colunas)",[177,262,263],{"align":166},"2",[177,265,266],{"align":166},"0,0%",[11,268,269,271],{},[15,270,207],{}," 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.",[28,273,275],{"id":274},"montando-o-dataframe-mestre","Montando o dataframe mestre",[11,277,278,279,282,283,285,286,288,289,291],{},"A granularidade natural do dataset é ",[136,280,281],{},"item de pedido",", não pedido inteiro: um ",[15,284,104],{}," pode ter vários itens, de vendedores diferentes, cada um com seu próprio preço e frete. Por isso o merge parte de ",[15,287,112],{},", não de ",[15,290,108],{},":",[33,293,295],{"className":35,"code":294,"language":37,"meta":38,"style":38},"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",[15,296,297,302,308,313,318,323,328,333,338,343,349],{"__ignoreMap":38},[42,298,299],{"class":44,"line":45},[42,300,301],{},"produtos_com_categoria_en = products.merge(category_translation, on='product_category_name', how='left')\n",[42,303,304],{"class":44,"line":51},[42,305,307],{"emptyLinePlaceholder":306},true,"\n",[42,309,310],{"class":44,"line":57},[42,311,312],{},"mestre = (\n",[42,314,315],{"class":44,"line":63},[42,316,317],{},"    order_items\n",[42,319,320],{"class":44,"line":69},[42,321,322],{},"    .merge(orders, on='order_id', how='left')\n",[42,324,325],{"class":44,"line":75},[42,326,327],{},"    .merge(customers, on='customer_id', how='left')\n",[42,329,330],{"class":44,"line":81},[42,331,332],{},"    .merge(produtos_com_categoria_en, on='product_id', how='left')\n",[42,334,335],{"class":44,"line":87},[42,336,337],{},"    .merge(sellers, on='seller_id', how='left')\n",[42,339,340],{"class":44,"line":93},[42,341,342],{},"    .merge(payments, on='order_id', how='left')\n",[42,344,346],{"class":44,"line":345},10,[42,347,348],{},"    .merge(reviews, on='order_id', how='left')\n",[42,350,352],{"class":44,"line":351},11,[42,353,354],{},")\n",[11,356,357,359,360,363,364,366,367,369,370,373],{},[15,358,112],{}," sozinho tem 112.650 linhas. Depois de todos os merges, o dataframe mestre tem ",[136,361,362],{},"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 ",[15,365,115],{},", ou recebe mais de uma avaliação em ",[15,368,119],{},", 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 ",[15,371,372],{},"shape"," antes e depois, passa despercebido e infla contagem em qualquer agregação futura.",[28,375,377],{"id":376},"os-quatro-cenários-de-ml","Os quatro cenários de ML",[11,379,380],{},"Com o dataframe mestre em mãos, definido nos capítulos seguintes:",[382,383,384,397,407,421],"ol",{},[385,386,387,390,391,393,394,26],"li",{},[136,388,389],{},"Atraso na entrega"," (classificação binária): comparar ",[15,392,207],{}," com ",[15,395,396],{},"order_estimated_delivery_date",[385,398,399,402,403,406],{},[136,400,401],{},"Nota da avaliação"," (classificação multiclasse ou regressão): ",[15,404,405],{},"review_score",", de 1 a 5.",[385,408,409,412,413,416,417,420],{},[136,410,411],{},"Valor do frete ou do pedido"," (regressão): ",[15,414,415],{},"freight_value",", ou a soma de ",[15,418,419],{},"price"," por pedido.",[385,422,423,426],{},[136,424,425],{},"Segmentação de clientes"," (clustering, sem target): Recência, Frequência e Valor monetário por cliente, técnica RFM.",[28,428,430],{"id":429},"pedidos-por-mês-o-pico-de-black-friday","Pedidos por mês: o pico de Black Friday",[11,432,433],{},"Uma primeira olhada temporal, contando pedido único por mês de compra:",[435,436],"olist-orders-per-month-chart",{},[11,438,439,440,443],{},"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 ",[136,441,442],{},"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.",[28,445,447],{"id":446},"fechando-o-capítulo","Fechando o capítulo",[11,449,450],{},"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.",[452,453,454],"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":38,"searchDepth":51,"depth":51,"links":456},[457,458,459,460,461,462],{"id":30,"depth":51,"text":31},{"id":142,"depth":51,"text":143},{"id":274,"depth":51,"text":275},{"id":376,"depth":51,"text":377},{"id":429,"depth":51,"text":430},{"id":446,"depth":51,"text":447},"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.","md",{},"\u002Fpt\u002Fprojects\u002Folist-ecommerce\u002F01-intro-relational-model-eda","olist-ecommerce",{"title":6,"description":464},"published","pt\u002Fprojects\u002Folist-ecommerce\u002F01-intro-relational-model-eda",[473,474],"pandas","eda","058fQ3ZIuDZk4VYmbtciafzHMkPtF5IyFJqEWIwsaXs",1787605215704]