ironlam commited on
Commit
033c834
·
verified ·
1 Parent(s): ee3eeb3

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. README.md +54 -12
  2. app.py +387 -0
  3. requirements.txt +4 -3
README.md CHANGED
@@ -1,20 +1,62 @@
1
  ---
2
- title: Idf Footballers
3
- emoji: 🚀
4
- colorFrom: red
5
  colorTo: red
6
- sdk: docker
7
- app_port: 8501
8
- tags:
9
- - streamlit
10
  pinned: false
11
- short_description: Explore professional footballers born in Île-de-France data
12
  license: mit
 
 
 
 
 
 
 
13
  ---
14
 
15
- # Welcome to Streamlit!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
18
 
19
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
20
- forums](https://discuss.streamlit.io).
 
1
  ---
2
+ title: IDF Footballers Explorer
3
+ emoji:
4
+ colorFrom: blue
5
  colorTo: red
6
+ sdk: streamlit
7
+ sdk_version: 1.28.0
8
+ app_file: app.py
 
9
  pinned: false
 
10
  license: mit
11
+ tags:
12
+ - football
13
+ - france
14
+ - wikidata
15
+ - demographics
16
+ - sports-analytics
17
+ - streamlit
18
  ---
19
 
20
+ # IDF Footballers Dataset Explorer
21
+
22
+ Interactive exploration of **1,165 professional footballers** born in the Paris region (Île-de-France) between 1980 and 2006.
23
+
24
+ ## Key Findings
25
+
26
+ | Metric | Value |
27
+ |--------|-------|
28
+ | Total players | 1,165 |
29
+ | Dual nationals | 39.4% |
30
+ | African diaspora* | 42.5% |
31
+ | Top département | Seine-Saint-Denis (316) |
32
+ | Top origin country | Mali (78) |
33
+
34
+ *Based on citizenship only — actual heritage is higher.
35
+
36
+ ## Features
37
+
38
+ - **Filter** by department, diaspora region, birth year, nationality status
39
+ - **Interactive map** of Île-de-France with player distribution
40
+ - **Charts**: department breakdown, diaspora regions, birth year trends, top origin countries
41
+ - **Search** players by name
42
+ - **Download** filtered data as CSV
43
+
44
+ ## Data Source
45
+
46
+ Data collected from [Wikidata](https://www.wikidata.org) using SPARQL queries.
47
+
48
+ ## Important Limitations
49
+
50
+ - **Citizenship ≠ Heritage**: Wikidata records legal nationality, not ancestry. Paul Pogba appears as "French only" despite Guinean parents.
51
+ - **Birthplace ≠ Childhood**: Players are mapped to birth location, not where they grew up.
52
+ - **Coverage bias**: Only players notable enough for Wikipedia/Wikidata are included.
53
+
54
+ ## Links
55
+
56
+ - **Dataset**: [HuggingFace](https://huggingface.co/datasets/ironlam/idf-footballers)
57
+ - **Code**: [GitHub](https://github.com/ironlam/psg-diaspora-dataset)
58
+ - **Article**: [Medium - Franciliens et PSG](https://medium.com/@diaby.lamine)
59
 
60
+ ## Author
61
 
62
+ Built by **Lamine DIABY**
 
app.py ADDED
@@ -0,0 +1,387 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ IDF Footballers Dataset Explorer
3
+
4
+ Interactive Streamlit app to explore the Île-de-France footballers dataset.
5
+ """
6
+
7
+ import streamlit as st
8
+ import pandas as pd
9
+ import plotly.express as px
10
+ from datasets import load_dataset
11
+
12
+ # Page config
13
+ st.set_page_config(
14
+ page_title="IDF Footballers Dataset",
15
+ page_icon="⚽",
16
+ layout="wide",
17
+ initial_sidebar_state="expanded"
18
+ )
19
+
20
+ # Department info with correct coordinates
21
+ DEPARTMENTS = {
22
+ 75: {"name": "Paris", "lat": 48.8566, "lon": 2.3522},
23
+ 77: {"name": "Seine-et-Marne", "lat": 48.8400, "lon": 2.9900},
24
+ 78: {"name": "Yvelines", "lat": 48.7800, "lon": 1.9900},
25
+ 91: {"name": "Essonne", "lat": 48.5300, "lon": 2.2300},
26
+ 92: {"name": "Hauts-de-Seine", "lat": 48.8500, "lon": 2.2200},
27
+ 93: {"name": "Seine-Saint-Denis", "lat": 48.9200, "lon": 2.4500},
28
+ 94: {"name": "Val-de-Marne", "lat": 48.7900, "lon": 2.4700},
29
+ 95: {"name": "Val-d'Oise", "lat": 49.0700, "lon": 2.1500},
30
+ }
31
+
32
+
33
+ @st.cache_data
34
+ def load_data():
35
+ """Load and cache the dataset from HuggingFace."""
36
+ dataset = load_dataset("ironlam/idf-footballers", split="train")
37
+ df = dataset.to_pandas()
38
+
39
+ # Drop rows with missing department (can't map them)
40
+ df = df.dropna(subset=['birth_department'])
41
+
42
+ # Ensure department is integer
43
+ df['birth_department'] = df['birth_department'].astype(int)
44
+
45
+ # Parse nationalities from string representation
46
+ df['nationalities'] = df['nationalities'].apply(lambda x: eval(x) if pd.notna(x) and isinstance(x, str) else x if isinstance(x, list) else [])
47
+ df['diaspora_countries'] = df['diaspora_countries'].apply(lambda x: eval(x) if pd.notna(x) and isinstance(x, str) and x != '[]' else x if isinstance(x, list) else [])
48
+
49
+ # Fill NaN values
50
+ df['diaspora_region'] = df['diaspora_region'].fillna('None')
51
+ df['birth_city'] = df['birth_city'].fillna('Unknown')
52
+
53
+ return df
54
+
55
+
56
+ def get_dept_label(dept_code):
57
+ """Get department label like '93 - Seine-Saint-Denis'"""
58
+ dept_int = int(dept_code)
59
+ name = DEPARTMENTS.get(dept_int, {}).get("name", "")
60
+ return f"{dept_int} - {name}" if name else str(dept_int)
61
+
62
+
63
+ def main():
64
+ # Load data
65
+ df = load_data()
66
+
67
+ # Header
68
+ st.title("⚽ IDF Footballers Dataset")
69
+ st.markdown("*Exploring professional footballers born in Île-de-France (1980-2006)*")
70
+
71
+ # Methodology expander
72
+ with st.expander("ℹ️ About this data & methodology"):
73
+ st.markdown("""
74
+ ### Data Source
75
+ This dataset was collected from **Wikidata** using SPARQL queries. It includes professional footballers
76
+ (association football players) born in Île-de-France between 1980 and 2006.
77
+
78
+ ### Key Definitions
79
+
80
+ | Term | Definition |
81
+ |------|------------|
82
+ | **Dual National** | Player with **2+ citizenships recorded** in Wikidata. This is based on legal nationality, not ancestry. |
83
+ | **African Diaspora** | Player holding citizenship from an African country (not just French). Does NOT capture heritage if player only has French citizenship. |
84
+ | **Birthplace** | Where the player was **born** (often a hospital), not necessarily where they grew up. |
85
+
86
+ ### Important Limitations
87
+
88
+ ⚠️ **Citizenship ≠ Heritage**: A player like Paul Pogba (parents from Guinea) appears as "French only" because
89
+ he doesn't hold Guinean citizenship. Kylian Mbappé shows France + Cameroon (father's nationality) but not Algeria (mother's origin).
90
+
91
+ ⚠️ **Birthplace ≠ Childhood**: Mbappé is listed as born in Paris 19e, but grew up in Bondy (93).
92
+
93
+ ⚠️ **Wikidata coverage**: Only players notable enough to have a Wikipedia/Wikidata entry are included.
94
+
95
+ ⚠️ **~90 players** have unknown departments (birthplace couldn't be mapped to a département).
96
+
97
+ ### What this data CAN tell us
98
+ - Geographic distribution of professional footballers across IDF
99
+ - Minimum bounds on diaspora representation (actual heritage is higher)
100
+ - Trends over time (birth years)
101
+
102
+ ### What this data CANNOT tell us
103
+ - Full ancestral/heritage backgrounds
104
+ - Where players actually grew up or trained
105
+ - Career success levels (all pros counted equally)
106
+ """)
107
+
108
+ st.divider()
109
+
110
+ # Sidebar filters
111
+ st.sidebar.header("🔍 Filters")
112
+
113
+ # Department filter
114
+ dept_options = ["All"] + [get_dept_label(d) for d in sorted(DEPARTMENTS.keys())]
115
+ selected_dept_display = st.sidebar.selectbox("Department", dept_options)
116
+
117
+ if selected_dept_display == "All":
118
+ selected_dept = "All"
119
+ else:
120
+ selected_dept = int(selected_dept_display.split(" - ")[0])
121
+
122
+ # Diaspora filter
123
+ diaspora_regions = ["All"] + sorted([r for r in df['diaspora_region'].unique() if r != 'None'])
124
+ selected_diaspora = st.sidebar.selectbox("Diaspora Region", diaspora_regions)
125
+
126
+ # Birth year range
127
+ min_year, max_year = int(df['birth_year'].min()), int(df['birth_year'].max())
128
+ year_range = st.sidebar.slider("Birth Year Range", min_year, max_year, (min_year, max_year))
129
+
130
+ # Dual nationality filter
131
+ dual_national_filter = st.sidebar.radio("Nationality", ["All", "Dual nationals only", "Single nationality only"])
132
+
133
+ # Apply filters
134
+ filtered_df = df.copy()
135
+
136
+ if selected_dept != "All":
137
+ filtered_df = filtered_df[filtered_df['birth_department'] == selected_dept]
138
+
139
+ if selected_diaspora != "All":
140
+ filtered_df = filtered_df[filtered_df['diaspora_region'] == selected_diaspora]
141
+
142
+ filtered_df = filtered_df[
143
+ (filtered_df['birth_year'] >= year_range[0]) &
144
+ (filtered_df['birth_year'] <= year_range[1])
145
+ ]
146
+
147
+ if dual_national_filter == "Dual nationals only":
148
+ filtered_df = filtered_df[filtered_df['is_dual_national'] == True]
149
+ elif dual_national_filter == "Single nationality only":
150
+ filtered_df = filtered_df[filtered_df['is_dual_national'] == False]
151
+
152
+ # Key metrics
153
+ col1, col2, col3, col4 = st.columns(4)
154
+
155
+ with col1:
156
+ st.metric("Total Players", f"{len(filtered_df):,}")
157
+
158
+ with col2:
159
+ dual_pct = (filtered_df['is_dual_national'].sum() / len(filtered_df) * 100) if len(filtered_df) > 0 else 0
160
+ st.metric("Dual Nationals", f"{dual_pct:.1f}%", help="Players with 2+ citizenships recorded in Wikidata.")
161
+
162
+ with col3:
163
+ african_diaspora_count = len(filtered_df[filtered_df['diaspora_region'] == 'Africa'])
164
+ african_diaspora_pct = (african_diaspora_count / len(filtered_df) * 100) if len(filtered_df) > 0 else 0
165
+ st.metric("African Diaspora*", f"{african_diaspora_pct:.1f}%", help="Based on citizenship only. Actual heritage is likely higher.")
166
+
167
+ with col4:
168
+ if len(filtered_df) > 0:
169
+ top_dept = filtered_df['birth_department'].mode().iloc[0]
170
+ top_dept_name = DEPARTMENTS.get(int(top_dept), {}).get("name", str(top_dept))
171
+ else:
172
+ top_dept_name = "N/A"
173
+ st.metric("Top Department", top_dept_name)
174
+
175
+ st.divider()
176
+
177
+ # Charts row 1
178
+ col1, col2 = st.columns(2)
179
+
180
+ with col1:
181
+ st.subheader("📍 Players by Department")
182
+
183
+ # Build department counts with proper labels
184
+ dept_data = []
185
+ for dept_code in DEPARTMENTS.keys():
186
+ count = len(filtered_df[filtered_df['birth_department'] == dept_code])
187
+ dept_data.append({
188
+ 'code': dept_code,
189
+ 'label': get_dept_label(dept_code),
190
+ 'count': count
191
+ })
192
+
193
+ dept_df = pd.DataFrame(dept_data)
194
+ dept_df = dept_df[dept_df['count'] > 0].sort_values('count', ascending=True)
195
+
196
+ if len(dept_df) > 0:
197
+ fig = px.bar(
198
+ dept_df,
199
+ x='count',
200
+ y='label',
201
+ orientation='h',
202
+ color='count',
203
+ color_continuous_scale='Blues',
204
+ text='count'
205
+ )
206
+ fig.update_layout(
207
+ showlegend=False,
208
+ xaxis_title="Number of Players",
209
+ yaxis_title="",
210
+ coloraxis_showscale=False,
211
+ height=400,
212
+ plot_bgcolor='rgba(0,0,0,0)',
213
+ paper_bgcolor='rgba(0,0,0,0)',
214
+ font=dict(size=12)
215
+ )
216
+ fig.update_traces(textposition='outside')
217
+ st.plotly_chart(fig, use_container_width=True)
218
+ else:
219
+ st.info("No data for current filters")
220
+
221
+ with col2:
222
+ st.subheader("🌍 Diaspora Regions")
223
+ diaspora_counts = filtered_df[filtered_df['diaspora_region'] != 'None']['diaspora_region'].value_counts()
224
+
225
+ if len(diaspora_counts) > 0:
226
+ fig = px.pie(
227
+ values=diaspora_counts.values,
228
+ names=diaspora_counts.index,
229
+ hole=0.4,
230
+ color_discrete_sequence=px.colors.qualitative.Set2
231
+ )
232
+ fig.update_layout(
233
+ height=400,
234
+ plot_bgcolor='rgba(0,0,0,0)',
235
+ paper_bgcolor='rgba(0,0,0,0)'
236
+ )
237
+ fig.update_traces(textposition='inside', textinfo='percent+label')
238
+ st.plotly_chart(fig, use_container_width=True)
239
+ else:
240
+ st.info("No diaspora data for current filters")
241
+
242
+ # Charts row 2
243
+ col1, col2 = st.columns(2)
244
+
245
+ with col1:
246
+ st.subheader("📅 Birth Year Distribution")
247
+ year_counts = filtered_df['birth_year'].value_counts().sort_index()
248
+
249
+ if len(year_counts) > 0:
250
+ fig = px.bar(
251
+ x=year_counts.index,
252
+ y=year_counts.values,
253
+ labels={'x': 'Birth Year', 'y': 'Number of Players'},
254
+ color=year_counts.values,
255
+ color_continuous_scale='Greens'
256
+ )
257
+ fig.update_layout(
258
+ height=350,
259
+ coloraxis_showscale=False,
260
+ plot_bgcolor='rgba(0,0,0,0)',
261
+ paper_bgcolor='rgba(0,0,0,0)'
262
+ )
263
+ st.plotly_chart(fig, use_container_width=True)
264
+
265
+ with col2:
266
+ st.subheader("🏆 Top Origin Countries")
267
+ # Flatten diaspora countries
268
+ all_countries = []
269
+ for countries in filtered_df['diaspora_countries']:
270
+ if isinstance(countries, list):
271
+ all_countries.extend(countries)
272
+
273
+ if all_countries:
274
+ country_counts = pd.Series(all_countries).value_counts().head(10)
275
+
276
+ fig = px.bar(
277
+ x=country_counts.values,
278
+ y=country_counts.index,
279
+ orientation='h',
280
+ color=country_counts.values,
281
+ color_continuous_scale='Oranges',
282
+ text=country_counts.values
283
+ )
284
+ fig.update_layout(
285
+ xaxis_title="Number of Players",
286
+ yaxis_title="",
287
+ coloraxis_showscale=False,
288
+ height=350,
289
+ yaxis={'categoryorder': 'total ascending'},
290
+ plot_bgcolor='rgba(0,0,0,0)',
291
+ paper_bgcolor='rgba(0,0,0,0)'
292
+ )
293
+ fig.update_traces(textposition='outside')
294
+ st.plotly_chart(fig, use_container_width=True)
295
+ else:
296
+ st.info("No origin country data for current filters")
297
+
298
+ st.divider()
299
+
300
+ # Map
301
+ st.subheader("🗺️ Geographic Distribution")
302
+
303
+ # Prepare map data
304
+ map_data = []
305
+ for dept_code, info in DEPARTMENTS.items():
306
+ count = len(filtered_df[filtered_df['birth_department'] == dept_code])
307
+ if count > 0:
308
+ map_data.append({
309
+ 'department': str(dept_code),
310
+ 'name': f"{dept_code} - {info['name']}",
311
+ 'lat': info['lat'],
312
+ 'lon': info['lon'],
313
+ 'count': count
314
+ })
315
+
316
+ if map_data:
317
+ map_df = pd.DataFrame(map_data)
318
+
319
+ fig = px.scatter_map(
320
+ map_df,
321
+ lat='lat',
322
+ lon='lon',
323
+ size='count',
324
+ color='count',
325
+ hover_name='name',
326
+ hover_data={'count': True, 'lat': False, 'lon': False, 'department': False},
327
+ color_continuous_scale='Reds',
328
+ size_max=60,
329
+ zoom=8.5,
330
+ center={'lat': 48.85, 'lon': 2.35}
331
+ )
332
+ fig.update_layout(
333
+ map_style='open-street-map',
334
+ height=500,
335
+ margin={'r': 0, 't': 0, 'l': 0, 'b': 0}
336
+ )
337
+ st.plotly_chart(fig, use_container_width=True)
338
+
339
+ st.divider()
340
+
341
+ # Data table
342
+ st.subheader("📋 Player Data")
343
+
344
+ # Search
345
+ search = st.text_input("🔎 Search by name", "")
346
+
347
+ display_df = filtered_df.copy()
348
+ if search:
349
+ display_df = display_df[display_df['name'].str.contains(search, case=False, na=False)]
350
+
351
+ # Format for display
352
+ display_cols = ['name', 'birth_year', 'birth_city', 'birth_department', 'diaspora_region', 'is_dual_national']
353
+ display_df_show = display_df[display_cols].copy()
354
+ display_df_show.columns = ['Name', 'Birth Year', 'Birth City', 'Department', 'Diaspora Region', 'Dual National']
355
+ display_df_show['Department'] = display_df_show['Department'].apply(lambda x: get_dept_label(x))
356
+ display_df_show['Dual National'] = display_df_show['Dual National'].apply(lambda x: '✓' if x else '')
357
+ display_df_show['Diaspora Region'] = display_df_show['Diaspora Region'].apply(lambda x: x if x != 'None' else '-')
358
+
359
+ st.dataframe(
360
+ display_df_show.sort_values('Name'),
361
+ use_container_width=True,
362
+ height=400
363
+ )
364
+
365
+ # Download button
366
+ csv = filtered_df.to_csv(index=False)
367
+ st.download_button(
368
+ label="📥 Download filtered data (CSV)",
369
+ data=csv,
370
+ file_name="idf_footballers_filtered.csv",
371
+ mime="text/csv"
372
+ )
373
+
374
+ # Footer
375
+ st.divider()
376
+ st.markdown("""
377
+ **Data source:** [Wikidata](https://www.wikidata.org) |
378
+ **Dataset:** [HuggingFace](https://huggingface.co/datasets/ironlam/idf-footballers) |
379
+ **Code:** [GitHub](https://github.com/ironlam/psg-diaspora-dataset) |
380
+ **Article:** [Medium](https://medium.com/@diaby.lamine)
381
+
382
+ *Built by Lamine DIABY*
383
+ """)
384
+
385
+
386
+ if __name__ == "__main__":
387
+ main()
requirements.txt CHANGED
@@ -1,3 +1,4 @@
1
- altair
2
- pandas
3
- streamlit
 
 
1
+ streamlit>=1.28.0
2
+ pandas>=2.0.0
3
+ plotly>=5.18.0
4
+ datasets>=2.14.0