Reordering CSV File

Problem

You want to reorder a CSV file to match the OEChem TK CSV File Format.

Ingredients

Difficulty Level

🌶️

Download

Download code

reordercsv.py

See also the Usage subsection.

Source Code

reordercsv
#!/usr/bin/env python3
# (C) 2026 Cadence Design Systems, Inc. (Cadence)
# All rights reserved.
# TERMS FOR USE OF SAMPLE CODE The software below ("Sample Code") is
# provided to current licensees or subscribers of Cadence products or
# SaaS offerings (each a "Customer").
# Customer is hereby permitted to use, copy, and modify the Sample Code,
# subject to these terms. Cadence claims no rights to Customer's
# modifications. Modification of Sample Code is at Customer's sole and
# exclusive risk. Sample Code may require Customer to have a then
# current license or subscription to the applicable Cadence offering.
# THE SAMPLE CODE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED.  OPENEYE DISCLAIMS ALL WARRANTIES, INCLUDING, BUT
# NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
# PARTICULAR PURPOSE AND NONINFRINGEMENT. In no event shall Cadence be
# liable for any damages or liability in connection with the Sample Code
# or its use.

"""Parse a CSV file and reorder it in the OEChem TK CSV file format."""

import argparse
import csv
import os
import sys
from pathlib import Path

from openeye import oechem
from rich.console import Console
from rich_argparse import HelpPreviewAction, RichHelpFormatter

__SCRIPT_NAME__ = Path(__file__).absolute().stem
__SCRIPT_DESC__ = "Parse a CSV file and reorder it in the OEChem TK CSV file format."
__SCRIPT_TOOLKITS__ = ["oechem"]


def parse_options() -> argparse.Namespace:
    """Set up command line options."""
    parser = argparse.ArgumentParser(
        add_help=True,
        formatter_class=RichHelpFormatter,
        description="[yellow]" + __SCRIPT_DESC__ + "[/yellow]",
    )
    parser.add_argument(
        "--in",
        dest="in_file",
        type=str,
        required=True,
        help="Input CSV filename",
    )
    parser.add_argument(
        "--out",
        dest="out_file",
        type=str,
        required=True,
        help="Output CSV filename",
    )
    parser.add_argument(
        "--no-header",
        action="store_true",
        default=False,
        help="Set if the CSV does not have a header",
    )
    parser.add_argument("--help-image", action=HelpPreviewAction)
    return parser.parse_args()


def main() -> int:
    """Parse and reorder a CSV file."""
    args = parse_options()
    console = Console()

    in_file: Path = Path(args.in_file)
    out_file: Path = Path(args.out_file)
    has_header: bool = not args.no_header

    if in_file.suffix != ".csv":
        oechem.OEThrow.Fatal(f"Input {in_file.name} must be CSV format!")

    if out_file.suffix != ".csv":
        oechem.OEThrow.Fatal(f"Output {out_file.name} must be CSV format!")

    header = _get_header(in_file, has_header)

    with in_file.open() as inf, out_file.open("w", newline="") as ofile:
        writer = csv.DictWriter(ofile, fieldnames=header)
        writer.writeheader()
        for row in csv.DictReader(inf):
            writer.writerow(row)

    console.print(f"[green]Reordered CSV written to:[/green] {out_file.name}")
    return os.EX_OK


def _get_header(iname: Path, has_header: bool) -> list[str]:
    """Extract and reorder the CSV header."""
    with iname.open() as f:
        header = list(csv.DictReader(f).fieldnames)

    if has_header:
        if not any("smiles" in h.lower() for h in header):
            oechem.OEThrow.Fatal("Input CSV file must have a SMILES column!")

        # find the SMILES column and move it to the front
        smiles = header.pop(
            next(i for i, h in enumerate(header) if "smiles" in h.lower())
        )
        header.insert(0, smiles)

        # move the TITLE column to be right after the SMILES
        if any("title" in h.lower() for h in header):
            title = header.pop(
                next(i for i, h in enumerate(header) if "title" in h.lower())
            )
            header.insert(1, title)
        else:
            header.insert(1, "TITLE")

    else:
        # find column that can be interpreted as SMILES
        smiles = None
        for i, h in enumerate(header):
            if _is_valid_smiles(h):
                smiles = header.pop(i)
                break
        if smiles is not None:
            header.insert(0, smiles)
        header.insert(1, "")

    return header


def _is_valid_smiles(field: str) -> bool:
    """Check if a field is a valid SMILES string."""
    mol = oechem.OEGraphMol()
    opts = oechem.OEParseSmilesOptions()
    opts.SetQuiet(True)
    return oechem.OEParseSmiles(mol, field, opts)


setattr(main, "__SCRIPT_NAME__", __SCRIPT_NAME__)
setattr(main, "__SCRIPT_DESC__", __SCRIPT_DESC__)
setattr(main, "__SCRIPT_TOOLKITS__", __SCRIPT_TOOLKITS__)

if __name__ == "__main__":
    sys.exit(main())

Solution

The OEChem TK CSV File Format expects the first column to be a SMILES string representing the molecule and the second column to be the molecule title. The CSV file can be with or without a header line.

The _get_header function handles various CSV file inputs. If the input CSV file has a header line, then the SMILES and the title field is identified by (case insensitive) substring matching. If there is no header line, then at least one field in the first line has to be interpreted as a valid SMILES string.

def _get_header(iname: Path, has_header: bool) -> list[str]:
    """Extract and reorder the CSV header."""
    with iname.open() as f:
        header = list(csv.DictReader(f).fieldnames)

    if has_header:
        if not any("smiles" in h.lower() for h in header):
            oechem.OEThrow.Fatal("Input CSV file must have a SMILES column!")

        # find the SMILES column and move it to the front
        smiles = header.pop(
            next(i for i, h in enumerate(header) if "smiles" in h.lower())
        )
        header.insert(0, smiles)

        # move the TITLE column to be right after the SMILES
        if any("title" in h.lower() for h in header):
            title = header.pop(
                next(i for i, h in enumerate(header) if "title" in h.lower())
            )
            header.insert(1, title)
        else:
            header.insert(1, "TITLE")

    else:
        # find column that can be interpreted as SMILES
        smiles = None
        for i, h in enumerate(header):
            if _is_valid_smiles(h):
                smiles = header.pop(i)
                break
        if smiles is not None:
            header.insert(0, smiles)
        header.insert(1, "")

    return header

After the header of the file is determined the script reorders the file accordingly.

Usage

See Download section to download the script.

> reordercsv --help
../_images/reordercsv-help.svg
> reordercsv --in test.csv --out reordered.csv

that will generate the test.csv file to

xxSMILESxx,TITLE,Case,MTP,diameter,petitjean
O=C1Cc2ccccc21,,0,14,5,0.40000001
Clc1ccc(cc1)C1c2c(OC(N)=C1C#N)[nH][nH0]c2C(F)(F)F,,0,20.5,9,0.44444445
O=C(OC)C(=Cc1ccccc1)Cc1ccccc1,,0,27.5,10,0.5
FC(F)(F)c1[nH0]cc2ccccc2c1,,0,30.5,7,0.42857143

To reorder a CSV file without a header line, use the --no-header flag:

> reordercsv --in no-header.csv --out reordered.csv --no-header

See also in OEChem TK manual

Theory

API