Tecnologia

Pandas na prática: como criar e manipular um DataFrame com dados reais

Aprenda os fundamentos do Pandas criando um DataFrame na prática com dados reais de qualidade do ar. Veja estatísticas, filtros e manipulação de linhas e colunas.

Capa do artigo “Pandas na prática: como criar e manipular um DataFrame com dados reais”, com mapa digital de São Paulo e paisagem urbana noturna.

Pandas na prática: como criar e manipular um DataFrame com dados reais#

No artigo anterior, usamos a biblioteca Requests para consultar dados de qualidade do ar por meio de uma API. Conseguimos enviar parâmetros, verificar erros HTTP e converter a resposta recebida em estruturas do Python.

Se ainda não leu o último artigo veja Como consumir uma API com Python usando Requests: guia prático

Agora, vamos avançar para a etapa seguinte: transformar esses dados em uma tabela e explorá-los com o Pandas.

Neste guia, você aprenderá a:

  • criar um DataFrame;
  • entender linhas, colunas, índice e tipos de dados;
  • usar head(), info() e describe();
  • calcular média, mediana e desvio-padrão;
  • selecionar linhas e colunas;
  • usar loc e iloc;
  • criar filtros;
  • criar, alterar, renomear e remover colunas;
  • ordenar os dados.

Usaremos previsões de qualidade do ar para a cidade de São Paulo. O projeto poderá ser executado localmente com o gerenciador de dependências uv ou diretamente no Google Colab.

O que é o Pandas?#

Pandas é uma biblioteca de código aberto voltada à análise e à manipulação de dados em Python. Ela fornece estruturas que facilitam o trabalho com informações organizadas em linhas e colunas. ([Pandas][1])

As duas estruturas fundamentais da biblioteca são o DataFrame e a Series.

Um DataFrame é uma estrutura bidimensional. Ele se parece com uma tabela, pois possui linhas e colunas.

Uma Series é uma estrutura unidimensional. Na prática, cada coluna de um DataFrame é uma Series. Quando selecionamos uma única coluna, o Pandas devolve uma Series; quando selecionamos várias colunas, o resultado é outro DataFrame. ([Pandas][2])

Importaremos a biblioteca usando a convenção mais comum:

PYTHON
import pandas as pd

O nome pd é apenas um apelido que evita escrever pandas repetidamente.

Sobre os dados utilizados#

Usaremos a Air Quality API da Open-Meteo para obter previsões de:

  • material particulado PM10;
  • material particulado PM2.5;
  • dióxido de nitrogênio;
  • ozônio;
  • índice europeu de qualidade do ar.

A resposta da API contém um objeto hourly. Dentro dele, cada variável é representada por uma lista de valores, acompanhada por uma lista de horários. A resposta também contém hourly_units, que informa as unidades de cada variável. ([Open Meteo][3])

Para PM10, PM2.5, dióxido de nitrogênio e ozônio, as concentrações são expressas em microgramas por metro cúbico, representados por μg/m³. A documentação descreve essas variáveis como concentrações próximas à superfície, aproximadamente 10 metros acima do solo. ([Open Meteo][3])

Esses dados não são medições de uma estação#

Os valores utilizados neste artigo são produzidos por modelos atmosféricos. Eles não representam necessariamente medições feitas por um sensor localizado exatamente nas coordenadas consultadas.

Para São Paulo, a fonte relevante é o CAMS Global Atmospheric Composition Forecast, que possui resolução espacial aproximada de 45 quilômetros e resolução temporal nativa de três horas. O modelo é atualizado a cada 12 horas e fornece até cinco dias de previsão. A API organiza os resultados solicitados em uma série horária. ([Open Meteo][3])

Isso significa que os dados são adequados para aprendizado, exploração e demonstrações de análise, mas não devem ser apresentados como leituras de uma estação local ou usados automaticamente para decisões regulatórias.

O campo european_aqi também merece atenção. Ele representa o índice europeu de qualidade do ar e corresponde ao maior valor entre os índices individuais calculados para diferentes poluentes. Essa classificação não deve ser confundida com índices ou normas ambientais brasileiras. ([Open Meteo][3])

Preparando o ambiente com uv#

O uv pode criar o projeto, registrar as dependências em pyproject.toml, gerenciar o ambiente virtual e manter as versões resolvidas em uv.lock. ([Astral Docs][4])

Crie um novo projeto:

BASH
uv init pandas-qualidade-ar

