
# Fichier: python_cheats/cheatsheets/pandas_complete.txt
# Python Pandas - Data Analysis Library Complète



[OK] INSTALLATION ET IMPORT


[OK] INSTALLATION
    pip install pandas
    pip install pandas openpyxl  # Pour Excel
    pip install pandas xlrd      # Pour ancien format Excel (.xls)
    pip install pandas sqlalchemy  # Pour bases de données

[OK] IMPORT
    import pandas as pd
    import numpy as np
    
    # Afficher la version
    print(pd.__version__)

[OK] OPTIONS D'AFFICHAGE
    # Nombre de lignes/colonnes affichées
    pd.set_option('display.max_rows', 100)
    pd.set_option('display.max_columns', 50)
    
    # Largeur d'affichage
    pd.set_option('display.width', 1000)
    pd.set_option('display.max_colwidth', 50)
    
    # Précision des nombres
    pd.set_option('display.precision', 2)
    
    # Réinitialiser
    pd.reset_option('all')


[OK] LECTURE DE DONNÉES


[OK] CSV
    # Basique
    df = pd.read_csv('data.csv')
    
    # Avec options
    df = pd.read_csv('data.csv',
                     sep=';',              # Séparateur
                     encoding='utf-8',      # Encodage
                     header=0,              # Ligne des en-têtes
                     names=['col1', 'col2'], # Noms personnalisés
                     index_col='id',        # Colonne index
                     usecols=['col1', 'col2'], # Colonnes à lire
                     skiprows=5,            # Ignorer les 5 premières lignes
                     nrows=1000,            # Lire seulement 1000 lignes
                     na_values=['NA', 'null'], # Valeurs considérées comme NaN
                     parse_dates=['date'],  # Parser les dates
                     dtype={'age': int},    # Types de données
                     thousands=',',         # Séparateur de milliers
                     decimal='.')           # Séparateur décimal

[OK] EXCEL
    # Lire une feuille
    df = pd.read_excel('data.xlsx', sheet_name='Sheet1')
    
    # Lire toutes les feuilles
    all_sheets = pd.read_excel('data.xlsx', sheet_name=None)
    df1 = all_sheets['Sheet1']
    
    # Avec options
    df = pd.read_excel('data.xlsx',
                       sheet_name='Sheet1',
                       header=0,
                       index_col=0,
                       usecols='A:D',      # Colonnes A à D
                       skiprows=2,
                       nrows=100)

[OK] JSON
    # Depuis fichier
    df = pd.read_json('data.json')
    
    # Depuis string JSON
    import json
    json_str = '{"name":["Alice","Bob"],"age":[25,30]}'
    df = pd.read_json(json_str)
    
    # Avec orientation
    df = pd.read_json('data.json', orient='records')
    # orient: 'split', 'records', 'index', 'columns', 'values'

[OK] SQL
    import sqlite3
    
    # SQLite
    conn = sqlite3.connect('database.db')
    df = pd.read_sql('SELECT * FROM users', conn)
    df = pd.read_sql_query('SELECT * FROM users WHERE age > 25', conn)
    df = pd.read_sql_table('users', conn)
    
    # Avec SQLAlchemy
    from sqlalchemy import create_engine
    engine = create_engine('postgresql://user:pass@localhost/dbname')
    df = pd.read_sql('SELECT * FROM users', engine)

[OK] HTML
    # Lire toutes les tables d'une page
    tables = pd.read_html('https://example.com')
    df = tables[0]  # Première table

[OK] CLIPBOARD
    # Copier depuis Excel/tableur
    df = pd.read_clipboard()

[OK] PARQUET (FORMAT EFFICACE)
    # Lire
    df = pd.read_parquet('data.parquet')
    
    # Écrire
    df.to_parquet('data.parquet')

[OK] PICKLE (PANDAS NATIF)
    # Sauvegarder
    df.to_pickle('data.pkl')
    
    # Charger
    df = pd.read_pickle('data.pkl')

[OK] URL
    # Lire directement depuis URL
    url = 'https://example.com/data.csv'
    df = pd.read_csv(url)


[OK] CRÉATION DE DATAFRAMES


[OK] DEPUIS DICTIONNAIRE
    # Dict de listes
    data = {
        'name': ['Alice', 'Bob', 'Charlie'],
        'age': [25, 30, 35],
        'city': ['Paris', 'London', 'Berlin']
    }
    df = pd.DataFrame(data)
    
    # Liste de dicts
    data = [
        {'name': 'Alice', 'age': 25},
        {'name': 'Bob', 'age': 30}
    ]
    df = pd.DataFrame(data)

[OK] DEPUIS NUMPY ARRAY
    import numpy as np
    
    arr = np.array([[1, 2, 3], [4, 5, 6]])
    df = pd.DataFrame(arr, columns=['A', 'B', 'C'])

[OK] DEPUIS SERIES
    s1 = pd.Series([1, 2, 3], name='col1')
    s2 = pd.Series([4, 5, 6], name='col2')
    df = pd.concat([s1, s2], axis=1)

[OK] DATAFRAME VIDE
    # Vide avec colonnes
    df = pd.DataFrame(columns=['A', 'B', 'C'])
    
    # Avec index
    df = pd.DataFrame(index=range(10), columns=['A', 'B'])

[OK] RANGE ET DATES
    # Range de dates
    dates = pd.date_range('2024-01-01', periods=10, freq='D')
    df = pd.DataFrame({'date': dates, 'value': range(10)})
    
    # Range numérique
    df = pd.DataFrame({'x': range(100), 'y': np.random.randn(100)})


[OK] EXPLORATION DES DONNÉES


[OK] AFFICHAGE DE BASE
    # Premières/dernières lignes
    df.head()           # 5 premières
    df.head(10)         # 10 premières
    df.tail()           # 5 dernières
    df.tail(3)          # 3 dernières
    
    # Échantillon aléatoire
    df.sample()         # 1 ligne aléatoire
    df.sample(5)        # 5 lignes aléatoires
    df.sample(frac=0.1) # 10% du dataset

