um produto orientado a dados governamentais: parte 2
relatos e nuances da construção dos datasets de treino, validação e teste
Esse é o terceiro 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.
- Em um produto orientado a dados governamentais: parte 1 realizamos uma análise exploratória dos dados e definimos um recorte e um escopo para os dados do projeto.
No último post delimitados um escopo para o conjunto de dados que será utilizado em nosso projeto. Portanto, iremos iniciar carregando o subset construído na etapa anterior.
sampling_tags = pd.read_csv("./data/sampling_tags.csv", sep=';')
sampling_tags.set_index('datePublished', inplace=True)
sampling_tags.sort_index(inplace=True)
sampling_tags.head(2)
Precisamos garantr que nosso conjunto de treinamento esteja restrito ao universo delimitado a normativos que possuam apenas as tags
delimitadas para a POC, Por exemplo, o Decreto 97.251 de 1988 possui as seguintes classificações:
keywords = sampling_tags['keywords'].unique()
#restringir o dataset apenas para os normativos que serão utilizados para o treinamento do modelo
sample = population[population['legislationIdentifier'].isin(sampling_tags['legislationIdentifier'])].copy()
sample.loc[0, 'keywords']
Todavia, MINISTERIO DA EDUCAÇÃO (MEC)
não está contemplado no rol de tags
delimitadas para o projeto, desse modo devemos removê-las.
set(sample.loc[0, 'keywords']).difference(keywords)
sample['keywords'] = sample['keywords'].apply(
lambda x : list(set(x).intersection(keywords)) if x else np.nan
)
Como temos um componente temporal em nosso dataset, isto é, normativos publicados e classificados ao longo de diferentes anos é válido realizarmos um verificação se, eventualmente, houve algum período se alguma das tags
não foi utilizada, por exemplo. Para isso, iremos subdividr o dataset em 4 décadas e verificaremos se para algum delas não há ocorrência de uso de alguma das tags
.
decade1 = sampling_tags["1980-01-01" : "1989-12-31"].copy()
decade2 = sampling_tags["1990-01-01" : "1999-12-31"].copy()
decade3 = sampling_tags["2000-01-01" : "2009-12-31"].copy()
decade4 = sampling_tags["2010-01-01" : "2020-12-31"].copy()
for _, decades in zip([decade1, decade2, decade3, decade4], ['80-89', '90-99', '2000-2009', '2010-2020']):
inspect = set(keywords).difference(set(_['keywords'].unique()))
if inspect:
print(f"{decades} : {inspect}")
Para década 80-89 não há registro de uso das tag :ORÇAMENTO DA SEGURIDADE SOCIAL
.
Tirando o fato que na década de 80 parecia não haver muita preocupação com destinação de recursos orçamentários para fins de seguridade social, no tocante ao nosso problema o fato temporal não se mostrou um fator relevante na hora de realizar a estratificação do dataset para fins de treinamento e avaliação.
Como bem apontado por Rachel Thomas, devemos subdividr o nosso conjunto de dados em 3 diferentes subsets: treinamento, validação e teste. O último, deve ser o mais representativo possível da realidade esperada em na "vida real". Portanto, vamos fazer uma amostragem aleatória que obedeça a distribuição de frequência das tags
existentes em nosso dataset.
Precisamos fazer uma seleção de dados para compor o dataset de teste que esteja bem próxima aos dados reais, principalmente, no que tocante a distribuição de frequência das tags
. Portanto, vamos analisar essa distribuição.
def build_frequency_of_occurrences_for_top_tags(df: pd.DataFrame) -> pd.DataFrame:
freq_dataframe = pd.DataFrame(df['keywords'].value_counts(normalize=True))\
.assign(count=df['keywords'].value_counts().values)\
.rename(columns={'keywords' : 'percent'})
freq_dataframe = freq_dataframe.assign(
frequency=freq_dataframe['percent']*freq_dataframe['count']
)
freq_dataframe['frequency'] = freq_dataframe['frequency'].astype('int')
return freq_dataframe
calculate_theoretical_test_sampling_frequency = build_frequency_of_occurrences_for_top_tags(sampling_tags)
calculate_theoretical_test_sampling_frequency.head(5)
Em seguida, podemos criar uma função para criar o nosso dataset de teste
com uma distribuição mais próxima ao desejado.
def create_test_dataset(frequency_dataset: pd.DataFrame,
sampling_dataset: pd.DataFrame) -> pd.DataFrame:
"""
Construção do dataset de test.
"""
df = sampling_dataset.copy()
container = {tag : "" for tag in df['keywords'].unique()}
consolida_ids = []
for tag in container.keys():
get_frequency = frequency_dataset.loc[tag, 'frequency']
get_id_samples = df[df['keywords'] == tag].sample(get_frequency).index.tolist()
container[tag] = get_id_samples
df = df.drop(get_id_samples)
[consolida_ids.extend(ids) for ids in container.values()]
test_dataset = sampling_dataset.loc[consolida_ids]
test_dataset = test_dataset.sort_index()
return test_dataset
test_dataset = create_test_dataset(calculate_theoretical_test_sampling_frequency, sampling_tags)
Uma vez consolidado o dataset de teste
, podemos avaliar a distribuição de frequência das tags comparado ao nosso conjunto amostral.
calculate_practical_test_sampling_frequency = build_frequency_of_occurrences_for_top_tags(test_dataset).rename(columns={
'percent': 'pract_percent',
'count' : 'pract_count',
'frequency' : 'pract_frequency'
}
)
#cria um dataframe para comparar os datasets de treino com o da amostra.
compare_theory_with_practical = pd.concat(
[calculate_theoretical_test_sampling_frequency, \
calculate_practical_test_sampling_frequency],
axis='columns'
).assign(
diff_percent=((calculate_practical_test_sampling_frequency.pract_percent-calculate_theoretical_test_sampling_frequency.percent)*100))
compare_theory_with_practical.head(2)
compare_theory_with_practical.sort_values(by='diff_percent', ascending=False)\
['diff_percent'].tail(1)
compare_theory_with_practical.sort_values(by='diff_percent', ascending=False)\
['diff_percent'].head(1)
Para a nossa amostragem realizada para construção do dataset de teste
temos uma variação positiva de +2.45% da frequência esperada para RADIODIFUSÃO
e de variação negativa -1.53% para CREDITO SUPLEMENTAR
. Consideramos esses desvios aceitáveis ao que se esperava para a distribuição das tags
no dataset original.
Por fim, selecionamos os normativos que farão parte do dataset de teste
.
sample_test = sample.loc[set(test_dataset.index)].copy()
sample_test.head(1)
Uma vez concluída a construção dos dados de teste
, precisamos construir aqueles designados para as etapas de treinamento
e validação
. Uma das formas mais comuns de realizar isso é fazer uma separação 70/30, isto é, 70%
dos dados serão destinadas para parte de treinamento
e os demais para validação
. Um das formas mais comuns, é fazer uso da função train_test_split da biblioteca scikit-learn. Iremos avaliar se essa abordagem de divisão do dataset mediante o uso da função train_test_split
é satisfatatória para nosso caso, isto é, esperamos que após o split tenhamos uma distribuição de frequências das tags
similar ao nosso conjunto amostral.
Em seguida, excluímos os normativos que fazem parte do dataset de teste
e podemos dar prosseguimento para a delimitação dos datasets de treino
e validação
.
sample_treino_validacao = sample.drop(test_dataset.index)
Y_sample_treino_validacao = sample_treino_validacao.drop(['legislationType', 'description', 'datePublished'], axis='columns')
X_sample_treino_validacao = sample_treino_validacao.drop(['keywords'], axis='columns')
Por fim, podemos fazer uma chamada a função train_test_split
com intuito de separar nosso dataset na proporção de 70%
dos dados para treino e 30%
para validação.
xTrain, xValidate, yTrain, yValidate = train_test_split(
X_sample_treino_validacao,
Y_sample_treino_validacao,
test_size = 0.3,
random_state = 42)
Vamos inspecionar se para cada um dos conjuntos de dados, foram contemplados com as 47 tags
que compõe o escopo da nossa POC.
yTrain.explode('keywords')['keywords'].nunique(), yValidate.explode('keywords')['keywords'].nunique()
O primeiro requisito que é a presença de todas as tags
está cumprido e em seguida podemos realizar uma avaliação da distribuição de frequência destas em ambos os datasets
. Primeiramente, vamos calcular a frequência relativa.
y_train_percentage = pd.DataFrame(yTrain.explode('keywords')['keywords']\
.value_counts(normalize=True))\
.rename(columns={'keywords': 'y_train_percentage'})
y_train_percentage.index.name = 'keywords'
y_validate_percentage = pd.DataFrame(yValidate.explode('keywords')['keywords']\
.value_counts(normalize=True))\
.rename(columns={'keywords': 'y_validate_percentage'})
y_validate_percentage.index.name = 'keywords'
Depois podemos realizar um cálculo para conhecer a diferença das frequências relativas das tags
do dataset de treino
e validação
com aqueles da nossa amostragem.
analyze_keywords_distribution = y_train_percentage.join(y_validate_percentage, how='left')\
.join(pd.DataFrame(calculate_theoretical_test_sampling_frequency['percent'])\
.rename(columns={'percent' : 'sampling_percent'}), how='left')
analyze_keywords_distribution = analyze_keywords_distribution.assign(
diff_train_to_sample=(analyze_keywords_distribution.y_train_percentage-analyze_keywords_distribution.sampling_percent)*100)\
.assign(
diff_validate_to_sample=(analyze_keywords_distribution.y_validate_percentage-analyze_keywords_distribution.sampling_percent)*100)\
.reset_index()
O gráfico de barras abaixo, mostra a diferença encontrada na frequência relativa das tags
do conjunto de treino
quando comparado aos dados amostrais
. Assim, é possível concluir que a variação absoluta não foi superior a 0.8%.
alt.Chart(analyze_keywords_distribution, title='Diferença de frequências das keywords do conjunto de treino para o conjunto amostral.').mark_bar().encode(
alt.X("keywords"),
alt.Y("diff_train_to_sample:Q", title='diferença de frequência / %'),
color=alt.condition(
alt.datum.diff_train_to_sample > 0,
alt.value(default_color_1), # The positive color
alt.value(default_color_2), # The negative color
),
tooltip=['keywords', 'diff_train_to_sample']
).interactive().properties(width=800)
De forma similar, abaixo temos o mesmo gráfico com comparativo entre o dataset de validação
e o conjunto de dados amostrais
. Assim, de forma análoga a sua variação absoluta também não foi superior a 0.8%.
alt.Chart(analyze_keywords_distribution, title='Diferença de frequências das keywords do conjunto de validação para o conjunto amostral.').mark_bar().encode(
alt.X("keywords"),
alt.Y("diff_validate_to_sample:Q", title='diferença de frequência / %'),
color=alt.condition(
alt.datum.diff_train_to_sample > 0,
alt.value(default_color_1), # The positive color
alt.value(default_color_2) # The negative color
),
tooltip=['keywords', 'diff_validate_to_sample']
).interactive().properties(width=800)
Consideramos ambos os desvios excelentes, portanto, concluímos que o uso do test_train_split
satisfatório. Por fim, e não menos importante, vamos inspecionar se não houve nenhum erro no processamento e se os três conjuntos de dados construídos não compartilham de nenhum normativo em comum.
set(sample_test.index).intersection(xTrain.index).intersection(xValidate.index)
Por conseguinte, com todas as verificações razóaveis tendo sido realizadas e validadas, podemos persistir os dados para as etapas futuras do trabalho.
sample_test.drop(['keywords'], axis='columns').to_csv("./data/xTest.csv", sep=';')
sample_test.drop(['legislationType', 'description', 'datePublished'], axis='columns').to_csv("./data/yTest.csv", sep=';')
xTrain.to_csv("./data/xTrain.csv", sep=';')
xValidate.to_csv("./data/xValidate.csv", sep=';')
yTrain.to_csv("./data/yTrain.csv", sep=';')
yValidate.to_csv("./data/yValidate.csv", sep=';')
Assim, concluímos a construção dos conjuntos de dados necessários para o treinamento do modelo de aprendizado de máquina. No próximo post, iremos começar a etapa de treinamento 👨💻! Até mais!