Systems Thinking in R

systems thinking causal loop diagram network flows r

In this post we will explore how to utilize systems thinking tools in R and network flows with the goal of increasing understanding of a system.

Author

Affiliation

Blake Conrad

 

Published

Sept. 2, 2022

DOI

Introduction

In this post we will explore how to effectively draw out Causal Loop Diagrams after a system has been designed. The plan is to:

  1. Obtain a system
  2. Extend the system with goals
  3. Flow through the system
  4. Analyze the flow

We will be leveraging the Systems Thinker post to obtain a thought out system, and we will add a couple Goal nodes and flow Positive or Negative directions to them. Once we flow through some positive and negative energy we will see how the system responds to achieving it’s goal. This is not system dynamics. The purpose of this post is to loosely extend systems thinking and systems causal loop diagramming with network flows and utility theory to see how a goal is accomplished within a system’s structure. The end-state for this will be a clear understanding of which system components are 1) primary drivers to the goal, 2) enablers to the goal, 3) edifying the system from it’s goals (in the given structure), 4) deterring the system from it’s goals (in the given structure).

Obtain a system

We will be using a very simple system for this post. Training causing activities, causing threat perception from managers, limiting those activities. We will extend this by adding a simple goal Employee Skill Development.

Visualize the system

#library(remotes)
#remotes::install_github("jarrod-dalton/causalloop")
library("causalloop")
L <- CLD(from=c("TQM Training", 
                "TQM Activities",
                "TQM Activities",
                "Perceived Threat",
                "Resistence by Middle Managers",
                "TQM Activities"),
         to=c("TQM Activities",
              "TQM Training",
              "Perceived Threat",
              "Resistence by Middle Managers",
              "TQM Activities",
              "Goal: Employee Skill Development"), 
         polarity = c(1,
                      1,
                      1,
                      1,
                      -1,
                      1
           
         )
         )
# L$edges
plot(L)
%0 2->3 3->4 4->1 4->2 4->5 5->4 1 Goal: Employee Skill Development 2 Perceived Threat 3 Resistence by Middle Managers 4 TQM Activities 5 TQM Training

Clearly we can see that increasing training will increase activities and increase the goal. But a side effect occurs with a perceived threat and action by middle managers. Let’s see if we can better identify this through a network flow algorithm.

Flow through the system

If we simulate 1000 units flowing through the system, how does it balance with the negative feedback loop from middle managers? How does it affect the goal of the system?

library(ggplot2)

simulate_flow <- function(L, n = 1000, normalize = TRUE, plot = TRUE){
  L$nodes$score = 1
  for(sim in 1:n){
    for(i in 1:nrow(L$edges)){
      # print(i)
      edge <- L$edges[i, ]
      
      # cat("Adding ", edge$polarity, " from ", edge$from, " to ",edge$to, ".\n")
      tgt_idx = which(L$nodes$node == edge$to)
      src_idx = which(L$nodes$node == edge$from)
      L$nodes$score[tgt_idx] = L$nodes$score[tgt_idx] + 
                                  L$nodes$score[src_idx] * edge$polarity
    }
  }
  
  if(normalize){
    # L$nodes$score = L$nodes$score / n
    L$nodes$score = L$nodes$score / sum(L$nodes$score)
  }
  
  if(plot){
    plt <- ggplot(L$nodes) +
            geom_col(aes(x=reorder(node, -score),y=score, fill=score))+
            scale_fill_gradient(low = "red", high = "green") +
            coord_flip() +
            theme_bw() +
            ggtitle("Systemic Behavior (Desire) Polarity Chart",
                    subtitle = "Red = Desire to Decay | Green = Desire to Grow") +
            labs(fill = "Score", x="Score", y = "Node")
    
  }
  else{
    plt <- ggplot()
  }
  
  list(nodes = L$nodes,
       plt = plt)
}

nodes <- simulate_flow(L, n = 1000, normalize = TRUE, plot=TRUE)
nodes$nodes
# A tibble: 5 × 3
  node                             group       score
  <chr>                            <chr>       <dbl>
1 Goal: Employee Skill Development <default>  0.0834
2 Perceived Threat                 <default>  0.353 
3 Resistence by Middle Managers    <default>  0.417 
4 TQM Activities                   <default> -0.206 
5 TQM Training                     <default>  0.353 

Okay, let’s take a step back. What did we just do? With the following formula:

balance(nodej)=Nn=1a=(i,j)Nbalance(nodej)+flow(i,j)polarity(i,j)jJnodes

Note: The j is fixed, so the inner sum will only trigger for this specific node, then for all nodes.

This gives us a way to see how the flows are going to naturally navigate through the system. So once we accrue a flow balance for each node, we should normalize so all are compared equally.

balance(nodej)=balance(nodej)i=1balance(nodei)foralljJnodes

Analyze the flow

nodes$plt

A few remarks.

  1. TQM Activities desire to be decreased in favor of all the non-zero value nodes.
  2. The goal is attained, but with 1/4 the cost/benefit due to the training being over 4x greater in value.
  3. The real driver/desire is the resistance by middle managers.

This system isn’t horrible, because the goal will eventually be attained with more effort; but it certainly isn’t optimal.

More Complex System