[OK] INFORMATIONS GÉNÉRALES
    # Structure
    df.info()           # Types, mémoire, non-null
    df.shape            # (lignes, colonnes)
    df.columns          # Index des colonnes
    df.index            # Index des lignes
    df.dtypes           # Types de chaque colonne
    
    # Taille
    len(df)             # Nombre de lignes
    df.size             # Nombre total d'éléments
    df.ndim             # Nombre de dimensions (toujours 2)
    
    # Mémoire
    df.memory_usage()          # Usage par colonne
    df.memory_usage(deep=True) # Usage détaillé

[OK] STATISTIQUES DESCRIPTIVES
    # Toutes les colonnes numériques
    df.describe()
    
    # Inclure toutes les colonnes
    df.describe(include='all')
    
    # Statistiques spécifiques
    df['age'].mean()      # Moyenne
    df['age'].median()    # Médiane
    df['age'].mode()      # Mode
    df['age'].std()       # Écart-type
    df['age'].var()       # Variance
    df['age'].min()       # Minimum
    df['age'].max()       # Maximum
    df['age'].sum()       # Somme
    df['age'].count()     # Nombre de non-NaN
    df['age'].quantile(0.25)  # Premier quartile
    
    # Corrélation
    df.corr()             # Matrice de corrélation
    df['col1'].corr(df['col2'])  # Entre deux colonnes
    
    # Covariance
    df.cov()

[OK] VALEURS UNIQUES
    df['city'].unique()        # Array des valeurs uniques
    df['city'].nunique()       # Nombre de valeurs uniques
    df['city'].value_counts()  # Comptage par valeur
    df['city'].value_counts(normalize=True)  # Proportions

[OK] APERÇU DES DONNÉES
    # Afficher toutes les colonnes (temporairement)
    with pd.option_context('display.max_columns', None):
        print(df.head())
    
    # Afficher les valeurs min/max par colonne
    df.min()
    df.max()
    
    # Identifier les types
    df.select_dtypes(include=['int64', 'float64']).columns
    df.select_dtypes(include='object').columns


[OK] SÉLECTION ET INDEXATION


[OK] SÉLECTION DE COLONNES
    # Une colonne (retourne Series)
    df['name']
    df.name  # Si nom valide Python
    
    # Plusieurs colonnes (retourne DataFrame)
    df[['name', 'age']]
    
    # Par type
    df.select_dtypes(include=['float64', 'int64'])
    df.select_dtypes(exclude='object')

[OK] SÉLECTION PAR POSITION (iloc)
    # Lignes
    df.iloc[0]              # Première ligne
    df.iloc[-1]             # Dernière ligne
    df.iloc[0:5]            # Lignes 0 à 4
    df.iloc[[0, 2, 4]]      # Lignes spécifiques
    
    # Colonnes
    df.iloc[:, 0]           # Première colonne
    df.iloc[:, 0:3]         # 3 premières colonnes
    df.iloc[:, [0, 2]]      # Colonnes spécifiques
    
    # Lignes et colonnes
    df.iloc[0, 0]           # Cellule [0, 0]
    df.iloc[0:5, 0:3]       # Bloc de données
    df.iloc[[0, 2], [1, 3]] # Positions spécifiques

[OK] SÉLECTION PAR LABEL (loc)
    # Lignes
    df.loc[0]               # Ligne avec index 0
    df.loc[0:5]             # Lignes 0 à 5 (inclusif!)
    df.loc[[0, 2, 4]]       # Index spécifiques
    
    # Colonnes
    df.loc[:, 'name']       # Colonne 'name'
    df.loc[:, ['name', 'age']]  # Plusieurs colonnes
    
    # Lignes et colonnes
    df.loc[0, 'name']       # Valeur spécifique
    df.loc[0:5, 'name':'age']  # Plage de lignes et colonnes
    df.loc[df['age'] > 30, ['name', 'city']]  # Avec condition

[OK] SÉLECTION PAR CONDITION (BOOLEAN INDEXING)
    # Condition simple
    df[df['age'] > 30]
    df[df['city'] == 'Paris']
    df[df['name'].str.startswith('A')]
    
    # Conditions multiples (AND)
    df[(df['age'] > 30) & (df['city'] == 'Paris')]
    
    # Conditions multiples (OR)
    df[(df['age'] > 30) | (df['city'] == 'Paris')]
    
    # NOT
    df[~(df['age'] > 30)]
    
    # IN
    df[df['city'].isin(['Paris', 'London'])]
    
    # NOT IN
    df[~df['city'].isin(['Paris', 'London'])]
    
    # BETWEEN
    df[df['age'].between(25, 35)]
    
    # STRING contains
    df[df['name'].str.contains('Ali', case=False, na=False)]
    
    # NULL/NOT NULL
    df[df['age'].isnull()]
    df[df['age'].notnull()]

[OK] AT ET IAT (ACCÈS RAPIDE)
    # at: par label (plus rapide que loc)
    value = df.at[0, 'name']
    df.at[0, 'name'] = 'NewName'
    
    # iat: par position (plus rapide que iloc)
    value = df.iat[0, 0]
    df.iat[0, 0] = 'NewValue'

[OK] QUERY METHOD
    # Requêtes style SQL
    df.query('age > 30')
    df.query('age > 30 and city == "Paris"')
    df.query('age > @min_age', local_dict={'min_age': 30})
    df.query('city in ["Paris", "London"]')


[OK] MODIFICATION DES DONNÉES


[OK] AJOUTER DES COLONNES
    # Nouvelle colonne constante
    df['country'] = 'France'
    
    # Depuis calcul
    df['age_double'] = df['age'] * 2
    
    # Depuis fonction
    df['age_category'] = df['age'].apply(lambda x: 'Young' if x < 30 else 'Old')
    
    # Avec condition
    df['status'] = np.where(df['age'] > 30, 'Senior', 'Junior')
    
    # Conditions multiples
    conditions = [
        df['age'] < 20,
        (df['age'] >= 20) & (df['age'] < 40),
        df['age'] >= 40
    ]
    choices = ['Young', 'Adult', 'Senior']
    df['category'] = np.select(conditions, choices, default='Unknown')
    
    # Insérer à une position spécifique
    df.insert(1, 'new_col', [1, 2, 3])  # À l'index 1

