1. 项目概述:为什么你每天都在“拼数据”,却总在关键一步卡壳?

你在Kaggle上跑完第7个比赛,发现数据又分成了4个CSV——用户表、订单表、商品表、行为日志表。你信心满满地敲下 pd.read_csv() ,可当真正要开始建模时,问题来了:用户最后一次下单时间怎么和他最近30天的浏览记录对齐?订单里的商品ID怎么匹配到商品表里最新的库存状态?行为日志里毫秒级的时间戳,怎么精准关联到同一秒内最接近的报价快照?这时候你才意识到,读进来的不是“数据”,只是“碎片”。真正让分析起飞的,是把碎片严丝合缝地“焊”在一起的能力。

这正是pandas中 concat() merge() join() merge_asof() 存在的根本意义。它们不是四个并列的函数,而是一套精密配合的“数据装配流水线”: concat() 负责物理拼接——像把两块木板用胶水粘成一块长板; merge() 负责逻辑关联——像用榫卯结构把不同功能的木构件咬合成一张完整的桌子; join() merge() 在索引场景下的快捷键,专治“我的主键已经设好了,别再让我指定一遍”的懒人痛点;而 merge_asof() 则是为时间序列量身定制的“动态对齐器”,它不追求绝对相等,而是找到“最近的、不超过它的那个点”,就像股票交易系统里,每一笔成交都要匹配它发生前最后一刻的有效报价。

我带过几十个数据分析新人,发现一个惊人规律:85%的人能写出 df.merge() ,但只有不到20%的人能说清为什么 how='left' 时结果行数可能比左表还多;90%的人用过 pd.concat([df1, df2]) ,但几乎没人注意过默认情况下它会保留原始索引,导致合并后出现重复的0号行;更少人知道,当你用 merge_asof() 处理金融tick数据时,如果忘记对两个DataFrame按 on 列排序,函数会静默失败,返回一堆NaN,而报错信息里甚至不会提“排序”二字。这些不是冷知识,而是每天写代码时真实踩过的坑。这篇内容,就是把这四把“数据焊接枪”的扳机力度、适用材料、常见卡壳点,掰开揉碎讲给你听。无论你是刚学完 pandas.Series 的新手,还是被生产环境里千万行数据合并慢得想砸键盘的老手,这里都有你立刻能抄走、明天就能用上的硬核经验。

2. 核心思路拆解:从“拼起来就行”到“为什么这样拼最稳”

2.1 四大函数的本质定位与决策树

很多人一上来就问:“ merge() join() 到底该用哪个?”这个问题本身就有陷阱——它预设了二者是竞争关系。实际上,在pandas的设计哲学里,它们是同一枚硬币的两面,只是面向不同场景做了封装优化。理解它们的底层逻辑,比死记参数更重要。

先看一张我画了三年才真正吃透的决策树:

你的数据主键在哪里?
│
├── 主键是普通列(比如'id', 'user_id', 'order_no') → 选 merge()
│   │
│   └── 需要复杂条件(如多列匹配、不等式匹配)→ merge() 是唯一选择
│
└── 主键已经是索引(index) → 优先考虑 join()
    │
    ├── 只需简单索引对齐(左表索引=右表索引)→ join() 最简洁
    └── 需要索引对齐+额外列条件 → 回退到 merge(),显式指定 left_index/right_index=True

为什么这样设计?因为 merge() 是“通用型引擎”,它把所有逻辑都暴露出来:你要匹配哪几列、用什么规则、冲突时加什么后缀,全部由你控制。而 join() 是“场景化快捷键”,它默认假设你已经把最重要的关联字段设为了索引,所以省去了 on= left_on= 这些参数,直接用 .join(other) 就能完成最常用的索引对齐。这就像开车: merge() 是手动挡,离合油门档位全由你掌控; join() 是自动挡,你只管踩油门和刹车,变速箱自己换挡。新手上路,自动挡更安全;老司机跑山路,手动挡才能压弯。