Entre na pasta:

BASH
cd pandas-qualidade-ar

Adicione o Pandas e o Requests:

BASH
uv add pandas requests

O comando uv add registra as bibliotecas como dependências do projeto.

Ao final do artigo, você encontrará um main.py completo. Para executá-lo:

BASH
uv run main.py

Antes de executar o programa, o uv run verifica se o ambiente e o arquivo de bloqueio estão sincronizados com as dependências declaradas no projeto. ([Astral Docs][4])

Preparando o Google Colab#

O Google Colab é um ambiente hospedado de notebooks Jupyter. Ele permite executar código Python no navegador sem configurar um ambiente local. ([Google Pesquisa][5])

Crie um novo notebook e use uma célula exclusiva para instalar as dependências:

PYTHON
%pip install -q pandas requests

Não vamos concentrar todo o notebook em uma única célula. Cada bloco de código apresentado nas próximas seções pode ser colocado em uma célula separada.

A organização recomendada será:

TEXT
Célula 1 — instalação
Célula 2 — importações e configurações
Célula 3 — função de consulta
Célula 4 — execução da requisição
Célula 5 — inspeção do JSON
Célula 6 — criação do DataFrame
Célula 7 — inspeção do DataFrame
Célula 8 — seleção de linhas e colunas
Célula 9 — estatísticas
Célula 10 — filtros
Célula 11 — criação e alteração de colunas
Célula 12 — ordenação e resultado final

Importando as bibliotecas#

Em uma nova célula do Colab ou no início do arquivo Python, importe as bibliotecas:

PYTHON
import pandas as pd
import requests

Configurando a consulta#

Vamos usar as coordenadas aproximadas da cidade de São Paulo:

PYTHON
API_URL = "https://air-quality-api.open-meteo.com/v1/air-quality"

LATITUDE = -23.5505
LONGITUDE = -46.6333

VARIAVEIS = (
    "european_aqi",
    "pm10",
    "pm2_5",
    "nitrogen_dioxide",
    "ozone",
)

Usaremos três dias de previsões. Isso gera observações suficientes para praticar filtros e estatísticas sem tornar o exemplo desnecessariamente grande.

A API permite controlar o período por meio de forecast_days, aceitando até sete dias. Quando o parâmetro não é informado, o padrão é cinco dias. ([Open Meteo][3])

Criando a função de consulta#

Em outra célula, defina a função:

PYTHON
def consultar_qualidade_ar(
    latitude: float,
    longitude: float,
) -> dict:
    """Consulta previsões horárias de qualidade do ar."""

    parametros = {
        "latitude": latitude,
        "longitude": longitude,
        "hourly": ",".join(VARIAVEIS),
        "forecast_days": 3,
        "timezone": "America/Sao_Paulo",
        "domains": "cams_global",
    }

    resposta = requests.get(
        API_URL,
        params=parametros,
        timeout=30,
    )

    resposta.raise_for_status()

    return resposta.json()

O parâmetro hourly informa quais variáveis devem ser devolvidas como séries horárias.

A expressão:

PYTHON
",".join(VARIAVEIS)

transforma a tupla em uma string:

TEXT
european_aqi,pm10,pm2_5,nitrogen_dioxide,ozone

O parâmetro timezone solicita horários no fuso America/Sao_Paulo. Segundo a documentação da API, quando um fuso é informado, os horários são devolvidos no horário local e os dados começam à meia-noite local. ([Open Meteo][3])

Executando a requisição#

Em uma nova célula:

PYTHON
try:
    dados = consultar_qualidade_ar(
        latitude=LATITUDE,
        longitude=LONGITUDE,
    )

except requests.exceptions.Timeout as erro:
    raise RuntimeError(
        "A API demorou muito para responder."
    ) from erro

except requests.exceptions.HTTPError as erro:
    raise RuntimeError(
        f"A API devolveu um erro HTTP: {erro}"
    ) from erro

except requests.exceptions.JSONDecodeError as erro:
    raise RuntimeError(
        "A resposta não contém um JSON válido."
    ) from erro

except requests.exceptions.RequestException as erro:
    raise RuntimeError(
        f"Não foi possível consultar a API: {erro}"
    ) from erro

Em um notebook, lançar uma exceção interrompe a execução. Isso evita que as células seguintes tentem usar uma variável dados que não foi criada corretamente.

Inspecionando a resposta#

