2009-08-20 8 views
18

Tło: Mam sekwencję ciągłych, stempelkowanych danych. Sekwencja danych ma dziury, niektóre duże, inne tylko jedną brakującą wartość.
Ilekroć otwór jest tylko pojedynczą brakującą wartością, chcę załatać dziury za pomocą fałszywej wartości (większe dziury zostaną zignorowane).Dlaczego użycie sekwencji jest o wiele wolniejsze niż użycie listy w tym przykładzie?

Chciałbym użyć leniwego generowania poprawionej sekwencji, a tym samym używam Seq.unfold.

Wykonałem dwie wersje metody łatania otworów w danych.

Pierwszy zużywa sekwencję danych z otworów w nim i produkuje połatany sekwencję. To jest to, czego chcę, ale metody działają strasznie wolno, gdy liczba elementów w sekwencji wejściowej wzrośnie powyżej 1000, i stopniowo staje się coraz gorsza, im więcej elementów zawiera sekwencja wejściowa.

Druga metoda zużywa listy danych z otworami i produkuje połatany sekwencję i działa szybko. Nie jest to jednak to, czego chcę, ponieważ wymusza to utworzenie w pamięci całej listy danych wejściowych.

Chciałbym użyć metody (sekwencji -> sekwencji) zamiast metody (list -> sequence), aby w tej samej chwili nie mieć całej listy wejść w pamięci.

Pytania:

1) Dlaczego pierwsza metoda tak powoli (stopniowo coraz gorzej z większych list wejściowych) (jestem podejrzewać, że ma do czynienia z wielokrotnie tworzeniu nowych sekwencji z Seq.skip 1, ale nie jestem pewien)

2) w jaki sposób mogę dokonać łatanie dziur w danych szybko, podczas korzystania z wejścia sekwencję raczej niż wejściowego listy?

Kod:

open System 

// Method 1 (Slow) 
let insertDummyValuesWhereASingleValueIsMissing1 (timeBetweenContiguousValues : TimeSpan) (values : seq<(DateTime * float)>) = 
    let sizeOfHolesToPatch = timeBetweenContiguousValues.Add timeBetweenContiguousValues // Only insert dummy-values when the gap is twice the normal 
    (None, values) |> Seq.unfold (fun (prevValue, restOfValues) -> 
     if restOfValues |> Seq.isEmpty then 
      None // Reached the end of the input seq 
     else 
      let currentValue = Seq.hd restOfValues 
      if prevValue.IsNone then 
       Some(currentValue, (Some(currentValue), Seq.skip 1 restOfValues )) // Only happens to the first item in the seq 
      else 
       let currentTime = fst currentValue 
       let prevTime = fst prevValue.Value 
       let timeDiffBetweenPrevAndCurrentValue = currentTime.Subtract(prevTime) 
       if timeDiffBetweenPrevAndCurrentValue = sizeOfHolesToPatch then 
        let dummyValue = (prevTime.Add timeBetweenContiguousValues, 42.0) // 42 is chosen here for obvious reasons, making this comment superfluous 
        Some(dummyValue, (Some(dummyValue), restOfValues)) 
       else 
        Some(currentValue, (Some(currentValue), Seq.skip 1 restOfValues))) // Either the two values were contiguous, or the gap between them was too large to patch 

// Method 2 (Fast) 
let insertDummyValuesWhereASingleValueIsMissing2 (timeBetweenContiguousValues : TimeSpan) (values : (DateTime * float) list) = 
    let sizeOfHolesToPatch = timeBetweenContiguousValues.Add timeBetweenContiguousValues // Only insert dummy-values when the gap is twice the normal 
    (None, values) |> Seq.unfold (fun (prevValue, restOfValues) -> 
     match restOfValues with 
     | [] -> None // Reached the end of the input list 
     | currentValue::restOfValues -> 
      if prevValue.IsNone then 
       Some(currentValue, (Some(currentValue), restOfValues )) // Only happens to the first item in the list 
      else 
       let currentTime = fst currentValue 
       let prevTime = fst prevValue.Value 
       let timeDiffBetweenPrevAndCurrentValue = currentTime.Subtract(prevTime) 
       if timeDiffBetweenPrevAndCurrentValue = sizeOfHolesToPatch then 
        let dummyValue = (prevTime.Add timeBetweenContiguousValues, 42.0) 
        Some(dummyValue, (Some(dummyValue), currentValue::restOfValues)) 
       else 
        Some(currentValue, (Some(currentValue), restOfValues))) // Either the two values were contiguous, or the gap between them was too large to patch 

// Test data 
let numbers = {1.0..10000.0} 
let contiguousTimeStamps = seq { for n in numbers -> DateTime.Now.AddMinutes(n)} 

let dataWithOccationalHoles = Seq.zip contiguousTimeStamps numbers |> Seq.filter (fun (dateTime, num) -> num % 77.0 <> 0.0) // Has a gap in the data every 77 items 

let timeBetweenContiguousValues = (new TimeSpan(0,1,0)) 

