Кластер / Группировка непрерывных записей в кадре данных по нескольким столбцам - PullRequest
2 голосов
/ 10 января 2020

Вопрос

Предположим, у меня есть k скалярных столбцов, и я хотел бы сгруппировать записи, если они находятся на определенном расстоянии друг от друга вдоль каждого столбца.

Предположим, для простоты k это 2, и они мои единственные столбцы.

pd.DataFrame(list(zip(sorted(choices(range(0,10), k=20)), choices(range(20,29), k=20))), columns=['a','b'])

выходы

[(1, 27),
 (1, 27),
 (1, 21),
 (2, 23),
 (3, 25),
 (4, 23),
 (4, 28),
 (4, 27),
 (4, 22),
 (4, 24),
 (5, 26),
 (6, 21),
 (7, 26),
 (7, 20),
 (8, 24),
 (8, 25),
 (8, 23),
 (9, 20),
 (9, 28),
 (9, 21)]

Я хочу, чтобы группировки были такими, чтобы группы включали записи в столбце a, которые находятся не более m друг от друга, И в столбце b, которые не превышают n отдельно. если m = n = 1, то кластеризация будет:

(1, 27), (1, 27)
(1, 21)
(2, 23)
(3, 25), (4, 23), (4, 22), (4, 24)
(4, 28), (4, 27), (5, 26)
(6, 21), (7, 20)
(7, 26), (8, 24), (8, 25), (8, 23)
(9, 20), (9, 21)
(9, 28),

Примечания

Один из способов сделать это будет с помощью pdist , но это не очень хорошее решение, потому что:

  • У меня много данных - я не хочу делать квадратные операции.
  • данные уже отсортированы, и m, n невелики по отношению к диапазонам столбцов
  • m = / = n (не всегда), иначе пороговое значение манхэттенского расстояния m + n будет работать

Я полагаю, что это могло быть очень связанный вопрос, но у него нет общего ответа:

Эскиз подход, который может привести вас к ответу:

a, b, c, d, e = tee(range(10), 5)
next(b, None)
next(c, None);next(c, None)
next(d, None);next(d, None);next(d, None)
next(e, None);next(e, None);next(e, None);next(e, None)
list(zip(a, b, c, d, e))

[(0, 1, 2, 3, 4),
 (1, 2, 3, 4, 5),
 (2, 3, 4, 5, 6),
 (3, 4, 5, 6, 7),
 (4, 5, 6, 7, 8),
 (5, 6, 7, 8, 9)]

Ответы [ 2 ]

2 голосов
/ 22 января 2020

Во-первых, мы делаем pdist с metric = 'chebyshev'

test = np.array([(1, 27),
 (1, 27),
 (1, 21),
 (2, 23),
 (3, 25),
 (4, 23),
 (4, 28),
 (4, 27),
 (4, 22),
 (4, 24),
 (5, 26),
 (6, 21),
 (7, 26),
 (7, 20),
 (8, 24),
 (8, 25),
 (8, 23),
 (9, 20),
 (9, 28),
 (9, 21)])

from scipy.spatial.distance import pdist, squareform
c_mat = squareform(pdist(test, metric = 'chebyshev')) <= 1

, теперь c_mat - это в основном график узлов, которые связаны, если они <1 друг от друга в каждом направлении </p>

Чтобы найти полные несвязанные графы, вероятно, есть быстрая операция, которую вы можете сделать в networx, но я просто немного сложнее в numpy, так как я не знаю, какие ключевые слова теории графов искать там.

out = np.ones((c_mat.shape[0], 2))
while out.sum(0).max() >1:
    c_mat = c_mat @ c_mat
    out = np.unique(c_mat, axis = 0)

Теперь c_mat равно True, если есть какая-либо цепочка, соединяющая две строки, а out - это логические индексы всех отдельных групп. Теперь для возврата результата:

for mask in list(out):
    print(np.unique(test[mask], axis = 0))

[[ 9 28]]
[[ 9 20]
 [ 9 21]]
[[ 7 26]
 [ 8 23]
 [ 8 24]
 [ 8 25]]
[[ 6 21]
 [ 7 20]]
[[ 4 27]
 [ 4 28]
 [ 5 26]]
[[ 3 25]
 [ 4 22]
 [ 4 23]
 [ 4 24]]
[[ 2 23]]
[[ 1 21]]
[[ 1 27]]

Вы также можете использовать эти логические индексы для доступа к строкам данных в вашем оригинале DataFrame

РЕДАКТИРОВАТЬ 1:

