運用Map merge來加總多種統計資料

前陣子在Java的推特看到一篇文章 Map merge and compute, hidden API diamonds,討論Java API的Map功能中一組比較常被忽略的工具computemerge,而這陣子因為備課的關係常撈取各國COVID-19病毒相關的統計資料,剛好也用到merge這支程式,因此就順勢來做一下進階應用案例探討。

Map是一種由key-value pairs組成的資料結構,傳統上要創建一個這樣的結構大致上是這樣:

Map<String, Integer> myMap = new HashMap<>();
myMap.put("John", 32);
myMap.put("Mary", 40);

在Java 9時,Map的功能被增強了不少,因此可以使用像下面的語法產生上面的資料:
Map<String, Integer> myMap = Map.of("John", 32, "Mary", 40);

不過有點不同的是,Map.of創建的資料是immutable的,因此內容無法再做任何變動。而在撈取一串統計數據時,資料可能長得像這樣:
"A","US",1,1,1,4,4,5,7,7,7,11,16,21,22,22,22,24, ...
"B","US",0,0,0,0,2,10,12,23,33,38,42,51,55,59,64,70, ...
...

以上代表同一個國家"US"的不同省份(province)"A""B",後面則是每日的統計資料(以這裡為例是COVID-19每日的確診人數)。撈取之後可能會把它存在像下面這樣的結構中:
Map<String, List<Integer>> confirmed = Map.of(
   "A", List.of(1, 1, 1, 4, 4, 5, 7, 7, 7, 11, 16, 21, 22, 22, 22, 24, ...), 
   "B", List.of(0, 0, 0, 0, 2, 10, 12, 23, 33, 38, 42, 51, 55, 59, 64, 70, ...));

假定還有另一組每日死亡人數的資料,結構很類似,像這樣:
Map<String, List<Integer>> deaths = Map.of(
   "A", List.of(0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, ...), 
   "B", List.of(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...));
這裡的List.of()同樣也是Java 9對List所做的增強。

假定我們已將上述的國家/省份/確診及死亡等數據匯整成一個資料集合,接下來想把這些數字加總成各國的全國性數據,存在以國家為key的Map結構中,其對應的value為另一個Map,以"confirmed"確診"deaths"死亡)為key、分別對應以每日統計人數的Listvalue的資料結構:
Map<String, Map<String, List<Integer>>> country_updated

例如:
"US":{"confirmed":[1, 1, 1, 4, 6, 15, 19, 30, ...],
      "deaths":[0, 0, 0, 0, 0, 0, 1, 1, ...]}

因為這裡只是要得到全國加總,因此已略去省份名稱,也就是過程中同一國家可能對應多筆不同省份的資料。對於原本空的變數country_updated來說,遇到第一筆資料時是新增進去,而第二筆以後就要加總進去,因此這裡使用merge較為合適。

merge的簽章有些複雜,如下:

default V merge(K key, V value,
                BiFunction<? super V,? super V,? extends V> remappingFunction)

特性就是當這裡的key, value關係尚不存在時,就將它們直接存進去;若存在的話則將舊值取出,連同待加進去的value一起導入remappingFunction來算出一個新的值以存進去,並且覆蓋舊值。這裡的remappingFunction是一個BiFunction,也就是須傳入兩個參數的函數,而傳入的順序為取出的舊值在先、待加入的值在後。

為了簡化程式碼,我們還需要一段小程式,將兩個Map中同keyList取出,並將所有的元素相加,包成一道Map Entry回傳:
<K> Entry<K, List<Integer>> sum(K key, 
                                Map<K, List<Integer>> m1, 
                                Map<K, List<Integer>> m2) {
   return Map.entry(key, IntStream.range(0, m1.get(key).size())
                            .map(i -> m1.get(key).get(i) + m2.get(key).get(i))
                            .boxed()
                            .collect(Collectors.toList()));
}

假設我們將預備加入國名為country的省份資料存在變數province

Map<String, List<Integer>> province = Map.of("confirmed", confirmed, "deaths", deaths);
(留意其結構與目的地相同,即country_updated外層Map的值結構

那麼將這個省份的資料加進country_updated中的程式碼就可以寫成:

country_updated.merge(country, province, 
                      (o, n)->Map.ofEntries(
                                     sum("confirmed", o, n),
                                     sum("deaths", o, n)));

country尚未存在country_update中,則會直接將province的內容存進去;若存在的話則會以(o, n)導入BiFunction;而回傳值則是透過sum將兩個導入值的內容一一相加所得到的Map。

以上這個關於Map merge的進階應用案例,不知道是否能幫助你更加了解它的妙用呢?


*使用hilite.me做為code beautifier

留言

這個網誌中的熱門文章

病毒肆虐下、資料集間國名比對的聯想

關於「複合形式」(摘錄自作品首演文件)

以圖形資料建構Star Wars星戰宇宙中的行星們