Build an AI Data Analyst That Thinks Like a Senior Analyst
TL;DR · AI 摘要
构建六阶段AI数据分析师工具,通过验证数据量和生成SQL查询提升分析可靠性。
核心要点
- 使用六阶段流程确保AI分析结果的可信度,包括数据验证和SQL生成
- 工具兼容Anthropic和OpenAI API,支持自定义密钥集成
- 小数据集(如29行记录)能暴露快速回答的缺陷
结构提纲
按章节快速跳转。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- AI数据分析师工具
- 六阶段流程
- 业务理解
- SQL验证
- 数据量检查
- 技术实现
- Python工具包
- LLM API集成
- 数据案例
- online_orders.csv
金句 / Highlights
值得收藏与分享的关键句。
高级分析师会检查结果背后的数据量,而chatbot可能直接给出未经验证的结论
六阶段流程确保每个分析步骤都经过验证,避免基于少量数据的错误决策
29行记录的数据集能暴露快速回答在统计显著性判断上的缺陷
构建一个像高级分析师一样思考的AI数据分析师 - KDnuggets
publ: 2026年9月9日
- 博客热门文章
- 主题 AI 职业建议 计算机视觉 数据工程 数据科学 语言模型 机器学习 MLOps NLP 编程 Python SQL
- 数据集
- 活动
- 资源 快速参考指南 推荐 技术简报
- 广告
加入新闻通讯
#header end
/ad_wrapper
构建一个像高级分析师一样思考的AI数据分析师
一个在得出答案前会验证数据的六阶段流程。
作者:
Nate Rosidi
,KDnuggets市场趋势与SQL内容专家,2026年9月9日发布于
人工智能
<div class="addthis_native_toolbox"></div>
向聊天机器人询问"我们应该多运行哪种促销活动",它会立即给出答案。它选择一个数字,自信地陈述,然后停止。它选择看起来数字最好的促销活动,并自信地表达选择。但它可能从未检查这个数字背后的数据量。基于10笔订单看起来很好的促销活动,远不如在1000笔订单中表现良好的促销活动有说服力。
高级分析师故意放慢速度。他们会重新表述问题,形成假设,编写查询,然后在向高管汇报前检查结果是否有足够的数据支撑。
我们可以将这种严谨性编码到程序中。
在本教程中,我们将构建一个小型Python工具包,通过六个阶段处理问题,而不是一个提示:业务理解、假设生成、SQL规划、验证、高管摘要和建议。
该工具包兼容Anthropic或OpenAI API,因此您需要自行提供密钥。指向任何表格,它都会运行相同的六个阶段。
以下所有代码按顺序执行,从加载CSV到最终建议,因此您可以在自己的数据上使用笔记本进行跟踪。
数据
在本文中,我们将使用名为online_orders.csv的数据表。您可以在StrataScratch面试问题中查看此数据集。它包含29行订单级数据:售出的产品、应用的促销活动、每单位成本、客户、日期和售出单位数。
product_id
promotion_id
cost_in_dollars
customer_id
date_sold
units_sold
1
2
2022-04-01
4
3
6
2022-05-24
10
2022-05-01
9
…
5
8
15
首先,我们使用Pandas加载它:
import pandas as pd
from IPython.display import display
orders = pd.read_csv("online_orders.csv")
print(f"Loaded {len(orders):,} rows and {len(orders.columns)} columns.")
display(orders.head())#### 输出
Loaded 29 rows and 6 columns.跨越3个月、4个促销活动和11个产品的29个订单。这个数据集足够小,以至于每个分组中的每个组都非常重要,这正是快速答案容易出错的那种数据集。
#### 检查模式
在接触任何大型语言模型(LLM)之前,我们先查看表中实际包含的内容:
schema_preview = pd.DataFrame({
"column": orders.columns,
"dtype": orders.dtypes.astype(str).values,
"missing_values": orders.isna().sum().values,
})
display(schema_preview)column
dtype
missing_values
int64
0
object
没有缺失值,且date_sold存储为文本而不是真正的日期。
确定性合理性检查
在调用任何LLM之前,普通的SQL已经能告诉我们一些信息。我们使用DuckDB注册数据框,这使我们能够在不设置数据库服务器的情况下对其运行真正的SQL。
import duckdb
con = duckdb.connect()
con.register("online_orders", orders)
preview = con.execute("""
SELECT
promotion_id,
COUNT(*) AS n_orders,
SUM(units_sold) AS total_units,
SUM(cost_in_dollars * units_sold) AS total_revenue,
ROUND(AVG(units_sold), 2) AS avg_units_per_order
FROM online_orders
GROUP BY promotion_id
ORDER BY avg_units_per_order DESC
""").df()
display(preview)订单数量
总销售单位数
总营收
每单平均销售单位数
8.0
64.0
8.00
12
77.0
407.0
6.42
55.0
199.0
5.50
31.0
185.0
5.17
按每单平均销售单位数排序,促销活动4以8.00位居榜首。
它后面紧跟着恰好1个订单。如果有人问"哪个促销活动的平均表现最好",并立即根据单个订单的强度推荐促销活动4,这就是整个流水线其余部分要避免的陷阱。
LLM包装器
流水线不应该关心你给它的是Anthropic客户端还是OpenAI客户端。一个轻量级包装器会显式指定提供者并调用对应的方法。对于Anthropic,回复可能包含多个内容块,因此它会扫描内容寻找第一个类型为文本的块,而不是假设它在最前面。
class LLMClient:
def __init__(self, client, model, provider):
self.client = client
self.model = model
self.provider = provider
def complete(self, prompt):
if self.provider == "anthropic":
response = self.client.messages.create(
model=self.model,
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
for block in response.content:
if block.type == "text":
return block.text
raise ValueError("No text block found in Claude's response.")
if self.provider == "openai":
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
)
return response.choices[0].message.content
raise ValueError(f"Unsupported provider: {self.provider}")这为流水线其余部分提供了一个统一的complete()方法。特定提供者的响应格式被隐藏在包装器内部,因此后续阶段不需要单独处理Anthropic和OpenAI的代码路径。如果提供者不被支持,或者Claude没有返回可用的文本块,包装器会明确失败,而不是静默传递无效响应。
后续每个阶段都会要求模型返回JSON,因此我们需要一个额外的辅助工具从文本回复中提取JSON。一些回复会用三重反引号代码围栏包裹,因此辅助工具会先剥离这些标记,然后回退到扫描文本寻找第一个有效的JSON对象或数组。
import json
import re
def parse_json(text):
text = text.strip()
if text.startswith("```"):
text = re.sub(r"^```(?:json)?\s*", "", text, flags=re.IGNORECASE)
text = re.sub(r"\s*```$", "", text)
try:
return json.loads(text)
except json.JSONDecodeError:
pass
candidates = []
object_match = re.search(r"\{.*\}", text, re.DOTALL)
array_match = re.search(r"\[.*\]", text, re.DOTALL)
if object_match:
candidates.append(object_match)
if array_match:
candidates.append(array_match)
candidates.sort(key=lambda match: match.start())
for match in candidates:
try:
return json.loads(match.group(0))
except json.JSONDecodeError:
continue
raise ValueError(f"No valid JSON found in model output:\n{text}")解析器从最简单的情况开始:如果整个回复本身就是有效的 JSON,会立即返回。如果失败,它会寻找嵌入在周围文本中的对象或数组,并按出现顺序尝试候选内容。这种方式使处理流程对常见的模型格式错误更加宽容,但仍然会在没有可用 JSON 时抛出错误。
第一阶段:业务理解
第一阶段会用表实际能回答的表述方式重述问题,明确数据粒度,并列出任何分析开始前的限制条件。
class SeniorAnalyst:
MIN_SUPPORT = 3 # 仅在组别订单数达到此阈值后才信任结果
def __init__(self, llm, table_name, dataframe):
self.llm = llm
self.table_name = table_name
self.con = duckdb.connect()
self.con.register(table_name, dataframe)
self.schema = self.con.execute(f"DESCRIBE {table_name}").df()
def understand_business_context(self, question):
row_count = self.con.execute(
f"SELECT COUNT(*) FROM {self.table_name}"
).fetchone()[0]
columns = self.schema[
["column_name", "column_type"]
].to_dict("records")
prompt = f"""你是一位资深数据分析师。利益相关者提出问题:"{question}"
表:{self.table_name}
列:{columns}
行数:{row_count}
用这张表实际能回答的表述方式重述利益相关者的问题。
同时说明表的粒度(一行代表什么),并列出你已经看到的任何限制条件:样本量、时间范围、缺失维度、缺失上下文等。
仅返回 JSON: {{"restated_question": "...", "grain": "...",
"limitations": ["...", "..."]}}"""
context = parse_json(self.llm.complete(prompt))
self.context = context
return context我们在处理"我们应该多运行哪种促销活动"这个问题时使用了claude-sonnet-5模型。以下是返回的结果。
它在运行任何查询之前就指出了样本量过小的问题——这与上面普通的SQL分组聚合已经显示的陷阱相同。这个提示只是一个线索,而非验证。处理流程仍然需要在代码中强制执行该限制,这就是下面验证阶段要做的事情。
第二阶段:假设生成
第二阶段会使用表中已有的列,提出具体且可验证的假设。
def generate_hypotheses(self, n=2):
columns = list(self.schema["column_name"])
prompt = f"""Business context: {self.context}
Propose {n} specific, testable hypotheses that would help answer the
restated question, using only columns in: {columns}.
Each hypothesis should be something we can test using SQL.
Return JSON only: [{{"hypothesis": "...", "why": "..."}}, ...]""
hypotheses = parse_json(self.llm.complete(prompt))
self.hypotheses = hypotheses
return hypotheses该流程会验证第一个假设。请注意这不是简单的平均值计算:它关注的是销量领先者是否以实际差距超越第二名,这与上面"最高平均值"查询中将一个等级的促销置于首位的表述已有明显区别。
阶段3:SQL规划
第三阶段将最佳假设转化为实际查询。我们要求同时返回行数和任何分组指标,因为验证阶段接下来会检查分组规模。
def plan_sql(self, hypothesis):
columns = list(self.schema["column_name"])
prompt = f"""Table: {self.table_name}
Columns: {columns}
Hypothesis to test: {hypothesis['hypothesis']}
Write one DuckDB SQL query that tests this hypothesis.
Use only the available columns, do not invent columns, and if the query
groups rows, include a COUNT(*) column named n_orders so the result can
be checked for sample size before anyone trusts it.
Return JSON only: {{"sql": "...", "purpose": "..."}}"""
plan = parse_json(self.llm.complete(prompt))
return plan生成的SQL:
WITH promo_sums AS (
SELECT promotion_id, SUM(units_sold) AS total_units, COUNT(*) AS n_orders
FROM online_orders
GROUP BY promotion_id
),
ranked AS (
SELECT promotion_id, total_units, n_orders,
RANK() OVER (ORDER BY total_units DESC) AS rnk
FROM promo_sums
)
SELECT
r1.promotion_id AS top_promotion_id,
r1.total_units AS top_total_units,
r1.n_orders AS top_n_orders,
r2.promotion_id AS second_promotion_id,
r2.total_units AS second_total_units,
r2.n_orders AS second_n_orders,
(r1.total_units - r2.total_units) * 1.0 / r2.total_units AS pct_difference
FROM ranked r1
JOIN ranked r2 ON r2.rnk = 2
WHERE r1.rnk = 1
'purpose': 'Identify the promotion_id with the highest total units
sold and compare it to the second-highest to test whether it exceeds
it by at least 20%, including order counts to assess statistical
support.'与简单的group by不同,模型采用了带窗口函数的通用表表达式(CTE),按总销量对促销活动进行排名,并将前两名放在同一行进行比较。
阶段4:验证
第四阶段运行查询并检查n_orders是否达到最低支持阈值。这是唯一一个纯代码阶段而非模型调用,因为该检查必须强制执行而非仅作建议。
def validate(self, sql_plan):
result = self.con.execute(sql_plan["sql"]).df()
if "n_orders" in result.columns:
result["low_confidence"] = result["n_orders"] < self.MIN_SUPPORT
else:
result["low_confidence"] = False
return resulttop_promotion_id
top_total_units
top_n_orders
second_promotion_id
second_total_units
second_n_orders
pct_difference
low_confidence
0.4
False
此查询仅生成一行数据,且未被标记。促销活动1以总销量领先,有12个订单支持,促销活动2以10个订单排名第二,两者均超过了我们设定的最低3个订单门槛。检查仍然在此处执行——只是没有需要捕获的内容,因为此假设比较的是两个有充分数据支持的群体,而非依赖促销活动4的单一订单。
阶段5:高管摘要
第五阶段生成摘要,明确要求将任何被标记的行排除在主要结论之外。
def summarize(self, hypothesis, validated_result):
flagged = validated_result[validated_result["low_confidence"]]
prompt = f"""假设:{hypothesis['hypothesis']}
查询结果:
{validated_result.to_string(index=False)}
被标记为低置信度的行支持的订单数量少于{self.MIN_SUPPORT}个
不应作为结论依据。
低置信度行:{flagged.to_dict('records')}
根据结果撰写简洁的3到4句高管摘要
结论仅基于展示的数据,明确避免
将低置信度行作为主要结论,并且不要
虚构数据不支持的解释。"""
return self.llm.complete(prompt)阶段6:建议
第六阶段提出行动建议,同样明确要求遵循相同规则:任何建议都不能基于低置信度数据或摘要未支持的事实。
def recommend(self, summary):
prompt = f"""高管摘要:{summary}
根据摘要支持的内容提出2到3个具体业务建议
建议必须基于证据,不得依赖低置信度数据
或虚构事实,若证据薄弱,应建议进一步分析
而不是假装结论已明确。"""
return self.llm.complete(prompt)整合流程
运行方法将六个阶段串联起来。一次调用接收一个问题并返回所有中间结果:上下文、假设、SQL计划、验证后的表格、摘要和建议。
def run(self, question):
context = self.understand_business_context(question)
hypotheses = self.generate_hypotheses()
top_hypothesis = hypotheses[0]
plan = self.plan_sql(top_hypothesis)
validated = self.validate(plan)
summary = self.summarize(top_hypothesis, validated)
recommendation = self.recommend(summary)
return {
"context": context,
"hypotheses": hypotheses,
"sql_plan": plan,
"validated_result": validated,
"summary": summary,
"recommendation": recommendation,
}调用方式
无论使用哪个服务提供商,调用方式都保持一致。服务提供商需要显式指定而非从客户端对象猜测,如果忘记粘贴真实密钥,流程将拒绝执行。
PROVIDER = "anthropic"
API_KEY = "YOUR_API_KEY_HERE"
ANTHROPIC_MODEL = "claude-sonnet-5"
OPENAI_MODEL = "gpt-4o"
if API_KEY == "YOUR_API_KEY_HERE":
raise ValueError(
"在运行LLM部分之前,请将真实的API密钥粘贴到API_KEY中。"
)
if PROVIDER.lower() == "anthropic":
from anthropic import Anthropic
client = Anthropic(api_key=API_KEY)
llm = LLMClient(client=client, model=ANTHROPIC_MODEL, provider="anthropic")
elif PROVIDER.lower() == "openai":
from openai import OpenAI
client = OpenAI(api_key=API_KEY)
llm = LLMClient(client=client, model=OPENAI_MODEL, provider="openai")
else:
raise ValueError("PROVIDER必须是'openai'或'anthropic'之一。")
analyst = SeniorAnalyst(llm, "online_orders", orders)
result = analyst.run("我们应该增加哪个促销活动?")
print(result["summary"])
print(result["recommendation"])将PROVIDER设置为openai,插入一个OpenAI密钥,相同的六个阶段会无变化地针对gpt-4o运行。LLMClient是唯一知道它正在与哪个API通信的部分。
结论
这里六个阶段中的每一个单独来看都不复杂。重述问题、编写SQL、总结表格,这些都是单个提示已经能合理完成的事情。价值来自于查询和总结之间的验证阶段——在任何内容被称作答案之前,先检查n_orders。
在这个数据集上,即使在LLM被调用之前,这个检查就已经发现了问题:上面的普通SQL groupby按订单平均单位数对促销活动进行排序时,促销活动4仅基于一个订单就排名第一。这次模型选择测试的假设比较了两个更有依据的组——12个订单对10个订单——因此validate()没有需要标记的问题。无论模型提供什么比较,这个流程都会执行相同的n_orders检查,因此未来的表格或未来测试平均值而非总数的运行,都会被相同的代码行捕获。
这个流程在一个类上有6个方法,当指向下一个表格时,相同的6个方法会再次运行。
Nate Rosidi是一名数据科学家,目前从事产品战略工作。他还是一个兼职教授,教授分析课程,同时也是StrataScratch的创始人,这是一个帮助数据科学家通过顶级公司真实面试题准备面试的平台。Nate撰写关于职业市场最新趋势的文章,提供面试建议,分享数据科学项目,并涵盖所有与SQL相关的内容。
更多相关内容
- 像软件工程师一样测试SQL:单元测试、CI/CD和数据…
- 10个鲜为人知的Python库,它们会让你感觉自己像数据巫师
- 7个看似玩笑的Python内置函数(直到你使用它们)
- 新手指南:Gemini CLI的安装、设置和专业使用
- 像专业人士一样调试和追踪LLM
- 如何像专业人士一样使用深度研究
<hr class="grey-line"><br> <div><h3>我们推荐的5门顶级免费课程</h3><br> </div>
Mailchimp for WordPress v4.14.0 - https://wordpress.org/plugins/mailchimp-for-wp/
/ Mailchimp for WordPress插件
你可以从这里开始编辑。
如果评论已关闭。
<= 上一篇
#content end
<script type="text/javascript">kda_sid_write(kda_sid_n);</script>
最新文章
/
- 打造一个思维如资深分析师的AI数据分析师 7种在有限硬件上高效训练LLM的方法 从RAG到自主智能AI:构建下一代智能企业系统 ArrowJS真的是自主智能时代的用户界面吗?我的发现 5种免费访问编码模型的方法 Switchyard:NVIDIA的开源路由库
热门文章
- 5门免费课程助你从LLM入门者进阶为实践者
- 2026年可使用的5个免费LLM API提供商
- ArrowJS真的是自主智能时代的用户界面吗?我的发现
- 自主智能AI在企业自动化中的5个实际应用案例
- 5种免费访问编码模型的方法
- 从RAG到自主智能AI:构建下一代智能企业系统
- 2026年可本地运行的7大编码模型
- 我让ChatGPT分析3个数据集,它每次都犯同样的错误
- 停止使用if-else链:在Python中改用注册表模式
- 从AI编码代理获取更好结果的10条规则
#content_wrapper end
© 2026
Guiding Tech Media
|
关于
联系我们
广告合作
隐私政策
服务条款
2026年9月9日由Nate Rosidi发布
blank
不,谢谢!
/.main_wrapper
<script defer type="text/javascript" src="https://s7.addthis.com/js/300/addthis_widget.js#pubid=gpsaddthis"></script>
noptimize
/noptimize