成为百万富翁的几率 vs. 被公交车撞的几率:一次 ES|QL 分析

178 阅读13分钟

作者:来自 Elastic Baha Azarmi

ES|QL 专为快速、高效地查询大型数据集而设计。它具有简单的语法,可让你使用基于管道的语言轻松编写复杂查询,从而降低学习曲线。我们将使用 ES|QL 运行统计分析并比较不同的几率。

如果你在读这篇文章,你可能想知道在达到与被公交车撞同样几率之前,你能变得多富有。我不怪你,我也想知道。让我们算一算这些几率,这样我们可以确保赢得彩票,而不是发生事故!

我们将在本博客中看到计算被公共汽车撞到的概率和获得财富的概率。然后,我们将比较两者,并了解你致富的机会在什么时候更高,以及你应该何时考虑购买人寿保险。

那么我们要怎么做呢?这将是从网上的不同文章中提取的神奇数字、一些合成数据和 ES|QL(新的 Elasticsearch 查询语言)的强大功能的组合。让我们开始吧。

数据

神奇的数字

挑战从这里开始,因为数据集有点难以找到。然后,为了举例说明,我们假设 ChatGPT 总是正确的。让我们看看以下问题会得到什么:

咳咳……听起来不错,这将是我们的神奇数字。

生成财富数据

先决条件

在运行以下任何脚本之前,请确保安装以下软件包:

 2.  elasticsearch==8.14.0
3.  matplotlib
4.  numpy
5.  panda
6.  scipy

现在,我们还需要一件事,即一个具有财富分布的代表性数据集,以计算财富概率。这里和那里肯定有一些部分,但同样,对于示例,我们将使用以下 Python 脚本生成一个 500K 行的数据集。我在这个例子中使用的是 Python 3.11.5:

 2.  import pandas as pd
3.  import numpy as np
4.  import getpass
5.  from elasticsearch import Elasticsearch, helpers

7.  # Input the Elasticsearch host
8.  hosts = input('Enter your Elasticsearch host address : ')

10.  # Securely input the Elasticsearch API key
11.  api_key = getpass.getpass(prompt='Enter your Elasticsearch API Key: ')

13.  # Initialize Elasticsearch client
14.  client = Elasticsearch(
15.      hosts=hosts,
16.      api_key=api_key,
17.  )

19.  # Generate synthetic data with a highly skewed distribution
20.  num_records = 500000
21.  np.random.seed(42)  # Ensure reproducibility

23.  # Generate net worth using a highly skewed distribution
24.  ages = np.random.randint(20, 80, num_records)  # Random ages between 20 and 80
25.  incomes = np.random.exponential(scale=10000, size=num_records)  # Exponential distribution for income
26.  # Use a more skewed distribution for net worth with a much larger range
27.  net_worths = np.random.exponential(scale=100000000, size=num_records)  # Extremely skewed net worth

29.  # Scale up the net worths to reach up to $100 billion
30.  net_worths = np.clip(net_worths, 0, 100000000000)

32.  # Create DataFrame
33.  df = pd.DataFrame({
34.      'id': range(1, num_records + 1),
35.      'age': ages,
36.      'income': incomes,
37.      'net_worth': net_worths,
38.      'counter': range(1, num_records + 1)  # Add a counter field for pagination
39.  })

41.  # Index the data into Elasticsearch
42.  index_name = 'raw_wealth_data_large'
43.  try:
44.      if client.indices.exists(index=index_name):
45.          client.indices.delete(index=index_name)
46.  except exceptions.NotFoundError:
47.      pass
48.  client.indices.create(index=index_name)

51.  def generator(df):
52.      for index, row in df.iterrows():
53.          yield {
54.              "_index": index_name,
55.              "_source": row.to_dict()
56.          }

58.  helpers.bulk(client, generator(df))

60.  print("Data indexed successfully.")

由于我们在这里注入了 500K 份文档,因此根据你的配置,运行可能需要一些时间!

仅供参考,在使用了上述脚本的几个版本和对合成数据进行 ES|QL 查询后,很明显,整个人口产生的净资产并不能真正代表现实世界。因此,我决定对收入使用对数正态分布 (np.random.lognormal),以反映更现实的分布,其中大多数人的收入较低,而收入很高的人较少。

