June 24

⚫️.merge(indicator=True): как быстро сравнить таблицы

Когда приходит новая выгрузка — ты открываешь Excel, сводные, фильтры, ломаешь глаза. Но в Python есть способ проще, быстрее и надёжнее.

🛠 Что делает .merge(indicator=True)?
— Добавляет колонку _merge, где видно:
• left_only — строки, которые были только в первой таблице
• right_only — только во второй
• both — те, что совпали

💡 Это удобнее, чем писать по 5 .isin():
• Сразу видно, кто исчез и кто появился
• Можно понять, совпали ли ключи
• Отличный способ тестировать join'ы

📦 Где пригодится:
• сравнение выгрузок из CRM и БД
• сверка API-результатов
• проверка “до/после” расчётов
• аудит отчётов и BI

✍️ Пример:
df.merge(df2, how='outer', on='user_id', indicator=True)
Вместо 10 строк сравнения — один понятный датафрейм.


🧠 Сравнивать данные — это не скучная рутина, а навык настоящего аналитика.

Ты выгрузил отчёт, получил API-ответ, проверил pipeline — а в данных что-то изменилось. Вопрос: где? почему? а было ли так вчера?

Обычно сравнение таблиц выглядит как боль:
added = df_new[~df_new["id"].isin(df_old["id"])] removed = df_old[~df_old["id"].isin(df_new["id"])]


Или костыль из concat + assign, который потом страшно открывать.
💡 Но можно проще, красивее и безопаснее — через .merge(indicator=True).

▫️ Что это даёт?

При объединении двух таблиц ты получаешь колонку _merge:
• 'left_only' — строки только в старой таблице
• 'right_only' — строки только в новой
• 'both' — всё совпало

Работает с разными источниками (Excel + API, SQL + csv) и не боится разного порядка колонок. Удобно? Очень.

⚙️ На практике

▪️ Пропавшие строки:
merged.query("_merge == 'left_only'")

▪️ Новые записи:
merged.query("_merge == 'right_only'")

▪️ Проверка на полное совпадение:
assert merged["_merge"].eq("both").all()

▪️ Добавить в отчёт статус:
merged["status"] = merged["_merge"].map({ "left_only": "удалён", "right_only": "добавлен", "both": "остался"
})


▪️ Сравнение значений по ключу:
df_old.merge(df_new, on="sku", how="outer", suffixes=("_old", "_new"), indicator=True)

▫️ Где это реально спасает:
• Сравнение выгрузок (отчёты вчера/сегодня)
• Сверка поставок или данных от партнёров
• Проверка отчётов: auto vs ручной
• Сравнение таблиц между PostgreSQL и BigQuery
• Диагностика: что потерялось в пайплайне

▫️ Почему это инженерный подход .merge(indicator=True) — это:
• читаемо
• безопасно
• тестируемо

▫️ Полезные советы:

• Переименуй _merge → diff_status — читается лучше
• Используй .map() — сразу видно, что добавилось, что исчезло
• Сохраняй diff в Excel — чтобы показать менеджеру/тимлиду
• Используй .query() — фильтрация понятнее, чем через mask
• Добавляй suffixes=("_old", "_new") — чтобы видеть конкретные отличия

▫️ А если всё это собрать в одну функцию — получаешь свой мини-дифф-движок для данных. Уже подготовил как бонус 👇

import pandas as pd from pandas.api.types 
import CategoricalDtype from typing 
import List, Optional

def diff_tables(
    df_old: pd.DataFrame,    
    df_new: pd.DataFrame,    
    on: List[str],    
    compare_cols: Optional[List[str]] = None) 
    -> pd.DataFrame:  
      
    """   
    Возвращает DataFrame с diff-результатом между двумя таблицами.    
    - compare_cols: если указаны, покажет различия в этих колонках (
    по суффиксам _old/_new)    
    """    
    
    how = "outer"    
    suffixes = ("_old", "_new")    
    
    df = df_old.merge(
      df_new, on=on, 
      how=how, 
      suffixes=suffixes, 
      indicator=True)
      
    df["diff_status"] = df["_merge"].map({
        "left_only": "удалён",        
        "right_only": "добавлен",        
        "both": "остался"    
    })
    
    if compare_cols:        
        for col in compare_cols:            
            col_old, col_new = f"{col}_old", f"{col}_new"            
            mask = df[col_old] != df[col_new]            
            new_label = f"изменён: {col}"            
            # Добавим категорию, если diff_status категориальный            
                if isinstance(df["diff_status"].dtype, CategoricalDtype):                
                    df["diff_status"] = df["diff_status"].astype(str)            
                df.loc[mask & (df["diff_status"] == "остался"), "diff_status"] = new_label
    
    return df.drop(columns=["_merge"])
    

# Пример использования:
df_old = pd.DataFrame({"user_id": [1, 2, 3, 4], "value": [10, 20, 30, 40]})
df_new = pd.DataFrame({"user_id": [3, 4, 5, 6], "value": [30, 45, 50, 60]})

diff = diff_tables(df_old, df_new, on=["user_id"], compare_cols=["value"])

print(diff)

Ты видишь, откуда пришла строка, и можешь писать юнит-тесты под diff. А главное — всё прозрачно.