a
    %û»j‰<  ã                   @   sÔ   d Z ddlmZ ddlZddlZedƒZdZej	dd�ej	dd�ej	d	d�ej	d
d�e 	d¡dœZ
dd„ Zdd„ Zdd„ Zdd„ Zh d£Zdd„ Zdd„ Zdd„ Zdd„ Zdd„ Zd d!„ Zd"d#„ Zed$krÐeƒ  dS )%aR  Pre-match features for every match in matches_joined.csv (ML experiment).

Run after ml_join.py, with the isolated env:
    E:\soccer\ml_env\Scripts\python.exe research\ml_features.py

Every feature is known BEFORE kickoff. Team form is built from each club's
previous matches only (shift, then exponentially weighted), across divisions,
so a promoted club carries its lower-division form with it.

Groups (column prefix):
  elo_     pre-match Elo (ml_elo.py)
  h_/a_    home/away team form, EWM half-life 5 matches: goals, shots, shots
           on target, corners, points, and Understat xG/npxG/PPDA/deep
           completions where the league has them; plus rest days
  bk_      bookmaker implied probabilities, de-vigged: market-average opening
           and closing, Pinnacle closing, and the open->close move
  k_       Kalshi: de-vigged mid at KO-24h / -6h / -1h / -15m / KO, the ask
           and bid at KO (what a bet would actually pay), volume and open
           interest
  tv_      Transfermarkt squad market value: top-18 / top-11 players by
           value on the club's books at the last month-end before the match
           (first tiers only; valuations frozen 2026-07-06)
  sf_      SoFIFA squad ratings from the latest snapshot on or before the
           match (added once that download finishes)

Writes research/ml_data/features.csv.
é    )ÚPathNzE:\soccer\research\ml_dataé   é   ©Úhoursé   é   é   )Úminutes)Ú24hZ6hZ1hZ15mÚkoc                 C   s.   d| | j tjdd� }|j|jdd�dd�S )Nr   Úcoerce)Úerrors©Úaxisr   )ÚapplyÚpdÚ
to_numericÚdivÚsum)ÚdfÚcolsÚinv© r   ú!E:\soccer\research\ml_features.pyÚdevig(   s    r   c           
      C   sÀ  t  | j| j| jd| j| j| j| j| j	| j
| j| j| j| j| j| j| j| j| j| jdœ¡}t  | j| j| jd| j| j| j| j| j
| j	| j| j| j| j| j| j| j| j| j| jdœ¡}t  ||g¡ g d¢¡jdd�}t |j|jk|j|jkgddgd¡|d< g d	¢}|jd
dd�}|D ]"}||  dd„ ¡|d|› �< �q| ¡ |d< |d   ¡ j!j"j#dd�|d< dd„ |D ƒddg }||j$dk  %d¡|  &d¡}||j$dk  %d¡|  &d¡}	| '|	¡S )zCLong table (one row per team per match) -> EWM of previous matches.r   )ÚmidÚdateÚteamÚhomeÚgfÚgaÚsfÚsaÚstfÚstaÚcfÚcaÚxgfÚxgaÚnpxgfÚnpxgaÚppdaÚppda_oppÚdeepfÚdeepar   )r   r   r   T)Údropé   Úpts)r    r!   r"   r#   r$   r%   r&   r'   r2   r(   r)   r*   r+   r,   r-   r.   r/   r   F)Úsortc                 S   s   |   d¡jtdd� ¡ S )Nr   r1   )ÚhalflifeÚmin_periods)ÚshiftÚewmÚ	HALF_LIFEÚmean)Úsr   r   r   Ú<lambda>F   s   ÿzteam_form.<locals>.<lambda>Úf_Zf_nr   é   )ÚupperZf_restc                 S   s   g | ]}d |› �‘qS )r<   r   ©Ú.0Úcr   r   r   Ú
<listcomp>J   ó    zteam_form.<locals>.<listcomp>r   Zh_Úa_)(r   Ú	DataFrameÚindexÚDateÚHomeTeamÚFTHGÚFTAGZHSÚASÚHSTÚASTZHCZACZ
us_home_xgZ
us_away_xgZus_home_np_xgZus_away_np_xgZus_home_ppdaZus_away_ppdaZus_home_deep_completionsZus_away_deep_completionsÚAwayTeamÚconcatÚsort_valuesÚreset_indexÚnpÚselectr    r!   ÚgroupbyÚ	transformÚcumcountÚdiffÚdtÚdaysÚclipr   Ú	set_indexÚ
add_prefixÚjoin)
Úmr   ZawayÚtZstatsÚgrA   ZfeatÚhÚar   r   r   Ú	team_form.   s8    úú * rc   c                 C   sÎ   t j| jd�}g d¢g d¢g d¢g d¢dœ ¡ D ]<\}}t| |ƒ}| ¡ |d|› d�d|› d	�d|› d
�g< q0dD ]*}|d|› � |d|› �  |d|› �< qrt| ddgƒjd d …df |d< | d |d< |S )N©rF   )ZAvgCHZAvgCDZAvgCA)ZAvgHZAvgDZAvgA)ZPSCHZPSCDZPSCA)ZB365CHZB365CDZB365CA)ZavgcZavgoZpscZb365cZbk_Ú_hÚ_dZ_aÚhdaZbk_avgc_Zbk_avgo_Zbk_move_zAvgC>2.5zAvgC<2.5r   Zbk_over25_cZAHChZbk_ahc_line)r   rE   rF   Úitemsr   Úto_numpyÚiloc)r^   ÚoutÚnamer   Úpr:   r   r   r   Ú	bookmakerP   s    ÿ
,( rn   c                 C   s¦  t jtd d g d¢dgd� d¡}g }t jdddd	�D ]†}||j|k  d
¡ d¡}||j|t jdd� k }|jddd� d¡}| 	t  
||j dd„ ¡|j dd„ ¡| ¡ dœ¡ ¡ ¡ q8t  |¡jddid� d¡}t j
| jd�}dD ]¢\}}	| |	dg  ¡ j|	did�}
|
j t¡|
d< |
 ¡  d¡}