Antes de criar o DataFrame, devemos observar a estrutura recebida.

PYTHON
dados.keys()

A resposta deverá conter chaves semelhantes a:

TEXT
dict_keys([
    'latitude',
    'longitude',
    'generationtime_ms',
    'utc_offset_seconds',
    'timezone',
    'timezone_abbreviation',
    'elevation',
    'hourly_units',
    'hourly'
])

Agora, inspecione as chaves dos dados horários:

PYTHON
dados["hourly"].keys()

O resultado será semelhante a:

TEXT
dict_keys([
    'time',
    'european_aqi',
    'pm10',
    'pm2_5',
    'nitrogen_dioxide',
    'ozone'
])

Também podemos visualizar as unidades:

PYTHON
dados["hourly_units"]

A estrutura de hourly é adequada à criação de um DataFrame: cada chave será transformada em uma coluna, enquanto os elementos de mesma posição nas listas formarão uma linha. A documentação da API especifica que os horários são devolvidos em uma lista e cada variável em um vetor de valores. ([Open Meteo][3])

Criando o DataFrame#

Agora podemos criar a tabela:

PYTHON
df = pd.DataFrame(dados["hourly"])

No Colab, deixe df como última expressão da célula:

PYTHON
df

Isso produz uma visualização tabular mais adequada do que:

PYTHON
print(df)

Cada linha representa um horário. Cada coluna representa uma variável.

Visualizando as primeiras linhas#

Para visualizar as cinco primeiras linhas:

PYTHON
df.head()

Para visualizar as dez primeiras:

PYTHON
df.head(10)

Para visualizar as últimas:

PYTHON
df.tail()

O método head() devolve as primeiras linhas e é útil para verificar rapidamente se os dados possuem o formato esperado. ([Pandas][6])

Conhecendo o tamanho do DataFrame#

Use o atributo shape:

PYTHON
df.shape

O resultado possui o formato:

TEXT
(quantidade_de_linhas, quantidade_de_colunas)

Por exemplo:

TEXT
(72, 6)

O valor exato deve ser verificado na execução, mas três dias de valores horários normalmente produzem 72 registros.

Observe que shape é um atributo, não um método. Portanto, não usamos parênteses.

Conhecendo as colunas e o índice#

Para visualizar os nomes das colunas:

PYTHON
df.columns

Para visualizar o índice:

PYTHON
df.index

O índice padrão geralmente começa em zero:

TEXT
RangeIndex(start=0, stop=72, step=1)

O índice identifica as linhas do DataFrame. Ele não precisa ser uma coluna dos dados.

Verificando os tipos#

Use:

PYTHON
df.dtypes

Inicialmente, a coluna time provavelmente será apresentada como str ou object, pois os horários chegaram no JSON como texto.

Para obter um resumo mais completo:

PYTHON
df.info()

O método info() mostra as colunas, seus tipos, a quantidade de valores não ausentes e informações sobre uso de memória. ([Pandas][7])

Convertendo a coluna de data e hora#

Para trabalhar corretamente com datas, converta time:

PYTHON
df["time"] = pd.to_datetime(df["time"])

A função pd.to_datetime() converte valores escalares, listas, arrays ou Series em objetos de data e hora do Pandas. ([Pandas][8])

Confirme a alteração:

PYTHON
df.dtypes

Agora a coluna deverá apresentar um tipo semelhante a:

TEXT
datetime64[us]

O tipo exato pode variar conforme a versão e o ambiente.

Selecionando uma coluna#

Para selecionar somente PM2.5:

PYTHON
df["pm2_5"]

Verifique o tipo:

PYTHON
type(df["pm2_5"])

O resultado será uma Series:

TEXT
pandas.Series

Cada coluna de um DataFrame é uma Series. ([Pandas][2])

Selecionando várias colunas#

Para selecionar data, PM2.5 e PM10:

PYTHON
df[["time", "pm2_5", "pm10"]]

Agora o resultado é outro DataFrame:

PYTHON
type(df[["time", "pm2_5", "pm10"]])

Resultado:

TEXT
pandas.DataFrame

Ao selecionar várias colunas, usamos uma lista dentro dos colchetes externos. ([Pandas][2])

Selecionando linhas com iloc#

O iloc trabalha com posições inteiras. A contagem começa em zero. ([Pandas][9])

Selecione a primeira linha:

PYTHON
df.iloc[0]

Selecione as cinco primeiras linhas:

