本文与《SQL 打地基——SELECT / WHERE / JOIN / GROUP BY / HAVING 一篇理解(以电商分析为例)》呼应,用最常用的 5 大核心能力 搭起 pandas 的“肌肉记忆”:loc/iloc(取数/筛选)groupby(分组聚合)pivot/pivot_table(透视)merge(连接)datetime(时间处理)

准备:一份微型电商日志

import pandas as pd
# 构造一张“事件表”(一行=一个用户在某时刻做了一个动作)

raw = pd.DataFrame({
"user_id": ["u1","u1","u1","u2","u2","u3","u4"],
"event": ["view","cart","purchase","view","cart","view","view"],
"ts": pd.to_datetime([
"2025-09-01 10:00","2025-09-01 10:02","2025-09-01 10:05",
"2025-09-01 11:00","2025-09-01 11:03","2025-09-02 12:00","2025-09-02 15:00"
]),
"channel": ["ads","ads","ads","seo","seo","ads","ads"],
"price": [None,None,99,None,None,None,None],
})

表格呈现如图

其中event 有三种:view 浏览、cart 加购、purchase 购买(漏斗三层)。

一、loc / iloc —— 取数与筛选(类比 SQL: SELECT + WHERE)

loc用标签/列名取数;iloc 用位置取数。通俗来说:loc 是按“名字/条件”选,iloc 是按“位置/下标”选。“&”是“和”,raw.event=="view" 和raw.channel=="ads"两个条件要加小括号

df.loc[行条件, 列名列表]

df.iloc[行号/切片, 列号/切片]

(1)需求一:事件是 view 且渠道是 ads,只取出 user_id 和 ts 两列

效果如图

(2)需求二:取出表格的第2行,第2行,第4行;第0列,第2列

效果如图

代码如下:

# (1) 条件筛选 + 选列
# 业务筛选:事件是 view 且渠道是 ads,只拿 user_id 和 ts 两列
beijing_view = raw.loc[(raw.event=="view") & (raw.channel=="ads"), ["user_id","ts"]]

# (2) iloc:第 0,2,4 行;第 0,2 列
sample_pos = raw.iloc[[0,2,4], [0,2]]

二、groupby...agg...:“分组 + 聚合”

pandas 中的 groupby 就像 SQL 的 GROUP BY ;把数据按某列分组,再在每组上做统计(均值、中位数、计数、去重计数……)。相当于返回一个 GroupBy 对象,它“记住了”分组规则,不做计算。

agg 接受一个 字典{列名: 聚合函数},对每个组里的每一列,执行你指定的函数,然后把结果合并到一张新表。它存在于多种对象上,例如:

Series.agg(...) 一列做聚合
DataFrame.agg(...) 多列做聚合(可 row-wise/column-wise)
GroupBy.agg(...) 分组后对组内做聚合
Resampler.agg(...) 时间重采样后的聚合(按天/周等)
Rolling/Expanding.agg(...) 滑窗/累积窗口的聚合

聚合后,agg可以接收很多函数:

函数 作用 NA 处理
sum 求和 忽略 NA(相当于按有效值求和)
mean 均值 忽略 NA
median 中位数 忽略 NA
min/max 最小/最大 忽略 NA
std/var 标准差/方差(样本) 忽略 NA
count 非空计数 忽略 NA(只算非 NA)
size 行数 包含 NA(不看列是否为空)
nunique 去重后计数(默认不统计 NA) 默认 计 NA(可 dropna=False
quantile 分位数,需提供 q(0~1),如 0.95 需传参

(1)需求一:取出每个渠道的“去重用户数”(活跃用户口径)

这句话看着很绕,但是翻译成人话就是算一算:在广告渠道(ads)、自然搜索渠道(seo)等等,每个渠道里到底来了多少不同的用户。如果一个用户在同一个渠道出现很多次,也只算 1 个。这个数字一般就被叫作 活跃用户数

效果如图

(2)需求二:购买的日级指标(购买用户数、订单数、GMV)

意思是把所有“购买行为”单独拎出来,按“每天”来统计 3 个数字:

  1. 购买用户数:当天到底有多少不同的用户下单。

  2. 订单数:当天一共发生了多少个下单事件。

  3. GMV:当天成交的总金额(把当天所有订单价格加起来)。

效果如图

代码如下

# 每个渠道的“去重用户数”(活跃用户口径)
active_users = raw.groupby("channel")["user_id"].nunique() # nunique=去重计数


# 购买的日级指标(购买用户数、订单数、GMV)
p = raw[raw.event == "purchase"]
p_daily = p.groupby(p["ts"].dt.date).agg({
"user_id": "nunique", # buyers
"event": "size", # orders(用事件数近似)
"price": "sum", # GMV
}).rename(columns={"user_id":"buyers","event":"orders","price":"GMV"})

代码分析

第 1 部分:groupby("channel")["user_id"].nunique()

  1. raw.groupby("channel")
    → 按照 channel 这一列的值分组(ads 一组,seo 一组……)。

  2. ["user_id"]
    → 在每组里只看 user_id 这一列。

  3. .nunique()
    → 计算“去重后的数量”。

结果是一个 Series

channel
ads    3
seo    1
Name: user_id, dtype: int64

表示:ads 渠道有 3 个活跃用户,seo 渠道有 1 个。

第 2 部分:p = raw[raw.event == "purchase"]

  • 这是用 loc 的简写:把 event == "purchase" 的行筛出来。

  • 得到只包含购买行为的子表 p

第 3 部分:p["ts"].dt.date

  1. p["ts"] → 时间戳这一列。

  2. .dt → pandas 的 datetime 访问器,可以取出时间的某个组成部分。
    常见:.dt.year, .dt.month, .dt.date, .dt.hour

  3. .dt.date → 提取“日期部分”(去掉时分秒,只保留年月日)。

第 4 部分:groupby(...).agg({...})

  • groupby(p["ts"].dt.date) → 按日期分组。

  • .agg({...}) → 在每组里做多种聚合统计。字典里的 key=列名,value=聚合方法

其中

{
  "user_id": "nunique",   # 每天有多少不同用户购买
  "event": "size",        # 每天有多少条购买事件(近似订单数)
  "price": "sum",         # 每天购买的总金额(GMV)
}

第 5 部分:.rename(columns={...})

聚合后的结果表列名还是原始列名(user_id, event, price),不直观。
rename 把它们改成更好理解的名字:

  • user_idbuyers(购买用户数)

  • eventorders(订单数)

  • priceGMV(总成交额)

类比SQL

SELECT DATE(ts) AS date,
       COUNT(DISTINCT user_id) AS buyers,
       COUNT(*) AS orders,
       SUM(price) AS GMV
FROM raw
WHERE event = 'purchase'
GROUP BY DATE(ts);

三、pivot / pivot_table:把“长表 → 宽表”

pivotpivot_table 的区别

pivot(index, columns, values):要求 (index, columns) 组合唯一

pivot_table(..., aggfunc=...):不唯一也能用,先聚合再透视;默认均值。

为什么要“宽表化”?

因为很多分析要 对比同一用户在不同事件上的关系,比如漏斗(view→cart→purchase 顺序)。
如果还是长表形式,你要对比同一个用户的三行,很麻烦;变成宽表后,同一行里就能直接对比。

需求:需要一个 “用户×步骤”宽表,用来做漏斗顺序校验。其中“行=用户、列=步骤、值=最早发生时间”

效果如图

代码如下

# 每个用户、每个事件的“最早时间”
first_time = raw.groupby(["user_id","event"], as_index=False)["ts"].min()

# 长表 → 宽表(行=user,列=event,值=ts)
wide = first_time.pivot(index="user_id", columns="event", values="ts")
# 按漏斗顺序重排列
wide = wide.reindex(columns=["view","cart","purchase"])
print(wide)

四、merge:连接两张表

就像 SQL 的 JOIN。常见 howinner(内连接)、left(左连接)、rightouter(全外)。

内置参数

# 1) 同名键 inner join
out = pd.merge(A, B, how="inner", on="user_id")

# 2) 不同名键 left join
out = A.merge(B, how="left", left_on="uid", right_on="user_id")

# 3) 用索引对列 join
out = A.merge(B, left_index=True, right_on="user_id")

# 4) 防止列名冲突,用自定义后缀
out = A.merge(B, on="user_id", suffixes=("_left", "_right"))

# 5) 查看每行来自哪里(左/右/两边)
out = A.merge(B, on="user_id", how="outer", indicator=True)

# 6) 断言匹配关系,避免意外“一对多”膨胀
out = A.merge(B, on="user_id", how="left", validate="one_to_one")

需求:合并两个表

效果如图

代码如下

# 先取购买事件的“首单时间和金额”
first_order = (raw[raw.event=="purchase"]
.sort_values(["user_id","ts"]) # 按时间排序
.drop_duplicates(subset=["user_id"]) # 保留每人第1单
[["user_id","ts","price"]]
.rename(columns={"ts":"first_order_time","price":"first_order_price"}))


# 连接到用户画像上(保留所有用户)
profile = pd.merge(users, first_order, on="user_id", how="left")
print(profile)

五、datetime:日期相关统计

1. 转时间格式

在原始数据里,ts 往往是字符串。先统一转成 pandas 的时间戳类型(datetime64[ns]):

df["ts"] = pd.to_datetime(df["ts"], errors="coerce")  # 非法时间转为 NaT(空值)

errors="coerce":遇到 0000-00-00、拼写错误等,把它们变 NaT,避免报错。

2. 拿取“年/月/日/小时/周”

df["date"] = df["ts"].dt.date      # 只要“日期”(去掉时分秒)
df["hour"] = df["ts"].dt.hour      # 小时(0~23)
df["ym"]   = df["ts"].dt.to_period("M")  # 年-月周期对象(如 2025-09)
df["dow"]  = df["ts"].dt.dayofweek # 星期(周一=0)
df["ts_floor_h"] = df["ts"].dt.floor("H")  # 向下取整到整点
df["ts_floor_d"] = df["ts"].dt.floor("D")  # 向下取整到当天 00:00
# 还有 .ceil("H") 向上取整;.normalize() 直接把时间设为当天 00:00

3. 最常见的“按日/按小时统计”

3.1 按“日”统计 DAU(每天去重用户)

需求

每天有多少不同用户来过(活跃用户数,DAU)。

思路

  1. 把时间列 ts 提取成“日期”这一粒度(去掉时分秒),这样同一天的事件能归到同一桶。

  2. date 分组。

  3. 每组统计 去重用户数nunique())。

