Repository navigation
Description
Noticed while watching coding agents work from the installed package, then audited module by module. Checked on main @ c5f4b2a; each claim below was confirmed by running a small snippet.
What: #585 is one case of a wider pattern. Docstrings explain how something is computed, such as the backend, ibis calls or layout internals, but leave out what the caller needs to use the result. That means: the range or base of a value, its units, the output's columns and index, its shape, which rows it includes, the valid values of string parameters, and the input dtypes it requires. A few docstrings state things the code contradicts.
Ask: rewrite these to state the contract (what goes in, what comes out) and drop the mechanics. Where a docstring contradicts the code, fix whichever one is wrong. Links: L = line on main @ c5f4b2a.
Analysis
GainLoss (gain_loss.py)
- The masks are documented as "boolean masks or lists aligned to
df.index" (L3, L20, L41, L47-52), but Python lists raiseTypeError: unsupported operand type(s) for |: 'list' and 'list'. "Aligned todf.index" also invitesset_index('customer_id'), which with repeated ids always fails "p1_index and p2_index should not overlap". Say: one entry per row ofdf, as a Series ondf's index or a numpy array;dfneeds a unique index. - Output is documented only in private
_calc_gain_loss. Document on the class:gain_loss_dfis indexed by customer ((group_col, customer_id)when grouped), socustomer_idis not a column.- The 15 columns.
- The six components sum to
focus_diff. - Rows that are all zero are dropped.
gain_loss_table_dfis one row when ungrouped.
-
agg_func: say it takes a pandas groupby aggregation name, case-sensitive.
CohortAnalysis (cohort.py)
-
.dfshape is unstated. It is a square matrix: the index is the cohort start (min_period_shopped) and the columns are periods since start (period_since, 0-based). Cells past the end of the data, and cohorts with no customers, are 0, the same as a genuine zero. -
percentage=True"converts cohort values into retention percentages" (L86-88). The values are fractions (1.0 = 100%), rounded to 2 decimals. -
agg_func: list the accepted values. The defaultnuniqueon the docstring's ownaggregation_column="unit_spend"counts distinct spend values, not customers; say to usecustomer_idwithnuniquefor retention. - Say
transaction_datemust be datetime (see the error-message issue).
CrossShop (cross_shop.py)
-
percentincross_shop_table_dfis a share of the summedvalue_col, so spend by default, not customers.plot(L242, L253) calls the result "customer segment overlaps". Withagg_func="mean", the table sums per-customer means. - Document
cross_shop_df:- its index is the
group_colvalue; groupsis a tuple of positional 0/1 flags;- entities in no group are kept as "No Groups" and count in the denominator.
- its index is the
-
group_3_colwithoutgroup_3_valraisesValueError(L155), which__init__'s Raises section doesn't list.
RevenueTree (revenue_tree.py)
-
period_col, "Column identifying the period" (L225-228): say it holds a period label per row, assigned by the caller, and thatp1_value/p2_valueare matched exactly. A raw transaction-date column matches almost nothing. Both labels must be present in the data. -
.dfindex and columns are documented nowhere (the class docstring says only "{metric}_p1"). Document:- A single row (RangeIndex) when ungrouped. A
CategoricalIndexfor onegroup_col, aMultiIndexfor several. - Columns
{metric}_p1/_p2/_diff/_pct_diff/_contribforcustomers,transactions,spend,spend_per_customer, … There is norevenue_*. _pct_diffis a fraction.- A group missing from one period gets 0 for it.
- A single row (RangeIndex) when ungrouped. A
- Private
_agg_dataclaims a["p1","p2"]index (L273-275); the result has none.
CompositeRank (composite_rank.py)
- L22-27 and L47-50 describe
ibis.least,ibis.greatest,ibis.rankandibis.row_number. State the contract instead:- ties share the lowest rank (1, 2, 2, 4);
ignore_ties=Truebreaks ties arbitrarily;- nulls rank last;
- row order is not guaranteed;
composite_rankis a float formean.
CustomerDecisionHierarchy (customer_decision_hierarchy.py)
- Say what distance means: 0 = bought by the same customers, 1 = never. With the default exclusion, near products are substitutes.
- Say that products bought only in multi-item transactions are dropped.
- Say that
distancesis ordered bypairs_df[product_col].cat.categories(stated only on a private method, L148-152).
TransactionChurn (customer.py, L239-253)
- Define the denominator:
- row N counts customers whose N-th purchase day is at least
churn_perioddays before the last day in the data; churned_pct= churned / (churned + retained);transaction_numberis 1-based.
- row N counts customers whose N-th purchase day is at least
ProductAssociation (product_association.py)
-
group_col(L54-55): say it defines the basket. The defaultcustomer_idmakes all of a customer's purchases one basket; passtransaction_idfor per-transaction baskets. Support differed 0.667 vs 0.25 on the same data. -
target_item(L56-57) is typedstr | float. Say int product ids are rejected (TypeError). -
upliftuses undefinedprob_*; define it asprob_i = occurrences_i / total.
haversine_distance (haversine.py)
- Replace "pushed to the backend" and "nothing is materialized" with the contract: a column expression in
radiusunits (km by default).
Segmentation
RFMSegmentation (rfm.py): the NTILE and backend wording is #585.
-
.df(L303) "Returns the dataframe with the segment names" is wrong; it has no segment names. List the columns:recency_days,frequency,monetary,r_score/f_score/m_score,rfm_segment,fm_segment. Say it is indexed bycustomer_id.
ThresholdSegmentation (threshold.py)
-
thresholds(L43-48) says only "unique and match the number of segments". Say they are ascending percentile ranks in (0, 1] and the last must be 1.0. Today[0.5, 0.8]silently leaves the top 20% with segmentNone, and an unsorted list puts everyone in one segment; whether to validate instead is in the error-message issue. - Replace the "percent_rank" and "window" wording (L33-38) with the contract: a customer takes the first segment whose threshold their percentile rank is ≤; ties are broken by
customer_id. -
agg_func(L109): list the accepted values.
HMLSegmentation (hml.py, L156-173)
- Say that the 50/80 cut points are percentile ranks of customers, not spend values.
- Define each
zero_value_customersmode. - Say
.dfis indexed bycustomer_id(andgroup_col).
SegTransactionStats (segstats.py)
-
.df: document- RangeIndex, with segment columns as columns;
- row order unspecified unless
sort_by; - one row per segment combination present per grouping set;
- columns left out of a grouping set hold
rollup_value; - ratios are null when the denominator is 0;
- customer metrics are absent without a customer column.
-
grouping_sets: say each tuple is one aggregation level, and that the columns it omits are labelledrollup_value. -
unknown_customer_value(L238-242): "metrics are split into identified,_unknown, and_totalvariants" is inaccurate.customersand the per-customer ratios have no variants, and plainspend/transactionsthen cover identified customers only. -
extra_aggs(L225-227): say which aggregation names are valid. - Replace the mechanics:
calc_rollup"Adds O(n) extra aggregation passes" (L229-231);sort_by"using ibis .order_by() so the sort runs at the backend" (L253-254).
Utils, options, experimental
-
filter_and_label_by_periods(date.py, L103-111): a date-onlyendmeans 00:00 that day, so later times on that day are dropped from a timestamp column. Document it, or makeendinclusive of the whole day. -
label_by_condition(label.py, L277): say the result has onlylabel_colandreturn_col. -
CLVStats(clv.py):- Replace the Oracle day-granularity explanation (L19-21) with:
recencyandTare fractionalperiodunits from calendar-day differences. - Replace the query-count text (L138-142) with the eager/lazy contract.
pymc_time_unit(L476-478) contradicts itself.
- Replace the Oracle day-granularity explanation (L19-21) with:
-
AcvandPctOfStores(experimental/metrics):Acvhardcodes "$MM" whatever the currency.- Say
stores_pctis 0-100, not a fraction. - Say the group columns are columns (RangeIndex) and row order is unspecified.
Plots
-
area.plot(area.py, L33-40): "if None, the index is used", but withgroup_colset,x_col=NoneraisesKeyError: None.lineandscatterhandle the same case. -
cohort.plot(plots/cohort.py, L150-165): describes label placement and colour-bar format. Say instead that withpercentage=Truethe cells must be fractions; 25 renders as "2500%". -
line.plot(line.py, L206-210): saysmove_legend_outsideandlegend_titleare ignored whenever a legend shows. That is true only for"end_of_line";time.pyL62-64 has it right. -
waterfall.plot(waterfall.py):remove_zero_amounts"Drop zero amounts before the net is computed" (L376-377) can't change the net. It hides the zero bars and their labels."percentage"labels are shares of the net total, so they can exceed 100% or be negative (L388-392).
-
broken_timeline.plot(broken_timeline.py, L508-510): "gets a shorter bar, not a gap" is wrong. Bars are whole periods, and the threshold removes rows, so an empty period becomes a gap. -
time.plot(time.py, L37-42): say the date column must be datetime. It also addstransaction_periodto the caller'sdfin place (L79); better to copy first. -
index.plot(index.py, L194): list theagg_funcvalues, which today appear only onget_indexes(L417). -
bar.plot(bar.py):percentage_by_bar_groupneeds unique x values (L75-90).sort_ordersorts by the firstvalue_colonly (L80-82).
-
venn.plot(venn.py, L592-596): say that flag i ingroupscorresponds tolabels[i], thatgroupsmust be a tuple, and thatpercentis shown unscaled. -
tree_diagram(tree_diagram.py, L174, L526): saypercentis in percentage points, not a fraction. -
apply_chart_chromeandapply_legend(styling_helpers.py): replace the "resize-invariant recipe",_ChromeLayoutEngineand "reflow" wording with what the caller gets. - Every plot's
x_label/y_label: say that omitting them gives no label, not the column name.
Related: #585 (RFM), #586 (revenue tree contribution formulas), #590 (exports, dir(), help()).
Activity
fix(date): include the whole end day for date-only period bounds (#615)