2
votes

Python 3: supprimer les chevauchements dans le tableau

J'ai une table (sortie simplifiée d'un programme), que je dois filtrer:

id   hit from   to valu
A   hit1    56  102 0.00085
C   hit4    332 480 3.40E-15
D   hit5    291 512 3.80E-24
D   hit10   514 600 0.0021

Pour chaque id, le df doit être trié par à partir de et, s'il y a des hits qui se chevauchent, conservez celui avec la value la plus basse .

Jusqu'à présent, c'est mon code, qui fait d'abord le début par de puis par value:

import pandas
df = pandas.read_csv("table", sep='\s+', names=["id", "hit", "from", "to", "value"])
df.sort_values(['from', "value"]).groupby('id')

Mais comment vérifier le chevauchement ( de code> à à ) et supprimer celui avec le score plus élevé ?

Voici mon résultat attendu:

id   hit from   to value
A   hit1    56  102 0.00085
B   hit2    89  275 0.00034
B   hit3    240 349 0.00034
C   hit4    332 480 3.40E-15
D   hit5    291 512 3.80E-24
D   hit6    287 313 0.00098
D   hit7    381 426 0.00098
D   hit8    287 316 0.0029
D   hit9    373 422 0.0029
D   hit10   514 600 0.0021


5 commentaires

pouvez-vous mettre des valeurs qui se chevauchent dans les données de test?


Pouvez-vous expliquer le chevauchement?


désolé si ce n'était pas clair. Considérez de et à comme coordonnées. Par conséquent, A n'a pas de résultats qui se chevauchent car c'est le seul résultat, alors que B a un chevauchement dans les coordonnées 240 à 275. Et D a 5 résultats qui se chevauchent, dont 1 doit être sélectionné en fonction de la valeur la plus basse, mais le dernier hit10 ne se chevauche pas avec les autres de D .


Il semble que sous votre logique, id 'D', hit 'hit5' devrait être abandonné, mais vous l'avez dans votre exemple. Est-ce que je lis correctement votre logique?


Le hit avec la valeur LOWER doit être conservé


5 Réponses :


0
votes

Si vous triez id == 'D'

    id  hit from    to  value
5   D   hit6    287 313 9.800000e-04
7   D   hit8    287 316 2.900000e-03
4   D   hit5    291 512 3.800000e-24
8   D   hit9    373 422 2.900000e-03
6   D   hit7    381 426 9.800000e-04
9   D   hit10   514 600 2.100000e-03

Le chevauchement sera:

  • atteindre 6, 8 et 5 = conserver la valeur la plus basse de 5 bc

  • appuyez sur 9 et 7 = kepp 7

  • Hit 10 est seul le garder?


2 commentaires

Pas assez. Frappez 6,8,5 chevauchement comme vous dites, mais frappez 9 chevauchements avec le coup 5 et frappez 7 chevauchements avec le coup 9. Ici, choisissez celui (sur 5) qui a la valeur la plus basse. Hit 10 n'a aucun chevauchement, alors continuez.


La logique est assez compliquée et je n'ai pas le temps de la reprendre cette semaine. Désolé.



2
votes

Si plusieurs lignes de votre code ne vous dérangent pas, quelque chose comme ça devrait fonctionner, je suppose ... (python newbie here ...) source

  id    hit from   to     value
0  A   hit1   56  102   0.00085
1  C   hit4  332  480  3.40E-15
2  D   hit5  291  512  3.80E-24
3  D  hit10  514  600    0.0021

" keep em Le paramètre> "est défini sur false car vous ne voulez pas du tout les lignes en double.

Ce qui donne:

df.reset_index(drop=True, inplace=True)

Et pour se débarrasser de la colonne d'index en désordre:

  id    hit from   to     value
0  A   hit1   56  102   0.00085
3  C   hit4  332  480  3.40E-15
4  D   hit5  291  512  3.80E-24
9  D  hit10  514  600    0.0021

Ce qui donne:

df.sort_values(['from', "value"]).groupby('id')
df.drop_duplicates(subset=['id', 'value'], keep=False, inplace=True)