df["date"] = df["ts"].dt.date
dau = df.groupby("date")["user_id"].nunique()

3.2 购买的“日级指标”(购买用户数、订单数、GMV)

需求

对“购买”这类事件,按天统计三件事:

  1. 当天有多少不同的购买用户(buyers)

  2. 当天发生了多少购买事件条数(orders,通常≈订单数)

  3. 当天的成交额(GMV)

思路

  1. 先把只有购买的记录筛出来(这样后面聚合不被其他事件干扰)。

  2. 派生 date(同上)。

  3. 确保金额是数值(字符串/货币符号要清洗)。

  4. groupby(date) 后做多指标聚合

    • user_idnunique(购买用户数)

    • eventsize(行数 = 购买事件条数 ≈ 订单数)

    • pricesum(总成交额)

p = df.loc[df["event"]=="purchase"].copy()
p["date"] = p["ts"].dt.date
p["price"] = pd.to_numeric(p["price"], errors="coerce")

daily = (p.groupby("date", as_index=False)
          .agg(buyers=("user_id","nunique"),   # 购买用户数
               orders=("event","size"),        # 订单条数(事件数近似)
               GMV=("price","sum")))           # 成交额

3.3 按“小时”统计浏览量

需求

每个整点(如 10:00–10:59)发生了多少条浏览事件(view)。

思路

  1. 过滤出 event == "view"

  2. ts 向下取整到小时(例如 10:23→10:00),这样同一小时的事件落在同一时间桶。

  3. 对这个“整点时间”分组,统计行数

views_per_hour = (df.loc[df["event"]=="view"]
                    .groupby(df["ts"].dt.floor("H"))  # 每小时一个桶
                    .size())

4.时间统计速查

需求 写法
字符串→时间戳 pd.to_datetime(df["ts"], errors="coerce")
UTC→上海时区 df["ts"]=pd.to_datetime(...,utc=True).dt.tz_convert("Asia/Shanghai")
取日期 df["date"]=df["ts"].dt.date
取小时 df["hour"]=df["ts"].dt.hour或df["ts"].dt.floor("H")
每日去重用户 df.groupby(df["ts"].dt.date)["user_id"].nunique()
每小时浏览数 df.loc[df.event=="view"].groupby(df["ts"].dt.floor("H")).size()
规则时间序列按天 df.set_index("ts").resample("D").agg({...})或resample("D", on="ts")
补齐空日期 resample("D").size().fillna(0)或reindex(full_range, fill_value=0)

六、操作对照表

需求 pandas 写法 备注
条件筛选 df.loc[(cond1) & (cond2), col_list] `&/   /~` 加括号
选位置 df.iloc[row_slice, col_slice] 右端不包含
赋值 df.loc[cond, col] = val 不要链式赋值
去重计数 groupby(...)[col].nunique() UV/DAU
行数 groupby(...).size() 事件次数
透视 pivot(index, columns, values) 唯一组合
透视(聚合) pivot_table(..., aggfunc='first') 不唯一先聚合
连接 merge(left,right,on=...,how=...) join 类型
日期 to_datetime + .dt.date/hour 解析后再分组

七、完成“漏斗”和“日级指标”

7.1 用户×步骤的“最早时间”宽表

first_time = raw.groupby(["user_id","event"], as_index=False)["ts"].min()
wide = first_time.pivot(index="user_id", columns="event", values="ts").reindex(columns=["view","cart","purchase"])

7.2 顺序校验(purchase≥cart≥view )

import numpy as np
valid = np.ones(len(wide), dtype=bool)
for prev, nxt in zip(["view","cart"],["cart","purchase"]):
valid &= wide[nxt].isna() | (wide[prev].notna() & (wide[nxt] >= wide[prev]))
wide_valid = wide[valid]

7.3 漏斗人数与转化率

reach = wide_valid.notna().sum() # 每层到达的“去空”人数
base = reach.iloc[0] if len(reach)>0 else 0 # 第一层人数
funnel = pd.DataFrame({
"step": ["view","cart","purchase"],
"reach_users": reach.values,
"step_conv": (reach/ reach.shift(1)).fillna(1.0).values, # 相对上一层
"overall_conv": (reach/ base).fillna(0.0).values, # 相对第一层
})
print(funnel)

7.4 日级指标

raw["date"] = raw["ts"].dt.date
DAU = raw.groupby("date")["user_id"].nunique()
orders = raw[raw.event=="purchase"].groupby("date").size().rename("orders")
GMV = raw.loc[raw.event=="purchase","price"].groupby(raw["date"]).sum(min_count=1).rename("GMV")

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