# How to Calculate the US EPA PM2.5 AQI

**URL:** <https://community.purpleair.com/t/how-to-calculate-the-us-epa-pm2-5-aqi/877>\
**Category:** Data\
**Created:** [June 7, 2022, 8:17pm UTC](https://community.purpleair.com/t/how-to-calculate-the-us-epa-pm2-5-aqi/877 "2022-06-07T20:17:56Z")\
**Posts on this page:** 12\
**Page:** 1

<div class="post-metadata">

**Author:** ![Ethan](https://sea2.discourse-cdn.com/flex020/user_avatar/community.purpleair.com/ethan/32/177_2.png) [@Ethan](https://community.purpleair.com/u/Ethan)\
**Post date:** [June 7, 2022, 8:17pm UTC](https://community.purpleair.com/t/how-to-calculate-the-us-epa-pm2-5-aqi/877/1 "2022-06-07T20:17:56Z")

</div>

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](https://aqs.epa.gov/aqsweb/documents/codetables/aqi_breakpoints.html). The AQI equation can be found on here: [https://forum.airnowtech.org/t/the-aqi-equation/169](https://forum.airnowtech.org/t/the-aqi-equation/169).

```javascript
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);
}

```

---

<div class="post-metadata">

**Author:** ![AndyB](https://avatars.discourse-cdn.com/v4/letter/a/96bed5/32.png) [@AndyB](https://community.purpleair.com/u/AndyB)\
**Post date:** [July 14, 2022, 4:26pm UTC](https://community.purpleair.com/t/how-to-calculate-the-us-epa-pm2-5-aqi/877/3 "2022-07-14T16:26:09Z")

</div>

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=](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](http://purpleair.com) map shows 60 and none of the numbers are even close.

I get the following:

```auto
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
                )

        )

)
```

---

<div class="post-metadata">

**Author:** ![Ethan](https://sea2.discourse-cdn.com/flex020/user_avatar/community.purpleair.com/ethan/32/177_2.png) [@Ethan](https://community.purpleair.com/u/Ethan)\
**Post date:** [July 14, 2022, 4:57pm UTC](https://community.purpleair.com/t/how-to-calculate-the-us-epa-pm2-5-aqi/877/4 "2022-07-14T16:57:57Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![AndyB](https://avatars.discourse-cdn.com/v4/letter/a/96bed5/32.png) [@AndyB](https://community.purpleair.com/u/AndyB)\
**Post date:** [July 14, 2022, 5:54pm UTC](https://community.purpleair.com/t/how-to-calculate-the-us-epa-pm2-5-aqi/877/5 "2022-07-14T17:54:15Z")

</div>

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:

```auto
// 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);
}
```

---

<div class="post-metadata">

**Author:** ![AndyB](https://avatars.discourse-cdn.com/v4/letter/a/96bed5/32.png) [@AndyB](https://community.purpleair.com/u/AndyB)\
**Post date:** [July 20, 2022, 6:37pm UTC](https://community.purpleair.com/t/how-to-calculate-the-us-epa-pm2-5-aqi/877/7 "2022-07-20T18:37:33Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![Ethan](https://sea2.discourse-cdn.com/flex020/user_avatar/community.purpleair.com/ethan/32/177_2.png) [@Ethan](https://community.purpleair.com/u/Ethan)\
**Post date:** [July 20, 2022, 7:06pm UTC](https://community.purpleair.com/t/how-to-calculate-the-us-epa-pm2-5-aqi/877/8 "2022-07-20T19:06:18Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![AndyB](https://avatars.discourse-cdn.com/v4/letter/a/96bed5/32.png) [@AndyB](https://community.purpleair.com/u/AndyB)\
**Post date:** [July 22, 2022, 8:39pm UTC](https://community.purpleair.com/t/how-to-calculate-the-us-epa-pm2-5-aqi/877/9 "2022-07-22T20:39:11Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![janet](https://sea2.discourse-cdn.com/flex020/user_avatar/community.purpleair.com/janet/32/904_2.png) [@janet](https://community.purpleair.com/u/janet)\
**Post date:** [August 5, 2022, 11:45pm UTC](https://community.purpleair.com/t/how-to-calculate-the-us-epa-pm2-5-aqi/877/10 "2022-08-05T23:45:25Z")

</div>

@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:

 ![image](https://us1.discourse-cdn.com/flex020/uploads/purpleair/original/1X/b070e3d0a70ca450f6b62d029f9a56632ce5cd70.png)

Found on: [Loading...](https://api.purpleair.com/#api-sensors-get-sensor-data)

---

<div class="post-metadata">

**Author:** ![CRRC\_Air\_Monitor](https://sea2.discourse-cdn.com/flex020/user_avatar/community.purpleair.com/crrc_air_monitor/32/1611_2.png) [@CRRC\_Air\_Monitor](https://community.purpleair.com/u/CRRC_Air_Monitor)\
**Post date:** [December 19, 2022, 9:12pm UTC](https://community.purpleair.com/t/how-to-calculate-the-us-epa-pm2-5-aqi/877/11 "2022-12-19T21:12:02Z")

</div>

here is the python variant of the code, in case someone needs it:

```auto
# 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)

```

---

<div class="post-metadata">

**Author:** ![David\_Mills](https://sea2.discourse-cdn.com/flex020/user_avatar/community.purpleair.com/david_mills/32/1731_2.png) [@David\_Mills](https://community.purpleair.com/u/David_Mills)\
**Post date:** [September 23, 2023, 5:09pm UTC](https://community.purpleair.com/t/how-to-calculate-the-us-epa-pm2-5-aqi/877/12 "2023-09-23T17:09:34Z")

</div>

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:

```auto
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))
}

```

---

<div class="post-metadata">

**Author:** ![Neal\_Barnett](https://sea2.discourse-cdn.com/flex020/user_avatar/community.purpleair.com/neal_barnett/32/3859_2.png) [@Neal\_Barnett](https://community.purpleair.com/u/Neal_Barnett)\
**Post date:** [October 21, 2023, 4:01pm UTC](https://community.purpleair.com/t/how-to-calculate-the-us-epa-pm2-5-aqi/877/13 "2023-10-21T16:01:02Z")

</div>

Excel VBA format

```auto
' 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
```

---

<div class="post-metadata">

**Author:** ![davepwhite](https://avatars.discourse-cdn.com/v4/letter/d/13edae/32.png) [@davepwhite](https://community.purpleair.com/u/davepwhite)\
**Post date:** [December 20, 2025, 6:21pm UTC](https://community.purpleair.com/t/how-to-calculate-the-us-epa-pm2-5-aqi/877/14 "2025-12-20T18:21:19Z")

</div>

Thanks for all this information!