Let’s consider some more realistic dynamics and see how this method weathers.

library("causalloop")

L <- CLD(from=c("TQM Training", 
                "TQM Activities",
                "TQM Activities",
                "Perceived Threat",
                "Resistence by Middle Managers",
                "TQM Activities",
                "TQM Activities",
                "Labor Expended on Activities"),
         to=c("TQM Activities",
              "TQM Training",
              "Perceived Threat",
              "Resistence by Middle Managers",
              "TQM Activities",
              "Goal: Employee Skill Development",
              "Labor Expended on Activities",
              "TQM Training"), 
         polarity = c(1,
                      1,
                      1,
                      1,
                      -1,
                      1,
                      1,
                      -1
           
         )
         )
# L$edges
plot(L)
%0 2->6 3->4 4->5 5->1 5->2 5->3 5->6 6->5 1 Goal: Employee Skill Development 2 Labor Expended on Activities 3 Perceived Threat 4 Resistence by Middle Managers 5 TQM Activities 6 TQM Training

Notice we now have several feedback loops. Two balancing, one reinforcing.

library(dplyr)
getLoops(L, S = 100, nsteps = 6)
[[1]]
[[1]]$loopIx
[1] 1

[[1]]$label
[1] ""

[[1]]$type
[1] "Balancing"

[[1]]$loop
# A tibble: 3 × 3
  from                         to                           polarity
  <chr>                        <chr>                           <dbl>
1 TQM Training                 TQM Activities                      1
2 TQM Activities               Labor Expended on Activities        1
3 Labor Expended on Activities TQM Training                       -1


[[2]]
[[2]]$loopIx
[1] 2

[[2]]$label
[1] ""

[[2]]$type
[1] "Reinforcing"

[[2]]$loop
# A tibble: 2 × 3
  from           to             polarity
  <chr>          <chr>             <dbl>
1 TQM Training   TQM Activities        1
2 TQM Activities TQM Training          1


[[3]]
[[3]]$loopIx
[1] 3

[[3]]$label
[1] ""

[[3]]$type
[1] "Balancing"

[[3]]$loop
# A tibble: 3 × 3
  from                          to                            polarity
  <chr>                         <chr>                            <dbl>
1 TQM Activities                Perceived Threat                     1
2 Perceived Threat              Resistence by Middle Managers        1
3 Resistence by Middle Managers TQM Activities                      -1


attr(,"class")
[1] "feedbackLoops"
nodes <- simulate_flow(L, n = 600, normalize = TRUE, plot=TRUE)


nodes$nodes
# A tibble: 6 × 3
  node                             group      score
  <chr>                            <chr>      <dbl>
1 Goal: Employee Skill Development <default> -0.604
2 Labor Expended on Activities     <default> -0.604
3 Perceived Threat                 <default>  0.489
4 Resistence by Middle Managers    <default>  0.731
5 TQM Activities                   <default> -0.404
6 TQM Training                     <default>  1.39 

Notice by adding the realistic consideration of labor expended on activities (plus the psychological affects of middle management resisting) it will balance the activities. Since the activities are already being balanced by middle management, this further balances the training themselves, because like all things, the system is connected.

nodes$plt

This system is toxic. The goal is never attained, activities go down, but there is an interesting desire within the system to decrease labor and increase training. However, through resistance the main efforts of the system are not accomplished.

Final Scenario

What if the goal itself reinforced the training (after all, good work does pay off with rewards)? what if the resistance from managers started to increase labor on the training themselves (after a while, people get tired)?

library("causalloop")

L <- CLD(from=c("TQM Training", 
                "TQM Activities",
                "TQM Activities",
                "Perceived Threat",
                "Resistence by Middle Managers",
                "TQM Activities",
                "TQM Activities",
                "Labor Expended on Activities",
                "Goal: Employee Skill Development",
                "Resistence by Middle Managers"),
         to=c("TQM Activities",
              "TQM Training",
              "Perceived Threat",
              "Resistence by Middle Managers",
              "TQM Activities",
              "Goal: Employee Skill Development",
              "Labor Expended on Activities",
              "TQM Training",
              "TQM Training",
              "Labor Expended on Activities"), 
         polarity = c(1,
                      1,
                      1,
                      1,
                      -1,
                      1,
                      1,
                      -1,
                      1,
                      1
           
         )
         )
# L$edges
plot(L)
%0 1->6 2->6 3->4 4->2 4->5 5->1 5->2 5->3 5->6 6->5 1 Goal: Employee Skill Development 2 Labor Expended on Activities 3 Perceived Threat 4 Resistence by Middle Managers 5 TQM Activities 6 TQM Training

Now we have a reward signal from the goal back into training. Let’s see how this changes things.

nodes <- simulate_flow(L, n = 500, normalize = TRUE, plot=TRUE)
nodes$plt

Again this system is not constructive. The goal will diminish over time. The desire to decrease activities and increase training is outweighed by threats, labor, and resistance.

This is Systems Thinking and provides a framework to redesign our systems w/o any need for Systems Dynamics.

References

[1] https://thesystemsthinker.com/causal-loop-construction-the-basics/

[2] https://rdrr.io/github/jarrod-dalton/causalloop/man/plot.CLD.html