[{"data":1,"prerenderedAt":538},["ShallowReactive",2],{"lang-switch-post-\u002Fen\u002Fprojects\u002Folist-ecommerce\u002F02-visualizacoes-exploratorias":3,"chapter-en-olist-ecommerce-02-visualizacoes-exploratorias":4},null,{"id":5,"title":6,"body":7,"cover":3,"date":524,"description":525,"extension":526,"meta":527,"navigation":62,"order":47,"path":528,"project":529,"seo":530,"status":531,"stem":532,"tags":533,"__hash__":537},"projectChapters\u002Fen\u002Fprojects\u002Folist-ecommerce\u002F02-visualizacoes-exploratorias.md","Brazil Shops on Weekdays: Olist's Exploratory Storytelling",{"type":8,"value":9,"toc":515},"minimark",[10,22,27,106,111,114,118,193,196,204,208,256,261,275,279,321,324,331,335,373,378,381,384,431,436,443,447,485,489,504,508,511],[11,12,13,14,21],"p",{},"Chapter 1 mapped the relational model and built the master dataframe. Now I let the data speak visually, six different angles on the same business, before training a single model. Full executed notebook, ",[15,16,20],"a",{"href":17,"rel":18},"https:\u002F\u002Fcolab.research.google.com\u002Fdrive\u002F1PrCuoWoAnb5paNi5dxzbSlxAGrcyA_mk?usp=sharing",[19],"nofollow","public on Colab",".",[23,24,26],"h2",{"id":25},"temporal-brazil-shops-on-weekdays","Temporal: Brazil shops on weekdays",[28,29,34],"pre",{"className":30,"code":31,"language":32,"meta":33,"style":33},"language-python shiki shiki-themes github-light github-dark","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","python","",[35,36,37,45,51,57,64,70,76,82,88,94,100],"code",{"__ignoreMap":33},[38,39,42],"span",{"class":40,"line":41},"line",1,[38,43,44],{},"orders_com_hora = orders.dropna(subset=['order_purchase_timestamp']).copy()\n",[38,46,48],{"class":40,"line":47},2,[38,49,50],{},"orders_com_hora['weekday'] = orders_com_hora['order_purchase_timestamp'].dt.weekday\n",[38,52,54],{"class":40,"line":53},3,[38,55,56],{},"orders_com_hora['hour'] = orders_com_hora['order_purchase_timestamp'].dt.hour\n",[38,58,60],{"class":40,"line":59},4,[38,61,63],{"emptyLinePlaceholder":62},true,"\n",[38,65,67],{"class":40,"line":66},5,[38,68,69],{},"weekday_hour = (\n",[38,71,73],{"class":40,"line":72},6,[38,74,75],{},"    orders_com_hora\n",[38,77,79],{"class":40,"line":78},7,[38,80,81],{},"    .groupby(['weekday', 'hour'])['order_id']\n",[38,83,85],{"class":40,"line":84},8,[38,86,87],{},"    .nunique()\n",[38,89,91],{"class":40,"line":90},9,[38,92,93],{},"    .reset_index()\n",[38,95,97],{"class":40,"line":96},10,[38,98,99],{},"    .rename(columns={'order_id': 'orders'})\n",[38,101,103],{"class":40,"line":102},11,[38,104,105],{},")\n",[107,108],"olist-weekday-hour-heatmap",{":weekday-labels":109,"y-label":110},"[\"Mon\",\"Tue\",\"Wed\",\"Thu\",\"Fri\",\"Sat\",\"Sun\"]","day of week",[11,112,113],{},"168 cells (7 days × 24 hours), the sum matches the total order count exactly, always worth checking before trusting the chart. The peak is Tuesday at 2pm, with 1,124 orders in a single hour. Looking at totals by weekday, Monday through Friday each land between 14 and 16 thousand orders, while Saturday drops to 10,887 and Sunday to 11,960, almost a 30% dip. By hour, lunchtime and early afternoon (11am, 1pm-4pm) hold the bulk of activity, and the early morning hours (3am-5am) practically flatline, under 300 orders in each of those hours across the entire dataset. That's the classic \"shopping from work or on a break\" pattern, not late-night leisure browsing.",[23,115,117],{"id":116},"geographic-the-north-waits-longer","Geographic: the North waits longer",[28,119,121],{"className":30,"code":120,"language":32,"meta":33,"style":33},"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",[35,122,123,128,132,137,142,146,151,156,161,165,170,175,181,187],{"__ignoreMap":33},[38,124,125],{"class":40,"line":41},[38,126,127],{},"orders_com_cliente = orders.merge(customers, on='customer_id', how='left')\n",[38,129,130],{"class":40,"line":47},[38,131,63],{"emptyLinePlaceholder":62},[38,133,134],{"class":40,"line":53},[38,135,136],{},"frete_por_pedido = order_items.groupby('order_id')['freight_value'].sum().reset_index()\n",[38,138,139],{"class":40,"line":59},[38,140,141],{},"orders_com_cliente = orders_com_cliente.merge(frete_por_pedido, on='order_id', how='left')\n",[38,143,144],{"class":40,"line":66},[38,145,63],{"emptyLinePlaceholder":62},[38,147,148],{"class":40,"line":72},[38,149,150],{},"orders_com_cliente['delivery_days'] = (\n",[38,152,153],{"class":40,"line":78},[38,154,155],{},"    orders_com_cliente['order_delivered_customer_date'] - orders_com_cliente['order_purchase_timestamp']\n",[38,157,158],{"class":40,"line":84},[38,159,160],{},").dt.days\n",[38,162,163],{"class":40,"line":90},[38,164,63],{"emptyLinePlaceholder":62},[38,166,167],{"class":40,"line":96},[38,168,169],{},"por_estado = orders_com_cliente.groupby('customer_state').agg(\n",[38,171,172],{"class":40,"line":102},[38,173,174],{},"    orders=('order_id', 'nunique'),\n",[38,176,178],{"class":40,"line":177},12,[38,179,180],{},"    avg_freight=('freight_value', 'mean'),\n",[38,182,184],{"class":40,"line":183},13,[38,185,186],{},"    avg_delivery_days=('delivery_days', 'mean'),\n",[38,188,190],{"class":40,"line":189},14,[38,191,192],{},").reset_index()\n",[194,195],"olist-state-choropleth",{},[11,197,198,199,203],{},"The map shows real average delivery time (purchase to delivery) by customer state. ",[200,201,202],"strong",{},"Roraima leads with almost 29 days of average wait"," (28.98), followed by Amapá (26.73), Amazonas (25.99), and Alagoas (24.04). It's no coincidence those states top the list: Roraima, Amapá, and Amazonas are three of the states farthest from the Southeast's industrial and logistics hub, where most Olist sellers are concentrated. São Paulo, for comparison, sits much closer to the fast end of this distribution. That distance-to-delivery-time relationship is a natural candidate for a strong feature once I build the delay prediction scenario.",[23,205,207],{"id":206},"category-health-and-beauty-leads-revenue","Category: health and beauty leads revenue",[28,209,211],{"className":30,"code":210,"language":32,"meta":33,"style":33},"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",[35,212,213,218,223,228,233,238,243,248,252],{"__ignoreMap":33},[38,214,215],{"class":40,"line":41},[38,216,217],{},"top_categorias = (\n",[38,219,220],{"class":40,"line":47},[38,221,222],{},"    mestre\n",[38,224,225],{"class":40,"line":53},[38,226,227],{},"    .dropna(subset=['product_category_name_english'])\n",[38,229,230],{"class":40,"line":59},[38,231,232],{},"    .groupby('product_category_name_english')['price']\n",[38,234,235],{"class":40,"line":66},[38,236,237],{},"    .sum()\n",[38,239,240],{"class":40,"line":72},[38,241,242],{},"    .sort_values(ascending=False)\n",[38,244,245],{"class":40,"line":78},[38,246,247],{},"    .head(15)\n",[38,249,250],{"class":40,"line":84},[38,251,93],{},[38,253,254],{"class":40,"line":90},[38,255,105],{},[257,258],"olist-top-categories-chart",{"x-label":259,"y-label":260},"category","revenue (US$)",[11,262,263,266,267,270,271,274],{},[35,264,265],{},"health_beauty"," leads with 1,301,947.97 dollars in combined revenue, closely followed by ",[35,268,269],{},"watches_gifts"," (1,254,322.95) and ",[35,272,273],{},"bed_bath_table"," (1,107,249.09). This is revenue, not freight or item count, so a category can lead either by selling expensive items (watches) or by selling a lot of them (bed\u002Fbath\u002Ftable items get replaced often).",[23,276,278],{"id":277},"payment-credit-card-dominates","Payment: credit card dominates",[28,280,282],{"className":30,"code":281,"language":32,"meta":33,"style":33},"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",[35,283,284,289,294,299,303,307,312,317],{"__ignoreMap":33},[38,285,286],{"class":40,"line":41},[38,287,288],{},"tipos_pagamento = (\n",[38,290,291],{"class":40,"line":47},[38,292,293],{},"    payments[payments['payment_type'] != 'not_defined']\n",[38,295,296],{"class":40,"line":53},[38,297,298],{},"    .groupby('payment_type')['order_id']\n",[38,300,301],{"class":40,"line":59},[38,302,87],{},[38,304,305],{"class":40,"line":66},[38,306,93],{},[38,308,309],{"class":40,"line":72},[38,310,311],{},"    .rename(columns={'order_id': 'count'})\n",[38,313,314],{"class":40,"line":78},[38,315,316],{},"    .sort_values('count', ascending=False)\n",[38,318,319],{"class":40,"line":84},[38,320,105],{},[322,323],"olist-payment-types-chart",{},[11,325,326,327,330],{},"Credit card dominates by a wide margin: 76,505 orders, over 3 times the runner-up (boleto, a Brazilian bank-slip payment method, 19,784). Voucher comes in at 3,866 and debit card at only 1,528. I dropped the ",[35,328,329],{},"not_defined"," category from the chart, it only had 3 rows out of over 100 thousand payments, pure noise, not worth a pie slice.",[23,332,334],{"id":333},"satisfaction-most-love-it-but-the-haters-are-vocal","Satisfaction: most love it, but the haters are vocal",[28,336,338],{"className":30,"code":337,"language":32,"meta":33,"style":33},"distribuicao_notas = (\n    reviews\n    .groupby('review_score')['review_id']\n    .count()\n    .reset_index()\n    .rename(columns={'review_id': 'count'})\n)\n",[35,339,340,345,350,355,360,364,369],{"__ignoreMap":33},[38,341,342],{"class":40,"line":41},[38,343,344],{},"distribuicao_notas = (\n",[38,346,347],{"class":40,"line":47},[38,348,349],{},"    reviews\n",[38,351,352],{"class":40,"line":53},[38,353,354],{},"    .groupby('review_score')['review_id']\n",[38,356,357],{"class":40,"line":59},[38,358,359],{},"    .count()\n",[38,361,362],{"class":40,"line":66},[38,363,93],{},[38,365,366],{"class":40,"line":72},[38,367,368],{},"    .rename(columns={'review_id': 'count'})\n",[38,370,371],{"class":40,"line":78},[38,372,105],{},[374,375],"olist-review-score-chart",{"x-label":376,"y-label":377},"score","number of reviews",[11,379,380],{},"57,328 five-star reviews, over half the total, with four-star coming in second at 19,142. But look at third place: one-star reviews, at 11,424, alone outnumber two-star (3,151) and three-star (8,179) combined. That's the classic e-commerce review pattern: a satisfied customer sometimes doesn't bother reviewing at all, a neutral customer almost never does, and a very unhappy customer always shows up to vent. A distribution shaped like this (a big peak at 5, a smaller second peak at 1) is already a warning for when I build the score prediction scenario later: it's not a well-behaved continuous scale, it's closer to a \"loved it or hated it\" decision with a rare middle ground.",[11,382,383],{},"Alongside that, a real sample (400 orders) crossing delivery delay against the score given:",[28,385,387],{"className":30,"code":386,"language":32,"meta":33,"style":33},"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",[35,388,389,394,399,404,409,413,417,422,427],{"__ignoreMap":33},[38,390,391],{"class":40,"line":41},[38,392,393],{},"nota_atraso = (\n",[38,395,396],{"class":40,"line":47},[38,397,398],{},"    orders[['order_id', 'order_delivered_customer_date', 'order_estimated_delivery_date']]\n",[38,400,401],{"class":40,"line":53},[38,402,403],{},"    .merge(reviews[['order_id', 'review_score']], on='order_id', how='inner')\n",[38,405,406],{"class":40,"line":59},[38,407,408],{},"    .dropna(subset=['order_delivered_customer_date', 'order_estimated_delivery_date', 'review_score'])\n",[38,410,411],{"class":40,"line":66},[38,412,105],{},[38,414,415],{"class":40,"line":72},[38,416,63],{"emptyLinePlaceholder":62},[38,418,419],{"class":40,"line":78},[38,420,421],{},"nota_atraso['delay_days'] = (\n",[38,423,424],{"class":40,"line":84},[38,425,426],{},"    nota_atraso['order_delivered_customer_date'] - nota_atraso['order_estimated_delivery_date']\n",[38,428,429],{"class":40,"line":90},[38,430,160],{},[432,433],"olist-review-delay-scatter",{"x-label":434,"y-label":435},"delay (days, negative = early)","review score",[11,437,438,439,442],{},"A negative ",[35,440,441],{},"delay_days"," means the order arrived early, positive means a real delay. Notice how the one-star points (the bottom row) spread across the whole axis, including into negative territory, but with a visibly heavier concentration on the delay side (right) than the five-star points show.",[23,444,446],{"id":445},"correlation-the-punchline-before-the-model","Correlation: the punchline before the model",[28,448,450],{"className":30,"code":449,"language":32,"meta":33,"style":33},"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",[35,451,452,457,462,467,471,476,480],{"__ignoreMap":33},[38,453,454],{"class":40,"line":41},[38,455,456],{},"features_numericas = mestre[['price', 'freight_value', 'product_weight_g', 'payment_value', 'review_score']].copy()\n",[38,458,459],{"class":40,"line":47},[38,460,461],{},"features_numericas['delivery_days'] = (\n",[38,463,464],{"class":40,"line":53},[38,465,466],{},"    mestre['order_delivered_customer_date'] - mestre['order_purchase_timestamp']\n",[38,468,469],{"class":40,"line":59},[38,470,160],{},[38,472,473],{"class":40,"line":66},[38,474,475],{},"features_numericas = features_numericas.dropna()\n",[38,477,478],{"class":40,"line":72},[38,479,63],{"emptyLinePlaceholder":62},[38,481,482],{"class":40,"line":78},[38,483,484],{},"matriz_corr = features_numericas.corr()\n",[486,487],"olist-correlation-heatmap",{":feature-labels":488},"{\"price\":\"price\",\"freight_value\":\"freight\",\"product_weight_g\":\"weight\",\"payment_value\":\"payment\",\"review_score\":\"review score\",\"delivery_days\":\"delivery days\"}",[11,490,491,492,495,496,499,500,503],{},"Here's the number I promised back in Chapter 1's closing: ",[35,493,494],{},"review_score"," correlates at ",[200,497,498],{},"-0.30"," with ",[35,501,502],{},"delivery_days",", computed over 114,838 rows of the master dataframe. That's the strongest relationship any numeric feature has with the review score (price, freight, and product weight all sit below 0.08 in absolute value). It's not a huge correlation (nowhere near -1), but it's clearly the standout of the group, and it lines up exactly with what the geographic map already hinted at: slow delivery looks like the single factor most associated with dragging customer satisfaction down, more than what the product cost or weighed.",[23,505,507],{"id":506},"wrapping-up-the-chapter","Wrapping up the chapter",[11,509,510],{},"Six angles, one pattern emerging: delivery time shows up three separate times (map, scatter, correlation) as the strongest thread for explaining dissatisfaction. Credit card dominates payment, health and beauty dominates revenue, Tuesday afternoon is peak shopping time. Next chapter I move into feature engineering and selection for the four scenarios defined in Chapter 1, and the data-leakage discussion for the delay scenario is going to land a lot more concretely with these geographic and correlation numbers already in hand.",[512,513,514],"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":516},[517,518,519,520,521,522,523],{"id":25,"depth":47,"text":26},{"id":116,"depth":47,"text":117},{"id":206,"depth":47,"text":207},{"id":277,"depth":47,"text":278},{"id":333,"depth":47,"text":334},{"id":445,"depth":47,"text":446},{"id":506,"depth":47,"text":507},"2026-08-24","Second chapter of the Olist case study: a purchase-time heatmap, a delay choropleth by state, top revenue categories, payment method, and the correlation that delivers the punchline before any model gets trained: delay kills the review score.","md",{},"\u002Fen\u002Fprojects\u002Folist-ecommerce\u002F02-visualizacoes-exploratorias","olist-ecommerce",{"title":6,"description":525},"published","en\u002Fprojects\u002Folist-ecommerce\u002F02-visualizacoes-exploratorias",[534,535,536],"pandas","eda","visualization","BnrLxILdDrkWpuFL2BtwKuLZUU_h4b6ecFEa5Q28zb4",1787605216022]