Automatizando a busca de emprego: um projeto de raspagem de dados e aprendizado de máquina

Vitor Sampaio
9 min readJul 14, 2023

Convenhamos, depender do algoritmo do Linkedin para pesquisar vagas não é nada agradável. Vagas totalmente não relacionadas com o que você está procurando, patrocinadas, ou até mesmo em áreas que não tem nada a ver com o que você tem procurado.

Baseado nisso resolvi desenvolver um aplicativo que diariamente me envia uma seleção de vagas ordenadas por relevância.

Mas como isso ocorre? Como as vagas são selecionadas? Como a IA ordena quais as vagas mais relevantes para MIM? A máquina está pensando como eu penso? Como tudo acontece sem nenhum input do usuário?

Acompanhe-me no desenvolvimento deste texto que todas as perguntas serão respondidas.

E-mail que recebo todas as manhãs com seleção de vagas por compatibilidade

1. A estrutura do App

O aplicativo deve buscar as vagas publicadas nas ultimas 24h automaticamente, armazena-las, processar pelo algoritmo de machine learning, organiza-las em um e-mail e me enviar todo dia às 8:30 — enquanto tomo meu café.

Diagrama de funcionamento do App

2. Seleção inicial de vagas

Para treinar meu modelo de machine learning eu precisava da base inicial de uma pesquisa. Defini que a pesquisa padrão para mim seria a busca booleana ”(“(data science)” OR “cientista de dados)” OR “machine learning”)”.

Screenshot do dataframe de treino original obtido

Uma análise rápida do material nos traz alguns insights:

Uma rápida análise de dados nos traz os seguintes insights:

Número de Trabalhos Remotos: Há 120 postagens de emprego que mencionam “remoto” ou “remote” em suas descrições, sugerindo que eles podem oferecer trabalho remoto.

Empresas Oferecendo Trabalhos Remotos: As empresas que mais mencionam “remoto” ou “remote” em suas postagens de emprego são “Crossover”, “TELUS International AI Data Solutions”, e “MonetizeMore”, cada uma com várias postagens.

Localização dos empregos: Grande parte dos empregos estão localizados no Sudeste, em cidades como São Paulo, Rio de Janeiro e Belo Horizonte.

A análise completa pode ser lida no Github do projeto.

3. Categorização dos dados

Para treinamento do algoritmo, precisamos “labelizar” os nossos dados, isso é, avaliar vaga por vaga (das 400+ raspadas).

Avaliei as vagas e as categorizei com 0, 1 e 2. Onde:

  • 0 — vaga nao relevante
  • 1 — vaga relevante
  • 2 — vaga muito relevante

Obtendo a seguinte distribuição:

Já nota-se que a pesquisa do LinkedIn não é nada eficaz. Poucas vagas relevantes, e muitas vagas irrelevantes.

Como uma base de dados desbalanceada é um complicador no treinamento de um algoritmo de machine learning, vamos reestruturar a base. Tudo que foi categorizado como Muito Relevante (2), será alterado para relevante (1). No entanto, classificaremos a vaga como relevante ou não:

Os dados seguem desbalanceados, mas muito melhor que antes. Agora antes de qualquer alteração ou engenharia de features, vamos dividir os dados em treino e teste, para se evitar o temido e famoso DATA LEAKAGE.

4. Engenharia de Features

Não vou entrar muito nos detalhes, todo esse processo está no github. Mas basicamente removemos pontuações, removemos stopwords, removemos dados duplicados (muitas vagas são anunciadas em cidades diferentes, porem com o mesmo título), e juntamos todo o conteúdo relevante de texto em uma unica coluna — para ambos datasets de treino e teste:

# Juntar as colunas "DESCRICAO", "TITULO", "LOCAL" e "EMPRESA" em uma nova coluna "TEXT" no conjunto de treinamento
X_train['TEXT'] = X_train['LOCAL'] + ' ' + X_train['EMPRESA'] + ' ' + X_train['TITULO'] + ' ' + X_train['DESCRICAO']

# Juntar as colunas "DESCRICAO", "TITULO", "LOCAL" e "EMPRESA" em uma nova coluna "TEXT" no conjunto de teste
X_test['TEXT'] = X_test['LOCAL'] + ' ' + X_test['EMPRESA'] + ' ' + X_test['TITULO'] + ' ' + X_test['DESCRICAO']

# Exibir as primeiras linhas dos conjuntos de treinamento e teste com a nova coluna "TEXT"
print("Conjunto de treinamento:")
print(X_train.head())
print()
print("Conjunto de teste:")
print(X_test.head())

