Объединить слева со смешанным количеством идентификаторов - PullRequest
2 голосов
/ 01 мая 2020

У меня есть maptable и df, где я хочу применить левое слияние, чтобы отобразить дополнительный столбец на наборе из одного или нескольких столбцов. Однако в моем случае доступные идентификаторы отличаются в каждой строке.

Вот пример:

maptable =

  asset_class currency target
0      Equity      EUR     t1
1          FX      EUR     t2
2       Rates      USD     t3
3       Rates              t3o
4       Bonds              t4o
5       Bonds      AAA     t4

Предположим, у нас есть следующее значение df:

df =

  asset_class currency
0      Equity      EUR
1      Equity      USD
2      Equity      GBP
3       Rates      EUR
4       Rates      USD
5       Rates      GBP
6       Bonds      AAA
7       Bonds      BBB
8       Bonds      CCC

В этом случае желаемый результат должен быть:

  asset_class currency target
0      Equity      EUR     t1   (we have Equity+EUR)
1      Equity      USD    NaN   (we don't have Equity+USD and also not Equity)
2      Equity      GBP    NaN   (we don't have Equity+GBP and also not Equity)
3       Rates      EUR    t3o   (we don't have Rates+EUR, but we do have Rates)
4       Rates      USD     t3   (we have Rates+USD)
5       Rates      GBP    t30   (we don't have Rates+GBP, but we do have Rates)
6       Bonds      AAA     t4   (we have Bonds+AA)
7       Bonds      BBB    t4o   (we don't have Bonds+BBB, but we do have Bonds)
8       Bonds      CCC    t4o   (we don't have Bonds+CCC, but we do have Bonds)

Простое применение слияния, оставленного для asset_class и валюты, не будет работать, поскольку в случаях, когда один столбец идентификатора из двух имеет значения, он будет игнорироваться:

df_m = df.merge(maptable, how='left', on=['asset_class','currency'])

Также очень важно что нам нужно перезаписать в случае, когда целевой столбец уже сопоставлен, если мы используем больше столбцов идентификаторов. Например, использование 'asset_class' и 'currency' имеет больший приоритет, чем просто сопоставление с 'asset_class'. По этой причине fillna не будет работать, так как нам на самом деле нужен update.

Как этого можно добиться эффективным способом?


Пример данных

Вы можете воссоздать приведенный выше пример следующим образом:

import pandas as pd

maptable = pd.DataFrame({
    'asset_class': ['Equity', 'FX',   'Rates', 'Rates', 'Bonds', 'Bonds'],
    'currency':    ['EUR',    'EUR',  'USD',   '',      '',      'AAA'],
    'target':      ['t1',     't2',   't3',    't3o',    't4o',    't4']
})

df = pd.DataFrame({
    'asset_class': ['Equity', 'Equity', 'Equity', 'Rates', 'Rates', 'Rates', 'Bonds', 'Bonds', 'Bonds'],
    'currency':    ['EUR', 'USD', 'GBP', 'EUR', 'USD', 'GBP', 'AAA', 'BBB', 'CCC'],
})

Что я пробовал до сих пор

Вот то, что я пробовал пока (но это действительно элементарно):

def merge_mix(dl, dr, target_cols, id_cols):
    """Apply a merge left with a mixed number of identifiers

    :param dl:  target DataFrame on which we want to map the target_cols, contains id_cols but might also
    contain target_cols. If non-NA matching target values are found in dr, it will overwrite the values for the
    index/col combinations
    :param dr:  mapping DataFrame that contains both target_cols and id_cols
    :param target_cols: list of column names that we want to map from the dr
    :param id_cols: list of columns that we want to use as identifier, can be empty
    """
    def is_empty(x):
        """Check if empty"""
        if x is not None:
            if isinstance(x, str) and x != '':
                return False
            else:
                if not pd.np.isnan(value):
                    return False
        return True

    # Append target col
    for target_col in target_cols:
        if target_col not in dl:
            dl.insert(loc=len(dl.columns), column=target_col, value=None)

    # Clean dr
    dr = dr[id_cols + target_cols]
    dr = dr.drop_duplicates(keep='last')

    # Loop over all the indices and check which combinations exists
    for index in dr.index:
        combo_cols = []
        for col in id_cols:
            value = dr.loc[index, col]

            # Add combination if value is not empty
            if not is_empty(value):
                combo_cols.append(col)

        # The combination for this index
        dr.loc[index, 'combo_cols'] = "+".join(combo_cols)
        dr.loc[index, 'combo_count'] = len(combo_cols)

    # Get the unique combo cols combinations. Take first the least granular and then work towards more granular
    # as we are working with .update and not with .merge
    combos_count = list(dr['combo_count'].unique())  # Unique list
    combos_count = [x for x in combos_count if x > 0]  # Take out zero count combo cols
    combos_count.sort(reverse=False)  # Sort to move the least granular first

    for count in combos_count:

        # For a given count, check all combo combinations with this count
        dr_cc = dr[dr['combo_count'] == count]
        unique_combo_cols_cc = list(dr_cc['combo_cols'].unique())

        for combo_col in unique_combo_cols_cc:

            # Maptable for given combo col
            dr_uc_cc = dr_cc[dr_cc['combo_cols'] == combo_col]
            dr_uc_cc = dr_uc_cc.drop_duplicates(keep='last')

            # Set index on the id cols for this combo combination
            id_cols_uc_cc = combo_col.split('+')
            dl = dl.set_index(id_cols_uc_cc)
            dr_uc_cc = dr_uc_cc.set_index(id_cols_uc_cc)

            # Update matching row, cols
            dl.update(dr_uc_cc[target_cols])
            dl = dl.reset_index()

    return dl

Ответы [ 4 ]

1 голос
/ 01 мая 2020

Другой подход - сначала проверить, существует ли конкретная пара asset_class и currency в maptable; сначала заполнить недостающее значением по умолчанию (''), , а затем объединить:

keys = ['asset_class', 'currency']
df_m = df.assign(currency= \
    np.where(df.set_index(keys).index.isin(maptable.set_index(keys).index), df['currency'], '') \
    ).merge(maptable, on=keys, how='left').assign(currency=df['currency'])

Результат:

  asset_class currency target
0      Equity      EUR     t1
1      Equity      USD    NaN
2      Equity      GBP    NaN
3       Rates      EUR    t3o
4       Rates      USD     t3
5       Rates      GBP    t3o
6       Bonds      AAA     t4
7       Bonds      BBB    t4o
8       Bonds      CCC    t4o
1 голос
/ 01 мая 2020

Используя pd.merge, объедините два кадра данных в столбцах "asset_class", "currency", получив df_m.

df_m = pd.merge(df, maptable, on=["asset_class", "currency"], how="left")
# df_m

 asset_class currency target
0      Equity      EUR     t1
1      Equity      USD    NaN
2      Equity      GBP    NaN
3       Rates      EUR    NaN
4       Rates      USD     t3
5       Rates      GBP    NaN
6       Bonds      AAA     t4
7       Bonds      BBB    NaN
8       Bonds      CCC    NaN

Затем получите словарь mappings из кадра данных df, соответствующего в строки, где значение валюты равно '', а ключи в этом словаре - из столбца asset_class, а значения - из столбца target.

mappings = maptable[maptable["currency"].eq('')].set_index("asset_class")["target"].to_dict()
# mappings

{'Rates': 't3o', 'Bonds': 't4o'}

Теперь отфильтруйте столбец asset_class из df_m, где значения target col равны nan, и сопоставьте этот столбец, используя словарь mappings, полученный на предыдущем шаге, для создания новой серии s.

s = df_m.loc[df_m["target"].isna(), "asset_class"].map(mappings)
# s
1    NaN
2    NaN
3    t3o
5    t3o
7    t4o
8    t4o

Затем с помощью * Функция 1026 *.fillna заполняет значения nan столбца target в df_m, используя серию s.


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

df_m = pd.merge(df, maptable, on=["asset_class", "currency"], how="left")

# {'Bonds': 't4o', 'Rates': 't3o'}
mappings = maptable[maptable["currency"].eq('')].set_index("asset_class")["target"].to_dict()

s = df_m.loc[df_m["target"].isna(), "asset_class"].map(mappings)
df_m["target"] = df_m["target"].fillna(s)
print(df_m)

Печать:

  asset_class currency target
0      Equity      EUR     t1
1      Equity      USD    NaN
2      Equity      GBP    NaN
3       Rates      EUR    t3o
4       Rates      USD     t3
5       Rates      GBP    t3o
6       Bonds      AAA     t4
7       Bonds      BBB    t4o
8       Bonds      CCC    t4o
1 голос
/ 01 мая 2020

Сначала вы можете начать с объединенных данных:

merged = df.merge(maptable, how='left', on=['asset_class','currency'])

Это даст вам первый слой:

  asset_class currency target
0      Equity      EUR     t1
1      Equity      USD    NaN
2      Equity      GBP    NaN
3       Rates      EUR    NaN
4       Rates      USD     t3
5       Rates      GBP    NaN
6       Bonds      AAA     t4
7       Bonds      BBB    NaN
8       Bonds      CCC    NaN

Затем выполните fillna из другого слияния, сопоставив значение по умолчанию '' только для currency:

merged['target'].fillna(df.assign(currency='').merge(maptable, on=['asset_class','currency'], how='left')['target'], inplace=True)

Что даст вам результат:

>>> merged
  asset_class currency target
0      Equity      EUR     t1
1      Equity      USD    NaN
2      Equity      GBP    NaN
3       Rates      EUR    t3o
4       Rates      USD     t3
5       Rates      GBP    t3o
6       Bonds      AAA     t4
7       Bonds      BBB    t4o
8       Bonds      CCC    t4o

Само собой разумеется, в зависимости от вашего запасного значения, вы будете необходимо обновить '' соответственно. Если это NaN, используйте maptable['currency'].isna().

Один лайнер будет:

df_m = df.assign(target=\
    df.merge(maptable, on=['asset_class','currency'], how='left')['target'].fillna( \
    df.assign(currency='').merge(maptable, on=['asset_class','currency'], how='left')['target']))
1 голос
/ 01 мая 2020

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

mapdict = {
    tuple(filter(pd.notna, (a, c))): t
    for a, c, t in maptable.itertuples(index=False)
}

def get(x):
    return mapdict.get(x, mapdict.get((x[0], ''), mapdict.get(x[:1])))

list_of_cols = ['asset_class', 'currency']
df.assign(target=[*map(get, zip(*map(df.get, list_of_cols)))])

  asset_class currency target
0      Equity      EUR     t1
1      Equity      USD   None
2      Equity      GBP   None
3       Rates      EUR    t3o
4       Rates      USD     t3
5       Rates      GBP    t3o
6       Bonds      AAA     t4
7       Bonds      BBB    t4o
8       Bonds      CCC    t4o
...