<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Phelipe Müller]]></title><description><![CDATA[Phelipe Müller]]></description><link>https://phelipemuller.com.br</link><generator>RSS for Node</generator><lastBuildDate>Fri, 17 Apr 2026 21:27:16 GMT</lastBuildDate><atom:link href="https://phelipemuller.com.br/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[A lib que o seu time de dados precisa (e que você pode construir)]]></title><description><![CDATA[Era uma reunião de rotina com o cliente. Daquelas de alinhamento mensal, sem pauta específica. Até que, quase de passagem, ele comentou: "Ah, e aquele modelo de previsão de demanda — a gente parou de ]]></description><link>https://phelipemuller.com.br/mlops-lib</link><guid isPermaLink="true">https://phelipemuller.com.br/mlops-lib</guid><category><![CDATA[mlops]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[Python]]></category><category><![CDATA[mlflow]]></category><category><![CDATA[ML ENGINEERING]]></category><category><![CDATA[scikit learn]]></category><dc:creator><![CDATA[Phelipe Müller]]></dc:creator><pubDate>Fri, 10 Apr 2026 15:19:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69231b6801313a5627cdb0cc/282ea520-654b-43a6-b156-7cbfbaad229b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Era uma reunião de rotina com o cliente. Daquelas de alinhamento mensal, sem pauta específica. Até que, quase de passagem, ele comentou: <em>"Ah, e aquele modelo de previsão de demanda — a gente parou de usar faz uns dois meses. Parou de funcionar e a gente foi ajustando na mão mesmo."</em></p>
<p>Dois meses.</p>
<p>O modelo havia quebrado dois meses atrás e ninguém no time tinha descoberto antes daquele café virtual. Nenhum alerta, nenhuma métrica de observabilidade, nenhuma notificação. O cliente simplesmente tinha absorvido o problema e seguido em frente.</p>
<p>Esse episódio foi o gatilho. Éramos um time com cerca de uma dezena de cientistas de dados, nenhum engenheiro de ML dedicado, e estávamos em pleno processo de profissionalizar a operação — sair do modo <em>"entrega modelo e abandona"</em> e começar a ter posse real do que colocávamos em produção. Depois daquela conversa, ficou claro que precisávamos de infraestrutura. E que essa infraestrutura precisava ser construída por nós.</p>
<p>O que veio depois foi uma lib interna. Este artigo é a história de como ela nasceu, o que ela faz, e — mais importante — uma proposta aberta para você fazer o mesmo no seu ambiente.</p>
<hr />
<h2>O custo do caos silencioso</h2>
<p>Antes da lib, o cenário era esse:</p>
<p>Cada cientista tinha o seu jeito de salvar modelo. Um usava <code>pickle</code>, outro <code>joblib</code>, um terceiro tinha inventado um esquema com JSON + arquivo de configuração separado. Ninguém sabia ao certo o que estava em produção, quais hiperparâmetros tinham sido usados, qual versão do dado havia treinado aquele modelo.</p>
<p>Quando um modelo dava problema, o único caminho era chamar o cientista original. Que tinha mais três projetos rodando em paralelo. Que precisava parar tudo, reabrir o notebook, tentar lembrar o que havia feito seis meses atrás.</p>
<p>Os melhores cientistas do time — os mais produtivos, os que tocavam mais projetos — eram exatamente os que mais travavam a operação quando algo quebrava. Porque tudo dependia da memória deles.</p>
<p>Não era falta de competência técnica. Era falta de padronização. E padronização não acontece por boa vontade — acontece quando você remove a fricção de fazer a coisa certa e aumenta a fricção de fazer a coisa errada.</p>
<hr />
<h2>Por que pedir para todo mundo aprender MLOps não resolve</h2>
<p>A solução óbvia seria: <em>"vamos treinar o time em MLOps, ensinar a usar MLflow direito, definir um padrão de salvamento de modelo e criar uma documentação."</em></p>
<p>Isso não funciona. Ou pelo menos não funciona sozinho.</p>
<p>Primeiro, porque documentação não é enforcement. As pessoas vão seguir o padrão nos primeiros dois sprints e depois vão cair no velho jeito quando o prazo apertar. Segundo, porque você está pedindo para o cientista de dados — cuja especialidade é modelagem — se tornar fluente em ferramentas de engenharia de ML que não são o core do trabalho dele. Isso é custo cognitivo desnecessário.</p>
<p>O cientista precisa estar focado no problema de negócio, na feature engineering, na escolha do algoritmo. Não em qual método usar para serializar o modelo ou como registrar um experimento no MLflow.</p>
<p>A solução que faz sentido é inversa: você constrói a infraestrutura de MLOps de forma que ela seja invisível para o cientista. Ele continua fazendo o que sempre fez. A lib faz o resto.</p>
<hr />
<h2>A abordagem: um wrapper que carrega o MLOps junto</h2>
<p>A ideia central é simples: você cria um wrapper em torno dos modelos do scikit-learn (e do ecossistema compatível — XGBoost, LightGBM, CatBoost) que intercepta os momentos-chave do ciclo de vida do modelo e executa automaticamente as práticas que o time definiu.</p>
<p>Na prática, o código do cientista fica assim:</p>
<pre><code class="language-python">from sua_lib import Model

# Instancia o modelo — por dentro, já abre um experimento no MLflow
model = Model(XGBClassifier(n_estimators=100, max_depth=4))

# Treina — por dentro, loga hiperparâmetros, métricas e salva o modelo versionado
model.fit(X_train, y_train)

