From 566f838a3a34fd7792141effded45f43aeacb1fb Mon Sep 17 00:00:00 2001 From: Rsl1122 Date: Wed, 20 Jun 2018 12:13:17 +0300 Subject: [PATCH] Player retention --- .../command/commands/QInspectCommand.java | 2 +- .../djrapitops/plan/data/PlayerProfile.java | 2 +- .../plan/data/calculation/AnalysisData.java | 20 ++-- .../plan/data/calculation/HealthNotes.java | 4 +- .../plan/data/container/StickyData.java | 108 ------------------ .../store/containers/AnalysisContainer.java | 59 ++++++++-- .../store/containers/PlayerContainer.java | 7 ++ .../plan/data/store/keys/AnalysisKeys.java | 14 ++- .../mutators}/ActivityIndex.java | 3 +- .../data/store/mutators/PlayersMutator.java | 70 +++++++++++- .../store/mutators/PlayersOnlineResolver.java | 35 ++++++ .../data/store/mutators/RetentionData.java | 77 +++++++++++++ .../data/store/mutators/SessionsMutator.java | 16 ++- .../store/mutators/formatting/Formatters.java | 8 +- .../webserver/pages/parsing/AnalysisPage.java | 4 +- .../webserver/pages/parsing/InspectPage.java | 2 +- .../utilities/analysis/AnalysisUtils.java | 17 ++- .../html/graphs/ActivityStackGraph.java | 2 +- .../html/graphs/pie/ActivityPie.java | 2 +- .../utilities/html/tables/PlayersTable.java | 2 +- .../html/tables/PlayersTableCreator.java | 2 +- .../data/calculation/ActivityIndexTest.java | 1 + .../plan/utilities/html/graphs/GraphTest.java | 2 +- 23 files changed, 297 insertions(+), 162 deletions(-) delete mode 100644 Plan/src/main/java/com/djrapitops/plan/data/container/StickyData.java rename Plan/src/main/java/com/djrapitops/plan/data/{calculation => store/mutators}/ActivityIndex.java (97%) create mode 100644 Plan/src/main/java/com/djrapitops/plan/data/store/mutators/PlayersOnlineResolver.java create mode 100644 Plan/src/main/java/com/djrapitops/plan/data/store/mutators/RetentionData.java diff --git a/Plan/src/main/java/com/djrapitops/plan/command/commands/QInspectCommand.java b/Plan/src/main/java/com/djrapitops/plan/command/commands/QInspectCommand.java index 58a5450fa..7cab64660 100644 --- a/Plan/src/main/java/com/djrapitops/plan/command/commands/QInspectCommand.java +++ b/Plan/src/main/java/com/djrapitops/plan/command/commands/QInspectCommand.java @@ -2,10 +2,10 @@ package com.djrapitops.plan.command.commands; import com.djrapitops.plan.PlanPlugin; import com.djrapitops.plan.api.exceptions.database.DBOpException; -import com.djrapitops.plan.data.calculation.ActivityIndex; import com.djrapitops.plan.data.container.GeoInfo; import com.djrapitops.plan.data.store.containers.PlayerContainer; import com.djrapitops.plan.data.store.keys.PlayerKeys; +import com.djrapitops.plan.data.store.mutators.ActivityIndex; import com.djrapitops.plan.data.store.mutators.GeoInfoMutator; import com.djrapitops.plan.data.store.mutators.SessionsMutator; import com.djrapitops.plan.data.store.mutators.formatting.Formatter; diff --git a/Plan/src/main/java/com/djrapitops/plan/data/PlayerProfile.java b/Plan/src/main/java/com/djrapitops/plan/data/PlayerProfile.java index 6f99e9264..2d01a95c9 100644 --- a/Plan/src/main/java/com/djrapitops/plan/data/PlayerProfile.java +++ b/Plan/src/main/java/com/djrapitops/plan/data/PlayerProfile.java @@ -4,11 +4,11 @@ */ package com.djrapitops.plan.data; -import com.djrapitops.plan.data.calculation.ActivityIndex; import com.djrapitops.plan.data.container.Action; import com.djrapitops.plan.data.container.GeoInfo; import com.djrapitops.plan.data.container.PlayerKill; import com.djrapitops.plan.data.container.Session; +import com.djrapitops.plan.data.store.mutators.ActivityIndex; import com.djrapitops.plan.data.time.WorldTimes; import com.djrapitops.plan.system.info.server.ServerInfo; import com.djrapitops.plan.utilities.comparators.ActionComparator; diff --git a/Plan/src/main/java/com/djrapitops/plan/data/calculation/AnalysisData.java b/Plan/src/main/java/com/djrapitops/plan/data/calculation/AnalysisData.java index e49f229a3..90a333019 100644 --- a/Plan/src/main/java/com/djrapitops/plan/data/calculation/AnalysisData.java +++ b/Plan/src/main/java/com/djrapitops/plan/data/calculation/AnalysisData.java @@ -4,10 +4,10 @@ import com.djrapitops.plan.PlanPlugin; import com.djrapitops.plan.data.PlayerProfile; import com.djrapitops.plan.data.ServerProfile; import com.djrapitops.plan.data.container.Session; -import com.djrapitops.plan.data.container.StickyData; import com.djrapitops.plan.data.container.TPS; import com.djrapitops.plan.data.element.AnalysisContainer; import com.djrapitops.plan.data.plugin.PluginData; +import com.djrapitops.plan.data.store.mutators.RetentionData; import com.djrapitops.plan.data.store.mutators.SessionsMutator; import com.djrapitops.plan.data.store.mutators.formatting.Formatters; import com.djrapitops.plan.data.time.WorldTimes; @@ -55,7 +55,7 @@ public class AnalysisData extends RawData { private long refreshDate; private Map analyzedValues; - private Set stickyMonthData; + private Set stickyMonthData; private List players; public AnalysisData() { @@ -250,7 +250,7 @@ public class AnalysisData extends RawData { got("stuckPerM", stuckPerM); got("stuckPerW", stuckPerW); - stickyMonthData = newMonth.stream().map(StickyData::new).distinct().collect(Collectors.toSet()); + stickyMonthData = newMonth.stream().map(RetentionData::new).distinct().collect(Collectors.toSet()); addValue("playersStuckMonth", stuckPerM); addValue("playersStuckWeek", stuckPerW); @@ -288,16 +288,16 @@ public class AnalysisData extends RawData { return; } - List stuck = stuckAfterMonth.stream().map(StickyData::new).collect(Collectors.toList()); - List nonStuck = notStuckAfterMonth.stream().map(StickyData::new).collect(Collectors.toList()); + List stuck = stuckAfterMonth.stream().map(RetentionData::new).collect(Collectors.toList()); + List nonStuck = notStuckAfterMonth.stream().map(RetentionData::new).collect(Collectors.toList()); - StickyData avgStuck = AnalysisUtils.average(stuck); - StickyData avgNonStuck = AnalysisUtils.average(nonStuck); + RetentionData avgStuck = AnalysisUtils.average(stuck); + RetentionData avgNonStuck = AnalysisUtils.average(nonStuck); int stuckPerD = 0; for (PlayerProfile player : newDay) { - StickyData stickyData = new StickyData(player); - if (stickyData.distance(avgStuck) < stickyData.distance(avgNonStuck)) { + RetentionData retentionData = new RetentionData(player); + if (retentionData.distance(avgStuck) < retentionData.distance(avgNonStuck)) { stuckPerD++; } } @@ -403,7 +403,7 @@ public class AnalysisData extends RawData { return analyzedValues.getOrDefault(key, 0L); } - public Set getStickyMonthData() { + public Set getStickyMonthData() { return stickyMonthData; } diff --git a/Plan/src/main/java/com/djrapitops/plan/data/calculation/HealthNotes.java b/Plan/src/main/java/com/djrapitops/plan/data/calculation/HealthNotes.java index 269f79255..2dd5ba2f2 100644 --- a/Plan/src/main/java/com/djrapitops/plan/data/calculation/HealthNotes.java +++ b/Plan/src/main/java/com/djrapitops/plan/data/calculation/HealthNotes.java @@ -6,8 +6,8 @@ package com.djrapitops.plan.data.calculation; import com.djrapitops.plan.data.PlayerProfile; import com.djrapitops.plan.data.ServerProfile; -import com.djrapitops.plan.data.container.StickyData; import com.djrapitops.plan.data.container.TPS; +import com.djrapitops.plan.data.store.mutators.RetentionData; import com.djrapitops.plan.data.store.mutators.formatting.Formatter; import com.djrapitops.plan.data.store.mutators.formatting.Formatters; import com.djrapitops.plan.system.settings.Settings; @@ -128,7 +128,7 @@ public class HealthNotes { } private void newPlayerNote() { - double avgOnlineOnRegister = MathUtils.averageDouble(analysisData.getStickyMonthData().stream().map(StickyData::getOnlineOnJoin)); + double avgOnlineOnRegister = MathUtils.averageDouble(analysisData.getStickyMonthData().stream().map(RetentionData::getOnlineOnJoin)); if (avgOnlineOnRegister >= 1) { notes.add("

" + Html.GREEN_THUMB.parse() + " New Players have players to play with when they join (" + FormatUtils.cutDecimals(avgOnlineOnRegister) + " on average)

"); diff --git a/Plan/src/main/java/com/djrapitops/plan/data/container/StickyData.java b/Plan/src/main/java/com/djrapitops/plan/data/container/StickyData.java deleted file mode 100644 index 1035d3023..000000000 --- a/Plan/src/main/java/com/djrapitops/plan/data/container/StickyData.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Licence is provided in the jar as license.yml also here: - * https://github.com/Rsl1122/Plan-PlayerAnalytics/blob/master/Plan/src/main/resources/license.yml - */ -package com.djrapitops.plan.data.container; - -import com.djrapitops.plan.data.Actions; -import com.djrapitops.plan.data.PlayerProfile; -import com.djrapitops.plugin.api.TimeAmount; -import com.google.common.base.Objects; - -import java.util.List; - -public class StickyData { - private final double activityIndex; - private Double messagesSent; - private Double onlineOnJoin; - - public StickyData(PlayerProfile player) { - activityIndex = player.getActivityIndex(player.getRegistered() + TimeAmount.DAY.ms()).getValue(); - loadActionVariables(player.getActions()); - } - - public StickyData(double activityIndex, Double messagesSent, Double onlineOnJoin) { - this.activityIndex = activityIndex; - this.messagesSent = messagesSent; - this.onlineOnJoin = onlineOnJoin; - } - - private void loadActionVariables(List actions) { - for (Action action : actions) { - try { - if (messagesSent == null && action.getDoneAction() == Actions.FIRST_LOGOUT) { - messagesSent = (double) loadSentMessages(action); - } - if (onlineOnJoin == null && action.getDoneAction() == Actions.FIRST_SESSION) { - onlineOnJoin = (double) loadOnlineOnJoin(action); - } - } catch (IllegalArgumentException ignore) { - /* continue */ - } - } - setDefaultValuesIfNull(); - } - - private void setDefaultValuesIfNull() { - if (messagesSent == null) { - messagesSent = 0.0; - } - if (onlineOnJoin == null) { - onlineOnJoin = 0.0; - } - } - - private int loadOnlineOnJoin(Action action) { - String additionalInfo = action.getAdditionalInfo(); - String[] split = additionalInfo.split(" "); - if (split.length == 3) { - return Integer.parseInt(split[1]); - } - throw new IllegalArgumentException("Improper Action"); - } - - private int loadSentMessages(Action action) { - String additionalInfo = action.getAdditionalInfo(); - String[] split = additionalInfo.split(": "); - if (split.length == 2) { - return Integer.parseInt(split[1]); - } - throw new IllegalArgumentException("Improper Action"); - } - - public double distance(StickyData data) { - double num = 0; - num += Math.abs(data.activityIndex - activityIndex) * 2.0; - num += Math.abs(data.onlineOnJoin - onlineOnJoin) / 10.0; - num += Math.abs(data.messagesSent - messagesSent) / 10.0; - - return num; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - StickyData that = (StickyData) o; - return Double.compare(that.activityIndex, activityIndex) == 0 && - Objects.equal(messagesSent, that.messagesSent) && - Objects.equal(onlineOnJoin, that.onlineOnJoin); - } - - @Override - public int hashCode() { - return Objects.hashCode(activityIndex, messagesSent, onlineOnJoin); - } - - public double getOnlineOnJoin() { - return onlineOnJoin; - } - - public double getActivityIndex() { - return activityIndex; - } - - public Double getMessagesSent() { - return messagesSent; - } -} \ No newline at end of file diff --git a/Plan/src/main/java/com/djrapitops/plan/data/store/containers/AnalysisContainer.java b/Plan/src/main/java/com/djrapitops/plan/data/store/containers/AnalysisContainer.java index 450f51e99..a8b2a450c 100644 --- a/Plan/src/main/java/com/djrapitops/plan/data/store/containers/AnalysisContainer.java +++ b/Plan/src/main/java/com/djrapitops/plan/data/store/containers/AnalysisContainer.java @@ -5,10 +5,7 @@ import com.djrapitops.plan.data.store.Key; import com.djrapitops.plan.data.store.keys.AnalysisKeys; import com.djrapitops.plan.data.store.keys.PlayerKeys; import com.djrapitops.plan.data.store.keys.ServerKeys; -import com.djrapitops.plan.data.store.mutators.CommandUseMutator; -import com.djrapitops.plan.data.store.mutators.PlayersMutator; -import com.djrapitops.plan.data.store.mutators.SessionsMutator; -import com.djrapitops.plan.data.store.mutators.TPSMutator; +import com.djrapitops.plan.data.store.mutators.*; import com.djrapitops.plan.data.store.mutators.formatting.Formatters; import com.djrapitops.plan.data.time.WorldTimes; import com.djrapitops.plan.system.database.databases.Database; @@ -32,10 +29,7 @@ import com.djrapitops.plan.utilities.html.tables.PlayersTable; import com.djrapitops.plan.utilities.html.tables.ServerSessionTable; import com.djrapitops.plugin.api.TimeAmount; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; /** @@ -172,6 +166,28 @@ public class AnalysisContainer extends DataContainer { putSupplier(AnalysisKeys.AVG_PLAYERS_NEW_DAY, () -> getUnsafe(uniqueDay).newPerDay()); putSupplier(AnalysisKeys.AVG_PLAYERS_NEW_WEEK, () -> getUnsafe(uniqueWeek).newPerDay()); putSupplier(AnalysisKeys.AVG_PLAYERS_NEW_MONTH, () -> getUnsafe(uniqueMonth).newPerDay()); + + Key retentionDay = new Key<>(Integer.class, "RETENTION_DAY"); + // compareAndFindThoseLikelyToBeRetained can throw exception. + putSupplier(retentionDay, () -> getUnsafe(AnalysisKeys.PLAYERS_MUTATOR).compareAndFindThoseLikelyToBeRetained( + getUnsafe(newDay).all(), getUnsafe(AnalysisKeys.ANALYSIS_TIME_MONTH_AGO), + getUnsafe(AnalysisKeys.PLAYERS_ONLINE_RESOLVER) + ).count() + ); + putSupplier(AnalysisKeys.PLAYERS_RETAINED_DAY, () -> { + try { + return getUnsafe(retentionDay); + } catch (IllegalStateException e) { + return 0; + } + }); + putSupplier(AnalysisKeys.PLAYERS_RETAINED_DAY_PERC, () -> { + try { + return Formatters.percentage().apply(1.0 * getUnsafe(retentionDay) / getUnsafe(AnalysisKeys.PLAYERS_NEW_DAY)); + } catch (IllegalStateException e) { + return "Not enough data"; + } + }); } private void addSessionSuppliers() { @@ -228,6 +244,24 @@ public class AnalysisContainer extends DataContainer { putSupplier(AnalysisKeys.AVG_PLAYERS_DAY, () -> getUnsafe(sessionsDay).toUniqueJoinsPerDay()); putSupplier(AnalysisKeys.AVG_PLAYERS_WEEK, () -> getUnsafe(sessionsWeek).toUniqueJoinsPerDay()); putSupplier(AnalysisKeys.AVG_PLAYERS_MONTH, () -> getUnsafe(sessionsMonth).toUniqueJoinsPerDay()); + putSupplier(AnalysisKeys.PLAYERS_RETAINED_WEEK, () -> + PlayersMutator.copyOf(getUnsafe(AnalysisKeys.PLAYERS_MUTATOR)).filterRetained( + getUnsafe(AnalysisKeys.ANALYSIS_TIME_WEEK_AGO), + getUnsafe(AnalysisKeys.ANALYSIS_TIME) + ).count() + ); + putSupplier(AnalysisKeys.PLAYERS_RETAINED_MONTH, () -> + PlayersMutator.copyOf(getUnsafe(AnalysisKeys.PLAYERS_MUTATOR)).filterRetained( + getUnsafe(AnalysisKeys.ANALYSIS_TIME_MONTH_AGO), + getUnsafe(AnalysisKeys.ANALYSIS_TIME) + ).count() + ); + putSupplier(AnalysisKeys.PLAYERS_RETAINED_WEEK_PERC, () -> Formatters.percentage().apply( + 1.0 * getUnsafe(AnalysisKeys.PLAYERS_RETAINED_WEEK) / getUnsafe(AnalysisKeys.PLAYERS_NEW_WEEK)) + ); + putSupplier(AnalysisKeys.PLAYERS_RETAINED_MONTH_PERC, () -> Formatters.percentage().apply( + 1.0 * getUnsafe(AnalysisKeys.PLAYERS_RETAINED_MONTH) / getUnsafe(AnalysisKeys.PLAYERS_NEW_MONTH)) + ); } private void addGraphSuppliers() { @@ -256,6 +290,13 @@ public class AnalysisContainer extends DataContainer { putSupplier(AnalysisKeys.ACTIVITY_PIE_SERIES, () -> new ActivityPie(getUnsafe(AnalysisKeys.ACTIVITY_DATA).get(getUnsafe(AnalysisKeys.ANALYSIS_TIME))).toHighChartsSeries() ); + putSupplier(AnalysisKeys.PLAYERS_REGULAR, () -> { + Map> activityNow = getUnsafe(AnalysisKeys.ACTIVITY_DATA).get(getUnsafe(AnalysisKeys.ANALYSIS_TIME)); + Set veryActiveNow = activityNow.getOrDefault("Very Active", new HashSet<>()); + Set activeNow = activityNow.getOrDefault("Active", new HashSet<>()); + Set regularNow = activityNow.getOrDefault("Regular", new HashSet<>()); + return veryActiveNow.size() + activeNow.size() + regularNow.size(); + }); } private void addTPSAverageSuppliers() { @@ -273,6 +314,8 @@ public class AnalysisContainer extends DataContainer { .filterDataBetween(getUnsafe(AnalysisKeys.ANALYSIS_TIME_DAY_AGO), getUnsafe(AnalysisKeys.ANALYSIS_TIME)) ); + putSupplier(AnalysisKeys.PLAYERS_ONLINE_RESOLVER, () -> new PlayersOnlineResolver(getUnsafe(AnalysisKeys.TPS_MUTATOR))); + putSupplier(AnalysisKeys.TPS_SPIKE_MONTH, () -> getUnsafe(tpsMonth).lowTpsSpikeCount()); putSupplier(AnalysisKeys.AVG_TPS_MONTH, () -> getUnsafe(tpsMonth).averageTPS()); putSupplier(AnalysisKeys.AVG_CPU_MONTH, () -> getUnsafe(tpsMonth).averageCPU()); diff --git a/Plan/src/main/java/com/djrapitops/plan/data/store/containers/PlayerContainer.java b/Plan/src/main/java/com/djrapitops/plan/data/store/containers/PlayerContainer.java index a61a98579..37a4c8413 100644 --- a/Plan/src/main/java/com/djrapitops/plan/data/store/containers/PlayerContainer.java +++ b/Plan/src/main/java/com/djrapitops/plan/data/store/containers/PlayerContainer.java @@ -1,5 +1,7 @@ package com.djrapitops.plan.data.store.containers; +import com.djrapitops.plan.data.store.mutators.SessionsMutator; + /** * DataContainer about a Player. *

@@ -9,4 +11,9 @@ package com.djrapitops.plan.data.store.containers; * @see com.djrapitops.plan.data.store.keys.PlayerKeys For Key objects. */ public class PlayerContainer extends DataContainer { + + public boolean playedBetween(long after, long before) { + return SessionsMutator.forContainer(this).playedBetween(after, before); + } + } \ No newline at end of file diff --git a/Plan/src/main/java/com/djrapitops/plan/data/store/keys/AnalysisKeys.java b/Plan/src/main/java/com/djrapitops/plan/data/store/keys/AnalysisKeys.java index c8266edd2..851f784fe 100644 --- a/Plan/src/main/java/com/djrapitops/plan/data/store/keys/AnalysisKeys.java +++ b/Plan/src/main/java/com/djrapitops/plan/data/store/keys/AnalysisKeys.java @@ -4,6 +4,7 @@ import com.djrapitops.plan.data.store.Key; import com.djrapitops.plan.data.store.PlaceholderKey; import com.djrapitops.plan.data.store.Type; import com.djrapitops.plan.data.store.mutators.PlayersMutator; +import com.djrapitops.plan.data.store.mutators.PlayersOnlineResolver; import com.djrapitops.plan.data.store.mutators.SessionsMutator; import com.djrapitops.plan.data.store.mutators.TPSMutator; @@ -86,12 +87,12 @@ public class AnalysisKeys { public static final PlaceholderKey AVG_PLAYERS_NEW_DAY = new PlaceholderKey<>(Integer.class, "playersNewAverageDay"); public static final PlaceholderKey AVG_PLAYERS_NEW_WEEK = new PlaceholderKey<>(Integer.class, "playersNewAverageWeek"); public static final PlaceholderKey AVG_PLAYERS_NEW_MONTH = new PlaceholderKey<>(Integer.class, "playersNewAverageMonth"); - public static final PlaceholderKey PLAYERS_STUCK_DAY = new PlaceholderKey<>(Integer.class, "playersStuckDay"); - public static final PlaceholderKey PLAYERS_STUCK_DAY_PERC = new PlaceholderKey<>(String.class, "playersStuckPercDay"); - public static final PlaceholderKey PLAYERS_STUCK_WEEK = new PlaceholderKey<>(Integer.class, "playersStuckWeek"); - public static final PlaceholderKey PLAYERS_STUCK_WEEK_PERC = new PlaceholderKey<>(String.class, "playersStuckPercWeek"); - public static final PlaceholderKey PLAYERS_STUCK_MONTH = new PlaceholderKey<>(Integer.class, "playersStuckMonth"); - public static final PlaceholderKey PLAYERS_STUCK_MONTH_PERC = new PlaceholderKey<>(String.class, "playersStuckPercMonth"); + public static final PlaceholderKey PLAYERS_RETAINED_DAY = new PlaceholderKey<>(Integer.class, "playersStuckDay"); + public static final PlaceholderKey PLAYERS_RETAINED_DAY_PERC = new PlaceholderKey<>(String.class, "playersStuckPercDay"); + public static final PlaceholderKey PLAYERS_RETAINED_WEEK = new PlaceholderKey<>(Integer.class, "playersStuckWeek"); + public static final PlaceholderKey PLAYERS_RETAINED_WEEK_PERC = new PlaceholderKey<>(String.class, "playersStuckPercWeek"); + public static final PlaceholderKey PLAYERS_RETAINED_MONTH = new PlaceholderKey<>(Integer.class, "playersStuckMonth"); + public static final PlaceholderKey PLAYERS_RETAINED_MONTH_PERC = new PlaceholderKey<>(String.class, "playersStuckPercMonth"); // public static final PlaceholderKey TPS_SPIKE_MONTH = new PlaceholderKey<>(Integer.class, "tpsSpikeMonth"); public static final PlaceholderKey TPS_SPIKE_WEEK = new PlaceholderKey<>(Integer.class, "tpsSpikeWeek"); @@ -130,6 +131,7 @@ public class AnalysisKeys { public static final Key SESSIONS_MUTATOR = new Key<>(SessionsMutator.class, "SESSIONS_MUTATOR"); public static final Key TPS_MUTATOR = new Key<>(TPSMutator.class, "TPS_MUTATOR"); public static final Key PLAYERS_MUTATOR = new Key<>(PlayersMutator.class, "PLAYERS_MUTATOR"); + public static final Key PLAYERS_ONLINE_RESOLVER = new Key<>(PlayersOnlineResolver.class, "PLAYERS_ONLINE_RESOLVER"); public static final Key PLAYTIME_TOTAL = new Key<>(Long.class, "PLAYTIME_TOTAL"); public static final Key ANALYSIS_TIME = new Key<>(Long.class, "ANALYSIS_TIME"); public static final Key ANALYSIS_TIME_DAY_AGO = new Key<>(Long.class, "ANALYSIS_TIME_DAY_AGO"); diff --git a/Plan/src/main/java/com/djrapitops/plan/data/calculation/ActivityIndex.java b/Plan/src/main/java/com/djrapitops/plan/data/store/mutators/ActivityIndex.java similarity index 97% rename from Plan/src/main/java/com/djrapitops/plan/data/calculation/ActivityIndex.java rename to Plan/src/main/java/com/djrapitops/plan/data/store/mutators/ActivityIndex.java index bbd5d5291..0faa72ac0 100644 --- a/Plan/src/main/java/com/djrapitops/plan/data/calculation/ActivityIndex.java +++ b/Plan/src/main/java/com/djrapitops/plan/data/store/mutators/ActivityIndex.java @@ -1,7 +1,6 @@ -package com.djrapitops.plan.data.calculation; +package com.djrapitops.plan.data.store.mutators; import com.djrapitops.plan.data.store.containers.DataContainer; -import com.djrapitops.plan.data.store.mutators.SessionsMutator; import com.djrapitops.plan.system.settings.Settings; import com.djrapitops.plan.utilities.FormatUtils; import com.djrapitops.plugin.api.TimeAmount; diff --git a/Plan/src/main/java/com/djrapitops/plan/data/store/mutators/PlayersMutator.java b/Plan/src/main/java/com/djrapitops/plan/data/store/mutators/PlayersMutator.java index ad354a9b7..3471576fb 100644 --- a/Plan/src/main/java/com/djrapitops/plan/data/store/mutators/PlayersMutator.java +++ b/Plan/src/main/java/com/djrapitops/plan/data/store/mutators/PlayersMutator.java @@ -1,6 +1,5 @@ package com.djrapitops.plan.data.store.mutators; -import com.djrapitops.plan.data.calculation.ActivityIndex; import com.djrapitops.plan.data.container.GeoInfo; import com.djrapitops.plan.data.store.containers.DataContainer; import com.djrapitops.plan.data.store.containers.PlayerContainer; @@ -8,6 +7,7 @@ import com.djrapitops.plan.data.store.keys.PlayerKeys; import com.djrapitops.plan.data.store.keys.ServerKeys; import com.djrapitops.plan.data.store.keys.SessionKeys; import com.djrapitops.plan.data.store.mutators.formatting.Formatters; +import com.djrapitops.plan.utilities.analysis.AnalysisUtils; import com.djrapitops.plugin.api.TimeAmount; import java.util.*; @@ -54,6 +54,19 @@ public class PlayersMutator { return this; } + public PlayersMutator filterRetained(long after, long before) { + players = players.stream() + .filter(player -> { + long backLimit = Math.max(after, player.getValue(PlayerKeys.REGISTERED).orElse(0L)); + long half = backLimit + ((before - backLimit) / 2L); + SessionsMutator firstHalf = SessionsMutator.forContainer(player); + SessionsMutator secondHalf = SessionsMutator.copyOf(firstHalf); + return !firstHalf.playedBetween(backLimit, half) && !secondHalf.playedBetween(half, before); + }) + .collect(Collectors.toList()); + return this; + } + public List all() { return players; } @@ -117,4 +130,59 @@ public class PlayersMutator { } return total / numberOfDays; } + + /** + * Compares players in the mutator to other players in terms of player retention. + * + * @param compareTo Players to compare to. + * @param dateLimit Epoch ms back limit, if the player registered after this their value is not used. + * @return Mutator containing the players that are considered to be retained. + * @throws IllegalStateException If all players are rejected due to dateLimit. + */ + public PlayersMutator compareAndFindThoseLikelyToBeRetained(Iterable compareTo, + long dateLimit, + PlayersOnlineResolver onlineResolver) { + Collection retainedAfterMonth = new ArrayList<>(); + Collection notRetainedAfterMonth = new ArrayList<>(); + + for (PlayerContainer player : players) { + long registered = player.getValue(PlayerKeys.REGISTERED).orElse(System.currentTimeMillis()); + + // Discard uncertain data + if (registered > dateLimit) { + continue; + } + + long monthAfterRegister = registered + TimeAmount.MONTH.ms(); + long half = registered + (TimeAmount.MONTH.ms() / 2L); + if (player.playedBetween(registered, half) && player.playedBetween(half, monthAfterRegister)) { + retainedAfterMonth.add(player); + } else { + notRetainedAfterMonth.add(player); + } + } + + if (retainedAfterMonth.isEmpty() || notRetainedAfterMonth.isEmpty()) { + throw new IllegalStateException("No players to compare to after rejecting with dateLimit"); + } + + List retained = retainedAfterMonth.stream() + .map(player -> new RetentionData(player, onlineResolver)) + .collect(Collectors.toList()); + List notRetained = notRetainedAfterMonth.stream() + .map(player -> new RetentionData(player, onlineResolver)) + .collect(Collectors.toList()); + + RetentionData avgRetained = AnalysisUtils.average(retained); + RetentionData avgNotRetained = AnalysisUtils.average(notRetained); + + List toBeRetained = new ArrayList<>(); + for (PlayerContainer player : compareTo) { + RetentionData retentionData = new RetentionData(player, onlineResolver); + if (retentionData.distance(avgRetained) < retentionData.distance(avgNotRetained)) { + toBeRetained.add(player); + } + } + return new PlayersMutator(toBeRetained); + } } \ No newline at end of file diff --git a/Plan/src/main/java/com/djrapitops/plan/data/store/mutators/PlayersOnlineResolver.java b/Plan/src/main/java/com/djrapitops/plan/data/store/mutators/PlayersOnlineResolver.java new file mode 100644 index 000000000..7bb1baffd --- /dev/null +++ b/Plan/src/main/java/com/djrapitops/plan/data/store/mutators/PlayersOnlineResolver.java @@ -0,0 +1,35 @@ +package com.djrapitops.plan.data.store.mutators; + +import com.djrapitops.plan.utilities.html.graphs.line.Point; + +import java.util.*; + +/** + * Resolves dates into players online numbers with a help of a NavigableMap. + *

+ * Time Complexity of O(n / 2) with the use of TreeMap. + * + * @author Rsl1122 + */ +public class PlayersOnlineResolver { + + private final NavigableMap onlineNumberMap; + + public PlayersOnlineResolver(TPSMutator mutator) { + List points = mutator.playersOnlinePoints(); + onlineNumberMap = new TreeMap<>(); + for (Point point : points) { + double date = point.getX(); + double value = point.getY(); + onlineNumberMap.put((long) date, (int) value); + } + } + + public Optional getOnlineOn(long date) { + Map.Entry entry = onlineNumberMap.floorEntry(date); + if (entry == null) { + return Optional.empty(); + } + return Optional.of(entry.getValue()); + } +} \ No newline at end of file diff --git a/Plan/src/main/java/com/djrapitops/plan/data/store/mutators/RetentionData.java b/Plan/src/main/java/com/djrapitops/plan/data/store/mutators/RetentionData.java new file mode 100644 index 000000000..6dee390c0 --- /dev/null +++ b/Plan/src/main/java/com/djrapitops/plan/data/store/mutators/RetentionData.java @@ -0,0 +1,77 @@ +/* + * Licence is provided in the jar as license.yml also here: + * https://github.com/Rsl1122/Plan-PlayerAnalytics/blob/master/Plan/src/main/resources/license.yml + */ +package com.djrapitops.plan.data.store.mutators; + +import com.djrapitops.plan.data.PlayerProfile; +import com.djrapitops.plan.data.store.containers.PlayerContainer; +import com.djrapitops.plan.data.store.keys.PlayerKeys; +import com.djrapitops.plugin.api.TimeAmount; +import com.google.common.base.Objects; + +import java.util.Optional; + +/** + * Utility class for player retention calculations. + *

+ * Previously known as StickyData. + * + * @author Rsl1122 + */ +public class RetentionData { + private final double activityIndex; + private double onlineOnJoin; + + @Deprecated + public RetentionData(PlayerProfile player) { + activityIndex = player.getActivityIndex(player.getRegistered() + TimeAmount.DAY.ms()).getValue(); + } + + public RetentionData(double activityIndex, double onlineOnJoin) { + this.activityIndex = activityIndex; + this.onlineOnJoin = onlineOnJoin; + } + + public RetentionData(PlayerContainer player, PlayersOnlineResolver onlineOnJoin) { + Optional registeredValue = player.getValue(PlayerKeys.REGISTERED); + activityIndex = registeredValue + .map(registered -> new ActivityIndex(player, registered + TimeAmount.DAY.ms()).getValue()) + .orElse(0.0); + this.onlineOnJoin = registeredValue + .map(registered -> onlineOnJoin.getOnlineOn(registered).orElse(-1)) + .orElse(0); + } + + public double distance(RetentionData data) { + double num = 0; + num += Math.abs(data.activityIndex - activityIndex) * 2.0; + num += data.onlineOnJoin != -1 && onlineOnJoin != -1 + ? Math.abs(data.onlineOnJoin - onlineOnJoin) / 10.0 + : 0; + + return num; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RetentionData that = (RetentionData) o; + return Double.compare(that.activityIndex, activityIndex) == 0 && + Objects.equal(onlineOnJoin, that.onlineOnJoin); + } + + @Override + public int hashCode() { + return Objects.hashCode(activityIndex, onlineOnJoin); + } + + public double getOnlineOnJoin() { + return onlineOnJoin; + } + + public double getActivityIndex() { + return activityIndex; + } +} \ No newline at end of file diff --git a/Plan/src/main/java/com/djrapitops/plan/data/store/mutators/SessionsMutator.java b/Plan/src/main/java/com/djrapitops/plan/data/store/mutators/SessionsMutator.java index 83a85109b..c0a97910c 100644 --- a/Plan/src/main/java/com/djrapitops/plan/data/store/mutators/SessionsMutator.java +++ b/Plan/src/main/java/com/djrapitops/plan/data/store/mutators/SessionsMutator.java @@ -11,6 +11,7 @@ import com.djrapitops.plan.utilities.analysis.MathUtils; import java.util.*; import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; /** @@ -42,8 +43,7 @@ public class SessionsMutator { public SessionsMutator filterSessionsBetween(long after, long before) { sessions = sessions.stream() - .filter(session -> after <= session.getValue(SessionKeys.END).orElse(System.currentTimeMillis()) - && session.getUnsafe(SessionKeys.START) <= before) + .filter(getBetweenPredicate(after, before)) .collect(Collectors.toList()); return this; } @@ -164,4 +164,16 @@ public class SessionsMutator { public int toPlayerKillCount() { return toPlayerKillList().size(); } + + public boolean playedBetween(long after, long before) { + return sessions.stream().anyMatch(getBetweenPredicate(after, before)); + } + + private Predicate getBetweenPredicate(long after, long before) { + return session -> { + Long start = session.getUnsafe(SessionKeys.START); + Long end = session.getValue(SessionKeys.END).orElse(System.currentTimeMillis()); + return (after <= start && start <= before) || (after <= end && end <= before); + }; + } } \ No newline at end of file diff --git a/Plan/src/main/java/com/djrapitops/plan/data/store/mutators/formatting/Formatters.java b/Plan/src/main/java/com/djrapitops/plan/data/store/mutators/formatting/Formatters.java index ec83e3100..df84ffae6 100644 --- a/Plan/src/main/java/com/djrapitops/plan/data/store/mutators/formatting/Formatters.java +++ b/Plan/src/main/java/com/djrapitops/plan/data/store/mutators/formatting/Formatters.java @@ -25,9 +25,7 @@ public class Formatters { } public static Formatter yearLongValue() { - return date -> { - return date > 0 ? FormatUtils.formatTimeStampYear(date) : "-"; - }; + return date -> date > 0 ? FormatUtils.formatTimeStampYear(date) : "-"; } public static Formatter day() { @@ -66,4 +64,8 @@ public class Formatters { return day.get(Calendar.DAY_OF_YEAR); }; } + + public static Formatter percentage() { + return value -> value >= 0 ? FormatUtils.cutDecimals(value * 100.0) + "%" : "-"; + } } \ No newline at end of file diff --git a/Plan/src/main/java/com/djrapitops/plan/system/webserver/pages/parsing/AnalysisPage.java b/Plan/src/main/java/com/djrapitops/plan/system/webserver/pages/parsing/AnalysisPage.java index 8a0136748..9164630a8 100644 --- a/Plan/src/main/java/com/djrapitops/plan/system/webserver/pages/parsing/AnalysisPage.java +++ b/Plan/src/main/java/com/djrapitops/plan/system/webserver/pages/parsing/AnalysisPage.java @@ -62,8 +62,8 @@ public class AnalysisPage extends Page { // AVG_PLAYERS, AVG_PLAYERS_DAY, AVG_PLAYERS_WEEK, // AVG_PLAYERS_MONTH, AVG_PLAYERS_NEW, AVG_PLAYERS_NEW_DAY, // AVG_PLAYERS_NEW_WEEK, AVG_PLAYERS_NEW_MONTH, PLAYERS_STUCK_DAY, -// PLAYERS_STUCK_DAY_PERC, PLAYERS_STUCK_WEEK, PLAYERS_STUCK_WEEK_PERC, -// PLAYERS_STUCK_MONTH, PLAYERS_STUCK_MONTH_PERC, +// PLAYERS_STUCK_DAY_PERC, PLAYERS_RETAINED_WEEK, PLAYERS_RETAINED_WEEK_PERC, +// PLAYERS_RETAINED_MONTH, PLAYERS_RETAINED_MONTH_PERC, // // TPS_SPIKE_MONTH, TPS_SPIKE_WEEK, TPS_SPIKE_DAY, // AVG_TPS_MONTH, AVG_TPS_WEEK, AVG_TPS_DAY, diff --git a/Plan/src/main/java/com/djrapitops/plan/system/webserver/pages/parsing/InspectPage.java b/Plan/src/main/java/com/djrapitops/plan/system/webserver/pages/parsing/InspectPage.java index 7c1a59a39..af60bb895 100644 --- a/Plan/src/main/java/com/djrapitops/plan/system/webserver/pages/parsing/InspectPage.java +++ b/Plan/src/main/java/com/djrapitops/plan/system/webserver/pages/parsing/InspectPage.java @@ -6,12 +6,12 @@ package com.djrapitops.plan.system.webserver.pages.parsing; import com.djrapitops.plan.api.exceptions.ParseException; import com.djrapitops.plan.data.Actions; -import com.djrapitops.plan.data.calculation.ActivityIndex; import com.djrapitops.plan.data.container.Action; import com.djrapitops.plan.data.container.Session; import com.djrapitops.plan.data.store.containers.PerServerContainer; import com.djrapitops.plan.data.store.containers.PlayerContainer; import com.djrapitops.plan.data.store.keys.PlayerKeys; +import com.djrapitops.plan.data.store.mutators.ActivityIndex; import com.djrapitops.plan.data.store.mutators.PerServerDataMutator; import com.djrapitops.plan.data.store.mutators.SessionsMutator; import com.djrapitops.plan.data.store.mutators.formatting.Formatter; diff --git a/Plan/src/main/java/com/djrapitops/plan/utilities/analysis/AnalysisUtils.java b/Plan/src/main/java/com/djrapitops/plan/utilities/analysis/AnalysisUtils.java index 0bc21cbb5..ef72220b4 100644 --- a/Plan/src/main/java/com/djrapitops/plan/utilities/analysis/AnalysisUtils.java +++ b/Plan/src/main/java/com/djrapitops/plan/utilities/analysis/AnalysisUtils.java @@ -1,10 +1,10 @@ package com.djrapitops.plan.utilities.analysis; import com.djrapitops.plan.data.PlayerProfile; -import com.djrapitops.plan.data.calculation.ActivityIndex; import com.djrapitops.plan.data.container.Session; -import com.djrapitops.plan.data.container.StickyData; import com.djrapitops.plan.data.store.keys.SessionKeys; +import com.djrapitops.plan.data.store.mutators.ActivityIndex; +import com.djrapitops.plan.data.store.mutators.RetentionData; import com.djrapitops.plan.data.time.GMTimes; import com.djrapitops.plan.data.time.WorldTimes; import com.djrapitops.plan.system.settings.WorldAliasSettings; @@ -243,24 +243,21 @@ public class AnalysisUtils { return gmTimesPerAlias; } - public static StickyData average(Collection stuck) { + public static RetentionData average(Collection stuck) { int size = stuck.size(); double totalIndex = 0.0; - double totalMsgsSent = 0.0; double totalPlayersOnline = 0.0; - for (StickyData stickyData : stuck) { - totalIndex += stickyData.getActivityIndex(); - totalMsgsSent += stickyData.getMessagesSent(); - totalPlayersOnline += stickyData.getOnlineOnJoin(); + for (RetentionData retentionData : stuck) { + totalIndex += retentionData.getActivityIndex(); + totalPlayersOnline += retentionData.getOnlineOnJoin(); } double averageIndex = totalIndex / (double) size; - double averageMessagesSent = totalMsgsSent / (double) size; double averagePlayersOnline = totalPlayersOnline / (double) size; - return new StickyData(averageIndex, averageMessagesSent, averagePlayersOnline); + return new RetentionData(averageIndex, averagePlayersOnline); } public static String getLongestWorldPlayed(Session session) { diff --git a/Plan/src/main/java/com/djrapitops/plan/utilities/html/graphs/ActivityStackGraph.java b/Plan/src/main/java/com/djrapitops/plan/utilities/html/graphs/ActivityStackGraph.java index cb1e44d25..b8f2466c2 100644 --- a/Plan/src/main/java/com/djrapitops/plan/utilities/html/graphs/ActivityStackGraph.java +++ b/Plan/src/main/java/com/djrapitops/plan/utilities/html/graphs/ActivityStackGraph.java @@ -4,7 +4,7 @@ */ package com.djrapitops.plan.utilities.html.graphs; -import com.djrapitops.plan.data.calculation.ActivityIndex; +import com.djrapitops.plan.data.store.mutators.ActivityIndex; import com.djrapitops.plan.data.store.mutators.PlayersMutator; import com.djrapitops.plan.system.settings.theme.Theme; import com.djrapitops.plan.system.settings.theme.ThemeVal; diff --git a/Plan/src/main/java/com/djrapitops/plan/utilities/html/graphs/pie/ActivityPie.java b/Plan/src/main/java/com/djrapitops/plan/utilities/html/graphs/pie/ActivityPie.java index afdc6d8d7..661195338 100644 --- a/Plan/src/main/java/com/djrapitops/plan/utilities/html/graphs/pie/ActivityPie.java +++ b/Plan/src/main/java/com/djrapitops/plan/utilities/html/graphs/pie/ActivityPie.java @@ -4,7 +4,7 @@ */ package com.djrapitops.plan.utilities.html.graphs.pie; -import com.djrapitops.plan.data.calculation.ActivityIndex; +import com.djrapitops.plan.data.store.mutators.ActivityIndex; import com.djrapitops.plan.system.settings.theme.Theme; import com.djrapitops.plan.system.settings.theme.ThemeVal; diff --git a/Plan/src/main/java/com/djrapitops/plan/utilities/html/tables/PlayersTable.java b/Plan/src/main/java/com/djrapitops/plan/utilities/html/tables/PlayersTable.java index 1f086bd9e..f5f41294c 100644 --- a/Plan/src/main/java/com/djrapitops/plan/utilities/html/tables/PlayersTable.java +++ b/Plan/src/main/java/com/djrapitops/plan/utilities/html/tables/PlayersTable.java @@ -1,11 +1,11 @@ package com.djrapitops.plan.utilities.html.tables; import com.djrapitops.plan.api.PlanAPI; -import com.djrapitops.plan.data.calculation.ActivityIndex; import com.djrapitops.plan.data.container.GeoInfo; import com.djrapitops.plan.data.element.TableContainer; import com.djrapitops.plan.data.store.containers.PlayerContainer; import com.djrapitops.plan.data.store.keys.PlayerKeys; +import com.djrapitops.plan.data.store.mutators.ActivityIndex; import com.djrapitops.plan.data.store.mutators.GeoInfoMutator; import com.djrapitops.plan.data.store.mutators.SessionsMutator; import com.djrapitops.plan.data.store.mutators.formatting.Formatters; diff --git a/Plan/src/main/java/com/djrapitops/plan/utilities/html/tables/PlayersTableCreator.java b/Plan/src/main/java/com/djrapitops/plan/utilities/html/tables/PlayersTableCreator.java index 8f2fdd213..f391e4bed 100644 --- a/Plan/src/main/java/com/djrapitops/plan/utilities/html/tables/PlayersTableCreator.java +++ b/Plan/src/main/java/com/djrapitops/plan/utilities/html/tables/PlayersTableCreator.java @@ -2,7 +2,7 @@ package com.djrapitops.plan.utilities.html.tables; import com.djrapitops.plan.api.PlanAPI; import com.djrapitops.plan.data.PlayerProfile; -import com.djrapitops.plan.data.calculation.ActivityIndex; +import com.djrapitops.plan.data.store.mutators.ActivityIndex; import com.djrapitops.plan.data.store.mutators.formatting.Formatters; import com.djrapitops.plan.system.info.server.ServerInfo; import com.djrapitops.plan.system.settings.Settings; diff --git a/Plan/src/test/java/com/djrapitops/plan/data/calculation/ActivityIndexTest.java b/Plan/src/test/java/com/djrapitops/plan/data/calculation/ActivityIndexTest.java index 4e22228f9..c5ec42b58 100644 --- a/Plan/src/test/java/com/djrapitops/plan/data/calculation/ActivityIndexTest.java +++ b/Plan/src/test/java/com/djrapitops/plan/data/calculation/ActivityIndexTest.java @@ -3,6 +3,7 @@ package com.djrapitops.plan.data.calculation; import com.djrapitops.plan.data.container.Session; import com.djrapitops.plan.data.store.containers.PlayerContainer; import com.djrapitops.plan.data.store.keys.PlayerKeys; +import com.djrapitops.plan.data.store.mutators.ActivityIndex; import com.djrapitops.plan.system.settings.Settings; import com.djrapitops.plugin.api.TimeAmount; import org.junit.BeforeClass; diff --git a/Plan/src/test/java/com/djrapitops/plan/utilities/html/graphs/GraphTest.java b/Plan/src/test/java/com/djrapitops/plan/utilities/html/graphs/GraphTest.java index e1960f53a..9168abf8f 100644 --- a/Plan/src/test/java/com/djrapitops/plan/utilities/html/graphs/GraphTest.java +++ b/Plan/src/test/java/com/djrapitops/plan/utilities/html/graphs/GraphTest.java @@ -4,8 +4,8 @@ */ package com.djrapitops.plan.utilities.html.graphs; -import com.djrapitops.plan.data.calculation.ActivityIndex; import com.djrapitops.plan.data.container.TPS; +import com.djrapitops.plan.data.store.mutators.ActivityIndex; import com.djrapitops.plan.system.settings.Settings; import com.djrapitops.plan.utilities.html.graphs.line.*; import com.djrapitops.plan.utilities.html.graphs.stack.AbstractStackGraph;