Ethan
(Ethan)
June 7, 2022, 8:17pm
1
The following code is used to calculate the US EPA PM2.5 AQI from a Raw PM 2.5 value. It is written in JavaScript. The US EPA PM2.5 AQI is calculated using the EPAβs breakpoints as described in this link: AQI Breakpoints | Air Quality System | US EPA . The AQI equation can be found on here: https://forum.airnowtech.org/t/the-aqi-equation/169 .
var AQI = aqiFromPM(pm25value);
function aqiFromPM(pm) {
if (isNaN(pm)) return "-";
if (pm == undefined) return "-";
if (pm < 0) return pm;
if (pm > 1000) return "-";
/* AQI RAW PM2.5
Good 0 - 50 | 0.0 β 12.0
Moderate 51 - 100 | 12.1 β 35.4
Unhealthy for Sensitive Groups 101 β 150 | 35.5 β 55.4
Unhealthy 151 β 200 | 55.5 β 150.4
Very Unhealthy 201 β 300 | 150.5 β 250.4
Hazardous 301 β 400 | 250.5 β 350.4
Hazardous 401 β 500 | 350.5 β 500.4
*/
if (pm > 350.5) {
return calcAQI(pm, 500, 401, 500.4, 350.5); //Hazardous
} else if (pm > 250.5) {
return calcAQI(pm, 400, 301, 350.4, 250.5); //Hazardous
} else if (pm > 150.5) {
return calcAQI(pm, 300, 201, 250.4, 150.5); //Very Unhealthy
} else if (pm > 55.5) {
return calcAQI(pm, 200, 151, 150.4, 55.5); //Unhealthy
} else if (pm > 35.5) {
return calcAQI(pm, 150, 101, 55.4, 35.5); //Unhealthy for Sensitive Groups
} else if (pm > 12.1) {
return calcAQI(pm, 100, 51, 35.4, 12.1); //Moderate
} else if (pm >= 0) {
return calcAQI(pm, 50, 0, 12, 0); //Good
} else {
return undefined;
}
}
function calcAQI(Cp, Ih, Il, BPh, BPl) {
var a = (Ih - Il);
var b = (BPh - BPl);
var c = (Cp - BPl);
return Math.round((a/b) * c + Il);
}
2 Likes
AndyB
July 14, 2022, 4:26pm
3
Hi Ethan,
Looks like I need to verify if my URL is correct. Iβm trying to get the data from Luna Park. My URL is:
https://api.purpleair.com/v1/sensors/78307?api_key= {my api key}
Iβm getting what appears to be valid data, but the purpleair.com map shows 60 and none of the numbers are even close.
I get the following:
Array
(
[api_version] => V1.0.10-0.0.17
[time_stamp] => 1657815652
[data_time_stamp] => 1657815636
[sensor] => Array
(
[sensor_index] => 78307
[last_modified] => 1603145243
[date_created] => 1602089165
[last_seen] => 1657815525
[private] => 0
[is_owner] => 0
[name] => Luna Park
[icon] => 0
[location_type] => 0
[model] => PA-II
[hardware] => 2.0+BME280+PMSX003-B+PMSX003-A
[led_brightness] => 35
[firmware_version] => 7.00
[rssi] => -54
[uptime] => 14070
[pa_latency] => 382
[memory] => 15728
[position_rating] => 5
[latitude] => 37.357487
[longitude] => -121.88776
[altitude] => 73
[channel_state] => 3
[channel_flags] => 0
[channel_flags_manual] => 0
[channel_flags_auto] => 0
[confidence] => 100
[confidence_auto] => 100
[confidence_manual] => 100
[humidity] => 43
[humidity_a] => 43
[temperature] => 80
[temperature_a] => 80
[pressure] => 1013.9
[pressure_a] => 1013.88
[analog_input] => 0.05
[pm1.0] => 10
[pm1.0_a] => 10.1
[pm1.0_b] => 9.8
[pm1.0_atm] => 10
[pm1.0_cf_1] => 10
[pm2.5] => 17.3
[pm2.5_a] => 17.4
[pm2.5_b] => 17.2
[pm2.5_atm] => 17.3
[pm2.5_cf_1] => 17.3
[pm2.5_alt] => 10.8
[pm2.5_alt_a] => 10.6
[pm2.5_alt_b] => 10.9
[pm10.0] => 18.6
[pm10.0_a] => 18.5
[pm10.0_b] => 18.6
[pm10.0_atm] => 18.6
[pm10.0_cf_1] => 18.6
[scattering_coefficient] => 28.7
[scattering_coefficient_a] => 29.4
[scattering_coefficient_b] => 28.1
[deciviews] => 14.9
[deciviews_a] => 15.1
[deciviews_b] => 14.7
[visual_range] => 87.7
[visual_range_a] => 86.2
[visual_range_b] => 89.3
[0.3_um_count] => 1915
[0.3_um_count_a] => 1959
[0.3_um_count_b] => 1872
[0.5_um_count] => 550
[0.5_um_count_a] => 561
[0.5_um_count_b] => 540
[1.0_um_count] => 124
[1.0_um_count_a] => 121
[1.0_um_count_b] => 128
[2.5_um_count] => 9
[2.5_um_count_a] => 10
[2.5_um_count_b] => 9
[5.0_um_count] => 1
[5.0_um_count_a] => 1
[5.0_um_count_b] => 2
[10.0_um_count] => 0
[10.0_um_count_a] => 0
[10.0_um_count_b] => 1
[pm1.0_atm_a] => 10.12
[pm2.5_atm_a] => 17.35
[pm10.0_atm_a] => 18.49
[pm1.0_cf_1_a] => 10.12
[pm2.5_cf_1_a] => 17.35
[pm10.0_cf_1_a] => 18.49
[pm1.0_atm_b] => 9.85
[pm2.5_atm_b] => 17.17
[pm10.0_atm_b] => 18.63
[pm1.0_cf_1_b] => 9.85
[pm2.5_cf_1_b] => 17.17
[pm10.0_cf_1_b] => 18.63
[primary_id_a] => 1177312
[primary_key_a] => WOK8AWYYN2Q5B3Z1
[primary_id_b] => 1177314
[primary_key_b] => MA6IRH6WTMJLUIYR
[secondary_id_a] => 1177313
[secondary_key_a] => 6MS419QA63EN2S61
[secondary_id_b] => 1177315
[secondary_key_b] => BBZQO50JDETGOJEN
[stats] => Array
(
[pm2.5] => 17.3
[pm2.5_10minute] => 16.4
[pm2.5_30minute] => 15
[pm2.5_60minute] => 13.4
[pm2.5_6hour] => 7.6
[pm2.5_24hour] => 5.3
[pm2.5_1week] => 5.2
[time_stamp] => 1657815525
)
[stats_a] => Array
(
[pm2.5] => 17.4
[pm2.5_10minute] => 16.2
[pm2.5_30minute] => 14.9
[pm2.5_60minute] => 13.2
[pm2.5_6hour] => 7.4
[pm2.5_24hour] => 5.2
[pm2.5_1week] => 5
[time_stamp] => 1657815525
)
[stats_b] => Array
(
[pm2.5] => 17.2
[pm2.5_10minute] => 16.5
[pm2.5_30minute] => 15.2
[pm2.5_60minute] => 13.5
[pm2.5_6hour] => 7.7
[pm2.5_24hour] => 5.4
[pm2.5_1week] => 5.3
[time_stamp] => 1657815525
)
)
)
Ethan
(Ethan)
July 14, 2022, 4:57pm
4
The numbers returned by the API are raw PM2.5 and must be converted to the AQI. You will most likely want to use the pm2.5_atm or pm2.5_cf_1 fields. Plugging those values (17.3) into the code written above outputs 62 AQI.
AndyB
July 14, 2022, 5:54pm
5
Thank you, Ethan. That works very well, itβs only off by a few points, but for my needs that is fine.
Hereβs the PHP code I used:
// get url
$url = ('https://api.purpleair.com/v1/sensors/78307?api_key={my api key}');
// Get the sensor data via JSON
$json = @file_get_contents($url);
$array = json_decode($json, true);
// get pm25value
$pm25value = $array['sensor']['pm2.5_cf_1_a'];
// check condition
if ($pm25value > 0 AND $pm25value < 1000)
{
// get aqi
$aqi = $this->aqiFromPM($pm25value);
}
else
{
$aqi = 'n/a';
}
function aqiFromPM($pm)
{
/* AQI RAW PM2.5
Good 0 - 50 | 0.0 β 12.0
Moderate 51 - 100 | 12.1 β 35.4
Unhealthy for Sensitive Groups 101 β 150 | 35.5 β 55.4
Unhealthy 151 β 200 | 55.5 β 150.4
Very Unhealthy 201 β 300 | 150.5 β 250.4
Hazardous 301 β 400 | 250.5 β 350.4
Hazardous 401 β 500 | 350.5 β 500.4
*/
if ($pm > 350.5)
{
return $this->calcAQI($pm, 500, 401, 500.4, 350.5); // Hazardous
}
else if ($pm > 250.5)
{
return $this->calcAQI($pm, 400, 301, 350.4, 250.5); // Hazardous
}
else if ($pm > 150.5)
{
return $this->calcAQI($pm, 300, 201, 250.4, 150.5); // Very Unhealthy
}
else if ($pm > 55.5)
{
return $this->calcAQI($pm, 200, 151, 150.4, 55.5); // Unhealthy
}
else if ($pm > 35.5)
{
return $this->calcAQI($pm, 150, 101, 55.4, 35.5); // Unhealthy for Sensitive Groups
}
else if ($pm > 12.1)
{
return $this->calcAQI($pm, 100, 51, 35.4, 12.1); // Moderate
}
else if ($pm >= 0)
{
return $this->calcAQI($pm, 50, 0, 12, 0); // Good
}
}
function calcAQI($Cp, $Ih, $Il, $BPh, $BPl)
{
$a = ($Ih - $Il);
$b = ($BPh - $BPl);
$c = ($Cp - $BPl);
return round(($a/$b) * $c + $Il);
}
AndyB
July 20, 2022, 6:37pm
7
Today I checked my calculation (see post above) against the purpleair real-time map and the map shows 40 for the location Iβm using, while my calculation shows 26. Is there another value I need to use to get closer to the 40 AQI?
Thank you.
Ethan
(Ethan)
July 20, 2022, 7:06pm
8
What is the raw pm2.5 value you input? Also, ensure that you are viewing the US EPA PM2.5 AQI data layer without any conversion factors on the map.
AndyB
July 22, 2022, 8:39pm
9
Hi Ethan,
I was looking at the pm2.5_cf_1_a value. I since changed it to pm2.5_cf_1 and my calculated AQI is much closer.
Sure would be nice if someone knew the exact code to match what is shown in the real time map. Or better yet if the API would include this value.
Thank you.
1 Like
janet
(Janet)
August 5, 2022, 11:45pm
10
@AndyB We might have the same issue. I thought my math was off until I realized the Purple Air map uses a 10 minute average as the main value rather than a point value to calculate AQI (although further down you can see the real-time data on the map also). Once I switched to using the 10-minute average my calculated values match the main value on the map.
Iβm using Python but I believe in php it would be:
$pm25value = $array['sensor']['stats']['pm2.5_10minute']
There are some other handy averages for longer time periods in the data stream.
screenshot of options:
Found on: Loading...
here is the python variant of the code, in case someone needs it:
# Convert US AQI from raw pm2.5 data
def aqiFromPM(pm):
if not float(pm):
return "-"
if pm == 'undefined':
return "-"
if pm < 0:
return pm
if pm > 1000:
return "-"
"""
AQI | RAW PM2.5
Good 0 - 50 | 0.0 β 12.0
Moderate 51 - 100 | 12.1 β 35.4
Unhealthy for Sensitive Groups 101 β 150 | 35.5 β 55.4
Unhealthy 151 β 200 | 55.5 β 150.4
Very Unhealthy 201 β 300 | 150.5 β 250.4
Hazardous 301 β 400 | 250.5 β 350.4
Hazardous 401 β 500 | 350.5 β 500.4
"""
if pm > 350.5:
return calcAQI(pm, 500, 401, 500.4, 350.5) # Hazardous
elif pm > 250.5:
return calcAQI(pm, 400, 301, 350.4, 250.5) # Hazardous
elif pm > 150.5:
return calcAQI(pm, 300, 201, 250.4, 150.5) # Very Unhealthy
elif pm > 55.5:
return calcAQI(pm, 200, 151, 150.4, 55.5) # Unhealthy
elif pm > 35.5:
return calcAQI(pm, 150, 101, 55.4, 35.5) # Unhealthy for Sensitive Groups
elif pm > 12.1:
return calcAQI(pm, 100, 51, 35.4, 12.1) # Moderate
elif pm >= 0:
return calcAQI(pm, 50, 0, 12, 0) # Good
else:
return 'undefined'
# Calculate AQI from standard ranges
def calcAQI(Cp, Ih, Il, BPh, BPl):
a = (Ih - Il)
b = (BPh - BPl)
c = (Cp - BPl)
return round((a / b) * c + Il)
2 Likes
David_Mills
(David Mills)
September 23, 2023, 5:09pm
12
I clean my data up before it gets to this point, so I took out the first four conditional statements.
Without those, here is the R variant of the code in case someone needs it:
aqiFromPM <- function(pm) {
# AQI RAW PM2.5
# Good 0 - 50 | 0.0 β 12.0
# Moderate 51 - 100 | 12.1 β 35.4
# Unhealthy for Sensitive Groups 101 β 150 | 35.5 β 55.4
# Unhealthy 151 β 200 | 55.5 β 150.4
# Very Unhealthy 201 β 300 | 150.5 β 250.4
# Hazardous 301 β 400 | 250.5 β 350.4
# Hazardous 401 β 500 | 350.5 β 500.4
if (pm > 350.5) {
return(calcAQI(pm, 500, 401, 500.4, 350.5)) # Hazardous
} else if (pm > 250.5) {
return(calcAQI(pm, 400, 301, 350.4, 250.5)) # Hazardous
} else if (pm > 150.5) {
return(calcAQI(pm, 300, 201, 250.4, 150.5)) # Very Unhealthy
} else if (pm > 55.5) {
return(calcAQI(pm, 200, 151, 150.4, 55.5)) # Unhealthy
} else if (pm > 35.5) {
return(calcAQI(pm, 150, 101, 55.4, 35.5)) # Unhealthy for Sensitive Groups
} else if (pm > 12.1) {
return(calcAQI(pm, 100, 51, 35.4, 12.1)) # Moderate
} else if (pm >= 0) {
return(calcAQI(pm, 50, 0, 12, 0)) # Good
} else {
return(undefined)
}
}
calcAQI <- function(Cp, Ih, Il, BPh, BPl) {
a <- (Ih - Il)
b <- (BPh - BPl)
c <- (Cp - BPl)
return(round((a/b) * c + Il))
}
1 Like
Excel VBA format
' Convert US AQI from raw pm2.5 data
Function AQIFromPM(pm) As Single
Dim answer As Single
If Not IsNumeric(pm) Then
answer = -1
ElseIf pm < 0 Then
answer = pm
ElseIf pm > 1000 Then
answer = -1
Else
answer = 0
End If
' AQI | RAW PM2.5
' Good 0 - 50 | 0.0 β 12.0
' Moderate 51 - 100 | 12.1 β 35.4
' Unhealthy for Sensitive Groups 101 β 150 | 35.5 β 55.4
' Unhealthy 151 β 200 | 55.5 β 150.4
' Very Unhealthy 201 β 300 | 150.5 β 250.4
' Hazardous 301 β 400 | 250.5 β 350.4
' Hazardous 401 β 500 | 350.5 β 500.4
If answer >= 0 Then
If pm > 350.5 Then
answer = calcAQI(pm, 500, 401, 500.4, 350.5) ' Hazardous
ElseIf pm > 250.5 Then
answer = calcAQI(pm, 400, 301, 350.4, 250.5) ' Hazardous
ElseIf pm > 150.5 Then
answer = calcAQI(pm, 300, 201, 250.4, 150.5) ' Very Unhealthy
ElseIf pm > 55.5 Then
answer = calcAQI(pm, 200, 151, 150.4, 55.5) ' Unhealthy
ElseIf pm > 35.5 Then
answer = calcAQI(pm, 150, 101, 55.4, 35.5) ' Unhealthy for Sensitive Groups
ElseIf pm > 12.1 Then
answer = calcAQI(pm, 100, 51, 35.4, 12.1) ' Moderate
ElseIf pm >= 0 Then
answer = calcAQI(pm, 50, 0, 12, 0) ' Good
Else
answer = -1
End If
End If
AQIFromPM = answer
End Function
' Calculate AQI from standard ranges
Function calcAQI(Cp, Ih, Il, BPh, BPl) As Single
Dim a As Single, b As Single, c As Single
a = (Ih - Il)
b = (BPh - BPl)
c = (Cp - BPl)
calcAQI = Round((a / b) * c + Il)
End Function