# Predict — igual a qualquer modelo sklearn
predictions = model.predict(X_test)
</code></pre>
<p>O que acontece por baixo em cada etapa:</p>
<p><strong>Na instanciação:</strong> um experimento é aberto no MLflow com nome padronizado (definido pelas regras da empresa), vinculado ao projeto corrente. Nada vai se perder.</p>
<p><strong>No</strong> <code>fit</code><strong>:</strong> os hiperparâmetros são logados automaticamente. As métricas de treino são calculadas e registradas. O modelo é serializado no formato definido pelo time — incluindo metadados como features utilizadas, data de treino, versão do dado, ambiente. Uma versão é criada no Model Registry.</p>
<p><strong>No</strong> <code>predict</code> <strong>em produção:</strong> cada chamada é rastreada. Os inputs e outputs ficam salvos numa tabela de log — no Databricks, no S3, onde a infra da empresa estiver.</p>
<p>O cientista não precisa saber que nada disso está acontecendo. Ele usa <code>model.fit()</code> e <code>model.predict()</code> como sempre fez.</p>
<hr />
<h2>O módulo de deploy</h2>
<p>Quando um modelo é aprovado para produção, existe um módulo específico na lib que cuida do processo:</p>
<pre><code class="language-python">from sua_lib import deploy

# Promove a versão para produção e cria o endpoint
deploy.promote(model_name="churn_predictor", version=12)
</code></pre>
<p>Por baixo, isso:</p>
<ul>
<li><p>Adiciona a tag <code>production</code> à versão no MLflow Registry</p>
</li>
<li><p>Disponibiliza um endpoint de inferência (via Databricks Model Serving, SageMaker, ou container, dependendo da infra)</p>
</li>
<li><p>Ativa o rastreamento de produção para aquela versão</p>
</li>
</ul>
<p>A partir daí, cada predição feita em produção fica registrada com timestamp, os X de entrada, o output do modelo, e a versão que foi usada. Isso não é dado de treino — é dado de operação. É o que permite, depois, construir dashboards de observabilidade e detectar drift antes que o cliente note.</p>
<p>Se você está no Databricks, esse rastreamento pode alimentar diretamente uma tabela Delta. Se está na AWS, é questão de apontar para um bucket S3 com um schema fixo e construir a query em cima com boto3. A lógica é a mesma — o que muda é o conector.</p>
<hr />
<h2>O que mudou na prática</h2>
<p>Depois que a lib entrou em operação, algumas coisas mudaram de forma concreta:</p>
<p>A primeira é que o onboarding de novos cientistas ficou trivial em relação a MLOps. Você instala a lib, configura as variáveis de ambiente uma vez, e pronto. A pessoa não precisa aprender MLflow, não precisa saber onde os modelos ficam salvos, não precisa se preocupar com versionamento. Ela aprende a usar <code>Model()</code> e está operacional.</p>
<p>A segunda é que, pela primeira vez, tínhamos uma visão centralizada de todos os modelos em produção. Qual versão estava rodando, quando havia sido treinada, qual performance havia apresentado no treino, quando foi a última predição. Não era perfeito — mas era infinitamente melhor do que antes, quando a resposta para <em>"o que está em produção?"</em> dependia de perguntar para cada cientista individualmente.</p>
<p>A terceira — e mais importante do ponto de vista de gestão — é que os engenheiros de ML pararam de ser bombeiros. Em vez de ficarem sendo acionados toda vez que um modelo quebrava ou precisava de um redeploy, passaram a focar na evolução da própria lib. Cada melhoria na lib multiplica por todos os projetos do time.</p>
<hr />
<h2>O que essa lib ainda não faz (e o que vem depois)</h2>
<p>Importante ser honesto: essa abordagem, como descrevi, cobre modelo supervisionado. Classificação, regressão — o ciclo clássico de <code>fit</code> e <code>predict</code>.</p>
<p>Mas o mundo não é só isso. Modelos de linguagem, sistemas de recomendação não supervisionados, pipelines semi-supervisionados — cada um desses tem um ciclo de vida diferente, métricas diferentes, e padrões de observabilidade diferentes. Há muito espaço para expandir.</p>
<p>Além disso, o que descrevi é a camada de rastreamento e versionamento. Tem uma camada de qualidade de dado que pode vir antes — validação de schema, detecção de drift nos inputs antes de o modelo nem ser chamado. Tem uma camada de retraining automático que pode vir depois. A lib que construímos foi um ponto de partida, não um destino.</p>
<hr />
<h2>A carta aberta</h2>
<p>Se você está liderando um time de dados com mais de dois ou três cientistas, já passou — ou vai passar — pelo problema que descrevi. O modelo que ninguém sabe quem treinou. O script que só o autor entende. O deploy que é um arquivo copiado à mão para um servidor.</p>
<p>A lib que o seu time precisa não precisa ser um projeto de seis meses. Pode começar pequena: um wrapper que força um formato de salvamento padronizado. Uma função que abre um experimento no MLflow antes de qualquer treino. Um script que loga as predições em produção numa tabela.</p>
<p>O importante é começar. Porque a alternativa — confiar que cada pessoa vai fazer a coisa certa de forma independente, indefinidamente — não escala. E o custo dessa aposta aparece sempre da pior forma possível: numa reunião de rotina, quando o cliente menciona de passagem que o modelo parou de funcionar dois meses atrás.</p>
<p>Você já tem esse problema no seu time? Tem feito algo parecido? Curioso pra saber como outras empresas estão resolvendo isso.</p>
]]></content:encoded></item><item><title><![CDATA[Previsão de Demanda: Como Sair do Excel e Entrar no GFM com Reconciliação Hierárquica]]></title><description><![CDATA[Há alguns anos, trabalhei num projeto para uma loja de cosméticos de médio porte. A dona do negócio era uma empreendedora excelente, com bom feeling de mercado e anos de experiência no setor. Mas quan]]></description><link>https://phelipemuller.com.br/previsao-demanda-conheca-gfm</link><guid isPermaLink="true">https://phelipemuller.com.br/previsao-demanda-conheca-gfm</guid><category><![CDATA[Data Science]]></category><category><![CDATA[supply chain]]></category><category><![CDATA[Demand forecasting]]></category><dc:creator><![CDATA[Phelipe Müller]]></dc:creator><pubDate>Tue, 24 Mar 2026 07:57:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69231b6801313a5627cdb0cc/af1efa81-3266-47fd-98df-74d91ffb97fe.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Há alguns anos, trabalhei num projeto para uma loja de cosméticos de médio porte. A dona do negócio era uma empreendedora excelente, com bom feeling de mercado e anos de experiência no setor. Mas quando o assunto era quanto pedir aos fornecedores no mês seguinte, o processo era mais ou menos assim: abrir o Excel, olhar o mês do ano anterior, ajustar na intuição (produto a produto), e fazer o pedido.</p>
<p>Não era descuido. Era o que ela sabia fazer — e o processo foi evoluindo com o tempo. No começo, o estoque acabava e o pedido vinha na hora. Depois, ela foi mantendo uma lista dos produtos prestes a acabar e pedindo tudo junto no final do mês. Com o tempo, percebeu que alguns produtos precisavam ser pedidos com dois meses de antecedência por conta da sazonalidade, e aí surgiu o hábito de olhar o mesmo mês do ano anterior como referência.</p>
<p>Durante muito tempo, isso funcionou razoavelmente bem. O problema é que o negócio cresceu. O portfólio foi de 40 SKUs para quase 300. O que antes cabia na cabeça de uma pessoa passou a gerar um volume de decisões impossível de gerenciar manualmente com precisão — sem gastar mais de um dia inteiro por mês só nisso.</p>
<p>Foi aí que começamos nosso projeto.</p>
<p>Este artigo conta como estruturamos a solução para esse problema — e por que a combinação de um Global Forecasting Model com reconciliação hierárquica foi a resposta certa.</p>
<hr />
<h2>O custo do erro de previsão</h2>
<p>Previsão de demanda errada tem dois lados igualmente ruins: pedir de mais e pedir de menos.</p>
<p>Quando você pede demais, o estoque parado ocupa capital de giro, ocupa prateleira, e às vezes estraga. Quando você pede de menos, perde venda, perde cliente, e paga frete de emergência.</p>
<p>Os dois lados do erro de previsão têm um custo, e sem método, você oscila entre eles sem saber exatamente o quanto está errando.</p>
<p>Ao calcular o erro médio ponderado (WMAPE) do processo anterior, chegamos a algo em torno de <strong>70%</strong>. Num portfólio de cosmético com margens apertadas, isso é dinheiro deixado na mesa todo mês.</p>
<hr />
<h2>A hierarquia de produtos e por que ela importa</h2>
<p>Em qualquer loja de cosméticos, os produtos formam uma hierarquia natural — mas tornar essa hierarquia computável é um trabalho por si só. Nesse projeto, passamos por diversas conversas com a dona da loja para estruturar o que ela já carregava de forma intuitiva. Ela sabia, por exemplo, que não adiantava pedir muito do protetor solar A se ainda havia estoque do B. Esse tipo de percepção precisava virar dado. A categorização que parecia óbvia na prática exigiu entrevistas, validações e ajustes até se tornar uma estrutura utilizável pelo modelo.</p>
<pre><code class="language-plaintext">Loja (total)
└── Categoria (Maquiagem, Skincare, Perfumaria, Cabelos...)
    └── Subcategoria (Protetor Solar, Base, Batom, Sérum...)
        └── Produto (Protetor Solar FPS 50 Marca X, 200ml)
