Figure Friday 2025 - week 8

join the Figure Friday session on February 28, at noon Eastern Time, to showcase your creation and receive feedback from the community.

Did you know that according to Dallas Animal Services, the Live Release Rate of dogs and cats in January 2025 was 89%. Despite the high release rate, the total Dog Kennel Capacity is at 122% (Daily Report Card).

In this week’s Figure Friday, we’ll explore the Animals Inventory Dataset of the animal shelter in Dallas.

The data is limited to all animals that were accepted by the shelter (Intake_Date) in January 2024. For the full and most recent data, please export it from the Dallas Animal Services on Dallas Open Data.

Things to consider:

  • what can you improve in the app or sample figure below (Strip Chart)?
  • would you like to tell a different data story using a different graph?
  • can you create a different Dash app?

Sample figure:

Code for sample figure:
from dash import Dash, dcc
import dash_ag_grid as dag
import plotly.express as px
import pandas as pd

df = pd.read_csv("https://raw.githubusercontent.com/plotly/Figure-Friday/refs/heads/main/2025/week-8/Dallas_Animal_Shelter_Data_Fiscal_Year_Jan_2024.csv")
df["Intake_Date"] = pd.to_datetime(df['Intake_Date'])
df["Outcome_Date"] = pd.to_datetime(df['Outcome_Date'])
df["Animal_Stay_Days"] = (df["Outcome_Date"] - df["Intake_Date"]).dt.days

df_filtered = df[df["Animal_Type"] == "DOG"]
fig = px.strip(df_filtered, x="Animal_Stay_Days", y="Intake_Type", height=650,
               title='Number of Days Dogs Spend in Shelter by Intake type')


grid = dag.AgGrid(
    rowData=df.to_dict("records"),
    columnDefs=[{"field": i, 'filter': True, 'sortable': True} for i in df.columns],
    dashGridOptions={"pagination": True}
)

app = Dash()
app.layout = [
    grid,
    dcc.Graph(figure=fig)
]


if __name__ == "__main__":
    app.run(debug=True)

Participation Instructions:

  • Create - use the weekly data set to build your own Plotly visualization or Dash app. Or, enhance the sample figure provided in this post, using Plotly or Dash.
  • Submit - post your creation to LinkedIn or Twitter with the hashtags #FigureFriday and #plotly by midnight Thursday, your time zone. Please also submit your visualization as a new post in this thread.
  • Celebrate - join the Figure Friday sessions to showcase your creation and receive feedback from the community.

:point_right: If you prefer to collaborate with others on Discord, join the Plotly Discord channel.

Data Source:

Thank you to Dallas Open Data and Dallas Animal Services for the data.

2 Likes

I’m sorry for the code, this is just a bit of I want this, I want that, and fast, what works works, hurray for the colorpicker. Not responsive.

Py.cafe app (it’s not an app, it’s a onepager without important interaction): PyCafe - Dash - A bit of px.strip, a lot of animal welfare

Update: I’ve removed my remarks about “horrible website”. What I meant was pagespeed & sometimes most important info beyond the fold (need to scroll to discover it) and images to important pages not clickable). It’s a combination of Wordpress & Elementor, a recipe for beautiful effects and also a pagespeedkiller. BUT: you can see it was made with love.

7 Likes

I started, but it’s still a work in progress. I’m thinking about putting the charts on multiple pages. :cat::dog:

6 Likes

@Ester why are you thinking of putting the charts on multiple pages? I kind of like it that it’s all on one page.

1 Like

I love that you added the donate button, @marieanne . Not every app needs to have controls. Sometime an info-graphic like the one you created is perfect to draw people’s attention.

2 Likes

I only thought that because this is a reduced image, you have to scroll on it anyway.:thinking:

2 Likes

I tried making the cards smaller, but I still couldn’t get the colors to match. :thinking:

Update:
Each chart requires a different color setting, so it’s hard to set it all the same. I prefer to use fewer colors. Maybe it’s better.

3 Likes

I think you mean the color of for instance “dogs”? Maybe you can manage with the directly mapping colors to data values, see Discrete colors in Python, the Directly Mapping Colors to Data Values example at 60% of the page.
Define it once dog=red, cat=blue etc, use it everywhere. I hope :slight_smile:

Forgot you can insert code, it looks like this:

