Count the number of columns in a row with a specific value - r

I have a data set with numerical responses to several questions. I would like to know the number of times a person answers a question with a value of 1,2...
Here is an example of the data:
df=data.frame("Person"=c("person a", "person b"),
"Q1"=c(2,2),"Q2"=c(1,2),"Q3"=c(1,1))
Which looks like this:
Person Q1 Q2 Q3
person a 2 1 1
person b 2 2 1
I would like this and would prefer to use dplyr:
Person Q1 Q2 Q3 Total.1 Total.2
person a 2 1 1 2 1
person b 2 2 1 1 2

The base R approach suggested by #dww is quite simple and straight forward. However, if you prefer dplyr approach we can use rowwise and do to calculate occurrence of 1 and 2 respectively.
library(dplyr)
df %>%
rowwise() %>%
do( (.) %>% as.data.frame %>%
mutate(Total.1 = sum(.==1),
Total.2 = sum(.==2)))
# Person Q1 Q2 Q3 Total.1 Total.2
# <fct> <dbl> <dbl> <dbl> <int> <int>
#1 person a 2 1 1 2 1
#2 person b 2 2 1 1 2
A base R approach using apply
df[c("Total.1", "Total.2")] <- t(apply(df, 1, function(x) c(sum(x==1), sum(x==2))))
df
# Person Q1 Q2 Q3 Total.1 Total.2
#1 person a 2 1 1 2 1
#2 person b 2 2 1 1 2

No need for dplyr. In base R it is quite simple
df = cbind(df, Total.1 = rowSums(df[,-1]==1), Total.2 = rowSums(df[,-1]==2))

Here is one option with tidyverse
library(tidyverse)
df %>%
mutate(Total = pmap(.[-1], ~
c(...) %>%
paste0("Total.", .) %>%
table %>%
as.list %>%
as_tibble )) %>%
# unnest
# Person Q1 Q2 Q3 Total.1 Total.2
#1 person a 2 1 1 2 1
#2 person b 2 2 1 1 2
Or another way is
df %>%
mutate(Total = pmap(.[-1], ~
c(...) %>%
table %>%
toString)) %>%
separate(Total, into = c("Total.1", "Total.2"))
# Person Q1 Q2 Q3 Total.1 Total.2
#1 person a 2 1 1 2 1
#2 person b 2 2 1 1 2

Related

How to drop NA's out of the summarise(count = n()) function in R?