t j|
|ddddd�}| d¡}t |jjdd �¡|d!|› d"�< t |jjdd �¡|d!|› d#�< qî|j|j |d$< |S )%zôTransfermarkt squad value per club at month-end snapshots.

    A player counts for the club named on his latest valuation as of the
    snapshot; valuations are revised a few times a year, so month-ends are
    finer than the data itself.
    Ztransfermarktzplayer_valuations.csv.gz)Ú	player_idr   Úmarket_value_in_eurÚcurrent_club_idr   )ÚusecolsÚparse_datesz
2019-06-30z
2026-07-31ÚME)Úfreqro   r   i&  ©rY   rp   F©Ú	ascendingrq   c                 S   s   |   d¡ ¡ S )Né   ©Úheadr   ©Úxr   r   r   r;   n   rC   zsquad_value.<locals>.<lambda>c                 S   s   |   d¡ ¡ S )Né   rz   r|   r   r   r   r;   o   rC   )ÚsnapÚtop18Útop11ÚnÚclub©Úcolumnsr   rd   ))ra   Útm_home_club_id)rb   Útm_away_club_idrG   Úbackward)Úleft_onÚright_onÚbyÚ	directionrF   ç     jø@)ÚlowerZtv_Z_top18Z_top11Ú	tv_diff18)r   Úread_csvÚMLrP   Ú
date_ranger   rT   ÚtailÚ	TimedeltaÚappendrE   rp   r   ÚsizerQ   rO   ÚrenamerF   Údropnarƒ   ÚastypeÚintÚ
merge_asofr[   rR   Úlog10r€   rZ   r�   Z
tv_h_top18Z
tv_a_top18)ÚfÚvZsnapsÚmeÚcurÚvalsÚsnrk   ÚsideÚcolÚqÚjr   r   r   Úsquad_value]   s:    þþü
 "r§   >"   ÚusÚsvZvflÚifÚasZscÚssZfkÚskÚderƒ   r'   ZtsgÚ1ZkulubuZfootballÚsdZsscÚfcÚudZkrcZkaaZcalcioZtheZsporZrscÚrcZkvZcdr&   ZvfbZbkÚacZafcc                 C   sT   dd l }| dt| ƒ¡ dd¡ ¡  ¡ } d dd„ | D ƒ¡} d d	d„ |  ¡ D ƒ¡S )
