Skip to content

Commit 0e2d3df

Browse files
authored
allow push!/pushfirst!/append!/prepend! with multiple values (#3372)
1 parent 7aec87d commit 0e2d3df

File tree

5 files changed

+279
-39
lines changed

5 files changed

+279
-39
lines changed

NEWS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## New functionalities
44

5+
* Allow passing multiple values to add in `push!`, `pushfirst!`,
6+
`append!`, and `prepend!`
7+
([#3372](https://github.com/JuliaData/DataFrames.jl/pull/3372))
58
* `rename` and `rename!` now allow to apply a function transforming
69
column names only to a subset of the columns specified by the `cols`
710
keyword argument

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "DataFrames"
22
uuid = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
3-
version = "1.6.1"
3+
version = "1.7.0"
44

55
[deps]
66
Compat = "34da2185-b29b-5c13-b0c7-acf172513d20"

src/dataframe/insertion.jl

Lines changed: 166 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
"""
2-
append!(df::DataFrame, df2::AbstractDataFrame; cols::Symbol=:setequal,
3-
promote::Bool=(cols in [:union, :subset]))
4-
append!(df::DataFrame, table; cols::Symbol=:setequal,
2+
append!(df::DataFrame, tables...; cols::Symbol=:setequal,
53
promote::Bool=(cols in [:union, :subset]))
64
7-
Add the rows of `df2` to the end of `df`. If the second argument `table` is not
8-
an `AbstractDataFrame` then it is converted using `DataFrame(table,
9-
copycols=false)` before being appended.
5+
Add the rows of tables passed as `tables` to the end of `df`. If the table is not
6+
an `AbstractDataFrame` then it is converted using
7+
`DataFrame(table, copycols=false)` before being appended.
108
119
The exact behavior of `append!` depends on the `cols` argument:
1210
* If `cols == :setequal` (this is the default) then `df2` must contain exactly
@@ -78,18 +76,53 @@ julia> df1
7876
4 │ 4 4
7977
5 │ 5 5
8078
6 │ 6 6
79+
80+
julia> append!(df2, DataFrame(A=1), (; C=1:2), cols=:union)
81+
6×3 DataFrame
82+
Row │ A B C
83+
│ Float64? Int64? Int64?
84+
─────┼─────────────────────────────
85+
1 │ 4.0 4 missing
86+
2 │ 5.0 5 missing
87+
3 │ 6.0 6 missing
88+
4 │ 1.0 missing missing
89+
5 │ missing missing 1
90+
6 │ missing missing 2
8191
```
8292
"""
8393
Base.append!(df1::DataFrame, df2::AbstractDataFrame; cols::Symbol=:setequal,
8494
promote::Bool=(cols in [:union, :subset])) =
8595
_append_or_prepend!(df1, df2, cols=cols, promote=promote, atend=true)
8696

97+
function Base.append!(df::DataFrame, table; cols::Symbol=:setequal,
98+
promote::Bool=(cols in [:union, :subset]))
99+
if table isa Dict && cols == :orderequal
100+
throw(ArgumentError("passing `Dict` as `table` when `cols` is equal to " *
101+
"`:orderequal` is not allowed as it is unordered"))
102+
end
103+
append!(df, DataFrame(table, copycols=false), cols=cols, promote=promote)
104+
end
105+
106+
function Base.append!(df::DataFrame, @nospecialize tables...;
107+
cols::Symbol=:setequal,
108+
promote::Bool=(cols in [:union, :subset]))
109+
if !(cols in (:orderequal, :setequal, :intersect, :subset, :union))
110+
throw(ArgumentError("`cols` keyword argument must be " *
111+
":orderequal, :setequal, :intersect, :subset or :union)"))
112+
end
113+
114+
return foldl((df, table) -> append!(df, table, cols=cols, promote=promote),
115+
collect(Any, tables), init=df)
116+
end
117+
87118
"""
88-
prepend!(df::DataFrame, df2::AbstractDataFrame; cols::Symbol=:setequal,
89-
promote::Bool=(cols in [:union, :subset]))
90-
prepend!(df::DataFrame, table; cols::Symbol=:setequal,
119+
prepend!(df::DataFrame, tables...; cols::Symbol=:setequal,
91120
promote::Bool=(cols in [:union, :subset]))
92121
122+
Add the rows of tables passed as `tables` to the beginning of `df`. If the table is not
123+
an `AbstractDataFrame` then it is converted using
124+
`DataFrame(table, copycols=false)` before being appended.
125+
93126
Add the rows of `df2` to the beginning of `df`. If the second argument `table`
94127
is not an `AbstractDataFrame` then it is converted using `DataFrame(table,
95128
copycols=false)` before being prepended.
@@ -164,12 +197,45 @@ julia> df1
164197
4 │ 1 1
165198
5 │ 2 2
166199
6 │ 3 3
200+
201+
julia> prepend!(df2, DataFrame(A=1), (; C=1:2), cols=:union)
202+
6×3 DataFrame
203+
Row │ A B C
204+
│ Float64? Int64? Int64?
205+
─────┼─────────────────────────────
206+
1 │ 1.0 missing missing
207+
2 │ missing missing 1
208+
3 │ missing missing 2
209+
4 │ 4.0 4 missing
210+
5 │ 5.0 5 missing
211+
6 │ 6.0 6 missing
167212
```
168213
"""
169214
Base.prepend!(df1::DataFrame, df2::AbstractDataFrame; cols::Symbol=:setequal,
170215
promote::Bool=(cols in [:union, :subset])) =
171216
_append_or_prepend!(df1, df2, cols=cols, promote=promote, atend=false)
172217

218+
function Base.prepend!(df::DataFrame, table; cols::Symbol=:setequal,
219+
promote::Bool=(cols in [:union, :subset]))
220+
if table isa Dict && cols == :orderequal
221+
throw(ArgumentError("passing `Dict` as `table` when `cols` is equal to " *
222+
"`:orderequal` is not allowed as it is unordered"))
223+
end
224+
prepend!(df, DataFrame(table, copycols=false), cols=cols, promote=promote)
225+
end
226+
227+
function Base.prepend!(df::DataFrame, @nospecialize tables...;
228+
cols::Symbol=:setequal,
229+
promote::Bool=(cols in [:union, :subset]))
230+
if !(cols in (:orderequal, :setequal, :intersect, :subset, :union))
231+
throw(ArgumentError("`cols` keyword argument must be " *
232+
":orderequal, :setequal, :intersect, :subset or :union)"))
233+
end
234+
235+
return foldr((table, df) -> prepend!(df, table, cols=cols, promote=promote),
236+
collect(Any, tables), init=df)
237+
end
238+
173239
function _append_or_prepend!(df1::DataFrame, df2::AbstractDataFrame; cols::Symbol,
174240
promote::Bool, atend::Bool)
175241
if !(cols in (:orderequal, :setequal, :intersect, :subset, :union))
@@ -355,6 +421,10 @@ following way:
355421
added to `df` (using `missing` for existing rows) and a `missing` value is
356422
pushed to columns missing in `row` that are present in `df`.
357423
424+
If `row` is not a `DataFrameRow`, `NamedTuple`, `AbstractDict`, or `Tables.AbstractRow`
425+
the `cols` keyword argument must be `:setequal` (the default),
426+
because such rows do not provide column name information.
427+
358428
If `promote=true` and element type of a column present in `df` does not allow
359429
the type of a pushed argument then a new column with a promoted element type
360430
allowing it is freshly allocated and stored in `df`. If `promote=false` an error
@@ -371,12 +441,14 @@ $METADATA_FIXED
371441
"""
372442

373443
"""
374-
push!(df::DataFrame, row::Union{Tuple, AbstractArray}; promote::Bool=false)
444+
push!(df::DataFrame, row::Union{Tuple, AbstractArray}...;
445+
cols::Symbol=:setequal, promote::Bool=false)
375446
push!(df::DataFrame, row::Union{DataFrameRow, NamedTuple, AbstractDict,
376-
Tables.AbstractRow};
447+
Tables.AbstractRow}...;
377448
cols::Symbol=:setequal, promote::Bool=(cols in [:union, :subset]))
378449
379450
Add one row at the end of `df` in-place, taking the values from `row`.
451+
Several rows can be added by passing them as separate arguments.
380452
381453
$INSERTION_COMMON
382454
@@ -452,18 +524,36 @@ julia> push!(df, NamedTuple(), cols=:subset)
452524
6 │ 11 12 missing
453525
7 │ 1.0 missing 1.0
454526
8 │ missing missing missing
527+
528+
julia> push!(DataFrame(a=1, b=2), (3, 4), (5, 6))
529+
3×2 DataFrame
530+
Row │ a b
531+
│ Int64 Int64
532+
─────┼──────────────
533+
1 │ 1 2
534+
2 │ 3 4
535+
3 │ 5 6
455536
```
456537
"""
457-
Base.push!(df::DataFrame, row::Any; promote::Bool=false) =
458-
_row_inserter!(df, -1, row, Val{:push}(), promote)
538+
function Base.push!(df::DataFrame, row::Any;
539+
cols=:setequal, promote::Bool=false)
540+
if cols !== :setequal
541+
throw(ArgumentError("`cols` can only be `:setequal` when `row` is a `$(typeof(row))` " *
542+
"as this type does not provide column names"))
543+
end
544+
545+
return _row_inserter!(df, -1, row, Val{:push}(), promote)
546+
end
459547

460548
"""
461-
pushfirst!(df::DataFrame, row::Union{Tuple, AbstractArray}; promote::Bool=false)
549+
pushfirst!(df::DataFrame, row::Union{Tuple, AbstractArray}...;
550+
cols::Symbol=:setequal, promote::Bool=false)
462551
pushfirst!(df::DataFrame, row::Union{DataFrameRow, NamedTuple, AbstractDict,
463-
Tables.AbstractRow};
552+
Tables.AbstractRow}...;
464553
cols::Symbol=:setequal, promote::Bool=(cols in [:union, :subset]))
465554
466555
Add one row at the beginning of `df` in-place, taking the values from `row`.
556+
Several rows can be added by passing them as separate arguments.
467557
468558
$INSERTION_COMMON
469559
@@ -539,13 +629,30 @@ julia> pushfirst!(df, NamedTuple(), cols=:subset)
539629
6 │ a 1 missing
540630
7 │ b 2 missing
541631
8 │ c 3 missing
632+
633+
julia> pushfirst!(DataFrame(a=1, b=2), (3, 4), (5, 6))
634+
3×2 DataFrame
635+
Row │ a b
636+
│ Int64 Int64
637+
─────┼──────────────
638+
1 │ 3 4
639+
2 │ 5 6
640+
3 │ 1 2
542641
```
543642
"""
544-
Base.pushfirst!(df::DataFrame, row::Any; promote::Bool=false) =
545-
_row_inserter!(df, -1, row, Val{:pushfirst}(), promote)
643+
function Base.pushfirst!(df::DataFrame, row::Any;
644+
cols=:setequal, promote::Bool=false)
645+
if cols !== :setequal
646+
throw(ArgumentError("`cols` can only be `:setequal` when `row` is a `$(typeof(row))` " *
647+
"as this type does not provide column names"))
648+
end
649+
650+
return _row_inserter!(df, -1, row, Val{:pushfirst}(), promote)
651+
end
546652

547653
"""
548-
insert!(df::DataFrame, index::Integer, row::Union{Tuple, AbstractArray}; promote::Bool=false)
654+
insert!(df::DataFrame, index::Integer, row::Union{Tuple, AbstractArray};
655+
cols::Symbol=:setequal, promote::Bool=false)
549656
insert!(df::DataFrame, index::Integer, row::Union{DataFrameRow, NamedTuple,
550657
AbstractDict, Tables.AbstractRow};
551658
cols::Symbol=:setequal, promote::Bool=(cols in [:union, :subset]))
@@ -629,7 +736,13 @@ julia> insert!(df, 3, NamedTuple(), cols=:subset)
629736
8 │ 1.0 missing 1.0
630737
```
631738
"""
632-
function Base.insert!(df::DataFrame, index::Integer, row::Any; promote::Bool=false)
739+
function Base.insert!(df::DataFrame, index::Integer, row::Any;
740+
cols=:setequal, promote::Bool=false)
741+
if cols !== :setequal
742+
throw(ArgumentError("`cols` can only be `:setequal` when `row` is a `$(typeof(row))` " *
743+
"as this type does not provide column names"))
744+
end
745+
633746
index isa Bool && throw(ArgumentError("invalid index: $index of type Bool"))
634747
1 <= index <= nrow(df)+1 ||
635748
throw(ArgumentError("invalid index: $index for data frame with $(nrow(df)) rows"))
@@ -986,3 +1099,37 @@ function _row_inserter!(df::DataFrame, loc::Integer,
9861099
_drop_all_nonnote_metadata!(df)
9871100
return df
9881101
end
1102+
1103+
function Base.push!(df::DataFrame, @nospecialize rows...;
1104+
cols::Symbol=:setequal,
1105+
promote::Bool=(cols in [:union, :subset]))
1106+
if !(cols in (:orderequal, :setequal, :intersect, :subset, :union))
1107+
throw(ArgumentError("`cols` keyword argument must be " *
1108+
":orderequal, :setequal, :intersect, :subset or :union)"))
1109+
end
1110+
with_names_count = count(rows) do row
1111+
row isa Union{DataFrameRow,AbstractDict,NamedTuple,Tables.AbstractRow}
1112+
end
1113+
if 0 < with_names_count < length(rows)
1114+
throw(ArgumentError("Mixing rows with column names and without column names " *
1115+
"in a single `push!` call is not allowed"))
1116+
end
1117+
return foldl((df, row) -> push!(df, row, cols=cols, promote=promote), rows, init=df)
1118+
end
1119+
1120+
function Base.pushfirst!(df::DataFrame, @nospecialize rows...;
1121+
cols::Symbol=:setequal,
1122+
promote::Bool=(cols in [:union, :subset]))
1123+
if !(cols in (:orderequal, :setequal, :intersect, :subset, :union))
1124+
throw(ArgumentError("`cols` keyword argument must be " *
1125+
":orderequal, :setequal, :intersect, :subset or :union)"))
1126+
end
1127+
with_names_count = count(rows) do row
1128+
row isa Union{DataFrameRow,AbstractDict,NamedTuple,Tables.AbstractRow}
1129+
end
1130+
if 0 < with_names_count < length(rows)
1131+
throw(ArgumentError("Mixing rows with column names and without column names " *
1132+
"in a single `push!` call is not allowed"))
1133+
end
1134+
return foldr((row, df) -> pushfirst!(df, row, cols=cols, promote=promote), rows, init=df)
1135+
end

src/other/tables.jl

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -63,31 +63,13 @@ function DataFrame(x; copycols::Union{Nothing, Bool}=nothing)
6363
end
6464

6565
# the logic here relies on the fact that Tables.CopiedColumns
66-
# is the only exception for default copycols value
66+
# is the only exception for default copycols value
6767
DataFrame(x, cnames::AbstractVector; makeunique::Bool=false,
6868
copycols::Union{Nothing, Bool}=nothing) =
6969
rename!(DataFrame(x, copycols=something(copycols, !(x isa Tables.CopiedColumns))),
7070
_name2symbol(cnames),
7171
makeunique=makeunique)
7272

73-
function Base.append!(df::DataFrame, table; cols::Symbol=:setequal,
74-
promote::Bool=(cols in [:union, :subset]))
75-
if table isa Dict && cols == :orderequal
76-
throw(ArgumentError("passing `Dict` as `table` when `cols` is equal to " *
77-
"`:orderequal` is not allowed as it is unordered"))
78-
end
79-
append!(df, DataFrame(table, copycols=false), cols=cols, promote=promote)
80-
end
81-
82-
function Base.prepend!(df::DataFrame, table; cols::Symbol=:setequal,
83-
promote::Bool=(cols in [:union, :subset]))
84-
if table isa Dict && cols == :orderequal
85-
throw(ArgumentError("passing `Dict` as `table` when `cols` is equal to " *
86-
"`:orderequal` is not allowed as it is unordered"))
87-
end
88-
prepend!(df, DataFrame(table, copycols=false), cols=cols, promote=promote)
89-
end
90-
9173
# This supports the Tables.RowTable type; needed to avoid ambiguities w/ another constructor
9274
DataFrame(x::AbstractVector{NamedTuple{names, T}}; copycols::Bool=true) where {names, T} =
9375
fromcolumns(Tables.columns(Tables.IteratorWrapper(x)), collect(names), copycols=false)

0 commit comments

Comments
 (0)