Programmatically grouping multiple layers into separate groups using QGIS?

You can do it in three steps: Get group names, create groups, and move layers.

For testing purposes, I've replicated your sample scenario:

enter image description here

Run the following code snippet in your QGIS Python console:

# 1. Get group names and list of layer ids
root = QgsProject.instance().layerTreeRoot()
dictGroups={}
for layer in root.findLayers():
  if QgsLayerTree.isLayer(layer):
    prefix="Site "+layer.layerName().split("_")[0] # Adjust this to fit your needs
    if not prefix in dictGroups:
      dictGroups[prefix]=[]
    dictGroups[prefix].append(layer.layerId())

# 2. Create groups
for key in dictGroups:
  root.addGroup(key)

# 3. Move layers
for key in dictGroups:
  parent = root.findGroup(key)
  for id in dictGroups[key]:
    layer = root.findLayer(id)
    clone = layer.clone()
    parent.insertChildNode(0, clone)
    root.removeChildNode(layer)

You should get something like this:

enter image description here


I'm only adding a little extra here but I hope the explination may help others.

I spent a long time wrapping my head around nodes vs layers.

It seems that the Layers are the "heavy weight" underlying objects, and the nodes are used to represent the arrangement in the tree. (https://qgis.org/api/classQgsLayerTreeNode.html#details)

I offer the below code with couple of advantages over the accepted post (and I would't have gotten here with out the accepted answer, so THANK YOU to Germán Carrillo):

  1. It manipulates the tree rather than any layers. (Actually, just double-checked the docs - findLayer does actually return a LayerTreeNode (as a LayerTreeLayer), so stays in the "node" realm. Sometimes shortening names can lead to misdirection...)
  2. It gets the selected nodes and only forms them into a sub-group. (This was my own personal use-case, not necessarily what the OP asked for.)
  3. Keeps object (node) references rather than collecting and the re-looking up QGIS layer id's.
  4. Uses addChildNode rather than insertChildNode(0,..., in order to preserve the order of the nodes in the new group.
  5. Deliberately using an extra loop so I could verify correct working before deleting the original nodes. (The last three loops can actually be combined.)

#

v = iface.layerTreeView()
# v.collapseAllNodes()
nodes = v.selectedNodes()
parent = nodes[0].parent()

groups = {}
for n in nodes: 
    key = n.name()[:1]
    if not key in groups:
        groups[key]=[]
    groups[key].append(n)

new_groups = {}
for key in groups:
    grp_name = key+'00'
    grp = parent.addGroup(grp_name)
    new_groups[key] = grp

for key in groups:
    new_group = new_groups[key]
    for n in groups[key]:
        clone = n.clone()
        new_group.addChildNode(clone)

for key in groups:
    for n in groups[key]:
        parent.removeChildNode(n)