How do I plot the groupby in altair? - pandas-groupby

I did groupby by genre and trying to plot using altair and I'm getting the error below.
disney_revenue = disney_movies.assign(inflation_adjusted_gross = disney_movies['inflation_adjusted_gross'].str.strip('$').str.replace(',','').astype(float))
disney_total_revenue = disney_revenue.assign(total_gross = disney_revenue['total_gross'].str.strip('$').str.replace(',','').astype(float))
disney_group = disney_total_revenue.groupby(by='genre')
chart2 = alt.Chart(disney_group, width=500, height=300).mark_circle().encode(
x='movie_title:N',
y='inflation_adjusted_gross:Q').properties(title='Total Adjusted Gross per Genre')
chart2
---------------------------------------------------------------------------
SchemaValidationError: Invalid specification
altair.vegalite.v4.api.Chart->0, validating 'type'
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x7f0611f2ac10> is not of type 'object'

You cannot pass a pandas groupby object to alt.Chart – you must pass a dataframe. But if you want to visualize grouped data, you can do that via the Altair encoding syntax. For example, here is a version of the chart you were trying to create, faceted by genre:
alt.Chart(disney_total_revenue).mark_circle().encode(
x='movie_title:N',
y='inflation_adjusted_gross:Q',
facet='genre:N',
).properties(
title='Total Adjusted Gross per Genre'
)

Related

Altair: Remove title from layered faceted graphs

I tried layering faceted graphs and it failed, so moved to the method suggested in here - https://stackoverflow.com/a/52882510/20390480 which basically layer the graphs and then call .facet(column). With this method I am unable to remove the facet title.
I tried .facet(column, title=None) throws the following error.
import altair as alt
from vega_datasets import data
cars = data.cars()
horse = alt.Chart().mark_point().encode(
x = 'Weight_in_lbs',
y = 'Horsepower'
)
miles = alt.Chart().mark_point(color='red').encode(
x = 'Weight_in_lbs',
y = 'Miles_per_Gallon'
)
alt.layer(horse, miles, data=cars).facet(column='Origin', title=None)
SchemaValidationError: Invalid specification
altair.vegalite.v4.api.Chart, validating 'required'
'data' is a required property
alt.FacetChart(...)
Try:
alt.layer(horse, miles, data=cars).facet(column=alt.Column('Origin', title=None))

Is there a way to specify what the legend shows in Altair?

I have the following graph in Altair:
The code used to generate it is as follows:
data = pd.read_csv(data_csv)
display(data)
display(set(data['algo_score_raw']))
# First generate base graph
base = alt.Chart(data).mark_circle(opacity=1, stroke='#4c78a8').encode(
x=alt.X('Paragraph:N', axis=None),
y=alt.Y('Section:N', sort=list(OrderedDict.fromkeys(data['Section']))),
size=alt.Size('algo_score_raw:Q', title="Number of Matches"),
).properties(
width=900,
height=500
)
# Next generate the overlying graph with the lines
lines = alt.Chart(data).mark_rule(stroke='#4c78a8').encode(
x=alt.X('Paragraph:N', axis=alt.Axis(labelAngle=0)),
y=alt.Y('Section:N', sort=list(OrderedDict.fromkeys(data['Section'])))
).properties(
width=900,
height=500
)
if max(data['algo_score_raw']) == 0:
return lines # no circles if no matches
else:
return base + lines
However, I don't want the decimal values in my legend; I only want 1.0, 2.0, and 3.0, because those are the only values that are actually present in my data. However, Altair seems to default to what you see above.
The legend is generated based on how you specify your encoding. It sounds like your data are better represented as ordered categories than as a continuous quantitative scale. You can specify this by changing the encoding type to ordinal:
size=alt.Size('algo_score_raw:O')
You can read more about encoding types at https://altair-viz.github.io/user_guide/encoding.html
You can use alt.Legend(tickCount=2)) (labelExpr could also be helpful, see the docs for more):
import altair as alt
from vega_datasets import data
source = data.cars()
source['Acceleration'] = source['Acceleration'] / 10
chart = alt.Chart(source).mark_circle(size=60).encode(
x='Horsepower',
y='Miles_per_Gallon',
size='Acceleration',
)
chart
chart.encode(size=alt.Size('Acceleration', legend=alt.Legend(tickCount=2)))