净资产计算:使用随机乘数 (np.random.uniform(0.5, 5)) 和额外噪声 (np.random.normal(0, 10000)) 的组合来计算净资产。通过使用 np.maximum(0, net_worths) 添加了检查以确保没有负净资产值。

我们不仅生成了 500K 份文档,还使用 ​​Elasticsearch python 客户端在我们的部署中批量提取所有这些文档。请注意,你将在上面的代码中找到要作为主机云 ID 传入的端点。

对于部署 API 密钥,请打开 Kibana,并在 Stack Management / API Keys 中生成密钥:

好消息是,如果你有真实的数据集,你需要做的就是更改上述代码以读取数据集并使用相同的数据映射编写文档。

好的,我们到了那里!下一步是倾注我们的财富分配。

ES|QL 财富分析

介绍 ES|QL:强大的数据分析工具

Elasticsearch 查询语言 (ES|QL) 的出现对我们的用户来说是一个非常令人兴奋的消息。它大大简化了查询、分析和可视化存储在 Elasticsearch 中的数据,使其成为所有数据驱动用例的强大工具。

ES|QL 附带各种函数和运算符,用于执行聚合、统计分析和数据转换。我们不会在本篇博文中介绍所有这些内容,但我们的文档非常详细,将帮助你熟悉该语言及其可能性。

要立即开始使用 ES|QL 并运行博文查询,只需在 Elastic Cloud 上开始试用,加载数据并运行你的第一个 ES|QL 查询。

通过我们的第一个查询了解财富分布

要熟悉数据集,请前往 Kibana 中的 Discover,并在左侧的下拉菜单中切换到 ES|QL:

让我们发出第一个请求:

from raw_wealth_data_large | keep age, id, income, net_worth | limit 10 

正如你从我们之前的索引脚本中所预料的那样,我们正在查找批量提取的文档,请注意使用 ES|QL 从给定数据集中提取数据的简单性,其中每个查询都以 From 子句开头,然后是你的索引。

在上面给出的查询中,我们有 500K 行,我们将返回的文档数量限制为 10。为此,我们通过管道将查询第一段的输出传递给 limit 命令,以仅获得 10 个结果。很直观,对吧?

好吧,更有趣的是了解我们数据集中的财富分布,为此,我们将利用 ES|QL 提供的 30 个函数之一,即百分位数(percentile)。

这将使我们能够了解每个数据点在净资产分布中的相对位置。通过计算中位数百分位数(第 50 个百分位数),我们可以衡量个人的净资产与其他人相比处于什么位置。

1.  FROM raw_wealth_data_large
2.  | stats p50 = percentile(net_worth, 50) 

与我们的第一个查询类似,我们将索引的输出传递给另一个函数 Stats,该函数与百分位数函数结合将输出中位净资产值:

中位数约为 54K,不幸的是,与现实世界相比,这可能过于乐观了,但我们不会在这里解决这个问题。如果我们再进一步,我们可以通过计算更多百分位数来更详细地查看分布:

 2.  FROM raw_wealth_data_large
3.  | STATS  p25 = percentile(net_worth, 25)
4.         , p50 = percentile(net_worth, 50)
5.         , p75 = percentile(net_worth, 75)
6.         , p90 = percentile(net_worth, 90)
7.         , p95 = percentile(net_worth, 95)
8.         , p96 = percentile(net_worth, 96)
9.         , p98 = percentile(net_worth, 98)
10.         , p97 = percentile(net_worth, 97)
11.         , p99 = percentile(net_worth, 99)
12.  | keep p25, p25, p50, p75, p90, p95, p96, p97, p98, p99 

输出如下:

数据显示财富分配存在显著差异,大部分财富集中在最富有的人手中。具体来说,前 5%(第 95 个百分位)的人拥有不成比例的总财富,净资产从 852,988.26 美元开始,并在更高的百分位中急剧增加。

第 99 个百分位的人拥有超过 200 万美元的净资产,凸显了财富分配的不平衡性。这表明相当一部分人口拥有适中的净资产,这可能正是我们在这个例子中想要的。

