# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3.7.13 ('py37-dsup')
#     language: python
#     name: python3
# ---

# # Pandas - Part 2
#
# မင်္ဂလာပါ။ Data Science using Python - Week 9 - Day 2 - Pandas - Part 2 မှ ကြိုဆိုပါတယ်။ 
#
# ဒီကနေ့ အစီအစဉ်ကတော့ ... 
#
# * SQL via `pandas` 
# * Data Types and Date/Time in `pandas`
# * Missing Data Handling
# * `groupby`
# * `DataFrame` manipulation 
#
# တို့ပဲ ဖြစ်ပါတယ်။

# ## SQL via `pandas`

# !wget https://www.sqlitetutorial.net/wp-content/uploads/2018/03/chinook.zip
# !unzip chinook.zip

import numpy as np
import pandas as pd
import sqlalchemy as sa

# format of sqlite path are: 
# sqlite:// + /relative/path/from/current/directory.db
# sqlite:/// + /absolute/path/to/data.db
# sqlite:// + <nothing for in memory db>
engine = sa.create_engine("sqlite:///chinook.db")
with engine.connect() as con:
  df = pd.read_sql("SELECT * FROM INVOICES;", con=con)
  print(df.head())

# ## A Crash Course to SQLite
#
# **SQL** or Structured Query Language ဆိုတာ C/C++ လိုပဲ ISO standard တခုဖြစ်ပြီး Relational Database တွေကို Query လုပ်ဖို့ တီထွင်ထားတဲ့ Language ဖြစ်တယ်။ Database internal ကလီစာတွေဖြစ်တဲ့ Cache size, loop, query optimization တွေ သိစရာမလိုပဲ ကိုယ်လိုချင်တာ ကိုယ်ပြောရတဲ့ Language မို့လို့ Declarative Language တမျိုးလဲ ဖြစ်တယ်။ Microsoft, IBM နဲ့ Oracle တို့လို commercial database vendor တွေတင်မကပဲ MySQL နဲ့ PostgreSQL တို့လိုမျိုး non-commercial database တွေကပါ လိုက်နာကြရတဲ့ စံနှုန်းဖြစ်တယ်။
#
# > တကယ်တမ်းမှာတော့ PostgreSQL ကလွဲလို့ ISO စံအတိုင်း လိုက်နာတဲ့ RDBMS က ရှားပါးပါတယ်။ 
#
# > relational database မဟုတ်တဲ့ mongoDb, HBase နဲ့ Cassandra လိုဟာမျိုးတွေကို NoSQL Database လို့ ခေါ်ကြတယ်။
#
# SQLite ဆိုတာ Python နဲ့ အတူ built-in တွဲပါတဲ့ relational database တခုဖြစ်ပြီး testing အတွက် အသုံးကျတယ်။ 
#
# **chinook** ဆိုတာက Microsoft Access နဲ့ အတူတွဲပါတဲ့ tradewind လိုပဲ နမူနာ စမ်းလို့ရအောင် လုပ်ထားတဲ့ Dataset တခု ဖြစ်တယ်။ သူ့ရဲ့ Entity Relationship Diagram ကို အောက်မှာ တွေ့နိုင်တယ်။ 
#
# ![](https://www.sqlitetutorial.net/wp-content/uploads/2015/11/sqlite-sample-database-color.jpg)

