WN9 implementation

Contents

Data tables

WN9 uses a table of average performances at each tier, as shown below. This table shouldn't need to change with future updates, and so can be hardcoded.

var tierAvg = [			// from 150816 EU avgs exc scout/arty
	{ win:0.477, dmg:88.9, frag:0.68, spot:0.90, def:0.53, cap:1.0, weight:0.40 },
	{ win:0.490, dmg:118.2, frag:0.66, spot:0.85, def:0.65, cap:1.0, weight:0.41 },
	{ win:0.495, dmg:145.1, frag:0.59, spot:1.05, def:0.51, cap:1.0, weight:0.44 },
	{ win:0.492, dmg:214.0, frag:0.60, spot:0.81, def:0.55, cap:1.0, weight:0.44 },
	{ win:0.495, dmg:388.3, frag:0.75, spot:0.93, def:0.63, cap:1.0, weight:0.60 },
	{ win:0.497, dmg:578.7, frag:0.74, spot:0.93, def:0.52, cap:1.0, weight:0.70 },
	{ win:0.498, dmg:791.1, frag:0.76, spot:0.87, def:0.58, cap:1.0, weight:0.82 },
	{ win:0.497, dmg:1098.7, frag:0.79, spot:0.87, def:0.58, cap:1.0, weight:1.00 },
	{ win:0.498, dmg:1443.2, frag:0.86, spot:0.94, def:0.56, cap:1.0, weight:1.23 },
	{ win:0.498, dmg:1963.8, frag:1.04, spot:1.08, def:0.61, cap:1.0, weight:1.60 }];

WN9 also requires a table of per-tank values: wn9exp, wn9scale and wn9nerf, along with each tank's tier and matchmaking range. Account WN9 also uses the tank's type (or class). These will be updated when new tanks are released, and can be found on the Tank Expected Values page.

NEW: The expected values JSON file also includes two multipliers which WN9 uses to map account and recent results to the numerical scale. These may be updated over time along with the expected values. The recent multiplier should be used for interval data, and the account multiplier for overall data.

Per-tank WN9

Usage

Per-tank WN9 values can be used to compare a player's performances in different tanks. The same calculation is used for single battles by setting the battle count to 1. WN9 values can also be generated for account subsets, such as per-tier or per-class WN9, by battle-weighting the per-tank WN9 values.

As the WN9 expected values are based purely on recent data, per-tank WN9 results are less valid for tanks that have had significant balance changes since they were played. However, WN9 includes data for tanks that have had significant nerfs. Sites can either mark these tanks, or provide a second result for the maximum historical capability of each tank.

Per-tank WN9 results shouldn't show any decimal places except for testing, as the results are not that accurate.

Functional description

Per-tank damage, frags, spots and defence data can be acquired from the tanks/stats public API, dossier file or service record. Only random battle data should be used: You can get this from the API by using the parameter &extra=random. Be sure to use the fields parameter to reduce the data downloaded to what you require, eg. &fields=tank_id,random

  1. Select tier average from table, adding +1 to tier if tank has +3 tier MM.
  2. Divide damage, frags, spots and defence points per game by the tier averages to give rdmg, rfrag, rspot and rdef.
  3. Generate wn9base value using the formula 0.7*rdmg + 0.25*sqrt(rfrag*rspot) + 0.05*sqrt(rfrag*sqrt(rdef)), or the formula 0.7*rdmg + 0.14*rfrag + 0.13*sqrt(rspot) + 0.03*sqrt(rdef) for low battle counts.
  4. For maximum historical capability, wn9exp = wn9exp * (1 + wn9nerf)
  5. Apply the tank-selection adjustment wn9 = 1 + (wn9base / wn9exp - 1) / wn9scale
  6. Multiply the final result by the account WN9 multiplier and cap to zero.

Example javascript

// inputs:
// tank is object containing tank_id variable & "random" object
//     "random" object contains battles, damage_dealt, frags, spotted and dropped_capture_points
// expvals is array containing wn9exp/wn9scale/wn9nerf/tier/mmrange for each tank, indexed by tank_id
// maxhist should be false for current values and true for maximum historical values
// multiplier should be the account WN9 multiplier, unless interval data is used