</code></pre>
<p>Essa hierarquia existe porque as decisões de negócio acontecem em níveis diferentes. O financeiro quer saber o faturamento total do mês. O comprador quer saber quanto de Skincare pedir. O estoquista precisa saber quantas unidades de cada produto específico separar.</p>
<p>A abordagem mais intuitiva seria prever os menores grãos — cada produto individualmente — e agregar para os níveis superiores. O problema é que o nível de produto é justamente o mais difícil de prever: séries esparsas, produtos novos sem histórico (phase-in), produtos sendo descontinuados (phase-out), variações de estoque que distorcem a demanda real.</p>
<p>Uma alternativa mais robusta é usar os níveis hierárquicos mais altos, que têm séries mais estáveis e fáceis de prever, e a partir deles fazer uma decomposição para os níveis mais baixos — garantindo que os números sejam coerentes entre si em todos os níveis.</p>
<p>É exatamente isso que a reconciliação hierárquica resolve. Se você prevê cada nível de forma independente, os números não fecham — a soma das previsões dos produtos de Skincare não dá automaticamente o total da categoria Skincare. Isso não é só um problema estético: o comprador e o estoquista passam a operar com duas versões diferentes do futuro.</p>
<hr />
<h2>Como a ciencia de dados resolveu isso: do modelo por item ao GFM</h2>
<p>Por muito tempo, o estado da arte era um modelo por série: ARIMA, Prophet, Holt-Winters — treinados separadamente para cada item. Num caso razoavelmente simples como o nosso, com 300 SKUs, já estaríamos falando de 300 modelos para treinar, monitorar e manter.</p>
<p>Isso mudou com a <strong>M5 Competition</strong> (Kaggle, 2020). O desafio: prever 42.840 séries temporais hierárquicas de vendas do Walmart. Quase 6.000 participantes de mais de 100 países.</p>
<p>O que a competição revelou: <strong>modelos treinados em múltiplas séries simultaneamente superaram sistematicamente os modelos treinados série por série.</strong> Não por uma margem pequena — de forma consistente o suficiente pra mudar o que o mercado passou a considerar boa prática. Os melhores colocados combinavam modelos globais com técnicas de reconciliação hierárquica — exatamente a abordagem que adotamos aqui. A competição não só consolidou o GFM como padrão, como trouxe implementações abertas e benchmarks públicos que aceleraram a adoção na indústria.</p>
<p>O GFM (Global Forecasting Model) é o nome dessa abordagem: um único modelo que aprende de todas as séries ao mesmo tempo, capturando padrões compartilhados de sazonalidade, tendência e comportamento de categoria — e tornando a manutenção e a observabilidade do sistema muito mais simples.</p>
<hr />
<h2>O que é reconciliação hierárquica</h2>
<p>Mesmo com o GFM, as previsões geradas por nível ainda não são automaticamente coerentes entre si. A reconciliação hierárquica é a etapa que resolve isso: ela ajusta todas as previsões de forma coordenada, garantindo que os números fechem em todos os níveis.</p>
<p>Para entender como a reconciliação funciona, a forma mais intuitiva é pensar no Top-Down: você prevê o total da loja, distribui proporcionalmente pelas categorias, e desce até o produto. Simples, mas frágil — qualquer erro no topo se propaga pra baixo, e você ignora os padrões específicos de cada SKU.</p>
<p>Uma das formas mais robustas que temos hoje é o <strong>MinTrace (Trace Minimization)</strong>. Em vez de privilegiar um nível específico da hierarquia, o MinTrace analisa a qualidade das previsões históricas em cada nível e torna mais rígidas as séries que ele prevê melhor — ajustando as demais para que sejam consistentes com essas. O resultado é um ajuste ótimo que usa a informação de todos os níveis ao mesmo tempo, não só do topo ou da base.</p>
<hr />
<h2>Implementação em Python: os pontos-chave</h2>
<p>Usamos a biblioteca <code>hierarchicalforecast</code> da Nixtla, integrada ao TimeGPT como modelo base.</p>
<p><strong>Estrutura dos dados</strong></p>
<pre><code class="language-python"># unique_id codifica a hierarquia com separador "/"
# Loja
# Loja/Skincare
# Loja/Skincare/Protetor
# Loja/Skincare/Protetor/FPS50_MarcaX
</code></pre>
<p><strong>Gerando as previsões base com TimeGPT</strong></p>
<pre><code class="language-python">from nixtla import NixtlaClient