# `pandas` ကနေ Relational Database တွေကို (`sqlalchemy` ကတဆင့်) access လုပ်ရတာ အင်မတန် လွယ်ပါတယ်။ စုစုပေါင်း program code 3 ကြောင်းပဲ လိုပါတယ်။
#
# ```python
# # line 1 : create_engine
# engine = sa.create_engine("sqlite:///chinook.db")
# # line 2 : connect to db
# with engine.connect() as con:
#   # line 3 : read_sql
#   df = pd.read_sql("SELECT * FROM INVOICES;", con=con)
#   print(df.head())
# ```
#
# 1. ပထမဆုံး line 1 အနေနဲ့ `sqlalchemy` ရဲ့ `create_engine` ကို ခေါ်ရပါမယ်။ ဒီစာကြောင်းက `sqlalchemy` library ကို ကိုယ်ချိတ်မဲ့ database အကြောင်း အသိပေးလိုက်တာပဲ ဖြစ်ပြီး ဘာချိတ်ဆက်မှုမှ မလုပ်ရသေးပါဘူး။ password မှားတာ၊ firewall ကာထားတာ၊ whitelist/blacklist မရှင်းရသေးတာတွေကြောင့် ဖြစ်တဲ့ error တွေ ဒီတဆင့်မှာ **မပေါ်ပါဘူး**။ ဒါကို debug လုပ်တဲ့အခါ သတိထားပါ။ အကျွမ်းတဝင်မရှိတဲ့သူတွေက `create_engine` ok ရင် database နဲ့ ချိတ်လို့ ရနေပြီလို့ ထင်တတ်ပါတယ်။
#
# `create_engine` ရဲ့ url format က 
#
# > `dialect+driver://username:password@host:port/database`
#
# ဖြစ်ပါတယ်။
#
# အခု ပြထားတာက postgresql ရဲ့ url နမူနာတွေဖြစ်ပါတယ်။
#
# ```python
# # default
# engine = create_engine('postgresql://scott:tiger@localhost/mydatabase')
#
# # psycopg2
# engine = create_engine('postgresql+psycopg2://scott:tiger@localhost/mydatabase')
#
# # pg8000
# engine = create_engine('postgresql+pg8000://scott:tiger@localhost/mydatabase')
# ```
#
# အခြား database url နမူနာတွေနဲ့ နောက်ဆုံး support လုပ်တဲ့ database အမျိုးအစားများကို [ဒီလင့်ခ်မှာ](https://docs.sqlalchemy.org/en/14/core/engines.html#supported-databases) ကြည့်နိုင်ပါတယ်။
#
# 2. ဒုတိယ line 2 မှာ database ကို `connect` လုပ်ရမှာ ဖြစ်ပါတယ်။ ဒီ `connect` function က connection object return ပြန်ပေးပါတယ်။ အဲဒီ connection ဟာ `closeable` ဖြစ်ပြီး **မမေ့မလျော့** ပြန်ပိတ်ဖို့လိုပါတယ်။ အားလုံး သိကြတဲ့အတိုင်း ပြန်ပိတ်ဖို့ မမေ့အောင် `with` ဆိုပြီး context manager နဲ့ ဖြေရှင်းနိုင်ပါတယ်။
#
# 3. တတိယ line 3 မှာ `pd.read_sql` ဆိုပြီး data ကို read ရမှာ ဖြစ်ပါတယ်။ ဒီနေရာမှာ ပထမ positional argument က sql statement ဖြစ်ပါတယ်။ ပုံမှန်အားဖြင့် ဒီလို ရေးရပါတယ်။
#
# ```sql
# SELECT col_1 AS alias_1, col_2 AS alias_2, ...
# FROM database_object_name
# WHERE predicate_1 AND predicate_2
# OR predicate_3 AND predicate_4;
# ```
#
# database_object_name ဟာ table သို့မဟုတ် view ဖြစ်နိုင်ပါတယ်။
# predicate ဆိုတာ boolean condition မျိုးတွေပါပဲ။
#
# ဒီ course က SQL သင်တန်းမဟုတ်လို့ အကျယ်မပြောတော့ပါ။
#
# ဒုတိယ `con` ဆိုတဲ့ argument မှာ ဖွင့်ထားတဲ့ connection object ကို ပေးရပါမယ်။

# ### Creating DataFrame in *ad hoc* manner
#
# `pandas` မှာ DataFrame တခုကို ဒီလိုဖန်တီးနိုင်ပါတယ်။ 
#
# ```python
# df = pd.DataFrame(data={"col_a" : [1, 2, 3], "col_b" : ["a", "b", "c"]})
# ```

# test create one


# နောက်ထပ်ပုံစံတမျိုးအနေနဲ့ `numpy` array သို့မဟုတ် `list` of `list` နဲ့လဲ ဖန်တီးနိုင်ပါတယ်။
#
# ```python
# df = pd.DataFrame(data=np.array([[1, "a"], [2, "b"], [3, "c"]]), columns=["col_a", "col_b"])
# # or 
# df = pd.DataFrame(data=[[1, "a"], [2, "b"], [3, "c"]], columns=["col_a", "col_b"])
# ```

# +
# test it out
# -

