Советы по выявлению неиспользуемых значений в ваших таблицах с помощью Python

Если вы работали с реляционными базами данных, вы должны знать SQL соединения — у них много вариантов использования, но в этом посте я сосредоточусь на очистке данных.

При выполнении левого, правого или полного внешнего соединения вы создаете таблицы, в которых присутствуют либо все записи, либо только записи из определенных таблиц. Для строки без совпадения помещается нулевое значение. Таким образом, соединения чрезвычайно полезны для определения отсутствующих или неиспользуемых значений.

Представьте, что в вашей базе данных есть таблица users, содержащая всех ваших пользователей. Кроме того, у вас есть несколько других таблиц, которые ссылаются на идентификаторы таблицы users, например posts, logins, subscriptions и т. д. Вам интересно узнать, какие пользователи могут быть удалены из базы данных, поскольку они не взаимодействовали с вашим Веб-сайт. Это можно сделать, проверив, ссылаются ли идентификаторы в другом месте.

SELECT 
  u.userID AS 'User ID', 
  p.userID AS 'Post table' 
FROM users AS u
  LEFT JOIN posts AS p ON u.userID = p.userID
-- as users can have multiple posts
GROUP BY p.userID;
User ID      Post table   
---------    ----------  
1            Null   
2            Null   
3            3
4            4
5            Null

В приведенной выше таблице показано, что пользователи 1, 2, 5 не создали никаких сообщений. Возможно, вы захотите продолжить исследование и добавить logins и subscriptions — это нормально, но если у вас есть много дополнительных таблиц, которые вы хотите соединить таким образом, у вас могут возникнуть проблемы с производительностью (СОВЕТ: если вы играете со сценариями SQL, никогда не делайте этого в рабочей базе данных, сначала сделайте локальную копию).

Проанализируйте свои таблицы в Python

Если у вас возникли проблемы с производительностью или вам нужны более качественные инструменты для анализа вашей базы данных, одна из идей — обратиться к Python, поскольку у него есть замечательная экосистема для данных. Вы можете использовать, например, SQLAlchemy или магические функции SQL Jupyter Notebook для получения записей и сохранения их в списках (или словарях).

Чтобы продемонстрировать, как выполнить левое внешнее соединение в Python, в данном случае мы не собираемся подключаться к базе данных, вместо этого создадим некоторые случайные данные и сохраним их в словаре. У нас будет таблица users со всеми возможными идентификаторами пользователей и пять других таблиц со случайными ссылками на идентификаторы:

import random
import pandas as pd

# defining range for userIDs, one to ten
r = (1, 11)
s, e  = r

# creating dict to hold 'tables' and adding all possible user IDs 
tables = {}
tables['users'] = list(range(*r))

# generating ten tables with random IDs from the initial defined range of userIDs
for i in range(1, 6):
   table = random.sample(range(*r), random.randint(s-1, e-1))
   tables[f'table{i}'] = table

Панды

Использование pandas может показаться очевидным, так как это удобный пакет для данных в python. У него есть две функции для объединения таблиц, pd.merge() и pd.join() (также pd.concat() — обратите внимание, что это работает немного по-другому), но эти функции работают лучше всего, если у вас есть как минимум два столбца, один из которых вы объединяете, а другой содержит ваши значения. Это не наш случай, так как у нас есть только списки идентификаторов.

Давайте посмотрим, что произойдет, если мы объединим два из этих списков, tables['users'] и tables['table1']:

df_users = pd.DataFrame(tables['users'])
df_table1 = pd.DataFrame(tables['table1'])

pd.merge(df_users, df_table1, how='left')
OUTPUT:

     0
   ---
 0   1
 1   2
 2   3
 3   4
 4   5
 5   6
 6   7
 7   8
 8   9
 9  10

Ну а результат неутешителен, вроде ничего не делал. По умолчанию функция объединяет два кадра данных в единственном столбце, поэтому мы получаем все идентификаторы пользователей и ничего больше (кроме индекса). Под капотом он выполняет правильное объединение, но поскольку у нас нет дополнительных столбцов, отображать нечего. Нам нужно добавить параметр indicator=True, чтобы увидеть результат:

pd.merge(df_users, df_table1, how='left', indicator=True)
OUTPUT:

            0      _merge
    ---------   ---------
0           1   left_only
1           2   left_only
2           3        both
3           4        both
4           5   left_only
5           6   left_only
6           7        both
7           8   left_only
8           9   left_only
9          10        both

Столбец _merge показывает, существует ли запись в обоих списках или только в первом. Мы можем сделать результат еще лучше, установив исходные индексы DataFrames в единственный существующий столбец и объединив их:

pd.merge(df_users.set_index(0), df_table1.set_index(0), how='left',
  left_index=True, right_index=True, indicator=True)
OUTPUT:

       _merge
    ---------
0 
1   left_only
2   left_only
3        both
4        both
5   left_only
6   left_only
7        both
8   left_only
9   left_only
10       both

Хотя этот подход работает, он очень неуклюж, если вы хотите объединить несколько списков (таблиц).

Наборы

Хотя это и не объединение, с помощью наборов в Python (обратите внимание, наборы не могут содержать повторяющиеся значения) можно достичь желаемого результата — определить значения, на которые нет ссылок.

set_users = set(tables['users'])
set_table1 = set(tables['table1'])

unreferenced_ids = set_users - set_table1

Вычитая один набор из другого, можно найти разницу между двумя наборами — элементами, присутствующими в users, но отсутствующими в table1 наборе. Это можно повторить и с оставшимися таблицами.

Использование циклов

Решение, которое сработало для меня лучше всего, состоит в том, чтобы перебирать списки (таблицы) и добавлять значения None для идентификаторов без ссылок. Это возможно, потому что списки упорядочены, и мы можем перебирать все идентификаторы пользователей и проверять, существуют ли они в других таблицах.

# creating a new dict
final_tables = {}

# transfering user IDs
final_tables['users'] = tables.pop('users')

# looping through the tables
for key, value in tables.items():

    # creating a new column
    column = []

    # checking values against all user IDs
    for user in final_tables['users']:
        # adding True if ID is referenced
        if user in value:
            column.append(True)
        # adding None if ID is not referenced
        else:
            column.append(None)

    final_tables[key] = column
 
# converting the new dict holding the processed tables to a dataframe
df = pd.DataFrame.from_dict(final_tables).set_index('users')
OUTPUT:

         table1   table2   table3   table4   table5
        ------   ------   ------   ------   ------
users     
    1     True     True     True     True     True
    2     True     None     Nooe     True     None
    3     None     True     True     None     True
    4     None     None     True     True     True
    5     True     None     None     True     None
    6     True     True     True     None     True
    7     None     None     True     True     True
    8     True     True     None     True     None
    9     True     None     True     None     None
   10     None     None     True     True     None

…и вот. Таблица, показывающая, как идентификаторы пользователей упоминаются в других таблицах в кадре данных pandas.

Подводя итог, если вы привыкли выполнять левые соединения таблиц в своей реляционной базе данных и хотите добиться чего-то подобного в Python, у вас есть несколько вариантов. Есть панды, но, как ни странно, выполнить соединение двух отдельных столбцов, чтобы найти значения, на которые нет ссылок, непросто. Кроме того, вы можете использовать Sets, чтобы получить разницу уникальных значений двух столбцов. Но, вероятно, лучший вариант — использовать простые циклы, особенно если вы хотите идентифицировать несопоставленные записи в нескольких таблицах.