| 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 |
50 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. What differs is short enough to state in one chapter. The sections below cover how a column is named, how a sentence is written over several lines, how values are written, and what counts as a table. Then come computed columns, showing a plot, and the six names Python already uses. After them comes the operators section. Python groups the operators in the same order as R, and an arrangement, two plots joined by | or /, needs two kinds of parentheses. The vocabulary and the refusals follow, and they do not change. Reading from a database and installing the package come last.
Every Python block on this page was run when the page was built, through the same engine every other chapter uses. Each block runs after from gog import *, and after gapminder_2007 is read as the section The table shows. The blocks leave those lines out. The database and install commands at the end are shown, not run.
50.1 A column is written col.gdp
In Python, gog marks each column with four characters, col., so that a column is never mistaken for a value. R writes a column as a bare name and Julia as a symbol. Python writes it through col, a small object called an accessor: col.gdp names the column gdp:
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.”
This is the sentence from A first plot, without its title and with col. in front of each column. The four characters are needed because R and Python pass an argument differently. In R, the call x(gdp) receives the word gdp itself, not a value. Python always passes values, so a bare gdp would have to be a variable you defined earlier. 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 both be plain strings, and gog could not tell which one you meant.
So a string where a column belongs is refused, and the message says what to write:
data(gapminder_2007) + point + x('gdp') + y(col.life)gog: `x("gdp")` binds a *value*, and a channel takes a *column*. In Python, a bare name is a variable, 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). Where one name is both a channel and a setting, such as color, the message gives both spellings, because the two do different things. A channel maps a column and the plot gets a legend for it; a setting sets one value and gets none (Setting vs mapping). The opposite mistake, a column where a value belongs, 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)`.
50.1.1 Names that need brackets
After col., Python accepts only an identifier: a name made of letters, digits and underscores that does not start with a digit. Any other name is written in square brackets instead: col["life exp"], col["gdp.2007"], col["2007"]. Inside the brackets the name is a string, but col still marks it as a column. Be careful with a hyphen: Python reads col.life-exp as col.life minus a variable named exp, so write col["life-exp"].
Python also keeps some words for its own syntax, such as class, from and global. These reserved words need brackets too: col["class"]. If you write col.class, Python stops with SyntaxError: invalid syntax before any gog code runs. Unlike gog’s messages above, this message comes from Python, so it cannot tell you to write col["class"]. help("keywords") prints the full list.
A name in another writing system needs no brackets: col.국가 (country) works with the dot. Python accepts letters from any writing system in a name. The bracket form works for any name.
50.2 A sentence over several lines
The second thing you type differently is about lines rather than names. 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, not a rule of gog’s. Those parentheses and the accessor are nearly the only things you will type differently. The rest is as in R, except that values take Python’s spelling.
50.3 How values are written
In the other chapters, a code block that is a sentence has a tab for each language. Code written inside the text has none, so it is written in R, as in x(life, limits = c(37.5, 87.5)). Most of it reads the same in Python once col. is written before each column. The values are what change. R writes several values inside c(), and Python writes them in a list, inside square brackets. The table gives the Python spelling of each kind of value the book uses:
| Kind of value | R | Python |
|---|---|---|
| true or false | TRUE, FALSE |
True, False |
| several numbers | c(37.5, 87.5) |
[37.5, 87.5] |
| several strings, such as colors | c("tomato", "steelblue") |
["tomato", "steelblue"] |
| a color for each category | c(Asia = "tomato", Europe = "steelblue") |
{"Asia": "tomato", "Europe": "steelblue"} |
| a missing value | NA |
None |
| the number e | exp(1) |
math.e, after import math |
| a declared order | factor(values, levels = c("Low", "High")) |
ordered(values, ["Low", "High"]) |
So x(life, limits = c(37.5, 87.5)) in the text is x(col.life, limits=[37.5, 87.5]) in Python. A tuple works too: limits=(37.5, 87.5). A named argument is written as in R, because Python has named arguments of its own: legend = FALSE is legend=False. ordered() declares the order of a column’s categories, as R’s factor() does. The next section says more about it, and about a pandas column that already carries an order.
50.4 The table
The simplest table is a dict of columns, so a first plot needs no package besides gog:
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.”
Any object with a .columns attribute that you can index by column name is also a table, which covers pandas and polars. The binding imports neither one, so neither is a dependency. You read a file the way you already do. GAPMINDER_CSV here is the path to the book’s gapminder_2007.csv, and The book’s data says where to get that file:
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']
Apart from the small bar plot above, every plot on this page draws from this table or from a table computed from it.
The column kinds follow the Python types. A column of numbers is read as continuous, and a column of text as categorical. A column of datetime64, date or datetime values is drawn on a calendar axis. NaN and None are missing values. A row with one in a column the plot maps is left out, and the engine says how many rows it left out. A pandas Categorical keeps its declared order when the engine reads it. It is the pandas counterpart of R’s factor(x, levels = …). A plain dict of lists declares the same order with ordered(values, levels), which the Data chapter describes. A column with a declared order is categorical even when its values are numbers, such as years.
A pyarrow Table is a table too. A dictionary-encoded column, pyarrow’s form of a category, keeps the order of its categories, as a pandas Categorical does. A polars LazyFrame is a query that has not been run yet, so it holds no rows. gog refuses it and names the call that runs it, frame.collect().
Two kinds of value are refused rather than drawn. The first is an infinite number. numpy.log10(0) is -inf, and an infinite value is not a missing one, so gog does not drop its row. The first value of log_cells is -inf:
import numpy as np
growth = {'day': [1, 2, 3, 4], 'log_cells': np.log10([0, 12, 150, 1900])}
print(growth['log_cells'])[ -inf 1.07918125 2.17609126 3.2787536 ]
gog refuses the plot and names the column:
data(growth) + point + x(col.day) + y(col.log_cells)gog: column `log_cells` of `growth` holds -inf, which is not a number a scale can place. Use `None` for a missing value.
If a row with an infinite value should be left out, write None in its place, and gog drops that row and says so.
The second is a length of time. A timedelta, or a pandas, polars or numpy column of them, can be shown in minutes, hours or days. gog does not choose the unit for you. travel holds how many minutes the trip to work took on five days:
travel = pd.DataFrame({
'day': pd.date_range('2024-05-06', periods=5),
'time': pd.to_timedelta([42, 38, 55, 40, 47], unit='min'),
})gog refuses the plot and names the column, and the message gives the code that makes it a number:
data(travel) + line + point + x(col.day) + y(col.time)gog: column `time` of `travel` holds lengths of time, and gog has no scale for a duration. Make it a number in the unit you mean, such as hours: `travel['time'].dt.total_seconds() / 3600` for a pandas column, or `td.total_seconds() / 3600` for each `timedelta`.
Choose the unit you want to read on the axis. Here it is minutes:
travel['minutes'] = travel['time'].dt.total_seconds() / 60
print(travel['minutes'].tolist())[42.0, 38.0, 55.0, 40.0, 47.0]
data(travel) + line + point + x(col.day) + y(col.minutes)“Given the travel table: a line and also points, x is day, y is minutes.”
gog checks for both kinds of value only in the columns the plot maps. travel still holds time, and this plot draws, because it never reads that column.
50.5 Computing a column first
A channel takes a column name and never an expression. That rule is the same in all four languages. There is no x(log(col.gdp)), and gog has no language of its own for deriving values, because Python already has one. So compute the column first, then bind it by name.
Dividing a column inside the channel is a natural thing to try. gog refuses it, and says where the computation belongs:
data(gapminder_2007) + point + x(col.gdp / 1000) + y(col.life)gog: a channel takes a column name, not an expression — gog has no computed channels. Compute the column in Python first and bind the result: `df['ratio'] = df['a'] / df['b']`, then `col.ratio` where the expression was.
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 that draws from it is one you already know:
(data(europe) + point + x(col.gdp_thousands) + y(col.life)
+ size(col.population) + style(color='steelblue')
+ x_label('GDP per person (thousands)')
+ title('Europe, 2007'))You did the division in pandas, in a line of your own code that you can check. The plot then names a column that exists. Keeping the computing and the plotting apart is deliberate rather than a missing feature. A plot is a mapping from a table (Data), so a channel names one of its columns. Anything you want to compute is computed before you write the sentence.
50.6 Showing the plot
The plots above appear on this page because the book drew each one when it was built. In your own session, where a plot appears depends on how you are running Python. In a Jupyter notebook, a plot on the last line of a cell is drawn below that cell, as the R console draws a plot you type. A script displays nothing on its own, so you ask for the picture yourself:
plot = data(gapminder_2007) + point + x(col.gdp) + y(col.life)
print(plot)
print(save_svg(plot, '/tmp/life.svg'))<gog plot: point on gapminder_2007>
/tmp/life.svg
Printing a plot gives one line, naming its mark and its table. It does not give the picture. save_svg() writes the SVG to a file and returns its path, which is the second line printed above. plot.show() opens the SVG in a browser, and render_svg(plot) returns it as a string. R’s print() draws the picture, and Python’s does not. Each language is following the convention it already has.
50.7 Six names Python already uses
Six of gog’s names are also Python builtins, the functions Python provides without any import: bin, map, max, min, range and sum. After from gog import *, those six names mean gog’s atoms, not Python’s builtins, for the rest of the file or notebook. gog’s R package shares names with R as well, and max, min, range and sum are four of them. gog does not rename an atom to avoid a name the language already uses. If you call sum, min, max or map as you would call Python’s builtin, gog refuses the call. So does range with whole numbers outside 0 to 1. The message says the name is gog’s, and how to reach Python’s builtin:
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.
That is range’s message. Its word for this is shadows: after the import, the name reaches gog’s atom instead of Python’s builtin. sum, min, max and map share a different message, which lists all six names and says how to reach Python’s builtin.
One name, bin, gives no message at all. bin(5) is also how you ask gog for five bins, so it returns a transform instead of Python’s string '0b101'.
If a file needs Python’s builtins as well as gog, write import gog and then gog.point, or restore one builtin with from builtins import sum.
50.8 The operators, and their precedence
The four operators do not change from R to Python, and neither does their precedence, the order in which they group. * and / group first, then +, then |. That order is Python’s own, and R’s is the same. So bar * mean below becomes one compound mark before any + joins it, exactly as 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 same order decides where an arrangement needs parentheses. In an arrangement, Python has two kinds of parentheses, and they do different jobs. The outer pair, around the whole sentence, lets it continue on the next line (A sentence over several lines). It groups nothing inside the sentence. So an arrangement with only the outer pair is still refused when / joins its plots. / groups before +, so it joins the first plot’s last atom to the second plot’s data():
(data(gapminder_2007) + point + x(col.gdp) + y(col.life) /
data(gapminder_2007) + bar * count + x(col.continent))gog: `/` places one plot below another, and it groups before `+`, so here it joined `y()` to the plot after it. Put each plot in parentheses: `(data(df) + point + ...) / (data(df) + bar + ...)`.
The inner pairs, one around each plot, are the ones that group. Each plot is then complete before / joins it. This book writes both kinds whenever it composes plots:
((data(gapminder_2007) + point + x(col.gdp) + y(col.life)) /
(data(gapminder_2007) + bar * count + x(col.continent)))“Given gapminder 2007: points, x is gdp, y is life, above bars derived by count, x is continent.”
With | between the plots, gog draws the arrangement without the inner pairs, because | groups after +. Write them anyway. The arrangement then means the same thing with either operator, and the same as in Julia, which groups | together with +. An arrangement inside an arrangement needs a pair of its own too, and so does a whole arrangement before a theme() for it. Inside an arrangement, / groups before |, so a | b / c is read as a | (b / c). Without the pair around a whole arrangement, + groups before |, and the theme() goes to the second plot (Parentheses in an arrangement).
50.9 The same vocabulary and the same refusals
Like the operators, the vocabulary does not change: every mark, transform, channel, setting and plot-level atom in the kernel card keeps its name in Python. An engine refusal does not change either. The engine does not know which binding asked, so it sends you the same words it sends an R user:
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. To count the rows in each slot instead, write `bar * count`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
The refusals that differ are the binding’s own, like the two about the accessor. Each of those has to teach the spelling of the language you are writing in.
This chapter’s examples are only a small sample. On every change to the project, its automated tests translate the book’s sentences into Python and draw them again. How far the two spellings differ was measured that way, sentence by sentence. Of the book’s 926 sentences, eleven have no Python spelling at all. Nine are in the R chapter and use R’s own syntax, such as the pipe. Two are the GIF exports in the Play chapter. Every one of the rest was translated by rule and run through the Python binding. Of those, 708 drew the same plot, byte for byte, and 187 gave the same refusal, word for word. The other 20 were refused in both languages, in a message worded for each one. Two rewrites do nearly all of the translating. The accessor is needed in 909 sentences, and the parentheses in 635.
50.10 Reading from a database
Your table may be stored in a database rather than held in your session. Python’s database standard is DB-API, also called PEP 249, and query() reads any connection that follows it. It also reads a Spark session, because many Python users reach their data through Spark.
Any connection with a .cursor() method works: sqlite3 from Python’s standard library, DuckDB, psycopg, pyodbc, Snowflake’s connector, and the Databricks SQL connector.
import sqlite3
from gog import query, bar, x, y, col, save_svg
con = sqlite3.connect("sales.db")
plot = query(con, "SELECT status, revenue FROM orders") + bar + x(col.status) + y(col.revenue)
save_svg(plot, "orders.svg")Pass the Spark session itself to query(). In the SQL, write the table’s full name as Spark lists it, such as main.sales.orders. This is how you read 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, and %pip install gog is the only setup step.
Neither way adds a dependency to gog. You open the connection, and gog only reads rows from it.
50.11 Installing gog
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 package carries it. A Python package is released as wheels, ready-built files for one platform each, and each wheel holds the engine built for its platform. So installing needs no Rust toolchain.
pip install gogThat command works. gog is on PyPI as five wheels. They cover macOS on Intel and ARM processors, Linux on Intel, AMD and ARM processors, and Windows on Intel and AMD processors. On any other platform, pip install gog stops with an error, because there is no wheel for it. The other three bindings install from their own registries, and the preface lists all four commands.
To work from a checkout instead, a copy of gog’s source repository, build the engine once. Then start Python in the repository’s top folder and import the package from there:
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. Only a checkout has the fourth, which also accepts a target/debug build. This page used the first: the book sets GOG_CLI_PATH to a local build before any Python runs. If GOG_CLI_PATH names no file, gog refuses to draw and says which path it was given. It does not move on to the next place, because a mistyped path would then draw with an engine you did not choose.
50.12 What comes next
Python’s differences are spelling rather than grammar. Julia has four differences of its own. A column is a symbol, | has a different precedence, nine names need one extra import line, and the package has a different name.