- Introducció
- Entorn de treball
- Dades
- Data Frame
- Chart
- Marques i codificacions
- Canviar el tipus de marca
- Personalitzar una visualització
- Interactivity
- JSON Output
- Activitat
- TODO
Introducció
Altair és una biblioteca declarativa de visualització estadística per a Python que ofereix una gramàtica de visualització potent i concisa per crear ràpidament una àmplia gamma de gràfics estadístics.
Quan diem declarativa, volem dir que pots proporcionar una especificació d’alt nivell de què vols que inclogui la visualització, en termes de dades, marcs gràfics i canals de codificació, en lloc d’haver d’especificar com implementar la visualització amb bucles for, ordres de dibuix de baix nivell, etc.
La idea clau és que declares enllaços entre camps de dades i canals de codificació visual, com ara l’eix x, l’eix y, el color, etc. La resta de detalls del gràfic es gestionen automàticament.
A partir d’aquesta idea de traçat declaratiu, es pot crear una gamma sorprenent de visualitzacions, des de senzilles fins a sofisticades, utilitzant una gramàtica concisa.
Altair es basa en Typescript - Vega, una gramàtica d’alt nivell per a gràfics interactius, i proporciona una API de Python senzilla que genera especificacions de Vega-Lite en format Dades - JSON.
Entorn de treball
Crea un projecte amb streamlit:
uv init streamlit-basic
cd streamlit-basic
uv add polars streamlit vega-datasets
Dades
Sovint faràs servir conjunts de dades del repositori vega-datasets.
Alguns d’aquests conjunts de dades estan disponibles directament com a data frames:
import streamlit as st
import vega_datasets
@st.cache_data
def load_data():
return vega_datasets.data.cars()
cars = load_data()
st.write(cars)
Arrenca el servidor de Streamlit:
streamlit run main.py
Obre el navegador: http://localhost:8501/
Els conjunts de dades de la col·lecció vega-datasets
també es poden accedir mitjançant URLs:
st.write(vega_datasets.data.cars.url)
https://cdn.jsdelivr.net/npm/vega-datasets@v1.29.0/data/cars.json'
Les URLs dels datasets es poden passar directament a Altair (per a formats compatibles com JSON i CSV) o bé carregar-se en un data frame de Polars com es mostra a continuació:
pl.read_json(data.cars.url).head()
Per a més informació sobre els data frames —i algunes transformacions útils per preparar data frames de Polars per a representar gràfics amb Altair! — consulta la documentació Specifying Data with Altair.
Data Frame
A Altair, les dades es basen en data frames, que consisteix en un conjunt de columnes de dades amb nom.
La visualització estadística comença amb data frames "tidy"
.
Aquí començarem creant un data frame senzill (df
) que conté la precipitació mitjana (precip
) per a una ciutat i un mes determinat:
import polars as pl
import streamlit as st
df = pl.DataFrame({
'city': ['Seattle', 'Seattle', 'Seattle', 'New York', 'New York', 'New York', 'Chicago', 'Chicago', 'Chicago'],
'month': ['Apr', 'Aug', 'Dec', 'Apr', 'Aug', 'Dec', 'Apr', 'Aug', 'Dec'],
'precip': [2.68, 0.87, 5.31, 3.94, 4.13, 3.58, 3.62, 3.98, 2.56]
})
st.write(df)
Chart
L’objecte fonamental a Altair és Chart
, que rep un data frame com a únic argument:
import altair as alt
chart = alt.Chart(df)
Fins ara hem definit l’objecte Chart
i li hem passat el data frame senzill que hem generat més amunt. Encara no hem indicat al gràfic que faci res amb les dades.
Marques i codificacions
Amb un objecte gràfic, ara podem especificar com volem visualitzar les dades.
Primer indiquem quin tipus de marca gràfica (forma geomètrica) volem utilitzar per representar les dades. Podem establir l’atribut mark
de l’objecte gràfic utilitzant els mètodes Chart.mark_*
.
Per exemple, pots mostrar les dades com a punts utilitzant Chart.mark_point()
:
# ...
chart = alt.Chart(df).mark_point()
st.altair_chart(chart)
Aquí el renderitzat consisteix en un punt per fila del conjunt de dades, tots dibuixats un damunt de l’altre, ja que encara no hem especificat la posició d’aquests punts.
Per separar visualment els punts, pots mapar diversos canals de codificació, o simplement canals, a camps del conjunt de dades. Per exemple, pots codificar el camp city
de les dades utilitzant el canal y
, que representa la posició a l’eix y dels punts.
Per especificar-ho, utilitza el mètode encode
:
chart = alt.Chart(df).mark_point().encode(
y='city'
)
El mètode encode()
construeix un mapa clau-valor entre canals de codificació (com ara x
, y
, color
, shape
, size
, etc.) i camps del conjunt de dades, accedits pel seu nom.
Per als data frames, Altair determina automàticament un tipus de dada apropiat per a la columna assignada, que en aquest cas és el tipus nominal, que indica valors categorics no ordenats.
Tot i que ara hem separat les dades per un atribut, encara tenim múltiples punts sobreposats dins de cada categoria.
Separem-los encara més afegint un canal de codificació x
, mapat al camp 'precip'
:
chart = alt.Chart(df).mark_point().encode(
x='precip',
y='city'
)
El tipus de dada del camp 'precip'
torna a ser inferit automàticament per Altair, i en aquest cas es tracta com a tipus quantitatiu (és a dir, un nombre real). Veiem que també s’afegeixen automàticament línies de quadrícula i títols d’eixos adequats.
A dalt hem especificat parelles clau-valor utilitzant arguments per paraula clau (x='precip'
). A més, Altair ofereix mètodes de construcció per a definicions de codificació, utilitzant la sintaxi alt.X('precip')
. Aquesta alternativa és útil per proporcionar més paràmetres a una codificació, com veurem més endavant.
chart = alt.Chart(df).mark_point().encode(
alt.X('precip'),
alt.Y('city')
)
Els dos estils per especificar codificacions es poden barrejar: x='precip', alt.Y('city')
també és una entrada vàlida per a la funció encode
.
Als exemples anteriors, el tipus de dada per a cada camp s’infereix automàticament en funció del seu tipus dins del data frame.
També pots indicar explícitament el tipus de dada a Altair anotant el nom del camp:
Nom del camp | Tipus de dada |
---|---|
'b:N' | indica un tipus nominal (dades categòriques no ordenades) |
'b:O' | indica un tipus ordinal (dades ordenades per rang) |
'b:Q' | indica un tipus quantitatiu (dades numèriques amb magnituds significatives) |
'b:T' | indica un tipus temporal (dades de data/hora) |
Per exemple, alt.X('precip:N')
.
L’anotació explícita de tipus de dades és necessària quan les dades es carreguen des d’una URL externa directament per Vega-Lite (evitant Polars completament), o quan volem utilitzar un tipus diferent del que s’ha inferit automàticament.
Què creus que passarà amb el nostre gràfic anterior si tractem precip
com una variable nominal o ordinal, en lloc d’una variable quantitativa?
Modifica el codi anterior i comprova-ho!
Nominal
Transformació de dades: agregació
Per permetre més flexibilitat en la manera com es visualitzen les dades, Altair disposa d’una sintaxi integrada per a l’agregació de dades.
Per exemple, podem calcular la mitjana de tots els valors especificant una funció d’agregació juntament amb el nom del camp:
alt.Chart(df).mark_point().encode(
x='average(precip)',
y='city'
)
Ara, dins de cada categoria de l’eix x, veiem un únic punt que reflecteix la mitja dels valors dins d’aquella categoria.
Realment Seattle té la precipitació mitjana més baixa d’aquestes ciutats? (Sí!) Així i tot, com podria induir a error aquest gràfic? Quins mesos s’hi inclouen? Què compta com a precipitació?
Altair admet una varietat de funcions d’agregació, incloent-hi count
, min
(mínim), max
(màxim), average
, median
i stdev
(desviació estàndard).
Canviar el tipus de marca
Suposem que volem representar els valors agregats amb barres rectangulars en lloc de punts circulars
Podem fer-ho substituint Chart.mark_point
per Chart.mark_bar
:
alt.Chart(df).mark_bar().encode(
x='average(precip)',
y='city'
)
Com que el camp nominal city
es mapeja a l’eix y
, el resultat és un diagrama de barres horitzontals.
Per obtenir un diagrama de barres verticals, podem simplement intercanviar els valors de x
i y
:
alt.Chart(df).mark_bar().encode(
x='city'
y='average(precip)',
)
Personalitzar una visualització
Per defecte, Altair prenen algunes decisions sobre les propietats de la visualització, però es poden canviar utilitzant mètodes per personalitzar-ne l’aparença.
Per exemple, pots especificar els títols dels eixos utilitzant l’atribut axis
de les classes de canal, pots modificar les propietats d’escala amb l’atribut scale
, i pots especificar el color de la marca establint el paràmetre color
dels mètodes Chart.mark_*
a qualsevol cadena de color CSS vàlida:
alt.Chart(df).mark_point(color='firebrick').encode(
alt.X('precip', scale=alt.Scale(type='log'), axis=alt.Axis(title='Log-Scaled Values')),
alt.Y('city', axis=alt.Axis(title='Category')),
)
Vistes múltiples
Com hem vist més amunt, l’objecte Chart
d’Altair representa un gràfic amb un sol tipus de marca. I què passa amb diagrames més complicats, que impliquin múltiples gràfics o capes? Mitjançant un conjunt d’operadors de composició de vistes, Altair pot combinar múltiples definicions de gràfics per crear vistes més complexes.
Com a punt de partida, representem el conjunt de dades de cotxes amb un gràfic de línies que mostri la mitjana de consum per any de fabricació:
import streamlit as st
import vega_datasets
@st.cache_data
def load_data():
return vega_datasets.data.cars()
cars = load_data()
chart = alt.Chart(cars).mark_line().encode(
alt.X('Year'),
alt.Y('average(Miles_per_Gallon)')
)
st.altair_chart(chart)
Per augmentar aquest gràfic, potser vols afegir marques circle
per a cada punt de dades mitjanes. (La marca circle
és només una abreviatura convenient de les marques point
que utilitzen cercles plens.)
Pots començar definint cada gràfic per separat: primer un gràfic de línies i, després, un de dispersió. Després pots utilitzar l’operador layer
per combinar-los en un gràfic amb capes. Aquí fem servir l’operador abreujat +
(suma) per activar l’estratificació:
line = alt.Chart(cars).mark_line().encode(
alt.X('Year'),
alt.Y('average(Miles_per_Gallon)')
)
point = alt.Chart(cars).mark_circle().encode(
alt.X('Year'),
alt.Y('average(Miles_per_Gallon)')
)
chart = line + point
També pots crear aquest gràfic reutilitzant i modificant una definició de gràfic anterior! En lloc de reescriure completament un gràfic, podem començar amb el gràfic de línies i, a continuació, invocar el mètode mark_point
per generar una nova definició de gràfic amb un tipus de marca diferent:
chart = alt.Chart(cars).mark_line().encode(
alt.X('Year'),
alt.Y('average(Miles_per_Gallon)')
)
chart + chart.mark_circle()
La necessitat de col·locar punts sobre línies és tan habitual que la marca de línia també inclou una drecera per generar-te una capa nova. Prova d’afegir l’argument point=True
al mètode mark_line
!
chart = alt.Chart(cars).mark_line(point=True).encode(
alt.X("Year"),
alt.Y('average(Miles_per_Gallon)')
)
Ara bé, i si vols veure aquest gràfic al costat d’altres, com ara la potència mitjana al llarg del temps?
Pots utilitzar operadors de concatenació per col·locar múltiples gràfics de costat, bé verticalment, bé horitzontalment.
Aquí farem servir l’operador |
(pipe) per fer la concatenació horitzontal de dos gràfics:
mpg = alt.Chart(cars).mark_line(point=True).encode(
alt.X('Year'),
alt.Y('average(Miles_per_Gallon)')
)
hp = alt.Chart(cars).mark_line(point=True).encode(
alt.X('Year'),
alt.Y('average(Horsepower)')
)
chart = mpg | hp
Podem veure que, en aquest conjunt de dades, durant els anys setanta i principis dels vuitanta l’eficiència mitjana del combustible va millorar mentre la potència mitjana va disminuir.
Interactivity
A més del traçat bàsic i la composició de vistes, una de les característiques més interessants d’Altair i Vega-Lite és el seu suport per a la interacció.
Per crear un gràfic interactiu senzill que admeti desplaçament i zoom, pots invocar el mètode interactive()
de l’objecte Chart
.
Al gràfic següent, fes clic i arrossega per desplaçar-te o utilitza la roda del ratolí per fer zoom:
alt.Chart(cars).mark_point().encode(
x='Horsepower',
y='Miles_per_Gallon',
color='Origin',
).interactive()
Per oferir més detalls en passar el cursor, podem utilitzar el canal de codificació tooltip
:
To provide more details upon mouse hover, we can use the tooltip
encoding channel:
alt.Chart(cars).mark_point().encode(
x='Horsepower',
y='Miles_per_Gallon',
color='Origin',
tooltip=['Name', 'Origin'] # show Name and Origin in a tooltip
).interactive()
Per a interaccions més complexes, com ara gràfics enllaçats i filtratge creuat, Altair proporciona una abstracció de selecció per definir seleccions interactives i després vincular-les als components d’un gràfic. Ho tractarem en detall més endavant.
A continuació hi ha un exemple més complex:
L’histograma superior mostra el recompte de cotxes per any i utilitza una selecció interactiva per modificar l’opacitat dels punts al gràfic de dispersió inferior, que mostra potència enfront de consum.
Dibuixa un interval al gràfic superior i observa com afecta els punts del gràfic inferior. Quan examinis el codi, no et preocupis si hi ha parts que encara no tenen sentit! És un exemple aspiracional.
# create an interval selection over an x-axis encoding
brush = alt.selection_interval(encodings=['x'])
# determine opacity based on brush
opacity = alt.condition(brush, alt.value(0.9), alt.value(0.1))
# an overview histogram of cars per year
# add the interval brush to select cars over time
overview = alt.Chart(cars).mark_bar().encode(
alt.X('Year:O', timeUnit='year', # extract year unit, treat as ordinal
axis=alt.Axis(title=None, labelAngle=0) # no title, no label angle
),
alt.Y('count()', title=None), # counts, no axis title
opacity=opacity
).add_params(
brush # add interval brush selection to the chart
).properties(
width=400, # set the chart width to 400 pixels
height=50 # set the chart height to 50 pixels
)
# a detail scatterplot of horsepower vs. mileage
# modulate point opacity based on the brush selection
detail = alt.Chart(cars).mark_point().encode(
alt.X('Horsepower'),
alt.Y('Miles_per_Gallon'),
# set opacity based on brush selection
opacity=opacity
).properties(width=400) # set chart width to match the first chart
# vertically concatenate (vconcat) charts using the '&' operator
overview & detail
JSON Output
Com a API de Python per a Vega-Lite, el propòsit principal d’Altair és convertir les especificacions dels gràfics en una cadena JSON que s’ajusti a l’esquema de Vega-Lite. Amb el mètode Chart.to_json
, podem inspeccionar l’especificació JSON que Altair exporta i envia a Vega-Lite:
chart = alt.Chart(df).mark_bar().encode(
x='average(precip)',
y='city',
)
print(chart.to_json())
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json",
"config": {
"view": {
"continuousHeight": 300,
"continuousWidth": 300
}
},
"data": {
"name": "data-8e72c2f67818e64f2c6d729f1a903405"
},
"datasets": {
"data-8e72c2f67818e64f2c6d729f1a903405": [
{ "city": "Seattle", "month": "Apr", "precip": 2.68 },
{ "city": "Seattle", "month": "Aug", "precip": 0.87 },
{ "city": "Seattle", "month": "Dec", "precip": 5.31 },
{ "city": "New York", "month": "Apr", "precip": 3.94 },
{ "city": "New York", "month": "Aug", "precip": 4.13 },
{ "city": "New York", "month": "Dec", "precip": 3.58 },
{ "city": "Chicago", "month": "Apr", "precip": 3.62 },
{ "city": "Chicago", "month": "Aug", "precip": 3.98 },
{ "city": "Chicago", "month": "Dec", "precip": 2.56}
]
},
"encoding": {
"x": { "aggregate": "average", "field": "precip", "type": "quantitative" },
"y": { "field": "city", "type": "nominal"}
},
"mark": { "type": "bar" }
}
Observa que encode(x='average(precip)')
s’ha expandit a una estructura JSON amb un nom de field
, un type
per a les dades i inclou un camp aggregate
. La instrucció encode(y='city')
s’ha expandit de manera similar.
Com has vist abans, la sintaxi abreujada d’Altair també inclou una manera d’especificar el tipus del camp:
x = alt.X('average(precip):Q')
print(x.to_json())
{ "aggregate": "average", "field": "precip", "type": "quantitative" }
Aquesta forma abreviada és equivalent a escriure els atributs pel seu nom:
x = alt.X(aggregate='average', field='precip', type='quantitative')
print(x.to_json())
{ "aggregate": "average", "field": "precip", "type": "quantitative" }
Activitat
Has de crear una aplicació multipàgina: Multipage apps amb almenys dos datasets, per exemple: annual_precip
.
Vega Datasets és el centre centralitzat de més de 70 conjunts de dades que apareixen als exemples i a la documentació de Vega, Vega-Lite, Altair i projectes relacionats.
vega-datasets
és una biblioteca de Python que et permet accedir a diversos conjunts de dades tal com s’explica a vega-datasets
Al final has de desplegar l’aplicació a Streamlit.