Nr   ZNFKDÚasciiÚignoreÚ c                 s   s   | ]}|  ¡ r|nd V  qdS )ú N)Úisalnum)r@   Úchr   r   r   Ú	<genexpr>‡   rC   z_norm.<locals>.<genexpr>r¸   c                 s   s   | ]}|t vr|V  qd S ©N)Ú_STOP)r@   r_   r   r   r   r»   ˆ   rC   )ÚunicodedataÚ	normalizeÚstrÚencodeÚdecoderŽ   r]   Úsplit)r}   r¾   r   r   r   Ú_norm„   s     rÄ   c                 C   sN   ddl m} t| ƒt|ƒ } }| r&|s*dS | |v s:|| v r>dS |d | |ƒ ¡ S )Nr   )ÚSequenceMatcherg        gffffffî?)ZdifflibrÅ   rÄ   Zratio)rb   ÚbrÅ   r   r   r   Ú_sim‹   s    rÇ   c                    sV  ddl }ddlm} i ‰ t td ¡}||j }t|j|j	| 
d|j	¡ƒD ],\‰}}ˆ  ˆˆh¡ dd„ ||fD ƒ¡ qJ| tdƒjd	d
�¡}| ¡ D ]L\}}	t|	tƒr–|	 
d¡r–ˆ  |	d |	d h¡ |gt|	 
d¡pÚg ƒ ¡ q–i g  }
}|  d¡D �]&\}}|j ¡ tjdd� |j ¡ tjdd�  }}||j|k|j ||¡@  }tt|jƒt|jƒB ƒ‰|  d¡ddg  !¡ }ˆrøt"|ƒ�sŠqøt# $‡ ‡fdd„|D ƒ¡}|| ƒ\}}t||ƒD ]b\}}|||f dk�rðˆ| |
||| d f< n,| %||| d ˆ| t&|||f dƒf¡ �q¼qø|�rRt'dt"|ƒ› d�ƒ |D ]}t'd|ƒ �q@|
S )a  SoFIFA (league, team_id) -> league_data name, per division.

    SoFIFA has no fixtures, so co-occurrence cannot work. Instead, within each
    division, SoFIFA clubs and the clubs league_data has in that division
    within a year of the snapshots are paired one-to-one (Hungarian
    assignment) by the best name similarity over every alias the other
    sources gave us (Understat / Transfermarkt / Kalshi names from
    name_map.csv, and team_map.json). Pairs below 0.6 similarity are dropped
    and printed.
    r   N)Úlinear_sum_assignmentzname_map.csvZ	src_labelc                 s   s$   | ]}t |tƒr| ¡ s|V  qd S r¼   )Ú