PYTHON
df.iloc[:5]

Selecione as cinco primeiras linhas e as três primeiras colunas:

PYTHON
df.iloc[:5, :3]

No último exemplo:

TEXT
df.iloc[linhas, colunas]

O primeiro seletor controla as linhas e o segundo controla as colunas.

Como os intervalos seguem as regras comuns do Python, :5 seleciona as posições de zero a quatro.

Selecionando linhas e colunas com loc#

O loc trabalha principalmente com rótulos do índice e nomes de colunas. Ele também aceita condições booleanas. ([Pandas][10])

Selecione a linha cujo índice é zero:

PYTHON
df.loc[0]

Selecione um valor específico:

PYTHON
df.loc[0, "pm2_5"]

Selecione as linhas de índice zero até quatro e duas colunas:

PYTHON
df.loc[0:4, ["time", "pm2_5"]]

Há uma diferença importante: no loc, o limite final do intervalo é incluído. Portanto, 0:4 inclui a linha de índice quatro. ([Pandas][10])

Calculando a média#

A média do PM2.5 pode ser calculada assim:

PYTHON
media_pm25 = df["pm2_5"].mean()

media_pm25

Para arredondar o resultado:

PYTHON
round(media_pm25, 2)

Também podemos calcular a média de várias colunas:

PYTHON
colunas_concentracao = [
    "pm10",
    "pm2_5",
    "nitrogen_dioxide",
    "ozone",
]

df[colunas_concentracao].mean().round(2)

O resultado é uma Series contendo a média de cada coluna.

Calculando a mediana#

A mediana é o valor central dos dados após sua ordenação:

PYTHON
df["pm2_5"].median()

A mediana pode ser menos influenciada por valores extremos do que a média.

Calculando o desvio-padrão#

Use:

PYTHON
df["pm2_5"].std()

Para várias colunas:

PYTHON
df[colunas_concentracao].std().round(2)

Por padrão, std() calcula o desvio-padrão amostral, usando divisor N - 1. Esse comportamento pode ser alterado pelo argumento ddof. ([Pandas][11])

Para calcular o desvio-padrão populacional:

PYTHON
df["pm2_5"].std(ddof=0)

Para este artigo, manteremos o comportamento padrão do Pandas.

Mínimo e máximo#

Para encontrar o menor valor:

PYTHON
df["pm2_5"].min()

Para encontrar o maior:

PYTHON
df["pm2_5"].max()

Também podemos encontrar o índice da linha com o maior PM2.5:

PYTHON
indice_maior_pm25 = df["pm2_5"].idxmax()

indice_maior_pm25

E selecionar a linha completa:

PYTHON
df.loc[indice_maior_pm25]

Resumindo os dados com describe()#

O método describe() reúne várias estatísticas:

PYTHON
df[colunas_concentracao].describe().round(2)

Para colunas numéricas, a saída normalmente contém:

count: quantidade de valores não ausentes.

mean: média.

std: desvio-padrão.

min: menor valor.

25%: primeiro quartil.

50%: mediana.

75%: terceiro quartil.

max: maior valor.

A documentação define describe() como um método para gerar estatísticas descritivas de tendência central, dispersão e formato da distribuição, desconsiderando valores NaN. Por padrão, ele apresenta os percentis de 25%, 50% e 75%. ([Pandas][12])

Não incluímos european_aqi na lista de concentrações porque ele é um índice, e não uma concentração medida em μg/m³.

Criando um filtro básico#

Vamos selecionar os horários em que o PM2.5 está acima da média do período.

Primeiro, crie a condição:

PYTHON
media_pm25 = df["pm2_5"].mean()

filtro_pm25 = df["pm2_5"] > media_pm25

Visualize a condição:

PYTHON
filtro_pm25.head()

O resultado é uma Series de valores booleanos:

TEXT
0    False
1    False
2     True
3     True
4    False

Agora aplique o filtro:

PYTHON
df_acima_media = df.loc[filtro_pm25].copy()

df_acima_media.head()

Usamos .copy() porque pretendemos tratar o resultado como um novo conjunto de dados independente.

Filtrando e selecionando colunas#

Podemos aplicar o filtro e escolher apenas algumas colunas:

PYTHON
df.loc[
    df["pm2_5"] > media_pm25,
    ["time", "pm2_5", "pm10"],
]

Esse comando combina:

  • seleção por condição;
  • seleção de linhas;
  • seleção de colunas.