client = NixtlaClient(api_key="sua_api_key")

fcst_df = client.forecast(
    df=Y_train_df,
    h=4,
    freq='W',
    time_col='ds',
    target_col='y'
)
</code></pre>
<p>Uma linha. Um modelo. Trezentas séries.</p>
<p><strong>Reconciliando com MinTrace</strong></p>
<pre><code class="language-python">from hierarchicalforecast.core import HierarchicalReconciliation
from hierarchicalforecast.methods import MinTrace

hrec = HierarchicalReconciliation(reconcilers=[MinTrace(method='mint_shrink')])

reconciled_df = hrec.reconcile(
    Y_hat_df=fcst_df,
    Y_df=Y_train_df,
    S=S_df,
    tags=tags
)
</code></pre>
<p><strong>Antes e depois da reconciliação:</strong></p>
<pre><code class="language-plaintext">Antes:
  Soma das previsões de produto:  R$ 163.200
  Previsão da loja (nível total): R$ 157.800
  Diferença: R$ 5.400

Depois:
  Diferença: R$ 0 — todos os níveis coerentes
</code></pre>
<hr />
<h2>O que mudou na prática</h2>
<p>O ganho mais direto foi no erro de previsão: o WMAPE caiu de 70% para 20% — menos de um terço do erro original. Na prática, isso significa menos pedido emergencial, menos estoque parado e mais capital de giro disponível. Para qualquer empresa que precisa aproveitar toda margem possível, isso não é número de apresentação. É dinheiro.</p>
<p>Mais do que o número, mudou o processo. A decisão de compra deixou de depender da intuição de uma pessoa que, além de tudo, precisava dedicar mais de um dia inteiro por mês só pra isso — e passou a ter uma base quantitativa.</p>
<p>Um ponto que vale explorar em mais profundidade em algum momento: <strong>o problema da ruptura de estoque como contaminante do modelo</strong>. Quando um produto fica sem estoque, as vendas registradas caem pra zero — não porque a demanda sumiu, mas porque não havia produto. Se você usa esse histórico "sujo" pra treinar o modelo, o GFM aprende que a demanda é menor do que é. A ruptura gera ruptura.</p>
<hr />
<h2>Conclusão</h2>
<p>A dona da loja não queria um modelo de machine learning. Ela queria passar menos tempo preocupada com pedido e mais tempo cuidando do negócio.</p>
<p>No fim, foi exatamente isso que entregamos. Com o processo rodando de forma automatizada e mais precisa, a decisão de compra saiu do modo reativo — correr atrás do que estava acabando — e entrou num ciclo previsível. Ela ficou surpresa com o quanto tempo liberou só com essa mudança.</p>
<p>É isso que uma boa solução de dados faz: não substitui o conhecimento de quem está no negócio, mas tira o peso das decisões que podem ser automatizadas — pra que o julgamento humano fique reservado onde realmente importa.</p>
<hr />
<p><strong>Referências</strong></p>
<ul>
<li><p>Olivares et al. (2024). <em>HierarchicalForecast: A Reference Framework for Hierarchical Forecasting in Python</em>. arXiv:2207.03517</p>
</li>
<li><p>Makridakis et al. (2022). <em>M5 accuracy competition: Results, findings, and conclusions</em>. International Journal of Forecasting.</p>
</li>
<li><p>Nixtla. <em>HierarchicalForecast documentation</em>.</p>
</li>
<li><p>Kaggle. <em>M5 Forecasting — Accuracy</em>.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Organizando a Casa: Um Framework Prático para Projetos de Data Science (Além do Cookiecutter)]]></title><description><![CDATA[Quando falamos sobre organizar processos e repositórios, a Engenharia de Software tem bibliotecas inteiras de conteúdo. Mas, quando afunilamos para Ciência de Dados, o material se torna escasso.
A referência mais conhecida é, sem dúvida, o Cookiecutt...]]></description><link>https://phelipemuller.com.br/framework-setup-loop</link><guid isPermaLink="true">https://phelipemuller.com.br/framework-setup-loop</guid><category><![CDATA[ciência de dados]]></category><category><![CDATA[framework]]></category><category><![CDATA[dados]]></category><category><![CDATA[Data Science]]></category><dc:creator><![CDATA[Phelipe Müller]]></dc:creator><pubDate>Fri, 26 Dec 2025 20:41:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1766781588590/51f91a3e-bf1e-41f6-90f3-4e1b44f6bab0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Quando falamos sobre organizar processos e repositórios, a Engenharia de Software tem bibliotecas inteiras de conteúdo. Mas, quando afunilamos para <strong>Ciência de Dados</strong>, o material se torna escasso.</p>
<p>A referência mais conhecida é, sem dúvida, o <a target="_blank" href="https://cookiecutter-data-science.drivendata.org/">Cookiecutter Data Science</a>. Embora apresente uma estrutura madura, ele ainda carrega o DNA da Engenharia de Software, o que gera uma certa estranheza no dia a dia exploratório de um cientista.</p>
<p>Do outro lado, temos frameworks de <em>processo</em> como CRISP-DM, TDSP e o MLOps Lifecycle. Todos são excelentes, mas costumam deixar lacunas:</p>
<ul>
<li><p><strong>CRISP-DM:</strong> É o "avô" da área (quase 40 anos!). Define bem as etapas (Dados -&gt; Modelagem -&gt; Deploy), mas ignora dores modernas como versionamento de código, organização de arquivos e iterações rápidas.</p>
</li>
<li><p><strong>TDSP (Microsoft):</strong> Foca muito em colaboração e agile (ótimo para times), mas dá pouco suporte para o "depois do deploy": monitoramento e retreino.</p>
</li>
<li><p><strong>MLOps Lifecycle:</strong> Cobre muito bem a produção (Design -&gt; Ops -&gt; Monitoring), mas muitas vezes passa rápido demais pela fase de experimentação e modelagem.</p>
</li>
</ul>
<p>Este artigo propõe uma <strong>forma prática de organizar seu repositório e seu projeto</strong>. O objetivo é aproveitar o melhor desses frameworks, mas priorizando o <em>mindset</em> do Cientista: fugir da rigidez excessiva do desenvolvedor, mas evitar que fiquemos presos eternamente no laboratório sem entregar valor.</p>
<h2 id="heading-o-mindset-setup-linear-e-loop-infinito">O Mindset: Setup Linear e Loop Infinito</h2>
<p>O segredo para usar esse framework é entender o pensamento por trás dele: <strong>construir o fluxo de ponta a ponta o mais rápido possível (Setup) e, só então, iterar para melhorar etapas pontuais (Loop).</strong></p>
<p>Vamos mergulhar nessas duas etapas.</p>
<h3 id="heading-1-o-setup-a-construcao-do-baseline">1. O SETUP (A Construção do Baseline)</h3>
<p>Esta é a etapa inicial onde descrevemos o contexto e construímos a "Versão Zero". Assim como no CRISP-DM, começamos pelo <strong>Entendimento do Negócio</strong>: contexto, dores, dados disponíveis e métricas de sucesso. A melhor forma de registrar isso é simples: arquivos Markdown (<code>.md</code>) na pasta de documentação.</p>
<p>Neste momento, temos um desafio claro: <strong>construir o modelo de "uma tarde"</strong>. No setup, o objetivo é ter um modelo rodando com todo o pipeline (pré e pós-processamento) no menor tempo possível.</p>
<ul>
<li><p>O modelo vai ser ruim? <strong>Vai.</strong></p>
</li>
<li><p>A acurácia vai ser baixa? <strong>Sim.</strong></p>
</li>
</ul>
<p><strong>Não tenha medo disso.</strong> O objetivo do setup não é performance, é <strong>infraestrutura</strong>. Um modelo baseline, mesmo que "burro", permite que o Engenheiro de MLOps comece a criar a esteira de deploy e o Engenheiro de Dados valide o delivery das tabelas. Ninguém fica esperando o Cientista encontrar o hiperparâmetro perfeito para começar a trabalhar.</p>
<blockquote>
<p><strong>Regra de Ouro do Setup:</strong> Se um obstáculo não é impeditivo, documente-o como uma "Proposta de Experimento" e siga em frente. Isso te dá velocidade e cria um backlog valioso para a próxima fase.</p>
</blockquote>
<p><strong>Um detalhe crucial: O Ambiente.</strong> Não adianta ter pastas organizadas se as bibliotecas são uma caixa preta. Ainda na fase de setup, crie um arquivo <code>requirements.txt</code> (ou use Poetry/Conda). A regra é simples: instalou uma lib nova para testar? Adicione ao arquivo de dependências imediatamente. Reprodutibilidade não é luxo, é pré-requisito para que o "modelo de uma tarde" rode na máquina do seu colega.</p>
<h4 id="heading-a-estrutura-de-pastas-pos-setup">A Estrutura de Pastas (Pós-Setup)</h4>
<p>Ao final dessa tarde de configuração, seu repositório deve ter esta cara:</p>
<pre><code class="lang-plaintext">data/
|---raw/
|---processed/
docs/
|---00_context.md
|---01_experiments_backlog.md
experiments/
|---00_setup.ipynb
|---archive/
jobs/
|---01_preprocess.py
|---02_featuring.py
|---03_model_train.py
|---04_model_predict.py
|---05_evaluate.py
|---06_posprocess.py
</code></pre>
<p>Entendendo cada diretório:</p>
<ul>
<li><p><code>data/</code>: Onde os dados (ou as queries) moram. Separar em <code>raw</code>, <code>processed</code> e <code>predict</code> ajuda a entender o ciclo de vida da informação.</p>
</li>
<li><p><code>docs/</code>: A memória do projeto. Já nasce com o <code>00_</code><a target="_blank" href="http://context.md"><code>context.md</code></a> (objetivos) e o <code>01_experiments_</code><a target="_blank" href="http://backlog.md"><code>backlog.md</code></a> (lista de ideias que você teve durante o setup mas não implementou).</p>
</li>
<li><p><code>experiments/</code>: <strong>O Laboratório.</strong> É aqui que testamos, falhamos e descobrimos, sem medo de quebrar a produção. Usamos Notebooks numerados (<code>00_setup.ipynb</code> é o primeiro) para manter uma ordem cronológica.</p>
<ul>
<li><em>Gerenciando o Caos:</em> Com o tempo, essa pasta pode ficar cheia. Crie uma subpasta <code>archive/</code> e mova para lá experimentos antigos ou descartados. Mantenha na raiz apenas o que é recente ou referência ativa.</li>
</ul>
</li>
<li><p><code>jobs/</code>: <strong>A Fábrica.</strong> É a fonte da verdade. O código aqui deve ser "sério", limpo e modularizado em scripts <code>.py</code> numerados pela ordem de execução.</p>
<ul>
<li><em>Por que separar?</em> Isso facilita muito a vida do MLOps. Se o pipeline quebrar, ele sabe exatamente qual script falhou (<code>02_</code><a target="_blank" href="http://featuring.py"><code>featuring.py</code></a>) e pode re-executar dali para frente, sem precisar rodar um notebook gigante desde o início.</li>
</ul>
</li>
</ul>
<h3 id="heading-2-o-loop-a-melhoria-continua">2. O LOOP (A Melhoria Contínua)</h3>
<p>Com o baseline em produção (ou pronto para tal), entramos no ciclo de melhoria científica.</p>
<p><strong>A. Escolha do Experimento</strong> Vá até o seu <code>docs/01_experiments_</code><a target="_blank" href="http://backlog.md"><code>backlog.md</code></a>. Baseado no tempo que você tem e na dor do modelo, escolha uma carta.</p>
<ul>
<li><p><em>Quer reduzir custo?</em> Pegue um experimento de Feature Selection.</p>
</li>
<li><p><em>A métrica de negócio está ruim?</em> Teste uma nova arquitetura de modelo.</p>
</li>
</ul>
<p><strong>B. Preparo do Experimento (A "Capa" do Notebook)</strong> Crie um novo notebook na pasta <code>experiments/</code> (ex: <code>05_teste_xgboost.ipynb</code>). Para evitar que ele vire um código "zumbi" que ninguém entende daqui a 2 meses, padronize a primeira célula com os metadados do estudo:</p>
<ul>
<li><p><strong>Nome:</strong> Um título descritivo (mais que o nome do arquivo).</p>
</li>
<li><p><strong>Status:</strong> <code>Em Andamento</code> / <code>Concluído</code> / <code>Aprovado</code> / <code>Descartado</code>. (Essencial para leitura dinâmica).</p>
</li>
<li><p><strong>Job Associado:</strong> Qual script da pasta <code>jobs</code> esse experimento pretende melhorar?</p>
</li>
<li><p><strong>Benefício Esperado:</strong> O que esperamos ganhar com isso?</p>
</li>
<li><p><strong>Metodologia:</strong> Breve descrição do que será feito (quais libs, qual técnica).</p>
</li>
<li><p><strong>Conclusão:</strong> (Preenchido ao final) O resultado foi atingido? Vamos levar para produção?</p>
</li>
</ul>
<p><strong>C. Execução e POO (Dica de Ouro)</strong> Agora você coda e mede o impacto. Mantenha o isolamento: mude apenas uma variável por vez para ter certeza da causalidade.</p>
<blockquote>
<p><strong>Dica Pro (Nível Sênior):</strong> Se você já está confortável com Python, use <strong>Programação Orientada a Objetos (POO)</strong>. Se seus scripts em <code>jobs/</code> forem classes, você pode importá-las dentro do notebook de experimento e usar <strong>polimorfismo</strong> para alterar apenas o método que você quer testar. Isso evita aquele "copia e cola" perigoso entre notebook e script oficial.</p>
</blockquote>
<p><strong>D. Conclusão e Ajuste dos Jobs</strong> O experimento deu certo? O gráfico subiu? Ótimo.</p>
<ol>
<li><p>Atualize a "Capa" do notebook com a <strong>Conclusão</strong> e mude o status para <code>Aprovado</code>.</p>
</li>
<li><p>Atualize o <code>docs/01_experiments_</code><a target="_blank" href="http://backlog.md"><code>backlog.md</code></a>.</p>
</li>
<li><p>Vá até a pasta <code>jobs/</code>.</p>
</li>
<li><p><strong>Refatore.</strong> Pegue a lógica validada no notebook (sem os gráficos e prints) e atualize o script <code>.py</code> oficial.</p>
</li>
<li><p><strong>O Teste de Paridade (Trust, but Verify):</strong> Este é o passo que separa amadores de profissionais. Antes de comemorar, rode o novo script <code>job</code> e o notebook original com o mesmo input. Os resultados devem ser <strong>exatamente idênticos</strong>. Se houver uma divergência mínima (0.01%), existe um erro na refatoração. Nunca faça o merge sem essa validação.</p>
</li>
</ol>
<h2 id="heading-conclusao-a-resposta-para-o-quanto-tempo-falta">Conclusão: A Resposta para o "Quanto tempo falta?"</h2>
<p>Essa estrutura não organiza apenas seus arquivos; ela organiza sua cabeça e a comunicação com o time.</p>
<p>Existem aditivos para projetos maiores? Claro: pastas <code>src</code> (para código compartilhado), <code>tests</code> (unitários), <code>conf</code> (para tirar parâmetros do código) e <code>infra</code> (Terraform/Docker). Mas isso é assunto para um próximo artigo.</p>
<p>O ponto central aqui é conectar nosso papel ao <strong>método científico</strong>. Ao trabalhar em blocos (Setup -&gt; Loop), você para de dar respostas vagas como <em>"Posso melhorar o modelo para sempre, não sei quando vai estar pronto"</em>.</p>
<p>Agora, sua resposta para o gerente é técnica e precisa:</p>
<blockquote>
<p><em>"O baseline já está rodando. Tenho 10 experimentos priorizados no backlog. Estimo 2 dias para cada. Daqui a 20 dias teremos a melhor versão possível dentro do prazo, mas o produto já existe hoje."</em></p>
</blockquote>
<p>Isso é Ciência de Dados profissional.</p>
]]></content:encoded></item><item><title><![CDATA[Viés de Seleção: O Inimigo Oculto nas suas Pesquisas]]></title><description><![CDATA[Já viu esse meme?
Ele brinca com a ideia de que quem responderia "não gosto" é justamente quem ignorou a pesquisa. Essa brincadeira ilustra perfeitamente um dos maiores vilões na análise de dados: o Viés de Seleção. Esse é um risco oculto nas não-res...]]></description><link>https://phelipemuller.com.br/selection-bias</link><guid isPermaLink="true">https://phelipemuller.com.br/selection-bias</guid><category><![CDATA[Machine Learning]]></category><category><![CDATA[Bias in AI]]></category><category><![CDATA[survey]]></category><category><![CDATA[Data Science]]></category><dc:creator><![CDATA[Phelipe Müller]]></dc:creator><pubDate>Tue, 16 Dec 2025 12:25:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765237818311/ceda5a80-651e-489e-9bac-370999126b2e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Já viu esse meme?</p>
<p>Ele brinca com a ideia de que quem responderia "não gosto" é justamente quem ignorou a pesquisa. Essa brincadeira ilustra perfeitamente um dos maiores vilões na análise de dados: o <strong>Viés de Seleção</strong>. Esse é um risco oculto nas não-respostas que pode fazer com que supostas decisões "data-driven" sejam piores do que aquelas tomadas apenas por instinto.</p>
<h2 id="heading-os-viloes-invisiveis-tipos-de-vies">Os Vilões Invisíveis: Tipos de Viés</h2>
<p>Vamos explorar alguns casos clássicos.</p>
<p>O cenário dos 95% acima representa o <strong>viés da não-resposta</strong>, onde a decisão de responder está diretamente ligada à variável que queremos estudar.</p>
<p>Outra situação clássica é o <strong>viés do sobrevivente</strong>. Próximo do final da Segunda Guerra, militares estudaram onde os aviões que retornavam eram mais atingidos para reforçar a blindagem. Mais tarde, se deram conta de que apenas consideravam os danos "suportáveis". As peças que mais precisavam de reforço eram justamente as vitais, atingidas nos aviões que caíram e não <strong>sobreviveram</strong> para contar a estória.</p>
<p>No mundo corporativo, o NPS é um paralelo claro. Frequentemente analisamos a satisfação de quem respondeu, esquecendo que o cliente insatisfeito (Detrator) muitas vezes já abandonou a base (<em>Churn</em>) e não "sobreviveu" para responder à pesquisa, criando, assim, uma miragem de satisfação.</p>
<p>Por fim, semelhante ao viés da não-resposta, temos o <strong>viés do voluntário</strong>. Em pesquisas de satisfação de produtos em e-commerce, a base tende a polarizar. Os consumidores que tiveram uma percepção "ok" tendem a não se interessar em opinar. Isso causa uma falsa polarização, podendo indicar erroneamente que um produto está abaixo do padrão de qualidade por simples coincidência amostral.</p>
<p>De forma geral, o viés de seleção cria um abismo entre os respondentes e a população real. Nas próximas linhas, vamos explorar como a Ciência de Dados nos permite construir pontes sobre esse abismo.</p>
<h2 id="heading-aprofundamento-pratico-tratando-a-diferenca">Aprofundamento Prático: Tratando a Diferença</h2>
<p>Antes de mais nada, vamos visualizar o viés:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765237860519/bf07cdc5-4f18-4288-9452-09b67b6568f1.png" alt /></p>
<p>Considere que o gráfico refere-se a qualquer variável de interesse (renda, horas no computador, etc.). O viés da seleção faz com que sua distribuição assuma um perfil completamente diferente da população real. Se você basear sua decisão apenas na curva laranja (respondentes), pode precificar errado seu produto e perder a maior parte do mercado (curva azul).</p>
<p>A solução intuitiva parece simples: "Podemos apenas deformar os dados dos respondentes para se parecerem mais com a população?".</p>
<p>A ideia é exatamente essa. Mas, no mundo real não temos a distribuição da população na variável de interesse, caso contrário não faríamos a pesquisa. Então, o que fazer? Temos algumas abordagens principais:</p>
<h3 id="heading-1-raking-o-ajuste-demografico">1. Raking (O Ajuste Demográfico)</h3>
<p>O plano é minimizar a diferença nas variáveis que já temos (idade, sexo, bairro), na intenção de minimizar por consequência a diferença nas variáveis de interesse.</p>
<p>Você pega a distribuição dessas variáveis conhecidas e, iterativamente, aumenta ou diminui o peso de cada amostra. O objetivo é fazer com que a demografia da sua pesquisa fique cada vez mais parecida com a da população total. Ao ajustar o peso demográfico, a variável de estudo tende a se corrigir também.</p>
<p>Em Python, a lógica iterativa se parece com isso:</p>
<pre><code class="lang-python"><span class="hljs-comment"># target_dist: Dicionário com a % real da população (ex: IBGE)</span>
<span class="hljs-comment"># df: Dataframe da sua pesquisa</span>
variables = [<span class="hljs-string">'sexo'</span>, <span class="hljs-string">'faixa_etaria'</span>, <span class="hljs-string">'regiao'</span>]