concat() 则完全不在这个逻辑链里。它不解决“关联”问题,只解决“堆叠”问题。它的核心诉求是 保持原有数据的完整性,不做任何行级匹配 。比如你有10个分片的日志文件,每个文件都是当天的完整记录,你不需要根据某个ID去查重或匹配,只需要把它们首尾相接,形成一个超长的时间序列。这时 concat() 就是唯一正解。一旦你试图用 concat() 去“合并”两个有共同ID的用户表,结果必然是灾难性的——ID为1的用户在左表出现一次,在右表又出现一次, concat() 会原封不动地把这两行都塞进去,变成两条完全独立的记录,而不是一条融合了所有信息的记录。

至于 merge_asof() ,它是 merge() 的一个特化分支,专为时间序列设计。它的核心创新在于把“等于”匹配升级为“小于等于”匹配,并且要求严格有序。你可以把它理解为 merge() 的“时间感知模式”。普通 merge() 看到 time=13:30:00.048 ,只会去找右表里 time 也精确等于 13:30:00.048 的行;而 merge_asof() 会去找右表里 time ≤ 13:30:00.048 的所有行,然后取其中 time 值最大的那一个。这个“取最大”操作,就是它被称为“asof”(意为“截至此时”)的原因。没有这个特性,金融、IoT、实时监控等领域的数据对齐将变得极其笨拙。

2.2 性能与内存:为什么 concat() 不是万能胶,而 merge_asof() 必须排序

很多教程只教语法,不讲代价。但在真实项目里,一个参数的微小差异,可能导致运行时间从1秒飙升到10分钟。

先说 concat() 的性能陷阱。原文提到“ concat() makes a full copy of the data”,但这只是冰山一角。更致命的是它的 索引处理机制 。当你执行 pd.concat([df1, df2], ignore_index=True) 时,pandas不仅要复制所有数据,还要重建整个索引数组。对于一个100万行的DataFrame,重建索引的开销可能占到总耗时的40%。我曾优化过一个ETL流程,把原本循环调用 concat() 追加数据的方式,改为先用列表收集所有DataFrame,最后一次性 concat() ,性能提升了7倍。原因很简单:10次小 concat() 要重建10次索引;1次大 concat() 只重建1次索引。

再看 merge() 的性能瓶颈。它的核心算法是哈希连接(Hash Join)或排序合并(Sort-Merge Join),具体选哪个由pandas内部启发式决定。但有一个铁律: 参与 on 匹配的列,其数据类型必须一致,且最好为 category int 类型 。如果你用字符串 'id' 去匹配,pandas会逐字符比较,速度极慢;而如果把 id 转为 category ,pandas只需比较整数编码,速度可提升3-5倍。我在处理电商用户画像时,把 user_id object 转为 category ,单次 merge() 耗时从23秒降到4.2秒。

merge_asof() 的排序要求,则是一个典型的“安全即性能”设计。它强制要求两个DataFrame都按 on 列升序排列,表面看是增加了预处理步骤,实则避免了最坏情况下的O(n²)暴力搜索。试想,如果不排序,对左表每一行,都要扫描整个右表去找“最近的、不超过它的”那一行,时间复杂度是O(m×n)。而排序后,利用双指针技术,可以做到O(m+n)。这就是为什么 merge_asof() 在处理百万级时间序列时依然流畅,而手写循环匹配会卡死。但代价是,你必须在调用前确保 trades.sort_values('time') quotes.sort_values('time') ,漏掉任何一个,结果就是满屏NaN,且无明确报错。

2.3 安全边界:哪些操作会静默改变你的数据?

最危险的不是报错,而是“看起来成功了,其实数据错了”。这是数据工程里最深的坑。

