【问题标题】:Complex data manipulation in ClojureClojure 中的复杂数据操作
【发布时间】:2016-08-03 07:52:41
【问题描述】:

我正在从事一个个人市场分析项目。我有一个代表市场最近所有转折点的数据结构,如下所示:

[{:high 1.121455, :time "2016-08-03T05:15:00.000000Z"}
 {:low 1.12109, :time "2016-08-03T05:15:00.000000Z"}
 {:high 1.12173, :time "2016-08-03T04:30:00.000000Z"}
 {:high 1.121925, :time "2016-08-03T00:00:00.000000Z"}
 {:high 1.12215, :time "2016-08-02T23:00:00.000000Z"}
 {:high 1.12273, :time "2016-08-02T21:15:00.000000Z"}
 {:high 1.12338, :time "2016-08-02T18:15:00.000000Z"}
 {:low 1.119215, :time "2016-08-02T12:30:00.000000Z"}
 {:low 1.118755, :time "2016-08-02T12:00:00.000000Z"}
 {:low 1.117575, :time "2016-08-02T06:00:00.000000Z"}
 {:low 1.117135, :time "2016-08-02T04:30:00.000000Z"}
 {:low 1.11624, :time "2016-08-02T02:00:00.000000Z"}
 {:low 1.115895, :time "2016-08-01T21:30:00.000000Z"}
 {:low 1.11552, :time "2016-08-01T11:45:00.000000Z"}
 {:low 1.11049, :time "2016-07-29T12:15:00.000000Z"}
 {:low 1.108825, :time "2016-07-29T08:30:00.000000Z"}
 {:low 1.10839, :time "2016-07-29T08:00:00.000000Z"}
 {:low 1.10744, :time "2016-07-29T05:45:00.000000Z"}
 {:low 1.10716, :time "2016-07-28T19:30:00.000000Z"}
 {:low 1.10705, :time "2016-07-28T18:45:00.000000Z"}
 {:low 1.106875, :time "2016-07-28T18:00:00.000000Z"}
 {:low 1.10641, :time "2016-07-28T05:45:00.000000Z"}
 {:low 1.10591, :time "2016-07-28T01:45:00.000000Z"}
 {:low 1.10579, :time "2016-07-27T23:15:00.000000Z"}
 {:low 1.105275, :time "2016-07-27T22:00:00.000000Z"}
 {:low 1.096135, :time "2016-07-27T18:00:00.000000Z"}]

从概念上讲,我想匹配:high/:low 对,计算出价格范围(高低)和中点(高低的平均值),但我不希望每个可能的对都是生成。

我想做的是从集合{:high 1.121455, :time "2016-08-03T05:15:00.000000Z"} 中的第一个项目开始,然后“向下”遍历集合的其余部分,与每个 :low 项目创建一对,直到我点击下一个 :high 项目.一旦我点击了下一个:high 项目,我对任何其他配对都不感兴趣。在这种情况下,只创建了一对,即:high 和第一个:low - 我停在那里,因为下一个(第三个)项目是:high。生成的 1 条记录应类似于 {:price-range 0.000365, :midpoint 1.121272, :extremes [{:high 1.121455, :time "2016-08-03T05:15:00.000000Z"}{:low 1.12109, :time "2016-08-03T05:15:00.000000Z"}]}

接下来,我将移动到集合 {:low 1.12109, :time "2016-08-03T05:15:00.000000Z"} 中的第二个项目,然后“向下”遍历集合的其余部分,与每个 :high 项目创建一对,直到我点击下一个 :low 项目。在这种情况下,我生成了 5 条新记录,分别是 :low 和接下来的 5 个 :high 项目,它们都是连续的;这 5 条记录中的第一个看起来像

{:price-range 0.000064, :midpoint 1.12131, :extremes [{:low 1.12109, :time "2016-08-03T05:15:00.000000Z"}{:high 1.12173, :time "2016-08-03T04:30:00.000000Z"}]}

这 5 条记录中的第二条看起来像

{:price-range 0.000835, :midpoint 1.1215075, :extremes [{:low 1.12109, :time "2016-08-03T05:15:00.000000Z"}{:high 1.121925, :time "2016-08-03T00:00:00.000000Z"}]}