另一种看待这个问题的方法是增强前面的查询并按年龄分组,看看(在我们的合成数据集中)财富和年龄之间是否存在关系:

 2.  FROM raw_wealth_data_large
3.  | STATS  p25 = percentile(net_worth, 25)
4.        , p50 = percentile(net_worth, 50)
5.        , p75 = percentile(net_worth, 75)
6.        , p90 = percentile(net_worth, 90)
7.        , p95 = percentile(net_worth, 95)
8.        , p96 = percentile(net_worth, 96)
9.        , p98 = percentile(net_worth, 98)
10.        , p97 = percentile(net_worth, 97)
11.        , p99 = percentile(net_worth, 99) by age
12.  | keep p25, p25, p50, p75, p90, p95, p96, p97, p98, p99, age

这可以在 Kibana 仪表板中可视化。只需:

  • 导航到 Dashboard
  • 添加新的 ES|QL 可视化
  • 复制并粘贴我们的查询
  • 将 age 字段移动到可视化配置中的横轴

这将输出:

以上内容表明,数据生成器在整个人口年龄中均匀地随机化财富,我们无法真正看到特定的趋势模式。

中位数绝对偏差 (Median Absolute Deviation - MAD)

我们计算中位数绝对偏差 (MAD),以稳健的方式衡量净值的变动性,较少受到异常值的影响。



1.  FROM raw_wealth_data_large
2.  | stats median_net_worth = MEDIAN(net_worth), mad_net_worth = MEDIAN_ABSOLUTE_DEVIATION(net_worth)
3.  | keep median_net_worth, mad_net_worth


平均净资产为 53,787.22,MAD 为 44,205.44,我们可以推断出净资产的典型范围:大多数个人的净资产在 44,205.44 左右的范围内。
这给出了一个大约 44,205.44 美元的典型范围,高于和低于中位数。这给出了一个典型的范围,大约为 9,581.78 美元到 97,992.66 美元。

净资产和公交车碰撞之间的统计对决

好吧,现在是时候根据我们的数据集了解我们在被公交车撞到之前能赚多少钱了。为此,我们将利用 ES|QL 将整个数据集分成几块并将其加载到 pandas 数据框中以构建净资产概率分布。最后,我们将确定净资产和公交车碰撞概率之间的交汇点。

完整的 Python 笔记本可在此处获得。我还建议你阅读这篇博客文章,它将指导你如何使用 ES|QL 和 pandas dataframes。

辅助函数

正如你在前面提到的博客文章中看到的,我们从 Elasticsearch Python 客户端 8.12 版开始引入了对 ES|QL 的支持。因此,我们的笔记本首先定义了以下函数:

 2.  from io import StringIO

4.  # Function to execute ESQL query and fetch data in chunks
5.  def execute_esql_query(query):
6.      response = client.esql.query(query=query, format="csv")
7.      return pd.read_csv(StringIO(response.body))

9.  # Function to fetch paginated data using the counter field
10.  def fetch_paginated_data(index, num_records, size=10000):
11.      all_data = pd.DataFrame()
12.      for start in range(1, num_records + 1, size):
13.          end = start + size - 1
14.          query = f"""
15.          FROM {index}
16.          | WHERE counter >= {start} AND counter <= {end}
17.          | limit {size}
18.          """
19.          data_chunk = execute_esql_query(query)
20.          all_data = pd.concat([all_data, data_chunk], ignore_index=True)
21.      return all_data 

第一个函数很简单,执行 ES|QL 查询,第二个函数从我们的索引中获取整个数据集。请注意其中的技巧,我使用索引中字段内置的计数器来分页数据。这是我使用的解决方法,而我们的工程团队正在研究 ES|QL 中的分页支持

接下来,知道我们的索引中有 500K 个文档,我们只需调用这些函数将数据加载到数据框中:



1.  # Fetch all data using pagination and ES|QL
2.  num_records = 500000
3.  all_data_df = fetch_paginated_data(index_name, num_records)
4.  print(f"Total Data Retrieved: {len(all_data_df)} records")


拟合 Pareto 分布