5. Treinamento do modelo de Machine Learning

Partindo para o mais interessante do nosso processo: O treinamento do modelo de machine learning. Tentei inicialmente Naive Bayes e Random Forest, com baixa perfomance dos modelos. Então parti para artilharia pesada: XGBoost.

5.1 O que é o XGBoost?

XGBoost, ou Extreme Gradient Boosting, é uma biblioteca de aprendizado de máquina otimizada que implementa algoritmos de árvore de decisão sob o framework de boosting. Boosting é uma técnica de aprendizado de conjunto que visa converter aprendizes fracos em aprendizes fortes.

O princípio por trás do XGBoost é que ele opera adicionando novos modelos a um modelo existente de maneira iterativa, para corrigir os erros cometidos pelos modelos existentes. Modelos ou árvores de decisão são adicionados uma de cada vez, e os erros existentes de cada modelo são corrigidos e melhorados para tornar o modelo final mais preciso e robusto.

O XGBoost também oferece várias técnicas de regularização para controlar o overfitting, tornando-o eficaz e versátil em muitos problemas de aprendizado supervisionado.

Pode-se dizer então que o XGBoost é um Random Forest tunado com lasers?

Essa é uma forma divertida e simplificada de comparar os dois métodos, mas não é exatamente precisa do ponto de vista técnico. Ambos, XGBoost e Random Forest, são técnicas de aprendizado de máquina baseadas em árvores, mas têm diferenças significativas.

Random Forest é uma técnica de aprendizado de conjunto que cria um grande número de árvores de decisão independentes durante o treinamento e produz a classe que é a moda das classes (classificação) ou média das previsões individuais (regressão) das árvores na previsão.

Por outro lado, XGBoost é uma implementação de árvores de decisão com boost gradiental. Diferente de Random Forest, onde as árvores são geradas de forma independente, no XGBoost, as árvores são adicionadas sequencialmente para corrigir os erros dos modelos anteriores. Além disso, o XGBoost inclui uma etapa de regularização para evitar o overfitting, o que não acontece no Random Forest.

Portanto, se formos usar a analogia, poderíamos dizer que o Random Forest é como um batalhão de soldados onde cada um atira de forma independente, enquanto o XGBoost é mais como um time de soldados com laser, onde cada soldado aprende com os erros dos soldados anteriores para melhorar sua própria mira. Ambos podem ser poderosos dependendo da situação, mas têm estratégias diferentes.

5.2 Métricas do modelo treinado

Após treinar o modelo, obtivemos as seguintes métricas:

Excelente! Traduzindo os números, podemos dizer que:

  • Precision: Precision é a proporção de previsões corretas para uma classe em relação ao total de previsões para essa classe. Por exemplo, para a classe 0, a precisão de 0,81 significa que 81% das previsões de “classe 0” que o modelo fez estavam corretas.
  • Recall: Recall (ou sensibilidade) é a proporção de verdadeiros positivos em relação à soma de verdadeiros positivos e falsos negativos. Basicamente, é a porcentagem de instâncias de uma classe que o modelo conseguiu identificar corretamente. Para a classe 0, um recall de 0,93 significa que o modelo identificou corretamente 93% das instâncias da “classe 0”.
  • F1-score: O F1-score é uma média harmônica de precision e recall. Ele combina ambos em um único número que varia entre 0 (pior) e 1 (melhor). O F1-score é útil se você quiser comparar dois ou mais modelos e quer uma única métrica para isso. Para a classe 0, um F1-score de 0,87 indica um bom equilíbrio entre precision e recall.
  • Support: Support é o número de amostras reais da classe em seu conjunto de dados. Por exemplo, para a classe 0, há 42 amostras no conjunto de dados.
  • Accuracy: A accuracy (acurácia) é a proporção de previsões corretas feitas pelo modelo em relação ao total de previsões. Neste caso, a accuracy do modelo é 0,82, ou 82%, o que significa que o modelo fez previsões corretas para 82% das amostras no conjunto de dados.

6. Avaliando o modelo — Uma lição sobre Data Drift

Agora que nosso modelo está pronto, vamos testa-lo em relação à novos dados. Pedi o chat GPT para gerar um dataframe novo com 10 vagas, 5 que eu classificaria como relevantes, e 5 como não relevantes, conforme a seguir:

Dados Sintéticos gerados pelo GPT

E rodando no nosso modelo, obtivemos os seguintes resultados:

Resultados das predições dos dados sintéticos

Péssima performance. Por que?