[OK] MODIFIER DES VALEURS
    # Une cellule
    df.loc[0, 'age'] = 26
    df.at[0, 'age'] = 26
    df.iloc[0, 1] = 26
    df.iat[0, 1] = 26
    
    # Colonne entière
    df['age'] = df['age'] + 1
    
    # Avec condition
    df.loc[df['age'] > 30, 'category'] = 'Senior'
    
    # Replace
    df['city'] = df['city'].replace('Paris', 'PARIS')
    df['city'] = df['city'].replace({'Paris': 'PARIS', 'London': 'LONDON'})
    
    # Map (dict mapping)
    mapping = {'Paris': 1, 'London': 2, 'Berlin': 3}
    df['city_code'] = df['city'].map(mapping)

[OK] SUPPRIMER COLONNES/LIGNES
    # Colonnes
    df.drop('col_name', axis=1, inplace=True)
    df.drop(['col1', 'col2'], axis=1, inplace=True)
    
    # Sans inplace (retourne nouvelle copie)
    df_new = df.drop('col_name', axis=1)
    
    # Lignes par index
    df.drop(0, axis=0, inplace=True)
    df.drop([0, 1, 2], axis=0, inplace=True)
    
    # Lignes par condition
    df.drop(df[df['age'] < 18].index, inplace=True)
    
    # Supprimer doublons
    df.drop_duplicates(inplace=True)
    df.drop_duplicates(subset=['name'], keep='first', inplace=True)
    # keep: 'first', 'last', False (supprimer tous)

[OK] RENOMMER
    # Colonnes
    df.rename(columns={'old_name': 'new_name'}, inplace=True)
    df.rename(columns={'name': 'full_name', 'age': 'years'}, inplace=True)
    
    # Avec fonction
    df.rename(columns=str.upper, inplace=True)
    df.rename(columns=lambda x: x.replace(' ', '_'), inplace=True)
    
    # Index
    df.rename(index={0: 'first', 1: 'second'}, inplace=True)
    
    # Renommer toutes les colonnes
    df.columns = ['new1', 'new2', 'new3']

[OK] RÉORGANISER
    # Réordonner colonnes
    df = df[['col2', 'col1', 'col3']]
    
    # Déplacer colonne en première position
    cols = ['col_to_move'] + [c for c in df.columns if c != 'col_to_move']
    df = df[cols]


[OK] FILTRAGE AVANCÉ


[OK] FILTRES NUMÉRIQUES
    # Comparaisons
    df[df['age'] > 30]
    df[df['age'] >= 30]
    df[df['age'] < 30]
    df[df['age'] <= 30]
    df[df['age'] == 30]
    df[df['age'] != 30]
    
    # Between
    df[df['age'].between(25, 35, inclusive='both')]
    
    # Top N valeurs
    df.nlargest(10, 'age')
    df.nsmallest(10, 'age')

[OK] FILTRES STRING
    # Contains
    df[df['name'].str.contains('Ali')]
    df[df['name'].str.contains('ali', case=False)]
    df[df['name'].str.contains('Ali|Bob', regex=True)]
    
    # Startswith/Endswith
    df[df['name'].str.startswith('A')]
    df[df['name'].str.endswith('son')]
    
    # Exact match
    df[df['name'] == 'Alice']
    
    # Length
    df[df['name'].str.len() > 5]
    
    # Regex
    df[df['email'].str.match(r'^[\w\.-]+@[\w\.-]+\.\w+$')]

[OK] FILTRES MULTIPLES
    # AND
    df[(df['age'] > 25) & (df['city'] == 'Paris')]
    
    # OR
    df[(df['age'] < 20) | (df['age'] > 60)]
    
    # NOT
    df[~(df['age'] > 30)]
    
    # Complexe
    df[(df['age'] > 25) & ((df['city'] == 'Paris') | (df['city'] == 'London'))]

[OK] FILTRES SUR INDEX
    # Index dans liste
    df[df.index.isin([0, 2, 4])]
    
    # Index par nom
    df.loc[['row1', 'row2']]


[OK] TRI ET CLASSEMENT


[OK] SORT_VALUES
    # Une colonne croissant
    df.sort_values('age')
    df.sort_values('age', ascending=True)
    
    # Décroissant
    df.sort_values('age', ascending=False)
    
    # Plusieurs colonnes
    df.sort_values(['city', 'age'])
    df.sort_values(['city', 'age'], ascending=[True, False])
    
    # NaN en dernier/premier
    df.sort_values('age', na_position='last')  # 'first' ou 'last'
    
    # Inplace
    df.sort_values('age', inplace=True)

[OK] SORT_INDEX
    # Trier par index
    df.sort_index()
    df.sort_index(ascending=False)
    
    # Trier colonnes
    df.sort_index(axis=1)

[OK] RANKING
    # Rang
    df['age_rank'] = df['age'].rank()
    
    # Méthodes de tie-breaking
    df['rank'] = df['age'].rank(method='min')
    # method: 'average', 'min', 'max', 'first', 'dense'
    
    # Rang percentile
    df['percentile'] = df['age'].rank(pct=True)


[OK] AGRÉGATION ET GROUPEMENT


[OK] GROUPBY BASIQUE
    # Une colonne
    df.groupby('city').mean()
    df.groupby('city').sum()
    df.groupby('city').count()
    df.groupby('city').min()
    df.groupby('city').max()
    df.groupby('city').std()
    
    # Plusieurs colonnes
    df.groupby(['city', 'gender']).mean()
    
    # Grouper et sélectionner
    df.groupby('city')['age'].mean()
    df.groupby('city')[['age', 'salary']].mean()