isinstancerÀ   Úisdigit)r@   r}   r   r   r   r»   ¦   rC   z"sofifa_team_map.<locals>.<genexpr>zE:\soccer\data\team_map.jsonzutf-8)ÚencodingZfdcoÚaliasesÚfdco_leagueim  rv   Úteam_idÚ	team_namec                    s$   g | ]\}‰ ‡‡ fd d„ˆD ƒ‘qS )c                    s,   g | ]$}t ‡fd d„ˆ  ||h¡D ƒƒ‘qS )c                 3   s   | ]}t ˆ |ƒV  qd S r¼   )rÇ   )r@   rb   ©Útnr   r   r»   ³   rC   z8sofifa_team_map.<locals>.<listcomp>.<listcomp>.<genexpr>)ÚmaxÚget)r@   r�   )ÚaliasrÑ   r   r   rB   ³   rC   z.sofifa_team_map.<locals>.<listcomp>.<listcomp>r   )r@   Ú_©rÔ   ÚfdrÐ   r   rB   ³   rC   z#sofifa_team_map.<locals>.<listcomp>g333333ã?r   é   zsofifa: z+ clubs left unmapped (best pair below 0.6):z   )(ÚjsonZscipy.optimizerÈ   r   r�   r‘   ÚokÚzipZfd_nameZsrc_namerÓ   Ú
setdefaultÚupdateÚloadsr   Ú	read_textrh   rÉ   ÚdictÚlistrT   Ú	update_dtÚminr”   rÒ   ÚDivrG   ÚbetweenÚsortedÚsetrH   rN   Údrop_duplicatesri   ÚlenrR   Úarrayr•   ÚroundÚprint)r"   ÚspinerÙ   rÈ   Únmrb   rÆ   ZtmapÚkrž   rk   Údroppedr   r`   ÚloÚhiÚspZteamsÚSÚrrA   Úir¦   Údr   rÖ   r   Úsofifa_team_map•   s<    
$$2
.2rø   c                 C   sf   t | ƒ dd¡ ¡ } |  d¡r"dn|  d¡r0dnd}zt|  d¡ƒ| W S  ty`   tj Y S 0 d S )	Nu   â‚¬r·   ÚMg    €„.AÚKg     @�@r   ZMK)	rÀ   ÚreplaceÚstripÚendswithÚfloatÚrstripÚ
ValueErrorrR   Únan)r}   Úmultr   r   r   Ú_moneyÁ   s     r  c                    sB  t d }| ¡ stj| jd�S t |¡}tj|jdd�|d< t||ƒ‰‡fdd„t	|j
|jƒD ƒ|d< ||j ¡   ¡ }|j t¡|d	< |j d
¡j ¡ jd }t |dk| g d¢¡| g d¢¡gg d¢d¡|d< g }| ddg¡D ]¬\\}}‰ ˆ jddd�‰ ‡ fdd„}| ||ˆ j d¡ ¡ ˆ jjdd…  ¡ |ddƒ|ddƒ|ddƒ|ddƒˆ  d¡j  ¡ ˆ  d¡j! ¡ t "t#ˆ j$ d¡ %¡ dƒ¡d œ¡ qæt |¡ d!¡}	tj| jd�}
d"D ]v\}}| d#|g j&|d$id%� '¡  d#¡}tj(||	d#d!d$d&tj)d'd(�d)� *d*¡}d+D ]}|| |
d,|› d-|› �< �q
�q¶|
j+|
j, |
d.< |
S )/zFSoFIFA squad ratings from the latest snapshot on or before each match.zsofifa_player_ratings.csvrd   z	%b %d, %Y)Úformatrâ   c                    s   g | ]\}}ˆ   ||f¡‘qS r   )rÓ   )r@   r÷   r_   )Úmpr   r   rB   Ò   rC   zsofifa.<locals>.<listcomp>r×   Úvalr·   r   ZGK)ZCBÚLBZRBZLWBZRWB)ZCDMZCMZCAMZLMZRM)ÚgkÚdefr   ÚattÚlineÚoaFrw   c                    s   ˆ ˆ j | k j |¡ ¡ S r¼   )r  r  r{   r9   )r  r‚   )r`   r   r   r;   Ü   rC   zsofifa.<locals>.<lambda>r~   ry   r  r   r	  é   r   r1   r�   )r   r   r�   Údepthr  r	  r   r
  Úage11Úpot11Úval18r   ))ra   rH   )rb   rN   rG   r   r„   rˆ   éx   rv   )r‰   rŠ   r‹   rŒ   Ú	tolerancerF   )	r�   r  r  r	  r   r
  r  r  r  Zsf_rÕ   Ú	sf_diff11)-r‘   Úexistsr   rE   rF   r�   Úto_datetimeÚupdate_daterø   rÛ   rÍ   rÎ   r×   ÚnotnaÚcopyÚvlÚmapr  Ú	positionsÚfillnarÀ   rÃ   rR   rS   ÚisinrT   rP   r•   r  r{   r9   rj   ZaeÚptrœ   rÒ   r  r   r—   rQ   r›   r”   r[   Z
sf_h_top11Z
sf_a_top11)r�   rí   Úpathr"   ÚposÚrowsZdt_r×   Útopr   rk   r£   r¤   r¥   r¦   rA   r   )r`   r  r   ÚsofifaÊ   sL    

 ÿþ"ü