PS: C'est la première fois que je donne une réponse alors, soyez gentil. Et aussi, j'apprends toujours l'anglais.


3 commentaires

La solution est mauvaise, il n'y a pas de chevauchement de suppression, malheureusement les données ne sont pas MOVE, alors obtenez une sortie comme le besoin OP. Mais avec une autre solution de données a échoué.


Je suis d'accord, cette solution ne supprime que les valeurs en double malheureusement ...


Alors peut-être cette réponse à cette question pourrait être utile? @Saraha



1
votes
df = pd.DataFrame({'id': ['A', 'B', 'B', 'C', 'D', 'D' ,'D', 'D', 'D', 'D', 'D'],
                  'hit': ['hit1', 'hit2', 'hit3','hit4', 'hit5','hit6', 'hit7','hit8', 'hit9','hit10', 'hit11'],
                  'from': [56,89,240,332,291,287,381,287,373,514, 599],
                  'to':[102,275,349,480,512,313,426,316,422,600, 602],
                  'value': [0.00085,0.00034,0.00034,3.40E-15,3.80E-24,0.00098,0.00098,0.0029,0.0029,0.0021, 0.002]})

overlapMask =  df.sort_values(by = 'from')\
                 .groupby('id')\
                 .apply(lambda x: np.where(x['from'] < x['to'].shift(), 0 , 1).cumsum())\
                 .reset_index()

df['Mask'] = np.concatenate((overlapMask[0].values))


df.drop_duplicates(subset = ['id','value'], keep = False, inplace = True)


df.sort_values(by = 'value')\
  .groupby(['id', 'Mask'])\
  .head(1)\
  .reset_index()\
  .drop(['Mask', 'index'],axis = 1)\
  .sort_values(by = 'id')


    id  hit    from  to    value
2   A   hit1    56  102 8.500000e-04
1   C   hit4    332 480 3.400000e-15
0   D   hit5    291 512 3.800000e-24
3   D   hit11   599 602 2.000000e-03
So my solution uses a mask to check for overlap. By sorting the 'from' values, and checking if the next 'from' value is less than the previous 'to' value. The np.inf is to just make sure the first value will always be a 0 in a grouping.We then make the mask its own column in our df. We then group by everything we need to, drop any duplicates, reset the index, then finally drop our mask.

5 commentaires

J'obtiens un TypeError: shift () a obtenu un argument de mot clé inattendu 'fill_value'


Cela semble être nouveau dans pandas 0.24.0, vérifiez votre version de pandas. Je viens également de vérifier, vous ne semblez pas avoir besoin de fill_value, vous pouvez donc simplement le supprimer. Ce qui se passe, c'est que la première valeur pour tout est juste 1 au lieu de 0, mais cela n'affecte pas le regroupement du masque.


@Saraha, avez-vous essayé à nouveau?


Je l'ai essayé en ajoutant une autre ligne à la table ( D hit11 599 602 0.0002 ). Malheureusement, cela n'est pas pris en compte par le code car j'obtiens une mauvaise sortie ...


Votre droite, il y avait un bug que j'ai manqué, j'ai corrigé le code il devrait fonctionner correctement maintenant. Désolé pour ça.



1
votes

Dans un premier temps, nous introduisons un ID unique et utilisons pd.Interval :

print(df.loc[df.ID.isin(selected), :].drop(columns=['ID', 'Interval']))
  id    hit  from   to         value
0  A   hit1    56  102  8.500000e-04
3  C   hit4   332  480  3.400000e-15
4  D   hit5   291  512  3.800000e-24
9  D  hit10   514  600  2.100000e-03

Après cela, nous joignons df sur lui-même et calculons les parties qui se chevauchent: p>

def select_min(x):
    m = x['value'].min()
    if len(x) > 1 and (x['value'] == m).all():
        return -1
    else:
        return x['value'].idxmin()

selected = data.groupby(['id', 'Subgraph'])['value', 'ID'].apply(select_min)
selected = selected[selected >= 0]

Nous savons maintenant quels ID se chevauchent mais nous ne savons pas lequel d'entre eux construit un composant connecté. En général, cela ne peut pas être fait par un simple algorithme tel que la réorganisation mais un peu théorie des graphes aide. Nous construisons donc un graphique

