Seite 1 von 1

Warum sollte ich transponieren? / Wie nutze ich es für mehr effizienz?

Verfasst: Samstag 2. Mai 2020, 13:33
von Bayne
Hallo liebe Coder dieses Boards seid gegrüßt,

Ich muss aktuell viele List comprehensions (besonders verschachtelte List comprehensions) anwenden und beim überlegen kam mir der gedanke: "Da war doch was mit transponierten Listen/Arrays/Matrizen".

Wofür nutzt ihr sie ?
Wie kann ich sie nutzen um Code zeitlich effizienter zu gestalten?

Vielen dank und bleibt gesund ;-)

Re: Warum sollte ich transponieren? / Wie nutze ich es für mehr effizienz?

Verfasst: Samstag 2. Mai 2020, 13:40
von __blackjack__
@Bayne: Wofür nutze ich *was*? Die Frage macht so keinen Sinn. Man sollte transponieren wenn man die Daten transponiert benötigt, weil das semantisch Sinn macht. Das ist erst einmal unabhängig von Laufzeiteffizienz.

Re: Warum sollte ich transponieren? / Wie nutze ich es für mehr effizienz?

Verfasst: Samstag 2. Mai 2020, 15:27
von Bayne
Ich gebe zu das meine Frage zu unspezifisch Formuliert ist. Übertragen wir sie doch einmal auf folgende Beispiele:

Code: Alles auswählen

