Skip to content

ENH: Method for building Spatiotemporal Graphs #873

Description

@FirePheonix

As discussed in
GWLearn's issue: pysal/spml#69

To perform spatiotemporal regression (GTWR), users currently have to manually build separate spatial and temporal graphs, intersect them, and handle the weight multiplication themselves. This is very complicated.

There's a need for adding a first class method to handle spatio-temporal weight construction.
We can likely use the "Product of Kernels" approach ($W_{ST} = W_S \times W_T$). This allows independent bandwidths for space and time.
Few options I'd like to mention (totally open for more, for a more cleaner API)

  1. Graph.build_spatiotemporal
    A dedicated constructor that takes both spatial and temporal data.
st_graph = Graph.build_spatiotemporal(
    gdf, 
    t_values, 
    spatial_k=50, 
    temporal_bandwidth=365
)
  1. Hadamard Product (Graph.multiply)
    A more general method that allows scaling weights of one graph by those of another (assuming matching edges/indices).
spatial_graph = Graph.build_knn(gdf, k=50)
temporal_graph = Graph.build_kernel(t_values, bandwidth=365)
st_graph = spatial_graph.multiply(temporal_graph)

Moving this to libpysal ensures:

A clean, "PySAL-ic" API that favors composition.
Cross-package compatibility (e.g., gwlearn and esda can both use the same ST graph logic). Libraries like gwlearn or esda can focus on modeling/statistics rather than.. having an unnecessary graph intersection logics

Activity

  1. FirePheonix commented on Feb 23, 2026

    @FirePheonix
    ContributorAuthor

    Thanks for the redirect @martinfleis , and so sorry for the confusion..
    What I was thinking, we can put (Graph.multiply) as the primitive, with build_spatiotemporal as a thin convenience wrapper on top.

    The reason for multiply as primitive the same compositional style as the existing intersection - so, you build two graphs independently, then combine them with one operation:

    # the current pattern:
    st_graph = temporal_g.intersection(spatial_g)
    # the new pattern:
    st_graph = temporal_g.multiply(spatial_g)

    The math behind it:

    $$W_{ST}(i,j) = W_S(i,j) \cdot W_T(i,j)$$

    Using separate bandwidths $h_S$ and $h_T$ for space and time avoids the steepness issue that arises when spatial and temporal distances are collapsed into a single Euclidean metric. (the results related to same method are in the pysal/spml#69 issue's discussion)

    For users who don't need complete/fine-grained control, build_spatiotemporal automates the process easily with:

    # full control:
    spatial_g  = Graph.build_kernel(gdf, bandwidth=500, kernel="gaussian")
    temporal_g = Graph.build_kernel(t_dists, metric="precomputed", bandwidth=365, kernel="gaussian")
    st_graph   = temporal_g.multiply(spatial_g)
    
    # one-liner:
    st_graph = Graph.build_spatiotemporal(gdf, t=t_values, spatial_bandwidth=500, temporal_bandwidth=365)

    please let me know your opinion on this proposal.. I'd love to navigate and work upon building the correct, developer friendly spatiotemporal graph building api.. maybe i'm thinking of too much of a bloated api right now considering two methods. please correct me wherever i'm wrong.

  2. martinfleis commented on Feb 23, 2026

    @martinfleis
    Member

    I'd like to hear what others think because some folks here have way more experience with spatiotemporal analysis than I do.

    I am not a big fan of build_spatiotemporal as it implicitly reduces spatiotemporal graphs to a kernel graphs on the spatial side. I think we should avoid that and ensure that you can turn any spatial graph spatiotemporal by intersecting (or multiplying) it with time. The current way of doing that via any spatial graph, kernel builder to get temporal one and an intersection works until you want to make use of weights, as intersection reduces those to binary. The multiply option could be a step forward, though I am still not convinced that multiplication is always the best way of combining the weights.

    Maybe we should expand capabilities of our algebraic ops to do the actual algebra on weights rather than doing that only on index. That way you could technically write your own formula. In practice, it is just an operation on adjacency tables. Not sure, let's wait for other ideas.

    edit: for a reference, here they're using the product of spatial and temporal weights https://www.sciencedirect.com/science/article/pii/S0167587799001117

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions