TidyTuesday Week 36: Henley Passport Index Data

This week we are exploring data from the Henley Passport Index API. The Henley Passport Index is produced by Henley & Partners and captures the number of countries to which travelers in possession of each passport in the world may enter visa free.

TidyTuesday
Data Visualization
Python Programming
2025
streamlit
dashboard
Author

Peter Gray

Published

September 8, 2025

Chart FIgure of my passport app :::

1. Python code

Show code
import pandas as pd
import streamlit as st
import plotly.express as px
import pycountry
import ast

country_lists = pd.read_csv(
    "https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2025/2025-09-09/country_lists.csv"
)
rank_by_year = pd.read_csv(
    "https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2025/2025-09-09/rank_by_year.csv"
)


visa_columns = [
    "visa_required",
    "visa_online",
    "visa_on_arrival",
    "visa_free_access",
    "electronic_travel_authorisation",
]

visa_columns_map = {
    "Visa Required": "visa_required",
    "eVisa (Online Application)": "visa_online",
    "Visa on Arrival": "visa_on_arrival",
    "Visa Free Access": "visa_free_access",
    "Electronic Travel Authorisation (ETA)": "electronic_travel_authorisation",
}

for col in visa_columns:
    country_lists[col] = country_lists[col].apply(
        lambda x: ast.literal_eval(x) if pd.notnull(x) else []
    )


tab1, tab2 = st.tabs(
    ["📊 Change in Visa Ranking Graph", "🌍 Interactive Visa Requirements Map"]
)

with tab1:
    st.subheader("Change in Country Visa Ranking over time")
    default = ["France", "Germany", "Italy", "Spain", "United Kingdom", "United States"]
    countries = sorted(rank_by_year["country"].unique())

    # set up the containers
    with st.container(border=True):
        country = st.multiselect("Countries", countries, default=default)

    data = (
        rank_by_year[rank_by_year["country"].isin(country)]
        .pivot(index="year", columns="country", values="rank")
        .sort_index()
    )

    plotly_data = data.reset_index().melt(
        id_vars="year", var_name="country", value_name="rank"
    )

    rank_fig = px.line(
        plotly_data,
        x="year",
        y="rank",
        color="country",
        labels={
            "year": "Year",
            "rank": "Visa Free Access Rank",
            "country": "Country",
        },
        title="Country Visa Free Access Rank Over Time",
    )
    rank_fig.update_yaxes(autorange="reversed")

    # ✅ display inside tab1
    st.plotly_chart(rank_fig, use_container_width=True)

with tab2:
    st.subheader("Interactive Visa Map")

    # Sort the Data
    country_lists["country"] = sorted(country_lists["country"])

    # Dropdown for selecting home country
    home_country = st.selectbox(
        "Select your country of citizenship:", country_lists["country"]
    )

    visa_label = st.selectbox(
        "Select the Visa Requirement of Interest:", list(visa_columns_map.keys())
    )
    visa_type = visa_columns_map[visa_label]

    # Extract destinations for selected visa type
    destinations_raw = country_lists.loc[
        country_lists["country"] == home_country, visa_type
    ].values[0]

    # Flatten nested [[{...}]] → [{...}]
    if (
        isinstance(destinations_raw, list)
        and len(destinations_raw) == 1
        and isinstance(destinations_raw[0], list)
    ):
        destinations_raw = destinations_raw[0]

    if isinstance(destinations_raw, list) and len(destinations_raw) > 0:
        df_map = pd.DataFrame(destinations_raw)

        # Convert alpha-2 → alpha-3
        def alpha2_to_alpha3(alpha2):
            try:
                return pycountry.countries.get(alpha_2=alpha2).alpha_3
            except:
                return None

        df_map["iso_alpha3"] = df_map["code"].apply(alpha2_to_alpha3)
        df_map = df_map.dropna(subset=["iso_alpha3"])

        if not df_map.empty:
            fig = px.choropleth(
                df_map,
                locations="iso_alpha3",
                hover_name="name",
                color_discrete_sequence=["#0083B8"],
                title=f"{visa_type.replace('_',' ').title()} Destinations for {home_country}",
            )
            st.plotly_chart(fig, use_container_width=True)
        else:
            st.warning(f"No {visa_type} destinations found for {home_country}.")
    else:
        st.warning(f"No {visa_type} data available for {home_country}.")

    st.write(
        f"**Countries with {visa_type.replace('_',' ').title()} for {home_country}:**"
    )
    df_map = df_map[["name", "code"]].rename(
        columns={"name": "Name", "code": "2 Digit Country Code"}
    )
    st.dataframe(df_map[["Name", "2 Digit Country Code"]])
Back to top