PyInv

プログラミングのメモ、海外投資のメモ

Unsupervised Learning: PCA (主成分分析)の流れ

PCA (Principal Component Analysis) とは


データ全体から互いに独立な成分(主成分)を抜き出して、主成分によりもとのデータを表現すること。
分析の中では、データの次元を削減(圧縮)する手段としてもちいられる。また、説明変数の多重共線性の回避の手段として重要。(でも解釈は難しくなるよ)


まずは対象となる特徴量行列として表された各アイテム間の分散共分散行列をとり、その固有値固有ベクトルを求めることから始まる。


こう書くと大学1年の冬を思い出す。(以下読み飛ばし推奨)



大学入学当初は授業(証明しか解説しない。しかも所々省略する。)と試験(証明には全く触れず、計算問題のみ)のギャップにみごとにはまって落第寸前の日々を送っていた。長時間かけて一つ一つ証明を写経して理解し、自分で解説できるようになってもテストではボロボロ。。。友人に勉強を教えていたのに成績は断然自分のほうが低い。頭の悪さを呪いながら生きていた。

線形代数はそんな時代(1年の冬学期)に苦学して習得し、そしてすぐに証明も含めて忘れ去った分野である。

3年になって、遅ればせながら友人の一人から「授業なんてでないで、教務課でもらった過去問だけ解いてれば優は楽勝だ」と教えられて「え?過去問なんてあるんだ。しかも教務課でもらえるの?というか毎回似たような内容の問題をだすコマなんてあるの?」と目から鱗が落ちる思いだった。そういえば大学受験でも過去問を解いていなかったな。

友人たちは誰からそんなことを教えてもらっていたのか。小ヒルベリーエレジー現象である。
ダブルスクールにこもっていてサークルをおろそかにしていた自分にも原因はあるのだが。。
singapp.hatenablog.com