Combinando condições#

Para selecionar horários em que PM2.5 e ozônio estão acima de suas respectivas médias:

PYTHON
media_ozonio = df["ozone"].mean()

filtro_combinado = (
    (df["pm2_5"] > media_pm25)
    & (df["ozone"] > media_ozonio)
)

df.loc[
    filtro_combinado,
    ["time", "pm2_5", "ozone"],
]

O operador & representa “e”.

Para selecionar registros que atendam a pelo menos uma das condições:

PYTHON
filtro_alternativo = (
    (df["pm2_5"] > media_pm25)
    | (df["ozone"] > media_ozonio)
)

df.loc[filtro_alternativo]

O operador | representa “ou”.

Cada condição deve ficar entre parênteses.

Filtrando um intervalo#

O método between() facilita filtros entre dois valores:

PYTHON
df.loc[
    df["european_aqi"].between(20, 40),
    ["time", "european_aqi"],
]

Esse exemplo seleciona valores do índice europeu entre 20 e 40.

A classificação pertence ao índice europeu e não deve ser tratada como uma classificação oficial brasileira. Segundo a Open-Meteo, o intervalo acima de 20 até 40 corresponde à categoria europeia “fair”, geralmente traduzida como razoável. ([Open Meteo][3])

Criando novas colunas#

Podemos criar uma coluna contendo apenas a data:

PYTHON
df["data"] = df["time"].dt.date

Outra com a hora:

PYTHON
df["hora"] = df["time"].dt.hour

Visualize o resultado:

PYTHON
df[["time", "data", "hora"]].head()

Também podemos criar uma coluna com base em um cálculo:

PYTHON
df["diferenca_pm"] = df["pm10"] - df["pm2_5"]

Ou uma coluna booleana:

PYTHON
df["pm2_5_acima_da_media"] = (
    df["pm2_5"] > media_pm25
)

Confira:

PYTHON
df[
    [
        "time",
        "pm10",
        "pm2_5",
        "diferenca_pm",
        "pm2_5_acima_da_media",
    ]
].head()

Alterando valores de uma coluna#

Podemos arredondar todas as concentrações:

PYTHON
df[colunas_concentracao] = (
    df[colunas_concentracao].round(2)
)

Nesse caso, estamos substituindo as colunas existentes por suas versões arredondadas.

Também podemos criar uma coluna de texto e alterar apenas determinadas linhas:

PYTHON
df["periodo"] = "noturno"

df.loc[
    df["hora"].between(6, 17),
    "periodo",
] = "diurno"

Agora:

PYTHON
df[["time", "hora", "periodo"]].head(20)

A primeira atribuição preenche toda a coluna com "noturno". Depois, o loc altera somente as linhas cuja hora está entre 6 e 17.

Renomeando colunas#

O método rename() pode receber um dicionário com os nomes antigos e novos:

PYTHON
df_renomeado = df.rename(
    columns={
        "time": "data_hora",
        "nitrogen_dioxide": "no2",
        "ozone": "o3",
    }
)

Confira:

PYTHON
df_renomeado.columns

Por padrão, rename() devolve um novo objeto. Os rótulos que não aparecem no dicionário permanecem inalterados. ([Pandas][13])

Removendo colunas#

Para remover uma coluna:

PYTHON
df_sem_diferenca = df.drop(
    columns=["diferenca_pm"]
)

Para remover várias:

PYTHON
df_reduzido = df.drop(
    columns=[
        "diferenca_pm",
        "pm2_5_acima_da_media",
        "periodo",
    ]
)

O método drop() remove linhas ou colunas por seus rótulos. Como seu comportamento padrão devolve um novo DataFrame, armazenamos o resultado em outra variável. ([Pandas][14])

Removendo linhas#

Para remover a primeira linha pelo índice:

PYTHON
df_sem_primeira_linha = df.drop(
    index=df.index[0]
)

Isso não altera o df original.

Para manter a alteração:

PYTHON
df = df.drop(
    index=df.index[0]
)

Em análises reais, uma linha não deve ser removida apenas por estar no início ou no fim. O exemplo serve para demonstrar a sintaxe.

Ordenando os dados#

Para ordenar do menor para o maior PM2.5:

PYTHON
df.sort_values(
    by="pm2_5",
)

Para colocar os maiores valores primeiro:

PYTHON
df_ordenado = df.sort_values(
    by="pm2_5",
    ascending=False,
)

