Эффективное объединение наборов данных больших объектов, имеющих несколько совпадающих ключей - PullRequest
0 голосов
/ 06 декабря 2018

В скрипте Powershell у меня есть два набора данных, которые имеют несколько столбцов.Не все эти столбцы являются общими.

Например, набор данных 1:

A B    XY   ZY  
- -    --   --  
1 val1 foo1 bar1
2 val2 foo2 bar2
3 val3 foo3 bar3
4 val4 foo4 bar4
5 val5 foo5 bar5
6 val6 foo6 bar6

и набор данных 2:

A B    ABC  GH  
- -    ---  --  
3 val3 foo3 bar3
4 val4 foo4 bar4
5 val5 foo5 bar5
6 val6 foo6 bar6
7 val7 foo7 bar7
8 val8 foo8 bar8

Я хочу объединить эти два набора данных, указав, какие столбцы выступают в качестве ключа (A и B в моем простом случае).Ожидаемый результат:

A B    XY   ZY   ABC  GH  
- -    --   --   ---  --  
1 val1 foo1 bar1          
2 val2 foo2 bar2          
3 val3 foo3 bar3 foo3 bar3
4 val4 foo4 bar4 foo4 bar4
5 val5 foo5 bar5 foo5 bar5
6 val6 foo6 bar6 foo6 bar6
7 val7           foo7 bar7
8 val8           foo8 bar8

Эта концепция очень похожа на запрос перекрестного соединения SQL.

Мне удалось успешно написать функцию, которая объединяет объекты.К сожалению, продолжительность вычислений является экспоненциальной.

Если я генерирую свои наборы данных, используя:

$dsLength = 10
$dataset1 = 0..$dsLength | %{
    New-Object psobject -Property @{ A=$_ ; B="val$_" ; XY = "foo$_"; ZY ="bar$_" }
}
$dataset2 = ($dsLength/2)..($dsLength*1.5) | %{
    New-Object psobject -Property @{ A=$_ ; B="val$_" ; ABC = "foo$_"; GH ="bar$_" }
}

, я получаю следующие результаты:

  • $dsLength = 10==> 33 мс (отлично)
  • $dsLength = 100 ==> 89 мс (отлично)
  • $dsLength = 1000 ==> 1563 мс (приемлемо)
  • $dsLength = 5000 ==> 35764 мс (слишком много)
  • $dsLength = 10000 ==> 138047 мс (слишком много)
  • $dsLength = 20000 ==> 573614 мс (слишком много)

Как я могу эффективно объединять наборы данных, когда наборы данных большие (моя цель - около 20 тыс. Элементов)?

Сейчас я определил эти функции:

function Merge-Objects{
    param(
        [Parameter(Mandatory=$true)]
        [object[]]$Dataset1,
        [Parameter(Mandatory=$true)]
        [object[]]$Dataset2,
        [Parameter()]
        [string[]]$Properties
    )

    $result = @()

    $ds1props = $Dataset1 | gm -MemberType Properties
    $ds2props = $Dataset2 | gm -MemberType Properties
    $ds1propsNotInDs2Props = $ds1props | ? { $_.Name -notin ($ds2props | Select -ExpandProperty Name) }
    $ds2propsNotInDs1Props = $ds2props | ? { $_.Name -notin ($ds1props | Select -ExpandProperty Name) }

    foreach($row1 in $Dataset1){
        $result += $row1
        $ds2propsNotInDs1Props | % {
            $row1 | Add-Member -MemberType $_.MemberType -Name $_.Name -Value $null
        }
    }

    foreach($row2 in $Dataset2){
        $existing = foreach($candidate in $result){
            $match = $true
            foreach($prop in $Properties){
                if(-not ($row2.$prop -eq $candidate.$prop)){
                    $match = $false                   
                    break                  
                }
            }
            if($match){
                $candidate
                break
            }
        }
        if(!$existing){
            $ds1propsNotInDs2Props | % {
                $row2 | Add-Member -MemberType $_.MemberType -Name $_.Name -Value $null
            }
            $result += $row2
        }else{
            $ds2propsNotInDs1Props | % {
                $existing.$($_.Name) = $row2.$($_.Name)
            }

        }
    }

    $result
}