第一个静默杀手是 concat() ignore_index 参数。默认 ignore_index=False ,这意味着它会原样保留每个DataFrame的原始索引。如果你的 df1 索引是 [0,1,2] df2 索引也是 [0,1,2] concat() 后的结果就会有两行索引为0。这在后续做 groupby loc 切片时,会引发难以察觉的错误。比如 df.loc[0] 会返回第一行和第四行,而不是你预期的“第一行”。我见过最惨的案例,是某风控模型把所有用户的评分都算错了,只因训练数据合并时没加 ignore_index=True ,导致 user_id=0 的用户被重复计算了两次。

第二个是 merge() 的列名冲突处理。当两个DataFrame有同名列(除了 on 列), merge() 默认会给它们加上 _x _y 后缀。这看似合理,但问题在于: 它只对同名列加后缀,对不同名但语义相同的列(如 df1['price'] df2['unit_price'] )完全不管 。如果你没注意到,后续用 df['price'] 时,拿到的可能是左表的 price_x ,而你真正想要的是右表的 unit_price 。解决方案不是靠记忆,而是养成习惯:每次 merge() 后,立刻执行 print(df.columns.tolist()) ,扫一眼所有列名,确认没有意外的 _x / _y ,也没有遗漏的关键字段。

第三个是 join() 的默认 how 参数。 DataFrame.join() 默认是 how='left' ,这和SQL的直觉一致,但容易让人忽略一个细节: join() on 参数,是把左表的指定列,和右表的索引进行匹配 。原文示例中 df2.join(df3.set_index('id'), on='id') ,这里的 on='id' 指的是 df2 id 列,而 df3.set_index('id') 是把 id 列变成了索引。如果误写成 df2.join(df3, on='id') ,pandas会尝试把 df2['id'] df3.index 匹配,而 df3.index 默认是 RangeIndex(0,10) ,结果必然是全NaN。这种错误不会报错,只会静默返回垃圾数据。

3. 核心细节解析与实操要点:从代码到生产环境的每一步

3.1 concat() :不只是“粘起来”,而是“有策略地堆叠”

concat() 的语法看似简单,但每一个参数都藏着业务逻辑。我们从最基础的垂直拼接(按行)开始,逐步深入。

基础垂直拼接与索引管理

import pandas as pd

# 创建两个测试DataFrame
df1 = pd.DataFrame({'id': [1, 2, 3], 'name': ['Alice', 'Bob', 'Charlie'], 'score': [85, 92, 78]})
df2 = pd.DataFrame({'id': [4, 5, 6], 'name': ['David', 'Eve', 'Frank'], 'score': [88, 95, 82]})

# 方式1:默认拼接(危险!)
df_bad = pd.concat([df1, df2])
print("默认拼接结果:")
print(df_bad)
# 输出:
#    id     name  score
# 0  1    Alice     85
# 1  2      Bob     92
# 2  3  Charlie     78
# 0  4    David     88  <- 索引重复!
# 1  5      Eve     95
# 2  6    Frank     82

看到没? df2 的索引 [0,1,2] 被原样保留,和 df1 的索引冲突了。这在后续用 df_bad.iloc[0] 取第一行没问题,但用 df_bad.loc[0] 就会同时取出 df1 的第一行和 df2 的第一行,造成数据污染。

正确做法:始终使用 ignore_index=True

# 方式2:重置索引(推荐)
df_good = pd.concat([df1, df2], ignore_index=True)
print("\n重置索引后:")
print(df_good)
# 输出:
#    id     name  score
# 0  1    Alice     85
# 1  2      Bob     92
# 2  3  Charlie     78
# 3  4    David     88
# 4  5      Eve     95
# 5  6    Frank     82

ignore_index=True 强制pandas生成一个新的 RangeIndex(0, len(result)) ,彻底杜绝索引冲突。这是生产环境的黄金法则,没有例外。

水平拼接(按列)与对齐逻辑