nextInt = 1
winPoints  = [[max(upSlice[:i])  for i in range(nextInt,len(upSlice)+1)] for upSlice in ppValues]```
oder

Code: Alles auswählen

winningRatio =[ [1 if ratioUp >ratioDown  else 0 if (ratioDown>ratioUp) else
                1 if (pointsUp>pointsDown) else 0 if (pointsDown>pointsUp) else 1   
                for ratioUp,ratioDown,pointsUp,pointsDown                  in  zip(ratioUpSlice,ratioDownSlice,pointsUpSlice,pointsDownSlice)] 
                for ratioUpSlice,ratioDownSlice,pointsUpSlice,pointsDownSlice in  zip(ratios_Up,ratios_Down, pointsUpSlices,pointsDownSlices)]
(alle Slices/Sublisten (bzw. Listen Innerhalb der übergeordneten Liste) sind von keiner Festen Länge! jedoch sind die Sublisten die gleichzeitig in einer Iteration auftauchen immer gleich lang )

Re: Warum sollte ich transponieren? / Wie nutze ich es für mehr effizienz?

Verfasst: Samstag 2. Mai 2020, 17:51
von __blackjack__
@Bayne: Was das jetzt mit der Frage „Warum soll ich transponieren?“ zu tun hat ist mir immer noch nicht wirklich klar. Eventuell würde `numpy` bei dem einen oder anderen Problem Sinn machen.

Was unangenehm auffällt ist die unkonventionelle Schreibweise zusammen mit der Menge an Namen. Das liest sich so gar nicht wie Python.

Das erste Beispiel könnte theoretisch mit `itertools.accumulate()` effizienter werden, weil man dann nicht immer Slices erstellen und den `max()`-Wert von immer mehr Werten in jedem Schritt berechnen muss:

Code: Alles auswählen

    win_points = [list(accumulate(up_slice, max)) for up_slice in pp_values]
Beim zweiten Beispiel kommt zu den Namen noch der vierfach verschachtelte bedingte Ausdruck dazu. War das ein Versuch das so unlesbar wie möglich zu schreiben? Was soll das? Ausserdem erscheint mir das redunant weil bei den vier Bedingungen eigentlich nur zwei existieren und die anderen beiden das Gegenteil mit gegenteiligen Ergebnis zu sein scheinen. Also letztlich wohl das hier:

Code: Alles auswählen

    winning_ratio = [
        [
            1 if ratio_up > ratio_down or points_up > points_down else 0
            for ratio_up, ratio_down, points_up, points_down in zip(
                ratio_up_slice,
                ratio_down_slice,
                points_up_slice,
                points_down_slice,
            )
        ]
        for ratio_up_slice, ratio_down_slice, points_up_slice, points_down_slice in zip(
            ratios_up, ratios_down, points_up_slices, points_down_slices
        )
    ]
Ich weiss nicht ob Du das mit ”transponieren” meintest, aber das hier sogar zweifach parallele Datenstrukturen mit `zip()` zusammengeführt werden müssen ist ein „code smell“. Nicht wegen Effizienz, sondern weil das unübersichtlich und fehleranfällig ist, wenn man zusammengehörende Daten in parallelen Datenstrukturen speichert.

Re: Warum sollte ich transponieren? / Wie nutze ich es für mehr effizienz?

Verfasst: Samstag 2. Mai 2020, 22:08
von Bayne
__blackjack__ hat geschrieben: Samstag 2. Mai 2020, 17:51 @Bayne: Was das jetzt mit der Frage „Warum soll ich transponieren?“ zu tun hat ist mir immer noch nicht wirklich klar. Eventuell würde `numpy` bei dem einen oder anderen Problem Sinn machen.

Was unangenehm auffällt ist die unkonventionelle Schreibweise zusammen mit der Menge an Namen. Das liest sich so gar nicht wie Python.

Das erste Beispiel könnte theoretisch mit `itertools.accumulate()` effizienter werden, weil man dann nicht immer Slices erstellen und den `max()`-Wert von immer mehr Werten in jedem Schritt berechnen muss:

Code: Alles auswählen

    win_points = [list(accumulate(up_slice, max)) for up_slice in pp_values]
Beim zweiten Beispiel kommt zu den Namen noch der vierfach verschachtelte bedingte Ausdruck dazu. War das ein Versuch das so unlesbar wie möglich zu schreiben? Was soll das? Ausserdem erscheint mir das redunant weil bei den vier Bedingungen eigentlich nur zwei existieren und die anderen beiden das Gegenteil mit gegenteiligen Ergebnis zu sein scheinen. Also letztlich wohl das hier:

Code: Alles auswählen

    winning_ratio = [
        [
            1 if ratio_up > ratio_down or points_up > points_down else 0
            for ratio_up, ratio_down, points_up, points_down in zip(
                ratio_up_slice,
                ratio_down_slice,
                points_up_slice,
                points_down_slice,
            )
        ]
        for ratio_up_slice, ratio_down_slice, points_up_slice, points_down_slice in zip(
            ratios_up, ratios_down, points_up_slices, points_down_slices
        )
    ]
Ich weiss nicht ob Du das mit ”transponieren” meintest, aber das hier sogar zweifach parallele Datenstrukturen mit `zip()` zusammengeführt werden müssen ist ein „code smell“. Nicht wegen Effizienz, sondern weil das unübersichtlich und fehleranfällig ist, wenn man zusammengehörende Daten in parallelen Datenstrukturen speichert.
die erschwerte übersicht hat sich leider so ergeben und es geht wohl definitiv erkenntlicher. Danke für den Hinweis.
Die Konditionen waren schon richtig so, da sie als
if, elif, elif, elif, else abefrüstückt werden müssen, da die ersten beiden Prootität haben und wenn (ratio_up== ratio_down) die anderen beiden( points_up & points_down) verglichen werden, sollten die auch gleich sein, wird eine 0 gesetzt (oder war es nicht eine 1 oben?).


EDIT:

Code: Alles auswählen

win_points = [list(accumulate(up_slice, max)) for up_slice in pp_values]
würde leider nicht funktionieren, da über die Subliste "up_slice" iteriert werden muss und der aktuelle wert immer dem größten bisher in der Sublist aufgetauchten wert entsprechen muss.

zu itertools: Ist das "slicen" mit itertools denn schneller als eine Listcomprehension?
mit "slicen meine ich sowas wie

Code: Alles auswählen

slices = [ alleWerte [von : bis] for von, bis  in zip(von_Indizes, bis_Indizes) ]
(alleWerte ist eine Liste mit in reihenfolge geordneten Werten und von_Indizes und bis_indizes Indizes die unterschiedlich weit ausseinander liegen können ( abweichung um etwa 1-5 indizes immer), also das erste "von" kann 20 elemente von "bis" entfernt sein, aber das x'te kann z.b. 15 oder bspw. 24 elemente von seinem zugehörigen "bis" wegsein. Dabei ist der "von"Index immer vor dem "bis" Index.)

Re: Warum sollte ich transponieren? / Wie nutze ich es für mehr effizienz?

Verfasst: Sonntag 3. Mai 2020, 00:36
von __blackjack__
Das mit `accumulate()` funktioniert genau so wie Du das Ergebnis beschreibst, es ist immer das bisher grösste Element aus dem ursprünglichen „iterable“:

Code: Alles auswählen

In [59]: list(itertools.accumulate([1, 3, 2], max))                               
Out[59]: [1, 3, 3]

In [60]: list(itertools.accumulate([3, 2, 1], max))                               
Out[60]: [3, 3, 3]

In [61]: list(itertools.accumulate([1, 2, 3], max))                               
Out[61]: [1, 2, 3]
Mit `itertools` kann man nicht ”slicen”, also kann man das damit auch nicht schneller machen. Bei dem `accumulate()`-Beispiel *vermeidet* man damit das ”slicen”, insbesondere das bei Deinem Code für jedes Element in der ursprünglichen Liste ein geslicet wurde und der Slice bei jedem Element alle vorherigen umfasste, also immer grösser wurde, was die Geschichte an der Stelle dann O(n²) macht, während `accumulate()` jedes Element genau einmal anfasst, also mit linearer Laufzeit- und Speicherkomplexität (O(n)) auskommt. Das heisst nicht zwangsläufig, dass es für die konkreten Werte schneller ist, aber es skaliert auf jeden Fall besser.

Wenn man etwas mit quadratischer Laufzeit macht weil das schneller ist als etwas äquivalentes mit linearer Laufzeit, dann würde ich das zumindest mit einem Kommentar dokumentieren, weil das eine Stelle ist die man sich anschauen sollte wenn sich an der Umgebung was ändert. Beispielsweise wenn eine neue Python-Version heraus kommt, oder man das mit einer anderen Python-Implementierung ausführt.

Ansonsten verwendet man Iteratoren/Generatoren eher nicht um etwas insgesamt schneller zu machen, sondern um Speicher zu sparen. Manchmal auch um Berechnungen zu sparen. Und Teilergebnisse sind damit oft schneller verfügbar, auch wenn vielleicht die Gesamtrechenzeit etwas höher wird.

Re: Warum sollte ich transponieren? / Wie nutze ich es für mehr effizienz?

Verfasst: Sonntag 3. Mai 2020, 07:17
von __blackjack__
Okay jetzt noch mal `winningRatio` ohne verschachtelten bedingten Ausdruck:

Code: Alles auswählen

    winningRatio = [
        [
            int(
                (ratioUp > ratioDown and pointsUp <= pointsDown)
                or (ratioUp >= ratioDown and pointsUp >= pointsDown)
            )
            for ratioUp, ratioDown, pointsUp, pointsDown in zip(
                ratioUpSlice, ratioDownSlice, pointsUpSlice, pointsDownSlice
            )
        ]
        for ratioUpSlice, ratioDownSlice, pointsUpSlice, pointsDownSlice in zip(
            ratios_Up, ratios_Down, pointsUpSlices, pointsDownSlices
        )
    ]
Und zumindest auf meinem Rechner ist `accumulate()` selbst bei drei Elementen schneller als `max()` + „slicing“ mit immer grösser werdenen Slices. Und erwartungsgemäss wird der Gewinn deutlich grösser, je mehr Elemente es sind:

Code: Alles auswählen

In [72]: up_slice = list(range(3))                                              

In [73]: %timeit [max(up_slice[:i]) for i in range(1, len(up_slice) + 1)]       
990 ns ± 9.84 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [74]: %timeit list(accumulate(up_slice, max))                                
528 ns ± 3.75 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [75]: up_slice = list(range(10))                                             

In [76]: %timeit [max(up_slice[:i]) for i in range(1, len(up_slice) + 1)]       
2.96 µs ± 14.4 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [77]: %timeit list(accumulate(up_slice, max))                                
1.21 µs ± 12.5 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [78]: up_slice = list(range(100))                                            

In [79]: %timeit [max(up_slice[:i]) for i in range(1, len(up_slice) + 1)]       
89.1 µs ± 429 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

In [80]: %timeit list(accumulate(up_slice, max))                                
10.5 µs ± 83.9 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [81]: up_slice = list(range(1000))                                           

In [82]: %timeit [max(up_slice[:i]) for i in range(1, len(up_slice) + 1)]       
6.91 ms ± 31.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [83]: %timeit list(accumulate(up_slice, max))                                
101 µs ± 220 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Re: Warum sollte ich transponieren? / Wie nutze ich es für mehr effizienz?

Verfasst: Sonntag 3. Mai 2020, 08:55
von Sirius3
Ab Listen der Länge ~20 ist numpy.fmax.accumulate deutlich schneller als itertools.accumulate,
wenn es nicht Listen sind sondern Numpy-Arrays sind, dann ab Länge 1.

Lustigerweise schafft es numba ab Array-Längen von ~200 schneller zu sein als selbst numpy:

Code: Alles auswählen

@numba.njit
def accumulate_max(values):
    result = numpy.empty_like(values)
    m = -numpy.inf
    for i, v in enumerate(values):
        if v > m: m = v
        result[i] = m
    return result
Statt zip würde man natürlich bei numpy-Arrays auch die entsprechenden Vektor-Vergleiche nehmen.
Wenn ich die vierfach if-else-Verschachtelung richtig interpretiere, dann sind das folgende Umformungsschritte

Code: Alles auswählen

not (ratioDown>ratioUp or (ratioUp == ratioDown and pointsDown>pointsUp))
ratioDown <= ratioUp and not (ratioUp == ratioDown and pointsDown > pointsUp)
ratioDown <= ratioUp and (ratioDown != ratioUp or pointsDown <= pointsUp)
(ratioDown <= ratioUp and ratioDown != ratioUp) or (ratioDown <= ratioUp and pointsDown <= pointsUp)
ratioDown < ratioUp or (ratioDown == ratioUp and pointsDown <= pointsUp)
Mit numpy-Arrays also insgesamt:

Code: Alles auswählen

win_points = [numpy.fmax.accumulate(up_slice) for up_slice in ppValues]
winning_ratio = [
    (ratioDownSlice < ratioUpSlice) | ((ratioDownSlice == ratioUpSlice) & (pointsDownSlice <= pointsUpSlice))
    for ratioUpSlice,ratioDownSlice,pointsUpSlice,pointsDownSlice in zip(ratios_Up,ratios_Down, pointsUpSlices,pointsDownSlices)
]