fig = px.bar(df, y="continent", x="pop", color="continent", orientation="h", hover_name="country",
             color_discrete_map={
                "Europe": "red",
                "Asia": "green",
                "Americas": "blue",
                "Oceania": "goldenrod",
                "Africa": "magenta"},
             title="Explicit color mapping")
4 Likes

Hello Everyone,

My contributiion/approach to this week 8, let´s me start by saying it´s quite similar to Ester´s dashboard, just my focus was on Dogs Breeds :joy:. :stuck_out_tongue_winking_eye:

In Summary

The code provides an interactive tool to analyze the fate of dog breeds at the Dallas Animal Shelter, exploring the relationship between breed, exit probability, distribution, and length of stay.

These are the images



The code

import pandas as pd
import plotly.express as px
from sksurv.nonparametric import kaplan_meier_estimator
import dash
from dash import dcc, html, Input, Output
import dash_bootstrap_components as dbc


DATA_FILE = 'Dallas_Animal_Shelter_Data_Fiscal_Year_Jan_2024.csv'
OUTCOME_MAPPING = {
    'ADOPTION': 'Exit', 'RETURNED TO OWNER': 'Exit', 'TRANSFER': 'Exit', 'FOSTER': 'Exit', 'DISPOSAL': 'Exit',
    'TNR': 'Exit', 'WILDLIFE': 'Exit', 'SNR': 'Exit', 'EUTHANIZED': 'Stay', 'DIED': 'Stay', 'LOST EXP': 'Stay',
    'FOUND EXP': 'Stay', 'TREATMENT': 'Stay', 'MISSING': 'Stay'
}
TOP_BREEDS_COUNT = 20

def load_and_preprocess_data():
    df = pd.read_csv(DATA_FILE)
    df_dogs = df[df['Animal_Type'] == 'DOG'].copy()  # Usar .copy() para evitar warnings

    df_dogs['Outcome_Category'] = df_dogs['Outcome_Type'].replace(OUTCOME_MAPPING)
    df_dogs['Intake_DateTime'] = pd.to_datetime(df_dogs['Intake_Date'] + ' ' + df_dogs['Intake_Time'])
    df_dogs['Outcome_DateTime'] = pd.to_datetime(df_dogs['Outcome_Date'] + ' ' + df_dogs['Outcome_Time'])
    df_dogs['Duration'] = (df_dogs['Outcome_DateTime'] - df_dogs['Intake_DateTime']).dt.total_seconds() / (60 * 60 * 24)
    df_dogs['Event'] = df_dogs['Outcome_Category'] == 'Exit'
    df_dogs.dropna(subset=['Duration'], inplace=True)
    df_dogs = df_dogs[df_dogs['Duration'] >= 0]

    top_breeds = df_dogs['Animal_Breed'].value_counts().nlargest(TOP_BREEDS_COUNT).index
    df_top_breeds = df_dogs[df_dogs['Animal_Breed'].isin(top_breeds)]

    return df_top_breeds, top_breeds

df_top_breeds, top_breeds = load_and_preprocess_data()


breed_colors = {breed: px.colors.qualitative.D3[i % len(px.colors.qualitative.D3)] for i, breed in enumerate(top_breeds)}


def calculate_survival_curves(breeds):
    survival_data = []
    for breed in breeds:
        breed_data = df_top_breeds[df_top_breeds['Animal_Breed'] == breed]
        time, survival_prob = kaplan_meier_estimator(breed_data['Event'].astype(bool), breed_data['Duration'])
        survival_data.append(pd.DataFrame({'Time': time, 'Survival Probability': survival_prob, 'Breed': breed}))
    return pd.concat(survival_data)


def create_survival_plot(survival_df):
    if survival_df.empty:
        return px.line(title="No data to show")
    fig = px.line(survival_df, x='Time', y='Survival Probability', color='Breed',
                  color_discrete_map=breed_colors,markers=True,
                  labels={'Time': 'Days', 'Survival Probability': '% Exit Probability'},
                  line_shape="spline", template='plotly_white', 
                 )
    fig.update_layout(
        title_font=dict(size=20, family='Arial', color='black'),
        xaxis_title_font=dict(size=14, family='Arial', color='black'),
        yaxis_title_font=dict(size=14, family='Arial', color='black'),
        legend_title_font=dict(size=16, family='Arial', color='black')
    )
    
    
    return fig

