24h new player retention prediction

This commit is contained in:
Rsl1122 2019-08-14 09:22:25 +03:00
parent 63c2db3cb2
commit 4d48954473
5 changed files with 147 additions and 7 deletions

View File

@ -136,6 +136,12 @@ public class ActivityIndex {
return formatter.apply(value);
}
public double distance(ActivityIndex other) {
// Logarithm makes the distance function more skewed towards active players
// https://www.wolframalpha.com/input/?i=plot+y+%3D+log(5+-+5+*+(1+%2F+(pi%2F2+*+x%2B1)))+and+5+-+5+*+(1+%2F+(pi%2F2+*+x%2B1))+and+y+%3D1+and+y+%3D+2+and+y+%3D+3+and+y+%3D+3.75+from+-0.5+to+3
return Math.abs(Math.log(other.value) - Math.log(value));
}
public String getGroup() {
if (value >= VERY_ACTIVE) {
return "Very Active";

View File

@ -75,6 +75,18 @@ public class RetentionData {
.orElse(0);
}
public static int countRetentionPrediction(
Collection<ActivityIndex> newPlayers,
ActivityIndex retained,
ActivityIndex nonRetained
) {
int count = 0;
for (ActivityIndex player : newPlayers) {
if (player.distance(retained) < player.distance(nonRetained)) count++;
}
return count;
}
public double distance(RetentionData data) {
double num = 0;
num += Math.abs(data.activityIndex - activityIndex) * 2.0;

View File

@ -25,9 +25,7 @@ import com.djrapitops.plan.db.sql.tables.UserInfoTable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.*;
import java.util.concurrent.TimeUnit;
import static com.djrapitops.plan.db.sql.parsing.Sql.*;
@ -214,4 +212,116 @@ public class ActivityIndexQueries {
}
};
}
public static Query<Collection<ActivityIndex>> activityIndexForNewPlayers(long after, long before, UUID serverUUID, Long threshold) {
String selectNewUUIDs = SELECT + UserInfoTable.USER_UUID +
FROM + UserInfoTable.TABLE_NAME +
WHERE + UserInfoTable.REGISTERED + "<=?" +
AND + UserInfoTable.REGISTERED + ">=?" +
AND + UserInfoTable.SERVER_UUID + "=?";
String sql = SELECT + "activity_index" +
FROM + '(' + selectNewUUIDs + ") n" +
INNER_JOIN + '(' + selectActivityIndexSQL() + ") a on n." + SessionsTable.USER_UUID + "=a." + SessionsTable.USER_UUID;
return new QueryStatement<Collection<ActivityIndex>>(sql) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setLong(1, before);
statement.setLong(2, after);
statement.setString(3, serverUUID.toString());
setSelectActivityIndexSQLParameters(statement, 4, threshold, serverUUID, before);
}
@Override
public Collection<ActivityIndex> processResults(ResultSet set) throws SQLException {
Collection<ActivityIndex> indexes = new ArrayList<>();
while (set.next()) {
indexes.add(new ActivityIndex(set.getDouble("activity_index"), before));
}
return indexes;
}
};
}
public static Query<ActivityIndex> averageActivityIndexForRetainedPlayers(long after, long before, UUID serverUUID, Long threshold) {
String selectNewUUIDs = SELECT + UserInfoTable.USER_UUID +
FROM + UserInfoTable.TABLE_NAME +
WHERE + UserInfoTable.REGISTERED + "<=?" +
AND + UserInfoTable.REGISTERED + ">=?" +
AND + UserInfoTable.SERVER_UUID + "=?";
String selectUniqueUUIDs = SELECT + "DISTINCT " + SessionsTable.USER_UUID +
FROM + SessionsTable.TABLE_NAME +
WHERE + SessionsTable.SESSION_START + ">=?" +
AND + SessionsTable.SESSION_END + "<=?" +
AND + SessionsTable.SERVER_UUID + "=?";
String sql = SELECT + "AVG(activity_index) as average" +
FROM + '(' + selectNewUUIDs + ") n" +
INNER_JOIN + '(' + selectUniqueUUIDs + ") u on n." + SessionsTable.USER_UUID + "=u." + SessionsTable.USER_UUID +
INNER_JOIN + '(' + selectActivityIndexSQL() + ") a on n." + SessionsTable.USER_UUID + "=a." + SessionsTable.USER_UUID;
return new QueryStatement<ActivityIndex>(sql) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setLong(1, before);
statement.setLong(2, after);
statement.setString(3, serverUUID.toString());
// Have played in the last half of the time frame
long half = before - (after - before) / 2;
statement.setLong(4, half);
statement.setLong(5, before);
statement.setString(6, serverUUID.toString());
setSelectActivityIndexSQLParameters(statement, 7, threshold, serverUUID, before);
}
@Override
public ActivityIndex processResults(ResultSet set) throws SQLException {
return set.next() ? new ActivityIndex(set.getDouble("average"), before) : new ActivityIndex(0.0, before);
}
};
}
public static Query<ActivityIndex> averageActivityIndexForNonRetainedPlayers(long after, long before, UUID serverUUID, Long threshold) {
String selectNewUUIDs = SELECT + UserInfoTable.USER_UUID +
FROM + UserInfoTable.TABLE_NAME +
WHERE + UserInfoTable.REGISTERED + "<=?" +
AND + UserInfoTable.REGISTERED + ">=?" +
AND + UserInfoTable.SERVER_UUID + "=?";
String selectUniqueUUIDs = SELECT + "DISTINCT " + SessionsTable.USER_UUID +
FROM + SessionsTable.TABLE_NAME +
WHERE + SessionsTable.SESSION_START + ">=?" +
AND + SessionsTable.SESSION_END + "<=?" +
AND + SessionsTable.SERVER_UUID + "=?";
String sql = SELECT + "AVG(activity_index) as average" +
FROM + '(' + selectNewUUIDs + ") n" +
LEFT_JOIN + '(' + selectUniqueUUIDs + ") u on n." + SessionsTable.USER_UUID + "=u." + SessionsTable.USER_UUID +
INNER_JOIN + '(' + selectActivityIndexSQL() + ") a on n." + SessionsTable.USER_UUID + "=a." + SessionsTable.USER_UUID +
WHERE + "n." + SessionsTable.USER_UUID + IS_NULL;
return new QueryStatement<ActivityIndex>(sql) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setLong(1, before);
statement.setLong(2, after);
statement.setString(3, serverUUID.toString());
// Have played in the last half of the time frame
long half = before - (after - before) / 2;
statement.setLong(4, half);
statement.setLong(5, before);
statement.setString(6, serverUUID.toString());
setSelectActivityIndexSQLParameters(statement, 7, threshold, serverUUID, before);
}
@Override
public ActivityIndex processResults(ResultSet set) throws SQLException {
return set.next() ? new ActivityIndex(set.getDouble("average"), before) : new ActivityIndex(0.0, before);
}
};
}
}