df_ordenado.head()

O método sort_values() ordena os registros de acordo com uma ou mais colunas. O argumento ascending=False solicita ordem decrescente. ([Pandas][15])

Também podemos usar:

PYTHON
df.nlargest(
    5,
    "pm2_5",
)

Esse comando devolve as cinco linhas com maiores valores de PM2.5.

Resultado final da análise#

Vamos criar uma tabela com os dez horários de maior PM2.5:

PYTHON
maiores_valores_pm25 = (
    df.loc[
        :,
        [
            "time",
            "pm2_5",
            "pm10",
            "ozone",
            "european_aqi",
        ],
    ]
    .sort_values(
        by="pm2_5",
        ascending=False,
    )
    .head(10)
)

maiores_valores_pm25

Também podemos apresentar o resumo estatístico:

PYTHON
resumo_estatistico = (
    df[colunas_concentracao]
    .describe()
    .round(2)
)

resumo_estatistico

Assim, terminamos o exercício com dois resultados:

  • os dez horários com maior previsão de PM2.5;
  • um resumo estatístico das concentrações consultadas.

Código completo para projeto com uv#

Substitua o conteúdo de main.py pelo código abaixo:

PYTHON
import pandas as pd
import requests

API_URL = "https://air-quality-api.open-meteo.com/v1/air-quality"

LATITUDE = -23.5505
LONGITUDE = -46.6333

VARIAVEIS = (
    "european_aqi",
    "pm10",
    "pm2_5",
    "nitrogen_dioxide",
    "ozone",
)

COLUNAS_CONCENTRACAO = [
    "pm10",
    "pm2_5",
    "nitrogen_dioxide",
    "ozone",
]

def consultar_qualidade_ar(
    latitude: float,
    longitude: float,
) -> dict:
    """Consulta previsões horárias de qualidade do ar."""

    parametros = {
        "latitude": latitude,
        "longitude": longitude,
        "hourly": ",".join(VARIAVEIS),
        "forecast_days": 3,
        "timezone": "America/Sao_Paulo",
        "domains": "cams_global",
    }

    resposta = requests.get(
        API_URL,
        params=parametros,
        timeout=30,
    )

    resposta.raise_for_status()

    return resposta.json()

def criar_dataframe(dados: dict) -> pd.DataFrame:
    """Transforma os dados horários em um DataFrame."""

    if "hourly" not in dados:
        raise KeyError(
            "A resposta não contém a chave 'hourly'."
        )

    df = pd.DataFrame(dados["hourly"])

    df["time"] = pd.to_datetime(df["time"])

    return df

def preparar_dataframe(
    df: pd.DataFrame,
) -> pd.DataFrame:
    """Cria colunas auxiliares para a análise."""

    df = df.copy()

    media_pm25 = df["pm2_5"].mean()

    df["data"] = df["time"].dt.date
    df["hora"] = df["time"].dt.hour

    df["diferenca_pm"] = (
        df["pm10"] - df["pm2_5"]
    )

    df["pm2_5_acima_da_media"] = (
        df["pm2_5"] > media_pm25
    )

    df["periodo"] = "noturno"

    df.loc[
        df["hora"].between(6, 17),
        "periodo",
    ] = "diurno"

    df[COLUNAS_CONCENTRACAO] = (
        df[COLUNAS_CONCENTRACAO].round(2)
    )

    return df

def exibir_resultados(
    df: pd.DataFrame,
    dados: dict,
) -> None:
    """Exibe informações e resultados da análise."""

    print("Dados de qualidade do ar")
    print("=" * 50)

    print(
        "Célula do modelo:",
        dados["latitude"],
        dados["longitude"],
    )

    print("Fuso horário:", dados["timezone"])
    print("Dimensões do DataFrame:", df.shape)

    print("\nTipos das colunas:")
    print(df.dtypes)

    print("\nPrimeiras linhas:")
    print(df.head())

    print("\nMédias:")
    print(
        df[COLUNAS_CONCENTRACAO]
        .mean()
        .round(2)
    )

    print("\nDesvios-padrão:")
    print(
        df[COLUNAS_CONCENTRACAO]
        .std()
        .round(2)
    )

    print("\nResumo estatístico:")
    print(
        df[COLUNAS_CONCENTRACAO]
        .describe()
        .round(2)
    )

    maiores_valores = (
        df.loc[
            :,
            [
                "time",
                "pm2_5",
                "pm10",
                "ozone",
                "european_aqi",
            ],
        ]
        .sort_values(
            by="pm2_5",
            ascending=False,
        )
        .head(10)
    )

    print("\nDez maiores valores de PM2.5:")
    print(maiores_valores.to_string(index=False))