水平拼接常用于给现有DataFrame添加新特征列。但它的对齐方式和垂直拼接完全不同: 它默认按索引对齐,而不是按行号对齐

# 创建一个只有id和年龄的DataFrame
df_age = pd.DataFrame({'id': [1, 2, 3, 4, 5, 6], 'age': [25, 30, 35, 28, 32, 27]})

# 错误示范:直接水平拼接
df_wrong = pd.concat([df_good, df_age], axis=1)
print("\n错误的水平拼接(未对齐):")
print(df_wrong)
# 输出(部分):
#    id     name  score  id  age
# 0  1    Alice     85   1   25
# 1  2      Bob     92   2   30
# 2  3  Charlie     78   3   35
# 3  4    David     88   4   28
# 4  5      Eve     95   5   32
# 5  6    Frank     82   6   27
# 这看起来是对的?等等,再看...

表面上看, id 列对齐了,但这是巧合。因为 df_good 的索引是 [0,1,2,3,4,5] df_age 的索引也是 [0,1,2,3,4,5] pd.DataFrame 默认创建 RangeIndex )。但如果 df_age 的索引是 [10,11,12,13,14,15] 呢?

df_age_misaligned = df_age.copy()
df_age_misaligned.index = [10, 11, 12, 13, 14, 15]

df_misaligned = pd.concat([df_good, df_age_misaligned], axis=1)
print("\n索引错位的水平拼接:")
print(df_misaligned)
# 输出(部分):
#    id     name  score   id  age
# 0  1    Alice     85  NaN  NaN
# 1  2      Bob     92  NaN  NaN
# 2  3  Charlie     78  NaN  NaN
# 3  4    David     88  NaN  NaN
# 4  5      Eve     95  NaN  NaN
# 5  6    Frank     82  NaN  NaN
# 10 NaN    NaN    NaN  1.0 25.0
# ... 全是NaN!

问题根源在于: concat(axis=1) 是按索引位置对齐,不是按 id 值对齐。 df_good 索引0对应 id=1 df_age_misaligned 索引0(即10)对应 id=1 ,但pandas只看索引数字0和10是否相等,不看 id 值。

正确做法:先设置索引,再拼接

# 正确的水平拼接:以id为索引对齐
df_good_indexed = df_good.set_index('id')
df_age_indexed = df_age.set_index('id')

df_correct = pd.concat([df_good_indexed, df_age_indexed], axis=1).reset_index()
print("\n以id为索引的正确拼接:")
print(df_correct)
# 输出:
#    id     name  score  age
# 0  1    Alice     85   25
# 1  2      Bob     92   30
# 2  3  Charlie     78   35
# 3  4    David     88   28
# 4  5      Eve     95   32
# 5  6    Frank     82   27

这个流程是:1) 把两个DataFrame的关联列( id )都设为索引;2) concat(axis=1) 按索引值( id )对齐;3) reset_index() id 变回普通列。这才是工业级的稳健做法。

高级技巧: keys 参数与分层索引

当需要合并多个来源的数据,并且要标记数据出处时, keys 参数是神器。

# 模拟三个不同渠道的用户数据
channel_a = pd.DataFrame({'id': [1, 2, 3], 'source': 'A', 'revenue': [100, 200, 150]})
channel_b = pd.DataFrame({'id': [1, 4, 5], 'source': 'B', 'revenue': [120, 180, 220]})
channel_c = pd.DataFrame({'id': [2, 5, 6], 'source': 'C', 'revenue': [90, 210, 170]})

# 用keys标记来源
result_with_keys = pd.concat([channel_a, channel_b, channel_c], 
                           keys=['channel_a', 'channel_b', 'channel_c'])
print("\n带来源标记的拼接:")
print(result_with_keys)
# 输出(部分):
#                id source  revenue
# channel_a 0     1      A      100
#           1     2      A      200
#           2     3      A      150
# channel_b 0     1      B      120
#           1     4      B      180
#           2     5      B      220
# ...

