Experimental properties from PubChem View¶
PubChem's PUG-View service holds the experimental data deposited for a
compound: melting and boiling points, vapour pressure, solubility, logP,
flash point and more, each value with the source it came from. The values are
prose written by whoever deposited them ("173.3 °F at 760 mmHg (NTP, 1992)"), and PubChemView turns that prose back into numbers, in SI units,
so values deposited in °F and °C can be compared.
This tutorial builds a small property table for five solvents. Experimental properties from PubChem lists every field and the parser's rules.
PubChem View is an online service, and every cell that fetches data here needs the network. The outputs below were produced on 2026-09-23.
import pandas as pd
from provesid import PubChemID, PubChemView
pd.set_option("display.width", 200)
pd.set_option("display.max_columns", 12)
pd.set_option("display.max_colwidth", 45)
CIDs, offline¶
PubChem View is asked by CID. If the PubChem identifier database is installed, the CIDs come from disk:
solvents = {"acetone": "67-64-1", "ethanol": "64-17-5", "methanol": "67-56-1",
"benzene": "71-43-2", "toluene": "108-88-3"}
with PubChemID() as db:
cid_of = db.batch_cas_to_cid(list(solvents.values()))
cids = {name: cid_of[cas] for name, cas in solvents.items()}
cids
{'acetone': 180,
'ethanol': 702,
'methanol': 887,
'benzene': 241,
'toluene': 1140}
What PubChem holds for a compound¶
view = PubChemView()
view.get_available_properties(cids["ethanol"])
['Physical Description', 'Color/Form', 'Odor', 'Taste', 'Boiling Point', 'Melting Point', 'Flash Point', 'Solubility', 'Density', 'Vapor Density', 'Vapor Pressure', 'LogP', "Henry's Law Constant", 'Stability/Shelf Life', 'Autoignition Temperature', 'Viscosity', 'Heat of Combustion', 'Heat of Vaporization', 'Surface Tension', 'Ionization Potential', 'Odor Threshold', 'Refractive Index', 'Dissociation Constants', 'Kovats Retention Index', 'Other Experimental Properties']
The names are PUG-View's headings, and they must be spelled exactly. A heading
the compound does not have gives an empty result, not an error. Any PUG-View
heading works, not only the experimental ones. Acute toxicity values such as
LD50 are under "Non-Human Toxicity Values":
[entry.value for entry in view.extract_property_data(cids["ethanol"], "Non-Human Toxicity Values")]
['LD50 Mouse iv 2.0 g/L', 'LD50 Mouse sc 8.3 g/L', 'LD50 Mouse ip 0.9 g/L', 'LC50 Mouse inhalation 39 g/cu m/4 hr', 'For more Non-Human Toxicity Values (Complete) data for Ethanol (16 total), please visit the HSDB record page.']
One heading: the text and the numbers¶
extract_property_data returns one PropertyData per deposited value. value
is the text exactly as PubChem has it, and parsed holds the numbers
recovered from it:
for entry in view.extract_property_data(cids["ethanol"], "Boiling Point"):
p = entry.parsed
print(f"{entry.value!r:38} -> {p.value} {p.unit} = {p.value_si:.2f} {p.unit_si}")
'173.3 °F at 760 mmHg (NTP, 1992)' -> 173.3 °F = 351.65 K '78.2 °C' -> 78.2 °C = 351.35 K '78.24 °C' -> 78.24 °C = 351.39 K '78 °C' -> 78.0 °C = 351.15 K '78 °C' -> 78.0 °C = 351.15 K '173 °F' -> 173.0 °F = 351.48 K '78.29 °C @760 [mm Hg]' -> 78.29 °C = 351.44 K '173 °F' -> 173.0 °F = 351.48 K
The °F and °C entries agree once both are in kelvin. Comparing the raw numbers would have put ethanol's boiling point at 173.
As a table¶
get_property_table gives the same data as a DataFrame, with PubChem's
reference numbers resolved to full citations:
vp = view.get_property_table(cids["acetone"], "Vapor Pressure")
vp[["StringWithMarkup", "ExperimentalValue", "Unit", "ValueSI", "UnitSI", "Temperature"]]
| StringWithMarkup | ExperimentalValue | Unit | ValueSI | UnitSI | Temperature | |
|---|---|---|---|---|---|---|
| 0 | 180 mmHg at 68 °F ; 270 mmHg at 86 °F (NT... | 180.0 | mmHg | 23998.029735 | Pa | 20.0 |
| 1 | 231.0 [mmHg] | 231.0 | mmHg | 30797.471493 | Pa | NaN |
| 2 | 231 mm Hg at 25 °C | 231.0 | mmHg | 30797.471493 | Pa | 25.0 |
| 3 | Vapor pressure, kPa at 20 °C: 24 | 24.0 | kPa | 24000.000000 | Pa | 20.0 |
| 4 | 180 mmHg | 180.0 | mmHg | 23998.029735 | Pa | NaN |
| 5 | 180 [mm Hg] @20 °C | 180.0 | mmHg | 23998.029735 | Pa | 20.0 |
| 6 | 180 mmHg | 180.0 | mmHg | 23998.029735 | Pa | NaN |
Temperature is the condition the value was measured at, not a value. The
first entry reads 180 mmHg at 68 °F ; 270 mmHg at 86 °F: the parser takes
the first value and its temperature, and the full text stays in
StringWithMarkup. Entries with no temperature are usually at room
temperature, but the parser does not guess.
list(vp.columns)
['CID', 'Heading', 'StringWithMarkup', 'ExperimentalValue', 'ValueMin', 'ValueMax', 'Unit', 'ValueSI', 'ValueMinSI', 'ValueMaxSI', 'UnitSI', 'Operator', 'Qualitative', 'Temperature', 'Conditions', 'FullReference']
A property table for five solvents¶
One row per solvent, one column per property, each the median of the entries PubChem holds, in SI units. For vapour pressure only entries measured between 20 and 25 °C count:
headings = ["Melting Point", "Boiling Point", "Flash Point", "Vapor Pressure"]
rows = []
for name, cid in cids.items():
row = {"solvent": name}
for heading in headings:
table = view.get_property_table(cid, heading)
if heading == "Vapor Pressure":
table = table[table["Temperature"].between(20, 25)]
row[heading] = table["ValueSI"].median()
row[f"n {heading}"] = table["ValueSI"].notna().sum()
rows.append(row)
properties = pd.DataFrame(rows).set_index("solvent")
properties
| Melting Point | n Melting Point | Boiling Point | n Boiling Point | Flash Point | n Flash Point | Vapor Pressure | n Vapor Pressure | |
|---|---|---|---|---|---|---|---|---|
| solvent | ||||||||
| acetone | 178.250000 | 7 | 329.230000 | 7 | 255.372222 | 8 | 23999.014867 | 4 |
| ethanol | 159.050000 | 8 | 351.415000 | 8 | 285.927778 | 8 | 6853.008787 | 2 |
| methanol | 175.372222 | 7 | 337.755556 | 6 | 284.261111 | 9 | 13116.119371 | 4 |
| benzene | 278.650000 | 7 | 353.235000 | 6 | 262.038889 | 7 | 10000.000000 | 3 |
| toluene | 178.150000 | 8 | 383.780000 | 7 | 277.594444 | 7 | 3793.177901 | 2 |
Kelvin and pascal are the SI units the parser uses. Converting for display is ordinary pandas:
pd.DataFrame({
"mp (°C)": properties["Melting Point"] - 273.15,
"bp (°C)": properties["Boiling Point"] - 273.15,
"flash point (°C)": properties["Flash Point"] - 273.15,
"vp at 20-25 °C (kPa)": properties["Vapor Pressure"] / 1000,
}).round(1)
| mp (°C) | bp (°C) | flash point (°C) | vp at 20-25 °C (kPa) | |
|---|---|---|---|---|
| solvent | ||||
| acetone | -94.9 | 56.1 | -17.8 | 24.0 |
| ethanol | -114.1 | 78.3 | 12.8 | 6.9 |
| methanol | -97.8 | 64.6 | 11.1 | 13.1 |
| benzene | 5.5 | 80.1 | -11.1 | 10.0 |
| toluene | -95.0 | 110.6 | 4.4 | 3.8 |
The n columns show how many entries each median rests on. Before using a
number, look at the entries behind it. Methanol's flash points mix closed-cup
and open-cup measurements, which differ by several degrees, and the method is
only in the text:
fp = view.get_property_table(cids["methanol"], "Flash Point")
fp[["StringWithMarkup", "ValueSI", "Conditions", "FullReference"]]
| StringWithMarkup | ValueSI | Conditions | FullReference | |
|---|---|---|---|---|
| 0 | 52 °F (NTP, 1992) | 284.261111 | NaN | CAMEO Chemicals | METHANOL | CAMEO Chemic... |
| 1 | 52 °F (NFPA, 2010) | 284.261111 | NaN | CAMEO Chemicals | METHANOL, TALLOW ALKYL ... |
| 2 | 54 °F | 285.372222 | NaN | Haz-Map, Information on Hazardous Chemica... |
| 3 | 9.7 °C (49.5 °F) - closed cup | 282.850000 | NaN | Hazardous Substances Data Bank (HSDB) | M... |
| 4 | 15.6 °C (open cup) /from table/ | 288.750000 | from table | Hazardous Substances Data Bank (HSDB) | M... |
| 5 | 52 °F (11 °C) (closed cup) | 284.261111 | NaN | Hazardous Substances Data Bank (HSDB) | M... |
| 6 | 9 °C c.c. | 282.150000 | NaN | ILO-WHO International Chemical Safety Car... |
| 7 | 52 °F | 284.261111 | NaN | Occupational Safety and Health Administra... |
| 8 | 52 °F | 284.261111 | NaN | The National Institute for Occupational S... |
Headings that are not numbers¶
Some entries hold words, not numbers. The text is always kept, and the parser
records the word it recognised where a number would be. Not every string
yields something: Solubility in water, g/100ml at 25 °C: 0.18 is not
parsed, and a percentage is never converted, because that would need a
density the string does not give:
solubility = view.get_property_table(cids["benzene"], "Solubility")
solubility[["StringWithMarkup", "ValueSI", "UnitSI", "Qualitative"]].head(8)
| StringWithMarkup | ValueSI | UnitSI | Qualitative | |
|---|---|---|---|---|
| 0 | 1 to 5 mg/mL at 64 °F (NTP, 1992) | NaN | kg/m³ | NaN |
| 1 | In water, 1.79X10+3 mg/L at 25 °C | 1.79 | kg/m³ | NaN |
| 2 | Miscible with alcohol, chloroform, ether,... | NaN | NaN | miscible |
| 3 | Miscible with ethanol, ethyl ether, aceto... | NaN | NaN | miscible |
| 4 | 1.79 mg/mL | 1.79 | kg/m³ | NaN |
| 5 | Solubility in water, g/100ml at 25 °C: 0.18 | NaN | NaN | NaN |
| 6 | 0.07% | NaN | NaN | NaN |
parse_value is the same parser on its own, which is the quickest way to see
what it does with a string:
from provesid import parse_value
for text, heading in [("138-140", "Melting Point"),
("8.5X10-5 mm Hg at 25 °C", "Vapor Pressure"),
("greater than or equal to 100 mg/mL", "Solubility"),
("Miscible with water", "Solubility"),
("log Kow = 1.19", "LogP")]:
v = parse_value(text, heading)
print(f"{text!r:38} min={v.value_min} max={v.value_max} {v.unit} "
f"-> SI {v.value_min_si} {v.unit_si or ''} op={v.operator} "
f"qualitative={v.qualitative}")
'138-140' min=138.0 max=140.0 °C -> SI 411.15 K op=None qualitative=None '8.5X10-5 mm Hg at 25 °C' min=8.5e-05 max=8.5e-05 mmHg -> SI 0.011332402930275001 Pa op=None qualitative=None 'greater than or equal to 100 mg/mL' min=100.0 max=100.0 mg/mL -> SI 100.0 kg/m³ op=>= qualitative=None 'Miscible with water' min=None max=None None -> SI None op=None qualitative=miscible 'log Kow = 1.19' min=1.19 max=1.19 None -> SI None op=None qualitative=None
A range such as 138-140 has no single value. value_min and value_max
hold its bounds, and for a single value they are equal, so a filter on them
needs no special case.
Requests and the cache¶
Each heading for each compound is one request. PubChem allows five requests a second per address. The client paces itself, waits when PubChem asks it to back off, and gives up on a host that has asked for a longer pause than it is willing to wait. Answers are cached on disk, so running this notebook a second time makes no requests. See Network behaviour and Caching.