The confidence score has remained unchanged since its initial implementation in 2017. It’s well due for an update and is something we would like to improve.
The confidence score is a percent score out of 100, indicating whether a sensor’s laser counters appear to be reading properly and in line. It’s only calculated for sensors with two channels of PM data. Otherwise, it’s set to a default value of 30.
Confidence for sensors can be viewed using the sensor confidence data layer on the PurpleAir Map and can be obtained from our API. It’s not available for local data collection, like SD data or accessing sensor JSON, but can be calculated using the information below.
An overview of confidence and how it affects markers on the PurpleAir Map is found in the following article: The Confidence Score.
How the Confidence Score Works
On a high level, the confidence score is calculated using PM data from both channels. This score is then lowered based on downgrades. Confidence considers both automatically-calculated downgrades and manual downgrades applied by PurpleAir staff.
The steps for calculating confidence are as follows:
- Channel sums are calculated for channel A and channel B using pseudo averaged data.
- Automatic downgrades are set for channels A and B. This is done if either:
- The channel sum is over 2000.
- The channel sum is more than 10 times the other (only the higher reading channel is downgraded).
- The initial confidence score is calculated using only the channel sums
- Initial confidence is lowered if one or more channel sums are above 2000.
- Both
confidence_auto
andconfidence_manual
are calculated by lowering the initial confidence using automatic and manual downgrades, respectively.initial_confidence * (100 - (40 * number_of_downgraded_channels)) / 100;
channel_flags
reflecting downgrades are set.- The final
confidence
value is setconfidence = (confidence_auto + confidence_manual) / 2
Implementation
The following code calculates the confidence score, with some minor adjustments for readability.
public class ConfidenceCalculator {
public static final int MAX_PM = 2000;
public static double getZeroIfNodeIsNull(final JsonNode node) {
if (node == null) {
return 0;
} else {
return (double)node.asDouble();
}
}
/*
* This is where we start the confidence calculation
*
* objA -- stats_a -- real-time pseudo averaged data for channel A
* objB -- stats_b -- real-time pseudo averaged data for channel B
*/
public static void calculateFlags(final Object e, int channelState, int channelFlagsManual, ObjectNode objA, ObjectNode objB){
ObjectNode performance = calculatePerformance(objA, objB, channelState, channelFlagsManual);
Integer confidenceAuto = null;
Integer confidenceManual = null;
int channelFlagsAuto = 3;
// It starts as flags = 3, all bad.
if (performance.get("channel_flags_auto") != null) {
channelFlagsAuto = performance.get("channel_flags_auto").asInt();
}
if (performance.get("confidence_manual") != null) {
confidenceManual = performance.get("confidence_manual").asInt();
}
if (performance.get("confidence_auto") != null) {
confidenceAuto = performance.get("confidence_auto").asInt();
}
int channelFlags = channelFlagsManual;
if (channelFlagsManual == 0) {
channelFlags = channelFlagsAuto;
}
Integer confidence;
if (confidenceManual == null && confidenceAuto == null) {
confidence = 30;
} else if (confidenceManual == null) {
confidence = confidenceAuto;
} else if (confidenceAuto == null) {
confidence = confidenceManual;
} else {
confidence = (confidenceManual + confidenceAuto) / 2;
}
}
// Calculates confidence_manual and confidence_auto
public static ObjectNode calculatePerformance(final ObjectNode objA,
final ObjectNode objB, final int channelState, final int channelFlagsManual) {
// Initialize our perf object holding confidence values
ObjectMapper objectMapper = new ObjectMapper();
ObjectNode perf = (ObjectNode) objectMapper.readValue("{}", JsonNode.class);
// Return an object with no confidence values if there's no PM data
if (objA == null && objB == null) {
return perf;
}
float pmA = 0;
if (objA != null) {
JsonNode jsA = objA.get("pm2.5_atm"); // pm2.5_atm_a
if (jsA != null) {
pmA = jsA.floatValue();
}
}
float pmB = 0;
if (objB != null) {
JsonNode jsB = objB.get("pm2.5_atm"); // pm2.5_atm_b
if (jsB != null) {
pmB = jsB.floatValue();
}
}
double totala = pmA;
double vA = 0;
if (objA != null) {
vA = getZeroIfNodeIsNull(objA.get("pm2.5")); // pm2.5_a
totala += vA;
totala += getZeroIfNodeIsNull(objA.get("pm2.5_10minute")); // pm2.5_10minute_a
totala += getZeroIfNodeIsNull(objA.get("pm2.5_30minute")); // pm2.5_30minute_a
totala += getZeroIfNodeIsNull(objA.get("pm2.5_60minute")); // pm2.5_60minute_a
totala += getZeroIfNodeIsNull(objA.get("pm2.5_6hour")); // pm2.5_6hour_a
totala += getZeroIfNodeIsNull(objA.get("pm2.5_24hour")); // pm2.5_24hour_a
totala += getZeroIfNodeIsNull(objA.get("pm2.5_1week")); // pm2.5_1week_a
}
double totalb = pmB;
double vB = 0;
if (objB != null) {
vB = getZeroIfNodeIsNull(objB.get("pm2.5")); // pm2.5_b
totalb += vB;
totalb += getZeroIfNodeIsNull(objB.get("pm2.5_10minute")); // pm2.5_10minute_b
totalb += getZeroIfNodeIsNull(objB.get("pm2.5_30minute")); // pm2.5_30minute_b
totalb += getZeroIfNodeIsNull(objB.get("pm2.5_60minute")); // pm2.5_60minute_b
totalb += getZeroIfNodeIsNull(objB.get("pm2.5_6hour")); // pm2.5_6hour_b
totalb += getZeroIfNodeIsNull(objB.get("pm2.5_24hour")); // pm2.5_24hour_b
totalb += getZeroIfNodeIsNull(objB.get("pm2.5_1week")); // pm2.5_1week_b
}
// Calculate automatic downgrades
int channelFlagsAuto = 0;
if (vA > MAX_PM || objB != null && totala > (totalb * 10)) {
channelFlagsAuto = channelFlagsAuto + 1;
}
if (vB > MAX_PM || objA != null && totalb > (totala * 10)) {
channelFlagsAuto = channelFlagsAuto + 2;
}
perf.put("channel_flags_auto", channelFlagsAuto);
if (objA != null && objB != null) {
// calculate initial confidence
int confidence;
confidence = (int) getConfidence(totala, totalb);
// Lower confidence for abnormally high readings
if (pmA > MAX_PM && pmB > MAX_PM) {
confidence = confidence - 40;
} else if (pmA > MAX_PM) {
confidence = confidence - 60;
} else if (pmB > MAX_PM) {
confidence = confidence - 60;
}
// Calculate confidence_auto by adjusting for automatic downgrades
perf.put("confidence_auto", getConfidenceAdjusted(confidence, channelState, channelFlagsAuto));
// Calculate confidence_manual by adjusting for manual downgrades
perf.put("confidence_manual", getConfidenceAdjusted(confidence, channelState, channelFlagsManual));
}
return perf;
}
// Gets the initial confidence between two channel sums
private static double getConfidence(final double a, final double b) {
double diff = Math.abs(a - b);
double avg = (a + b) / 2;
double pc = Math.round(diff / avg * 100 / 1.6);
pc = pc - 25;
if (pc < 0) {
pc = 0;
}
double npcx;
npcx = 100 - pc;
if (npcx < 0) {
npcx = 0;
}
return npcx;
}
// Adjusts confidence based on downgrades (channelFlags)
private static int getConfidenceAdjusted(final int conf, final int state, final int channelFlags) {
int c = conf;
if (c < 0) {
c = 0;
}
if (state == 3 && channelFlags > 0) {
int correction = 1;
if (channelFlags > 2) {
correction = 2;
}
return c * (100 - (40 * correction)) / 100;
}
return c;
}
}