Tableau Prep 调用外部 API 实战:用 TabPy 把天气数据接进流程

用 Tableau Prep 的「脚本」步骤配合 TabPy 调用 Open-Meteo 天气 API,把 12 城风速、降水、气温拉进数据流程,补齐航班延误分析的天气维度。含可行性验证、完整代码与实战踩坑。

做航班延误分析,我们一直缺天气这个维度。风速、降水、气温都跟延误强相关,但数据散在外部接口里。Tableau Prep 没有原生的 REST 输入连接器,这件事拖了挺久。

上周试出一条路:Tableau Prep 的「脚本」步骤走 TabPy(Python 分析扩展),能跑任意 Python。只要在脚本里写几行 requests,就能把外部 API 的数据变成一张表,接着在 Tableau Prep 里清洗、Join。下面是我们踩完坑跑通的记录。

Tableau Prep 脚本步骤怎么吐数据

Tableau Prep 从 2019.3.1 起支持 Python 脚本步骤。数据从上游流进脚本,脚本返回一张 DataFrame 流回去。官方文档有个硬约束:脚本步骤不能当流程的第一个步骤,前面必须有数据喂进来,calculation(df) 必须吃一个 df。

想凭空拉一个 API,得先找个占位输入当引子。

最小验证:占位种子拉北京天气

先做一个能跑通的最小版本,证明链路成立。

seed.csv 放一行占位数据,内容无所谓,只为让脚本步骤有上游:

seed
1

脚本里直接忽略 df,拉北京当前天气。跑通后返回一张天气表,价值不在于拉到了什么,而在于证明「Tableau Prep 脚本步骤 + TabPy 能拉外部 API 并返回表」这条链路成立。链路一通,后面都是扩展。

Tableau Prep 里接法:输入选 seed.csv → 加「脚本」步骤,连接类型选「Tableau Python (TabPy) 服务器」,Host 填 127.0.0.1、Port 9004,函数名 calculation。

这版要验证的就是Tableau Prep 能直接调外部API。calculation 里其实没读传入的 df,直接拿北京坐标拉 Open-Meteo,返回成功、解析成表。代价是 Prep 不能让脚本步骤凭空起头,得先垫一张占位表当上游——多了这一步,麻烦点,但路是通的。下边的多城市版直接用 df 里的真实坐标循环拉取,就更顺手了。

多城市:把数据喂进 df

占位只是验证。真要多个城市,把城市数据放进上游输入,用 df 传进函数,函数里逐行循环。

seed.csv:

city,latitude,longitude
北京,39.9042,116.4074
上海,31.2304,121.4737
广州,23.1291,113.2644
深圳,22.5431,114.0579
成都,30.5728,104.0668
杭州,30.2741,120.1551
武汉,30.5928,114.3055
西安,34.3416,108.9398
重庆,29.4316,106.9123
哈尔滨,45.8038,126.5344
乌鲁木齐,43.8256,87.6168
拉萨,29.6520,91.1721

脚本改成真正用 df:

import pandas as pd
import requests

BASE_URL = "https://api.open-meteo.com/v1/forecast"
CURRENT_FIELDS = (
    "temperature_2m,relative_humidity_2m,wind_speed_10m,"
    "wind_direction_10m,weather_code,is_day"
)


def _fetch_weather(lat, lon):
    params = {
        "latitude": lat,
        "longitude": lon,
        "current": CURRENT_FIELDS,
        "timezone": "Asia/Shanghai",
    }
    resp = requests.get(BASE_URL, params=params, timeout=30)
    resp.raise_for_status()
    return resp.json()


def calculation(df):
    rows = []
    for _, r in df.iterrows():
        name = str(r.get("city", ""))
        lat = float(r["latitude"])
        lon = float(r["longitude"])
        j = _fetch_weather(lat, lon)
        c = j["current"]
        rows.append({
            "city": name,
            "latitude": j.get("latitude"),
            "longitude": j.get("longitude"),
            "obs_time": c.get("time"),
            "temperature": c.get("temperature_2m"),
            "humidity": c.get("relative_humidity_2m"),
            "windspeed": c.get("wind_speed_10m"),
            "winddir": c.get("wind_direction_10m"),
            "weathercode": c.get("weather_code"),
            "is_day": c.get("is_day"),
        })
    return pd.DataFrame(rows)


def get_output_schema():
    return pd.DataFrame({
        "city": prep_string(),
        "latitude": prep_decimal(),
        "longitude": prep_decimal(),
        "obs_time": prep_string(),
        "temperature": prep_decimal(),
        "humidity": prep_decimal(),
        "windspeed": prep_decimal(),
        "winddir": prep_int(),
        "weathercode": prep_int(),
        "is_day": prep_int(),
    })

加城市只改 seed.csv 加一行,脚本不动。以后接延误分析,把这一层换成「机场 → 经纬度」表,脚本原样复用。我们本地实测 12 城全部拉通,北京时间 16:00 那次,北京 33.8°C、上海 29.5°C、拉萨 17.9°C。

Tableau Prep 脚本拉取 Open-Meteo 后返回的 12 城天气表

几个真坑

TabPy 装完命令找不到。 pip3 install tabpy 之后打 tabpy 报 command not found,因为它装到 ~/Library/Python/3.9/bin,不在 PATH。用全路径启动,或把目录加进 ~/.zshrc 的 PATH。启动后默认监听 localhost:9004,终端窗口别关。

Tableau Prep 连不上 TabPy。 报错「请输入有效的服务器配置」,根因几乎都是 Host 填法:Host 框只认主机名,把 http://127.0.0.1:9004 整串塞进去会被判无效。分开填:Host 127.0.0.1、Port 9004。用 127.0.0.1 不用 localhost,能绕开 Mac 上 localhost 优先解析 IPv6 的坑。不勾 SSL,不填账号密码。

Open-Meteo 只认经纬度。 forecast 接口不吃城市名,必须给 latitude/longitude,所以上游表得带这两列。

时区要显式写。 不传 timezone,接口默认返 UTC,和航班表的北京时间差 8 小时,Join 必错。加上 timezone=Asia/Shanghai,返回的就是北京时间。

参数用新的 `current=`. 旧的 current_weather=true 还能用,但官方已转向 current=字段列表,返回更全。新流程直接上新的。

能复用到哪

这套骨架不限于天气。任何 Tableau Prep 没原生连接器、但能 HTTP 取到的数据源,汇率、股价、航班状态、内部 REST 接口,都能用同一个模子接:占位种子(或真实输入表)+ TabPy 脚本 requests 拉数 + get_output_schema 声明字段。

参考文档

• Tableau 帮助:在流程中使用 R 和 Python 脚本(官方文档)

• Vizwise:一文玩转 Tableau Prep 调用 Python 脚本全流程

• Open-Meteo Forecast API 文档

No comments yet