um produto orientado a dados governamentais: parte 1
construção de um produto data driven
Esse é o segundo post de uma série de como construir um produto data-driven de ponta a ponta, caso você ainda não tenha acompanhado os demais, abaixo segue uma síntese com os respectivos links 😀.
- Em metadados de normas jurídicas federais coletamos dados do sistema LexML.
No último post coletamos um dataset com o objetivo de criar um data product a partir do treinamento de um modelo de classificação multi-label, treinando-o sobre as ementas de normativos federais.
Como primeiro passo do nosso processo de construção de um produto derivado de aprendizado de máquina, precisamos iniciar com um processo de análise exploratória dos dados (AED). Portanto, vamos carregar os dados em uma instância do TinyDB
, em seguida vamos selecionar as features do nosso interesse e carregá-las em um dataframe.
db = TinyDB("./data/metadados_ficha_catalografica.json")
total_normativos = db.all()
def alter_cols(df: pd.DataFrame) -> pd.DataFrame:
"""faz um tratamento nas colunas dataPublished e legislationType"""
normas = df.copy()
df['datePublished'] = pd.to_datetime(df['datePublished'])
df['legislationType'] = df['legislationType'].apply(lambda x : x.split("/")[-1] if x else np.nan)
return df
def load_into_dataframe(records: list) -> pd.DataFrame:
"""carrega os dados em um dataframe"""
# https://stackoverflow.com/questions/5352546/extract-subset-of-key-value-pairs-from-python-dictionary-object/5352658
content = [{k: data.get(k, None) for k in ('legislationIdentifier', 'legislationType', 'description', 'keywords', 'datePublished')} for data in records]
df = pd.DataFrame(content)
df = alter_cols(df)
return df
df = load_into_dataframe(total_normativos)
Podemos inspecionar os dois primeiros registros dos nossos dados.
df.head(2)
O campo legislationType
nos informa o tipo de normativos existente na base de dados, abaixo apresentamos uma visualização onde podemos identificar os tipos e os respectivos quantitativos.
count_normas_by_year = df.groupby(['legislationType', 'year'])['legislationIdentifier']\
.count()\
.reset_index()\
.rename(columns={'legislationIdentifier' : 'quantitativo'})
Além disso, podemos inspecionar o campo keywords que contém múltiplas tags
associadas a mesma norma, como apresentado abaixo:
df.loc[0, 'keywords']
Nesse momento precisamos alterar a estrutura do DataFrame
para que possamos ter o nosso modelo de dados
com uma granularidade de um registro de keyword
por linha da tabela.
tags = df.explode('keywords').copy()
tags.head(2)
Com a estrutura dos dados adequada a nossa análise, determinaremos as tags
mais frequentes em nosso dataset.
def plot_topN_tags(data: pd.DataFrame, field: str, N: int) -> None:
"""Plota um gráfico de barras horizontais com as Top N tags mais frequentes"""
_ = pd.DataFrame(data)[field].value_counts()[:N]\
.reset_index()\
.rename(columns={'index' : 'keywords', 'keywords' : 'quantitativo'})
chart = alt.Chart(_)\
.mark_bar()\
.encode(
alt.X('quantitativo'),
alt.Y("keywords", sort='-x'),
tooltip='quantitativo'
)\
.properties(height=700)\
.configure_mark(color=default_color_2)
display(chart)
plot_topN_tags(tags, 'keywords', 25)
Pudemos avaliar que as TOP 25 tags
variam em ocorrências desde 16k como o caso de MUNICIPIO
até algo próximo a 2.7k para CRIAÇÃO
. Além disso, precisamos avaliar quantas tags
distintas existem em nosso dataset
.
num_tags = tags['keywords'].nunique()
Constatada o elevado número de tags
distintas utilizadas em nosso dataset, é necessário explorar a distribuição do percentual acumulado das suas respectivas frequências. Essa visão poderá nos auxiliar se, eventualmente, estamos lidando com uma distribuição de cauda longa, isto é, um número grande de tags
que foram utilizadas apenas algumas vezes ao longo do processo de anotação da base de dados.
count_clf_tags = pd.DataFrame(tags.groupby('keywords')['legislationIdentifier']\
.count()\
.sort_values(ascending=False)\
.copy())\
.rename(columns={'legislationIdentifier' : 'quantitativo'})
count_clf_tags = count_clf_tags.assign(
cumulative_sum=count_clf_tags.quantitativo.cumsum())
count_clf_tags = count_clf_tags.assign(
cumulative_perc= 100*count_clf_tags.cumulative_sum/count_clf_tags.quantitativo.sum(),
rank=range(1, count_clf_tags.shape[0]+1)
)
count_clf_tags['rank'] = count_clf_tags['rank'].astype('category')
count_clf_tags.reset_index(inplace=True)
A tabela abaixo apresenta que as 25 tags
mais frequentes são responsáveis por aproximadamente 40% de todas as ocorrências de anotação em nosso dataset. Assim, temos um indício considerável sobre uma possível distribuição de cauda longa para essa feature
.
count_clf_tags.head(25)
Dessa forma, podemos avaliar quantas tags representam 95% das ocorrências de anotação.
count_select = count_clf_tags[count_clf_tags['cumulative_perc'] <= 95].copy()
Portanto, concluímos que 33% da população de tags
do dataset são responsáveis por 95% de todas as anotações realizadas. Por fim, podemos plotar a distribuição de frequência da ocorrência dessa feature e claramente confirma o indício que trata-se de uma feature com distribuição assimétrica e com cauda longa.
chart_all_rank = alt.Chart(count_select).mark_area(
interpolate='step-after',
line=True
).encode(
alt.X('rank:O', axis=alt.Axis(values=[50, 500, 1000, 2000, 3000])),
alt.Y('quantitativo:Q'),
color=alt.value(default_color_1),
).properties(
width=400,
height=300,
title='Distribuição da frequência de ocorrência das tags'
).interactive()
chart_top100_rank = alt.Chart(count_select[:100]).mark_area(
interpolate='step-after',
line=True
).encode(
alt.X('rank:O', axis=alt.Axis(values=[10, 20, 30, 50, 75, 100])),
alt.Y('quantitativo:Q'),
tooltip='keywords',
color=alt.value(default_color_2)
).properties(
width=400,
height=300,
title='Distribuição da frequência de ocorrência das TOP 100 tags'
).interactive()
alt.vconcat(chart_all_rank, chart_top100_rank)
#chart_all_rank | chart_top100_rank
Para nossa prova de conceito iremos restringir, inicialmente, as tags
que representam 50% das ocorrências. Essa decisão restringe o quantitativo a 47 tags
, mas pela representatividade destas um percentual muito pequeno de normas serão excluídas da nossa amostragem.
top50p_tags = count_clf_tags[count_clf_tags['cumulative_perc'] <= 50.00]\
['keywords'].values\
.tolist() #lista das tags amostradas
#faz um recorte nos dados que contenham as tags selecionadas
sampling_tags = tags[tags['keywords'].isin(top50p_tags)].copy()
#numero de tags selecionadas
num_tags_sampled = sampling_tags['keywords'].nunique()
#quantitativo de normas que serão excluídas da POC
num_diff_normas = (tags['legislationIdentifier'].nunique()-sampling_tags['legislationIdentifier'].nunique())
#percentual de normas que serão excluídas da POC
percent_diff_normas = num_diff_normas/tags['legislationIdentifier'].nunique()*100
Quando apresentamos o nosso problema de aprendizado de máquina falamos que tratava-se de um tarefa de classificação multi-label, portanto, sabendo que cada norma pode receber simultaneamente mais de uma tag, uma pergunta relevante é: quantas tags por normativos costumam ser atribuídas a cada norma?
Abaixo, podemos ver que um comparativo entre o conjunto de dados originais e da amostragem que realizamos. Para o primeiro, a maior frequência de ocorrência é uma norma ter 10 registros de tags
anotadas. A quantidade elevada de anotações por normativo é algo que ao meu ver confirma o potencial de uso de classificação automática por aprendizado de máquina de modo a garantir uma maior homegeniedade e acurácia no processo de classificação.
Nessa POC decidimos restringir o treinamento inicial a um subset das tags
mais representativas e como temos uma distribuição de cauda longa é razoável que tenhamos muitos registros de tags
pouco frequentes pulverizadas em várias classificações. Mas ao reduzir o escopo de tags
em nossa amostragem espera-se uma redução no quantitativo de anotações por norma, e de fato isso aconteceu já que pudemos constatar uma redução da mediana das frequências das quantidades de tags
por normativo reduziu de 10 para 5.
Assim, definimos um recorte inicial para podermos iniciar o treinamento do nosso modelo de classificação multi-label para a nossa POC, todavia, o treinamento ficará para o próximo post. Espero que tenham gostado de conhecer essa base, tanto quanto eu 👨, até a próxima!