View File

@ -19,9 +19,11 @@ package com.djrapitops.plan.system.json;
import com.djrapitops.plan.data.container.TPS;
import com.djrapitops.plan.data.store.keys.SessionKeys;
import com.djrapitops.plan.data.store.mutators.PlayersOnlineResolver;
import com.djrapitops.plan.data.store.mutators.RetentionData;
import com.djrapitops.plan.data.store.mutators.SessionsMutator;
import com.djrapitops.plan.data.store.mutators.TPSMutator;
import com.djrapitops.plan.db.Database;
import com.djrapitops.plan.db.access.queries.analysis.ActivityIndexQueries;
import com.djrapitops.plan.db.access.queries.analysis.PlayerCountQueries;
import com.djrapitops.plan.db.access.queries.objects.SessionQueries;
import com.djrapitops.plan.db.access.queries.objects.TPSQueries;
@ -29,6 +31,7 @@ import com.djrapitops.plan.db.access.queries.objects.UserInfoQueries;
import com.djrapitops.plan.system.database.DBSystem;
import com.djrapitops.plan.system.settings.config.PlanConfig;
import com.djrapitops.plan.system.settings.paths.DisplaySettings;
import com.djrapitops.plan.system.settings.paths.TimeSettings;
import com.djrapitops.plan.utilities.formatting.Formatter;
import com.djrapitops.plan.utilities.formatting.Formatters;
@ -83,6 +86,7 @@ public class OnlineActivityOverviewJSONParser implements TabJSONParser<Map<Strin
long halfMonthAgo = now - TimeUnit.DAYS.toMillis(15L);
long monthAgo = now - TimeUnit.DAYS.toMillis(30L);
int timeZoneOffset = timeZone.getOffset(now);
Long playThreshold = config.get(TimeSettings.ACTIVE_PLAY_THRESHOLD);
Map<String, Object> numbers = new HashMap<>();
@ -106,6 +110,7 @@ public class OnlineActivityOverviewJSONParser implements TabJSONParser<Map<Strin
Integer new30d = db.query(PlayerCountQueries.newPlayerCount(monthAgo, now, serverUUID));
Integer new7d = db.query(PlayerCountQueries.newPlayerCount(weekAgo, now, serverUUID));
Integer new1d = db.query(PlayerCountQueries.newPlayerCount(dayAgo, now, serverUUID));
numbers.put("new_players_30d", new30d);
numbers.put("new_players_30d_trend", new Trend(
db.query(PlayerCountQueries.newPlayerCount(monthAgo, halfMonthAgo, serverUUID)),
@ -113,7 +118,7 @@ public class OnlineActivityOverviewJSONParser implements TabJSONParser<Map<Strin
false
));
numbers.put("new_players_7d", new7d);
numbers.put("new_players_24h", db.query(PlayerCountQueries.newPlayerCount(dayAgo, now, serverUUID)));
numbers.put("new_players_24h", new1d);
numbers.put("new_players_30d_avg", db.query(PlayerCountQueries.averageNewPlayerCount(monthAgo, now, timeZoneOffset, serverUUID)));
numbers.put("new_players_30d_avg_trend", new Trend(
@ -132,8 +137,15 @@ public class OnlineActivityOverviewJSONParser implements TabJSONParser<Map<Strin
numbers.put("new_players_retention_30d_perc", percentageFormatter.apply(retentionPerc30d));
numbers.put("new_players_retention_7d", retained7d);
numbers.put("new_players_retention_7d_perc", percentageFormatter.apply(retentionPerc7d));
numbers.put("new_players_retention_24h", 0); // TODO
numbers.put("new_players_retention_24h_perc", percentageFormatter.apply(-1.0)); // TODO
int prediction1d = RetentionData.countRetentionPrediction(
db.query(ActivityIndexQueries.activityIndexForNewPlayers(dayAgo, now, serverUUID, playThreshold)),
db.query(ActivityIndexQueries.averageActivityIndexForRetainedPlayers(monthAgo, now, serverUUID, playThreshold)),
db.query(ActivityIndexQueries.averageActivityIndexForNonRetainedPlayers(monthAgo, now, serverUUID, playThreshold))
);
double retentionPerc1d = new1d != 0 ? (double) prediction1d / new1d : -1;
numbers.put("new_players_retention_24h", prediction1d);
numbers.put("new_players_retention_24h_perc", percentageFormatter.apply(retentionPerc1d));
Long playtimeMonth = db.query(SessionQueries.playtime(monthAgo, now, serverUUID));
Long playtimeWeek = db.query(SessionQueries.playtime(weekAgo, now, serverUUID));

View File

@ -919,7 +919,7 @@
class="fas fa-fw fa-globe col-green"></i>
Geolocations</h6>
</div>
<div class="chart-area row">
<div class="chart-area row" style="height: 100%;">
<div class="col-xs-12 col-sm-12 col-md-3 col-lg-3">
<div id="countryBarChart"><span class="loader"></span></div>
</div>