graph = connected.groupby (['id', 'ID_x']). Agg (list)

et calculons le composants connectés via première recherche approfondie

def connections(graph, id):
    def dict_to_df(d):
        df = pd.DataFrame(data=[d.keys(), d.values()], index=['ID', 'Subgraph']).T
        df['id'] = id
        return df[['id', 'Subgraph', 'ID']]

    def dfs(node, num):
        visited[node] = num
        for _node in graph.loc[node].iloc[0]:
            if _node not in visited:
                dfs(_node, num)

    visited = {}
    graph = graph.loc[id]
    for (num, node) in enumerate(graph.index):
        if node not in visited:
            dfs(node, num)

    return dict_to_df(visited)

dfs = []
for id in graph.index.get_level_values(0).unique():
    dfs.append(connections(graph, id))

conns = pd.concat(dfs)

conns contient les composants connectés et nous pouvons mettre les choses ensemble:

data = df.merge (conns [['Subgraph', 'ID']] , on = ['ID'])

Notre dernière tâche est de choisir les lignes que nous voulons conserver:

columns = ['id', 'Interval', 'ID']
connected = df[columns].merge(df[columns], on='id')
connected['Overlap'] = connected.apply(lambda x: x['Interval_x'].overlaps(x['Interval_y']), axis=1) 
connected = connected.loc[connected['Overlap'] == True, ['id', 'ID_x', 'ID_y']]

Maintenant, nous sont terminés:

df['ID'] = range(df.shape[0])
df['Interval'] = df.apply(lambda x: pd.Interval(x['from'], x['to'], closed='both'), axis=1)


3 commentaires

Merci pour l'explication soignée! Je reçois un AttributeError: ("L'objet 'pandas._libs.interval.Interval' n'a pas d'attribut 'overlaps'", 's'est produit à l'index 0')


Quelle version de pandas ( pd .__ version__ ) utilisez-vous? Avec 24.1, tout fonctionne bien.


Eh bien, je suis très content!



0
votes

On dirait que l'implémentation si vous allez groupe par groupe alors ligne par ligne est assez simple. Il ne semble pas y avoir de moyen dans les pandas d'écrire une fonction qui opère sur plusieurs lignes et colonnes à la fois de manière efficace.

def nonoverlapping(df):
  return df.Dataframe.from_records([strip(group) for name,group in df.sort_values(['from', "value"]).groupby('id')])

L'algorithme de recherche est assez simple, vous avez un groupe de lignes triées non vides qui ont toutes le même ID. Vous allez au premier élément et prenez la fin. toutes les lignes suivantes qui ont commencé avant cela se chevauchent et si elles se terminent après que nous en faisons notre nouvelle valeur finale.

def reduce_overlap(overlapping):
    overlapping= sorted(overlapping,key=lambda x: x.value)
    if len(overlapping)==1 or overlapping[0].value != overlapping[1].value:
        return overlapping[0]
    else:
        return []

Pour trouver la valeur à renvoyer nous trions par la valeur ne renvoyant rien si par hasard ils sont deux de la même valeur.

Modifier: Voici une fonction qui l'applique à l'ensemble du dataframe que je n'ai pas testé.

def strip(group):
    non_overlapping=[]
    overlapping = [list(group.itertuples())[0]]
    end = list(group.itertuples())[0].to
    for row in list(group.itertuples())[1:]:
        if row[3]<=end:
            overlapping.append(row)
            if row.to > end:
                end = row.to
        else:
            non_overlapping.append(reduce_overlap(overlapping))
            overlapping=[row]
    non_overlapping.append(reduce_overlap(overlapping))
    return non_overlapping


2 commentaires

Comment appliquer la fonction strip (group) à l'ensemble du dataframe?


vous ne l'appliquez pas groupe par groupe. Il y a encore quelques retouches pour le faire recombiner en un df. J'ai bouclé sur eux groupe par groupe avec pour nom, groupe dans df.sort_values ​​(['from', "value"]). Groupby ('id'): print (strip (group))