2

I want to classify a road network similar to Strahler's Order (keep track of how many smaller roads flow into a bigger road). I'm working with mountain roads, so most of them "look" like rivers.

The Spatial Analyst "Stream Order" tool derives results from a flow accumulation raster, this does not work for me, as I'm not classifying based on flow/elevation, but based on existing network. Is there a tool that will take my roads network and classify all the "nodes".

I don't have the "Network Analyst" extension, I do have "Spatial Analyst"

3
  • Output will depend on sink node. It is scripting exercise, simple if you use networkX module.
    – FelixIP
    Commented Jan 23, 2018 at 1:52
  • I have a script that will accomplish this task but there are some conditions that must be met for the script to work. 1. The streams need to point in a down hill direction. for you this means the road ends of all order 2 roads must connect to the order 1 line...and so on. 2. The network needs to be connected. The end and start nodes must be coincident with the lines below them. If your data satisfies these criteria then let me know and I will post the code for you.
    – GBG
    Commented Jan 23, 2018 at 5:09
  • Thanks for your replies. GBG, I think my road network satisfies your parameters, if you don't mind sharing your code that would be great. Are you using the networkX module mentioned by FelixIP?
    – Olak
    Commented Jan 23, 2018 at 15:55

2 Answers 2

1

I've had spectacular results using the RivEX tool. See also How to derive stream order from vector network

RivEX works with vector data, so you won't need Spatial Analyst. Nor will you need Network Analyst.

I've only used it with hydro line data, but I can't think of any reason why it wouldn't work with road data, since a line is a line!

RivEX is not free, but the cost is quite reasonable considering the number of functions that it contains.

1
  • I'm the Author behind RivEX, so your plug is much appreciated! I've seen this sort of request before on this forum, people wanting to apply Strahler Order to a road network. Whilst RivEX would generate values for such a network, I caution readers that the underlying assumption behind the algorithm is that the network represents a dentritic pattern, which roads are clearly not. So whilst I guess any strahler ordering algorithm would generate values they are unlikely to be meaningful for a road network and should be interpreted with caution.
    – Hornbydd
    Commented Sep 22, 2020 at 0:44
0

There is no class for rooted trees with Strahler stream orders. So I implemented one according to the algorithm described in the Wikipedia.

The same numbers may also be generated via a pruning process in which the tree is simplified in a sequence of stages, where in each stage one removes all leaf nodes and all of the paths of degree-one nodes leading to leaves: the Strahler number of a node is the stage at which it would be removed by this process, and the Strahler number of a tree is the number of stages required to remove all of its nodes. [1]

import networkx as nx
import pandas as pd
from pandas.core.frame import DataFrame


class StrahlerLevel(DataFrame):
    """Class for one level in a Strahler rooted tree."""

    def __init__(self, dat: DataFrame):
        """Init a data frame for nodes in the lowest level.

        Args:
            dat (DataFrame): types of nodes in different tributaries.
        """
        index = pd.MultiIndex.from_tuples(dat.keys(), names=_index_names[1:3])
        super().__init__(
            dat.values(),
            columns=['type'],
            index=index
        )

    @classmethod
    def parse(cls, rt: nx.DiGraph):
        """Collect nodes in the lost level of a rooted tree.

        Args:
            rt (nx.DiGraph): a rooted (directed) mathematical tree.

        Returns:
            StrahlerLevel: with all nodes in the lost level.
        """
        predecessors = nx.dfs_predecessors(rt)

        def parse_tributary(leaf: str, idx: int) -> Dict[Tuple[str, int], str]:
            """Collect nodes in a tributary specified by the leaf.

            Args:
                leaf (str): leaf node of a tributary.
                idx (int): index of the tributary in the current level.

            Returns:
                Dict[Tuple[str, int], str]: containing the leaf, fork
                    and all middle nodes in a tributary.
            """
            dat_tributary = {}
            dat_tributary[(leaf, idx)] = _types['leaf']
            pre = predecessors[leaf]
            if_continue = True
            while if_continue:
                if rt.out_degree(pre) == 1:
                    dat_tributary[(pre, idx)] = _types['middle']
                elif rt.out_degree(pre) > 1:
                    dat_tributary[(pre, idx)] = _types['fork']
                    if_continue = False
                # Focus on the next predecessor.
                try:
                    pre = predecessors[pre]
                except KeyError:  # When 'pre' reaches the root.
                    dat_tributary[(pre, idx)] = _types['fork']
                    if_continue = False
            return dat_tributary

        leaves = [bus for bus, d in rt.out_degree() if d == 0]
        dat = {}
        idx = 1
        for leaf in leaves:
            dat.update(parse_tributary(leaf, idx))
            idx += 1
        return cls(dat)

    @property
    def nodes_to_remove(self) -> List[str]:
        """Collect nodes in the current level excluding forking nodes.

        Returns:
            List[str]: nodes in the current Strahler level.
        """
        to_remove = self.loc[self['type'] >= 0]
        return to_remove.index.unique('node')


class StrahlerRootedTree(nx.DiGraph):
    """Class for rooted trees with Strahler ordering system.
    
    Attributes:
        df_strahler (DataFrame): all nodes with Strahler ordering info.
    """

    def __init__(self, rt: nx.DiGraph):
        """Init a rooted tree with Strahler ordering system.

        Args:
            rt (nx.DiGraph): initiated rooted tree.
        """
        super().__init__(rt)
        self._launch()

    def _set_level(self, nodes: List[str], level: int):
        """Assign Strahler numbers to all nodes and edges in a level.

        Args:
            nodes (List[str]): names of nodes to be assigned a number.
            level (int): which Strahler number to be assigned.
        """
        for n in nodes:
            self.nodes[n]['level'] = level
            the_edge = list(self.in_edges(n))[0]
            self.edges[the_edge]['level'] = level

    def _launch(self):
        """Analyse the rooted tree and store Strahler numbers.

        Raises:
            Exception: when the analysis is unable to terminate withing
                reasonable number of iterations.
        """
        rt = nx.DiGraph(self)  # Create a mutable directed graph.
        sls = {}
        level = 0
        while len(rt.nodes) > 1 and level <= 10:
            level += 1
            sls[level] = StrahlerLevel.parse(rt)
            rt.remove_nodes_from(sls[level].nodes_to_remove)
            self._set_level(
                nodes=sls[level].nodes_to_remove, level=level
            )

        if level == 100:
            raise Exception("The iteration is interrupted by the counter!")

        self.df_strahler = pd.concat(
            sls.values(), keys=sls.keys(), names=_index_names
        )
        self.nodes[self.root]['level'] = level

  1. https://en.wikipedia.org/wiki/Strahler_number
1
  • Is this technically still Strahler's number? I like this solution, but using the pruning method and removing each leaf node would mean that no two nodes downstream of one another can ever have equal values for the Strahler's number? In other words, in the wikipedia figure the two connecting streams that both have values of "3" would have to change to a "3" and a "4" using this method right? Or am I mistaken?
    – B. R.
    Commented Jul 27, 2021 at 22:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.