# **Database သို့ Data ပြန်ရေးခြင်း**
#
# ဖတ်တုန်းကလိုပဲ ရေးဖို့အတွက် 3 line of code ပဲ လိုပါတယ်။ 
#
# ```python
# engine = sa.create_engine("sqlite:///chinook.db")
# with engine.connect() as con:
#     df.to_sql("my_table", con=con, index=False, if_exists="fail")
# ```
#
# ကွာခြားသွားတာက တတိယ line 3 ပါပဲ။ ပထမဆုံး positional argument က table_name ကို ပေးရပါတယ်။ `con` argument က တူတူပဲ ဖြစ်ပြီး `index` ဆိုတာကတော့ index တွေကိုပါ ထည့်မရေးစေချင်ရင် `False` လို့ပေးရပါမယ်။ နောက်ဆုံး `if_exists` ကတော့ `"fail"`, `"replace"` နဲ့ `"append"` သုံးမျိုး ဖြစ်နိုင်ပါတယ်။ `dtype` ဆိုတဲ့ အထဲမှာတော့ column name တွေက key, `sqlalchemy.types` အောက်က အောက်ပါ object တွေက value အဖြစ် ပါတဲ့ `dict` တခု ပေးရပါမယ်။ 
#
# **BigInteger**
#
# A type for bigger int integers.
#
# **Boolean**
#
# A bool datatype.
#
# **Date**
#
# A type for datetime.date() objects.
#
# **DateTime**
#
# A type for datetime.datetime() objects.
#
# **Enum**
#
# Generic Enum Type.
#
# **Float**
#
# Type representing floating point types, such as FLOAT or REAL.
#
# **Integer**
#
# A type for int integers.
#
# **Interval**
#
# A type for datetime.timedelta() objects.
#
# **LargeBinary**
#
# A type for large binary byte data.
#
# **MatchType**
#
# Refers to the return type of the MATCH operator.
#
# **Numeric**
#
# Base for non-integer numeric types, such as NUMERIC, FLOAT, DECIMAL, and other variants.
#
# **PickleType**
#
# Holds Python objects, which are serialized using pickle.
#
# **SchemaType**
#
# Mark a type as possibly requiring schema-level DDL for usage.
#
# **SmallInteger**
#
# A type for smaller int integers.
#
# **String**
#
# The base for all string and character types.
#
# **Text**
#
# A variably sized string type.
#
# **Time**
#
# A type for datetime.time() objects.
#
# **Unicode**
#
# A variable length Unicode string type.
#
# **UnicodeText**
#
# An unbounded-length Unicode string type.
#
# > be careful! sqlalchemy's type inference is **very very BAD**. It's a good practice *to always provide `dtype`*

engine = sa.create_engine("sqlite:///chinook.db")
with engine.connect() as con:
    df.to_sql("my_table", con=con, index=False, if_exists="fail")

# +
# try it again. it will error out since the table exists. 
# change if_exists to make it not error out.
# -

# ## `pandas` Data Types
#
# အရင်နေ့က `dtypes` ဆိုတဲ့ property ကို သုံးပြီး data type တွေကို ကြည့်နိုင်တာ စမ်းကြည့်ပြီးပြီ။ ဒီနေ့ `pandas` Data type တွေကို အသေးစိတ် လေ့လာကြမယ်။
#
# ```
# Pandas dtype	Python type     Usage
# object	        str or mixed	Text or mixed numeric and non-numeric values
# int<x>          int             Integer numbers 
#                                 
#                                 
# float<x>        float	        Floating point numbers
#                                 
# bool	        bool	        True/False values
# datetime64      datetime lib    Date and time values
# timedelta[ns]	datetime lib	Differences between two datetimes
# category        NA              Finite list of text values
# ```
#
# > int\<x\> means int8, int16, int32, int64 etc.
#
# > so is float\<x\>
#
# ### `astype` function
#
# ```python
# import numpy as np
# import pandas as pd
# df = pd.DataFrame({"my_column": ["1", "2", "3"]})
# print (df.my_column)
# my_column_as_int = df.my_column.astype(np.int)
# print (my_column_as_int.dtype)
# ```

# try it; convert other data types as well


# one need to define sub-types of CategoricalDType (well, I use "sub-type" loosely here)
cat_type = pd.api.types.CategoricalDtype(categories=["possible", "values", "here"], ordered=True)
df["cat_column"] = ["possible", "here", "here"]
df

# you will see the data type as object here
df.cat_column

df["cat_column"] = df.cat_column.astype(cat_type)
df.cat_column

# you will see an error here. why ?
df["another_cat_column"] = ["value", "here", "don't", "belong"]
df["another_cat_column"] = df.another_cat_column.astype(cat_type)

# ### date and time
#
# Problem အပေးဆုံး data type တွေကတော့ datetime ပါပဲ။ 

df["period_column"] = pd.period_range(start='2022-01-01', periods=3, freq='3 M') # default frequency is 'D' or '1 D'
df

# you will see very strange dtype 'period[3M]'
df.dtypes

# converting from period to datetime requires
# 1. conversion to str and
# 2. using pandas' to_datetime function and
# 3. identification of format string (optional)
df["dates_1"] = pd.to_datetime(df.period_column.astype(str), format='%Y-%m')
df