等等。

在那之后,我收到了一个:low,所以我停在那里。

然后我会移动到第三个项目{:high 1.12173, :time "2016-08-03T04:30:00.000000Z"} 并走“下”与每个:low 创建对,直到我点击下一个:high。在这种情况下,我生成了 0 对,因为 :high 后面紧跟着另一个 :high。接下来的 3 个 :high 项也是如此,所有这些项都紧随其后的是另一个 :high

接下来我会看到第 7 个项目 {:high 1.12338, :time "2016-08-02T18:15:00.000000Z"},它应该与以下 20 个 :low 项目中的每一个生成一对。

我生成的结果将是创建的所有对的列表:

[{:price-range 0.000365, :midpoint 1.121272, :extremes [{:high 1.121455, :time "2016-08-03T05:15:00.000000Z"}{:low 1.12109, :time "2016-08-03T05:15:00.000000Z"}]}
 {:price-range 0.000064, :midpoint 1.12131, :extremes [{:low 1.12109, :time "2016-08-03T05:15:00.000000Z"}{:high 1.12173, :time "2016-08-03T04:30:00.000000Z"}]}
 ...

如果我使用 Python 之类的东西来实现它,我可能会使用几个嵌套循环,当我不再看到 :highs 与我的 :low 配对时,使用 break 退出内部循环反之亦然,并在我遍历 2 个循环时将所有生成的记录累积到一个数组中。我只是想不出一个使用 Clojure 攻击它的好方法......

有什么想法吗?

【问题讨论】:

  • 一般建议:将问题分解为小函数并查看reduce/reduced + 用法示例。
  • 这是常见的面试问题“直方图能容纳多少水”的另一种形式,我想我会用这个来让人们更抽象地思考它
  • 继续使用你的嵌套循环和中断。在 Clojure 中,您通常会改用 looprecur

标签: clojure destructuring


【解决方案1】:

首先你可以改写如下:

  1. 你必须找到所有的边界点,其中:high后面跟着:low,反之亦然
  2. 您需要在绑定之前获取项目,并用它和绑定之后的每个项目制作一些东西,但直到下一个切换绑定。

为简单起见,让我们使用以下数据模型:

(def data0 [{:a 1} {:b 2} {:b 3} {:b 4} {:a 5} {:a 6} {:a 7}])

第一部分可以通过使用partition-by 函数来实现,每次函数更改处理项的值时,它都会拆分输入集合:

user> (def step1 (partition-by (comp boolean :a) data0))
#'user/step1
user> step1
(({:a 1}) ({:b 2} {:b 3} {:b 4}) ({:a 5} {:a 6} {:a 7}))

现在您需要获取这些组中的每两个并操纵它们。组应该是这样的: [({:a 1}) ({:b 2} {:b 3} {:b 4})] [({:b 2} {:b 3} {:b 4}) ({:a 5} {:a 6} {:a 7})]

这是通过partition函数实现的:

user> (def step2 (partition 2 1 step1))
#'user/step2
user> step2
((({:a 1}) ({:b 2} {:b 3} {:b 4})) 
 (({:b 2} {:b 3} {:b 4}) ({:a 5} {:a 6} {:a 7})))

您必须为每对组做一些事情。你可以用地图来做:

user> (def step3 (map (fn [[lbounds rbounds]]
                    (map #(vector (last lbounds) %)
                         rbounds))
                  step2))
#'user/step3
user> step3
(([{:a 1} {:b 2}] [{:a 1} {:b 3}] [{:a 1} {:b 4}]) 
 ([{:b 4} {:a 5}] [{:b 4} {:a 6}] [{:b 4} {:a 7}]))

但由于您需要串联列表,而不是分组列表,您可能希望使用mapcat 而不是map

user> (def step3 (mapcat (fn [[lbounds rbounds]]
                           (map #(vector (last lbounds) %)
                                rbounds))
                         step2))
#'user/step3
user> step3
([{:a 1} {:b 2}] 
 [{:a 1} {:b 3}] 
 [{:a 1} {:b 4}] 
 [{:b 4} {:a 5}] 
 [{:b 4} {:a 6}] 
 [{:b 4} {:a 7}])

这就是我们想要的结果(几乎是这样,因为我们只生成向量,而不是地图)。

现在您可以使用线程宏来美化它:

(->> data0
     (partition-by (comp boolean :a))
     (partition 2 1)
     (mapcat (fn [[lbounds rbounds]]
               (map #(vector (last lbounds) %)
                    rbounds))))

这会给你完全相同的结果。

应用于您的数据,它看起来几乎相同(另一个结果生成 fn)

user> (defn hi-or-lo [item]
        (item :high (item :low)))
#'user/hi-or-lo
user> 
(->> data
     (partition-by (comp boolean :high))
     (partition 2 1)
     (mapcat (fn [[lbounds rbounds]]
               (let [left-bound (last lbounds)
                     left-val (hi-or-lo left-bound)]
                 (map #(let [right-val (hi-or-lo %)
                             diff (Math/abs (- right-val left-val))]
                         {:extremes [left-bound %]
                          :price-range diff
                          :midpoint (+ (min right-val left-val)
                                       (/ diff 2))})
                      rbounds))))
     (clojure.pprint/pprint))