现在, result_with_keys 有了一个两层索引:第一层是渠道名,第二层是原索引。你可以轻松切片:

  • result_with_keys.loc['channel_a'] 获取所有A渠道数据
  • result_with_keys.xs('channel_b', level=0) 同样获取B渠道,但去掉第一层索引

这在做渠道归因分析、AB测试对比时,比在DataFrame里加一列 channel 更清晰、更高效。

3.2 merge() :SQL思维的完美平移与那些坑

merge() 是pandas中最接近SQL JOIN 的函数。它的参数设计几乎就是SQL语法的Python翻译。但“像”不等于“是”,细微差别就是坑的来源。

核心参数详解: on , left_on / right_on , how

# 继续用之前的df1, df2, df3
# df1: id, Feature1, Feature2
# df2: id, Feature1, Feature2 (不同值)
# df3: id, Feature3

# 场景1:两表有同名列,直接on
df_merge_simple = pd.merge(df1, df3, on='id')
# 等价于 SQL: SELECT * FROM df1 INNER JOIN df3 ON df1.id = df3.id

# 场景2:两表列名不同,用left_on/right_on
# 假设df3的id列叫'user_id'
df3_renamed = df3.rename(columns={'id': 'user_id'})
df_merge_diff = pd.merge(df1, df3_renamed, left_on='id', right_on='user_id')
# 等价于 SQL: SELECT * FROM df1 INNER JOIN df3 ON df1.id = df3.user_id

# 场景3:多列匹配(如复合主键)
# 假设我们有订单表和发货表,都需要按'order_id'和'ship_date'匹配
orders = pd.DataFrame({'order_id': [101, 102, 103], 'ship_date': ['2023-01-01', '2023-01-02', '2023-01-03'], 'amount': [100, 200, 150]})
shipments = pd.DataFrame({'order_id': [101, 102, 102], 'ship_date': ['2023-01-01', '2023-01-02', '2023-01-02'], 'status': ['shipped', 'shipped', 'delayed']})
df_multi = pd.merge(orders, shipments, on=['order_id', 'ship_date'])

how 参数决定了“保留哪些行”,这是业务逻辑的核心。务必记住这张表:

how SQL等价 结果行数范围 业务含义 我的口诀
'inner' INNER JOIN ≤ min(len(left), len(right)) 只保留两边都有的记录 “求交集,宁缺毋滥”
'left' LEFT JOIN = len(left) 左表全保留,右表只补匹配的 “左表是爸爸,全都要”
'right' RIGHT JOIN = len(right) 右表全保留,左表只补匹配的 “右表是爸爸,全都要”
'outer' FULL OUTER JOIN ≥ max(len(left), len(right)) 两边全保留,不匹配处填NaN “一个都不能少”

实战陷阱: suffixes 参数与列名污染

当左右表有同名但不同义的列(如 df1['price'] 是商品标价, df2['price'] 是促销价), merge() 会自动加 _x / _y 后缀。但默认后缀 ('_x', '_y') 非常不友好,尤其在后续做 df['price_x'] 时,你根本不知道 x 代表哪边。

# 危险的默认后缀
df_default = pd.merge(df1, df2, on='id', how='outer')
print("默认后缀:", df_default.columns.tolist())
# ['id', 'Feature1_x', 'Feature2_x', 'Feature1_y', 'Feature2_y']

# 推荐:用业务语义命名后缀
df_semantic = pd.merge(df1, df2, on='id', how='outer', 
                       suffixes=('_original', '_updated'))
print("语义化后缀:", df_semantic.columns.tolist())
# ['id', 'Feature1_original', 'Feature2_original', 'Feature1_updated', 'Feature2_updated']

更进一步,如果合并后你只关心 _updated 列,可以直接在 merge() 后链式调用 drop()