接下来,我们将数据拟合到 Pareto 分布中,该分布通常用于模拟财富分布,因为它反映了一小部分人口控制大部分财富的现实。通过将数据拟合到此分布中,我们可以更准确地表示不同净资产水平的概率。



1.  from scipy.stats import pareto

5.  # Fit a Pareto distribution to the data
6.  shape, loc, scale = pareto.fit(all_data_df['net_worth'], floc=0)

8.  # Calculate the probability density for each net worth
9.  all_data_df['net_worth_probability'] = pareto.pdf(all_data_df['net_worth'], shape, loc=loc, scale=scale)

11.  # Normalize the probabilities to sum to 1
12.  all_data_df['net_worth_probability'] /= all_data_df['net_worth_probability'].sum()

14.  print("Data with Net Worth Probability:")
15.  print(all_data_df.head())


我们可以使用下面的代码来可视化 pareto 分布:



1.  import matplotlib.pyplot as plt
2.  from scipy.stats import pareto

4.  # Assuming all_data_df contains the fetched net worth data from Elasticsearch
5.  # Fit a Pareto distribution to the data
6.  shape, loc, scale = pareto.fit(all_data_df['net_worth'], floc=0)

8.  # Plot the Net Worth Probability Distribution
9.  plt.figure(figsize=(10, 6))

11.  # Plot histogram of empirical net worth data
12.  plt.hist(all_data_df['net_worth'], bins=100, density=True, alpha=0.6, color='g', label='Empirical Data')

14.  # Plot fitted Pareto distribution
15.  xmin, xmax = plt.xlim()
16.  x = np.linspace(xmin, xmax, 100)
17.  p = pareto.pdf(x, shape, loc=loc, scale=scale)
18.  plt.plot(x, p, 'k', linewidth=2, label='Fitted Pareto Distribution')

20.  # Show the plot
21.  plt.xlabel('Net Worth')
22.  plt.y bnblabel('Probability')
23.  plt.title('Net Worth Probability Distribution')
24.  plt.legend()
25.  plt.grid(True)
26.  plt.show()


临界点

最后,根据计算出的概率,我们确定与公交车撞车概率相对应的目标净值并将其可视化。请记住,我们使用 ChatGPT 给出的神奇数字来表示被公交车撞到的概率:

 2.  # Find the Net Worth Corresponding to the Bus Hit Probability
3.  target_probability = 0.0000181
4.  cumulative_probability = all_data_df['net_worth_probability'].cumsum()
5.  target_net_worth_df = all_data_df[cumulative_probability >= target_probability].head(1)
6.  target_net_worth = target_net_worth_df['net_worth'].iloc[0]
7.  print(f"Net Worth with Probability >= {target_probability}: {target_net_worth}")

9.  # Plot the Net Worth Probability Distribution
10.  plt.figure(figsize=(10, 6))
11.  plt.hist(all_data_df['net_worth'], bins=100, density=True, alpha=0.6, color='g', label='Empirical Data')
12.  xmin, xmax = plt.xlim()
13.  x = np.linspace(xmin, xmax, 100)
14.  p = pareto.pdf(x, shape, loc=loc, scale=scale)
15.  plt.plot(x, p, 'k', linewidth=2, label='Fitted Pareto Distribution')
16.  plt.axhline(y=target_probability, color='r', linestyle='--', label='Bus Hit Probability')
17.  plt.axvline(x=target_net_worth, color='g', linestyle='--', label=f'Net Worth = {target_net_worth:.2f}')
18.  plt.xlabel('Net Worth')
19.  plt.ylabel('Probability')
20.  plt.title('Net Worth Probability Distribution')
21.  plt.legend()
22.  plt.grid(True)
23.  plt.show()

结论

根据我们的合成数据集,该图表生动地说明了积累约 1250 万美元净资产的概率与被公共汽车撞到的概率一样小。

为了好玩,我们来问问 ChatGPT 这个概率是多少:

好吧……4.39 亿美元?我觉得 ChatGPT 可能又出现幻觉了。

准备好亲自尝试一下了吗?开始免费试用
想要获得 Elastic 认证吗?了解下一期 Elasticsearch 工程师培训何时开课!

原文:Using Elasticsearch Query Language (ES|QL) for statistical analysis — Search Labs