Clarity with Data: Logistics Decision Making under Risk#

Balancing Cost and Risk: Optimizing Supply Chains through Advanced Modeling and Network Visualization#

In this series, we go beyond visualization to highlight features of supply chain optimization. We demonstrate how to strike the delicate balance between cost efficiency and disruption risk mitigation by leveraging advanced modeling techniques and real-world data. Whether you’re a supply chain manager, logistics expert, or data analyst, this tutorial empowers you to transform raw supply chain data into strategic insights and interactive network graphs, fostering informed decision-making and building resilience in an increasingly complex global landscape.

Use Case: Data-Driven Resilience for Supply Chains Facing Uncertainty#

In today’s volatile global landscape, supply chains face constant disruptions from natural disasters, geopolitical events, and unforeseen crises. Traditional risk management strategies, often reactive and siloed, struggle to keep pace with the dynamic nature of modern supply chains.

Why It Matters:#

  • Comprehensive Risk Analysis: Go beyond simple visualization to conduct in-depth risk analysis, considering factors like supplier stability, transportation vulnerabilities, and potential disruptions.

  • Proactive Decision-Making: Leverage advanced modeling and simulation to forecast the impact of various scenarios, empowering stakeholders to make informed decisions about sourcing, inventory, and logistics.

  • Increased Efficiency & Resilience: Identify cost-effective transportation routes while minimizing exposure to risk, ensuring a resilient and efficient supply chain.

Practical Example:#

Consider an industrial valve manufacturer in the Midwest facing a potential hurricane threat to its coastal suppliers. By leveraging our platform’s analytical capabilities, the company can:

  • Model Complex Interdependencies: Map the entire supply chain network, capturing intricate relationships between suppliers, manufacturers, and customers.

  • Quantify & Predict Risks: Assess the financial and operational impact of potential disruptions, utilizing predictive analytics to forecast the likelihood and severity of events.

  • Optimize for Resilience: Identify alternative sourcing options and transportation routes that balance cost efficiency with risk mitigation, ensuring continuity of operations even in the face of disruptions. This example highlights how our platform transcends visualization, enabling data-driven decision-making and proactive risk management. By combining advanced analytics with interactive network visualization, we empower organizations to build resilient supply chains that thrive amidst uncertainty.

Install#

We begin by downloading and installing our needed libraries.

Note: If running this notebook for the first time, please uncomment the following line to install the required packages.

Hide code cell source
#!pip install networkx osmnx folium plotly ipython
Hide code cell source
import warnings
warnings.filterwarnings('ignore') # Ignore all warnings
import os
import random
import logging
import time
import networkx as nx
import osmnx as ox
import folium
from folium import Map, PolyLine
from folium.plugins import MeasureControl
from IPython.display import IFrame, display

# Configure Plotly to display figures inline in the notebook
import plotly.io as pio
pio.renderers.default = 'notebook'

logging.disable(logging.CRITICAL)

Step 1: Define Scope#

The scope of the model includes the valve manufacturer, its coastal suppliers, and the transportation routes between them. The model will focus on the risk of hurricane disruption to the supply chain.

Step 2: Define Supplier Regions and Bootstrap the Logistics Network#

In this step, we will define the geographic regions of the valve manufacturer and its coastal suppliers. We will then bootstrap the logistics network by leveraging OpenStreetMap (OSM) data to create a road network that connects these regions. This network will serve as the foundation for analyzing the impact of potential disruptions.

Supplier Regions#

The supplier regions include:

  • Wausau Region: This region represents the location of the valve manufacturer.

  • Coastal Regions: These regions represent the locations of the suppliers along the coast, which are susceptible to hurricane disruptions.

Bootstrapping the Logistics Network#

To bootstrap the logistics network, we will:

  1. Import Libraries: Import necessary libraries such as osmnx, networkx, and folium.

  2. Define Locations: Specify the geographic coordinates of the Wausau region and the coastal regions.

  3. Create Graph: Use osmnx to download the road network data from OpenStreetMap and create a graph.

  4. Add Nodes and Edges: Add nodes for the Wausau region and coastal regions to the graph, and create edges representing the transportation routes between them.

  5. Visualize Network: Use folium to visualize the supply chain road network on an interactive map.

Code Block#

Hide code cell source
# Set up logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

# Define coastal regions of interest (Gulf Coast and Savannah/Charleston)
regions = [
    "Wausau, WI, USA",
    "Houston, TX, USA",
    "New Orleans, LA, USA",
    "Mobile, AL, USA", 
    "Tampa, FL, USA",
    "Savannah, GA, USA",
    "Charleston, SC, USA"
]

combined_graph_filepath = "combined_graph.graphml"