<span class="hljs-comment"># O Raking é um processo iterativo até a convergência</span>
<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">10</span>): 
    <span class="hljs-keyword">for</span> var <span class="hljs-keyword">in</span> variables:
        <span class="hljs-comment"># 1. Calcular distribuição atual na amostra (ponderada)</span>
        current_dist = df.groupby(var)[<span class="hljs-string">'weight'</span>].sum() / df[<span class="hljs-string">'weight'</span>].sum()

        <span class="hljs-comment"># 2. Calcular fator de correção (Meta / Atual)</span>
        <span class="hljs-comment"># Se a meta é 50% mulheres e temos 25%, o fator será 2.0</span>
        factors = target_dist[var] / current_dist

        <span class="hljs-comment"># 3. Atualizar pesos das amostras</span>
        df[<span class="hljs-string">'weight'</span>] *= df[var].map(factors)
</code></pre>
<h3 id="heading-2-ipw-inverse-probability-weighting">2. IPW (Inverse Probability Weighting)</h3>
<p>Diferente do Raking, o IPW foca na probabilidade.</p>
<p>Treinamos um modelo de Machine Learning para prever se uma pessoa vai ou não responder à pesquisa. O peso atribuído a cada respondente será o inverso dessa probabilidade (1 / probabilidade). Ou seja, damos "um megafone" para o respondente que tinha baixa chance de responder, pois estatisticamente ele representa o grupo silencioso.</p>
<p>A implementação conceitual seria:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> sklearn.linear_model <span class="hljs-keyword">import</span> LogisticRegression