Я называю эти функции следующим образом:

Measure-Command -Expression {

    $data = Merge-Objects -Dataset1 $dataset1 -Dataset2 $dataset2 -Properties "A","B" 

}

Мне кажется, что медлительность связана со вторым циклом, где я пытаюсь сопоставить существующую строку в каждой итерации

[Edit] Второй подход с использованиемхэш в качестве индекса.Удивительно, но это событие медленнее, чем первая попытка

$dsLength = 1000
$dataset1 = 0..$dsLength | %{
    New-Object psobject -Property @{ A=$_ ; B="val$_" ; XY = "foo$_"; ZY ="bar$_" }
}
$dataset2 = ($dsLength/2)..($dsLength*1.5) | %{
    New-Object psobject -Property @{ A=$_ ; B="val$_" ; ABC = "foo$_"; GH ="bar$_" }
}

function Get-Hash{
    param(
        [Parameter(Mandatory=$true)]
        [object]$InputObject,
        [Parameter()]
        [string[]]$Properties    
    )

    $InputObject | Select-object $properties | Out-String
}


function Merge-Objects{
    param(
        [Parameter(Mandatory=$true)]
        [object[]]$Dataset1,
        [Parameter(Mandatory=$true)]
        [object[]]$Dataset2,
        [Parameter()]
        [string[]]$Properties
    )

    $result = @()
    $index = @{}

    $ds1props = $Dataset1 | gm -MemberType Properties
    $ds2props = $Dataset2 | gm -MemberType Properties
    $allProps = $ds1props + $ds2props | select -Unique

    $ds1propsNotInDs2Props = $ds1props | ? { $_.Name -notin ($ds2props | Select -ExpandProperty Name) }
    $ds2propsNotInDs1Props = $ds2props | ? { $_.Name -notin ($ds1props | Select -ExpandProperty Name) }

    $ds1index = @{}

    foreach($row1 in $Dataset1){
        $tempObject = new-object psobject
        $result += $tempObject
        $ds2propsNotInDs1Props | % {
            $tempObject | Add-Member -MemberType $_.MemberType -Name $_.Name -Value $null
        }
        $ds1props | % {
            $tempObject | Add-Member -MemberType $_.MemberType -Name $_.Name -Value $row1.$($_.Name)
        }

        $hash1 = Get-Hash -InputObject $row1 -Properties $Properties
        $ds1index.Add($hash1, $tempObject)

    }

    foreach($row2 in $Dataset2){
        $hash2 = Get-Hash -InputObject $row2 -Properties $Properties

        if($ds1index.ContainsKey($hash2)){
            # merge object
            $existing = $ds1index[$hash2]
            $ds2propsNotInDs1Props | % {
                $existing.$($_.Name) = $row2.$($_.Name)
            }
            $ds1index.Remove($hash2)

        }else{
            # add object
            $tempObject = new-object psobject
            $ds1propsNotInDs2Props | % {
                $tempObject | Add-Member -MemberType $_.MemberType -Name $_.Name -Value $null
            }
            $ds2props | % {
                $tempObject | Add-Member -MemberType $_.MemberType -Name $_.Name -Value $row2.$($_.Name)
            }
            $result += $tempObject
        }
    }

    $result
}

Measure-Command -Expression {

    $data = Merge-Objects -Dataset1 $dataset1 -Dataset2 $dataset2 -Properties "A","B" 

}

[Edit2] Размещение команд измерения вокруг двух циклов показывает, что событие в первом цикле еще медленное .На самом деле первый цикл занимает более 50% от общего времени

Ответы [ 2 ]

0 голосов
/ 06 апреля 2019

У меня было много сомнений включить бинарный поиск (хеш-таблицу) в мой командлет Join-Object (см. Также: В Powershell,Каков наилучший способ объединения двух таблиц в одну? ), так как есть несколько проблем, которые нужно преодолеть, которые удобно исключить из примера в вопросе.

К сожалению, я не могу конкурировать спроизводительность решения @mhhollomon:

