Como tratar dados com formatação internacional no Python

imagem: Jason Leung & Tyler Easton / Unsplash

Um inconveniente frequente ao tratar dados de diversas fontes internacionais é como lidar com as diferenças entre as várias línguas e culturas representam os seus separadores decimais e de milhares, a ordem de ano, mês e dia nas datas, etc. Muitos países vão da menor (dia) à maior (ano) unidade de tempo, enquanto que alguns, como os E.U.A., fazem a coisa estranha que é começar no meio (mês), então ir para o menor (dia) e enfim inverter completamente a direção e ir para a maior unidade (ano).

Se você olhar para os separadores decimais, parece que mais ou menos a metade do mundo usa pontos e a outra metade usa vírgulas. O separador de milhares é o outro marcador. Isto é, em países que usam o ponto como separador decimal, a vírgula é o dos milhares e vice versa.

Mapa mundi mostrando lugares por tipo de separador decimal

Separador decimal em cada lugar:
   Ponto (.)
   Vírgula (,)
   Ambos (pode variar dependendo do local ou outros fatores)
   Separador decimal arábico (٫)
   Dados não disponíveis
Mapa feito por NuclearVacuum na Wikipedia

Interpretando números no Python, da maneira ingênua

O que as pessoas fazem frequentemente ao interpretar esses números com Python é simplesmente usar o método replace da classe str.

In [1]: number = '12,75'

In [2]: parsed = float(number.replace(',', '.'))

In [3]: parsed
Out[3]: 12.75

Mas e quando você também tem que levar em consideração os diferentes separadores de milhares? E se você tiver um símbolo monetário antes ou depois do número? As coisas ficam complicadas a ponto de deixar o código pouco legível.

In [1]: price = 'R$ 1.999,99'

In [2]: parsed = float(
   ...:     price
   ...:     .replace('R$', '')
   ...:     .strip()
   ...:     .replace('.', '')
   ...:     .replace(',', '.')
   ...: )

In [3]: parsed
Out[3]: 1999.99

Código assim também é frágil ao lidar com variabilidades na entrada.

Interpretando datas no Python

Há situações em que você irá encontrar datas em fontes de dados que estão formatadas em ordens diferentes, às vezes até com nomes de meses e abreviaturas, como: 4-Feb-2021. O Python vem com um módulo chamado datetime na biblioteca padrão, que é bastante útil para lidar com datas em praticamente qualquer formato que você seja capaz de encontrar: simplesmente use o método strptime da classe datetime. Apenas veja a tabela de códigos de formatos e construa a sua máscara de formato. Aqui estão os códigos de formato relevantes para o nosso exemplo:

Diretiva Significado Exemplo
%d Dia do mês como um número decimal preenchido por zero. 01, 02, …, 31
%b Mês como o nome abreviado da localidade. Jan, Feb, …, Dec (en_US);
Jan, Feb, …, Dez (de_DE)
%Y Ano com o século, como um número decimal. 0001, 0002, …, 2013, 2014, …, 9998, 9999

Note que a documentação diz que, ao usar o strptime, o preenchimento de zeros no %d é opcional.

Então faça

In [1]: from datetime import datetime

In [2]: written_date = '4-Feb-2021'

In [3]: parsed = datetime.strptime(written_date, '%d-%b-%Y').date()

In [4]: parsed
Out[4]: datetime.date(2021, 2, 4)

Essa abordagem também pode ser usada nos casos em que a ordem dos componente da data é diferente. Apenas reordene a máscara de formato e use o strptime.

Mas e se a data que você quer interpretar tem componentes em idiomas diferentes (por exemplo, em português)? Você usa dicionários com os nomes dos meses nessas línguas?

french_months = {
  'janvier': 1,
  'février': 2,
  # meu Deus, por favor, não, pare! 😖 Arrête tout de suite ! 🤢
}

Por favor não faça isso.

A solução apropriada, de fato

Tudo isso nos faz desejar que houvesse uma maneira padrão no Python de lidar com todas essas diferenças em formatos de números e datas. E há! Conheça o módulo locale. Ele também é parte da biblioteca padrão do Python, mas é tão frequentemente ignorado em código que tenho visto nos meios de ciência e engenharia de dados que me causa vergonha alheia ver coisas como os exemplos deliberadamente ruins acima.

Configurando as localidades

Antes de usar as localidades que você precisa no Python, todavia, você precisa instalar essas localidades no seu sistema. O motivo para isso é que o Python usa a base de dados de localidades do POSIX por trás dos panos. A maneira de fazer isso varia, a depender do seu sistema operacional e versão, mas se você está usando o Debian ou um sistema baseado em Debian como o Ubuntu, então o seguinte deve funcionar.

  1. Se já não tiver feito isso, instale o módulo locales no seu sistema.

    sudo apt-get install locales
    
  2. Procure e edite o arquivo /etc/locale.gen no seu sistema. Isso provavelmente necessitará de privilégios de administrador. Encontre as localidades que você quer usar, descomente essas linhas e salve.

    Alternativamente, você pode usar o sed para editar as linhas no próprio lugar, assim,

    sed -i 's/^# pt_BR.UTF-8 UTF-8$/pt_BR.UTF-8 UTF-8/g' /etc/locale.gen
    sed -i 's/^# fr_FR.UTF-8 UTF-8$/fr_FR.UTF-8 UTF-8/g' /etc/locale.gen
    

    por exemplo, para habilitar as configurações de localidade para o francês.

  3. Rode o locale-gen para gerar as localidades que você escolheu. Por exemplo, se você quer ter disponíveis inglês dos E.U.A., francês e português do Brasil, você faria

    locale-gen en_US.UTF-8 fr_FR.UTF-8 pt_BR.UTF-8
    
  4. Finalmente, atualize as localidades. Aqui você não precisa listar todas elas, apenas a padrão.

    update-locale LANG=pt_BR.UTF-8 LC_ALL=pt_BR.UTF-8
    