<span class="hljs-comment"># 1. Treinar modelo para prever a probabilidade de resposta (Propensity Score)</span>
<span class="hljs-comment"># Usamos dados demográficos que temos de TODOS (respondentes e não respondentes)</span>
model = LogisticRegression()
model.fit(X_demographics, y_responded) <span class="hljs-comment"># y: 1=Respondeu, 0=Ignorou</span>

<span class="hljs-comment"># 2. Prever a probabilidade de cada indivíduo ter respondido</span>
probs = model.predict_proba(X_demographics)[:, <span class="hljs-number">1</span>]

<span class="hljs-comment"># 3. Calcular o peso IPW</span>
<span class="hljs-comment"># O "Pulo do Gato": Inverter a probabilidade.</span>
<span class="hljs-comment"># Quem tinha probabilidade baixa (0.1) ganha peso alto (10x)</span>
df[<span class="hljs-string">'ipw_weight'</span>] = <span class="hljs-number">1.0</span> / probs
</code></pre>
<h3 id="heading-3-analise-de-onda-wave-analysis">3. Análise de Onda (Wave Analysis)</h3>
<p>Mas e se não conhecemos a população total? Análise de onda utiliza o comportamento dos respondentes na própria pesquisa, para apoiar em entender o comportamento de quem nem respondeu. Partindo da premissa de que responder não é um booleano (Sim/Não), mas sim um degradê.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765237876690/268ae36d-e8a4-4eac-9d31-e7912c1b3e16.png" alt /></p>
<p>Imagine que enviamos 3 lembretes. A premissa diz que quem respondeu só no terceiro lembrete se assemelha mais a quem <em>não respondeu</em> do que quem respondeu logo de cara. Então, aumentamos o peso dos respondentes tardios para inferir o comportamento dos não-respondentes.</p>
<p>Podemos usar uma regressão simples para extrapolar essa tendência:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> sklearn.linear_model <span class="hljs-keyword">import</span> LinearRegression