function CalcWN9Tank(tank, expvals, maxhist, multiplier)
{
	var exp = expvals[tank.tank_id];
	if (!exp) { console.log("Tank ID not found: " + tank.tank_id); return -1; }

	var rtank = tank.random;
	var avg = tierAvg[exp.mmrange >= 3 ? exp.tier : exp.tier-1];
	var rdmg = rtank.damage_dealt / (rtank.battles * avg.dmg);
	var rfrag = rtank.frags / (rtank.battles * avg.frag);
	var rspot = rtank.spotted / (rtank.battles * avg.spot);
	var rdef = rtank.dropped_capture_points / (rtank.battles * avg.def);

	// Calculate raw winrate-correlated wn9base
	// Use different formula for low battle counts
	var wn9base = 0.7*rdmg;
	if (rtank.battles < 5) wn9base += 0.14*rfrag + 0.13*Math.sqrt(rspot) + 0.03*Math.sqrt(rdef);
	else wn9base += 0.25*Math.sqrt(rfrag*rspot) + 0.05*Math.sqrt(rfrag*Math.sqrt(rdef));
	// Adjust expected value if generating maximum historical value
	var wn9exp = maxhist ? exp.wn9exp * (1+exp.wn9nerf) : exp.wn9exp;
	// Calculate final WN9 based on tank expected value & skill scaling 
	var wn9 = multiplier * Math.max(0, 1 + (wn9base / wn9exp - 1) / exp.wn9scale );
	return wn9;
}

Recent WN9

Usage

This is the primary skill metric in WN9. Players can use it to see how they're improving over time and to compare their performance with other players.

In addition to the usual 24-hour to 1000-battle intervals, Sites should ideally include a "long recent" interval of around 3-5k battles to demonstrate a player's sustainable performance and reduce the incentive for short term padding, eg. by paying better players to pad an account.

Because recent WN9 only uses recent data, tank buffs and nerfs have no long-term effect and no adjustment is necessary. The recent WN9 method can be used to generate an "overall WN9" value, but this should only be used for testing. Decimal places should not be used.

Functional description

This is similar in principle to the WN8 method. It typically uses the difference between snapshots from the account/info and account/tanks APIs. The only per-tank variable used is battles, while the other stats apply to the whole account. For consistency with account/tanks, overall stats should be taken from the "all" section of account/info, not the "random" section.

  1. For each tank played in the interval:
    1. Select tier average from table, adding +1 to tier if tank has +3 tier MM.
    2. Accumulate expected tier damage, frags, spots and defence values multipled by battle count.
    3. Accumulate tier wn9weight value multiplied by battle count.
    4. Accumulate per-tank wn9exp value multiplied by battle count and tier weight.
    5. Accumulate per-tank wn9scale value multiplied by wn9exp, battle count and tier weight.
  2. Divide interval damage, frags, spots and defence by the accumulated tier averages to give rdmg, rfrag, rspot and rdef.
  3. Where the battle counts differ, apply adjustment so that result isn't skewed
  4. Generate wn9base value using the formula 0.7*rdmg + 0.25*sqrt(rfrag*rspot) + 0.05*sqrt(rfrag*sqrt(rdef))
  5. Divide the accumulated wn9scale by wn9exp, and then wn9exp by wn9weight.
  6. Apply the tank-selection adjustment wn9 = 1 + (wn9base / wn9exp - 1) / wn9scale
  7. Multiply the final result by the recent WN9 multiplier and cap to zero.

Valid recent WN9 values can also be generated by using tanks/stats data and battle-weighting the Per-tank WN9 method, but the accuracy gain is small compared to the additional storage and traffic requirements, so it's not recommended unless you're collecting full per-tank data for other reasons.

Example javascript

// inputs:
// accstats is object containing total battles, damage_dealt, frags, spotted and dropped_capture_points
// tanks is array of objects each with tank_id and statistics.battles vars
// expvals is array containing wn9exp/wn9scale/tier/mmrange for each tank, indexed by tank_id
// wn9recmul is the recent WN9 multiplier value