if os.path.exists(combined_graph_filepath):
    logging.info("Combined graph file already exists. Loading the graph.")
    combined_graph = ox.load_graphml(combined_graph_filepath)
    logging.info("Combined graph loaded successfully")
else:
    graphs = []

    # Function to get the bounding box for a list of places
    def get_bounding_box(places):
        min_lat, min_lon, max_lat, max_lon = float('inf'), float('inf'), float('-inf'), float('-inf')
        for place in places:
            geocode_result = ox.geocode(place)
            lat, lon = geocode_result[0], geocode_result[1]
            min_lat, min_lon = min(min_lat, lat), min(min_lon, lon)
            max_lat, max_lon = max(max_lat, lat), max(max_lon, lon)
        return min_lat, min_lon, max_lat, max_lon

    # Get the bounding box for the regions
    min_lat, min_lon, max_lat, max_lon = get_bounding_box(regions)

    # Function to download graph with retries
    def download_graph_with_retries(query_func, *args, retries=3, delay=60, **kwargs):
        for attempt in range(retries):
            try:
                return query_func(*args, **kwargs)
            except Exception as e:
                logging.error(f"Error downloading graph: {e}")
                if attempt < retries - 1:
                    logging.info(f"Retrying in {delay} seconds...")
                    time.sleep(delay)
                else:
                    raise

    for place in regions:
        filename = f"{place.replace(', ', '_').replace(' ', '_').lower()}.graphml"
        logging.info(f"Processing place: {place}")
        if os.path.exists(filename):
            logging.info(f"Loading graph from file: {filename}")
            # Load the graph from the file if it exists
            graph = ox.load_graphml(filename)
        else:
            logging.info(f"Downloading and building graph for: {place}")
            # Download and build the graph, then save it to a file
            graph = download_graph_with_retries(ox.graph_from_place, place, network_type='drive')
            logging.info(f"Saving graph to file: {filename}")
            ox.save_graphml(graph, filename)
        graphs.append(graph)
        logging.info(f"Graph for {place} processed successfully")

    filename = 'interstates.graphml'

    if os.path.exists(filename):
        logging.info(f"Loading graph from file: {filename}")
        # Load the graph from the file if it exists
        interstates_graph = ox.load_graphml(filename)
    else:
        # Download interstates within the bounding box
        logging.info("Downloading interstates within the bounding box")
        interstates_graph = download_graph_with_retries(
            ox.graph_from_bbox, north=max_lat, south=min_lat, east=max_lon, west=min_lon, custom_filter='["highway"~"motorway"]'
        )
        logging.info(f"Saving graph to file: {filename}")
        ox.save_graphml(interstates_graph, filename)

    graphs.append(interstates_graph)
    logging.info("Interstates graph processed successfully")

    logging.info("All graphs processed successfully")

    # Combine all graphs into a single graph
    combined_graph = nx.compose_all(graphs)
    logging.info("Combined graph created successfully")

    # Save the combined graph to a file (optional)
    ox.save_graphml(combined_graph, filepath=combined_graph_filepath)
    logging.info("Combined graph saved successfully")

Step 3: Simulate the Supplier Network to the Wausau Plant#

In this step, we will simulate the supplier network that connects various suppliers to the Wausau plant. This simulation will help us understand the logistics and potential vulnerabilities in the supply chain. We will use OpenStreetMap (OSM) data to create a realistic road network and randomly select locations for the suppliers and the Wausau plant within a specified region.

Objectives:#

  1. Randomly Select Locations: Randomly select geographic locations for the Wausau plant and its suppliers from the road network graph.

  2. Define Supplier Types: Assign each supplier a type from a predefined list of supplier categories.

  3. Create Transportation Routes: Establish transportation routes from each supplier to the Wausau plant.

  4. Visualize the Network: Use Folium to visualize the supplier network on an interactive map.

Workflow:#

  1. Import Necessary Libraries: We will use libraries such as osmnx for downloading and manipulating OSM data, networkx for graph operations, and folium for visualization.

  2. Define Supplier Types: Create a list of supplier types that are relevant to the manufacturing process, such as Metal Suppliers, Plastic and Polymer Suppliers, Material Suppliers, Casting and Forging Suppliers, Actuator and Control Suppliers, and Packaging Material Suppliers.

  3. Simulate Supplier Network:

    • Create Graph: Use osmnx to create a road network graph from OSM data for the specified region.

    • Randomly Select Locations: Randomly select a node from the graph to represent the Wausau plant location. Similarly, randomly select nodes for the suppliers.

    • Add Nodes and Edges: Add nodes for the Wausau plant and suppliers to the graph. Create edges representing the transportation routes from each supplier to the Wausau plant.

  4. Visualize the Network: Use Folium to plot the nodes and edges on an interactive map, providing a visual representation of the supplier network.

