Update to the latest AQI ranges

Is That Right

The RaspberryPi powered AQI visualization showing a inaccurate AQI of 666

What's Changing

As I noted in the AQI Everywhere post the forum where I had sourced my current AQI calculation had indicated that it was now obsolete, and instead pointed to an updated calculation.

After walking though the update post, it appeared that the only change was in μg/m3 ranges for each AQI level. Since monitor keys of PM2.5 I had to compare the first (obsolete) set of ranges with the second (updated) ranges and update the calculation code accordingly.

Obsolete Table

Obsolete Table of μg/m3 to AQI values for PM25 particulate pollution

Current Table

Current table of μg/m3 to AQI values for PM25 particulate pollution

You can see that in the for the "Good" AQI rating, the AQI Number is still between 0 and 50, but the μg/m3 ranges from a low of 0.0 to a hight of 9.0 in the updated calculation. The μg/m3 used to range between 9 and 12.0. To figure out what this means, lets look at the formula being applied to the raw μg/m3 data to calculate the final AQI value & level.

Formula

AQI Calculation: $$ AQI = \frac{(AQI_{Hi}) - (AQI_{Lo})}{(Conc_{Hi}) - (Conc_{Lo})} * ((\bold{Conc_{i}}) - (Conc_{Lo})) + (AQI_{Lo}) $$

For the Good AQI Range with a raw reading of 4.5 μg/m3 Good AQI Calculation: $$ AQI = \frac{(50) - (0)}{(9.0) - (0)} * ((\bold{4.5}) - (0)) + (0) $$ Simplified Good AQI Calculation: $$ AQI = \frac{50}{9.0} * \bold{4.5} $$

The raw reading of 4.5 μg/m3 would generate an AQI score of 25. The formulate is basically mapping the raw range into the AQI range values. Good AQI has raw values from 0-9, where 0 μg/m3 = 0 AQI and 9.0 μg/m3 = 50 AQI. Since 4.5 μg/m3 is half way between 0 and 9. It's AQI is half way between 0 and 50.

What do I need to fix?

In the end, I had to update a pretty straight forward function that figures out which "AQI Range" the raw reading falls into. I just needed to map the values from the updated table into my function and I'd be current. I'd be upgrading some particulate matter ratings to higher risk levels, and then handling the slightly confusing Concentrations above 325.4 ug/m3 are still considered Hazardous and use the Hazardous breakpoints when calculating AQI line. Some previously Unhealthy Level concentrations were now Hazardous, but now the AQI Numerical values for concentrations above 325.4 ug/m3 would overlap with those between 225.5 and 325.4 ug/m3.

Obsolete Range Calculation

 1const pm25aqi = (concentration) => {
 2    const _conc = parseFloat(concentration);
 3    const c = (Math.floor(10 * _conc)) / 10;
 4    switch (true) {
 5        case (c >= 0 && c < 12.1):
 6            return aqiEquation(50, 0, 12, 0, c);
 7        case (c >= 12.1 && c < 35.5):
 8            return aqiEquation(100, 51, 35.4, 12.1, c);
 9        case (c >= 35.5 && c < 55.5):
10            return aqiEquation(150, 101, 55.4, 35.5, c);
11        case (c >= 55.5 && c < 150.5):
12            return aqiEquation(200, 151, 150.4, 55.5, c);
13        case (c >= 150.5 && c < 250.5):
14            return aqiEquation(300, 201, 250.4, 150.5, c);
15        case (c >= 250.5 && c < 350.5):
16            return aqiEquation(400, 301, 350.4, 250.5, c);
17        case (c >= 350.5 && c < 500.5):
18            return aqiEquation(500, 401, 500.4, 350.5, c);
19        default:
20            // We're in hell
21            return 666;
22    }
23}

First Pass at an update

 1const pm25aqi = (concentration) => {
 2    const _conc = parseFloat(concentration);
 3    const c = (Math.floor(1000 * _conc)) / 1000;
 4    switch (true) {
 5        case (c >= 0 && c < 9):
 6            return aqiEquation(50, 0, 9, 0, c);
 7        case (c >= 9.1 && c < 35.4):
 8            return aqiEquation(100, 51, 35.4, 9.1, c);
 9        case (c >= 35.5 && c < 55.5):
10            return aqiEquation(150, 101, 55.4, 35.5, c);
11        case (c >= 55.5 && c < 125.4):
12            return aqiEquation(200, 151, 125.4, 55.5, c);
13        case (c >= 125.5 && c < 225.4):
14            return aqiEquation(300, 201, 225.4, 125.5, c);
15        case (c >= 225.5 && c < 325.4):
16            return aqiEquation(500, 301, 325.4, 225.5, c);
17        case (c >= 325.5 && c < 500.4):
18            return aqiEquation(500, 301, 325.4, 225.5, c);
19        default:
20            // We're in hell
21            return 666;
22    }
23};

I updated my function and everything was awesome, almost. I happened to glance at the visualization a few days back, and saw the screen above. I looked outside, sniffed, and became somewhat confused. within a minute the rating had updated back to 47 AQI/Green and everything was right with the world.

Except, it happened again. So there had to be a bug, in my code. Unbelievable. Of course a quick look at the lines below and I saw issue. If I had a concentration of 9, I would fall through to the default 666 value.

1case (c >= 0 && c < 9):
2            return aqiEquation(50, 0, 9, 0, c);
3        case (c >= 9.1 && c < 35.4):

There were a few other quirks I had to iron out, but I think I've got it corrected with the version below.

 1const pm25aqi = (concentration) => {
 2    const _conc = parseFloat(concentration);
 3    const c = (Math.floor(1000 * _conc)) / 1000;
 4    switch (true) {
 5        case (c >= 0 && c <= 9):
 6            return aqiEquation(50, 0, 9, 0, c);
 7        case (c >= 9.1 && c <= 35.4):
 8            return aqiEquation(100, 51, 35.4, 9.1, c);
 9        case (c >= 35.5 && c <= 55.4):
10            return aqiEquation(150, 101, 55.4, 35.5, c);
11        case (c >= 55.5 && c <= 125.4):
12            return aqiEquation(200, 151, 125.4, 55.5, c);
13        case (c >= 125.5 && c <= 225.4):
14            return aqiEquation(300, 201, 225.4, 125.5, c);
15        case (c >= 225.5 && c <= 325.4):
16            return aqiEquation(500, 301, 325.4, 225.5, c);
17        case (c >= 325.5 && c <= 500.4):
18            return aqiEquation(500, 301, 500.4, 325.5, c);
19        default:
20            // We're in hell
21            return 666;
22    }
23};

Better

I'll revisit this function the future. I want to walk through it in more detail to see if it work's the way I'm expecting it to. I've also realized I need to clean up my repo. I plan to make specific ones for the each visualization approach, as well as monitor only code. It's way to hard to know what's actually being used, and what was just throw away code.

QOTD

“The dream begins with a teacher who believes in you, who tugs and pushes and leads you to the next plateau, sometimes poking you with a sharp stick called truth.” ― Dan Rather

Posts in this series