"ÿÿ r$  c              	      sÜ  | | j  ¡  }|jr"tj| jd�S tjtd d dgd�}tjtd d dgd�}|jj	 
d¡|d< |jj	 
d¡|d< | d¡}| d¡}g }| ¡ D �]¢\}}t |j¡ 
d¡}d|i}d	|jfd
|jfd|jffD �]V\}	}
||j|
k }||j|
k }t ¡ D ]à\‰}|| }ˆdk�r.|tjdd�fn|tjdd�f\}}||j|k|j|| k@   d¡}t|ƒ�r|jjd |jjd  }}|| d |dˆ› d|	› �< ˆdk�r|| |d|	› �< |d|	› �< |jjd |d|	› �< �q||j|k|j|tjdd� k@  }t|ƒ�r|j ¡ ntj|d|	› �< qÖ|  |¡ q–t |¡ !d¡‰ tD ]†‰‡fdd„dD ƒ}t"‡ fdd„|D ƒƒ�rPˆ | jdd�}|d ˆ d ˆ› �< dD ].}ˆ dˆ› d|› � | ˆ d!ˆ› d|› �< �q¤�qPˆ S )"aÀ  Mid / bid / ask per side at fixed offsets before kickoff.

    A candle is stamped with the END of its period, so the bar stamped at KO
    covers the minute before kickoff. Bars exist only where the book existed;
    the value at time t is the last bar at or before t (a quiet book simply
    kept its quote), but never older than 3h for the 1-minute offsets or 12h
    for KO-24h, so a stale quote from the day before cannot pose as current.
    rd   Zkalshi_historyzcandles_1m.csvÚts_utc)rs   zcandles_1h.csvNr   ra   r÷   rb   r   é   r   r1   r   r   rØ   Úk_mid_rÕ   r   Zk_bid_Zk_ask_Zk_oi_Zk_vol3h_c                    s   g | ]}d ˆ › d|› �‘qS )r'  rÕ   r   )r@   r:   )Úlabr   r   rB     rC   zkalshi.<locals>.<listcomp>rg   c                 3   s   | ]}|ˆ v V  qd S r¼   r   r?   )rï   r   r   r»     rC   zkalshi.<locals>.<genexpr>r   Zk_overround_Zk_p_)#Úkalshi_eventr  Úemptyr   rE   rF   r�   r‘   r%  rX   Ú
tz_convertrP   ÚiterrowsÚ	TimestampÚkalshi_kickoff_utcZkalshi_tk_homeZkalshi_tk_tieZkalshi_tk_awayZtickerÚ	K_OFFSETSrh   r”   r“   ré   Z	bid_closerj   Z	ask_closeZopen_interestÚvolumer   rR   r  r•   r[   Úall)r^   ZkmÚc1rº   r"  r   rõ   r   Úrecr£   ZtkÚb1ÚbhÚoffr_   ÚsrcÚtolr}   ÚbidZaskÚprer   Útotr:   r   )rï   r(  r   Úkalshiî   sL    	

&."

$&0r<  c                  C   s>  t jtd dgdd�} | g d¢  ¡ }| t| ƒ¡ t| ƒ¡}| t|ƒ¡}| t|| ƒ¡}t	| ƒ}| |¡}|j
td dd� ||jdk }td	t|ƒ› d
|jd › dtd › �ƒ tdt|ƒ› d|j ¡  ¡ › d| dt jtd�¡ ¡  ¡ › d�ƒ tdƒ dD ]4}||v �rtd|d›d||  ¡  ¡ d›�ƒ �qd S )Nzmatches_joined.csvrG   F)rs   Ú
low_memory)rä   ZseasonrG   ÚTimerH   rN   rI   rJ   ZFTRZelo_homeZelo_awayÚelo_diffZelo_exp_homer)  r.  Z
tm_game_idr†   r‡   zfeatures.csvrd   z
2025-05-01z
features: z rows x r   z	 cols -> zsince 2025-05-01: z
 matches, z with Kalshi, Úk_p_ko_h)Údtypez with a Kalshi price at KOz,
fill rate of key features since 2025-05-01:)r?  Zh_f_sfZh_f_xgfr�   r  Z	bk_avgc_hZbk_psc_hZ	k_p_24h_hZk_p_1h_hr@  Zk_ask_hz  Z12r¸   z6.1%)r   r�   r‘   r  r]   rc   rn   r§   r$  r<  Úto_csvrG   rì   ré   Úshaper)  r  r   rÓ   ÚSeriesrþ   r9   )r^   Úbaser�   rï   ÚkwrA   r   r   r   Úmain  s"    
(ÿ

rG  Ú__main__)Ú__doc__Úpathlibr   ÚnumpyrR   Úpandasr   r‘   r8   r”   r/  r   rc   rn   r§   r½   rÄ   rÇ   rø   r  r$  r<  rG  Ú__name__r   r   r   r   Ú<module>   s.   þ""
,	$1