diff --git a/extended/src/main/java/apoc/agg/MultiStats.java b/extended/src/main/java/apoc/agg/MultiStats.java index f69718150a..3c4a19e85b 100644 --- a/extended/src/main/java/apoc/agg/MultiStats.java +++ b/extended/src/main/java/apoc/agg/MultiStats.java @@ -1,8 +1,6 @@ package apoc.agg; import apoc.Extended; -import com.amazonaws.util.NumberUtils; -import org.HdrHistogram.HistogramUtil; import org.neo4j.graphdb.Entity; import org.neo4j.kernel.impl.util.ValueUtils; import org.neo4j.procedure.Description; @@ -10,14 +8,14 @@ import org.neo4j.procedure.UserAggregationFunction; import org.neo4j.procedure.UserAggregationResult; import org.neo4j.procedure.UserAggregationUpdate; -import org.neo4j.values.storable.LongValue; +import org.neo4j.values.AnyValue; import org.neo4j.values.storable.NumberValue; import org.neo4j.values.utils.ValueMath; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; @Extended public class MultiStats { @@ -59,7 +57,7 @@ public static class MultiStatsFunction { // private DoubleHistogram doubles; // private List percentiles = asList(0.5D, 0.75D, 0.9D, 0.95D, 0.9D, 0.99D); // private Number minValue; - private final Map> result = new HashMap<>(); + private final Map>> result = new HashMap<>(); // --> TODO - sum must be similar to https://neo4j.com/docs/cypher-manual/current/functions/aggregating/#functions-sum @@ -73,55 +71,44 @@ public void aggregate( // todo - can be also a map, maybe? Entity entity = (Entity) value; - // per ogni prop + // for each prop keys.forEach(key -> { if (entity.hasProperty(key)) { Object property = entity.getProperty(key); - // todo - forse metterlo all'esterno + result.compute(key, (ignored, v) -> { - Map map; - if (v == null) { - map = new HashMap<>(); - } else { - map = v; - } - - NumberValue count = map.compute("count", ((subKey, subVal) -> (NumberValue) ValueUtils.of(subVal == null ? 1 : subVal.longValue() + 1)) ); + Map> map1 = Objects.requireNonNullElseGet(v, HashMap::new); - if (property instanceof Number propNum) { -// NumberValue -// - NumberValue of = (NumberValue) ValueUtils.of(property); - -// ValueMath.overflowSafeAdd( - -// ValueMath.overflowSafeAdd( ) - + // todo - it can be null? + map1.compute(property.toString(), (propKey, propVal) -> { + + Map map = Objects.requireNonNullElseGet(propVal, HashMap::new); - // todo - double and long must be different ? - NumberValue sum = map.compute("sum", ((subKey, subVal) -> subVal == null ? of : ValueMath.overflowSafeAdd(subVal, of))); + NumberValue count = map.compute("count", ((subKey, subVal) -> (NumberValue) ValueUtils.of(subVal == null ? 1 : subVal.longValue() + 1)) ); - // NB: avg() return always a double - NumberValue avg = map.compute("avg", ((subKey, subVal) -> subVal == null ? of : sum.dividedBy(count.doubleValue()) )); -// NumberValue avg = map.compute("avg", ((subKey, subVal) -> subVal == null ? of : sum.divideBy (count) )); - } + AnyValue of1 = ValueUtils.of(property); - return map; + if (of1 instanceof NumberValue of) { + NumberValue sum = map.compute("sum", ((subKey, subVal) -> subVal == null ? of : ValueMath.overflowSafeAdd(subVal, of))); + + // NB: avg() return always a double + NumberValue avg = map.compute("avg", ((subKey, subVal) -> subVal == null ? of : sum.dividedBy(count.doubleValue()) )); + } + + return map; + }); + + + return map1; }); - -// Map orDefault = result.getOrDefault(key, new HashMap<>()); -// orDefault.compute("count", ((k, v) -> v == null ? 1 : v.longValue() + 1)); -// System.out.println("orDefault = " + orDefault); } }); -// value.getProperty() } - // --> Map> > @UserAggregationResult // apoc.agg.multiStats([key1,key2,key3]) -> Map> - public Map> result() { + public Map>> result() { return result; } } diff --git a/extended/src/test/java/apoc/agg/MultiStatsTest.java b/extended/src/test/java/apoc/agg/MultiStatsTest.java index 872e7a47be..c537cb627f 100644 --- a/extended/src/test/java/apoc/agg/MultiStatsTest.java +++ b/extended/src/test/java/apoc/agg/MultiStatsTest.java @@ -4,6 +4,7 @@ import apoc.util.TestUtil; import apoc.util.Util; import apoc.util.collection.Iterators; +import org.junit.After; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.ClassRule; @@ -27,34 +28,16 @@ public class MultiStatsTest { @BeforeClass public static void setUp() { TestUtil.registerProcedure(db, Maps.class, MultiStats.class); - - db.executeTransactionally(""" - CREATE (n:Person { - louvain: 596, - neo4jImportId: "18349390", - wcc: 48, - lpa: 598, - name: "aaa", - wcc_cypher: 548 - });"""); - db.executeTransactionally(""" - CREATE (n:Person { - louvain: 596, - neo4jImportId: "18349390", - wcc: 48, - lpa: 598, - name: "eee", - wcc_cypher: 549 - });"""); - db.executeTransactionally(""" - CREATE (n:Person { - louvain: 596, - neo4jImportId: "18349390", - wcc: 47, - lpa: 596, - name: "iii", - wcc_cypher: 549 - })"""); + } + + @AfterClass + public static void tearDown() { + db.shutdown(); + } + + @After + public void after() { + db.executeTransactionally("MATCH (n) DETACH DELETE n"); } @Test @@ -83,41 +66,151 @@ public void test2() { }); } - @AfterClass - public static void tearDown() { - db.shutdown(); + @Test + public void test123123123() { + db.executeTransactionally(""" + CREATE (:Person { louvain: 596, neo4jImportId: "18349390", wcc: 48, lpa: 598, name: "aaa", another: 548}), + (:Person { louvain: 596, neo4jImportId: "18349390", wcc: 48, lpa: 598, name: "eee", another: 549}), + (:Person { louvain: 596, neo4jImportId: "18349390", wcc: 48, lpa: 598, name: "eee", another: 549}), + (:Person { louvain: 597, neo4jImportId: "18349391", wcc: 48, lpa: 598, name: "eee", another: 549}), + (:Person { louvain: 597, neo4jImportId: "18349392", wcc: 47, lpa: 596, name: "iii", another: 549}), + (:Person { louvain: 597, neo4jImportId: "18349393", wcc: 47, lpa: 596, name: "iii", another: 549}), + (:Person { louvain: 597, neo4jImportId: "18349394", wcc: 47, lpa: 596, name: "iii", another: 549}), + (:Person { louvain: 597, neo4jImportId: "18349393", wcc: 47, lpa: 596, name: "iii", another: 10}), + (:Person { louvain: 597, neo4jImportId: "18349394", wcc: 47, lpa: 596, name: "iii", another: 10}) + """); + + + db.executeTransactionally(""" + CREATE (:Person { louvain: 596, neo4jImportId: "18349390", wcc: 48, lpa: 598, name: "aaa", another: 548}), + (:Person { louvain: 596, neo4jImportId: "18349390", wcc: 48, lpa: 598, name: "eee", another: 549}), + (:Person { louvain: 596, neo4jImportId: "18349390", wcc: 48, lpa: 598, name: "eee", another: 549}), + (:Person { louvain: 597, neo4jImportId: "18349391", wcc: 48, lpa: 598, name: "eee", another: 549}), + (:Person { louvain: 597, neo4jImportId: "18349392", wcc: 47, lpa: 596, name: "iii", another: 549}), + (:Person { louvain: 597, neo4jImportId: "18349393", wcc: 47, lpa: 596, name: "iii", another: 549}), + (:Person { louvain: 597, neo4jImportId: "18349394", wcc: 47, lpa: 596, name: "iii", another: 549}), + (:Person { louvain: 597, neo4jImportId: "18349393", wcc: 47, lpa: 596, name: "iii", another: 10}), + (:Person { louvain: 597, neo4jImportId: "18349394", wcc: 47, lpa: 596, name: "iii", another: 10}) + """); +// db.executeTransactionally(""" +// CREATE (:Person { louvain: 596, neo4jImportId: "18349390", wcc: 48, lpa: 598, name: "eee", another: 549});"""); +// db.executeTransactionally(""" +// CREATE (:Person { louvain: 596, neo4jImportId: "18349390", wcc: 47, lpa: 596, name: "iii", another: 549})"""); + + + + String s = db.executeTransactionally(""" + MATCH (p:Person) + WITH p + CALL { + WITH p + MATCH (n:Person {louvain: p.louvain}) + RETURN sum(p.louvain) AS sumLouvain, avg(p.louvain) AS avgLouvain, count(p.louvain) AS countLouvain + } + CALL { + WITH p + MATCH (n:Person {wcc: p.wcc}) + RETURN sum(p.wcc) AS sumWcc, avg(p.wcc) AS avgWcc, count(p.wcc) AS countWcc + } + CALL { + WITH p + MATCH (n:Person {another: p.another}) + RETURN sum(p.another) AS sumAnother, avg(p.another) AS avgAnother, count(p.another) AS countAnother + } + CALL { + WITH p + MATCH (lpa:Person {lpa: p.lpa}) + RETURN sum(p.lpa) AS sumLpa, avg(p.lpa) AS avgLpa, count(p.lpa) AS countLpa + } + RETURN p.name, sumLouvain, avgLouvain, countLouvain, sumWcc, avgWcc, countWcc, sumAnother, avgAnother, countAnother, sumLpa, avgLpa, countLpa + """, Map.of(), + Result::resultAsString); + System.out.println("s = " + s); + + + String s1 = db.executeTransactionally(""" + MATCH (p:Person) + WITH p.lpa as lpa, count(*) as sizeLpa + WITH apoc.map.fromPairs(collect([toString(lpa), sizeLpa])) as map_lpa + MATCH (p:Person) + WITH map_lpa, p.wcc as wcc, count(*) as sizeWcc + WITH map_lpa, apoc.map.fromPairs(collect([toString(wcc), sizeWcc])) as map_wcc + MATCH (p:Person) + WITH map_lpa, map_wcc, p.louvain as louvain, count(*) as sizeLouvain + WITH map_lpa, map_wcc, apoc.map.fromPairs(collect([toString(louvain), sizeLouvain])) as map_louvain + MATCH (p:Person) + RETURN p.name, map_lpa[toString(p.lpa)] as lpaSize, map_wcc[toString(p.wcc)] as wccSize, map_louvain[toString(p.louvain)] as louvainSize""", + Map.of(), Result::resultAsString); + System.out.println("s1 = " + s1); + + +// String s2 = db.executeTransactionally(""" +// match (p:Person) +// WITH apoc.agg.multiStats(p, ["lpa","wcc","louvain"], ["count"]) as data +// MATCH (p:Person) +// RETURN p.name, data.lpa.count as lpaSize, data.wcc.count as wccSize, data.louvain.count as louvainSize +// """, Map.of(), Result::resultAsString); +// System.out.println("s2 = " + s2); + String s2 = db.executeTransactionally(""" + match (p:Person) + with apoc.agg.multiStats(p, ["lpa","wcc","louvain"], ["count"]) as data + match (p:Person) + return p.name, data.wcc[toString(p.wcc)], data.louvain[toString(p.louvain)], data.lpa[toString(p.lpa)] + //MATCH (p:Person) + //RETURN p.name, data.lpa.count as lpaSize, data.wcc.count as wccSize, data.louvain.count as louvainSize + """, Map.of(), Result::resultAsString); + System.out.println("s2 = " + s2); + } + // similar to https://community.neo4j.com/t/listing-the-community-size-of-different-community-detection-algorithms-already-calculated/42895 @Test public void test() { + db.executeTransactionally(""" + CREATE (:Person { louvain: 596, neo4jImportId: "18349390", wcc: 48, lpa: 598, name: "aaa", another: 548}), + (:Person { louvain: 596, neo4jImportId: "18349390", wcc: 48, lpa: 598, name: "eee", another: 549}), + (:Person { louvain: 596, neo4jImportId: "18349390", wcc: 48, lpa: 598, name: "eee", another: 549}), + (:Person { louvain: 597, neo4jImportId: "18349391", wcc: 48, lpa: 598, name: "eee", another: 549}), + (:Person { louvain: 597, neo4jImportId: "18349392", wcc: 47, lpa: 596, name: "iii", another: 549}), + (:Person { louvain: 597, neo4jImportId: "18349393", wcc: 47, lpa: 596, name: "iii", another: 549}), + (:Person { louvain: 597, neo4jImportId: "18349394", wcc: 47, lpa: 596, name: "iii", another: 549}), + (:Person { louvain: 597, neo4jImportId: "18349393", wcc: 47, lpa: 596, name: "iii", another: 10}), + (:Person { louvain: 597, neo4jImportId: "18349394", wcc: 47, lpa: 596, name: "iii", another: 10}) + """); +// db.executeTransactionally(""" +// CREATE (:Person { louvain: 596, neo4jImportId: "18349390", wcc: 48, lpa: 598, name: "eee", another: 549});"""); +// db.executeTransactionally(""" +// CREATE (:Person { louvain: 596, neo4jImportId: "18349390", wcc: 47, lpa: 596, name: "iii", another: 549})"""); + + + String s = db.executeTransactionally(""" MATCH (p:Person) WITH p CALL { WITH p - MATCH (l:Person {louvain: p.louvain}) - RETURN count(*) AS louvain + MATCH (n:Person {louvain: p.louvain}) + RETURN sum(p.louvain) AS sumLouvain, avg(p.louvain) AS avgLouvain, count(p.louvain) AS countLouvain } CALL { WITH p - MATCH (wcc:Person {wcc: p.wcc}) - RETURN count(*) AS wcc + MATCH (n:Person {wcc: p.wcc}) + RETURN sum(p.wcc) AS sumWcc, avg(p.wcc) AS avgWcc, count(p.wcc) AS countWcc } CALL { WITH p - MATCH (wcc_cypher:Person {wcc_cypher: p.wcc_cypher}) - RETURN count(*) AS wcc_cypher + MATCH (n:Person {another: p.another}) + RETURN sum(p.another) AS sumAnother, avg(p.another) AS avgAnother, count(p.another) AS countAnother } CALL { WITH p MATCH (lpa:Person {lpa: p.lpa}) - RETURN count(*) AS lpa + RETURN sum(p.lpa) AS sumLpa, avg(p.lpa) AS avgLpa, count(p.lpa) AS countLpa } - RETURN p.name, lpa, wcc, wcc_cypher, louvain + RETURN p.name, sumLouvain, avgLouvain, countLouvain, sumWcc, avgWcc, countWcc, sumAnother, avgAnother, countAnother, sumLpa, avgLpa, countLpa """, Map.of(), Result::resultAsString); System.out.println("s = " + s); - String s1 = db.executeTransactionally(""" @@ -145,9 +238,9 @@ WITH map_lpa, map_wcc, p.louvain as louvain, count(*) as sizeLouvain // System.out.println("s2 = " + s2); String s2 = db.executeTransactionally(""" match (p:Person) - RETURN apoc.agg.multiStats(p, ["lpa","wcc","louvain"], ["count"]) as data + with apoc.agg.multiStats(p, ["lpa","wcc","louvain"], ["count"]) as data match (p:Person) - return p.name, data.wcc.[toString(p.wcc)].count as size + return p.name, data.wcc[toString(p.wcc)], data.louvain[toString(p.louvain)], data.lpa[toString(p.lpa)] //MATCH (p:Person) //RETURN p.name, data.lpa.count as lpaSize, data.wcc.count as wccSize, data.louvain.count as louvainSize """, Map.of(), Result::resultAsString); @@ -155,4 +248,92 @@ WITH map_lpa, map_wcc, p.louvain as louvain, count(*) as sizeLouvain // todo... } + + + // similar to https://community.neo4j.com/t/how-could-to-replace-groupe-by-roll-up-and-group-by-cube-in-cypher-aggregation/44762 + @Test + public void test23() { + db.executeTransactionally(""" + CREATE (:Sales {ProductID: 10, CustomerID: 48, salesAmount: 100}), + (:Sales {ProductID: 90, CustomerID: 38, salesAmount: 200}), + (:Sales {ProductID: 90, CustomerID: 38, salesAmount: 300}), + (:Sales {ProductID: 90, CustomerID: 108, salesAmount: 300}), + (:Sales {ProductID: 90, CustomerID: 108, salesAmount: 300}), + (:Sales {ProductID: 20, CustomerID: 108, salesAmount: 300}), + (:Sales {ProductID: 20, CustomerID: 108, salesAmount: 300}), + (:Sales {ProductID: 20, CustomerID: 108, salesAmount: 301}), + (:Sales {ProductID: 20, CustomerID: 108, salesAmount: 301}), + (:Sales {ProductID: 30, CustomerID: 108, salesAmount: 300}), + (:Sales {ProductID: 30, CustomerID: 108, salesAmount: 301}), + (:Sales {ProductID: 30, CustomerID: 108, salesAmount: 301}), + (:Sales {ProductID: 30, CustomerID: 118, salesAmount: 300}), + (:Sales {ProductID: 30, CustomerID: 118, salesAmount: 301}), + (:Sales {ProductID: 40, CustomerID: 28, salesAmount: 400})"""); + + +// String s1 = db.executeTransactionally(""" +// MATCH (s:Sales) +// WITH s.ProductID as pid, s.CustomerID as cid, sum(s.salesAmount) as sa +// CALL { +// WITH pid, cid, sa +// WITH pid, cid, sum(sa) as saAmount RETURN saAmount +// } +// CALL { +// WITH pid, cid, sa +// WITH cid, sa, sum(pid) as saProduct RETURN saProduct +// } +// RETURN pid, cid, saProduct, saAmount, sa""", +// Map.of(), Result::resultAsString); + String s1 = db.executeTransactionally(""" + MATCH (p:Sales) + WITH p + CALL { + WITH p + MATCH (l:Sales {ProductID: p.ProductID}) + RETURN sum(l.ProductID) AS louvain + } + CALL { + WITH p + MATCH (another:Sales {salesAmount: p.salesAmount}) + RETURN sum(another.salesAmount) AS another + } + RETURN p.CustomerID, another, louvain""", + Map.of(), Result::resultAsString); + System.out.println("s1 = " + s1); + + +// String s11 = db.executeTransactionally(""" +// MATCH (s:Sales) +// WITH s.CustomerID as cid, sum(s.ProductID) as pid, sum(s.salesAmount) as sum +// RETURN cid, pid, sum +// // UNION +// // MATCH (s:Sales) +// // WITH null as cid, null as pid, sum(s.salesAmount) as sum +// // RETURN cid, pid, sum +// // UNION +// // MATCH (s:Sales) +// // WITH s.CustomerID as cid, null as pid, sum(s.salesAmount) as sum +// // RETURN cid, pid, sum;""", +// Map.of(), Result::resultAsString); +// System.out.println("s11 = " + s11); + + + String s2 = db.executeTransactionally(""" + match (p:Sales) + WITH apoc.agg.multiStats(p, ["ProductID","salesAmount"], ["sum"]) as data + match (p:Sales) + RETURN p.CustomerID, data.ProductID[toString(p.ProductID)].sum, data.salesAmount[toString(p.salesAmount)].sum + """, Map.of(), Result::resultAsString); + System.out.println("s2 = " + s2); + + String s23 = db.executeTransactionally(""" + match (p:Sales) + RETURN apoc.agg.multiStats(p, ["ProductID","salesAmount"], ["sum"]) as data + // match (p:Sales) + // RETURN DISTINCT p.CustomerID, data.ProductID[toString(p.ProductID)].sum, data.salesAmount[toString(p.salesAmount)].sum + """, Map.of(), Result::resultAsString); + System.out.println("s23 = " + s23); + + } + }