ちなみに、写経して証明を理解していくという手法は、社会人になってから他の数学・統計の分野を自学するにあたって大変役に立ってます。
GPAの低下という大いなる犠牲を払って学んだことは無駄ではなかった(´;ω;`) 
並行して、過去問を大事にするという学びもCPAとCFAを取得したときに助けてくれた。

余談の余談ですが、当時周りに数多くいた本当に頭の良い人たちは証明も実践も軽くこなしていました。(やっぱり自分の頭が良くないだけやん)

(回想終わり)



さて、本題にもどる
求めた固有ベクトルを使って、特徴量行列を分散共分散行列の固有値の数まで減少させることが主成分分析である。
分散共分散行列の各固有ベクトルの大きさがすなわち固有ベクトルで特徴量を表現する際の分散の大きさとなるので、大きいほうから順番にとっていけばできあがり。

Structure

sklearn.decomposition.PCA(n_components=None,
                          *,
                          copy=True,
                          whiten=False,
                          svd_solver='auto',
                          tol=0.0,
                          iterated_power='auto',
                          random_state=None


主なParameters

Parameters Description
n_components 主成分の数
iterated_power solverの反復回数

主なAttributes

Attributes Description
components_ 主成分を返す
explained_variance_ 各主成分の大きさ、つまり元の特徴量空間の分散
explained_variance_ratio_ 上記の分散のパーセンテージ
n_components 主成分の数

PCA を 1からやってみる

流れ

  • 準備: Sample Dataの作成
  • Sample Dataを可視化して傾向をみる
  • PCAの実行
  • 結果の確認


Sample Dataの作成

今日もUCI(いつもお世話になっております)からアワビを取り寄せ。

import requests
import io
import pandas as pd

url = 'http://archive.ics.uci.edu/ml/machine-learning-databases/abalone/abalone.data'
data = requests.get(url ,verify = False)
df = pd.read_csv(io.StringIO(data.content.decode('utf-8')), header=None)

#こちらからNameデータを
#http://archive.ics.uci.edu/ml/machine-learning-databases/abalone/abalone.names
column_names =['Sex', 'Length', 'Diameter', 'Height',
               'Whole weight', 'Shucked weight',
               'Viscera weight','Shell weight',
              'Rings']

df.columns=column_names

#Sexをget_dummiesでBooleanに変換
df=pd.get_dummies(df['Sex']).join(df).drop('Sex',axis=1)



Sample Dataの可視化

import seaborn as sns
import matplotlib.pyplot as plt
g= sns.pairplot(df)
g.fig.set_figheight(10)
g.fig.set_figwidth(10)
plt.show()

説明変数間で相関の高そうなものが散見される
f:id:singapp:20200927153938p:plain

PCAの実行

from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA

X = df.drop('Rings', axis=1)
y = df['Rings']

#説明変数の標準化
sc= StandardScaler()
X_std = sc.fit_transform(X)

pca = PCA()
pca.fit(X_std)

#主成分
X_pca = pca.transform(X_std)
pd.DataFrame(X_pca, columns=["PC{}".format(x + 1) for x in range(len(X_pca[1]))]).head()


f:id:singapp:20200927160419p:plain

結果の確認

Explained Variance Ratio (寄与率)


まずは分散の寄与率を確認

pc_df=pd.DataFrame(pca.explained_variance_ratio_, index=["PC{}".format(x + 1) for x in range(len(X_pca[1]))])
print(pc_df)

plt.bar([n for n in range(1, len(pca.explained_variance_ratio_)+1)], pca.explained_variance_ratio_)
plt.show()


f:id:singapp:20200928010312p:plain
f:id:singapp:20200928011338p:plain

Cumulative Explained Variance (累積寄与率)

次に累積寄与率を図示してみる

plt.plot(np.cumsum(pca.explained_variance_ratio_),marker=".")
plt.xlabel('Number of Components')
plt.ylabel('Cumulative Explained Variance')
plt.grid(True)
plt.show()


f:id:singapp:20200928011424p:plain
第2主成分までで85%に届いているので、ここまでの成分を使えば十分であることがわかる。実務上は累積寄与率80%程度で切り捨てられる。

Eigenvector(固有ベクトル

主成分の解釈を助ける手段として、固有ベクトルを各特徴量名に対応させる


固有ベクトル

pd.DataFrame(pca.components_, columns=df.columns[1:], index=["PC{}".format(x + 1) for x in range(len(X.columns))])


f:id:singapp:20200928014512p:plain

主成分の数を調整してPCAの再実行

結果の確認を受けて、第2主成分までを使う主成分分析をやり直す

pca = PCA(n_components=2)
pca.fit(X_std)

X_pca = pca.transform(X_std)
print(X_pca.shape)

f:id:singapp:20200928012553p:plain

プロット

採用した第1主成分と第2主成分における特徴量の寄与度をプロットすることで、各主成分の解釈を試みる

plt.figure(figsize=(7,7))
for x, y, name in zip(pca.components_[0], pca.components_[1], X.columns[1:]):
    plt.text(x, y, name)
    plt.scatter(pca.components_[0], pca.components_[1], alpha=0.8)
    plt.grid()
    plt.xlabel("PC1")
    plt.ylabel("PC2")
plt.show()


f:id:singapp:20200928015803p:plain

え、これ元のデータのうちほとんどの特徴が意味をなしてないじゃん。。。(ランクが低すぎる)

第1主成分はM、I(とLength)、F(とその他)という性別を評価した成分。
第2成分はLength、M(とその他)、Iを評価した成分。

どうやらアワビの特徴は性別でほとんど分類されてしまうらしい。あえて性別を除いて主成分分析をしてみたほうがよかったかも。

アルゴリズム思考術 問題解決の最強ツール



 読むべき時に読めた本。ちょうど最適化アルゴリズムを仕事で使う必要があったので、本書からのインサイトはすぐに役に立った。(自分で組んだわけではなく、納品されたものを検査する側)


 こういった本を読んでいて関心するのは、公私含めて普段なにげなく行っている行動の多く(例えば、本棚やメールの整理から配船計画まで)について大学院等の研究結果が公開されていることである。世間では当たり前のことなのだが、自分はほとんど意識していなかった。

 
 これに気が付いたとき、会社で働いているときのモヤモヤ感が一つ晴れた気がした。もっと研究結果を仕事に応用しよう。


 一個人に担える発明・発見の量は微々たるものか、もしくはすでに他のだれかが発見して使っているものでありうる。
 日本では事務系・技術系と区分された業務と思想が一般的である。一方、発見・発明の必要性は技術系に限らず事務系にも当てはまるのである。


 事務系ももっと外と交流して知見を広げることで会社の生産性をぐっと上げることができるのではないだろうか。なにも産学連携とは決して研究開発や技術系分野に限ったもんものではなく、もっと広義に捉えて活用すべきである。また、こういった機能をコンサルばかりに頼っていては会社にknowledgeを蓄積するノウハウが内なわれてしまう。


 今更こんなことを考えるとは、潤沢な投資をされた大学で教育を受けた身ながら大変恐縮である。



以下、本の内容のうち覚えておきたいものを抜粋


 本書では、代表的な事例に対するアルゴリズムが学術的にはどのように考えられているか、簡単なロジックとともに紹介されている。今後必要になったときに適宜文献を参照したい。大事なのは絶対的な解を見つける方法より少ない計算量による合理的な解見つける方法。

  • 最適停止問題
    • 最良の伴侶の見つけ方として巷では紹介されている。大事なのは何回「検討」する機会があるか。質が未知もしくは分布が均等ならば37%点以降に今までで最適なものを選ぶ

  • ソート
    • バブルソートは問題外。O(n)、O(n^2)という試行回数の基準をもちいて比較する。有効なのは比較数え上げ(分割して、分割された部分集合のなかでソートする)


  • ファイリング(キャッシュ)


  • 汎用スケジューリング
    • 各仕事の重要度を処理時間でわり、単位時間重要度で降順ソートする


  • スモールデータ問題(ベイズ
    • 既知の情報に対して乗法(べき分布:観測値になんらかの一定の値をかける)、平均(正規分布:そのうち平均になる)、加法(アーラン分布:残りは常に一定時間)のどれかを当てはめて未知の部分を予測する。メディアの取り上げは事前確率(既知の情報)を歪めるかも。


  • 離散最適化問題
    • 各種緩和法
      • 最小全域木(制約緩和)から始める。
      • 連続緩和により離散→連続に問題を置き換えて近似値を出す
      • ラグランジュ緩和(連続緩和)により制約条件をスコアに変換して考える。
    • ランダム性
      • 貪欲アルゴリズム(初期値をランダムに選択して、緩和された制約でラフに計画していく)で基本計画を作った後
      • +山登り法(局所的改善を繰り返す)
      • +ジッター(揺さぶってもう一度改善を始めさせる)
      • +メトロポリス(ランダムに再出発)
    • 焼きなまし法
      • 完全なランダムサンプリングで基本計画を作り、それに対応するエントロピーパラメータ(T)をおく。Tを徐々に減らしながら、またTに対応する大胆差で局所的改善をしていく by IBM。Machine LearningのGradient Boostingと同じ考え方。


  • アポを取るときはこちらから候補を絞ったほうが相手のメモリ消費を低減できる。



Exploratory Data Analysis - 探索的データ解析

Exploratory Data Analysisとは

データの理解をすること。ざっくりと、
データの型、分布の形状や性質、欠損値や異常値の確認
前処理を含む

  • inforによるデータ型、欠損値の確認
  • 上記で発見した不具合、不都合の処理
  • boxplotを使った異常値の確認
  • describeによる基本統計量の確認
  • pairplotで散布図に
  • 相関係数をheatmapで視覚化
  • 必要であればOne-Hot処理

実践編

データ準備

UCIからアワビのとりよせ
Ring(年輪)を予想するモデルを作る

import requests
import io
import pandas as pd

url = 'http://archive.ics.uci.edu/ml/machine-learning-databases/abalone/abalone.data'
data = requests.get(url ,verify = False)
df = pd.read_csv(io.StringIO(data.content.decode('utf-8')), header=None)

#こちらからNameデータを
#http://archive.ics.uci.edu/ml/machine-learning-databases/abalone/abalone.names
column_names =['Sex', 'Length', 'Diameter', 'Height',
               'Whole weight', 'Shucked weight',
               'Viscera weight','Shell weight',
              'Rings']

df.columns=column_names


データの観察

眺める
df.head()
df.tail()
df.info()

f:id:singapp:20200926004149p:plain
列はきれいに集められている模様。アワビのオス・メスってどうやってみわけるんだろう。


f:id:singapp:20200926004357p:plain
欠損値なし。データも数値として認識されている。


データの前処理

データの成型
  • 型を変更したり
    • to_numeric()
    • to_string(),

_ to_datetime()

  • データsplitしたり
  • テーブルを結合したり
    • pd.merge()
    • .join()

いろいろやってみる

欠損値処理

  • dropna()
  • fillna()


Box plotで分布の広がりと異常値を観察

import matplotlib as mpl
import matplotlib.pyplot as plt

#Ringsは目的変数なのでdrop
df.drop('Rings',axis=1).boxplot(figsize=(20,6))
plt.show()


f:id:singapp:20200926010712p:plain

Whole weightの広がりが大きい。異常値であると判断すれば処理。


実力不足により自分では処理できないので、Zepprixさまの記事を参考にさせていただきました。自分でも記事書きたいです。

qiita.com

基本統計量の確認

df.describe()


f:id:singapp:20200926011846p:plain

とてもお手軽。HeightとShuckedのminが気になるところ。


組み合わせ散布図 Pair Plot

import seaborn as sns
sns.pairplot(df)

f:id:singapp:20200926012300p:plain

これまたとてもお手軽。Ringsにたいして関係が深そうなものを更なる探索対象にする。
全体的にまとまりにかける。Heightは除いてもよさそう。

相関行列のHeat Map

cor = df.corr()
plt.figure(figsize=(10,6))
sns.heatmap(cor, square=True, cmap='plasma', vmax=1,vmin=-1, annot=True)
plt.show()


f:id:singapp:20200926013022p:plain
やはり全体的にRingに対する相関が低め。

その後

所与のデータでうまくいきそうなら、次にはモデルの選択とチューニングへ
ダメそうなら特徴量エンジニアリングで目的変数と説明変数の関係性が何か見いだせないか分析

特徴量エンジニアリング

Unsupervised-Larningによって特徴量を操作

  • 次元の削減
    • 次元が大きすぎると、Machine-Learningの必要時間が増える
  • 多重共線性の回避
    • 統計的なあれ

第三回:Ensemble Model - Bagging (Bootstrap Aggregating)

Baggingとは

Bootstrapping(サンプル集合から復元抽出によりsub-sample setを作成)により作成したsub-sample setそれぞれに対してモデルを作成し、モデルの結果を集約して予測すること。
結果の集約はClassifierの場合は多数決、Regressionの場合は平均値をとる。

Structure

sklearn.ensemble.BaggingClassifier(base_estimator=None,
                                   n_estimators=10,
                                   *,
                                   max_samples=1.0,
                                   max_features=1.0,
                                   bootstrap=True,
                                   bootstrap_features=False,
                                   oob_score=False,
                                   warm_start=False,
                                   n_jobs=None,
                                   random_state=None,
                                   verbose=0


主なParameters

Parameters Description
base_estimator 一つ一つのsub-sample setに用いるモデルを選択
好みがなければDecision Treeがおすすめ(それはつまりRandom Forest..)
n_estimators 何個のsub-setに対してモデルを作成するか
max_samples 元のsampleの何割を一つのsub-sampleとして抽出するか
割合が大きすぎると過学習しがち
n_features sub-sampleで説明変数を何種類使うか
過学習しているときには種類を減らす

Sample

estimatorとしてK-Neighborを使う

import pandas as pd
from sklearn.ensemble import BaggingClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_breast_cancer

cancer = load_breast_cancer()

X= pd.DataFrame(cancer.data, columns=cancer.feature_names)
y = pd.Series(cancer.target)

X_train, X_test, y_train, y_test = train_test_split(X, y,test_size=0.5 ,random_state = 0)

models = {'kNN': KNeighborsClassifier(),
         'bagging': BaggingClassifier(KNeighborsClassifier(),
                                      n_estimators=50, random_state=0)}

scores = {}
for model_name, model in models.items():
    model.fit(X_train,y_train)
    scores[(model_name, 'train_score')] = model.score(X_train,y_train)
    scores[(model_name, 'test_score')] = model.score(X_test,y_test)

pd.Series(scores).unstack()

K-Neighbor単体の場合とくらべて多少test_scoreが改善された
f:id:singapp:20200923233527p:plain

回帰モデルの評価

回帰モデルの評価

分類モデルでは分類結果に対しての評価に加えて、計算された確率に対しての評価を行う必要があった。

しかし、回帰モデルでは目的変数が数値であるために計算結果に対して直感的な指標でモデルを評価することができる

主なモデルは

  • 平均二乗誤差(MSE: Mean Squared Error )
  • 平均絶対誤差(MAE: Mean Absolute Error)
  • 中央値絶対誤差(MedAE: Median Absolute Error)
  • 決定係数(R2)

分類モデルの評価はこちら
singapp.hatenablog.com
singapp.hatenablog.com
singapp.hatenablog.com

平均二乗誤差(MSE: Mean Squared Error )

予測値と目的変数の差(残差)の2乗を足しあげたもの(SSE: Sum of squared errors)をサンプル数でわったもの
線形回帰分析でおなじみ

sklearn.metrics.mean_squared_error(y_true,
                                   y_pred,
                                   *,
                                   sample_weight=None,
                                   multioutput='uniform_average',
                                   squared=True)
crross_validateの引数
  • neg_mean_squared_error


平均絶対誤差(MAE: Mean Absolute Error)

残差の絶対値を足しあげて、サンプル数で割ったもの。2乗されていないのでMSEと比べて外れ値の影響を受けにくい

sklearn.metrics.mean_absolute_error(y_true,
                                    y_pred,
                                    *,
                                    sample_weight=None,
                                    multioutput='uniform_average')

crross_validateの引数
  • neg_mean_absolute_error


中央値絶対誤差(MedAE: Median Absolute Error)

残差の絶対値の中央値。MAEよりのさらに外れ値に対して強い

sklearn.metrics.median_absolute_error(y_true,
                                      y_pred,
                                      *,
                                      multioutput='uniform_average')

crross_validateの引数
  • neg_median_absolute_error


決定係数(R2)

検証データの平均値で予測をした場合の残差平方和(SST: Sum of Squared Total)と残差平方和SSEの比率
平均値での予測をベースにモデルでどれだけ2乗誤差を削れたかを示す指標

sklearn.metrics.r2_score(y_true,
                         y_pred,
                         *,
                         sample_weight=None,
                         multioutput='uniform_average')

crross_validateの引数
  • r2

実践

データの準備

from sklearn.datasets import load_boston
boston = load_boston()
X = pd.DataFrame(boston.data, columns=boston.feature_names)
y = pd.Series(boston.target, name='MEDV')

Sample

特徴として

  • Cross ValidationとStandard Scalingを同時に行うためにpiplelineを使う
  • Cross Validationではk-foldに代わってRepeated random subsamplingを行う
    • データの並びに偏りがあるため、単純なk-foldでは一部のスコアが極端に悪い
  • 複数の評価値を同時に計算するためにcross_val_scoreに代わってcross_validateを使う
    • 終値は各検証結果の平均値にする
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.model_selection import cross_validate
from sklearn.model_selection import RepeatedKFold
from sklearn.linear_model import LinearRegression, Ridge
from sklearn.tree import DecisionTreeRegressor


#検証したいモデルをdict形式に格納
models = {
    'LinearRgression': LinearRegression(),
    'Ridge': Ridge(random_state=0,max_iter=1200000),
    'DecisionTreeRegressor': DecisionTreeRegressor(random_state=0),
    'LinearSVR': LinearSVR(random_state=0,max_iter=2400000)
}

#評価指標の引数をリスト化
scoring = ('neg_mean_squared_error', 'neg_mean_absolute_error', 'neg_median_absolute_error','r2')

#PipeLineに入れるScalerオブジェクトを作成
scaler= StandardScaler()

#cross_validateに入れるRepeated  Random subsumplingのオブジェクトを作成
cv=RepeatedKFold(n_splits=5, n_repeats=5)

scores = {}

for model_name, model in models.items():
    pipeline = Pipeline([('transformer', scaler), ('estimator', model)])
    scores[model_name] = pd.DataFrame(cross_validate(pipeline, X, y, cv = cv,scoring=scoring)).mean()

scores = pd.DataFrame(scores).unstack().unstack()

f:id:singapp:20200923015413p:plain

Hot Encoder

Hot Encoderとは

特徴量(行列X、多分類のときにはyも)をOne Hot Vectorつまり1つの成分が1で残りの成分が0となるような列ベクトルの集合に変換する機能(classだったり、methodだったり)
pythonとそのLibraryではいろいろ用意されている。

  • OneHotEncoder
  • get_dummies
  • label_binirize


列ベクトルにて表現されている特徴量がカテゴリーの時(数値でない場合)に、特徴を0 or 1で列ごとに1つずつの特徴を表すように変換。そうしないと意味のない数値差(例えばasciiコードの数値差等)が学習データとして採用されてしまう。

カテゴリーの数だけ列を増やすので計算負荷がかかる。

Dataの準備

配管工のおじさんが大好きなキノコのデータを例として準備する
(画像を貼りたいが危険なのでガマン)

Column Nameの格納の仕方が汚かったために思わぬところで前処理の実践をすることに。。。

#UCIのデータベースからキノコデータをダウンロード
url='https://archive.ics.uci.edu/ml/machine-learning-databases/mushroom/agaricus-lepiota.data'
data = requests.get(url, verify=False)
df = pd.read_csv(io.StringIO(data.content.decode('utf-8')),header=None)

#Column Nameを取得
url2='https://archive.ics.uci.edu/ml/machine-learning-databases/mushroom/agaricus-lepiota.names'
name_data = requests.get(url2, verify=False)
name_data= pd.read_csv(io.StringIO(name_data.content.decode('utf-8')),sep='\n',engine='python',error_bad_lines=False)
name_data = name_data.iloc[79:112,0].map(lambda x: str(x).strip())
name_data = name_data[name_data.map(lambda x: x[0].isnumeric())==True]
name_data = name_data.map(lambda x: x.split(':')[0].split('.')[1].strip())
name_data[0] = 'classes'
name_data=name_data.sort_index().reset_index(drop=True)

#データに付加
df.columns=name_data


OneHotEncoder


Class
注意点として、nanをうまく扱えないので事前に処理が必要
変数をいい具合に全体から判断して勝手に詰めてくれる

Structure
sklearn.preprocessing.OneHotEncoder(*,
                                   categories='auto',
                                   drop=None,
                                   sparse=True,
                                   dtype=<class 'numpy.float64'>,
                                   handle_unknown='error')


主なParameters

Parameters Description
categories defaultはautoで列の中からカテゴリを自動で作ってくれる。自分で指定もできる。
sparse True: array, メモリを食う
False: Sparse matrix(疎行列)

Sample
from sklearn.preprocessing import OneHotEncoder
henc= OneHotEncoder(sparse=False)
hotMushroom=henc.fit_transform(df[['gill-color', 'gill-attachment', 'odor', 'cap-color']])
print(hotMushroom) 

f:id:singapp:20200922005550p:plain

get_dummies

nanを勝手に処理してくれる

Structure
pandas.get_dummies(data,
                   prefix=None,
                   prefix_sep='_',
                   dummy_na=False,
                   columns=None,
                   sparse=False,
                   drop_first=False,
                   dtype=None)


主なParameters

Parameters Description
data One-Hotにしたいデータを入れる
統計ではdummy
dummy_na nanをdummyデータとして処理するか
sparse True: array, メモリを食う
False: Sparse matrix(疎行列)
drop_first カテゴリー数がk個あった時、k-1個のダミー変数を作成する
統計の自由度の観点から

Sample
import pandas as pd
hotMushroom=pd.get_dummies(df[['gill-color', 'gill-attachment', 'odor', 'cap-color']],sparse=False)
hotMushroom.head()


f:id:singapp:20200922012454p:plain

label_binarize

Label(y)用
1列に含まれる多種のLabelを複数列に置き換える。

Structure
sklearn.preprocessing.label_binarize(y,
                                     *,
                                     classes,
                                     neg_label=0,
                                     pos_label=1,
                                     sparse_output=False)


主なParameters

Parameters Description
y array
次のclass指定を考慮すると現実的に1列なので目的変数が対象かな
classes カテゴリーを指定
対象列に対してuniqueの結果を流用しできる
sparse_output True: array, メモリを食う
False: Sparse matrix(疎行列)


Sample
from sklearn.preprocessing import label_binarize
hotMushroom = label_binarize(df['classes'],classes=df['classes'].unique(),sparse_output=False)
hotMushroom


f:id:singapp:20200922014548p:plain