Python module
Lesson 14
DataFrames and Postgres
Move data between pandas and PostgreSQL with psycopg — load from a DataFrame, read query results back.
beginner40 minRun on your machine
What you will be able to do
- Insert DataFrame rows with executemany
- Read query results into a DataFrame
- Keep the load path explicit without an ORM
Why this matters for data work
Explore in pandas, land in Postgres. Many teams avoid hiding SQL behind heavy ORMs in pipelines — explicit inserts and queries are easier to debug. We stay on psycopg only (no SQLAlchemy), so DataFrame.to_sql is out of scope on purpose.
Concepts
DataFrame → DB — iterate rows (itertuples is fine at lab scale) or build sequences for executemany.
DB → DataFrame — cur.fetchall() plus column names from cur.description, or build a DataFrame from the result.
dtypes vs SQL types — map intentionally: int → INTEGER, float → NUMERIC, str → TEXT, timestamps → TIMESTAMPTZ.
Idempotent loads — TRUNCATE before reload in labs; production uses staging tables and swaps (later DE track).
Bring Postgres up again if you stopped it (~/dcubes/python-lab/pg from the previous lesson).
Practice on your machine
cd ~/dcubes/python-lab/pg
docker compose up -d
cd ~/dcubes/python-lab
source .venv/bin/activate
cat > pandas_pg_lab.py << 'EOF'
import os
from pathlib import Path
import pandas as pd
import psycopg
root = Path(__file__).resolve().parent
df = pd.read_csv(root / "data" / "raw" / "orders-frames.csv")
df = df.dropna(subset=["quantity"]).copy()
df["quantity"] = df["quantity"].astype(int)
df["line_total"] = df["quantity"] * df["unit_price"]
conninfo = dict(
host=os.environ.get("PGHOST", "127.0.0.1"),
port=int(os.environ.get("PGPORT", "15432")),
dbname=os.environ.get("PGDATABASE", "lab"),
user=os.environ.get("PGUSER", "dcubes"),
password=os.environ.get("PGPASSWORD", "dcubes"),
)
with psycopg.connect(**conninfo) as conn:
with conn.cursor() as cur:
cur.execute(
"""
CREATE TABLE IF NOT EXISTS order_lines (
order_id TEXT PRIMARY KEY,
sku TEXT NOT NULL,
quantity INTEGER NOT NULL,
unit_price NUMERIC(10, 2) NOT NULL,
store TEXT,
line_total NUMERIC(12, 2) NOT NULL
)
"""
)
cur.execute("TRUNCATE order_lines")
payload = [
(
r.order_id,
r.sku,
int(r.quantity),
float(r.unit_price),
r.store,
float(r.line_total),
)
for r in df.itertuples(index=False)
]
cur.executemany(
"""
INSERT INTO order_lines
(order_id, sku, quantity, unit_price, store, line_total)
VALUES (%s, %s, %s, %s, %s, %s)
""",
payload,
)
cur.execute(
"""
SELECT store, count(*) AS n, sum(line_total) AS revenue
FROM order_lines
GROUP BY store
ORDER BY store
"""
)
columns = [d.name for d in cur.description]
summary = pd.DataFrame(cur.fetchall(), columns=columns)
conn.commit()
print(summary)
out = root / "data" / "out" / "store_summary.csv"
summary.to_csv(out, index=False)
print("wrote", out)
EOF
python pandas_pg_lab.py
What you should see
- A small DataFrame of store, count, revenue printed.
data/out/store_summary.csvwritten.
Common mistakes
- itertuples on huge frames — fine for labs; bulk copy paths exist later for scale.
- Inserting NaN into NOT NULL columns — clean in pandas first.
- Leaving Compose down — connection errors look like code bugs.