from datetime import datetime as dt
from datetime import timedelta as td
df["dates_2"] = dt.now().strftime("%Y-%m-%d")
df["dates_2"] = pd.to_datetime(df.dates_2)
df

# subtracting two datetime64 will give you timedelta64
df["dates_2"] - df["dates_1"]

# why so many multiplication of 1k ?
df["dates_1"] + np.timedelta64(24*3600 * 1000 * 1000 * 1000)

# ## Let's move on to Missing Data Handling
#
# missing data ဆိုတာ real-life မှာ ခဏခဏ တွေ့ရတယ်။ experiment တွေမှာ sample ပျက်သွားတာ၊ ပျောက်သွားတာ ဖြစ်တတ်တယ်။ web scraping လုပ်နေတုန်းမှာ internet ကျကွဲသွားတာတို့၊ တခြား error တချို့တို့ကြောင့် missing data ဖြစ်သွားတတ်သလို ... ဘယ်လိုပင် ကြိုးစား ထိန်းသိမ်းသော်လည်း အကြောင်းကြောင်းကြောင့် data ပျောက်နေတာတွေ ရှိတတ်တယ်။
#
# missing data ကို ဘာလုပ်ရမလဲဆိုတာ one-size-fits-all အဖြေ မရှိဘူး။ data ပေါ်မှာ မူတည်တယ်။ 
#
# * score data/exam marks data
# * weather data (temperature/pressure etc.)
# * gender data or survey data asking what people likes from a list of 3 dishes
#
# `pandas` မှာ missing data/value တွေကို ကိုင်တွယ်ဖို့ 
#
# * `dropna`
# * `fillna` နဲ့
# * `interpolate` ဆိုတဲ့ function ၃ ခုရှိတယ်။

df_with_md = pd.DataFrame({
    "score" : [10, 3, np.nan, 7, 9],
    "temp" : [42.0, 35.0, 22.8, np.nan, 30.8],
    "gender": ["M", "F", np.nan, "M", "F"], 
})
df_with_md

df_with_md.dropna(axis=0, inplace=False)

df_with_md.dropna(axis=1) # oh O!

df_with_md.fillna({"gender": "not_available", "score": 0})

df_with_md.temp.fillna(method="ffill")

df_with_md.temp.fillna(method="bfill")

df_with_md["temp"].interpolate()

# deleting columns
df_with_md.drop("temp", axis=1)

# deleting rows
df_with_md.drop([1, 3], axis=0)

# ## How to handle **BIG Data** ?
#
# Please do NOT give the following answers in your interviews:
#
# * I don't know. That's what Data Engineers are supposed to do.
# * Buy me larger machines with 128GB of RAM.
# * I leave it run overnight and pick up the results in the morning.
#
# Instead, (depending on the interviewer) give these answers (or all of them):
#
# * I'll try to reduce the Big O notation of the operation if possible
#   * Equal-joins tend to be easier to reduce using hashes
# * I'll sample the data based on its distribution
#   * Central Limit Theorem etc.
# * I'll divide and conquer using `pandas` `DataFrame` `groupby`.

engine = sa.create_engine("sqlite:///chinook.db")
with engine.connect() as con:
  df = pd.read_sql("SELECT * FROM INVOICES;", con=con)
  print (df.shape)

df.columns

groups = df.groupby("BillingCity")
for city, df_by_city in groups:
    print ("city : {}".format(city))
    print (df_by_city.shape)
    print (df_by_city.head(n=1))

# get max of all column by BillingCity
groups.max()

groups.apply(lambda x : x.select_dtypes(include=np.number).max() - x.select_dtypes(include=np.number).min())

df_stuttgart = groups.get_group("Stuttgart")
df_stuttgart

df_stuttgart.loc[11]

# compare this result with above's
df_stuttgart = df_stuttgart.set_index("InvoiceId", drop=True, inplace=False)
df_stuttgart

df_stuttgart.loc[12]

df_stuttgart.reset_index(drop=False, inplace=True)
df_stuttgart

# ## Lastly, the Joins

engine = sa.create_engine("sqlite:///chinook.db")
with engine.connect() as con:
  df_invoice_items = pd.read_sql("SELECT * FROM INVOICE_ITEMS;", con=con)
  print (df_invoice_items.shape)

df_invoice_items.head()

df.head()

df_invoice_items_with_invoice_data = pd.merge(left=df_invoice_items, right=df, on="InvoiceId", how="left")
df_invoice_items_with_invoice_data.head()

#

StarcoderdataJupyterScriptsDedupFiltered