Search code examples
pandasnumpygeopandasshapelyplotly-express

Plotting ways (linestrings) over a map in Python


this is my second try for the same question and I really hope that someone may help me... Even thought some really nice people tried to help me. There is a lot I couldn't figure out, despite there help.

From the beginning: I created a dataframe. This dataframe is huge and gives information about travellers in a city. The dataframe looks like this. This is only the head.

In origin and destination you have the ids of the citylocations, in move how many travelled from origin to destination. longitude and latitude is where the exact point is and the linestring the combination of the points..

I created the linestring with this code:

erg2['Linestring'] = erg2.apply(lambda x: LineString([(x['latitude_origin'], x['longitude_origin']), (x['latitude_destination'], x['longitude_destination'])]), axis = 1)

Now my question is how to plot the ways over a map. Even thought I tried all th eexamples from the geopandas documentary etc. I cant help myself..

I cant show you what I already plotted because it doesnt make sense and I guess it would be smarter to start plotting from the beginning.

  1. You see that in the column move there are some 0. This means that no one travelled this route. So this I dont need to plot..

  2. I have to plot the lines with the information where the traveller started origin and where he went destination.

  3. also I need to outline the different lines depending on movements..

with this plotting code

fig = px.line_mapbox(erg2, lat="latitude_origin", lon="longitude_origin", color="move", 
hover_name= gdf["origin"] + " - " + gdf["destination"],
                         center =dict(lon=13.41053,lat=52.52437), zoom=3, height=600
                        )
    
    fig.update_layout(mapbox_style="stamen-terrain", mapbox_zoom=4, mapbox_center_lat = 52.52437,
        margin={"r":0,"t":0,"l":0,"b":0})
    
    fig.show()

Maybe someone has an idea???

I tried it with thios code:

    import requests, io, json
import geopandas as gpd
import shapely.geometry
import pandas as pd
import numpy as np
import itertools
import plotly.express as px

# get some public addressess - hospitals.  data that has GPS lat / lon
dfhos = pd.read_csv(io.StringIO(requests.get("http://media.nhschoices.nhs.uk/data/foi/Hospital.csv").text),
    sep="¬",engine="python",).loc[:, ["OrganisationName", "Latitude", "Longitude"]]

a = np.arange(len(dfhos))
np.random.shuffle(a)
# establish N links between hospitals
N = 10
df = (
    pd.DataFrame({0:a[0:N], 1:a[25:25+N]}).merge(dfhos,left_on=0,right_index=True)
    .merge(dfhos,left_on=1, right_index=True, suffixes=("_origin", "_destination"))
)

# build a geopandas data frame that has LineString between two hospitals
gdf = gpd.GeoDataFrame(
    data=df,
    geometry=df.apply(
        lambda r: shapely.geometry.LineString(
            [(r["Longitude_origin"], r["Latitude_origin"]),
             (r["Longitude_destination"], r["Latitude_destination"]) ]), axis=1)
)

# sample code https://plotly.com/python/lines-on-mapbox/#lines-on-mapbox-maps-from-geopandas
lats = []
lons = []
names = []

for feature, name in zip(gdf.geometry, gdf["OrganisationName_origin"] + " - " + gdf["OrganisationName_destination"]):
    if isinstance(feature, shapely.geometry.linestring.LineString):
        linestrings = [feature]
    elif isinstance(feature, shapely.geometry.multilinestring.MultiLineString):
        linestrings = feature.geoms
    else:
        continue
    for linestring in linestrings:
        x, y = linestring.xy
        lats = np.append(lats, y)
        lons = np.append(lons, x)
        names = np.append(names, [name]*len(y))
        lats = np.append(lats, None)
        lons = np.append(lons, None)
        names = np.append(names, None)

fig = px.line_mapbox(lat=lats, lon=lons, hover_name=names)

fig.update_layout(mapbox_style="stamen-terrain",
                  mapbox_zoom=4,
                  mapbox_center_lon=gdf.total_bounds[[0,2]].mean(),
                  mapbox_center_lat=gdf.total_bounds[[1,3]].mean(),
                  margin={"r":0,"t":0,"l":0,"b":0}
                 )

which looks like the perfect code but I cant really use it for my data.. I am very new to coding. So please be patient a bit;))

Thanks a lot in advance.

All the best


Solution

    • previously answered this question How to plot visualize a Linestring over a map with Python?. I suggested that you update that question, I still recommend that you do
    • line strings IMHO are not the way to go. plotly does not use line strings, so it's extra complexity to encode to line strings to decode to numpy arrays. check out the examples on official documentation https://plotly.com/python/lines-on-mapbox/. here it is very clear geopandas is just a source that has to be encoded into numpy arrays

    data

    • your sample data it appears should be one Dataframe and has no need for geopandas or line strings
    • almost all of your sample data is unusable as every row where origin and destination are different have move of zero which you note should be excluded
    import pandas as pd
    import numpy as np
    import plotly.express as px
    
    df = pd.DataFrame({"origin": [88, 88, 88, 88, 88, 87], 
                       "destination": [88, 89, 110, 111, 112, 83], 
                       "move": [20, 0, 5, 0, 0, 10], 
                       "longitude_origin": [13.481016, 13.481016, 13.481016, 13.481016, 13.481016, 13.479667], 
                       "latitude_origin": [52.457055, 52.457055, 52.457055, 52.457055, 52.457055, 52.4796], 
                       "longitude_destination": [13.481016, 13.504075, 13.613772, 13.586891, 13.559341, 13.481016], 
                       "latitude_destination": [52.457055, 52.443923, 52.533194, 52.523562, 52.507418, 52.457055]})
    

    solution

    • have further refined line_array() function so it can be used to encode hover and color parameters from simplified solution I previously provided
    # lines in plotly are delimited by none
    def line_array(data, cols=[], empty_val=None):
        if isinstance(data, pd.DataFrame):
            vals = data.loc[:, cols].values
        elif isinstance(data, pd.Series):
            a = data.values
            vals = np.pad(a.reshape(a.shape[0], -1), [(0, 0), (0, 1)], mode="edge")
        return np.pad(vals, [(0, 0), (0, 1)], constant_values=empty_val).reshape(
            1, (len(df) * 3))[0]
    
    
    # only draw lines where move > 0 and destination is different to origin
    df = df.loc[df["move"].gt(0) & (df["origin"]!=df["destination"])]
    
    lons = line_array(df, ["longitude_origin", "longitude_destination"])
    lats = line_array(df, ["latitude_origin", "latitude_destination"])
    
    fig = px.line_mapbox(
        lat=lats,
        lon=lons,
        hover_name=line_array(
            df.loc[:, ["origin", "destination"]].astype(str).apply(" - ".join, axis=1)
        ),
        hover_data={
            "move": line_array(df, ["move", "move"], empty_val=-99),
            "origin": line_array(df, ["origin", "origin"], empty_val=-99),
        },
        color=line_array(df, ["origin", "origin"], empty_val=-99),
    ).update_traces(visible=False, selector={"name": "-99"})
    
    fig.update_layout(
        mapbox={
            "style": "stamen-terrain",
            "zoom": 9.5,
            "center": {"lat": lats[0], "lon": lons[0]},
        },
        margin={"r": 0, "t": 0, "l": 0, "b": 0},
    )
    

    enter image description here