J'ai une grande liste 2D comme [[name, lower_limit, upper_limit], ...]. Je veux fusionner la liste d'articles du même name.

Par exemple, convertir

a = [['a1', 1, 10],['a2', -1, 20],['a1', 0, 8], ['a2', 0, 1]]

À

[['a1', 0, 10], ['a2', -1, 20]]

Les listes d'éléments du même name sont fusionnées et leur limite inférieure minimale et limite supérieure maximale sont considérées comme les limites inférieure et supérieure fusionnées, respectivement.

1
Lee 8 mars 2016 à 00:26

4 réponses

Meilleure réponse

Voici une façon

a = [['a1', 1, 10],['a2', -1, 20],['a1', 0, 8], ['a2', 0, 1]]
b = [[key,min(el[1] for el in a if el[0] == key),max(el[2] for el in a if el[0] == key)] for key in set([el[0] for el in a])]

La compréhension de la liste externe crée un ensemble de clés; les compréhensions de la liste intérieure associent les clés avec les éléments max des premier et deuxième bacs en utilisant les méthodes min / max intégrées

0
en_Knight 7 mars 2016 à 21:35

Voici mon point de vue sur le nettoyage du code de Paul (n'hésitez pas à le copier et je supprimerai cette réponse). Cela me semble relativement lisible:

from itertools import groupby
from operator import itemgetter

a = [['a1', 1, 10], ['a2', -1, 20], ['a1', 0, 8], ['a2', 0, 1]]

merged = []
for key, groups in groupby(sorted(a), key=itemgetter(0)):
    _, lowers, uppers = zip(*groups)
    merged.append([key, min(lowers), max(uppers)])

Cependant, comme nous savons que nous voulons que chaque clé se produise exactement une fois, je ne vois aucun inconvénient à utiliser un dictionnaire.

merged = {}
for key, groups in groupby(sorted(a), key=itemgetter(0)):
    _, lowers, uppers = zip(*groups)
    merged[key] = (min(lowers), max(uppers))
1
Jared Goguen 7 mars 2016 à 22:10

Je ne sais pas quelle est la méthode la plus Pythonique, mais voici une méthode pour le faire en utilisant itertools.groupby():

from itertools import groupby
from operator import itemgetter
a = [['a1', 1, 10],['a2', -1, 20],['a1', 0, 8], ['a2', 0, 1]]

merged = []
for k, g in groupby(sorted(a), key=itemgetter(0)):
    _, low_limits, high_limits = zip(*g)
    merged.append([k, min(low_limits), max(high_limits)])

Cela trie et regroupe la liste externe par la clé (premier élément), puis itère sur celle qui trouve juste la valeur minimale dans la liste des limites basses et la valeur maximale dans la liste des limites hautes.

Modifier : nettoyé conformément à la suggestion de @ JaredGoguen ci-dessous.

Deuxième édition Étant donné qu'OP semblait préoccupé par les performances, je dirais qu'il me semble que si vous avez une énorme quantité de ces clés de sorte que les performances seront un problème, vous voudrez peut-être envisager d'utiliser quelque chose comme numpy ou pandas pour cette tâche, mais cette méthode groupby n'est pas quelque chose qui évolue.

J'ai fait un peu de profilage:

import numpy as np
import pandas as pd

from itertools import groupby
from operator import itemgetter

def merge_groupby(a):
    merged = []
    for k, g in groupby(sorted(a), key=itemgetter(0)):
        _, low_limits, high_limits = zip(*g)
        merged.append([k, min(low_limits), max(high_limits)])

    return merged

def merge_g4dget(a):
    d = {}
    for name, low, high in a:
        if name not in d:
            d[name] = [low, high]
            continue
        if low<d[name][0]: d[name][0] = low
        if high>d[name][1]: d[name][1] = high

def merge_pandas(a):
    df = pd.DataFrame(a).set_index(0)
    ndf = df.groupby(level=0).agg({1: np.min, 2:np.max})
    return [[k, v[1], v[2]] for k, v in ndf.iterrows()]

if __name__ == "__main__":
    # Construct a large array of these things
    keys = ['a1', 'a2', 'a3', 'a4', 'a5', 'a6']
    N = 1000000

    get_randint = lambda: np.random.randint(-50, 50)
    large_array = [[np.random.choice(keys), get_randint(), get_randint()]
                    for x in range(N)]

Puis dans un shell IPython:

In [1]: run -i groupby_demo.py
%load_ext line_profiler

In [2]: %load_ext line_profiler

In [3]: %lprun -f merge_groupby merge_groupby(large_array)
Timer unit: 1e-06 s

Total time: 7.01214 s
File: groupby_demo.py
Function: merge_groupby at line 7

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     7                                           def merge_groupby(a):
     8         1            4      4.0      0.0      merged = []
     9         7      4328680 618382.9     61.7      for k, g in groupby(sorted(a), key=itemgetter(0)):
    10         6      2555118 425853.0     36.4          _, low_limits, high_limits = zip(*g)
    11         6       128342  21390.3      1.8          merged.append([k, min(low_limits), max(high_limits)])
    12                                           
    13         1            1      1.0      0.0      return merged

In [4]: %lprun -f merge_g4dget merge_g4dget(large_array)
Timer unit: 1e-06 s

Total time: 2.84788 s
File: groupby_demo.py
Function: merge_g4dget at line 15

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    15                                           def merge_g4dget(a):
    16         1            5      5.0      0.0      d = {}
    17   1000001       579263      0.6     20.3      for name, low, high in a:
    18   1000000       668371      0.7     23.5          if name not in d:
    19         6           11      1.8      0.0              d[name] = [low, high]
    20         6            5      0.8      0.0              continue
    21    999994       828477      0.8     29.1          if low<d[name][0]: d[name][0] = low
    22    999994       771750      0.8     27.1          if high>d[name][1]: d[name][1] = high

In [5]: %lprun -f merge_pandas merge_pandas(large_array)
Timer unit: 1e-06 s

Total time: 0.662813 s
File: groupby_demo.py
Function: merge_pandas at line 24

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    24                                           def merge_pandas(a):
    25         1       568868 568868.0     85.8      df = pd.DataFrame(a).set_index(0)
    26         1        92455  92455.0     13.9      ndf = df.groupby(level=0).agg({1: np.min, 2:np.max})
    27         1         1490   1490.0      0.2      return [[k, v[1], v[2]] for k, v in ndf.iterrows()]

D'après cela, il semble que l'utilisation de pandas serait plus rapide, et la part du lion du travail se fait en fait dans la construction initiale de la trame de données Pandas (qui, si vous travaillez avec des DataFrames ou des tableaux numpy au lieu de listes de listes en premier lieu , est une sorte de coût fixe).

Attention, cela n'est pas cohérent avec les résultats %timeit pour une raison quelconque:

In [6]: %timeit merge_pandas(large_array)
1 loops, best of 3: 619 ms per loop

In [7]: %timeit merge_g4dget(large_array)
1 loops, best of 3: 396 ms per loop

Je ne sais pas pourquoi, mais il semble qu'il y ait une différence entre les appels ou autre chose. Quoi qu'il en soit, si vous avez d'autres tâches qui sont mieux exécutées sur les données dans pandas de toute façon, vous feriez probablement mieux de les utiliser.

3
Paul 9 mars 2016 à 00:25
L = [['a1', 1, 10],['a2', -1, 20],['a1', 0, 8], ['a2', 0, 1]]
d = {}

for name, low, high in L:
    if name not in d:
        d[name] = [low, high]
        continue
    if low<d[name][0]: d[name][0] = low
    if high>d[name][1]: d[name][1] = high
3
inspectorG4dget 7 mars 2016 à 21:31