import pandas as pd
import numpy as np
import dash
from dash import dcc, html, Input, Output, State
import plotly.express as px
import plotly.graph_objects as go
import dash_bootstrap_components as dbc
# ------------------------------------------------------------------------
# CONFIGURATION
# ------------------------------------------------------------------------
# Color scheme and consistent styling - Modified to more journal-like colors
PET_COLORS = {
'Dog': '#1A5276', # Darker blue
'Cat': '#922B21', # Darker red
'Fish': '#196F3D', # Darker green
'Bird': '#B9770E' # Darker orange/yellow
}
PET_ICONS = {
'Dog': '🐶', # Dog Face
'Cat': '🐱', # Cat Face
'Fish': '🐟', # Fish
'Bird': '🐦' # Bird
}
PET_LABELS = {
'Dog': 'Dogs',
'Cat': 'Cats',
'Fish': 'Fish',
'Bird': 'Birds'
}
# Define regions once
REGION_MAP = {
'Europe': ['Germany', 'France', 'United Kingdom', 'Italy', 'Spain', 'Russia', 'Turkey','Czech Republic', 'Belgium', 'Swedem','Poland','Netherlands'],
'Americas': ['United States', 'Canada', 'Brazil', 'Mexico', 'Argentina'],
'Asia-Pacific': ['China', 'Japan', 'India', 'Australia', 'South Korea'],
}
# ------------------------------------------------------------------------
# DATA PREPARATION
# ------------------------------------------------------------------------
# Load data
df = pd.read_csv("pet_ownership_data_corrected.csv")
# Calculate the shannon diversity index correctly
def calculate_shannon_index(row):
# Get values and filter zeros
values = [row['Dog'], row['Cat'], row['Fish'], row['Bird']]
values = [val for val in values if val > 0]
if not values:
return 0
# Normalize to ensure sum equals 100
total = sum(values)
proportions = [val/total for val in values]
# Calculate Shannon index
shannon = -sum(p * np.log(p) for p in proportions)
# Normalize to 0-100 scale
max_shannon = np.log(len(proportions)) if len(proportions) > 1 else 1
normalized = (shannon / max_shannon) * 100
return round(normalized, 1)
# Process and enrich data all at once
def prepare_data(df):
# Add predominant pet
df['Predominant_Pet'] = df[['Dog', 'Cat', 'Fish', 'Bird']].idxmax(axis=1)
# Add diversity index
df['Diversity_Index'] = df.apply(calculate_shannon_index, axis=1)
# Add region
for region, countries in REGION_MAP.items():
df.loc[df['Country'].isin(countries), 'Region'] = region
return df
df = prepare_data(df)
# Pre-calculate insights
def generate_insights(df):
insights = {}
# Pet summary counts
insights['pet_counts'] = {
pet: df[df['Predominant_Pet'] == pet].shape[0]
for pet in ['Dog', 'Cat', 'Fish', 'Bird']
}
# Regional analysis
insights['regional_data'] = df.groupby('Region').agg({
'Dog': 'mean',
'Cat': 'mean',
'Fish': 'mean',
'Bird': 'mean',
'Diversity_Index': 'mean'
}).reset_index()
# Additional insights
insights['region_predominant'] = insights['regional_data'].apply(
lambda x: x[['Dog', 'Cat', 'Fish', 'Bird']].idxmax(), axis=1
)
# Add global averages
insights['global_avg'] = {
pet: df[pet].mean() for pet in ['Dog', 'Cat', 'Fish', 'Bird']
}
return insights
insights = generate_insights(df)
# ------------------------------------------------------------------------
# APP INITIALIZATION
# ------------------------------------------------------------------------
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.JOURNAL])
app.title = "Global Pet Ownership Analysis"
# ------------------------------------------------------------------------
# LAYOUT - REDESIGNED FOR JOURNAL INFOGRAPHIC STYLE WITH IMPROVED ALIGNMENT
# ------------------------------------------------------------------------
app.layout = dbc.Container([
# Header with journal-style title and subtitle
html.Div([
html.H1("The World of Pets",
className="text-center mt-4 mb-0",
),
html.H2("An Analysis of Global Pet Preference Patterns",
className="text-center mb-2"),
html.Hr(style={"width": "40%", "margin": "auto", "marginBottom": "2rem", "border": "1px solid #666"}),
# Brief introduction - journal style
html.H5("Cultural, social, and regional factors shape our choices in animal companions. How pet ownership differs globally.",
className="text-center mb-4"),
], className="mb-5", style={"backgroundColor": "#f9f9f9", "padding": "20px 0"}),
# Main content section - Removed fixed heights for responsive design
dbc.Row([
# Map section
dbc.Col([
html.H3("Global Pet Preference Map",
className="mb-3"),
# Map controls with journal-style formatting
html.Div([
dcc.RadioItems(
id="map-color-option",
options=[
{'label': 'Predominant pet', 'value': 'predominant'},
{'label': 'Diversity index', 'value': 'diversity'}
],
value='predominant',
inline=True,
className="mb-2",
inputStyle={"marginRight": "5px"},
labelStyle={"marginRight": "15px", "fontSize": "0.9rem"}
),
# Botón de información que aparece condicionalmente
html.Div(
id="diversity-info-container",
children=[
html.Button(
"What is Diversity Score?",
id="diversity-info-button",
className="btn btn-sm btn-outline-secondary ms-2",
style={"fontSize": "0.8rem"}
),
],
style={"display": "none"} # Inicialmente oculto
),
dcc.RadioItems(
id="map-type",
options=[
{'label': 'Natural Earth', 'value': 'natural earth'},
{'label': 'Orthographic', 'value': 'orthographic'}
],
value='natural earth',
inline=True,
className="mb-2 ms-4",
inputStyle={"marginRight": "5px"},
labelStyle={"marginRight": "15px", "fontSize": "0.9rem"}
),
], className="d-flex justify-content-center mb-3 align-items-center"),
# Añadimos el modal de explicación
dbc.Modal([
dbc.ModalHeader("Understanding the Diversity Score"),
dbc.ModalBody([
html.P([
"The Diversity Score measures how varied or balanced pet preferences are in a country or region, on a scale from 0 to 100."
]),
html.Hr(),
html.H6("How to interpret the score:", className="mt-3"),
html.Ul([
html.Li([
html.Strong("Low Score (0-30): "),
"Strong preference for one pet type. Most pet owners choose the same type of pet."
]),
html.Li([
html.Strong("Medium Score (30-70): "),
"Some diversity in preferences. There's a more balanced distribution between different pet types."
]),
html.Li([
html.Strong("High Score (70-100): "),
"High diversity. Pet ownership is distributed evenly across multiple pet types."
])
]),
html.Hr(),
html.P([
"This score is calculated using the Shannon Diversity Index, a metric commonly used in ecology to measure biodiversity, adapted to measure the diversity of pet preferences."
], className="mt-3 fst-italic text-muted")
]),
dbc.ModalFooter(
dbc.Button("Close", id="close-diversity-modal", className="ms-auto")
),
],id="diversity-modal",centered=True,
size="lg"),
# Map with invisible borders
html.Div([
dcc.Graph(id="world-map", style={"height": "480px"}
)
], style={"padding": "10px", "backgroundColor": "#f9f9f9", "boxShadow": "0 0 10px rgba(0,0,0,0.05)"})
], width=7, className="pe-4"),
# Country Spotlight with journal styling - Responsive design
dbc.Col([
html.H3("Country Spotlight",
className="mb-3"),
html.P("Click on a country on the map to see detailed analysis",
className="text-center mb-3 fst-italic"),
# Selected country information with journal-style formatting and no visible borders
html.Div([
html.H4(id="selected-country",
className="text-center mb-3"),
html.Div(id="country-profile", className="mb-3"),
html.Div(id="country-comparison", className="mb-3"),
html.Div(id="country-chart")
], style={"backgroundColor": "#f9f9f9", "padding": "20px", "borderRadius": "0", "boxShadow": "0 0 10px rgba(0,0,0,0.05)", "minHeight": "480px", "overflowY": "auto"})
], width=5, className="ps-4")
], className="mb-5"),
# Journal-style separator with quote
html.Div([
html.Hr(style={"width": "30%", "margin": "auto", "marginBottom": "15px", "marginTop": "15px"}),
html.P("The greatness of a nation and its moral progress can be judged by the way its animals are treated. — Mahatma Gandhi",
className="text-center fst-italic",
style={"fontSize": "1rem", "color": "#666", "maxWidth": "600px", "margin": "auto"}),
html.Hr(style={"width": "30%", "margin": "auto", "marginTop": "15px", "marginBottom": "40px"})
]),
# Second row: Global Pet Landscape and Regional Insights with journal styling
dbc.Row([
# Global statistics - journal style
dbc.Col([
html.H3("Global Pet Landscape",
className="mb-4"),
# Pet distribution with journal-style formatting and no visible borders
html.Div([
html.H5("Predominant Pet by Country",
className="mb-3",
),
html.Div([
*[html.Div([
html.Span(PET_ICONS[pet], style={"fontSize": "28px"}),
html.Span(f" {count} countries",
style={"fontSize": "16px", "marginLeft": "10px",
"color": PET_COLORS[pet]})
], className="me-4 d-inline-block") for pet, count in insights['pet_counts'].items()]
], className="d-flex justify-content-center mb-4"),
# Global averages visualization with journal styling
html.Div([
dcc.Graph(
figure=px.bar(
x=list(insights['global_avg'].keys()),
y=list(insights['global_avg'].values()),
color=list(insights['global_avg'].keys()),
color_discrete_map=PET_COLORS,
labels={'x': 'Pet Type', 'y': 'Global Average %'},
text=[f"{val:.1f}%" for val in insights['global_avg'].values()],
).update_layout(
showlegend=False,
margin=dict(l=40, r=40, t=30, b=40),
height=350,
title="Global Average (%) Pet Ownership",
plot_bgcolor='rgba(0,0,0,0)',
paper_bgcolor='rgba(0,0,0,0)',
).update_xaxes(
showgrid=False
).update_yaxes(
visible=False,
gridcolor='#eee'
)
)
], className="mb-4"),
], style={"backgroundColor": "#f9f9f9", "padding": "20px", "boxShadow": "0 0 10px rgba(0,0,0,0.05)", "minHeight": "530px"})
], md=6, className="pe-4"),
# Regional analysis with journal styling
dbc.Col([
html.H3("Regional Insights",
className="mb-4", ),
html.Div([
# Regional comparison graph with journal styling
dcc.Graph(
figure=px.bar(
insights['regional_data'].melt(
id_vars='Region',
value_vars=['Dog', 'Cat', 'Fish', 'Bird'],
var_name='Pet', value_name='Percentage'
),
x='Region', y='Percentage', color='Pet',
color_discrete_map=PET_COLORS,
barmode='group',
text_auto='.1f',
labels={'Percentage': 'Average %', 'Pet': 'Pet Type', 'Region':''},
title="Pet Preference Distribution by Region (Average %)"
).update_layout(
margin=dict(l=40, r=40, t=40, b=40),
showlegend=False,
# legend_title_text='Pet Type',
plot_bgcolor='rgba(0,0,0,0)',
paper_bgcolor='rgba(0,0,0,0)',
height=280
).update_xaxes(
showgrid=False
).update_yaxes(
visible=False,
gridcolor='#eee'
),
),
# Regional diversity index with journal styling
html.H5("Pet Diversity by Region (Diversity Score)",
className="mt-4 mb-3 text-center",
style={"fontFamily": "Georgia, serif"}),
dcc.Graph(
figure=px.bar(
insights['regional_data'],
x='Region', y='Diversity_Index',
color='Diversity_Index',
color_continuous_scale=px.colors.sequential.Viridis,
labels={'Diversity_Index': 'Diversity Score'},
text=[f"{val:.1f}" for val in insights['regional_data']['Diversity_Index']]
).update_layout(
showlegend=False,
coloraxis_showscale=False,
margin=dict(l=40, r=40, t=20, b=40),
height=175,
plot_bgcolor='rgba(0,0,0,0)',
paper_bgcolor='rgba(0,0,0,0)',
).update_xaxes(
showgrid=False
).update_yaxes(
visible=False,
gridcolor='#eee'
)
),
html.P([
html.Strong("Key finding: "),
"Europe shows the highest diversity in pet preferences, while the Americas display more pronounced preference for dogs."
], className="mt-3 text-center fst-italic")
], style={"backgroundColor": "#f9f9f9", "padding": "20px", "boxShadow": "0 0 10px rgba(0,0,0,0.05)", "minHeight": "530px"})
], md=6, className="ps-4")
], className="mb-5"),
# Footer with journal style citation
html.Div([
html.Hr(style={"width": "50%", "margin": "auto", "marginBottom": "20px"}),
html.P([
"© 2025 Global Pet Analysis",
html.Br(),
"Thank you to MakeoverMonday and GfK for the data.",
html.Br(),
"Analysis and visualization by The Journal of Pet Demographics"
], className="text-center", style={"fontSize": "0.9rem", "color": "#666"}),
html.Hr(style={"width": "50%", "margin": "auto", "marginBottom": "20px"})
], style={"marginTop": "20px",})
], fluid=True, style={
"backgroundColor": "#F5F5EA",
"color": "#0d0d0d",
"lineHeight": "1.6"
})
# Map callback - update for better responsiveness
@app.callback(
Output("world-map", "figure"),
[Input("map-color-option", "value"),
Input("map-type", "value")]
)
def update_map(color_option, map_type):
if color_option == 'predominant':
# Map colored by predominant pet
fig = px.choropleth(
df,
locations='Country',
labels={'Predominant_Pet':''},
locationmode='country names',
color='Predominant_Pet',
color_discrete_map=PET_COLORS,
hover_name='Country',
hover_data={
'Country': False,
'Predominant_Pet': True,
'Dog': ':.1f',
'Cat': ':.1f',
'Fish': ':.1f',
'Bird': ':.1f',
'Diversity_Index': ':.1f'
}
)
else: # diversity
# Map colored by diversity index
fig = px.choropleth(
df,
locations='Country',
locationmode='country names',
color='Diversity_Index',
color_continuous_scale=px.colors.sequential.Viridis,
range_color=[0, 100],
hover_name='Country',
hover_data={
'Country': False,
'Diversity_Index': ':.1f',
'Dog': ':.1f',
'Cat': ':.1f',
'Fish': ':.1f',
'Bird': ':.1f',
'Predominant_Pet': True
}
)
fig.update_layout(
geo=dict(
showframe=False,
showcoastlines=True,
projection_type=map_type
),
margin={"r":0,"t":0,"l":0,"b":0},
# font=dict(family="Georgia, serif"),
autosize=True,
coloraxis_colorbar=dict(len=0.5, thickness=20,orientation="h", y=-0.15, x=0.5, xanchor='center', title="Diversity Index"),
paper_bgcolor='rgb(249, 249, 249)',
plot_bgcolor='rgb(249, 249, 249)'
)
fig.update_geos(
showocean=True,
oceancolor="#EBF5FB",
showland=True,
landcolor="#F0F0E8",
showlakes=True,
lakecolor="#EBF5FB",
showcountries=True,
countrycolor="#BBBBBB",
bgcolor='rgb(249, 249, 249)'
)
return fig
# Country details callback - journal style formatting
@app.callback(
[Output("selected-country", "children"),
Output("country-profile", "children"),
Output("country-comparison", "children"),
Output("country-chart", "children")],
[Input("world-map", "clickData")]
)
def update_country_details(click_data):
if click_data is None:
return "Select a country on the map", [], [], []
# Extract country name and data
country_name = click_data['points'][0]['location']
country_data = df[df['Country'] == country_name].iloc[0]
# Get key data points
predominant_pet = country_data['Predominant_Pet']
diversity_index = country_data['Diversity_Index']
region = country_data['Region']
# Interpret diversity index
if diversity_index < 25:
diversity_interpretation = "Low diversity (strong preference for one pet type)"
elif diversity_index < 75:
diversity_interpretation = "Medium diversity (some variation in preferences)"
else:
diversity_interpretation = "High diversity (balanced preferences across pet types)"
# Create profile content with journal styling
profile = [
html.H5(f"Pet Ownership Profile",
className="mb-3"),
html.P([
f"{country_name} shows a ",
html.Strong(f"strong preference for {PET_LABELS[predominant_pet].lower()}",
style={"color": PET_COLORS[predominant_pet]}),
f", with {country_data[predominant_pet]:.1f}% of pet ownership."
]),
html.P([
f"The country has ",
html.Strong(diversity_interpretation),
f" (index: {diversity_index:.1f}/100)."
]),
# Regional context - new comparative information
html.P([
f"Compared to other {region} countries, {country_name}'s pet preference profile is ",
html.Strong("typical" if abs(country_data[predominant_pet] -
df[df['Region'] == region][predominant_pet].mean()) < 10
else "distinctive"),
"."
])
]
# Create comparison with global and regional averages
comparison_data = pd.DataFrame({
'Category': ['Dogs', 'Cats', 'Fish', 'Birds'],
'Country': [country_data['Dog'], country_data['Cat'],
country_data['Fish'], country_data['Bird']],
'Region Avg': [df[df['Region'] == region]['Dog'].mean(),
df[df['Region'] == region]['Cat'].mean(),
df[df['Region'] == region]['Fish'].mean(),
df[df['Region'] == region]['Bird'].mean()],
'Global Avg': [df['Dog'].mean(), df['Cat'].mean(),
df['Fish'].mean(), df['Bird'].mean()]
})
# Find notable differences with journal styling
notable = []
for pet in ['Dog', 'Cat', 'Fish', 'Bird']:
country_val = country_data[pet]
global_val = df[pet].mean()
diff = country_val - global_val
if abs(diff) > 15: # Significant difference threshold
direction = "higher" if diff > 0 else "lower"
notable.append(
html.Li(f"{PET_LABELS[pet]}: {abs(diff):.1f}% {direction} than global average",
style={"marginBottom": "5px"})
)
# Create comparison section with journal styling
comparison_section = [
html.H5("Notable Differences",
className="mt-4 mb-3"),
html.Ul(notable, style={"paddingLeft": "20px"}) if notable else
html.P("No significant deviations from global averages."
)
]
# Create chart with journal styling
fig = px.bar(
comparison_data,
x='Category', y=['Country', 'Region Avg', 'Global Avg'],
barmode='group',
labels={'value': 'Percentage', 'variable': ''},
title=f"Pet Preferences: {country_name} vs. Averages(%)",
color_discrete_sequence=['#5D6D7E', '#85929E', '#AEB6BF'] # Journal-like muted colors
)
fig.update_layout(
legend_title_text='',
margin=dict(l=40, r=40, t=40, b=40),
height=225,
plot_bgcolor='rgba(0,0,0,0)',
paper_bgcolor='rgba(0,0,0,0)',
autosize=True
)
fig.update_xaxes(showgrid=False)
fig.update_yaxes(gridcolor='#eee')
country_chart = dcc.Graph(figure=fig)
return f"{country_name}", profile, comparison_section, country_chart
@app.callback(
Output("diversity-info-container", "style"),
[Input("map-color-option", "value")]
)
def toggle_diversity_info_button(color_option):
if color_option == 'diversity':
return {"display": "inline-block"}
else:
return {"display": "none"}
# 2. Callback para abrir el modal cuando se hace clic en el botón
@app.callback(
Output("diversity-modal", "is_open"),
[Input("diversity-info-button", "n_clicks"), Input("close-diversity-modal", "n_clicks")],
[State("diversity-modal", "is_open")]
)
def toggle_modal(n1, n2, is_open):
if n1 or n2:
return not is_open
return is_open
if __name__ == '__main__':
app.run_server(debug=True)