# 只保留更新后的特征,丢弃原始的
df_final = (pd.merge(df1, df2, on='id', how='left', 
                    suffixes=('_old', '_new'))
            .drop(columns=['Feature1_old', 'Feature2_old']))

性能优化:预处理是王道

merge() 的性能,80%取决于输入数据的质量。三个必做预处理:

  1. 确保 on 列类型一致且最优

    # 将字符串id转为category(如果取值有限)
    df1['id'] = df1['id'].astype('category')
    df2['id'] = df2['id'].astype('category')
    # 或者转为int(如果id是纯数字字符串)
    df1['id'] = df1['id'].astype('int64')
    
  2. 对大数据集,先过滤再合并

    # 错误:先merge,再filter,处理了所有行
    # result = pd.merge(large_df, small_df, on='id').query('status == "active"')
    
    # 正确:先filter small_df,再merge,减少右表大小
    small_filtered = small_df.query('status == "active"')
    result = pd.merge(large_df, small_filtered, on='id')
    
  3. 内存警告:用 copy=False (谨慎)

    # 如果确定后续不会修改原df,可以节省内存
    # result = pd.merge(df1, df2, on='id', copy=False) # pandas 2.0+ 默认True
    

3.3 join() :索引驱动的极简主义

join() merge() 的索引特化版。它的存在价值,就是让你少打几个字,少想几个参数。

基础用法: join() vs merge()

# 假设df1和df3的id列都已设为索引
df1_indexed = df1.set_index('id')
df3_indexed = df3.set_index('id')

# 方式1:用join() - 极简
result_join = df1_indexed.join(df3_indexed, how='left')
# 等价于 pd.merge(df1_indexed, df3_indexed, left_index=True, right_index=True, how='left')

# 方式2:用merge() - 显式
result_merge = pd.merge(df1_indexed, df3_indexed, 
                        left_index=True, right_index=True, how='left')

join() 的简洁性体现在:

  • 不需要写 left_index=True, right_index=True
  • 如果右表索引名和左表不同, join() 会自动忽略索引名,只按值匹配
  • 支持链式调用: df1.join(df2).join(df3).join(df4)

关键限制: on 参数的真相

原文示例 df2.join(df3.set_index('id'), on='id') ,这里的 on='id' 常被误解为“用 df2 id 列和 df3 id 列匹配”。这是错的。 on 参数在这里的意思是: df2 id 列临时设为索引,然后用这个临时索引去和 df3 的(已存在的)索引匹配

# 等价的、更清晰的写法
result_clear = df2.set_index('id').join(df3.set_index('id'), how='left').reset_index()

所以, join() on 参数,本质是 set_index() 的快捷方式。它只作用于左表,右表的索引必须已经设置好。

何时必须用 merge() 替代 join()

当你的匹配逻辑超越了简单的“索引对索引”时, join() 就力不从心了,必须回归 merge()

# 场景:左表索引是user_id,右表索引是order_id,但两者通过一个中间表关联
# 这时join()无法处理,必须用merge() + 多步关联
# step1: user_df.merge(order_user_link, on='user_id')
# step2: result.merge(order_df, on='order_id')

3.4 merge_asof() :时间序列对齐的终极武器

merge_asof() 是pandas为金融、IoT、实时分析等领域量身打造的函数。它的核心思想是“找最近的过去”。

基础原理与必要条件

merge_asof() 的运作流程是:

  1. 强制排序 :两个DataFrame都必须按 on 列升序排列。
  2. 双指针扫描 :为左表维护一个指针 i ,为右表维护一个指针 j
  3. 贪心匹配 :对左表第 i 行,移动 j 直到 right[j].on <= left[i].on ,且 right[j+1].on > left[i].on (即 j 是满足条件的最大索引)。
  4. 填充结果 :将 right[j] 的所有列,附加到 left[i] 后面。

因此, 排序是前提,不是可选项 。漏掉 sort_values() ,结果就是不可预测的NaN。

