Potenziare la tua app con la nuova funzionalità di connessioni di Streamlit e mappe interattive di Plotly

Potenziare la tua app con connessioni di Streamlit e mappe interattive di Plotly

Aeroa: Un’app per la visualizzazione della qualità dell’aria

Immagine creata dall'autore

Introduzione

Recentemente, Streamlit ha annunciato, al momento in cui questo articolo viene scritto, la sua nuova funzionalità, st.experimental_connection, e sono molto interessato a usarla e capire come funziona. Maggiori dettagli possono essere trovati nella loro documentazione ufficiale.

Immagine di Streamlit

Allora, cos’è questa nuova funzionalità e cosa puoi fare con essa? Attraverso di essa, puoi creare una nuova connessione a un datastore o API o restituire una già esistente. Hai anche molte opzioni di configurazione, come credenziali, segreti, ecc., per le connessioni che vengono prese da varie fonti, come file di configurazione specifici della connessione e i file secrets.toml dell’app e gli argomenti passati a questa funzione. Se mi chiedi, per cose del genere, potresti costruire qualcosa da solo con Streamlit e il tuo codice (tempo richiesto), ma ora Streamlit ti offre migliori capacità con una funzionalità integrata.

Dettagli della classe di connessione

Quindi, vediamo alcuni dettagli in più sulla classe principale che questa funzionalità utilizza. Streamlit ti dà la possibilità di creare la tua classe di connessione e chiamarla all’interno della tua app. Ci sono già alcune classi di connessione integrate per SQL e Snowpark in Snowflake. È molto facile usarle, come nell’esempio per SQL qui sotto:

import streamlit as stconn = st.experimental_connection("sql")

Puoi anche fare cose più complesse, ma le discuteremo di seguito nel prossimo esempio specifico.

Crea la tua classe di connessione

Streamlit ha annunciato il suo nuovo hackathon per costruire app che ti permettono di creare le tue classi di connessione. Così ho deciso di partecipare e creare un’app semplice a causa delle restrizioni di tempo. Questa app utilizzerà dati sulla qualità dell’aria e alcune informazioni meteorologiche fornite da un’API aperta chiamata OpenAQ. Fornisce diverse informazioni per quasi ogni paese del mondo basandosi su sensori installati in aree specifiche.

Per utilizzare la suddetta API, dobbiamo creare una nuova classe di connessione. Questa classe includerà la nuova sessione della libreria requests, una query che ottiene i paesi (necessita di un piccolo codice personalizzato), una query principale che ottiene i dati specifici del paese scelto e… questo è tutto. La parte sottostante sarà inclusa in un file “connection.py”.

from streamlit.connections import ExperimentalBaseConnectionimport requestsimport streamlit as stclass OpenAQConnection(ExperimentalBaseConnection[requests.Session]):    def __init__(self, *args, **kwargs):        super().__init__(*args, **kwargs)        self._resource = self._connect(**kwargs)    def _connect(self, **kwargs) -> requests.Session:        session = requests.Session()        return session    def cursor(self):        return self._resource    def query_countries(        self, limit=100, page=1, sort="asc", order_by="name", ttl: int = 3600    ):        @st.cache_data(ttl=ttl)        def _query_countries(limit, page, sort, order_by):            params = {                "limit": limit,                "page": page,                "sort": sort,                "order_by": order_by,            }            with self._resource as s:                response = s.get("https://api.openaq.org/v2/countries", params=params)            return response.json()        return _query_countries(limit, page, sort, order_by)    def query(        self,        country_id,        limit=1000,        page=1,        offset=0,        sort="desc",        radius=1000,        order_by="lastUpdated",        dumpRaw="false",        ttl: int = 3600,    ):        @st.cache_data(ttl=ttl)        def _get_locations_measurements(            country_id, limit, page, offset, sort, radius, order_by, dumpRaw        ):            params = {                "limit": limit,                "page": page,                "offset": offset,                "sort": sort,                "radius": radius,                "order_by": order_by,                "dumpRaw": dumpRaw,            }            if country_id is not None:                params["country_id"] = country_id            with self._resource as s:                response = s.get("https://api.openaq.org/v2/locations", params=params)            return response.json()        return _get_locations_measurements(            country_id, limit, page, offset, sort, radius, order_by, dumpRaw        )

Ovviamente, all’interno di questa connessione, utilizzo @st.cache_data(ttl=ttl) per memorizzare nella cache gli output. Per capire meglio gli argomenti utilizzati per la chiamata ai diversi endpoint, consulta la documentazione API corrispondente.

Crea la funzione di visualizzazione

Per la visualizzazione, viene utilizzata la libreria plotly e in particolare la classe Scattermapbox della classe go. (la funzione di seguito è molto grande per motivi di layout e potrebbe essere suddivisa in più parti, ma perdonami):