Pandas Series boolean maps and plotting

I am just trying to up my understanding of plotting Pandas Series data using Booleans to mask out values I don't want. I am not sure that what I have is the correct or efficient way to do it.
Don't get me wrong, I do get the chart I am after but are my assumptions on the syntax correct?
All I want to do is plot the non zero values on my chart. I have not formatted the charts as I would normally as this was just a test of Booleans and masking data and not for creating report grade charts.
If I masked this as a Pandas DataFrame I would do the following if df1 were my DataFrame.
I understand this and it makes sense that the df1[mask] returns my values as required
# Plot our graph with only items that are non-zero
fig = px.bar(df1[mask], x = 'Animals', y = 'Count')
fig.show()
Doing it as a Pandas Series
This is the snippet that creates the graph I require
# Plot our graph with only items that are non-zero
fig = px.bar(sf, x = sf.index[sf_mask], y = sf[sf_mask])
fig.show()
After my initial test with adding my mask to sf and getting an error. I deduced that I needed to add the mask against the x and y parameters. I take it this is because a Series is just a single column and the index is set as my "animals". Therefore by mapping the sf.index[sf_mask] I get the returned animals in the index and sf[sf_mask] returns me the values. failure to add either one would give a "ValueError" stating that the arguments should have the same length.
Here is what I did to test my workings
My initial imports and setting up Plotly as my plotting backend
import pandas as pd
import plotly.express as px
# Set our plotting backend to Plotly
pd.options.plotting.backend = "plotly"
I just created a test dataset from a dictionary
animals = {'rabbits' : 1,
'dogs' : 3,
'cats' : 0,
'ferrets' : 3,
'horses' : 8,
'goldfish' : 0,
'guinea_pigs' : 2,
'hamsters' : 6,
'mice' : 3,
'rats' : 0
}
Then converted it to a pandas Series
sf = pd.Series(animals)
I then create my boolean mask to mask out all our non-Zero entries on our Pandas Series
sf_mask = sf != 0
And if I then view the mask I can see I only get non zero values which is exactly what I am looking for.
sf[sf_mask]
Which outputs my non-zero items in my series.
rabbits 1
dogs 3
ferrets 3
horses 8
guinea_pigs 2
hamsters 6
mice 3
dtype: int64
If I plot without my Boolean mask 'sf_mask' using the following syntax I get my complete Pandas Series charted
# Plot our Series showing all items
fig = px.bar(sf, x = sf.index, y = sf)
fig.show()
Which outputs the following chart
If I plot with my Boolean mask 'sf_mask' using the following syntax I get the chart I want which excludes the gaps with zero value items.
# Plot our graph with only items that are non-zero
fig = px.bar(sf, x = sf.index[sf_mask], y = sf[sf_mask])
fig.show()
Which outputs the correct chart.
Your understanding of booleans and masking is correct.
You can simplify your syntax a little though: if you take a look at the plotly.express.bar documentation, you'll see that the arguments 'x' and 'y' are optional. You don't need to pass 'x' or 'y' because by default plotly.express will create the bars using the index of the Series as x and the values of the Series as y. You can also pass the masked series in place of the entire series.
For example, this will produce the same bar chart:
fig = px.bar(sf[sf>0])
fig.update_layout(showlegend=False)

How to group-by twice, preserve original columns, and plot