[OK] AGG (AGRÉGATIONS MULTIPLES)
    # Même fonction pour toutes
    df.groupby('city').agg('mean')
    
    # Fonctions différentes par colonne
    df.groupby('city').agg({
        'age': 'mean',
        'salary': 'sum',
        'score': ['min', 'max', 'std']
    })
    
    # Avec fonctions personnalisées
    df.groupby('city').agg({
        'age': ['mean', 'median', lambda x: x.max() - x.min()]
    })
    
    # Renommer les colonnes
    df.groupby('city').agg(
        avg_age=('age', 'mean'),
        total_salary=('salary', 'sum'),
        count=('age', 'count')
    )

[OK] TRANSFORM
    # Appliquer fonction et garder la forme originale
    df['age_mean_by_city'] = df.groupby('city')['age'].transform('mean')
    df['age_normalized'] = df.groupby('city')['age'].transform(
        lambda x: (x - x.mean()) / x.std()
    )

[OK] APPLY
    # Fonction personnalisée sur chaque groupe
    def range_calc(group):
        return group.max() - group.min()
    
    df.groupby('city')['age'].apply(range_calc)
    
    # Retourner DataFrame
    def top_2(group):
        return group.nlargest(2, 'age')
    
    df.groupby('city').apply(top_2)

[OK] FILTER
    # Garder groupes qui satisfont condition
    df.groupby('city').filter(lambda x: len(x) > 10)
    df.groupby('city').filter(lambda x: x['age'].mean() > 30)

[OK] SIZE ET COUNT
    # Taille des groupes
    df.groupby('city').size()
    
    # Count (exclut NaN)
    df.groupby('city').count()
    
    # Avec nom
    df.groupby('city').size().reset_index(name='count')

[OK] PIVOT TABLE
    # Tableau croisé
    pd.pivot_table(df,
                   values='salary',
                   index='city',
                   columns='gender',
                   aggfunc='mean')
    
    # Plusieurs agrégations
    pd.pivot_table(df,
                   values='salary',
                   index='city',
                   columns='gender',
                   aggfunc=['mean', 'sum', 'count'],
                   fill_value=0)

[OK] CROSSTAB
    # Tableau de fréquences
    pd.crosstab(df['city'], df['gender'])
    
    # Avec marges (totaux)
    pd.crosstab(df['city'], df['gender'], margins=True)
    
    # Avec normalisation
    pd.crosstab(df['city'], df['gender'], normalize=True)


[OK] FUSION ET JOINTURE


[OK] CONCAT
    # Vertical (empiler)
    pd.concat([df1, df2])
    pd.concat([df1, df2], ignore_index=True)
    
    # Horizontal (côte à côte)
    pd.concat([df1, df2], axis=1)
    
    # Avec keys
    pd.concat([df1, df2], keys=['first', 'second'])

[OK] MERGE (JOIN SQL-LIKE)
    # Inner join (défaut)
    pd.merge(df1, df2, on='id')
    
    # Left join
    pd.merge(df1, df2, on='id', how='left')
    
    # Right join
    pd.merge(df1, df2, on='id', how='right')
    
    # Outer join (full)
    pd.merge(df1, df2, on='id', how='outer')
    
    # Plusieurs colonnes
    pd.merge(df1, df2, on=['id', 'date'])
    
    # Colonnes différentes
    pd.merge(df1, df2, left_on='id1', right_on='id2')
    
    # Sur index
    pd.merge(df1, df2, left_index=True, right_index=True)
    
    # Avec suffixes pour colonnes communes
    pd.merge(df1, df2, on='id', suffixes=('_left', '_right'))

[OK] JOIN
    # Join sur index
    df1.join(df2)
    df1.join(df2, how='left')
    df1.join(df2, on='key')

[OK] APPEND (DÉPRÉCIÉ, UTILISER CONCAT)
    # Ajouter lignes
    df = pd.concat([df, new_rows], ignore_index=True)


[OK] VALEURS MANQUANTES


[OK] DÉTECTION
    # Détecter NaN
    df.isnull()
    df.isna()  # Alias
    
    # Détecter non-NaN
    df.notnull()
    df.notna()  # Alias
    
    # Compter NaN par colonne
    df.isnull().sum()
    
    # Compter NaN total
    df.isnull().sum().sum()
    
    # Pourcentage de NaN
    (df.isnull().sum() / len(df)) * 100
    
    # Lignes avec au moins un NaN
    df[df.isnull().any(axis=1)]
    
    # Colonnes avec au moins un NaN
    df.columns[df.isnull().any()]

[OK] SUPPRIMER
    # Supprimer lignes avec NaN
    df.dropna()
    
    # Supprimer lignes où TOUTES les valeurs sont NaN
    df.dropna(how='all')
    
    # Supprimer si NaN dans colonnes spécifiques
    df.dropna(subset=['age', 'city'])
    
    # Supprimer colonnes avec NaN
    df.dropna(axis=1)
    
    # Seuil: garder si au moins N non-NaN
    df.dropna(thresh=5)  # Au moins 5 valeurs non-NaN

[OK] REMPLIR
    # Valeur constante
    df.fillna(0)
    df.fillna('Unknown')
    
    # Dictionnaire par colonne
    df.fillna({'age': 0, 'city': 'Unknown'})
    
    # Forward fill (propager dernière valeur valide)
    df.fillna(method='ffill')
    df.fillna(method='pad')  # Alias
    
    # Backward fill (propager prochaine valeur valide)
    df.fillna(method='bfill')
    df.fillna(method='backfill')  # Alias
    
    # Limite de propagation
    df.fillna(method='ffill', limit=2)
    
    # Statistiques
    df['age'].fillna(df['age'].mean())
    df['age'].fillna(df['age'].median())
    df['age'].fillna(df['age'].mode()[0])
    
    # Par groupe
    df['age'] = df.groupby('city')['age'].transform(
        lambda x: x.fillna(x.mean())
    )

[OK] INTERPOLER
    # Interpolation linéaire
    df['age'].interpolate()
    
    # Méthodes
    df['age'].interpolate(method='linear')
    df['age'].interpolate(method='polynomial', order=2)
    df['age'].interpolate(method='spline', order=3)