Hide code cell source
# Set up logging
logging.basicConfig(level=logging.ERROR)

# Define supplier types
supplier_types = [
    'Metal Suppliers', 
    'Plastic and Polymer Suppliers', 
    'Material Suppliers', 
    'Casting and Forging Suppliers', 
    'Actuator and Control Suppliers', 
    'Packaging Material Suppliers'
]

# Define regions of suppliers
locations = {
    "Wausau, WI, USA": (44.9591, -89.6301),
    "Houston, TX, USA": (29.7604, -95.3698),
    "New Orleans, LA, USA": (29.9511, -90.0715),
    "Mobile, AL, USA": (30.6954, -88.0399),
    "Savannah, GA, USA": (32.0809, -81.0912),
    "Charleston, SC, USA": (32.7765, -79.9311)
}

# Add a random supplier type to each location, except Wausau which is the Valve Manufacturer
locations_with_suppliers = {
    location: {
        "coordinates": coords,
        "supplier_type": "Valve Manufacturer" if location == "Wausau, WI, USA" else random.choice(supplier_types)
    }
    for location, coords in locations.items()
}

# Print the resulting dictionary
for location, details in locations_with_suppliers.items():
    logging.info(f"{location}: {details}")

# Load the combined graph from the saved file
combined_graph = ox.load_graphml("combined_graph.graphml")

# Define a color map for each supplier type
color_map = {
    'Metal Suppliers': 'blue',
    'Plastic and Polymer Suppliers': 'green',
    'Material Suppliers': 'red',
    'Casting and Forging Suppliers': 'purple',
    'Actuator and Control Suppliers': 'orange',
    'Packaging Material Suppliers': 'brown',
    'Valve Manufacturer': 'black'
}

# Initialize the Folium map centered around Wausau with the default OpenStreetMap tiles
wausau_location = locations_with_suppliers["Wausau, WI, USA"]["coordinates"]
m = folium.Map(location=wausau_location, zoom_start=4)

# Add a marker for the manufacturing site (Wausau)
folium.Marker(
    location=wausau_location,
    popup='Valve Manufacturer',
    icon=folium.Icon(color='black', icon='industry', prefix='fa')
).add_to(m)

for location, details in locations_with_suppliers.items():
    if location == "Wausau, WI, USA":
        continue  # Skip the Wausau plant itself

    start_location = details["coordinates"]
    supplier_type = details["supplier_type"]
    color = color_map[supplier_type]

    # Add a marker for each supplier
    folium.Marker(
        location=start_location,
        popup=f'{supplier_type} ({location})',
        icon=folium.Icon(color=color, icon='warehouse', prefix='fa')
    ).add_to(m)

    # Find the nearest nodes in the graph to the start and end locations
    start_node = ox.distance.nearest_nodes(combined_graph, X=start_location[1], Y=start_location[0])
    end_node = ox.distance.nearest_nodes(combined_graph, X=wausau_location[1], Y=wausau_location[0])

    # Compute the shortest path
    try:
        shortest_path = nx.shortest_path(combined_graph, source=start_node, target=end_node, weight='length')
        logging.info(f"Shortest path for {location} to Wausau computed successfully")
        
        # Get the coordinates of the shortest path
        path_coords = [(combined_graph.nodes[node]['y'], combined_graph.nodes[node]['x']) for node in shortest_path]
        
        # Plot the route on the map with the assigned color
        folium.PolyLine(
            locations=path_coords,
            color=color,
            weight=4,
            opacity=0.7
        ).add_to(m)
    except nx.NetworkXNoPath:
        logging.warning(f"No path found from {location} to Wausau.")

# Add a title to the map
title_html = '''
             <h3 align="center" style="font-size:20px"><b>Supplier Network Map</b></h3>
             '''
m.get_root().html.add_child(folium.Element(title_html))

# Add a scale bar to the map using MeasureControl
m.add_child(MeasureControl(primary_length_unit='kilometers'))

# Save the map to an HTML file
m.save('supplier_map.html')

# Display the map in the notebook
m
Make this Notebook Trusted to load map: File -> Trust Notebook

Conclusion#

By following these steps, the Midwest valve manufacturer can develop a comprehensive model of its supply chain. This model enables the quantification and prediction of risks associated with hurricane disruptions, as well as the evaluation of various risk mitigation strategies. In our next post, we will delve deeper into this model to simulate and predict the supply chain risks in the face of an impending hurricane, providing actionable insights to enhance resilience and preparedness. Stay tuned!