def main() -> None:
    try:
        dados = consultar_qualidade_ar(
            latitude=LATITUDE,
            longitude=LONGITUDE,
        )

        df = criar_dataframe(dados)
        df = preparar_dataframe(df)

    except requests.exceptions.Timeout:
        print("A API demorou muito para responder.")

    except requests.exceptions.HTTPError as erro:
        print("A API devolveu um erro HTTP.")
        print(erro)

    except requests.exceptions.JSONDecodeError:
        print("A resposta não contém um JSON válido.")

    except requests.exceptions.RequestException as erro:
        print("Não foi possível consultar a API.")
        print(erro)

    except (KeyError, ValueError) as erro:
        print("Não foi possível preparar os dados.")
        print(erro)

    else:
        exibir_resultados(
            df=df,
            dados=dados,
        )

if __name__ == "__main__":
    main()

Execute:

BASH
uv run main.py

Exercícios para praticar#

Depois de executar o código, tente realizar estas modificações:

Consultar outra cidade#

Altere:

PYTHON
LATITUDE = -22.9068
LONGITUDE = -43.1729

As coordenadas acima apontam aproximadamente para o Rio de Janeiro.

Consultar sete dias#

Altere o parâmetro:

PYTHON
"forecast_days": 7,

A API permite até sete dias de previsão. ([Open Meteo][3])

Encontrar os cinco maiores valores de ozônio#

PYTHON
df.nlargest(
    5,
    "ozone",
)[["time", "ozone"]]

Selecionar somente o período diurno#

PYTHON
df_diurno = df.loc[
    df["periodo"] == "diurno"
].copy()

Calcular a média apenas do período diurno#

PYTHON
df_diurno[
    COLUNAS_CONCENTRACAO
].mean().round(2)

Selecionar o primeiro dia da previsão#

PYTHON
primeira_data = df["data"].min()

df_primeiro_dia = df.loc[
    df["data"] == primeira_data
].copy()

O que aprendemos#

Neste artigo, transformamos a resposta de uma API em um DataFrame e exploramos os principais recursos introdutórios do Pandas.

Aprendemos a:

  • criar um DataFrame a partir de um dicionário;
  • inspecionar linhas, colunas, tipos e dimensões;
  • converter texto em data e hora;
  • diferenciar Series e DataFrame;
  • selecionar dados com colchetes, loc e iloc;
  • calcular média, mediana, mínimo, máximo e desvio-padrão;
  • gerar estatísticas descritivas com describe();
  • aplicar filtros booleanos;
  • combinar condições;
  • criar e modificar colunas;
  • remover linhas e colunas;
  • ordenar registros.

Também observamos uma regra essencial da análise de dados: antes de calcular estatísticas, precisamos entender o que cada variável representa.

Os valores usados aqui são previsões produzidas por um modelo atmosférico. Eles não são necessariamente medições de uma estação local, e o índice europeu de qualidade do ar não corresponde automaticamente às classificações adotadas no Brasil.

Ao publicar resultados produzidos com essa API, inclua uma atribuição clara à Open-Meteo e ao provedor CAMS, conforme solicitado na documentação. ([Open Meteo][3])

Uma atribuição simples pode ser:

Fonte dos dados: Open-Meteo Air Quality API, com dados do CAMS Global Atmospheric Composition Forecast.

No próximo artigo, podemos avançar para o tratamento de valores ausentes, duplicidades e inconsistências antes de iniciar análises mais complexas.

Referências oficiais#

  1. Pandas — documentação oficial
  2. Pandas — seleção de subconjuntos de dados
  3. Open-Meteo — Air Quality API
  4. Astral — gerenciamento de projetos com uv
  5. Google Colab — perguntas frequentes
  6. Pandas — DataFrame.head()
  7. Pandas — DataFrame.info()
  8. Pandas — pandas.to_datetime()
  9. Pandas — DataFrame.iloc
  10. Pandas — DataFrame.loc
  11. Pandas — Series.std()
  12. Pandas — DataFrame.describe()
  13. Pandas — DataFrame.rename()
  14. Pandas — DataFrame.drop()
  15. Pandas — DataFrame.sort_values()