This module is focused on how your group might build consensus, particularly with regard to specific hypotheses or research questions that fit within your larger project frame. We’ll also discuss some checks you may want to run on your data and specific functions in R that perform those operations.
Note Pre-Class Preparation
There is no specific pre-class preparation for this module!
Collaborative Convergence
Tip Learning Objectives
After completing this topic you will be able to:
Recognize the stages of a participatory decision making process and the type of thinking required for each stage
Identify the value of and potential obstacles to emergent thinking in the “groan zone”
Understand the diamond model of participatory decision making as a framework for group process design
At the beginning of a collaborative process, the most important initial outcome is getting convergence or group alignment on a set of shared goals and objectives and a plan for how to achieve them. If your team process is effective, this plan will be an inclusive solution–one that works for everyone in the group. Achieving this shared vision can be more difficult than one might expect. While you may expect that participants have already agreed to the vision in joining the group, agreement does not always equate to alignment. This module focuses on tools and resources to help your group navigate to convergent, inclusive solutions that everyone on the team can align around.
Embracing Divergent Thinking
The first stage of group decision making is divergent thinking (Kaner et al. 2014). Confronted with a new, complex topic, the group will gradually move from the safe territory of familiar opinions into sharing their diverse perspectives and exploring new ideas. This can feel like the group process is devolving away from what was assumed to be shared agreement, but it is actually a critical part of the collaborative process.
Drawing of the ‘Groan Zone’ by Carrie Kappel, adapted from Sam Kaner’s Facilitator’s Guide to Participatory Decision making
When a diverse group comes together to work on a complex problem, their views are likely to diverge widely across many dimensions from problem definition to priorities to methods/approaches to the definition of success. But you can tap that divergent thinking to generate entirely new ideas and options that emerge through the group’s productive struggle for mutual understanding.
While your working group is in the divergent thinking stage, it’s critical to foster dialogue to surface different perspectives. Examine hidden assumptions. Create room for disagreement and questioning. Amplify diverse perspectives–and particularly, voices from the edge (e.g., junior members, new collaborators, people from different disciplines, non-scientists who may be affected by the research)–in order to expand the range of possibilities. Mirror and validate what you hear. Invite people who are good at bridging across disciplinary or other differences to help translate and build shared understanding of methods and ways of thinking. Suspend judgment and encourage full participation.
Beware of the most common pitfall at this stage, which is to converge too quickly on an early conclusion, staying in the safe space of familiar opinions and status quo solutions. You can prepare for this stage and help to avoid that pitfall by reviewing prior work and synthesizing data and knowledge gaps, promising approaches, and critical questions. Your team can use that synthesis of the current state of the science as a jumping off point.
Making it Through the Groan Zone
It’s natural for groups to go through a period of confusion and frustration as they struggle to integrate their diverse perspectives into a shared framework of understanding (Kaner et al. 2014). The goal is to get the group across this no man’s land between divergent thinking and convergence known as the “groan zone”. In the groan zone, the group leader or facilitator’s job is to keep the group from getting frustrated and shutting down.
While the groan zone can be challenging, it can also be an extremely fruitful and creative stage. Here in the messy middle of a group process, an open and flexible mindset and a process that invites participants to engage in emergent thinking can enable true innovation. Emergent thinking builds upon ideas generated in the divergent thinking stage, recombining or adapting them in novel ways. It seeks to identify patterns and make meaning in the face of complexity and uncertainty. Done well, emergent thinking enables a group to adapt, sense opportunities, and generate new and exciting ideas.
Warning Activity: Rapid Brainstorm
A variety of factors and dynamics can impede emergent thinking and make the groan zone especially challenging. Which have you observed?
Some useful techniques for navigating the groan zone and fostering emergent thinking include:
Cultivating presence and patience
Active listening
Building shared understanding via translation (e.g. across disciplines), metacognition (thinking about how you are thinking), and inquiry
Exploring new data, models, and ways of presenting information
Creating categories to reveal structure and allow sorting and prioritization of ideas
Combining or recombining ideas or methods to yield new approaches
Working together to separate facts from opinions
Carefully examining language, e.g. by looking word by word at a key statement or question that is being debated and asking what questions each word raises
Capturing side issues in writing and reserving time to revisit these – taking the tangents seriously is a critical part of letting participants know you value their contributions
Examining how proposed ideas might affect each individual in the group
Honoring objections to the process and asking for suggestions
Addressing power imbalances and elevating voices from the “edge”
Some typical obstacles to emergent thinking include:
Disciplinary differences in epistemology, vocabulary, and methods that impede understanding
Analysis paralysis - getting lost in the weeds of endless analysis and detail
Polarization - opposite camps anchored in
Power dynamics that squelch creative contributions from the “edges”
Avoidance of a deeper issue impeding collaboration (e.g. lack of trust)
Turf wars, competition
Risk aversion, perception management, fear of failure / getting it wrong
Confirmation bias and resistance to ideas that challenge group identity and beliefs
If you find the conversation getting off track or the dynamics becoming difficult, useful techniques that allow you to remain committed to being supportive and respectful of all group members (including ones you might experience as “difficult”) include:
Reminding individuals of the larger purpose of the group and reconnecting them to their own personal reasons for caring about and working on the issue, e.g. by inviting them to take a moment to reflect or to restate what success looks like
Focusing on common ground and areas of potential alignment
Inviting constructive opposition - ask the critic to say what they can support about a given proposal and what they would like to see changed or discussed further
Switching the participation format (e.g., going to breakout groups, brainstorming, a go-around, or individual writing)
Taking a break
Stepping out of the content and addressing the process
Educating members about group dynamics and asking them to reflect on how they are showing up
Encouraging more people to participate
Reframing the discussion, e.g. by surfacing underlying issues, and/or focusing on concrete actions that the group can take to resolve the conflict
Don’t get discouraged by the groan zone. Misunderstanding and miscommunication are normal parts of the process of collaboration. And even more importantly, “the act of working through these misunderstandings is what builds the foundation for sustainable agreements [and]… meaningful collaboration” (Kaner et al. 2014).
Getting to Convergence
Once the group has a strong foundation of shared understanding, things often start to click into place and feel easier and faster as you enter the zone of convergent thinking. At this point, the group is ready to devise inclusive solutions, weigh alternatives and make decisions. As the group leader your role is to help the group devise specific proposals, evaluate and decide among them, refine and synthesize into an overall approach, and lay out a concrete plan. The risk at this stage is that the group never converges on a clear decision or plan, leading the group to spin its wheels in the future.
Discuss as a team how you want to make decisions - what is your decision making process going to be? Will it be based on consensus? Majority rules? Or will you delegate the decision making to the team leader or a sub-team who are closest to the decision? What’s your fallback plan if you can’t reach a decision during your time together? The figure below arrays a variety of different decision making processes onto the axes of level of group member involvement and level of group member ownership. Note that the two red circles that are off the graph in the bottom left should definitely be avoided!
Decision making approaches and fallback options
Techniques that are useful in this phase include:
Pulling up concrete examples for inspiration
Inviting concrete, written proposals
Clarifying selection criteria and evaluating proposals against them
Combining the best elements of multiple ideas to support more innovative, inclusive solutions
Deciding what ideas to pursue and which to keep on the back burner in case the team needs to adapt
Defining steps and milestones, planning the work flow, and assigning roles and responsibilities
While the big work of the initial stage of a synthesis science project is getting to convergence on the overall work plan, you should expect that the group may go through the process of divergence and convergence again at multiple points in the process as you dive into the work and uncover new challenges. But the shared understanding and social rapport that come from successfully struggling together early on will allow the group to more easily and rapidly develop and implement new solutions in subsequent meetings.
Custom Functions
Tip Learning Objectives
After completing this topic you will be able to:
Describe the process of writing a new function
Identify the core syntax required for writing a function in R
Write your own function
Define “defensive” coding in the context of functions
Create custom warning and error messages for predictable error states
Writing your own functions can be really useful, particularly when doing synthesis work. Custom functions are generally useful for reducing duplication and increasing ease of maintenance. They may also become end products of synthesis work in and of themselves!
If one of your group’s outputs is a new standard data format or analytical workflow, the functions that you develop to aid yourself become valuable to anyone who adopts your synthesis project’s findings into their own workflows. If you get enough functions you can even release a package that others can install and use on their own computers. Such packages are a valuable product of synthesis efforts and can be a nice addition to a robust scientific resume or CV.
Core Function Writing Process
There are a number of ways one might go about writing a function, this topic will focus on one that is fairly flexible and should be applicable to most contexts you’re likely to run into in a synthesis context.
To make this more tangible, let’s load a dataset from the lterdatasampler package. We’ll use a dataset collected at the Niwot Ridge LTER on American Pika (Ochotona princeps) stress and habitat. You can see the full metadata for this dataset on the Environmental Data Initiative’s (EDI) website (Package ID: knb-lter-nwt.268.1).
# Load the needed libraries (install if needed)## install.packages("librarian")librarian::shelf(lterdatasampler, dplyr)# Load the dataset and assign to an objectpika_df <- lterdatasampler::nwt_pikas# Check the structurestr(pika_df)
1
Just doing this to have a shorter object name to refer to as we continue
The first step of writing your own function is something you’d likely already do: write normal, non-function code that does what you want!
In this case, let’s say we’re interested in extracting the results of a simple linear model into a tidy table. This is a classic context for a custom function because we’ll fit many models and likely will want to just copy/paste those results into our manuscript.
# Fit one linear modelpika_mod <-lm(concentration_pg_g ~ site, data = pika_df)# Get the summary of the model and assign that to an objectpika_result <-summary(pika_mod)# Extract the coefficient table as a dataframepika_coef <-as.data.frame(pika_result$coefficients)# The terms are preserved as row names but we'll need them as columnspika_coef2 <- pika_coef |> dplyr::mutate(terms =rownames(pika_coef),.before = dplyr::everything()) |># Rename columns to avoid spaces and special characters dplyr::rename(estimate = Estimate,std.error =`Std. Error`,t.value =`t value`,p.value =`Pr(>|t|)`) |># Grab theadjusted R2 as well dplyr::mutate(adj.r.squared = pika_result$adj.r.squared)# Finally, ditch the superseded row namesrownames(pika_coef2) <-NULL# Does that look like what we want?pika_coef2
1
Here, we are testing whether the concentration of a stress-related metabolite differs among sites
2
We’re using R’s “native pipe” (|>) to avoid magrittr as a “dependency” (i.e., a package required by our function)
3
Namespace ALL functions when you’re planning on writing a function so that there’s no chance that the order in which a user loads their packages breaks your function. It’s okay to skip base R function namespacing for brevity’s sake
4
Note the adjusted R2 is found in a different part of the result object (rather than the coefficient object where the rest of the results table can be found)
In order to write something like the above, you’ll (likely) need to do some experimenting to figure out where in each data object the information you want lives! This might look something like the following.
# Fit one linear modelpika_mod <-lm(concentration_pg_g ~ site, data = pika_df)# Get the summary of the model and assign that to an objectpika_result <-summary(pika_mod)# Check the structure of the 'results' objectstr(pika_result)
1
Here we can see the “coefficients” element has what we want but in some cases you may need to manually check each element (i.e., by running str) until you find the one(s) that have what you want
Once we’ve written the code that does what we want, the next step is to identify what pieces of the code the user of our function should have control over. These pieces will become the “arguments” (a.k.a. “parameters”) in our function.
For our function code, the user should be able to:
Supply any linear model returned by lm
Accepting the object returned by lm means we don’t need to have an argument in our function for every possible argument in lm. We can just accept a single object and work with that–that’ll be much easier for us!
Decide whether or not the adjusted R2 is returned
This is not necessarily critical to the intent of our function but by giving the user control here, we can demonstrate some other nice features of custom functions
Note that this step may not require writing any code! You can just look at your code from step 1 and think critically about what ‘levers’ you want to give the user.
Once we’ve written normal code and identified what our arguments should be, we need to translate our code into R’s function syntax.
Function syntax requires a few critical pieces; each will be flagged with a number footnote on the right side of the code chunk. Hover over those numbers for more details.
Note that it’s good practice to give your function a name that is a verb so the user can get as much information as possible from just the function name.
# Define the functionresult_extract <-function(model =NULL,include_r2 =TRUE){ # Extract the summary result_list <-summary(model)# Extract the coefficient table coef_df <-as.data.frame(result_list$coefficients)# Process that table as we did before## We'll exclude comments for brevity but you totally could include comments! coef.tidy_df <- coef_df |> dplyr::mutate(terms =rownames(coef_df),.before = dplyr::everything()) |> dplyr::rename(estimate = Estimate,std.error =`Std. Error`,t.value =`t value`,p.value =`Pr(>|t|)`)# Add R2 if requestedif(include_r2 ==TRUE){ result <- dplyr::mutate(.data = coef.tidy_df, adj.r.squared = result_list$adj.r.squared)# If not requested, just duplicate the tidy coefficient table with a new name } else { result <- coef.tidy_df }# Remove rownamesrownames(result) <-NULL# Return that to the userreturn(result)}
1
We use the function function to create a new function
2
Inside the function parentheses ((/)), we define what arguments the function uses and can set default values! NULL is a great default so that the function will obviously break if the user happens to have an object of the same name in their script already
3
As we translate our step 1 code into the function, be sure to replace your original object names with their respective argument names!
4
Critical that the final object has the same name, regardless of which side of the conditional is activated
5
By default, an R function will return the last object invoked. Relying on that default is risky! It is good practice to use the return function to be explicit about what the function gives back to the user.
6
End your function with a closing curly brace (}). This syntax may be familiar to you if you’ve used for loops or written conditionals before.
# Fit a linear modelpika_mod <-lm(concentration_pg_g ~ site, data = pika_df)# Invoke our new custom function!result_extract(model = pika_mod, include_r2 =FALSE)
If you follow the above steps, you should have a working function so you could go about your business and leave it at that. However, it is good practice to add your own custom error and warning messages. That way, if your function breaks, the user has some useful hints on how they can fix it on their end without being mad at you for writing a bad function.
Writing code in this way is known as programming “defensively.” This involves anticipating likely errors and writing your own error messages that are more informative to the user than whatever machine-generated error would otherwise get generated.
Just like the process of writing the function in the first place, you can follow some steps to make sure your functions are as robust as possible.
The first step of programming defensively (fourth overall in writing functions), is to think about what errors a reasonable user is likely to encounter. You don’t need to anticipate everything, just think through some of the more ‘obvious’ possible breaking points.
A good tip for this is to consider how a user could break your arguments. That is how they’ll interact with your function so it is also their means of accidentally breaking it.
In our function from earlier, there are two likely breaking points:
They supply a model returned from some statistics function other than lm
Our function relies entirely on the exact internal structure of the object returned by lm. The odds that a different function returns the same internal components (so that our function would still work) are basically zero so we need to account for that process
They supply a weird value to the include_r2 argument
Because we wrote the function we know that the include_r2 argument needs to be either TRUE or FALSE to be interpreted correctly. However, some other user might think they can supply some other value (e.g., “yes”, “not-adjusted”) and our code would not know what to do with that.
Once we have thought through the potential failure points, we can add checks inside the function to catch those and return informative messages to the user.
Keep in mind, we know what the function requires! That means we can write our checks to say ’if <some condition> is NOT true, then <informative error/warning message'. In R, that means we’ll use the stop and warning functions (for errors and warnings, respectively).
# Define the functionresult_extract <-function(model =NULL, include_r2 =TRUE){ # Error checks for 'model' argumentif(is.null(model) ||class(model) !="lm")stop("'model' must be an object returned by `stats::lm`")# Warning check for 'include_r2' argumentif(is.logical(include_r2) !=TRUE||length(include_r2) !=1){warning("'include_r2' must be a single logical. Coercing to TRUE") include_r2 <-TRUE }# Extract the summary result_list <-summary(model)# Extract the coefficient table coef_df <-as.data.frame(result_list$coefficients)# Process that table as we did before coef.tidy_df <- coef_df |> dplyr::mutate(terms =rownames(coef_df),.before = dplyr::everything()) |> dplyr::rename(estimate = Estimate,std.error =`Std. Error`,t.value =`t value`,p.value =`Pr(>|t|)`)# Add R2 if requestedif(include_r2 ==TRUE){ result <- dplyr::mutate(.data = coef.tidy_df, adj.r.squared = result_list$adj.r.squared)# If not requested, just duplicate the tidy coefficient table with a new name } else { result <- coef.tidy_df }# Remove rownamesrownames(result) <-NULL# Return that to the userreturn(result)}
1
The || operator is a special conditional that returns TRUEas soon as the first condition returns TRUEand doesn’t try to compute any other conditions in the same if. This is super useful because if the model argument isNULL, then we already know the class check will fail so we don’t need to even test it. You can use this property to list out your conditions for failure from most general to most precise to save computing time on arguments that receive larger data objects or where computing the check itself is computationally-intensive. You want the function to fail early if it is going to fail anyway!
2
For arguments that we know have to be either TRUE or FALSE, we can skip some of the other checks and jump straight to asking “is this a logical value?” and then return a warning/error if it isn’t.
3
Here we use curly braces ({/}) after the if because we want to both return an informative message and re-set the argument to an appropriate value. R doesn’t do “informative indents” like some other languages (e.g., Python), so if we want multiple operations to trigger from the same condition, we need to wrap them in curly braces.
Once we’ve added some custom error checks, if we re-run the function with entries we know are bad, we can see that our custom error/warning messages show up!
# Invoke our function on the wrong value passed to 'model'result_extract(model ="banana", include_r2 ="please")
Error in `result_extract()`:
! 'model' must be an object returned by `stats::lm`
# Invoke our function on the elevation model with a bad value passed to 'include_r2'result_extract(model = pika_mod2, include_r2 =c(TRUE, FALSE))
Warning in result_extract(model = pika_mod2, include_r2 = c(TRUE, FALSE)):
'include_r2' must be a single logical. Coercing to TRUE