dsLength Steve1 Steve2 mhhollomon Join-Object
-------- ------ ------ ---------- -----------
      10     19    129         21          50
     100    145    915        158         329
    1000   2936   9646       1575        3355
    5000  56129  69558       5814       12653
   10000 183813  95472      14740       25730
   20000 761450 265061      36822       80644

Но я думаю, что могу добавить некоторое значение:

не верно

Хеш-ключи - это строки, а это значит, что вам нужноприведение связанных свойств к строкам, что немного сомнительно просто, потому что:

$Left -eq $Right ≠ "$Left" -eq "$Right"

В большинстве случаев это будет работать, особенно когда источником является файл .csv, но он может работать неправильно, например, в случаеданные поступают из командлета, где $Null означает что-то еще, а не пустую строку ('').Поэтому я рекомендую явно определять ключи $Null, например, с помощью управляющего символа .
И поскольку значения свойств могут легко содержать двоеточие (:), я бы также рекомендовал использовать элемент управлениясимвол для разделения (объединения) нескольких клавиш.

Также верно

Существует еще одна ловушка при использовании хеш-таблицы, которая на самом деле не должна быть проблемой: что делать, если слева ($dataset1) и / или справа ($dataset2) есть несколько совпадений.Возьмите, например, следующие наборы данных:

$dataset1 = ConvertFrom-SourceTable '

    A B    XY    ZY  
    - -    --    --  
    1 val1 foo1  bar1
    2 val2 foo2  bar2
    3 val3 foo3  bar3
    4 val4 foo4  bar4
    4 val4 foo4a bar4a
    5 val5 foo5  bar5
    6 val6 foo6  bar6
'

$dataset2 = ConvertFrom-SourceTable '

    A B    ABC   GH  
    - -    ---   --  
    3 val3 foo3  bar3
    4 val4 foo4  bar4
    5 val5 foo5  bar5
    5 val5 foo5a bar5a
    6 val6 foo6  bar6
    7 val7 foo7  bar7
    8 val8 foo8  bar8
'

В этом случае я ожидал бы аналогичного результата при соединении SQL и без ошибки Item has already been added. Key in dictionary:

$Dataset1 | FullJoin $dataset2 -On A, B | Format-Table

A B    XY    ZY    ABC   GH
- -    --    --    ---   --
1 val1 foo1  bar1
2 val2 foo2  bar2
3 val3 foo3  bar3  foo3  bar3
4 val4 foo4  bar4  foo4  bar4
4 val4 foo4a bar4a foo4  bar4
5 val5 foo5  bar5  foo5  bar5
5 val5 foo5  bar5  foo5a bar5a
6 val6 foo6  bar6  foo6  bar6
7 val7             foo7  bar7
8 val8             foo8  bar8

Only Right

AsВы могли бы выяснить, что нет причин помещать обе стороны в хеш-таблицу, но вы можете рассмотреть stream левую сторону (вместо того, чтобы задушить ввод).В примере с вопросом оба набора данных загружаются непосредственно в память, что вряд ли используется.Чаще всего ваши данные поступают откуда-то еще, например, удаленно из активного каталога, где вы могли бы одновременно выполнять поиск каждого входящего объекта в хеш-таблице, прежде чем появится следующий объект. То же самое относится и к следующему командлету: он может запускаться напрямуюобрабатывает вывод и не должен ждать, пока ваш командлет будет завершен (обратите внимание, что данные сразу же освобождаются из командлета Join-Object, когда он будет готов).В таком случае измерение производительности с использованием Measure-Command требует совершенно другого подхода ...
См. Также: Компьютерное программирование. Является ли последовательный режим конвейера PowerShell более эффективным для использования памяти?Почему или почему нет?

0 голосов
/ 06 декабря 2018

Я согласен с @Matt.Используйте хеш-таблицу - что-то вроде ниже.Это должно выполняться в m + 2n, а не mn времени.

Время в моей системе

Исходное решение выше

#10    TotalSeconds      :   0.07788
#100   TotalSeconds      :   0.37937
#1000  TotalSeconds      :   5.25092
#10000 TotalSeconds      : 242.82018
#20000 TotalSeconds      : 906.01584