// The fast sequence-patching (method 2) 
dataWithOccationalHoles |> List.of_seq |> insertDummyValuesWhereASingleValueIsMissing2 timeBetweenContiguousValues |> Seq.iter (fun pair -> printfn "%f %s" (snd pair) ((fst pair).ToString())) 

// The SLOOOOOOW sequence-patching (method 1) 
dataWithOccationalHoles |> insertDummyValuesWhereASingleValueIsMissing1 timeBetweenContiguousValues |> Seq.iter (fun pair -> printfn "%f %s" (snd pair) ((fst pair).ToString())) 

Odpowiedz

13

Seq.skip konstruuje nową sekwencję. Myślę, że właśnie dlatego twoje oryginalne podejście jest powolne.

Moja pierwsza skłonność to użycie wyrażenia sekwencji i Seq.pairwise. Jest to szybkie i łatwe do odczytania.

let insertDummyValuesWhereASingleValueIsMissingSeq (timeBetweenContiguousValues : TimeSpan) (values : seq<(DateTime * float)>) = 
    let sizeOfHolesToPatch = timeBetweenContiguousValues.Add timeBetweenContiguousValues // Only insert dummy-values when the gap is twice the normal 
    seq { 
    yield Seq.hd values 
    for ((prevTime, _), ((currentTime, _) as next)) in Seq.pairwise values do 
     let timeDiffBetweenPrevAndCurrentValue = currentTime.Subtract(prevTime) 
     if timeDiffBetweenPrevAndCurrentValue = sizeOfHolesToPatch then 
     let dummyValue = (prevTime.Add timeBetweenContiguousValues, 42.0) // 42 is chosen here for obvious reasons, making this comment superfluous 
     yield dummyValue 
     yield next 
    } 
+3

+1: Kiedy uczyłem się F #, dostałem się do programowania funkcjonalnego, eliminując wszystkie imperatywne konstrukcje. Obserwowałem, jak czytelność mojego kodu bierze piksel za pomocą Seq.unfold, a nie w nieskończenie prostszą metodę "loop and ref". – Juliet

+0

Jason, to jest rozwiązanie, którego szukałem. Moją początkową skłonnością przy pisaniu metody było wykorzystanie wydajności (pochodzę z tła C#), ale ponieważ nie mam F # -book (czekając na wydanie Don Syme'a w grudniu) nie mogłem się zorientować, w jaki sposób F # stosuje wydajność, więc poszedłem z Seq.unfold. – Treefrog

+0

@TreeFrog. jeszcze lepsze f # ma 'yield!', który jest 'foreach wydajności ', które chciałem dodać do C# – ShuggyCoUk

31

każdym razem, gdy rozpadają się nast użyciu Seq.hd i Seq.skip 1 jesteś prawie na pewno wpadnięcia w pułapkę będzie O (n^2). IEnumerable<T> jest okropnym typem dla algorytmów rekursywnych (w tym np. Seq.unfold), ponieważ algorytmy te prawie zawsze mają strukturę "pierwszego elementu" i "pozostałej części elementów", i nie ma skutecznego sposobu na stworzenie nowego IEnumerable, który reprezentuje "pozostałość" elementów ". (IEnumerator<T> działa, ale jego model programowania API nie jest tak zabawny/łatwy w obsłudze).

Jeśli potrzebujesz oryginalnych danych, aby "zostać leniwym", powinieneś użyć LazyList (w F # PowerPack). Jeśli nie potrzebujesz lenistwa, powinieneś użyć konkretnego typu danych, takiego jak 'list', do którego możesz się "wciągnąć" w O (1).

(Należy również sprawdzić Avoiding stack overflow (with F# infinite sequences of sequences) jako FYI, choć to tylko stycznie dotyczy ten problem.)

+0

Brian, mogę cię zrozumieć poprawnie, że proces tworzenia nowej sekwencji z istniejącego (np niech seq2 = Seq.skip 1 seq1) jest kosztowna operacja? (Założę się, że to O (1)) Jeśli to jest kosztowne, dlaczego tak jest? (Miałem wrażenie, że sekwencje są oceniane tylko w razie potrzeby?) – Treefrog

+14

Cóż, budowa jest naprawdę szybka: O (1). Ale ocena pierwszego elementu oznacza stworzenie modułu wyliczającego dla pierwotnej sekwencji, obliczenie jego pierwszej wartości, wyrzucenie go, obliczenie następnej wartości, a następnie poddanie jej. Tak więc dwie wartości "Seq.skip 1" dają seq, które przy ocenie będą: tworzyć moduł wyliczający nad wewnętrznym, który tworzy moduł wyliczający nad origem, oblicza pierwszą wartość, wyrzuca, dostarcza kolejną wartość do wnętrza, która wewnętrznie odrzuca, oblicza jeden więcej wartości i daje to. Każdy zagnieżdżony Seq.skip 1 dodaje jeszcze więcej pracy, co daje O (N) czas i przestrzeń. – Brian

+0

Dziękuję za poświęcenie czasu na odpowiedź Briana! – Treefrog