Você só precisa fazer essa configuração uma vez para cada vez que você quiser adicionar ou remover uma localidade.

Usando o módulo locale

Agora que o sistema tem as localidades que queremos usar, precismos ajustá-las no Python antes de processar esses números.

In [1]: import locale
   ...: locale.setlocale(locale.LC_ALL, 'pt_BR.UTF-8')
Out[1]: 'pt_BR.UTF-8'

Se estiver configurado corretamente, você não deve ver nenhum erro aqui. Caso veja uma mensagem “Error: unsupported locale setting”, ou equivalente, volte e reconfigure as localidades e certifique-se de ter habilitado aquela localidade.

Agora você pode trocar de localidades e tanto interpretar quanto formatar números e datas da maneira apropriada para aquela localidade. Você também pode trocar entre localidades sempre que necessário.

In [1]: import locale
   ...: locale.setlocale(locale.LC_ALL, 'pt_BR.UTF-8')
Out[1]: 'pt_BR.UTF-8'

In [2]: locale.currency(0.5)
Out[2]: 'R$ 0,50'

In [3]: locale.currency(1000.5, grouping=True) # separador de milhar
Out[3]: 'R$ 1.000,50'

In [4]: print('meia unidade: ' + locale.format_string('%.2f', 0.5))
meia unidade: 0,50

In [5]: locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
Out[5]: 'en_US.UTF-8'

In [6]: print('half a unit: ' + locale.format_string('%.2f', 0.5))
half a unit: 0.50

Interpretando números em Python, da maneira apropriada

Para interpretar números de uma maneira que observe a localidade, o módulo locale oferece as funções atoi (para inteiros) e atof (para ponto flutuante).

In [1]: import locale
   ...: locale.setlocale(locale.LC_ALL, 'pt_BR.UTF-8')
Out[1]: 'pt_BR.UTF-8'

In [2]: number = '12,75'

In [3]: parsed_number = locale.atof(number)

In [4]: parsed_number
Out[4]: 12.75

In [5]: price = 'R$ 1.999,99'

In [6]: parsed_price = locale.atof(price.split()[-1])

In [7]: parsed_price
Out[7]: 1999.99

A função atof também é muito útil para converter dados no Pandas, a biblioteca ubíqua de ciência de dados em Python.

In [1]: import pandas as pd

In [2]: import locale
   ...: locale.setlocale(locale.LC_ALL, 'pt_BR.UTF-8')
Out[2]: 'pt_BR.UTF-8'

In [3]: df = pd.DataFrame(
   ...:       [
   ...:         ['banana', 'R$ 3,99'],
   ...:         ['maçã', 'R$ 4,49'],
   ...:         ['pêssego', 'R$ 8,90']
   ...:     ],
   ...:     columns=['fruta', 'preço'],
   ...: )

In [4]: df
Out[4]: 
     fruta    preço
0   banana  R$ 3,99
1     maçã  R$ 4,49
2  pêssego  R$ 8,90

In [5]: df['price'] = df['preço'].apply(lambda v: locale.atof(v.split()[-1]))

In [6]: df
Out[6]: 
     fruta    preço  price
0   banana  R$ 3,99   3.99
1     maçã  R$ 4,49   4.49
2  pêssego  R$ 8,90   8.90

Observe que a atof consegue inclusive converter para floats strings contendo números em notação científica:

In [7]: locale.atof('1E+10')
Out[7]: 10000000000.0

Interpretando datas

O módulo datetime, assim como toda a biblioteca padrão do Python, observa o locale. Por isso, é fácil interpretar datas com os números em ordem diferente ou com os nomes dos meses.

In [1]: import locale
   ...: locale.setlocale(locale.LC_ALL, 'pt_BR.UTF-8')
Out[1]: 'pt_BR.UTF-8'

In [2]: from datetime import datetime

In [3]: written_date = '1-Mai-2021'

In [4]: parsed = datetime.strptime(written_date, '%d-%b-%Y').date()

In [5]: parsed
Out[5]: datetime.date(2021, 5, 1)

Conclusão

Usar o módulo locale é uma habilidade essencial para muitas tarefas em engenharia de dados e ciência de dados quando dados internacionais estão envolvidos. Vale a pena adquirir prática com ele, já que o seu processamento dos dados se tornará mais robusto e evitará erros comuns ao fazer a limpeza dos dados.

Nota: editado em 2022-02-14 para incluir exemplos usando moedas com separadores de milhar e convertendo números em notação científica. Obrigado pelas suas sugestões, Washington e Salomão!