Almost everything you do to a data frame in the tidyverse has a one-line pandas equivalent, with the verbs renamed, the pipe replaced by a full stop, and columns addressed with square brackets rather than bare names.
The translation table
Task
R / tidyverse
Python / pandas
Data frame
data.frame(), tibble()
pd.DataFrame()
Chain operations
df %>% f() %>% g()
df.f().g()
Filter rows
filter(df, prev > 0.4)
df[df["prev"] > 0.4]
Add a column
mutate(df, x = a * b)
df.assign(x=lambda d: d["a"] * d["b"])
Grouped summary
df %>% group_by(g) %>% summarise(m = mean(x))
df.groupby("g").agg(m=("x", "mean"))
Sort
arrange(df, desc(x))
df.sort_values("x", ascending=False)
Select columns
select(df, a, b)
df[["a", "b"]]
First rows
head(df)
df.head()
Structure
str(df)
df.info()
Vector
c(1, 2, 3)
[1, 2, 3]
Named list
list(a = 1, b = 2)
{"a": 1, "b": 2}
Empty
NULL
None
Missing number
NA_real_
np.nan
Index a loop
seq_along(x)
enumerate(x)
Map over a vector
sapply(x, f)
[f(i) for i in x]
df["prev"] > 0.4 is a vector of True and False, exactly as in R, and indexing the frame with it keeps the True rows.
A Python list is not an R vector, being untyped and performing no arithmetic, so [1, 2, 3] * 2 repeats the list instead of doubling the numbers. Vector arithmetic needs a NumPy array or a pandas column.
str() becomes .info(), which reports the column names, how many values are present in each, and the type pandas has inferred for each column. The output is printed rather than returned.
assign takes a lambda, an unnamed one-line function, because the new column is computed from the frame as it stands at that point in the chain, which does not yet have a name. Nothing is mutated in place.
group_by plus summarise becomes groupby().agg(), where each keyword names an output column and takes a (column, function) pair. reset_index() turns the grouping key back into a column. Without it, resistance stays as the frame’s index, which is closer to R’s row names than to a column.
sapply becomes a list comprehension, with the expression first and then what it ranges over. The order is preserved. dn0 is the probability that a mosquito dies on contact with a treated net.
from estimint import calculate_dn0levels = [0.0, 0.25, 0.50, 0.75]dn0 = [calculate_dn0(r, py_pbo=0.7).dn0 for r in levels]pd.DataFrame({"resistance": levels, "dn0": dn0})
resistance
dn0
0
0.00
0.50935
1
0.25
0.48930
2
0.50
0.45665
3
0.75
0.39165
enumerate does the job of seq_along, returning the position as well as the value. Python counts from zero.
for i, r inenumerate(levels):print(i, r)
0 0.0
1 0.25
2 0.5
3 0.75
Dataclasses
Scenario and EirTarget are dataclasses, and the nearest thing R has is a list() whose named fields are fixed in advance. A misspelled keyword is rejected with a TypeError at construction rather than returning NULL. Seven of the seventeen fields are required (name, res_use, Q0, phi, seasonal, irs and eir_target). The other ten default to zero or None.
Fields are read back with a full stop, where R would use $, and they nest, so eir_target is itself a dataclass whose own fields are reached the same way. pbo.lsm is already 0.0 although it was never given a value, because defaults are filled in at construction.
An R controller took parallel vectors of names, resistance levels and net types, with position i of each belonging to scenario i. Drop an element from one vector and every scenario after it shifts too.
Python makes each scenario one Scenario object, and run_scenarios takes a list of them.
setting is a dict of everything the three scenarios share, and **setting unpacks it into each constructor, so the shared arguments are written once and cannot drift apart.
pd.DataFrame([ {"name": s.name, "net": s.net_type_future, "itn_future": s.itn_future}for s in candidates])
name
net
itn_future
0
Like-for-like
pyrethroid_only
0.7
1
PBO switch
pyrethroid_pbo
0.7
2
Pyrrole switch
pyrethroid_pyrrole
0.7
Passing candidates to run_scenarios returns one row of results per scenario, in order.