function CalcWN9Recent(accstats, tanks, expvals, wn9recmul)
{
	var bc=0, edmg=0, efrag=0, espot=0, edef=0;
	var wn9exp=0, wn9scale=0, wn9weight=0;
	for (var i=0; i<tanks.length; i++)
	{
		var exp = expvals[tanks[i].tank_id];
		// Be sure to report this error so that players know that their results are inaccurate
		if (!exp) { console.log("Tank ID not found: " + tanks[i].tank_id); continue; }

		var tankbat = tanks[i].statistics.battles;
		bc += tankbat;

		var avg = tierAvg[exp.mmrange >= 3 ? exp.tier : exp.tier-1];
		edmg += avg.dmg * tankbat;
		efrag += avg.frag * tankbat;
		espot += avg.spot * tankbat;
		edef += avg.def * tankbat;

		wn9weight += avg.weight * tankbat;
		wn9exp += exp.wn9exp * tankbat * avg.weight;
		wn9scale += exp.wn9scale * exp.wn9exp * tankbat * avg.weight;
	}
	if (!accstats.battles || !bc) return 0;		// handle no battles/tanks cases
	if (accstats.battles < bc) console.log(bc-accstats.battles + " battles missing from account/info");
	if (accstats.battles > bc) console.log(accstats.battles-bc + " battles missing from account/tanks");

	// batmod is important for handling cases where the battle counts differ
	var batmod = bc / accstats.battles;
	var rdmg = batmod * accstats.damage_dealt / edmg;
	var rfrag = batmod * accstats.frags / efrag;
	var rspot = batmod * accstats.spotted / espot;
	var rdef = batmod * accstats.dropped_capture_points / edef;

	var wn9base = 0.7*rdmg + 0.25*Math.sqrt(rfrag*rspot) + 0.05*Math.sqrt(rfrag*Math.sqrt(rdef));
	wn9scale /= wn9exp;
	wn9exp /= wn9weight;
	var wn9 = wn9recmul * Math.max(0, 1 + (wn9base / wn9exp - 1) / wn9scale );
	return wn9;
}

Account WN9

Usage

This is a hybrid achievement/skill metric, intended to be used as a replacement for overall metrics. It discards the worst 35% of battles by tank to discourage re-rolling, and limits the influence of single tanks, especially those that were historically overpowered. Artillery are discarded because the historical data for them is extremely poor.

Account WN9 is not as good a skill metric as a recent WN9 of 3-5k battles, but it can be used where you don't have access to interval data. All sites with a correct implementation should give the same result. Unlike the other WN9 metrics, one decimal place may be shown so that players can see it increasing with time.

Functional description

Account WN9 uses the same random-battle per-tank data as Per-tank WN9. You can get this from the API by using the parameter &extra=random. Be sure to use the fields parameter to reduce the data downloaded to what you require, eg. &fields=tank_id,random

  1. Create an array of per-tank WN9 values and battle counts, discarding SPGs
  2. For each tank, cap the battle count to tier*(40 + tier*totalbat/2000).
  3. For each tank with historical nerfs, divide the battle count by 2.
  4. Sort tanks by WN9.
  5. Generate a battle-weighted WN9 using the top 65% of capped battles.

Example javascript

// inputs:
// tank is object containing tank_id variable & "random" object
//     "random" object contains battles, damage_dealt, frags, spotted and dropped_capture_points
// expvals is array containing wn9exp/wn9scale/wn9nerf/tier/mmrange for each tank, indexed by tank_id
// wn9accmul is the account WN9 multiplier value

function CalcWN9Account(tanks, expvals, wn9accmul)
{
	// compile list of valid tanks with battles & WN9 
	var tanklist = [];
	var totbat = 0;
	for (var i=0; i<tanks.length; i++)
	{
		var exp = expvals[tanks[i].tank_id];
		if (!exp || exp.type == "SPG") continue;	// don't use SPGs & missing tanks
		var wn9 = CalcWN9Tank(tanks[i], expvals, false, wn9accmul);
		var tankentry = { wn9:wn9, bat:tanks[i].random.battles, exp:exp };
		tanklist.push(tankentry);
		totbat += tankentry.bat;
	}
	if (!totbat) return -1;		// handle case with no valid tanks

	// cap tank weight according to tier, total battles & nerf status
	var totweight = 0;
	for (var i=0; i<tanklist.length; i++)
	{
		var exp = tanklist[i].exp;
		var batcap = exp.tier*(40.0 + exp.tier*totbat/2000.0);
		tanklist[i].weight = Math.min(batcap, tanklist[i].bat);
		if (exp.wn9nerf) tanklist[i].weight /= 2;
		totweight += tanklist[i].weight;
	}

	// sort tanks by WN9 decreasing
	function compareTanks(a, b) { return b.wn9 - a.wn9 };
	tanklist.sort(compareTanks);

	// add up account WN9 over top 65% of capped battles
	totweight *= 0.65;
	var wn9tot = 0, usedweight = 0, i = 0;
	for (; usedweight+tanklist[i].weight <= totweight; i++)
	{
		wn9tot += tanklist[i].wn9 * tanklist[i].weight;
		usedweight += tanklist[i].weight;
	}
	// last tank before cutoff uses remaining weight, not its battle count
	wn9tot += tanklist[i].wn9 * (totweight - usedweight);
	return wn9tot / totweight;
}