I have a dataset containing 4 organisation units (org_unit) with different number of participants and 2 Questions (Q1,Q2) on a 2-degree scale (1:2). I want to know how many people per unit answered the respective question with [1] and divide them by the total number of participants / unit.
Org_unit <- c(1,1,1,1,2,2,2,3,3,4)
Q1 <- c(1,2,1,2,1,2,1,2,1,2)
Q2 <- c(-9,-9,-9,-9,-9,-9,-9,-9,-9,-9)
The problem is, my Q2 only consists of [-9] which stands for non-response. I therefore assigned NA to [-9].
DF <- data.frame(Org_unit, Q1, Q2)
DF[DF == -9] <- NA
DF
Org_unit Q1 Q2
1 1 1 NA
2 1 2 NA
3 1 1 NA
4 1 2 NA
5 2 1 NA
6 2 2 NA
7 2 1 NA
8 3 2 NA
9 3 1 NA
10 4 2 NA
Next I calculated the proportion of people who answered Q1 with [1], which works fine.
prop_q1 <- DF %>%
group_by(Org_unit) %>%
summarise(count = n(),
prop = mean(Q1 == 1))
prop_q1
# A tibble: 4 x 3
Org_unit count prop
<dbl> <int> <dbl>
1 1 4 0.5
2 2 3 0.667
3 3 2 0.5
4 4 1 0
when i run the same code for Q2 however, I get the same amount of members per unit (count = c(1,2,3,4), although nobody answered the question and I don't want them to be registered as participants, since they technically didn't participate in the study.
prop_q2 <- DF %>%
group_by(Org_unit) %>%
summarise(count = n(),
prop = mean(Q2 == 1))
prop_q2
# A tibble: 4 x 3
Org_unit count prop
<dbl> <int> <dbl>
1 1 4 NA
2 2 3 NA
3 3 2 NA
4 4 1 NA
Is there a way to calculate the right amount of members per unit when facing NA's? [-9]
Thanks!
Would
prop_q2 <- DF %>%
filter(!is.na(Q2)) %>%
group_by(Org_unit) %>%
summarise(count = n(),
prop = mean(Q2 == 1))
do the job?
Given that you want to do this across multiple columns, I think that using across() within the dplyr verbs will be better for you. I explain the solution below.
Org_unit <- c(1,1,1,1,2,2,2,3,3,4)
Q1 <- c(1,2,1,2,1,2,1,2,1,2)
Q2 <- c(1,-9,-9,-9,-9,-9,-9,-9,-9,-9) #Note one response
df <- tibble(Org_unit, Q1, Q2)
df %>%
mutate(across(starts_with("Q"), ~na_if(., -9))) %>%
group_by(Org_unit) %>%
summarize(across(starts_with("Q"),
list(
N = ~sum(!is.na(.)),
prop = ~sum(. == 1, na.rm = TRUE)/sum(!is.na(.)))
))
# A tibble: 4 x 5
Org_unit Q1_N Q1_prop Q2_N Q2_prop
* <dbl> <int> <dbl> <int> <dbl>
1 1 4 0.5 1 1
2 2 3 0.667 0 NaN
3 3 2 0.5 0 NaN
4 4 1 0 0 NaN
First, we take the data frame (which I created as a tibble) and substitute NA for all values that equal -9 for all columns that start with a capital "Q". This converts all question columns to have NAs in place of -9s.
Second, we group by the organizational unit and then summarize using two functions. The first sums all values where the response to the question is not NA. The string _N will be appended to columns with these values. The second calculates the proportion and will have _prop appended to the values.

Using dplyr, count non-numeric grades in each class

Given the input and code below, using dplyr and groups, how can I produce the results shown in the output? I know how to sum columns in groups using dplyr, but in this case I need to count how many of each non-numeric grade occurred in each class.
**INPUT**
Class Student Grade
1 Jack C
1 Mary B
1 Mo B
1 Jane A
1 Tom C
2 Don C
2 Betsy B
2 Sue C
2 Tayna B
2 Kim C
**CODE**
# Create the dataframe
Class <- c(1,1,1,1,1,2,2,2,2,2)
Name <- c("Jack", "Mary", "Mo", "Jane", "Tom", "Don", "Betsy", "Sue", "Tayna", "Kim")
Grade <- c("C","B","B","A","C","C","B","C","B","C")
StudentGrades <- data.frame(Class, Name, Grade)
**OUTPUT**
Class Grade-A Grade-B Grade-C
1 1 2 2
2 0 2 3
We can use count to get the frequency count and then with pivot_wider change from 'long' to 'wide' format
library(dplyr)
library(tidyr)
library(stringr)
StudentGrades %>%
count(Class, Grade = str_c('Grade_', Grade)) %>%
pivot_wider(names_from = Grade, values_from = n, values_fill = list(n = 0))
# A tibble: 2 x 4
# Class Grade_A Grade_B Grade_C
# <dbl> <int> <int> <int>
#1 1 1 2 2
#2 2 0 2 3
Or in base R
table(StudentGrades[c('Class', 'Grade')])
Here is a base R solution, where table() + split() are used
dfout <- do.call(rbind,lapply(split(StudentGrades,StudentGrades$Class),
function(v) c(unique(v[1]),table(v$Grade))))
such that
> dfout
Class A B C
1 1 1 2 2
2 2 0 2 3

Left join with multiple conditions in R

I'm trying to replace ids for their respective values. The problem is that each id has a different value according to the previous column type, like this:
>df
type id
1 q1 1
2 q1 2
3 q2 1
4 q2 3
5 q3 1
6 q3 2
Here's the type ids with its value:
>q1
id value
1 1 yes
2 2 no
>q2
id value
1 1 one hour
2 2 two hours
3 3 more than two hours
>q3
id value
1 1 blue
2 2 yellow
I've tried something like this:
df <- left_join(subset(df, type %in% c("q1"), q1, by = "id"))
But it removes the other values.
I' like to know how to do a one liner solution (or kind of) because there are more than 20 vectors with types description.
Any ideias on how to do it?
This is the df i'm expecting:
>df
type id value
1 q1 1 yes
2 q1 2 no
3 q2 1 one hour
4 q2 3 more than two hours
5 q3 1 blue
6 q3 2 yellow
You can join on more than one variable. The example df you give would actually make a suitable lookup table for this:
value_lookup <- data.frame(
type = c('q1', 'q1', 'q2', 'q2', 'q3', 'q3'),
id = c(1, 2, 1, 3, 1, 2),
value = c('yes', 'no', 'one hour', 'more than two hours', 'blue', 'yellow')
)
Then you just merge on both type and id:
df <- left_join(df, value_lookup, by = c('type', 'id'))
Usually when I need a lookup table like that I store it in a CSV rather than write it all out in the code, but do whatever suits you.
tempList = split(df, df$type)
do.call(rbind,
lapply(names(tempList), function(nm)
merge(tempList[[nm]], get(nm))))
# id type value
#1 1 q1 yes
#2 2 q1 no
#3 1 q2 one hour
#4 3 q2 more than two hours
#5 1 q3 blue
#6 2 q3 yellow
Get the values of 'q\d+' data.frame object identifiers in a list, bind them together into a single data.frame with bind_rows while creating the 'type' column as the identifier name and right_join with the dataset object 'df'
library(tidyverse)
mget(paste0("q", 1:3)) %>%
bind_rows(.id = 'type') %>%
right_join(df)
# type id value
#1 q1 1 yes
#2 q1 2 no
#3 q2 1 one hour
#4 q2 3 more than two hours
#5 q3 1 blue
#6 q3 2 yellow
You can do it by a series of left joins:
df1 = left_join(df, q1, by='id') %>% filter(type=="q1")
> df1
type id value
1 q1 1 yes
2 q1 2 no
df2 = left_join(df, q2, by='id') %>% filter(type=="q2")
> df2
type id value
1 q2 1 one hour
2 q2 3 more than two hours
df3 = left_join(df, q3, by='id') %>% filter(type=="q3")
> df3
type id value
1 q3 1 blue
2 q3 2 yellow
> rbind(df1,df2,df3)
type id value
1 q1 1 yes
2 q1 2 no
3 q2 1 one hour
4 q2 3 more than two hours
5 q3 1 blue
6 q3 2 yellow
One liner would be:
rbind(left_join(df, q1, by='id') %>% filter(type=="q1"),
left_join(df, q2, by='id') %>% filter(type=="q2"),
left_join(df, q3, by='id') %>% filter(type=="q3"))
If you have more vectors then probably you should loop through the names of vector types and execute left_join and bind_rows one by one as:
vecQs = c(paste("q", seq(1,3,1),sep="")) #Types of variables q1, q2 ...
result = tibble()
#Execute left_join for the types and store it in result.
for(i in vecQs) {
result = bind_rows(result, left_join(df,eval(as.symbol(i)) , by='id') %>% filter(type==!!i))
}
This will give:
> result
# A tibble: 6 x 3
type id value
<chr> <int> <chr>
1 q1 1 yes
2 q1 2 no
3 q2 1 one hour
4 q2 3 more than two hours
5 q3 1 blue
6 q3 2 yellow

Dynamically Normalize all rows with first element within a group

Suppose I have the following data frame:
year subject grade study_time
1 1 a 30 20
2 2 a 60 60
3 1 b 30 10
4 2 b 90 100
What I would like to do is be able to divide grade and study_time by their first record within each subject. I do the following:
df %>%
group_by(subject) %>%
mutate(RN = row_number()) %>%
mutate(study_time = study_time/study_time[RN ==1],
grade = grade/grade[RN==1]) %>%
select(-RN)
I would get the following output
year subject grade study_time
1 1 a 1 1
2 2 a 2 3
3 1 b 1 1
4 2 b 3 10
It's fairly easy to do when I know what the variable names are. However, I'm trying to write a generalize function that would be able to act on any data.frame/data.table/tibble where I may not know the name of the variables that I need to mutate, I'll only know the variables names not to mutate. I'm trying to get this done using tidyverse/data.table and I can't get anything to work.
Any help would be greatly appreciated.
We group by 'subject' and use mutate_at to change multiple columns by dividing the element by the first element
library(dplyr)
df %>%
group_by(subject) %>%
mutate_at(3:4, funs(./first(.)))
# A tibble: 4 x 4
# Groups: subject [2]
# year subject grade study_time
# <int> <chr> <dbl> <dbl>
#1 1 a 1 1
#2 2 a 2 3
#3 1 b 1 1
#4 2 b 3 10

Reorder a single column in a dataframe within each level of another column

Probably the solution to this problem is really easy but I just can't see it. Here is my sample data frame:
df <- data.frame(id=c(1,1,1,2,2,2), value=rep(1:3,2), level=rep(letters[1:3],2))
df[6,2] <- NA
And here is the desired output that I would like to create:
df$new_value <- c(3,2,1,NA,2,1)
So the order of all columns is the same, and for the new_value column the value column order is reversed within each level of the id column. Any ideas? Thanks!
As I understood your question, it's a coincidence that your data is sorted, if you just want to reverse the order without sorting:
library(dplyr)
df %>% group_by(id) %>% mutate(new_value = rev(value)) %>% ungroup
# A tibble: 6 x 4
id value level new_value
<dbl> <int> <fctr> <int>
1 1 1 a 3
2 1 2 b 2
3 1 3 c 1
4 2 1 a NA
5 2 2 b 2
6 2 NA c 1
A slightly different approach, using the parameters in the sort function:
library(dplyr)
df %>% group_by(id) %>%
mutate(value = sort(value, decreasing=TRUE, na.last=FALSE))
Output:
# A tibble: 6 x 3
# Groups: id [2]
id value level
<dbl> <int> <fctr>
1 1.00 3 a
2 1.00 2 b
3 1.00 1 c
4 2.00 NA a
5 2.00 2 b
6 2.00 1 c
Hope this helps!
We can use order on the missing values and on the column itself
library(dplyr)
df %>%
group_by(id) %>%
mutate(new_value = value[order(!is.na(value), -value)])
# A tibble: 6 x 4
# Groups: id [2]
# id value level new_value
# <dbl> <int> <fctr> <int>
#1 1.00 1 a 3
#2 1.00 2 b 2
#3 1.00 3 c 1
#4 2.00 1 a NA
#5 2.00 2 b 2
#6 2.00 NA c 1
Or using the arrange from dplyr
df %>%
arrange(id, !is.na(value), desc(value)) %>%
transmute(new_value = value) %>%
bind_cols(df, .)
Or using base R and specify the na.last option as FALSE in order
with(df, ave(value, id, FUN = function(x) x[order(-x, na.last = FALSE)]))
#[1] 3 2 1 NA 2 1

Resources