Теперь мы можем значительно ускорить это, используя тот факт, что входные данные отсортированы по полусортировке. Но для этого нам нужно numba

from numba import jit

@jit
def find_connected(data, dist = 1):
    i = list(range(data.shape[0]))
    j = list(range(data.shape[0]))
    l = data.shape[0]
    for x in range(1, l):
        for y in range(x, l):
            v = np.abs(data[x] - data[y])
            if v.max() <= dist:
                i += [x, y]
                j += [y, x]
            if v.min() > dist:
                break
    d = [1] * len(i)
    return (d, (i, j))

, теперь нам нужно загрузить это в разреженную матрицу

from scipy.sparse import csr_matrix

c_mat =  csr_matrix(find_connected(test), dtype = bool)

csr делает точечные продукты намного быстрее, поэтому c_mat = c_mat @ c_mat работает, но критерий остановки нарушается. Вы можете использовать превосходный ответ Анреаса К. здесь или просто сделать out = np.unique(c_mat.todense(), axis = 0).

РЕДАКТИРОВАТЬ 2:

Не удалось выкинь это из головы, пока я не решу это без создания плотной матрицы.

import numba as nb
import numpy as np
@nb.njit
def find_connected_semisort(data, dist = 1):
    l = data.shape[0]
    out = []
    for x in range(l):
        for y in range(x, l):
            v = np.abs(data[x] - data[y])
            if v.max() <= dist:
                out.append(set([x, y]))
            if v.min() > dist:
                break
    outlen = len(out)
    for x in range(outlen):
        for y in range(x + 1, outlen):
            if len(out[x] & out[y]) > 0:
                out[y] |= out[x]
                out[x].clear()
    return [list(i) for i in out if len(i) > 0]

[np.unique(test[i], axis = 0).squeeze() for i in find_connected_semisort(test)]
Out[]: 
[array([ 1, 27]), array([ 1, 21]), array([ 2, 23]), array([[ 3, 25],
    [ 4, 22],
    [ 4, 23],
    [ 4, 24]]), array([[ 4, 27],
    [ 4, 28],
    [ 5, 26]]), array([[ 6, 21],
    [ 7, 20]]), array([[ 7, 26],
    [ 8, 23],
    [ 8, 24],
    [ 8, 25]]), array([ 9, 28]), array([[ 9, 20],
    [ 9, 21]])]

Возможно, есть какой-то способ сделать это без двух петель, но я не смог бы это сделать.

0 голосов
/ 10 января 2020

Ваш вопрос напоминает мне lag операцию и cumsum. Вот ответ pandas. Если ваши данные огромны, я думаю, что использовать python list и tuple нормально, модули по умолчанию должны иметь функции для выполнения нашей миссии.

Шаг 1: получить данные

# generate data
import pandas as pd
import numpy as np
from random import choices,seed
seed(1245)
data = pd.DataFrame(list(zip(sorted(choices(range(0,10), k=20,)), choices(range(20,29), k=20))), columns=['a','b'])

Шаг 2: лаг с длиной 1

# lag opertion
data_shift = data.shift(1,fill_value = -999)
data_shift.columns = ["a_last","b_last"]

# conbine them together to apply. If your data is huge, just call function on these 2 pieces of data
data_flat = pd.concat([data,data_shift],axis = 1)
data_flat.head()

Out:

   a   b  a_last  b_last
0  1  26    -999    -999
1  1  27       1      26
2  1  28       1      27
3  2  22       1      28
4  2  24       2      22

Шаг 3: определить функции custum, а затем сгруппировать ваши наблюдения

# define your function with args m,n
def your_func(x,m,n):
    cond1 = (abs(x.a - x.a_last) <= m)
    cond2 = (abs(x.b - x.b_last) <= n)
    if cond1 & cond2:
        return 0
    else: 
        return 1
# calculate per row and get the group_id of samples
groups = data_flat.apply(your_func,axis = 1,m=1,n=1).cumsum()
# get the result
data.groupby(groups).apply(lambda x:list(map(tuple,x.values)))

Out:

1     [(1, 26), (1, 27), (1, 28)]
2                       [(2, 22)]
3                       [(2, 24)]
4                       [(3, 20)]
5                       [(3, 26)]
6              [(4, 21), (4, 20)]
7                       [(5, 28)]
8              [(5, 26), (5, 26)]
9                       [(6, 28)]
10                      [(6, 24)]
11                      [(6, 28)]
12                      [(7, 23)]
13                      [(7, 26)]
14             [(8, 28), (8, 28)]
15                      [(9, 26)]
dtype: object
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...