Two Series with one label in common. Look at what comes back, and at how many rows it has.
import pandas as pd
a = pd.Series({'x': 1.0, 'y': 2.0})
b = pd.Series({'y': 30.0, 'z': 40.0})
(a + b).to_dict()
# -> {'x': nan, 'y': 32.0, 'z': nan}
# three rows, not two: the union of the labels.
# 'y' was in both and is summed. 'x' and 'z' were in one, and are NaN.
# The version that treats a missing label as zero:
a.add(b, fill_value=0).to_dict()
# -> {'x': 1.0, 'y': 32.0, 'z': 40.0}
# And what NaN does next, which is why it spreads:
float('nan') + 5 # -> nan
float('nan') == float('nan') # -> False
- Alignment happens before the arithmetic
pandas matches the two Series up by label, takes the union of the labels, and only then adds. So the result can be longer than either input, the order comes from the alignment rather than from you, and position never enters into it. This is the largest difference between a Series and a list, and the reason the same code over two lists would have been right.
- A label on only one side becomes NaN, not the value it had
There is no sensible sum of 12 and nothing, so pandas says NaN — missing, unknown. Only you know that 'missing' here means 'did not play, so zero'. .add(other, fill_value=0) is how you say that: it substitutes 0 for whichever side is absent, and still gives NaN when both sides are absent.
- NaN is quiet and it spreads
It does not raise, it survives arithmetic, and it is not equal to itself — so a NaN that enters in week two is still there three transformations later, having turned a sum, a mean and a ranking into nonsense on the way. The cheapest defence is to assert there are none at the point where you believe there should be none.
Print week1 + week2 and count the rows. There are more than either input has, and the ones that are NaN are exactly the players who appear in only one week.
The + operator lines the two Series up by index label and takes the union of them. There is no sum of 12 and nothing, so pandas says NaN. You have to tell it that 'absent' means zero here.
Series.add(other, fill_value=0) is the long form of +, with a value to substitute for whichever side is missing the label. Do not reach for fillna(0) after the addition — by then the real score has already been swallowed by the NaN, and you would be writing a zero where twelve belonged.
import pandas as pd def combined_scores(week1, week2): totals = week1.____(week2, ____=0) return {name: float(totals[name]) for name in ____(totals.index)}
import pandas as pd
def combined_scores(week1, week2):
# .add is + with a say in what happens to a label only one side has.
# fill_value=0 reads as 'absent from a week means scored nothing that week'.
totals = week1.add(week2, fill_value=0)
# Sorted by name, because the alignment's order is not a promise.
return {name: float(totals[name]) for name in sorted(totals.index)}
fill_value=0 is applied *during* the alignment, so a player missing from week two contributes 0 to a sum that still includes their week-one score. fillna(0) runs *after*, on a value that is already NaN — the 12 is gone by then, and you get 0.0 for a player who scored twelve points. The two read almost the same and differ on every row that matters.
sub, mul, div, floordiv, mod and pow all take fill_value the same way, and all of them take an axis when the other side is a DataFrame. Reaching for the method rather than the operator is the habit worth forming the moment two objects might not share an index — the operator leaves you nowhere to say what a missing label means.
The same behaviour that produced NaN here is what stops you adding one region’s column to another’s when a row was filtered out upstream: the labels no longer line up, so pandas refuses to pretend they do rather than silently pairing row three with row three. Code that drops to .values to 'avoid the hassle' is throwing away the only thing checking that your rows still correspond.
fill_value=0 is right for a sum of scores and wrong for plenty else. A missing temperature reading is not zero degrees; a missing price is not free. When absent genuinely means unknown, leave the NaN and decide later — dropna() to exclude those rows from a mean, or an 'incomplete' count reported alongside the figure. The mistake is not choosing zero, it is choosing it by reflex: ask what the gap means in the data before you fill it, and if the answer is 'it should not be there at all', assert that instead of filling anything.
- Return the number of weeks each player actually appeared in alongside their total, so a mean per week becomes possible.
- Extend it to a list of weeks of any length, and see which approach still reads well at ten.
- Try it with .values instead, and work out how it fails when one week is shorter than the other.