Isso se deve ao fenômeno chamado de data drift. O termo “data drift” refere-se a um fenômeno que ocorre quando os dados utilizados para treinar um modelo de aprendizado de máquina (machine learning) se tornam diferentes dos dados com os quais o modelo é posteriormente implantado ou usado para fazer previsões. Em outras palavras, ocorre uma mudança nos padrões ou distribuição dos dados ao longo do tempo.

Essa mudança nos dados de entrada pode ser causada por vários fatores, como alterações nas preferências dos usuários, evolução das condições do mundo real, mudanças nos processos de negócios ou até mesmo erros de medição. O problema é que, quando o modelo é treinado com um conjunto inicial de dados, ele aprende a reconhecer padrões específicos presentes nesses dados. Se esses padrões mudam ao longo do tempo, o modelo pode se tornar menos preciso ou até mesmo produzir resultados incorretos.

Trazendo para o contexto no nosso desenvolvimento do modelo, o que ocorreu foi que treinamos o modelo com uma formatação e uma sintaxe específica. E os dados gerados são escritos e formatados de uma maneira completamente diferente, causando a confusão nas previsões — ou seja: Data Drift.

6.1 Testando com dados válidos

Rodamos o webscraper novamente, em uma simulação de como dados seriam encontrados no aplicativo, com a mesma formatação e sintaxe, assim, obtivemos a seguinte previsão, onde a ultima coluna é a probabilidade da vaga anunciada ser classificada com “relevante”:

Perfeito! Até assustador, eu diria. Pensar que o computador está pensando “por mim”, é um pouco estranho. Sinto como se tivesse transplantado minha consciência pra um algoritmo, através de matemática, computação e estatística.

7. Desenvolvimento do template e script de envio

Feita a parte do core do nosso app, temos agora que levar ao usuário (eu) os dados da melhor forma. Desenvolvi um algoritmo que elenca as 5 vagas mais relevantes, baseadas em probabilidade, seguidas das outras vagas em ordem decrescente.

Além disso foi necessário também formatar de uma maneira amigável ao usuário de se ler, com hyperlinks, emojis, etc. Não entrarei no detalhe por aqui, mas tudo está no Github, novamente, no arquivo email_sender.py.

Trecho do email enviado

8. Deploy e agendamento do script

Para deploy do aplicativo, utilizamos uma instancia EC2 de Linux. Acessamos através da chave .ppk no putty, e transferimos os arquivos via WinSCP. Um processo muito simples que pode ser encontrado em tutoriais pela internet.

Para agendar o script, utilizamos a funcionalidade CRON, nativa do Linux, que permite o agendamento de tarefas — ou “Cron Jobs”, para serem executadas em horários fixos, datas e até intervalos personalizados.

Acessamos o crontab:

crontab -e

Adicionamos os cron jobs no final do arquivo:

0 5 * * * /usr/bin/python3  /home/ubuntu/fetch_new_data.py
20 5 * * * /usr/bin/python3 /home/ubuntu/classificator.py
30 11 * * * /usr/bin/python3 /home/ubuntu/email_sender.py

Salvamos e fechamos, e pronto!

Temos nosso sistema rodando na nuvem de maneira totalmente gratuita. Vale lembrar que o padrão de horário do EC2 é ECT. Portanto, às 2:00 (GMT-3) será executado o fetch_new_data.py, às 2:20 o classificator.py, e às 8:30 será enviado o email, através do email_sender.py.

9. Resultados

O resultado final? Um e-mail que recebo todos os dias às 8:30 da manhã com as vagas mais relevantes para mim. A facilidade de ter vagas personalizadas, sem custo e com uma máquina treinada para pensar como eu, é inestimável.

10. Oportunidades de Melhoria

É possível fazer um trabalho melhor de MLOPs, com um algoritmo que se retreine dada uma interação com o e-mail, avaliando se a vaga informada realmente é relevante, ou não.

Além disso, também é interessante se fazer um webscraper mais orientado à objetos, condensando o código em um main.py e executando sequencialmente o código ao invés de um agendamento sequencial no cron.

11.Limitações

Vale ressaltar que o projeto atual depende fortemente da estrutura do LinkedIn permanecer a mesma. Se ocorrerem mudanças no site ou em sua política de raspagem de dados, o código pode quebrar.

12. Links úteis:

Repositório do projeto:

https://github.com/Sampaio-Vitor/webscraper.classifier/tree/main

Meu LinkedIn:

https://www.linkedin.com/in/vitorcsampaio/

Meu portfolio:

https://www.datascienceportfol.io/vitor

--

--