def create_breed_count_plot(filtered_df):
    breed_counts = filtered_df['Animal_Breed'].value_counts().reset_index()
    breed_counts.columns = ['Breed', 'Count']
    fig_bar_breedcount = px.bar(breed_counts, x='Breed', y='Count', 
                                 text_auto='.2f', template='plotly_white', labels={'Breed': ''},
                                 color='Breed', color_discrete_map=breed_colors)

    fig_bar_breedcount.update_yaxes(visible=False)
    fig_bar_breedcount.update_layout(showlegend=False)
    
    
    return fig_bar_breedcount

def create_breed_duration_plot(filtered_df):
    breed_duration = (filtered_df.groupby('Animal_Breed')['Duration'].agg(['mean', 'median'])
                      .reset_index().sort_values('mean', ascending=False))
    fig_bar_breedmean = px.bar(breed_duration, x='Animal_Breed', y='mean', 
                               text_auto='.2f', template='plotly_white', labels={'Animal_Breed': ''},
                               color='Animal_Breed', color_discrete_map=breed_colors)
    
    fig_bar_breedmean.update_yaxes(visible=False)
    fig_bar_breedmean.update_layout(showlegend=False)
    
    for index, row in breed_duration.iterrows():
        fig_bar_breedmean.add_annotation(x=row['Animal_Breed'], y=row['mean'],
                             text=f"Median: {row['median']:.2f}", showarrow=False, yshift=10)
    return fig_bar_breedmean

#Styles spaces
style_space = {'border': 'none', 'height': '5px', 'background': 'linear-gradient(to right, #007bff, #ff7b00)', 'margin': '10px 0'}

# Dash App
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.LUX])

app.title=' Dallas Animal Shelter'

app.layout = dbc.Container([
    html.Hr(style=style_space),
    html.H2("Dog Diaries: Analyzing Breeds' Fate in Dallas Animal Shelter", style={'text-align': 'center'}),
    html.Hr(style=style_space),
    html.Div([
        html.P("Analyzing only dogs, which make up 80% of the shelter animals, we selected the top 20 breeds, representing 90% of this group", style={'text-align': 'center', 'margin-top': '20px', 'font-style': 'italic','font-size': '24px',  'color': 'black'}),
        html.Hr(style=style_space)
    ]),
    dbc.Row([
        dbc.Col(dbc.Card(dcc.Dropdown(id='breed-dropdown', options=[{'label': breed, 'value': breed} for breed in top_breeds],
                             value=top_breeds[:3], multi=True)), width=12,class_name="btn-group dash-dropdown")
    ]),
    html.Hr(style={'border': 'none', 'height': '2px', 'background': 'linear-gradient(to right, #007bff, #ff7b00)', 'margin': '10px 0'}),
    dbc.Row([html.H5("Exit Probability by Days: A Closer Look at Each Breed",style={'text-align': 'center'}),
             dbc.Col(dcc.Graph(id='survival-plot'), width=12)]),
    dbc.Row(
        [dbc.Col([
            html.H5("Top Dog Breeds at Dallas Shelter: A Count Analysis",style={'text-align': 'center'}),
            dcc.Graph(id='breed-count-plot')], width=6),
         dbc.Col([
            html.H5("Shelter Stays: Average and Median Duration Dog Breeds",style={'text-align': 'center'}),
            dcc.Graph(id='breed-duration-plot')], width=6)
        ])
    
],fluid=True)

@app.callback(
    [Output('survival-plot', 'figure'), Output('breed-count-plot', 'figure'),
     Output('breed-duration-plot', 'figure')],
    Input('breed-dropdown', 'value')
)
def update_plot(selected_breeds):
    if selected_breeds:
        filtered_df = df_top_breeds[df_top_breeds['Animal_Breed'].isin(selected_breeds)]
        return (create_survival_plot(calculate_survival_curves(selected_breeds)),
                create_breed_count_plot(filtered_df),
                create_breed_duration_plot(filtered_df))
    else:
        return create_survival_plot(pd.DataFrame()), px.bar(), px.bar()

if __name__ == '__main__':
    app.run_server(debug=True)

