@@ -39,6 +39,58 @@ def _truncate_label(text: str, max_len: int = 80) -> str:
3939 return safe if len (safe ) <= max_len else (safe [: max_len - 1 ] + "…" )
4040
4141
42+ # ---------------- Participant-only Degree (Co-attendance) ----------------
43+
44+ def extract_participants (record : Dict [str , Any ]) -> List [str ]:
45+ """Extract likely participants from a meeting record.
46+ - peoplePresent: comma-separated string under meetingInfo
47+ - host, documenter: added if present (deduped)
48+ """
49+ participants : List [str ] = []
50+ meeting_info = {}
51+ if isinstance (record , dict ):
52+ meeting_info = record .get ("meetingInfo" , {}) or {}
53+ # peoplePresent as comma-separated string
54+ pp = meeting_info .get ("peoplePresent" , "" )
55+ if isinstance (pp , str ) and pp .strip ():
56+ participants .extend ([p .strip () for p in pp .split ("," ) if p .strip ()])
57+ # host/documenter as single names
58+ for key in ("host" , "documenter" ):
59+ val = meeting_info .get (key )
60+ if isinstance (val , str ) and val .strip ():
61+ participants .append (val .strip ())
62+ # dedupe while preserving order
63+ seen = set ()
64+ deduped : List [str ] = []
65+ for p in participants :
66+ if p not in seen :
67+ seen .add (p )
68+ deduped .append (p )
69+ return deduped
70+
71+
72+ def build_coattendance_graph (records : Iterable [Any ]) -> nx .Graph :
73+ G = nx .Graph ()
74+ for rec in records :
75+ participants = extract_participants (rec )
76+ if len (participants ) < 2 :
77+ continue
78+ for p in participants :
79+ G .add_node (p )
80+ for u , v in combinations (participants , 2 ):
81+ if G .has_edge (u , v ):
82+ G [u ][v ]["weight" ] += 1
83+ else :
84+ G .add_edge (u , v , weight = 1 )
85+ return G
86+
87+
88+ def degree_analysis (G : nx .Graph ) -> Tuple [Dict [str , int ], Counter ]:
89+ degree_dict = dict (G .degree ())
90+ degree_counts = Counter (degree_dict .values ())
91+ return degree_dict , degree_counts
92+
93+
4294# ---------------- JSON Path Structure ----------------
4395
4496def extract_json_paths (obj : Any , prefix : str = "" ) -> List [str ]:
@@ -161,6 +213,9 @@ def connected_components_info(G: nx.Graph, top: int) -> Dict[str, Any]:
161213def write_report (
162214 output_file : str ,
163215 summary : Dict [str , Any ],
216+ attend_deg : Tuple [Dict [str , int ], Counter ],
217+ attend_top : List [Tuple [str , int ]],
218+ attend_dist : List [Tuple [int , int ]],
164219 field_deg : Tuple [Dict [str , int ], Counter ],
165220 field_top : List [Tuple [str , int ]],
166221 field_dist : List [Tuple [int , int ]],
@@ -182,6 +237,20 @@ def write_report(
182237 f .write (f"- { k } : { v } \n " )
183238 f .write ("\n " )
184239
240+ # Participant-only Degree (Co-attendance)
241+ f .write ("## Degree (Co-attendance) Analysis\n " )
242+ f .write ("### Top Nodes by Degree\n " )
243+ f .write ("| Rank | Node | Degree |\n |------|------|--------|\n " )
244+ for i , (node , deg ) in enumerate (attend_top , 1 ):
245+ label = _truncate_label (node , 80 )
246+ f .write (f"| { i } | { label } | { deg } |\n " )
247+ f .write ("\n " )
248+ f .write ("### Degree Distribution\n " )
249+ f .write ("| Degree | Count of Nodes |\n |--------|-----------------|\n " )
250+ for d , c in attend_dist :
251+ f .write (f"| { d } | { c } |\n " )
252+ f .write ("\n " )
253+
185254 # JSON Field Degree Analysis
186255 f .write ("## JSON Field Degree Analysis\n " )
187256 f .write ("### Top Fields by Degree\n " )
@@ -276,6 +345,13 @@ def main() -> None:
276345 args = parser .parse_args ()
277346
278347 data = load_json (args .input )
348+ records = ensure_iterable_records (data )
349+
350+ # Participant-only co-attendance
351+ G_attend = build_coattendance_graph (records )
352+ attend_deg_dict , attend_deg_counts = degree_analysis (G_attend )
353+ attend_top = sorted (attend_deg_dict .items (), key = lambda x : x [1 ], reverse = True )[: args .limit_top ]
354+ attend_dist = sorted (attend_deg_counts .items (), key = lambda x : x [0 ])
279355
280356 # Path analysis
281357 all_paths = extract_json_paths (data )
@@ -299,6 +375,8 @@ def main() -> None:
299375 components = connected_components_info (G_fields , args .limit_top )
300376
301377 summary = {
378+ "Co-attendance graph (nodes)" : len (G_attend .nodes ),
379+ "Co-attendance graph (edges)" : len (G_attend .edges ),
302380 "Path graph (nodes)" : len (G_paths .nodes ),
303381 "Path graph (edges)" : len (G_paths .edges ),
304382 "Field graph (nodes)" : len (G_fields .nodes ),
@@ -308,6 +386,9 @@ def main() -> None:
308386 write_report (
309387 output_file = args .output ,
310388 summary = summary ,
389+ attend_deg = (attend_deg_dict , attend_deg_counts ),
390+ attend_top = attend_top ,
391+ attend_dist = attend_dist ,
311392 field_deg = (fdeg_dict , fdeg_counts ),
312393 field_top = field_top ,
313394 field_dist = field_dist ,
0 commit comments