<span class="hljs-comment"># Premissa: O comportamento muda linearmente conforme a "demora" (Onda)</span>
<span class="hljs-comment"># Onda 1: Resposta Imediata | Onda 2: Lembrete 1 | Onda 3: Lembrete Final</span>

<span class="hljs-comment"># Treinar regressão: X = Onda, Y = Variável de Interesse (ex: Satisfação)</span>
reg = LinearRegression()
reg.fit(df[[<span class="hljs-string">'wave_number'</span>]], df[<span class="hljs-string">'satisfaction_score'</span>])

<span class="hljs-comment"># Extrapolar para a "Onda 4" (O perfil teórico do não-respondente)</span>
non_respondents_score = reg.predict([[<span class="hljs-number">4</span>]])

print(<span class="hljs-string">f"Estimativa ajustada para o público invisível: <span class="hljs-subst">{non_respondents_score}</span>"</span>)
</code></pre>
<h2 id="heading-validando-a-confianca-analise-de-sensibilidade">Validando a Confiança: Análise de Sensibilidade</h2>
<p>Mesmo com os ajustes, como garantir a segurança do insight? Recomendo a Análise de Sensibilidade com Ponto de Virada.</p>
<p>Para cada insight, pergunte: "Quão diferente os não-respondentes precisariam ser para que este insight estivesse errado?".</p>
<p>A fórmula abaixo nos ajuda a simular esse cenário, onde <code>Valor</code> é a média da pesquisa e <code>Alteração</code> é o quão pior imaginamos que os não-respondentes sejam:</p>
<p>$$\text{Média Simulada} = (\text{Valor} \times \text{Taxa}) + (\text{Valor} \times (1 - \text{Taxa}) \times (1 + \text{Alteração}))$$</p><p><strong>Exemplo 1:</strong></p>
<blockquote>
<p>"A nota média de satisfação é 8 (na realidade 8.4 arredondado pra 8)" (com 50% de respondentes).</p>
</blockquote>
<p>Simulamos que os não-respondentes têm uma percepção pior:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Alteração</td><td>Média simulada</td></tr>
</thead>
<tbody>
<tr>
<td>0%</td><td>8.4</td></tr>
<tr>
<td>-10%</td><td>8</td></tr>
<tr>
<td>-20%</td><td>7.6</td></tr>
<tr>
<td>-25%</td><td>7.4</td></tr>
<tr>
<td>Nesse caso, a percepção de quem não respondeu precisaria ser <strong>25% pior</strong> para derrubar significativamente a média. É uma margem de segurança alta. O insight é robusto.</td></tr>
</tbody>
</table>
</div><p><strong>Exemplo 2:</strong></p>
<blockquote>
<p>"A renda média anual é de 97K" (70% de resposta).</p>
</blockquote>
<p>Ao rodar a simulação, percebemos que uma alteração de <strong>apenas 2%</strong> nos não-respondentes já invalida a afirmação!</p>
<p>Isso é um lembrete de que, em <em>surveys</em>, devemos trabalhar com faixas (ranges). Se no exemplo 2 usássemos a premissa de que a média está "entre 90k e 100k", o valor de alteração necessário para quebrar a afirmação subiria para 26%, sem mudar o sentido do insight e trazendo muito mais confiança. No exemplo 1 já levamos isso em conta de forma discreta, por conta do arredondamento dizer que a nota é 8, diz que ela está entre 7.6 e 8.4.</p>
<h2 id="heading-conclusao">Conclusão</h2>
<p>Como pode ver, a Ciência de Dados apresenta ferramentas não só para analisar o que foi coletado, mas para iluminar o que não foi. Ferramentas como Raking, IPW e Análise de Sensibilidade são essenciais para transformar dados brutos em inteligência estratégica, garantindo que suas decisões de negócio estejam não só embasadas em dados, mas em dados confiáveis.</p>
]]></content:encoded></item></channel></rss>