Estoy tratando de acelerar el ciclo, en el que los marcos de datos consecutivos se unen con el primero con la primera columna como clave. Los marcos de datos son producidos por una función my_function. La primera columna se llama :REF. Los marcos de datos consecutivos podrían ser más cortos que el primero, por lo que no puedo asignarlos directamente a la columna DF, como lo haría en pandas.

base_df = my_function(elem1)

for elem in elems[2:end]
    tmp = my_function(elem)
    base_df = join(base_df, tmp, on=:REF, kind=:left)
end

¿Hay alguna forma de unir la lista de marcos de datos en uno? Gracias,

PD: Los DataFrames son de diferentes tipos: String, Int, Float64.

Upd. Por ejemplo, DataFrames:

df1 = DataFrame(REF = 1:5, D1=rand(5))
df2 = DataFrame(REF = 1:3, D1=rand(3))
df3 = DataFrame(REF = 1:4, D1=rand(4))

Lo que busco es combinar esos tres (o más) en un solo DataFrame a la vez. Tenga en cuenta las diferencias de recuento de filas.

Upd2. Lo sentimos, deberían haber sido columnas diferentes en df1, df2 y df3 (D1, D2 y D3). Aquí está la configuración correcta de DF

df1 = DataFrame(REF = 1:5, D1=rand(5))
df2 = DataFrame(REF = 1:3, D2=rand(3))
df3 = DataFrame(REF = 1:4, D3=rand(4))
2
crayxt 19 feb. 2018 a las 14:23

2 respuestas

La mejor respuesta

Aquí hay un enfoque alternativo que asume que desea una combinación izquierda (como en su pregunta, si necesita otro tipo de combinación, debería ser simple ajustarlo). La diferencia con la solución Dan Getz es que no usa DataVector pero opera en arreglos que permiten missing (puede verificar la diferencia ejecutando showcols en el DataFrame resultante; el beneficio es que será más eficiente trabajar con dichos datos más adelante ya que conoceremos sus tipos):

function joiner(ref_left, ref_right, val_right)
    x = DataFrames.similar_missing(val_right, length(ref_left))
    j = 1
    for i in 1:length(ref_left)
        while ref_left[i] > ref_right[j]
            j += 1
            j > length(ref_right) && return x
        end
        if ref_left[i] == ref_right[j]
            x[i] = val_right[j]
        end
    end
    return x
end

function left_join_sorted(elems::Vector{DataFrame}, on::Symbol)
    # we perform left join to base_df
    # the columns of elems[1] will be reused, use deepcopy if you want fresh columns
    base_df = copy(elems[1])
    ref_left = base_df[:REF]
    for i in 2:length(elems)
        df = elems[i]
        ref_right = df[:REF]
        for n in names(df)
            if n != on
                # this assumes that column names in all data frames except on are unique, otherwise they will be overwritten
                # we perform left join to the first DataFrame in elems
                base_df[n] = joiner(ref_left, ref_right, df[n])
            end
        end
    end
    base_df
end

Aquí hay un ejemplo de uso:

julia> left_join_sorted([df1, df2, df3], :REF)
5×4 DataFrames.DataFrame
│ Row │ REF │ D1       │ D2        │ D3       │
├─────┼─────┼──────────┼───────────┼──────────┤
│ 1   │ 1   │ 0.133361 │ 0.179822  │ 0.200842 │
│ 2   │ 2   │ 0.548581 │ 0.836018  │ 0.906814 │
│ 3   │ 3   │ 0.304062 │ 0.0797432 │ 0.946639 │
│ 4   │ 4   │ 0.755515 │ missing   │ 0.519437 │
│ 5   │ 5   │ 0.571302 │ missing   │ missing  │

Como beneficio adicional, mis puntos de referencia muestran que esto es ~ 20 veces más rápido que usar DataVector (si desea una aceleración adicional, use @inbounds pero probablemente los beneficios no valen los riesgos).

EDITAR: condición fija en el bucle joiner.

2
Bogumił Kamiński 20 feb. 2018 a las 06:28

Solo para configurar la respuesta, suponga:

df1 = DataFrame(REF = 1:5, D1=rand(5))
df2 = DataFrame(REF = 1:3, D1=rand(3))
df3 = DataFrame(REF = 1:4, D1=rand(4))

elems = [df1, df2, df3]
my_function = identity

Ahora el código para generar el Big DataFrame:

dfs = my_function.(elems)
base_df = DataFrame(Dict([f=>vcat(getindex.(dfs,f)...) for f in names(dfs[1])]...))

Dando algo como:

12×2 DataFrames.DataFrame
│ Row │ D1         │ REF │
├─────┼────────────┼─────┤
│ 1   │ 0.664144   │ 1   │
│ 2   │ 0.119155   │ 2   │
│ 3   │ 0.471053   │ 3   │
│ 4   │ 0.547811   │ 4   │
│ 5   │ 0.600263   │ 5   │
│ 6   │ 0.21306    │ 1   │
│ 7   │ 0.985412   │ 2   │
│ 8   │ 0.886738   │ 3   │
│ 9   │ 0.00926173 │ 1   │
│ 10  │ 0.701962   │ 2   │
│ 11  │ 0.328322   │ 3   │
│ 12  │ 0.753062   │ 4   │

Este enfoque reduce la memoria utilizada de cuadrática a lineal (y el rendimiento mejora en línea con la reducción de memoria)

ACTUALIZACIÓN

A medida que salieron a la luz nuevos detalles (y mi comprensión de la pregunta mejoró), aquí está el código para generar mejor el base_df deseado:

df1 = DataFrame(REF = 1:5, D1=rand(5))
df2 = DataFrame(REF = 1:3, D2=rand(3))
df3 = DataFrame(REF = 1:4, D3=rand(4))
elems = [df1, df2, df3]

cols = [(i,f) for (i,t) in enumerate(elems) for f in names(t) if !(f == :REF)]
rows = union(getindex.(elems,:REF)...)
ref2row = Dict(v=>i for (i,v) in enumerate(rows))

pre_df = Dict{Symbol,DataVector{Any}}([c[2]=>DataArray(eltype(elems[c[1]][c[2]]),
 length(rows)) for c in cols])

foreach(tpl -> pre_df[tpl[3][1]][ref2row[tpl[2]]] = tpl[3][2],
 [(i,r[:REF],v) 
  for (i,t) in enumerate(elems) 
  for r in eachrow(t) 
  for v in r if v[1] != :REF
 ])

pre_df[:REF] = [ref2row[i] for i=1:length(rows)]

base_df = DataFrame(pre_df)

Dando:

5×4 DataFrames.DataFrame
│ Row │ D1       │ D2       │ D3        │ REF │
├─────┼──────────┼──────────┼───────────┼─────┤
│ 1   │ 0.93479  │ 0.582954 │ 0.133983  │ 1   │
│ 2   │ 0.472456 │ 0.992173 │ 0.32442   │ 2   │
│ 3   │ 0.365478 │ 0.117772 │ 0.62522   │ 3   │
│ 4   │ 0.976192 │ NA       │ 0.0861988 │ 4   │
│ 5   │ 0.76358  │ NA       │ NA        │ 5   │
2
Dan Getz 19 feb. 2018 a las 17:29