// Label-conditional degree statistics: the average number of relationships // of one type, in one direction, per node carrying one label. The global // AvgDegree averages a type's count over the type's OWN sources, which says // nothing about how a particular label's nodes fan out over it -- a type // whose rels overwhelmingly leave one label reads as a huge average for // every label that merely touches it. A chain-cost estimator multiplying // hop fan-outs needs the conditional form, or a low-cardinality anchor // hides an exploding chain behind it.
package chickpeas // labelDegreeEntry is one label's per-type incident-rel counts by // direction, summed over every node carrying the label. Zero-degree // members stay in the denominator: the statistic answers "expected fan-out // of a node with this label", not "average over nodes having such a rel". type labelDegreeEntry struct { out, in map[RelType]uint64 } // labelDegreeStats returns (building once) the label's per-type degree // counts. Same lazy-cache choreography as the other snapshot indexes: // check under lock, build outside it, re-acquire and keep the first // insert. func ( *Snapshot) ( Label) *labelDegreeEntry { .labelDegreeMu.Lock() if , := .labelDegrees[]; { .labelDegreeMu.Unlock() return } .labelDegreeMu.Unlock() := &labelDegreeEntry{out: map[RelType]uint64{}, in: map[RelType]uint64{}} if , := .labelIndex[]; { for := range .Iter() { := int() if +1 < len(.outOffsets) { for := .outOffsets[]; < .outOffsets[+1]; ++ { .out[.outTypes[]]++ } } if +1 < len(.inOffsets) { for := .inOffsets[]; < .inOffsets[+1]; ++ { .in[.inTypes[]]++ } } } } .labelDegreeMu.Lock() defer .labelDegreeMu.Unlock() if , := .labelDegrees[]; { return } .labelDegrees[] = return } // AvgDegreeByLabel is AvgDegree conditioned on the node's label: the // average number of relType relationships in dir per node carrying label, // zero-degree members included. ok is false for an unknown or empty label // (the caller falls back to the global statistic); a known label with no // such rels answers (0, true) -- a real "this hop never fires" fact, not // a missing statistic. Both averages the two directions' counts together. func ( *Snapshot) (, string, Direction) (float64, bool) { , := .Label() if ! { return 0, false } , := .labelIndex[] if ! || .Len() == 0 { return 0, false } , := .RelType() if ! { return 0, true } := .labelDegreeStats() := float64(.Len()) switch { case Outgoing: return float64(.out[]) / , true case Incoming: return float64(.in[]) / , true } return float64(.out[]+.in[]) / , true }