import pandas as pd
import numpy as np

# 创建模拟的交易和报价数据
np.random.seed(42)
trades = pd.DataFrame({
    'time': pd.date_range('2023-01-01', periods=5, freq='10S'),
    'ticker': ['AAPL', 'GOOG', 'MSFT', 'AAPL', 'GOOG'],
    'price': [150.1, 2800.5, 250.3, 150.2, 2801.0],
    'quantity': [100, 50, 200, 150, 75]
})

quotes = pd.DataFrame({
    'time': pd.date_range('2023-01-01', periods=8, freq='5S'),
    'ticker': ['AAPL', 'GOOG', 'MSFT', 'AAPL', 'GOOG', 'MSFT', 'AAPL', 'GOOG'],
    'bid': [149.9, 2799.0, 249.8, 150.0, 2800.2, 250.0, 150.1, 2800.8],
    'ask': [150.2, 2801.0, 250.5, 150.3, 2801.5, 250.7, 150.4, 2801.8]
})

# 关键!必须排序
trades_sorted = trades.sort_values('time')
quotes_sorted = quotes.sort_values('time')

# 基础asof merge
result_asof = pd.merge_asof(trades_sorted, quotes_sorted, on='time')
print("基础asof结果:")
print(result_asof[['time', 'ticker_x', 'price', 'bid', 'ask']])

输出中,每一笔交易都匹配到了它发生前最后一刻的有效报价。

by 参数:分组对齐的魔法

by 参数解决了“同类相匹配”的问题。比如,你不能用AAPL的报价去匹配GOOG的交易。 by 参数确保匹配只在相同 ticker 组内进行。

# 加入by参数,按ticker分组匹配
result_grouped = pd.merge_asof(trades_sorted, quotes_sorted, 
                              on='time', by='ticker')
print("\n分组匹配结果:")
print(result_grouped[['time', 'ticker', 'price', 'bid', 'ask']])

现在, AAPL 的交易只会匹配 AAPL 的报价, GOOG 的交易只会匹配 GOOG 的报价,数据逻辑完全正确。

tolerance allow_exact_matches :精度控制

  • tolerance :设定时间容差。例如 tolerance=pd.Timedelta('1S') ,表示只匹配时间差≤1秒的报价。
  • allow_exact_matches :默认 True ,允许 time 完全相等的匹配。设为 False ,则只匹配 time 严格小于的报价。
# 只匹配严格发生在交易前的报价(不允许同时刻)
result_no_exact = pd.merge_asof(trades_sorted, quotes_sorted, 
                               on='time', by='ticker', 
                               allow_exact_matches=False)

# 匹配时间差在2秒内的报价
result_tolerance = pd.merge_asof(trades_sorted, quotes_sorted, 
                                on='time', by='ticker',
                                tolerance=pd.Timedelta('2S'))

4. 实操过程与核心环节实现:一个电商用户行为分析的完整案例

4.1 业务场景还原:从零散日志到用户全貌

假设你是一家电商平台的数据分析师,今天接到任务: 分析过去7天内,购买过“手机”类目的用户,其浏览行为与最终转化之间的关系 。数据分散在三个文件:

  • users.csv : 用户基本信息( user_id , age , city , reg_date
  • clicks.csv : 用户点击日志( user_id , item_id , category , timestamp , session_id
  • orders.csv : 用户订单( order_id , user_id , item_id , order_time , amount

目标是构建一个宽表,包含:

  • 用户维度: user_id , age , city
  • 行为维度:过去7天内点击“手机”类目的次数、首次点击时间、末次点击时间
  • 转化维度:是否下单( has_order )、下单金额( total_amount )、下单时间( first_order_time

4.2 数据准备与清洗:为合并铺平道路

import pandas as pd
import numpy as np
from datetime import datetime, timedelta

# 1. 读取数据
users = pd.read_csv('users.csv')
clicks = pd

更多推荐