Это определенно выглядит O (n ^ 2)

Решение ниже

#10    TotalSeconds      :  0.094
#100   TotalSeconds      :  0.425
#1000  TotalSeconds      :  3.757
#10000 TotalSeconds      : 45.652
#20000 TotalSeconds      : 92.918

Это выглядит линейно.

Решение

Я использовал три метода для увеличения скорости:

  1. Переход на хеш-таблицу.Это позволяет осуществлять постоянный поиск, поэтому вам не нужно иметь вложенные циклы.Это единственное изменение, действительно необходимое для перехода от O (n ^ 2) к линейному времени.Недостатком является то, что больше работы по настройке сделано.Таким образом, преимущество линейного времени не будет видно, пока количество циклов не станет достаточно большим, чтобы оплатить настройку.
  2. Используйте ArrayList вместо собственного массива.Добавление элемента в собственный массив требует перераспределения массива и копирования всех элементов.Так что это тоже операция O (n ^ 2).Поскольку эта операция выполняется на уровне двигателя, константа очень мала, поэтому она не изменится намного позже.
  3. Используйте PsObject.Copy для создания нового объекта.Это небольшая оптимизация по сравнению с двумя другими, но для меня она сократила время выполнения вдвое.

-

function Get-Hash{
    param(
        [Parameter(Mandatory=$true)]
        [object]$InputObject,
        [Parameter()]
        [string[]]$Properties    
    )

    $arr = [System.Collections.ArrayList]::new()

    foreach($p in $Properties) { $arr += $InputObject.$($p) }

    return ( $arr -join ':' )
}

function Merge-Objects{
    param(
        [Parameter(Mandatory=$true)]
        [object[]]$Dataset1,
        [Parameter(Mandatory=$true)]
        [object[]]$Dataset2,
        [Parameter()]
        [string[]]$Properties
    )

    $results = [System.Collections.ArrayList]::new()

    $ds1props = $Dataset1 | gm -MemberType Properties
    $ds2props = $Dataset2 | gm -MemberType Properties
    $ds1propsNotInDs2Props = $ds1props | ? { $_.Name -notin ($ds2props | Select -ExpandProperty Name) }
    $ds2propsNotInDs1Props = $ds2props | ? { $_.Name -notin ($ds1props | Select -ExpandProperty Name) }


    $hash = @{}
    $Dataset2 | % { $hash.Add( (Get-Hash $_ $Properties), $_) }

    foreach ($row in $dataset1) {

        $key = Get-Hash $row $Properties

        $tempObject = $row.PSObject.Copy()

        if ($hash.containskey($key)) {
            $r2 = $hash[$key]

            $hash.remove($key)
            $ds2propsNotInDs1Props | % {
                $tempObject | Add-Member -MemberType $_.MemberType -Name $_.Name -Value $r2.$($_.Name)
            }

        } else {
            $ds2propsNotInDs1Props | % {
                $tempObject | Add-Member -MemberType $_.MemberType -Name $_.Name -Value $null
            }
        }
        [void]$results.Add($tempObject)
    }

    foreach ($row in $hash.values ) {
        # add missing dataset2 objects and extend
        $tempObject = $row.PSObject.Copy()

        $ds1propsNotInDs2Props | % {
            $tempObject | Add-Member -MemberType $_.MemberType -Name $_.Name -Value $null
        }

        [void]$results.Add($tempObject)
    }

    $results
}

########

$dsLength = 10000
$dataset1 = 0..$dsLength | %{
    New-Object psobject -Property @{ A=$_ ; B="val$_" ; XY = "foo$_"; ZY ="bar$_" }
}
$dataset2 = ($dsLength/2)..($dsLength*1.5) | %{
    New-Object psobject -Property @{ A=$_ ; B="val$_" ; ABC = "foo$_"; GH ="bar$_" }
}

Measure-Command -Expression {

    $data = Merge-Objects -Dataset1 $dataset1 -Dataset2 $dataset2 -Properties "A","B" 

}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...