它打印以下内容:

({:extremes
  [{:high 1.121455, :time "2016-08-03T05:15:00.000000Z"}
   {:low 1.12109, :time "2016-08-03T05:15:00.000000Z"}],
  :price-range 3.6500000000017074E-4,
  :midpoint 1.1212725}
 {:extremes
  [{:low 1.12109, :time "2016-08-03T05:15:00.000000Z"}
   {:high 1.12173, :time "2016-08-03T04:30:00.000000Z"}],
  :price-range 6.399999999999739E-4,
  :midpoint 1.12141}
 {:extremes
  [{:low 1.12109, :time "2016-08-03T05:15:00.000000Z"}
   {:high 1.121925, :time "2016-08-03T00:00:00.000000Z"}],
  :price-range 8.350000000001412E-4,
  :midpoint 1.1215074999999999}
 {:extremes
  [{:low 1.12109, :time "2016-08-03T05:15:00.000000Z"}
   {:high 1.12215, :time "2016-08-02T23:00:00.000000Z"}],
  :price-range 0.001060000000000061,
  :midpoint 1.12162}
 {:extremes
  [{:low 1.12109, :time "2016-08-03T05:15:00.000000Z"}
   {:high 1.12273, :time "2016-08-02T21:15:00.000000Z"}],
  :price-range 0.0016400000000000858,
  :midpoint 1.12191}
 {:extremes
  [{:low 1.12109, :time "2016-08-03T05:15:00.000000Z"}
   {:high 1.12338, :time "2016-08-02T18:15:00.000000Z"}],
  :price-range 0.0022900000000001253,
  :midpoint 1.1222349999999999}
 {:extremes
  [{:high 1.12338, :time "2016-08-02T18:15:00.000000Z"}
   {:low 1.119215, :time "2016-08-02T12:30:00.000000Z"}],
  :price-range 0.004164999999999974,
  :midpoint 1.1212975}
 {:extremes
  [{:high 1.12338, :time "2016-08-02T18:15:00.000000Z"}
   {:low 1.118755, :time "2016-08-02T12:00:00.000000Z"}],
  :price-range 0.004625000000000101,
  :midpoint 1.1210675}
 ...

作为关于“复杂数据操作”的问题的答案,我建议您从 clojure 核心查看所有集合的操作功能,然后尝试将任何任务分解为这些应用程序。没有太多情况下您需要超出它们的东西。

【讨论】:

  • 很棒的答案 - 谢谢你的课!我可以按照您的想法将问题分解为一系列较小的问题,并且我很确定我对 Clojure 数据操作的理解至少提高了 2 个级别,现在我了解了解决方案。现在我要按照你的建议去做 - (再次)通过 clojure.core 中的一组集合操作函数工作,并尝试吸收它们如何像你如此清晰地概述的那样结合在一起
猜你喜欢
  • 1970-01-01
  • 2011-05-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-11-23
  • 2011-06-25
相关资源
最近更新 更多