Skip to content

数据合并

纵向追加、横向绑定与按键合并。

核心代码

py
def join_dataframes(
    *,
    current_df: pd.DataFrame,
    secondary_df: pd.DataFrame,
    left_keys: list[str] | str | None,
    right_keys: list[str] | str | None,
    how: str = 'left',
    suffix: str = '_right',
) -> tuple[pd.DataFrame, dict[str, Any], list[str]]:
    """按键执行 inner/left/right/outer 合并。"""
    left_keys = _normalize_keys(left_keys)
    right_keys = _normalize_keys(right_keys)
    if len(left_keys) != len(right_keys):
        raise DataProcessingValidationError(_('左右合并键数量必须一致'))

    require_columns(current_df, left_keys, label=_('左侧合并键'))
    require_columns(secondary_df, right_keys, label=_('右侧合并键'))
    if how not in {'inner', 'left', 'right', 'outer'}:
        raise DataProcessingValidationError(_('匹配合并方式仅支持 inner、left、right、outer'))

    secondary_df = secondary_df.copy()
    rename_map = {}
    for left_key, right_key in zip(left_keys, right_keys):
        if left_key != right_key:
            rename_map[right_key] = left_key
    if rename_map:
        secondary_df = secondary_df.rename(columns=rename_map)
    right_keys = [rename_map.get(key, key) for key in right_keys]

    duplicate_left_rows = int(current_df.duplicated(subset=left_keys, keep=False).sum())
    duplicate_right_rows = int(secondary_df.duplicated(subset=left_keys, keep=False).sum())

    secondary_for_merge = secondary_df.copy()
    non_key_columns = [column for column in secondary_for_merge.columns if column not in left_keys]
    renamed_secondary, renamed_columns = resolve_conflicting_columns(
        existing_columns=[str(column) for column in current_df.columns],
        incoming_df=secondary_for_merge[non_key_columns],
        suffix=suffix,
    )
    secondary_for_merge = pd.concat([secondary_for_merge[left_keys], renamed_secondary], axis=1)

    merged_df = current_df.merge(
        secondary_for_merge,
        on=left_keys,
        how=how,
        indicator=True,
    )
    indicator = merged_df['_merge']
    match_counts = indicator.value_counts().to_dict()
    matched_rows = int(match_counts.get('both', 0))
    unmatched_left_rows = int(match_counts.get('left_only', 0))
    unmatched_right_rows = int(match_counts.get('right_only', 0))
    result_df = merged_df.drop(columns=['_merge'])

    base_rows = len(current_df) if how in {'left', 'inner'} else len(result_df)
    match_rate = float(matched_rows / base_rows) if base_rows else 0.0
    details = {
        'merge_mode': 'join',
        'how': how,
        'left_keys': left_keys,
        'right_keys': right_keys,
        'matched_rows': matched_rows,
        'unmatched_left_rows': unmatched_left_rows,
        'unmatched_right_rows': unmatched_right_rows,
        'match_rate': round(match_rate, 4),
        'duplicate_left_rows': duplicate_left_rows,
        'duplicate_right_rows': duplicate_right_rows,
        'renamed_conflicts': renamed_columns,
    }
    warnings = []
    if duplicate_left_rows:
        warnings.append(_('左侧数据存在 %(duplicate_left_rows)s 行重复合并键,结果可能出现一对多扩展') % {'duplicate_left_rows': duplicate_left_rows})
    if duplicate_right_rows:
        warnings.append(_('右侧数据存在 %(duplicate_right_rows)s 行重复合并键,结果可能出现多对一或多对多扩展') % {'duplicate_right_rows': duplicate_right_rows})
    if unmatched_left_rows:
        warnings.append(_('有 %(unmatched_left_rows)s 行左侧记录未匹配成功') % {'unmatched_left_rows': unmatched_left_rows})
    if unmatched_right_rows and how in {'right', 'outer'}:
        warnings.append(_('有 %(unmatched_right_rows)s 行右侧记录未匹配成功') % {'unmatched_right_rows': unmatched_right_rows})
    if renamed_columns:
        warnings.append(_('有 %(count)s 个重名变量已自动追加后缀 %(suffix)s') % {'count': len(renamed_columns), 'suffix': suffix})
    return result_df, details, warnings

Released under the AGPL-3.0 License.