[OK] REMPLACER
    # Valeurs spécifiques
    df.replace(0, np.nan)
    df.replace([0, -1], np.nan)
    df.replace({0: np.nan, -1: np.nan})
    
    # Par colonne
    df.replace({'age': {0: np.nan}})


[OK] APPLY ET FONCTIONS


[OK] APPLY SUR COLONNE (Series)
    # Fonction lambda
    df['age_double'] = df['age'].apply(lambda x: x * 2)
    
    # Fonction nommée
    def categorize_age(age):
        if age < 18:
            return 'Minor'
        elif age < 65:
            return 'Adult'
        else:
            return 'Senior'
    
    df['category'] = df['age'].apply(categorize_age)
    
    # Avec arguments
    df['adjusted'] = df['age'].apply(lambda x: x + 10 if x > 30 else x)

[OK] APPLY SUR DATAFRAME
    # Sur chaque colonne (axis=0, défaut)
    df.apply(lambda x: x.max() - x.min())
    
    # Sur chaque ligne (axis=1)
    df['total'] = df.apply(lambda row: row['col1'] + row['col2'], axis=1)
    
    # Retourner Series
    df.apply(lambda row: row['age'] * 2 if row['city'] == 'Paris' else row['age'], axis=1)

[OK] APPLYMAP (ÉLÉMENT PAR ÉLÉMENT)
    # Sur tout le DataFrame
    df.applymap(lambda x: x * 2)
    df.applymap(str.upper)  # Pour strings
    
    # Note: Déprécié, utiliser map() sur Series

[OK] MAP (SERIES UNIQUEMENT)
    # Dictionnaire de mapping
    mapping = {'Paris': 'FR', 'London': 'UK', 'Berlin': 'DE'}
    df['country_code'] = df['city'].map(mapping)
    
    # Fonction
    df['city_upper'] = df['city'].map(str.upper)
    df['city_upper'] = df['city'].map(lambda x: x.upper())

[OK] PIPE
    # Chaîner opérations
    def remove_outliers(df):
        return df[df['age'] < 100]
    
    def normalize(df):
        df['age'] = (df['age'] - df['age'].mean()) / df['age'].std()
        return df
    
    result = (df.pipe(remove_outliers)
               .pipe(normalize))

[OK] VECTORISATION (PLUS RAPIDE)
    # [X] Lent avec apply
    df['result'] = df['age'].apply(lambda x: x * 2)
    
    # [OK] Rapide avec opérations vectorisées
    df['result'] = df['age'] * 2
    
    # Avec numpy
    import numpy as np
    df['result'] = np.sqrt(df['age'])


[OK] MANIPULATION DE DATES


[OK] CRÉER DES DATES
    # Parser string en datetime
    df['date'] = pd.to_datetime(df['date_string'])
    df['date'] = pd.to_datetime(df['date_string'], format='%Y-%m-%d')
    
    # Depuis composants
    df['date'] = pd.to_datetime(df[['year', 'month', 'day']])
    
    # Date actuelle
    pd.Timestamp.now()
    pd.Timestamp.today()

[OK] EXTRAIRE COMPOSANTS
    # Année, mois, jour
    df['year'] = df['date'].dt.year
    df['month'] = df['date'].dt.month
    df['day'] = df['date'].dt.day
    
    # Jour de la semaine
    df['dayofweek'] = df['date'].dt.dayofweek  # 0=Lundi
    df['day_name'] = df['date'].dt.day_name()   # Monday, Tuesday...
    
    # Semaine, trimestre
    df['week'] = df['date'].dt.isocalendar().week
    df['quarter'] = df['date'].dt.quarter
    
    # Heure, minute, seconde
    df['hour'] = df['date'].dt.hour
    df['minute'] = df['date'].dt.minute
    df['second'] = df['date'].dt.second

[OK] OPÉRATIONS SUR DATES
    # Différence
    df['days_diff'] = (df['date2'] - df['date1']).dt.days
    
    # Ajouter/soustraire
    df['next_week'] = df['date'] + pd.Timedelta(days=7)
    df['last_month'] = df['date'] - pd.DateOffset(months=1)
    
    # Arrondir
    df['date_floor'] = df['date'].dt.floor('D')  # Début du jour
    df['date_ceil'] = df['date'].dt.ceil('H')    # Fin de l'heure

[OK] FILTRER PAR DATE
    # Après une date
    df[df['date'] > '2024-01-01']
    
    # Entre deux dates
    df[(df['date'] >= '2024-01-01') & (df['date'] <= '2024-12-31')]
    df[df['date'].between('2024-01-01', '2024-12-31')]
    
    # Année spécifique
    df[df['date'].dt.year == 2024]
    
    # Mois spécifique
    df[df['date'].dt.month == 6]

[OK] RESAMPLE (SÉRIES TEMPORELLES)
    # Définir date comme index
    df.set_index('date', inplace=True)
    
    # Agréger par jour
    df.resample('D').sum()
    
    # Par semaine
    df.resample('W').mean()
    
    # Par mois
    df.resample('M').mean()
    
    # Fréquences: 'D' (jour), 'W' (semaine), 'M' (mois), 'Q' (trimestre), 'Y' (année)

[OK] ROLLING WINDOWS
    # Moyenne mobile sur 7 jours
    df['rolling_mean'] = df['value'].rolling(window=7).mean()
    
    # Autres statistiques
    df['rolling_sum'] = df['value'].rolling(window=7).sum()
    df['rolling_std'] = df['value'].rolling(window=7).std()
    
    # Avec centre
    df['centered'] = df['value'].rolling(window=7, center=True).mean()


[OK] MANIPULATION DE STRINGS


[OK] MÉTHODES STRING DE BASE
    # Majuscules/minuscules
    df['name'].str.upper()
    df['name'].str.lower()
    df['name'].str.title()
    df['name'].str.capitalize()
    
    # Strip whitespace
    df['name'].str.strip()
    df['name'].str.lstrip()
    df['name'].str.rstrip()
    
    # Replace
    df['name'].str.replace('old', 'new')
    df['name'].str.replace('[0-9]', '', regex=True)

