| country | continent | year | life | population | gdp |
|---|---|---|---|---|---|
| Afghanistan | Asia | 2007 | 43.828 | 31889923 | 974.5803 |
| Albania | Europe | 2007 | 76.423 | 3600523 | 5937.0295 |
| Algeria | Africa | 2007 | 72.301 | 33333216 | 6223.3675 |
| Angola | Africa | 2007 | 42.731 | 12420476 | 4797.2313 |
| Argentina | Americas | 2007 | 75.320 | 40301927 | 12779.3796 |
47 Python
The grammar does not change when the language does. One engine reads one specification, so a sentence means the same thing no matter who wrote it. Everything in this book up to here is as true in Python as it is in R, and what differs is short enough to state in one chapter.
Every Python block with a plot under it was executed, through the same engine every other chapter uses.
47.1 A column is written col.gdp
R writes a column as a bare name, Julia as a symbol, Python through a small accessor:
data(gapminder_2007) + point + x(col.gdp, scale='log') + y(col.life) + color(col.continent)“Given gapminder 2007: points, x is gdp on a log scale, y is life, color by continent.”
The R form of that sentence is the one from A first plot, with col. in front of each column. Those four characters are not decoration, and the next paragraph says what they do. In R a bare gdp cannot be anything except a name; Python has no bare names, and in this grammar a plain string is how you spell a value: style(color="tomato"), title("…"), palette("okabe"). Without the accessor a column and a value would look identical, and the rule that a channel takes a column, never a value, would be invisible in Python.
So a string where a column belongs is refused, and told what to write:
data(gapminder_2007) + point + x('gdp') + y(col.life)gog: `x("gdp")` binds a *value*, and a channel takes a *column*. Python has no bare names, so a column is written with the accessor: `x(col.gdp)` maps the column called `gdp`.
The message names the column spelling, x(col.gdp). On a channel you could also set, it names both, because one maps a column and earns a legend while the other sets one value and earns none (Setting vs mapping). The mistake in the other direction is refused the same way:
data(gapminder_2007) + point + x(col.gdp) + y(col.life) + style(color=col.continent)gog: `style(color=)` fixes one value for the whole layer, and `col.continent` is a column. To *map* it — one value per category, with a legend to decode it — that is a channel: `color(col.continent)`.
A name Python cannot spell after a dot takes the bracket form instead: col["life exp"] for a space, col["gdp.2007"] for a dot. A non-Latin column name needs nothing special either: col.국가 works, and the bracket form covers the rest.
47.2 A sentence over several lines takes parentheses
R continues a line that ends in +. Python ends the statement there, so a sentence spanning several lines is wrapped in parentheses:
(data(gapminder_2007) + point + x(col.gdp, scale='log') + y(col.life)
+ color(col.continent) + size(col.population)
+ title('Wealth and life expectancy, 2007'))“Given gapminder 2007: points, x is gdp on a log scale, y is life, color by continent, size by population.”
That is Python’s rule about lines rather than anything of gog’s. With the accessor, it covers almost everything you will type differently.
47.3 The table
A table is a dict of columns, so a first plot needs nothing installed:
data({'level': ['Low', 'Medium', 'High'], 'count': [30.0, 20.0, 10.0]},
name='small') + bar + x(col.level) + y(col.count)“Given the small table: bars, x is level, y is count.”
Anything carrying .columns and df[name] is also a table, which covers pandas and polars. Neither is imported, so neither is a dependency of the binding, and you read a file the way you already do. GAPMINDER_CSV here is the path to the book’s gapminder_2007.csv, which The book’s data says where to get:
import pandas as pd
gapminder_2007 = pd.read_csv(GAPMINDER_CSV)
print(gapminder_2007.shape, list(gapminder_2007.columns))(142, 6) ['country', 'continent', 'year', 'life', 'population', 'gdp']
Every plot on this page reads that table. Types convert the way you would expect. Numbers become positions, text becomes categories, and datetime64, date and datetime all become a calendar axis. NaN and None are missing values: the engine leaves out the whole row and says how many it left out. A pandas Categorical carries its declared order across, which is the counterpart of R’s factor(x, levels = …). A plain dict of lists declares the same order with ordered(values, levels), which Data describes.
47.4 Computing a column first
A channel takes a column name and never an expression. This is true in all four languages, and pandas is where a Python reader meets it. There is no x(log(col.gdp)), and gog has no small language for deriving values, because Python is already one: compute the column, then bind it by name.
europe = gapminder_2007[gapminder_2007.continent == 'Europe'].copy()
europe['gdp_thousands'] = europe.gdp / 1000
europe = europe.sort_values('life', ascending=False)
print(europe[['country', 'gdp_thousands', 'life']].head(3).to_string(index=False))country gdp_thousands life
Iceland 36.180789 81.757
Switzerland 37.506419 81.701
Spain 28.821064 80.941
That result is a table like any other, and the sentence over it is the sentence you already know:
(data(europe) + point + x(col.gdp_thousands) + y(col.life)
+ size(col.population) + style(color='steelblue')
+ x_label('GDP per capita (thousands)')
+ title('Europe, 2007'))The division stayed in pandas, where you can see it, and the plot names a column that exists. That split is deliberate rather than a missing feature. A plot is a mapping from a table (Data), so a channel names one of its columns, and anything you would rather compute is computed first.
47.5 Showing the plot
In a Jupyter notebook a plot displays itself, as it does in the R console. A script displays nothing on its own, so say where the plot goes:
plot = data(gapminder_2007) + point + x(col.gdp) + y(col.life)
print(plot)
print(plot.save('/tmp/life.svg'))<gog plot: point on gapminder_2007>
/tmp/life.svg
Printing a plot gives one line naming its mark and its table, not the picture. save() writes the SVG, show() opens it in a browser, and render() returns it as a string. R’s print() opens the viewer without being asked; Python waits to be told, which is the convention each language already has.
47.6 Six names Python already uses
bin, map, max, min, range and sum are gog’s words here and builtins there, so from gog import * puts gog’s over Python’s for the rest of the file. The R package does the same to base R, with max, min, range and sum among them: a grammar keeps its own vocabulary. Calling five of the six says so, and names the fix. Only bin is silent: bin(5) returns a transform where Python would have returned a string.
try:
range(10)
except GogError as error:
print(error)gog: `range()` takes the band's two ends, each one number between 0 and 1, e.g. `range(0.25, 0.75)`. For a sequence of integers, gog shadows that name: use `builtins.range` for Python's.
The worst of the six is map, because it is the only one whose error does not mention gog. Python reports the wrong number of arguments and names nothing you can act on.
A module that needs both can import gog and write gog.point, or take the builtin back with from builtins import sum.
47.7 What does not change
The four operators, including how tightly they bind. * groups before +, + before |, and / sits beside *. Those are Python’s own precedences and R’s alike, so a derived layer and a crossed facet group exactly as they do in Operators:
(data(gapminder_2007) + bar * mean + x(col.continent) + y(col.life)
+ order(col.life, desc=True)
+ title('Mean life expectancy, highest first'))“Given gapminder 2007: bars derived by mean, x is continent, y is life, ordered by life, largest first.”
The vocabulary does not change either: every mark, transform, channel, setting and plot-level atom in the kernel card exists in Python under the same name. Neither does an engine refusal, which reaches you word for word. A refusal the binding raises is the one that differs, because it teaches Python’s own spelling:
data(gapminder_2007) + bar + x(col.continent)gog: `bar` needs `y()` but none is set. Add `y(<column>)` — a bar cannot be drawn without it.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
The distance between the two spellings is measured rather than argued. Every sentence in this book was translated by rule and run through the Python binding: 632 drew a byte-identical plot, 166 earned a word-identical refusal, and 19 differed only in the example inside the message, where each binding teaches its own syntax. Eleven sentences do not carry over at all. Nine are in R, about R-only spellings, and two are the GIF exports in Play. Two rewrites account for nearly all the rest: the accessor, 812 times, and the parentheses, 554.
47.8 Reading from a database
Python has a database standard, and query() speaks it. It also speaks Spark, because that is where a large share of Python’s data is stored.
The standard is DB-API, also called PEP 249. Anything with a .cursor() method works: the built-in sqlite3, DuckDB, psycopg, pyodbc, Snowflake’s connector, and the Databricks SQL connector.
import sqlite3
from gog import query, bar, x, y, col
con = sqlite3.connect("sales.db")
plot = query(con, "SELECT status, revenue FROM orders") + bar + x(col.status) + y(col.revenue)
plot.save("orders.svg")A Spark session works too. Hand query() the session itself, and the table name is whatever your catalog calls it. This is the route to a Databricks table:
plot = query(spark, "SELECT status, revenue FROM main.sales.orders") + bar + x(col.status) + y(col.revenue)In a Databricks notebook the session is already there, under the name spark. gog installs from PyPI with its engine included, so %pip install gog is the only setup step.
Neither route adds a dependency to gog. You bring the connection; gog reads rows from it.
47.9 Getting it
A plot is drawn by the engine, which is a compiled Rust binary, so the Python package has to be able to find one. The released wheel carries it: one wheel per platform, each holding the engine built for that platform, so installing needs no Rust toolchain.
pip install gogThat command works today. gog is on PyPI. Five wheels cover macOS and Linux on both common processor families, and Windows on Intel. The other three bindings install from their own registries, and the preface lists all four commands.
To work from a checkout instead, build the engine once and import the package from where it sits:
cargo build --release -p gog-cliimport sys
sys.path.insert(0, "py-pkg/gog")
from gog import *The engine is looked for in four places, in this order: the GOG_CLI_PATH environment variable, the copy inside the installed package, gog-cli on your PATH, and a local target/release build. The first three are how an installed copy works. This page was rendered through the first: the book points GOG_CLI_PATH at a local build before any Python runs.