I have the following data sets (only sample is shown):
I want to find the most impactful exercise per area and then plot it via Seaborn barplot.
I use the following code to do so.
# Create Dataset Using Only Area, Exercise and Impact Level Chategories
CA_data = Data[['area', 'exercise', 'impact level']]
# Compute Mean Impact Level per Exercise per Area
mean_il_CA = CA_data.groupby(['area', 'exercise'])['impact level'].mean().reset_index()
mean_il_CA_hello = mean_il_CA.groupby('area')['impact level'].max().reset_index()
# Plot
cx = sns.barplot(x="impact level", y="area", data=mean_il_CA_hello)
plt.title('Most Impactful Exercises Considering Area')
plt.show()
The resulting dataset is:
This means that when I plot, on the y axis only the label relative to the area appears, NOT 'area label' + 'exercise label' like I would like.
How do I reinsert 'exercise column into my final dataset?
How do I get both the name of the area and the exercise on the y plot?
The problem of losing the values of 'exercise' when grouping by the maximum of 'area' can be solved by keeping the MultiIndex (i.e. not using reset_index) and using .transform to create a boolean mask to select the appropriate full rows of mean_il_CA that contain the maximum 'impact_level' values per 'area'. This solution is based on the code provided in this answer by unutbu. The full labels for the bar chart can be created by concatenating the labels of 'area' and 'exercise'.
Here is an example using the titanic dataset from the seaborn package. The variables 'class', 'embark_town', and 'fare' are used in place of 'area', 'exercise', and 'impact_level'. The categorical variables both contain three unique values: 'First', 'Second', 'Third', and 'Cherbourg', 'Queenstown', 'Southampton'.
import pandas as pd # v 1.2.5
import seaborn as sns # v 0.11.1
df = sns.load_dataset('titanic')
data = df[['class', 'embark_town', 'fare']]
data.head()
data_mean = data.groupby(['class', 'embark_town'])['fare'].mean()
data_mean
# Select max values in each class and create concatenated labels
mask_max = data_mean.groupby(level=0).transform(lambda x: x == x.max())
data_mean_max = data_mean[mask_max].reset_index()
data_mean_max['class, embark_town'] = data_mean_max['class'].astype(str) + ', ' \
+ data_mean_max['embark_town']
data_mean_max
# Draw seaborn bar chart
sns.barplot(data=data_mean_max,
x=data_mean_max['fare'],
y=data_mean_max['class, embark_town'])

How to change the limits for geo_shape in altair (python vega-lite)

I am trying to plot locations in three states in the US in python with Altair. I saw the tutorial about the us map but I am wondering if there is anyway to zoom the image to the only three states of interest, i.e. NY,NJ and CT.
Currently, I have the following code:
from vega_datasets import data
states = alt.topo_feature(data.us_10m.url, 'states')
# US states background
background = alt.Chart(states).mark_geoshape(
fill='lightgray',
stroke='white',
limit=1000
).properties(
title='US State Capitols',
width=700,
height=400
).project("albers")
points=alt.Chart(accts).mark_point().encode(
longitude = "longitude",
latitude = "latitude",
color = "Group")
background+points
I inspected the us_10m.url data set and seems like there is no field which specifies the individual states. So I am hoping if I could just somehow change the xlim and ylim for the background to [-80,-70] and [35,45] for example. I want to zoom in to the regions where there are data points(blue dots).
Could someone kindly show me how to do that? Thanks!!
Update
There is a field called ID in the JSON file and I manually found out that NJ is 34, NY is 36 and CT is 9. Is there a way to filter on these IDs? That will get the job done!
Alright seems like the selection/zoom/xlim/ylim feature for geotype is not supported yet:
Document and add warning that geo-position doesn't support selection yet #3305
So I end up with a hackish way to solve this problem by first filtering based on the IDs using pure python. Basically, load the JSON file into a dictionary and then change the value field before converting the dictionary to topojson format. Below is an example for 5 states,PA,NJ,NY,CT,RI and MA.
import altair as alt
from vega_datasets import data
# Load the data, which is loaded as a dict object
us_10m = data.us_10m()
# Select the geometries under states under objects, filter on id (9,25,34,36,42,44)
us_10m['objects']['states']['geometries']=[item for item in us_10m['objects'] \
['states']['geometries'] if item['id'] in [9,25,34,36,42,44]]
# Make the topojson data
states = alt.Data(
values=us_10m,
format=alt.TopoDataFormat(feature='states',type='topojson'))
# Plot background (now only has 5 states)
background = alt.Chart(states).mark_geoshape(
fill='lightgray',
stroke='white',
limit=1000
).properties(
title='US State Capitols',
width=700,
height=400
).project("mercator")
# Plot the points
points=alt.Chart(accts).mark_circle(size=60).encode(
longitude = "longitude",
latitude = "latitude",
color = "Group").project("mercator")
# Overlay the two plots
background+points
The resulting plot looks ok:

Resources