[OK] RECHERCHE ET FILTRAGE
    # Contains
    df[df['name'].str.contains('Alice')]
    df[df['name'].str.contains('alice', case=False)]
    df[df['name'].str.contains('Alice|Bob', regex=True)]
    
    # Startswith/Endswith
    df[df['name'].str.startswith('A')]
    df[df['name'].str.endswith('son')]
    
    # Match (regex complet)
    df[df['email'].str.match(r'^[\w\.-]+@[\w\.-]+\.\w+)]

[OK] EXTRACTION
    # Extract avec regex
    df['area_code'] = df['phone'].str.extract(r'(\d{3})')
    
    # Extract all
    df['numbers'] = df['text'].str.extractall(r'(\d+)')
    
    # Slice
    df['first_3'] = df['name'].str[:3]
    df['last_char'] = df['name'].str[-1]

[OK] SPLIT
    # Split
    df['name'].str.split(' ')
    
    # Expand en colonnes
    df[['first_name', 'last_name']] = df['name'].str.split(' ', expand=True)
    
    # Get élément
    df['first_name'] = df['name'].str.split(' ').str[0]

[OK] JOINTURE ET CONCAT
    # Join
    df['full_name'] = df['first_name'].str.cat(df['last_name'], sep=' ')
    
    # Concat multiple
    df['address'] = df['street'].str.cat([df['city'], df['zip']], sep=', ')

[OK] AUTRES OPÉRATIONS
    # Length
    df['name_length'] = df['name'].str.len()
    
    # Pad
    df['padded'] = df['id'].str.pad(width=5, side='left', fillchar='0')
    
    # Repeat
    df['repeated'] = df['char'].str.repeat(3)
    
    # Count occurrences
    df['vowel_count'] = df['text'].str.count(r'[aeiou]')


[OK] RESHAPE ET PIVOT


[OK] PIVOT
    # Wide to long
    df_pivot = df.pivot(index='date', columns='city', values='value')
    
    # Avec plusieurs valeurs
    df_pivot = df.pivot(index='date', columns='city', values=['sales', 'profit'])

[OK] PIVOT_TABLE (AVEC AGRÉGATION)
    # Basique
    pd.pivot_table(df, values='value', index='date', columns='city', aggfunc='mean')
    
    # Multiples agrégations
    pd.pivot_table(df,
                   values='value',
                   index='date',
                   columns='city',
                   aggfunc=['mean', 'sum', 'count'])

[OK] MELT (WIDE TO LONG)
    # Basique
    df_long = pd.melt(df, id_vars=['id'], value_vars=['col1', 'col2'])
    
    # Avec noms personnalisés
    df_long = pd.melt(df,
                      id_vars=['id'],
                      value_vars=['col1', 'col2'],
                      var_name='variable',
                      value_name='value')

[OK] STACK ET UNSTACK
    # Stack (colonnes -> index)
    df_stacked = df.stack()
    
    # Unstack (index -> colonnes)
    df_unstacked = df.unstack()
    
    # Spécifier niveau
    df.unstack(level=0)
    df.unstack(level='column_name')

[OK] TRANSPOSE
    # Inverser lignes et colonnes
    df_transposed = df.T
    df_transposed = df.transpose()


[OK] EXPORT DE DONNÉES


[OK] CSV
    # Basique
    df.to_csv('output.csv')
    
    # Sans index
    df.to_csv('output.csv', index=False)
    
    # Avec options
    df.to_csv('output.csv',
              sep=';',
              encoding='utf-8',
              index=False,
              header=True,
              columns=['col1', 'col2'],  # Colonnes spécifiques
              na_rep='NULL',             # Représentation des NaN
              float_format='%.2f')       # Format des floats

[OK] EXCEL
    # Une feuille
    df.to_excel('output.xlsx', index=False)
    
    # Plusieurs feuilles
    with pd.ExcelWriter('output.xlsx') as writer:
        df1.to_excel(writer, sheet_name='Sheet1', index=False)
        df2.to_excel(writer, sheet_name='Sheet2', index=False)
    
    # Avec formatage
    with pd.ExcelWriter('output.xlsx', engine='xlsxwriter') as writer:
        df.to_excel(writer, sheet_name='Data', index=False)
        workbook = writer.book
        worksheet = writer.sheets['Data']
        format1 = workbook.add_format({'num_format': '#,##0.00'})
        worksheet.set_column('A:A', 18, format1)

[OK] JSON
    # Records (liste de dicts)
    df.to_json('output.json', orient='records')
    
    # Autres orientations
    df.to_json('output.json', orient='split')    # Dict avec index, colonnes, data
    df.to_json('output.json', orient='index')    # Dict avec index comme clés
    df.to_json('output.json', orient='columns')  # Dict avec colonnes comme clés
    df.to_json('output.json', orient='values')   # Seulement valeurs
    
    # Avec indentation
    df.to_json('output.json', orient='records', indent=2)

[OK] SQL
    from sqlalchemy import create_engine
    
    engine = create_engine('sqlite:///database.db')
    
    # Créer/remplacer table
    df.to_sql('table_name', engine, if_exists='replace', index=False)
    
    # Append
    df.to_sql('table_name', engine, if_exists='append', index=False)
    
    # if_exists: 'fail', 'replace', 'append'

[OK] HTML
    # Table HTML
    html = df.to_html()
    
    # Sans index
    html = df.to_html(index=False)
    
    # Avec classes CSS
    html = df.to_html(classes='table table-striped')

[OK] AUTRES FORMATS
    # Parquet (efficace pour gros datasets)
    df.to_parquet('output.parquet')
    
    # Pickle (format pandas natif)
    df.to_pickle('output.pkl')
    
    # Clipboard (coller dans Excel)
    df.to_clipboard(index=False)
    
    # LaTeX
    df.to_latex('output.tex')
    
    # Markdown
    df.to_markdown()


[OK] INDEX ET MULTIINDEX


[OK] DÉFINIR INDEX
    # Colonne comme index
    df.set_index('id', inplace=True)
    
    # Plusieurs colonnes (MultiIndex)
    df.set_index(['city', 'date'], inplace=True)
    
    # Sans supprimer la colonne
    df.set_index('id', drop=False, inplace=True)

[OK] RÉINITIALISER INDEX
    # Reset à index numérique
    df.reset_index(inplace=True)
    
    # Sans créer colonne d'index
    df.reset_index(drop=True, inplace=True)

[OK] MULTIINDEX
    # Créer MultiIndex
    arrays = [
        ['A', 'A', 'B', 'B'],
        [1, 2, 1, 2]
    ]
    index = pd.MultiIndex.from_arrays(arrays, names=['letter', 'number'])
    df = pd.DataFrame({'value': [10, 20, 30, 40]}, index=index)
    
    # Sélection MultiIndex
    df.loc['A']              # Toutes les lignes où letter='A'
    df.loc[('A', 1)]         # letter='A' et number=1
    df.xs('A', level='letter')  # Cross-section
    
    # Swap levels
    df.swaplevel('letter', 'number')
    
    # Sort MultiIndex
    df.sort_index(level=['letter', 'number'])

[OK] OPÉRATIONS SUR INDEX
    # Renommer index
    df.index = df.index.str.upper()
    df.rename(index={0: 'first', 1: 'second'}, inplace=True)
    
    # Vérifier existence
    'id' in df.index
    
    # Obtenir position
    df.index.get_loc('id')


[OK] WINDOW FUNCTIONS


[OK] ROLLING (FENÊTRE GLISSANTE)
    # Moyenne mobile
    df['rolling_mean'] = df['value'].rolling(window=7).mean()
    
    # Autres stats
    df['rolling_sum'] = df['value'].rolling(window=7).sum()
    df['rolling_std'] = df['value'].rolling(window=7).std()
    df['rolling_min'] = df['value'].rolling(window=7).min()
    df['rolling_max'] = df['value'].rolling(window=7).max()
    
    # Fenêtre centrée
    df['centered'] = df['value'].rolling(window=7, center=True).mean()
    
    # Fenêtre avec min_periods
    df['rolling'] = df['value'].rolling(window=7, min_periods=3).mean()

[OK] EXPANDING (FENÊTRE CUMULATIVE)
    # Cumul depuis le début
    df['expanding_mean'] = df['value'].expanding().mean()
    df['expanding_sum'] = df['value'].expanding().sum()

[OK] SHIFT
    # Décalage de lignes
    df['prev_value'] = df['value'].shift(1)    # Précédent
    df['next_value'] = df['value'].shift(-1)   # Suivant
    
    # Calculer différences
    df['diff'] = df['value'] - df['value'].shift(1)
    df['pct_change'] = df['value'].pct_change()

[OK] DIFF
    # Différence avec ligne précédente
    df['diff'] = df['value'].diff()
    df['diff_2'] = df['value'].diff(2)  # Avec 2 lignes avant

[OK] CUMSUM, CUMPROD
    # Somme cumulative
    df['cumsum'] = df['value'].cumsum()
    
    # Produit cumulatif
    df['cumprod'] = df['value'].cumprod()
    
    # Min/Max cumulatif
    df['cummin'] = df['value'].cummin()
    df['cummax'] = df['value'].cummax()


[OK] STYLE ET FORMATAGE


[OK] STYLE BASIQUE
    # Mettre en évidence
    df.style.highlight_max(axis=0)  # Max par colonne
    df.style.highlight_min(axis=1)  # Min par ligne
    
    # Background gradient
    df.style.background_gradient(cmap='viridis')
    
    # Bar
    df.style.bar(subset=['col1', 'col2'], color='lightblue')

[OK] FORMATAGE DES NOMBRES
    # Format spécifique
    df.style.format({'col1': '{:.2f}', 'col2': '{:,.0f}'})
    
    # Pourcentages
    df.style.format({'percentage': '{:.2%}'})
    
    # Devise
    df.style.format({'price': '${:,.2f}'})

[OK] STYLE CONDITIONNEL
    def color_negative_red(val):
        color = 'red' if val < 0 else 'black'
        return f'color: {color}'
    
    df.style.applymap(color_negative_red, subset=['col1'])
    
    # Sur lignes
    def highlight_max_row(row):
        return ['background-color: yellow' if v == row.max() else '' for v in row]
    
    df.style.apply(highlight_max_row, axis=1)

[OK] EXPORTER STYLE
    # Vers HTML
    styled = df.style.background_gradient(cmap='viridis')
    styled.to_html('styled_table.html')
    
    # Vers Excel (nécessite openpyxl)
    styled.to_excel('styled_table.xlsx', engine='openpyxl')


[OK] OPTIMISATION ET PERFORMANCE


[OK] TYPES DE DONNÉES
    # Réduire usage mémoire
    df['int_col'] = df['int_col'].astype('int32')  # Au lieu de int64
    df['float_col'] = df['float_col'].astype('float32')
    
    # Categorical pour répétitions
    df['category'] = df['category'].astype('category')
    
    # Vérifier économie
    df.memory_usage(deep=True)

[OK] CHUNKING (GROS FICHIERS)
    # Lire par chunks
    chunk_size = 10000
    chunks = []
    
    for chunk in pd.read_csv('large_file.csv', chunksize=chunk_size):
        # Traiter chunk
        processed = chunk[chunk['age'] > 30]
        chunks.append(processed)
    
    df = pd.concat(chunks, ignore_index=True)

[OK] QUERY AU LIEU DE BOOLEAN INDEXING
    # [OK] Plus rapide
    df.query('age > 30 and city == "Paris"')
    
    # [X] Plus lent
    df[(df['age'] > 30) & (df['city'] == 'Paris')]

[OK] ÉVITER APPLY QUAND POSSIBLE
    # [X] Lent
    df['result'] = df['age'].apply(lambda x: x * 2)
    
    # [OK] Rapide (vectorisé)
    df['result'] = df['age'] * 2

[OK] INPLACE OPERATIONS
    # Économise mémoire
    df.drop('col', axis=1, inplace=True)
    df.fillna(0, inplace=True)
    df.sort_values('age', inplace=True)

[OK] COPY VS VIEW
    # View (référence)
    subset = df[['col1', 'col2']]  # Peut être une view
    
    # Copy explicite
    subset = df[['col1', 'col2']].copy()
    
    # Forcer copy pour éviter SettingWithCopyWarning
    df_copy = df.copy()


[OK] ANALYSE AVANCÉE


[OK] CORRÉLATION ET COVARIANCE
    # Matrice de corrélation
    df.corr()
    df.corr(method='pearson')  # 'pearson', 'kendall', 'spearman'
    
    # Entre deux colonnes
    df['col1'].corr(df['col2'])
    
    # Covariance
    df.cov()

[OK] QUANTILES ET PERCENTILES
    # Quantiles
    df['age'].quantile(0.25)  # Premier quartile
    df['age'].quantile([0.25, 0.5, 0.75])  # Quartiles
    
    # Binning
    df['age_group'] = pd.cut(df['age'], bins=5)
    df['age_group'] = pd.cut(df['age'], bins=[0, 18, 30, 60, 100],
                             labels=['Minor', 'Young', 'Adult', 'Senior'])
    
    # Qcut (quantile-based)
    df['age_quartile'] = pd.qcut(df['age'], q=4)

[OK] OUTLIERS
    # IQR method
    Q1 = df['age'].quantile(0.25)
    Q3 = df['age'].quantile(0.75)
    IQR = Q3 - Q1
    
    # Définir outliers
    outliers = df[(df['age'] < Q1 - 1.5*IQR) | (df['age'] > Q3 + 1.5*IQR)]
    
    # Supprimer outliers
    df_clean = df[(df['age'] >= Q1 - 1.5*IQR) & (df['age'] <= Q3 + 1.5*IQR)]

[OK] NORMALISATION
    # Min-Max scaling
    df['normalized'] = (df['age'] - df['age'].min()) / (df['age'].max() - df['age'].min())
    
    # Z-score standardization
    df['standardized'] = (df['age'] - df['age'].mean()) / df['age'].std()

[OK] SAMPLING
    # Échantillon aléatoire
    sample = df.sample(n=100)
    sample = df.sample(frac=0.1)  # 10%
    
    # Avec seed pour reproductibilité
    sample = df.sample(n=100, random_state=42)
    
    # Sample stratifié
    sample = df.groupby('category', group_keys=False).apply(lambda x: x.sample(frac=0.1))


[OK] ERREURS COURANTES ET SOLUTIONS


"""
[X] SETTINGWITHCOPYWARNING
    df[df['age'] > 30]['city'] = 'Paris'  # [X] Warning

[OK] SOLUTION:
    df.loc[df['age'] > 30, 'city'] = 'Paris'

[X] CHAINED INDEXING
    df['col1']['row1'] = value  # [X] Peut ne pas fonctionner

[OK] SOLUTION:
    df.loc['row1', 'col1'] = value

[X] INPLACE SANS ASSIGNATION
    df.drop('col', axis=1, inplace=True)
    # [OK] OK si inplace=True
    
    new_df = df.drop('col', axis=1)
    # [OK] OK si assignation

[X] ITÉRER SUR DATAFRAME
    for index, row in df.iterrows():  # [X] Très lent
        df.loc[index, 'new'] = row['old'] * 2

[OK] SOLUTION:
    df['new'] = df['old'] * 2  # Vectorisé

[X] APPEND DANS BOUCLE
    df = pd.DataFrame()
    for i in range(1000):
        df = df.append({'col': i}, ignore_index=True)  # [X] Lent

[OK] SOLUTION:
    data = [{'col': i} for i in range(1000)]
    df = pd.DataFrame(data)

[X] NE PAS GÉRER LES NaN
    df['result'] = df['col1'] / df['col2']  # [X] Peut créer inf/NaN

[OK] SOLUTION:
    df['result'] = df['col1'].div(df['col2']).fillna(0)
"""


[OK] BONNES PRATIQUES


"""
1. TOUJOURS VÉRIFIER LES DONNÉES
   - df.info() pour voir types et NaN
   - df.describe() pour statistiques
   - df.head() pour aperçu

2. GÉRER LES NaN EXPLICITEMENT
   - Décider: supprimer, remplir ou laisser
   - fillna(), dropna() selon contexte

3. UTILISER DES TYPES APPROPRIÉS
   - category pour variables catégorielles
   - int32/float32 au lieu de 64 si possible
   - datetime64 pour dates

4. ÉVITER LES BOUCLES
   - Préférer opérations vectorisées
   - groupby, apply si nécessaire
   - Pas de iterrows() si possible

5. COPY VS VIEW
   - Utiliser .copy() pour éviter effets de bord
   - Attention au SettingWithCopyWarning

6. NOMMER CLAIREMENT
   - Colonnes explicites
   - Index significatif si utile

7. VALIDER LES RÉSULTATS
   - Vérifier shape après transformations
   - assert pour tests
   - Samples pour vérification manuelle

8. OPTIMISER LA MÉMOIRE
   - Chunking pour gros fichiers
   - Types efficaces
   - dropna() avant traitement

9. DOCUMENTER LES TRANSFORMATIONS
   - Commentaires pour logique complexe
   - Pipeline clair et lisible

10. SAUVEGARDER INTERMÉDIAIRE
    - Checkpoints pour longs traitements
    - Pickle ou Parquet pour rapidité
"""


[OK] RESSOURCES


"""
Documentation officielle:
https://pandas.pydata.org/docs/

User Guide:
https://pandas.pydata.org/docs/user_guide/index.html

API Reference:
https://pandas.pydata.org/docs/reference/index.html

10 minutes to pandas:
https://pandas.pydata.org/docs/user_guide/10min.html

Cheat Sheet officiel:
https://pandas.pydata.org/Pandas_Cheat_Sheet.pdf

Community tutorials:
https://pandas.pydata.org/community/ecosystem.html
"""


# FIN DU CHEATSHEET PANDAS COMPLET