import plotly.graph_objects as godef visualize_variable_on_map(data_dict, variable):    is_day = is_daytime()    mapbox_style = "carto-darkmatter" if not is_day else "open-street-map"    # Inizializza le liste per memorizzare i dati di più posizioni    latitudes = []    longitudes = []    values = []    display_names = []    last_updated = []    # Scorri i risultati ed estrai i dati rilevanti per ogni posizione    for result in data_dict.get("results", []):        measurements = result.get("parameters", [])        for measurement in measurements:            if measurement["parameter"] == variable:                value = measurement["lastValue"]                display_name = measurement["displayName"]                latitude = result["coordinates"]["latitude"]                longitude = result["coordinates"]["longitude"]                last_updated_value = result["lastUpdated"]                latitudes.append(latitude)                longitudes.append(longitude)                values.append(value)                display_names.append(display_name)                last_updated.append(last_updated_value)    if not latitudes or not longitudes or not values:        print(f"I dati di {variable} non sono stati trovati.")        return create_custom_markdown_card(            f"I dati di {variable} non sono stati trovati per il paese selezionato."        )    # Crea la visualizzazione    fig = go.Figure()    marker = [        custom_markers["humidity"]        if variable == "humidity"        else custom_markers["others"]    ]    # Aggiungi un'unica traccia di mappa con tutte le posizioni    fig.add_trace(        go.Scattermapbox(            lat=latitudes,            lon=longitudes,            mode="markers+text",            marker=dict(                size=20,                color=values,                colorscale="Viridis",  # Puoi scegliere altre scale di colore anche                colorbar=dict(title=f"{variable.capitalize()}"),            ),            text=[                f"{marker[0]} {display_name}: {values[i]}<br>Ultimo aggiornamento: {last_updated[i]}"                for i, display_name in enumerate(display_names)            ],            hoverinfo="text",        )    )    # Aggiorna il layout della mappa    fig.update_layout(        mapbox=dict(            style=mapbox_style,  # Scegli lo stile di mappa desiderato            zoom=5,  # Regola il livello di zoom iniziale come necessario            center=dict(                lat=sum(latitudes) / len(latitudes),                lon=sum(longitudes) / len(longitudes),            ),        ),        margin=dict(l=0, r=0, t=0, b=0),    )    create_custom_markdown_card(information)    st.plotly_chart(fig, use_container_width=True)

Crea l’applicazione

Il codice seguente è incluso nel nostro file “app.py”:

import streamlit as stfrom connection import OpenAQConnectionfrom utils import * # una parte di utilità personalizzata con funzioni di supportost.set_page_config(page_title="OpenAQ Connection", layout="wide")conn = st.experimental_connection("openaq", type=OpenAQConnection)# nel caso in cui tu abbia un file readme tomlreadme = load_config("config_readme.toml")# Infost.title("Dati sulla qualità dell'aria")with st.expander("Che cos'è questa app?", expanded=False):    st.write(readme["app"]["app_intro"])    st.write("")st.write("")st.sidebar.image(load_image("logo.png"), use_column_width=True)display_links(readme["links"]["repo"], readme["links"]["other_link"])with st.spinner("Caricamento dei paesi disponibili..."):    # I paesi esistono nelle prime 2 pagine    countries = []    for page in [1, 2]:        try:            countries_request = conn.query_countries(page=page)["results"]            countries = countries + countries_request        except Exception:            countries_error = True    transformed_countries = {        country["name"]: {            "code": country["code"],            "parameters": country["parameters"],            "locations": country["locations"],            "lastUpdated": country["lastUpdated"],        }        for country in countries    }    # Aggiungi un valore globale predefinito quando l'app viene inizializzata    transformed_countries["Global"] = {        "code": None,        "parameters": general_parameters,        "locations": None,        "lastUpdated": None,    }# Parametrist.sidebar.title("Selezione")selected_country = st.sidebar.selectbox(    "Seleziona il paese desiderato",    transformed_countries,    placeholder="Paese",    index=len(transformed_countries) - 1,  # Ottiene l'ultimo "Global"    help=readme["tooltips"]["country"],)selected_viariable = st.sidebar.selectbox(    "Seleziona la variabile desiderata",    transformed_countries[selected_country]["parameters"],    placeholder="Variabile",    index=1,    help=readme["tooltips"]["variable"],)radius = st.sidebar.slider(    "Seleziona un raggio",    min_value=100,    max_value=25000,    step=100,    value=1000,    help=readme["tooltips"]["radius"],)total_locations = transformed_countries[selected_country]["locations"]last_time = transformed_countries[selected_country]["lastUpdated"]information = f"Il paese selezionato è {selected_country}. Le posizioni trovate in totale sono {total_locations} con gli ultimi aggiornamenti a {last_time}."code = transformed_countries[selected_country]["code"]locations_response = conn.query(code, radius)st.title("Mappa")visualize_variable_on_map(locations_response, selected_viariable)

Quindi, dopo aver eseguito la nostra app “streamlit run app.py”, abbiamo la nostra app in esecuzione.

Ho chiamato l’app “AEROA” e puoi trovarla implementata nella community cloud di streamlit qui. Puoi anche trovare il codice sorgente su Github e giocarci secondo le tue preferenze.

Conclusioni

In questo breve tutorial, abbiamo mostrato la nuova funzionalità st.experimental_connection di streamlit e l’abbiamo utilizzata per stabilire una connessione con un’API aperta che fornisce dati sulla qualità dell’aria. Inoltre, abbiamo sviluppato anche una nuova app che mostra i risultati in una mappa di plotly.