I´m trying to uploading to PyCafe, unfortunately it can not be possible since I´m using a library
sksurv which PyCafe does not recognize or can work with it, may be Py Cafe limitations, I do not know, anyway I´ll keep trying in case I succeed, I´ll share the app, up and running.

Any clues/comments/suggestions are more than welcome

4 Likes

The question that stays in my mind. What is happening here?

Hi Marieanne,

Do you mean why this value drop to Zero?

If so, In this context of the animal shelter, this would mean that, according to the observed data, all animals in that group have experienced the event (e.g., have been adopted or have left the shelter) at that point in time or before.

Just for reference I made some assumptions beyond just Animal leaving the shelters because of adoption (as you can see on the code)

2 Likes

Yes, the line goes from gradually doing something, boom, fastforward to zero.

1 Like

Thanks for asking Marieanne

1 Like

Happy Birthday dude!! :smiling_face:
Thanks to be there!! :birthday:

2 Likes

Thank you Thank you Juan :birthday: :pray:

1 Like

For Week 8 I made this simple visualization of positive vs. negative outcome percentages by breed. I defined positive outcomes as returned to owner or adoption; all others are considered negative (not 100% sure if this is a valid assumption.

Here is the code:

import polars as pl
import plotly.express as px
df = (
    pl.scan_csv('Dallas_Animal_Shelter_Data_Fiscal_Year_Jan_2024.csv')
    .filter(pl.col('Animal_Type').str.to_uppercase() == 'DOG')
    .select(
        BREED = pl.col('Animal_Breed').str.to_titlecase(),
        IN_TYPE = pl.col('Intake_Type').str.to_titlecase().cast(pl.Categorical),
        OUT_TYPE = pl.col('Outcome_Type').str.to_titlecase(),
        IN_DATE = pl.col('Intake_Date').str.to_date('%d/%m/%Y', strict=False),
        OUT_DATE = pl.col('Outcome_Date').str.to_date('%d/%m/%Y', strict=False),
    )
    .filter(pl.col('IN_TYPE') == 'Stray')
    .with_columns(
        OUTCOME = 
            pl.when(pl.col('OUT_TYPE').is_in(
                ['Returned To Owner', 'Adoption']))
               .then(pl.lit('POS')).otherwise(pl.lit('NEG'))
    )
    .with_columns(
        BREED_OUTCOME_COUNT = 
            pl.col('BREED').count().over('BREED', 'OUTCOME')
    )
    .with_columns(
        TOTAL_DAYS = (
            pl.col('OUT_DATE') 
            - 
            pl.col('IN_DATE')
        )
        .dt.total_days()
        .cast(pl.Int16)
    )
    # .filter(pl.col('TOTAL_DAYS') > 1)
    .unique(['BREED', 'OUTCOME'])
    .collect()
    .pivot(
        on = 'OUTCOME',
        index='BREED',
        values='BREED_OUTCOME_COUNT'
    )
    .drop_nulls()
    .with_columns(
        POS_PCT = 
        100* pl.col('POS')
        /
        (
            pl.col('POS') + pl.col('NEG')
        ))
    .with_columns(
        TOTAL_DAYS = (
            pl.col('POS') 
            + 
            pl.col('NEG')
        )
    )
    .with_columns(NEG_PCT = (100 - pl.col('POS_PCT')))
    .filter(pl.col('BREED') != 'Mixed Breed')
    .sort('POS_PCT', descending=False)
    .select('BREED', 'TOTAL_DAYS', 'POS', 'NEG', 'POS_PCT', 'NEG_PCT')
)
fig = px.bar(
    df,
    x=['POS_PCT', 'NEG_PCT'],
    y='BREED',
    height=1000, width=1200,
    template='simple_white',
    title = (
        'SHELTER STRAY DOGS: PAWS. vs NEG. OUTCOMES<br>' + 
        '<sup>POS OUTCOME MEANS OWNER FOUND OR ADOPTION<sup>'
    )
)
fig.update_layout(
    xaxis_title='Percentage'.upper(),
    legend_title='OUTCOME(%)')
fig.show()
4 Likes

That’s an interesting analysis, @Mike_Purtell . I wonder if there is a correlation between size of dog and adoption. That correlation definitely exist in city shelters where people don’t have the apartment size to adopt a large dog.
Another interesting question would be which correlation is stronger: dog_size to adoption or dog_age to adoption.