Reputation: 47300
There are great questions and answers on how to move a column to the first or last place.
Using dplyr
The best answers are respectively analog to :
iris2 <- iris %>% head(2)
iris2 %>% select( Sepal.Width, everything()) # move Sepal.Width to first
# Sepal.Width Sepal.Length Petal.Length Petal.Width Species
# 1 3.5 5.1 1.4 0.2 setosa
# 2 3.0 4.9 1.4 0.2 setosa
iris2 %>% select(-Sepal.Width, Sepal.Width) # move Sepal.Width to last
# Sepal.Length Petal.Length Petal.Width Species Sepal.Width
# 1 5.1 1.4 0.2 setosa 3.5
# 2 4.9 1.4 0.2 setosa 3.0
However I didn't find any easy way to move a column after or before a given one.
Expected output :
iris2 %>% move_at(Species, Sepal.Width, side = "before")
# Sepal.Length Species Sepal.Width Petal.Length Petal.Width
# 1 5.1 setosa 3.5 1.4 0.2
# 2 4.9 setosa 3.0 1.4 0.2
iris2 %>% move_at(Species, Sepal.Width, side = "after")
# Sepal.Length Sepal.Width Species Petal.Length Petal.Width
# 1 5.1 3.5 setosa 1.4 0.2
# 2 4.9 3.0 setosa 1.4 0.2
Upvotes: 11
Views: 1054
Reputation: 5788
Another Base R solution (not sure how it benchmarks against the others).
# Function to move column names before or after another column name:
# .move_vec_name_to => function
.move_vec_name_to <- function(vec_names, move_vec_name, near_vec_name, side = c("before", "after")){
# Resolve the side to move the col vector to:
# .side => character scalar
.side <- match.arg(side)
# Resolve the number of column vectors: n => integer scalar
n <- length(vec_names)
# Resolve the index of the col vector to be moved:
# move_vec_idx => integer scalar
move_vec_idx <- which(vec_names == move_vec_name)
# Resolve the index of where the col vector is to be
# moved to: near_vec_idx => integer scalar
near_vec_idx <- which(vec_names == near_vec_name)
# If we want to move something before or after and there is no need:
if((move_vec_idx <= near_vec_idx & .side == "before") || (near_vec_idx <= move_vec_idx && .side == "after")){
# Keep the names the same: new_col_name_vec => character vector
new_col_name_vec <- vec_names
# Otherwise:
}else{
# Drop the name of the vector to be moved from the col
# name vector: vec_wo_move_vec => character vector
vec_wo_move_vec <- vec_names[-move_vec_idx]
# Resolve the new column name vector:
# if we want to move the column before a given col vector:
if(.side == "before"){
# new_col_name_vec => character vector
new_col_name_vec <- c(
vec_wo_move_vec[seq_len(near_vec_idx - 1)],
move_vec_name,
near_vec_name,
vec_wo_move_vec[seq(pmin(near_vec_idx + 1, n), length(vec_names))]
)[seq_len(n)]
# Otherwise if we want to move it after:
}else{
# new_col_name_vec => character vector
new_col_name_vec <- c(
vec_wo_move_vec[seq_len(pmax(near_vec_idx-2, 0))],
near_vec_name,
move_vec_name,
vec_wo_move_vec[seq(pmax(near_vec_idx, 1), n, 1)]
)[seq_len(n)]
}
}
# Explicitly define the returned object:
# character vector => env
return(new_col_name_vec)
}
# Function to move multiple vector names to a certain side of another vector name:
# .move_vec_names_to => function
.move_vec_names_to <- function(vec_names, move_vec_names, near_vec_name, side = c("before", "after")){
# Resolve the side: .side => character vector
.side <- match.arg(side)
# Reverse the input vectors to be moved: .move_vec_names => character vector
.move_vec_names <- if(.side == "after"){
rev(move_vec_names)
}else{
move_vec_names
}
# Set the termination case:
if(length(.move_vec_names) <= 1){
# Return vector names with columns moved: character vector => env
return(
.move_vec_name_to(
vec_names,
.move_vec_names,
near_vec_name,
.side
)
)
# Otherwise:
}else{
# Apply the column movement function recursively:
# character vector => env
return(
.move_vec_name_to(
.move_vec_name_to(
vec_names,
.move_vec_names[1],
near_vec_name,
.side
),
.move_vec_names[-1],
near_vec_name,
.side
)
)
}
}
# Function to move column vector before or after another column vector:
# move_to => function
move_to <- function(df, move_vec_name, near_vec_name, side = c("before", "after")){
# Resolve the side to move the col vector to:
# .side => character scalar
.side <- match.arg(side)
# Apply vector name move function: df => data.frame
df <- if(length(move_vec_name) > 1){
df[,.move_vec_names_to(colnames(df), move_vec_name, near_vec_name, .side), drop = FALSE]
}else{
df[,.move_vec_name_to(colnames(df), move_vec_name, near_vec_name, .side), drop = FALSE]
}
# Explicitly define the returned object:
# data.frame => env
return(df)
}
# Function to test the move_to user defined function:
# test_single_col_move_to => function
test_single_col_move_to <- function(df){
# Import required pacakage:
library(dplyr)
# Generate a data.frame of test cases:
# test_val_df => data.frame
test_val_df <- setNames(
expand.grid(
names(df),
names(df),
c("before", "after")
),
c(
"move_vec",
"near_vec",
"side"
)
)
# Convert vals to chars: test_val_df => data.frame
test_val_df[] <- lapply(
test_val_df,
as.character
)
# Test all vector names in iris are in the resulting df
# and that all names are where they are supposed to be:
# test_vec_names => list of boolean vectors
test_vec_names <- lapply(
seq_len(
nrow(test_val_df)
),
function(i){
# Resolve the test values:
move_vec <- test_val_df[i, 1, drop = TRUE]
near_vec <- test_val_df[i, 2, drop = TRUE]
side <- test_val_df[i, 3, drop = TRUE]
# Test 1 base R functionality:
test1 <- names(
move_to(
df,
move_vec,
near_vec,
side
)
)
# Test 2 base R functionality:
test2 <- df |> move_to(move_vec, near_vec, side) |> names()
# Test 3 dplyr functionality:
test3 <- df %>% move_to(move_vec, near_vec, side) %>% names
# Test 4 dply functionality:
test4 <- df %>% move_to(., move_vec, near_vec, side) %>% names
# Store all tests in a list: test_list => list of character vectors
test_list <- list(test1, test2, test3, test4)
# list of tests: list of lists of boolean vectors => env
list(
# Test all names in new col vectors are in df:
unlist(Map(function(x){all(x %in% names(df))}, test_list)),
# Test befores & afters:
unlist(Map(function(y){
ifelse(
side == "before",
which(y == move_vec) <= which(y == near_vec),
which(y == move_vec) >= which(y == near_vec)
)
},
test_list
)
)
)
}
)
# Resolve if all tests have been passed:
# tests_passed => boolean scalar
tests_passed <- all(unlist(test_vec_names))
# Explicitly define returned argument:
# boolean scalar => env
return(tests_passed)
}
# Test any move of any column vector to anywhere on iris:
# boolean scalar => stdout(console)
test_single_col_move_to(iris)
# Apply the function to move multiple vectors before or after another
# vector: data.frame => stdout(console)
names(iris)
move_to(
iris,
c("Sepal.Width", "Petal.Length"),
"Species",
"after"
)
move_to(
iris,
c("Species", "Petal.Width"),
"Petal.Length",
"before"
)
Upvotes: 0
Reputation: 8506
To complete the answers, there is a function called relocate()
since dplyr 1.0.0
:
library(dplyr)
iris %>%
head(n = 2) %>%
relocate(Species, .before = Sepal.Width)
#> Sepal.Length Species Sepal.Width Petal.Length Petal.Width
#> 1 5.1 setosa 3.5 1.4 0.2
#> 2 4.9 setosa 3.0 1.4 0.2
Created on 2022-10-18 with reprex v2.0.2
Upvotes: 3
Reputation: 47300
UPDATE : using rlang::enquo
I could make it much better, then using @Zsombor's answer I could make it much shorter and more elegant. old solution (in base R) at the end of answer
#' Move column or selection of columns
#'
#' Column(s) described by \code{cols} are moved before (default) or after the reference
#' column described by \code{ref}
#'
#' @param data A \code{data.frame}
#' @param cols unquoted column name or numeric or selection of columns using a select helper
#' @param ref unquoted column name
#' @param side \code{"before"} or \code{"after"}
#'
#' @return A data.frame with reordered columns
#' @export
#'
#' @examples
#' iris2 <- head(iris,2)
#' move(iris2, Species, Sepal.Width)
#' move(iris2, Species, Sepal.Width, "after")
#' move(iris2, 5, 2)
#' move(iris2, 4:5, 2)
#' move(iris2, one_of("Sepal.Width","Species"), Sepal.Width)
#' move(iris2, starts_with("Petal"), Sepal.Width)
move <- function(data, cols, ref, side = c("before","after")){
if(! requireNamespace("dplyr"))
stop("Make sure package 'dplyr' is installed to use function 'move'")
side <- match.arg(side)
cols <- rlang::enquo(cols)
ref <- rlang::enquo(ref)
if(side == "before")
dplyr::select(data,1:!!ref,-!!ref,-!!cols,!!cols,dplyr::everything())
else
dplyr::select(data,1:!!ref,-!!cols,!!cols,dplyr::everything())
}
examples:
iris2 %>% move(Species, Sepal.Width)
# Sepal.Length Species Sepal.Width Petal.Length Petal.Width
# 1 5.1 setosa 3.5 1.4 0.2
# 2 4.9 setosa 3.0 1.4 0.2
iris2 %>% move(Species, Sepal.Width, "after")
# Sepal.Length Sepal.Width Species Petal.Length Petal.Width
# 1 5.1 3.5 setosa 1.4 0.2
# 2 4.9 3.0 setosa 1.4 0.2
iris2 %>% move(5, 2)
# Sepal.Length Species Sepal.Width Petal.Length Petal.Width
# 1 5.1 setosa 3.5 1.4 0.2
# 2 4.9 setosa 3.0 1.4 0.2
iris2 %>% move(4:5, 2)
# Sepal.Length Petal.Width Species Sepal.Width Petal.Length
# 1 5.1 0.2 setosa 3.5 1.4
# 2 4.9 0.2 setosa 3.0 1.4
iris2 %>% move(one_of("Sepal.Width","Species"), Sepal.Width)
# Sepal.Length Sepal.Width Species Petal.Length Petal.Width
# 1 5.1 3.5 setosa 1.4 0.2
# 2 4.9 3.0 setosa 1.4 0.2
iris2 %>% move(starts_with("Petal"), Sepal.Width)
# Sepal.Length Petal.Length Petal.Width Sepal.Width Species
# 1 5.1 1.4 0.2 3.5 setosa
# 2 4.9 1.4 0.2 3.0 setosa
Old solution
Here's a simple solution using only base R programming :
move_at <- function(data, col, ref, side = c("before","after")){
side = match.arg(side)
col_pos <- match(as.character(substitute(col)),names(data))
ref_pos <- match(as.character(substitute(ref)),names(data))
sorted_pos <- c(col_pos,ref_pos)
if(side =="after") sorted_pos <- rev(sorted_pos)
data[c(setdiff(seq_len(ref_pos-1),col_pos),
sorted_pos,
setdiff(seq_along(data),c(seq_len(ref_pos),col_pos)))]
}
iris2 %>% move_at(Species, Sepal.Width)
# Sepal.Length Species Sepal.Width Petal.Length Petal.Width
# 1 5.1 setosa 3.5 1.4 0.2
# 2 4.9 setosa 3.0 1.4 0.2
iris2 %>% move_at(Species, Sepal.Width, "after")
# Sepal.Length Sepal.Width Species Petal.Length Petal.Width
# 1 5.1 3.5 setosa 1.4 0.2
# 2 4.9 3.0 setosa 1.4 0.2
Upvotes: 15
Reputation: 7822
Just for the record, another solution would be
library(tidyverse)
data(iris)
iris %>%
select(-Species) %>%
add_column(Specis = iris$Species, .before = "Petal.Length") %>%
head()
#> Sepal.Length Sepal.Width Specis Petal.Length Petal.Width
#> 1 5.1 3.5 setosa 1.4 0.2
#> 2 4.9 3.0 setosa 1.4 0.2
#> 3 4.7 3.2 setosa 1.3 0.2
#> 4 4.6 3.1 setosa 1.5 0.2
#> 5 5.0 3.6 setosa 1.4 0.2
#> 6 5.4 3.9 setosa 1.7 0.4
Created on 2018-08-31 by the reprex package (v0.2.0).
Upvotes: 3
Reputation: 136
This seems to work, regardless of original column order (thanks for the comment to @Moody_Mudskipper ):
iris %>% select(1:Sepal.Width, -Species, Species, everything()) %>% head(2)
#> Sepal.Length Sepal.Width Species Petal.Length Petal.Width
#> 1 5.1 3.5 setosa 1.4 0.2
#> 2 4.9 3.0 setosa 1.4 0.2
iris %>% select(1:Sepal.Width, -Sepal.Width, -Species, Species, everything()) %>% head(2)
#> Sepal.Length Species Sepal.Width Petal.Length Petal.Width
#> 1 5.1 setosa 3.5 1.4 0.2
#> 2 4.9 setosa 3.0 1.4 0.2
Upvotes: 7
Reputation: 17289
I found an interesting function (moveMe, written by @A5C1D2H2I1M1N2O1R2T1) that closely fits the problem:
source('https://raw.githubusercontent.com/mrdwab/SOfun/master/R/moveMe.R')
head(iris[ moveMe(names(iris), 'Species before Sepal.Width') ], 2)
# Sepal.Length Species Sepal.Width Petal.Length Petal.Width
# 1 5.1 setosa 3.5 1.4 0.2
# 2 4.9 setosa 3.0 1.4 0.2
head(iris[ moveMe(names(iris), 'Species after Sepal.Width') ], 2)
# Sepal.Length Sepal.Width Species Petal.Length Petal.Width
# 1 5.1 3.5 setosa 1.4 0.2
# 2 4.9 3.0 setosa 1.4 0.2
It also allows for more complex instructions:
head(iris[ moveMe(names(iris), 'Species after Sepal.Width; Petal.Width first; Sepal.Length last') ], 2)
# Petal.Width Sepal.Width Species Petal.Length Sepal.Length
# 1 0.2 3.5 setosa 1.4 5.1
# 2 0.2 3.0 setosa 1.4 4.9
Upvotes: 2