DataFrame has row labels, called its index, and column labels; different columns can hold different data types. This makes it useful for a position table containing instrument identifiers, share quantities and prices. Its labels help keep the right price attached to the right holding. The pandas reference defines the structure and its labelled arithmetic.Two holdings, reversed quotes
Suppose a fictional portfolio holds two shares of A and three shares of B. At one common valuation time, A costs 100 USD per share and B costs 50 USD per share. The quote input happens to list B before A.
Series is a one-dimensional labelled object. Constructing a DataFrame from labelled Series performs index alignment: values match by label, rather than by their incoming position. Here the explicit index fixes the output row order as A, B. The checks reject duplicated identifiers, missing identifiers and missing values before valuation.import pandas as pd
quantity = pd.Series([2, 3], index=["A", "B"], dtype="int64")
quotes = pd.Series([50, 100], index=["B", "A"], dtype="int64")
assert quantity.index.is_unique and quotes.index.is_unique
assert set(quantity.index) == set(quotes.index)
holdings = pd.DataFrame(
{"quantity": quantity, "price_usd": quotes}, index=["A", "B"]
)
assert not holdings[["quantity", "price_usd"]].isna().any().any()
holdings["value_usd"] = holdings["quantity"] * holdings["price_usd"]
print(holdings["value_usd"].to_dict())
# {'A': 200, 'B': 150}
print(int(holdings["value_usd"].sum()))
# 350
print(int(holdings.loc["A", "price_usd"]), int(holdings.iloc[0, 0]))
# 100 22 * 50 + 3 * 100 = 400: the arithmetic runs, but both asset-price matches are wrong. The data-structures guide explains construction from labelled Series and column calculations.A label and a position answer different questions
holdings.loc["A", "price_usd"] asks for A's price; holdings.iloc[0, 0] asks for the first row's first field, its quantity. After sorting, “first row” may mean a different instrument. Use the selection rule that matches the question. Selecting holdings["quantity"] returns a Series; selecting holdings[["quantity"]] keeps a one-column DataFrame. These distinctions are documented in the indexing guide.Labels still need an economic contract. Two rows labelled A are not interchangeable if one belongs to a different date, venue or currency. For a multi-date table, define the row identity using all the necessary fields. A numeric dtype does not enforce “USD per share”, and a table does not automatically reject duplicated instrument labels.
A partial sum can look complete
If B's quote is absent, assigning the remaining labelled price leaves B missing. It does not create a zero-priced holding. This separate example exposes the danger:
import pandas as pd
holdings = pd.DataFrame({"quantity": [2, 3]}, index=["A", "B"])
holdings["price_usd"] = pd.Series([100], index=["A"], dtype="int64")
holdings["value_usd"] = holdings["quantity"] * holdings["price_usd"]
print(holdings["value_usd"].isna().to_dict())
# {'A': False, 'B': True}
print(float(holdings["value_usd"].sum()))
# 200.0sum(skipna=False) would also propagate the missing value. Missing-value representation depends on dtype, so use isna() rather than testing equality with a missing-value marker. See the missing-data guide..loc[row, column] = value operation. pandas 3.0 made Copy-on-Write the default; code that relies on changing a selected object to mutate its parent is unsafe to carry across versions. The migration guide explains this change.